fix(agents): dedupe subagent browser session cleanup wrapper with dispatch flag#68669
Conversation
Greptile SummaryThis PR adds a per-entry Confidence Score: 5/5Safe to merge — fix is minimal, synchronous, and directly tested. Change is a small additive guard that exactly mirrors the existing endedHookEmittedAt pattern. The synchronous check-then-set correctly handles concurrent JS microtask interleaving across the preceding awaits. The rearm reset in subagent-registry-run-manager.ts is in place, and the regression test covers the parallel-invocation scenario that triggered the bug. No P0 or P1 issues found. No files require special attention. Reviews (1): Last reviewed commit: "fix(agents): dedupe subagent browser ses..." | Re-trigger Greptile |
de10efb to
cbe53aa
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cbe53aa68c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
cbe53aa to
6410bfa
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6410bfaeec
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
0054c0e to
00cab42
Compare
|
Codex deep review: this looks correct and worth landing. Bug / behavior:
Why this is the best fix:
Current-main proof:
No blocking findings from me. |
|
Codex review: needs maintainer review before merge. Reviewed May 29, 2026, 3:32 AM ET / 07:32 UTC. Summary PR surface: Source +12, Tests +89. Total +101 across 4 files. Reproducibility: yes. Source inspection shows the in-process listener and gateway wait path both converge on Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land this narrow dispatch guard after required checks and maintainer merge validation, then rebase or adjust broader lifecycle cleanup work around the landed behavior. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows the in-process listener and gateway wait path both converge on Is this the best way to solve the issue? Yes. The PR is the narrowest maintainable fix I found: it dedupes only the browser cleanup wrapper, leaves retire/announce finalization available to every caller, and resets the marker on rearm. AGENTS.md: found and applied where relevant. Codex review notes: model gpt-5.5, reasoning high; reviewed against 854cb9292db8. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +12, Tests +89. Total +101 across 4 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
00cab42 to
cb3e21b
Compare
81d901e to
4abfc02
Compare
|
@clawsweeper review |
17ccc9b to
7067f30
Compare
7067f30 to
352bc76
Compare
|
Rebased onto current upstream/main (
Local regression test green: @greptile review and provide confidence score |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
@greptile review and provide confidence score |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
d4276de to
526e641
Compare
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
526e641 to
e3e70aa
Compare
|
Heads up: this PR needs to be updated against current |
…patch flag completeSubagentRun can be invoked in parallel from two completion paths for the same runId: the in-process listener (registry.ts phase='end') and the gateway waitForSubagentCompletion RPC (run-manager.ts). registerSubagentRun pairs both unconditionally, so embedded subagent completion invokes the cleanupBrowserSessionsForLifecycleEnd wrapper twice for the same childSessionKey. Actual CDP browserCloseTab IPC is already idempotent — the bundled browser extension drains tracked tabs via takeTrackedTabsForSessionKeys, so the second call short-circuits with tabs.length === 0. However both wrapper invocations still pay plugin-sdk facade lazy-load, normalizeSessionKeys, and runBestEffortCleanup overhead, and the guard closes a defense-in-depth gap if the drain pattern is ever removed or replaced. Add SubagentRunRecord.browserCleanupDispatchedAt and a sync check-then-set in completeSubagentRun that matches the endedHookEmittedAt dedup flag introduced in 48042c3 for the sibling hook-emit race. Reset the flag at rearm time in run-manager.ts alongside endedHookEmittedAt. [AI-assisted]
…per only ClawSweeper re-review flagged [P2] "Keep duplicate completions on the finalization path": the browserCleanupDispatchedAt guard returned from completeSubagentRun entirely, so a duplicate completion caller skipped retireRunModeBundleMcpRuntime and startSubagentAnnounceCleanupFlow, not just the browser-cleanup wrapper. On pre-fix main the duplicate caller still ran that tail because its own cleanupBrowserSessionsForLifecycleEnd returns immediately once takeTrackedTabsForSessionKeys has drained the tabs. Scope the check-then-set guard to the cleanupBrowserSessions wrapper only. retireRunModeBundleMcpRuntime (idempotent) and startSubagentAnnounceCleanupFlow (single-caller via beginSubagentCleanup) now run for every completion caller, restoring pre-fix tail behavior while still firing the tab-close IPC once. Add a held-first-cleanup regression test: with the first caller parked inside a pending cleanup promise, a second completeSubagentRun must still reach retireSessionMcpRuntimeForSessionKey and runSubagentAnnounceFlow. [AI-assisted]
e3e70aa to
abbb467
Compare
|
Maintainer landing verification for PR #68669. Behavior addressed: duplicate embedded subagent completion callers no longer dispatch Real environment tested: GitHub CI on PR head Exact steps or command run after this patch:
Evidence after fix: PR CI run Observed result after fix: source review confirms the new What was not tested: I did not run a local branch checkout test or a live browser-backed embedded subagent run during landing; CI and the supplied real-behavior proof cover the patched controller path. |
steipete
left a comment
There was a problem hiding this comment.
Approved for landing. Narrow lifecycle guard, focused regression coverage, CI green on PR head, and local merge-tree against current main is clean.
Summary
cleanupBrowserSessionsForLifecycleEndwrapper twice for the samerunId.registerSubagentRunpairs an in-processensureListener()and a gatewaywaitForSubagentCompletionRPC unconditionally; both paths reachcompleteSubagentRun(..., triggerCleanup: true)and the wrapper sits outside any dedup guard.browserCloseTabIPC is already idempotent viaextensions/browser/src/browser/session-tab-registry.tstake-and-drain. The duplication is at the wrapper layer (normalizeSessionKeys,runBestEffortCleanup,plugin-sdkfacade dispatch,onWarn). Sibling race48042c3875introducedendedHookEmittedAtin the same file for the same dual-dispatch pattern; this extends that pattern to browser cleanup for consistency and defense-in-depth if take-and-drain is ever replaced.SubagentRunRecord.browserCleanupDispatchedAt?: number, a sync check-then-set guard incompleteSubagentRunscoped to thecleanupBrowserSessionsForLifecycleEndwrapper only, and a rearm reset insubagent-registry-run-manager.tsalongsideendedHookEmittedAt. Two regression tests insubagent-registry-lifecycle.test.ts.startSubagentAnnounceCleanupFlow, no change to the bundled browser extension. TheretireRunModeBundleMcpRuntime+startSubagentAnnounceCleanupFlowtail still runs for every completion caller — only the browser tab-close IPC is deduped. CDP IPC semantics unchanged.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Root Cause (if applicable)
registerSubagentRunfires bothensureListener()andvoid waitForSubagentCompletion(runId, ...)unconditionally for every subagent. In embedded mode both paths resolve and both invokecompleteSubagentRunfor the samerunId.cleanupBrowserSessionsForLifecycleEndsits outside thebeginSubagentCleanupatomic guard (which lives insidestartSubagentAnnounceCleanupFlow), so the wrapper fires twice.48042c3875addedendedHookEmittedAtfor the sibling hook-emit race, but only covered the hook dispatch — the browser cleanup wrapper in the same function body remained unguarded.Regression Test Plan (if applicable)
src/agents/subagent-registry-lifecycle.test.ts— two cases:"dedupes browser cleanup when two callers complete the same run in parallel"— parallelcompleteSubagentRunwith the samerunId+triggerCleanup: trueresults incleanupBrowserSessionsForLifecycleEndbeing called exactly once, withentry.browserCleanupDispatchedAtset to a number."drains the retire + announce tail for a duplicate completion held behind a slow first browser cleanup"— added after ClawSweeper re-review: with the first caller parked inside a still-pending cleanup promise, a secondcompleteSubagentRunmust still reachretireSessionMcpRuntimeForSessionKeyandrunSubagentAnnounceFlow— i.e. the dispatch guard never strands the completion tail of a duplicate caller behind a slow first cleanup.ensureListenercallback insubagent-registry.tsandwaitForSubagentCompletionresolve insubagent-registry-run-manager.ts) converge on the samecreateSubagentRegistryLifecycleController.completeSubagentRunhelper, so a controller-level invocation exercises the same branch without requiring a full gateway harness.ClawSweeper re-review follow-up
ClawSweeper's re-review flagged [P2] "Keep duplicate completions on the finalization path": the original
browserCleanupDispatchedAtguardreturned fromcompleteSubagentRunentirely, so a duplicate completion caller skippedretireRunModeBundleMcpRuntimeandstartSubagentAnnounceCleanupFlow— not just the browser-cleanup wrapper. On pre-fixmainthe duplicate caller still ran that tail (its owncleanupBrowserSessionsForLifecycleEndreturns immediately becausetakeTrackedTabsForSessionKeysalready drained the tabs synchronously), so the original guard did remove that fallback. The guard now wraps only thecleanupBrowserSessionscall:retire(idempotent —retireSessionMcpRuntimeno-ops once the runtime is gone) and announce (single-caller via thebeginSubagentCleanupatomic guard) run for every caller again, restoring pre-fix tail behavior while still firing the tab-close IPC exactly once.User-visible / Behavior Changes
None. CDP browser tab-close IPC remains idempotent (as before) and the subagent completion flow behaves identically for single-caller paths.
Security Impact (required)
Repro + Verification
Environment
Steps
pnpm installin a fresh checkout.src/agents/subagent-registry-lifecycle.test.ts(or run the existing branch).pnpm test src/agents/subagent-registry-lifecycle.test.ts -t "dedupes browser cleanup"→expected "vi.fn()" to be called 1 times, but got 2 times.Expected
cleanupBrowserSessionsForLifecycleEndfires exactly once;entry.browserCleanupDispatchedAtis a number; a duplicate completion caller still drains the retire + announce tail.Actual
Matches expected after this PR.
Evidence
Before (on clean
upstream/main, existing dedup case):After (this branch):
Scoped subagent-registry suites (
subagent-registry-lifecycle,subagent-registry-completion,subagent-registry.steer-restart,subagent-registry): 154/154 passing.pnpm check,pnpm buildall green.Human Verification (required)
Promise.allparallel invocation with samerunId+triggerCleanup: true; held-first-cleanup duplicate caller still reaching retire + announce;pnpm check,pnpm buildall clean.cleanupBrowserSessionswrapper only —retireRunModeBundleMcpRuntimeandstartSubagentAnnounceCleanupFlowremain unguarded so every completion caller drains the tail (ClawSweeper [P2]).subagent-registry-run-manager.ts:236) resetsbrowserCleanupDispatchedAt: undefinedalongsideendedHookEmittedAt: undefinedso a restart / steer-restart can legitimately re-dispatch cleanup for the new run.session-tab-registry.ts'stakeTrackedTabsForSessionKeysdrain already idempotent-izes CDP IPC — narrative framed as defense-in-depth + wrapper overhead rather than an IPC-level bug.await) — no micro-race between check and set.Review Conversations
Compatibility / Migration
Risks and Mitigations
browserCleanupDispatchedAtpersisting across disk persistence means a crashed process that already dispatched cleanup will skip the wrapper on restore.session-tab-registrytabs are drained on first call; restart-side orphan handling (subagent-registry.tsrestore path) does not route throughcompleteSubagentRun's cleanup branch.[AI-assisted]
Real behavior proof
Behavior or issue addressed: Without this patch,
completeSubagentRuninvokescleanupBrowserSessionsForLifecycleEndtwice when the in-process listener (registry.tsphase=end) and the gatewaywaitForSubagentCompletionRPC complete in parallel for the samerunId. Across 100 production-style parallel-completion trials, every single trial triggered 2 cleanup invocations (200 total) — the wrapper/facade/logging that surrounds the cleanup is invoked twice for every embedded subagent completion. With this patch, the per-runbrowserCleanupDispatchedAtflag turns the second invocation into a no-op while preserving the first; 100 trials show 1 invocation each (100 total).Real environment tested: macOS 25.4 (darwin arm64), Node 23.9, OpenClaw 2026.5.5 from this branch built locally with
pnpm build. The production helper (src/agents/subagent-registry-lifecycle.tscompleteSubagentRun) is invoked throughpnpm openclaw runagainst the real registry state machine; 100 parallel-completion trials drive both completion paths simultaneously and record the actual cleanup invocation count per trial.Exact steps or command run after this patch:
For the without-fix comparison, the same command was run on a build where the three patched
.tsfiles were reverted to the base commit (git checkout 384432fd22 -- src/agents/subagent-registry.types.ts src/agents/subagent-registry-lifecycle.ts src/agents/subagent-registry-run-manager.ts) beforepnpm build. The harness file itself is identical between the two runs.Evidence after fix: live console output of the
pnpm openclaw runcommand above against patched and unpatched production source. EachPRODUCTION_DEMO_SUMMARYline is a real measurement ofcleanupBrowserSessionsForLifecycleEndinvocation count across 100 parallel-completion trials.Observed result after fix: With the patch,
cleanupBrowserSessionsForLifecycleEndis invoked exactly once perrunIdacross 100 parallel-completion trials (totalInvocations: 100, perTrialMax: 1, duplicatedTrials: 0). Without the patch, every single trial fires the cleanup twice (totalInvocations: 200, perTrialMin: 2, duplicatedTrials: 100/100) — every embedded subagent completion in production runs the wrapper/facade/logging twice. The only difference between the two runs is the three patched.tsfiles; the harness, mock harness, and assertion thresholds are identical.What was not tested: A live Pi/embedded run with the actual headless browser session attached. The patched code path is producer-side dedupe of cleanup dispatch; the harness already drives the real registry state machine and counts the real cleanup invocations, so the dedupe boundary is the artifact under test.