pi-openai-codex-compat 0.0.9 → 0.0.10-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/APPLY_PATCH_INSTRUCTION_FEEDBACK.md +11 -55
- package/CHANGELOG.md +50 -0
- package/README.md +116 -38
- package/THIRD_PARTY_NOTICES.md +1 -27
- package/extensions/openai-codex-compat/apply-patch-diagnostics.ts +573 -0
- package/extensions/openai-codex-compat/apply-patch-diff-render.ts +20 -344
- package/extensions/openai-codex-compat/apply-patch-engine/apply-patch-engine-contracts.ts +92 -0
- package/extensions/openai-codex-compat/apply-patch-engine/apply-patch-engine-details-schema.ts +325 -0
- package/extensions/openai-codex-compat/apply-patch-engine/apply-patch-engine-details.ts +227 -0
- package/extensions/openai-codex-compat/apply-patch-engine/apply-patch-engine-errors.ts +60 -0
- package/extensions/openai-codex-compat/apply-patch-engine/apply-patch-engine-executor.ts +583 -0
- package/extensions/openai-codex-compat/apply-patch-engine/apply-patch-engine-failure-inspection.ts +493 -0
- package/extensions/openai-codex-compat/apply-patch-engine/apply-patch-engine-feedback.ts +445 -0
- package/extensions/openai-codex-compat/apply-patch-engine/apply-patch-engine-filesystem-inspection.ts +49 -0
- package/extensions/openai-codex-compat/apply-patch-engine/apply-patch-engine-filesystem-model.ts +188 -0
- package/extensions/openai-codex-compat/apply-patch-engine/apply-patch-engine-filesystem-mutations.ts +324 -0
- package/extensions/openai-codex-compat/apply-patch-engine/apply-patch-engine-mutation-queue.ts +95 -0
- package/extensions/openai-codex-compat/apply-patch-engine/apply-patch-engine-operation-semantics.ts +142 -0
- package/extensions/openai-codex-compat/apply-patch-engine/apply-patch-engine-parser.ts +384 -0
- package/extensions/openai-codex-compat/apply-patch-engine/apply-patch-engine-path-identity.ts +78 -0
- package/extensions/openai-codex-compat/apply-patch-engine/apply-patch-engine-semantic-planner.ts +1363 -0
- package/extensions/openai-codex-compat/apply-patch-engine.ts +81 -4572
- package/extensions/openai-codex-compat/apply-patch-matcher/apply-patch-matcher-contracts.ts +12 -0
- package/extensions/openai-codex-compat/apply-patch-matcher/apply-patch-matcher-line-matching.ts +123 -0
- package/extensions/openai-codex-compat/apply-patch-matcher/apply-patch-matcher-strict-content.ts +101 -0
- package/extensions/openai-codex-compat/apply-patch-matcher.ts +10 -1527
- package/extensions/openai-codex-compat/apply-patch-render.ts +14 -57
- package/extensions/openai-codex-compat/apply-patch.ts +87 -9
- package/extensions/openai-codex-compat/codex-cache-diagnostics.ts +24 -24
- package/extensions/openai-codex-compat/codex-identifiers.ts +8 -0
- package/extensions/openai-codex-compat/codex-installation.ts +3 -10
- package/extensions/openai-codex-compat/codex-metadata.ts +49 -28
- package/extensions/openai-codex-compat/codex-protocol.ts +260 -161
- package/extensions/openai-codex-compat/codex-provider/codex-provider-contracts.ts +108 -0
- package/extensions/openai-codex-compat/codex-provider/codex-provider-history.ts +106 -0
- package/extensions/openai-codex-compat/codex-provider/codex-provider-response-attempts.ts +408 -0
- package/extensions/openai-codex-compat/codex-provider/codex-provider-runtime.ts +1281 -0
- package/extensions/openai-codex-compat/codex-provider.ts +11 -1651
- package/extensions/openai-codex-compat/codex-stream.ts +76 -77
- package/extensions/openai-codex-compat/codex-thread-lineage.ts +83 -37
- package/extensions/openai-codex-compat/codex-tool-surface.ts +18 -8
- package/extensions/openai-codex-compat/codex-transport/codex-transport-contracts.ts +262 -0
- package/extensions/openai-codex-compat/codex-transport/codex-transport-diagnostics.ts +263 -0
- package/extensions/openai-codex-compat/codex-transport/codex-transport-errors.ts +172 -0
- package/extensions/openai-codex-compat/codex-transport/codex-transport-events.ts +68 -0
- package/extensions/openai-codex-compat/codex-transport/codex-transport-request-headers.ts +315 -0
- package/extensions/openai-codex-compat/codex-transport/codex-transport-retry.ts +124 -0
- package/extensions/openai-codex-compat/codex-transport/codex-transport-sse-stream.ts +214 -0
- package/extensions/openai-codex-compat/codex-transport/codex-transport-turn-state.ts +23 -0
- package/extensions/openai-codex-compat/codex-transport/codex-transport-websocket-continuation.ts +107 -0
- package/extensions/openai-codex-compat/codex-transport/codex-transport-websocket-pool.ts +257 -0
- package/extensions/openai-codex-compat/codex-transport/codex-transport-websocket-stream.ts +320 -0
- package/extensions/openai-codex-compat/codex-transport.ts +251 -2074
- package/extensions/openai-codex-compat/compaction-checkpoint.ts +169 -78
- package/extensions/openai-codex-compat/config-context.ts +10 -0
- package/extensions/openai-codex-compat/config.ts +124 -88
- package/extensions/openai-codex-compat/error-from-thrown.ts +3 -0
- package/extensions/openai-codex-compat/footer.ts +87 -18
- package/extensions/openai-codex-compat/image-generation-render.ts +31 -33
- package/extensions/openai-codex-compat/image-generation-schema.ts +5 -9
- package/extensions/openai-codex-compat/image-generation.ts +81 -51
- package/extensions/openai-codex-compat/index.ts +23 -11
- package/extensions/openai-codex-compat/model-context.ts +20 -0
- package/extensions/openai-codex-compat/model-policy.ts +77 -12
- package/extensions/openai-codex-compat/namespaced-tools.ts +2 -1
- package/extensions/openai-codex-compat/native-history.ts +55 -55
- package/extensions/openai-codex-compat/output-limit-continuation.ts +89 -21
- package/extensions/openai-codex-compat/remote-compaction.ts +151 -58
- package/extensions/openai-codex-compat/request-options.ts +29 -22
- package/extensions/openai-codex-compat/required-value.ts +6 -0
- package/extensions/openai-codex-compat/responses-event-schema.ts +7 -0
- package/extensions/openai-codex-compat/responses-item-schema.ts +461 -0
- package/extensions/openai-codex-compat/responses-lite.ts +16 -16
- package/extensions/openai-codex-compat/responses-replay.ts +18 -4
- package/extensions/openai-codex-compat/responses-tool-schema.ts +92 -0
- package/extensions/openai-codex-compat/settings-pane.ts +120 -45
- package/extensions/openai-codex-compat/tool-definition-contract.ts +28 -0
- package/extensions/openai-codex-compat/tool-runtime.ts +27 -13
- package/extensions/openai-codex-compat/tools.ts +21 -12
- package/extensions/openai-codex-compat/value-contracts.ts +37 -0
- package/extensions/openai-codex-compat/vendor/pi-ai/README.md +6 -1
- package/extensions/openai-codex-compat/vendor/pi-ai/openai-responses-serialization.ts +117 -73
- package/extensions/openai-codex-compat/web-run-output.ts +22 -13
- package/extensions/openai-codex-compat/web-run-render.ts +54 -40
- package/extensions/openai-codex-compat/web-run-schema.ts +5 -55
- package/extensions/openai-codex-compat/web-run.ts +83 -39
- package/package.json +8 -11
- package/LICENSES/tree-sitter-wasms-MIT.txt +0 -21
- package/LICENSES/web-tree-sitter-MIT.txt +0 -21
- package/extensions/openai-codex-compat/provider-error.ts +0 -79
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import type { Stats } from "node:fs";
|
|
3
|
+
import { chmod, lstat, mkdir, readFile, readlink, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { arch, platform, release } from "node:os";
|
|
5
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
getAgentDir,
|
|
8
|
+
VERSION as PI_VERSION,
|
|
9
|
+
type ExtensionContext,
|
|
10
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import type {
|
|
12
|
+
ApplyPatchDetails,
|
|
13
|
+
ApplyPatchDiagnosticsReference,
|
|
14
|
+
PatchOperation,
|
|
15
|
+
} from "./apply-patch-engine.ts";
|
|
16
|
+
import {
|
|
17
|
+
parsePatchDocument,
|
|
18
|
+
scanPatchInstructions,
|
|
19
|
+
} from "./apply-patch-engine/apply-patch-engine-parser.ts";
|
|
20
|
+
import { isObject } from "./codex-protocol.ts";
|
|
21
|
+
import { nodeErrorCode } from "./value-contracts.ts";
|
|
22
|
+
|
|
23
|
+
export const APPLY_PATCH_DIAGNOSTICS_DIRECTORY = "openai-codex-compat-apply-patch-diagnostics";
|
|
24
|
+
export const APPLY_PATCH_DIAGNOSTICS_SCHEMA_VERSION = 2;
|
|
25
|
+
|
|
26
|
+
type ApplyPatchDiagnosticsSessionManager = Pick<
|
|
27
|
+
ExtensionContext["sessionManager"],
|
|
28
|
+
"getBranch" | "getSessionFile" | "getSessionId"
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
export type ApplyPatchDiagnosticsContext = {
|
|
32
|
+
cwd: string;
|
|
33
|
+
sessionManager: ApplyPatchDiagnosticsSessionManager;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type DiagnosticError = {
|
|
37
|
+
name: string;
|
|
38
|
+
message: string;
|
|
39
|
+
stack?: string;
|
|
40
|
+
code?: string;
|
|
41
|
+
errno?: number | string;
|
|
42
|
+
syscall?: string;
|
|
43
|
+
path?: string;
|
|
44
|
+
destination?: string;
|
|
45
|
+
cause?: DiagnosticError;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type FilesystemMetadata = {
|
|
49
|
+
mode: number;
|
|
50
|
+
modifiedMs: number;
|
|
51
|
+
size: number;
|
|
52
|
+
device: number;
|
|
53
|
+
inode: number;
|
|
54
|
+
linkCount: number;
|
|
55
|
+
userId: number;
|
|
56
|
+
groupId: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type SnapshotContent =
|
|
60
|
+
| {
|
|
61
|
+
encoding: "binary";
|
|
62
|
+
byteLength: number;
|
|
63
|
+
sha256: string;
|
|
64
|
+
}
|
|
65
|
+
| {
|
|
66
|
+
encoding: "utf8";
|
|
67
|
+
data: string;
|
|
68
|
+
byteLength: number;
|
|
69
|
+
sha256: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type SnapshotReference = {
|
|
73
|
+
instruction: number;
|
|
74
|
+
role: "destination" | "source";
|
|
75
|
+
patchPath: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type FileSnapshot = {
|
|
79
|
+
absolutePath: string;
|
|
80
|
+
references: SnapshotReference[];
|
|
81
|
+
entry:
|
|
82
|
+
| { kind: "absent" }
|
|
83
|
+
| ({ kind: "directory" } & FilesystemMetadata)
|
|
84
|
+
| ({
|
|
85
|
+
kind: "regular-file";
|
|
86
|
+
content: SnapshotContent;
|
|
87
|
+
} & FilesystemMetadata)
|
|
88
|
+
| ({
|
|
89
|
+
kind: "symlink";
|
|
90
|
+
target: string;
|
|
91
|
+
resolvedTarget: string;
|
|
92
|
+
content?: SnapshotContent;
|
|
93
|
+
contentError?: DiagnosticError;
|
|
94
|
+
} & FilesystemMetadata)
|
|
95
|
+
| ({ kind: "unsupported" } & FilesystemMetadata)
|
|
96
|
+
| { kind: "inspection-error"; error: DiagnosticError };
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
type ParentSnapshot = {
|
|
100
|
+
absolutePath: string;
|
|
101
|
+
entry:
|
|
102
|
+
| { kind: "absent" }
|
|
103
|
+
| ({ kind: "directory" } & FilesystemMetadata)
|
|
104
|
+
| ({ kind: "regular-file" } & FilesystemMetadata)
|
|
105
|
+
| ({ kind: "unsupported" } & FilesystemMetadata)
|
|
106
|
+
| ({ kind: "symlink"; target: string; resolvedTarget: string } & FilesystemMetadata)
|
|
107
|
+
| { kind: "inspection-error"; error: DiagnosticError };
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type ApplyPatchDiagnosticsOutcome = {
|
|
111
|
+
status: "failed";
|
|
112
|
+
durationMs: number;
|
|
113
|
+
error: DiagnosticError;
|
|
114
|
+
details?: ApplyPatchDetails;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type PreparedApplyPatchDiagnostics = {
|
|
118
|
+
diagnosticsDirectory: string;
|
|
119
|
+
directory: string;
|
|
120
|
+
reference: ApplyPatchDiagnosticsReference;
|
|
121
|
+
request: Record<string, unknown> & { snapshots: FileSnapshot[] };
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const MAX_ERROR_CAUSE_DEPTH = 8;
|
|
125
|
+
|
|
126
|
+
function diagnosticError(error: unknown, seen = new Set<unknown>(), depth = 0): DiagnosticError {
|
|
127
|
+
if (seen.has(error)) {
|
|
128
|
+
return {
|
|
129
|
+
name: "CircularErrorCause",
|
|
130
|
+
message: "Error cause cycle omitted.",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (depth >= MAX_ERROR_CAUSE_DEPTH) {
|
|
134
|
+
return {
|
|
135
|
+
name: "TruncatedErrorCause",
|
|
136
|
+
message: `Error cause depth exceeded ${MAX_ERROR_CAUSE_DEPTH}.`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (!(error instanceof Error)) {
|
|
140
|
+
return {
|
|
141
|
+
name: "NonErrorThrown",
|
|
142
|
+
message: String(error),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
seen.add(error);
|
|
146
|
+
const diagnostic: DiagnosticError = {
|
|
147
|
+
name: error.name,
|
|
148
|
+
message: error.message,
|
|
149
|
+
};
|
|
150
|
+
if (error.stack) diagnostic.stack = error.stack;
|
|
151
|
+
const code = nodeErrorCode(error);
|
|
152
|
+
if (code) diagnostic.code = code;
|
|
153
|
+
const errno: unknown = Reflect.get(error, "errno");
|
|
154
|
+
if (typeof errno === "number" || typeof errno === "string") diagnostic.errno = errno;
|
|
155
|
+
for (const [source, target] of [
|
|
156
|
+
["syscall", "syscall"],
|
|
157
|
+
["path", "path"],
|
|
158
|
+
["dest", "destination"],
|
|
159
|
+
] as const) {
|
|
160
|
+
const value: unknown = Reflect.get(error, source);
|
|
161
|
+
if (typeof value === "string") diagnostic[target] = value;
|
|
162
|
+
}
|
|
163
|
+
if (error.cause !== undefined) {
|
|
164
|
+
diagnostic.cause = diagnosticError(error.cause, seen, depth + 1);
|
|
165
|
+
}
|
|
166
|
+
return diagnostic;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function filesystemMetadata(metadata: Stats): FilesystemMetadata {
|
|
170
|
+
return {
|
|
171
|
+
mode: metadata.mode & 0o7777,
|
|
172
|
+
modifiedMs: metadata.mtimeMs,
|
|
173
|
+
size: metadata.size,
|
|
174
|
+
device: metadata.dev,
|
|
175
|
+
inode: metadata.ino,
|
|
176
|
+
linkCount: metadata.nlink,
|
|
177
|
+
userId: metadata.uid,
|
|
178
|
+
groupId: metadata.gid,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
type CompatibilityVersionResult = {
|
|
183
|
+
version: string;
|
|
184
|
+
error?: DiagnosticError;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
let compatibilityVersionPromise: Promise<CompatibilityVersionResult> | undefined;
|
|
188
|
+
|
|
189
|
+
function compatibilityVersion(): Promise<CompatibilityVersionResult> {
|
|
190
|
+
compatibilityVersionPromise ??= readFile(new URL("../../package.json", import.meta.url), "utf8")
|
|
191
|
+
.then((source) => {
|
|
192
|
+
const manifest: unknown = JSON.parse(source);
|
|
193
|
+
const version = isObject(manifest) ? manifest["version"] : undefined;
|
|
194
|
+
return { version: typeof version === "string" ? version : "unknown" };
|
|
195
|
+
})
|
|
196
|
+
.catch((error: unknown) => ({
|
|
197
|
+
version: "unknown",
|
|
198
|
+
error: diagnosticError(error),
|
|
199
|
+
}));
|
|
200
|
+
return compatibilityVersionPromise;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function processMetadata(): Record<string, unknown> {
|
|
204
|
+
const metadata: Record<string, unknown> = {
|
|
205
|
+
id: process.pid,
|
|
206
|
+
workingDirectory: process.cwd(),
|
|
207
|
+
umask: process.umask(),
|
|
208
|
+
};
|
|
209
|
+
for (const [key, value] of [
|
|
210
|
+
["userId", typeof process.getuid === "function" ? process.getuid() : undefined],
|
|
211
|
+
["effectiveUserId", typeof process.geteuid === "function" ? process.geteuid() : undefined],
|
|
212
|
+
["groupId", typeof process.getgid === "function" ? process.getgid() : undefined],
|
|
213
|
+
["effectiveGroupId", typeof process.getegid === "function" ? process.getegid() : undefined],
|
|
214
|
+
] as const) {
|
|
215
|
+
if (value !== undefined) metadata[key] = value;
|
|
216
|
+
}
|
|
217
|
+
return metadata;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function runtimeMetadata(): Promise<Record<string, unknown>> {
|
|
221
|
+
const compatibility = await compatibilityVersion();
|
|
222
|
+
return {
|
|
223
|
+
compatibilityVersion: compatibility.version,
|
|
224
|
+
...(compatibility.error ? { compatibilityVersionError: compatibility.error } : {}),
|
|
225
|
+
piVersion: PI_VERSION,
|
|
226
|
+
nodeVersion: process.version,
|
|
227
|
+
operatingSystem: {
|
|
228
|
+
platform: platform(),
|
|
229
|
+
release: release(),
|
|
230
|
+
architecture: arch(),
|
|
231
|
+
},
|
|
232
|
+
process: processMetadata(),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function safePathSegment(value: string): string {
|
|
237
|
+
const safe = value.replace(/[^0-9A-Za-z._-]/gu, "_");
|
|
238
|
+
return safe || "unknown-session";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function snapshotContent(content: Buffer): SnapshotContent {
|
|
242
|
+
const metadata = {
|
|
243
|
+
byteLength: content.byteLength,
|
|
244
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
245
|
+
};
|
|
246
|
+
try {
|
|
247
|
+
return {
|
|
248
|
+
encoding: "utf8",
|
|
249
|
+
data: new TextDecoder("utf-8", { fatal: true }).decode(content),
|
|
250
|
+
...metadata,
|
|
251
|
+
};
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (!(error instanceof TypeError)) throw error;
|
|
254
|
+
return {
|
|
255
|
+
encoding: "binary",
|
|
256
|
+
...metadata,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function fileSnapshot(
|
|
262
|
+
absolutePath: string,
|
|
263
|
+
references: SnapshotReference[],
|
|
264
|
+
): Promise<FileSnapshot> {
|
|
265
|
+
try {
|
|
266
|
+
const metadata = await lstat(absolutePath);
|
|
267
|
+
const filesystem = filesystemMetadata(metadata);
|
|
268
|
+
if (metadata.isFile()) {
|
|
269
|
+
return {
|
|
270
|
+
absolutePath,
|
|
271
|
+
references,
|
|
272
|
+
entry: {
|
|
273
|
+
kind: "regular-file",
|
|
274
|
+
...filesystem,
|
|
275
|
+
content: snapshotContent(await readFile(absolutePath)),
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (metadata.isSymbolicLink()) {
|
|
280
|
+
const target = await readlink(absolutePath);
|
|
281
|
+
const resolvedTarget = isAbsolute(target)
|
|
282
|
+
? resolve(target)
|
|
283
|
+
: resolve(dirname(absolutePath), target);
|
|
284
|
+
const entry: Extract<FileSnapshot["entry"], { kind: "symlink" }> = {
|
|
285
|
+
kind: "symlink",
|
|
286
|
+
...filesystem,
|
|
287
|
+
target,
|
|
288
|
+
resolvedTarget,
|
|
289
|
+
};
|
|
290
|
+
try {
|
|
291
|
+
entry.content = snapshotContent(await readFile(absolutePath));
|
|
292
|
+
} catch (error) {
|
|
293
|
+
entry.contentError = diagnosticError(error);
|
|
294
|
+
}
|
|
295
|
+
return { absolutePath, references, entry };
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
absolutePath,
|
|
299
|
+
references,
|
|
300
|
+
entry: metadata.isDirectory()
|
|
301
|
+
? { kind: "directory", ...filesystem }
|
|
302
|
+
: { kind: "unsupported", ...filesystem },
|
|
303
|
+
};
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (nodeErrorCode(error) === "ENOENT") {
|
|
306
|
+
return { absolutePath, references, entry: { kind: "absent" } };
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
absolutePath,
|
|
310
|
+
references,
|
|
311
|
+
entry: { kind: "inspection-error", error: diagnosticError(error) },
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function parentSnapshot(absolutePath: string): Promise<ParentSnapshot> {
|
|
317
|
+
try {
|
|
318
|
+
const metadata = await lstat(absolutePath);
|
|
319
|
+
const filesystem = filesystemMetadata(metadata);
|
|
320
|
+
if (metadata.isSymbolicLink()) {
|
|
321
|
+
const target = await readlink(absolutePath);
|
|
322
|
+
return {
|
|
323
|
+
absolutePath,
|
|
324
|
+
entry: {
|
|
325
|
+
kind: "symlink",
|
|
326
|
+
...filesystem,
|
|
327
|
+
target,
|
|
328
|
+
resolvedTarget: isAbsolute(target)
|
|
329
|
+
? resolve(target)
|
|
330
|
+
: resolve(dirname(absolutePath), target),
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
absolutePath,
|
|
336
|
+
entry: metadata.isDirectory()
|
|
337
|
+
? { kind: "directory", ...filesystem }
|
|
338
|
+
: metadata.isFile()
|
|
339
|
+
? { kind: "regular-file", ...filesystem }
|
|
340
|
+
: { kind: "unsupported", ...filesystem },
|
|
341
|
+
};
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (nodeErrorCode(error) === "ENOENT") {
|
|
344
|
+
return { absolutePath, entry: { kind: "absent" } };
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
absolutePath,
|
|
348
|
+
entry: { kind: "inspection-error", error: diagnosticError(error) },
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function parentPaths(paths: Iterable<string>): string[] {
|
|
354
|
+
const parents = new Set<string>();
|
|
355
|
+
for (const path of paths) {
|
|
356
|
+
let parent = dirname(path);
|
|
357
|
+
while (!parents.has(parent)) {
|
|
358
|
+
parents.add(parent);
|
|
359
|
+
const next = dirname(parent);
|
|
360
|
+
if (next === parent) break;
|
|
361
|
+
parent = next;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return [...parents];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function diagnosticOperations(patch: string):
|
|
368
|
+
| { status: "parsed"; operations: PatchOperation[] }
|
|
369
|
+
| {
|
|
370
|
+
status: "parse-error";
|
|
371
|
+
error: DiagnosticError;
|
|
372
|
+
instructions: ReturnType<typeof scanPatchInstructions>;
|
|
373
|
+
} {
|
|
374
|
+
try {
|
|
375
|
+
return {
|
|
376
|
+
status: "parsed",
|
|
377
|
+
operations: parsePatchDocument(patch).operations,
|
|
378
|
+
};
|
|
379
|
+
} catch (error) {
|
|
380
|
+
return {
|
|
381
|
+
status: "parse-error",
|
|
382
|
+
error: diagnosticError(error),
|
|
383
|
+
instructions: scanPatchInstructions(patch),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function snapshotReferences(
|
|
389
|
+
cwd: string,
|
|
390
|
+
parsed: ReturnType<typeof diagnosticOperations>,
|
|
391
|
+
): Map<string, SnapshotReference[]> {
|
|
392
|
+
const paths = new Map<string, SnapshotReference[]>();
|
|
393
|
+
const add = (instruction: number, role: SnapshotReference["role"], patchPath: string): void => {
|
|
394
|
+
const absolutePath = isAbsolute(patchPath) ? resolve(patchPath) : resolve(cwd, patchPath);
|
|
395
|
+
const references = paths.get(absolutePath) ?? [];
|
|
396
|
+
references.push({ instruction, role, patchPath });
|
|
397
|
+
paths.set(absolutePath, references);
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
if (parsed.status === "parsed") {
|
|
401
|
+
for (const [index, operation] of parsed.operations.entries()) {
|
|
402
|
+
add(index + 1, operation.kind === "add" ? "destination" : "source", operation.path);
|
|
403
|
+
if (operation.kind === "update" && operation.moveTo) {
|
|
404
|
+
add(index + 1, "destination", operation.moveTo);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
for (const instruction of parsed.instructions) {
|
|
409
|
+
add(
|
|
410
|
+
instruction.index,
|
|
411
|
+
instruction.kind === "add" ? "destination" : "source",
|
|
412
|
+
instruction.path,
|
|
413
|
+
);
|
|
414
|
+
if (instruction.moveTo) add(instruction.index, "destination", instruction.moveTo);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return paths;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function optionalString(record: Record<string, unknown>, key: string): string | undefined {
|
|
421
|
+
const value = record[key];
|
|
422
|
+
return typeof value === "string" ? value : undefined;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function requestIdentifiers(
|
|
426
|
+
context: ApplyPatchDiagnosticsContext,
|
|
427
|
+
toolCallId: string,
|
|
428
|
+
): Record<string, unknown> {
|
|
429
|
+
const separator = toolCallId.indexOf("|");
|
|
430
|
+
const identifiers: Record<string, unknown> = {
|
|
431
|
+
toolCallId,
|
|
432
|
+
providerCallId: separator < 0 ? toolCallId : toolCallId.slice(0, separator),
|
|
433
|
+
};
|
|
434
|
+
if (separator >= 0 && separator < toolCallId.length - 1) {
|
|
435
|
+
identifiers["providerItemId"] = toolCallId.slice(separator + 1);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const entry of context.sessionManager.getBranch().toReversed()) {
|
|
439
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
440
|
+
const hasCall = entry.message.content.some(
|
|
441
|
+
(block) => block.type === "toolCall" && block.id === toolCallId,
|
|
442
|
+
);
|
|
443
|
+
if (!hasCall) continue;
|
|
444
|
+
|
|
445
|
+
identifiers["assistantEntryId"] = entry.id;
|
|
446
|
+
identifiers["provider"] = entry.message.provider;
|
|
447
|
+
identifiers["api"] = entry.message.api;
|
|
448
|
+
identifiers["model"] = entry.message.model;
|
|
449
|
+
if (entry.message.responseId) identifiers["responseId"] = entry.message.responseId;
|
|
450
|
+
|
|
451
|
+
const transportDiagnostic = entry.message.diagnostics?.findLast(
|
|
452
|
+
(diagnostic) => diagnostic.type === "codex_transport_request",
|
|
453
|
+
);
|
|
454
|
+
if (!isObject(transportDiagnostic?.details)) break;
|
|
455
|
+
const details = transportDiagnostic.details;
|
|
456
|
+
for (const key of [
|
|
457
|
+
"turnId",
|
|
458
|
+
"threadId",
|
|
459
|
+
"windowId",
|
|
460
|
+
"sessionId",
|
|
461
|
+
"promptCacheKey",
|
|
462
|
+
"previousResponseId",
|
|
463
|
+
]) {
|
|
464
|
+
const value = optionalString(details, key);
|
|
465
|
+
if (value !== undefined) identifiers[key] = value;
|
|
466
|
+
}
|
|
467
|
+
if (isObject(details["cacheIdentity"])) {
|
|
468
|
+
const cacheIdentity = details["cacheIdentity"];
|
|
469
|
+
for (const [source, target] of [
|
|
470
|
+
["clientRequestHeader", "clientRequestId"],
|
|
471
|
+
["sessionHeader", "sessionHeader"],
|
|
472
|
+
["threadHeader", "threadHeader"],
|
|
473
|
+
] as const) {
|
|
474
|
+
const value = optionalString(cacheIdentity, source);
|
|
475
|
+
if (value !== undefined) identifiers[target] = value;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
return identifiers;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function privateDirectory(path: string): Promise<void> {
|
|
484
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
485
|
+
await chmod(path, 0o700);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function writePrivateJson(path: string, value: unknown): Promise<void> {
|
|
489
|
+
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
490
|
+
try {
|
|
491
|
+
await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, {
|
|
492
|
+
encoding: "utf8",
|
|
493
|
+
flag: "wx",
|
|
494
|
+
mode: 0o600,
|
|
495
|
+
});
|
|
496
|
+
await rename(temporaryPath, path);
|
|
497
|
+
} finally {
|
|
498
|
+
await rm(temporaryPath, { force: true });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export async function prepareApplyPatchDiagnostics(
|
|
503
|
+
context: ApplyPatchDiagnosticsContext,
|
|
504
|
+
toolCallId: string,
|
|
505
|
+
patch: string,
|
|
506
|
+
agentDir = getAgentDir(),
|
|
507
|
+
): Promise<PreparedApplyPatchDiagnostics> {
|
|
508
|
+
const capturedAt = new Date().toISOString();
|
|
509
|
+
const recordId = randomUUID();
|
|
510
|
+
const sessionId = context.sessionManager.getSessionId();
|
|
511
|
+
const diagnosticsDirectory = join(agentDir, APPLY_PATCH_DIAGNOSTICS_DIRECTORY);
|
|
512
|
+
const directory = join(diagnosticsDirectory, safePathSegment(sessionId));
|
|
513
|
+
|
|
514
|
+
const prefix = `${capturedAt.replace(/[:.]/gu, "-")}_${recordId}`;
|
|
515
|
+
const requestPath = join(directory, `${prefix}.request.json`);
|
|
516
|
+
const resultPath = join(directory, `${prefix}.result.json`);
|
|
517
|
+
const parsed = diagnosticOperations(patch);
|
|
518
|
+
const references = snapshotReferences(context.cwd, parsed);
|
|
519
|
+
const [runtime, snapshots, parents] = await Promise.all([
|
|
520
|
+
runtimeMetadata(),
|
|
521
|
+
Promise.all(
|
|
522
|
+
[...references].map(([path, pathReferences]) => fileSnapshot(path, pathReferences)),
|
|
523
|
+
),
|
|
524
|
+
Promise.all(parentPaths(references.keys()).map(parentSnapshot)),
|
|
525
|
+
]);
|
|
526
|
+
const sessionFile = context.sessionManager.getSessionFile();
|
|
527
|
+
const request = {
|
|
528
|
+
schemaVersion: APPLY_PATCH_DIAGNOSTICS_SCHEMA_VERSION,
|
|
529
|
+
recordId,
|
|
530
|
+
capturedAt,
|
|
531
|
+
cwd: context.cwd,
|
|
532
|
+
runtime,
|
|
533
|
+
session: {
|
|
534
|
+
id: sessionId,
|
|
535
|
+
...(sessionFile ? { file: sessionFile } : {}),
|
|
536
|
+
},
|
|
537
|
+
request: requestIdentifiers(context, toolCallId),
|
|
538
|
+
patch,
|
|
539
|
+
parsed,
|
|
540
|
+
snapshots,
|
|
541
|
+
parents,
|
|
542
|
+
};
|
|
543
|
+
return {
|
|
544
|
+
diagnosticsDirectory,
|
|
545
|
+
directory,
|
|
546
|
+
reference: { recordId, requestPath, resultPath },
|
|
547
|
+
request,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export async function writeApplyPatchDiagnosticsRequest(
|
|
552
|
+
prepared: PreparedApplyPatchDiagnostics,
|
|
553
|
+
): Promise<void> {
|
|
554
|
+
await privateDirectory(prepared.diagnosticsDirectory);
|
|
555
|
+
await privateDirectory(prepared.directory);
|
|
556
|
+
await writePrivateJson(prepared.reference.requestPath, prepared.request);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export async function writeApplyPatchDiagnosticsOutcome(
|
|
560
|
+
reference: ApplyPatchDiagnosticsReference,
|
|
561
|
+
outcome: ApplyPatchDiagnosticsOutcome,
|
|
562
|
+
): Promise<void> {
|
|
563
|
+
await writePrivateJson(reference.resultPath, {
|
|
564
|
+
schemaVersion: APPLY_PATCH_DIAGNOSTICS_SCHEMA_VERSION,
|
|
565
|
+
recordId: reference.recordId,
|
|
566
|
+
completedAt: new Date().toISOString(),
|
|
567
|
+
...outcome,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function applyPatchDiagnosticError(error: unknown): DiagnosticError {
|
|
572
|
+
return diagnosticError(error);
|
|
573
|
+
}
|