Skip to content

fix(orchestrator): scope Stop button to the active thread#46

Merged
yourbuddyconner merged 1 commit into
mainfrom
fix/stop-button-channel-scope
Jun 12, 2026
Merged

fix(orchestrator): scope Stop button to the active thread#46
yourbuddyconner merged 1 commit into
mainfrom
fix/stop-button-channel-scope

Conversation

@yourbuddyconner

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #45. The Stop button issued a runner-wide abort regardless of which thread the user was viewing. Pre-#45 that was harmless because the orchestrator ran one turn at a time; with cross-thread concurrent drain enabled, the same global abort kills every in-flight turn.

The runner's `handleAbort` already supports per-channel scoping (`packages/runner/src/prompt.ts:2428`) — it just wasn't receiving the channel info from the client/DO.

Changes

  • `packages/client/src/components/chat/chat-container.tsx` — `handleAbort` passes `('thread', activeThreadId)` to `abort()` instead of an empty call.
  • `packages/worker/src/durable-objects/session-agent.ts` (WebSocket `abort` case) — forwards `msg.channelType` / `msg.channelId` to `handleAbort`.
  • `packages/worker/src/durable-objects/session-agent.ts` (HTTP `/prompt` interrupt path) — derives `abortChannelType`/`abortChannelId` from `body.threadId` (or `body.channelType`/`body.channelId`).

The `/stop` slash command at `session-agent.ts:1422` and the broader empty-args `handleAbort()` paths are intentionally left as session-wide aborts.

Test plan

  • `pnpm vitest run packages/worker/src/durable-objects/session-agent.test.ts packages/worker/src/durable-objects/prompt-queue.test.ts packages/worker/src/services/orchestrator.test.ts packages/worker/src/routes/sessions.test.ts packages/worker/src/routes/slack-events.test.ts packages/worker/src/routes/channel-webhooks.test.ts packages/worker/src/services/session-cross.test.ts packages/worker/src/lib/cron.test.ts packages/worker/src/routes/threads.test.ts` — 234 passed.
  • `pnpm typecheck` (workspace root) and `cd packages/client && pnpm build` (client production build).
  • Two new tests in `session-agent.test.ts`: WebSocket abort forwards channel scope; HTTP `/prompt` interrupt with `threadId` scopes the runner abort.
  • Manual: open two concurrent thread turns on one orchestrator session, click Stop on one — confirm only that thread aborts.

The Stop button issued a runner-wide abort regardless of which thread the
user was viewing. Pre PR #45 this was harmless because the orchestrator
only ran one turn at a time. With cross-thread concurrent drain enabled,
the same global abort kills every in-flight turn.

Three call paths needed channel scoping:
- packages/client/src/components/chat/chat-container.tsx: pass
  ('thread', activeThreadId) to abort() instead of an empty call.
- packages/worker/src/durable-objects/session-agent.ts WebSocket 'abort'
  case: forward msg.channelType / msg.channelId to handleAbort.
- packages/worker/src/durable-objects/session-agent.ts HTTP /prompt
  interrupt path: derive abortChannelType/abortChannelId from body.threadId
  (or body.channelType/body.channelId) so /stop-style HTTP interrupts also
  target the right thread.

The runner's handleAbort already supports per-channel scoping
(prompt.ts:2428) — it just wasn't receiving the channel info.

Regression tests verify both the WebSocket and HTTP paths send a scoped
abort frame.
@github-actions

Copy link
Copy Markdown

Preview deployment: https://pr-46.dev-valet-turnkey-client.pages.dev

@yourbuddyconner yourbuddyconner merged commit f6fb4df into main Jun 12, 2026
4 checks passed
yourbuddyconner added a commit that referenced this pull request Jun 12, 2026
…r their own stop button

After PR #46 the stop button correctly aborted only the active thread, but
switching to a still-running parallel thread showed no stop button. Root
cause: the chat state stored a single global agentStatus, so whichever
thread emitted the most recent agentStatus event clobbered every other
thread's busy indicator — and the abort handler optimistically cleared
that single field session-wide.

Track agent status per-thread:
- ChatState now carries threadStatuses: Record<threadId, {status, detail}>.
- The agentStatus WebSocket handler updates threadStatuses[msg.threadId]
  alongside the legacy global fields.
- The abort handler clears only the targeted thread's slot when scoped to
  ('thread', channelId); other threads' busy indicators stay intact.
- chat-container derives activeThreadStatus from threadStatuses[
  activeThreadId] (with a fallback to the legacy agentStatus for sessions
  whose first event predates this map), and computes isAgentActive /
  isAgentThinkingInThread from that. MessageList now receives the thread-
  scoped status rather than the global one.

Net effect: clicking Stop on thread A leaves thread B's stop button visible
while thread B is still running.
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