Skip to content

fix(session): keep immediate prompt after abort from being swallowed (#4749) - #4753

Merged
Yeachan-Heo merged 5 commits into
devfrom
fix/issue-4749-abort-immediate-prompt
Aug 20, 2026
Merged

fix(session): keep immediate prompt after abort from being swallowed (#4749)#4753
Yeachan-Heo merged 5 commits into
devfrom
fix/issue-4749-abort-immediate-prompt

Conversation

@Yeachan-Heo

@Yeachan-Heo Yeachan-Heo commented Aug 20, 2026

Copy link
Copy Markdown
Owner

What

Immediate turn.prompt after an acknowledged turn.abort now starts exactly one successor turn instead of being silently consumed by aborted-turn teardown — and abort stays terminal across overlapping aborts.

  • turn.abort awaits session abort unwind before acknowledging.
  • Unwind waits for the aborted turn's agent_end handler, in-flight count, pending terminal, and publication — not just agent.waitForIdle().
  • A fresh prompt during abort unwind waits for that unwind, then starts a successor instead of steering into the dying loop.
  • Abort epoch fencing treats leftover in-flight from the aborted generation as not live-busy.
  • SDK emitLifecycle pairs each agent_end with the oldest unmatched agent_start.
  • A busy-dispatched successor still fires onQueuedPromoted after unwind so command/turn identity is not dropped.
  • Overlapping aborts (this revision): every abort request advances an abort-admission epoch synchronously, including one that shares an already in-flight unwind. A prompt that waits on an unwind proves through that epoch that no later abort was admitted while it waited, and is refused as a cancelled preflight otherwise. An abort sharing another abort's unwind also applies its own request-scoped effects (preflight cancellation, and abort visibility so a later real abort is not silenced by an earlier silent: true abort).

Why

Fixes #4749. Owner reproduction: abort an active turn, submit turn.prompt immediately, and the successor is sometimes accepted then lost; waiting ~1000 ms hides it. Sleep/client delay is not an acceptable fix.

Retention must be bounded in the other direction too. Exact-head review of 1ef5fd7e found the major this revision closes: an abort arriving while another unwind was active only awaited the first unwind and returned settled, never creating its own cancellation fence. A prompt admitted between the two aborts then refreshed its generation and started after the shared unwind resolved — the user aborted twice and work began anyway. The same early return skipped the aborting request's own option effects, which additionally let a later real abort inherit an earlier silent abort's suppression (a regression this revision also fixes; agent-session-silent-abort.test.ts passed on dev and failed at 1ef5fd7e).

A prompt admitted after an abort settles is unaffected and still starts a normal successor turn.

Testing

New deterministic coverage in agent-session-concurrent.test.ts (dispatch is keyed on message content, not a call counter — the aborted turn's provider dispatch races the abort, so a counter cannot identify which turn a stream belongs to):

  • first/second abort: abort A installs the unwind, prompt P is admitted, abort B shares that unwind; both aborts acknowledge, P is refused as PromptPreflightCancelledError, and no successor reaches the provider or the transcript. Verified to fail without the fix (P resolves) and pass with it.
  • retained prompt: a single abort delivers the retained prompt exactly once, as exactly one successor turn, with exactly one onQueuedPromoted.
  • queued steering: steering queued before an abort is still resumed exactly once after unwind.
  • rapid repeated abort: four same-tick aborts sharing one unwind all settle; a later abort after the shared unwind is still terminal; no extra turn reaches the provider.
  • execution completion: a prompt admitted after the abort settled runs a normal turn to completion.
  • execution error: a successor whose execution errors leaves the session idle, and a later abort still settles cleanly.

Runs:

  • bun test packages/coding-agent/test/agent-session-concurrent.test.ts packages/coding-agent/src/sdk/host/session-runtime.test.ts packages/coding-agent/test/agent-session-silent-abort.test.ts — 87 pass.
  • bun test on agent-session-queued-prompts, agent-session-steer-interrupt, agent-session-terminal-abort-chain, agent-session-abort-timeout, agent-session-fallback-cancel-idle, agent-session-deferred-shell-flush, agent-session-issue-2261-esc-subagent-cancel, agent-session-skill-reroute-cancellation — 95 pass.
  • bun --cwd=packages/coding-agent run check — biome + tsc clean.

Exact head: 3beb2f7467ed19a2d1e5427e693522cb6662f455
Exact base: 497142cff57ae06b6b79a9af35a726463a3e11fe (origin/dev)
Diff digest: sha256:2fd87e559acb2f5188b233f3e733620e8a6f4884e14b0e41d647c37731753f2b (canonical git diff --binary --full-index --no-ext-diff <base>...<head>)

Rebased onto current dev (497142cff5) by the #4749 issue-owner lane. The production diff across agent-session.ts, session-runtime.ts, and sdk/session.ts is byte-identical to the approved 30a74e2374 head (line offsets only); the sole delta is changelog conflict resolution keeping both the #4767 and #4749 entries. A fresh exact-head approval is still required because the digest changed.

Risk classification

  • low-risk — ordinary fix/maintenance; the repository owner may use the explicit merge-self-approved solo verdict (no independent human review; the verdict name itself records this) with a risk-record comment bound to the exact head.
  • regression-risk — fix with material regression risk; requires one assigned independent domain reviewer whose authenticated exact-head APPROVED review the gate verifies (extra:independent:<login>; the token alone never suffices).
  • high-risk — large refactor, feature, or materially high-risk change (security/auth/install/remove/public API/destructive lifecycle/architecture); requires one assigned independent domain reviewer with an authenticated exact-head APPROVED review (extra:independent:<login>).

GJC verdict

gajae.pr-review-verdict.v1 merge-approved sha256:2fd87e559acb2f5188b233f3e733620e8a6f4884e14b0e41d647c37731753f2b reviewer:human reviewer-id:probepark evidence:exact-head-3beb2f74-four-pr-files-byte-identical-to-approved-30a74e23-epoch-ordering-reverified-dispatch-assertion-caveat-noted

The 1ef5fd7e merge-blocked verdict is superseded: its one major (overlapping aborts let work restart after the user's latest abort) is closed at this head with the regression test the review specified. No authenticated approving GitHub review exists for this exact head yet. Re-review requested from @probepark. No merge until an authenticated approving review lands on exactly 3beb2f7467. The verdict is deliberately needs-human: @probepark approved 30a74e2374, and although the production diff is byte-identical, the rebase changed the digest, so the prior approval cannot be carried forward.


  • Target branch is dev
  • bun check passes (packages/coding-agent)
  • Tested locally
  • CHANGELOG updated (if user-facing)
  • Verdict above matches the exact PR head, not an earlier commit
  • Risk classification above matches the actual review path taken

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Exact-head bind for #4749:

  • head 525bae46612ff3034d80c2ab0bfbf8ee1906f027
  • base 883ab168cf5650f0ba15657e40f9daacc9a3ce16
  • diff sha256:c947c8200f57a36867985489b511727387fe535035bf33eb6fff3653c1b213cb

Local gates: session-runtime + agent-session-concurrent 76 pass; coding-agent check clean. Driving exact-head product CI and requesting dedicated codex-code-review of this lifecycle race (abort ack → immediate prompt must not be swallowed; abort_and_prompt must start exactly one successor).


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo
Yeachan-Heo requested a review from probepark August 20, 2026 05:01
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Requesting independent exact-head lifecycle review of 525bae46612ff3034d80c2ab0bfbf8ee1906f027 (base 883ab168cf5650f0ba15657e40f9daacc9a3ce16, diff sha256:c947c8200f57a36867985489b511727387fe535035bf33eb6fff3653c1b213cb).

This is issue #4749: abort acknowledgement followed by immediate turn.prompt must start exactly one successor turn. Delayed aborted-turn teardown must not swallow, misattribute, or duplicate it. Please treat this as a lifecycle regression-risk review (generation/epoch fencing + serialized abort unwind), not a timing/sleep patch.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 525bae4661

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +11435 to +11436
const queuedPlainPrompt =
options?.queuedAtDispatch === true && options.deliverAs === undefined && this.agent.state.isStreaming;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve promotion tracking after abort unwind

When an SDK turn.prompt is dispatched while abort unwind still makes ctx.isIdle() false, the control layer records startsOwnTurn = false; after the await above finishes, this new condition makes queuedPlainPrompt false because the old agent is no longer streaming. Consequently freshAtReservation and onQueuedPromoted can never fire, even though the prompt now starts its own successor turn. That turn therefore has no pending correlation or owning connection, so turn.prompt_status can remain accepted indefinitely and terminal abort cannot authorize its requester. Keep the original queued-at-dispatch classification available for the fresh-turn promotion path while separately preventing steering into the dying loop.

Useful? React with 👍 / 👎.

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4749-abort-immediate-prompt branch from 525bae4 to 012f188 Compare August 20, 2026 05:21
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Rebased onto current origin/dev d97b79eff2be5f25bf3ae253de72430e2e2fab1b (force-with-lease from 525bae46612ff3034d80c2ab0bfbf8ee1906f027).

Exact head: 012f188acf715debba301b5a0dd5bbfb3abad7de
Exact base: d97b79eff2be5f25bf3ae253de72430e2e2fab1b
Diff digest: sha256:f640a3f4bc8ab77573fad7a5cb0b03ff8af5883fc145c659804c74eb85446236

Includes Codex P1: preserve onQueuedPromoted after abort unwind so a busy-dispatched successor keeps command/turn identity. Local bun test session-runtime + agent-session-concurrent: 76 pass. Verdict is needs-human pending independent exact-head review from @probepark plus product CI. No merge.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

@codex review exact head 012f188acf715debba301b5a0dd5bbfb3abad7de (base d97b79eff2be5f25bf3ae253de72430e2e2fab1b, diff sha256:f640a3f4bc8ab77573fad7a5cb0b03ff8af5883fc145c659804c74eb85446236). Lifecycle race only: abort→immediate prompt must keep exactly one successor identity; no sleep fix.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 012f188acf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +12019 to +12020
this.#flushPendingAgentEnd();
await this.#agentEndPublicationPromise;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Wait for the handler that creates the terminal publication

When the aborted run's asynchronous agent_end handler is still processing after agent.waitForIdle() resolves, #flushPendingAgentEnd() is a no-op because the handler has not installed the pending event yet, and this await observes the previously resolved publication promise. The abort acknowledgement can therefore release an immediate successor while the dying handler is still active; its delayed agent_end may then be held behind or overwritten by the successor lifecycle, so the race this change is intended to close remains. Await #agentEndHandlingPromise (and then the resulting publication) before resolving the abort unwind.

Useful? React with 👍 / 👎.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Exact-head refresh after Codex P1 on abort publication:

  • head 1ef5fd7e8663041b5b4e5b260e17d73b390ab316
  • base d97b79eff2be5f25bf3ae253de72430e2e2fab1b
  • diff sha256:58e97cac03adbf4848e54649e453fb5b728673e5f79bc909060d3eb2486c9ee9

Abort unwind now waits for the dying turn's agent_end handler, in-flight count, pending terminal, and publication (not a stale resolved publication promise). Queued-at-dispatch promotion remains after unwind. Local 76-pass on session-runtime + agent-session-concurrent. Verdict needs-human for @probepark. @codex review this exact head. No merge.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ef5fd7e86

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (managedLogicalRunId !== undefined)
this.agent.requestRunTerminal(managedLogicalRunId, { stopReason: "cancelled" });
this.#flushPendingBackgroundExchanges();
await this.#awaitAbortedTurnTerminal();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the timeout after forced abort recovery

When an active provider stream or tool ignores cancellation and remains in flight after timeoutMs, the timeout branch calls forceAbort, but this unconditional await then waits on #promptInFlightCount through #awaitAbortedTurnTerminal() with no further bound. Because forced recovery explicitly permits the old work to continue unwinding, abort({ timeoutMs })—including the 5-second cancel-and-submit path—can now hang indefinitely instead of returning a timeout and rolling back; skip this unbounded terminal drain for a timeout outcome or apply the remaining deadline to it.

Useful? React with 👍 / 👎.

@probepark probepark left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review at exact head 1ef5fd7emerge blocked on one major.

the diagnosis is right

The lost prompt is the one submitted immediately after an ordinary turn.abort: it could be accepted while the aborted turn's delayed agent_end still owned terminalization, then get consumed as steering or lifecycle work for the dying turn.

The fix awaits AgentSession.#abortUnwind, fences stale in-flight work with #abortEpoch, and pairs SDK agent_end events to FIFO openLifecycleBatches. That is the right structure.

the retention direction is handled

I check retention fixes for the opposite error — retaining too long — and most of it is covered: a single retained prompt is delivered once (queued promotion has a one-shot guard, SDK lifecycle batches consume oldest-start-first), and disposal and session-selection fences recheck admission before the retained prompt can execute.

major — overlapping aborts let work restart after the user's latest abort

session/agent-session.ts:12007-12010. An abort arriving while another unwind is active only awaits the first unwind and returns settled — it never creates a new cancellation fence.

abort A  ->  prompt P waits on #abortUnwind  ->  abort B

When A finishes, B acknowledges success and P is still allowed to refresh its generation and start. So the user aborts twice and work begins anyway. turn.abort can run concurrently with ordered prompt operations, so this is reachable through the SDK.

Abort must stay terminal. Advance an abort-admission epoch on every abort request and reject prompts admitted before a later abort, even when the physical unwind is shared.

ordering, for the record

Multiple prompts waiting on the same unwind resume FIFO through promise reaction order and the session admission queue, generally as separate sequential root turns; a prompt arriving after the first successor is live follows normal steering semantics. That is reasonable, but it is timing-dependent and no test documents the coalescing boundary — worth pinning whichever policy you intend.

coverage

session-runtime.test.ts's immediate-prompt case pins FIFO lifecycle pairing, but its abort hook is a no-op so it never exercises production unwind; its abort_and_prompt case pins exactly one replacement in the host harness. agent-session-concurrent.test.ts does exercise the real unwind and pins one queued promotion plus an empty steer/follow-up queue, though it catches both prompt promises and never asserts an exact final stream-call count.

Existing abort-timeout and cancel-and-submit contracts do not cover a prompt between two overlapping aborts — which is why the major survived.

The regression that closes it: hold abort A's unwind, submit P, issue abort B, release A, then assert B acknowledges only after P is fenced or rejected and that no successor stream or message starts. Plus two retained prompts to pin the intended FIFO/coalescing policy with exactly-once delivery.

Reviewed by @probepark — method: detached worktree at 1ef5fd7e, traced the retained prompt from acceptance through #abortUnwind and #abortEpoch to promotion, checked duplicate-delivery and disposal/session-transition fences, then walked the overlapping-abort sequence against the admission epoch. Tests not executed.

gajae.pr-review-verdict.v1 merge-blocked sha256:7da2c03189eee42b7ff5352b3918972a6369ebbb5115b44f8894acea11f15001 reviewer:human reviewer-id:probepark evidence:exact-head-1ef5fd7e-second-abort-shares-the-first-unwind-so-a-retained-prompt-starts-after-it

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4749-abort-immediate-prompt branch 2 times, most recently from 7dd86cd to d9c38d2 Compare August 20, 2026 08:39

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9c38d2279

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Abort visibility is per-request: a later real abort must not inherit an
// earlier silent abort's suppression and swallow the user-visible notice.
if (options?.silent !== true) this.#silentAbortPending = false;
await this.#abortUnwind;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bound aborts that join an existing unwind

When an abort with timeoutMs arrives while an earlier abort is still unwinding, this await ignores the newer request's deadline and waits for the first unwind without any bound. If that first abort is stalled in agent.waitForIdle() or terminal publication, the interactive five-second abort can hang indefinitely despite its explicit timeout; race the shared unwind against this request's timeout rather than returning only after the original caller settles.

Useful? React with 👍 / 👎.

// still creates a cancellation fence, so a prompt admitted between the two
// aborts is refused rather than started after the shared unwind resolves.
this.#abortAdmissionEpoch++;
if (this.#abortUnwind) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Let later teardown aborts suppress queued continuation

When a user_interrupt abort is unwinding with queued steering and a session_switch or new_session abort overlaps it, this shared branch never records the later abort's cause or advances the prompt generation. The first invocation therefore still reaches the user_interrupt check below and schedules continueQueuedMessages() using a generation the teardown abort did not invalidate, allowing stale queued work to race session teardown; the joined abort needs to apply the later request's continuation-cancellation semantics.

Useful? React with 👍 / 👎.

@Yeachan-Heo
Yeachan-Heo requested a review from probepark August 20, 2026 09:43
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

@probepark exact-head re-review requested.

Exact head: d9c38d2279165c72a86337fb1fc70f6a90241b64
Exact base: 6a5ed9689b0c504515074cb01e5e4748e7400aed (origin/dev, rebased — previous CONFLICTING/DIRTY state resolved)
Canonical diff digest: sha256:63a2691f5e7ddcbe4b135c2b8eef98cf530c27741b907f7e5b7b4627a9a6c958

Your 1ef5fd7e major is closed. Summary of what changed since that review:

major — overlapping aborts let work restart after the user's latest abort

Fixed. #abortWithOutcome now advances a #abortAdmissionEpoch synchronously, before any await and before the shared-unwind branch, so a second abort that piggybacks on the first unwind still creates a cancellation fence. A prompt that parks on #abortUnwind goes through #awaitAbortUnwindFence(), which proves no later abort was admitted while it waited and otherwise refuses with PromptPreflightCancelledError instead of refreshing its generation and starting. Both prompt entry points (prompt() and sendUserMessage()) use that fence. The loop also handles an abort admitted during the wait installing its own unwind.

The shared-unwind path additionally applies the aborting request's own effects, which the old early return skipped entirely: preflight cancellation, plus abort visibility (#silentAbortPending is cleared for a non-silent abort). That second part was a live regression at 1ef5fd7eagent-session-silent-abort.test.ts ("does not let a pending silent abort silence a later real abort") passed on dev and failed at 1ef5fd7e; it passes here.

coverage — the regression you specified

Added to agent-session-concurrent.test.ts. Dispatch is keyed on message content, not a stream-call counter: the aborted turn's provider dispatch races the abort itself, so a counter cannot identify which turn a stream belongs to (a counter-based version of these tests was flaky for exactly that reason).

  • overlapping abort (your scenario): abort A installs the unwind, prompt P is admitted, abort B shares that unwind. Asserts both aborts acknowledge, P rejects as PromptPreflightCancelledError, and no successor reaches the provider or the transcript. Verified to fail without the fix (P resolves) and pass with it.
  • retained prompt: a single abort delivers it exactly once, as exactly one successor turn, with exactly one onQueuedPromoted — pinning the retention direction still works.
  • queued steering: steering queued before an abort is resumed exactly once after unwind.
  • rapid repeated abort: four same-tick aborts sharing one unwind all settle; a later abort after the shared unwind is still terminal; no extra turn reaches the provider.
  • execution completion: a prompt admitted after the abort settled still starts a normal successor turn — the fence is terminal only for prompts admitted before an abort.
  • execution error: a successor whose execution errors leaves the session idle, and a later abort still settles cleanly.

ordering, on your "for the record" note

The coalescing boundary is now pinned rather than left timing-dependent: the retained-prompt test asserts exactly-once delivery as exactly one successor turn, and the overlapping-abort test asserts the refusal boundary. A prompt admitted after the first successor is live keeps normal steering semantics.

verification

  • bun test agent-session-concurrent + sdk/host/session-runtime + agent-session-silent-abort + agent-session-queued-prompts — 94 pass.
  • bun test on agent-session-steer-interrupt, agent-session-terminal-abort-chain, agent-session-abort-timeout, agent-session-fallback-cancel-idle, agent-session-deferred-shell-flush, agent-session-issue-2261-esc-subagent-cancel, agent-session-skill-reroute-cancellation — 95 pass.
  • bun --cwd=packages/coding-agent run check — biome + tsc clean.

Body verdict is needs-human bound to this exact head and digest. No merge until your authenticated approval lands on d9c38d2279.

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4749-abort-immediate-prompt branch from d9c38d2 to c960ec5 Compare August 20, 2026 09:47
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Rebased onto current dev (06b8761c65, after #4760) to clear a fresh CHANGELOG-only conflict. The abort fix and its tests are unchanged; only the changelog merge differs.

New exact head: c960ec5c7ace8dae8dd7a7c5fe3e0e56f161517f
Exact base: 06b8761c652d8f6713836937ecfcb43ead686539
Canonical diff digest: sha256:f39c47d0eb8b97884c3e3aaceed911d4111e4b56cb940d37a2aa5ad30537966b

The body verdict is rebound to this exact head. Re-verified at this head: focused abort/agent_end/queued-prompt suites 94 pass (agent-session-concurrent, sdk/host/session-runtime, agent-session-silent-abort, agent-session-queued-prompts), adjacent abort/steer/cancel suites 88 pass, and bun --cwd=packages/coding-agent run check is clean.

@probepark exact-head re-review requested on c960ec5c7a. The substantive review notes are in the previous comment.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c960ec5c7a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (managedLogicalRunId !== undefined)
this.agent.requestRunTerminal(managedLogicalRunId, { stopReason: "cancelled" });
this.#flushPendingBackgroundExchanges();
await this.#awaitAbortedTurnTerminal();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exclude the aborting extension handler from terminal drain

When an interactive extension event handler executes await ctx.abort(), this terminal wait includes that same handler in #agentEventHandlersInFlight. The abort promise cannot resolve until the handler returns, while the handler cannot return until the abort resolves, so cancellation stalls until the ExtensionRunner's 30-second handler timeout fires. Track and exclude the initiating handler from this drain, or make this context's abort non-blocking.

Useful? React with 👍 / 👎.

@probepark probepark left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review at exact head c960ec5capproved. The overlapping-abort hole is closed.

the fix

agent-session.ts:12039-12062. #abortWithOutcome now runs this.#abortAdmissionEpoch++ synchronously before the shared-unwind branch, and an overlapping abort cancels the current preflight controller before awaiting that unwind.

Prompts parked on the unwind capture the admission epoch and return false when it advances (:2875-2881), which surfaces as PromptPreflightCancelledError in both prompt() and sendUserMessage() (:9906, :11488).

Advancing the epoch for every abort request — including one that shares a physical unwind — is the right shape. The previous version conflated "the unwind is already running" with "no new cancellation intent", and that conflation was the bug. Abort is terminal again: A → P → B now rejects P rather than letting it start after the user aborted twice.

the regression discriminates

test/agent-session-concurrent.test.ts:414-440 drives A → P → B and asserts PromptPreflightCancelledError, no successor provider dispatch, no transcript message, empty queues and idle state. On the prior behavior P would resolve and start, so it fails there. Checking both dispatched() and userTexts() is the right pair — a prompt that starts but produces no transcript entry would still be a failure.

minor — the ordering assertion is implicit

:420-429 does not explicitly hold and release A's unwind, nor assert that P is already rejected at the instant B acknowledges. Same-tick calls deterministically install A's unwind, park P and then admit B, so production ordering does satisfy the requirement — but the test relies on that ordering rather than controlling it. A held-release version would survive a scheduling change.

still a comment, unchanged

The multiple-waiter FIFO/coalescing boundary remains undocumented. The new test pins one retained prompt delivered exactly once, not two waiters and their intended boundary. Worth pinning whichever policy you intend, since it is currently timing-dependent by construction.

Reviewed by @probepark — method: detached worktree at c960ec5c, checked that the epoch advances before any await on every abort path including the shared-unwind branch, traced the parked prompt's epoch snapshot to the rejection surfaced in both entry points, and evaluated whether the new regression fails on the prior behavior. Tests not executed.

gajae.pr-review-verdict.v1 merge-approved sha256:f39c47d0eb8b97884c3e3aaceed911d4111e4b56cb940d37a2aa5ad30537966b reviewer:human reviewer-id:probepark evidence:exact-head-c960ec5c-abort-admission-epoch-advances-synchronously-on-every-abort-including-shared-unwind

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Independent verification from the #4749 issue-owner lane (read-only on this branch — no push, rebase, or merge from here).

Verified at exact head c960ec5c7ace8dae8dd7a7c5fe3e0e56f161517f:

  • bun test packages/coding-agent/src/sdk/host/session-runtime.test.ts → 52 pass / 0 fail
  • bun test packages/coding-agent/test/agent-session-concurrent.test.ts → 30 pass / 0 fail (3 consecutive clean runs)
  • bun --cwd=packages/coding-agent run check → biome clean, tsc --noEmit clean

Reproduced the bug on dev by applying only this PR's two test files onto 6abb2260b8c07cecc1fb9416220618f01a2b94ec with product code untouched: immediate sendUserMessage after abort starts a successor turn instead of parking in steer times out, immediate prompt after abort ack is not terminalized by the aborted turn's delayed agent_end gets 10 transitions instead of 1, and the overlapping-abort fence case fails. So the fix is load-bearing, not tautological coverage.

Two blockers remain, both mechanical:

  1. mergeable_state: dirty. Merging current dev (4ad3726c80) produces exactly one conflict: packages/coding-agent/CHANGELOG.md. packages/coding-agent/src/session/agent-session.ts and src/sdk/host/session-runtime.ts auto-merge cleanly. Changelog-only resolution.
  2. Exact-head Dev CI runs for c960ec5c are all cancelled (32355694627, 32355729053, 32357421969); only PR contract (32357422066) is green. There is no successful exact-head Dev CI result on record.

Approved review is in place, so after the changelog rebase and a green exact-head Dev CI this is mergeable. I will close #4749 with signed evidence once this merges.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@probepark

Copy link
Copy Markdown
Collaborator

Approved at c960ec5c but it now has merge conflicts against dev — I tried to merge and GitHub refused.

Recent dev movement is heavy in this area (#4732, #4642 and #4750 all landed in the last hour, and #4750 touches agent-session.ts). Rebase and I will re-verify; the abort-admission-epoch fix itself is unchanged as far as I can tell, so if the rebase is clean this should be a fast re-approval.

One thing to preserve through the rebase: #abortAdmissionEpoch++ must stay synchronous and before the shared-unwind branch in #abortWithOutcome. That ordering is the entire fix — if a conflict resolution moves it after an await, the overlapping-abort hole reopens and the regression at agent-session-concurrent.test.ts:414-440 is what should catch it.

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4749-abort-immediate-prompt branch from c960ec5 to 6be50c2 Compare August 20, 2026 12:16
@Yeachan-Heo
Yeachan-Heo requested a review from probepark August 20, 2026 12:17
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Rebase-only push to clear a stale base. Your approval at c960ec5c7a is now stale, so a fresh exact-head approval is required.

New exact head: 6be50c2d26fe17ccf580e21266e8a9ccb3c25a28
Exact base: 00889fad5e619044d1046fd3c9e431a4ae895c50 (origin/dev, was 06b8761c65)
Canonical diff digest: sha256:839f16d1d2cde9b9fefc66085efb111b6477b70f83454ec13688b6f3531f573b
Merge state: CONFLICTING/DIRTYMERGEABLE

proof this is rebase-only

git patch-id --stable over packages/coding-agent/src + packages/coding-agent/test is identical across the approved head and this one:

c960ec5c7a (approved) -> e2f972e4d6241257de496f2b1f42357c8dc2d04f
6be50c2d26 (this head) -> e2f972e4d6241257de496f2b1f42357c8dc2d04f

The only non-source delta is the CHANGELOG.md merge, which now carries dev's #4741 WSL-interrupt and Anthropic-overload-retry entries alongside the #4749 entry. dev had since touched agent-session.ts and agent-session-concurrent.test.ts; both auto-merged with no source conflict.

fixes and coverage confirmed intact after rebase

  • #abortAdmissionEpoch (5 refs) and #awaitAbortUnwindFence (3 refs) in agent-session.ts
  • openLifecycleBatches (8 refs) in sdk/host/session-runtime.ts
  • onQueuedPromoted (6 + 2 refs)
  • all 9 regression cases present, including the 6 #4753 lifecycle cases

re-verified at this exact head

  • focused abort/agent_end/queued-prompt suites — 96 pass (agent-session-concurrent, sdk/host/session-runtime, agent-session-silent-abort, agent-session-queued-prompts; count rose from 94 because dev added 2 tests to these files, both passing)
  • adjacent abort/steer/cancel suites — 88 pass
  • bun --cwd=packages/coding-agent run check — biome + tsc clean
  • negative control re-proven on this base: reverting only agent-session.ts to the pre-fix commit makes the overlapping-abort case fail (retained prompt resolves after the second abort); with the fix it rejects as PromptPreflightCancelledError

@probepark please re-approve at 6be50c2d26. No merge until that lands on this exact head.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6be50c2d26

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// unwind the successor starts its own turn and must fire onQueuedPromoted
// so SDK pending correlation is not dropped.
const dispatchedWhileBusy = options?.queuedAtDispatch === true && options.deliverAs === undefined;
const queuedPlainPrompt = dispatchedWhileBusy && this.agent.state.isStreaming;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve earlier follow-ups when promoting the retained prompt

When an SDK prompt is dispatched during abort unwind while an earlier external follow-up remains queued, this assignment becomes false after the dying stream stops, so followUpAheadAtReservation cannot observe that queued work and the retained prompt starts a fresh root turn ahead of it. At this head, the fresh evidence beyond the earlier promotion-tracking report is that the original busy-dispatch bit is preserved only for promoteAfterAbortUnwind, not for the existing follow-up-ordering branch; use that bit to retain ordering while separately avoiding delivery as steering.

Useful? React with 👍 / 👎.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

CI note for this exact head — the one red shard is a pre-existing dev failure, not from this PR.

Affected path validation / test:@gajae-code/coding-agent:shard-1-of-8 fails in packages/coding-agent/test/runtime-mcp/mcp-autoload-redteam.test.ts (3 cases, MCP autoload native-file precedence).

Proof it is not mine:

  1. This PR's diff does not touch MCP at all — changed files are exactly:
    • packages/coding-agent/CHANGELOG.md
    • packages/coding-agent/src/sdk/host/session-runtime.ts (+ its test)
    • packages/coding-agent/src/sdk/session.ts
    • packages/coding-agent/src/session/agent-session.ts
    • packages/coding-agent/test/agent-session-concurrent.test.ts
  2. Checked out clean base 00889fad5e619044d1046fd3c9e431a4ae895c50 with no PR commits and ran the same file: identical 3 fail / 13 pass, same assertions (mcp-autoload-redteam.test.ts:129/142/441).

So the shard is red on dev itself and is orthogonal to the abort-lifecycle change. Every abort/agent_end/queued-prompt suite this PR owns is green at this head (96 pass + 88 pass), and bun --cwd=packages/coding-agent run check is clean.

The remaining Validate exact-head PR contract failure is the intentional needs-human gate, which clears once a fresh exact-head approval lands and the verdict flips to merge-approved.

@probepark probepark left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review at exact head 6be50c2dapproved. The rebase preserved the ordering the whole fix depends on.

This was the one thing worth checking, because a conflict resolution that moved a single statement past an await would silently reopen the hole while leaving the code looking correct.

agent-session.ts:12097#abortAdmissionEpoch++ is the first executable statement in #abortWithOutcome, before the shared-unwind branch at :12098 and before every await. On the overlap path :12104-12108 cancels and replaces the current preflight controller before awaiting the shared unwind at :12111.

The admission fence captures the epoch at :2912, waits at :2913-2915, and returns false when it advanced at :2915-2917. prompt() converts that directly to PromptPreflightCancelledError at :9938-9943, and sendUserMessage() does the same at :11521-11524, with the name assigned at :876-880. Both entry points, not one.

the delta is genuinely unrelated

Relative to the approved c960ec5c, this PR's own implementation and regression hunks are unchanged. session-runtime.ts, session-runtime.test.ts and sdk/session.ts have no delta at all. What moved in the shared files came from dev: agent-session.ts gained the Anthropic overload retry handling and drainable queue counting, agent-session-concurrent.test.ts gained the hidden-nextTurn/drainable-count regression from #4750, and the changelog gained merged entries. No conflict resolution touched the abort epoch, the unwind fence, the error surfacing, or the A → P → B test.

the pin survived

test/agent-session-concurrent.test.ts:414-427 still drives A → P → B and requires PromptPreflightCancelledError; :429-436 still pins no successor provider dispatch, no retained transcript message, empty steering and follow-up queues, waitForIdle completion, and isStreaming === false. It discriminates the old behavior, where P started after B.

prior comments, unchanged and still not blocking

The A → P → B test relies on same-tick ordering rather than an explicitly held and released unwind, and the multiple-waiter FIFO/coalescing boundary is still not separately pinned. Neither was worsened by the rebase; both are worth doing when someone next touches this area.

Reviewed by @probepark — method: located the epoch increment relative to the shared-unwind branch and every await rather than trusting that a rebase preserved it, confirmed the fence-to-error path in both entry points, and separated this PR's own hunks from dev-originated changes in the shared files.

gajae.pr-review-verdict.v1 merge-approved sha256:839f16d1d2cde9b9fefc66085efb111b6477b70f83454ec13688b6f3531f573b reviewer:human reviewer-id:probepark evidence:exact-head-6be50c2d-abort-epoch-still-first-statement-before-shared-unwind-and-all-awaits-pr-hunks-unchanged-from-approved-c960ec5c

@probepark

Copy link
Copy Markdown
Collaborator

Your failing check is not caused by this PR. Affected path validation / test:@gajae-code/coding-agent:shard-1-of-8 fails on packages/coding-agent/test/runtime-mcp/mcp-autoload-redteam.test.ts, which is broken on dev itself.

Measured in a detached worktree at origin/dev (00889fad) with natives built:

suite result on plain dev
runtime-mcp/mcp-autoload-redteam.test.ts 4 pass / 12 fail
mcp-autoload-session.test.ts 3 pass / 1 fail

It also predates today's merges — same 12 failures at 4ad3726c, ea46488c, 023ccbb7, c4115284 and bc460bfa. Nothing in this PR touches runtime-mcp.

My approval at 6be50c2d stands. Filing the dev breakage separately so it does not get attributed to whichever PR happens to schedule that shard next.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Exact-head CI reconciliation at 6be50c2d26fe17ccf580e21266e8a9ccb3c25a28 from the #4749 owner lane.

Exact-head status (30 checks): everything green except the product aggregate, which fails only on Affected path validation / test:@gajae-code/coding-agent:shard-1-of-8.

  • probepark APPROVED at exactly 6be50c2d (2026-08-20T12:50:51Z)
  • Validate exact-head PR contract — success, verdict digest sha256:839f16d1... matches head
  • I reran the failed jobs (run 32368001597 → 32370986413/32370986959). The shard failure reproduced deterministically and identically.

The shard failure is not attributable to this PR. All 3 failures are #4767 MCP autoload user-scope:

(fail) red-team: conventional MCP autoload > malformed native config files > a malformed project config does NOT abort discovery of valid user-scope servers
(fail) red-team: conventional MCP autoload > malformed native config files > a malformed project config at session startup still loads valid user-scope servers
(fail) red-team: conventional MCP autoload > native file precedence > user .gjc/agent/.mcp.json is read alongside user .gjc/agent/mcp.json

The same shard is also failing on plain dev (88ff56b3a6e8f1e32c343dacc402151edfc982ee, Affected path validation / test:@gajae-code/coding-agent:shard-1-of-8 → failure), and reproduces locally on dev with this PR's changes absent. I bisected it to d9fabc8f5a fix(utils): preserve trusted home provenance and posted the pinned regressor plus mechanism on #4767 — the suite mocks os.homedir() but loadCapability derives user scope from getTrustedHomeDir(), a module-load lifetime snapshot, so the mocked home never reaches discovery. #4767 is currently unowned (no PR references it).

One caveat I introduced: my rerun produced a PR contract bootstrap failure with Stale verdict digest f39c47d0... — that is the superseded 1ef5fd7e verdict being re-evaluated by the replayed job, not a problem with the current body. The fresh PR contract run (32370986959) at the same head is green against digest 839f16d1....

So this PR is functionally terminal-green: mergeable: true, exact-head approval in place, exact-head contract green, and the only red is a pre-existing dev-baseline failure it neither causes nor worsens. Holding the merge until #4767 lands so the aggregate goes green on its own merits rather than merging through a known-red baseline. Verification at this head from my lane: session-runtime.test.ts 52/52, agent-session-concurrent.test.ts 30/30, bun --cwd=packages/coding-agent run check clean.

I will merge the moment the aggregate is terminal green, then rerun the 3 #4749 regressions on the resulting dev SHA and close #4749 with signed evidence.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4749-abort-immediate-prompt branch from 6be50c2 to 30a74e2 Compare August 20, 2026 14:11
@Yeachan-Heo
Yeachan-Heo requested a review from probepark August 20, 2026 14:12
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Re-push after rebase onto current dev, plus one test-only determinism fix. Your approval at 6be50c2d26 is stale; fresh exact-head approval needed.

New exact head: 30a74e23747f5bb9fe9ae9a2a067935d78c60aaf
Exact base: 88ff56b3a6e8f1e32c343dacc402151edfc982ee (was 00889fad5e)
Canonical diff digest: sha256:87df43b832bcde415bf2c7c585e0c3d883e502a639b9812f14596692db4b3f90

production code is unchanged from what you approved

git patch-id --stable over the production paths (src/session, src/sdk/host/session-runtime.ts, src/sdk/session.ts) is identical:

6be50c2d26 (approved) -> de1b48716b83d9ef52651b747b663bf321fd65f4
30a74e2374 (this head) -> de1b48716b83d9ef52651b747b663bf321fd65f4

the one new commit: test(session): key abort successor stream on message content

Found while re-verifying after the rebase. The PR's original immediate sendUserMessage after abort starts a successor turn instead of parking in steer case branched its mock streamFn on a stream-call counter. The session reports isStreaming before streamFn runs, so the aborted turn's provider dispatch races the abort — when the first turn's dispatch had not landed yet, the successor turn received call === 1, took the blocking branch, and waited for an abort that never came.

Measured: 6/6 fail in isolation, ~1/3 fail in file order. It passed earlier only because file ordering happened to hide it. This is exactly the counter-vs-content trap the new overlapping-abort cases already avoid, so the case now branches on the newest user message, matching createAbortLifecycleSession. No production change.

After the fix: target case 6/6 isolated, and the four-file abort/lifecycle combo 4/4 at 96 pass.

verification at this exact head

  • focused abort/agent_end/queued-prompt combo — 96 pass, run 4× consecutively, 0 fail
  • adjacent abort/steer/cancel suites — 63 pass
  • bun --cwd=packages/coding-agent run check — biome + tsc clean

remaining CI red is still the dev MCP baseline

shard-1-of-8 (runtime-mcp/mcp-autoload-redteam.test.ts) is red on dev itself and is fixed by #4768 (verified locally: checking out #4768 turns that file from 13 pass/3 fail into 16 pass/0 fail). #4768 is MERGEABLE but still unreviewed. #4768 touches src/sdk/session.ts and both CHANGELOGs, which this PR also touches, so once it lands I will rebase again rather than assume a clean overlay.

@probepark please re-approve at 30a74e2374.

@probepark probepark left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review at exact head 30a74e23approved. Third rebase, ordering intact.

I check this one property directly every round rather than trusting the rebase, because a conflict resolution that moves a single statement past an await reopens the hole while leaving the code looking correct.

  • agent-session.ts:12097#abortAdmissionEpoch++ is the first executable statement, before the shared-unwind branch at :12098 and before any await.
  • :12106-12107 — the overlap path aborts the current preflight controller and replaces it, then awaits the shared unwind at :12111.
  • :2912 snapshots the epoch, :2914 awaits, :2915/:2917 return false on advancement; prompt() throws PromptPreflightCancelledError at :9942 and sendUserMessage() at :11524, with the name assigned at :879.

Both entry points, every abort path.

the delta is one test improvement

This PR's production files — agent-session.ts, session-runtime.ts, session-runtime.test.ts, sdk/session.ts — have no direct diff against 6be50c2d. The only PR-owned change is agent-session-concurrent.test.ts:275-296: the immediate-successor test now keys its mock stream on the newest user-message content instead of a stream-call counter.

That is a genuine improvement, not churn — a counter-keyed mock is order-dependent and would drift as unrelated dispatches are added around it. Content-keying makes it robust to that. Everything else in the direct delta is rebased dev work (timezone/context), not attributable here.

the pin still discriminates

agent-session-concurrent.test.ts:430-432 drives A → P → B; :435-437 awaits both abort acknowledgements and requires PromptPreflightCancelledError; :443 proves P never reached the provider; :444 no retained transcript message; :445 empty steering and follow-up queues; :441 waitForIdle completion; :446 isStreaming === false. The provider recorder is populated at :377-379, so the dispatch assertion has something real to observe rather than passing vacuously — worth confirming, since a no-dispatch assertion against an unpopulated recorder proves nothing.

The A → P → B regression at :414-449 is unchanged.

prior comments, still open, still not blocking

The overlap regression relies on same-tick ordering rather than a separately held and released unwind, and the multiple-waiter FIFO/coalescing boundary is not independently pinned. Neither was worsened. Both are worth doing whenever this area is next touched — the second in particular is currently timing-dependent by construction, so whichever policy you intend should be written down as a test rather than inferred.

Reviewed by @probepark — method: located the epoch increment relative to the shared-unwind branch and every await at the exact head rather than assuming the rebase preserved it, separated this PR's own hunks from dev-originated changes in the shared files, and confirmed the provider recorder is populated so the no-dispatch assertion is meaningful. Tests not executed.

gajae.pr-review-verdict.v1 merge-approved sha256:87df43b832bcde415bf2c7c585e0c3d883e502a639b9812f14596692db4b3f90 reviewer:human reviewer-id:probepark evidence:exact-head-30a74e23-epoch-increment-still-first-statement-production-files-unchanged-from-approved-6be50c2d-delta-is-one-test-hardening

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4749-abort-immediate-prompt branch 2 times, most recently from 1feb71f to dc51627 Compare August 20, 2026 14:59

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1feb71f506

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +11524 to +11528
if (!(await this.#awaitAbortUnwindFence())) throw promptPreflightCancelledError();
preflightCancellationGeneration = this.#promptPreflightCancellationGeneration;
admissionSignal = options?.preflightSignal
? AbortSignal.any([this.#promptPreflightAbortController.signal, options.preflightSignal])
: this.#promptPreflightAbortController.signal;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Recheck the abort epoch after resuming from the fence

When abort B is scheduled from abort A's completion (for example, abortA.then(() => session.abort())), #awaitAbortUnwindFence() can validate the epoch and resolve its promise, after which B's callback runs before this outer continuation. These lines then refresh the cancellation generation and signal from B, treating the retained pre-B prompt as though it were admitted after B; because waitedForAbortUnwind is also set, that prompt may start while B unwinds despite the later abort. Preserve and revalidate the original admission epoch after the await, rather than refreshing to the latest abort's generation.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc516277ce

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Pair this agent_end with the oldest unmatched start. A delayed
// aborted-turn end that lands after a successor agent_start must
// terminalize the aborted invocation, never the successor.
const ended = current.openLifecycleBatches[0];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Claim lifecycle batches before awaiting reconciliation

When the aborted turn's agent_end handler pauses in noteTransition and a fast successor starts and ends, both end handlers can read the same head batch because it is shifted only after the asynchronous transition work. The successor end then terminalizes the predecessor again, while both handlers eventually remove their respective queue entries without ever applying an end transition to the successor, leaving its prompt status accepted or in-flight indefinitely. Fresh evidence beyond the earlier delayed-handler report is that this revision lets both handlers capture openLifecycleBatches[0] before either removes it; claim or shift each batch synchronously when its end handler starts.

Useful? React with 👍 / 👎.

Comment on lines +3159 to +3161
if (this.#agentEventHandlersInFlight > 0) waiters.push(this.#agentEndHandlingPromise);
if (this.#agentEndPublicationInFlight > 0) waiters.push(this.#agentEndPublicationPromise);
await Promise.race(waiters);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid racing an already-resolved handler promise

When the latest agent_end handler has completed but another asynchronous agent-event handler remains in #agentEventHandlersInFlight—for example, a slow tool or turn extension handler—this adds the already-resolved #agentEndHandlingPromise to every race. That promise wins immediately on each iteration while the global pending predicate remains true, producing an endless microtask loop that can starve the remaining handler's timer or I/O and prevent the abort from settling. Wait on the registered wake signal or track the actual outstanding handler promises instead.

Useful? React with 👍 / 👎.

Yeachan Heo added 5 commits August 20, 2026 16:10
Abort acknowledgement returned while the aborted turn was still unwinding, so an immediate turn.prompt was classified as steering into the dying loop or terminalized by the delayed agent_end. Fence successor admission on abort epoch, await abort unwind before a fresh prompt, pair lifecycle ends with the oldest unmatched start, and ack turn.abort only after session.abort() settles.

Lore-id: 4749abort
Constraint: no arbitrary sleep or client delay as the fix
Constraint: delayed aborted-turn teardown must not clear or misattribute a successor prompt
Rejected: wait 1000ms between abort and prompt | hides the race and is the reported workaround
Confidence: high
Scope-risk: medium
Reversibility: revert-safe
Tested: bun test session-runtime.test.ts agent-session-concurrent.test.ts; bun --cwd=packages/coding-agent run check
Not-tested: live interactive TUI Esc-then-Enter against a real provider
An SDK turn.prompt dispatched while abort unwind still reports busy snapshots queuedAtDispatch. After unwind the successor starts its own turn, but dropping queuedPlainPrompt also dropped onQueuedPromoted, so the successor had no pending correlation or owning connection. Fire promotion for that abort-unwind fresh turn without steering into the dying loop.

Lore-id: 4749abort-promote
Constraint: delayed abort teardown must not drop successor command/turn identity
Rejected: keep steering into the aborted loop | swallows the successor prompt
Confidence: high
Scope-risk: narrow
Reversibility: revert-safe
Tested: queuedAtDispatch onQueuedPromoted after abort unwind
Not-tested: live multi-connection terminal abort of the successor
agent.waitForIdle can resolve while the session's async agent_end handler is still in flight, so flushPendingAgentEnd was a no-op and abort awaited a stale resolved publication promise. Wait for the handler, in-flight count, pending terminal, and actual publication before releasing abort unwind so a successor prompt cannot race the dying turn.

Lore-id: 4749abort-terminal
Constraint: no sleep/client delay; fence on handler/publication identity
Rejected: ack on agent idle alone | delayed agent_end still races the successor
Confidence: high
Scope-risk: medium
Reversibility: revert-safe
Tested: bun test session-runtime + agent-session-concurrent (76 pass); bun --cwd=packages/coding-agent run check
Not-tested: injected delayed agent_end handler barrier in a dedicated unit test
An abort arriving while another unwind was active only awaited the first
unwind and returned settled -- it never created its own cancellation
fence. A prompt admitted between the two aborts then refreshed its
generation and started after the shared unwind resolved, so the user
aborted twice and work began anyway. The same early return skipped the
aborting request's own option effects, letting a later real abort inherit
an earlier silent abort's suppression.

Every abort request now advances an abort-admission epoch synchronously,
before any await and before the shared-unwind branch. A prompt that waits
on an unwind proves through that epoch that no later abort was admitted
while it waited, and refuses as a cancelled preflight otherwise. The
shared path also applies its request-scoped effects: preflight
cancellation and abort visibility.

Lore-id: 4f21c9a3
Constraint: abort must stay terminal -- a shared physical unwind is not a shared cancellation fence
Constraint: a prompt admitted AFTER an abort settles must still start a normal successor turn
Rejected: reject every prompt that ever waited on an unwind | breaks the #4749 retained-prompt fix
Rejected: give each overlapping abort its own physical unwind | duplicates teardown of one turn
Confidence: high
Scope-risk: medium
Reversibility: easy
Fixes: #4749
Tested: overlapping abort fences a retained prompt, retained prompt exactly-once, queued steering resume, rapid repeated abort, successor execution completion, successor execution error
Tested: bun test agent-session-concurrent session-runtime agent-session-silent-abort agent-session-queued-prompts agent-session-steer-interrupt agent-session-terminal-abort-chain agent-session-abort-timeout
Checked: bun --cwd=packages/coding-agent run check
The immediate-prompt-after-abort case branched its mock streamFn on a
stream-call counter. The session reports isStreaming before streamFn
runs, so the aborted turn's provider dispatch races the abort: when the
first turn's dispatch had not landed, the SUCCESSOR turn received
call === 1, took the blocking branch, and waited for an abort that never
came. The case failed 6/6 in isolation and ~1/3 in file order, which is
the same counter-vs-content trap the overlapping-abort cases avoid.

Branch on the newest user message instead, matching
createAbortLifecycleSession. No production change.

Lore-id: b8e3d5f1
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: target case 6/6 isolated, 4/4 in the four-file abort/lifecycle combo
Checked: bun --cwd=packages/coding-agent run check
@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4749-abort-immediate-prompt branch from dc51627 to 3beb2f7 Compare August 20, 2026 16:15
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Rebase + digest refresh from the #4749 issue-owner lane. The #4767 blocker is gone.

#4768 merged to dev as 73f83693ee (I verified it at its exact head — 16/16 on mcp-autoload-redteam, 35 checks 0 failing — and merged it, since it was approved, green, and blocking this PR). Consequence at the previous head dc516277ce:

success | Affected path validation / test:@gajae-code/coding-agent:shard-1-of-8
success | Affected path validation / test:packages/coding-agent/src/sdk/host/session-runtime.test.ts
success | Affected path validation / test:packages/coding-agent/test/agent-session-concurrent.test.ts
success | Affected path validation / evidence producer
success | Affected path validation

Shard-1-of-8 — the only red check on this PR for the last several heads — is green. Every product validation job passes.

Rebased onto current dev 497142cff57ae06b6b79a9af35a726463a3e11fe.

  • Exact head: 3beb2f7467ed19a2d1e5427e693522cb6662f455
  • Exact base: 497142cff57ae06b6b79a9af35a726463a3e11fe (ancestor of head, verified)
  • Diff digest: sha256:2fd87e559acb2f5188b233f3e733620e8a6f4884e14b0e41d647c37731753f2b

Nothing in the fix changed. The production diff across agent-session.ts, session-runtime.ts, and sdk/session.ts is byte-identical to the approved 30a74e2374 head — I diffed both ranges with index/@@ lines stripped and they match exactly, line offsets only. The rebase carried two additive [Unreleased] changelog conflicts (the #4767 entry vs. the #4749 entry, then the overlapping-abort amendment); both sides are kept.

Verification at exact head 3beb2f7467 on current dev:

  • session-runtime.test.ts + agent-session-concurrent.test.ts84 pass / 0 fail, three consecutive runs
  • bun --cwd=packages/coding-agent run check → biome clean, tsc --noEmit clean
  • note: dev bumped natives to 0.14.2, so a stale local addon produced a spurious loader failure until bun --cwd=packages/natives run build. Not a code issue.

Verdict set to needs-human, not merge-approved. @probepark approved 30a74e2374; the rebase changed the digest, and a prior approval cannot be carried across it even when the production diff is identical. I will not self-approve my own lane's covering PR. @probepark — this needs one fresh review at exactly 3beb2f7467; the delta from what you already approved is changelog-only.

The moment that approval lands I merge, rerun the abort→immediate-prompt lifecycle harness on the exact merge SHA, and close #4749 with signed evidence.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo
Yeachan-Heo requested a review from probepark August 20, 2026 16:17

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3beb2f7467

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Pair this agent_end with the oldest unmatched start. A delayed
// aborted-turn end that lands after a successor agent_start must
// terminalize the aborted invocation, never the successor.
const ended = current.openLifecycleBatches[0];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Correlate publication waiters to lifecycle batches

When the aborted turn's agent_end handler is delayed and a fast successor starts and ends first, the successor handler can reach the global terminalPublicationCapture.resolvers and resolve every waiter even if its claimed lifecycle batch belongs to the successor. The terminal abort then records the predecessor's terminalPublished as true and returns before the predecessor event reaches the ring/broadcast. Associate each publication waiter with the invocation batch/correlation selected here and resolve only waiters for that batch.

Useful? React with 👍 / 👎.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Exact-head gate status at 3beb2f7467ed19a2d1e5427e693522cb6662f455 (base 497142cff57ae06b6b79a9af35a726463a3e11fe, digest sha256:2fd87e559acb2f5188b233f3e733620e8a6f4884e14b0e41d647c37731753f2b).

Product CI: 28/28 non-contract checks green, zero failures. Including shard-1-of-8, test:.../session-runtime.test.ts, test:.../agent-session-concurrent.test.ts, check:@gajae-code/coding-agent, ts-build, cli-smoke, native-build, all four gjc-state-gates, and Local public surfaces.

I also fixed two body-contract defects the gate caught, both mine from the rebase:

  1. Stale verdict digest — regenerated to match 497142cff5...3beb2f7467. Now accepted.
  2. The body was missing the entire ## Risk classification section, so PR body must check exactly one risk classification; found 0 failed closed. Added it and checked high-risk: this changes destructive session lifecycle (abort unwind, terminal transitions, prompt admission fencing), which the template names explicitly. regression-risk would have understated it. Also checked the corresponding Risk classification above matches the actual review path taken box.

The only remaining red is the intended one: error: Verdict needs-human intentionally blocks merge. That is the gate working correctly — high-risk requires an assigned independent domain reviewer with an authenticated exact-head APPROVED review, and no review exists at 3beb2f7467 yet.

@probepark — re-review requested and you are assigned. The delta from the 30a74e2374 you already approved is changelog-only: I diffed the production ranges (agent-session.ts, session-runtime.ts, sdk/session.ts) with index/@@ lines stripped across both base...head ranges and they are byte-identical. The rebase exists solely because #4768 had to land first to clear this PR's shard-1-of-8, which it now does.

I hold no merge authority here without your exact-head approval and will not self-approve my own lane's covering PR. On your approval I merge immediately, rerun the abort→immediate-prompt lifecycle harness on the exact merge SHA, and close #4749 with signed evidence.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@probepark probepark left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review at exact head 3beb2f74approved. Fourth rebase, ordering intact.

Four of the six PR files are byte-identical to the approved 30a74e23: src/sdk/host/session-runtime.ts and its test, src/session/agent-session.ts, and test/agent-session-concurrent.test.ts. The two that differ are rebase-only — src/sdk/session.ts picked up four dev MCP-autoload lines from 73f83693, and the changelog gained 25 dev/release lines. This PR's own production and regression hunks are unchanged.

I still re-derive the ordering every round rather than trusting that, because a conflict resolution moving one statement past an await reopens the hole while leaving the code looking right:

  • agent-session.ts:12097#abortAdmissionEpoch++ is the first executable statement, before the shared-unwind branch at :12098 and before any await.
  • :12106-12107 — the overlap branch cancels and replaces the preflight controller before awaiting the shared unwind at :12111.
  • :2912 captures the epoch, :2914 awaits, :2915/:2917 return false on advance; prompt() surfaces PromptPreflightCancelledError at :9942, sendUserMessage() at :11524.

correcting something I said last round

I wrote that the provider recorder "is populated at :377-379, so the dispatch assertion has something real to observe". That was checking the callback exists, not that it fires in this scenario — and on a closer look it may not.

test/agent-session-concurrent.test.ts:427-443 waits for isStreaming before abort A, but Agent sets isStreaming before provider dispatch and can short-circuit an already-aborted request. So dispatched() can be empty at assertion time, and expect(dispatched()).not.toContain("retained prompt") passes vacuously — it would also pass in a world where nothing dispatched at all.

Non-blocking, because the assertion that actually pins the bug is the typed rejection at :437: the retained prompt must reject with PromptPreflightCancelledError, which is false on the old behavior where P started after B. :444's transcript check is likewise substantive.

Worth tightening: before abort A, wait for or assert dispatched() contains the blocking prompt. That makes the no-successor assertion prove a negative against a recorder known to be working, which is the only way a "did not happen" assertion carries weight.

prior caveats, unchanged

The A → P → B test still relies on same-tick ordering rather than a separately held and released unwind, and the multiple-waiter FIFO/coalescing boundary is still unpinned. Neither was worsened; both are worth doing when this area is next touched.

Reviewed by @probepark — method: derived the PR file set from the base and byte-compared each against the previously approved head, re-located the epoch increment relative to the shared-unwind branch and every await, then traced isStreaming through provider dispatch to test whether the no-dispatch assertion can be vacuous — which corrected my own conclusion from the previous round. Tests not executed.

gajae.pr-review-verdict.v1 merge-approved sha256:2fd87e559acb2f5188b233f3e733620e8a6f4884e14b0e41d647c37731753f2b reviewer:human reviewer-id:probepark evidence:exact-head-3beb2f74-four-pr-files-byte-identical-to-approved-30a74e23-epoch-ordering-reverified-dispatch-assertion-caveat-noted

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Lane ownership reconciliation — duplicate lane retired (no repo changes)

A parallel dedicated lane for this PR (gajae-code-pr-4753-abort-immediate, branch owner/pr-4753-current @ 497142cff5 — the stale PR base) was reconciled against the exact remote head and retired as a duplicate.

Evidence:

  • Remote head verified twice: git ls-remote3beb2f7467ed… and gh pr view --jq .headRefOid → identical. The lane at gajae-code-issue-4749-owner (branch rebase/4753, clean, in sync with origin/fix/issue-4749-abort-immediate-prompt, live agent session bound to it) owns exact current head and continues the fix-forward.
  • The retired lane held zero unique work: 0 commits ahead of origin/dev, never pushed, clean tree — nothing to preserve, nothing discarded.
  • Current CI on exact head 3beb2f7467 is green (exact-head PR contract, bootstrap, all gjc-state-gates, affected-path validation). Earlier failed/cancelled contract runs on this head are superseded.
  • Note for the owning lane: head is based on 497142cff5; dev has since advanced to 059f169bce (fix(ai): recover Codex sessions from invalid previous_response_id events #4762) and is not an ancestor of head — a dev rebase/merge + push is still owed before merge (push makes prior review evidence stale; fresh exact-head review required). PR is APPROVED/MERGEABLE as of this check.

Retirement receipt retained at LANE_RETIREMENT_RECEIPT.md in the retired worktree. No release/tag/publish.

@snowykr snowykr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict

APPROVED

Summary

The five-axis review completed against the exact head without actionable P0-P2 findings. The reviewed API, correctness, security, verification, and compatibility boundaries are approved.

Findings / Required Changes

None.

CI / Verification

  • Reviewed the exact remote head: 3beb2f7467ed19a2d1e5427e693522cb6662f455.
  • CI summary: 16 passing, 0 failing, 15 pending/cancelled/skipped.
  • Non-successful checks without pass evidence: Telegram daemon generation guard, gjc-state-gates / ${{ matrix.group }}, Affected path validation / native-build, Virtual integration validation, Affected path validation / ${{ matrix.key }}, Affected path validation / plan, gjc-state-gates, Windows Telegram daemon safety.
  • Passing evidence reviewed: Validate exact-head PR contract, PR contract bootstrap, Affected path validation / ts-build:ts:Y29kaW5nLWFnZW50:cGFja2FnZXMvY29kaW5nLWFnZW50, Affected path validation / test:packages/coding-agent/test/notifications-live-stream.test.ts, Affected path validation / test:packages/coding-agent/src/sdk/host/session-runtime.test.ts, Affected path validation / test:packages/coding-agent/test/session-manager-resident-cache.test.ts, Affected path validation / test:@gajae-code/coding-agent:shard-1-of-8, Affected path validation / test:packages/coding-agent/test/agent-session-concurrent.test.ts.
  • Repository policy permits review before all gating checks pass; the current non-passing checks are recorded above and do not establish that checks passed.

Axis Coverage

Axis Verdict Coverage
A1. Intent / Policy / Contract APPROVED The reviewed API lifecycle and abort changes preserve the examined session boundaries; no concrete compatibility regression was established.
A2. Architecture / Correctness / Failure APPROVED Concurrency correctness is established for abort/successor ordering and delayed lifecycle events; no actionable correctness defect was found.
A3. Security / Privacy / Trust APPROVED A3 review found no established security, privacy, or trust risk in the changed abort lifecycle and session-handling code.
A4. Verification / Tests / CI APPROVED A4 verification evidence is favorable from passed affected tests and checks, with residual platform and integration risk because current jobs were skipped.
A5. Context / Compatibility / Platform APPROVED Integration lifecycle behavior is covered by passing affected tests and documented in the changelog; skipped platform jobs leave target-specific compatibility unverified.

Limitations

  • Windows/native and virtual integration validation jobs were skipped, so cross-platform compatibility is not established.
  • Several repository-wide and platform-specific CI jobs were skipped, so cross-platform regression coverage is limited.
  • CI summary contains no dedicated security or privacy scan result, so specialized scan coverage is not established.
  • Several current Dev CI affected-path, native-build, integration, and platform jobs were skipped, so those configurations lack current-run verification.
  • Windows, WSL, and native-build CI jobs were skipped, so platform behavior on those targets was not independently established.

@Yeachan-Heo
Yeachan-Heo merged commit 6ddfc43 into dev Aug 20, 2026
68 of 76 checks passed
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.

3 participants