fix(cli): don't brick the chat REPL when the first turn fails upstream #110

Merged
michal merged 1 commits from fix/chat-repl-thread-brick into main 2026-08-10 21:55:11 +00:00
Owner

A failed first turn (e.g. anthropic 429 before the final frame) made streamOnce resolve '' as the thread id; the REPL stored it and every later message sent threadId: "", which mcpd's z.string().min(1) rejects with HTTP 400 — permanently bricking the session, since no turn could succeed to repair the id.

Fix, two independent layers:

  • streamOnce resolves string | undefined (undefined when no final frame arrived); the REPL keeps its previous thread state on undefined. One-shot mode skips the thread footer when there is none.
  • chatBody never serializes an empty threadId, so a leaked '' can't reach the wire.

7 regression tests in chat-thread-brick.test.ts, including the full REPL chain (failed turn 1 → turn 2 body has no threadId key). CLI suite 726 passed, lint + tsc clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_016dNpnBqyyz9GxfznVcX2sP

A failed first turn (e.g. anthropic 429 before the `final` frame) made streamOnce resolve '' as the thread id; the REPL stored it and every later message sent `threadId: ""`, which mcpd's z.string().min(1) rejects with HTTP 400 — permanently bricking the session, since no turn could succeed to repair the id. Fix, two independent layers: - streamOnce resolves `string | undefined` (undefined when no `final` frame arrived); the REPL keeps its previous thread state on undefined. One-shot mode skips the thread footer when there is none. - chatBody never serializes an empty threadId, so a leaked '' can't reach the wire. 7 regression tests in chat-thread-brick.test.ts, including the full REPL chain (failed turn 1 → turn 2 body has no threadId key). CLI suite 726 passed, lint + tsc clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016dNpnBqyyz9GxfznVcX2sP
michal added 1 commit 2026-08-10 21:55:06 +00:00
fix(cli): don't brick the chat REPL when the first turn fails upstream
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m20s
CI/CD / lint (pull_request) Successful in 2m48s
CI/CD / test (pull_request) Successful in 1m24s
CI/CD / build (pull_request) Successful in 2m40s
CI/CD / smoke (pull_request) Failing after 3m46s
CI/CD / publish (pull_request) Has been skipped
ac5dee906e
Observed live: turn 1 died with an anthropic 429 before the stream's `final`
frame, streamOnce resolved '' as the thread id, the REPL stored it, and every
later message sent `threadId: ""` — rejected by mcpd's z.string().min(1) with
HTTP 400. Permanently stuck: no turn could succeed again, so no `final` frame
could ever repair the id.

Two independent layers:
- streamOnce now resolves `string | undefined` — undefined when no `final`
  frame arrived — and the REPL keeps its previous thread state on undefined
  instead of overwriting it. One-shot mode skips the `(thread: ...)` footer
  when there is none to report.
- chatBody refuses to serialize an empty threadId at all, so even a leaked ''
  can never reach the wire.

Regression cover in chat-thread-brick.test.ts (7 tests), including the full
REPL chain: failed turn 1 → turn 2 body carries no threadId key. The
assertions are the direct inverse of the old behavior, so they fail pre-fix
by construction.

CLI suite 726 passed, lint clean, tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dNpnBqyyz9GxfznVcX2sP
michal merged commit db38de7e09 into main 2026-08-10 21:55:11 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: michal/mcpctl#110