Update files

This commit is contained in:
Mohamed Boudra
2026-02-10 13:43:07 +07:00
parent ade4fb2e45
commit 7e5537bcc5
29 changed files with 1296 additions and 515 deletions
@@ -977,7 +977,6 @@ function mapCodexPatchNotificationToToolCall(params: {
stdout?: string | null;
stderr?: string | null;
success?: boolean | null;
latestUnifiedDiff?: string | null;
running: boolean;
}): ToolCallTimelineItem {
const files = parseCodexPatchChanges(params.changes);
@@ -985,7 +984,7 @@ function mapCodexPatchNotificationToToolCall(params: {
const firstPatchText = files
.map((file) => file.content?.trim())
.find((value): value is string => typeof value === "string" && value.length > 0);
const patchText = params.latestUnifiedDiff?.trim() || firstPatchText;
const patchText = firstPatchText;
const patchFields = codexPatchTextFields(patchText);
const mapped = mapCodexRolloutToolCall({
callId: params.callId ?? null,
@@ -1736,7 +1735,6 @@ class CodexAppServerAgentSession implements AgentSession {
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;
private collaborationModes: Array<{
@@ -2422,7 +2420,6 @@ class CodexAppServerAgentSession implements AgentSession {
if (parsed.kind === "turn_started") {
this.currentTurnId = parsed.turnId;
this.latestTurnUnifiedDiff = null;
this.emittedItemStartedIds.clear();
this.emittedItemCompletedIds.clear();
this.pendingCommandOutputDeltas.clear();
@@ -2444,7 +2441,6 @@ class CodexAppServerAgentSession implements AgentSession {
} else {
this.emitEvent({ type: "turn_completed", provider: CODEX_PROVIDER, usage: this.latestUsage });
}
this.latestTurnUnifiedDiff = null;
this.emittedItemStartedIds.clear();
this.emittedItemCompletedIds.clear();
this.pendingCommandOutputDeltas.clear();
@@ -2470,8 +2466,6 @@ class CodexAppServerAgentSession implements AgentSession {
}
if (parsed.kind === "diff_updated") {
const trimmedDiff = parsed.diff.trim();
this.latestTurnUnifiedDiff = trimmedDiff.length > 0 ? trimmedDiff : null;
// NOTE: Codex app-server emits frequent `turn/diff/updated` notifications
// containing a full accumulated unified diff for the *entire turn*.
// This is not a concrete file-change tool call; it is progress telemetry.
@@ -2557,7 +2551,6 @@ class CodexAppServerAgentSession implements AgentSession {
callId: parsed.callId,
changes: parsed.changes,
cwd: this.config.cwd ?? null,
latestUnifiedDiff: this.latestTurnUnifiedDiff,
running: true,
});
this.warnOnIncompleteEditToolCall(timelineItem, "patch_apply_started", {
@@ -2580,7 +2573,6 @@ class CodexAppServerAgentSession implements AgentSession {
stdout: parsed.stdout ?? bufferedOutput,
stderr: parsed.stderr,
success: parsed.success,
latestUnifiedDiff: this.latestTurnUnifiedDiff,
running: false,
});
this.warnOnIncompleteEditToolCall(timelineItem, "patch_apply_completed", {
@@ -463,4 +463,38 @@ describe("codex tool-call mapper", () => {
expect(item.detail.newString).toBeUndefined();
}
});
it("maps path-only fileChange payloads to unknown detail instead of empty edit detail", () => {
const item = mapCodexToolCallFromThreadItem(
{
type: "fileChange",
id: "codex-file-change-path-only",
status: "completed",
changes: [{ path: "/tmp/repo/src/path-only.ts", kind: "modify" }],
},
{ cwd: "/tmp/repo" }
);
expect(item?.detail.type).toBe("unknown");
if (item?.detail.type === "unknown") {
expect(item.detail.input).toEqual({
files: [{ path: "src/path-only.ts", kind: "modify" }],
});
}
});
it("maps path-only apply_patch rollout payloads to unknown detail instead of empty edit detail", () => {
const item = mapCodexRolloutToolCall({
callId: "codex-call-apply-path-only",
name: "apply_patch",
input: { path: "/tmp/repo/src/path-only-rollout.ts" },
output: { files: [{ path: "/tmp/repo/src/path-only-rollout.ts", kind: "modify" }] },
cwd: "/tmp/repo",
});
expect(item.detail.type).toBe("unknown");
if (item.detail.type === "unknown") {
expect(item.detail.input).toEqual({ path: "/tmp/repo/src/path-only-rollout.ts" });
}
});
});
@@ -309,6 +309,17 @@ function asPatchOrContentFields(text: string | undefined): { patch?: string; con
return { content: text };
}
function hasRenderableEditContent(detail: ToolCallDetail): boolean {
if (detail.type !== "edit") {
return false;
}
return (
(typeof detail.unifiedDiff === "string" && detail.unifiedDiff.length > 0) ||
(typeof detail.newString === "string" && detail.newString.length > 0) ||
(typeof detail.oldString === "string" && detail.oldString.length > 0)
);
}
function pickFirstPatchLikeString(values: unknown[]): string | undefined {
for (const value of values) {
if (typeof value === "string" && value.length > 0) {
@@ -610,12 +621,19 @@ function mapFileChangeItem(
const firstFile = files[0];
const firstTextFields = asEditTextFields(firstFile?.diff);
const hasFirstTextFields = Object.keys(firstTextFields).length > 0;
const detail = firstFile?.path
? {
? hasFirstTextFields
? {
type: "edit" as const,
filePath: firstFile.path,
...firstTextFields,
}
: {
type: "unknown" as const,
input,
output,
}
: {
type: "unknown" as const,
input,
@@ -755,6 +773,13 @@ export function mapCodexRolloutToolCall(params: {
detail = fallbackDetail;
}
}
if (detail.type === "edit" && !hasRenderableEditContent(detail)) {
detail = {
type: "unknown",
input,
output,
};
}
return buildToolCall({
callId,
+54 -49
View File
@@ -147,18 +147,22 @@ export async function createPaseoDaemon(
);
}
const serverId = getOrCreateServerId(config.paseoHome, { logger });
const daemonKeyPair = await loadOrCreateDaemonKeyPair(config.paseoHome, logger);
let relayTransport: RelayTransportController | null = null;
// Acquire PID lock before expensive bootstrap work so duplicate starts fail immediately.
await acquirePidLock(config.paseoHome, config.listen);
const staticDir = config.staticDir;
const downloadTokenTtlMs = config.downloadTokenTtlMs ?? 60000;
try {
const serverId = getOrCreateServerId(config.paseoHome, { logger });
const daemonKeyPair = await loadOrCreateDaemonKeyPair(config.paseoHome, logger);
let relayTransport: RelayTransportController | null = null;
const downloadTokenStore = new DownloadTokenStore({ ttlMs: downloadTokenTtlMs });
const staticDir = config.staticDir;
const downloadTokenTtlMs = config.downloadTokenTtlMs ?? 60000;
const listenTarget = parseListenString(config.listen);
const downloadTokenStore = new DownloadTokenStore({ ttlMs: downloadTokenTtlMs });
const app = express();
const listenTarget = parseListenString(config.listen);
const app = express();
// Host allowlist / DNS rebinding protection (vite-like semantics).
// For non-TCP (unix sockets), skip host validation.
@@ -495,12 +499,9 @@ export async function createPaseoDaemon(
}
);
const start = async () => {
// Acquire PID lock
await acquirePidLock(config.paseoHome, config.listen);
// Start main HTTP server
await new Promise<void>((resolve, reject) => {
const start = async () => {
// Start main HTTP server
await new Promise<void>((resolve, reject) => {
const onError = (err: Error) => {
httpServer.off("listening", onListening);
reject(err);
@@ -581,43 +582,47 @@ export async function createPaseoDaemon(
}
httpServer.listen(listenTarget.path);
}
});
};
});
};
const stop = async () => {
await closeAllAgents(logger, agentManager);
await agentManager.flush().catch(() => undefined);
detachAgentStoragePersistence();
await agentStorage.flush().catch(() => undefined);
await shutdownProviders(logger);
terminalManager.killAll();
cleanupSpeechRuntime();
await relayTransport?.stop().catch(() => undefined);
if (wsServer) {
await wsServer.close();
}
if (voiceMcpBridgeManager) {
await voiceMcpBridgeManager.stop().catch(() => undefined);
}
await new Promise<void>((resolve) => {
httpServer.close(() => resolve());
});
// Clean up socket files
if (listenTarget.type === "socket" && existsSync(listenTarget.path)) {
unlinkSync(listenTarget.path);
}
// Release PID lock
await releasePidLock(config.paseoHome);
};
const stop = async () => {
await closeAllAgents(logger, agentManager);
await agentManager.flush().catch(() => undefined);
detachAgentStoragePersistence();
await agentStorage.flush().catch(() => undefined);
await shutdownProviders(logger);
terminalManager.killAll();
cleanupSpeechRuntime();
await relayTransport?.stop().catch(() => undefined);
if (wsServer) {
await wsServer.close();
}
if (voiceMcpBridgeManager) {
await voiceMcpBridgeManager.stop().catch(() => undefined);
}
await new Promise<void>((resolve) => {
httpServer.close(() => resolve());
});
// Clean up socket files
if (listenTarget.type === "socket" && existsSync(listenTarget.path)) {
unlinkSync(listenTarget.path);
}
// Release PID lock
await releasePidLock(config.paseoHome);
};
return {
config,
agentManager,
agentStorage,
terminalManager,
start,
stop,
};
return {
config,
agentManager,
agentStorage,
terminalManager,
start,
stop,
};
} catch (err) {
await releasePidLock(config.paseoHome).catch(() => undefined);
throw err;
}
}
async function closeAllAgents(
+11 -3
View File
@@ -1,4 +1,4 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { z } from "zod";
import { AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js";
@@ -157,10 +157,18 @@ export function loadPersistedConfig(
): PersistedConfig {
const log = getLogger(logger);
const configPath = getConfigPath(paseoHome);
const defaultConfig = PersistedConfigSchema.parse({});
if (!existsSync(configPath)) {
log?.info(`No config file at ${configPath}, using defaults`);
return PersistedConfigSchema.parse({});
try {
mkdirSync(path.dirname(configPath), { recursive: true });
writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + "\n");
log?.info(`Initialized config file at ${configPath}`);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`[Config] Failed to initialize ${configPath}: ${message}`);
}
return defaultConfig;
}
let raw: string;