Replies: 1 comment
|
The prevention and historical-repair paths should stay separate here. Normalizing an empty adapter ID before persistence prevents new damage. Repairing an existing log requires one deterministic replacement across the complete logical identity set: the assistant For an affected operator, stop the writer first, preserve the original artifact, decompress into a separate copy when needed, then locate the first invalid event and repair only a disposable copy with full replay validation. I documented the relevant validation and recovery boundary here: https://sandbaseai.github.io/deepseek-harness-handbook/session-log-durability.html Canonical runbook: sandbaseai/deepseek-harness-handbook#22 |
Uh oh!
There was an error while loading. Please reload this page.
Summary
A degenerate model/adapter can emit a tool call with an empty id/name (
tool-call-delta id:""or ablock-endpayload carryingid:""). The harness persists it verbatim:BlockAssembler.assemble()falls back only viapartial.toolCallId ?? CallId(...)—??does not catch""(onlyundefined), and theblock-endpath (if (partial.block) return partial.block) bypasses the fallback entirely;AgentLoop.step/executeToolCallswriteblock.idstraight intotool/call callId:""andtool/result message.source.callId:"";assertMessageEventShaperequires a non-emptytool/resultsource.callId→ the whole session log is refused (session event at seq N message must have tool source). A single bad event makes every previously recorded message unreachable.Repro context
A polyglot provider streamed
tool_callwith emptyidandname; the executor answeredunknown tool ""; that emptycallIdwas durably recorded; every subsequent load of the session failed withSessionPersistenceCorruptionError. The same corruption hit several sessions on our deployment.Suggested fix (3 layers)
BlockAssembler.assemble()(packages/llm/llm/lib/types/assembler.js): fall back on empty string too:id: partial.toolCallId && partial.toolCallId.length > 0 ? partial.toolCallId : CallId(\call-${index}`)andname: partial.toolCallName && partial.toolCallName.length > 0 ? partial.toolCallName : 'unknown-tool'`.BlockAssembler.blocks(): sanitize at the exit so directblock-endpayloads are normalized as well (they currently skip theassemble()fallback).AgentLoop.step(packages/core/agent-loop/lib/index.js): normalizetool-callblocks before writingassistant/message, so the assistant message,tool/callandtool/resultall share one non-empty call id.This prevents new bad data. Existing corrupted logs still need a one-off repair pass that assigns deterministic ids (
call-<index>or similar) to the empty ids.Happy to open a PR with the exact diff if useful.
All reactions