diff --git a/extensions/codex/src/app-server/protocol.ts b/extensions/codex/src/app-server/protocol.ts index 3f101ec686d9..00047cfd23ed 100644 --- a/extensions/codex/src/app-server/protocol.ts +++ b/extensions/codex/src/app-server/protocol.ts @@ -365,6 +365,7 @@ type CodexTurnInterruptParams = JsonObject & { export type CodexTurnStartParams = JsonObject & { threadId: string; input: CodexUserInput[]; + additionalContext?: Record; cwd?: string; model?: string; approvalPolicy?: CodexApprovalPolicy | null; diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index f2634e8d0250..6e6db95d8d06 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -5016,6 +5016,90 @@ describe("runCodexAppServerAttempt", () => { const resumeRequestParams = resumeRequest?.params as Record | undefined; expect(resumeRequestParams?.developerInstructions).not.toContain(CODEX_GPT5_BEHAVIOR_CONTRACT); }); + it("sends the current recorded sender on successive turns of one resumed Codex thread", async () => { + const { sessionFile, workspaceDir } = createRunPaths(); + await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); + const turnIds = ["turn-ada", "turn-grace"] as const; + let nextTurnIndex = 0; + const harness = createAppServerHarness(async (method, params) => { + if (method === "thread/resume") { + return threadStartResult((params as { threadId?: string }).threadId ?? "thread-existing"); + } + if (method === "turn/start") { + const turnId = turnIds[nextTurnIndex++]; + if (!turnId) { + throw new Error("unexpected extra turn/start"); + } + return turnStartResult(turnId); + } + return {}; + }); + + const runTurn = async (sender: { id: string; name: string }, prompt: string, runId: string) => { + const expectedTurnStarts = + harness.requests.filter((request) => request.method === "turn/start").length + 1; + const params = createParams(sessionFile, workspaceDir, { prompt, runId }); + params.trigger = "user"; + const message = { + role: "user" as const, + content: prompt, + timestamp: Date.now(), + __openclaw: { senderId: sender.id, senderName: sender.name }, + }; + params.userTurnTranscriptRecorder = { + message, + resolveMessage: async () => message, + getAdmissionReceipt: () => undefined, + markRuntimePersistencePending() {}, + markRuntimePersisted() {}, + } as unknown as EmbeddedRunAttemptParams["userTurnTranscriptRecorder"]; + const run = runCodexAppServerAttempt(params); + await vi.waitFor( + () => + expect( + harness.requests.filter((request) => request.method === "turn/start"), + ).toHaveLength(expectedTurnStarts), + fastWait, + ); + await harness.completeTurn({ + threadId: "thread-existing", + turnId: `turn-${sender.name.toLowerCase()}`, + }); + await run; + }; + + await runTurn({ id: "profile-ada", name: "Ada" }, "first request", "run-ada"); + await runTurn({ id: "profile-grace", name: "Grace" }, "second request", "run-grace"); + + expect( + harness.requests + .filter((request) => + ["thread/start", "thread/resume", "turn/start"].includes(request.method), + ) + .map((request) => request.method), + ).toEqual(["thread/resume", "turn/start", "turn/start"]); + expect( + harness.requests + .filter((request) => request.method === "turn/start") + .map( + (request) => + ( + request.params as { + additionalContext?: Record; + } + ).additionalContext?.openclaw_current_sender, + ), + ).toEqual([ + { + kind: "untrusted", + value: '{"sender":{"id":"profile-ada","name":"Ada"}}', + }, + { + kind: "untrusted", + value: '{"sender":{"id":"profile-grace","name":"Grace"}}', + }, + ]); + }); it("starts a fresh Codex thread before resume when the native rollout reaches the fallback fuse", async () => { const { sessionFile, workspaceDir, agentDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); diff --git a/extensions/codex/src/app-server/turn-params.ts b/extensions/codex/src/app-server/turn-params.ts index 82d1f6aee6e9..fad63c624700 100644 --- a/extensions/codex/src/app-server/turn-params.ts +++ b/extensions/codex/src/app-server/turn-params.ts @@ -1,5 +1,10 @@ import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime"; import { GPT5_HEARTBEAT_PROMPT_OVERLAY as CODEX_GPT5_HEARTBEAT_PROMPT_OVERLAY } from "openclaw/plugin-sdk/provider-model-shared"; +import { + asOptionalRecord, + normalizeOptionalString, +} from "openclaw/plugin-sdk/string-coerce-runtime"; +import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { codexSandboxPolicyForTurn, type CodexAppServerRuntimeOptions } from "./config.js"; import type { CodexSandboxPolicy, @@ -14,6 +19,37 @@ import { } from "./thread-model-selection.js"; import { buildCodexUserInput } from "./user-input.js"; +const CODEX_CURRENT_SENDER_FIELD_MAX_CHARS = 256; + +function buildCodexCurrentSenderContextValue(params: EmbeddedRunAttemptParams): string | undefined { + const metadata = asOptionalRecord( + asOptionalRecord(params.userTurnTranscriptRecorder?.message as unknown)?.["__openclaw"], + ); + const recorded = [ + normalizeOptionalString(metadata?.["senderId"]), + normalizeOptionalString(metadata?.["senderName"]), + normalizeOptionalString(metadata?.["senderUsername"]), + ] as const; + const [id, name, username] = recorded.some(Boolean) + ? recorded + : [ + normalizeOptionalString(params.senderId), + normalizeOptionalString(params.senderName), + normalizeOptionalString(params.senderUsername), + ]; + if (!id && !name && !username) { + return undefined; + } + const bound = (value: string) => truncateUtf16Safe(value, CODEX_CURRENT_SENDER_FIELD_MAX_CHARS); + return JSON.stringify({ + sender: { + ...(id ? { id: bound(id) } : {}), + ...(name ? { name: bound(name) } : {}), + ...(username ? { username: bound(username) } : {}), + }, + }); +} + export function buildTurnStartParams( params: EmbeddedRunAttemptParams, options: { @@ -43,9 +79,16 @@ export function buildTurnStartParams( config: params.config, }); const useThreadPermissionProfile = options.appServer.networkProxy && !options.sandboxPolicy; + const currentSenderContext = + params.trigger === "user" ? buildCodexCurrentSenderContextValue(params) : undefined; + // Untrusted context exposes authenticated attribution without promoting human-controlled labels. + const additionalContext: CodexTurnStartParams["additionalContext"] = currentSenderContext + ? { openclaw_current_sender: { kind: "untrusted", value: currentSenderContext } } + : undefined; return { threadId: options.threadId, input: buildCodexUserInput(options.promptText ?? params.prompt, params.images), + ...(additionalContext ? { additionalContext } : {}), cwd: options.cwd, approvalPolicy: options.appServer.approvalPolicy, approvalsReviewer: options.appServer.approvalsReviewer, diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md index 58e6337dae24..1436749602ee 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md @@ -132,6 +132,12 @@ ```json { + "additionalContext": { + "openclaw_current_sender": { + "kind": "untrusted", + "value": "{\"sender\":{\"id\":\"424242\",\"name\":\"Pash\",\"username\":\"pash\"}}" + } + }, "approvalPolicy": "never", "approvalsReviewer": "user", "collaborationMode": { diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md index 551ae85b77e8..cec3d2a001c7 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md @@ -132,6 +132,12 @@ ```json { + "additionalContext": { + "openclaw_current_sender": { + "kind": "untrusted", + "value": "{\"sender\":{\"id\":\"1000001\",\"name\":\"Pash\",\"username\":\"pash\"}}" + } + }, "approvalPolicy": "never", "approvalsReviewer": "user", "collaborationMode": {