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
+16 -2
View File
@@ -4,7 +4,15 @@ import { piEngineForSpec, type PiEngine } from "../agent/models.ts";
import { createJobRunner } from "../agent/subagents/runner.ts";
import { createExtractionPipeline } from "../agent/subagents/extraction.ts";
import type { ModelRole } from "../../config.ts";
import { deliverJobResult, registerLiveAgent, setJobRespawn, setTurnRunner, settleSteers, unregisterLiveAgent } from "./jobs.ts";
import {
deliverJobResult,
noteSteerVisible,
registerLiveAgent,
setJobRespawn,
setTurnRunner,
settleSteers,
unregisterLiveAgent,
} from "./jobs.ts";
import { piTurnResponse, runPiTurn } from "./pi/ui-stream.ts";
import { effectiveSettings, stores } from "./stores.ts";
import { searchAvailable, searchProvider } from "./search.ts";
@@ -28,7 +36,7 @@ import {
type TurnInput,
} from "../core/input-events.ts";
import { steerDelivered } from "../core/contracts.ts";
import type { CallUsage } from "../core/contracts.ts";
import type {CallUsage, SteerMessage} from "../core/contracts.ts";
/**
* One chat turn over the server-owned native record. Framework-free (plain
@@ -219,6 +227,12 @@ export async function handleChatInput(
const merged = [...record.messages, ...appends];
try {
stores.chatStore.saveMessages(chatId, merged);
// Dock fix (owner ruling 2026-09-26): persisted markers are visible
// in the record — flip their jobs now (the wake's steers ride this
// write-ahead; the delivering turn may run for minutes).
for (const appended of appends) {
if ((appended as { role?: string }).role === "custom") noteSteerVisible(appended as SteerMessage);
}
} catch (err) {
console.error(`[chat ${chatId}] write-ahead persist failed:`, err);
}
+14
View File
@@ -156,6 +156,20 @@ function jobIdOf(steer: SteerMessage): string | null {
return typeof details?.jobId === "string" ? details.jobId : null;
}
/** Dock bookkeeping: the marker reached the learner's view — flip its job
* NOW instead of waiting for the delivering turn to settle. Called from
* BOTH visibility seams: the marker's message_end wire event (live steer
* injection — the card is on screen while the teacher keeps teaching) and
* the write-ahead persist in handleChatInput (the wake's input appends —
* the card is durable in the record). The registry's delivery promise
* still settles via settleSteers / the runner-200 path; finalizeJob is
* idempotent, so the later flip is a no-op. Owner ruling (2026-09-26): a
* delivered subagent leaves the background dock the moment its result is
* visible, not when the teacher's turn ends. */
export function noteSteerVisible(steer: SteerMessage): void {
finalizeSteer(steer);
}
/** Finalize a delivered steer's job bookkeeping — job steers only. */
function finalizeSteer(steer: SteerMessage): void {
const jobId = jobIdOf(steer);
+7
View File
@@ -1,4 +1,6 @@
import type { Agent, AgentEvent, AgentMessage } from "@earendil-works/pi-agent-core";
import type { SteerMessage } from "../../core/contracts.ts";
import { noteSteerVisible } from "../jobs.ts";
/**
* THE NATIVE WIRE (Wave D, owner-approved 2026-09-26): one agent turn, streamed
@@ -123,6 +125,11 @@ export function runPiTurn(spec: PiTurnSpec): PiTurnHandle {
if (event.type === "message_end") {
const m = event.message as { role?: string; stopReason?: string } | undefined;
if (m?.role === "assistant" && m.stopReason === "aborted") sawAbort = true;
// Dock fix (owner ruling 2026-09-26): an injected steer marker's
// message_end = the card is visible to the learner — flip the
// job's bookkeeping now (the turn may keep teaching for minutes;
// settleSteers re-runs the flip as a no-op idempotently).
if (m?.role === "custom") noteSteerVisible(event.message as SteerMessage);
}
if (event.type === "agent_end") {
closeAll();