@evident-ai/cli 3.5.2-dev.2828873 → 3.5.2-dev.2bb04b2
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 +924 -292
- 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
|
|
@@ -1070,6 +1069,76 @@ import chalk7 from "chalk";
|
|
|
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.2bb04b2" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1162
1231
|
function getCliVersion() {
|
|
1163
1232
|
return CLI_VERSION;
|
|
1164
1233
|
}
|
|
@@ -1321,6 +1390,219 @@ var EventTypes = {
|
|
|
1321
1390
|
|
|
1322
1391
|
// src/lib/runner-activity-telemetry.ts
|
|
1323
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
|
|
1324
1606
|
var FORWARDED_LEVELS_BY_SOURCE = {
|
|
1325
1607
|
"cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
|
|
1326
1608
|
opencode: /* @__PURE__ */ new Set(["warn", "error"]),
|
|
@@ -1337,11 +1619,13 @@ function parseOpenCodeLogLine(line) {
|
|
|
1337
1619
|
if (!levelMatch) return null;
|
|
1338
1620
|
const level = levelMatch[1].toUpperCase();
|
|
1339
1621
|
if (level !== "WARN" && level !== "ERROR") return null;
|
|
1340
|
-
const sessionMatch = normalisedLine.match(/(?:^|\s)sessionID=(\S+)/);
|
|
1622
|
+
const sessionMatch = normalisedLine.match(/(?:^|\s)(?:sessionID|session\.id)=(\S+)/);
|
|
1623
|
+
const messageIDMatch = normalisedLine.match(/(?:^|\s)messageID=(\S+)/);
|
|
1341
1624
|
const messageMatch = normalisedLine.match(/(?:^|\s)message=(?:"((?:\\.|[^"])*)"|(\S+))/);
|
|
1342
1625
|
const parsed = {
|
|
1343
1626
|
level: level === "WARN" ? "warn" : "error",
|
|
1344
|
-
sessionID: sessionMatch?.[1]
|
|
1627
|
+
sessionID: sessionMatch?.[1],
|
|
1628
|
+
messageID: messageIDMatch?.[1]
|
|
1345
1629
|
};
|
|
1346
1630
|
const message = messageMatch?.[1] ?? messageMatch?.[2];
|
|
1347
1631
|
if (message !== void 0) parsed.message = message;
|
|
@@ -1349,12 +1633,13 @@ function parseOpenCodeLogLine(line) {
|
|
|
1349
1633
|
}
|
|
1350
1634
|
var MAX_LINE_BUFFER_BYTES = 16 * 1024;
|
|
1351
1635
|
var REPEAT_SUPPRESSION_WINDOW_MS = 6e4;
|
|
1352
|
-
function createOpenCodeActivityForwarder(getContext) {
|
|
1636
|
+
function createOpenCodeActivityForwarder(getContext, onSessionStoreWriteFailure) {
|
|
1353
1637
|
let buffer = Buffer.alloc(0);
|
|
1354
1638
|
const repeatStates = /* @__PURE__ */ new Map();
|
|
1355
1639
|
const forwardLine = (line, parsed, repeatsSuppressed = 0) => {
|
|
1356
1640
|
const metadata = {
|
|
1357
1641
|
...parsed.sessionID ? { sessionID: parsed.sessionID } : {},
|
|
1642
|
+
...parsed.messageID ? { messageID: parsed.messageID } : {},
|
|
1358
1643
|
...repeatsSuppressed > 0 ? { repeats_suppressed: repeatsSuppressed } : {}
|
|
1359
1644
|
};
|
|
1360
1645
|
forwardRunnerActivity(
|
|
@@ -1379,10 +1664,19 @@ function createOpenCodeActivityForwarder(getContext) {
|
|
|
1379
1664
|
}
|
|
1380
1665
|
};
|
|
1381
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
|
+
}
|
|
1382
1676
|
const parsed = parseOpenCodeLogLine(line);
|
|
1383
1677
|
if (!parsed) return;
|
|
1384
1678
|
const now = Date.now();
|
|
1385
|
-
const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}`;
|
|
1679
|
+
const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}\0${parsed.sessionID ?? ""}\0${parsed.messageID ?? ""}`;
|
|
1386
1680
|
expireRepeatStates(now, key);
|
|
1387
1681
|
if (key === void 0) {
|
|
1388
1682
|
forwardLine(line, parsed);
|
|
@@ -1449,7 +1743,7 @@ function redactHomeDirectory(message) {
|
|
|
1449
1743
|
}
|
|
1450
1744
|
return result + message.slice(searchFrom);
|
|
1451
1745
|
}
|
|
1452
|
-
function
|
|
1746
|
+
function redactDiagnosticText(message) {
|
|
1453
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>");
|
|
1454
1748
|
return redactHomeDirectory(redacted);
|
|
1455
1749
|
}
|
|
@@ -1468,7 +1762,10 @@ function sanitiseMetadata(metadata) {
|
|
|
1468
1762
|
const sanitised = [];
|
|
1469
1763
|
for (const [key, value] of entries) {
|
|
1470
1764
|
if (typeof value === "string") {
|
|
1471
|
-
sanitised.push([
|
|
1765
|
+
sanitised.push([
|
|
1766
|
+
key,
|
|
1767
|
+
truncate(redactDiagnosticText(value).slice(0, MAX_METADATA_VALUE_LENGTH))
|
|
1768
|
+
]);
|
|
1472
1769
|
} else if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
|
|
1473
1770
|
sanitised.push([key, value]);
|
|
1474
1771
|
}
|
|
@@ -1513,7 +1810,7 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1513
1810
|
if (!context.agentId || !context.authHeader) return;
|
|
1514
1811
|
if (!admitUnderRateLimit(source, Date.now())) return;
|
|
1515
1812
|
const rawMessage = entry.error ?? entry.message ?? "";
|
|
1516
|
-
const message = truncate(
|
|
1813
|
+
const message = truncate(redactDiagnosticText(rawMessage));
|
|
1517
1814
|
logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
|
|
1518
1815
|
severity: SEVERITY_BY_LEVEL[entry.level],
|
|
1519
1816
|
message,
|
|
@@ -1812,8 +2109,46 @@ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
|
|
|
1812
2109
|
|
|
1813
2110
|
// src/lib/http-timeout.ts
|
|
1814
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
|
+
}
|
|
1815
2136
|
function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
1816
|
-
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
|
+
});
|
|
1817
2152
|
}
|
|
1818
2153
|
|
|
1819
2154
|
// src/lib/opencode/client.ts
|
|
@@ -1879,7 +2214,7 @@ async function getOpenCodeDirectory(port, client) {
|
|
|
1879
2214
|
return null;
|
|
1880
2215
|
}
|
|
1881
2216
|
}
|
|
1882
|
-
function
|
|
2217
|
+
function roleOf2(m) {
|
|
1883
2218
|
if (!m || typeof m !== "object") return void 0;
|
|
1884
2219
|
if (typeof m.role === "string") return m.role;
|
|
1885
2220
|
const infoRole = m.info?.role;
|
|
@@ -1893,13 +2228,13 @@ function createdOf(m) {
|
|
|
1893
2228
|
if (!m || typeof m !== "object") return void 0;
|
|
1894
2229
|
return m.info?.time?.created ?? m.time?.created;
|
|
1895
2230
|
}
|
|
1896
|
-
function
|
|
2231
|
+
function idOf2(m) {
|
|
1897
2232
|
if (!m || typeof m !== "object") return void 0;
|
|
1898
2233
|
if (typeof m.id === "string") return m.id;
|
|
1899
2234
|
const infoId = m.info?.id;
|
|
1900
2235
|
return typeof infoId === "string" ? infoId : void 0;
|
|
1901
2236
|
}
|
|
1902
|
-
function
|
|
2237
|
+
function parentIdOf2(m) {
|
|
1903
2238
|
if (!m || typeof m !== "object") return void 0;
|
|
1904
2239
|
if (typeof m.parentID === "string") return m.parentID;
|
|
1905
2240
|
const infoParent = m.info?.parentID;
|
|
@@ -1964,9 +2299,10 @@ async function pollSessionMessagesForRedrive(port, sessionId, client) {
|
|
|
1964
2299
|
function isSessionActivelyGenerating(messages) {
|
|
1965
2300
|
if (!messages || messages.length === 0) return false;
|
|
1966
2301
|
const last = messages[messages.length - 1];
|
|
1967
|
-
if (
|
|
2302
|
+
if (roleOf2(last) !== "assistant") return false;
|
|
1968
2303
|
return completedOf(last) == null;
|
|
1969
2304
|
}
|
|
2305
|
+
var FULL_SESSION_ENUMERATION_MAX_SESSIONS = 3200;
|
|
1970
2306
|
function sessionLastActivityMs(session) {
|
|
1971
2307
|
const candidates = [
|
|
1972
2308
|
session.time?.updated,
|
|
@@ -1981,30 +2317,60 @@ function sessionLastActivityMs(session) {
|
|
|
1981
2317
|
}
|
|
1982
2318
|
return null;
|
|
1983
2319
|
}
|
|
1984
|
-
|
|
1985
|
-
|
|
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);
|
|
1986
2328
|
try {
|
|
1987
|
-
const res = await requestWithClient(port, client,
|
|
2329
|
+
const res = await requestWithClient(port, client, path);
|
|
1988
2330
|
if (!res.ok) return null;
|
|
1989
2331
|
const body = await res.json();
|
|
1990
2332
|
return Array.isArray(body) ? body : null;
|
|
1991
2333
|
} catch (error2) {
|
|
1992
2334
|
console.error(
|
|
1993
|
-
`[
|
|
2335
|
+
`[listSessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1994
2336
|
);
|
|
1995
2337
|
return null;
|
|
1996
2338
|
}
|
|
1997
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
|
+
}
|
|
1998
2358
|
async function deleteSession(port, id, client) {
|
|
1999
2359
|
if (client?.version === "v2") return deleteV2Session(client, id);
|
|
2000
2360
|
try {
|
|
2001
|
-
const res = await requestWithClient(
|
|
2002
|
-
|
|
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 };
|
|
2003
2369
|
} catch (error2) {
|
|
2004
2370
|
console.error(
|
|
2005
2371
|
`[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2006
2372
|
);
|
|
2007
|
-
return false;
|
|
2373
|
+
return { ok: false, status: "threw" };
|
|
2008
2374
|
}
|
|
2009
2375
|
}
|
|
2010
2376
|
async function sessionExists(port, id, client) {
|
|
@@ -2245,7 +2611,7 @@ function messageText(m) {
|
|
|
2245
2611
|
async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
|
|
2246
2612
|
const before = await getSessionMessages(port, sessionId, client);
|
|
2247
2613
|
const knownUserIds = new Set(
|
|
2248
|
-
(before ?? []).filter((m) =>
|
|
2614
|
+
(before ?? []).filter((m) => roleOf2(m) === "user").map((m) => idOf2(m)).filter((id) => typeof id === "string")
|
|
2249
2615
|
);
|
|
2250
2616
|
const parts = [{ type: "text", text: content }];
|
|
2251
2617
|
let pendingOutcomes = null;
|
|
@@ -2288,8 +2654,8 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
|
|
|
2288
2654
|
if (after) {
|
|
2289
2655
|
let best = null;
|
|
2290
2656
|
for (const m of after) {
|
|
2291
|
-
if (
|
|
2292
|
-
const id =
|
|
2657
|
+
if (roleOf2(m) !== "user") continue;
|
|
2658
|
+
const id = idOf2(m);
|
|
2293
2659
|
if (typeof id !== "string" || knownUserIds.has(id)) continue;
|
|
2294
2660
|
if (messageText(m) !== content) continue;
|
|
2295
2661
|
const created = createdOf(m) ?? 0;
|
|
@@ -2311,13 +2677,13 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
|
|
|
2311
2677
|
function findAssistantReplyAfter(messages, userMessageId) {
|
|
2312
2678
|
if (!messages || messages.length === 0) return null;
|
|
2313
2679
|
const byParent = messages.find(
|
|
2314
|
-
(m) =>
|
|
2680
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
|
|
2315
2681
|
);
|
|
2316
2682
|
if (byParent) return byParent;
|
|
2317
|
-
const userIndex = messages.findIndex((m) =>
|
|
2683
|
+
const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
|
|
2318
2684
|
if (userIndex === -1) return null;
|
|
2319
2685
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2320
|
-
if (
|
|
2686
|
+
if (roleOf2(messages[i]) === "assistant") return messages[i];
|
|
2321
2687
|
}
|
|
2322
2688
|
return null;
|
|
2323
2689
|
}
|
|
@@ -2327,7 +2693,7 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
2327
2693
|
let lastNonErrored = null;
|
|
2328
2694
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2329
2695
|
const m = messages[i];
|
|
2330
|
-
if (
|
|
2696
|
+
if (roleOf2(m) !== "assistant" || parentIdOf2(m) !== userMessageId) continue;
|
|
2331
2697
|
if (lastCorrelated === null) lastCorrelated = m;
|
|
2332
2698
|
if (errorOf(m) == null) {
|
|
2333
2699
|
lastNonErrored = m;
|
|
@@ -2335,12 +2701,12 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
2335
2701
|
}
|
|
2336
2702
|
}
|
|
2337
2703
|
if (lastCorrelated) return lastNonErrored ?? lastCorrelated;
|
|
2338
|
-
const userIndex = messages.findIndex((m) =>
|
|
2704
|
+
const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
|
|
2339
2705
|
if (userIndex === -1) return null;
|
|
2340
2706
|
let last = null;
|
|
2341
2707
|
let lastOk = null;
|
|
2342
2708
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2343
|
-
const role =
|
|
2709
|
+
const role = roleOf2(messages[i]);
|
|
2344
2710
|
if (role === "user") break;
|
|
2345
2711
|
if (role === "assistant") {
|
|
2346
2712
|
last = messages[i];
|
|
@@ -2352,16 +2718,16 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
2352
2718
|
function collectSubagentSessions(messages, userMessageId) {
|
|
2353
2719
|
if (!messages || messages.length === 0) return [];
|
|
2354
2720
|
const byParent = messages.filter(
|
|
2355
|
-
(message) =>
|
|
2721
|
+
(message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
|
|
2356
2722
|
);
|
|
2357
2723
|
const assistants = byParent.length > 0 ? byParent : [];
|
|
2358
2724
|
if (assistants.length === 0) {
|
|
2359
|
-
const userIndex = messages.findIndex((message) =>
|
|
2725
|
+
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
2360
2726
|
if (userIndex === -1) return [];
|
|
2361
2727
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2362
2728
|
const message = messages[i];
|
|
2363
|
-
if (
|
|
2364
|
-
if (
|
|
2729
|
+
if (roleOf2(message) === "user") break;
|
|
2730
|
+
if (roleOf2(message) === "assistant") assistants.push(message);
|
|
2365
2731
|
}
|
|
2366
2732
|
}
|
|
2367
2733
|
const refs = [];
|
|
@@ -2401,7 +2767,7 @@ function collectTaskCalls(messages, userMessageId) {
|
|
|
2401
2767
|
if (!messages || messages.length === 0) return [];
|
|
2402
2768
|
const calls = [];
|
|
2403
2769
|
for (const message of messages) {
|
|
2404
|
-
if (
|
|
2770
|
+
if (roleOf2(message) !== "assistant" || parentIdOf2(message) !== userMessageId) continue;
|
|
2405
2771
|
for (const part of message.parts ?? []) {
|
|
2406
2772
|
if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
|
|
2407
2773
|
continue;
|
|
@@ -2431,7 +2797,7 @@ function attributeTaskCallUsage(messages, windows) {
|
|
|
2431
2797
|
for (const window of eligibleWindows) assignments.set(window.callID, []);
|
|
2432
2798
|
const unattributed = [];
|
|
2433
2799
|
for (const message of messages ?? []) {
|
|
2434
|
-
if (
|
|
2800
|
+
if (roleOf2(message) !== "assistant") continue;
|
|
2435
2801
|
const created = finiteNumber(createdOf(message));
|
|
2436
2802
|
const matching = created === null ? [] : eligibleWindows.filter(
|
|
2437
2803
|
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
@@ -2508,7 +2874,7 @@ function sumAssistantUsage(messages) {
|
|
|
2508
2874
|
function messageUsage(messages, userMessageId) {
|
|
2509
2875
|
if (!messages || messages.length === 0) return null;
|
|
2510
2876
|
const byParentAll = messages.filter(
|
|
2511
|
-
(m) =>
|
|
2877
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
|
|
2512
2878
|
);
|
|
2513
2879
|
const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
|
|
2514
2880
|
const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
|
|
@@ -2524,7 +2890,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
2524
2890
|
}
|
|
2525
2891
|
function messageRunState(messages, userMessageId) {
|
|
2526
2892
|
if (!messages || messages.length === 0) return "unknown";
|
|
2527
|
-
const hasUser = messages.some((m) =>
|
|
2893
|
+
const hasUser = messages.some((m) => idOf2(m) === userMessageId);
|
|
2528
2894
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
2529
2895
|
if (!hasUser) {
|
|
2530
2896
|
if (!reply) return "unknown";
|
|
@@ -2616,7 +2982,7 @@ function findLatestSubagentAuthOutcome(messages, sinceMs) {
|
|
|
2616
2982
|
if (!messages || messages.length === 0) return null;
|
|
2617
2983
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2618
2984
|
const message = messages[i];
|
|
2619
|
-
if (
|
|
2985
|
+
if (roleOf2(message) !== "assistant") continue;
|
|
2620
2986
|
const created = createdOf(message);
|
|
2621
2987
|
if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
|
|
2622
2988
|
const failure = classifyReplyAuthError(message);
|
|
@@ -2648,25 +3014,25 @@ function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProvi
|
|
|
2648
3014
|
function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
2649
3015
|
if (!messages || messages.length === 0) return false;
|
|
2650
3016
|
return messages.some(
|
|
2651
|
-
(m) =>
|
|
3017
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
2652
3018
|
);
|
|
2653
3019
|
}
|
|
2654
3020
|
function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
|
|
2655
3021
|
if (!messages || messages.length === 0) return false;
|
|
2656
|
-
const userIndex = messages.findIndex((message) =>
|
|
3022
|
+
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
2657
3023
|
if (userIndex === -1) return false;
|
|
2658
3024
|
let hasLaterUser = false;
|
|
2659
3025
|
let hasStartedLaterUser = false;
|
|
2660
3026
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2661
3027
|
const message = messages[i];
|
|
2662
|
-
if (
|
|
3028
|
+
if (roleOf2(message) !== "user") continue;
|
|
2663
3029
|
hasLaterUser = true;
|
|
2664
|
-
const laterUserMessageId =
|
|
3030
|
+
const laterUserMessageId = idOf2(message);
|
|
2665
3031
|
if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
|
|
2666
3032
|
return false;
|
|
2667
3033
|
}
|
|
2668
3034
|
if (messages.some(
|
|
2669
|
-
(candidate) =>
|
|
3035
|
+
(candidate) => roleOf2(candidate) === "assistant" && parentIdOf2(candidate) === laterUserMessageId
|
|
2670
3036
|
)) {
|
|
2671
3037
|
hasStartedLaterUser = true;
|
|
2672
3038
|
}
|
|
@@ -3137,13 +3503,17 @@ async function listV2Sessions(client) {
|
|
|
3137
3503
|
}
|
|
3138
3504
|
async function deleteV2Session(client, sessionId) {
|
|
3139
3505
|
try {
|
|
3140
|
-
await client.request(
|
|
3141
|
-
|
|
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 };
|
|
3142
3512
|
} catch (error2) {
|
|
3143
3513
|
console.error(
|
|
3144
3514
|
`[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3145
3515
|
);
|
|
3146
|
-
return false;
|
|
3516
|
+
return { ok: false, status: "threw" };
|
|
3147
3517
|
}
|
|
3148
3518
|
}
|
|
3149
3519
|
async function v2SessionExists(client, sessionId) {
|
|
@@ -3765,9 +4135,9 @@ async function importOpenCodeV2Credentials({
|
|
|
3765
4135
|
|
|
3766
4136
|
// src/lib/opencode/session-db-boot.ts
|
|
3767
4137
|
import { spawn as spawn2 } from "node:child_process";
|
|
3768
|
-
import { mkdirSync, statSync as
|
|
4138
|
+
import { mkdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
|
|
3769
4139
|
import { homedir as homedir4 } from "node:os";
|
|
3770
|
-
import { dirname as
|
|
4140
|
+
import { dirname as dirname3, resolve as resolvePath } from "node:path";
|
|
3771
4141
|
|
|
3772
4142
|
// src/lib/runner-synchroniser.ts
|
|
3773
4143
|
import { spawn } from "node:child_process";
|
|
@@ -3892,7 +4262,7 @@ function clearMarker(options) {
|
|
|
3892
4262
|
function markNoReplicate(options, message) {
|
|
3893
4263
|
if (options.noReplicateMarker) {
|
|
3894
4264
|
try {
|
|
3895
|
-
mkdirSync(
|
|
4265
|
+
mkdirSync(dirname3(options.noReplicateMarker), { recursive: true });
|
|
3896
4266
|
writeFileSync(options.noReplicateMarker, "");
|
|
3897
4267
|
} catch (error2) {
|
|
3898
4268
|
options.log(
|
|
@@ -4017,7 +4387,7 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
4017
4387
|
return null;
|
|
4018
4388
|
}
|
|
4019
4389
|
try {
|
|
4020
|
-
if (
|
|
4390
|
+
if (statSync3(configPath).size > 0) return configPath;
|
|
4021
4391
|
} catch (error2) {
|
|
4022
4392
|
if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")) {
|
|
4023
4393
|
options.log(
|
|
@@ -4047,7 +4417,7 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
4047
4417
|
return null;
|
|
4048
4418
|
}
|
|
4049
4419
|
try {
|
|
4050
|
-
mkdirSync(
|
|
4420
|
+
mkdirSync(dirname3(configPath), { recursive: true });
|
|
4051
4421
|
writeFileSync(configPath, rendered.stdout);
|
|
4052
4422
|
} catch (error2) {
|
|
4053
4423
|
options.log(
|
|
@@ -4208,7 +4578,7 @@ async function verifySessionDb(options, configPath, env) {
|
|
|
4208
4578
|
}
|
|
4209
4579
|
function fileExists(path) {
|
|
4210
4580
|
try {
|
|
4211
|
-
|
|
4581
|
+
statSync3(path);
|
|
4212
4582
|
return true;
|
|
4213
4583
|
} catch (error2) {
|
|
4214
4584
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return false;
|
|
@@ -4297,7 +4667,7 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
4297
4667
|
// src/lib/opencode/session-db-provenance.ts
|
|
4298
4668
|
import { createRequire } from "node:module";
|
|
4299
4669
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4300
|
-
import { dirname as
|
|
4670
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
4301
4671
|
var require2 = createRequire(import.meta.url);
|
|
4302
4672
|
function readSessionDbMigrationIds(dbPath) {
|
|
4303
4673
|
let db;
|
|
@@ -4363,7 +4733,7 @@ function loadSessionDbProvenanceState(path) {
|
|
|
4363
4733
|
}
|
|
4364
4734
|
function saveSessionDbProvenanceState(path, state) {
|
|
4365
4735
|
try {
|
|
4366
|
-
mkdirSync2(
|
|
4736
|
+
mkdirSync2(dirname4(path), { recursive: true });
|
|
4367
4737
|
writeFileSync2(path, `${JSON.stringify(state, null, 2)}
|
|
4368
4738
|
`, "utf8");
|
|
4369
4739
|
} catch (error2) {
|
|
@@ -4944,7 +5314,10 @@ function selectSessionsToDelete(sessions, opts) {
|
|
|
4944
5314
|
toDelete.push(s.id);
|
|
4945
5315
|
}
|
|
4946
5316
|
}
|
|
4947
|
-
|
|
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
|
+
);
|
|
4948
5321
|
}
|
|
4949
5322
|
var DEFAULT_INTERVAL = "1h";
|
|
4950
5323
|
function resolve(flag, envValue, fallback) {
|
|
@@ -5004,13 +5377,13 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
5004
5377
|
}
|
|
5005
5378
|
|
|
5006
5379
|
// src/lib/opencode/session-db-size.ts
|
|
5007
|
-
import { statSync as
|
|
5380
|
+
import { statSync as statSync4 } from "node:fs";
|
|
5008
5381
|
import { join as join6 } from "node:path";
|
|
5009
5382
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
5010
5383
|
function statSessionDbBytes(homeDir) {
|
|
5011
5384
|
const dbPath = join6(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
5012
5385
|
try {
|
|
5013
|
-
return
|
|
5386
|
+
return statSync4(dbPath).size;
|
|
5014
5387
|
} catch (err) {
|
|
5015
5388
|
const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
5016
5389
|
if (!isMissingFile) {
|
|
@@ -5036,7 +5409,7 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
5036
5409
|
}
|
|
5037
5410
|
|
|
5038
5411
|
// src/lib/opencode/log-tail.ts
|
|
5039
|
-
import { statSync as
|
|
5412
|
+
import { statSync as statSync5 } from "node:fs";
|
|
5040
5413
|
import { homedir as homedir5 } from "node:os";
|
|
5041
5414
|
import { join as join7 } from "node:path";
|
|
5042
5415
|
import { open as open2, stat } from "node:fs/promises";
|
|
@@ -5059,7 +5432,7 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
|
5059
5432
|
let inode = null;
|
|
5060
5433
|
let baselineReady = true;
|
|
5061
5434
|
try {
|
|
5062
|
-
const initial =
|
|
5435
|
+
const initial = statSync5(logPath);
|
|
5063
5436
|
offset = initial.size;
|
|
5064
5437
|
inode = initial.ino;
|
|
5065
5438
|
} catch (error2) {
|
|
@@ -5072,173 +5445,50 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
|
5072
5445
|
let stopped = false;
|
|
5073
5446
|
const poll = async () => {
|
|
5074
5447
|
if (polling || stopped) return;
|
|
5075
|
-
polling = true;
|
|
5076
|
-
try {
|
|
5077
|
-
let current;
|
|
5078
|
-
try {
|
|
5079
|
-
current = await stat(logPath);
|
|
5080
|
-
} catch (error2) {
|
|
5081
|
-
if (!isEnoent(error2)) reportFailure("stat", logPath, error2);
|
|
5082
|
-
return;
|
|
5083
|
-
}
|
|
5084
|
-
if (!baselineReady) {
|
|
5085
|
-
offset = current.size;
|
|
5086
|
-
inode = current.ino;
|
|
5087
|
-
baselineReady = true;
|
|
5088
|
-
return;
|
|
5089
|
-
}
|
|
5090
|
-
if (inode !== null && current.ino !== inode || current.size < offset) {
|
|
5091
|
-
offset = 0;
|
|
5092
|
-
}
|
|
5093
|
-
inode = current.ino;
|
|
5094
|
-
if (current.size === offset) return;
|
|
5095
|
-
const length = current.size - offset;
|
|
5096
|
-
const fh = await open2(logPath, "r");
|
|
5097
|
-
try {
|
|
5098
|
-
const buf = Buffer.alloc(length);
|
|
5099
|
-
const { bytesRead } = await fh.read(buf, 0, length, offset);
|
|
5100
|
-
offset += bytesRead;
|
|
5101
|
-
if (bytesRead > 0) onChunk(buf.subarray(0, bytesRead));
|
|
5102
|
-
} finally {
|
|
5103
|
-
await fh.close();
|
|
5104
|
-
}
|
|
5105
|
-
} catch (error2) {
|
|
5106
|
-
if (!isEnoent(error2)) reportFailure("poll", logPath, error2);
|
|
5107
|
-
} finally {
|
|
5108
|
-
polling = false;
|
|
5109
|
-
}
|
|
5110
|
-
};
|
|
5111
|
-
const interval = setInterval(() => void poll(), pollIntervalMs);
|
|
5112
|
-
void poll();
|
|
5113
|
-
return {
|
|
5114
|
-
stop: () => {
|
|
5115
|
-
stopped = true;
|
|
5116
|
-
clearInterval(interval);
|
|
5117
|
-
}
|
|
5118
|
-
};
|
|
5119
|
-
}
|
|
5120
|
-
|
|
5121
|
-
// src/lib/opencode/session-db-reclaim.ts
|
|
5122
|
-
import { statSync as statSync5, statfsSync } from "node:fs";
|
|
5123
|
-
import { dirname as dirname4 } from "node:path";
|
|
5124
|
-
function errorMessage(error2) {
|
|
5125
|
-
if (!(error2 instanceof Error)) return String(error2);
|
|
5126
|
-
return error2.cause instanceof Error ? error2.cause.message : error2.message;
|
|
5127
|
-
}
|
|
5128
|
-
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
5129
|
-
try {
|
|
5130
|
-
const fsStats = statfsSync(dirname4(dbPath));
|
|
5131
|
-
const availableBytes = fsStats.bavail * fsStats.bsize;
|
|
5132
|
-
if (availableBytes < requiredBytes) {
|
|
5133
|
-
return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
|
|
5134
|
-
}
|
|
5135
|
-
return null;
|
|
5136
|
-
} catch (err) {
|
|
5137
|
-
return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
|
|
5138
|
-
}
|
|
5139
|
-
}
|
|
5140
|
-
function readLogicalBytes(db) {
|
|
5141
|
-
const pageCount = db.prepare("PRAGMA page_count").get().page_count;
|
|
5142
|
-
const pageSize = db.prepare("PRAGMA page_size").get().page_size;
|
|
5143
|
-
return pageCount * pageSize;
|
|
5144
|
-
}
|
|
5145
|
-
function readCheckpointResult(db) {
|
|
5146
|
-
const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
5147
|
-
return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
|
|
5148
|
-
}
|
|
5149
|
-
async function probeReclaimAvailability(input) {
|
|
5150
|
-
const { dbPath, requiredBytes } = input;
|
|
5151
|
-
let sqlite;
|
|
5152
|
-
try {
|
|
5153
|
-
sqlite = await import("node:sqlite");
|
|
5154
|
-
} catch (err) {
|
|
5155
|
-
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
5156
|
-
console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
|
|
5157
|
-
return { reason: "sqlite-unavailable", detail };
|
|
5158
|
-
}
|
|
5159
|
-
let autoVacuum = null;
|
|
5160
|
-
try {
|
|
5161
|
-
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
5162
|
-
try {
|
|
5163
|
-
db.exec("PRAGMA busy_timeout=5000");
|
|
5164
|
-
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
5165
|
-
} finally {
|
|
5166
|
-
db.close();
|
|
5167
|
-
}
|
|
5168
|
-
} catch (err) {
|
|
5169
|
-
console.warn(
|
|
5170
|
-
`[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
5171
|
-
);
|
|
5172
|
-
}
|
|
5173
|
-
if (autoVacuum !== 0) return null;
|
|
5174
|
-
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
5175
|
-
}
|
|
5176
|
-
async function reclaimSessionDbSpace(input) {
|
|
5177
|
-
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
5178
|
-
let sqlite;
|
|
5179
|
-
try {
|
|
5180
|
-
sqlite = await import("node:sqlite");
|
|
5181
|
-
} catch (err) {
|
|
5182
|
-
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
5183
|
-
console.warn(
|
|
5184
|
-
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
|
|
5185
|
-
);
|
|
5186
|
-
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
5187
|
-
}
|
|
5188
|
-
const { DatabaseSync } = sqlite;
|
|
5189
|
-
let db;
|
|
5190
|
-
try {
|
|
5191
|
-
db = new DatabaseSync(dbPath);
|
|
5192
|
-
db.exec("PRAGMA busy_timeout=5000");
|
|
5193
|
-
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
5194
|
-
if (autoVacuum === 0) {
|
|
5195
|
-
if (!allowFullVacuum) {
|
|
5196
|
-
console.warn(
|
|
5197
|
-
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
|
|
5198
|
-
);
|
|
5199
|
-
return {
|
|
5200
|
-
ok: false,
|
|
5201
|
-
skipped: "full-vacuum-blocked",
|
|
5202
|
-
detail: "a session turn is live"
|
|
5203
|
-
};
|
|
5448
|
+
polling = true;
|
|
5449
|
+
try {
|
|
5450
|
+
let current;
|
|
5451
|
+
try {
|
|
5452
|
+
current = await stat(logPath);
|
|
5453
|
+
} catch (error2) {
|
|
5454
|
+
if (!isEnoent(error2)) reportFailure("stat", logPath, error2);
|
|
5455
|
+
return;
|
|
5204
5456
|
}
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
);
|
|
5211
|
-
return { ok: false, skipped: "insufficient-disk-space", detail: skipReason };
|
|
5457
|
+
if (!baselineReady) {
|
|
5458
|
+
offset = current.size;
|
|
5459
|
+
inode = current.ino;
|
|
5460
|
+
baselineReady = true;
|
|
5461
|
+
return;
|
|
5212
5462
|
}
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5463
|
+
if (inode !== null && current.ino !== inode || current.size < offset) {
|
|
5464
|
+
offset = 0;
|
|
5465
|
+
}
|
|
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;
|
|
5219
5482
|
}
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5483
|
+
};
|
|
5484
|
+
const interval = setInterval(() => void poll(), pollIntervalMs);
|
|
5485
|
+
void poll();
|
|
5486
|
+
return {
|
|
5487
|
+
stop: () => {
|
|
5488
|
+
stopped = true;
|
|
5489
|
+
clearInterval(interval);
|
|
5227
5490
|
}
|
|
5228
|
-
|
|
5229
|
-
`[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
|
|
5230
|
-
);
|
|
5231
|
-
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
5232
|
-
} catch (err) {
|
|
5233
|
-
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
5234
|
-
return {
|
|
5235
|
-
ok: false,
|
|
5236
|
-
skipped: "reclaim-error",
|
|
5237
|
-
detail: errorMessage(err)
|
|
5238
|
-
};
|
|
5239
|
-
} finally {
|
|
5240
|
-
db?.close();
|
|
5241
|
-
}
|
|
5491
|
+
};
|
|
5242
5492
|
}
|
|
5243
5493
|
|
|
5244
5494
|
// src/lib/tunnel/connection.ts
|
|
@@ -5470,6 +5720,8 @@ var StreamForwarder = class {
|
|
|
5470
5720
|
if (done) break;
|
|
5471
5721
|
const chunk = Buffer.from(value);
|
|
5472
5722
|
for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
|
|
5723
|
+
await stream.pausePromise;
|
|
5724
|
+
if (ac.signal.aborted) break;
|
|
5473
5725
|
const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
|
|
5474
5726
|
if (this.binaryFramesSupported) {
|
|
5475
5727
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
@@ -5512,6 +5764,7 @@ function classifyUpgradeRejection(headers) {
|
|
|
5512
5764
|
}
|
|
5513
5765
|
var MAX_RECONNECT_DELAY = 3e4;
|
|
5514
5766
|
var BASE_RECONNECT_DELAY = 500;
|
|
5767
|
+
var RELAY_SILENCE_TIMEOUT_MS = 9e4;
|
|
5515
5768
|
function getReconnectDelay(attempt) {
|
|
5516
5769
|
const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
|
|
5517
5770
|
const jitter = Math.random() * 1e3;
|
|
@@ -5583,10 +5836,27 @@ function connectTunnel(options) {
|
|
|
5583
5836
|
ws.close();
|
|
5584
5837
|
reject(new Error("Connection timeout"));
|
|
5585
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
|
+
};
|
|
5586
5855
|
let upgradeRejection = null;
|
|
5587
5856
|
let upgradeRejectionReason = null;
|
|
5588
5857
|
ws.on("unexpected-response", (_req, res) => {
|
|
5589
5858
|
clearTimeout(connectionTimeout);
|
|
5859
|
+
clearRelaySilenceTimeout();
|
|
5590
5860
|
const reason = classifyUpgradeRejection(res.headers);
|
|
5591
5861
|
upgradeRejectionReason = reason;
|
|
5592
5862
|
const chunks = [];
|
|
@@ -5616,6 +5886,7 @@ function connectTunnel(options) {
|
|
|
5616
5886
|
onInfo?.("WebSocket connection established");
|
|
5617
5887
|
});
|
|
5618
5888
|
ws.on("message", (data, isBinary) => {
|
|
5889
|
+
if (relaySilenceTimeout !== void 0) armRelaySilenceTimeout();
|
|
5619
5890
|
if (isBinary) {
|
|
5620
5891
|
try {
|
|
5621
5892
|
const frame = decodeBinaryBodyFrame(toUint8Array(data));
|
|
@@ -5649,6 +5920,7 @@ function connectTunnel(options) {
|
|
|
5649
5920
|
switch (message.type) {
|
|
5650
5921
|
case "connected": {
|
|
5651
5922
|
clearTimeout(connectionTimeout);
|
|
5923
|
+
armRelaySilenceTimeout();
|
|
5652
5924
|
forwarder.setBinaryFramesSupported(message.binary_frames === true);
|
|
5653
5925
|
const connectedAgentId = message.agent_id ?? agentId;
|
|
5654
5926
|
onConnected?.(connectedAgentId);
|
|
@@ -5673,6 +5945,7 @@ function connectTunnel(options) {
|
|
|
5673
5945
|
});
|
|
5674
5946
|
ws.on("error", (error2) => {
|
|
5675
5947
|
clearTimeout(connectionTimeout);
|
|
5948
|
+
clearRelaySilenceTimeout();
|
|
5676
5949
|
const detail = upgradeRejection ?? describeSocketError(error2, url);
|
|
5677
5950
|
if (upgradeRejectionReason === "do_code_updated") {
|
|
5678
5951
|
onWarning?.("Relay redeployed \u2014 reconnecting");
|
|
@@ -5684,7 +5957,8 @@ function connectTunnel(options) {
|
|
|
5684
5957
|
);
|
|
5685
5958
|
});
|
|
5686
5959
|
ws.on("close", (code, reason) => {
|
|
5687
|
-
|
|
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");
|
|
5688
5962
|
forwarder.abortAll();
|
|
5689
5963
|
onDisconnected?.(code, reasonStr);
|
|
5690
5964
|
});
|
|
@@ -5692,9 +5966,10 @@ function connectTunnel(options) {
|
|
|
5692
5966
|
}
|
|
5693
5967
|
|
|
5694
5968
|
// src/lib/tunnel/runner-connection.ts
|
|
5695
|
-
var RunnerConnection = class {
|
|
5969
|
+
var RunnerConnection = class _RunnerConnection {
|
|
5696
5970
|
opts;
|
|
5697
5971
|
sleep;
|
|
5972
|
+
now;
|
|
5698
5973
|
connection = null;
|
|
5699
5974
|
resolvedAgentId;
|
|
5700
5975
|
/** True while a (re)connect loop is in flight. */
|
|
@@ -5703,10 +5978,17 @@ var RunnerConnection = class {
|
|
|
5703
5978
|
reconnectPromise = null;
|
|
5704
5979
|
/** 1-based count of the current reconnect attempt streak. */
|
|
5705
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
|
+
]);
|
|
5706
5987
|
constructor(opts) {
|
|
5707
5988
|
this.opts = opts;
|
|
5708
5989
|
this.resolvedAgentId = opts.agentId;
|
|
5709
5990
|
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5991
|
+
this.now = opts.now ?? Date.now;
|
|
5710
5992
|
}
|
|
5711
5993
|
get agentId() {
|
|
5712
5994
|
return this.resolvedAgentId;
|
|
@@ -5742,14 +6024,24 @@ var RunnerConnection = class {
|
|
|
5742
6024
|
port: this.opts.port,
|
|
5743
6025
|
openCodePassword: this.opts.openCodePassword,
|
|
5744
6026
|
onConnected: (agentId) => {
|
|
6027
|
+
const attempt = this.reconnectAttempt + 1;
|
|
5745
6028
|
this.reconnectAttempt = 0;
|
|
5746
6029
|
this.reconnecting = false;
|
|
5747
6030
|
this.resolvedAgentId = agentId;
|
|
5748
|
-
|
|
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
|
+
});
|
|
5749
6039
|
},
|
|
5750
6040
|
onDisconnected: (code, reason) => {
|
|
5751
|
-
|
|
5752
|
-
|
|
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) {
|
|
5753
6045
|
this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
|
|
5754
6046
|
events.onError?.(`Reconnection failed: ${err.message}`);
|
|
5755
6047
|
});
|
|
@@ -6088,6 +6380,83 @@ function createResourceUsageCollector(homeDir) {
|
|
|
6088
6380
|
};
|
|
6089
6381
|
}
|
|
6090
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
|
+
|
|
6091
6460
|
// src/lib/channels/driver.ts
|
|
6092
6461
|
import { homedir as homedir6 } from "node:os";
|
|
6093
6462
|
|
|
@@ -6546,6 +6915,7 @@ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
|
6546
6915
|
var MAX_BUFFERED_SESSION_ERRORS = 256;
|
|
6547
6916
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
6548
6917
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
6918
|
+
var DRIVER_SESSION_ENUMERATION_LIMIT = 20;
|
|
6549
6919
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
6550
6920
|
var HEARTBEAT_MS = 6e4;
|
|
6551
6921
|
var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
|
|
@@ -6607,6 +6977,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6607
6977
|
maxActiveSessions;
|
|
6608
6978
|
watcherStallMs;
|
|
6609
6979
|
wedgeWarningIntervalMs;
|
|
6980
|
+
sessionStoreHealth;
|
|
6610
6981
|
openCodeClient;
|
|
6611
6982
|
/** Cache of conversationId → opencode sessionId. */
|
|
6612
6983
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -6976,6 +7347,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6976
7347
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
6977
7348
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
6978
7349
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
7350
|
+
this.sessionStoreHealth = config.sessionStoreHealth;
|
|
6979
7351
|
}
|
|
6980
7352
|
get isV2() {
|
|
6981
7353
|
return this.openCodeClient.version === "v2";
|
|
@@ -6991,7 +7363,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6991
7363
|
return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6992
7364
|
}
|
|
6993
7365
|
async listSessions() {
|
|
6994
|
-
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
|
+
});
|
|
6995
7369
|
}
|
|
6996
7370
|
async sessionExists(sessionId) {
|
|
6997
7371
|
return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
|
|
@@ -8445,8 +8819,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8445
8819
|
b2LastDescendantCheckMs: 0,
|
|
8446
8820
|
b2RootOngoingHeldLogged: false,
|
|
8447
8821
|
b2AbandonedSignalled: false,
|
|
8822
|
+
b2AbandonedWithoutAnswer: false,
|
|
8448
8823
|
ambiguousPinnedSinceMs: 0,
|
|
8449
8824
|
ambiguousResolved: false,
|
|
8825
|
+
sessionStoreFailurePending: false,
|
|
8450
8826
|
neverStartedResolved: false
|
|
8451
8827
|
});
|
|
8452
8828
|
const buffered = this.bufferedSessionErrors.get(sessionId);
|
|
@@ -8563,8 +8939,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8563
8939
|
b2LastDescendantCheckMs: 0,
|
|
8564
8940
|
b2RootOngoingHeldLogged: false,
|
|
8565
8941
|
b2AbandonedSignalled: false,
|
|
8942
|
+
b2AbandonedWithoutAnswer: false,
|
|
8566
8943
|
ambiguousPinnedSinceMs: 0,
|
|
8567
8944
|
ambiguousResolved: false,
|
|
8945
|
+
sessionStoreFailurePending: false,
|
|
8568
8946
|
neverStartedResolved: false
|
|
8569
8947
|
});
|
|
8570
8948
|
}
|
|
@@ -8978,6 +9356,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8978
9356
|
if (state === "running" || state === "done" || state === "failed") {
|
|
8979
9357
|
this.neverStartedAttempts.delete(id);
|
|
8980
9358
|
}
|
|
9359
|
+
if (inFlight.sessionStoreFailurePending) {
|
|
9360
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
9361
|
+
return;
|
|
9362
|
+
}
|
|
8981
9363
|
if (inFlight.neverStartedResolved) {
|
|
8982
9364
|
if (state === "running" || state === "done" || state === "failed") {
|
|
8983
9365
|
inFlight.neverStartedResolved = false;
|
|
@@ -9137,10 +9519,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9137
9519
|
inFlight.b2LastDescendantCheckMs = 0;
|
|
9138
9520
|
inFlight.b2RootOngoingHeldLogged = false;
|
|
9139
9521
|
inFlight.b2AbandonedSignalled = false;
|
|
9522
|
+
inFlight.b2AbandonedWithoutAnswer = false;
|
|
9140
9523
|
}
|
|
9141
9524
|
} else {
|
|
9142
9525
|
if (inFlight.b2AbandonedSignalled) {
|
|
9143
|
-
|
|
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
|
+
}
|
|
9144
9538
|
return;
|
|
9145
9539
|
}
|
|
9146
9540
|
if (inFlight.b2PinnedSinceMs === 0) inFlight.b2PinnedSinceMs = this.now();
|
|
@@ -9157,17 +9551,47 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9157
9551
|
descendantOngoing,
|
|
9158
9552
|
rootOngoing
|
|
9159
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
|
+
}
|
|
9160
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
|
+
}
|
|
9161
9586
|
this.log({
|
|
9162
9587
|
level: "warn",
|
|
9163
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`,
|
|
9164
9589
|
conversation_id: conv.id,
|
|
9165
9590
|
message_id: id
|
|
9166
9591
|
});
|
|
9167
|
-
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
9168
9592
|
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
9169
9593
|
watched_for_ms: pinnedForMs,
|
|
9170
|
-
finish
|
|
9594
|
+
finish,
|
|
9171
9595
|
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
9172
9596
|
...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
|
|
9173
9597
|
opencode_message_id: inFlight.opencodeMessageId
|
|
@@ -9215,6 +9639,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9215
9639
|
maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
|
|
9216
9640
|
sessionOngoing: ongoing
|
|
9217
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
|
+
}
|
|
9218
9652
|
inFlight.ambiguousResolved = true;
|
|
9219
9653
|
this.log({
|
|
9220
9654
|
level: "warn",
|
|
@@ -9439,6 +9873,74 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9439
9873
|
}
|
|
9440
9874
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9441
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
|
+
}
|
|
9442
9944
|
/**
|
|
9443
9945
|
* Report a queued message as failed after the never-started bound. The accepted
|
|
9444
9946
|
* OpenCode copy is not cancelled, so the local fence remains on unsuccessful
|
|
@@ -9494,6 +9996,66 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9494
9996
|
this.neverStartedAttempts.delete(inFlight.evidentMessageId);
|
|
9495
9997
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9496
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
|
+
}
|
|
9497
10059
|
// Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
|
|
9498
10060
|
/**
|
|
9499
10061
|
* Re-adopt this agent's `processing` messages on drain (ADR-0046 Decision §1).
|
|
@@ -12225,6 +12787,7 @@ function createCredentialSync({
|
|
|
12225
12787
|
|
|
12226
12788
|
// src/commands/run.ts
|
|
12227
12789
|
var MAX_ACTIVITY_LOG_ENTRIES = 10;
|
|
12790
|
+
var MAX_DISCONNECT_REASON_LENGTH = 200;
|
|
12228
12791
|
var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
|
|
12229
12792
|
var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
|
|
12230
12793
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
|
|
@@ -12443,7 +13006,9 @@ function displayStatus(state) {
|
|
|
12443
13006
|
const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
|
|
12444
13007
|
const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
|
|
12445
13008
|
const last = state.activityLog[state.activityLog.length - 1];
|
|
12446
|
-
const detail = last ? chalk7.dim(
|
|
13009
|
+
const detail = last ? chalk7.dim(
|
|
13010
|
+
` \xB7 ${last.timestamp.toISOString()} ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`
|
|
13011
|
+
) : "";
|
|
12447
13012
|
const agent = state.agentName ?? state.agentId;
|
|
12448
13013
|
console.log(
|
|
12449
13014
|
`${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
|
|
@@ -12695,9 +13260,9 @@ async function driveChannels(state, driver) {
|
|
|
12695
13260
|
}
|
|
12696
13261
|
}
|
|
12697
13262
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
12698
|
-
var
|
|
13263
|
+
var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
|
|
12699
13264
|
function shouldWarnForReclaimSkip(reason) {
|
|
12700
|
-
if (reason !== "sqlite-unavailable") return false;
|
|
13265
|
+
if (reason !== "sqlite-unavailable" && reason !== "disk-exhaustion") return false;
|
|
12701
13266
|
const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
|
|
12702
13267
|
if (!version2) return false;
|
|
12703
13268
|
const major = Number(version2[1]);
|
|
@@ -12754,61 +13319,96 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
|
|
|
12754
13319
|
async function runSweep(state, driver, config) {
|
|
12755
13320
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
12756
13321
|
try {
|
|
12757
|
-
const
|
|
12758
|
-
|
|
12759
|
-
|
|
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) {
|
|
13326
|
+
logActivity(state, {
|
|
12760
13327
|
type: "info",
|
|
12761
|
-
|
|
12762
|
-
|
|
12763
|
-
outcome: "sweep-skipped",
|
|
12764
|
-
reason: "opencode-unreachable"
|
|
12765
|
-
}
|
|
13328
|
+
level: "warn",
|
|
13329
|
+
message: `Session cleanup: could not enumerate sessions (${enumerationReason}); skipping deletes and continuing with space reclaim (${mode})`
|
|
12766
13330
|
});
|
|
12767
|
-
return;
|
|
12768
13331
|
}
|
|
12769
|
-
const toDelete = selectSessionsToDelete(
|
|
12770
|
-
sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
|
|
12771
|
-
{
|
|
12772
|
-
maxAgeMs: config.maxAgeMs,
|
|
12773
|
-
maxCount: config.maxCount,
|
|
12774
|
-
nowMs: Date.now(),
|
|
12775
|
-
protectedIds: driver.protectedSessionIds()
|
|
12776
|
-
}
|
|
12777
|
-
);
|
|
12778
|
-
const protectedNow = driver.protectedSessionIds();
|
|
12779
13332
|
let deleted = 0;
|
|
12780
13333
|
let failed = 0;
|
|
13334
|
+
let failed4xx = 0;
|
|
13335
|
+
let failed5xx = 0;
|
|
13336
|
+
let failedOtherStatus = 0;
|
|
13337
|
+
let failedThrew = 0;
|
|
12781
13338
|
let skippedNewlyActive = 0;
|
|
12782
|
-
|
|
12783
|
-
|
|
12784
|
-
|
|
12785
|
-
|
|
12786
|
-
|
|
12787
|
-
|
|
12788
|
-
|
|
12789
|
-
|
|
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
|
+
}
|
|
12790
13376
|
}
|
|
12791
|
-
|
|
12792
|
-
|
|
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
|
+
});
|
|
12793
13384
|
}
|
|
12794
|
-
const
|
|
12795
|
-
|
|
12796
|
-
|
|
12797
|
-
|
|
12798
|
-
|
|
12799
|
-
|
|
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
|
+
};
|
|
12800
13397
|
const reclaimResult = await reclaimSessionDbSpace({
|
|
12801
13398
|
dbPath: sessionDbPath(),
|
|
12802
|
-
|
|
13399
|
+
chunkPages: SESSION_DB_RECLAIM_CHUNK_PAGES,
|
|
13400
|
+
pageCeiling: SESSION_DB_RECLAIM_PAGE_CEILING,
|
|
13401
|
+
timeBudgetMs: SESSION_DB_RECLAIM_TIME_BUDGET_MS,
|
|
12803
13402
|
allowFullVacuum: protectedNow.size === 0 && !state.sessionDbProvenanceAnomaly
|
|
12804
13403
|
});
|
|
12805
13404
|
if (reclaimResult.ok) {
|
|
13405
|
+
state.sessionStoreHealth.observeWritable();
|
|
12806
13406
|
const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
|
|
12807
13407
|
const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
|
|
12808
13408
|
const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
|
|
12809
13409
|
logSessionCleanupActivity(state, "sweep", {
|
|
12810
13410
|
type: "info",
|
|
12811
|
-
message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${checkpointNote
|
|
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,
|
|
12812
13412
|
metadata: {
|
|
12813
13413
|
outcome: "reclaimed",
|
|
12814
13414
|
mode: reclaimResult.mode,
|
|
@@ -12816,25 +13416,27 @@ async function runSweep(state, driver, config) {
|
|
|
12816
13416
|
after_bytes: reclaimResult.afterBytes,
|
|
12817
13417
|
checkpoint_busy: reclaimResult.checkpoint.busy,
|
|
12818
13418
|
wal_frames_pending: reclaimResult.checkpoint.log,
|
|
12819
|
-
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
|
|
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
|
|
12823
13425
|
}
|
|
12824
13426
|
});
|
|
12825
13427
|
} else {
|
|
13428
|
+
if (reclaimResult.skipped === "disk-exhaustion") {
|
|
13429
|
+
state.sessionStoreHealth.observeUnwritable();
|
|
13430
|
+
}
|
|
12826
13431
|
logSessionCleanupActivity(state, "sweep", {
|
|
12827
13432
|
type: "info",
|
|
12828
|
-
level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
|
|
13433
|
+
level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "disk-exhaustion" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
|
|
12829
13434
|
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : ""),
|
|
12830
13435
|
metadata: {
|
|
12831
13436
|
outcome: "reclaim-skipped",
|
|
12832
13437
|
reason: reclaimResult.skipped,
|
|
12833
13438
|
...reclaimResult.detail !== void 0 ? { detail: reclaimResult.detail } : {},
|
|
12834
|
-
|
|
12835
|
-
sessions_deleted: deleted,
|
|
12836
|
-
sessions_failed: failed,
|
|
12837
|
-
sessions_skipped_active: skippedNewlyActive
|
|
13439
|
+
...sessionMetadata
|
|
12838
13440
|
}
|
|
12839
13441
|
});
|
|
12840
13442
|
}
|
|
@@ -13091,6 +13693,19 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
13091
13693
|
message: `Resource usage collection: ${warning2}`
|
|
13092
13694
|
});
|
|
13093
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
|
+
}
|
|
13094
13709
|
const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
|
|
13095
13710
|
if (result.ok) {
|
|
13096
13711
|
if (consecutiveFailures > 0) {
|
|
@@ -13337,6 +13952,7 @@ async function run(options) {
|
|
|
13337
13952
|
opencodeClient: null,
|
|
13338
13953
|
opencodeV2CredentialImportGuard: { inFlight: false },
|
|
13339
13954
|
sessionDbProvenanceAnomaly: false,
|
|
13955
|
+
sessionStoreHealth: createSessionStoreHealth(),
|
|
13340
13956
|
opencodeProcess: null,
|
|
13341
13957
|
stopOpenCodeLogTail: null,
|
|
13342
13958
|
litestreamProcess: null,
|
|
@@ -13354,6 +13970,7 @@ async function run(options) {
|
|
|
13354
13970
|
openaiUsageRearm: null,
|
|
13355
13971
|
resourceUsageTimer: null,
|
|
13356
13972
|
stopResourceUsageSampling: null,
|
|
13973
|
+
lowDiskWarningState: { worstBand: null, warnedAtMs: null },
|
|
13357
13974
|
credentialSync: null,
|
|
13358
13975
|
authHeader: ""
|
|
13359
13976
|
};
|
|
@@ -13544,10 +14161,13 @@ async function run(options) {
|
|
|
13544
14161
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
13545
14162
|
}
|
|
13546
14163
|
state.credentialSync?.arm();
|
|
13547
|
-
const openCodeActivityForwarder = createOpenCodeActivityForwarder(
|
|
13548
|
-
|
|
13549
|
-
|
|
13550
|
-
|
|
14164
|
+
const openCodeActivityForwarder = createOpenCodeActivityForwarder(
|
|
14165
|
+
() => ({
|
|
14166
|
+
agentId: state.agentId,
|
|
14167
|
+
authHeader: state.authHeader
|
|
14168
|
+
}),
|
|
14169
|
+
state.sessionStoreHealth.observeUnwritable
|
|
14170
|
+
);
|
|
13551
14171
|
const openCodeLogTail = tailOpenCodeLogFile(
|
|
13552
14172
|
resolveOpenCodeLogPath(homedir7(), process.env),
|
|
13553
14173
|
openCodeActivityForwarder
|
|
@@ -13839,6 +14459,7 @@ async function run(options) {
|
|
|
13839
14459
|
getAuthHeader: () => state.authHeader,
|
|
13840
14460
|
conversationFilter: state.conversationFilter,
|
|
13841
14461
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
14462
|
+
sessionStoreHealth: state.sessionStoreHealth,
|
|
13842
14463
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
13843
14464
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
13844
14465
|
fileSyncDirectories,
|
|
@@ -13864,12 +14485,13 @@ async function run(options) {
|
|
|
13864
14485
|
openCodePassword: state.opencodePassword,
|
|
13865
14486
|
isRunning: () => state.running,
|
|
13866
14487
|
events: {
|
|
13867
|
-
onConnected: (agentId, isReconnect) => {
|
|
14488
|
+
onConnected: (agentId, isReconnect, transition) => {
|
|
13868
14489
|
state.connected = true;
|
|
13869
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})`;
|
|
13870
14492
|
logActivity(state, {
|
|
13871
14493
|
type: "info",
|
|
13872
|
-
message:
|
|
14494
|
+
message: connectionMessage
|
|
13873
14495
|
});
|
|
13874
14496
|
if (options.tunnelReadyFile) {
|
|
13875
14497
|
const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
|
|
@@ -13890,7 +14512,12 @@ async function run(options) {
|
|
|
13890
14512
|
version: state.opencodeVersion,
|
|
13891
14513
|
major: state.opencodeApiVersion,
|
|
13892
14514
|
connected: state.opencodeConnected
|
|
13893
|
-
})
|
|
14515
|
+
}),
|
|
14516
|
+
connection_generation: transition.generation,
|
|
14517
|
+
...isReconnect && transition.downtimeMs !== null ? {
|
|
14518
|
+
reconnect_attempt: transition.attempt,
|
|
14519
|
+
downtime_ms: transition.downtimeMs
|
|
14520
|
+
} : {}
|
|
13894
14521
|
});
|
|
13895
14522
|
if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
|
|
13896
14523
|
if (state.interactive) displayStatus(state);
|
|
@@ -13912,13 +14539,18 @@ async function run(options) {
|
|
|
13912
14539
|
if (state.interactive) displayStatus(state);
|
|
13913
14540
|
});
|
|
13914
14541
|
},
|
|
13915
|
-
onDisconnected: (code, reason) => {
|
|
14542
|
+
onDisconnected: (code, reason, transition) => {
|
|
13916
14543
|
state.connected = false;
|
|
14544
|
+
const safeReason = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
|
|
13917
14545
|
logActivity(state, {
|
|
13918
14546
|
type: "info",
|
|
13919
|
-
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
|
|
13920
14553
|
});
|
|
13921
|
-
emitAgentDisconnected(state.agentId, { code, reason });
|
|
13922
14554
|
if (state.interactive) displayStatus(state);
|
|
13923
14555
|
},
|
|
13924
14556
|
onError: (error2) => {
|
|
@@ -14061,7 +14693,7 @@ async function run(options) {
|
|
|
14061
14693
|
}
|
|
14062
14694
|
|
|
14063
14695
|
// src/index.ts
|
|
14064
|
-
var
|
|
14696
|
+
var version = getCliVersion();
|
|
14065
14697
|
var program = new Command();
|
|
14066
14698
|
program.name("evident").description("Run OpenCode locally and connect it to Evident").version(version).option(
|
|
14067
14699
|
"--endpoint <url>",
|