Skip to content

Reactivate threads when a provider resumes work on a completed turn - #1697

Open
SawyerHood wants to merge 2 commits into
mainfrom
bb/investigate-1646-thr_8unj696hjt
Open

Reactivate threads when a provider resumes work on a completed turn#1697
SawyerHood wants to merge 2 commits into
mainfrom
bb/investigate-1646-thr_8unj696hjt

Conversation

@SawyerHood

Copy link
Copy Markdown
Collaborator

Fixes #1646

Problem

Codex can keep streaming root work (commandExecution, fileChange, agentMessage, reasoning) on a turn it already reported with turn/completed, then complete that turn a second time. The store proves this: BB rejects turn-scoped content for a turn with no stored turn/started (409), and the idle-gap items in the report were persisted, so they used an already-started turn id. BB only derived thread status from turn/started and turn/completed, so:

  • the thread showed idle (no spinner, bb thread wait --status idle returned early) while commands ran;
  • the daemon held no active turn, so thread/stop was a no-op and idle-session reaping could target the busy provider session.

Change

  • Server (apps/server/src/internal/events.ts): a root item/started on an idle thread for a turn that already has a stored turn/completed, with no system/thread/interrupted after that completion, applies run.started again. The next turn/completed settles the thread to idle as before.
  • Daemon (packages/agent-runtime/src/runtime-turn-state.ts, runtime.ts): RuntimeTurnState reopens the active turn from that work so stop/steer target it, and the provider session is marked busy again.

No wire format change; no HOST_DAEMON_PROTOCOL_VERSION bump.

Tests

  • apps/server/test/internal/internal-event-append-ownership.test.ts: reactivation on late provider work; stopped thread stays idle.
  • packages/agent-runtime/src/runtime-turn-state.test.ts: reopen from root work; ignore delegated child work and active turns.
  • pnpm exec turbo run typecheck/test --filter=@bb/agent-runtime --filter=@bb/server pass (one pre-existing unrelated failure in internal-skill-trees.test.ts from a local umask file mode).

AGENT GENERATED: by Claude Opus 5

Codex can keep streaming root work (commands, file changes, messages)
on a turn it already reported as completed, and then complete that turn
a second time. BB only tracked status from turn/started and
turn/completed, so the thread showed idle while commands ran, and
thread/stop had no active turn to interrupt.

- Server: item/started on an idle thread for a completed turn, with no
  stop after that completion, applies run.started again.
- Daemon: RuntimeTurnState reopens the active turn from that work so
  stop/steer target it, and the provider session is not reaped as idle.

Fixes #1646

Co-Authored-By: Claude <noreply@anthropic.com>
@bb-slop-cop

bb-slop-cop Bot commented Aug 17, 2026

Copy link
Copy Markdown

🚨 SLOP COP 🚨 · review

I am SlopCop. I am reviewing this pull request for security, code quality, performance, architecture, and test coverage.

Comment thread apps/server/src/internal/events.ts Outdated
and(
eq(storedEvents.threadId, args.threadId),
eq(storedEvents.type, "system/thread/interrupted"),
gt(storedEvents.sequence, turnCompleted.sequence),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 slopcop/review — A later completion can bypass a prior stop.

resolveReopenedTurnId uses the latest turn/completed as the lower sequence bound. The order turn/completed → stop → later turn/completed → root item/started moves that bound past the stop. The thread then returns to active after the user stopped it. A temporary route test reproduced active instead of idle. Please compare the stop with the turn start or request boundary. Please add this event order as a regression test.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 2ef9350: the reopen guard now checks for any stop after the turn request that produced the turn (same lower bound as hasThreadStopBeforeTurnStarted), with a regression test for completed → stop → completed → item/started.

// that work so stop/steer can still target it.
if (
event.type === "item/started" &&
event.item.type !== "userMessage" &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 slopcop/review — A background task can reopen and strand the turn.

This predicate excludes user messages and parented items. It still accepts an unparented backgroundTask. The runtime permits background tasks to outlive a turn while the thread stays idle. Their item/backgroundTask/completed event does not clear this active-turn state. A temporary unit test reproduced turn-1 instead of null. Please exclude background tasks in the server and runtime predicates. A shared domain helper can keep both rules equal.

@bb-slop-cop bb-slop-cop Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 SLOP COP 🚨 · review

Plain English summary: This change makes a completed thread active again when its provider sends more work. It also restores the active turn for stop and steer actions.

I found two high-impact correctness defects. Both defects can leave the thread active when it should stay idle.

  1. A later completion can move the stop boundary past an earlier user stop. The next root item then reactivates the stopped thread.
  2. An unparented background task can reopen a completed turn. Its background completion never closes that reopened turn.

The first defect also affects the user-control security boundary. I found no host ownership or authentication defect.

The new database queries use targeted indexes. The runtime additions use constant-time map operations. I found no performance defect.

The server and runtime duplicate the root foreground-item rule. A shared domain helper would prevent the two rules from drifting.

Validation:

  • All GitHub checks passed.
  • The clean server route test passed all 12 tests.
  • The clean runtime state test passed all 12 tests.
  • A new stop-order test failed with active instead of idle.
  • A new background-task test failed with turn-1 instead of null.
  • git diff --check passed.

A browser test does not reach this daemon-only event route. The route and runtime tests exercise the changed behavior directly.

I posted both defects as inline comments. This is a comment-only review, as required.

- The reopen guard now checks for any stop after the turn request that
  produced the turn, so a later completion cannot move the boundary past
  a user stop.
- Server and daemon share isTurnReopeningWorkItem, which excludes user
  messages, delegated child items, and background tasks that can outlive
  a turn.

Co-Authored-By: Claude <noreply@anthropic.com>
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.

Codex thread shows idle (no spinner) while actively running commands

1 participant