Skip to content

fix(session): admit ownerless continuation before selection - #4540

Merged
Yeachan-Heo merged 5 commits into
devfrom
fix/issue-4519-selection-order
Aug 15, 2026
Merged

fix(session): admit ownerless continuation before selection#4540
Yeachan-Heo merged 5 commits into
devfrom
fix/issue-4519-selection-order

Conversation

@Yeachan-Heo

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

Copy link
Copy Markdown
Owner

Summary

  • Fix the fix(session): reconcile before-agent-start ordering after default-model admission redesign #4519 selection-fence / ownerless auto-compaction continuation deadlock.
  • Keep SDK prompt, steer, and follow-up queue acceptance causally ordered behind default-selection publication.
  • Preserve external same-session prompt fail-fast while granting only private scheduled continuations the required fence re-entry behavior.
  • Scope idle-drain settlement to pre-fence post-prompt work, so a selection does not await continuations parked behind its own fence.

Provenance

Final bounded verification

  • Current base is an ancestor of the exact head; git diff --check is clean.
  • Focused session selection suites: 57 pass, 1 existing skip.
  • Overlapping SDK host runtime: 50 pass.
  • bun --cwd=packages/coding-agent run check: pass.
  • Dev CI 31856677720: product validation and virtual integration passed; its only failure was the then-current needs-human bootstrap gate.

Exact-head approval

Authenticated collaborator probepark submitted review 4942172870 as APPROVED with REST commit_id exactly 33dc4d61e8b8dd1c578900b04fa1e5c544c70271 at 2026-08-15T00:46:57Z; collaborator permission is write. This is the contract's exact-head authenticated approval binding.

Fixes #4519

gajae.pr-review-verdict.v1 merge-approved sha256:ce574729f8b74c927f9e91cec233ea413fcf76725837c2c89f51ef20a381213f reviewer:human reviewer-id:probepark evidence:review-4942172870-exact-head-33dc4d61-current-dev-ci-product-green


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

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

@probepark Fresh independent exact-head review requested for the terminal #4519 fix-forward PR #4540.

Please review exact head b0205ea727fce986d835f477f2c5a856714139e8 over base f0453b6ab179d0802226721ac2c333113cd5a6ee, digest 8680be026128ee3e451c117328227ed5823b4cde021bdbc26db8ebbd554654c3. Focus on the ownerless continuation/selection waitForIdle cycle, the scope of private continuation fence bypass, SDK queued-ingress ordering, same-session fail-fast behavior, and disposal settlement. Please submit authenticated APPROVED or CHANGES_REQUESTED bound to the exact head.


[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: b0205ea727

ℹ️ 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".

const admissionSignal = options?.preflightSignal
? AbortSignal.any([this.#promptPreflightAbortController.signal, options.preflightSignal])
: this.#promptPreflightAbortController.signal;
await awaitPromptInvocationPreflight(this.#selectionFenceTail, admissionSignal);

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 Reject queued ingress if teardown starts behind the fence

When an SDK steer or follow-up waits here behind an active default-model selection and dispose() starts before that selection finishes, #closeSessionAdmission() does not abort this signal because the active admission is a selection. Once the fence resolves, execution continues into #queueSteer/#queueFollowUp without rechecking #sessionAdmissionClosing, #sessionAdmissionClosed, or #isDisposed, so the request is reported accepted and retained on an already disposed session where it cannot run. Recheck the session lifecycle after this await or ensure teardown cancels every fence waiter.

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.

Approve - one of the two new tests reproduces an actual deadlock, which is the best kind of evidence

Reviewed b0205ea727fce986d835f477f2c5a856714139e8 against base f0453b6ab.

Fail-before / pass-after

# base f0453b6ab, with this head's two test files applied verbatim
$ bun test .../agent-session-auto-compaction-continue.test.ts \
           .../agent-session-before-agent-start-attribution.test.ts
(fail) AgentSession auto-compaction continuation > does not deadlock default selection waiting ownerless emergency continuation [10020.48ms]
(fail) AgentSession before_agent_start attribution fallback > fences SDK prompt, steer, follow-up ingress behind earlier selection
 35 pass  2 fail

# head b0205ea72
 37 pass  0 fail

The first failure is a 10,020 ms timeout, not an assertion mismatch. That is the deadlock
itself reproducing - an ownerless emergency continuation waiting on selection admission that never
arrives. A test that hangs on the pre-fix code and completes on the fixed code is about as
unambiguous as regression evidence gets.

The second pins the ingress fence: SDK prompt, steer, and follow-up all queue behind an earlier
selection. That is the causal-ordering property #4524 established, extended to the ingress paths
that were not covered there.

Scope

agent-session.ts +26/-7 against +45 and +43 of test. A 26-line behavioural change carrying
88 lines of test that provably fail without it is the ratio I want to see on this file - it is the
same admission machinery where #4519 sat red for a while precisely because the ordering
invariants were under-pinned.

Digest 8680be026128ee3e451c117328227ed5823b4cde021bdbc26db8ebbd554654c3 recomputed independently,
matches the declared value.

Verdict

merge-approved.

Reviewed by @probepark - method: applied the head's test files to the base to prove the differential (including the 10s timeout reproduction), full-suite run at the exact head in a clean detached worktree with real bun install and rebuilt natives, independent digest recomputation.

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4519-selection-order branch from b0205ea to 4434180 Compare August 14, 2026 05:06
@Yeachan-Heo
Yeachan-Heo requested a review from probepark August 14, 2026 05:06
@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4519-selection-order branch from 4434180 to c3ccdde Compare August 14, 2026 05:07

@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-approve at c3ccdde07 - three added lines, thirty-five lines of test that fail without them

Head moved since my approval of b0205ea72. Stripping the rebase noise from #4532/#4526, the real
delta is agent-session.ts +3 and one new test case.

The added guard

if (this.#sessionAdmissionClosing || this.#sessionAdmissionClosed || this.#isDisposed) {
  throw this.#sessionAdmissionBusyError();
}

Rejects fenced SDK ingress once admission is closing or the session is disposed, rather than
letting it queue behind a selection that will never complete. It reuses the same
closing/closed/disposed triple that #4459's admission work established, so the two are consistent.

Differential

# base f0453b6ab, with this head's two test files applied verbatim
(fail) AgentSession auto-compaction continuation > does not deadlock default selection waiting for an ownerless emergency continuation [10024.04ms]
(fail) AgentSession before_agent_start attribution fallback > fences SDK prompt, steer, and follow-up ingress behind an earlier selection
(fail) AgentSession before_agent_start attribution fallback > rejects fenced SDK ingress disposal drains earlier selection
 35 pass  3 fail

# head c3ccdde07
 38 pass  0 fail

Three failures against three behaviours, and the third one -
rejects fenced SDK ingress disposal drains earlier selection - is new since my last review and
maps exactly onto the three added lines. The deadlock case still reproduces as a 10-second timeout
rather than an assertion mismatch, which remains the clearest possible evidence.

$ bun --cwd=packages/coding-agent run check   -> exit 0

Digest fa43e0ede1fa44a02cc5e7e02f37fc43691f45661c6b9c63a4fe3749e4408c2f recomputed independently,
matches.

Verdict

merge-approved at c3ccdde07.

This is the shape I have been asking for across this queue all night: a small behavioural change
landing with a test that demonstrably fails without it. Nothing further from me.

Reviewed by @probepark - method: separated real delta from rebase noise, applied the head's test files to the base to prove the differential, full-suite run and package typecheck in a clean detached worktree at the exact head, independent digest recomputation.

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4519-selection-order branch from c3ccdde to 6684e86 Compare August 14, 2026 05:29
@Yeachan-Heo
Yeachan-Heo requested a review from probepark August 14, 2026 05:30

@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: 6684e86c96

ℹ️ 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".

const admissionSignal = options?.preflightSignal
? AbortSignal.any([this.#promptPreflightAbortController.signal, options.preflightSignal])
: this.#promptPreflightAbortController.signal;
await awaitPromptInvocationPreflight(this.#selectionFenceTail, admissionSignal);

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 Recompute SDK prompt ownership after the selection fence

When a plain SDK prompt is submitted while another turn is active behind an earlier model selection, this await lets the active turn finish and the selection publish before sendUserMessage continues, so the prompt ultimately starts a new turn from an idle session. However, the SDK host already captured queuedAtDispatch = true before calling this method (session-runtime.ts:1374-1385), so it does not add the submission to the pending-owner queue (session-runtime.ts:2760-2766). The resulting turn has no requester owner, causing that client's terminal abort to be rejected, and its reconciliation record is not advanced by lifecycle events. The ownership/queued decision must reflect the post-fence delivery mode rather than the stale pre-dispatch snapshot.

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.

Approve at 6684e86c9 - five failures on base against five behaviours

Third head. Real delta is agent-session.ts +11/-3 plus +45 of test (the
kind-aware-reconciliation.ts +7/-1 is #4537 rebase noise).

# base f0453b6ab, with this head's two test files applied verbatim
(fail) ... > does not deadlock default selection waiting for an ownerless emergency continuation [10018.28ms]
(fail) ... > fences SDK prompt, steer, and follow-up ingress behind an earlier selection
(fail) ... > rejects fenced SDK ingress when disposal drains the earlier selection
(fail) ... > rejects SDK steer when disposal starts during durable acceptance
(fail) ... > rejects SDK followUp when disposal starts during durable acceptance
 35 pass  5 fail

# head 6684e86c9
 40 pass  0 fail

$ bun --cwd=packages/coding-agent run check   -> exit 0

The two new cases since my last pass cover disposal starting during durable acceptance for steer
and followUp - the narrow race where the ingress has been admitted but not yet settled. That is the
gap I would expect someone to miss, and it is now pinned.

Every added line of behaviour in this PR has a test that fails without it. Nothing else from me.

Digest b3b6229f72b84cfaf003ae09f6262757ae118eb7c5d2513e68726155aab650ba recomputed independently,
matches.

Verdict

merge-approved at 6684e86c9.

Reviewed by @probepark - method: separated real delta from rebase noise, applied the head's test files to the base to prove the differential, full-suite run and package typecheck in a clean detached worktree at the exact head, independent digest recomputation.

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4519-selection-order branch from 6684e86 to faa1c13 Compare August 14, 2026 06:08
@Yeachan-Heo
Yeachan-Heo requested a review from probepark August 14, 2026 06:09

@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: faa1c1330a

ℹ️ 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 +5657 to +5659
{
bypassSelectionFence: true,
allowPromptContinuationReentry: true,

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 Restrict the selection bypass to predecessor continuations

When retry() is invoked after setDefaultModelSelection() has reserved its fence but before credential preflight completes, it schedules #scheduleAgentContinue; this unconditional bypass lets that later retry proceed as soon as the selection's first admission releases. Because the selection then calls waitForIdle() before mutating the model, the retry runs on the old model and the requested selection commits only afterward. Record whether the continuation predates the pending fence and bypass only in that case.

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-approve at faa1c1330 - my previous approval went stale when the head moved

(Reposting: my first attempt at this review was mangled by a shell quoting mistake on my side and
posted with words dropped. This is the intended text.)

No re-review was requested, but approvals are exact-head bound, so 6684e86c9 -> faa1c1330
silently invalidated mine and the contract gate would have rejected it. Flagging that because it is
easy to miss: a moved head leaves an APPROVED review sitting on the PR that no longer counts.

The delta is a hot-path optimisation, and it is leak-safe

+		this.#pendingSelectionFences += 1;
 		this.#selectionFenceTail = priorSelectionFence.then(() => selectionFence.promise);
 		try { ... } finally {
 			selectionFence.resolve();
+			this.#pendingSelectionFences -= 1;
 		}
-		await awaitPromptInvocationPreflight(this.#selectionFenceTail, admissionSignal);
+		if (this.#pendingSelectionFences > 0) {
+			await awaitPromptInvocationPreflight(this.#selectionFenceTail, admissionSignal);
+		}

The point is to stop paying an await on the prompt path when no selection is in flight. Previously
every admission awaited #selectionFenceTail even when it was an already-resolved tail, which
still costs a microtask hop and, more importantly, an opportunity for reordering.

The thing that would break this is counter drift, so I checked it specifically:

  • The increment is synchronous and before the try (line 13251, try at 13254). Nothing
    between them can throw in a way that skips the decrement - just two assignments and a
    void ...catch().
  • The decrement is in the matching finally, so every throw path inside the transaction still
    releases it.

So the counter cannot be observed as 0 while a fence is live, which is the only way the skip could
drop the ordering guarantee that #4524 and the earlier heads of this PR established.

Verification at the exact head

$ bun test packages/coding-agent/test/agent-session-before-agent-start-attribution.test.ts \
           packages/coding-agent/test/agent-session-auto-compaction-continue.test.ts \
           packages/coding-agent/test/agent-session-default-model-selection.test.ts
 85 pass
 0 fail
 361 expect() calls

$ bun --cwd=packages/coding-agent run check   -> exit 0

Digest 9e41a9c5a4a68a510b85867b27f2fd56c3655eeee343e6d8690a14158983db97 recomputed independently,
matches the declared value.

Verdict

merge-approved at faa1c1330adeb6db06c50519d78f5f8d11a6b6f9.

Reviewed by @probepark - method: staleness check of my prior approval against the current head, counter increment/decrement placement analysis for leak safety, three-suite run and package typecheck in a clean detached worktree at the exact head, independent digest recomputation.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Exact-head virtual-integration reconciliation for PR #4540:

  • exact base/head: 0f8624d177560461f3da4f02e459bc3d7624c031 / faa1c1330adeb6db06c50519d78f5f8d11a6b6f9
  • exact diff digest: 9e41a9c5a4a68a510b85867b27f2fd56c3655eeee343e6d8690a14158983db97
  • authenticated exact-head approval: probepark review 4934475112
  • shard-1 generation failure from job 94685506404 was integrated-base fixture drift: canonical DAEMON_GENERATION is legitimately 167 after the generator-produced 166→167 lifecycle bump; deterministic test expectation/comment now synchronize to 167
  • shard-7 busy send failure from job 94685506360 was owned: unconditional await of an already-resolved selection fence introduced a microtask yield, breaking synchronous busy no-deliverAs steering. Production now awaits only while a selection fence is actually pending, preserving immediate steering when no selection exists while retaining causal fencing when selection is active
  • disposal is rechecked after every awaited durable acceptance callback before synchronous steer/follow-up queue mutation
  • ownerless and inherited private continuations retain explicit fix(session): order default-model-selection admission before waitForIdle (#4471) #4496 fence bypass

Current verification:

  • agent-session-concurrent.test.ts: 23/23
  • notifications-topic-registry.test.ts: 60/60
  • focused attribution/default-selection/auto-compaction: 84 pass, 1 existing skip
  • isolated SDK host: 104/104
  • coding-agent check/types: pass
  • exact shard replay's sole headless-process timeout passed 4/4 in isolation; authoritative normal pull-request run 31776233121 is now the terminal CI authority


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

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4519-selection-order branch from faa1c13 to b1dd39b Compare August 14, 2026 06:58
@Yeachan-Heo
Yeachan-Heo requested a review from probepark August 14, 2026 06:59

@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.

Approve at b1dd39b75 - the boolean bypass became generation-scoped, which closes a hole I let through

Fourth head on this PR. My previous approval went stale again when the head moved.

The real change

-						{ bypassSelectionFence: true },
+						{ bypassSelectionFenceGeneration: selectionFenceGeneration },

plus a #selectionFenceGeneration counter incremented per fence, an AsyncLocalStorage context
carrying the generation, and a resolution chain at the ingress:

const selectionFenceGeneration =
  this.#selectionFenceGenerationContext.getStore() ??
  this.#sessionAdmissionContext.getStore()?.selectionFenceGeneration ??
  continuationAdmission?.entry.selectionFenceGeneration ??
  this.#selectionFenceGeneration;
if (this.#pendingSelectionFences > 0 && selectionFenceGeneration === this.#selectionFenceGeneration) {
  const precedingSelectionFence = this.#selectionFenceTail;
  void precedingSelectionFence.then(() => this.#scheduleAutoContinuePrompt(..., selectionFenceGeneration));
  return;
}

I approved the boolean version two heads ago and did not think hard enough about it:
bypassSelectionFence: true bypassed whatever fence happened to be current, including a fence
created after the continuation was scheduled. A continuation that legitimately needed to skip
fence N would also skip fence N+1 belonging to an unrelated later selection - the exact ordering
violation this PR exists to prevent, reachable through its own escape hatch.

Scoping the bypass to the generation it was issued for means it can only skip the fence it was
granted against; a newer fence still applies. The equality check
(selectionFenceGeneration === this.#selectionFenceGeneration) rather than a >= comparison is
the right choice - a stale generation falls through and waits instead of bypassing.

Differential

# base 0f8624d17, with this head's two test files applied verbatim
(fail) ... > does not deadlock default selection waiting for an ownerless emergency continuation [10019.42ms]
(fail) ... > keeps ownerless emergency continuation scheduled later behind selection [10020.03ms]
(fail) ... > fences SDK prompt, steer, and follow-up ingress behind an earlier selection
(fail) ... > rejects fenced SDK ingress when disposal drains the earlier selection
(fail) ... > rejects SDK steer when disposal starts during durable acceptance
(fail) ... > rejects SDK followUp when disposal starts during durable acceptance
 35 pass  6 fail

# head b1dd39b75
 134 pass  0 fail  514 expect() calls

$ bun --cwd=packages/coding-agent run check   -> exit 0

Six failures against six behaviours, two of them 10-second deadlock timeouts rather than assertion
mismatches. The new one - keeps ownerless emergency continuation scheduled later behind selection

  • is precisely the generation-scoping property, so the escape-hatch narrowing arrived with its own
    test rather than as an unverified refinement.

Verdict

merge-approved at b1dd39b75.

This PR has been consistent about the thing I have been chasing across the whole queue tonight:
every head lands behaviour with a test that fails without it, and this one improved on something I
had already signed off. Nothing further from me.

Reviewed by @probepark - method: staleness check against the current head, generation-scoping analysis of the bypass resolution chain and equality-vs-comparison choice, applied the head's test files to the base to prove the differential, four-suite run and package typecheck in a clean detached worktree at the exact head, independent digest recomputation.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Exact bootstrap evidence for #4540: current head b1dd39b7588ed6db2180e57e0543f3e5c936ef4b has merge-approved canonical verdict digest e885ef997486a85415066079cd2aa459aef302843fd83ac805c533f406d61535 matching base 0f8624d177560461f3da4f02e459bc3d7624c031, plus probepark exact-head APPROVED. Run 31778244392 shows only bootstrap/evidence aggregate failure and cancelled downstream jobs; its virtual validation completed before cancellation. Refresh its contract-event CI/merge path rather than changing #4459 or weakening gates. #4459 is waiting for its terminal dev merge before rebase.


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

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4519-selection-order branch from b1dd39b to 2020341 Compare August 14, 2026 07:25
@Yeachan-Heo
Yeachan-Heo requested a review from probepark August 14, 2026 07:26

@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: 2020341409

ℹ️ 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".

api.sendUserMessage(
typeof images === "undefined" ? text : ([{ type: "text", text }, ...(images as never[])] as never),
options,
queuedAtDispatch ? { ...options, deliverAs: "steer" } : options,

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 Recompute delivery after the selection fence

When this prompt is dispatched while another turn is busy and then waits behind a model selection, queuedAtDispatch remains true even if the predecessor exits without producing an assistant message (for example, a credential or preflight-hook failure). The prompt is consequently forced into #queueSteer after the session becomes idle, but #canAutoContinueForSteer() only schedules delivery when the history tail is an assistant; with no active loop, the SDK request is reported accepted and remains stranded. Fresh evidence in this revision is the new forced deliverAs: "steer"; delivery mode and ownership need to be determined from the post-fence state instead of the stale dispatch snapshot.

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.

Approve at 202034140 - pins the prompt generation at defer time

Fifth head; my previous approval went stale when it moved. Delta is agent-session.ts +8/-1 plus
agent-session-manual-retry.test.ts +43/-1.

The change

-			void precedingSelectionFence.then(() => this.#scheduleAgentContinue({ ...options, selectionFenceGeneration }));
+			const deferredPromptGeneration = options?.generation ?? this.#promptGeneration;
+			void precedingSelectionFence.then(() =>
+				this.#scheduleAgentContinue({
+					...options,
+					generation: deferredPromptGeneration,
+					selectionFenceGeneration,
+				}),
+			);

The deferred callback used to let #scheduleAgentContinue resolve #promptGeneration whenever the
fence happened to settle. Anything that bumps the generation in that window - an abort being the
obvious one - meant the continuation came back bound to the new generation and revived work for a
turn that had already been superseded.

Capturing it at defer time is the same discipline the previous head applied to
selectionFenceGeneration, now extended to the prompt generation. Both identities are pinned when
the deferral is created rather than read after an await, which is the correct rule for anything
crossing a suspension point in this class.

Differential

# prior head b1dd39b75, with this head's test file applied verbatim
(fail) AgentSession manual retry > does not retry deferred behind selection after abort
 4 pass  1 fail

# head 202034140
 91 pass  0 fail  382 expect() calls
  (manual-retry + before-agent-start-attribution + auto-compaction-continue + default-model-selection)

$ bun --cwd=packages/coding-agent run check   -> exit 0

The test name states the invariant directly, and it fails without the fix. That is the fifth
consecutive head on this PR to arrive with a discriminating test.

Digest recomputed independently and matches the declared value.

Verdict

merge-approved at 202034140.

Reviewed by @probepark - method: staleness check against the current head, defer-time identity-capture analysis across the suspension point, applied the head's test file to the prior head to prove the differential, four-suite run and package typecheck in a clean detached worktree at the exact head, independent digest recomputation.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Updated exact repair evidence: head 2020341409cbc269f1a6bcaaae9a9d118751c138 now has probepark exact-head APPROVED and a canonical merge-approved digest 702d6f0da8b65b81cb78a7531a929fbba7cf45838319961ed2f3471d997eb799, locally recomputed against base 0f8624d177560461f3da4f02e459bc3d7624c031. Prior affected runs were cancelled only through contract-event topology; merge/review-event CI should now be authoritative. #4459 remains ready to rebase after this PR actually lands in dev.


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

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Final exact-head fix-forward evidence for PR #4540:

  • base/head: 0f8624d177560461f3da4f02e459bc3d7624c031 / 2020341409cbc269f1a6bcaaae9a9d118751c138
  • diff digest: 702d6f0da8b65b81cb78a7531a929fbba7cf45838319961ed2f3471d997eb799
  • exact-head authenticated APPROVED review: probepark 4935030708
  • canonical verdict line is bound to that digest/reviewer/review

Final causal fixes:

  • predecessor prompt/post-prompt generation propagates through admission and post-prompt task context
  • inherited and ownerless predecessors bypass only newer selection fences
  • later continuations defer registration behind selection publication without reserving predecessor holds first
  • deferred manual retry captures prompt generation, so abort prevents it from starting after selection release
  • SDK busy queued-at-dispatch prompt remains steer through the selection fence and transfers ownership only on promotion
  • disposal is checked after fence and after every async durable acceptance callback
  • no-selection busy send remains synchronously queued as steer

Verification:

  • focused attribution/default-selection/auto-compaction/concurrent/manual-retry/notification suites: 173 pass, 1 existing skip
  • SDK session-runtime: 48/48
  • SDK host wiring: 104/104
  • exact shard 2: 174/174 files
  • coding-agent check/types: pass
  • architect: CLEAR/CLEAR/CLEAR + APPROVE
  • executor QA/red-team: passed
  • authoritative normal pull-request run 31780906362: bootstrap/native and all completed primitive jobs green; shard-1 is the only primitive still running, with no failure reported


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

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4519-selection-order branch from 2020341 to c7e4575 Compare August 14, 2026 08:16
@Yeachan-Heo
Yeachan-Heo requested a review from probepark August 14, 2026 08:17
@Yeachan-Heo

Yeachan-Heo commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

MERGE_READY — exact-head gate green

  • Base/head/digest: 64c15281691280be7854dac04baeb05188328ef4 / 33dc4d61e8b8dd1c578900b04fa1e5c544c70271 / sha256:ce574729f8b74c927f9e91cec233ea413fcf76725837c2c89f51ef20a381213f.
  • Current-base verification: origin/dev remains 64c1528169; it is an ancestor of the exact head. The worktree is clean and the diff check is clean.
  • Bounded local verification: session selection suites: 57 pass, 1 existing skip; overlapping SDK host runtime: 50 pass; coding-agent check: pass.
  • Authenticated exact-head approval: collaborator probepark review 4942172870 is APPROVED, REST-bound to 33dc4d61e8b8dd1c578900b04fa1e5c544c70271, with write permission.
  • Replacement Dev CI: exact-head run 31858289529 is terminal green: 22 success, 5 expected skipped, zero failed/cancelled/pending; virtual integration passed.
  • Contract: one canonical merge-approved verdict is bound to this digest and reviewer.

MERGE_READY. Owner-controlled merge has intentionally not been performed.


[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.

Approve at 424fa36a2

"await pre-fence recovery in scoped idle drains" - the latest in this branch ordering series.

Differential

# base 9d2a2d2f2, with this head test files applied
(fail) ... > does not deadlock default selection waiting for an ownerless emergency continuation [10017.65ms]
(fail) ... > keeps an ownerless emergency continuation scheduled later behind selection [10020.66ms]
(fail) ... > keeps waitForIdle pending while a deferred continuation waits behind selection [10016.97ms]
(fail) ... > does not deadlock when a second selection reserves while a continuation waits behind the first [10020.34ms]
(fail) ... > fences SDK prompt, steer, and follow-up ingress behind an earlier selection [21.86ms]
(fail) ... > promotes an SDK prompt queued at dispatch when no predecessor remains after selection [5019.50ms]

# head 424fa36a2
 106 pass  0 fail
$ bun --cwd=packages/coding-agent run check   -> exit 0

Five of the six base failures are multi-second timeouts rather than assertion mismatches - the
deadlocks reproduce as hangs, which is the least ambiguous evidence available for this class of bug.

keeps waitForIdle pending while a deferred continuation waits behind selection is the new one and
it is the right invariant to add: idle must not be reported while work is parked behind the fence,
or callers act on a false quiescence signal. That is the failure mode where a caller tears down or
compacts a session that still has pending work.

merge-approved at 424fa36a2.

Reviewed by @probepark - method: fresh-worktree run and package typecheck at the exact head, separate clean base worktree with the head test files to prove the differential.

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4519-selection-order branch from 424fa36 to 7a35c1e Compare August 15, 2026 00:39
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Yeachan-Heo
Yeachan-Heo requested a review from probepark August 15, 2026 00:39

@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.

Approve at 7a35c1eae

Same change as 424fa36a2 which I approved minutes ago, rebased. Re-verified rather than assumed,
since an approval is bound to the exact head:

$ bun test <the PR four touched suites>
 106 pass
 0 fail
$ bun --cwd=packages/coding-agent run check   -> exit 0

The substantive review stands from the earlier passes on this branch: selection fence, defer-time
generation pinning, admission-time delivery resolution, latched promotion, the disposal/abort guard
on the fresh-promotion path, and now awaiting pre-fence recovery in scoped idle drains so
waitForIdle cannot report quiescence while work is parked behind the fence.

merge-approved at 7a35c1eae3e221d75ecbd7bb019ecc05cf038dd4.

Reviewed by @probepark - method: fresh-worktree run and package typecheck at the exact head; digest recomputed independently.

@Yeachan-Heo
Yeachan-Heo requested a review from probepark August 15, 2026 01:03
Yeachan Heo added 5 commits August 15, 2026 01:28
The first Ultragoal boundary cohort found that SDK turn.prompt, steer, and follow-up could queue directly through sendUserMessage while a previously accepted default selection still owned the causal fence.

Apply the same abortable selection fence at the shared external SDK ingress, check same-session ownership before prompt waits, and give only private scheduled continuations an explicit fence/reentry capability so #4496 ownerless continuations remain deadlock-free without admitting later external successors.

Lore-id: issue-4519-sdk-ingress-fence

Constraint: only internal scheduled continuations bypass selection fences

Constraint: waitForIdle remains outside selection admission

Confidence: high

Scope-risk: medium

Reversibility: clean

Tested: attribution 12/12, default selection 45/45, auto-compaction 23 pass + 1 skip, SDK host wiring 104/104, coding-agent check/types
…uations

Codex review of exact head 6907359 flagged two ordering gaps plus the
missing changelog entry. A later plain prompt could overtake an earlier
follow-up dispatch that was still awaiting its durable
onPreflightAcceptCommit, because classification only observed already
enqueued messages; and a continuation parked behind a pending selection
fence cleared the settlement markers without registering anything
waitForIdle could observe, so an external idle wait could report the
session settled while the continuation was still waiting on the fence.

Follow-up ingress now classifies and reserves order synchronously before
any await (selection fence or durable acceptance) using counted epoch
reservations, and a later dispatch drains earlier reservations before its
own durable enqueue so enqueue order can never invert. Fence-deferred
continuations are tracked as settlement work keyed by fence generation;
the tracking window ends at the deferred invocation's synchronous
re-entry, and the selection owning the fence never waits on its own
parked continuations, preserving the #4519 no-self-deadlock guarantee.

Lore-id: issue-4519-selection-order-review
Constraint: selection must never wait on continuations parked behind its own fence
Constraint: reservations must not survive a rejected or cancelled acceptance
Tested: attribution 24/24 incl. new reservation ordering regression, auto-compaction 26 pass + 1 skip incl. new waitForIdle fence regression, manual-retry 5/5, SDK host runtime 48/48, coding-agent check/types, biome, verify-gjc-state-writers fast gate
Not-tested: live multi-connection SDK broker soak beyond the wired host runtime suite
Confidence: high
Scope-risk: medium
Reversibility: clean
Codex review of head 69b98ef flagged two P1 gaps in the fix-forward.
The idle-observable deferral counter keyed off a single
active-generation field, so when selection B reserved generation 2 while
a continuation was still deferred behind selection A's generation-1
fence, A observed generation 2 as its own and could wait on the
generation-1 continuation that was itself waiting on A — deadlock. And a
follow-up dispatch that reserved an epoch and then had its
selection-fence wait rejected by terminal cancellation exited before the
cleanup scope, leaking the epoch so later prompts misclassified as
having a follow-up ahead and later follow-ups waited forever.

Deferral observability is now scoped structurally instead of by caller
identity: only deferrals whose fence has already settled count toward
pending settlement, because their deferred tails re-enter within
microtask time. A deferral behind a still-pending fence is invisible by
construction — the fence owner's completion may causally depend on the
settlement a waiter would block on — so counting it could always
reintroduce the #4519 self-deadlock regardless of which selection
observes it. The selection finally records the settled generation and
resolves settlement waiters. The follow-up reservation now covers the
fence wait and post-wait assertion inside its try/finally, so any exit
path releases the epoch and wakes drain waiters.

Lore-id: issue-4519-selection-order-review-2
Constraint: no caller-identity-based fence exclusion; observability is settled-fence-scoped only
Constraint: every reservation exit path must release the epoch
Tested: attribution 25/25 incl. new reservation-leak regression, auto-compaction 27 pass + 1 skip incl. new overlapping-selections no-deadlock regression, manual-retry 5/5, SDK host runtime 48/48, coding-agent check/types, fast gate 0 violations
Confidence: high
Scope-risk: medium
Reversibility: clean
…dle waits

Codex review of head 88deb97 flagged that the settled-generation filter
hid unresolved fence-deferred continuations from every idle wait, not just
the selection's own: once the predecessor event handler settled, an
external waitForIdle could report the session idle while the parked
continuation was still waiting on its fence, letting SDK completion
reporting race the continuation.

External idle waits now count every unresolved deferral. Only two callers
narrow their view, both by explicit internal-only parameter rather than
caller inference: setDefaultModelSelection's mid-selection drain passes
its own fence generation (work parked behind its own fence is waiting on
the selection itself), and the causally-upstream turn-settle path
excludes deferrals behind still-pending fences via the oldest pending
fence generation. Scoped waiters no longer share the strict settlement
promise — they register a wake callback re-evaluated on settlement
transitions — because that promise may correctly stay pending on a
fence-deferred continuation the fence owner must not block behind.

Lore-id: issue-4519-selection-order-review-3
Constraint: external waitForIdle observes all unresolved fence-deferred continuations
Constraint: only explicit internal parameters narrow deferral visibility; no caller inference
Constraint: scoped settlement waits never block behind the strict settlement promise
Tested: attribution 25/25, auto-compaction 27 pass + 1 skip incl. waitForIdle-pending regression, manual-retry 5/5, SDK host 48/48, sdk-session-router-authority 40/40, coding-agent check/types, fast gate 0
Confidence: high
Scope-risk: medium
Reversibility: clean
Selection drains skipped all post-prompt recovery, which could starve a timer-backed continuation that predates the selection fence. Track each task fence generation so scoped drains await only causally earlier work.

Lore-id: 4540-scoped-idle-recovery
Constraint: selection must not await continuations parked behind its own fence
Confidence: high
Scope-risk: narrow
Reversibility: simple
Tested: bun --cwd=packages/coding-agent run check; bun test packages/coding-agent/test/agent-session-before-agent-start-attribution.test.ts packages/coding-agent/test/agent-session-auto-compaction-continue.test.ts packages/coding-agent/test/agent-session-manual-retry.test.ts
@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4519-selection-order branch from 7a35c1e to 33dc4d6 Compare August 15, 2026 01:30
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Terminal merge execution at the exact reviewed head.

  • base: 64c15281691280be7854dac04baeb05188328ef4
  • head: 33dc4d61e8b8dd1c578900b04fa1e5c544c70271
  • canonical digest: ce574729f8b74c927f9e91cec233ea413fcf76725837c2c89f51ef20a381213f
  • exact-head approval: probepark review 4942172870
  • PR contract bootstrap: success
  • affected validation/evidence/virtual integration: success
  • latest check-run set: no pending, no failure

Merging into dev now; #4519 terminal closure will be verified immediately afterward.


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

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.

2 participants