The lq-ai contract & upstream loop
Donna implements no legal-AI logic — it consumes a contract. This is what happens when the contract isn't enough: file an ask, relay it, bump the pin.
Decision: lq-ai backend pin
Donna vendors LegalQuants/lq-ai at vendor/lq-ai as a git submodule.
- Pinned SHA:
c4d4482(bumped 2026-06-07 from0097b01) - Why: the UX/behavior reference docs and the build target must track the same backend version. Bump deliberately (one PR per bump), regenerating API types.
Bump log
0097b01→c4d4482(2026-06-07): lq-ai #138 + #139 (Donna asks #8 + #9, migration head → 0047) —- #138
338579e— document-grade run artifacts (asklq-ai-autonomous-run-artifacts.md, shape (a)): artifacts persist as real Documents in the run'starget_kb_id(doc-panel / download / RAG for free; v1 markdown-only,mimepinnedtext/markdownserver-side). New owner-gated paginatedGET /sessions/{id}/artifacts→AutonomousArtifactListResponse(AutonomousArtifactRead:id, name, mime, size_bytes, file_id?, document_id?, created_at;document_idread-time-enriched → drives "Open",file_id→/files/{id}/contentDownload; both SET-NULL on file hard-delete, metadata survives). Emission is opt-in per automation:emit_artifacts(default false) on ScheduleCreate/Update/Read, WatchCreate/Update/Read, andAutonomousManualRunRequest— REQUIRED (non-optional) in the create/run-now bodies, so Donna call sites must pass it. Notification payload now always carriesartifact_countnext tofinding_count. Ordering iscreated_at ASC, id ASC(transaction-stable, NOT emission sequence; same tiebreaker retrofitted to findings). Honest fallbacks arrive as ordinary findings (opted-in-but-no-KB → one info finding; storage failure → one warn finding per artifact). Session delete CASCADEs only the references — the KB documents outlive the session. Loop/echo prevention closed upstream (artifacts don't re-trigger watches or next-tick analysis).npm run gen:api→ +183-line additive diff. Unblocks the artifacts slice (ships with this bump): Documents block in RunResults + opt-in toggles + inbox copy. - #139
c4d4482— arq-worker skill registry init (asklq-ai-autonomous-skill-registry-init.md, PR #70): registry bootstrap extracted toapp/skills/bootstrap.py::install_skill_registry, called from BOTH the FastAPI lifespan and the arq workeron_startup; worker fails loudly at startup if the skills dir can't load. Upstream corrections to our ask: (1) never a regression — worker-sideskill_refresolution never worked on any image (our 06-05 "completed" tick was afirst_tick_no_baselinebaseline tick that skips inference); (2) the fix alone wouldn't work in containers — vendordocker-compose.ymlnow mounts./skills:/skills:ro+ setsLQ_AI_SKILLS_DIRonarq-worker(mount is REQUIRED; without it the worker exits at startup by design). The API also now fails at startup on a missing/unreadable skills dir (was warn+empty). Donna verifies by liveskill_refrun (no Donna code).
- #138
fc832ca→0097b01(2026-06-05): lq-ai #135 (Donna asklq-ai-autonomous-run-output.md) — run findings persisted + readable: newautonomous_findingstable (cascade-delete with the session) + paginated, owner-gatedGET /sessions/{id}/findings(limit clamped [1,200],created_atASC = emission order;severityfree-text — intendedinfo|warn|critical), plus?source_session_id=onGET /memory("memories this run proposed"). Precedents deliberately NOT session-filterable (recurrence-aggregated) — deferred upstream.npm run gen:api→ additive diff (+99 lines: typedAutonomousFindingRead/AutonomousFindingListResponseschemas + new/sessions/{id}/findingspath +source_session_idquery param on/memory). Unblocks the run-output-surfacing slice (this bump ships with it): the "Results" section on/automations/[id].35c8bb6→fc832ca(2026-06-05): lq-ai #133 —project_idadded toAutonomousScheduleUpdateANDAutonomousWatchUpdate, so a schedule's/watch's matter is reassignable via PATCH (value → reassign · explicitnull→ unassign · omit → unchanged). Caller-owns-the-project now validated (404{"detail": "project not found"}, id-probing-safe via_load_owned_project) on POST/schedules, POST/watches, POST/run-now, and both PATCHes.npm run gen:api→ ~28-line diff (new 404 responses on POST/schedules//run-now, updated PATCH 404 descriptions, and the two Update-schemaproject_idfields). Unblocks the editable-matter slice (this bump ships with it): editableMatterPickerinScheduleForm/WatchFormedit mode + 404→"matter not found" mapping.541bd6f→35c8bb6(2026-06-04, recorded retroactively — bumps shipped mid-slice-F in PR #60, in two steps):541bd6f→69a0d35: lq-ai #129 —max_cost_usdOpenAPI schema-drift fix (AutonomousScheduleCreate/Update/Readnow correctly typed); also includes lq-ai #128 = the BYOK provider-keys backend (/api/v1/admin/provider-keysCRUD), making the Donna BYOK frontend buildable in-pin. (Donna commita66f982)69a0d35→35c8bb6: lq-ai #130 — all autonomous session/schedule/watch cost fields (max_cost_usd,cost_total_usd) uniformly typedstringon the wire to match runtime; Donna's defensivenum()parser already accepted string. (Donna commit0ea7f9c)
c22360a→541bd6f(2026-06-03): lq-ai #127 (Donna ask #6) — per-columnensemble_verificationfor tabular.ColumnSpecgainsensemble_verification?: boolean | null(true → routes that column's cells through Stage 4 of the Citation Engine cascade). The cost-preview response gainsensemble_cells_count?+ensemble_premium_usd?(judge-call premium folded intoestimated_cost_usd). Each tabular cell result + its citations now carryverification_method(string|null:ensemble_strict/ensemble_majority; null when the column isn't ensemble-verified or support wasn't confirmed) — described in the loosely-typedresultsprose (like thesource_*fields, DE-330), so hand-typed inparseTabularResults.npm run gen:api→ +28-line additive diff (ColumnSpec field + cost-preview fields + prose);npm run check0/0. Unblocks P6-C.1 (per-column ensemble toggle) and closes P6-B.1 (surfaceverification_methodon tabular cell citations instead of the doc-panel "Unverified" chip). (Prior pinc22360a= lq-ai #125, P6-B navigable tabular cell citations — its bump-log entry was not recorded here.)badf83d→945ad31(2026-06-01): lq-ai #120 (Donna ask P1.4) — exposes nullabledeletion_scheduled_aton theUserobject returned byGET /users/me(+login/refresh). Read-only echo of the existing column (non-null while a grace-period deletion is pending, null otherwise); no migration; caller-scoped (no cross-user leak);test_openapistays 114.npm run gen:apiproduced a +5-line additive diff (the field on theUserschema only);npm run check0/0. Unblocks the P7-2 follow-up: replace the always-visible "Cancel scheduled deletion" link on/settings/datawith a conditional "Pending deletion — cancel by<date>" banner gated ondata.user.deletion_scheduled_at(this bump ships with that banner). Closes the last of the four Donna backend asks (P1.1–P1.4 all landed).438198c→badf83d(2026-06-01): consolidated bump landing all three Donna backend asks (relay docs/upstream-requests/lq-ai-backend-asks-for-donna.md), plus the wholev0.3.1→v0.4.0upstream range:- #115 (DE-328, ask P1.1) — gateway skill assembler now appends unreferenced
bound
skill_inputsas a labelled context block, so inputs reach the model for non-templated skills (every built-in), not just{{placeholder}}-templated bodies. Unblocks Donna's deferred composer skill-input form. - #116/#117 (ask P1.2) —
MessageCreate.file_ids?: string[](Part A) forwarded to the gateway aslq_ai_file_idsand echoed asapplied_file_ids, plus file content reaching the model verbatim (Part B). Unblocks per-message chat file attachment. - #118 (ask P1.3) —
PATCH /api/v1/users/mewith a newUserProfileUpdateschema (display_name edit; email edit deferred → DE-329, #119). Unblocks Settings profile edit. - #119 — files DE-329 (email-edit follow-up) + marks DE-328 resolved (docs).
Contract delta is almost entirely additive: the asks above + the v0.4.0 autonomous
workflows surface (
/api/v1/autonomous/*— sessions, memory, precedents, schedules, watches, notifications, run-now). The only removal across the whole range was a reworded preferences-schema comment (no schema removal). Donna consumes none of the new surface yet, sonpm run checkis 0/0 against the regenerated contract. Full local stack rebuilt to v0.4.0 (api + gateway + ingest-worker + arq-worker), applying the new autonomous-table DB migrations. Verified: check 0/0; full unit suite green; live verified. Newly buildable Donna slices: composer skill-input form (P1.1), chat file-attach (P1.2), Settings profile-edit (P1.3). The autonomous-workflows API is now available to consume (see docs/roadmap/donna-future-roadmap.md). (Note: this supersedes an intermediate staged bump to396e19ffor #115 alone that was never shipped standalone.)
- #115 (DE-328, ask P1.1) — gateway skill assembler now appends unreferenced
bound
7c7ce14→438198c(2026-05-25): lq-ai #105 documents the/v1/modelsalias fields to match the live gateway — addslq_ai_resolves_to/lq_ai_fallback_counttoModelEntryand corrects therouted_inference_tierdescription (it's present on aliases too, as the primary-resolution tier). Docs-only on the backend (no behavior change; the rich model-config capability is untouched).npm run gen:apinow emits both fields on the/api/v1/models200 schema, so Donna's P2c-B1 picker drops its hand-typedRawModelEntryextension and derives the type from the generated contract. Verified:npm run check0/0, model unit + live e2e green. See docs/upstream-requests/lq-ai-models-undocumented-alias-fields.md.4df3b9b→7c7ce14(2026-05-25): lq-ai #103 fixes the gateway so streamed completions persist theirinference_routing_logrow (the success-path write was after the SSE[DONE], so connection teardown cancelled it). Without it, the P2c Receipts drawer + anonymization indicator were blank for UI (streamed) chats. Verified live: a streamed turn now yields one inference receipt.npm run gen:apiproduced no type diff. See docs/upstream-requests/lq-ai-streaming-inference-routing-log.md.8b8e549→4df3b9b(2026-05-24): lq-ai #102 surfacesanonymization_appliedandmessage_idin the receiptsinference/errorevent detail — the data source for Donna's P2c anonymization indicator (the indicator was deferred until this landed; see docs/upstream-requests/lq-ai-expose-anonymization-in-receipts.md).npm run gen:apiproduced no type diff (the receiptsdetailisadditionalProperties: true), but was re-run to confirm.
Compose bundling mechanism (resolves plan open-question #3)
Donna's docker-compose.yml uses top-level include: [vendor/lq-ai/docker-compose.yml]
and adds its own frontend as service donna-web.
- Why
donna-web, not overridingweb: Compose v2include:does NOT allow overriding an imported service name (it errorsservices.web conflicts with imported resource). So lq-ai'swebstays in the merged spec under its own name. - Avoiding lq-ai's
web: bring up an explicit service list that omits it:docker compose up -d --build postgres redis minio gateway api donna-web. Its host port is also parked off 3000 viaWEB_HOST_PORTas a backstop. - Runs alongside a separate lq-ai stack: project name is
donna(own network); all host ports are shifted via.env(see.env.example). - donna-web healthcheck uses Node's global
fetchagainst/login(thenode:alpineimage has no reliablewget/curl).
First-run admin (for login / e2e)
On first boot the api mints admin@lq.ai with a random password (logged at WARNING,
must_change_password=true). For a login-ready fixture:
docker compose exec api python -m app.cli reset-admin-password \
--email admin@lq.ai --password '<pw>' --no-force-change
P2a streaming — gateway alias (resolved)
No gateway config change was needed for P2a. The seeded gateway.yaml (from
gateway.yaml.example) already maps the smart alias → anthropic-prod /
claude-opus-4-7, and anthropic-prod reads ANTHROPIC_API_KEY (set in the
gitignored .env; the compose gateway service passes it through). Recreate
the gateway container after setting the key (docker compose up -d --force-recreate gateway) so it's in the container env. Streaming verified
end-to-end against Claude Opus 4.7 at Tier 4.
Known follow-ups
- Upstream lq-ai
backend-openapi.yamluses backticks in plain YAML scalars, which breaks the codegen parser; scripts/sanitize-openapi.js works around it atnpm run gen:apitime. Worth an upstream fix (quote those scalars). - Donna's refresh-cookie TTL is 8h (
REFRESH_TTL_SECONDS) while lq-ai's refresh token default is 7d (JWT_REFRESH_TOKEN_TTL_SECONDS=604800) — users re-auth sooner than necessary. Consider aligning when chat/session UX lands in P2. - Reliability (P2 follow-up, from the final review):
hooks.server.tstreats any non-200/403 from/users/me(e.g. a 5xx when the api is briefly down) as logged-out, andauth.logincollapses a 500 into "invalid credentials". Both should distinguish "auth invalid" from "backend unavailable" (surface a 503) rather than silently logging users out / mislabeling outages.