Agent proposal queue — backend — one gate, one audit trail, RBAC-scoped
A firm-scoped backend that aggregates the work Lawrence executes — within a matter, across many matters, or independent of any matter — behind a single review gate, attributes every piece of it to its execution thread, and shows each viewer only what their access allows.
This page is a review surface — select any text to leave a comment; reactions and a sign-off / approval are supported (at the bottom of the page). Proposed options are recommendations, open to challenge.
Overview
When Lawrence does work today, the output goes straight to wherever that artifact lives — a drafted document lands in matter documents, a task in the task list, a note in notes — each by its own path, with no common place to review it and, for most types, no review at all. There is nothing that says "here is everything Lawrence has done for this firm, waiting for a human to check it."
Today that's a latent gap — when a lawyer triggers work and watches it unfold in a thread, they at least see it happen. It becomes a real problem the moment agents start executing work in the background: with no human watching a thread live, there is no surface at all to catch, review, or audit what the agent produced. A firm-wide review surface is what turns background and autonomous agent work from invisible into reviewable — which is exactly what makes the autonomous authority modes (and auto-apply, §2) safe to switch on.
The whole plan turns on one idea: a single persisted Proposal record that every piece of agent output becomes. That one record does three jobs at once — it is the review gate, it is the audit trail (it carries the execution thread), and it is the list the queue reads (filtered by the viewer's access).
flowchart LR
subgraph runs["Lawrence executes work (within a firm)"]
direction TB
M1["on one matter"]
M2["across many matters"]
M3["independent of a matter"]
end
runs --> P[("Proposal store
one record per produced artifact/edit")]
P --> Q["Queue: list proposals the
viewer's RBAC allows"]
Q --> R{Human reviews}
R -->|approve| AP["Apply to the artifact's home"]
R -->|reject| X["Discard"]
classDef d fill:#eef1f6,stroke:#9aa3b2,color:#1f2430;
classDef hot fill:#fff,stroke:#B5491F,color:#1f2430,stroke-width:2px;
class M1,M2,M3,Q,R,AP,X d; class P hot;
The three pieces of the project map onto that record: attribution (§3) is the thread it carries, the gate (§2) is the deferral of its home write, and access control (§4) is how the queue filters it.
Solution shape
Four blocks. The recurring theme: today's behaviour is split and mostly ungated; we replace it with one record and one gate, reusing the per-type apply paths that already exist.
| Block | Delivers | Reality today | The new edge |
|---|---|---|---|
| §1 Proposal record | One persisted record representing a proposed artifact/edit of any type, with a status lifecycle. | No unified record exists; the closest analogues are matter-service's ReviewRequest (a human staff-approval workflow — see §1) and a client-only, browser-local "pending operations" array for note/email edits. | A server-side Proposal model + lifecycle. |
| §2 Gate & deferred apply | All agent output becomes a pending Proposal; the write to the live artifact is deferred until a human approves (CRDT types still store an excluded draft in the home — see §2). | Creates + document/form edits write directly to their home (no gate); note/email text edits are client-only pending ops. | Intercept writes into Proposals; replay through the existing per-protocol apply paths on approval. |
| §3 Attribution & audit | Every proposal (and the applied artifact) is traceable to the thread that produced it. | threadId is dropped at the VFS write boundary; produced records carry no thread reference and there is no audit log. | Carry threadId through the write boundary; the Proposal is the audit join. |
| §4 Access control | Each viewer sees only the executed work their RBAC allows — in a matter, across matters, or firm-level. | Matter access is per-matter and not author-scoped; a compiled scope where exists for matter lists. | A firm-scoped, RBAC-compiled query over the Proposal store for the three cases. |
1 · The Proposal record Backend
Everything hangs off one new record. It has to represent a proposed change of any artifact type without executing it, hold everything needed to apply it later, and carry the provenance the queue and audit need.
There's a shape worth borrowing — ReviewRequest. matter-service already has one persisted "submit something for approval" workflow, unrelated to the agent: a lawyer submits a completed fee agreement or a matter reassignment, and Lawhive staff (admin) approve or reject it. It's stored as a row with a PENDING → APPROVED/REJECTED lifecycle, a pointer to the affected record, and a JSON detail blob, and it fires a notification on submit. That's a good structural precedent — a Proposal is essentially a review row whose applied effect is deferred — but ReviewRequest is human-only and touches no agent work today, so we're introducing a new model rather than overloading it.
erDiagram
AIThread ||--o{ Proposal : "produced in"
Proposal {
string id "prop_"
string firmId "always firm-scoped"
string matterId "nullable = matter-less"
string threadId "the execution thread"
string proposerIdentityId "on-behalf-of user"
string actorIdentityId "Lawrence"
string protocol "documents notes emails tasks keydates messages artifacts"
string action "create edit delete"
string targetPath "null for create"
json payload "deferred write: create data or edit patch or update"
string status "pending approved rejected applied failed"
string reviewPolicy "REQUIRES_REVIEW or AUTO_APPLY"
string reviewedByIdentityId "nullable"
string appliedResourceId "nullable, set on apply"
}
The payload is the deferred write, captured in whatever shape that type's apply path already consumes — the create data dict, an edit patch, or a document update. status is a superset of the two lifecycles that exist today (the client pending-op states and the ReviewRequest states).
stateDiagram-v2 [*] --> pending: agent produces work pending --> approved: human approves pending --> rejected: human rejects pending --> applied: AUTO_APPLY (no review, trusted config) approved --> applied: replayed to the home approved --> failed: apply error (retryable) rejected --> [*] applied --> [*]
Why one record and not per-type gating. The queue has to aggregate across types and matters; a single record makes that a query rather than a fan-in across many stores. It also gives attribution (§3) and access (§4) a single home. See KD1 for where it lives.
2 · Gating & deferred apply Backend Legal AI
The problem to fix. Agent output takes two completely different paths today, and most of it has no review at all: creates (document, note, email, task, key-date, message) and document/form edits are server-fulfilled — written straight to their home service the moment the tool runs — while only note/email text edits become client-only pending operations that a human sees (that's today's pre-migration state — notes/emails join the CRDT family as they move into content-service). There is no single place a proposal is gated.
flowchart TB
subgraph now["Today — split, mostly ungated"]
A1["agent create/edit"] --> S1["server-fulfilled → writes home immediately"]
A1 --> C1["note/email text edit → client-only pending op (localStorage)"]
end
subgraph next["Proposed — one gate"]
A2["agent create/edit"] --> G["Proposal (pending) — home write deferred"]
G --> RV{approved?}
RV -->|yes| DISP["dispatch by protocol to the existing apply path"]
RV -->|no| DROP["discard / mark rejected"]
end
now --> next
classDef d fill:#eef1f6,stroke:#9aa3b2,color:#1f2430;
classDef n fill:#fff,stroke:#5a8a64,color:#1f2430,stroke-width:2px;
classDef bad fill:#fff,stroke:#a83838,color:#1f2430;
class A1,S1,C1,A2,RV,DROP d; class S1,C1 bad; class G,DISP n;
Capture, don't execute — then replay on approve
The gate has two halves, and the second one already exists.
Proposal rather than writing the live artifact. For the compose-at-read types the home service isn't touched yet; for CRDT content the proposal is stored in the home as a draft excluded from live reads (KD8). Either way the live artifact is untouched. This is the one genuinely new interception — it sits where the protocol→adapter dispatch already happens for writes.pending until a human approves or rejects it in the queue. Nothing reaches the live artifact before that (a CRDT draft may sit in the home store, but excluded from every live read).What actually reaches the home service, and when. Compose-at-read types (tasks, key-dates, messages, filled-form fields) don't touch the home until approval. CRDT content (documents; notes then emails as they migrate) is written to the home now, as a draft excluded from every live read. Every type writes or promotes to the live artifact on approval. And auto-apply (KD7) writes to the live artifact immediately. The gate's invariant is about the live artifact — not whether the home service is storing a draft.
Be precise about what's CRDT-hard — it's rendering, not staleness. The genuinely CRDT-specific difficulty is materialising the proposed state: a Yjs edit is an operation against a base, so showing (or applying) it needs the Yjs runtime, which lives in content-service — you can't cheaply reconstruct it elsewhere. That's the whole reason CRDT content leans draft-in-home. The "base moved while the proposal waited" problem is not CRDT-specific — nothing freezes an artefact in the queue, so a proposed rename, reschedule or field edit can equally land on a base that changed. That staleness is a cross-cutting apply-time concern for every type (OQ1); if anything it favours compose-at-read (which re-composes against the current base on every read) over a stored draft (a frozen snapshot that must rebase on approve). The structured types (tasks, key-dates, messages, filled-form fields) both render and reconcile cheaply.
The existing client-side pending-operations layer becomes a thin view over server Proposals rather than the source of truth — so note/email review keeps working, now persisted and cross-device.
Previewing a proposal that isn't in the home store
If the proposed change hasn't been written to the artifact's home yet, how does the reviewer see it? Because the proposed state is persisted — in the Proposal's payload — and the review surface composes the preview at read time rather than the home holding it. For declarative artifacts (tasks, key-dates, messages, filled-form fields) this is cheap:
- Creates — the payload is the content, so the review surface renders the proposed artifact straight from the Proposal (there is no home row yet to query).
- Edits — preview = the base artifact (read from the home) with the payload applied for display only. This is exactly what today's in-memory pending-operations preview already does: it applies the operation to a working copy and renders it, without ever writing to the home.
Why not draft-in-home for every type? It's tempting to write a draft of everything into its home and reuse the native viewer uniformly. The catch: a drafted row must then be made inert across every downstream path — not just excluded from DB reads, but kept out of Inngest events, CCO/RAG indexing, counts, notifications and search — or it becomes a phantom artifact acting in production before anyone approved it. Compose-at-read sidesteps all of that by construction: for structured types nothing exists in the home until approval, so nothing downstream can see it, and rendering from the payload is cheap. So we pay the draft-in-home tax only where it actually buys something — CRDT content, where re-rendering outside content-service is infeasible and the home already owns the machinery — and use compose-at-read everywhere else. (Doing it uniformly anyway is KD8's alternative, gated on building that global "proposed = inert" guarantee — OQ9.)
Rich, collaborative (CRDT) content is the exception — and that set is growing. Reconstructing a Yjs document's proposed state outside content-service is awkward — a rendering problem (it needs the Yjs runtime), distinct from the universal stale-base concern (OQ1) — so the pragmatic option is to persist the proposal as a pending/draft version inside content-service that live queries exclude, letting the native viewer render it; on approve it's promoted to live, on reject discarded. This is not a document quirk: notes are migrating to content-service now, and emails after, so documents / notes / emails converge on one shared content-service pending-version mechanism. Compose-at-read then narrows to the genuinely structured records (tasks, key-dates, messages, filled-form fields). So the split is by content model, not artifact name (KD8) — and as more types migrate in, the draft-in-home bucket grows. Rather than reconstruct that per type, the clean long-term move is to make the pending version a native content-service capability the whole CRDT family shares — see KD9.
In-thread vs the queue — one store, two surfaces
"When a user is working in a thread and the agent proposes work, does that go to the queue too?" — yes, but that's a statement about the backend, not the UI. Keep two things apart: where a proposal lives (always the Proposal store — one uniform path, no special case, which is exactly what keeps audit and access consistent) and where it's reviewed (contextual). They are two views over the same record, which is a big reason to keep the single unified record (KD2) — the thread and the queue are the same data, not two pipelines.
- In an active thread, the thread renders its own pending proposals inline — reviewable in place, one click, no context switch. It's just the query "proposals where
threadId= this thread andstatus= pending", and it reuses the in-thread inline review that already exists (the pending-ops + review bar), now backed by the Proposal record. - The queue is the aggregate view for everything you're not watching — work produced while you were away, by background / autonomous runs, on other matters, or by teammates. Same records, filtered by RBAC.
So "it goes to the queue but is shown to the user without switching" is the model: the proposal is created pending and surfaced inline. No separate "queued vs in-review" state is needed — pending already means awaiting review, wherever the reviewer happens to be.
One fork to decide (OQ7): do in-thread proposals still require an explicit (one-click, inline) approval before they touch the home — a uniform gate, best for audit — or should tightly-interactive edits apply live in the thread and only fall back to the queue when unattended? Recommendation: uniform gate with frictionless inline approval, so the UI isn't annoying without opening a second, ungated write path.
Auto-apply — through the queue, without a review gate
In the long run some work should happen completely autonomously — written without a human approving each change. The clean way to support that without losing audit or attribution is to keep it on the same rails: the work still becomes a Proposal (so it is attributed to its thread, recorded, and visible in the queue after the fact), it just carries a reviewPolicy of AUTO_APPLY that skips the human gate — the dispatcher applies it to the home immediately and marks it applied, with no pending dwell and no reviewer.
reviewPolicy = AUTO_APPLY, the dispatcher replays the payload to the home at once and records applied — distinguishable in the queue from human-approved work.The guardrail: the decision to auto-apply is never the agent's to make — it is set by trusted configuration on the capability / surface (mirroring the identity plan's rule that authorization comes from context, not model output). Because no human sees it before it lands, auto-applied work should stay easily reversible and monitored — the queue is its after-the-fact review. This pairs naturally with the autonomous authority modes (UC3 / UC4) in the identity plan, and is a forward-looking capability: modelled now, built when a real autonomous use case lands (KD7, OQ8).
3 · Thread attribution & audit Backend Legal AI
Requirement. For each execution thread, show all the proposal artifacts and edits it generated, and keep that link for auditing.
Where it breaks today. The only thread→work link is a JSON blob (tool-invocations-complete) buried inside chat message parts — the threadId is dropped at the VFS write boundary (the write carries only path + data), so produced records store who (the acting identity) but never which thread, and there is no audit log or agent source anywhere.
flowchart LR TH["AIThread aithrd_"] --> P1["Proposal (threadId set)"] TH --> P2["Proposal (threadId set)"] P1 --> AR1["applied artifact
(stamped source=AGENT + threadId)"] P2 --> AR2["applied artifact
(stamped source=AGENT + threadId)"] Q["List work by thread X"] --> TH classDef d fill:#eef1f6,stroke:#9aa3b2,color:#1f2430; classDef n fill:#fff,stroke:#5a8a64,color:#1f2430,stroke-width:2px; class TH,AR1,AR2,Q d; class P1,P2 n;
Because every proposal is created with its threadId, "list everything thread X produced" becomes a single query over the Proposal store — no JSON-scraping. Two supporting moves make the audit trail durable:
- Carry
threadIdthrough the write boundary. The VFS request schemas, router, and adapters currently pass onlypath+data; addthreadId(and the originating message / tool-call id) so the Proposal — and the applied record — can record it. - Stamp provenance on the applied artifact. On apply, tag the home record with
source = AGENTand thethreadIdso provenance survives even outside the queue (there is no agentsourcevalue today). See KD4.
Attribution reuses the identity split from the identity plan: the Proposal records both the actor (Lawrence) and the on-behalf-of proposer, so an audit answers both "what did Lawrence do" and "for whom".
4 · Access control & the three scopes Backend
Requirement. The queue is always within a firm, and aggregates work executed (1) within one matter, (2) across many matters, (3) independent of any matter — showing each viewer only what their RBAC allows.
The three cases map onto the platform's existing access primitives. The rule to follow (and the documented anti-pattern to avoid): compile access into the query rather than resolving permissions one matter at a time.
| Case | Proposal filter | Access gate |
|---|---|---|
| (1) Within one matter | matterId = X | authoriseWith("can_view", { matterId, identityId }) — the standard matter gate. |
| (2) Across many matters | matterId IN (accessible matters) | Compile a scope where: participant + team matters for a regular viewer; the whole firm for an admin holding can_view_all_matters / can_list_matters. Do not fan the per-matter cascade. |
| (3) Independent of a matter | matterId IS NULL, firmId = F | Gate the procedure with a firm grant (resolved off FirmMember). |
Can a viewer see other people's agent work?
Under today's model, yes — and for free. Matter visibility is purely matter-access-based, with no author dimension: if you and a colleague can both view a matter, each of you already sees all work on it regardless of who triggered it. So "the queue lets me review a teammate's agent work on a shared matter" is the natural default.
The opposite — making a proposal private to its requester until shared — is the thing that costs extra: it is a net-new requestedBy/owner filter that exists nowhere in the current RBAC model. Whether we want that is KD5 / OQ4 — my recommendation is to ship the matter-access default and add author-privacy only if a real need appears.
5 · Key design decisions
Genuine forks only. Proposed = recommendation, open to challenge.
| # | Decision | Proposed | Alternatives | Why |
|---|---|---|---|---|
| KD1 | Where the Proposal store lives | lawrence-api — it already sits at the agent write boundary and owns the threads (the attribution join), resolving matter RBAC via matter-service | A new proposal-service; put it in matter-service | Proposals are agent-centric and span multiple homes; co-locating with the write boundary + threads keeps capture and attribution local, and RBAC is a cross-service query either way. A new service is cleaner-in-theory but heavier. |
| KD2 | One unified record vs per-type gating | One Proposal record for all types | Add a review state to each artifact type separately | The queue must aggregate across types/matters and attribute uniformly; one record makes that a query and gives attribution + access a single home. Per-type gating re-scatters exactly what we're trying to unify. |
| KD3 | Deferred apply | Capture payload, replay on approve through the existing per-protocol apply paths | Apply immediately then allow rollback; snapshot-and-revert | Deferral is the only model that truly gates (nothing reaches the live artifact unapproved) and it reuses the apply code that exists. Apply-then-rollback leaks unreviewed writes and needs per-type undo. |
| KD4 | Attribution mechanism | The Proposal row is the audit record; also stamp source=AGENT + threadId on the applied artifact | A separate agent-action audit-log table; a threadId column on every produced record only | The Proposal already holds thread + actor + payload, so it is the natural audit join; stamping the applied record keeps provenance outside the queue too. A separate log duplicates state. |
| KD5 | Visibility model | Matter-access-based (see co-accessors' agent work) — matches the platform | Author-private (only the requester sees a proposal until shared) | Matter-access is free and consistent with every other matter read. Author-privacy is net-new (a requestedBy filter) and only worth it if the product needs private drafts. |
| KD6 | What goes through the gate | All agent-produced content artifacts & edits (creates + edits across types) | Only the types that are already client-reviewed (notes/emails); gate everything including state changes | The requirement is one surface all proposals pass through; gating only some re-creates the split. State-only changes (e.g. assignee) may not need review — scoped at OQ5. |
| KD7 | Autonomous auto-apply | A per-proposal reviewPolicy: default REQUIRES_REVIEW; AUTO_APPLY writes through the queue without gating | A separate ungated write path that bypasses the queue entirely | Autonomous work still becomes a Proposal — attributed, audited, visible — it just skips the human gate. Keeping it in the one pipe preserves audit; a bypass path loses it. Note the rule vs record split: reviewPolicy on the row is the resolved record (immutable audit + what the apply path reads), while the eligibility rule that sets it is separate trusted config — a static in-code matrix (as grants are here), promoted to a stored per-firm table only if admins must toggle it at runtime (OQ8). Never the agent's request. |
| KD8 | Previewing a deferred proposal | Split by content model: content-service-backed CRDT content (documents — and notes, then emails, as they migrate) → one shared pending/draft version; structured records (tasks, key-dates, messages, filled-form fields) → compose-at-read | Uniform draft-in-home for all types + delete-on-reject; or uniform compose-at-read (infeasible for CRDT content) | The rich-text artifacts converge on content-service, so build the pending-version once and documents/notes/emails all reuse it. Compose-at-read then covers only the structured records — cheapest and safest (nothing exists until approved). Extending draft-in-home to the structured records too is on the table, but only worth it if we build the global "proposed = inert" guarantee it needs (OQ9): a drafted row must be invisible to events, CCO/RAG, counts and notifications, not just DB reads. |
| KD9 | Unify CRDT proposals in content-service | Yes — a native "proposed version" in content-service (a branch off the live doc; rebase-on-approve; discard-on-reject) that documents / notes / emails all share, built as its own scoped workstream | Reimplement CRDT preview/deferral per type inside the queue; ad-hoc "reject if the base moved" | The CRDT runtime and base state live in content-service, so implement deferral where the machinery is — once, for the whole family (and future CRDT types), and it's how CRDT content reconciles a moved base — rebase-on-approve (the general, all-types version of that problem is OQ1). The queue defines the "deferred-change" contract; content-service implements it natively for its content. Caveat: a real content-service investment and a cross-team dependency — on the critical path for reviewing rich-content edits but not for the queue overall, so sequence it with the notes/emails migration rather than ahead of it. |
6 · Open questions
- OQ1 — Stale base at apply time (all proposals, not just CRDT). Nothing freezes an artefact while its proposal sits in the queue, so the base can move between propose and approve — for any type. Each needs an apply-time policy: declarative types → an optimistic-concurrency check (compare a version /
updatedAt; last-writer-wins, or re-diff / re-confirm if it moved); CRDT content → a merge / rebase, best owned by content-service's proposed-version (KD9). Same problem class, different machinery. - OQ2 — Approval granularity. Is approval per-proposal, or can a thread's proposals be approved/rejected as a batch? Can a single proposal be partially accepted (e.g. some fields of a document)?
- OQ3 — Notifications & freshness. How are reviewers told work is waiting, and do stale unreviewed proposals expire (and if so, is the thread's context still meaningful later)?
- OQ4 — Author privacy (your open question). Do we ever want a proposal private to its requester until shared? Default is matter-access-based visibility (KD5); author-privacy is net-new.
- OQ5 — Do state changes gate too? Content artifacts clearly go through the queue. Do non-content state changes (task assignee, mark-for-review, delete) also become proposals, or do those apply directly and only appear in the audit view?
- OQ6 — Cross-matter / firm-level gate. Which firm grant gates the cross-matter and matter-less views — reuse
can_view_all_matters/can_list_matters, or a new agent-work grant? - OQ7 — In-thread review vs live apply. Do interactive in-thread proposals apply only on inline approval (a uniform gate, best for audit), or apply live with the queue as a fallback when the run is unattended? This decides whether there is ever an ungated write path.
- OQ8 — What governs auto-apply eligibility? Which capabilities / surfaces (and which authority modes — e.g. autonomous UC3/UC4 runs) may be configured
AUTO_APPLY, who configures it, and what reversibility / monitoring is required given no human sees it before it lands? And is eligibility a static in-code policy (keyed by capability / protocol / authority mode — matching how grants are matrices, not tables) or a stored per-firm config table (needed only if admins toggle it at runtime)? - OQ9 — Draft-in-home downstream inertness. If we draft proposed artifacts into their home stores (KD8), what must treat a proposed/draft row as non-existent until approved — DB reads (the soft-delete interceptor is precedent), but also Inngest events, CCO/RAG indexing, counts, notifications, search, cross-service reads? Is one global suppression layer sufficient, or must each downstream path be audited?
7 · Rollout
- Sequence. (1) Proposal store + lifecycle; (2) the structured records (tasks, key-dates, messages, filled-form fields) via compose-at-read — they defer cleanly, lowest risk; (3) re-back today's client note/email pending-ops with server Proposals (still patch-based, pre-migration) so existing review keeps working, now persisted; (4) CRDT content — documents now, notes/emails as they migrate — via the content-service proposed-version (KD9): the hard part, sequenced with the migration; (5) thread attribution + provenance stamping; (6) the RBAC-scoped queries for the three scopes.
- No backfill. Proposals are new records for new agent work; nothing to migrate except moving client pending-ops to server-backed.
- Flag it. Gate the whole surface behind a flag so the direct-write behaviour remains until the queue is trusted; per-type capture can be enabled incrementally.
- Fail loudly. An apply failure moves the proposal to
failed(retryable) and surfaces — never a silent drop, and never a partial home write. - Access is enforced server-side. The queue query compiles the viewer's scope into the DB filter; the UI never receives proposals the viewer can't see.
8 · Execution & review plan
Atomic PRs, per-responsibility stacks (≤7). Sizes are review estimates, not measured.
Stack A — Proposal store & lifecycle
| PR | Contents |
|---|---|
| A1 | Proposal model + migration (KD1 location, KD2 unified) with the status lifecycle. |
| A2 | Proposal CRUD + approve/reject transitions; validation that a payload matches its protocol/action. |
Stack B — Gate & deferred apply
| PR | Contents |
|---|---|
| B1 | Intercept agent writes at the protocol dispatch: create a pending Proposal instead of writing home (KD3). |
| B2 | Apply-on-approve dispatcher: replay the payload through the existing per-protocol adapter apply paths. |
| B3 | CRDT deferral for documents (and notes/emails as they migrate) per the OQ1 / KD9 resolution; make the client pending-ops layer a view over server Proposals. (Filled-form fields are structured — they ship with the compose-at-read types, not here.) |
Stack C — Attribution & audit
| PR | Contents |
|---|---|
| C1 | Thread threadId through the VFS request schemas → router → adapters so it reaches the Proposal. |
| C2 | Stamp source=AGENT + threadId on the applied artifact (KD4); "list work by thread" query. |
Stack D — Access-scoped queries
| PR | Contents |
|---|---|
| D1 | In-matter list gated by can_view; cross-matter list via a compiled participant+team / firm-scope where (KD5 visibility). |
| D2 | Matter-less (firm-level) list gated by a firm grant (OQ6); the aggregate firm-scoped queue query. |
Glossary
| Term | Meaning |
|---|---|
| Proposal | The new persisted record representing one agent-produced artifact/edit, gated for review, carrying its thread and payload. The linchpin of this plan. |
| Gate / deferred apply | Capturing an agent write as a pending Proposal without executing it, then replaying it to the artifact's home only on approval. |
| Home | Where an artifact type actually lives (matter documents in content-service, tasks/notes/key-dates in matter-service, etc.). Today the agent writes there directly. |
| Execution thread | The Lawrence chat thread (aithrd_, mode AGENT) a piece of work was produced in — the unit of attribution. |
| Pending operation | Today's client-only, browser-local preview of a note/email edit. Becomes a view over server Proposals. |
| Protocol / VFS path | How the agent addresses artifacts (documents://matter/id, tasks://…). The protocol is what the apply dispatcher keys on. |
| Scope (1/2/3) | Within one matter / across many matters / independent of a matter — the three ways the queue aggregates, each with its own access gate. |
| ReviewRequest | An existing matter-service workflow, unrelated to the agent: a lawyer submits something (a completed fee agreement, or a matter reassignment) for Lawhive staff to approve/reject, as a persisted row with a status lifecycle. Cited here only as a shape precedent for the new Proposal record. |
| Lawrence | Lawhive's AI agent — the paralegal that does legal work on a matter. The thing whose output this queue reviews. |
| VFS | The agent's "virtual file system": the write layer where it addresses every artifact by a protocol://matter/id path. It's the single boundary all agent writes pass through — where we intercept them into Proposals. |
| CRDT / Yjs | The collaborative-editing representation used by content-service documents (and notes/emails as they migrate): content is a stream of mergeable update bytes (Yjs is the library), materialised through a runtime and defined relative to a moving base — which is why deferring / previewing it is the hard case. |
| CCO / RAG | Downstream consumers of matter content: the Case Context Object (the cached, structured understanding of a matter) and retrieval / search indexing. A drafted, not-yet-approved artifact must not leak into either. |
| FirmMember / firm grant | The row that makes an identity part of a firm (with a role), and the capabilities that role confers (e.g. can_view_all_matters). How firm-level and admin access are resolved. |
| Inngest | The platform's async event / workflow system. "Inngest events" fire on domain changes (e.g. a document being added); a proposed artifact must not fire them until approved. |