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:
+14
-4
@@ -156,18 +156,28 @@ export function projectView(messages: readonly LhMessage[], live?: LiveTurnState
|
||||
|
||||
if (role === "assistant") {
|
||||
const assistant = m as AssistantMessage;
|
||||
const parts: LhViewPart[] = [];
|
||||
// OWNER RULING (2026-09-26): reasoning ALWAYS renders above the
|
||||
// message it belongs to. Providers emit trailing reasoning tokens
|
||||
// (GLM fires a final reasoning burst after the answer), which
|
||||
// lands the thinking block AFTER the text in the persisted
|
||||
// content — presenting arrival order verbatim put the reasoning
|
||||
// UNDER the message. The view canon owns presentation order:
|
||||
// stable-partition each assistant message, reasoning parts first
|
||||
// (their own order kept), then everything else.
|
||||
const rest: LhViewPart[] = [];
|
||||
const reasoning: LhViewPart[] = [];
|
||||
let textN = 0;
|
||||
let reasoningN = 0;
|
||||
for (const block of assistant.content) {
|
||||
if (block.type === "text") {
|
||||
parts.push({ type: "text", id: `${m.id}-t${textN++}`, text: block.text, state: "done" });
|
||||
rest.push({ type: "text", id: `${m.id}-t${textN++}`, text: block.text, state: "done" });
|
||||
} else if (block.type === "thinking") {
|
||||
parts.push({ type: "reasoning", id: `${m.id}-r${reasoningN++}`, text: block.thinking, state: "done" });
|
||||
reasoning.push({ type: "reasoning", id: `${m.id}-r${reasoningN++}`, text: block.thinking, state: "done" });
|
||||
} else if (block.type === "toolCall") {
|
||||
parts.push(toolPartOf(block, live));
|
||||
rest.push(toolPartOf(block, live));
|
||||
}
|
||||
}
|
||||
const parts: LhViewPart[] = [...reasoning, ...rest];
|
||||
if (parts.length === 0) continue; // presentation-only (nothing renderable)
|
||||
const group = lastAssistant();
|
||||
if (group) {
|
||||
|
||||
Reference in New Issue
Block a user