Update files
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user