From d6f8fd67d9b27632cdd3de83e2164c82786c1aec Mon Sep 17 00:00:00 2001 From: Deepak Jain Date: Mon, 17 Aug 2026 01:00:34 -0700 Subject: [PATCH 1/2] fix(onboard): prune channels from reused sandbox selection Fixes #9283 Signed-off-by: Deepak Jain --- .../machine/handlers/sandbox-messaging.test.ts | 16 ++++++++++++++++ .../machine/handlers/sandbox-messaging.ts | 17 +++++++++-------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/lib/onboard/machine/handlers/sandbox-messaging.test.ts b/src/lib/onboard/machine/handlers/sandbox-messaging.test.ts index 2467c86c288..f543548c174 100644 --- a/src/lib/onboard/machine/handlers/sandbox-messaging.test.ts +++ b/src/lib/onboard/machine/handlers/sandbox-messaging.test.ts @@ -424,6 +424,22 @@ describe("reconcileReusedSandboxMessaging", () => { expect(result.selectedChannels).toEqual(["whatsapp"]); }); + it("keeps a lifecycle-selected channel in a reused sandbox selection (#9283)", () => { + const plan = { + ...discordPlan(hashCredential("previous-discord-token") ?? ""), + workflow: "add-channel" as const, + }; + vi.stubEnv("DISCORD_BOT_TOKEN", ""); + + const result = reconcileReusedSandboxMessaging( + plan, + { name: "openclaw" }, + { clearPlanEnv() {} }, + ); + + expect(result).toEqual({ plan, selectedChannels: ["discord"], changed: false }); + }); + it("removes every unsupported channel artifact from a reused plan", () => { // Keep the channel host-configured so this case stays about unsupported // artifact removal, not the #9283 unconfigured-channel selection filter. diff --git a/src/lib/onboard/machine/handlers/sandbox-messaging.ts b/src/lib/onboard/machine/handlers/sandbox-messaging.ts index ad6efb84acd..0970629c315 100644 --- a/src/lib/onboard/machine/handlers/sandbox-messaging.ts +++ b/src/lib/onboard/machine/handlers/sandbox-messaging.ts @@ -435,15 +435,16 @@ export function reconcileReusedSandboxMessaging( const filtered = plan ? filterMessagingPlanForCurrentAgent(plan, agent) : null; const changed = !isDeepStrictEqual(filtered, recordedPlan); if (changed) deps.clearPlanEnv(); - // The reused plan records the previous selection, not the current host - // input. Report only channels the environment still configures so the - // policies handler can classify a retired channel as unconfigured and drop - // its egress preset (#9283). The plan itself stays untouched. + const selection = { + plan: filtered, + selectedChannels: getActiveChannelsFromPlan(filtered), + }; + const currentSelection = + filtered && registryPlanRecordsLifecycleSelection(filtered) + ? selection + : filterUnconfiguredHostChannelsFromSelection(selection, agent); return { - ...filterUnconfiguredHostChannelsFromSelection( - { plan: filtered, selectedChannels: getActiveChannelsFromPlan(filtered) }, - agent, - ), + ...currentSelection, changed, }; } From 57ad709ef23889a2ad468049d25cc3c177a28486 Mon Sep 17 00:00:00 2001 From: Deepak Jain Date: Mon, 17 Aug 2026 03:35:50 -0700 Subject: [PATCH 2/2] fix(onboard): preserve lifecycle selection on resume Signed-off-by: Deepak Jain --- .../handlers/sandbox-messaging.test.ts | 24 +++++++++++++++++++ .../machine/handlers/sandbox-messaging.ts | 1 + 2 files changed, 25 insertions(+) diff --git a/src/lib/onboard/machine/handlers/sandbox-messaging.test.ts b/src/lib/onboard/machine/handlers/sandbox-messaging.test.ts index f543548c174..d0df8e75ca6 100644 --- a/src/lib/onboard/machine/handlers/sandbox-messaging.test.ts +++ b/src/lib/onboard/machine/handlers/sandbox-messaging.test.ts @@ -613,6 +613,30 @@ describe("reconcileSandboxMessaging plan authority", () => { expect(result).toEqual({ plan: null, selectedChannels: ["whatsapp"] }); }); + it("keeps a lifecycle-selected channel in a completed registry resume (#9283)", async () => { + const registryPlan = { + ...discordPlan(hashCredential("previous-discord-token") ?? ""), + workflow: "add-channel" as const, + }; + const deps = reconcileDeps([]); + deps.getRegistrySandboxMessagingAuthority.mockReturnValue({ + authoritative: true, + plan: registryPlan, + }); + vi.stubEnv("DISCORD_BOT_TOKEN", ""); + + const result = await reconcileSandboxMessaging({ + resume: true, + session: completedCheckpointSession(registryPlan), + sandboxName: "alpha", + agent: { name: "openclaw" }, + deps, + }); + + expect(deps.setupMessagingChannels).not.toHaveBeenCalled(); + expect(result).toEqual({ plan: registryPlan, selectedChannels: ["discord"] }); + }); + it("keeps an in-sandbox QR channel in a completed registry resume (#9109)", async () => { const registryPlan = whatsappPlan(); const deps = reconcileDeps([]); diff --git a/src/lib/onboard/machine/handlers/sandbox-messaging.ts b/src/lib/onboard/machine/handlers/sandbox-messaging.ts index 0970629c315..2b468ad1f05 100644 --- a/src/lib/onboard/machine/handlers/sandbox-messaging.ts +++ b/src/lib/onboard/machine/handlers/sandbox-messaging.ts @@ -615,6 +615,7 @@ async function selectionFromRegistryAuthority( authority.plan, false, ); + if (authority.plan && registryPlanRecordsLifecycleSelection(authority.plan)) return selection; return filterUnconfiguredHostChannelsFromSelection(selection, options.agent); } if (authority.plan) return selectionFromRegistryPlan(authority.plan, options);