fix(reset): preserve bootstrap guidance for dynamic models (#119179)

Punchcard-Session: silver-valley-valley-dt
This commit is contained in:
Vincent Koc
2026-08-05 10:45:23 +08:00
committed by GitHub
parent e8eb765ab7
commit 1b499e2c3c
3 changed files with 121 additions and 6 deletions
@@ -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<string, unknown>) => {
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);
});
});
@@ -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);
});
});
+17 -5
View File
@@ -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<boolean> {
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<boolean>);
}): 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({