Skip to content

[Bug]: mcode exec fails with limit_exceeded (exit 7) when auto-compaction runs, because the checkpoint consumes --max-steps #514

Open

Description

@7723qqq

Product or interface

CLI - headless

Version

  • Source checkout at commit 5821f7c (package.json version 0.6.5, docs/open-source-status.md calls this the 0.6.3 source preview)
  • Node.js 24.9.0, pnpm 9.12.0
  • Affects packages/tui and packages/local-runtime-v2 as shipped in this repository

Platform

Multiple platforms

OS version and architecture

Originally reproduced on Windows x64. The defect is platform-independent: it is a message-classification bug in shared TypeScript, with no platform, filesystem or native dependency involved.

Issue area

Agent stuck / slow / repeated loop

Steps to reproduce

Use a local OpenAI-compatible fixture so no real provider or account is involved. The key requirement is that automatic context compaction triggers during the turn.

  1. Register a provider with a small context window, which lowers the auto-compaction trigger:
mcode provider add --name ctx --base-url http://127.0.0.1:<port>/v1 \
  --api-format openai-completions --model fixture-model \
  --context-limit 32768 --support-image --use
  1. Run a single-turn task with --max-steps 1:
mcode exec "IMAGE_INPUT_TEST" \
  --model custom_provider:ctx/fixture-model \
  --file fixture.png \
  --timeout 20s --max-steps 1

--file only matters because it is one way to push the request over the compaction trigger. Permission mode is irrelevant; the failure happens before the tool loop. No credentials are needed beyond the fixture key.

The same failure occurs with any prompt when the context is near the trigger, e.g. --context-limit 8192 with no attachment at all.

Expected and actual behavior

Expected: --max-steps 1 bounds how many times the agent calls the model for the user's turn. An internal compaction checkpoint is bookkeeping, not a user turn step, so the run should succeed with one model response and report the real answer.

Actual:

mcode exec limit_exceeded: The run reached its step limit. Increase --max-steps or narrow the task.
$ echo $?
7

Diagnostics confirm the first provider request of the turn is the checkpoint, and the real turn is the second request:

{"kind":"model_phase_started","operationId":"turn_...:model:1",...}
# request 1 -> "Checkpoint control: summarize the preceding conversation now. ..."
# request 2 -> the user's actual turn

Deterministic before/after matrix (local fixture, --max-steps 1):

context limit image attachment checkpoint performed result
32768 yes 1 exit 7
32768 no 0 exit 0
8192 yes 0 exit 7
8192 no 0 exit 7

The 8192 rows fail even without a checkpoint because the request alone exceeds the trigger; either way the reported reason is "step limit", which points a user at --max-steps instead of at context compaction.

Same root cause, four additional user-visible effects (all reachable from the same misclassification, see below):

  1. A lifecycle frame can replace an answer that was already selected: answer is cleared and the retained streamed draft is discarded, so the turn can end with no answer or with the checkpoint text.
  2. Compaction content is projected to ACP clients as agent_message_chunk, so an IDE sees checkpoint prose as assistant output.
  3. Compaction checkpoint text renders as the live assistant reply in the TUI and is written into exported Markdown transcripts.
  4. Chat-channel replies (Feishu/Telegram/WeChat) leak compaction and review text, because the collector only filters the terminal kind === 'compaction' state and misses compaction_start, compaction_failed and all review_* frames.

Root cause

Compaction and review lifecycle frames are persisted with role: 'assistant' because they share the message store with model responses. The two facts are orthogonal — role says who produced the frame, kind says which lifecycle state it is — but the repository never defined a single "is this a model response" predicate. Each consumer re-derived it:

Location Predicate Consequence
packages/tui/src/application/run-coordinator.ts role === 'assistant' lifecycle frame consumes a --max-steps slot
packages/tui/src/application/response-usage.ts kind?.startsWith('compaction') review_* contributes usage and response identity
packages/tui/src/headless/events.ts role === 'assistant' checkpoint text becomes agent_message output
packages/tui/src/acp/updates.ts role === 'assistant' checkpoint text sent to IDE clients
packages/tui/src/tui/controller/projection/turn-live-projection.ts none checkpoint text rendered as assistant body
packages/tui/src/tui/transcript/export.ts role only checkpoint text enters exported Markdown
packages/local-runtime/src/channels/sse-collector.ts kind === 'compaction' three of eight lifecycle kinds filtered
packages/local-runtime/src/api/conversation-message-adapter.ts three literal compaction kinds review_* dropped, then re-read as model output

--max-steps is therefore consumed by an internal auxiliary request, and the CLI reports a limit error for a run that never exceeded its intended budget.

Suggested fix

Introduce one classification module and make every producer type and consumer read it, so no alternate path can drift again. In my working tree this is @mavis/shared/turn-message-class (chosen because packages/tui does not depend on @mavis/agent-core, and @mavis/shared sits at the bottom of the dependency graph):

  • lifecycle kind sets and kind → lifecycle-state tables, replacing three separate if/else ladders
  • isTurnResponseMessage(message) = role === 'assistant' && !isTurnLifecycleMessage(message)
  • MessageKind and RuntimeMessageKind derive from the shared union instead of re-listing literals
  • run-coordinator gates response steps, usage and answer selection through one composed predicate; a lifecycle frame is not a step, but still marks usage incomplete because its auxiliary call spends tokens
  • the other eight call sites ask the same module instead of comparing role or kind

A local fix in run-coordinator alone would stop the exit-7 symptom but leave items 2–4 above in place, which is why I am reporting the shared root cause rather than only the step counter.

Redacted error summary

mcode exec limit_exceeded: The run reached its step limit. Increase --max-steps or narrow the task.

Exit code 7, mapped from limit_exceeded in packages/tui/src/headless/exit-policy.ts. ACP returns stopReason: max_turn_requests for the same underlying condition.

Verification I can and cannot claim

Reproduced deterministically on Windows x64 against a local OpenAI-compatible fixture, and pnpm test:byok fails on this at IMAGE_INPUT_TEST (exit 7) on every run before the fix and passes after it.

Not verified: macOS and Linux were not run at all; no real MiniMax backend, Token Plan or BYOK provider was involved, so this is offline fixture evidence, not live-service acceptance; compaction output quality was not assessed, only whether the checkpoint request occurred; ACP was checked at the projection level only, not against a real IDE client; IM channels were checked at the pure-function level only, not against real services.

Screenshots

No response

Before submitting

  • I have searched existing issues.
  • I have included my version and removed sensitive information.

A working patch exists on a fork branch fix/lifecycle-frame-classification (31 files, +633/−113, four new files, no moved or renamed files, generated release/public-source.json and tsconfig.standalone.json regenerated with their scripts). I understand from CONTRIBUTING.md and the issue-template config that external PRs are not accepted; I am not attaching it as a pull request. I can paste the diff, describe any file in detail, or hand the branch to a collaborator if that is useful.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions