Files
stack/agents/darkwing/work/ledger-t3-source/build.md
T
jason.woltjeandClaude Opus 5.5 136958c98b feat(ledger): Gate F, the ledger's T3 thread source (#1506)
packages/ledger/src/t3.mjs reads ~/.t3/userdata/state.sqlite read-only,
in one transaction. It maps each thread to a seat by title and checks
self-addressed headers. Unmatched threads go in a t3:unmapped row. A
missing or locked database exits 1 and names --no-t3. Gate F is on by
default (lead decision 12). The 6a uppercase-class fix rides here.

Separate item: the Pi session reader splits lines only on \n, so a raw
U+2028 or U+2029 in a string no longer splits a record. Node 26.8.1's
readline split there, and the live ledger refused on HEAD.

Darkwing built to brief R3 (f3c05c1b); manifest ba73a163. Filbert
approved the build (e47ec6da) and the U+2028 fix as its own item; brief
review be1aa414. On an index export: the eight suites
24/90/43/17/14/15/63/18, ledger 47/47. Four nonblocking notes go to a
small follow-up.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 16:39:56 -05:00

162 lines
8.4 KiB
Markdown

# Gate F build: the ledger's T3 source (#1506), candidate for review
Darkwing built this on 2026-09-26 from the approved brief R3,
`docs/plans/2026-09-26_ledger-t3-source.md` (sha256 f3c05c1b, committed in
ffc22c04). Sage gave the go once Filbert confirmed R3. Filbert reviews the
code; Sage commits after the suites. Base is HEAD 1c5f6bc3. Nothing is
committed or pushed.
## Files
`build-manifest.sha256` pins the five files, and `build.patch` is the diff
against 1c5f6bc3 with `t3.mjs` included as a new file.
- `packages/ledger/src/t3.mjs` (new). `readT3(root, range, {dbPath, isDefault})`:
path checks, one read transaction, schema check, project, title mapping,
header cross-check, counts, mentions, diagnostic.
- `packages/ledger/src/ledger.mjs`. The class fix, `t3Header()`,
`readSeats()`, `mergeSources()`, the `pi` and `t3` keys in the report, the
text line for `--no-t3` or a non-default path, and the U+2028 fix below.
- `packages/ledger/src/cli.mjs`. `--no-t3` and `--t3-db PATH`, which refuse
each other; the usage line.
- `packages/ledger/tests/ledger.test.mjs`. HOME at both spawn sites, the
empty default database, 25 new tests.
- `packages/ledger/README.md`. A new "T3 source" section.
The commit should also carry Filbert's updated review,
`agents/filbert/work/ledger-t3-source-review-2026-09-26.md` (be1aa414), and
this directory's new files.
## Beyond the brief: the Pi reader split valid lines
The brief's live read has to exit 0. It didn't, and T3 wasn't the cause. HEAD
refuses the live checkout the same way:
`Malformed session JSON: filbert/2026-09-12T16-38-58-597Z_01a0967c-….jsonl:611`.
That line parses. It holds a raw U+2028 inside a JSON string, which JSON
allows and `JSON.stringify` writes unescaped. Node 26.8.1's `readline` ends a
line at U+2028 too, so it cut the record in two (733 lines by `readline`, 732
by `\n`). The reader parses every line before it checks the range, so on
Node 26.8.1 every live run refuses, whatever the dates. The file was last
written 2026-09-14. I haven't checked which Node version first split there.
The fix replaces `readline` with a small splitter that ends lines at `\n`
only. It sits in `ledger.mjs`, which this build already changes, and it
blocked acceptance, so I made it here instead of filing it. A new test writes a
Pi log with a raw U+2028 and CRLF endings; it fails with `readline` and passes
with the splitter. Please review it as its own item.
## Choices the brief left open
- Imported threads are excluded by the `import:` prefix alone. Live, all
1678 `historyImport` events sit in `import:` streams, so the two rules agree
today. The events table stays optional, so the exclusion doesn't depend on it.
- The header cross-check runs over every user message in a counted thread, in
range or not. The title mapping is current state, so a conflict in old
history still misassigns counts for any range that includes it.
- Validation (role, text, `created_at`) also covers every message in a
counted thread, assistant rows included, and not only rows in range.
- The diagnostic is in range: `humanSentThroughApi` and `humanWithoutEvent`.
One unparseable event, or an event with no string `messageId`, makes both
`unknown`, the same as a missing table (F5).
- Project and thread matching compare `workspace_root` in JavaScript, so a
declared collation on the column can't loosen byte-for-byte equality.
- JSON adds top-level `pi` (Pi rows) and `t3` (read flag, database, seats with
threads, unmapped, excluded, diagnostic). `seats` and `totals` keep their
shape, so existing consumers and tests are unchanged. `t3.seats` lists a
seat whenever it has a mapped thread, even with zero counts in range.
- The unmapped row comes last in `seats`, and only when it has counts.
## Evidence
- Ledger tests: `node --test packages/ledger/tests/`, 47/47 (ledger 44,
of which 25 are new, and gitea helper 3). The busy-timeout test takes about 5.4 s.
- Class fix against HEAD. HEAD's `messageKind` (from `git show
1c5f6bc3:packages/ledger/src/ledger.mjs`) calls a T3 header with
`class=REVIEW-REQUEST`, a tmux preamble with `class=DECISION` and a T3 header
with `class=Actionable` all human. The build calls them agent. The existing
test asserting `class=Actionable` is human now asserts agent.
- Mutations, each on a scratch copy of the package. Three `gitea-helper`
tests fail in every scratch copy because they need the repository's
`scripts/`, so the counts below leave them out.
- Classes back to `[a-z-]+`: 6 fail.
- No header cross-check: 2 fail.
- No symlink refusal: 4 fail.
- Busy timeout 0: 1 fails.
- Two projects allowed: 1 fails.
- Deleted threads kept, imported threads kept, or range filter removed:
4 fail each.
- No role check: 1 fails.
- No diagnostic table check: 1 fails.
- `readline` restored: 1 fails.
- Two mutations pass, and I'm naming them rather than hiding them:
- Removing `mode=ro` changes nothing, because `readOnly: true` already
opens read-only. Both stay, as the brief says.
- Removing `BEGIN` fails 19 tests, but only because `COMMIT` then has no
transaction. No test proves that the queries share one snapshot.
- Eight suites on a local clone of 1c5f6bc3 with the five files: config 24,
task 90, foundation 43, conductor 17, release 14, auth 15, discord 63,
extension-package 18. The first task run showed 89/1, and I didn't capture
the failing line. Three more task runs passed 90/90. I count it as a flake
I can't name, not as green on the first try.
- Union (control-board, webui, seat, mosaic, ledger, discord) on the same
clone: 434/434 three times, 23 to 24 s each. No fake pi left running.
- No test opens the real `~/.t3`. Every CLI spawn sets `HOME` to a temp
directory, and no test calls `readT3` in process. After the runs, no
`ledger-*` temp directories remained.
## Live read
`node packages/ledger/src/cli.mjs --since 2026-09-01 --until 2026-09-26
--no-issues`, exit 0 three times, no header conflict. The table is the run at
2026-09-26T21:31:02Z.
| Seat | T3 threads | T3 board / agent / human | Pi board / agent / human |
|---|---|---|---|
| darkwing | Darkwing; Darkwing in Claude (archived) | 0 / 19 / 28 | 14 / 109 / 141 |
| dewey | Dewey; Dewey in Claude | 0 / 17 / 7 | 7 / 57 / 16 |
| filbert | Filbert | 0 / 25 / 1 | 5 / 92 / 9 |
| rocko | Rocko | 0 / 20 / 1 | none |
| sage | Sage | 0 / 52 / 10 | 0 / 193 / 72 |
| researcher | none | none | 3 / 1 / 1 |
| t3:unmapped | Discord Bot | 0 / 0 / 68 | none |
This matches the brief, allowing for messages sent since 20:54Z. It maps the
same seven threads. Discord Bot has 68 human: 54 without a header and the 14
free-text headers. The diagnostic reads exactly those 14
(`humanSentThroughApi: 14`, `humanWithoutEvent: 0`). T3 agent messages total
133, against the brief's 96 API headers (80 plus the 16 uppercase ones) at
20:54Z. Two imported threads are excluded, and this project has no deleted
threads.
## Not covered
- Snapshot isolation across the queries (see the `BEGIN` mutation above).
- A seat directory named `t3:unmapped` would share the unmapped row. Directory
names that contain a colon aren't used in `agents/`.
- The live read's effect on the main database file can't be checked while T3
writes to it. The stopped and writer-attached WAL tests check it on
fixtures.
## Review and correction
Filbert approved manifest ba73a163 and the U+2028 fix as its own item:
`agents/filbert/work/ledger-t3-build-review-2026-09-26.md`, sha256 e47ec6da.
Correction to "Beyond the brief" above. Line 611 holds a raw U+2028 and a raw
U+2029, and `readline` ends a line at each. The file has 731 lines by `\n`
(`wc -l` agrees), and `readline` makes 733. I wrote 732 because I counted the
empty string after the final newline. The splitter already ends lines at `\n`
only, so the fix covers both characters. The test and the README name only
U+2028.
Filbert's nonblocking notes, for a follow-up after the Gate F commit, since
changing the pinned files now would void the approval:
1. Add a U+2029 to the splitter test and the README line.
2. Two diagnostic mutations survive: `humanWithoutEvent` hardcoded to 0, and
an unparseable event skipped instead of making the diagnostic `unknown`.
Each needs one fixture message.
3. `readT3`'s catch reports any error that isn't a `SourceError` as a SQLite
read failure. It still exits 1, but a bug would read as a database
problem. Rethrow errors that carry no `errcode`.
4. Snapshot isolation stays untested, as recorded above.