fix: reasoning always renders above its message; dock drops delivered subagents at delivery

TWO owner-reported bugs from the live red-circle session
(chat_muhpyoll_27305e23), both investigated to root cause first:

1. THINKING BLOCKS UNDER THE MESSAGE. GLM emits trailing reasoning
   tokens, so the persisted content lands [text, thinking] (4 of the 6
   thinking-bearing messages from [21] on in that session) - and the
   view canon presented arrival order verbatim, rendering the reasoning
   UNDER its own message. Fix: projectView stable-partitions each
   assistant message - reasoning parts first (their own order kept),
   then text/tool parts. Owner ruling 2026-09-26: the reasoning belongs
   above, always. Covers live partials and settled records alike (one
   projection canon). NOT addressed (owner ruling): the plan_tools
   "missing" claim in the same session - wire-verified 11 tools incl.
   plan_init; GLM confabulation, no mitigation wanted.

2. DOCK CARDS LINGERING AFTER DELIVERY. /api/activity lists jobs with
   status "running", and the status flipped only in settleSteers at the
   DELIVERING TURN'S onFinish - so a subagent whose card was already
   visible in the transcript (marker injected mid-run) stayed in the
   background dock until the teacher finished teaching. Fix: flip the
   job at the moment the result reaches the learner - noteSteerVisible
   (jobs.ts) called from BOTH visibility seams: the marker's message_end
   wire event (live steer injection, ui-stream) and the write-ahead
   persist (the wake's input appends, chat-handler). finalizeJob is
   idempotent; settleSteers/runner-200 re-run it as a no-op, and the
   registry's delivery promise still settles exactly as before.

Tests: 1001/1001 - view-canon hoist pin (trailing [text, think, call]
projects reasoning-first; think-first unchanged), noteSteerVisible unit
pin (flip at the seam, idempotent, handoff no-op), and the mid-run
steering e2e now holds the turn open past the injection and pins the
job flipping done at the marker's message_end WHILE the turn still
streams. Live-verified read-only on the owner's server: the original
session reloads with every reasoning trigger above its message.
This commit is contained in:
amosblomqvist
2026-09-26 05:46:33 +03:00
parent ad0a6e5201
commit b84ab1cdc0
6 changed files with 129 additions and 8 deletions
+18 -1
View File
@@ -50,8 +50,9 @@ import { handleChatRequest } from "../src/server/chat-handler.ts";
import {
abortJob,
deliverJobResult,
registerLiveAgent,
noteSteerVisible,
recoverStaleJobs,
registerLiveAgent,
setJobRespawn,
setTurnRunner,
settleSteers,
@@ -991,6 +992,22 @@ describe("Wave C: live steering handoff + settlement reconciliation", () => {
assert.equal(jobStore.getJob(jobId)!.status, "done");
});
it("noteSteerVisible flips the job at the visibility seam — before any settle (owner ruling 2026-09-26)", async () => {
// The dock fix: the marker reaching the learner's view (wire
// message_end / write-ahead persist) flips the job NOW — the
// delivering turn may keep teaching for minutes. Idempotent with
// the settle-path finalize.
const { chatId, jobId, steer } = setup();
assert.equal(jobStore.getJob(jobId)!.status, "running");
noteSteerVisible(steer);
assert.equal(jobStore.getJob(jobId)!.status, "done", "flipped at the seam, no turn settled");
noteSteerVisible(steer); // the settle-path flip re-runs — no-op
assert.equal(jobStore.getJob(jobId)!.status, "done");
// A probe-handoff (no jobId) is a harmless no-op.
const handoff = buildProbeHandoffSteer(chatId);
noteSteerVisible(handoff);
});
it("the idle wake carries EVERY pending marker in ONE turn", async () => {
const { chatId, steer } = setup();
const job2 = jobStore.createJob({ kind: "research", chatId, toolCallId: "call-2" });