Repository navigation
Conversation
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
ApprovabilityVerdict: Approved at 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. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (15)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review. 📝 WalkthroughWalkthroughInterrupt 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. ChangesInterrupt Attribution and Thread Navigation
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
Suggested reviewers: Merge Risk: ⚪ Minimal · up to The attribution and thread-navigation changes appear mergeable after normal checks. No supported issue remains that requires a fix before merge. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to 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 Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
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.
Problem
Fixes #16110. When an agent stops another thread's run through
t3_thread_interruptortask_cancel, the stopped thread shows "Run interrupted by user".makeInterruptResultTurnItemhardcoded 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.
run.interrupt,run_interrupt_requestandrun_interrupt_resultget the optionalcreatedByandsenderThreadIdthatmessage.dispatchalready uses.createdBy: "agent"and the calling thread. Client commands getcreatedBystamped bywithCreationProvenance, as messages do, so a client can't claim to be an agent. The request row recordscreatedBy, defaulting touser.runInterruptSenderThreadIdin@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_cancelnow stops the child withthread.stop. This PR givesthread.stopthe same optional fields and carries them into therun.interruptit sends, the request rowmarkStoppedRunwrites when the turn can't be interrupted, and every delegated taskstopDelegatedTasksstops 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.stopis 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.interruptdrops anysenderThreadIdit 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 waymainalready 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
ping -c 150 127.0.0.1, and a second thread's agent calledt3_thread_interrupton it. The stored rows havecreatedBy: "agent"and the second thread assenderThreadId. The divider reads "Run interrupted by an agent", and clicking it opens the second thread.RunExecutionService.test.ts: agent, user and no-request results.OrchestratorMcpToolkit.integration.test.ts:t3_thread_interruptandtask_cancelend to end record agent attribution.CodexReplayFixturesturn_interrupt: an unattributed Stop reads "Run interrupted by user", through the real request lookup.OrchestratorMcpService.test.ts:task_cancelsendsthread.stopwith agent attribution.ThreadStop.test.ts: athread.stopthat 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 spoofcreatedByorsenderThreadId.orchestrationV2Timeline.test.ts: when a row links to the sender.vp test runon those files plusthreadActivity.test.tsandruntimeLayer.test.ts: 241 passed. One test inOrchestratorMcpToolkit.integration.test.tssometimes times out on "mcp-fanout-parent". Unmodifiedmainfails the same way in about 1 of 3 runs.main.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. TwoAntigravityAdapterV2.test.tspath tests fail on macOS temp paths, outside this PR. Typecheck passes in all five packages, exceptexternalLauncher.test.ts, which also fails onmain.task_cancelby hand (covered by the integration test).Before (0.0.46 nightly) / after (this branch):
Before:
After:
Built with Claude Opus 5.5 through Claude Code, running in T3 Code.