@evident-ai/cli 3.2.1-dev.f48fa70 → 3.3.0
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 +5 -3
- package/dist/index.js +431 -99
- package/dist/index.js.map +1 -1
- package/package.json +2 -4
package/dist/index.js
CHANGED
|
@@ -212,51 +212,80 @@ var api = {
|
|
|
212
212
|
|
|
213
213
|
// src/lib/keychain.ts
|
|
214
214
|
var SERVICE_NAME = "evident-cli";
|
|
215
|
-
var
|
|
216
|
-
|
|
215
|
+
var PROBE_SERVICE_NAME = "evident-cli-probe";
|
|
216
|
+
var keychainWarned = false;
|
|
217
|
+
function warnUnavailable(err) {
|
|
218
|
+
if (!keychainWarned) {
|
|
219
|
+
keychainWarned = true;
|
|
220
|
+
console.warn(
|
|
221
|
+
`System keychain unavailable, falling back to file-based credential storage: ${err instanceof Error ? err.message : String(err)}`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
var keychain;
|
|
226
|
+
async function probeKeychain() {
|
|
217
227
|
try {
|
|
218
|
-
const keytar = await import("keytar");
|
|
228
|
+
const keytar = await import("@napi-rs/keyring/keytar.js");
|
|
219
229
|
if (typeof keytar.setPassword !== "function") {
|
|
220
230
|
return null;
|
|
221
231
|
}
|
|
232
|
+
await keytar.findCredentials(PROBE_SERVICE_NAME);
|
|
222
233
|
return keytar;
|
|
223
234
|
} catch (err) {
|
|
224
|
-
|
|
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
|
-
}
|
|
235
|
+
warnUnavailable(err);
|
|
230
236
|
return null;
|
|
231
237
|
}
|
|
232
238
|
}
|
|
239
|
+
function resolveKeychain() {
|
|
240
|
+
if (!keychain) {
|
|
241
|
+
keychain = probeKeychain();
|
|
242
|
+
}
|
|
243
|
+
return keychain;
|
|
244
|
+
}
|
|
233
245
|
function keychainAccount() {
|
|
234
246
|
return getApiUrlConfig();
|
|
235
247
|
}
|
|
248
|
+
function storeInFileFallback(credentials2) {
|
|
249
|
+
setCredentials({
|
|
250
|
+
token: credentials2.token,
|
|
251
|
+
user: credentials2.user,
|
|
252
|
+
expiresAt: credentials2.expiresAt
|
|
253
|
+
});
|
|
254
|
+
}
|
|
236
255
|
async function storeToken(credentials2) {
|
|
237
|
-
const keytar = await
|
|
256
|
+
const keytar = await resolveKeychain();
|
|
238
257
|
if (keytar) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
});
|
|
258
|
+
try {
|
|
259
|
+
await keytar.setPassword(SERVICE_NAME, keychainAccount(), JSON.stringify(credentials2));
|
|
260
|
+
return;
|
|
261
|
+
} catch (err) {
|
|
262
|
+
warnUnavailable(err);
|
|
263
|
+
}
|
|
246
264
|
}
|
|
265
|
+
storeInFileFallback(credentials2);
|
|
247
266
|
}
|
|
248
267
|
async function getToken() {
|
|
249
|
-
const keytar = await
|
|
268
|
+
const keytar = await resolveKeychain();
|
|
250
269
|
if (keytar) {
|
|
251
270
|
const account = keychainAccount();
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
271
|
+
try {
|
|
272
|
+
const stored = await keytar.getPassword(SERVICE_NAME, account);
|
|
273
|
+
if (stored) {
|
|
274
|
+
try {
|
|
275
|
+
return JSON.parse(stored);
|
|
276
|
+
} catch {
|
|
277
|
+
try {
|
|
278
|
+
await keytar.deletePassword(SERVICE_NAME, account);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
console.warn(
|
|
281
|
+
`Failed to clear invalid keychain entry for ${account}: ${err instanceof Error ? err.message : String(err)}`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
259
286
|
}
|
|
287
|
+
} catch (err) {
|
|
288
|
+
warnUnavailable(err);
|
|
260
289
|
}
|
|
261
290
|
}
|
|
262
291
|
const creds = getCredentials();
|
|
@@ -273,7 +302,7 @@ function toError(err) {
|
|
|
273
302
|
return err instanceof Error ? err : new Error(String(err));
|
|
274
303
|
}
|
|
275
304
|
async function deleteToken(options = {}) {
|
|
276
|
-
const keytar = await
|
|
305
|
+
const keytar = await resolveKeychain();
|
|
277
306
|
const failures = [];
|
|
278
307
|
if (keytar) {
|
|
279
308
|
if (options.all) {
|
|
@@ -286,14 +315,25 @@ async function deleteToken(options = {}) {
|
|
|
286
315
|
await Promise.all(
|
|
287
316
|
accounts.map(async (entry) => {
|
|
288
317
|
try {
|
|
289
|
-
await keytar.deletePassword(SERVICE_NAME, entry.account);
|
|
318
|
+
const deleted = await keytar.deletePassword(SERVICE_NAME, entry.account);
|
|
319
|
+
if (!deleted) {
|
|
320
|
+
failures.push({
|
|
321
|
+
type: "delete",
|
|
322
|
+
account: entry.account,
|
|
323
|
+
error: new Error("deletePassword resolved false")
|
|
324
|
+
});
|
|
325
|
+
}
|
|
290
326
|
} catch (err) {
|
|
291
327
|
failures.push({ type: "delete", account: entry.account, error: toError(err) });
|
|
292
328
|
}
|
|
293
329
|
})
|
|
294
330
|
);
|
|
295
331
|
} else {
|
|
296
|
-
|
|
332
|
+
try {
|
|
333
|
+
await keytar.deletePassword(SERVICE_NAME, keychainAccount());
|
|
334
|
+
} catch (err) {
|
|
335
|
+
warnUnavailable(err);
|
|
336
|
+
}
|
|
297
337
|
}
|
|
298
338
|
}
|
|
299
339
|
if (options.all) {
|
|
@@ -1052,6 +1092,7 @@ var MAX_FILE_SYNC_DIRECTORIES = 16;
|
|
|
1052
1092
|
|
|
1053
1093
|
// ../../packages/types/src/logging/index.ts
|
|
1054
1094
|
var CORRELATION_ID_HEADER = "x-evident-correlation-id";
|
|
1095
|
+
var FORWARD_FAILURE_REASON_HEADER = "X-Evident-Failure-Reason";
|
|
1055
1096
|
function log(level, event, fields) {
|
|
1056
1097
|
const method = level === "debug" ? "log" : level;
|
|
1057
1098
|
try {
|
|
@@ -2106,7 +2147,9 @@ function messageRunState(messages, userMessageId) {
|
|
|
2106
2147
|
}
|
|
2107
2148
|
if (!reply) return "queued";
|
|
2108
2149
|
if (isAssistantInFlight(reply)) return "running";
|
|
2109
|
-
|
|
2150
|
+
if (errorOf(reply) != null) return "failed";
|
|
2151
|
+
if (isAmbiguousTerminalFinish(reply)) return "running";
|
|
2152
|
+
return "done";
|
|
2110
2153
|
}
|
|
2111
2154
|
function isPreamblePinnedRunning(messages, userMessageId) {
|
|
2112
2155
|
if (messageRunState(messages, userMessageId) !== "running") return false;
|
|
@@ -2116,6 +2159,19 @@ function isPreamblePinnedRunning(messages, userMessageId) {
|
|
|
2116
2159
|
function isB2AbandonmentConfirmed(params) {
|
|
2117
2160
|
return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false;
|
|
2118
2161
|
}
|
|
2162
|
+
function isAmbiguousTerminalFinish(m) {
|
|
2163
|
+
if (completedOf(m) == null) return false;
|
|
2164
|
+
if (errorOf(m) != null) return false;
|
|
2165
|
+
const finish = finishOf(m);
|
|
2166
|
+
return finish !== "tool-calls" && finish !== "stop";
|
|
2167
|
+
}
|
|
2168
|
+
function isAmbiguousFinishPinnedRunning(messages, userMessageId) {
|
|
2169
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
2170
|
+
return isAmbiguousTerminalFinish(reply);
|
|
2171
|
+
}
|
|
2172
|
+
function isAmbiguousFinishResolved(params) {
|
|
2173
|
+
return params.sessionOngoing === false || params.pinnedForMs >= params.maxPinnedMs;
|
|
2174
|
+
}
|
|
2119
2175
|
function messageError(messages, userMessageId) {
|
|
2120
2176
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
2121
2177
|
const error2 = errorOf(reply);
|
|
@@ -2341,10 +2397,128 @@ function statSessionDbBytes(homeDir) {
|
|
|
2341
2397
|
}
|
|
2342
2398
|
}
|
|
2343
2399
|
function buildSessionStoreSizeWarning(input) {
|
|
2344
|
-
const { dbBytes, cleanupEnabled } = input;
|
|
2345
|
-
if (dbBytes === null || dbBytes <= LARGE_DB_THRESHOLD_BYTES
|
|
2400
|
+
const { dbBytes, cleanupEnabled, reclaimSkipReason } = input;
|
|
2401
|
+
if (dbBytes === null || dbBytes <= LARGE_DB_THRESHOLD_BYTES) return null;
|
|
2346
2402
|
const mib = Math.round(dbBytes / 1024 / 1024);
|
|
2347
|
-
|
|
2403
|
+
if (!cleanupEnabled) {
|
|
2404
|
+
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.`;
|
|
2405
|
+
}
|
|
2406
|
+
if (reclaimSkipReason === "sqlite-unavailable" || reclaimSkipReason === "insufficient-disk-space") {
|
|
2407
|
+
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";
|
|
2408
|
+
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.`;
|
|
2409
|
+
}
|
|
2410
|
+
return null;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
// src/lib/opencode/session-db-reclaim.ts
|
|
2414
|
+
import { statSync as statSync3, statfsSync } from "fs";
|
|
2415
|
+
import { dirname as dirname2 } from "path";
|
|
2416
|
+
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
2417
|
+
try {
|
|
2418
|
+
const fsStats = statfsSync(dirname2(dbPath));
|
|
2419
|
+
const availableBytes = fsStats.bavail * fsStats.bsize;
|
|
2420
|
+
if (availableBytes < requiredBytes) {
|
|
2421
|
+
return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
|
|
2422
|
+
}
|
|
2423
|
+
return null;
|
|
2424
|
+
} catch (err) {
|
|
2425
|
+
return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
function readLogicalBytes(db) {
|
|
2429
|
+
const pageCount = db.prepare("PRAGMA page_count").get().page_count;
|
|
2430
|
+
const pageSize = db.prepare("PRAGMA page_size").get().page_size;
|
|
2431
|
+
return pageCount * pageSize;
|
|
2432
|
+
}
|
|
2433
|
+
function readCheckpointResult(db) {
|
|
2434
|
+
const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
2435
|
+
return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
|
|
2436
|
+
}
|
|
2437
|
+
async function probeReclaimAvailability(input) {
|
|
2438
|
+
const { dbPath, requiredBytes } = input;
|
|
2439
|
+
let sqlite;
|
|
2440
|
+
try {
|
|
2441
|
+
sqlite = await import("sqlite");
|
|
2442
|
+
} catch (err) {
|
|
2443
|
+
console.warn(
|
|
2444
|
+
`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2445
|
+
);
|
|
2446
|
+
return "sqlite-unavailable";
|
|
2447
|
+
}
|
|
2448
|
+
let autoVacuum = null;
|
|
2449
|
+
try {
|
|
2450
|
+
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
2451
|
+
try {
|
|
2452
|
+
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
2453
|
+
} finally {
|
|
2454
|
+
db.close();
|
|
2455
|
+
}
|
|
2456
|
+
} catch (err) {
|
|
2457
|
+
console.warn(
|
|
2458
|
+
`[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2459
|
+
);
|
|
2460
|
+
}
|
|
2461
|
+
if (autoVacuum !== 0) return null;
|
|
2462
|
+
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? "insufficient-disk-space" : null;
|
|
2463
|
+
}
|
|
2464
|
+
async function reclaimSessionDbSpace(input) {
|
|
2465
|
+
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
2466
|
+
let sqlite;
|
|
2467
|
+
try {
|
|
2468
|
+
sqlite = await import("sqlite");
|
|
2469
|
+
} catch (err) {
|
|
2470
|
+
console.warn(
|
|
2471
|
+
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2472
|
+
);
|
|
2473
|
+
return { ok: false, skipped: "sqlite-unavailable" };
|
|
2474
|
+
}
|
|
2475
|
+
const { DatabaseSync } = sqlite;
|
|
2476
|
+
let db;
|
|
2477
|
+
try {
|
|
2478
|
+
db = new DatabaseSync(dbPath);
|
|
2479
|
+
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
2480
|
+
if (autoVacuum === 0) {
|
|
2481
|
+
if (!allowFullVacuum) {
|
|
2482
|
+
console.warn(
|
|
2483
|
+
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
|
|
2484
|
+
);
|
|
2485
|
+
return { ok: false, skipped: "full-vacuum-blocked" };
|
|
2486
|
+
}
|
|
2487
|
+
const fileBytesForGuard = statSync3(dbPath).size;
|
|
2488
|
+
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
2489
|
+
if (skipReason !== null) {
|
|
2490
|
+
console.warn(
|
|
2491
|
+
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
|
|
2492
|
+
);
|
|
2493
|
+
return { ok: false, skipped: "insufficient-disk-space" };
|
|
2494
|
+
}
|
|
2495
|
+
const beforeBytes = readLogicalBytes(db);
|
|
2496
|
+
db.exec("PRAGMA auto_vacuum=INCREMENTAL");
|
|
2497
|
+
db.exec("VACUUM");
|
|
2498
|
+
const afterBytes = readLogicalBytes(db);
|
|
2499
|
+
const checkpoint = readCheckpointResult(db);
|
|
2500
|
+
return { ok: true, mode: "convert", beforeBytes, afterBytes, checkpoint };
|
|
2501
|
+
}
|
|
2502
|
+
if (autoVacuum === 2) {
|
|
2503
|
+
const beforeBytes = readLogicalBytes(db);
|
|
2504
|
+
const bound = Math.max(0, Math.trunc(maxPages));
|
|
2505
|
+
db.exec(`PRAGMA incremental_vacuum(${bound})`);
|
|
2506
|
+
const afterBytes = readLogicalBytes(db);
|
|
2507
|
+
const checkpoint = readCheckpointResult(db);
|
|
2508
|
+
return { ok: true, mode: "incremental", beforeBytes, afterBytes, checkpoint };
|
|
2509
|
+
}
|
|
2510
|
+
console.warn(
|
|
2511
|
+
`[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
|
|
2512
|
+
);
|
|
2513
|
+
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
2514
|
+
} catch (err) {
|
|
2515
|
+
console.error(
|
|
2516
|
+
`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2517
|
+
);
|
|
2518
|
+
return { ok: false, skipped: "reclaim-error" };
|
|
2519
|
+
} finally {
|
|
2520
|
+
db?.close();
|
|
2521
|
+
}
|
|
2348
2522
|
}
|
|
2349
2523
|
|
|
2350
2524
|
// src/lib/tunnel/connection.ts
|
|
@@ -2512,6 +2686,17 @@ var StreamForwarder = class {
|
|
|
2512
2686
|
};
|
|
2513
2687
|
|
|
2514
2688
|
// src/lib/tunnel/connection.ts
|
|
2689
|
+
var FAILURE_REASON_HEADER_LC = FORWARD_FAILURE_REASON_HEADER.toLowerCase();
|
|
2690
|
+
var TunnelUpgradeRejectedError = class extends Error {
|
|
2691
|
+
constructor(message, reason) {
|
|
2692
|
+
super(message);
|
|
2693
|
+
this.reason = reason;
|
|
2694
|
+
}
|
|
2695
|
+
};
|
|
2696
|
+
function classifyUpgradeRejection(headers) {
|
|
2697
|
+
const value = headers[FAILURE_REASON_HEADER_LC];
|
|
2698
|
+
return value === "do_code_updated" ? "do_code_updated" : "unknown";
|
|
2699
|
+
}
|
|
2515
2700
|
var MAX_RECONNECT_DELAY = 3e4;
|
|
2516
2701
|
var BASE_RECONNECT_DELAY = 500;
|
|
2517
2702
|
function getReconnectDelay(attempt) {
|
|
@@ -2556,6 +2741,7 @@ function connectTunnel(options) {
|
|
|
2556
2741
|
onError,
|
|
2557
2742
|
onResponse,
|
|
2558
2743
|
onInfo,
|
|
2744
|
+
onWarning,
|
|
2559
2745
|
onDrainPing
|
|
2560
2746
|
} = options;
|
|
2561
2747
|
const tunnelUrl = getTunnelUrlConfig();
|
|
@@ -2575,8 +2761,11 @@ function connectTunnel(options) {
|
|
|
2575
2761
|
reject(new Error("Connection timeout"));
|
|
2576
2762
|
}, 3e4);
|
|
2577
2763
|
let upgradeRejection = null;
|
|
2764
|
+
let upgradeRejectionReason = null;
|
|
2578
2765
|
ws.on("unexpected-response", (_req, res) => {
|
|
2579
2766
|
clearTimeout(connectionTimeout);
|
|
2767
|
+
const reason = classifyUpgradeRejection(res.headers);
|
|
2768
|
+
upgradeRejectionReason = reason;
|
|
2580
2769
|
const chunks = [];
|
|
2581
2770
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
2582
2771
|
res.on("end", () => {
|
|
@@ -2590,8 +2779,14 @@ function connectTunnel(options) {
|
|
|
2590
2779
|
}
|
|
2591
2780
|
const statusLine = `HTTP ${res.statusCode}${res.statusMessage ? ` ${res.statusMessage}` : ""}`;
|
|
2592
2781
|
upgradeRejection = detail ? `${statusLine}: ${detail}` : statusLine;
|
|
2593
|
-
|
|
2594
|
-
|
|
2782
|
+
if (reason === "do_code_updated") {
|
|
2783
|
+
onWarning?.("Relay redeployed \u2014 reconnecting");
|
|
2784
|
+
} else {
|
|
2785
|
+
onError?.(`Tunnel refused by relay (${upgradeRejection})`);
|
|
2786
|
+
}
|
|
2787
|
+
reject(
|
|
2788
|
+
new TunnelUpgradeRejectedError(`Tunnel handshake rejected: ${upgradeRejection}`, reason)
|
|
2789
|
+
);
|
|
2595
2790
|
});
|
|
2596
2791
|
});
|
|
2597
2792
|
ws.on("open", () => {
|
|
@@ -2637,8 +2832,14 @@ function connectTunnel(options) {
|
|
|
2637
2832
|
ws.on("error", (error2) => {
|
|
2638
2833
|
clearTimeout(connectionTimeout);
|
|
2639
2834
|
const detail = upgradeRejection ?? describeSocketError(error2, url);
|
|
2640
|
-
|
|
2641
|
-
|
|
2835
|
+
if (upgradeRejectionReason === "do_code_updated") {
|
|
2836
|
+
onWarning?.("Relay redeployed \u2014 reconnecting");
|
|
2837
|
+
} else {
|
|
2838
|
+
onError?.(`Connection error: ${detail}`);
|
|
2839
|
+
}
|
|
2840
|
+
reject(
|
|
2841
|
+
upgradeRejectionReason !== null ? new TunnelUpgradeRejectedError(detail, upgradeRejectionReason) : new Error(detail)
|
|
2842
|
+
);
|
|
2642
2843
|
});
|
|
2643
2844
|
ws.on("close", (code, reason) => {
|
|
2644
2845
|
const reasonStr = reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
|
|
@@ -2714,7 +2915,8 @@ var RunnerConnection = class {
|
|
|
2714
2915
|
onError: (error2) => events.onError?.(error2),
|
|
2715
2916
|
onResponse: () => events.onResponse?.(),
|
|
2716
2917
|
onDrainPing: () => events.onDrainPing?.(),
|
|
2717
|
-
onInfo: (message) => events.onInfo?.(message)
|
|
2918
|
+
onInfo: (message) => events.onInfo?.(message),
|
|
2919
|
+
onWarning: (message) => events.onWarning?.(message)
|
|
2718
2920
|
});
|
|
2719
2921
|
return;
|
|
2720
2922
|
} catch (error2) {
|
|
@@ -2725,7 +2927,12 @@ var RunnerConnection = class {
|
|
|
2725
2927
|
}
|
|
2726
2928
|
const delay = getReconnectDelay(this.reconnectAttempt);
|
|
2727
2929
|
events.onReconnecting?.(this.reconnectAttempt);
|
|
2728
|
-
|
|
2930
|
+
const retryMessage = `Connection failed, retrying in ${Math.round(delay / 1e3)}s...`;
|
|
2931
|
+
if (error2 instanceof TunnelUpgradeRejectedError && error2.reason === "do_code_updated") {
|
|
2932
|
+
events.onWarning?.(retryMessage);
|
|
2933
|
+
} else {
|
|
2934
|
+
events.onError?.(retryMessage);
|
|
2935
|
+
}
|
|
2729
2936
|
await this.sleep(delay);
|
|
2730
2937
|
}
|
|
2731
2938
|
}
|
|
@@ -2782,7 +2989,7 @@ import { homedir as homedir2 } from "os";
|
|
|
2782
2989
|
// src/lib/file-push.ts
|
|
2783
2990
|
import { randomUUID } from "crypto";
|
|
2784
2991
|
import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
|
|
2785
|
-
import { basename, dirname as
|
|
2992
|
+
import { basename, dirname as dirname3, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
|
|
2786
2993
|
var FILE_MODE = 384;
|
|
2787
2994
|
var DIRECTORY_MODE = 448;
|
|
2788
2995
|
async function writePushedFile(request) {
|
|
@@ -2813,7 +3020,7 @@ async function writePushedFile(request) {
|
|
|
2813
3020
|
}
|
|
2814
3021
|
try {
|
|
2815
3022
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
2816
|
-
|
|
3023
|
+
dirname3(candidate)
|
|
2817
3024
|
);
|
|
2818
3025
|
const realTarget = join3(existingAncestor, ...missingSegments, basename(candidate));
|
|
2819
3026
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
@@ -2825,8 +3032,8 @@ async function writePushedFile(request) {
|
|
|
2825
3032
|
}
|
|
2826
3033
|
if (missingSegments.length > 0) {
|
|
2827
3034
|
await createMissingDirectories(existingAncestor, missingSegments);
|
|
2828
|
-
const realParent = await realpath(
|
|
2829
|
-
if (realParent !==
|
|
3035
|
+
const realParent = await realpath(dirname3(realTarget));
|
|
3036
|
+
if (realParent !== dirname3(realTarget) || !contains(allowedDirectory, realTarget)) {
|
|
2830
3037
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
2831
3038
|
path: realTarget,
|
|
2832
3039
|
bytes,
|
|
@@ -2869,7 +3076,7 @@ async function resolveNearestExistingAncestor(directory) {
|
|
|
2869
3076
|
try {
|
|
2870
3077
|
return { existingAncestor: await realpath(current), missingSegments };
|
|
2871
3078
|
} catch (err) {
|
|
2872
|
-
const parent =
|
|
3079
|
+
const parent = dirname3(current);
|
|
2873
3080
|
if (err.code !== "ENOENT" || parent === current) {
|
|
2874
3081
|
throw err;
|
|
2875
3082
|
}
|
|
@@ -2930,7 +3137,7 @@ async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
|
2930
3137
|
}
|
|
2931
3138
|
}
|
|
2932
3139
|
async function writeAtomically(realTarget, content) {
|
|
2933
|
-
const temporaryPath = join3(
|
|
3140
|
+
const temporaryPath = join3(dirname3(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
2934
3141
|
let handle;
|
|
2935
3142
|
try {
|
|
2936
3143
|
handle = await open2(temporaryPath, "wx", FILE_MODE);
|
|
@@ -3203,6 +3410,7 @@ var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
|
3203
3410
|
var HEARTBEAT_MS = 6e4;
|
|
3204
3411
|
var ABSOLUTE_MAX_PROCESSING_MS = 6 * 60 * 60 * 1e3;
|
|
3205
3412
|
var B2_ABANDONMENT_MIN_PINNED_MS = 3 * 6e4;
|
|
3413
|
+
var AMBIGUOUS_FINISH_MAX_PINNED_MS = 3 * 6e4;
|
|
3206
3414
|
var B2_ABANDONMENT_RECHECK_MS = HEARTBEAT_MS;
|
|
3207
3415
|
var POLL_MISS_GRACE_MS = HEARTBEAT_MS;
|
|
3208
3416
|
var MAX_SUPERSEDED_CONVERSATIONS = 256;
|
|
@@ -4037,6 +4245,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4037
4245
|
return this.reattachRedrive(conv, sessionId, message, ocId);
|
|
4038
4246
|
}
|
|
4039
4247
|
if (ongoing === false) {
|
|
4248
|
+
if (state === "running" && isAmbiguousFinishPinnedRunning(messages, ocId ?? "")) {
|
|
4249
|
+
return this.settleRedrive(conv, sessionId, message, ocId, messages, "done");
|
|
4250
|
+
}
|
|
4040
4251
|
this.clearRedriveUnresolved(message.id);
|
|
4041
4252
|
void this.postSignal(conv.id, message.id, "redrive_redispatched");
|
|
4042
4253
|
return "dispatch";
|
|
@@ -4648,7 +4859,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4648
4859
|
deliveryDeadlineAnchored: false,
|
|
4649
4860
|
b2PinnedSinceMs: 0,
|
|
4650
4861
|
b2LastDescendantCheckMs: 0,
|
|
4651
|
-
b2AbandonedSignalled: false
|
|
4862
|
+
b2AbandonedSignalled: false,
|
|
4863
|
+
ambiguousPinnedSinceMs: 0,
|
|
4864
|
+
ambiguousResolved: false
|
|
4652
4865
|
});
|
|
4653
4866
|
}
|
|
4654
4867
|
/**
|
|
@@ -4727,7 +4940,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4727
4940
|
deliveryDeadlineAnchored: false,
|
|
4728
4941
|
b2PinnedSinceMs: 0,
|
|
4729
4942
|
b2LastDescendantCheckMs: 0,
|
|
4730
|
-
b2AbandonedSignalled: false
|
|
4943
|
+
b2AbandonedSignalled: false,
|
|
4944
|
+
ambiguousPinnedSinceMs: 0,
|
|
4945
|
+
ambiguousResolved: false
|
|
4731
4946
|
});
|
|
4732
4947
|
}
|
|
4733
4948
|
/**
|
|
@@ -4995,6 +5210,49 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4995
5210
|
}
|
|
4996
5211
|
}
|
|
4997
5212
|
}
|
|
5213
|
+
const ambiguousPinnedNow = activelyRunning && isAmbiguousFinishPinnedRunning(messages, inFlight.opencodeMessageId);
|
|
5214
|
+
if (!ambiguousPinnedNow) {
|
|
5215
|
+
if (snapshotReadable) {
|
|
5216
|
+
inFlight.ambiguousPinnedSinceMs = 0;
|
|
5217
|
+
inFlight.ambiguousResolved = false;
|
|
5218
|
+
}
|
|
5219
|
+
} else {
|
|
5220
|
+
if (inFlight.ambiguousResolved) {
|
|
5221
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
5222
|
+
return;
|
|
5223
|
+
}
|
|
5224
|
+
if (inFlight.ambiguousPinnedSinceMs === 0) {
|
|
5225
|
+
inFlight.ambiguousPinnedSinceMs = this.now();
|
|
5226
|
+
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
5227
|
+
const finish = reply?.info?.finish ?? reply?.finish;
|
|
5228
|
+
this.log({
|
|
5229
|
+
level: "warn",
|
|
5230
|
+
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)`,
|
|
5231
|
+
conversation_id: conv.id,
|
|
5232
|
+
message_id: id
|
|
5233
|
+
});
|
|
5234
|
+
}
|
|
5235
|
+
const pinnedForMs = this.now() - inFlight.ambiguousPinnedSinceMs;
|
|
5236
|
+
const ongoing = await isSessionOngoing(this.port, sessionId);
|
|
5237
|
+
if (isAmbiguousFinishResolved({
|
|
5238
|
+
pinnedForMs,
|
|
5239
|
+
maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
|
|
5240
|
+
sessionOngoing: ongoing
|
|
5241
|
+
})) {
|
|
5242
|
+
inFlight.ambiguousResolved = true;
|
|
5243
|
+
this.log({
|
|
5244
|
+
level: "warn",
|
|
5245
|
+
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`,
|
|
5246
|
+
conversation_id: conv.id,
|
|
5247
|
+
message_id: id
|
|
5248
|
+
});
|
|
5249
|
+
void this.postSignal(conv.id, id, "ambiguous_finish_resolved", {
|
|
5250
|
+
watched_for_ms: pinnedForMs
|
|
5251
|
+
});
|
|
5252
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
5253
|
+
return;
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
4998
5256
|
if (activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
|
|
4999
5257
|
this.log({
|
|
5000
5258
|
level: "warn",
|
|
@@ -5249,48 +5507,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5249
5507
|
const ocId = row.opencode_message_id;
|
|
5250
5508
|
const state = messageRunState(messages, ocId ?? "");
|
|
5251
5509
|
if (state === "done") {
|
|
5252
|
-
|
|
5253
|
-
this.log({
|
|
5254
|
-
level: "debug",
|
|
5255
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} markDone is terminally undeliverable \u2014 left to the cron; skipping until it leaves processing`,
|
|
5256
|
-
conversation_id: row.conversation_id,
|
|
5257
|
-
message_id: row.id
|
|
5258
|
-
});
|
|
5259
|
-
return;
|
|
5260
|
-
}
|
|
5261
|
-
this.log({
|
|
5262
|
-
level: "info",
|
|
5263
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} completed while unwatched \u2014 marking done`,
|
|
5264
|
-
conversation_id: row.conversation_id,
|
|
5265
|
-
message_id: row.id
|
|
5266
|
-
});
|
|
5267
|
-
try {
|
|
5268
|
-
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
5269
|
-
const usage = messageUsage(messages, ocId ?? "");
|
|
5270
|
-
await this.markDone(row.conversation_id, row.id, sessionId, ocId, title, usage);
|
|
5271
|
-
} catch (err) {
|
|
5272
|
-
if (err instanceof ChannelAuthError) throw err;
|
|
5273
|
-
if (err instanceof ChannelTerminalError) {
|
|
5274
|
-
this.doneUndeliverable.add(row.id);
|
|
5275
|
-
this.log({
|
|
5276
|
-
level: "warn",
|
|
5277
|
-
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}`,
|
|
5278
|
-
conversation_id: row.conversation_id,
|
|
5279
|
-
message_id: row.id
|
|
5280
|
-
});
|
|
5281
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_undeliverable");
|
|
5282
|
-
return;
|
|
5283
|
-
}
|
|
5284
|
-
this.log({
|
|
5285
|
-
level: "warn",
|
|
5286
|
-
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} done (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
5287
|
-
conversation_id: row.conversation_id,
|
|
5288
|
-
message_id: row.id
|
|
5289
|
-
});
|
|
5290
|
-
return;
|
|
5291
|
-
}
|
|
5292
|
-
this.dontRedispatch.delete(row.id);
|
|
5293
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
5510
|
+
await this.deliverReadoptedDone(sessionId, row, messages, ocId);
|
|
5294
5511
|
return;
|
|
5295
5512
|
}
|
|
5296
5513
|
const restartAborted = state === "failed" && sessionOngoing === false && isAbortedTerminalReply(messages, ocId ?? "");
|
|
@@ -5355,6 +5572,17 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5355
5572
|
const ongoing = sessionOngoing;
|
|
5356
5573
|
statusReadableOngoing = ongoing;
|
|
5357
5574
|
if (ongoing === false) {
|
|
5575
|
+
if (isAmbiguousFinishPinnedRunning(messages, ocId ?? "")) {
|
|
5576
|
+
const finish = reply?.info?.finish ?? reply?.finish;
|
|
5577
|
+
this.log({
|
|
5578
|
+
level: "info",
|
|
5579
|
+
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`,
|
|
5580
|
+
conversation_id: row.conversation_id,
|
|
5581
|
+
message_id: row.id
|
|
5582
|
+
});
|
|
5583
|
+
await this.deliverReadoptedDone(sessionId, row, messages, ocId);
|
|
5584
|
+
return;
|
|
5585
|
+
}
|
|
5358
5586
|
this.log({
|
|
5359
5587
|
level: "info",
|
|
5360
5588
|
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)`,
|
|
@@ -5431,6 +5659,63 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5431
5659
|
}
|
|
5432
5660
|
await this.forceReadoptRun(sessionId, row);
|
|
5433
5661
|
}
|
|
5662
|
+
/**
|
|
5663
|
+
* Deliver a `processing` row whose correlated reply already completed while
|
|
5664
|
+
* nobody was watching (ADR-0046) — the `readoptOne` `state === 'done'` body,
|
|
5665
|
+
* extracted (#1493 Task 2.4) so the ambiguous-finish guard above can call the
|
|
5666
|
+
* SAME delivery instead of duplicating it.
|
|
5667
|
+
*
|
|
5668
|
+
* EVEN IF the row was previously parked in `dontRedispatch` (a give-up stops
|
|
5669
|
+
* re-dispatch, not delivery — Bugbot #202). Guarded EXACTLY like the watcher's
|
|
5670
|
+
* `settleMessageDone`: auth re-throws; terminal → park in `doneUndeliverable` +
|
|
5671
|
+
* leave for cron; transient → log + leave for the next drain (the still-
|
|
5672
|
+
* `processing` row is re-read and retried). markDone is idempotent server-side
|
|
5673
|
+
* (status-gated), so a repeat can never double-post.
|
|
5674
|
+
*/
|
|
5675
|
+
async deliverReadoptedDone(sessionId, row, messages, ocId) {
|
|
5676
|
+
if (this.doneUndeliverable.has(row.id)) {
|
|
5677
|
+
this.log({
|
|
5678
|
+
level: "debug",
|
|
5679
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} markDone is terminally undeliverable \u2014 left to the cron; skipping until it leaves processing`,
|
|
5680
|
+
conversation_id: row.conversation_id,
|
|
5681
|
+
message_id: row.id
|
|
5682
|
+
});
|
|
5683
|
+
return;
|
|
5684
|
+
}
|
|
5685
|
+
this.log({
|
|
5686
|
+
level: "info",
|
|
5687
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} completed while unwatched \u2014 marking done`,
|
|
5688
|
+
conversation_id: row.conversation_id,
|
|
5689
|
+
message_id: row.id
|
|
5690
|
+
});
|
|
5691
|
+
try {
|
|
5692
|
+
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
5693
|
+
const usage = messageUsage(messages, ocId ?? "");
|
|
5694
|
+
await this.markDone(row.conversation_id, row.id, sessionId, ocId, title, usage);
|
|
5695
|
+
} catch (err) {
|
|
5696
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
5697
|
+
if (err instanceof ChannelTerminalError) {
|
|
5698
|
+
this.doneUndeliverable.add(row.id);
|
|
5699
|
+
this.log({
|
|
5700
|
+
level: "warn",
|
|
5701
|
+
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}`,
|
|
5702
|
+
conversation_id: row.conversation_id,
|
|
5703
|
+
message_id: row.id
|
|
5704
|
+
});
|
|
5705
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_undeliverable");
|
|
5706
|
+
return;
|
|
5707
|
+
}
|
|
5708
|
+
this.log({
|
|
5709
|
+
level: "warn",
|
|
5710
|
+
message: `Re-adopt: failed to mark message ${row.id.slice(0, 8)} done (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
5711
|
+
conversation_id: row.conversation_id,
|
|
5712
|
+
message_id: row.id
|
|
5713
|
+
});
|
|
5714
|
+
return;
|
|
5715
|
+
}
|
|
5716
|
+
this.dontRedispatch.delete(row.id);
|
|
5717
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
5718
|
+
}
|
|
5434
5719
|
/**
|
|
5435
5720
|
* Re-dispatch an orphaned (`unknown`/null-id) `processing` row (ADR-0046 §2).
|
|
5436
5721
|
*
|
|
@@ -6060,17 +6345,25 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6060
6345
|
* the aborted-in-flight production bug after a restart.
|
|
6061
6346
|
* - `b2` — a COMPLETED reply pinned running only by `finish === "tool-calls"`
|
|
6062
6347
|
* (the sub-agent preamble — #253's shape).
|
|
6063
|
-
* - `
|
|
6064
|
-
*
|
|
6065
|
-
*
|
|
6066
|
-
* `
|
|
6348
|
+
* - `ambiguous` — a COMPLETED, non-errored reply whose `finish` is neither
|
|
6349
|
+
* "tool-calls" nor "stop" (issue #1493, class 4 — see
|
|
6350
|
+
* `isAmbiguousFinishPinnedRunning`/Task 2.4).
|
|
6351
|
+
* - `other` — any other shape (defensive; a running row is normally b1, b2 or
|
|
6352
|
+
* ambiguous).
|
|
6353
|
+
* Reads `info.time.completed` / `info.finish` / `info.error` (tolerating the
|
|
6354
|
+
* legacy top-level shape) directly rather than re-importing the module-private
|
|
6355
|
+
* `completedOf`/`finishOf`/`errorOf` — this is a display label only, not a
|
|
6356
|
+
* correctness predicate (that is `isAmbiguousFinishPinnedRunning`'s job).
|
|
6067
6357
|
*/
|
|
6068
6358
|
replyCompletionShape(reply) {
|
|
6069
6359
|
if (!reply) return "other";
|
|
6070
6360
|
const completed = reply.info?.time?.completed ?? reply.time?.completed;
|
|
6071
6361
|
if (completed == null) return "b1";
|
|
6072
6362
|
const finish = reply.info?.finish ?? reply.finish;
|
|
6073
|
-
|
|
6363
|
+
if (finish === "tool-calls") return "b2";
|
|
6364
|
+
const error2 = reply.info?.error ?? reply.error;
|
|
6365
|
+
if (finish !== "stop" && error2 == null) return "ambiguous";
|
|
6366
|
+
return "other";
|
|
6074
6367
|
}
|
|
6075
6368
|
/**
|
|
6076
6369
|
* Attribute a surfaced interaction to the in-flight message it paused on (M-1).
|
|
@@ -6988,6 +7281,10 @@ async function driveChannels(state, driver) {
|
|
|
6988
7281
|
}
|
|
6989
7282
|
}
|
|
6990
7283
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
7284
|
+
var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
|
|
7285
|
+
function sessionDbPath() {
|
|
7286
|
+
return join4(homedir3(), ".local", "share", "opencode", "opencode.db");
|
|
7287
|
+
}
|
|
6991
7288
|
async function runSweep(state, driver, config) {
|
|
6992
7289
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
6993
7290
|
try {
|
|
@@ -7030,6 +7327,25 @@ async function runSweep(state, driver, config) {
|
|
|
7030
7327
|
type: "info",
|
|
7031
7328
|
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote} (${mode})`
|
|
7032
7329
|
});
|
|
7330
|
+
const reclaimResult = await reclaimSessionDbSpace({
|
|
7331
|
+
dbPath: sessionDbPath(),
|
|
7332
|
+
maxPages: SESSION_DB_RECLAIM_MAX_PAGES,
|
|
7333
|
+
allowFullVacuum: protectedNow.size === 0
|
|
7334
|
+
});
|
|
7335
|
+
if (reclaimResult.ok) {
|
|
7336
|
+
const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
|
|
7337
|
+
const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
|
|
7338
|
+
const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
|
|
7339
|
+
logActivity(state, {
|
|
7340
|
+
type: "info",
|
|
7341
|
+
message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB${checkpointNote}`
|
|
7342
|
+
});
|
|
7343
|
+
} else {
|
|
7344
|
+
logActivity(state, {
|
|
7345
|
+
type: "info",
|
|
7346
|
+
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})`
|
|
7347
|
+
});
|
|
7348
|
+
}
|
|
7033
7349
|
} catch (error2) {
|
|
7034
7350
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
7035
7351
|
logActivity(state, {
|
|
@@ -7050,13 +7366,22 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
7050
7366
|
for (const warning2 of config.warnings) {
|
|
7051
7367
|
logActivity(state, { type: "info", level: "warn", message: `Session cleanup: ${warning2}` });
|
|
7052
7368
|
}
|
|
7053
|
-
const
|
|
7054
|
-
|
|
7055
|
-
|
|
7369
|
+
const dbBytes = statSessionDbBytes(homedir3());
|
|
7370
|
+
void (async () => {
|
|
7371
|
+
const reclaimSkipReason = dbBytes !== null && config.enabled ? await probeReclaimAvailability({ dbPath: sessionDbPath(), requiredBytes: dbBytes }) : null;
|
|
7372
|
+
const sizeWarning = buildSessionStoreSizeWarning({
|
|
7373
|
+
dbBytes,
|
|
7374
|
+
cleanupEnabled: config.enabled,
|
|
7375
|
+
reclaimSkipReason
|
|
7376
|
+
});
|
|
7377
|
+
if (sizeWarning !== null) {
|
|
7378
|
+
logActivity(state, { type: "info", level: "warn", message: sizeWarning });
|
|
7379
|
+
}
|
|
7380
|
+
})().catch((err) => {
|
|
7381
|
+
console.error(
|
|
7382
|
+
`[scheduleSessionCleanup] size-warning preflight failed: ${err instanceof Error ? err.message : String(err)}`
|
|
7383
|
+
);
|
|
7056
7384
|
});
|
|
7057
|
-
if (sizeWarning !== null) {
|
|
7058
|
-
logActivity(state, { type: "info", level: "warn", message: sizeWarning });
|
|
7059
|
-
}
|
|
7060
7385
|
if (!config.enabled) return;
|
|
7061
7386
|
logActivity(state, {
|
|
7062
7387
|
type: "info",
|
|
@@ -7638,6 +7963,13 @@ async function run(options) {
|
|
|
7638
7963
|
logActivity(state, { type: "error", error: error2 });
|
|
7639
7964
|
if (state.interactive) displayStatus(state);
|
|
7640
7965
|
},
|
|
7966
|
+
// `warn`, not `info`: `forwardRunnerActivity`'s FORWARDED_LEVELS floor is
|
|
7967
|
+
// {'warn','error'}, so an `info` entry would never leave the machine and
|
|
7968
|
+
// an operator couldn't correlate a reconnect storm with a relay deploy.
|
|
7969
|
+
onWarning: (message) => {
|
|
7970
|
+
logActivity(state, { type: "info", level: "warn", message });
|
|
7971
|
+
if (state.interactive) displayStatus(state);
|
|
7972
|
+
},
|
|
7641
7973
|
// Web traffic is proxied transparently; note opencode is live and stamp
|
|
7642
7974
|
// proxied activity so the idle loop treats interactive proxy use as work.
|
|
7643
7975
|
// Fires per forwarded response head (incl. every SSE open) and excludes
|