Skip to content

fix(supervisor): resume a conversation only once Claude Code has it on disk - #67

Merged
httpsphl merged 1 commit into
mainfrom
fix/resume-before-first-turn
Sep 30, 2026
Merged

httpsphl merged 1 commit into
mainfrom
fix/resume-before-first-turn

Conversation

@httpsphl

Copy link
Copy Markdown
Owner

The problem

A bot restarted before its first turn came back with --resume for a conversation Claude Code had never saved. Seen on 2026-09-30 with a dev daemon and Claude Code 2.1.284: bots.create, then bots.setPermissionMode 20 ms later.

  • the restart for the new mode killed the first process after 50 ms;
  • the second one started with --resume <session id> and Claude Code answered No conversation found with session ID: <uuid>;
  • the chat got a turn item with error: "error_during_execution" before the owner had sent anything;
  • the bot only came back, in a new conversation, about 6 s later.

The same happens to any bot whose mode or model is changed before its first message, since the daemon remembered the session id at launch.

What Claude Code does (2.1.284, recorded in spec 19)

  • --resume of a session it does not have: the stderr line above, then on stdout, with no system/init before it, a result with subtype: "error_during_execution", is_error: true, num_turns: 0, duration_ms: 0, total_cost_usd: 0, errors: ["No conversation found with session ID: <uuid>"] and no terminal_reason. The process exits by itself with code 1.
  • The conversation is written when its first turn begins, not before:
The first process ended --resume afterwards
with no message, after 6 s not found
killed at the first system/init not found
killed at the user replay (0.6 to 1.4 s after the init) resumed
killed at the first assistant resumed
killed at the result resumed

So system/init is too early to call a session resumable. The replay of the turn's message is the first event that is late enough.

The fix

  • When a session is remembered (spec 7.3, 8.1): from the user replay of a turn, no longer from system/init, and never at launch. Until a conversation has had a turn, a restart starts again with --session-id and a new id.
  • A conversation Claude Code lost (its transcript cleaned up or removed): that result is recognised by its subtype and the start of the error text. It is not a turn, so it adds no chat item and closes no pending turn, and it marks the next start as a new conversation however long the process takes to exit. If the text ever changes, the daemon falls back to what it did before (a failed turn, then a fresh start if the process dies within fresh_start_if_dies_within_s).
  • A conversation left behind leaves bots.session_id too. Before, bots.restart {fresh: true} followed by a daemon restart went back to the old conversation, because the database still had its id until the new one had a turn.

crates/botloftd/src/supervisor/session.rs and crates/botloftd/src/chat/session.rs are new; events.rs gets a little shorter.

Tests

crates/botloftd/tests/sessions.rs, on FakeRuntime. All four failed before the fix:

  • a bot restarted before its first turn starts a new conversation (the reported case);
  • a conversation is resumed once a turn began in it, not at system/init;
  • a conversation Claude Code lost is replaced without a failed turn, even when the process takes longer than fresh_start_if_dies_within_s to exit;
  • a fresh restart leaves the old conversation for good.

Four older tests that expected --resume for a bot that never had a turn now give the bot a turn first.

cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings and cargo test --workspace pass. The app is not touched.

With the real Claude Code

Done with a dev daemon on a scratch home and Claude Code 2.1.284 (Haiku):

  1. bots.create, then bots.setPermissionMode 20 ms later: the log shows the second bot started with resumed=false, no stderr line, and the chat stays empty.
  2. One message, then another mode change: resumed=true, and the bot repeated the word from its earlier turn.
  3. Daemon stopped, the bot's transcript moved out of %USERPROFILE%\.claude\projects\..., daemon started: No conversation found in the debug log, no new item in the chat, the next bot started with resumed=false, and bots.session_id empty.

Merging with #63

This PR is on main. A trial merge with feat/bot-effort-context (#63, and #64 and #66 on top of it) has one conflict and two tests to adjust, whichever merges second:

  • crates/botloftd/src/supervisor/spawn.rs: keep both sides, without the sessions.insert(bot.id.clone(), launch.session.clone()); line of feat(daemon): effort per bot, the context in use and compacting the conversation #63.
  • tests/bot_effort.rs, an_idle_bot_restarts_on_its_new_effort_in_the_same_conversation: let session = s.turn(&first).await; instead of reading --session-id.
  • tests/compaction.rs, a_process_that_ends_takes_its_compaction_and_a_new_conversation_its_size: s.turn(&first).await; before the first holds(...).

With those three edits the merged tree passes cargo test -p botloftd. The other open PRs merge cleanly with this one.

…n disk

A bot restarted before its first turn (a mode, model or settings change
right after it was created) came back with `--resume` for a session Claude
Code had never saved. Claude Code answered "No conversation found", the chat
got a failed turn nobody asked for, and the bot lost a few seconds before
the fresh start.

Seen with Claude Code 2.1.284 (spec 19): the conversation is written when
its first turn begins. A process killed at the first `system/init` cannot be
resumed; one killed at the replay of the message can. So the session id is
now remembered from the `user` replay instead of `system/init`, and never at
launch: until then a restart uses `--session-id` with a new id.

When Claude Code does not have the conversation it was told to resume, its
`result` (no turn before it, `error_during_execution`, "No conversation
found" in `errors`) is no longer a turn in the chat. It marks the next start
as a new conversation, however long the process takes to exit.

A conversation a new one takes the place of also leaves `bots.session_id`,
so a daemon restart does not resume it: before, `bots.restart {fresh}`
followed by a daemon restart went back to the old conversation.

Spec 7.3, 7.4, 8.1 and 19 say the new rule.
@httpsphl
httpsphl merged commit 4fdf715 into main Sep 30, 2026
3 checks passed
@httpsphl httpsphl mentioned this pull request Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant