feat: Wave C pivot - steer markers, not follow-ups

Owner correction (2026-09-26, later): a real lesson is ONE continuous
teach->quiz->teach run - there are no run-would-stop moments mid-lesson,
and the run ends that DO exist are quiz stopper exits where delivery
must not fire. Subagent results must land BETWEEN messages INSIDE the
run, never splitting a toolCall from its toolResult.

Vehicle 1 now hands markers to agent.steer() - pi's STEERING queue:
the loop drains it at TURN BOUNDARIES (after a completed assistant
message AND its tool results - the bonded pair survives injection) and
again at RUN START (a continuation drains before its first LLM call),
then CONTINUES the run with the marker in context.
steeringMode "all" replaces followUpMode.

registerLiveAgent moved BEFORE runPiTurn and now hands pending markers
itself: the start poll drains the queue SYNCHRONOUSLY inside the
runPiTurn call, so a later registration would miss it. A marker
stranded by a quiz-terminated run therefore rides the ANSWER run's
first LLM call - the native replacement for the old continuation
coalescing.

Settlement reconciliation, the idle wake, probe-handoff exclusion,
and the registry-as-truth design are UNCHANGED; stranding is now
exactly the stopper-exit case.

Tests: jobs 45/45 (fakeAgent.steer; new pin - registration steers
pending markers into the new run, parked wake never fires); chat-flow
24/24 (new e2e through the real composition - a gated mid-run turn, a
marker delivered while the turn streams, record shape
[call, result, MARKER, response] with the marker adjacent to the
completed unit, the model SEES the marker in its next prompt, zero
wakes); full suite 999/999, tsc/lint/build green.

Live smoke (PORT=3004): research dispatched in the probe turn parked
behind the dangling quiz; on the quiz answer the marker injected at
the answer run's START poll - record [answer result, marker, teacher
response], job flipped done/delivered at settlement, brief card
rendered with its sources toggle.

Plan doc: docs/PI-NATIVE-MIGRATION-PLAN.md gains the pivot section.
Deferred (owner list): thinking blocks rendering after messages;
vanishing tool-call output tokens.
This commit is contained in:
amosblomqvist
2026-09-26 04:31:07 +03:00
parent 7f51259fc6
commit ad0a6e5201
6 changed files with 239 additions and 55 deletions
+9 -5
View File
@@ -315,6 +315,15 @@ export async function handleChatInput(
// is just the FIRST subscriber (a view). A client disconnect can no
// longer abort the turn; the remote kill is the abort channel, and
// late views reattach via GET /api/chats/[id]/stream.
// Wave C (owner correction, 2026-09-26 — steering): register the live
// agent BEFORE runPiTurn — the loop's start steering-poll drains the
// queue SYNCHRONOUSLY inside that call, so a later registration would
// miss it. Registration also steers any pending registry markers into
// this run: a marker parked behind a just-answered quiz rides THIS
// run's first LLM call (the native replacement for continuation
// coalescing). Settlement still cannot beat the flow below: it needs
// a streamed response, which never happens inside the constructor.
registerLiveAgent(chatId, constructed);
const handle = runPiTurn({
agent: constructed,
// Stream-level failures are provider/transport internals — masked,
@@ -373,11 +382,6 @@ export async function handleChatInput(
// runPiTurn call is race-free: the agent loop only ticks in later
// microtasks, so settle (and thus unregister) cannot beat us here.
registerTurn(chatId, handle);
// Wave C: the pump hands queued steer markers to THIS run's agent
// (pi's own follow-up queue) - same race-freedom as registerTurn:
// the loop only ticks in later microtasks, so settlement cannot
// beat the registration.
registerLiveAgent(chatId, constructed);
// The POST response: just this caller's view onto the running turn —
// identical wire dialect to the reattach stream.
return piTurnResponse(handle);