diff --git a/CHANGELOG.md b/CHANGELOG.md index 23f4f657d58..0a43fa35035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai - CLI/gateway: keep `gateway status --deep` plugin-aware so configured plugin manifest warnings, including missing channel config metadata, stay visible during install and update smoke checks. - Doctor: stop flagging the live compatibility agent directory as orphaned when the configured default agent is not `main`. Fixes #74313. (#74438) Thanks @carlos4s. - Auth/Claude CLI: persist fresher managed external CLI OAuth credentials back to `auth-profiles.json`, preventing stale `anthropic:claude-cli` profiles from repeatedly bootstrapping and flooding debug logs. Fixes #80129. Thanks @Caulderein. +- Context: render `/context map` only from actual run context and persist Codex app-server run reports without counting deferred tool-search schemas as prompt-loaded tool schemas. - Codex app-server: report Codex-native tool execution to diagnostics so long-running native `bash`, web, file, and MCP tools no longer look like stale embedded runs to the watchdog. (#80217) - Telegram: preserve blank lines between manually indented bullet blocks and following numbered sections in rendered replies. Fixes #76998. Thanks @evgyur. - Slack: pass configured agent identity through draft preview sends so partial streaming replies keep custom username/avatar on the initial Slack message. Fixes #38235. (#38237) Thanks @lacymorrow. diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index d5119c06d2e..a7f4f0d846d 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -740,6 +740,41 @@ describe("runCodexAppServerAttempt", () => { expect(heartbeat?.deferLoading).toBe(true); }); + it("returns a run context report without deferred Codex dynamic tool schemas", async () => { + __testing.setOpenClawCodingToolsFactoryForTests(() => [ + createRuntimeDynamicTool("message"), + createRuntimeDynamicTool("web_search"), + ]); + const harness = createStartedThreadHarness(); + const params = createParams( + path.join(tempDir, "session.jsonl"), + path.join(tempDir, "workspace"), + ); + params.disableTools = false; + params.runtimePlan = createCodexRuntimePlanFixture(); + params.sourceReplyDeliveryMode = "message_tool_only"; + params.toolsAllow = ["message", "web_search"]; + + const run = runCodexAppServerAttempt(params, { + pluginConfig: { appServer: { mode: "yolo" } }, + }); + await harness.waitForMethod("turn/start", 120_000); + await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); + const result = await run; + + const report = result.systemPromptReport; + expect(report?.source).toBe("run"); + expect(report?.provider).toBe("codex"); + expect(report?.model).toBe("gpt-5.4-codex"); + expect(report?.systemPrompt.chars).toBeGreaterThan(0); + + const message = report?.tools.entries.find((tool) => tool.name === "message"); + const webSearch = report?.tools.entries.find((tool) => tool.name === "web_search"); + expect(message?.schemaChars).toBeGreaterThan(0); + expect(webSearch?.schemaChars).toBe(0); + expect(report?.tools.schemaChars).toBe(message?.schemaChars); + }); + it("keeps searchable Codex dynamic tools canonical in mirrored transcript snapshots", async () => { __testing.setOpenClawCodingToolsFactoryForTests(() => [ createRuntimeDynamicTool("wiki_status"), diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index d069b8293fe..e10ada0d2bd 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -95,6 +95,7 @@ import { type CodexUserInput, isJsonObject, type CodexServerNotification, + type CodexDynamicToolSpec, type CodexDynamicToolCallParams, type CodexDynamicToolCallResponse, type CodexTurnStartResponse, @@ -158,6 +159,11 @@ type OpenClawCodingToolsOptions = NonNullable< >; type OpenClawCodingToolsFactory = (typeof import("openclaw/plugin-sdk/agent-harness"))["createOpenClawCodingTools"]; +type CodexBootstrapContext = Awaited>; +type CodexBootstrapFile = CodexBootstrapContext["bootstrapFiles"][number]; +type CodexSystemPromptReport = NonNullable; +type CodexToolReportEntry = CodexSystemPromptReport["tools"]["entries"][number]; +type CodexWorkspaceBootstrapContext = CodexBootstrapContext & { instructions?: string }; const testClientFactoryStorage = new AsyncLocalStorage(); const clientFactory = defaultCodexAppServerClientFactory; @@ -571,13 +577,14 @@ export async function runCodexAppServerAttempt( // Build the workspace bootstrap block before finalizing developer // instructions so persona files (SOUL.md, IDENTITY.md, ...) reach Codex // through the explicit `developerInstructions` field. - const workspaceBootstrapInstructions = await buildCodexWorkspaceBootstrapInstructions({ + const workspaceBootstrapContext = await buildCodexWorkspaceBootstrapContext({ params, resolvedWorkspace, effectiveWorkspace, sessionKey: sandboxSessionKey, sessionAgentId, }); + const workspaceBootstrapInstructions = workspaceBootstrapContext.instructions; let promptText = params.prompt; let developerInstructions = joinPresentSections( baseDeveloperInstructions, @@ -639,6 +646,14 @@ export async function runCodexAppServerAttempt( messages: historyMessages, ctx: hookContext, }); + const systemPromptReport = buildCodexSystemPromptReport({ + attempt: params, + sessionKey: sandboxSessionKey, + workspaceDir: effectiveWorkspace, + developerInstructions: promptBuild.developerInstructions, + workspaceBootstrapContext, + tools: toolBridge.specs, + }); const trajectoryRecorder = createCodexTrajectoryRecorder({ attempt: params, cwd: effectiveWorkspace, @@ -1528,6 +1543,7 @@ export async function runCodexAppServerAttempt( aborted: finalAborted, promptError: finalPromptError, promptErrorSource: finalPromptErrorSource, + systemPromptReport, }; } finally { emitLifecycleTerminal({ @@ -2195,15 +2211,15 @@ async function readMirroredSessionHistoryMessages( return messages; } -async function buildCodexWorkspaceBootstrapInstructions(params: { +async function buildCodexWorkspaceBootstrapContext(params: { params: EmbeddedRunAttemptParams; resolvedWorkspace: string; effectiveWorkspace: string; sessionKey: string; sessionAgentId: string; -}): Promise { +}): Promise { try { - const { contextFiles } = await resolveBootstrapContextForRun({ + const bootstrapContext = await resolveBootstrapContextForRun({ workspaceDir: params.resolvedWorkspace, config: params.params.config, sessionKey: params.sessionKey, @@ -2213,21 +2229,158 @@ async function buildCodexWorkspaceBootstrapInstructions(params: { contextMode: params.params.bootstrapContextMode, runKind: params.params.bootstrapContextRunKind, }); - return renderCodexWorkspaceBootstrapInstructions( - contextFiles.map((file) => - remapCodexContextFilePath({ - file, - sourceWorkspaceDir: params.resolvedWorkspace, - targetWorkspaceDir: params.effectiveWorkspace, - }), - ), + const contextFiles = bootstrapContext.contextFiles.map((file) => + remapCodexContextFilePath({ + file, + sourceWorkspaceDir: params.resolvedWorkspace, + targetWorkspaceDir: params.effectiveWorkspace, + }), ); + return { + ...bootstrapContext, + contextFiles, + instructions: renderCodexWorkspaceBootstrapInstructions(contextFiles), + }; } catch (error) { embeddedAgentLog.warn("failed to load codex workspace bootstrap instructions", { error }); - return undefined; + return { bootstrapFiles: [], contextFiles: [] }; } } +function buildCodexSystemPromptReport(params: { + attempt: EmbeddedRunAttemptParams; + sessionKey: string; + workspaceDir: string; + developerInstructions: string; + workspaceBootstrapContext: CodexWorkspaceBootstrapContext; + tools: CodexDynamicToolSpec[]; +}): CodexSystemPromptReport { + const toolEntries = params.tools.map(buildCodexToolReportEntry); + const schemaChars = toolEntries.reduce((sum, tool) => sum + tool.schemaChars, 0); + const projectContextChars = params.workspaceBootstrapContext.instructions?.length ?? 0; + const bootstrapMaxChars = readPositiveNumber( + params.attempt.config?.agents?.defaults?.bootstrapMaxChars, + ); + const bootstrapTotalMaxChars = readPositiveNumber( + params.attempt.config?.agents?.defaults?.bootstrapTotalMaxChars, + ); + return { + source: "run", + generatedAt: Date.now(), + sessionId: params.attempt.sessionId, + sessionKey: params.sessionKey, + provider: params.attempt.provider, + model: params.attempt.modelId, + workspaceDir: params.workspaceDir, + ...(bootstrapMaxChars ? { bootstrapMaxChars } : {}), + ...(bootstrapTotalMaxChars ? { bootstrapTotalMaxChars } : {}), + systemPrompt: { + chars: params.developerInstructions.length, + projectContextChars, + nonProjectContextChars: Math.max( + 0, + params.developerInstructions.length - projectContextChars, + ), + }, + injectedWorkspaceFiles: buildCodexBootstrapInjectionStats({ + bootstrapFiles: params.workspaceBootstrapContext.bootstrapFiles, + injectedFiles: params.workspaceBootstrapContext.contextFiles, + }), + skills: { + promptChars: 0, + entries: [], + }, + tools: { + listChars: 0, + schemaChars, + entries: toolEntries, + }, + }; +} + +function buildCodexToolReportEntry(tool: CodexDynamicToolSpec): CodexToolReportEntry { + const summary = tool.description.trim(); + if (tool.deferLoading === true) { + return { + name: tool.name, + summaryChars: summary.length, + schemaChars: 0, + propertiesCount: null, + }; + } + return { + name: tool.name, + summaryChars: summary.length, + ...buildCodexToolSchemaStats(tool.inputSchema), + }; +} + +function buildCodexToolSchemaStats( + schema: JsonValue, +): Pick { + const schemaChars = (() => { + try { + return JSON.stringify(schema).length; + } catch { + return 0; + } + })(); + const properties = + isJsonObject(schema) && isJsonObject(schema.properties) ? schema.properties : null; + return { + schemaChars, + propertiesCount: properties ? Object.keys(properties).length : null, + }; +} + +function buildCodexBootstrapInjectionStats(params: { + bootstrapFiles: CodexBootstrapFile[]; + injectedFiles: EmbeddedContextFile[]; +}): CodexSystemPromptReport["injectedWorkspaceFiles"] { + const injectedByPath = new Map(); + const injectedByBaseName = new Map(); + for (const file of params.injectedFiles) { + const pathValue = readNonEmptyString(file.path); + if (!pathValue) { + continue; + } + if (!injectedByPath.has(pathValue)) { + injectedByPath.set(pathValue, file.content); + } + const baseName = path.posix.basename(pathValue.replaceAll("\\", "/")); + if (!injectedByBaseName.has(baseName)) { + injectedByBaseName.set(baseName, file.content); + } + } + return params.bootstrapFiles.map((file) => { + const pathValue = readNonEmptyString(file.path) ?? file.name; + const rawChars = file.missing ? 0 : (file.content ?? "").trimEnd().length; + const injected = + injectedByPath.get(pathValue) ?? + injectedByPath.get(file.name) ?? + injectedByBaseName.get(file.name); + const injectedChars = injected?.length ?? 0; + return { + name: file.name, + path: pathValue, + missing: file.missing, + rawChars, + injectedChars, + truncated: !file.missing && injectedChars < rawChars, + }; + }); +} + +function readPositiveNumber(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) && value > 0 + ? Math.floor(value) + : undefined; +} + +function readNonEmptyString(value: unknown): string | undefined { + return typeof value === "string" && value.trim().length > 0 ? value : undefined; +} + function renderCodexWorkspaceBootstrapInstructions( contextFiles: EmbeddedContextFile[], ): string | undefined { diff --git a/src/auto-reply/reply/commands-context-report.test.ts b/src/auto-reply/reply/commands-context-report.test.ts index 0df2c9709b1..92c1ed1952f 100644 --- a/src/auto-reply/reply/commands-context-report.test.ts +++ b/src/auto-reply/reply/commands-context-report.test.ts @@ -182,4 +182,26 @@ describe("buildContextReply", () => { await unlink(result.mediaUrl); } }); + + it("does not render context map from an estimated report", async () => { + const params = makeParams("/context map", false); + const report = params.sessionEntry?.systemPromptReport; + if (!report) { + throw new Error("missing context report"); + } + params.sessionEntry = { + ...params.sessionEntry, + systemPromptReport: { + ...report, + source: "estimate", + }, + } as SessionEntry; + + const result = await buildContextReply(params); + + expect(result.text).toContain("Context treemap unavailable."); + expect(result.text).toContain("No actual run context is cached for this session yet."); + expect(result.text).not.toContain("Source: estimate"); + expect(result.mediaUrl).toBeUndefined(); + }); }); diff --git a/src/auto-reply/reply/commands-context-report.ts b/src/auto-reply/reply/commands-context-report.ts index 3b770fb3ec6..2c3dc882b0a 100644 --- a/src/auto-reply/reply/commands-context-report.ts +++ b/src/auto-reply/reply/commands-context-report.ts @@ -43,15 +43,21 @@ function formatListTop( return { lines, omitted }; } +function resolveRunContextReport(params: HandleCommandsParams): SessionSystemPromptReport | null { + const targetSessionEntry = params.sessionStore?.[params.sessionKey] ?? params.sessionEntry; + const existing = targetSessionEntry?.systemPromptReport; + return existing?.source === "run" ? existing : null; +} + async function resolveContextReport( params: HandleCommandsParams, ): Promise { - const targetSessionEntry = params.sessionStore?.[params.sessionKey] ?? params.sessionEntry; - const existing = targetSessionEntry?.systemPromptReport; - if (existing && existing.source === "run") { - return existing; + const runReport = resolveRunContextReport(params); + if (runReport) { + return runReport; } + const targetSessionEntry = params.sessionStore?.[params.sessionKey] ?? params.sessionEntry; const bootstrapMaxChars = resolveBootstrapMaxChars(params.cfg); const bootstrapTotalMaxChars = resolveBootstrapTotalMaxChars(params.cfg); const { resolveCommandsSystemPromptBundle } = await import("./commands-system-prompt.js"); @@ -100,7 +106,6 @@ export async function buildContextReply(params: HandleCommandsParams): Promise