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:
28 files changed
+595
-81
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user