Skip to content

fix(agent-core-v2): invalidate streamed attempt state when the llm requester retries below the turn - #3734

Merged
RealKai42 merged 1 commit into
MoonshotAI:mainfrom
kimi-agent-bot:fix/infinite-retry-stream-invalidation
Sep 11, 2026
Merged

fix(agent-core-v2): invalidate streamed attempt state when the llm requester retries below the turn#3734
RealKai42 merged 1 commit into
MoonshotAI:mainfrom
kimi-agent-bot:fix/infinite-retry-stream-invalidation

Conversation

@kimi-agent-bot

Copy link
Copy Markdown
Collaborator

Related Issue

No tracking issue. Reported via the Mangrove platform (task 201346, container 9123219, kimi 0.42.0, KIMI_CODE_INFINITE_RETRY=1): a streaming APIConnectionError: terminated mid-response led to process exit 1.

Problem

With KIMI_CODE_INFINITE_RETRY=1, AgentLLMRequesterService retries failed requests inside its own for (;;) loop (and inside the projection-policy resend path) without telling the turn machine. llm.sent is emitted only once, before the loop. When a stream dies after emitting half a tool call (function part at index=0), the retry reuses stream index 0 while the turn's HistoryAccumulator and ToolCallIdNormalizer still hold the interrupted attempt's state:

  1. the retried stream's new tool call at index=0 hits the stale assignedByIndex[0] mapping and is silently rewritten to the previous attempt's id;
  2. the finished assistant message ends up with duplicate tool call ids [A, A, B];
  3. createMachineTools.beginBatch did not dedupe, flushIfReady drained the same pending entry twice (pending.get(A)undefined on the second read), and runBatch dereferenced entry.input.toolCall.id outside its try block;
  4. the void runBatch(...) call had no rejection handler → unhandled rejection → process exit 1.

A secondary leak: a duplicate pending.set(id, ...) overwrote the first entry, leaving its promise pending forever.

What changed

Root fix — the turn machine now learns about every below-the-turn retry:

  • AgentLLMRequesterService accepts a new onAttemptRetry override and invokes it before each projection-policy resend and before each infinite-retry backoff sleep.
  • createMachineRequester maps that callback to a new llm.request.retrying requester event (LlmRequestEvent / LlmEvent).
  • The turn machine handles llm.request.retrying in thinking with the same action as llm.sent (extracted as discardAttemptStream): roll back and recreate the accumulator and its response normalizer, so each attempt accumulates from zero. Parts of the interrupted attempt already forwarded to the parent machine/UI are not reclaimed — noted in docs/{en,zh}/llm.md.
  • model-requester-impl handles the new event in its event switch (same reset as a re-sent request).

Hardening in agent/loop/machine/tools.ts so a duplicate id can never kill the process again:

  • beginBatch dedupes expected ids;
  • flushIfReady skips missing pending entries instead of a non-null assertion;
  • execute settles a superseded pending entry with an error result instead of leaking its promise;
  • all batch launches go through startBatch, which catches rejections into onBatchError, and the entry-setup lines moved inside runBatch's try.

Tests:

  • test/agent/loop/machineTools.test.ts (new): beginBatch with duplicate ids runs one batch per unique id and settles the superseded call; executor failures reach onBatchError and settle every pending call. Red before the fix (reproduces the exact unhandled TypeError: Cannot read properties of undefined (reading 'input')).
  • test/agent/llmRequester/llmRequesterService.test.ts: unit coverage for onAttemptRetry on both retry paths and the final-error path; an integration test wiring the real service → machine requester → turn machine where attempt 1 streams function(index=0, id=call_a) then fails with APIConnectionError and attempt 2 streams id=call_b, asserting the finished message has tool call ids ['call_b'] (duplicate ['call_a', 'call_a'] before the fix). Fake timers throughout.

agent-core-v2: 368 test files / 6569 tests passed, tsc --noEmit clean, oxlint --type-aware 0 errors (warnings at baseline), check-no-comments and check-import-boundaries OK.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue (external PRs: the issue must have a maintainer's /approve).
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

@changeset-bot

changeset-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 72147a6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@kimi-agent-bot

Copy link
Copy Markdown
Collaborator Author

@codex review

@pkg-pr-new

pkg-pr-new Bot commented Sep 11, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@72147a6
npx https://pkg.pr.new/@moonshot-ai/kimi-code@72147a6

commit: 72147a6

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 72147a69aa

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@RealKai42
RealKai42 merged commit ee2cac1 into MoonshotAI:main Sep 11, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants