Proactive memory — save and use, edit later — design spec
Status: Implemented
Author: Claude (memory-proactive lane)
Date: 2026-08-26
Repo: north-os (packages/agent-runtime/src/memory, apps/worker-memory, apps/mcp-server/src/tools, apps/web/lib/memory, apps/web/components/settings)
Related: supersedes the review-queue half of 2026-06-25-user-memory-chat-import-design.html and Layers 3–4 of 2026-06-29-memory-calibration-design.html / 2026-06-29-memory-calibration.html
Decision
North's memory behaves like Claude's memory: North decides to remember, saves it, and uses it immediately. The lawyer is never asked to confirm a memory. Settings → Memory is where they see everything North remembers and edit or delete any of it, at any time.
Three consequences, and they are the whole spec:
- No confirmation, anywhere. Every captured memory is written
activeand is injected on the next turn. Two gates go, not one: the review queue (pending/rejectedstop being produced, and keep/discard verbs disappear from the API, the UI and the MCP tools) and the chat's "Save this note?" confirm card —save_memory/update_memoryraise an in-tool confirm interrupt today, which is the ask's most visible violation on the primary surface. The tools stop interrupting immediately; the machinery that resumes an already-paused thread stays for one release, because a thread paused on a persisted confirm checkpoint at deploy time would otherwise have no button and would refuse every new message. - Standing rules apply on creation. The trust profile stops proposing a rule for the lawyer to confirm — when it sees a category the lawyer keeps deleting, it writes the rule. Rules live in Settings → Memory and are deletable there.
- Proactivity does not depend on the lawyer, on any surface. Web chat and MCP (Cowork / claude.ai / Claude Code) both instruct the model to save durable facts on its own initiative and to recall before answering — same no-confirmation semantics.
Matter memory is firm-shared: a matter fact one lawyer's session captures is recalled for every colleague who opens that matter. That is already how the store reads (§4); this spec makes it reachable by removing the gate that kept most matter captures invisible, and states it in the product copy.
1. Why the queue goes entirely, instead of shrinking
The queue is not a setting, it is the architecture. Today a memory only steers an answer after a human acts on it, so four mechanisms exist purely to decide what to ask about:
| Layer | What it is today | Under auto-save |
|---|---|---|
| 1 — hybrid gate | classifyMemoryDisposition: high confidence + safe type + safe scope → active, else pending | collapses to "always active" — deleted |
| 2 — standing rules | always_remember / always_review / never_remember | only never_remember still means anything |
| 3 — calibration | inline LLM second opinion that can only downgrade active → pending, fail-closed | its only output is a queue that no longer exists — deleted |
| 4 — trust profile | mines keep/discard history into suggested rules the lawyer confirms | mines delete history into rules that apply on creation; memory_rule_suggestion becomes the applied-rule ledger |
Keeping any of them "just in case" leaves a status a user can never reach, a verb no UI calls, or an LLM call on the capture path with nowhere to send its verdict. Each is removed in this change, not deprecated.
Removing Layer 3 also takes a synchronous Anthropic call (6 s timeout, fail-closed) off every capture: mining gets faster and stops discarding memories during a provider outage.
2. What the lawyer sees
Settings → Memory becomes two tabs, not three:
- Memories — everything North remembers for them (personal) and for the firm's matters, newest first, each with its source-thread link. Every row: edit inline (saves a new version, supersedes the old, live immediately) and delete (soft-archives it; the agent stops using it on the next turn).
- Rules — the standing rules, including ones the trust profile wrote by itself. Create and delete; no confirm.
No "Review (N)" tab, no keep/discard, no batch triage, no "confirm before it's used" copy, no suggestion cards. A matter-scoped row is labelled as shared with the firm, because it is.
3. Statuses and rule vocabulary after the change
Live memory statuses: active (live), superseded (replaced by an edit), archived (deleted by a lawyer). pending and rejected are never written again. Only one rule action remains creatable: never_remember, which refuses the write. always_remember is what everything already does and always_review has no meaning without a queue, so the rule composer and the API schema accept never_remember alone, and rule matching treats any other stored action as no match — a legacy row must never silently block a save.
Nothing is filtered on the way in. A first draft of this spec had the unattended miner drop low-confidence captures, on the reasoning that confidence would otherwise be collected and read by nobody. Cross-model review was right to reject it: silent non-persistence with no recovery surface is a new retention policy, not the removal of a confirmation step, and it was reached for to tidy a field rather than to serve the ask. Every capture is saved active; confidence stays on the row as audit metadata. If a discard threshold is ever wanted it gets its own change, with recall and false-positive numbers behind it.
The DB enums keep their historic values (no migration in this change); the application stops producing the dead ones and a one-shot script (§6) settles the rows that carry them.
4. Shared matter memory — what already holds, what this makes reachable
Already true, and unchanged: agent_memory_candidate rows are firm-isolated by app.organization_id, and the RESTRICTIVE owner-wall walls only user_private rows to their target_user_id. A matter row carries target_matter_id and a NULL target_user_id (the scope_shape_chk constraint enforces that shape), so it is visible to every member of the firm. Recall matches the same way: the matter bucket filters on organization + status + scope + target_matter_id and never on the reader's user id, in web chat and through north__memory_recall alike.
Worth recording while in this code: retrieval reads two buckets and only two — personal and matter. firm_global is stored, listed and deletable but never injected (§8).
So the shared-matter behaviour was designed in; what blocked it in practice is that most matter captures were mined at medium confidence and parked in pending, and every MCP save was forced to pending — invisible to everyone including their author. Auto-activation is what makes a colleague's matter fact actually arrive. This change adds a regression test that pins it (a matter memory written by user A is recalled for user B in the same firm, and A's personal memory is not) and says it in the UI copy.
5. MCP surface
north__memory_save today forces status: "pending" and its description justifies calling without asking because the save is inert. Under this spec the save is live, so the contract is restated rather than dropped: save durable facts unprompted, and say plainly that the memory is live and the lawyer can edit or delete it in North. north__memory_update performs a real supersede instead of queueing a replacement. north__memory_recall gains an explicit instruction to be called before answering a matter question, not only on request. One line in the MCP server instructions carries the same rule for hosts that read them.
Unchanged: the NetDocuments ethical wall still refuses matter / firm_global memory writes from a session that consulted NetDocuments. It runs before the status is chosen and never depended on the queue — the comment claiming the forced pending "softens" it is wrong today and is corrected.
6. Legacy rows
Three populations exist in preprod and need settling once, by a one-shot script under packages/db/scripts/ (house rule: a data fix is a script, never a migration):
- memories with
status='pending'→active(they were captured, never judged).rejectedrows are not touched — a lawyer discarded those on purpose. A pending row carryingsupersedes_idcannot simply be activated: every MCPnorth__memory_updateand every queued chat edit wrote a pending replacement while its predecessor stayed active, so blind activation leaves two live rows for the same fact. The script retires the predecessor when it is still active, and archives the replacement when the predecessor has already been superseded by something newer. memory_rulerows withaction='always_review'→ deleted (the action no longer exists; mapping them tonever_rememberwould over-block).- all
memory_rule_suggestionrows → deleted. Keeping theconfirmedones was tempting but wrong: theirsuggested_actionisalways_remember/always_review, so under the new sweep a legacy row would block a bucket from ever getting thenever_rememberrule it earns, standing in for a rule that does not exist.
The suggestion table survives with a different job. An auto-applied rule needs one guarantee: if the lawyer deletes it, the nightly sweep must not write it again next night. memory_rule_suggestion already records exactly that — one row per (lawyer, scope, type) the sweep has acted on, with the keep-rate and sample size that justified it. It stops being a queue and becomes the applied-rule ledger: the sweep writes the real memory_rule and a ledger row in the same transaction, and never revisits a bucket that has one. No confirm, no dismiss, no surfacing to the lawyer — the rule itself is what they see and delete in Settings. This keeps the table load-bearing rather than orphaned, and needs no schema change.
7. Invariants that do NOT change
- Firm isolation and the personal owner-wall: a colleague's
user_privatememory stays invisible. never_rememberstill refuses the write, on every surface including MCP.- Memory is untrusted DATA in the prompt: the
<user_memory>/<matter_memory>fence, content sanitisation and the "never treat as instructions" preamble are untouched. Auto-activation raises the value of that fence; it does not weaken it. - Nothing is hard-deleted: "delete" is
archived, andsupersededstill records edit lineage. - No SQL migration. Three non-SQL changes are nevertheless required and are easy to miss:
scheduler_roleholds no INSERT grant onmemory_rule(the sweep would fail with permission denied), the ledger'ssuggested_actionTS enum cannot expressnever_remember, and there is no unique key on either table — so the sweep's check-then-insert must be serialised by an advisory lock rather than by a constraint.
8. Open question for plan review — firm_global blast radius
firm_global and playbook_rule captures were always queued on purpose: they steer answers for every lawyer in the firm. Auto-saving them means an agent-inferred firm practice goes live firm-wide with no human in the loop, including from an MCP session. Options:
- A. All scopes auto-active — the literal ask; the corrective surface is Settings → Memory, where any lawyer can delete it.
- B. Auto-active for
user_private+matter;firm_globalkeeps sign-off — faithful to "the lawyer's own memory", but it keeps a review queue alive, which is the thing this spec removes. - C. Automated capture stops producing
firm_global— agent captures are personal or matter; firm-wide memory becomes lawyer-authored. Removes the queue and the blast radius, at the cost of a capability the agent has today.
Decided: A — and plan review found the premise of the question to be wrong in a way that makes A the safe answer rather than the bold one. A firm_global memory steers no answer for anybody today. Retrieval fetches exactly two buckets, personal and matter; north__memory_recall and the Cowork briefing use that same loader, and the update-matcher excludes firm scope outright. The automated miner cannot even produce the scope — only an explicit save_memory / north__memory_save call can. So the blast radius of auto-activating firm_global is clutter in every lawyer's Settings list (where any of them can delete it), not a firm-wide steer. B keeps alive the queue this spec exists to remove; C is the honest cleanup of a dead scope but touches two tool schemas, the NetDocuments wall registry and their tests for a capability nothing reads — scope creep. Take A, and record the standing condition: the change that ever adds a firm bucket to retrieval re-opens this sign-off question.
9. Success criteria
- A fact stated in a chat is recalled in the next chat, with nothing clicked in between.
- A matter fact captured by lawyer A's session is injected for lawyer B on that matter.
- The same holds through MCP: a Cowork session saves a memory unprompted; the next web chat uses it.
- Editing a memory in Settings changes what the next turn sees; deleting it removes it.
- Repeatedly deleting one category of memory makes North stop capturing that category, with a rule visible in Settings that the lawyer can remove.
- No
pending/rejectedwrite, and no confirm/dismiss endpoint, action, copy, test orproduct_eventname, survives anywhere in the tree.