Skip to content

fix: keep chat Stop and Close independent of receipt bookkeeping - #26461

Closed
brennanb2025 wants to merge 17 commits into
mainfrom
brennanb2025/ssh-chat-stop-close-never-gated
Closed

brennanb2025 wants to merge 17 commits into
mainfrom
brennanb2025/ssh-chat-stop-close-never-gated

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 23 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1928 $\color{#cf222e}{\Huge{\mathbf{−}}}$​11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1917
Prod 39 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​773 $\color{#cf222e}{\Huge{\mathbf{−}}}$​144 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​629

ELI5

Stop and Close have to work every time. Over an SSH host they could fail in a few ways: Stop could be refused after a heavy day of chat actions, a Stop pressed just as a turn started (or while the agent's background task list was briefly unreadable) could report success without stopping anything, and Close could leave the agent running if saving the hidden-tab state failed. Main's #26543 has since removed the request limit that caused the refusal; this PR fixes the rest.

What Changed

  • Stop names what it stops. A desktop or phone that supports it sends the specific turn, waiting message, or background task the user saw when they pressed Stop. The SSH server (the execution host) checks that against its own state: live work is stopped, work that has positively ended is a quiet no-op, and anything it can't confirm is reported as a failed Stop instead of a silent "ok". A retried Stop can never stop later work.
  • A Stop pressed while a message is just starting follows the server's own record of which turn that message started, so it isn't lost in the moment before the agent echoes the message back.
  • A failed read of the background task list is treated as "unknown", never as "ended", and never skips the check that only the server running the agent may stop it.
  • Close starts stopping the agent at the same time as it saves the hidden-tab state, and settles any waiting messages only after the agent has stopped; a slow or failed save or settlement is reported but never delays the stop. Waiting messages still become cards for the chat's next turn, and nothing queued is handed to the agent in between. Two small ordering changes come with this:
    • Intended: the reopen marker now also covers cards kept by the agent's exit, not only cards Close itself kept. Both are messages the agent never ran, so marking them alike means none of them is sent on its own after the chat reopens.
    • Intended: when a message handed to the agent and queued messages are both waiting, their cards now settle in the order they were sent. The same cards with the same outcome are written; only their order changes, and it now matches the order you sent them.
  • Request limits: this PR now uses main's fix(native-chat): never refuse a user's write because the retry record is full #26543 policy as-is (no limit on how many actions are recorded) and no longer changes it. Targeted Stop doesn't need a delivery record at all.

Why

Naming the work removes the need for Stop to depend on request bookkeeping, which is what made it refusable. Checking the name against the execution host's own state keeps the stop correct across reconnects and retries, without a saved "Stop still owed" flag.

The common pattern has the execution host resolve which work it owns before acting on a control request; this PR matches that part. Differences:

  • Intended: stop targets are host-issued identities (turn, waiting message, background task invocation) checked on each request, rather than looking up stored per-request records.
  • Intended: a waiting message that the agent never answered and that has no turn yet counts as ended when a Stop of everything in flight finished without failing after it reached the agent. The common pattern reads only the run's own status; here the message has no status of its own until the agent answers, so the finished Stop is the evidence. A failed Stop never counts, so the next Stop tries again. A cleaner later shape is for the Stop to settle the waiting message's own record.
  • Temporary: older clients that send an unnamed Stop keep the existing best-effort path until they're retired.
  • Temporary: Close still stops the agent for everyone watching the chat; whether it should only close your own view is an open product decision (U15).

The stop target is only sent to a server that advertises agent-session.targeted-stop.v1, and older servers get their existing request shape. No new stream message type.

Linked Issue

Coordinator's structured SSH chat fix plan, U21 and U23. No separate public issue.

Visual Proof

The controls, layout and copy are unchanged. Live run on a managed SSH host (Docker Linux container, desktop on macOS, a credential-free stand-in Claude), with this PR combined with the other SSH chat fixes:

  • Stop mid-reply: the agent stopped after 7 streamed chunks, and the reply reads "Interrupted after 16s".
  • Stop pressed 31 ms after sending, before any reply text: the agent was stopped and the turn reads "Stopped manually", with no reply.
  • Closing the tab mid-reply: in an earlier run, the agent process on the host was gone within 2 s.

Stop mid-reply and Stop right after sending, on an SSH host

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

On macOS, each review round's failure was reproduced as a regression that failed before its fix: the Stop pressed while a message is starting (with the real Codex adapter), the unreadable background-task list, ended targets after the agent was released or the server restarted, and Close with a failed or stuck tab-hide save. After the final sync with main (which brought in #26780's background-task Stop and #26672's Send replay), the PR's 18 desktop test files (194 tests), 10 neighbouring main test files and its 5 mobile test files pass; the node, web and mobile typechecks pass; and CI is green. orca-ci-checks passes except main's known mobile import-cycle warnings in files this PR doesn't touch.

Live managed-SSH run (desktop on macOS, Linux container host, stand-in Claude, combined with the other SSH chat fixes): Stop mid-reply, Stop 31 ms after sending, and closing the tab mid-reply all stopped the agent; see Visual Proof. Not run: WSL, native Windows or Linux desktops, the phone, a real Claude login, or old/new client and server pairs end to end (covered by compatibility tests).

Targeted Stop follow-up

The problem. Stop could report success without stopping anything in two moments:

  • When a message had been sent but the agent had not yet started a turn for it. Claude takes about 3 seconds on average, and sometimes 18 or more, to echo a message back. Codex has a short gap while it admits a turn.
  • When a later message reached the server just before the Stop did. The server treated the waiting message as replaced by the newer one. That newer message could be an ordinary send, another device's send, or a coordinator's mail.

In both cases the agent kept working on the message, and the Stop button reported success.

What changes for you. Pressing Stop while a message is waiting to start now stops it, whatever arrives after the press. If the server can't confirm what happened, Stop reports it as not confirmed instead of quietly succeeding. A Stop that is retried after the reply was lost still never stops a later message or a later turn. A permission or question card's Cancel always settles that card, even when the turn you were looking at ended just before the server processed it.

How it works.

  • The desktop and the phone send the identity of what was on screen when you pressed Stop: the running turn, the message waiting to start, or the background task. A message still held in the server's queue is never named. When only queued messages are waiting, the desktop sends the plain Stop, which withdraws them.
  • The server that runs the agent checks that identity against its own records:
    • A message still waiting is stopped.
    • It is a quiet no-op only on proof the work already ended. The proof is that the message was rejected, its turn ended or a newer turn replaced it, or a Stop of everything in flight was recorded after it reached the agent and finished without failing.
    • A Stop whose interrupt and agent shutdown both failed is not proof: the chat keeps reading "Stopping…", that Stop reports itself as not confirmed, and the next Stop tries again, the same as an unnamed Stop.
    • Otherwise the Stop is reported as not confirmed.
  • Messages sent after the named one never count as proof that it ended.
  • A Stop pressed while a turn is opening follows the server's own record of which turn that message started.
  • A card's Cancel is decided by the card itself. The Stop identity only limits what the Stop part of that Cancel may interrupt.
  • No saved "Stop still owed" flag, delay or polling was added.

Other notes.

  • Stop works from the identity of the work, not from a Send's delivery record. The plain Stop still reaches the agent when record keeping fails.
  • Close starts shutting the agent down at the same time as it saves the hidden-tab state. A save failure is reported, but it never blocks the shutdown.
  • The new fields are optional and only sent to servers that support them, so older phones, desktops and servers keep working.

Request limits. The limits come from main's #26543. This PR adopts that policy and no longer changes operation limits: no request is refused for having too many recorded actions, in flight or settled. Delivery-ID validation, duplicate replay and conflict checks stay as they are on main; since main's #26672 a retried Send replays from its own receipt instead of expiring, and this PR follows that.

Testing. The tests run the production server code over scripted agent traffic, including the real Codex adapter. They cover:

  • the original race, and a turn admitted before Codex announces it
  • a missing link between message and turn, and a refused interrupt while a turn opens
  • completed targets, retries, and protection of later turns
  • a later message queued just before the Stop, which fails on the previous head
  • a card's Cancel after its Stop target ended, which also fails on the previous head
  • a Stop pressed again after a Stop that failed (interrupt and shutdown both failed): it tries again, and the failed Stop reports "not confirmed". This fails on the previous head, where the failed Stop counted as proof the message had ended

The live managed-SSH run above covers Stop and Close on the desktop; WSL, Windows and the phone were not run live.

Known difference, temporary. On a phone, Stop pressed while the only waiting message is still held by the server (for example while the agent starts) shows "Stop not sent", the same as main. The desktop stops it. This follows main's current behavior and is tracked with the uncertain-send follow-up (U10/U22), which decides whether the phone may send a plain Stop there.

Review

Draft; independent reviews are run on every round, and the final one runs on the final head. Known gaps, stated plainly:

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

The execution host owns the behavior for local, SSH, WSL, folder, and Git workspaces. No platform-specific process handling or path behavior was added. Fingerprint and authority checks remain. There are no capacity limits or capacity exemptions in this PR; request limits are main's #26543 policy.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

… pre-turn

A Stop that names its target can never run work twice, so it no longer expires
with delivery receipts; a Stop for a background task that already ended answers
as a no-op without needing a live provider; and the phone can stop a pending
submission before its turn opens.
Keep one target observation through admission and provider dispatch.
Require execution ownership for unavailable roster recovery, surface unconfirmed
Stops as failures, and preserve ended-target retries after release and restart.
…-stop-close-never-gated

# Conflicts:
#	src/main/runtime/rpc/methods/agent-launch-restart-replay.test.ts
#	src/shared/agent-session-operation-ledger.test.ts
#	src/shared/agent-session-operation-ledger.ts
A Stop captured before a turn opens could be discarded before its user echo
linked the submission. Resolve the submission through the execution host's
existing dispatch binding and interrupt only that admitted turn. Report the
existing unknown-outcome failure when identity or cancellation is unconfirmed;
keep ended and superseded targets quiet so retries cannot stop later work.

Add real-host and production-adapter regressions for the opening/echo race,
admitted turns, missing binding, ended targets, and later-turn protection.
Closing a chat settled its waiting messages and wrote the queue's reopen
mark before it stopped the agent, so a slow or pending journal write kept
the agent running until that bookkeeping finished.

Close now stops the agent first and settles the waiting messages after,
even when the stop fails. Nothing queued can be handed over in between:
every send and start waits behind the Close step, and a settlement that
fails after a person's close is redone before the next start. The stop's
check for work being cut short ignores queued sends during a close, so
Close records the same Stop row as before. Unsent messages still become
cards that wait for the chat's next turn.
…-stop-close-never-gated

# Conflicts:
#	mobile/src/session/use-mobile-structured-agent-session-send.test.tsx
#	src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts
#	src/renderer/src/components/native-chat/use-structured-agent-session.ts
A Stop naming a waiting send became a silent no-op when a later send was
queued first, because the send counted as live only while it was the newest
unanswered one. The host now reads the named send itself: still waiting
means live, a Stop recorded after it reached the agent or an answer with no
turn running means ended. Clients never name a queued send. A card's Cancel
again settles the card when the turn its Stop named already ended; the
target bounds only the Stop route.
A targeted Stop treated any recorded Stop after the send reached the agent
as proof the send had ended, including a Stop whose interrupt and child end
both failed. A repeat Stop then did nothing and answered success, and the
failed Stop itself answered success instead of the unconfirmed failure.

The Stop now counts only once it took effect: settled, and not failed, the
same settle state that keeps the chat reading Stopping.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review summary (final review, head a9a1bd7)

Problem: on the SSH server, pressing Stop or closing a chat tab could fail to stop the agent. After enough chat actions, Stop could be refused because the server's saved request records filled up. Close could leave the agent running when saving the hidden-tab state failed. And a Stop pressed right as a message was handed to the agent could report success while the agent kept working.

User-facing change:

  • Stop and Close always reach the agent; they are never refused because of how many actions were recorded.
  • Stop acts on exactly what you saw when you pressed it (a turn, a waiting message, or a background task), so a delayed or retried Stop can't stop newer work.
  • Stop never says it worked when it didn't. If stopping can't be confirmed, Stop reports that, and pressing it again tries again.
  • Close stops the agent first, then hides the tab; a failure to save the tab state is reported but no longer keeps the agent running.

How: a desktop or phone that supports it names the host-issued turn, message or task it wants stopped. The SSH server checks that against its own state: live work is interrupted, work that has positively ended is a quiet no-op, and anything it can't verify is retried or reported. Older clients and older servers keep the previous request shape.

Review rounds: eight independent rounds. Round 7 found that a Stop that failed was itself being counted as proof its message had ended, so the failed Stop answered "ok" and every later Stop of that message did nothing. Round 7's fix counts a Stop only once it finished without failing.

  • Round 8 (independent, head a9a1bd7): no findings. 474 tests in 45 files pass. The new failed-Stop regression test fails 2/2 on the previous head and passes on this one. The Close test fails 5/7 on main and passes 7/7 here. The reviewer's probe showed: Stop fails, repeat Stop succeeds, a third Stop is a quiet no-op; two failed Stops in a row and a third still retries.
  • Earlier rounds' fixes still hold: a message queued just before Stop isn't stopped; a card's Cancel after its turn ended is a quiet no-op; an unreadable background-task list is never treated as "task ended".

Live SSH QA (integration build with all the SSH chat fixes, Docker SSH host, stand-in Claude): Stop mid-reply stopped the agent in about 2 s and showed "Interrupted after 13s"; closing the tab mid-reply stopped it in under 2 s; Stop pressed 27 ms after sending, before any reply text, still stopped it. Screenshots go in the PR body before this is marked ready.

Known gaps (in the PR body): unbounded growth of recorded requests comes from main #26543 and needs its own follow-up; older clients' unnamed Stop keeps the best-effort path; Close stopping the agent for everyone watching is an open product decision.

Not verified: WSL, native Windows or Linux hosts, a real Claude login, the rendered phone, and mixed old/new desktop and server pairs end to end (covered by compatibility tests only).

@brennanb2025
brennanb2025 marked this pull request as ready for review October 9, 2026 06:38
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8762f11a-a649-44f3-9c9e-4a76bf416755


📥 Commits

Reviewing files that changed from the base of the PR and between fac6037 and 0cf04b5.



📒 Files selected for processing (1)
  • config/scripts/vitest-sqlite-runtime-files.mjs


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




📝 Walkthrough
📝 Walkthrough

Walkthrough

The change adds turn, submission, and background-task Stop targets to shared contracts and renderer and mobile clients. Host-side observation classifies targets before cancellation and scopes Stop actions to the observed work. Operation admission adds a path that can omit receipt rows, with updated replay and retention tests. Session close now attempts provider shutdown before tab visibility and queued-send cleanup complete.



Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 0cf04

Two narrow Stop edge cases remain: a reused operation ID can let a targeted Stop cancel work despite a conflict, and a delayed turn Stop can interrupt later work. The test-runner change is correctly routed; merge with awareness of these remaining chat risks.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 39 functions across 53 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 Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly summarizes the primary change: keeping chat Stop and Close independent of receipt bookkeeping.
Description check Passed The description is comprehensive and covers the required explanation, changes, rationale, testing, visual proof, compatibility considerations, known gaps, and checklist. The Linked Issue section does …

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR




  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2a9651e3-7c25-42fc-881b-9aecaabfe06e
📥 Commits

Reviewing files that changed from the base of the PR and between ebb192e and a9a1bd7.

📒 Files selected for processing (62)
  • mobile/src/session/mobile-structured-agent-session-cancel.ts
  • mobile/src/session/mobile-structured-agent-session-host-support.test.ts
  • mobile/src/session/mobile-structured-agent-session-host-support.ts
  • mobile/src/session/mobile-structured-agent-session-stop-target.test.ts
  • mobile/src/session/mobile-structured-operation-id-retirement.test.ts
  • mobile/src/session/use-mobile-structured-agent-session-background-tasks.test.tsx
  • mobile/src/session/use-mobile-structured-agent-session-send.test.tsx
  • mobile/src/session/use-mobile-structured-agent-session.ts
  • mobile/src/session/use-mobile-structured-background-tasks.ts
  • src/main/codex/codex-structured-dispatch-echo.test.ts
  • src/main/codex/codex-structured-dispatch-echo.ts
  • src/main/codex/codex-structured-session-adapter.ts
  • src/main/codex/codex-structured-submission-turn.ts
  • src/main/native-chat/agent-session-journal/journal-stop-marks.ts
  • src/main/native-chat/agent-session-journal/queued-message-pause.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-background-target-lifetime.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-background-target-unverifiable.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-cancel-plan.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-chat-stop.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-close-storage.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-codex-submission-stop.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-conversation-lifetime.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-failed-stop-repeat.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-stop-event.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-mutation-admission.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-mutation-admits-now.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-prompt-cancel-ended-target.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-message-rig.test-fixture.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-mutations.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stop-capacity.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stop-target-observation.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-stopping.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-submission-stop-opening.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-target-expiry.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-targeted-stop.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-turns-cancel.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-work-receipt-window.test.ts
  • src/main/runtime/agent-session-operation-admission.ts
  • src/main/runtime/agent-session-operation-retention.test.ts
  • src/main/runtime/rpc/methods/agent-launch-restart-replay.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session-close.ts
  • src/main/runtime/rpc/methods/structured-agent-session.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-cancel.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-stop.test.tsx
  • src/renderer/src/components/native-chat/use-structured-agent-session-stop.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session.ts
  • src/shared/agent-child-work-invocation.ts
  • src/shared/agent-session-background-stop-target.ts
  • src/shared/agent-session-operation-ledger.ts
  • src/shared/agent-session-stop-capabilities.ts
  • src/shared/agent-session-stop-target.test.ts
  • src/shared/agent-session-stop-target.ts
  • src/shared/agent-status-child-work.ts
  • src/shared/rpc-contract/agent-session-stop-target-params.test.ts
  • src/shared/rpc-contract/agent-session-stop-target-params.ts
  • src/shared/rpc-contract/structured-agent-session-params.ts
💤 Files with no reviewable changes (1)
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-mutations.ts

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

Comment on lines +25 to +29
if (target.kind !== 'submission') {
return agentSessionStopTargetIsLive(target, activeTurnId, submissions, ctx.fence)
? { verdict: 'live' }
: { verdict: 'exited' }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Return the target's turn ID for a live turn target, so the host runs the Stop as a named Stop.

For a turn target, the observation returns { verdict: 'live' } with no turnId. The desktop Stop hook (use-structured-agent-session-stop.ts, Lines 60-66) sends only { stopTarget } and omits the top-level turnId. As a result, structured-agent-session-host-mutations.ts Line 159 calls stop(undefined), and the host runs an unnamed "stop whatever is in flight" Stop.

mutateWithChatStop awaits several steps between this observation and the interrupt, such as the queued-withdrawal and the Stop event append. cancelTurn then gets no turnId and reads the live turn through resolveLiveTurnId when it sends the interrupt. Suppose the target turn ends and a queued or later send opens a new turn inside that window. The interrupt then reaches the new turn. The target was meant to prevent exactly this case.

The submission branch already returns turnId: activeTurnId for this reason. Mobile avoids the window because it also sends the top-level turnId.

Proposed fix
--- "a/src/main/native-chat/agent-session-wire/structured-agent-session-stop-target-observation.ts"
+++ "b/src/main/native-chat/agent-session-wire/structured-agent-session-stop-target-observation.ts"
@@ -22,11 +22,12 @@
 ): StopTargetObservation {
   const activeTurnId = ctx.journal.activeTurnId()
   const submissions = ctx.journal.submissions()
   if (target.kind !== 'submission') {
-    return agentSessionStopTargetIsLive(target, activeTurnId, submissions, ctx.fence)
-      ? { verdict: 'live' }
-      : { verdict: 'exited' }
+    if (!agentSessionStopTargetIsLive(target, activeTurnId, submissions, ctx.fence)) {
+      return { verdict: 'exited' }
+    }
+    return target.kind === 'turn' ? { verdict: 'live', turnId: target.turnId } : { verdict: 'live' }
   }
   const submission = submissions.find((entry) => entry.clientMessageId === target.clientMessageId)
   if (!submission) {
     return { verdict: 'unverifiable' }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (target.kind !== 'submission') {
return agentSessionStopTargetIsLive(target, activeTurnId, submissions, ctx.fence)
? { verdict: 'live' }
: { verdict: 'exited' }
}
if (target.kind !== 'submission') {
if (!agentSessionStopTargetIsLive(target, activeTurnId, submissions, ctx.fence)) {
return { verdict: 'exited' }
}
return target.kind === 'turn' ? { verdict: 'live', turnId: target.turnId } : { verdict: 'live' }
}

Comment on lines +58 to +59
if (args.skipReceipt) {
return { rows, decision: evaluateAgentSessionOperation({ ...args, rows: new Map() }) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Check existing receipts before admitting a receipt-free Stop.

If a targeted Stop reuses a clientOperationId recorded for a Send, the empty map hides that receipt. The host can then admit the Stop and cancel a live target instead of refusing the fingerprint conflict. Keep the existing-row replay and conflict checks; skip only creation of the new receipt.

…-stop-close-never-gated

# Conflicts:
#	src/main/native-chat/agent-session-wire/structured-agent-session-turns-cancel.ts
#	src/renderer/src/components/native-chat/use-structured-agent-session.ts
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review summary addendum (final main sync, head 0cf04b5)

What changed since the last review: a sync with main (151 commits) and one CI fix.

  • Background-task Stop: main's Let a Codex chat stop a command it left running in the background #26780 reworked it into one up-front call. A task that's still listed afterwards is refused, and a Stop that throws answers "unconfirmed". This PR now keeps main's version and passes its own targeted task identity into that one call, instead of rethrowing a failed Stop.
  • A failed Stop is still never reported as ended, and pressing Stop again still tries again.
  • Send retries: a test now expects main's fix(native-chat): accept sends through their own receipt #26672 behaviour, where a retried Send replays from its own record instead of expiring.
  • File split: one Codex file was split to stay under the 300-line limit, with no behaviour change.
  • CI fix: one new test was added to the list of tests that run under Node.

Independent review of the merge: no P0–P2.

  • Nothing lost: all 62 of the PR's files carry its change unchanged on top of main, apart from the 8 the sync deliberately adapted.
  • Background-task Stop checked with a scratch test:
    • a thrown Stop answers "unknown";
    • a still-running task is refused;
    • a retry with the same ID reaches the agent again;
    • a reused task ID under a new run never reaches the agent;
    • a retry after a successful Stop is quiet;
    • old clients get main's answer unchanged.
  • Tests: 194 PR desktop tests, 102 neighbouring tests and 61 mobile tests pass.
  • Without the fix: removing the fixes still fails the PR's key tests: 2 of 2 failed-Stop cases and 5 of 7 Close cases.

Optional test and comment tidy-ups (not fixed):

  • a test that retries a thrown Stop with the same ID;
  • collapsing a leftover two-step clock check in the Send retry test;
  • two one-line comments lost when code moved into new files.

CI is green and the PR merges cleanly.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Replaced by #28243, which keeps main's untargeted Stop (the common pattern) and fixes the remaining phone and background-task gaps in about 30 lines.

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