Harness spec: stubs per request, the compaction table from the reads it covers

context.md and loop.json `context`, spec 3.0.0-draft:

- A stub names only a result in the request being built. A chat on the
  server sends no earlier reply's tool results, so a read in the next reply
  is answered in full there; the CLI, which resends every result, keeps
  stubbing across steps. Said for reads and for repeated listings alike.
- The compaction table is drawn from the reads the summary covers: the
  full reads since the previous compaction in turns still in the history,
  whether or not the request still carries them. A turn taken back takes
  its reads with it. The CLI's dedupe table is exactly that set; a chat
  finds it in what it stored.
- Ids are the result's number in the session, in hex, at least 4 digits
  (#0001); the examples and the prompt block now use such ids.
- A bracket names the path as the call gave it; what identifies a file
  is its absolute path.

Conformance `context`: a `reply` step (a request carrying none of the
results so far) and a case for both rules. The CLI's runner keeps the
history apart from the request; compactionTable takes the reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-10-28 01:20:28 +00:00
1 parent eacf84fd67
commit 4ca6af12b1
6 files changed
+150 -40

No files matched your search

+39 -19
View File
@@ -15,32 +15,46 @@ replaces the history with a summary.
## 1. Every result has an id
Every tool result is given an id when it is inserted: `#` and lowercase hex, at least
`ids.digits` (4) digits, unique in the session and never reused — not after a compaction either. It
is the first thing in the result, in brackets: `[#a3f2]`, then a newline and the result.
Every tool result is given an id when it is inserted: `#` and the result's number in the session —
counting from 1, in lowercase hex, at least `ids.digits` (4) digits — so `#0001`, `#0002`, …,
`#000a`, …, `#10000`. Unique in the session and never reused — not after a compaction either. It is
the first thing in the result, in brackets: `[#0001]`, then a newline and the result.
A read's bracket says more: the path as the call gave it, the lines shown, the file's length in
A read's bracket says more: the path **as the call gave it**, the lines shown, the file's length in
lines and the first `sha_digits` (8) hex digits of the SHA-256 of the file's content —
```
[#a3f2 src/app.ts lines 1-200 of 340 sha 1c9e04ab]
[#0012 src/app.ts lines 1-200 of 340 sha 1c9e04ab]
1: import …
```
— and an empty file `[#a3f2 notes.txt empty sha e3b0c442]`. The full hash is kept by the session;
— and an empty file `[#0013 notes.txt empty sha e3b0c442]`. The full hash is kept by the session;
only it decides whether a file is unchanged.
**Paths in a bracket are the call's.** A read's bracket, an edit's (§5) and the compaction table
(§7) name the path as the call gave it — what the model wrote, so it recognises it. What identifies
the file — for a stub, for the table's "newest read of each path", for whether a file was read before
it is changed — is its absolute, resolved path (`filesystem.md`, *Paths*). Every other answer (an
error, a refusal, a search's results) names the absolute path.
**Every result is stored with the session, in full** — before any cap, and for a stub the text it
stands for — so `recall` can show any of them for as long as the session exists.
## 2. An unchanged re-read is a stub
**A stub names only a result that is in the request being built** — one whose text is among the
tool results this request sends to the model. That set, not anything the session stored, is the
dedupe table. Where a request does not carry earlier results — a chat on the server sends no earlier
reply's tool results with the next reply; after an undo, a rewind or a compaction they are gone — a
read is answered in full, because what a stub would point at is not in front of the model. It
holds for §2 and §6 alike.
A read is answered with a stub when the file's content hash equals that of an **earlier full read
of the same path still in the dedupe table**, and the lines asked for lie **wholly inside** that
of the same file still in the dedupe table**, and the lines asked for lie **wholly inside** that
read's lines:
```
[#b710 src/app.ts lines 50-80 of 340 sha 1c9e04ab] Unchanged since #a3f2, which already shows lines 1-200.
[#0019 src/app.ts lines 50-80 of 340 sha 1c9e04ab] Unchanged since #0012, which already shows lines 1-200.
```
The bracket is the new read's; the sentence names the earlier result and its lines. A stub points
@@ -55,7 +69,7 @@ as many whole lines as fit `head_lines` (200) and `head_chars` (20,000) — and
`tail_lines` (100) and `tail_chars` (10,000) — with one line between:
```
[1234 lines elided, full output #c9d0]
[1234 lines elided, full output #0020]
```
A first line longer than `head_chars` is cut to it and ends ` […cut]`; the elided line follows it
@@ -67,11 +81,11 @@ from an offset).
`recall(id, offset?, limit?)` shows a stored result again, in full, up to 2000 lines from `offset`:
```
[#d004 recall #c9d0 lines 1-2000 of 3400]
[#0021 recall #0020 lines 1-2000 of 3400]
…
```
An id the session never gave: `[#d004] No stored result #ffff in this session.` The `#` may be left
An id the session never gave: `[#0021] No stored result #ffff in this session.` The `#` may be left
out of the argument. A recall is not capped, is not deduplicated, and is itself stored.
## 5. Edits answer with the diff
@@ -81,17 +95,17 @@ of the change (three lines of context, hunks only — the path is in the bracket
naming the path and the new content's hash:
```
[#e201 src/app.ts sha 7d0a91c3]
[#0022 src/app.ts sha 7d0a91c3]
@@ -12,3 +12,3 @@
```
`write` of a new file answers `[#e202 docs/new.md sha 52b6e3a0] Created, 14 lines.` — the model
wrote the content and has it. A deleted file is `[#e203 old.md deleted]`. An `apply_patch` over
`write` of a new file answers `[#0023 docs/new.md sha 52b6e3a0] Created, 14 lines.` — the model
wrote the content and has it. A deleted file is `[#0024 old.md deleted]`. An `apply_patch` over
several files answers the id alone, then each file in its own bracket with its change, in the
patch's order:
```
[#e204]
[#0025]
[src/a.ts sha 7d0a91c3]
@@ -1,2 +1,2 @@
[docs/new.md sha 52b6e3a0] Created, 14 lines.
@@ -105,7 +119,7 @@ capped like any output, the first line kept.
For `dedupe.by_result` — `list`, `glob`, `grep`, `search` — a call with the same arguments (compared
as JSON with sorted keys) as an earlier one in the dedupe table, **whose result is the same text**,
is answered `[#f002] Same result as #f001.` The call is run again and compared, so a change from
is answered `[#0027] Same result as #0026.` The call is run again and compared, so a change from
anywhere shows up as a full result. Other tools are never deduplicated: a command's output twice is
two results.
@@ -114,8 +128,14 @@ two results.
Compaction is the one deliberate break of the cache, and it already exists. In 3.0.0 two things
come with it:
- After the summary the harness adds — itself, not the model — a table of the reads still in the
dedupe table: the newest full read of each path, newest last, at most `compaction.rows` (50):
- After the summary the harness adds — itself, not the model — a table of the **reads the summary
covers**: the full reads made since the previous compaction (or since the session began) in turns
that are still part of the history being summarised — whether or not their results are still in
the request. A turn taken back (an undo, a rewind, a reply regenerated or deleted) takes its reads
with it. Of those, the newest full read of each file (by its absolute path), newest last, at most
`compaction.rows` (50); a stub is not a read here. A session that resends every result (the CLI)
finds exactly these in its dedupe table; a chat, which does not, finds them in what it stored for
the turns being summarised:
```
## Files read before this summary
@@ -124,7 +144,7 @@ come with it:
| Path | Lines | sha | Result |
|---|---|---|---|
| src/app.ts | 1-200 of 340 | 1c9e04ab | #a3f2 |
| src/app.ts | 1-200 of 340 | 1c9e04ab | #0012 |
```
With no reads there is no table.