From 0dd57f6965ca7b3e11bb63ec1c5383d767c129f5 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Sat, 14 Feb 2026 18:19:02 -0500 Subject: [PATCH] fix(workspace): keep bootstrap one-shot after onboarding --- CHANGELOG.md | 1 + src/agents/workspace.e2e.test.ts | 29 ++++++++++++++++++++++++++--- src/agents/workspace.ts | 22 ++++++++++++++-------- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb3cc561285..54c993da9b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai - Agents/Process/Bootstrap: preserve unbounded `process log` offset-only pagination (default tail applies only when both `offset` and `limit` are omitted) and enforce strict `bootstrapTotalMaxChars` budgeting across injected bootstrap content (including markers), skipping additional injection when remaining budget is too small. (#16539) Thanks @CharlieGreenman. - Sessions/Agents: harden transcript path resolution for mismatched agent context by preserving explicit store roots and adding safe absolute-path fallback to the correct agent sessions directory. (#16288) Thanks @robbyczgw-cla. - Agents: keep unresolved mutating tool failures visible until the same action retry succeeds, scope mutation-error surfacing to mutating calls (including `session_status` model changes), and dedupe duplicate failure warnings in outbound replies. (#16131) Thanks @Swader. +- Agents/Workspace: create `BOOTSTRAP.md` when core workspace files are seeded in partially initialized workspaces, while keeping BOOTSTRAP one-shot after onboarding deletion. (#16457) - Agents: classify external timeout aborts during compaction the same as internal timeouts, preventing unnecessary auth-profile rotation and preserving compaction-timeout snapshot fallback behavior. (#9855) Thanks @mverrilli. - Ollama/Agents: avoid forcing `` tag enforcement for Ollama models, which could suppress all output as `(no output)`. (#16191) Thanks @Glucksberg. - Plugins: suppress false duplicate plugin id warnings when the same extension is discovered via multiple paths (config/workspace/global vs bundled), while still warning on genuine duplicates. (#16222) Thanks @shadril238. diff --git a/src/agents/workspace.e2e.test.ts b/src/agents/workspace.e2e.test.ts index 92e2f425c28..c3b3cfc27b2 100644 --- a/src/agents/workspace.e2e.test.ts +++ b/src/agents/workspace.e2e.test.ts @@ -3,6 +3,7 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { makeTempWorkspace, writeWorkspaceFile } from "../test-helpers/workspace.js"; import { + DEFAULT_AGENTS_FILENAME, DEFAULT_BOOTSTRAP_FILENAME, DEFAULT_MEMORY_ALT_FILENAME, DEFAULT_MEMORY_FILENAME, @@ -23,13 +24,35 @@ describe("resolveDefaultAgentWorkspaceDir", () => { }); describe("ensureAgentWorkspace", () => { - it("creates BOOTSTRAP.md even when workspace already has other bootstrap files", async () => { + it("creates BOOTSTRAP.md for a brand new workspace", async () => { const tempDir = await makeTempWorkspace("openclaw-workspace-"); - await writeWorkspaceFile({ dir: tempDir, name: "AGENTS.md", content: "existing" }); await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }); - await expect(fs.access(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME))).resolves.toBeUndefined(); + await expect( + fs.access(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME)), + ).resolves.toBeUndefined(); + }); + + it("creates BOOTSTRAP.md even when workspace already has other bootstrap files", async () => { + const tempDir = await makeTempWorkspace("openclaw-workspace-"); + await writeWorkspaceFile({ dir: tempDir, name: DEFAULT_AGENTS_FILENAME, content: "existing" }); + + await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }); + + await expect( + fs.access(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME)), + ).resolves.toBeUndefined(); + }); + + it("does not recreate BOOTSTRAP.md after onboarding deletion", async () => { + const tempDir = await makeTempWorkspace("openclaw-workspace-"); + await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }); + await fs.unlink(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME)); + + await ensureAgentWorkspace({ dir: tempDir, ensureBootstrapFiles: true }); + + await expect(fs.access(path.join(tempDir, DEFAULT_BOOTSTRAP_FILENAME))).rejects.toThrow(); }); }); diff --git a/src/agents/workspace.ts b/src/agents/workspace.ts index cb6849e6c8a..052e6952035 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -106,17 +106,19 @@ const VALID_BOOTSTRAP_NAMES: ReadonlySet = new Set([ DEFAULT_MEMORY_ALT_FILENAME, ]); -async function writeFileIfMissing(filePath: string, content: string) { +async function writeFileIfMissing(filePath: string, content: string): Promise { try { await fs.writeFile(filePath, content, { encoding: "utf-8", flag: "wx", }); + return true; } catch (err) { const anyErr = err as { code?: string }; if (anyErr.code !== "EEXIST") { throw err; } + return false; } } @@ -215,13 +217,17 @@ export async function ensureAgentWorkspace(params?: { const heartbeatTemplate = await loadTemplate(DEFAULT_HEARTBEAT_FILENAME); const bootstrapTemplate = await loadTemplate(DEFAULT_BOOTSTRAP_FILENAME); - await writeFileIfMissing(agentsPath, agentsTemplate); - await writeFileIfMissing(soulPath, soulTemplate); - await writeFileIfMissing(toolsPath, toolsTemplate); - await writeFileIfMissing(identityPath, identityTemplate); - await writeFileIfMissing(userPath, userTemplate); - await writeFileIfMissing(heartbeatPath, heartbeatTemplate); - await writeFileIfMissing(bootstrapPath, bootstrapTemplate); + const wroteAgents = await writeFileIfMissing(agentsPath, agentsTemplate); + const wroteSoul = await writeFileIfMissing(soulPath, soulTemplate); + const wroteTools = await writeFileIfMissing(toolsPath, toolsTemplate); + const wroteIdentity = await writeFileIfMissing(identityPath, identityTemplate); + const wroteUser = await writeFileIfMissing(userPath, userTemplate); + const wroteHeartbeat = await writeFileIfMissing(heartbeatPath, heartbeatTemplate); + const wroteAnyCoreBootstrapFile = + wroteAgents || wroteSoul || wroteTools || wroteIdentity || wroteUser || wroteHeartbeat; + if (isBrandNewWorkspace || wroteAnyCoreBootstrapFile) { + await writeFileIfMissing(bootstrapPath, bootstrapTemplate); + } await ensureGitRepo(dir, isBrandNewWorkspace); return {