From 21cb241f02ae16c6075bb5369401f8985b7ebb0a Mon Sep 17 00:00:00 2001 From: Topanga Ludwitt Date: Wed, 11 Mar 2026 13:09:41 -0700 Subject: [PATCH 1/2] fix: workspace template resolution fails in bundled dist/ layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After bundling, all source files land in dist/ (one level deep). The FALLBACK_TEMPLATE_DIR used '../../docs/reference/templates' relative to the module file, which correctly resolves from the source layout (src/agents/) but overshoots in the bundled layout (dist/) — landing in node_modules/ instead of the package root. When resolveOpenClawPackageRoot returns null (e.g. in isolated cron sessions or worker processes where the module context differs), the resolution falls through to cwd-based lookup, which for the gateway daemon is the home directory. This causes: Error: Missing workspace template: AGENTS.md (/home/user/docs/reference/templates/AGENTS.md) Fix: Add both '../' and '../../' relative paths as fallback candidates (covering bundled and source layouts). Also reorder candidates to prefer module-relative paths over cwd-based paths, since cwd is unreliable for daemon processes. Closes: gateway crash when TUI input triggers session creation in isolated cron contexts. --- src/agents/workspace-templates.test.ts | 23 ++++++++++++++++++++ src/agents/workspace-templates.ts | 30 ++++++++++++++++++++------ 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/agents/workspace-templates.test.ts b/src/agents/workspace-templates.test.ts index 1da24828792..1495744f94f 100644 --- a/src/agents/workspace-templates.test.ts +++ b/src/agents/workspace-templates.test.ts @@ -51,4 +51,27 @@ describe("resolveWorkspaceTemplateDir", () => { const resolved = await resolveWorkspaceTemplateDir({ cwd: distDir, moduleUrl }); expect(path.normalize(resolved)).toBe(path.resolve("docs", "reference", "templates")); }); + + it("resolves via module-relative ../docs fallback when packageRoot is null and cwd lacks templates", async () => { + // Simulates the bundled dist/ layout where ../../ overshoots but ../ is correct. + // When resolveOpenClawPackageRoot returns null (no package.json in ancestors), + // the ../docs/reference/templates fallback relative to the module should work. + const root = await makeTempRoot(); + // No package.json — packageRoot will be null + + const distDir = path.join(root, "dist"); + await fs.mkdir(distDir, { recursive: true }); + + // Create templates at ../docs (one level up from dist/) — the bundled layout + const templatesDir = path.join(root, "docs", "reference", "templates"); + await fs.mkdir(templatesDir, { recursive: true }); + await fs.writeFile(path.join(templatesDir, "AGENTS.md"), "# ok\n"); + + const moduleUrl = pathToFileURL(path.join(distDir, "workspace-XXXX.mjs")).toString(); + // Use a cwd that does NOT have docs/reference/templates + const fakeCwd = await makeTempRoot(); + + const resolved = await resolveWorkspaceTemplateDir({ cwd: fakeCwd, moduleUrl }); + expect(resolved).toBe(templatesDir); + }); }); diff --git a/src/agents/workspace-templates.ts b/src/agents/workspace-templates.ts index 11d733fa92c..65b7786ed7c 100644 --- a/src/agents/workspace-templates.ts +++ b/src/agents/workspace-templates.ts @@ -3,10 +3,23 @@ import { fileURLToPath } from "node:url"; import { resolveOpenClawPackageRoot } from "../infra/openclaw-root.js"; import { pathExists } from "../utils.js"; -const FALLBACK_TEMPLATE_DIR = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - "../../docs/reference/templates", -); +/** + * Compute fallback template directories relative to the current module. + * + * In the source tree the module lives at `src/agents/workspace-templates.ts`, + * so `../../docs/reference/templates` resolves correctly to the repo root. + * + * After bundling, all files land in `dist/` (one level deep inside the + * package root), so `../../docs/reference/templates` overshoots by one + * directory (lands in node_modules/ instead of the package). We include + * both `../../` (source) and `../` (bundled dist) as fallback candidates + * to handle either layout. + */ +const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url)); +const FALLBACK_TEMPLATE_DIRS = [ + path.resolve(MODULE_DIR, "../../docs/reference/templates"), // source layout (src/agents/) + path.resolve(MODULE_DIR, "../docs/reference/templates"), // bundled layout (dist/) +]; let cachedTemplateDir: string | undefined; let resolvingTemplateDir: Promise | undefined; @@ -30,9 +43,12 @@ export async function resolveWorkspaceTemplateDir(opts?: { const packageRoot = await resolveOpenClawPackageRoot({ moduleUrl, argv1, cwd }); const candidates = [ + // Preferred: resolved package root (most reliable) packageRoot ? path.join(packageRoot, "docs", "reference", "templates") : null, + // Fallback: relative to module file (handles both source and bundled layouts) + ...FALLBACK_TEMPLATE_DIRS, + // Last resort: relative to cwd (only useful if running from repo checkout) cwd ? path.resolve(cwd, "docs", "reference", "templates") : null, - FALLBACK_TEMPLATE_DIR, ].filter(Boolean) as string[]; for (const candidate of candidates) { @@ -42,7 +58,9 @@ export async function resolveWorkspaceTemplateDir(opts?: { } } - cachedTemplateDir = candidates[0] ?? FALLBACK_TEMPLATE_DIR; + // No candidate exists — return the package-root-based path (or first + // fallback) so the downstream error message shows a meaningful path. + cachedTemplateDir = candidates[0] ?? FALLBACK_TEMPLATE_DIRS[0]; return cachedTemplateDir; })(); From d4dcd583d2cbbe327b3128c67a374fd56b636cb8 Mon Sep 17 00:00:00 2001 From: Topanga Ludwitt Date: Wed, 11 Mar 2026 15:27:58 -0700 Subject: [PATCH 2/2] fix: make fallback template dirs use opts.moduleUrl for testability Greptile correctly identified that FALLBACK_TEMPLATE_DIRS was a module-level constant computed from import.meta.url at load time, making it impossible to test the bundled-layout fallback path via the moduleUrl option. Refactor: replace the module-level constant with a function computeFallbackTemplateDirs(moduleUrl) that is called inside the resolver, using the same moduleUrl that opts can override. This makes the new test actually exercise the ../docs fallback code path. --- src/agents/workspace-templates.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/agents/workspace-templates.ts b/src/agents/workspace-templates.ts index 65b7786ed7c..d5032280523 100644 --- a/src/agents/workspace-templates.ts +++ b/src/agents/workspace-templates.ts @@ -4,7 +4,7 @@ import { resolveOpenClawPackageRoot } from "../infra/openclaw-root.js"; import { pathExists } from "../utils.js"; /** - * Compute fallback template directories relative to the current module. + * Compute fallback template directory candidates relative to a module URL. * * In the source tree the module lives at `src/agents/workspace-templates.ts`, * so `../../docs/reference/templates` resolves correctly to the repo root. @@ -15,11 +15,13 @@ import { pathExists } from "../utils.js"; * both `../../` (source) and `../` (bundled dist) as fallback candidates * to handle either layout. */ -const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url)); -const FALLBACK_TEMPLATE_DIRS = [ - path.resolve(MODULE_DIR, "../../docs/reference/templates"), // source layout (src/agents/) - path.resolve(MODULE_DIR, "../docs/reference/templates"), // bundled layout (dist/) -]; +function computeFallbackTemplateDirs(moduleUrl: string): string[] { + const moduleDir = path.dirname(fileURLToPath(moduleUrl)); + return [ + path.resolve(moduleDir, "../../docs/reference/templates"), // source layout (src/agents/) + path.resolve(moduleDir, "../docs/reference/templates"), // bundled layout (dist/) + ]; +} let cachedTemplateDir: string | undefined; let resolvingTemplateDir: Promise | undefined; @@ -42,11 +44,12 @@ export async function resolveWorkspaceTemplateDir(opts?: { const cwd = opts?.cwd ?? process.cwd(); const packageRoot = await resolveOpenClawPackageRoot({ moduleUrl, argv1, cwd }); + const fallbackDirs = computeFallbackTemplateDirs(moduleUrl); const candidates = [ // Preferred: resolved package root (most reliable) packageRoot ? path.join(packageRoot, "docs", "reference", "templates") : null, // Fallback: relative to module file (handles both source and bundled layouts) - ...FALLBACK_TEMPLATE_DIRS, + ...fallbackDirs, // Last resort: relative to cwd (only useful if running from repo checkout) cwd ? path.resolve(cwd, "docs", "reference", "templates") : null, ].filter(Boolean) as string[]; @@ -60,7 +63,7 @@ export async function resolveWorkspaceTemplateDir(opts?: { // No candidate exists — return the package-root-based path (or first // fallback) so the downstream error message shows a meaningful path. - cachedTemplateDir = candidates[0] ?? FALLBACK_TEMPLATE_DIRS[0]; + cachedTemplateDir = candidates[0] ?? fallbackDirs[0]; return cachedTemplateDir; })();