Skip to content

Add session working memory (facts / open_questions / decisions) with audit-gated anti-poisoning#20

Merged
fxspeiser merged 1 commit into
mainfrom
feature/session-memory
May 26, 2026
Merged

Add session working memory (facts / open_questions / decisions) with audit-gated anti-poisoning#20
fxspeiser merged 1 commit into
mainfrom
feature/session-memory

Conversation

@fxspeiser
Copy link
Copy Markdown
Owner

Summary

Per-session ledger of `{facts, open_questions, decisions}` that tools auto-populate from synthesis output and that other calls can opt into as carry-forward context. The key safety property: audit-gated anti-poisoning — a failed `tool_audit` automatically marks every session memory row stale, so contradicted facts can't propagate forward.

  • New `session_memory` table (CREATE IF NOT EXISTS, additive)
  • Helpers: `_session_memory_add / _list / _mark_stale / _clear / _block / _inject`
  • Tool: `session_memory(action: list | add | mark_stale | clear, session_id, ...)`
  • Auto-write from `coordinate.synthesis_structured`: consensus → decision, key_claims → fact, dissent / open_questions → open_question
  • Audit gate: `tool_audit` with `passed: false` marks every non-stale memory row stale (both single-judge and coalesced paths). Surfaced as `session_memory_marked_stale` on the response and a `session_memory_stale` event
  • Opt-in injection: `inject_session_memory: true` on `confer` / `coordinate` prepends the non-stale memory block to the user message / topic

Error taxonomy: `SESSION_MEMORY_MISSING_SESSION_ID`, `SESSION_MEMORY_BAD_ACTION`, `SESSION_MEMORY_BAD_KIND`, `SESSION_MEMORY_EMPTY_CONTENT`, `SESSION_MEMORY_INVALID`.

Test plan

  • New `scripts/test_session_memory.py` — CRUD, stale filtering, injection, error taxonomy, coordinate auto-write, audit-gated stale-marking, post-stale empty block
  • Full suite (32 scripts) passes locally

🤖 Generated with Claude Code

Per-session ledger that tools (`coordinate`) auto-populate from their
synthesis output and that other calls can opt into injecting as
carry-forward context. Anti-poisoning is enforced by gating injection
on audit pass/fail: when `tool_audit` returns passed=false, every
non-stale memory row for that session is marked stale and excluded
from future injection.

Schema:
- New `session_memory(id, session_id, kind, content, source_tool,
  source_call_id, confidence, created_at, stale_at, stale_reason)`
  table with kind in {fact, open_question, decision}, plus indexes
  on session_id and kind. CREATE IF NOT EXISTS — additive to the
  existing schema.

Helpers:
- `_session_memory_add` (truncates content to 2000 chars, rejects
  unknown kinds, ensures the session row exists first)
- `_session_memory_list` (filters by kinds + include_stale + limit)
- `_session_memory_mark_stale` (by id, by kind, or all-of-session)
- `_session_memory_clear`
- `_session_memory_block` renders a compact `<session_memory>` text
  block (decisions / facts / open_questions) capped at 4000 chars
- `_session_memory_inject` prepends the block to the first user msg

Tool:
- `session_memory(action, session_id, ...)` with action in
  {list, add, mark_stale, clear}. Error taxonomy:
  SESSION_MEMORY_MISSING_SESSION_ID, SESSION_MEMORY_BAD_ACTION,
  SESSION_MEMORY_BAD_KIND, SESSION_MEMORY_EMPTY_CONTENT,
  SESSION_MEMORY_INVALID.

Auto-write from `coordinate.synthesis_structured`:
- consensus               -> kind=decision (carries weighted_confidence)
- key_claims[].claim      -> kind=fact     (carries per-claim confidence)
- dissent[].claim         -> kind=open_question prefixed `DISSENT:`
- open_questions[]        -> kind=open_question
Best-effort; never blocks the coordinate response.

Audit gate (anti-poisoning):
- Both single-judge and coalesced `tool_audit` paths: when passed=false,
  mark every non-stale memory row for the session stale with
  reason=`audit_failed:overall=<score>`. Marker count surfaced as
  `session_memory_marked_stale` on the audit response and as a
  `session_memory_stale` ndjson event.

Injection (opt-in):
- `confer` and `coordinate` accept `inject_session_memory: true`. When
  set with a session_id, the memory block is prepended to the user
  message (confer) or topic block (coordinate). Stale rows are excluded.

Tests (scripts/test_session_memory.py):
- CRUD helpers (add / list / mark_stale by id and kind / clear)
- Stale rows excluded from default listings and from the injection block
- `_session_memory_inject` prepends a `<session_memory>` wrapper to first
  user message; system message untouched; no-op when no user message
- `tool_session_memory` error taxonomy + happy paths for all four actions
- coordinate auto-writes consensus / key_claims / dissent / open_questions
  with content matching the synthesis schema
- inject_session_memory:true on coordinate prepends the block to the
  proposer + critic + synthesizer prompts
- Failed `tool_audit` marks ALL session memory stale; subsequent
  `_session_memory_block` returns empty

Full suite (32 scripts) passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@fxspeiser fxspeiser merged commit 5677395 into main May 26, 2026
1 check passed
@fxspeiser fxspeiser deleted the feature/session-memory branch May 26, 2026 12:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant