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]>
162 lines
8.4 KiB
Markdown
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.
|