Skip to content

Commit 7715b29

Browse files
authored
fix(codex): preserve inbound sender metadata
Summary: - Preserve inbound sender metadata and source-channel provenance in Codex app-server prompt mirrors. - Reuse the shared prompt-mirror builder for normal and `turn/start` failure snapshots. - Add regression coverage for provider variants such as `discord-voice` while keeping `sourceChannel` on the originating channel. Verification: - `pnpm test extensions/codex/src/app-server/event-projector.test.ts extensions/codex/src/app-server/run-attempt.test.ts` - `pnpm exec oxfmt --check extensions/codex/src/app-server/transcript-mirror.ts extensions/codex/src/app-server/event-projector.test.ts extensions/codex/src/app-server/run-attempt.test.ts` - `git diff --check temp/landpr-82184..HEAD` - `/Users/steipete/Projects/agent-scripts/skills/codex-review/scripts/codex-review --parallel-tests "pnpm test extensions/codex/src/app-server/event-projector.test.ts extensions/codex/src/app-server/run-attempt.test.ts"`
1 parent 011f98c commit 7715b29

6 files changed

Lines changed: 118 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
1919
- LINE: acknowledge signed webhook events before agent processing so slow model replies do not cause LINE `request_timeout` delivery failures. Fixes #65375. Thanks @myericho.
2020
- LINE: stop cron recovery from inferring lowercased LINE recipients from canonical session keys, so long-running task replies do not silently retry undeliverable push targets. Fixes #81628. (#81704) Thanks @edenfunf.
2121
- TTS: preserve channel-derived voice-note delivery for `/tts audio` replies even when the provider output is not natively voice-compatible. (#82174) Thanks @xuruiray.
22+
- Codex app-server: preserve inbound sender metadata and source-channel provenance on mirrored user prompts, including failure snapshots, so channel history keeps the original sender identity. (#82184) Thanks @zknicker.
2223
- Codex/Lossless: keep Codex explicit compaction on native app-server threads while allowing Lossless through the context-engine slot; `openclaw doctor --fix` now migrates legacy `compaction.provider: "lossless-claw"` config to `plugins.slots.contextEngine`.
2324
- Cron/doctor: report scheduled jobs with explicit `payload.model` overrides, including provider namespace counts and default-model mismatches, so stale cron model pins are visible during auth or billing investigations. Fixes #82151. Thanks @mgonto.
2425
- Codex app-server: keep the short turn-completion idle watchdog armed after the last non-assistant current-turn item completes, so a quiet Codex app-server releases the OpenClaw session lane before the outer attempt timeout. Fixes #82171. (#82172) Thanks @funmerlin.

extensions/codex/src/app-server/event-projector.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,37 @@ describe("CodexAppServerEventProjector", () => {
307307
expect(result.replayMetadata.replaySafe).toBe(true);
308308
});
309309

310+
it("preserves inbound sender metadata on the mirrored user prompt", async () => {
311+
const params = await createParams();
312+
const projector = await createProjector({
313+
...params,
314+
messageChannel: "discord",
315+
messageProvider: "discord-voice",
316+
senderId: "user-123",
317+
senderName: "Test User",
318+
senderUsername: "testuser",
319+
inputProvenance: {
320+
kind: "external_user",
321+
sourceChannel: "discord",
322+
},
323+
});
324+
325+
const result = projector.buildResult(buildEmptyToolTelemetry());
326+
327+
const userMessage = requireRecord(result.messagesSnapshot[0], "user message");
328+
expect(userMessage.role).toBe("user");
329+
expect(userMessage.content).toBe("hello");
330+
expect(userMessage.sourceChannel).toBe("discord");
331+
expect(userMessage.senderId).toBe("user-123");
332+
expect(userMessage.senderName).toBe("Test User");
333+
expect(userMessage.senderUsername).toBe("testuser");
334+
expect(userMessage.senderLabel).toBe("Test User (user-123)");
335+
expect(userMessage.provenance).toEqual({
336+
kind: "external_user",
337+
sourceChannel: "discord",
338+
});
339+
});
340+
310341
it("does not treat cumulative-only token usage as fresh context usage", async () => {
311342
const projector = await createProjector();
312343

extensions/codex/src/app-server/event-projector.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {
4040
sanitizeCodexAgentEventRecord,
4141
sanitizeCodexToolArguments,
4242
} from "./tool-progress-normalization.js";
43-
import { attachCodexMirrorIdentity } from "./transcript-mirror.js";
43+
import { attachCodexMirrorIdentity, buildCodexUserPromptMessage } from "./transcript-mirror.js";
4444

4545
export type CodexAppServerToolTelemetry = {
4646
didSendViaMessagingTool: boolean;
@@ -260,14 +260,7 @@ export class CodexAppServerEventProjector {
260260
// distinct turnIds → distinct identities → both kept.
261261
const turnId = this.turnId;
262262
const messagesSnapshot: AgentMessage[] = [
263-
attachCodexMirrorIdentity(
264-
{
265-
role: "user",
266-
content: this.params.prompt,
267-
timestamp: Date.now(),
268-
},
269-
`${turnId}:prompt`,
270-
),
263+
attachCodexMirrorIdentity(buildCodexUserPromptMessage(this.params), `${turnId}:prompt`),
271264
];
272265
// Codex owns the canonical thread. These mirror records keep enough local
273266
// context for OpenClaw history, search, and future harness switching.

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3496,6 +3496,15 @@ describe("runCodexAppServerAttempt", () => {
34963496

34973497
const params = createParams(sessionFile, workspaceDir);
34983498
params.runtimePlan = createCodexRuntimePlanFixture();
3499+
params.messageChannel = "discord";
3500+
params.messageProvider = "discord-voice";
3501+
params.senderId = "user-123";
3502+
params.senderName = "Test User";
3503+
params.senderUsername = "testuser";
3504+
params.inputProvenance = {
3505+
kind: "external_user",
3506+
sourceChannel: "discord",
3507+
};
34993508

35003509
await expect(runCodexAppServerAttempt(params)).rejects.toThrow("turn start exploded");
35013510

@@ -3528,7 +3537,31 @@ describe("runCodexAppServerAttempt", () => {
35283537
expect(agentEndPayload.success).toBe(false);
35293538
expect(agentEndPayload.error).toBe("turn start exploded");
35303539
expect(agentEndPayload.messages?.some((message) => message.role === "assistant")).toBe(true);
3531-
expect(agentEndPayload.messages?.some((message) => message.role === "user")).toBe(true);
3540+
const userMessage = agentEndPayload.messages?.find((message) => message.role === "user") as
3541+
| {
3542+
content?: unknown;
3543+
provenance?: unknown;
3544+
role?: string;
3545+
senderId?: unknown;
3546+
senderLabel?: unknown;
3547+
senderName?: unknown;
3548+
senderUsername?: unknown;
3549+
sourceChannel?: unknown;
3550+
}
3551+
| undefined;
3552+
expect(userMessage).toMatchObject({
3553+
role: "user",
3554+
content: "hello",
3555+
sourceChannel: "discord",
3556+
senderId: "user-123",
3557+
senderName: "Test User",
3558+
senderUsername: "testuser",
3559+
senderLabel: "Test User (user-123)",
3560+
provenance: {
3561+
kind: "external_user",
3562+
sourceChannel: "discord",
3563+
},
3564+
});
35323565
});
35333566

35343567
it("fires agent_end with success false when the codex turn is aborted", async () => {

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,10 @@ import {
140140
recordCodexTrajectoryCompletion,
141141
recordCodexTrajectoryContext,
142142
} from "./trajectory.js";
143-
import { mirrorCodexAppServerTranscript } from "./transcript-mirror.js";
143+
import {
144+
buildCodexUserPromptMessage,
145+
mirrorCodexAppServerTranscript,
146+
} from "./transcript-mirror.js";
144147
import { createCodexUserInputBridge } from "./user-input-bridge.js";
145148
import { filterToolsForVisionInputs } from "./vision-tools.js";
146149

@@ -1496,11 +1499,7 @@ export async function runCodexAppServerAttempt(
14961499
};
14971500
const turnStartFailureMessages = [
14981501
...historyMessages,
1499-
{
1500-
role: "user" as const,
1501-
content: promptBuild.prompt,
1502-
timestamp: Date.now(),
1503-
},
1502+
buildCodexUserPromptMessage({ ...params, prompt: promptBuild.prompt }),
15041503
];
15051504

15061505
let turn: CodexTurnStartResponse | undefined;

extensions/codex/src/app-server/transcript-mirror.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,58 @@ import {
77
resolveSessionWriteLockAcquireTimeoutMs,
88
runAgentHarnessBeforeMessageWriteHook,
99
type AgentMessage,
10+
type EmbeddedRunAttemptParams,
1011
type SessionWriteLockAcquireTimeoutConfig,
1112
} from "openclaw/plugin-sdk/agent-harness-runtime";
1213

1314
type MirroredAgentMessage = Extract<AgentMessage, { role: "user" | "assistant" | "toolResult" }>;
1415

1516
const MIRROR_IDENTITY_META_KEY = "mirrorIdentity" as const;
1617

18+
function normalizeOptionalString(value: string | null | undefined): string | undefined {
19+
const normalized = value?.trim();
20+
return normalized ? normalized : undefined;
21+
}
22+
23+
function buildSenderLabel(params: {
24+
senderId?: string;
25+
senderName?: string;
26+
senderUsername?: string;
27+
senderE164?: string;
28+
}): string | undefined {
29+
const label = params.senderName ?? params.senderUsername ?? params.senderE164 ?? params.senderId;
30+
if (!label) {
31+
return undefined;
32+
}
33+
if (!params.senderId || label.includes(params.senderId)) {
34+
return label;
35+
}
36+
return `${label} (${params.senderId})`;
37+
}
38+
39+
export function buildCodexUserPromptMessage(params: EmbeddedRunAttemptParams): AgentMessage {
40+
const senderId = normalizeOptionalString(params.senderId);
41+
const senderName = normalizeOptionalString(params.senderName);
42+
const senderUsername = normalizeOptionalString(params.senderUsername);
43+
const senderE164 = normalizeOptionalString(params.senderE164);
44+
const senderLabel = buildSenderLabel({ senderId, senderName, senderUsername, senderE164 });
45+
const sourceChannel = normalizeOptionalString(
46+
params.inputProvenance?.sourceChannel ?? params.messageChannel ?? params.messageProvider,
47+
);
48+
return {
49+
role: "user",
50+
content: params.prompt,
51+
timestamp: Date.now(),
52+
...(params.inputProvenance ? { provenance: params.inputProvenance } : {}),
53+
...(sourceChannel ? { sourceChannel } : {}),
54+
...(senderId ? { senderId } : {}),
55+
...(senderName ? { senderName } : {}),
56+
...(senderUsername ? { senderUsername } : {}),
57+
...(senderE164 ? { senderE164 } : {}),
58+
...(senderLabel ? { senderLabel } : {}),
59+
} as AgentMessage;
60+
}
61+
1762
/**
1863
* Tag a message with a stable logical identity for mirror dedupe. Callers
1964
* should use a value that is invariant for the same logical message across

0 commit comments

Comments
 (0)