@evident-ai/cli 3.4.1-dev.31006db → 3.4.1-dev.48f83ae
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 +6 -0
- package/dist/index.js +650 -101
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { createRequire } from "module";
|
|
4
|
+
import { createRequire as createRequire2 } from "module";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/login.ts
|
|
@@ -763,6 +763,13 @@ function toReportedOpenAiWindow(window) {
|
|
|
763
763
|
resets_at: window.resetsAt
|
|
764
764
|
};
|
|
765
765
|
}
|
|
766
|
+
function toReportedOpenAiSubscription(snapshot) {
|
|
767
|
+
if (!snapshot.subscription) return null;
|
|
768
|
+
return {
|
|
769
|
+
owner_email: snapshot.subscription.ownerEmail,
|
|
770
|
+
plan_type: snapshot.subscription.planType
|
|
771
|
+
};
|
|
772
|
+
}
|
|
766
773
|
async function reportOpenAiUsage(agentId, authHeader, snapshot) {
|
|
767
774
|
try {
|
|
768
775
|
const apiUrl = getApiUrlConfig();
|
|
@@ -773,7 +780,8 @@ async function reportOpenAiUsage(agentId, authHeader, snapshot) {
|
|
|
773
780
|
primary: toReportedOpenAiWindow(snapshot.primary),
|
|
774
781
|
secondary: toReportedOpenAiWindow(snapshot.secondary),
|
|
775
782
|
has_credits: snapshot.hasCredits,
|
|
776
|
-
credits_unlimited: snapshot.creditsUnlimited
|
|
783
|
+
credits_unlimited: snapshot.creditsUnlimited,
|
|
784
|
+
subscription: toReportedOpenAiSubscription(snapshot)
|
|
777
785
|
}),
|
|
778
786
|
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
779
787
|
});
|
|
@@ -1183,9 +1191,9 @@ async function claudeUsage() {
|
|
|
1183
1191
|
}
|
|
1184
1192
|
|
|
1185
1193
|
// src/commands/run.ts
|
|
1186
|
-
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as
|
|
1194
|
+
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
1187
1195
|
import { homedir as homedir5 } from "os";
|
|
1188
|
-
import { isAbsolute as isAbsolute3, join as
|
|
1196
|
+
import { isAbsolute as isAbsolute3, join as join9, parse, resolve as resolvePath2 } from "path";
|
|
1189
1197
|
import chalk6 from "chalk";
|
|
1190
1198
|
|
|
1191
1199
|
// ../../packages/types/src/agents/index.ts
|
|
@@ -1525,7 +1533,14 @@ function drainSessionDbRecoveryReport({
|
|
|
1525
1533
|
skippedLines++;
|
|
1526
1534
|
return [];
|
|
1527
1535
|
}
|
|
1528
|
-
return [
|
|
1536
|
+
return [
|
|
1537
|
+
{
|
|
1538
|
+
...value,
|
|
1539
|
+
provenance_reason: value.provenance_reason ?? null,
|
|
1540
|
+
provenance_migration_delta: value.provenance_migration_delta ?? null,
|
|
1541
|
+
replication_suspended: value.replication_suspended ?? false
|
|
1542
|
+
}
|
|
1543
|
+
];
|
|
1529
1544
|
} catch (error2) {
|
|
1530
1545
|
skippedLines++;
|
|
1531
1546
|
console.error(
|
|
@@ -1628,6 +1643,12 @@ function buildSessionDbRecoveryActivity(record) {
|
|
|
1628
1643
|
metadata: withoutContractFields(record),
|
|
1629
1644
|
message: `This runner did not come online because its damaged session database could not be safely separated from its active backup or proven removed. Backed-up session history remains readable at ${record.quarantine_destination ?? "its original location or the quarantine destination named in the boot logs"}; any local session-database files that remain were left in place and nothing opened or wrote them. See the runner boot logs for SESSION-DB-LOCAL-DISCARD-FAILED details.`
|
|
1630
1645
|
};
|
|
1646
|
+
case "schema_provenance_mismatch":
|
|
1647
|
+
return {
|
|
1648
|
+
level,
|
|
1649
|
+
metadata: withoutContractFields(record),
|
|
1650
|
+
message: `Session database schema provenance mismatch for ${record.dbPath ?? record.db_path ?? "unknown"}: recorded version=${record.recorded_version ?? "unknown"}, current version=${record.current_version ?? "unknown"}, reason=${record.provenance_reason ?? "unknown"}, migration delta=${record.provenance_migration_delta ?? "unknown"}. Inspect the session database and runner backup before continuing.`
|
|
1651
|
+
};
|
|
1631
1652
|
default:
|
|
1632
1653
|
return null;
|
|
1633
1654
|
}
|
|
@@ -1642,7 +1663,8 @@ var OUTCOMES = /* @__PURE__ */ new Set([
|
|
|
1642
1663
|
"fresh_session_db",
|
|
1643
1664
|
"history_rolled_back",
|
|
1644
1665
|
"restore_misconfigured",
|
|
1645
|
-
"session_db_boot_refused"
|
|
1666
|
+
"session_db_boot_refused",
|
|
1667
|
+
"schema_provenance_mismatch"
|
|
1646
1668
|
]);
|
|
1647
1669
|
var STAGES = /* @__PURE__ */ new Set(["restore", "verify"]);
|
|
1648
1670
|
var SEVERITIES = /* @__PURE__ */ new Set(["warning", "error"]);
|
|
@@ -1660,7 +1682,7 @@ var STRING_OR_NULL_FIELDS = ["quarantine_destination", "verified_restore_point"]
|
|
|
1660
1682
|
function isSessionDbRecoveryRecord(value) {
|
|
1661
1683
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1662
1684
|
const record = value;
|
|
1663
|
-
return record.v === 1 && record.event === "session_db_recovery" && typeof record.at === "string" && STAGES.has(record.stage) && OUTCOMES.has(record.outcome) && SEVERITIES.has(record.severity) && typeof record.reason === "string" && (record.replication_suspended === void 0 || typeof record.replication_suspended === "boolean") && NUMBER_FIELDS.every((field) => record[field] === null || Number.isInteger(record[field])) && STRING_OR_NULL_FIELDS.every(
|
|
1685
|
+
return record.v === 1 && record.event === "session_db_recovery" && typeof record.at === "string" && STAGES.has(record.stage) && OUTCOMES.has(record.outcome) && SEVERITIES.has(record.severity) && typeof record.reason === "string" && (record.replication_suspended === void 0 || typeof record.replication_suspended === "boolean") && (record.provenance_reason === void 0 || record.provenance_reason === null || typeof record.provenance_reason === "string") && (record.provenance_migration_delta === void 0 || record.provenance_migration_delta === null || Number.isInteger(record.provenance_migration_delta)) && NUMBER_FIELDS.every((field) => record[field] === null || Number.isInteger(record[field])) && STRING_OR_NULL_FIELDS.every(
|
|
1664
1686
|
(field) => record[field] === null || typeof record[field] === "string"
|
|
1665
1687
|
);
|
|
1666
1688
|
}
|
|
@@ -1714,10 +1736,14 @@ function runSynchroniser(args, opts) {
|
|
|
1714
1736
|
let stderr = "";
|
|
1715
1737
|
let settled = false;
|
|
1716
1738
|
const timer = {};
|
|
1739
|
+
let abortListener;
|
|
1740
|
+
let spawnListener;
|
|
1717
1741
|
const finish = (result) => {
|
|
1718
1742
|
if (settled) return;
|
|
1719
1743
|
settled = true;
|
|
1720
1744
|
if (timer.handle) clearTimeout(timer.handle);
|
|
1745
|
+
if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
|
|
1746
|
+
if (spawnListener) child.removeListener("spawn", spawnListener);
|
|
1721
1747
|
resolve4(result);
|
|
1722
1748
|
};
|
|
1723
1749
|
try {
|
|
@@ -1743,6 +1769,25 @@ function runSynchroniser(args, opts) {
|
|
|
1743
1769
|
child.once("close", (code) => {
|
|
1744
1770
|
finish({ code, stdout, stderr, timedOut: false });
|
|
1745
1771
|
});
|
|
1772
|
+
if (opts.signal) {
|
|
1773
|
+
const killChild = () => {
|
|
1774
|
+
if (child.pid === void 0) {
|
|
1775
|
+
if (!spawnListener) {
|
|
1776
|
+
spawnListener = killChild;
|
|
1777
|
+
child.once("spawn", spawnListener);
|
|
1778
|
+
}
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
child.kill("SIGKILL");
|
|
1782
|
+
};
|
|
1783
|
+
abortListener = killChild;
|
|
1784
|
+
if (opts.signal.aborted) {
|
|
1785
|
+
abortListener();
|
|
1786
|
+
} else {
|
|
1787
|
+
opts.signal.addEventListener("abort", abortListener, { once: true });
|
|
1788
|
+
if (opts.signal.aborted) abortListener();
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1746
1791
|
timer.handle = setTimeout(
|
|
1747
1792
|
() => {
|
|
1748
1793
|
child.kill("SIGKILL");
|
|
@@ -1780,6 +1825,8 @@ function reportRecord(stage, outcome, reason, litestreamExitCode, options) {
|
|
|
1780
1825
|
quarantined_bytes: null,
|
|
1781
1826
|
verified_restore_point: null,
|
|
1782
1827
|
restore_points_tried: null,
|
|
1828
|
+
provenance_reason: null,
|
|
1829
|
+
provenance_migration_delta: null,
|
|
1783
1830
|
replication_suspended: stage === "restore"
|
|
1784
1831
|
});
|
|
1785
1832
|
}
|
|
@@ -2200,6 +2247,131 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
2200
2247
|
return { verifyFatal: await verifySessionDb(options, configPath, env) };
|
|
2201
2248
|
}
|
|
2202
2249
|
|
|
2250
|
+
// src/lib/opencode/session-db-provenance.ts
|
|
2251
|
+
import { createRequire } from "module";
|
|
2252
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2253
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
2254
|
+
var require2 = createRequire(import.meta.url);
|
|
2255
|
+
function readSessionDbMigrationIds(dbPath) {
|
|
2256
|
+
let db;
|
|
2257
|
+
try {
|
|
2258
|
+
const { DatabaseSync } = require2("node:sqlite");
|
|
2259
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
2260
|
+
const columns = db.prepare("PRAGMA table_info(migration)").all();
|
|
2261
|
+
const hasExpectedShape = columns.length === 2 && columns.some(
|
|
2262
|
+
(column) => column.name === "id" && typeof column.type === "string" && column.type.toUpperCase() === "TEXT" && column.pk === 1
|
|
2263
|
+
) && columns.some(
|
|
2264
|
+
(column) => column.name === "time_completed" && typeof column.type === "string" && column.type.toUpperCase() === "INTEGER" && column.notnull === 1 && column.pk === 0
|
|
2265
|
+
);
|
|
2266
|
+
if (!hasExpectedShape) {
|
|
2267
|
+
console.warn(`[readSessionDbMigrationIds] unsupported migration table shape in ${dbPath}`);
|
|
2268
|
+
return null;
|
|
2269
|
+
}
|
|
2270
|
+
const rows = db.prepare("SELECT id FROM migration ORDER BY id").all();
|
|
2271
|
+
if (rows.some((row) => typeof row.id !== "string")) return null;
|
|
2272
|
+
return rows.map((row) => row.id);
|
|
2273
|
+
} catch (error2) {
|
|
2274
|
+
console.warn(
|
|
2275
|
+
`[readSessionDbMigrationIds] could not read migration history from ${dbPath}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2276
|
+
);
|
|
2277
|
+
return null;
|
|
2278
|
+
} finally {
|
|
2279
|
+
try {
|
|
2280
|
+
db?.close();
|
|
2281
|
+
} catch (error2) {
|
|
2282
|
+
console.warn(
|
|
2283
|
+
`[readSessionDbMigrationIds] could not close ${dbPath}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
function sessionDbProvenanceStatePath(homeDir, env) {
|
|
2289
|
+
const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
|
|
2290
|
+
return override || join3(homeDir, ".local", "state", "evident", "session-db-provenance.json");
|
|
2291
|
+
}
|
|
2292
|
+
function loadSessionDbProvenanceState(path) {
|
|
2293
|
+
let value;
|
|
2294
|
+
try {
|
|
2295
|
+
value = JSON.parse(readFileSync3(path, "utf8"));
|
|
2296
|
+
} catch (error2) {
|
|
2297
|
+
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return {};
|
|
2298
|
+
console.error(
|
|
2299
|
+
`[session-db-provenance] could not read ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2300
|
+
);
|
|
2301
|
+
return {};
|
|
2302
|
+
}
|
|
2303
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2304
|
+
console.error(`[session-db-provenance] ignored malformed state in ${path}`);
|
|
2305
|
+
return {};
|
|
2306
|
+
}
|
|
2307
|
+
const state = {};
|
|
2308
|
+
for (const [dbPath, record] of Object.entries(value)) {
|
|
2309
|
+
if (!isSessionDbProvenanceRecord(record)) {
|
|
2310
|
+
console.error(`[session-db-provenance] ignored malformed state in ${path}`);
|
|
2311
|
+
return {};
|
|
2312
|
+
}
|
|
2313
|
+
state[dbPath] = record;
|
|
2314
|
+
}
|
|
2315
|
+
return state;
|
|
2316
|
+
}
|
|
2317
|
+
function saveSessionDbProvenanceState(path, state) {
|
|
2318
|
+
try {
|
|
2319
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
2320
|
+
writeFileSync2(path, `${JSON.stringify(state, null, 2)}
|
|
2321
|
+
`, "utf8");
|
|
2322
|
+
} catch (error2) {
|
|
2323
|
+
console.error(
|
|
2324
|
+
`[session-db-provenance] could not write ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2325
|
+
);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
function evaluateSessionDbProvenance(input) {
|
|
2329
|
+
const { currentVersion, currentIds, previous } = input;
|
|
2330
|
+
if (!previous) return { anomaly: false, reason: null };
|
|
2331
|
+
const current = new Set(currentIds);
|
|
2332
|
+
const prior = new Set(previous.migrationIds);
|
|
2333
|
+
for (const id of prior) {
|
|
2334
|
+
if (!current.has(id)) return { anomaly: true, reason: "migration-history-regressed" };
|
|
2335
|
+
}
|
|
2336
|
+
if (current.size > prior.size && previous.opencodeVersion === currentVersion) {
|
|
2337
|
+
return { anomaly: true, reason: "foreign-version-migrations" };
|
|
2338
|
+
}
|
|
2339
|
+
return { anomaly: false, reason: null };
|
|
2340
|
+
}
|
|
2341
|
+
function checkSessionDbProvenance(input) {
|
|
2342
|
+
const { dbPath, currentVersion, homeDir, env } = input;
|
|
2343
|
+
const path = sessionDbProvenanceStatePath(homeDir, env);
|
|
2344
|
+
const state = loadSessionDbProvenanceState(path);
|
|
2345
|
+
const previous = state[dbPath];
|
|
2346
|
+
const currentIds = readSessionDbMigrationIds(dbPath);
|
|
2347
|
+
if (currentIds === null) {
|
|
2348
|
+
return {
|
|
2349
|
+
anomaly: false,
|
|
2350
|
+
reason: null,
|
|
2351
|
+
recordedVersion: previous?.opencodeVersion ?? null,
|
|
2352
|
+
migrationDelta: null
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
const decision = evaluateSessionDbProvenance({ currentVersion, currentIds, previous });
|
|
2356
|
+
const migrationDelta = previous ? new Set(currentIds).size - new Set(previous.migrationIds).size : null;
|
|
2357
|
+
state[dbPath] = {
|
|
2358
|
+
opencodeVersion: currentVersion,
|
|
2359
|
+
migrationIds: currentIds,
|
|
2360
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2361
|
+
};
|
|
2362
|
+
saveSessionDbProvenanceState(path, state);
|
|
2363
|
+
return {
|
|
2364
|
+
...decision,
|
|
2365
|
+
recordedVersion: previous?.opencodeVersion ?? null,
|
|
2366
|
+
migrationDelta
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
function isSessionDbProvenanceRecord(value) {
|
|
2370
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2371
|
+
const record = value;
|
|
2372
|
+
return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2203
2375
|
// src/lib/opencode/opencode-version-gate.ts
|
|
2204
2376
|
var QUEUE_VALIDATED_OPENCODE_VERSIONS = ["1.17.11", "1.18.3"];
|
|
2205
2377
|
function isQueueValidatedVersion(version2) {
|
|
@@ -3286,10 +3458,10 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
3286
3458
|
|
|
3287
3459
|
// src/lib/opencode/session-db-size.ts
|
|
3288
3460
|
import { statSync as statSync3 } from "fs";
|
|
3289
|
-
import { join as
|
|
3461
|
+
import { join as join4 } from "path";
|
|
3290
3462
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
3291
3463
|
function statSessionDbBytes(homeDir) {
|
|
3292
|
-
const dbPath =
|
|
3464
|
+
const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
3293
3465
|
try {
|
|
3294
3466
|
return statSync3(dbPath).size;
|
|
3295
3467
|
} catch (err) {
|
|
@@ -3318,10 +3490,10 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
3318
3490
|
|
|
3319
3491
|
// src/lib/opencode/session-db-reclaim.ts
|
|
3320
3492
|
import { statSync as statSync4, statfsSync } from "fs";
|
|
3321
|
-
import { dirname as
|
|
3493
|
+
import { dirname as dirname4 } from "path";
|
|
3322
3494
|
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
3323
3495
|
try {
|
|
3324
|
-
const fsStats = statfsSync(
|
|
3496
|
+
const fsStats = statfsSync(dirname4(dbPath));
|
|
3325
3497
|
const availableBytes = fsStats.bavail * fsStats.bsize;
|
|
3326
3498
|
if (availableBytes < requiredBytes) {
|
|
3327
3499
|
return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
|
|
@@ -3703,8 +3875,8 @@ function connectTunnel(options) {
|
|
|
3703
3875
|
try {
|
|
3704
3876
|
message = JSON.parse(data.toString());
|
|
3705
3877
|
} catch (error2) {
|
|
3706
|
-
const
|
|
3707
|
-
onError?.(`Failed to handle message: ${
|
|
3878
|
+
const errorMessage2 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
3879
|
+
onError?.(`Failed to handle message: ${errorMessage2}`);
|
|
3708
3880
|
return;
|
|
3709
3881
|
}
|
|
3710
3882
|
if (isStreamFrame(message)) {
|
|
@@ -3847,10 +4019,10 @@ var RunnerConnection = class {
|
|
|
3847
4019
|
};
|
|
3848
4020
|
|
|
3849
4021
|
// src/lib/tunnel/ready-marker.ts
|
|
3850
|
-
import { writeFileSync as
|
|
4022
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
3851
4023
|
function writeTunnelReadyMarker(path, agentId) {
|
|
3852
4024
|
try {
|
|
3853
|
-
|
|
4025
|
+
writeFileSync3(path, `${agentId}
|
|
3854
4026
|
`);
|
|
3855
4027
|
return { ok: true };
|
|
3856
4028
|
} catch (error2) {
|
|
@@ -3875,7 +4047,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
3875
4047
|
}
|
|
3876
4048
|
|
|
3877
4049
|
// src/lib/process-liveness.ts
|
|
3878
|
-
import { readFileSync as
|
|
4050
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
3879
4051
|
function isProcessAlive(pid) {
|
|
3880
4052
|
try {
|
|
3881
4053
|
process.kill(pid, 0);
|
|
@@ -3890,7 +4062,7 @@ function isProcessAlive(pid) {
|
|
|
3890
4062
|
}
|
|
3891
4063
|
if (process.platform !== "linux") return true;
|
|
3892
4064
|
try {
|
|
3893
|
-
const status2 =
|
|
4065
|
+
const status2 = readFileSync4(`/proc/${pid}/status`, "utf8");
|
|
3894
4066
|
return !/^State:\s+Z(?:\s|$)/m.test(status2);
|
|
3895
4067
|
} catch (error2) {
|
|
3896
4068
|
console.error(
|
|
@@ -3901,9 +4073,9 @@ function isProcessAlive(pid) {
|
|
|
3901
4073
|
}
|
|
3902
4074
|
|
|
3903
4075
|
// src/lib/openai-usage.ts
|
|
3904
|
-
import { readFileSync as
|
|
4076
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3905
4077
|
import { homedir as homedir3 } from "os";
|
|
3906
|
-
import { join as
|
|
4078
|
+
import { join as join5 } from "path";
|
|
3907
4079
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
3908
4080
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
3909
4081
|
var OpenAiUsageError = class extends Error {
|
|
@@ -3917,7 +4089,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
3917
4089
|
}
|
|
3918
4090
|
function readOpenCodeChatGptCredentials() {
|
|
3919
4091
|
try {
|
|
3920
|
-
const raw =
|
|
4092
|
+
const raw = readFileSync5(join5(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
3921
4093
|
let parsed;
|
|
3922
4094
|
try {
|
|
3923
4095
|
parsed = JSON.parse(raw);
|
|
@@ -3939,6 +4111,23 @@ function readOpenCodeChatGptCredentials() {
|
|
|
3939
4111
|
return null;
|
|
3940
4112
|
}
|
|
3941
4113
|
}
|
|
4114
|
+
function parseChatGptIdentity(accessToken) {
|
|
4115
|
+
const segments = accessToken.split(".");
|
|
4116
|
+
if (segments.length !== 3) return null;
|
|
4117
|
+
let payload;
|
|
4118
|
+
try {
|
|
4119
|
+
const parsed = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf8"));
|
|
4120
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
4121
|
+
payload = parsed;
|
|
4122
|
+
} catch {
|
|
4123
|
+
return null;
|
|
4124
|
+
}
|
|
4125
|
+
const profile = payload["https://api.openai.com/profile"];
|
|
4126
|
+
const auth = payload["https://api.openai.com/auth"];
|
|
4127
|
+
const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
|
|
4128
|
+
const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
|
|
4129
|
+
return ownerEmail === null && planType === null ? null : { ownerEmail, planType };
|
|
4130
|
+
}
|
|
3942
4131
|
function toWindow2(headers, name) {
|
|
3943
4132
|
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
3944
4133
|
const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
|
|
@@ -4014,6 +4203,7 @@ async function getOpenAiUsage(port) {
|
|
|
4014
4203
|
"credentials_expired"
|
|
4015
4204
|
);
|
|
4016
4205
|
}
|
|
4206
|
+
const subscription = parseChatGptIdentity(credentials2.accessToken);
|
|
4017
4207
|
const models = await resolveProbeModels(port);
|
|
4018
4208
|
if (models.length === 0) {
|
|
4019
4209
|
throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
|
|
@@ -4046,7 +4236,7 @@ async function getOpenAiUsage(port) {
|
|
|
4046
4236
|
"no_usable_window"
|
|
4047
4237
|
);
|
|
4048
4238
|
}
|
|
4049
|
-
return usage;
|
|
4239
|
+
return { ...usage, subscription };
|
|
4050
4240
|
}
|
|
4051
4241
|
if (res.status === 401) {
|
|
4052
4242
|
throw new OpenAiUsageError(
|
|
@@ -4293,12 +4483,12 @@ function createResourceUsageCollector(homeDir) {
|
|
|
4293
4483
|
import { homedir as homedir4 } from "os";
|
|
4294
4484
|
|
|
4295
4485
|
// src/lib/runner-file-sync.ts
|
|
4296
|
-
import { join as
|
|
4486
|
+
import { join as join7 } from "path";
|
|
4297
4487
|
|
|
4298
4488
|
// src/lib/file-push.ts
|
|
4299
4489
|
import { randomUUID } from "crypto";
|
|
4300
4490
|
import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
|
|
4301
|
-
import { basename, dirname as
|
|
4491
|
+
import { basename, dirname as dirname5, isAbsolute, join as join6, relative, resolve as resolve2, sep } from "path";
|
|
4302
4492
|
var FILE_MODE = 384;
|
|
4303
4493
|
var DIRECTORY_MODE = 448;
|
|
4304
4494
|
async function writePushedFile(request) {
|
|
@@ -4329,9 +4519,9 @@ async function writePushedFile(request) {
|
|
|
4329
4519
|
}
|
|
4330
4520
|
try {
|
|
4331
4521
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
4332
|
-
|
|
4522
|
+
dirname5(candidate)
|
|
4333
4523
|
);
|
|
4334
|
-
const realTarget =
|
|
4524
|
+
const realTarget = join6(existingAncestor, ...missingSegments, basename(candidate));
|
|
4335
4525
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
4336
4526
|
if (allowedDirectory === null) {
|
|
4337
4527
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -4341,8 +4531,8 @@ async function writePushedFile(request) {
|
|
|
4341
4531
|
}
|
|
4342
4532
|
if (missingSegments.length > 0) {
|
|
4343
4533
|
await createMissingDirectories(existingAncestor, missingSegments);
|
|
4344
|
-
const realParent = await realpath(
|
|
4345
|
-
if (realParent !==
|
|
4534
|
+
const realParent = await realpath(dirname5(realTarget));
|
|
4535
|
+
if (realParent !== dirname5(realTarget) || !contains(allowedDirectory, realTarget)) {
|
|
4346
4536
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
4347
4537
|
path: realTarget,
|
|
4348
4538
|
bytes,
|
|
@@ -4367,7 +4557,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
4367
4557
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
4368
4558
|
return null;
|
|
4369
4559
|
}
|
|
4370
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4560
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4371
4561
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
4372
4562
|
return null;
|
|
4373
4563
|
}
|
|
@@ -4385,7 +4575,7 @@ async function resolveNearestExistingAncestor(directory) {
|
|
|
4385
4575
|
try {
|
|
4386
4576
|
return { existingAncestor: await realpath(current), missingSegments };
|
|
4387
4577
|
} catch (err) {
|
|
4388
|
-
const parent =
|
|
4578
|
+
const parent = dirname5(current);
|
|
4389
4579
|
if (err.code !== "ENOENT" || parent === current) {
|
|
4390
4580
|
throw err;
|
|
4391
4581
|
}
|
|
@@ -4440,13 +4630,13 @@ function contains(realDirectory, realTarget) {
|
|
|
4440
4630
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
4441
4631
|
let current = existingAncestor;
|
|
4442
4632
|
for (const segment of missingSegments) {
|
|
4443
|
-
current =
|
|
4633
|
+
current = join6(current, segment);
|
|
4444
4634
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
4445
4635
|
await chmod(current, DIRECTORY_MODE);
|
|
4446
4636
|
}
|
|
4447
4637
|
}
|
|
4448
4638
|
async function writeAtomically(realTarget, content) {
|
|
4449
|
-
const temporaryPath =
|
|
4639
|
+
const temporaryPath = join6(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
4450
4640
|
let handle;
|
|
4451
4641
|
try {
|
|
4452
4642
|
handle = await open2(temporaryPath, "wx", FILE_MODE);
|
|
@@ -4576,12 +4766,12 @@ var NOT_APPLIED = {
|
|
|
4576
4766
|
opencodeAuthApplied: false
|
|
4577
4767
|
};
|
|
4578
4768
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
4579
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4580
|
-
return expanded ===
|
|
4769
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4770
|
+
return expanded === join7(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
4581
4771
|
}
|
|
4582
4772
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
4583
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4584
|
-
return expanded ===
|
|
4773
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4774
|
+
return expanded === join7(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
4585
4775
|
}
|
|
4586
4776
|
async function applyOne(options, file) {
|
|
4587
4777
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -5108,6 +5298,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5108
5298
|
* and stops opencode.
|
|
5109
5299
|
*/
|
|
5110
5300
|
stopped = false;
|
|
5301
|
+
recycleRequestedFlag = false;
|
|
5111
5302
|
constructor(config) {
|
|
5112
5303
|
this.agentId = config.agentId;
|
|
5113
5304
|
this.port = config.port;
|
|
@@ -5213,6 +5404,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5213
5404
|
let dispatched = 0;
|
|
5214
5405
|
try {
|
|
5215
5406
|
const conversations = await this.getPendingConversations();
|
|
5407
|
+
if (this.recycleRequestedFlag) {
|
|
5408
|
+
this.stop();
|
|
5409
|
+
}
|
|
5216
5410
|
if (conversations.length > 0) {
|
|
5217
5411
|
const total = conversations.reduce((sum, c) => sum + (c.pending_message_count ?? 0), 0);
|
|
5218
5412
|
this.log({
|
|
@@ -5341,6 +5535,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5341
5535
|
stop() {
|
|
5342
5536
|
this.stopped = true;
|
|
5343
5537
|
}
|
|
5538
|
+
/**
|
|
5539
|
+
* The server clears this request when a new MicroVM identity is recorded, so a
|
|
5540
|
+
* same-VM tunnel reconnect does not consume it. This is a plain read rather
|
|
5541
|
+
* than a consume; `run.ts` guards the action once-only.
|
|
5542
|
+
*/
|
|
5543
|
+
get recycleRequested() {
|
|
5544
|
+
return this.recycleRequestedFlag;
|
|
5545
|
+
}
|
|
5344
5546
|
/**
|
|
5345
5547
|
* Wait (up to `timeoutMs`) for in-flight watcher work to settle during a
|
|
5346
5548
|
* graceful shutdown, so a turn whose reply is ready — or completes within the
|
|
@@ -5478,7 +5680,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5478
5680
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
5479
5681
|
break;
|
|
5480
5682
|
}
|
|
5481
|
-
const
|
|
5683
|
+
const errorMessage2 = err instanceof Error ? err.message : String(err);
|
|
5482
5684
|
this.sessions.delete(conv.id);
|
|
5483
5685
|
this.supersede(conv.id, sessionId);
|
|
5484
5686
|
this.log({
|
|
@@ -5487,7 +5689,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5487
5689
|
conversation_id: conv.id,
|
|
5488
5690
|
message_id: message.id
|
|
5489
5691
|
});
|
|
5490
|
-
await this.markFailed(conv.id, message.id, null,
|
|
5692
|
+
await this.markFailed(conv.id, message.id, null, errorMessage2).catch((markErr) => {
|
|
5491
5693
|
this.log({
|
|
5492
5694
|
level: "warn",
|
|
5493
5695
|
message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -5498,7 +5700,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5498
5700
|
});
|
|
5499
5701
|
this.log({
|
|
5500
5702
|
level: "error",
|
|
5501
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
5703
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage2}`,
|
|
5502
5704
|
conversation_id: conv.id,
|
|
5503
5705
|
message_id: message.id
|
|
5504
5706
|
});
|
|
@@ -5519,14 +5721,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5519
5721
|
this.unconfirmedDispatchFailures.delete(message.id);
|
|
5520
5722
|
this.sessions.delete(conv.id);
|
|
5521
5723
|
this.supersede(conv.id, sessionId);
|
|
5522
|
-
const
|
|
5724
|
+
const errorMessage2 = `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
|
|
5523
5725
|
this.log({
|
|
5524
5726
|
level: "error",
|
|
5525
|
-
message:
|
|
5727
|
+
message: errorMessage2,
|
|
5526
5728
|
conversation_id: conv.id,
|
|
5527
5729
|
message_id: message.id
|
|
5528
5730
|
});
|
|
5529
|
-
await this.markFailed(conv.id, message.id, null,
|
|
5731
|
+
await this.markFailed(conv.id, message.id, null, errorMessage2).catch((markErr) => {
|
|
5530
5732
|
this.log({
|
|
5531
5733
|
level: "warn",
|
|
5532
5734
|
message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -7490,14 +7692,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7490
7692
|
this.unconfirmedDispatchFailures.delete(row.id);
|
|
7491
7693
|
this.sessions.delete(readoptConv.id);
|
|
7492
7694
|
this.supersede(readoptConv.id, sessionId);
|
|
7493
|
-
const
|
|
7695
|
+
const errorMessage2 = `OpenCode accepted this re-dispatched message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
|
|
7494
7696
|
this.log({
|
|
7495
7697
|
level: "error",
|
|
7496
|
-
message:
|
|
7698
|
+
message: errorMessage2,
|
|
7497
7699
|
conversation_id: row.conversation_id,
|
|
7498
7700
|
message_id: row.id
|
|
7499
7701
|
});
|
|
7500
|
-
await this.markFailed(row.conversation_id, row.id, null,
|
|
7702
|
+
await this.markFailed(row.conversation_id, row.id, null, errorMessage2).catch((markErr) => {
|
|
7501
7703
|
this.log({
|
|
7502
7704
|
level: "warn",
|
|
7503
7705
|
message: `markFailed PATCH for message ${row.id.slice(0, 8)} (conversation ${row.conversation_id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -8112,6 +8314,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8112
8314
|
throw new Error(`Failed to get pending conversations: HTTP ${res.status}`);
|
|
8113
8315
|
}
|
|
8114
8316
|
const data = await res.json();
|
|
8317
|
+
this.recycleRequestedFlag = data.recycle_requested === true;
|
|
8115
8318
|
let conversations = data.conversations;
|
|
8116
8319
|
if (this.conversationFilter) {
|
|
8117
8320
|
conversations = conversations.filter((c) => c.id === this.conversationFilter);
|
|
@@ -8629,7 +8832,7 @@ Port ${port} is already in use.`));
|
|
|
8629
8832
|
}
|
|
8630
8833
|
|
|
8631
8834
|
// src/lib/runner-credentials.ts
|
|
8632
|
-
import { chmodSync as chmodSync2, writeFileSync as
|
|
8835
|
+
import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
8633
8836
|
import { spawn as spawn5 } from "child_process";
|
|
8634
8837
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
8635
8838
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
@@ -8870,8 +9073,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
8870
9073
|
}
|
|
8871
9074
|
try {
|
|
8872
9075
|
env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
|
|
8873
|
-
|
|
8874
|
-
|
|
9076
|
+
writeFileSync4(GIT_CONFIG_GLOBAL, "");
|
|
9077
|
+
writeFileSync4(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
|
|
8875
9078
|
chmodSync2(GIT_CREDENTIAL_HELPER, 448);
|
|
8876
9079
|
const config = [
|
|
8877
9080
|
["user.name", env.GIT_USER_NAME ?? "evident-bot"],
|
|
@@ -8904,7 +9107,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
8904
9107
|
// src/lib/opencode/config-overlay.ts
|
|
8905
9108
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
8906
9109
|
import { copyFileSync, existsSync as existsSync2, statSync as statSync5 } from "fs";
|
|
8907
|
-
import { isAbsolute as isAbsolute2, join as
|
|
9110
|
+
import { isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "path";
|
|
8908
9111
|
function isFile(filePath) {
|
|
8909
9112
|
return existsSync2(filePath) && statSync5(filePath).isFile();
|
|
8910
9113
|
}
|
|
@@ -8918,7 +9121,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
8918
9121
|
return;
|
|
8919
9122
|
}
|
|
8920
9123
|
const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
|
|
8921
|
-
const target = isFile(
|
|
9124
|
+
const target = isFile(join8(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
8922
9125
|
if (!isFile(source)) {
|
|
8923
9126
|
log3(
|
|
8924
9127
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -8926,7 +9129,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
8926
9129
|
);
|
|
8927
9130
|
return;
|
|
8928
9131
|
}
|
|
8929
|
-
copyFileSync(source,
|
|
9132
|
+
copyFileSync(source, join8(cwd, target));
|
|
8930
9133
|
try {
|
|
8931
9134
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
8932
9135
|
stdio: "ignore"
|
|
@@ -8935,7 +9138,242 @@ function applyRunnerOpenCodeConfig({
|
|
|
8935
9138
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
8936
9139
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
8937
9140
|
}
|
|
8938
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
9141
|
+
log3(`Applied runner OpenCode config ${source} to ${join8(cwd, target)}`);
|
|
9142
|
+
}
|
|
9143
|
+
|
|
9144
|
+
// src/lib/credential-sync.ts
|
|
9145
|
+
import { renameSync, writeFileSync as writeFileSync5 } from "fs";
|
|
9146
|
+
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
9147
|
+
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
9148
|
+
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
9149
|
+
var DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS = 60;
|
|
9150
|
+
var STORES = ["claude", "opencode"];
|
|
9151
|
+
var MAX_FLUSH_PASSES = 2;
|
|
9152
|
+
function outcomesWith(outcome) {
|
|
9153
|
+
return { claude: outcome, opencode: outcome };
|
|
9154
|
+
}
|
|
9155
|
+
function errorMessage(error2) {
|
|
9156
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
9157
|
+
}
|
|
9158
|
+
function waitForSettlement(promise, timeoutMs) {
|
|
9159
|
+
return new Promise((resolve4) => {
|
|
9160
|
+
let settled = false;
|
|
9161
|
+
const timer = setTimeout(() => finish(false), Math.max(0, timeoutMs));
|
|
9162
|
+
const finish = (value) => {
|
|
9163
|
+
if (settled) return;
|
|
9164
|
+
settled = true;
|
|
9165
|
+
clearTimeout(timer);
|
|
9166
|
+
resolve4(value);
|
|
9167
|
+
};
|
|
9168
|
+
promise.then(
|
|
9169
|
+
() => finish(true),
|
|
9170
|
+
() => finish(true)
|
|
9171
|
+
);
|
|
9172
|
+
});
|
|
9173
|
+
}
|
|
9174
|
+
function writeMarker(markerPath, outcomes, log3) {
|
|
9175
|
+
const body = `${STORES.map((store) => `${store}=${outcomes[store]}`).join("\n")}
|
|
9176
|
+
`;
|
|
9177
|
+
const temporaryPath = `${markerPath}.tmp`;
|
|
9178
|
+
try {
|
|
9179
|
+
writeFileSync5(temporaryPath, body, { mode: 384 });
|
|
9180
|
+
renameSync(temporaryPath, markerPath);
|
|
9181
|
+
} catch (error2) {
|
|
9182
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage(error2)}`, "warn");
|
|
9183
|
+
}
|
|
9184
|
+
}
|
|
9185
|
+
function intervalSeconds(env, log3) {
|
|
9186
|
+
const raw = env.CREDS_SYNC_INTERVAL;
|
|
9187
|
+
if (raw === void 0 || /^[1-9][0-9]*$/.test(raw) && Number.isSafeInteger(Number(raw))) {
|
|
9188
|
+
return raw === void 0 ? DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS : Number(raw);
|
|
9189
|
+
}
|
|
9190
|
+
log3(
|
|
9191
|
+
`CREDENTIAL-SYNC-INTERVAL-INVALID: CREDS_SYNC_INTERVAL='${raw}' is not a positive integer; using ${DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS}s`,
|
|
9192
|
+
"warn"
|
|
9193
|
+
);
|
|
9194
|
+
return DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS;
|
|
9195
|
+
}
|
|
9196
|
+
async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
9197
|
+
const remainingMs = deadlineAt - Date.now();
|
|
9198
|
+
if (remainingMs <= 0) return { outcome: "timeout", orphaned: false };
|
|
9199
|
+
const controller = new AbortController();
|
|
9200
|
+
let result;
|
|
9201
|
+
let failed = false;
|
|
9202
|
+
const completion = Promise.resolve().then(
|
|
9203
|
+
() => synchroniserRunner(["sync-once", store], {
|
|
9204
|
+
timeoutMs: remainingMs,
|
|
9205
|
+
env,
|
|
9206
|
+
signal: controller.signal
|
|
9207
|
+
})
|
|
9208
|
+
).then(
|
|
9209
|
+
(value) => {
|
|
9210
|
+
result = value;
|
|
9211
|
+
},
|
|
9212
|
+
(error2) => {
|
|
9213
|
+
failed = true;
|
|
9214
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage(error2)}`, "warn");
|
|
9215
|
+
}
|
|
9216
|
+
);
|
|
9217
|
+
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
9218
|
+
const settledBeforeDeadline = await waitForSettlement(completion, remainingMs);
|
|
9219
|
+
clearTimeout(abortTimer);
|
|
9220
|
+
if (!settledBeforeDeadline) {
|
|
9221
|
+
controller.abort();
|
|
9222
|
+
const settledAfterAbort = await waitForSettlement(completion, CREDENTIAL_FLUSH_ABORT_GRACE_MS);
|
|
9223
|
+
if (!settledAfterAbort) return { outcome: "timeout", orphaned: true };
|
|
9224
|
+
return { outcome: "timeout", orphaned: false };
|
|
9225
|
+
}
|
|
9226
|
+
if (failed || !result) return { outcome: "failed", orphaned: false };
|
|
9227
|
+
if (result.timedOut || Date.now() >= deadlineAt) {
|
|
9228
|
+
return { outcome: "timeout", orphaned: false };
|
|
9229
|
+
}
|
|
9230
|
+
return { outcome: result.code === 0 ? "ok" : "failed", orphaned: false };
|
|
9231
|
+
}
|
|
9232
|
+
function createCredentialSync({
|
|
9233
|
+
markerPath,
|
|
9234
|
+
env,
|
|
9235
|
+
log: log3,
|
|
9236
|
+
synchroniserRunner = runSynchroniser
|
|
9237
|
+
}) {
|
|
9238
|
+
const persistenceDisabled = !env.PERSISTENCE_BUCKET;
|
|
9239
|
+
let disabled = persistenceDisabled;
|
|
9240
|
+
let armed = false;
|
|
9241
|
+
let stopped = false;
|
|
9242
|
+
let timer;
|
|
9243
|
+
let inFlight;
|
|
9244
|
+
let activeTickAbort;
|
|
9245
|
+
let lastTickFailed;
|
|
9246
|
+
let flushPromise;
|
|
9247
|
+
const scheduleTick = (intervalMs, startTick2) => {
|
|
9248
|
+
if (stopped) return;
|
|
9249
|
+
timer = setTimeout(() => {
|
|
9250
|
+
timer = void 0;
|
|
9251
|
+
startTick2();
|
|
9252
|
+
}, intervalMs);
|
|
9253
|
+
};
|
|
9254
|
+
const startTick = (intervalMs) => {
|
|
9255
|
+
if (stopped) return;
|
|
9256
|
+
const controller = new AbortController();
|
|
9257
|
+
activeTickAbort = controller;
|
|
9258
|
+
const tick = (async () => {
|
|
9259
|
+
const outcomes = {
|
|
9260
|
+
claude: "failed",
|
|
9261
|
+
opencode: "failed"
|
|
9262
|
+
};
|
|
9263
|
+
for (const store of STORES) {
|
|
9264
|
+
if (controller.signal.aborted) break;
|
|
9265
|
+
try {
|
|
9266
|
+
const result = await synchroniserRunner(["sync-once", store], {
|
|
9267
|
+
timeoutMs: CREDENTIAL_SYNC_TICK_TIMEOUT_MS,
|
|
9268
|
+
env,
|
|
9269
|
+
signal: controller.signal
|
|
9270
|
+
});
|
|
9271
|
+
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
9272
|
+
} catch (error2) {
|
|
9273
|
+
outcomes[store] = "failed";
|
|
9274
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage(error2)}`, "debug");
|
|
9275
|
+
}
|
|
9276
|
+
}
|
|
9277
|
+
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
9278
|
+
log3(
|
|
9279
|
+
`CREDENTIAL-SYNC-TICK: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
|
|
9280
|
+
"debug"
|
|
9281
|
+
);
|
|
9282
|
+
if (failed && lastTickFailed !== true) {
|
|
9283
|
+
log3(
|
|
9284
|
+
"CREDENTIAL-SYNC-FAILED: an interval credential sync failed; retrying on the next tick",
|
|
9285
|
+
"warn"
|
|
9286
|
+
);
|
|
9287
|
+
} else if (!failed && lastTickFailed === true) {
|
|
9288
|
+
log3("CREDENTIAL-SYNC-RECOVERED: interval credential sync succeeded again", "warn");
|
|
9289
|
+
}
|
|
9290
|
+
lastTickFailed = failed;
|
|
9291
|
+
})().finally(() => {
|
|
9292
|
+
if (activeTickAbort === controller) activeTickAbort = void 0;
|
|
9293
|
+
if (inFlight === tick) inFlight = void 0;
|
|
9294
|
+
scheduleTick(intervalMs, () => startTick(intervalMs));
|
|
9295
|
+
});
|
|
9296
|
+
inFlight = tick;
|
|
9297
|
+
};
|
|
9298
|
+
const performFlush = async () => {
|
|
9299
|
+
stopped = true;
|
|
9300
|
+
if (timer) {
|
|
9301
|
+
clearTimeout(timer);
|
|
9302
|
+
timer = void 0;
|
|
9303
|
+
}
|
|
9304
|
+
const deadlineAt = Date.now() + CREDENTIAL_FLUSH_DEADLINE_MS;
|
|
9305
|
+
if (inFlight) {
|
|
9306
|
+
const settled = await waitForSettlement(inFlight, CREDENTIAL_FLUSH_DEADLINE_MS);
|
|
9307
|
+
if (!settled) {
|
|
9308
|
+
activeTickAbort?.abort();
|
|
9309
|
+
const settledAfterAbort = await waitForSettlement(
|
|
9310
|
+
inFlight,
|
|
9311
|
+
CREDENTIAL_FLUSH_ABORT_GRACE_MS
|
|
9312
|
+
);
|
|
9313
|
+
if (!settledAfterAbort) {
|
|
9314
|
+
log3(
|
|
9315
|
+
"CREDENTIAL-FLUSH-ORPHANED-TICK: a sync-once child could not be proven settled; skipping the boundary flush rather than becoming a second writer",
|
|
9316
|
+
"warn"
|
|
9317
|
+
);
|
|
9318
|
+
return { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
9319
|
+
}
|
|
9320
|
+
}
|
|
9321
|
+
}
|
|
9322
|
+
if (disabled) return { outcomes: outcomesWith("skipped"), orphaned: false };
|
|
9323
|
+
const outcomes = outcomesWith("timeout");
|
|
9324
|
+
for (const store of STORES) {
|
|
9325
|
+
const result = await runFlushStore(store, env, synchroniserRunner, log3, deadlineAt);
|
|
9326
|
+
if (result.orphaned) {
|
|
9327
|
+
log3(
|
|
9328
|
+
"CREDENTIAL-FLUSH-ORPHANED-CHILD: a flush sync-once child could not be proven settled; refusing another flush pass rather than becoming a second writer",
|
|
9329
|
+
"warn"
|
|
9330
|
+
);
|
|
9331
|
+
return { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
9332
|
+
}
|
|
9333
|
+
outcomes[store] = result.outcome;
|
|
9334
|
+
}
|
|
9335
|
+
return { outcomes, orphaned: false };
|
|
9336
|
+
};
|
|
9337
|
+
let flushPasses = 0;
|
|
9338
|
+
let lastFlush;
|
|
9339
|
+
return {
|
|
9340
|
+
arm() {
|
|
9341
|
+
if (stopped || armed) return;
|
|
9342
|
+
armed = true;
|
|
9343
|
+
if (persistenceDisabled) {
|
|
9344
|
+
disabled = true;
|
|
9345
|
+
log3(
|
|
9346
|
+
"CREDENTIAL-SYNC-DISABLED: LITESTREAM_BUCKET/LITESTREAM_PREFIX are not both set; no interval credential sync this boot",
|
|
9347
|
+
"warn"
|
|
9348
|
+
);
|
|
9349
|
+
return;
|
|
9350
|
+
}
|
|
9351
|
+
disabled = false;
|
|
9352
|
+
const intervalMs = intervalSeconds(env, log3) * 1e3;
|
|
9353
|
+
scheduleTick(intervalMs, () => startTick(intervalMs));
|
|
9354
|
+
},
|
|
9355
|
+
async stopAndFlush(publish) {
|
|
9356
|
+
let result;
|
|
9357
|
+
const runningFlush = flushPromise;
|
|
9358
|
+
if (runningFlush) {
|
|
9359
|
+
result = await runningFlush;
|
|
9360
|
+
} else if (flushPasses >= MAX_FLUSH_PASSES || lastFlush?.orphaned) {
|
|
9361
|
+
result = lastFlush ?? { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
9362
|
+
} else {
|
|
9363
|
+
flushPasses++;
|
|
9364
|
+
const currentFlush = performFlush();
|
|
9365
|
+
flushPromise = currentFlush;
|
|
9366
|
+
try {
|
|
9367
|
+
result = await currentFlush;
|
|
9368
|
+
lastFlush = result;
|
|
9369
|
+
} finally {
|
|
9370
|
+
if (flushPromise === currentFlush) flushPromise = void 0;
|
|
9371
|
+
}
|
|
9372
|
+
}
|
|
9373
|
+
if (publish) writeMarker(markerPath, result.outcomes, log3);
|
|
9374
|
+
return result.outcomes;
|
|
9375
|
+
}
|
|
9376
|
+
};
|
|
8939
9377
|
}
|
|
8940
9378
|
|
|
8941
9379
|
// src/commands/run.ts
|
|
@@ -8975,7 +9413,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
8975
9413
|
if (trimmed === "") {
|
|
8976
9414
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
8977
9415
|
}
|
|
8978
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
9416
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join9(homeDir, trimmed.slice(2)) : trimmed;
|
|
8979
9417
|
if (!isAbsolute3(expanded)) {
|
|
8980
9418
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
8981
9419
|
}
|
|
@@ -9235,6 +9673,10 @@ async function driveChannels(state, driver) {
|
|
|
9235
9673
|
consecutiveDrainFailures = 0;
|
|
9236
9674
|
unreachableMs = 0;
|
|
9237
9675
|
state.messageCount += processed;
|
|
9676
|
+
if (driver.recycleRequested) {
|
|
9677
|
+
await beginGracefulShutdown(state, "recycle");
|
|
9678
|
+
return;
|
|
9679
|
+
}
|
|
9238
9680
|
const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;
|
|
9239
9681
|
lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
|
|
9240
9682
|
const fileActivitySnapshot = driver.fileSyncActivity();
|
|
@@ -9277,8 +9719,8 @@ async function driveChannels(state, driver) {
|
|
|
9277
9719
|
state.running = false;
|
|
9278
9720
|
break;
|
|
9279
9721
|
}
|
|
9280
|
-
const
|
|
9281
|
-
logActivity(state, { type: "error", error: `Channel processing error: ${
|
|
9722
|
+
const errorMessage2 = error2 instanceof Error ? error2.message : String(error2);
|
|
9723
|
+
logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage2}` });
|
|
9282
9724
|
if (state.interactive) displayStatus(state);
|
|
9283
9725
|
if (driver.hasInFlightWatchers()) {
|
|
9284
9726
|
consecutiveDrainFailures = 0;
|
|
@@ -9318,7 +9760,43 @@ async function driveChannels(state, driver) {
|
|
|
9318
9760
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
9319
9761
|
var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
|
|
9320
9762
|
function sessionDbPath() {
|
|
9321
|
-
return
|
|
9763
|
+
return join9(homedir5(), ".local", "share", "opencode", "opencode.db");
|
|
9764
|
+
}
|
|
9765
|
+
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
9766
|
+
const record = {
|
|
9767
|
+
v: 1,
|
|
9768
|
+
event: "session_db_recovery",
|
|
9769
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9770
|
+
stage: "verify",
|
|
9771
|
+
outcome: "schema_provenance_mismatch",
|
|
9772
|
+
severity: "error",
|
|
9773
|
+
reason: provenance.reason ?? "schema-provenance-mismatch",
|
|
9774
|
+
litestream_exit_code: null,
|
|
9775
|
+
attempt: null,
|
|
9776
|
+
replica_objects: null,
|
|
9777
|
+
replica_bytes: null,
|
|
9778
|
+
quarantine_destination: null,
|
|
9779
|
+
quarantined_objects: null,
|
|
9780
|
+
quarantine_failed_objects: null,
|
|
9781
|
+
quarantined_bytes: null,
|
|
9782
|
+
verified_restore_point: null,
|
|
9783
|
+
restore_points_tried: null,
|
|
9784
|
+
provenance_reason: provenance.reason,
|
|
9785
|
+
provenance_migration_delta: provenance.migrationDelta,
|
|
9786
|
+
replication_suspended: false,
|
|
9787
|
+
dbPath: sessionDbPath(),
|
|
9788
|
+
recorded_version: provenance.recordedVersion,
|
|
9789
|
+
current_version: currentVersion,
|
|
9790
|
+
provenance_pre_boot_migration_count: preBootMigrationCount
|
|
9791
|
+
};
|
|
9792
|
+
const activity = buildSessionDbRecoveryActivity(record);
|
|
9793
|
+
if (!activity) throw new Error("could not map session-DB provenance activity");
|
|
9794
|
+
logActivity(state, {
|
|
9795
|
+
type: activity.level === "error" ? "error" : "info",
|
|
9796
|
+
level: activity.level,
|
|
9797
|
+
...activity.level === "error" ? { error: activity.message } : { message: activity.message },
|
|
9798
|
+
metadata: activity.metadata
|
|
9799
|
+
});
|
|
9322
9800
|
}
|
|
9323
9801
|
async function runSweep(state, driver, config) {
|
|
9324
9802
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
@@ -9365,7 +9843,7 @@ async function runSweep(state, driver, config) {
|
|
|
9365
9843
|
const reclaimResult = await reclaimSessionDbSpace({
|
|
9366
9844
|
dbPath: sessionDbPath(),
|
|
9367
9845
|
maxPages: SESSION_DB_RECLAIM_MAX_PAGES,
|
|
9368
|
-
allowFullVacuum: protectedNow.size === 0
|
|
9846
|
+
allowFullVacuum: protectedNow.size === 0 && !state.sessionDbProvenanceAnomaly
|
|
9369
9847
|
});
|
|
9370
9848
|
if (reclaimResult.ok) {
|
|
9371
9849
|
const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
|
|
@@ -9712,21 +10190,39 @@ async function cleanup(state, opts = {}) {
|
|
|
9712
10190
|
clearTimeout(state.resourceUsageTimer);
|
|
9713
10191
|
state.resourceUsageTimer = null;
|
|
9714
10192
|
}
|
|
10193
|
+
const credentialSync = state.credentialSync;
|
|
10194
|
+
const flushCredentials = credentialSync ? async (phase, publish) => {
|
|
10195
|
+
await timeShutdownPhase(state, durations, phase, async () => {
|
|
10196
|
+
const outcomes = await credentialSync.stopAndFlush(publish);
|
|
10197
|
+
const level = Object.values(outcomes).every((outcome) => outcome === "ok") ? "info" : "warn";
|
|
10198
|
+
log2(
|
|
10199
|
+
state,
|
|
10200
|
+
`Credential flush: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
|
|
10201
|
+
level
|
|
10202
|
+
);
|
|
10203
|
+
});
|
|
10204
|
+
} : void 0;
|
|
10205
|
+
let drainSettled = true;
|
|
9715
10206
|
if (opts.graceful && state.channelDriver) {
|
|
9716
10207
|
state.channelDriver.stop();
|
|
10208
|
+
}
|
|
10209
|
+
if (flushCredentials) {
|
|
10210
|
+
await flushCredentials("credential_flush", !(opts.graceful && state.channelDriver));
|
|
10211
|
+
}
|
|
10212
|
+
if (opts.graceful && state.channelDriver) {
|
|
9717
10213
|
log2(state, "Draining in-flight channel work before shutdown...");
|
|
9718
10214
|
if (state.interactive) {
|
|
9719
10215
|
logActivity(state, { type: "info", message: "Draining in-flight work before shutdown..." });
|
|
9720
10216
|
displayStatus(state);
|
|
9721
10217
|
}
|
|
9722
10218
|
const driver = state.channelDriver;
|
|
9723
|
-
|
|
10219
|
+
drainSettled = await timeShutdownPhase(
|
|
9724
10220
|
state,
|
|
9725
10221
|
durations,
|
|
9726
10222
|
"drain",
|
|
9727
10223
|
() => driver.waitForInFlight(SHUTDOWN_DRAIN_TIMEOUT_MS)
|
|
9728
10224
|
);
|
|
9729
|
-
if (!
|
|
10225
|
+
if (!drainSettled) {
|
|
9730
10226
|
logActivity(state, {
|
|
9731
10227
|
type: "info",
|
|
9732
10228
|
message: "Shutdown drain timed out with work still in flight \u2014 leaving it for restart recovery"
|
|
@@ -9734,6 +10230,9 @@ async function cleanup(state, opts = {}) {
|
|
|
9734
10230
|
if (state.interactive) displayStatus(state);
|
|
9735
10231
|
}
|
|
9736
10232
|
}
|
|
10233
|
+
if (opts.graceful && state.channelDriver && flushCredentials && drainSettled) {
|
|
10234
|
+
await flushCredentials("credential_flush_final", true);
|
|
10235
|
+
}
|
|
9737
10236
|
await timeShutdownPhase(state, durations, "offline_notify", () => notifyOffline(state));
|
|
9738
10237
|
if (state.connection) {
|
|
9739
10238
|
const connection = state.connection;
|
|
@@ -9769,6 +10268,44 @@ async function cleanup(state, opts = {}) {
|
|
|
9769
10268
|
}
|
|
9770
10269
|
return durations;
|
|
9771
10270
|
}
|
|
10271
|
+
async function beginGracefulShutdown(state, trigger) {
|
|
10272
|
+
if (state.shuttingDown) return;
|
|
10273
|
+
state.shuttingDown = true;
|
|
10274
|
+
const shutdownStartedAt = Date.now();
|
|
10275
|
+
const shutdownMessage = trigger === "recycle" ? "Recycle requested \u2014 finishing in-flight work, then exiting" : "Shutting down...";
|
|
10276
|
+
if (state.interactive) {
|
|
10277
|
+
logActivity(state, { type: "info", message: shutdownMessage });
|
|
10278
|
+
displayStatus(state);
|
|
10279
|
+
} else {
|
|
10280
|
+
log2(state, shutdownMessage);
|
|
10281
|
+
}
|
|
10282
|
+
const durations = await cleanup(state, { graceful: true });
|
|
10283
|
+
const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
|
|
10284
|
+
await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
|
|
10285
|
+
let timer;
|
|
10286
|
+
const flushed = shutdownTelemetry().then(
|
|
10287
|
+
() => true,
|
|
10288
|
+
(error2) => {
|
|
10289
|
+
log2(
|
|
10290
|
+
state,
|
|
10291
|
+
`Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
10292
|
+
"warn"
|
|
10293
|
+
);
|
|
10294
|
+
return true;
|
|
10295
|
+
}
|
|
10296
|
+
);
|
|
10297
|
+
const timedOut = new Promise((resolve4) => {
|
|
10298
|
+
timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
|
|
10299
|
+
});
|
|
10300
|
+
if (!await Promise.race([flushed, timedOut])) {
|
|
10301
|
+
log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
|
|
10302
|
+
}
|
|
10303
|
+
clearTimeout(timer);
|
|
10304
|
+
});
|
|
10305
|
+
const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
|
|
10306
|
+
log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
|
|
10307
|
+
process.exit(0);
|
|
10308
|
+
}
|
|
9772
10309
|
async function run(options) {
|
|
9773
10310
|
const interactive = isInteractive(options.json);
|
|
9774
10311
|
let logLevel;
|
|
@@ -9804,6 +10341,7 @@ async function run(options) {
|
|
|
9804
10341
|
connected: false,
|
|
9805
10342
|
opencodeConnected: false,
|
|
9806
10343
|
opencodeVersion: null,
|
|
10344
|
+
sessionDbProvenanceAnomaly: false,
|
|
9807
10345
|
opencodeProcess: null,
|
|
9808
10346
|
litestreamProcess: null,
|
|
9809
10347
|
connection: null,
|
|
@@ -9819,9 +10357,23 @@ async function run(options) {
|
|
|
9819
10357
|
openaiUsageTimer: null,
|
|
9820
10358
|
openaiUsageRearm: null,
|
|
9821
10359
|
resourceUsageTimer: null,
|
|
10360
|
+
credentialSync: null,
|
|
9822
10361
|
authHeader: ""
|
|
9823
10362
|
};
|
|
9824
10363
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
10364
|
+
if (options.credentialSyncMarker) {
|
|
10365
|
+
state.credentialSync = createCredentialSync({
|
|
10366
|
+
markerPath: options.credentialSyncMarker,
|
|
10367
|
+
env: process.env,
|
|
10368
|
+
log: (message, level = "info") => {
|
|
10369
|
+
if (level === "error") {
|
|
10370
|
+
logActivity(state, { type: "error", error: message });
|
|
10371
|
+
} else {
|
|
10372
|
+
logActivity(state, { type: "info", level, message });
|
|
10373
|
+
}
|
|
10374
|
+
}
|
|
10375
|
+
});
|
|
10376
|
+
}
|
|
9825
10377
|
if (fileSyncDirectories.length > 0) {
|
|
9826
10378
|
log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
|
|
9827
10379
|
} else {
|
|
@@ -9847,43 +10399,7 @@ async function run(options) {
|
|
|
9847
10399
|
"warn"
|
|
9848
10400
|
);
|
|
9849
10401
|
}
|
|
9850
|
-
const handleSignal =
|
|
9851
|
-
if (state.shuttingDown) return;
|
|
9852
|
-
state.shuttingDown = true;
|
|
9853
|
-
const shutdownStartedAt = Date.now();
|
|
9854
|
-
if (state.interactive) {
|
|
9855
|
-
logActivity(state, { type: "info", message: "Shutting down..." });
|
|
9856
|
-
displayStatus(state);
|
|
9857
|
-
} else {
|
|
9858
|
-
log2(state, "Shutting down...");
|
|
9859
|
-
}
|
|
9860
|
-
const durations = await cleanup(state, { graceful: true });
|
|
9861
|
-
const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
|
|
9862
|
-
await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
|
|
9863
|
-
let timer;
|
|
9864
|
-
const flushed = shutdownTelemetry().then(
|
|
9865
|
-
() => true,
|
|
9866
|
-
(error2) => {
|
|
9867
|
-
log2(
|
|
9868
|
-
state,
|
|
9869
|
-
`Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
9870
|
-
"warn"
|
|
9871
|
-
);
|
|
9872
|
-
return true;
|
|
9873
|
-
}
|
|
9874
|
-
);
|
|
9875
|
-
const timedOut = new Promise((resolve4) => {
|
|
9876
|
-
timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
|
|
9877
|
-
});
|
|
9878
|
-
if (!await Promise.race([flushed, timedOut])) {
|
|
9879
|
-
log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
|
|
9880
|
-
}
|
|
9881
|
-
clearTimeout(timer);
|
|
9882
|
-
});
|
|
9883
|
-
const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
|
|
9884
|
-
log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
|
|
9885
|
-
process.exit(0);
|
|
9886
|
-
};
|
|
10402
|
+
const handleSignal = () => beginGracefulShutdown(state, "signal");
|
|
9887
10403
|
process.on("SIGINT", handleSignal);
|
|
9888
10404
|
process.on("SIGTERM", handleSignal);
|
|
9889
10405
|
try {
|
|
@@ -10029,6 +10545,7 @@ async function run(options) {
|
|
|
10029
10545
|
await restoreCredentialStores(credentialContext);
|
|
10030
10546
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
10031
10547
|
}
|
|
10548
|
+
state.credentialSync?.arm();
|
|
10032
10549
|
let sessionDbVerifyFatal = false;
|
|
10033
10550
|
if (!options.restoreSessionDb) {
|
|
10034
10551
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -10082,6 +10599,7 @@ async function run(options) {
|
|
|
10082
10599
|
for (const warning2 of maxActiveSessionsWarnings) {
|
|
10083
10600
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
10084
10601
|
}
|
|
10602
|
+
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
10085
10603
|
const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
|
|
10086
10604
|
try {
|
|
10087
10605
|
const oc = await ensureOpenCodeRunning({
|
|
@@ -10097,7 +10615,7 @@ async function run(options) {
|
|
|
10097
10615
|
state.opencodeVersion = oc.version;
|
|
10098
10616
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
10099
10617
|
try {
|
|
10100
|
-
|
|
10618
|
+
writeFileSync6(options.opencodePidFile, `${oc.process.pid}
|
|
10101
10619
|
`, { mode: 384 });
|
|
10102
10620
|
chmodSync3(options.opencodePidFile, 384);
|
|
10103
10621
|
} catch (error2) {
|
|
@@ -10107,6 +10625,23 @@ async function run(options) {
|
|
|
10107
10625
|
});
|
|
10108
10626
|
}
|
|
10109
10627
|
}
|
|
10628
|
+
if (state.opencodeVersion !== null) {
|
|
10629
|
+
const provenance = checkSessionDbProvenance({
|
|
10630
|
+
dbPath: sessionDbPath(),
|
|
10631
|
+
currentVersion: state.opencodeVersion,
|
|
10632
|
+
homeDir: homedir5(),
|
|
10633
|
+
env: process.env
|
|
10634
|
+
});
|
|
10635
|
+
if (provenance.anomaly) {
|
|
10636
|
+
state.sessionDbProvenanceAnomaly = true;
|
|
10637
|
+
logSessionDbProvenanceMismatch(
|
|
10638
|
+
state,
|
|
10639
|
+
provenance,
|
|
10640
|
+
state.opencodeVersion,
|
|
10641
|
+
preBootMigrationIds?.length ?? null
|
|
10642
|
+
);
|
|
10643
|
+
}
|
|
10644
|
+
}
|
|
10110
10645
|
state.opencodeConnected = oc.notReadyReason === null;
|
|
10111
10646
|
const version2 = state.opencodeVersion ? ` (v${state.opencodeVersion})` : "";
|
|
10112
10647
|
ocSpinner?.succeed(`OpenCode running on port ${state.port}${version2}`);
|
|
@@ -10159,7 +10694,7 @@ async function run(options) {
|
|
|
10159
10694
|
let existingPid;
|
|
10160
10695
|
if (existsSync3(options.litestreamPidFile)) {
|
|
10161
10696
|
try {
|
|
10162
|
-
const rawPid =
|
|
10697
|
+
const rawPid = readFileSync6(options.litestreamPidFile, "utf8").trim();
|
|
10163
10698
|
const parsedPid = Number(rawPid);
|
|
10164
10699
|
if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
|
|
10165
10700
|
existingPid = parsedPid;
|
|
@@ -10196,7 +10731,7 @@ async function run(options) {
|
|
|
10196
10731
|
});
|
|
10197
10732
|
try {
|
|
10198
10733
|
if (litestreamProcess.pid !== void 0) {
|
|
10199
|
-
|
|
10734
|
+
writeFileSync6(options.litestreamPidFile, `${litestreamProcess.pid}
|
|
10200
10735
|
`, {
|
|
10201
10736
|
mode: 384
|
|
10202
10737
|
});
|
|
@@ -10402,7 +10937,17 @@ async function run(options) {
|
|
|
10402
10937
|
setTimer: (timer) => {
|
|
10403
10938
|
state.openaiUsageTimer = timer;
|
|
10404
10939
|
},
|
|
10405
|
-
fetchUsage: () =>
|
|
10940
|
+
fetchUsage: async () => {
|
|
10941
|
+
const usage = await getOpenAiUsage(state.port);
|
|
10942
|
+
if (usage.subscription === null) {
|
|
10943
|
+
logActivity(state, {
|
|
10944
|
+
type: "info",
|
|
10945
|
+
level: "debug",
|
|
10946
|
+
message: "OpenAI usage subscription could not be identified from the local credential"
|
|
10947
|
+
});
|
|
10948
|
+
}
|
|
10949
|
+
return usage;
|
|
10950
|
+
},
|
|
10406
10951
|
report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
|
|
10407
10952
|
isLocalCredentialProblem: isLocalCredentialProblem2,
|
|
10408
10953
|
forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
@@ -10448,7 +10993,7 @@ async function run(options) {
|
|
|
10448
10993
|
}
|
|
10449
10994
|
|
|
10450
10995
|
// src/index.ts
|
|
10451
|
-
var { version } =
|
|
10996
|
+
var { version } = createRequire2(import.meta.url)("../package.json");
|
|
10452
10997
|
var program = new Command();
|
|
10453
10998
|
program.name("evident").description("Run OpenCode locally and connect it to Evident").version(version).option(
|
|
10454
10999
|
"--endpoint <url>",
|
|
@@ -10526,6 +11071,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10526
11071
|
).option(
|
|
10527
11072
|
"--opencode-config-overlay <path>",
|
|
10528
11073
|
"Apply this runner-provided OpenCode config before starting OpenCode."
|
|
11074
|
+
).option(
|
|
11075
|
+
"--credential-sync-marker <path>",
|
|
11076
|
+
"Own the interval credential sync and write this marker once the shutdown flush has finished, so the runner image's lifecycle hooks can wait on it."
|
|
10529
11077
|
).action(
|
|
10530
11078
|
(options) => {
|
|
10531
11079
|
run({
|
|
@@ -10564,7 +11112,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10564
11112
|
sessionDbNoReplicateMarker: options.sessionDbNoReplicateMarker,
|
|
10565
11113
|
restoreSessionDb: options.restoreSessionDb,
|
|
10566
11114
|
restoreRunnerCredentials: options.restoreRunnerCredentials,
|
|
10567
|
-
opencodeConfigOverlay: options.opencodeConfigOverlay
|
|
11115
|
+
opencodeConfigOverlay: options.opencodeConfigOverlay,
|
|
11116
|
+
credentialSyncMarker: options.credentialSyncMarker
|
|
10568
11117
|
});
|
|
10569
11118
|
}
|
|
10570
11119
|
);
|