@evident-ai/cli 3.2.0 → 3.2.1-dev.165c9c6
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/README.md +7 -2
- package/dist/index.js +432 -76
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -212,6 +212,7 @@ var api = {
|
|
|
212
212
|
|
|
213
213
|
// src/lib/keychain.ts
|
|
214
214
|
var SERVICE_NAME = "evident-cli";
|
|
215
|
+
var keytarWarned = false;
|
|
215
216
|
async function getKeytar() {
|
|
216
217
|
try {
|
|
217
218
|
const keytar = await import("keytar");
|
|
@@ -219,7 +220,13 @@ async function getKeytar() {
|
|
|
219
220
|
return null;
|
|
220
221
|
}
|
|
221
222
|
return keytar;
|
|
222
|
-
} catch {
|
|
223
|
+
} catch (err) {
|
|
224
|
+
if (!keytarWarned) {
|
|
225
|
+
keytarWarned = true;
|
|
226
|
+
console.warn(
|
|
227
|
+
`System keychain unavailable, falling back to file-based credential storage: ${err instanceof Error ? err.message : String(err)}`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
223
230
|
return null;
|
|
224
231
|
}
|
|
225
232
|
}
|
|
@@ -368,8 +375,9 @@ async function deviceFlowLogin(options) {
|
|
|
368
375
|
await waitForEnter("Press Enter to open the browser...");
|
|
369
376
|
try {
|
|
370
377
|
await open(verification_uri);
|
|
371
|
-
} catch {
|
|
372
|
-
|
|
378
|
+
} catch (error2) {
|
|
379
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
380
|
+
console.log(chalk2.dim(`Could not open browser (${message}). Please visit the URL manually.`));
|
|
373
381
|
}
|
|
374
382
|
}
|
|
375
383
|
const spinner = ora("Waiting for authentication...").start();
|
|
@@ -909,14 +917,25 @@ function readClaudeCliCredentials() {
|
|
|
909
917
|
{ encoding: "utf-8", timeout: 2e3, stdio: ["pipe", "pipe", "ignore"] }
|
|
910
918
|
);
|
|
911
919
|
return parseClaudeCliCredentials(raw);
|
|
912
|
-
} catch {
|
|
920
|
+
} catch (err) {
|
|
921
|
+
if (err.status !== 44) {
|
|
922
|
+
console.warn(
|
|
923
|
+
`readClaudeCliCredentials: security find-generic-password failed: ${err instanceof Error ? err.message : String(err)}`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
913
926
|
return null;
|
|
914
927
|
}
|
|
915
928
|
}
|
|
916
929
|
try {
|
|
917
930
|
const raw = readFileSync(join(homedir(), ".claude", ".credentials.json"), "utf-8");
|
|
918
931
|
return parseClaudeCliCredentials(raw);
|
|
919
|
-
} catch {
|
|
932
|
+
} catch (err) {
|
|
933
|
+
const code = err.code;
|
|
934
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
935
|
+
console.warn(
|
|
936
|
+
`readClaudeCliCredentials: reading .claude/.credentials.json failed: ${err instanceof Error ? err.message : String(err)}`
|
|
937
|
+
);
|
|
938
|
+
}
|
|
920
939
|
return null;
|
|
921
940
|
}
|
|
922
941
|
}
|
|
@@ -1004,7 +1023,7 @@ async function claudeUsage() {
|
|
|
1004
1023
|
|
|
1005
1024
|
// src/commands/run.ts
|
|
1006
1025
|
import { homedir as homedir3 } from "os";
|
|
1007
|
-
import { isAbsolute as isAbsolute2, join as
|
|
1026
|
+
import { isAbsolute as isAbsolute2, join as join4, parse, resolve as resolvePath } from "path";
|
|
1008
1027
|
import chalk6 from "chalk";
|
|
1009
1028
|
|
|
1010
1029
|
// ../../packages/types/src/agents/index.ts
|
|
@@ -1033,6 +1052,7 @@ var MAX_FILE_SYNC_DIRECTORIES = 16;
|
|
|
1033
1052
|
|
|
1034
1053
|
// ../../packages/types/src/logging/index.ts
|
|
1035
1054
|
var CORRELATION_ID_HEADER = "x-evident-correlation-id";
|
|
1055
|
+
var FORWARD_FAILURE_REASON_HEADER = "X-Evident-Failure-Reason";
|
|
1036
1056
|
function log(level, event, fields) {
|
|
1037
1057
|
const method = level === "debug" ? "log" : level;
|
|
1038
1058
|
try {
|
|
@@ -1414,7 +1434,10 @@ function findOpenCodeProcesses() {
|
|
|
1414
1434
|
}
|
|
1415
1435
|
}
|
|
1416
1436
|
}
|
|
1417
|
-
} catch {
|
|
1437
|
+
} catch (err) {
|
|
1438
|
+
console.warn(
|
|
1439
|
+
`findOpenCodeProcesses: ps fallback failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1440
|
+
);
|
|
1418
1441
|
}
|
|
1419
1442
|
}
|
|
1420
1443
|
for (const pid of pids) {
|
|
@@ -1437,7 +1460,10 @@ function findOpenCodeProcesses() {
|
|
|
1437
1460
|
}
|
|
1438
1461
|
}
|
|
1439
1462
|
}
|
|
1440
|
-
} catch {
|
|
1463
|
+
} catch (err) {
|
|
1464
|
+
console.warn(
|
|
1465
|
+
`findOpenCodeProcesses: process detection failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1466
|
+
);
|
|
1441
1467
|
}
|
|
1442
1468
|
return instances;
|
|
1443
1469
|
}
|
|
@@ -1511,7 +1537,12 @@ function stopOpenCode(opencodeProcess) {
|
|
|
1511
1537
|
} else {
|
|
1512
1538
|
process.kill(-opencodeProcess.pid, "SIGTERM");
|
|
1513
1539
|
}
|
|
1514
|
-
} catch {
|
|
1540
|
+
} catch (err) {
|
|
1541
|
+
if (err.code !== "ESRCH") {
|
|
1542
|
+
console.warn(
|
|
1543
|
+
`stopOpenCode: kill failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1515
1546
|
}
|
|
1516
1547
|
}
|
|
1517
1548
|
|
|
@@ -2076,7 +2107,9 @@ function messageRunState(messages, userMessageId) {
|
|
|
2076
2107
|
}
|
|
2077
2108
|
if (!reply) return "queued";
|
|
2078
2109
|
if (isAssistantInFlight(reply)) return "running";
|
|
2079
|
-
|
|
2110
|
+
if (errorOf(reply) != null) return "failed";
|
|
2111
|
+
if (isAmbiguousTerminalFinish(reply)) return "running";
|
|
2112
|
+
return "done";
|
|
2080
2113
|
}
|
|
2081
2114
|
function isPreamblePinnedRunning(messages, userMessageId) {
|
|
2082
2115
|
if (messageRunState(messages, userMessageId) !== "running") return false;
|
|
@@ -2086,6 +2119,19 @@ function isPreamblePinnedRunning(messages, userMessageId) {
|
|
|
2086
2119
|
function isB2AbandonmentConfirmed(params) {
|
|
2087
2120
|
return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false;
|
|
2088
2121
|
}
|
|
2122
|
+
function isAmbiguousTerminalFinish(m) {
|
|
2123
|
+
if (completedOf(m) == null) return false;
|
|
2124
|
+
if (errorOf(m) != null) return false;
|
|
2125
|
+
const finish = finishOf(m);
|
|
2126
|
+
return finish !== "tool-calls" && finish !== "stop";
|
|
2127
|
+
}
|
|
2128
|
+
function isAmbiguousFinishPinnedRunning(messages, userMessageId) {
|
|
2129
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
2130
|
+
return isAmbiguousTerminalFinish(reply);
|
|
2131
|
+
}
|
|
2132
|
+
function isAmbiguousFinishResolved(params) {
|
|
2133
|
+
return params.sessionOngoing === false || params.pinnedForMs >= params.maxPinnedMs;
|
|
2134
|
+
}
|
|
2089
2135
|
function messageError(messages, userMessageId) {
|
|
2090
2136
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
2091
2137
|
const error2 = errorOf(reply);
|
|
@@ -2292,6 +2338,149 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
2292
2338
|
return { enabled, maxAgeMs, maxCount, intervalMs, warnings };
|
|
2293
2339
|
}
|
|
2294
2340
|
|
|
2341
|
+
// src/lib/opencode/session-db-size.ts
|
|
2342
|
+
import { statSync as statSync2 } from "fs";
|
|
2343
|
+
import { join as join2 } from "path";
|
|
2344
|
+
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
2345
|
+
function statSessionDbBytes(homeDir) {
|
|
2346
|
+
const dbPath = join2(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
2347
|
+
try {
|
|
2348
|
+
return statSync2(dbPath).size;
|
|
2349
|
+
} catch (err) {
|
|
2350
|
+
const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
2351
|
+
if (!isMissingFile) {
|
|
2352
|
+
console.error(
|
|
2353
|
+
`[statSessionDbBytes] could not stat ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
return null;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
function buildSessionStoreSizeWarning(input) {
|
|
2360
|
+
const { dbBytes, cleanupEnabled, reclaimSkipReason } = input;
|
|
2361
|
+
if (dbBytes === null || dbBytes <= LARGE_DB_THRESHOLD_BYTES) return null;
|
|
2362
|
+
const mib = Math.round(dbBytes / 1024 / 1024);
|
|
2363
|
+
if (!cleanupEnabled) {
|
|
2364
|
+
return `Session store is large: opencode.db is ${mib} MiB and automatic session cleanup is off. Enable it with --session-cleanup-max-age 24h (env EVIDENT_SESSION_CLEANUP_MAX_AGE) to stop it growing \u2014 a store much larger than this can exceed a hosted runner's session-history restore budget on the next start, losing this runner's session history.`;
|
|
2365
|
+
}
|
|
2366
|
+
if (reclaimSkipReason === "sqlite-unavailable" || reclaimSkipReason === "insufficient-disk-space") {
|
|
2367
|
+
const reasonText = reclaimSkipReason === "sqlite-unavailable" ? "this Node runtime lacks node:sqlite (needs Node >=22.5)" : "there is not enough free disk space to compact it";
|
|
2368
|
+
return `Session store is large: opencode.db is ${mib} MiB. Automatic session cleanup is on, but space cannot currently be reclaimed because ${reasonText} \u2014 a store this large can exceed a hosted runner's session-history restore budget on the next start, losing this runner's session history.`;
|
|
2369
|
+
}
|
|
2370
|
+
return null;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
// src/lib/opencode/session-db-reclaim.ts
|
|
2374
|
+
import { statSync as statSync3, statfsSync } from "fs";
|
|
2375
|
+
import { dirname as dirname2 } from "path";
|
|
2376
|
+
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
2377
|
+
try {
|
|
2378
|
+
const fsStats = statfsSync(dirname2(dbPath));
|
|
2379
|
+
const availableBytes = fsStats.bavail * fsStats.bsize;
|
|
2380
|
+
if (availableBytes < requiredBytes) {
|
|
2381
|
+
return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
|
|
2382
|
+
}
|
|
2383
|
+
return null;
|
|
2384
|
+
} catch (err) {
|
|
2385
|
+
return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
function readLogicalBytes(db) {
|
|
2389
|
+
const pageCount = db.prepare("PRAGMA page_count").get().page_count;
|
|
2390
|
+
const pageSize = db.prepare("PRAGMA page_size").get().page_size;
|
|
2391
|
+
return pageCount * pageSize;
|
|
2392
|
+
}
|
|
2393
|
+
function readCheckpointResult(db) {
|
|
2394
|
+
const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
2395
|
+
return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
|
|
2396
|
+
}
|
|
2397
|
+
async function probeReclaimAvailability(input) {
|
|
2398
|
+
const { dbPath, requiredBytes } = input;
|
|
2399
|
+
let sqlite;
|
|
2400
|
+
try {
|
|
2401
|
+
sqlite = await import("sqlite");
|
|
2402
|
+
} catch (err) {
|
|
2403
|
+
console.warn(
|
|
2404
|
+
`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2405
|
+
);
|
|
2406
|
+
return "sqlite-unavailable";
|
|
2407
|
+
}
|
|
2408
|
+
let autoVacuum = null;
|
|
2409
|
+
try {
|
|
2410
|
+
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
2411
|
+
try {
|
|
2412
|
+
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
2413
|
+
} finally {
|
|
2414
|
+
db.close();
|
|
2415
|
+
}
|
|
2416
|
+
} catch (err) {
|
|
2417
|
+
console.warn(
|
|
2418
|
+
`[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
if (autoVacuum !== 0) return null;
|
|
2422
|
+
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? "insufficient-disk-space" : null;
|
|
2423
|
+
}
|
|
2424
|
+
async function reclaimSessionDbSpace(input) {
|
|
2425
|
+
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
2426
|
+
let sqlite;
|
|
2427
|
+
try {
|
|
2428
|
+
sqlite = await import("sqlite");
|
|
2429
|
+
} catch (err) {
|
|
2430
|
+
console.warn(
|
|
2431
|
+
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2432
|
+
);
|
|
2433
|
+
return { ok: false, skipped: "sqlite-unavailable" };
|
|
2434
|
+
}
|
|
2435
|
+
const { DatabaseSync } = sqlite;
|
|
2436
|
+
let db;
|
|
2437
|
+
try {
|
|
2438
|
+
db = new DatabaseSync(dbPath);
|
|
2439
|
+
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
2440
|
+
if (autoVacuum === 0) {
|
|
2441
|
+
if (!allowFullVacuum) {
|
|
2442
|
+
console.warn(
|
|
2443
|
+
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
|
|
2444
|
+
);
|
|
2445
|
+
return { ok: false, skipped: "full-vacuum-blocked" };
|
|
2446
|
+
}
|
|
2447
|
+
const fileBytesForGuard = statSync3(dbPath).size;
|
|
2448
|
+
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
2449
|
+
if (skipReason !== null) {
|
|
2450
|
+
console.warn(
|
|
2451
|
+
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
|
|
2452
|
+
);
|
|
2453
|
+
return { ok: false, skipped: "insufficient-disk-space" };
|
|
2454
|
+
}
|
|
2455
|
+
const beforeBytes = readLogicalBytes(db);
|
|
2456
|
+
db.exec("PRAGMA auto_vacuum=INCREMENTAL");
|
|
2457
|
+
db.exec("VACUUM");
|
|
2458
|
+
const afterBytes = readLogicalBytes(db);
|
|
2459
|
+
const checkpoint = readCheckpointResult(db);
|
|
2460
|
+
return { ok: true, mode: "convert", beforeBytes, afterBytes, checkpoint };
|
|
2461
|
+
}
|
|
2462
|
+
if (autoVacuum === 2) {
|
|
2463
|
+
const beforeBytes = readLogicalBytes(db);
|
|
2464
|
+
const bound = Math.max(0, Math.trunc(maxPages));
|
|
2465
|
+
db.exec(`PRAGMA incremental_vacuum(${bound})`);
|
|
2466
|
+
const afterBytes = readLogicalBytes(db);
|
|
2467
|
+
const checkpoint = readCheckpointResult(db);
|
|
2468
|
+
return { ok: true, mode: "incremental", beforeBytes, afterBytes, checkpoint };
|
|
2469
|
+
}
|
|
2470
|
+
console.warn(
|
|
2471
|
+
`[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
|
|
2472
|
+
);
|
|
2473
|
+
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
2474
|
+
} catch (err) {
|
|
2475
|
+
console.error(
|
|
2476
|
+
`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2477
|
+
);
|
|
2478
|
+
return { ok: false, skipped: "reclaim-error" };
|
|
2479
|
+
} finally {
|
|
2480
|
+
db?.close();
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2295
2484
|
// src/lib/tunnel/connection.ts
|
|
2296
2485
|
import WebSocket2 from "ws";
|
|
2297
2486
|
|
|
@@ -2457,6 +2646,17 @@ var StreamForwarder = class {
|
|
|
2457
2646
|
};
|
|
2458
2647
|
|
|
2459
2648
|
// src/lib/tunnel/connection.ts
|
|
2649
|
+
var FAILURE_REASON_HEADER_LC = FORWARD_FAILURE_REASON_HEADER.toLowerCase();
|
|
2650
|
+
var TunnelUpgradeRejectedError = class extends Error {
|
|
2651
|
+
constructor(message, reason) {
|
|
2652
|
+
super(message);
|
|
2653
|
+
this.reason = reason;
|
|
2654
|
+
}
|
|
2655
|
+
};
|
|
2656
|
+
function classifyUpgradeRejection(headers) {
|
|
2657
|
+
const value = headers[FAILURE_REASON_HEADER_LC];
|
|
2658
|
+
return value === "do_code_updated" ? "do_code_updated" : "unknown";
|
|
2659
|
+
}
|
|
2460
2660
|
var MAX_RECONNECT_DELAY = 3e4;
|
|
2461
2661
|
var BASE_RECONNECT_DELAY = 500;
|
|
2462
2662
|
function getReconnectDelay(attempt) {
|
|
@@ -2501,6 +2701,7 @@ function connectTunnel(options) {
|
|
|
2501
2701
|
onError,
|
|
2502
2702
|
onResponse,
|
|
2503
2703
|
onInfo,
|
|
2704
|
+
onWarning,
|
|
2504
2705
|
onDrainPing
|
|
2505
2706
|
} = options;
|
|
2506
2707
|
const tunnelUrl = getTunnelUrlConfig();
|
|
@@ -2520,8 +2721,11 @@ function connectTunnel(options) {
|
|
|
2520
2721
|
reject(new Error("Connection timeout"));
|
|
2521
2722
|
}, 3e4);
|
|
2522
2723
|
let upgradeRejection = null;
|
|
2724
|
+
let upgradeRejectionReason = null;
|
|
2523
2725
|
ws.on("unexpected-response", (_req, res) => {
|
|
2524
2726
|
clearTimeout(connectionTimeout);
|
|
2727
|
+
const reason = classifyUpgradeRejection(res.headers);
|
|
2728
|
+
upgradeRejectionReason = reason;
|
|
2525
2729
|
const chunks = [];
|
|
2526
2730
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
2527
2731
|
res.on("end", () => {
|
|
@@ -2535,8 +2739,14 @@ function connectTunnel(options) {
|
|
|
2535
2739
|
}
|
|
2536
2740
|
const statusLine = `HTTP ${res.statusCode}${res.statusMessage ? ` ${res.statusMessage}` : ""}`;
|
|
2537
2741
|
upgradeRejection = detail ? `${statusLine}: ${detail}` : statusLine;
|
|
2538
|
-
|
|
2539
|
-
|
|
2742
|
+
if (reason === "do_code_updated") {
|
|
2743
|
+
onWarning?.("Relay redeployed \u2014 reconnecting");
|
|
2744
|
+
} else {
|
|
2745
|
+
onError?.(`Tunnel refused by relay (${upgradeRejection})`);
|
|
2746
|
+
}
|
|
2747
|
+
reject(
|
|
2748
|
+
new TunnelUpgradeRejectedError(`Tunnel handshake rejected: ${upgradeRejection}`, reason)
|
|
2749
|
+
);
|
|
2540
2750
|
});
|
|
2541
2751
|
});
|
|
2542
2752
|
ws.on("open", () => {
|
|
@@ -2582,8 +2792,14 @@ function connectTunnel(options) {
|
|
|
2582
2792
|
ws.on("error", (error2) => {
|
|
2583
2793
|
clearTimeout(connectionTimeout);
|
|
2584
2794
|
const detail = upgradeRejection ?? describeSocketError(error2, url);
|
|
2585
|
-
|
|
2586
|
-
|
|
2795
|
+
if (upgradeRejectionReason === "do_code_updated") {
|
|
2796
|
+
onWarning?.("Relay redeployed \u2014 reconnecting");
|
|
2797
|
+
} else {
|
|
2798
|
+
onError?.(`Connection error: ${detail}`);
|
|
2799
|
+
}
|
|
2800
|
+
reject(
|
|
2801
|
+
upgradeRejectionReason !== null ? new TunnelUpgradeRejectedError(detail, upgradeRejectionReason) : new Error(detail)
|
|
2802
|
+
);
|
|
2587
2803
|
});
|
|
2588
2804
|
ws.on("close", (code, reason) => {
|
|
2589
2805
|
const reasonStr = reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
|
|
@@ -2659,7 +2875,8 @@ var RunnerConnection = class {
|
|
|
2659
2875
|
onError: (error2) => events.onError?.(error2),
|
|
2660
2876
|
onResponse: () => events.onResponse?.(),
|
|
2661
2877
|
onDrainPing: () => events.onDrainPing?.(),
|
|
2662
|
-
onInfo: (message) => events.onInfo?.(message)
|
|
2878
|
+
onInfo: (message) => events.onInfo?.(message),
|
|
2879
|
+
onWarning: (message) => events.onWarning?.(message)
|
|
2663
2880
|
});
|
|
2664
2881
|
return;
|
|
2665
2882
|
} catch (error2) {
|
|
@@ -2670,7 +2887,12 @@ var RunnerConnection = class {
|
|
|
2670
2887
|
}
|
|
2671
2888
|
const delay = getReconnectDelay(this.reconnectAttempt);
|
|
2672
2889
|
events.onReconnecting?.(this.reconnectAttempt);
|
|
2673
|
-
|
|
2890
|
+
const retryMessage = `Connection failed, retrying in ${Math.round(delay / 1e3)}s...`;
|
|
2891
|
+
if (error2 instanceof TunnelUpgradeRejectedError && error2.reason === "do_code_updated") {
|
|
2892
|
+
events.onWarning?.(retryMessage);
|
|
2893
|
+
} else {
|
|
2894
|
+
events.onError?.(retryMessage);
|
|
2895
|
+
}
|
|
2674
2896
|
await this.sleep(delay);
|
|
2675
2897
|
}
|
|
2676
2898
|
}
|
|
@@ -2727,7 +2949,7 @@ import { homedir as homedir2 } from "os";
|
|
|
2727
2949
|
// src/lib/file-push.ts
|
|
2728
2950
|
import { randomUUID } from "crypto";
|
|
2729
2951
|
import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
|
|
2730
|
-
import { basename, dirname as
|
|
2952
|
+
import { basename, dirname as dirname3, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
|
|
2731
2953
|
var FILE_MODE = 384;
|
|
2732
2954
|
var DIRECTORY_MODE = 448;
|
|
2733
2955
|
async function writePushedFile(request) {
|
|
@@ -2758,9 +2980,9 @@ async function writePushedFile(request) {
|
|
|
2758
2980
|
}
|
|
2759
2981
|
try {
|
|
2760
2982
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
2761
|
-
|
|
2983
|
+
dirname3(candidate)
|
|
2762
2984
|
);
|
|
2763
|
-
const realTarget =
|
|
2985
|
+
const realTarget = join3(existingAncestor, ...missingSegments, basename(candidate));
|
|
2764
2986
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
2765
2987
|
if (allowedDirectory === null) {
|
|
2766
2988
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -2770,8 +2992,8 @@ async function writePushedFile(request) {
|
|
|
2770
2992
|
}
|
|
2771
2993
|
if (missingSegments.length > 0) {
|
|
2772
2994
|
await createMissingDirectories(existingAncestor, missingSegments);
|
|
2773
|
-
const realParent = await realpath(
|
|
2774
|
-
if (realParent !==
|
|
2995
|
+
const realParent = await realpath(dirname3(realTarget));
|
|
2996
|
+
if (realParent !== dirname3(realTarget) || !contains(allowedDirectory, realTarget)) {
|
|
2775
2997
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
2776
2998
|
path: realTarget,
|
|
2777
2999
|
bytes,
|
|
@@ -2796,7 +3018,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
2796
3018
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
2797
3019
|
return null;
|
|
2798
3020
|
}
|
|
2799
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
3021
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join3(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
2800
3022
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
2801
3023
|
return null;
|
|
2802
3024
|
}
|
|
@@ -2814,7 +3036,7 @@ async function resolveNearestExistingAncestor(directory) {
|
|
|
2814
3036
|
try {
|
|
2815
3037
|
return { existingAncestor: await realpath(current), missingSegments };
|
|
2816
3038
|
} catch (err) {
|
|
2817
|
-
const parent =
|
|
3039
|
+
const parent = dirname3(current);
|
|
2818
3040
|
if (err.code !== "ENOENT" || parent === current) {
|
|
2819
3041
|
throw err;
|
|
2820
3042
|
}
|
|
@@ -2869,13 +3091,13 @@ function contains(realDirectory, realTarget) {
|
|
|
2869
3091
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
2870
3092
|
let current = existingAncestor;
|
|
2871
3093
|
for (const segment of missingSegments) {
|
|
2872
|
-
current =
|
|
3094
|
+
current = join3(current, segment);
|
|
2873
3095
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
2874
3096
|
await chmod(current, DIRECTORY_MODE);
|
|
2875
3097
|
}
|
|
2876
3098
|
}
|
|
2877
3099
|
async function writeAtomically(realTarget, content) {
|
|
2878
|
-
const temporaryPath =
|
|
3100
|
+
const temporaryPath = join3(dirname3(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
2879
3101
|
let handle;
|
|
2880
3102
|
try {
|
|
2881
3103
|
handle = await open2(temporaryPath, "wx", FILE_MODE);
|
|
@@ -3148,6 +3370,7 @@ var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
|
3148
3370
|
var HEARTBEAT_MS = 6e4;
|
|
3149
3371
|
var ABSOLUTE_MAX_PROCESSING_MS = 6 * 60 * 60 * 1e3;
|
|
3150
3372
|
var B2_ABANDONMENT_MIN_PINNED_MS = 3 * 6e4;
|
|
3373
|
+
var AMBIGUOUS_FINISH_MAX_PINNED_MS = 3 * 6e4;
|
|
3151
3374
|
var B2_ABANDONMENT_RECHECK_MS = HEARTBEAT_MS;
|
|
3152
3375
|
var POLL_MISS_GRACE_MS = HEARTBEAT_MS;
|
|
3153
3376
|
var MAX_SUPERSEDED_CONVERSATIONS = 256;
|
|
@@ -3982,6 +4205,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3982
4205
|
return this.reattachRedrive(conv, sessionId, message, ocId);
|
|
3983
4206
|
}
|
|
3984
4207
|
if (ongoing === false) {
|
|
4208
|
+
if (state === "running" && isAmbiguousFinishPinnedRunning(messages, ocId ?? "")) {
|
|
4209
|
+
return this.settleRedrive(conv, sessionId, message, ocId, messages, "done");
|
|
4210
|
+
}
|
|
3985
4211
|
this.clearRedriveUnresolved(message.id);
|
|
3986
4212
|
void this.postSignal(conv.id, message.id, "redrive_redispatched");
|
|
3987
4213
|
return "dispatch";
|
|
@@ -4593,7 +4819,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4593
4819
|
deliveryDeadlineAnchored: false,
|
|
4594
4820
|
b2PinnedSinceMs: 0,
|
|
4595
4821
|
b2LastDescendantCheckMs: 0,
|
|
4596
|
-
b2AbandonedSignalled: false
|
|
4822
|
+
b2AbandonedSignalled: false,
|
|
4823
|
+
ambiguousPinnedSinceMs: 0,
|
|
4824
|
+
ambiguousResolved: false
|
|
4597
4825
|
});
|
|
4598
4826
|
}
|
|
4599
4827
|
/**
|
|
@@ -4672,7 +4900,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4672
4900
|
deliveryDeadlineAnchored: false,
|
|
4673
4901
|
b2PinnedSinceMs: 0,
|
|
4674
4902
|
b2LastDescendantCheckMs: 0,
|
|
4675
|
-
b2AbandonedSignalled: false
|
|
4903
|
+
b2AbandonedSignalled: false,
|
|
4904
|
+
ambiguousPinnedSinceMs: 0,
|
|
4905
|
+
ambiguousResolved: false
|
|
4676
4906
|
});
|
|
4677
4907
|
}
|
|
4678
4908
|
/**
|
|
@@ -4940,6 +5170,49 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4940
5170
|
}
|
|
4941
5171
|
}
|
|
4942
5172
|
}
|
|
5173
|
+
const ambiguousPinnedNow = activelyRunning && isAmbiguousFinishPinnedRunning(messages, inFlight.opencodeMessageId);
|
|
5174
|
+
if (!ambiguousPinnedNow) {
|
|
5175
|
+
if (snapshotReadable) {
|
|
5176
|
+
inFlight.ambiguousPinnedSinceMs = 0;
|
|
5177
|
+
inFlight.ambiguousResolved = false;
|
|
5178
|
+
}
|
|
5179
|
+
} else {
|
|
5180
|
+
if (inFlight.ambiguousResolved) {
|
|
5181
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
5182
|
+
return;
|
|
5183
|
+
}
|
|
5184
|
+
if (inFlight.ambiguousPinnedSinceMs === 0) {
|
|
5185
|
+
inFlight.ambiguousPinnedSinceMs = this.now();
|
|
5186
|
+
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
5187
|
+
const finish = reply?.info?.finish ?? reply?.finish;
|
|
5188
|
+
this.log({
|
|
5189
|
+
level: "warn",
|
|
5190
|
+
message: `Message ${id.slice(0, 8)} pinned running by an unrecognised finish ("${finish ?? "(absent)"}") \u2014 corroborating against opencode's session status before settling (issue #1493)`,
|
|
5191
|
+
conversation_id: conv.id,
|
|
5192
|
+
message_id: id
|
|
5193
|
+
});
|
|
5194
|
+
}
|
|
5195
|
+
const pinnedForMs = this.now() - inFlight.ambiguousPinnedSinceMs;
|
|
5196
|
+
const ongoing = await isSessionOngoing(this.port, sessionId);
|
|
5197
|
+
if (isAmbiguousFinishResolved({
|
|
5198
|
+
pinnedForMs,
|
|
5199
|
+
maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
|
|
5200
|
+
sessionOngoing: ongoing
|
|
5201
|
+
})) {
|
|
5202
|
+
inFlight.ambiguousResolved = true;
|
|
5203
|
+
this.log({
|
|
5204
|
+
level: "warn",
|
|
5205
|
+
message: `Message ${id.slice(0, 8)} ambiguous-finish-pinned for ${Math.round(pinnedForMs / 1e3)}s \u2014 resolved (${ongoing === false ? "session confirmed not-ongoing" : "pin exceeded the no-hang cap"}) \u2014 settling done`,
|
|
5206
|
+
conversation_id: conv.id,
|
|
5207
|
+
message_id: id
|
|
5208
|
+
});
|
|
5209
|
+
void this.postSignal(conv.id, id, "ambiguous_finish_resolved", {
|
|
5210
|
+
watched_for_ms: pinnedForMs
|
|
5211
|
+
});
|
|
5212
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
5213
|
+
return;
|
|
5214
|
+
}
|
|
5215
|
+
}
|
|
4943
5216
|
if (activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
|
|
4944
5217
|
this.log({
|
|
4945
5218
|
level: "warn",
|
|
@@ -5194,48 +5467,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5194
5467
|
const ocId = row.opencode_message_id;
|
|
5195
5468
|
const state = messageRunState(messages, ocId ?? "");
|
|
5196
5469
|
if (state === "done") {
|
|
5197
|
-
|
|
5198
|
-
this.log({
|
|
5199
|
-
level: "debug",
|
|
5200
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} markDone is terminally undeliverable \u2014 left to the cron; skipping until it leaves processing`,
|
|
5201
|
-
conversation_id: row.conversation_id,
|
|
5202
|
-
message_id: row.id
|
|
5203
|
-
});
|
|
5204
|
-
return;
|
|
5205
|
-
}
|
|
5206
|
-
this.log({
|
|
5207
|
-
level: "info",
|
|
5208
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} completed while unwatched \u2014 marking done`,
|
|
5209
|
-
conversation_id: row.conversation_id,
|
|
5210
|
-
message_id: row.id
|
|
5211
|
-
});
|
|
5212
|
-
try {
|
|
5213
|
-
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
5214
|
-
const usage = messageUsage(messages, ocId ?? "");
|
|
5215
|
-
await this.markDone(row.conversation_id, row.id, sessionId, ocId, title, usage);
|
|
5216
|
-
} catch (err) {
|
|
5217
|
-
if (err instanceof ChannelAuthError) throw err;
|
|
5218
|
-
if (err instanceof ChannelTerminalError) {
|
|
5219
|
-
this.doneUndeliverable.add(row.id);
|
|
5220
|
-
this.log({
|
|
5221
|
-
level: "warn",
|
|
5222
|
-
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} done (terminal HTTP ${err.status}) \u2014 parking until it leaves processing; leaving for the cron safety net: ${err.message}`,
|
|
5223
|
-
conversation_id: row.conversation_id,
|
|
5224
|
-
message_id: row.id
|
|
5225
|
-
});
|
|
5226
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_undeliverable");
|
|
5227
|
-
return;
|
|
5228
|
-
}
|
|
5229
|
-
this.log({
|
|
5230
|
-
level: "warn",
|
|
5231
|
-
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} done (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
5232
|
-
conversation_id: row.conversation_id,
|
|
5233
|
-
message_id: row.id
|
|
5234
|
-
});
|
|
5235
|
-
return;
|
|
5236
|
-
}
|
|
5237
|
-
this.dontRedispatch.delete(row.id);
|
|
5238
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
5470
|
+
await this.deliverReadoptedDone(sessionId, row, messages, ocId);
|
|
5239
5471
|
return;
|
|
5240
5472
|
}
|
|
5241
5473
|
const restartAborted = state === "failed" && sessionOngoing === false && isAbortedTerminalReply(messages, ocId ?? "");
|
|
@@ -5300,6 +5532,17 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5300
5532
|
const ongoing = sessionOngoing;
|
|
5301
5533
|
statusReadableOngoing = ongoing;
|
|
5302
5534
|
if (ongoing === false) {
|
|
5535
|
+
if (isAmbiguousFinishPinnedRunning(messages, ocId ?? "")) {
|
|
5536
|
+
const finish = reply?.info?.finish ?? reply?.finish;
|
|
5537
|
+
this.log({
|
|
5538
|
+
level: "info",
|
|
5539
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} has an ambiguous finish ("${finish ?? "(absent)"}") but session ${sessionId.slice(0, 8)} is confirmed not-ongoing per GET /session/status \u2014 delivering the existing reply instead of re-dispatching`,
|
|
5540
|
+
conversation_id: row.conversation_id,
|
|
5541
|
+
message_id: row.id
|
|
5542
|
+
});
|
|
5543
|
+
await this.deliverReadoptedDone(sessionId, row, messages, ocId);
|
|
5544
|
+
return;
|
|
5545
|
+
}
|
|
5303
5546
|
this.log({
|
|
5304
5547
|
level: "info",
|
|
5305
5548
|
message: `Re-adopt: message ${row.id.slice(0, 8)} running/${shape} but session ${sessionId.slice(0, 8)} is not-ongoing per GET /session/status (absent/idle) \u2014 re-dispatching from scratch (status-gated recovery)`,
|
|
@@ -5376,6 +5619,63 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5376
5619
|
}
|
|
5377
5620
|
await this.forceReadoptRun(sessionId, row);
|
|
5378
5621
|
}
|
|
5622
|
+
/**
|
|
5623
|
+
* Deliver a `processing` row whose correlated reply already completed while
|
|
5624
|
+
* nobody was watching (ADR-0046) — the `readoptOne` `state === 'done'` body,
|
|
5625
|
+
* extracted (#1493 Task 2.4) so the ambiguous-finish guard above can call the
|
|
5626
|
+
* SAME delivery instead of duplicating it.
|
|
5627
|
+
*
|
|
5628
|
+
* EVEN IF the row was previously parked in `dontRedispatch` (a give-up stops
|
|
5629
|
+
* re-dispatch, not delivery — Bugbot #202). Guarded EXACTLY like the watcher's
|
|
5630
|
+
* `settleMessageDone`: auth re-throws; terminal → park in `doneUndeliverable` +
|
|
5631
|
+
* leave for cron; transient → log + leave for the next drain (the still-
|
|
5632
|
+
* `processing` row is re-read and retried). markDone is idempotent server-side
|
|
5633
|
+
* (status-gated), so a repeat can never double-post.
|
|
5634
|
+
*/
|
|
5635
|
+
async deliverReadoptedDone(sessionId, row, messages, ocId) {
|
|
5636
|
+
if (this.doneUndeliverable.has(row.id)) {
|
|
5637
|
+
this.log({
|
|
5638
|
+
level: "debug",
|
|
5639
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} markDone is terminally undeliverable \u2014 left to the cron; skipping until it leaves processing`,
|
|
5640
|
+
conversation_id: row.conversation_id,
|
|
5641
|
+
message_id: row.id
|
|
5642
|
+
});
|
|
5643
|
+
return;
|
|
5644
|
+
}
|
|
5645
|
+
this.log({
|
|
5646
|
+
level: "info",
|
|
5647
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} completed while unwatched \u2014 marking done`,
|
|
5648
|
+
conversation_id: row.conversation_id,
|
|
5649
|
+
message_id: row.id
|
|
5650
|
+
});
|
|
5651
|
+
try {
|
|
5652
|
+
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
5653
|
+
const usage = messageUsage(messages, ocId ?? "");
|
|
5654
|
+
await this.markDone(row.conversation_id, row.id, sessionId, ocId, title, usage);
|
|
5655
|
+
} catch (err) {
|
|
5656
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
5657
|
+
if (err instanceof ChannelTerminalError) {
|
|
5658
|
+
this.doneUndeliverable.add(row.id);
|
|
5659
|
+
this.log({
|
|
5660
|
+
level: "warn",
|
|
5661
|
+
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} done (terminal HTTP ${err.status}) \u2014 parking until it leaves processing; leaving for the cron safety net: ${err.message}`,
|
|
5662
|
+
conversation_id: row.conversation_id,
|
|
5663
|
+
message_id: row.id
|
|
5664
|
+
});
|
|
5665
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_undeliverable");
|
|
5666
|
+
return;
|
|
5667
|
+
}
|
|
5668
|
+
this.log({
|
|
5669
|
+
level: "warn",
|
|
5670
|
+
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} done (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
5671
|
+
conversation_id: row.conversation_id,
|
|
5672
|
+
message_id: row.id
|
|
5673
|
+
});
|
|
5674
|
+
return;
|
|
5675
|
+
}
|
|
5676
|
+
this.dontRedispatch.delete(row.id);
|
|
5677
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
5678
|
+
}
|
|
5379
5679
|
/**
|
|
5380
5680
|
* Re-dispatch an orphaned (`unknown`/null-id) `processing` row (ADR-0046 §2).
|
|
5381
5681
|
*
|
|
@@ -6005,17 +6305,25 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6005
6305
|
* the aborted-in-flight production bug after a restart.
|
|
6006
6306
|
* - `b2` — a COMPLETED reply pinned running only by `finish === "tool-calls"`
|
|
6007
6307
|
* (the sub-agent preamble — #253's shape).
|
|
6008
|
-
* - `
|
|
6009
|
-
*
|
|
6010
|
-
*
|
|
6011
|
-
* `
|
|
6308
|
+
* - `ambiguous` — a COMPLETED, non-errored reply whose `finish` is neither
|
|
6309
|
+
* "tool-calls" nor "stop" (issue #1493, class 4 — see
|
|
6310
|
+
* `isAmbiguousFinishPinnedRunning`/Task 2.4).
|
|
6311
|
+
* - `other` — any other shape (defensive; a running row is normally b1, b2 or
|
|
6312
|
+
* ambiguous).
|
|
6313
|
+
* Reads `info.time.completed` / `info.finish` / `info.error` (tolerating the
|
|
6314
|
+
* legacy top-level shape) directly rather than re-importing the module-private
|
|
6315
|
+
* `completedOf`/`finishOf`/`errorOf` — this is a display label only, not a
|
|
6316
|
+
* correctness predicate (that is `isAmbiguousFinishPinnedRunning`'s job).
|
|
6012
6317
|
*/
|
|
6013
6318
|
replyCompletionShape(reply) {
|
|
6014
6319
|
if (!reply) return "other";
|
|
6015
6320
|
const completed = reply.info?.time?.completed ?? reply.time?.completed;
|
|
6016
6321
|
if (completed == null) return "b1";
|
|
6017
6322
|
const finish = reply.info?.finish ?? reply.finish;
|
|
6018
|
-
|
|
6323
|
+
if (finish === "tool-calls") return "b2";
|
|
6324
|
+
const error2 = reply.info?.error ?? reply.error;
|
|
6325
|
+
if (finish !== "stop" && error2 == null) return "ambiguous";
|
|
6326
|
+
return "other";
|
|
6019
6327
|
}
|
|
6020
6328
|
/**
|
|
6021
6329
|
* Attribute a surfaced interaction to the in-flight message it paused on (M-1).
|
|
@@ -6632,7 +6940,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
6632
6940
|
if (trimmed === "") {
|
|
6633
6941
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
6634
6942
|
}
|
|
6635
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
6943
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join4(homeDir, trimmed.slice(2)) : trimmed;
|
|
6636
6944
|
if (!isAbsolute2(expanded)) {
|
|
6637
6945
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
6638
6946
|
}
|
|
@@ -6826,7 +7134,9 @@ async function handleAuthError(state, error2) {
|
|
|
6826
7134
|
);
|
|
6827
7135
|
const newAuthHeader = getAuthHeader(credentials2);
|
|
6828
7136
|
return { success: true, newAuthHeader };
|
|
6829
|
-
} catch {
|
|
7137
|
+
} catch (error3) {
|
|
7138
|
+
const message = error3 instanceof Error ? error3.message : String(error3);
|
|
7139
|
+
logActivity(state, { type: "error", error: `Re-authentication failed: ${message}` });
|
|
6830
7140
|
return { success: false };
|
|
6831
7141
|
}
|
|
6832
7142
|
}
|
|
@@ -6931,6 +7241,10 @@ async function driveChannels(state, driver) {
|
|
|
6931
7241
|
}
|
|
6932
7242
|
}
|
|
6933
7243
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
7244
|
+
var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
|
|
7245
|
+
function sessionDbPath() {
|
|
7246
|
+
return join4(homedir3(), ".local", "share", "opencode", "opencode.db");
|
|
7247
|
+
}
|
|
6934
7248
|
async function runSweep(state, driver, config) {
|
|
6935
7249
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
6936
7250
|
try {
|
|
@@ -6973,6 +7287,25 @@ async function runSweep(state, driver, config) {
|
|
|
6973
7287
|
type: "info",
|
|
6974
7288
|
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote} (${mode})`
|
|
6975
7289
|
});
|
|
7290
|
+
const reclaimResult = await reclaimSessionDbSpace({
|
|
7291
|
+
dbPath: sessionDbPath(),
|
|
7292
|
+
maxPages: SESSION_DB_RECLAIM_MAX_PAGES,
|
|
7293
|
+
allowFullVacuum: protectedNow.size === 0
|
|
7294
|
+
});
|
|
7295
|
+
if (reclaimResult.ok) {
|
|
7296
|
+
const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
|
|
7297
|
+
const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
|
|
7298
|
+
const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
|
|
7299
|
+
logActivity(state, {
|
|
7300
|
+
type: "info",
|
|
7301
|
+
message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${checkpointNote}`
|
|
7302
|
+
});
|
|
7303
|
+
} else {
|
|
7304
|
+
logActivity(state, {
|
|
7305
|
+
type: "info",
|
|
7306
|
+
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})`
|
|
7307
|
+
});
|
|
7308
|
+
}
|
|
6976
7309
|
} catch (error2) {
|
|
6977
7310
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
6978
7311
|
logActivity(state, {
|
|
@@ -6993,6 +7326,22 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
6993
7326
|
for (const warning2 of config.warnings) {
|
|
6994
7327
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
6995
7328
|
}
|
|
7329
|
+
const dbBytes = statSessionDbBytes(homedir3());
|
|
7330
|
+
void (async () => {
|
|
7331
|
+
const reclaimSkipReason = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
7332
|
+
const sizeWarning = buildSessionStoreSizeWarning({
|
|
7333
|
+
dbBytes,
|
|
7334
|
+
cleanupEnabled: config.enabled,
|
|
7335
|
+
reclaimSkipReason
|
|
7336
|
+
});
|
|
7337
|
+
if (sizeWarning !== null) {
|
|
7338
|
+
logActivity(state, { type: "info", level: "warn", message: sizeWarning });
|
|
7339
|
+
}
|
|
7340
|
+
})().catch((err) => {
|
|
7341
|
+
console.error(
|
|
7342
|
+
`[scheduleSessionCleanup] size-warning preflight failed: ${err instanceof Error ? err.message : String(err)}`
|
|
7343
|
+
);
|
|
7344
|
+
});
|
|
6996
7345
|
if (!config.enabled) return;
|
|
6997
7346
|
logActivity(state, {
|
|
6998
7347
|
type: "info",
|
|
@@ -7574,6 +7923,13 @@ async function run(options) {
|
|
|
7574
7923
|
logActivity(state, { type: "error", error: error2 });
|
|
7575
7924
|
if (state.interactive) displayStatus(state);
|
|
7576
7925
|
},
|
|
7926
|
+
// `warn`, not `info`: `forwardRunnerActivity`'s FORWARDED_LEVELS floor is
|
|
7927
|
+
// {'warn','error'}, so an `info` entry would never leave the machine and
|
|
7928
|
+
// an operator couldn't correlate a reconnect storm with a relay deploy.
|
|
7929
|
+
onWarning: (message) => {
|
|
7930
|
+
logActivity(state, { type: "info", level: "warn", message });
|
|
7931
|
+
if (state.interactive) displayStatus(state);
|
|
7932
|
+
},
|
|
7577
7933
|
// Web traffic is proxied transparently; note opencode is live and stamp
|
|
7578
7934
|
// proxied activity so the idle loop treats interactive proxy use as work.
|
|
7579
7935
|
// Fires per forwarded response head (incl. every SSE open) and excludes
|