viveworker 0.1.3 → 0.1.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/package.json +1 -1
- package/scripts/viveworker-bridge.mjs +1221 -56
- package/web/app.css +242 -30
- package/web/app.js +345 -113
- package/web/i18n.js +15 -7
- package/web/sw.js +1 -1
|
@@ -10,6 +10,7 @@ import os from "node:os";
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import process from "node:process";
|
|
12
12
|
import { createInterface } from "node:readline";
|
|
13
|
+
import { inspect } from "node:util";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
14
15
|
import webPush from "web-push";
|
|
15
16
|
import { DEFAULT_LOCALE, SUPPORTED_LOCALES, localeDisplayName, normalizeLocale, resolveLocalePreference, t } from "../web/i18n.js";
|
|
@@ -33,7 +34,7 @@ const PAIRING_RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000;
|
|
|
33
34
|
const PAIRING_RATE_LIMIT_MAX_ATTEMPTS = 8;
|
|
34
35
|
const DEFAULT_COMPLETION_REPLY_IMAGE_MAX_BYTES = 15 * 1024 * 1024;
|
|
35
36
|
const DEFAULT_COMPLETION_REPLY_UPLOAD_TTL_MS = 24 * 60 * 60 * 1000;
|
|
36
|
-
const MAX_COMPLETION_REPLY_IMAGE_COUNT =
|
|
37
|
+
const MAX_COMPLETION_REPLY_IMAGE_COUNT = 4;
|
|
37
38
|
|
|
38
39
|
const cli = parseCliArgs(process.argv.slice(2));
|
|
39
40
|
const envFile = resolveEnvFile(cli.envFile);
|
|
@@ -58,6 +59,7 @@ const runtime = {
|
|
|
58
59
|
sourceFile: "",
|
|
59
60
|
},
|
|
60
61
|
rolloutThreadLabels: new Map(),
|
|
62
|
+
rolloutThreadCwds: new Map(),
|
|
61
63
|
threadStates: new Map(),
|
|
62
64
|
threadOwnerClientIds: new Map(),
|
|
63
65
|
nativeApprovalsByToken: new Map(),
|
|
@@ -83,6 +85,7 @@ const restoredPendingPlanStateChanged = restorePendingPlanRequests({ config, run
|
|
|
83
85
|
const restoredPendingUserInputStateChanged = restorePendingUserInputRequests({ config, runtime, state });
|
|
84
86
|
runtime.recentHistoryItems = normalizeHistoryItems(state.recentHistoryItems ?? [], config.maxHistoryItems);
|
|
85
87
|
runtime.recentTimelineEntries = normalizeTimelineEntries(state.recentTimelineEntries ?? [], config.maxTimelineEntries);
|
|
88
|
+
const restoredTimelineImagePathsStateChanged = await backfillPersistedTimelineImagePaths({ config, runtime, state });
|
|
86
89
|
runtime.historyFileState.offset = Number(state.historyFileOffset) || 0;
|
|
87
90
|
runtime.historyFileState.sourceFile = cleanText(state.historyFileSourceFile ?? "");
|
|
88
91
|
|
|
@@ -216,6 +219,29 @@ function kindTitle(locale, kind) {
|
|
|
216
219
|
}
|
|
217
220
|
}
|
|
218
221
|
|
|
222
|
+
function looksLikeGeneratedThreadTitle(value) {
|
|
223
|
+
const normalized = cleanText(value || "");
|
|
224
|
+
if (!normalized.includes("|")) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
const prefix = cleanText(normalized.split("|", 1)[0] || "");
|
|
228
|
+
if (!prefix) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
const titleKeys = [
|
|
232
|
+
"server.title.userMessage",
|
|
233
|
+
"server.title.assistantCommentary",
|
|
234
|
+
"server.title.assistantFinal",
|
|
235
|
+
"server.title.approval",
|
|
236
|
+
"server.title.plan",
|
|
237
|
+
"server.title.planReady",
|
|
238
|
+
"server.title.choice",
|
|
239
|
+
"server.title.choiceReadOnly",
|
|
240
|
+
"server.title.complete",
|
|
241
|
+
];
|
|
242
|
+
return SUPPORTED_LOCALES.some((locale) => titleKeys.some((key) => t(locale, key) === prefix));
|
|
243
|
+
}
|
|
244
|
+
|
|
219
245
|
function formatLocalizedTitle(locale, baseKeyOrTitle, threadLabel) {
|
|
220
246
|
const baseTitle = baseKeyOrTitle.includes(".") ? t(locale, baseKeyOrTitle) : baseKeyOrTitle;
|
|
221
247
|
return formatTitle(baseTitle, threadLabel);
|
|
@@ -277,7 +303,7 @@ function normalizeHistoryItem(raw) {
|
|
|
277
303
|
const stableId = cleanText(raw.stableId ?? raw.id ?? "");
|
|
278
304
|
const kind = cleanText(raw.kind ?? "");
|
|
279
305
|
const title = cleanText(raw.title ?? "");
|
|
280
|
-
const messageText =
|
|
306
|
+
const messageText = normalizeTimelineMessageText(raw.messageText ?? "");
|
|
281
307
|
const createdAtMs = Number(raw.createdAtMs) || Date.now();
|
|
282
308
|
if (!stableId || !historyKinds.has(kind) || !title) {
|
|
283
309
|
return null;
|
|
@@ -292,6 +318,7 @@ function normalizeHistoryItem(raw) {
|
|
|
292
318
|
threadLabel: cleanText(raw.threadLabel ?? ""),
|
|
293
319
|
summary: normalizeNotificationText(raw.summary ?? "") || formatNotificationBody(messageText, 100) || "",
|
|
294
320
|
messageText,
|
|
321
|
+
imagePaths: normalizeTimelineImagePaths(raw.imagePaths ?? raw.localImagePaths ?? []),
|
|
295
322
|
createdAtMs,
|
|
296
323
|
readOnly: raw.readOnly !== false,
|
|
297
324
|
primaryLabel: cleanText(raw.primaryLabel ?? "") || "詳細",
|
|
@@ -365,7 +392,7 @@ function normalizeTimelineEntry(raw) {
|
|
|
365
392
|
return null;
|
|
366
393
|
}
|
|
367
394
|
|
|
368
|
-
const messageText =
|
|
395
|
+
const messageText = normalizeTimelineMessageText(raw.messageText ?? "");
|
|
369
396
|
const summary =
|
|
370
397
|
normalizeNotificationText(raw.summary ?? "") ||
|
|
371
398
|
formatNotificationBody(messageText, 180) ||
|
|
@@ -383,6 +410,7 @@ function normalizeTimelineEntry(raw) {
|
|
|
383
410
|
title,
|
|
384
411
|
summary,
|
|
385
412
|
messageText,
|
|
413
|
+
imagePaths: normalizeTimelineImagePaths(raw.imagePaths ?? raw.localImagePaths ?? []),
|
|
386
414
|
createdAtMs,
|
|
387
415
|
readOnly: raw.readOnly !== false,
|
|
388
416
|
primaryLabel: cleanText(raw.primaryLabel ?? "") || "詳細",
|
|
@@ -728,6 +756,9 @@ async function scanOnce({ config, runtime, state }) {
|
|
|
728
756
|
}
|
|
729
757
|
|
|
730
758
|
if (sessionIndexChanged || knownFilesChanged) {
|
|
759
|
+
if (knownFilesChanged) {
|
|
760
|
+
runtime.rolloutThreadCwds = new Map();
|
|
761
|
+
}
|
|
731
762
|
runtime.rolloutThreadLabels = await buildRolloutThreadLabelIndex(runtime.knownFiles, runtime.sessionIndex);
|
|
732
763
|
dirty = refreshResolvedThreadLabels({ config, runtime, state }) || dirty;
|
|
733
764
|
}
|
|
@@ -769,6 +800,27 @@ async function scanOnce({ config, runtime, state }) {
|
|
|
769
800
|
now,
|
|
770
801
|
});
|
|
771
802
|
dirty = dirty || historyTimelineChanged;
|
|
803
|
+
|
|
804
|
+
const timelineImageBackfillChanged = await backfillRecentTimelineEntryImages({
|
|
805
|
+
config,
|
|
806
|
+
runtime,
|
|
807
|
+
state,
|
|
808
|
+
});
|
|
809
|
+
dirty = dirty || timelineImageBackfillChanged;
|
|
810
|
+
|
|
811
|
+
const persistedTimelineImageBackfillChanged = await backfillPersistedTimelineImagePaths({
|
|
812
|
+
config,
|
|
813
|
+
runtime,
|
|
814
|
+
state,
|
|
815
|
+
});
|
|
816
|
+
dirty = dirty || persistedTimelineImageBackfillChanged;
|
|
817
|
+
|
|
818
|
+
const interruptedTimelineBackfillChanged = backfillInterruptedTimelineEntries({
|
|
819
|
+
config,
|
|
820
|
+
runtime,
|
|
821
|
+
state,
|
|
822
|
+
});
|
|
823
|
+
dirty = dirty || interruptedTimelineBackfillChanged;
|
|
772
824
|
}
|
|
773
825
|
|
|
774
826
|
dirty = cleanupExpiredPlanRequests({ runtime, state, now }) || dirty;
|
|
@@ -1181,6 +1233,242 @@ async function processHistoryTimelineFile({ config, runtime, state, now }) {
|
|
|
1181
1233
|
return dirty;
|
|
1182
1234
|
}
|
|
1183
1235
|
|
|
1236
|
+
async function backfillRecentTimelineEntryImages({ config, runtime, state }) {
|
|
1237
|
+
const candidates = runtime.recentTimelineEntries.filter(
|
|
1238
|
+
(entry) =>
|
|
1239
|
+
cleanText(entry?.kind || "") === "user_message" &&
|
|
1240
|
+
cleanText(entry?.threadId || "") &&
|
|
1241
|
+
normalizeTimelineImagePaths(entry?.imagePaths ?? []).length === 0
|
|
1242
|
+
);
|
|
1243
|
+
if (candidates.length === 0 || !Array.isArray(runtime.knownFiles) || runtime.knownFiles.length === 0) {
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const fileCache = new Map();
|
|
1248
|
+
let changed = false;
|
|
1249
|
+
const nextEntries = runtime.recentTimelineEntries.map((entry) => ({ ...entry }));
|
|
1250
|
+
|
|
1251
|
+
for (let index = 0; index < nextEntries.length; index += 1) {
|
|
1252
|
+
const entry = nextEntries[index];
|
|
1253
|
+
if (
|
|
1254
|
+
cleanText(entry?.kind || "") !== "user_message" ||
|
|
1255
|
+
!cleanText(entry?.threadId || "") ||
|
|
1256
|
+
normalizeTimelineImagePaths(entry?.imagePaths ?? []).length > 0
|
|
1257
|
+
) {
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const hydrated = await hydrateTimelineEntryImagesFromRollout({
|
|
1262
|
+
config,
|
|
1263
|
+
runtime,
|
|
1264
|
+
entry,
|
|
1265
|
+
fileCache,
|
|
1266
|
+
});
|
|
1267
|
+
if (!hydrated) {
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
nextEntries[index] = hydrated;
|
|
1272
|
+
changed = true;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (!changed) {
|
|
1276
|
+
return false;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const normalized = normalizeTimelineEntries(nextEntries, config.maxTimelineEntries);
|
|
1280
|
+
runtime.recentTimelineEntries = normalized;
|
|
1281
|
+
state.recentTimelineEntries = normalized;
|
|
1282
|
+
return true;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
async function backfillPersistedTimelineImagePaths({ config, runtime, state }) {
|
|
1286
|
+
let changed = false;
|
|
1287
|
+
const nextEntries = [];
|
|
1288
|
+
for (const entry of runtime.recentTimelineEntries) {
|
|
1289
|
+
const nextImagePaths = await normalizePersistedTimelineImagePaths({
|
|
1290
|
+
config,
|
|
1291
|
+
state,
|
|
1292
|
+
imagePaths: entry?.imagePaths ?? [],
|
|
1293
|
+
});
|
|
1294
|
+
if (JSON.stringify(nextImagePaths) !== JSON.stringify(normalizeTimelineImagePaths(entry?.imagePaths ?? []))) {
|
|
1295
|
+
changed = true;
|
|
1296
|
+
nextEntries.push({
|
|
1297
|
+
...entry,
|
|
1298
|
+
imagePaths: nextImagePaths,
|
|
1299
|
+
});
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
nextEntries.push(entry);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
if (!changed) {
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
const normalized = normalizeTimelineEntries(nextEntries, config.maxTimelineEntries);
|
|
1310
|
+
runtime.recentTimelineEntries = normalized;
|
|
1311
|
+
state.recentTimelineEntries = normalized;
|
|
1312
|
+
return true;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function backfillInterruptedTimelineEntries({ config, runtime, state }) {
|
|
1316
|
+
const locale = normalizeLocale(config.defaultLocale) || DEFAULT_LOCALE;
|
|
1317
|
+
let changed = false;
|
|
1318
|
+
const nextEntries = runtime.recentTimelineEntries.map((entry) => {
|
|
1319
|
+
const nextMessageText = normalizeTimelineMessageText(entry?.messageText ?? "", locale);
|
|
1320
|
+
const nextSummary = normalizeNotificationText(entry?.summary ?? "", locale) || formatNotificationBody(nextMessageText, 180);
|
|
1321
|
+
if (nextMessageText === (entry?.messageText ?? "") && nextSummary === (entry?.summary ?? "")) {
|
|
1322
|
+
return entry;
|
|
1323
|
+
}
|
|
1324
|
+
changed = true;
|
|
1325
|
+
return {
|
|
1326
|
+
...entry,
|
|
1327
|
+
messageText: nextMessageText,
|
|
1328
|
+
summary: nextSummary,
|
|
1329
|
+
};
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
if (!changed) {
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
runtime.recentTimelineEntries = nextEntries;
|
|
1337
|
+
state.recentTimelineEntries = nextEntries;
|
|
1338
|
+
return true;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
async function hydrateTimelineEntryImagesFromRollout({ config, runtime, entry, fileCache }) {
|
|
1342
|
+
const threadId = cleanText(entry?.threadId || "");
|
|
1343
|
+
if (!threadId) {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
const rolloutFile = findRolloutFileForThread(runtime, threadId);
|
|
1348
|
+
if (!rolloutFile) {
|
|
1349
|
+
return null;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
let recentMessages = fileCache.get(rolloutFile);
|
|
1353
|
+
if (!recentMessages) {
|
|
1354
|
+
recentMessages = await readRecentRolloutUserMessagesWithImages({
|
|
1355
|
+
filePath: rolloutFile,
|
|
1356
|
+
maxBytes: Math.max(config.maxReadBytes * 4, 1024 * 1024),
|
|
1357
|
+
});
|
|
1358
|
+
fileCache.set(rolloutFile, recentMessages);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
if (!recentMessages.length) {
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
const entryCreatedAtMs = Number(entry?.createdAtMs) || 0;
|
|
1366
|
+
const entryMessageText = normalizeTimelineMessageText(entry?.messageText ?? "");
|
|
1367
|
+
let bestMatch = null;
|
|
1368
|
+
|
|
1369
|
+
for (const candidate of recentMessages) {
|
|
1370
|
+
const candidateCreatedAtMs = Number(candidate?.createdAtMs) || 0;
|
|
1371
|
+
if (entryCreatedAtMs && candidateCreatedAtMs && Math.abs(candidateCreatedAtMs - entryCreatedAtMs) > 15_000) {
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
if (entryMessageText && normalizeTimelineMessageText(candidate?.messageText ?? "") !== entryMessageText) {
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
if (!bestMatch) {
|
|
1379
|
+
bestMatch = candidate;
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
const previousDiff = Math.abs((Number(bestMatch.createdAtMs) || 0) - entryCreatedAtMs);
|
|
1384
|
+
const nextDiff = Math.abs(candidateCreatedAtMs - entryCreatedAtMs);
|
|
1385
|
+
if (nextDiff < previousDiff) {
|
|
1386
|
+
bestMatch = candidate;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
if (!bestMatch || normalizeTimelineImagePaths(bestMatch.imagePaths).length === 0) {
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
const nextMessageText = normalizeTimelineMessageText(bestMatch.messageText ?? entryMessageText);
|
|
1395
|
+
return normalizeTimelineEntry({
|
|
1396
|
+
...entry,
|
|
1397
|
+
messageText: nextMessageText,
|
|
1398
|
+
summary: formatNotificationBody(nextMessageText, 180) || cleanText(entry.summary || ""),
|
|
1399
|
+
imagePaths: bestMatch.imagePaths,
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function findRolloutFileForThread(runtime, threadId) {
|
|
1404
|
+
const normalizedThreadId = cleanText(threadId || "");
|
|
1405
|
+
if (!normalizedThreadId) {
|
|
1406
|
+
return "";
|
|
1407
|
+
}
|
|
1408
|
+
return (
|
|
1409
|
+
(Array.isArray(runtime.knownFiles) ? runtime.knownFiles : []).find(
|
|
1410
|
+
(filePath) => extractThreadIdFromRolloutPath(filePath) === normalizedThreadId
|
|
1411
|
+
) || ""
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
async function readRecentRolloutUserMessagesWithImages({ filePath, maxBytes }) {
|
|
1416
|
+
let stat;
|
|
1417
|
+
try {
|
|
1418
|
+
stat = await fs.stat(filePath);
|
|
1419
|
+
} catch {
|
|
1420
|
+
return [];
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const readLength = Math.max(0, Math.min(Number(maxBytes) || 0, stat.size));
|
|
1424
|
+
const startOffset = Math.max(0, stat.size - readLength);
|
|
1425
|
+
let chunk = "";
|
|
1426
|
+
try {
|
|
1427
|
+
const handle = await fs.open(filePath, "r");
|
|
1428
|
+
const buffer = Buffer.alloc(readLength);
|
|
1429
|
+
try {
|
|
1430
|
+
await handle.read(buffer, 0, readLength, startOffset);
|
|
1431
|
+
} finally {
|
|
1432
|
+
await handle.close();
|
|
1433
|
+
}
|
|
1434
|
+
chunk = buffer.toString("utf8");
|
|
1435
|
+
} catch {
|
|
1436
|
+
return [];
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
const lines = chunk.split("\n");
|
|
1440
|
+
if (startOffset > 0 && lines.length > 0) {
|
|
1441
|
+
lines.shift();
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const matches = [];
|
|
1445
|
+
for (const rawLine of lines) {
|
|
1446
|
+
if (!rawLine.trim()) {
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
let record;
|
|
1451
|
+
try {
|
|
1452
|
+
record = JSON.parse(rawLine);
|
|
1453
|
+
} catch {
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
const extracted = extractRolloutUserMessage(record);
|
|
1458
|
+
if (!extracted || normalizeTimelineImagePaths(extracted.imagePaths).length === 0) {
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
matches.push({
|
|
1463
|
+
createdAtMs: Date.parse(record.timestamp ?? "") || 0,
|
|
1464
|
+
messageText: extracted.messageText,
|
|
1465
|
+
imagePaths: extracted.imagePaths,
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
return matches;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1184
1472
|
async function querySqliteTimelineRows({ logsDbFile, cursorId, minTsSec = 0 }) {
|
|
1185
1473
|
const conditions = [
|
|
1186
1474
|
`id > ${Math.max(0, Number(cursorId) || 0)}`,
|
|
@@ -1272,9 +1560,31 @@ function buildSqliteTimelineEntry({ row, config, runtime }) {
|
|
|
1272
1560
|
});
|
|
1273
1561
|
}
|
|
1274
1562
|
|
|
1275
|
-
function
|
|
1563
|
+
function extractRolloutUserMessage(record) {
|
|
1276
1564
|
const payload = isPlainObject(record?.payload) ? record.payload : null;
|
|
1277
|
-
if (!payload
|
|
1565
|
+
if (!payload) {
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
if (record?.type === "event_msg" && payload.type === "user_message") {
|
|
1570
|
+
const messageText = normalizeTimelineMessageText(payload.message ?? "");
|
|
1571
|
+
const imagePaths = normalizeTimelineImagePaths(payload.local_images ?? payload.localImagePaths ?? []);
|
|
1572
|
+
if (!messageText && imagePaths.length === 0) {
|
|
1573
|
+
return null;
|
|
1574
|
+
}
|
|
1575
|
+
return {
|
|
1576
|
+
itemId: cleanText(payload.turn_id || record.timestamp || ""),
|
|
1577
|
+
messageText,
|
|
1578
|
+
imagePaths,
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
if (payload.type !== "message" || payload.role !== "user") {
|
|
1583
|
+
return null;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
if (rolloutContentHasImages(payload.content)) {
|
|
1587
|
+
// Prefer the richer event_msg.user_message entry when images are attached.
|
|
1278
1588
|
return null;
|
|
1279
1589
|
}
|
|
1280
1590
|
|
|
@@ -1283,13 +1593,139 @@ function buildRolloutUserTimelineEntry({ record, fileState, runtime }) {
|
|
|
1283
1593
|
return null;
|
|
1284
1594
|
}
|
|
1285
1595
|
|
|
1596
|
+
return {
|
|
1597
|
+
itemId: cleanText(payload.id || record.timestamp || ""),
|
|
1598
|
+
messageText,
|
|
1599
|
+
imagePaths: [],
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
function extractTimestampPrefixFromImagePath(filePath) {
|
|
1604
|
+
const match = path.basename(cleanText(filePath || "")).match(/^(\d{10,})-/u);
|
|
1605
|
+
return match ? Number(match[1]) || 0 : 0;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
async function listReplyUploadFiles(config) {
|
|
1609
|
+
try {
|
|
1610
|
+
const entries = await fs.readdir(config.replyUploadsDir, { withFileTypes: true });
|
|
1611
|
+
return entries
|
|
1612
|
+
.filter((entry) => entry.isFile())
|
|
1613
|
+
.map((entry) => {
|
|
1614
|
+
const filePath = path.join(config.replyUploadsDir, entry.name);
|
|
1615
|
+
return {
|
|
1616
|
+
filePath,
|
|
1617
|
+
extension: path.extname(entry.name).toLowerCase(),
|
|
1618
|
+
ts: extractTimestampPrefixFromImagePath(entry.name),
|
|
1619
|
+
};
|
|
1620
|
+
})
|
|
1621
|
+
.filter((entry) => entry.ts > 0);
|
|
1622
|
+
} catch {
|
|
1623
|
+
return [];
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
async function findReplyUploadFallback(config, sourcePath, usedPaths = new Set()) {
|
|
1628
|
+
const targetTs = extractTimestampPrefixFromImagePath(sourcePath);
|
|
1629
|
+
const targetExtension = path.extname(cleanText(sourcePath || "")).toLowerCase();
|
|
1630
|
+
if (!targetTs) {
|
|
1631
|
+
return "";
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
const uploads = await listReplyUploadFiles(config);
|
|
1635
|
+
const candidates = uploads
|
|
1636
|
+
.filter((entry) => !usedPaths.has(entry.filePath))
|
|
1637
|
+
.filter((entry) => !targetExtension || entry.extension === targetExtension)
|
|
1638
|
+
.filter((entry) => Math.abs(entry.ts - targetTs) <= 60_000)
|
|
1639
|
+
.sort((left, right) => Math.abs(left.ts - targetTs) - Math.abs(right.ts - targetTs));
|
|
1640
|
+
|
|
1641
|
+
return candidates[0]?.filePath || "";
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
async function copyTimelineAttachmentToPersistentDir(config, sourcePath) {
|
|
1645
|
+
const normalizedSourcePath = resolvePath(cleanText(sourcePath || ""));
|
|
1646
|
+
if (!normalizedSourcePath) {
|
|
1647
|
+
return "";
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
await fs.mkdir(config.timelineAttachmentsDir, { recursive: true });
|
|
1651
|
+
const extension = path.extname(normalizedSourcePath) || ".img";
|
|
1652
|
+
const destinationPath = path.join(
|
|
1653
|
+
config.timelineAttachmentsDir,
|
|
1654
|
+
`${Date.now()}-${crypto.randomUUID()}${extension}`
|
|
1655
|
+
);
|
|
1656
|
+
await fs.copyFile(normalizedSourcePath, destinationPath);
|
|
1657
|
+
return destinationPath;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
async function normalizePersistedTimelineImagePaths({ config, state, imagePaths = [] }) {
|
|
1661
|
+
const normalizedImagePaths = normalizeTimelineImagePaths(imagePaths);
|
|
1662
|
+
if (normalizedImagePaths.length === 0) {
|
|
1663
|
+
return [];
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
const aliases = isPlainObject(state.timelineImagePathAliases) ? state.timelineImagePathAliases : (state.timelineImagePathAliases = {});
|
|
1667
|
+
const usedFallbacks = new Set();
|
|
1668
|
+
const nextPaths = [];
|
|
1669
|
+
|
|
1670
|
+
for (const rawPath of normalizedImagePaths) {
|
|
1671
|
+
const normalizedPath = cleanText(rawPath || "");
|
|
1672
|
+
if (!normalizedPath) {
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
const aliasedPath = cleanText(aliases[normalizedPath] || "");
|
|
1677
|
+
if (aliasedPath) {
|
|
1678
|
+
try {
|
|
1679
|
+
await fs.access(aliasedPath);
|
|
1680
|
+
nextPaths.push(aliasedPath);
|
|
1681
|
+
continue;
|
|
1682
|
+
} catch {
|
|
1683
|
+
// Fall through and repair below.
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
let existingSourcePath = normalizedPath;
|
|
1688
|
+
try {
|
|
1689
|
+
await fs.access(existingSourcePath);
|
|
1690
|
+
} catch {
|
|
1691
|
+
existingSourcePath = await findReplyUploadFallback(config, normalizedPath, usedFallbacks);
|
|
1692
|
+
if (!existingSourcePath) {
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
usedFallbacks.add(existingSourcePath);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
let persistentPath = existingSourcePath;
|
|
1699
|
+
if (!existingSourcePath.startsWith(`${config.timelineAttachmentsDir}${path.sep}`)) {
|
|
1700
|
+
persistentPath = await copyTimelineAttachmentToPersistentDir(config, existingSourcePath);
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
aliases[normalizedPath] = persistentPath;
|
|
1704
|
+
nextPaths.push(persistentPath);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
return normalizeTimelineImagePaths(nextPaths);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function buildRolloutUserTimelineEntry({ record, fileState, runtime }) {
|
|
1711
|
+
const extracted = extractRolloutUserMessage(record);
|
|
1712
|
+
if (!extracted) {
|
|
1713
|
+
return null;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1286
1716
|
const threadId = cleanText(fileState.threadId || "");
|
|
1287
1717
|
if (!threadId) {
|
|
1288
1718
|
return null;
|
|
1289
1719
|
}
|
|
1290
1720
|
|
|
1291
1721
|
const createdAtMs = Date.parse(record.timestamp ?? "") || Date.now();
|
|
1292
|
-
const stableId = messageTimelineStableId(
|
|
1722
|
+
const stableId = messageTimelineStableId(
|
|
1723
|
+
"user_message",
|
|
1724
|
+
threadId,
|
|
1725
|
+
extracted.itemId,
|
|
1726
|
+
extracted.messageText,
|
|
1727
|
+
createdAtMs
|
|
1728
|
+
);
|
|
1293
1729
|
const threadLabel = getNativeThreadLabel({
|
|
1294
1730
|
runtime,
|
|
1295
1731
|
conversationId: threadId,
|
|
@@ -1303,8 +1739,9 @@ function buildRolloutUserTimelineEntry({ record, fileState, runtime }) {
|
|
|
1303
1739
|
threadId,
|
|
1304
1740
|
threadLabel,
|
|
1305
1741
|
title: threadLabel || kindTitle(DEFAULT_LOCALE, "user_message"),
|
|
1306
|
-
summary: formatNotificationBody(messageText, 180) || messageText,
|
|
1307
|
-
messageText,
|
|
1742
|
+
summary: formatNotificationBody(extracted.messageText, 180) || extracted.messageText,
|
|
1743
|
+
messageText: extracted.messageText,
|
|
1744
|
+
imagePaths: extracted.imagePaths,
|
|
1308
1745
|
createdAtMs,
|
|
1309
1746
|
readOnly: true,
|
|
1310
1747
|
});
|
|
@@ -1316,7 +1753,7 @@ function buildHistoryUserTimelineEntry({ record, runtime, config }) {
|
|
|
1316
1753
|
}
|
|
1317
1754
|
|
|
1318
1755
|
const threadId = cleanText(record.session_id || record.sessionId || "");
|
|
1319
|
-
const messageText =
|
|
1756
|
+
const messageText = normalizeTimelineMessageText(record.text ?? "");
|
|
1320
1757
|
if (!threadId || !messageText) {
|
|
1321
1758
|
return null;
|
|
1322
1759
|
}
|
|
@@ -3303,19 +3740,142 @@ function runCurl(args) {
|
|
|
3303
3740
|
}
|
|
3304
3741
|
|
|
3305
3742
|
const IMPLEMENT_PLAN_PROMPT_PREFIX = "PLEASE IMPLEMENT THIS PLAN:";
|
|
3743
|
+
const COMPLETION_REPLY_WORKSPACE_STAGE_DIR = ".viveworker-attachments";
|
|
3744
|
+
const COMPLETION_REPLY_WORKSPACE_STAGE_TTL_MS = 60 * 60 * 1000;
|
|
3745
|
+
const COMPLETION_REPLY_WORKSPACE_STAGE_CLEANUP_DELAY_MS = 10 * 60 * 1000;
|
|
3306
3746
|
|
|
3307
3747
|
function buildImplementPlanPrompt(planContent) {
|
|
3308
3748
|
return `${IMPLEMENT_PLAN_PROMPT_PREFIX}\n${formatPlanDetailText(planContent)}`;
|
|
3309
3749
|
}
|
|
3310
3750
|
|
|
3311
|
-
function
|
|
3312
|
-
|
|
3313
|
-
|
|
3751
|
+
function buildTurnInput(text, options = {}) {
|
|
3752
|
+
const items = [];
|
|
3753
|
+
const normalizedText = String(text ?? "");
|
|
3754
|
+
const localImagePaths = Array.isArray(options?.localImagePaths)
|
|
3755
|
+
? options.localImagePaths
|
|
3756
|
+
.map((value) => cleanText(value || ""))
|
|
3757
|
+
.filter(Boolean)
|
|
3758
|
+
: [];
|
|
3759
|
+
|
|
3760
|
+
if (normalizedText) {
|
|
3761
|
+
items.push({
|
|
3314
3762
|
type: "text",
|
|
3315
|
-
text:
|
|
3763
|
+
text: normalizedText,
|
|
3316
3764
|
text_elements: [],
|
|
3317
|
-
}
|
|
3318
|
-
|
|
3765
|
+
});
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
for (const localImagePath of localImagePaths) {
|
|
3769
|
+
items.push({
|
|
3770
|
+
type: "local_image",
|
|
3771
|
+
path: localImagePath,
|
|
3772
|
+
});
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
return items;
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
function buildComposerStyleLocalImageInput(text, localImagePaths = []) {
|
|
3779
|
+
const items = [];
|
|
3780
|
+
const normalizedText = String(text ?? "");
|
|
3781
|
+
|
|
3782
|
+
if (normalizedText) {
|
|
3783
|
+
items.push({
|
|
3784
|
+
type: "text",
|
|
3785
|
+
text: normalizedText,
|
|
3786
|
+
text_elements: [],
|
|
3787
|
+
});
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
for (const localImagePath of localImagePaths) {
|
|
3791
|
+
const normalizedPath = cleanText(localImagePath || "");
|
|
3792
|
+
if (!normalizedPath) {
|
|
3793
|
+
continue;
|
|
3794
|
+
}
|
|
3795
|
+
items.push({
|
|
3796
|
+
type: "localImage",
|
|
3797
|
+
path: normalizedPath,
|
|
3798
|
+
});
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
return items;
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
function buildComposerStyleImageInput(text, imageDataUrls = []) {
|
|
3805
|
+
const items = [];
|
|
3806
|
+
const normalizedText = String(text ?? "");
|
|
3807
|
+
|
|
3808
|
+
if (normalizedText) {
|
|
3809
|
+
items.push({
|
|
3810
|
+
type: "text",
|
|
3811
|
+
text: normalizedText,
|
|
3812
|
+
text_elements: [],
|
|
3813
|
+
});
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
for (const imageUrl of imageDataUrls) {
|
|
3817
|
+
const normalizedUrl = cleanText(imageUrl || "");
|
|
3818
|
+
if (!normalizedUrl) {
|
|
3819
|
+
continue;
|
|
3820
|
+
}
|
|
3821
|
+
items.push({
|
|
3822
|
+
type: "image",
|
|
3823
|
+
url: normalizedUrl,
|
|
3824
|
+
});
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
return items;
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
function buildUserInputPayload(items, finalOutputJsonSchema = null) {
|
|
3831
|
+
return {
|
|
3832
|
+
items: Array.isArray(items) ? items : [],
|
|
3833
|
+
final_output_json_schema: finalOutputJsonSchema ?? null,
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3837
|
+
function buildTurnContentItems(text, imageDataUrls = []) {
|
|
3838
|
+
const items = [];
|
|
3839
|
+
const normalizedText = String(text ?? "");
|
|
3840
|
+
if (normalizedText) {
|
|
3841
|
+
items.push({
|
|
3842
|
+
type: "input_text",
|
|
3843
|
+
text: normalizedText,
|
|
3844
|
+
});
|
|
3845
|
+
}
|
|
3846
|
+
for (const imageUrl of imageDataUrls) {
|
|
3847
|
+
if (!imageUrl) {
|
|
3848
|
+
continue;
|
|
3849
|
+
}
|
|
3850
|
+
items.push({
|
|
3851
|
+
type: "input_image",
|
|
3852
|
+
image_url: imageUrl,
|
|
3853
|
+
detail: "original",
|
|
3854
|
+
});
|
|
3855
|
+
}
|
|
3856
|
+
return items;
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
function buildTurnImageItems(text, imageDataUrls = []) {
|
|
3860
|
+
const items = [];
|
|
3861
|
+
const normalizedText = String(text ?? "");
|
|
3862
|
+
if (normalizedText) {
|
|
3863
|
+
items.push({
|
|
3864
|
+
type: "text",
|
|
3865
|
+
text: normalizedText,
|
|
3866
|
+
text_elements: [],
|
|
3867
|
+
});
|
|
3868
|
+
}
|
|
3869
|
+
for (const imageUrl of imageDataUrls) {
|
|
3870
|
+
if (!imageUrl) {
|
|
3871
|
+
continue;
|
|
3872
|
+
}
|
|
3873
|
+
items.push({
|
|
3874
|
+
type: "image",
|
|
3875
|
+
image_url: imageUrl,
|
|
3876
|
+
});
|
|
3877
|
+
}
|
|
3878
|
+
return items;
|
|
3319
3879
|
}
|
|
3320
3880
|
|
|
3321
3881
|
function buildRequestedCollaborationMode(threadState, requestedMode = "default") {
|
|
@@ -3338,6 +3898,49 @@ function buildRequestedCollaborationMode(threadState, requestedMode = "default")
|
|
|
3338
3898
|
};
|
|
3339
3899
|
}
|
|
3340
3900
|
|
|
3901
|
+
function normalizeIpcErrorMessage(errorValue) {
|
|
3902
|
+
if (typeof errorValue === "string") {
|
|
3903
|
+
return cleanText(errorValue || "") || "ipc-request-failed";
|
|
3904
|
+
}
|
|
3905
|
+
if (errorValue instanceof Error) {
|
|
3906
|
+
return cleanText(errorValue.message || "") || errorValue.name || "ipc-request-failed";
|
|
3907
|
+
}
|
|
3908
|
+
if (Array.isArray(errorValue)) {
|
|
3909
|
+
try {
|
|
3910
|
+
return JSON.stringify(errorValue);
|
|
3911
|
+
} catch {
|
|
3912
|
+
return "ipc-request-failed";
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
if (isPlainObject(errorValue)) {
|
|
3916
|
+
const candidateFields = [
|
|
3917
|
+
errorValue.message,
|
|
3918
|
+
errorValue.error,
|
|
3919
|
+
errorValue.details,
|
|
3920
|
+
errorValue.reason,
|
|
3921
|
+
];
|
|
3922
|
+
const directMessage = candidateFields
|
|
3923
|
+
.map((value) => (typeof value === "string" ? cleanText(value || "") : ""))
|
|
3924
|
+
.find(Boolean);
|
|
3925
|
+
if (directMessage) {
|
|
3926
|
+
return directMessage;
|
|
3927
|
+
}
|
|
3928
|
+
try {
|
|
3929
|
+
return JSON.stringify(errorValue);
|
|
3930
|
+
} catch {
|
|
3931
|
+
return "ipc-request-failed";
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
if (errorValue && typeof errorValue === "object") {
|
|
3935
|
+
try {
|
|
3936
|
+
return JSON.stringify(errorValue);
|
|
3937
|
+
} catch {
|
|
3938
|
+
return "ipc-request-failed";
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
return cleanText(String(errorValue ?? "")) || "ipc-request-failed";
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3341
3944
|
function buildDefaultCollaborationMode(threadState) {
|
|
3342
3945
|
// Fallback turns must leave Plan mode unless the caller explicitly opts in.
|
|
3343
3946
|
return buildRequestedCollaborationMode(threadState, "default");
|
|
@@ -3417,6 +4020,18 @@ class NativeIpcClient {
|
|
|
3417
4020
|
);
|
|
3418
4021
|
}
|
|
3419
4022
|
|
|
4023
|
+
async startTurnDirect(conversationId, turnStartParams, ownerClientId = null) {
|
|
4024
|
+
const targetClientId =
|
|
4025
|
+
ownerClientId ??
|
|
4026
|
+
this.runtime.threadOwnerClientIds.get(conversationId) ??
|
|
4027
|
+
null;
|
|
4028
|
+
return this.sendRequest(
|
|
4029
|
+
"turn/start",
|
|
4030
|
+
buildDirectTurnStartPayload(conversationId, turnStartParams),
|
|
4031
|
+
{ targetClientId }
|
|
4032
|
+
);
|
|
4033
|
+
}
|
|
4034
|
+
|
|
3420
4035
|
async submitUserInputRequest(conversationId, requestId, answers, ownerClientId = null) {
|
|
3421
4036
|
return this.sendThreadFollowerRequest(
|
|
3422
4037
|
"thread-follower-submit-user-input-request",
|
|
@@ -3573,7 +4188,14 @@ class NativeIpcClient {
|
|
|
3573
4188
|
clearTimeout(pending.timeout);
|
|
3574
4189
|
|
|
3575
4190
|
if (message.resultType === "error") {
|
|
3576
|
-
|
|
4191
|
+
console.log(
|
|
4192
|
+
`[ipc] error method=${cleanText(message.method || "") || "unknown"} requestId=${cleanText(message.requestId || "") || "unknown"} payload=${inspect(message.error, { depth: 6, breakLength: 160 })}`
|
|
4193
|
+
);
|
|
4194
|
+
const error = new Error(normalizeIpcErrorMessage(message.error));
|
|
4195
|
+
if (message.error && typeof message.error === "object") {
|
|
4196
|
+
error.ipcError = message.error;
|
|
4197
|
+
}
|
|
4198
|
+
pending.reject(error);
|
|
3577
4199
|
return;
|
|
3578
4200
|
}
|
|
3579
4201
|
|
|
@@ -3954,10 +4576,68 @@ function getNativeThreadLabel({ runtime, conversationId, cwd }) {
|
|
|
3954
4576
|
return rolloutLabel;
|
|
3955
4577
|
}
|
|
3956
4578
|
}
|
|
3957
|
-
if (cwd) {
|
|
3958
|
-
return truncate(cleanText(path.basename(cwd)), 90) || shortId(normalizedConversationId);
|
|
4579
|
+
if (cwd) {
|
|
4580
|
+
return truncate(cleanText(path.basename(cwd)), 90) || shortId(normalizedConversationId);
|
|
4581
|
+
}
|
|
4582
|
+
return shortId(normalizedConversationId) || "Codex task";
|
|
4583
|
+
}
|
|
4584
|
+
|
|
4585
|
+
async function findRolloutThreadCwd(runtime, conversationId) {
|
|
4586
|
+
const normalizedConversationId = cleanText(conversationId || "");
|
|
4587
|
+
if (!normalizedConversationId) {
|
|
4588
|
+
return "";
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
const cachedCwd = resolvePath(cleanText(runtime.rolloutThreadCwds?.get(normalizedConversationId) || ""));
|
|
4592
|
+
if (cachedCwd) {
|
|
4593
|
+
return cachedCwd;
|
|
4594
|
+
}
|
|
4595
|
+
|
|
4596
|
+
const knownFiles = Array.isArray(runtime.knownFiles) ? runtime.knownFiles : [];
|
|
4597
|
+
if (!knownFiles.length) {
|
|
4598
|
+
return "";
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
const prioritizedFiles = [];
|
|
4602
|
+
const fallbackFiles = [];
|
|
4603
|
+
for (const filePath of knownFiles) {
|
|
4604
|
+
if (extractThreadIdFromRolloutPath(filePath) === normalizedConversationId) {
|
|
4605
|
+
prioritizedFiles.push(filePath);
|
|
4606
|
+
} else {
|
|
4607
|
+
fallbackFiles.push(filePath);
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
|
|
4611
|
+
const filesToInspect = prioritizedFiles.length ? [...prioritizedFiles, ...fallbackFiles] : fallbackFiles;
|
|
4612
|
+
for (const filePath of filesToInspect) {
|
|
4613
|
+
const metadata = await extractRolloutThreadMetadata(filePath);
|
|
4614
|
+
if (cleanText(metadata?.threadId || "") !== normalizedConversationId) {
|
|
4615
|
+
continue;
|
|
4616
|
+
}
|
|
4617
|
+
const resolvedCwd = resolvePath(cleanText(metadata?.cwd || ""));
|
|
4618
|
+
if (resolvedCwd) {
|
|
4619
|
+
runtime.rolloutThreadCwds.set(normalizedConversationId, resolvedCwd);
|
|
4620
|
+
return resolvedCwd;
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
|
|
4624
|
+
return "";
|
|
4625
|
+
}
|
|
4626
|
+
|
|
4627
|
+
async function resolveConversationCwd(runtime, conversationId) {
|
|
4628
|
+
const normalizedConversationId = cleanText(conversationId || "");
|
|
4629
|
+
if (!normalizedConversationId) {
|
|
4630
|
+
return "";
|
|
3959
4631
|
}
|
|
3960
|
-
|
|
4632
|
+
|
|
4633
|
+
const threadStateCwd = resolvePath(
|
|
4634
|
+
cleanText(runtime.threadStates.get(normalizedConversationId)?.cwd || "")
|
|
4635
|
+
);
|
|
4636
|
+
if (threadStateCwd) {
|
|
4637
|
+
return threadStateCwd;
|
|
4638
|
+
}
|
|
4639
|
+
|
|
4640
|
+
return await findRolloutThreadCwd(runtime, normalizedConversationId);
|
|
3961
4641
|
}
|
|
3962
4642
|
|
|
3963
4643
|
function formatNativeApprovalMessage(kind, params, locale = config?.defaultLocale || DEFAULT_LOCALE) {
|
|
@@ -5119,6 +5799,28 @@ function buildOperationalTimelineEntries(runtime, state, config, locale) {
|
|
|
5119
5799
|
return items.filter(Boolean);
|
|
5120
5800
|
}
|
|
5121
5801
|
|
|
5802
|
+
function sanitizeTimelineThreadFilterLabel(value, threadId = "") {
|
|
5803
|
+
const normalized = cleanText(value || "");
|
|
5804
|
+
if (!normalized) {
|
|
5805
|
+
return "";
|
|
5806
|
+
}
|
|
5807
|
+
|
|
5808
|
+
const normalizedThreadId = cleanText(threadId || "");
|
|
5809
|
+
if (normalizedThreadId && (normalized === normalizedThreadId || normalized === shortId(normalizedThreadId))) {
|
|
5810
|
+
return "";
|
|
5811
|
+
}
|
|
5812
|
+
|
|
5813
|
+
if (/^[0-9a-f]{8}(?:-[0-9a-f]{4}){0,4}$/iu.test(normalized)) {
|
|
5814
|
+
return "";
|
|
5815
|
+
}
|
|
5816
|
+
|
|
5817
|
+
if (looksLikeGeneratedThreadTitle(normalized)) {
|
|
5818
|
+
return "";
|
|
5819
|
+
}
|
|
5820
|
+
|
|
5821
|
+
return normalized;
|
|
5822
|
+
}
|
|
5823
|
+
|
|
5122
5824
|
function buildTimelineThreads(entries, config) {
|
|
5123
5825
|
const byThread = new Map();
|
|
5124
5826
|
for (const entry of entries) {
|
|
@@ -5126,11 +5828,14 @@ function buildTimelineThreads(entries, config) {
|
|
|
5126
5828
|
if (!threadId) {
|
|
5127
5829
|
continue;
|
|
5128
5830
|
}
|
|
5831
|
+
const preferredLabel =
|
|
5832
|
+
sanitizeTimelineThreadFilterLabel(entry.threadLabel || "", threadId) ||
|
|
5833
|
+
t(DEFAULT_LOCALE, "server.fallback.codexTask");
|
|
5129
5834
|
const existing = byThread.get(threadId);
|
|
5130
5835
|
if (!existing) {
|
|
5131
5836
|
byThread.set(threadId, {
|
|
5132
5837
|
id: threadId,
|
|
5133
|
-
label:
|
|
5838
|
+
label: preferredLabel,
|
|
5134
5839
|
latestAtMs: Number(entry.createdAtMs) || 0,
|
|
5135
5840
|
preview: cleanText(entry.summary || entry.title || ""),
|
|
5136
5841
|
entryCount: 1,
|
|
@@ -5140,7 +5845,7 @@ function buildTimelineThreads(entries, config) {
|
|
|
5140
5845
|
existing.entryCount += 1;
|
|
5141
5846
|
if (Number(entry.createdAtMs) > Number(existing.latestAtMs)) {
|
|
5142
5847
|
existing.latestAtMs = Number(entry.createdAtMs) || existing.latestAtMs;
|
|
5143
|
-
existing.label =
|
|
5848
|
+
existing.label = preferredLabel || existing.label;
|
|
5144
5849
|
existing.preview = cleanText(entry.summary || entry.title || "") || existing.preview;
|
|
5145
5850
|
}
|
|
5146
5851
|
}
|
|
@@ -5166,6 +5871,7 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
5166
5871
|
threadId: entry.threadId,
|
|
5167
5872
|
threadLabel: entry.threadLabel,
|
|
5168
5873
|
summary: entry.summary,
|
|
5874
|
+
imageUrls: buildTimelineEntryImageUrls(entry),
|
|
5169
5875
|
createdAtMs: entry.createdAtMs,
|
|
5170
5876
|
}));
|
|
5171
5877
|
|
|
@@ -5228,6 +5934,49 @@ function buildPreviousApprovalContext(runtime, approval) {
|
|
|
5228
5934
|
};
|
|
5229
5935
|
}
|
|
5230
5936
|
|
|
5937
|
+
function buildInterruptedTimelineContext(runtime, entry, locale) {
|
|
5938
|
+
if (!runtime || !isTurnAbortedDisplayMessage(entry?.messageText)) {
|
|
5939
|
+
return null;
|
|
5940
|
+
}
|
|
5941
|
+
|
|
5942
|
+
const threadId = cleanText(entry?.threadId || "");
|
|
5943
|
+
const interruptedCreatedAtMs = Number(entry?.createdAtMs) || 0;
|
|
5944
|
+
if (!threadId || !interruptedCreatedAtMs) {
|
|
5945
|
+
return null;
|
|
5946
|
+
}
|
|
5947
|
+
|
|
5948
|
+
const previousEntry = runtime.recentTimelineEntries
|
|
5949
|
+
.filter((candidate) => {
|
|
5950
|
+
if (!timelineMessageKinds.has(cleanText(candidate?.kind || ""))) {
|
|
5951
|
+
return false;
|
|
5952
|
+
}
|
|
5953
|
+
if (cleanText(candidate?.threadId || "") !== threadId) {
|
|
5954
|
+
return false;
|
|
5955
|
+
}
|
|
5956
|
+
if (Number(candidate?.createdAtMs) <= 0 || Number(candidate?.createdAtMs) >= interruptedCreatedAtMs) {
|
|
5957
|
+
return false;
|
|
5958
|
+
}
|
|
5959
|
+
return !isTurnAbortedDisplayMessage(candidate?.messageText);
|
|
5960
|
+
})
|
|
5961
|
+
.sort((left, right) => Number(right?.createdAtMs ?? 0) - Number(left?.createdAtMs ?? 0))[0];
|
|
5962
|
+
|
|
5963
|
+
if (!previousEntry) {
|
|
5964
|
+
return null;
|
|
5965
|
+
}
|
|
5966
|
+
|
|
5967
|
+
const sourceText = normalizeLongText(previousEntry.messageText || previousEntry.summary || "");
|
|
5968
|
+
if (!sourceText) {
|
|
5969
|
+
return null;
|
|
5970
|
+
}
|
|
5971
|
+
|
|
5972
|
+
return {
|
|
5973
|
+
kind: previousEntry.kind,
|
|
5974
|
+
label: t(locale, "detail.interruptedTask"),
|
|
5975
|
+
createdAtMs: Number(previousEntry.createdAtMs) || 0,
|
|
5976
|
+
messageHtml: renderMessageHtml(sourceText, "<p></p>"),
|
|
5977
|
+
};
|
|
5978
|
+
}
|
|
5979
|
+
|
|
5231
5980
|
function buildPendingPlanDetail(planRequest, locale) {
|
|
5232
5981
|
return {
|
|
5233
5982
|
kind: "plan",
|
|
@@ -5371,19 +6120,32 @@ function buildHistoryDetail(item, locale, runtime = null) {
|
|
|
5371
6120
|
threadLabel: item.threadLabel || "",
|
|
5372
6121
|
createdAtMs: Number(item.createdAtMs) || 0,
|
|
5373
6122
|
messageHtml: renderMessageHtml(item.messageText, `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`),
|
|
6123
|
+
interruptNotice: interruptedDetailNotice(item.messageText, locale),
|
|
5374
6124
|
readOnly: true,
|
|
5375
6125
|
reply: replyEnabled
|
|
5376
6126
|
? {
|
|
5377
6127
|
enabled: true,
|
|
5378
6128
|
supportsPlanMode: true,
|
|
5379
|
-
supportsImages:
|
|
6129
|
+
supportsImages: true,
|
|
5380
6130
|
}
|
|
5381
6131
|
: null,
|
|
5382
6132
|
actions: [],
|
|
5383
6133
|
};
|
|
5384
6134
|
}
|
|
5385
6135
|
|
|
5386
|
-
function
|
|
6136
|
+
function buildTimelineEntryImageUrls(entry) {
|
|
6137
|
+
const imagePaths = normalizeTimelineImagePaths(entry?.imagePaths ?? []);
|
|
6138
|
+
if (imagePaths.length === 0) {
|
|
6139
|
+
return [];
|
|
6140
|
+
}
|
|
6141
|
+
const token = cleanText(entry?.token || "");
|
|
6142
|
+
if (!token) {
|
|
6143
|
+
return [];
|
|
6144
|
+
}
|
|
6145
|
+
return imagePaths.map((_, index) => `/api/timeline/${encodeURIComponent(token)}/images/${index}`);
|
|
6146
|
+
}
|
|
6147
|
+
|
|
6148
|
+
function buildTimelineMessageDetail(entry, locale, runtime = null) {
|
|
5387
6149
|
return {
|
|
5388
6150
|
kind: entry.kind,
|
|
5389
6151
|
token: entry.token,
|
|
@@ -5392,6 +6154,9 @@ function buildTimelineMessageDetail(entry, locale) {
|
|
|
5392
6154
|
threadLabel: entry.threadLabel || "",
|
|
5393
6155
|
createdAtMs: Number(entry.createdAtMs) || 0,
|
|
5394
6156
|
messageHtml: renderMessageHtml(entry.messageText, `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`),
|
|
6157
|
+
imageUrls: buildTimelineEntryImageUrls(entry),
|
|
6158
|
+
previousContext: buildInterruptedTimelineContext(runtime, entry, locale),
|
|
6159
|
+
interruptNotice: interruptedDetailNotice(entry.messageText, locale),
|
|
5395
6160
|
readOnly: true,
|
|
5396
6161
|
actions: [],
|
|
5397
6162
|
};
|
|
@@ -5484,8 +6249,224 @@ function normalizeCompletionReplyLocalImagePaths(paths) {
|
|
|
5484
6249
|
.filter(Boolean);
|
|
5485
6250
|
}
|
|
5486
6251
|
|
|
6252
|
+
function guessImageMimeTypeFromPath(filePath) {
|
|
6253
|
+
const extension = path.extname(cleanText(filePath || "")).toLowerCase();
|
|
6254
|
+
const mimeTypes = {
|
|
6255
|
+
".jpg": "image/jpeg",
|
|
6256
|
+
".jpeg": "image/jpeg",
|
|
6257
|
+
".png": "image/png",
|
|
6258
|
+
".webp": "image/webp",
|
|
6259
|
+
".gif": "image/gif",
|
|
6260
|
+
".heic": "image/heic",
|
|
6261
|
+
".heif": "image/heif",
|
|
6262
|
+
};
|
|
6263
|
+
return mimeTypes[extension] || "application/octet-stream";
|
|
6264
|
+
}
|
|
6265
|
+
|
|
6266
|
+
async function buildCompletionReplyImageDataUrls(localImagePaths) {
|
|
6267
|
+
const urls = [];
|
|
6268
|
+
for (const filePath of localImagePaths) {
|
|
6269
|
+
const buffer = await fs.readFile(filePath);
|
|
6270
|
+
const mimeType = guessImageMimeTypeFromPath(filePath);
|
|
6271
|
+
urls.push(`data:${mimeType};base64,${buffer.toString("base64")}`);
|
|
6272
|
+
}
|
|
6273
|
+
return urls;
|
|
6274
|
+
}
|
|
6275
|
+
|
|
6276
|
+
function scheduleBestEffortFileCleanup(paths, delayMs = COMPLETION_REPLY_WORKSPACE_STAGE_CLEANUP_DELAY_MS) {
|
|
6277
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
6278
|
+
return;
|
|
6279
|
+
}
|
|
6280
|
+
|
|
6281
|
+
const timer = setTimeout(async () => {
|
|
6282
|
+
await Promise.all(
|
|
6283
|
+
paths.map(async (filePath) => {
|
|
6284
|
+
try {
|
|
6285
|
+
await fs.rm(filePath, { force: true });
|
|
6286
|
+
} catch {
|
|
6287
|
+
// Ignore best-effort cleanup errors.
|
|
6288
|
+
}
|
|
6289
|
+
})
|
|
6290
|
+
);
|
|
6291
|
+
}, delayMs);
|
|
6292
|
+
timer.unref?.();
|
|
6293
|
+
}
|
|
6294
|
+
|
|
6295
|
+
function buildDirectTurnStartPayload(conversationId, turnStartParams = {}) {
|
|
6296
|
+
return {
|
|
6297
|
+
threadId: cleanText(conversationId || ""),
|
|
6298
|
+
input: Array.isArray(turnStartParams.input) ? turnStartParams.input : [],
|
|
6299
|
+
cwd: cleanText(turnStartParams.cwd || "") || null,
|
|
6300
|
+
approvalPolicy: turnStartParams.approvalPolicy ?? null,
|
|
6301
|
+
approvalsReviewer: cleanText(turnStartParams.approvalsReviewer || "") || "user",
|
|
6302
|
+
sandboxPolicy: turnStartParams.sandboxPolicy ?? null,
|
|
6303
|
+
model: turnStartParams.model ?? null,
|
|
6304
|
+
serviceTier: turnStartParams.serviceTier ?? null,
|
|
6305
|
+
effort: turnStartParams.effort ?? null,
|
|
6306
|
+
summary: cleanText(turnStartParams.summary || "") || "none",
|
|
6307
|
+
personality: turnStartParams.personality ?? null,
|
|
6308
|
+
outputSchema: turnStartParams.outputSchema ?? null,
|
|
6309
|
+
collaborationMode: isPlainObject(turnStartParams.collaborationMode)
|
|
6310
|
+
? turnStartParams.collaborationMode
|
|
6311
|
+
: null,
|
|
6312
|
+
attachments: Array.isArray(turnStartParams.attachments) ? turnStartParams.attachments : [],
|
|
6313
|
+
};
|
|
6314
|
+
}
|
|
6315
|
+
|
|
6316
|
+
async function cleanupExpiredWorkspaceReplyImages(stageDir) {
|
|
6317
|
+
try {
|
|
6318
|
+
const entries = await fs.readdir(stageDir, { withFileTypes: true });
|
|
6319
|
+
const cutoffMs = Date.now() - COMPLETION_REPLY_WORKSPACE_STAGE_TTL_MS;
|
|
6320
|
+
await Promise.all(
|
|
6321
|
+
entries.map(async (entry) => {
|
|
6322
|
+
if (!entry.isFile()) {
|
|
6323
|
+
return;
|
|
6324
|
+
}
|
|
6325
|
+
const filePath = path.join(stageDir, entry.name);
|
|
6326
|
+
try {
|
|
6327
|
+
const stat = await fs.stat(filePath);
|
|
6328
|
+
if (Number(stat.mtimeMs) < cutoffMs) {
|
|
6329
|
+
await fs.rm(filePath, { force: true });
|
|
6330
|
+
}
|
|
6331
|
+
} catch {
|
|
6332
|
+
// Ignore best-effort cleanup errors.
|
|
6333
|
+
}
|
|
6334
|
+
})
|
|
6335
|
+
);
|
|
6336
|
+
} catch {
|
|
6337
|
+
// Ignore missing stage dir.
|
|
6338
|
+
}
|
|
6339
|
+
}
|
|
6340
|
+
|
|
6341
|
+
async function stageCompletionReplyImagesForThreadCwd(localImagePaths, cwd) {
|
|
6342
|
+
const normalizedCwd = resolvePath(cleanText(cwd || ""));
|
|
6343
|
+
if (!normalizedCwd || !Array.isArray(localImagePaths) || localImagePaths.length === 0) {
|
|
6344
|
+
return [];
|
|
6345
|
+
}
|
|
6346
|
+
|
|
6347
|
+
const stageDir = path.join(normalizedCwd, COMPLETION_REPLY_WORKSPACE_STAGE_DIR);
|
|
6348
|
+
await cleanupExpiredWorkspaceReplyImages(stageDir);
|
|
6349
|
+
await fs.mkdir(stageDir, { recursive: true });
|
|
6350
|
+
|
|
6351
|
+
const stagedPaths = [];
|
|
6352
|
+
for (const sourcePath of localImagePaths) {
|
|
6353
|
+
const extension = path.extname(cleanText(sourcePath || "")) || ".img";
|
|
6354
|
+
const stagedPath = path.join(stageDir, `${Date.now()}-${crypto.randomUUID()}${extension}`);
|
|
6355
|
+
await fs.copyFile(sourcePath, stagedPath);
|
|
6356
|
+
stagedPaths.push(stagedPath);
|
|
6357
|
+
}
|
|
6358
|
+
return stagedPaths;
|
|
6359
|
+
}
|
|
6360
|
+
|
|
6361
|
+
async function buildCompletionReplyTurnCandidates(
|
|
6362
|
+
messageText,
|
|
6363
|
+
localImagePaths,
|
|
6364
|
+
collaborationMode,
|
|
6365
|
+
cwd = null,
|
|
6366
|
+
workspaceLocalImagePaths = []
|
|
6367
|
+
) {
|
|
6368
|
+
const baseCandidate = {
|
|
6369
|
+
attachments: [],
|
|
6370
|
+
cwd: cleanText(cwd || "") || null,
|
|
6371
|
+
approvalPolicy: null,
|
|
6372
|
+
sandboxPolicy: null,
|
|
6373
|
+
model: null,
|
|
6374
|
+
serviceTier: null,
|
|
6375
|
+
effort: null,
|
|
6376
|
+
summary: "none",
|
|
6377
|
+
personality: null,
|
|
6378
|
+
outputSchema: null,
|
|
6379
|
+
collaborationMode,
|
|
6380
|
+
};
|
|
6381
|
+
|
|
6382
|
+
if (!localImagePaths.length) {
|
|
6383
|
+
return [
|
|
6384
|
+
{
|
|
6385
|
+
name: "text-only",
|
|
6386
|
+
transport: "thread-follower",
|
|
6387
|
+
turnStartParams: {
|
|
6388
|
+
...baseCandidate,
|
|
6389
|
+
input: buildTurnInput(messageText),
|
|
6390
|
+
localImagePaths: [],
|
|
6391
|
+
local_image_paths: [],
|
|
6392
|
+
remoteImageUrls: [],
|
|
6393
|
+
remote_image_urls: [],
|
|
6394
|
+
},
|
|
6395
|
+
},
|
|
6396
|
+
];
|
|
6397
|
+
}
|
|
6398
|
+
|
|
6399
|
+
const imageDataUrls = await buildCompletionReplyImageDataUrls(localImagePaths);
|
|
6400
|
+
const workspaceImagePaths = normalizeCompletionReplyLocalImagePaths(workspaceLocalImagePaths);
|
|
6401
|
+
const candidates = [];
|
|
6402
|
+
|
|
6403
|
+
if (workspaceImagePaths.length) {
|
|
6404
|
+
candidates.push({
|
|
6405
|
+
// Match the Desktop composer path as closely as possible:
|
|
6406
|
+
// text + localImage(path) passed through the normal thread-follower route.
|
|
6407
|
+
name: "workspace-local-image-composer-input",
|
|
6408
|
+
transport: "thread-follower",
|
|
6409
|
+
turnStartParams: {
|
|
6410
|
+
...baseCandidate,
|
|
6411
|
+
input: buildComposerStyleLocalImageInput(messageText, workspaceImagePaths),
|
|
6412
|
+
localImagePaths: [],
|
|
6413
|
+
local_image_paths: [],
|
|
6414
|
+
remoteImageUrls: [],
|
|
6415
|
+
remote_image_urls: [],
|
|
6416
|
+
},
|
|
6417
|
+
});
|
|
6418
|
+
}
|
|
6419
|
+
|
|
6420
|
+
candidates.push(
|
|
6421
|
+
{
|
|
6422
|
+
// This mirrors the desktop composer input items before submission:
|
|
6423
|
+
// text + image(url=data:image/...).
|
|
6424
|
+
name: "image-data-url-composer-input",
|
|
6425
|
+
transport: "thread-follower",
|
|
6426
|
+
turnStartParams: {
|
|
6427
|
+
...baseCandidate,
|
|
6428
|
+
input: buildComposerStyleImageInput(messageText, imageDataUrls),
|
|
6429
|
+
localImagePaths: [],
|
|
6430
|
+
local_image_paths: [],
|
|
6431
|
+
remoteImageUrls: [],
|
|
6432
|
+
remote_image_urls: [],
|
|
6433
|
+
},
|
|
6434
|
+
},
|
|
6435
|
+
{
|
|
6436
|
+
name: "local-image-composer-input",
|
|
6437
|
+
transport: "thread-follower",
|
|
6438
|
+
turnStartParams: {
|
|
6439
|
+
...baseCandidate,
|
|
6440
|
+
input: buildComposerStyleLocalImageInput(messageText, localImagePaths),
|
|
6441
|
+
localImagePaths: [],
|
|
6442
|
+
local_image_paths: [],
|
|
6443
|
+
remoteImageUrls: [],
|
|
6444
|
+
remote_image_urls: [],
|
|
6445
|
+
},
|
|
6446
|
+
},
|
|
6447
|
+
{
|
|
6448
|
+
// This currently reaches Codex, but the image is dropped before the final
|
|
6449
|
+
// UserInput core submission. Keep it last as a diagnostic fallback.
|
|
6450
|
+
name: "remote-image-urls-data-url",
|
|
6451
|
+
transport: "thread-follower",
|
|
6452
|
+
turnStartParams: {
|
|
6453
|
+
...baseCandidate,
|
|
6454
|
+
input: buildTurnInput(messageText),
|
|
6455
|
+
localImagePaths: [],
|
|
6456
|
+
local_image_paths: [],
|
|
6457
|
+
remoteImageUrls: imageDataUrls,
|
|
6458
|
+
remote_image_urls: imageDataUrls,
|
|
6459
|
+
},
|
|
6460
|
+
}
|
|
6461
|
+
);
|
|
6462
|
+
|
|
6463
|
+
return candidates;
|
|
6464
|
+
}
|
|
6465
|
+
|
|
5487
6466
|
async function handleCompletionReply({
|
|
6467
|
+
config,
|
|
5488
6468
|
runtime,
|
|
6469
|
+
state,
|
|
5489
6470
|
completionItem,
|
|
5490
6471
|
text,
|
|
5491
6472
|
planMode = false,
|
|
@@ -5520,34 +6501,97 @@ async function handleCompletionReply({
|
|
|
5520
6501
|
}
|
|
5521
6502
|
|
|
5522
6503
|
const threadState = runtime.threadStates.get(conversationId) ?? null;
|
|
6504
|
+
const resolvedCwd = await resolveConversationCwd(runtime, conversationId);
|
|
6505
|
+
const stagedWorkspaceImagePaths = await stageCompletionReplyImagesForThreadCwd(
|
|
6506
|
+
normalizedLocalImagePaths,
|
|
6507
|
+
resolvedCwd
|
|
6508
|
+
);
|
|
6509
|
+
const timelineImageAliases = [];
|
|
6510
|
+
if (normalizedLocalImagePaths.length > 0) {
|
|
6511
|
+
const persistentTimelineImagePaths = await normalizePersistedTimelineImagePaths({
|
|
6512
|
+
config,
|
|
6513
|
+
state,
|
|
6514
|
+
imagePaths: normalizedLocalImagePaths,
|
|
6515
|
+
});
|
|
6516
|
+
for (let index = 0; index < persistentTimelineImagePaths.length; index += 1) {
|
|
6517
|
+
const persistentPath = cleanText(persistentTimelineImagePaths[index] || "");
|
|
6518
|
+
if (!persistentPath) {
|
|
6519
|
+
continue;
|
|
6520
|
+
}
|
|
6521
|
+
const uploadPath = cleanText(normalizedLocalImagePaths[index] || "");
|
|
6522
|
+
const stagedPath = cleanText(stagedWorkspaceImagePaths[index] || "");
|
|
6523
|
+
if (uploadPath) {
|
|
6524
|
+
timelineImageAliases.push([uploadPath, persistentPath]);
|
|
6525
|
+
}
|
|
6526
|
+
if (stagedPath) {
|
|
6527
|
+
timelineImageAliases.push([stagedPath, persistentPath]);
|
|
6528
|
+
}
|
|
6529
|
+
}
|
|
6530
|
+
}
|
|
5523
6531
|
const collaborationMode = buildRequestedCollaborationMode(
|
|
5524
6532
|
threadState,
|
|
5525
6533
|
planMode ? "plan" : "default"
|
|
5526
6534
|
);
|
|
5527
|
-
const
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
localImagePaths: normalizedLocalImagePaths,
|
|
5531
|
-
local_image_paths: normalizedLocalImagePaths,
|
|
5532
|
-
remoteImageUrls: [],
|
|
5533
|
-
remote_image_urls: [],
|
|
5534
|
-
cwd: null,
|
|
5535
|
-
approvalPolicy: null,
|
|
5536
|
-
sandboxPolicy: null,
|
|
5537
|
-
model: null,
|
|
5538
|
-
serviceTier: null,
|
|
5539
|
-
effort: null,
|
|
5540
|
-
summary: "none",
|
|
5541
|
-
personality: null,
|
|
5542
|
-
outputSchema: null,
|
|
6535
|
+
const turnCandidates = await buildCompletionReplyTurnCandidates(
|
|
6536
|
+
messageText,
|
|
6537
|
+
normalizedLocalImagePaths,
|
|
5543
6538
|
collaborationMode,
|
|
5544
|
-
|
|
6539
|
+
resolvedCwd,
|
|
6540
|
+
stagedWorkspaceImagePaths
|
|
6541
|
+
);
|
|
6542
|
+
let lastError = null;
|
|
6543
|
+
const ownerClientId = runtime.threadOwnerClientIds.get(conversationId) ?? null;
|
|
5545
6544
|
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
6545
|
+
for (const candidate of turnCandidates) {
|
|
6546
|
+
try {
|
|
6547
|
+
console.log(
|
|
6548
|
+
`[completion-reply] try candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")} owner=${cleanText(ownerClientId || "") || "none"} images=${normalizedLocalImagePaths.length} workspaceImages=${stagedWorkspaceImagePaths.length} cwd=${cleanText(resolvedCwd || "") || "none"}`
|
|
6549
|
+
);
|
|
6550
|
+
if (candidate.transport === "direct-turn-start" && ownerClientId) {
|
|
6551
|
+
await runtime.ipcClient.startTurnDirect(
|
|
6552
|
+
conversationId,
|
|
6553
|
+
candidate.turnStartParams,
|
|
6554
|
+
ownerClientId
|
|
6555
|
+
);
|
|
6556
|
+
} else {
|
|
6557
|
+
await runtime.ipcClient.startTurn(
|
|
6558
|
+
conversationId,
|
|
6559
|
+
candidate.turnStartParams,
|
|
6560
|
+
ownerClientId
|
|
6561
|
+
);
|
|
6562
|
+
}
|
|
6563
|
+
console.log(
|
|
6564
|
+
`[completion-reply] success candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")}`
|
|
6565
|
+
);
|
|
6566
|
+
if (timelineImageAliases.length > 0) {
|
|
6567
|
+
const aliases = isPlainObject(state.timelineImagePathAliases)
|
|
6568
|
+
? state.timelineImagePathAliases
|
|
6569
|
+
: (state.timelineImagePathAliases = {});
|
|
6570
|
+
for (const [sourcePath, persistentPath] of timelineImageAliases) {
|
|
6571
|
+
aliases[sourcePath] = persistentPath;
|
|
6572
|
+
}
|
|
6573
|
+
await saveState(config.stateFile, state);
|
|
6574
|
+
}
|
|
6575
|
+
scheduleBestEffortFileCleanup(stagedWorkspaceImagePaths);
|
|
6576
|
+
return;
|
|
6577
|
+
} catch (error) {
|
|
6578
|
+
lastError = error;
|
|
6579
|
+
console.log(
|
|
6580
|
+
`[completion-reply] failed candidate=${candidate.name} transport=${cleanText(candidate.transport || "thread-follower")} error=${normalizeIpcErrorMessage(error)} raw=${inspect(error?.ipcError ?? error, { depth: 6, breakLength: 160 })}`
|
|
6581
|
+
);
|
|
6582
|
+
}
|
|
6583
|
+
}
|
|
6584
|
+
|
|
6585
|
+
await Promise.all(
|
|
6586
|
+
stagedWorkspaceImagePaths.map(async (filePath) => {
|
|
6587
|
+
try {
|
|
6588
|
+
await fs.rm(filePath, { force: true });
|
|
6589
|
+
} catch {
|
|
6590
|
+
// Ignore best-effort cleanup errors.
|
|
6591
|
+
}
|
|
6592
|
+
})
|
|
5550
6593
|
);
|
|
6594
|
+
throw lastError || new Error("completion-reply-image-send-failed");
|
|
5551
6595
|
}
|
|
5552
6596
|
|
|
5553
6597
|
async function handlePlanDecision({ config, runtime, state, planRequest, decision }) {
|
|
@@ -5565,7 +6609,7 @@ async function handlePlanDecision({ config, runtime, state, planRequest, decisio
|
|
|
5565
6609
|
planRequest.threadState
|
|
5566
6610
|
);
|
|
5567
6611
|
const turnStartParams = {
|
|
5568
|
-
input:
|
|
6612
|
+
input: buildTurnInput(buildImplementPlanPrompt(planRequest.rawPlanContent)),
|
|
5569
6613
|
attachments: [],
|
|
5570
6614
|
cwd: null,
|
|
5571
6615
|
approvalPolicy: null,
|
|
@@ -5648,7 +6692,7 @@ async function handleNativeApprovalDecision({ config, runtime, state, approval,
|
|
|
5648
6692
|
function buildApiItemDetail({ config, runtime, state, kind, token, locale }) {
|
|
5649
6693
|
if (timelineMessageKinds.has(kind)) {
|
|
5650
6694
|
const entry = timelineEntryByToken(runtime, token, kind);
|
|
5651
|
-
return entry ? buildTimelineMessageDetail(entry, locale) : null;
|
|
6695
|
+
return entry ? buildTimelineMessageDetail(entry, locale, runtime) : null;
|
|
5652
6696
|
}
|
|
5653
6697
|
if (kind === "approval") {
|
|
5654
6698
|
const approval = runtime.nativeApprovalsByToken.get(token);
|
|
@@ -5679,6 +6723,16 @@ function buildApiItemDetail({ config, runtime, state, kind, token, locale }) {
|
|
|
5679
6723
|
return historyItem ? buildHistoryDetail(historyItem, locale, runtime) : null;
|
|
5680
6724
|
}
|
|
5681
6725
|
|
|
6726
|
+
function resolveTimelineEntryImagePath(runtime, token, index) {
|
|
6727
|
+
const entry = timelineEntryByToken(runtime, token);
|
|
6728
|
+
if (!entry) {
|
|
6729
|
+
return "";
|
|
6730
|
+
}
|
|
6731
|
+
const imagePaths = normalizeTimelineImagePaths(entry.imagePaths ?? []);
|
|
6732
|
+
const resolvedIndex = Math.max(0, Number(index) || 0);
|
|
6733
|
+
return cleanText(imagePaths[resolvedIndex] || "");
|
|
6734
|
+
}
|
|
6735
|
+
|
|
5682
6736
|
function resolveWebAsset(urlPath) {
|
|
5683
6737
|
let relativePath = cleanText(urlPath || "");
|
|
5684
6738
|
if (!relativePath || relativePath === "/") {
|
|
@@ -5711,6 +6765,17 @@ function contentTypeForFile(filePath) {
|
|
|
5711
6765
|
return "image/svg+xml";
|
|
5712
6766
|
case ".png":
|
|
5713
6767
|
return "image/png";
|
|
6768
|
+
case ".jpg":
|
|
6769
|
+
case ".jpeg":
|
|
6770
|
+
return "image/jpeg";
|
|
6771
|
+
case ".webp":
|
|
6772
|
+
return "image/webp";
|
|
6773
|
+
case ".gif":
|
|
6774
|
+
return "image/gif";
|
|
6775
|
+
case ".heic":
|
|
6776
|
+
return "image/heic";
|
|
6777
|
+
case ".heif":
|
|
6778
|
+
return "image/heif";
|
|
5714
6779
|
default:
|
|
5715
6780
|
return "application/octet-stream";
|
|
5716
6781
|
}
|
|
@@ -6096,7 +7161,10 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
6096
7161
|
await saveState(config.stateFile, state);
|
|
6097
7162
|
return writeJson(res, 410, { error: "push-subscription-expired" });
|
|
6098
7163
|
}
|
|
6099
|
-
return writeJson(res, 500, {
|
|
7164
|
+
return writeJson(res, 500, {
|
|
7165
|
+
error: error.message,
|
|
7166
|
+
ipcError: error.ipcError ?? null,
|
|
7167
|
+
});
|
|
6100
7168
|
}
|
|
6101
7169
|
}
|
|
6102
7170
|
|
|
@@ -6155,6 +7223,34 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
6155
7223
|
return writeJson(res, 200, buildTimelineResponse(runtime, state, config, locale));
|
|
6156
7224
|
}
|
|
6157
7225
|
|
|
7226
|
+
const apiTimelineImageMatch = url.pathname.match(/^\/api\/timeline\/([^/]+)\/images\/(\d+)$/u);
|
|
7227
|
+
if (apiTimelineImageMatch && req.method === "GET") {
|
|
7228
|
+
const session = requireApiSession(req, res, config, state);
|
|
7229
|
+
if (!session) {
|
|
7230
|
+
return;
|
|
7231
|
+
}
|
|
7232
|
+
const token = decodeURIComponent(apiTimelineImageMatch[1]);
|
|
7233
|
+
const index = Number(apiTimelineImageMatch[2]) || 0;
|
|
7234
|
+
const filePath = resolveTimelineEntryImagePath(runtime, token, index);
|
|
7235
|
+
if (!filePath) {
|
|
7236
|
+
res.statusCode = 404;
|
|
7237
|
+
res.end("not-found");
|
|
7238
|
+
return;
|
|
7239
|
+
}
|
|
7240
|
+
try {
|
|
7241
|
+
const body = await fs.readFile(filePath);
|
|
7242
|
+
res.statusCode = 200;
|
|
7243
|
+
res.setHeader("Content-Type", contentTypeForFile(filePath));
|
|
7244
|
+
res.setHeader("Cache-Control", "private, max-age=300");
|
|
7245
|
+
res.end(body);
|
|
7246
|
+
return;
|
|
7247
|
+
} catch {
|
|
7248
|
+
res.statusCode = 404;
|
|
7249
|
+
res.end("not-found");
|
|
7250
|
+
return;
|
|
7251
|
+
}
|
|
7252
|
+
}
|
|
7253
|
+
|
|
6158
7254
|
const apiItemMatch = url.pathname.match(/^\/api\/items\/([^/]+)\/([^/]+)$/u);
|
|
6159
7255
|
if (apiItemMatch && req.method === "GET") {
|
|
6160
7256
|
const session = requireApiSession(req, res, config, state);
|
|
@@ -6190,7 +7286,9 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
6190
7286
|
? await stageCompletionReplyImages(config, req)
|
|
6191
7287
|
: await parseJsonBody(req);
|
|
6192
7288
|
await handleCompletionReply({
|
|
7289
|
+
config,
|
|
6193
7290
|
runtime,
|
|
7291
|
+
state,
|
|
6194
7292
|
completionItem,
|
|
6195
7293
|
text: payload?.text ?? "",
|
|
6196
7294
|
planMode: payload?.planMode === true,
|
|
@@ -6210,8 +7308,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
|
|
|
6210
7308
|
error.message === "completion-reply-image-limit" ||
|
|
6211
7309
|
error.message === "completion-reply-image-invalid-type" ||
|
|
6212
7310
|
error.message === "completion-reply-image-too-large" ||
|
|
6213
|
-
error.message === "completion-reply-image-invalid-upload"
|
|
6214
|
-
error.message === "completion-reply-image-disabled"
|
|
7311
|
+
error.message === "completion-reply-image-invalid-upload"
|
|
6215
7312
|
) {
|
|
6216
7313
|
return writeJson(res, 400, { error: error.message });
|
|
6217
7314
|
}
|
|
@@ -6935,10 +8032,6 @@ async function stageCompletionReplyImages(config, req) {
|
|
|
6935
8032
|
.getAll("image")
|
|
6936
8033
|
.filter((value) => typeof File !== "undefined" && value instanceof File);
|
|
6937
8034
|
|
|
6938
|
-
if (files.length > 0) {
|
|
6939
|
-
throw new Error("completion-reply-image-disabled");
|
|
6940
|
-
}
|
|
6941
|
-
|
|
6942
8035
|
if (files.length > MAX_COMPLETION_REPLY_IMAGE_COUNT) {
|
|
6943
8036
|
throw new Error("completion-reply-image-limit");
|
|
6944
8037
|
}
|
|
@@ -7932,6 +9025,9 @@ function buildConfig(cli) {
|
|
|
7932
9025
|
codexLogsDbFile: resolvePath(process.env.CODEX_LOGS_DB_FILE || ""),
|
|
7933
9026
|
stateFile,
|
|
7934
9027
|
replyUploadsDir: resolvePath(process.env.REPLY_UPLOADS_DIR || path.join(path.dirname(stateFile), "uploads")),
|
|
9028
|
+
timelineAttachmentsDir: resolvePath(
|
|
9029
|
+
process.env.TIMELINE_ATTACHMENTS_DIR || path.join(path.dirname(stateFile), "timeline-attachments")
|
|
9030
|
+
),
|
|
7935
9031
|
pollIntervalMs: numberEnv("POLL_INTERVAL_MS", 2500),
|
|
7936
9032
|
replaySeconds: numberEnv("REPLAY_SECONDS", 300),
|
|
7937
9033
|
sessionIndexRefreshMs: numberEnv("SESSION_INDEX_REFRESH_MS", 30000),
|
|
@@ -8171,6 +9267,7 @@ async function loadState(stateFile) {
|
|
|
8171
9267
|
pendingUserInputRequests: parsed.pendingUserInputRequests ?? {},
|
|
8172
9268
|
recentHistoryItems: parsed.recentHistoryItems ?? [],
|
|
8173
9269
|
recentTimelineEntries: parsed.recentTimelineEntries ?? [],
|
|
9270
|
+
timelineImagePathAliases: parsed.timelineImagePathAliases ?? {},
|
|
8174
9271
|
sqliteCompletionCursorId: Number(parsed.sqliteCompletionCursorId) || 0,
|
|
8175
9272
|
sqliteCompletionSourceFile: cleanText(parsed.sqliteCompletionSourceFile ?? ""),
|
|
8176
9273
|
sqliteMessageCursorId: Number(parsed.sqliteMessageCursorId) || 0,
|
|
@@ -8195,6 +9292,7 @@ async function loadState(stateFile) {
|
|
|
8195
9292
|
pendingUserInputRequests: {},
|
|
8196
9293
|
recentHistoryItems: [],
|
|
8197
9294
|
recentTimelineEntries: [],
|
|
9295
|
+
timelineImagePathAliases: {},
|
|
8198
9296
|
sqliteCompletionCursorId: 0,
|
|
8199
9297
|
sqliteCompletionSourceFile: "",
|
|
8200
9298
|
sqliteMessageCursorId: 0,
|
|
@@ -8281,6 +9379,10 @@ function sanitizeResolvedThreadLabel(value, conversationId) {
|
|
|
8281
9379
|
return "";
|
|
8282
9380
|
}
|
|
8283
9381
|
|
|
9382
|
+
if (looksLikeGeneratedThreadTitle(normalized)) {
|
|
9383
|
+
return "";
|
|
9384
|
+
}
|
|
9385
|
+
|
|
8284
9386
|
return normalized;
|
|
8285
9387
|
}
|
|
8286
9388
|
|
|
@@ -8318,7 +9420,7 @@ function extractRolloutMessageText(content) {
|
|
|
8318
9420
|
content
|
|
8319
9421
|
.map((entry) =>
|
|
8320
9422
|
isPlainObject(entry) && (entry.type === "input_text" || entry.type === "output_text")
|
|
8321
|
-
?
|
|
9423
|
+
? normalizeTimelineMessageText(entry.text ?? "")
|
|
8322
9424
|
: ""
|
|
8323
9425
|
)
|
|
8324
9426
|
.filter(Boolean)
|
|
@@ -8326,6 +9428,24 @@ function extractRolloutMessageText(content) {
|
|
|
8326
9428
|
);
|
|
8327
9429
|
}
|
|
8328
9430
|
|
|
9431
|
+
function rolloutContentHasImages(content) {
|
|
9432
|
+
if (!Array.isArray(content)) {
|
|
9433
|
+
return false;
|
|
9434
|
+
}
|
|
9435
|
+
return content.some((entry) => {
|
|
9436
|
+
if (!isPlainObject(entry)) {
|
|
9437
|
+
return false;
|
|
9438
|
+
}
|
|
9439
|
+
if (entry.type === "input_image" || entry.type === "image" || entry.type === "localImage") {
|
|
9440
|
+
return true;
|
|
9441
|
+
}
|
|
9442
|
+
if (entry.type !== "input_text" && entry.type !== "output_text") {
|
|
9443
|
+
return false;
|
|
9444
|
+
}
|
|
9445
|
+
return isInlineImagePlaceholderText(entry.text ?? "");
|
|
9446
|
+
});
|
|
9447
|
+
}
|
|
9448
|
+
|
|
8329
9449
|
function deriveRolloutThreadTitleCandidate(text) {
|
|
8330
9450
|
const normalized = cleanText(normalizeNotificationText(text));
|
|
8331
9451
|
if (!normalized) {
|
|
@@ -8706,8 +9826,50 @@ function normalizeLongText(value) {
|
|
|
8706
9826
|
.trim();
|
|
8707
9827
|
}
|
|
8708
9828
|
|
|
8709
|
-
function
|
|
8710
|
-
|
|
9829
|
+
function replaceTurnAbortedMarkup(value, locale = DEFAULT_LOCALE) {
|
|
9830
|
+
const raw = String(value || "");
|
|
9831
|
+
if (!/<turn_aborted>/iu.test(raw)) {
|
|
9832
|
+
return raw;
|
|
9833
|
+
}
|
|
9834
|
+
return raw.replace(/<turn_aborted>[\s\S]*?<\/turn_aborted>/giu, t(locale, "server.message.turnAborted"));
|
|
9835
|
+
}
|
|
9836
|
+
|
|
9837
|
+
function isTurnAbortedDisplayMessage(value) {
|
|
9838
|
+
const normalized = normalizeLongText(String(value || ""));
|
|
9839
|
+
const englishMessage = normalizeLongText(t(DEFAULT_LOCALE, "server.message.turnAborted"));
|
|
9840
|
+
const japaneseMessage = normalizeLongText(t("ja", "server.message.turnAborted"));
|
|
9841
|
+
return /<turn_aborted>/iu.test(String(value || "")) || normalized === englishMessage || normalized === japaneseMessage;
|
|
9842
|
+
}
|
|
9843
|
+
|
|
9844
|
+
function interruptedDetailNotice(value, locale = DEFAULT_LOCALE) {
|
|
9845
|
+
return isTurnAbortedDisplayMessage(value) ? t(locale, "detail.turnAbortedNotice") : "";
|
|
9846
|
+
}
|
|
9847
|
+
|
|
9848
|
+
function stripInlineImagePlaceholderMarkup(value) {
|
|
9849
|
+
return String(value || "")
|
|
9850
|
+
.replace(/<image\b[^>]*>/giu, "")
|
|
9851
|
+
.replace(/<\/image>/giu, "");
|
|
9852
|
+
}
|
|
9853
|
+
|
|
9854
|
+
function isInlineImagePlaceholderText(value) {
|
|
9855
|
+
return cleanText(stripInlineImagePlaceholderMarkup(value)) === "" && /<\/?image\b/iu.test(String(value || ""));
|
|
9856
|
+
}
|
|
9857
|
+
|
|
9858
|
+
function normalizeTimelineMessageText(value, locale = DEFAULT_LOCALE) {
|
|
9859
|
+
return normalizeLongText(replaceTurnAbortedMarkup(stripInlineImagePlaceholderMarkup(value), locale));
|
|
9860
|
+
}
|
|
9861
|
+
|
|
9862
|
+
function normalizeTimelineImagePaths(value) {
|
|
9863
|
+
if (!Array.isArray(value)) {
|
|
9864
|
+
return [];
|
|
9865
|
+
}
|
|
9866
|
+
return value
|
|
9867
|
+
.map((entry) => cleanText(entry || ""))
|
|
9868
|
+
.filter(Boolean);
|
|
9869
|
+
}
|
|
9870
|
+
|
|
9871
|
+
function normalizeNotificationText(value, locale = DEFAULT_LOCALE) {
|
|
9872
|
+
return normalizeLongText(replaceTurnAbortedMarkup(stripNotificationMarkup(value), locale))
|
|
8711
9873
|
.replace(/\n{2,}/gu, "\n")
|
|
8712
9874
|
.trim();
|
|
8713
9875
|
}
|
|
@@ -8783,6 +9945,8 @@ function stripNotificationMarkup(value) {
|
|
|
8783
9945
|
return String(value || "")
|
|
8784
9946
|
.replace(/^\s*<\/?proposed_plan>\s*$/gimu, "")
|
|
8785
9947
|
.replace(/<\/?proposed_plan>/giu, "")
|
|
9948
|
+
.replace(/<image\b[^>]*>/giu, "")
|
|
9949
|
+
.replace(/<\/image>/giu, "")
|
|
8786
9950
|
.replace(/!\[([^\]]*)\]\(([^)]+)\)/gu, "$1")
|
|
8787
9951
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/gu, "$1")
|
|
8788
9952
|
.replace(/^\s{0,3}#{1,6}\s+/gmu, "")
|
|
@@ -8982,6 +10146,7 @@ async function main() {
|
|
|
8982
10146
|
if (
|
|
8983
10147
|
migratedPairedDevicesStateChanged ||
|
|
8984
10148
|
restoredPendingPlanStateChanged ||
|
|
10149
|
+
restoredTimelineImagePathsStateChanged ||
|
|
8985
10150
|
restoredPendingUserInputStateChanged ||
|
|
8986
10151
|
refreshResolvedThreadLabels({ config, runtime, state })
|
|
8987
10152
|
) {
|