From 6eb3b5172f91960f2c71d00e3d3a2a6f6d835659 Mon Sep 17 00:00:00 2001 From: San Dang Date: Fri, 12 Jun 2026 08:39:06 +0530 Subject: [PATCH] fix(cli): refresh stale messaging render plans --- .../compiler/workflow-planner.test.ts | 49 ++++++++++++++++ .../messaging/compiler/workflow-planner.ts | 58 ++++++++++++++++++- 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/lib/messaging/compiler/workflow-planner.test.ts b/src/lib/messaging/compiler/workflow-planner.test.ts index e18c69ad6d7..1c51b12d830 100644 --- a/src/lib/messaging/compiler/workflow-planner.test.ts +++ b/src/lib/messaging/compiler/workflow-planner.test.ts @@ -446,6 +446,55 @@ describe("MessagingWorkflowPlanner", () => { ]); }); + it("refreshes missing manifest render entries from stale rebuild plans", async () => { + const existingPlan = await planner().buildPlan({ + sandboxName: "demo", + agent: "hermes", + workflow: "onboard", + isInteractive: false, + configuredChannels: ["discord"], + credentialAvailability: { + DISCORD_BOT_TOKEN: true, + }, + }); + const stalePlan = { + ...existingPlan, + credentialBindings: existingPlan.credentialBindings.map((binding) => ({ + ...binding, + credentialHash: "hash-discord-token", + })), + agentRender: [], + buildSteps: [], + }; + + const plan = await planner().buildRebuildPlanFromSandboxEntry({ + sandboxName: "demo", + agent: "hermes", + sandboxEntry: { + name: "demo", + agent: "hermes", + messaging: { schemaVersion: 1, plan: stalePlan }, + }, + supportedChannelIds: ["discord"], + }); + + expect(plan?.workflow).toBe("rebuild"); + expect( + plan?.credentialBindings.find((binding) => binding.providerEnvKey === "DISCORD_BOT_TOKEN") + ?.credentialHash, + ).toBe("hash-discord-token"); + const discordEnvRender = plan?.agentRender.find( + (entry) => + entry.channelId === "discord" && + entry.kind === "env-lines" && + entry.target === "~/.hermes/.env", + ); + expect(discordEnvRender).toMatchObject({ + kind: "env-lines", + lines: expect.arrayContaining(["DISCORD_BOT_TOKEN=openshell:resolve:env:DISCORD_BOT_TOKEN"]), + }); + }); + it("adds one manifest channel into an existing sandbox entry plan", async () => { const existingPlan = await planner().buildPlan({ sandboxName: "demo", diff --git a/src/lib/messaging/compiler/workflow-planner.ts b/src/lib/messaging/compiler/workflow-planner.ts index acb0f01b871..0c188de1ab5 100644 --- a/src/lib/messaging/compiler/workflow-planner.ts +++ b/src/lib/messaging/compiler/workflow-planner.ts @@ -101,11 +101,34 @@ export class MessagingWorkflowPlanner { ): Promise { const existingPlan = readSandboxEntryPlan(context); if (existingPlan) { - return setPlanDisabledChannels( + const normalizedPlan = setPlanDisabledChannels( existingPlan, disabledChannelsFromSandboxEntry(context.sandboxEntry, existingPlan), "rebuild", ); + if (!planMissingActiveChannelRender(normalizedPlan)) return normalizedPlan; + + const configuredChannels = uniqueChannels( + normalizedPlan.channels.map((channel) => channel.channelId), + ); + const refreshedPlan = await this.buildPlan({ + sandboxName: context.sandboxName, + agent: context.agent, + workflow: "rebuild", + isInteractive: false, + configuredChannels, + disabledChannels: normalizedPlan.disabledChannels, + supportedChannelIds: context.supportedChannelIds, + credentialAvailability: mergeAvailability( + credentialAvailabilityFromPlan(normalizedPlan), + this.credentialAvailabilityFromSandboxEntry(context.sandboxEntry, configuredChannels), + context.credentialAvailability, + ), + }); + return mergeSandboxMessagingPlans( + normalizedPlan, + preserveCredentialBindingHashes(normalizedPlan, refreshedPlan), + ); } return null; } @@ -353,6 +376,39 @@ function setPlanDisabledChannels( }); } +function preserveCredentialBindingHashes( + existing: SandboxMessagingPlan, + incoming: SandboxMessagingPlan, +): SandboxMessagingPlan { + const existingHashes = new Map( + existing.credentialBindings + .filter((binding) => binding.credentialHash) + .map((binding) => [credentialBindingKey(binding), binding.credentialHash] as const), + ); + if (existingHashes.size === 0) return incoming; + + return clonePlan({ + ...incoming, + credentialBindings: incoming.credentialBindings.map((binding) => ({ + ...binding, + credentialHash: binding.credentialHash ?? existingHashes.get(credentialBindingKey(binding)), + })), + }); +} + +function credentialBindingKey( + binding: Pick, +): string { + return binding.channelId + "\0" + binding.providerEnvKey; +} + +function planMissingActiveChannelRender(plan: SandboxMessagingPlan): boolean { + const renderedChannels = new Set(plan.agentRender.map((entry) => entry.channelId)); + return plan.channels.some( + (channel) => channel.active && !channel.disabled && !renderedChannels.has(channel.channelId), + ); +} + function removePlanChannel( plan: SandboxMessagingPlan, channelId: MessagingChannelId,