diff --git a/CHANGELOG.md b/CHANGELOG.md index 2037b65a58f..16bff485f52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Agents/workspace: add `agents.defaults.skipOptionalBootstrapFiles` for skipping selected optional workspace files during bootstrap without disabling required workspace setup. (#62110) Thanks @mainstay22. - Voice Call/Google Meet: add Twilio Meet join phase logs around pre-connect DTMF, realtime stream setup, and initial greeting handoff for easier live-call debugging. Thanks @donkeykong91 and @PfanP. - macOS app: move recent session context rows into a Context submenu while keeping usage and cost details root-level, so the menu bar companion stays compact with many active sessions. Thanks @guti. - Gateway/SDK: add SDK-facing tools.invoke RPC with shared HTTP policy, typed approval/refusal results, and SDK helper support. Refs #74705. Thanks @BunsDev and @ai-hpc. diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 8c999c3e90b..5b532b8ffcd 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -d70e31fd5f36d4b117ffa750fba88072d6714edc245a18d4b0915a2d11ce603a config-baseline.json -0a259216178a582c567d1fa48c5236bff4bbd27c3e6af838ffcd042459ffce3c config-baseline.core.json +516de8f5049d2c8b7f326cfc1b665cf459609aa491c432d93b8ca8b9463d7243 config-baseline.json +b06e5cd6e7d3a26d99fd4d31d576c49958195451b0b1e9c2db45f038a3c16c44 config-baseline.core.json da8e055ebba0730498703d209f9e2cfaa1484a83f3240e611dcdd7280e22a525 config-baseline.channel.json 4d017161b4dc986fdc6cc68167fedbd1d415ddbcd66125a872e18aa1769cd182 config-baseline.plugin.json diff --git a/docs/gateway/config-agents.md b/docs/gateway/config-agents.md index f3cc2276b68..8849e6342e7 100644 --- a/docs/gateway/config-agents.md +++ b/docs/gateway/config-agents.md @@ -67,6 +67,20 @@ Disables automatic creation of workspace bootstrap files (`AGENTS.md`, `SOUL.md` } ``` +### `agents.defaults.skipOptionalBootstrapFiles` + +Skips creation of selected optional workspace files while still writing required bootstrap files. Valid values: `SOUL.md`, `USER.md`, `HEARTBEAT.md`, and `IDENTITY.md`. + +```json5 +{ + agents: { + defaults: { + skipOptionalBootstrapFiles: ["SOUL.md", "USER.md"], + }, + }, +} +``` + ### `agents.defaults.contextInjection` Controls when workspace bootstrap files are injected into the system prompt. Default: `"always"`. diff --git a/extensions/brave/package.json b/extensions/brave/package.json index bc7ce6f6a5a..ed34380269a 100644 --- a/extensions/brave/package.json +++ b/extensions/brave/package.json @@ -4,9 +4,6 @@ "private": true, "description": "OpenClaw Brave plugin", "type": "module", - "dependencies": { - "typebox": "1.1.34" - }, "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 535c7264561..9aca7c7c1e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -327,10 +327,6 @@ importers: version: link:../../packages/plugin-sdk extensions/brave: - dependencies: - typebox: - specifier: 1.1.34 - version: 1.1.34 devDependencies: '@openclaw/plugin-sdk': specifier: workspace:* diff --git a/scripts/e2e/npm-telegram-rtt-driver.mjs b/scripts/e2e/npm-telegram-rtt-driver.mjs index 93338c2210b..ffa2286b149 100755 --- a/scripts/e2e/npm-telegram-rtt-driver.mjs +++ b/scripts/e2e/npm-telegram-rtt-driver.mjs @@ -10,12 +10,12 @@ const timeoutMs = Number(process.env.OPENCLAW_QA_TELEGRAM_SCENARIO_TIMEOUT_MS ?? const canaryTimeoutMs = Number( process.env.OPENCLAW_QA_TELEGRAM_CANARY_TIMEOUT_MS ?? String(timeoutMs), ); -const scenarioIds = ( - process.env.OPENCLAW_NPM_TELEGRAM_SCENARIOS ?? "telegram-mentioned-message-reply" -) - .split(",") - .map((value) => value.trim()) - .filter(Boolean); +const scenarioIds = new Set( + (process.env.OPENCLAW_NPM_TELEGRAM_SCENARIOS ?? "telegram-mentioned-message-reply") + .split(",") + .map((value) => value.trim()) + .filter(Boolean), +); if (!groupId || !driverToken || !sutToken) { throw new Error( @@ -63,10 +63,6 @@ function messageText(message) { return message.text ?? message.caption ?? ""; } -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - async function flushUpdates(bot) { let updates = await bot.getUpdates({ timeout: 0, allowed_updates: ["message"] }); let nextOffset; @@ -194,7 +190,7 @@ async function main() { }), ); - if (scenarioIds.includes("telegram-mentioned-message-reply")) { + if (scenarioIds.has("telegram-mentioned-message-reply")) { const marker = `OPENCLAW_RTT_${Date.now().toString(36)}`; scenarios.push( await runScenario({ diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index b6931954e52..993a253156f 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -373,6 +373,7 @@ async function prepareAgentCommandExecution( const workspace = await ensureAgentWorkspace({ dir: workspaceDirRaw, ensureBootstrapFiles: !agentCfg?.skipBootstrap, + skipOptionalBootstrapFiles: agentCfg?.skipOptionalBootstrapFiles, }); const workspaceDir = workspace.dir; const runId = opts.runId?.trim() || sessionId; diff --git a/src/agents/sandbox/context.ts b/src/agents/sandbox/context.ts index 970a0b0fe26..5929934a3ab 100644 --- a/src/agents/sandbox/context.ts +++ b/src/agents/sandbox/context.ts @@ -49,6 +49,7 @@ async function ensureSandboxWorkspaceLayout(params: { sandboxWorkspaceDir, agentWorkspaceDir, params.config?.agents?.defaults?.skipBootstrap, + params.config?.agents?.defaults?.skipOptionalBootstrapFiles, ); if (cfg.workspaceAccess !== "rw") { try { diff --git a/src/agents/sandbox/workspace.ts b/src/agents/sandbox/workspace.ts index cca63819fde..667847d5844 100644 --- a/src/agents/sandbox/workspace.ts +++ b/src/agents/sandbox/workspace.ts @@ -1,6 +1,7 @@ import syncFs from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; +import type { OptionalBootstrapFileName } from "../../config/types.agent-defaults.js"; import { openBoundaryFile } from "../../infra/boundary-file-read.js"; import { resolveUserPath } from "../../utils.js"; import { @@ -18,6 +19,7 @@ export async function ensureSandboxWorkspace( workspaceDir: string, seedFrom?: string, skipBootstrap?: boolean, + skipOptionalBootstrapFiles?: OptionalBootstrapFileName[], ) { await fs.mkdir(workspaceDir, { recursive: true }); if (seedFrom) { @@ -61,5 +63,6 @@ export async function ensureSandboxWorkspace( await ensureAgentWorkspace({ dir: workspaceDir, ensureBootstrapFiles: !skipBootstrap, + skipOptionalBootstrapFiles, }); } diff --git a/src/agents/workspace.test.ts b/src/agents/workspace.test.ts index d110f3dcc89..8e338783a59 100644 --- a/src/agents/workspace.test.ts +++ b/src/agents/workspace.test.ts @@ -155,6 +155,55 @@ describe("ensureAgentWorkspace", () => { await expectCompletedWithoutBootstrap(tempDir); }); + it("skips configured optional bootstrap files without skipping required files", async () => { + const tempDir = await makeTempWorkspace("openclaw-workspace-"); + + await ensureAgentWorkspace({ + dir: tempDir, + ensureBootstrapFiles: true, + skipOptionalBootstrapFiles: [ + DEFAULT_SOUL_FILENAME, + DEFAULT_IDENTITY_FILENAME, + DEFAULT_USER_FILENAME, + DEFAULT_HEARTBEAT_FILENAME, + ], + }); + + await expect(fs.access(path.join(tempDir, DEFAULT_AGENTS_FILENAME))).resolves.toBeUndefined(); + await expect(fs.access(path.join(tempDir, DEFAULT_TOOLS_FILENAME))).resolves.toBeUndefined(); + await expect( + fs.access(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME)), + ).resolves.toBeUndefined(); + for (const fileName of [ + DEFAULT_SOUL_FILENAME, + DEFAULT_IDENTITY_FILENAME, + DEFAULT_USER_FILENAME, + DEFAULT_HEARTBEAT_FILENAME, + ]) { + await expect(fs.access(path.join(tempDir, fileName))).rejects.toMatchObject({ + code: "ENOENT", + }); + } + }); + + it("preserves legacy setup detection when skipped profile files already exist", async () => { + const tempDir = await makeTempWorkspace("openclaw-workspace-"); + await writeWorkspaceFile({ dir: tempDir, name: DEFAULT_IDENTITY_FILENAME, content: "custom" }); + await writeWorkspaceFile({ dir: tempDir, name: DEFAULT_USER_FILENAME, content: "custom" }); + + await ensureAgentWorkspace({ + dir: tempDir, + ensureBootstrapFiles: true, + skipOptionalBootstrapFiles: [DEFAULT_IDENTITY_FILENAME, DEFAULT_USER_FILENAME], + }); + + await expect(fs.access(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME))).rejects.toMatchObject({ + code: "ENOENT", + }); + const state = await readWorkspaceState(tempDir); + expect(state.setupCompletedAt).toMatch(/\d{4}-\d{2}-\d{2}T/); + }); + it("migrates legacy onboardingCompletedAt markers to setupCompletedAt", async () => { const tempDir = await makeTempWorkspace("openclaw-workspace-"); await fs.mkdir(path.join(tempDir, ".openclaw"), { recursive: true }); diff --git a/src/agents/workspace.ts b/src/agents/workspace.ts index b9ed0ca3921..69bf64ca604 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -174,6 +174,13 @@ const VALID_BOOTSTRAP_NAMES: ReadonlySet = new Set([ DEFAULT_MEMORY_FILENAME, ]); +const OPTIONAL_BOOTSTRAP_FILENAMES: ReadonlySet = new Set([ + DEFAULT_SOUL_FILENAME, + DEFAULT_IDENTITY_FILENAME, + DEFAULT_USER_FILENAME, + DEFAULT_HEARTBEAT_FILENAME, +]); + async function writeFileIfMissing(filePath: string, content: string): Promise { try { await fs.writeFile(filePath, content, { @@ -467,6 +474,12 @@ async function ensureGitRepo(dir: string, isBrandNewWorkspace: boolean) { export async function ensureAgentWorkspace(params?: { dir?: string; ensureBootstrapFiles?: boolean; + /** + * List of optional bootstrap filenames to skip writing. + * Applies only to SOUL.md, USER.md, HEARTBEAT.md, IDENTITY.md. + * Required workspace setup such as AGENTS.md and TOOLS.md still runs. + */ + skipOptionalBootstrapFiles?: string[]; }): Promise<{ dir: string; agentsPath?: string; @@ -519,12 +532,24 @@ export async function ensureAgentWorkspace(params?: { const identityTemplate = await loadTemplate(DEFAULT_IDENTITY_FILENAME); const userTemplate = await loadTemplate(DEFAULT_USER_FILENAME); const heartbeatTemplate = await loadTemplate(DEFAULT_HEARTBEAT_FILENAME); + const skipOptionalBootstrapFiles = new Set(params?.skipOptionalBootstrapFiles ?? []); + const shouldWriteBootstrapFile = (fileName: string): boolean => + !OPTIONAL_BOOTSTRAP_FILENAMES.has(fileName) || !skipOptionalBootstrapFiles.has(fileName); + await writeFileIfMissing(agentsPath, agentsTemplate); - await writeFileIfMissing(soulPath, soulTemplate); + if (shouldWriteBootstrapFile(DEFAULT_SOUL_FILENAME)) { + await writeFileIfMissing(soulPath, soulTemplate); + } await writeFileIfMissing(toolsPath, toolsTemplate); - const identityPathCreated = await writeFileIfMissing(identityPath, identityTemplate); - await writeFileIfMissing(userPath, userTemplate); - await writeFileIfMissing(heartbeatPath, heartbeatTemplate); + const identityPathCreated = shouldWriteBootstrapFile(DEFAULT_IDENTITY_FILENAME) + ? await writeFileIfMissing(identityPath, identityTemplate) + : false; + if (shouldWriteBootstrapFile(DEFAULT_USER_FILENAME)) { + await writeFileIfMissing(userPath, userTemplate); + } + if (shouldWriteBootstrapFile(DEFAULT_HEARTBEAT_FILENAME)) { + await writeFileIfMissing(heartbeatPath, heartbeatTemplate); + } let state = await readWorkspaceSetupState(statePath, { persistLegacyMigration: true, diff --git a/src/auto-reply/reply/get-reply.ts b/src/auto-reply/reply/get-reply.ts index be2d0828f2d..532b152013e 100644 --- a/src/auto-reply/reply/get-reply.ts +++ b/src/auto-reply/reply/get-reply.ts @@ -242,6 +242,7 @@ export async function getReplyFromConfig( : await ensureAgentWorkspace({ dir: workspaceDirRaw, ensureBootstrapFiles: !agentCfg?.skipBootstrap && !isFastTestEnv, + skipOptionalBootstrapFiles: agentCfg?.skipOptionalBootstrapFiles, }); const workspaceDir = workspace.dir; const agentDir = resolveAgentDir(cfg, agentId); diff --git a/src/commands/agents.commands.add.ts b/src/commands/agents.commands.add.ts index e9de17c1eb9..9a798032eef 100644 --- a/src/commands/agents.commands.add.ts +++ b/src/commands/agents.commands.add.ts @@ -177,6 +177,7 @@ export async function agentsAddCommand( const quietRuntime = opts.json ? createQuietRuntime(runtime) : runtime; await ensureWorkspaceAndSessions(workspaceDir, quietRuntime, { skipBootstrap: Boolean(bindingResult.config.agents?.defaults?.skipBootstrap), + skipOptionalBootstrapFiles: bindingResult.config.agents?.defaults?.skipOptionalBootstrapFiles, agentId, }); @@ -431,6 +432,7 @@ export async function agentsAddCommand( logConfigUpdated(runtime); await ensureWorkspaceAndSessions(workspaceDir, runtime, { skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap), + skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles, agentId, }); diff --git a/src/commands/configure.wizard.ts b/src/commands/configure.wizard.ts index 8c533b4614d..80b0e055075 100644 --- a/src/commands/configure.wizard.ts +++ b/src/commands/configure.wizard.ts @@ -589,7 +589,10 @@ export async function runConfigureWizard( }, }, }; - await ensureWorkspaceAndSessions(workspaceDir, runtime); + await ensureWorkspaceAndSessions(workspaceDir, runtime, { + skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap), + skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles, + }); }; const configureChannelsSection = async () => { diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts index 1b69d59faec..905f3c7b00f 100644 --- a/src/commands/onboard-helpers.ts +++ b/src/commands/onboard-helpers.ts @@ -6,6 +6,7 @@ import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace } from "../agents/wor import { resolveAgentModelPrimaryValue } from "../config/model-input.js"; import { resolveConfigPath } from "../config/paths.js"; import { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js"; +import type { OptionalBootstrapFileName } from "../config/types.agent-defaults.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveControlUiLinks } from "../gateway/control-ui-links.js"; import { normalizeControlUiBasePath } from "../gateway/control-ui-shared.js"; @@ -164,11 +165,16 @@ function resolveSshTargetHint(): string { export async function ensureWorkspaceAndSessions( workspaceDir: string, runtime: RuntimeEnv, - options?: { skipBootstrap?: boolean; agentId?: string }, + options?: { + skipBootstrap?: boolean; + skipOptionalBootstrapFiles?: OptionalBootstrapFileName[]; + agentId?: string; + }, ) { const ws = await ensureAgentWorkspace({ dir: workspaceDir, ensureBootstrapFiles: !options?.skipBootstrap, + skipOptionalBootstrapFiles: options?.skipOptionalBootstrapFiles, }); runtime.log(`Workspace OK: ${shortenHomePath(ws.dir)}`); const sessionsDir = resolveSessionTranscriptsDirForAgent(options?.agentId); diff --git a/src/commands/onboard-non-interactive/local.ts b/src/commands/onboard-non-interactive/local.ts index 4149dc2af06..da522480d75 100644 --- a/src/commands/onboard-non-interactive/local.ts +++ b/src/commands/onboard-non-interactive/local.ts @@ -214,6 +214,7 @@ export async function runNonInteractiveLocalSetup(params: { await ensureWorkspaceAndSessions(workspaceDir, runtime, { skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap), + skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles, }); const daemonRuntimeRaw = opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME; diff --git a/src/commands/setup.test.ts b/src/commands/setup.test.ts index ef5396a2dbb..64e835c820e 100644 --- a/src/commands/setup.test.ts +++ b/src/commands/setup.test.ts @@ -84,6 +84,42 @@ describe("setupCommand", () => { }); }); + it("threads skipOptionalBootstrapFiles into workspace creation", async () => { + await withTempHome(async (home) => { + const runtime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }; + const configDir = path.join(home, ".openclaw"); + const configPath = path.join(configDir, "openclaw.json"); + const deps = createSetupDeps(home); + const workspace = path.join(home, "custom-workspace"); + + await fs.mkdir(configDir, { recursive: true }); + await fs.writeFile( + configPath, + JSON.stringify({ + agents: { + defaults: { + workspace, + skipOptionalBootstrapFiles: ["IDENTITY.md", "USER.md"], + }, + }, + }), + ); + + await setupCommand(undefined, runtime, deps); + + expect(deps.ensureAgentWorkspace).toHaveBeenCalledWith( + expect.objectContaining({ + dir: workspace, + skipOptionalBootstrapFiles: ["IDENTITY.md", "USER.md"], + }), + ); + }); + }); + it("treats non-object config roots as empty config", async () => { await withTempHome(async (home) => { const runtime = { diff --git a/src/commands/setup.ts b/src/commands/setup.ts index 003d9abbd3e..e12dcd1a6ec 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import JSON5 from "json5"; import { z } from "zod"; +import type { OptionalBootstrapFileName } from "../config/types.agent-defaults.js"; import type { OpenClawConfig } from "../config/types.js"; import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; @@ -16,6 +17,7 @@ type ConfigIO = { type EnsureAgentWorkspace = (params: { dir: string; ensureBootstrapFiles?: boolean; + skipOptionalBootstrapFiles?: OptionalBootstrapFileName[]; }) => Promise<{ dir: string }>; type SetupCommandDeps = { @@ -191,6 +193,7 @@ export async function setupCommand( const ws = await (deps.ensureAgentWorkspace ?? ensureDefaultAgentWorkspace)({ dir: workspace, ensureBootstrapFiles: !next.agents?.defaults?.skipBootstrap, + skipOptionalBootstrapFiles: next.agents?.defaults?.skipOptionalBootstrapFiles, }); runtime.log(`Workspace OK: ${shortenHomePath(ws.dir)}`); diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index 0e95499a7d1..d19ea0529ad 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -3690,6 +3690,16 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { skipBootstrap: { type: "boolean", }, + skipOptionalBootstrapFiles: { + type: "array", + items: { + type: "string", + enum: ["SOUL.md", "USER.md", "HEARTBEAT.md", "IDENTITY.md"], + }, + title: "Skipped Optional Bootstrap Files", + description: + "Optional bootstrap files that should not be created in agent workspaces. Valid values: SOUL.md, USER.md, HEARTBEAT.md, IDENTITY.md.", + }, contextInjection: { anyOf: [ { @@ -26184,6 +26194,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { help: 'Friendly interaction-style layer for GPT-5-family models ("friendly" or "on" enables it; "off" disables only that layer). The tagged behavior contract remains enabled for matching GPT-5 models.', tags: ["advanced"], }, + "agents.defaults.skipOptionalBootstrapFiles": { + label: "Skipped Optional Bootstrap Files", + help: "Optional bootstrap files that should not be created in agent workspaces. Valid values: SOUL.md, USER.md, HEARTBEAT.md, IDENTITY.md.", + tags: ["storage"], + }, "agents.defaults.contextInjection": { label: "Context Injection", help: 'Controls when workspace bootstrap files are injected into the system prompt: "always" (default) or "continuation-skip" for safe continuation turns after a completed assistant response.', diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index ca8b1540d04..57272b772d4 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -958,6 +958,8 @@ export const FIELD_HELP: Record = { "Maximum same-provider auth-profile rotations allowed for rate-limit errors before switching to model fallback (default: 1).", "agents.defaults.workspace": "Default workspace path exposed to agent runtime tools for filesystem context and repo-aware behavior. Set this explicitly when running from wrappers so path resolution stays deterministic.", + "agents.defaults.skipOptionalBootstrapFiles": + "Optional bootstrap files that should not be created in agent workspaces. Valid values: SOUL.md, USER.md, HEARTBEAT.md, IDENTITY.md.", "agents.defaults.contextInjection": 'Controls when workspace bootstrap files are injected into the system prompt: "always" (default) or "continuation-skip" for safe continuation turns after a completed assistant response.', "agents.defaults.bootstrapMaxChars": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 484d4830ca6..6b06cc5a954 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -387,6 +387,7 @@ export const FIELD_LABELS: Record = { "agents.defaults.promptOverlays": "Prompt Overlays", "agents.defaults.promptOverlays.gpt5": "GPT-5 Prompt Overlay", "agents.defaults.promptOverlays.gpt5.personality": "GPT-5 Personality Overlay", + "agents.defaults.skipOptionalBootstrapFiles": "Skipped Optional Bootstrap Files", "agents.defaults.contextInjection": "Context Injection", "agents.defaults.bootstrapMaxChars": "Bootstrap Max Chars", "agents.defaults.bootstrapTotalMaxChars": "Bootstrap Total Max Chars", diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index 5265437ccaa..65a3c77ed18 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -17,6 +17,7 @@ import type { import type { MemorySearchConfig } from "./types.tools.js"; export type AgentContextInjection = "always" | "continuation-skip" | "never"; +export type OptionalBootstrapFileName = "SOUL.md" | "USER.md" | "HEARTBEAT.md" | "IDENTITY.md"; export type EmbeddedPiExecutionContract = "default" | "strict-agentic"; export type Gpt5PromptOverlayConfig = { @@ -224,6 +225,13 @@ export type AgentDefaultsConfig = { promptOverlays?: PromptOverlaysConfig; /** Skip bootstrap (BOOTSTRAP.md creation, etc.) for pre-configured deployments. */ skipBootstrap?: boolean; + /** + * List of optional bootstrap filenames to skip writing to the workspace root. + * Applies to: SOUL.md, USER.md, HEARTBEAT.md, IDENTITY.md. + * Required workspace setup such as AGENTS.md and TOOLS.md still runs. + * Example: ["SOUL.md", "USER.md", "HEARTBEAT.md", "IDENTITY.md"] + */ + skipOptionalBootstrapFiles?: OptionalBootstrapFileName[]; /** * Controls when workspace bootstrap files (AGENTS.md, SOUL.md, etc.) are * injected into the system prompt: diff --git a/src/config/zod-schema.agent-defaults.test.ts b/src/config/zod-schema.agent-defaults.test.ts index 06030ddab5a..95d2bdceea6 100644 --- a/src/config/zod-schema.agent-defaults.test.ts +++ b/src/config/zod-schema.agent-defaults.test.ts @@ -83,6 +83,25 @@ describe("agent defaults schema", () => { expect(() => AgentDefaultsSchema.parse({ contextInjection: "unknown" })).toThrow(); }); + it("accepts supported optional bootstrap filenames", () => { + const result = AgentDefaultsSchema.parse({ + skipOptionalBootstrapFiles: ["SOUL.md", "USER.md", "HEARTBEAT.md", "IDENTITY.md"], + })!; + expect(result.skipOptionalBootstrapFiles).toEqual([ + "SOUL.md", + "USER.md", + "HEARTBEAT.md", + "IDENTITY.md", + ]); + }); + + it("rejects unsupported optional bootstrap filenames", () => { + expect(() => + AgentDefaultsSchema.parse({ skipOptionalBootstrapFiles: ["AGENTS.md"] }), + ).toThrow(); + expect(() => AgentDefaultsSchema.parse({ skipOptionalBootstrapFiles: ["SOUL.MD"] })).toThrow(); + }); + it("accepts embeddedPi.executionContract", () => { const result = AgentDefaultsSchema.parse({ embeddedPi: { diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index 76d04eb4cce..ce55069e36f 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -24,6 +24,13 @@ const NonNegativeByteSizeSchema = z.union([ z.string().refine(isValidNonNegativeByteSizeString, "Expected byte size string like 2mb"), ]); +const OptionalBootstrapFileNameSchema = z.enum([ + "SOUL.md", + "USER.md", + "HEARTBEAT.md", + "IDENTITY.md", +]); + export const SilentReplyPolicyConfigSchema = z .object({ direct: SilentReplyPolicySchema.optional(), @@ -89,6 +96,7 @@ export const AgentDefaultsSchema = z .strict() .optional(), skipBootstrap: z.boolean().optional(), + skipOptionalBootstrapFiles: z.array(OptionalBootstrapFileNameSchema).optional(), contextInjection: z .union([z.literal("always"), z.literal("continuation-skip"), z.literal("never")]) .optional(), @@ -231,9 +239,7 @@ export const AgentDefaultsSchema = z ]) .optional(), verboseDefault: z.union([z.literal("off"), z.literal("on"), z.literal("full")]).optional(), - reasoningDefault: z - .union([z.literal("off"), z.literal("on"), z.literal("stream")]) - .optional(), + reasoningDefault: z.union([z.literal("off"), z.literal("on"), z.literal("stream")]).optional(), elevatedDefault: z .union([z.literal("off"), z.literal("on"), z.literal("ask"), z.literal("full")]) .optional(), diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index 95ad674d388..1e83a15314a 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -519,6 +519,7 @@ async function prepareCronRunContext(params: { const workspace = await ensureAgentWorkspace({ dir: workspaceDirRaw, ensureBootstrapFiles: !agentCfg?.skipBootstrap && !params.isFastTestEnv, + skipOptionalBootstrapFiles: agentCfg?.skipOptionalBootstrapFiles, }); const workspaceDir = workspace.dir; diff --git a/src/gateway/server-methods/agents.ts b/src/gateway/server-methods/agents.ts index 0f93665f447..d8a47cc5721 100644 --- a/src/gateway/server-methods/agents.ts +++ b/src/gateway/server-methods/agents.ts @@ -494,7 +494,11 @@ export const agentsHandlers: GatewayRequestHandlers = { // Ensure workspace & transcripts exist BEFORE writing config so a failure // here does not leave a broken config entry behind. const skipBootstrap = Boolean(nextConfig.agents?.defaults?.skipBootstrap); - await ensureAgentWorkspace({ dir: workspaceDir, ensureBootstrapFiles: !skipBootstrap }); + await ensureAgentWorkspace({ + dir: workspaceDir, + ensureBootstrapFiles: !skipBootstrap, + skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles, + }); await fs.mkdir(resolveSessionTranscriptsDirForAgent(agentId), { recursive: true }); const persistedIdentity = normalizeIdentityForFile(resolveAgentIdentity(nextConfig, agentId)); @@ -575,6 +579,7 @@ export const agentsHandlers: GatewayRequestHandlers = { ensuredWorkspace = await ensureAgentWorkspace({ dir: workspaceDir, ensureBootstrapFiles: !skipBootstrap, + skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles, }); } diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index bf83f6f5c98..3d3a17e21de 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -687,6 +687,24 @@ function hasExplicitCompatibilityInputs(options: PluginLoadOptions): boolean { ); } +function pluginLoadOptionsMatchCacheKey( + options: PluginLoadOptions, + expectedCacheKey: string, +): boolean { + if (resolvePluginLoadCacheContext(options).cacheKey === expectedCacheKey) { + return true; + } + if (options.installBundledRuntimeDeps !== false) { + return false; + } + return ( + resolvePluginLoadCacheContext({ + ...options, + installBundledRuntimeDeps: undefined, + }).cacheKey === expectedCacheKey + ); +} + type PluginRegistrationPlan = { /** Public compatibility label passed to plugin register(api). */ mode: PluginRegistrationMode; @@ -896,15 +914,15 @@ function getCompatibleActivePluginRegistry( return undefined; } const loadContext = resolvePluginLoadCacheContext(options); - if (loadContext.cacheKey === activeCacheKey) { + if (pluginLoadOptionsMatchCacheKey(options, activeCacheKey)) { return activeRegistry; } if (!loadContext.shouldActivate) { - const activatingCacheKey = resolvePluginLoadCacheContext({ + const activatingOptions = { ...options, activate: true, - }).cacheKey; - if (activatingCacheKey === activeCacheKey) { + }; + if (pluginLoadOptionsMatchCacheKey(activatingOptions, activeCacheKey)) { return activeRegistry; } } @@ -912,26 +930,26 @@ function getCompatibleActivePluginRegistry( loadContext.runtimeSubagentMode === "default" && getActivePluginRuntimeSubagentMode() === "gateway-bindable" ) { - const gatewayBindableCacheKey = resolvePluginLoadCacheContext({ + const gatewayBindableOptions = { ...options, runtimeOptions: { ...options.runtimeOptions, allowGatewaySubagentBinding: true, }, - }).cacheKey; - if (gatewayBindableCacheKey === activeCacheKey) { + }; + if (pluginLoadOptionsMatchCacheKey(gatewayBindableOptions, activeCacheKey)) { return activeRegistry; } if (!loadContext.shouldActivate) { - const activatingGatewayBindableCacheKey = resolvePluginLoadCacheContext({ + const activatingGatewayBindableOptions = { ...options, activate: true, runtimeOptions: { ...options.runtimeOptions, allowGatewaySubagentBinding: true, }, - }).cacheKey; - if (activatingGatewayBindableCacheKey === activeCacheKey) { + }; + if (pluginLoadOptionsMatchCacheKey(activatingGatewayBindableOptions, activeCacheKey)) { return activeRegistry; } } diff --git a/src/wizard/setup.ts b/src/wizard/setup.ts index 19da67f29af..8913dbbdfaa 100644 --- a/src/wizard/setup.ts +++ b/src/wizard/setup.ts @@ -741,6 +741,7 @@ export async function runSetupWizard( logConfigUpdated(runtime); await onboardHelpers.ensureWorkspaceAndSessions(workspaceDir, runtime, { skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap), + skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles, }); if (opts.skipSearch) {