Brief e8300cb6 (Darkwing), review bb02d8d3 (Filbert, approve with three nits), R1 record and R1-to-R2 diff. Lead rulings in item 14. Co-Authored-By: Claude Opus 5.5 <[email protected]>
199 lines
10 KiB
Markdown
199 lines
10 KiB
Markdown
# Ledger: a read-only T3 thread source for Table 2 (Gate F brief)
|
|
|
|
Brief only, no code. Darkwing wrote it on 2026-09-26 at Sage's request. Filbert
|
|
reviews it, and Jason sees it on the decision sheet before anyone builds it.
|
|
Issue #1506.
|
|
|
|
## Why
|
|
|
|
Table 2 counts user messages per seat from `.pi/state/<seat>/sessions/*.jsonl`
|
|
only. Development seats now run in T3 on the Claude and Codex harnesses, so
|
|
their prompts, Jason's included, never reach a Pi log. Today the Human column
|
|
can't see T3 at all, and the zero it shows for T3 seats means "no source", not
|
|
"no human prompts". 6a (ef0020ad) taught `messageKind` the T3 header, but no
|
|
source the ledger reads contains one. Gate F (QUEUE row 6) passes when
|
|
Filbert's item closes with zero human messages from Jason. While the ledger
|
|
can't see T3, a zero there proves nothing.
|
|
|
|
## Where T3 keeps messages
|
|
|
|
T3 keeps its state in one SQLite database, `~/.t3/userdata/state.sqlite`, in
|
|
WAL mode (`state.sqlite-wal` and `state.sqlite-shm` sit beside it). Three
|
|
projection tables are enough:
|
|
|
|
- `projection_projects`: `project_id`, `workspace_root`, `deleted_at`.
|
|
- `projection_threads`: `thread_id`, `project_id`, `title`, `archived_at`,
|
|
`deleted_at`.
|
|
- `projection_thread_messages`: `message_id` (primary key), `thread_id`,
|
|
`role` (`user` or `assistant`), `text`, `created_at` (ISO UTC).
|
|
|
|
One more table is optional. In `orchestration_events`, each
|
|
`thread.message-sent` event carries `metadata_json.origin`. Messages typed in
|
|
the T3 app carry an `appVersion` there. Messages sent through T3's API or MCP
|
|
tools, which is how seats talk to each other, don't. See the cross-check below.
|
|
|
|
The same directory also holds `secrets/`, `clerk-tokens.json` and other
|
|
settings files. The reader opens `state.sqlite` and nothing else, and it
|
|
selects named columns only, never `*`.
|
|
|
|
## Reading it, with T3 running or not
|
|
|
|
The file stays on disk whether T3 runs or not. The reader opens it with Node's
|
|
built-in `node:sqlite` (`DatabaseSync`, `file:<path>?mode=ro`, `readOnly:
|
|
true`). That needs no dependency, and Node 26.8.1 prints no warning for it. I
|
|
read the live database this way today, while T3 was running, with no errors
|
|
and no locks. A WAL reader sees every committed message, including those still
|
|
in the `-wal` file.
|
|
|
|
Two rules:
|
|
- Never open with `immutable=1` and never copy the file. Both skip the WAL
|
|
and silently lose the newest messages. A copy of the three files is also
|
|
not atomic.
|
|
- If T3 stopped uncleanly and left a `-wal` without its `-shm`, a read-only
|
|
connection may be unable to rebuild the index. If the open fails, the
|
|
ledger reports it and refuses. I have not tested this case or the fully
|
|
stopped case. Both are acceptance checks below.
|
|
|
|
## Jason or agent
|
|
|
|
Reuse the 6a rule. The first line of `text` decides: a T3 header or the tmux
|
|
preamble counts as agent, `control-board` as the sender counts as board, and
|
|
anything else counts as human. Messages with role `user` count; assistant
|
|
messages don't.
|
|
|
|
6a has a defect this source would expose. Its regex allows only a lowercase
|
|
class (`class=[a-z-]+`). Seats send uppercase classes: Sage's DECISION, INFO,
|
|
REVIEW-REQUEST and REVIEW-NOTE, and my own REVIEW-REQUEST. In this project's
|
|
threads, 16 real agent headers fail on that alone and would count as human.
|
|
The fix is to make the class match case-insensitive. It belongs in this build
|
|
or just before it, reviewed with it. The ms-communications table lists
|
|
lowercase names, so the fix follows what seats send, not the table.
|
|
|
|
Cross-check, read at 2026-09-26T20:54Z for the mosaic-stack project (209
|
|
user messages outside imported and deleted threads, every one with its
|
|
`thread.message-sent` event):
|
|
|
|
| T3 origin | Header matches 6a | Count |
|
|
|---|---|---|
|
|
| typed in the app (has `appVersion`) | no | 99 |
|
|
| sent through the API (no `appVersion`) | yes | 80 |
|
|
| sent through the API | no, uppercase class | 16 |
|
|
| sent through the API | no, free-text roles | 14 |
|
|
|
|
No message typed in the app carries a header, and every API message in this
|
|
project carries one of the three forms. The 14 free-text ones are older
|
|
Discord Bot thread headers such as `[from: SetSpark coordinator (…) -> to:
|
|
Discord Bot (…)]`, written before the guide fixed the format. With the class
|
|
fix they still count as human. That's 14 wrong human counts, all dated
|
|
2026-09-17 to 2026-09-22.
|
|
|
|
Recommendation: the header rule decides, as Sage asked. The reader also
|
|
reports one diagnostic number, not used in any table: user messages the rule
|
|
calls human that T3 recorded as sent through the API. That count is how the
|
|
uppercase-class bug showed up, and it would catch the next format drift. The
|
|
origin field is T3's internal metadata, not a documented contract, so it
|
|
shouldn't decide anything. I'd make it JSON only, so Table 2's layout stays
|
|
the same.
|
|
|
|
## Thread to seat
|
|
|
|
A thread counts for this checkout only if its project's `workspace_root` is
|
|
the ledger's repository root. That is `/mnt/storage/src/mosaic-stack`, project
|
|
`34050c07`.
|
|
|
|
Thread IDs change whenever Jason starts a new thread for a seat, so there's no
|
|
fixed map. T3-AGENT-COMMS.md already names threads after the seat ("Darkwing",
|
|
"Sage", "Dewey in Claude"). Proposed rule: a thread belongs to seat `<s>` when
|
|
`<s>` is a real directory under `agents/` and the lower-cased title equals
|
|
`<s>` or starts with `<s>` followed by a space. Several threads can map to one
|
|
seat. Their counts add up, as several Pi session files already do.
|
|
|
|
Today that maps Sage, Darkwing, Filbert, Dewey and Rocko (one thread each,
|
|
created 2026-09-26), plus "Darkwing in Claude" (archived) and "Dewey in
|
|
Claude". Three threads map to no seat. Two are imported and excluded anyway
|
|
("FINDINGS.md review" and "[dragon-lin:darkwing -> …"). The third is
|
|
"Discord Bot" with 68 user messages: 54 without a header, and the 14
|
|
free-text headers above. The guide's own advice, titles like `review:
|
|
<topic>`, will produce more unmapped threads.
|
|
|
|
Unmapped threads go in one Table 2 row, `t3:unmapped`, so Jason's messages
|
|
there still count toward the Human column and the human-per-closed ratio. The
|
|
other choice is to drop them, which would hide those 54 headerless prompts.
|
|
That is Jason's decision. I recommend the row.
|
|
|
|
A seat's row sums its Pi and T3 counts. JSON splits them by source. Nothing is
|
|
counted twice: every T3 session today runs on `claudeAgent` or `codex`, which
|
|
don't write `.pi/state`, and Filbert found no T3 header in any Pi log.
|
|
|
|
Excluded, with the reason stated in the README:
|
|
- Imported threads (`thread_id` starting `import:`, events marked
|
|
`historyImport`). They are partial copies of Claude Code sessions, not T3
|
|
traffic: 55 user messages in two threads here.
|
|
- Deleted threads (`deleted_at` set). Across all projects there are 3, with
|
|
3 messages. Archived threads count.
|
|
|
|
## What fails closed
|
|
|
|
With the T3 source on, each of these refuses the report with exit 1, the
|
|
code the ledger already uses for unreadable session evidence. The report
|
|
never falls back to Pi logs alone. As with `--no-issues`, `--no-t3` turns the
|
|
source off, and the report then says T3 was not read.
|
|
- The database is missing, unreadable, or won't open read-only (including
|
|
the `-wal` without `-shm` case). This differs from the Pi reader, which
|
|
treats a missing `.pi` as no messages. A missing Pi directory means no Pi
|
|
seats ran here. A missing T3 database on this host means the path or T3
|
|
changed, and a silent zero is the failure Gate F exists to prevent.
|
|
- A required table or column is missing. The reader checks `PRAGMA
|
|
table_info` and names what's missing. This catches a T3 upgrade that
|
|
changes the schema.
|
|
- No project row, or more than one non-deleted row, for this repository root.
|
|
- A counted row has a bad `role`, non-string `text`, or a `created_at` that
|
|
doesn't parse. The Pi reader already refuses malformed JSONL and bad
|
|
timestamps the same way.
|
|
- `state.sqlite` or `~/.t3/userdata` is a symlink. The Pi reader skips
|
|
symlinked entries instead. For one named file, skipping would be another
|
|
silent zero, so this reader refuses.
|
|
|
|
The source never writes to the database. It never reads other files in
|
|
`~/.t3`, and it passes no message text beyond `messageKind` and
|
|
`issueNumbers`, the same rule as for Pi logs. The one outside effect is
|
|
SQLite's own: a WAL reader takes read locks in the `-shm` file, as T3's own
|
|
connections do.
|
|
|
|
## Decisions for Jason
|
|
|
|
1. The source is on by default, with `--no-t3` to turn it off. The other
|
|
choice is off by default with `--t3` to turn it on. I recommend on by
|
|
default, because Gate F exists to count these messages.
|
|
2. Unmapped threads get a `t3:unmapped` row. The other choice is to drop
|
|
them. I recommend the row.
|
|
3. The 14 free-text headers from 09-17 to 09-22 stay counted as human. Fixing
|
|
them would mean loosening the header grammar for history only, and I don't
|
|
recommend it.
|
|
|
|
## Acceptance for the build
|
|
|
|
- Fixture databases built with `node:sqlite` in a temp dir, in WAL mode:
|
|
seat threads and an unmapped thread; imported, deleted and archived
|
|
threads; all three header forms, uppercase classes included; a message
|
|
outside the date range; another project with the same seat titles.
|
|
- Each fail-closed case above has its own test, including a schema column
|
|
removed and `-wal` without `-shm`. One test opens a database whose newest
|
|
message is still in the WAL and counts it. Another reads a database closed
|
|
cleanly with no writer attached, which is the T3-stopped case.
|
|
- The class fix is proven against HEAD's `messageKind`: an uppercase class
|
|
counts as agent after the fix and as human before it.
|
|
- A read against the live database gives the counts in this brief, allowing
|
|
for messages sent since.
|
|
- The ledger README's counting rules name the new source, the mapping rule
|
|
and the exclusions.
|
|
- No suite runs the ledger tests, so the BUILD-LOG entry names the test file.
|
|
|
|
## Not in scope
|
|
|
|
- Claude Code transcripts (`~/.claude/projects`) and Codex sessions
|
|
(`~/.codex/sessions`). T3's database already holds every message T3
|
|
delivered, so those files would only duplicate it.
|
|
- Any write to T3, any T3 API call, or anything that needs T3 running.
|
|
- Fixing the two tmux misclassifications Filbert found in 6a.
|