fix(infra/agents): session-routing guard for coalesced gateway restart continuations (#86742)#87323
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 7, 2026, 6:30 AM ET / 10:30 UTC. Summary PR surface: Source +98, Tests +354, Other +12. Total +464 across 9 files. Reproducibility: yes. Current main is source-reproducible: Review metrics: 2 noteworthy metrics.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the scheduler-owned session guard after exact-head checks and maintainer acceptance of the runtime-session boundary, then let the linked bug close through the merged fix. Do we have a high-confidence way to reproduce the issue? Yes. Current main is source-reproducible: Is this the best way to solve the issue? Yes. The scheduler is the right owner boundary because it owns pending restart coalescing; rejecting cross-session hook replacement and explicitly reporting unqueued continuations is safer than rewriting the global sentinel after the fact. AGENTS.md: found and applied where relevant. Codex review notes: model gpt-5.5, reasoning high; reviewed against bae607b9f196. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +98, Tests +354, Other +12. Total +464 across 9 files. View PR surface stats
Acceptance criteria:
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
|
|
ClawSweeper PR egg ✨ Hatched: 🥚 common Cosmic Diff Drake Hatch commandComment Hatchability rules:
Rarity: 🥚 common. What is this egg doing here?
|
8f9cec0 to
ef1b98b
Compare
ef1b98b to
592364d
Compare
db40282 to
7e74eaf
Compare
This comment was marked as spam.
This comment was marked as spam.
f76eb84 to
bf968f6
Compare
…t continuations (openclaw#86742) When two sessions issue gateway.restart with continuationMessage close together, the scheduler Path B updatePendingRestartEmitHooks unconditionally overwrote the existing pending hooks, silently dropping the first sessions continuation and potentially routing the second sessions continuation back to the first session (CWE-200 finding flagged by aisle-research-bot on prior attempt openclaw#74443). Add a session-routing guard: scheduleGatewaySigusr1Restart now accepts an optional sessionKey and tracks the pending restarts owning session. Coalesced callers from a different session are rejected at the hook- update step and the new ScheduledRestart.emitHooksQueued: false field surfaces the drop to the caller. The gateway tool propagates this as continuationQueued: false in the tool response, matching openclaw#83370 narrow report-only surface. Same-session debounce/replace and legacy hookless callers behave the same as before. Refs openclaw#86742
81e1737 to
efe8a0d
Compare
|
Land-ready proof for head What changed:
Proof run:
Known proof gaps: none for the touched restart/session-routing surfaces. |
…t continuations (openclaw#86742) (openclaw#87323) * fix(infra/agents): session-routing guard for coalesced gateway restart continuations (openclaw#86742) When two sessions issue gateway.restart with continuationMessage close together, the scheduler Path B updatePendingRestartEmitHooks unconditionally overwrote the existing pending hooks, silently dropping the first sessions continuation and potentially routing the second sessions continuation back to the first session (CWE-200 finding flagged by aisle-research-bot on prior attempt openclaw#74443). Add a session-routing guard: scheduleGatewaySigusr1Restart now accepts an optional sessionKey and tracks the pending restarts owning session. Coalesced callers from a different session are rejected at the hook- update step and the new ScheduledRestart.emitHooksQueued: false field surfaces the drop to the caller. The gateway tool propagates this as continuationQueued: false in the tool response, matching openclaw#83370 narrow report-only surface. Same-session debounce/replace and legacy hookless callers behave the same as before. Refs openclaw#86742 * fix(infra): preserve queued restart continuation on forced bypass * fix(infra): make forced restart hook preservation explicit * fix(infra): guard restart continuation ownership before reschedule * fix(infra): report hookless coalesced restarts accurately * fix(infra): trust runtime session for restart sentinel routing * fix(infra): preserve earlier restart reschedule semantics * fix(agents): trust runtime session for update continuations * fix(infra): preserve hookless forced restart continuations --------- Co-authored-by: Peter Steinberger <[email protected]>
…t continuations (openclaw#86742) (openclaw#87323) * fix(infra/agents): session-routing guard for coalesced gateway restart continuations (openclaw#86742) When two sessions issue gateway.restart with continuationMessage close together, the scheduler Path B updatePendingRestartEmitHooks unconditionally overwrote the existing pending hooks, silently dropping the first sessions continuation and potentially routing the second sessions continuation back to the first session (CWE-200 finding flagged by aisle-research-bot on prior attempt openclaw#74443). Add a session-routing guard: scheduleGatewaySigusr1Restart now accepts an optional sessionKey and tracks the pending restarts owning session. Coalesced callers from a different session are rejected at the hook- update step and the new ScheduledRestart.emitHooksQueued: false field surfaces the drop to the caller. The gateway tool propagates this as continuationQueued: false in the tool response, matching openclaw#83370 narrow report-only surface. Same-session debounce/replace and legacy hookless callers behave the same as before. Refs openclaw#86742 * fix(infra): preserve queued restart continuation on forced bypass * fix(infra): make forced restart hook preservation explicit * fix(infra): guard restart continuation ownership before reschedule * fix(infra): report hookless coalesced restarts accurately * fix(infra): trust runtime session for restart sentinel routing * fix(infra): preserve earlier restart reschedule semantics * fix(agents): trust runtime session for update continuations * fix(infra): preserve hookless forced restart continuations --------- Co-authored-by: Peter Steinberger <[email protected]>
…t continuations (openclaw#86742) (openclaw#87323) * fix(infra/agents): session-routing guard for coalesced gateway restart continuations (openclaw#86742) When two sessions issue gateway.restart with continuationMessage close together, the scheduler Path B updatePendingRestartEmitHooks unconditionally overwrote the existing pending hooks, silently dropping the first sessions continuation and potentially routing the second sessions continuation back to the first session (CWE-200 finding flagged by aisle-research-bot on prior attempt openclaw#74443). Add a session-routing guard: scheduleGatewaySigusr1Restart now accepts an optional sessionKey and tracks the pending restarts owning session. Coalesced callers from a different session are rejected at the hook- update step and the new ScheduledRestart.emitHooksQueued: false field surfaces the drop to the caller. The gateway tool propagates this as continuationQueued: false in the tool response, matching openclaw#83370 narrow report-only surface. Same-session debounce/replace and legacy hookless callers behave the same as before. Refs openclaw#86742 * fix(infra): preserve queued restart continuation on forced bypass * fix(infra): make forced restart hook preservation explicit * fix(infra): guard restart continuation ownership before reschedule * fix(infra): report hookless coalesced restarts accurately * fix(infra): trust runtime session for restart sentinel routing * fix(infra): preserve earlier restart reschedule semantics * fix(agents): trust runtime session for update continuations * fix(infra): preserve hookless forced restart continuations --------- Co-authored-by: Peter Steinberger <[email protected]>
Summary
gatewaytoolaction: "restart"withcontinuationMessagereturns success (mode: "emit",coalesced: true) while the continuation is silently never persisted nor delivered after the restart, because the SIGUSR1 restart was already in flight or already scheduled. Two prior attempts (fix(gateway/restart): write sentinel with continuationMessage on coalesced restart #74443 by hclsys, Fix restart continuations after explicit restarts #73812 by VACInc) were closed without merging; the issue creator explicitly notes thataisle-research-botflagged fix(gateway/restart): write sentinel with continuationMessage on coalesced restart #74443 as a Medium security finding ("Cross-session continuation overwrite when coalescing gateway restart requests", CWE-200) because a naive global rewrite ofrestart-sentinel.jsoncould overwrite or misroute another session's continuation. The accepted resolutions the issue creator names are "queue the continuation for the correct pending restart" OR "an explicit non-queued result"; fix(gateway): report unqueued restart continuations #83370 (stainlu, currentlyready for maintainer look) covers the explicit-non-queued half viacontinuationQueued: falsereporting; the cross-session overwrite hole on the silent-overwrite Path B (updatePendingRestartEmitHooks) remains.src/infra/restart.tsexposes two coalesced-restart paths fromscheduleGatewaySigusr1Restart. Path A (hasUnconsumedRestartSignal) is correctly hooks-less — SIGUSR1 already fired and the caller cannot opt in. Path B (pendingRestartTimer || pendingRestartPreparing) callsupdatePendingRestartEmitHooks(opts?.emitHooks)which unconditionally overwrites the existing pending emit hooks. The emit hooks own writing therestart-sentinel.jsonpayload (containing the caller's sessionKey, deliveryContext, threadId, continuation) — so a second session's coalesced restart silently replaces the first session's continuation and routes the post-restart continuation back to the wrong session. This is the CWE-200 hole aisle-research-bot identified on fix(gateway/restart): write sentinel with continuationMessage on coalesced restart #74443.updatePendingRestartEmitHooksso it only accepts new hooks when the new caller'ssessionKeymatches the pending restart'ssessionKey. ThreadingsessionKey?: stringthrough thescheduleGatewaySigusr1Restartopts and remembering it aspendingRestartSessionKeymodule state. AddemitHooksQueued: booleanto theScheduledRestartreturn shape so callers can detect that their hooks were rejected (matches the field name fix(gateway): report unqueued restart continuations #83370 chose for the same purpose). Wire the gateway tool to passagentSessionKeyand surfacecontinuationQueued: scheduled.emitHooksQueuedin the tool response whenever the caller supplied acontinuation.src/infra/restart.ts— newpendingRestartSessionKeymodule state;scheduleGatewaySigusr1Restartopts gainsessionKey?: string;ScheduledRestartgainsemitHooksQueued: boolean;updatePendingRestartEmitHooksbecomes session-guarded and returns whether the slot was claimed; the Path B coalesced branch threads that result back into the response and logs arestart continuation droppedwarning when the guard rejects new hooks. Path A continues to returnemitHooksQueued: falsebecause the hooks cannot run for the already-emitted cycle.clearPendingScheduledRestart, the fresh-schedule andskipDeferralpaths resetpendingRestartSessionKeyalongsidependingRestartEmitHooks.emitPreparedGatewayRestartkeepspendingRestartSessionKeyalive across theawait beforeEmit()window and only clears it after the chained-hook loop exits — without this, a coalesced different-session caller could slip past the guard during the in-flight preparation window (ClawSweeper P1 review point on initial PR head8f9cec097c6c, addressed in latest head).src/agents/tools/gateway-tool.ts— passsessionKeytoscheduleGatewaySigusr1Restartand reportcontinuationQueued: scheduled.emitHooksQueuedin the tool result when the caller supplied acontinuation.src/auto-reply/reply/commands-session.ts—handleRestartCommandnow also threadssentinelPayload.sessionKeyintoscheduleGatewaySigusr1Restart(sibling cross-session guard ClawSweeper P1 review identified — the chat/restartalso writes a session-scoped sentinel with continuation, and previously bypassed the new guard by omitting the schedulersessionKeyopt).src/infra/infra-runtime.test.ts— three new tests pin the session-guard semantics: different sessions → second caller observesemitHooksQueued: falseand its hooks are not invoked; same session → debounce/replace behavior preserved; different session coalescing during in-flightbeforeEmitpreparation → guard still holds (the in-flight preparation race the ClawSweeper P1 review asked for).src/agents/tools/gateway-tool.test.ts— existing mock fixture upgraded to the fullScheduledRestartshape (was a partial{ scheduled, delayMs }); new regression test pinscontinuationQueued: falsepropagation when the coalesced restart belongs to another session.src/auto-reply/reply/commands-session-restart.test.ts— new regression test assertshandleRestartCommandpassessessionKeyto the scheduler so the sibling/restartpath is covered by the same cross-session guard.restart-sentinel.jsonschema (RestartSentinelPayload) is unchanged — same single-slotcontinuation+sessionKeyshape; the fix is in the scheduler, not the sentinel.SIGUSR1already emitted) is unchanged — the new caller's hooks still cannot run for the current cycle, only the explicit reporting viaemitHooksQueued: falseis added.sessionKey(existing behavior) continue to chain/overwrite as before — the guard only fires when both sides identify a session.#83370(stainlu) is complementary: itscontinuationQueuedsurfacing in the tool response was the explicit-non-queued half of the issue creator's OR clause; this PR adds the queued-for-the-correct-restart half by rejecting cross-session overwrites at the scheduler level.docs/reference/configedits).extensions/api.ts/runtime-api.ts, registry, or loader edits).Reproduction
gatewaytoolaction: "restart"withcontinuationMessagenear each other (e.g. a deploy script plus an agent-driven plugin install both restarting the gateway).gateway.restartwith continuation A. The scheduler storespendingRestartEmitHookswhosebeforeEmitwritesrestart-sentinel.jsonwithsessionKey: Aand continuation A.gateway.restartwith continuation B.updatePendingRestartEmitHooks(B's hooks)replaces A's hooks. The restart fires; the sentinel is written by B'sbeforeEmitwithsessionKey: Band continuation B. After restart, the gateway dispatches continuation B as if A had asked for it (or simply drops A's continuation depending on routing) — A's promised reply is silently lost (this is the CWE-200 hole). Both tool responses look successful.pendingRestartEmitHooksin place; B's coalesced call returnscoalesced: true, emitHooksQueued: false, continuationQueued: falseso the agent that issued B knows the continuation was not queued. The gateway log recordsrestart continuation dropped: another session owns the pending restart. A's continuation still fires after restart as promised.Real behavior proof
Behavior addressed (#86742): cross-session coalesced restarts no longer silently overwrite the existing pending continuation. Same-session debounce/replace is preserved. Legacy callers (no
sessionKey) keep the old chain behavior. Thegateway.restarttool response now surfacescontinuationQueued: falsewhen the caller'scontinuationcould not take ownership of the pending restart slot.Real environment tested (Linux, Node 22, real-component tsx harness driving production scheduleGatewaySigusr1Restart end-to-end): tsx-resolved import of production
scheduleGatewaySigusr1Restart+__testing.resetSigusr1State. A realSIGUSR1listener is registered so the scheduler runs inemitmode. Each scenario invokes the production scheduler twice with combinations ofsessionKeyand inspects the returnedcoalesced/emitHooksQueuedfields. No mocks of the scheduler itself.Exact steps or command run after this patch:
node_modules/.bin/tsx /tmp/qmd86742/repro.mts(real-component session-routing-guard sweep)node scripts/run-vitest.mjs src/infra/infra-runtime.test.ts src/agents/tools/gateway-tool.test.tspnpm exec oxfmt --check --threads=1 src/infra/restart.ts src/infra/infra-runtime.test.ts src/agents/tools/gateway-tool.ts src/agents/tools/gateway-tool.test.tsEvidence after fix (verbatim real-component harness output, ANSI stripped):
Vitest output for the two touched test files:
Observed result after fix:
Rejects coalesced emit hooks from a different sessiontest confirms session A'sbeforeEmitruns and session B's does not, while session B observesemitHooksQueued: falsesynchronously.Allows same-session coalesced restart to replace its own preparation hooktest confirms the existing debounce/replace semantics are intact.continuationQueued: falsewhen the coalesced caller belongs to another session.restart continuation dropped: another session owns the pending restartwith both session keys for operator diagnosis.What was not tested:
gateway.restart). The real-component harness drives the production scheduler with twosessionKeys but does not invoke a real channelgateway.restarttool call end-to-end; the gateway-tool branch is covered by mocked-scheduler unit tests instead.Regression tests:
infra-runtime.test.ts→rejects coalesced emit hooks from a different session and reports emitHooksQueued=false (#86742)+allows same-session coalesced restart to replace its own preparation hook (#86742)+rejects coalesced emit hooks from a different session while preparation is in flight (#86742)(the in-flightbeforeEmitrace the ClawSweeper P1 review asked for) + the preservedkeeps existing preparation hook when a hookless restart coalescesanduses the latest preparation hook when scheduled restarts coalescetests.gateway-tool.test.ts→ upgraded full-shape mock plus newreports continuationQueued=false when a coalesced restart belongs to another session (#86742).Risk / Mitigation
updatePendingRestartEmitHooksalways replaced when caller had hooks pre-fix); the only change is rejecting cross-session overwrites. Same-session callers can choose to inspectemitHooksQueued: trueto confirm their hooks installed.sessionKeycontinue to chain/overwrite, leaving the CWE-200 hole open for any code path that doesn't identify a session. Mitigation: the two known cross-session callers that write a session-scoped sentinel + continuation aregateway-tool.tsaction: "restart"andcommands-session.tshandleRestartCommand(chat/restart); this PR updates both to always passsessionKey. The remaining legacy callers (CLI run-loop'sscheduleGatewaySigusr1Restart({ delayMs: 0, reason: "SIGUSR1" })) do not write a sentinel continuation and are restart-only callers, not affected by the overwrite hole.ScheduledRestartreturn shape now has a new required fieldemitHooksQueued. Mitigation: the only production callers reading the return aregateway-tool.ts(this PR),commands-session.ts(calls but does not destructure additional fields), the CLI run-loop (calls but does not destructure additional fields), andconfig-write-flow.ts(usesReturnType<typeof scheduleGatewaySigusr1Restart>, type-only). The test mock fixture ingateway-tool.test.tswas upgraded in this PR.ready for maintainer look). Mitigation: this PR is intentionally a follow-up, not a replacement. fix(gateway): report unqueued restart continuations #83370 surfacescontinuationQueued: falsein the tool response when the scheduler reportsemitHooksQueued: false. This PR adds the actual scheduler-level session guard plus matching tests for the cross-session path. If fix(gateway): report unqueued restart continuations #83370 lands first, this PR rebases cleanly: the newemitHooksQueuedfield and the gateway-tool continuationQueued plumbing match.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Refs #86742