feat: Wave C - pi-native steer/follow-up delivery

Delivery now rides pi's OWN follow-up queue when a run is live, and a
carry-everything idle wake otherwise. One registry, two vehicles;
claimSteers and continuation coalescing are deleted (owner rulings,
2026-09-26: follow-ups only; one queue; the wake is the only appender).

THE LIVE RUN (vehicle 1): the chat-handler registers the streaming
turn's agent (registerLiveAgent, next to registerTurn); the pump hands
every eligible queued marker to agent.followUp() - pi polls that queue
when the run would otherwise stop, injects the markers
(message_start/message_end on the live wire), and CONTINUES the same
run. followUpMode "all" joins every handed marker in one continuation
turn. The question stopper runs before the follow-up poll, so an open
quiz is never overridden - a handed marker strands at settlement and
waits (today's pacing). probe-handoffs never take this path (the
handoff must START the teacher's run - a record-level branch).

SETTLEMENT RECONCILIATION: settleSteers(chatId, persisted) at onFinish
(after unregisterLiveAgent) resolves+flips entries the run delivered
and returns stranded ones to pending - the registry stays the truth;
the dead agent's queued copy is abandoned. This is what makes the
handoff safe: pi owns no retry for a stranded follow-up.

THE IDLE WAKE (vehicle 2): waitForIdle (unchanged gates) then ONE turn
carrying EVERY pending marker as its input appends - dequeue-at-fire
(the Wave B regression lesson: the wake run's own follow-up poll must
never see its own cargo; a 409 pushes the cargo back and retries).

CLIENT: applyWireEvent now upserts non-assistant messages by id, so
in-run injected markers render live instead of dropping until settle.

Tests: jobs 44/44 (live handoff, settlement stranding, handoff-waits,
one-wake-carries-all); chat-flow 23/23 (the double-append pin drives
the real pump through the new input); session-core 28/28 (custom-role
wire appends). Full suite 997/997, tsc/lint/build green.

Live smoke (PORT=3004): research job delivered mid-probe - parked
behind the dangling questions exactly per semantics, landed once on
the open gates, teacher folded the brief in and re-dispatched; the
second (good) result rendered the full 10-source brief card and the
lesson continued from it. No duplicate ids anywhere in the record.

Known gap flagged to owner (NOT from this change): the server accepts
malformed answer events via raw API (answers without the answers[]
shape) and AnsweredCard then crashes the page render - surfaced by the
smoke's scripted answers, repaired by hand in the scratch record.
This commit is contained in:
amosblomqvist
2026-09-26 03:46:09 +03:00
parent 7f3d8aadcc
commit 7f51259fc6
7 changed files with 426 additions and 265 deletions
+40
View File
@@ -394,6 +394,46 @@ describe("send (one input event, verbatim consumption)", () => {
await done;
});
it("Wave C: markers injected mid-run (custom role, follow-up delivery) append to the tail by id", () => {
// A never-settling fetch: the send only needs to flip the model into its
// streaming status synchronously — its POST floats (never rejects) while
// the injected wire events are asserted.
const m = model({ fetch: () => new Promise<Response>(() => {}) });
m.adoptRecord([
{ id: "u1", role: "user", content: "hello", timestamp: 1 } as never,
] as never);
void m.send({ type: "text", text: "hello" });
// pi emits message_start + message_end for an injected follow-up marker
// (no streaming partials) — both must UPSERT the same tail slot by id.
const marker = {
id: "steer_job_x_1",
role: "custom",
customType: "subagent-steer",
content: "The research result arrived.",
display: true,
details: { jobId: "job_x", kind: "research" },
timestamp: 2,
};
m.applyWireEvent({ type: "message_start", message: marker } as never);
m.applyWireEvent({ type: "message_end", message: marker } as never);
const state = m.getState();
const ids = state.messages.map((v) => v.parts.length);
assert.ok(state.messages.length >= 2, "the marker produced a view message");
const tail = state.messages.at(-1)!;
assert.equal(tail.role, "user", "a steer marker projects as a user-role view message");
assert.ok(
tail.parts.some((p) => p.type === "data-subagent-artifact"),
"the marker renders its artifact part (SubagentSteerCard)",
);
// A re-delivery of the same id (message_end replay) must NOT duplicate.
m.applyWireEvent({ type: "message_end", message: marker } as never);
const again = m.getState();
assert.equal(again.messages.length, state.messages.length, "same id upserts, never appends a second copy");
});
it("a provider error rides VERBATIM (owner ruling) — no masking anywhere", async () => {
globalThis.fetch = (async () =>
sseResponse(