# Row 225 (C2-10b) Console chat: replies, citations, write notices, status line Owner: Morgana, from sage-3582 (18:53Z 10-23). Base: origin/refactor 45dca69f6. Branch: morgana/row225. Brief: docs/plans/2026-10-20_case2-rows.md, C2-10b. Design: agents/gosalyn/work/case2/DESIGN.md 7.4, 5.2 (the reply schema and the check result) and the write notice and `library.revert` (about line 286). ## Ruling on the dependency gap (sage-3591) 225 builds against the design contracts. `renderReply` stays pure, and the caller supplies the answers it can't know. The revert route proxies to the bus verb and shows the refusal code until the verb exists. Landing order: 225 lands after 217 (C2-06), not after 288 (C2-02). Sage adds `after 217` to row 225. Before 288 lands, the Revert button gets the bus's refusal and shows its code, as the brief allows. ## A1. The check result I assume (to 217, Launchpad) The broker stores one check result with each agent-to-human message the guard passes. The Console reads it on the message row that `/api/chat/threads//messages` returns, under the key `check`: ```json { "check": { "citations": [ { "scope": "canon", "agent": null }, { "scope": "supporting", "agent": "acme/scribe" }, { "scope": "web", "agent": null } ] } } ``` - `check` is `null` or absent on a message the guard didn't check: a human's message, a free-text reply, a notice, or a message from before 217. - `check.citations` has one entry per entry of the body's `citations`, in the same order. - `scope` is one of `canon`, `playground`, `supporting` and `web`. A url cite is `web`. - `agent` is the `Mosaic-Agent` trailer value, `/`, from `git log -1 --format=%(trailers:key=Mosaic-Agent,valueonly) -- `, or `null` when the file's last commit at `commit` carries none. A url cite has `agent: null`. The Console shows the agent-written label when `agent` is a nonempty string, and names the agent. The Console trusts nothing in the body for scope or label. A cite with no matching check entry renders with no badge and no label. It doesn't fall back to a scope it worked out itself. If 217 uses other names, I change mine to theirs before either of us pushes (sage-3591 item 1). ## A2. The revert verb I assume (to 288, Honker) - Verb: `library.revert`. A human verb (design M4): from an agent session it refuses `human-required`, like `library.status` (`broker.mjs:5133`). - Args: `{ "project": , "commit": <40 lowercase hex> }`. The design writes `library.revert {commit}`. `project` is added to match the other library verbs, and like theirs it is the key of `business.projects` (`mirror.mjs:31`), which is the chat project's `repo` link, not its UUID. Both are required, and no other key is allowed (`invalid-request`), as the other library verbs check with `keys(args, ...)`. - Result: `{ "project", "path", "reverted": , "commit": }`. - Refusal codes the Console shows by name: - `library-revert-not-agent`: `commit` has no `Mosaic-Agent` trailer for this business, or isn't reachable from the served commit. - `stale-base`: a later commit changed that path. - `human-required`, `invalid-request`, `project-not-configured`. - Before 288 lands, the bus refuses the verb (expected `unknown-verb`, `broker.mjs:6578`), and the Console shows that code. - HTTP status in the Console (`packages/webui/src/bus.mjs` `busStatus`): `stale-base` and `library-revert-not-agent` map to 409. Every other code keeps the existing mapping. ## A3. The status line `GET /api/library/status?project=` proxies `library.status {project}` through the existing `libraryStatus` option. `cli.mjs:110` already passes that option, but `startServer` doesn't read it. The status line shows: - the served commit (7 characters) and its age; - `library-diverged` when the sync state names it; - the fetch error code; - the reply limits in force, and the keys that fell back; - the count of R-problems, linking to the list. It reads the fields `readSyncState` returns at the base (`packages/library/src/mirror.mjs:232`). It shows any field 217 adds (the limits) only when present. ## A4. "Changed since" (ruled sage-3596) Sage ruled that 217 supplies `last`. On a `thread.messages` row that carries `check`, each path cite's entry gets `last`, the file's last commit at the served tip, read when the message is read. When `last` is absent the Console marks nothing changed, and a test pins that. One more field is needed, raised in morgana-0200. The cited `commit` is a library commit, not the file's own last commit, so a cite made at a commit that didn't touch the file would differ from `last` while the file is unchanged. I asked 217 to also store `file`, the file's last commit at the cited commit. That's `%H` from the same `git log -1 -- ` call that reads the trailer, so it costs no extra git call. The Console marks a cite changed when both `file` and `last` are 40-hex and differ. The cited commit is reachable from the served one (5.2), so `file` is an ancestor of `last`, and differing means newer. A missing or malformed `file` or `last` marks nothing. The entry the Console reads, in full: `{ scope, agent, file?, last? }`. ## A5. `no-cited-answer` retires `renderItems` and its `NOTICES` (`no-cited-answer`, `no-answer`, `turn-failed`) go. 217 sends the runner's fixed texts as plain bodies (design 5.2, "Runner texts"), and renderReply shows those as free text. Design 7.4 names no other notice than `library-write`, so nothing is lost (sage-3596 item 3). ## Files Both deviations below were accepted in sage-3596 and are named in the manifest for Dewey. The brief owns these: - `packages/library/src/render.mjs`: `renderReply` replaces `renderItems`. - `packages/library/src/index.mjs`: the export. - `packages/library/README.md`: the section. - `packages/library/tests/render.test.mjs`. It also owns `bus.js`, its stylesheet, `serve.mjs` (only the two routes) and new webui tests. Two deviations, both recorded here: - The chat view's message list is `packages/webui/src/public/chat.js:85` (`window.MosaicChat`), not `bus.js`. The reply, notice and Revert rendering goes in `chat.js` and `chat.css`. The status line goes in the chat thread view, so it is also in `chat.js`. - `packages/webui/src/console-bus.mjs` and `cli.mjs` each get one `libraryRevert` line, beside the existing `libraryStatus` wiring. ## Overlap with row 220 (C2-10a, Researcher, 3ef6da90d) 220 changes one line each in the webui tests discord.test.mjs, fixture.mjs, history-fixture.mjs, relaunch.test.mjs and return-flow.test.mjs. Its other paths are in control-board, conversation, discord and business. 225 doesn't touch those five files or those packages. Its webui tests are new files. ## Handoff to Fethry (mos-1501, Q245) The row moved to Fethry (Codex, fethry/row225). Everything below is **untested**: no suite or single file has run on it, since a run needs Sage's CLEAR. Each changed file passes `node --check`. Done in the handoff commit: - `render.mjs` `renderReply(body, {check, changed, name})`; index export; README section; `render.test.mjs` rewritten. `renderItems` is gone from every package. - `serve.mjs`: `/render.mjs` static entry (owner and user file maps), `GET /api/library/status` (one `project` query, answer must name that project), `POST /api/library/revert` (save's boundary, strict `{project, commit}`, refusal shows the code, lost or unusable answer is `outcome-unknown`, never retried). Both are nulled for a granted user, and `userRoute` keeps them owner-only. The tester listener refuses `libraryRevert`. - `bus.mjs`: `library-revert-not-agent: 409`. That re-pins `packages/webui/src/bus.mjs` in `packages/bus/tests/channel-proof.test.mjs`, with a comment line, as row 254 did. It's a third deviation, in another package's test. - `console-bus.mjs` and `cli.mjs`: `libraryRevert`, beside `libraryStatus`. - `chat.js`: dynamic `import('/render.mjs')` (CSP is `script-src 'self'`); body text until it loads. Cites with a scope badge, the agent-written label, "changed since" from `file`/`last`, links only for https with `rel="noopener noreferrer"`. A notice gets a Revert button behind `confirm()`. The library status line goes under the thread heading. - `chat.css`: cite, badge, notice and status styles. - New tests: `packages/webui/tests/library-routes.test.mjs` and `packages/webui/tests/chat-replies-browser.test.mjs`. 220's five files are untouched. Decisions made in the build: - The library name is the chat project's `repo`. A project with no repo reads no status and its Revert does nothing. - These codes hide the status line: `project-not-declared`, `library-mapping-invalid`, `project-not-configured`, `not-configured` and `owner-only`. - Otherwise every code-repo thread, and every granted user, would show "unavailable". - Cost: a broken library mapping also hides the line. `library.status` from the CLI still shows it. - "Age" is the time since `lastFetch` ("fetched 5 min ago"), not the served commit's age, which the status doesn't carry. - The limits fields are assumed to be `replyLimits {requireCitation, citationsPerReply}` and `replyLimitsFallback [keys]`, and are shown only when present. **Not agreed with 217.** Confirm with Beakley and rename. - A notice body's shape is all the renderer checks. An agent in a business without a library could send a body that looks like a notice. The page would show it, but `library.revert` still refuses it. Left: 1. Get CLEAR and run the webui and library suites, `channel-proof.test.mjs`, and the two new files whole. The browser test's selectors and waits have never run. 2. Agree the limits field names with 217 (Beakley). 3. Granted-user R6 G5 (`granted-user.test.mjs:252`) lists owner verbs by hand. Add `library.revert` once 288 lands. 4. BUILD.md: the mutant list and the mos-1275 timeout plan. 5. The manifest names the three deviations: chat.js and chat.css in place of bus.js, the console-bus and cli lines, and the channel-proof re-pin.