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]>
383 lines
19 KiB
Markdown
383 lines
19 KiB
Markdown
# 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
|
||
|
||
1. **Transcripts.** `evidence/sends.mjs` walks every `.pi/state/*/sessions`
|
||
file. 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 with `stopReason: stop`. The
|
||
output is `evidence/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.
|
||
2. **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
|
||
`aborted` assistant 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.
|
||
3. **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.
|
||
4. **The 09-13 Console, replayed.** The pinned files are in
|
||
`evidence/replay-0913/`: the test, the diff against 42c08d52, the TAP
|
||
output and a README.
|
||
- The scratch extract of `ea00ec66` is byte-identical to that commit (45
|
||
files checked).
|
||
- The test is the 42c08d52 return-flow test with the Age, pending and
|
||
clear-once assertions removed, since ea00ec66 has 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.
|
||
5. **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/board` showed the row `idle`, with
|
||
`lastAssistantText: "pong"`, the same `sessionFile` and a live
|
||
registration (`evidence/board-researcher-*.json`).
|
||
- The live WebUI (pid 1266267, port 7330) serves `app.js` byte-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.
|
||
|
||
### 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 `~/.mosaic` transcripts. 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/board` had the answer, so the defect is in the Console or the
|
||
WebUI proxy. Capture the Console DOM and the `/api/board` network 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/reply` with `application/json`. The board requires that content
|
||
type but not a same-origin `Host`, 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`, then `fstat` the 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`/`parentId` tree 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_entries` order 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 `cwd` names 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.
|
||
- `Host` must be the loopback name and the board's own port.
|
||
- A cross-origin `Origin` is refused.
|
||
- No CORS headers are sent.
|
||
- Responses are `application/json` with `nosniff` and `no-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 42c08d52 shipped 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.
|
||
1. **Path.** Send from the conversation view. The seat then appends a user
|
||
entry, a toolCall, a toolResult and a final answer.
|
||
2. **Exactness.** The answer has a sentinel after character 240 and another
|
||
at the very end. The view shows the exact text, with no "…", once.
|
||
3. **Continuation.** A second answer is long enough to split into
|
||
continuation parts. The test checks that it reassembles exactly and in
|
||
order.
|
||
4. **Thread order.** In the view, the sent message comes first, then the
|
||
collapsed tool call and result, then the answer.
|
||
5. **Interleaving.** A peer agent-send lands before the final answer, as in
|
||
§1.1 item 1. Both entries show, in file order.
|
||
6. **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.
|
||
7. **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:342` defers "the actual execution/writer-claim
|
||
record" to CHAT-02.
|
||
- `docs/plans/chat-01c/README.md:217–218` puts 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.md` item 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:193` and restated at
|
||
`chat-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-harness` is a **new CHAT-02 reason value**, not an existing
|
||
contract code. The CHAT-01 schema has only a nullable `unsupportedReason`
|
||
field, 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.
|
||
- **D3: accepted.** `packages/conversation` is a library with no server. The
|
||
board adds the two read-only routes, with the §2.1 guard, in
|
||
`packages/control-board/src/serve.mjs` and `scan.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/conversation` until 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 |
|