Skip to content

fix(server): a run stopped by an agent no longer says "by user" - #16136

Open
akbarakma wants to merge 3 commits into
pingdotgg:mainfrom
akbarakma:fix/interrupt-attribution
Open

akbarakma wants to merge 3 commits into
pingdotgg:mainfrom
akbarakma:fix/interrupt-attribution

Conversation

@akbarakma

@akbarakma akbarakma commented Oct 5, 2026 •

Copy link
Copy Markdown

Problem

Fixes #16110. When an agent stops another thread's run through t3_thread_interrupt or task_cancel, the stopped thread shows "Run interrupted by user". makeInterruptResultTurnItem hardcoded that text for every interrupted run, including runs nobody asked to stop (for example an OpenCode turn finished as interrupted after a reconnect).

Change

This follows the fix area in the #16110 triage.

  • Contracts: run.interrupt, run_interrupt_request and run_interrupt_result get the optional createdBy and senderThreadId that message.dispatch already uses.
  • Who asked: both MCP interrupt paths send createdBy: "agent" and the calling thread. Client commands get createdBy stamped by withCreationProvenance, as messages do, so a client can't claim to be an agent. The request row records createdBy, defaulting to user.
  • Result text: the result reads its request and says "Run interrupted by user", "Run interrupted by an agent", or nothing after "Run interrupted" when nothing requested the stop. The pre-provider-start result names the actor too.
  • Web and mobile: when an agent in another thread sent the stop, the divider (web) and the activity row (mobile) open that thread. Mobile reuses the link its subagent notification rows already have. The rule lives once in runInterruptSenderThreadId in @t3tools/shared/orchestrationV2Timeline.

Old rows keep the text they were written with. The new text also reaches later turns' history and handoff context, so models see who stopped the run.

After #16002: task_cancel now stops the child with thread.stop. This PR gives thread.stop the same optional fields and carries them into the run.interrupt it sends, the request row markStoppedRun writes when the turn can't be interrupted, and every delegated task stopDelegatedTasks stops below it. An agent's cancel says "by an agent" all the way down. A user's Stop that cascades to delegated tasks says "by user", with no link. thread.stop is an internal command, so clients can't set these fields.

After #15442: Stop can now finish a stalled run itself, writing the result row in the orchestrator. That row reads the run's stop request too, so a recovered run also says who stopped it. A client's run.interrupt drops any senderThreadId it sends, since only an agent's stop links to a thread. If a run is stopped twice before it ends, the label names the last stop, the same way main already rewrites the request row on a repeat Stop.

Scope and approval

Triaged in #16110 (bug, via-triage). This implements the outlined direction, including the mobile link and the pre-start attribution.

Verification

  • Manual, dev server: a Claude Haiku 4.5 thread ran ping -c 150 127.0.0.1, and a second thread's agent called t3_thread_interrupt on it. The stored rows have createdBy: "agent" and the second thread as senderThreadId. The divider reads "Run interrupted by an agent", and clicking it opens the second thread.
  • Tests:
    • RunExecutionService.test.ts: agent, user and no-request results.
    • OrchestratorMcpToolkit.integration.test.ts: t3_thread_interrupt and task_cancel end to end record agent attribution.
    • CodexReplayFixtures turn_interrupt: an unattributed Stop reads "Run interrupted by user", through the real request lookup.
    • OrchestratorMcpService.test.ts: task_cancel sends thread.stop with agent attribution.
    • ThreadStop.test.ts: a thread.stop that can't interrupt the turn still records who asked.
    • BackgroundWorkStop.integration.test.ts: the 5 stalled-run Stop cases record agent attribution.
    • ThreadManagementService.test.ts: a client can't spoof createdBy or senderThreadId.
    • orchestrationV2Timeline.test.ts: when a row links to the sender.
  • vp test run on those files plus threadActivity.test.ts and runtimeLayer.test.ts: 241 passed. One test in OrchestratorMcpToolkit.integration.test.ts sometimes times out on "mcp-fanout-parent". Unmodified main fails the same way in about 1 of 3 runs.
  • Typecheck passes for contracts, shared, server, web and mobile. Lint shows only warnings already on main.
  • After merging main (through fix(orchestration-v2): let Stop recover stalled runs #15442): the server orchestration and MCP tests (1,965) and the shared and contracts tests (55) pass. Two AntigravityAdapterV2.test.ts path tests fail on macOS temp paths, outside this PR. Typecheck passes in all five packages, except externalLauncher.test.ts, which also fails on main.
  • Not checked: mobile on a device (covered by the shared test and typecheck), and task_cancel by hand (covered by the integration test).

Before (0.0.46 nightly) / after (this branch):

Before:

pr-b-before

After:

pr-b-after

Built with Claude Opus 5.5 through Claude Code, running in T3 Code.

run.interrupt and the interrupt request and result items carry the optional createdBy and senderThreadId that message.dispatch uses. Both MCP interrupt paths record the agent and calling thread, client commands get createdBy stamped like messages, and the result text comes from the request: by user, by an agent, or no attribution when nothing requested the stop. Web and mobile link an agent stop to the thread that sent it.

Fixes pingdotgg#16110
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Oct 5, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 87a20ca

Macroscope's review found this PR approvable — This is a focused, backwards-compatible fix that propagates interrupt provenance through existing server paths and updates web/mobile presentation. The added attribution and thread-link behavior is small, isolated to run interruption, and covered by targeted tests.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: pingdotgg/t3code/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f95f6041-abce-4da1-9e46-b2944325e456
📥 Commits

Reviewing files that changed from the base of the PR and between 3e6b450 and 87a20ca.

📒 Files selected for processing (15)
  • apps/mobile/src/features/threads/thread-work-log.tsx
  • apps/server/src/mcp/OrchestratorMcpService.test.ts
  • apps/server/src/mcp/OrchestratorMcpService.ts
  • apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts
  • apps/server/src/orchestration-v2/Orchestrator.ts
  • apps/server/src/orchestration-v2/ProviderTurnStartService.ts
  • apps/server/src/orchestration-v2/RunExecutionService.test.ts
  • apps/server/src/orchestration-v2/RunExecutionService.ts
  • apps/server/src/orchestration-v2/ThreadManagementService.test.ts
  • apps/server/src/orchestration-v2/ThreadManagementService.ts
  • apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/codex_output.ts
  • apps/web/src/components/chat/V2LifecycleRow.tsx
  • packages/contracts/src/orchestrationV2.ts
  • packages/shared/src/orchestrationV2Timeline.test.ts
  • packages/shared/src/orchestrationV2Timeline.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

Interrupt commands now carry actor and sender-thread attribution through run requests and results. Finalization uses the request to select interruption text. Shared timeline logic identifies a different-thread agent sender, and mobile and web rows can open that thread.

Changes

Interrupt Attribution and Thread Navigation

Layer / File(s) Summary
Attribution contracts and sender resolution
packages/contracts/src/orchestrationV2.ts, packages/shared/src/orchestrationV2Timeline.ts, packages/shared/src/orchestrationV2Timeline.test.ts
Interrupt commands and turn items add optional actor and sender-thread fields. The shared helper returns the sender thread only for an agent-created result from a different thread; tests cover the other cases.
Interrupt command attribution
apps/server/src/orchestration-v2/ThreadManagementService.ts, apps/server/src/orchestration-v2/ThreadManagementService.test.ts, apps/server/src/mcp/OrchestratorMcpService.ts, apps/server/src/mcp/OrchestratorMcpService.test.ts
Thread management forwards optional attribution on interrupt commands. MCP interruption paths set agent attribution and the parent thread as sender. Provenance and dispatch tests check these fields.
Attributed interrupt result finalization
apps/server/src/orchestration-v2/Orchestrator.ts, apps/server/src/orchestration-v2/ProviderTurnStartService.ts, apps/server/src/orchestration-v2/RunExecutionService.ts, apps/server/src/orchestration-v2/RunExecutionService.test.ts, apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts, apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/codex_output.ts
The orchestrator records attribution on interrupt items. Run finalization loads the request and uses it to set result attribution and message. Tests cover agent requests, user Stop, and missing requests.
Interrupting-thread navigation
apps/mobile/src/features/threads/thread-work-log.tsx, apps/web/src/components/chat/V2LifecycleRow.tsx
Mobile and web timeline rows use the sender-thread helper to link an agent interruption to its sending thread.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Low

Sequence Diagram(s)

sequenceDiagram
  participant MCPInterruptPath
  participant Orchestrator
  participant ProviderTurnStartService
  participant ProjectionStore
  participant RunExecutionService
  participant ThreadTimeline
  participant User
  participant ThreadRoute
  MCPInterruptPath->>Orchestrator: dispatch run.interrupt with attribution
  Orchestrator->>ProjectionStore: record run_interrupt_request
  ProviderTurnStartService->>ProjectionStore: load interrupt request
  ProviderTurnStartService-->>RunExecutionService: provide request loader
  RunExecutionService->>ProjectionStore: write attributed run_interrupt_result
  ThreadTimeline->>ThreadTimeline: resolve sender with runInterruptSenderThreadId
  User->>ThreadTimeline: select open interrupting thread
  ThreadTimeline->>ThreadRoute: navigate to sender thread
Loading

Suggested reviewers: juliusmarminge

Merge Risk: ⚪ Minimal · up to 87a20

The attribution and thread-navigation changes appear mergeable after normal checks. No supported issue remains that requires a fix before merge.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 87a20

The inspected changes preserve stop permissions and prevent client-originated stops from being falsely attributed to automated work. Remaining uncertainty concerns attribution during failures and compatibility across versions, rather than a demonstrated privilege expansion.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — Cross-thread interruption targeting predates this PR. The additions expose sender provenance in the target run's history and add navigation within the current environment, rather than a new stop privilege or external destination.

Trust Boundaries and Controls

  • observed — Public intake stamps user provenance. MCP interrupt calls stamp agent provenance and derive sender identity from invocation scope. The inspected cross-thread MCP path retains live-caller ownership and runtime/interaction-mode checks before dispatch.

Resilience and Maintainability Implications

  • inferred — Missing attribution suppresses the new sender link rather than substituting another identity. This limits the inspected failure outcome to incomplete attribution and navigation; it does not establish durable audit-grade provenance.

Hardening Proposals

  • proposed — If interruption provenance becomes an audit or policy input, distinguish an absent request from a failed request read and make attribution loss observable without blocking run termination.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 15 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #16110 requires agent attribution for MCP-requested stops, user attribution for client Stop, no attribution when no stop request exists, and a sender-thread link when available. `OrchestratorMcp…
Out of Scope Changes check ✅ Passed The reviewed changes support issue #16110. Contract updates, request lookup, client provenance enforcement, web and mobile sender-thread links, and their tests implement or verify the requested attrib…
Title check ✅ Passed The title clearly summarizes the main change: correcting the attribution shown when an agent stops a run. It is concise and uses a conventional commit format.
Description check ✅ Passed The description covers the problem, change, scope and approval, and verification. It also reports test results, limitations, and UI evidence.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

main's pingdotgg#16002 moved task_cancel onto thread.stop. thread.stop now carries
the stop's createdBy and senderThreadId into its run.interrupt, the request
row markStoppedRun writes, and every delegated task stopDelegatedTasks stops,
so an agent's cancel still says "Run interrupted by an agent".
main's pingdotgg#15442 finishes a stalled run on Stop and writes its interrupt
result itself. That result now reads the run's stop request too, from the
same command when the session is already dead, or from the store when the
run settles later, so a recovered run says who stopped it.

A client's run.interrupt also drops any senderThreadId it sends, since only
an agent's stop links to a thread.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: A run stopped by an agent says "Run interrupted by user"

1 participant