@evident-ai/cli 3.5.2-dev.817a271 → 3.5.2-dev.afae02b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +802 -237
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1069,6 +1069,76 @@ import chalk7 from "chalk";
|
|
|
1069
1069
|
// ../../packages/types/src/agents/index.ts
|
|
1070
1070
|
var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
|
|
1071
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
|
+
|
|
1072
1142
|
// ../../packages/types/src/telemetry/index.ts
|
|
1073
1143
|
var TelemetryEventTypes = {
|
|
1074
1144
|
// Agent activity events (shown in web UI activity log)
|
|
@@ -1157,7 +1227,7 @@ import ora4 from "ora";
|
|
|
1157
1227
|
import { select as select4 } from "@inquirer/prompts";
|
|
1158
1228
|
|
|
1159
1229
|
// src/lib/telemetry.ts
|
|
1160
|
-
var CLI_VERSION = (true ? "3.5.2-dev.
|
|
1230
|
+
var CLI_VERSION = (true ? "3.5.2-dev.afae02b" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1161
1231
|
function getCliVersion() {
|
|
1162
1232
|
return CLI_VERSION;
|
|
1163
1233
|
}
|
|
@@ -1320,6 +1390,219 @@ var EventTypes = {
|
|
|
1320
1390
|
|
|
1321
1391
|
// src/lib/runner-activity-telemetry.ts
|
|
1322
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
|
|
1323
1606
|
var FORWARDED_LEVELS_BY_SOURCE = {
|
|
1324
1607
|
"cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
|
|
1325
1608
|
opencode: /* @__PURE__ */ new Set(["warn", "error"]),
|
|
@@ -1336,11 +1619,13 @@ function parseOpenCodeLogLine(line) {
|
|
|
1336
1619
|
if (!levelMatch) return null;
|
|
1337
1620
|
const level = levelMatch[1].toUpperCase();
|
|
1338
1621
|
if (level !== "WARN" && level !== "ERROR") return null;
|
|
1339
|
-
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+)/);
|
|
1340
1624
|
const messageMatch = normalisedLine.match(/(?:^|\s)message=(?:"((?:\\.|[^"])*)"|(\S+))/);
|
|
1341
1625
|
const parsed = {
|
|
1342
1626
|
level: level === "WARN" ? "warn" : "error",
|
|
1343
|
-
sessionID: sessionMatch?.[1]
|
|
1627
|
+
sessionID: sessionMatch?.[1],
|
|
1628
|
+
messageID: messageIDMatch?.[1]
|
|
1344
1629
|
};
|
|
1345
1630
|
const message = messageMatch?.[1] ?? messageMatch?.[2];
|
|
1346
1631
|
if (message !== void 0) parsed.message = message;
|
|
@@ -1348,12 +1633,13 @@ function parseOpenCodeLogLine(line) {
|
|
|
1348
1633
|
}
|
|
1349
1634
|
var MAX_LINE_BUFFER_BYTES = 16 * 1024;
|
|
1350
1635
|
var REPEAT_SUPPRESSION_WINDOW_MS = 6e4;
|
|
1351
|
-
function createOpenCodeActivityForwarder(getContext) {
|
|
1636
|
+
function createOpenCodeActivityForwarder(getContext, onSessionStoreWriteFailure) {
|
|
1352
1637
|
let buffer = Buffer.alloc(0);
|
|
1353
1638
|
const repeatStates = /* @__PURE__ */ new Map();
|
|
1354
1639
|
const forwardLine = (line, parsed, repeatsSuppressed = 0) => {
|
|
1355
1640
|
const metadata = {
|
|
1356
1641
|
...parsed.sessionID ? { sessionID: parsed.sessionID } : {},
|
|
1642
|
+
...parsed.messageID ? { messageID: parsed.messageID } : {},
|
|
1357
1643
|
...repeatsSuppressed > 0 ? { repeats_suppressed: repeatsSuppressed } : {}
|
|
1358
1644
|
};
|
|
1359
1645
|
forwardRunnerActivity(
|
|
@@ -1378,10 +1664,19 @@ function createOpenCodeActivityForwarder(getContext) {
|
|
|
1378
1664
|
}
|
|
1379
1665
|
};
|
|
1380
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
|
+
}
|
|
1381
1676
|
const parsed = parseOpenCodeLogLine(line);
|
|
1382
1677
|
if (!parsed) return;
|
|
1383
1678
|
const now = Date.now();
|
|
1384
|
-
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 ?? ""}`;
|
|
1385
1680
|
expireRepeatStates(now, key);
|
|
1386
1681
|
if (key === void 0) {
|
|
1387
1682
|
forwardLine(line, parsed);
|
|
@@ -1919,7 +2214,7 @@ async function getOpenCodeDirectory(port, client) {
|
|
|
1919
2214
|
return null;
|
|
1920
2215
|
}
|
|
1921
2216
|
}
|
|
1922
|
-
function
|
|
2217
|
+
function roleOf2(m) {
|
|
1923
2218
|
if (!m || typeof m !== "object") return void 0;
|
|
1924
2219
|
if (typeof m.role === "string") return m.role;
|
|
1925
2220
|
const infoRole = m.info?.role;
|
|
@@ -1933,13 +2228,13 @@ function createdOf(m) {
|
|
|
1933
2228
|
if (!m || typeof m !== "object") return void 0;
|
|
1934
2229
|
return m.info?.time?.created ?? m.time?.created;
|
|
1935
2230
|
}
|
|
1936
|
-
function
|
|
2231
|
+
function idOf2(m) {
|
|
1937
2232
|
if (!m || typeof m !== "object") return void 0;
|
|
1938
2233
|
if (typeof m.id === "string") return m.id;
|
|
1939
2234
|
const infoId = m.info?.id;
|
|
1940
2235
|
return typeof infoId === "string" ? infoId : void 0;
|
|
1941
2236
|
}
|
|
1942
|
-
function
|
|
2237
|
+
function parentIdOf2(m) {
|
|
1943
2238
|
if (!m || typeof m !== "object") return void 0;
|
|
1944
2239
|
if (typeof m.parentID === "string") return m.parentID;
|
|
1945
2240
|
const infoParent = m.info?.parentID;
|
|
@@ -2004,9 +2299,10 @@ async function pollSessionMessagesForRedrive(port, sessionId, client) {
|
|
|
2004
2299
|
function isSessionActivelyGenerating(messages) {
|
|
2005
2300
|
if (!messages || messages.length === 0) return false;
|
|
2006
2301
|
const last = messages[messages.length - 1];
|
|
2007
|
-
if (
|
|
2302
|
+
if (roleOf2(last) !== "assistant") return false;
|
|
2008
2303
|
return completedOf(last) == null;
|
|
2009
2304
|
}
|
|
2305
|
+
var FULL_SESSION_ENUMERATION_MAX_SESSIONS = 3200;
|
|
2010
2306
|
function sessionLastActivityMs(session) {
|
|
2011
2307
|
const candidates = [
|
|
2012
2308
|
session.time?.updated,
|
|
@@ -2021,30 +2317,60 @@ function sessionLastActivityMs(session) {
|
|
|
2021
2317
|
}
|
|
2022
2318
|
return null;
|
|
2023
2319
|
}
|
|
2024
|
-
|
|
2025
|
-
|
|
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);
|
|
2026
2328
|
try {
|
|
2027
|
-
const res = await requestWithClient(port, client,
|
|
2329
|
+
const res = await requestWithClient(port, client, path);
|
|
2028
2330
|
if (!res.ok) return null;
|
|
2029
2331
|
const body = await res.json();
|
|
2030
2332
|
return Array.isArray(body) ? body : null;
|
|
2031
2333
|
} catch (error2) {
|
|
2032
2334
|
console.error(
|
|
2033
|
-
`[
|
|
2335
|
+
`[listSessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2034
2336
|
);
|
|
2035
2337
|
return null;
|
|
2036
2338
|
}
|
|
2037
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
|
+
}
|
|
2038
2358
|
async function deleteSession(port, id, client) {
|
|
2039
2359
|
if (client?.version === "v2") return deleteV2Session(client, id);
|
|
2040
2360
|
try {
|
|
2041
|
-
const res = await requestWithClient(
|
|
2042
|
-
|
|
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 };
|
|
2043
2369
|
} catch (error2) {
|
|
2044
2370
|
console.error(
|
|
2045
2371
|
`[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2046
2372
|
);
|
|
2047
|
-
return false;
|
|
2373
|
+
return { ok: false, status: "threw" };
|
|
2048
2374
|
}
|
|
2049
2375
|
}
|
|
2050
2376
|
async function sessionExists(port, id, client) {
|
|
@@ -2285,7 +2611,7 @@ function messageText(m) {
|
|
|
2285
2611
|
async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
|
|
2286
2612
|
const before = await getSessionMessages(port, sessionId, client);
|
|
2287
2613
|
const knownUserIds = new Set(
|
|
2288
|
-
(before ?? []).filter((m) =>
|
|
2614
|
+
(before ?? []).filter((m) => roleOf2(m) === "user").map((m) => idOf2(m)).filter((id) => typeof id === "string")
|
|
2289
2615
|
);
|
|
2290
2616
|
const parts = [{ type: "text", text: content }];
|
|
2291
2617
|
let pendingOutcomes = null;
|
|
@@ -2328,8 +2654,8 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
|
|
|
2328
2654
|
if (after) {
|
|
2329
2655
|
let best = null;
|
|
2330
2656
|
for (const m of after) {
|
|
2331
|
-
if (
|
|
2332
|
-
const id =
|
|
2657
|
+
if (roleOf2(m) !== "user") continue;
|
|
2658
|
+
const id = idOf2(m);
|
|
2333
2659
|
if (typeof id !== "string" || knownUserIds.has(id)) continue;
|
|
2334
2660
|
if (messageText(m) !== content) continue;
|
|
2335
2661
|
const created = createdOf(m) ?? 0;
|
|
@@ -2351,13 +2677,13 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
|
|
|
2351
2677
|
function findAssistantReplyAfter(messages, userMessageId) {
|
|
2352
2678
|
if (!messages || messages.length === 0) return null;
|
|
2353
2679
|
const byParent = messages.find(
|
|
2354
|
-
(m) =>
|
|
2680
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
|
|
2355
2681
|
);
|
|
2356
2682
|
if (byParent) return byParent;
|
|
2357
|
-
const userIndex = messages.findIndex((m) =>
|
|
2683
|
+
const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
|
|
2358
2684
|
if (userIndex === -1) return null;
|
|
2359
2685
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2360
|
-
if (
|
|
2686
|
+
if (roleOf2(messages[i]) === "assistant") return messages[i];
|
|
2361
2687
|
}
|
|
2362
2688
|
return null;
|
|
2363
2689
|
}
|
|
@@ -2367,7 +2693,7 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
2367
2693
|
let lastNonErrored = null;
|
|
2368
2694
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2369
2695
|
const m = messages[i];
|
|
2370
|
-
if (
|
|
2696
|
+
if (roleOf2(m) !== "assistant" || parentIdOf2(m) !== userMessageId) continue;
|
|
2371
2697
|
if (lastCorrelated === null) lastCorrelated = m;
|
|
2372
2698
|
if (errorOf(m) == null) {
|
|
2373
2699
|
lastNonErrored = m;
|
|
@@ -2375,12 +2701,12 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
2375
2701
|
}
|
|
2376
2702
|
}
|
|
2377
2703
|
if (lastCorrelated) return lastNonErrored ?? lastCorrelated;
|
|
2378
|
-
const userIndex = messages.findIndex((m) =>
|
|
2704
|
+
const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
|
|
2379
2705
|
if (userIndex === -1) return null;
|
|
2380
2706
|
let last = null;
|
|
2381
2707
|
let lastOk = null;
|
|
2382
2708
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2383
|
-
const role =
|
|
2709
|
+
const role = roleOf2(messages[i]);
|
|
2384
2710
|
if (role === "user") break;
|
|
2385
2711
|
if (role === "assistant") {
|
|
2386
2712
|
last = messages[i];
|
|
@@ -2392,16 +2718,16 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
2392
2718
|
function collectSubagentSessions(messages, userMessageId) {
|
|
2393
2719
|
if (!messages || messages.length === 0) return [];
|
|
2394
2720
|
const byParent = messages.filter(
|
|
2395
|
-
(message) =>
|
|
2721
|
+
(message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
|
|
2396
2722
|
);
|
|
2397
2723
|
const assistants = byParent.length > 0 ? byParent : [];
|
|
2398
2724
|
if (assistants.length === 0) {
|
|
2399
|
-
const userIndex = messages.findIndex((message) =>
|
|
2725
|
+
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
2400
2726
|
if (userIndex === -1) return [];
|
|
2401
2727
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2402
2728
|
const message = messages[i];
|
|
2403
|
-
if (
|
|
2404
|
-
if (
|
|
2729
|
+
if (roleOf2(message) === "user") break;
|
|
2730
|
+
if (roleOf2(message) === "assistant") assistants.push(message);
|
|
2405
2731
|
}
|
|
2406
2732
|
}
|
|
2407
2733
|
const refs = [];
|
|
@@ -2441,7 +2767,7 @@ function collectTaskCalls(messages, userMessageId) {
|
|
|
2441
2767
|
if (!messages || messages.length === 0) return [];
|
|
2442
2768
|
const calls = [];
|
|
2443
2769
|
for (const message of messages) {
|
|
2444
|
-
if (
|
|
2770
|
+
if (roleOf2(message) !== "assistant" || parentIdOf2(message) !== userMessageId) continue;
|
|
2445
2771
|
for (const part of message.parts ?? []) {
|
|
2446
2772
|
if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
|
|
2447
2773
|
continue;
|
|
@@ -2471,7 +2797,7 @@ function attributeTaskCallUsage(messages, windows) {
|
|
|
2471
2797
|
for (const window of eligibleWindows) assignments.set(window.callID, []);
|
|
2472
2798
|
const unattributed = [];
|
|
2473
2799
|
for (const message of messages ?? []) {
|
|
2474
|
-
if (
|
|
2800
|
+
if (roleOf2(message) !== "assistant") continue;
|
|
2475
2801
|
const created = finiteNumber(createdOf(message));
|
|
2476
2802
|
const matching = created === null ? [] : eligibleWindows.filter(
|
|
2477
2803
|
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
@@ -2548,7 +2874,7 @@ function sumAssistantUsage(messages) {
|
|
|
2548
2874
|
function messageUsage(messages, userMessageId) {
|
|
2549
2875
|
if (!messages || messages.length === 0) return null;
|
|
2550
2876
|
const byParentAll = messages.filter(
|
|
2551
|
-
(m) =>
|
|
2877
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
|
|
2552
2878
|
);
|
|
2553
2879
|
const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
|
|
2554
2880
|
const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
|
|
@@ -2564,7 +2890,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
2564
2890
|
}
|
|
2565
2891
|
function messageRunState(messages, userMessageId) {
|
|
2566
2892
|
if (!messages || messages.length === 0) return "unknown";
|
|
2567
|
-
const hasUser = messages.some((m) =>
|
|
2893
|
+
const hasUser = messages.some((m) => idOf2(m) === userMessageId);
|
|
2568
2894
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
2569
2895
|
if (!hasUser) {
|
|
2570
2896
|
if (!reply) return "unknown";
|
|
@@ -2656,7 +2982,7 @@ function findLatestSubagentAuthOutcome(messages, sinceMs) {
|
|
|
2656
2982
|
if (!messages || messages.length === 0) return null;
|
|
2657
2983
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2658
2984
|
const message = messages[i];
|
|
2659
|
-
if (
|
|
2985
|
+
if (roleOf2(message) !== "assistant") continue;
|
|
2660
2986
|
const created = createdOf(message);
|
|
2661
2987
|
if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
|
|
2662
2988
|
const failure = classifyReplyAuthError(message);
|
|
@@ -2688,25 +3014,25 @@ function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProvi
|
|
|
2688
3014
|
function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
2689
3015
|
if (!messages || messages.length === 0) return false;
|
|
2690
3016
|
return messages.some(
|
|
2691
|
-
(m) =>
|
|
3017
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
2692
3018
|
);
|
|
2693
3019
|
}
|
|
2694
3020
|
function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
|
|
2695
3021
|
if (!messages || messages.length === 0) return false;
|
|
2696
|
-
const userIndex = messages.findIndex((message) =>
|
|
3022
|
+
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
2697
3023
|
if (userIndex === -1) return false;
|
|
2698
3024
|
let hasLaterUser = false;
|
|
2699
3025
|
let hasStartedLaterUser = false;
|
|
2700
3026
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2701
3027
|
const message = messages[i];
|
|
2702
|
-
if (
|
|
3028
|
+
if (roleOf2(message) !== "user") continue;
|
|
2703
3029
|
hasLaterUser = true;
|
|
2704
|
-
const laterUserMessageId =
|
|
3030
|
+
const laterUserMessageId = idOf2(message);
|
|
2705
3031
|
if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
|
|
2706
3032
|
return false;
|
|
2707
3033
|
}
|
|
2708
3034
|
if (messages.some(
|
|
2709
|
-
(candidate) =>
|
|
3035
|
+
(candidate) => roleOf2(candidate) === "assistant" && parentIdOf2(candidate) === laterUserMessageId
|
|
2710
3036
|
)) {
|
|
2711
3037
|
hasStartedLaterUser = true;
|
|
2712
3038
|
}
|
|
@@ -3177,13 +3503,17 @@ async function listV2Sessions(client) {
|
|
|
3177
3503
|
}
|
|
3178
3504
|
async function deleteV2Session(client, sessionId) {
|
|
3179
3505
|
try {
|
|
3180
|
-
await client.request(
|
|
3181
|
-
|
|
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 };
|
|
3182
3512
|
} catch (error2) {
|
|
3183
3513
|
console.error(
|
|
3184
3514
|
`[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3185
3515
|
);
|
|
3186
|
-
return false;
|
|
3516
|
+
return { ok: false, status: "threw" };
|
|
3187
3517
|
}
|
|
3188
3518
|
}
|
|
3189
3519
|
async function v2SessionExists(client, sessionId) {
|
|
@@ -3805,9 +4135,9 @@ async function importOpenCodeV2Credentials({
|
|
|
3805
4135
|
|
|
3806
4136
|
// src/lib/opencode/session-db-boot.ts
|
|
3807
4137
|
import { spawn as spawn2 } from "node:child_process";
|
|
3808
|
-
import { mkdirSync, statSync as
|
|
4138
|
+
import { mkdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
|
|
3809
4139
|
import { homedir as homedir4 } from "node:os";
|
|
3810
|
-
import { dirname as
|
|
4140
|
+
import { dirname as dirname3, resolve as resolvePath } from "node:path";
|
|
3811
4141
|
|
|
3812
4142
|
// src/lib/runner-synchroniser.ts
|
|
3813
4143
|
import { spawn } from "node:child_process";
|
|
@@ -3932,7 +4262,7 @@ function clearMarker(options) {
|
|
|
3932
4262
|
function markNoReplicate(options, message) {
|
|
3933
4263
|
if (options.noReplicateMarker) {
|
|
3934
4264
|
try {
|
|
3935
|
-
mkdirSync(
|
|
4265
|
+
mkdirSync(dirname3(options.noReplicateMarker), { recursive: true });
|
|
3936
4266
|
writeFileSync(options.noReplicateMarker, "");
|
|
3937
4267
|
} catch (error2) {
|
|
3938
4268
|
options.log(
|
|
@@ -4057,7 +4387,7 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
4057
4387
|
return null;
|
|
4058
4388
|
}
|
|
4059
4389
|
try {
|
|
4060
|
-
if (
|
|
4390
|
+
if (statSync3(configPath).size > 0) return configPath;
|
|
4061
4391
|
} catch (error2) {
|
|
4062
4392
|
if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")) {
|
|
4063
4393
|
options.log(
|
|
@@ -4087,7 +4417,7 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
4087
4417
|
return null;
|
|
4088
4418
|
}
|
|
4089
4419
|
try {
|
|
4090
|
-
mkdirSync(
|
|
4420
|
+
mkdirSync(dirname3(configPath), { recursive: true });
|
|
4091
4421
|
writeFileSync(configPath, rendered.stdout);
|
|
4092
4422
|
} catch (error2) {
|
|
4093
4423
|
options.log(
|
|
@@ -4248,7 +4578,7 @@ async function verifySessionDb(options, configPath, env) {
|
|
|
4248
4578
|
}
|
|
4249
4579
|
function fileExists(path) {
|
|
4250
4580
|
try {
|
|
4251
|
-
|
|
4581
|
+
statSync3(path);
|
|
4252
4582
|
return true;
|
|
4253
4583
|
} catch (error2) {
|
|
4254
4584
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return false;
|
|
@@ -4337,7 +4667,7 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
4337
4667
|
// src/lib/opencode/session-db-provenance.ts
|
|
4338
4668
|
import { createRequire } from "node:module";
|
|
4339
4669
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4340
|
-
import { dirname as
|
|
4670
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
4341
4671
|
var require2 = createRequire(import.meta.url);
|
|
4342
4672
|
function readSessionDbMigrationIds(dbPath) {
|
|
4343
4673
|
let db;
|
|
@@ -4403,7 +4733,7 @@ function loadSessionDbProvenanceState(path) {
|
|
|
4403
4733
|
}
|
|
4404
4734
|
function saveSessionDbProvenanceState(path, state) {
|
|
4405
4735
|
try {
|
|
4406
|
-
mkdirSync2(
|
|
4736
|
+
mkdirSync2(dirname4(path), { recursive: true });
|
|
4407
4737
|
writeFileSync2(path, `${JSON.stringify(state, null, 2)}
|
|
4408
4738
|
`, "utf8");
|
|
4409
4739
|
} catch (error2) {
|
|
@@ -4984,7 +5314,10 @@ function selectSessionsToDelete(sessions, opts) {
|
|
|
4984
5314
|
toDelete.push(s.id);
|
|
4985
5315
|
}
|
|
4986
5316
|
}
|
|
4987
|
-
|
|
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
|
+
);
|
|
4988
5321
|
}
|
|
4989
5322
|
var DEFAULT_INTERVAL = "1h";
|
|
4990
5323
|
function resolve(flag, envValue, fallback) {
|
|
@@ -5044,13 +5377,13 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
5044
5377
|
}
|
|
5045
5378
|
|
|
5046
5379
|
// src/lib/opencode/session-db-size.ts
|
|
5047
|
-
import { statSync as
|
|
5380
|
+
import { statSync as statSync4 } from "node:fs";
|
|
5048
5381
|
import { join as join6 } from "node:path";
|
|
5049
5382
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
5050
5383
|
function statSessionDbBytes(homeDir) {
|
|
5051
5384
|
const dbPath = join6(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
5052
5385
|
try {
|
|
5053
|
-
return
|
|
5386
|
+
return statSync4(dbPath).size;
|
|
5054
5387
|
} catch (err) {
|
|
5055
5388
|
const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
5056
5389
|
if (!isMissingFile) {
|
|
@@ -5076,7 +5409,7 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
5076
5409
|
}
|
|
5077
5410
|
|
|
5078
5411
|
// src/lib/opencode/log-tail.ts
|
|
5079
|
-
import { statSync as
|
|
5412
|
+
import { statSync as statSync5 } from "node:fs";
|
|
5080
5413
|
import { homedir as homedir5 } from "node:os";
|
|
5081
5414
|
import { join as join7 } from "node:path";
|
|
5082
5415
|
import { open as open2, stat } from "node:fs/promises";
|
|
@@ -5099,7 +5432,7 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
|
5099
5432
|
let inode = null;
|
|
5100
5433
|
let baselineReady = true;
|
|
5101
5434
|
try {
|
|
5102
|
-
const initial =
|
|
5435
|
+
const initial = statSync5(logPath);
|
|
5103
5436
|
offset = initial.size;
|
|
5104
5437
|
inode = initial.ino;
|
|
5105
5438
|
} catch (error2) {
|
|
@@ -5158,129 +5491,6 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
|
5158
5491
|
};
|
|
5159
5492
|
}
|
|
5160
5493
|
|
|
5161
|
-
// src/lib/opencode/session-db-reclaim.ts
|
|
5162
|
-
import { statSync as statSync5, statfsSync } from "node:fs";
|
|
5163
|
-
import { dirname as dirname4 } from "node:path";
|
|
5164
|
-
function errorMessage(error2) {
|
|
5165
|
-
if (!(error2 instanceof Error)) return String(error2);
|
|
5166
|
-
return error2.cause instanceof Error ? error2.cause.message : error2.message;
|
|
5167
|
-
}
|
|
5168
|
-
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
5169
|
-
try {
|
|
5170
|
-
const fsStats = statfsSync(dirname4(dbPath));
|
|
5171
|
-
const availableBytes = fsStats.bavail * fsStats.bsize;
|
|
5172
|
-
if (availableBytes < requiredBytes) {
|
|
5173
|
-
return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
|
|
5174
|
-
}
|
|
5175
|
-
return null;
|
|
5176
|
-
} catch (err) {
|
|
5177
|
-
return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
|
|
5178
|
-
}
|
|
5179
|
-
}
|
|
5180
|
-
function readLogicalBytes(db) {
|
|
5181
|
-
const pageCount = db.prepare("PRAGMA page_count").get().page_count;
|
|
5182
|
-
const pageSize = db.prepare("PRAGMA page_size").get().page_size;
|
|
5183
|
-
return pageCount * pageSize;
|
|
5184
|
-
}
|
|
5185
|
-
function readCheckpointResult(db) {
|
|
5186
|
-
const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
5187
|
-
return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
|
|
5188
|
-
}
|
|
5189
|
-
async function probeReclaimAvailability(input) {
|
|
5190
|
-
const { dbPath, requiredBytes } = input;
|
|
5191
|
-
let sqlite;
|
|
5192
|
-
try {
|
|
5193
|
-
sqlite = await import("node:sqlite");
|
|
5194
|
-
} catch (err) {
|
|
5195
|
-
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
5196
|
-
console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
|
|
5197
|
-
return { reason: "sqlite-unavailable", detail };
|
|
5198
|
-
}
|
|
5199
|
-
let autoVacuum = null;
|
|
5200
|
-
try {
|
|
5201
|
-
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
5202
|
-
try {
|
|
5203
|
-
db.exec("PRAGMA busy_timeout=5000");
|
|
5204
|
-
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
5205
|
-
} finally {
|
|
5206
|
-
db.close();
|
|
5207
|
-
}
|
|
5208
|
-
} catch (err) {
|
|
5209
|
-
console.warn(
|
|
5210
|
-
`[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
5211
|
-
);
|
|
5212
|
-
}
|
|
5213
|
-
if (autoVacuum !== 0) return null;
|
|
5214
|
-
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
5215
|
-
}
|
|
5216
|
-
async function reclaimSessionDbSpace(input) {
|
|
5217
|
-
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
5218
|
-
let sqlite;
|
|
5219
|
-
try {
|
|
5220
|
-
sqlite = await import("node:sqlite");
|
|
5221
|
-
} catch (err) {
|
|
5222
|
-
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
5223
|
-
console.warn(
|
|
5224
|
-
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
|
|
5225
|
-
);
|
|
5226
|
-
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
5227
|
-
}
|
|
5228
|
-
const { DatabaseSync } = sqlite;
|
|
5229
|
-
let db;
|
|
5230
|
-
try {
|
|
5231
|
-
db = new DatabaseSync(dbPath);
|
|
5232
|
-
db.exec("PRAGMA busy_timeout=5000");
|
|
5233
|
-
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
5234
|
-
if (autoVacuum === 0) {
|
|
5235
|
-
if (!allowFullVacuum) {
|
|
5236
|
-
console.warn(
|
|
5237
|
-
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
|
|
5238
|
-
);
|
|
5239
|
-
return {
|
|
5240
|
-
ok: false,
|
|
5241
|
-
skipped: "full-vacuum-blocked",
|
|
5242
|
-
detail: "a session turn is live"
|
|
5243
|
-
};
|
|
5244
|
-
}
|
|
5245
|
-
const fileBytesForGuard = statSync5(dbPath).size;
|
|
5246
|
-
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
5247
|
-
if (skipReason !== null) {
|
|
5248
|
-
console.warn(
|
|
5249
|
-
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
|
|
5250
|
-
);
|
|
5251
|
-
return { ok: false, skipped: "insufficient-disk-space", detail: skipReason };
|
|
5252
|
-
}
|
|
5253
|
-
const beforeBytes = readLogicalBytes(db);
|
|
5254
|
-
db.exec("PRAGMA auto_vacuum=INCREMENTAL");
|
|
5255
|
-
db.exec("VACUUM");
|
|
5256
|
-
const afterBytes = readLogicalBytes(db);
|
|
5257
|
-
const checkpoint = readCheckpointResult(db);
|
|
5258
|
-
return { ok: true, mode: "convert", beforeBytes, afterBytes, checkpoint };
|
|
5259
|
-
}
|
|
5260
|
-
if (autoVacuum === 2) {
|
|
5261
|
-
const beforeBytes = readLogicalBytes(db);
|
|
5262
|
-
const bound = Math.max(0, Math.trunc(maxPages));
|
|
5263
|
-
db.exec(`PRAGMA incremental_vacuum(${bound})`);
|
|
5264
|
-
const afterBytes = readLogicalBytes(db);
|
|
5265
|
-
const checkpoint = readCheckpointResult(db);
|
|
5266
|
-
return { ok: true, mode: "incremental", beforeBytes, afterBytes, checkpoint };
|
|
5267
|
-
}
|
|
5268
|
-
console.warn(
|
|
5269
|
-
`[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
|
|
5270
|
-
);
|
|
5271
|
-
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
5272
|
-
} catch (err) {
|
|
5273
|
-
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
5274
|
-
return {
|
|
5275
|
-
ok: false,
|
|
5276
|
-
skipped: "reclaim-error",
|
|
5277
|
-
detail: errorMessage(err)
|
|
5278
|
-
};
|
|
5279
|
-
} finally {
|
|
5280
|
-
db?.close();
|
|
5281
|
-
}
|
|
5282
|
-
}
|
|
5283
|
-
|
|
5284
5494
|
// src/lib/tunnel/connection.ts
|
|
5285
5495
|
import WebSocket2 from "ws";
|
|
5286
5496
|
|
|
@@ -5510,6 +5720,8 @@ var StreamForwarder = class {
|
|
|
5510
5720
|
if (done) break;
|
|
5511
5721
|
const chunk = Buffer.from(value);
|
|
5512
5722
|
for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
|
|
5723
|
+
await stream.pausePromise;
|
|
5724
|
+
if (ac.signal.aborted) break;
|
|
5513
5725
|
const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
|
|
5514
5726
|
if (this.binaryFramesSupported) {
|
|
5515
5727
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
@@ -5552,6 +5764,7 @@ function classifyUpgradeRejection(headers) {
|
|
|
5552
5764
|
}
|
|
5553
5765
|
var MAX_RECONNECT_DELAY = 3e4;
|
|
5554
5766
|
var BASE_RECONNECT_DELAY = 500;
|
|
5767
|
+
var RELAY_SILENCE_TIMEOUT_MS = 9e4;
|
|
5555
5768
|
function getReconnectDelay(attempt) {
|
|
5556
5769
|
const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
|
|
5557
5770
|
const jitter = Math.random() * 1e3;
|
|
@@ -5623,10 +5836,27 @@ function connectTunnel(options) {
|
|
|
5623
5836
|
ws.close();
|
|
5624
5837
|
reject(new Error("Connection timeout"));
|
|
5625
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
|
+
};
|
|
5626
5855
|
let upgradeRejection = null;
|
|
5627
5856
|
let upgradeRejectionReason = null;
|
|
5628
5857
|
ws.on("unexpected-response", (_req, res) => {
|
|
5629
5858
|
clearTimeout(connectionTimeout);
|
|
5859
|
+
clearRelaySilenceTimeout();
|
|
5630
5860
|
const reason = classifyUpgradeRejection(res.headers);
|
|
5631
5861
|
upgradeRejectionReason = reason;
|
|
5632
5862
|
const chunks = [];
|
|
@@ -5656,6 +5886,7 @@ function connectTunnel(options) {
|
|
|
5656
5886
|
onInfo?.("WebSocket connection established");
|
|
5657
5887
|
});
|
|
5658
5888
|
ws.on("message", (data, isBinary) => {
|
|
5889
|
+
if (relaySilenceTimeout !== void 0) armRelaySilenceTimeout();
|
|
5659
5890
|
if (isBinary) {
|
|
5660
5891
|
try {
|
|
5661
5892
|
const frame = decodeBinaryBodyFrame(toUint8Array(data));
|
|
@@ -5689,6 +5920,7 @@ function connectTunnel(options) {
|
|
|
5689
5920
|
switch (message.type) {
|
|
5690
5921
|
case "connected": {
|
|
5691
5922
|
clearTimeout(connectionTimeout);
|
|
5923
|
+
armRelaySilenceTimeout();
|
|
5692
5924
|
forwarder.setBinaryFramesSupported(message.binary_frames === true);
|
|
5693
5925
|
const connectedAgentId = message.agent_id ?? agentId;
|
|
5694
5926
|
onConnected?.(connectedAgentId);
|
|
@@ -5713,6 +5945,7 @@ function connectTunnel(options) {
|
|
|
5713
5945
|
});
|
|
5714
5946
|
ws.on("error", (error2) => {
|
|
5715
5947
|
clearTimeout(connectionTimeout);
|
|
5948
|
+
clearRelaySilenceTimeout();
|
|
5716
5949
|
const detail = upgradeRejection ?? describeSocketError(error2, url);
|
|
5717
5950
|
if (upgradeRejectionReason === "do_code_updated") {
|
|
5718
5951
|
onWarning?.("Relay redeployed \u2014 reconnecting");
|
|
@@ -5724,7 +5957,8 @@ function connectTunnel(options) {
|
|
|
5724
5957
|
);
|
|
5725
5958
|
});
|
|
5726
5959
|
ws.on("close", (code, reason) => {
|
|
5727
|
-
|
|
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");
|
|
5728
5962
|
forwarder.abortAll();
|
|
5729
5963
|
onDisconnected?.(code, reasonStr);
|
|
5730
5964
|
});
|
|
@@ -5732,7 +5966,7 @@ function connectTunnel(options) {
|
|
|
5732
5966
|
}
|
|
5733
5967
|
|
|
5734
5968
|
// src/lib/tunnel/runner-connection.ts
|
|
5735
|
-
var RunnerConnection = class {
|
|
5969
|
+
var RunnerConnection = class _RunnerConnection {
|
|
5736
5970
|
opts;
|
|
5737
5971
|
sleep;
|
|
5738
5972
|
now;
|
|
@@ -5746,6 +5980,10 @@ var RunnerConnection = class {
|
|
|
5746
5980
|
reconnectAttempt = 0;
|
|
5747
5981
|
generation = 0;
|
|
5748
5982
|
disconnectedAtMs = null;
|
|
5983
|
+
static RELAY_RECONNECT_CLOSE_REASONS = /* @__PURE__ */ new Set([
|
|
5984
|
+
"No pong received",
|
|
5985
|
+
"Orphaned socket"
|
|
5986
|
+
]);
|
|
5749
5987
|
constructor(opts) {
|
|
5750
5988
|
this.opts = opts;
|
|
5751
5989
|
this.resolvedAgentId = opts.agentId;
|
|
@@ -5802,7 +6040,8 @@ var RunnerConnection = class {
|
|
|
5802
6040
|
onDisconnected: (code, reason) => {
|
|
5803
6041
|
this.disconnectedAtMs = this.now();
|
|
5804
6042
|
events.onDisconnected(code, reason, { generation: this.generation });
|
|
5805
|
-
|
|
6043
|
+
const relayRequestedReconnect = code === 1e3 && _RunnerConnection.RELAY_RECONNECT_CLOSE_REASONS.has(reason);
|
|
6044
|
+
if (this.opts.isRunning() && (code !== 1e3 || relayRequestedReconnect) && !this.reconnecting) {
|
|
5806
6045
|
this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
|
|
5807
6046
|
events.onError?.(`Reconnection failed: ${err.message}`);
|
|
5808
6047
|
});
|
|
@@ -6141,6 +6380,83 @@ function createResourceUsageCollector(homeDir) {
|
|
|
6141
6380
|
};
|
|
6142
6381
|
}
|
|
6143
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
|
+
|
|
6144
6460
|
// src/lib/channels/driver.ts
|
|
6145
6461
|
import { homedir as homedir6 } from "node:os";
|
|
6146
6462
|
|
|
@@ -6599,6 +6915,7 @@ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
|
6599
6915
|
var MAX_BUFFERED_SESSION_ERRORS = 256;
|
|
6600
6916
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
6601
6917
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
6918
|
+
var DRIVER_SESSION_ENUMERATION_LIMIT = 20;
|
|
6602
6919
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
6603
6920
|
var HEARTBEAT_MS = 6e4;
|
|
6604
6921
|
var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
|
|
@@ -6660,6 +6977,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6660
6977
|
maxActiveSessions;
|
|
6661
6978
|
watcherStallMs;
|
|
6662
6979
|
wedgeWarningIntervalMs;
|
|
6980
|
+
sessionStoreHealth;
|
|
6663
6981
|
openCodeClient;
|
|
6664
6982
|
/** Cache of conversationId → opencode sessionId. */
|
|
6665
6983
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -7029,6 +7347,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7029
7347
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
7030
7348
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
7031
7349
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
7350
|
+
this.sessionStoreHealth = config.sessionStoreHealth;
|
|
7032
7351
|
}
|
|
7033
7352
|
get isV2() {
|
|
7034
7353
|
return this.openCodeClient.version === "v2";
|
|
@@ -7044,7 +7363,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7044
7363
|
return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
7045
7364
|
}
|
|
7046
7365
|
async listSessions() {
|
|
7047
|
-
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
|
+
});
|
|
7048
7369
|
}
|
|
7049
7370
|
async sessionExists(sessionId) {
|
|
7050
7371
|
return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
|
|
@@ -8498,8 +8819,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8498
8819
|
b2LastDescendantCheckMs: 0,
|
|
8499
8820
|
b2RootOngoingHeldLogged: false,
|
|
8500
8821
|
b2AbandonedSignalled: false,
|
|
8822
|
+
b2AbandonedWithoutAnswer: false,
|
|
8501
8823
|
ambiguousPinnedSinceMs: 0,
|
|
8502
8824
|
ambiguousResolved: false,
|
|
8825
|
+
sessionStoreFailurePending: false,
|
|
8503
8826
|
neverStartedResolved: false
|
|
8504
8827
|
});
|
|
8505
8828
|
const buffered = this.bufferedSessionErrors.get(sessionId);
|
|
@@ -8616,8 +8939,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8616
8939
|
b2LastDescendantCheckMs: 0,
|
|
8617
8940
|
b2RootOngoingHeldLogged: false,
|
|
8618
8941
|
b2AbandonedSignalled: false,
|
|
8942
|
+
b2AbandonedWithoutAnswer: false,
|
|
8619
8943
|
ambiguousPinnedSinceMs: 0,
|
|
8620
8944
|
ambiguousResolved: false,
|
|
8945
|
+
sessionStoreFailurePending: false,
|
|
8621
8946
|
neverStartedResolved: false
|
|
8622
8947
|
});
|
|
8623
8948
|
}
|
|
@@ -9031,6 +9356,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9031
9356
|
if (state === "running" || state === "done" || state === "failed") {
|
|
9032
9357
|
this.neverStartedAttempts.delete(id);
|
|
9033
9358
|
}
|
|
9359
|
+
if (inFlight.sessionStoreFailurePending) {
|
|
9360
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
9361
|
+
return;
|
|
9362
|
+
}
|
|
9034
9363
|
if (inFlight.neverStartedResolved) {
|
|
9035
9364
|
if (state === "running" || state === "done" || state === "failed") {
|
|
9036
9365
|
inFlight.neverStartedResolved = false;
|
|
@@ -9190,10 +9519,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9190
9519
|
inFlight.b2LastDescendantCheckMs = 0;
|
|
9191
9520
|
inFlight.b2RootOngoingHeldLogged = false;
|
|
9192
9521
|
inFlight.b2AbandonedSignalled = false;
|
|
9522
|
+
inFlight.b2AbandonedWithoutAnswer = false;
|
|
9193
9523
|
}
|
|
9194
9524
|
} else {
|
|
9195
9525
|
if (inFlight.b2AbandonedSignalled) {
|
|
9196
|
-
|
|
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
|
+
}
|
|
9197
9538
|
return;
|
|
9198
9539
|
}
|
|
9199
9540
|
if (inFlight.b2PinnedSinceMs === 0) inFlight.b2PinnedSinceMs = this.now();
|
|
@@ -9210,17 +9551,47 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9210
9551
|
descendantOngoing,
|
|
9211
9552
|
rootOngoing
|
|
9212
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
|
+
}
|
|
9213
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
|
+
}
|
|
9214
9586
|
this.log({
|
|
9215
9587
|
level: "warn",
|
|
9216
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`,
|
|
9217
9589
|
conversation_id: conv.id,
|
|
9218
9590
|
message_id: id
|
|
9219
9591
|
});
|
|
9220
|
-
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
9221
9592
|
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
9222
9593
|
watched_for_ms: pinnedForMs,
|
|
9223
|
-
finish
|
|
9594
|
+
finish,
|
|
9224
9595
|
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
9225
9596
|
...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
|
|
9226
9597
|
opencode_message_id: inFlight.opencodeMessageId
|
|
@@ -9268,6 +9639,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9268
9639
|
maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
|
|
9269
9640
|
sessionOngoing: ongoing
|
|
9270
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
|
+
}
|
|
9271
9652
|
inFlight.ambiguousResolved = true;
|
|
9272
9653
|
this.log({
|
|
9273
9654
|
level: "warn",
|
|
@@ -9492,6 +9873,74 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9492
9873
|
}
|
|
9493
9874
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9494
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
|
+
}
|
|
9495
9944
|
/**
|
|
9496
9945
|
* Report a queued message as failed after the never-started bound. The accepted
|
|
9497
9946
|
* OpenCode copy is not cancelled, so the local fence remains on unsuccessful
|
|
@@ -9547,6 +9996,66 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9547
9996
|
this.neverStartedAttempts.delete(inFlight.evidentMessageId);
|
|
9548
9997
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9549
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
|
+
}
|
|
9550
10059
|
// Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
|
|
9551
10060
|
/**
|
|
9552
10061
|
* Re-adopt this agent's `processing` messages on drain (ADR-0046 Decision §1).
|
|
@@ -12751,9 +13260,9 @@ async function driveChannels(state, driver) {
|
|
|
12751
13260
|
}
|
|
12752
13261
|
}
|
|
12753
13262
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
12754
|
-
var
|
|
13263
|
+
var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
|
|
12755
13264
|
function shouldWarnForReclaimSkip(reason) {
|
|
12756
|
-
if (reason !== "sqlite-unavailable") return false;
|
|
13265
|
+
if (reason !== "sqlite-unavailable" && reason !== "disk-exhaustion") return false;
|
|
12757
13266
|
const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
|
|
12758
13267
|
if (!version2) return false;
|
|
12759
13268
|
const major = Number(version2[1]);
|
|
@@ -12810,61 +13319,96 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
|
|
|
12810
13319
|
async function runSweep(state, driver, config) {
|
|
12811
13320
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
12812
13321
|
try {
|
|
12813
|
-
const
|
|
12814
|
-
|
|
12815
|
-
|
|
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, {
|
|
12816
13327
|
type: "info",
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
outcome: "sweep-skipped",
|
|
12820
|
-
reason: "opencode-unreachable"
|
|
12821
|
-
}
|
|
13328
|
+
level: "warn",
|
|
13329
|
+
message: `Session cleanup: could not enumerate sessions (${enumerationReason}); skipping deletes and continuing with space reclaim (${mode})`
|
|
12822
13330
|
});
|
|
12823
|
-
return;
|
|
12824
13331
|
}
|
|
12825
|
-
const toDelete = selectSessionsToDelete(
|
|
12826
|
-
sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
|
|
12827
|
-
{
|
|
12828
|
-
maxAgeMs: config.maxAgeMs,
|
|
12829
|
-
maxCount: config.maxCount,
|
|
12830
|
-
nowMs: Date.now(),
|
|
12831
|
-
protectedIds: driver.protectedSessionIds()
|
|
12832
|
-
}
|
|
12833
|
-
);
|
|
12834
|
-
const protectedNow = driver.protectedSessionIds();
|
|
12835
13332
|
let deleted = 0;
|
|
12836
13333
|
let failed = 0;
|
|
13334
|
+
let failed4xx = 0;
|
|
13335
|
+
let failed5xx = 0;
|
|
13336
|
+
let failedOtherStatus = 0;
|
|
13337
|
+
let failedThrew = 0;
|
|
12837
13338
|
let skippedNewlyActive = 0;
|
|
12838
|
-
|
|
12839
|
-
|
|
12840
|
-
|
|
12841
|
-
|
|
12842
|
-
|
|
12843
|
-
|
|
12844
|
-
|
|
12845
|
-
|
|
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
|
+
}
|
|
12846
13376
|
}
|
|
12847
|
-
|
|
12848
|
-
|
|
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
|
+
});
|
|
12849
13384
|
}
|
|
12850
|
-
const
|
|
12851
|
-
|
|
12852
|
-
|
|
12853
|
-
|
|
12854
|
-
|
|
12855
|
-
|
|
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
|
+
};
|
|
12856
13397
|
const reclaimResult = await reclaimSessionDbSpace({
|
|
12857
13398
|
dbPath: sessionDbPath(),
|
|
12858
|
-
|
|
13399
|
+
chunkPages: SESSION_DB_RECLAIM_CHUNK_PAGES,
|
|
13400
|
+
pageCeiling: SESSION_DB_RECLAIM_PAGE_CEILING,
|
|
13401
|
+
timeBudgetMs: SESSION_DB_RECLAIM_TIME_BUDGET_MS,
|
|
12859
13402
|
allowFullVacuum: protectedNow.size === 0 && !state.sessionDbProvenanceAnomaly
|
|
12860
13403
|
});
|
|
12861
13404
|
if (reclaimResult.ok) {
|
|
13405
|
+
state.sessionStoreHealth.observeWritable();
|
|
12862
13406
|
const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
|
|
12863
13407
|
const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
|
|
12864
13408
|
const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
|
|
12865
13409
|
logSessionCleanupActivity(state, "sweep", {
|
|
12866
13410
|
type: "info",
|
|
12867
|
-
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,
|
|
12868
13412
|
metadata: {
|
|
12869
13413
|
outcome: "reclaimed",
|
|
12870
13414
|
mode: reclaimResult.mode,
|
|
@@ -12872,25 +13416,27 @@ async function runSweep(state, driver, config) {
|
|
|
12872
13416
|
after_bytes: reclaimResult.afterBytes,
|
|
12873
13417
|
checkpoint_busy: reclaimResult.checkpoint.busy,
|
|
12874
13418
|
wal_frames_pending: reclaimResult.checkpoint.log,
|
|
12875
|
-
|
|
12876
|
-
|
|
12877
|
-
|
|
12878
|
-
|
|
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
|
|
12879
13425
|
}
|
|
12880
13426
|
});
|
|
12881
13427
|
} else {
|
|
13428
|
+
if (reclaimResult.skipped === "disk-exhaustion") {
|
|
13429
|
+
state.sessionStoreHealth.observeUnwritable();
|
|
13430
|
+
}
|
|
12882
13431
|
logSessionCleanupActivity(state, "sweep", {
|
|
12883
13432
|
type: "info",
|
|
12884
|
-
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",
|
|
12885
13434
|
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : ""),
|
|
12886
13435
|
metadata: {
|
|
12887
13436
|
outcome: "reclaim-skipped",
|
|
12888
13437
|
reason: reclaimResult.skipped,
|
|
12889
13438
|
...reclaimResult.detail !== void 0 ? { detail: reclaimResult.detail } : {},
|
|
12890
|
-
|
|
12891
|
-
sessions_deleted: deleted,
|
|
12892
|
-
sessions_failed: failed,
|
|
12893
|
-
sessions_skipped_active: skippedNewlyActive
|
|
13439
|
+
...sessionMetadata
|
|
12894
13440
|
}
|
|
12895
13441
|
});
|
|
12896
13442
|
}
|
|
@@ -13147,6 +13693,19 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
13147
13693
|
message: `Resource usage collection: ${warning2}`
|
|
13148
13694
|
});
|
|
13149
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
|
+
}
|
|
13150
13709
|
const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
|
|
13151
13710
|
if (result.ok) {
|
|
13152
13711
|
if (consecutiveFailures > 0) {
|
|
@@ -13393,6 +13952,7 @@ async function run(options) {
|
|
|
13393
13952
|
opencodeClient: null,
|
|
13394
13953
|
opencodeV2CredentialImportGuard: { inFlight: false },
|
|
13395
13954
|
sessionDbProvenanceAnomaly: false,
|
|
13955
|
+
sessionStoreHealth: createSessionStoreHealth(),
|
|
13396
13956
|
opencodeProcess: null,
|
|
13397
13957
|
stopOpenCodeLogTail: null,
|
|
13398
13958
|
litestreamProcess: null,
|
|
@@ -13410,6 +13970,7 @@ async function run(options) {
|
|
|
13410
13970
|
openaiUsageRearm: null,
|
|
13411
13971
|
resourceUsageTimer: null,
|
|
13412
13972
|
stopResourceUsageSampling: null,
|
|
13973
|
+
lowDiskWarningState: { worstBand: null, warnedAtMs: null },
|
|
13413
13974
|
credentialSync: null,
|
|
13414
13975
|
authHeader: ""
|
|
13415
13976
|
};
|
|
@@ -13600,10 +14161,13 @@ async function run(options) {
|
|
|
13600
14161
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
13601
14162
|
}
|
|
13602
14163
|
state.credentialSync?.arm();
|
|
13603
|
-
const openCodeActivityForwarder = createOpenCodeActivityForwarder(
|
|
13604
|
-
|
|
13605
|
-
|
|
13606
|
-
|
|
14164
|
+
const openCodeActivityForwarder = createOpenCodeActivityForwarder(
|
|
14165
|
+
() => ({
|
|
14166
|
+
agentId: state.agentId,
|
|
14167
|
+
authHeader: state.authHeader
|
|
14168
|
+
}),
|
|
14169
|
+
state.sessionStoreHealth.observeUnwritable
|
|
14170
|
+
);
|
|
13607
14171
|
const openCodeLogTail = tailOpenCodeLogFile(
|
|
13608
14172
|
resolveOpenCodeLogPath(homedir7(), process.env),
|
|
13609
14173
|
openCodeActivityForwarder
|
|
@@ -13895,6 +14459,7 @@ async function run(options) {
|
|
|
13895
14459
|
getAuthHeader: () => state.authHeader,
|
|
13896
14460
|
conversationFilter: state.conversationFilter,
|
|
13897
14461
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
14462
|
+
sessionStoreHealth: state.sessionStoreHealth,
|
|
13898
14463
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
13899
14464
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
13900
14465
|
fileSyncDirectories,
|