diff --git a/extensions/openai/openclaw.plugin.test.ts b/extensions/openai/openclaw.plugin.test.ts index 86ebdf12ff2..8c41321d667 100644 --- a/extensions/openai/openclaw.plugin.test.ts +++ b/extensions/openai/openclaw.plugin.test.ts @@ -17,6 +17,17 @@ const manifest = JSON.parse( }>; }; +const packageJson = JSON.parse( + readFileSync(new URL("./package.json", import.meta.url), "utf8"), +) as { + dependencies?: Record; + openclaw?: { + bundle?: { + stageRuntimeDependencies?: boolean; + }; + }; +}; + function manifestComparableWizardFields(choice: { choiceId?: string; choiceLabel?: string; @@ -53,6 +64,12 @@ function providerWizardByKey() { } describe("OpenAI plugin manifest", () => { + it("opts into staging bundled runtime dependencies", () => { + expect(packageJson.dependencies?.["@mariozechner/pi-ai"]).toBe("0.70.6"); + expect(packageJson.dependencies?.ws).toBe("^8.20.0"); + expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true); + }); + it("keeps removed Codex CLI import auth choice as a deprecated browser-login alias", () => { const codexBrowserLogin = manifest.providerAuthChoices?.find( (choice) => choice.choiceId === "openai-codex", diff --git a/extensions/openai/package.json b/extensions/openai/package.json index b5b4e5c953d..28ce45cb376 100644 --- a/extensions/openai/package.json +++ b/extensions/openai/package.json @@ -12,6 +12,9 @@ "@openclaw/plugin-sdk": "workspace:*" }, "openclaw": { + "bundle": { + "stageRuntimeDependencies": true + }, "extensions": [ "./index.ts" ] diff --git a/scripts/run-node.mjs b/scripts/run-node.mjs index bc8fa28a362..c0896c3984d 100644 --- a/scripts/run-node.mjs +++ b/scripts/run-node.mjs @@ -798,7 +798,10 @@ const writeBuildStamp = (deps) => { } }; -const shouldSkipCleanWatchRuntimeSync = (deps) => deps.env.OPENCLAW_WATCH_MODE === "1"; +const shouldSkipWatchRuntimeSync = (deps, requirement) => + deps.env.OPENCLAW_WATCH_MODE === "1" && + requirement.reason === "missing_runtime_postbuild_stamp" && + hasDirtyRuntimePostBuildInputs(deps) !== true; const isGatewayClientCommand = (args) => args[0] === "gateway" && (args[1] === "call" || args[1] === "status"); @@ -885,9 +888,12 @@ export async function runNodeMain(params = {}) { return await closeRunNodeOutputTee(deps, exitCode); } if (!buildRequirement.shouldBuild) { - if (!useExistingGatewayClientDist && !shouldSkipCleanWatchRuntimeSync(deps)) { + if (!useExistingGatewayClientDist) { const runtimePostBuildRequirement = resolveRuntimePostBuildRequirement(deps); - if (runtimePostBuildRequirement.shouldSync) { + if ( + runtimePostBuildRequirement.shouldSync && + !shouldSkipWatchRuntimeSync(deps, runtimePostBuildRequirement) + ) { const synced = await withRunNodeBuildLock(deps, async () => { const lockedRuntimePostBuildRequirement = resolveRuntimePostBuildRequirement(deps); if (!lockedRuntimePostBuildRequirement.shouldSync) { diff --git a/src/infra/run-node.test.ts b/src/infra/run-node.test.ts index 8c8b7dec693..27453994cc0 100644 --- a/src/infra/run-node.test.ts +++ b/src/infra/run-node.test.ts @@ -697,6 +697,44 @@ describe("run-node script", () => { }); }); + it("restages dirty runtime metadata in watch mode when dist is already current", async () => { + await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { + await setupTrackedProject(tmp, { + files: { + [ROOT_SRC]: "export const value = 1;\n", + [EXTENSION_PACKAGE]: '{"openclaw":{"bundle":{"stageRuntimeDependencies":true}}}\n', + [RUNTIME_POSTBUILD_STAMP]: '{"head":"abc123"}\n', + }, + buildPaths: [ + ROOT_SRC, + EXTENSION_PACKAGE, + ROOT_TSCONFIG, + ROOT_PACKAGE, + DIST_ENTRY, + BUILD_STAMP, + RUNTIME_POSTBUILD_STAMP, + ], + }); + + const runRuntimePostBuild = vi.fn(); + const { spawnCalls, spawn, spawnSync } = createSpawnRecorder({ + gitHead: "abc123\n", + gitStatus: ` M ${EXTENSION_PACKAGE}\n`, + }); + const exitCode = await runStatusCommand({ + tmp, + spawn, + spawnSync, + env: { OPENCLAW_WATCH_MODE: "1" }, + runRuntimePostBuild, + }); + + expect(exitCode).toBe(0); + expect(spawnCalls).toEqual([statusCommandSpawn()]); + expect(runRuntimePostBuild).toHaveBeenCalledOnce(); + }); + }); + it("runs QA parity report from source without rebuilding private QA dist", async () => { await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { await setupTrackedProject(tmp, {