Skip to content

Commit c4f14a3

Browse files
IWhatsskillclawsweeper[bot]Takhoffman
authored
fix(codex): guard path-only bootstrap files [AI-assisted] (#84736)
Summary: - The PR updates Codex app-server system-prompt reporting to tolerate bootstrap files with `path` and `content` but no `name`, adds a focused regression test, and records the fix in the changelog. - Reproducibility: yes. The PR body supplies current-main before output with the `undefined.trim()` stack, and source inspection confirms hook-supplied path-only bootstrap files can reach the Codex report helper. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(codex): guard path-only bootstrap files [AI-assisted] Validation: - ClawSweeper review passed for head 4667110. - Required merge gates passed before the squash merge. Prepared head SHA: 4667110 Review: #84736 (comment) Co-authored-by: JARVIS-Glasses <whatsskilll@gmail.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
1 parent 9cdf8a1 commit c4f14a3

3 files changed

Lines changed: 66 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
1717

1818
### Fixes
1919

20+
- Codex app-server: keep system-prompt reports working when bootstrap hooks provide workspace files with only a path and content, so hook-supplied SOUL/IDENTITY/TOOLS/USER context still reports injected characters correctly. (#84736) Thanks @JARVIS-Glasses.
2021
- WhatsApp: update Baileys to `7.0.0-rc12`.
2122
- Build: suppress per-locale `rolldown-plugin-dts:fake-js` CommonJS dts warnings emitted while bundling the intentionally-inlined `zod/v4/locales/*.d.cts` files, so `pnpm build` output stays readable after the 0.25.1 plugin bump. Thanks @romneyda.
2223
- CLI/nodes: route lazy plugin-registration logs to stderr for JSON-mode `openclaw nodes` commands so stdout stays parseable. (#84684) Thanks @TurboTheTurtle.

extensions/codex/src/app-server/run-attempt.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import {
2222
type DiagnosticEventPayload,
2323
} from "openclaw/plugin-sdk/diagnostic-runtime";
2424
import {
25+
clearInternalHooks,
2526
initializeGlobalHookRunner,
27+
registerInternalHook,
2628
resetGlobalHookRunner,
2729
} from "openclaw/plugin-sdk/hook-runtime";
2830
import { clearPluginCommands, registerPluginCommand } from "openclaw/plugin-sdk/plugin-runtime";
@@ -646,6 +648,7 @@ function extractRelayIdFromThreadRequest(params: unknown): string {
646648

647649
describe("runCodexAppServerAttempt", () => {
648650
beforeEach(async () => {
651+
clearInternalHooks();
649652
resetAgentEventsForTest();
650653
resetDiagnosticEventsForTest();
651654
vi.stubEnv("OPENCLAW_TRAJECTORY", "0");
@@ -663,6 +666,7 @@ describe("runCodexAppServerAttempt", () => {
663666
resetAgentEventsForTest();
664667
resetDiagnosticEventsForTest();
665668
resetGlobalHookRunner();
669+
clearInternalHooks();
666670
defaultCodexAppInventoryCache.clear();
667671
vi.useRealTimers();
668672
vi.restoreAllMocks();
@@ -4825,6 +4829,43 @@ describe("runCodexAppServerAttempt", () => {
48254829
});
48264830
});
48274831

4832+
it("reports hook-supplied bootstrap files that only expose path and content", async () => {
4833+
const sessionFile = path.join(tempDir, "session.jsonl");
4834+
const workspaceDir = path.join(tempDir, "workspace");
4835+
const soulPath = path.join(workspaceDir, "SOUL.md");
4836+
const soulGuidance = "Hook supplied soul guidance.";
4837+
await fs.mkdir(workspaceDir, { recursive: true });
4838+
registerInternalHook("agent:bootstrap", (event) => {
4839+
const context = event.context as {
4840+
bootstrapFiles: Array<{ content: string; missing: boolean; path: string }>;
4841+
};
4842+
context.bootstrapFiles = [
4843+
{
4844+
path: soulPath,
4845+
content: soulGuidance,
4846+
missing: false,
4847+
},
4848+
];
4849+
});
4850+
const harness = createStartedThreadHarness();
4851+
4852+
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
4853+
await harness.waitForMethod("turn/start");
4854+
await new Promise<void>((resolve) => setImmediate(resolve));
4855+
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
4856+
const result = await run;
4857+
4858+
expect(result.systemPromptReport?.injectedWorkspaceFiles).toEqual([
4859+
expect.objectContaining({
4860+
name: "SOUL.md",
4861+
path: soulPath,
4862+
rawChars: soulGuidance.length,
4863+
injectedChars: soulGuidance.length,
4864+
truncated: false,
4865+
}),
4866+
]);
4867+
});
4868+
48284869
it("points heartbeat Codex turns at HEARTBEAT.md without injecting its contents", async () => {
48294870
const sessionFile = path.join(tempDir, "session.jsonl");
48304871
const workspaceDir = path.join(tempDir, "workspace");

extensions/codex/src/app-server/run-attempt.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4440,12 +4440,14 @@ function buildCodexBootstrapInjectionStats(params: {
44404440
params.developerInstructionFiles ?? [],
44414441
);
44424442
return params.bootstrapFiles.map((file) => {
4443-
const pathValue = readNonEmptyString(file.path) ?? file.name;
4444-
const baseName = getCodexContextFileBasename(pathValue || file.name);
4443+
const fileName = readNonEmptyString(file.name);
4444+
const pathValue = readNonEmptyString(file.path) ?? fileName ?? "";
4445+
const displayName = (fileName ?? getCodexContextFileDisplayBasename(pathValue)) || pathValue;
4446+
const baseName = getCodexContextFileBasename(pathValue || fileName || "");
44454447
const rawChars = file.missing ? 0 : (file.content ?? "").trimEnd().length;
44464448
const injected =
4447-
readCodexIndexedContextFileContent(injectedIndex, pathValue, file.name) ??
4448-
readCodexIndexedContextFileContent(developerInstructionIndex, pathValue, file.name);
4449+
readCodexIndexedContextFileContent(injectedIndex, pathValue, fileName) ??
4450+
readCodexIndexedContextFileContent(developerInstructionIndex, pathValue, fileName);
44494451
let injectedChars = injected?.length ?? 0;
44504452
let truncated = !file.missing && injectedChars < rawChars;
44514453
if (injected === undefined) {
@@ -4458,7 +4460,7 @@ function buildCodexBootstrapInjectionStats(params: {
44584460
}
44594461
}
44604462
return {
4461-
name: file.name,
4463+
name: displayName,
44624464
path: pathValue,
44634465
missing: file.missing,
44644466
rawChars,
@@ -4493,13 +4495,20 @@ function indexCodexContextFileContent(files: EmbeddedContextFile[]): {
44934495
function readCodexIndexedContextFileContent(
44944496
index: { byPath: Map<string, string>; byBaseName: Map<string, string> },
44954497
pathValue: string,
4496-
fileName: string,
4498+
fileName: string | undefined,
44974499
): string | undefined {
4498-
return (
4499-
index.byPath.get(pathValue) ??
4500-
index.byPath.get(fileName) ??
4501-
index.byBaseName.get(getCodexContextFileBasename(fileName))
4502-
);
4500+
const pathContent = index.byPath.get(pathValue);
4501+
if (pathContent !== undefined) {
4502+
return pathContent;
4503+
}
4504+
if (fileName) {
4505+
const nameContent = index.byPath.get(fileName);
4506+
if (nameContent !== undefined) {
4507+
return nameContent;
4508+
}
4509+
}
4510+
const baseName = getCodexContextFileBasename(fileName ?? pathValue);
4511+
return baseName ? index.byBaseName.get(baseName) : undefined;
45034512
}
45044513

45054514
function readPositiveNumber(value: unknown): number | undefined {
@@ -4705,6 +4714,10 @@ function normalizeCodexContextFilePath(filePath: string): string {
47054714
return filePath.trim().replaceAll("\\", "/").toLowerCase();
47064715
}
47074716

4717+
function getCodexContextFileDisplayBasename(filePath: string): string {
4718+
return filePath.trim().replaceAll("\\", "/").split("/").pop()?.trim() ?? "";
4719+
}
4720+
47084721
function getCodexContextFileBasename(filePath: string): string {
47094722
return normalizeCodexContextFilePath(filePath).split("/").pop() ?? "";
47104723
}

0 commit comments

Comments
 (0)