CommandCodeAI / CommandCodeAI/command-code
todo_write removal-detection note causes an oscillating resubmission loop with literal-minded models (grok-4.6): case/rewording flips treated as "REMOVED unfinished items", recovery note mandates resubmission, never converges
Nobody has claimed this yet.
- Dominant language
- No language data
- Stars
- 4k
- Forks
- 350
- PR merge metrics
- No merged PRs in 30d
Description
Summary
The todo_write tool replaces the whole list and detects removals by exact content-string comparison (droppedUnfinished() in cli.mjs: old items whose status !== "completed" and whose content is not in the new set). When the model re-cases or rewords its own plan items between calls — e.g. Phase -1: Auto-update check vs Phase -1: Auto-Update Check, both copied from the skill's own headings — every cosmetic difference is classified as a removal of unfinished work.
Each such update appends a recovery note to the tool result:
NOTE: This update REMOVED N unfinished items from the previous list ("…"). Every todo_write call replaces the ENTIRE list. If you did not mean to drop them, call todo_write again with the complete list.
The note instructs resubmission but does not include the previous list verbatim and does not say which spelling is authoritative. A literal-minded model (observed on grok-4.6) interprets the note as "my list was mangled — restore the correct one", resubmits with the other casing, fires the note again, and oscillates indefinitely. In two real sessions the loop consumed 7 and 9 consecutive note firings (8 and 13 todo_write calls) before the user manually interrupted. The model's own reasoning shows the mechanism: "Restoring the full todo list first. Then running the update check…" — the restore always won, the real work never started.
The failure is model-robustness × harness-guidance, not provider-specific: the same model served through both BYOK (xkiro/x-ai/grok-4.6) and the Command Code catalog (xai/grok-4.6) receives and executes tool calls fine on the wire; the loop only emerges when in-session history primes the model to rephrase its plan items.
Expected Behavior
- Cosmetic differences in item
content(casing,&vsand, minor rewording) between consecutivetodo_writecalls should not be classified as removal of unfinished work, or at minimum should not trigger a recovery note that demands resubmission. - The recovery note should give the model a convergent action: either embed the previous list verbatim as the authoritative copy, or state that the current list is acceptable and the model should proceed without resubmitting.
- A model that follows the note's advice ("call todo_write again with the complete list") should exit the loop after one resubmission, not cycle indefinitely.
Actual Behavior
- Every re-cased/reworded resubmission fires the
REMOVED N unfinished itemsnote again (9 firings in one session across 13todo_writecalls; 7 firings across 8 calls in a second session). - The model oscillates between two spellings of the same items, burning turns on
todo_writewhile announcing the next real step in text each time ("I'll run Phase -1 and Phase 0…" — never executed). - The loop survives user steering attempts like "skip the auto-update check": the model applies the instruction but rewords an item in the process (
Auto-Update Check→Auto-Update Check (skipped)), re-triggering the note. - The user must manually interrupt ("stop touching todo_write, just proceed") to break the cycle; the model then proceeds normally, confirming tools and permissions were never the problem.
- One session ended with no user intervention available mid-run and burned turns to
max_turns.
Observed oscillation pairs (exact strings from session transcripts):
Phase -1: Auto-update check↔Phase -1: Auto-Update CheckPhase 0: Schema and template resolution↔Phase 0: Schema & Template ResolutionPhase 1: Five-lens discovery↔Phase 1: Interactive Discovery (5-lens Q&A)
Steps to reproduce the issue
Reproduction requires in-session state — the previous-list store (toolSessionState) is in-memory per process, so two chained cmd -p calls do not reproduce it (old list is empty in the second process). Drive it in a single session:
- Use a model prone to cosmetic rephrasing of plan items (grok-4.6 observed; likely any model with the same habit). Interactive TUI or headless both work as long as all calls happen in one session.
- Get the model to create a todo list whose items it will later re-spell. Reliably triggers when a loaded skill's own headings differ in casing from the model's natural phrasing (e.g.
openspec-plus-proposalmandates Phase -1/0/1/2 with Title Case headings):Use todo_write to create this checklist: [Phase -1: Auto-update check (in_progress), Phase 0: Resolve template (pending), Phase 1: Draft proposal (pending)]. - Instruct a list replacement that drops unfinished items — either explicitly ("replace the list with only item 1, still in_progress") or implicitly ("skip step 1 and mark it complete, then reword the remaining steps to match the skill's exact headings").
- Observe the tool result:
NOTE: This update REMOVED N unfinished items… call todo_write again with the complete list. - The model resubmits with the other casing → note fires again → repeat. Without user interruption the run exhausts its turn budget; with
--output-format jsonyou can count consecutiveREMOVEDfirings directly in the NDJSON stream.
Minimal deterministic-ish driver (single session, both steps in one run):
cmd -p "Do these steps in order: (1) todo_write: [A (in_progress), B (pending), C (pending), D (pending)]. (2) todo_write: replace with [a (in_progress)] — same item, different casing. (3) Continue with the actual task." \
--model <grok-4.6-id> --tools-enable todo_write --max-turns 12
Whether step 2 loops depends on the model noticing the note and "restoring"; forcing several drop cycles in one prompt ("now restore, now drop again") makes the note storm visible in any model that obeys the note literally.
Command Code Version
1.52.0
Operating System
Linux
Terminal/IDE
Unknown
Shell
command-code
Session file (optional)
No response
Fix prompt (optional)
Suggested remediations (any one breaks the oscillation; first is the real fix, others are defense in depth):
- Fuzzy-match removal detection. In
droppedUnfinished(), compare with normalization: casefold + collapse whitespace + strip punctuation (&/and,-/–) before set membership. A resubmission differing only in casing must never count as a removal. - Make the note convergent. When the note fires, append the previous list verbatim:
Previous list was: <json>. If the model is told to restore, give it the exact bytes to restore. Alternatively, add:If the removal was intentional, do NOT call todo_write again — proceed with the task. - Escalation cap. Track consecutive
REMOVEDfirings per session; after 2, replace the resubmission advice with a hard "stop calling todo_write; proceed with the task" instruction. - Deterministic item ids. Items already support an optional
idfield. Prefer id-match over content-match when ids are present, and encourage ids in the tool description so reworded items stay linked.
Additional context
- Environment: Command Code CLI 1.52.0 on Linux; model
xkiro/x-ai/grok-4.6(BYOK, OpenAI-completions wire) andxai/grok-4.6(Command Code catalog) — both exhibit the loop; the loop is absent on short fresh-context runs of the same model, so it is conversational-state dependent, not transport dependent. - Wire-level ruling-out: headless A/B tests on both servings show declared tools (
read_file) and progressively-disclosed tools (web_search, MCP tools) emit and execute correctly; prompt caching differences and odd tool-call IDs on BYOK are unrelated cosmetics. - Session evidence:
~/.commandcode/projects/home-pbtrudel-git-automation-hub/— session13a7a016…(9 REMOVED notes / 13 todo_write calls, user-interrupt recovered it), sessiona14438fa…(7 notes / 8 calls, ended at max-turns-adjacent stall). Transcripts show the model's thinking explicitly planning "restore the full list" every turn. - Interactive-mode note:
todo_writecalls render only as TODOS-panel updates (no tool block in the transcript view), so the loop looks to the user like the model endlessly announcing intent with nothing happening — expensive to diagnose from the TUI alone. - Related but distinct: the loop superficially resembles the known undeclared-tool/strict-wire issue (
toolcalls-issue.mdin the working notes), but that issue is about tools never being emitted; here tool calls fire fine and the failure is purely repeated no-optodo_writeresubmissions.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in cli.mjs at droppedUnfinished() and the todo_write recovery-note path. Run the provided single-session command driver and inspect consecutive REMOVED notes in the NDJSON output. Compare the proposed matching, note wording, and escalation approaches; done means cosmetic rewordings do not create a non-converging resubmission loop and intentional removals remain understandable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100