gizmoduck: rows 270 and 271 packets: the 264c count and 264d clear contracts pinned for row 272 (Huey), and the Pi 0.85.1 compaction source contract

This commit is contained in:
jason.woltje committed 2026-10-22 14:52:58 -05:00
1 parent 0cbe2e9b35
commit 8365e4e5b4
2 files changed
+121

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
# Row 270 build notes (Gizmoduck)
264c, T1 item 7: the compaction count, exact for Pi. Gitea #1775. Brief:
docs/plans/2026-10-21_t1-sessions.md section 4 (D6), with RULINGS Q6 (Pi exact, Claude `unknown` with
the Vikunja 466 reason). Reviewer Duckworth, gate owner Sage. Branch gizmoduck/row270 from
gizmoduck/row269 2d0d407ba (sage-2797, Q220). Row 269 lands first, and this branch rebases with it.
## Contract for 264e (pinned for Huey, row 272)
The surface reads the count from `session.list`. Each row gains one key, `compactions`:
```json
{ "id": "...", "at": "...", "name": "...", "predecessor": null, "default": true,
"compactions": { "count": 3, "known": true, "reason": null, "at": "2026-10-22T16:40:00.000Z" } }
```
- `known: true`: `count` is the exact number of completed compactions over the session's lifetime,
an integer of 0 or more. `reason` is null.
- `known: false`: `count` is a lower bound (an integer) or null when there is no bound. Show
`unknown` with the reason, never a bare 0. `reason` is one of:
- `harness-unsupported`: Claude Code, until Vikunja 466 pins its format.
- `not-collected`: no collection has run for this session yet.
- `store-unreadable`: a store file couldn't be read, or isn't a Pi session.
- `store-incomplete`: a stream file got shorter, was replaced, or has unreadable lines, so
entries may be missing.
- `at`: ISO time of the last collection, or null if none has run.
- A clear successor (264d) starts at `known: true, count: 0` once its first collection runs. The
retired session keeps its row and its count.
- On a base without 264c, the key is absent. The surface shows the count as unavailable, which is
what Huey's plan does (Sage Q1).
No new verb and no new input key. Access is `session.list`'s: the thread owner, or the tester.
## Pi source contract (pinned offline at 0.85.1)
`@earendil-works/pi-coding-agent` 0.85.1 (package.json pin), read in node_modules:
- One line per completed compaction: `{"type":"compaction","id":...,"parentId":...,"timestamp":...,
"summary":...,"firstKeptEntryId":...,"tokensBefore":...}` from `appendCompaction`
(dist/core/session-manager.js:818-833). The two callers write it after the summary exists
(dist/core/agent-session.js:1539, :1826). A start or an aborted compaction writes no entry.
- What doesn't count: a `message` entry with role `compactionSummary` (a rendered summary,
docs/session-format.md:154), any message inside a compaction's `retainedTail`, and
`branch_summary` entries.
- Identity: the stream is the `*.jsonl` file name under the session's `session/` directory, and the
event is the entry `id`. Pi's ids are unique within a file (`generateId(this.byId)`). An entry
id repeated in one file counts once.
- Partial trailing line: wait. Only lines that end with a newline are read, as
packages/conversation/src/pi.mjs:35-38 does.
+72
View File
@@ -0,0 +1,72 @@
# Row 271 build notes (Gizmoduck)
264d, T1 item 7: clear context through row 194's lifecycle stop. Gitea #1776. Brief:
docs/plans/2026-10-21_t1-sessions.md section 5 (D7, D8), with RULINGS Q4 (rebase onto 194's
landing, no 194 code copied). Reviewer Duckworth, gate owner Sage. Built after row 270 (sage-2797).
## Contract for 264e (pinned for Huey, row 272)
Proposed, pending Duckworth's review. Any change goes to Huey before it's pushed.
### session.clear
Input, every key required, with no other key (`keys()` as `session.create` does):
```json
{ "thread": "<thread id>", "session": "<session id>", "revision": 0, "confirm": true, "op": "<op key>" }
```
- `revision`: the integer from the session's `session.list` row (below). It names the state the
human confirmed.
- `confirm`: the literal `true`. Anything else refuses `confirm-required`.
- `op`: the caller's operation key, an identifier, unique per thread. It is also the idempotency
key.
Answer, the operation envelope:
```json
{ "op": "...", "thread": "...", "session": "<old id>", "successor": "<new id>",
"state": "clearing", "start": null, "refusal": null, "at": "..." }
```
- `successor` is bound when the request is accepted, before any asynchronous work.
- `state`:
- `clearing`: accepted. Sends and launches to the old session are fenced, and the drain or stop
is in progress.
- `uncertain`: stop proof isn't available. Capacity stays reserved and nothing is forced. It
resumes when proof arrives.
- `retired`: the old session is retired and the successor exists, in one bus transaction.
- `failed`: no retirement happened, and `refusal` names why. The old session stays as it was.
- `start` is null until `state` is `retired`. After that it is `held` (waiting for capacity),
`started`, or `refused` with `refusal` set.
- `at`: the time of the latest state change.
Repeats and conflicts:
- The same `op` with the same payload returns the current envelope and starts nothing new.
- The same `op` with any other payload refuses `session-op-conflict`.
- A new `op` on a session with an open clear refuses `session-clearing`.
- A `revision` that isn't current refuses `session-revision-stale`.
- Other refusals: `session-not-found`, `session-retired`, `processing-halted` (D8: a halt refuses
and changes nothing), and the owner refusals `session.create` already gives.
### session.operation
Input `{ "thread": "...", "op": "..." }`. Answer: the same envelope. Unknown op refuses
`operation-not-found`. A CLI that waits 30 s and then reports pending uses this to poll. A retry
queries the op here and never resends the clear.
### session.list additions
Each row gains:
- `state`: `active`, `clearing`, `uncertain` or `retired`.
- `revision`: an integer. 0 at creation, plus 1 for each lifecycle event on that session.
- `successor`: the successor's id once retired, otherwise null.
A successor row has `predecessor` set (264a's column). When the cleared session was the thread's
default, `default` moves to the successor at retirement. Clearing any other session leaves the
default where it is.
On a base without 264d, `session.clear` refuses `unknown-verb`, and the rows have no `state`,
`revision` or `successor`.