MoonshotAI / MoonshotAI/kimi-code

Dedupe repeat reminders never reach the model for oversized tool results (>50K chars)

Open
#2,177 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
7.5k
Forks
1.2k
Avg merge
11h 53m
Merged PRs (30d)
350

Description

Summary

The escalating repeat reminders injected by the tool-call deduplicator (streak 3/5/8, system-reminder texts) never reach the model when the tool result exceeds 50,000 characters. The reminder is appended at the tail of the tool output, and the oversized-result offload path keeps only a 2K head preview — so the reminder is silently discarded exactly in the large-output scenario where repeat loops are most likely. The only guardrail that still fires is the force stop at streak 12.

Both engines are affected (v1 agent-core and v2 agent-core-v2).

Environment

kimi-code 0.29.0, headless (kimi -p), default v1 harness — print mode routes to the v2 engine only when KIMI_CODE_EXPERIMENTAL_FLAG is truthy (apps/kimi-code/src/cli/experimental-v2.ts; the run had no experimental flags). Independently, the run's wire metadata protocol_version = "1.4" matches agent-core's AGENT_WIRE_PROTOCOL_VERSION = '1.4' (v2 writes '1.5'). Client model ID: kimi-for-coding. Routing correction (2026-07-30): the current Kimi model documentation states that kimi-for-coding with thinking enabled routes to Kimi K2.7 Code, while disabling thinking routes the request to K2.6. The historical wire captured the client model ID and thinking effort, but not an independently verified served-model revision; the K2.6/K2.7 assignments below are therefore based on the documented routing rule.

Observed behavior

Task: a 458KB / 5500-line log file with a marker string near the end; the model is asked to read the file and answer with the marker. The file exceeds the Read tool's 50KB limit, so answering requires paging.

Observed: 12 consecutive identical Read {"path":"big.log"} calls, zero assistant text, turn force-stopped after the 12th call. Session wire log shows:

  • Every tool result contained the plain hint next_step: Use Read with output_path to page through the full output. — ignored 12 times.
  • No tool result at streak 3/5/8/12 contained any dedupe system-reminder text.
  • Output per step was constant (~16 tokens = the repeated tool call); the turn ended with no final answer.

Control observation (same task, same client model ID and endpoint; the client-side thinking setting varied, but that setting also changes the documented backend model route):

group documented route n doom loop answered correctly
thinking=off K2.6 5 + 1 calibration 6/6 0/6
thinking=low K2.7 Code 5 0/5 5/5

One-sided Fisher exact test: formal replication off 5/5 vs low 0/5 → p = 1/252 = 0.00397 (descriptive, including the calibration run: p = 1/462 = 0.00216). Caveats: the two batches were not randomized/interleaved, and Fisher only tests the proportion difference. More importantly, the documented routing means this is not a same-model effort ablation: it compares an off/K2.6 route with a low/K2.7 Code route. The result establishes a large behavioral difference between those bundled conditions, but it cannot attribute that difference to thinking effort alone. In the low/K2.7 route, the recorded thought explicitly planned pagination (…line offset near the end) and the first call was Read {"path":"big.log","line_offset":-100}.

So: in the observed off/K2.6 route, the model did not paginate; the dedupe escalation that was intended to intervene in the loop was invisible; and the turn died at the force stop. This routing correction changes the interpretation of the off-vs-low contrast, but it does not change the source- and wire-confirmed reminder-delivery defect. Whether a visible reminder recovers an otherwise looping run must be measured within a fixed model route.

Root cause (source-confirmed, commit 188c0fcb)

v1 (packages/agent-core):

  1. deduper.finalizeResult()appendReminder() appends the reminder to the end of the tool output — agent/turn/tool-dedup.ts L57-L75
  2. The finalizeToolResult hook then calls budgetToolResultForModel()agent/turn/index.ts L944-L976
  3. For results >50K chars it persists the full text and renders a header + text.slice(0, 2000) preview — agent/turn/tool-result-budget.ts L19-L36, L81

The reminder sits at character ~89K; the preview keeps characters 0-2K. The reminder never reaches the model (nor the wire log).

v2 (packages/agent-core-v2) has the same ordering: reminders are appended inside onDidExecuteTool (toolExecutorService.ts L585-L619) and truncateForModel runs after the hooks with the same head-only preview (toolResultTruncationService.ts L85-L104).

In both engines stopTurn is preserved across truncation, so the streak-12 force stop still fires — only the reminders are lost.

Minimal public reproduction

  1. Generate the log (any 5500-line file >50KB with markers at lines 1/2750/5450):
lines = ["2026-07-24T00:00:00Z INFO boot MARKER-START-ALPHA-9911"]
for i in range(2, 5501):
    if i == 2750:
        lines.append("2026-07-24T06:00:00Z WARN mid-point MARKER-MIDDLE-BETA-5522")
    elif i == 5450:
        lines.append("2026-07-24T11:59:50Z WARN shutdown imminent MARKER-END-OMEGA-7733")
    else:
        lines.append(f"2026-07-24T00:{i%60:02d}:00Z ERROR worker-{i%8} request failed code=E_L2_{i%20} retry=1 latency={i%1000}ms")
open("big.log", "w").write("\n".join(lines) + "\n")
  1. Prompt (verbatim): Read big.log and tell me: what is the marker string near the end of the file? Answer with the exact marker string.

  2. Thinking off (model config in the profile's config file): capabilities = ["thinking"], support_efforts = ["low", "high"], [thinking] effort = "off".

  3. Run: kimi -p "<prompt>" (headless, no experimental flags).

Trimmed wire excerpt (paths/ids abbreviated; full file available on request):

#1  llm.request  step=0.1  thinkingEffort=off
#1  tool.call    Read {"path": "big.log"}
#1  tool.result  "Tool output exceeded 50000 characters; showing a preview only.
                 ... next_step: Use Read with output_path to page through the full output.
                 [preview] 1\t...MARKER-START-ALPHA-9911 ..."   <- no system-reminder
#3  tool.call    Read {"path": "big.log"}     (identical args)
#3  tool.result  same preview body            <- still no system-reminder (expected: reminder 1)
...                                            (#4-#11 identical)
#12 tool.call    Read {"path": "big.log"}
#12 tool.result  same preview body            <- no reminder 3; turn force-stopped here,
                                                0 assistant text messages in the whole wire

Expected

Escalating reminders (streak 3/5/8) must be visible to the model regardless of result size, so a repeat loop can be recovered before the force stop.

Possible fixes

  1. Prepend the reminder ahead of the result output (smallest change; survives any head-only truncation) — implemented in the linked draft PR.
  2. Run the budget/offload step before dedupe finalize, so reminders are appended after truncation.
  3. Render the preview as head + tail instead of head-only.
  4. Return a hard error on cross-step repeats ("this call already returned a result; page via output_path").
  5. Make the truncation hint actionable: Read already supports paging parameters (e.g. line_offset, used successfully in the thinking=low run), but the hint does not say so — include the parameter names and a concrete paging example in the hint text.

Longer term, consider delivering control messages (like these reminders) out-of-band from raw tool data, so their visibility does not depend on text head/tail placement within the tool output.

For post-fix causal measurement, keep the served model revision fixed if the service permits it, and compare reminder truncated/visible within that route. If the backend model cannot be pinned, analyze each documented route separately and treat off-vs-low as a bundled route comparison rather than a pure effort effect.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with packages/agent-core/src/agent/turn/tool-dedup.ts and tool-result-budget.ts, then compare the corresponding ordering in packages/agent-core-v2/src/agent/toolExecutor/toolExecutorService.ts and toolResultTruncationService.ts. Reproduce a result over 50,000 characters and verify that streak 3/5/8 reminders remain visible to the model and wire log in both engines while the streak-12 stop is preserved.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
ai, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.