From 1b499e2c3ca1492ff2cfef874829cab193e0fbf2 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 5 Aug 2026 10:45:23 +0800 Subject: [PATCH] fix(reset): preserve bootstrap guidance for dynamic models (#119179) Punchcard-Session: silver-valley-valley-dt --- ...session-reset-prompt.runtime-model.test.ts | 89 +++++++++++++++++++ .../reply/session-reset-prompt.test.ts | 16 +++- src/auto-reply/reply/session-reset-prompt.ts | 22 +++-- 3 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 src/auto-reply/reply/session-reset-prompt.runtime-model.test.ts diff --git a/src/auto-reply/reply/session-reset-prompt.runtime-model.test.ts b/src/auto-reply/reply/session-reset-prompt.runtime-model.test.ts new file mode 100644 index 000000000000..91ea3cb5f720 --- /dev/null +++ b/src/auto-reply/reply/session-reset-prompt.runtime-model.test.ts @@ -0,0 +1,89 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../../config/types.openclaw.js"; + +const inventoryMocks = vi.hoisted(() => { + const runtimeModel = { + id: "dynamic-chat", + name: "dynamic-chat", + provider: "dynamic-provider", + api: "openai-responses", + baseUrl: "https://example.invalid/v1", + }; + return { + runtimeModel, + resolveRuntimeModelContext: vi.fn(async () => ({ + modelApi: runtimeModel.api, + runtimeModel, + })), + resolveInventory: vi.fn((params: Record) => { + if (!Object.hasOwn(params, "modelApi") || !Object.hasOwn(params, "runtimeModel")) { + throw new Error("runtime model facts must be explicitly owner-published"); + } + return { + agentId: "main", + profile: "coding", + groups: [ + { + id: "core", + label: "Built-in tools", + source: "core", + tools: [ + { + id: "read", + label: "Read", + description: "Read files", + rawDescription: "Read files", + source: "core", + }, + ], + }, + ], + }; + }), + }; +}); + +vi.mock("../../agents/tools-effective-inventory.js", () => ({ + resolveEffectiveToolInventory: inventoryMocks.resolveInventory, + resolveEffectiveToolInventoryRuntimeModelContextAsync: inventoryMocks.resolveRuntimeModelContext, +})); + +describe("resolveBareResetBootstrapFileAccess runtime model ownership", () => { + beforeEach(() => { + inventoryMocks.resolveInventory.mockClear(); + inventoryMocks.resolveRuntimeModelContext.mockClear(); + }); + + it("resolves runtime model context once and passes explicit facts to sync inventory", async () => { + const { resolveBareResetBootstrapFileAccess } = await import("./session-reset-prompt.js"); + const cfg = {} as OpenClawConfig; + const params = { + cfg, + agentId: "main", + sessionKey: "agent:main:main", + workspaceDir: "/tmp/workspace-main", + modelProvider: "dynamic-provider", + modelId: "dynamic-chat", + }; + + await expect(resolveBareResetBootstrapFileAccess(params)).resolves.toBe(true); + + expect(inventoryMocks.resolveRuntimeModelContext).toHaveBeenCalledTimes(1); + expect(inventoryMocks.resolveRuntimeModelContext).toHaveBeenCalledWith({ + cfg, + agentId: params.agentId, + workspaceDir: params.workspaceDir, + modelProvider: params.modelProvider, + modelId: params.modelId, + }); + expect(inventoryMocks.resolveInventory).toHaveBeenCalledTimes(1); + const inventoryParams = inventoryMocks.resolveInventory.mock.calls[0]?.[0]; + expect(inventoryParams).toMatchObject({ + ...params, + modelApi: inventoryMocks.runtimeModel.api, + runtimeModel: inventoryMocks.runtimeModel, + }); + expect(Object.hasOwn(inventoryParams ?? {}, "modelApi")).toBe(true); + expect(Object.hasOwn(inventoryParams ?? {}, "runtimeModel")).toBe(true); + }); +}); diff --git a/src/auto-reply/reply/session-reset-prompt.test.ts b/src/auto-reply/reply/session-reset-prompt.test.ts index 4eee3eea3c1d..165f18be7f2d 100644 --- a/src/auto-reply/reply/session-reset-prompt.test.ts +++ b/src/auto-reply/reply/session-reset-prompt.test.ts @@ -1,7 +1,7 @@ // Tests session reset prompt generation and transcript-preserving restart hints. import fs from "node:fs/promises"; import path from "node:path"; -import { describe, it, expect } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; import { makeTempWorkspace } from "../../test-helpers/workspace.js"; import { resolveBareSessionResetPromptState } from "./session-reset-prompt.js"; @@ -141,4 +141,18 @@ describe("resolveBareSessionResetPromptState", () => { expect(pending.prompt).toContain("while bootstrap is still pending for this workspace"); expect(pending.prompt).not.toContain("Execute your Session Startup sequence now"); }); + + it("awaits async bootstrap file access before selecting reset mode", async () => { + const workspaceDir = await makeBootstrapPendingWorkspace(); + const hasBootstrapFileAccess = vi.fn(async () => false); + + const pending = await resolveBareSessionResetPromptState({ + workspaceDir, + hasBootstrapFileAccess, + }); + + expect(hasBootstrapFileAccess).toHaveBeenCalledTimes(1); + expect(pending.bootstrapMode).toBe("limited"); + expect(pending.shouldPrependStartupContext).toBe(false); + }); }); diff --git a/src/auto-reply/reply/session-reset-prompt.ts b/src/auto-reply/reply/session-reset-prompt.ts index cf16d9903872..42d0f127b98d 100644 --- a/src/auto-reply/reply/session-reset-prompt.ts +++ b/src/auto-reply/reply/session-reset-prompt.ts @@ -5,7 +5,10 @@ import { buildLimitedBootstrapPromptLines, } from "../../agents/bootstrap-prompt.js"; import { appendCronStyleCurrentTimeLine } from "../../agents/current-time.js"; -import { resolveEffectiveToolInventory } from "../../agents/tools-effective-inventory.js"; +import { + resolveEffectiveToolInventory, + resolveEffectiveToolInventoryRuntimeModelContextAsync, +} from "../../agents/tools-effective-inventory.js"; import { isWorkspaceBootstrapPending } from "../../agents/workspace.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; @@ -36,17 +39,24 @@ const BARE_SESSION_RESET_PROMPT_BOOTSTRAP_LIMITED = [ "Do not mention internal steps, files, tools, or reasoning.", ].join(" "); -export function resolveBareResetBootstrapFileAccess(params: { +export async function resolveBareResetBootstrapFileAccess(params: { cfg?: OpenClawConfig; agentId?: string; sessionKey?: string; workspaceDir?: string; modelProvider?: string; modelId?: string; -}): boolean { +}): Promise { if (!params.cfg) { return false; } + const runtimeModelContext = await resolveEffectiveToolInventoryRuntimeModelContextAsync({ + cfg: params.cfg, + agentId: params.agentId, + workspaceDir: params.workspaceDir, + modelProvider: params.modelProvider, + modelId: params.modelId, + }); const inventory = resolveEffectiveToolInventory({ cfg: params.cfg, agentId: params.agentId, @@ -54,6 +64,8 @@ export function resolveBareResetBootstrapFileAccess(params: { workspaceDir: params.workspaceDir, modelProvider: params.modelProvider, modelId: params.modelId, + modelApi: runtimeModelContext.modelApi, + runtimeModel: runtimeModelContext.runtimeModel, }); return inventory.groups.some((group) => group.tools.some((tool) => tool.id === "read")); } @@ -64,7 +76,7 @@ export async function resolveBareSessionResetPromptState(params: { nowMs?: number; isPrimaryRun?: boolean; isCanonicalWorkspace?: boolean; - hasBootstrapFileAccess?: boolean | (() => boolean); + hasBootstrapFileAccess?: boolean | (() => boolean | Promise); }): Promise<{ bootstrapMode: BootstrapMode; prompt: string; @@ -75,7 +87,7 @@ export async function resolveBareSessionResetPromptState(params: { : false; const hasBootstrapFileAccess = bootstrapPending ? typeof params.hasBootstrapFileAccess === "function" - ? params.hasBootstrapFileAccess() + ? await params.hasBootstrapFileAccess() : (params.hasBootstrapFileAccess ?? true) : true; const bootstrapMode = resolveBootstrapMode({