tool-call id uniquifier: the Lacan fix (owner-approved 2026-10-04)
DeepSeek via OpenRouter returns tool-call ids that reset every response — a one-call-per-turn session names every call call_0. Everything keys on toolCallId (the fold's call↔result join, the pending gate, answer routing, the option-shuffle seed), so the Lacan session corrupted: the quiz answer overwrote the ask card, the ask question vanished, and the order scrambled. Record itself was always intact. - src/server/pi/call-ids.ts (new): ToolCallIdUniquifier at the wire boundary — colliding ids remint once (orig~2, ~3…), the same mints at settlement; tables key by message OBJECT IDENTITY (streaming messages carry no ids; the message_end object is reference-identical to the state object). History never rewrites; providers see paired ids, which is protocol-legal (pi synthesizes ids for providers that send none). - ui-stream: runPiTurn remaps events + settles through the uniquifier (spec.usedToolCallIds, seeded by chat-handler from the record). - view.ts: findToolPart scans newest-first — pre-fix duplicate-id records (the existing Lacan session) heal on next render. - tests: 9 unit pins (call-ids.test.ts) + the two-call_0 e2e (chat-flow): ask answered as ask, quiz as quiz, ids unique in the record.
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
unregisterLiveAgent,
|
||||
} from "./jobs.ts";
|
||||
import { piTurnResponse, runPiTurn } from "./pi/ui-stream.ts";
|
||||
import { collectToolCallIds } from "./pi/call-ids.ts";
|
||||
import { effectiveSettings, stores } from "./stores.ts";
|
||||
import { searchAvailable, searchProvider } from "./search.ts";
|
||||
import { providerKeyPresent } from "./keys.ts";
|
||||
@@ -379,6 +380,12 @@ export async function handleChatInput(
|
||||
registerLiveAgent(chatId, constructed);
|
||||
const handle = runPiTurn({
|
||||
agent: constructed,
|
||||
// THE LACAN FIX (2026-10-04): seed the uniquifier with every
|
||||
// tool-call id the record already holds — a provider that resets
|
||||
// ids per response (DeepSeek) then gets fresh unique ids for this
|
||||
// turn's calls at the wire boundary, on the wire, in the record,
|
||||
// and in the next turn's context alike.
|
||||
usedToolCallIds: new Set(collectToolCallIds(merged)),
|
||||
// Stream-level failures are provider/transport internals — masked,
|
||||
// like the SDK defaults. Tool-input validation messages never
|
||||
// route through here: they ride the error-result path and reach the
|
||||
|
||||
Reference in New Issue
Block a user