fix(openclaw): release the gateway binding when the status callback throws - #952
fix(openclaw): release the gateway binding when the status callback throws#952chughtapan wants to merge 1 commit into
Conversation
…hrows startGatewayAccountEffect registers the account in activeClients before connecting, but the connect path had no cleanup on failure. The host's setStatus is arbitrary caller code, and reportConnected invokes it inside Effect.sync, so a throw surfaces as a defect rather than a failure -- which connectGatewayCore's catchAll does not observe. The binding stayed registered and the core was never disconnected. Release the binding on any error escaping the connect path, and make disconnectAndRemove drop the entry even when disconnect itself fails, matching disconnectCoreOnAbort which already used Effect.ensuring. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P76aaa1STr3WPZ3nDascta
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
Closing as obsolete rather than folding into #974 — the bug this fixed is already gone there, by construction. This patched The guarantee survives without the patch. In #974: yield* Effect.sync(() => activeHarnessClients.set(ctx.accountId, active));
yield* reportHarnessConnected(client, ctx).pipe( // the setStatus call
Effect.zipRight(...),
Effect.ensuring(finishHarnessClient(activeHarnessClients, ctx.accountId, active)),
);
Merging this would resurrect deleted symbols to fix a bug that no longer exists. Reopen if you disagree. |
Plan step 2. Based on
origin/main, deliberately not on the #943→#951 stack: this bug is pre-existing in production, and per the plan's D3 the legacy path stays reachable in production until the OpenClaw cutover lands. This shortens that exposure window and merges independently of the endpoint chain.The bug
startGatewayAccountEffectregisters the account inactiveClientsbefore connecting, then ends withconnectGatewayCore(...)and no cleanup.The host's
setStatusis arbitrary caller code.reportConnectedinvokes it insideEffect.sync, so a throw surfaces as a defect, not a failure — andconnectGatewayCore'sEffect.catchAllonly observes failures. The defect escapes with the binding still registered and the core never disconnected: the account is wedged with no route to recovery.The fix
Two changes, same defect class:
Effect.onError, which observes defects as well as failures).disconnectAndRemoveusesEffect.ensuringinstead ofEffect.tap, so a failing disconnect still drops the entry. This matchesdisconnectCoreOnAbort, which already usedEffect.ensuring— that inconsistency was the tell.No public surface change: the diff adds and removes no exported symbol, type, or interface.
Non-vacuity
The new test fails against the pre-fix source with
expected +0 to be 1—closeCalls.countwas 0, i.e. the core was never disconnected. Verified by reverting onlyopenclaw-entry.tstoorigin/mainand re-running with the test in place.It asserts binding release the same way the existing
stopRemovesClientdoes (sendText→/not connected/i), sinceactiveClientsis private.Gates
pnpm nx run @moltzap/openclaw-channel:test— 67 passed / 7 filespnpm typecheck --skip-nx-cache— greenpnpm lint— 0 warnings, 0 errorsgit diff origin/main | grep -c ActiveLegacyGateway→ 0, and the diff touches exactly 2 source filesThe
ActiveLegacyGateway<Service>retype from the adapters worktree is deliberately dropped:openclaw-gateway-lifecycle.tsdoes not exist onorigin/main, and every symbol it would retype is deleted by the OpenClaw cutover. GeneratedMODULE.md/src.mdxchurn is line-number shift only.ADR conformance (R-pass)
CONFORMS(vacuously)docs/decisions/README.mdonmain(49 records). No accepted outcome governs internal error handling in the OpenClaw adapter's gateway binding.20260723-lifecycle-rides-l3concerns conversation lifecycle at L3, not adapter process lifecycle, and ispartially-superseded.Per R-pass rule 4, "no ADR governs this" is stated as a reviewable claim, not left as silence. The diff changes no wire field, tool catalog, public contract, or trust boundary — the categories ADRs in this repo govern.
🤖 Generated with Claude Code
https://claude.ai/code/session_01P76aaa1STr3WPZ3nDascta