Update files

This commit is contained in:
Mohamed Boudra
2026-02-10 11:43:07 +07:00
parent 7001928f4f
commit ade4fb2e45
15 changed files with 1026 additions and 77 deletions
@@ -6,9 +6,10 @@ import { mkdtempSync } from "node:fs";
import { CodexAppServerAgentClient } from "./codex-app-server-agent.js";
import { createTestLogger } from "../../../test-utils/test-logger.js";
import { agentConfigs } from "../../daemon-e2e/agent-configs.js";
const CODEX_TEST_MODEL = "gpt-5.1-codex-mini";
const CODEX_TEST_THINKING_OPTION_ID = "low";
const CODEX_TEST_MODEL = agentConfigs.codex.model;
const CODEX_TEST_THINKING_OPTION_ID = agentConfigs.codex.thinkingOptionId;
function isCodexInstalled(): boolean {
try {
@@ -5,18 +5,20 @@ import os from "node:os";
import path from "node:path";
import {
__codexAppServerInternals,
CodexAppServerAgentClient,
codexAppServerTurnInputFromPrompt,
} from "./codex-app-server-agent.js";
import { createTestLogger } from "../../../test-utils/test-logger.js";
import { agentConfigs } from "../../daemon-e2e/agent-configs.js";
import type {
AgentPermissionRequest,
AgentPromptContentBlock,
AgentTimelineItem,
} from "../agent-sdk-types.js";
const CODEX_TEST_MODEL = "gpt-5.1-codex-mini";
const CODEX_TEST_THINKING_OPTION_ID = "low";
const CODEX_TEST_MODEL = agentConfigs.codex.model;
const CODEX_TEST_THINKING_OPTION_ID = agentConfigs.codex.thinkingOptionId;
const ONE_BY_ONE_PNG_BASE64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+X1r0AAAAASUVORK5CYII=";
@@ -87,6 +89,29 @@ function hasApplyPatchFile(item: AgentTimelineItem, fileName: string): boolean {
return inInput || inOutput || inDiff || inputPath === fileName || outputPath === fileName;
}
function buildStrictApplyPatchPrompt(
patch: string,
completionToken: string,
options?: { includePermissionStep?: boolean }
): string {
const lines = [
"You are running an automated integration test.",
"Required behavior:",
"- Call the apply_patch tool exactly once using the patch below.",
"- Do not call shell, Bash, exec_command, write_file, or any other tool.",
"- Do not ask for confirmation in text or via any tool call.",
];
if (options?.includePermissionStep) {
lines.push(
"- If permission is required, wait for approval and then continue with the same apply_patch call."
);
}
lines.push("Patch to apply exactly:");
lines.push(patch);
lines.push(`After successful apply_patch completion, reply exactly ${completionToken}.`);
return lines.join("\n");
}
async function waitForFileToContainText(
filePath: string,
expectedText: string,
@@ -175,6 +200,50 @@ describe("Codex app-server provider (integration)", () => {
}
});
test("maps patch notifications with array-style changes and alias diff keys", () => {
const item = __codexAppServerInternals.mapCodexPatchNotificationToToolCall({
callId: "patch-array-alias",
changes: [
{
path: "/tmp/repo/src/array-alias.ts",
kind: "modify",
unified_diff: "@@\n-old\n+new\n",
},
],
cwd: "/tmp/repo",
running: false,
});
expect(item.detail.type).toBe("edit");
if (item.detail.type === "edit") {
expect(item.detail.filePath).toBe("src/array-alias.ts");
expect(item.detail.unifiedDiff).toContain("-old");
expect(item.detail.unifiedDiff).toContain("+new");
expect(item.detail.newString).toBeUndefined();
}
});
test("maps patch notifications with object-style single change payloads", () => {
const item = __codexAppServerInternals.mapCodexPatchNotificationToToolCall({
callId: "patch-object-single",
changes: {
path: "/tmp/repo/src/object-single.ts",
kind: "modify",
patch: "@@\n-before\n+after\n",
},
cwd: "/tmp/repo",
running: false,
});
expect(item.detail.type).toBe("edit");
if (item.detail.type === "edit") {
expect(item.detail.filePath).toBe("src/object-single.ts");
expect(item.detail.unifiedDiff).toContain("-before");
expect(item.detail.unifiedDiff).toContain("+after");
expect(item.detail.newString).toBeUndefined();
}
});
test.runIf(isCodexInstalled())("listModels returns live Codex models", async () => {
const client = new CodexAppServerAgentClient(logger);
const models = await client.listModels();
@@ -209,19 +278,28 @@ describe("Codex app-server provider (integration)", () => {
const codexHome = tmpCwd("codex-home-defaults-");
const prevCodexHome = process.env.CODEX_HOME;
process.env.CODEX_HOME = codexHome;
writeFileSync(
path.join(codexHome, "config.toml"),
[
'model = "gpt-5.3-codex"',
'model_reasoning_effort = "xhigh"',
].join("\n"),
"utf8"
);
try {
const client = new CodexAppServerAgentClient(logger);
const baselineModels = await client.listModels();
const baselineDefaultModel =
baselineModels.find((model) => model.isDefault) ?? baselineModels[0];
expect(baselineDefaultModel).toBeDefined();
const configuredModelId = baselineDefaultModel?.id;
expect(typeof configuredModelId).toBe("string");
expect((configuredModelId ?? "").length).toBeGreaterThan(0);
writeFileSync(
path.join(codexHome, "config.toml"),
[
`model = "${configuredModelId}"`,
'model_reasoning_effort = "xhigh"',
].join("\n"),
"utf8"
);
const models = await client.listModels();
const configuredModel = models.find((model) => model.id === "gpt-5.3-codex");
const configuredModel = models.find((model) => model.id === configuredModelId);
expect(configuredModel).toBeDefined();
expect(configuredModel?.isDefault).toBe(true);
expect(configuredModel?.defaultThinkingOptionId).toBe("xhigh");
@@ -703,14 +781,9 @@ describe("Codex app-server provider (integration)", () => {
"*** End Patch",
].join("\n");
const patchEvents = session.stream(
[
"Use the apply_patch tool and nothing else.",
"Do not use the shell tool for file changes.",
"Do not respond with any message until the apply_patch tool completes.",
"Apply the following patch exactly:",
patch,
"After it completes, reply PATCH_DONE.",
].join("\n")
buildStrictApplyPatchPrompt(patch, "PATCH_DONE", {
includePermissionStep: true,
})
);
for await (const event of patchEvents) {
@@ -801,13 +874,9 @@ describe("Codex app-server provider (integration)", () => {
"*** End Patch",
].join("\n");
const events = session.stream(
[
"Use the apply_patch tool and nothing else.",
"Do not use shell or any other file-edit tool.",
"Apply this patch exactly:",
patch,
"After tool completion, reply PATCH_DONE.",
].join("\n")
buildStrictApplyPatchPrompt(patch, "PATCH_DONE", {
includePermissionStep: true,
})
);
for await (const event of events) {
@@ -1252,14 +1321,9 @@ describe("Codex app-server provider (integration)", () => {
"*** End Patch",
].join("\n");
const events = session.stream(
[
"Use the apply_patch tool and nothing else.",
"If you need approval before writing files, request approval first.",
"Do not respond with any message until the apply_patch tool completes.",
"Apply the following patch exactly:",
patch,
"After approval, reply FILE_DONE.",
].join("\n")
buildStrictApplyPatchPrompt(patch, "FILE_DONE", {
includePermissionStep: true,
})
);
let failure: string | null = null;
@@ -1298,7 +1362,19 @@ describe("Codex app-server provider (integration)", () => {
expect(sawPermissionResolved).toBe(true);
}
const sawPatch = timelineItems.some((item) => hasApplyPatchFile(item, "approval-test.txt"));
expect(sawPatch).toBe(true);
if (!sawPatch) {
const toolCalls = timelineItems
.filter((item): item is Extract<AgentTimelineItem, { type: "tool_call" }> => item.type === "tool_call")
.map((item) => ({
name: item.name,
status: item.status,
callId: item.callId,
detail: item.detail,
}));
throw new Error(
`Did not observe apply_patch timeline detail for approval-test.txt. Tool calls: ${JSON.stringify(toolCalls)}`
);
}
const text = await waitForFileToContainText(targetPath, "ok", { timeoutMs: 10000 });
if (!text) {
@@ -708,6 +708,27 @@ type CodexPatchFileChange = {
content?: string;
};
function extractPatchLikeText(value: unknown): string | undefined {
if (!value || typeof value !== "object") {
return undefined;
}
const record = value as Record<string, unknown>;
const candidates = [
record.diff,
record.patch,
record.unified_diff,
record.unifiedDiff,
record.content,
record.newString,
];
for (const candidate of candidates) {
if (typeof candidate === "string" && candidate.length > 0) {
return candidate;
}
}
return undefined;
}
function normalizeCodexThreadItemType(rawType: string | undefined): string | undefined {
if (!rawType) {
return rawType;
@@ -780,20 +801,60 @@ function parseCodexPatchChanges(changes: unknown): CodexPatchFileChange[] {
if (!changes || typeof changes !== "object") {
return [];
}
return Object.entries(changes as Record<string, unknown>)
if (Array.isArray(changes)) {
return changes
.map((entry): CodexPatchFileChange | null => {
if (!entry || typeof entry !== "object") {
return null;
}
const record = entry as Record<string, unknown>;
const pathValue =
typeof record.path === "string" && record.path.trim().length > 0
? record.path.trim()
: "";
if (!pathValue) {
return null;
}
return {
path: pathValue,
kind:
(typeof record.kind === "string" && record.kind) ||
(typeof record.type === "string" && record.type) ||
undefined,
content: extractPatchLikeText(record),
};
})
.filter((entry): entry is CodexPatchFileChange => entry !== null);
}
const recordChanges = changes as Record<string, unknown>;
if (typeof recordChanges.path === "string" && recordChanges.path.trim().length > 0) {
return [
{
path: recordChanges.path.trim(),
kind:
(typeof recordChanges.kind === "string" && recordChanges.kind) ||
(typeof recordChanges.type === "string" && recordChanges.type) ||
undefined,
content: extractPatchLikeText(recordChanges),
},
];
}
return Object.entries(recordChanges)
.map(([path, value]): CodexPatchFileChange | null => {
const normalizedPath = path.trim();
if (!normalizedPath) {
return null;
}
const parsed =
value && typeof value === "object"
? (value as { type?: unknown; content?: unknown })
: null;
return {
path: normalizedPath,
kind: typeof parsed?.type === "string" ? parsed.type : undefined,
content: typeof parsed?.content === "string" ? parsed.content : undefined,
kind:
value && typeof value === "object" && typeof (value as { type?: unknown }).type === "string"
? ((value as { type?: string }).type ?? undefined)
: undefined,
content: extractPatchLikeText(value),
};
})
.filter((entry): entry is CodexPatchFileChange => entry !== null);
@@ -822,6 +883,46 @@ function toRunningToolCall(item: ToolCallTimelineItem): ToolCallTimelineItem {
};
}
function isEditToolCallWithoutContent(item: ToolCallTimelineItem): boolean {
if (item.type !== "tool_call") {
return false;
}
if (item.detail.type !== "edit") {
return false;
}
const hasDiff =
typeof item.detail.unifiedDiff === "string" &&
item.detail.unifiedDiff.trim().length > 0;
const hasNewString =
typeof item.detail.newString === "string" &&
item.detail.newString.trim().length > 0;
return !hasDiff && !hasNewString;
}
function decodeCodexOutputDeltaChunk(chunk: string): string {
const trimmed = chunk.trim();
if (trimmed.length === 0) {
return chunk;
}
if (!/^[A-Za-z0-9+/=]+$/.test(trimmed) || trimmed.length % 4 !== 0) {
return chunk;
}
try {
const decoded = Buffer.from(trimmed, "base64").toString("utf8");
if (decoded.length === 0) {
return chunk;
}
const normalizedInput = trimmed.replace(/=+$/, "");
const normalizedRoundTrip = Buffer.from(decoded, "utf8")
.toString("base64")
.replace(/=+$/, "");
return normalizedRoundTrip === normalizedInput ? decoded : chunk;
} catch {
return chunk;
}
}
function mapCodexExecNotificationToToolCall(params: {
callId?: string | null;
command: unknown;
@@ -881,10 +982,10 @@ function mapCodexPatchNotificationToToolCall(params: {
}): ToolCallTimelineItem {
const files = parseCodexPatchChanges(params.changes);
const firstPath = files[0]?.path;
const firstContent = files
const firstPatchText = files
.map((file) => file.content?.trim())
.find((value): value is string => typeof value === "string" && value.length > 0);
const patchText = params.latestUnifiedDiff?.trim() || firstContent;
const patchText = params.latestUnifiedDiff?.trim() || firstPatchText;
const patchFields = codexPatchTextFields(patchText);
const mapped = mapCodexRolloutToolCall({
callId: params.callId ?? null,
@@ -907,7 +1008,7 @@ function mapCodexPatchNotificationToToolCall(params: {
files: files.map((file) => ({
path: file.path,
...(file.kind ? { kind: file.kind } : {}),
...patchFields,
...codexPatchTextFields(file.content ?? patchText),
})),
}
: {}),
@@ -1138,6 +1239,18 @@ const CodexEventExecCommandEndNotificationSchema = z.object({
.passthrough(),
}).passthrough();
const CodexEventExecCommandOutputDeltaNotificationSchema = z.object({
msg: z
.object({
type: z.literal("exec_command_output_delta"),
call_id: z.string().optional(),
stream: z.string().optional(),
chunk: z.string().optional(),
delta: z.string().optional(),
})
.passthrough(),
}).passthrough();
const CodexEventPatchApplyBeginNotificationSchema = z.object({
msg: z
.object({
@@ -1161,6 +1274,12 @@ const CodexEventPatchApplyEndNotificationSchema = z.object({
.passthrough(),
}).passthrough();
const ItemFileChangeOutputDeltaNotificationSchema = z.object({
itemId: z.string(),
delta: z.string().optional(),
chunk: z.string().optional(),
}).passthrough();
const CodexEventTurnDiffNotificationSchema = z.object({
msg: z
.object({
@@ -1206,6 +1325,12 @@ type ParsedCodexNotification =
success: boolean | null;
stderr: string | null;
}
| {
kind: "exec_command_output_delta";
callId: string | null;
stream: string | null;
chunk: string | null;
}
| {
kind: "patch_apply_started";
callId: string | null;
@@ -1219,6 +1344,11 @@ type ParsedCodexNotification =
stderr: string | null;
success: boolean | null;
}
| {
kind: "file_change_output_delta";
itemId: string;
delta: string | null;
}
| { kind: "invalid_payload"; method: string; params: unknown }
| { kind: "unknown_method"; method: string; params: unknown };
@@ -1370,6 +1500,23 @@ const CodexNotificationSchema = z.union([
z.object({ method: z.literal("codex/event/exec_command_end"), params: z.unknown() }).transform(
({ method, params }): ParsedCodexNotification => ({ kind: "invalid_payload", method, params })
),
z.object({
method: z.literal("codex/event/exec_command_output_delta"),
params: CodexEventExecCommandOutputDeltaNotificationSchema,
}).transform(
({ params }): ParsedCodexNotification => ({
kind: "exec_command_output_delta",
callId: params.msg.call_id ?? null,
stream: params.msg.stream ?? null,
chunk: params.msg.chunk ?? params.msg.delta ?? null,
})
),
z.object({
method: z.literal("codex/event/exec_command_output_delta"),
params: z.unknown(),
}).transform(
({ method, params }): ParsedCodexNotification => ({ kind: "invalid_payload", method, params })
),
z.object({
method: z.literal("codex/event/patch_apply_begin"),
params: CodexEventPatchApplyBeginNotificationSchema,
@@ -1399,6 +1546,19 @@ const CodexNotificationSchema = z.union([
z.object({ method: z.literal("codex/event/patch_apply_end"), params: z.unknown() }).transform(
({ method, params }): ParsedCodexNotification => ({ kind: "invalid_payload", method, params })
),
z.object({
method: z.literal("item/fileChange/outputDelta"),
params: ItemFileChangeOutputDeltaNotificationSchema,
}).transform(
({ params }): ParsedCodexNotification => ({
kind: "file_change_output_delta",
itemId: params.itemId,
delta: params.delta ?? params.chunk ?? null,
})
),
z.object({ method: z.literal("item/fileChange/outputDelta"), params: z.unknown() }).transform(
({ method, params }): ParsedCodexNotification => ({ kind: "invalid_payload", method, params })
),
z.object({
method: z.literal("codex/event/turn_diff"),
params: CodexEventTurnDiffNotificationSchema,
@@ -1539,6 +1699,10 @@ export async function codexAppServerTurnInputFromPrompt(
return output;
}
export const __codexAppServerInternals = {
mapCodexPatchNotificationToToolCall,
};
class CodexAppServerAgentSession implements AgentSession {
readonly provider = CODEX_PROVIDER;
readonly capabilities = CODEX_APP_SERVER_CAPABILITIES;
@@ -1565,10 +1729,13 @@ class CodexAppServerAgentSession implements AgentSession {
private resolvedPermissionRequests = new Set<string>();
private pendingAgentMessages = new Map<string, string>();
private pendingReasoning = new Map<string, string[]>();
private pendingCommandOutputDeltas = new Map<string, string[]>();
private pendingFileChangeOutputDeltas = new Map<string, string[]>();
private emittedItemStartedIds = new Set<string>();
private emittedItemCompletedIds = new Set<string>();
private warnedUnknownNotificationMethods = new Set<string>();
private warnedInvalidNotificationPayloads = new Set<string>();
private warnedIncompleteEditToolCallIds = new Set<string>();
private latestTurnUnifiedDiff: string | null = null;
private latestUsage: AgentUsage | undefined;
private connected = false;
@@ -1757,6 +1924,9 @@ class CodexAppServerAgentSession implements AgentSession {
cwd: this.config.cwd ?? null,
});
if (timelineItem) {
if (timelineItem.type === "tool_call") {
this.warnOnIncompleteEditToolCall(timelineItem, "thread_read", item);
}
threadTimeline.push(timelineItem);
}
}
@@ -2255,6 +2425,9 @@ class CodexAppServerAgentSession implements AgentSession {
this.latestTurnUnifiedDiff = null;
this.emittedItemStartedIds.clear();
this.emittedItemCompletedIds.clear();
this.pendingCommandOutputDeltas.clear();
this.pendingFileChangeOutputDeltas.clear();
this.warnedIncompleteEditToolCallIds.clear();
this.emitEvent({ type: "turn_started", provider: CODEX_PROVIDER });
return;
}
@@ -2274,6 +2447,9 @@ class CodexAppServerAgentSession implements AgentSession {
this.latestTurnUnifiedDiff = null;
this.emittedItemStartedIds.clear();
this.emittedItemCompletedIds.clear();
this.pendingCommandOutputDeltas.clear();
this.pendingFileChangeOutputDeltas.clear();
this.warnedIncompleteEditToolCallIds.clear();
this.eventQueue?.end();
return;
}
@@ -2321,7 +2497,25 @@ class CodexAppServerAgentSession implements AgentSession {
return;
}
if (parsed.kind === "exec_command_output_delta") {
this.appendOutputDeltaChunk(
this.pendingCommandOutputDeltas,
parsed.callId,
parsed.chunk,
{ decodeBase64: true }
);
return;
}
if (parsed.kind === "file_change_output_delta") {
this.appendOutputDeltaChunk(this.pendingFileChangeOutputDeltas, parsed.itemId, parsed.delta);
return;
}
if (parsed.kind === "exec_command_started") {
if (parsed.callId) {
this.pendingCommandOutputDeltas.delete(parsed.callId);
}
const timelineItem = mapCodexExecNotificationToToolCall({
callId: parsed.callId,
command: parsed.command,
@@ -2335,11 +2529,15 @@ class CodexAppServerAgentSession implements AgentSession {
}
if (parsed.kind === "exec_command_completed") {
const bufferedOutput = this.consumeOutputDelta(
this.pendingCommandOutputDeltas,
parsed.callId
);
const timelineItem = mapCodexExecNotificationToToolCall({
callId: parsed.callId,
command: parsed.command,
cwd: parsed.cwd ?? this.config.cwd ?? null,
output: parsed.output,
output: parsed.output ?? bufferedOutput,
exitCode: parsed.exitCode,
success: parsed.success,
stderr: parsed.stderr,
@@ -2352,6 +2550,9 @@ class CodexAppServerAgentSession implements AgentSession {
}
if (parsed.kind === "patch_apply_started") {
if (parsed.callId) {
this.pendingFileChangeOutputDeltas.delete(parsed.callId);
}
const timelineItem = mapCodexPatchNotificationToToolCall({
callId: parsed.callId,
changes: parsed.changes,
@@ -2359,21 +2560,34 @@ class CodexAppServerAgentSession implements AgentSession {
latestUnifiedDiff: this.latestTurnUnifiedDiff,
running: true,
});
this.warnOnIncompleteEditToolCall(timelineItem, "patch_apply_started", {
callId: parsed.callId,
changes: parsed.changes,
});
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
return;
}
if (parsed.kind === "patch_apply_completed") {
const bufferedOutput = this.consumeOutputDelta(
this.pendingFileChangeOutputDeltas,
parsed.callId
);
const timelineItem = mapCodexPatchNotificationToToolCall({
callId: parsed.callId,
changes: parsed.changes,
cwd: this.config.cwd ?? null,
stdout: parsed.stdout,
stdout: parsed.stdout ?? bufferedOutput,
stderr: parsed.stderr,
success: parsed.success,
latestUnifiedDiff: this.latestTurnUnifiedDiff,
running: false,
});
this.warnOnIncompleteEditToolCall(timelineItem, "patch_apply_completed", {
callId: parsed.callId,
changes: parsed.changes,
stdout: parsed.stdout,
});
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
return;
}
@@ -2406,10 +2620,15 @@ class CodexAppServerAgentSession implements AgentSession {
timelineItem.text = buffered.join("");
}
}
if (timelineItem.type === "tool_call") {
this.warnOnIncompleteEditToolCall(timelineItem, "item_completed", parsed.item);
}
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
if (itemId) {
this.emittedItemCompletedIds.add(itemId);
this.emittedItemStartedIds.delete(itemId);
this.pendingCommandOutputDeltas.delete(itemId);
this.pendingFileChangeOutputDeltas.delete(itemId);
}
}
return;
@@ -2428,9 +2647,12 @@ class CodexAppServerAgentSession implements AgentSession {
if (itemId && this.emittedItemStartedIds.has(itemId)) {
return;
}
this.warnOnIncompleteEditToolCall(timelineItem, "item_started", parsed.item);
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
if (itemId) {
this.emittedItemStartedIds.add(itemId);
this.pendingCommandOutputDeltas.delete(itemId);
this.pendingFileChangeOutputDeltas.delete(itemId);
}
}
return;
@@ -2464,6 +2686,62 @@ class CodexAppServerAgentSession implements AgentSession {
);
}
private appendOutputDeltaChunk(
store: Map<string, string[]>,
id: string | null | undefined,
chunk: string | null | undefined,
options?: { decodeBase64?: boolean }
): void {
if (!id || !chunk) {
return;
}
const normalized = options?.decodeBase64 ? decodeCodexOutputDeltaChunk(chunk) : chunk;
if (!normalized.length) {
return;
}
const prev = store.get(id) ?? [];
prev.push(normalized);
store.set(id, prev);
}
private consumeOutputDelta(store: Map<string, string[]>, id: string | null | undefined): string | null {
if (!id) {
return null;
}
const buffered = store.get(id);
if (!buffered || buffered.length === 0) {
return null;
}
store.delete(id);
return buffered.join("");
}
private warnOnIncompleteEditToolCall(
item: ToolCallTimelineItem,
source: string,
payload: unknown
): void {
if (!isEditToolCallWithoutContent(item)) {
return;
}
const warnKey = `${source}:${item.callId}`;
if (this.warnedIncompleteEditToolCallIds.has(warnKey)) {
return;
}
this.warnedIncompleteEditToolCallIds.add(warnKey);
this.logger.warn(
{
source,
callId: item.callId,
status: item.status,
name: item.name,
detail: item.detail,
payload,
},
"Codex edit tool call is missing diff/content fields"
);
}
private handleCommandApprovalRequest(params: unknown): Promise<unknown> {
const parsed = params as {
itemId: string;
@@ -411,4 +411,56 @@ describe("codex tool-call mapper", () => {
expect(item.detail.newString).toBeUndefined();
}
});
it("maps fileChange patch alias fields into edit unified diff detail", () => {
const item = mapCodexToolCallFromThreadItem(
{
type: "fileChange",
id: "codex-file-change-patch-alias",
status: "completed",
changes: [
{
path: "/tmp/repo/src/from-patch-alias.ts",
kind: "modify",
patch: "@@\n-oldAlias\n+newAlias\n",
},
],
},
{ cwd: "/tmp/repo" }
);
expect(item?.detail?.type).toBe("edit");
if (item?.detail?.type === "edit") {
expect(item.detail.filePath).toBe("src/from-patch-alias.ts");
expect(item.detail.unifiedDiff).toContain("-oldAlias");
expect(item.detail.unifiedDiff).toContain("+newAlias");
expect(item.detail.newString).toBeUndefined();
}
});
it("maps fileChange unifiedDiff alias fields into edit unified diff detail", () => {
const item = mapCodexToolCallFromThreadItem(
{
type: "fileChange",
id: "codex-file-change-unified-diff-alias",
status: "completed",
changes: [
{
path: "/tmp/repo/src/from-unified-diff-alias.ts",
kind: "modify",
unified_diff: "@@\n-beforeAlias\n+afterAlias\n",
},
],
},
{ cwd: "/tmp/repo" }
);
expect(item?.detail?.type).toBe("edit");
if (item?.detail?.type === "edit") {
expect(item.detail.filePath).toBe("src/from-unified-diff-alias.ts");
expect(item.detail.unifiedDiff).toContain("-beforeAlias");
expect(item.detail.unifiedDiff).toContain("+afterAlias");
expect(item.detail.newString).toBeUndefined();
}
});
});
@@ -59,7 +59,11 @@ const CodexFileChangeItemSchema = z
path: z.string().optional(),
kind: z.string().optional(),
diff: z.string().optional(),
patch: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
content: z.string().optional(),
newString: z.string().optional(),
})
.passthrough()
)
@@ -305,6 +309,15 @@ function asPatchOrContentFields(text: string | undefined): { patch?: string; con
return { content: text };
}
function pickFirstPatchLikeString(values: unknown[]): string | undefined {
for (const value of values) {
if (typeof value === "string" && value.length > 0) {
return value;
}
}
return undefined;
}
function removePatchLikeFields(input: Record<string, unknown>): Record<string, unknown> {
const {
patch: _patch,
@@ -560,7 +573,14 @@ function mapFileChangeItem(
return {
path: pathValue,
kind: change.kind,
diff: change.diff ?? change.content,
diff: pickFirstPatchLikeString([
change.diff,
change.patch,
change.unified_diff,
change.unifiedDiff,
change.content,
change.newString,
]),
};
})
.filter((change) => change.path !== undefined);
+12 -1
View File
@@ -64,7 +64,7 @@ import type {
AgentClient,
AgentProvider,
} from "./agent/agent-sdk-types.js";
import { acquirePidLock, releasePidLock } from "./pid-lock.js";
import { acquirePidLock, releasePidLock, isLocked, PidLockError } from "./pid-lock.js";
import { isHostAllowed, type AllowedHostsConfig } from "./allowed-hosts.js";
import {
createVoiceMcpSocketBridgeManager,
@@ -136,6 +136,17 @@ export async function createPaseoDaemon(
rootLogger: Logger
): Promise<PaseoDaemon> {
const logger = rootLogger.child({ module: "bootstrap" });
// Fail fast before expensive bootstrap (speech/runtime/model checks) if another daemon is active.
const lockState = await isLocked(config.paseoHome);
if (lockState.locked) {
const existingLock = lockState.info;
throw new PidLockError(
`Another Paseo daemon is already running (PID ${existingLock?.pid ?? "unknown"}, started ${existingLock?.startedAt ?? "unknown"})`,
existingLock
);
}
const serverId = getOrCreateServerId(config.paseoHome, { logger });
const daemonKeyPair = await loadOrCreateDaemonKeyPair(config.paseoHome, logger);
let relayTransport: RelayTransportController | null = null;
+10 -1
View File
@@ -34,7 +34,16 @@ async function main() {
config.mcpEnabled = false;
}
const daemon = await createPaseoDaemon(config, logger);
let daemon;
try {
daemon = await createPaseoDaemon(config, logger);
} catch (err) {
if (err instanceof PidLockError) {
logger.error({ pid: err.existingLock?.pid }, err.message);
process.exit(1);
}
throw err;
}
try {
await daemon.start();
@@ -643,7 +643,11 @@ export class VoiceAssistantWebSocketServer {
void this.pushService.sendPush(tokens, {
title,
body,
data: { agentId: params.agentId, reason: params.reason },
data: {
serverId: this.serverId,
agentId: params.agentId,
reason: params.reason,
},
});
}
}