@evident-ai/cli 3.5.2-dev.8d3aa06 → 3.5.2-dev.afae02b
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/index.js +1114 -313
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { createRequire as createRequire2 } from "module";
|
|
5
4
|
import { Command } from "commander";
|
|
6
5
|
|
|
7
6
|
// src/commands/login.ts
|
|
@@ -1063,13 +1062,83 @@ async function claudeUsage() {
|
|
|
1063
1062
|
|
|
1064
1063
|
// src/commands/run.ts
|
|
1065
1064
|
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1066
|
-
import { homedir as
|
|
1065
|
+
import { homedir as homedir7 } from "node:os";
|
|
1067
1066
|
import { isAbsolute as isAbsolute3, join as join11, parse, resolve as resolvePath2 } from "node:path";
|
|
1068
1067
|
import chalk7 from "chalk";
|
|
1069
1068
|
|
|
1070
1069
|
// ../../packages/types/src/agents/index.ts
|
|
1071
1070
|
var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
|
|
1072
1071
|
|
|
1072
|
+
// ../../packages/types/src/opencode/index.ts
|
|
1073
|
+
function roleOf(m) {
|
|
1074
|
+
if (!m || typeof m !== "object") return void 0;
|
|
1075
|
+
if (typeof m.role === "string") return m.role;
|
|
1076
|
+
const infoRole = m.info?.role;
|
|
1077
|
+
return typeof infoRole === "string" ? infoRole : void 0;
|
|
1078
|
+
}
|
|
1079
|
+
function idOf(m) {
|
|
1080
|
+
if (!m || typeof m !== "object") return void 0;
|
|
1081
|
+
if (typeof m.id === "string") return m.id;
|
|
1082
|
+
const infoId = m.info?.id;
|
|
1083
|
+
return typeof infoId === "string" ? infoId : void 0;
|
|
1084
|
+
}
|
|
1085
|
+
function parentIdOf(m) {
|
|
1086
|
+
if (!m || typeof m !== "object") return void 0;
|
|
1087
|
+
if (typeof m.parentID === "string") return m.parentID;
|
|
1088
|
+
const infoParent = m.info?.parentID;
|
|
1089
|
+
return typeof infoParent === "string" ? infoParent : void 0;
|
|
1090
|
+
}
|
|
1091
|
+
function textPartsOf(m) {
|
|
1092
|
+
if (!m || typeof m !== "object") return [];
|
|
1093
|
+
const parts = Array.isArray(m.parts) ? m.parts : [];
|
|
1094
|
+
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text ?? "").filter(Boolean);
|
|
1095
|
+
}
|
|
1096
|
+
function correlatedAssistantRun(messages, userMessageId) {
|
|
1097
|
+
if (!messages || messages.length === 0) return [];
|
|
1098
|
+
const byParent = messages.filter(
|
|
1099
|
+
(m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
|
|
1100
|
+
);
|
|
1101
|
+
if (byParent.length > 0) return byParent;
|
|
1102
|
+
const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
|
|
1103
|
+
if (userIndex === -1) return [];
|
|
1104
|
+
const run2 = [];
|
|
1105
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
1106
|
+
const role = roleOf(messages[i]);
|
|
1107
|
+
if (role === "user") break;
|
|
1108
|
+
if (role === "assistant") run2.push(messages[i]);
|
|
1109
|
+
}
|
|
1110
|
+
return run2;
|
|
1111
|
+
}
|
|
1112
|
+
function lastText(assistantRun) {
|
|
1113
|
+
for (let i = assistantRun.length - 1; i >= 0; i--) {
|
|
1114
|
+
const textParts = textPartsOf(assistantRun[i]);
|
|
1115
|
+
if (textParts.length > 0) return textParts.join("\n").trim() || null;
|
|
1116
|
+
}
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
function effectiveCorrelatedRun(messages, userMessageId) {
|
|
1120
|
+
const run2 = correlatedAssistantRun(messages, userMessageId);
|
|
1121
|
+
if (lastText(run2) !== null) return run2;
|
|
1122
|
+
const userIndex = messages?.findIndex((m) => idOf(m) === userMessageId) ?? -1;
|
|
1123
|
+
if (userIndex === -1) return run2;
|
|
1124
|
+
let continuation;
|
|
1125
|
+
for (let i = userIndex + 1; messages && i < messages.length; i++) {
|
|
1126
|
+
if (roleOf(messages[i]) === "user") {
|
|
1127
|
+
continuation = messages[i];
|
|
1128
|
+
break;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
const continuationId = idOf(continuation);
|
|
1132
|
+
if (roleOf(continuation) === "user" && textPartsOf(continuation).join("\n").trim().toLowerCase() === "continue" && continuationId) {
|
|
1133
|
+
const continuationRun = correlatedAssistantRun(messages, continuationId);
|
|
1134
|
+
if (continuationRun.length > 0) return continuationRun;
|
|
1135
|
+
}
|
|
1136
|
+
return run2;
|
|
1137
|
+
}
|
|
1138
|
+
function extractReplyTextForMessage(messages, opencodeUserMessageId) {
|
|
1139
|
+
return lastText(effectiveCorrelatedRun(messages, opencodeUserMessageId));
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1073
1142
|
// ../../packages/types/src/telemetry/index.ts
|
|
1074
1143
|
var TelemetryEventTypes = {
|
|
1075
1144
|
// Agent activity events (shown in web UI activity log)
|
|
@@ -1158,7 +1227,7 @@ import ora4 from "ora";
|
|
|
1158
1227
|
import { select as select4 } from "@inquirer/prompts";
|
|
1159
1228
|
|
|
1160
1229
|
// src/lib/telemetry.ts
|
|
1161
|
-
var CLI_VERSION = (true ? "3.5.2-dev.
|
|
1230
|
+
var CLI_VERSION = (true ? "3.5.2-dev.afae02b" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1162
1231
|
function getCliVersion() {
|
|
1163
1232
|
return CLI_VERSION;
|
|
1164
1233
|
}
|
|
@@ -1172,6 +1241,23 @@ var authProvider = null;
|
|
|
1172
1241
|
function setTelemetryAuthProvider(provider) {
|
|
1173
1242
|
authProvider = provider;
|
|
1174
1243
|
}
|
|
1244
|
+
var diagnosticsReporter = null;
|
|
1245
|
+
function setTelemetryDiagnosticsReporter(reporter) {
|
|
1246
|
+
diagnosticsReporter = reporter;
|
|
1247
|
+
}
|
|
1248
|
+
function reportTelemetryDiagnostic(message) {
|
|
1249
|
+
if (!diagnosticsReporter) {
|
|
1250
|
+
console.error(message);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
try {
|
|
1254
|
+
diagnosticsReporter(message);
|
|
1255
|
+
} catch (error2) {
|
|
1256
|
+
console.error(
|
|
1257
|
+
`Telemetry diagnostics reporter failed: ${error2 instanceof Error ? error2.message : String(error2)}; original diagnostic: ${message}`
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1175
1261
|
var FLUSH_FAILURE_LOG_INTERVAL_MS = 6e4;
|
|
1176
1262
|
var lastFlushFailureLoggedAt = 0;
|
|
1177
1263
|
var suppressedFlushFailureCount = 0;
|
|
@@ -1237,7 +1323,9 @@ async function flushEvents() {
|
|
|
1237
1323
|
signal: controller.signal
|
|
1238
1324
|
});
|
|
1239
1325
|
if (!response.ok) {
|
|
1240
|
-
|
|
1326
|
+
reportTelemetryDiagnostic(
|
|
1327
|
+
`Telemetry could not send events (HTTP ${response.status}); runner and OpenCode are unaffected`
|
|
1328
|
+
);
|
|
1241
1329
|
}
|
|
1242
1330
|
} finally {
|
|
1243
1331
|
clearTimeout(timeout);
|
|
@@ -1247,7 +1335,9 @@ async function flushEvents() {
|
|
|
1247
1335
|
if (now - lastFlushFailureLoggedAt >= FLUSH_FAILURE_LOG_INTERVAL_MS) {
|
|
1248
1336
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1249
1337
|
const suffix = suppressedFlushFailureCount > 0 ? ` (${suppressedFlushFailureCount} more suppressed in the last ${FLUSH_FAILURE_LOG_INTERVAL_MS / 1e3}s)` : "";
|
|
1250
|
-
|
|
1338
|
+
reportTelemetryDiagnostic(
|
|
1339
|
+
`Telemetry flush failed: ${message}; runner and OpenCode are unaffected${suffix}`
|
|
1340
|
+
);
|
|
1251
1341
|
lastFlushFailureLoggedAt = now;
|
|
1252
1342
|
suppressedFlushFailureCount = 0;
|
|
1253
1343
|
} else {
|
|
@@ -1299,8 +1389,227 @@ var EventTypes = {
|
|
|
1299
1389
|
};
|
|
1300
1390
|
|
|
1301
1391
|
// src/lib/runner-activity-telemetry.ts
|
|
1302
|
-
|
|
1392
|
+
import { homedir as homedir2 } from "node:os";
|
|
1393
|
+
|
|
1394
|
+
// src/lib/opencode/session-db-reclaim.ts
|
|
1395
|
+
import { statSync as statSync2, statfsSync } from "node:fs";
|
|
1396
|
+
import { dirname as dirname2 } from "node:path";
|
|
1397
|
+
var SESSION_DB_RECLAIM_CHUNK_PAGES = 2e3;
|
|
1398
|
+
var SESSION_DB_RECLAIM_TIME_BUDGET_MS = 500;
|
|
1399
|
+
var SESSION_DB_RECLAIM_PAGE_CEILING = 4e4;
|
|
1400
|
+
function errorMessage(error2) {
|
|
1401
|
+
if (!(error2 instanceof Error)) return String(error2);
|
|
1402
|
+
return error2.cause instanceof Error ? error2.cause.message : error2.message;
|
|
1403
|
+
}
|
|
1404
|
+
function isSessionStoreWriteFailure(error2) {
|
|
1405
|
+
const messages = [];
|
|
1406
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1407
|
+
let current = error2;
|
|
1408
|
+
while (current !== null && current !== void 0 && !visited.has(current)) {
|
|
1409
|
+
visited.add(current);
|
|
1410
|
+
if (typeof current === "string") {
|
|
1411
|
+
messages.push(current);
|
|
1412
|
+
break;
|
|
1413
|
+
}
|
|
1414
|
+
if (current instanceof Error) {
|
|
1415
|
+
messages.push(current.name, current.message);
|
|
1416
|
+
current = current.cause;
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
if (typeof current === "object") {
|
|
1420
|
+
const record = current;
|
|
1421
|
+
if (typeof record.code === "string") messages.push(record.code);
|
|
1422
|
+
if (typeof record.message === "string") messages.push(record.message);
|
|
1423
|
+
current = record.cause;
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
break;
|
|
1427
|
+
}
|
|
1428
|
+
return messages.some(
|
|
1429
|
+
(message) => /(?:database or disk is full|disk is full|no space left on device|SQLITE_(?:FULL|IOERR)(?:\b|_))/i.test(
|
|
1430
|
+
message
|
|
1431
|
+
)
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
1435
|
+
try {
|
|
1436
|
+
const fsStats = statfsSync(dirname2(dbPath));
|
|
1437
|
+
const availableBytes = fsStats.bavail * fsStats.bsize;
|
|
1438
|
+
if (availableBytes < requiredBytes) {
|
|
1439
|
+
return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
|
|
1440
|
+
}
|
|
1441
|
+
return null;
|
|
1442
|
+
} catch (err) {
|
|
1443
|
+
return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
function readLogicalBytes(db) {
|
|
1447
|
+
const pageCount = db.prepare("PRAGMA page_count").get().page_count;
|
|
1448
|
+
const pageSize = db.prepare("PRAGMA page_size").get().page_size;
|
|
1449
|
+
return pageCount * pageSize;
|
|
1450
|
+
}
|
|
1451
|
+
function readFreelistCount(db) {
|
|
1452
|
+
return db.prepare("PRAGMA freelist_count").get().freelist_count;
|
|
1453
|
+
}
|
|
1454
|
+
function readCheckpointResult(db) {
|
|
1455
|
+
const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
1456
|
+
return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
|
|
1457
|
+
}
|
|
1458
|
+
async function probeReclaimAvailability(input) {
|
|
1459
|
+
const { dbPath, requiredBytes } = input;
|
|
1460
|
+
let sqlite;
|
|
1461
|
+
try {
|
|
1462
|
+
sqlite = await import("node:sqlite");
|
|
1463
|
+
} catch (err) {
|
|
1464
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
1465
|
+
console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
|
|
1466
|
+
return { reason: "sqlite-unavailable", detail };
|
|
1467
|
+
}
|
|
1468
|
+
let autoVacuum = null;
|
|
1469
|
+
try {
|
|
1470
|
+
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
1471
|
+
try {
|
|
1472
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
1473
|
+
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
1474
|
+
} finally {
|
|
1475
|
+
db.close();
|
|
1476
|
+
}
|
|
1477
|
+
} catch (err) {
|
|
1478
|
+
console.warn(
|
|
1479
|
+
`[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
if (autoVacuum !== 0) return null;
|
|
1483
|
+
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
1484
|
+
}
|
|
1485
|
+
async function reclaimSessionDbSpace(input) {
|
|
1486
|
+
const {
|
|
1487
|
+
dbPath,
|
|
1488
|
+
pageCeiling = SESSION_DB_RECLAIM_PAGE_CEILING,
|
|
1489
|
+
timeBudgetMs = SESSION_DB_RECLAIM_TIME_BUDGET_MS,
|
|
1490
|
+
chunkPages = SESSION_DB_RECLAIM_CHUNK_PAGES,
|
|
1491
|
+
now = () => performance.now(),
|
|
1492
|
+
allowFullVacuum = true
|
|
1493
|
+
} = input;
|
|
1494
|
+
let sqlite;
|
|
1495
|
+
try {
|
|
1496
|
+
sqlite = await import("node:sqlite");
|
|
1497
|
+
} catch (err) {
|
|
1498
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
1499
|
+
console.warn(
|
|
1500
|
+
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
|
|
1501
|
+
);
|
|
1502
|
+
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
1503
|
+
}
|
|
1504
|
+
const { DatabaseSync } = sqlite;
|
|
1505
|
+
let db;
|
|
1506
|
+
try {
|
|
1507
|
+
db = new DatabaseSync(dbPath);
|
|
1508
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
1509
|
+
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
1510
|
+
if (autoVacuum === 0) {
|
|
1511
|
+
if (!allowFullVacuum) {
|
|
1512
|
+
console.warn(
|
|
1513
|
+
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
|
|
1514
|
+
);
|
|
1515
|
+
return {
|
|
1516
|
+
ok: false,
|
|
1517
|
+
skipped: "full-vacuum-blocked",
|
|
1518
|
+
detail: "a session turn is live"
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
const fileBytesForGuard = statSync2(dbPath).size;
|
|
1522
|
+
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
1523
|
+
if (skipReason !== null) {
|
|
1524
|
+
console.warn(
|
|
1525
|
+
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
|
|
1526
|
+
);
|
|
1527
|
+
return { ok: false, skipped: "insufficient-disk-space", detail: skipReason };
|
|
1528
|
+
}
|
|
1529
|
+
const beforeBytes = readLogicalBytes(db);
|
|
1530
|
+
const freelistBefore = readFreelistCount(db);
|
|
1531
|
+
const startedAt = now();
|
|
1532
|
+
db.exec("PRAGMA auto_vacuum=INCREMENTAL");
|
|
1533
|
+
db.exec("VACUUM");
|
|
1534
|
+
const afterBytes = readLogicalBytes(db);
|
|
1535
|
+
const freelistAfter = readFreelistCount(db);
|
|
1536
|
+
const checkpoint = readCheckpointResult(db);
|
|
1537
|
+
return {
|
|
1538
|
+
ok: true,
|
|
1539
|
+
mode: "convert",
|
|
1540
|
+
beforeBytes,
|
|
1541
|
+
afterBytes,
|
|
1542
|
+
freelistBefore,
|
|
1543
|
+
freelistAfter,
|
|
1544
|
+
pagesReclaimed: Math.max(0, freelistBefore - freelistAfter),
|
|
1545
|
+
stoppedBy: "freelist-drained",
|
|
1546
|
+
elapsedMs: Math.max(0, now() - startedAt),
|
|
1547
|
+
checkpoint
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
if (autoVacuum === 2) {
|
|
1551
|
+
const beforeBytes = readLogicalBytes(db);
|
|
1552
|
+
const freelistBefore = readFreelistCount(db);
|
|
1553
|
+
const startedAt = now();
|
|
1554
|
+
const pageLimit = Math.max(0, Math.trunc(pageCeiling));
|
|
1555
|
+
const chunkLimit = Math.max(1, Math.trunc(chunkPages));
|
|
1556
|
+
const budget = Math.max(0, timeBudgetMs);
|
|
1557
|
+
let pagesAttempted = 0;
|
|
1558
|
+
let freelistAfter = freelistBefore;
|
|
1559
|
+
let stoppedBy = "freelist-drained";
|
|
1560
|
+
while (freelistAfter > 0) {
|
|
1561
|
+
if (pagesAttempted >= pageLimit) {
|
|
1562
|
+
stoppedBy = "page-ceiling";
|
|
1563
|
+
break;
|
|
1564
|
+
}
|
|
1565
|
+
if (now() - startedAt >= budget) {
|
|
1566
|
+
stoppedBy = "time-budget";
|
|
1567
|
+
break;
|
|
1568
|
+
}
|
|
1569
|
+
const pages = Math.min(chunkLimit, pageLimit - pagesAttempted, freelistAfter);
|
|
1570
|
+
db.exec(`PRAGMA incremental_vacuum(${pages})`);
|
|
1571
|
+
pagesAttempted += pages;
|
|
1572
|
+
freelistAfter = readFreelistCount(db);
|
|
1573
|
+
}
|
|
1574
|
+
const afterBytes = readLogicalBytes(db);
|
|
1575
|
+
const checkpoint = readCheckpointResult(db);
|
|
1576
|
+
return {
|
|
1577
|
+
ok: true,
|
|
1578
|
+
mode: "incremental",
|
|
1579
|
+
beforeBytes,
|
|
1580
|
+
afterBytes,
|
|
1581
|
+
freelistBefore,
|
|
1582
|
+
freelistAfter,
|
|
1583
|
+
pagesReclaimed: Math.max(0, freelistBefore - freelistAfter),
|
|
1584
|
+
stoppedBy,
|
|
1585
|
+
elapsedMs: Math.max(0, now() - startedAt),
|
|
1586
|
+
checkpoint
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
console.warn(
|
|
1590
|
+
`[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
|
|
1591
|
+
);
|
|
1592
|
+
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
1593
|
+
} catch (err) {
|
|
1594
|
+
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
1595
|
+
return {
|
|
1596
|
+
ok: false,
|
|
1597
|
+
skipped: isSessionStoreWriteFailure(err) ? "disk-exhaustion" : "reclaim-error",
|
|
1598
|
+
detail: errorMessage(err)
|
|
1599
|
+
};
|
|
1600
|
+
} finally {
|
|
1601
|
+
db?.close();
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/lib/runner-activity-telemetry.ts
|
|
1606
|
+
var FORWARDED_LEVELS_BY_SOURCE = {
|
|
1607
|
+
"cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
|
|
1608
|
+
opencode: /* @__PURE__ */ new Set(["warn", "error"]),
|
|
1609
|
+
"cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"])
|
|
1610
|
+
};
|
|
1303
1611
|
var SEVERITY_BY_LEVEL = {
|
|
1612
|
+
info: "info",
|
|
1304
1613
|
warn: "warning",
|
|
1305
1614
|
error: "error"
|
|
1306
1615
|
};
|
|
@@ -1310,26 +1619,85 @@ function parseOpenCodeLogLine(line) {
|
|
|
1310
1619
|
if (!levelMatch) return null;
|
|
1311
1620
|
const level = levelMatch[1].toUpperCase();
|
|
1312
1621
|
if (level !== "WARN" && level !== "ERROR") return null;
|
|
1313
|
-
const sessionMatch = normalisedLine.match(/(?:^|\s)sessionID=(\S+)/);
|
|
1314
|
-
|
|
1622
|
+
const sessionMatch = normalisedLine.match(/(?:^|\s)(?:sessionID|session\.id)=(\S+)/);
|
|
1623
|
+
const messageIDMatch = normalisedLine.match(/(?:^|\s)messageID=(\S+)/);
|
|
1624
|
+
const messageMatch = normalisedLine.match(/(?:^|\s)message=(?:"((?:\\.|[^"])*)"|(\S+))/);
|
|
1625
|
+
const parsed = {
|
|
1626
|
+
level: level === "WARN" ? "warn" : "error",
|
|
1627
|
+
sessionID: sessionMatch?.[1],
|
|
1628
|
+
messageID: messageIDMatch?.[1]
|
|
1629
|
+
};
|
|
1630
|
+
const message = messageMatch?.[1] ?? messageMatch?.[2];
|
|
1631
|
+
if (message !== void 0) parsed.message = message;
|
|
1632
|
+
return parsed;
|
|
1315
1633
|
}
|
|
1316
1634
|
var MAX_LINE_BUFFER_BYTES = 16 * 1024;
|
|
1317
|
-
|
|
1635
|
+
var REPEAT_SUPPRESSION_WINDOW_MS = 6e4;
|
|
1636
|
+
function createOpenCodeActivityForwarder(getContext, onSessionStoreWriteFailure) {
|
|
1318
1637
|
let buffer = Buffer.alloc(0);
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1638
|
+
const repeatStates = /* @__PURE__ */ new Map();
|
|
1639
|
+
const forwardLine = (line, parsed, repeatsSuppressed = 0) => {
|
|
1640
|
+
const metadata = {
|
|
1641
|
+
...parsed.sessionID ? { sessionID: parsed.sessionID } : {},
|
|
1642
|
+
...parsed.messageID ? { messageID: parsed.messageID } : {},
|
|
1643
|
+
...repeatsSuppressed > 0 ? { repeats_suppressed: repeatsSuppressed } : {}
|
|
1644
|
+
};
|
|
1322
1645
|
forwardRunnerActivity(
|
|
1323
1646
|
{
|
|
1324
1647
|
level: parsed.level,
|
|
1325
1648
|
error: line,
|
|
1326
|
-
metadata:
|
|
1649
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
|
|
1327
1650
|
source: "opencode"
|
|
1328
1651
|
},
|
|
1329
1652
|
getContext()
|
|
1330
1653
|
);
|
|
1331
1654
|
};
|
|
1332
|
-
|
|
1655
|
+
const expireRepeatStates = (now, currentKey) => {
|
|
1656
|
+
for (const [key, state] of repeatStates) {
|
|
1657
|
+
if (key === currentKey || now - state.lastForwardedAt < REPEAT_SUPPRESSION_WINDOW_MS) {
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
if (state.repeatsSuppressed > 0) {
|
|
1661
|
+
forwardLine(state.line, state.parsed, state.repeatsSuppressed);
|
|
1662
|
+
}
|
|
1663
|
+
repeatStates.delete(key);
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
const flushLine = (line) => {
|
|
1667
|
+
if (onSessionStoreWriteFailure && isSessionStoreWriteFailure(line)) {
|
|
1668
|
+
try {
|
|
1669
|
+
onSessionStoreWriteFailure();
|
|
1670
|
+
} catch (err) {
|
|
1671
|
+
reportTelemetryDiagnostic(
|
|
1672
|
+
`Runner activity telemetry could not record session-store write failure: ${err instanceof Error ? err.message : String(err)}`
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
const parsed = parseOpenCodeLogLine(line);
|
|
1677
|
+
if (!parsed) return;
|
|
1678
|
+
const now = Date.now();
|
|
1679
|
+
const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}\0${parsed.sessionID ?? ""}\0${parsed.messageID ?? ""}`;
|
|
1680
|
+
expireRepeatStates(now, key);
|
|
1681
|
+
if (key === void 0) {
|
|
1682
|
+
forwardLine(line, parsed);
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
const previous = repeatStates.get(key);
|
|
1686
|
+
if (!previous || now - previous.lastForwardedAt >= REPEAT_SUPPRESSION_WINDOW_MS) {
|
|
1687
|
+
forwardLine(line, parsed, previous?.repeatsSuppressed ?? 0);
|
|
1688
|
+
repeatStates.set(key, {
|
|
1689
|
+
line,
|
|
1690
|
+
parsed,
|
|
1691
|
+
lastForwardedAt: now,
|
|
1692
|
+
repeatsSuppressed: 0
|
|
1693
|
+
});
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
previous.line = line;
|
|
1697
|
+
previous.parsed = parsed;
|
|
1698
|
+
previous.repeatsSuppressed++;
|
|
1699
|
+
};
|
|
1700
|
+
const forward = ((chunk) => {
|
|
1333
1701
|
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8")]);
|
|
1334
1702
|
let newlineIndex;
|
|
1335
1703
|
while ((newlineIndex = buffer.indexOf(10)) !== -1) {
|
|
@@ -1340,14 +1708,44 @@ function createOpenCodeActivityForwarder(getContext) {
|
|
|
1340
1708
|
flushLine(buffer.toString("utf-8"));
|
|
1341
1709
|
buffer = Buffer.alloc(0);
|
|
1342
1710
|
}
|
|
1711
|
+
});
|
|
1712
|
+
forward.flush = () => {
|
|
1713
|
+
for (const state of repeatStates.values()) {
|
|
1714
|
+
if (state.repeatsSuppressed > 0) {
|
|
1715
|
+
forwardLine(state.line, state.parsed, state.repeatsSuppressed);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
repeatStates.clear();
|
|
1343
1719
|
};
|
|
1720
|
+
return forward;
|
|
1344
1721
|
}
|
|
1345
1722
|
var MAX_MESSAGE_LENGTH = 500;
|
|
1346
1723
|
var MAX_METADATA_VALUE_LENGTH = 200;
|
|
1347
1724
|
var MAX_METADATA_ENTRIES = 20;
|
|
1348
1725
|
var TRUNCATION_MARKER = "\u2026";
|
|
1349
|
-
function
|
|
1350
|
-
|
|
1726
|
+
function redactHomeDirectory(message) {
|
|
1727
|
+
const home = homedir2();
|
|
1728
|
+
if (!home || home === "/") {
|
|
1729
|
+
return message;
|
|
1730
|
+
}
|
|
1731
|
+
const pathSegmentCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-";
|
|
1732
|
+
let result = "";
|
|
1733
|
+
let searchFrom = 0;
|
|
1734
|
+
let homeIndex;
|
|
1735
|
+
while ((homeIndex = message.indexOf(home, searchFrom)) !== -1) {
|
|
1736
|
+
const homeEnd = homeIndex + home.length;
|
|
1737
|
+
const previousCharacter = message[homeIndex - 1];
|
|
1738
|
+
const nextCharacter = message[homeEnd];
|
|
1739
|
+
const atPathBoundary = home === "/" || (!previousCharacter || !pathSegmentCharacters.includes(previousCharacter)) && (!nextCharacter || !pathSegmentCharacters.includes(nextCharacter));
|
|
1740
|
+
result += message.slice(searchFrom, homeIndex);
|
|
1741
|
+
result += atPathBoundary ? "~" : home;
|
|
1742
|
+
searchFrom = homeEnd;
|
|
1743
|
+
}
|
|
1744
|
+
return result + message.slice(searchFrom);
|
|
1745
|
+
}
|
|
1746
|
+
function redactDiagnosticText(message) {
|
|
1747
|
+
const redacted = message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/(?<![A-Za-z0-9_-])sk-ant-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g, "sk-ant-***").replace(/(?<![A-Za-z0-9_-])sk-proj-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g, "sk-proj-***").replace(/(?<![A-Za-z0-9_-])sk-[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/g, "sk-***").replace(/(?<![A-Za-z0-9_-])(gh[oprsu])_[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/g, "$1_***").replace(/(?<![A-Za-z0-9_-])github_pat_[A-Za-z0-9_]{20,}(?![A-Za-z0-9_-])/g, "github_pat_***").replace(/https?:\/\/\S+/g, "<url>");
|
|
1748
|
+
return redactHomeDirectory(redacted);
|
|
1351
1749
|
}
|
|
1352
1750
|
function truncate(message) {
|
|
1353
1751
|
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
@@ -1357,14 +1755,17 @@ function sanitiseMetadata(metadata) {
|
|
|
1357
1755
|
if (!metadata) return {};
|
|
1358
1756
|
const entries = Object.entries(metadata).slice(0, MAX_METADATA_ENTRIES);
|
|
1359
1757
|
if (Object.keys(metadata).length > entries.length) {
|
|
1360
|
-
|
|
1361
|
-
`
|
|
1758
|
+
reportTelemetryDiagnostic(
|
|
1759
|
+
`Runner activity telemetry dropped ${Object.keys(metadata).length - entries.length} metadata entries; those entries were not sent`
|
|
1362
1760
|
);
|
|
1363
1761
|
}
|
|
1364
1762
|
const sanitised = [];
|
|
1365
1763
|
for (const [key, value] of entries) {
|
|
1366
1764
|
if (typeof value === "string") {
|
|
1367
|
-
sanitised.push([
|
|
1765
|
+
sanitised.push([
|
|
1766
|
+
key,
|
|
1767
|
+
truncate(redactDiagnosticText(value).slice(0, MAX_METADATA_VALUE_LENGTH))
|
|
1768
|
+
]);
|
|
1368
1769
|
} else if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
|
|
1369
1770
|
sanitised.push([key, value]);
|
|
1370
1771
|
}
|
|
@@ -1382,8 +1783,8 @@ function admitUnderRateLimit(source, now) {
|
|
|
1382
1783
|
}
|
|
1383
1784
|
if (now - window.windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
|
|
1384
1785
|
if (window.windowDroppedCount > 0) {
|
|
1385
|
-
|
|
1386
|
-
`
|
|
1786
|
+
reportTelemetryDiagnostic(
|
|
1787
|
+
`Runner activity telemetry rate cap reached: dropped ${window.windowDroppedCount} ${window.windowDroppedCount === 1 ? "entry" : "entries"} in the last ${RATE_LIMIT_WINDOW_MS / 1e3}s (limit ${RATE_LIMIT_MAX_EVENTS}/min); those entries were not sent for source "${source}"`
|
|
1387
1788
|
);
|
|
1388
1789
|
}
|
|
1389
1790
|
window.windowStartedAt = now;
|
|
@@ -1393,8 +1794,8 @@ function admitUnderRateLimit(source, now) {
|
|
|
1393
1794
|
if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
|
|
1394
1795
|
window.windowDroppedCount++;
|
|
1395
1796
|
if (window.windowDroppedCount === 1) {
|
|
1396
|
-
|
|
1397
|
-
`
|
|
1797
|
+
reportTelemetryDiagnostic(
|
|
1798
|
+
`Runner activity telemetry rate cap reached (${RATE_LIMIT_MAX_EVENTS} entries per ${RATE_LIMIT_WINDOW_MS / 1e3}s limit; further entries were not sent for source "${source}"`
|
|
1398
1799
|
);
|
|
1399
1800
|
}
|
|
1400
1801
|
return false;
|
|
@@ -1404,12 +1805,12 @@ function admitUnderRateLimit(source, now) {
|
|
|
1404
1805
|
}
|
|
1405
1806
|
function forwardRunnerActivity(entry, context) {
|
|
1406
1807
|
try {
|
|
1407
|
-
if (!FORWARDED_LEVELS.has(entry.level)) return;
|
|
1408
|
-
if (!context.agentId || !context.authHeader) return;
|
|
1409
1808
|
const source = entry.source ?? "cli.run";
|
|
1809
|
+
if (!FORWARDED_LEVELS_BY_SOURCE[source].has(entry.level)) return;
|
|
1810
|
+
if (!context.agentId || !context.authHeader) return;
|
|
1410
1811
|
if (!admitUnderRateLimit(source, Date.now())) return;
|
|
1411
1812
|
const rawMessage = entry.error ?? entry.message ?? "";
|
|
1412
|
-
const message = truncate(
|
|
1813
|
+
const message = truncate(redactDiagnosticText(rawMessage));
|
|
1413
1814
|
logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
|
|
1414
1815
|
severity: SEVERITY_BY_LEVEL[entry.level],
|
|
1415
1816
|
message,
|
|
@@ -1417,8 +1818,8 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1417
1818
|
agentId: context.agentId
|
|
1418
1819
|
});
|
|
1419
1820
|
} catch (err) {
|
|
1420
|
-
|
|
1421
|
-
`
|
|
1821
|
+
reportTelemetryDiagnostic(
|
|
1822
|
+
`Runner activity telemetry failed to forward runner activity: ${err instanceof Error ? err.message : String(err)}`
|
|
1422
1823
|
);
|
|
1423
1824
|
}
|
|
1424
1825
|
}
|
|
@@ -1708,8 +2109,46 @@ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
|
|
|
1708
2109
|
|
|
1709
2110
|
// src/lib/http-timeout.ts
|
|
1710
2111
|
var REQUEST_TIMEOUT_MS = 6e4;
|
|
2112
|
+
var RequestTimeoutError = class extends Error {
|
|
2113
|
+
constructor(operation, timeoutMs, elapsedMs) {
|
|
2114
|
+
super(`${operation} timed out after ${elapsedMs}ms (limit ${timeoutMs}ms)`);
|
|
2115
|
+
this.operation = operation;
|
|
2116
|
+
this.timeoutMs = timeoutMs;
|
|
2117
|
+
this.elapsedMs = elapsedMs;
|
|
2118
|
+
this.name = "TimeoutError";
|
|
2119
|
+
}
|
|
2120
|
+
};
|
|
2121
|
+
function describeRequestOperation(input, init) {
|
|
2122
|
+
const method = (init?.method ?? input.method ?? "GET").toUpperCase();
|
|
2123
|
+
const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
2124
|
+
let path;
|
|
2125
|
+
try {
|
|
2126
|
+
path = new URL(rawUrl).pathname;
|
|
2127
|
+
} catch (error2) {
|
|
2128
|
+
if (!(error2 instanceof TypeError)) throw error2;
|
|
2129
|
+
path = rawUrl.split(/[?#]/, 1)[0];
|
|
2130
|
+
}
|
|
2131
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2132
|
+
const prefixedIdentifier = /^[a-z]+_[A-Za-z0-9_-]{10,}$/;
|
|
2133
|
+
const maskedPath = path.split("/").map((segment) => uuid.test(segment) || prefixedIdentifier.test(segment) ? ":id" : segment).join("/");
|
|
2134
|
+
return `${method} ${maskedPath}`;
|
|
2135
|
+
}
|
|
1711
2136
|
function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
1712
|
-
return ((input, init) =>
|
|
2137
|
+
return (async (input, init) => {
|
|
2138
|
+
const startedAt = Date.now();
|
|
2139
|
+
try {
|
|
2140
|
+
return await fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
2141
|
+
} catch (error2) {
|
|
2142
|
+
if (error2?.name === "TimeoutError") {
|
|
2143
|
+
throw new RequestTimeoutError(
|
|
2144
|
+
describeRequestOperation(input, init),
|
|
2145
|
+
timeoutMs,
|
|
2146
|
+
Date.now() - startedAt
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
throw error2;
|
|
2150
|
+
}
|
|
2151
|
+
});
|
|
1713
2152
|
}
|
|
1714
2153
|
|
|
1715
2154
|
// src/lib/opencode/client.ts
|
|
@@ -1775,7 +2214,7 @@ async function getOpenCodeDirectory(port, client) {
|
|
|
1775
2214
|
return null;
|
|
1776
2215
|
}
|
|
1777
2216
|
}
|
|
1778
|
-
function
|
|
2217
|
+
function roleOf2(m) {
|
|
1779
2218
|
if (!m || typeof m !== "object") return void 0;
|
|
1780
2219
|
if (typeof m.role === "string") return m.role;
|
|
1781
2220
|
const infoRole = m.info?.role;
|
|
@@ -1789,13 +2228,13 @@ function createdOf(m) {
|
|
|
1789
2228
|
if (!m || typeof m !== "object") return void 0;
|
|
1790
2229
|
return m.info?.time?.created ?? m.time?.created;
|
|
1791
2230
|
}
|
|
1792
|
-
function
|
|
2231
|
+
function idOf2(m) {
|
|
1793
2232
|
if (!m || typeof m !== "object") return void 0;
|
|
1794
2233
|
if (typeof m.id === "string") return m.id;
|
|
1795
2234
|
const infoId = m.info?.id;
|
|
1796
2235
|
return typeof infoId === "string" ? infoId : void 0;
|
|
1797
2236
|
}
|
|
1798
|
-
function
|
|
2237
|
+
function parentIdOf2(m) {
|
|
1799
2238
|
if (!m || typeof m !== "object") return void 0;
|
|
1800
2239
|
if (typeof m.parentID === "string") return m.parentID;
|
|
1801
2240
|
const infoParent = m.info?.parentID;
|
|
@@ -1860,9 +2299,10 @@ async function pollSessionMessagesForRedrive(port, sessionId, client) {
|
|
|
1860
2299
|
function isSessionActivelyGenerating(messages) {
|
|
1861
2300
|
if (!messages || messages.length === 0) return false;
|
|
1862
2301
|
const last = messages[messages.length - 1];
|
|
1863
|
-
if (
|
|
2302
|
+
if (roleOf2(last) !== "assistant") return false;
|
|
1864
2303
|
return completedOf(last) == null;
|
|
1865
2304
|
}
|
|
2305
|
+
var FULL_SESSION_ENUMERATION_MAX_SESSIONS = 3200;
|
|
1866
2306
|
function sessionLastActivityMs(session) {
|
|
1867
2307
|
const candidates = [
|
|
1868
2308
|
session.time?.updated,
|
|
@@ -1877,30 +2317,60 @@ function sessionLastActivityMs(session) {
|
|
|
1877
2317
|
}
|
|
1878
2318
|
return null;
|
|
1879
2319
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
2320
|
+
function sessionListPath(options) {
|
|
2321
|
+
const params = new URLSearchParams();
|
|
2322
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
2323
|
+
const query = params.toString();
|
|
2324
|
+
return query ? `/session?${query}` : "/session";
|
|
2325
|
+
}
|
|
2326
|
+
async function listSessionPage(port, client, options) {
|
|
2327
|
+
const path = sessionListPath(options);
|
|
1882
2328
|
try {
|
|
1883
|
-
const res = await requestWithClient(port, client,
|
|
2329
|
+
const res = await requestWithClient(port, client, path);
|
|
1884
2330
|
if (!res.ok) return null;
|
|
1885
2331
|
const body = await res.json();
|
|
1886
2332
|
return Array.isArray(body) ? body : null;
|
|
1887
2333
|
} catch (error2) {
|
|
1888
2334
|
console.error(
|
|
1889
|
-
`[
|
|
2335
|
+
`[listSessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1890
2336
|
);
|
|
1891
2337
|
return null;
|
|
1892
2338
|
}
|
|
1893
2339
|
}
|
|
2340
|
+
async function listSessions(port, client, options) {
|
|
2341
|
+
if (client?.version === "v2") return listV2Sessions(client);
|
|
2342
|
+
return listSessionPage(port, client, options);
|
|
2343
|
+
}
|
|
2344
|
+
async function listAllSessions(port, client) {
|
|
2345
|
+
if (client?.version === "v2") {
|
|
2346
|
+
const sessions2 = await listV2Sessions(client);
|
|
2347
|
+
return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
|
|
2348
|
+
}
|
|
2349
|
+
const sessions = await listSessionPage(port, client, {
|
|
2350
|
+
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
|
|
2351
|
+
});
|
|
2352
|
+
if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
|
|
2353
|
+
if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
|
|
2354
|
+
return { sessions: null, reason: "session-enumeration-limit-bound" };
|
|
2355
|
+
}
|
|
2356
|
+
return { sessions };
|
|
2357
|
+
}
|
|
1894
2358
|
async function deleteSession(port, id, client) {
|
|
1895
2359
|
if (client?.version === "v2") return deleteV2Session(client, id);
|
|
1896
2360
|
try {
|
|
1897
|
-
const res = await requestWithClient(
|
|
1898
|
-
|
|
2361
|
+
const res = await requestWithClient(
|
|
2362
|
+
port,
|
|
2363
|
+
client,
|
|
2364
|
+
`/session/${id}`,
|
|
2365
|
+
{ method: "DELETE" },
|
|
2366
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
2367
|
+
);
|
|
2368
|
+
return { ok: res.status >= 200 && res.status < 300, status: res.status };
|
|
1899
2369
|
} catch (error2) {
|
|
1900
2370
|
console.error(
|
|
1901
2371
|
`[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1902
2372
|
);
|
|
1903
|
-
return false;
|
|
2373
|
+
return { ok: false, status: "threw" };
|
|
1904
2374
|
}
|
|
1905
2375
|
}
|
|
1906
2376
|
async function sessionExists(port, id, client) {
|
|
@@ -2141,7 +2611,7 @@ function messageText(m) {
|
|
|
2141
2611
|
async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
|
|
2142
2612
|
const before = await getSessionMessages(port, sessionId, client);
|
|
2143
2613
|
const knownUserIds = new Set(
|
|
2144
|
-
(before ?? []).filter((m) =>
|
|
2614
|
+
(before ?? []).filter((m) => roleOf2(m) === "user").map((m) => idOf2(m)).filter((id) => typeof id === "string")
|
|
2145
2615
|
);
|
|
2146
2616
|
const parts = [{ type: "text", text: content }];
|
|
2147
2617
|
let pendingOutcomes = null;
|
|
@@ -2184,8 +2654,8 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
|
|
|
2184
2654
|
if (after) {
|
|
2185
2655
|
let best = null;
|
|
2186
2656
|
for (const m of after) {
|
|
2187
|
-
if (
|
|
2188
|
-
const id =
|
|
2657
|
+
if (roleOf2(m) !== "user") continue;
|
|
2658
|
+
const id = idOf2(m);
|
|
2189
2659
|
if (typeof id !== "string" || knownUserIds.has(id)) continue;
|
|
2190
2660
|
if (messageText(m) !== content) continue;
|
|
2191
2661
|
const created = createdOf(m) ?? 0;
|
|
@@ -2207,13 +2677,13 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
|
|
|
2207
2677
|
function findAssistantReplyAfter(messages, userMessageId) {
|
|
2208
2678
|
if (!messages || messages.length === 0) return null;
|
|
2209
2679
|
const byParent = messages.find(
|
|
2210
|
-
(m) =>
|
|
2680
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
|
|
2211
2681
|
);
|
|
2212
2682
|
if (byParent) return byParent;
|
|
2213
|
-
const userIndex = messages.findIndex((m) =>
|
|
2683
|
+
const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
|
|
2214
2684
|
if (userIndex === -1) return null;
|
|
2215
2685
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2216
|
-
if (
|
|
2686
|
+
if (roleOf2(messages[i]) === "assistant") return messages[i];
|
|
2217
2687
|
}
|
|
2218
2688
|
return null;
|
|
2219
2689
|
}
|
|
@@ -2223,7 +2693,7 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
2223
2693
|
let lastNonErrored = null;
|
|
2224
2694
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2225
2695
|
const m = messages[i];
|
|
2226
|
-
if (
|
|
2696
|
+
if (roleOf2(m) !== "assistant" || parentIdOf2(m) !== userMessageId) continue;
|
|
2227
2697
|
if (lastCorrelated === null) lastCorrelated = m;
|
|
2228
2698
|
if (errorOf(m) == null) {
|
|
2229
2699
|
lastNonErrored = m;
|
|
@@ -2231,12 +2701,12 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
2231
2701
|
}
|
|
2232
2702
|
}
|
|
2233
2703
|
if (lastCorrelated) return lastNonErrored ?? lastCorrelated;
|
|
2234
|
-
const userIndex = messages.findIndex((m) =>
|
|
2704
|
+
const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
|
|
2235
2705
|
if (userIndex === -1) return null;
|
|
2236
2706
|
let last = null;
|
|
2237
2707
|
let lastOk = null;
|
|
2238
2708
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2239
|
-
const role =
|
|
2709
|
+
const role = roleOf2(messages[i]);
|
|
2240
2710
|
if (role === "user") break;
|
|
2241
2711
|
if (role === "assistant") {
|
|
2242
2712
|
last = messages[i];
|
|
@@ -2248,16 +2718,16 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
2248
2718
|
function collectSubagentSessions(messages, userMessageId) {
|
|
2249
2719
|
if (!messages || messages.length === 0) return [];
|
|
2250
2720
|
const byParent = messages.filter(
|
|
2251
|
-
(message) =>
|
|
2721
|
+
(message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
|
|
2252
2722
|
);
|
|
2253
2723
|
const assistants = byParent.length > 0 ? byParent : [];
|
|
2254
2724
|
if (assistants.length === 0) {
|
|
2255
|
-
const userIndex = messages.findIndex((message) =>
|
|
2725
|
+
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
2256
2726
|
if (userIndex === -1) return [];
|
|
2257
2727
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2258
2728
|
const message = messages[i];
|
|
2259
|
-
if (
|
|
2260
|
-
if (
|
|
2729
|
+
if (roleOf2(message) === "user") break;
|
|
2730
|
+
if (roleOf2(message) === "assistant") assistants.push(message);
|
|
2261
2731
|
}
|
|
2262
2732
|
}
|
|
2263
2733
|
const refs = [];
|
|
@@ -2297,7 +2767,7 @@ function collectTaskCalls(messages, userMessageId) {
|
|
|
2297
2767
|
if (!messages || messages.length === 0) return [];
|
|
2298
2768
|
const calls = [];
|
|
2299
2769
|
for (const message of messages) {
|
|
2300
|
-
if (
|
|
2770
|
+
if (roleOf2(message) !== "assistant" || parentIdOf2(message) !== userMessageId) continue;
|
|
2301
2771
|
for (const part of message.parts ?? []) {
|
|
2302
2772
|
if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
|
|
2303
2773
|
continue;
|
|
@@ -2327,7 +2797,7 @@ function attributeTaskCallUsage(messages, windows) {
|
|
|
2327
2797
|
for (const window of eligibleWindows) assignments.set(window.callID, []);
|
|
2328
2798
|
const unattributed = [];
|
|
2329
2799
|
for (const message of messages ?? []) {
|
|
2330
|
-
if (
|
|
2800
|
+
if (roleOf2(message) !== "assistant") continue;
|
|
2331
2801
|
const created = finiteNumber(createdOf(message));
|
|
2332
2802
|
const matching = created === null ? [] : eligibleWindows.filter(
|
|
2333
2803
|
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
@@ -2404,7 +2874,7 @@ function sumAssistantUsage(messages) {
|
|
|
2404
2874
|
function messageUsage(messages, userMessageId) {
|
|
2405
2875
|
if (!messages || messages.length === 0) return null;
|
|
2406
2876
|
const byParentAll = messages.filter(
|
|
2407
|
-
(m) =>
|
|
2877
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
|
|
2408
2878
|
);
|
|
2409
2879
|
const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
|
|
2410
2880
|
const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
|
|
@@ -2420,7 +2890,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
2420
2890
|
}
|
|
2421
2891
|
function messageRunState(messages, userMessageId) {
|
|
2422
2892
|
if (!messages || messages.length === 0) return "unknown";
|
|
2423
|
-
const hasUser = messages.some((m) =>
|
|
2893
|
+
const hasUser = messages.some((m) => idOf2(m) === userMessageId);
|
|
2424
2894
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
2425
2895
|
if (!hasUser) {
|
|
2426
2896
|
if (!reply) return "unknown";
|
|
@@ -2512,7 +2982,7 @@ function findLatestSubagentAuthOutcome(messages, sinceMs) {
|
|
|
2512
2982
|
if (!messages || messages.length === 0) return null;
|
|
2513
2983
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2514
2984
|
const message = messages[i];
|
|
2515
|
-
if (
|
|
2985
|
+
if (roleOf2(message) !== "assistant") continue;
|
|
2516
2986
|
const created = createdOf(message);
|
|
2517
2987
|
if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
|
|
2518
2988
|
const failure = classifyReplyAuthError(message);
|
|
@@ -2544,25 +3014,25 @@ function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProvi
|
|
|
2544
3014
|
function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
2545
3015
|
if (!messages || messages.length === 0) return false;
|
|
2546
3016
|
return messages.some(
|
|
2547
|
-
(m) =>
|
|
3017
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
2548
3018
|
);
|
|
2549
3019
|
}
|
|
2550
3020
|
function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
|
|
2551
3021
|
if (!messages || messages.length === 0) return false;
|
|
2552
|
-
const userIndex = messages.findIndex((message) =>
|
|
3022
|
+
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
2553
3023
|
if (userIndex === -1) return false;
|
|
2554
3024
|
let hasLaterUser = false;
|
|
2555
3025
|
let hasStartedLaterUser = false;
|
|
2556
3026
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2557
3027
|
const message = messages[i];
|
|
2558
|
-
if (
|
|
3028
|
+
if (roleOf2(message) !== "user") continue;
|
|
2559
3029
|
hasLaterUser = true;
|
|
2560
|
-
const laterUserMessageId =
|
|
3030
|
+
const laterUserMessageId = idOf2(message);
|
|
2561
3031
|
if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
|
|
2562
3032
|
return false;
|
|
2563
3033
|
}
|
|
2564
3034
|
if (messages.some(
|
|
2565
|
-
(candidate) =>
|
|
3035
|
+
(candidate) => roleOf2(candidate) === "assistant" && parentIdOf2(candidate) === laterUserMessageId
|
|
2566
3036
|
)) {
|
|
2567
3037
|
hasStartedLaterUser = true;
|
|
2568
3038
|
}
|
|
@@ -3033,13 +3503,17 @@ async function listV2Sessions(client) {
|
|
|
3033
3503
|
}
|
|
3034
3504
|
async function deleteV2Session(client, sessionId) {
|
|
3035
3505
|
try {
|
|
3036
|
-
await client.request(
|
|
3037
|
-
|
|
3506
|
+
const response = await client.request(
|
|
3507
|
+
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
3508
|
+
{ method: "DELETE" },
|
|
3509
|
+
{ allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100) }
|
|
3510
|
+
);
|
|
3511
|
+
return { ok: response.status >= 200 && response.status < 300, status: response.status };
|
|
3038
3512
|
} catch (error2) {
|
|
3039
3513
|
console.error(
|
|
3040
3514
|
`[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3041
3515
|
);
|
|
3042
|
-
return false;
|
|
3516
|
+
return { ok: false, status: "threw" };
|
|
3043
3517
|
}
|
|
3044
3518
|
}
|
|
3045
3519
|
async function v2SessionExists(client, sessionId) {
|
|
@@ -3214,7 +3688,7 @@ import { join as join4 } from "node:path";
|
|
|
3214
3688
|
|
|
3215
3689
|
// src/lib/openai-usage.ts
|
|
3216
3690
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
3217
|
-
import { homedir as
|
|
3691
|
+
import { homedir as homedir3 } from "node:os";
|
|
3218
3692
|
import { join as join3 } from "node:path";
|
|
3219
3693
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
3220
3694
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
@@ -3229,7 +3703,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
3229
3703
|
}
|
|
3230
3704
|
function readOpenCodeChatGptCredentials() {
|
|
3231
3705
|
try {
|
|
3232
|
-
const raw = readFileSync3(join3(
|
|
3706
|
+
const raw = readFileSync3(join3(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
3233
3707
|
let parsed;
|
|
3234
3708
|
try {
|
|
3235
3709
|
parsed = JSON.parse(raw);
|
|
@@ -3370,16 +3844,17 @@ function hasPrimaryHeaders(headers) {
|
|
|
3370
3844
|
].some((name) => headers.has(name));
|
|
3371
3845
|
}
|
|
3372
3846
|
async function getOpenAiUsage(port, client) {
|
|
3847
|
+
const openCodeVersion = client?.version ?? "v1";
|
|
3373
3848
|
const credentials2 = readOpenCodeChatGptCredentials();
|
|
3374
3849
|
if (!credentials2) {
|
|
3375
3850
|
throw new OpenAiUsageError(
|
|
3376
|
-
"No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
|
|
3851
|
+
openCodeVersion === "v2" ? "OpenAI usage reporting is not available on OpenCode V2 because V2 keeps a connected ChatGPT login in a store Evident cannot read." : "No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
|
|
3377
3852
|
"no_credentials"
|
|
3378
3853
|
);
|
|
3379
3854
|
}
|
|
3380
3855
|
if (credentials2.expiresAt < Date.now()) {
|
|
3381
3856
|
throw new OpenAiUsageError(
|
|
3382
|
-
"ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
|
|
3857
|
+
openCodeVersion === "v2" ? "The delivered ChatGPT login has expired. OpenCode V2 cannot be given a replacement by file; connect the account again from the runner's page in Evident." : "ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
|
|
3383
3858
|
"credentials_expired"
|
|
3384
3859
|
);
|
|
3385
3860
|
}
|
|
@@ -3660,9 +4135,9 @@ async function importOpenCodeV2Credentials({
|
|
|
3660
4135
|
|
|
3661
4136
|
// src/lib/opencode/session-db-boot.ts
|
|
3662
4137
|
import { spawn as spawn2 } from "node:child_process";
|
|
3663
|
-
import { mkdirSync, statSync as
|
|
3664
|
-
import { homedir as
|
|
3665
|
-
import { dirname as
|
|
4138
|
+
import { mkdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
|
|
4139
|
+
import { homedir as homedir4 } from "node:os";
|
|
4140
|
+
import { dirname as dirname3, resolve as resolvePath } from "node:path";
|
|
3666
4141
|
|
|
3667
4142
|
// src/lib/runner-synchroniser.ts
|
|
3668
4143
|
import { spawn } from "node:child_process";
|
|
@@ -3787,7 +4262,7 @@ function clearMarker(options) {
|
|
|
3787
4262
|
function markNoReplicate(options, message) {
|
|
3788
4263
|
if (options.noReplicateMarker) {
|
|
3789
4264
|
try {
|
|
3790
|
-
mkdirSync(
|
|
4265
|
+
mkdirSync(dirname3(options.noReplicateMarker), { recursive: true });
|
|
3791
4266
|
writeFileSync(options.noReplicateMarker, "");
|
|
3792
4267
|
} catch (error2) {
|
|
3793
4268
|
options.log(
|
|
@@ -3912,7 +4387,7 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
3912
4387
|
return null;
|
|
3913
4388
|
}
|
|
3914
4389
|
try {
|
|
3915
|
-
if (
|
|
4390
|
+
if (statSync3(configPath).size > 0) return configPath;
|
|
3916
4391
|
} catch (error2) {
|
|
3917
4392
|
if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")) {
|
|
3918
4393
|
options.log(
|
|
@@ -3942,7 +4417,7 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
3942
4417
|
return null;
|
|
3943
4418
|
}
|
|
3944
4419
|
try {
|
|
3945
|
-
mkdirSync(
|
|
4420
|
+
mkdirSync(dirname3(configPath), { recursive: true });
|
|
3946
4421
|
writeFileSync(configPath, rendered.stdout);
|
|
3947
4422
|
} catch (error2) {
|
|
3948
4423
|
options.log(
|
|
@@ -4017,14 +4492,14 @@ async function restoreSessionDb(options, configPath, env) {
|
|
|
4017
4492
|
case 0:
|
|
4018
4493
|
return;
|
|
4019
4494
|
case 31:
|
|
4020
|
-
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(
|
|
4495
|
+
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
|
|
4021
4496
|
options.log(
|
|
4022
4497
|
"SESSION-DB-REPLICA-UNUSABLE: booting with a fresh opencode.db and replicating into the existing prefix this boot",
|
|
4023
4498
|
"warn"
|
|
4024
4499
|
);
|
|
4025
4500
|
return;
|
|
4026
4501
|
case 32:
|
|
4027
|
-
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(
|
|
4502
|
+
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
|
|
4028
4503
|
discardSessionDbDebris(options);
|
|
4029
4504
|
markNoReplicate(
|
|
4030
4505
|
options,
|
|
@@ -4103,7 +4578,7 @@ async function verifySessionDb(options, configPath, env) {
|
|
|
4103
4578
|
}
|
|
4104
4579
|
function fileExists(path) {
|
|
4105
4580
|
try {
|
|
4106
|
-
|
|
4581
|
+
statSync3(path);
|
|
4107
4582
|
return true;
|
|
4108
4583
|
} catch (error2) {
|
|
4109
4584
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return false;
|
|
@@ -4113,7 +4588,7 @@ function fileExists(path) {
|
|
|
4113
4588
|
async function restoreAndVerifySessionDb(options) {
|
|
4114
4589
|
const env = options.env ?? process.env;
|
|
4115
4590
|
clearMarker(options);
|
|
4116
|
-
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(
|
|
4591
|
+
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
|
|
4117
4592
|
const synchroniserEnv = await runSynchroniser(["env"], {
|
|
4118
4593
|
timeoutMs: SESSION_DB_SYNCHRONISER_TIMEOUT_MS,
|
|
4119
4594
|
env
|
|
@@ -4192,7 +4667,7 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
4192
4667
|
// src/lib/opencode/session-db-provenance.ts
|
|
4193
4668
|
import { createRequire } from "node:module";
|
|
4194
4669
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4195
|
-
import { dirname as
|
|
4670
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
4196
4671
|
var require2 = createRequire(import.meta.url);
|
|
4197
4672
|
function readSessionDbMigrationIds(dbPath) {
|
|
4198
4673
|
let db;
|
|
@@ -4258,7 +4733,7 @@ function loadSessionDbProvenanceState(path) {
|
|
|
4258
4733
|
}
|
|
4259
4734
|
function saveSessionDbProvenanceState(path, state) {
|
|
4260
4735
|
try {
|
|
4261
|
-
mkdirSync2(
|
|
4736
|
+
mkdirSync2(dirname4(path), { recursive: true });
|
|
4262
4737
|
writeFileSync2(path, `${JSON.stringify(state, null, 2)}
|
|
4263
4738
|
`, "utf8");
|
|
4264
4739
|
} catch (error2) {
|
|
@@ -4839,7 +5314,10 @@ function selectSessionsToDelete(sessions, opts) {
|
|
|
4839
5314
|
toDelete.push(s.id);
|
|
4840
5315
|
}
|
|
4841
5316
|
}
|
|
4842
|
-
|
|
5317
|
+
const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
|
|
5318
|
+
return toDelete.sort(
|
|
5319
|
+
(a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
|
|
5320
|
+
);
|
|
4843
5321
|
}
|
|
4844
5322
|
var DEFAULT_INTERVAL = "1h";
|
|
4845
5323
|
function resolve(flag, envValue, fallback) {
|
|
@@ -4899,13 +5377,13 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
4899
5377
|
}
|
|
4900
5378
|
|
|
4901
5379
|
// src/lib/opencode/session-db-size.ts
|
|
4902
|
-
import { statSync as
|
|
5380
|
+
import { statSync as statSync4 } from "node:fs";
|
|
4903
5381
|
import { join as join6 } from "node:path";
|
|
4904
5382
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
4905
5383
|
function statSessionDbBytes(homeDir) {
|
|
4906
5384
|
const dbPath = join6(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
4907
5385
|
try {
|
|
4908
|
-
return
|
|
5386
|
+
return statSync4(dbPath).size;
|
|
4909
5387
|
} catch (err) {
|
|
4910
5388
|
const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
4911
5389
|
if (!isMissingFile) {
|
|
@@ -4931,12 +5409,12 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
4931
5409
|
}
|
|
4932
5410
|
|
|
4933
5411
|
// src/lib/opencode/log-tail.ts
|
|
4934
|
-
import { statSync as
|
|
4935
|
-
import { homedir as
|
|
5412
|
+
import { statSync as statSync5 } from "node:fs";
|
|
5413
|
+
import { homedir as homedir5 } from "node:os";
|
|
4936
5414
|
import { join as join7 } from "node:path";
|
|
4937
5415
|
import { open as open2, stat } from "node:fs/promises";
|
|
4938
5416
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
4939
|
-
function resolveOpenCodeLogPath(homeDir =
|
|
5417
|
+
function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
|
|
4940
5418
|
const dataDir = env.XDG_DATA_HOME || join7(homeDir, ".local", "share");
|
|
4941
5419
|
return join7(dataDir, "opencode", "log", "opencode.log");
|
|
4942
5420
|
}
|
|
@@ -4954,7 +5432,7 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
|
4954
5432
|
let inode = null;
|
|
4955
5433
|
let baselineReady = true;
|
|
4956
5434
|
try {
|
|
4957
|
-
const initial =
|
|
5435
|
+
const initial = statSync5(logPath);
|
|
4958
5436
|
offset = initial.size;
|
|
4959
5437
|
inode = initial.ino;
|
|
4960
5438
|
} catch (error2) {
|
|
@@ -4980,156 +5458,37 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
|
4980
5458
|
offset = current.size;
|
|
4981
5459
|
inode = current.ino;
|
|
4982
5460
|
baselineReady = true;
|
|
4983
|
-
return;
|
|
4984
|
-
}
|
|
4985
|
-
if (inode !== null && current.ino !== inode || current.size < offset) {
|
|
4986
|
-
offset = 0;
|
|
4987
|
-
}
|
|
4988
|
-
inode = current.ino;
|
|
4989
|
-
if (current.size === offset) return;
|
|
4990
|
-
const length = current.size - offset;
|
|
4991
|
-
const fh = await open2(logPath, "r");
|
|
4992
|
-
try {
|
|
4993
|
-
const buf = Buffer.alloc(length);
|
|
4994
|
-
const { bytesRead } = await fh.read(buf, 0, length, offset);
|
|
4995
|
-
offset += bytesRead;
|
|
4996
|
-
if (bytesRead > 0) onChunk(buf.subarray(0, bytesRead));
|
|
4997
|
-
} finally {
|
|
4998
|
-
await fh.close();
|
|
4999
|
-
}
|
|
5000
|
-
} catch (error2) {
|
|
5001
|
-
if (!isEnoent(error2)) reportFailure("poll", logPath, error2);
|
|
5002
|
-
} finally {
|
|
5003
|
-
polling = false;
|
|
5004
|
-
}
|
|
5005
|
-
};
|
|
5006
|
-
const interval = setInterval(() => void poll(), pollIntervalMs);
|
|
5007
|
-
void poll();
|
|
5008
|
-
return {
|
|
5009
|
-
stop: () => {
|
|
5010
|
-
stopped = true;
|
|
5011
|
-
clearInterval(interval);
|
|
5012
|
-
}
|
|
5013
|
-
};
|
|
5014
|
-
}
|
|
5015
|
-
|
|
5016
|
-
// src/lib/opencode/session-db-reclaim.ts
|
|
5017
|
-
import { statSync as statSync5, statfsSync } from "node:fs";
|
|
5018
|
-
import { dirname as dirname4 } from "node:path";
|
|
5019
|
-
function errorMessage(error2) {
|
|
5020
|
-
if (!(error2 instanceof Error)) return String(error2);
|
|
5021
|
-
return error2.cause instanceof Error ? error2.cause.message : error2.message;
|
|
5022
|
-
}
|
|
5023
|
-
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
5024
|
-
try {
|
|
5025
|
-
const fsStats = statfsSync(dirname4(dbPath));
|
|
5026
|
-
const availableBytes = fsStats.bavail * fsStats.bsize;
|
|
5027
|
-
if (availableBytes < requiredBytes) {
|
|
5028
|
-
return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
|
|
5029
|
-
}
|
|
5030
|
-
return null;
|
|
5031
|
-
} catch (err) {
|
|
5032
|
-
return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
|
|
5033
|
-
}
|
|
5034
|
-
}
|
|
5035
|
-
function readLogicalBytes(db) {
|
|
5036
|
-
const pageCount = db.prepare("PRAGMA page_count").get().page_count;
|
|
5037
|
-
const pageSize = db.prepare("PRAGMA page_size").get().page_size;
|
|
5038
|
-
return pageCount * pageSize;
|
|
5039
|
-
}
|
|
5040
|
-
function readCheckpointResult(db) {
|
|
5041
|
-
const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
5042
|
-
return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
|
|
5043
|
-
}
|
|
5044
|
-
async function probeReclaimAvailability(input) {
|
|
5045
|
-
const { dbPath, requiredBytes } = input;
|
|
5046
|
-
let sqlite;
|
|
5047
|
-
try {
|
|
5048
|
-
sqlite = await import("node:sqlite");
|
|
5049
|
-
} catch (err) {
|
|
5050
|
-
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
5051
|
-
console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
|
|
5052
|
-
return { reason: "sqlite-unavailable", detail };
|
|
5053
|
-
}
|
|
5054
|
-
let autoVacuum = null;
|
|
5055
|
-
try {
|
|
5056
|
-
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
5057
|
-
try {
|
|
5058
|
-
db.exec("PRAGMA busy_timeout=5000");
|
|
5059
|
-
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
5060
|
-
} finally {
|
|
5061
|
-
db.close();
|
|
5062
|
-
}
|
|
5063
|
-
} catch (err) {
|
|
5064
|
-
console.warn(
|
|
5065
|
-
`[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
5066
|
-
);
|
|
5067
|
-
}
|
|
5068
|
-
if (autoVacuum !== 0) return null;
|
|
5069
|
-
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
5070
|
-
}
|
|
5071
|
-
async function reclaimSessionDbSpace(input) {
|
|
5072
|
-
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
5073
|
-
let sqlite;
|
|
5074
|
-
try {
|
|
5075
|
-
sqlite = await import("node:sqlite");
|
|
5076
|
-
} catch (err) {
|
|
5077
|
-
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
5078
|
-
console.warn(
|
|
5079
|
-
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
|
|
5080
|
-
);
|
|
5081
|
-
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
5082
|
-
}
|
|
5083
|
-
const { DatabaseSync } = sqlite;
|
|
5084
|
-
let db;
|
|
5085
|
-
try {
|
|
5086
|
-
db = new DatabaseSync(dbPath);
|
|
5087
|
-
db.exec("PRAGMA busy_timeout=5000");
|
|
5088
|
-
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
5089
|
-
if (autoVacuum === 0) {
|
|
5090
|
-
if (!allowFullVacuum) {
|
|
5091
|
-
console.warn(
|
|
5092
|
-
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
|
|
5093
|
-
);
|
|
5094
|
-
return { ok: false, skipped: "full-vacuum-blocked" };
|
|
5461
|
+
return;
|
|
5095
5462
|
}
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
if (skipReason !== null) {
|
|
5099
|
-
console.warn(
|
|
5100
|
-
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
|
|
5101
|
-
);
|
|
5102
|
-
return { ok: false, skipped: "insufficient-disk-space" };
|
|
5463
|
+
if (inode !== null && current.ino !== inode || current.size < offset) {
|
|
5464
|
+
offset = 0;
|
|
5103
5465
|
}
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
const
|
|
5108
|
-
|
|
5109
|
-
|
|
5466
|
+
inode = current.ino;
|
|
5467
|
+
if (current.size === offset) return;
|
|
5468
|
+
const length = current.size - offset;
|
|
5469
|
+
const fh = await open2(logPath, "r");
|
|
5470
|
+
try {
|
|
5471
|
+
const buf = Buffer.alloc(length);
|
|
5472
|
+
const { bytesRead } = await fh.read(buf, 0, length, offset);
|
|
5473
|
+
offset += bytesRead;
|
|
5474
|
+
if (bytesRead > 0) onChunk(buf.subarray(0, bytesRead));
|
|
5475
|
+
} finally {
|
|
5476
|
+
await fh.close();
|
|
5477
|
+
}
|
|
5478
|
+
} catch (error2) {
|
|
5479
|
+
if (!isEnoent(error2)) reportFailure("poll", logPath, error2);
|
|
5480
|
+
} finally {
|
|
5481
|
+
polling = false;
|
|
5110
5482
|
}
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5483
|
+
};
|
|
5484
|
+
const interval = setInterval(() => void poll(), pollIntervalMs);
|
|
5485
|
+
void poll();
|
|
5486
|
+
return {
|
|
5487
|
+
stop: () => {
|
|
5488
|
+
stopped = true;
|
|
5489
|
+
clearInterval(interval);
|
|
5118
5490
|
}
|
|
5119
|
-
|
|
5120
|
-
`[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
|
|
5121
|
-
);
|
|
5122
|
-
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
5123
|
-
} catch (err) {
|
|
5124
|
-
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
5125
|
-
return {
|
|
5126
|
-
ok: false,
|
|
5127
|
-
skipped: "reclaim-error",
|
|
5128
|
-
detail: errorMessage(err)
|
|
5129
|
-
};
|
|
5130
|
-
} finally {
|
|
5131
|
-
db?.close();
|
|
5132
|
-
}
|
|
5491
|
+
};
|
|
5133
5492
|
}
|
|
5134
5493
|
|
|
5135
5494
|
// src/lib/tunnel/connection.ts
|
|
@@ -5361,6 +5720,8 @@ var StreamForwarder = class {
|
|
|
5361
5720
|
if (done) break;
|
|
5362
5721
|
const chunk = Buffer.from(value);
|
|
5363
5722
|
for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
|
|
5723
|
+
await stream.pausePromise;
|
|
5724
|
+
if (ac.signal.aborted) break;
|
|
5364
5725
|
const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
|
|
5365
5726
|
if (this.binaryFramesSupported) {
|
|
5366
5727
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
@@ -5403,6 +5764,7 @@ function classifyUpgradeRejection(headers) {
|
|
|
5403
5764
|
}
|
|
5404
5765
|
var MAX_RECONNECT_DELAY = 3e4;
|
|
5405
5766
|
var BASE_RECONNECT_DELAY = 500;
|
|
5767
|
+
var RELAY_SILENCE_TIMEOUT_MS = 9e4;
|
|
5406
5768
|
function getReconnectDelay(attempt) {
|
|
5407
5769
|
const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
|
|
5408
5770
|
const jitter = Math.random() * 1e3;
|
|
@@ -5474,10 +5836,27 @@ function connectTunnel(options) {
|
|
|
5474
5836
|
ws.close();
|
|
5475
5837
|
reject(new Error("Connection timeout"));
|
|
5476
5838
|
}, 3e4);
|
|
5839
|
+
let relaySilenceTimeout;
|
|
5840
|
+
let relaySilenceTimedOut = false;
|
|
5841
|
+
const clearRelaySilenceTimeout = () => {
|
|
5842
|
+
if (relaySilenceTimeout === void 0) return;
|
|
5843
|
+
clearTimeout(relaySilenceTimeout);
|
|
5844
|
+
relaySilenceTimeout = void 0;
|
|
5845
|
+
};
|
|
5846
|
+
const armRelaySilenceTimeout = () => {
|
|
5847
|
+
clearRelaySilenceTimeout();
|
|
5848
|
+
relaySilenceTimeout = setTimeout(() => {
|
|
5849
|
+
relaySilenceTimeout = void 0;
|
|
5850
|
+
relaySilenceTimedOut = true;
|
|
5851
|
+
ws.terminate();
|
|
5852
|
+
}, RELAY_SILENCE_TIMEOUT_MS);
|
|
5853
|
+
relaySilenceTimeout.unref?.();
|
|
5854
|
+
};
|
|
5477
5855
|
let upgradeRejection = null;
|
|
5478
5856
|
let upgradeRejectionReason = null;
|
|
5479
5857
|
ws.on("unexpected-response", (_req, res) => {
|
|
5480
5858
|
clearTimeout(connectionTimeout);
|
|
5859
|
+
clearRelaySilenceTimeout();
|
|
5481
5860
|
const reason = classifyUpgradeRejection(res.headers);
|
|
5482
5861
|
upgradeRejectionReason = reason;
|
|
5483
5862
|
const chunks = [];
|
|
@@ -5507,6 +5886,7 @@ function connectTunnel(options) {
|
|
|
5507
5886
|
onInfo?.("WebSocket connection established");
|
|
5508
5887
|
});
|
|
5509
5888
|
ws.on("message", (data, isBinary) => {
|
|
5889
|
+
if (relaySilenceTimeout !== void 0) armRelaySilenceTimeout();
|
|
5510
5890
|
if (isBinary) {
|
|
5511
5891
|
try {
|
|
5512
5892
|
const frame = decodeBinaryBodyFrame(toUint8Array(data));
|
|
@@ -5540,6 +5920,7 @@ function connectTunnel(options) {
|
|
|
5540
5920
|
switch (message.type) {
|
|
5541
5921
|
case "connected": {
|
|
5542
5922
|
clearTimeout(connectionTimeout);
|
|
5923
|
+
armRelaySilenceTimeout();
|
|
5543
5924
|
forwarder.setBinaryFramesSupported(message.binary_frames === true);
|
|
5544
5925
|
const connectedAgentId = message.agent_id ?? agentId;
|
|
5545
5926
|
onConnected?.(connectedAgentId);
|
|
@@ -5564,6 +5945,7 @@ function connectTunnel(options) {
|
|
|
5564
5945
|
});
|
|
5565
5946
|
ws.on("error", (error2) => {
|
|
5566
5947
|
clearTimeout(connectionTimeout);
|
|
5948
|
+
clearRelaySilenceTimeout();
|
|
5567
5949
|
const detail = upgradeRejection ?? describeSocketError(error2, url);
|
|
5568
5950
|
if (upgradeRejectionReason === "do_code_updated") {
|
|
5569
5951
|
onWarning?.("Relay redeployed \u2014 reconnecting");
|
|
@@ -5575,7 +5957,8 @@ function connectTunnel(options) {
|
|
|
5575
5957
|
);
|
|
5576
5958
|
});
|
|
5577
5959
|
ws.on("close", (code, reason) => {
|
|
5578
|
-
|
|
5960
|
+
clearRelaySilenceTimeout();
|
|
5961
|
+
const reasonStr = (relaySilenceTimedOut ? "no relay heartbeat for 90s \u2014 reconnecting" : void 0) || reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
|
|
5579
5962
|
forwarder.abortAll();
|
|
5580
5963
|
onDisconnected?.(code, reasonStr);
|
|
5581
5964
|
});
|
|
@@ -5583,9 +5966,10 @@ function connectTunnel(options) {
|
|
|
5583
5966
|
}
|
|
5584
5967
|
|
|
5585
5968
|
// src/lib/tunnel/runner-connection.ts
|
|
5586
|
-
var RunnerConnection = class {
|
|
5969
|
+
var RunnerConnection = class _RunnerConnection {
|
|
5587
5970
|
opts;
|
|
5588
5971
|
sleep;
|
|
5972
|
+
now;
|
|
5589
5973
|
connection = null;
|
|
5590
5974
|
resolvedAgentId;
|
|
5591
5975
|
/** True while a (re)connect loop is in flight. */
|
|
@@ -5594,10 +5978,17 @@ var RunnerConnection = class {
|
|
|
5594
5978
|
reconnectPromise = null;
|
|
5595
5979
|
/** 1-based count of the current reconnect attempt streak. */
|
|
5596
5980
|
reconnectAttempt = 0;
|
|
5981
|
+
generation = 0;
|
|
5982
|
+
disconnectedAtMs = null;
|
|
5983
|
+
static RELAY_RECONNECT_CLOSE_REASONS = /* @__PURE__ */ new Set([
|
|
5984
|
+
"No pong received",
|
|
5985
|
+
"Orphaned socket"
|
|
5986
|
+
]);
|
|
5597
5987
|
constructor(opts) {
|
|
5598
5988
|
this.opts = opts;
|
|
5599
5989
|
this.resolvedAgentId = opts.agentId;
|
|
5600
5990
|
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5991
|
+
this.now = opts.now ?? Date.now;
|
|
5601
5992
|
}
|
|
5602
5993
|
get agentId() {
|
|
5603
5994
|
return this.resolvedAgentId;
|
|
@@ -5633,14 +6024,24 @@ var RunnerConnection = class {
|
|
|
5633
6024
|
port: this.opts.port,
|
|
5634
6025
|
openCodePassword: this.opts.openCodePassword,
|
|
5635
6026
|
onConnected: (agentId) => {
|
|
6027
|
+
const attempt = this.reconnectAttempt + 1;
|
|
5636
6028
|
this.reconnectAttempt = 0;
|
|
5637
6029
|
this.reconnecting = false;
|
|
5638
6030
|
this.resolvedAgentId = agentId;
|
|
5639
|
-
|
|
6031
|
+
this.generation++;
|
|
6032
|
+
const downtimeMs = this.disconnectedAtMs === null ? null : this.now() - this.disconnectedAtMs;
|
|
6033
|
+
this.disconnectedAtMs = null;
|
|
6034
|
+
events.onConnected(agentId, isReconnect, {
|
|
6035
|
+
generation: this.generation,
|
|
6036
|
+
attempt,
|
|
6037
|
+
downtimeMs
|
|
6038
|
+
});
|
|
5640
6039
|
},
|
|
5641
6040
|
onDisconnected: (code, reason) => {
|
|
5642
|
-
|
|
5643
|
-
|
|
6041
|
+
this.disconnectedAtMs = this.now();
|
|
6042
|
+
events.onDisconnected(code, reason, { generation: this.generation });
|
|
6043
|
+
const relayRequestedReconnect = code === 1e3 && _RunnerConnection.RELAY_RECONNECT_CLOSE_REASONS.has(reason);
|
|
6044
|
+
if (this.opts.isRunning() && (code !== 1e3 || relayRequestedReconnect) && !this.reconnecting) {
|
|
5644
6045
|
this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
|
|
5645
6046
|
events.onError?.(`Reconnection failed: ${err.message}`);
|
|
5646
6047
|
});
|
|
@@ -5979,8 +6380,85 @@ function createResourceUsageCollector(homeDir) {
|
|
|
5979
6380
|
};
|
|
5980
6381
|
}
|
|
5981
6382
|
|
|
6383
|
+
// src/lib/resource-usage-low-disk.ts
|
|
6384
|
+
var RUNNER_DISK_ALERT_ENTRY_FREE_BYTES = 4 * 1024 ** 3;
|
|
6385
|
+
var RUNNER_DISK_ALERT_ENTRY_FREE_FRACTION = 0.25;
|
|
6386
|
+
var RUNNER_DISK_ALERT_VERY_LOW_FREE_BYTES = 1 * 1024 ** 3;
|
|
6387
|
+
var RUNNER_DISK_ALERT_CRITICAL_FREE_BYTES = 256 * 1024 ** 2;
|
|
6388
|
+
var RUNNER_DISK_ALERT_RECOVERY_FREE_BYTES = 6 * 1024 ** 3;
|
|
6389
|
+
var RUNNER_DISK_ALERT_RECOVERY_FREE_FRACTION = 0.375;
|
|
6390
|
+
var LOCAL_DISK_WARNING_REASSERTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
6391
|
+
var BAND_RANK = {
|
|
6392
|
+
low: 1,
|
|
6393
|
+
very_low: 2,
|
|
6394
|
+
critical: 3
|
|
6395
|
+
};
|
|
6396
|
+
function bandFor(freeBytes) {
|
|
6397
|
+
if (freeBytes < RUNNER_DISK_ALERT_CRITICAL_FREE_BYTES) return "critical";
|
|
6398
|
+
if (freeBytes < RUNNER_DISK_ALERT_VERY_LOW_FREE_BYTES) return "very_low";
|
|
6399
|
+
return "low";
|
|
6400
|
+
}
|
|
6401
|
+
function formatBytes(bytes) {
|
|
6402
|
+
if (bytes === null) return "unavailable";
|
|
6403
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
6404
|
+
const unitIndex = Math.min(Math.floor(Math.log2(Math.max(bytes, 1)) / 10), units.length - 1);
|
|
6405
|
+
const value = bytes / 1024 ** unitIndex;
|
|
6406
|
+
const digits = unitIndex === 0 || value >= 100 ? 0 : value >= 10 ? 1 : 2;
|
|
6407
|
+
return `${value.toFixed(digits)} ${units[unitIndex]}`;
|
|
6408
|
+
}
|
|
6409
|
+
function warningMessage(band, diskFreeBytes, diskTotalBytes, opencodeDbBytes) {
|
|
6410
|
+
return `Low disk warning (${band} severity): ${formatBytes(diskFreeBytes)} free of ${formatBytes(diskTotalBytes)}; opencode.db is ${formatBytes(opencodeDbBytes)}. Clear space on the machine running this runner before session history or new work can no longer be saved.`;
|
|
6411
|
+
}
|
|
6412
|
+
function evaluateLowDiskWarning(input) {
|
|
6413
|
+
const { diskFreeBytes, diskTotalBytes, opencodeDbBytes, nowMs, state } = input;
|
|
6414
|
+
if (diskFreeBytes === null || diskTotalBytes === null || diskTotalBytes <= 0 || diskFreeBytes >= diskTotalBytes) {
|
|
6415
|
+
return { warning: null, state };
|
|
6416
|
+
}
|
|
6417
|
+
const recovered = diskFreeBytes >= RUNNER_DISK_ALERT_RECOVERY_FREE_BYTES || diskFreeBytes >= diskTotalBytes * RUNNER_DISK_ALERT_RECOVERY_FREE_FRACTION;
|
|
6418
|
+
if (recovered) {
|
|
6419
|
+
return { warning: null, state: { worstBand: null, warnedAtMs: null } };
|
|
6420
|
+
}
|
|
6421
|
+
const low = diskFreeBytes < RUNNER_DISK_ALERT_ENTRY_FREE_BYTES && diskFreeBytes < diskTotalBytes * RUNNER_DISK_ALERT_ENTRY_FREE_FRACTION;
|
|
6422
|
+
if (!low) return { warning: null, state };
|
|
6423
|
+
const currentBand = bandFor(diskFreeBytes);
|
|
6424
|
+
const currentRank = BAND_RANK[currentBand];
|
|
6425
|
+
const previousRank = state.worstBand === null ? 0 : BAND_RANK[state.worstBand];
|
|
6426
|
+
const worse = currentRank > previousRank;
|
|
6427
|
+
const reassert = state.warnedAtMs !== null && nowMs - state.warnedAtMs >= LOCAL_DISK_WARNING_REASSERTION_INTERVAL_MS;
|
|
6428
|
+
const nextState = {
|
|
6429
|
+
worstBand: worse ? currentBand : state.worstBand ?? currentBand,
|
|
6430
|
+
warnedAtMs: worse || state.warnedAtMs === null || reassert ? nowMs : state.warnedAtMs
|
|
6431
|
+
};
|
|
6432
|
+
return {
|
|
6433
|
+
warning: worse || state.warnedAtMs === null || reassert ? warningMessage(nextState.worstBand, diskFreeBytes, diskTotalBytes, opencodeDbBytes) : null,
|
|
6434
|
+
state: nextState
|
|
6435
|
+
};
|
|
6436
|
+
}
|
|
6437
|
+
|
|
6438
|
+
// src/lib/opencode/session-store-health.ts
|
|
6439
|
+
var SESSION_STORE_HEALTH_STALENESS_MS = 5 * 6e4;
|
|
6440
|
+
function createSessionStoreHealth(options = {}) {
|
|
6441
|
+
const now = options.now ?? (() => Date.now());
|
|
6442
|
+
const stalenessMs = Math.max(0, options.stalenessMs ?? SESSION_STORE_HEALTH_STALENESS_MS);
|
|
6443
|
+
let lastUnwritableAt = null;
|
|
6444
|
+
let lastWritableAt = null;
|
|
6445
|
+
return {
|
|
6446
|
+
observeUnwritable: () => {
|
|
6447
|
+
lastUnwritableAt = now();
|
|
6448
|
+
},
|
|
6449
|
+
observeWritable: () => {
|
|
6450
|
+
lastWritableAt = now();
|
|
6451
|
+
},
|
|
6452
|
+
isUnwritable: () => {
|
|
6453
|
+
if (lastUnwritableAt === null) return false;
|
|
6454
|
+
if (lastWritableAt !== null && lastWritableAt >= lastUnwritableAt) return false;
|
|
6455
|
+
return now() - lastUnwritableAt < stalenessMs;
|
|
6456
|
+
}
|
|
6457
|
+
};
|
|
6458
|
+
}
|
|
6459
|
+
|
|
5982
6460
|
// src/lib/channels/driver.ts
|
|
5983
|
-
import { homedir as
|
|
6461
|
+
import { homedir as homedir6 } from "node:os";
|
|
5984
6462
|
|
|
5985
6463
|
// src/lib/runner-file-sync.ts
|
|
5986
6464
|
import { join as join9 } from "node:path";
|
|
@@ -6437,6 +6915,7 @@ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
|
6437
6915
|
var MAX_BUFFERED_SESSION_ERRORS = 256;
|
|
6438
6916
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
6439
6917
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
6918
|
+
var DRIVER_SESSION_ENUMERATION_LIMIT = 20;
|
|
6440
6919
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
6441
6920
|
var HEARTBEAT_MS = 6e4;
|
|
6442
6921
|
var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
|
|
@@ -6498,6 +6977,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6498
6977
|
maxActiveSessions;
|
|
6499
6978
|
watcherStallMs;
|
|
6500
6979
|
wedgeWarningIntervalMs;
|
|
6980
|
+
sessionStoreHealth;
|
|
6501
6981
|
openCodeClient;
|
|
6502
6982
|
/** Cache of conversationId → opencode sessionId. */
|
|
6503
6983
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -6863,10 +7343,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6863
7343
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
6864
7344
|
this.now = config.now ?? (() => Date.now());
|
|
6865
7345
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
6866
|
-
this.homeDir = config.homeDir ??
|
|
7346
|
+
this.homeDir = config.homeDir ?? homedir6();
|
|
6867
7347
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
6868
7348
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
6869
7349
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
7350
|
+
this.sessionStoreHealth = config.sessionStoreHealth;
|
|
6870
7351
|
}
|
|
6871
7352
|
get isV2() {
|
|
6872
7353
|
return this.openCodeClient.version === "v2";
|
|
@@ -6882,7 +7363,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6882
7363
|
return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6883
7364
|
}
|
|
6884
7365
|
async listSessions() {
|
|
6885
|
-
return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient
|
|
7366
|
+
return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient, {
|
|
7367
|
+
limit: DRIVER_SESSION_ENUMERATION_LIMIT
|
|
7368
|
+
});
|
|
6886
7369
|
}
|
|
6887
7370
|
async sessionExists(sessionId) {
|
|
6888
7371
|
return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
|
|
@@ -8336,8 +8819,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8336
8819
|
b2LastDescendantCheckMs: 0,
|
|
8337
8820
|
b2RootOngoingHeldLogged: false,
|
|
8338
8821
|
b2AbandonedSignalled: false,
|
|
8822
|
+
b2AbandonedWithoutAnswer: false,
|
|
8339
8823
|
ambiguousPinnedSinceMs: 0,
|
|
8340
8824
|
ambiguousResolved: false,
|
|
8825
|
+
sessionStoreFailurePending: false,
|
|
8341
8826
|
neverStartedResolved: false
|
|
8342
8827
|
});
|
|
8343
8828
|
const buffered = this.bufferedSessionErrors.get(sessionId);
|
|
@@ -8454,8 +8939,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8454
8939
|
b2LastDescendantCheckMs: 0,
|
|
8455
8940
|
b2RootOngoingHeldLogged: false,
|
|
8456
8941
|
b2AbandonedSignalled: false,
|
|
8942
|
+
b2AbandonedWithoutAnswer: false,
|
|
8457
8943
|
ambiguousPinnedSinceMs: 0,
|
|
8458
8944
|
ambiguousResolved: false,
|
|
8945
|
+
sessionStoreFailurePending: false,
|
|
8459
8946
|
neverStartedResolved: false
|
|
8460
8947
|
});
|
|
8461
8948
|
}
|
|
@@ -8869,6 +9356,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8869
9356
|
if (state === "running" || state === "done" || state === "failed") {
|
|
8870
9357
|
this.neverStartedAttempts.delete(id);
|
|
8871
9358
|
}
|
|
9359
|
+
if (inFlight.sessionStoreFailurePending) {
|
|
9360
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
9361
|
+
return;
|
|
9362
|
+
}
|
|
8872
9363
|
if (inFlight.neverStartedResolved) {
|
|
8873
9364
|
if (state === "running" || state === "done" || state === "failed") {
|
|
8874
9365
|
inFlight.neverStartedResolved = false;
|
|
@@ -9028,10 +9519,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9028
9519
|
inFlight.b2LastDescendantCheckMs = 0;
|
|
9029
9520
|
inFlight.b2RootOngoingHeldLogged = false;
|
|
9030
9521
|
inFlight.b2AbandonedSignalled = false;
|
|
9522
|
+
inFlight.b2AbandonedWithoutAnswer = false;
|
|
9031
9523
|
}
|
|
9032
9524
|
} else {
|
|
9033
9525
|
if (inFlight.b2AbandonedSignalled) {
|
|
9034
|
-
|
|
9526
|
+
if (inFlight.b2AbandonedWithoutAnswer) {
|
|
9527
|
+
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
9528
|
+
await this.settleMessageAbandonedWithoutAnswer(
|
|
9529
|
+
sessionId,
|
|
9530
|
+
watcher,
|
|
9531
|
+
inFlight,
|
|
9532
|
+
messages,
|
|
9533
|
+
reply?.info?.finish ?? reply?.finish
|
|
9534
|
+
);
|
|
9535
|
+
} else {
|
|
9536
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
9537
|
+
}
|
|
9035
9538
|
return;
|
|
9036
9539
|
}
|
|
9037
9540
|
if (inFlight.b2PinnedSinceMs === 0) inFlight.b2PinnedSinceMs = this.now();
|
|
@@ -9048,17 +9551,47 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9048
9551
|
descendantOngoing,
|
|
9049
9552
|
rootOngoing
|
|
9050
9553
|
})) {
|
|
9554
|
+
if (this.sessionStoreHealth?.isUnwritable()) {
|
|
9555
|
+
inFlight.sessionStoreFailurePending = true;
|
|
9556
|
+
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
9557
|
+
watched_for_ms: pinnedForMs,
|
|
9558
|
+
run_state: "failed",
|
|
9559
|
+
opencode_message_id: inFlight.opencodeMessageId
|
|
9560
|
+
});
|
|
9561
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
9562
|
+
return;
|
|
9563
|
+
}
|
|
9051
9564
|
inFlight.b2AbandonedSignalled = true;
|
|
9565
|
+
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
9566
|
+
const finish = reply?.info?.finish ?? reply?.finish;
|
|
9567
|
+
if (extractReplyTextForMessage(messages, inFlight.opencodeMessageId) === null) {
|
|
9568
|
+
inFlight.b2AbandonedWithoutAnswer = true;
|
|
9569
|
+
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
9570
|
+
watched_for_ms: pinnedForMs,
|
|
9571
|
+
run_state: "failed",
|
|
9572
|
+
finish,
|
|
9573
|
+
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
9574
|
+
...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
|
|
9575
|
+
opencode_message_id: inFlight.opencodeMessageId
|
|
9576
|
+
});
|
|
9577
|
+
await this.settleMessageAbandonedWithoutAnswer(
|
|
9578
|
+
sessionId,
|
|
9579
|
+
watcher,
|
|
9580
|
+
inFlight,
|
|
9581
|
+
messages,
|
|
9582
|
+
finish
|
|
9583
|
+
);
|
|
9584
|
+
return;
|
|
9585
|
+
}
|
|
9052
9586
|
this.log({
|
|
9053
9587
|
level: "warn",
|
|
9054
9588
|
message: `Message ${id.slice(0, 8)} b2-pinned for ${Math.round(pinnedForMs / 1e3)}s with no ongoing descendant sub-agent session (status-map confirmed) \u2014 treating the delegated/tool turn as abandoned, resolving done`,
|
|
9055
9589
|
conversation_id: conv.id,
|
|
9056
9590
|
message_id: id
|
|
9057
9591
|
});
|
|
9058
|
-
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
9059
9592
|
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
9060
9593
|
watched_for_ms: pinnedForMs,
|
|
9061
|
-
finish
|
|
9594
|
+
finish,
|
|
9062
9595
|
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
9063
9596
|
...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
|
|
9064
9597
|
opencode_message_id: inFlight.opencodeMessageId
|
|
@@ -9106,6 +9639,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9106
9639
|
maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
|
|
9107
9640
|
sessionOngoing: ongoing
|
|
9108
9641
|
})) {
|
|
9642
|
+
if (this.sessionStoreHealth?.isUnwritable()) {
|
|
9643
|
+
inFlight.sessionStoreFailurePending = true;
|
|
9644
|
+
void this.postSignal(conv.id, id, "ambiguous_finish_resolved", {
|
|
9645
|
+
watched_for_ms: pinnedForMs,
|
|
9646
|
+
run_state: "failed",
|
|
9647
|
+
opencode_message_id: inFlight.opencodeMessageId
|
|
9648
|
+
});
|
|
9649
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
9650
|
+
return;
|
|
9651
|
+
}
|
|
9109
9652
|
inFlight.ambiguousResolved = true;
|
|
9110
9653
|
this.log({
|
|
9111
9654
|
level: "warn",
|
|
@@ -9330,6 +9873,74 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9330
9873
|
}
|
|
9331
9874
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9332
9875
|
}
|
|
9876
|
+
/** Report a confirmed b2 abandonment that produced no answer text. */
|
|
9877
|
+
async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages, finish) {
|
|
9878
|
+
const conv = watcher.conv;
|
|
9879
|
+
const error2 = `OpenCode ended this turn after a tool call (finish="${finish ?? "(unknown)"}") without writing an answer, and confirmed it is no longer working on this conversation. Evident is reporting the run as failed so the failure and its reason are visible in failed runs.`;
|
|
9880
|
+
this.anchorDeliveryDeadline(inFlight);
|
|
9881
|
+
this.log({
|
|
9882
|
+
level: "warn",
|
|
9883
|
+
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} was abandoned without an answer \u2014 ${error2}`,
|
|
9884
|
+
conversation_id: conv.id,
|
|
9885
|
+
message_id: inFlight.evidentMessageId
|
|
9886
|
+
});
|
|
9887
|
+
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
9888
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
9889
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
9890
|
+
messages,
|
|
9891
|
+
inFlight.opencodeMessageId,
|
|
9892
|
+
inFlight.evidentMessageId
|
|
9893
|
+
);
|
|
9894
|
+
try {
|
|
9895
|
+
await this.markFailed(
|
|
9896
|
+
conv.id,
|
|
9897
|
+
inFlight.evidentMessageId,
|
|
9898
|
+
sessionId,
|
|
9899
|
+
error2,
|
|
9900
|
+
usage,
|
|
9901
|
+
void 0,
|
|
9902
|
+
usageAgentName,
|
|
9903
|
+
subagentInvocations
|
|
9904
|
+
);
|
|
9905
|
+
} catch (err) {
|
|
9906
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
9907
|
+
if (err instanceof ChannelTerminalError) {
|
|
9908
|
+
this.log({
|
|
9909
|
+
level: "warn",
|
|
9910
|
+
message: `Failed to report abandoned no-answer message ${inFlight.evidentMessageId.slice(0, 8)} as failed (terminal HTTP ${err.status}) \u2014 leaving it for the cron safety net: ${err.message}`,
|
|
9911
|
+
conversation_id: conv.id,
|
|
9912
|
+
message_id: inFlight.evidentMessageId
|
|
9913
|
+
});
|
|
9914
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9915
|
+
return;
|
|
9916
|
+
}
|
|
9917
|
+
if (this.now() >= inFlight.deadline) {
|
|
9918
|
+
this.log({
|
|
9919
|
+
level: "warn",
|
|
9920
|
+
message: `Failed to report abandoned no-answer message ${inFlight.evidentMessageId.slice(0, 8)} as failed within the delivery window \u2014 leaving it for the cron safety net: ${err instanceof Error ? err.message : String(err)}`,
|
|
9921
|
+
conversation_id: conv.id,
|
|
9922
|
+
message_id: inFlight.evidentMessageId
|
|
9923
|
+
});
|
|
9924
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9925
|
+
return;
|
|
9926
|
+
}
|
|
9927
|
+
this.log({
|
|
9928
|
+
level: "warn",
|
|
9929
|
+
message: `Failed to report abandoned no-answer message ${inFlight.evidentMessageId.slice(0, 8)} as failed (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
9930
|
+
conversation_id: conv.id,
|
|
9931
|
+
message_id: inFlight.evidentMessageId
|
|
9932
|
+
});
|
|
9933
|
+
return;
|
|
9934
|
+
}
|
|
9935
|
+
inFlight.done = true;
|
|
9936
|
+
await this.reportSubagentAuthFailures(
|
|
9937
|
+
watcher.conv.id,
|
|
9938
|
+
inFlight.opencodeMessageId,
|
|
9939
|
+
inFlight.evidentMessageId,
|
|
9940
|
+
messages
|
|
9941
|
+
);
|
|
9942
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9943
|
+
}
|
|
9333
9944
|
/**
|
|
9334
9945
|
* Report a queued message as failed after the never-started bound. The accepted
|
|
9335
9946
|
* OpenCode copy is not cancelled, so the local fence remains on unsuccessful
|
|
@@ -9385,6 +9996,66 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9385
9996
|
this.neverStartedAttempts.delete(inFlight.evidentMessageId);
|
|
9386
9997
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9387
9998
|
}
|
|
9999
|
+
/**
|
|
10000
|
+
* Report a turn as failed when the session database was observed unwritable.
|
|
10001
|
+
* The queue row is retained as failed, while the OpenCode copy stays fenced so
|
|
10002
|
+
* a later drain cannot blindly prompt the same turn again.
|
|
10003
|
+
*/
|
|
10004
|
+
async settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight) {
|
|
10005
|
+
const conv = watcher.conv;
|
|
10006
|
+
const error2 = "The runner session database could not be written, so this turn did not finish. Nothing was lost from the queue. Clear disk space and retry.";
|
|
10007
|
+
this.log({
|
|
10008
|
+
level: "error",
|
|
10009
|
+
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} was not resolved as done: ${error2}`,
|
|
10010
|
+
conversation_id: conv.id,
|
|
10011
|
+
message_id: inFlight.evidentMessageId
|
|
10012
|
+
});
|
|
10013
|
+
this.anchorDeliveryDeadline(inFlight);
|
|
10014
|
+
try {
|
|
10015
|
+
await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2);
|
|
10016
|
+
} catch (err) {
|
|
10017
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
10018
|
+
if (err instanceof ChannelTerminalError) {
|
|
10019
|
+
this.log({
|
|
10020
|
+
level: "warn",
|
|
10021
|
+
message: `Failed to report session-store failure for message ${inFlight.evidentMessageId.slice(0, 8)} as failed (terminal HTTP ${err.status}) \u2014 keeping its OpenCode copy fenced: ${err.message}`,
|
|
10022
|
+
conversation_id: conv.id,
|
|
10023
|
+
message_id: inFlight.evidentMessageId
|
|
10024
|
+
});
|
|
10025
|
+
this.recordReleasedOpencodeId(
|
|
10026
|
+
inFlight.evidentMessageId,
|
|
10027
|
+
sessionId,
|
|
10028
|
+
inFlight.opencodeMessageId
|
|
10029
|
+
);
|
|
10030
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
10031
|
+
return;
|
|
10032
|
+
}
|
|
10033
|
+
if (this.now() >= inFlight.deadline) {
|
|
10034
|
+
this.log({
|
|
10035
|
+
level: "warn",
|
|
10036
|
+
message: `Failed to report session-store failure for message ${inFlight.evidentMessageId.slice(0, 8)} as failed within the delivery window \u2014 keeping its OpenCode copy fenced: ${err instanceof Error ? err.message : String(err)}`,
|
|
10037
|
+
conversation_id: conv.id,
|
|
10038
|
+
message_id: inFlight.evidentMessageId
|
|
10039
|
+
});
|
|
10040
|
+
this.recordReleasedOpencodeId(
|
|
10041
|
+
inFlight.evidentMessageId,
|
|
10042
|
+
sessionId,
|
|
10043
|
+
inFlight.opencodeMessageId
|
|
10044
|
+
);
|
|
10045
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
10046
|
+
return;
|
|
10047
|
+
}
|
|
10048
|
+
this.log({
|
|
10049
|
+
level: "warn",
|
|
10050
|
+
message: `Failed to report session-store failure for message ${inFlight.evidentMessageId.slice(0, 8)} as failed (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
10051
|
+
conversation_id: conv.id,
|
|
10052
|
+
message_id: inFlight.evidentMessageId
|
|
10053
|
+
});
|
|
10054
|
+
return;
|
|
10055
|
+
}
|
|
10056
|
+
inFlight.done = true;
|
|
10057
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
10058
|
+
}
|
|
9388
10059
|
// Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
|
|
9389
10060
|
/**
|
|
9390
10061
|
* Re-adopt this agent's `processing` messages on drain (ADR-0046 Decision §1).
|
|
@@ -12116,6 +12787,7 @@ function createCredentialSync({
|
|
|
12116
12787
|
|
|
12117
12788
|
// src/commands/run.ts
|
|
12118
12789
|
var MAX_ACTIVITY_LOG_ENTRIES = 10;
|
|
12790
|
+
var MAX_DISCONNECT_REASON_LENGTH = 200;
|
|
12119
12791
|
var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
|
|
12120
12792
|
var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
|
|
12121
12793
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
|
|
@@ -12271,7 +12943,13 @@ function logActivity(state, entry) {
|
|
|
12271
12943
|
const level = entry.level ?? (entry.type === "error" ? "error" : "info");
|
|
12272
12944
|
if (!meetsThreshold(state, level)) return;
|
|
12273
12945
|
forwardRunnerActivity(
|
|
12274
|
-
{
|
|
12946
|
+
{
|
|
12947
|
+
level,
|
|
12948
|
+
message: entry.message,
|
|
12949
|
+
error: entry.error,
|
|
12950
|
+
metadata: entry.metadata,
|
|
12951
|
+
...entry.source ? { source: entry.source } : {}
|
|
12952
|
+
},
|
|
12275
12953
|
{ agentId: state.agentId, authHeader: state.authHeader }
|
|
12276
12954
|
);
|
|
12277
12955
|
const fullEntry = {
|
|
@@ -12293,7 +12971,7 @@ function logActivity(state, entry) {
|
|
|
12293
12971
|
}
|
|
12294
12972
|
function reportSessionDbRecovery(state) {
|
|
12295
12973
|
try {
|
|
12296
|
-
const report = drainSessionDbRecoveryReport({ homeDir:
|
|
12974
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir7(), env: process.env });
|
|
12297
12975
|
for (const record of report.records) {
|
|
12298
12976
|
const activity = buildSessionDbRecoveryActivity(record);
|
|
12299
12977
|
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
@@ -12328,7 +13006,9 @@ function displayStatus(state) {
|
|
|
12328
13006
|
const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
|
|
12329
13007
|
const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
|
|
12330
13008
|
const last = state.activityLog[state.activityLog.length - 1];
|
|
12331
|
-
const detail = last ? chalk7.dim(
|
|
13009
|
+
const detail = last ? chalk7.dim(
|
|
13010
|
+
` \xB7 ${last.timestamp.toISOString()} ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`
|
|
13011
|
+
) : "";
|
|
12332
13012
|
const agent = state.agentName ?? state.agentId;
|
|
12333
13013
|
console.log(
|
|
12334
13014
|
`${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
|
|
@@ -12347,7 +13027,7 @@ function reportOpenCodeV2CredentialImport(state, result) {
|
|
|
12347
13027
|
logActivity(state, {
|
|
12348
13028
|
type: "info",
|
|
12349
13029
|
level: "warn",
|
|
12350
|
-
message: `OpenCode V2 saved login(s) for ${integrationIds} on this machine, but OpenCode V2 will not use them.
|
|
13030
|
+
message: `OpenCode V2 saved login(s) for ${integrationIds} on this machine, but OpenCode V2 will not use them. Connect the provider again from the runner's page in Evident; no shell on this machine is needed.`
|
|
12351
13031
|
});
|
|
12352
13032
|
}
|
|
12353
13033
|
if (result.imported.length === 0 && result.unsupported.length === 0 && result.failed.length === 0) {
|
|
@@ -12372,7 +13052,7 @@ async function importOpenCodeV2CredentialsForState(state) {
|
|
|
12372
13052
|
}
|
|
12373
13053
|
const result = await importOpenCodeV2Credentials({
|
|
12374
13054
|
request: client.request,
|
|
12375
|
-
homeDir:
|
|
13055
|
+
homeDir: homedir7(),
|
|
12376
13056
|
directory,
|
|
12377
13057
|
guard: state.opencodeV2CredentialImportGuard,
|
|
12378
13058
|
log: (message, level = "info") => logActivity(state, { type: "info", level, message })
|
|
@@ -12580,9 +13260,9 @@ async function driveChannels(state, driver) {
|
|
|
12580
13260
|
}
|
|
12581
13261
|
}
|
|
12582
13262
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
12583
|
-
var
|
|
13263
|
+
var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
|
|
12584
13264
|
function shouldWarnForReclaimSkip(reason) {
|
|
12585
|
-
if (reason !== "sqlite-unavailable") return false;
|
|
13265
|
+
if (reason !== "sqlite-unavailable" && reason !== "disk-exhaustion") return false;
|
|
12586
13266
|
const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
|
|
12587
13267
|
if (!version2) return false;
|
|
12588
13268
|
const major = Number(version2[1]);
|
|
@@ -12591,7 +13271,14 @@ function shouldWarnForReclaimSkip(reason) {
|
|
|
12591
13271
|
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
12592
13272
|
}
|
|
12593
13273
|
function sessionDbPath() {
|
|
12594
|
-
return join11(
|
|
13274
|
+
return join11(homedir7(), ".local", "share", "opencode", "opencode.db");
|
|
13275
|
+
}
|
|
13276
|
+
function logSessionCleanupActivity(state, phase, entry) {
|
|
13277
|
+
logActivity(state, {
|
|
13278
|
+
...entry,
|
|
13279
|
+
source: "cli.session-cleanup",
|
|
13280
|
+
metadata: { ...entry.metadata, phase }
|
|
13281
|
+
});
|
|
12595
13282
|
}
|
|
12596
13283
|
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
12597
13284
|
const record = {
|
|
@@ -12632,69 +13319,136 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
|
|
|
12632
13319
|
async function runSweep(state, driver, config) {
|
|
12633
13320
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
12634
13321
|
try {
|
|
12635
|
-
const
|
|
12636
|
-
|
|
13322
|
+
const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0);
|
|
13323
|
+
const sessions = enumeration.sessions;
|
|
13324
|
+
const enumerationReason = sessions === null ? enumeration.reason : null;
|
|
13325
|
+
if (enumerationReason !== null) {
|
|
12637
13326
|
logActivity(state, {
|
|
12638
13327
|
type: "info",
|
|
12639
|
-
|
|
13328
|
+
level: "warn",
|
|
13329
|
+
message: `Session cleanup: could not enumerate sessions (${enumerationReason}); skipping deletes and continuing with space reclaim (${mode})`
|
|
12640
13330
|
});
|
|
12641
|
-
return;
|
|
12642
13331
|
}
|
|
12643
|
-
const toDelete = selectSessionsToDelete(
|
|
12644
|
-
sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
|
|
12645
|
-
{
|
|
12646
|
-
maxAgeMs: config.maxAgeMs,
|
|
12647
|
-
maxCount: config.maxCount,
|
|
12648
|
-
nowMs: Date.now(),
|
|
12649
|
-
protectedIds: driver.protectedSessionIds()
|
|
12650
|
-
}
|
|
12651
|
-
);
|
|
12652
|
-
const protectedNow = driver.protectedSessionIds();
|
|
12653
13332
|
let deleted = 0;
|
|
12654
13333
|
let failed = 0;
|
|
13334
|
+
let failed4xx = 0;
|
|
13335
|
+
let failed5xx = 0;
|
|
13336
|
+
let failedOtherStatus = 0;
|
|
13337
|
+
let failedThrew = 0;
|
|
12655
13338
|
let skippedNewlyActive = 0;
|
|
12656
|
-
|
|
12657
|
-
|
|
12658
|
-
|
|
12659
|
-
|
|
12660
|
-
|
|
12661
|
-
|
|
12662
|
-
|
|
12663
|
-
|
|
13339
|
+
let remaining = 0;
|
|
13340
|
+
let protectedNow = driver.protectedSessionIds();
|
|
13341
|
+
if (sessions !== null) {
|
|
13342
|
+
const toDelete = selectSessionsToDelete(
|
|
13343
|
+
sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
|
|
13344
|
+
{
|
|
13345
|
+
maxAgeMs: config.maxAgeMs,
|
|
13346
|
+
maxCount: config.maxCount,
|
|
13347
|
+
nowMs: Date.now(),
|
|
13348
|
+
protectedIds: protectedNow
|
|
13349
|
+
}
|
|
13350
|
+
);
|
|
13351
|
+
const deleteCandidates = toDelete.slice(0, SESSION_CLEANUP_MAX_DELETES_PER_SWEEP);
|
|
13352
|
+
remaining = toDelete.length - deleteCandidates.length;
|
|
13353
|
+
protectedNow = driver.protectedSessionIds();
|
|
13354
|
+
for (const id of deleteCandidates) {
|
|
13355
|
+
if (protectedNow.has(id)) {
|
|
13356
|
+
skippedNewlyActive++;
|
|
13357
|
+
logActivity(state, {
|
|
13358
|
+
type: "info",
|
|
13359
|
+
message: `Session cleanup: skipping ${id} \u2014 became active/bound after selection (${mode})`
|
|
13360
|
+
});
|
|
13361
|
+
continue;
|
|
13362
|
+
}
|
|
13363
|
+
const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
|
|
13364
|
+
if (outcome.ok) {
|
|
13365
|
+
deleted++;
|
|
13366
|
+
} else if (outcome.status === "threw") {
|
|
13367
|
+
failed++;
|
|
13368
|
+
failedThrew++;
|
|
13369
|
+
} else {
|
|
13370
|
+
failed++;
|
|
13371
|
+
const statusClass = Math.floor(outcome.status / 100);
|
|
13372
|
+
if (statusClass === 4) failed4xx++;
|
|
13373
|
+
else if (statusClass === 5) failed5xx++;
|
|
13374
|
+
else failedOtherStatus++;
|
|
13375
|
+
}
|
|
12664
13376
|
}
|
|
12665
|
-
|
|
12666
|
-
|
|
13377
|
+
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
13378
|
+
const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
|
|
13379
|
+
const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
|
|
13380
|
+
logActivity(state, {
|
|
13381
|
+
type: "info",
|
|
13382
|
+
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
|
|
13383
|
+
});
|
|
12667
13384
|
}
|
|
12668
|
-
const
|
|
12669
|
-
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
|
-
|
|
12673
|
-
|
|
13385
|
+
const sessionMetadata = {
|
|
13386
|
+
sessions_inspected: sessions?.length ?? null,
|
|
13387
|
+
sessions_deleted: deleted,
|
|
13388
|
+
sessions_failed: failed,
|
|
13389
|
+
sessions_failed_4xx: failed4xx,
|
|
13390
|
+
sessions_failed_5xx: failed5xx,
|
|
13391
|
+
sessions_failed_other: failedOtherStatus,
|
|
13392
|
+
sessions_failed_threw: failedThrew,
|
|
13393
|
+
sessions_skipped_active: skippedNewlyActive,
|
|
13394
|
+
sessions_remaining: remaining,
|
|
13395
|
+
...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
|
|
13396
|
+
};
|
|
12674
13397
|
const reclaimResult = await reclaimSessionDbSpace({
|
|
12675
13398
|
dbPath: sessionDbPath(),
|
|
12676
|
-
|
|
13399
|
+
chunkPages: SESSION_DB_RECLAIM_CHUNK_PAGES,
|
|
13400
|
+
pageCeiling: SESSION_DB_RECLAIM_PAGE_CEILING,
|
|
13401
|
+
timeBudgetMs: SESSION_DB_RECLAIM_TIME_BUDGET_MS,
|
|
12677
13402
|
allowFullVacuum: protectedNow.size === 0 && !state.sessionDbProvenanceAnomaly
|
|
12678
13403
|
});
|
|
12679
13404
|
if (reclaimResult.ok) {
|
|
13405
|
+
state.sessionStoreHealth.observeWritable();
|
|
12680
13406
|
const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
|
|
12681
13407
|
const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
|
|
12682
13408
|
const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
|
|
12683
|
-
|
|
13409
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12684
13410
|
type: "info",
|
|
12685
|
-
message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${
|
|
13411
|
+
message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB (stopped by ${reclaimResult.stoppedBy}; freelist ${reclaimResult.freelistBefore} -> ${reclaimResult.freelistAfter} pages)` + checkpointNote,
|
|
13412
|
+
metadata: {
|
|
13413
|
+
outcome: "reclaimed",
|
|
13414
|
+
mode: reclaimResult.mode,
|
|
13415
|
+
before_bytes: reclaimResult.beforeBytes,
|
|
13416
|
+
after_bytes: reclaimResult.afterBytes,
|
|
13417
|
+
checkpoint_busy: reclaimResult.checkpoint.busy,
|
|
13418
|
+
wal_frames_pending: reclaimResult.checkpoint.log,
|
|
13419
|
+
stopped_by: reclaimResult.stoppedBy,
|
|
13420
|
+
freelist_before: reclaimResult.freelistBefore,
|
|
13421
|
+
freelist_after: reclaimResult.freelistAfter,
|
|
13422
|
+
pages_reclaimed: reclaimResult.pagesReclaimed,
|
|
13423
|
+
reclaim_elapsed_ms: reclaimResult.elapsedMs,
|
|
13424
|
+
...sessionMetadata
|
|
13425
|
+
}
|
|
12686
13426
|
});
|
|
12687
13427
|
} else {
|
|
12688
|
-
|
|
13428
|
+
if (reclaimResult.skipped === "disk-exhaustion") {
|
|
13429
|
+
state.sessionStoreHealth.observeUnwritable();
|
|
13430
|
+
}
|
|
13431
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12689
13432
|
type: "info",
|
|
12690
|
-
|
|
13433
|
+
level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "disk-exhaustion" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
|
|
13434
|
+
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : ""),
|
|
13435
|
+
metadata: {
|
|
13436
|
+
outcome: "reclaim-skipped",
|
|
13437
|
+
reason: reclaimResult.skipped,
|
|
13438
|
+
...reclaimResult.detail !== void 0 ? { detail: reclaimResult.detail } : {},
|
|
13439
|
+
...sessionMetadata
|
|
13440
|
+
}
|
|
12691
13441
|
});
|
|
12692
13442
|
}
|
|
12693
13443
|
} catch (error2) {
|
|
12694
13444
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
12695
|
-
|
|
13445
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12696
13446
|
type: "error",
|
|
12697
|
-
error: `Session cleanup sweep failed (non-fatal, ${mode}): ${message}
|
|
13447
|
+
error: `Session cleanup sweep failed (non-fatal, ${mode}): ${message}`,
|
|
13448
|
+
metadata: {
|
|
13449
|
+
outcome: "sweep-failed",
|
|
13450
|
+
reason: "sweep-error"
|
|
13451
|
+
}
|
|
12698
13452
|
});
|
|
12699
13453
|
}
|
|
12700
13454
|
}
|
|
@@ -12710,14 +13464,19 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
12710
13464
|
for (const warning2 of config.warnings) {
|
|
12711
13465
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
12712
13466
|
}
|
|
12713
|
-
const dbBytes = statSessionDbBytes(
|
|
13467
|
+
const dbBytes = statSessionDbBytes(homedir7());
|
|
12714
13468
|
void (async () => {
|
|
12715
13469
|
const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
12716
13470
|
if (reclaimAvailability !== null) {
|
|
12717
|
-
|
|
13471
|
+
logSessionCleanupActivity(state, "startup", {
|
|
12718
13472
|
type: "info",
|
|
12719
13473
|
level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
|
|
12720
|
-
message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : "")
|
|
13474
|
+
message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : ""),
|
|
13475
|
+
metadata: {
|
|
13476
|
+
outcome: "preflight-skipped",
|
|
13477
|
+
reason: reclaimAvailability.reason,
|
|
13478
|
+
...reclaimAvailability.detail !== void 0 ? { detail: reclaimAvailability.detail } : {}
|
|
13479
|
+
}
|
|
12721
13480
|
});
|
|
12722
13481
|
}
|
|
12723
13482
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
@@ -12734,9 +13493,15 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
12734
13493
|
);
|
|
12735
13494
|
});
|
|
12736
13495
|
if (!config.enabled) return;
|
|
12737
|
-
|
|
13496
|
+
logSessionCleanupActivity(state, "startup", {
|
|
12738
13497
|
type: "info",
|
|
12739
|
-
message: `Session cleanup enabled (age=${config.maxAgeMs ?? "\u2014"}, count=${config.maxCount ?? "\u2014"}, interval=${config.intervalMs}ms)
|
|
13498
|
+
message: `Session cleanup enabled (age=${config.maxAgeMs ?? "\u2014"}, count=${config.maxCount ?? "\u2014"}, interval=${config.intervalMs}ms)`,
|
|
13499
|
+
metadata: {
|
|
13500
|
+
outcome: "armed",
|
|
13501
|
+
interval_ms: config.intervalMs,
|
|
13502
|
+
max_age_ms: config.maxAgeMs ?? null,
|
|
13503
|
+
max_count: config.maxCount ?? null
|
|
13504
|
+
}
|
|
12740
13505
|
});
|
|
12741
13506
|
const interval = setInterval(() => void runSweep(state, driver, config), config.intervalMs);
|
|
12742
13507
|
const firstSweep = setTimeout(
|
|
@@ -12915,7 +13680,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
12915
13680
|
});
|
|
12916
13681
|
return;
|
|
12917
13682
|
}
|
|
12918
|
-
const { collect, stop } = createResourceUsageCollector(
|
|
13683
|
+
const { collect, stop } = createResourceUsageCollector(homedir7());
|
|
12919
13684
|
state.stopResourceUsageSampling = stop;
|
|
12920
13685
|
let consecutiveFailures = 0;
|
|
12921
13686
|
const tick = async () => {
|
|
@@ -12928,6 +13693,19 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
12928
13693
|
message: `Resource usage collection: ${warning2}`
|
|
12929
13694
|
});
|
|
12930
13695
|
}
|
|
13696
|
+
const lowDiskDecision = evaluateLowDiskWarning({
|
|
13697
|
+
...usage,
|
|
13698
|
+
nowMs: Date.now(),
|
|
13699
|
+
state: state.lowDiskWarningState
|
|
13700
|
+
});
|
|
13701
|
+
state.lowDiskWarningState = lowDiskDecision.state;
|
|
13702
|
+
if (lowDiskDecision.warning !== null) {
|
|
13703
|
+
logActivity(state, {
|
|
13704
|
+
type: "info",
|
|
13705
|
+
level: "warn",
|
|
13706
|
+
message: lowDiskDecision.warning
|
|
13707
|
+
});
|
|
13708
|
+
}
|
|
12931
13709
|
const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
|
|
12932
13710
|
if (result.ok) {
|
|
12933
13711
|
if (consecutiveFailures > 0) {
|
|
@@ -13140,7 +13918,7 @@ async function run(options) {
|
|
|
13140
13918
|
let fileSyncDirectories;
|
|
13141
13919
|
try {
|
|
13142
13920
|
logLevel = resolveLogLevel(options);
|
|
13143
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
13921
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir7());
|
|
13144
13922
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
13145
13923
|
throw new Error(
|
|
13146
13924
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -13174,6 +13952,7 @@ async function run(options) {
|
|
|
13174
13952
|
opencodeClient: null,
|
|
13175
13953
|
opencodeV2CredentialImportGuard: { inFlight: false },
|
|
13176
13954
|
sessionDbProvenanceAnomaly: false,
|
|
13955
|
+
sessionStoreHealth: createSessionStoreHealth(),
|
|
13177
13956
|
opencodeProcess: null,
|
|
13178
13957
|
stopOpenCodeLogTail: null,
|
|
13179
13958
|
litestreamProcess: null,
|
|
@@ -13191,10 +13970,12 @@ async function run(options) {
|
|
|
13191
13970
|
openaiUsageRearm: null,
|
|
13192
13971
|
resourceUsageTimer: null,
|
|
13193
13972
|
stopResourceUsageSampling: null,
|
|
13973
|
+
lowDiskWarningState: { worstBand: null, warnedAtMs: null },
|
|
13194
13974
|
credentialSync: null,
|
|
13195
13975
|
authHeader: ""
|
|
13196
13976
|
};
|
|
13197
13977
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
13978
|
+
setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
|
|
13198
13979
|
if (options.credentialSyncMarker) {
|
|
13199
13980
|
state.credentialSync = createCredentialSync({
|
|
13200
13981
|
markerPath: options.credentialSyncMarker,
|
|
@@ -13380,13 +14161,21 @@ async function run(options) {
|
|
|
13380
14161
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
13381
14162
|
}
|
|
13382
14163
|
state.credentialSync?.arm();
|
|
13383
|
-
|
|
13384
|
-
|
|
13385
|
-
createOpenCodeActivityForwarder(() => ({
|
|
14164
|
+
const openCodeActivityForwarder = createOpenCodeActivityForwarder(
|
|
14165
|
+
() => ({
|
|
13386
14166
|
agentId: state.agentId,
|
|
13387
14167
|
authHeader: state.authHeader
|
|
13388
|
-
})
|
|
13389
|
-
|
|
14168
|
+
}),
|
|
14169
|
+
state.sessionStoreHealth.observeUnwritable
|
|
14170
|
+
);
|
|
14171
|
+
const openCodeLogTail = tailOpenCodeLogFile(
|
|
14172
|
+
resolveOpenCodeLogPath(homedir7(), process.env),
|
|
14173
|
+
openCodeActivityForwarder
|
|
14174
|
+
);
|
|
14175
|
+
state.stopOpenCodeLogTail = () => {
|
|
14176
|
+
openCodeActivityForwarder.flush();
|
|
14177
|
+
openCodeLogTail.stop();
|
|
14178
|
+
};
|
|
13390
14179
|
let sessionDbVerifyFatal = false;
|
|
13391
14180
|
if (!options.restoreSessionDb) {
|
|
13392
14181
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -13503,7 +14292,7 @@ async function run(options) {
|
|
|
13503
14292
|
const provenance = checkSessionDbProvenance({
|
|
13504
14293
|
dbPath: sessionDbPath(),
|
|
13505
14294
|
currentVersion: state.opencodeVersion,
|
|
13506
|
-
homeDir:
|
|
14295
|
+
homeDir: homedir7(),
|
|
13507
14296
|
env: process.env
|
|
13508
14297
|
});
|
|
13509
14298
|
if (provenance.anomaly) {
|
|
@@ -13670,10 +14459,11 @@ async function run(options) {
|
|
|
13670
14459
|
getAuthHeader: () => state.authHeader,
|
|
13671
14460
|
conversationFilter: state.conversationFilter,
|
|
13672
14461
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
14462
|
+
sessionStoreHealth: state.sessionStoreHealth,
|
|
13673
14463
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
13674
14464
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
13675
14465
|
fileSyncDirectories,
|
|
13676
|
-
homeDir:
|
|
14466
|
+
homeDir: homedir7(),
|
|
13677
14467
|
maxActiveSessions,
|
|
13678
14468
|
log: (entry) => (
|
|
13679
14469
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -13695,12 +14485,13 @@ async function run(options) {
|
|
|
13695
14485
|
openCodePassword: state.opencodePassword,
|
|
13696
14486
|
isRunning: () => state.running,
|
|
13697
14487
|
events: {
|
|
13698
|
-
onConnected: (agentId, isReconnect) => {
|
|
14488
|
+
onConnected: (agentId, isReconnect, transition) => {
|
|
13699
14489
|
state.connected = true;
|
|
13700
14490
|
state.agentId = agentId;
|
|
14491
|
+
const connectionMessage = isReconnect ? `Tunnel reconnected (runner: ${agentId}, generation: ${transition.generation}, attempt: ${transition.attempt}, offline for ${transition.downtimeMs}ms)` : `Tunnel connected (runner: ${agentId}, generation: ${transition.generation})`;
|
|
13701
14492
|
logActivity(state, {
|
|
13702
14493
|
type: "info",
|
|
13703
|
-
message:
|
|
14494
|
+
message: connectionMessage
|
|
13704
14495
|
});
|
|
13705
14496
|
if (options.tunnelReadyFile) {
|
|
13706
14497
|
const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
|
|
@@ -13721,7 +14512,12 @@ async function run(options) {
|
|
|
13721
14512
|
version: state.opencodeVersion,
|
|
13722
14513
|
major: state.opencodeApiVersion,
|
|
13723
14514
|
connected: state.opencodeConnected
|
|
13724
|
-
})
|
|
14515
|
+
}),
|
|
14516
|
+
connection_generation: transition.generation,
|
|
14517
|
+
...isReconnect && transition.downtimeMs !== null ? {
|
|
14518
|
+
reconnect_attempt: transition.attempt,
|
|
14519
|
+
downtime_ms: transition.downtimeMs
|
|
14520
|
+
} : {}
|
|
13725
14521
|
});
|
|
13726
14522
|
if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
|
|
13727
14523
|
if (state.interactive) displayStatus(state);
|
|
@@ -13743,13 +14539,18 @@ async function run(options) {
|
|
|
13743
14539
|
if (state.interactive) displayStatus(state);
|
|
13744
14540
|
});
|
|
13745
14541
|
},
|
|
13746
|
-
onDisconnected: (code, reason) => {
|
|
14542
|
+
onDisconnected: (code, reason, transition) => {
|
|
13747
14543
|
state.connected = false;
|
|
14544
|
+
const safeReason = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
|
|
13748
14545
|
logActivity(state, {
|
|
13749
14546
|
type: "info",
|
|
13750
|
-
message: `Tunnel disconnected (code: ${code}, reason: ${
|
|
14547
|
+
message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason})`
|
|
14548
|
+
});
|
|
14549
|
+
emitAgentDisconnected(state.agentId, {
|
|
14550
|
+
code,
|
|
14551
|
+
reason: safeReason,
|
|
14552
|
+
connection_generation: transition.generation
|
|
13751
14553
|
});
|
|
13752
|
-
emitAgentDisconnected(state.agentId, { code, reason });
|
|
13753
14554
|
if (state.interactive) displayStatus(state);
|
|
13754
14555
|
},
|
|
13755
14556
|
onError: (error2) => {
|
|
@@ -13849,7 +14650,7 @@ async function run(options) {
|
|
|
13849
14650
|
},
|
|
13850
14651
|
report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
|
|
13851
14652
|
isLocalCredentialProblem: isLocalCredentialProblem2,
|
|
13852
|
-
forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
14653
|
+
forcedOnHint: state.opencodeApiVersion === "v2" ? "OpenCode V2 keeps its connected ChatGPT login in a store Evident cannot read" : "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
13853
14654
|
firstDelayMs: firstReportDelayMs,
|
|
13854
14655
|
nextDelayMs: usageReportDelayMs,
|
|
13855
14656
|
failureLogLevel: usageReportFailureLogLevel
|
|
@@ -13892,7 +14693,7 @@ async function run(options) {
|
|
|
13892
14693
|
}
|
|
13893
14694
|
|
|
13894
14695
|
// src/index.ts
|
|
13895
|
-
var
|
|
14696
|
+
var version = getCliVersion();
|
|
13896
14697
|
var program = new Command();
|
|
13897
14698
|
program.name("evident").description("Run OpenCode locally and connect it to Evident").version(version).option(
|
|
13898
14699
|
"--endpoint <url>",
|