Context hygiene at insert time: result ids, read stubs, the cap, recall

Harness spec 3.0.0 (context.md, loop.json `context`), in session/results.ts,
applied by the engine when a result goes into the conversation and never
afterwards:

- every result is headed by its id ([#0001]); a read also by the path as
  given, its lines, the file's length and the first 8 hex digits of the
  content's SHA-256; an empty file says so;
- a read of an unchanged file whose lines lie inside an earlier full read is
  a stub naming it; a list/glob/grep repeated with the same arguments and the
  same result is "Same result as #…";
- output over 400 lines or 40,000 characters keeps head and tail with the
  elided line; the full result is kept with the session (a new `results`
  table in sessions.db, gone with the session);
- edit, multiedit, write and apply_patch answer with their hunks under a
  bracket with the new content's hash; a new file is "Created, N lines.";
- recall (new built-in tool) shows any stored result, or a range of it, also
  after a resume; ids are never reused;
- compaction appends the table of the reads still in the conversation to the
  summary.

A stub only ever points at a result still in the conversation, so after a
compaction, an /undo or a rewind the file is read in full. compaction.prune
is gone: cutting old outputs in place threw the prompt cache away (an old
config's key is dropped with a warning). blocks/context.md is in the prompt
wherever recall is offered. Conformance runner for the `context` area (11
cases pass).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-10-27 20:27:17 +00:00
1 parent 8cfbfbbca4
commit f20e8c4307
28 files changed
+595 -81

No files matched your search

+30
View File
@@ -48,6 +48,11 @@ CREATE TABLE IF NOT EXISTS jobs (
PRIMARY KEY (session_id, id)
);
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(text, content='', contentless_delete=1);
CREATE TABLE IF NOT EXISTS results (
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, seq INTEGER NOT NULL, id TEXT NOT NULL,
json TEXT NOT NULL, text TEXT NOT NULL,
PRIMARY KEY (session_id, id)
);
CREATE TABLE IF NOT EXISTS session_meta (
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, key TEXT NOT NULL, value TEXT NOT NULL,
PRIMARY KEY (session_id, key)
@@ -195,6 +200,31 @@ export class Store {
return last ? [...summaryTurns(JSON.parse(last.json).summary as string), ...msgs] : msgs
}
/** A session's tool results, each in full (harness spec 3.0.0, context.md): what `recall` shows
* and what a stub is decided against. Kept as long as the session is. */
results(sessionId: string): import("./results.ts").ResultStore {
const q = {
count: this.db.query("SELECT COUNT(*) AS n FROM results WHERE session_id = ?"),
save: this.db.query("INSERT OR REPLACE INTO results (session_id, seq, id, json, text) VALUES (?, ?, ?, ?, ?)"),
get: this.db.query("SELECT json, text FROM results WHERE session_id = ? AND id = ?"),
all: this.db.query("SELECT json FROM results WHERE session_id = ? ORDER BY seq"),
}
type R = import("./results.ts").StoredResult
return {
count: () => (q.count.get(sessionId) as { n: number }).n,
save: (r) => {
const { text, ...rest } = r
q.save.run(sessionId, (q.count.get(sessionId) as { n: number }).n + 1, r.id, JSON.stringify(rest), text)
},
get: (id) => {
const row = q.get.get(sessionId, id) as { json: string; text: string } | null
return row ? ({ ...JSON.parse(row.json), text: row.text } as R) : undefined
},
// Without their text: the table is decided by hashes and ranges.
all: () => (q.all.all(sessionId) as { json: string }[]).map((r) => ({ ...JSON.parse(r.json), text: "" }) as R),
}
}
/** Something a session keeps besides its messages, as JSON. */
meta<T>(sessionId: string, key: string): T | undefined {
const r = this.db.query("SELECT value FROM session_meta WHERE session_id = ? AND key = ?").get(sessionId, key) as { value: string } | null