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

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
@github-actions github-actions Bot mentioned this pull request Sep 11, 2026
7723qqq added a commit to 7723qqq/kimi-code that referenced this pull request Sep 15, 2026
…ment audit

The matrix carried claims that do not match the code: native/glob.rs and
native/bash.rs were named as the Glob and Bash implementations (they are a
55-line matcher helper and timeout constants), read_media.rs and
core_tool_defs.rs were placed outside src/tools/, compaction/micro.rs was
"354 lines + 8 tests" (326 + 7), server/remote_control.rs was 1339 lines
(1317), agent-core-v2 was 1018 files (1544), the reverse RPC set was "10 of
11" (9, one of them dead), the lib suite was 2107 tests (2349), and the claim
that packages/kosong/src/providers/* was deleted is false — two model
metadata modules remain with live consumers.

Two alignment claims were overstated and are now marked as partial: the
injection board omitted the permission_mode reminder entirely, and the ACP
board omitted the non-ACP stopReason values, the missing $/cancel_request
handling, the uncalled terminal/kill, the silently dropped
additionalDirectories, and the Bash rerouting that hardcodes the shell and
passes cwd=None.

Section 6 records the audit method, the delta ratchet, the shipped fixes and
the open work items (permission-mode reminder, MoonshotAI#3734, the eight unverified
behavior commits, MoonshotAI#3532, and the ACP findings).
7723qqq added a commit to 7723qqq/kimi-code that referenced this pull request Sep 17, 2026
Brings the fork's Rust engine up to the behaviors ported from upstream since
0.42.0, and moves the version to 2.0.0 to follow upstream (never bumped
independently).

Ports landed here, each recorded in packages/kimi-agent/ROADMAP.md §6.2 and
scripts/upstream-v2-delta-allowlist.json with its own evidence:

- MoonshotAI#3688 MCP media originals are preserved instead of flattened to a text
  preview: src/mcp/output.rs adds the content-block conversion and the
  result-to-delivery path, FileStore gains content-addressed writing, and the
  attachment reference is resolvable by Read/ReadMediaFile.
- MoonshotAI#3734 streamed attempt state is invalidated when the LLM requester retries:
  a turn-scoped tool-call id ledger, streamed tool-call deltas from all three
  wire protocols, and an attempt guard that rolls back on every failure exit.
- MoonshotAI#3697 steer interrupts a background-task wait — plus the blocking wait it
  needed. The engine's WaitFor had been routed through a synchronous local
  store, so a 5s wait returned in 6ms reporting a timeout; the toolset now
  waits on its own task runner, and a steer signal ends that wait early
  without cancelling the turn or its tasks. WaitFor gains the wait-any form.
- MoonshotAI#3846 an MCP tool call returning 401 flips the server to needs-auth.
- MCP servers connect once per process, starting at startup.
- AI session titles are generated through the managed chat_title tool.
- The abort fix in the preceding commit.

Also refreshes the ROADMAP evidence blocks, the upstream delta allowlist, the
bilingual docs those behaviors surface in, and the TUI rendering for the new
wait status.
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