fix: parallel question calls are asked, saved, and answered in the model's call order

Live observation (modal-realism run): the teacher batched two quizzes in
one turn; the dock showed the SECOND question first, so the answering
order was reversed vs the saved record and the transcript cards.

Cause: findPending scanned parts BACKWARD within a message, so with two
input-available quizzes it picked the last one first. No overwriting was
involved — parts are created in call order on both wire and record; the
scan direction alone reversed the interaction order. (pi executes a
parallel batch with starts + result messages in call order; the
execution-end order is completion-racy, which also allowed a millisecond
dock-subject flip mid-turn.)

Fix A — src/core/pending.ts: parts now scan FORWARD within a message
(messages stay newest-first): the FIRST unanswered question is the dock
subject; the second appears only after the first is answered.

Fix B — src/agent/lh-tool.ts: question tools opt into sequential
execution (pi runs the batch in call order when any called tool opts
in), so tool-input-available chunks always arrive in call order and the
dock subject can never flip mid-turn. Question executes are instant.

Pinned in chat-flow.test.ts: a parallel two-quiz batch asserts wire
order (A's available chunk before B's), part persistence order, and the
dock selection sequence (A first, then B after A is answered).

Gates: tsc, lint, build, 307/307.
This commit is contained in:
amosblomqvist
2026-09-12 04:37:05 +03:00
parent 6af7a38684
commit 4baf8385be
3 changed files with 70 additions and 1 deletions
+6
View File
@@ -67,6 +67,12 @@ export function toPiTool(def: LhTool<any, any>): AgentTool<any, any> {
label: def.name,
description: def.description,
parameters: z.toJSONSchema(def.schema as z.ZodType) as never,
// Question tools opt into SEQUENTIAL execution: pi runs the whole
// batch in call order when any called tool opts in, so the
// tool-input-available chunks arrive in the model's call order — the
// dock's subject can never flip mid-turn between two parallel
// questions. Question executes are instant; no latency is lost.
executionMode: def.clientQuestion ? "sequential" : "parallel",
execute: async (toolCallId, args) => {
// The authoring check: structural JSON Schema validation already
// ran in the loop's prepare step; this re-run adds the refinements