Skip to content

feat(memory): a coding turn from two weeks ago can still be found, after the conversation forgot it - #504

Merged
brcampidelli merged 1 commit into
mainfrom
feat/a-turn-from-two-weeks-ago-can-still-be-found
Sep 17, 2026
Merged

brcampidelli merged 1 commit into
mainfrom
feat/a-turn-from-two-weeks-ago-can-still-be-found

Conversation

@brcampidelli

Copy link
Copy Markdown
Owner

What

A coding turn from two weeks ago can still be found after the conversation itself forgot it: every finished Code-screen turn joins an append-only SQLite/FTS5 history index, and a recall_history tool searches it. Item 5 of the list audited on 2026-09-16 ("SQLite long-term memory — remembers the login function from two weeks ago").

Why

A code session keeps the model's own message list and trims it at a user boundary (DEFAULT_MAX_MESSAGES = 200, about thirty tool-using turns). That is the right bound for a transcript re-sent to a provider every turn, and the wrong bound for "what did we decide about the login function two weeks ago?" — the turn that decided it is the first one trimming drops. The memory store is the wrong place to look: a memory is a fact the agent chose to keep, not a record of what was asked. Before this PR there was no SQLite index of conversation history at all; the marketing sentence described a store that did not exist.

How

  • chimera/memory/history.pyHistoryIndex at <home>/history.db: one row per completed turn (turn_id, session_id, project, asked_at, tainted, asked, answered, files, edited, tools). FTS5 virtual table, LIKE fallback when FTS5 is not compiled in (the degradation sqlite_store.py already documents), the same tokenizer and function-word list the memory store uses, scoped by the same project_key. Terms are OR-ed and ranked (bm25), quoted so login_user reaches login_user. Written through redact. record replaces on the same turn_id; forget_session/forget_sessions; one index per home per process (history_for, like jobs_for), one connection under a lock because the turn's thread writes and the agent's thread reads.
  • chimera/tools/history.pyRecallHistoryTool (query, days, k ≤ 10, everywhere). Dated excerpts centred on the first matching term (one excerpt rule for both search paths), edited: and read: told apart, the project named only when the search left it. A tainted turn carries [this turn read untrusted content — weigh its answer accordingly] in-line, the way a tainted memory is labelled on recall. Says "nothing recorded yet" vs "no match" vs "no match in the last N days" distinctly.
  • Wiringdefault_registry mounts the tool (so it passes restrict_registry and the governance wrappers like everything else); PARALLEL_READ_TOOLS gains it; _verify_and_finish in code_api.py records the turn after the transcript is saved, reading the files off the same fold the replay endpoint shows (exchanges_from_messagesfiles_of_exchange), with edited from the turn's on_edit and tainted from the receipt. Both the native and the external-provider (ACP) paths go through it. A record that will not write is logged and the turn goes on.
  • DeletionDELETE /api/code/sessions/{id} and DELETE /api/code/projects forget the rows (ids collected from list_meta before the files go, because the index is keyed by session id). The screen says the conversation is gone; an index that still answered about it would make that a lie.
  • Ten tools.desc.recall_history dictionaries; docs/usage.md says which store is which, translated in nine languages and restamped.

Not done, on purpose

  • The memory backend default is unchanged (CHIMERA_MEMORY_BACKEND=json). The audit note said to measure JSON vs SQLite recall before flipping it; that measurement is a separate, pre-registered bench and the next PR, not a line in this one.
  • chimera chat sessions (prose pairs in SessionStore) are not indexed — this covers the Code screen, which is the conversation the item describes.
  • No sidebar search UI over the index; the surface is the tool (and the recorded rows), which is what the item promises.

Tests

tests/test_a_turn_from_two_weeks_ago_can_still_be_found.py (13): found by message, answer and file; project scoping and everywhere; a window in days; function-word query finds nothing; same turn_id twice is one row; forgotten session is gone; a pasted sk-… key is not in the .db bytes; tainted label present/absent; the LIKE fallback answers the same questions on the same data; files_of_exchange; the tool's output shape and its three "nothing" sentences; recall_history ∈ PARALLEL_READ_TOOLS; through the app: a turn is indexed with the files its tool calls named, the tool the app's own registry mounts finds it, the rows survive the session file being unlinked; deleting one conversation and then the whole project forgets exactly those rows; an index whose record raises does not fail the turn.

Sabotage: history.record( replaced by a no-op → the app test fails; forget_session removed → the delete test fails; both restored. test_document_args_match_the_tools classifies days/k/everywhere; test_tool_descriptions_reach_the_user pins the ten dictionaries; the translation-provenance guard passes on the restamped usage.md.

Full gate on a clean copy in WSL: ruff clean, mypy clean (360 files), 6529 passed, 18 skipped, 10 xfailed in 183 s. OpenAPI unchanged (no new endpoints).

🤖 Generated with Claude Code

…ter the conversation forgot it

Every finished Code-screen turn joins an append-only SQLite/FTS5 history index
(`chimera/memory/history.py`, `<home>/history.db`): the message, the answer, the
files the turn read or edited (off the same fold the replay endpoint shows), the
tools, when, and whether it ran tainted. The session file trims at a user
boundary and this never does. Written by the turn's finishing code through
`redact`, never by the model; the LIKE degradation the memory store documents;
the same tokenizer and function-word list; scoped by the same project key.

`recall_history` (read-only, in the run-together set) prints dated excerpts with
the files each turn edited or read, scoped to the current project unless asked
for every project, with a window in days; a tainted turn is labelled in-line the
way a tainted memory is on recall. Deleting a conversation or a project deletes
its rows. An index that will not write logs and the turn goes on.

Ten dictionaries for the Capabilities screen; the usage guide says which store
is which in ten languages, translations restamped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@brcampidelli
brcampidelli merged commit 649a7ab into main Sep 17, 2026
16 checks passed
@brcampidelli
brcampidelli deleted the feat/a-turn-from-two-weeks-ago-can-still-be-found branch September 17, 2026 06:21
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