Files

9.9 KiB

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/<id>/messages returns, under the key check:

{
  "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, <business>/<instance>, from git log -1 --format=%(trailers:key=Mosaic-Agent,valueonly) <commit> -- <path>, 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": <business project name>, "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": <the commit given>, "commit": <the new 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=<id> 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 <commit> -- <path> 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.