Repository navigation
agent memory: tool_result snapshots are not live file views (re-read or they’re stale) #693
Description
Parent
#548 (agent session architecture)
Type
A1-adjacent lock — not a fifth identity issue. #549 makes the next turn see prior tool work. This issue is the other half: seeing it does not make it true. Plan with A1; do not wait for A4 to notice the hole.
Problem
Once A1 (#549) puts truncated tool_use / tool_result on the wire, turn N+1 will still contain the read_file / grep / exec bytes from turn N. That is how the agent stops being a new hire. It is also a snapshot.
If src/foo.ts changed on disk (this session’s later write_file, another tab, exec, a human, another device on the same sandbox), the logged result is not rewritten. Nothing in core re-reads a file because a new /api/agent POST started.
We already enforce this at mutate time inside a single run (#277 RunFileFreshness, shipped):
- Gate 1: this HTTP run must have
read_file’d the path. - Gate 2: disk mtime+size must still match.
- Each Function POST starts an empty ledger. Fingerprints are forbidden on the session blob.
So after A1 ships, the default story is DeepSeek’s resume story, every turn:
| Layer | After A1, next Function POST |
|---|---|
| Model-facing history | old tool_result text still in the request (“I already know this file”) |
#277 ledger |
empty (new run) |
str_replace / overwrite |
soft ERROR: read required / file changed |
| After A4 compact | even the stale snapshot is gone from the request; only notes + files-touched remain |
If we do not name this, A1 looks broken: the model “remembers” the file, the edit fails, it re-reads the whole tree, and the session feels one-shot again — just with extra ERROR noise.
Goal
Keep the #277 invariant (observation is proven this run + live disk, never SessionStore CAS). Teach the model-facing side the same law:
- Do not rewrite a persisted
tool_resultwhen the file changes. Persist the read; ask “still true?” at mutate time. - Every new
/api/agentPOST is a new observation run. A1 history is evidence of a prior look, not a grant. - A short, volatile reminder (block 2 / trailing user turn — inference: cache-stable two-block system prompt (don’t bust the KV prefix) #558) tells the model: paths you read last turn still need
read_filethis turn before edit. Do not dump fingerprints or file bodies intometa. - After compaction (compaction: model-capability context compaction (not API payload slices) #552), the reminder + files-touched list in the checkpoint is how “what did you already find?” survives without the raw bytes. The model re-reads before the next surgical edit.
- Failed
str_replacestill returns a small window (agent tool: stale/failed str_replace returns a small file window #564), not a forced full-file dump.
Success: a two-turn edit of foo.ts does read_file on turn 2 (cheap, windowed — #563), then str_replace, and does not (a) hallucinate from turn-1 bytes or (b) rediscover the repo from zero.
Constraints (lock)
- Do not put mtime/size/content hashes in Redis envelope
metaor the Blob transcript as a cross-turn CAS cache. plan: agent read-before-edit + external file freshness (parent) #277 already banned this (multi-device / shared jail). Freshness is disk, not session. - Do not rewrite
tool_resultrows in the model-facing history when a later tool or another writer changes the file. Snapshot stays a snapshot. - Do not port DeepSeek’s in-process
WeakMap(dies on Function end anyway) or Oh My Pi hashline as the edit dialect (agent tool: stale/failed str_replace returns a small file window #564: keep uniqueold_string; hashline is a later land-rate question). - Feature-divide: reminder is server-assembled (same place as persona / notes). Wasm paint stays
tool_runcards. No dual chat. - Caps: the reminder is a small string (paths + one-line law), not a second 8 MiB problem. NEW cap in the A1 or this issue’s
create-planif it rides a Function body. - Optional later: sandbox-sourced “these paths changed since last turn” inject (DeepSeek
agent.inject()). v1 is the empty-ledger reminder, not a file watcher. Cloud Functions have no process-local watcher.
Maps from peer harnesses (2026-08-19 source notes)
| Peer | Persist | Freshness |
|---|---|---|
| DeepSeek Harness | tool/result stays on the surface until compaction shadows it |
Observation WeakMap + CAS on write/edit; empty after resume; inject() file-change notices |
| Oh My Pi | toolResult snapshot + hashline tag on read |
Edit-time whole-file hash; reject or 3-way merge; SnapshotStore gone on resume |
| Codex | FunctionCall + FunctionCallOutput until compact drops them |
None — re-read or stale. AGENTS.md is world-state diffs, not file bytes |
| OpenCode | tool parts until compaction cutoff; V1 also prunes old tool blobs | None for reads. Git tree snapshots are for revert, not freshness |
Invincible already has the honest half of DeepSeek’s freshness layer (#277, per-POST). We are missing the prompt-side sentence that makes A1 and #277 compose.
Shared peer line, worth stealing as the product rule:
Snapshots in the transcript, freshness beside it. Do not rewrite
tool_resultwhen a file changes.
Non-goals
- Replacing plan: agent read-before-edit + external file freshness (parent) #277 or weakening read-before-edit
- Persisting the freshness ledger across Function turns
- Hashline / LSP-on-write / in-process bash
- File-watcher inbox as v1
- Compaction engine (compaction: model-capability context compaction (not API payload slices) #552) — but A4 must keep files-touched so this reminder has names after compact
- Cross-session handbook (separate later issue)
- Identity / first-turn lobotomy — that remains agent memory: structured truncated tool_result on the wire (not Tool: crumbs) #549
Suggested next
Fold into the #549 A1 create-plan (reminder copy + “do not rewrite results” + “do not put fingerprints on the session”). If A1’s plan wants this as its own phase, keep this issue; do not invent a third store.
Related sandbox/tools (not this board): #277 (shipped), #563 windowed read_file, #564 stale str_replace window, #565 exec summary + disk log.
Peer split vs #550 / #552
- agent memory: durable working notes / findings across turns (identity, not one-shot) #550 notes are decisions (“don’t touch Z”). They stay true until the operator says otherwise.
- This issue is file bytes. They go stale. Notes must not be used as a substitute for
read_file. - compaction: model-capability context compaction (not API payload slices) #552 drops raw tool bytes from the model request. After that, this reminder + files-touched is the investigation memory for files.
Activity