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

Abierto
#826 1 comentario 0 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

Lenguaje dominante
Sin datos de lenguaje
Estrellas
4k
Forks
350
Métricas de merge de PR
Sin PR fusionados en 30 d

Descripción

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, & vs and, minor rewording) between consecutive todo_write calls 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 items note again (9 firings in one session across 13 todo_write calls; 7 firings across 8 calls in a second session).
  • The model oscillates between two spellings of the same items, burning turns on todo_write while 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 CheckAuto-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 checkPhase -1: Auto-Update Check
  • Phase 0: Schema and template resolutionPhase 0: Schema & Template Resolution
  • Phase 1: Five-lens discoveryPhase 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:

  1. 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.
  2. 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-proposal mandates 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)].
    
  3. 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").
  4. Observe the tool result: NOTE: This update REMOVED N unfinished items… call todo_write again with the complete list.
  5. The model resubmits with the other casing → note fires again → repeat. Without user interruption the run exhausts its turn budget; with --output-format json you can count consecutive REMOVED firings 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):

  1. 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.
  2. 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.
  3. Escalation cap. Track consecutive REMOVED firings per session; after 2, replace the resubmission advice with a hard "stop calling todo_write; proceed with the task" instruction.
  4. Deterministic item ids. Items already support an optional id field. 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) and xai/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/ — session 13a7a016… (9 REMOVED notes / 13 todo_write calls, user-interrupt recovered it), session a14438fa… (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_write calls 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.md in the working notes), but that issue is about tools never being emitted; here tool calls fire fine and the failure is purely repeated no-op todo_write resubmissions.

Guía de contribución

No hay ninguna guía de contribución indexada para este repositorio

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Línea de trabajo

Comienza en cli.mjs, en droppedUnfinished() y en la ruta de la nota de recuperación de todo_write. Ejecuta el command driver de una sola sesión proporcionado e inspecciona las notas REMOVED consecutivas en la salida NDJSON. Compara la correspondencia propuesta, la redacción de las notas y los enfoques de escalado; terminado significa que las reformulaciones cosméticas no crean un bucle de reenvío que no converge y que las eliminaciones intencionadas siguen siendo comprensibles.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
javascript
Área
cli, tooling
Tipo de issue
Error
Dificultad
4/5
Tiempo estimado
3-5 días
Estado de actividad
Activo
Claridad
Bastante claro
Aptitud para principiantes
52/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.