diff --git a/extensions/codex/src/app-server/dynamic-tools.test.ts b/extensions/codex/src/app-server/dynamic-tools.test.ts index 944351fed52..4126bdfc26a 100644 --- a/extensions/codex/src/app-server/dynamic-tools.test.ts +++ b/extensions/codex/src/app-server/dynamic-tools.test.ts @@ -266,6 +266,55 @@ describe("createCodexDynamicToolBridge", () => { expect(text).toContain("rerun with narrower args"); }); + it("honors normalized per-agent dynamic tool result caps", async () => { + const bridge = createCodexDynamicToolBridge({ + tools: [ + createTool({ + name: "large_lookup", + execute: vi.fn(async () => textToolResult("x".repeat(400))), + }), + ], + signal: new AbortController().signal, + hookContext: { + agentId: "research-bot", + config: { + agents: { + defaults: { + contextLimits: { + toolResultMaxChars: 1_000, + }, + }, + list: [ + { + id: "Research Bot", + contextLimits: { + toolResultMaxChars: 180, + }, + }, + ], + }, + } as never, + }, + }); + + const result = await bridge.handleToolCall({ + threadId: "thread-1", + turnId: "turn-1", + callId: "call-1", + namespace: null, + tool: "large_lookup", + arguments: {}, + }); + + expect(result.success).toBe(true); + const firstItem = result.contentItems[0]; + if (firstItem?.type !== "inputText" || typeof firstItem.text !== "string") { + throw new Error("expected inputText tool result"); + } + expect(firstItem.text.length).toBeLessThanOrEqual(180); + expect(firstItem.text).toContain("OpenClaw truncated dynamic tool result"); + }); + it("keeps truncation notices within tiny configured caps", async () => { const bridge = createCodexDynamicToolBridge({ tools: [ diff --git a/extensions/codex/src/app-server/dynamic-tools.ts b/extensions/codex/src/app-server/dynamic-tools.ts index 9f7f8471bf1..149957202e0 100644 --- a/extensions/codex/src/app-server/dynamic-tools.ts +++ b/extensions/codex/src/app-server/dynamic-tools.ts @@ -18,6 +18,7 @@ import { type MessagingToolSourceReplyPayload, wrapToolWithBeforeToolCallHook, } from "openclaw/plugin-sdk/agent-harness-runtime"; +import { normalizeAgentId } from "openclaw/plugin-sdk/routing"; import type { CodexDynamicToolsLoading } from "./config.js"; import { invalidInlineImageText, sanitizeInlineImageDataUrl } from "./image-payload-sanitizer.js"; import { @@ -253,7 +254,11 @@ function resolveAgentContextLimitValue(params: { if (!Array.isArray(list)) { return defaultValue; } - const agent = list.find((entry) => readRecord(entry)?.id === params.agentId); + const normalizedAgentId = normalizeAgentId(params.agentId); + const agent = list.find((entry) => { + const entryId = readRecord(entry)?.id; + return typeof entryId === "string" && normalizeAgentId(entryId) === normalizedAgentId; + }); const agentValue = readPositiveInteger( readRecord(readRecord(agent)?.contextLimits)?.[params.key], ); diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index e76fdfdb061..a8f1b3f1fd2 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -6452,6 +6452,45 @@ describe("runCodexAppServerAttempt", () => { expect(savedBinding).toBeUndefined(); }); + it("clears native rollouts at the configured byte limit", async () => { + const sessionFile = path.join(tempDir, "session.jsonl"); + const workspaceDir = path.join(tempDir, "workspace"); + const agentDir = path.join(tempDir, "agent"); + await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); + await fs.writeFile( + path.join(path.dirname(sessionFile), "sessions.json"), + JSON.stringify({ + "agent:main:session-1": { + sessionFile, + totalTokens: 12_000, + }, + }), + ); + const rolloutDir = path.join(agentDir, "codex-home", "sessions"); + await fs.mkdir(rolloutDir, { recursive: true }); + await fs.writeFile(path.join(rolloutDir, "rollout-thread-existing.jsonl"), "x".repeat(1_000)); + + const binding = await __testing.rotateOversizedCodexAppServerStartupBinding({ + binding: await readCodexAppServerBinding(sessionFile), + sessionFile, + agentDir, + config: { + agents: { + defaults: { + compaction: { + truncateAfterCompaction: true, + maxActiveTranscriptBytes: 1_000, + }, + }, + }, + } as never, + }); + + expect(binding).toBeUndefined(); + const savedBinding = await readCodexAppServerBinding(sessionFile); + expect(savedBinding).toBeUndefined(); + }); + it("resumes a bound Codex thread when only dynamic tool descriptions change", async () => { const sessionFile = path.join(tempDir, "session.jsonl"); const workspaceDir = path.join(tempDir, "workspace"); diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index 1d3c89e1483..32846d45a7d 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -679,7 +679,7 @@ async function rotateOversizedCodexAppServerStartupBinding(params: { params.codexHome, ); if (maxBytes !== undefined) { - const oversizedFiles = rolloutFiles.filter((file) => file.bytes > maxBytes); + const oversizedFiles = rolloutFiles.filter((file) => file.bytes >= maxBytes); if (oversizedFiles.length > 0) { embeddedAgentLog.warn( "codex app-server native transcript exceeded active byte limit; starting a fresh thread",