@evident-ai/cli 3.6.1-dev.4db2683 → 3.6.1-dev.5d5fa2a
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 +2487 -612
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1179,9 +1179,9 @@ async function claudeUsage() {
|
|
|
1179
1179
|
}
|
|
1180
1180
|
|
|
1181
1181
|
// src/commands/run.ts
|
|
1182
|
-
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";
|
|
1183
1183
|
import { homedir as homedir8 } from "node:os";
|
|
1184
|
-
import { isAbsolute as isAbsolute4, join as
|
|
1184
|
+
import { isAbsolute as isAbsolute4, join as join16, parse, resolve as resolvePath2 } from "node:path";
|
|
1185
1185
|
import chalk7 from "chalk";
|
|
1186
1186
|
|
|
1187
1187
|
// ../../packages/types/src/agents/index.ts
|
|
@@ -1256,6 +1256,36 @@ function effectiveCorrelatedRun(messages, userMessageId) {
|
|
|
1256
1256
|
function extractReplyTextForMessage(messages, opencodeUserMessageId) {
|
|
1257
1257
|
return lastText(effectiveCorrelatedRun(messages, opencodeUserMessageId));
|
|
1258
1258
|
}
|
|
1259
|
+
function isRecord(value) {
|
|
1260
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1261
|
+
}
|
|
1262
|
+
function projectV2ToolContent(content) {
|
|
1263
|
+
if (!isRecord(content) || content.type !== "tool" || typeof content.name !== "string" || !isRecord(content.state)) {
|
|
1264
|
+
return null;
|
|
1265
|
+
}
|
|
1266
|
+
const { state } = content;
|
|
1267
|
+
const status2 = state.status;
|
|
1268
|
+
if (status2 !== "streaming" && status2 !== "running" && status2 !== "completed" && status2 !== "error") {
|
|
1269
|
+
return null;
|
|
1270
|
+
}
|
|
1271
|
+
const projectedState = {
|
|
1272
|
+
status: status2 === "streaming" ? "pending" : status2
|
|
1273
|
+
};
|
|
1274
|
+
if (status2 !== "streaming" && isRecord(state.input)) projectedState.input = state.input;
|
|
1275
|
+
const output = Array.isArray(state.content) ? state.content.filter(
|
|
1276
|
+
(entry) => isRecord(entry) && entry.type === "text" && typeof entry.text === "string"
|
|
1277
|
+
).map((entry) => entry.text) : [];
|
|
1278
|
+
if (output.length > 0) projectedState.output = output.join("\n");
|
|
1279
|
+
if (status2 === "error" && isRecord(state.error) && typeof state.error.message === "string") {
|
|
1280
|
+
projectedState.error = state.error.message;
|
|
1281
|
+
}
|
|
1282
|
+
return {
|
|
1283
|
+
type: "tool",
|
|
1284
|
+
tool: content.name,
|
|
1285
|
+
...typeof content.id === "string" ? { callID: content.id } : {},
|
|
1286
|
+
state: projectedState
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1259
1289
|
|
|
1260
1290
|
// ../../packages/types/src/telemetry/index.ts
|
|
1261
1291
|
var TelemetryEventTypes = {
|
|
@@ -1445,7 +1475,7 @@ import ora4 from "ora";
|
|
|
1445
1475
|
import { select as select4 } from "@inquirer/prompts";
|
|
1446
1476
|
|
|
1447
1477
|
// src/lib/telemetry.ts
|
|
1448
|
-
var CLI_VERSION = (true ? "3.6.1-dev.
|
|
1478
|
+
var CLI_VERSION = (true ? "3.6.1-dev.5d5fa2a" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1449
1479
|
function getCliVersion() {
|
|
1450
1480
|
return CLI_VERSION;
|
|
1451
1481
|
}
|
|
@@ -2195,6 +2225,7 @@ var FORWARDED_LEVELS_BY_SOURCE = {
|
|
|
2195
2225
|
"cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
|
|
2196
2226
|
opencode: /* @__PURE__ */ new Set(["warn", "error"]),
|
|
2197
2227
|
"cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"]),
|
|
2228
|
+
"cli.session-archive": /* @__PURE__ */ new Set(["info", "warn", "error"]),
|
|
2198
2229
|
"cli.runtime-provenance": /* @__PURE__ */ new Set(["info", "warn", "error"])
|
|
2199
2230
|
};
|
|
2200
2231
|
var SEVERITY_BY_LEVEL = {
|
|
@@ -2757,153 +2788,1347 @@ function boundedError(error2) {
|
|
|
2757
2788
|
return message.slice(0, MAX_DIAGNOSTIC_LENGTH);
|
|
2758
2789
|
}
|
|
2759
2790
|
|
|
2760
|
-
// src/lib/opencode/
|
|
2761
|
-
|
|
2762
|
-
|
|
2791
|
+
// src/lib/opencode/session-binding-ledger.ts
|
|
2792
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2793
|
+
import { dirname as dirname5, join as join5 } from "node:path";
|
|
2794
|
+
var SESSION_BINDING_LEDGER_MAX_RECORDS = 1e3;
|
|
2795
|
+
var MAX_DIAGNOSTIC_LENGTH2 = 200;
|
|
2796
|
+
function sessionBindingLedgerPath(homeDir, env) {
|
|
2797
|
+
const override = env.EVIDENT_SESSION_BINDING_LEDGER?.trim();
|
|
2798
|
+
return override || join5(homeDir, ".local", "state", "evident", "session-bindings.jsonl");
|
|
2799
|
+
}
|
|
2800
|
+
function appendSessionBinding(path, record, log3) {
|
|
2801
|
+
mkdirSync2(dirname5(path), { recursive: true });
|
|
2802
|
+
const current = readSessionBindingRecords(path, log3);
|
|
2803
|
+
if (current.readError !== null) {
|
|
2804
|
+
throw new Error(`cannot enforce the ledger limit after reading ${path}: ${current.readError}`);
|
|
2805
|
+
}
|
|
2806
|
+
if (current.records.some(
|
|
2807
|
+
(existing) => existing.conversation_id === record.conversation_id && existing.opencode_session_id === record.opencode_session_id
|
|
2808
|
+
)) {
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
const kept = [...current.records, record].slice(-SESSION_BINDING_LEDGER_MAX_RECORDS);
|
|
2812
|
+
if (kept.length > current.records.length) {
|
|
2813
|
+
appendFileSync2(path, `${JSON.stringify(record)}
|
|
2814
|
+
`, "utf8");
|
|
2815
|
+
} else {
|
|
2816
|
+
writeFileSync2(path, `${kept.map((entry) => JSON.stringify(entry)).join("\n")}
|
|
2817
|
+
`, "utf8");
|
|
2818
|
+
}
|
|
2763
2819
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2820
|
+
function readSessionBindings(path, log3) {
|
|
2821
|
+
const result = readSessionBindingRecords(path, log3);
|
|
2822
|
+
if (result.readError !== null) {
|
|
2823
|
+
log3(`[session-binding-ledger] could not read ${path}: ${result.readError}`);
|
|
2824
|
+
}
|
|
2825
|
+
return new Map(
|
|
2826
|
+
result.records.map((record) => [record.opencode_session_id, record.conversation_id])
|
|
2827
|
+
);
|
|
2828
|
+
}
|
|
2829
|
+
function readSessionBindingRecords(path, log3) {
|
|
2830
|
+
let content;
|
|
2767
2831
|
try {
|
|
2768
|
-
|
|
2769
|
-
signal: AbortSignal.timeout(2e3)
|
|
2770
|
-
// 2 second timeout
|
|
2771
|
-
});
|
|
2772
|
-
if (response.status === 401) {
|
|
2773
|
-
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
2774
|
-
}
|
|
2775
|
-
if (!response.ok) {
|
|
2776
|
-
return { healthy: false, error: `HTTP ${response.status}` };
|
|
2777
|
-
}
|
|
2778
|
-
const data = await response.json().catch(() => ({}));
|
|
2779
|
-
return { healthy: true, version: data.version };
|
|
2832
|
+
content = readFileSync4(path, "utf8");
|
|
2780
2833
|
} catch (error2) {
|
|
2781
|
-
|
|
2782
|
-
|
|
2834
|
+
if (isMissingFile2(error2)) return { records: [], readError: null };
|
|
2835
|
+
const readError = boundedError2(error2);
|
|
2836
|
+
return { records: [], readError };
|
|
2783
2837
|
}
|
|
2838
|
+
const records = content.split("\n").flatMap((line) => {
|
|
2839
|
+
if (!line.trim()) return [];
|
|
2840
|
+
const record = parseRecord2(line, path, log3);
|
|
2841
|
+
return record ? [record] : [];
|
|
2842
|
+
});
|
|
2843
|
+
return { records, readError: null };
|
|
2784
2844
|
}
|
|
2785
|
-
function
|
|
2786
|
-
const excerpt = body.replace(/\s+/g, " ").trim().slice(0, 160);
|
|
2787
|
-
return excerpt ? ` (body="${excerpt}")` : " (empty body)";
|
|
2788
|
-
}
|
|
2789
|
-
async function checkOpenCode2Health(port, password) {
|
|
2845
|
+
function parseRecord2(line, path, log3) {
|
|
2790
2846
|
try {
|
|
2791
|
-
const
|
|
2792
|
-
if (
|
|
2793
|
-
|
|
2794
|
-
}
|
|
2795
|
-
const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
|
|
2796
|
-
headers,
|
|
2797
|
-
signal: AbortSignal.timeout(2e3)
|
|
2798
|
-
});
|
|
2799
|
-
if (response.status === 401) {
|
|
2800
|
-
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
2801
|
-
}
|
|
2802
|
-
if (!response.ok) {
|
|
2803
|
-
return { healthy: false, error: `HTTP ${response.status}` };
|
|
2804
|
-
}
|
|
2805
|
-
const body = await response.text();
|
|
2806
|
-
const excerpt = bodyExcerpt(body);
|
|
2807
|
-
let data;
|
|
2808
|
-
try {
|
|
2809
|
-
data = JSON.parse(body);
|
|
2810
|
-
} catch (error2) {
|
|
2811
|
-
return {
|
|
2812
|
-
healthy: false,
|
|
2813
|
-
error: `Invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2814
|
-
};
|
|
2815
|
-
}
|
|
2816
|
-
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
2817
|
-
return {
|
|
2818
|
-
healthy: false,
|
|
2819
|
-
error: `Invalid health response${excerpt}: expected healthy=true`
|
|
2820
|
-
};
|
|
2821
|
-
}
|
|
2822
|
-
const health = data;
|
|
2823
|
-
if (health.healthy !== true) {
|
|
2824
|
-
return {
|
|
2825
|
-
healthy: false,
|
|
2826
|
-
error: `Invalid health response${excerpt}: expected healthy=true`
|
|
2827
|
-
};
|
|
2828
|
-
}
|
|
2829
|
-
return typeof health.version === "string" ? { healthy: true, version: health.version } : { healthy: true };
|
|
2847
|
+
const value = JSON.parse(line);
|
|
2848
|
+
if (isSessionBindingRecord(value)) return value;
|
|
2849
|
+
log3(`[session-binding-ledger] skipped malformed record in ${path}: invalid shape`);
|
|
2830
2850
|
} catch (error2) {
|
|
2831
|
-
|
|
2832
|
-
return { healthy: false, error: message };
|
|
2851
|
+
log3(`[session-binding-ledger] skipped malformed record in ${path}: ${boundedError2(error2)}`);
|
|
2833
2852
|
}
|
|
2853
|
+
return null;
|
|
2834
2854
|
}
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2855
|
+
function isSessionBindingRecord(value) {
|
|
2856
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2857
|
+
const record = value;
|
|
2858
|
+
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";
|
|
2859
|
+
}
|
|
2860
|
+
function isMissingFile2(error2) {
|
|
2861
|
+
return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
|
|
2862
|
+
}
|
|
2863
|
+
function boundedError2(error2) {
|
|
2864
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2865
|
+
return message.slice(0, MAX_DIAGNOSTIC_LENGTH2);
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
// src/lib/opencode/session-archive-config.ts
|
|
2869
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2870
|
+
|
|
2871
|
+
// src/lib/opencode/session-cleanup.ts
|
|
2872
|
+
var DURATION_UNIT_MS = {
|
|
2873
|
+
s: 1e3,
|
|
2874
|
+
m: 60 * 1e3,
|
|
2875
|
+
h: 60 * 60 * 1e3,
|
|
2876
|
+
d: 24 * 60 * 60 * 1e3
|
|
2877
|
+
};
|
|
2878
|
+
function parseDurationMs(input) {
|
|
2879
|
+
const trimmed = input.trim();
|
|
2880
|
+
const match = /^(\d+)([smhd])$/.exec(trimmed);
|
|
2881
|
+
if (!match) {
|
|
2882
|
+
throw new Error(
|
|
2883
|
+
`Invalid duration "${input}": expected <number><unit> where unit is one of s, m, h, d (e.g. "7d", "24h", "30m", "90s").`
|
|
2884
|
+
);
|
|
2843
2885
|
}
|
|
2844
|
-
|
|
2886
|
+
const value = Number(match[1]);
|
|
2887
|
+
if (value <= 0) {
|
|
2888
|
+
throw new Error(`Invalid duration "${input}": must be a positive value.`);
|
|
2889
|
+
}
|
|
2890
|
+
return value * DURATION_UNIT_MS[match[2]];
|
|
2845
2891
|
}
|
|
2846
|
-
|
|
2847
|
-
const
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
if (
|
|
2851
|
-
|
|
2892
|
+
function selectSessionsToDelete(sessions, opts) {
|
|
2893
|
+
const { maxAgeMs, maxCount, nowMs, protectedIds } = opts;
|
|
2894
|
+
if (maxAgeMs === void 0 && maxCount === void 0) return [];
|
|
2895
|
+
const ageEligible = (s) => {
|
|
2896
|
+
if (maxAgeMs === void 0) return false;
|
|
2897
|
+
if (s.lastActivityMs === null) return true;
|
|
2898
|
+
return nowMs - s.lastActivityMs > maxAgeMs;
|
|
2899
|
+
};
|
|
2900
|
+
const countEligibleIds = /* @__PURE__ */ new Set();
|
|
2901
|
+
if (maxCount !== void 0) {
|
|
2902
|
+
const byActivityDesc = [...sessions].sort(
|
|
2903
|
+
(a, b) => (b.lastActivityMs ?? -Infinity) - (a.lastActivityMs ?? -Infinity)
|
|
2904
|
+
);
|
|
2905
|
+
for (const s of byActivityDesc.slice(maxCount)) {
|
|
2906
|
+
countEligibleIds.add(s.id);
|
|
2852
2907
|
}
|
|
2853
|
-
await new Promise((resolve5) => setTimeout(resolve5, 1e3));
|
|
2854
2908
|
}
|
|
2855
|
-
|
|
2909
|
+
const toDelete = [];
|
|
2910
|
+
for (const s of sessions) {
|
|
2911
|
+
if (protectedIds.has(s.id)) continue;
|
|
2912
|
+
if (ageEligible(s) || countEligibleIds.has(s.id)) {
|
|
2913
|
+
toDelete.push(s.id);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
|
|
2917
|
+
return toDelete.sort(
|
|
2918
|
+
(a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
|
|
2919
|
+
);
|
|
2856
2920
|
}
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
this.elapsedMs = elapsedMs;
|
|
2866
|
-
this.name = "TimeoutError";
|
|
2921
|
+
var DEFAULT_INTERVAL = "1h";
|
|
2922
|
+
function resolve2(flag, envValue, fallback) {
|
|
2923
|
+
return flag ?? envValue ?? fallback;
|
|
2924
|
+
}
|
|
2925
|
+
function parseMaxCount(input) {
|
|
2926
|
+
const trimmed = input.trim();
|
|
2927
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
2928
|
+
throw new Error(`Invalid max-count "${input}": expected a positive integer.`);
|
|
2867
2929
|
}
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
2872
|
-
let path;
|
|
2873
|
-
try {
|
|
2874
|
-
path = new URL(rawUrl).pathname;
|
|
2875
|
-
} catch (error2) {
|
|
2876
|
-
if (!(error2 instanceof TypeError)) throw error2;
|
|
2877
|
-
path = rawUrl.split(/[?#]/, 1)[0];
|
|
2930
|
+
const value = Number(trimmed);
|
|
2931
|
+
if (value <= 0) {
|
|
2932
|
+
throw new Error(`Invalid max-count "${input}": must be greater than 0.`);
|
|
2878
2933
|
}
|
|
2879
|
-
|
|
2880
|
-
const prefixedIdentifier = /^[a-z]+_[A-Za-z0-9_-]{10,}$/;
|
|
2881
|
-
const maskedPath = path.split("/").map((segment) => uuid.test(segment) || prefixedIdentifier.test(segment) ? ":id" : segment).join("/");
|
|
2882
|
-
return `${method} ${maskedPath}`;
|
|
2934
|
+
return value;
|
|
2883
2935
|
}
|
|
2884
|
-
function
|
|
2885
|
-
|
|
2886
|
-
|
|
2936
|
+
function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
2937
|
+
const warnings = [];
|
|
2938
|
+
const maxAgeRaw = resolve2(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
|
|
2939
|
+
const maxCountRaw = resolve2(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
|
|
2940
|
+
const intervalRaw = resolve2(
|
|
2941
|
+
flags.interval,
|
|
2942
|
+
env.EVIDENT_SESSION_CLEANUP_INTERVAL,
|
|
2943
|
+
DEFAULT_INTERVAL
|
|
2944
|
+
);
|
|
2945
|
+
let maxAgeMs;
|
|
2946
|
+
if (maxAgeRaw !== void 0) {
|
|
2887
2947
|
try {
|
|
2888
|
-
|
|
2889
|
-
} catch (
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
timeoutMs,
|
|
2894
|
-
Date.now() - startedAt
|
|
2895
|
-
);
|
|
2896
|
-
}
|
|
2897
|
-
throw error2;
|
|
2948
|
+
maxAgeMs = parseDurationMs(maxAgeRaw);
|
|
2949
|
+
} catch (err) {
|
|
2950
|
+
warnings.push(
|
|
2951
|
+
`Ignoring invalid --session-cleanup-max-age: ${err instanceof Error ? err.message : String(err)}`
|
|
2952
|
+
);
|
|
2898
2953
|
}
|
|
2899
|
-
}
|
|
2954
|
+
}
|
|
2955
|
+
let maxCount;
|
|
2956
|
+
if (maxCountRaw !== void 0) {
|
|
2957
|
+
try {
|
|
2958
|
+
maxCount = parseMaxCount(maxCountRaw);
|
|
2959
|
+
} catch (err) {
|
|
2960
|
+
warnings.push(
|
|
2961
|
+
`Ignoring invalid --session-cleanup-max-count: ${err instanceof Error ? err.message : String(err)}`
|
|
2962
|
+
);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
let intervalMs;
|
|
2966
|
+
try {
|
|
2967
|
+
intervalMs = parseDurationMs(intervalRaw ?? DEFAULT_INTERVAL);
|
|
2968
|
+
} catch (err) {
|
|
2969
|
+
warnings.push(
|
|
2970
|
+
`Ignoring invalid --session-cleanup-interval, using default ${DEFAULT_INTERVAL}: ${err instanceof Error ? err.message : String(err)}`
|
|
2971
|
+
);
|
|
2972
|
+
intervalMs = parseDurationMs(DEFAULT_INTERVAL);
|
|
2973
|
+
}
|
|
2974
|
+
const enabled = maxAgeMs !== void 0 || maxCount !== void 0;
|
|
2975
|
+
return { enabled, maxAgeMs, maxCount, intervalMs, warnings };
|
|
2900
2976
|
}
|
|
2901
2977
|
|
|
2902
|
-
// src/lib/opencode/
|
|
2903
|
-
|
|
2904
|
-
|
|
2978
|
+
// src/lib/opencode/session-archive-config.ts
|
|
2979
|
+
var DEFAULT_INTERVAL2 = "1h";
|
|
2980
|
+
var DEFAULT_FLUSH_BUDGET_MS = 2e3;
|
|
2981
|
+
var MAX_SESSION_ARCHIVE_TIMER_DELAY_MS = 2147483647;
|
|
2982
|
+
var STATE_PREFIX_FILE = "/dev/shm/evident-state-prefix";
|
|
2983
|
+
var ENABLED_VALUES = /* @__PURE__ */ new Set(["on", "true", "1"]);
|
|
2984
|
+
var DISABLED_VALUES = /* @__PURE__ */ new Set(["off", "false", "0"]);
|
|
2985
|
+
function nonEmpty(value) {
|
|
2986
|
+
const trimmed = value?.trim();
|
|
2987
|
+
return trimmed ? trimmed : null;
|
|
2988
|
+
}
|
|
2989
|
+
function parseFlushBudgetMs(value) {
|
|
2990
|
+
const trimmed = value.trim();
|
|
2991
|
+
const budgetMs = Number(trimmed);
|
|
2992
|
+
if (!/^\d+$/.test(trimmed) || !Number.isSafeInteger(budgetMs) || budgetMs <= 0 || budgetMs > MAX_SESSION_ARCHIVE_TIMER_DELAY_MS) {
|
|
2993
|
+
throw new Error(
|
|
2994
|
+
`Invalid flush budget "${value}": expected a positive integer number of milliseconds no greater than ${MAX_SESSION_ARCHIVE_TIMER_DELAY_MS}.`
|
|
2995
|
+
);
|
|
2996
|
+
}
|
|
2997
|
+
return budgetMs;
|
|
2905
2998
|
}
|
|
2906
|
-
function
|
|
2999
|
+
function resolveSessionArchiveConfig(flags, env = process.env) {
|
|
3000
|
+
const warnings = [];
|
|
3001
|
+
const archiveRaw = env.EVIDENT_SESSION_ARCHIVE;
|
|
3002
|
+
let enabled = flags.enabled ?? false;
|
|
3003
|
+
if (flags.enabled === void 0 && archiveRaw !== void 0 && archiveRaw.trim() !== "") {
|
|
3004
|
+
const normalized = archiveRaw.trim().toLowerCase();
|
|
3005
|
+
if (ENABLED_VALUES.has(normalized)) {
|
|
3006
|
+
enabled = true;
|
|
3007
|
+
} else if (DISABLED_VALUES.has(normalized)) {
|
|
3008
|
+
enabled = false;
|
|
3009
|
+
} else {
|
|
3010
|
+
warnings.push(
|
|
3011
|
+
`Ignoring invalid EVIDENT_SESSION_ARCHIVE "${archiveRaw}": expected on or off; session archiving remains off.`
|
|
3012
|
+
);
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
let intervalMs;
|
|
3016
|
+
const intervalRaw = flags.interval ?? env.EVIDENT_SESSION_ARCHIVE_INTERVAL ?? DEFAULT_INTERVAL2;
|
|
3017
|
+
try {
|
|
3018
|
+
intervalMs = parseDurationMs(intervalRaw);
|
|
3019
|
+
if (intervalMs > MAX_SESSION_ARCHIVE_TIMER_DELAY_MS) {
|
|
3020
|
+
throw new Error(
|
|
3021
|
+
`Duration "${intervalRaw}" exceeds Node's maximum timer delay of ${MAX_SESSION_ARCHIVE_TIMER_DELAY_MS}ms.`
|
|
3022
|
+
);
|
|
3023
|
+
}
|
|
3024
|
+
} catch (err) {
|
|
3025
|
+
warnings.push(
|
|
3026
|
+
`Ignoring invalid session archive interval, using default ${DEFAULT_INTERVAL2}: ${err instanceof Error ? err.message : String(err)}`
|
|
3027
|
+
);
|
|
3028
|
+
intervalMs = parseDurationMs(DEFAULT_INTERVAL2);
|
|
3029
|
+
}
|
|
3030
|
+
let flushBudgetMs;
|
|
3031
|
+
const flushBudgetRaw = env.EVIDENT_SESSION_ARCHIVE_FLUSH_MS ?? String(DEFAULT_FLUSH_BUDGET_MS);
|
|
3032
|
+
try {
|
|
3033
|
+
flushBudgetMs = parseFlushBudgetMs(flushBudgetRaw);
|
|
3034
|
+
} catch (err) {
|
|
3035
|
+
warnings.push(
|
|
3036
|
+
`Ignoring invalid EVIDENT_SESSION_ARCHIVE_FLUSH_MS, using default ${DEFAULT_FLUSH_BUDGET_MS}: ${err instanceof Error ? err.message : String(err)}`
|
|
3037
|
+
);
|
|
3038
|
+
flushBudgetMs = DEFAULT_FLUSH_BUDGET_MS;
|
|
3039
|
+
}
|
|
3040
|
+
const bucket = nonEmpty(env.PERSISTENCE_BUCKET);
|
|
3041
|
+
let keyPrefix = nonEmpty(env.LITESTREAM_PREFIX);
|
|
3042
|
+
if (enabled && bucket === null) {
|
|
3043
|
+
warnings.push("Session archiving is off because PERSISTENCE_BUCKET is unset or empty.");
|
|
3044
|
+
enabled = false;
|
|
3045
|
+
}
|
|
3046
|
+
if (enabled && keyPrefix === null) {
|
|
3047
|
+
let fallbackReadError = null;
|
|
3048
|
+
try {
|
|
3049
|
+
keyPrefix = nonEmpty(readFileSync5(STATE_PREFIX_FILE, "utf8"));
|
|
3050
|
+
} catch (err) {
|
|
3051
|
+
fallbackReadError = err instanceof Error ? err.message : String(err);
|
|
3052
|
+
}
|
|
3053
|
+
if (keyPrefix === null) {
|
|
3054
|
+
const readFailure = fallbackReadError ? ` Could not read ${STATE_PREFIX_FILE}: ${fallbackReadError}.` : "";
|
|
3055
|
+
warnings.push(
|
|
3056
|
+
`Session archiving is off because neither LITESTREAM_PREFIX nor ${STATE_PREFIX_FILE} contains a prefix.${readFailure}`
|
|
3057
|
+
);
|
|
3058
|
+
enabled = false;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
return { enabled, bucket, keyPrefix, intervalMs, flushBudgetMs, warnings };
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
// src/lib/opencode/session-archive-snapshot.ts
|
|
3065
|
+
import { createHash } from "node:crypto";
|
|
3066
|
+
var SessionArchiveExportError = class extends Error {
|
|
3067
|
+
constructor(code, message) {
|
|
3068
|
+
super(message);
|
|
3069
|
+
this.code = code;
|
|
3070
|
+
this.name = "SessionArchiveExportError";
|
|
3071
|
+
}
|
|
3072
|
+
};
|
|
3073
|
+
function canonicalize(value) {
|
|
3074
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
3075
|
+
if (typeof value !== "object" || value === null) return value;
|
|
3076
|
+
return Object.fromEntries(
|
|
3077
|
+
Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, nested]) => [key, canonicalize(nested)])
|
|
3078
|
+
);
|
|
3079
|
+
}
|
|
3080
|
+
function contentForFingerprint(archive) {
|
|
3081
|
+
return JSON.stringify(
|
|
3082
|
+
canonicalize({
|
|
3083
|
+
opencode_api_version: archive.opencode_api_version,
|
|
3084
|
+
opencode_session_id: archive.opencode_session_id,
|
|
3085
|
+
evident_conversation_id: archive.evident_conversation_id,
|
|
3086
|
+
messages: archive.messages
|
|
3087
|
+
})
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
function fingerprintArchive(archive) {
|
|
3091
|
+
return createHash("sha256").update(contentForFingerprint(archive)).digest("hex");
|
|
3092
|
+
}
|
|
3093
|
+
function buildSessionArchive(input) {
|
|
3094
|
+
const archive = {
|
|
3095
|
+
schema_version: 1,
|
|
3096
|
+
opencode_api_version: input.apiVersion,
|
|
3097
|
+
opencode_session_id: input.session.id,
|
|
3098
|
+
evident_conversation_id: input.conversationId,
|
|
3099
|
+
evident_runner_id: input.runnerId,
|
|
3100
|
+
binding_source: input.bindingSource,
|
|
3101
|
+
session: input.session,
|
|
3102
|
+
messages: input.messages,
|
|
3103
|
+
message_count: input.messages.length,
|
|
3104
|
+
archived_at: input.archivedAt
|
|
3105
|
+
};
|
|
3106
|
+
return { ...archive, content_fingerprint: fingerprintArchive(archive) };
|
|
3107
|
+
}
|
|
3108
|
+
function isRecord2(value) {
|
|
3109
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3110
|
+
}
|
|
3111
|
+
function errorMessage2(error2) {
|
|
3112
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
3113
|
+
}
|
|
3114
|
+
function invalidResponse(version2, sessionId, path, reason) {
|
|
3115
|
+
return new SessionArchiveExportError(
|
|
3116
|
+
"invalid-response",
|
|
3117
|
+
`OpenCode ${version2} session ${sessionId} returned an invalid response from GET ${path}: ${reason}`
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
async function requestPage(client, sessionId, path, signal) {
|
|
3121
|
+
let response;
|
|
3122
|
+
try {
|
|
3123
|
+
response = await client.request(path, signal ? { signal } : void 0, {
|
|
3124
|
+
allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100)
|
|
3125
|
+
});
|
|
3126
|
+
} catch (error2) {
|
|
3127
|
+
throw new SessionArchiveExportError(
|
|
3128
|
+
"request-failed",
|
|
3129
|
+
`OpenCode ${client.version} session ${sessionId} request GET ${path} failed: ${errorMessage2(error2)}`
|
|
3130
|
+
);
|
|
3131
|
+
}
|
|
3132
|
+
if (response.status === 404) {
|
|
3133
|
+
throw new SessionArchiveExportError(
|
|
3134
|
+
"not-found",
|
|
3135
|
+
`OpenCode ${client.version} session ${sessionId} was not found at GET ${path} (HTTP 404)`
|
|
3136
|
+
);
|
|
3137
|
+
}
|
|
3138
|
+
if (!response.ok) {
|
|
3139
|
+
throw new SessionArchiveExportError(
|
|
3140
|
+
"request-failed",
|
|
3141
|
+
`OpenCode ${client.version} session ${sessionId} request GET ${path} returned HTTP ${response.status}`
|
|
3142
|
+
);
|
|
3143
|
+
}
|
|
3144
|
+
let bodyText;
|
|
3145
|
+
try {
|
|
3146
|
+
bodyText = await response.text();
|
|
3147
|
+
} catch (error2) {
|
|
3148
|
+
throw new SessionArchiveExportError(
|
|
3149
|
+
"request-failed",
|
|
3150
|
+
`OpenCode ${client.version} session ${sessionId} response body from GET ${path} could not be read: ${errorMessage2(error2)}`
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
try {
|
|
3154
|
+
return { response, body: JSON.parse(bodyText) };
|
|
3155
|
+
} catch (error2) {
|
|
3156
|
+
if (!(error2 instanceof SyntaxError)) throw error2;
|
|
3157
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
3158
|
+
const looksLikeHtml = contentType.toLowerCase().includes("text/html") || /^\s*<!doctype\s+html|^\s*<html/i.test(bodyText);
|
|
3159
|
+
throw invalidResponse(
|
|
3160
|
+
client.version,
|
|
3161
|
+
sessionId,
|
|
3162
|
+
path,
|
|
3163
|
+
looksLikeHtml ? "received text/html, expected JSON" : "received non-JSON content, expected JSON"
|
|
3164
|
+
);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
function v1NextCursor(response, sessionId, path) {
|
|
3168
|
+
const headerCursor = response.headers.get("x-next-cursor")?.trim() || null;
|
|
3169
|
+
const linkHeader = response.headers.get("link");
|
|
3170
|
+
const nextLink = linkHeader?.split(",").map((link) => link.trim()).find((link) => /;\s*rel\s*=\s*"?next"?/i.test(link));
|
|
3171
|
+
let linkCursor = null;
|
|
3172
|
+
if (nextLink) {
|
|
3173
|
+
const target = nextLink.match(/<([^>]+)>/)?.[1];
|
|
3174
|
+
if (!target) {
|
|
3175
|
+
throw new SessionArchiveExportError(
|
|
3176
|
+
"pagination-failed",
|
|
3177
|
+
`OpenCode v1 session ${sessionId} returned a next Link without a URL for GET ${path}`
|
|
3178
|
+
);
|
|
3179
|
+
}
|
|
3180
|
+
try {
|
|
3181
|
+
linkCursor = new URL(target, "http://127.0.0.1").searchParams.get("before");
|
|
3182
|
+
} catch (error2) {
|
|
3183
|
+
throw new SessionArchiveExportError(
|
|
3184
|
+
"pagination-failed",
|
|
3185
|
+
`OpenCode v1 session ${sessionId} returned an invalid next Link for GET ${path}: ${errorMessage2(error2)}`
|
|
3186
|
+
);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
if (headerCursor && linkCursor && headerCursor !== linkCursor) {
|
|
3190
|
+
throw new SessionArchiveExportError(
|
|
3191
|
+
"pagination-failed",
|
|
3192
|
+
`OpenCode v1 session ${sessionId} returned conflicting next cursors for GET ${path}`
|
|
3193
|
+
);
|
|
3194
|
+
}
|
|
3195
|
+
const cursor = headerCursor ?? linkCursor;
|
|
3196
|
+
if ((headerCursor || nextLink) && !cursor) {
|
|
3197
|
+
throw new SessionArchiveExportError(
|
|
3198
|
+
"pagination-failed",
|
|
3199
|
+
`OpenCode v1 session ${sessionId} returned a next page without a usable cursor for GET ${path}`
|
|
3200
|
+
);
|
|
3201
|
+
}
|
|
3202
|
+
return cursor;
|
|
3203
|
+
}
|
|
3204
|
+
function v1Messages(body, sessionId, path) {
|
|
3205
|
+
if (!Array.isArray(body)) {
|
|
3206
|
+
throw invalidResponse("v1", sessionId, path, "expected an array of messages");
|
|
3207
|
+
}
|
|
3208
|
+
return body;
|
|
3209
|
+
}
|
|
3210
|
+
async function exportV1SessionMessages(client, sessionId, signal) {
|
|
3211
|
+
const basePath = `/session/${encodeURIComponent(sessionId)}/message`;
|
|
3212
|
+
const firstPage = await requestPage(client, sessionId, basePath, signal);
|
|
3213
|
+
let messages = v1Messages(firstPage.body, sessionId, basePath);
|
|
3214
|
+
let cursor = v1NextCursor(firstPage.response, sessionId, basePath);
|
|
3215
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
3216
|
+
while (cursor !== null) {
|
|
3217
|
+
if (seenCursors.has(cursor)) {
|
|
3218
|
+
throw new SessionArchiveExportError(
|
|
3219
|
+
"pagination-failed",
|
|
3220
|
+
`OpenCode v1 session ${sessionId} repeated cursor while reading GET ${basePath}`
|
|
3221
|
+
);
|
|
3222
|
+
}
|
|
3223
|
+
seenCursors.add(cursor);
|
|
3224
|
+
const path = `${basePath}?${new URLSearchParams({ before: cursor }).toString()}`;
|
|
3225
|
+
const page = await requestPage(client, sessionId, path, signal);
|
|
3226
|
+
messages = [...v1Messages(page.body, sessionId, path), ...messages];
|
|
3227
|
+
cursor = v1NextCursor(page.response, sessionId, path);
|
|
3228
|
+
}
|
|
3229
|
+
return messages;
|
|
3230
|
+
}
|
|
3231
|
+
async function exportV2SessionMessages(client, sessionId, signal) {
|
|
3232
|
+
const basePath = `/api/session/${encodeURIComponent(sessionId)}/message`;
|
|
3233
|
+
const messages = [];
|
|
3234
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
3235
|
+
let cursor;
|
|
3236
|
+
let hasNextPage = true;
|
|
3237
|
+
while (hasNextPage) {
|
|
3238
|
+
const params = new URLSearchParams({ limit: "100" });
|
|
3239
|
+
if (cursor === void 0) {
|
|
3240
|
+
params.set("order", "asc");
|
|
3241
|
+
} else {
|
|
3242
|
+
params.set("cursor", cursor);
|
|
3243
|
+
}
|
|
3244
|
+
const path = `${basePath}?${params.toString()}`;
|
|
3245
|
+
const { body } = await requestPage(client, sessionId, path, signal);
|
|
3246
|
+
if (!isRecord2(body) || !Array.isArray(body.data) || !isRecord2(body.cursor)) {
|
|
3247
|
+
throw invalidResponse("v2", sessionId, path, "expected a {data: [], cursor: {\u2026}} envelope");
|
|
3248
|
+
}
|
|
3249
|
+
messages.push(...body.data);
|
|
3250
|
+
const next = body.cursor.next;
|
|
3251
|
+
if (next === void 0 || next === null) {
|
|
3252
|
+
hasNextPage = false;
|
|
3253
|
+
continue;
|
|
3254
|
+
}
|
|
3255
|
+
if (typeof next !== "string" || next.length === 0 || seenCursors.has(next)) {
|
|
3256
|
+
throw new SessionArchiveExportError(
|
|
3257
|
+
"pagination-failed",
|
|
3258
|
+
`OpenCode v2 session ${sessionId} returned an invalid or repeated cursor from GET ${path}`
|
|
3259
|
+
);
|
|
3260
|
+
}
|
|
3261
|
+
seenCursors.add(next);
|
|
3262
|
+
cursor = next;
|
|
3263
|
+
}
|
|
3264
|
+
return messages;
|
|
3265
|
+
}
|
|
3266
|
+
async function exportSessionMessages(client, sessionId, signal) {
|
|
3267
|
+
if (client.version === "v1") return exportV1SessionMessages(client, sessionId, signal);
|
|
3268
|
+
if (client.version === "v2") return exportV2SessionMessages(client, sessionId, signal);
|
|
3269
|
+
throw new SessionArchiveExportError(
|
|
3270
|
+
"unsupported-dialect",
|
|
3271
|
+
`OpenCode session ${sessionId} cannot be archived: unsupported API dialect ${String(client.version)}`
|
|
3272
|
+
);
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
// src/lib/opencode/session-archive-watermarks.ts
|
|
3276
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3277
|
+
import { dirname as dirname6, join as join6 } from "node:path";
|
|
3278
|
+
function sessionArchiveWatermarkPath(homeDir, env) {
|
|
3279
|
+
const override = env.EVIDENT_SESSION_ARCHIVE_WATERMARK?.trim();
|
|
3280
|
+
return override || join6(homeDir, ".local", "state", "evident", "session-archive-watermarks.json");
|
|
3281
|
+
}
|
|
3282
|
+
function readWatermarks(path, log3) {
|
|
3283
|
+
let content;
|
|
3284
|
+
try {
|
|
3285
|
+
content = readFileSync6(path, "utf8");
|
|
3286
|
+
} catch (error2) {
|
|
3287
|
+
if (isMissingFile3(error2)) return /* @__PURE__ */ new Map();
|
|
3288
|
+
log3(`[session-archive-watermarks] could not read ${path}; treating every session as changed`);
|
|
3289
|
+
return /* @__PURE__ */ new Map();
|
|
3290
|
+
}
|
|
3291
|
+
try {
|
|
3292
|
+
const parsed = JSON.parse(content);
|
|
3293
|
+
if (!isRecord3(parsed)) throw new Error("invalid watermark object");
|
|
3294
|
+
const entries = Object.entries(parsed);
|
|
3295
|
+
if (!entries.every(([sessionId, value]) => sessionId.length > 0 && isWatermark(value))) {
|
|
3296
|
+
throw new Error("invalid watermark entry");
|
|
3297
|
+
}
|
|
3298
|
+
return new Map(entries);
|
|
3299
|
+
} catch (error2) {
|
|
3300
|
+
log3(`[session-archive-watermarks] could not parse ${path}; treating every session as changed`);
|
|
3301
|
+
return /* @__PURE__ */ new Map();
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
function recordUpload(path, sessionId, entry, watermarks) {
|
|
3305
|
+
const updated = new Map(watermarks);
|
|
3306
|
+
updated.set(sessionId, entry);
|
|
3307
|
+
mkdirSync3(dirname6(path), { recursive: true });
|
|
3308
|
+
writeFileSync3(path, `${JSON.stringify(Object.fromEntries(updated))}
|
|
3309
|
+
`, {
|
|
3310
|
+
encoding: "utf8",
|
|
3311
|
+
mode: 384
|
|
3312
|
+
});
|
|
3313
|
+
watermarks.clear();
|
|
3314
|
+
for (const [id, watermark] of updated) watermarks.set(id, watermark);
|
|
3315
|
+
}
|
|
3316
|
+
function isRecord3(value) {
|
|
3317
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3318
|
+
}
|
|
3319
|
+
function isWatermark(value) {
|
|
3320
|
+
if (!isRecord3(value)) return false;
|
|
3321
|
+
return typeof value.fingerprint === "string" && typeof value.uploaded_at === "string" && typeof value.key === "string" && (!("conversation_id" in value) || value.conversation_id === null || typeof value.conversation_id === "string");
|
|
3322
|
+
}
|
|
3323
|
+
function isMissingFile3(error2) {
|
|
3324
|
+
return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
// src/lib/runner-synchroniser.ts
|
|
3328
|
+
import { spawn } from "node:child_process";
|
|
3329
|
+
|
|
3330
|
+
// ../../runner/synchroniser/src/shell-contract.json
|
|
3331
|
+
var shell_contract_default = {
|
|
3332
|
+
commands: [
|
|
3333
|
+
"env",
|
|
3334
|
+
"litestream-config",
|
|
3335
|
+
"restore",
|
|
3336
|
+
"sync-once",
|
|
3337
|
+
"model-auth-ready",
|
|
3338
|
+
"self-stop",
|
|
3339
|
+
"session-db-classify",
|
|
3340
|
+
"session-db-verify",
|
|
3341
|
+
"archive-put"
|
|
3342
|
+
],
|
|
3343
|
+
archiveStdinMaxBytes: 33554432
|
|
3344
|
+
};
|
|
3345
|
+
|
|
3346
|
+
// src/lib/runner-synchroniser.ts
|
|
3347
|
+
var MAX_ARCHIVE_STDIN_BYTES = shell_contract_default.archiveStdinMaxBytes;
|
|
3348
|
+
var ERROR_CLOSE_FALLBACK_MS = 1e3;
|
|
3349
|
+
function isPipeClosed(error2) {
|
|
3350
|
+
return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "EPIPE";
|
|
3351
|
+
}
|
|
3352
|
+
function appendError(stderr, error2) {
|
|
3353
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
3354
|
+
return stderr === "" ? message : `${stderr}
|
|
3355
|
+
${message}`;
|
|
3356
|
+
}
|
|
3357
|
+
function runSynchroniser(args, opts) {
|
|
3358
|
+
return new Promise((resolve5) => {
|
|
3359
|
+
let child;
|
|
3360
|
+
let stdout = "";
|
|
3361
|
+
let stderr = "";
|
|
3362
|
+
let settled = false;
|
|
3363
|
+
let shuttingDown = false;
|
|
3364
|
+
let timedOut = false;
|
|
3365
|
+
let stdinFailure;
|
|
3366
|
+
let childFailure;
|
|
3367
|
+
const timer = {};
|
|
3368
|
+
const closeFallbackTimer = {};
|
|
3369
|
+
let abortListener;
|
|
3370
|
+
let spawnListener;
|
|
3371
|
+
const finish = (result) => {
|
|
3372
|
+
if (settled) return;
|
|
3373
|
+
settled = true;
|
|
3374
|
+
if (timer.handle) clearTimeout(timer.handle);
|
|
3375
|
+
if (closeFallbackTimer.handle) clearTimeout(closeFallbackTimer.handle);
|
|
3376
|
+
if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
|
|
3377
|
+
if (spawnListener) child.removeListener("spawn", spawnListener);
|
|
3378
|
+
resolve5(result);
|
|
3379
|
+
};
|
|
3380
|
+
const armErrorCloseFallback = () => {
|
|
3381
|
+
if (closeFallbackTimer.handle) return;
|
|
3382
|
+
closeFallbackTimer.handle = setTimeout(() => {
|
|
3383
|
+
const failure = stdinFailure ?? childFailure;
|
|
3384
|
+
finish({
|
|
3385
|
+
code: null,
|
|
3386
|
+
stdout,
|
|
3387
|
+
stderr: failure === void 0 ? stderr : appendError(stderr, failure),
|
|
3388
|
+
timedOut
|
|
3389
|
+
});
|
|
3390
|
+
}, ERROR_CLOSE_FALLBACK_MS);
|
|
3391
|
+
};
|
|
3392
|
+
const terminateChild = () => {
|
|
3393
|
+
if (!shuttingDown) {
|
|
3394
|
+
shuttingDown = true;
|
|
3395
|
+
child.stdin?.destroy();
|
|
3396
|
+
}
|
|
3397
|
+
if (child.pid !== void 0) {
|
|
3398
|
+
child.kill("SIGKILL");
|
|
3399
|
+
} else if (!spawnListener) {
|
|
3400
|
+
spawnListener = () => {
|
|
3401
|
+
if (child.pid !== void 0) child.kill("SIGKILL");
|
|
3402
|
+
};
|
|
3403
|
+
child.once("spawn", spawnListener);
|
|
3404
|
+
}
|
|
3405
|
+
};
|
|
3406
|
+
try {
|
|
3407
|
+
child = spawn("runner-synchroniser", args, {
|
|
3408
|
+
env: opts.env ?? process.env,
|
|
3409
|
+
stdio: [opts.stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"]
|
|
3410
|
+
});
|
|
3411
|
+
} catch (error2) {
|
|
3412
|
+
finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3415
|
+
child.stdout?.setEncoding("utf8");
|
|
3416
|
+
child.stdout?.on("data", (chunk) => {
|
|
3417
|
+
stdout += chunk;
|
|
3418
|
+
});
|
|
3419
|
+
child.stderr?.setEncoding("utf8");
|
|
3420
|
+
child.stderr?.on("data", (chunk) => {
|
|
3421
|
+
stderr += chunk;
|
|
3422
|
+
});
|
|
3423
|
+
child.once("error", (error2) => {
|
|
3424
|
+
childFailure = error2;
|
|
3425
|
+
if (child.pid !== void 0) terminateChild();
|
|
3426
|
+
armErrorCloseFallback();
|
|
3427
|
+
});
|
|
3428
|
+
child.once("close", (code) => {
|
|
3429
|
+
const failure = stdinFailure ?? childFailure;
|
|
3430
|
+
finish({
|
|
3431
|
+
code: failure === void 0 ? code : null,
|
|
3432
|
+
stdout,
|
|
3433
|
+
stderr: failure === void 0 ? stderr : appendError(stderr, failure),
|
|
3434
|
+
timedOut
|
|
3435
|
+
});
|
|
3436
|
+
});
|
|
3437
|
+
const stdin = child.stdin;
|
|
3438
|
+
const failStdin = (error2) => {
|
|
3439
|
+
if (settled || shuttingDown || isPipeClosed(error2)) return;
|
|
3440
|
+
stdinFailure = error2;
|
|
3441
|
+
terminateChild();
|
|
3442
|
+
armErrorCloseFallback();
|
|
3443
|
+
};
|
|
3444
|
+
stdin?.once("error", failStdin);
|
|
3445
|
+
if (opts.stdin !== void 0 && stdin === null) {
|
|
3446
|
+
failStdin(new Error("synchroniser stdin is unavailable"));
|
|
3447
|
+
} else if (opts.stdin !== void 0 && stdin !== null) {
|
|
3448
|
+
const drained = stdin.write(opts.stdin, (error2) => {
|
|
3449
|
+
if (error2) failStdin(error2);
|
|
3450
|
+
});
|
|
3451
|
+
if (!drained) {
|
|
3452
|
+
stdin.once("drain", () => stdin.end());
|
|
3453
|
+
} else {
|
|
3454
|
+
stdin.end();
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
if (opts.signal) {
|
|
3458
|
+
const killChild = () => {
|
|
3459
|
+
terminateChild();
|
|
3460
|
+
};
|
|
3461
|
+
abortListener = killChild;
|
|
3462
|
+
if (opts.signal.aborted) {
|
|
3463
|
+
abortListener();
|
|
3464
|
+
} else {
|
|
3465
|
+
opts.signal.addEventListener("abort", abortListener, { once: true });
|
|
3466
|
+
if (opts.signal.aborted) abortListener();
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
timer.handle = setTimeout(
|
|
3470
|
+
() => {
|
|
3471
|
+
timedOut = true;
|
|
3472
|
+
terminateChild();
|
|
3473
|
+
},
|
|
3474
|
+
Math.max(0, opts.timeoutMs)
|
|
3475
|
+
);
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
// src/lib/opencode/session-archive-upload.ts
|
|
3480
|
+
var MAX_SESSION_ARCHIVE_BYTES = MAX_ARCHIVE_STDIN_BYTES;
|
|
3481
|
+
var EXIT_ARCHIVE_CONFIG_MISSING = 50;
|
|
3482
|
+
var EXIT_ARCHIVE_ACCESS_DENIED = 51;
|
|
3483
|
+
function transportFailure(bytes, reason, exitCode) {
|
|
3484
|
+
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.";
|
|
3485
|
+
return {
|
|
3486
|
+
kind: "transport-failed",
|
|
3487
|
+
bytes,
|
|
3488
|
+
reason,
|
|
3489
|
+
message,
|
|
3490
|
+
...exitCode !== void 0 ? { exitCode } : {}
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
3493
|
+
function isJson(body) {
|
|
3494
|
+
try {
|
|
3495
|
+
JSON.parse(body.toString("utf8"));
|
|
3496
|
+
return true;
|
|
3497
|
+
} catch (error2) {
|
|
3498
|
+
void error2;
|
|
3499
|
+
return false;
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
async function uploadSessionArchive(key, body, options) {
|
|
3503
|
+
const bytes = Buffer.from(body);
|
|
3504
|
+
if (bytes.byteLength > MAX_SESSION_ARCHIVE_BYTES) {
|
|
3505
|
+
return transportFailure(bytes.byteLength, "too-large");
|
|
3506
|
+
}
|
|
3507
|
+
if (!isJson(bytes)) return transportFailure(bytes.byteLength, "invalid-json");
|
|
3508
|
+
const run2 = options.runSynchroniser ?? runSynchroniser;
|
|
3509
|
+
let result;
|
|
3510
|
+
try {
|
|
3511
|
+
result = await run2(["archive-put", key], {
|
|
3512
|
+
timeoutMs: options.timeoutMs,
|
|
3513
|
+
env: options.env,
|
|
3514
|
+
signal: options.signal,
|
|
3515
|
+
stdin: bytes
|
|
3516
|
+
});
|
|
3517
|
+
} catch (error2) {
|
|
3518
|
+
return transportFailure(
|
|
3519
|
+
bytes.byteLength,
|
|
3520
|
+
error2 instanceof Error && /ENOENT|runner-synchroniser/i.test(error2.message) ? "missing-executable" : "spawn-failed"
|
|
3521
|
+
);
|
|
3522
|
+
}
|
|
3523
|
+
if (result.timedOut) return transportFailure(bytes.byteLength, "timeout");
|
|
3524
|
+
if (result.code === 0) return { kind: "ok", bytes: bytes.byteLength };
|
|
3525
|
+
if (result.code === EXIT_ARCHIVE_CONFIG_MISSING) {
|
|
3526
|
+
return { kind: "config-missing", bytes: bytes.byteLength };
|
|
3527
|
+
}
|
|
3528
|
+
if (result.code === EXIT_ARCHIVE_ACCESS_DENIED) {
|
|
3529
|
+
return { kind: "access-denied", bytes: bytes.byteLength };
|
|
3530
|
+
}
|
|
3531
|
+
if (result.code === null && /ENOENT|runner-synchroniser/i.test(result.stderr)) {
|
|
3532
|
+
return transportFailure(bytes.byteLength, "missing-executable");
|
|
3533
|
+
}
|
|
3534
|
+
return transportFailure(
|
|
3535
|
+
bytes.byteLength,
|
|
3536
|
+
result.code === null ? "spawn-failed" : "process-failed",
|
|
3537
|
+
result.code ?? void 0
|
|
3538
|
+
);
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
// src/lib/opencode/session-binding-resolution.ts
|
|
3542
|
+
var SESSION_BINDING_PROJECTION_CHUNK_SIZE = 500;
|
|
3543
|
+
var SESSION_BINDING_PROJECTION_TIMEOUT_MS = 1e4;
|
|
3544
|
+
function isRecord4(value) {
|
|
3545
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3546
|
+
}
|
|
3547
|
+
function isProjectionSource(value) {
|
|
3548
|
+
return value === "current" || value === "invocation" || value === "history";
|
|
3549
|
+
}
|
|
3550
|
+
function emptySessionBindingProjection() {
|
|
3551
|
+
return {
|
|
3552
|
+
current: /* @__PURE__ */ new Map(),
|
|
3553
|
+
invocation: /* @__PURE__ */ new Map(),
|
|
3554
|
+
history: /* @__PURE__ */ new Map(),
|
|
3555
|
+
conflicts: /* @__PURE__ */ new Set()
|
|
3556
|
+
};
|
|
3557
|
+
}
|
|
3558
|
+
function requestedSessionIds(sessions) {
|
|
3559
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3560
|
+
for (const session of sessions) {
|
|
3561
|
+
if (session.id.length > 0) ids.add(session.id);
|
|
3562
|
+
if (typeof session.parentID === "string" && session.parentID.length > 0) {
|
|
3563
|
+
ids.add(session.parentID);
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
return [...ids];
|
|
3567
|
+
}
|
|
3568
|
+
function parseProjectionResponse(value, requestedIds) {
|
|
3569
|
+
if (!isRecord4(value) || !Array.isArray(value.bindings) || !Array.isArray(value.conflicts)) {
|
|
3570
|
+
return null;
|
|
3571
|
+
}
|
|
3572
|
+
const projection = emptySessionBindingProjection();
|
|
3573
|
+
for (const candidate of value.bindings) {
|
|
3574
|
+
if (!isRecord4(candidate) || typeof candidate.opencode_session_id !== "string" || typeof candidate.conversation_id !== "string" || !isProjectionSource(candidate.source) || !requestedIds.has(candidate.opencode_session_id) || projection.conflicts.has(candidate.opencode_session_id)) {
|
|
3575
|
+
return null;
|
|
3576
|
+
}
|
|
3577
|
+
const bindings = projection[candidate.source];
|
|
3578
|
+
if (bindings.has(candidate.opencode_session_id)) return null;
|
|
3579
|
+
bindings.set(candidate.opencode_session_id, candidate.conversation_id);
|
|
3580
|
+
}
|
|
3581
|
+
for (const conflict of value.conflicts) {
|
|
3582
|
+
if (typeof conflict !== "string" || !requestedIds.has(conflict) || projection.conflicts.has(conflict) || projection.current.has(conflict) || projection.invocation.has(conflict) || projection.history.has(conflict)) {
|
|
3583
|
+
return null;
|
|
3584
|
+
}
|
|
3585
|
+
projection.conflicts.add(conflict);
|
|
3586
|
+
}
|
|
3587
|
+
return projection;
|
|
3588
|
+
}
|
|
3589
|
+
async function fetchSessionBindingProjection({
|
|
3590
|
+
apiUrl,
|
|
3591
|
+
runnerId,
|
|
3592
|
+
authHeader,
|
|
3593
|
+
sessions,
|
|
3594
|
+
signal,
|
|
3595
|
+
fetchImpl = fetch
|
|
3596
|
+
}) {
|
|
3597
|
+
const ids = requestedSessionIds(sessions);
|
|
3598
|
+
const projection = emptySessionBindingProjection();
|
|
3599
|
+
const failures = [];
|
|
3600
|
+
if (ids.length === 0) return { projection, complete: true, failures };
|
|
3601
|
+
for (let offset = 0; offset < ids.length; offset += SESSION_BINDING_PROJECTION_CHUNK_SIZE) {
|
|
3602
|
+
const chunk = ids.slice(offset, offset + SESSION_BINDING_PROJECTION_CHUNK_SIZE);
|
|
3603
|
+
const chunkIndex = Math.floor(offset / SESSION_BINDING_PROJECTION_CHUNK_SIZE) + 1;
|
|
3604
|
+
try {
|
|
3605
|
+
const response = await fetchImpl(
|
|
3606
|
+
`${apiUrl}/runners/${runnerId}/conversations/session-bindings`,
|
|
3607
|
+
{
|
|
3608
|
+
method: "POST",
|
|
3609
|
+
headers: {
|
|
3610
|
+
Authorization: authHeader,
|
|
3611
|
+
"Content-Type": "application/json"
|
|
3612
|
+
},
|
|
3613
|
+
body: JSON.stringify({ opencode_session_ids: chunk }),
|
|
3614
|
+
signal: signal ?? AbortSignal.timeout(SESSION_BINDING_PROJECTION_TIMEOUT_MS)
|
|
3615
|
+
}
|
|
3616
|
+
);
|
|
3617
|
+
if (!response.ok) {
|
|
3618
|
+
failures.push({ chunk: chunkIndex, kind: "http", status: response.status });
|
|
3619
|
+
continue;
|
|
3620
|
+
}
|
|
3621
|
+
const result = parseProjectionResponse(await response.json(), new Set(chunk));
|
|
3622
|
+
if (result === null) {
|
|
3623
|
+
failures.push({ chunk: chunkIndex, kind: "invalid-response" });
|
|
3624
|
+
continue;
|
|
3625
|
+
}
|
|
3626
|
+
for (const source of ["current", "invocation", "history"]) {
|
|
3627
|
+
for (const [sessionId, conversationId] of result[source]) {
|
|
3628
|
+
projection[source].set(sessionId, conversationId);
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
for (const conflict of result.conflicts) projection.conflicts.add(conflict);
|
|
3632
|
+
} catch (error2) {
|
|
3633
|
+
failures.push({
|
|
3634
|
+
chunk: chunkIndex,
|
|
3635
|
+
kind: "network",
|
|
3636
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
3637
|
+
});
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
return { projection, complete: failures.length === 0, failures };
|
|
3641
|
+
}
|
|
3642
|
+
function directResolution(sessionId, sources) {
|
|
3643
|
+
const localBinding = sources.ledger.get(sessionId);
|
|
3644
|
+
if (localBinding) return { conversation_id: localBinding, source: "ledger" };
|
|
3645
|
+
const watermarkedBinding = sources.watermarks.get(sessionId)?.conversation_id;
|
|
3646
|
+
if (typeof watermarkedBinding === "string" && watermarkedBinding.length > 0) {
|
|
3647
|
+
return { conversation_id: watermarkedBinding, source: "watermark" };
|
|
3648
|
+
}
|
|
3649
|
+
if (sources.projection.conflicts.has(sessionId)) return { reason: "conflict" };
|
|
3650
|
+
for (const source of ["current", "invocation", "history"]) {
|
|
3651
|
+
const conversationId = sources.projection[source].get(sessionId);
|
|
3652
|
+
if (conversationId) return { conversation_id: conversationId, source };
|
|
3653
|
+
}
|
|
3654
|
+
return null;
|
|
3655
|
+
}
|
|
3656
|
+
function isResolved(resolution) {
|
|
3657
|
+
return "conversation_id" in resolution;
|
|
3658
|
+
}
|
|
3659
|
+
function resolveSessionBindings(sessions, sources) {
|
|
3660
|
+
const resolutions = /* @__PURE__ */ new Map();
|
|
3661
|
+
for (const session of sessions) {
|
|
3662
|
+
resolutions.set(session.id, directResolution(session.id, sources) ?? { reason: "unmapped" });
|
|
3663
|
+
}
|
|
3664
|
+
let assignedParent = true;
|
|
3665
|
+
while (assignedParent) {
|
|
3666
|
+
assignedParent = false;
|
|
3667
|
+
for (const session of sessions) {
|
|
3668
|
+
const resolution = resolutions.get(session.id);
|
|
3669
|
+
if (isResolved(resolution) || resolution.reason !== "unmapped") continue;
|
|
3670
|
+
if (typeof session.parentID !== "string" || session.parentID.length === 0) continue;
|
|
3671
|
+
const parent = resolutions.get(session.parentID) ?? directResolution(session.parentID, sources) ?? { reason: "unmapped" };
|
|
3672
|
+
if (isResolved(parent)) {
|
|
3673
|
+
resolutions.set(session.id, {
|
|
3674
|
+
conversation_id: parent.conversation_id,
|
|
3675
|
+
source: "parent"
|
|
3676
|
+
});
|
|
3677
|
+
assignedParent = true;
|
|
3678
|
+
} else if (parent.reason === "conflict" || parent.reason === "parent-conflict") {
|
|
3679
|
+
resolutions.set(session.id, { reason: "parent-conflict" });
|
|
3680
|
+
assignedParent = true;
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
return resolutions;
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
// src/lib/opencode/session-archive-run.ts
|
|
3688
|
+
var SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS = 6e4;
|
|
3689
|
+
function updatedAt(session) {
|
|
3690
|
+
const updated = session.time?.updated;
|
|
3691
|
+
return typeof updated === "number" && Number.isFinite(updated) ? updated : -Infinity;
|
|
3692
|
+
}
|
|
3693
|
+
function sessionArchiveKey(prefix, sessionId) {
|
|
3694
|
+
return `${prefix.replace(/\/+$/, "")}/conversation-archives/${encodeURIComponent(sessionId)}.json`;
|
|
3695
|
+
}
|
|
3696
|
+
function failureMessage(error2) {
|
|
3697
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
3698
|
+
}
|
|
3699
|
+
function countMessageParts(messages) {
|
|
3700
|
+
return messages.reduce((count, message) => {
|
|
3701
|
+
if (typeof message !== "object" || message === null || !("parts" in message)) return count;
|
|
3702
|
+
return count + (Array.isArray(message.parts) ? message.parts.length : 0);
|
|
3703
|
+
}, 0);
|
|
3704
|
+
}
|
|
3705
|
+
function isBudgetExpired(deadline, now, signal) {
|
|
3706
|
+
return signal?.aborted === true || now() >= deadline;
|
|
3707
|
+
}
|
|
3708
|
+
function createBudgetSignal(parent, timeoutMs) {
|
|
3709
|
+
const controller = new AbortController();
|
|
3710
|
+
const forwardAbort = () => controller.abort(parent?.reason);
|
|
3711
|
+
if (parent?.aborted) forwardAbort();
|
|
3712
|
+
else parent?.addEventListener("abort", forwardAbort, { once: true });
|
|
3713
|
+
const timer = setTimeout(
|
|
3714
|
+
() => controller.abort(new Error("session archive budget exceeded")),
|
|
3715
|
+
timeoutMs
|
|
3716
|
+
);
|
|
3717
|
+
return {
|
|
3718
|
+
signal: controller.signal,
|
|
3719
|
+
dispose: () => {
|
|
3720
|
+
clearTimeout(timer);
|
|
3721
|
+
parent?.removeEventListener("abort", forwardAbort);
|
|
3722
|
+
}
|
|
3723
|
+
};
|
|
3724
|
+
}
|
|
3725
|
+
async function runSessionArchive({
|
|
3726
|
+
sessions,
|
|
3727
|
+
deps,
|
|
3728
|
+
budgetMs,
|
|
3729
|
+
now = Date.now
|
|
3730
|
+
}) {
|
|
3731
|
+
const startedAt = now();
|
|
3732
|
+
const deadline = startedAt + Math.max(0, budgetMs);
|
|
3733
|
+
const summary = {
|
|
3734
|
+
inspected: sessions.length,
|
|
3735
|
+
uploaded: 0,
|
|
3736
|
+
unchanged: 0,
|
|
3737
|
+
failed: 0,
|
|
3738
|
+
uploadAttempts: 0,
|
|
3739
|
+
uploadFailures: 0,
|
|
3740
|
+
skippedBudget: 0,
|
|
3741
|
+
skippedBudgetIds: [],
|
|
3742
|
+
neverWatermarkedFailures: 0,
|
|
3743
|
+
unmappedIds: [],
|
|
3744
|
+
conflictedIds: [],
|
|
3745
|
+
unresolved: 0,
|
|
3746
|
+
conflicts: 0,
|
|
3747
|
+
reconciliationComplete: deps.reconciliationComplete ?? true,
|
|
3748
|
+
reconciliationIncomplete: 0,
|
|
3749
|
+
bindingSourceCounts: {
|
|
3750
|
+
ledger: 0,
|
|
3751
|
+
watermark: 0,
|
|
3752
|
+
current: 0,
|
|
3753
|
+
invocation: 0,
|
|
3754
|
+
history: 0,
|
|
3755
|
+
parent: 0
|
|
3756
|
+
},
|
|
3757
|
+
archivedSessionIds: [],
|
|
3758
|
+
failures: [],
|
|
3759
|
+
staleSince: null
|
|
3760
|
+
};
|
|
3761
|
+
const ordered = [...sessions].sort((left, right) => updatedAt(right) - updatedAt(left));
|
|
3762
|
+
const upload = deps.upload ?? uploadSessionArchive;
|
|
3763
|
+
const exportMessages = deps.exportMessages ?? exportSessionMessages;
|
|
3764
|
+
const writeWatermark = deps.recordWatermark ?? recordUpload;
|
|
3765
|
+
const resolutions = resolveSessionBindings(sessions, {
|
|
3766
|
+
ledger: deps.bindings,
|
|
3767
|
+
watermarks: deps.watermarks,
|
|
3768
|
+
projection: deps.projection ?? emptySessionBindingProjection()
|
|
3769
|
+
});
|
|
3770
|
+
for (const [sessionId, resolution] of resolutions) {
|
|
3771
|
+
if ("conversation_id" in resolution) {
|
|
3772
|
+
summary.bindingSourceCounts[resolution.source]++;
|
|
3773
|
+
} else if (resolution.reason === "conflict" || resolution.reason === "parent-conflict") {
|
|
3774
|
+
summary.conflictedIds.push(sessionId);
|
|
3775
|
+
} else {
|
|
3776
|
+
summary.unmappedIds.push(sessionId);
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
summary.unresolved = summary.unmappedIds.length + summary.conflictedIds.length;
|
|
3780
|
+
summary.conflicts = summary.conflictedIds.length;
|
|
3781
|
+
const failedWatermarks = [];
|
|
3782
|
+
const recordFailedWatermark = (watermark) => {
|
|
3783
|
+
if (watermark) failedWatermarks.push(watermark.uploaded_at);
|
|
3784
|
+
else summary.neverWatermarkedFailures++;
|
|
3785
|
+
};
|
|
3786
|
+
for (let index = 0; index < ordered.length; index++) {
|
|
3787
|
+
const session = ordered[index];
|
|
3788
|
+
const remainingMs = Math.floor(deadline - now());
|
|
3789
|
+
if (remainingMs <= 0 || deps.signal?.aborted) {
|
|
3790
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3791
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3792
|
+
break;
|
|
3793
|
+
}
|
|
3794
|
+
const resolution = resolutions.get(session.id) ?? { reason: "unmapped" };
|
|
3795
|
+
const resolved = "conversation_id" in resolution;
|
|
3796
|
+
const conversationId = resolved ? resolution.conversation_id : null;
|
|
3797
|
+
const bindingSource = resolved ? resolution.source : "unmapped";
|
|
3798
|
+
if (!resolved && !summary.reconciliationComplete) {
|
|
3799
|
+
summary.failed++;
|
|
3800
|
+
summary.reconciliationIncomplete++;
|
|
3801
|
+
summary.failures.push({
|
|
3802
|
+
sessionId: session.id,
|
|
3803
|
+
outcome: "reconciliation-incomplete",
|
|
3804
|
+
reason: resolution.reason,
|
|
3805
|
+
archived: false
|
|
3806
|
+
});
|
|
3807
|
+
recordFailedWatermark(deps.watermarks.get(session.id));
|
|
3808
|
+
continue;
|
|
3809
|
+
}
|
|
3810
|
+
const exportBudget = createBudgetSignal(deps.signal, remainingMs);
|
|
3811
|
+
let messages;
|
|
3812
|
+
try {
|
|
3813
|
+
messages = await exportMessages(deps.client, session.id, exportBudget.signal);
|
|
3814
|
+
} catch (error2) {
|
|
3815
|
+
if (isBudgetExpired(deadline, now, deps.signal) || exportBudget.signal.aborted) {
|
|
3816
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3817
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3818
|
+
break;
|
|
3819
|
+
}
|
|
3820
|
+
summary.failed++;
|
|
3821
|
+
const reason = error2 instanceof SessionArchiveExportError ? error2.code : "export-failed";
|
|
3822
|
+
summary.failures.push({
|
|
3823
|
+
sessionId: session.id,
|
|
3824
|
+
outcome: "export-failed",
|
|
3825
|
+
reason,
|
|
3826
|
+
message: failureMessage(error2),
|
|
3827
|
+
archived: false
|
|
3828
|
+
});
|
|
3829
|
+
recordFailedWatermark(deps.watermarks.get(session.id));
|
|
3830
|
+
continue;
|
|
3831
|
+
} finally {
|
|
3832
|
+
exportBudget.dispose();
|
|
3833
|
+
}
|
|
3834
|
+
if (isBudgetExpired(deadline, now, deps.signal) || exportBudget.signal.aborted) {
|
|
3835
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3836
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3837
|
+
break;
|
|
3838
|
+
}
|
|
3839
|
+
try {
|
|
3840
|
+
const archivedAt = new Date(now()).toISOString();
|
|
3841
|
+
const snapshot = buildSessionArchive({
|
|
3842
|
+
session,
|
|
3843
|
+
messages,
|
|
3844
|
+
apiVersion: deps.client.version,
|
|
3845
|
+
conversationId,
|
|
3846
|
+
bindingSource,
|
|
3847
|
+
runnerId: deps.runnerId,
|
|
3848
|
+
archivedAt
|
|
3849
|
+
});
|
|
3850
|
+
const key = sessionArchiveKey(deps.keyPrefix, session.id);
|
|
3851
|
+
const existing = deps.watermarks.get(session.id);
|
|
3852
|
+
if (existing?.fingerprint === snapshot.content_fingerprint) {
|
|
3853
|
+
summary.unchanged++;
|
|
3854
|
+
summary.archivedSessionIds.push(session.id);
|
|
3855
|
+
continue;
|
|
3856
|
+
}
|
|
3857
|
+
const uploadRemainingMs = Math.floor(deadline - now());
|
|
3858
|
+
if (uploadRemainingMs <= 0 || deps.signal?.aborted) {
|
|
3859
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3860
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3861
|
+
break;
|
|
3862
|
+
}
|
|
3863
|
+
summary.uploadAttempts++;
|
|
3864
|
+
const uploadBudget = createBudgetSignal(deps.signal, uploadRemainingMs);
|
|
3865
|
+
let outcome;
|
|
3866
|
+
try {
|
|
3867
|
+
outcome = await upload(key, JSON.stringify(snapshot), {
|
|
3868
|
+
timeoutMs: Math.min(uploadRemainingMs, SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS),
|
|
3869
|
+
signal: uploadBudget.signal
|
|
3870
|
+
});
|
|
3871
|
+
} finally {
|
|
3872
|
+
uploadBudget.dispose();
|
|
3873
|
+
}
|
|
3874
|
+
if (outcome.kind !== "ok") {
|
|
3875
|
+
summary.failed++;
|
|
3876
|
+
summary.uploadFailures++;
|
|
3877
|
+
const reason = "reason" in outcome ? outcome.reason : void 0;
|
|
3878
|
+
summary.failures.push({
|
|
3879
|
+
sessionId: session.id,
|
|
3880
|
+
outcome: outcome.kind,
|
|
3881
|
+
...reason !== void 0 ? { reason } : {},
|
|
3882
|
+
..."message" in outcome ? { message: outcome.message } : {},
|
|
3883
|
+
bytes: outcome.bytes,
|
|
3884
|
+
byteLimit: MAX_SESSION_ARCHIVE_BYTES,
|
|
3885
|
+
messageCount: messages.length,
|
|
3886
|
+
partCount: countMessageParts(messages),
|
|
3887
|
+
..."exitCode" in outcome && outcome.exitCode !== void 0 ? { exitCode: outcome.exitCode } : {},
|
|
3888
|
+
archived: false
|
|
3889
|
+
});
|
|
3890
|
+
recordFailedWatermark(existing);
|
|
3891
|
+
if (uploadBudget.signal.aborted) {
|
|
3892
|
+
summary.skippedBudgetIds.push(...ordered.slice(index + 1).map(({ id }) => id));
|
|
3893
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3894
|
+
break;
|
|
3895
|
+
}
|
|
3896
|
+
continue;
|
|
3897
|
+
}
|
|
3898
|
+
summary.uploaded++;
|
|
3899
|
+
summary.archivedSessionIds.push(session.id);
|
|
3900
|
+
const watermark = {
|
|
3901
|
+
fingerprint: snapshot.content_fingerprint,
|
|
3902
|
+
uploaded_at: new Date(now()).toISOString(),
|
|
3903
|
+
key,
|
|
3904
|
+
conversation_id: conversationId
|
|
3905
|
+
};
|
|
3906
|
+
try {
|
|
3907
|
+
writeWatermark(deps.watermarkPath, session.id, watermark, deps.watermarks);
|
|
3908
|
+
} catch (error2) {
|
|
3909
|
+
summary.failures.push({
|
|
3910
|
+
sessionId: session.id,
|
|
3911
|
+
outcome: "watermark-write-failed",
|
|
3912
|
+
message: failureMessage(error2),
|
|
3913
|
+
archived: true
|
|
3914
|
+
});
|
|
3915
|
+
}
|
|
3916
|
+
} catch (error2) {
|
|
3917
|
+
if (isBudgetExpired(deadline, now, deps.signal)) {
|
|
3918
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3919
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3920
|
+
break;
|
|
3921
|
+
}
|
|
3922
|
+
summary.failed++;
|
|
3923
|
+
summary.failures.push({
|
|
3924
|
+
sessionId: session.id,
|
|
3925
|
+
outcome: "archive-failed",
|
|
3926
|
+
message: failureMessage(error2),
|
|
3927
|
+
archived: false
|
|
3928
|
+
});
|
|
3929
|
+
recordFailedWatermark(deps.watermarks.get(session.id));
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
summary.staleSince = failedWatermarks.sort()[0] ?? null;
|
|
3933
|
+
return summary;
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3936
|
+
// src/lib/opencode/auth.ts
|
|
3937
|
+
function buildOpenCodeBasicAuthHeader(password) {
|
|
3938
|
+
return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
// src/lib/opencode/health.ts
|
|
3942
|
+
async function checkOpenCodeHealth(port) {
|
|
3943
|
+
try {
|
|
3944
|
+
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
|
3945
|
+
signal: AbortSignal.timeout(2e3)
|
|
3946
|
+
// 2 second timeout
|
|
3947
|
+
});
|
|
3948
|
+
if (response.status === 401) {
|
|
3949
|
+
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
3950
|
+
}
|
|
3951
|
+
if (!response.ok) {
|
|
3952
|
+
return { healthy: false, error: `HTTP ${response.status}` };
|
|
3953
|
+
}
|
|
3954
|
+
const body = await response.text();
|
|
3955
|
+
let data;
|
|
3956
|
+
try {
|
|
3957
|
+
data = JSON.parse(body);
|
|
3958
|
+
} catch (error2) {
|
|
3959
|
+
return {
|
|
3960
|
+
healthy: false,
|
|
3961
|
+
okWithNonJsonBody: true,
|
|
3962
|
+
error: `HTTP ${response.status}: invalid JSON health response${bodyExcerpt(body)}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3963
|
+
};
|
|
3964
|
+
}
|
|
3965
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
3966
|
+
return {
|
|
3967
|
+
healthy: false,
|
|
3968
|
+
okWithNonJsonBody: true,
|
|
3969
|
+
error: `HTTP ${response.status}: invalid health response${bodyExcerpt(body)}: expected JSON object`
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
const version2 = data.version;
|
|
3973
|
+
return typeof version2 === "string" ? { healthy: true, version: version2 } : { healthy: true };
|
|
3974
|
+
} catch (error2) {
|
|
3975
|
+
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
3976
|
+
return { healthy: false, error: message };
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
function bodyExcerpt(body) {
|
|
3980
|
+
const excerpt = body.replace(/\s+/g, " ").trim().slice(0, 160);
|
|
3981
|
+
return excerpt ? ` (body="${excerpt}")` : " (empty body)";
|
|
3982
|
+
}
|
|
3983
|
+
async function checkOpenCode2Health(port, password) {
|
|
3984
|
+
try {
|
|
3985
|
+
const headers = {};
|
|
3986
|
+
if (password !== void 0) {
|
|
3987
|
+
headers.Authorization = buildOpenCodeBasicAuthHeader(password);
|
|
3988
|
+
}
|
|
3989
|
+
let response = await fetch(`http://127.0.0.1:${port}/api/info`, {
|
|
3990
|
+
headers,
|
|
3991
|
+
signal: AbortSignal.timeout(2e3)
|
|
3992
|
+
});
|
|
3993
|
+
const usesHealthFallback = response.status === 404;
|
|
3994
|
+
if (usesHealthFallback) {
|
|
3995
|
+
response = await fetch(`http://127.0.0.1:${port}/api/health`, {
|
|
3996
|
+
headers,
|
|
3997
|
+
signal: AbortSignal.timeout(2e3)
|
|
3998
|
+
});
|
|
3999
|
+
}
|
|
4000
|
+
if (response.status === 401) {
|
|
4001
|
+
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
4002
|
+
}
|
|
4003
|
+
if (!response.ok) {
|
|
4004
|
+
return { healthy: false, error: `HTTP ${response.status}` };
|
|
4005
|
+
}
|
|
4006
|
+
const body = await response.text();
|
|
4007
|
+
const excerpt = bodyExcerpt(body);
|
|
4008
|
+
let data;
|
|
4009
|
+
try {
|
|
4010
|
+
data = JSON.parse(body);
|
|
4011
|
+
} catch (error2) {
|
|
4012
|
+
return {
|
|
4013
|
+
healthy: false,
|
|
4014
|
+
error: `HTTP ${response.status}: invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4015
|
+
};
|
|
4016
|
+
}
|
|
4017
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
4018
|
+
return {
|
|
4019
|
+
healthy: false,
|
|
4020
|
+
error: `HTTP ${response.status}: invalid health response${excerpt}: expected healthy=true`
|
|
4021
|
+
};
|
|
4022
|
+
}
|
|
4023
|
+
const bodyObject = data;
|
|
4024
|
+
if (usesHealthFallback && bodyObject.healthy !== true) {
|
|
4025
|
+
return {
|
|
4026
|
+
healthy: false,
|
|
4027
|
+
error: `Invalid health response${excerpt}: expected healthy=true`
|
|
4028
|
+
};
|
|
4029
|
+
}
|
|
4030
|
+
if (!usesHealthFallback) {
|
|
4031
|
+
return typeof bodyObject.version === "string" && bodyObject.version.length > 0 ? { healthy: true, version: bodyObject.version } : {
|
|
4032
|
+
healthy: false,
|
|
4033
|
+
error: `HTTP ${response.status}: invalid info response${excerpt}: expected a non-empty version`
|
|
4034
|
+
};
|
|
4035
|
+
}
|
|
4036
|
+
return typeof bodyObject.version === "string" ? { healthy: true, version: bodyObject.version } : { healthy: true };
|
|
4037
|
+
} catch (error2) {
|
|
4038
|
+
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
4039
|
+
return { healthy: false, error: message };
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
4043
|
+
const startTime = Date.now();
|
|
4044
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
4045
|
+
const health = await checkOpenCodeHealth(port);
|
|
4046
|
+
if (health.healthy || health.authFailed) {
|
|
4047
|
+
return health;
|
|
4048
|
+
}
|
|
4049
|
+
await new Promise((resolve5) => setTimeout(resolve5, 1e3));
|
|
4050
|
+
}
|
|
4051
|
+
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
4052
|
+
}
|
|
4053
|
+
async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
|
|
4054
|
+
const startTime = Date.now();
|
|
4055
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
4056
|
+
const health = await checkOpenCode2Health(port, password);
|
|
4057
|
+
if (health.healthy || health.authFailed) {
|
|
4058
|
+
return health;
|
|
4059
|
+
}
|
|
4060
|
+
await new Promise((resolve5) => setTimeout(resolve5, 1e3));
|
|
4061
|
+
}
|
|
4062
|
+
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
// src/lib/http-timeout.ts
|
|
4066
|
+
var REQUEST_TIMEOUT_MS = 6e4;
|
|
4067
|
+
var RequestTimeoutError = class extends Error {
|
|
4068
|
+
constructor(operation, timeoutMs, elapsedMs) {
|
|
4069
|
+
super(`${operation} timed out after ${elapsedMs}ms (limit ${timeoutMs}ms)`);
|
|
4070
|
+
this.operation = operation;
|
|
4071
|
+
this.timeoutMs = timeoutMs;
|
|
4072
|
+
this.elapsedMs = elapsedMs;
|
|
4073
|
+
this.name = "TimeoutError";
|
|
4074
|
+
}
|
|
4075
|
+
};
|
|
4076
|
+
function describeRequestOperation(input, init) {
|
|
4077
|
+
const method = (init?.method ?? input.method ?? "GET").toUpperCase();
|
|
4078
|
+
const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
4079
|
+
let path;
|
|
4080
|
+
try {
|
|
4081
|
+
path = new URL(rawUrl).pathname;
|
|
4082
|
+
} catch (error2) {
|
|
4083
|
+
if (!(error2 instanceof TypeError)) throw error2;
|
|
4084
|
+
path = rawUrl.split(/[?#]/, 1)[0];
|
|
4085
|
+
}
|
|
4086
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4087
|
+
const prefixedIdentifier = /^[a-z]+_[A-Za-z0-9_-]{10,}$/;
|
|
4088
|
+
const maskedPath = path.split("/").map((segment) => uuid.test(segment) || prefixedIdentifier.test(segment) ? ":id" : segment).join("/");
|
|
4089
|
+
return `${method} ${maskedPath}`;
|
|
4090
|
+
}
|
|
4091
|
+
function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
4092
|
+
return (async (input, init) => {
|
|
4093
|
+
const startedAt = Date.now();
|
|
4094
|
+
const controller = new AbortController();
|
|
4095
|
+
const abortFromCaller = () => controller.abort(init?.signal?.reason);
|
|
4096
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
4097
|
+
const abortFromTimeout = () => controller.abort(timeoutSignal.reason);
|
|
4098
|
+
if (init?.signal?.aborted) abortFromCaller();
|
|
4099
|
+
else init?.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
4100
|
+
if (timeoutSignal.aborted) abortFromTimeout();
|
|
4101
|
+
else timeoutSignal.addEventListener("abort", abortFromTimeout, { once: true });
|
|
4102
|
+
try {
|
|
4103
|
+
return await fetchImpl(input, { ...init, signal: controller.signal });
|
|
4104
|
+
} catch (error2) {
|
|
4105
|
+
if (timeoutSignal.aborted) {
|
|
4106
|
+
throw new RequestTimeoutError(
|
|
4107
|
+
describeRequestOperation(input, init),
|
|
4108
|
+
timeoutMs,
|
|
4109
|
+
Date.now() - startedAt
|
|
4110
|
+
);
|
|
4111
|
+
}
|
|
4112
|
+
throw error2;
|
|
4113
|
+
} finally {
|
|
4114
|
+
init?.signal?.removeEventListener("abort", abortFromCaller);
|
|
4115
|
+
timeoutSignal.removeEventListener("abort", abortFromTimeout);
|
|
4116
|
+
}
|
|
4117
|
+
});
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
// src/lib/opencode/client.ts
|
|
4121
|
+
var OpenCodeRequestError = class extends Error {
|
|
4122
|
+
constructor(statusCode, message) {
|
|
4123
|
+
super(message);
|
|
4124
|
+
this.statusCode = statusCode;
|
|
4125
|
+
this.name = "OpenCodeRequestError";
|
|
4126
|
+
}
|
|
4127
|
+
};
|
|
4128
|
+
function redactPassword(message, password) {
|
|
4129
|
+
return message.replaceAll(password, "[redacted]");
|
|
4130
|
+
}
|
|
4131
|
+
function createOpenCodeClient(options) {
|
|
2907
4132
|
const password = options.password ?? null;
|
|
2908
4133
|
const fetchImpl = withRequestTimeout(options.fetchImpl ?? fetch, REQUEST_TIMEOUT_MS);
|
|
2909
4134
|
const baseUrl = `http://127.0.0.1:${options.port}`;
|
|
@@ -2921,7 +4146,8 @@ function createOpenCodeClient(options) {
|
|
|
2921
4146
|
const response = await fetchImpl(`${baseUrl}${path}`, requestInit);
|
|
2922
4147
|
if (!response.ok && !requestOptions?.allowStatuses?.includes(response.status)) {
|
|
2923
4148
|
const body = await response.text();
|
|
2924
|
-
throw new
|
|
4149
|
+
throw new OpenCodeRequestError(
|
|
4150
|
+
response.status,
|
|
2925
4151
|
`OpenCode request failed: HTTP ${response.status}${body ? `: ${body}` : ""}`
|
|
2926
4152
|
);
|
|
2927
4153
|
}
|
|
@@ -2929,7 +4155,11 @@ function createOpenCodeClient(options) {
|
|
|
2929
4155
|
} catch (error2) {
|
|
2930
4156
|
if (options.version === "v2" && password !== null) {
|
|
2931
4157
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2932
|
-
|
|
4158
|
+
const redactedMessage = redactPassword(message, password);
|
|
4159
|
+
if (error2 instanceof OpenCodeRequestError) {
|
|
4160
|
+
throw new OpenCodeRequestError(error2.statusCode, redactedMessage);
|
|
4161
|
+
}
|
|
4162
|
+
throw new Error(redactedMessage);
|
|
2933
4163
|
}
|
|
2934
4164
|
throw error2;
|
|
2935
4165
|
}
|
|
@@ -3078,28 +4308,38 @@ function sessionListPath(options) {
|
|
|
3078
4308
|
async function listSessionPage(port, client, options) {
|
|
3079
4309
|
const path = sessionListPath(options);
|
|
3080
4310
|
try {
|
|
3081
|
-
const res = await requestWithClient(
|
|
3082
|
-
|
|
4311
|
+
const res = await requestWithClient(
|
|
4312
|
+
port,
|
|
4313
|
+
client,
|
|
4314
|
+
path,
|
|
4315
|
+
options?.signal ? { signal: options.signal } : void 0,
|
|
4316
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
4317
|
+
);
|
|
4318
|
+
if (!res.ok) {
|
|
4319
|
+
console.error(`[listSessionPage] GET ${path} returned HTTP ${res.status} (port ${port})`);
|
|
4320
|
+
return null;
|
|
4321
|
+
}
|
|
3083
4322
|
const body = await res.json();
|
|
3084
4323
|
return Array.isArray(body) ? body : null;
|
|
3085
4324
|
} catch (error2) {
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
);
|
|
4325
|
+
if (options?.signal?.aborted) return null;
|
|
4326
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
4327
|
+
console.error(`[listSessionPage] GET ${path} failed (port ${port}): ${errorName}`);
|
|
3089
4328
|
return null;
|
|
3090
4329
|
}
|
|
3091
4330
|
}
|
|
3092
4331
|
async function listSessions(port, client, options) {
|
|
3093
|
-
if (client?.version === "v2") return listV2Sessions(client);
|
|
4332
|
+
if (client?.version === "v2") return listV2Sessions(client, options?.signal);
|
|
3094
4333
|
return listSessionPage(port, client, options);
|
|
3095
4334
|
}
|
|
3096
|
-
async function listAllSessions(port, client) {
|
|
4335
|
+
async function listAllSessions(port, client, signal) {
|
|
3097
4336
|
if (client?.version === "v2") {
|
|
3098
|
-
const sessions2 = await listV2Sessions(client);
|
|
4337
|
+
const sessions2 = await listV2Sessions(client, signal);
|
|
3099
4338
|
return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
|
|
3100
4339
|
}
|
|
3101
4340
|
const sessions = await listSessionPage(port, client, {
|
|
3102
|
-
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
|
|
4341
|
+
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS,
|
|
4342
|
+
signal
|
|
3103
4343
|
});
|
|
3104
4344
|
if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
|
|
3105
4345
|
if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
|
|
@@ -3595,28 +4835,38 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
3595
4835
|
}
|
|
3596
4836
|
return last;
|
|
3597
4837
|
}
|
|
4838
|
+
function assistantMessagesInTurnBlock(messages, userMessageId) {
|
|
4839
|
+
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
4840
|
+
if (userIndex === -1) return [];
|
|
4841
|
+
const assistants = [];
|
|
4842
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
4843
|
+
const message = messages[i];
|
|
4844
|
+
const role = roleOf2(message);
|
|
4845
|
+
if (role === "user") break;
|
|
4846
|
+
if (role === "assistant") assistants.push(message);
|
|
4847
|
+
}
|
|
4848
|
+
return assistants;
|
|
4849
|
+
}
|
|
3598
4850
|
function collectSubagentSessions(messages, userMessageId) {
|
|
3599
4851
|
if (!messages || messages.length === 0) return [];
|
|
3600
4852
|
const byParent = messages.filter(
|
|
3601
4853
|
(message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
|
|
3602
4854
|
);
|
|
3603
|
-
const assistants = byParent.length > 0 ? byParent :
|
|
3604
|
-
if (assistants.length === 0) {
|
|
3605
|
-
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
3606
|
-
if (userIndex === -1) return [];
|
|
3607
|
-
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
3608
|
-
const message = messages[i];
|
|
3609
|
-
if (roleOf2(message) === "user") break;
|
|
3610
|
-
if (roleOf2(message) === "assistant") assistants.push(message);
|
|
3611
|
-
}
|
|
3612
|
-
}
|
|
4855
|
+
const assistants = byParent.length > 0 ? byParent : assistantMessagesInTurnBlock(messages, userMessageId);
|
|
3613
4856
|
const refs = [];
|
|
3614
4857
|
const seen = /* @__PURE__ */ new Set();
|
|
3615
4858
|
for (const message of assistants) {
|
|
3616
4859
|
const parts = Array.isArray(message.parts) ? message.parts : [];
|
|
3617
4860
|
for (const part of parts) {
|
|
3618
|
-
if (!part || typeof part !== "object" || part.type !== "tool"
|
|
4861
|
+
if (!part || typeof part !== "object" || part.type !== "tool") continue;
|
|
4862
|
+
const childSession = part.childSession;
|
|
4863
|
+
if (childSession) {
|
|
4864
|
+
if (seen.has(childSession.sessionId)) continue;
|
|
4865
|
+
seen.add(childSession.sessionId);
|
|
4866
|
+
refs.push(childSession);
|
|
3619
4867
|
continue;
|
|
4868
|
+
}
|
|
4869
|
+
if (part.tool !== "task") continue;
|
|
3620
4870
|
const state = part.state;
|
|
3621
4871
|
if (!state || typeof state !== "object") continue;
|
|
3622
4872
|
const metadata = state.metadata;
|
|
@@ -3764,8 +5014,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
3764
5014
|
if (byParentAll.length > 0) {
|
|
3765
5015
|
correlated = byParentAll;
|
|
3766
5016
|
} else {
|
|
3767
|
-
|
|
3768
|
-
correlated = reply ? [reply] : [];
|
|
5017
|
+
correlated = assistantMessagesInTurnBlock(messages, userMessageId);
|
|
3769
5018
|
}
|
|
3770
5019
|
if (correlated.length === 0) return null;
|
|
3771
5020
|
return sumAssistantUsage(correlated);
|
|
@@ -3900,8 +5149,8 @@ function classifyReplyAuthError(reply) {
|
|
|
3900
5149
|
};
|
|
3901
5150
|
}
|
|
3902
5151
|
}
|
|
3903
|
-
const
|
|
3904
|
-
if (
|
|
5152
|
+
const errorMessage4 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
|
|
5153
|
+
if (errorMessage4 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage4.includes(knownMessage))) {
|
|
3905
5154
|
return {
|
|
3906
5155
|
kind: "model_auth",
|
|
3907
5156
|
providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
|
|
@@ -3987,39 +5236,85 @@ async function hasAnyConfiguredProvider(port, client) {
|
|
|
3987
5236
|
);
|
|
3988
5237
|
return null;
|
|
3989
5238
|
}
|
|
3990
|
-
const
|
|
5239
|
+
const query = `?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
|
|
5240
|
+
const integrationPath = `/api/integration${query}`;
|
|
5241
|
+
let integrationReady = null;
|
|
3991
5242
|
try {
|
|
3992
|
-
const res = await client.request(
|
|
5243
|
+
const res = await client.request(integrationPath);
|
|
3993
5244
|
if (!res.ok) {
|
|
3994
5245
|
console.error(
|
|
3995
|
-
`[hasAnyConfiguredProvider] GET ${
|
|
5246
|
+
`[hasAnyConfiguredProvider] GET ${integrationPath} returned HTTP ${res.status} (port ${port})`
|
|
3996
5247
|
);
|
|
3997
|
-
|
|
5248
|
+
} else {
|
|
5249
|
+
const body = await res.json();
|
|
5250
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
|
|
5251
|
+
console.error(
|
|
5252
|
+
`[hasAnyConfiguredProvider] GET ${integrationPath} body had no integration data array (port ${port})`
|
|
5253
|
+
);
|
|
5254
|
+
} else {
|
|
5255
|
+
let valid = true;
|
|
5256
|
+
for (const integration of body.data) {
|
|
5257
|
+
if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
5258
|
+
console.error(
|
|
5259
|
+
`[hasAnyConfiguredProvider] GET ${integrationPath} body contained an invalid integration (port ${port})`
|
|
5260
|
+
);
|
|
5261
|
+
valid = false;
|
|
5262
|
+
break;
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5265
|
+
if (valid) {
|
|
5266
|
+
integrationReady = body.data.some(
|
|
5267
|
+
(integration) => integration.connections.length > 0
|
|
5268
|
+
);
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
3998
5271
|
}
|
|
3999
|
-
|
|
4000
|
-
|
|
5272
|
+
} catch (error2) {
|
|
5273
|
+
console.error(
|
|
5274
|
+
`[hasAnyConfiguredProvider] GET ${integrationPath} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
5275
|
+
);
|
|
5276
|
+
}
|
|
5277
|
+
if (integrationReady === true) return true;
|
|
5278
|
+
const providerPath = `/api/provider${query}`;
|
|
5279
|
+
let providerReady = null;
|
|
5280
|
+
try {
|
|
5281
|
+
const res = await client.request(providerPath);
|
|
5282
|
+
if (!res.ok) {
|
|
4001
5283
|
console.error(
|
|
4002
|
-
`[hasAnyConfiguredProvider] GET ${
|
|
5284
|
+
`[hasAnyConfiguredProvider] GET ${providerPath} returned HTTP ${res.status} (port ${port})`
|
|
4003
5285
|
);
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
5286
|
+
} else {
|
|
5287
|
+
const body = await res.json();
|
|
5288
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
|
|
4008
5289
|
console.error(
|
|
4009
|
-
`[hasAnyConfiguredProvider] GET ${
|
|
5290
|
+
`[hasAnyConfiguredProvider] GET ${providerPath} body had no provider data array (port ${port})`
|
|
4010
5291
|
);
|
|
4011
|
-
|
|
5292
|
+
} else {
|
|
5293
|
+
let valid = true;
|
|
5294
|
+
for (const provider of body.data) {
|
|
5295
|
+
if (!provider || typeof provider !== "object" || Array.isArray(provider) || typeof provider.id !== "string") {
|
|
5296
|
+
console.error(
|
|
5297
|
+
`[hasAnyConfiguredProvider] GET ${providerPath} body contained an invalid provider (port ${port})`
|
|
5298
|
+
);
|
|
5299
|
+
valid = false;
|
|
5300
|
+
break;
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
if (valid) {
|
|
5304
|
+
providerReady = body.data.some(
|
|
5305
|
+
(provider) => provider.activation === "enabled"
|
|
5306
|
+
);
|
|
5307
|
+
}
|
|
4012
5308
|
}
|
|
4013
5309
|
}
|
|
4014
|
-
return body.data.some(
|
|
4015
|
-
(integration) => Array.isArray(integration.connections) && integration.connections.length > 0
|
|
4016
|
-
);
|
|
4017
5310
|
} catch (error2) {
|
|
4018
5311
|
console.error(
|
|
4019
|
-
`[hasAnyConfiguredProvider] GET ${
|
|
5312
|
+
`[hasAnyConfiguredProvider] GET ${providerPath} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4020
5313
|
);
|
|
4021
|
-
return null;
|
|
4022
5314
|
}
|
|
5315
|
+
if (providerReady === true) return true;
|
|
5316
|
+
if (integrationReady === null || providerReady === null) return null;
|
|
5317
|
+
return false;
|
|
4023
5318
|
}
|
|
4024
5319
|
try {
|
|
4025
5320
|
const res = await requestWithClient(port, client, "/config/providers");
|
|
@@ -4172,14 +5467,15 @@ async function probeOpenCodeRequestReadiness(port, client) {
|
|
|
4172
5467
|
}
|
|
4173
5468
|
|
|
4174
5469
|
// src/lib/opencode/session-v2.ts
|
|
4175
|
-
|
|
5470
|
+
var ALL_HTTP_STATUSES2 = Array.from({ length: 500 }, (_, index) => index + 100);
|
|
5471
|
+
function isRecord5(value) {
|
|
4176
5472
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4177
5473
|
}
|
|
4178
5474
|
function finiteNumber2(value) {
|
|
4179
5475
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
4180
5476
|
}
|
|
4181
5477
|
function adaptTime(value) {
|
|
4182
|
-
if (!
|
|
5478
|
+
if (!isRecord5(value)) return void 0;
|
|
4183
5479
|
const created = finiteNumber2(value.created);
|
|
4184
5480
|
const completed = finiteNumber2(value.completed);
|
|
4185
5481
|
if (created === void 0 && completed === void 0) return void 0;
|
|
@@ -4189,11 +5485,11 @@ function adaptTime(value) {
|
|
|
4189
5485
|
};
|
|
4190
5486
|
}
|
|
4191
5487
|
function adaptTokens(value) {
|
|
4192
|
-
if (!
|
|
5488
|
+
if (!isRecord5(value)) return void 0;
|
|
4193
5489
|
const input = finiteNumber2(value.input);
|
|
4194
5490
|
const output = finiteNumber2(value.output);
|
|
4195
5491
|
const reasoning = finiteNumber2(value.reasoning);
|
|
4196
|
-
const cache =
|
|
5492
|
+
const cache = isRecord5(value.cache) ? {
|
|
4197
5493
|
...finiteNumber2(value.cache.read) !== void 0 ? { read: finiteNumber2(value.cache.read) } : {},
|
|
4198
5494
|
...finiteNumber2(value.cache.write) !== void 0 ? { write: finiteNumber2(value.cache.write) } : {}
|
|
4199
5495
|
} : void 0;
|
|
@@ -4217,7 +5513,7 @@ function adaptMessageInfo(value, role) {
|
|
|
4217
5513
|
if (typeof value.finish === "string") info.finish = value.finish;
|
|
4218
5514
|
if ("error" in value) info.error = value.error;
|
|
4219
5515
|
if (typeof value.agent === "string") info.agent = value.agent;
|
|
4220
|
-
if (
|
|
5516
|
+
if (isRecord5(value.model)) {
|
|
4221
5517
|
if (typeof value.model.id === "string") info.modelID = value.model.id;
|
|
4222
5518
|
if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
|
|
4223
5519
|
}
|
|
@@ -4226,8 +5522,16 @@ function adaptMessageInfo(value, role) {
|
|
|
4226
5522
|
if (tokens) info.tokens = tokens;
|
|
4227
5523
|
return info;
|
|
4228
5524
|
}
|
|
5525
|
+
function v2ChildSessionEvidence(content) {
|
|
5526
|
+
const state = isRecord5(content.state) ? content.state : null;
|
|
5527
|
+
const metadata = state && isRecord5(state.metadata) ? state.metadata : null;
|
|
5528
|
+
const sessionId = typeof metadata?.sessionID === "string" && metadata.sessionID.length > 0 ? metadata.sessionID : typeof metadata?.sessionId === "string" && metadata.sessionId.length > 0 ? metadata.sessionId : null;
|
|
5529
|
+
if (sessionId === null) return null;
|
|
5530
|
+
const time = isRecord5(content.time) ? content.time : null;
|
|
5531
|
+
return { sessionId, startedAtMs: finiteNumber2(time?.created) ?? null };
|
|
5532
|
+
}
|
|
4229
5533
|
function adaptV2Message(value) {
|
|
4230
|
-
if (!
|
|
5534
|
+
if (!isRecord5(value) || typeof value.id !== "string" || typeof value.type !== "string") {
|
|
4231
5535
|
return null;
|
|
4232
5536
|
}
|
|
4233
5537
|
if (value.type === "user") {
|
|
@@ -4240,7 +5544,13 @@ function adaptV2Message(value) {
|
|
|
4240
5544
|
if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
|
|
4241
5545
|
const parts = [];
|
|
4242
5546
|
for (const content of value.content) {
|
|
4243
|
-
if (!
|
|
5547
|
+
if (!isRecord5(content) || typeof content.type !== "string") return null;
|
|
5548
|
+
const toolPart = projectV2ToolContent(content);
|
|
5549
|
+
if (toolPart) {
|
|
5550
|
+
const childSession = v2ChildSessionEvidence(content);
|
|
5551
|
+
parts.push(childSession ? { ...toolPart, childSession } : toolPart);
|
|
5552
|
+
continue;
|
|
5553
|
+
}
|
|
4244
5554
|
if (content.type === "text") {
|
|
4245
5555
|
if (typeof content.text !== "string") return null;
|
|
4246
5556
|
parts.push({ type: "text", text: content.text });
|
|
@@ -4254,23 +5564,23 @@ function adaptV2Message(value) {
|
|
|
4254
5564
|
};
|
|
4255
5565
|
}
|
|
4256
5566
|
function adaptFormTool(value) {
|
|
4257
|
-
if (!
|
|
5567
|
+
if (!isRecord5(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
|
|
4258
5568
|
return void 0;
|
|
4259
5569
|
}
|
|
4260
5570
|
return { messageID: value.messageID, callID: value.id };
|
|
4261
5571
|
}
|
|
4262
5572
|
function adaptV2FormWire(value) {
|
|
4263
|
-
if (!
|
|
5573
|
+
if (!isRecord5(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
4264
5574
|
return null;
|
|
4265
5575
|
}
|
|
4266
5576
|
return value;
|
|
4267
5577
|
}
|
|
4268
5578
|
function adaptV2FormField(value, header) {
|
|
4269
|
-
if (!
|
|
5579
|
+
if (!isRecord5(value)) return null;
|
|
4270
5580
|
const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
|
|
4271
5581
|
if (!question) return null;
|
|
4272
5582
|
const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
|
|
4273
|
-
if (!
|
|
5583
|
+
if (!isRecord5(option)) return [];
|
|
4274
5584
|
const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
|
|
4275
5585
|
if (!label) return [];
|
|
4276
5586
|
return [
|
|
@@ -4288,7 +5598,7 @@ function adaptV2Form(value) {
|
|
|
4288
5598
|
const header = typeof form.title === "string" ? form.title : "";
|
|
4289
5599
|
const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
|
|
4290
5600
|
if (questions.length === 0) return null;
|
|
4291
|
-
const tool =
|
|
5601
|
+
const tool = isRecord5(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
|
|
4292
5602
|
return {
|
|
4293
5603
|
id: form.id,
|
|
4294
5604
|
sessionID: form.sessionID,
|
|
@@ -4298,7 +5608,7 @@ function adaptV2Form(value) {
|
|
|
4298
5608
|
};
|
|
4299
5609
|
}
|
|
4300
5610
|
function adaptV2FormList(value) {
|
|
4301
|
-
if (!
|
|
5611
|
+
if (!isRecord5(value) || !Array.isArray(value.data)) return null;
|
|
4302
5612
|
return value.data.map(adaptV2Form).filter((form) => form !== null);
|
|
4303
5613
|
}
|
|
4304
5614
|
function adaptPattern(value) {
|
|
@@ -4309,7 +5619,7 @@ function adaptPattern(value) {
|
|
|
4309
5619
|
return void 0;
|
|
4310
5620
|
}
|
|
4311
5621
|
function adaptV2PermissionWire(value) {
|
|
4312
|
-
if (!
|
|
5622
|
+
if (!isRecord5(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
4313
5623
|
return null;
|
|
4314
5624
|
}
|
|
4315
5625
|
if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
|
|
@@ -4321,12 +5631,12 @@ function adaptV2Permission(value) {
|
|
|
4321
5631
|
const type = permission.permission ?? permission.action;
|
|
4322
5632
|
if (!type) return null;
|
|
4323
5633
|
const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
|
|
4324
|
-
const time =
|
|
5634
|
+
const time = isRecord5(permission.time) ? finiteNumber2(permission.time.created) !== void 0 ? { created: finiteNumber2(permission.time.created) } : void 0 : void 0;
|
|
4325
5635
|
return {
|
|
4326
5636
|
id: permission.id,
|
|
4327
5637
|
type,
|
|
4328
5638
|
sessionID: permission.sessionID,
|
|
4329
|
-
metadata:
|
|
5639
|
+
metadata: isRecord5(permission.metadata) ? permission.metadata : {},
|
|
4330
5640
|
raw: permission,
|
|
4331
5641
|
...pattern !== void 0 ? { pattern } : {},
|
|
4332
5642
|
...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
|
|
@@ -4336,12 +5646,12 @@ function adaptV2Permission(value) {
|
|
|
4336
5646
|
};
|
|
4337
5647
|
}
|
|
4338
5648
|
function adaptV2PermissionList(value) {
|
|
4339
|
-
if (!
|
|
5649
|
+
if (!isRecord5(value) || !Array.isArray(value.data)) return null;
|
|
4340
5650
|
return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
|
|
4341
5651
|
}
|
|
4342
5652
|
function adaptV2Session(value) {
|
|
4343
|
-
if (!
|
|
4344
|
-
const time =
|
|
5653
|
+
if (!isRecord5(value) || typeof value.id !== "string" || value.id.length === 0) return null;
|
|
5654
|
+
const time = isRecord5(value.time) ? {
|
|
4345
5655
|
...finiteNumber2(value.time.created) !== void 0 ? { created: finiteNumber2(value.time.created) } : {},
|
|
4346
5656
|
...finiteNumber2(value.time.updated) !== void 0 ? { updated: finiteNumber2(value.time.updated) } : {}
|
|
4347
5657
|
} : void 0;
|
|
@@ -4353,7 +5663,7 @@ function adaptV2Session(value) {
|
|
|
4353
5663
|
};
|
|
4354
5664
|
}
|
|
4355
5665
|
function adaptV2SessionList(value) {
|
|
4356
|
-
if (!
|
|
5666
|
+
if (!isRecord5(value) || !Array.isArray(value.data) || !isRecord5(value.cursor)) return null;
|
|
4357
5667
|
return {
|
|
4358
5668
|
data: value.data.map(adaptV2Session).filter((session) => session !== null),
|
|
4359
5669
|
cursor: value.cursor
|
|
@@ -4362,18 +5672,18 @@ function adaptV2SessionList(value) {
|
|
|
4362
5672
|
function adaptV2Location(value) {
|
|
4363
5673
|
const candidates = [
|
|
4364
5674
|
value,
|
|
4365
|
-
|
|
4366
|
-
|
|
5675
|
+
isRecord5(value) ? value.data : void 0,
|
|
5676
|
+
isRecord5(value) ? value.location : void 0
|
|
4367
5677
|
];
|
|
4368
5678
|
for (const candidate of candidates) {
|
|
4369
|
-
if (!
|
|
5679
|
+
if (!isRecord5(candidate) || typeof candidate.directory !== "string") continue;
|
|
4370
5680
|
const directory = candidate.directory.trim();
|
|
4371
5681
|
if (directory) return directory;
|
|
4372
5682
|
}
|
|
4373
5683
|
return null;
|
|
4374
5684
|
}
|
|
4375
5685
|
function adaptV2Messages(value) {
|
|
4376
|
-
if (!
|
|
5686
|
+
if (!isRecord5(value) || !Array.isArray(value.data)) return [];
|
|
4377
5687
|
return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
|
|
4378
5688
|
}
|
|
4379
5689
|
async function readJson(response) {
|
|
@@ -4388,7 +5698,7 @@ async function readJson(response) {
|
|
|
4388
5698
|
async function readData(client, path, init) {
|
|
4389
5699
|
const response = await client.request(path, init);
|
|
4390
5700
|
const body = await readJson(response);
|
|
4391
|
-
if (!
|
|
5701
|
+
if (!isRecord5(body) || !("data" in body)) {
|
|
4392
5702
|
throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
|
|
4393
5703
|
}
|
|
4394
5704
|
return body.data;
|
|
@@ -4399,6 +5709,14 @@ var OpenCodeV2PromptAckError = class extends Error {
|
|
|
4399
5709
|
this.name = "OpenCodeV2PromptAckError";
|
|
4400
5710
|
}
|
|
4401
5711
|
};
|
|
5712
|
+
var OpenCodeV2ModelSwitchError = class extends Error {
|
|
5713
|
+
statusCode;
|
|
5714
|
+
constructor(error2) {
|
|
5715
|
+
super(error2 instanceof Error ? error2.message : String(error2));
|
|
5716
|
+
this.name = "OpenCodeV2ModelSwitchError";
|
|
5717
|
+
this.statusCode = error2 instanceof OpenCodeRequestError ? error2.statusCode : null;
|
|
5718
|
+
}
|
|
5719
|
+
};
|
|
4402
5720
|
async function getOpenCodeDirectoryV2(client) {
|
|
4403
5721
|
try {
|
|
4404
5722
|
return adaptV2Location(await readJson(await client.request("/api/location")));
|
|
@@ -4415,7 +5733,7 @@ async function createV2Session(client, directory) {
|
|
|
4415
5733
|
headers: { "Content-Type": "application/json" },
|
|
4416
5734
|
body: JSON.stringify({ location: { directory } })
|
|
4417
5735
|
});
|
|
4418
|
-
if (!
|
|
5736
|
+
if (!isRecord5(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
4419
5737
|
throw new Error("OpenCode V2 create session response was missing data.id");
|
|
4420
5738
|
}
|
|
4421
5739
|
return data.id;
|
|
@@ -4426,25 +5744,35 @@ async function getV2Session(client, sessionId) {
|
|
|
4426
5744
|
if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
|
|
4427
5745
|
return session;
|
|
4428
5746
|
}
|
|
4429
|
-
async function listV2SessionPage(client, cursor) {
|
|
5747
|
+
async function listV2SessionPage(client, cursor, signal) {
|
|
5748
|
+
const operation = cursor ? "GET /api/session (next page)" : "GET /api/session";
|
|
4430
5749
|
const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
|
|
4431
5750
|
try {
|
|
4432
|
-
|
|
5751
|
+
const response = await client.request(path, signal ? { signal } : void 0, {
|
|
5752
|
+
allowStatuses: ALL_HTTP_STATUSES2
|
|
5753
|
+
});
|
|
5754
|
+
if (!response.ok) {
|
|
5755
|
+
console.error(
|
|
5756
|
+
`[listV2SessionPage] ${operation} returned HTTP ${response.status} (port ${client.port})`
|
|
5757
|
+
);
|
|
5758
|
+
return null;
|
|
5759
|
+
}
|
|
5760
|
+
return adaptV2SessionList(await readJson(response));
|
|
4433
5761
|
} catch (error2) {
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
);
|
|
5762
|
+
if (signal?.aborted) return null;
|
|
5763
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
5764
|
+
console.error(`[listV2SessionPage] ${operation} failed (port ${client.port}): ${errorName}`);
|
|
4437
5765
|
return null;
|
|
4438
5766
|
}
|
|
4439
5767
|
}
|
|
4440
|
-
async function listV2Sessions(client) {
|
|
5768
|
+
async function listV2Sessions(client, signal) {
|
|
4441
5769
|
const sessions = [];
|
|
4442
5770
|
const seenCursors = /* @__PURE__ */ new Set();
|
|
4443
5771
|
let cursor;
|
|
4444
5772
|
let hasNextPage = true;
|
|
4445
5773
|
try {
|
|
4446
5774
|
while (hasNextPage) {
|
|
4447
|
-
const page = await listV2SessionPage(client, cursor);
|
|
5775
|
+
const page = await listV2SessionPage(client, cursor, signal);
|
|
4448
5776
|
if (!page) return null;
|
|
4449
5777
|
sessions.push(...page.data);
|
|
4450
5778
|
const next = page.cursor.next;
|
|
@@ -4460,8 +5788,9 @@ async function listV2Sessions(client) {
|
|
|
4460
5788
|
}
|
|
4461
5789
|
return sessions;
|
|
4462
5790
|
} catch (error2) {
|
|
5791
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
4463
5792
|
console.error(
|
|
4464
|
-
`[listV2Sessions] session pagination failed
|
|
5793
|
+
`[listV2Sessions] GET /api/session pagination failed (port ${client.port}): ${errorName}`
|
|
4465
5794
|
);
|
|
4466
5795
|
return null;
|
|
4467
5796
|
}
|
|
@@ -4471,7 +5800,7 @@ async function deleteV2Session(client, sessionId) {
|
|
|
4471
5800
|
const response = await client.request(
|
|
4472
5801
|
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
4473
5802
|
{ method: "DELETE" },
|
|
4474
|
-
{ allowStatuses:
|
|
5803
|
+
{ allowStatuses: ALL_HTTP_STATUSES2 }
|
|
4475
5804
|
);
|
|
4476
5805
|
if (response.status >= 200 && response.status < 300) {
|
|
4477
5806
|
return { ok: true, status: response.status };
|
|
@@ -4507,7 +5836,34 @@ async function v2SessionExists(client, sessionId) {
|
|
|
4507
5836
|
return null;
|
|
4508
5837
|
}
|
|
4509
5838
|
}
|
|
4510
|
-
async function sendV2Prompt(client, sessionId, text) {
|
|
5839
|
+
async function sendV2Prompt(client, sessionId, text, options, onInvalidModelSelection) {
|
|
5840
|
+
const modelSelection = options?.model?.trim();
|
|
5841
|
+
const { model, variant } = splitModelVariant(modelSelection);
|
|
5842
|
+
const separator = model?.indexOf("/") ?? -1;
|
|
5843
|
+
const modelReference = model && separator > 0 && separator < model.length - 1 ? {
|
|
5844
|
+
id: model.slice(separator + 1),
|
|
5845
|
+
providerID: model.slice(0, separator),
|
|
5846
|
+
...variant ? { variant } : {}
|
|
5847
|
+
} : void 0;
|
|
5848
|
+
if (modelSelection && !modelReference) onInvalidModelSelection?.();
|
|
5849
|
+
if (options?.agent) {
|
|
5850
|
+
await client.request(`/api/session/${encodeURIComponent(sessionId)}/agent`, {
|
|
5851
|
+
method: "POST",
|
|
5852
|
+
headers: { "Content-Type": "application/json" },
|
|
5853
|
+
body: JSON.stringify({ agent: options.agent })
|
|
5854
|
+
});
|
|
5855
|
+
}
|
|
5856
|
+
if (modelReference) {
|
|
5857
|
+
try {
|
|
5858
|
+
await client.request(`/api/session/${encodeURIComponent(sessionId)}/model`, {
|
|
5859
|
+
method: "POST",
|
|
5860
|
+
headers: { "Content-Type": "application/json" },
|
|
5861
|
+
body: JSON.stringify({ model: modelReference })
|
|
5862
|
+
});
|
|
5863
|
+
} catch (error2) {
|
|
5864
|
+
throw new OpenCodeV2ModelSwitchError(error2);
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
4511
5867
|
const path = `/api/session/${encodeURIComponent(sessionId)}/prompt`;
|
|
4512
5868
|
const response = await client.request(path, {
|
|
4513
5869
|
method: "POST",
|
|
@@ -4522,8 +5878,8 @@ async function sendV2Prompt(client, sessionId, text) {
|
|
|
4522
5878
|
`OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4523
5879
|
);
|
|
4524
5880
|
}
|
|
4525
|
-
const data =
|
|
4526
|
-
if (!
|
|
5881
|
+
const data = isRecord5(body) && "data" in body ? body.data : void 0;
|
|
5882
|
+
if (!isRecord5(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
4527
5883
|
throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
|
|
4528
5884
|
}
|
|
4529
5885
|
return data.id;
|
|
@@ -4532,7 +5888,7 @@ async function getV2SessionMessages(client, sessionId) {
|
|
|
4532
5888
|
const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
|
|
4533
5889
|
try {
|
|
4534
5890
|
const body = await readJson(await client.request(path));
|
|
4535
|
-
if (!
|
|
5891
|
+
if (!isRecord5(body) || !Array.isArray(body.data) || !isRecord5(body.cursor)) return null;
|
|
4536
5892
|
return adaptV2Messages(body);
|
|
4537
5893
|
} catch (error2) {
|
|
4538
5894
|
console.error(
|
|
@@ -4571,7 +5927,7 @@ async function interruptV2Session(client, sessionId) {
|
|
|
4571
5927
|
async function getV2ActiveSessions(client) {
|
|
4572
5928
|
try {
|
|
4573
5929
|
const body = await readJson(await client.request("/api/session/active"));
|
|
4574
|
-
if (!
|
|
5930
|
+
if (!isRecord5(body) || !isRecord5(body.data)) return null;
|
|
4575
5931
|
return body.data;
|
|
4576
5932
|
} catch (error2) {
|
|
4577
5933
|
console.error(
|
|
@@ -4587,8 +5943,8 @@ async function isV2SessionOngoing(client, sessionId) {
|
|
|
4587
5943
|
}
|
|
4588
5944
|
function sessionErrorReason2(value) {
|
|
4589
5945
|
if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
|
|
4590
|
-
if (
|
|
4591
|
-
const data =
|
|
5946
|
+
if (isRecord5(value)) {
|
|
5947
|
+
const data = isRecord5(value.data) ? value.data : void 0;
|
|
4592
5948
|
const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
|
|
4593
5949
|
if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
4594
5950
|
}
|
|
@@ -4604,17 +5960,17 @@ function adaptV2SessionErrorEvent(value) {
|
|
|
4604
5960
|
return null;
|
|
4605
5961
|
}
|
|
4606
5962
|
}
|
|
4607
|
-
if (!
|
|
5963
|
+
if (!isRecord5(parsed)) return null;
|
|
4608
5964
|
try {
|
|
4609
5965
|
const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
|
|
4610
5966
|
if (establishedShape) return establishedShape;
|
|
4611
5967
|
} catch (error2) {
|
|
4612
5968
|
void error2;
|
|
4613
5969
|
}
|
|
4614
|
-
const candidates = [parsed, parsed.payload, parsed.data].filter(
|
|
5970
|
+
const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord5);
|
|
4615
5971
|
for (const event of candidates) {
|
|
4616
5972
|
if (event.type !== "session.error") continue;
|
|
4617
|
-
const properties = [event.properties, event.data, event].find(
|
|
5973
|
+
const properties = [event.properties, event.data, event].find(isRecord5);
|
|
4618
5974
|
if (!properties) continue;
|
|
4619
5975
|
const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
|
|
4620
5976
|
if (!sessionId) continue;
|
|
@@ -4664,13 +6020,13 @@ async function readV2SessionErrorStream(client, options) {
|
|
|
4664
6020
|
}
|
|
4665
6021
|
|
|
4666
6022
|
// src/lib/opencode/credential-import-v2.ts
|
|
4667
|
-
import { readFileSync as
|
|
4668
|
-
import { join as
|
|
6023
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
6024
|
+
import { join as join8 } from "node:path";
|
|
4669
6025
|
|
|
4670
6026
|
// src/lib/openai-usage.ts
|
|
4671
|
-
import { readFileSync as
|
|
6027
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4672
6028
|
import { homedir as homedir3 } from "node:os";
|
|
4673
|
-
import { join as
|
|
6029
|
+
import { join as join7 } from "node:path";
|
|
4674
6030
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
4675
6031
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
4676
6032
|
var OpenAiUsageError = class extends Error {
|
|
@@ -4684,7 +6040,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
4684
6040
|
}
|
|
4685
6041
|
function readOpenCodeChatGptCredentials() {
|
|
4686
6042
|
try {
|
|
4687
|
-
const raw =
|
|
6043
|
+
const raw = readFileSync7(join7(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
4688
6044
|
let parsed;
|
|
4689
6045
|
try {
|
|
4690
6046
|
parsed = JSON.parse(raw);
|
|
@@ -4750,7 +6106,7 @@ function parseCodexUsageHeaders(headers) {
|
|
|
4750
6106
|
function normalizeProbeModel(model) {
|
|
4751
6107
|
return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
|
|
4752
6108
|
}
|
|
4753
|
-
function
|
|
6109
|
+
function isRecord6(value) {
|
|
4754
6110
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4755
6111
|
}
|
|
4756
6112
|
function unsupportedProbeModels(reason, port) {
|
|
@@ -4761,14 +6117,14 @@ async function resolveV1ProbeModels(client, port) {
|
|
|
4761
6117
|
try {
|
|
4762
6118
|
const response = await client.request("/config/providers");
|
|
4763
6119
|
const body = await response.json();
|
|
4764
|
-
if (!
|
|
6120
|
+
if (!isRecord6(body) || !Array.isArray(body.providers)) {
|
|
4765
6121
|
return unsupportedProbeModels("V1 provider response did not contain a providers array", port);
|
|
4766
6122
|
}
|
|
4767
6123
|
const provider = body.providers.find(
|
|
4768
|
-
(candidate) =>
|
|
6124
|
+
(candidate) => isRecord6(candidate) && candidate.id === "openai"
|
|
4769
6125
|
);
|
|
4770
|
-
if (!provider || !
|
|
4771
|
-
const defaults2 =
|
|
6126
|
+
if (!provider || !isRecord6(provider.models)) return { status: "supported", models: [] };
|
|
6127
|
+
const defaults2 = isRecord6(body.default) ? body.default : void 0;
|
|
4772
6128
|
const candidates = [
|
|
4773
6129
|
...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
|
|
4774
6130
|
...Object.keys(provider.models)
|
|
@@ -4790,14 +6146,14 @@ async function resolveV2ProbeModels(client, port) {
|
|
|
4790
6146
|
try {
|
|
4791
6147
|
const response = await client.request(path);
|
|
4792
6148
|
const body = await response.json();
|
|
4793
|
-
if (!
|
|
6149
|
+
if (!isRecord6(body) || !Array.isArray(body.data)) {
|
|
4794
6150
|
return unsupportedProbeModels(`V2 GET ${path} did not contain a provider data array`, port);
|
|
4795
6151
|
}
|
|
4796
6152
|
const provider = body.data.find(
|
|
4797
|
-
(candidate) =>
|
|
6153
|
+
(candidate) => isRecord6(candidate) && candidate.id === "openai"
|
|
4798
6154
|
);
|
|
4799
6155
|
if (!provider) return { status: "supported", models: [] };
|
|
4800
|
-
if (!
|
|
6156
|
+
if (!isRecord6(provider.models)) {
|
|
4801
6157
|
return unsupportedProbeModels(
|
|
4802
6158
|
"V2 provider response has no safe OpenAI model catalogue",
|
|
4803
6159
|
port
|
|
@@ -4907,7 +6263,7 @@ var CONNECT_KEY_MAX_ATTEMPTS = 3;
|
|
|
4907
6263
|
var CONNECT_KEY_RETRY_DELAY_MS = 250;
|
|
4908
6264
|
var INTEGRATION_LIST_MAX_READS = 10;
|
|
4909
6265
|
var INTEGRATION_LIST_RETRY_DELAY_MS = 1e3;
|
|
4910
|
-
function
|
|
6266
|
+
function isRecord7(value) {
|
|
4911
6267
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4912
6268
|
}
|
|
4913
6269
|
var sleep2 = (milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds));
|
|
@@ -4916,14 +6272,14 @@ function emptyResult(unsupported = [], outcome = "completed") {
|
|
|
4916
6272
|
}
|
|
4917
6273
|
function readOpenCodeCredentials(homeDir, log3) {
|
|
4918
6274
|
try {
|
|
4919
|
-
const raw =
|
|
6275
|
+
const raw = readFileSync8(join8(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
|
|
4920
6276
|
let parsed;
|
|
4921
6277
|
try {
|
|
4922
6278
|
parsed = JSON.parse(raw);
|
|
4923
6279
|
} catch (error2) {
|
|
4924
6280
|
return null;
|
|
4925
6281
|
}
|
|
4926
|
-
return
|
|
6282
|
+
return isRecord7(parsed) ? parsed : null;
|
|
4927
6283
|
} catch (error2) {
|
|
4928
6284
|
const code = error2.code;
|
|
4929
6285
|
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
@@ -4936,7 +6292,7 @@ function classifyOpenCodeCredentials(credentials2) {
|
|
|
4936
6292
|
const importable = [];
|
|
4937
6293
|
const unsupported = [];
|
|
4938
6294
|
for (const [integrationId, value] of Object.entries(credentials2)) {
|
|
4939
|
-
if (!
|
|
6295
|
+
if (!isRecord7(value) || typeof value.type !== "string") {
|
|
4940
6296
|
unsupported.push({ integrationId, reason: "unrecognised credential entry" });
|
|
4941
6297
|
continue;
|
|
4942
6298
|
}
|
|
@@ -4951,7 +6307,7 @@ function classifyOpenCodeCredentials(credentials2) {
|
|
|
4951
6307
|
unsupported.push({ integrationId, reason: "unrecognised credential entry" });
|
|
4952
6308
|
continue;
|
|
4953
6309
|
}
|
|
4954
|
-
if (value.metadata === void 0 ||
|
|
6310
|
+
if (value.metadata === void 0 || isRecord7(value.metadata) && Object.keys(value.metadata).length === 0) {
|
|
4955
6311
|
importable.push({ integrationId, key: value.key });
|
|
4956
6312
|
} else {
|
|
4957
6313
|
unsupported.push({ integrationId, reason: "needs the interactive connect flow" });
|
|
@@ -4964,7 +6320,7 @@ function statusFromRequestError(error2) {
|
|
|
4964
6320
|
const match = /^OpenCode request failed: HTTP (\d{3})/.exec(error2.message);
|
|
4965
6321
|
return match ? Number(match[1]) : void 0;
|
|
4966
6322
|
}
|
|
4967
|
-
function
|
|
6323
|
+
function failureMessage2(status2) {
|
|
4968
6324
|
return status2 === void 0 ? "OpenCode credential import request failed" : `OpenCode credential import request failed with HTTP ${status2}`;
|
|
4969
6325
|
}
|
|
4970
6326
|
function isRetryableStatus(status2) {
|
|
@@ -4991,18 +6347,18 @@ async function importOneCredential(request3, credential, sleepForRetry, log3) {
|
|
|
4991
6347
|
const failure = {
|
|
4992
6348
|
integrationId: credential.integrationId,
|
|
4993
6349
|
status: response.status,
|
|
4994
|
-
message:
|
|
6350
|
+
message: failureMessage2(response.status)
|
|
4995
6351
|
};
|
|
4996
|
-
log3(`${
|
|
6352
|
+
log3(`${failureMessage2(failure.status)} for ${failure.integrationId}`, "warn");
|
|
4997
6353
|
return { ok: false, failure };
|
|
4998
6354
|
} catch (error2) {
|
|
4999
6355
|
const status2 = statusFromRequestError(error2);
|
|
5000
6356
|
const failure = {
|
|
5001
6357
|
integrationId: credential.integrationId,
|
|
5002
6358
|
...status2 === void 0 ? {} : { status: status2 },
|
|
5003
|
-
message:
|
|
6359
|
+
message: failureMessage2(status2)
|
|
5004
6360
|
};
|
|
5005
|
-
log3(`${
|
|
6361
|
+
log3(`${failureMessage2(status2)} for ${failure.integrationId}`, "warn");
|
|
5006
6362
|
return { ok: false, failure };
|
|
5007
6363
|
}
|
|
5008
6364
|
}
|
|
@@ -5028,10 +6384,10 @@ async function importOpenCodeCredentials({
|
|
|
5028
6384
|
return result;
|
|
5029
6385
|
}
|
|
5030
6386
|
function parseIntegrationList(value) {
|
|
5031
|
-
if (!
|
|
6387
|
+
if (!isRecord7(value) || !Array.isArray(value.data)) return null;
|
|
5032
6388
|
const integrationIds = /* @__PURE__ */ new Set();
|
|
5033
6389
|
for (const integration of value.data) {
|
|
5034
|
-
if (!
|
|
6390
|
+
if (!isRecord7(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
5035
6391
|
return null;
|
|
5036
6392
|
}
|
|
5037
6393
|
if (integration.connections.length > 0) integrationIds.add(integration.id);
|
|
@@ -5116,87 +6472,9 @@ async function importOpenCodeV2Credentials({
|
|
|
5116
6472
|
|
|
5117
6473
|
// src/lib/opencode/session-db-boot.ts
|
|
5118
6474
|
import { spawn as spawn2 } from "node:child_process";
|
|
5119
|
-
import { mkdirSync as
|
|
6475
|
+
import { mkdirSync as mkdirSync4, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5120
6476
|
import { homedir as homedir4 } from "node:os";
|
|
5121
|
-
import { dirname as
|
|
5122
|
-
|
|
5123
|
-
// src/lib/runner-synchroniser.ts
|
|
5124
|
-
import { spawn } from "node:child_process";
|
|
5125
|
-
function appendError(stderr, error2) {
|
|
5126
|
-
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
5127
|
-
return stderr === "" ? message : `${stderr}
|
|
5128
|
-
${message}`;
|
|
5129
|
-
}
|
|
5130
|
-
function runSynchroniser(args, opts) {
|
|
5131
|
-
return new Promise((resolve5) => {
|
|
5132
|
-
let child;
|
|
5133
|
-
let stdout = "";
|
|
5134
|
-
let stderr = "";
|
|
5135
|
-
let settled = false;
|
|
5136
|
-
const timer = {};
|
|
5137
|
-
let abortListener;
|
|
5138
|
-
let spawnListener;
|
|
5139
|
-
const finish = (result) => {
|
|
5140
|
-
if (settled) return;
|
|
5141
|
-
settled = true;
|
|
5142
|
-
if (timer.handle) clearTimeout(timer.handle);
|
|
5143
|
-
if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
|
|
5144
|
-
if (spawnListener) child.removeListener("spawn", spawnListener);
|
|
5145
|
-
resolve5(result);
|
|
5146
|
-
};
|
|
5147
|
-
try {
|
|
5148
|
-
child = spawn("runner-synchroniser", args, {
|
|
5149
|
-
env: opts.env ?? process.env,
|
|
5150
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5151
|
-
});
|
|
5152
|
-
} catch (error2) {
|
|
5153
|
-
finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
|
|
5154
|
-
return;
|
|
5155
|
-
}
|
|
5156
|
-
child.stdout?.setEncoding("utf8");
|
|
5157
|
-
child.stdout?.on("data", (chunk) => {
|
|
5158
|
-
stdout += chunk;
|
|
5159
|
-
});
|
|
5160
|
-
child.stderr?.setEncoding("utf8");
|
|
5161
|
-
child.stderr?.on("data", (chunk) => {
|
|
5162
|
-
stderr += chunk;
|
|
5163
|
-
});
|
|
5164
|
-
child.once("error", (error2) => {
|
|
5165
|
-
finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
|
|
5166
|
-
});
|
|
5167
|
-
child.once("close", (code) => {
|
|
5168
|
-
finish({ code, stdout, stderr, timedOut: false });
|
|
5169
|
-
});
|
|
5170
|
-
if (opts.signal) {
|
|
5171
|
-
const killChild = () => {
|
|
5172
|
-
if (child.pid === void 0) {
|
|
5173
|
-
if (!spawnListener) {
|
|
5174
|
-
spawnListener = killChild;
|
|
5175
|
-
child.once("spawn", spawnListener);
|
|
5176
|
-
}
|
|
5177
|
-
return;
|
|
5178
|
-
}
|
|
5179
|
-
child.kill("SIGKILL");
|
|
5180
|
-
};
|
|
5181
|
-
abortListener = killChild;
|
|
5182
|
-
if (opts.signal.aborted) {
|
|
5183
|
-
abortListener();
|
|
5184
|
-
} else {
|
|
5185
|
-
opts.signal.addEventListener("abort", abortListener, { once: true });
|
|
5186
|
-
if (opts.signal.aborted) abortListener();
|
|
5187
|
-
}
|
|
5188
|
-
}
|
|
5189
|
-
timer.handle = setTimeout(
|
|
5190
|
-
() => {
|
|
5191
|
-
child.kill("SIGKILL");
|
|
5192
|
-
finish({ code: null, stdout, stderr, timedOut: true });
|
|
5193
|
-
},
|
|
5194
|
-
Math.max(0, opts.timeoutMs)
|
|
5195
|
-
);
|
|
5196
|
-
});
|
|
5197
|
-
}
|
|
5198
|
-
|
|
5199
|
-
// src/lib/opencode/session-db-boot.ts
|
|
6477
|
+
import { dirname as dirname7, resolve as resolvePath } from "node:path";
|
|
5200
6478
|
var SESSION_DB_RESTORE_TIMEOUT_MS = 3e5;
|
|
5201
6479
|
var SESSION_DB_VERIFY_TIMEOUT_MS = 36e4;
|
|
5202
6480
|
var SESSION_DB_SYNCHRONISER_TIMEOUT_MS = 12e4;
|
|
@@ -5246,8 +6524,8 @@ function clearMarker(options) {
|
|
|
5246
6524
|
function markNoReplicate(options, message) {
|
|
5247
6525
|
if (options.noReplicateMarker) {
|
|
5248
6526
|
try {
|
|
5249
|
-
|
|
5250
|
-
|
|
6527
|
+
mkdirSync4(dirname7(options.noReplicateMarker), { recursive: true });
|
|
6528
|
+
writeFileSync4(options.noReplicateMarker, "");
|
|
5251
6529
|
} catch (error2) {
|
|
5252
6530
|
options.log(
|
|
5253
6531
|
`Could not write session-DB no-replicate marker ${options.noReplicateMarker}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
@@ -5401,8 +6679,8 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
5401
6679
|
return null;
|
|
5402
6680
|
}
|
|
5403
6681
|
try {
|
|
5404
|
-
|
|
5405
|
-
|
|
6682
|
+
mkdirSync4(dirname7(configPath), { recursive: true });
|
|
6683
|
+
writeFileSync4(configPath, rendered.stdout);
|
|
5406
6684
|
} catch (error2) {
|
|
5407
6685
|
options.log(
|
|
5408
6686
|
`Could not write ${configPath}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
@@ -5667,8 +6945,8 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
5667
6945
|
|
|
5668
6946
|
// src/lib/opencode/session-db-provenance.ts
|
|
5669
6947
|
import { createRequire } from "node:module";
|
|
5670
|
-
import { mkdirSync as
|
|
5671
|
-
import { dirname as
|
|
6948
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6949
|
+
import { dirname as dirname8, join as join9 } from "node:path";
|
|
5672
6950
|
var require2 = createRequire(import.meta.url);
|
|
5673
6951
|
function readSessionDbMigrationIds(dbPath) {
|
|
5674
6952
|
let db;
|
|
@@ -5705,12 +6983,12 @@ function readSessionDbMigrationIds(dbPath) {
|
|
|
5705
6983
|
}
|
|
5706
6984
|
function sessionDbProvenanceStatePath(homeDir, env) {
|
|
5707
6985
|
const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
|
|
5708
|
-
return override ||
|
|
6986
|
+
return override || join9(homeDir, ".local", "state", "evident", "session-db-provenance.json");
|
|
5709
6987
|
}
|
|
5710
6988
|
function loadSessionDbProvenanceState(path) {
|
|
5711
6989
|
let value;
|
|
5712
6990
|
try {
|
|
5713
|
-
value = JSON.parse(
|
|
6991
|
+
value = JSON.parse(readFileSync9(path, "utf8"));
|
|
5714
6992
|
} catch (error2) {
|
|
5715
6993
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return {};
|
|
5716
6994
|
console.error(
|
|
@@ -5734,8 +7012,8 @@ function loadSessionDbProvenanceState(path) {
|
|
|
5734
7012
|
}
|
|
5735
7013
|
function saveSessionDbProvenanceState(path, state) {
|
|
5736
7014
|
try {
|
|
5737
|
-
|
|
5738
|
-
|
|
7015
|
+
mkdirSync5(dirname8(path), { recursive: true });
|
|
7016
|
+
writeFileSync5(path, `${JSON.stringify(state, null, 2)}
|
|
5739
7017
|
`, "utf8");
|
|
5740
7018
|
} catch (error2) {
|
|
5741
7019
|
console.error(
|
|
@@ -5790,6 +7068,26 @@ function isSessionDbProvenanceRecord(value) {
|
|
|
5790
7068
|
return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
|
|
5791
7069
|
}
|
|
5792
7070
|
|
|
7071
|
+
// src/lib/opencode/detect.ts
|
|
7072
|
+
function describeProbe(route, result) {
|
|
7073
|
+
if (result.error?.startsWith("HTTP ")) return `${route}: ${result.error}`;
|
|
7074
|
+
if (result.error) return `${route}: no HTTP response (${result.error})`;
|
|
7075
|
+
return `${route}: ${result.healthy ? "healthy" : "no response detail"}`;
|
|
7076
|
+
}
|
|
7077
|
+
async function classifyRunningOpenCode(port) {
|
|
7078
|
+
const v1 = await checkOpenCodeHealth(port);
|
|
7079
|
+
if (v1.healthy) return { kind: "v1", version: v1.version ?? null };
|
|
7080
|
+
const v2 = await checkOpenCode2Health(port);
|
|
7081
|
+
if (v2.healthy) return { kind: "v2", version: v2.version ?? null };
|
|
7082
|
+
const detail = [
|
|
7083
|
+
describeProbe("V1 /global/health", v1),
|
|
7084
|
+
describeProbe("V2 /api/info (or /api/health fallback)", v2)
|
|
7085
|
+
].join("; ");
|
|
7086
|
+
if (v1.okWithNonJsonBody && v2.authFailed) return { kind: "v2", version: null };
|
|
7087
|
+
if (v1.authFailed && v2.authFailed) return { kind: "authenticated-unknown", detail };
|
|
7088
|
+
return { kind: "no-server-identified", detail };
|
|
7089
|
+
}
|
|
7090
|
+
|
|
5793
7091
|
// src/lib/opencode/opencode2-package.ts
|
|
5794
7092
|
var OPENCODE2_PACKAGE_SPEC = "@opencode-ai/cli@0.0.0-beta-17823";
|
|
5795
7093
|
|
|
@@ -6098,26 +7396,28 @@ async function startOpenCode(port, options = {}) {
|
|
|
6098
7396
|
}
|
|
6099
7397
|
async function startOpenCode2(port, options = {}) {
|
|
6100
7398
|
const password = randomBytes(24).toString("hex");
|
|
6101
|
-
let command = "opencode2";
|
|
7399
|
+
let command = options.command ?? "opencode2";
|
|
6102
7400
|
const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
|
|
6103
7401
|
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...logLevel];
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
7402
|
+
if (options.command === void 0) {
|
|
7403
|
+
try {
|
|
7404
|
+
execSync("which opencode2", { stdio: "ignore" });
|
|
7405
|
+
} catch {
|
|
7406
|
+
command = "npx";
|
|
7407
|
+
args = [
|
|
7408
|
+
"-y",
|
|
7409
|
+
"-p",
|
|
7410
|
+
OPENCODE2_PACKAGE_SPEC,
|
|
7411
|
+
"--",
|
|
7412
|
+
"opencode2",
|
|
7413
|
+
"serve",
|
|
7414
|
+
"--port",
|
|
7415
|
+
port.toString(),
|
|
7416
|
+
"--hostname",
|
|
7417
|
+
"127.0.0.1",
|
|
7418
|
+
...logLevel
|
|
7419
|
+
];
|
|
7420
|
+
}
|
|
6121
7421
|
}
|
|
6122
7422
|
const child = spawn3(command, args, {
|
|
6123
7423
|
env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
|
|
@@ -6174,6 +7474,56 @@ function isOpenCode2Installed() {
|
|
|
6174
7474
|
return false;
|
|
6175
7475
|
}
|
|
6176
7476
|
}
|
|
7477
|
+
function readVersionOutput(command) {
|
|
7478
|
+
try {
|
|
7479
|
+
return execSync2(`${command} --version`, {
|
|
7480
|
+
encoding: "utf-8",
|
|
7481
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
7482
|
+
}).trim();
|
|
7483
|
+
} catch (error2) {
|
|
7484
|
+
const output = typeof error2 === "object" && error2 !== null ? [
|
|
7485
|
+
"stdout" in error2 ? String(error2.stdout ?? "").trim() : "",
|
|
7486
|
+
"stderr" in error2 ? String(error2.stderr ?? "").trim() : ""
|
|
7487
|
+
].filter(Boolean).join("\n") : "";
|
|
7488
|
+
return output || (error2 instanceof Error ? error2.message : String(error2));
|
|
7489
|
+
}
|
|
7490
|
+
}
|
|
7491
|
+
function parseOpenCodeVersion(rawVersion) {
|
|
7492
|
+
const output = rawVersion.trim();
|
|
7493
|
+
const prefix = output.match(/^(opencode2|opencode)\s+/i)?.[1]?.toLowerCase();
|
|
7494
|
+
const version2 = (prefix ? output.slice(prefix.length).trim() : output).replace(/^v/i, "");
|
|
7495
|
+
if (prefix === "opencode2") return { major: "v2", version: version2 };
|
|
7496
|
+
const major = version2.match(/^(\d+)(?:\.\d+)*(?:[-+][0-9A-Za-z.-]+)?$/)?.[1];
|
|
7497
|
+
if (!major) return null;
|
|
7498
|
+
if (Number(major) === 1) return { major: "v1", version: version2 };
|
|
7499
|
+
if (Number(major) >= 2) return { major: "v2", version: version2 };
|
|
7500
|
+
return null;
|
|
7501
|
+
}
|
|
7502
|
+
function resolveInstalledOpenCode() {
|
|
7503
|
+
let opencode2Available = false;
|
|
7504
|
+
if (isOpenCodeInstalled()) {
|
|
7505
|
+
const rawVersion = readVersionOutput("opencode");
|
|
7506
|
+
const parsed = parseOpenCodeVersion(rawVersion);
|
|
7507
|
+
if (parsed?.major === "v1") {
|
|
7508
|
+
return { kind: "installed", major: "v1", command: "opencode", version: parsed.version };
|
|
7509
|
+
}
|
|
7510
|
+
if (parsed?.major === "v2") {
|
|
7511
|
+
return { kind: "installed", major: "v2", command: "opencode", version: parsed.version };
|
|
7512
|
+
}
|
|
7513
|
+
opencode2Available = isOpenCode2Installed();
|
|
7514
|
+
if (!opencode2Available) return { kind: "unknown", command: "opencode", rawVersion };
|
|
7515
|
+
}
|
|
7516
|
+
if (opencode2Available || isOpenCode2Installed()) {
|
|
7517
|
+
const parsed = parseOpenCodeVersion(readVersionOutput("opencode2"));
|
|
7518
|
+
return {
|
|
7519
|
+
kind: "installed",
|
|
7520
|
+
major: "v2",
|
|
7521
|
+
command: "opencode2",
|
|
7522
|
+
version: parsed?.version
|
|
7523
|
+
};
|
|
7524
|
+
}
|
|
7525
|
+
return { kind: "none" };
|
|
7526
|
+
}
|
|
6177
7527
|
async function promptOpenCodeInstall(interactive) {
|
|
6178
7528
|
if (!interactive) {
|
|
6179
7529
|
console.log(
|
|
@@ -6274,124 +7624,17 @@ function buildNoProviderWarning(hasProvider) {
|
|
|
6274
7624
|
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).";
|
|
6275
7625
|
}
|
|
6276
7626
|
|
|
6277
|
-
// src/lib/opencode/session-cleanup.ts
|
|
6278
|
-
var DURATION_UNIT_MS = {
|
|
6279
|
-
s: 1e3,
|
|
6280
|
-
m: 60 * 1e3,
|
|
6281
|
-
h: 60 * 60 * 1e3,
|
|
6282
|
-
d: 24 * 60 * 60 * 1e3
|
|
6283
|
-
};
|
|
6284
|
-
function parseDurationMs(input) {
|
|
6285
|
-
const trimmed = input.trim();
|
|
6286
|
-
const match = /^(\d+)([smhd])$/.exec(trimmed);
|
|
6287
|
-
if (!match) {
|
|
6288
|
-
throw new Error(
|
|
6289
|
-
`Invalid duration "${input}": expected <number><unit> where unit is one of s, m, h, d (e.g. "7d", "24h", "30m", "90s").`
|
|
6290
|
-
);
|
|
6291
|
-
}
|
|
6292
|
-
const value = Number(match[1]);
|
|
6293
|
-
if (value <= 0) {
|
|
6294
|
-
throw new Error(`Invalid duration "${input}": must be a positive value.`);
|
|
6295
|
-
}
|
|
6296
|
-
return value * DURATION_UNIT_MS[match[2]];
|
|
6297
|
-
}
|
|
6298
|
-
function selectSessionsToDelete(sessions, opts) {
|
|
6299
|
-
const { maxAgeMs, maxCount, nowMs, protectedIds } = opts;
|
|
6300
|
-
if (maxAgeMs === void 0 && maxCount === void 0) return [];
|
|
6301
|
-
const ageEligible = (s) => {
|
|
6302
|
-
if (maxAgeMs === void 0) return false;
|
|
6303
|
-
if (s.lastActivityMs === null) return true;
|
|
6304
|
-
return nowMs - s.lastActivityMs > maxAgeMs;
|
|
6305
|
-
};
|
|
6306
|
-
const countEligibleIds = /* @__PURE__ */ new Set();
|
|
6307
|
-
if (maxCount !== void 0) {
|
|
6308
|
-
const byActivityDesc = [...sessions].sort(
|
|
6309
|
-
(a, b) => (b.lastActivityMs ?? -Infinity) - (a.lastActivityMs ?? -Infinity)
|
|
6310
|
-
);
|
|
6311
|
-
for (const s of byActivityDesc.slice(maxCount)) {
|
|
6312
|
-
countEligibleIds.add(s.id);
|
|
6313
|
-
}
|
|
6314
|
-
}
|
|
6315
|
-
const toDelete = [];
|
|
6316
|
-
for (const s of sessions) {
|
|
6317
|
-
if (protectedIds.has(s.id)) continue;
|
|
6318
|
-
if (ageEligible(s) || countEligibleIds.has(s.id)) {
|
|
6319
|
-
toDelete.push(s.id);
|
|
6320
|
-
}
|
|
6321
|
-
}
|
|
6322
|
-
const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
|
|
6323
|
-
return toDelete.sort(
|
|
6324
|
-
(a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
|
|
6325
|
-
);
|
|
6326
|
-
}
|
|
6327
|
-
var DEFAULT_INTERVAL = "1h";
|
|
6328
|
-
function resolve2(flag, envValue, fallback) {
|
|
6329
|
-
return flag ?? envValue ?? fallback;
|
|
6330
|
-
}
|
|
6331
|
-
function parseMaxCount(input) {
|
|
6332
|
-
const trimmed = input.trim();
|
|
6333
|
-
if (!/^\d+$/.test(trimmed)) {
|
|
6334
|
-
throw new Error(`Invalid max-count "${input}": expected a positive integer.`);
|
|
6335
|
-
}
|
|
6336
|
-
const value = Number(trimmed);
|
|
6337
|
-
if (value <= 0) {
|
|
6338
|
-
throw new Error(`Invalid max-count "${input}": must be greater than 0.`);
|
|
6339
|
-
}
|
|
6340
|
-
return value;
|
|
6341
|
-
}
|
|
6342
|
-
function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
6343
|
-
const warnings = [];
|
|
6344
|
-
const maxAgeRaw = resolve2(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
|
|
6345
|
-
const maxCountRaw = resolve2(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
|
|
6346
|
-
const intervalRaw = resolve2(
|
|
6347
|
-
flags.interval,
|
|
6348
|
-
env.EVIDENT_SESSION_CLEANUP_INTERVAL,
|
|
6349
|
-
DEFAULT_INTERVAL
|
|
6350
|
-
);
|
|
6351
|
-
let maxAgeMs;
|
|
6352
|
-
if (maxAgeRaw !== void 0) {
|
|
6353
|
-
try {
|
|
6354
|
-
maxAgeMs = parseDurationMs(maxAgeRaw);
|
|
6355
|
-
} catch (err) {
|
|
6356
|
-
warnings.push(
|
|
6357
|
-
`Ignoring invalid --session-cleanup-max-age: ${err instanceof Error ? err.message : String(err)}`
|
|
6358
|
-
);
|
|
6359
|
-
}
|
|
6360
|
-
}
|
|
6361
|
-
let maxCount;
|
|
6362
|
-
if (maxCountRaw !== void 0) {
|
|
6363
|
-
try {
|
|
6364
|
-
maxCount = parseMaxCount(maxCountRaw);
|
|
6365
|
-
} catch (err) {
|
|
6366
|
-
warnings.push(
|
|
6367
|
-
`Ignoring invalid --session-cleanup-max-count: ${err instanceof Error ? err.message : String(err)}`
|
|
6368
|
-
);
|
|
6369
|
-
}
|
|
6370
|
-
}
|
|
6371
|
-
let intervalMs;
|
|
6372
|
-
try {
|
|
6373
|
-
intervalMs = parseDurationMs(intervalRaw ?? DEFAULT_INTERVAL);
|
|
6374
|
-
} catch (err) {
|
|
6375
|
-
warnings.push(
|
|
6376
|
-
`Ignoring invalid --session-cleanup-interval, using default ${DEFAULT_INTERVAL}: ${err instanceof Error ? err.message : String(err)}`
|
|
6377
|
-
);
|
|
6378
|
-
intervalMs = parseDurationMs(DEFAULT_INTERVAL);
|
|
6379
|
-
}
|
|
6380
|
-
const enabled = maxAgeMs !== void 0 || maxCount !== void 0;
|
|
6381
|
-
return { enabled, maxAgeMs, maxCount, intervalMs, warnings };
|
|
6382
|
-
}
|
|
6383
|
-
|
|
6384
7627
|
// src/lib/opencode/session-db-size.ts
|
|
6385
7628
|
import { statSync as statSync4 } from "node:fs";
|
|
6386
|
-
import { join as
|
|
7629
|
+
import { join as join10 } from "node:path";
|
|
6387
7630
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
6388
7631
|
function statSessionDbBytes(homeDir) {
|
|
6389
|
-
const dbPath =
|
|
7632
|
+
const dbPath = join10(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
6390
7633
|
try {
|
|
6391
7634
|
return statSync4(dbPath).size;
|
|
6392
7635
|
} catch (err) {
|
|
6393
|
-
const
|
|
6394
|
-
if (!
|
|
7636
|
+
const isMissingFile4 = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
7637
|
+
if (!isMissingFile4) {
|
|
6395
7638
|
console.error(
|
|
6396
7639
|
`[statSessionDbBytes] could not stat ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
6397
7640
|
);
|
|
@@ -6416,12 +7659,12 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
6416
7659
|
// src/lib/opencode/log-tail.ts
|
|
6417
7660
|
import { statSync as statSync5 } from "node:fs";
|
|
6418
7661
|
import { homedir as homedir5 } from "node:os";
|
|
6419
|
-
import { join as
|
|
7662
|
+
import { join as join11 } from "node:path";
|
|
6420
7663
|
import { open as open2, stat } from "node:fs/promises";
|
|
6421
7664
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
6422
7665
|
function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
|
|
6423
|
-
const dataDir = env.XDG_DATA_HOME ||
|
|
6424
|
-
return
|
|
7666
|
+
const dataDir = env.XDG_DATA_HOME || join11(homeDir, ".local", "share");
|
|
7667
|
+
return join11(dataDir, "opencode", "log", "opencode.log");
|
|
6425
7668
|
}
|
|
6426
7669
|
function isEnoent(error2) {
|
|
6427
7670
|
return error2?.code === "ENOENT";
|
|
@@ -6906,8 +8149,8 @@ function connectTunnel(options) {
|
|
|
6906
8149
|
}
|
|
6907
8150
|
forwarder.handleBinaryBodyFrame(frame.sid, frame.payload);
|
|
6908
8151
|
} catch (error2) {
|
|
6909
|
-
const
|
|
6910
|
-
onError?.(`Failed to handle binary message: ${
|
|
8152
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
8153
|
+
onError?.(`Failed to handle binary message: ${errorMessage4}`);
|
|
6911
8154
|
}
|
|
6912
8155
|
return;
|
|
6913
8156
|
}
|
|
@@ -6915,8 +8158,8 @@ function connectTunnel(options) {
|
|
|
6915
8158
|
try {
|
|
6916
8159
|
message = JSON.parse(data.toString());
|
|
6917
8160
|
} catch (error2) {
|
|
6918
|
-
const
|
|
6919
|
-
onError?.(`Failed to handle message: ${
|
|
8161
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
8162
|
+
onError?.(`Failed to handle message: ${errorMessage4}`);
|
|
6920
8163
|
return;
|
|
6921
8164
|
}
|
|
6922
8165
|
if (isStreamFrame(message)) {
|
|
@@ -7088,10 +8331,10 @@ var RunnerConnection = class _RunnerConnection {
|
|
|
7088
8331
|
};
|
|
7089
8332
|
|
|
7090
8333
|
// src/lib/tunnel/ready-marker.ts
|
|
7091
|
-
import { writeFileSync as
|
|
8334
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
7092
8335
|
function writeTunnelReadyMarker(path, agentId) {
|
|
7093
8336
|
try {
|
|
7094
|
-
|
|
8337
|
+
writeFileSync6(path, `${agentId}
|
|
7095
8338
|
`);
|
|
7096
8339
|
return { ok: true };
|
|
7097
8340
|
} catch (error2) {
|
|
@@ -7116,7 +8359,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
7116
8359
|
}
|
|
7117
8360
|
|
|
7118
8361
|
// src/lib/process-liveness.ts
|
|
7119
|
-
import { readFileSync as
|
|
8362
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
7120
8363
|
function isProcessAlive(pid) {
|
|
7121
8364
|
try {
|
|
7122
8365
|
process.kill(pid, 0);
|
|
@@ -7131,7 +8374,7 @@ function isProcessAlive(pid) {
|
|
|
7131
8374
|
}
|
|
7132
8375
|
if (process.platform !== "linux") return true;
|
|
7133
8376
|
try {
|
|
7134
|
-
const status2 =
|
|
8377
|
+
const status2 = readFileSync10(`/proc/${pid}/status`, "utf8");
|
|
7135
8378
|
return !/^State:\s+Z(?:\s|$)/m.test(status2);
|
|
7136
8379
|
} catch (error2) {
|
|
7137
8380
|
console.error(
|
|
@@ -7198,8 +8441,8 @@ function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
|
7198
8441
|
}
|
|
7199
8442
|
|
|
7200
8443
|
// src/lib/resource-usage-reporting.ts
|
|
7201
|
-
var
|
|
7202
|
-
var
|
|
8444
|
+
var ENABLED_VALUES2 = /* @__PURE__ */ new Set(["on", "true", "1"]);
|
|
8445
|
+
var DISABLED_VALUES2 = /* @__PURE__ */ new Set(["off", "false", "0"]);
|
|
7203
8446
|
function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
7204
8447
|
if (flagValue === false) {
|
|
7205
8448
|
return { enabled: false, warnings: [] };
|
|
@@ -7209,10 +8452,10 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
|
7209
8452
|
return { enabled: true, warnings: [] };
|
|
7210
8453
|
}
|
|
7211
8454
|
const normalized = raw.trim().toLowerCase();
|
|
7212
|
-
if (
|
|
8455
|
+
if (DISABLED_VALUES2.has(normalized)) {
|
|
7213
8456
|
return { enabled: false, warnings: [] };
|
|
7214
8457
|
}
|
|
7215
|
-
if (
|
|
8458
|
+
if (ENABLED_VALUES2.has(normalized)) {
|
|
7216
8459
|
return { enabled: true, warnings: [] };
|
|
7217
8460
|
}
|
|
7218
8461
|
return {
|
|
@@ -7490,18 +8733,18 @@ function createSessionStoreHealth(options = {}) {
|
|
|
7490
8733
|
import { homedir as homedir6 } from "node:os";
|
|
7491
8734
|
|
|
7492
8735
|
// src/lib/runner-file-sync.ts
|
|
7493
|
-
import { join as
|
|
8736
|
+
import { join as join13 } from "node:path";
|
|
7494
8737
|
|
|
7495
8738
|
// src/lib/file-push.ts
|
|
7496
8739
|
import { randomUUID } from "node:crypto";
|
|
7497
8740
|
import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
|
|
7498
|
-
import { basename as basename2, dirname as
|
|
8741
|
+
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";
|
|
7499
8742
|
var FILE_MODE = 384;
|
|
7500
8743
|
var DIRECTORY_MODE = 448;
|
|
7501
8744
|
async function writePushedFile(request3) {
|
|
7502
|
-
const { requestedPath, content, allowedDirectories, homeDir } = request3;
|
|
8745
|
+
const { requestedPath, content, allowedDirectories, allowedFiles, homeDir } = request3;
|
|
7503
8746
|
const bytes = content.byteLength;
|
|
7504
|
-
if (allowedDirectories.length === 0) {
|
|
8747
|
+
if (allowedDirectories.length === 0 && allowedFiles.length === 0) {
|
|
7505
8748
|
return refuse("file_sync_disabled", "File sync is not enabled on this runner.", {
|
|
7506
8749
|
path: requestedPath,
|
|
7507
8750
|
bytes
|
|
@@ -7526,10 +8769,10 @@ async function writePushedFile(request3) {
|
|
|
7526
8769
|
}
|
|
7527
8770
|
try {
|
|
7528
8771
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
7529
|
-
|
|
8772
|
+
dirname9(candidate)
|
|
7530
8773
|
);
|
|
7531
|
-
const realTarget =
|
|
7532
|
-
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
8774
|
+
const realTarget = join12(existingAncestor, ...missingSegments, basename2(candidate));
|
|
8775
|
+
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget) ?? await findMatchingAllowedFile(allowedFiles, realTarget);
|
|
7533
8776
|
if (allowedDirectory === null) {
|
|
7534
8777
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
7535
8778
|
path: realTarget,
|
|
@@ -7538,8 +8781,8 @@ async function writePushedFile(request3) {
|
|
|
7538
8781
|
}
|
|
7539
8782
|
if (missingSegments.length > 0) {
|
|
7540
8783
|
await createMissingDirectories(existingAncestor, missingSegments);
|
|
7541
|
-
const realParent = await realpath(
|
|
7542
|
-
if (realParent !==
|
|
8784
|
+
const realParent = await realpath(dirname9(realTarget));
|
|
8785
|
+
if (realParent !== dirname9(realTarget) || !contains(allowedDirectory, realTarget)) {
|
|
7543
8786
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
7544
8787
|
path: realTarget,
|
|
7545
8788
|
bytes,
|
|
@@ -7564,7 +8807,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
7564
8807
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
7565
8808
|
return null;
|
|
7566
8809
|
}
|
|
7567
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
8810
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join12(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
7568
8811
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
7569
8812
|
return null;
|
|
7570
8813
|
}
|
|
@@ -7582,7 +8825,7 @@ async function resolveNearestExistingAncestor(directory) {
|
|
|
7582
8825
|
try {
|
|
7583
8826
|
return { existingAncestor: await realpath(current), missingSegments };
|
|
7584
8827
|
} catch (err) {
|
|
7585
|
-
const parent =
|
|
8828
|
+
const parent = dirname9(current);
|
|
7586
8829
|
if (err.code !== "ENOENT" || parent === current) {
|
|
7587
8830
|
throw err;
|
|
7588
8831
|
}
|
|
@@ -7597,9 +8840,25 @@ async function findContainingAllowedDirectory(allowedDirectories, realTarget) {
|
|
|
7597
8840
|
log("warn", "file_push_allowed_directory_skipped", { directory, reason: "not_absolute" });
|
|
7598
8841
|
continue;
|
|
7599
8842
|
}
|
|
7600
|
-
const realDirectory = await realpathCreatingIfMissing(directory);
|
|
7601
|
-
if (realDirectory !== null && contains(realDirectory, realTarget)) {
|
|
7602
|
-
return realDirectory;
|
|
8843
|
+
const realDirectory = await realpathCreatingIfMissing(directory);
|
|
8844
|
+
if (realDirectory !== null && contains(realDirectory, realTarget)) {
|
|
8845
|
+
return realDirectory;
|
|
8846
|
+
}
|
|
8847
|
+
}
|
|
8848
|
+
return null;
|
|
8849
|
+
}
|
|
8850
|
+
async function findMatchingAllowedFile(allowedFiles, realTarget) {
|
|
8851
|
+
for (const entry of allowedFiles) {
|
|
8852
|
+
if (!isAbsolute2(entry)) {
|
|
8853
|
+
log("warn", "file_push_allowed_directory_skipped", {
|
|
8854
|
+
directory: entry,
|
|
8855
|
+
reason: "not_absolute_file"
|
|
8856
|
+
});
|
|
8857
|
+
continue;
|
|
8858
|
+
}
|
|
8859
|
+
const realParent = await realpathCreatingIfMissing(dirname9(entry));
|
|
8860
|
+
if (realParent !== null && join12(realParent, basename2(entry)) === realTarget) {
|
|
8861
|
+
return realParent;
|
|
7603
8862
|
}
|
|
7604
8863
|
}
|
|
7605
8864
|
return null;
|
|
@@ -7637,13 +8896,13 @@ function contains(realDirectory, realTarget) {
|
|
|
7637
8896
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
7638
8897
|
let current = existingAncestor;
|
|
7639
8898
|
for (const segment of missingSegments) {
|
|
7640
|
-
current =
|
|
8899
|
+
current = join12(current, segment);
|
|
7641
8900
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
7642
8901
|
await chmod(current, DIRECTORY_MODE);
|
|
7643
8902
|
}
|
|
7644
8903
|
}
|
|
7645
8904
|
async function writeAtomically(realTarget, content) {
|
|
7646
|
-
const temporaryPath =
|
|
8905
|
+
const temporaryPath = join12(dirname9(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
7647
8906
|
let handle;
|
|
7648
8907
|
try {
|
|
7649
8908
|
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
@@ -7773,16 +9032,16 @@ var NOT_APPLIED = {
|
|
|
7773
9032
|
opencodeAuthApplied: false
|
|
7774
9033
|
};
|
|
7775
9034
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
7776
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
7777
|
-
return expanded ===
|
|
9035
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
9036
|
+
return expanded === join13(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
7778
9037
|
}
|
|
7779
9038
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
7780
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
7781
|
-
return expanded ===
|
|
9039
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
9040
|
+
return expanded === join13(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
7782
9041
|
}
|
|
7783
9042
|
async function applyOne(options, file) {
|
|
7784
9043
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
7785
|
-
if (options.allowedDirectories.length === 0) {
|
|
9044
|
+
if (options.allowedDirectories.length === 0 && options.allowedFiles.length === 0) {
|
|
7786
9045
|
options.log({
|
|
7787
9046
|
level: "warn",
|
|
7788
9047
|
message: `Runner file ${label} rejected: file sync is not enabled on this runner (start it with --enable-file-sync-to)`
|
|
@@ -7809,6 +9068,7 @@ async function applyOne(options, file) {
|
|
|
7809
9068
|
requestedPath: file.path,
|
|
7810
9069
|
content: download.content,
|
|
7811
9070
|
allowedDirectories: options.allowedDirectories,
|
|
9071
|
+
allowedFiles: options.allowedFiles,
|
|
7812
9072
|
homeDir: options.homeDir
|
|
7813
9073
|
});
|
|
7814
9074
|
} catch (err) {
|
|
@@ -7916,7 +9176,7 @@ function describe(err) {
|
|
|
7916
9176
|
}
|
|
7917
9177
|
|
|
7918
9178
|
// src/lib/vault-file-sync.ts
|
|
7919
|
-
import { createHash } from "node:crypto";
|
|
9179
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
7920
9180
|
async function syncVaultFiles(options) {
|
|
7921
9181
|
const files = await listVaultFiles(options);
|
|
7922
9182
|
if (files === null) return { applied: 0 };
|
|
@@ -8012,7 +9272,7 @@ async function reconcileVaultFile(options, file) {
|
|
|
8012
9272
|
}
|
|
8013
9273
|
const content = await downloadVaultFile(options, file);
|
|
8014
9274
|
if (content === null) return false;
|
|
8015
|
-
const actualSha256 =
|
|
9275
|
+
const actualSha256 = createHash2("sha256").update(content).digest("hex");
|
|
8016
9276
|
if (actualSha256 !== file.content_sha256) {
|
|
8017
9277
|
options.log({
|
|
8018
9278
|
level: "warn",
|
|
@@ -8060,6 +9320,7 @@ async function applyVaultFile(options, file, content) {
|
|
|
8060
9320
|
requestedPath: file.destination_path,
|
|
8061
9321
|
content,
|
|
8062
9322
|
allowedDirectories: options.allowedDirectories,
|
|
9323
|
+
allowedFiles: options.allowedFiles,
|
|
8063
9324
|
homeDir: options.homeDir
|
|
8064
9325
|
});
|
|
8065
9326
|
if (!outcome.ok) {
|
|
@@ -8212,6 +9473,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8212
9473
|
conversationFilter;
|
|
8213
9474
|
retry;
|
|
8214
9475
|
log;
|
|
9476
|
+
recordBinding;
|
|
8215
9477
|
fetchImpl;
|
|
8216
9478
|
sleep;
|
|
8217
9479
|
pausedPollIntervalMs;
|
|
@@ -8219,6 +9481,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8219
9481
|
stuckQueuedMs;
|
|
8220
9482
|
now;
|
|
8221
9483
|
fileSyncDirectories;
|
|
9484
|
+
fileSyncExactFiles;
|
|
8222
9485
|
homeDir;
|
|
8223
9486
|
maxActiveSessions;
|
|
8224
9487
|
watcherStallMs;
|
|
@@ -8573,6 +9836,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8573
9836
|
this.retry = { ...DEFAULT_RETRY_POLICY, ...config.retry };
|
|
8574
9837
|
this.log = config.log ?? (() => {
|
|
8575
9838
|
});
|
|
9839
|
+
this.recordBinding = config.recordBinding;
|
|
8576
9840
|
this.fetchImpl = withRequestTimeout(
|
|
8577
9841
|
config.fetchImpl ?? fetch,
|
|
8578
9842
|
config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
|
|
@@ -8588,6 +9852,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8588
9852
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
8589
9853
|
this.now = config.now ?? (() => Date.now());
|
|
8590
9854
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
9855
|
+
this.fileSyncExactFiles = config.fileSyncExactFiles ?? [];
|
|
8591
9856
|
this.homeDir = config.homeDir ?? homedir6();
|
|
8592
9857
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
8593
9858
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
@@ -8685,6 +9950,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8685
9950
|
getAuthHeader: this.getAuthHeader,
|
|
8686
9951
|
fetchImpl: this.fetchImpl,
|
|
8687
9952
|
allowedDirectories: this.fileSyncDirectories,
|
|
9953
|
+
allowedFiles: this.fileSyncExactFiles,
|
|
8688
9954
|
homeDir: this.homeDir,
|
|
8689
9955
|
ackFailures: this.fileAckFailures,
|
|
8690
9956
|
log: this.log
|
|
@@ -8695,6 +9961,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8695
9961
|
getAuthHeader: this.getAuthHeader,
|
|
8696
9962
|
fetchImpl: this.fetchImpl,
|
|
8697
9963
|
allowedDirectories: this.fileSyncDirectories,
|
|
9964
|
+
allowedFiles: this.fileSyncExactFiles,
|
|
8698
9965
|
homeDir: this.homeDir,
|
|
8699
9966
|
outcomes: this.vaultFileOutcomes,
|
|
8700
9967
|
log: this.log
|
|
@@ -8757,6 +10024,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8757
10024
|
let cappedSkips = 0;
|
|
8758
10025
|
for (const conv of conversations) {
|
|
8759
10026
|
if (this.stopped) break;
|
|
10027
|
+
if (conv.opencode_session_id) {
|
|
10028
|
+
this.recordSessionBinding(conv.id, conv.opencode_session_id);
|
|
10029
|
+
}
|
|
8760
10030
|
if (this.maxActiveSessions !== void 0) {
|
|
8761
10031
|
const activeSessionIds = this.activeSessionIdsForCap();
|
|
8762
10032
|
const resolvedSessionId = this.sessions.get(conv.id) ?? conv.opencode_session_id;
|
|
@@ -8839,12 +10109,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8839
10109
|
this.sessions.delete(conversationId);
|
|
8840
10110
|
this.supersede(conversationId, sessionId);
|
|
8841
10111
|
const dispatchDescription = source === "readopt" ? "re-dispatched message" : "message";
|
|
8842
|
-
const
|
|
8843
|
-
this.untrackableAck.set(messageId,
|
|
10112
|
+
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.`;
|
|
10113
|
+
this.untrackableAck.set(messageId, errorMessage4);
|
|
8844
10114
|
this.untrackableAckSessionIds.set(messageId, null);
|
|
8845
10115
|
this.log({
|
|
8846
10116
|
level: "error",
|
|
8847
|
-
message:
|
|
10117
|
+
message: errorMessage4,
|
|
8848
10118
|
conversation_id: conversationId,
|
|
8849
10119
|
message_id: messageId
|
|
8850
10120
|
});
|
|
@@ -8852,7 +10122,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8852
10122
|
conversationId,
|
|
8853
10123
|
messageId,
|
|
8854
10124
|
null,
|
|
8855
|
-
|
|
10125
|
+
errorMessage4
|
|
8856
10126
|
);
|
|
8857
10127
|
if (reportError !== null) {
|
|
8858
10128
|
this.log({
|
|
@@ -9033,12 +10303,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9033
10303
|
conversation_id: conv.id,
|
|
9034
10304
|
message_id: message.id
|
|
9035
10305
|
});
|
|
9036
|
-
const
|
|
10306
|
+
const errorMessage4 = this.untrackableAck.get(message.id);
|
|
9037
10307
|
const reportError = await this.reportUntrackableAck(
|
|
9038
10308
|
conv.id,
|
|
9039
10309
|
message.id,
|
|
9040
10310
|
null,
|
|
9041
|
-
|
|
10311
|
+
errorMessage4
|
|
9042
10312
|
);
|
|
9043
10313
|
if (reportError === null) {
|
|
9044
10314
|
continue;
|
|
@@ -9126,7 +10396,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9126
10396
|
const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
|
|
9127
10397
|
const providerId = hasProviderAndModel ? dispatchedModel.slice(0, separator) : null;
|
|
9128
10398
|
const modelId = hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null;
|
|
9129
|
-
const
|
|
10399
|
+
const errorMessage4 = `Provider namespace not loaded on this runner: ${message.opencode_model}. Refused before dispatch; no turn was started.`;
|
|
9130
10400
|
this.log({
|
|
9131
10401
|
level: "error",
|
|
9132
10402
|
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`,
|
|
@@ -9134,7 +10404,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9134
10404
|
message_id: message.id
|
|
9135
10405
|
});
|
|
9136
10406
|
try {
|
|
9137
|
-
await this.markFailed(conv.id, message.id, sessionId,
|
|
10407
|
+
await this.markFailed(conv.id, message.id, sessionId, errorMessage4, void 0, {
|
|
9138
10408
|
classification: {
|
|
9139
10409
|
kind: "model_auth",
|
|
9140
10410
|
providerId,
|
|
@@ -9179,7 +10449,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9179
10449
|
false
|
|
9180
10450
|
);
|
|
9181
10451
|
}
|
|
9182
|
-
opencodeMessageId = this.isV2 ? await sendV2Prompt(
|
|
10452
|
+
opencodeMessageId = this.isV2 ? await sendV2Prompt(
|
|
10453
|
+
this.openCodeClient,
|
|
10454
|
+
sessionId,
|
|
10455
|
+
message.content,
|
|
10456
|
+
options,
|
|
10457
|
+
() => this.logInvalidV2ModelSelection(conv.id, message.id)
|
|
10458
|
+
) : await this.dispatchLocked(
|
|
9183
10459
|
sessionId,
|
|
9184
10460
|
() => sendPromptAsync(
|
|
9185
10461
|
this.port,
|
|
@@ -9206,12 +10482,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9206
10482
|
} catch (err) {
|
|
9207
10483
|
if (err instanceof ChannelAuthError) throw err;
|
|
9208
10484
|
if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
|
|
9209
|
-
const
|
|
9210
|
-
this.untrackableAck.set(message.id,
|
|
10485
|
+
const errorMessage5 = err instanceof Error ? err.message : String(err);
|
|
10486
|
+
this.untrackableAck.set(message.id, errorMessage5);
|
|
9211
10487
|
this.untrackableAckSessionIds.set(message.id, null);
|
|
9212
10488
|
this.log({
|
|
9213
10489
|
level: "error",
|
|
9214
|
-
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${
|
|
10490
|
+
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage5}`,
|
|
9215
10491
|
conversation_id: conv.id,
|
|
9216
10492
|
message_id: message.id
|
|
9217
10493
|
});
|
|
@@ -9219,7 +10495,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9219
10495
|
conv.id,
|
|
9220
10496
|
message.id,
|
|
9221
10497
|
null,
|
|
9222
|
-
|
|
10498
|
+
errorMessage5
|
|
9223
10499
|
);
|
|
9224
10500
|
if (markError !== null) {
|
|
9225
10501
|
this.log({
|
|
@@ -9254,7 +10530,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9254
10530
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
9255
10531
|
break;
|
|
9256
10532
|
}
|
|
9257
|
-
|
|
10533
|
+
if (this.isV2 && err instanceof OpenCodeV2ModelSwitchError) {
|
|
10534
|
+
await this.failV2ModelSwitch(conv, message, sessionId, err);
|
|
10535
|
+
break;
|
|
10536
|
+
}
|
|
10537
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
9258
10538
|
this.sessions.delete(conv.id);
|
|
9259
10539
|
this.supersede(conv.id, sessionId);
|
|
9260
10540
|
this.log({
|
|
@@ -9263,7 +10543,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9263
10543
|
conversation_id: conv.id,
|
|
9264
10544
|
message_id: message.id
|
|
9265
10545
|
});
|
|
9266
|
-
await this.markFailed(conv.id, message.id, null,
|
|
10546
|
+
await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
|
|
9267
10547
|
this.log({
|
|
9268
10548
|
level: "warn",
|
|
9269
10549
|
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)}`,
|
|
@@ -9274,7 +10554,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9274
10554
|
});
|
|
9275
10555
|
this.log({
|
|
9276
10556
|
level: "error",
|
|
9277
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
10557
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage4}`,
|
|
9278
10558
|
conversation_id: conv.id,
|
|
9279
10559
|
message_id: message.id
|
|
9280
10560
|
});
|
|
@@ -9847,6 +11127,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9847
11127
|
this.dispatchNotStartedSignalled.set(message.id, branch);
|
|
9848
11128
|
void this.postSignal(conv.id, message.id, "dispatch_not_started", { branch });
|
|
9849
11129
|
}
|
|
11130
|
+
logInvalidV2ModelSelection(conversationId, messageId) {
|
|
11131
|
+
this.log({
|
|
11132
|
+
level: "warn",
|
|
11133
|
+
message: `Skipping an invalid OpenCode V2 model selection for message ${messageId.slice(0, 8)}; expected provider/model format`,
|
|
11134
|
+
conversation_id: conversationId,
|
|
11135
|
+
message_id: messageId
|
|
11136
|
+
});
|
|
11137
|
+
}
|
|
9850
11138
|
/**
|
|
9851
11139
|
* Class B (#1340): the runner DECIDED an outcome (reattach/settle/fail_permanent)
|
|
9852
11140
|
* but its own PATCH to record it failed. Fires at most once per (message,
|
|
@@ -10061,6 +11349,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10061
11349
|
return { sessionId: await this.createAndBindSession(conv.id), created: true };
|
|
10062
11350
|
}
|
|
10063
11351
|
this.sessions.set(conv.id, bound);
|
|
11352
|
+
this.recordSessionBinding(conv.id, bound);
|
|
10064
11353
|
return { sessionId: bound, created: false };
|
|
10065
11354
|
}
|
|
10066
11355
|
return { sessionId: await this.createAndBindSession(conv.id), created: true };
|
|
@@ -10074,6 +11363,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10074
11363
|
const directory = await this.resolveOpenCodeDirectory();
|
|
10075
11364
|
const sessionId = await this.createOpenCodeSession(directory);
|
|
10076
11365
|
this.sessions.set(conversationId, sessionId);
|
|
11366
|
+
this.recordSessionBinding(conversationId, sessionId);
|
|
10077
11367
|
await this.persistSession(conversationId, sessionId).catch((err) => {
|
|
10078
11368
|
this.log({
|
|
10079
11369
|
level: "warn",
|
|
@@ -10083,6 +11373,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10083
11373
|
});
|
|
10084
11374
|
return sessionId;
|
|
10085
11375
|
}
|
|
11376
|
+
recordSessionBinding(conversationId, sessionId) {
|
|
11377
|
+
if (!this.recordBinding) return;
|
|
11378
|
+
try {
|
|
11379
|
+
this.recordBinding(conversationId, sessionId);
|
|
11380
|
+
} catch (error2) {
|
|
11381
|
+
this.log({
|
|
11382
|
+
level: "warn",
|
|
11383
|
+
message: `Recording OpenCode session ${sessionId.slice(0, 8)} for conversation ${conversationId.slice(0, 8)} failed (best-effort): ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
11384
|
+
conversation_id: conversationId
|
|
11385
|
+
});
|
|
11386
|
+
}
|
|
11387
|
+
}
|
|
10086
11388
|
/**
|
|
10087
11389
|
* Lazily resolve (and cache) opencode's root directory via the selected client's
|
|
10088
11390
|
* location lookup.
|
|
@@ -12183,12 +13485,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12183
13485
|
conversation_id: row.conversation_id,
|
|
12184
13486
|
message_id: row.id
|
|
12185
13487
|
});
|
|
12186
|
-
const
|
|
13488
|
+
const errorMessage4 = this.untrackableAck.get(row.id);
|
|
12187
13489
|
const reportError = await this.reportUntrackableAck(
|
|
12188
13490
|
row.conversation_id,
|
|
12189
13491
|
row.id,
|
|
12190
13492
|
this.untrackableAckSessionIds.get(row.id) ?? null,
|
|
12191
|
-
|
|
13493
|
+
errorMessage4
|
|
12192
13494
|
);
|
|
12193
13495
|
if (reportError !== null) {
|
|
12194
13496
|
this.log({
|
|
@@ -12473,7 +13775,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12473
13775
|
let ocId;
|
|
12474
13776
|
let preDispatchUserIds = null;
|
|
12475
13777
|
try {
|
|
12476
|
-
ocId = this.isV2 ? await sendV2Prompt(
|
|
13778
|
+
ocId = this.isV2 ? await sendV2Prompt(
|
|
13779
|
+
this.openCodeClient,
|
|
13780
|
+
sessionId,
|
|
13781
|
+
row.content,
|
|
13782
|
+
options,
|
|
13783
|
+
() => this.logInvalidV2ModelSelection(row.conversation_id, row.id)
|
|
13784
|
+
) : await this.dispatchLocked(
|
|
12477
13785
|
sessionId,
|
|
12478
13786
|
() => sendPromptAsync(
|
|
12479
13787
|
this.port,
|
|
@@ -12501,14 +13809,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12501
13809
|
this.awaitingReadopt.delete(row.id);
|
|
12502
13810
|
if (err instanceof ChannelAuthError) throw err;
|
|
12503
13811
|
if (this.isV2) {
|
|
12504
|
-
const
|
|
13812
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
12505
13813
|
const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
|
|
12506
13814
|
if (!invalidPromptAck) {
|
|
12507
13815
|
const exists = await this.sessionExists(sessionId);
|
|
12508
13816
|
if (exists === false) {
|
|
12509
13817
|
this.log({
|
|
12510
13818
|
level: "warn",
|
|
12511
|
-
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: ${
|
|
13819
|
+
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}`,
|
|
12512
13820
|
conversation_id: row.conversation_id,
|
|
12513
13821
|
message_id: row.id
|
|
12514
13822
|
});
|
|
@@ -12518,7 +13826,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12518
13826
|
if (exists === null) {
|
|
12519
13827
|
this.log({
|
|
12520
13828
|
level: "warn",
|
|
12521
|
-
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: ${
|
|
13829
|
+
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}`,
|
|
12522
13830
|
conversation_id: row.conversation_id,
|
|
12523
13831
|
message_id: row.id
|
|
12524
13832
|
});
|
|
@@ -12526,12 +13834,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12526
13834
|
return;
|
|
12527
13835
|
}
|
|
12528
13836
|
} else {
|
|
12529
|
-
this.untrackableAck.set(row.id,
|
|
13837
|
+
this.untrackableAck.set(row.id, errorMessage4);
|
|
12530
13838
|
this.untrackableAckSessionIds.set(row.id, sessionId);
|
|
12531
13839
|
}
|
|
13840
|
+
if (err instanceof OpenCodeV2ModelSwitchError) {
|
|
13841
|
+
await this.failV2ModelSwitch(readoptConv, readoptMessage, sessionId, err);
|
|
13842
|
+
return;
|
|
13843
|
+
}
|
|
12532
13844
|
this.log({
|
|
12533
13845
|
level: "error",
|
|
12534
|
-
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${
|
|
13846
|
+
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage4}`,
|
|
12535
13847
|
conversation_id: row.conversation_id,
|
|
12536
13848
|
message_id: row.id
|
|
12537
13849
|
});
|
|
@@ -12540,7 +13852,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12540
13852
|
row.conversation_id,
|
|
12541
13853
|
row.id,
|
|
12542
13854
|
sessionId,
|
|
12543
|
-
|
|
13855
|
+
errorMessage4
|
|
12544
13856
|
);
|
|
12545
13857
|
if (markError !== null) {
|
|
12546
13858
|
this.log({
|
|
@@ -12551,7 +13863,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12551
13863
|
});
|
|
12552
13864
|
}
|
|
12553
13865
|
} else {
|
|
12554
|
-
await this.markFailed(row.conversation_id, row.id, sessionId,
|
|
13866
|
+
await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage4).catch(
|
|
12555
13867
|
(markErr) => {
|
|
12556
13868
|
this.log({
|
|
12557
13869
|
level: "warn",
|
|
@@ -13535,9 +14847,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
13535
14847
|
* trackable. Callers use the returned error to keep their path-specific logging
|
|
13536
14848
|
* while the same report payload can be retried on a later drain.
|
|
13537
14849
|
*/
|
|
13538
|
-
async reportUntrackableAck(conversationId, messageId, sessionId,
|
|
14850
|
+
async reportUntrackableAck(conversationId, messageId, sessionId, errorMessage4) {
|
|
13539
14851
|
try {
|
|
13540
|
-
await this.markFailed(conversationId, messageId, sessionId,
|
|
14852
|
+
await this.markFailed(conversationId, messageId, sessionId, errorMessage4);
|
|
13541
14853
|
return null;
|
|
13542
14854
|
} catch (err) {
|
|
13543
14855
|
this.signalUntrackableAck(conversationId, messageId);
|
|
@@ -13606,6 +14918,38 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
13606
14918
|
}
|
|
13607
14919
|
this.clearSubagentInvocationCaches(messageId);
|
|
13608
14920
|
}
|
|
14921
|
+
async failV2ModelSwitch(conv, message, sessionId, error2) {
|
|
14922
|
+
const { model } = splitModelVariant(message.opencode_model ?? void 0);
|
|
14923
|
+
const separator = model?.indexOf("/") ?? -1;
|
|
14924
|
+
const classification = error2.statusCode === 404 && model && separator > 0 && separator < model.length - 1 ? {
|
|
14925
|
+
kind: "model_auth",
|
|
14926
|
+
providerId: model.slice(0, separator),
|
|
14927
|
+
modelId: model.slice(separator + 1),
|
|
14928
|
+
reason: "missing"
|
|
14929
|
+
} : null;
|
|
14930
|
+
this.log({
|
|
14931
|
+
level: "error",
|
|
14932
|
+
message: `OpenCode could not apply the selected model for message ${message.id.slice(0, 8)}: ${error2.message}`,
|
|
14933
|
+
conversation_id: conv.id,
|
|
14934
|
+
message_id: message.id
|
|
14935
|
+
});
|
|
14936
|
+
try {
|
|
14937
|
+
await this.markFailed(conv.id, message.id, sessionId, error2.message, void 0, {
|
|
14938
|
+
classification,
|
|
14939
|
+
errorShape: { name: null, statusCode: error2.statusCode }
|
|
14940
|
+
});
|
|
14941
|
+
} catch (markErr) {
|
|
14942
|
+
this.log({
|
|
14943
|
+
level: "warn",
|
|
14944
|
+
message: `markFailed PATCH for V2 model switch failure on message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
14945
|
+
conversation_id: conv.id,
|
|
14946
|
+
message_id: message.id
|
|
14947
|
+
});
|
|
14948
|
+
this.signalDispatchNotStarted(conv, message, "failure_unreported");
|
|
14949
|
+
return;
|
|
14950
|
+
}
|
|
14951
|
+
if (classification) await this.repairProviderRegistry(conv.id, message.id);
|
|
14952
|
+
}
|
|
13609
14953
|
/**
|
|
13610
14954
|
* Classify an errored turn and extract its bounded error shape for `markFailed`.
|
|
13611
14955
|
*
|
|
@@ -14049,7 +15393,7 @@ import ora3 from "ora";
|
|
|
14049
15393
|
import { select as select3 } from "@inquirer/prompts";
|
|
14050
15394
|
function unknownPasswordError2(port) {
|
|
14051
15395
|
return new Error(
|
|
14052
|
-
`
|
|
15396
|
+
`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.`
|
|
14053
15397
|
);
|
|
14054
15398
|
}
|
|
14055
15399
|
var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
|
|
@@ -14067,9 +15411,9 @@ async function ensureOpenCode2Running(ctx) {
|
|
|
14067
15411
|
password: null
|
|
14068
15412
|
};
|
|
14069
15413
|
}
|
|
14070
|
-
if (!isOpenCode2Installed()) {
|
|
15414
|
+
if (!ctx.command && !isOpenCode2Installed()) {
|
|
14071
15415
|
throw new Error(
|
|
14072
|
-
|
|
15416
|
+
"OpenCode V2 is not installed. Install it with `npm install -g @opencode/cli` or `curl -fsSL https://opencode.ai/v2/install | bash`."
|
|
14073
15417
|
);
|
|
14074
15418
|
}
|
|
14075
15419
|
let port = ctx.port;
|
|
@@ -14097,7 +15441,8 @@ Port ${port} is already in use.`));
|
|
|
14097
15441
|
if (!ctx.interactive) {
|
|
14098
15442
|
ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
|
|
14099
15443
|
const { child: proc, password } = await startOpenCode2(port, {
|
|
14100
|
-
inheritStdio: ctx.inheritStdio
|
|
15444
|
+
inheritStdio: ctx.inheritStdio,
|
|
15445
|
+
command: ctx.command
|
|
14101
15446
|
});
|
|
14102
15447
|
const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
|
|
14103
15448
|
if (!health.healthy) {
|
|
@@ -14118,13 +15463,14 @@ Port ${port} is already in use.`));
|
|
|
14118
15463
|
password
|
|
14119
15464
|
};
|
|
14120
15465
|
}
|
|
15466
|
+
const command = ctx.command ?? "opencode2";
|
|
14121
15467
|
const action = await select3({
|
|
14122
15468
|
message: "OpenCode V2 is not running. What would you like to do?",
|
|
14123
15469
|
choices: [
|
|
14124
15470
|
{
|
|
14125
15471
|
name: "Start OpenCode V2 for me",
|
|
14126
15472
|
value: "start",
|
|
14127
|
-
description: `Run '
|
|
15473
|
+
description: `Run '${command} serve --port ${port}'`
|
|
14128
15474
|
},
|
|
14129
15475
|
{
|
|
14130
15476
|
name: "Show me the command",
|
|
@@ -14142,14 +15488,15 @@ Port ${port} is already in use.`));
|
|
|
14142
15488
|
blank();
|
|
14143
15489
|
console.log(chalk6.bold("Run this command in another terminal:"));
|
|
14144
15490
|
blank();
|
|
14145
|
-
console.log(` ${chalk6.cyan(
|
|
15491
|
+
console.log(` ${chalk6.cyan(`${command} serve --port ${port}`)}`);
|
|
14146
15492
|
blank();
|
|
14147
15493
|
throw new Error("Please start OpenCode V2 manually");
|
|
14148
15494
|
}
|
|
14149
15495
|
if (action === "start") {
|
|
14150
15496
|
const spinner = ora3("Starting OpenCode V2...").start();
|
|
14151
15497
|
const { child: proc, password } = await startOpenCode2(port, {
|
|
14152
|
-
inheritStdio: ctx.inheritStdio
|
|
15498
|
+
inheritStdio: ctx.inheritStdio,
|
|
15499
|
+
command: ctx.command
|
|
14153
15500
|
});
|
|
14154
15501
|
const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
|
|
14155
15502
|
if (!health.healthy) {
|
|
@@ -14175,7 +15522,7 @@ Port ${port} is already in use.`));
|
|
|
14175
15522
|
}
|
|
14176
15523
|
|
|
14177
15524
|
// src/lib/runner-credentials.ts
|
|
14178
|
-
import { chmodSync as chmodSync2, writeFileSync as
|
|
15525
|
+
import { chmodSync as chmodSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
14179
15526
|
import { spawn as spawn5 } from "node:child_process";
|
|
14180
15527
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
14181
15528
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
@@ -14418,8 +15765,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
14418
15765
|
}
|
|
14419
15766
|
try {
|
|
14420
15767
|
env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
|
|
14421
|
-
|
|
14422
|
-
|
|
15768
|
+
writeFileSync7(GIT_CONFIG_GLOBAL, "");
|
|
15769
|
+
writeFileSync7(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
|
|
14423
15770
|
chmodSync2(GIT_CREDENTIAL_HELPER, 448);
|
|
14424
15771
|
const config = [
|
|
14425
15772
|
["user.name", env.GIT_USER_NAME ?? "evident-bot"],
|
|
@@ -14452,7 +15799,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
14452
15799
|
// src/lib/opencode/config-overlay.ts
|
|
14453
15800
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
14454
15801
|
import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
|
|
14455
|
-
import { isAbsolute as isAbsolute3, join as
|
|
15802
|
+
import { isAbsolute as isAbsolute3, join as join14, resolve as resolve4 } from "node:path";
|
|
14456
15803
|
function parseConfigContent(content) {
|
|
14457
15804
|
if (!content?.trim()) return {};
|
|
14458
15805
|
const parsed = JSON.parse(content);
|
|
@@ -14482,7 +15829,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
14482
15829
|
return;
|
|
14483
15830
|
}
|
|
14484
15831
|
const source = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
|
|
14485
|
-
const target = isFile(
|
|
15832
|
+
const target = isFile(join14(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
14486
15833
|
if (!isFile(source)) {
|
|
14487
15834
|
log3(
|
|
14488
15835
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -14490,7 +15837,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
14490
15837
|
);
|
|
14491
15838
|
return;
|
|
14492
15839
|
}
|
|
14493
|
-
copyFileSync(source,
|
|
15840
|
+
copyFileSync(source, join14(cwd, target));
|
|
14494
15841
|
try {
|
|
14495
15842
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
14496
15843
|
stdio: "ignore"
|
|
@@ -14499,13 +15846,13 @@ function applyRunnerOpenCodeConfig({
|
|
|
14499
15846
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
14500
15847
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
14501
15848
|
}
|
|
14502
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
15849
|
+
log3(`Applied runner OpenCode config ${source} to ${join14(cwd, target)}`);
|
|
14503
15850
|
}
|
|
14504
15851
|
|
|
14505
15852
|
// src/lib/opencode/runtime-provenance.ts
|
|
14506
|
-
import { readFileSync as
|
|
15853
|
+
import { readFileSync as readFileSync11, readdirSync } from "node:fs";
|
|
14507
15854
|
import { homedir as homedir7 } from "node:os";
|
|
14508
|
-
import { join as
|
|
15855
|
+
import { join as join15 } from "node:path";
|
|
14509
15856
|
var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
|
14510
15857
|
var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
|
|
14511
15858
|
function errorKind(error2) {
|
|
@@ -14573,7 +15920,7 @@ function parseJsonc(value) {
|
|
|
14573
15920
|
return JSON.parse(output);
|
|
14574
15921
|
}
|
|
14575
15922
|
function openCodeCacheRoot(env = process.env, homeDir = homedir7()) {
|
|
14576
|
-
return
|
|
15923
|
+
return join15(env.XDG_CACHE_HOME ?? join15(homeDir, ".cache"), "opencode");
|
|
14577
15924
|
}
|
|
14578
15925
|
function readConfiguredPluginSpecs({
|
|
14579
15926
|
cwd = process.cwd(),
|
|
@@ -14582,10 +15929,10 @@ function readConfiguredPluginSpecs({
|
|
|
14582
15929
|
let config;
|
|
14583
15930
|
try {
|
|
14584
15931
|
try {
|
|
14585
|
-
config = parseJsonc(
|
|
15932
|
+
config = parseJsonc(readFileSync11(join15(cwd, "opencode.jsonc"), "utf8"));
|
|
14586
15933
|
} catch (error2) {
|
|
14587
15934
|
if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
|
|
14588
|
-
config = parseJsonc(
|
|
15935
|
+
config = parseJsonc(readFileSync11(join15(cwd, "opencode.json"), "utf8"));
|
|
14589
15936
|
}
|
|
14590
15937
|
} catch (error2) {
|
|
14591
15938
|
log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
|
|
@@ -14613,7 +15960,7 @@ function resolveInstalledPluginVersions({
|
|
|
14613
15960
|
homeDir = homedir7(),
|
|
14614
15961
|
log: log3
|
|
14615
15962
|
}) {
|
|
14616
|
-
const packagesRoot =
|
|
15963
|
+
const packagesRoot = join15(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
|
|
14617
15964
|
let directories;
|
|
14618
15965
|
try {
|
|
14619
15966
|
directories = readdirSync(packagesRoot, { withFileTypes: true });
|
|
@@ -14634,8 +15981,8 @@ function resolveInstalledPluginVersions({
|
|
|
14634
15981
|
let resolvedVersion = null;
|
|
14635
15982
|
try {
|
|
14636
15983
|
const packageJson = JSON.parse(
|
|
14637
|
-
|
|
14638
|
-
|
|
15984
|
+
readFileSync11(
|
|
15985
|
+
join15(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
|
|
14639
15986
|
"utf8"
|
|
14640
15987
|
)
|
|
14641
15988
|
);
|
|
@@ -14656,7 +16003,7 @@ function resolveInstalledPluginVersions({
|
|
|
14656
16003
|
}
|
|
14657
16004
|
|
|
14658
16005
|
// src/lib/credential-sync.ts
|
|
14659
|
-
import { renameSync, writeFileSync as
|
|
16006
|
+
import { renameSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
14660
16007
|
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
14661
16008
|
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
14662
16009
|
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
@@ -14666,7 +16013,7 @@ var MAX_FLUSH_PASSES = 2;
|
|
|
14666
16013
|
function outcomesWith(outcome) {
|
|
14667
16014
|
return { claude: outcome, opencode: outcome };
|
|
14668
16015
|
}
|
|
14669
|
-
function
|
|
16016
|
+
function errorMessage3(error2) {
|
|
14670
16017
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
14671
16018
|
}
|
|
14672
16019
|
function waitForSettlement(promise, timeoutMs) {
|
|
@@ -14690,10 +16037,10 @@ function writeMarker(markerPath, outcomes, log3) {
|
|
|
14690
16037
|
`;
|
|
14691
16038
|
const temporaryPath = `${markerPath}.tmp`;
|
|
14692
16039
|
try {
|
|
14693
|
-
|
|
16040
|
+
writeFileSync8(temporaryPath, body, { mode: 384 });
|
|
14694
16041
|
renameSync(temporaryPath, markerPath);
|
|
14695
16042
|
} catch (error2) {
|
|
14696
|
-
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${
|
|
16043
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage3(error2)}`, "warn");
|
|
14697
16044
|
}
|
|
14698
16045
|
}
|
|
14699
16046
|
function intervalSeconds(env, log3) {
|
|
@@ -14725,7 +16072,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
|
14725
16072
|
},
|
|
14726
16073
|
(error2) => {
|
|
14727
16074
|
failed = true;
|
|
14728
|
-
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${
|
|
16075
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage3(error2)}`, "warn");
|
|
14729
16076
|
}
|
|
14730
16077
|
);
|
|
14731
16078
|
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
@@ -14785,7 +16132,7 @@ function createCredentialSync({
|
|
|
14785
16132
|
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
14786
16133
|
} catch (error2) {
|
|
14787
16134
|
outcomes[store] = "failed";
|
|
14788
|
-
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${
|
|
16135
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage3(error2)}`, "debug");
|
|
14789
16136
|
}
|
|
14790
16137
|
}
|
|
14791
16138
|
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
@@ -14908,14 +16255,21 @@ function request2(options) {
|
|
|
14908
16255
|
function safeFailure(operation, status2) {
|
|
14909
16256
|
return status2 === void 0 ? `${operation} could not reach the managed credential service` : `${operation} returned HTTP ${status2}`;
|
|
14910
16257
|
}
|
|
14911
|
-
function
|
|
14912
|
-
let
|
|
16258
|
+
function redactAuthorizedText(message, authorization) {
|
|
16259
|
+
let redacted = message;
|
|
14913
16260
|
const credential = authorization.split(/\s+/, 2)[1];
|
|
14914
16261
|
for (const secret of [authorization, credential]) {
|
|
14915
|
-
if (secret)
|
|
16262
|
+
if (secret) redacted = redacted.replaceAll(secret, "[redacted]");
|
|
14916
16263
|
}
|
|
16264
|
+
return redactDiagnosticText(redacted);
|
|
16265
|
+
}
|
|
16266
|
+
function logTransportFailure(options, operation, error2, authorization) {
|
|
16267
|
+
const detail = redactAuthorizedText(
|
|
16268
|
+
error2 instanceof Error ? error2.message : String(error2),
|
|
16269
|
+
authorization
|
|
16270
|
+
);
|
|
14917
16271
|
options.log(
|
|
14918
|
-
`MANAGED-INFERENCE-TRANSPORT-FAILED: operation=${operation} detail=${
|
|
16272
|
+
`MANAGED-INFERENCE-TRANSPORT-FAILED: operation=${operation} detail=${detail}`,
|
|
14919
16273
|
"debug"
|
|
14920
16274
|
);
|
|
14921
16275
|
}
|
|
@@ -14935,6 +16289,18 @@ function readSetup(value) {
|
|
|
14935
16289
|
}
|
|
14936
16290
|
return setup;
|
|
14937
16291
|
}
|
|
16292
|
+
function readModelDescriptors(value) {
|
|
16293
|
+
if (!Array.isArray(value)) return null;
|
|
16294
|
+
return value.every(
|
|
16295
|
+
(descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
|
|
16296
|
+
) ? value : null;
|
|
16297
|
+
}
|
|
16298
|
+
function logDescriptorRefreshFallback(options, branch, generation) {
|
|
16299
|
+
options.log(
|
|
16300
|
+
`MANAGED-INFERENCE-DESCRIPTORS-${branch}: runner=${options.agentId} generation=${generation}`,
|
|
16301
|
+
"warn"
|
|
16302
|
+
);
|
|
16303
|
+
}
|
|
14938
16304
|
function clearLocalSetup(options) {
|
|
14939
16305
|
const env = options.env ?? process.env;
|
|
14940
16306
|
const store = options.store ?? defaultStore;
|
|
@@ -14966,7 +16332,14 @@ async function discoverManagedInferenceSetup(options) {
|
|
|
14966
16332
|
return { kind: "none" };
|
|
14967
16333
|
}
|
|
14968
16334
|
if (!response.ok) {
|
|
14969
|
-
|
|
16335
|
+
const body2 = await responseJson(response);
|
|
16336
|
+
const serverMessage = typeof body2?.error === "string" && body2.error.trim() ? redactAuthorizedText(body2.error.trim(), headers.Authorization).slice(0, 500) : void 0;
|
|
16337
|
+
const failure = serverMessage ? `Managed setup discovery returned HTTP ${response.status}: ${serverMessage}` : safeFailure("Managed setup discovery", response.status);
|
|
16338
|
+
const runnerKeyGuidance = response.status === 401 || response.status === 403 ? "Authenticate as this runner with its runner key (EVIDENT_RUNNER_KEY)." : void 0;
|
|
16339
|
+
return {
|
|
16340
|
+
kind: "failed",
|
|
16341
|
+
reason: runnerKeyGuidance ? `${failure}. ${runnerKeyGuidance}` : failure
|
|
16342
|
+
};
|
|
14970
16343
|
}
|
|
14971
16344
|
const body = await responseJson(response);
|
|
14972
16345
|
const setup = readSetup(body?.setup);
|
|
@@ -15066,7 +16439,27 @@ async function reaffirmStored(options, credential) {
|
|
|
15066
16439
|
if (!setup || setup.generation !== credential.generation || setup.token_id !== credential.tokenId) {
|
|
15067
16440
|
return { kind: "failed", reason: "Managed credential reaffirmation returned a mismatch" };
|
|
15068
16441
|
}
|
|
15069
|
-
const
|
|
16442
|
+
const descriptorsValue = body?.model_descriptors;
|
|
16443
|
+
const refreshedDescriptors = readModelDescriptors(descriptorsValue);
|
|
16444
|
+
let credentialToApply = credential;
|
|
16445
|
+
if (descriptorsValue === void 0 || descriptorsValue === null) {
|
|
16446
|
+
logDescriptorRefreshFallback(options, "UNAVAILABLE", credential.generation);
|
|
16447
|
+
} else if (refreshedDescriptors === null) {
|
|
16448
|
+
logDescriptorRefreshFallback(options, "MALFORMED", credential.generation);
|
|
16449
|
+
} else if (refreshedDescriptors.length === 0) {
|
|
16450
|
+
logDescriptorRefreshFallback(options, "EMPTY", credential.generation);
|
|
16451
|
+
} else {
|
|
16452
|
+
const { config, unsupportedProviders } = buildCredentialProxyNativeConfig({
|
|
16453
|
+
inferenceBaseUrl: credential.inferenceBaseUrl,
|
|
16454
|
+
modelDescriptors: refreshedDescriptors
|
|
16455
|
+
});
|
|
16456
|
+
if (unsupportedProviders.length > 0 || Object.keys(config.provider).length === 0) {
|
|
16457
|
+
logDescriptorRefreshFallback(options, "UNSUPPORTED-PROVIDER", credential.generation);
|
|
16458
|
+
} else {
|
|
16459
|
+
credentialToApply = { ...credential, modelDescriptors: refreshedDescriptors };
|
|
16460
|
+
}
|
|
16461
|
+
}
|
|
16462
|
+
const failure = applyCredential(options, credentialToApply);
|
|
15070
16463
|
return failure ? { kind: "failed", reason: failure } : { kind: "ready", generation: credential.generation, tokenId: credential.tokenId };
|
|
15071
16464
|
}
|
|
15072
16465
|
async function prepareManagedInferenceSetup(options, discovery, health) {
|
|
@@ -15100,10 +16493,8 @@ async function prepareManagedInferenceSetup(options, discovery, health) {
|
|
|
15100
16493
|
const setup = readSetup(body?.setup);
|
|
15101
16494
|
const token = body?.inference_token;
|
|
15102
16495
|
const inferenceBaseUrl = body?.inference_base_url;
|
|
15103
|
-
const modelDescriptors = body?.model_descriptors;
|
|
15104
|
-
if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" ||
|
|
15105
|
-
(descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
|
|
15106
|
-
)) {
|
|
16496
|
+
const modelDescriptors = readModelDescriptors(body?.model_descriptors);
|
|
16497
|
+
if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" || modelDescriptors === null) {
|
|
15107
16498
|
return { kind: "failed", reason: "Managed credential bootstrap returned an invalid response" };
|
|
15108
16499
|
}
|
|
15109
16500
|
const credential = {
|
|
@@ -15172,33 +16563,49 @@ function resolveLogLevel(options) {
|
|
|
15172
16563
|
}
|
|
15173
16564
|
return "info";
|
|
15174
16565
|
}
|
|
15175
|
-
function
|
|
15176
|
-
const
|
|
16566
|
+
function resolveFileSyncEntry(entry, homeDir, flagName, labels) {
|
|
16567
|
+
const trimmed = entry.trim();
|
|
16568
|
+
if (trimmed === "") {
|
|
16569
|
+
throw new Error(`${flagName} requires a ${labels.noun} path (got an empty value)`);
|
|
16570
|
+
}
|
|
16571
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join16(homeDir, trimmed.slice(2)) : trimmed;
|
|
16572
|
+
if (!isAbsolute4(expanded)) {
|
|
16573
|
+
throw new Error(`${flagName} requires an absolute ${labels.noun} path; got "${entry}"`);
|
|
16574
|
+
}
|
|
16575
|
+
const normalized = resolvePath2(expanded);
|
|
16576
|
+
if (parse(normalized).root === normalized) {
|
|
16577
|
+
throw new Error(
|
|
16578
|
+
`${flagName} will not allow-list the filesystem root ("${entry}"); name the specific ${labels.noun} the credentials belong in (for example ${labels.rootExample})`
|
|
16579
|
+
);
|
|
16580
|
+
}
|
|
16581
|
+
return normalized;
|
|
16582
|
+
}
|
|
16583
|
+
function resolveFileSyncAllowList(raw, homeDir, flagName, labels) {
|
|
16584
|
+
const paths = [];
|
|
15177
16585
|
for (const entry of raw ?? []) {
|
|
15178
|
-
const
|
|
15179
|
-
if (
|
|
15180
|
-
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
15181
|
-
}
|
|
15182
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join14(homeDir, trimmed.slice(2)) : trimmed;
|
|
15183
|
-
if (!isAbsolute4(expanded)) {
|
|
15184
|
-
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
15185
|
-
}
|
|
15186
|
-
const normalized = resolvePath2(expanded);
|
|
15187
|
-
if (parse(normalized).root === normalized) {
|
|
15188
|
-
throw new Error(
|
|
15189
|
-
`--enable-file-sync-to will not allow-list the filesystem root ("${entry}"); name the specific directory the credentials belong in (for example ~/.claude)`
|
|
15190
|
-
);
|
|
15191
|
-
}
|
|
15192
|
-
if (!directories.includes(normalized)) {
|
|
15193
|
-
directories.push(normalized);
|
|
15194
|
-
}
|
|
16586
|
+
const normalized = resolveFileSyncEntry(entry, homeDir, flagName, labels);
|
|
16587
|
+
if (!paths.includes(normalized)) paths.push(normalized);
|
|
15195
16588
|
}
|
|
15196
|
-
if (
|
|
16589
|
+
if (paths.length > MAX_FILE_SYNC_DIRECTORIES) {
|
|
15197
16590
|
throw new Error(
|
|
15198
|
-
|
|
16591
|
+
`${flagName} accepts at most ${MAX_FILE_SYNC_DIRECTORIES} ${labels.plural}; got ${paths.length}`
|
|
15199
16592
|
);
|
|
15200
16593
|
}
|
|
15201
|
-
return
|
|
16594
|
+
return paths;
|
|
16595
|
+
}
|
|
16596
|
+
function resolveFileSyncDirectories(raw, homeDir) {
|
|
16597
|
+
return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to", {
|
|
16598
|
+
noun: "directory",
|
|
16599
|
+
plural: "directories",
|
|
16600
|
+
rootExample: "~/.claude"
|
|
16601
|
+
});
|
|
16602
|
+
}
|
|
16603
|
+
function resolveFileSyncExactFiles(raw, homeDir) {
|
|
16604
|
+
return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to-exact-file", {
|
|
16605
|
+
noun: "file",
|
|
16606
|
+
plural: "files",
|
|
16607
|
+
rootExample: "~/.local/share/opencode/mcp-auth.json"
|
|
16608
|
+
});
|
|
15202
16609
|
}
|
|
15203
16610
|
var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
|
|
15204
16611
|
var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
|
|
@@ -15214,13 +16621,13 @@ function resolveOpenCodeVersion(options, env = process.env) {
|
|
|
15214
16621
|
raw = env[OPENCODE_VERSION_ENV];
|
|
15215
16622
|
source = OPENCODE_VERSION_ENV;
|
|
15216
16623
|
} else {
|
|
15217
|
-
return { version: "
|
|
16624
|
+
return { version: "auto", warnings: [] };
|
|
15218
16625
|
}
|
|
15219
16626
|
const normalized = raw.trim().toLowerCase();
|
|
15220
|
-
if (normalized !== "v1" && normalized !== "v2") {
|
|
16627
|
+
if (normalized !== "v1" && normalized !== "v2" && normalized !== "auto") {
|
|
15221
16628
|
return {
|
|
15222
|
-
version: "
|
|
15223
|
-
warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or
|
|
16629
|
+
version: "auto",
|
|
16630
|
+
warnings: [`Ignoring invalid ${source} "${raw}": expected v1, v2, or auto; using auto`]
|
|
15224
16631
|
};
|
|
15225
16632
|
}
|
|
15226
16633
|
return { version: normalized, warnings: [] };
|
|
@@ -15730,8 +17137,8 @@ async function driveChannels(state, driver) {
|
|
|
15730
17137
|
state.running = false;
|
|
15731
17138
|
break;
|
|
15732
17139
|
}
|
|
15733
|
-
const
|
|
15734
|
-
logActivity(state, { type: "error", error: `Channel processing error: ${
|
|
17140
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : String(error2);
|
|
17141
|
+
logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage4}` });
|
|
15735
17142
|
if (state.interactive) displayStatus(state);
|
|
15736
17143
|
if (driver.hasInFlightWatchers()) {
|
|
15737
17144
|
consecutiveDrainFailures = 0;
|
|
@@ -15821,6 +17228,7 @@ async function driveChannels(state, driver) {
|
|
|
15821
17228
|
}
|
|
15822
17229
|
}
|
|
15823
17230
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
17231
|
+
var SESSION_ARCHIVE_FIRST_RUN_MS = SESSION_CLEANUP_FIRST_SWEEP_MS + 1e3;
|
|
15824
17232
|
var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
|
|
15825
17233
|
var SESSION_CLEANUP_MAX_ISOLATED_DELETE_FAILURES = 2;
|
|
15826
17234
|
var SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES = 2;
|
|
@@ -15835,7 +17243,7 @@ function shouldWarnForReclaimSkip(reason) {
|
|
|
15835
17243
|
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
15836
17244
|
}
|
|
15837
17245
|
function sessionDbPath() {
|
|
15838
|
-
return
|
|
17246
|
+
return join16(homedir8(), ".local", "share", "opencode", "opencode.db");
|
|
15839
17247
|
}
|
|
15840
17248
|
function logSessionCleanupActivity(state, phase, entry) {
|
|
15841
17249
|
logActivity(state, {
|
|
@@ -15844,6 +17252,93 @@ function logSessionCleanupActivity(state, phase, entry) {
|
|
|
15844
17252
|
metadata: { ...entry.metadata, phase }
|
|
15845
17253
|
});
|
|
15846
17254
|
}
|
|
17255
|
+
function logSessionArchiveActivity(state, phase, entry) {
|
|
17256
|
+
logActivity(state, {
|
|
17257
|
+
...entry,
|
|
17258
|
+
source: "cli.session-archive",
|
|
17259
|
+
metadata: { ...entry.metadata, phase }
|
|
17260
|
+
});
|
|
17261
|
+
}
|
|
17262
|
+
function logSessionArchiveSummary(state, phase, summary) {
|
|
17263
|
+
const everyUploadFailed = summary.uploadAttempts > 0 && summary.uploadFailures === summary.uploadAttempts;
|
|
17264
|
+
const staleSince = everyUploadFailed && summary.staleSince !== null ? `; archive stale since ${summary.staleSince}` : "";
|
|
17265
|
+
const neverWatermarked = summary.neverWatermarkedFailures > 0 ? `; ${summary.neverWatermarkedFailures} failed session(s) had no prior successful upload` : "";
|
|
17266
|
+
const warning2 = summary.failed > 0 || summary.failures.length > 0 || everyUploadFailed || !summary.reconciliationComplete;
|
|
17267
|
+
logSessionArchiveActivity(state, phase, {
|
|
17268
|
+
type: "info",
|
|
17269
|
+
level: warning2 ? "warn" : "info",
|
|
17270
|
+
message: `Session archive: inspected ${summary.inspected}, uploaded ${summary.uploaded}, unchanged ${summary.unchanged}, failed ${summary.failed}, skipped budget ${summary.skippedBudget}, unmapped ${summary.unmappedIds.length}, conflicts ${summary.conflicts}, reconciliation incomplete ${summary.reconciliationIncomplete}; bindings ledger ${summary.bindingSourceCounts.ledger}, watermark ${summary.bindingSourceCounts.watermark}, current ${summary.bindingSourceCounts.current}, invocation ${summary.bindingSourceCounts.invocation}, history ${summary.bindingSourceCounts.history}, parent ${summary.bindingSourceCounts.parent}` + staleSince + neverWatermarked,
|
|
17271
|
+
metadata: {
|
|
17272
|
+
outcome: "archive-run-complete",
|
|
17273
|
+
sessions_inspected: summary.inspected,
|
|
17274
|
+
sessions_uploaded: summary.uploaded,
|
|
17275
|
+
sessions_unchanged: summary.unchanged,
|
|
17276
|
+
sessions_failed: summary.failed,
|
|
17277
|
+
uploads_attempted: summary.uploadAttempts,
|
|
17278
|
+
uploads_failed: summary.uploadFailures,
|
|
17279
|
+
sessions_skipped_budget: summary.skippedBudget,
|
|
17280
|
+
sessions_unmapped: summary.unmappedIds.length,
|
|
17281
|
+
sessions_unresolved: summary.unresolved,
|
|
17282
|
+
sessions_conflicted: summary.conflicts,
|
|
17283
|
+
sessions_reconciliation_incomplete: summary.reconciliationIncomplete,
|
|
17284
|
+
binding_reconciliation_complete: summary.reconciliationComplete,
|
|
17285
|
+
bindings_from_ledger: summary.bindingSourceCounts.ledger,
|
|
17286
|
+
bindings_from_watermark: summary.bindingSourceCounts.watermark,
|
|
17287
|
+
bindings_from_current: summary.bindingSourceCounts.current,
|
|
17288
|
+
bindings_from_invocation: summary.bindingSourceCounts.invocation,
|
|
17289
|
+
bindings_from_history: summary.bindingSourceCounts.history,
|
|
17290
|
+
bindings_from_parent: summary.bindingSourceCounts.parent,
|
|
17291
|
+
sessions_without_prior_watermark: summary.neverWatermarkedFailures,
|
|
17292
|
+
...everyUploadFailed && summary.staleSince !== null ? { stale_since: summary.staleSince } : {}
|
|
17293
|
+
}
|
|
17294
|
+
});
|
|
17295
|
+
for (const failure of summary.failures) {
|
|
17296
|
+
const permanentSizeFailure = failure.reason === "too-large";
|
|
17297
|
+
const hasPayloadDetails = failure.bytes !== void 0 && failure.byteLimit !== void 0 && failure.messageCount !== void 0 && failure.partCount !== void 0;
|
|
17298
|
+
const payloadDetails = hasPayloadDetails ? `; payload bytes=${failure.bytes} limit=${failure.byteLimit} messages=${failure.messageCount} parts=${failure.partCount}` : "";
|
|
17299
|
+
const safeMessage = !hasPayloadDetails && failure.message ? `: ${redactDiagnosticText(failure.message).slice(0, 200)}` : "";
|
|
17300
|
+
logSessionArchiveActivity(state, phase, {
|
|
17301
|
+
type: "info",
|
|
17302
|
+
level: "warn",
|
|
17303
|
+
message: `Session archive: session ${failure.sessionId} ` + (permanentSizeFailure ? "failed permanently (too-large payload)" : `failed (${failure.outcome}`) + (permanentSizeFailure ? "" : failure.reason ? `, reason=${failure.reason})` : ")") + payloadDetails + (failure.exitCode !== void 0 ? `; child exit code=${failure.exitCode}` : "") + safeMessage,
|
|
17304
|
+
metadata: {
|
|
17305
|
+
outcome: failure.outcome,
|
|
17306
|
+
session_id: failure.sessionId,
|
|
17307
|
+
archived: failure.archived,
|
|
17308
|
+
...failure.reason !== void 0 ? { reason: failure.reason } : {},
|
|
17309
|
+
...failure.exitCode !== void 0 ? { child_exit_code: failure.exitCode } : {},
|
|
17310
|
+
...hasPayloadDetails ? {
|
|
17311
|
+
payload_bytes: failure.bytes,
|
|
17312
|
+
payload_limit_bytes: failure.byteLimit,
|
|
17313
|
+
message_count: failure.messageCount,
|
|
17314
|
+
part_count: failure.partCount
|
|
17315
|
+
} : {}
|
|
17316
|
+
}
|
|
17317
|
+
});
|
|
17318
|
+
}
|
|
17319
|
+
if (summary.unmappedIds.length > 0) {
|
|
17320
|
+
logSessionArchiveActivity(state, phase, {
|
|
17321
|
+
type: "info",
|
|
17322
|
+
level: "warn",
|
|
17323
|
+
message: `Session archive: ${summary.unmappedIds.length} unmapped session(s): ` + summary.unmappedIds.join(", "),
|
|
17324
|
+
metadata: {
|
|
17325
|
+
outcome: "unmapped-sessions",
|
|
17326
|
+
sessions_unmapped: summary.unmappedIds.length
|
|
17327
|
+
}
|
|
17328
|
+
});
|
|
17329
|
+
}
|
|
17330
|
+
if (summary.conflictedIds.length > 0) {
|
|
17331
|
+
logSessionArchiveActivity(state, phase, {
|
|
17332
|
+
type: "info",
|
|
17333
|
+
level: "warn",
|
|
17334
|
+
message: `Session archive: ${summary.conflictedIds.length} session(s) have conflicting conversation identities: ${summary.conflictedIds.join(", ")}`,
|
|
17335
|
+
metadata: {
|
|
17336
|
+
outcome: "conflicted-session-bindings",
|
|
17337
|
+
sessions_conflicted: summary.conflicts
|
|
17338
|
+
}
|
|
17339
|
+
});
|
|
17340
|
+
}
|
|
17341
|
+
}
|
|
15847
17342
|
async function drainSessionDeletionReports(state) {
|
|
15848
17343
|
const path = sessionDeletionReportPath(homedir8(), process.env);
|
|
15849
17344
|
let delivered = 0;
|
|
@@ -15941,6 +17436,7 @@ async function runSweep(state, driver, config) {
|
|
|
15941
17436
|
let failedOtherStatus = 0;
|
|
15942
17437
|
let failedThrew = 0;
|
|
15943
17438
|
let skippedNewlyActive = 0;
|
|
17439
|
+
let skippedArchive = 0;
|
|
15944
17440
|
let remaining = 0;
|
|
15945
17441
|
const deletionRecords = [];
|
|
15946
17442
|
let protectedNow = driver.protectedSessionIds();
|
|
@@ -15952,6 +17448,7 @@ async function runSweep(state, driver, config) {
|
|
|
15952
17448
|
const lastActivityById = new Map(
|
|
15953
17449
|
snapshots.map(({ id, lastActivityMs }) => [id, lastActivityMs])
|
|
15954
17450
|
);
|
|
17451
|
+
const sessionById = new Map(sessions.map((session) => [session.id, session]));
|
|
15955
17452
|
const toDelete = selectSessionsToDelete(snapshots, {
|
|
15956
17453
|
maxAgeMs: config.maxAgeMs,
|
|
15957
17454
|
maxCount: config.maxCount,
|
|
@@ -15974,6 +17471,42 @@ async function runSweep(state, driver, config) {
|
|
|
15974
17471
|
});
|
|
15975
17472
|
continue;
|
|
15976
17473
|
}
|
|
17474
|
+
if (state.sessionArchiveConfig.enabled) {
|
|
17475
|
+
const candidate = sessionById.get(id);
|
|
17476
|
+
const archiveConfig = state.sessionArchiveConfig;
|
|
17477
|
+
const archiveBudgetMs = Math.min(
|
|
17478
|
+
archiveConfig.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
|
|
17479
|
+
MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
|
|
17480
|
+
);
|
|
17481
|
+
const archived = candidate ? await runSessionArchiveTask(
|
|
17482
|
+
state,
|
|
17483
|
+
"sweep",
|
|
17484
|
+
archiveBudgetMs,
|
|
17485
|
+
(signal) => runSessionArchiveBatch(state, "sweep", [candidate], archiveBudgetMs, signal)
|
|
17486
|
+
) : null;
|
|
17487
|
+
if (!archived?.archivedSessionIds.includes(id)) {
|
|
17488
|
+
skippedArchive++;
|
|
17489
|
+
logSessionArchiveActivity(state, "sweep", {
|
|
17490
|
+
type: "info",
|
|
17491
|
+
level: "warn",
|
|
17492
|
+
message: `Session cleanup: retaining session ${id}; deletion is held back pending successful archival`,
|
|
17493
|
+
metadata: {
|
|
17494
|
+
outcome: "deletion-held-for-archive",
|
|
17495
|
+
session_id: id
|
|
17496
|
+
}
|
|
17497
|
+
});
|
|
17498
|
+
continue;
|
|
17499
|
+
}
|
|
17500
|
+
protectedNow = driver.protectedSessionIds();
|
|
17501
|
+
if (protectedNow.has(id)) {
|
|
17502
|
+
skippedNewlyActive++;
|
|
17503
|
+
logActivity(state, {
|
|
17504
|
+
type: "info",
|
|
17505
|
+
message: `Session cleanup: skipping ${id} \u2014 became active/bound during archival (${mode})`
|
|
17506
|
+
});
|
|
17507
|
+
continue;
|
|
17508
|
+
}
|
|
17509
|
+
}
|
|
15977
17510
|
const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
|
|
15978
17511
|
if (outcome.ok) {
|
|
15979
17512
|
deleted++;
|
|
@@ -16037,10 +17570,11 @@ async function runSweep(state, driver, config) {
|
|
|
16037
17570
|
}
|
|
16038
17571
|
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
16039
17572
|
const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
|
|
17573
|
+
const archiveHeldNote = skippedArchive > 0 ? `, archive held ${skippedArchive}` : "";
|
|
16040
17574
|
const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
|
|
16041
17575
|
logActivity(state, {
|
|
16042
17576
|
type: "info",
|
|
16043
|
-
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
|
|
17577
|
+
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${archiveHeldNote}${remainingNote} (${mode})`
|
|
16044
17578
|
});
|
|
16045
17579
|
}
|
|
16046
17580
|
if (deletionRecords.length > 0) {
|
|
@@ -16078,6 +17612,7 @@ async function runSweep(state, driver, config) {
|
|
|
16078
17612
|
sessions_failed_other: failedOtherStatus,
|
|
16079
17613
|
sessions_failed_threw: failedThrew,
|
|
16080
17614
|
sessions_skipped_active: skippedNewlyActive,
|
|
17615
|
+
sessions_archive_held: skippedArchive,
|
|
16081
17616
|
sessions_remaining: remaining,
|
|
16082
17617
|
...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
|
|
16083
17618
|
};
|
|
@@ -16209,6 +17744,213 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
16209
17744
|
);
|
|
16210
17745
|
state.sessionCleanupTimers.push(interval, firstSweep);
|
|
16211
17746
|
}
|
|
17747
|
+
function runSessionArchiveTask(state, phase, budgetMs, work) {
|
|
17748
|
+
if (state.sessionArchiveInFlight) {
|
|
17749
|
+
logSessionArchiveActivity(state, phase, {
|
|
17750
|
+
type: "info",
|
|
17751
|
+
level: "debug",
|
|
17752
|
+
message: "Session archive skipped \u2014 previous archive run still in progress",
|
|
17753
|
+
metadata: { outcome: "skipped-in-flight" }
|
|
17754
|
+
});
|
|
17755
|
+
return Promise.resolve(null);
|
|
17756
|
+
}
|
|
17757
|
+
const controller = new AbortController();
|
|
17758
|
+
state.sessionArchiveAbortController = controller;
|
|
17759
|
+
const timeout = setTimeout(() => {
|
|
17760
|
+
controller.abort();
|
|
17761
|
+
logSessionArchiveActivity(state, phase, {
|
|
17762
|
+
type: "info",
|
|
17763
|
+
level: "warn",
|
|
17764
|
+
message: `Session archive exceeded ${budgetMs}ms; remaining sessions were not confirmed archived`,
|
|
17765
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17766
|
+
});
|
|
17767
|
+
}, budgetMs);
|
|
17768
|
+
const task = Promise.resolve().then(() => work(controller.signal)).catch((error2) => {
|
|
17769
|
+
logSessionArchiveActivity(state, phase, {
|
|
17770
|
+
type: "info",
|
|
17771
|
+
level: "warn",
|
|
17772
|
+
message: `Session archive run failed: ${redactDiagnosticText(error2 instanceof Error ? error2.message : String(error2)).slice(0, 200)}`,
|
|
17773
|
+
metadata: { outcome: "archive-run-failed" }
|
|
17774
|
+
});
|
|
17775
|
+
return null;
|
|
17776
|
+
}).finally(() => {
|
|
17777
|
+
clearTimeout(timeout);
|
|
17778
|
+
if (state.sessionArchiveInFlight === task) {
|
|
17779
|
+
state.sessionArchiveInFlight = null;
|
|
17780
|
+
state.sessionArchiveAbortController = null;
|
|
17781
|
+
}
|
|
17782
|
+
});
|
|
17783
|
+
state.sessionArchiveInFlight = task;
|
|
17784
|
+
return task;
|
|
17785
|
+
}
|
|
17786
|
+
async function runSessionArchiveBatch(state, phase, sessions, budgetMs, signal) {
|
|
17787
|
+
const config = state.sessionArchiveConfig;
|
|
17788
|
+
const client = state.opencodeClient;
|
|
17789
|
+
const watermarkPath = state.sessionArchiveWatermarkPath;
|
|
17790
|
+
if (!client || !config.keyPrefix || !watermarkPath || !state.agentId) {
|
|
17791
|
+
logSessionArchiveActivity(state, phase, {
|
|
17792
|
+
type: "info",
|
|
17793
|
+
level: "warn",
|
|
17794
|
+
message: "Session archive skipped because runner identity or archive configuration is unavailable",
|
|
17795
|
+
metadata: { outcome: "archive-configuration-unavailable" }
|
|
17796
|
+
});
|
|
17797
|
+
return null;
|
|
17798
|
+
}
|
|
17799
|
+
const bindings = readSessionBindings(
|
|
17800
|
+
sessionBindingLedgerPath(homedir8(), process.env),
|
|
17801
|
+
(message) => logSessionArchiveActivity(state, phase, {
|
|
17802
|
+
type: "info",
|
|
17803
|
+
level: "warn",
|
|
17804
|
+
message: redactDiagnosticText(message),
|
|
17805
|
+
metadata: { outcome: "binding-ledger-read-failed" }
|
|
17806
|
+
})
|
|
17807
|
+
);
|
|
17808
|
+
const reconciliation = await fetchSessionBindingProjection({
|
|
17809
|
+
apiUrl: getApiUrlConfig(),
|
|
17810
|
+
runnerId: state.agentId,
|
|
17811
|
+
authHeader: state.authHeader,
|
|
17812
|
+
sessions,
|
|
17813
|
+
signal
|
|
17814
|
+
});
|
|
17815
|
+
if (!reconciliation.complete) {
|
|
17816
|
+
const failure = reconciliation.failures[0];
|
|
17817
|
+
const detail = failure.kind === "http" ? `HTTP ${failure.status} for chunk ${failure.chunk}` : failure.kind === "network" ? `network failure for chunk ${failure.chunk}: ${redactDiagnosticText(failure.message ?? "unknown error").slice(0, 200)}` : `invalid response for chunk ${failure.chunk}`;
|
|
17818
|
+
logSessionArchiveActivity(state, phase, {
|
|
17819
|
+
type: "info",
|
|
17820
|
+
level: "warn",
|
|
17821
|
+
message: `Session archive: binding reconciliation incomplete for runner ${state.agentId}; ${reconciliation.failures.length} chunk(s) failed (${detail}), unresolved sessions will not replace existing archives`,
|
|
17822
|
+
metadata: {
|
|
17823
|
+
outcome: "binding-reconciliation-incomplete",
|
|
17824
|
+
runner_id: state.agentId,
|
|
17825
|
+
failed_chunks: reconciliation.failures.length,
|
|
17826
|
+
first_failed_chunk: failure.chunk,
|
|
17827
|
+
first_failure_kind: failure.kind,
|
|
17828
|
+
...failure.status !== void 0 ? { http_status: failure.status } : {}
|
|
17829
|
+
}
|
|
17830
|
+
});
|
|
17831
|
+
}
|
|
17832
|
+
const summary = await runSessionArchive({
|
|
17833
|
+
sessions,
|
|
17834
|
+
deps: {
|
|
17835
|
+
client,
|
|
17836
|
+
runnerId: state.agentId,
|
|
17837
|
+
keyPrefix: config.keyPrefix,
|
|
17838
|
+
bindings,
|
|
17839
|
+
projection: reconciliation.projection,
|
|
17840
|
+
reconciliationComplete: reconciliation.complete,
|
|
17841
|
+
watermarks: state.sessionArchiveWatermarks,
|
|
17842
|
+
watermarkPath,
|
|
17843
|
+
signal,
|
|
17844
|
+
upload: uploadSessionArchive,
|
|
17845
|
+
exportMessages: exportSessionMessages,
|
|
17846
|
+
recordWatermark: recordUpload
|
|
17847
|
+
},
|
|
17848
|
+
budgetMs
|
|
17849
|
+
});
|
|
17850
|
+
logSessionArchiveSummary(state, phase, summary);
|
|
17851
|
+
return summary;
|
|
17852
|
+
}
|
|
17853
|
+
async function runSessionArchiveFleet(state, phase, budgetMs, signal) {
|
|
17854
|
+
const startedAt = Date.now();
|
|
17855
|
+
const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0, signal);
|
|
17856
|
+
if (enumeration.sessions === null) {
|
|
17857
|
+
const interrupted = signal.aborted;
|
|
17858
|
+
logSessionArchiveActivity(state, phase, {
|
|
17859
|
+
type: "info",
|
|
17860
|
+
level: "warn",
|
|
17861
|
+
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`,
|
|
17862
|
+
metadata: {
|
|
17863
|
+
outcome: interrupted ? "enumeration-interrupted" : "enumeration-refused",
|
|
17864
|
+
...!interrupted ? { reason: enumeration.reason } : {}
|
|
17865
|
+
}
|
|
17866
|
+
});
|
|
17867
|
+
return null;
|
|
17868
|
+
}
|
|
17869
|
+
return runSessionArchiveBatch(
|
|
17870
|
+
state,
|
|
17871
|
+
phase,
|
|
17872
|
+
enumeration.sessions,
|
|
17873
|
+
Math.max(0, budgetMs - (Date.now() - startedAt)),
|
|
17874
|
+
signal
|
|
17875
|
+
);
|
|
17876
|
+
}
|
|
17877
|
+
async function runSessionArchiveShutdownFlush(state, budgetMs) {
|
|
17878
|
+
const deadline = Date.now() + budgetMs;
|
|
17879
|
+
const previousRun = state.sessionArchiveInFlight;
|
|
17880
|
+
if (previousRun) {
|
|
17881
|
+
state.sessionArchiveAbortController?.abort(
|
|
17882
|
+
new Error("graceful shutdown archive flush requested")
|
|
17883
|
+
);
|
|
17884
|
+
let timeout;
|
|
17885
|
+
const settled = await Promise.race([
|
|
17886
|
+
previousRun.then(() => true),
|
|
17887
|
+
new Promise((resolve5) => {
|
|
17888
|
+
timeout = setTimeout(() => resolve5(false), budgetMs);
|
|
17889
|
+
})
|
|
17890
|
+
]);
|
|
17891
|
+
if (timeout) clearTimeout(timeout);
|
|
17892
|
+
if (!settled) {
|
|
17893
|
+
state.sessionArchiveAbortController?.abort(new Error("session archive flush budget expired"));
|
|
17894
|
+
logSessionArchiveActivity(state, "shutdown", {
|
|
17895
|
+
type: "info",
|
|
17896
|
+
level: "warn",
|
|
17897
|
+
message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
|
|
17898
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17899
|
+
});
|
|
17900
|
+
return null;
|
|
17901
|
+
}
|
|
17902
|
+
}
|
|
17903
|
+
const remainingMs = Math.floor(deadline - Date.now());
|
|
17904
|
+
if (remainingMs <= 0) {
|
|
17905
|
+
logSessionArchiveActivity(state, "shutdown", {
|
|
17906
|
+
type: "info",
|
|
17907
|
+
level: "warn",
|
|
17908
|
+
message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
|
|
17909
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17910
|
+
});
|
|
17911
|
+
return null;
|
|
17912
|
+
}
|
|
17913
|
+
return runSessionArchiveTask(
|
|
17914
|
+
state,
|
|
17915
|
+
"shutdown",
|
|
17916
|
+
remainingMs,
|
|
17917
|
+
(signal) => runSessionArchiveFleet(state, "shutdown", remainingMs, signal)
|
|
17918
|
+
);
|
|
17919
|
+
}
|
|
17920
|
+
function scheduleSessionArchive(state) {
|
|
17921
|
+
const config = state.sessionArchiveConfig;
|
|
17922
|
+
if (!config.enabled) return;
|
|
17923
|
+
state.sessionArchiveWatermarkPath = sessionArchiveWatermarkPath(homedir8(), process.env);
|
|
17924
|
+
state.sessionArchiveWatermarks = readWatermarks(
|
|
17925
|
+
state.sessionArchiveWatermarkPath,
|
|
17926
|
+
(message) => logSessionArchiveActivity(state, "startup", {
|
|
17927
|
+
type: "info",
|
|
17928
|
+
level: "warn",
|
|
17929
|
+
message: redactDiagnosticText(message),
|
|
17930
|
+
metadata: { outcome: "watermark-read-failed" }
|
|
17931
|
+
})
|
|
17932
|
+
);
|
|
17933
|
+
logSessionArchiveActivity(state, "startup", {
|
|
17934
|
+
type: "info",
|
|
17935
|
+
message: `Session archive enabled (interval=${config.intervalMs}ms)`,
|
|
17936
|
+
metadata: { outcome: "armed", interval_ms: config.intervalMs }
|
|
17937
|
+
});
|
|
17938
|
+
const cycleBudgetMs = Math.min(
|
|
17939
|
+
config.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
|
|
17940
|
+
MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
|
|
17941
|
+
);
|
|
17942
|
+
const run2 = (phase = "scheduled") => {
|
|
17943
|
+
void runSessionArchiveTask(
|
|
17944
|
+
state,
|
|
17945
|
+
phase,
|
|
17946
|
+
cycleBudgetMs,
|
|
17947
|
+
(signal) => runSessionArchiveFleet(state, phase, cycleBudgetMs, signal)
|
|
17948
|
+
);
|
|
17949
|
+
};
|
|
17950
|
+
const interval = setInterval(run2, config.intervalMs);
|
|
17951
|
+
const firstRun = setTimeout(() => run2(), SESSION_ARCHIVE_FIRST_RUN_MS);
|
|
17952
|
+
state.sessionArchiveTimers.push(interval, firstRun);
|
|
17953
|
+
}
|
|
16212
17954
|
function scheduleUsageReporting(state, params) {
|
|
16213
17955
|
const { mode, warnings } = params.resolved;
|
|
16214
17956
|
for (const warning2 of warnings) {
|
|
@@ -16483,6 +18225,12 @@ async function cleanup(state, opts = {}) {
|
|
|
16483
18225
|
clearTimeout(timer);
|
|
16484
18226
|
}
|
|
16485
18227
|
state.sessionCleanupTimers = [];
|
|
18228
|
+
for (const timer of state.sessionArchiveTimers) {
|
|
18229
|
+
clearInterval(timer);
|
|
18230
|
+
clearTimeout(timer);
|
|
18231
|
+
}
|
|
18232
|
+
state.sessionArchiveTimers = [];
|
|
18233
|
+
state.sessionArchiveAbortController?.abort(new Error("runner cleanup started"));
|
|
16486
18234
|
state.stopOpenCodeLogTail?.();
|
|
16487
18235
|
state.stopOpenCodeLogTail = null;
|
|
16488
18236
|
if (state.claudeUsageTimer) {
|
|
@@ -16550,6 +18298,15 @@ async function cleanup(state, opts = {}) {
|
|
|
16550
18298
|
await timeShutdownPhase(state, durations, "tunnel_close", () => connection.close());
|
|
16551
18299
|
state.connection = null;
|
|
16552
18300
|
}
|
|
18301
|
+
if (opts.graceful && state.sessionArchiveConfig.enabled) {
|
|
18302
|
+
const budgetMs = state.sessionArchiveConfig.flushBudgetMs;
|
|
18303
|
+
await timeShutdownPhase(
|
|
18304
|
+
state,
|
|
18305
|
+
durations,
|
|
18306
|
+
"session_archive_flush",
|
|
18307
|
+
() => runSessionArchiveShutdownFlush(state, budgetMs)
|
|
18308
|
+
);
|
|
18309
|
+
}
|
|
16553
18310
|
if (state.opencodeProcess) {
|
|
16554
18311
|
const opencodeProcess = state.opencodeProcess;
|
|
16555
18312
|
const result = await timeShutdownPhase(
|
|
@@ -16621,9 +18378,11 @@ async function run(options) {
|
|
|
16621
18378
|
const interactive = isInteractive(options.json);
|
|
16622
18379
|
let logLevel;
|
|
16623
18380
|
let fileSyncDirectories;
|
|
18381
|
+
let fileSyncExactFiles;
|
|
16624
18382
|
try {
|
|
16625
18383
|
logLevel = resolveLogLevel(options);
|
|
16626
18384
|
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir8());
|
|
18385
|
+
fileSyncExactFiles = resolveFileSyncExactFiles(options.enableFileSyncToExactFile, homedir8());
|
|
16627
18386
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
16628
18387
|
throw new Error(
|
|
16629
18388
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -16671,6 +18430,18 @@ async function run(options) {
|
|
|
16671
18430
|
messageCount: 0,
|
|
16672
18431
|
lastProxiedActivityAt: null,
|
|
16673
18432
|
sessionCleanupTimers: [],
|
|
18433
|
+
sessionArchiveConfig: resolveSessionArchiveConfig(
|
|
18434
|
+
{
|
|
18435
|
+
enabled: options.sessionArchive,
|
|
18436
|
+
interval: options.sessionArchiveInterval
|
|
18437
|
+
},
|
|
18438
|
+
process.env
|
|
18439
|
+
),
|
|
18440
|
+
sessionArchiveTimers: [],
|
|
18441
|
+
sessionArchiveWatermarks: /* @__PURE__ */ new Map(),
|
|
18442
|
+
sessionArchiveWatermarkPath: null,
|
|
18443
|
+
sessionArchiveInFlight: null,
|
|
18444
|
+
sessionArchiveAbortController: null,
|
|
16674
18445
|
sessionDeleteFailures: /* @__PURE__ */ new Map(),
|
|
16675
18446
|
claudeUsageTimer: null,
|
|
16676
18447
|
claudeUsageRearm: null,
|
|
@@ -16684,6 +18455,14 @@ async function run(options) {
|
|
|
16684
18455
|
};
|
|
16685
18456
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
16686
18457
|
setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
|
|
18458
|
+
for (const warning2 of state.sessionArchiveConfig.warnings) {
|
|
18459
|
+
logSessionArchiveActivity(state, "startup", {
|
|
18460
|
+
type: "info",
|
|
18461
|
+
level: "warn",
|
|
18462
|
+
message: `Session archive: ${warning2}`,
|
|
18463
|
+
metadata: { outcome: "configuration-warning" }
|
|
18464
|
+
});
|
|
18465
|
+
}
|
|
16687
18466
|
if (options.credentialSyncMarker) {
|
|
16688
18467
|
state.credentialSync = createCredentialSync({
|
|
16689
18468
|
markerPath: options.credentialSyncMarker,
|
|
@@ -16699,7 +18478,11 @@ async function run(options) {
|
|
|
16699
18478
|
}
|
|
16700
18479
|
if (fileSyncDirectories.length > 0) {
|
|
16701
18480
|
log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
|
|
16702
|
-
}
|
|
18481
|
+
}
|
|
18482
|
+
if (fileSyncExactFiles.length > 0) {
|
|
18483
|
+
log2(state, `File sync exact files enabled for: ${fileSyncExactFiles.join(", ")}`);
|
|
18484
|
+
}
|
|
18485
|
+
if (fileSyncDirectories.length === 0 && fileSyncExactFiles.length === 0) {
|
|
16703
18486
|
log2(state, "File sync is disabled (no --enable-file-sync-to given)", "debug");
|
|
16704
18487
|
}
|
|
16705
18488
|
if (!options.runner && options.agent) {
|
|
@@ -17001,13 +18784,75 @@ async function run(options) {
|
|
|
17001
18784
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
17002
18785
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
17003
18786
|
}
|
|
17004
|
-
const { version:
|
|
17005
|
-
options,
|
|
17006
|
-
process.env
|
|
17007
|
-
);
|
|
18787
|
+
const { version: opencodeVersionSelection, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(options, process.env);
|
|
17008
18788
|
for (const warning2 of opencodeVersionWarnings) {
|
|
17009
18789
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
17010
18790
|
}
|
|
18791
|
+
let opencodeMajor;
|
|
18792
|
+
let opencodeV2Command;
|
|
18793
|
+
let opencodeDetectionSource;
|
|
18794
|
+
let opencodeDetectionLevel = "info";
|
|
18795
|
+
let opencodeDetectionMessage;
|
|
18796
|
+
if (opencodeVersionSelection === "v1") {
|
|
18797
|
+
opencodeMajor = "v1";
|
|
18798
|
+
opencodeDetectionSource = "explicit";
|
|
18799
|
+
opencodeDetectionMessage = "OpenCode V1 was selected explicitly.";
|
|
18800
|
+
} else if (opencodeVersionSelection === "v2") {
|
|
18801
|
+
const installed = resolveInstalledOpenCode();
|
|
18802
|
+
opencodeMajor = "v2";
|
|
18803
|
+
opencodeDetectionSource = "explicit";
|
|
18804
|
+
if (installed.kind === "installed" && installed.major === "v2") {
|
|
18805
|
+
opencodeV2Command = installed.command;
|
|
18806
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; using ${installed.command}${installed.version ? ` (${installed.version})` : ""}.`;
|
|
18807
|
+
} else if (installed.kind === "unknown") {
|
|
18808
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported ${JSON.stringify(installed.rawVersion)}, but no V2 command was resolved.`;
|
|
18809
|
+
} else if (installed.kind === "installed") {
|
|
18810
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported V1 ${installed.version ?? "with an unknown version"}, so no V2 command was resolved.`;
|
|
18811
|
+
} else {
|
|
18812
|
+
opencodeDetectionMessage = "OpenCode V2 was selected explicitly, but no V2 command was resolved.";
|
|
18813
|
+
}
|
|
18814
|
+
} else {
|
|
18815
|
+
const running = await classifyRunningOpenCode(state.port);
|
|
18816
|
+
if (running.kind === "v1") {
|
|
18817
|
+
opencodeMajor = "v1";
|
|
18818
|
+
opencodeDetectionSource = "running-server";
|
|
18819
|
+
opencodeDetectionMessage = `OpenCode detection: found V1 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
|
|
18820
|
+
} else if (running.kind === "v2") {
|
|
18821
|
+
opencodeMajor = "v2";
|
|
18822
|
+
opencodeDetectionSource = "running-server";
|
|
18823
|
+
opencodeDetectionMessage = `OpenCode detection: found V2 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
|
|
18824
|
+
} else if (running.kind === "authenticated-unknown") {
|
|
18825
|
+
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.`;
|
|
18826
|
+
logActivity(state, {
|
|
18827
|
+
type: "info",
|
|
18828
|
+
level: "warn",
|
|
18829
|
+
message: `OpenCode detection: authenticated-unknown on port ${state.port}; ${running.detail}. ${message}`
|
|
18830
|
+
});
|
|
18831
|
+
throw new Error(message);
|
|
18832
|
+
} else {
|
|
18833
|
+
const installed = resolveInstalledOpenCode();
|
|
18834
|
+
opencodeDetectionLevel = "warn";
|
|
18835
|
+
if (installed.kind === "installed") {
|
|
18836
|
+
opencodeMajor = installed.major;
|
|
18837
|
+
opencodeDetectionSource = "installed-binary";
|
|
18838
|
+
if (installed.major === "v2") opencodeV2Command = installed.command;
|
|
18839
|
+
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"}.`;
|
|
18840
|
+
} else if (installed.kind === "unknown") {
|
|
18841
|
+
opencodeMajor = "v1";
|
|
18842
|
+
opencodeDetectionSource = "fallback-v1";
|
|
18843
|
+
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.`;
|
|
18844
|
+
} else {
|
|
18845
|
+
opencodeMajor = "v1";
|
|
18846
|
+
opencodeDetectionSource = "fallback-v1";
|
|
18847
|
+
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.`;
|
|
18848
|
+
}
|
|
18849
|
+
}
|
|
18850
|
+
}
|
|
18851
|
+
logActivity(state, {
|
|
18852
|
+
type: "info",
|
|
18853
|
+
level: opencodeDetectionLevel,
|
|
18854
|
+
message: opencodeDetectionMessage
|
|
18855
|
+
});
|
|
17011
18856
|
const managedSetupOptions = {
|
|
17012
18857
|
agentId: state.agentId,
|
|
17013
18858
|
apiUrl: getApiUrlConfig(),
|
|
@@ -17028,7 +18873,7 @@ async function run(options) {
|
|
|
17028
18873
|
managedPreparation = managedDiscovery;
|
|
17029
18874
|
managedSetupOptions.log(`MANAGED-INFERENCE-NOT-READY: ${managedDiscovery.reason}`, "warn");
|
|
17030
18875
|
} else if (managedDiscovery.kind === "setup") {
|
|
17031
|
-
const managedHealth =
|
|
18876
|
+
const managedHealth = opencodeMajor === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
|
|
17032
18877
|
managedPreparation = await prepareManagedInferenceSetup(
|
|
17033
18878
|
managedSetupOptions,
|
|
17034
18879
|
managedDiscovery,
|
|
@@ -17053,13 +18898,14 @@ async function run(options) {
|
|
|
17053
18898
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
17054
18899
|
const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
|
|
17055
18900
|
try {
|
|
17056
|
-
const oc =
|
|
18901
|
+
const oc = opencodeMajor === "v2" ? await ensureOpenCode2Running({
|
|
17057
18902
|
port: state.port,
|
|
17058
18903
|
interactive: state.interactive,
|
|
17059
18904
|
agentId: state.agentId,
|
|
17060
18905
|
log: (message) => log2(state, message),
|
|
17061
18906
|
startTimeoutMs: opencodeStartTimeoutMs,
|
|
17062
|
-
inheritStdio: Boolean(options.opencodePidFile)
|
|
18907
|
+
inheritStdio: Boolean(options.opencodePidFile),
|
|
18908
|
+
command: opencodeV2Command
|
|
17063
18909
|
}) : await ensureOpenCodeRunning({
|
|
17064
18910
|
port: state.port,
|
|
17065
18911
|
interactive: state.interactive,
|
|
@@ -17071,7 +18917,7 @@ async function run(options) {
|
|
|
17071
18917
|
state.port = oc.port;
|
|
17072
18918
|
state.opencodeProcess = options.opencodePidFile ? null : oc.process;
|
|
17073
18919
|
state.opencodeVersion = oc.version;
|
|
17074
|
-
state.opencodeApiVersion =
|
|
18920
|
+
state.opencodeApiVersion = opencodeMajor;
|
|
17075
18921
|
let configuredPlugins = [];
|
|
17076
18922
|
let resolvedPlugins = [];
|
|
17077
18923
|
try {
|
|
@@ -17093,13 +18939,14 @@ async function run(options) {
|
|
|
17093
18939
|
metadata: {
|
|
17094
18940
|
opencode_version: state.opencodeVersion,
|
|
17095
18941
|
opencode_api_version: state.opencodeApiVersion,
|
|
18942
|
+
opencode_detection_source: opencodeDetectionSource,
|
|
17096
18943
|
configured_plugins: JSON.stringify(configuredPlugins),
|
|
17097
18944
|
resolved_plugins: JSON.stringify(resolvedPlugins)
|
|
17098
18945
|
},
|
|
17099
18946
|
source: "cli.runtime-provenance"
|
|
17100
18947
|
});
|
|
17101
18948
|
let opencodePassword = null;
|
|
17102
|
-
if (
|
|
18949
|
+
if (opencodeMajor === "v2" && "password" in oc) {
|
|
17103
18950
|
const value = oc.password;
|
|
17104
18951
|
if (typeof value === "string" || value === null) opencodePassword = value;
|
|
17105
18952
|
}
|
|
@@ -17112,7 +18959,7 @@ async function run(options) {
|
|
|
17112
18959
|
state.opencodeClient = openCodeClient;
|
|
17113
18960
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
17114
18961
|
try {
|
|
17115
|
-
|
|
18962
|
+
writeFileSync9(options.opencodePidFile, `${oc.process.pid}
|
|
17116
18963
|
`, { mode: 384 });
|
|
17117
18964
|
chmodSync3(options.opencodePidFile, 384);
|
|
17118
18965
|
} catch (error2) {
|
|
@@ -17222,7 +19069,7 @@ async function run(options) {
|
|
|
17222
19069
|
let existingPid;
|
|
17223
19070
|
if (existsSync3(options.litestreamPidFile)) {
|
|
17224
19071
|
try {
|
|
17225
|
-
const rawPid =
|
|
19072
|
+
const rawPid = readFileSync12(options.litestreamPidFile, "utf8").trim();
|
|
17226
19073
|
const parsedPid = Number(rawPid);
|
|
17227
19074
|
if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
|
|
17228
19075
|
existingPid = parsedPid;
|
|
@@ -17259,7 +19106,7 @@ async function run(options) {
|
|
|
17259
19106
|
});
|
|
17260
19107
|
try {
|
|
17261
19108
|
if (litestreamProcess.pid !== void 0) {
|
|
17262
|
-
|
|
19109
|
+
writeFileSync9(options.litestreamPidFile, `${litestreamProcess.pid}
|
|
17263
19110
|
`, {
|
|
17264
19111
|
mode: 384
|
|
17265
19112
|
});
|
|
@@ -17309,6 +19156,15 @@ async function run(options) {
|
|
|
17309
19156
|
log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
|
|
17310
19157
|
}
|
|
17311
19158
|
const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
|
|
19159
|
+
const recordBinding = state.sessionArchiveConfig.enabled ? (conversationId, sessionId) => appendSessionBinding(
|
|
19160
|
+
sessionBindingLedgerPath(homedir8(), process.env),
|
|
19161
|
+
{
|
|
19162
|
+
conversation_id: conversationId,
|
|
19163
|
+
opencode_session_id: sessionId,
|
|
19164
|
+
observed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
19165
|
+
},
|
|
19166
|
+
(message) => logActivity(state, { type: "info", level: "warn", message })
|
|
19167
|
+
) : void 0;
|
|
17312
19168
|
const channelDriver = new ChannelDriver({
|
|
17313
19169
|
agentId: state.agentId,
|
|
17314
19170
|
port: state.port,
|
|
@@ -17318,9 +19174,11 @@ async function run(options) {
|
|
|
17318
19174
|
conversationFilter: state.conversationFilter,
|
|
17319
19175
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
17320
19176
|
sessionStoreHealth: state.sessionStoreHealth,
|
|
17321
|
-
|
|
17322
|
-
//
|
|
19177
|
+
recordBinding,
|
|
19178
|
+
// #559: the file-sync allow-lists. Empty ⇒ pending files are REJECTED with
|
|
19179
|
+
// `file_sync_disabled` on the ack, not silently ignored.
|
|
17323
19180
|
fileSyncDirectories,
|
|
19181
|
+
fileSyncExactFiles,
|
|
17324
19182
|
homeDir: homedir8(),
|
|
17325
19183
|
maxActiveSessions,
|
|
17326
19184
|
log: (entry) => (
|
|
@@ -17496,6 +19354,7 @@ async function run(options) {
|
|
|
17496
19354
|
}
|
|
17497
19355
|
if (!state.running) return;
|
|
17498
19356
|
scheduleSessionCleanup(state, channelDriver, options);
|
|
19357
|
+
scheduleSessionArchive(state);
|
|
17499
19358
|
state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
|
|
17500
19359
|
state.openaiUsageRearm = scheduleUsageReporting(state, {
|
|
17501
19360
|
label: "OpenAI",
|
|
@@ -17591,8 +19450,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17591
19450
|
"--opencode-start-timeout <seconds>",
|
|
17592
19451
|
"Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
|
|
17593
19452
|
).option(
|
|
17594
|
-
"--opencode-version <v1|v2>",
|
|
17595
|
-
"
|
|
19453
|
+
"--opencode-version <v1|v2|auto>",
|
|
19454
|
+
"OpenCode major version: auto (default) detects the running server, then installed binary; v1 and v2 skip server-protocol detection. Env: EVIDENT_OPENCODE_VERSION"
|
|
17596
19455
|
).option("--json", "Output in JSON format").option(
|
|
17597
19456
|
"--session-cleanup-max-age <duration>",
|
|
17598
19457
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
@@ -17605,6 +19464,12 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17605
19464
|
).option(
|
|
17606
19465
|
"--session-cleanup-interval <duration>",
|
|
17607
19466
|
"How often the cleanup sweep runs (default: 1h). Env: EVIDENT_SESSION_CLEANUP_INTERVAL"
|
|
19467
|
+
).option(
|
|
19468
|
+
"--session-archive",
|
|
19469
|
+
"Upload full OpenCode transcripts to the runner archive (off by default). Env: EVIDENT_SESSION_ARCHIVE"
|
|
19470
|
+
).option(
|
|
19471
|
+
"--session-archive-interval <duration>",
|
|
19472
|
+
"How often session archives are uploaded (default: 1h). Env: EVIDENT_SESSION_ARCHIVE_INTERVAL"
|
|
17608
19473
|
).option(
|
|
17609
19474
|
"--claude-usage-reporting <mode>",
|
|
17610
19475
|
"Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
@@ -17619,6 +19484,11 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17619
19484
|
"Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.",
|
|
17620
19485
|
(value, previous) => previous.concat([value]),
|
|
17621
19486
|
[]
|
|
19487
|
+
).option(
|
|
19488
|
+
"--enable-file-sync-to-exact-file <path>",
|
|
19489
|
+
"Allow Evident to write files to exactly this path (repeatable).",
|
|
19490
|
+
(value, previous) => previous.concat([value]),
|
|
19491
|
+
[]
|
|
17622
19492
|
).option(
|
|
17623
19493
|
"--tunnel-ready-file <path>",
|
|
17624
19494
|
"Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
|
|
@@ -17671,6 +19541,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17671
19541
|
sessionCleanupMaxCount: options.sessionCleanupMaxCount,
|
|
17672
19542
|
maxActiveSessions: options.maxActiveSessions,
|
|
17673
19543
|
sessionCleanupInterval: options.sessionCleanupInterval,
|
|
19544
|
+
sessionArchive: options.sessionArchive,
|
|
19545
|
+
sessionArchiveInterval: options.sessionArchiveInterval,
|
|
17674
19546
|
// Raw string — the resolver in run.ts single-sources parsing
|
|
17675
19547
|
// (resolveClaudeUsageReportingMode).
|
|
17676
19548
|
claudeUsageReporting: options.claudeUsageReporting,
|
|
@@ -17681,6 +19553,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17681
19553
|
// Raw values — expansion/validation is single-sourced in run.ts's
|
|
17682
19554
|
// resolveFileSyncDirectories.
|
|
17683
19555
|
enableFileSyncTo: options.enableFileSyncTo,
|
|
19556
|
+
// Raw values — expansion/validation is single-sourced in run.ts's
|
|
19557
|
+
// resolveFileSyncExactFiles.
|
|
19558
|
+
enableFileSyncToExactFile: options.enableFileSyncToExactFile,
|
|
17684
19559
|
tunnelReadyFile: options.tunnelReadyFile,
|
|
17685
19560
|
litestreamConfig: options.litestreamConfig,
|
|
17686
19561
|
opencodePidFile: options.opencodePidFile,
|