@evident-ai/cli 3.5.1-dev.bf9e537 → 3.5.2-dev.07822ab
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 +932 -267
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { createRequire as createRequire2 } from "module";
|
|
5
4
|
import { Command } from "commander";
|
|
6
5
|
|
|
7
6
|
// src/commands/login.ts
|
|
@@ -1063,7 +1062,7 @@ async function claudeUsage() {
|
|
|
1063
1062
|
|
|
1064
1063
|
// src/commands/run.ts
|
|
1065
1064
|
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1066
|
-
import { homedir as
|
|
1065
|
+
import { homedir as homedir7 } from "node:os";
|
|
1067
1066
|
import { isAbsolute as isAbsolute3, join as join11, parse, resolve as resolvePath2 } from "node:path";
|
|
1068
1067
|
import chalk7 from "chalk";
|
|
1069
1068
|
|
|
@@ -1158,7 +1157,7 @@ import ora4 from "ora";
|
|
|
1158
1157
|
import { select as select4 } from "@inquirer/prompts";
|
|
1159
1158
|
|
|
1160
1159
|
// src/lib/telemetry.ts
|
|
1161
|
-
var CLI_VERSION = (true ? "3.5.
|
|
1160
|
+
var CLI_VERSION = (true ? "3.5.2-dev.07822ab" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1162
1161
|
function getCliVersion() {
|
|
1163
1162
|
return CLI_VERSION;
|
|
1164
1163
|
}
|
|
@@ -1172,6 +1171,23 @@ var authProvider = null;
|
|
|
1172
1171
|
function setTelemetryAuthProvider(provider) {
|
|
1173
1172
|
authProvider = provider;
|
|
1174
1173
|
}
|
|
1174
|
+
var diagnosticsReporter = null;
|
|
1175
|
+
function setTelemetryDiagnosticsReporter(reporter) {
|
|
1176
|
+
diagnosticsReporter = reporter;
|
|
1177
|
+
}
|
|
1178
|
+
function reportTelemetryDiagnostic(message) {
|
|
1179
|
+
if (!diagnosticsReporter) {
|
|
1180
|
+
console.error(message);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
try {
|
|
1184
|
+
diagnosticsReporter(message);
|
|
1185
|
+
} catch (error2) {
|
|
1186
|
+
console.error(
|
|
1187
|
+
`Telemetry diagnostics reporter failed: ${error2 instanceof Error ? error2.message : String(error2)}; original diagnostic: ${message}`
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1175
1191
|
var FLUSH_FAILURE_LOG_INTERVAL_MS = 6e4;
|
|
1176
1192
|
var lastFlushFailureLoggedAt = 0;
|
|
1177
1193
|
var suppressedFlushFailureCount = 0;
|
|
@@ -1237,7 +1253,9 @@ async function flushEvents() {
|
|
|
1237
1253
|
signal: controller.signal
|
|
1238
1254
|
});
|
|
1239
1255
|
if (!response.ok) {
|
|
1240
|
-
|
|
1256
|
+
reportTelemetryDiagnostic(
|
|
1257
|
+
`Telemetry could not send events (HTTP ${response.status}); runner and OpenCode are unaffected`
|
|
1258
|
+
);
|
|
1241
1259
|
}
|
|
1242
1260
|
} finally {
|
|
1243
1261
|
clearTimeout(timeout);
|
|
@@ -1247,7 +1265,9 @@ async function flushEvents() {
|
|
|
1247
1265
|
if (now - lastFlushFailureLoggedAt >= FLUSH_FAILURE_LOG_INTERVAL_MS) {
|
|
1248
1266
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1249
1267
|
const suffix = suppressedFlushFailureCount > 0 ? ` (${suppressedFlushFailureCount} more suppressed in the last ${FLUSH_FAILURE_LOG_INTERVAL_MS / 1e3}s)` : "";
|
|
1250
|
-
|
|
1268
|
+
reportTelemetryDiagnostic(
|
|
1269
|
+
`Telemetry flush failed: ${message}; runner and OpenCode are unaffected${suffix}`
|
|
1270
|
+
);
|
|
1251
1271
|
lastFlushFailureLoggedAt = now;
|
|
1252
1272
|
suppressedFlushFailureCount = 0;
|
|
1253
1273
|
} else {
|
|
@@ -1299,8 +1319,227 @@ var EventTypes = {
|
|
|
1299
1319
|
};
|
|
1300
1320
|
|
|
1301
1321
|
// src/lib/runner-activity-telemetry.ts
|
|
1302
|
-
|
|
1322
|
+
import { homedir as homedir2 } from "node:os";
|
|
1323
|
+
|
|
1324
|
+
// src/lib/opencode/session-db-reclaim.ts
|
|
1325
|
+
import { statSync as statSync2, statfsSync } from "node:fs";
|
|
1326
|
+
import { dirname as dirname2 } from "node:path";
|
|
1327
|
+
var SESSION_DB_RECLAIM_CHUNK_PAGES = 2e3;
|
|
1328
|
+
var SESSION_DB_RECLAIM_TIME_BUDGET_MS = 500;
|
|
1329
|
+
var SESSION_DB_RECLAIM_PAGE_CEILING = 4e4;
|
|
1330
|
+
function errorMessage(error2) {
|
|
1331
|
+
if (!(error2 instanceof Error)) return String(error2);
|
|
1332
|
+
return error2.cause instanceof Error ? error2.cause.message : error2.message;
|
|
1333
|
+
}
|
|
1334
|
+
function isSessionStoreWriteFailure(error2) {
|
|
1335
|
+
const messages = [];
|
|
1336
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1337
|
+
let current = error2;
|
|
1338
|
+
while (current !== null && current !== void 0 && !visited.has(current)) {
|
|
1339
|
+
visited.add(current);
|
|
1340
|
+
if (typeof current === "string") {
|
|
1341
|
+
messages.push(current);
|
|
1342
|
+
break;
|
|
1343
|
+
}
|
|
1344
|
+
if (current instanceof Error) {
|
|
1345
|
+
messages.push(current.name, current.message);
|
|
1346
|
+
current = current.cause;
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
if (typeof current === "object") {
|
|
1350
|
+
const record = current;
|
|
1351
|
+
if (typeof record.code === "string") messages.push(record.code);
|
|
1352
|
+
if (typeof record.message === "string") messages.push(record.message);
|
|
1353
|
+
current = record.cause;
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
break;
|
|
1357
|
+
}
|
|
1358
|
+
return messages.some(
|
|
1359
|
+
(message) => /(?:database or disk is full|disk is full|no space left on device|SQLITE_(?:FULL|IOERR)(?:\b|_))/i.test(
|
|
1360
|
+
message
|
|
1361
|
+
)
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
1365
|
+
try {
|
|
1366
|
+
const fsStats = statfsSync(dirname2(dbPath));
|
|
1367
|
+
const availableBytes = fsStats.bavail * fsStats.bsize;
|
|
1368
|
+
if (availableBytes < requiredBytes) {
|
|
1369
|
+
return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
|
|
1370
|
+
}
|
|
1371
|
+
return null;
|
|
1372
|
+
} catch (err) {
|
|
1373
|
+
return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
function readLogicalBytes(db) {
|
|
1377
|
+
const pageCount = db.prepare("PRAGMA page_count").get().page_count;
|
|
1378
|
+
const pageSize = db.prepare("PRAGMA page_size").get().page_size;
|
|
1379
|
+
return pageCount * pageSize;
|
|
1380
|
+
}
|
|
1381
|
+
function readFreelistCount(db) {
|
|
1382
|
+
return db.prepare("PRAGMA freelist_count").get().freelist_count;
|
|
1383
|
+
}
|
|
1384
|
+
function readCheckpointResult(db) {
|
|
1385
|
+
const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
1386
|
+
return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
|
|
1387
|
+
}
|
|
1388
|
+
async function probeReclaimAvailability(input) {
|
|
1389
|
+
const { dbPath, requiredBytes } = input;
|
|
1390
|
+
let sqlite;
|
|
1391
|
+
try {
|
|
1392
|
+
sqlite = await import("node:sqlite");
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
1395
|
+
console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
|
|
1396
|
+
return { reason: "sqlite-unavailable", detail };
|
|
1397
|
+
}
|
|
1398
|
+
let autoVacuum = null;
|
|
1399
|
+
try {
|
|
1400
|
+
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
1401
|
+
try {
|
|
1402
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
1403
|
+
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
1404
|
+
} finally {
|
|
1405
|
+
db.close();
|
|
1406
|
+
}
|
|
1407
|
+
} catch (err) {
|
|
1408
|
+
console.warn(
|
|
1409
|
+
`[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
if (autoVacuum !== 0) return null;
|
|
1413
|
+
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
1414
|
+
}
|
|
1415
|
+
async function reclaimSessionDbSpace(input) {
|
|
1416
|
+
const {
|
|
1417
|
+
dbPath,
|
|
1418
|
+
pageCeiling = SESSION_DB_RECLAIM_PAGE_CEILING,
|
|
1419
|
+
timeBudgetMs = SESSION_DB_RECLAIM_TIME_BUDGET_MS,
|
|
1420
|
+
chunkPages = SESSION_DB_RECLAIM_CHUNK_PAGES,
|
|
1421
|
+
now = () => performance.now(),
|
|
1422
|
+
allowFullVacuum = true
|
|
1423
|
+
} = input;
|
|
1424
|
+
let sqlite;
|
|
1425
|
+
try {
|
|
1426
|
+
sqlite = await import("node:sqlite");
|
|
1427
|
+
} catch (err) {
|
|
1428
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
1429
|
+
console.warn(
|
|
1430
|
+
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
|
|
1431
|
+
);
|
|
1432
|
+
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
1433
|
+
}
|
|
1434
|
+
const { DatabaseSync } = sqlite;
|
|
1435
|
+
let db;
|
|
1436
|
+
try {
|
|
1437
|
+
db = new DatabaseSync(dbPath);
|
|
1438
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
1439
|
+
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
1440
|
+
if (autoVacuum === 0) {
|
|
1441
|
+
if (!allowFullVacuum) {
|
|
1442
|
+
console.warn(
|
|
1443
|
+
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
|
|
1444
|
+
);
|
|
1445
|
+
return {
|
|
1446
|
+
ok: false,
|
|
1447
|
+
skipped: "full-vacuum-blocked",
|
|
1448
|
+
detail: "a session turn is live"
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
const fileBytesForGuard = statSync2(dbPath).size;
|
|
1452
|
+
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
1453
|
+
if (skipReason !== null) {
|
|
1454
|
+
console.warn(
|
|
1455
|
+
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
|
|
1456
|
+
);
|
|
1457
|
+
return { ok: false, skipped: "insufficient-disk-space", detail: skipReason };
|
|
1458
|
+
}
|
|
1459
|
+
const beforeBytes = readLogicalBytes(db);
|
|
1460
|
+
const freelistBefore = readFreelistCount(db);
|
|
1461
|
+
const startedAt = now();
|
|
1462
|
+
db.exec("PRAGMA auto_vacuum=INCREMENTAL");
|
|
1463
|
+
db.exec("VACUUM");
|
|
1464
|
+
const afterBytes = readLogicalBytes(db);
|
|
1465
|
+
const freelistAfter = readFreelistCount(db);
|
|
1466
|
+
const checkpoint = readCheckpointResult(db);
|
|
1467
|
+
return {
|
|
1468
|
+
ok: true,
|
|
1469
|
+
mode: "convert",
|
|
1470
|
+
beforeBytes,
|
|
1471
|
+
afterBytes,
|
|
1472
|
+
freelistBefore,
|
|
1473
|
+
freelistAfter,
|
|
1474
|
+
pagesReclaimed: Math.max(0, freelistBefore - freelistAfter),
|
|
1475
|
+
stoppedBy: "freelist-drained",
|
|
1476
|
+
elapsedMs: Math.max(0, now() - startedAt),
|
|
1477
|
+
checkpoint
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
if (autoVacuum === 2) {
|
|
1481
|
+
const beforeBytes = readLogicalBytes(db);
|
|
1482
|
+
const freelistBefore = readFreelistCount(db);
|
|
1483
|
+
const startedAt = now();
|
|
1484
|
+
const pageLimit = Math.max(0, Math.trunc(pageCeiling));
|
|
1485
|
+
const chunkLimit = Math.max(1, Math.trunc(chunkPages));
|
|
1486
|
+
const budget = Math.max(0, timeBudgetMs);
|
|
1487
|
+
let pagesAttempted = 0;
|
|
1488
|
+
let freelistAfter = freelistBefore;
|
|
1489
|
+
let stoppedBy = "freelist-drained";
|
|
1490
|
+
while (freelistAfter > 0) {
|
|
1491
|
+
if (pagesAttempted >= pageLimit) {
|
|
1492
|
+
stoppedBy = "page-ceiling";
|
|
1493
|
+
break;
|
|
1494
|
+
}
|
|
1495
|
+
if (now() - startedAt >= budget) {
|
|
1496
|
+
stoppedBy = "time-budget";
|
|
1497
|
+
break;
|
|
1498
|
+
}
|
|
1499
|
+
const pages = Math.min(chunkLimit, pageLimit - pagesAttempted, freelistAfter);
|
|
1500
|
+
db.exec(`PRAGMA incremental_vacuum(${pages})`);
|
|
1501
|
+
pagesAttempted += pages;
|
|
1502
|
+
freelistAfter = readFreelistCount(db);
|
|
1503
|
+
}
|
|
1504
|
+
const afterBytes = readLogicalBytes(db);
|
|
1505
|
+
const checkpoint = readCheckpointResult(db);
|
|
1506
|
+
return {
|
|
1507
|
+
ok: true,
|
|
1508
|
+
mode: "incremental",
|
|
1509
|
+
beforeBytes,
|
|
1510
|
+
afterBytes,
|
|
1511
|
+
freelistBefore,
|
|
1512
|
+
freelistAfter,
|
|
1513
|
+
pagesReclaimed: Math.max(0, freelistBefore - freelistAfter),
|
|
1514
|
+
stoppedBy,
|
|
1515
|
+
elapsedMs: Math.max(0, now() - startedAt),
|
|
1516
|
+
checkpoint
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
console.warn(
|
|
1520
|
+
`[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
|
|
1521
|
+
);
|
|
1522
|
+
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
1523
|
+
} catch (err) {
|
|
1524
|
+
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
1525
|
+
return {
|
|
1526
|
+
ok: false,
|
|
1527
|
+
skipped: isSessionStoreWriteFailure(err) ? "disk-exhaustion" : "reclaim-error",
|
|
1528
|
+
detail: errorMessage(err)
|
|
1529
|
+
};
|
|
1530
|
+
} finally {
|
|
1531
|
+
db?.close();
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// src/lib/runner-activity-telemetry.ts
|
|
1536
|
+
var FORWARDED_LEVELS_BY_SOURCE = {
|
|
1537
|
+
"cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
|
|
1538
|
+
opencode: /* @__PURE__ */ new Set(["warn", "error"]),
|
|
1539
|
+
"cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"])
|
|
1540
|
+
};
|
|
1303
1541
|
var SEVERITY_BY_LEVEL = {
|
|
1542
|
+
info: "info",
|
|
1304
1543
|
warn: "warning",
|
|
1305
1544
|
error: "error"
|
|
1306
1545
|
};
|
|
@@ -1310,26 +1549,85 @@ function parseOpenCodeLogLine(line) {
|
|
|
1310
1549
|
if (!levelMatch) return null;
|
|
1311
1550
|
const level = levelMatch[1].toUpperCase();
|
|
1312
1551
|
if (level !== "WARN" && level !== "ERROR") return null;
|
|
1313
|
-
const sessionMatch = normalisedLine.match(/(?:^|\s)sessionID=(\S+)/);
|
|
1314
|
-
|
|
1552
|
+
const sessionMatch = normalisedLine.match(/(?:^|\s)(?:sessionID|session\.id)=(\S+)/);
|
|
1553
|
+
const messageIDMatch = normalisedLine.match(/(?:^|\s)messageID=(\S+)/);
|
|
1554
|
+
const messageMatch = normalisedLine.match(/(?:^|\s)message=(?:"((?:\\.|[^"])*)"|(\S+))/);
|
|
1555
|
+
const parsed = {
|
|
1556
|
+
level: level === "WARN" ? "warn" : "error",
|
|
1557
|
+
sessionID: sessionMatch?.[1],
|
|
1558
|
+
messageID: messageIDMatch?.[1]
|
|
1559
|
+
};
|
|
1560
|
+
const message = messageMatch?.[1] ?? messageMatch?.[2];
|
|
1561
|
+
if (message !== void 0) parsed.message = message;
|
|
1562
|
+
return parsed;
|
|
1315
1563
|
}
|
|
1316
1564
|
var MAX_LINE_BUFFER_BYTES = 16 * 1024;
|
|
1317
|
-
|
|
1565
|
+
var REPEAT_SUPPRESSION_WINDOW_MS = 6e4;
|
|
1566
|
+
function createOpenCodeActivityForwarder(getContext, onSessionStoreWriteFailure) {
|
|
1318
1567
|
let buffer = Buffer.alloc(0);
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1568
|
+
const repeatStates = /* @__PURE__ */ new Map();
|
|
1569
|
+
const forwardLine = (line, parsed, repeatsSuppressed = 0) => {
|
|
1570
|
+
const metadata = {
|
|
1571
|
+
...parsed.sessionID ? { sessionID: parsed.sessionID } : {},
|
|
1572
|
+
...parsed.messageID ? { messageID: parsed.messageID } : {},
|
|
1573
|
+
...repeatsSuppressed > 0 ? { repeats_suppressed: repeatsSuppressed } : {}
|
|
1574
|
+
};
|
|
1322
1575
|
forwardRunnerActivity(
|
|
1323
1576
|
{
|
|
1324
1577
|
level: parsed.level,
|
|
1325
1578
|
error: line,
|
|
1326
|
-
metadata:
|
|
1579
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
|
|
1327
1580
|
source: "opencode"
|
|
1328
1581
|
},
|
|
1329
1582
|
getContext()
|
|
1330
1583
|
);
|
|
1331
1584
|
};
|
|
1332
|
-
|
|
1585
|
+
const expireRepeatStates = (now, currentKey) => {
|
|
1586
|
+
for (const [key, state] of repeatStates) {
|
|
1587
|
+
if (key === currentKey || now - state.lastForwardedAt < REPEAT_SUPPRESSION_WINDOW_MS) {
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
if (state.repeatsSuppressed > 0) {
|
|
1591
|
+
forwardLine(state.line, state.parsed, state.repeatsSuppressed);
|
|
1592
|
+
}
|
|
1593
|
+
repeatStates.delete(key);
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
const flushLine = (line) => {
|
|
1597
|
+
if (onSessionStoreWriteFailure && isSessionStoreWriteFailure(line)) {
|
|
1598
|
+
try {
|
|
1599
|
+
onSessionStoreWriteFailure();
|
|
1600
|
+
} catch (err) {
|
|
1601
|
+
reportTelemetryDiagnostic(
|
|
1602
|
+
`Runner activity telemetry could not record session-store write failure: ${err instanceof Error ? err.message : String(err)}`
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
const parsed = parseOpenCodeLogLine(line);
|
|
1607
|
+
if (!parsed) return;
|
|
1608
|
+
const now = Date.now();
|
|
1609
|
+
const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}\0${parsed.sessionID ?? ""}\0${parsed.messageID ?? ""}`;
|
|
1610
|
+
expireRepeatStates(now, key);
|
|
1611
|
+
if (key === void 0) {
|
|
1612
|
+
forwardLine(line, parsed);
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
const previous = repeatStates.get(key);
|
|
1616
|
+
if (!previous || now - previous.lastForwardedAt >= REPEAT_SUPPRESSION_WINDOW_MS) {
|
|
1617
|
+
forwardLine(line, parsed, previous?.repeatsSuppressed ?? 0);
|
|
1618
|
+
repeatStates.set(key, {
|
|
1619
|
+
line,
|
|
1620
|
+
parsed,
|
|
1621
|
+
lastForwardedAt: now,
|
|
1622
|
+
repeatsSuppressed: 0
|
|
1623
|
+
});
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
previous.line = line;
|
|
1627
|
+
previous.parsed = parsed;
|
|
1628
|
+
previous.repeatsSuppressed++;
|
|
1629
|
+
};
|
|
1630
|
+
const forward = ((chunk) => {
|
|
1333
1631
|
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8")]);
|
|
1334
1632
|
let newlineIndex;
|
|
1335
1633
|
while ((newlineIndex = buffer.indexOf(10)) !== -1) {
|
|
@@ -1340,14 +1638,44 @@ function createOpenCodeActivityForwarder(getContext) {
|
|
|
1340
1638
|
flushLine(buffer.toString("utf-8"));
|
|
1341
1639
|
buffer = Buffer.alloc(0);
|
|
1342
1640
|
}
|
|
1641
|
+
});
|
|
1642
|
+
forward.flush = () => {
|
|
1643
|
+
for (const state of repeatStates.values()) {
|
|
1644
|
+
if (state.repeatsSuppressed > 0) {
|
|
1645
|
+
forwardLine(state.line, state.parsed, state.repeatsSuppressed);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
repeatStates.clear();
|
|
1343
1649
|
};
|
|
1650
|
+
return forward;
|
|
1344
1651
|
}
|
|
1345
1652
|
var MAX_MESSAGE_LENGTH = 500;
|
|
1346
1653
|
var MAX_METADATA_VALUE_LENGTH = 200;
|
|
1347
1654
|
var MAX_METADATA_ENTRIES = 20;
|
|
1348
1655
|
var TRUNCATION_MARKER = "\u2026";
|
|
1349
|
-
function
|
|
1350
|
-
|
|
1656
|
+
function redactHomeDirectory(message) {
|
|
1657
|
+
const home = homedir2();
|
|
1658
|
+
if (!home || home === "/") {
|
|
1659
|
+
return message;
|
|
1660
|
+
}
|
|
1661
|
+
const pathSegmentCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-";
|
|
1662
|
+
let result = "";
|
|
1663
|
+
let searchFrom = 0;
|
|
1664
|
+
let homeIndex;
|
|
1665
|
+
while ((homeIndex = message.indexOf(home, searchFrom)) !== -1) {
|
|
1666
|
+
const homeEnd = homeIndex + home.length;
|
|
1667
|
+
const previousCharacter = message[homeIndex - 1];
|
|
1668
|
+
const nextCharacter = message[homeEnd];
|
|
1669
|
+
const atPathBoundary = home === "/" || (!previousCharacter || !pathSegmentCharacters.includes(previousCharacter)) && (!nextCharacter || !pathSegmentCharacters.includes(nextCharacter));
|
|
1670
|
+
result += message.slice(searchFrom, homeIndex);
|
|
1671
|
+
result += atPathBoundary ? "~" : home;
|
|
1672
|
+
searchFrom = homeEnd;
|
|
1673
|
+
}
|
|
1674
|
+
return result + message.slice(searchFrom);
|
|
1675
|
+
}
|
|
1676
|
+
function redactDiagnosticText(message) {
|
|
1677
|
+
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>");
|
|
1678
|
+
return redactHomeDirectory(redacted);
|
|
1351
1679
|
}
|
|
1352
1680
|
function truncate(message) {
|
|
1353
1681
|
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
@@ -1357,14 +1685,17 @@ function sanitiseMetadata(metadata) {
|
|
|
1357
1685
|
if (!metadata) return {};
|
|
1358
1686
|
const entries = Object.entries(metadata).slice(0, MAX_METADATA_ENTRIES);
|
|
1359
1687
|
if (Object.keys(metadata).length > entries.length) {
|
|
1360
|
-
|
|
1361
|
-
`
|
|
1688
|
+
reportTelemetryDiagnostic(
|
|
1689
|
+
`Runner activity telemetry dropped ${Object.keys(metadata).length - entries.length} metadata entries; those entries were not sent`
|
|
1362
1690
|
);
|
|
1363
1691
|
}
|
|
1364
1692
|
const sanitised = [];
|
|
1365
1693
|
for (const [key, value] of entries) {
|
|
1366
1694
|
if (typeof value === "string") {
|
|
1367
|
-
sanitised.push([
|
|
1695
|
+
sanitised.push([
|
|
1696
|
+
key,
|
|
1697
|
+
truncate(redactDiagnosticText(value).slice(0, MAX_METADATA_VALUE_LENGTH))
|
|
1698
|
+
]);
|
|
1368
1699
|
} else if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
|
|
1369
1700
|
sanitised.push([key, value]);
|
|
1370
1701
|
}
|
|
@@ -1382,8 +1713,8 @@ function admitUnderRateLimit(source, now) {
|
|
|
1382
1713
|
}
|
|
1383
1714
|
if (now - window.windowStartedAt >= RATE_LIMIT_WINDOW_MS) {
|
|
1384
1715
|
if (window.windowDroppedCount > 0) {
|
|
1385
|
-
|
|
1386
|
-
`
|
|
1716
|
+
reportTelemetryDiagnostic(
|
|
1717
|
+
`Runner activity telemetry rate cap reached: dropped ${window.windowDroppedCount} ${window.windowDroppedCount === 1 ? "entry" : "entries"} in the last ${RATE_LIMIT_WINDOW_MS / 1e3}s (limit ${RATE_LIMIT_MAX_EVENTS}/min); those entries were not sent for source "${source}"`
|
|
1387
1718
|
);
|
|
1388
1719
|
}
|
|
1389
1720
|
window.windowStartedAt = now;
|
|
@@ -1393,8 +1724,8 @@ function admitUnderRateLimit(source, now) {
|
|
|
1393
1724
|
if (window.windowCount >= RATE_LIMIT_MAX_EVENTS) {
|
|
1394
1725
|
window.windowDroppedCount++;
|
|
1395
1726
|
if (window.windowDroppedCount === 1) {
|
|
1396
|
-
|
|
1397
|
-
`
|
|
1727
|
+
reportTelemetryDiagnostic(
|
|
1728
|
+
`Runner activity telemetry rate cap reached (${RATE_LIMIT_MAX_EVENTS} entries per ${RATE_LIMIT_WINDOW_MS / 1e3}s limit; further entries were not sent for source "${source}"`
|
|
1398
1729
|
);
|
|
1399
1730
|
}
|
|
1400
1731
|
return false;
|
|
@@ -1404,12 +1735,12 @@ function admitUnderRateLimit(source, now) {
|
|
|
1404
1735
|
}
|
|
1405
1736
|
function forwardRunnerActivity(entry, context) {
|
|
1406
1737
|
try {
|
|
1407
|
-
if (!FORWARDED_LEVELS.has(entry.level)) return;
|
|
1408
|
-
if (!context.agentId || !context.authHeader) return;
|
|
1409
1738
|
const source = entry.source ?? "cli.run";
|
|
1739
|
+
if (!FORWARDED_LEVELS_BY_SOURCE[source].has(entry.level)) return;
|
|
1740
|
+
if (!context.agentId || !context.authHeader) return;
|
|
1410
1741
|
if (!admitUnderRateLimit(source, Date.now())) return;
|
|
1411
1742
|
const rawMessage = entry.error ?? entry.message ?? "";
|
|
1412
|
-
const message = truncate(
|
|
1743
|
+
const message = truncate(redactDiagnosticText(rawMessage));
|
|
1413
1744
|
logEvent(TelemetryEventTypes.RUNNER_ACTIVITY, {
|
|
1414
1745
|
severity: SEVERITY_BY_LEVEL[entry.level],
|
|
1415
1746
|
message,
|
|
@@ -1417,8 +1748,8 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1417
1748
|
agentId: context.agentId
|
|
1418
1749
|
});
|
|
1419
1750
|
} catch (err) {
|
|
1420
|
-
|
|
1421
|
-
`
|
|
1751
|
+
reportTelemetryDiagnostic(
|
|
1752
|
+
`Runner activity telemetry failed to forward runner activity: ${err instanceof Error ? err.message : String(err)}`
|
|
1422
1753
|
);
|
|
1423
1754
|
}
|
|
1424
1755
|
}
|
|
@@ -1620,6 +1951,9 @@ async function checkOpenCodeHealth(port) {
|
|
|
1620
1951
|
signal: AbortSignal.timeout(2e3)
|
|
1621
1952
|
// 2 second timeout
|
|
1622
1953
|
});
|
|
1954
|
+
if (response.status === 401) {
|
|
1955
|
+
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
1956
|
+
}
|
|
1623
1957
|
if (!response.ok) {
|
|
1624
1958
|
return { healthy: false, error: `HTTP ${response.status}` };
|
|
1625
1959
|
}
|
|
@@ -1684,7 +2018,7 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
|
1684
2018
|
const startTime = Date.now();
|
|
1685
2019
|
while (Date.now() - startTime < timeoutMs) {
|
|
1686
2020
|
const health = await checkOpenCodeHealth(port);
|
|
1687
|
-
if (health.healthy) {
|
|
2021
|
+
if (health.healthy || health.authFailed) {
|
|
1688
2022
|
return health;
|
|
1689
2023
|
}
|
|
1690
2024
|
await new Promise((resolve4) => setTimeout(resolve4, 1e3));
|
|
@@ -1705,8 +2039,46 @@ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
|
|
|
1705
2039
|
|
|
1706
2040
|
// src/lib/http-timeout.ts
|
|
1707
2041
|
var REQUEST_TIMEOUT_MS = 6e4;
|
|
2042
|
+
var RequestTimeoutError = class extends Error {
|
|
2043
|
+
constructor(operation, timeoutMs, elapsedMs) {
|
|
2044
|
+
super(`${operation} timed out after ${elapsedMs}ms (limit ${timeoutMs}ms)`);
|
|
2045
|
+
this.operation = operation;
|
|
2046
|
+
this.timeoutMs = timeoutMs;
|
|
2047
|
+
this.elapsedMs = elapsedMs;
|
|
2048
|
+
this.name = "TimeoutError";
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
function describeRequestOperation(input, init) {
|
|
2052
|
+
const method = (init?.method ?? input.method ?? "GET").toUpperCase();
|
|
2053
|
+
const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
2054
|
+
let path;
|
|
2055
|
+
try {
|
|
2056
|
+
path = new URL(rawUrl).pathname;
|
|
2057
|
+
} catch (error2) {
|
|
2058
|
+
if (!(error2 instanceof TypeError)) throw error2;
|
|
2059
|
+
path = rawUrl.split(/[?#]/, 1)[0];
|
|
2060
|
+
}
|
|
2061
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2062
|
+
const prefixedIdentifier = /^[a-z]+_[A-Za-z0-9_-]{10,}$/;
|
|
2063
|
+
const maskedPath = path.split("/").map((segment) => uuid.test(segment) || prefixedIdentifier.test(segment) ? ":id" : segment).join("/");
|
|
2064
|
+
return `${method} ${maskedPath}`;
|
|
2065
|
+
}
|
|
1708
2066
|
function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
1709
|
-
return ((input, init) =>
|
|
2067
|
+
return (async (input, init) => {
|
|
2068
|
+
const startedAt = Date.now();
|
|
2069
|
+
try {
|
|
2070
|
+
return await fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
2071
|
+
} catch (error2) {
|
|
2072
|
+
if (error2?.name === "TimeoutError") {
|
|
2073
|
+
throw new RequestTimeoutError(
|
|
2074
|
+
describeRequestOperation(input, init),
|
|
2075
|
+
timeoutMs,
|
|
2076
|
+
Date.now() - startedAt
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
throw error2;
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
1710
2082
|
}
|
|
1711
2083
|
|
|
1712
2084
|
// src/lib/opencode/client.ts
|
|
@@ -1860,6 +2232,7 @@ function isSessionActivelyGenerating(messages) {
|
|
|
1860
2232
|
if (roleOf(last) !== "assistant") return false;
|
|
1861
2233
|
return completedOf(last) == null;
|
|
1862
2234
|
}
|
|
2235
|
+
var FULL_SESSION_ENUMERATION_MAX_SESSIONS = 3200;
|
|
1863
2236
|
function sessionLastActivityMs(session) {
|
|
1864
2237
|
const candidates = [
|
|
1865
2238
|
session.time?.updated,
|
|
@@ -1874,30 +2247,60 @@ function sessionLastActivityMs(session) {
|
|
|
1874
2247
|
}
|
|
1875
2248
|
return null;
|
|
1876
2249
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
2250
|
+
function sessionListPath(options) {
|
|
2251
|
+
const params = new URLSearchParams();
|
|
2252
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
2253
|
+
const query = params.toString();
|
|
2254
|
+
return query ? `/session?${query}` : "/session";
|
|
2255
|
+
}
|
|
2256
|
+
async function listSessionPage(port, client, options) {
|
|
2257
|
+
const path = sessionListPath(options);
|
|
1879
2258
|
try {
|
|
1880
|
-
const res = await requestWithClient(port, client,
|
|
2259
|
+
const res = await requestWithClient(port, client, path);
|
|
1881
2260
|
if (!res.ok) return null;
|
|
1882
2261
|
const body = await res.json();
|
|
1883
2262
|
return Array.isArray(body) ? body : null;
|
|
1884
2263
|
} catch (error2) {
|
|
1885
2264
|
console.error(
|
|
1886
|
-
`[
|
|
2265
|
+
`[listSessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1887
2266
|
);
|
|
1888
2267
|
return null;
|
|
1889
2268
|
}
|
|
1890
2269
|
}
|
|
2270
|
+
async function listSessions(port, client, options) {
|
|
2271
|
+
if (client?.version === "v2") return listV2Sessions(client);
|
|
2272
|
+
return listSessionPage(port, client, options);
|
|
2273
|
+
}
|
|
2274
|
+
async function listAllSessions(port, client) {
|
|
2275
|
+
if (client?.version === "v2") {
|
|
2276
|
+
const sessions2 = await listV2Sessions(client);
|
|
2277
|
+
return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
|
|
2278
|
+
}
|
|
2279
|
+
const sessions = await listSessionPage(port, client, {
|
|
2280
|
+
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
|
|
2281
|
+
});
|
|
2282
|
+
if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
|
|
2283
|
+
if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
|
|
2284
|
+
return { sessions: null, reason: "session-enumeration-limit-bound" };
|
|
2285
|
+
}
|
|
2286
|
+
return { sessions };
|
|
2287
|
+
}
|
|
1891
2288
|
async function deleteSession(port, id, client) {
|
|
1892
2289
|
if (client?.version === "v2") return deleteV2Session(client, id);
|
|
1893
2290
|
try {
|
|
1894
|
-
const res = await requestWithClient(
|
|
1895
|
-
|
|
2291
|
+
const res = await requestWithClient(
|
|
2292
|
+
port,
|
|
2293
|
+
client,
|
|
2294
|
+
`/session/${id}`,
|
|
2295
|
+
{ method: "DELETE" },
|
|
2296
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
2297
|
+
);
|
|
2298
|
+
return { ok: res.status >= 200 && res.status < 300, status: res.status };
|
|
1896
2299
|
} catch (error2) {
|
|
1897
2300
|
console.error(
|
|
1898
2301
|
`[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1899
2302
|
);
|
|
1900
|
-
return false;
|
|
2303
|
+
return { ok: false, status: "threw" };
|
|
1901
2304
|
}
|
|
1902
2305
|
}
|
|
1903
2306
|
async function sessionExists(port, id, client) {
|
|
@@ -3030,13 +3433,17 @@ async function listV2Sessions(client) {
|
|
|
3030
3433
|
}
|
|
3031
3434
|
async function deleteV2Session(client, sessionId) {
|
|
3032
3435
|
try {
|
|
3033
|
-
await client.request(
|
|
3034
|
-
|
|
3436
|
+
const response = await client.request(
|
|
3437
|
+
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
3438
|
+
{ method: "DELETE" },
|
|
3439
|
+
{ allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100) }
|
|
3440
|
+
);
|
|
3441
|
+
return { ok: response.status >= 200 && response.status < 300, status: response.status };
|
|
3035
3442
|
} catch (error2) {
|
|
3036
3443
|
console.error(
|
|
3037
3444
|
`[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3038
3445
|
);
|
|
3039
|
-
return false;
|
|
3446
|
+
return { ok: false, status: "threw" };
|
|
3040
3447
|
}
|
|
3041
3448
|
}
|
|
3042
3449
|
async function v2SessionExists(client, sessionId) {
|
|
@@ -3211,7 +3618,7 @@ import { join as join4 } from "node:path";
|
|
|
3211
3618
|
|
|
3212
3619
|
// src/lib/openai-usage.ts
|
|
3213
3620
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
3214
|
-
import { homedir as
|
|
3621
|
+
import { homedir as homedir3 } from "node:os";
|
|
3215
3622
|
import { join as join3 } from "node:path";
|
|
3216
3623
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
3217
3624
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
@@ -3226,7 +3633,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
3226
3633
|
}
|
|
3227
3634
|
function readOpenCodeChatGptCredentials() {
|
|
3228
3635
|
try {
|
|
3229
|
-
const raw = readFileSync3(join3(
|
|
3636
|
+
const raw = readFileSync3(join3(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
3230
3637
|
let parsed;
|
|
3231
3638
|
try {
|
|
3232
3639
|
parsed = JSON.parse(raw);
|
|
@@ -3367,16 +3774,17 @@ function hasPrimaryHeaders(headers) {
|
|
|
3367
3774
|
].some((name) => headers.has(name));
|
|
3368
3775
|
}
|
|
3369
3776
|
async function getOpenAiUsage(port, client) {
|
|
3777
|
+
const openCodeVersion = client?.version ?? "v1";
|
|
3370
3778
|
const credentials2 = readOpenCodeChatGptCredentials();
|
|
3371
3779
|
if (!credentials2) {
|
|
3372
3780
|
throw new OpenAiUsageError(
|
|
3373
|
-
"No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
|
|
3781
|
+
openCodeVersion === "v2" ? "OpenAI usage reporting is not available on OpenCode V2 because V2 keeps a connected ChatGPT login in a store Evident cannot read." : "No ChatGPT login found. Connect a ChatGPT account to this runner, or run `opencode auth login`.",
|
|
3374
3782
|
"no_credentials"
|
|
3375
3783
|
);
|
|
3376
3784
|
}
|
|
3377
3785
|
if (credentials2.expiresAt < Date.now()) {
|
|
3378
3786
|
throw new OpenAiUsageError(
|
|
3379
|
-
"ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
|
|
3787
|
+
openCodeVersion === "v2" ? "The delivered ChatGPT login has expired. OpenCode V2 cannot be given a replacement by file; connect the account again from the runner's page in Evident." : "ChatGPT credentials have expired. Run `opencode auth login` to refresh them.",
|
|
3380
3788
|
"credentials_expired"
|
|
3381
3789
|
);
|
|
3382
3790
|
}
|
|
@@ -3657,9 +4065,9 @@ async function importOpenCodeV2Credentials({
|
|
|
3657
4065
|
|
|
3658
4066
|
// src/lib/opencode/session-db-boot.ts
|
|
3659
4067
|
import { spawn as spawn2 } from "node:child_process";
|
|
3660
|
-
import { mkdirSync, statSync as
|
|
3661
|
-
import { homedir as
|
|
3662
|
-
import { dirname as
|
|
4068
|
+
import { mkdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
|
|
4069
|
+
import { homedir as homedir4 } from "node:os";
|
|
4070
|
+
import { dirname as dirname3, resolve as resolvePath } from "node:path";
|
|
3663
4071
|
|
|
3664
4072
|
// src/lib/runner-synchroniser.ts
|
|
3665
4073
|
import { spawn } from "node:child_process";
|
|
@@ -3784,7 +4192,7 @@ function clearMarker(options) {
|
|
|
3784
4192
|
function markNoReplicate(options, message) {
|
|
3785
4193
|
if (options.noReplicateMarker) {
|
|
3786
4194
|
try {
|
|
3787
|
-
mkdirSync(
|
|
4195
|
+
mkdirSync(dirname3(options.noReplicateMarker), { recursive: true });
|
|
3788
4196
|
writeFileSync(options.noReplicateMarker, "");
|
|
3789
4197
|
} catch (error2) {
|
|
3790
4198
|
options.log(
|
|
@@ -3909,7 +4317,7 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
3909
4317
|
return null;
|
|
3910
4318
|
}
|
|
3911
4319
|
try {
|
|
3912
|
-
if (
|
|
4320
|
+
if (statSync3(configPath).size > 0) return configPath;
|
|
3913
4321
|
} catch (error2) {
|
|
3914
4322
|
if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")) {
|
|
3915
4323
|
options.log(
|
|
@@ -3939,7 +4347,7 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
3939
4347
|
return null;
|
|
3940
4348
|
}
|
|
3941
4349
|
try {
|
|
3942
|
-
mkdirSync(
|
|
4350
|
+
mkdirSync(dirname3(configPath), { recursive: true });
|
|
3943
4351
|
writeFileSync(configPath, rendered.stdout);
|
|
3944
4352
|
} catch (error2) {
|
|
3945
4353
|
options.log(
|
|
@@ -4014,14 +4422,14 @@ async function restoreSessionDb(options, configPath, env) {
|
|
|
4014
4422
|
case 0:
|
|
4015
4423
|
return;
|
|
4016
4424
|
case 31:
|
|
4017
|
-
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(
|
|
4425
|
+
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
|
|
4018
4426
|
options.log(
|
|
4019
4427
|
"SESSION-DB-REPLICA-UNUSABLE: booting with a fresh opencode.db and replicating into the existing prefix this boot",
|
|
4020
4428
|
"warn"
|
|
4021
4429
|
);
|
|
4022
4430
|
return;
|
|
4023
4431
|
case 32:
|
|
4024
|
-
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(
|
|
4432
|
+
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
|
|
4025
4433
|
discardSessionDbDebris(options);
|
|
4026
4434
|
markNoReplicate(
|
|
4027
4435
|
options,
|
|
@@ -4100,7 +4508,7 @@ async function verifySessionDb(options, configPath, env) {
|
|
|
4100
4508
|
}
|
|
4101
4509
|
function fileExists(path) {
|
|
4102
4510
|
try {
|
|
4103
|
-
|
|
4511
|
+
statSync3(path);
|
|
4104
4512
|
return true;
|
|
4105
4513
|
} catch (error2) {
|
|
4106
4514
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return false;
|
|
@@ -4110,7 +4518,7 @@ function fileExists(path) {
|
|
|
4110
4518
|
async function restoreAndVerifySessionDb(options) {
|
|
4111
4519
|
const env = options.env ?? process.env;
|
|
4112
4520
|
clearMarker(options);
|
|
4113
|
-
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(
|
|
4521
|
+
acknowledgeSessionDbRecoveryReport(sessionDbRecoveryReportPath(homedir4(), env));
|
|
4114
4522
|
const synchroniserEnv = await runSynchroniser(["env"], {
|
|
4115
4523
|
timeoutMs: SESSION_DB_SYNCHRONISER_TIMEOUT_MS,
|
|
4116
4524
|
env
|
|
@@ -4189,7 +4597,7 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
4189
4597
|
// src/lib/opencode/session-db-provenance.ts
|
|
4190
4598
|
import { createRequire } from "node:module";
|
|
4191
4599
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4192
|
-
import { dirname as
|
|
4600
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
4193
4601
|
var require2 = createRequire(import.meta.url);
|
|
4194
4602
|
function readSessionDbMigrationIds(dbPath) {
|
|
4195
4603
|
let db;
|
|
@@ -4255,7 +4663,7 @@ function loadSessionDbProvenanceState(path) {
|
|
|
4255
4663
|
}
|
|
4256
4664
|
function saveSessionDbProvenanceState(path, state) {
|
|
4257
4665
|
try {
|
|
4258
|
-
mkdirSync2(
|
|
4666
|
+
mkdirSync2(dirname4(path), { recursive: true });
|
|
4259
4667
|
writeFileSync2(path, `${JSON.stringify(state, null, 2)}
|
|
4260
4668
|
`, "utf8");
|
|
4261
4669
|
} catch (error2) {
|
|
@@ -4311,6 +4719,9 @@ function isSessionDbProvenanceRecord(value) {
|
|
|
4311
4719
|
return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
|
|
4312
4720
|
}
|
|
4313
4721
|
|
|
4722
|
+
// src/lib/opencode/opencode2-package.ts
|
|
4723
|
+
var OPENCODE2_PACKAGE_SPEC = "@opencode-ai/cli@0.0.0-beta-17823";
|
|
4724
|
+
|
|
4314
4725
|
// src/lib/opencode/opencode-version-gate.ts
|
|
4315
4726
|
var QUEUE_VALIDATED_OPENCODE_VERSIONS = ["1.17.11", "1.18.3"];
|
|
4316
4727
|
function isQueueValidatedVersion(version2) {
|
|
@@ -4487,12 +4898,15 @@ function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
|
4487
4898
|
}
|
|
4488
4899
|
} catch {
|
|
4489
4900
|
try {
|
|
4490
|
-
const psOutput = execSync(
|
|
4901
|
+
const psOutput = execSync("ps aux", {
|
|
4491
4902
|
encoding: "utf-8",
|
|
4492
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4903
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
4904
|
+
// The whole table reaches Node; the 1 MiB default would surface a spurious failure.
|
|
4905
|
+
maxBuffer: 16 * 1024 * 1024
|
|
4493
4906
|
}).trim();
|
|
4494
4907
|
if (psOutput) {
|
|
4495
4908
|
for (const line of psOutput.split("\n")) {
|
|
4909
|
+
if (!psPattern.test(line)) continue;
|
|
4496
4910
|
const parts = line.trim().split(/\s+/);
|
|
4497
4911
|
if (parts.length >= 2) {
|
|
4498
4912
|
const pid = parseInt(parts[1], 10);
|
|
@@ -4501,8 +4915,9 @@ function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
|
4501
4915
|
}
|
|
4502
4916
|
}
|
|
4503
4917
|
} catch (err) {
|
|
4918
|
+
const reason = (typeof err === "object" && err !== null && "stderr" in err && typeof err.stderr === "string" ? err.stderr : "").split("\n").map((line) => line.trim()).find(Boolean) ?? (err instanceof Error ? err.message : String(err)).split("\n").map((line) => line.trim()).find(Boolean) ?? "no error detail available";
|
|
4504
4919
|
console.warn(
|
|
4505
|
-
`
|
|
4920
|
+
`Could not list running processes, so an already-running OpenCode may not be detected: ${reason}`
|
|
4506
4921
|
);
|
|
4507
4922
|
}
|
|
4508
4923
|
}
|
|
@@ -4534,7 +4949,7 @@ function findProcessesByPattern(pgrepPattern, psPattern) {
|
|
|
4534
4949
|
return instances;
|
|
4535
4950
|
}
|
|
4536
4951
|
function findOpenCodeProcesses() {
|
|
4537
|
-
return findProcessesByPattern("opencode serve|opencode-serve",
|
|
4952
|
+
return findProcessesByPattern("opencode serve|opencode-serve", /opencode (serve|--port)/);
|
|
4538
4953
|
}
|
|
4539
4954
|
async function scanPortsForOpenCode() {
|
|
4540
4955
|
const instances = [];
|
|
@@ -4601,7 +5016,8 @@ async function startOpenCode(port, options = {}) {
|
|
|
4601
5016
|
const child = spawn3(command, args, {
|
|
4602
5017
|
detached: true,
|
|
4603
5018
|
stdio: options.inheritStdio ? "inherit" : "ignore",
|
|
4604
|
-
cwd: process.cwd()
|
|
5019
|
+
cwd: process.cwd(),
|
|
5020
|
+
env: { ...process.env, OPENCODE_SERVER_PASSWORD: void 0 }
|
|
4605
5021
|
});
|
|
4606
5022
|
return child;
|
|
4607
5023
|
}
|
|
@@ -4617,7 +5033,7 @@ async function startOpenCode2(port, options = {}) {
|
|
|
4617
5033
|
args = [
|
|
4618
5034
|
"-y",
|
|
4619
5035
|
"-p",
|
|
4620
|
-
|
|
5036
|
+
OPENCODE2_PACKAGE_SPEC,
|
|
4621
5037
|
"--",
|
|
4622
5038
|
"opencode2",
|
|
4623
5039
|
"serve",
|
|
@@ -4694,7 +5110,7 @@ async function promptOpenCodeInstall(interactive) {
|
|
|
4694
5110
|
npm: "npm install -g opencode-ai",
|
|
4695
5111
|
curl: "curl -fsSL https://opencode.ai/install.sh | sh",
|
|
4696
5112
|
v2: {
|
|
4697
|
-
npm:
|
|
5113
|
+
npm: `npm install -g ${OPENCODE2_PACKAGE_SPEC}`,
|
|
4698
5114
|
curl: "curl -fsSL https://opencode.ai/v2/install | bash"
|
|
4699
5115
|
}
|
|
4700
5116
|
}
|
|
@@ -4828,7 +5244,10 @@ function selectSessionsToDelete(sessions, opts) {
|
|
|
4828
5244
|
toDelete.push(s.id);
|
|
4829
5245
|
}
|
|
4830
5246
|
}
|
|
4831
|
-
|
|
5247
|
+
const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
|
|
5248
|
+
return toDelete.sort(
|
|
5249
|
+
(a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
|
|
5250
|
+
);
|
|
4832
5251
|
}
|
|
4833
5252
|
var DEFAULT_INTERVAL = "1h";
|
|
4834
5253
|
function resolve(flag, envValue, fallback) {
|
|
@@ -4888,13 +5307,13 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
4888
5307
|
}
|
|
4889
5308
|
|
|
4890
5309
|
// src/lib/opencode/session-db-size.ts
|
|
4891
|
-
import { statSync as
|
|
5310
|
+
import { statSync as statSync4 } from "node:fs";
|
|
4892
5311
|
import { join as join6 } from "node:path";
|
|
4893
5312
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
4894
5313
|
function statSessionDbBytes(homeDir) {
|
|
4895
5314
|
const dbPath = join6(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
4896
5315
|
try {
|
|
4897
|
-
return
|
|
5316
|
+
return statSync4(dbPath).size;
|
|
4898
5317
|
} catch (err) {
|
|
4899
5318
|
const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
4900
5319
|
if (!isMissingFile) {
|
|
@@ -4920,12 +5339,12 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
4920
5339
|
}
|
|
4921
5340
|
|
|
4922
5341
|
// src/lib/opencode/log-tail.ts
|
|
4923
|
-
import { statSync as
|
|
4924
|
-
import { homedir as
|
|
5342
|
+
import { statSync as statSync5 } from "node:fs";
|
|
5343
|
+
import { homedir as homedir5 } from "node:os";
|
|
4925
5344
|
import { join as join7 } from "node:path";
|
|
4926
5345
|
import { open as open2, stat } from "node:fs/promises";
|
|
4927
5346
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
4928
|
-
function resolveOpenCodeLogPath(homeDir =
|
|
5347
|
+
function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
|
|
4929
5348
|
const dataDir = env.XDG_DATA_HOME || join7(homeDir, ".local", "share");
|
|
4930
5349
|
return join7(dataDir, "opencode", "log", "opencode.log");
|
|
4931
5350
|
}
|
|
@@ -4943,7 +5362,7 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
|
4943
5362
|
let inode = null;
|
|
4944
5363
|
let baselineReady = true;
|
|
4945
5364
|
try {
|
|
4946
|
-
const initial =
|
|
5365
|
+
const initial = statSync5(logPath);
|
|
4947
5366
|
offset = initial.size;
|
|
4948
5367
|
inode = initial.ino;
|
|
4949
5368
|
} catch (error2) {
|
|
@@ -5002,125 +5421,6 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
|
5002
5421
|
};
|
|
5003
5422
|
}
|
|
5004
5423
|
|
|
5005
|
-
// src/lib/opencode/session-db-reclaim.ts
|
|
5006
|
-
import { statSync as statSync5, statfsSync } from "node:fs";
|
|
5007
|
-
import { dirname as dirname4 } from "node:path";
|
|
5008
|
-
function errorMessage(error2) {
|
|
5009
|
-
if (!(error2 instanceof Error)) return String(error2);
|
|
5010
|
-
return error2.cause instanceof Error ? error2.cause.message : error2.message;
|
|
5011
|
-
}
|
|
5012
|
-
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
5013
|
-
try {
|
|
5014
|
-
const fsStats = statfsSync(dirname4(dbPath));
|
|
5015
|
-
const availableBytes = fsStats.bavail * fsStats.bsize;
|
|
5016
|
-
if (availableBytes < requiredBytes) {
|
|
5017
|
-
return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
|
|
5018
|
-
}
|
|
5019
|
-
return null;
|
|
5020
|
-
} catch (err) {
|
|
5021
|
-
return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
|
|
5022
|
-
}
|
|
5023
|
-
}
|
|
5024
|
-
function readLogicalBytes(db) {
|
|
5025
|
-
const pageCount = db.prepare("PRAGMA page_count").get().page_count;
|
|
5026
|
-
const pageSize = db.prepare("PRAGMA page_size").get().page_size;
|
|
5027
|
-
return pageCount * pageSize;
|
|
5028
|
-
}
|
|
5029
|
-
function readCheckpointResult(db) {
|
|
5030
|
-
const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
5031
|
-
return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
|
|
5032
|
-
}
|
|
5033
|
-
async function probeReclaimAvailability(input) {
|
|
5034
|
-
const { dbPath, requiredBytes } = input;
|
|
5035
|
-
let sqlite;
|
|
5036
|
-
try {
|
|
5037
|
-
sqlite = await import("node:sqlite");
|
|
5038
|
-
} catch (err) {
|
|
5039
|
-
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
5040
|
-
console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
|
|
5041
|
-
return { reason: "sqlite-unavailable", detail };
|
|
5042
|
-
}
|
|
5043
|
-
let autoVacuum = null;
|
|
5044
|
-
try {
|
|
5045
|
-
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
5046
|
-
try {
|
|
5047
|
-
db.exec("PRAGMA busy_timeout=5000");
|
|
5048
|
-
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
5049
|
-
} finally {
|
|
5050
|
-
db.close();
|
|
5051
|
-
}
|
|
5052
|
-
} catch (err) {
|
|
5053
|
-
console.warn(
|
|
5054
|
-
`[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
5055
|
-
);
|
|
5056
|
-
}
|
|
5057
|
-
if (autoVacuum !== 0) return null;
|
|
5058
|
-
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
5059
|
-
}
|
|
5060
|
-
async function reclaimSessionDbSpace(input) {
|
|
5061
|
-
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
5062
|
-
let sqlite;
|
|
5063
|
-
try {
|
|
5064
|
-
sqlite = await import("node:sqlite");
|
|
5065
|
-
} catch (err) {
|
|
5066
|
-
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
5067
|
-
console.warn(
|
|
5068
|
-
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
|
|
5069
|
-
);
|
|
5070
|
-
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
5071
|
-
}
|
|
5072
|
-
const { DatabaseSync } = sqlite;
|
|
5073
|
-
let db;
|
|
5074
|
-
try {
|
|
5075
|
-
db = new DatabaseSync(dbPath);
|
|
5076
|
-
db.exec("PRAGMA busy_timeout=5000");
|
|
5077
|
-
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
5078
|
-
if (autoVacuum === 0) {
|
|
5079
|
-
if (!allowFullVacuum) {
|
|
5080
|
-
console.warn(
|
|
5081
|
-
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
|
|
5082
|
-
);
|
|
5083
|
-
return { ok: false, skipped: "full-vacuum-blocked" };
|
|
5084
|
-
}
|
|
5085
|
-
const fileBytesForGuard = statSync5(dbPath).size;
|
|
5086
|
-
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
5087
|
-
if (skipReason !== null) {
|
|
5088
|
-
console.warn(
|
|
5089
|
-
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
|
|
5090
|
-
);
|
|
5091
|
-
return { ok: false, skipped: "insufficient-disk-space" };
|
|
5092
|
-
}
|
|
5093
|
-
const beforeBytes = readLogicalBytes(db);
|
|
5094
|
-
db.exec("PRAGMA auto_vacuum=INCREMENTAL");
|
|
5095
|
-
db.exec("VACUUM");
|
|
5096
|
-
const afterBytes = readLogicalBytes(db);
|
|
5097
|
-
const checkpoint = readCheckpointResult(db);
|
|
5098
|
-
return { ok: true, mode: "convert", beforeBytes, afterBytes, checkpoint };
|
|
5099
|
-
}
|
|
5100
|
-
if (autoVacuum === 2) {
|
|
5101
|
-
const beforeBytes = readLogicalBytes(db);
|
|
5102
|
-
const bound = Math.max(0, Math.trunc(maxPages));
|
|
5103
|
-
db.exec(`PRAGMA incremental_vacuum(${bound})`);
|
|
5104
|
-
const afterBytes = readLogicalBytes(db);
|
|
5105
|
-
const checkpoint = readCheckpointResult(db);
|
|
5106
|
-
return { ok: true, mode: "incremental", beforeBytes, afterBytes, checkpoint };
|
|
5107
|
-
}
|
|
5108
|
-
console.warn(
|
|
5109
|
-
`[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
|
|
5110
|
-
);
|
|
5111
|
-
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
5112
|
-
} catch (err) {
|
|
5113
|
-
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
5114
|
-
return {
|
|
5115
|
-
ok: false,
|
|
5116
|
-
skipped: "reclaim-error",
|
|
5117
|
-
detail: errorMessage(err)
|
|
5118
|
-
};
|
|
5119
|
-
} finally {
|
|
5120
|
-
db?.close();
|
|
5121
|
-
}
|
|
5122
|
-
}
|
|
5123
|
-
|
|
5124
5424
|
// src/lib/tunnel/connection.ts
|
|
5125
5425
|
import WebSocket2 from "ws";
|
|
5126
5426
|
|
|
@@ -5350,6 +5650,8 @@ var StreamForwarder = class {
|
|
|
5350
5650
|
if (done) break;
|
|
5351
5651
|
const chunk = Buffer.from(value);
|
|
5352
5652
|
for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
|
|
5653
|
+
await stream.pausePromise;
|
|
5654
|
+
if (ac.signal.aborted) break;
|
|
5353
5655
|
const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
|
|
5354
5656
|
if (this.binaryFramesSupported) {
|
|
5355
5657
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
@@ -5392,6 +5694,7 @@ function classifyUpgradeRejection(headers) {
|
|
|
5392
5694
|
}
|
|
5393
5695
|
var MAX_RECONNECT_DELAY = 3e4;
|
|
5394
5696
|
var BASE_RECONNECT_DELAY = 500;
|
|
5697
|
+
var RELAY_SILENCE_TIMEOUT_MS = 9e4;
|
|
5395
5698
|
function getReconnectDelay(attempt) {
|
|
5396
5699
|
const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
|
|
5397
5700
|
const jitter = Math.random() * 1e3;
|
|
@@ -5463,10 +5766,27 @@ function connectTunnel(options) {
|
|
|
5463
5766
|
ws.close();
|
|
5464
5767
|
reject(new Error("Connection timeout"));
|
|
5465
5768
|
}, 3e4);
|
|
5769
|
+
let relaySilenceTimeout;
|
|
5770
|
+
let relaySilenceTimedOut = false;
|
|
5771
|
+
const clearRelaySilenceTimeout = () => {
|
|
5772
|
+
if (relaySilenceTimeout === void 0) return;
|
|
5773
|
+
clearTimeout(relaySilenceTimeout);
|
|
5774
|
+
relaySilenceTimeout = void 0;
|
|
5775
|
+
};
|
|
5776
|
+
const armRelaySilenceTimeout = () => {
|
|
5777
|
+
clearRelaySilenceTimeout();
|
|
5778
|
+
relaySilenceTimeout = setTimeout(() => {
|
|
5779
|
+
relaySilenceTimeout = void 0;
|
|
5780
|
+
relaySilenceTimedOut = true;
|
|
5781
|
+
ws.terminate();
|
|
5782
|
+
}, RELAY_SILENCE_TIMEOUT_MS);
|
|
5783
|
+
relaySilenceTimeout.unref?.();
|
|
5784
|
+
};
|
|
5466
5785
|
let upgradeRejection = null;
|
|
5467
5786
|
let upgradeRejectionReason = null;
|
|
5468
5787
|
ws.on("unexpected-response", (_req, res) => {
|
|
5469
5788
|
clearTimeout(connectionTimeout);
|
|
5789
|
+
clearRelaySilenceTimeout();
|
|
5470
5790
|
const reason = classifyUpgradeRejection(res.headers);
|
|
5471
5791
|
upgradeRejectionReason = reason;
|
|
5472
5792
|
const chunks = [];
|
|
@@ -5496,6 +5816,7 @@ function connectTunnel(options) {
|
|
|
5496
5816
|
onInfo?.("WebSocket connection established");
|
|
5497
5817
|
});
|
|
5498
5818
|
ws.on("message", (data, isBinary) => {
|
|
5819
|
+
if (relaySilenceTimeout !== void 0) armRelaySilenceTimeout();
|
|
5499
5820
|
if (isBinary) {
|
|
5500
5821
|
try {
|
|
5501
5822
|
const frame = decodeBinaryBodyFrame(toUint8Array(data));
|
|
@@ -5529,6 +5850,7 @@ function connectTunnel(options) {
|
|
|
5529
5850
|
switch (message.type) {
|
|
5530
5851
|
case "connected": {
|
|
5531
5852
|
clearTimeout(connectionTimeout);
|
|
5853
|
+
armRelaySilenceTimeout();
|
|
5532
5854
|
forwarder.setBinaryFramesSupported(message.binary_frames === true);
|
|
5533
5855
|
const connectedAgentId = message.agent_id ?? agentId;
|
|
5534
5856
|
onConnected?.(connectedAgentId);
|
|
@@ -5553,6 +5875,7 @@ function connectTunnel(options) {
|
|
|
5553
5875
|
});
|
|
5554
5876
|
ws.on("error", (error2) => {
|
|
5555
5877
|
clearTimeout(connectionTimeout);
|
|
5878
|
+
clearRelaySilenceTimeout();
|
|
5556
5879
|
const detail = upgradeRejection ?? describeSocketError(error2, url);
|
|
5557
5880
|
if (upgradeRejectionReason === "do_code_updated") {
|
|
5558
5881
|
onWarning?.("Relay redeployed \u2014 reconnecting");
|
|
@@ -5564,7 +5887,8 @@ function connectTunnel(options) {
|
|
|
5564
5887
|
);
|
|
5565
5888
|
});
|
|
5566
5889
|
ws.on("close", (code, reason) => {
|
|
5567
|
-
|
|
5890
|
+
clearRelaySilenceTimeout();
|
|
5891
|
+
const reasonStr = (relaySilenceTimedOut ? "no relay heartbeat for 90s \u2014 reconnecting" : void 0) || reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
|
|
5568
5892
|
forwarder.abortAll();
|
|
5569
5893
|
onDisconnected?.(code, reasonStr);
|
|
5570
5894
|
});
|
|
@@ -5572,9 +5896,10 @@ function connectTunnel(options) {
|
|
|
5572
5896
|
}
|
|
5573
5897
|
|
|
5574
5898
|
// src/lib/tunnel/runner-connection.ts
|
|
5575
|
-
var RunnerConnection = class {
|
|
5899
|
+
var RunnerConnection = class _RunnerConnection {
|
|
5576
5900
|
opts;
|
|
5577
5901
|
sleep;
|
|
5902
|
+
now;
|
|
5578
5903
|
connection = null;
|
|
5579
5904
|
resolvedAgentId;
|
|
5580
5905
|
/** True while a (re)connect loop is in flight. */
|
|
@@ -5583,10 +5908,17 @@ var RunnerConnection = class {
|
|
|
5583
5908
|
reconnectPromise = null;
|
|
5584
5909
|
/** 1-based count of the current reconnect attempt streak. */
|
|
5585
5910
|
reconnectAttempt = 0;
|
|
5911
|
+
generation = 0;
|
|
5912
|
+
disconnectedAtMs = null;
|
|
5913
|
+
static RELAY_RECONNECT_CLOSE_REASONS = /* @__PURE__ */ new Set([
|
|
5914
|
+
"No pong received",
|
|
5915
|
+
"Orphaned socket"
|
|
5916
|
+
]);
|
|
5586
5917
|
constructor(opts) {
|
|
5587
5918
|
this.opts = opts;
|
|
5588
5919
|
this.resolvedAgentId = opts.agentId;
|
|
5589
5920
|
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
5921
|
+
this.now = opts.now ?? Date.now;
|
|
5590
5922
|
}
|
|
5591
5923
|
get agentId() {
|
|
5592
5924
|
return this.resolvedAgentId;
|
|
@@ -5622,14 +5954,24 @@ var RunnerConnection = class {
|
|
|
5622
5954
|
port: this.opts.port,
|
|
5623
5955
|
openCodePassword: this.opts.openCodePassword,
|
|
5624
5956
|
onConnected: (agentId) => {
|
|
5957
|
+
const attempt = this.reconnectAttempt + 1;
|
|
5625
5958
|
this.reconnectAttempt = 0;
|
|
5626
5959
|
this.reconnecting = false;
|
|
5627
5960
|
this.resolvedAgentId = agentId;
|
|
5628
|
-
|
|
5961
|
+
this.generation++;
|
|
5962
|
+
const downtimeMs = this.disconnectedAtMs === null ? null : this.now() - this.disconnectedAtMs;
|
|
5963
|
+
this.disconnectedAtMs = null;
|
|
5964
|
+
events.onConnected(agentId, isReconnect, {
|
|
5965
|
+
generation: this.generation,
|
|
5966
|
+
attempt,
|
|
5967
|
+
downtimeMs
|
|
5968
|
+
});
|
|
5629
5969
|
},
|
|
5630
5970
|
onDisconnected: (code, reason) => {
|
|
5631
|
-
|
|
5632
|
-
|
|
5971
|
+
this.disconnectedAtMs = this.now();
|
|
5972
|
+
events.onDisconnected(code, reason, { generation: this.generation });
|
|
5973
|
+
const relayRequestedReconnect = code === 1e3 && _RunnerConnection.RELAY_RECONNECT_CLOSE_REASONS.has(reason);
|
|
5974
|
+
if (this.opts.isRunning() && (code !== 1e3 || relayRequestedReconnect) && !this.reconnecting) {
|
|
5633
5975
|
this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
|
|
5634
5976
|
events.onError?.(`Reconnection failed: ${err.message}`);
|
|
5635
5977
|
});
|
|
@@ -5968,8 +6310,85 @@ function createResourceUsageCollector(homeDir) {
|
|
|
5968
6310
|
};
|
|
5969
6311
|
}
|
|
5970
6312
|
|
|
6313
|
+
// src/lib/resource-usage-low-disk.ts
|
|
6314
|
+
var RUNNER_DISK_ALERT_ENTRY_FREE_BYTES = 4 * 1024 ** 3;
|
|
6315
|
+
var RUNNER_DISK_ALERT_ENTRY_FREE_FRACTION = 0.25;
|
|
6316
|
+
var RUNNER_DISK_ALERT_VERY_LOW_FREE_BYTES = 1 * 1024 ** 3;
|
|
6317
|
+
var RUNNER_DISK_ALERT_CRITICAL_FREE_BYTES = 256 * 1024 ** 2;
|
|
6318
|
+
var RUNNER_DISK_ALERT_RECOVERY_FREE_BYTES = 6 * 1024 ** 3;
|
|
6319
|
+
var RUNNER_DISK_ALERT_RECOVERY_FREE_FRACTION = 0.375;
|
|
6320
|
+
var LOCAL_DISK_WARNING_REASSERTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
6321
|
+
var BAND_RANK = {
|
|
6322
|
+
low: 1,
|
|
6323
|
+
very_low: 2,
|
|
6324
|
+
critical: 3
|
|
6325
|
+
};
|
|
6326
|
+
function bandFor(freeBytes) {
|
|
6327
|
+
if (freeBytes < RUNNER_DISK_ALERT_CRITICAL_FREE_BYTES) return "critical";
|
|
6328
|
+
if (freeBytes < RUNNER_DISK_ALERT_VERY_LOW_FREE_BYTES) return "very_low";
|
|
6329
|
+
return "low";
|
|
6330
|
+
}
|
|
6331
|
+
function formatBytes(bytes) {
|
|
6332
|
+
if (bytes === null) return "unavailable";
|
|
6333
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
6334
|
+
const unitIndex = Math.min(Math.floor(Math.log2(Math.max(bytes, 1)) / 10), units.length - 1);
|
|
6335
|
+
const value = bytes / 1024 ** unitIndex;
|
|
6336
|
+
const digits = unitIndex === 0 || value >= 100 ? 0 : value >= 10 ? 1 : 2;
|
|
6337
|
+
return `${value.toFixed(digits)} ${units[unitIndex]}`;
|
|
6338
|
+
}
|
|
6339
|
+
function warningMessage(band, diskFreeBytes, diskTotalBytes, opencodeDbBytes) {
|
|
6340
|
+
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.`;
|
|
6341
|
+
}
|
|
6342
|
+
function evaluateLowDiskWarning(input) {
|
|
6343
|
+
const { diskFreeBytes, diskTotalBytes, opencodeDbBytes, nowMs, state } = input;
|
|
6344
|
+
if (diskFreeBytes === null || diskTotalBytes === null || diskTotalBytes <= 0 || diskFreeBytes >= diskTotalBytes) {
|
|
6345
|
+
return { warning: null, state };
|
|
6346
|
+
}
|
|
6347
|
+
const recovered = diskFreeBytes >= RUNNER_DISK_ALERT_RECOVERY_FREE_BYTES || diskFreeBytes >= diskTotalBytes * RUNNER_DISK_ALERT_RECOVERY_FREE_FRACTION;
|
|
6348
|
+
if (recovered) {
|
|
6349
|
+
return { warning: null, state: { worstBand: null, warnedAtMs: null } };
|
|
6350
|
+
}
|
|
6351
|
+
const low = diskFreeBytes < RUNNER_DISK_ALERT_ENTRY_FREE_BYTES && diskFreeBytes < diskTotalBytes * RUNNER_DISK_ALERT_ENTRY_FREE_FRACTION;
|
|
6352
|
+
if (!low) return { warning: null, state };
|
|
6353
|
+
const currentBand = bandFor(diskFreeBytes);
|
|
6354
|
+
const currentRank = BAND_RANK[currentBand];
|
|
6355
|
+
const previousRank = state.worstBand === null ? 0 : BAND_RANK[state.worstBand];
|
|
6356
|
+
const worse = currentRank > previousRank;
|
|
6357
|
+
const reassert = state.warnedAtMs !== null && nowMs - state.warnedAtMs >= LOCAL_DISK_WARNING_REASSERTION_INTERVAL_MS;
|
|
6358
|
+
const nextState = {
|
|
6359
|
+
worstBand: worse ? currentBand : state.worstBand ?? currentBand,
|
|
6360
|
+
warnedAtMs: worse || state.warnedAtMs === null || reassert ? nowMs : state.warnedAtMs
|
|
6361
|
+
};
|
|
6362
|
+
return {
|
|
6363
|
+
warning: worse || state.warnedAtMs === null || reassert ? warningMessage(nextState.worstBand, diskFreeBytes, diskTotalBytes, opencodeDbBytes) : null,
|
|
6364
|
+
state: nextState
|
|
6365
|
+
};
|
|
6366
|
+
}
|
|
6367
|
+
|
|
6368
|
+
// src/lib/opencode/session-store-health.ts
|
|
6369
|
+
var SESSION_STORE_HEALTH_STALENESS_MS = 5 * 6e4;
|
|
6370
|
+
function createSessionStoreHealth(options = {}) {
|
|
6371
|
+
const now = options.now ?? (() => Date.now());
|
|
6372
|
+
const stalenessMs = Math.max(0, options.stalenessMs ?? SESSION_STORE_HEALTH_STALENESS_MS);
|
|
6373
|
+
let lastUnwritableAt = null;
|
|
6374
|
+
let lastWritableAt = null;
|
|
6375
|
+
return {
|
|
6376
|
+
observeUnwritable: () => {
|
|
6377
|
+
lastUnwritableAt = now();
|
|
6378
|
+
},
|
|
6379
|
+
observeWritable: () => {
|
|
6380
|
+
lastWritableAt = now();
|
|
6381
|
+
},
|
|
6382
|
+
isUnwritable: () => {
|
|
6383
|
+
if (lastUnwritableAt === null) return false;
|
|
6384
|
+
if (lastWritableAt !== null && lastWritableAt >= lastUnwritableAt) return false;
|
|
6385
|
+
return now() - lastUnwritableAt < stalenessMs;
|
|
6386
|
+
}
|
|
6387
|
+
};
|
|
6388
|
+
}
|
|
6389
|
+
|
|
5971
6390
|
// src/lib/channels/driver.ts
|
|
5972
|
-
import { homedir as
|
|
6391
|
+
import { homedir as homedir6 } from "node:os";
|
|
5973
6392
|
|
|
5974
6393
|
// src/lib/runner-file-sync.ts
|
|
5975
6394
|
import { join as join9 } from "node:path";
|
|
@@ -6426,6 +6845,7 @@ var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
|
6426
6845
|
var MAX_BUFFERED_SESSION_ERRORS = 256;
|
|
6427
6846
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
6428
6847
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
6848
|
+
var DRIVER_SESSION_ENUMERATION_LIMIT = 20;
|
|
6429
6849
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
6430
6850
|
var HEARTBEAT_MS = 6e4;
|
|
6431
6851
|
var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
|
|
@@ -6487,6 +6907,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6487
6907
|
maxActiveSessions;
|
|
6488
6908
|
watcherStallMs;
|
|
6489
6909
|
wedgeWarningIntervalMs;
|
|
6910
|
+
sessionStoreHealth;
|
|
6490
6911
|
openCodeClient;
|
|
6491
6912
|
/** Cache of conversationId → opencode sessionId. */
|
|
6492
6913
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -6852,10 +7273,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6852
7273
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
6853
7274
|
this.now = config.now ?? (() => Date.now());
|
|
6854
7275
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
6855
|
-
this.homeDir = config.homeDir ??
|
|
7276
|
+
this.homeDir = config.homeDir ?? homedir6();
|
|
6856
7277
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
6857
7278
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
6858
7279
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
7280
|
+
this.sessionStoreHealth = config.sessionStoreHealth;
|
|
6859
7281
|
}
|
|
6860
7282
|
get isV2() {
|
|
6861
7283
|
return this.openCodeClient.version === "v2";
|
|
@@ -6871,7 +7293,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6871
7293
|
return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
6872
7294
|
}
|
|
6873
7295
|
async listSessions() {
|
|
6874
|
-
return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient
|
|
7296
|
+
return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient, {
|
|
7297
|
+
limit: DRIVER_SESSION_ENUMERATION_LIMIT
|
|
7298
|
+
});
|
|
6875
7299
|
}
|
|
6876
7300
|
async sessionExists(sessionId) {
|
|
6877
7301
|
return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
|
|
@@ -8327,6 +8751,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8327
8751
|
b2AbandonedSignalled: false,
|
|
8328
8752
|
ambiguousPinnedSinceMs: 0,
|
|
8329
8753
|
ambiguousResolved: false,
|
|
8754
|
+
sessionStoreFailurePending: false,
|
|
8330
8755
|
neverStartedResolved: false
|
|
8331
8756
|
});
|
|
8332
8757
|
const buffered = this.bufferedSessionErrors.get(sessionId);
|
|
@@ -8445,6 +8870,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8445
8870
|
b2AbandonedSignalled: false,
|
|
8446
8871
|
ambiguousPinnedSinceMs: 0,
|
|
8447
8872
|
ambiguousResolved: false,
|
|
8873
|
+
sessionStoreFailurePending: false,
|
|
8448
8874
|
neverStartedResolved: false
|
|
8449
8875
|
});
|
|
8450
8876
|
}
|
|
@@ -8858,6 +9284,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8858
9284
|
if (state === "running" || state === "done" || state === "failed") {
|
|
8859
9285
|
this.neverStartedAttempts.delete(id);
|
|
8860
9286
|
}
|
|
9287
|
+
if (inFlight.sessionStoreFailurePending) {
|
|
9288
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
9289
|
+
return;
|
|
9290
|
+
}
|
|
8861
9291
|
if (inFlight.neverStartedResolved) {
|
|
8862
9292
|
if (state === "running" || state === "done" || state === "failed") {
|
|
8863
9293
|
inFlight.neverStartedResolved = false;
|
|
@@ -9037,6 +9467,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9037
9467
|
descendantOngoing,
|
|
9038
9468
|
rootOngoing
|
|
9039
9469
|
})) {
|
|
9470
|
+
if (this.sessionStoreHealth?.isUnwritable()) {
|
|
9471
|
+
inFlight.sessionStoreFailurePending = true;
|
|
9472
|
+
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
9473
|
+
watched_for_ms: pinnedForMs,
|
|
9474
|
+
run_state: "failed",
|
|
9475
|
+
opencode_message_id: inFlight.opencodeMessageId
|
|
9476
|
+
});
|
|
9477
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
9478
|
+
return;
|
|
9479
|
+
}
|
|
9040
9480
|
inFlight.b2AbandonedSignalled = true;
|
|
9041
9481
|
this.log({
|
|
9042
9482
|
level: "warn",
|
|
@@ -9095,6 +9535,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9095
9535
|
maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
|
|
9096
9536
|
sessionOngoing: ongoing
|
|
9097
9537
|
})) {
|
|
9538
|
+
if (this.sessionStoreHealth?.isUnwritable()) {
|
|
9539
|
+
inFlight.sessionStoreFailurePending = true;
|
|
9540
|
+
void this.postSignal(conv.id, id, "ambiguous_finish_resolved", {
|
|
9541
|
+
watched_for_ms: pinnedForMs,
|
|
9542
|
+
run_state: "failed",
|
|
9543
|
+
opencode_message_id: inFlight.opencodeMessageId
|
|
9544
|
+
});
|
|
9545
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
9546
|
+
return;
|
|
9547
|
+
}
|
|
9098
9548
|
inFlight.ambiguousResolved = true;
|
|
9099
9549
|
this.log({
|
|
9100
9550
|
level: "warn",
|
|
@@ -9374,6 +9824,66 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9374
9824
|
this.neverStartedAttempts.delete(inFlight.evidentMessageId);
|
|
9375
9825
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9376
9826
|
}
|
|
9827
|
+
/**
|
|
9828
|
+
* Report a turn as failed when the session database was observed unwritable.
|
|
9829
|
+
* The queue row is retained as failed, while the OpenCode copy stays fenced so
|
|
9830
|
+
* a later drain cannot blindly prompt the same turn again.
|
|
9831
|
+
*/
|
|
9832
|
+
async settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight) {
|
|
9833
|
+
const conv = watcher.conv;
|
|
9834
|
+
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.";
|
|
9835
|
+
this.log({
|
|
9836
|
+
level: "error",
|
|
9837
|
+
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} was not resolved as done: ${error2}`,
|
|
9838
|
+
conversation_id: conv.id,
|
|
9839
|
+
message_id: inFlight.evidentMessageId
|
|
9840
|
+
});
|
|
9841
|
+
this.anchorDeliveryDeadline(inFlight);
|
|
9842
|
+
try {
|
|
9843
|
+
await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2);
|
|
9844
|
+
} catch (err) {
|
|
9845
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
9846
|
+
if (err instanceof ChannelTerminalError) {
|
|
9847
|
+
this.log({
|
|
9848
|
+
level: "warn",
|
|
9849
|
+
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}`,
|
|
9850
|
+
conversation_id: conv.id,
|
|
9851
|
+
message_id: inFlight.evidentMessageId
|
|
9852
|
+
});
|
|
9853
|
+
this.recordReleasedOpencodeId(
|
|
9854
|
+
inFlight.evidentMessageId,
|
|
9855
|
+
sessionId,
|
|
9856
|
+
inFlight.opencodeMessageId
|
|
9857
|
+
);
|
|
9858
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9859
|
+
return;
|
|
9860
|
+
}
|
|
9861
|
+
if (this.now() >= inFlight.deadline) {
|
|
9862
|
+
this.log({
|
|
9863
|
+
level: "warn",
|
|
9864
|
+
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)}`,
|
|
9865
|
+
conversation_id: conv.id,
|
|
9866
|
+
message_id: inFlight.evidentMessageId
|
|
9867
|
+
});
|
|
9868
|
+
this.recordReleasedOpencodeId(
|
|
9869
|
+
inFlight.evidentMessageId,
|
|
9870
|
+
sessionId,
|
|
9871
|
+
inFlight.opencodeMessageId
|
|
9872
|
+
);
|
|
9873
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9874
|
+
return;
|
|
9875
|
+
}
|
|
9876
|
+
this.log({
|
|
9877
|
+
level: "warn",
|
|
9878
|
+
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)}`,
|
|
9879
|
+
conversation_id: conv.id,
|
|
9880
|
+
message_id: inFlight.evidentMessageId
|
|
9881
|
+
});
|
|
9882
|
+
return;
|
|
9883
|
+
}
|
|
9884
|
+
inFlight.done = true;
|
|
9885
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9886
|
+
}
|
|
9377
9887
|
// Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
|
|
9378
9888
|
/**
|
|
9379
9889
|
* Re-adopt this agent's `processing` messages on drain (ADR-0046 Decision §1).
|
|
@@ -11279,8 +11789,16 @@ function checkNonInteractivePortConflict(port, isPortInUseFn) {
|
|
|
11279
11789
|
);
|
|
11280
11790
|
}
|
|
11281
11791
|
}
|
|
11792
|
+
function unknownPasswordError(port) {
|
|
11793
|
+
return new Error(
|
|
11794
|
+
`OpenCode is already running on port ${port}, but this runner cannot authenticate to it. It may have been started with OPENCODE_SERVER_PASSWORD set in its environment. Free the port or pass --port.`
|
|
11795
|
+
);
|
|
11796
|
+
}
|
|
11282
11797
|
async function ensureOpenCodeRunning(ctx) {
|
|
11283
11798
|
const healthCheck = await checkOpenCodeHealth(ctx.port);
|
|
11799
|
+
if (healthCheck.authFailed) {
|
|
11800
|
+
throw unknownPasswordError(ctx.port);
|
|
11801
|
+
}
|
|
11284
11802
|
if (healthCheck.healthy) {
|
|
11285
11803
|
return {
|
|
11286
11804
|
port: ctx.port,
|
|
@@ -11331,6 +11849,14 @@ async function ensureOpenCodeRunning(ctx) {
|
|
|
11331
11849
|
const proc = await startOpenCode(ctx.port, { inheritStdio: ctx.inheritStdio });
|
|
11332
11850
|
const health = await waitForOpenCodeHealth(ctx.port, ctx.startTimeoutMs);
|
|
11333
11851
|
if (!health.healthy) {
|
|
11852
|
+
if (health.authFailed) {
|
|
11853
|
+
return {
|
|
11854
|
+
port: ctx.port,
|
|
11855
|
+
process: proc,
|
|
11856
|
+
version: null,
|
|
11857
|
+
notReadyReason: "it is answering with an authentication challenge, likely because OPENCODE_SERVER_PASSWORD is set in the environment of the process that started it; a longer start timeout will not resolve this"
|
|
11858
|
+
};
|
|
11859
|
+
}
|
|
11334
11860
|
return {
|
|
11335
11861
|
port: ctx.port,
|
|
11336
11862
|
process: proc,
|
|
@@ -11400,6 +11926,9 @@ Port ${port} is already in use.`));
|
|
|
11400
11926
|
const health = await waitForOpenCodeHealth(port, INTERACTIVE_START_TIMEOUT_MS);
|
|
11401
11927
|
if (!health.healthy) {
|
|
11402
11928
|
spinner.fail("Failed to start OpenCode");
|
|
11929
|
+
if (health.authFailed) {
|
|
11930
|
+
throw unknownPasswordError(port);
|
|
11931
|
+
}
|
|
11403
11932
|
throw new Error("OpenCode failed to start");
|
|
11404
11933
|
}
|
|
11405
11934
|
spinner.stop();
|
|
@@ -11412,7 +11941,7 @@ Port ${port} is already in use.`));
|
|
|
11412
11941
|
import chalk6 from "chalk";
|
|
11413
11942
|
import ora3 from "ora";
|
|
11414
11943
|
import { select as select3 } from "@inquirer/prompts";
|
|
11415
|
-
function
|
|
11944
|
+
function unknownPasswordError2(port) {
|
|
11416
11945
|
return new Error(
|
|
11417
11946
|
`OpenCode V2 (opencode2) is already running on port ${port} with a password this runner doesn't know. Free the port or pass --port.`
|
|
11418
11947
|
);
|
|
@@ -11421,7 +11950,7 @@ var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
|
|
|
11421
11950
|
async function ensureOpenCode2Running(ctx) {
|
|
11422
11951
|
const initialHealth = await checkOpenCode2Health(ctx.port);
|
|
11423
11952
|
if (initialHealth.authFailed) {
|
|
11424
|
-
throw
|
|
11953
|
+
throw unknownPasswordError2(ctx.port);
|
|
11425
11954
|
}
|
|
11426
11955
|
if (initialHealth.healthy) {
|
|
11427
11956
|
return {
|
|
@@ -11434,7 +11963,7 @@ async function ensureOpenCode2Running(ctx) {
|
|
|
11434
11963
|
}
|
|
11435
11964
|
if (!isOpenCode2Installed()) {
|
|
11436
11965
|
throw new Error(
|
|
11437
|
-
|
|
11966
|
+
`OpenCode V2 (opencode2) is not installed. Install it with: npm install -g ${OPENCODE2_PACKAGE_SPEC}`
|
|
11438
11967
|
);
|
|
11439
11968
|
}
|
|
11440
11969
|
let port = ctx.port;
|
|
@@ -12086,6 +12615,7 @@ function createCredentialSync({
|
|
|
12086
12615
|
|
|
12087
12616
|
// src/commands/run.ts
|
|
12088
12617
|
var MAX_ACTIVITY_LOG_ENTRIES = 10;
|
|
12618
|
+
var MAX_DISCONNECT_REASON_LENGTH = 200;
|
|
12089
12619
|
var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
|
|
12090
12620
|
var CHANNEL_STUCK_QUEUED_MS = Number(process.env.EVIDENT_STUCK_QUEUED_MS) || void 0;
|
|
12091
12621
|
var SHUTDOWN_DRAIN_TIMEOUT_MS = Number(process.env.EVIDENT_SHUTDOWN_DRAIN_MS) || 25e3;
|
|
@@ -12241,7 +12771,13 @@ function logActivity(state, entry) {
|
|
|
12241
12771
|
const level = entry.level ?? (entry.type === "error" ? "error" : "info");
|
|
12242
12772
|
if (!meetsThreshold(state, level)) return;
|
|
12243
12773
|
forwardRunnerActivity(
|
|
12244
|
-
{
|
|
12774
|
+
{
|
|
12775
|
+
level,
|
|
12776
|
+
message: entry.message,
|
|
12777
|
+
error: entry.error,
|
|
12778
|
+
metadata: entry.metadata,
|
|
12779
|
+
...entry.source ? { source: entry.source } : {}
|
|
12780
|
+
},
|
|
12245
12781
|
{ agentId: state.agentId, authHeader: state.authHeader }
|
|
12246
12782
|
);
|
|
12247
12783
|
const fullEntry = {
|
|
@@ -12263,7 +12799,7 @@ function logActivity(state, entry) {
|
|
|
12263
12799
|
}
|
|
12264
12800
|
function reportSessionDbRecovery(state) {
|
|
12265
12801
|
try {
|
|
12266
|
-
const report = drainSessionDbRecoveryReport({ homeDir:
|
|
12802
|
+
const report = drainSessionDbRecoveryReport({ homeDir: homedir7(), env: process.env });
|
|
12267
12803
|
for (const record of report.records) {
|
|
12268
12804
|
const activity = buildSessionDbRecoveryActivity(record);
|
|
12269
12805
|
if (!activity) throw new Error("could not map session-DB recovery record");
|
|
@@ -12298,7 +12834,9 @@ function displayStatus(state) {
|
|
|
12298
12834
|
const opencode = state.opencodeConnected ? chalk7.green(`opencode: :${state.port}`) : chalk7.red(`opencode: :${state.port} (down)`);
|
|
12299
12835
|
const messages = state.messageCount > 0 ? chalk7.dim(` \xB7 ${state.messageCount} processed`) : "";
|
|
12300
12836
|
const last = state.activityLog[state.activityLog.length - 1];
|
|
12301
|
-
const detail = last ? chalk7.dim(
|
|
12837
|
+
const detail = last ? chalk7.dim(
|
|
12838
|
+
` \xB7 ${last.timestamp.toISOString()} ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`
|
|
12839
|
+
) : "";
|
|
12302
12840
|
const agent = state.agentName ?? state.agentId;
|
|
12303
12841
|
console.log(
|
|
12304
12842
|
`${chalk7.bold("Evident")} ${chalk7.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
|
|
@@ -12317,7 +12855,7 @@ function reportOpenCodeV2CredentialImport(state, result) {
|
|
|
12317
12855
|
logActivity(state, {
|
|
12318
12856
|
type: "info",
|
|
12319
12857
|
level: "warn",
|
|
12320
|
-
message: `OpenCode V2 saved login(s) for ${integrationIds} on this machine, but OpenCode V2 will not use them.
|
|
12858
|
+
message: `OpenCode V2 saved login(s) for ${integrationIds} on this machine, but OpenCode V2 will not use them. Connect the provider again from the runner's page in Evident; no shell on this machine is needed.`
|
|
12321
12859
|
});
|
|
12322
12860
|
}
|
|
12323
12861
|
if (result.imported.length === 0 && result.unsupported.length === 0 && result.failed.length === 0) {
|
|
@@ -12342,7 +12880,7 @@ async function importOpenCodeV2CredentialsForState(state) {
|
|
|
12342
12880
|
}
|
|
12343
12881
|
const result = await importOpenCodeV2Credentials({
|
|
12344
12882
|
request: client.request,
|
|
12345
|
-
homeDir:
|
|
12883
|
+
homeDir: homedir7(),
|
|
12346
12884
|
directory,
|
|
12347
12885
|
guard: state.opencodeV2CredentialImportGuard,
|
|
12348
12886
|
log: (message, level = "info") => logActivity(state, { type: "info", level, message })
|
|
@@ -12550,9 +13088,9 @@ async function driveChannels(state, driver) {
|
|
|
12550
13088
|
}
|
|
12551
13089
|
}
|
|
12552
13090
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
12553
|
-
var
|
|
13091
|
+
var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
|
|
12554
13092
|
function shouldWarnForReclaimSkip(reason) {
|
|
12555
|
-
if (reason !== "sqlite-unavailable") return false;
|
|
13093
|
+
if (reason !== "sqlite-unavailable" && reason !== "disk-exhaustion") return false;
|
|
12556
13094
|
const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
|
|
12557
13095
|
if (!version2) return false;
|
|
12558
13096
|
const major = Number(version2[1]);
|
|
@@ -12561,7 +13099,14 @@ function shouldWarnForReclaimSkip(reason) {
|
|
|
12561
13099
|
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
12562
13100
|
}
|
|
12563
13101
|
function sessionDbPath() {
|
|
12564
|
-
return join11(
|
|
13102
|
+
return join11(homedir7(), ".local", "share", "opencode", "opencode.db");
|
|
13103
|
+
}
|
|
13104
|
+
function logSessionCleanupActivity(state, phase, entry) {
|
|
13105
|
+
logActivity(state, {
|
|
13106
|
+
...entry,
|
|
13107
|
+
source: "cli.session-cleanup",
|
|
13108
|
+
metadata: { ...entry.metadata, phase }
|
|
13109
|
+
});
|
|
12565
13110
|
}
|
|
12566
13111
|
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
12567
13112
|
const record = {
|
|
@@ -12602,69 +13147,136 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
|
|
|
12602
13147
|
async function runSweep(state, driver, config) {
|
|
12603
13148
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
12604
13149
|
try {
|
|
12605
|
-
const
|
|
12606
|
-
|
|
13150
|
+
const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0);
|
|
13151
|
+
const sessions = enumeration.sessions;
|
|
13152
|
+
const enumerationReason = sessions === null ? enumeration.reason : null;
|
|
13153
|
+
if (enumerationReason !== null) {
|
|
12607
13154
|
logActivity(state, {
|
|
12608
13155
|
type: "info",
|
|
12609
|
-
|
|
13156
|
+
level: "warn",
|
|
13157
|
+
message: `Session cleanup: could not enumerate sessions (${enumerationReason}); skipping deletes and continuing with space reclaim (${mode})`
|
|
12610
13158
|
});
|
|
12611
|
-
return;
|
|
12612
13159
|
}
|
|
12613
|
-
const toDelete = selectSessionsToDelete(
|
|
12614
|
-
sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
|
|
12615
|
-
{
|
|
12616
|
-
maxAgeMs: config.maxAgeMs,
|
|
12617
|
-
maxCount: config.maxCount,
|
|
12618
|
-
nowMs: Date.now(),
|
|
12619
|
-
protectedIds: driver.protectedSessionIds()
|
|
12620
|
-
}
|
|
12621
|
-
);
|
|
12622
|
-
const protectedNow = driver.protectedSessionIds();
|
|
12623
13160
|
let deleted = 0;
|
|
12624
13161
|
let failed = 0;
|
|
13162
|
+
let failed4xx = 0;
|
|
13163
|
+
let failed5xx = 0;
|
|
13164
|
+
let failedOtherStatus = 0;
|
|
13165
|
+
let failedThrew = 0;
|
|
12625
13166
|
let skippedNewlyActive = 0;
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
13167
|
+
let remaining = 0;
|
|
13168
|
+
let protectedNow = driver.protectedSessionIds();
|
|
13169
|
+
if (sessions !== null) {
|
|
13170
|
+
const toDelete = selectSessionsToDelete(
|
|
13171
|
+
sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
|
|
13172
|
+
{
|
|
13173
|
+
maxAgeMs: config.maxAgeMs,
|
|
13174
|
+
maxCount: config.maxCount,
|
|
13175
|
+
nowMs: Date.now(),
|
|
13176
|
+
protectedIds: protectedNow
|
|
13177
|
+
}
|
|
13178
|
+
);
|
|
13179
|
+
const deleteCandidates = toDelete.slice(0, SESSION_CLEANUP_MAX_DELETES_PER_SWEEP);
|
|
13180
|
+
remaining = toDelete.length - deleteCandidates.length;
|
|
13181
|
+
protectedNow = driver.protectedSessionIds();
|
|
13182
|
+
for (const id of deleteCandidates) {
|
|
13183
|
+
if (protectedNow.has(id)) {
|
|
13184
|
+
skippedNewlyActive++;
|
|
13185
|
+
logActivity(state, {
|
|
13186
|
+
type: "info",
|
|
13187
|
+
message: `Session cleanup: skipping ${id} \u2014 became active/bound after selection (${mode})`
|
|
13188
|
+
});
|
|
13189
|
+
continue;
|
|
13190
|
+
}
|
|
13191
|
+
const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
|
|
13192
|
+
if (outcome.ok) {
|
|
13193
|
+
deleted++;
|
|
13194
|
+
} else if (outcome.status === "threw") {
|
|
13195
|
+
failed++;
|
|
13196
|
+
failedThrew++;
|
|
13197
|
+
} else {
|
|
13198
|
+
failed++;
|
|
13199
|
+
const statusClass = Math.floor(outcome.status / 100);
|
|
13200
|
+
if (statusClass === 4) failed4xx++;
|
|
13201
|
+
else if (statusClass === 5) failed5xx++;
|
|
13202
|
+
else failedOtherStatus++;
|
|
13203
|
+
}
|
|
12634
13204
|
}
|
|
12635
|
-
|
|
12636
|
-
|
|
13205
|
+
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
13206
|
+
const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
|
|
13207
|
+
const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
|
|
13208
|
+
logActivity(state, {
|
|
13209
|
+
type: "info",
|
|
13210
|
+
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
|
|
13211
|
+
});
|
|
12637
13212
|
}
|
|
12638
|
-
const
|
|
12639
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
|
|
13213
|
+
const sessionMetadata = {
|
|
13214
|
+
sessions_inspected: sessions?.length ?? null,
|
|
13215
|
+
sessions_deleted: deleted,
|
|
13216
|
+
sessions_failed: failed,
|
|
13217
|
+
sessions_failed_4xx: failed4xx,
|
|
13218
|
+
sessions_failed_5xx: failed5xx,
|
|
13219
|
+
sessions_failed_other: failedOtherStatus,
|
|
13220
|
+
sessions_failed_threw: failedThrew,
|
|
13221
|
+
sessions_skipped_active: skippedNewlyActive,
|
|
13222
|
+
sessions_remaining: remaining,
|
|
13223
|
+
...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
|
|
13224
|
+
};
|
|
12644
13225
|
const reclaimResult = await reclaimSessionDbSpace({
|
|
12645
13226
|
dbPath: sessionDbPath(),
|
|
12646
|
-
|
|
13227
|
+
chunkPages: SESSION_DB_RECLAIM_CHUNK_PAGES,
|
|
13228
|
+
pageCeiling: SESSION_DB_RECLAIM_PAGE_CEILING,
|
|
13229
|
+
timeBudgetMs: SESSION_DB_RECLAIM_TIME_BUDGET_MS,
|
|
12647
13230
|
allowFullVacuum: protectedNow.size === 0 && !state.sessionDbProvenanceAnomaly
|
|
12648
13231
|
});
|
|
12649
13232
|
if (reclaimResult.ok) {
|
|
13233
|
+
state.sessionStoreHealth.observeWritable();
|
|
12650
13234
|
const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
|
|
12651
13235
|
const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
|
|
12652
13236
|
const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
|
|
12653
|
-
|
|
13237
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12654
13238
|
type: "info",
|
|
12655
|
-
message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${
|
|
13239
|
+
message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB (stopped by ${reclaimResult.stoppedBy}; freelist ${reclaimResult.freelistBefore} -> ${reclaimResult.freelistAfter} pages)` + checkpointNote,
|
|
13240
|
+
metadata: {
|
|
13241
|
+
outcome: "reclaimed",
|
|
13242
|
+
mode: reclaimResult.mode,
|
|
13243
|
+
before_bytes: reclaimResult.beforeBytes,
|
|
13244
|
+
after_bytes: reclaimResult.afterBytes,
|
|
13245
|
+
checkpoint_busy: reclaimResult.checkpoint.busy,
|
|
13246
|
+
wal_frames_pending: reclaimResult.checkpoint.log,
|
|
13247
|
+
stopped_by: reclaimResult.stoppedBy,
|
|
13248
|
+
freelist_before: reclaimResult.freelistBefore,
|
|
13249
|
+
freelist_after: reclaimResult.freelistAfter,
|
|
13250
|
+
pages_reclaimed: reclaimResult.pagesReclaimed,
|
|
13251
|
+
reclaim_elapsed_ms: reclaimResult.elapsedMs,
|
|
13252
|
+
...sessionMetadata
|
|
13253
|
+
}
|
|
12656
13254
|
});
|
|
12657
13255
|
} else {
|
|
12658
|
-
|
|
13256
|
+
if (reclaimResult.skipped === "disk-exhaustion") {
|
|
13257
|
+
state.sessionStoreHealth.observeUnwritable();
|
|
13258
|
+
}
|
|
13259
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12659
13260
|
type: "info",
|
|
12660
|
-
|
|
13261
|
+
level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "disk-exhaustion" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
|
|
13262
|
+
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : ""),
|
|
13263
|
+
metadata: {
|
|
13264
|
+
outcome: "reclaim-skipped",
|
|
13265
|
+
reason: reclaimResult.skipped,
|
|
13266
|
+
...reclaimResult.detail !== void 0 ? { detail: reclaimResult.detail } : {},
|
|
13267
|
+
...sessionMetadata
|
|
13268
|
+
}
|
|
12661
13269
|
});
|
|
12662
13270
|
}
|
|
12663
13271
|
} catch (error2) {
|
|
12664
13272
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
12665
|
-
|
|
13273
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12666
13274
|
type: "error",
|
|
12667
|
-
error: `Session cleanup sweep failed (non-fatal, ${mode}): ${message}
|
|
13275
|
+
error: `Session cleanup sweep failed (non-fatal, ${mode}): ${message}`,
|
|
13276
|
+
metadata: {
|
|
13277
|
+
outcome: "sweep-failed",
|
|
13278
|
+
reason: "sweep-error"
|
|
13279
|
+
}
|
|
12668
13280
|
});
|
|
12669
13281
|
}
|
|
12670
13282
|
}
|
|
@@ -12680,14 +13292,19 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
12680
13292
|
for (const warning2 of config.warnings) {
|
|
12681
13293
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
12682
13294
|
}
|
|
12683
|
-
const dbBytes = statSessionDbBytes(
|
|
13295
|
+
const dbBytes = statSessionDbBytes(homedir7());
|
|
12684
13296
|
void (async () => {
|
|
12685
13297
|
const reclaimAvailability = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
12686
13298
|
if (reclaimAvailability !== null) {
|
|
12687
|
-
|
|
13299
|
+
logSessionCleanupActivity(state, "startup", {
|
|
12688
13300
|
type: "info",
|
|
12689
13301
|
level: shouldWarnForReclaimSkip(reclaimAvailability.reason) ? "warn" : "info",
|
|
12690
|
-
message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : "")
|
|
13302
|
+
message: `Session cleanup: reclaim preflight skipped (${reclaimAvailability.reason})` + (reclaimAvailability.detail ? `: ${reclaimAvailability.detail}` : ""),
|
|
13303
|
+
metadata: {
|
|
13304
|
+
outcome: "preflight-skipped",
|
|
13305
|
+
reason: reclaimAvailability.reason,
|
|
13306
|
+
...reclaimAvailability.detail !== void 0 ? { detail: reclaimAvailability.detail } : {}
|
|
13307
|
+
}
|
|
12691
13308
|
});
|
|
12692
13309
|
}
|
|
12693
13310
|
const sizeWarning = buildSessionStoreSizeWarning({
|
|
@@ -12704,9 +13321,15 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
12704
13321
|
);
|
|
12705
13322
|
});
|
|
12706
13323
|
if (!config.enabled) return;
|
|
12707
|
-
|
|
13324
|
+
logSessionCleanupActivity(state, "startup", {
|
|
12708
13325
|
type: "info",
|
|
12709
|
-
message: `Session cleanup enabled (age=${config.maxAgeMs ?? "\u2014"}, count=${config.maxCount ?? "\u2014"}, interval=${config.intervalMs}ms)
|
|
13326
|
+
message: `Session cleanup enabled (age=${config.maxAgeMs ?? "\u2014"}, count=${config.maxCount ?? "\u2014"}, interval=${config.intervalMs}ms)`,
|
|
13327
|
+
metadata: {
|
|
13328
|
+
outcome: "armed",
|
|
13329
|
+
interval_ms: config.intervalMs,
|
|
13330
|
+
max_age_ms: config.maxAgeMs ?? null,
|
|
13331
|
+
max_count: config.maxCount ?? null
|
|
13332
|
+
}
|
|
12710
13333
|
});
|
|
12711
13334
|
const interval = setInterval(() => void runSweep(state, driver, config), config.intervalMs);
|
|
12712
13335
|
const firstSweep = setTimeout(
|
|
@@ -12885,7 +13508,7 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
12885
13508
|
});
|
|
12886
13509
|
return;
|
|
12887
13510
|
}
|
|
12888
|
-
const { collect, stop } = createResourceUsageCollector(
|
|
13511
|
+
const { collect, stop } = createResourceUsageCollector(homedir7());
|
|
12889
13512
|
state.stopResourceUsageSampling = stop;
|
|
12890
13513
|
let consecutiveFailures = 0;
|
|
12891
13514
|
const tick = async () => {
|
|
@@ -12898,6 +13521,19 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
12898
13521
|
message: `Resource usage collection: ${warning2}`
|
|
12899
13522
|
});
|
|
12900
13523
|
}
|
|
13524
|
+
const lowDiskDecision = evaluateLowDiskWarning({
|
|
13525
|
+
...usage,
|
|
13526
|
+
nowMs: Date.now(),
|
|
13527
|
+
state: state.lowDiskWarningState
|
|
13528
|
+
});
|
|
13529
|
+
state.lowDiskWarningState = lowDiskDecision.state;
|
|
13530
|
+
if (lowDiskDecision.warning !== null) {
|
|
13531
|
+
logActivity(state, {
|
|
13532
|
+
type: "info",
|
|
13533
|
+
level: "warn",
|
|
13534
|
+
message: lowDiskDecision.warning
|
|
13535
|
+
});
|
|
13536
|
+
}
|
|
12901
13537
|
const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
|
|
12902
13538
|
if (result.ok) {
|
|
12903
13539
|
if (consecutiveFailures > 0) {
|
|
@@ -13110,7 +13746,7 @@ async function run(options) {
|
|
|
13110
13746
|
let fileSyncDirectories;
|
|
13111
13747
|
try {
|
|
13112
13748
|
logLevel = resolveLogLevel(options);
|
|
13113
|
-
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo,
|
|
13749
|
+
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir7());
|
|
13114
13750
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
13115
13751
|
throw new Error(
|
|
13116
13752
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -13144,6 +13780,7 @@ async function run(options) {
|
|
|
13144
13780
|
opencodeClient: null,
|
|
13145
13781
|
opencodeV2CredentialImportGuard: { inFlight: false },
|
|
13146
13782
|
sessionDbProvenanceAnomaly: false,
|
|
13783
|
+
sessionStoreHealth: createSessionStoreHealth(),
|
|
13147
13784
|
opencodeProcess: null,
|
|
13148
13785
|
stopOpenCodeLogTail: null,
|
|
13149
13786
|
litestreamProcess: null,
|
|
@@ -13161,10 +13798,12 @@ async function run(options) {
|
|
|
13161
13798
|
openaiUsageRearm: null,
|
|
13162
13799
|
resourceUsageTimer: null,
|
|
13163
13800
|
stopResourceUsageSampling: null,
|
|
13801
|
+
lowDiskWarningState: { worstBand: null, warnedAtMs: null },
|
|
13164
13802
|
credentialSync: null,
|
|
13165
13803
|
authHeader: ""
|
|
13166
13804
|
};
|
|
13167
13805
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
13806
|
+
setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
|
|
13168
13807
|
if (options.credentialSyncMarker) {
|
|
13169
13808
|
state.credentialSync = createCredentialSync({
|
|
13170
13809
|
markerPath: options.credentialSyncMarker,
|
|
@@ -13350,13 +13989,21 @@ async function run(options) {
|
|
|
13350
13989
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
13351
13990
|
}
|
|
13352
13991
|
state.credentialSync?.arm();
|
|
13353
|
-
|
|
13354
|
-
|
|
13355
|
-
createOpenCodeActivityForwarder(() => ({
|
|
13992
|
+
const openCodeActivityForwarder = createOpenCodeActivityForwarder(
|
|
13993
|
+
() => ({
|
|
13356
13994
|
agentId: state.agentId,
|
|
13357
13995
|
authHeader: state.authHeader
|
|
13358
|
-
})
|
|
13359
|
-
|
|
13996
|
+
}),
|
|
13997
|
+
state.sessionStoreHealth.observeUnwritable
|
|
13998
|
+
);
|
|
13999
|
+
const openCodeLogTail = tailOpenCodeLogFile(
|
|
14000
|
+
resolveOpenCodeLogPath(homedir7(), process.env),
|
|
14001
|
+
openCodeActivityForwarder
|
|
14002
|
+
);
|
|
14003
|
+
state.stopOpenCodeLogTail = () => {
|
|
14004
|
+
openCodeActivityForwarder.flush();
|
|
14005
|
+
openCodeLogTail.stop();
|
|
14006
|
+
};
|
|
13360
14007
|
let sessionDbVerifyFatal = false;
|
|
13361
14008
|
if (!options.restoreSessionDb) {
|
|
13362
14009
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -13368,6 +14015,12 @@ async function run(options) {
|
|
|
13368
14015
|
"Skipping session-DB restore: OpenCode is already serving this database",
|
|
13369
14016
|
"debug"
|
|
13370
14017
|
);
|
|
14018
|
+
} else if (health.authFailed) {
|
|
14019
|
+
log2(
|
|
14020
|
+
state,
|
|
14021
|
+
"Skipping session-DB restore: OpenCode answered with an authentication challenge; assuming this database is in use",
|
|
14022
|
+
"debug"
|
|
14023
|
+
);
|
|
13371
14024
|
} else {
|
|
13372
14025
|
const result = await restoreAndVerifySessionDb({
|
|
13373
14026
|
dbPath: sessionDbPath(),
|
|
@@ -13467,7 +14120,7 @@ async function run(options) {
|
|
|
13467
14120
|
const provenance = checkSessionDbProvenance({
|
|
13468
14121
|
dbPath: sessionDbPath(),
|
|
13469
14122
|
currentVersion: state.opencodeVersion,
|
|
13470
|
-
homeDir:
|
|
14123
|
+
homeDir: homedir7(),
|
|
13471
14124
|
env: process.env
|
|
13472
14125
|
});
|
|
13473
14126
|
if (provenance.anomaly) {
|
|
@@ -13634,10 +14287,11 @@ async function run(options) {
|
|
|
13634
14287
|
getAuthHeader: () => state.authHeader,
|
|
13635
14288
|
conversationFilter: state.conversationFilter,
|
|
13636
14289
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
14290
|
+
sessionStoreHealth: state.sessionStoreHealth,
|
|
13637
14291
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
13638
14292
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
13639
14293
|
fileSyncDirectories,
|
|
13640
|
-
homeDir:
|
|
14294
|
+
homeDir: homedir7(),
|
|
13641
14295
|
maxActiveSessions,
|
|
13642
14296
|
log: (entry) => (
|
|
13643
14297
|
// Thread the driver's real level straight through so `debug`/`warn`
|
|
@@ -13659,12 +14313,13 @@ async function run(options) {
|
|
|
13659
14313
|
openCodePassword: state.opencodePassword,
|
|
13660
14314
|
isRunning: () => state.running,
|
|
13661
14315
|
events: {
|
|
13662
|
-
onConnected: (agentId, isReconnect) => {
|
|
14316
|
+
onConnected: (agentId, isReconnect, transition) => {
|
|
13663
14317
|
state.connected = true;
|
|
13664
14318
|
state.agentId = agentId;
|
|
14319
|
+
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})`;
|
|
13665
14320
|
logActivity(state, {
|
|
13666
14321
|
type: "info",
|
|
13667
|
-
message:
|
|
14322
|
+
message: connectionMessage
|
|
13668
14323
|
});
|
|
13669
14324
|
if (options.tunnelReadyFile) {
|
|
13670
14325
|
const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
|
|
@@ -13685,7 +14340,12 @@ async function run(options) {
|
|
|
13685
14340
|
version: state.opencodeVersion,
|
|
13686
14341
|
major: state.opencodeApiVersion,
|
|
13687
14342
|
connected: state.opencodeConnected
|
|
13688
|
-
})
|
|
14343
|
+
}),
|
|
14344
|
+
connection_generation: transition.generation,
|
|
14345
|
+
...isReconnect && transition.downtimeMs !== null ? {
|
|
14346
|
+
reconnect_attempt: transition.attempt,
|
|
14347
|
+
downtime_ms: transition.downtimeMs
|
|
14348
|
+
} : {}
|
|
13689
14349
|
});
|
|
13690
14350
|
if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
|
|
13691
14351
|
if (state.interactive) displayStatus(state);
|
|
@@ -13707,13 +14367,18 @@ async function run(options) {
|
|
|
13707
14367
|
if (state.interactive) displayStatus(state);
|
|
13708
14368
|
});
|
|
13709
14369
|
},
|
|
13710
|
-
onDisconnected: (code, reason) => {
|
|
14370
|
+
onDisconnected: (code, reason, transition) => {
|
|
13711
14371
|
state.connected = false;
|
|
14372
|
+
const safeReason = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
|
|
13712
14373
|
logActivity(state, {
|
|
13713
14374
|
type: "info",
|
|
13714
|
-
message: `Tunnel disconnected (code: ${code}, reason: ${
|
|
14375
|
+
message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason})`
|
|
14376
|
+
});
|
|
14377
|
+
emitAgentDisconnected(state.agentId, {
|
|
14378
|
+
code,
|
|
14379
|
+
reason: safeReason,
|
|
14380
|
+
connection_generation: transition.generation
|
|
13715
14381
|
});
|
|
13716
|
-
emitAgentDisconnected(state.agentId, { code, reason });
|
|
13717
14382
|
if (state.interactive) displayStatus(state);
|
|
13718
14383
|
},
|
|
13719
14384
|
onError: (error2) => {
|
|
@@ -13813,7 +14478,7 @@ async function run(options) {
|
|
|
13813
14478
|
},
|
|
13814
14479
|
report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
|
|
13815
14480
|
isLocalCredentialProblem: isLocalCredentialProblem2,
|
|
13816
|
-
forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
14481
|
+
forcedOnHint: state.opencodeApiVersion === "v2" ? "OpenCode V2 keeps its connected ChatGPT login in a store Evident cannot read" : "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
13817
14482
|
firstDelayMs: firstReportDelayMs,
|
|
13818
14483
|
nextDelayMs: usageReportDelayMs,
|
|
13819
14484
|
failureLogLevel: usageReportFailureLogLevel
|
|
@@ -13856,7 +14521,7 @@ async function run(options) {
|
|
|
13856
14521
|
}
|
|
13857
14522
|
|
|
13858
14523
|
// src/index.ts
|
|
13859
|
-
var
|
|
14524
|
+
var version = getCliVersion();
|
|
13860
14525
|
var program = new Command();
|
|
13861
14526
|
program.name("evident").description("Run OpenCode locally and connect it to Evident").version(version).option(
|
|
13862
14527
|
"--endpoint <url>",
|