codex-relay 1.0.2 → 1.0.4
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/dist/api-schema.js +2 -2
- package/dist/api-schema2.js +178 -2
- package/dist/cli.js +1 -1
- package/dist/src.js +611 -41
- package/package.json +1 -1
- package/src/api-schema.ts +174 -0
package/dist/src.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { A as PairResponseSchema,
|
|
1
|
+
import { A as PairResponseSchema, C as ListQueuedThreadInputsResponseSchema, D as ListWorkspaceFilesResponseSchema, Dt as apiPaths, E as ListWorkspaceDirectoriesResponseSchema, G as ResolveApprovalRequestSchema, K as ResolveApprovalResponseSchema, Lt as stripPromptSkillMentions, Mt as promptMarkdownWithSkills, Ot as chatMessageDetailsFromPromptContext, Q as RuntimePreferencesSchema, S as ListModelsResponseSchema, St as WorkspaceGitActionResponseSchema, T as ListThreadsResponseSchema, U as RateLimitsResponseSchema, X as RuntimePreferencesByWorkspacePathSchema, Z as RuntimePreferencesResponseSchema, _ as EncryptedPayloadSchema, a as ArchiveThreadResponseSchema, at as ThreadContextWindowResponseSchema, b as InterruptThreadRunResponseSchema, bt as WorkspaceFileContentResponseSchema, ct as ThreadMessageDetailResponseSchema, d as CheckoutWorkspaceBranchRequestSchema, dt as ThreadSummarySchema, et as StatusResponseSchema, ft as UpdateRuntimePreferencesRequestSchema, h as CreateThreadRequestSchema, jt as normalizePromptContext, k as PairRequestSchema, kt as createOpenApiDocument, l as ChatMessageSchema, m as ContextWindowUsageSchema, mt as VersionResponseSchema, nt as StreamThreadRunRequestSchema, ot as ThreadDetailResponseSchema, p as CommitPushWorkspaceRequestSchema, pt as UpdateWorkspaceFileContentRequestSchema, q as RunThreadRequestSchema, rt as SubmitThreadInputResponseSchema, st as ThreadMessageDetailFieldSchema, tt as StreamThreadRunEventSchema, vt as WorkspaceChangesResponseSchema, w as ListSkillsResponseSchema, y as ImageAttachmentUploadResponseSchema, z as QueuedThreadInputActionResponseSchema } from "./api-schema2.js";
|
|
2
2
|
import { r as legacyCodexRelayDataPath, t as codexRelayDataPath } from "./paths.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import qrcode from "qrcode-terminal";
|
|
5
5
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
6
6
|
import fs, { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { access, appendFile, copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
8
|
-
import path, { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
7
|
+
import { access, appendFile, copyFile, mkdir, open, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import path, { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import os, { homedir, hostname, networkInterfaces } from "node:os";
|
|
@@ -539,14 +539,16 @@ function updatePreferencesFile(current, patch, workspacePath) {
|
|
|
539
539
|
});
|
|
540
540
|
}
|
|
541
541
|
function mergeRuntimePreferences(current, patch) {
|
|
542
|
-
const { model, reasoningEffort, threadId: _threadId, workspacePath: _workspacePath, ...rest } = UpdateRuntimePreferencesRequestSchema.parse(patch);
|
|
542
|
+
const { model, serviceTier, reasoningEffort, threadId: _threadId, workspacePath: _workspacePath, ...rest } = UpdateRuntimePreferencesRequestSchema.parse(patch);
|
|
543
543
|
const next = {
|
|
544
544
|
...current,
|
|
545
545
|
...rest,
|
|
546
546
|
...model === null ? {} : { model },
|
|
547
|
+
...serviceTier === null ? {} : { serviceTier },
|
|
547
548
|
...reasoningEffort === null ? {} : { reasoningEffort }
|
|
548
549
|
};
|
|
549
550
|
if (model === null) delete next.model;
|
|
551
|
+
if (serviceTier === null) delete next.serviceTier;
|
|
550
552
|
if (reasoningEffort === null) delete next.reasoningEffort;
|
|
551
553
|
return RuntimePreferencesSchema.parse({ ...next });
|
|
552
554
|
}
|
|
@@ -798,6 +800,9 @@ const defaultWorkspacePath = process.cwd();
|
|
|
798
800
|
const defaultCodexModel = "gpt-5.5";
|
|
799
801
|
const execFileAsync = promisify(execFile);
|
|
800
802
|
const IMAGE_ATTACHMENT_MAX_BYTES = 8 * 1024 * 1024;
|
|
803
|
+
const WORKSPACE_FILE_PREVIEW_MAX_BYTES = 256 * 1024;
|
|
804
|
+
const LOCAL_MARKDOWN_IMAGE_PATTERN = /!\[([^\]]*)\]\(([^)]*)\)/g;
|
|
805
|
+
const LOCAL_IMAGE_REFERENCE_PATTERN = /\.(gif|heic|heif|jpe?g|png|webp)$/i;
|
|
801
806
|
const imageAttachmentDirectory = codexRelayDataPath("attachments/images");
|
|
802
807
|
const relayPackage = createRequire(import.meta.url)("../package.json");
|
|
803
808
|
const collaborationModeTemplateNames = [
|
|
@@ -1157,8 +1162,12 @@ function createApp(options = {}) {
|
|
|
1157
1162
|
const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, c.req.query("workspacePath"));
|
|
1158
1163
|
if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
|
|
1159
1164
|
const query = c.req.query("query")?.trim() ?? "";
|
|
1165
|
+
const directoryPath = normalizeWorkspaceDirectoryPath(c.req.query("directory") ?? "");
|
|
1166
|
+
if (!directoryPath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_file_path", directoryPath.error), 400);
|
|
1160
1167
|
const response = ListWorkspaceFilesResponseSchema.parse({
|
|
1161
|
-
|
|
1168
|
+
directory: directoryPath.path,
|
|
1169
|
+
files: await listWorkspaceFiles(selectedWorkspacePath.path, query, directoryPath.path),
|
|
1170
|
+
parentDirectory: parentWorkspaceDirectory(directoryPath.path),
|
|
1162
1171
|
query,
|
|
1163
1172
|
workspacePath: selectedWorkspacePath.path
|
|
1164
1173
|
});
|
|
@@ -1167,6 +1176,40 @@ function createApp(options = {}) {
|
|
|
1167
1176
|
return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_files_unavailable", errorMessage(error)), 502);
|
|
1168
1177
|
}
|
|
1169
1178
|
});
|
|
1179
|
+
app.get(apiPaths.workspaceFileContent, async (c) => {
|
|
1180
|
+
try {
|
|
1181
|
+
const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, c.req.query("workspacePath"));
|
|
1182
|
+
if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
|
|
1183
|
+
const requestedPath = c.req.query("path")?.trim();
|
|
1184
|
+
if (!requestedPath) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("missing_workspace_file_path", "Workspace file path is required."), 400);
|
|
1185
|
+
const file = await readWorkspaceFileContent(selectedWorkspacePath.path, requestedPath);
|
|
1186
|
+
if (!file.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError(file.code, file.error), file.status);
|
|
1187
|
+
const response = WorkspaceFileContentResponseSchema.parse({
|
|
1188
|
+
workspacePath: selectedWorkspacePath.path,
|
|
1189
|
+
...file.content
|
|
1190
|
+
});
|
|
1191
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_file_unavailable", errorMessage(error)), 502);
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
app.put(apiPaths.workspaceFileContent, async (c) => {
|
|
1197
|
+
const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, UpdateWorkspaceFileContentRequestSchema);
|
|
1198
|
+
if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
|
|
1199
|
+
try {
|
|
1200
|
+
const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, parsed.data.workspacePath);
|
|
1201
|
+
if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
|
|
1202
|
+
const file = await updateWorkspaceFileContent(selectedWorkspacePath.path, parsed.data);
|
|
1203
|
+
if (!file.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError(file.code, file.error), file.status);
|
|
1204
|
+
const response = WorkspaceFileContentResponseSchema.parse({
|
|
1205
|
+
workspacePath: selectedWorkspacePath.path,
|
|
1206
|
+
...file.content
|
|
1207
|
+
});
|
|
1208
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_file_update_unavailable", errorMessage(error)), 502);
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1170
1213
|
app.get(apiPaths.rateLimits, async (c) => {
|
|
1171
1214
|
if (!appServer) return secureJson(c, options.pairing, secureSessionsByTokenHash, RateLimitsResponseSchema.parse({ buckets: [] }));
|
|
1172
1215
|
try {
|
|
@@ -1217,14 +1260,24 @@ function createApp(options = {}) {
|
|
|
1217
1260
|
return c.json(apiError("not_found", "Image attachment not found."), 404);
|
|
1218
1261
|
}
|
|
1219
1262
|
const fileStat = statSync(filePath);
|
|
1263
|
+
if (!fileStat.isFile() || fileStat.size === 0) {
|
|
1264
|
+
relayDebugLog("image_attachment.rejected", {
|
|
1265
|
+
attachmentId,
|
|
1266
|
+
reason: fileStat.isFile() ? "empty_file" : "not_file",
|
|
1267
|
+
userAgent: c.req.header("user-agent")
|
|
1268
|
+
});
|
|
1269
|
+
return c.json(apiError("not_found", "Image attachment not found."), 404);
|
|
1270
|
+
}
|
|
1271
|
+
const imageBytes = await readFile(filePath);
|
|
1220
1272
|
relayDebugLog("image_attachment.served", {
|
|
1221
1273
|
attachmentId,
|
|
1222
1274
|
mimeType: imageMimeType(filePath),
|
|
1223
|
-
size:
|
|
1275
|
+
size: imageBytes.length,
|
|
1224
1276
|
userAgent: c.req.header("user-agent")
|
|
1225
1277
|
});
|
|
1226
|
-
return new Response(
|
|
1278
|
+
return new Response(imageBytes, { headers: {
|
|
1227
1279
|
"cache-control": "private, max-age=31536000, immutable",
|
|
1280
|
+
"content-length": String(imageBytes.length),
|
|
1228
1281
|
"content-type": imageMimeType(filePath)
|
|
1229
1282
|
} });
|
|
1230
1283
|
});
|
|
@@ -1289,7 +1342,7 @@ function createApp(options = {}) {
|
|
|
1289
1342
|
let loadedMessages = false;
|
|
1290
1343
|
let messages = cachedMessages;
|
|
1291
1344
|
let responseThread = preserveKnownRunningThreadState(mappedThread, wasKnownRunning);
|
|
1292
|
-
const rolloutHistory = readRolloutThreadMessages(threadId);
|
|
1345
|
+
const rolloutHistory = readRolloutThreadMessages(threadId, workspacePath);
|
|
1293
1346
|
if (rolloutHistory.messages.length > 0) {
|
|
1294
1347
|
messages = mergeThreadMessagePages(rolloutHistory.messages, cachedMessages);
|
|
1295
1348
|
responseThread = rememberRolloutThreadMessages(threads, responseThread, messages, rolloutHistory.messageCountLowerBound);
|
|
@@ -1328,7 +1381,7 @@ function createApp(options = {}) {
|
|
|
1328
1381
|
});
|
|
1329
1382
|
}
|
|
1330
1383
|
const cachedMessages = messagesByThreadId.get(threadId) ?? [];
|
|
1331
|
-
const rolloutHistory = readRolloutThreadMessages(threadId);
|
|
1384
|
+
const rolloutHistory = readRolloutThreadMessages(threadId, workspacePath);
|
|
1332
1385
|
const baseThread = threads.get(threadId) ?? knownThread ?? (rolloutHistory.rolloutPath ? rolloutThreadMetadata(threadId, workspacePath, rolloutHistory.rolloutPath, [...rolloutHistory.messages, ...cachedMessages]) : void 0);
|
|
1333
1386
|
if (baseThread && (cachedMessages.length > 0 || rolloutHistory.messages.length > 0)) {
|
|
1334
1387
|
const messages = rolloutHistory.messages.length > 0 ? mergeThreadMessagePages(rolloutHistory.messages, cachedMessages) : dedupeThreadMessages(cachedMessages);
|
|
@@ -1870,7 +1923,8 @@ async function createAppServerThreadRecord(input) {
|
|
|
1870
1923
|
experimentalRawEvents: false,
|
|
1871
1924
|
model: input.options.model ?? null,
|
|
1872
1925
|
persistExtendedHistory: true,
|
|
1873
|
-
sandbox: runtime.sandbox
|
|
1926
|
+
sandbox: runtime.sandbox,
|
|
1927
|
+
serviceTier: input.options.serviceTier ?? null
|
|
1874
1928
|
});
|
|
1875
1929
|
const metadata = ThreadSummarySchema.parse({
|
|
1876
1930
|
...mapAppServerThread({
|
|
@@ -2123,6 +2177,7 @@ async function startAppServerTurn(appServer, threadId, input) {
|
|
|
2123
2177
|
input: appServerTurnInput(input.prompt, input.attachments, input.skills),
|
|
2124
2178
|
model: input.runOptions.model ?? null,
|
|
2125
2179
|
sandboxPolicy: runtime.sandboxPolicy,
|
|
2180
|
+
serviceTier: input.runOptions.serviceTier ?? null,
|
|
2126
2181
|
threadId
|
|
2127
2182
|
};
|
|
2128
2183
|
await resumeAppServerThreadIfNeeded(appServer, threadId, input, runtime);
|
|
@@ -2154,6 +2209,7 @@ async function resumeAppServerThread(appServer, threadId, input, runtime) {
|
|
|
2154
2209
|
model: input.runOptions.model ?? null,
|
|
2155
2210
|
persistExtendedHistory: true,
|
|
2156
2211
|
sandbox: runtime.sandbox,
|
|
2212
|
+
serviceTier: input.runOptions.serviceTier ?? null,
|
|
2157
2213
|
threadId
|
|
2158
2214
|
});
|
|
2159
2215
|
}
|
|
@@ -2517,7 +2573,11 @@ async function runAppServerPromptStreamed(input) {
|
|
|
2517
2573
|
case "item/completed": {
|
|
2518
2574
|
const item = params?.item;
|
|
2519
2575
|
if (!item || typeof item !== "object") return;
|
|
2520
|
-
|
|
2576
|
+
const canonicalUserMessage = replaceDuplicateInitialUserMessage(input.messagesByThreadId, activeThreadId, firstString(params, ["turnId"]) ?? activeTurnId, item, userMessage.id, prompt);
|
|
2577
|
+
if (canonicalUserMessage) {
|
|
2578
|
+
userMessage = canonicalUserMessage;
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2521
2581
|
const turnId = firstString(params, ["turnId"]) ?? activeTurnId;
|
|
2522
2582
|
const message = upsertAppServerItemMessage(input.messagesByThreadId, activeThreadId, turnId, item);
|
|
2523
2583
|
if (!message) return;
|
|
@@ -2750,7 +2810,11 @@ async function runAppServerPromptStreamed(input) {
|
|
|
2750
2810
|
debugStream("start turn complete", activeThreadId, activeTurnId);
|
|
2751
2811
|
let streamedReturnedItem = false;
|
|
2752
2812
|
for (const item of turn.items) {
|
|
2753
|
-
|
|
2813
|
+
const canonicalUserMessage = replaceDuplicateInitialUserMessage(input.messagesByThreadId, activeThreadId, activeTurnId, item, userMessage.id, displayPrompt);
|
|
2814
|
+
if (canonicalUserMessage) {
|
|
2815
|
+
userMessage = canonicalUserMessage;
|
|
2816
|
+
continue;
|
|
2817
|
+
}
|
|
2754
2818
|
const message = upsertAppServerItemMessage(input.messagesByThreadId, activeThreadId, activeTurnId, item);
|
|
2755
2819
|
if (!message) continue;
|
|
2756
2820
|
streamedReturnedItem = true;
|
|
@@ -2900,7 +2964,8 @@ async function recoverMissingAppServerThread(input) {
|
|
|
2900
2964
|
experimentalRawEvents: false,
|
|
2901
2965
|
model: input.runOptions.model ?? null,
|
|
2902
2966
|
persistExtendedHistory: true,
|
|
2903
|
-
sandbox: runtime.sandbox
|
|
2967
|
+
sandbox: runtime.sandbox,
|
|
2968
|
+
serviceTier: input.runOptions.serviceTier ?? null
|
|
2904
2969
|
});
|
|
2905
2970
|
const recoveredThread = mapAppServerThread({
|
|
2906
2971
|
...thread,
|
|
@@ -3062,6 +3127,12 @@ function upsertAppServerItemMessage(messagesByThreadId, threadId, turnId, item)
|
|
|
3062
3127
|
turnId: message.turnId
|
|
3063
3128
|
});
|
|
3064
3129
|
}
|
|
3130
|
+
function replaceDuplicateInitialUserMessage(messagesByThreadId, threadId, turnId, item, localMessageId, prompt) {
|
|
3131
|
+
if (!isDuplicateInitialUserMessage(messagesByThreadId, threadId, item, localMessageId, prompt)) return;
|
|
3132
|
+
const message = mapAppServerItem(threadId, appServerTurnShell(turnId), item);
|
|
3133
|
+
if (!message) return;
|
|
3134
|
+
return replaceMessage(messagesByThreadId, threadId, localMessageId, messageWithReplacementDetail(message, localMessageId));
|
|
3135
|
+
}
|
|
3065
3136
|
function isDuplicateInitialUserMessage(messagesByThreadId, threadId, item, localMessageId, prompt) {
|
|
3066
3137
|
if (item.type !== "userMessage" || !("content" in item) || !Array.isArray(item.content)) return false;
|
|
3067
3138
|
const localMessage = messagesByThreadId.get(threadId)?.find((message) => message.id === localMessageId);
|
|
@@ -3069,6 +3140,15 @@ function isDuplicateInitialUserMessage(messagesByThreadId, threadId, item, local
|
|
|
3069
3140
|
const normalizedPrompt = stripPromptSkillMentions(prompt, skills);
|
|
3070
3141
|
return stripPromptSkillMentions(localMessage?.content ?? "", skills) === normalizedPrompt && stripPromptSkillMentions(appServerUserMessageText(item), skills) === normalizedPrompt;
|
|
3071
3142
|
}
|
|
3143
|
+
function messageWithReplacementDetail(message, replacesMessageId) {
|
|
3144
|
+
return ChatMessageSchema.parse({
|
|
3145
|
+
...message,
|
|
3146
|
+
details: {
|
|
3147
|
+
...message.details,
|
|
3148
|
+
replacesMessageId
|
|
3149
|
+
}
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3072
3152
|
function appServerUserMessageText(item) {
|
|
3073
3153
|
const skills = appServerUserMessageSkills(item);
|
|
3074
3154
|
return promptMarkdownWithSkills(promptWithAppServerImageReferences(item.content.map((content) => {
|
|
@@ -3076,6 +3156,9 @@ function appServerUserMessageText(item) {
|
|
|
3076
3156
|
case "text": return content.text;
|
|
3077
3157
|
case "image":
|
|
3078
3158
|
case "localImage":
|
|
3159
|
+
case "document":
|
|
3160
|
+
case "file":
|
|
3161
|
+
case "localFile":
|
|
3079
3162
|
case "mention":
|
|
3080
3163
|
case "skill": return "";
|
|
3081
3164
|
}
|
|
@@ -3094,14 +3177,90 @@ function appServerUserMessageDetails(item) {
|
|
|
3094
3177
|
type: "image"
|
|
3095
3178
|
}];
|
|
3096
3179
|
case "localImage": return localImageAttachmentDetails(content.path);
|
|
3180
|
+
case "document":
|
|
3181
|
+
case "file":
|
|
3182
|
+
case "localFile": return appServerDocumentAttachmentDetails(content);
|
|
3097
3183
|
default: return [];
|
|
3098
3184
|
}
|
|
3099
3185
|
});
|
|
3100
3186
|
return attachments.length > 0 ? { attachments } : void 0;
|
|
3101
3187
|
}
|
|
3188
|
+
function appServerAgentMessageParts(item) {
|
|
3189
|
+
return agentMessageParts(item.text);
|
|
3190
|
+
}
|
|
3191
|
+
function agentMessageParts(text) {
|
|
3192
|
+
const imageReferences = localMarkdownImageReferences(text);
|
|
3193
|
+
if (imageReferences.length === 0) return {
|
|
3194
|
+
content: text,
|
|
3195
|
+
details: void 0
|
|
3196
|
+
};
|
|
3197
|
+
const materializedReferences = /* @__PURE__ */ new Set();
|
|
3198
|
+
const attachments = imageReferences.flatMap((reference) => {
|
|
3199
|
+
const name = reference.alt || basename(localImageFilePath(reference.destination) ?? reference.destination);
|
|
3200
|
+
const details = localImageAttachmentDetails(reference.destination, name);
|
|
3201
|
+
if (details.length > 0) materializedReferences.add(reference.destination);
|
|
3202
|
+
return details;
|
|
3203
|
+
});
|
|
3204
|
+
return {
|
|
3205
|
+
content: attachments.length > 0 ? stripMaterializedMarkdownImages(text, materializedReferences) : text,
|
|
3206
|
+
details: attachments.length > 0 ? { attachments } : void 0
|
|
3207
|
+
};
|
|
3208
|
+
}
|
|
3209
|
+
function localMarkdownImageReferences(markdown) {
|
|
3210
|
+
const references = [];
|
|
3211
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3212
|
+
for (const match of markdown.matchAll(LOCAL_MARKDOWN_IMAGE_PATTERN)) {
|
|
3213
|
+
const destination = markdownImageDestination(match[2] ?? "");
|
|
3214
|
+
if (!destination || seen.has(destination) || !isLocalMarkdownImageReference(destination)) continue;
|
|
3215
|
+
seen.add(destination);
|
|
3216
|
+
references.push({
|
|
3217
|
+
alt: (match[1] ?? "").trim(),
|
|
3218
|
+
destination
|
|
3219
|
+
});
|
|
3220
|
+
}
|
|
3221
|
+
return references;
|
|
3222
|
+
}
|
|
3223
|
+
function stripMaterializedMarkdownImages(markdown, destinations) {
|
|
3224
|
+
return markdown.replace(LOCAL_MARKDOWN_IMAGE_PATTERN, (match, _alt, rawDestination) => {
|
|
3225
|
+
const destination = markdownImageDestination(rawDestination);
|
|
3226
|
+
return destination && destinations.has(destination) ? "" : match;
|
|
3227
|
+
}).replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
3228
|
+
}
|
|
3229
|
+
function markdownImageDestination(value) {
|
|
3230
|
+
let destination = value.trim();
|
|
3231
|
+
if (destination.startsWith("<")) {
|
|
3232
|
+
const endIndex = destination.indexOf(">");
|
|
3233
|
+
if (endIndex > 0) destination = destination.slice(1, endIndex);
|
|
3234
|
+
} else {
|
|
3235
|
+
const titleIndex = destination.search(/\s+["']/);
|
|
3236
|
+
if (titleIndex > 0) destination = destination.slice(0, titleIndex);
|
|
3237
|
+
}
|
|
3238
|
+
try {
|
|
3239
|
+
return decodeURI(destination);
|
|
3240
|
+
} catch {
|
|
3241
|
+
return destination;
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
function isLocalMarkdownImageReference(reference) {
|
|
3245
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(reference) && !reference.startsWith("file://")) return false;
|
|
3246
|
+
const filePath = localImageFilePath(reference);
|
|
3247
|
+
return Boolean(filePath && LOCAL_IMAGE_REFERENCE_PATTERN.test(filePath));
|
|
3248
|
+
}
|
|
3249
|
+
function appServerDocumentAttachmentDetails(input) {
|
|
3250
|
+
const reference = input.path ?? input.url;
|
|
3251
|
+
if (!reference) return [];
|
|
3252
|
+
return [{
|
|
3253
|
+
mimeType: input.mimeType,
|
|
3254
|
+
name: input.name ?? basename(reference),
|
|
3255
|
+
path: input.path,
|
|
3256
|
+
type: "document",
|
|
3257
|
+
url: input.url
|
|
3258
|
+
}];
|
|
3259
|
+
}
|
|
3102
3260
|
async function saveUploadedImageAttachment(file) {
|
|
3103
3261
|
if (!file.type.startsWith("image/")) throw new Error(`Unsupported attachment type: ${file.type || "unknown"}`);
|
|
3104
3262
|
if (file.size > IMAGE_ATTACHMENT_MAX_BYTES) throw new Error(`Image ${file.name || "attachment"} is too large.`);
|
|
3263
|
+
if (file.size === 0) throw new Error(`Image ${file.name || "attachment"} is empty.`);
|
|
3105
3264
|
const attachmentId = `${Date.now()}-${randomUUID()}${imageExtension(file.name, file.type)}`;
|
|
3106
3265
|
const filePath = resolve(imageAttachmentDirectory, attachmentId);
|
|
3107
3266
|
await mkdir(imageAttachmentDirectory, { recursive: true });
|
|
@@ -3152,7 +3311,7 @@ function materializeLocalImageFile(path, name = basename(path)) {
|
|
|
3152
3311
|
const filePath = localImageFilePath(path);
|
|
3153
3312
|
if (!filePath || !existsSync(filePath)) return;
|
|
3154
3313
|
const fileStat = statSync(filePath);
|
|
3155
|
-
if (!fileStat.isFile() || fileStat.size > IMAGE_ATTACHMENT_MAX_BYTES) return;
|
|
3314
|
+
if (!fileStat.isFile() || fileStat.size === 0 || fileStat.size > IMAGE_ATTACHMENT_MAX_BYTES) return;
|
|
3156
3315
|
const buffer = readFileSync(filePath);
|
|
3157
3316
|
const mimeType = imageMimeType(filePath);
|
|
3158
3317
|
const attachmentId = `${createHash("sha256").update(buffer).digest("hex").slice(0, 24)}${imageExtension(name, mimeType)}`;
|
|
@@ -3268,6 +3427,17 @@ function updateMessage(messagesByThreadId, threadId, messageId, update) {
|
|
|
3268
3427
|
messages[index] = next;
|
|
3269
3428
|
return next;
|
|
3270
3429
|
}
|
|
3430
|
+
function replaceMessage(messagesByThreadId, threadId, messageId, replacement) {
|
|
3431
|
+
const messages = messagesByThreadId.get(threadId) ?? [];
|
|
3432
|
+
const index = messages.findIndex((message) => message.id === messageId);
|
|
3433
|
+
if (index === -1) throw new Error(`Unknown message: ${messageId}`);
|
|
3434
|
+
const next = ChatMessageSchema.parse({
|
|
3435
|
+
...replacement,
|
|
3436
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3437
|
+
});
|
|
3438
|
+
messages[index] = next;
|
|
3439
|
+
return next;
|
|
3440
|
+
}
|
|
3271
3441
|
function replaceLocalThreadId(threads, messagesByThreadId, liveThreads, currentThreadId, sdkThreadId) {
|
|
3272
3442
|
if (!sdkThreadId || sdkThreadId === currentThreadId) return currentThreadId;
|
|
3273
3443
|
const metadata = threads.get(currentThreadId);
|
|
@@ -3511,6 +3681,7 @@ function runtimeMetadataFromOptions(options) {
|
|
|
3511
3681
|
const sandboxMode = options.sandboxMode ?? runtime.sandboxMode;
|
|
3512
3682
|
return {
|
|
3513
3683
|
...options.model ? { model: options.model } : {},
|
|
3684
|
+
...options.serviceTier ? { serviceTier: options.serviceTier } : {},
|
|
3514
3685
|
...options.runtimeMode ? { runtimeMode: options.runtimeMode } : {},
|
|
3515
3686
|
...options.collaborationMode ? { collaborationMode: options.collaborationMode } : {},
|
|
3516
3687
|
...approvalPolicy ? { approvalPolicy } : {},
|
|
@@ -3523,6 +3694,7 @@ function withRuntimePreferences(preferences, options) {
|
|
|
3523
3694
|
...options,
|
|
3524
3695
|
approvalPolicy: options.approvalPolicy,
|
|
3525
3696
|
model: options.model ?? preferences.model,
|
|
3697
|
+
serviceTier: options.serviceTier ?? preferences.serviceTier,
|
|
3526
3698
|
reasoningEffort: options.reasoningEffort ?? preferences.reasoningEffort,
|
|
3527
3699
|
runtimeMode: options.runtimeMode ?? preferences.runtimeMode,
|
|
3528
3700
|
sandboxMode: options.sandboxMode
|
|
@@ -3570,7 +3742,7 @@ function sandboxPolicyForMode(sandboxMode, workspacePath) {
|
|
|
3570
3742
|
};
|
|
3571
3743
|
}
|
|
3572
3744
|
function hasExplicitRunOptions(options) {
|
|
3573
|
-
return Boolean(options.model || options.reasoningEffort || options.approvalPolicy || options.sandboxMode || options.collaborationMode === "plan" || options.runtimeMode);
|
|
3745
|
+
return Boolean(options.model || options.serviceTier || options.reasoningEffort || options.approvalPolicy || options.sandboxMode || options.collaborationMode === "plan" || options.runtimeMode);
|
|
3574
3746
|
}
|
|
3575
3747
|
function mapAppServerThread(thread, fallbackMessageCount) {
|
|
3576
3748
|
const createdAt = fromUnixSeconds(thread.createdAt);
|
|
@@ -3628,26 +3800,58 @@ function mergeThreadMessagePages(incomingMessages, cachedMessages) {
|
|
|
3628
3800
|
return dedupeThreadMessages(Array.from(byId.values()));
|
|
3629
3801
|
}
|
|
3630
3802
|
function dedupeThreadMessages(messages) {
|
|
3803
|
+
const byCrossSourceKey = /* @__PURE__ */ new Map();
|
|
3631
3804
|
const byImageKey = /* @__PURE__ */ new Map();
|
|
3632
3805
|
const deduped = [];
|
|
3633
3806
|
const sortedMessages = [...messages].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
3634
3807
|
for (const message of sortedMessages) {
|
|
3808
|
+
const crossSourceKey = crossSourceMessageKey(message);
|
|
3809
|
+
const crossSourceIndex = crossSourceKey ? byCrossSourceKey.get(crossSourceKey) : void 0;
|
|
3810
|
+
if (crossSourceIndex !== void 0) {
|
|
3811
|
+
const existingMessage = deduped[crossSourceIndex];
|
|
3812
|
+
if (existingMessage && shouldPreferDuplicateThreadMessage(message, existingMessage)) deduped[crossSourceIndex] = message;
|
|
3813
|
+
continue;
|
|
3814
|
+
}
|
|
3815
|
+
const previous = deduped[deduped.length - 1];
|
|
3816
|
+
if (previous && isDuplicateCrossSourceMessage(previous, message)) {
|
|
3817
|
+
if (shouldPreferDuplicateThreadMessage(message, previous)) deduped[deduped.length - 1] = message;
|
|
3818
|
+
continue;
|
|
3819
|
+
}
|
|
3635
3820
|
const imageKey = userImageMessageKey(message);
|
|
3636
3821
|
if (!imageKey) {
|
|
3822
|
+
if (crossSourceKey) byCrossSourceKey.set(crossSourceKey, deduped.length);
|
|
3637
3823
|
deduped.push(message);
|
|
3638
3824
|
continue;
|
|
3639
3825
|
}
|
|
3640
3826
|
const existingIndex = byImageKey.get(imageKey);
|
|
3641
3827
|
if (existingIndex === void 0) {
|
|
3642
3828
|
byImageKey.set(imageKey, deduped.length);
|
|
3829
|
+
if (crossSourceKey) byCrossSourceKey.set(crossSourceKey, deduped.length);
|
|
3643
3830
|
deduped.push(message);
|
|
3644
3831
|
continue;
|
|
3645
3832
|
}
|
|
3646
3833
|
const existingMessage = deduped[existingIndex];
|
|
3647
|
-
if (existingMessage &&
|
|
3834
|
+
if (existingMessage && shouldPreferDuplicateThreadMessage(message, existingMessage)) deduped[existingIndex] = message;
|
|
3648
3835
|
}
|
|
3649
3836
|
return deduped;
|
|
3650
3837
|
}
|
|
3838
|
+
function crossSourceMessageKey(message) {
|
|
3839
|
+
if (!isSyntheticHistoryMessageId(message.id)) return;
|
|
3840
|
+
if (message.role !== "user" && message.role !== "assistant") return;
|
|
3841
|
+
return [
|
|
3842
|
+
message.threadId,
|
|
3843
|
+
message.createdAt.slice(0, 19),
|
|
3844
|
+
message.role,
|
|
3845
|
+
message.kind,
|
|
3846
|
+
message.content
|
|
3847
|
+
].join("\n");
|
|
3848
|
+
}
|
|
3849
|
+
function isDuplicateCrossSourceMessage(previous, next) {
|
|
3850
|
+
return previous.id !== next.id && (isSyntheticHistoryMessageId(previous.id) || isSyntheticHistoryMessageId(next.id)) && previous.threadId === next.threadId && previous.role === next.role && previous.kind === next.kind && previous.content === next.content;
|
|
3851
|
+
}
|
|
3852
|
+
function isSyntheticHistoryMessageId(id) {
|
|
3853
|
+
return id.startsWith("msg-") || id.startsWith("rollout:");
|
|
3854
|
+
}
|
|
3651
3855
|
function userImageMessageKey(message) {
|
|
3652
3856
|
if (message.role !== "user") return;
|
|
3653
3857
|
const imageUris = imageAttachmentUris(message);
|
|
@@ -3665,9 +3869,11 @@ function imageAttachmentUris(message) {
|
|
|
3665
3869
|
return ["url" in attachment ? attachment.url : void 0, "path" in attachment ? attachment.path : void 0].filter((value) => typeof value === "string" && value.length > 0);
|
|
3666
3870
|
});
|
|
3667
3871
|
}
|
|
3668
|
-
function
|
|
3872
|
+
function shouldPreferDuplicateThreadMessage(candidate, existing) {
|
|
3669
3873
|
if (existing.id.startsWith("rollout") && !candidate.id.startsWith("rollout")) return true;
|
|
3670
3874
|
if (!existing.id.startsWith("rollout") && candidate.id.startsWith("rollout")) return false;
|
|
3875
|
+
if (!existing.turnId && candidate.turnId) return true;
|
|
3876
|
+
if (existing.turnId && !candidate.turnId) return false;
|
|
3671
3877
|
return candidate.content.length < existing.content.length;
|
|
3672
3878
|
}
|
|
3673
3879
|
function rememberRolloutThreadMessages(threads, thread, messages, messageCountLowerBound = messages.length) {
|
|
@@ -3712,7 +3918,7 @@ function readSessionIndexThreadTitle(threadId) {
|
|
|
3712
3918
|
} catch {}
|
|
3713
3919
|
}
|
|
3714
3920
|
}
|
|
3715
|
-
function readRolloutThreadMessages(threadId) {
|
|
3921
|
+
function readRolloutThreadMessages(threadId, workspacePath = defaultWorkspacePath) {
|
|
3716
3922
|
const rolloutPath = findRolloutFileForThread(threadId);
|
|
3717
3923
|
if (!rolloutPath) return {
|
|
3718
3924
|
messageCountLowerBound: 0,
|
|
@@ -3720,6 +3926,8 @@ function readRolloutThreadMessages(threadId) {
|
|
|
3720
3926
|
rolloutPath
|
|
3721
3927
|
};
|
|
3722
3928
|
const collected = [];
|
|
3929
|
+
const applyPatchInputs = /* @__PURE__ */ new Map();
|
|
3930
|
+
const pendingApplyPatchChanges = [];
|
|
3723
3931
|
const lines = readFileSync(rolloutPath, "utf8").split("\n");
|
|
3724
3932
|
for (let index = 0; index < lines.length; index += 1) {
|
|
3725
3933
|
const line = lines[index];
|
|
@@ -3727,7 +3935,15 @@ function readRolloutThreadMessages(threadId) {
|
|
|
3727
3935
|
if (!line.trim()) continue;
|
|
3728
3936
|
if (!isRolloutMessageLine(line)) continue;
|
|
3729
3937
|
try {
|
|
3730
|
-
const
|
|
3938
|
+
const record = JSON.parse(line);
|
|
3939
|
+
rememberRolloutApplyPatchInput(record, applyPatchInputs);
|
|
3940
|
+
collectRolloutApplyPatchOutput(record, workspacePath, applyPatchInputs, pendingApplyPatchChanges);
|
|
3941
|
+
if (isRolloutTaskComplete(record) && pendingApplyPatchChanges.length > 0) {
|
|
3942
|
+
collected.push(rolloutApplyPatchSummaryMessage(threadId, record, `rollout:${lineNumber}:apply_patch`, pendingApplyPatchChanges));
|
|
3943
|
+
pendingApplyPatchChanges.length = 0;
|
|
3944
|
+
continue;
|
|
3945
|
+
}
|
|
3946
|
+
const message = rolloutRecordMessage(threadId, record, `rollout:${lineNumber}`, workspacePath);
|
|
3731
3947
|
if (!message) continue;
|
|
3732
3948
|
collected.push(message);
|
|
3733
3949
|
} catch {}
|
|
@@ -3754,7 +3970,10 @@ function findRolloutFileForThread(threadId) {
|
|
|
3754
3970
|
}
|
|
3755
3971
|
}
|
|
3756
3972
|
}
|
|
3757
|
-
function
|
|
3973
|
+
function isRolloutTaskComplete(record) {
|
|
3974
|
+
return record.type === "event_msg" && record.payload?.type === "task_complete";
|
|
3975
|
+
}
|
|
3976
|
+
function rolloutRecordMessage(threadId, record, messageKey, workspacePath = defaultWorkspacePath) {
|
|
3758
3977
|
const timestamp = typeof record.timestamp === "string" ? record.timestamp : (/* @__PURE__ */ new Date()).toISOString();
|
|
3759
3978
|
const payload = record.payload;
|
|
3760
3979
|
if (!payload) return;
|
|
@@ -3774,15 +3993,37 @@ function rolloutRecordMessage(threadId, record, messageKey) {
|
|
|
3774
3993
|
if (record.type === "event_msg" && payload.type === "agent_message") {
|
|
3775
3994
|
const content = firstString(payload, ["message"]);
|
|
3776
3995
|
if (!content) return;
|
|
3996
|
+
const messageParts = agentMessageParts(content);
|
|
3777
3997
|
return ChatMessageSchema.parse({
|
|
3778
3998
|
id: `${messageKey}:assistant`,
|
|
3779
3999
|
threadId,
|
|
3780
4000
|
role: "assistant",
|
|
3781
|
-
content,
|
|
4001
|
+
content: messageParts.content,
|
|
4002
|
+
details: messageParts.details,
|
|
3782
4003
|
createdAt: timestamp,
|
|
3783
4004
|
state: "completed"
|
|
3784
4005
|
});
|
|
3785
4006
|
}
|
|
4007
|
+
if (record.type === "event_msg" && payload.type === "patch_apply_end") {
|
|
4008
|
+
const changes = rolloutPatchApplyChanges(payload.changes, workspacePath);
|
|
4009
|
+
if (changes.length === 0) return;
|
|
4010
|
+
const patchPreview = largeTextPreview(changes.map((change) => change.patch).filter((patch) => Boolean(patch)).join("\n"));
|
|
4011
|
+
return ChatMessageSchema.parse({
|
|
4012
|
+
id: `${messageKey}:patch:${firstString(payload, ["call_id"]) ?? ""}`,
|
|
4013
|
+
threadId,
|
|
4014
|
+
role: "tool",
|
|
4015
|
+
kind: "fileChange",
|
|
4016
|
+
content: summarizeFileChanges(changes),
|
|
4017
|
+
createdAt: timestamp,
|
|
4018
|
+
state: "completed",
|
|
4019
|
+
details: {
|
|
4020
|
+
changes: changes.map(({ patch: _patch, ...change }) => change),
|
|
4021
|
+
patch: patchPreview?.text,
|
|
4022
|
+
patchOriginalLength: patchPreview?.originalLength,
|
|
4023
|
+
patchTruncated: patchPreview?.truncated
|
|
4024
|
+
}
|
|
4025
|
+
});
|
|
4026
|
+
}
|
|
3786
4027
|
if (record.type === "event_msg" && payload.type === "exec_command_end") {
|
|
3787
4028
|
const command = Array.isArray(payload.command) ? payload.command.map((part) => String(part)).join(" ") : firstString(payload, ["command"]) || "Command";
|
|
3788
4029
|
const outputPreview = largeTextPreview(firstString(payload, ["aggregated_output"]));
|
|
@@ -3837,8 +4078,114 @@ function rolloutImageAttachments(value, source) {
|
|
|
3837
4078
|
return localImageAttachmentDetails(image, name);
|
|
3838
4079
|
});
|
|
3839
4080
|
}
|
|
4081
|
+
function rememberRolloutApplyPatchInput(record, applyPatchInputs) {
|
|
4082
|
+
const payload = record.payload;
|
|
4083
|
+
if (record.type !== "response_item" || payload?.type !== "custom_tool_call" || firstString(payload, ["name"]) !== "apply_patch") return;
|
|
4084
|
+
const callId = firstString(payload, ["call_id"]);
|
|
4085
|
+
const input = firstString(payload, ["input"]);
|
|
4086
|
+
if (callId && input) applyPatchInputs.set(callId, input);
|
|
4087
|
+
}
|
|
4088
|
+
function collectRolloutApplyPatchOutput(record, workspacePath, applyPatchInputs, pendingApplyPatchChanges) {
|
|
4089
|
+
const payload = record.payload;
|
|
4090
|
+
if (record.type !== "response_item" || payload?.type !== "custom_tool_call_output" || !firstString(payload, ["call_id"])) return;
|
|
4091
|
+
const changes = rolloutApplyPatchOutputChanges(rolloutCustomToolOutputText(payload), workspacePath);
|
|
4092
|
+
if (changes.length === 0) return;
|
|
4093
|
+
const callId = firstString(payload, ["call_id"]);
|
|
4094
|
+
const patch = callId ? applyPatchInputs.get(callId) : void 0;
|
|
4095
|
+
for (const change of changes) pendingApplyPatchChanges.push({
|
|
4096
|
+
...change,
|
|
4097
|
+
patch
|
|
4098
|
+
});
|
|
4099
|
+
}
|
|
4100
|
+
function rolloutApplyPatchSummaryMessage(threadId, record, messageKey, pendingApplyPatchChanges) {
|
|
4101
|
+
const timestamp = typeof record.timestamp === "string" ? record.timestamp : (/* @__PURE__ */ new Date()).toISOString();
|
|
4102
|
+
const patchPreview = largeTextPreview([...new Set(pendingApplyPatchChanges.map((change) => change.patch))].filter((patch) => Boolean(patch)).join("\n"));
|
|
4103
|
+
return ChatMessageSchema.parse({
|
|
4104
|
+
id: `${messageKey}:summary`,
|
|
4105
|
+
threadId,
|
|
4106
|
+
role: "tool",
|
|
4107
|
+
kind: "fileChange",
|
|
4108
|
+
content: summarizeFileChanges(pendingApplyPatchChanges),
|
|
4109
|
+
createdAt: timestamp,
|
|
4110
|
+
state: "completed",
|
|
4111
|
+
details: {
|
|
4112
|
+
changes: pendingApplyPatchChanges.map(({ patch: _patch, ...change }) => change),
|
|
4113
|
+
patch: patchPreview?.text,
|
|
4114
|
+
patchOriginalLength: patchPreview?.originalLength,
|
|
4115
|
+
patchTruncated: patchPreview?.truncated
|
|
4116
|
+
}
|
|
4117
|
+
});
|
|
4118
|
+
}
|
|
4119
|
+
function rolloutCustomToolOutputText(payload) {
|
|
4120
|
+
const rawOutput = firstString(payload, ["output"]);
|
|
4121
|
+
if (!rawOutput) return;
|
|
4122
|
+
try {
|
|
4123
|
+
const parsed = JSON.parse(rawOutput);
|
|
4124
|
+
return typeof parsed.output === "string" ? parsed.output : rawOutput;
|
|
4125
|
+
} catch {
|
|
4126
|
+
return rawOutput;
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
function rolloutApplyPatchOutputChanges(value, workspacePath) {
|
|
4130
|
+
if (!value || !value.includes("Updated the following files")) return [];
|
|
4131
|
+
return value.split("\n").flatMap((line) => {
|
|
4132
|
+
const match = line.match(/^\s*([ADM])\s+(.+?)\s*$/);
|
|
4133
|
+
if (!match) return [];
|
|
4134
|
+
return [{
|
|
4135
|
+
kind: rolloutPatchChangeKind(match[1] === "A" ? "added" : match[1] === "D" ? "deleted" : "modified"),
|
|
4136
|
+
path: rolloutPatchDisplayPath(match[2] ?? "", workspacePath)
|
|
4137
|
+
}];
|
|
4138
|
+
});
|
|
4139
|
+
}
|
|
4140
|
+
function rolloutPatchApplyChanges(value, workspacePath = defaultWorkspacePath) {
|
|
4141
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
|
|
4142
|
+
return Object.entries(value).flatMap(([rawPath, rawChange]) => {
|
|
4143
|
+
if (!rawChange || typeof rawChange !== "object") return [];
|
|
4144
|
+
const record = rawChange;
|
|
4145
|
+
return [{
|
|
4146
|
+
kind: rolloutPatchChangeKind(firstString(record, ["type"]) ?? "modified"),
|
|
4147
|
+
patch: firstString(record, ["unified_diff", "patch"]),
|
|
4148
|
+
path: rolloutPatchDisplayPath(rawPath, workspacePath)
|
|
4149
|
+
}];
|
|
4150
|
+
});
|
|
4151
|
+
}
|
|
4152
|
+
function rolloutPatchChangeKind(type) {
|
|
4153
|
+
const normalized = type.toLowerCase();
|
|
4154
|
+
if ([
|
|
4155
|
+
"add",
|
|
4156
|
+
"added",
|
|
4157
|
+
"create",
|
|
4158
|
+
"created"
|
|
4159
|
+
].includes(normalized)) return "added";
|
|
4160
|
+
if ([
|
|
4161
|
+
"delete",
|
|
4162
|
+
"deleted",
|
|
4163
|
+
"remove",
|
|
4164
|
+
"removed"
|
|
4165
|
+
].includes(normalized)) return "deleted";
|
|
4166
|
+
if ([
|
|
4167
|
+
"move",
|
|
4168
|
+
"moved",
|
|
4169
|
+
"rename",
|
|
4170
|
+
"renamed"
|
|
4171
|
+
].includes(normalized)) return "renamed";
|
|
4172
|
+
return "modified";
|
|
4173
|
+
}
|
|
4174
|
+
function rolloutPatchDisplayPath(value, workspacePath = defaultWorkspacePath) {
|
|
4175
|
+
const filePath = value.startsWith("file://") ? fileUrlPath(value) : value;
|
|
4176
|
+
if (!isAbsolute(filePath)) return filePath;
|
|
4177
|
+
const relativePath = relative(resolve(workspacePath), filePath);
|
|
4178
|
+
return relativePath && !relativePath.startsWith("..") && !isAbsolute(relativePath) ? relativePath.split("\\").join("/") : filePath;
|
|
4179
|
+
}
|
|
4180
|
+
function fileUrlPath(value) {
|
|
4181
|
+
try {
|
|
4182
|
+
return fileURLToPath(value);
|
|
4183
|
+
} catch {
|
|
4184
|
+
return value;
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
3840
4187
|
function isRolloutMessageLine(line) {
|
|
3841
|
-
return line.includes("\"type\":\"event_msg\"") && (line.includes("\"type\":\"user_message\"") || line.includes("\"type\":\"agent_message\"") || line.includes("\"type\":\"exec_command_end\"") || line.includes("\"type\":\"mcp_tool_call_end\""));
|
|
4188
|
+
return line.includes("\"type\":\"event_msg\"") && (line.includes("\"type\":\"user_message\"") || line.includes("\"type\":\"agent_message\"") || line.includes("\"type\":\"patch_apply_end\"") || line.includes("\"type\":\"task_complete\"") || line.includes("\"type\":\"exec_command_end\"") || line.includes("\"type\":\"mcp_tool_call_end\"")) || line.includes("\"type\":\"response_item\"") && (line.includes("\"type\":\"custom_tool_call\"") || line.includes("\"type\":\"custom_tool_call_output\""));
|
|
3842
4189
|
}
|
|
3843
4190
|
function threadDetailResponse(input) {
|
|
3844
4191
|
return ThreadDetailResponseSchema.parse({
|
|
@@ -3917,12 +4264,13 @@ function mapAppServerItem(threadId, turn, item) {
|
|
|
3917
4264
|
case "agentMessage": {
|
|
3918
4265
|
const agentItem = item;
|
|
3919
4266
|
const planContent = proposedPlanContent(agentItem.text);
|
|
4267
|
+
const messageParts = appServerAgentMessageParts(agentItem);
|
|
3920
4268
|
return ChatMessageSchema.parse({
|
|
3921
4269
|
...base,
|
|
3922
4270
|
role: "assistant",
|
|
3923
4271
|
kind: planContent ? "plan" : void 0,
|
|
3924
|
-
content: planContent ??
|
|
3925
|
-
details: planContent ? { raw: agentItem.text } :
|
|
4272
|
+
content: planContent ?? messageParts.content,
|
|
4273
|
+
details: planContent ? { raw: agentItem.text } : messageParts.details
|
|
3926
4274
|
});
|
|
3927
4275
|
}
|
|
3928
4276
|
case "reasoning": {
|
|
@@ -4036,6 +4384,11 @@ function appServerTurnShell(turnId) {
|
|
|
4036
4384
|
};
|
|
4037
4385
|
}
|
|
4038
4386
|
function mapAppServerModel(model) {
|
|
4387
|
+
const serviceTiers = model.serviceTiers ?? model.additionalSpeedTiers?.map((tier) => ({
|
|
4388
|
+
id: tier,
|
|
4389
|
+
name: tier === "fast" ? "Fast" : tier,
|
|
4390
|
+
description: void 0
|
|
4391
|
+
})) ?? [];
|
|
4039
4392
|
return {
|
|
4040
4393
|
id: model.id,
|
|
4041
4394
|
model: model.model,
|
|
@@ -4043,7 +4396,8 @@ function mapAppServerModel(model) {
|
|
|
4043
4396
|
description: model.description,
|
|
4044
4397
|
isDefault: Boolean(model.isDefault),
|
|
4045
4398
|
defaultReasoningEffort: model.defaultReasoningEffort,
|
|
4046
|
-
supportedReasoningEfforts: model.supportedReasoningEfforts?.map((effort) => effort.reasoningEffort) ?? []
|
|
4399
|
+
supportedReasoningEfforts: model.supportedReasoningEfforts?.map((effort) => effort.reasoningEffort) ?? [],
|
|
4400
|
+
serviceTiers
|
|
4047
4401
|
};
|
|
4048
4402
|
}
|
|
4049
4403
|
function normalizeRateLimitBuckets(rateLimits) {
|
|
@@ -4104,7 +4458,13 @@ function fallbackModels() {
|
|
|
4104
4458
|
{ reasoningEffort: "medium" },
|
|
4105
4459
|
{ reasoningEffort: "high" },
|
|
4106
4460
|
{ reasoningEffort: "xhigh" }
|
|
4107
|
-
]
|
|
4461
|
+
],
|
|
4462
|
+
additionalSpeedTiers: ["fast"],
|
|
4463
|
+
serviceTiers: [{
|
|
4464
|
+
id: "priority",
|
|
4465
|
+
name: "Fast",
|
|
4466
|
+
description: "1.5x speed, increased usage"
|
|
4467
|
+
}]
|
|
4108
4468
|
}];
|
|
4109
4469
|
}
|
|
4110
4470
|
function mapAppServerThreadState(status, turns) {
|
|
@@ -4808,53 +5168,235 @@ async function git(cwd, args) {
|
|
|
4808
5168
|
});
|
|
4809
5169
|
return stdout.trimEnd();
|
|
4810
5170
|
}
|
|
4811
|
-
async function listWorkspaceFiles(workspacePath, query) {
|
|
5171
|
+
async function listWorkspaceFiles(workspacePath, query, directory) {
|
|
4812
5172
|
const normalizedQuery = query.toLowerCase();
|
|
4813
5173
|
const filePaths = await workspaceFilePaths(workspacePath);
|
|
4814
5174
|
const entriesByPath = /* @__PURE__ */ new Map();
|
|
4815
5175
|
for (const path of filePaths) {
|
|
4816
5176
|
if (!path || path.startsWith("../") || path.includes("/.git/")) continue;
|
|
4817
|
-
|
|
4818
|
-
|
|
5177
|
+
if (!directory && normalizedQuery) {
|
|
5178
|
+
entriesByPath.set(path, {
|
|
5179
|
+
directory: dirname(path) === "." ? "" : dirname(path),
|
|
5180
|
+
kind: "file",
|
|
5181
|
+
name: path.split("/").pop() ?? path,
|
|
5182
|
+
path
|
|
5183
|
+
});
|
|
5184
|
+
const parts = path.split("/");
|
|
5185
|
+
for (let index = 1; index < parts.length; index += 1) {
|
|
5186
|
+
const directoryPath = parts.slice(0, index).join("/");
|
|
5187
|
+
entriesByPath.set(directoryPath, {
|
|
5188
|
+
directory: dirname(directoryPath) === "." ? "" : dirname(directoryPath),
|
|
5189
|
+
kind: "directory",
|
|
5190
|
+
name: parts[index - 1],
|
|
5191
|
+
path: directoryPath
|
|
5192
|
+
});
|
|
5193
|
+
}
|
|
5194
|
+
continue;
|
|
5195
|
+
}
|
|
5196
|
+
const prefix = directory ? `${directory}/` : "";
|
|
5197
|
+
if (!path.startsWith(prefix)) continue;
|
|
5198
|
+
const childParts = path.slice(prefix.length).split("/").filter(Boolean);
|
|
5199
|
+
if (childParts.length === 0) continue;
|
|
5200
|
+
if (childParts.length === 1) entriesByPath.set(path, {
|
|
5201
|
+
directory,
|
|
4819
5202
|
kind: "file",
|
|
4820
|
-
name:
|
|
5203
|
+
name: childParts[0],
|
|
4821
5204
|
path
|
|
4822
5205
|
});
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
const directoryPath = parts.slice(0, index).join("/");
|
|
5206
|
+
else {
|
|
5207
|
+
const directoryPath = [...directory ? directory.split("/") : [], childParts[0]].join("/");
|
|
4826
5208
|
entriesByPath.set(directoryPath, {
|
|
4827
|
-
directory
|
|
5209
|
+
directory,
|
|
4828
5210
|
kind: "directory",
|
|
4829
|
-
name:
|
|
5211
|
+
name: childParts[0],
|
|
4830
5212
|
path: directoryPath
|
|
4831
5213
|
});
|
|
4832
5214
|
}
|
|
4833
5215
|
}
|
|
4834
5216
|
return [...entriesByPath.values()].filter((entry) => {
|
|
4835
5217
|
if (!normalizedQuery) return true;
|
|
4836
|
-
return entry.path.toLowerCase().includes(normalizedQuery);
|
|
5218
|
+
return entry.name.toLowerCase().includes(normalizedQuery) || entry.path.toLowerCase().includes(normalizedQuery);
|
|
4837
5219
|
}).sort((left, right) => {
|
|
4838
5220
|
const leftScore = workspaceFileScore(left.path, normalizedQuery);
|
|
4839
5221
|
const rightScore = workspaceFileScore(right.path, normalizedQuery);
|
|
4840
5222
|
if (leftScore !== rightScore) return leftScore - rightScore;
|
|
4841
5223
|
if (left.kind !== right.kind) return left.kind === "directory" ? -1 : 1;
|
|
4842
5224
|
return left.path.localeCompare(right.path);
|
|
4843
|
-
}).slice(0, 80);
|
|
5225
|
+
}).slice(0, directory || !normalizedQuery ? 160 : 80);
|
|
5226
|
+
}
|
|
5227
|
+
async function readWorkspaceFileContent(workspacePath, requestedPath) {
|
|
5228
|
+
const relativePath = normalizeWorkspaceRelativePath(requestedPath);
|
|
5229
|
+
if (!relativePath.success) return {
|
|
5230
|
+
code: "invalid_workspace_file_path",
|
|
5231
|
+
error: relativePath.error,
|
|
5232
|
+
status: 400,
|
|
5233
|
+
success: false
|
|
5234
|
+
};
|
|
5235
|
+
const rootPath = resolve(workspacePath);
|
|
5236
|
+
const absolutePath = resolve(rootPath, relativePath.path);
|
|
5237
|
+
if (!isPathInside(rootPath, absolutePath)) return {
|
|
5238
|
+
code: "invalid_workspace_file_path",
|
|
5239
|
+
error: "Workspace file path must stay inside the workspace.",
|
|
5240
|
+
status: 400,
|
|
5241
|
+
success: false
|
|
5242
|
+
};
|
|
5243
|
+
const fileStat = await stat(absolutePath).catch(() => null);
|
|
5244
|
+
if (!fileStat?.isFile()) return {
|
|
5245
|
+
code: "workspace_file_not_found",
|
|
5246
|
+
error: "Workspace file was not found.",
|
|
5247
|
+
status: 404,
|
|
5248
|
+
success: false
|
|
5249
|
+
};
|
|
5250
|
+
const bytesToRead = Math.min(fileStat.size, WORKSPACE_FILE_PREVIEW_MAX_BYTES);
|
|
5251
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
5252
|
+
if (bytesToRead > 0) {
|
|
5253
|
+
const handle = await open(absolutePath, "r");
|
|
5254
|
+
try {
|
|
5255
|
+
await handle.read(buffer, 0, bytesToRead, 0);
|
|
5256
|
+
} finally {
|
|
5257
|
+
await handle.close();
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5260
|
+
const binary = buffer.includes(0);
|
|
5261
|
+
return {
|
|
5262
|
+
content: {
|
|
5263
|
+
binary,
|
|
5264
|
+
content: binary ? "" : buffer.toString("utf8"),
|
|
5265
|
+
directory: dirname(relativePath.path) === "." ? "" : dirname(relativePath.path),
|
|
5266
|
+
language: languageFromWorkspaceFile(relativePath.path),
|
|
5267
|
+
name: basename(relativePath.path),
|
|
5268
|
+
path: relativePath.path,
|
|
5269
|
+
size: fileStat.size,
|
|
5270
|
+
truncated: fileStat.size > WORKSPACE_FILE_PREVIEW_MAX_BYTES
|
|
5271
|
+
},
|
|
5272
|
+
success: true
|
|
5273
|
+
};
|
|
5274
|
+
}
|
|
5275
|
+
async function updateWorkspaceFileContent(workspacePath, input) {
|
|
5276
|
+
const relativePath = normalizeWorkspaceRelativePath(input.path);
|
|
5277
|
+
if (!relativePath.success) return {
|
|
5278
|
+
code: "invalid_workspace_file_path",
|
|
5279
|
+
error: relativePath.error,
|
|
5280
|
+
status: 400,
|
|
5281
|
+
success: false
|
|
5282
|
+
};
|
|
5283
|
+
const rootPath = resolve(workspacePath);
|
|
5284
|
+
const absolutePath = resolve(rootPath, relativePath.path);
|
|
5285
|
+
if (!isPathInside(rootPath, absolutePath)) return {
|
|
5286
|
+
code: "invalid_workspace_file_path",
|
|
5287
|
+
error: "Workspace file path must stay inside the workspace.",
|
|
5288
|
+
status: 400,
|
|
5289
|
+
success: false
|
|
5290
|
+
};
|
|
5291
|
+
if (!(await stat(absolutePath).catch(() => null))?.isFile()) return {
|
|
5292
|
+
code: "workspace_file_not_found",
|
|
5293
|
+
error: "Workspace file was not found.",
|
|
5294
|
+
status: 404,
|
|
5295
|
+
success: false
|
|
5296
|
+
};
|
|
5297
|
+
await writeFile(absolutePath, input.content, "utf8");
|
|
5298
|
+
return readWorkspaceFileContent(workspacePath, relativePath.path);
|
|
5299
|
+
}
|
|
5300
|
+
function normalizeWorkspaceRelativePath(requestedPath) {
|
|
5301
|
+
const normalized = requestedPath.trim().replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
5302
|
+
if (!normalized) return {
|
|
5303
|
+
error: "Workspace file path is required.",
|
|
5304
|
+
success: false
|
|
5305
|
+
};
|
|
5306
|
+
if (isAbsolute(normalized)) return {
|
|
5307
|
+
error: "Workspace file path must be relative.",
|
|
5308
|
+
success: false
|
|
5309
|
+
};
|
|
5310
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
5311
|
+
if (segments.some((segment) => segment === "." || segment === ".." || segment === ".git")) return {
|
|
5312
|
+
error: "Workspace file path contains an unsupported segment.",
|
|
5313
|
+
success: false
|
|
5314
|
+
};
|
|
5315
|
+
return {
|
|
5316
|
+
path: segments.join("/"),
|
|
5317
|
+
success: true
|
|
5318
|
+
};
|
|
5319
|
+
}
|
|
5320
|
+
function normalizeWorkspaceDirectoryPath(requestedPath) {
|
|
5321
|
+
const normalized = requestedPath.trim().replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
5322
|
+
if (!normalized) return {
|
|
5323
|
+
path: "",
|
|
5324
|
+
success: true
|
|
5325
|
+
};
|
|
5326
|
+
if (isAbsolute(normalized)) return {
|
|
5327
|
+
error: "Workspace directory path must be relative.",
|
|
5328
|
+
success: false
|
|
5329
|
+
};
|
|
5330
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
5331
|
+
if (segments.some((segment) => segment === "." || segment === ".." || segment === ".git")) return {
|
|
5332
|
+
error: "Workspace directory path contains an unsupported segment.",
|
|
5333
|
+
success: false
|
|
5334
|
+
};
|
|
5335
|
+
return {
|
|
5336
|
+
path: segments.join("/"),
|
|
5337
|
+
success: true
|
|
5338
|
+
};
|
|
5339
|
+
}
|
|
5340
|
+
function parentWorkspaceDirectory(directory) {
|
|
5341
|
+
if (!directory) return null;
|
|
5342
|
+
const parent = dirname(directory);
|
|
5343
|
+
return parent === "." ? "" : parent;
|
|
5344
|
+
}
|
|
5345
|
+
function isPathInside(rootPath, targetPath) {
|
|
5346
|
+
const pathFromRoot = relative(rootPath, targetPath);
|
|
5347
|
+
return Boolean(pathFromRoot) && !pathFromRoot.startsWith("..") && !isAbsolute(pathFromRoot);
|
|
5348
|
+
}
|
|
5349
|
+
function languageFromWorkspaceFile(path) {
|
|
5350
|
+
const name = basename(path).toLowerCase();
|
|
5351
|
+
const extension = extname(name).replace(/^\./, "");
|
|
5352
|
+
if (name === "dockerfile") return "dockerfile";
|
|
5353
|
+
if (name === "makefile") return "make";
|
|
5354
|
+
if (name === "package.json") return "json";
|
|
5355
|
+
return {
|
|
5356
|
+
cjs: "javascript",
|
|
5357
|
+
css: "css",
|
|
5358
|
+
diff: "diff",
|
|
5359
|
+
go: "go",
|
|
5360
|
+
htm: "html",
|
|
5361
|
+
html: "html",
|
|
5362
|
+
java: "java",
|
|
5363
|
+
js: "javascript",
|
|
5364
|
+
json: "json",
|
|
5365
|
+
jsx: "jsx",
|
|
5366
|
+
kt: "kotlin",
|
|
5367
|
+
lock: "yaml",
|
|
5368
|
+
log: "text",
|
|
5369
|
+
md: "markdown",
|
|
5370
|
+
mdx: "markdown",
|
|
5371
|
+
mjs: "javascript",
|
|
5372
|
+
plist: "xml",
|
|
5373
|
+
py: "python",
|
|
5374
|
+
rb: "ruby",
|
|
5375
|
+
rs: "rust",
|
|
5376
|
+
sh: "bash",
|
|
5377
|
+
swift: "swift",
|
|
5378
|
+
ts: "typescript",
|
|
5379
|
+
tsx: "tsx",
|
|
5380
|
+
txt: "text",
|
|
5381
|
+
xml: "xml",
|
|
5382
|
+
yaml: "yaml",
|
|
5383
|
+
yml: "yaml"
|
|
5384
|
+
}[extension] ?? extension;
|
|
4844
5385
|
}
|
|
4845
5386
|
async function workspaceFilePaths(workspacePath) {
|
|
5387
|
+
const isIgnored = await workspaceIgnoreMatcher(workspacePath);
|
|
4846
5388
|
try {
|
|
4847
5389
|
return (await git(workspacePath, [
|
|
4848
5390
|
"ls-files",
|
|
4849
5391
|
"--cached",
|
|
4850
5392
|
"--others",
|
|
4851
5393
|
"--exclude-standard"
|
|
4852
|
-
])).split("\n").filter(Boolean);
|
|
5394
|
+
])).split("\n").filter(Boolean).filter((path) => !isIgnored(path));
|
|
4853
5395
|
} catch {
|
|
4854
|
-
return recursiveWorkspaceFilePaths(workspacePath);
|
|
5396
|
+
return recursiveWorkspaceFilePaths(workspacePath, isIgnored);
|
|
4855
5397
|
}
|
|
4856
5398
|
}
|
|
4857
|
-
async function recursiveWorkspaceFilePaths(rootPath) {
|
|
5399
|
+
async function recursiveWorkspaceFilePaths(rootPath, isIgnored) {
|
|
4858
5400
|
const ignoredDirectories = new Set([
|
|
4859
5401
|
".git",
|
|
4860
5402
|
".expo",
|
|
@@ -4868,17 +5410,45 @@ async function recursiveWorkspaceFilePaths(rootPath) {
|
|
|
4868
5410
|
for (const entry of entries) {
|
|
4869
5411
|
if (entry.name.startsWith(".") && entry.name !== ".github") continue;
|
|
4870
5412
|
const absolutePath = resolve(directory, entry.name);
|
|
5413
|
+
const relativePath = relative(rootPath, absolutePath).split("\\").join("/");
|
|
5414
|
+
if (isIgnored(relativePath)) continue;
|
|
4871
5415
|
if (entry.isDirectory()) {
|
|
4872
5416
|
if (ignoredDirectories.has(entry.name)) continue;
|
|
4873
5417
|
await visit(absolutePath);
|
|
4874
5418
|
continue;
|
|
4875
5419
|
}
|
|
4876
|
-
if (entry.isFile()) results.push(
|
|
5420
|
+
if (entry.isFile()) results.push(relativePath);
|
|
4877
5421
|
}
|
|
4878
5422
|
}
|
|
4879
5423
|
await visit(rootPath);
|
|
4880
5424
|
return results;
|
|
4881
5425
|
}
|
|
5426
|
+
async function workspaceIgnoreMatcher(workspacePath) {
|
|
5427
|
+
const matchers = (await readFile(join(workspacePath, ".gitignore"), "utf8").catch(() => "")).split(/\r?\n/).map((line) => gitignorePatternMatcher(line)).filter((matcher) => Boolean(matcher));
|
|
5428
|
+
return (path) => matchers.some((matcher) => matcher(path.split("\\").join("/")));
|
|
5429
|
+
}
|
|
5430
|
+
function gitignorePatternMatcher(line) {
|
|
5431
|
+
const trimmed = line.trim();
|
|
5432
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("!")) return;
|
|
5433
|
+
const directoryOnly = trimmed.endsWith("/");
|
|
5434
|
+
const anchored = trimmed.startsWith("/");
|
|
5435
|
+
const pattern = trimmed.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
5436
|
+
if (!pattern) return;
|
|
5437
|
+
const matcher = gitignoreGlobMatcher(pattern);
|
|
5438
|
+
const hasSlash = pattern.includes("/");
|
|
5439
|
+
return (path) => {
|
|
5440
|
+
const normalizedPath = path.replace(/^\/+/, "");
|
|
5441
|
+
return (anchored || hasSlash ? [normalizedPath] : normalizedPath.split("/")).some((candidate) => {
|
|
5442
|
+
if (directoryOnly) return matcher(candidate) || normalizedPath.startsWith(`${candidate}/`);
|
|
5443
|
+
return matcher(candidate);
|
|
5444
|
+
});
|
|
5445
|
+
};
|
|
5446
|
+
}
|
|
5447
|
+
function gitignoreGlobMatcher(pattern) {
|
|
5448
|
+
const expression = pattern.split("**").map((part) => part.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")).join(".*").replace(/\?/g, "[^/]");
|
|
5449
|
+
const regex = new RegExp(`^${expression}(?:/.*)?$`);
|
|
5450
|
+
return (path) => regex.test(path);
|
|
5451
|
+
}
|
|
4882
5452
|
function workspaceFileScore(path, query) {
|
|
4883
5453
|
if (!query) return path.split("/").length;
|
|
4884
5454
|
const lowerPath = path.toLowerCase();
|