@evident-ai/cli 3.4.1-dev.59c7df3 → 3.4.1-dev.5eccf39
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 +732 -139
- 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
|
});
|
|
@@ -797,6 +805,7 @@ async function reportResourceUsage(agentId, authHeader, usage) {
|
|
|
797
805
|
headers: { Authorization: authHeader, "Content-Type": "application/json" },
|
|
798
806
|
body: JSON.stringify({
|
|
799
807
|
cpu_percent: usage.cpuPercent,
|
|
808
|
+
cpu_peak_percent: usage.cpuPeakPercent,
|
|
800
809
|
cpu_count: usage.cpuCount,
|
|
801
810
|
memory_total_bytes: usage.memoryTotalBytes,
|
|
802
811
|
memory_available_bytes: usage.memoryAvailableBytes,
|
|
@@ -1183,9 +1192,9 @@ async function claudeUsage() {
|
|
|
1183
1192
|
}
|
|
1184
1193
|
|
|
1185
1194
|
// src/commands/run.ts
|
|
1186
|
-
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as
|
|
1195
|
+
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
1187
1196
|
import { homedir as homedir5 } from "os";
|
|
1188
|
-
import { isAbsolute as isAbsolute3, join as
|
|
1197
|
+
import { isAbsolute as isAbsolute3, join as join9, parse, resolve as resolvePath2 } from "path";
|
|
1189
1198
|
import chalk6 from "chalk";
|
|
1190
1199
|
|
|
1191
1200
|
// ../../packages/types/src/agents/index.ts
|
|
@@ -1525,7 +1534,14 @@ function drainSessionDbRecoveryReport({
|
|
|
1525
1534
|
skippedLines++;
|
|
1526
1535
|
return [];
|
|
1527
1536
|
}
|
|
1528
|
-
return [
|
|
1537
|
+
return [
|
|
1538
|
+
{
|
|
1539
|
+
...value,
|
|
1540
|
+
provenance_reason: value.provenance_reason ?? null,
|
|
1541
|
+
provenance_migration_delta: value.provenance_migration_delta ?? null,
|
|
1542
|
+
replication_suspended: value.replication_suspended ?? false
|
|
1543
|
+
}
|
|
1544
|
+
];
|
|
1529
1545
|
} catch (error2) {
|
|
1530
1546
|
skippedLines++;
|
|
1531
1547
|
console.error(
|
|
@@ -1628,6 +1644,12 @@ function buildSessionDbRecoveryActivity(record) {
|
|
|
1628
1644
|
metadata: withoutContractFields(record),
|
|
1629
1645
|
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
1646
|
};
|
|
1647
|
+
case "schema_provenance_mismatch":
|
|
1648
|
+
return {
|
|
1649
|
+
level,
|
|
1650
|
+
metadata: withoutContractFields(record),
|
|
1651
|
+
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.`
|
|
1652
|
+
};
|
|
1631
1653
|
default:
|
|
1632
1654
|
return null;
|
|
1633
1655
|
}
|
|
@@ -1642,7 +1664,8 @@ var OUTCOMES = /* @__PURE__ */ new Set([
|
|
|
1642
1664
|
"fresh_session_db",
|
|
1643
1665
|
"history_rolled_back",
|
|
1644
1666
|
"restore_misconfigured",
|
|
1645
|
-
"session_db_boot_refused"
|
|
1667
|
+
"session_db_boot_refused",
|
|
1668
|
+
"schema_provenance_mismatch"
|
|
1646
1669
|
]);
|
|
1647
1670
|
var STAGES = /* @__PURE__ */ new Set(["restore", "verify"]);
|
|
1648
1671
|
var SEVERITIES = /* @__PURE__ */ new Set(["warning", "error"]);
|
|
@@ -1660,7 +1683,7 @@ var STRING_OR_NULL_FIELDS = ["quarantine_destination", "verified_restore_point"]
|
|
|
1660
1683
|
function isSessionDbRecoveryRecord(value) {
|
|
1661
1684
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1662
1685
|
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(
|
|
1686
|
+
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
1687
|
(field) => record[field] === null || typeof record[field] === "string"
|
|
1665
1688
|
);
|
|
1666
1689
|
}
|
|
@@ -1714,10 +1737,14 @@ function runSynchroniser(args, opts) {
|
|
|
1714
1737
|
let stderr = "";
|
|
1715
1738
|
let settled = false;
|
|
1716
1739
|
const timer = {};
|
|
1740
|
+
let abortListener;
|
|
1741
|
+
let spawnListener;
|
|
1717
1742
|
const finish = (result) => {
|
|
1718
1743
|
if (settled) return;
|
|
1719
1744
|
settled = true;
|
|
1720
1745
|
if (timer.handle) clearTimeout(timer.handle);
|
|
1746
|
+
if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
|
|
1747
|
+
if (spawnListener) child.removeListener("spawn", spawnListener);
|
|
1721
1748
|
resolve4(result);
|
|
1722
1749
|
};
|
|
1723
1750
|
try {
|
|
@@ -1743,6 +1770,25 @@ function runSynchroniser(args, opts) {
|
|
|
1743
1770
|
child.once("close", (code) => {
|
|
1744
1771
|
finish({ code, stdout, stderr, timedOut: false });
|
|
1745
1772
|
});
|
|
1773
|
+
if (opts.signal) {
|
|
1774
|
+
const killChild = () => {
|
|
1775
|
+
if (child.pid === void 0) {
|
|
1776
|
+
if (!spawnListener) {
|
|
1777
|
+
spawnListener = killChild;
|
|
1778
|
+
child.once("spawn", spawnListener);
|
|
1779
|
+
}
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
child.kill("SIGKILL");
|
|
1783
|
+
};
|
|
1784
|
+
abortListener = killChild;
|
|
1785
|
+
if (opts.signal.aborted) {
|
|
1786
|
+
abortListener();
|
|
1787
|
+
} else {
|
|
1788
|
+
opts.signal.addEventListener("abort", abortListener, { once: true });
|
|
1789
|
+
if (opts.signal.aborted) abortListener();
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1746
1792
|
timer.handle = setTimeout(
|
|
1747
1793
|
() => {
|
|
1748
1794
|
child.kill("SIGKILL");
|
|
@@ -1780,6 +1826,8 @@ function reportRecord(stage, outcome, reason, litestreamExitCode, options) {
|
|
|
1780
1826
|
quarantined_bytes: null,
|
|
1781
1827
|
verified_restore_point: null,
|
|
1782
1828
|
restore_points_tried: null,
|
|
1829
|
+
provenance_reason: null,
|
|
1830
|
+
provenance_migration_delta: null,
|
|
1783
1831
|
replication_suspended: stage === "restore"
|
|
1784
1832
|
});
|
|
1785
1833
|
}
|
|
@@ -2200,6 +2248,131 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
2200
2248
|
return { verifyFatal: await verifySessionDb(options, configPath, env) };
|
|
2201
2249
|
}
|
|
2202
2250
|
|
|
2251
|
+
// src/lib/opencode/session-db-provenance.ts
|
|
2252
|
+
import { createRequire } from "module";
|
|
2253
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2254
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
2255
|
+
var require2 = createRequire(import.meta.url);
|
|
2256
|
+
function readSessionDbMigrationIds(dbPath) {
|
|
2257
|
+
let db;
|
|
2258
|
+
try {
|
|
2259
|
+
const { DatabaseSync } = require2("node:sqlite");
|
|
2260
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
2261
|
+
const columns = db.prepare("PRAGMA table_info(migration)").all();
|
|
2262
|
+
const hasExpectedShape = columns.length === 2 && columns.some(
|
|
2263
|
+
(column) => column.name === "id" && typeof column.type === "string" && column.type.toUpperCase() === "TEXT" && column.pk === 1
|
|
2264
|
+
) && columns.some(
|
|
2265
|
+
(column) => column.name === "time_completed" && typeof column.type === "string" && column.type.toUpperCase() === "INTEGER" && column.notnull === 1 && column.pk === 0
|
|
2266
|
+
);
|
|
2267
|
+
if (!hasExpectedShape) {
|
|
2268
|
+
console.warn(`[readSessionDbMigrationIds] unsupported migration table shape in ${dbPath}`);
|
|
2269
|
+
return null;
|
|
2270
|
+
}
|
|
2271
|
+
const rows = db.prepare("SELECT id FROM migration ORDER BY id").all();
|
|
2272
|
+
if (rows.some((row) => typeof row.id !== "string")) return null;
|
|
2273
|
+
return rows.map((row) => row.id);
|
|
2274
|
+
} catch (error2) {
|
|
2275
|
+
console.warn(
|
|
2276
|
+
`[readSessionDbMigrationIds] could not read migration history from ${dbPath}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2277
|
+
);
|
|
2278
|
+
return null;
|
|
2279
|
+
} finally {
|
|
2280
|
+
try {
|
|
2281
|
+
db?.close();
|
|
2282
|
+
} catch (error2) {
|
|
2283
|
+
console.warn(
|
|
2284
|
+
`[readSessionDbMigrationIds] could not close ${dbPath}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
function sessionDbProvenanceStatePath(homeDir, env) {
|
|
2290
|
+
const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
|
|
2291
|
+
return override || join3(homeDir, ".local", "state", "evident", "session-db-provenance.json");
|
|
2292
|
+
}
|
|
2293
|
+
function loadSessionDbProvenanceState(path) {
|
|
2294
|
+
let value;
|
|
2295
|
+
try {
|
|
2296
|
+
value = JSON.parse(readFileSync3(path, "utf8"));
|
|
2297
|
+
} catch (error2) {
|
|
2298
|
+
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return {};
|
|
2299
|
+
console.error(
|
|
2300
|
+
`[session-db-provenance] could not read ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2301
|
+
);
|
|
2302
|
+
return {};
|
|
2303
|
+
}
|
|
2304
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2305
|
+
console.error(`[session-db-provenance] ignored malformed state in ${path}`);
|
|
2306
|
+
return {};
|
|
2307
|
+
}
|
|
2308
|
+
const state = {};
|
|
2309
|
+
for (const [dbPath, record] of Object.entries(value)) {
|
|
2310
|
+
if (!isSessionDbProvenanceRecord(record)) {
|
|
2311
|
+
console.error(`[session-db-provenance] ignored malformed state in ${path}`);
|
|
2312
|
+
return {};
|
|
2313
|
+
}
|
|
2314
|
+
state[dbPath] = record;
|
|
2315
|
+
}
|
|
2316
|
+
return state;
|
|
2317
|
+
}
|
|
2318
|
+
function saveSessionDbProvenanceState(path, state) {
|
|
2319
|
+
try {
|
|
2320
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
2321
|
+
writeFileSync2(path, `${JSON.stringify(state, null, 2)}
|
|
2322
|
+
`, "utf8");
|
|
2323
|
+
} catch (error2) {
|
|
2324
|
+
console.error(
|
|
2325
|
+
`[session-db-provenance] could not write ${path}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
function evaluateSessionDbProvenance(input) {
|
|
2330
|
+
const { currentVersion, currentIds, previous } = input;
|
|
2331
|
+
if (!previous) return { anomaly: false, reason: null };
|
|
2332
|
+
const current = new Set(currentIds);
|
|
2333
|
+
const prior = new Set(previous.migrationIds);
|
|
2334
|
+
for (const id of prior) {
|
|
2335
|
+
if (!current.has(id)) return { anomaly: true, reason: "migration-history-regressed" };
|
|
2336
|
+
}
|
|
2337
|
+
if (current.size > prior.size && previous.opencodeVersion === currentVersion) {
|
|
2338
|
+
return { anomaly: true, reason: "foreign-version-migrations" };
|
|
2339
|
+
}
|
|
2340
|
+
return { anomaly: false, reason: null };
|
|
2341
|
+
}
|
|
2342
|
+
function checkSessionDbProvenance(input) {
|
|
2343
|
+
const { dbPath, currentVersion, homeDir, env } = input;
|
|
2344
|
+
const path = sessionDbProvenanceStatePath(homeDir, env);
|
|
2345
|
+
const state = loadSessionDbProvenanceState(path);
|
|
2346
|
+
const previous = state[dbPath];
|
|
2347
|
+
const currentIds = readSessionDbMigrationIds(dbPath);
|
|
2348
|
+
if (currentIds === null) {
|
|
2349
|
+
return {
|
|
2350
|
+
anomaly: false,
|
|
2351
|
+
reason: null,
|
|
2352
|
+
recordedVersion: previous?.opencodeVersion ?? null,
|
|
2353
|
+
migrationDelta: null
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
const decision = evaluateSessionDbProvenance({ currentVersion, currentIds, previous });
|
|
2357
|
+
const migrationDelta = previous ? new Set(currentIds).size - new Set(previous.migrationIds).size : null;
|
|
2358
|
+
state[dbPath] = {
|
|
2359
|
+
opencodeVersion: currentVersion,
|
|
2360
|
+
migrationIds: currentIds,
|
|
2361
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2362
|
+
};
|
|
2363
|
+
saveSessionDbProvenanceState(path, state);
|
|
2364
|
+
return {
|
|
2365
|
+
...decision,
|
|
2366
|
+
recordedVersion: previous?.opencodeVersion ?? null,
|
|
2367
|
+
migrationDelta
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
function isSessionDbProvenanceRecord(value) {
|
|
2371
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2372
|
+
const record = value;
|
|
2373
|
+
return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2203
2376
|
// src/lib/opencode/opencode-version-gate.ts
|
|
2204
2377
|
var QUEUE_VALIDATED_OPENCODE_VERSIONS = ["1.17.11", "1.18.3"];
|
|
2205
2378
|
function isQueueValidatedVersion(version2) {
|
|
@@ -3286,10 +3459,10 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
3286
3459
|
|
|
3287
3460
|
// src/lib/opencode/session-db-size.ts
|
|
3288
3461
|
import { statSync as statSync3 } from "fs";
|
|
3289
|
-
import { join as
|
|
3462
|
+
import { join as join4 } from "path";
|
|
3290
3463
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
3291
3464
|
function statSessionDbBytes(homeDir) {
|
|
3292
|
-
const dbPath =
|
|
3465
|
+
const dbPath = join4(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
3293
3466
|
try {
|
|
3294
3467
|
return statSync3(dbPath).size;
|
|
3295
3468
|
} catch (err) {
|
|
@@ -3318,10 +3491,10 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
3318
3491
|
|
|
3319
3492
|
// src/lib/opencode/session-db-reclaim.ts
|
|
3320
3493
|
import { statSync as statSync4, statfsSync } from "fs";
|
|
3321
|
-
import { dirname as
|
|
3494
|
+
import { dirname as dirname4 } from "path";
|
|
3322
3495
|
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
3323
3496
|
try {
|
|
3324
|
-
const fsStats = statfsSync(
|
|
3497
|
+
const fsStats = statfsSync(dirname4(dbPath));
|
|
3325
3498
|
const availableBytes = fsStats.bavail * fsStats.bsize;
|
|
3326
3499
|
if (availableBytes < requiredBytes) {
|
|
3327
3500
|
return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
|
|
@@ -3703,8 +3876,8 @@ function connectTunnel(options) {
|
|
|
3703
3876
|
try {
|
|
3704
3877
|
message = JSON.parse(data.toString());
|
|
3705
3878
|
} catch (error2) {
|
|
3706
|
-
const
|
|
3707
|
-
onError?.(`Failed to handle message: ${
|
|
3879
|
+
const errorMessage2 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
3880
|
+
onError?.(`Failed to handle message: ${errorMessage2}`);
|
|
3708
3881
|
return;
|
|
3709
3882
|
}
|
|
3710
3883
|
if (isStreamFrame(message)) {
|
|
@@ -3847,10 +4020,10 @@ var RunnerConnection = class {
|
|
|
3847
4020
|
};
|
|
3848
4021
|
|
|
3849
4022
|
// src/lib/tunnel/ready-marker.ts
|
|
3850
|
-
import { writeFileSync as
|
|
4023
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
3851
4024
|
function writeTunnelReadyMarker(path, agentId) {
|
|
3852
4025
|
try {
|
|
3853
|
-
|
|
4026
|
+
writeFileSync3(path, `${agentId}
|
|
3854
4027
|
`);
|
|
3855
4028
|
return { ok: true };
|
|
3856
4029
|
} catch (error2) {
|
|
@@ -3875,7 +4048,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
3875
4048
|
}
|
|
3876
4049
|
|
|
3877
4050
|
// src/lib/process-liveness.ts
|
|
3878
|
-
import { readFileSync as
|
|
4051
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
3879
4052
|
function isProcessAlive(pid) {
|
|
3880
4053
|
try {
|
|
3881
4054
|
process.kill(pid, 0);
|
|
@@ -3890,7 +4063,7 @@ function isProcessAlive(pid) {
|
|
|
3890
4063
|
}
|
|
3891
4064
|
if (process.platform !== "linux") return true;
|
|
3892
4065
|
try {
|
|
3893
|
-
const status2 =
|
|
4066
|
+
const status2 = readFileSync4(`/proc/${pid}/status`, "utf8");
|
|
3894
4067
|
return !/^State:\s+Z(?:\s|$)/m.test(status2);
|
|
3895
4068
|
} catch (error2) {
|
|
3896
4069
|
console.error(
|
|
@@ -3901,9 +4074,9 @@ function isProcessAlive(pid) {
|
|
|
3901
4074
|
}
|
|
3902
4075
|
|
|
3903
4076
|
// src/lib/openai-usage.ts
|
|
3904
|
-
import { readFileSync as
|
|
4077
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3905
4078
|
import { homedir as homedir3 } from "os";
|
|
3906
|
-
import { join as
|
|
4079
|
+
import { join as join5 } from "path";
|
|
3907
4080
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
3908
4081
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
3909
4082
|
var OpenAiUsageError = class extends Error {
|
|
@@ -3917,7 +4090,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
3917
4090
|
}
|
|
3918
4091
|
function readOpenCodeChatGptCredentials() {
|
|
3919
4092
|
try {
|
|
3920
|
-
const raw =
|
|
4093
|
+
const raw = readFileSync5(join5(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
3921
4094
|
let parsed;
|
|
3922
4095
|
try {
|
|
3923
4096
|
parsed = JSON.parse(raw);
|
|
@@ -3939,6 +4112,23 @@ function readOpenCodeChatGptCredentials() {
|
|
|
3939
4112
|
return null;
|
|
3940
4113
|
}
|
|
3941
4114
|
}
|
|
4115
|
+
function parseChatGptIdentity(accessToken) {
|
|
4116
|
+
const segments = accessToken.split(".");
|
|
4117
|
+
if (segments.length !== 3) return null;
|
|
4118
|
+
let payload;
|
|
4119
|
+
try {
|
|
4120
|
+
const parsed = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf8"));
|
|
4121
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
4122
|
+
payload = parsed;
|
|
4123
|
+
} catch {
|
|
4124
|
+
return null;
|
|
4125
|
+
}
|
|
4126
|
+
const profile = payload["https://api.openai.com/profile"];
|
|
4127
|
+
const auth = payload["https://api.openai.com/auth"];
|
|
4128
|
+
const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
|
|
4129
|
+
const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
|
|
4130
|
+
return ownerEmail === null && planType === null ? null : { ownerEmail, planType };
|
|
4131
|
+
}
|
|
3942
4132
|
function toWindow2(headers, name) {
|
|
3943
4133
|
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
3944
4134
|
const windowMinutesHeader = headers.get(`x-codex-${name}-window-minutes`);
|
|
@@ -4014,6 +4204,7 @@ async function getOpenAiUsage(port) {
|
|
|
4014
4204
|
"credentials_expired"
|
|
4015
4205
|
);
|
|
4016
4206
|
}
|
|
4207
|
+
const subscription = parseChatGptIdentity(credentials2.accessToken);
|
|
4017
4208
|
const models = await resolveProbeModels(port);
|
|
4018
4209
|
if (models.length === 0) {
|
|
4019
4210
|
throw new OpenAiUsageError("No supported OpenAI probe model is available.", "no_probe_model");
|
|
@@ -4046,7 +4237,7 @@ async function getOpenAiUsage(port) {
|
|
|
4046
4237
|
"no_usable_window"
|
|
4047
4238
|
);
|
|
4048
4239
|
}
|
|
4049
|
-
return usage;
|
|
4240
|
+
return { ...usage, subscription };
|
|
4050
4241
|
}
|
|
4051
4242
|
if (res.status === 401) {
|
|
4052
4243
|
throw new OpenAiUsageError(
|
|
@@ -4247,45 +4438,84 @@ function readDisk(homeDir) {
|
|
|
4247
4438
|
};
|
|
4248
4439
|
}
|
|
4249
4440
|
}
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4441
|
+
var CPU_PEAK_WINDOW_MS = 6e4;
|
|
4442
|
+
var CPU_PEAK_WINDOW_SAMPLE_COUNT = 4;
|
|
4443
|
+
var CPU_PEAK_SAMPLE_INTERVAL_MS = CPU_PEAK_WINDOW_MS / CPU_PEAK_WINDOW_SAMPLE_COUNT;
|
|
4444
|
+
function createCpuPeakSampler() {
|
|
4445
|
+
const sampleHistory = new Array(CPU_PEAK_WINDOW_SAMPLE_COUNT);
|
|
4446
|
+
sampleHistory[0] = readCpuSample();
|
|
4447
|
+
let nextSampleIndex = 1;
|
|
4448
|
+
let sampleCount = 1;
|
|
4449
|
+
let peak = null;
|
|
4450
|
+
const timer = setInterval(() => {
|
|
4253
4451
|
const current = readCpuSample();
|
|
4254
|
-
const
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
const warnings = [];
|
|
4261
|
-
if (disk.warning) warnings.push(disk.warning);
|
|
4262
|
-
if (ecsWarning) warnings.push(ecsWarning);
|
|
4263
|
-
let cpuPercent = hostCpuPercent;
|
|
4264
|
-
let cpuCount = hostCpuCount;
|
|
4265
|
-
let memoryTotalBytes = totalmem();
|
|
4266
|
-
let memoryAvailableBytes = freemem();
|
|
4267
|
-
if (limits !== null) {
|
|
4268
|
-
cpuCount = limits.cpuCount;
|
|
4269
|
-
memoryTotalBytes = limits.memoryTotalBytes;
|
|
4270
|
-
memoryAvailableBytes = clamp(
|
|
4271
|
-
limits.memoryTotalBytes - (totalmem() - freemem()),
|
|
4272
|
-
0,
|
|
4273
|
-
limits.memoryTotalBytes
|
|
4274
|
-
);
|
|
4275
|
-
cpuPercent = hostCpuPercent === null ? null : clamp(round2(hostCpuPercent * hostCpuCount / limits.cpuCount), 0, 100);
|
|
4452
|
+
const sampleFromWindowAgo = sampleCount === CPU_PEAK_WINDOW_SAMPLE_COUNT ? sampleHistory[nextSampleIndex] : void 0;
|
|
4453
|
+
if (sampleFromWindowAgo !== void 0) {
|
|
4454
|
+
const percentage = cpuPercentBetween(sampleFromWindowAgo, current);
|
|
4455
|
+
if (percentage !== null) {
|
|
4456
|
+
peak = peak === null ? percentage : Math.max(peak, percentage);
|
|
4457
|
+
}
|
|
4276
4458
|
}
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4459
|
+
sampleHistory[nextSampleIndex] = current;
|
|
4460
|
+
nextSampleIndex = (nextSampleIndex + 1) % CPU_PEAK_WINDOW_SAMPLE_COUNT;
|
|
4461
|
+
sampleCount = Math.min(sampleCount + 1, CPU_PEAK_WINDOW_SAMPLE_COUNT);
|
|
4462
|
+
}, CPU_PEAK_SAMPLE_INTERVAL_MS);
|
|
4463
|
+
return {
|
|
4464
|
+
takeAndReset: () => {
|
|
4465
|
+
const currentPeak = peak;
|
|
4466
|
+
peak = null;
|
|
4467
|
+
return currentPeak;
|
|
4468
|
+
},
|
|
4469
|
+
stop: () => clearInterval(timer)
|
|
4470
|
+
};
|
|
4471
|
+
}
|
|
4472
|
+
function createResourceUsageCollector(homeDir) {
|
|
4473
|
+
let previous = readCpuSample();
|
|
4474
|
+
const cpuPeakSampler = createCpuPeakSampler();
|
|
4475
|
+
return {
|
|
4476
|
+
collect: async () => {
|
|
4477
|
+
const current = readCpuSample();
|
|
4478
|
+
const hostCpuPercent = cpuPercentBetween(previous, current);
|
|
4479
|
+
const hostCpuPeakPercent = cpuPeakSampler.takeAndReset();
|
|
4480
|
+
const hostCpuCount = cpus().length;
|
|
4481
|
+
previous = current;
|
|
4482
|
+
const disk = readDisk(homeDir);
|
|
4483
|
+
const opencodeDbBytes = statSessionDbBytes(homeDir);
|
|
4484
|
+
const { limits, warning: ecsWarning } = await readEcsTaskLimits(process.env);
|
|
4485
|
+
const warnings = [];
|
|
4486
|
+
if (disk.warning) warnings.push(disk.warning);
|
|
4487
|
+
if (ecsWarning) warnings.push(ecsWarning);
|
|
4488
|
+
let cpuPercent = hostCpuPercent;
|
|
4489
|
+
let cpuPeakPercent = hostCpuPeakPercent;
|
|
4490
|
+
let cpuCount = hostCpuCount;
|
|
4491
|
+
let memoryTotalBytes = totalmem();
|
|
4492
|
+
let memoryAvailableBytes = freemem();
|
|
4493
|
+
if (limits !== null) {
|
|
4494
|
+
cpuCount = limits.cpuCount;
|
|
4495
|
+
memoryTotalBytes = limits.memoryTotalBytes;
|
|
4496
|
+
memoryAvailableBytes = clamp(
|
|
4497
|
+
limits.memoryTotalBytes - (totalmem() - freemem()),
|
|
4498
|
+
0,
|
|
4499
|
+
limits.memoryTotalBytes
|
|
4500
|
+
);
|
|
4501
|
+
cpuPercent = hostCpuPercent === null ? null : clamp(round2(hostCpuPercent * hostCpuCount / limits.cpuCount), 0, 100);
|
|
4502
|
+
cpuPeakPercent = hostCpuPeakPercent === null ? null : clamp(round2(hostCpuPeakPercent * hostCpuCount / limits.cpuCount), 0, 100);
|
|
4503
|
+
}
|
|
4504
|
+
return {
|
|
4505
|
+
usage: {
|
|
4506
|
+
cpuPercent,
|
|
4507
|
+
cpuPeakPercent,
|
|
4508
|
+
cpuCount,
|
|
4509
|
+
memoryTotalBytes,
|
|
4510
|
+
memoryAvailableBytes,
|
|
4511
|
+
diskTotalBytes: disk.totalBytes,
|
|
4512
|
+
diskFreeBytes: disk.freeBytes,
|
|
4513
|
+
opencodeDbBytes
|
|
4514
|
+
},
|
|
4515
|
+
warnings
|
|
4516
|
+
};
|
|
4517
|
+
},
|
|
4518
|
+
stop: cpuPeakSampler.stop
|
|
4289
4519
|
};
|
|
4290
4520
|
}
|
|
4291
4521
|
|
|
@@ -4293,12 +4523,12 @@ function createResourceUsageCollector(homeDir) {
|
|
|
4293
4523
|
import { homedir as homedir4 } from "os";
|
|
4294
4524
|
|
|
4295
4525
|
// src/lib/runner-file-sync.ts
|
|
4296
|
-
import { join as
|
|
4526
|
+
import { join as join7 } from "path";
|
|
4297
4527
|
|
|
4298
4528
|
// src/lib/file-push.ts
|
|
4299
4529
|
import { randomUUID } from "crypto";
|
|
4300
4530
|
import { chmod, mkdir, open as open2, realpath, rename, unlink } from "fs/promises";
|
|
4301
|
-
import { basename, dirname as
|
|
4531
|
+
import { basename, dirname as dirname5, isAbsolute, join as join6, relative, resolve as resolve2, sep } from "path";
|
|
4302
4532
|
var FILE_MODE = 384;
|
|
4303
4533
|
var DIRECTORY_MODE = 448;
|
|
4304
4534
|
async function writePushedFile(request) {
|
|
@@ -4329,9 +4559,9 @@ async function writePushedFile(request) {
|
|
|
4329
4559
|
}
|
|
4330
4560
|
try {
|
|
4331
4561
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
4332
|
-
|
|
4562
|
+
dirname5(candidate)
|
|
4333
4563
|
);
|
|
4334
|
-
const realTarget =
|
|
4564
|
+
const realTarget = join6(existingAncestor, ...missingSegments, basename(candidate));
|
|
4335
4565
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
4336
4566
|
if (allowedDirectory === null) {
|
|
4337
4567
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -4341,8 +4571,8 @@ async function writePushedFile(request) {
|
|
|
4341
4571
|
}
|
|
4342
4572
|
if (missingSegments.length > 0) {
|
|
4343
4573
|
await createMissingDirectories(existingAncestor, missingSegments);
|
|
4344
|
-
const realParent = await realpath(
|
|
4345
|
-
if (realParent !==
|
|
4574
|
+
const realParent = await realpath(dirname5(realTarget));
|
|
4575
|
+
if (realParent !== dirname5(realTarget) || !contains(allowedDirectory, realTarget)) {
|
|
4346
4576
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
4347
4577
|
path: realTarget,
|
|
4348
4578
|
bytes,
|
|
@@ -4367,7 +4597,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
4367
4597
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
4368
4598
|
return null;
|
|
4369
4599
|
}
|
|
4370
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4600
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join6(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4371
4601
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
4372
4602
|
return null;
|
|
4373
4603
|
}
|
|
@@ -4385,7 +4615,7 @@ async function resolveNearestExistingAncestor(directory) {
|
|
|
4385
4615
|
try {
|
|
4386
4616
|
return { existingAncestor: await realpath(current), missingSegments };
|
|
4387
4617
|
} catch (err) {
|
|
4388
|
-
const parent =
|
|
4618
|
+
const parent = dirname5(current);
|
|
4389
4619
|
if (err.code !== "ENOENT" || parent === current) {
|
|
4390
4620
|
throw err;
|
|
4391
4621
|
}
|
|
@@ -4440,13 +4670,13 @@ function contains(realDirectory, realTarget) {
|
|
|
4440
4670
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
4441
4671
|
let current = existingAncestor;
|
|
4442
4672
|
for (const segment of missingSegments) {
|
|
4443
|
-
current =
|
|
4673
|
+
current = join6(current, segment);
|
|
4444
4674
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
4445
4675
|
await chmod(current, DIRECTORY_MODE);
|
|
4446
4676
|
}
|
|
4447
4677
|
}
|
|
4448
4678
|
async function writeAtomically(realTarget, content) {
|
|
4449
|
-
const temporaryPath =
|
|
4679
|
+
const temporaryPath = join6(dirname5(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
4450
4680
|
let handle;
|
|
4451
4681
|
try {
|
|
4452
4682
|
handle = await open2(temporaryPath, "wx", FILE_MODE);
|
|
@@ -4576,12 +4806,12 @@ var NOT_APPLIED = {
|
|
|
4576
4806
|
opencodeAuthApplied: false
|
|
4577
4807
|
};
|
|
4578
4808
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
4579
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4580
|
-
return expanded ===
|
|
4809
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4810
|
+
return expanded === join7(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
4581
4811
|
}
|
|
4582
4812
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
4583
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
4584
|
-
return expanded ===
|
|
4813
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join7(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
4814
|
+
return expanded === join7(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
4585
4815
|
}
|
|
4586
4816
|
async function applyOne(options, file) {
|
|
4587
4817
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -5108,6 +5338,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5108
5338
|
* and stops opencode.
|
|
5109
5339
|
*/
|
|
5110
5340
|
stopped = false;
|
|
5341
|
+
recycleRequestedFlag = false;
|
|
5111
5342
|
constructor(config) {
|
|
5112
5343
|
this.agentId = config.agentId;
|
|
5113
5344
|
this.port = config.port;
|
|
@@ -5213,6 +5444,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5213
5444
|
let dispatched = 0;
|
|
5214
5445
|
try {
|
|
5215
5446
|
const conversations = await this.getPendingConversations();
|
|
5447
|
+
if (this.recycleRequestedFlag) {
|
|
5448
|
+
this.stop();
|
|
5449
|
+
}
|
|
5216
5450
|
if (conversations.length > 0) {
|
|
5217
5451
|
const total = conversations.reduce((sum, c) => sum + (c.pending_message_count ?? 0), 0);
|
|
5218
5452
|
this.log({
|
|
@@ -5341,6 +5575,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5341
5575
|
stop() {
|
|
5342
5576
|
this.stopped = true;
|
|
5343
5577
|
}
|
|
5578
|
+
/**
|
|
5579
|
+
* The server clears this request when a new MicroVM identity is recorded, so a
|
|
5580
|
+
* same-VM tunnel reconnect does not consume it. This is a plain read rather
|
|
5581
|
+
* than a consume; `run.ts` guards the action once-only.
|
|
5582
|
+
*/
|
|
5583
|
+
get recycleRequested() {
|
|
5584
|
+
return this.recycleRequestedFlag;
|
|
5585
|
+
}
|
|
5344
5586
|
/**
|
|
5345
5587
|
* Wait (up to `timeoutMs`) for in-flight watcher work to settle during a
|
|
5346
5588
|
* graceful shutdown, so a turn whose reply is ready — or completes within the
|
|
@@ -5478,7 +5720,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5478
5720
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
5479
5721
|
break;
|
|
5480
5722
|
}
|
|
5481
|
-
const
|
|
5723
|
+
const errorMessage2 = err instanceof Error ? err.message : String(err);
|
|
5482
5724
|
this.sessions.delete(conv.id);
|
|
5483
5725
|
this.supersede(conv.id, sessionId);
|
|
5484
5726
|
this.log({
|
|
@@ -5487,7 +5729,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5487
5729
|
conversation_id: conv.id,
|
|
5488
5730
|
message_id: message.id
|
|
5489
5731
|
});
|
|
5490
|
-
await this.markFailed(conv.id, message.id, null,
|
|
5732
|
+
await this.markFailed(conv.id, message.id, null, errorMessage2).catch((markErr) => {
|
|
5491
5733
|
this.log({
|
|
5492
5734
|
level: "warn",
|
|
5493
5735
|
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 +5740,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5498
5740
|
});
|
|
5499
5741
|
this.log({
|
|
5500
5742
|
level: "error",
|
|
5501
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
5743
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage2}`,
|
|
5502
5744
|
conversation_id: conv.id,
|
|
5503
5745
|
message_id: message.id
|
|
5504
5746
|
});
|
|
@@ -5519,14 +5761,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5519
5761
|
this.unconfirmedDispatchFailures.delete(message.id);
|
|
5520
5762
|
this.sessions.delete(conv.id);
|
|
5521
5763
|
this.supersede(conv.id, sessionId);
|
|
5522
|
-
const
|
|
5764
|
+
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
5765
|
this.log({
|
|
5524
5766
|
level: "error",
|
|
5525
|
-
message:
|
|
5767
|
+
message: errorMessage2,
|
|
5526
5768
|
conversation_id: conv.id,
|
|
5527
5769
|
message_id: message.id
|
|
5528
5770
|
});
|
|
5529
|
-
await this.markFailed(conv.id, message.id, null,
|
|
5771
|
+
await this.markFailed(conv.id, message.id, null, errorMessage2).catch((markErr) => {
|
|
5530
5772
|
this.log({
|
|
5531
5773
|
level: "warn",
|
|
5532
5774
|
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 +7732,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7490
7732
|
this.unconfirmedDispatchFailures.delete(row.id);
|
|
7491
7733
|
this.sessions.delete(readoptConv.id);
|
|
7492
7734
|
this.supersede(readoptConv.id, sessionId);
|
|
7493
|
-
const
|
|
7735
|
+
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
7736
|
this.log({
|
|
7495
7737
|
level: "error",
|
|
7496
|
-
message:
|
|
7738
|
+
message: errorMessage2,
|
|
7497
7739
|
conversation_id: row.conversation_id,
|
|
7498
7740
|
message_id: row.id
|
|
7499
7741
|
});
|
|
7500
|
-
await this.markFailed(row.conversation_id, row.id, null,
|
|
7742
|
+
await this.markFailed(row.conversation_id, row.id, null, errorMessage2).catch((markErr) => {
|
|
7501
7743
|
this.log({
|
|
7502
7744
|
level: "warn",
|
|
7503
7745
|
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 +8354,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8112
8354
|
throw new Error(`Failed to get pending conversations: HTTP ${res.status}`);
|
|
8113
8355
|
}
|
|
8114
8356
|
const data = await res.json();
|
|
8357
|
+
this.recycleRequestedFlag = data.recycle_requested === true;
|
|
8115
8358
|
let conversations = data.conversations;
|
|
8116
8359
|
if (this.conversationFilter) {
|
|
8117
8360
|
conversations = conversations.filter((c) => c.id === this.conversationFilter);
|
|
@@ -8629,7 +8872,7 @@ Port ${port} is already in use.`));
|
|
|
8629
8872
|
}
|
|
8630
8873
|
|
|
8631
8874
|
// src/lib/runner-credentials.ts
|
|
8632
|
-
import { chmodSync as chmodSync2, writeFileSync as
|
|
8875
|
+
import { chmodSync as chmodSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
8633
8876
|
import { spawn as spawn5 } from "child_process";
|
|
8634
8877
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
8635
8878
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
@@ -8870,8 +9113,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
8870
9113
|
}
|
|
8871
9114
|
try {
|
|
8872
9115
|
env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
|
|
8873
|
-
|
|
8874
|
-
|
|
9116
|
+
writeFileSync4(GIT_CONFIG_GLOBAL, "");
|
|
9117
|
+
writeFileSync4(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
|
|
8875
9118
|
chmodSync2(GIT_CREDENTIAL_HELPER, 448);
|
|
8876
9119
|
const config = [
|
|
8877
9120
|
["user.name", env.GIT_USER_NAME ?? "evident-bot"],
|
|
@@ -8904,7 +9147,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
8904
9147
|
// src/lib/opencode/config-overlay.ts
|
|
8905
9148
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
8906
9149
|
import { copyFileSync, existsSync as existsSync2, statSync as statSync5 } from "fs";
|
|
8907
|
-
import { isAbsolute as isAbsolute2, join as
|
|
9150
|
+
import { isAbsolute as isAbsolute2, join as join8, resolve as resolve3 } from "path";
|
|
8908
9151
|
function isFile(filePath) {
|
|
8909
9152
|
return existsSync2(filePath) && statSync5(filePath).isFile();
|
|
8910
9153
|
}
|
|
@@ -8918,7 +9161,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
8918
9161
|
return;
|
|
8919
9162
|
}
|
|
8920
9163
|
const source = isAbsolute2(overlayPath) ? overlayPath : resolve3(cwd, overlayPath);
|
|
8921
|
-
const target = isFile(
|
|
9164
|
+
const target = isFile(join8(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
8922
9165
|
if (!isFile(source)) {
|
|
8923
9166
|
log3(
|
|
8924
9167
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -8926,7 +9169,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
8926
9169
|
);
|
|
8927
9170
|
return;
|
|
8928
9171
|
}
|
|
8929
|
-
copyFileSync(source,
|
|
9172
|
+
copyFileSync(source, join8(cwd, target));
|
|
8930
9173
|
try {
|
|
8931
9174
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
8932
9175
|
stdio: "ignore"
|
|
@@ -8935,7 +9178,242 @@ function applyRunnerOpenCodeConfig({
|
|
|
8935
9178
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
8936
9179
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
8937
9180
|
}
|
|
8938
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
9181
|
+
log3(`Applied runner OpenCode config ${source} to ${join8(cwd, target)}`);
|
|
9182
|
+
}
|
|
9183
|
+
|
|
9184
|
+
// src/lib/credential-sync.ts
|
|
9185
|
+
import { renameSync, writeFileSync as writeFileSync5 } from "fs";
|
|
9186
|
+
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
9187
|
+
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
9188
|
+
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
9189
|
+
var DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS = 60;
|
|
9190
|
+
var STORES = ["claude", "opencode"];
|
|
9191
|
+
var MAX_FLUSH_PASSES = 2;
|
|
9192
|
+
function outcomesWith(outcome) {
|
|
9193
|
+
return { claude: outcome, opencode: outcome };
|
|
9194
|
+
}
|
|
9195
|
+
function errorMessage(error2) {
|
|
9196
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
9197
|
+
}
|
|
9198
|
+
function waitForSettlement(promise, timeoutMs) {
|
|
9199
|
+
return new Promise((resolve4) => {
|
|
9200
|
+
let settled = false;
|
|
9201
|
+
const timer = setTimeout(() => finish(false), Math.max(0, timeoutMs));
|
|
9202
|
+
const finish = (value) => {
|
|
9203
|
+
if (settled) return;
|
|
9204
|
+
settled = true;
|
|
9205
|
+
clearTimeout(timer);
|
|
9206
|
+
resolve4(value);
|
|
9207
|
+
};
|
|
9208
|
+
promise.then(
|
|
9209
|
+
() => finish(true),
|
|
9210
|
+
() => finish(true)
|
|
9211
|
+
);
|
|
9212
|
+
});
|
|
9213
|
+
}
|
|
9214
|
+
function writeMarker(markerPath, outcomes, log3) {
|
|
9215
|
+
const body = `${STORES.map((store) => `${store}=${outcomes[store]}`).join("\n")}
|
|
9216
|
+
`;
|
|
9217
|
+
const temporaryPath = `${markerPath}.tmp`;
|
|
9218
|
+
try {
|
|
9219
|
+
writeFileSync5(temporaryPath, body, { mode: 384 });
|
|
9220
|
+
renameSync(temporaryPath, markerPath);
|
|
9221
|
+
} catch (error2) {
|
|
9222
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage(error2)}`, "warn");
|
|
9223
|
+
}
|
|
9224
|
+
}
|
|
9225
|
+
function intervalSeconds(env, log3) {
|
|
9226
|
+
const raw = env.CREDS_SYNC_INTERVAL;
|
|
9227
|
+
if (raw === void 0 || /^[1-9][0-9]*$/.test(raw) && Number.isSafeInteger(Number(raw))) {
|
|
9228
|
+
return raw === void 0 ? DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS : Number(raw);
|
|
9229
|
+
}
|
|
9230
|
+
log3(
|
|
9231
|
+
`CREDENTIAL-SYNC-INTERVAL-INVALID: CREDS_SYNC_INTERVAL='${raw}' is not a positive integer; using ${DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS}s`,
|
|
9232
|
+
"warn"
|
|
9233
|
+
);
|
|
9234
|
+
return DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS;
|
|
9235
|
+
}
|
|
9236
|
+
async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
9237
|
+
const remainingMs = deadlineAt - Date.now();
|
|
9238
|
+
if (remainingMs <= 0) return { outcome: "timeout", orphaned: false };
|
|
9239
|
+
const controller = new AbortController();
|
|
9240
|
+
let result;
|
|
9241
|
+
let failed = false;
|
|
9242
|
+
const completion = Promise.resolve().then(
|
|
9243
|
+
() => synchroniserRunner(["sync-once", store], {
|
|
9244
|
+
timeoutMs: remainingMs,
|
|
9245
|
+
env,
|
|
9246
|
+
signal: controller.signal
|
|
9247
|
+
})
|
|
9248
|
+
).then(
|
|
9249
|
+
(value) => {
|
|
9250
|
+
result = value;
|
|
9251
|
+
},
|
|
9252
|
+
(error2) => {
|
|
9253
|
+
failed = true;
|
|
9254
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage(error2)}`, "warn");
|
|
9255
|
+
}
|
|
9256
|
+
);
|
|
9257
|
+
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
9258
|
+
const settledBeforeDeadline = await waitForSettlement(completion, remainingMs);
|
|
9259
|
+
clearTimeout(abortTimer);
|
|
9260
|
+
if (!settledBeforeDeadline) {
|
|
9261
|
+
controller.abort();
|
|
9262
|
+
const settledAfterAbort = await waitForSettlement(completion, CREDENTIAL_FLUSH_ABORT_GRACE_MS);
|
|
9263
|
+
if (!settledAfterAbort) return { outcome: "timeout", orphaned: true };
|
|
9264
|
+
return { outcome: "timeout", orphaned: false };
|
|
9265
|
+
}
|
|
9266
|
+
if (failed || !result) return { outcome: "failed", orphaned: false };
|
|
9267
|
+
if (result.timedOut || Date.now() >= deadlineAt) {
|
|
9268
|
+
return { outcome: "timeout", orphaned: false };
|
|
9269
|
+
}
|
|
9270
|
+
return { outcome: result.code === 0 ? "ok" : "failed", orphaned: false };
|
|
9271
|
+
}
|
|
9272
|
+
function createCredentialSync({
|
|
9273
|
+
markerPath,
|
|
9274
|
+
env,
|
|
9275
|
+
log: log3,
|
|
9276
|
+
synchroniserRunner = runSynchroniser
|
|
9277
|
+
}) {
|
|
9278
|
+
const persistenceDisabled = !env.PERSISTENCE_BUCKET;
|
|
9279
|
+
let disabled = persistenceDisabled;
|
|
9280
|
+
let armed = false;
|
|
9281
|
+
let stopped = false;
|
|
9282
|
+
let timer;
|
|
9283
|
+
let inFlight;
|
|
9284
|
+
let activeTickAbort;
|
|
9285
|
+
let lastTickFailed;
|
|
9286
|
+
let flushPromise;
|
|
9287
|
+
const scheduleTick = (intervalMs, startTick2) => {
|
|
9288
|
+
if (stopped) return;
|
|
9289
|
+
timer = setTimeout(() => {
|
|
9290
|
+
timer = void 0;
|
|
9291
|
+
startTick2();
|
|
9292
|
+
}, intervalMs);
|
|
9293
|
+
};
|
|
9294
|
+
const startTick = (intervalMs) => {
|
|
9295
|
+
if (stopped) return;
|
|
9296
|
+
const controller = new AbortController();
|
|
9297
|
+
activeTickAbort = controller;
|
|
9298
|
+
const tick = (async () => {
|
|
9299
|
+
const outcomes = {
|
|
9300
|
+
claude: "failed",
|
|
9301
|
+
opencode: "failed"
|
|
9302
|
+
};
|
|
9303
|
+
for (const store of STORES) {
|
|
9304
|
+
if (controller.signal.aborted) break;
|
|
9305
|
+
try {
|
|
9306
|
+
const result = await synchroniserRunner(["sync-once", store], {
|
|
9307
|
+
timeoutMs: CREDENTIAL_SYNC_TICK_TIMEOUT_MS,
|
|
9308
|
+
env,
|
|
9309
|
+
signal: controller.signal
|
|
9310
|
+
});
|
|
9311
|
+
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
9312
|
+
} catch (error2) {
|
|
9313
|
+
outcomes[store] = "failed";
|
|
9314
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage(error2)}`, "debug");
|
|
9315
|
+
}
|
|
9316
|
+
}
|
|
9317
|
+
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
9318
|
+
log3(
|
|
9319
|
+
`CREDENTIAL-SYNC-TICK: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
|
|
9320
|
+
"debug"
|
|
9321
|
+
);
|
|
9322
|
+
if (failed && lastTickFailed !== true) {
|
|
9323
|
+
log3(
|
|
9324
|
+
"CREDENTIAL-SYNC-FAILED: an interval credential sync failed; retrying on the next tick",
|
|
9325
|
+
"warn"
|
|
9326
|
+
);
|
|
9327
|
+
} else if (!failed && lastTickFailed === true) {
|
|
9328
|
+
log3("CREDENTIAL-SYNC-RECOVERED: interval credential sync succeeded again", "warn");
|
|
9329
|
+
}
|
|
9330
|
+
lastTickFailed = failed;
|
|
9331
|
+
})().finally(() => {
|
|
9332
|
+
if (activeTickAbort === controller) activeTickAbort = void 0;
|
|
9333
|
+
if (inFlight === tick) inFlight = void 0;
|
|
9334
|
+
scheduleTick(intervalMs, () => startTick(intervalMs));
|
|
9335
|
+
});
|
|
9336
|
+
inFlight = tick;
|
|
9337
|
+
};
|
|
9338
|
+
const performFlush = async () => {
|
|
9339
|
+
stopped = true;
|
|
9340
|
+
if (timer) {
|
|
9341
|
+
clearTimeout(timer);
|
|
9342
|
+
timer = void 0;
|
|
9343
|
+
}
|
|
9344
|
+
const deadlineAt = Date.now() + CREDENTIAL_FLUSH_DEADLINE_MS;
|
|
9345
|
+
if (inFlight) {
|
|
9346
|
+
const settled = await waitForSettlement(inFlight, CREDENTIAL_FLUSH_DEADLINE_MS);
|
|
9347
|
+
if (!settled) {
|
|
9348
|
+
activeTickAbort?.abort();
|
|
9349
|
+
const settledAfterAbort = await waitForSettlement(
|
|
9350
|
+
inFlight,
|
|
9351
|
+
CREDENTIAL_FLUSH_ABORT_GRACE_MS
|
|
9352
|
+
);
|
|
9353
|
+
if (!settledAfterAbort) {
|
|
9354
|
+
log3(
|
|
9355
|
+
"CREDENTIAL-FLUSH-ORPHANED-TICK: a sync-once child could not be proven settled; skipping the boundary flush rather than becoming a second writer",
|
|
9356
|
+
"warn"
|
|
9357
|
+
);
|
|
9358
|
+
return { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
9359
|
+
}
|
|
9360
|
+
}
|
|
9361
|
+
}
|
|
9362
|
+
if (disabled) return { outcomes: outcomesWith("skipped"), orphaned: false };
|
|
9363
|
+
const outcomes = outcomesWith("timeout");
|
|
9364
|
+
for (const store of STORES) {
|
|
9365
|
+
const result = await runFlushStore(store, env, synchroniserRunner, log3, deadlineAt);
|
|
9366
|
+
if (result.orphaned) {
|
|
9367
|
+
log3(
|
|
9368
|
+
"CREDENTIAL-FLUSH-ORPHANED-CHILD: a flush sync-once child could not be proven settled; refusing another flush pass rather than becoming a second writer",
|
|
9369
|
+
"warn"
|
|
9370
|
+
);
|
|
9371
|
+
return { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
9372
|
+
}
|
|
9373
|
+
outcomes[store] = result.outcome;
|
|
9374
|
+
}
|
|
9375
|
+
return { outcomes, orphaned: false };
|
|
9376
|
+
};
|
|
9377
|
+
let flushPasses = 0;
|
|
9378
|
+
let lastFlush;
|
|
9379
|
+
return {
|
|
9380
|
+
arm() {
|
|
9381
|
+
if (stopped || armed) return;
|
|
9382
|
+
armed = true;
|
|
9383
|
+
if (persistenceDisabled) {
|
|
9384
|
+
disabled = true;
|
|
9385
|
+
log3(
|
|
9386
|
+
"CREDENTIAL-SYNC-DISABLED: LITESTREAM_BUCKET/LITESTREAM_PREFIX are not both set; no interval credential sync this boot",
|
|
9387
|
+
"warn"
|
|
9388
|
+
);
|
|
9389
|
+
return;
|
|
9390
|
+
}
|
|
9391
|
+
disabled = false;
|
|
9392
|
+
const intervalMs = intervalSeconds(env, log3) * 1e3;
|
|
9393
|
+
scheduleTick(intervalMs, () => startTick(intervalMs));
|
|
9394
|
+
},
|
|
9395
|
+
async stopAndFlush(publish) {
|
|
9396
|
+
let result;
|
|
9397
|
+
const runningFlush = flushPromise;
|
|
9398
|
+
if (runningFlush) {
|
|
9399
|
+
result = await runningFlush;
|
|
9400
|
+
} else if (flushPasses >= MAX_FLUSH_PASSES || lastFlush?.orphaned) {
|
|
9401
|
+
result = lastFlush ?? { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
9402
|
+
} else {
|
|
9403
|
+
flushPasses++;
|
|
9404
|
+
const currentFlush = performFlush();
|
|
9405
|
+
flushPromise = currentFlush;
|
|
9406
|
+
try {
|
|
9407
|
+
result = await currentFlush;
|
|
9408
|
+
lastFlush = result;
|
|
9409
|
+
} finally {
|
|
9410
|
+
if (flushPromise === currentFlush) flushPromise = void 0;
|
|
9411
|
+
}
|
|
9412
|
+
}
|
|
9413
|
+
if (publish) writeMarker(markerPath, result.outcomes, log3);
|
|
9414
|
+
return result.outcomes;
|
|
9415
|
+
}
|
|
9416
|
+
};
|
|
8939
9417
|
}
|
|
8940
9418
|
|
|
8941
9419
|
// src/commands/run.ts
|
|
@@ -8975,7 +9453,7 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
8975
9453
|
if (trimmed === "") {
|
|
8976
9454
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
8977
9455
|
}
|
|
8978
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
9456
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join9(homeDir, trimmed.slice(2)) : trimmed;
|
|
8979
9457
|
if (!isAbsolute3(expanded)) {
|
|
8980
9458
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
8981
9459
|
}
|
|
@@ -9235,6 +9713,10 @@ async function driveChannels(state, driver) {
|
|
|
9235
9713
|
consecutiveDrainFailures = 0;
|
|
9236
9714
|
unreachableMs = 0;
|
|
9237
9715
|
state.messageCount += processed;
|
|
9716
|
+
if (driver.recycleRequested) {
|
|
9717
|
+
await beginGracefulShutdown(state, "recycle");
|
|
9718
|
+
return;
|
|
9719
|
+
}
|
|
9238
9720
|
const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;
|
|
9239
9721
|
lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
|
|
9240
9722
|
const fileActivitySnapshot = driver.fileSyncActivity();
|
|
@@ -9277,8 +9759,8 @@ async function driveChannels(state, driver) {
|
|
|
9277
9759
|
state.running = false;
|
|
9278
9760
|
break;
|
|
9279
9761
|
}
|
|
9280
|
-
const
|
|
9281
|
-
logActivity(state, { type: "error", error: `Channel processing error: ${
|
|
9762
|
+
const errorMessage2 = error2 instanceof Error ? error2.message : String(error2);
|
|
9763
|
+
logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage2}` });
|
|
9282
9764
|
if (state.interactive) displayStatus(state);
|
|
9283
9765
|
if (driver.hasInFlightWatchers()) {
|
|
9284
9766
|
consecutiveDrainFailures = 0;
|
|
@@ -9318,7 +9800,43 @@ async function driveChannels(state, driver) {
|
|
|
9318
9800
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
9319
9801
|
var SESSION_DB_RECLAIM_MAX_PAGES = 2e3;
|
|
9320
9802
|
function sessionDbPath() {
|
|
9321
|
-
return
|
|
9803
|
+
return join9(homedir5(), ".local", "share", "opencode", "opencode.db");
|
|
9804
|
+
}
|
|
9805
|
+
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
9806
|
+
const record = {
|
|
9807
|
+
v: 1,
|
|
9808
|
+
event: "session_db_recovery",
|
|
9809
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9810
|
+
stage: "verify",
|
|
9811
|
+
outcome: "schema_provenance_mismatch",
|
|
9812
|
+
severity: "error",
|
|
9813
|
+
reason: provenance.reason ?? "schema-provenance-mismatch",
|
|
9814
|
+
litestream_exit_code: null,
|
|
9815
|
+
attempt: null,
|
|
9816
|
+
replica_objects: null,
|
|
9817
|
+
replica_bytes: null,
|
|
9818
|
+
quarantine_destination: null,
|
|
9819
|
+
quarantined_objects: null,
|
|
9820
|
+
quarantine_failed_objects: null,
|
|
9821
|
+
quarantined_bytes: null,
|
|
9822
|
+
verified_restore_point: null,
|
|
9823
|
+
restore_points_tried: null,
|
|
9824
|
+
provenance_reason: provenance.reason,
|
|
9825
|
+
provenance_migration_delta: provenance.migrationDelta,
|
|
9826
|
+
replication_suspended: false,
|
|
9827
|
+
dbPath: sessionDbPath(),
|
|
9828
|
+
recorded_version: provenance.recordedVersion,
|
|
9829
|
+
current_version: currentVersion,
|
|
9830
|
+
provenance_pre_boot_migration_count: preBootMigrationCount
|
|
9831
|
+
};
|
|
9832
|
+
const activity = buildSessionDbRecoveryActivity(record);
|
|
9833
|
+
if (!activity) throw new Error("could not map session-DB provenance activity");
|
|
9834
|
+
logActivity(state, {
|
|
9835
|
+
type: activity.level === "error" ? "error" : "info",
|
|
9836
|
+
level: activity.level,
|
|
9837
|
+
...activity.level === "error" ? { error: activity.message } : { message: activity.message },
|
|
9838
|
+
metadata: activity.metadata
|
|
9839
|
+
});
|
|
9322
9840
|
}
|
|
9323
9841
|
async function runSweep(state, driver, config) {
|
|
9324
9842
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
@@ -9365,7 +9883,7 @@ async function runSweep(state, driver, config) {
|
|
|
9365
9883
|
const reclaimResult = await reclaimSessionDbSpace({
|
|
9366
9884
|
dbPath: sessionDbPath(),
|
|
9367
9885
|
maxPages: SESSION_DB_RECLAIM_MAX_PAGES,
|
|
9368
|
-
allowFullVacuum: protectedNow.size === 0
|
|
9886
|
+
allowFullVacuum: protectedNow.size === 0 && !state.sessionDbProvenanceAnomaly
|
|
9369
9887
|
});
|
|
9370
9888
|
if (reclaimResult.ok) {
|
|
9371
9889
|
const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
|
|
@@ -9602,7 +10120,8 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
9602
10120
|
});
|
|
9603
10121
|
return;
|
|
9604
10122
|
}
|
|
9605
|
-
const collect = createResourceUsageCollector(homedir5());
|
|
10123
|
+
const { collect, stop } = createResourceUsageCollector(homedir5());
|
|
10124
|
+
state.stopResourceUsageSampling = stop;
|
|
9606
10125
|
let consecutiveFailures = 0;
|
|
9607
10126
|
const tick = async () => {
|
|
9608
10127
|
try {
|
|
@@ -9712,21 +10231,41 @@ async function cleanup(state, opts = {}) {
|
|
|
9712
10231
|
clearTimeout(state.resourceUsageTimer);
|
|
9713
10232
|
state.resourceUsageTimer = null;
|
|
9714
10233
|
}
|
|
10234
|
+
state.stopResourceUsageSampling?.();
|
|
10235
|
+
state.stopResourceUsageSampling = null;
|
|
10236
|
+
const credentialSync = state.credentialSync;
|
|
10237
|
+
const flushCredentials = credentialSync ? async (phase, publish) => {
|
|
10238
|
+
await timeShutdownPhase(state, durations, phase, async () => {
|
|
10239
|
+
const outcomes = await credentialSync.stopAndFlush(publish);
|
|
10240
|
+
const level = Object.values(outcomes).every((outcome) => outcome === "ok") ? "info" : "warn";
|
|
10241
|
+
log2(
|
|
10242
|
+
state,
|
|
10243
|
+
`Credential flush: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
|
|
10244
|
+
level
|
|
10245
|
+
);
|
|
10246
|
+
});
|
|
10247
|
+
} : void 0;
|
|
10248
|
+
let drainSettled = true;
|
|
9715
10249
|
if (opts.graceful && state.channelDriver) {
|
|
9716
10250
|
state.channelDriver.stop();
|
|
10251
|
+
}
|
|
10252
|
+
if (flushCredentials) {
|
|
10253
|
+
await flushCredentials("credential_flush", !(opts.graceful && state.channelDriver));
|
|
10254
|
+
}
|
|
10255
|
+
if (opts.graceful && state.channelDriver) {
|
|
9717
10256
|
log2(state, "Draining in-flight channel work before shutdown...");
|
|
9718
10257
|
if (state.interactive) {
|
|
9719
10258
|
logActivity(state, { type: "info", message: "Draining in-flight work before shutdown..." });
|
|
9720
10259
|
displayStatus(state);
|
|
9721
10260
|
}
|
|
9722
10261
|
const driver = state.channelDriver;
|
|
9723
|
-
|
|
10262
|
+
drainSettled = await timeShutdownPhase(
|
|
9724
10263
|
state,
|
|
9725
10264
|
durations,
|
|
9726
10265
|
"drain",
|
|
9727
10266
|
() => driver.waitForInFlight(SHUTDOWN_DRAIN_TIMEOUT_MS)
|
|
9728
10267
|
);
|
|
9729
|
-
if (!
|
|
10268
|
+
if (!drainSettled) {
|
|
9730
10269
|
logActivity(state, {
|
|
9731
10270
|
type: "info",
|
|
9732
10271
|
message: "Shutdown drain timed out with work still in flight \u2014 leaving it for restart recovery"
|
|
@@ -9734,6 +10273,9 @@ async function cleanup(state, opts = {}) {
|
|
|
9734
10273
|
if (state.interactive) displayStatus(state);
|
|
9735
10274
|
}
|
|
9736
10275
|
}
|
|
10276
|
+
if (opts.graceful && state.channelDriver && flushCredentials && drainSettled) {
|
|
10277
|
+
await flushCredentials("credential_flush_final", true);
|
|
10278
|
+
}
|
|
9737
10279
|
await timeShutdownPhase(state, durations, "offline_notify", () => notifyOffline(state));
|
|
9738
10280
|
if (state.connection) {
|
|
9739
10281
|
const connection = state.connection;
|
|
@@ -9769,6 +10311,44 @@ async function cleanup(state, opts = {}) {
|
|
|
9769
10311
|
}
|
|
9770
10312
|
return durations;
|
|
9771
10313
|
}
|
|
10314
|
+
async function beginGracefulShutdown(state, trigger) {
|
|
10315
|
+
if (state.shuttingDown) return;
|
|
10316
|
+
state.shuttingDown = true;
|
|
10317
|
+
const shutdownStartedAt = Date.now();
|
|
10318
|
+
const shutdownMessage = trigger === "recycle" ? "Recycle requested \u2014 finishing in-flight work, then exiting" : "Shutting down...";
|
|
10319
|
+
if (state.interactive) {
|
|
10320
|
+
logActivity(state, { type: "info", message: shutdownMessage });
|
|
10321
|
+
displayStatus(state);
|
|
10322
|
+
} else {
|
|
10323
|
+
log2(state, shutdownMessage);
|
|
10324
|
+
}
|
|
10325
|
+
const durations = await cleanup(state, { graceful: true });
|
|
10326
|
+
const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
|
|
10327
|
+
await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
|
|
10328
|
+
let timer;
|
|
10329
|
+
const flushed = shutdownTelemetry().then(
|
|
10330
|
+
() => true,
|
|
10331
|
+
(error2) => {
|
|
10332
|
+
log2(
|
|
10333
|
+
state,
|
|
10334
|
+
`Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
10335
|
+
"warn"
|
|
10336
|
+
);
|
|
10337
|
+
return true;
|
|
10338
|
+
}
|
|
10339
|
+
);
|
|
10340
|
+
const timedOut = new Promise((resolve4) => {
|
|
10341
|
+
timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
|
|
10342
|
+
});
|
|
10343
|
+
if (!await Promise.race([flushed, timedOut])) {
|
|
10344
|
+
log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
|
|
10345
|
+
}
|
|
10346
|
+
clearTimeout(timer);
|
|
10347
|
+
});
|
|
10348
|
+
const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
|
|
10349
|
+
log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
|
|
10350
|
+
process.exit(0);
|
|
10351
|
+
}
|
|
9772
10352
|
async function run(options) {
|
|
9773
10353
|
const interactive = isInteractive(options.json);
|
|
9774
10354
|
let logLevel;
|
|
@@ -9804,6 +10384,7 @@ async function run(options) {
|
|
|
9804
10384
|
connected: false,
|
|
9805
10385
|
opencodeConnected: false,
|
|
9806
10386
|
opencodeVersion: null,
|
|
10387
|
+
sessionDbProvenanceAnomaly: false,
|
|
9807
10388
|
opencodeProcess: null,
|
|
9808
10389
|
litestreamProcess: null,
|
|
9809
10390
|
connection: null,
|
|
@@ -9819,9 +10400,24 @@ async function run(options) {
|
|
|
9819
10400
|
openaiUsageTimer: null,
|
|
9820
10401
|
openaiUsageRearm: null,
|
|
9821
10402
|
resourceUsageTimer: null,
|
|
10403
|
+
stopResourceUsageSampling: null,
|
|
10404
|
+
credentialSync: null,
|
|
9822
10405
|
authHeader: ""
|
|
9823
10406
|
};
|
|
9824
10407
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
10408
|
+
if (options.credentialSyncMarker) {
|
|
10409
|
+
state.credentialSync = createCredentialSync({
|
|
10410
|
+
markerPath: options.credentialSyncMarker,
|
|
10411
|
+
env: process.env,
|
|
10412
|
+
log: (message, level = "info") => {
|
|
10413
|
+
if (level === "error") {
|
|
10414
|
+
logActivity(state, { type: "error", error: message });
|
|
10415
|
+
} else {
|
|
10416
|
+
logActivity(state, { type: "info", level, message });
|
|
10417
|
+
}
|
|
10418
|
+
}
|
|
10419
|
+
});
|
|
10420
|
+
}
|
|
9825
10421
|
if (fileSyncDirectories.length > 0) {
|
|
9826
10422
|
log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
|
|
9827
10423
|
} else {
|
|
@@ -9847,43 +10443,7 @@ async function run(options) {
|
|
|
9847
10443
|
"warn"
|
|
9848
10444
|
);
|
|
9849
10445
|
}
|
|
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
|
-
};
|
|
10446
|
+
const handleSignal = () => beginGracefulShutdown(state, "signal");
|
|
9887
10447
|
process.on("SIGINT", handleSignal);
|
|
9888
10448
|
process.on("SIGTERM", handleSignal);
|
|
9889
10449
|
try {
|
|
@@ -10029,6 +10589,7 @@ async function run(options) {
|
|
|
10029
10589
|
await restoreCredentialStores(credentialContext);
|
|
10030
10590
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
10031
10591
|
}
|
|
10592
|
+
state.credentialSync?.arm();
|
|
10032
10593
|
let sessionDbVerifyFatal = false;
|
|
10033
10594
|
if (!options.restoreSessionDb) {
|
|
10034
10595
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -10082,6 +10643,7 @@ async function run(options) {
|
|
|
10082
10643
|
for (const warning2 of maxActiveSessionsWarnings) {
|
|
10083
10644
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
10084
10645
|
}
|
|
10646
|
+
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
10085
10647
|
const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
|
|
10086
10648
|
try {
|
|
10087
10649
|
const oc = await ensureOpenCodeRunning({
|
|
@@ -10097,7 +10659,7 @@ async function run(options) {
|
|
|
10097
10659
|
state.opencodeVersion = oc.version;
|
|
10098
10660
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
10099
10661
|
try {
|
|
10100
|
-
|
|
10662
|
+
writeFileSync6(options.opencodePidFile, `${oc.process.pid}
|
|
10101
10663
|
`, { mode: 384 });
|
|
10102
10664
|
chmodSync3(options.opencodePidFile, 384);
|
|
10103
10665
|
} catch (error2) {
|
|
@@ -10107,6 +10669,23 @@ async function run(options) {
|
|
|
10107
10669
|
});
|
|
10108
10670
|
}
|
|
10109
10671
|
}
|
|
10672
|
+
if (state.opencodeVersion !== null) {
|
|
10673
|
+
const provenance = checkSessionDbProvenance({
|
|
10674
|
+
dbPath: sessionDbPath(),
|
|
10675
|
+
currentVersion: state.opencodeVersion,
|
|
10676
|
+
homeDir: homedir5(),
|
|
10677
|
+
env: process.env
|
|
10678
|
+
});
|
|
10679
|
+
if (provenance.anomaly) {
|
|
10680
|
+
state.sessionDbProvenanceAnomaly = true;
|
|
10681
|
+
logSessionDbProvenanceMismatch(
|
|
10682
|
+
state,
|
|
10683
|
+
provenance,
|
|
10684
|
+
state.opencodeVersion,
|
|
10685
|
+
preBootMigrationIds?.length ?? null
|
|
10686
|
+
);
|
|
10687
|
+
}
|
|
10688
|
+
}
|
|
10110
10689
|
state.opencodeConnected = oc.notReadyReason === null;
|
|
10111
10690
|
const version2 = state.opencodeVersion ? ` (v${state.opencodeVersion})` : "";
|
|
10112
10691
|
ocSpinner?.succeed(`OpenCode running on port ${state.port}${version2}`);
|
|
@@ -10159,7 +10738,7 @@ async function run(options) {
|
|
|
10159
10738
|
let existingPid;
|
|
10160
10739
|
if (existsSync3(options.litestreamPidFile)) {
|
|
10161
10740
|
try {
|
|
10162
|
-
const rawPid =
|
|
10741
|
+
const rawPid = readFileSync6(options.litestreamPidFile, "utf8").trim();
|
|
10163
10742
|
const parsedPid = Number(rawPid);
|
|
10164
10743
|
if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
|
|
10165
10744
|
existingPid = parsedPid;
|
|
@@ -10196,7 +10775,7 @@ async function run(options) {
|
|
|
10196
10775
|
});
|
|
10197
10776
|
try {
|
|
10198
10777
|
if (litestreamProcess.pid !== void 0) {
|
|
10199
|
-
|
|
10778
|
+
writeFileSync6(options.litestreamPidFile, `${litestreamProcess.pid}
|
|
10200
10779
|
`, {
|
|
10201
10780
|
mode: 384
|
|
10202
10781
|
});
|
|
@@ -10402,7 +10981,17 @@ async function run(options) {
|
|
|
10402
10981
|
setTimer: (timer) => {
|
|
10403
10982
|
state.openaiUsageTimer = timer;
|
|
10404
10983
|
},
|
|
10405
|
-
fetchUsage: () =>
|
|
10984
|
+
fetchUsage: async () => {
|
|
10985
|
+
const usage = await getOpenAiUsage(state.port);
|
|
10986
|
+
if (usage.subscription === null) {
|
|
10987
|
+
logActivity(state, {
|
|
10988
|
+
type: "info",
|
|
10989
|
+
level: "debug",
|
|
10990
|
+
message: "OpenAI usage subscription could not be identified from the local credential"
|
|
10991
|
+
});
|
|
10992
|
+
}
|
|
10993
|
+
return usage;
|
|
10994
|
+
},
|
|
10406
10995
|
report: (usage) => reportOpenAiUsage(state.agentId, state.authHeader, usage),
|
|
10407
10996
|
isLocalCredentialProblem: isLocalCredentialProblem2,
|
|
10408
10997
|
forcedOnHint: "connect a ChatGPT account to this runner, or run `opencode auth login`",
|
|
@@ -10448,7 +11037,7 @@ async function run(options) {
|
|
|
10448
11037
|
}
|
|
10449
11038
|
|
|
10450
11039
|
// src/index.ts
|
|
10451
|
-
var { version } =
|
|
11040
|
+
var { version } = createRequire2(import.meta.url)("../package.json");
|
|
10452
11041
|
var program = new Command();
|
|
10453
11042
|
program.name("evident").description("Run OpenCode locally and connect it to Evident").version(version).option(
|
|
10454
11043
|
"--endpoint <url>",
|
|
@@ -10526,6 +11115,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10526
11115
|
).option(
|
|
10527
11116
|
"--opencode-config-overlay <path>",
|
|
10528
11117
|
"Apply this runner-provided OpenCode config before starting OpenCode."
|
|
11118
|
+
).option(
|
|
11119
|
+
"--credential-sync-marker <path>",
|
|
11120
|
+
"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
11121
|
).action(
|
|
10530
11122
|
(options) => {
|
|
10531
11123
|
run({
|
|
@@ -10564,7 +11156,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10564
11156
|
sessionDbNoReplicateMarker: options.sessionDbNoReplicateMarker,
|
|
10565
11157
|
restoreSessionDb: options.restoreSessionDb,
|
|
10566
11158
|
restoreRunnerCredentials: options.restoreRunnerCredentials,
|
|
10567
|
-
opencodeConfigOverlay: options.opencodeConfigOverlay
|
|
11159
|
+
opencodeConfigOverlay: options.opencodeConfigOverlay,
|
|
11160
|
+
credentialSyncMarker: options.credentialSyncMarker
|
|
10568
11161
|
});
|
|
10569
11162
|
}
|
|
10570
11163
|
);
|