Files
openclaw/src/agents/bootstrap-files.ts
Jose E Velez 0c8fa63b93 feat: lightweight bootstrap context mode for heartbeat/cron runs (openclaw#26064) thanks @jose-velez
Verified:
- pnpm build
- pnpm check (fails on pre-existing unrelated repo issues in extensions/diffs and src/agents/tools/nodes-tool.test.ts)
- pnpm vitest run src/agents/bootstrap-files.test.ts src/infra/heartbeat-runner.model-override.test.ts src/cli/cron-cli.test.ts
- pnpm test:macmini (fails on pre-existing extensions/diffs import errors; touched suites pass)

Co-authored-by: jose-velez <10926182+jose-velez@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-01 20:13:24 -06:00

119 lines
3.8 KiB
TypeScript

import type { OpenClawConfig } from "../config/config.js";
import { getOrLoadBootstrapFiles } from "./bootstrap-cache.js";
import { applyBootstrapHookOverrides } from "./bootstrap-hooks.js";
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
import {
buildBootstrapContextFiles,
resolveBootstrapMaxChars,
resolveBootstrapTotalMaxChars,
} from "./pi-embedded-helpers.js";
import {
filterBootstrapFilesForSession,
loadWorkspaceBootstrapFiles,
type WorkspaceBootstrapFile,
} from "./workspace.js";
export type BootstrapContextMode = "full" | "lightweight";
export type BootstrapContextRunKind = "default" | "heartbeat" | "cron";
export function makeBootstrapWarn(params: {
sessionLabel: string;
warn?: (message: string) => void;
}): ((message: string) => void) | undefined {
if (!params.warn) {
return undefined;
}
return (message: string) => params.warn?.(`${message} (sessionKey=${params.sessionLabel})`);
}
function sanitizeBootstrapFiles(
files: WorkspaceBootstrapFile[],
warn?: (message: string) => void,
): WorkspaceBootstrapFile[] {
const sanitized: WorkspaceBootstrapFile[] = [];
for (const file of files) {
const pathValue = typeof file.path === "string" ? file.path.trim() : "";
if (!pathValue) {
warn?.(
`skipping bootstrap file "${file.name}" — missing or invalid "path" field (hook may have used "filePath" instead)`,
);
continue;
}
sanitized.push({ ...file, path: pathValue });
}
return sanitized;
}
function applyContextModeFilter(params: {
files: WorkspaceBootstrapFile[];
contextMode?: BootstrapContextMode;
runKind?: BootstrapContextRunKind;
}): WorkspaceBootstrapFile[] {
const contextMode = params.contextMode ?? "full";
const runKind = params.runKind ?? "default";
if (contextMode !== "lightweight") {
return params.files;
}
if (runKind === "heartbeat") {
return params.files.filter((file) => file.name === "HEARTBEAT.md");
}
// cron/default lightweight mode keeps bootstrap context empty on purpose.
return [];
}
export async function resolveBootstrapFilesForRun(params: {
workspaceDir: string;
config?: OpenClawConfig;
sessionKey?: string;
sessionId?: string;
agentId?: string;
warn?: (message: string) => void;
contextMode?: BootstrapContextMode;
runKind?: BootstrapContextRunKind;
}): Promise<WorkspaceBootstrapFile[]> {
const sessionKey = params.sessionKey ?? params.sessionId;
const rawFiles = params.sessionKey
? await getOrLoadBootstrapFiles({
workspaceDir: params.workspaceDir,
sessionKey: params.sessionKey,
})
: await loadWorkspaceBootstrapFiles(params.workspaceDir);
const bootstrapFiles = applyContextModeFilter({
files: filterBootstrapFilesForSession(rawFiles, sessionKey),
contextMode: params.contextMode,
runKind: params.runKind,
});
const updated = await applyBootstrapHookOverrides({
files: bootstrapFiles,
workspaceDir: params.workspaceDir,
config: params.config,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
agentId: params.agentId,
});
return sanitizeBootstrapFiles(updated, params.warn);
}
export async function resolveBootstrapContextForRun(params: {
workspaceDir: string;
config?: OpenClawConfig;
sessionKey?: string;
sessionId?: string;
agentId?: string;
warn?: (message: string) => void;
contextMode?: BootstrapContextMode;
runKind?: BootstrapContextRunKind;
}): Promise<{
bootstrapFiles: WorkspaceBootstrapFile[];
contextFiles: EmbeddedContextFile[];
}> {
const bootstrapFiles = await resolveBootstrapFilesForRun(params);
const contextFiles = buildBootstrapContextFiles(bootstrapFiles, {
maxChars: resolveBootstrapMaxChars(params.config),
totalMaxChars: resolveBootstrapTotalMaxChars(params.config),
warn: params.warn,
});
return { bootstrapFiles, contextFiles };
}