@evident-ai/cli 3.6.1-dev.d5a3d9d → 3.6.1-dev.d9505c3
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 +44 -7
- package/dist/index.js +2772 -716
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -723,6 +723,9 @@ async function reportResourceUsage(agentId, authHeader, usage) {
|
|
|
723
723
|
opencode_db_bytes: usage.opencodeDbBytes
|
|
724
724
|
});
|
|
725
725
|
}
|
|
726
|
+
async function reportSessionDeletions(agentId, authHeader, deletions) {
|
|
727
|
+
return postBestEffort(`/runners/${agentId}/session-deletions`, authHeader, { deletions });
|
|
728
|
+
}
|
|
726
729
|
async function getRunnerCredentialVault(agentId, authHeader) {
|
|
727
730
|
const empty = { prefix: null, vaultId: null, source: null };
|
|
728
731
|
try {
|
|
@@ -1176,9 +1179,9 @@ async function claudeUsage() {
|
|
|
1176
1179
|
}
|
|
1177
1180
|
|
|
1178
1181
|
// src/commands/run.ts
|
|
1179
|
-
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as
|
|
1182
|
+
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "node:fs";
|
|
1180
1183
|
import { homedir as homedir8 } from "node:os";
|
|
1181
|
-
import { isAbsolute as isAbsolute4, join as
|
|
1184
|
+
import { isAbsolute as isAbsolute4, join as join16, parse, resolve as resolvePath2 } from "node:path";
|
|
1182
1185
|
import chalk7 from "chalk";
|
|
1183
1186
|
|
|
1184
1187
|
// ../../packages/types/src/agents/index.ts
|
|
@@ -1442,7 +1445,7 @@ import ora4 from "ora";
|
|
|
1442
1445
|
import { select as select4 } from "@inquirer/prompts";
|
|
1443
1446
|
|
|
1444
1447
|
// src/lib/telemetry.ts
|
|
1445
|
-
var CLI_VERSION = (true ? "3.6.1-dev.
|
|
1448
|
+
var CLI_VERSION = (true ? "3.6.1-dev.d9505c3" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1446
1449
|
function getCliVersion() {
|
|
1447
1450
|
return CLI_VERSION;
|
|
1448
1451
|
}
|
|
@@ -2192,6 +2195,7 @@ var FORWARDED_LEVELS_BY_SOURCE = {
|
|
|
2192
2195
|
"cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
|
|
2193
2196
|
opencode: /* @__PURE__ */ new Set(["warn", "error"]),
|
|
2194
2197
|
"cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"]),
|
|
2198
|
+
"cli.session-archive": /* @__PURE__ */ new Set(["info", "warn", "error"]),
|
|
2195
2199
|
"cli.runtime-provenance": /* @__PURE__ */ new Set(["info", "warn", "error"])
|
|
2196
2200
|
};
|
|
2197
2201
|
var SEVERITY_BY_LEVEL = {
|
|
@@ -2638,256 +2642,1351 @@ function isSessionDbRecoveryRecord(value) {
|
|
|
2638
2642
|
);
|
|
2639
2643
|
}
|
|
2640
2644
|
|
|
2641
|
-
// src/lib/opencode/
|
|
2642
|
-
|
|
2643
|
-
|
|
2645
|
+
// src/lib/opencode/session-deletion-report.ts
|
|
2646
|
+
import { appendFileSync, mkdirSync, readFileSync as readFileSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
|
|
2647
|
+
import { dirname as dirname4, join as join4 } from "node:path";
|
|
2648
|
+
var SESSION_DELETION_SPOOL_MAX_RECORDS = 1e3;
|
|
2649
|
+
var MAX_DIAGNOSTIC_LENGTH = 200;
|
|
2650
|
+
function sessionDeletionReportPath(homeDir, env) {
|
|
2651
|
+
const override = env.EVIDENT_SESSION_DELETION_REPORT?.trim();
|
|
2652
|
+
return override || join4(homeDir, ".local", "state", "evident", "session-deletions.jsonl");
|
|
2653
|
+
}
|
|
2654
|
+
function appendSessionDeletionRecords(path, records) {
|
|
2655
|
+
if (records.length === 0) return 0;
|
|
2656
|
+
mkdirSync(dirname4(path), { recursive: true });
|
|
2657
|
+
const current = readSessionDeletionRecords(path);
|
|
2658
|
+
if (current.readError !== null) {
|
|
2659
|
+
throw new Error(`cannot enforce the spool limit after reading ${path}: ${current.readError}`);
|
|
2660
|
+
}
|
|
2661
|
+
const combined = [...current.records, ...records];
|
|
2662
|
+
const kept = combined.slice(-SESSION_DELETION_SPOOL_MAX_RECORDS);
|
|
2663
|
+
const dropped = combined.length - kept.length;
|
|
2664
|
+
if (dropped > 0) {
|
|
2665
|
+
writeFileSync(path, `${kept.map((record) => JSON.stringify(record)).join("\n")}
|
|
2666
|
+
`, "utf8");
|
|
2667
|
+
} else {
|
|
2668
|
+
appendFileSync(path, `${records.map((record) => JSON.stringify(record)).join("\n")}
|
|
2669
|
+
`, "utf8");
|
|
2670
|
+
}
|
|
2671
|
+
return dropped;
|
|
2644
2672
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
async function checkOpenCodeHealth(port) {
|
|
2673
|
+
function readSessionDeletionRecords(path) {
|
|
2674
|
+
let content;
|
|
2648
2675
|
try {
|
|
2649
|
-
|
|
2650
|
-
signal: AbortSignal.timeout(2e3)
|
|
2651
|
-
// 2 second timeout
|
|
2652
|
-
});
|
|
2653
|
-
if (response.status === 401) {
|
|
2654
|
-
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
2655
|
-
}
|
|
2656
|
-
if (!response.ok) {
|
|
2657
|
-
return { healthy: false, error: `HTTP ${response.status}` };
|
|
2658
|
-
}
|
|
2659
|
-
const data = await response.json().catch(() => ({}));
|
|
2660
|
-
return { healthy: true, version: data.version };
|
|
2676
|
+
content = readFileSync3(path, "utf8");
|
|
2661
2677
|
} catch (error2) {
|
|
2662
|
-
|
|
2663
|
-
|
|
2678
|
+
if (isMissingFile(error2)) return { records: [], skippedLines: 0, readError: null };
|
|
2679
|
+
const readError = boundedError(error2);
|
|
2680
|
+
console.error(`[session-deletion-report] could not read ${path}: ${readError}`);
|
|
2681
|
+
return { records: [], skippedLines: 0, readError };
|
|
2664
2682
|
}
|
|
2683
|
+
let skippedLines = 0;
|
|
2684
|
+
const records = content.split("\n").flatMap((line) => {
|
|
2685
|
+
if (!line.trim()) return [];
|
|
2686
|
+
const record = parseRecord(line, path);
|
|
2687
|
+
if (record) return [record];
|
|
2688
|
+
skippedLines++;
|
|
2689
|
+
return [];
|
|
2690
|
+
});
|
|
2691
|
+
return { records, skippedLines, readError: null };
|
|
2665
2692
|
}
|
|
2666
|
-
function
|
|
2667
|
-
|
|
2668
|
-
return excerpt ? ` (body="${excerpt}")` : " (empty body)";
|
|
2669
|
-
}
|
|
2670
|
-
async function checkOpenCode2Health(port, password) {
|
|
2693
|
+
function removeDeliveredSessionDeletionRecords(path, delivered) {
|
|
2694
|
+
let content;
|
|
2671
2695
|
try {
|
|
2672
|
-
|
|
2673
|
-
if (password !== void 0) {
|
|
2674
|
-
headers.Authorization = buildOpenCodeBasicAuthHeader(password);
|
|
2675
|
-
}
|
|
2676
|
-
const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
|
|
2677
|
-
headers,
|
|
2678
|
-
signal: AbortSignal.timeout(2e3)
|
|
2679
|
-
});
|
|
2680
|
-
if (response.status === 401) {
|
|
2681
|
-
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
2682
|
-
}
|
|
2683
|
-
if (!response.ok) {
|
|
2684
|
-
return { healthy: false, error: `HTTP ${response.status}` };
|
|
2685
|
-
}
|
|
2686
|
-
const body = await response.text();
|
|
2687
|
-
const excerpt = bodyExcerpt(body);
|
|
2688
|
-
let data;
|
|
2689
|
-
try {
|
|
2690
|
-
data = JSON.parse(body);
|
|
2691
|
-
} catch (error2) {
|
|
2692
|
-
return {
|
|
2693
|
-
healthy: false,
|
|
2694
|
-
error: `Invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2695
|
-
};
|
|
2696
|
-
}
|
|
2697
|
-
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
2698
|
-
return {
|
|
2699
|
-
healthy: false,
|
|
2700
|
-
error: `Invalid health response${excerpt}: expected healthy=true`
|
|
2701
|
-
};
|
|
2702
|
-
}
|
|
2703
|
-
const health = data;
|
|
2704
|
-
if (health.healthy !== true) {
|
|
2705
|
-
return {
|
|
2706
|
-
healthy: false,
|
|
2707
|
-
error: `Invalid health response${excerpt}: expected healthy=true`
|
|
2708
|
-
};
|
|
2709
|
-
}
|
|
2710
|
-
return typeof health.version === "string" ? { healthy: true, version: health.version } : { healthy: true };
|
|
2696
|
+
content = readFileSync3(path, "utf8");
|
|
2711
2697
|
} catch (error2) {
|
|
2712
|
-
|
|
2713
|
-
|
|
2698
|
+
if (isMissingFile(error2)) return;
|
|
2699
|
+
console.error(
|
|
2700
|
+
`[session-deletion-report] could not read ${path} before removing delivered records: ${boundedError(error2)}`
|
|
2701
|
+
);
|
|
2702
|
+
throw error2;
|
|
2714
2703
|
}
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
2719
|
-
const health = await checkOpenCodeHealth(port);
|
|
2720
|
-
if (health.healthy || health.authFailed) {
|
|
2721
|
-
return health;
|
|
2722
|
-
}
|
|
2723
|
-
await new Promise((resolve5) => setTimeout(resolve5, 1e3));
|
|
2704
|
+
const toRemove = /* @__PURE__ */ new Map();
|
|
2705
|
+
for (const record of delivered) {
|
|
2706
|
+
toRemove.set(record.opencode_session_id, (toRemove.get(record.opencode_session_id) ?? 0) + 1);
|
|
2724
2707
|
}
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2708
|
+
const remaining = [];
|
|
2709
|
+
for (const line of content.split("\n")) {
|
|
2710
|
+
if (!line.trim()) continue;
|
|
2711
|
+
const record = parseRecord(line, path);
|
|
2712
|
+
const count = record ? toRemove.get(record.opencode_session_id) ?? 0 : 0;
|
|
2713
|
+
if (record && count > 0) {
|
|
2714
|
+
if (count === 1) toRemove.delete(record.opencode_session_id);
|
|
2715
|
+
else toRemove.set(record.opencode_session_id, count - 1);
|
|
2716
|
+
} else {
|
|
2717
|
+
remaining.push(line);
|
|
2733
2718
|
}
|
|
2734
|
-
await new Promise((resolve5) => setTimeout(resolve5, 1e3));
|
|
2735
|
-
}
|
|
2736
|
-
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
2737
|
-
}
|
|
2738
|
-
|
|
2739
|
-
// src/lib/http-timeout.ts
|
|
2740
|
-
var REQUEST_TIMEOUT_MS = 6e4;
|
|
2741
|
-
var RequestTimeoutError = class extends Error {
|
|
2742
|
-
constructor(operation, timeoutMs, elapsedMs) {
|
|
2743
|
-
super(`${operation} timed out after ${elapsedMs}ms (limit ${timeoutMs}ms)`);
|
|
2744
|
-
this.operation = operation;
|
|
2745
|
-
this.timeoutMs = timeoutMs;
|
|
2746
|
-
this.elapsedMs = elapsedMs;
|
|
2747
|
-
this.name = "TimeoutError";
|
|
2748
2719
|
}
|
|
2749
|
-
};
|
|
2750
|
-
function describeRequestOperation(input, init) {
|
|
2751
|
-
const method = (init?.method ?? input.method ?? "GET").toUpperCase();
|
|
2752
|
-
const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
2753
|
-
let path;
|
|
2754
2720
|
try {
|
|
2755
|
-
|
|
2721
|
+
if (remaining.length === 0) unlinkSync2(path);
|
|
2722
|
+
else writeFileSync(path, `${remaining.join("\n")}
|
|
2723
|
+
`, "utf8");
|
|
2756
2724
|
} catch (error2) {
|
|
2757
|
-
if (
|
|
2758
|
-
|
|
2725
|
+
if (isMissingFile(error2)) return;
|
|
2726
|
+
console.error(
|
|
2727
|
+
`[session-deletion-report] could not write ${path} after removing delivered records: ${boundedError(error2)}`
|
|
2728
|
+
);
|
|
2729
|
+
throw error2;
|
|
2759
2730
|
}
|
|
2760
|
-
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2761
|
-
const prefixedIdentifier = /^[a-z]+_[A-Za-z0-9_-]{10,}$/;
|
|
2762
|
-
const maskedPath = path.split("/").map((segment) => uuid.test(segment) || prefixedIdentifier.test(segment) ? ":id" : segment).join("/");
|
|
2763
|
-
return `${method} ${maskedPath}`;
|
|
2764
|
-
}
|
|
2765
|
-
function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
2766
|
-
return (async (input, init) => {
|
|
2767
|
-
const startedAt = Date.now();
|
|
2768
|
-
try {
|
|
2769
|
-
return await fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
2770
|
-
} catch (error2) {
|
|
2771
|
-
if (error2?.name === "TimeoutError") {
|
|
2772
|
-
throw new RequestTimeoutError(
|
|
2773
|
-
describeRequestOperation(input, init),
|
|
2774
|
-
timeoutMs,
|
|
2775
|
-
Date.now() - startedAt
|
|
2776
|
-
);
|
|
2777
|
-
}
|
|
2778
|
-
throw error2;
|
|
2779
|
-
}
|
|
2780
|
-
});
|
|
2781
|
-
}
|
|
2782
|
-
|
|
2783
|
-
// src/lib/opencode/client.ts
|
|
2784
|
-
function redactPassword(message, password) {
|
|
2785
|
-
return message.replaceAll(password, "[redacted]");
|
|
2786
|
-
}
|
|
2787
|
-
function createOpenCodeClient(options) {
|
|
2788
|
-
const password = options.password ?? null;
|
|
2789
|
-
const fetchImpl = withRequestTimeout(options.fetchImpl ?? fetch, REQUEST_TIMEOUT_MS);
|
|
2790
|
-
const baseUrl = `http://127.0.0.1:${options.port}`;
|
|
2791
|
-
return {
|
|
2792
|
-
port: options.port,
|
|
2793
|
-
version: options.version,
|
|
2794
|
-
password,
|
|
2795
|
-
async request(path, init, requestOptions) {
|
|
2796
|
-
const requestInit = options.version === "v2" && password !== null ? (() => {
|
|
2797
|
-
const headers = new Headers(init?.headers);
|
|
2798
|
-
headers.set("Authorization", buildOpenCodeBasicAuthHeader(password));
|
|
2799
|
-
return { ...init, headers };
|
|
2800
|
-
})() : init;
|
|
2801
|
-
try {
|
|
2802
|
-
const response = await fetchImpl(`${baseUrl}${path}`, requestInit);
|
|
2803
|
-
if (!response.ok && !requestOptions?.allowStatuses?.includes(response.status)) {
|
|
2804
|
-
const body = await response.text();
|
|
2805
|
-
throw new Error(
|
|
2806
|
-
`OpenCode request failed: HTTP ${response.status}${body ? `: ${body}` : ""}`
|
|
2807
|
-
);
|
|
2808
|
-
}
|
|
2809
|
-
return response;
|
|
2810
|
-
} catch (error2) {
|
|
2811
|
-
if (options.version === "v2" && password !== null) {
|
|
2812
|
-
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2813
|
-
throw new Error(redactPassword(message, password));
|
|
2814
|
-
}
|
|
2815
|
-
throw error2;
|
|
2816
|
-
}
|
|
2817
|
-
}
|
|
2818
|
-
};
|
|
2819
|
-
}
|
|
2820
|
-
|
|
2821
|
-
// src/lib/opencode/session.ts
|
|
2822
|
-
var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
|
|
2823
|
-
function timedFetch(input, init) {
|
|
2824
|
-
return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
|
|
2825
|
-
}
|
|
2826
|
-
function requestWithClient(port, client, path, init, options) {
|
|
2827
|
-
return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
|
|
2828
|
-
}
|
|
2829
|
-
function opencodeBase(port) {
|
|
2830
|
-
return `http://127.0.0.1:${port}`;
|
|
2831
2731
|
}
|
|
2832
|
-
|
|
2732
|
+
function parseRecord(line, path) {
|
|
2833
2733
|
try {
|
|
2834
|
-
const
|
|
2835
|
-
if (
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
return dir && dir.trim() ? dir.trim() : null;
|
|
2734
|
+
const value = JSON.parse(line);
|
|
2735
|
+
if (isSessionDeletionRecord(value)) return value;
|
|
2736
|
+
console.error(`[session-deletion-report] skipped malformed record in ${path}: invalid shape`);
|
|
2737
|
+
return null;
|
|
2839
2738
|
} catch (error2) {
|
|
2840
2739
|
console.error(
|
|
2841
|
-
`[
|
|
2740
|
+
`[session-deletion-report] skipped malformed record in ${path}: ${boundedError(error2)}`
|
|
2842
2741
|
);
|
|
2843
2742
|
return null;
|
|
2844
2743
|
}
|
|
2845
2744
|
}
|
|
2846
|
-
function
|
|
2847
|
-
if (!
|
|
2848
|
-
|
|
2849
|
-
const
|
|
2850
|
-
|
|
2745
|
+
function isSessionDeletionRecord(value) {
|
|
2746
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2747
|
+
const record = value;
|
|
2748
|
+
const policy = record.policy;
|
|
2749
|
+
if (!policy || typeof policy !== "object" || Array.isArray(policy)) return false;
|
|
2750
|
+
const fields = policy;
|
|
2751
|
+
return typeof record.opencode_session_id === "string" && record.opencode_session_id.length > 0 && typeof record.deleted_at === "string" && record.reason === "auto_cleanup" && (fields.max_age_ms === null || typeof fields.max_age_ms === "number") && (fields.max_count === null || typeof fields.max_count === "number") && (fields.last_activity_at === null || typeof fields.last_activity_at === "string");
|
|
2851
2752
|
}
|
|
2852
|
-
function
|
|
2853
|
-
|
|
2854
|
-
return m.info?.time?.completed ?? m.time?.completed;
|
|
2753
|
+
function isMissingFile(error2) {
|
|
2754
|
+
return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
|
|
2855
2755
|
}
|
|
2856
|
-
function
|
|
2857
|
-
|
|
2858
|
-
return
|
|
2756
|
+
function boundedError(error2) {
|
|
2757
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2758
|
+
return message.slice(0, MAX_DIAGNOSTIC_LENGTH);
|
|
2859
2759
|
}
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2760
|
+
|
|
2761
|
+
// src/lib/opencode/session-binding-ledger.ts
|
|
2762
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2763
|
+
import { dirname as dirname5, join as join5 } from "node:path";
|
|
2764
|
+
var SESSION_BINDING_LEDGER_MAX_RECORDS = 1e3;
|
|
2765
|
+
var MAX_DIAGNOSTIC_LENGTH2 = 200;
|
|
2766
|
+
function sessionBindingLedgerPath(homeDir, env) {
|
|
2767
|
+
const override = env.EVIDENT_SESSION_BINDING_LEDGER?.trim();
|
|
2768
|
+
return override || join5(homeDir, ".local", "state", "evident", "session-bindings.jsonl");
|
|
2769
|
+
}
|
|
2770
|
+
function appendSessionBinding(path, record, log3) {
|
|
2771
|
+
mkdirSync2(dirname5(path), { recursive: true });
|
|
2772
|
+
const current = readSessionBindingRecords(path, log3);
|
|
2773
|
+
if (current.readError !== null) {
|
|
2774
|
+
throw new Error(`cannot enforce the ledger limit after reading ${path}: ${current.readError}`);
|
|
2775
|
+
}
|
|
2776
|
+
if (current.records.some(
|
|
2777
|
+
(existing) => existing.conversation_id === record.conversation_id && existing.opencode_session_id === record.opencode_session_id
|
|
2778
|
+
)) {
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
const kept = [...current.records, record].slice(-SESSION_BINDING_LEDGER_MAX_RECORDS);
|
|
2782
|
+
if (kept.length > current.records.length) {
|
|
2783
|
+
appendFileSync2(path, `${JSON.stringify(record)}
|
|
2784
|
+
`, "utf8");
|
|
2785
|
+
} else {
|
|
2786
|
+
writeFileSync2(path, `${kept.map((entry) => JSON.stringify(entry)).join("\n")}
|
|
2787
|
+
`, "utf8");
|
|
2788
|
+
}
|
|
2865
2789
|
}
|
|
2866
|
-
function
|
|
2867
|
-
|
|
2868
|
-
if (
|
|
2869
|
-
|
|
2870
|
-
|
|
2790
|
+
function readSessionBindings(path, log3) {
|
|
2791
|
+
const result = readSessionBindingRecords(path, log3);
|
|
2792
|
+
if (result.readError !== null) {
|
|
2793
|
+
log3(`[session-binding-ledger] could not read ${path}: ${result.readError}`);
|
|
2794
|
+
}
|
|
2795
|
+
return new Map(
|
|
2796
|
+
result.records.map((record) => [record.opencode_session_id, record.conversation_id])
|
|
2797
|
+
);
|
|
2871
2798
|
}
|
|
2872
|
-
function
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2799
|
+
function readSessionBindingRecords(path, log3) {
|
|
2800
|
+
let content;
|
|
2801
|
+
try {
|
|
2802
|
+
content = readFileSync4(path, "utf8");
|
|
2803
|
+
} catch (error2) {
|
|
2804
|
+
if (isMissingFile2(error2)) return { records: [], readError: null };
|
|
2805
|
+
const readError = boundedError2(error2);
|
|
2806
|
+
return { records: [], readError };
|
|
2807
|
+
}
|
|
2808
|
+
const records = content.split("\n").flatMap((line) => {
|
|
2809
|
+
if (!line.trim()) return [];
|
|
2810
|
+
const record = parseRecord2(line, path, log3);
|
|
2811
|
+
return record ? [record] : [];
|
|
2812
|
+
});
|
|
2813
|
+
return { records, readError: null };
|
|
2877
2814
|
}
|
|
2878
|
-
function
|
|
2879
|
-
|
|
2880
|
-
|
|
2815
|
+
function parseRecord2(line, path, log3) {
|
|
2816
|
+
try {
|
|
2817
|
+
const value = JSON.parse(line);
|
|
2818
|
+
if (isSessionBindingRecord(value)) return value;
|
|
2819
|
+
log3(`[session-binding-ledger] skipped malformed record in ${path}: invalid shape`);
|
|
2820
|
+
} catch (error2) {
|
|
2821
|
+
log3(`[session-binding-ledger] skipped malformed record in ${path}: ${boundedError2(error2)}`);
|
|
2822
|
+
}
|
|
2823
|
+
return null;
|
|
2881
2824
|
}
|
|
2882
|
-
function
|
|
2883
|
-
|
|
2825
|
+
function isSessionBindingRecord(value) {
|
|
2826
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2827
|
+
const record = value;
|
|
2828
|
+
return typeof record.conversation_id === "string" && record.conversation_id.length > 0 && typeof record.opencode_session_id === "string" && record.opencode_session_id.length > 0 && typeof record.observed_at === "string";
|
|
2884
2829
|
}
|
|
2885
|
-
function
|
|
2886
|
-
|
|
2887
|
-
if (errorOf(m) != null) return false;
|
|
2888
|
-
return finishOf(m) === "tool-calls";
|
|
2830
|
+
function isMissingFile2(error2) {
|
|
2831
|
+
return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
|
|
2889
2832
|
}
|
|
2890
|
-
|
|
2833
|
+
function boundedError2(error2) {
|
|
2834
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2835
|
+
return message.slice(0, MAX_DIAGNOSTIC_LENGTH2);
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
// src/lib/opencode/session-archive-config.ts
|
|
2839
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2840
|
+
|
|
2841
|
+
// src/lib/opencode/session-cleanup.ts
|
|
2842
|
+
var DURATION_UNIT_MS = {
|
|
2843
|
+
s: 1e3,
|
|
2844
|
+
m: 60 * 1e3,
|
|
2845
|
+
h: 60 * 60 * 1e3,
|
|
2846
|
+
d: 24 * 60 * 60 * 1e3
|
|
2847
|
+
};
|
|
2848
|
+
function parseDurationMs(input) {
|
|
2849
|
+
const trimmed = input.trim();
|
|
2850
|
+
const match = /^(\d+)([smhd])$/.exec(trimmed);
|
|
2851
|
+
if (!match) {
|
|
2852
|
+
throw new Error(
|
|
2853
|
+
`Invalid duration "${input}": expected <number><unit> where unit is one of s, m, h, d (e.g. "7d", "24h", "30m", "90s").`
|
|
2854
|
+
);
|
|
2855
|
+
}
|
|
2856
|
+
const value = Number(match[1]);
|
|
2857
|
+
if (value <= 0) {
|
|
2858
|
+
throw new Error(`Invalid duration "${input}": must be a positive value.`);
|
|
2859
|
+
}
|
|
2860
|
+
return value * DURATION_UNIT_MS[match[2]];
|
|
2861
|
+
}
|
|
2862
|
+
function selectSessionsToDelete(sessions, opts) {
|
|
2863
|
+
const { maxAgeMs, maxCount, nowMs, protectedIds } = opts;
|
|
2864
|
+
if (maxAgeMs === void 0 && maxCount === void 0) return [];
|
|
2865
|
+
const ageEligible = (s) => {
|
|
2866
|
+
if (maxAgeMs === void 0) return false;
|
|
2867
|
+
if (s.lastActivityMs === null) return true;
|
|
2868
|
+
return nowMs - s.lastActivityMs > maxAgeMs;
|
|
2869
|
+
};
|
|
2870
|
+
const countEligibleIds = /* @__PURE__ */ new Set();
|
|
2871
|
+
if (maxCount !== void 0) {
|
|
2872
|
+
const byActivityDesc = [...sessions].sort(
|
|
2873
|
+
(a, b) => (b.lastActivityMs ?? -Infinity) - (a.lastActivityMs ?? -Infinity)
|
|
2874
|
+
);
|
|
2875
|
+
for (const s of byActivityDesc.slice(maxCount)) {
|
|
2876
|
+
countEligibleIds.add(s.id);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
const toDelete = [];
|
|
2880
|
+
for (const s of sessions) {
|
|
2881
|
+
if (protectedIds.has(s.id)) continue;
|
|
2882
|
+
if (ageEligible(s) || countEligibleIds.has(s.id)) {
|
|
2883
|
+
toDelete.push(s.id);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
|
|
2887
|
+
return toDelete.sort(
|
|
2888
|
+
(a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2891
|
+
var DEFAULT_INTERVAL = "1h";
|
|
2892
|
+
function resolve2(flag, envValue, fallback) {
|
|
2893
|
+
return flag ?? envValue ?? fallback;
|
|
2894
|
+
}
|
|
2895
|
+
function parseMaxCount(input) {
|
|
2896
|
+
const trimmed = input.trim();
|
|
2897
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
2898
|
+
throw new Error(`Invalid max-count "${input}": expected a positive integer.`);
|
|
2899
|
+
}
|
|
2900
|
+
const value = Number(trimmed);
|
|
2901
|
+
if (value <= 0) {
|
|
2902
|
+
throw new Error(`Invalid max-count "${input}": must be greater than 0.`);
|
|
2903
|
+
}
|
|
2904
|
+
return value;
|
|
2905
|
+
}
|
|
2906
|
+
function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
2907
|
+
const warnings = [];
|
|
2908
|
+
const maxAgeRaw = resolve2(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
|
|
2909
|
+
const maxCountRaw = resolve2(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
|
|
2910
|
+
const intervalRaw = resolve2(
|
|
2911
|
+
flags.interval,
|
|
2912
|
+
env.EVIDENT_SESSION_CLEANUP_INTERVAL,
|
|
2913
|
+
DEFAULT_INTERVAL
|
|
2914
|
+
);
|
|
2915
|
+
let maxAgeMs;
|
|
2916
|
+
if (maxAgeRaw !== void 0) {
|
|
2917
|
+
try {
|
|
2918
|
+
maxAgeMs = parseDurationMs(maxAgeRaw);
|
|
2919
|
+
} catch (err) {
|
|
2920
|
+
warnings.push(
|
|
2921
|
+
`Ignoring invalid --session-cleanup-max-age: ${err instanceof Error ? err.message : String(err)}`
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
let maxCount;
|
|
2926
|
+
if (maxCountRaw !== void 0) {
|
|
2927
|
+
try {
|
|
2928
|
+
maxCount = parseMaxCount(maxCountRaw);
|
|
2929
|
+
} catch (err) {
|
|
2930
|
+
warnings.push(
|
|
2931
|
+
`Ignoring invalid --session-cleanup-max-count: ${err instanceof Error ? err.message : String(err)}`
|
|
2932
|
+
);
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
let intervalMs;
|
|
2936
|
+
try {
|
|
2937
|
+
intervalMs = parseDurationMs(intervalRaw ?? DEFAULT_INTERVAL);
|
|
2938
|
+
} catch (err) {
|
|
2939
|
+
warnings.push(
|
|
2940
|
+
`Ignoring invalid --session-cleanup-interval, using default ${DEFAULT_INTERVAL}: ${err instanceof Error ? err.message : String(err)}`
|
|
2941
|
+
);
|
|
2942
|
+
intervalMs = parseDurationMs(DEFAULT_INTERVAL);
|
|
2943
|
+
}
|
|
2944
|
+
const enabled = maxAgeMs !== void 0 || maxCount !== void 0;
|
|
2945
|
+
return { enabled, maxAgeMs, maxCount, intervalMs, warnings };
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
// src/lib/opencode/session-archive-config.ts
|
|
2949
|
+
var DEFAULT_INTERVAL2 = "1h";
|
|
2950
|
+
var DEFAULT_FLUSH_BUDGET_MS = 2e3;
|
|
2951
|
+
var MAX_SESSION_ARCHIVE_TIMER_DELAY_MS = 2147483647;
|
|
2952
|
+
var STATE_PREFIX_FILE = "/dev/shm/evident-state-prefix";
|
|
2953
|
+
var ENABLED_VALUES = /* @__PURE__ */ new Set(["on", "true", "1"]);
|
|
2954
|
+
var DISABLED_VALUES = /* @__PURE__ */ new Set(["off", "false", "0"]);
|
|
2955
|
+
function nonEmpty(value) {
|
|
2956
|
+
const trimmed = value?.trim();
|
|
2957
|
+
return trimmed ? trimmed : null;
|
|
2958
|
+
}
|
|
2959
|
+
function parseFlushBudgetMs(value) {
|
|
2960
|
+
const trimmed = value.trim();
|
|
2961
|
+
const budgetMs = Number(trimmed);
|
|
2962
|
+
if (!/^\d+$/.test(trimmed) || !Number.isSafeInteger(budgetMs) || budgetMs <= 0 || budgetMs > MAX_SESSION_ARCHIVE_TIMER_DELAY_MS) {
|
|
2963
|
+
throw new Error(
|
|
2964
|
+
`Invalid flush budget "${value}": expected a positive integer number of milliseconds no greater than ${MAX_SESSION_ARCHIVE_TIMER_DELAY_MS}.`
|
|
2965
|
+
);
|
|
2966
|
+
}
|
|
2967
|
+
return budgetMs;
|
|
2968
|
+
}
|
|
2969
|
+
function resolveSessionArchiveConfig(flags, env = process.env) {
|
|
2970
|
+
const warnings = [];
|
|
2971
|
+
const archiveRaw = env.EVIDENT_SESSION_ARCHIVE;
|
|
2972
|
+
let enabled = flags.enabled ?? false;
|
|
2973
|
+
if (flags.enabled === void 0 && archiveRaw !== void 0 && archiveRaw.trim() !== "") {
|
|
2974
|
+
const normalized = archiveRaw.trim().toLowerCase();
|
|
2975
|
+
if (ENABLED_VALUES.has(normalized)) {
|
|
2976
|
+
enabled = true;
|
|
2977
|
+
} else if (DISABLED_VALUES.has(normalized)) {
|
|
2978
|
+
enabled = false;
|
|
2979
|
+
} else {
|
|
2980
|
+
warnings.push(
|
|
2981
|
+
`Ignoring invalid EVIDENT_SESSION_ARCHIVE "${archiveRaw}": expected on or off; session archiving remains off.`
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
let intervalMs;
|
|
2986
|
+
const intervalRaw = flags.interval ?? env.EVIDENT_SESSION_ARCHIVE_INTERVAL ?? DEFAULT_INTERVAL2;
|
|
2987
|
+
try {
|
|
2988
|
+
intervalMs = parseDurationMs(intervalRaw);
|
|
2989
|
+
if (intervalMs > MAX_SESSION_ARCHIVE_TIMER_DELAY_MS) {
|
|
2990
|
+
throw new Error(
|
|
2991
|
+
`Duration "${intervalRaw}" exceeds Node's maximum timer delay of ${MAX_SESSION_ARCHIVE_TIMER_DELAY_MS}ms.`
|
|
2992
|
+
);
|
|
2993
|
+
}
|
|
2994
|
+
} catch (err) {
|
|
2995
|
+
warnings.push(
|
|
2996
|
+
`Ignoring invalid session archive interval, using default ${DEFAULT_INTERVAL2}: ${err instanceof Error ? err.message : String(err)}`
|
|
2997
|
+
);
|
|
2998
|
+
intervalMs = parseDurationMs(DEFAULT_INTERVAL2);
|
|
2999
|
+
}
|
|
3000
|
+
let flushBudgetMs;
|
|
3001
|
+
const flushBudgetRaw = env.EVIDENT_SESSION_ARCHIVE_FLUSH_MS ?? String(DEFAULT_FLUSH_BUDGET_MS);
|
|
3002
|
+
try {
|
|
3003
|
+
flushBudgetMs = parseFlushBudgetMs(flushBudgetRaw);
|
|
3004
|
+
} catch (err) {
|
|
3005
|
+
warnings.push(
|
|
3006
|
+
`Ignoring invalid EVIDENT_SESSION_ARCHIVE_FLUSH_MS, using default ${DEFAULT_FLUSH_BUDGET_MS}: ${err instanceof Error ? err.message : String(err)}`
|
|
3007
|
+
);
|
|
3008
|
+
flushBudgetMs = DEFAULT_FLUSH_BUDGET_MS;
|
|
3009
|
+
}
|
|
3010
|
+
const bucket = nonEmpty(env.PERSISTENCE_BUCKET);
|
|
3011
|
+
let keyPrefix = nonEmpty(env.LITESTREAM_PREFIX);
|
|
3012
|
+
if (enabled && bucket === null) {
|
|
3013
|
+
warnings.push("Session archiving is off because PERSISTENCE_BUCKET is unset or empty.");
|
|
3014
|
+
enabled = false;
|
|
3015
|
+
}
|
|
3016
|
+
if (enabled && keyPrefix === null) {
|
|
3017
|
+
let fallbackReadError = null;
|
|
3018
|
+
try {
|
|
3019
|
+
keyPrefix = nonEmpty(readFileSync5(STATE_PREFIX_FILE, "utf8"));
|
|
3020
|
+
} catch (err) {
|
|
3021
|
+
fallbackReadError = err instanceof Error ? err.message : String(err);
|
|
3022
|
+
}
|
|
3023
|
+
if (keyPrefix === null) {
|
|
3024
|
+
const readFailure = fallbackReadError ? ` Could not read ${STATE_PREFIX_FILE}: ${fallbackReadError}.` : "";
|
|
3025
|
+
warnings.push(
|
|
3026
|
+
`Session archiving is off because neither LITESTREAM_PREFIX nor ${STATE_PREFIX_FILE} contains a prefix.${readFailure}`
|
|
3027
|
+
);
|
|
3028
|
+
enabled = false;
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
return { enabled, bucket, keyPrefix, intervalMs, flushBudgetMs, warnings };
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
// src/lib/opencode/session-archive-snapshot.ts
|
|
3035
|
+
import { createHash } from "node:crypto";
|
|
3036
|
+
var SessionArchiveExportError = class extends Error {
|
|
3037
|
+
constructor(code, message) {
|
|
3038
|
+
super(message);
|
|
3039
|
+
this.code = code;
|
|
3040
|
+
this.name = "SessionArchiveExportError";
|
|
3041
|
+
}
|
|
3042
|
+
};
|
|
3043
|
+
function canonicalize(value) {
|
|
3044
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
3045
|
+
if (typeof value !== "object" || value === null) return value;
|
|
3046
|
+
return Object.fromEntries(
|
|
3047
|
+
Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, nested]) => [key, canonicalize(nested)])
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
3050
|
+
function contentForFingerprint(archive) {
|
|
3051
|
+
return JSON.stringify(
|
|
3052
|
+
canonicalize({
|
|
3053
|
+
opencode_api_version: archive.opencode_api_version,
|
|
3054
|
+
opencode_session_id: archive.opencode_session_id,
|
|
3055
|
+
evident_conversation_id: archive.evident_conversation_id,
|
|
3056
|
+
messages: archive.messages
|
|
3057
|
+
})
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
function fingerprintArchive(archive) {
|
|
3061
|
+
return createHash("sha256").update(contentForFingerprint(archive)).digest("hex");
|
|
3062
|
+
}
|
|
3063
|
+
function buildSessionArchive(input) {
|
|
3064
|
+
const archive = {
|
|
3065
|
+
schema_version: 1,
|
|
3066
|
+
opencode_api_version: input.apiVersion,
|
|
3067
|
+
opencode_session_id: input.session.id,
|
|
3068
|
+
evident_conversation_id: input.conversationId,
|
|
3069
|
+
evident_runner_id: input.runnerId,
|
|
3070
|
+
binding_source: input.bindingSource,
|
|
3071
|
+
session: input.session,
|
|
3072
|
+
messages: input.messages,
|
|
3073
|
+
message_count: input.messages.length,
|
|
3074
|
+
archived_at: input.archivedAt
|
|
3075
|
+
};
|
|
3076
|
+
return { ...archive, content_fingerprint: fingerprintArchive(archive) };
|
|
3077
|
+
}
|
|
3078
|
+
function isRecord(value) {
|
|
3079
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3080
|
+
}
|
|
3081
|
+
function errorMessage2(error2) {
|
|
3082
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
3083
|
+
}
|
|
3084
|
+
function invalidResponse(version2, sessionId, path, reason) {
|
|
3085
|
+
return new SessionArchiveExportError(
|
|
3086
|
+
"invalid-response",
|
|
3087
|
+
`OpenCode ${version2} session ${sessionId} returned an invalid response from GET ${path}: ${reason}`
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
async function requestPage(client, sessionId, path, signal) {
|
|
3091
|
+
let response;
|
|
3092
|
+
try {
|
|
3093
|
+
response = await client.request(path, signal ? { signal } : void 0, {
|
|
3094
|
+
allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100)
|
|
3095
|
+
});
|
|
3096
|
+
} catch (error2) {
|
|
3097
|
+
throw new SessionArchiveExportError(
|
|
3098
|
+
"request-failed",
|
|
3099
|
+
`OpenCode ${client.version} session ${sessionId} request GET ${path} failed: ${errorMessage2(error2)}`
|
|
3100
|
+
);
|
|
3101
|
+
}
|
|
3102
|
+
if (response.status === 404) {
|
|
3103
|
+
throw new SessionArchiveExportError(
|
|
3104
|
+
"not-found",
|
|
3105
|
+
`OpenCode ${client.version} session ${sessionId} was not found at GET ${path} (HTTP 404)`
|
|
3106
|
+
);
|
|
3107
|
+
}
|
|
3108
|
+
if (!response.ok) {
|
|
3109
|
+
throw new SessionArchiveExportError(
|
|
3110
|
+
"request-failed",
|
|
3111
|
+
`OpenCode ${client.version} session ${sessionId} request GET ${path} returned HTTP ${response.status}`
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
let bodyText;
|
|
3115
|
+
try {
|
|
3116
|
+
bodyText = await response.text();
|
|
3117
|
+
} catch (error2) {
|
|
3118
|
+
throw new SessionArchiveExportError(
|
|
3119
|
+
"request-failed",
|
|
3120
|
+
`OpenCode ${client.version} session ${sessionId} response body from GET ${path} could not be read: ${errorMessage2(error2)}`
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
try {
|
|
3124
|
+
return { response, body: JSON.parse(bodyText) };
|
|
3125
|
+
} catch (error2) {
|
|
3126
|
+
if (!(error2 instanceof SyntaxError)) throw error2;
|
|
3127
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
3128
|
+
const looksLikeHtml = contentType.toLowerCase().includes("text/html") || /^\s*<!doctype\s+html|^\s*<html/i.test(bodyText);
|
|
3129
|
+
throw invalidResponse(
|
|
3130
|
+
client.version,
|
|
3131
|
+
sessionId,
|
|
3132
|
+
path,
|
|
3133
|
+
looksLikeHtml ? "received text/html, expected JSON" : "received non-JSON content, expected JSON"
|
|
3134
|
+
);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
function v1NextCursor(response, sessionId, path) {
|
|
3138
|
+
const headerCursor = response.headers.get("x-next-cursor")?.trim() || null;
|
|
3139
|
+
const linkHeader = response.headers.get("link");
|
|
3140
|
+
const nextLink = linkHeader?.split(",").map((link) => link.trim()).find((link) => /;\s*rel\s*=\s*"?next"?/i.test(link));
|
|
3141
|
+
let linkCursor = null;
|
|
3142
|
+
if (nextLink) {
|
|
3143
|
+
const target = nextLink.match(/<([^>]+)>/)?.[1];
|
|
3144
|
+
if (!target) {
|
|
3145
|
+
throw new SessionArchiveExportError(
|
|
3146
|
+
"pagination-failed",
|
|
3147
|
+
`OpenCode v1 session ${sessionId} returned a next Link without a URL for GET ${path}`
|
|
3148
|
+
);
|
|
3149
|
+
}
|
|
3150
|
+
try {
|
|
3151
|
+
linkCursor = new URL(target, "http://127.0.0.1").searchParams.get("before");
|
|
3152
|
+
} catch (error2) {
|
|
3153
|
+
throw new SessionArchiveExportError(
|
|
3154
|
+
"pagination-failed",
|
|
3155
|
+
`OpenCode v1 session ${sessionId} returned an invalid next Link for GET ${path}: ${errorMessage2(error2)}`
|
|
3156
|
+
);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
if (headerCursor && linkCursor && headerCursor !== linkCursor) {
|
|
3160
|
+
throw new SessionArchiveExportError(
|
|
3161
|
+
"pagination-failed",
|
|
3162
|
+
`OpenCode v1 session ${sessionId} returned conflicting next cursors for GET ${path}`
|
|
3163
|
+
);
|
|
3164
|
+
}
|
|
3165
|
+
const cursor = headerCursor ?? linkCursor;
|
|
3166
|
+
if ((headerCursor || nextLink) && !cursor) {
|
|
3167
|
+
throw new SessionArchiveExportError(
|
|
3168
|
+
"pagination-failed",
|
|
3169
|
+
`OpenCode v1 session ${sessionId} returned a next page without a usable cursor for GET ${path}`
|
|
3170
|
+
);
|
|
3171
|
+
}
|
|
3172
|
+
return cursor;
|
|
3173
|
+
}
|
|
3174
|
+
function v1Messages(body, sessionId, path) {
|
|
3175
|
+
if (!Array.isArray(body)) {
|
|
3176
|
+
throw invalidResponse("v1", sessionId, path, "expected an array of messages");
|
|
3177
|
+
}
|
|
3178
|
+
return body;
|
|
3179
|
+
}
|
|
3180
|
+
async function exportV1SessionMessages(client, sessionId, signal) {
|
|
3181
|
+
const basePath = `/session/${encodeURIComponent(sessionId)}/message`;
|
|
3182
|
+
const firstPage = await requestPage(client, sessionId, basePath, signal);
|
|
3183
|
+
let messages = v1Messages(firstPage.body, sessionId, basePath);
|
|
3184
|
+
let cursor = v1NextCursor(firstPage.response, sessionId, basePath);
|
|
3185
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
3186
|
+
while (cursor !== null) {
|
|
3187
|
+
if (seenCursors.has(cursor)) {
|
|
3188
|
+
throw new SessionArchiveExportError(
|
|
3189
|
+
"pagination-failed",
|
|
3190
|
+
`OpenCode v1 session ${sessionId} repeated cursor while reading GET ${basePath}`
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
seenCursors.add(cursor);
|
|
3194
|
+
const path = `${basePath}?${new URLSearchParams({ before: cursor }).toString()}`;
|
|
3195
|
+
const page = await requestPage(client, sessionId, path, signal);
|
|
3196
|
+
messages = [...v1Messages(page.body, sessionId, path), ...messages];
|
|
3197
|
+
cursor = v1NextCursor(page.response, sessionId, path);
|
|
3198
|
+
}
|
|
3199
|
+
return messages;
|
|
3200
|
+
}
|
|
3201
|
+
async function exportV2SessionMessages(client, sessionId, signal) {
|
|
3202
|
+
const basePath = `/api/session/${encodeURIComponent(sessionId)}/message`;
|
|
3203
|
+
const messages = [];
|
|
3204
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
3205
|
+
let cursor;
|
|
3206
|
+
let hasNextPage = true;
|
|
3207
|
+
while (hasNextPage) {
|
|
3208
|
+
const params = new URLSearchParams({ limit: "100" });
|
|
3209
|
+
if (cursor === void 0) {
|
|
3210
|
+
params.set("order", "asc");
|
|
3211
|
+
} else {
|
|
3212
|
+
params.set("cursor", cursor);
|
|
3213
|
+
}
|
|
3214
|
+
const path = `${basePath}?${params.toString()}`;
|
|
3215
|
+
const { body } = await requestPage(client, sessionId, path, signal);
|
|
3216
|
+
if (!isRecord(body) || !Array.isArray(body.data) || !isRecord(body.cursor)) {
|
|
3217
|
+
throw invalidResponse("v2", sessionId, path, "expected a {data: [], cursor: {\u2026}} envelope");
|
|
3218
|
+
}
|
|
3219
|
+
messages.push(...body.data);
|
|
3220
|
+
const next = body.cursor.next;
|
|
3221
|
+
if (next === void 0 || next === null) {
|
|
3222
|
+
hasNextPage = false;
|
|
3223
|
+
continue;
|
|
3224
|
+
}
|
|
3225
|
+
if (typeof next !== "string" || next.length === 0 || seenCursors.has(next)) {
|
|
3226
|
+
throw new SessionArchiveExportError(
|
|
3227
|
+
"pagination-failed",
|
|
3228
|
+
`OpenCode v2 session ${sessionId} returned an invalid or repeated cursor from GET ${path}`
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
seenCursors.add(next);
|
|
3232
|
+
cursor = next;
|
|
3233
|
+
}
|
|
3234
|
+
return messages;
|
|
3235
|
+
}
|
|
3236
|
+
async function exportSessionMessages(client, sessionId, signal) {
|
|
3237
|
+
if (client.version === "v1") return exportV1SessionMessages(client, sessionId, signal);
|
|
3238
|
+
if (client.version === "v2") return exportV2SessionMessages(client, sessionId, signal);
|
|
3239
|
+
throw new SessionArchiveExportError(
|
|
3240
|
+
"unsupported-dialect",
|
|
3241
|
+
`OpenCode session ${sessionId} cannot be archived: unsupported API dialect ${String(client.version)}`
|
|
3242
|
+
);
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
// src/lib/opencode/session-archive-watermarks.ts
|
|
3246
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3247
|
+
import { dirname as dirname6, join as join6 } from "node:path";
|
|
3248
|
+
function sessionArchiveWatermarkPath(homeDir, env) {
|
|
3249
|
+
const override = env.EVIDENT_SESSION_ARCHIVE_WATERMARK?.trim();
|
|
3250
|
+
return override || join6(homeDir, ".local", "state", "evident", "session-archive-watermarks.json");
|
|
3251
|
+
}
|
|
3252
|
+
function readWatermarks(path, log3) {
|
|
3253
|
+
let content;
|
|
3254
|
+
try {
|
|
3255
|
+
content = readFileSync6(path, "utf8");
|
|
3256
|
+
} catch (error2) {
|
|
3257
|
+
if (isMissingFile3(error2)) return /* @__PURE__ */ new Map();
|
|
3258
|
+
log3(`[session-archive-watermarks] could not read ${path}; treating every session as changed`);
|
|
3259
|
+
return /* @__PURE__ */ new Map();
|
|
3260
|
+
}
|
|
3261
|
+
try {
|
|
3262
|
+
const parsed = JSON.parse(content);
|
|
3263
|
+
if (!isRecord2(parsed)) throw new Error("invalid watermark object");
|
|
3264
|
+
const entries = Object.entries(parsed);
|
|
3265
|
+
if (!entries.every(([sessionId, value]) => sessionId.length > 0 && isWatermark(value))) {
|
|
3266
|
+
throw new Error("invalid watermark entry");
|
|
3267
|
+
}
|
|
3268
|
+
return new Map(entries);
|
|
3269
|
+
} catch (error2) {
|
|
3270
|
+
log3(`[session-archive-watermarks] could not parse ${path}; treating every session as changed`);
|
|
3271
|
+
return /* @__PURE__ */ new Map();
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
function recordUpload(path, sessionId, entry, watermarks) {
|
|
3275
|
+
const updated = new Map(watermarks);
|
|
3276
|
+
updated.set(sessionId, entry);
|
|
3277
|
+
mkdirSync3(dirname6(path), { recursive: true });
|
|
3278
|
+
writeFileSync3(path, `${JSON.stringify(Object.fromEntries(updated))}
|
|
3279
|
+
`, {
|
|
3280
|
+
encoding: "utf8",
|
|
3281
|
+
mode: 384
|
|
3282
|
+
});
|
|
3283
|
+
watermarks.clear();
|
|
3284
|
+
for (const [id, watermark] of updated) watermarks.set(id, watermark);
|
|
3285
|
+
}
|
|
3286
|
+
function isRecord2(value) {
|
|
3287
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3288
|
+
}
|
|
3289
|
+
function isWatermark(value) {
|
|
3290
|
+
if (!isRecord2(value)) return false;
|
|
3291
|
+
return typeof value.fingerprint === "string" && typeof value.uploaded_at === "string" && typeof value.key === "string";
|
|
3292
|
+
}
|
|
3293
|
+
function isMissingFile3(error2) {
|
|
3294
|
+
return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
// src/lib/runner-synchroniser.ts
|
|
3298
|
+
import { spawn } from "node:child_process";
|
|
3299
|
+
|
|
3300
|
+
// ../../runner/synchroniser/src/shell-contract.json
|
|
3301
|
+
var shell_contract_default = {
|
|
3302
|
+
commands: [
|
|
3303
|
+
"env",
|
|
3304
|
+
"litestream-config",
|
|
3305
|
+
"restore",
|
|
3306
|
+
"sync-once",
|
|
3307
|
+
"model-auth-ready",
|
|
3308
|
+
"self-stop",
|
|
3309
|
+
"session-db-classify",
|
|
3310
|
+
"session-db-verify",
|
|
3311
|
+
"archive-put"
|
|
3312
|
+
],
|
|
3313
|
+
archiveStdinMaxBytes: 16777216
|
|
3314
|
+
};
|
|
3315
|
+
|
|
3316
|
+
// src/lib/runner-synchroniser.ts
|
|
3317
|
+
var MAX_ARCHIVE_STDIN_BYTES = shell_contract_default.archiveStdinMaxBytes;
|
|
3318
|
+
var ERROR_CLOSE_FALLBACK_MS = 1e3;
|
|
3319
|
+
function isPipeClosed(error2) {
|
|
3320
|
+
return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "EPIPE";
|
|
3321
|
+
}
|
|
3322
|
+
function appendError(stderr, error2) {
|
|
3323
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
3324
|
+
return stderr === "" ? message : `${stderr}
|
|
3325
|
+
${message}`;
|
|
3326
|
+
}
|
|
3327
|
+
function runSynchroniser(args, opts) {
|
|
3328
|
+
return new Promise((resolve5) => {
|
|
3329
|
+
let child;
|
|
3330
|
+
let stdout = "";
|
|
3331
|
+
let stderr = "";
|
|
3332
|
+
let settled = false;
|
|
3333
|
+
let shuttingDown = false;
|
|
3334
|
+
let timedOut = false;
|
|
3335
|
+
let stdinFailure;
|
|
3336
|
+
let childFailure;
|
|
3337
|
+
const timer = {};
|
|
3338
|
+
const closeFallbackTimer = {};
|
|
3339
|
+
let abortListener;
|
|
3340
|
+
let spawnListener;
|
|
3341
|
+
const finish = (result) => {
|
|
3342
|
+
if (settled) return;
|
|
3343
|
+
settled = true;
|
|
3344
|
+
if (timer.handle) clearTimeout(timer.handle);
|
|
3345
|
+
if (closeFallbackTimer.handle) clearTimeout(closeFallbackTimer.handle);
|
|
3346
|
+
if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
|
|
3347
|
+
if (spawnListener) child.removeListener("spawn", spawnListener);
|
|
3348
|
+
resolve5(result);
|
|
3349
|
+
};
|
|
3350
|
+
const armErrorCloseFallback = () => {
|
|
3351
|
+
if (closeFallbackTimer.handle) return;
|
|
3352
|
+
closeFallbackTimer.handle = setTimeout(() => {
|
|
3353
|
+
const failure = stdinFailure ?? childFailure;
|
|
3354
|
+
finish({
|
|
3355
|
+
code: null,
|
|
3356
|
+
stdout,
|
|
3357
|
+
stderr: failure === void 0 ? stderr : appendError(stderr, failure),
|
|
3358
|
+
timedOut
|
|
3359
|
+
});
|
|
3360
|
+
}, ERROR_CLOSE_FALLBACK_MS);
|
|
3361
|
+
};
|
|
3362
|
+
const terminateChild = () => {
|
|
3363
|
+
if (!shuttingDown) {
|
|
3364
|
+
shuttingDown = true;
|
|
3365
|
+
child.stdin?.destroy();
|
|
3366
|
+
}
|
|
3367
|
+
if (child.pid !== void 0) {
|
|
3368
|
+
child.kill("SIGKILL");
|
|
3369
|
+
} else if (!spawnListener) {
|
|
3370
|
+
spawnListener = () => {
|
|
3371
|
+
if (child.pid !== void 0) child.kill("SIGKILL");
|
|
3372
|
+
};
|
|
3373
|
+
child.once("spawn", spawnListener);
|
|
3374
|
+
}
|
|
3375
|
+
};
|
|
3376
|
+
try {
|
|
3377
|
+
child = spawn("runner-synchroniser", args, {
|
|
3378
|
+
env: opts.env ?? process.env,
|
|
3379
|
+
stdio: [opts.stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"]
|
|
3380
|
+
});
|
|
3381
|
+
} catch (error2) {
|
|
3382
|
+
finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
child.stdout?.setEncoding("utf8");
|
|
3386
|
+
child.stdout?.on("data", (chunk) => {
|
|
3387
|
+
stdout += chunk;
|
|
3388
|
+
});
|
|
3389
|
+
child.stderr?.setEncoding("utf8");
|
|
3390
|
+
child.stderr?.on("data", (chunk) => {
|
|
3391
|
+
stderr += chunk;
|
|
3392
|
+
});
|
|
3393
|
+
child.once("error", (error2) => {
|
|
3394
|
+
childFailure = error2;
|
|
3395
|
+
if (child.pid !== void 0) terminateChild();
|
|
3396
|
+
armErrorCloseFallback();
|
|
3397
|
+
});
|
|
3398
|
+
child.once("close", (code) => {
|
|
3399
|
+
const failure = stdinFailure ?? childFailure;
|
|
3400
|
+
finish({
|
|
3401
|
+
code: failure === void 0 ? code : null,
|
|
3402
|
+
stdout,
|
|
3403
|
+
stderr: failure === void 0 ? stderr : appendError(stderr, failure),
|
|
3404
|
+
timedOut
|
|
3405
|
+
});
|
|
3406
|
+
});
|
|
3407
|
+
const stdin = child.stdin;
|
|
3408
|
+
const failStdin = (error2) => {
|
|
3409
|
+
if (settled || shuttingDown || isPipeClosed(error2)) return;
|
|
3410
|
+
stdinFailure = error2;
|
|
3411
|
+
terminateChild();
|
|
3412
|
+
armErrorCloseFallback();
|
|
3413
|
+
};
|
|
3414
|
+
stdin?.once("error", failStdin);
|
|
3415
|
+
if (opts.stdin !== void 0 && stdin === null) {
|
|
3416
|
+
failStdin(new Error("synchroniser stdin is unavailable"));
|
|
3417
|
+
} else if (opts.stdin !== void 0 && stdin !== null) {
|
|
3418
|
+
const drained = stdin.write(opts.stdin, (error2) => {
|
|
3419
|
+
if (error2) failStdin(error2);
|
|
3420
|
+
});
|
|
3421
|
+
if (!drained) {
|
|
3422
|
+
stdin.once("drain", () => stdin.end());
|
|
3423
|
+
} else {
|
|
3424
|
+
stdin.end();
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
if (opts.signal) {
|
|
3428
|
+
const killChild = () => {
|
|
3429
|
+
terminateChild();
|
|
3430
|
+
};
|
|
3431
|
+
abortListener = killChild;
|
|
3432
|
+
if (opts.signal.aborted) {
|
|
3433
|
+
abortListener();
|
|
3434
|
+
} else {
|
|
3435
|
+
opts.signal.addEventListener("abort", abortListener, { once: true });
|
|
3436
|
+
if (opts.signal.aborted) abortListener();
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
timer.handle = setTimeout(
|
|
3440
|
+
() => {
|
|
3441
|
+
timedOut = true;
|
|
3442
|
+
terminateChild();
|
|
3443
|
+
},
|
|
3444
|
+
Math.max(0, opts.timeoutMs)
|
|
3445
|
+
);
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
// src/lib/opencode/session-archive-upload.ts
|
|
3450
|
+
var MAX_SESSION_ARCHIVE_BYTES = MAX_ARCHIVE_STDIN_BYTES;
|
|
3451
|
+
var EXIT_ARCHIVE_CONFIG_MISSING = 50;
|
|
3452
|
+
var EXIT_ARCHIVE_ACCESS_DENIED = 51;
|
|
3453
|
+
function transportFailure(bytes, reason) {
|
|
3454
|
+
const message = reason === "missing-executable" ? "runner-synchroniser is unavailable; session archival requires a hosted runner." : reason === "timeout" ? "runner-synchroniser timed out before the archive upload completed." : "session archive upload did not complete.";
|
|
3455
|
+
return { kind: "transport-failed", bytes, reason, message };
|
|
3456
|
+
}
|
|
3457
|
+
function isJson(body) {
|
|
3458
|
+
try {
|
|
3459
|
+
JSON.parse(body.toString("utf8"));
|
|
3460
|
+
return true;
|
|
3461
|
+
} catch (error2) {
|
|
3462
|
+
void error2;
|
|
3463
|
+
return false;
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
async function uploadSessionArchive(key, body, options) {
|
|
3467
|
+
const bytes = Buffer.from(body);
|
|
3468
|
+
if (bytes.byteLength > MAX_SESSION_ARCHIVE_BYTES) {
|
|
3469
|
+
return transportFailure(bytes.byteLength, "too-large");
|
|
3470
|
+
}
|
|
3471
|
+
if (!isJson(bytes)) return transportFailure(bytes.byteLength, "invalid-json");
|
|
3472
|
+
const run2 = options.runSynchroniser ?? runSynchroniser;
|
|
3473
|
+
let result;
|
|
3474
|
+
try {
|
|
3475
|
+
result = await run2(["archive-put", key], {
|
|
3476
|
+
timeoutMs: options.timeoutMs,
|
|
3477
|
+
env: options.env,
|
|
3478
|
+
signal: options.signal,
|
|
3479
|
+
stdin: bytes
|
|
3480
|
+
});
|
|
3481
|
+
} catch (error2) {
|
|
3482
|
+
return transportFailure(
|
|
3483
|
+
bytes.byteLength,
|
|
3484
|
+
error2 instanceof Error && /ENOENT|runner-synchroniser/i.test(error2.message) ? "missing-executable" : "spawn-failed"
|
|
3485
|
+
);
|
|
3486
|
+
}
|
|
3487
|
+
if (result.timedOut) return transportFailure(bytes.byteLength, "timeout");
|
|
3488
|
+
if (result.code === 0) return { kind: "ok", bytes: bytes.byteLength };
|
|
3489
|
+
if (result.code === EXIT_ARCHIVE_CONFIG_MISSING) {
|
|
3490
|
+
return { kind: "config-missing", bytes: bytes.byteLength };
|
|
3491
|
+
}
|
|
3492
|
+
if (result.code === EXIT_ARCHIVE_ACCESS_DENIED) {
|
|
3493
|
+
return { kind: "access-denied", bytes: bytes.byteLength };
|
|
3494
|
+
}
|
|
3495
|
+
if (result.code === null && /ENOENT|runner-synchroniser/i.test(result.stderr)) {
|
|
3496
|
+
return transportFailure(bytes.byteLength, "missing-executable");
|
|
3497
|
+
}
|
|
3498
|
+
return transportFailure(
|
|
3499
|
+
bytes.byteLength,
|
|
3500
|
+
result.code === null ? "spawn-failed" : "process-failed"
|
|
3501
|
+
);
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
// src/lib/opencode/session-archive-run.ts
|
|
3505
|
+
var SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS = 6e4;
|
|
3506
|
+
function updatedAt(session) {
|
|
3507
|
+
const updated = session.time?.updated;
|
|
3508
|
+
return typeof updated === "number" && Number.isFinite(updated) ? updated : -Infinity;
|
|
3509
|
+
}
|
|
3510
|
+
function sessionArchiveKey(prefix, sessionId) {
|
|
3511
|
+
return `${prefix.replace(/\/+$/, "")}/conversation-archives/${encodeURIComponent(sessionId)}.json`;
|
|
3512
|
+
}
|
|
3513
|
+
function failureMessage(error2) {
|
|
3514
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
3515
|
+
}
|
|
3516
|
+
function isBudgetExpired(deadline, now, signal) {
|
|
3517
|
+
return signal?.aborted === true || now() >= deadline;
|
|
3518
|
+
}
|
|
3519
|
+
function createBudgetSignal(parent, timeoutMs) {
|
|
3520
|
+
const controller = new AbortController();
|
|
3521
|
+
const forwardAbort = () => controller.abort(parent?.reason);
|
|
3522
|
+
if (parent?.aborted) forwardAbort();
|
|
3523
|
+
else parent?.addEventListener("abort", forwardAbort, { once: true });
|
|
3524
|
+
const timer = setTimeout(
|
|
3525
|
+
() => controller.abort(new Error("session archive budget exceeded")),
|
|
3526
|
+
timeoutMs
|
|
3527
|
+
);
|
|
3528
|
+
return {
|
|
3529
|
+
signal: controller.signal,
|
|
3530
|
+
dispose: () => {
|
|
3531
|
+
clearTimeout(timer);
|
|
3532
|
+
parent?.removeEventListener("abort", forwardAbort);
|
|
3533
|
+
}
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
async function runSessionArchive({
|
|
3537
|
+
sessions,
|
|
3538
|
+
deps,
|
|
3539
|
+
budgetMs,
|
|
3540
|
+
now = Date.now
|
|
3541
|
+
}) {
|
|
3542
|
+
const startedAt = now();
|
|
3543
|
+
const deadline = startedAt + Math.max(0, budgetMs);
|
|
3544
|
+
const summary = {
|
|
3545
|
+
inspected: sessions.length,
|
|
3546
|
+
uploaded: 0,
|
|
3547
|
+
unchanged: 0,
|
|
3548
|
+
failed: 0,
|
|
3549
|
+
uploadAttempts: 0,
|
|
3550
|
+
uploadFailures: 0,
|
|
3551
|
+
skippedBudget: 0,
|
|
3552
|
+
skippedBudgetIds: [],
|
|
3553
|
+
neverWatermarkedFailures: 0,
|
|
3554
|
+
unmappedIds: [],
|
|
3555
|
+
archivedSessionIds: [],
|
|
3556
|
+
failures: [],
|
|
3557
|
+
staleSince: null
|
|
3558
|
+
};
|
|
3559
|
+
const ordered = [...sessions].sort((left, right) => updatedAt(right) - updatedAt(left));
|
|
3560
|
+
const upload = deps.upload ?? uploadSessionArchive;
|
|
3561
|
+
const exportMessages = deps.exportMessages ?? exportSessionMessages;
|
|
3562
|
+
const writeWatermark = deps.recordWatermark ?? recordUpload;
|
|
3563
|
+
const failedWatermarks = [];
|
|
3564
|
+
const recordFailedWatermark = (watermark) => {
|
|
3565
|
+
if (watermark) failedWatermarks.push(watermark.uploaded_at);
|
|
3566
|
+
else summary.neverWatermarkedFailures++;
|
|
3567
|
+
};
|
|
3568
|
+
for (let index = 0; index < ordered.length; index++) {
|
|
3569
|
+
const session = ordered[index];
|
|
3570
|
+
const remainingMs = Math.floor(deadline - now());
|
|
3571
|
+
if (remainingMs <= 0 || deps.signal?.aborted) {
|
|
3572
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3573
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3574
|
+
break;
|
|
3575
|
+
}
|
|
3576
|
+
const conversationId = deps.bindings.get(session.id) ?? null;
|
|
3577
|
+
if (conversationId === null) summary.unmappedIds.push(session.id);
|
|
3578
|
+
const exportBudget = createBudgetSignal(deps.signal, remainingMs);
|
|
3579
|
+
let messages;
|
|
3580
|
+
try {
|
|
3581
|
+
messages = await exportMessages(deps.client, session.id, exportBudget.signal);
|
|
3582
|
+
} catch (error2) {
|
|
3583
|
+
if (isBudgetExpired(deadline, now, deps.signal) || exportBudget.signal.aborted) {
|
|
3584
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3585
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3586
|
+
break;
|
|
3587
|
+
}
|
|
3588
|
+
summary.failed++;
|
|
3589
|
+
const reason = error2 instanceof SessionArchiveExportError ? error2.code : "export-failed";
|
|
3590
|
+
summary.failures.push({
|
|
3591
|
+
sessionId: session.id,
|
|
3592
|
+
outcome: "export-failed",
|
|
3593
|
+
reason,
|
|
3594
|
+
message: failureMessage(error2),
|
|
3595
|
+
archived: false
|
|
3596
|
+
});
|
|
3597
|
+
recordFailedWatermark(deps.watermarks.get(session.id));
|
|
3598
|
+
continue;
|
|
3599
|
+
} finally {
|
|
3600
|
+
exportBudget.dispose();
|
|
3601
|
+
}
|
|
3602
|
+
if (isBudgetExpired(deadline, now, deps.signal) || exportBudget.signal.aborted) {
|
|
3603
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3604
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3605
|
+
break;
|
|
3606
|
+
}
|
|
3607
|
+
try {
|
|
3608
|
+
const archivedAt = new Date(now()).toISOString();
|
|
3609
|
+
const snapshot = buildSessionArchive({
|
|
3610
|
+
session,
|
|
3611
|
+
messages,
|
|
3612
|
+
apiVersion: deps.client.version,
|
|
3613
|
+
conversationId,
|
|
3614
|
+
bindingSource: conversationId === null ? "unmapped" : "ledger",
|
|
3615
|
+
runnerId: deps.runnerId,
|
|
3616
|
+
archivedAt
|
|
3617
|
+
});
|
|
3618
|
+
const key = sessionArchiveKey(deps.keyPrefix, session.id);
|
|
3619
|
+
const existing = deps.watermarks.get(session.id);
|
|
3620
|
+
if (existing?.fingerprint === snapshot.content_fingerprint) {
|
|
3621
|
+
summary.unchanged++;
|
|
3622
|
+
summary.archivedSessionIds.push(session.id);
|
|
3623
|
+
continue;
|
|
3624
|
+
}
|
|
3625
|
+
const uploadRemainingMs = Math.floor(deadline - now());
|
|
3626
|
+
if (uploadRemainingMs <= 0 || deps.signal?.aborted) {
|
|
3627
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3628
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3629
|
+
break;
|
|
3630
|
+
}
|
|
3631
|
+
summary.uploadAttempts++;
|
|
3632
|
+
const uploadBudget = createBudgetSignal(deps.signal, uploadRemainingMs);
|
|
3633
|
+
let outcome;
|
|
3634
|
+
try {
|
|
3635
|
+
outcome = await upload(key, JSON.stringify(snapshot), {
|
|
3636
|
+
timeoutMs: Math.min(uploadRemainingMs, SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS),
|
|
3637
|
+
signal: uploadBudget.signal
|
|
3638
|
+
});
|
|
3639
|
+
} finally {
|
|
3640
|
+
uploadBudget.dispose();
|
|
3641
|
+
}
|
|
3642
|
+
if (outcome.kind !== "ok") {
|
|
3643
|
+
summary.failed++;
|
|
3644
|
+
summary.uploadFailures++;
|
|
3645
|
+
const reason = "reason" in outcome ? outcome.reason : void 0;
|
|
3646
|
+
summary.failures.push({
|
|
3647
|
+
sessionId: session.id,
|
|
3648
|
+
outcome: outcome.kind,
|
|
3649
|
+
...reason !== void 0 ? { reason } : {},
|
|
3650
|
+
..."message" in outcome ? { message: outcome.message } : {},
|
|
3651
|
+
archived: false
|
|
3652
|
+
});
|
|
3653
|
+
recordFailedWatermark(existing);
|
|
3654
|
+
if (uploadBudget.signal.aborted) {
|
|
3655
|
+
summary.skippedBudgetIds.push(...ordered.slice(index + 1).map(({ id }) => id));
|
|
3656
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3657
|
+
break;
|
|
3658
|
+
}
|
|
3659
|
+
continue;
|
|
3660
|
+
}
|
|
3661
|
+
summary.uploaded++;
|
|
3662
|
+
summary.archivedSessionIds.push(session.id);
|
|
3663
|
+
const watermark = {
|
|
3664
|
+
fingerprint: snapshot.content_fingerprint,
|
|
3665
|
+
uploaded_at: new Date(now()).toISOString(),
|
|
3666
|
+
key
|
|
3667
|
+
};
|
|
3668
|
+
try {
|
|
3669
|
+
writeWatermark(deps.watermarkPath, session.id, watermark, deps.watermarks);
|
|
3670
|
+
} catch (error2) {
|
|
3671
|
+
summary.failures.push({
|
|
3672
|
+
sessionId: session.id,
|
|
3673
|
+
outcome: "watermark-write-failed",
|
|
3674
|
+
message: failureMessage(error2),
|
|
3675
|
+
archived: true
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
3678
|
+
} catch (error2) {
|
|
3679
|
+
if (isBudgetExpired(deadline, now, deps.signal)) {
|
|
3680
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3681
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3682
|
+
break;
|
|
3683
|
+
}
|
|
3684
|
+
summary.failed++;
|
|
3685
|
+
summary.failures.push({
|
|
3686
|
+
sessionId: session.id,
|
|
3687
|
+
outcome: "archive-failed",
|
|
3688
|
+
message: failureMessage(error2),
|
|
3689
|
+
archived: false
|
|
3690
|
+
});
|
|
3691
|
+
recordFailedWatermark(deps.watermarks.get(session.id));
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
summary.staleSince = failedWatermarks.sort()[0] ?? null;
|
|
3695
|
+
return summary;
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
// src/lib/opencode/auth.ts
|
|
3699
|
+
function buildOpenCodeBasicAuthHeader(password) {
|
|
3700
|
+
return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
// src/lib/opencode/health.ts
|
|
3704
|
+
async function checkOpenCodeHealth(port) {
|
|
3705
|
+
try {
|
|
3706
|
+
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
|
3707
|
+
signal: AbortSignal.timeout(2e3)
|
|
3708
|
+
// 2 second timeout
|
|
3709
|
+
});
|
|
3710
|
+
if (response.status === 401) {
|
|
3711
|
+
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
3712
|
+
}
|
|
3713
|
+
if (!response.ok) {
|
|
3714
|
+
return { healthy: false, error: `HTTP ${response.status}` };
|
|
3715
|
+
}
|
|
3716
|
+
const body = await response.text();
|
|
3717
|
+
let data;
|
|
3718
|
+
try {
|
|
3719
|
+
data = JSON.parse(body);
|
|
3720
|
+
} catch (error2) {
|
|
3721
|
+
return {
|
|
3722
|
+
healthy: false,
|
|
3723
|
+
okWithNonJsonBody: true,
|
|
3724
|
+
error: `HTTP ${response.status}: invalid JSON health response${bodyExcerpt(body)}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3725
|
+
};
|
|
3726
|
+
}
|
|
3727
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
3728
|
+
return {
|
|
3729
|
+
healthy: false,
|
|
3730
|
+
okWithNonJsonBody: true,
|
|
3731
|
+
error: `HTTP ${response.status}: invalid health response${bodyExcerpt(body)}: expected JSON object`
|
|
3732
|
+
};
|
|
3733
|
+
}
|
|
3734
|
+
const version2 = data.version;
|
|
3735
|
+
return typeof version2 === "string" ? { healthy: true, version: version2 } : { healthy: true };
|
|
3736
|
+
} catch (error2) {
|
|
3737
|
+
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
3738
|
+
return { healthy: false, error: message };
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
function bodyExcerpt(body) {
|
|
3742
|
+
const excerpt = body.replace(/\s+/g, " ").trim().slice(0, 160);
|
|
3743
|
+
return excerpt ? ` (body="${excerpt}")` : " (empty body)";
|
|
3744
|
+
}
|
|
3745
|
+
async function checkOpenCode2Health(port, password) {
|
|
3746
|
+
try {
|
|
3747
|
+
const headers = {};
|
|
3748
|
+
if (password !== void 0) {
|
|
3749
|
+
headers.Authorization = buildOpenCodeBasicAuthHeader(password);
|
|
3750
|
+
}
|
|
3751
|
+
let response = await fetch(`http://127.0.0.1:${port}/api/info`, {
|
|
3752
|
+
headers,
|
|
3753
|
+
signal: AbortSignal.timeout(2e3)
|
|
3754
|
+
});
|
|
3755
|
+
const usesHealthFallback = response.status === 404;
|
|
3756
|
+
if (usesHealthFallback) {
|
|
3757
|
+
response = await fetch(`http://127.0.0.1:${port}/api/health`, {
|
|
3758
|
+
headers,
|
|
3759
|
+
signal: AbortSignal.timeout(2e3)
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
3762
|
+
if (response.status === 401) {
|
|
3763
|
+
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
3764
|
+
}
|
|
3765
|
+
if (!response.ok) {
|
|
3766
|
+
return { healthy: false, error: `HTTP ${response.status}` };
|
|
3767
|
+
}
|
|
3768
|
+
const body = await response.text();
|
|
3769
|
+
const excerpt = bodyExcerpt(body);
|
|
3770
|
+
let data;
|
|
3771
|
+
try {
|
|
3772
|
+
data = JSON.parse(body);
|
|
3773
|
+
} catch (error2) {
|
|
3774
|
+
return {
|
|
3775
|
+
healthy: false,
|
|
3776
|
+
error: `HTTP ${response.status}: invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3777
|
+
};
|
|
3778
|
+
}
|
|
3779
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
3780
|
+
return {
|
|
3781
|
+
healthy: false,
|
|
3782
|
+
error: `HTTP ${response.status}: invalid health response${excerpt}: expected healthy=true`
|
|
3783
|
+
};
|
|
3784
|
+
}
|
|
3785
|
+
const bodyObject = data;
|
|
3786
|
+
if (usesHealthFallback && bodyObject.healthy !== true) {
|
|
3787
|
+
return {
|
|
3788
|
+
healthy: false,
|
|
3789
|
+
error: `Invalid health response${excerpt}: expected healthy=true`
|
|
3790
|
+
};
|
|
3791
|
+
}
|
|
3792
|
+
if (!usesHealthFallback) {
|
|
3793
|
+
return typeof bodyObject.version === "string" && bodyObject.version.length > 0 ? { healthy: true, version: bodyObject.version } : {
|
|
3794
|
+
healthy: false,
|
|
3795
|
+
error: `HTTP ${response.status}: invalid info response${excerpt}: expected a non-empty version`
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3798
|
+
return typeof bodyObject.version === "string" ? { healthy: true, version: bodyObject.version } : { healthy: true };
|
|
3799
|
+
} catch (error2) {
|
|
3800
|
+
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
3801
|
+
return { healthy: false, error: message };
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
3805
|
+
const startTime = Date.now();
|
|
3806
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
3807
|
+
const health = await checkOpenCodeHealth(port);
|
|
3808
|
+
if (health.healthy || health.authFailed) {
|
|
3809
|
+
return health;
|
|
3810
|
+
}
|
|
3811
|
+
await new Promise((resolve5) => setTimeout(resolve5, 1e3));
|
|
3812
|
+
}
|
|
3813
|
+
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
3814
|
+
}
|
|
3815
|
+
async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
|
|
3816
|
+
const startTime = Date.now();
|
|
3817
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
3818
|
+
const health = await checkOpenCode2Health(port, password);
|
|
3819
|
+
if (health.healthy || health.authFailed) {
|
|
3820
|
+
return health;
|
|
3821
|
+
}
|
|
3822
|
+
await new Promise((resolve5) => setTimeout(resolve5, 1e3));
|
|
3823
|
+
}
|
|
3824
|
+
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
// src/lib/http-timeout.ts
|
|
3828
|
+
var REQUEST_TIMEOUT_MS = 6e4;
|
|
3829
|
+
var RequestTimeoutError = class extends Error {
|
|
3830
|
+
constructor(operation, timeoutMs, elapsedMs) {
|
|
3831
|
+
super(`${operation} timed out after ${elapsedMs}ms (limit ${timeoutMs}ms)`);
|
|
3832
|
+
this.operation = operation;
|
|
3833
|
+
this.timeoutMs = timeoutMs;
|
|
3834
|
+
this.elapsedMs = elapsedMs;
|
|
3835
|
+
this.name = "TimeoutError";
|
|
3836
|
+
}
|
|
3837
|
+
};
|
|
3838
|
+
function describeRequestOperation(input, init) {
|
|
3839
|
+
const method = (init?.method ?? input.method ?? "GET").toUpperCase();
|
|
3840
|
+
const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
3841
|
+
let path;
|
|
3842
|
+
try {
|
|
3843
|
+
path = new URL(rawUrl).pathname;
|
|
3844
|
+
} catch (error2) {
|
|
3845
|
+
if (!(error2 instanceof TypeError)) throw error2;
|
|
3846
|
+
path = rawUrl.split(/[?#]/, 1)[0];
|
|
3847
|
+
}
|
|
3848
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3849
|
+
const prefixedIdentifier = /^[a-z]+_[A-Za-z0-9_-]{10,}$/;
|
|
3850
|
+
const maskedPath = path.split("/").map((segment) => uuid.test(segment) || prefixedIdentifier.test(segment) ? ":id" : segment).join("/");
|
|
3851
|
+
return `${method} ${maskedPath}`;
|
|
3852
|
+
}
|
|
3853
|
+
function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
3854
|
+
return (async (input, init) => {
|
|
3855
|
+
const startedAt = Date.now();
|
|
3856
|
+
const controller = new AbortController();
|
|
3857
|
+
const abortFromCaller = () => controller.abort(init?.signal?.reason);
|
|
3858
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
3859
|
+
const abortFromTimeout = () => controller.abort(timeoutSignal.reason);
|
|
3860
|
+
if (init?.signal?.aborted) abortFromCaller();
|
|
3861
|
+
else init?.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
3862
|
+
if (timeoutSignal.aborted) abortFromTimeout();
|
|
3863
|
+
else timeoutSignal.addEventListener("abort", abortFromTimeout, { once: true });
|
|
3864
|
+
try {
|
|
3865
|
+
return await fetchImpl(input, { ...init, signal: controller.signal });
|
|
3866
|
+
} catch (error2) {
|
|
3867
|
+
if (timeoutSignal.aborted) {
|
|
3868
|
+
throw new RequestTimeoutError(
|
|
3869
|
+
describeRequestOperation(input, init),
|
|
3870
|
+
timeoutMs,
|
|
3871
|
+
Date.now() - startedAt
|
|
3872
|
+
);
|
|
3873
|
+
}
|
|
3874
|
+
throw error2;
|
|
3875
|
+
} finally {
|
|
3876
|
+
init?.signal?.removeEventListener("abort", abortFromCaller);
|
|
3877
|
+
timeoutSignal.removeEventListener("abort", abortFromTimeout);
|
|
3878
|
+
}
|
|
3879
|
+
});
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
// src/lib/opencode/client.ts
|
|
3883
|
+
function redactPassword(message, password) {
|
|
3884
|
+
return message.replaceAll(password, "[redacted]");
|
|
3885
|
+
}
|
|
3886
|
+
function createOpenCodeClient(options) {
|
|
3887
|
+
const password = options.password ?? null;
|
|
3888
|
+
const fetchImpl = withRequestTimeout(options.fetchImpl ?? fetch, REQUEST_TIMEOUT_MS);
|
|
3889
|
+
const baseUrl = `http://127.0.0.1:${options.port}`;
|
|
3890
|
+
return {
|
|
3891
|
+
port: options.port,
|
|
3892
|
+
version: options.version,
|
|
3893
|
+
password,
|
|
3894
|
+
async request(path, init, requestOptions) {
|
|
3895
|
+
const requestInit = options.version === "v2" && password !== null ? (() => {
|
|
3896
|
+
const headers = new Headers(init?.headers);
|
|
3897
|
+
headers.set("Authorization", buildOpenCodeBasicAuthHeader(password));
|
|
3898
|
+
return { ...init, headers };
|
|
3899
|
+
})() : init;
|
|
3900
|
+
try {
|
|
3901
|
+
const response = await fetchImpl(`${baseUrl}${path}`, requestInit);
|
|
3902
|
+
if (!response.ok && !requestOptions?.allowStatuses?.includes(response.status)) {
|
|
3903
|
+
const body = await response.text();
|
|
3904
|
+
throw new Error(
|
|
3905
|
+
`OpenCode request failed: HTTP ${response.status}${body ? `: ${body}` : ""}`
|
|
3906
|
+
);
|
|
3907
|
+
}
|
|
3908
|
+
return response;
|
|
3909
|
+
} catch (error2) {
|
|
3910
|
+
if (options.version === "v2" && password !== null) {
|
|
3911
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
3912
|
+
throw new Error(redactPassword(message, password));
|
|
3913
|
+
}
|
|
3914
|
+
throw error2;
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
};
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
// src/lib/opencode/session.ts
|
|
3921
|
+
var ALL_HTTP_STATUSES = Array.from({ length: 500 }, (_, index) => index + 100);
|
|
3922
|
+
function timedFetch(input, init) {
|
|
3923
|
+
return withRequestTimeout(fetch, REQUEST_TIMEOUT_MS)(input, init);
|
|
3924
|
+
}
|
|
3925
|
+
function requestWithClient(port, client, path, init, options) {
|
|
3926
|
+
return client ? client.request(path, init, options) : timedFetch(`${opencodeBase(port)}${path}`, init);
|
|
3927
|
+
}
|
|
3928
|
+
function opencodeBase(port) {
|
|
3929
|
+
return `http://127.0.0.1:${port}`;
|
|
3930
|
+
}
|
|
3931
|
+
async function getOpenCodeDirectory(port, client) {
|
|
3932
|
+
try {
|
|
3933
|
+
const res = await requestWithClient(port, client, "/path");
|
|
3934
|
+
if (!res.ok) return null;
|
|
3935
|
+
const body = await res.json();
|
|
3936
|
+
const dir = typeof body.directory === "string" && body.directory || typeof body.worktree === "string" && body.worktree || typeof body.path?.cwd === "string" && body.path.cwd || typeof body.path?.directory === "string" && body.path.directory || null;
|
|
3937
|
+
return dir && dir.trim() ? dir.trim() : null;
|
|
3938
|
+
} catch (error2) {
|
|
3939
|
+
console.error(
|
|
3940
|
+
`[getOpenCodeDirectory] GET /path failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3941
|
+
);
|
|
3942
|
+
return null;
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
function roleOf2(m) {
|
|
3946
|
+
if (!m || typeof m !== "object") return void 0;
|
|
3947
|
+
if (typeof m.role === "string") return m.role;
|
|
3948
|
+
const infoRole = m.info?.role;
|
|
3949
|
+
return typeof infoRole === "string" ? infoRole : void 0;
|
|
3950
|
+
}
|
|
3951
|
+
function completedOf(m) {
|
|
3952
|
+
if (!m || typeof m !== "object") return void 0;
|
|
3953
|
+
return m.info?.time?.completed ?? m.time?.completed;
|
|
3954
|
+
}
|
|
3955
|
+
function createdOf(m) {
|
|
3956
|
+
if (!m || typeof m !== "object") return void 0;
|
|
3957
|
+
return m.info?.time?.created ?? m.time?.created;
|
|
3958
|
+
}
|
|
3959
|
+
function idOf2(m) {
|
|
3960
|
+
if (!m || typeof m !== "object") return void 0;
|
|
3961
|
+
if (typeof m.id === "string") return m.id;
|
|
3962
|
+
const infoId = m.info?.id;
|
|
3963
|
+
return typeof infoId === "string" ? infoId : void 0;
|
|
3964
|
+
}
|
|
3965
|
+
function parentIdOf2(m) {
|
|
3966
|
+
if (!m || typeof m !== "object") return void 0;
|
|
3967
|
+
if (typeof m.parentID === "string") return m.parentID;
|
|
3968
|
+
const infoParent = m.info?.parentID;
|
|
3969
|
+
return typeof infoParent === "string" ? infoParent : void 0;
|
|
3970
|
+
}
|
|
3971
|
+
function finishOf(m) {
|
|
3972
|
+
if (!m || typeof m !== "object") return void 0;
|
|
3973
|
+
if (typeof m.finish === "string") return m.finish;
|
|
3974
|
+
const infoFinish = m.info?.finish;
|
|
3975
|
+
return typeof infoFinish === "string" ? infoFinish : void 0;
|
|
3976
|
+
}
|
|
3977
|
+
function errorOf(m) {
|
|
3978
|
+
if (!m || typeof m !== "object") return void 0;
|
|
3979
|
+
return m.info?.error ?? m.error;
|
|
3980
|
+
}
|
|
3981
|
+
function isTerminalStopAnswer(m) {
|
|
3982
|
+
return completedOf(m) != null && errorOf(m) == null && finishOf(m) === "stop";
|
|
3983
|
+
}
|
|
3984
|
+
function isAssistantInFlight(m) {
|
|
3985
|
+
if (completedOf(m) == null) return true;
|
|
3986
|
+
if (errorOf(m) != null) return false;
|
|
3987
|
+
return finishOf(m) === "tool-calls";
|
|
3988
|
+
}
|
|
3989
|
+
async function getSessionMessages(port, sessionId, client) {
|
|
2891
3990
|
try {
|
|
2892
3991
|
const path = `/session/${sessionId}/message`;
|
|
2893
3992
|
const res = await requestWithClient(port, client, path);
|
|
@@ -2959,28 +4058,38 @@ function sessionListPath(options) {
|
|
|
2959
4058
|
async function listSessionPage(port, client, options) {
|
|
2960
4059
|
const path = sessionListPath(options);
|
|
2961
4060
|
try {
|
|
2962
|
-
const res = await requestWithClient(
|
|
2963
|
-
|
|
4061
|
+
const res = await requestWithClient(
|
|
4062
|
+
port,
|
|
4063
|
+
client,
|
|
4064
|
+
path,
|
|
4065
|
+
options?.signal ? { signal: options.signal } : void 0,
|
|
4066
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
4067
|
+
);
|
|
4068
|
+
if (!res.ok) {
|
|
4069
|
+
console.error(`[listSessionPage] GET ${path} returned HTTP ${res.status} (port ${port})`);
|
|
4070
|
+
return null;
|
|
4071
|
+
}
|
|
2964
4072
|
const body = await res.json();
|
|
2965
4073
|
return Array.isArray(body) ? body : null;
|
|
2966
4074
|
} catch (error2) {
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
);
|
|
4075
|
+
if (options?.signal?.aborted) return null;
|
|
4076
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
4077
|
+
console.error(`[listSessionPage] GET ${path} failed (port ${port}): ${errorName}`);
|
|
2970
4078
|
return null;
|
|
2971
4079
|
}
|
|
2972
4080
|
}
|
|
2973
4081
|
async function listSessions(port, client, options) {
|
|
2974
|
-
if (client?.version === "v2") return listV2Sessions(client);
|
|
4082
|
+
if (client?.version === "v2") return listV2Sessions(client, options?.signal);
|
|
2975
4083
|
return listSessionPage(port, client, options);
|
|
2976
4084
|
}
|
|
2977
|
-
async function listAllSessions(port, client) {
|
|
4085
|
+
async function listAllSessions(port, client, signal) {
|
|
2978
4086
|
if (client?.version === "v2") {
|
|
2979
|
-
const sessions2 = await listV2Sessions(client);
|
|
4087
|
+
const sessions2 = await listV2Sessions(client, signal);
|
|
2980
4088
|
return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
|
|
2981
4089
|
}
|
|
2982
4090
|
const sessions = await listSessionPage(port, client, {
|
|
2983
|
-
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
|
|
4091
|
+
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS,
|
|
4092
|
+
signal
|
|
2984
4093
|
});
|
|
2985
4094
|
if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
|
|
2986
4095
|
if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
|
|
@@ -3187,24 +4296,70 @@ async function getModelAttachmentCapability(port, model, client) {
|
|
|
3187
4296
|
const def = defaults2[provider.id];
|
|
3188
4297
|
if (typeof def === "string") modelId = def;
|
|
3189
4298
|
}
|
|
3190
|
-
if (!modelId) {
|
|
3191
|
-
if (providerId) {
|
|
3192
|
-
const keys = Object.keys(provider.models);
|
|
3193
|
-
if (keys.length === 1) modelId = keys[0];
|
|
3194
|
-
}
|
|
3195
|
-
if (!modelId) return null;
|
|
4299
|
+
if (!modelId) {
|
|
4300
|
+
if (providerId) {
|
|
4301
|
+
const keys = Object.keys(provider.models);
|
|
4302
|
+
if (keys.length === 1) modelId = keys[0];
|
|
4303
|
+
}
|
|
4304
|
+
if (!modelId) return null;
|
|
4305
|
+
}
|
|
4306
|
+
const entry = provider.models[modelId];
|
|
4307
|
+
if (!entry || typeof entry !== "object") return null;
|
|
4308
|
+
if (entry.capabilities && typeof entry.capabilities === "object") {
|
|
4309
|
+
if (typeof entry.capabilities.attachment === "boolean") {
|
|
4310
|
+
return entry.capabilities.attachment;
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
return typeof entry.attachment === "boolean" ? entry.attachment : null;
|
|
4314
|
+
} catch (err) {
|
|
4315
|
+
console.error(
|
|
4316
|
+
`[getModelAttachmentCapability] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
4317
|
+
);
|
|
4318
|
+
return null;
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
async function isPinnedProviderLoaded(port, model, client) {
|
|
4322
|
+
if (client?.version === "v2") return null;
|
|
4323
|
+
const { model: baseModel } = splitModelVariant(model);
|
|
4324
|
+
if (!baseModel) return null;
|
|
4325
|
+
const slash = baseModel.indexOf("/");
|
|
4326
|
+
if (slash <= 0) return null;
|
|
4327
|
+
const providerId = baseModel.slice(0, slash);
|
|
4328
|
+
try {
|
|
4329
|
+
const res = await requestWithClient(port, client, "/config/providers");
|
|
4330
|
+
if (!res.ok) {
|
|
4331
|
+
console.error(
|
|
4332
|
+
`[isPinnedProviderLoaded] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
4333
|
+
);
|
|
4334
|
+
return null;
|
|
4335
|
+
}
|
|
4336
|
+
const body = await res.json();
|
|
4337
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
4338
|
+
console.error(
|
|
4339
|
+
`[isPinnedProviderLoaded] GET /config/providers body was not a plain object (port ${port})`
|
|
4340
|
+
);
|
|
4341
|
+
return null;
|
|
4342
|
+
}
|
|
4343
|
+
const providers = body.providers;
|
|
4344
|
+
if (!Array.isArray(providers)) {
|
|
4345
|
+
console.error(
|
|
4346
|
+
`[isPinnedProviderLoaded] GET /config/providers body had no providers array (port ${port})`
|
|
4347
|
+
);
|
|
4348
|
+
return null;
|
|
3196
4349
|
}
|
|
3197
|
-
const
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
4350
|
+
const providerIds = providers.filter(
|
|
4351
|
+
(provider) => provider !== null && typeof provider === "object" && typeof provider.id === "string"
|
|
4352
|
+
).map((provider) => provider.id);
|
|
4353
|
+
if (providerIds.length === 0) {
|
|
4354
|
+
console.error(
|
|
4355
|
+
`[isPinnedProviderLoaded] GET /config/providers had no provider entries with string ids (port ${port})`
|
|
4356
|
+
);
|
|
4357
|
+
return null;
|
|
3203
4358
|
}
|
|
3204
|
-
return
|
|
4359
|
+
return providerIds.includes(providerId);
|
|
3205
4360
|
} catch (err) {
|
|
3206
4361
|
console.error(
|
|
3207
|
-
`[
|
|
4362
|
+
`[isPinnedProviderLoaded] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
3208
4363
|
);
|
|
3209
4364
|
return null;
|
|
3210
4365
|
}
|
|
@@ -3623,9 +4778,17 @@ function isPreamblePinnedRunning(messages, userMessageId) {
|
|
|
3623
4778
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
3624
4779
|
return completedOf(reply) != null && finishOf(reply) === "tool-calls";
|
|
3625
4780
|
}
|
|
4781
|
+
function isMidFlightPinnedRunning(messages, userMessageId) {
|
|
4782
|
+
if (messageRunState(messages, userMessageId) !== "running") return false;
|
|
4783
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
4784
|
+
return reply !== null && isAssistantInFlight(reply) && completedOf(reply) == null;
|
|
4785
|
+
}
|
|
3626
4786
|
function isB2AbandonmentConfirmed(params) {
|
|
3627
4787
|
return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false && params.rootOngoing === false;
|
|
3628
4788
|
}
|
|
4789
|
+
function isMidFlightOrphanConfirmed(params) {
|
|
4790
|
+
return params.pinnedForMs >= params.minPinnedMs && params.notOngoingStreak >= params.minNotOngoingStreak;
|
|
4791
|
+
}
|
|
3629
4792
|
function isAmbiguousTerminalFinish(m) {
|
|
3630
4793
|
if (completedOf(m) == null) return false;
|
|
3631
4794
|
if (errorOf(m) != null) return false;
|
|
@@ -3727,8 +4890,8 @@ function classifyReplyAuthError(reply) {
|
|
|
3727
4890
|
};
|
|
3728
4891
|
}
|
|
3729
4892
|
}
|
|
3730
|
-
const
|
|
3731
|
-
if (
|
|
4893
|
+
const errorMessage4 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
|
|
4894
|
+
if (errorMessage4 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage4.includes(knownMessage))) {
|
|
3732
4895
|
return {
|
|
3733
4896
|
kind: "model_auth",
|
|
3734
4897
|
providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
|
|
@@ -3999,14 +5162,15 @@ async function probeOpenCodeRequestReadiness(port, client) {
|
|
|
3999
5162
|
}
|
|
4000
5163
|
|
|
4001
5164
|
// src/lib/opencode/session-v2.ts
|
|
4002
|
-
|
|
5165
|
+
var ALL_HTTP_STATUSES2 = Array.from({ length: 500 }, (_, index) => index + 100);
|
|
5166
|
+
function isRecord3(value) {
|
|
4003
5167
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4004
5168
|
}
|
|
4005
5169
|
function finiteNumber2(value) {
|
|
4006
5170
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
4007
5171
|
}
|
|
4008
5172
|
function adaptTime(value) {
|
|
4009
|
-
if (!
|
|
5173
|
+
if (!isRecord3(value)) return void 0;
|
|
4010
5174
|
const created = finiteNumber2(value.created);
|
|
4011
5175
|
const completed = finiteNumber2(value.completed);
|
|
4012
5176
|
if (created === void 0 && completed === void 0) return void 0;
|
|
@@ -4016,11 +5180,11 @@ function adaptTime(value) {
|
|
|
4016
5180
|
};
|
|
4017
5181
|
}
|
|
4018
5182
|
function adaptTokens(value) {
|
|
4019
|
-
if (!
|
|
5183
|
+
if (!isRecord3(value)) return void 0;
|
|
4020
5184
|
const input = finiteNumber2(value.input);
|
|
4021
5185
|
const output = finiteNumber2(value.output);
|
|
4022
5186
|
const reasoning = finiteNumber2(value.reasoning);
|
|
4023
|
-
const cache =
|
|
5187
|
+
const cache = isRecord3(value.cache) ? {
|
|
4024
5188
|
...finiteNumber2(value.cache.read) !== void 0 ? { read: finiteNumber2(value.cache.read) } : {},
|
|
4025
5189
|
...finiteNumber2(value.cache.write) !== void 0 ? { write: finiteNumber2(value.cache.write) } : {}
|
|
4026
5190
|
} : void 0;
|
|
@@ -4044,7 +5208,7 @@ function adaptMessageInfo(value, role) {
|
|
|
4044
5208
|
if (typeof value.finish === "string") info.finish = value.finish;
|
|
4045
5209
|
if ("error" in value) info.error = value.error;
|
|
4046
5210
|
if (typeof value.agent === "string") info.agent = value.agent;
|
|
4047
|
-
if (
|
|
5211
|
+
if (isRecord3(value.model)) {
|
|
4048
5212
|
if (typeof value.model.id === "string") info.modelID = value.model.id;
|
|
4049
5213
|
if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
|
|
4050
5214
|
}
|
|
@@ -4054,7 +5218,7 @@ function adaptMessageInfo(value, role) {
|
|
|
4054
5218
|
return info;
|
|
4055
5219
|
}
|
|
4056
5220
|
function adaptV2Message(value) {
|
|
4057
|
-
if (!
|
|
5221
|
+
if (!isRecord3(value) || typeof value.id !== "string" || typeof value.type !== "string") {
|
|
4058
5222
|
return null;
|
|
4059
5223
|
}
|
|
4060
5224
|
if (value.type === "user") {
|
|
@@ -4067,7 +5231,7 @@ function adaptV2Message(value) {
|
|
|
4067
5231
|
if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
|
|
4068
5232
|
const parts = [];
|
|
4069
5233
|
for (const content of value.content) {
|
|
4070
|
-
if (!
|
|
5234
|
+
if (!isRecord3(content) || typeof content.type !== "string") return null;
|
|
4071
5235
|
if (content.type === "text") {
|
|
4072
5236
|
if (typeof content.text !== "string") return null;
|
|
4073
5237
|
parts.push({ type: "text", text: content.text });
|
|
@@ -4081,23 +5245,23 @@ function adaptV2Message(value) {
|
|
|
4081
5245
|
};
|
|
4082
5246
|
}
|
|
4083
5247
|
function adaptFormTool(value) {
|
|
4084
|
-
if (!
|
|
5248
|
+
if (!isRecord3(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
|
|
4085
5249
|
return void 0;
|
|
4086
5250
|
}
|
|
4087
5251
|
return { messageID: value.messageID, callID: value.id };
|
|
4088
5252
|
}
|
|
4089
5253
|
function adaptV2FormWire(value) {
|
|
4090
|
-
if (!
|
|
5254
|
+
if (!isRecord3(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
4091
5255
|
return null;
|
|
4092
5256
|
}
|
|
4093
5257
|
return value;
|
|
4094
5258
|
}
|
|
4095
5259
|
function adaptV2FormField(value, header) {
|
|
4096
|
-
if (!
|
|
5260
|
+
if (!isRecord3(value)) return null;
|
|
4097
5261
|
const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
|
|
4098
5262
|
if (!question) return null;
|
|
4099
5263
|
const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
|
|
4100
|
-
if (!
|
|
5264
|
+
if (!isRecord3(option)) return [];
|
|
4101
5265
|
const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
|
|
4102
5266
|
if (!label) return [];
|
|
4103
5267
|
return [
|
|
@@ -4115,7 +5279,7 @@ function adaptV2Form(value) {
|
|
|
4115
5279
|
const header = typeof form.title === "string" ? form.title : "";
|
|
4116
5280
|
const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
|
|
4117
5281
|
if (questions.length === 0) return null;
|
|
4118
|
-
const tool =
|
|
5282
|
+
const tool = isRecord3(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
|
|
4119
5283
|
return {
|
|
4120
5284
|
id: form.id,
|
|
4121
5285
|
sessionID: form.sessionID,
|
|
@@ -4125,7 +5289,7 @@ function adaptV2Form(value) {
|
|
|
4125
5289
|
};
|
|
4126
5290
|
}
|
|
4127
5291
|
function adaptV2FormList(value) {
|
|
4128
|
-
if (!
|
|
5292
|
+
if (!isRecord3(value) || !Array.isArray(value.data)) return null;
|
|
4129
5293
|
return value.data.map(adaptV2Form).filter((form) => form !== null);
|
|
4130
5294
|
}
|
|
4131
5295
|
function adaptPattern(value) {
|
|
@@ -4136,7 +5300,7 @@ function adaptPattern(value) {
|
|
|
4136
5300
|
return void 0;
|
|
4137
5301
|
}
|
|
4138
5302
|
function adaptV2PermissionWire(value) {
|
|
4139
|
-
if (!
|
|
5303
|
+
if (!isRecord3(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
4140
5304
|
return null;
|
|
4141
5305
|
}
|
|
4142
5306
|
if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
|
|
@@ -4148,12 +5312,12 @@ function adaptV2Permission(value) {
|
|
|
4148
5312
|
const type = permission.permission ?? permission.action;
|
|
4149
5313
|
if (!type) return null;
|
|
4150
5314
|
const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
|
|
4151
|
-
const time =
|
|
5315
|
+
const time = isRecord3(permission.time) ? finiteNumber2(permission.time.created) !== void 0 ? { created: finiteNumber2(permission.time.created) } : void 0 : void 0;
|
|
4152
5316
|
return {
|
|
4153
5317
|
id: permission.id,
|
|
4154
5318
|
type,
|
|
4155
5319
|
sessionID: permission.sessionID,
|
|
4156
|
-
metadata:
|
|
5320
|
+
metadata: isRecord3(permission.metadata) ? permission.metadata : {},
|
|
4157
5321
|
raw: permission,
|
|
4158
5322
|
...pattern !== void 0 ? { pattern } : {},
|
|
4159
5323
|
...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
|
|
@@ -4163,12 +5327,12 @@ function adaptV2Permission(value) {
|
|
|
4163
5327
|
};
|
|
4164
5328
|
}
|
|
4165
5329
|
function adaptV2PermissionList(value) {
|
|
4166
|
-
if (!
|
|
5330
|
+
if (!isRecord3(value) || !Array.isArray(value.data)) return null;
|
|
4167
5331
|
return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
|
|
4168
5332
|
}
|
|
4169
5333
|
function adaptV2Session(value) {
|
|
4170
|
-
if (!
|
|
4171
|
-
const time =
|
|
5334
|
+
if (!isRecord3(value) || typeof value.id !== "string" || value.id.length === 0) return null;
|
|
5335
|
+
const time = isRecord3(value.time) ? {
|
|
4172
5336
|
...finiteNumber2(value.time.created) !== void 0 ? { created: finiteNumber2(value.time.created) } : {},
|
|
4173
5337
|
...finiteNumber2(value.time.updated) !== void 0 ? { updated: finiteNumber2(value.time.updated) } : {}
|
|
4174
5338
|
} : void 0;
|
|
@@ -4180,7 +5344,7 @@ function adaptV2Session(value) {
|
|
|
4180
5344
|
};
|
|
4181
5345
|
}
|
|
4182
5346
|
function adaptV2SessionList(value) {
|
|
4183
|
-
if (!
|
|
5347
|
+
if (!isRecord3(value) || !Array.isArray(value.data) || !isRecord3(value.cursor)) return null;
|
|
4184
5348
|
return {
|
|
4185
5349
|
data: value.data.map(adaptV2Session).filter((session) => session !== null),
|
|
4186
5350
|
cursor: value.cursor
|
|
@@ -4189,18 +5353,18 @@ function adaptV2SessionList(value) {
|
|
|
4189
5353
|
function adaptV2Location(value) {
|
|
4190
5354
|
const candidates = [
|
|
4191
5355
|
value,
|
|
4192
|
-
|
|
4193
|
-
|
|
5356
|
+
isRecord3(value) ? value.data : void 0,
|
|
5357
|
+
isRecord3(value) ? value.location : void 0
|
|
4194
5358
|
];
|
|
4195
5359
|
for (const candidate of candidates) {
|
|
4196
|
-
if (!
|
|
5360
|
+
if (!isRecord3(candidate) || typeof candidate.directory !== "string") continue;
|
|
4197
5361
|
const directory = candidate.directory.trim();
|
|
4198
5362
|
if (directory) return directory;
|
|
4199
5363
|
}
|
|
4200
5364
|
return null;
|
|
4201
5365
|
}
|
|
4202
5366
|
function adaptV2Messages(value) {
|
|
4203
|
-
if (!
|
|
5367
|
+
if (!isRecord3(value) || !Array.isArray(value.data)) return [];
|
|
4204
5368
|
return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
|
|
4205
5369
|
}
|
|
4206
5370
|
async function readJson(response) {
|
|
@@ -4215,7 +5379,7 @@ async function readJson(response) {
|
|
|
4215
5379
|
async function readData(client, path, init) {
|
|
4216
5380
|
const response = await client.request(path, init);
|
|
4217
5381
|
const body = await readJson(response);
|
|
4218
|
-
if (!
|
|
5382
|
+
if (!isRecord3(body) || !("data" in body)) {
|
|
4219
5383
|
throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
|
|
4220
5384
|
}
|
|
4221
5385
|
return body.data;
|
|
@@ -4242,7 +5406,7 @@ async function createV2Session(client, directory) {
|
|
|
4242
5406
|
headers: { "Content-Type": "application/json" },
|
|
4243
5407
|
body: JSON.stringify({ location: { directory } })
|
|
4244
5408
|
});
|
|
4245
|
-
if (!
|
|
5409
|
+
if (!isRecord3(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
4246
5410
|
throw new Error("OpenCode V2 create session response was missing data.id");
|
|
4247
5411
|
}
|
|
4248
5412
|
return data.id;
|
|
@@ -4253,25 +5417,35 @@ async function getV2Session(client, sessionId) {
|
|
|
4253
5417
|
if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
|
|
4254
5418
|
return session;
|
|
4255
5419
|
}
|
|
4256
|
-
async function listV2SessionPage(client, cursor) {
|
|
5420
|
+
async function listV2SessionPage(client, cursor, signal) {
|
|
5421
|
+
const operation = cursor ? "GET /api/session (next page)" : "GET /api/session";
|
|
4257
5422
|
const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
|
|
4258
5423
|
try {
|
|
4259
|
-
|
|
5424
|
+
const response = await client.request(path, signal ? { signal } : void 0, {
|
|
5425
|
+
allowStatuses: ALL_HTTP_STATUSES2
|
|
5426
|
+
});
|
|
5427
|
+
if (!response.ok) {
|
|
5428
|
+
console.error(
|
|
5429
|
+
`[listV2SessionPage] ${operation} returned HTTP ${response.status} (port ${client.port})`
|
|
5430
|
+
);
|
|
5431
|
+
return null;
|
|
5432
|
+
}
|
|
5433
|
+
return adaptV2SessionList(await readJson(response));
|
|
4260
5434
|
} catch (error2) {
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
);
|
|
5435
|
+
if (signal?.aborted) return null;
|
|
5436
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
5437
|
+
console.error(`[listV2SessionPage] ${operation} failed (port ${client.port}): ${errorName}`);
|
|
4264
5438
|
return null;
|
|
4265
5439
|
}
|
|
4266
5440
|
}
|
|
4267
|
-
async function listV2Sessions(client) {
|
|
5441
|
+
async function listV2Sessions(client, signal) {
|
|
4268
5442
|
const sessions = [];
|
|
4269
5443
|
const seenCursors = /* @__PURE__ */ new Set();
|
|
4270
5444
|
let cursor;
|
|
4271
5445
|
let hasNextPage = true;
|
|
4272
5446
|
try {
|
|
4273
5447
|
while (hasNextPage) {
|
|
4274
|
-
const page = await listV2SessionPage(client, cursor);
|
|
5448
|
+
const page = await listV2SessionPage(client, cursor, signal);
|
|
4275
5449
|
if (!page) return null;
|
|
4276
5450
|
sessions.push(...page.data);
|
|
4277
5451
|
const next = page.cursor.next;
|
|
@@ -4287,8 +5461,9 @@ async function listV2Sessions(client) {
|
|
|
4287
5461
|
}
|
|
4288
5462
|
return sessions;
|
|
4289
5463
|
} catch (error2) {
|
|
5464
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
4290
5465
|
console.error(
|
|
4291
|
-
`[listV2Sessions] session pagination failed
|
|
5466
|
+
`[listV2Sessions] GET /api/session pagination failed (port ${client.port}): ${errorName}`
|
|
4292
5467
|
);
|
|
4293
5468
|
return null;
|
|
4294
5469
|
}
|
|
@@ -4298,7 +5473,7 @@ async function deleteV2Session(client, sessionId) {
|
|
|
4298
5473
|
const response = await client.request(
|
|
4299
5474
|
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
4300
5475
|
{ method: "DELETE" },
|
|
4301
|
-
{ allowStatuses:
|
|
5476
|
+
{ allowStatuses: ALL_HTTP_STATUSES2 }
|
|
4302
5477
|
);
|
|
4303
5478
|
if (response.status >= 200 && response.status < 300) {
|
|
4304
5479
|
return { ok: true, status: response.status };
|
|
@@ -4349,8 +5524,8 @@ async function sendV2Prompt(client, sessionId, text) {
|
|
|
4349
5524
|
`OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4350
5525
|
);
|
|
4351
5526
|
}
|
|
4352
|
-
const data =
|
|
4353
|
-
if (!
|
|
5527
|
+
const data = isRecord3(body) && "data" in body ? body.data : void 0;
|
|
5528
|
+
if (!isRecord3(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
4354
5529
|
throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
|
|
4355
5530
|
}
|
|
4356
5531
|
return data.id;
|
|
@@ -4359,7 +5534,7 @@ async function getV2SessionMessages(client, sessionId) {
|
|
|
4359
5534
|
const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
|
|
4360
5535
|
try {
|
|
4361
5536
|
const body = await readJson(await client.request(path));
|
|
4362
|
-
if (!
|
|
5537
|
+
if (!isRecord3(body) || !Array.isArray(body.data) || !isRecord3(body.cursor)) return null;
|
|
4363
5538
|
return adaptV2Messages(body);
|
|
4364
5539
|
} catch (error2) {
|
|
4365
5540
|
console.error(
|
|
@@ -4398,7 +5573,7 @@ async function interruptV2Session(client, sessionId) {
|
|
|
4398
5573
|
async function getV2ActiveSessions(client) {
|
|
4399
5574
|
try {
|
|
4400
5575
|
const body = await readJson(await client.request("/api/session/active"));
|
|
4401
|
-
if (!
|
|
5576
|
+
if (!isRecord3(body) || !isRecord3(body.data)) return null;
|
|
4402
5577
|
return body.data;
|
|
4403
5578
|
} catch (error2) {
|
|
4404
5579
|
console.error(
|
|
@@ -4414,8 +5589,8 @@ async function isV2SessionOngoing(client, sessionId) {
|
|
|
4414
5589
|
}
|
|
4415
5590
|
function sessionErrorReason2(value) {
|
|
4416
5591
|
if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
|
|
4417
|
-
if (
|
|
4418
|
-
const data =
|
|
5592
|
+
if (isRecord3(value)) {
|
|
5593
|
+
const data = isRecord3(value.data) ? value.data : void 0;
|
|
4419
5594
|
const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
|
|
4420
5595
|
if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
4421
5596
|
}
|
|
@@ -4431,17 +5606,17 @@ function adaptV2SessionErrorEvent(value) {
|
|
|
4431
5606
|
return null;
|
|
4432
5607
|
}
|
|
4433
5608
|
}
|
|
4434
|
-
if (!
|
|
5609
|
+
if (!isRecord3(parsed)) return null;
|
|
4435
5610
|
try {
|
|
4436
5611
|
const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
|
|
4437
5612
|
if (establishedShape) return establishedShape;
|
|
4438
5613
|
} catch (error2) {
|
|
4439
5614
|
void error2;
|
|
4440
5615
|
}
|
|
4441
|
-
const candidates = [parsed, parsed.payload, parsed.data].filter(
|
|
5616
|
+
const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord3);
|
|
4442
5617
|
for (const event of candidates) {
|
|
4443
5618
|
if (event.type !== "session.error") continue;
|
|
4444
|
-
const properties = [event.properties, event.data, event].find(
|
|
5619
|
+
const properties = [event.properties, event.data, event].find(isRecord3);
|
|
4445
5620
|
if (!properties) continue;
|
|
4446
5621
|
const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
|
|
4447
5622
|
if (!sessionId) continue;
|
|
@@ -4491,13 +5666,13 @@ async function readV2SessionErrorStream(client, options) {
|
|
|
4491
5666
|
}
|
|
4492
5667
|
|
|
4493
5668
|
// src/lib/opencode/credential-import-v2.ts
|
|
4494
|
-
import { readFileSync as
|
|
4495
|
-
import { join as
|
|
5669
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
5670
|
+
import { join as join8 } from "node:path";
|
|
4496
5671
|
|
|
4497
5672
|
// src/lib/openai-usage.ts
|
|
4498
|
-
import { readFileSync as
|
|
5673
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4499
5674
|
import { homedir as homedir3 } from "node:os";
|
|
4500
|
-
import { join as
|
|
5675
|
+
import { join as join7 } from "node:path";
|
|
4501
5676
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
4502
5677
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
4503
5678
|
var OpenAiUsageError = class extends Error {
|
|
@@ -4511,7 +5686,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
4511
5686
|
}
|
|
4512
5687
|
function readOpenCodeChatGptCredentials() {
|
|
4513
5688
|
try {
|
|
4514
|
-
const raw =
|
|
5689
|
+
const raw = readFileSync7(join7(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
4515
5690
|
let parsed;
|
|
4516
5691
|
try {
|
|
4517
5692
|
parsed = JSON.parse(raw);
|
|
@@ -4577,7 +5752,7 @@ function parseCodexUsageHeaders(headers) {
|
|
|
4577
5752
|
function normalizeProbeModel(model) {
|
|
4578
5753
|
return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
|
|
4579
5754
|
}
|
|
4580
|
-
function
|
|
5755
|
+
function isRecord4(value) {
|
|
4581
5756
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4582
5757
|
}
|
|
4583
5758
|
function unsupportedProbeModels(reason, port) {
|
|
@@ -4588,14 +5763,14 @@ async function resolveV1ProbeModels(client, port) {
|
|
|
4588
5763
|
try {
|
|
4589
5764
|
const response = await client.request("/config/providers");
|
|
4590
5765
|
const body = await response.json();
|
|
4591
|
-
if (!
|
|
5766
|
+
if (!isRecord4(body) || !Array.isArray(body.providers)) {
|
|
4592
5767
|
return unsupportedProbeModels("V1 provider response did not contain a providers array", port);
|
|
4593
5768
|
}
|
|
4594
5769
|
const provider = body.providers.find(
|
|
4595
|
-
(candidate) =>
|
|
5770
|
+
(candidate) => isRecord4(candidate) && candidate.id === "openai"
|
|
4596
5771
|
);
|
|
4597
|
-
if (!provider || !
|
|
4598
|
-
const defaults2 =
|
|
5772
|
+
if (!provider || !isRecord4(provider.models)) return { status: "supported", models: [] };
|
|
5773
|
+
const defaults2 = isRecord4(body.default) ? body.default : void 0;
|
|
4599
5774
|
const candidates = [
|
|
4600
5775
|
...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
|
|
4601
5776
|
...Object.keys(provider.models)
|
|
@@ -4617,14 +5792,14 @@ async function resolveV2ProbeModels(client, port) {
|
|
|
4617
5792
|
try {
|
|
4618
5793
|
const response = await client.request(path);
|
|
4619
5794
|
const body = await response.json();
|
|
4620
|
-
if (!
|
|
5795
|
+
if (!isRecord4(body) || !Array.isArray(body.data)) {
|
|
4621
5796
|
return unsupportedProbeModels(`V2 GET ${path} did not contain a provider data array`, port);
|
|
4622
5797
|
}
|
|
4623
5798
|
const provider = body.data.find(
|
|
4624
|
-
(candidate) =>
|
|
5799
|
+
(candidate) => isRecord4(candidate) && candidate.id === "openai"
|
|
4625
5800
|
);
|
|
4626
5801
|
if (!provider) return { status: "supported", models: [] };
|
|
4627
|
-
if (!
|
|
5802
|
+
if (!isRecord4(provider.models)) {
|
|
4628
5803
|
return unsupportedProbeModels(
|
|
4629
5804
|
"V2 provider response has no safe OpenAI model catalogue",
|
|
4630
5805
|
port
|
|
@@ -4734,7 +5909,7 @@ var CONNECT_KEY_MAX_ATTEMPTS = 3;
|
|
|
4734
5909
|
var CONNECT_KEY_RETRY_DELAY_MS = 250;
|
|
4735
5910
|
var INTEGRATION_LIST_MAX_READS = 10;
|
|
4736
5911
|
var INTEGRATION_LIST_RETRY_DELAY_MS = 1e3;
|
|
4737
|
-
function
|
|
5912
|
+
function isRecord5(value) {
|
|
4738
5913
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4739
5914
|
}
|
|
4740
5915
|
var sleep2 = (milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds));
|
|
@@ -4743,14 +5918,14 @@ function emptyResult(unsupported = [], outcome = "completed") {
|
|
|
4743
5918
|
}
|
|
4744
5919
|
function readOpenCodeCredentials(homeDir, log3) {
|
|
4745
5920
|
try {
|
|
4746
|
-
const raw =
|
|
5921
|
+
const raw = readFileSync8(join8(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
|
|
4747
5922
|
let parsed;
|
|
4748
5923
|
try {
|
|
4749
5924
|
parsed = JSON.parse(raw);
|
|
4750
5925
|
} catch (error2) {
|
|
4751
5926
|
return null;
|
|
4752
5927
|
}
|
|
4753
|
-
return
|
|
5928
|
+
return isRecord5(parsed) ? parsed : null;
|
|
4754
5929
|
} catch (error2) {
|
|
4755
5930
|
const code = error2.code;
|
|
4756
5931
|
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
@@ -4763,7 +5938,7 @@ function classifyOpenCodeCredentials(credentials2) {
|
|
|
4763
5938
|
const importable = [];
|
|
4764
5939
|
const unsupported = [];
|
|
4765
5940
|
for (const [integrationId, value] of Object.entries(credentials2)) {
|
|
4766
|
-
if (!
|
|
5941
|
+
if (!isRecord5(value) || typeof value.type !== "string") {
|
|
4767
5942
|
unsupported.push({ integrationId, reason: "unrecognised credential entry" });
|
|
4768
5943
|
continue;
|
|
4769
5944
|
}
|
|
@@ -4778,7 +5953,7 @@ function classifyOpenCodeCredentials(credentials2) {
|
|
|
4778
5953
|
unsupported.push({ integrationId, reason: "unrecognised credential entry" });
|
|
4779
5954
|
continue;
|
|
4780
5955
|
}
|
|
4781
|
-
if (value.metadata === void 0 ||
|
|
5956
|
+
if (value.metadata === void 0 || isRecord5(value.metadata) && Object.keys(value.metadata).length === 0) {
|
|
4782
5957
|
importable.push({ integrationId, key: value.key });
|
|
4783
5958
|
} else {
|
|
4784
5959
|
unsupported.push({ integrationId, reason: "needs the interactive connect flow" });
|
|
@@ -4791,7 +5966,7 @@ function statusFromRequestError(error2) {
|
|
|
4791
5966
|
const match = /^OpenCode request failed: HTTP (\d{3})/.exec(error2.message);
|
|
4792
5967
|
return match ? Number(match[1]) : void 0;
|
|
4793
5968
|
}
|
|
4794
|
-
function
|
|
5969
|
+
function failureMessage2(status2) {
|
|
4795
5970
|
return status2 === void 0 ? "OpenCode credential import request failed" : `OpenCode credential import request failed with HTTP ${status2}`;
|
|
4796
5971
|
}
|
|
4797
5972
|
function isRetryableStatus(status2) {
|
|
@@ -4818,18 +5993,18 @@ async function importOneCredential(request3, credential, sleepForRetry, log3) {
|
|
|
4818
5993
|
const failure = {
|
|
4819
5994
|
integrationId: credential.integrationId,
|
|
4820
5995
|
status: response.status,
|
|
4821
|
-
message:
|
|
5996
|
+
message: failureMessage2(response.status)
|
|
4822
5997
|
};
|
|
4823
|
-
log3(`${
|
|
5998
|
+
log3(`${failureMessage2(failure.status)} for ${failure.integrationId}`, "warn");
|
|
4824
5999
|
return { ok: false, failure };
|
|
4825
6000
|
} catch (error2) {
|
|
4826
6001
|
const status2 = statusFromRequestError(error2);
|
|
4827
6002
|
const failure = {
|
|
4828
6003
|
integrationId: credential.integrationId,
|
|
4829
6004
|
...status2 === void 0 ? {} : { status: status2 },
|
|
4830
|
-
message:
|
|
6005
|
+
message: failureMessage2(status2)
|
|
4831
6006
|
};
|
|
4832
|
-
log3(`${
|
|
6007
|
+
log3(`${failureMessage2(status2)} for ${failure.integrationId}`, "warn");
|
|
4833
6008
|
return { ok: false, failure };
|
|
4834
6009
|
}
|
|
4835
6010
|
}
|
|
@@ -4855,10 +6030,10 @@ async function importOpenCodeCredentials({
|
|
|
4855
6030
|
return result;
|
|
4856
6031
|
}
|
|
4857
6032
|
function parseIntegrationList(value) {
|
|
4858
|
-
if (!
|
|
6033
|
+
if (!isRecord5(value) || !Array.isArray(value.data)) return null;
|
|
4859
6034
|
const integrationIds = /* @__PURE__ */ new Set();
|
|
4860
6035
|
for (const integration of value.data) {
|
|
4861
|
-
if (!
|
|
6036
|
+
if (!isRecord5(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
4862
6037
|
return null;
|
|
4863
6038
|
}
|
|
4864
6039
|
if (integration.connections.length > 0) integrationIds.add(integration.id);
|
|
@@ -4943,87 +6118,9 @@ async function importOpenCodeV2Credentials({
|
|
|
4943
6118
|
|
|
4944
6119
|
// src/lib/opencode/session-db-boot.ts
|
|
4945
6120
|
import { spawn as spawn2 } from "node:child_process";
|
|
4946
|
-
import { mkdirSync, statSync as statSync3, unlinkSync as
|
|
6121
|
+
import { mkdirSync as mkdirSync4, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4947
6122
|
import { homedir as homedir4 } from "node:os";
|
|
4948
|
-
import { dirname as
|
|
4949
|
-
|
|
4950
|
-
// src/lib/runner-synchroniser.ts
|
|
4951
|
-
import { spawn } from "node:child_process";
|
|
4952
|
-
function appendError(stderr, error2) {
|
|
4953
|
-
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
4954
|
-
return stderr === "" ? message : `${stderr}
|
|
4955
|
-
${message}`;
|
|
4956
|
-
}
|
|
4957
|
-
function runSynchroniser(args, opts) {
|
|
4958
|
-
return new Promise((resolve5) => {
|
|
4959
|
-
let child;
|
|
4960
|
-
let stdout = "";
|
|
4961
|
-
let stderr = "";
|
|
4962
|
-
let settled = false;
|
|
4963
|
-
const timer = {};
|
|
4964
|
-
let abortListener;
|
|
4965
|
-
let spawnListener;
|
|
4966
|
-
const finish = (result) => {
|
|
4967
|
-
if (settled) return;
|
|
4968
|
-
settled = true;
|
|
4969
|
-
if (timer.handle) clearTimeout(timer.handle);
|
|
4970
|
-
if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
|
|
4971
|
-
if (spawnListener) child.removeListener("spawn", spawnListener);
|
|
4972
|
-
resolve5(result);
|
|
4973
|
-
};
|
|
4974
|
-
try {
|
|
4975
|
-
child = spawn("runner-synchroniser", args, {
|
|
4976
|
-
env: opts.env ?? process.env,
|
|
4977
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
4978
|
-
});
|
|
4979
|
-
} catch (error2) {
|
|
4980
|
-
finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
|
|
4981
|
-
return;
|
|
4982
|
-
}
|
|
4983
|
-
child.stdout?.setEncoding("utf8");
|
|
4984
|
-
child.stdout?.on("data", (chunk) => {
|
|
4985
|
-
stdout += chunk;
|
|
4986
|
-
});
|
|
4987
|
-
child.stderr?.setEncoding("utf8");
|
|
4988
|
-
child.stderr?.on("data", (chunk) => {
|
|
4989
|
-
stderr += chunk;
|
|
4990
|
-
});
|
|
4991
|
-
child.once("error", (error2) => {
|
|
4992
|
-
finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
|
|
4993
|
-
});
|
|
4994
|
-
child.once("close", (code) => {
|
|
4995
|
-
finish({ code, stdout, stderr, timedOut: false });
|
|
4996
|
-
});
|
|
4997
|
-
if (opts.signal) {
|
|
4998
|
-
const killChild = () => {
|
|
4999
|
-
if (child.pid === void 0) {
|
|
5000
|
-
if (!spawnListener) {
|
|
5001
|
-
spawnListener = killChild;
|
|
5002
|
-
child.once("spawn", spawnListener);
|
|
5003
|
-
}
|
|
5004
|
-
return;
|
|
5005
|
-
}
|
|
5006
|
-
child.kill("SIGKILL");
|
|
5007
|
-
};
|
|
5008
|
-
abortListener = killChild;
|
|
5009
|
-
if (opts.signal.aborted) {
|
|
5010
|
-
abortListener();
|
|
5011
|
-
} else {
|
|
5012
|
-
opts.signal.addEventListener("abort", abortListener, { once: true });
|
|
5013
|
-
if (opts.signal.aborted) abortListener();
|
|
5014
|
-
}
|
|
5015
|
-
}
|
|
5016
|
-
timer.handle = setTimeout(
|
|
5017
|
-
() => {
|
|
5018
|
-
child.kill("SIGKILL");
|
|
5019
|
-
finish({ code: null, stdout, stderr, timedOut: true });
|
|
5020
|
-
},
|
|
5021
|
-
Math.max(0, opts.timeoutMs)
|
|
5022
|
-
);
|
|
5023
|
-
});
|
|
5024
|
-
}
|
|
5025
|
-
|
|
5026
|
-
// src/lib/opencode/session-db-boot.ts
|
|
6123
|
+
import { dirname as dirname7, resolve as resolvePath } from "node:path";
|
|
5027
6124
|
var SESSION_DB_RESTORE_TIMEOUT_MS = 3e5;
|
|
5028
6125
|
var SESSION_DB_VERIFY_TIMEOUT_MS = 36e4;
|
|
5029
6126
|
var SESSION_DB_SYNCHRONISER_TIMEOUT_MS = 12e4;
|
|
@@ -5061,7 +6158,7 @@ function reportRecord(stage, outcome, reason, litestreamExitCode, options) {
|
|
|
5061
6158
|
function clearMarker(options) {
|
|
5062
6159
|
if (!options.noReplicateMarker) return;
|
|
5063
6160
|
try {
|
|
5064
|
-
|
|
6161
|
+
unlinkSync3(options.noReplicateMarker);
|
|
5065
6162
|
} catch (error2) {
|
|
5066
6163
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return;
|
|
5067
6164
|
options.log(
|
|
@@ -5073,8 +6170,8 @@ function clearMarker(options) {
|
|
|
5073
6170
|
function markNoReplicate(options, message) {
|
|
5074
6171
|
if (options.noReplicateMarker) {
|
|
5075
6172
|
try {
|
|
5076
|
-
|
|
5077
|
-
|
|
6173
|
+
mkdirSync4(dirname7(options.noReplicateMarker), { recursive: true });
|
|
6174
|
+
writeFileSync4(options.noReplicateMarker, "");
|
|
5078
6175
|
} catch (error2) {
|
|
5079
6176
|
options.log(
|
|
5080
6177
|
`Could not write session-DB no-replicate marker ${options.noReplicateMarker}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
@@ -5087,7 +6184,7 @@ function markNoReplicate(options, message) {
|
|
|
5087
6184
|
function discardSessionDbDebris(options) {
|
|
5088
6185
|
for (const path of [options.dbPath, `${options.dbPath}-wal`, `${options.dbPath}-shm`]) {
|
|
5089
6186
|
try {
|
|
5090
|
-
|
|
6187
|
+
unlinkSync3(path);
|
|
5091
6188
|
} catch (error2) {
|
|
5092
6189
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") continue;
|
|
5093
6190
|
options.log(
|
|
@@ -5228,8 +6325,8 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
5228
6325
|
return null;
|
|
5229
6326
|
}
|
|
5230
6327
|
try {
|
|
5231
|
-
|
|
5232
|
-
|
|
6328
|
+
mkdirSync4(dirname7(configPath), { recursive: true });
|
|
6329
|
+
writeFileSync4(configPath, rendered.stdout);
|
|
5233
6330
|
} catch (error2) {
|
|
5234
6331
|
options.log(
|
|
5235
6332
|
`Could not write ${configPath}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
@@ -5494,8 +6591,8 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
5494
6591
|
|
|
5495
6592
|
// src/lib/opencode/session-db-provenance.ts
|
|
5496
6593
|
import { createRequire } from "node:module";
|
|
5497
|
-
import { mkdirSync as
|
|
5498
|
-
import { dirname as
|
|
6594
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6595
|
+
import { dirname as dirname8, join as join9 } from "node:path";
|
|
5499
6596
|
var require2 = createRequire(import.meta.url);
|
|
5500
6597
|
function readSessionDbMigrationIds(dbPath) {
|
|
5501
6598
|
let db;
|
|
@@ -5532,12 +6629,12 @@ function readSessionDbMigrationIds(dbPath) {
|
|
|
5532
6629
|
}
|
|
5533
6630
|
function sessionDbProvenanceStatePath(homeDir, env) {
|
|
5534
6631
|
const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
|
|
5535
|
-
return override ||
|
|
6632
|
+
return override || join9(homeDir, ".local", "state", "evident", "session-db-provenance.json");
|
|
5536
6633
|
}
|
|
5537
6634
|
function loadSessionDbProvenanceState(path) {
|
|
5538
6635
|
let value;
|
|
5539
6636
|
try {
|
|
5540
|
-
value = JSON.parse(
|
|
6637
|
+
value = JSON.parse(readFileSync9(path, "utf8"));
|
|
5541
6638
|
} catch (error2) {
|
|
5542
6639
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return {};
|
|
5543
6640
|
console.error(
|
|
@@ -5561,8 +6658,8 @@ function loadSessionDbProvenanceState(path) {
|
|
|
5561
6658
|
}
|
|
5562
6659
|
function saveSessionDbProvenanceState(path, state) {
|
|
5563
6660
|
try {
|
|
5564
|
-
|
|
5565
|
-
|
|
6661
|
+
mkdirSync5(dirname8(path), { recursive: true });
|
|
6662
|
+
writeFileSync5(path, `${JSON.stringify(state, null, 2)}
|
|
5566
6663
|
`, "utf8");
|
|
5567
6664
|
} catch (error2) {
|
|
5568
6665
|
console.error(
|
|
@@ -5617,6 +6714,26 @@ function isSessionDbProvenanceRecord(value) {
|
|
|
5617
6714
|
return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
|
|
5618
6715
|
}
|
|
5619
6716
|
|
|
6717
|
+
// src/lib/opencode/detect.ts
|
|
6718
|
+
function describeProbe(route, result) {
|
|
6719
|
+
if (result.error?.startsWith("HTTP ")) return `${route}: ${result.error}`;
|
|
6720
|
+
if (result.error) return `${route}: no HTTP response (${result.error})`;
|
|
6721
|
+
return `${route}: ${result.healthy ? "healthy" : "no response detail"}`;
|
|
6722
|
+
}
|
|
6723
|
+
async function classifyRunningOpenCode(port) {
|
|
6724
|
+
const v1 = await checkOpenCodeHealth(port);
|
|
6725
|
+
if (v1.healthy) return { kind: "v1", version: v1.version ?? null };
|
|
6726
|
+
const v2 = await checkOpenCode2Health(port);
|
|
6727
|
+
if (v2.healthy) return { kind: "v2", version: v2.version ?? null };
|
|
6728
|
+
const detail = [
|
|
6729
|
+
describeProbe("V1 /global/health", v1),
|
|
6730
|
+
describeProbe("V2 /api/info (or /api/health fallback)", v2)
|
|
6731
|
+
].join("; ");
|
|
6732
|
+
if (v1.okWithNonJsonBody && v2.authFailed) return { kind: "v2", version: null };
|
|
6733
|
+
if (v1.authFailed && v2.authFailed) return { kind: "authenticated-unknown", detail };
|
|
6734
|
+
return { kind: "no-server-identified", detail };
|
|
6735
|
+
}
|
|
6736
|
+
|
|
5620
6737
|
// src/lib/opencode/opencode2-package.ts
|
|
5621
6738
|
var OPENCODE2_PACKAGE_SPEC = "@opencode-ai/cli@0.0.0-beta-17823";
|
|
5622
6739
|
|
|
@@ -5925,26 +7042,28 @@ async function startOpenCode(port, options = {}) {
|
|
|
5925
7042
|
}
|
|
5926
7043
|
async function startOpenCode2(port, options = {}) {
|
|
5927
7044
|
const password = randomBytes(24).toString("hex");
|
|
5928
|
-
let command = "opencode2";
|
|
7045
|
+
let command = options.command ?? "opencode2";
|
|
5929
7046
|
const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
|
|
5930
7047
|
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...logLevel];
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
7048
|
+
if (options.command === void 0) {
|
|
7049
|
+
try {
|
|
7050
|
+
execSync("which opencode2", { stdio: "ignore" });
|
|
7051
|
+
} catch {
|
|
7052
|
+
command = "npx";
|
|
7053
|
+
args = [
|
|
7054
|
+
"-y",
|
|
7055
|
+
"-p",
|
|
7056
|
+
OPENCODE2_PACKAGE_SPEC,
|
|
7057
|
+
"--",
|
|
7058
|
+
"opencode2",
|
|
7059
|
+
"serve",
|
|
7060
|
+
"--port",
|
|
7061
|
+
port.toString(),
|
|
7062
|
+
"--hostname",
|
|
7063
|
+
"127.0.0.1",
|
|
7064
|
+
...logLevel
|
|
7065
|
+
];
|
|
7066
|
+
}
|
|
5948
7067
|
}
|
|
5949
7068
|
const child = spawn3(command, args, {
|
|
5950
7069
|
env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
|
|
@@ -6001,6 +7120,56 @@ function isOpenCode2Installed() {
|
|
|
6001
7120
|
return false;
|
|
6002
7121
|
}
|
|
6003
7122
|
}
|
|
7123
|
+
function readVersionOutput(command) {
|
|
7124
|
+
try {
|
|
7125
|
+
return execSync2(`${command} --version`, {
|
|
7126
|
+
encoding: "utf-8",
|
|
7127
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
7128
|
+
}).trim();
|
|
7129
|
+
} catch (error2) {
|
|
7130
|
+
const output = typeof error2 === "object" && error2 !== null ? [
|
|
7131
|
+
"stdout" in error2 ? String(error2.stdout ?? "").trim() : "",
|
|
7132
|
+
"stderr" in error2 ? String(error2.stderr ?? "").trim() : ""
|
|
7133
|
+
].filter(Boolean).join("\n") : "";
|
|
7134
|
+
return output || (error2 instanceof Error ? error2.message : String(error2));
|
|
7135
|
+
}
|
|
7136
|
+
}
|
|
7137
|
+
function parseOpenCodeVersion(rawVersion) {
|
|
7138
|
+
const output = rawVersion.trim();
|
|
7139
|
+
const prefix = output.match(/^(opencode2|opencode)\s+/i)?.[1]?.toLowerCase();
|
|
7140
|
+
const version2 = (prefix ? output.slice(prefix.length).trim() : output).replace(/^v/i, "");
|
|
7141
|
+
if (prefix === "opencode2") return { major: "v2", version: version2 };
|
|
7142
|
+
const major = version2.match(/^(\d+)(?:\.\d+)*(?:[-+][0-9A-Za-z.-]+)?$/)?.[1];
|
|
7143
|
+
if (!major) return null;
|
|
7144
|
+
if (Number(major) === 1) return { major: "v1", version: version2 };
|
|
7145
|
+
if (Number(major) >= 2) return { major: "v2", version: version2 };
|
|
7146
|
+
return null;
|
|
7147
|
+
}
|
|
7148
|
+
function resolveInstalledOpenCode() {
|
|
7149
|
+
let opencode2Available = false;
|
|
7150
|
+
if (isOpenCodeInstalled()) {
|
|
7151
|
+
const rawVersion = readVersionOutput("opencode");
|
|
7152
|
+
const parsed = parseOpenCodeVersion(rawVersion);
|
|
7153
|
+
if (parsed?.major === "v1") {
|
|
7154
|
+
return { kind: "installed", major: "v1", command: "opencode", version: parsed.version };
|
|
7155
|
+
}
|
|
7156
|
+
if (parsed?.major === "v2") {
|
|
7157
|
+
return { kind: "installed", major: "v2", command: "opencode", version: parsed.version };
|
|
7158
|
+
}
|
|
7159
|
+
opencode2Available = isOpenCode2Installed();
|
|
7160
|
+
if (!opencode2Available) return { kind: "unknown", command: "opencode", rawVersion };
|
|
7161
|
+
}
|
|
7162
|
+
if (opencode2Available || isOpenCode2Installed()) {
|
|
7163
|
+
const parsed = parseOpenCodeVersion(readVersionOutput("opencode2"));
|
|
7164
|
+
return {
|
|
7165
|
+
kind: "installed",
|
|
7166
|
+
major: "v2",
|
|
7167
|
+
command: "opencode2",
|
|
7168
|
+
version: parsed?.version
|
|
7169
|
+
};
|
|
7170
|
+
}
|
|
7171
|
+
return { kind: "none" };
|
|
7172
|
+
}
|
|
6004
7173
|
async function promptOpenCodeInstall(interactive) {
|
|
6005
7174
|
if (!interactive) {
|
|
6006
7175
|
console.log(
|
|
@@ -6095,130 +7264,23 @@ async function promptOpenCodeInstall(interactive) {
|
|
|
6095
7264
|
return action;
|
|
6096
7265
|
}
|
|
6097
7266
|
|
|
6098
|
-
// src/lib/opencode/provider-check.ts
|
|
6099
|
-
function buildNoProviderWarning(hasProvider) {
|
|
6100
|
-
if (hasProvider !== false) return null;
|
|
6101
|
-
return "Warning: opencode has no authenticated model provider configured, so it won't be able to answer prompts. Run `opencode auth login` to set one up (see https://opencode.ai for details).";
|
|
6102
|
-
}
|
|
6103
|
-
|
|
6104
|
-
// src/lib/opencode/session-cleanup.ts
|
|
6105
|
-
var DURATION_UNIT_MS = {
|
|
6106
|
-
s: 1e3,
|
|
6107
|
-
m: 60 * 1e3,
|
|
6108
|
-
h: 60 * 60 * 1e3,
|
|
6109
|
-
d: 24 * 60 * 60 * 1e3
|
|
6110
|
-
};
|
|
6111
|
-
function parseDurationMs(input) {
|
|
6112
|
-
const trimmed = input.trim();
|
|
6113
|
-
const match = /^(\d+)([smhd])$/.exec(trimmed);
|
|
6114
|
-
if (!match) {
|
|
6115
|
-
throw new Error(
|
|
6116
|
-
`Invalid duration "${input}": expected <number><unit> where unit is one of s, m, h, d (e.g. "7d", "24h", "30m", "90s").`
|
|
6117
|
-
);
|
|
6118
|
-
}
|
|
6119
|
-
const value = Number(match[1]);
|
|
6120
|
-
if (value <= 0) {
|
|
6121
|
-
throw new Error(`Invalid duration "${input}": must be a positive value.`);
|
|
6122
|
-
}
|
|
6123
|
-
return value * DURATION_UNIT_MS[match[2]];
|
|
6124
|
-
}
|
|
6125
|
-
function selectSessionsToDelete(sessions, opts) {
|
|
6126
|
-
const { maxAgeMs, maxCount, nowMs, protectedIds } = opts;
|
|
6127
|
-
if (maxAgeMs === void 0 && maxCount === void 0) return [];
|
|
6128
|
-
const ageEligible = (s) => {
|
|
6129
|
-
if (maxAgeMs === void 0) return false;
|
|
6130
|
-
if (s.lastActivityMs === null) return true;
|
|
6131
|
-
return nowMs - s.lastActivityMs > maxAgeMs;
|
|
6132
|
-
};
|
|
6133
|
-
const countEligibleIds = /* @__PURE__ */ new Set();
|
|
6134
|
-
if (maxCount !== void 0) {
|
|
6135
|
-
const byActivityDesc = [...sessions].sort(
|
|
6136
|
-
(a, b) => (b.lastActivityMs ?? -Infinity) - (a.lastActivityMs ?? -Infinity)
|
|
6137
|
-
);
|
|
6138
|
-
for (const s of byActivityDesc.slice(maxCount)) {
|
|
6139
|
-
countEligibleIds.add(s.id);
|
|
6140
|
-
}
|
|
6141
|
-
}
|
|
6142
|
-
const toDelete = [];
|
|
6143
|
-
for (const s of sessions) {
|
|
6144
|
-
if (protectedIds.has(s.id)) continue;
|
|
6145
|
-
if (ageEligible(s) || countEligibleIds.has(s.id)) {
|
|
6146
|
-
toDelete.push(s.id);
|
|
6147
|
-
}
|
|
6148
|
-
}
|
|
6149
|
-
const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
|
|
6150
|
-
return toDelete.sort(
|
|
6151
|
-
(a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
|
|
6152
|
-
);
|
|
6153
|
-
}
|
|
6154
|
-
var DEFAULT_INTERVAL = "1h";
|
|
6155
|
-
function resolve2(flag, envValue, fallback) {
|
|
6156
|
-
return flag ?? envValue ?? fallback;
|
|
6157
|
-
}
|
|
6158
|
-
function parseMaxCount(input) {
|
|
6159
|
-
const trimmed = input.trim();
|
|
6160
|
-
if (!/^\d+$/.test(trimmed)) {
|
|
6161
|
-
throw new Error(`Invalid max-count "${input}": expected a positive integer.`);
|
|
6162
|
-
}
|
|
6163
|
-
const value = Number(trimmed);
|
|
6164
|
-
if (value <= 0) {
|
|
6165
|
-
throw new Error(`Invalid max-count "${input}": must be greater than 0.`);
|
|
6166
|
-
}
|
|
6167
|
-
return value;
|
|
6168
|
-
}
|
|
6169
|
-
function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
6170
|
-
const warnings = [];
|
|
6171
|
-
const maxAgeRaw = resolve2(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
|
|
6172
|
-
const maxCountRaw = resolve2(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
|
|
6173
|
-
const intervalRaw = resolve2(
|
|
6174
|
-
flags.interval,
|
|
6175
|
-
env.EVIDENT_SESSION_CLEANUP_INTERVAL,
|
|
6176
|
-
DEFAULT_INTERVAL
|
|
6177
|
-
);
|
|
6178
|
-
let maxAgeMs;
|
|
6179
|
-
if (maxAgeRaw !== void 0) {
|
|
6180
|
-
try {
|
|
6181
|
-
maxAgeMs = parseDurationMs(maxAgeRaw);
|
|
6182
|
-
} catch (err) {
|
|
6183
|
-
warnings.push(
|
|
6184
|
-
`Ignoring invalid --session-cleanup-max-age: ${err instanceof Error ? err.message : String(err)}`
|
|
6185
|
-
);
|
|
6186
|
-
}
|
|
6187
|
-
}
|
|
6188
|
-
let maxCount;
|
|
6189
|
-
if (maxCountRaw !== void 0) {
|
|
6190
|
-
try {
|
|
6191
|
-
maxCount = parseMaxCount(maxCountRaw);
|
|
6192
|
-
} catch (err) {
|
|
6193
|
-
warnings.push(
|
|
6194
|
-
`Ignoring invalid --session-cleanup-max-count: ${err instanceof Error ? err.message : String(err)}`
|
|
6195
|
-
);
|
|
6196
|
-
}
|
|
6197
|
-
}
|
|
6198
|
-
let intervalMs;
|
|
6199
|
-
try {
|
|
6200
|
-
intervalMs = parseDurationMs(intervalRaw ?? DEFAULT_INTERVAL);
|
|
6201
|
-
} catch (err) {
|
|
6202
|
-
warnings.push(
|
|
6203
|
-
`Ignoring invalid --session-cleanup-interval, using default ${DEFAULT_INTERVAL}: ${err instanceof Error ? err.message : String(err)}`
|
|
6204
|
-
);
|
|
6205
|
-
intervalMs = parseDurationMs(DEFAULT_INTERVAL);
|
|
6206
|
-
}
|
|
6207
|
-
const enabled = maxAgeMs !== void 0 || maxCount !== void 0;
|
|
6208
|
-
return { enabled, maxAgeMs, maxCount, intervalMs, warnings };
|
|
7267
|
+
// src/lib/opencode/provider-check.ts
|
|
7268
|
+
function buildNoProviderWarning(hasProvider) {
|
|
7269
|
+
if (hasProvider !== false) return null;
|
|
7270
|
+
return "Warning: opencode has no authenticated model provider configured, so it won't be able to answer prompts. Run `opencode auth login` to set one up (see https://opencode.ai for details).";
|
|
6209
7271
|
}
|
|
6210
7272
|
|
|
6211
7273
|
// src/lib/opencode/session-db-size.ts
|
|
6212
7274
|
import { statSync as statSync4 } from "node:fs";
|
|
6213
|
-
import { join as
|
|
7275
|
+
import { join as join10 } from "node:path";
|
|
6214
7276
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
6215
7277
|
function statSessionDbBytes(homeDir) {
|
|
6216
|
-
const dbPath =
|
|
7278
|
+
const dbPath = join10(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
6217
7279
|
try {
|
|
6218
7280
|
return statSync4(dbPath).size;
|
|
6219
7281
|
} catch (err) {
|
|
6220
|
-
const
|
|
6221
|
-
if (!
|
|
7282
|
+
const isMissingFile4 = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
7283
|
+
if (!isMissingFile4) {
|
|
6222
7284
|
console.error(
|
|
6223
7285
|
`[statSessionDbBytes] could not stat ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
6224
7286
|
);
|
|
@@ -6243,12 +7305,12 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
6243
7305
|
// src/lib/opencode/log-tail.ts
|
|
6244
7306
|
import { statSync as statSync5 } from "node:fs";
|
|
6245
7307
|
import { homedir as homedir5 } from "node:os";
|
|
6246
|
-
import { join as
|
|
7308
|
+
import { join as join11 } from "node:path";
|
|
6247
7309
|
import { open as open2, stat } from "node:fs/promises";
|
|
6248
7310
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
6249
7311
|
function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
|
|
6250
|
-
const dataDir = env.XDG_DATA_HOME ||
|
|
6251
|
-
return
|
|
7312
|
+
const dataDir = env.XDG_DATA_HOME || join11(homeDir, ".local", "share");
|
|
7313
|
+
return join11(dataDir, "opencode", "log", "opencode.log");
|
|
6252
7314
|
}
|
|
6253
7315
|
function isEnoent(error2) {
|
|
6254
7316
|
return error2?.code === "ENOENT";
|
|
@@ -6733,8 +7795,8 @@ function connectTunnel(options) {
|
|
|
6733
7795
|
}
|
|
6734
7796
|
forwarder.handleBinaryBodyFrame(frame.sid, frame.payload);
|
|
6735
7797
|
} catch (error2) {
|
|
6736
|
-
const
|
|
6737
|
-
onError?.(`Failed to handle binary message: ${
|
|
7798
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
7799
|
+
onError?.(`Failed to handle binary message: ${errorMessage4}`);
|
|
6738
7800
|
}
|
|
6739
7801
|
return;
|
|
6740
7802
|
}
|
|
@@ -6742,8 +7804,8 @@ function connectTunnel(options) {
|
|
|
6742
7804
|
try {
|
|
6743
7805
|
message = JSON.parse(data.toString());
|
|
6744
7806
|
} catch (error2) {
|
|
6745
|
-
const
|
|
6746
|
-
onError?.(`Failed to handle message: ${
|
|
7807
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
7808
|
+
onError?.(`Failed to handle message: ${errorMessage4}`);
|
|
6747
7809
|
return;
|
|
6748
7810
|
}
|
|
6749
7811
|
if (isStreamFrame(message)) {
|
|
@@ -6915,10 +7977,10 @@ var RunnerConnection = class _RunnerConnection {
|
|
|
6915
7977
|
};
|
|
6916
7978
|
|
|
6917
7979
|
// src/lib/tunnel/ready-marker.ts
|
|
6918
|
-
import { writeFileSync as
|
|
7980
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
6919
7981
|
function writeTunnelReadyMarker(path, agentId) {
|
|
6920
7982
|
try {
|
|
6921
|
-
|
|
7983
|
+
writeFileSync6(path, `${agentId}
|
|
6922
7984
|
`);
|
|
6923
7985
|
return { ok: true };
|
|
6924
7986
|
} catch (error2) {
|
|
@@ -6943,7 +8005,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
6943
8005
|
}
|
|
6944
8006
|
|
|
6945
8007
|
// src/lib/process-liveness.ts
|
|
6946
|
-
import { readFileSync as
|
|
8008
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
6947
8009
|
function isProcessAlive(pid) {
|
|
6948
8010
|
try {
|
|
6949
8011
|
process.kill(pid, 0);
|
|
@@ -6958,7 +8020,7 @@ function isProcessAlive(pid) {
|
|
|
6958
8020
|
}
|
|
6959
8021
|
if (process.platform !== "linux") return true;
|
|
6960
8022
|
try {
|
|
6961
|
-
const status2 =
|
|
8023
|
+
const status2 = readFileSync10(`/proc/${pid}/status`, "utf8");
|
|
6962
8024
|
return !/^State:\s+Z(?:\s|$)/m.test(status2);
|
|
6963
8025
|
} catch (error2) {
|
|
6964
8026
|
console.error(
|
|
@@ -7025,8 +8087,8 @@ function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
|
7025
8087
|
}
|
|
7026
8088
|
|
|
7027
8089
|
// src/lib/resource-usage-reporting.ts
|
|
7028
|
-
var
|
|
7029
|
-
var
|
|
8090
|
+
var ENABLED_VALUES2 = /* @__PURE__ */ new Set(["on", "true", "1"]);
|
|
8091
|
+
var DISABLED_VALUES2 = /* @__PURE__ */ new Set(["off", "false", "0"]);
|
|
7030
8092
|
function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
7031
8093
|
if (flagValue === false) {
|
|
7032
8094
|
return { enabled: false, warnings: [] };
|
|
@@ -7036,10 +8098,10 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
|
7036
8098
|
return { enabled: true, warnings: [] };
|
|
7037
8099
|
}
|
|
7038
8100
|
const normalized = raw.trim().toLowerCase();
|
|
7039
|
-
if (
|
|
8101
|
+
if (DISABLED_VALUES2.has(normalized)) {
|
|
7040
8102
|
return { enabled: false, warnings: [] };
|
|
7041
8103
|
}
|
|
7042
|
-
if (
|
|
8104
|
+
if (ENABLED_VALUES2.has(normalized)) {
|
|
7043
8105
|
return { enabled: true, warnings: [] };
|
|
7044
8106
|
}
|
|
7045
8107
|
return {
|
|
@@ -7317,18 +8379,18 @@ function createSessionStoreHealth(options = {}) {
|
|
|
7317
8379
|
import { homedir as homedir6 } from "node:os";
|
|
7318
8380
|
|
|
7319
8381
|
// src/lib/runner-file-sync.ts
|
|
7320
|
-
import { join as
|
|
8382
|
+
import { join as join13 } from "node:path";
|
|
7321
8383
|
|
|
7322
8384
|
// src/lib/file-push.ts
|
|
7323
8385
|
import { randomUUID } from "node:crypto";
|
|
7324
8386
|
import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
|
|
7325
|
-
import { basename as basename2, dirname as
|
|
8387
|
+
import { basename as basename2, dirname as dirname9, isAbsolute as isAbsolute2, join as join12, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
|
7326
8388
|
var FILE_MODE = 384;
|
|
7327
8389
|
var DIRECTORY_MODE = 448;
|
|
7328
8390
|
async function writePushedFile(request3) {
|
|
7329
|
-
const { requestedPath, content, allowedDirectories, homeDir } = request3;
|
|
8391
|
+
const { requestedPath, content, allowedDirectories, allowedFiles, homeDir } = request3;
|
|
7330
8392
|
const bytes = content.byteLength;
|
|
7331
|
-
if (allowedDirectories.length === 0) {
|
|
8393
|
+
if (allowedDirectories.length === 0 && allowedFiles.length === 0) {
|
|
7332
8394
|
return refuse("file_sync_disabled", "File sync is not enabled on this runner.", {
|
|
7333
8395
|
path: requestedPath,
|
|
7334
8396
|
bytes
|
|
@@ -7353,10 +8415,10 @@ async function writePushedFile(request3) {
|
|
|
7353
8415
|
}
|
|
7354
8416
|
try {
|
|
7355
8417
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
7356
|
-
|
|
8418
|
+
dirname9(candidate)
|
|
7357
8419
|
);
|
|
7358
|
-
const realTarget =
|
|
7359
|
-
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
8420
|
+
const realTarget = join12(existingAncestor, ...missingSegments, basename2(candidate));
|
|
8421
|
+
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget) ?? await findMatchingAllowedFile(allowedFiles, realTarget);
|
|
7360
8422
|
if (allowedDirectory === null) {
|
|
7361
8423
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
7362
8424
|
path: realTarget,
|
|
@@ -7365,8 +8427,8 @@ async function writePushedFile(request3) {
|
|
|
7365
8427
|
}
|
|
7366
8428
|
if (missingSegments.length > 0) {
|
|
7367
8429
|
await createMissingDirectories(existingAncestor, missingSegments);
|
|
7368
|
-
const realParent = await realpath(
|
|
7369
|
-
if (realParent !==
|
|
8430
|
+
const realParent = await realpath(dirname9(realTarget));
|
|
8431
|
+
if (realParent !== dirname9(realTarget) || !contains(allowedDirectory, realTarget)) {
|
|
7370
8432
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
7371
8433
|
path: realTarget,
|
|
7372
8434
|
bytes,
|
|
@@ -7391,7 +8453,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
7391
8453
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
7392
8454
|
return null;
|
|
7393
8455
|
}
|
|
7394
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
8456
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join12(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
7395
8457
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
7396
8458
|
return null;
|
|
7397
8459
|
}
|
|
@@ -7409,7 +8471,7 @@ async function resolveNearestExistingAncestor(directory) {
|
|
|
7409
8471
|
try {
|
|
7410
8472
|
return { existingAncestor: await realpath(current), missingSegments };
|
|
7411
8473
|
} catch (err) {
|
|
7412
|
-
const parent =
|
|
8474
|
+
const parent = dirname9(current);
|
|
7413
8475
|
if (err.code !== "ENOENT" || parent === current) {
|
|
7414
8476
|
throw err;
|
|
7415
8477
|
}
|
|
@@ -7431,6 +8493,22 @@ async function findContainingAllowedDirectory(allowedDirectories, realTarget) {
|
|
|
7431
8493
|
}
|
|
7432
8494
|
return null;
|
|
7433
8495
|
}
|
|
8496
|
+
async function findMatchingAllowedFile(allowedFiles, realTarget) {
|
|
8497
|
+
for (const entry of allowedFiles) {
|
|
8498
|
+
if (!isAbsolute2(entry)) {
|
|
8499
|
+
log("warn", "file_push_allowed_directory_skipped", {
|
|
8500
|
+
directory: entry,
|
|
8501
|
+
reason: "not_absolute_file"
|
|
8502
|
+
});
|
|
8503
|
+
continue;
|
|
8504
|
+
}
|
|
8505
|
+
const realParent = await realpathCreatingIfMissing(dirname9(entry));
|
|
8506
|
+
if (realParent !== null && join12(realParent, basename2(entry)) === realTarget) {
|
|
8507
|
+
return realParent;
|
|
8508
|
+
}
|
|
8509
|
+
}
|
|
8510
|
+
return null;
|
|
8511
|
+
}
|
|
7434
8512
|
async function realpathCreatingIfMissing(directory) {
|
|
7435
8513
|
try {
|
|
7436
8514
|
return await realpath(directory);
|
|
@@ -7464,13 +8542,13 @@ function contains(realDirectory, realTarget) {
|
|
|
7464
8542
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
7465
8543
|
let current = existingAncestor;
|
|
7466
8544
|
for (const segment of missingSegments) {
|
|
7467
|
-
current =
|
|
8545
|
+
current = join12(current, segment);
|
|
7468
8546
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
7469
8547
|
await chmod(current, DIRECTORY_MODE);
|
|
7470
8548
|
}
|
|
7471
8549
|
}
|
|
7472
8550
|
async function writeAtomically(realTarget, content) {
|
|
7473
|
-
const temporaryPath =
|
|
8551
|
+
const temporaryPath = join12(dirname9(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
7474
8552
|
let handle;
|
|
7475
8553
|
try {
|
|
7476
8554
|
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
@@ -7600,16 +8678,16 @@ var NOT_APPLIED = {
|
|
|
7600
8678
|
opencodeAuthApplied: false
|
|
7601
8679
|
};
|
|
7602
8680
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
7603
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
7604
|
-
return expanded ===
|
|
8681
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
8682
|
+
return expanded === join13(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
7605
8683
|
}
|
|
7606
8684
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
7607
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
7608
|
-
return expanded ===
|
|
8685
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
8686
|
+
return expanded === join13(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
7609
8687
|
}
|
|
7610
8688
|
async function applyOne(options, file) {
|
|
7611
8689
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
7612
|
-
if (options.allowedDirectories.length === 0) {
|
|
8690
|
+
if (options.allowedDirectories.length === 0 && options.allowedFiles.length === 0) {
|
|
7613
8691
|
options.log({
|
|
7614
8692
|
level: "warn",
|
|
7615
8693
|
message: `Runner file ${label} rejected: file sync is not enabled on this runner (start it with --enable-file-sync-to)`
|
|
@@ -7636,6 +8714,7 @@ async function applyOne(options, file) {
|
|
|
7636
8714
|
requestedPath: file.path,
|
|
7637
8715
|
content: download.content,
|
|
7638
8716
|
allowedDirectories: options.allowedDirectories,
|
|
8717
|
+
allowedFiles: options.allowedFiles,
|
|
7639
8718
|
homeDir: options.homeDir
|
|
7640
8719
|
});
|
|
7641
8720
|
} catch (err) {
|
|
@@ -7742,6 +8821,212 @@ function describe(err) {
|
|
|
7742
8821
|
return err instanceof Error ? err.message : String(err);
|
|
7743
8822
|
}
|
|
7744
8823
|
|
|
8824
|
+
// src/lib/vault-file-sync.ts
|
|
8825
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
8826
|
+
async function syncVaultFiles(options) {
|
|
8827
|
+
const files = await listVaultFiles(options);
|
|
8828
|
+
if (files === null) return { applied: 0 };
|
|
8829
|
+
const listedIds = new Set(files.map((file) => file.id));
|
|
8830
|
+
for (const fileId of options.outcomes.keys()) {
|
|
8831
|
+
if (!listedIds.has(fileId)) options.outcomes.delete(fileId);
|
|
8832
|
+
}
|
|
8833
|
+
let applied = 0;
|
|
8834
|
+
for (const file of files) {
|
|
8835
|
+
try {
|
|
8836
|
+
if (await reconcileVaultFile(options, file)) applied += 1;
|
|
8837
|
+
} catch (err) {
|
|
8838
|
+
options.log({
|
|
8839
|
+
level: "error",
|
|
8840
|
+
message: `Vault file ${file.id} reconciliation failed unexpectedly (message processing is unaffected): ${describe2(err)}`
|
|
8841
|
+
});
|
|
8842
|
+
}
|
|
8843
|
+
}
|
|
8844
|
+
return { applied };
|
|
8845
|
+
}
|
|
8846
|
+
async function listVaultFiles(options) {
|
|
8847
|
+
let response;
|
|
8848
|
+
try {
|
|
8849
|
+
response = await options.fetchImpl(`${options.apiUrl}/runners/${options.agentId}/vault-files`, {
|
|
8850
|
+
headers: { Authorization: options.getAuthHeader() }
|
|
8851
|
+
});
|
|
8852
|
+
} catch (err) {
|
|
8853
|
+
options.log({
|
|
8854
|
+
level: "warn",
|
|
8855
|
+
message: `Could not list vault files \u2014 retrying on the next drain: ${describe2(err)}`
|
|
8856
|
+
});
|
|
8857
|
+
return null;
|
|
8858
|
+
}
|
|
8859
|
+
if (!response.ok) {
|
|
8860
|
+
options.log({
|
|
8861
|
+
level: response.status === 404 ? "debug" : "warn",
|
|
8862
|
+
message: `Listing vault files returned HTTP ${response.status} \u2014 retrying on the next drain`
|
|
8863
|
+
});
|
|
8864
|
+
return null;
|
|
8865
|
+
}
|
|
8866
|
+
let body;
|
|
8867
|
+
try {
|
|
8868
|
+
body = await response.json();
|
|
8869
|
+
} catch (err) {
|
|
8870
|
+
options.log({
|
|
8871
|
+
level: "warn",
|
|
8872
|
+
message: `Vault file list was not readable JSON \u2014 retrying on the next drain: ${describe2(err)}`
|
|
8873
|
+
});
|
|
8874
|
+
return null;
|
|
8875
|
+
}
|
|
8876
|
+
if (body === null || typeof body !== "object" || !Array.isArray(body.files)) {
|
|
8877
|
+
options.log({
|
|
8878
|
+
level: "warn",
|
|
8879
|
+
message: "Vault file list was not an object with a files array \u2014 ignoring it for this drain"
|
|
8880
|
+
});
|
|
8881
|
+
return null;
|
|
8882
|
+
}
|
|
8883
|
+
const files = [];
|
|
8884
|
+
for (const entry of body.files) {
|
|
8885
|
+
const file = asVaultFile(entry);
|
|
8886
|
+
if (file === null) {
|
|
8887
|
+
options.log({
|
|
8888
|
+
level: "warn",
|
|
8889
|
+
message: "Ignoring a malformed vault file entry"
|
|
8890
|
+
});
|
|
8891
|
+
continue;
|
|
8892
|
+
}
|
|
8893
|
+
files.push(file);
|
|
8894
|
+
}
|
|
8895
|
+
return files;
|
|
8896
|
+
}
|
|
8897
|
+
function asVaultFile(entry) {
|
|
8898
|
+
if (entry === null || typeof entry !== "object") return null;
|
|
8899
|
+
const value = entry;
|
|
8900
|
+
if (typeof value.id !== "string" || value.id === "" || typeof value.destination_path !== "string" || value.destination_path === "" || typeof value.size_bytes !== "number" || !Number.isFinite(value.size_bytes) || value.size_bytes < 0 || typeof value.content_sha256 !== "string" || value.content_sha256 === "" || typeof value.created_at !== "string" || typeof value.updated_at !== "string") {
|
|
8901
|
+
return null;
|
|
8902
|
+
}
|
|
8903
|
+
return {
|
|
8904
|
+
id: value.id,
|
|
8905
|
+
destination_path: value.destination_path,
|
|
8906
|
+
size_bytes: value.size_bytes,
|
|
8907
|
+
content_sha256: value.content_sha256,
|
|
8908
|
+
created_at: value.created_at,
|
|
8909
|
+
updated_at: value.updated_at
|
|
8910
|
+
};
|
|
8911
|
+
}
|
|
8912
|
+
async function reconcileVaultFile(options, file) {
|
|
8913
|
+
const existing = options.outcomes.get(file.id);
|
|
8914
|
+
if (existing?.sha256 === file.content_sha256) {
|
|
8915
|
+
if (existing.reported || existing.reportAttempts >= MAX_ACK_ATTEMPTS) return false;
|
|
8916
|
+
await reportOutcome(options, file, existing);
|
|
8917
|
+
return false;
|
|
8918
|
+
}
|
|
8919
|
+
const content = await downloadVaultFile(options, file);
|
|
8920
|
+
if (content === null) return false;
|
|
8921
|
+
const actualSha256 = createHash2("sha256").update(content).digest("hex");
|
|
8922
|
+
if (actualSha256 !== file.content_sha256) {
|
|
8923
|
+
options.log({
|
|
8924
|
+
level: "warn",
|
|
8925
|
+
message: `Vault file ${file.id} content hash mismatch: expected ${file.content_sha256}, received ${actualSha256} \u2014 retrying on the next drain`
|
|
8926
|
+
});
|
|
8927
|
+
return false;
|
|
8928
|
+
}
|
|
8929
|
+
const outcome = await applyVaultFile(options, file, content);
|
|
8930
|
+
const entry = {
|
|
8931
|
+
sha256: file.content_sha256,
|
|
8932
|
+
outcome: outcome.ok ? "applied" : "refused",
|
|
8933
|
+
reason: outcome.ok ? null : outcome.code,
|
|
8934
|
+
reported: false,
|
|
8935
|
+
reportAttempts: 0
|
|
8936
|
+
};
|
|
8937
|
+
options.outcomes.set(file.id, entry);
|
|
8938
|
+
await reportOutcome(options, file, entry);
|
|
8939
|
+
return outcome.ok;
|
|
8940
|
+
}
|
|
8941
|
+
async function downloadVaultFile(options, file) {
|
|
8942
|
+
try {
|
|
8943
|
+
const response = await options.fetchImpl(
|
|
8944
|
+
`${options.apiUrl}/runners/${options.agentId}/vault-files/${file.id}/content`,
|
|
8945
|
+
{ headers: { Authorization: options.getAuthHeader() } }
|
|
8946
|
+
);
|
|
8947
|
+
if (!response.ok) {
|
|
8948
|
+
options.log({
|
|
8949
|
+
level: "warn",
|
|
8950
|
+
message: `Downloading vault file ${file.id} returned HTTP ${response.status} \u2014 retrying on the next drain`
|
|
8951
|
+
});
|
|
8952
|
+
return null;
|
|
8953
|
+
}
|
|
8954
|
+
return Buffer.from(await response.arrayBuffer());
|
|
8955
|
+
} catch (err) {
|
|
8956
|
+
options.log({
|
|
8957
|
+
level: "warn",
|
|
8958
|
+
message: `Downloading vault file ${file.id} failed \u2014 retrying on the next drain: ${describe2(err)}`
|
|
8959
|
+
});
|
|
8960
|
+
return null;
|
|
8961
|
+
}
|
|
8962
|
+
}
|
|
8963
|
+
async function applyVaultFile(options, file, content) {
|
|
8964
|
+
try {
|
|
8965
|
+
const outcome = await writePushedFile({
|
|
8966
|
+
requestedPath: file.destination_path,
|
|
8967
|
+
content,
|
|
8968
|
+
allowedDirectories: options.allowedDirectories,
|
|
8969
|
+
allowedFiles: options.allowedFiles,
|
|
8970
|
+
homeDir: options.homeDir
|
|
8971
|
+
});
|
|
8972
|
+
if (!outcome.ok) {
|
|
8973
|
+
options.log({
|
|
8974
|
+
level: "warn",
|
|
8975
|
+
message: `Vault file ${file.id} refused (${outcome.code}): ${outcome.message}`
|
|
8976
|
+
});
|
|
8977
|
+
} else {
|
|
8978
|
+
options.log({
|
|
8979
|
+
level: "info",
|
|
8980
|
+
message: `Vault file ${file.id} applied (${content.byteLength} bytes)`
|
|
8981
|
+
});
|
|
8982
|
+
}
|
|
8983
|
+
return outcome;
|
|
8984
|
+
} catch (err) {
|
|
8985
|
+
options.log({
|
|
8986
|
+
level: "error",
|
|
8987
|
+
message: `Vault file ${file.id} could not be written: ${describe2(err)}`
|
|
8988
|
+
});
|
|
8989
|
+
return { ok: false, code: "write_failed", message: "The runner could not write the file." };
|
|
8990
|
+
}
|
|
8991
|
+
}
|
|
8992
|
+
async function reportOutcome(options, file, entry) {
|
|
8993
|
+
const body = {
|
|
8994
|
+
status: entry.outcome === "applied" ? "applied" : "rejected",
|
|
8995
|
+
reported_sha256: entry.sha256
|
|
8996
|
+
};
|
|
8997
|
+
if (entry.outcome === "refused" && entry.reason !== null) body.reason = entry.reason;
|
|
8998
|
+
try {
|
|
8999
|
+
const response = await options.fetchImpl(
|
|
9000
|
+
`${options.apiUrl}/runners/${options.agentId}/vault-files/${file.id}/status`,
|
|
9001
|
+
{
|
|
9002
|
+
method: "POST",
|
|
9003
|
+
headers: {
|
|
9004
|
+
Authorization: options.getAuthHeader(),
|
|
9005
|
+
"Content-Type": "application/json"
|
|
9006
|
+
},
|
|
9007
|
+
body: JSON.stringify(body)
|
|
9008
|
+
}
|
|
9009
|
+
);
|
|
9010
|
+
if (response.ok) {
|
|
9011
|
+
entry.reported = true;
|
|
9012
|
+
return;
|
|
9013
|
+
}
|
|
9014
|
+
recordReportFailure(options, file, entry, `HTTP ${response.status}`);
|
|
9015
|
+
} catch (err) {
|
|
9016
|
+
recordReportFailure(options, file, entry, describe2(err));
|
|
9017
|
+
}
|
|
9018
|
+
}
|
|
9019
|
+
function recordReportFailure(options, file, entry, reason) {
|
|
9020
|
+
entry.reportAttempts += 1;
|
|
9021
|
+
options.log({
|
|
9022
|
+
level: "error",
|
|
9023
|
+
message: entry.reportAttempts >= MAX_ACK_ATTEMPTS ? `Reporting vault file ${file.id} as ${entry.outcome} failed (${reason}) \u2014 giving up after ${entry.reportAttempts} attempts` : `Reporting vault file ${file.id} as ${entry.outcome} failed (${reason}) \u2014 retrying on the next drain (attempt ${entry.reportAttempts} of ${MAX_ACK_ATTEMPTS})`
|
|
9024
|
+
});
|
|
9025
|
+
}
|
|
9026
|
+
function describe2(err) {
|
|
9027
|
+
return err instanceof Error ? err.message : String(err);
|
|
9028
|
+
}
|
|
9029
|
+
|
|
7745
9030
|
// src/lib/channels/driver.ts
|
|
7746
9031
|
function messageIdOf(m) {
|
|
7747
9032
|
if (!m || typeof m !== "object") return void 0;
|
|
@@ -7750,7 +9035,7 @@ function messageIdOf(m) {
|
|
|
7750
9035
|
return typeof infoId === "string" ? infoId : void 0;
|
|
7751
9036
|
}
|
|
7752
9037
|
function composeAbandonmentNote(finish, replyShape, outcome) {
|
|
7753
|
-
const base = `OpenCode ended this turn after a tool call (finish="${finish ?? "(unknown)"}") ` + (outcome === "without-answer" ? "without writing an answer, and confirmed it is no longer working on this conversation. Evident is reporting the run as failed so the failure and its reason are visible in failed runs." : "after writing only part of an answer, and confirmed it is no longer working on this conversation. Evident delivered what was written and is reporting the run as finished.");
|
|
9038
|
+
const base = outcome === "midflight-without-answer" || outcome === "midflight-with-answer" ? "OpenCode stopped this turn without ever finishing, and confirmed the session is no longer running. " + (outcome === "midflight-without-answer" ? "Evident is reporting the run as failed so the failure and its reason are visible in failed runs." : "Evident delivered what was written and is reporting the run as finished.") : `OpenCode ended this turn after a tool call (finish="${finish ?? "(unknown)"}") ` + (outcome === "without-answer" ? "without writing an answer, and confirmed it is no longer working on this conversation. Evident is reporting the run as failed so the failure and its reason are visible in failed runs." : "after writing only part of an answer, and confirmed it is no longer working on this conversation. Evident delivered what was written and is reporting the run as finished.");
|
|
7754
9039
|
if (replyShape === null) return base;
|
|
7755
9040
|
return `${base} ${replyShape.lastToolName === null ? "No tool call was recorded" : `The last tool call was "${replyShape.lastToolName}" with status "${replyShape.lastToolStatus ?? "(unknown)"}"`}; the reply's final part types were [${replyShape.partTypes.join(", ")}]${replyShape.droppedPartCount > 0 ? ` (${replyShape.droppedPartCount} earlier part types omitted)` : ""}.`;
|
|
7756
9041
|
}
|
|
@@ -7787,6 +9072,9 @@ var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
|
|
|
7787
9072
|
var ABSOLUTE_MAX_PROCESSING_MS = 6 * 60 * 60 * 1e3;
|
|
7788
9073
|
var B2_ABANDONMENT_MIN_PINNED_MS = 3 * 6e4;
|
|
7789
9074
|
var AMBIGUOUS_FINISH_MAX_PINNED_MS = 3 * 6e4;
|
|
9075
|
+
var MIDFLIGHT_ORPHAN_MIN_PINNED_MS = 3 * 6e4;
|
|
9076
|
+
var MIDFLIGHT_ORPHAN_RECHECK_MS = 6e4;
|
|
9077
|
+
var MIDFLIGHT_ORPHAN_MIN_NOT_ONGOING_STREAK = 2;
|
|
7790
9078
|
var B2_ABANDONMENT_RECHECK_MS = HEARTBEAT_MS;
|
|
7791
9079
|
var POLL_MISS_GRACE_MS = HEARTBEAT_MS;
|
|
7792
9080
|
var WATCHER_STALL_MS = 3 * POLL_MISS_GRACE_MS;
|
|
@@ -7831,6 +9119,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7831
9119
|
conversationFilter;
|
|
7832
9120
|
retry;
|
|
7833
9121
|
log;
|
|
9122
|
+
recordBinding;
|
|
7834
9123
|
fetchImpl;
|
|
7835
9124
|
sleep;
|
|
7836
9125
|
pausedPollIntervalMs;
|
|
@@ -7838,6 +9127,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7838
9127
|
stuckQueuedMs;
|
|
7839
9128
|
now;
|
|
7840
9129
|
fileSyncDirectories;
|
|
9130
|
+
fileSyncExactFiles;
|
|
7841
9131
|
homeDir;
|
|
7842
9132
|
maxActiveSessions;
|
|
7843
9133
|
watcherStallMs;
|
|
@@ -8150,6 +9440,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8150
9440
|
* re-downloaded and re-written every ~2s until the server expires it.
|
|
8151
9441
|
*/
|
|
8152
9442
|
fileAckFailures = /* @__PURE__ */ new Map();
|
|
9443
|
+
vaultFileOutcomes = /* @__PURE__ */ new Map();
|
|
8153
9444
|
/**
|
|
8154
9445
|
* Monotonic count of files this runner has pulled and written (#559). Only
|
|
8155
9446
|
* ever increases, so `run.ts` detects work by comparing it against the value
|
|
@@ -8191,6 +9482,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8191
9482
|
this.retry = { ...DEFAULT_RETRY_POLICY, ...config.retry };
|
|
8192
9483
|
this.log = config.log ?? (() => {
|
|
8193
9484
|
});
|
|
9485
|
+
this.recordBinding = config.recordBinding;
|
|
8194
9486
|
this.fetchImpl = withRequestTimeout(
|
|
8195
9487
|
config.fetchImpl ?? fetch,
|
|
8196
9488
|
config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
|
|
@@ -8206,6 +9498,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8206
9498
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
8207
9499
|
this.now = config.now ?? (() => Date.now());
|
|
8208
9500
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
9501
|
+
this.fileSyncExactFiles = config.fileSyncExactFiles ?? [];
|
|
8209
9502
|
this.homeDir = config.homeDir ?? homedir6();
|
|
8210
9503
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
8211
9504
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
@@ -8303,14 +9596,27 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8303
9596
|
getAuthHeader: this.getAuthHeader,
|
|
8304
9597
|
fetchImpl: this.fetchImpl,
|
|
8305
9598
|
allowedDirectories: this.fileSyncDirectories,
|
|
9599
|
+
allowedFiles: this.fileSyncExactFiles,
|
|
8306
9600
|
homeDir: this.homeDir,
|
|
8307
9601
|
ackFailures: this.fileAckFailures,
|
|
8308
9602
|
log: this.log
|
|
8309
9603
|
});
|
|
8310
|
-
|
|
9604
|
+
const vaultResult = await syncVaultFiles({
|
|
9605
|
+
agentId: this.agentId,
|
|
9606
|
+
apiUrl: this.apiUrl,
|
|
9607
|
+
getAuthHeader: this.getAuthHeader,
|
|
9608
|
+
fetchImpl: this.fetchImpl,
|
|
9609
|
+
allowedDirectories: this.fileSyncDirectories,
|
|
9610
|
+
allowedFiles: this.fileSyncExactFiles,
|
|
9611
|
+
homeDir: this.homeDir,
|
|
9612
|
+
outcomes: this.vaultFileOutcomes,
|
|
9613
|
+
log: this.log
|
|
9614
|
+
});
|
|
9615
|
+
const applied = result.applied + vaultResult.applied;
|
|
9616
|
+
this.appliedFileCount += applied;
|
|
8311
9617
|
if (result.claudeCredentialApplied) this.claudeCredentialApplyCount += 1;
|
|
8312
9618
|
if (result.opencodeAuthApplied) this.opencodeAuthApplyCount += 1;
|
|
8313
|
-
return
|
|
9619
|
+
return applied;
|
|
8314
9620
|
} catch (err) {
|
|
8315
9621
|
this.log({
|
|
8316
9622
|
level: "error",
|
|
@@ -8364,6 +9670,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8364
9670
|
let cappedSkips = 0;
|
|
8365
9671
|
for (const conv of conversations) {
|
|
8366
9672
|
if (this.stopped) break;
|
|
9673
|
+
if (conv.opencode_session_id) {
|
|
9674
|
+
this.recordSessionBinding(conv.id, conv.opencode_session_id);
|
|
9675
|
+
}
|
|
8367
9676
|
if (this.maxActiveSessions !== void 0) {
|
|
8368
9677
|
const activeSessionIds = this.activeSessionIdsForCap();
|
|
8369
9678
|
const resolvedSessionId = this.sessions.get(conv.id) ?? conv.opencode_session_id;
|
|
@@ -8446,12 +9755,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8446
9755
|
this.sessions.delete(conversationId);
|
|
8447
9756
|
this.supersede(conversationId, sessionId);
|
|
8448
9757
|
const dispatchDescription = source === "readopt" ? "re-dispatched message" : "message";
|
|
8449
|
-
const
|
|
8450
|
-
this.untrackableAck.set(messageId,
|
|
9758
|
+
const errorMessage4 = `OpenCode accepted this ${dispatchDescription} once, but its assigned id was never confirmed after ${attempts} attempts to match it up \u2014 the message was deliberately not sent again, so no duplicate turn was created; session ${sessionId.slice(0, 8)} was abandoned; a fresh one is used for further messages.`;
|
|
9759
|
+
this.untrackableAck.set(messageId, errorMessage4);
|
|
8451
9760
|
this.untrackableAckSessionIds.set(messageId, null);
|
|
8452
9761
|
this.log({
|
|
8453
9762
|
level: "error",
|
|
8454
|
-
message:
|
|
9763
|
+
message: errorMessage4,
|
|
8455
9764
|
conversation_id: conversationId,
|
|
8456
9765
|
message_id: messageId
|
|
8457
9766
|
});
|
|
@@ -8459,7 +9768,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8459
9768
|
conversationId,
|
|
8460
9769
|
messageId,
|
|
8461
9770
|
null,
|
|
8462
|
-
|
|
9771
|
+
errorMessage4
|
|
8463
9772
|
);
|
|
8464
9773
|
if (reportError !== null) {
|
|
8465
9774
|
this.log({
|
|
@@ -8640,12 +9949,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8640
9949
|
conversation_id: conv.id,
|
|
8641
9950
|
message_id: message.id
|
|
8642
9951
|
});
|
|
8643
|
-
const
|
|
9952
|
+
const errorMessage4 = this.untrackableAck.get(message.id);
|
|
8644
9953
|
const reportError = await this.reportUntrackableAck(
|
|
8645
9954
|
conv.id,
|
|
8646
9955
|
message.id,
|
|
8647
9956
|
null,
|
|
8648
|
-
|
|
9957
|
+
errorMessage4
|
|
8649
9958
|
);
|
|
8650
9959
|
if (reportError === null) {
|
|
8651
9960
|
continue;
|
|
@@ -8722,6 +10031,47 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8722
10031
|
agent: message.opencode_agent ?? void 0,
|
|
8723
10032
|
model: message.opencode_model ?? void 0
|
|
8724
10033
|
};
|
|
10034
|
+
const pinnedProviderLoaded = await isPinnedProviderLoaded(
|
|
10035
|
+
this.port,
|
|
10036
|
+
options.model,
|
|
10037
|
+
this.openCodeClient
|
|
10038
|
+
);
|
|
10039
|
+
if (pinnedProviderLoaded === false) {
|
|
10040
|
+
const dispatchedModel = message.opencode_model;
|
|
10041
|
+
const separator = dispatchedModel?.indexOf("/") ?? -1;
|
|
10042
|
+
const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
|
|
10043
|
+
const providerId = hasProviderAndModel ? dispatchedModel.slice(0, separator) : null;
|
|
10044
|
+
const modelId = hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null;
|
|
10045
|
+
const errorMessage4 = `Provider namespace not loaded on this runner: ${message.opencode_model}. Refused before dispatch; no turn was started.`;
|
|
10046
|
+
this.log({
|
|
10047
|
+
level: "error",
|
|
10048
|
+
message: `Refusing message ${message.id.slice(0, 8)} in conversation ${conv.id.slice(0, 8)}: provider namespace ${providerId ?? "(unknown)"} is not loaded; no turn was started`,
|
|
10049
|
+
conversation_id: conv.id,
|
|
10050
|
+
message_id: message.id
|
|
10051
|
+
});
|
|
10052
|
+
try {
|
|
10053
|
+
await this.markFailed(conv.id, message.id, sessionId, errorMessage4, void 0, {
|
|
10054
|
+
classification: {
|
|
10055
|
+
kind: "model_auth",
|
|
10056
|
+
providerId,
|
|
10057
|
+
modelId,
|
|
10058
|
+
reason: "missing"
|
|
10059
|
+
},
|
|
10060
|
+
errorShape: { name: null, statusCode: null }
|
|
10061
|
+
});
|
|
10062
|
+
this.signalDispatchNotStarted(conv, message, "model_provider_absent");
|
|
10063
|
+
} catch (markErr) {
|
|
10064
|
+
this.log({
|
|
10065
|
+
level: "warn",
|
|
10066
|
+
message: `markFailed PATCH for refused message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
10067
|
+
conversation_id: conv.id,
|
|
10068
|
+
message_id: message.id
|
|
10069
|
+
});
|
|
10070
|
+
this.signalDispatchNotStarted(conv, message, "failure_unreported");
|
|
10071
|
+
}
|
|
10072
|
+
await this.repairProviderRegistry(conv.id, message.id);
|
|
10073
|
+
break;
|
|
10074
|
+
}
|
|
8725
10075
|
let opencodeMessageId;
|
|
8726
10076
|
let preDispatchUserIds = null;
|
|
8727
10077
|
try {
|
|
@@ -8772,12 +10122,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8772
10122
|
} catch (err) {
|
|
8773
10123
|
if (err instanceof ChannelAuthError) throw err;
|
|
8774
10124
|
if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
|
|
8775
|
-
const
|
|
8776
|
-
this.untrackableAck.set(message.id,
|
|
10125
|
+
const errorMessage5 = err instanceof Error ? err.message : String(err);
|
|
10126
|
+
this.untrackableAck.set(message.id, errorMessage5);
|
|
8777
10127
|
this.untrackableAckSessionIds.set(message.id, null);
|
|
8778
10128
|
this.log({
|
|
8779
10129
|
level: "error",
|
|
8780
|
-
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${
|
|
10130
|
+
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage5}`,
|
|
8781
10131
|
conversation_id: conv.id,
|
|
8782
10132
|
message_id: message.id
|
|
8783
10133
|
});
|
|
@@ -8785,7 +10135,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8785
10135
|
conv.id,
|
|
8786
10136
|
message.id,
|
|
8787
10137
|
null,
|
|
8788
|
-
|
|
10138
|
+
errorMessage5
|
|
8789
10139
|
);
|
|
8790
10140
|
if (markError !== null) {
|
|
8791
10141
|
this.log({
|
|
@@ -8820,7 +10170,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8820
10170
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
8821
10171
|
break;
|
|
8822
10172
|
}
|
|
8823
|
-
const
|
|
10173
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
8824
10174
|
this.sessions.delete(conv.id);
|
|
8825
10175
|
this.supersede(conv.id, sessionId);
|
|
8826
10176
|
this.log({
|
|
@@ -8829,7 +10179,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8829
10179
|
conversation_id: conv.id,
|
|
8830
10180
|
message_id: message.id
|
|
8831
10181
|
});
|
|
8832
|
-
await this.markFailed(conv.id, message.id, null,
|
|
10182
|
+
await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
|
|
8833
10183
|
this.log({
|
|
8834
10184
|
level: "warn",
|
|
8835
10185
|
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)}`,
|
|
@@ -8840,7 +10190,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8840
10190
|
});
|
|
8841
10191
|
this.log({
|
|
8842
10192
|
level: "error",
|
|
8843
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
10193
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage4}`,
|
|
8844
10194
|
conversation_id: conv.id,
|
|
8845
10195
|
message_id: message.id
|
|
8846
10196
|
});
|
|
@@ -9627,6 +10977,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9627
10977
|
return { sessionId: await this.createAndBindSession(conv.id), created: true };
|
|
9628
10978
|
}
|
|
9629
10979
|
this.sessions.set(conv.id, bound);
|
|
10980
|
+
this.recordSessionBinding(conv.id, bound);
|
|
9630
10981
|
return { sessionId: bound, created: false };
|
|
9631
10982
|
}
|
|
9632
10983
|
return { sessionId: await this.createAndBindSession(conv.id), created: true };
|
|
@@ -9640,6 +10991,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9640
10991
|
const directory = await this.resolveOpenCodeDirectory();
|
|
9641
10992
|
const sessionId = await this.createOpenCodeSession(directory);
|
|
9642
10993
|
this.sessions.set(conversationId, sessionId);
|
|
10994
|
+
this.recordSessionBinding(conversationId, sessionId);
|
|
9643
10995
|
await this.persistSession(conversationId, sessionId).catch((err) => {
|
|
9644
10996
|
this.log({
|
|
9645
10997
|
level: "warn",
|
|
@@ -9649,6 +11001,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9649
11001
|
});
|
|
9650
11002
|
return sessionId;
|
|
9651
11003
|
}
|
|
11004
|
+
recordSessionBinding(conversationId, sessionId) {
|
|
11005
|
+
if (!this.recordBinding) return;
|
|
11006
|
+
try {
|
|
11007
|
+
this.recordBinding(conversationId, sessionId);
|
|
11008
|
+
} catch (error2) {
|
|
11009
|
+
this.log({
|
|
11010
|
+
level: "warn",
|
|
11011
|
+
message: `Recording OpenCode session ${sessionId.slice(0, 8)} for conversation ${conversationId.slice(0, 8)} failed (best-effort): ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
11012
|
+
conversation_id: conversationId
|
|
11013
|
+
});
|
|
11014
|
+
}
|
|
11015
|
+
}
|
|
9652
11016
|
/**
|
|
9653
11017
|
* Lazily resolve (and cache) opencode's root directory via the selected client's
|
|
9654
11018
|
* location lookup.
|
|
@@ -9846,6 +11210,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9846
11210
|
b2AbandonedWithoutAnswer: false,
|
|
9847
11211
|
ambiguousPinnedSinceMs: 0,
|
|
9848
11212
|
ambiguousResolved: false,
|
|
11213
|
+
midFlightPinnedSinceMs: 0,
|
|
11214
|
+
midFlightNotOngoingStreak: 0,
|
|
11215
|
+
midFlightLastStatusCheckMs: 0,
|
|
11216
|
+
midFlightOrphanSignalled: false,
|
|
11217
|
+
midFlightOrphanWithoutAnswer: false,
|
|
11218
|
+
midFlightHeldLogged: false,
|
|
9849
11219
|
sessionStoreFailurePending: false,
|
|
9850
11220
|
neverStartedResolved: false
|
|
9851
11221
|
});
|
|
@@ -9985,6 +11355,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9985
11355
|
b2AbandonedWithoutAnswer: false,
|
|
9986
11356
|
ambiguousPinnedSinceMs: 0,
|
|
9987
11357
|
ambiguousResolved: false,
|
|
11358
|
+
midFlightPinnedSinceMs: 0,
|
|
11359
|
+
midFlightNotOngoingStreak: 0,
|
|
11360
|
+
midFlightLastStatusCheckMs: 0,
|
|
11361
|
+
midFlightOrphanSignalled: false,
|
|
11362
|
+
midFlightOrphanWithoutAnswer: false,
|
|
11363
|
+
midFlightHeldLogged: false,
|
|
9988
11364
|
sessionStoreFailurePending: false,
|
|
9989
11365
|
neverStartedResolved: false
|
|
9990
11366
|
});
|
|
@@ -10756,7 +12132,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10756
12132
|
watcher,
|
|
10757
12133
|
inFlight,
|
|
10758
12134
|
messages,
|
|
10759
|
-
|
|
12135
|
+
composeAbandonmentNote(
|
|
12136
|
+
finish,
|
|
12137
|
+
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
12138
|
+
"without-answer"
|
|
12139
|
+
)
|
|
10760
12140
|
);
|
|
10761
12141
|
} else {
|
|
10762
12142
|
await this.settleMessageDone(
|
|
@@ -10815,7 +12195,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10815
12195
|
watcher,
|
|
10816
12196
|
inFlight,
|
|
10817
12197
|
messages,
|
|
10818
|
-
|
|
12198
|
+
composeAbandonmentNote(
|
|
12199
|
+
finish,
|
|
12200
|
+
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
12201
|
+
"without-answer"
|
|
12202
|
+
)
|
|
10819
12203
|
);
|
|
10820
12204
|
return;
|
|
10821
12205
|
}
|
|
@@ -10892,21 +12276,121 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10892
12276
|
run_state: "failed",
|
|
10893
12277
|
opencode_message_id: inFlight.opencodeMessageId
|
|
10894
12278
|
});
|
|
10895
|
-
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
10896
|
-
return;
|
|
12279
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
12280
|
+
return;
|
|
12281
|
+
}
|
|
12282
|
+
inFlight.ambiguousResolved = true;
|
|
12283
|
+
this.log({
|
|
12284
|
+
level: "warn",
|
|
12285
|
+
message: `Message ${id.slice(0, 8)} ambiguous-finish-pinned for ${Math.round(pinnedForMs / 1e3)}s \u2014 resolved (${ongoing === false ? "session confirmed not-ongoing" : "pin exceeded the no-hang cap"}) \u2014 settling done`,
|
|
12286
|
+
conversation_id: conv.id,
|
|
12287
|
+
message_id: id
|
|
12288
|
+
});
|
|
12289
|
+
void this.postSignal(conv.id, id, "ambiguous_finish_resolved", {
|
|
12290
|
+
watched_for_ms: pinnedForMs
|
|
12291
|
+
});
|
|
12292
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
12293
|
+
return;
|
|
12294
|
+
}
|
|
12295
|
+
}
|
|
12296
|
+
const midFlightPinnedNow = activelyRunning && isMidFlightPinnedRunning(messages, inFlight.opencodeMessageId);
|
|
12297
|
+
if (!midFlightPinnedNow) {
|
|
12298
|
+
if (snapshotReadable) {
|
|
12299
|
+
inFlight.midFlightPinnedSinceMs = 0;
|
|
12300
|
+
inFlight.midFlightNotOngoingStreak = 0;
|
|
12301
|
+
inFlight.midFlightLastStatusCheckMs = 0;
|
|
12302
|
+
inFlight.midFlightOrphanSignalled = false;
|
|
12303
|
+
inFlight.midFlightOrphanWithoutAnswer = false;
|
|
12304
|
+
inFlight.midFlightHeldLogged = false;
|
|
12305
|
+
}
|
|
12306
|
+
} else {
|
|
12307
|
+
if (inFlight.midFlightOrphanSignalled) {
|
|
12308
|
+
const note = composeAbandonmentNote(
|
|
12309
|
+
void 0,
|
|
12310
|
+
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
12311
|
+
inFlight.midFlightOrphanWithoutAnswer ? "midflight-without-answer" : "midflight-with-answer"
|
|
12312
|
+
);
|
|
12313
|
+
if (inFlight.midFlightOrphanWithoutAnswer) {
|
|
12314
|
+
await this.settleMessageAbandonedWithoutAnswer(
|
|
12315
|
+
sessionId,
|
|
12316
|
+
watcher,
|
|
12317
|
+
inFlight,
|
|
12318
|
+
messages,
|
|
12319
|
+
note
|
|
12320
|
+
);
|
|
12321
|
+
} else {
|
|
12322
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages, note);
|
|
12323
|
+
}
|
|
12324
|
+
return;
|
|
12325
|
+
}
|
|
12326
|
+
if (inFlight.midFlightPinnedSinceMs === 0) {
|
|
12327
|
+
inFlight.midFlightPinnedSinceMs = this.now();
|
|
12328
|
+
}
|
|
12329
|
+
const pinnedForMs = this.now() - inFlight.midFlightPinnedSinceMs;
|
|
12330
|
+
if (pinnedForMs >= MIDFLIGHT_ORPHAN_MIN_PINNED_MS && this.now() - inFlight.midFlightLastStatusCheckMs >= MIDFLIGHT_ORPHAN_RECHECK_MS) {
|
|
12331
|
+
inFlight.midFlightLastStatusCheckMs = this.now();
|
|
12332
|
+
const rootOngoing = await this.isSessionOngoing(sessionId);
|
|
12333
|
+
inFlight.midFlightNotOngoingStreak = rootOngoing === false ? inFlight.midFlightNotOngoingStreak + 1 : 0;
|
|
12334
|
+
if (isMidFlightOrphanConfirmed({
|
|
12335
|
+
pinnedForMs,
|
|
12336
|
+
minPinnedMs: MIDFLIGHT_ORPHAN_MIN_PINNED_MS,
|
|
12337
|
+
notOngoingStreak: inFlight.midFlightNotOngoingStreak,
|
|
12338
|
+
minNotOngoingStreak: MIDFLIGHT_ORPHAN_MIN_NOT_ONGOING_STREAK
|
|
12339
|
+
})) {
|
|
12340
|
+
if (this.sessionStoreHealth?.isUnwritable()) {
|
|
12341
|
+
inFlight.sessionStoreFailurePending = true;
|
|
12342
|
+
void this.postSignal(conv.id, id, "midflight_orphan_resolved", {
|
|
12343
|
+
watched_for_ms: pinnedForMs,
|
|
12344
|
+
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
12345
|
+
opencode_message_id: inFlight.opencodeMessageId,
|
|
12346
|
+
run_state: "failed"
|
|
12347
|
+
});
|
|
12348
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
12349
|
+
return;
|
|
12350
|
+
}
|
|
12351
|
+
inFlight.midFlightOrphanSignalled = true;
|
|
12352
|
+
const noteOutcome = extractReplyTextForMessage(messages, inFlight.opencodeMessageId) === null ? "midflight-without-answer" : "midflight-with-answer";
|
|
12353
|
+
inFlight.midFlightOrphanWithoutAnswer = noteOutcome === "midflight-without-answer";
|
|
12354
|
+
void this.postSignal(conv.id, id, "midflight_orphan_resolved", {
|
|
12355
|
+
watched_for_ms: pinnedForMs,
|
|
12356
|
+
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
12357
|
+
opencode_message_id: inFlight.opencodeMessageId,
|
|
12358
|
+
...inFlight.midFlightOrphanWithoutAnswer ? { run_state: "failed" } : {}
|
|
12359
|
+
});
|
|
12360
|
+
const note = composeAbandonmentNote(
|
|
12361
|
+
void 0,
|
|
12362
|
+
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
12363
|
+
noteOutcome
|
|
12364
|
+
);
|
|
12365
|
+
this.log({
|
|
12366
|
+
level: "warn",
|
|
12367
|
+
message: `Message ${id.slice(0, 8)} was pinned mid-flight for ${Math.round(pinnedForMs / 1e3)}s with root_ongoing=${rootOngoing} \u2014 resolving the orphaned turn`,
|
|
12368
|
+
conversation_id: conv.id,
|
|
12369
|
+
message_id: id
|
|
12370
|
+
});
|
|
12371
|
+
if (inFlight.midFlightOrphanWithoutAnswer) {
|
|
12372
|
+
await this.settleMessageAbandonedWithoutAnswer(
|
|
12373
|
+
sessionId,
|
|
12374
|
+
watcher,
|
|
12375
|
+
inFlight,
|
|
12376
|
+
messages,
|
|
12377
|
+
note
|
|
12378
|
+
);
|
|
12379
|
+
} else {
|
|
12380
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages, note);
|
|
12381
|
+
}
|
|
12382
|
+
return;
|
|
12383
|
+
}
|
|
12384
|
+
if (rootOngoing === true && !inFlight.midFlightHeldLogged) {
|
|
12385
|
+
inFlight.midFlightHeldLogged = true;
|
|
12386
|
+
const replyShape = describeAbandonedReplyShape(messages, inFlight.opencodeMessageId);
|
|
12387
|
+
this.log({
|
|
12388
|
+
level: "warn",
|
|
12389
|
+
message: `Message ${id.slice(0, 8)} was pinned mid-flight for ${Math.round(pinnedForMs / 1e3)}s with root_ongoing=true \u2014 holding; last tool ${replyShape?.lastToolName ?? "(unknown)"} status ${replyShape?.lastToolStatus ?? "(unknown)"}`,
|
|
12390
|
+
conversation_id: conv.id,
|
|
12391
|
+
message_id: id
|
|
12392
|
+
});
|
|
10897
12393
|
}
|
|
10898
|
-
inFlight.ambiguousResolved = true;
|
|
10899
|
-
this.log({
|
|
10900
|
-
level: "warn",
|
|
10901
|
-
message: `Message ${id.slice(0, 8)} ambiguous-finish-pinned for ${Math.round(pinnedForMs / 1e3)}s \u2014 resolved (${ongoing === false ? "session confirmed not-ongoing" : "pin exceeded the no-hang cap"}) \u2014 settling done`,
|
|
10902
|
-
conversation_id: conv.id,
|
|
10903
|
-
message_id: id
|
|
10904
|
-
});
|
|
10905
|
-
void this.postSignal(conv.id, id, "ambiguous_finish_resolved", {
|
|
10906
|
-
watched_for_ms: pinnedForMs
|
|
10907
|
-
});
|
|
10908
|
-
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
10909
|
-
return;
|
|
10910
12394
|
}
|
|
10911
12395
|
}
|
|
10912
12396
|
if (watcher.inFlight.has(inFlight.evidentMessageId) && activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
|
|
@@ -11108,13 +12592,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
11108
12592
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
11109
12593
|
}
|
|
11110
12594
|
/** Report a confirmed b2 abandonment that produced no answer text. */
|
|
11111
|
-
async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages,
|
|
12595
|
+
async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages, error2) {
|
|
11112
12596
|
const conv = watcher.conv;
|
|
11113
|
-
const error2 = composeAbandonmentNote(
|
|
11114
|
-
finish,
|
|
11115
|
-
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
11116
|
-
"without-answer"
|
|
11117
|
-
);
|
|
11118
12597
|
this.anchorDeliveryDeadline(inFlight);
|
|
11119
12598
|
this.log({
|
|
11120
12599
|
level: "warn",
|
|
@@ -11634,12 +13113,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
11634
13113
|
conversation_id: row.conversation_id,
|
|
11635
13114
|
message_id: row.id
|
|
11636
13115
|
});
|
|
11637
|
-
const
|
|
13116
|
+
const errorMessage4 = this.untrackableAck.get(row.id);
|
|
11638
13117
|
const reportError = await this.reportUntrackableAck(
|
|
11639
13118
|
row.conversation_id,
|
|
11640
13119
|
row.id,
|
|
11641
13120
|
this.untrackableAckSessionIds.get(row.id) ?? null,
|
|
11642
|
-
|
|
13121
|
+
errorMessage4
|
|
11643
13122
|
);
|
|
11644
13123
|
if (reportError !== null) {
|
|
11645
13124
|
this.log({
|
|
@@ -11952,14 +13431,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
11952
13431
|
this.awaitingReadopt.delete(row.id);
|
|
11953
13432
|
if (err instanceof ChannelAuthError) throw err;
|
|
11954
13433
|
if (this.isV2) {
|
|
11955
|
-
const
|
|
13434
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
11956
13435
|
const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
|
|
11957
13436
|
if (!invalidPromptAck) {
|
|
11958
13437
|
const exists = await this.sessionExists(sessionId);
|
|
11959
13438
|
if (exists === false) {
|
|
11960
13439
|
this.log({
|
|
11961
13440
|
level: "warn",
|
|
11962
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch hit a session (${sessionId.slice(0, 8)}) that was deleted mid-dispatch \u2014 deferring to the next drain: ${
|
|
13441
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch hit a session (${sessionId.slice(0, 8)}) that was deleted mid-dispatch \u2014 deferring to the next drain: ${errorMessage4}`,
|
|
11963
13442
|
conversation_id: row.conversation_id,
|
|
11964
13443
|
message_id: row.id
|
|
11965
13444
|
});
|
|
@@ -11969,7 +13448,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
11969
13448
|
if (exists === null) {
|
|
11970
13449
|
this.log({
|
|
11971
13450
|
level: "warn",
|
|
11972
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch failed and session (${sessionId.slice(0, 8)}) existence could not be confirmed \u2014 deferring to the next drain: ${
|
|
13451
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch failed and session (${sessionId.slice(0, 8)}) existence could not be confirmed \u2014 deferring to the next drain: ${errorMessage4}`,
|
|
11973
13452
|
conversation_id: row.conversation_id,
|
|
11974
13453
|
message_id: row.id
|
|
11975
13454
|
});
|
|
@@ -11977,12 +13456,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
11977
13456
|
return;
|
|
11978
13457
|
}
|
|
11979
13458
|
} else {
|
|
11980
|
-
this.untrackableAck.set(row.id,
|
|
13459
|
+
this.untrackableAck.set(row.id, errorMessage4);
|
|
11981
13460
|
this.untrackableAckSessionIds.set(row.id, sessionId);
|
|
11982
13461
|
}
|
|
11983
13462
|
this.log({
|
|
11984
13463
|
level: "error",
|
|
11985
|
-
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${
|
|
13464
|
+
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage4}`,
|
|
11986
13465
|
conversation_id: row.conversation_id,
|
|
11987
13466
|
message_id: row.id
|
|
11988
13467
|
});
|
|
@@ -11991,7 +13470,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
11991
13470
|
row.conversation_id,
|
|
11992
13471
|
row.id,
|
|
11993
13472
|
sessionId,
|
|
11994
|
-
|
|
13473
|
+
errorMessage4
|
|
11995
13474
|
);
|
|
11996
13475
|
if (markError !== null) {
|
|
11997
13476
|
this.log({
|
|
@@ -12002,7 +13481,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12002
13481
|
});
|
|
12003
13482
|
}
|
|
12004
13483
|
} else {
|
|
12005
|
-
await this.markFailed(row.conversation_id, row.id, sessionId,
|
|
13484
|
+
await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage4).catch(
|
|
12006
13485
|
(markErr) => {
|
|
12007
13486
|
this.log({
|
|
12008
13487
|
level: "warn",
|
|
@@ -12986,9 +14465,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12986
14465
|
* trackable. Callers use the returned error to keep their path-specific logging
|
|
12987
14466
|
* while the same report payload can be retried on a later drain.
|
|
12988
14467
|
*/
|
|
12989
|
-
async reportUntrackableAck(conversationId, messageId, sessionId,
|
|
14468
|
+
async reportUntrackableAck(conversationId, messageId, sessionId, errorMessage4) {
|
|
12990
14469
|
try {
|
|
12991
|
-
await this.markFailed(conversationId, messageId, sessionId,
|
|
14470
|
+
await this.markFailed(conversationId, messageId, sessionId, errorMessage4);
|
|
12992
14471
|
return null;
|
|
12993
14472
|
} catch (err) {
|
|
12994
14473
|
this.signalUntrackableAck(conversationId, messageId);
|
|
@@ -13500,7 +14979,7 @@ import ora3 from "ora";
|
|
|
13500
14979
|
import { select as select3 } from "@inquirer/prompts";
|
|
13501
14980
|
function unknownPasswordError2(port) {
|
|
13502
14981
|
return new Error(
|
|
13503
|
-
`
|
|
14982
|
+
`An assistant on port ${port} requires credentials this runner does not have. Released OpenCode V2 generates a server password even when none is configured. This CLI has no option to supply the password for an already-running server, so it will not join one you started. Free port ${port} so Evident can start its own, or pass --port to choose another.`
|
|
13504
14983
|
);
|
|
13505
14984
|
}
|
|
13506
14985
|
var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
|
|
@@ -13518,9 +14997,9 @@ async function ensureOpenCode2Running(ctx) {
|
|
|
13518
14997
|
password: null
|
|
13519
14998
|
};
|
|
13520
14999
|
}
|
|
13521
|
-
if (!isOpenCode2Installed()) {
|
|
15000
|
+
if (!ctx.command && !isOpenCode2Installed()) {
|
|
13522
15001
|
throw new Error(
|
|
13523
|
-
|
|
15002
|
+
"OpenCode V2 is not installed. Install it with `npm install -g @opencode/cli` or `curl -fsSL https://opencode.ai/v2/install | bash`."
|
|
13524
15003
|
);
|
|
13525
15004
|
}
|
|
13526
15005
|
let port = ctx.port;
|
|
@@ -13548,7 +15027,8 @@ Port ${port} is already in use.`));
|
|
|
13548
15027
|
if (!ctx.interactive) {
|
|
13549
15028
|
ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
|
|
13550
15029
|
const { child: proc, password } = await startOpenCode2(port, {
|
|
13551
|
-
inheritStdio: ctx.inheritStdio
|
|
15030
|
+
inheritStdio: ctx.inheritStdio,
|
|
15031
|
+
command: ctx.command
|
|
13552
15032
|
});
|
|
13553
15033
|
const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
|
|
13554
15034
|
if (!health.healthy) {
|
|
@@ -13569,13 +15049,14 @@ Port ${port} is already in use.`));
|
|
|
13569
15049
|
password
|
|
13570
15050
|
};
|
|
13571
15051
|
}
|
|
15052
|
+
const command = ctx.command ?? "opencode2";
|
|
13572
15053
|
const action = await select3({
|
|
13573
15054
|
message: "OpenCode V2 is not running. What would you like to do?",
|
|
13574
15055
|
choices: [
|
|
13575
15056
|
{
|
|
13576
15057
|
name: "Start OpenCode V2 for me",
|
|
13577
15058
|
value: "start",
|
|
13578
|
-
description: `Run '
|
|
15059
|
+
description: `Run '${command} serve --port ${port}'`
|
|
13579
15060
|
},
|
|
13580
15061
|
{
|
|
13581
15062
|
name: "Show me the command",
|
|
@@ -13593,14 +15074,15 @@ Port ${port} is already in use.`));
|
|
|
13593
15074
|
blank();
|
|
13594
15075
|
console.log(chalk6.bold("Run this command in another terminal:"));
|
|
13595
15076
|
blank();
|
|
13596
|
-
console.log(` ${chalk6.cyan(
|
|
15077
|
+
console.log(` ${chalk6.cyan(`${command} serve --port ${port}`)}`);
|
|
13597
15078
|
blank();
|
|
13598
15079
|
throw new Error("Please start OpenCode V2 manually");
|
|
13599
15080
|
}
|
|
13600
15081
|
if (action === "start") {
|
|
13601
15082
|
const spinner = ora3("Starting OpenCode V2...").start();
|
|
13602
15083
|
const { child: proc, password } = await startOpenCode2(port, {
|
|
13603
|
-
inheritStdio: ctx.inheritStdio
|
|
15084
|
+
inheritStdio: ctx.inheritStdio,
|
|
15085
|
+
command: ctx.command
|
|
13604
15086
|
});
|
|
13605
15087
|
const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
|
|
13606
15088
|
if (!health.healthy) {
|
|
@@ -13626,7 +15108,7 @@ Port ${port} is already in use.`));
|
|
|
13626
15108
|
}
|
|
13627
15109
|
|
|
13628
15110
|
// src/lib/runner-credentials.ts
|
|
13629
|
-
import { chmodSync as chmodSync2, writeFileSync as
|
|
15111
|
+
import { chmodSync as chmodSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
13630
15112
|
import { spawn as spawn5 } from "node:child_process";
|
|
13631
15113
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
13632
15114
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
@@ -13869,8 +15351,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
13869
15351
|
}
|
|
13870
15352
|
try {
|
|
13871
15353
|
env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
|
|
13872
|
-
|
|
13873
|
-
|
|
15354
|
+
writeFileSync7(GIT_CONFIG_GLOBAL, "");
|
|
15355
|
+
writeFileSync7(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
|
|
13874
15356
|
chmodSync2(GIT_CREDENTIAL_HELPER, 448);
|
|
13875
15357
|
const config = [
|
|
13876
15358
|
["user.name", env.GIT_USER_NAME ?? "evident-bot"],
|
|
@@ -13903,7 +15385,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
13903
15385
|
// src/lib/opencode/config-overlay.ts
|
|
13904
15386
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
13905
15387
|
import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
|
|
13906
|
-
import { isAbsolute as isAbsolute3, join as
|
|
15388
|
+
import { isAbsolute as isAbsolute3, join as join14, resolve as resolve4 } from "node:path";
|
|
13907
15389
|
function parseConfigContent(content) {
|
|
13908
15390
|
if (!content?.trim()) return {};
|
|
13909
15391
|
const parsed = JSON.parse(content);
|
|
@@ -13933,7 +15415,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
13933
15415
|
return;
|
|
13934
15416
|
}
|
|
13935
15417
|
const source = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
|
|
13936
|
-
const target = isFile(
|
|
15418
|
+
const target = isFile(join14(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
13937
15419
|
if (!isFile(source)) {
|
|
13938
15420
|
log3(
|
|
13939
15421
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -13941,7 +15423,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
13941
15423
|
);
|
|
13942
15424
|
return;
|
|
13943
15425
|
}
|
|
13944
|
-
copyFileSync(source,
|
|
15426
|
+
copyFileSync(source, join14(cwd, target));
|
|
13945
15427
|
try {
|
|
13946
15428
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
13947
15429
|
stdio: "ignore"
|
|
@@ -13950,13 +15432,13 @@ function applyRunnerOpenCodeConfig({
|
|
|
13950
15432
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
13951
15433
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
13952
15434
|
}
|
|
13953
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
15435
|
+
log3(`Applied runner OpenCode config ${source} to ${join14(cwd, target)}`);
|
|
13954
15436
|
}
|
|
13955
15437
|
|
|
13956
15438
|
// src/lib/opencode/runtime-provenance.ts
|
|
13957
|
-
import { readFileSync as
|
|
15439
|
+
import { readFileSync as readFileSync11, readdirSync } from "node:fs";
|
|
13958
15440
|
import { homedir as homedir7 } from "node:os";
|
|
13959
|
-
import { join as
|
|
15441
|
+
import { join as join15 } from "node:path";
|
|
13960
15442
|
var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
|
13961
15443
|
var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
|
|
13962
15444
|
function errorKind(error2) {
|
|
@@ -14024,7 +15506,7 @@ function parseJsonc(value) {
|
|
|
14024
15506
|
return JSON.parse(output);
|
|
14025
15507
|
}
|
|
14026
15508
|
function openCodeCacheRoot(env = process.env, homeDir = homedir7()) {
|
|
14027
|
-
return
|
|
15509
|
+
return join15(env.XDG_CACHE_HOME ?? join15(homeDir, ".cache"), "opencode");
|
|
14028
15510
|
}
|
|
14029
15511
|
function readConfiguredPluginSpecs({
|
|
14030
15512
|
cwd = process.cwd(),
|
|
@@ -14033,10 +15515,10 @@ function readConfiguredPluginSpecs({
|
|
|
14033
15515
|
let config;
|
|
14034
15516
|
try {
|
|
14035
15517
|
try {
|
|
14036
|
-
config = parseJsonc(
|
|
15518
|
+
config = parseJsonc(readFileSync11(join15(cwd, "opencode.jsonc"), "utf8"));
|
|
14037
15519
|
} catch (error2) {
|
|
14038
15520
|
if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
|
|
14039
|
-
config = parseJsonc(
|
|
15521
|
+
config = parseJsonc(readFileSync11(join15(cwd, "opencode.json"), "utf8"));
|
|
14040
15522
|
}
|
|
14041
15523
|
} catch (error2) {
|
|
14042
15524
|
log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
|
|
@@ -14064,7 +15546,7 @@ function resolveInstalledPluginVersions({
|
|
|
14064
15546
|
homeDir = homedir7(),
|
|
14065
15547
|
log: log3
|
|
14066
15548
|
}) {
|
|
14067
|
-
const packagesRoot =
|
|
15549
|
+
const packagesRoot = join15(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
|
|
14068
15550
|
let directories;
|
|
14069
15551
|
try {
|
|
14070
15552
|
directories = readdirSync(packagesRoot, { withFileTypes: true });
|
|
@@ -14085,8 +15567,8 @@ function resolveInstalledPluginVersions({
|
|
|
14085
15567
|
let resolvedVersion = null;
|
|
14086
15568
|
try {
|
|
14087
15569
|
const packageJson = JSON.parse(
|
|
14088
|
-
|
|
14089
|
-
|
|
15570
|
+
readFileSync11(
|
|
15571
|
+
join15(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
|
|
14090
15572
|
"utf8"
|
|
14091
15573
|
)
|
|
14092
15574
|
);
|
|
@@ -14107,7 +15589,7 @@ function resolveInstalledPluginVersions({
|
|
|
14107
15589
|
}
|
|
14108
15590
|
|
|
14109
15591
|
// src/lib/credential-sync.ts
|
|
14110
|
-
import { renameSync, writeFileSync as
|
|
15592
|
+
import { renameSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
14111
15593
|
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
14112
15594
|
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
14113
15595
|
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
@@ -14117,7 +15599,7 @@ var MAX_FLUSH_PASSES = 2;
|
|
|
14117
15599
|
function outcomesWith(outcome) {
|
|
14118
15600
|
return { claude: outcome, opencode: outcome };
|
|
14119
15601
|
}
|
|
14120
|
-
function
|
|
15602
|
+
function errorMessage3(error2) {
|
|
14121
15603
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
14122
15604
|
}
|
|
14123
15605
|
function waitForSettlement(promise, timeoutMs) {
|
|
@@ -14141,10 +15623,10 @@ function writeMarker(markerPath, outcomes, log3) {
|
|
|
14141
15623
|
`;
|
|
14142
15624
|
const temporaryPath = `${markerPath}.tmp`;
|
|
14143
15625
|
try {
|
|
14144
|
-
|
|
15626
|
+
writeFileSync8(temporaryPath, body, { mode: 384 });
|
|
14145
15627
|
renameSync(temporaryPath, markerPath);
|
|
14146
15628
|
} catch (error2) {
|
|
14147
|
-
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${
|
|
15629
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage3(error2)}`, "warn");
|
|
14148
15630
|
}
|
|
14149
15631
|
}
|
|
14150
15632
|
function intervalSeconds(env, log3) {
|
|
@@ -14176,7 +15658,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
|
14176
15658
|
},
|
|
14177
15659
|
(error2) => {
|
|
14178
15660
|
failed = true;
|
|
14179
|
-
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${
|
|
15661
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage3(error2)}`, "warn");
|
|
14180
15662
|
}
|
|
14181
15663
|
);
|
|
14182
15664
|
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
@@ -14236,7 +15718,7 @@ function createCredentialSync({
|
|
|
14236
15718
|
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
14237
15719
|
} catch (error2) {
|
|
14238
15720
|
outcomes[store] = "failed";
|
|
14239
|
-
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${
|
|
15721
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage3(error2)}`, "debug");
|
|
14240
15722
|
}
|
|
14241
15723
|
}
|
|
14242
15724
|
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
@@ -14381,15 +15863,37 @@ async function responseJson(response) {
|
|
|
14381
15863
|
function readSetup(value) {
|
|
14382
15864
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
14383
15865
|
const setup = value;
|
|
14384
|
-
if (typeof setup.runner_id !== "string" || typeof setup.generation !== "string" || typeof setup.status !== "string" || setup.token_id !== null && typeof setup.token_id !== "string") {
|
|
15866
|
+
if (typeof setup.runner_id !== "string" || typeof setup.generation !== "string" || typeof setup.status !== "string" || setup.token_id !== null && typeof setup.token_id !== "string" || typeof setup.acknowledged_at !== "string" && setup.acknowledged_at !== null) {
|
|
14385
15867
|
return null;
|
|
14386
15868
|
}
|
|
14387
15869
|
return setup;
|
|
14388
15870
|
}
|
|
15871
|
+
function readModelDescriptors(value) {
|
|
15872
|
+
if (!Array.isArray(value)) return null;
|
|
15873
|
+
return value.every(
|
|
15874
|
+
(descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
|
|
15875
|
+
) ? value : null;
|
|
15876
|
+
}
|
|
15877
|
+
function logDescriptorRefreshFallback(options, branch, generation) {
|
|
15878
|
+
options.log(
|
|
15879
|
+
`MANAGED-INFERENCE-DESCRIPTORS-${branch}: runner=${options.agentId} generation=${generation}`,
|
|
15880
|
+
"warn"
|
|
15881
|
+
);
|
|
15882
|
+
}
|
|
14389
15883
|
function clearLocalSetup(options) {
|
|
14390
15884
|
const env = options.env ?? process.env;
|
|
14391
|
-
|
|
14392
|
-
|
|
15885
|
+
const store = options.store ?? defaultStore;
|
|
15886
|
+
const stored = store.get(options.agentId);
|
|
15887
|
+
store.clear(options.agentId);
|
|
15888
|
+
const token = env.EVIDENT_PROXY_TOKEN;
|
|
15889
|
+
if (token !== void 0 && token === stored?.token) {
|
|
15890
|
+
delete env.EVIDENT_PROXY_TOKEN;
|
|
15891
|
+
} else if (token !== void 0) {
|
|
15892
|
+
options.log(
|
|
15893
|
+
`MANAGED-INFERENCE-CLEANUP-PRESERVED-EXTERNAL-TOKEN: runner=${options.agentId}`,
|
|
15894
|
+
"debug"
|
|
15895
|
+
);
|
|
15896
|
+
}
|
|
14393
15897
|
}
|
|
14394
15898
|
async function discoverManagedInferenceSetup(options) {
|
|
14395
15899
|
const headers = requestHeaders(options);
|
|
@@ -14413,6 +15917,25 @@ async function discoverManagedInferenceSetup(options) {
|
|
|
14413
15917
|
const setup = readSetup(body?.setup);
|
|
14414
15918
|
return setup ? { kind: "setup", setup } : { kind: "failed", reason: "Managed setup discovery returned an invalid response" };
|
|
14415
15919
|
}
|
|
15920
|
+
async function enrollManagedInferenceSetup(options) {
|
|
15921
|
+
const response = await post(options, "enroll", {});
|
|
15922
|
+
if (!response) {
|
|
15923
|
+
return { kind: "failed", reason: safeFailure("Managed credential enrollment") };
|
|
15924
|
+
}
|
|
15925
|
+
if (response.status === 404) {
|
|
15926
|
+
return { kind: "none" };
|
|
15927
|
+
}
|
|
15928
|
+
const body = await responseJson(response);
|
|
15929
|
+
if (!response.ok) {
|
|
15930
|
+
const serverError = body?.error;
|
|
15931
|
+
return {
|
|
15932
|
+
kind: "failed",
|
|
15933
|
+
reason: typeof serverError === "string" && serverError.trim() ? redactDiagnosticText(serverError).slice(0, 500) : safeFailure("Managed credential enrollment", response.status)
|
|
15934
|
+
};
|
|
15935
|
+
}
|
|
15936
|
+
const setup = readSetup(body?.setup);
|
|
15937
|
+
return setup ? { kind: "setup", setup } : { kind: "failed", reason: "Managed credential enrollment returned an invalid response" };
|
|
15938
|
+
}
|
|
14416
15939
|
async function post(options, operation, body) {
|
|
14417
15940
|
const headers = requestHeaders(options);
|
|
14418
15941
|
try {
|
|
@@ -14488,7 +16011,27 @@ async function reaffirmStored(options, credential) {
|
|
|
14488
16011
|
if (!setup || setup.generation !== credential.generation || setup.token_id !== credential.tokenId) {
|
|
14489
16012
|
return { kind: "failed", reason: "Managed credential reaffirmation returned a mismatch" };
|
|
14490
16013
|
}
|
|
14491
|
-
const
|
|
16014
|
+
const descriptorsValue = body?.model_descriptors;
|
|
16015
|
+
const refreshedDescriptors = readModelDescriptors(descriptorsValue);
|
|
16016
|
+
let credentialToApply = credential;
|
|
16017
|
+
if (descriptorsValue === void 0 || descriptorsValue === null) {
|
|
16018
|
+
logDescriptorRefreshFallback(options, "UNAVAILABLE", credential.generation);
|
|
16019
|
+
} else if (refreshedDescriptors === null) {
|
|
16020
|
+
logDescriptorRefreshFallback(options, "MALFORMED", credential.generation);
|
|
16021
|
+
} else if (refreshedDescriptors.length === 0) {
|
|
16022
|
+
logDescriptorRefreshFallback(options, "EMPTY", credential.generation);
|
|
16023
|
+
} else {
|
|
16024
|
+
const { config, unsupportedProviders } = buildCredentialProxyNativeConfig({
|
|
16025
|
+
inferenceBaseUrl: credential.inferenceBaseUrl,
|
|
16026
|
+
modelDescriptors: refreshedDescriptors
|
|
16027
|
+
});
|
|
16028
|
+
if (unsupportedProviders.length > 0 || Object.keys(config.provider).length === 0) {
|
|
16029
|
+
logDescriptorRefreshFallback(options, "UNSUPPORTED-PROVIDER", credential.generation);
|
|
16030
|
+
} else {
|
|
16031
|
+
credentialToApply = { ...credential, modelDescriptors: refreshedDescriptors };
|
|
16032
|
+
}
|
|
16033
|
+
}
|
|
16034
|
+
const failure = applyCredential(options, credentialToApply);
|
|
14492
16035
|
return failure ? { kind: "failed", reason: failure } : { kind: "ready", generation: credential.generation, tokenId: credential.tokenId };
|
|
14493
16036
|
}
|
|
14494
16037
|
async function prepareManagedInferenceSetup(options, discovery, health) {
|
|
@@ -14508,7 +16051,7 @@ async function prepareManagedInferenceSetup(options, discovery, health) {
|
|
|
14508
16051
|
}
|
|
14509
16052
|
return reaffirmStored(options, stored);
|
|
14510
16053
|
}
|
|
14511
|
-
if (discovery.setup.
|
|
16054
|
+
if (discovery.setup.acknowledged_at !== null && stored?.generation === discovery.setup.generation && stored.tokenId === discovery.setup.token_id) {
|
|
14512
16055
|
return reaffirmStored(options, stored);
|
|
14513
16056
|
}
|
|
14514
16057
|
const response = await post(options, "bootstrap", { generation: discovery.setup.generation });
|
|
@@ -14522,10 +16065,8 @@ async function prepareManagedInferenceSetup(options, discovery, health) {
|
|
|
14522
16065
|
const setup = readSetup(body?.setup);
|
|
14523
16066
|
const token = body?.inference_token;
|
|
14524
16067
|
const inferenceBaseUrl = body?.inference_base_url;
|
|
14525
|
-
const modelDescriptors = body?.model_descriptors;
|
|
14526
|
-
if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" ||
|
|
14527
|
-
(descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
|
|
14528
|
-
)) {
|
|
16068
|
+
const modelDescriptors = readModelDescriptors(body?.model_descriptors);
|
|
16069
|
+
if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" || modelDescriptors === null) {
|
|
14529
16070
|
return { kind: "failed", reason: "Managed credential bootstrap returned an invalid response" };
|
|
14530
16071
|
}
|
|
14531
16072
|
const credential = {
|
|
@@ -14594,33 +16135,49 @@ function resolveLogLevel(options) {
|
|
|
14594
16135
|
}
|
|
14595
16136
|
return "info";
|
|
14596
16137
|
}
|
|
14597
|
-
function
|
|
14598
|
-
const
|
|
16138
|
+
function resolveFileSyncEntry(entry, homeDir, flagName, labels) {
|
|
16139
|
+
const trimmed = entry.trim();
|
|
16140
|
+
if (trimmed === "") {
|
|
16141
|
+
throw new Error(`${flagName} requires a ${labels.noun} path (got an empty value)`);
|
|
16142
|
+
}
|
|
16143
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join16(homeDir, trimmed.slice(2)) : trimmed;
|
|
16144
|
+
if (!isAbsolute4(expanded)) {
|
|
16145
|
+
throw new Error(`${flagName} requires an absolute ${labels.noun} path; got "${entry}"`);
|
|
16146
|
+
}
|
|
16147
|
+
const normalized = resolvePath2(expanded);
|
|
16148
|
+
if (parse(normalized).root === normalized) {
|
|
16149
|
+
throw new Error(
|
|
16150
|
+
`${flagName} will not allow-list the filesystem root ("${entry}"); name the specific ${labels.noun} the credentials belong in (for example ${labels.rootExample})`
|
|
16151
|
+
);
|
|
16152
|
+
}
|
|
16153
|
+
return normalized;
|
|
16154
|
+
}
|
|
16155
|
+
function resolveFileSyncAllowList(raw, homeDir, flagName, labels) {
|
|
16156
|
+
const paths = [];
|
|
14599
16157
|
for (const entry of raw ?? []) {
|
|
14600
|
-
const
|
|
14601
|
-
if (
|
|
14602
|
-
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
14603
|
-
}
|
|
14604
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join13(homeDir, trimmed.slice(2)) : trimmed;
|
|
14605
|
-
if (!isAbsolute4(expanded)) {
|
|
14606
|
-
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
14607
|
-
}
|
|
14608
|
-
const normalized = resolvePath2(expanded);
|
|
14609
|
-
if (parse(normalized).root === normalized) {
|
|
14610
|
-
throw new Error(
|
|
14611
|
-
`--enable-file-sync-to will not allow-list the filesystem root ("${entry}"); name the specific directory the credentials belong in (for example ~/.claude)`
|
|
14612
|
-
);
|
|
14613
|
-
}
|
|
14614
|
-
if (!directories.includes(normalized)) {
|
|
14615
|
-
directories.push(normalized);
|
|
14616
|
-
}
|
|
16158
|
+
const normalized = resolveFileSyncEntry(entry, homeDir, flagName, labels);
|
|
16159
|
+
if (!paths.includes(normalized)) paths.push(normalized);
|
|
14617
16160
|
}
|
|
14618
|
-
if (
|
|
16161
|
+
if (paths.length > MAX_FILE_SYNC_DIRECTORIES) {
|
|
14619
16162
|
throw new Error(
|
|
14620
|
-
|
|
16163
|
+
`${flagName} accepts at most ${MAX_FILE_SYNC_DIRECTORIES} ${labels.plural}; got ${paths.length}`
|
|
14621
16164
|
);
|
|
14622
16165
|
}
|
|
14623
|
-
return
|
|
16166
|
+
return paths;
|
|
16167
|
+
}
|
|
16168
|
+
function resolveFileSyncDirectories(raw, homeDir) {
|
|
16169
|
+
return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to", {
|
|
16170
|
+
noun: "directory",
|
|
16171
|
+
plural: "directories",
|
|
16172
|
+
rootExample: "~/.claude"
|
|
16173
|
+
});
|
|
16174
|
+
}
|
|
16175
|
+
function resolveFileSyncExactFiles(raw, homeDir) {
|
|
16176
|
+
return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to-exact-file", {
|
|
16177
|
+
noun: "file",
|
|
16178
|
+
plural: "files",
|
|
16179
|
+
rootExample: "~/.local/share/opencode/mcp-auth.json"
|
|
16180
|
+
});
|
|
14624
16181
|
}
|
|
14625
16182
|
var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
|
|
14626
16183
|
var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
|
|
@@ -14636,13 +16193,13 @@ function resolveOpenCodeVersion(options, env = process.env) {
|
|
|
14636
16193
|
raw = env[OPENCODE_VERSION_ENV];
|
|
14637
16194
|
source = OPENCODE_VERSION_ENV;
|
|
14638
16195
|
} else {
|
|
14639
|
-
return { version: "
|
|
16196
|
+
return { version: "auto", warnings: [] };
|
|
14640
16197
|
}
|
|
14641
16198
|
const normalized = raw.trim().toLowerCase();
|
|
14642
|
-
if (normalized !== "v1" && normalized !== "v2") {
|
|
16199
|
+
if (normalized !== "v1" && normalized !== "v2" && normalized !== "auto") {
|
|
14643
16200
|
return {
|
|
14644
|
-
version: "
|
|
14645
|
-
warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or
|
|
16201
|
+
version: "auto",
|
|
16202
|
+
warnings: [`Ignoring invalid ${source} "${raw}": expected v1, v2, or auto; using auto`]
|
|
14646
16203
|
};
|
|
14647
16204
|
}
|
|
14648
16205
|
return { version: normalized, warnings: [] };
|
|
@@ -15152,8 +16709,8 @@ async function driveChannels(state, driver) {
|
|
|
15152
16709
|
state.running = false;
|
|
15153
16710
|
break;
|
|
15154
16711
|
}
|
|
15155
|
-
const
|
|
15156
|
-
logActivity(state, { type: "error", error: `Channel processing error: ${
|
|
16712
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : String(error2);
|
|
16713
|
+
logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage4}` });
|
|
15157
16714
|
if (state.interactive) displayStatus(state);
|
|
15158
16715
|
if (driver.hasInFlightWatchers()) {
|
|
15159
16716
|
consecutiveDrainFailures = 0;
|
|
@@ -15243,6 +16800,7 @@ async function driveChannels(state, driver) {
|
|
|
15243
16800
|
}
|
|
15244
16801
|
}
|
|
15245
16802
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
16803
|
+
var SESSION_ARCHIVE_FIRST_RUN_MS = SESSION_CLEANUP_FIRST_SWEEP_MS + 1e3;
|
|
15246
16804
|
var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
|
|
15247
16805
|
var SESSION_CLEANUP_MAX_ISOLATED_DELETE_FAILURES = 2;
|
|
15248
16806
|
var SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES = 2;
|
|
@@ -15257,7 +16815,7 @@ function shouldWarnForReclaimSkip(reason) {
|
|
|
15257
16815
|
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
15258
16816
|
}
|
|
15259
16817
|
function sessionDbPath() {
|
|
15260
|
-
return
|
|
16818
|
+
return join16(homedir8(), ".local", "share", "opencode", "opencode.db");
|
|
15261
16819
|
}
|
|
15262
16820
|
function logSessionCleanupActivity(state, phase, entry) {
|
|
15263
16821
|
logActivity(state, {
|
|
@@ -15266,6 +16824,104 @@ function logSessionCleanupActivity(state, phase, entry) {
|
|
|
15266
16824
|
metadata: { ...entry.metadata, phase }
|
|
15267
16825
|
});
|
|
15268
16826
|
}
|
|
16827
|
+
function logSessionArchiveActivity(state, phase, entry) {
|
|
16828
|
+
logActivity(state, {
|
|
16829
|
+
...entry,
|
|
16830
|
+
source: "cli.session-archive",
|
|
16831
|
+
metadata: { ...entry.metadata, phase }
|
|
16832
|
+
});
|
|
16833
|
+
}
|
|
16834
|
+
function logSessionArchiveSummary(state, phase, summary) {
|
|
16835
|
+
const everyUploadFailed = summary.uploadAttempts > 0 && summary.uploadFailures === summary.uploadAttempts;
|
|
16836
|
+
const staleSince = everyUploadFailed && summary.staleSince !== null ? `; archive stale since ${summary.staleSince}` : "";
|
|
16837
|
+
const neverWatermarked = summary.neverWatermarkedFailures > 0 ? `; ${summary.neverWatermarkedFailures} failed session(s) had no prior successful upload` : "";
|
|
16838
|
+
const warning2 = summary.failed > 0 || summary.failures.length > 0 || everyUploadFailed;
|
|
16839
|
+
logSessionArchiveActivity(state, phase, {
|
|
16840
|
+
type: "info",
|
|
16841
|
+
level: warning2 ? "warn" : "info",
|
|
16842
|
+
message: `Session archive: inspected ${summary.inspected}, uploaded ${summary.uploaded}, unchanged ${summary.unchanged}, failed ${summary.failed}, skipped budget ${summary.skippedBudget}, unmapped ${summary.unmappedIds.length}` + staleSince + neverWatermarked,
|
|
16843
|
+
metadata: {
|
|
16844
|
+
outcome: "archive-run-complete",
|
|
16845
|
+
sessions_inspected: summary.inspected,
|
|
16846
|
+
sessions_uploaded: summary.uploaded,
|
|
16847
|
+
sessions_unchanged: summary.unchanged,
|
|
16848
|
+
sessions_failed: summary.failed,
|
|
16849
|
+
uploads_attempted: summary.uploadAttempts,
|
|
16850
|
+
uploads_failed: summary.uploadFailures,
|
|
16851
|
+
sessions_skipped_budget: summary.skippedBudget,
|
|
16852
|
+
sessions_unmapped: summary.unmappedIds.length,
|
|
16853
|
+
sessions_without_prior_watermark: summary.neverWatermarkedFailures,
|
|
16854
|
+
...everyUploadFailed && summary.staleSince !== null ? { stale_since: summary.staleSince } : {}
|
|
16855
|
+
}
|
|
16856
|
+
});
|
|
16857
|
+
for (const failure of summary.failures) {
|
|
16858
|
+
const permanentSizeFailure = failure.reason === "too-large";
|
|
16859
|
+
const safeMessage = failure.message ? `: ${redactDiagnosticText(failure.message).slice(0, 200)}` : "";
|
|
16860
|
+
logSessionArchiveActivity(state, phase, {
|
|
16861
|
+
type: "info",
|
|
16862
|
+
level: "warn",
|
|
16863
|
+
message: `Session archive: session ${failure.sessionId} ` + (permanentSizeFailure ? "failed permanently (too-large payload)" : `failed (${failure.outcome}`) + (permanentSizeFailure ? "" : failure.reason ? `, reason=${failure.reason})` : ")") + safeMessage,
|
|
16864
|
+
metadata: {
|
|
16865
|
+
outcome: failure.outcome,
|
|
16866
|
+
session_id: failure.sessionId,
|
|
16867
|
+
archived: failure.archived,
|
|
16868
|
+
...failure.reason !== void 0 ? { reason: failure.reason } : {}
|
|
16869
|
+
}
|
|
16870
|
+
});
|
|
16871
|
+
}
|
|
16872
|
+
if (summary.unmappedIds.length > 0) {
|
|
16873
|
+
logSessionArchiveActivity(state, phase, {
|
|
16874
|
+
type: "info",
|
|
16875
|
+
level: "warn",
|
|
16876
|
+
message: `Session archive: ${summary.unmappedIds.length} unmapped session(s): ` + summary.unmappedIds.join(", "),
|
|
16877
|
+
metadata: {
|
|
16878
|
+
outcome: "unmapped-sessions",
|
|
16879
|
+
sessions_unmapped: summary.unmappedIds.length
|
|
16880
|
+
}
|
|
16881
|
+
});
|
|
16882
|
+
}
|
|
16883
|
+
}
|
|
16884
|
+
async function drainSessionDeletionReports(state) {
|
|
16885
|
+
const path = sessionDeletionReportPath(homedir8(), process.env);
|
|
16886
|
+
let delivered = 0;
|
|
16887
|
+
let failure = null;
|
|
16888
|
+
try {
|
|
16889
|
+
const spool = readSessionDeletionRecords(path);
|
|
16890
|
+
if (spool.readError !== null) {
|
|
16891
|
+
failure = `could not read spool: ${spool.readError}`;
|
|
16892
|
+
} else if (spool.records.length === 0) {
|
|
16893
|
+
return;
|
|
16894
|
+
} else if (!state.agentId || !state.authHeader) {
|
|
16895
|
+
failure = "runner authentication is not available";
|
|
16896
|
+
} else {
|
|
16897
|
+
for (let offset = 0; offset < spool.records.length; offset += 200) {
|
|
16898
|
+
const batch = spool.records.slice(offset, offset + 200);
|
|
16899
|
+
const result = await reportSessionDeletions(state.agentId, state.authHeader, batch);
|
|
16900
|
+
if (!result.ok) {
|
|
16901
|
+
failure = result.error ?? "unknown API error";
|
|
16902
|
+
break;
|
|
16903
|
+
}
|
|
16904
|
+
removeDeliveredSessionDeletionRecords(path, batch);
|
|
16905
|
+
delivered += batch.length;
|
|
16906
|
+
}
|
|
16907
|
+
}
|
|
16908
|
+
} catch (error2) {
|
|
16909
|
+
failure = error2 instanceof Error ? error2.message : String(error2);
|
|
16910
|
+
}
|
|
16911
|
+
const pending = readSessionDeletionRecords(path);
|
|
16912
|
+
const pendingCount = pending.readError === null ? pending.records.length : null;
|
|
16913
|
+
const message = `Session deletion reports: delivered ${delivered}, ${pendingCount === null ? "pending count unavailable" : `${pendingCount} pending`}` + (failure === null ? "." : `; delivery failed: ${failure}.`);
|
|
16914
|
+
logActivity(state, {
|
|
16915
|
+
type: "info",
|
|
16916
|
+
level: failure === null ? "info" : "warn",
|
|
16917
|
+
message,
|
|
16918
|
+
metadata: {
|
|
16919
|
+
outcome: failure === null ? "session-deletion-reports-drained" : "session-deletion-reports-pending",
|
|
16920
|
+
delivered,
|
|
16921
|
+
pending: pendingCount
|
|
16922
|
+
}
|
|
16923
|
+
});
|
|
16924
|
+
}
|
|
15269
16925
|
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
15270
16926
|
const record = {
|
|
15271
16927
|
v: 1,
|
|
@@ -15322,18 +16978,25 @@ async function runSweep(state, driver, config) {
|
|
|
15322
16978
|
let failedOtherStatus = 0;
|
|
15323
16979
|
let failedThrew = 0;
|
|
15324
16980
|
let skippedNewlyActive = 0;
|
|
16981
|
+
let skippedArchive = 0;
|
|
15325
16982
|
let remaining = 0;
|
|
16983
|
+
const deletionRecords = [];
|
|
15326
16984
|
let protectedNow = driver.protectedSessionIds();
|
|
15327
16985
|
if (sessions !== null) {
|
|
15328
|
-
const
|
|
15329
|
-
|
|
15330
|
-
|
|
15331
|
-
|
|
15332
|
-
|
|
15333
|
-
|
|
15334
|
-
protectedIds: protectedNow
|
|
15335
|
-
}
|
|
16986
|
+
const snapshots = sessions.map((session) => ({
|
|
16987
|
+
id: session.id,
|
|
16988
|
+
lastActivityMs: sessionLastActivityMs(session)
|
|
16989
|
+
}));
|
|
16990
|
+
const lastActivityById = new Map(
|
|
16991
|
+
snapshots.map(({ id, lastActivityMs }) => [id, lastActivityMs])
|
|
15336
16992
|
);
|
|
16993
|
+
const sessionById = new Map(sessions.map((session) => [session.id, session]));
|
|
16994
|
+
const toDelete = selectSessionsToDelete(snapshots, {
|
|
16995
|
+
maxAgeMs: config.maxAgeMs,
|
|
16996
|
+
maxCount: config.maxCount,
|
|
16997
|
+
nowMs: Date.now(),
|
|
16998
|
+
protectedIds: protectedNow
|
|
16999
|
+
});
|
|
15337
17000
|
const eligibleToDelete = toDelete.filter(
|
|
15338
17001
|
(id) => (state.sessionDeleteFailures.get(id)?.isolatedFailures ?? 0) < SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES
|
|
15339
17002
|
);
|
|
@@ -15350,9 +17013,56 @@ async function runSweep(state, driver, config) {
|
|
|
15350
17013
|
});
|
|
15351
17014
|
continue;
|
|
15352
17015
|
}
|
|
17016
|
+
if (state.sessionArchiveConfig.enabled) {
|
|
17017
|
+
const candidate = sessionById.get(id);
|
|
17018
|
+
const archiveConfig = state.sessionArchiveConfig;
|
|
17019
|
+
const archiveBudgetMs = Math.min(
|
|
17020
|
+
archiveConfig.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
|
|
17021
|
+
MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
|
|
17022
|
+
);
|
|
17023
|
+
const archived = candidate ? await runSessionArchiveTask(
|
|
17024
|
+
state,
|
|
17025
|
+
"sweep",
|
|
17026
|
+
archiveBudgetMs,
|
|
17027
|
+
(signal) => runSessionArchiveBatch(state, "sweep", [candidate], archiveBudgetMs, signal)
|
|
17028
|
+
) : null;
|
|
17029
|
+
if (!archived?.archivedSessionIds.includes(id)) {
|
|
17030
|
+
skippedArchive++;
|
|
17031
|
+
logSessionArchiveActivity(state, "sweep", {
|
|
17032
|
+
type: "info",
|
|
17033
|
+
level: "warn",
|
|
17034
|
+
message: `Session cleanup: retaining session ${id}; deletion is held back pending successful archival`,
|
|
17035
|
+
metadata: {
|
|
17036
|
+
outcome: "deletion-held-for-archive",
|
|
17037
|
+
session_id: id
|
|
17038
|
+
}
|
|
17039
|
+
});
|
|
17040
|
+
continue;
|
|
17041
|
+
}
|
|
17042
|
+
protectedNow = driver.protectedSessionIds();
|
|
17043
|
+
if (protectedNow.has(id)) {
|
|
17044
|
+
skippedNewlyActive++;
|
|
17045
|
+
logActivity(state, {
|
|
17046
|
+
type: "info",
|
|
17047
|
+
message: `Session cleanup: skipping ${id} \u2014 became active/bound during archival (${mode})`
|
|
17048
|
+
});
|
|
17049
|
+
continue;
|
|
17050
|
+
}
|
|
17051
|
+
}
|
|
15353
17052
|
const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
|
|
15354
17053
|
if (outcome.ok) {
|
|
15355
17054
|
deleted++;
|
|
17055
|
+
const lastActivityMs = lastActivityById.get(id) ?? null;
|
|
17056
|
+
deletionRecords.push({
|
|
17057
|
+
opencode_session_id: id,
|
|
17058
|
+
deleted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17059
|
+
reason: "auto_cleanup",
|
|
17060
|
+
policy: {
|
|
17061
|
+
max_age_ms: config.maxAgeMs ?? null,
|
|
17062
|
+
max_count: config.maxCount ?? null,
|
|
17063
|
+
last_activity_at: lastActivityMs === null ? null : new Date(lastActivityMs).toISOString()
|
|
17064
|
+
}
|
|
17065
|
+
});
|
|
15356
17066
|
} else if (outcome.status === "threw") {
|
|
15357
17067
|
failed++;
|
|
15358
17068
|
failedThrew++;
|
|
@@ -15402,12 +17112,39 @@ async function runSweep(state, driver, config) {
|
|
|
15402
17112
|
}
|
|
15403
17113
|
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
15404
17114
|
const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
|
|
17115
|
+
const archiveHeldNote = skippedArchive > 0 ? `, archive held ${skippedArchive}` : "";
|
|
15405
17116
|
const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
|
|
15406
17117
|
logActivity(state, {
|
|
15407
17118
|
type: "info",
|
|
15408
|
-
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
|
|
17119
|
+
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${archiveHeldNote}${remainingNote} (${mode})`
|
|
15409
17120
|
});
|
|
15410
17121
|
}
|
|
17122
|
+
if (deletionRecords.length > 0) {
|
|
17123
|
+
try {
|
|
17124
|
+
const dropped = appendSessionDeletionRecords(
|
|
17125
|
+
sessionDeletionReportPath(homedir8(), process.env),
|
|
17126
|
+
deletionRecords
|
|
17127
|
+
);
|
|
17128
|
+
if (dropped > 0) {
|
|
17129
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
17130
|
+
type: "info",
|
|
17131
|
+
level: "warn",
|
|
17132
|
+
message: `Session cleanup: dropped ${dropped} oldest pending deletion report(s) after reaching the spool limit`,
|
|
17133
|
+
metadata: { outcome: "deletion-reports-dropped", dropped_count: dropped }
|
|
17134
|
+
});
|
|
17135
|
+
}
|
|
17136
|
+
} catch (error2) {
|
|
17137
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
17138
|
+
type: "info",
|
|
17139
|
+
level: "warn",
|
|
17140
|
+
message: `Session cleanup: could not persist ${deletionRecords.length} confirmed deletion report(s): ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
17141
|
+
metadata: {
|
|
17142
|
+
outcome: "deletion-report-spool-write-failed",
|
|
17143
|
+
records: deletionRecords.length
|
|
17144
|
+
}
|
|
17145
|
+
});
|
|
17146
|
+
}
|
|
17147
|
+
}
|
|
15411
17148
|
const sessionMetadata = {
|
|
15412
17149
|
sessions_inspected: sessions?.length ?? null,
|
|
15413
17150
|
sessions_deleted: deleted,
|
|
@@ -15417,6 +17154,7 @@ async function runSweep(state, driver, config) {
|
|
|
15417
17154
|
sessions_failed_other: failedOtherStatus,
|
|
15418
17155
|
sessions_failed_threw: failedThrew,
|
|
15419
17156
|
sessions_skipped_active: skippedNewlyActive,
|
|
17157
|
+
sessions_archive_held: skippedArchive,
|
|
15420
17158
|
sessions_remaining: remaining,
|
|
15421
17159
|
...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
|
|
15422
17160
|
};
|
|
@@ -15477,6 +17215,7 @@ async function runSweep(state, driver, config) {
|
|
|
15477
17215
|
}
|
|
15478
17216
|
});
|
|
15479
17217
|
}
|
|
17218
|
+
await drainSessionDeletionReports(state);
|
|
15480
17219
|
} catch (error2) {
|
|
15481
17220
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
15482
17221
|
logSessionCleanupActivity(state, "sweep", {
|
|
@@ -15547,6 +17286,186 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
15547
17286
|
);
|
|
15548
17287
|
state.sessionCleanupTimers.push(interval, firstSweep);
|
|
15549
17288
|
}
|
|
17289
|
+
function runSessionArchiveTask(state, phase, budgetMs, work) {
|
|
17290
|
+
if (state.sessionArchiveInFlight) {
|
|
17291
|
+
logSessionArchiveActivity(state, phase, {
|
|
17292
|
+
type: "info",
|
|
17293
|
+
level: "debug",
|
|
17294
|
+
message: "Session archive skipped \u2014 previous archive run still in progress",
|
|
17295
|
+
metadata: { outcome: "skipped-in-flight" }
|
|
17296
|
+
});
|
|
17297
|
+
return Promise.resolve(null);
|
|
17298
|
+
}
|
|
17299
|
+
const controller = new AbortController();
|
|
17300
|
+
state.sessionArchiveAbortController = controller;
|
|
17301
|
+
const timeout = setTimeout(() => {
|
|
17302
|
+
controller.abort();
|
|
17303
|
+
logSessionArchiveActivity(state, phase, {
|
|
17304
|
+
type: "info",
|
|
17305
|
+
level: "warn",
|
|
17306
|
+
message: `Session archive exceeded ${budgetMs}ms; remaining sessions were not confirmed archived`,
|
|
17307
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17308
|
+
});
|
|
17309
|
+
}, budgetMs);
|
|
17310
|
+
const task = Promise.resolve().then(() => work(controller.signal)).catch((error2) => {
|
|
17311
|
+
logSessionArchiveActivity(state, phase, {
|
|
17312
|
+
type: "info",
|
|
17313
|
+
level: "warn",
|
|
17314
|
+
message: `Session archive run failed: ${redactDiagnosticText(error2 instanceof Error ? error2.message : String(error2)).slice(0, 200)}`,
|
|
17315
|
+
metadata: { outcome: "archive-run-failed" }
|
|
17316
|
+
});
|
|
17317
|
+
return null;
|
|
17318
|
+
}).finally(() => {
|
|
17319
|
+
clearTimeout(timeout);
|
|
17320
|
+
if (state.sessionArchiveInFlight === task) {
|
|
17321
|
+
state.sessionArchiveInFlight = null;
|
|
17322
|
+
state.sessionArchiveAbortController = null;
|
|
17323
|
+
}
|
|
17324
|
+
});
|
|
17325
|
+
state.sessionArchiveInFlight = task;
|
|
17326
|
+
return task;
|
|
17327
|
+
}
|
|
17328
|
+
async function runSessionArchiveBatch(state, phase, sessions, budgetMs, signal) {
|
|
17329
|
+
const config = state.sessionArchiveConfig;
|
|
17330
|
+
const client = state.opencodeClient;
|
|
17331
|
+
const watermarkPath = state.sessionArchiveWatermarkPath;
|
|
17332
|
+
if (!client || !config.keyPrefix || !watermarkPath || !state.agentId) {
|
|
17333
|
+
logSessionArchiveActivity(state, phase, {
|
|
17334
|
+
type: "info",
|
|
17335
|
+
level: "warn",
|
|
17336
|
+
message: "Session archive skipped because runner identity or archive configuration is unavailable",
|
|
17337
|
+
metadata: { outcome: "archive-configuration-unavailable" }
|
|
17338
|
+
});
|
|
17339
|
+
return null;
|
|
17340
|
+
}
|
|
17341
|
+
const summary = await runSessionArchive({
|
|
17342
|
+
sessions,
|
|
17343
|
+
deps: {
|
|
17344
|
+
client,
|
|
17345
|
+
runnerId: state.agentId,
|
|
17346
|
+
keyPrefix: config.keyPrefix,
|
|
17347
|
+
bindings: readSessionBindings(
|
|
17348
|
+
sessionBindingLedgerPath(homedir8(), process.env),
|
|
17349
|
+
(message) => logSessionArchiveActivity(state, phase, {
|
|
17350
|
+
type: "info",
|
|
17351
|
+
level: "warn",
|
|
17352
|
+
message: redactDiagnosticText(message),
|
|
17353
|
+
metadata: { outcome: "binding-ledger-read-failed" }
|
|
17354
|
+
})
|
|
17355
|
+
),
|
|
17356
|
+
watermarks: state.sessionArchiveWatermarks,
|
|
17357
|
+
watermarkPath,
|
|
17358
|
+
signal,
|
|
17359
|
+
upload: uploadSessionArchive,
|
|
17360
|
+
exportMessages: exportSessionMessages,
|
|
17361
|
+
recordWatermark: recordUpload
|
|
17362
|
+
},
|
|
17363
|
+
budgetMs
|
|
17364
|
+
});
|
|
17365
|
+
logSessionArchiveSummary(state, phase, summary);
|
|
17366
|
+
return summary;
|
|
17367
|
+
}
|
|
17368
|
+
async function runSessionArchiveFleet(state, phase, budgetMs, signal) {
|
|
17369
|
+
const startedAt = Date.now();
|
|
17370
|
+
const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0, signal);
|
|
17371
|
+
if (enumeration.sessions === null) {
|
|
17372
|
+
const interrupted = signal.aborted;
|
|
17373
|
+
logSessionArchiveActivity(state, phase, {
|
|
17374
|
+
type: "info",
|
|
17375
|
+
level: "warn",
|
|
17376
|
+
message: interrupted ? "Session archive: enumeration was interrupted before the complete store was known; no session was marked archived" : `Session archive: could not enumerate the complete session store (${enumeration.reason}); no session was marked archived`,
|
|
17377
|
+
metadata: {
|
|
17378
|
+
outcome: interrupted ? "enumeration-interrupted" : "enumeration-refused",
|
|
17379
|
+
...!interrupted ? { reason: enumeration.reason } : {}
|
|
17380
|
+
}
|
|
17381
|
+
});
|
|
17382
|
+
return null;
|
|
17383
|
+
}
|
|
17384
|
+
return runSessionArchiveBatch(
|
|
17385
|
+
state,
|
|
17386
|
+
phase,
|
|
17387
|
+
enumeration.sessions,
|
|
17388
|
+
Math.max(0, budgetMs - (Date.now() - startedAt)),
|
|
17389
|
+
signal
|
|
17390
|
+
);
|
|
17391
|
+
}
|
|
17392
|
+
async function runSessionArchiveShutdownFlush(state, budgetMs) {
|
|
17393
|
+
const deadline = Date.now() + budgetMs;
|
|
17394
|
+
const previousRun = state.sessionArchiveInFlight;
|
|
17395
|
+
if (previousRun) {
|
|
17396
|
+
state.sessionArchiveAbortController?.abort(
|
|
17397
|
+
new Error("graceful shutdown archive flush requested")
|
|
17398
|
+
);
|
|
17399
|
+
let timeout;
|
|
17400
|
+
const settled = await Promise.race([
|
|
17401
|
+
previousRun.then(() => true),
|
|
17402
|
+
new Promise((resolve5) => {
|
|
17403
|
+
timeout = setTimeout(() => resolve5(false), budgetMs);
|
|
17404
|
+
})
|
|
17405
|
+
]);
|
|
17406
|
+
if (timeout) clearTimeout(timeout);
|
|
17407
|
+
if (!settled) {
|
|
17408
|
+
state.sessionArchiveAbortController?.abort(new Error("session archive flush budget expired"));
|
|
17409
|
+
logSessionArchiveActivity(state, "shutdown", {
|
|
17410
|
+
type: "info",
|
|
17411
|
+
level: "warn",
|
|
17412
|
+
message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
|
|
17413
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17414
|
+
});
|
|
17415
|
+
return null;
|
|
17416
|
+
}
|
|
17417
|
+
}
|
|
17418
|
+
const remainingMs = Math.floor(deadline - Date.now());
|
|
17419
|
+
if (remainingMs <= 0) {
|
|
17420
|
+
logSessionArchiveActivity(state, "shutdown", {
|
|
17421
|
+
type: "info",
|
|
17422
|
+
level: "warn",
|
|
17423
|
+
message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
|
|
17424
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17425
|
+
});
|
|
17426
|
+
return null;
|
|
17427
|
+
}
|
|
17428
|
+
return runSessionArchiveTask(
|
|
17429
|
+
state,
|
|
17430
|
+
"shutdown",
|
|
17431
|
+
remainingMs,
|
|
17432
|
+
(signal) => runSessionArchiveFleet(state, "shutdown", remainingMs, signal)
|
|
17433
|
+
);
|
|
17434
|
+
}
|
|
17435
|
+
function scheduleSessionArchive(state) {
|
|
17436
|
+
const config = state.sessionArchiveConfig;
|
|
17437
|
+
if (!config.enabled) return;
|
|
17438
|
+
state.sessionArchiveWatermarkPath = sessionArchiveWatermarkPath(homedir8(), process.env);
|
|
17439
|
+
state.sessionArchiveWatermarks = readWatermarks(
|
|
17440
|
+
state.sessionArchiveWatermarkPath,
|
|
17441
|
+
(message) => logSessionArchiveActivity(state, "startup", {
|
|
17442
|
+
type: "info",
|
|
17443
|
+
level: "warn",
|
|
17444
|
+
message: redactDiagnosticText(message),
|
|
17445
|
+
metadata: { outcome: "watermark-read-failed" }
|
|
17446
|
+
})
|
|
17447
|
+
);
|
|
17448
|
+
logSessionArchiveActivity(state, "startup", {
|
|
17449
|
+
type: "info",
|
|
17450
|
+
message: `Session archive enabled (interval=${config.intervalMs}ms)`,
|
|
17451
|
+
metadata: { outcome: "armed", interval_ms: config.intervalMs }
|
|
17452
|
+
});
|
|
17453
|
+
const cycleBudgetMs = Math.min(
|
|
17454
|
+
config.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
|
|
17455
|
+
MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
|
|
17456
|
+
);
|
|
17457
|
+
const run2 = (phase = "scheduled") => {
|
|
17458
|
+
void runSessionArchiveTask(
|
|
17459
|
+
state,
|
|
17460
|
+
phase,
|
|
17461
|
+
cycleBudgetMs,
|
|
17462
|
+
(signal) => runSessionArchiveFleet(state, phase, cycleBudgetMs, signal)
|
|
17463
|
+
);
|
|
17464
|
+
};
|
|
17465
|
+
const interval = setInterval(run2, config.intervalMs);
|
|
17466
|
+
const firstRun = setTimeout(() => run2(), SESSION_ARCHIVE_FIRST_RUN_MS);
|
|
17467
|
+
state.sessionArchiveTimers.push(interval, firstRun);
|
|
17468
|
+
}
|
|
15550
17469
|
function scheduleUsageReporting(state, params) {
|
|
15551
17470
|
const { mode, warnings } = params.resolved;
|
|
15552
17471
|
for (const warning2 of warnings) {
|
|
@@ -15821,6 +17740,12 @@ async function cleanup(state, opts = {}) {
|
|
|
15821
17740
|
clearTimeout(timer);
|
|
15822
17741
|
}
|
|
15823
17742
|
state.sessionCleanupTimers = [];
|
|
17743
|
+
for (const timer of state.sessionArchiveTimers) {
|
|
17744
|
+
clearInterval(timer);
|
|
17745
|
+
clearTimeout(timer);
|
|
17746
|
+
}
|
|
17747
|
+
state.sessionArchiveTimers = [];
|
|
17748
|
+
state.sessionArchiveAbortController?.abort(new Error("runner cleanup started"));
|
|
15824
17749
|
state.stopOpenCodeLogTail?.();
|
|
15825
17750
|
state.stopOpenCodeLogTail = null;
|
|
15826
17751
|
if (state.claudeUsageTimer) {
|
|
@@ -15888,6 +17813,15 @@ async function cleanup(state, opts = {}) {
|
|
|
15888
17813
|
await timeShutdownPhase(state, durations, "tunnel_close", () => connection.close());
|
|
15889
17814
|
state.connection = null;
|
|
15890
17815
|
}
|
|
17816
|
+
if (opts.graceful && state.sessionArchiveConfig.enabled) {
|
|
17817
|
+
const budgetMs = state.sessionArchiveConfig.flushBudgetMs;
|
|
17818
|
+
await timeShutdownPhase(
|
|
17819
|
+
state,
|
|
17820
|
+
durations,
|
|
17821
|
+
"session_archive_flush",
|
|
17822
|
+
() => runSessionArchiveShutdownFlush(state, budgetMs)
|
|
17823
|
+
);
|
|
17824
|
+
}
|
|
15891
17825
|
if (state.opencodeProcess) {
|
|
15892
17826
|
const opencodeProcess = state.opencodeProcess;
|
|
15893
17827
|
const result = await timeShutdownPhase(
|
|
@@ -15959,9 +17893,11 @@ async function run(options) {
|
|
|
15959
17893
|
const interactive = isInteractive(options.json);
|
|
15960
17894
|
let logLevel;
|
|
15961
17895
|
let fileSyncDirectories;
|
|
17896
|
+
let fileSyncExactFiles;
|
|
15962
17897
|
try {
|
|
15963
17898
|
logLevel = resolveLogLevel(options);
|
|
15964
17899
|
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir8());
|
|
17900
|
+
fileSyncExactFiles = resolveFileSyncExactFiles(options.enableFileSyncToExactFile, homedir8());
|
|
15965
17901
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
15966
17902
|
throw new Error(
|
|
15967
17903
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -16009,6 +17945,18 @@ async function run(options) {
|
|
|
16009
17945
|
messageCount: 0,
|
|
16010
17946
|
lastProxiedActivityAt: null,
|
|
16011
17947
|
sessionCleanupTimers: [],
|
|
17948
|
+
sessionArchiveConfig: resolveSessionArchiveConfig(
|
|
17949
|
+
{
|
|
17950
|
+
enabled: options.sessionArchive,
|
|
17951
|
+
interval: options.sessionArchiveInterval
|
|
17952
|
+
},
|
|
17953
|
+
process.env
|
|
17954
|
+
),
|
|
17955
|
+
sessionArchiveTimers: [],
|
|
17956
|
+
sessionArchiveWatermarks: /* @__PURE__ */ new Map(),
|
|
17957
|
+
sessionArchiveWatermarkPath: null,
|
|
17958
|
+
sessionArchiveInFlight: null,
|
|
17959
|
+
sessionArchiveAbortController: null,
|
|
16012
17960
|
sessionDeleteFailures: /* @__PURE__ */ new Map(),
|
|
16013
17961
|
claudeUsageTimer: null,
|
|
16014
17962
|
claudeUsageRearm: null,
|
|
@@ -16022,6 +17970,14 @@ async function run(options) {
|
|
|
16022
17970
|
};
|
|
16023
17971
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
16024
17972
|
setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
|
|
17973
|
+
for (const warning2 of state.sessionArchiveConfig.warnings) {
|
|
17974
|
+
logSessionArchiveActivity(state, "startup", {
|
|
17975
|
+
type: "info",
|
|
17976
|
+
level: "warn",
|
|
17977
|
+
message: `Session archive: ${warning2}`,
|
|
17978
|
+
metadata: { outcome: "configuration-warning" }
|
|
17979
|
+
});
|
|
17980
|
+
}
|
|
16025
17981
|
if (options.credentialSyncMarker) {
|
|
16026
17982
|
state.credentialSync = createCredentialSync({
|
|
16027
17983
|
markerPath: options.credentialSyncMarker,
|
|
@@ -16037,7 +17993,11 @@ async function run(options) {
|
|
|
16037
17993
|
}
|
|
16038
17994
|
if (fileSyncDirectories.length > 0) {
|
|
16039
17995
|
log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
|
|
16040
|
-
}
|
|
17996
|
+
}
|
|
17997
|
+
if (fileSyncExactFiles.length > 0) {
|
|
17998
|
+
log2(state, `File sync exact files enabled for: ${fileSyncExactFiles.join(", ")}`);
|
|
17999
|
+
}
|
|
18000
|
+
if (fileSyncDirectories.length === 0 && fileSyncExactFiles.length === 0) {
|
|
16041
18001
|
log2(state, "File sync is disabled (no --enable-file-sync-to given)", "debug");
|
|
16042
18002
|
}
|
|
16043
18003
|
if (!options.runner && options.agent) {
|
|
@@ -16319,6 +18279,7 @@ async function run(options) {
|
|
|
16319
18279
|
}
|
|
16320
18280
|
}
|
|
16321
18281
|
reportSessionDbRecovery(state);
|
|
18282
|
+
await drainSessionDeletionReports(state);
|
|
16322
18283
|
if (sessionDbVerifyFatal) {
|
|
16323
18284
|
throw new Error(
|
|
16324
18285
|
sessionDbVerifyFatalReason === void 0 ? "SESSION-DB-INTEGRITY-EXHAUSTED: the corrupt session DB could not be proven separated from the active backup prefix or removed from disk" : `SESSION-DB-VERIFY-TIMEOUT: session DB verification timed out (${sessionDbVerifyFatalReason}); refusing to start`
|
|
@@ -16338,13 +18299,75 @@ async function run(options) {
|
|
|
16338
18299
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
16339
18300
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
16340
18301
|
}
|
|
16341
|
-
const { version:
|
|
16342
|
-
options,
|
|
16343
|
-
process.env
|
|
16344
|
-
);
|
|
18302
|
+
const { version: opencodeVersionSelection, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(options, process.env);
|
|
16345
18303
|
for (const warning2 of opencodeVersionWarnings) {
|
|
16346
18304
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
16347
18305
|
}
|
|
18306
|
+
let opencodeMajor;
|
|
18307
|
+
let opencodeV2Command;
|
|
18308
|
+
let opencodeDetectionSource;
|
|
18309
|
+
let opencodeDetectionLevel = "info";
|
|
18310
|
+
let opencodeDetectionMessage;
|
|
18311
|
+
if (opencodeVersionSelection === "v1") {
|
|
18312
|
+
opencodeMajor = "v1";
|
|
18313
|
+
opencodeDetectionSource = "explicit";
|
|
18314
|
+
opencodeDetectionMessage = "OpenCode V1 was selected explicitly.";
|
|
18315
|
+
} else if (opencodeVersionSelection === "v2") {
|
|
18316
|
+
const installed = resolveInstalledOpenCode();
|
|
18317
|
+
opencodeMajor = "v2";
|
|
18318
|
+
opencodeDetectionSource = "explicit";
|
|
18319
|
+
if (installed.kind === "installed" && installed.major === "v2") {
|
|
18320
|
+
opencodeV2Command = installed.command;
|
|
18321
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; using ${installed.command}${installed.version ? ` (${installed.version})` : ""}.`;
|
|
18322
|
+
} else if (installed.kind === "unknown") {
|
|
18323
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported ${JSON.stringify(installed.rawVersion)}, but no V2 command was resolved.`;
|
|
18324
|
+
} else if (installed.kind === "installed") {
|
|
18325
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported V1 ${installed.version ?? "with an unknown version"}, so no V2 command was resolved.`;
|
|
18326
|
+
} else {
|
|
18327
|
+
opencodeDetectionMessage = "OpenCode V2 was selected explicitly, but no V2 command was resolved.";
|
|
18328
|
+
}
|
|
18329
|
+
} else {
|
|
18330
|
+
const running = await classifyRunningOpenCode(state.port);
|
|
18331
|
+
if (running.kind === "v1") {
|
|
18332
|
+
opencodeMajor = "v1";
|
|
18333
|
+
opencodeDetectionSource = "running-server";
|
|
18334
|
+
opencodeDetectionMessage = `OpenCode detection: found V1 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
|
|
18335
|
+
} else if (running.kind === "v2") {
|
|
18336
|
+
opencodeMajor = "v2";
|
|
18337
|
+
opencodeDetectionSource = "running-server";
|
|
18338
|
+
opencodeDetectionMessage = `OpenCode detection: found V2 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
|
|
18339
|
+
} else if (running.kind === "authenticated-unknown") {
|
|
18340
|
+
const message = `A server on port ${state.port} requires credentials this runner does not have. This can be a password-protected V1 or a V2 server, so the protocol cannot be established. Select it explicitly with --opencode-version v1 or --opencode-version v2.`;
|
|
18341
|
+
logActivity(state, {
|
|
18342
|
+
type: "info",
|
|
18343
|
+
level: "warn",
|
|
18344
|
+
message: `OpenCode detection: authenticated-unknown on port ${state.port}; ${running.detail}. ${message}`
|
|
18345
|
+
});
|
|
18346
|
+
throw new Error(message);
|
|
18347
|
+
} else {
|
|
18348
|
+
const installed = resolveInstalledOpenCode();
|
|
18349
|
+
opencodeDetectionLevel = "warn";
|
|
18350
|
+
if (installed.kind === "installed") {
|
|
18351
|
+
opencodeMajor = installed.major;
|
|
18352
|
+
opencodeDetectionSource = "installed-binary";
|
|
18353
|
+
if (installed.major === "v2") opencodeV2Command = installed.command;
|
|
18354
|
+
opencodeDetectionMessage = `OpenCode detection: no server identified on port ${state.port} (${running.detail}); using installed ${installed.major} command ${installed.command}${installed.version ? ` (${installed.version})` : " with an unknown version"}.`;
|
|
18355
|
+
} else if (installed.kind === "unknown") {
|
|
18356
|
+
opencodeMajor = "v1";
|
|
18357
|
+
opencodeDetectionSource = "fallback-v1";
|
|
18358
|
+
opencodeDetectionMessage = `OpenCode detection: no server identified on port ${state.port} (${running.detail}); ${installed.command} reported ${JSON.stringify(installed.rawVersion)}, so assuming V1. Use --opencode-version v2 to select V2 explicitly.`;
|
|
18359
|
+
} else {
|
|
18360
|
+
opencodeMajor = "v1";
|
|
18361
|
+
opencodeDetectionSource = "fallback-v1";
|
|
18362
|
+
opencodeDetectionMessage = `OpenCode detection: no server identified on port ${state.port} (${running.detail}); no recognizable OpenCode binary is installed, so continuing into the V1 install flow.`;
|
|
18363
|
+
}
|
|
18364
|
+
}
|
|
18365
|
+
}
|
|
18366
|
+
logActivity(state, {
|
|
18367
|
+
type: "info",
|
|
18368
|
+
level: opencodeDetectionLevel,
|
|
18369
|
+
message: opencodeDetectionMessage
|
|
18370
|
+
});
|
|
16348
18371
|
const managedSetupOptions = {
|
|
16349
18372
|
agentId: state.agentId,
|
|
16350
18373
|
apiUrl: getApiUrlConfig(),
|
|
@@ -16356,13 +18379,16 @@ async function run(options) {
|
|
|
16356
18379
|
...level === "error" ? { error: message } : { message }
|
|
16357
18380
|
})
|
|
16358
18381
|
};
|
|
16359
|
-
|
|
18382
|
+
let managedDiscovery = await discoverManagedInferenceSetup(managedSetupOptions);
|
|
18383
|
+
if (managedDiscovery.kind === "none") {
|
|
18384
|
+
managedDiscovery = await enrollManagedInferenceSetup(managedSetupOptions);
|
|
18385
|
+
}
|
|
16360
18386
|
let managedPreparation = { kind: "none" };
|
|
16361
18387
|
if (managedDiscovery.kind === "failed") {
|
|
16362
18388
|
managedPreparation = managedDiscovery;
|
|
16363
18389
|
managedSetupOptions.log(`MANAGED-INFERENCE-NOT-READY: ${managedDiscovery.reason}`, "warn");
|
|
16364
18390
|
} else if (managedDiscovery.kind === "setup") {
|
|
16365
|
-
const managedHealth =
|
|
18391
|
+
const managedHealth = opencodeMajor === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
|
|
16366
18392
|
managedPreparation = await prepareManagedInferenceSetup(
|
|
16367
18393
|
managedSetupOptions,
|
|
16368
18394
|
managedDiscovery,
|
|
@@ -16387,13 +18413,14 @@ async function run(options) {
|
|
|
16387
18413
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
16388
18414
|
const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
|
|
16389
18415
|
try {
|
|
16390
|
-
const oc =
|
|
18416
|
+
const oc = opencodeMajor === "v2" ? await ensureOpenCode2Running({
|
|
16391
18417
|
port: state.port,
|
|
16392
18418
|
interactive: state.interactive,
|
|
16393
18419
|
agentId: state.agentId,
|
|
16394
18420
|
log: (message) => log2(state, message),
|
|
16395
18421
|
startTimeoutMs: opencodeStartTimeoutMs,
|
|
16396
|
-
inheritStdio: Boolean(options.opencodePidFile)
|
|
18422
|
+
inheritStdio: Boolean(options.opencodePidFile),
|
|
18423
|
+
command: opencodeV2Command
|
|
16397
18424
|
}) : await ensureOpenCodeRunning({
|
|
16398
18425
|
port: state.port,
|
|
16399
18426
|
interactive: state.interactive,
|
|
@@ -16405,7 +18432,7 @@ async function run(options) {
|
|
|
16405
18432
|
state.port = oc.port;
|
|
16406
18433
|
state.opencodeProcess = options.opencodePidFile ? null : oc.process;
|
|
16407
18434
|
state.opencodeVersion = oc.version;
|
|
16408
|
-
state.opencodeApiVersion =
|
|
18435
|
+
state.opencodeApiVersion = opencodeMajor;
|
|
16409
18436
|
let configuredPlugins = [];
|
|
16410
18437
|
let resolvedPlugins = [];
|
|
16411
18438
|
try {
|
|
@@ -16427,13 +18454,14 @@ async function run(options) {
|
|
|
16427
18454
|
metadata: {
|
|
16428
18455
|
opencode_version: state.opencodeVersion,
|
|
16429
18456
|
opencode_api_version: state.opencodeApiVersion,
|
|
18457
|
+
opencode_detection_source: opencodeDetectionSource,
|
|
16430
18458
|
configured_plugins: JSON.stringify(configuredPlugins),
|
|
16431
18459
|
resolved_plugins: JSON.stringify(resolvedPlugins)
|
|
16432
18460
|
},
|
|
16433
18461
|
source: "cli.runtime-provenance"
|
|
16434
18462
|
});
|
|
16435
18463
|
let opencodePassword = null;
|
|
16436
|
-
if (
|
|
18464
|
+
if (opencodeMajor === "v2" && "password" in oc) {
|
|
16437
18465
|
const value = oc.password;
|
|
16438
18466
|
if (typeof value === "string" || value === null) opencodePassword = value;
|
|
16439
18467
|
}
|
|
@@ -16446,7 +18474,7 @@ async function run(options) {
|
|
|
16446
18474
|
state.opencodeClient = openCodeClient;
|
|
16447
18475
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
16448
18476
|
try {
|
|
16449
|
-
|
|
18477
|
+
writeFileSync9(options.opencodePidFile, `${oc.process.pid}
|
|
16450
18478
|
`, { mode: 384 });
|
|
16451
18479
|
chmodSync3(options.opencodePidFile, 384);
|
|
16452
18480
|
} catch (error2) {
|
|
@@ -16556,7 +18584,7 @@ async function run(options) {
|
|
|
16556
18584
|
let existingPid;
|
|
16557
18585
|
if (existsSync3(options.litestreamPidFile)) {
|
|
16558
18586
|
try {
|
|
16559
|
-
const rawPid =
|
|
18587
|
+
const rawPid = readFileSync12(options.litestreamPidFile, "utf8").trim();
|
|
16560
18588
|
const parsedPid = Number(rawPid);
|
|
16561
18589
|
if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
|
|
16562
18590
|
existingPid = parsedPid;
|
|
@@ -16593,7 +18621,7 @@ async function run(options) {
|
|
|
16593
18621
|
});
|
|
16594
18622
|
try {
|
|
16595
18623
|
if (litestreamProcess.pid !== void 0) {
|
|
16596
|
-
|
|
18624
|
+
writeFileSync9(options.litestreamPidFile, `${litestreamProcess.pid}
|
|
16597
18625
|
`, {
|
|
16598
18626
|
mode: 384
|
|
16599
18627
|
});
|
|
@@ -16643,6 +18671,15 @@ async function run(options) {
|
|
|
16643
18671
|
log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
|
|
16644
18672
|
}
|
|
16645
18673
|
const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
|
|
18674
|
+
const recordBinding = state.sessionArchiveConfig.enabled ? (conversationId, sessionId) => appendSessionBinding(
|
|
18675
|
+
sessionBindingLedgerPath(homedir8(), process.env),
|
|
18676
|
+
{
|
|
18677
|
+
conversation_id: conversationId,
|
|
18678
|
+
opencode_session_id: sessionId,
|
|
18679
|
+
observed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
18680
|
+
},
|
|
18681
|
+
(message) => logActivity(state, { type: "info", level: "warn", message })
|
|
18682
|
+
) : void 0;
|
|
16646
18683
|
const channelDriver = new ChannelDriver({
|
|
16647
18684
|
agentId: state.agentId,
|
|
16648
18685
|
port: state.port,
|
|
@@ -16652,9 +18689,11 @@ async function run(options) {
|
|
|
16652
18689
|
conversationFilter: state.conversationFilter,
|
|
16653
18690
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
16654
18691
|
sessionStoreHealth: state.sessionStoreHealth,
|
|
16655
|
-
|
|
16656
|
-
//
|
|
18692
|
+
recordBinding,
|
|
18693
|
+
// #559: the file-sync allow-lists. Empty ⇒ pending files are REJECTED with
|
|
18694
|
+
// `file_sync_disabled` on the ack, not silently ignored.
|
|
16657
18695
|
fileSyncDirectories,
|
|
18696
|
+
fileSyncExactFiles,
|
|
16658
18697
|
homeDir: homedir8(),
|
|
16659
18698
|
maxActiveSessions,
|
|
16660
18699
|
log: (entry) => (
|
|
@@ -16830,6 +18869,7 @@ async function run(options) {
|
|
|
16830
18869
|
}
|
|
16831
18870
|
if (!state.running) return;
|
|
16832
18871
|
scheduleSessionCleanup(state, channelDriver, options);
|
|
18872
|
+
scheduleSessionArchive(state);
|
|
16833
18873
|
state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
|
|
16834
18874
|
state.openaiUsageRearm = scheduleUsageReporting(state, {
|
|
16835
18875
|
label: "OpenAI",
|
|
@@ -16925,8 +18965,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
16925
18965
|
"--opencode-start-timeout <seconds>",
|
|
16926
18966
|
"Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
|
|
16927
18967
|
).option(
|
|
16928
|
-
"--opencode-version <v1|v2>",
|
|
16929
|
-
"
|
|
18968
|
+
"--opencode-version <v1|v2|auto>",
|
|
18969
|
+
"OpenCode major version: auto (default) detects the running server, then installed binary; v1 and v2 skip server-protocol detection. Env: EVIDENT_OPENCODE_VERSION"
|
|
16930
18970
|
).option("--json", "Output in JSON format").option(
|
|
16931
18971
|
"--session-cleanup-max-age <duration>",
|
|
16932
18972
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
@@ -16939,6 +18979,12 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
16939
18979
|
).option(
|
|
16940
18980
|
"--session-cleanup-interval <duration>",
|
|
16941
18981
|
"How often the cleanup sweep runs (default: 1h). Env: EVIDENT_SESSION_CLEANUP_INTERVAL"
|
|
18982
|
+
).option(
|
|
18983
|
+
"--session-archive",
|
|
18984
|
+
"Upload full OpenCode transcripts to the runner archive (off by default). Env: EVIDENT_SESSION_ARCHIVE"
|
|
18985
|
+
).option(
|
|
18986
|
+
"--session-archive-interval <duration>",
|
|
18987
|
+
"How often session archives are uploaded (default: 1h). Env: EVIDENT_SESSION_ARCHIVE_INTERVAL"
|
|
16942
18988
|
).option(
|
|
16943
18989
|
"--claude-usage-reporting <mode>",
|
|
16944
18990
|
"Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
@@ -16953,6 +18999,11 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
16953
18999
|
"Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.",
|
|
16954
19000
|
(value, previous) => previous.concat([value]),
|
|
16955
19001
|
[]
|
|
19002
|
+
).option(
|
|
19003
|
+
"--enable-file-sync-to-exact-file <path>",
|
|
19004
|
+
"Allow Evident to write files to exactly this path (repeatable).",
|
|
19005
|
+
(value, previous) => previous.concat([value]),
|
|
19006
|
+
[]
|
|
16956
19007
|
).option(
|
|
16957
19008
|
"--tunnel-ready-file <path>",
|
|
16958
19009
|
"Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
|
|
@@ -17005,6 +19056,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17005
19056
|
sessionCleanupMaxCount: options.sessionCleanupMaxCount,
|
|
17006
19057
|
maxActiveSessions: options.maxActiveSessions,
|
|
17007
19058
|
sessionCleanupInterval: options.sessionCleanupInterval,
|
|
19059
|
+
sessionArchive: options.sessionArchive,
|
|
19060
|
+
sessionArchiveInterval: options.sessionArchiveInterval,
|
|
17008
19061
|
// Raw string — the resolver in run.ts single-sources parsing
|
|
17009
19062
|
// (resolveClaudeUsageReportingMode).
|
|
17010
19063
|
claudeUsageReporting: options.claudeUsageReporting,
|
|
@@ -17015,6 +19068,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17015
19068
|
// Raw values — expansion/validation is single-sourced in run.ts's
|
|
17016
19069
|
// resolveFileSyncDirectories.
|
|
17017
19070
|
enableFileSyncTo: options.enableFileSyncTo,
|
|
19071
|
+
// Raw values — expansion/validation is single-sourced in run.ts's
|
|
19072
|
+
// resolveFileSyncExactFiles.
|
|
19073
|
+
enableFileSyncToExactFile: options.enableFileSyncToExactFile,
|
|
17018
19074
|
tunnelReadyFile: options.tunnelReadyFile,
|
|
17019
19075
|
litestreamConfig: options.litestreamConfig,
|
|
17020
19076
|
opencodePidFile: options.opencodePidFile,
|