Jason ruled on seven open items (20:27Z-20:45Z): seat Gitea tokens read in place, one Discord restart after 6b with row 25 live, row 8 limited to the dev seats, DYOR in dyor-stack-v4 with Sage moved to SetSpark, skills/aws-* excluded locally, no second WebUI return defect, and go on CHAT-02 only. Sage persona files name SetSpark as its business work. Darkwing's SOUL drops harness names that were wrong for T3. DEFERRED adds the slash-prefix paste hazard and the board Host/Origin gap, and moves the ledger T3 item to Done. Dewey's approved CHAT-02 brief (636b0fac) and Filbert's review are recorded. Co-Authored-By: Claude Opus 5.5 <[email protected]>
19 KiB
CHAT-02 brief: read-only histories and Age (#1507, row 5)
Author: Dewey, 2026-09-26. R4. Filbert approved R3 (3b81a3f2…). R4
applies his two non-blocking nits: F17 and §1.1 item 1. His review, with the
R2 verdict and the R3 approval, is
agents/filbert/work/chat-02-brief-r2-review-2026-09-26.md. Earlier revisions
are frozen as BRIEF-r1-314da8b0.md, BRIEF-r2-ed177bf6.md and
BRIEF-r3-3b81a3f2.md. §6 maps his R2 findings to their changes. No source
edits.
Plan row: docs/plans/2026-09-13_webui-session-chat.md line 214. Depends on
CHAT-01 (28d4e98a) and uses the CHAT-01C companion (b023841c).
1. Is there a second defect in the return path?
Short answer: no second return-path defect has shown up for repository Pi seats. Two legs are shown directly: answers reach the transcript, and the board serves them. The Console leg is shown only by replaying the 09-13 code (item 4), until Jason answers the §1.4 question about today's send.
The best explanation for what Jason hit on 09-13 is the product gap. The
board collapses the last answer to 240 characters
(packages/control-board/src/scan.mjs, TEXT_LIMIT, line 23 at ea00ec66
and line 25 at HEAD). The Console showed that one clipped field in an
inspector, with no conversation view and no pending signal. 42c08d52 fixed
the pending display. CHAT-02 covers the clip and the missing view, which means
the new history routes must not go through the board's summary text.
I found one real defect on the send side (§1.3). While reviewing the new routes, I found a missing Host check on the board (§1.5).
1.1 Evidence so far
- Transcripts.
evidence/sends.mjswalks every.pi/state/*/sessionsfile. That is five seat directories: darkwing, dewey, filbert, researcher and sage, and sage has no board sends. For each board send, the script records the first later assistant entry withstopReason: stop. The output isevidence/board-sends-repo-pi.jsonl.- Of 30 sends, 29 are followed by a stop answer in the same file.
- Latency is 1.5 s minimum, 14.6 s median and 1019 s maximum.
- 16 of the 29 answers exceed 240 characters, so the board clipped them.
- The attribution is by file order only. In 7 of the 29, another user-role entry lands between the send and the answer, so the answer may also cover that later input. Filbert rechecked: all 7 are peer agent-sends (dewey to darkwing three times, rocko to darkwing, darkwing to dewey twice, filbert to dewey). Jason's 09-13 question (item 3) is not one of the 7.
- Filbert confirmed that none of the nine files involved branches, so file order equals branch order.
- The unanswered send. Filbert, 2026-09-12T16:33:17Z. The seat started
working on it and read files until 16:35:44Z. The last entry is an
abortedassistant turn, and a new filbert session began at 16:36:26Z. The seat was relaunched mid-turn, so the answer was lost in the seat, not in the return path. §2.3 point 5 turns this into a test. - Jason's report window. At 2026-09-13T00:49:59Z he asked dewey "What are the Gate E criteria?". The answer landed at 00:50:07Z: 365 characters, which the board clipped to 240. He wrote the report at 00:56:51Z. It says the sessions "are all cards within the project dashboard and not available as independent chat interfaces". That describes the missing view, not a missing answer.
- The 09-13 Console, replayed. The pinned files are in
evidence/replay-0913/: the test, the diff against42c08d52, the TAP output and a README.- The scratch extract of
ea00ec66is byte-identical to that commit (45 files checked). - The test is the
42c08d52return-flow test with the Age, pending and clear-once assertions removed, sinceea00ec66has none of those. - It passed (1/1, 20.7 s): the new answer arrived through the 10 s poll, once, in the open inspector.
- The replay's answer is short, so it does not exercise the clip.
- The scratch extract of
- Jason's live send today. Researcher, user entry at
2026-09-26T20:10:57.429Z, answer "pong" at 20:11:06.173Z (8.7 s).
- At 20:15:18Z,
/api/boardshowed the rowidle, withlastAssistantText: "pong", the samesessionFileand a live registration (evidence/board-researcher-*.json). - The live WebUI (pid 1266267, port 7330) serves
app.jsbyte-identical to HEAD (d1a51646…). Its server code has not changed since that process started on 09-13 at 16:19 CDT. - The board (pid 3977979, port 7331) started today at 15:03 CDT.
- At 20:15:18Z,
1.2 What is still unverified
- What Jason's screen showed. I have the transcript and the board JSON for today's send, but not the Console DOM. The Console leg for a live send is unproven until Jason answers §1.4.
- The 09-13 processes. The board keeps no reply receipts, and the 09-13 processes are gone. Item 4 shows that the same code works. It cannot show what those processes served.
- Fleet seats. I did not scan
~/.mosaictranscripts. They are out of scope for CHAT-02. - The clip in a live send. "pong" is 4 characters, so today's send doesn't exercise the clip.
1.3 Send-side defect found in passing
Today's user entry starts with /:
/[dragon-lin:control-board -> dragon-lin:researcher] ping
agent-send.sh adds the header, so the / was already in the Pi composer
when the paste arrived. send-message.sh pastes onto whatever the composer
holds, then presses Enter. Here Pi treated the result as plain text. If the
composer had held a real command prefix, a board reply could have become a
Pi slash command.
Sage confirmed this as a safety gap under CHAT-03I/B3 and recorded it in
DEFERRED.md. It is on #1507 (comment 26538). There is no tools/tmux change
now.
1.4 Evidence that settles it
Sage is putting one question to Jason: after today's ping, did "pong" appear in the Console inspector without pressing Refresh?
- Yes: there is no second defect, and CHAT-02 proceeds as a product-gap fix.
- No:
/api/boardhad the answer, so the defect is in the Console or the WebUI proxy. Capture the Console DOM and the/api/boardnetwork log during one send. - Missing from
/api/board: that would point at the scanner. Missing from the transcript: that would point at transport or the seat. Neither happened today.
One more live send, asking for an answer over 240 characters, would show the clip ending in "…".
1.5 Board Host check (found while reviewing D3)
The Console server checks Host and Origin
(packages/webui/src/serve.mjs lines 54–59). The board server does not. It
checks only the bind address.
A DNS-rebinding page that reaches port 7331 could:
- read
/api/board: 240 characters per row, plus task and cwd; POST /api/replywithapplication/json. The board requires that content type but not a same-originHost, so the text would be pasted into a live seat pane.
Modern browsers restrict some local-network requests, but I have not tested any browser against this. That makes it a finding, not a demonstrated exploit.
The new D3 routes must carry the guard (§2.1). Whether the same guard should
be applied to the existing board routes in that serve.mjs change is Sage's
decision. I recommend doing it.
2. Scope
CHAT-02 per the plan, narrowed by D2:
- a Pi catalogue;
- safe, full branch history, with pagination and cursors;
- separate timestamps;
- relative Age from last activity;
- no writes to logs.
It is read-only. Opening a conversation never resumes, forks, launches or controls anything.
2.1 Backend: packages/conversation/** (Dewey, D4)
Catalogue sources. Only approved source roots count: the board's
repository specs (<repo>/.pi/state/<seat>/sessions). There is no global
scan, and no path comes from the browser. A seat registration is
seat-written, so it is a hint, not authority (CHAT-01 line 62). Its
sessionFile is accepted only when the file is under an approved root for
the same project. Each conversation gets an opaque ID, which the server maps
to a file. Catalogue creation time, engine launch time and last activity are
separate nullable fields.
Opening a file safely.
- Check every path component with
lstat: no symlinks, and it stays inside the root. - Open with
O_RDONLY | O_NOFOLLOW, thenfstatthe descriptor. The descriptor's (dev, ino) must match the checked path. - Read only from that descriptor.
- Never use
SessionManager.open, which can migrate files (CHAT-00 line 67).
Parser.
- Build the
id/parentIdtree and select a leaf explicitly. The default is the last appended entry, which the implementer confirms against the pinned Pi session docs. Other leaves are read-only branches. - Compaction entries render as markers.
- Pi
get_entriesorder is not a branch transcript (CHAT-00 line 66), so it is not used. - The parser never follows
parentSession. The conversation still renders, with a "forked from an earlier session" marker, and the parent file is never opened. - If the Pi header's
cwdnames another project, the conversation is refused. - A malformed line becomes an unavailable part at its position, and reading continues. A truncated trailing line is incomplete, not an error.
Pages.
- CHAT-01 limits: at most 100 parts, at most 8 MiB of serialized UTF-8 bytes (enforced on bytes, not characters), 64 blocks per part, and 262144 characters per string.
- Oversize content splits into continuation parts and is never clipped.
Snapshots and epochs.
- The page reads up to the snapshot length that its cursor pins, so growth during a read is cut there.
- A source epoch is (dev, ino) plus a SHA-256 of the prefix the snapshot covers. Growth past that prefix is the same epoch.
- A different inode, a shorter file, or a changed prefix digest (an in-place rewrite with the same inode) is a new epoch.
Cursors.
- A cursor binds actor, purpose, conversation, branch, snapshot, source epoch and expiry.
- These all refuse, keep the old view and show a reconcile marker: an unknown, foreign, expired or source-replaced cursor.
- Nothing ever switches files silently.
- On this unauthenticated loopback route there is one actor,
local-operator, and cursors bind to it. That is not multi-actor safety. Authenticated actors come with CHAT-04R and must not be claimed here.
Board routes (D3). Two read-only GET routes, catalogue and page.
Hostmust be the loopback name and the board's own port.- A cross-origin
Originis refused. - No CORS headers are sent.
- Responses are
application/jsonwithnosniffandno-store.
Fixtures. Each one names its expected refusal or result.
| # | Fixture | Expected |
|---|---|---|
| F1 | Malformed line | Unavailable part at its position, reading continues |
| F2 | Truncated trailing line | Incomplete marker |
| F3 | Replaced file (new inode) | Old cursors refuse, reconcile |
| F4 | Same-inode prefix rewrite | Old cursors refuse, reconcile |
| F5 | Touched: growth between two pages and during one read | Same epoch, page cut at the pinned length |
| F6 | Unknown, foreign (actor, purpose, conversation or branch) and expired cursor | Each refuses and keeps the old view |
| F7 | Symlink at the file and at a directory component | Refused, never opened |
| F8 | File swapped for a symlink between catalogue and read | Refused (O_NOFOLLOW or a dev/ino mismatch) |
| F9 | Registration naming a file outside the roots, a symlink, or another project's file | Refused, never opened |
| F10 | Pi header cwd naming another project |
Refused |
| F11 | parentSession pointing outside the root |
Renders with a marker, and the parent is never opened |
| F12 | Branched file with two leaves | Default leaf shown, other branch readable, no merge |
| F13 | Compaction | Marker, then retained content |
| F14 | A string over 262144 characters, and a multibyte page reaching 8 MiB before 100 parts | Continuation parts, reassembled exactly, byte cap enforced |
| F15 | Claude harness | unsupported-harness refusal (D2) |
| F16 | Foreign Host and a cross-origin Origin on both routes |
403, no CORS headers |
| F17 | No writes | Before and after every reader operation (catalogue, open, page), in every fixture, including the ones that mutate files on purpose between reads (F3, F4, F5, §2.3 point 6): size, SHA-256, mtime, (dev, ino), and the session directory listing (no new files) are unchanged. Atime is excluded because relatime can update it on read. |
2.2 Console: packages/webui/** (Dewey)
This is the smallest thing that answers the 09-13 complaint: a read-only conversation view per session, opened from the card, the table or the inspector.
- It renders the selected branch in full, with nothing clipped: user text, assistant text, and tool calls and results collapsed.
- Thinking is hidden by default.
- Markdown is untrusted, so it has no active HTML, no unsafe URLs and no terminal escapes.
- Age stays as
42c08d52shipped it. - Reply keeps the existing board path. The view gets new answers through polling. Streaming belongs to CHAT-03.
The full chat UI (sidebar, composer, queue, approvals and uploads) stays in CHAT-05.
Hostile-render fixture (R1). Assistant text and tool output contain:
- HTML, including
<script>and<img onerror>; - a
javascript:link; - ANSI and OSC terminal escapes.
All of it renders as inert text. The test asserts that no element, handler or navigation was created.
2.3 Return-flow regression (required by the plan)
The regression runs on the real board, the real D3 routes and the real WebUI, the way the existing test does. Nothing is injected into the WebUI.
- Path. Send from the conversation view. The seat then appends a user entry, a toolCall, a toolResult and a final answer.
- Exactness. The answer has a sentinel after character 240 and another at the very end. The view shows the exact text, with no "…", once.
- Continuation. A second answer is long enough to split into continuation parts. The test checks that it reassembles exactly and in order.
- Thread order. In the view, the sent message comes first, then the collapsed tool call and result, then the answer.
- Interleaving. A peer agent-send lands before the final answer, as in §1.1 item 1. Both entries show, in file order.
- Relaunch mid-turn, modelled on the 09-12 filbert case. A new session file appears. The open view keeps its file, shows the reconcile marker and never switches silently.
- Timing. A delayed-result variant has the toolResult land after a poll. The draft and caret survive, and no manual refresh is used.
This covers Pi only (D2).
3. Decisions (Sage, 2026-09-26)
- D1: moved.
docs/plans/chat-01/README.md:342defers "the actual execution/writer-claim record" to CHAT-02.docs/plans/chat-01c/README.md:217–218puts R3-1 (reconciling dispatched but unconsumed input) in "CHAT-02 adapter evidence".- A read-only reader needs neither, so both move to CHAT-03, which owns binding and the single writer.
- The published CHAT-01 and CHAT-01C text stays as it is. The move is
recorded on #1507 (comment 26538) and in
docs/plans/2026-09-26_lead-decisions.mditem 8, so a reader of line 342 can find it.
- D2: accepted.
- Pi ships in CHAT-02. Claude's catalogue waits for B1, which is defined at
docs/plans/chat-00/README.md:193and restated atchat-01/README.md:382. Line 66 of CHAT-00 is the capability row that shows the gap: Claude's persisted branch format and leaf selection. unsupported-harnessis a new CHAT-02 reason value, not an existing contract code. The CHAT-01 schema has only a nullableunsupportedReasonfield, and this value fills it.- This narrows the plan's "both harnesses". Sage records it in the lead decisions file.
- Owner after B1 (proposed; Sage confirms): CHAT-03 supplies the Claude catalogue and history. It already owns Claude engine integration and is gated by B1. That gives CHAT-06's both-harness gate an owner.
- Pi ships in CHAT-02. Claude's catalogue waits for B1, which is defined at
- D3: accepted.
packages/conversationis a library with no server. The board adds the two read-only routes, with the §2.1 guard, inpackages/control-board/src/serve.mjsandscan.mjs. Darkwing reviews that change before it lands. The coordination note goes to him when the backend reaches review. §1.5 asks Sage whether the guard should also cover the existing routes. - D4: Dewey authors both. Backend first, then the Console against its fixtures. Filbert reviews the brief now and the code after.
- D5: stays with Jason. Sage's live reply test passed on the transcript
and board legs. The Console leg waits for Jason's §1.4 answer. Sage is
asking him for the go on CHAT-02. No code goes under
packages/conversationuntil Sage relays his answer.
4. Acceptance
-
Every fixture in §2.1 (F1–F17) and the §2.2 hostile-render fixture is covered by a test, and those tests pass.
-
The §2.3 regression passes on the served WebUI, through the real board routes.
-
These still pass:
node docs/plans/chat-00/check.mjs,chat-01/check.mjs,chat-01c/check.mjs, and the control-board, webui and seat suites. -
Browser evidence: the conversation view at 320 and 1440, in both themes, showing:
- a long answer;
- a tool call;
- the hostile-content fixture rendered inert;
- a malformed-line marker;
- a reconcile marker.
There is no horizontal overflow at 320.
-
Filbert approves the exact candidate hashes, and Darkwing reviews the board routes.
-
Live check: one board send with a long answer, shown in full in the view.
5. Not in scope
- live adapters, control, streaming, queues, uploads and approvals;
- fleet seats;
- Claude history until B1;
- changes to
tools/tmux/**,roles/**or session logs; - the
/paste hazard (CHAT-03I); - authenticated multi-actor cursors (CHAT-04R).
6. R2 findings and where R3 answers them
| Filbert R2 | R3 |
|---|---|
| §1 finding 1: Console leg overclaimed; D5 "passed" | §1 opening; §1.2; D5 |
| §1 finding 2: order-only attribution, 7 interleaved | §1.1 item 1; §2.3 point 5 |
| §1 finding 3: the clip is in the board | §1 opening; §2.3 point 2 |
| §1 finding 4: item 4 unpinned | evidence/replay-0913/; §1.1 item 4 |
| §1 finding 5: five directories scanned | §1.1 item 1 |
| §2 finding 1: cursor refusals | F6 |
| §2 finding 2: touched logs | F5 |
| §2 finding 3: same-inode rewrite | Epoch rule; F4 |
| §2 finding 4: registrations, cwd | Catalogue sources; F9; F10 |
| §2 finding 5: parentSession | Parser; F11 |
| §2 finding 6: symlink race | Opening a file safely; F8 |
| §2 finding 7: no-write check | F17 |
| §2 finding 8: Host check, actor | Board routes; Cursors; F16; §1.5 |
| §2 finding 9: byte cap, renderer | F14; §2.2 hostile-render fixture |
| §3 points 1–5 | §2.3 points 1–6 |
| §4: D1 line; D2 citations, reason value, owner | §3 D1, D2 |