@evident-ai/cli 3.6.1-dev.4db2683 → 3.6.1-dev.4f2b317
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 +2057 -498
- 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
|
|
@@ -1445,7 +1445,7 @@ import ora4 from "ora";
|
|
|
1445
1445
|
import { select as select4 } from "@inquirer/prompts";
|
|
1446
1446
|
|
|
1447
1447
|
// src/lib/telemetry.ts
|
|
1448
|
-
var CLI_VERSION = (true ? "3.6.1-dev.
|
|
1448
|
+
var CLI_VERSION = (true ? "3.6.1-dev.4f2b317" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1449
1449
|
function getCliVersion() {
|
|
1450
1450
|
return CLI_VERSION;
|
|
1451
1451
|
}
|
|
@@ -2195,6 +2195,7 @@ var FORWARDED_LEVELS_BY_SOURCE = {
|
|
|
2195
2195
|
"cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
|
|
2196
2196
|
opencode: /* @__PURE__ */ new Set(["warn", "error"]),
|
|
2197
2197
|
"cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"]),
|
|
2198
|
+
"cli.session-archive": /* @__PURE__ */ new Set(["info", "warn", "error"]),
|
|
2198
2199
|
"cli.runtime-provenance": /* @__PURE__ */ new Set(["info", "warn", "error"])
|
|
2199
2200
|
};
|
|
2200
2201
|
var SEVERITY_BY_LEVEL = {
|
|
@@ -2757,6 +2758,943 @@ function boundedError(error2) {
|
|
|
2757
2758
|
return message.slice(0, MAX_DIAGNOSTIC_LENGTH);
|
|
2758
2759
|
}
|
|
2759
2760
|
|
|
2761
|
+
// src/lib/opencode/session-binding-ledger.ts
|
|
2762
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2763
|
+
import { dirname as dirname5, join as join5 } from "node:path";
|
|
2764
|
+
var SESSION_BINDING_LEDGER_MAX_RECORDS = 1e3;
|
|
2765
|
+
var MAX_DIAGNOSTIC_LENGTH2 = 200;
|
|
2766
|
+
function sessionBindingLedgerPath(homeDir, env) {
|
|
2767
|
+
const override = env.EVIDENT_SESSION_BINDING_LEDGER?.trim();
|
|
2768
|
+
return override || join5(homeDir, ".local", "state", "evident", "session-bindings.jsonl");
|
|
2769
|
+
}
|
|
2770
|
+
function appendSessionBinding(path, record, log3) {
|
|
2771
|
+
mkdirSync2(dirname5(path), { recursive: true });
|
|
2772
|
+
const current = readSessionBindingRecords(path, log3);
|
|
2773
|
+
if (current.readError !== null) {
|
|
2774
|
+
throw new Error(`cannot enforce the ledger limit after reading ${path}: ${current.readError}`);
|
|
2775
|
+
}
|
|
2776
|
+
if (current.records.some(
|
|
2777
|
+
(existing) => existing.conversation_id === record.conversation_id && existing.opencode_session_id === record.opencode_session_id
|
|
2778
|
+
)) {
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
const kept = [...current.records, record].slice(-SESSION_BINDING_LEDGER_MAX_RECORDS);
|
|
2782
|
+
if (kept.length > current.records.length) {
|
|
2783
|
+
appendFileSync2(path, `${JSON.stringify(record)}
|
|
2784
|
+
`, "utf8");
|
|
2785
|
+
} else {
|
|
2786
|
+
writeFileSync2(path, `${kept.map((entry) => JSON.stringify(entry)).join("\n")}
|
|
2787
|
+
`, "utf8");
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
function readSessionBindings(path, log3) {
|
|
2791
|
+
const result = readSessionBindingRecords(path, log3);
|
|
2792
|
+
if (result.readError !== null) {
|
|
2793
|
+
log3(`[session-binding-ledger] could not read ${path}: ${result.readError}`);
|
|
2794
|
+
}
|
|
2795
|
+
return new Map(
|
|
2796
|
+
result.records.map((record) => [record.opencode_session_id, record.conversation_id])
|
|
2797
|
+
);
|
|
2798
|
+
}
|
|
2799
|
+
function readSessionBindingRecords(path, log3) {
|
|
2800
|
+
let content;
|
|
2801
|
+
try {
|
|
2802
|
+
content = readFileSync4(path, "utf8");
|
|
2803
|
+
} catch (error2) {
|
|
2804
|
+
if (isMissingFile2(error2)) return { records: [], readError: null };
|
|
2805
|
+
const readError = boundedError2(error2);
|
|
2806
|
+
return { records: [], readError };
|
|
2807
|
+
}
|
|
2808
|
+
const records = content.split("\n").flatMap((line) => {
|
|
2809
|
+
if (!line.trim()) return [];
|
|
2810
|
+
const record = parseRecord2(line, path, log3);
|
|
2811
|
+
return record ? [record] : [];
|
|
2812
|
+
});
|
|
2813
|
+
return { records, readError: null };
|
|
2814
|
+
}
|
|
2815
|
+
function parseRecord2(line, path, log3) {
|
|
2816
|
+
try {
|
|
2817
|
+
const value = JSON.parse(line);
|
|
2818
|
+
if (isSessionBindingRecord(value)) return value;
|
|
2819
|
+
log3(`[session-binding-ledger] skipped malformed record in ${path}: invalid shape`);
|
|
2820
|
+
} catch (error2) {
|
|
2821
|
+
log3(`[session-binding-ledger] skipped malformed record in ${path}: ${boundedError2(error2)}`);
|
|
2822
|
+
}
|
|
2823
|
+
return null;
|
|
2824
|
+
}
|
|
2825
|
+
function isSessionBindingRecord(value) {
|
|
2826
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2827
|
+
const record = value;
|
|
2828
|
+
return typeof record.conversation_id === "string" && record.conversation_id.length > 0 && typeof record.opencode_session_id === "string" && record.opencode_session_id.length > 0 && typeof record.observed_at === "string";
|
|
2829
|
+
}
|
|
2830
|
+
function isMissingFile2(error2) {
|
|
2831
|
+
return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
|
|
2832
|
+
}
|
|
2833
|
+
function boundedError2(error2) {
|
|
2834
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2835
|
+
return message.slice(0, MAX_DIAGNOSTIC_LENGTH2);
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
// src/lib/opencode/session-archive-config.ts
|
|
2839
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2840
|
+
|
|
2841
|
+
// src/lib/opencode/session-cleanup.ts
|
|
2842
|
+
var DURATION_UNIT_MS = {
|
|
2843
|
+
s: 1e3,
|
|
2844
|
+
m: 60 * 1e3,
|
|
2845
|
+
h: 60 * 60 * 1e3,
|
|
2846
|
+
d: 24 * 60 * 60 * 1e3
|
|
2847
|
+
};
|
|
2848
|
+
function parseDurationMs(input) {
|
|
2849
|
+
const trimmed = input.trim();
|
|
2850
|
+
const match = /^(\d+)([smhd])$/.exec(trimmed);
|
|
2851
|
+
if (!match) {
|
|
2852
|
+
throw new Error(
|
|
2853
|
+
`Invalid duration "${input}": expected <number><unit> where unit is one of s, m, h, d (e.g. "7d", "24h", "30m", "90s").`
|
|
2854
|
+
);
|
|
2855
|
+
}
|
|
2856
|
+
const value = Number(match[1]);
|
|
2857
|
+
if (value <= 0) {
|
|
2858
|
+
throw new Error(`Invalid duration "${input}": must be a positive value.`);
|
|
2859
|
+
}
|
|
2860
|
+
return value * DURATION_UNIT_MS[match[2]];
|
|
2861
|
+
}
|
|
2862
|
+
function selectSessionsToDelete(sessions, opts) {
|
|
2863
|
+
const { maxAgeMs, maxCount, nowMs, protectedIds } = opts;
|
|
2864
|
+
if (maxAgeMs === void 0 && maxCount === void 0) return [];
|
|
2865
|
+
const ageEligible = (s) => {
|
|
2866
|
+
if (maxAgeMs === void 0) return false;
|
|
2867
|
+
if (s.lastActivityMs === null) return true;
|
|
2868
|
+
return nowMs - s.lastActivityMs > maxAgeMs;
|
|
2869
|
+
};
|
|
2870
|
+
const countEligibleIds = /* @__PURE__ */ new Set();
|
|
2871
|
+
if (maxCount !== void 0) {
|
|
2872
|
+
const byActivityDesc = [...sessions].sort(
|
|
2873
|
+
(a, b) => (b.lastActivityMs ?? -Infinity) - (a.lastActivityMs ?? -Infinity)
|
|
2874
|
+
);
|
|
2875
|
+
for (const s of byActivityDesc.slice(maxCount)) {
|
|
2876
|
+
countEligibleIds.add(s.id);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
const toDelete = [];
|
|
2880
|
+
for (const s of sessions) {
|
|
2881
|
+
if (protectedIds.has(s.id)) continue;
|
|
2882
|
+
if (ageEligible(s) || countEligibleIds.has(s.id)) {
|
|
2883
|
+
toDelete.push(s.id);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
|
|
2887
|
+
return toDelete.sort(
|
|
2888
|
+
(a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2891
|
+
var DEFAULT_INTERVAL = "1h";
|
|
2892
|
+
function resolve2(flag, envValue, fallback) {
|
|
2893
|
+
return flag ?? envValue ?? fallback;
|
|
2894
|
+
}
|
|
2895
|
+
function parseMaxCount(input) {
|
|
2896
|
+
const trimmed = input.trim();
|
|
2897
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
2898
|
+
throw new Error(`Invalid max-count "${input}": expected a positive integer.`);
|
|
2899
|
+
}
|
|
2900
|
+
const value = Number(trimmed);
|
|
2901
|
+
if (value <= 0) {
|
|
2902
|
+
throw new Error(`Invalid max-count "${input}": must be greater than 0.`);
|
|
2903
|
+
}
|
|
2904
|
+
return value;
|
|
2905
|
+
}
|
|
2906
|
+
function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
2907
|
+
const warnings = [];
|
|
2908
|
+
const maxAgeRaw = resolve2(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
|
|
2909
|
+
const maxCountRaw = resolve2(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
|
|
2910
|
+
const intervalRaw = resolve2(
|
|
2911
|
+
flags.interval,
|
|
2912
|
+
env.EVIDENT_SESSION_CLEANUP_INTERVAL,
|
|
2913
|
+
DEFAULT_INTERVAL
|
|
2914
|
+
);
|
|
2915
|
+
let maxAgeMs;
|
|
2916
|
+
if (maxAgeRaw !== void 0) {
|
|
2917
|
+
try {
|
|
2918
|
+
maxAgeMs = parseDurationMs(maxAgeRaw);
|
|
2919
|
+
} catch (err) {
|
|
2920
|
+
warnings.push(
|
|
2921
|
+
`Ignoring invalid --session-cleanup-max-age: ${err instanceof Error ? err.message : String(err)}`
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
let maxCount;
|
|
2926
|
+
if (maxCountRaw !== void 0) {
|
|
2927
|
+
try {
|
|
2928
|
+
maxCount = parseMaxCount(maxCountRaw);
|
|
2929
|
+
} catch (err) {
|
|
2930
|
+
warnings.push(
|
|
2931
|
+
`Ignoring invalid --session-cleanup-max-count: ${err instanceof Error ? err.message : String(err)}`
|
|
2932
|
+
);
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
let intervalMs;
|
|
2936
|
+
try {
|
|
2937
|
+
intervalMs = parseDurationMs(intervalRaw ?? DEFAULT_INTERVAL);
|
|
2938
|
+
} catch (err) {
|
|
2939
|
+
warnings.push(
|
|
2940
|
+
`Ignoring invalid --session-cleanup-interval, using default ${DEFAULT_INTERVAL}: ${err instanceof Error ? err.message : String(err)}`
|
|
2941
|
+
);
|
|
2942
|
+
intervalMs = parseDurationMs(DEFAULT_INTERVAL);
|
|
2943
|
+
}
|
|
2944
|
+
const enabled = maxAgeMs !== void 0 || maxCount !== void 0;
|
|
2945
|
+
return { enabled, maxAgeMs, maxCount, intervalMs, warnings };
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
// src/lib/opencode/session-archive-config.ts
|
|
2949
|
+
var DEFAULT_INTERVAL2 = "1h";
|
|
2950
|
+
var DEFAULT_FLUSH_BUDGET_MS = 2e3;
|
|
2951
|
+
var MAX_SESSION_ARCHIVE_TIMER_DELAY_MS = 2147483647;
|
|
2952
|
+
var STATE_PREFIX_FILE = "/dev/shm/evident-state-prefix";
|
|
2953
|
+
var ENABLED_VALUES = /* @__PURE__ */ new Set(["on", "true", "1"]);
|
|
2954
|
+
var DISABLED_VALUES = /* @__PURE__ */ new Set(["off", "false", "0"]);
|
|
2955
|
+
function nonEmpty(value) {
|
|
2956
|
+
const trimmed = value?.trim();
|
|
2957
|
+
return trimmed ? trimmed : null;
|
|
2958
|
+
}
|
|
2959
|
+
function parseFlushBudgetMs(value) {
|
|
2960
|
+
const trimmed = value.trim();
|
|
2961
|
+
const budgetMs = Number(trimmed);
|
|
2962
|
+
if (!/^\d+$/.test(trimmed) || !Number.isSafeInteger(budgetMs) || budgetMs <= 0 || budgetMs > MAX_SESSION_ARCHIVE_TIMER_DELAY_MS) {
|
|
2963
|
+
throw new Error(
|
|
2964
|
+
`Invalid flush budget "${value}": expected a positive integer number of milliseconds no greater than ${MAX_SESSION_ARCHIVE_TIMER_DELAY_MS}.`
|
|
2965
|
+
);
|
|
2966
|
+
}
|
|
2967
|
+
return budgetMs;
|
|
2968
|
+
}
|
|
2969
|
+
function resolveSessionArchiveConfig(flags, env = process.env) {
|
|
2970
|
+
const warnings = [];
|
|
2971
|
+
const archiveRaw = env.EVIDENT_SESSION_ARCHIVE;
|
|
2972
|
+
let enabled = flags.enabled ?? false;
|
|
2973
|
+
if (flags.enabled === void 0 && archiveRaw !== void 0 && archiveRaw.trim() !== "") {
|
|
2974
|
+
const normalized = archiveRaw.trim().toLowerCase();
|
|
2975
|
+
if (ENABLED_VALUES.has(normalized)) {
|
|
2976
|
+
enabled = true;
|
|
2977
|
+
} else if (DISABLED_VALUES.has(normalized)) {
|
|
2978
|
+
enabled = false;
|
|
2979
|
+
} else {
|
|
2980
|
+
warnings.push(
|
|
2981
|
+
`Ignoring invalid EVIDENT_SESSION_ARCHIVE "${archiveRaw}": expected on or off; session archiving remains off.`
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
let intervalMs;
|
|
2986
|
+
const intervalRaw = flags.interval ?? env.EVIDENT_SESSION_ARCHIVE_INTERVAL ?? DEFAULT_INTERVAL2;
|
|
2987
|
+
try {
|
|
2988
|
+
intervalMs = parseDurationMs(intervalRaw);
|
|
2989
|
+
if (intervalMs > MAX_SESSION_ARCHIVE_TIMER_DELAY_MS) {
|
|
2990
|
+
throw new Error(
|
|
2991
|
+
`Duration "${intervalRaw}" exceeds Node's maximum timer delay of ${MAX_SESSION_ARCHIVE_TIMER_DELAY_MS}ms.`
|
|
2992
|
+
);
|
|
2993
|
+
}
|
|
2994
|
+
} catch (err) {
|
|
2995
|
+
warnings.push(
|
|
2996
|
+
`Ignoring invalid session archive interval, using default ${DEFAULT_INTERVAL2}: ${err instanceof Error ? err.message : String(err)}`
|
|
2997
|
+
);
|
|
2998
|
+
intervalMs = parseDurationMs(DEFAULT_INTERVAL2);
|
|
2999
|
+
}
|
|
3000
|
+
let flushBudgetMs;
|
|
3001
|
+
const flushBudgetRaw = env.EVIDENT_SESSION_ARCHIVE_FLUSH_MS ?? String(DEFAULT_FLUSH_BUDGET_MS);
|
|
3002
|
+
try {
|
|
3003
|
+
flushBudgetMs = parseFlushBudgetMs(flushBudgetRaw);
|
|
3004
|
+
} catch (err) {
|
|
3005
|
+
warnings.push(
|
|
3006
|
+
`Ignoring invalid EVIDENT_SESSION_ARCHIVE_FLUSH_MS, using default ${DEFAULT_FLUSH_BUDGET_MS}: ${err instanceof Error ? err.message : String(err)}`
|
|
3007
|
+
);
|
|
3008
|
+
flushBudgetMs = DEFAULT_FLUSH_BUDGET_MS;
|
|
3009
|
+
}
|
|
3010
|
+
const bucket = nonEmpty(env.PERSISTENCE_BUCKET);
|
|
3011
|
+
let keyPrefix = nonEmpty(env.LITESTREAM_PREFIX);
|
|
3012
|
+
if (enabled && bucket === null) {
|
|
3013
|
+
warnings.push("Session archiving is off because PERSISTENCE_BUCKET is unset or empty.");
|
|
3014
|
+
enabled = false;
|
|
3015
|
+
}
|
|
3016
|
+
if (enabled && keyPrefix === null) {
|
|
3017
|
+
let fallbackReadError = null;
|
|
3018
|
+
try {
|
|
3019
|
+
keyPrefix = nonEmpty(readFileSync5(STATE_PREFIX_FILE, "utf8"));
|
|
3020
|
+
} catch (err) {
|
|
3021
|
+
fallbackReadError = err instanceof Error ? err.message : String(err);
|
|
3022
|
+
}
|
|
3023
|
+
if (keyPrefix === null) {
|
|
3024
|
+
const readFailure = fallbackReadError ? ` Could not read ${STATE_PREFIX_FILE}: ${fallbackReadError}.` : "";
|
|
3025
|
+
warnings.push(
|
|
3026
|
+
`Session archiving is off because neither LITESTREAM_PREFIX nor ${STATE_PREFIX_FILE} contains a prefix.${readFailure}`
|
|
3027
|
+
);
|
|
3028
|
+
enabled = false;
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
return { enabled, bucket, keyPrefix, intervalMs, flushBudgetMs, warnings };
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
// src/lib/opencode/session-archive-snapshot.ts
|
|
3035
|
+
import { createHash } from "node:crypto";
|
|
3036
|
+
var SessionArchiveExportError = class extends Error {
|
|
3037
|
+
constructor(code, message) {
|
|
3038
|
+
super(message);
|
|
3039
|
+
this.code = code;
|
|
3040
|
+
this.name = "SessionArchiveExportError";
|
|
3041
|
+
}
|
|
3042
|
+
};
|
|
3043
|
+
function canonicalize(value) {
|
|
3044
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
3045
|
+
if (typeof value !== "object" || value === null) return value;
|
|
3046
|
+
return Object.fromEntries(
|
|
3047
|
+
Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, nested]) => [key, canonicalize(nested)])
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
3050
|
+
function contentForFingerprint(archive) {
|
|
3051
|
+
return JSON.stringify(
|
|
3052
|
+
canonicalize({
|
|
3053
|
+
opencode_api_version: archive.opencode_api_version,
|
|
3054
|
+
opencode_session_id: archive.opencode_session_id,
|
|
3055
|
+
evident_conversation_id: archive.evident_conversation_id,
|
|
3056
|
+
messages: archive.messages
|
|
3057
|
+
})
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
function fingerprintArchive(archive) {
|
|
3061
|
+
return createHash("sha256").update(contentForFingerprint(archive)).digest("hex");
|
|
3062
|
+
}
|
|
3063
|
+
function buildSessionArchive(input) {
|
|
3064
|
+
const archive = {
|
|
3065
|
+
schema_version: 1,
|
|
3066
|
+
opencode_api_version: input.apiVersion,
|
|
3067
|
+
opencode_session_id: input.session.id,
|
|
3068
|
+
evident_conversation_id: input.conversationId,
|
|
3069
|
+
evident_runner_id: input.runnerId,
|
|
3070
|
+
binding_source: input.bindingSource,
|
|
3071
|
+
session: input.session,
|
|
3072
|
+
messages: input.messages,
|
|
3073
|
+
message_count: input.messages.length,
|
|
3074
|
+
archived_at: input.archivedAt
|
|
3075
|
+
};
|
|
3076
|
+
return { ...archive, content_fingerprint: fingerprintArchive(archive) };
|
|
3077
|
+
}
|
|
3078
|
+
function isRecord(value) {
|
|
3079
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3080
|
+
}
|
|
3081
|
+
function errorMessage2(error2) {
|
|
3082
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
3083
|
+
}
|
|
3084
|
+
function invalidResponse(version2, sessionId, path, reason) {
|
|
3085
|
+
return new SessionArchiveExportError(
|
|
3086
|
+
"invalid-response",
|
|
3087
|
+
`OpenCode ${version2} session ${sessionId} returned an invalid response from GET ${path}: ${reason}`
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
async function requestPage(client, sessionId, path, signal) {
|
|
3091
|
+
let response;
|
|
3092
|
+
try {
|
|
3093
|
+
response = await client.request(path, signal ? { signal } : void 0, {
|
|
3094
|
+
allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100)
|
|
3095
|
+
});
|
|
3096
|
+
} catch (error2) {
|
|
3097
|
+
throw new SessionArchiveExportError(
|
|
3098
|
+
"request-failed",
|
|
3099
|
+
`OpenCode ${client.version} session ${sessionId} request GET ${path} failed: ${errorMessage2(error2)}`
|
|
3100
|
+
);
|
|
3101
|
+
}
|
|
3102
|
+
if (response.status === 404) {
|
|
3103
|
+
throw new SessionArchiveExportError(
|
|
3104
|
+
"not-found",
|
|
3105
|
+
`OpenCode ${client.version} session ${sessionId} was not found at GET ${path} (HTTP 404)`
|
|
3106
|
+
);
|
|
3107
|
+
}
|
|
3108
|
+
if (!response.ok) {
|
|
3109
|
+
throw new SessionArchiveExportError(
|
|
3110
|
+
"request-failed",
|
|
3111
|
+
`OpenCode ${client.version} session ${sessionId} request GET ${path} returned HTTP ${response.status}`
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
let bodyText;
|
|
3115
|
+
try {
|
|
3116
|
+
bodyText = await response.text();
|
|
3117
|
+
} catch (error2) {
|
|
3118
|
+
throw new SessionArchiveExportError(
|
|
3119
|
+
"request-failed",
|
|
3120
|
+
`OpenCode ${client.version} session ${sessionId} response body from GET ${path} could not be read: ${errorMessage2(error2)}`
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
try {
|
|
3124
|
+
return { response, body: JSON.parse(bodyText) };
|
|
3125
|
+
} catch (error2) {
|
|
3126
|
+
if (!(error2 instanceof SyntaxError)) throw error2;
|
|
3127
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
3128
|
+
const looksLikeHtml = contentType.toLowerCase().includes("text/html") || /^\s*<!doctype\s+html|^\s*<html/i.test(bodyText);
|
|
3129
|
+
throw invalidResponse(
|
|
3130
|
+
client.version,
|
|
3131
|
+
sessionId,
|
|
3132
|
+
path,
|
|
3133
|
+
looksLikeHtml ? "received text/html, expected JSON" : "received non-JSON content, expected JSON"
|
|
3134
|
+
);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
function v1NextCursor(response, sessionId, path) {
|
|
3138
|
+
const headerCursor = response.headers.get("x-next-cursor")?.trim() || null;
|
|
3139
|
+
const linkHeader = response.headers.get("link");
|
|
3140
|
+
const nextLink = linkHeader?.split(",").map((link) => link.trim()).find((link) => /;\s*rel\s*=\s*"?next"?/i.test(link));
|
|
3141
|
+
let linkCursor = null;
|
|
3142
|
+
if (nextLink) {
|
|
3143
|
+
const target = nextLink.match(/<([^>]+)>/)?.[1];
|
|
3144
|
+
if (!target) {
|
|
3145
|
+
throw new SessionArchiveExportError(
|
|
3146
|
+
"pagination-failed",
|
|
3147
|
+
`OpenCode v1 session ${sessionId} returned a next Link without a URL for GET ${path}`
|
|
3148
|
+
);
|
|
3149
|
+
}
|
|
3150
|
+
try {
|
|
3151
|
+
linkCursor = new URL(target, "http://127.0.0.1").searchParams.get("before");
|
|
3152
|
+
} catch (error2) {
|
|
3153
|
+
throw new SessionArchiveExportError(
|
|
3154
|
+
"pagination-failed",
|
|
3155
|
+
`OpenCode v1 session ${sessionId} returned an invalid next Link for GET ${path}: ${errorMessage2(error2)}`
|
|
3156
|
+
);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
if (headerCursor && linkCursor && headerCursor !== linkCursor) {
|
|
3160
|
+
throw new SessionArchiveExportError(
|
|
3161
|
+
"pagination-failed",
|
|
3162
|
+
`OpenCode v1 session ${sessionId} returned conflicting next cursors for GET ${path}`
|
|
3163
|
+
);
|
|
3164
|
+
}
|
|
3165
|
+
const cursor = headerCursor ?? linkCursor;
|
|
3166
|
+
if ((headerCursor || nextLink) && !cursor) {
|
|
3167
|
+
throw new SessionArchiveExportError(
|
|
3168
|
+
"pagination-failed",
|
|
3169
|
+
`OpenCode v1 session ${sessionId} returned a next page without a usable cursor for GET ${path}`
|
|
3170
|
+
);
|
|
3171
|
+
}
|
|
3172
|
+
return cursor;
|
|
3173
|
+
}
|
|
3174
|
+
function v1Messages(body, sessionId, path) {
|
|
3175
|
+
if (!Array.isArray(body)) {
|
|
3176
|
+
throw invalidResponse("v1", sessionId, path, "expected an array of messages");
|
|
3177
|
+
}
|
|
3178
|
+
return body;
|
|
3179
|
+
}
|
|
3180
|
+
async function exportV1SessionMessages(client, sessionId, signal) {
|
|
3181
|
+
const basePath = `/session/${encodeURIComponent(sessionId)}/message`;
|
|
3182
|
+
const firstPage = await requestPage(client, sessionId, basePath, signal);
|
|
3183
|
+
let messages = v1Messages(firstPage.body, sessionId, basePath);
|
|
3184
|
+
let cursor = v1NextCursor(firstPage.response, sessionId, basePath);
|
|
3185
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
3186
|
+
while (cursor !== null) {
|
|
3187
|
+
if (seenCursors.has(cursor)) {
|
|
3188
|
+
throw new SessionArchiveExportError(
|
|
3189
|
+
"pagination-failed",
|
|
3190
|
+
`OpenCode v1 session ${sessionId} repeated cursor while reading GET ${basePath}`
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
seenCursors.add(cursor);
|
|
3194
|
+
const path = `${basePath}?${new URLSearchParams({ before: cursor }).toString()}`;
|
|
3195
|
+
const page = await requestPage(client, sessionId, path, signal);
|
|
3196
|
+
messages = [...v1Messages(page.body, sessionId, path), ...messages];
|
|
3197
|
+
cursor = v1NextCursor(page.response, sessionId, path);
|
|
3198
|
+
}
|
|
3199
|
+
return messages;
|
|
3200
|
+
}
|
|
3201
|
+
async function exportV2SessionMessages(client, sessionId, signal) {
|
|
3202
|
+
const basePath = `/api/session/${encodeURIComponent(sessionId)}/message`;
|
|
3203
|
+
const messages = [];
|
|
3204
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
3205
|
+
let cursor;
|
|
3206
|
+
let hasNextPage = true;
|
|
3207
|
+
while (hasNextPage) {
|
|
3208
|
+
const params = new URLSearchParams({ limit: "100" });
|
|
3209
|
+
if (cursor === void 0) {
|
|
3210
|
+
params.set("order", "asc");
|
|
3211
|
+
} else {
|
|
3212
|
+
params.set("cursor", cursor);
|
|
3213
|
+
}
|
|
3214
|
+
const path = `${basePath}?${params.toString()}`;
|
|
3215
|
+
const { body } = await requestPage(client, sessionId, path, signal);
|
|
3216
|
+
if (!isRecord(body) || !Array.isArray(body.data) || !isRecord(body.cursor)) {
|
|
3217
|
+
throw invalidResponse("v2", sessionId, path, "expected a {data: [], cursor: {\u2026}} envelope");
|
|
3218
|
+
}
|
|
3219
|
+
messages.push(...body.data);
|
|
3220
|
+
const next = body.cursor.next;
|
|
3221
|
+
if (next === void 0 || next === null) {
|
|
3222
|
+
hasNextPage = false;
|
|
3223
|
+
continue;
|
|
3224
|
+
}
|
|
3225
|
+
if (typeof next !== "string" || next.length === 0 || seenCursors.has(next)) {
|
|
3226
|
+
throw new SessionArchiveExportError(
|
|
3227
|
+
"pagination-failed",
|
|
3228
|
+
`OpenCode v2 session ${sessionId} returned an invalid or repeated cursor from GET ${path}`
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
seenCursors.add(next);
|
|
3232
|
+
cursor = next;
|
|
3233
|
+
}
|
|
3234
|
+
return messages;
|
|
3235
|
+
}
|
|
3236
|
+
async function exportSessionMessages(client, sessionId, signal) {
|
|
3237
|
+
if (client.version === "v1") return exportV1SessionMessages(client, sessionId, signal);
|
|
3238
|
+
if (client.version === "v2") return exportV2SessionMessages(client, sessionId, signal);
|
|
3239
|
+
throw new SessionArchiveExportError(
|
|
3240
|
+
"unsupported-dialect",
|
|
3241
|
+
`OpenCode session ${sessionId} cannot be archived: unsupported API dialect ${String(client.version)}`
|
|
3242
|
+
);
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
// src/lib/opencode/session-archive-watermarks.ts
|
|
3246
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3247
|
+
import { dirname as dirname6, join as join6 } from "node:path";
|
|
3248
|
+
function sessionArchiveWatermarkPath(homeDir, env) {
|
|
3249
|
+
const override = env.EVIDENT_SESSION_ARCHIVE_WATERMARK?.trim();
|
|
3250
|
+
return override || join6(homeDir, ".local", "state", "evident", "session-archive-watermarks.json");
|
|
3251
|
+
}
|
|
3252
|
+
function readWatermarks(path, log3) {
|
|
3253
|
+
let content;
|
|
3254
|
+
try {
|
|
3255
|
+
content = readFileSync6(path, "utf8");
|
|
3256
|
+
} catch (error2) {
|
|
3257
|
+
if (isMissingFile3(error2)) return /* @__PURE__ */ new Map();
|
|
3258
|
+
log3(`[session-archive-watermarks] could not read ${path}; treating every session as changed`);
|
|
3259
|
+
return /* @__PURE__ */ new Map();
|
|
3260
|
+
}
|
|
3261
|
+
try {
|
|
3262
|
+
const parsed = JSON.parse(content);
|
|
3263
|
+
if (!isRecord2(parsed)) throw new Error("invalid watermark object");
|
|
3264
|
+
const entries = Object.entries(parsed);
|
|
3265
|
+
if (!entries.every(([sessionId, value]) => sessionId.length > 0 && isWatermark(value))) {
|
|
3266
|
+
throw new Error("invalid watermark entry");
|
|
3267
|
+
}
|
|
3268
|
+
return new Map(entries);
|
|
3269
|
+
} catch (error2) {
|
|
3270
|
+
log3(`[session-archive-watermarks] could not parse ${path}; treating every session as changed`);
|
|
3271
|
+
return /* @__PURE__ */ new Map();
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
function recordUpload(path, sessionId, entry, watermarks) {
|
|
3275
|
+
const updated = new Map(watermarks);
|
|
3276
|
+
updated.set(sessionId, entry);
|
|
3277
|
+
mkdirSync3(dirname6(path), { recursive: true });
|
|
3278
|
+
writeFileSync3(path, `${JSON.stringify(Object.fromEntries(updated))}
|
|
3279
|
+
`, {
|
|
3280
|
+
encoding: "utf8",
|
|
3281
|
+
mode: 384
|
|
3282
|
+
});
|
|
3283
|
+
watermarks.clear();
|
|
3284
|
+
for (const [id, watermark] of updated) watermarks.set(id, watermark);
|
|
3285
|
+
}
|
|
3286
|
+
function isRecord2(value) {
|
|
3287
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3288
|
+
}
|
|
3289
|
+
function isWatermark(value) {
|
|
3290
|
+
if (!isRecord2(value)) return false;
|
|
3291
|
+
return typeof value.fingerprint === "string" && typeof value.uploaded_at === "string" && typeof value.key === "string";
|
|
3292
|
+
}
|
|
3293
|
+
function isMissingFile3(error2) {
|
|
3294
|
+
return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
// src/lib/runner-synchroniser.ts
|
|
3298
|
+
import { spawn } from "node:child_process";
|
|
3299
|
+
|
|
3300
|
+
// ../../runner/synchroniser/src/shell-contract.json
|
|
3301
|
+
var shell_contract_default = {
|
|
3302
|
+
commands: [
|
|
3303
|
+
"env",
|
|
3304
|
+
"litestream-config",
|
|
3305
|
+
"restore",
|
|
3306
|
+
"sync-once",
|
|
3307
|
+
"model-auth-ready",
|
|
3308
|
+
"self-stop",
|
|
3309
|
+
"session-db-classify",
|
|
3310
|
+
"session-db-verify",
|
|
3311
|
+
"archive-put"
|
|
3312
|
+
],
|
|
3313
|
+
archiveStdinMaxBytes: 16777216
|
|
3314
|
+
};
|
|
3315
|
+
|
|
3316
|
+
// src/lib/runner-synchroniser.ts
|
|
3317
|
+
var MAX_ARCHIVE_STDIN_BYTES = shell_contract_default.archiveStdinMaxBytes;
|
|
3318
|
+
var ERROR_CLOSE_FALLBACK_MS = 1e3;
|
|
3319
|
+
function isPipeClosed(error2) {
|
|
3320
|
+
return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "EPIPE";
|
|
3321
|
+
}
|
|
3322
|
+
function appendError(stderr, error2) {
|
|
3323
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
3324
|
+
return stderr === "" ? message : `${stderr}
|
|
3325
|
+
${message}`;
|
|
3326
|
+
}
|
|
3327
|
+
function runSynchroniser(args, opts) {
|
|
3328
|
+
return new Promise((resolve5) => {
|
|
3329
|
+
let child;
|
|
3330
|
+
let stdout = "";
|
|
3331
|
+
let stderr = "";
|
|
3332
|
+
let settled = false;
|
|
3333
|
+
let shuttingDown = false;
|
|
3334
|
+
let timedOut = false;
|
|
3335
|
+
let stdinFailure;
|
|
3336
|
+
let childFailure;
|
|
3337
|
+
const timer = {};
|
|
3338
|
+
const closeFallbackTimer = {};
|
|
3339
|
+
let abortListener;
|
|
3340
|
+
let spawnListener;
|
|
3341
|
+
const finish = (result) => {
|
|
3342
|
+
if (settled) return;
|
|
3343
|
+
settled = true;
|
|
3344
|
+
if (timer.handle) clearTimeout(timer.handle);
|
|
3345
|
+
if (closeFallbackTimer.handle) clearTimeout(closeFallbackTimer.handle);
|
|
3346
|
+
if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
|
|
3347
|
+
if (spawnListener) child.removeListener("spawn", spawnListener);
|
|
3348
|
+
resolve5(result);
|
|
3349
|
+
};
|
|
3350
|
+
const armErrorCloseFallback = () => {
|
|
3351
|
+
if (closeFallbackTimer.handle) return;
|
|
3352
|
+
closeFallbackTimer.handle = setTimeout(() => {
|
|
3353
|
+
const failure = stdinFailure ?? childFailure;
|
|
3354
|
+
finish({
|
|
3355
|
+
code: null,
|
|
3356
|
+
stdout,
|
|
3357
|
+
stderr: failure === void 0 ? stderr : appendError(stderr, failure),
|
|
3358
|
+
timedOut
|
|
3359
|
+
});
|
|
3360
|
+
}, ERROR_CLOSE_FALLBACK_MS);
|
|
3361
|
+
};
|
|
3362
|
+
const terminateChild = () => {
|
|
3363
|
+
if (!shuttingDown) {
|
|
3364
|
+
shuttingDown = true;
|
|
3365
|
+
child.stdin?.destroy();
|
|
3366
|
+
}
|
|
3367
|
+
if (child.pid !== void 0) {
|
|
3368
|
+
child.kill("SIGKILL");
|
|
3369
|
+
} else if (!spawnListener) {
|
|
3370
|
+
spawnListener = () => {
|
|
3371
|
+
if (child.pid !== void 0) child.kill("SIGKILL");
|
|
3372
|
+
};
|
|
3373
|
+
child.once("spawn", spawnListener);
|
|
3374
|
+
}
|
|
3375
|
+
};
|
|
3376
|
+
try {
|
|
3377
|
+
child = spawn("runner-synchroniser", args, {
|
|
3378
|
+
env: opts.env ?? process.env,
|
|
3379
|
+
stdio: [opts.stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"]
|
|
3380
|
+
});
|
|
3381
|
+
} catch (error2) {
|
|
3382
|
+
finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
child.stdout?.setEncoding("utf8");
|
|
3386
|
+
child.stdout?.on("data", (chunk) => {
|
|
3387
|
+
stdout += chunk;
|
|
3388
|
+
});
|
|
3389
|
+
child.stderr?.setEncoding("utf8");
|
|
3390
|
+
child.stderr?.on("data", (chunk) => {
|
|
3391
|
+
stderr += chunk;
|
|
3392
|
+
});
|
|
3393
|
+
child.once("error", (error2) => {
|
|
3394
|
+
childFailure = error2;
|
|
3395
|
+
if (child.pid !== void 0) terminateChild();
|
|
3396
|
+
armErrorCloseFallback();
|
|
3397
|
+
});
|
|
3398
|
+
child.once("close", (code) => {
|
|
3399
|
+
const failure = stdinFailure ?? childFailure;
|
|
3400
|
+
finish({
|
|
3401
|
+
code: failure === void 0 ? code : null,
|
|
3402
|
+
stdout,
|
|
3403
|
+
stderr: failure === void 0 ? stderr : appendError(stderr, failure),
|
|
3404
|
+
timedOut
|
|
3405
|
+
});
|
|
3406
|
+
});
|
|
3407
|
+
const stdin = child.stdin;
|
|
3408
|
+
const failStdin = (error2) => {
|
|
3409
|
+
if (settled || shuttingDown || isPipeClosed(error2)) return;
|
|
3410
|
+
stdinFailure = error2;
|
|
3411
|
+
terminateChild();
|
|
3412
|
+
armErrorCloseFallback();
|
|
3413
|
+
};
|
|
3414
|
+
stdin?.once("error", failStdin);
|
|
3415
|
+
if (opts.stdin !== void 0 && stdin === null) {
|
|
3416
|
+
failStdin(new Error("synchroniser stdin is unavailable"));
|
|
3417
|
+
} else if (opts.stdin !== void 0 && stdin !== null) {
|
|
3418
|
+
const drained = stdin.write(opts.stdin, (error2) => {
|
|
3419
|
+
if (error2) failStdin(error2);
|
|
3420
|
+
});
|
|
3421
|
+
if (!drained) {
|
|
3422
|
+
stdin.once("drain", () => stdin.end());
|
|
3423
|
+
} else {
|
|
3424
|
+
stdin.end();
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
if (opts.signal) {
|
|
3428
|
+
const killChild = () => {
|
|
3429
|
+
terminateChild();
|
|
3430
|
+
};
|
|
3431
|
+
abortListener = killChild;
|
|
3432
|
+
if (opts.signal.aborted) {
|
|
3433
|
+
abortListener();
|
|
3434
|
+
} else {
|
|
3435
|
+
opts.signal.addEventListener("abort", abortListener, { once: true });
|
|
3436
|
+
if (opts.signal.aborted) abortListener();
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
timer.handle = setTimeout(
|
|
3440
|
+
() => {
|
|
3441
|
+
timedOut = true;
|
|
3442
|
+
terminateChild();
|
|
3443
|
+
},
|
|
3444
|
+
Math.max(0, opts.timeoutMs)
|
|
3445
|
+
);
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
// src/lib/opencode/session-archive-upload.ts
|
|
3450
|
+
var MAX_SESSION_ARCHIVE_BYTES = MAX_ARCHIVE_STDIN_BYTES;
|
|
3451
|
+
var EXIT_ARCHIVE_CONFIG_MISSING = 50;
|
|
3452
|
+
var EXIT_ARCHIVE_ACCESS_DENIED = 51;
|
|
3453
|
+
function transportFailure(bytes, reason) {
|
|
3454
|
+
const message = reason === "missing-executable" ? "runner-synchroniser is unavailable; session archival requires a hosted runner." : reason === "timeout" ? "runner-synchroniser timed out before the archive upload completed." : "session archive upload did not complete.";
|
|
3455
|
+
return { kind: "transport-failed", bytes, reason, message };
|
|
3456
|
+
}
|
|
3457
|
+
function isJson(body) {
|
|
3458
|
+
try {
|
|
3459
|
+
JSON.parse(body.toString("utf8"));
|
|
3460
|
+
return true;
|
|
3461
|
+
} catch (error2) {
|
|
3462
|
+
void error2;
|
|
3463
|
+
return false;
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
async function uploadSessionArchive(key, body, options) {
|
|
3467
|
+
const bytes = Buffer.from(body);
|
|
3468
|
+
if (bytes.byteLength > MAX_SESSION_ARCHIVE_BYTES) {
|
|
3469
|
+
return transportFailure(bytes.byteLength, "too-large");
|
|
3470
|
+
}
|
|
3471
|
+
if (!isJson(bytes)) return transportFailure(bytes.byteLength, "invalid-json");
|
|
3472
|
+
const run2 = options.runSynchroniser ?? runSynchroniser;
|
|
3473
|
+
let result;
|
|
3474
|
+
try {
|
|
3475
|
+
result = await run2(["archive-put", key], {
|
|
3476
|
+
timeoutMs: options.timeoutMs,
|
|
3477
|
+
env: options.env,
|
|
3478
|
+
signal: options.signal,
|
|
3479
|
+
stdin: bytes
|
|
3480
|
+
});
|
|
3481
|
+
} catch (error2) {
|
|
3482
|
+
return transportFailure(
|
|
3483
|
+
bytes.byteLength,
|
|
3484
|
+
error2 instanceof Error && /ENOENT|runner-synchroniser/i.test(error2.message) ? "missing-executable" : "spawn-failed"
|
|
3485
|
+
);
|
|
3486
|
+
}
|
|
3487
|
+
if (result.timedOut) return transportFailure(bytes.byteLength, "timeout");
|
|
3488
|
+
if (result.code === 0) return { kind: "ok", bytes: bytes.byteLength };
|
|
3489
|
+
if (result.code === EXIT_ARCHIVE_CONFIG_MISSING) {
|
|
3490
|
+
return { kind: "config-missing", bytes: bytes.byteLength };
|
|
3491
|
+
}
|
|
3492
|
+
if (result.code === EXIT_ARCHIVE_ACCESS_DENIED) {
|
|
3493
|
+
return { kind: "access-denied", bytes: bytes.byteLength };
|
|
3494
|
+
}
|
|
3495
|
+
if (result.code === null && /ENOENT|runner-synchroniser/i.test(result.stderr)) {
|
|
3496
|
+
return transportFailure(bytes.byteLength, "missing-executable");
|
|
3497
|
+
}
|
|
3498
|
+
return transportFailure(
|
|
3499
|
+
bytes.byteLength,
|
|
3500
|
+
result.code === null ? "spawn-failed" : "process-failed"
|
|
3501
|
+
);
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
// src/lib/opencode/session-archive-run.ts
|
|
3505
|
+
var SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS = 6e4;
|
|
3506
|
+
function updatedAt(session) {
|
|
3507
|
+
const updated = session.time?.updated;
|
|
3508
|
+
return typeof updated === "number" && Number.isFinite(updated) ? updated : -Infinity;
|
|
3509
|
+
}
|
|
3510
|
+
function sessionArchiveKey(prefix, sessionId) {
|
|
3511
|
+
return `${prefix.replace(/\/+$/, "")}/conversation-archives/${encodeURIComponent(sessionId)}.json`;
|
|
3512
|
+
}
|
|
3513
|
+
function failureMessage(error2) {
|
|
3514
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
3515
|
+
}
|
|
3516
|
+
function isBudgetExpired(deadline, now, signal) {
|
|
3517
|
+
return signal?.aborted === true || now() >= deadline;
|
|
3518
|
+
}
|
|
3519
|
+
function createBudgetSignal(parent, timeoutMs) {
|
|
3520
|
+
const controller = new AbortController();
|
|
3521
|
+
const forwardAbort = () => controller.abort(parent?.reason);
|
|
3522
|
+
if (parent?.aborted) forwardAbort();
|
|
3523
|
+
else parent?.addEventListener("abort", forwardAbort, { once: true });
|
|
3524
|
+
const timer = setTimeout(
|
|
3525
|
+
() => controller.abort(new Error("session archive budget exceeded")),
|
|
3526
|
+
timeoutMs
|
|
3527
|
+
);
|
|
3528
|
+
return {
|
|
3529
|
+
signal: controller.signal,
|
|
3530
|
+
dispose: () => {
|
|
3531
|
+
clearTimeout(timer);
|
|
3532
|
+
parent?.removeEventListener("abort", forwardAbort);
|
|
3533
|
+
}
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
async function runSessionArchive({
|
|
3537
|
+
sessions,
|
|
3538
|
+
deps,
|
|
3539
|
+
budgetMs,
|
|
3540
|
+
now = Date.now
|
|
3541
|
+
}) {
|
|
3542
|
+
const startedAt = now();
|
|
3543
|
+
const deadline = startedAt + Math.max(0, budgetMs);
|
|
3544
|
+
const summary = {
|
|
3545
|
+
inspected: sessions.length,
|
|
3546
|
+
uploaded: 0,
|
|
3547
|
+
unchanged: 0,
|
|
3548
|
+
failed: 0,
|
|
3549
|
+
uploadAttempts: 0,
|
|
3550
|
+
uploadFailures: 0,
|
|
3551
|
+
skippedBudget: 0,
|
|
3552
|
+
skippedBudgetIds: [],
|
|
3553
|
+
neverWatermarkedFailures: 0,
|
|
3554
|
+
unmappedIds: [],
|
|
3555
|
+
archivedSessionIds: [],
|
|
3556
|
+
failures: [],
|
|
3557
|
+
staleSince: null
|
|
3558
|
+
};
|
|
3559
|
+
const ordered = [...sessions].sort((left, right) => updatedAt(right) - updatedAt(left));
|
|
3560
|
+
const upload = deps.upload ?? uploadSessionArchive;
|
|
3561
|
+
const exportMessages = deps.exportMessages ?? exportSessionMessages;
|
|
3562
|
+
const writeWatermark = deps.recordWatermark ?? recordUpload;
|
|
3563
|
+
const failedWatermarks = [];
|
|
3564
|
+
const recordFailedWatermark = (watermark) => {
|
|
3565
|
+
if (watermark) failedWatermarks.push(watermark.uploaded_at);
|
|
3566
|
+
else summary.neverWatermarkedFailures++;
|
|
3567
|
+
};
|
|
3568
|
+
for (let index = 0; index < ordered.length; index++) {
|
|
3569
|
+
const session = ordered[index];
|
|
3570
|
+
const remainingMs = Math.floor(deadline - now());
|
|
3571
|
+
if (remainingMs <= 0 || deps.signal?.aborted) {
|
|
3572
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3573
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3574
|
+
break;
|
|
3575
|
+
}
|
|
3576
|
+
const conversationId = deps.bindings.get(session.id) ?? null;
|
|
3577
|
+
if (conversationId === null) summary.unmappedIds.push(session.id);
|
|
3578
|
+
const exportBudget = createBudgetSignal(deps.signal, remainingMs);
|
|
3579
|
+
let messages;
|
|
3580
|
+
try {
|
|
3581
|
+
messages = await exportMessages(deps.client, session.id, exportBudget.signal);
|
|
3582
|
+
} catch (error2) {
|
|
3583
|
+
if (isBudgetExpired(deadline, now, deps.signal) || exportBudget.signal.aborted) {
|
|
3584
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3585
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3586
|
+
break;
|
|
3587
|
+
}
|
|
3588
|
+
summary.failed++;
|
|
3589
|
+
const reason = error2 instanceof SessionArchiveExportError ? error2.code : "export-failed";
|
|
3590
|
+
summary.failures.push({
|
|
3591
|
+
sessionId: session.id,
|
|
3592
|
+
outcome: "export-failed",
|
|
3593
|
+
reason,
|
|
3594
|
+
message: failureMessage(error2),
|
|
3595
|
+
archived: false
|
|
3596
|
+
});
|
|
3597
|
+
recordFailedWatermark(deps.watermarks.get(session.id));
|
|
3598
|
+
continue;
|
|
3599
|
+
} finally {
|
|
3600
|
+
exportBudget.dispose();
|
|
3601
|
+
}
|
|
3602
|
+
if (isBudgetExpired(deadline, now, deps.signal) || exportBudget.signal.aborted) {
|
|
3603
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3604
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3605
|
+
break;
|
|
3606
|
+
}
|
|
3607
|
+
try {
|
|
3608
|
+
const archivedAt = new Date(now()).toISOString();
|
|
3609
|
+
const snapshot = buildSessionArchive({
|
|
3610
|
+
session,
|
|
3611
|
+
messages,
|
|
3612
|
+
apiVersion: deps.client.version,
|
|
3613
|
+
conversationId,
|
|
3614
|
+
bindingSource: conversationId === null ? "unmapped" : "ledger",
|
|
3615
|
+
runnerId: deps.runnerId,
|
|
3616
|
+
archivedAt
|
|
3617
|
+
});
|
|
3618
|
+
const key = sessionArchiveKey(deps.keyPrefix, session.id);
|
|
3619
|
+
const existing = deps.watermarks.get(session.id);
|
|
3620
|
+
if (existing?.fingerprint === snapshot.content_fingerprint) {
|
|
3621
|
+
summary.unchanged++;
|
|
3622
|
+
summary.archivedSessionIds.push(session.id);
|
|
3623
|
+
continue;
|
|
3624
|
+
}
|
|
3625
|
+
const uploadRemainingMs = Math.floor(deadline - now());
|
|
3626
|
+
if (uploadRemainingMs <= 0 || deps.signal?.aborted) {
|
|
3627
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3628
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3629
|
+
break;
|
|
3630
|
+
}
|
|
3631
|
+
summary.uploadAttempts++;
|
|
3632
|
+
const uploadBudget = createBudgetSignal(deps.signal, uploadRemainingMs);
|
|
3633
|
+
let outcome;
|
|
3634
|
+
try {
|
|
3635
|
+
outcome = await upload(key, JSON.stringify(snapshot), {
|
|
3636
|
+
timeoutMs: Math.min(uploadRemainingMs, SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS),
|
|
3637
|
+
signal: uploadBudget.signal
|
|
3638
|
+
});
|
|
3639
|
+
} finally {
|
|
3640
|
+
uploadBudget.dispose();
|
|
3641
|
+
}
|
|
3642
|
+
if (outcome.kind !== "ok") {
|
|
3643
|
+
summary.failed++;
|
|
3644
|
+
summary.uploadFailures++;
|
|
3645
|
+
const reason = "reason" in outcome ? outcome.reason : void 0;
|
|
3646
|
+
summary.failures.push({
|
|
3647
|
+
sessionId: session.id,
|
|
3648
|
+
outcome: outcome.kind,
|
|
3649
|
+
...reason !== void 0 ? { reason } : {},
|
|
3650
|
+
..."message" in outcome ? { message: outcome.message } : {},
|
|
3651
|
+
archived: false
|
|
3652
|
+
});
|
|
3653
|
+
recordFailedWatermark(existing);
|
|
3654
|
+
if (uploadBudget.signal.aborted) {
|
|
3655
|
+
summary.skippedBudgetIds.push(...ordered.slice(index + 1).map(({ id }) => id));
|
|
3656
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3657
|
+
break;
|
|
3658
|
+
}
|
|
3659
|
+
continue;
|
|
3660
|
+
}
|
|
3661
|
+
summary.uploaded++;
|
|
3662
|
+
summary.archivedSessionIds.push(session.id);
|
|
3663
|
+
const watermark = {
|
|
3664
|
+
fingerprint: snapshot.content_fingerprint,
|
|
3665
|
+
uploaded_at: new Date(now()).toISOString(),
|
|
3666
|
+
key
|
|
3667
|
+
};
|
|
3668
|
+
try {
|
|
3669
|
+
writeWatermark(deps.watermarkPath, session.id, watermark, deps.watermarks);
|
|
3670
|
+
} catch (error2) {
|
|
3671
|
+
summary.failures.push({
|
|
3672
|
+
sessionId: session.id,
|
|
3673
|
+
outcome: "watermark-write-failed",
|
|
3674
|
+
message: failureMessage(error2),
|
|
3675
|
+
archived: true
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
3678
|
+
} catch (error2) {
|
|
3679
|
+
if (isBudgetExpired(deadline, now, deps.signal)) {
|
|
3680
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3681
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3682
|
+
break;
|
|
3683
|
+
}
|
|
3684
|
+
summary.failed++;
|
|
3685
|
+
summary.failures.push({
|
|
3686
|
+
sessionId: session.id,
|
|
3687
|
+
outcome: "archive-failed",
|
|
3688
|
+
message: failureMessage(error2),
|
|
3689
|
+
archived: false
|
|
3690
|
+
});
|
|
3691
|
+
recordFailedWatermark(deps.watermarks.get(session.id));
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
summary.staleSince = failedWatermarks.sort()[0] ?? null;
|
|
3695
|
+
return summary;
|
|
3696
|
+
}
|
|
3697
|
+
|
|
2760
3698
|
// src/lib/opencode/auth.ts
|
|
2761
3699
|
function buildOpenCodeBasicAuthHeader(password) {
|
|
2762
3700
|
return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
|
|
@@ -2775,8 +3713,26 @@ async function checkOpenCodeHealth(port) {
|
|
|
2775
3713
|
if (!response.ok) {
|
|
2776
3714
|
return { healthy: false, error: `HTTP ${response.status}` };
|
|
2777
3715
|
}
|
|
2778
|
-
const
|
|
2779
|
-
|
|
3716
|
+
const body = await response.text();
|
|
3717
|
+
let data;
|
|
3718
|
+
try {
|
|
3719
|
+
data = JSON.parse(body);
|
|
3720
|
+
} catch (error2) {
|
|
3721
|
+
return {
|
|
3722
|
+
healthy: false,
|
|
3723
|
+
okWithNonJsonBody: true,
|
|
3724
|
+
error: `HTTP ${response.status}: invalid JSON health response${bodyExcerpt(body)}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3725
|
+
};
|
|
3726
|
+
}
|
|
3727
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
3728
|
+
return {
|
|
3729
|
+
healthy: false,
|
|
3730
|
+
okWithNonJsonBody: true,
|
|
3731
|
+
error: `HTTP ${response.status}: invalid health response${bodyExcerpt(body)}: expected JSON object`
|
|
3732
|
+
};
|
|
3733
|
+
}
|
|
3734
|
+
const version2 = data.version;
|
|
3735
|
+
return typeof version2 === "string" ? { healthy: true, version: version2 } : { healthy: true };
|
|
2780
3736
|
} catch (error2) {
|
|
2781
3737
|
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
2782
3738
|
return { healthy: false, error: message };
|
|
@@ -2792,10 +3748,17 @@ async function checkOpenCode2Health(port, password) {
|
|
|
2792
3748
|
if (password !== void 0) {
|
|
2793
3749
|
headers.Authorization = buildOpenCodeBasicAuthHeader(password);
|
|
2794
3750
|
}
|
|
2795
|
-
|
|
3751
|
+
let response = await fetch(`http://127.0.0.1:${port}/api/info`, {
|
|
2796
3752
|
headers,
|
|
2797
3753
|
signal: AbortSignal.timeout(2e3)
|
|
2798
3754
|
});
|
|
3755
|
+
const usesHealthFallback = response.status === 404;
|
|
3756
|
+
if (usesHealthFallback) {
|
|
3757
|
+
response = await fetch(`http://127.0.0.1:${port}/api/health`, {
|
|
3758
|
+
headers,
|
|
3759
|
+
signal: AbortSignal.timeout(2e3)
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
2799
3762
|
if (response.status === 401) {
|
|
2800
3763
|
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
2801
3764
|
}
|
|
@@ -2810,23 +3773,29 @@ async function checkOpenCode2Health(port, password) {
|
|
|
2810
3773
|
} catch (error2) {
|
|
2811
3774
|
return {
|
|
2812
3775
|
healthy: false,
|
|
2813
|
-
error: `
|
|
3776
|
+
error: `HTTP ${response.status}: invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2814
3777
|
};
|
|
2815
3778
|
}
|
|
2816
3779
|
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
2817
3780
|
return {
|
|
2818
3781
|
healthy: false,
|
|
2819
|
-
error: `
|
|
3782
|
+
error: `HTTP ${response.status}: invalid health response${excerpt}: expected healthy=true`
|
|
2820
3783
|
};
|
|
2821
3784
|
}
|
|
2822
|
-
const
|
|
2823
|
-
if (
|
|
3785
|
+
const bodyObject = data;
|
|
3786
|
+
if (usesHealthFallback && bodyObject.healthy !== true) {
|
|
2824
3787
|
return {
|
|
2825
3788
|
healthy: false,
|
|
2826
3789
|
error: `Invalid health response${excerpt}: expected healthy=true`
|
|
2827
3790
|
};
|
|
2828
3791
|
}
|
|
2829
|
-
|
|
3792
|
+
if (!usesHealthFallback) {
|
|
3793
|
+
return typeof bodyObject.version === "string" && bodyObject.version.length > 0 ? { healthy: true, version: bodyObject.version } : {
|
|
3794
|
+
healthy: false,
|
|
3795
|
+
error: `HTTP ${response.status}: invalid info response${excerpt}: expected a non-empty version`
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3798
|
+
return typeof bodyObject.version === "string" ? { healthy: true, version: bodyObject.version } : { healthy: true };
|
|
2830
3799
|
} catch (error2) {
|
|
2831
3800
|
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
2832
3801
|
return { healthy: false, error: message };
|
|
@@ -2884,10 +3853,18 @@ function describeRequestOperation(input, init) {
|
|
|
2884
3853
|
function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
2885
3854
|
return (async (input, init) => {
|
|
2886
3855
|
const startedAt = Date.now();
|
|
3856
|
+
const controller = new AbortController();
|
|
3857
|
+
const abortFromCaller = () => controller.abort(init?.signal?.reason);
|
|
3858
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
3859
|
+
const abortFromTimeout = () => controller.abort(timeoutSignal.reason);
|
|
3860
|
+
if (init?.signal?.aborted) abortFromCaller();
|
|
3861
|
+
else init?.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
3862
|
+
if (timeoutSignal.aborted) abortFromTimeout();
|
|
3863
|
+
else timeoutSignal.addEventListener("abort", abortFromTimeout, { once: true });
|
|
2887
3864
|
try {
|
|
2888
|
-
return await fetchImpl(input, { ...init, signal:
|
|
3865
|
+
return await fetchImpl(input, { ...init, signal: controller.signal });
|
|
2889
3866
|
} catch (error2) {
|
|
2890
|
-
if (
|
|
3867
|
+
if (timeoutSignal.aborted) {
|
|
2891
3868
|
throw new RequestTimeoutError(
|
|
2892
3869
|
describeRequestOperation(input, init),
|
|
2893
3870
|
timeoutMs,
|
|
@@ -2895,11 +3872,21 @@ function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
|
2895
3872
|
);
|
|
2896
3873
|
}
|
|
2897
3874
|
throw error2;
|
|
3875
|
+
} finally {
|
|
3876
|
+
init?.signal?.removeEventListener("abort", abortFromCaller);
|
|
3877
|
+
timeoutSignal.removeEventListener("abort", abortFromTimeout);
|
|
2898
3878
|
}
|
|
2899
3879
|
});
|
|
2900
3880
|
}
|
|
2901
3881
|
|
|
2902
3882
|
// src/lib/opencode/client.ts
|
|
3883
|
+
var OpenCodeRequestError = class extends Error {
|
|
3884
|
+
constructor(statusCode, message) {
|
|
3885
|
+
super(message);
|
|
3886
|
+
this.statusCode = statusCode;
|
|
3887
|
+
this.name = "OpenCodeRequestError";
|
|
3888
|
+
}
|
|
3889
|
+
};
|
|
2903
3890
|
function redactPassword(message, password) {
|
|
2904
3891
|
return message.replaceAll(password, "[redacted]");
|
|
2905
3892
|
}
|
|
@@ -2921,7 +3908,8 @@ function createOpenCodeClient(options) {
|
|
|
2921
3908
|
const response = await fetchImpl(`${baseUrl}${path}`, requestInit);
|
|
2922
3909
|
if (!response.ok && !requestOptions?.allowStatuses?.includes(response.status)) {
|
|
2923
3910
|
const body = await response.text();
|
|
2924
|
-
throw new
|
|
3911
|
+
throw new OpenCodeRequestError(
|
|
3912
|
+
response.status,
|
|
2925
3913
|
`OpenCode request failed: HTTP ${response.status}${body ? `: ${body}` : ""}`
|
|
2926
3914
|
);
|
|
2927
3915
|
}
|
|
@@ -2929,7 +3917,11 @@ function createOpenCodeClient(options) {
|
|
|
2929
3917
|
} catch (error2) {
|
|
2930
3918
|
if (options.version === "v2" && password !== null) {
|
|
2931
3919
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2932
|
-
|
|
3920
|
+
const redactedMessage = redactPassword(message, password);
|
|
3921
|
+
if (error2 instanceof OpenCodeRequestError) {
|
|
3922
|
+
throw new OpenCodeRequestError(error2.statusCode, redactedMessage);
|
|
3923
|
+
}
|
|
3924
|
+
throw new Error(redactedMessage);
|
|
2933
3925
|
}
|
|
2934
3926
|
throw error2;
|
|
2935
3927
|
}
|
|
@@ -3078,28 +4070,38 @@ function sessionListPath(options) {
|
|
|
3078
4070
|
async function listSessionPage(port, client, options) {
|
|
3079
4071
|
const path = sessionListPath(options);
|
|
3080
4072
|
try {
|
|
3081
|
-
const res = await requestWithClient(
|
|
3082
|
-
|
|
4073
|
+
const res = await requestWithClient(
|
|
4074
|
+
port,
|
|
4075
|
+
client,
|
|
4076
|
+
path,
|
|
4077
|
+
options?.signal ? { signal: options.signal } : void 0,
|
|
4078
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
4079
|
+
);
|
|
4080
|
+
if (!res.ok) {
|
|
4081
|
+
console.error(`[listSessionPage] GET ${path} returned HTTP ${res.status} (port ${port})`);
|
|
4082
|
+
return null;
|
|
4083
|
+
}
|
|
3083
4084
|
const body = await res.json();
|
|
3084
4085
|
return Array.isArray(body) ? body : null;
|
|
3085
4086
|
} catch (error2) {
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
);
|
|
4087
|
+
if (options?.signal?.aborted) return null;
|
|
4088
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
4089
|
+
console.error(`[listSessionPage] GET ${path} failed (port ${port}): ${errorName}`);
|
|
3089
4090
|
return null;
|
|
3090
4091
|
}
|
|
3091
4092
|
}
|
|
3092
4093
|
async function listSessions(port, client, options) {
|
|
3093
|
-
if (client?.version === "v2") return listV2Sessions(client);
|
|
4094
|
+
if (client?.version === "v2") return listV2Sessions(client, options?.signal);
|
|
3094
4095
|
return listSessionPage(port, client, options);
|
|
3095
4096
|
}
|
|
3096
|
-
async function listAllSessions(port, client) {
|
|
4097
|
+
async function listAllSessions(port, client, signal) {
|
|
3097
4098
|
if (client?.version === "v2") {
|
|
3098
|
-
const sessions2 = await listV2Sessions(client);
|
|
4099
|
+
const sessions2 = await listV2Sessions(client, signal);
|
|
3099
4100
|
return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
|
|
3100
4101
|
}
|
|
3101
4102
|
const sessions = await listSessionPage(port, client, {
|
|
3102
|
-
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
|
|
4103
|
+
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS,
|
|
4104
|
+
signal
|
|
3103
4105
|
});
|
|
3104
4106
|
if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
|
|
3105
4107
|
if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
|
|
@@ -3595,21 +4597,24 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
3595
4597
|
}
|
|
3596
4598
|
return last;
|
|
3597
4599
|
}
|
|
4600
|
+
function assistantMessagesInTurnBlock(messages, userMessageId) {
|
|
4601
|
+
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
4602
|
+
if (userIndex === -1) return [];
|
|
4603
|
+
const assistants = [];
|
|
4604
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
4605
|
+
const message = messages[i];
|
|
4606
|
+
const role = roleOf2(message);
|
|
4607
|
+
if (role === "user") break;
|
|
4608
|
+
if (role === "assistant") assistants.push(message);
|
|
4609
|
+
}
|
|
4610
|
+
return assistants;
|
|
4611
|
+
}
|
|
3598
4612
|
function collectSubagentSessions(messages, userMessageId) {
|
|
3599
4613
|
if (!messages || messages.length === 0) return [];
|
|
3600
4614
|
const byParent = messages.filter(
|
|
3601
4615
|
(message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
|
|
3602
4616
|
);
|
|
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
|
-
}
|
|
4617
|
+
const assistants = byParent.length > 0 ? byParent : assistantMessagesInTurnBlock(messages, userMessageId);
|
|
3613
4618
|
const refs = [];
|
|
3614
4619
|
const seen = /* @__PURE__ */ new Set();
|
|
3615
4620
|
for (const message of assistants) {
|
|
@@ -3764,8 +4769,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
3764
4769
|
if (byParentAll.length > 0) {
|
|
3765
4770
|
correlated = byParentAll;
|
|
3766
4771
|
} else {
|
|
3767
|
-
|
|
3768
|
-
correlated = reply ? [reply] : [];
|
|
4772
|
+
correlated = assistantMessagesInTurnBlock(messages, userMessageId);
|
|
3769
4773
|
}
|
|
3770
4774
|
if (correlated.length === 0) return null;
|
|
3771
4775
|
return sumAssistantUsage(correlated);
|
|
@@ -3900,8 +4904,8 @@ function classifyReplyAuthError(reply) {
|
|
|
3900
4904
|
};
|
|
3901
4905
|
}
|
|
3902
4906
|
}
|
|
3903
|
-
const
|
|
3904
|
-
if (
|
|
4907
|
+
const errorMessage4 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
|
|
4908
|
+
if (errorMessage4 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage4.includes(knownMessage))) {
|
|
3905
4909
|
return {
|
|
3906
4910
|
kind: "model_auth",
|
|
3907
4911
|
providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
|
|
@@ -3987,39 +4991,85 @@ async function hasAnyConfiguredProvider(port, client) {
|
|
|
3987
4991
|
);
|
|
3988
4992
|
return null;
|
|
3989
4993
|
}
|
|
3990
|
-
const
|
|
4994
|
+
const query = `?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
|
|
4995
|
+
const integrationPath = `/api/integration${query}`;
|
|
4996
|
+
let integrationReady = null;
|
|
3991
4997
|
try {
|
|
3992
|
-
const res = await client.request(
|
|
4998
|
+
const res = await client.request(integrationPath);
|
|
3993
4999
|
if (!res.ok) {
|
|
3994
5000
|
console.error(
|
|
3995
|
-
`[hasAnyConfiguredProvider] GET ${
|
|
5001
|
+
`[hasAnyConfiguredProvider] GET ${integrationPath} returned HTTP ${res.status} (port ${port})`
|
|
3996
5002
|
);
|
|
3997
|
-
|
|
5003
|
+
} else {
|
|
5004
|
+
const body = await res.json();
|
|
5005
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
|
|
5006
|
+
console.error(
|
|
5007
|
+
`[hasAnyConfiguredProvider] GET ${integrationPath} body had no integration data array (port ${port})`
|
|
5008
|
+
);
|
|
5009
|
+
} else {
|
|
5010
|
+
let valid = true;
|
|
5011
|
+
for (const integration of body.data) {
|
|
5012
|
+
if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
5013
|
+
console.error(
|
|
5014
|
+
`[hasAnyConfiguredProvider] GET ${integrationPath} body contained an invalid integration (port ${port})`
|
|
5015
|
+
);
|
|
5016
|
+
valid = false;
|
|
5017
|
+
break;
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
if (valid) {
|
|
5021
|
+
integrationReady = body.data.some(
|
|
5022
|
+
(integration) => integration.connections.length > 0
|
|
5023
|
+
);
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
3998
5026
|
}
|
|
3999
|
-
|
|
4000
|
-
|
|
5027
|
+
} catch (error2) {
|
|
5028
|
+
console.error(
|
|
5029
|
+
`[hasAnyConfiguredProvider] GET ${integrationPath} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
5030
|
+
);
|
|
5031
|
+
}
|
|
5032
|
+
if (integrationReady === true) return true;
|
|
5033
|
+
const providerPath = `/api/provider${query}`;
|
|
5034
|
+
let providerReady = null;
|
|
5035
|
+
try {
|
|
5036
|
+
const res = await client.request(providerPath);
|
|
5037
|
+
if (!res.ok) {
|
|
4001
5038
|
console.error(
|
|
4002
|
-
`[hasAnyConfiguredProvider] GET ${
|
|
4003
|
-
);
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
5039
|
+
`[hasAnyConfiguredProvider] GET ${providerPath} returned HTTP ${res.status} (port ${port})`
|
|
5040
|
+
);
|
|
5041
|
+
} else {
|
|
5042
|
+
const body = await res.json();
|
|
5043
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
|
|
4008
5044
|
console.error(
|
|
4009
|
-
`[hasAnyConfiguredProvider] GET ${
|
|
5045
|
+
`[hasAnyConfiguredProvider] GET ${providerPath} body had no provider data array (port ${port})`
|
|
4010
5046
|
);
|
|
4011
|
-
|
|
5047
|
+
} else {
|
|
5048
|
+
let valid = true;
|
|
5049
|
+
for (const provider of body.data) {
|
|
5050
|
+
if (!provider || typeof provider !== "object" || Array.isArray(provider) || typeof provider.id !== "string") {
|
|
5051
|
+
console.error(
|
|
5052
|
+
`[hasAnyConfiguredProvider] GET ${providerPath} body contained an invalid provider (port ${port})`
|
|
5053
|
+
);
|
|
5054
|
+
valid = false;
|
|
5055
|
+
break;
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
if (valid) {
|
|
5059
|
+
providerReady = body.data.some(
|
|
5060
|
+
(provider) => provider.activation === "enabled"
|
|
5061
|
+
);
|
|
5062
|
+
}
|
|
4012
5063
|
}
|
|
4013
5064
|
}
|
|
4014
|
-
return body.data.some(
|
|
4015
|
-
(integration) => Array.isArray(integration.connections) && integration.connections.length > 0
|
|
4016
|
-
);
|
|
4017
5065
|
} catch (error2) {
|
|
4018
5066
|
console.error(
|
|
4019
|
-
`[hasAnyConfiguredProvider] GET ${
|
|
5067
|
+
`[hasAnyConfiguredProvider] GET ${providerPath} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4020
5068
|
);
|
|
4021
|
-
return null;
|
|
4022
5069
|
}
|
|
5070
|
+
if (providerReady === true) return true;
|
|
5071
|
+
if (integrationReady === null || providerReady === null) return null;
|
|
5072
|
+
return false;
|
|
4023
5073
|
}
|
|
4024
5074
|
try {
|
|
4025
5075
|
const res = await requestWithClient(port, client, "/config/providers");
|
|
@@ -4172,14 +5222,15 @@ async function probeOpenCodeRequestReadiness(port, client) {
|
|
|
4172
5222
|
}
|
|
4173
5223
|
|
|
4174
5224
|
// src/lib/opencode/session-v2.ts
|
|
4175
|
-
|
|
5225
|
+
var ALL_HTTP_STATUSES2 = Array.from({ length: 500 }, (_, index) => index + 100);
|
|
5226
|
+
function isRecord3(value) {
|
|
4176
5227
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4177
5228
|
}
|
|
4178
5229
|
function finiteNumber2(value) {
|
|
4179
5230
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
4180
5231
|
}
|
|
4181
5232
|
function adaptTime(value) {
|
|
4182
|
-
if (!
|
|
5233
|
+
if (!isRecord3(value)) return void 0;
|
|
4183
5234
|
const created = finiteNumber2(value.created);
|
|
4184
5235
|
const completed = finiteNumber2(value.completed);
|
|
4185
5236
|
if (created === void 0 && completed === void 0) return void 0;
|
|
@@ -4189,11 +5240,11 @@ function adaptTime(value) {
|
|
|
4189
5240
|
};
|
|
4190
5241
|
}
|
|
4191
5242
|
function adaptTokens(value) {
|
|
4192
|
-
if (!
|
|
5243
|
+
if (!isRecord3(value)) return void 0;
|
|
4193
5244
|
const input = finiteNumber2(value.input);
|
|
4194
5245
|
const output = finiteNumber2(value.output);
|
|
4195
5246
|
const reasoning = finiteNumber2(value.reasoning);
|
|
4196
|
-
const cache =
|
|
5247
|
+
const cache = isRecord3(value.cache) ? {
|
|
4197
5248
|
...finiteNumber2(value.cache.read) !== void 0 ? { read: finiteNumber2(value.cache.read) } : {},
|
|
4198
5249
|
...finiteNumber2(value.cache.write) !== void 0 ? { write: finiteNumber2(value.cache.write) } : {}
|
|
4199
5250
|
} : void 0;
|
|
@@ -4217,7 +5268,7 @@ function adaptMessageInfo(value, role) {
|
|
|
4217
5268
|
if (typeof value.finish === "string") info.finish = value.finish;
|
|
4218
5269
|
if ("error" in value) info.error = value.error;
|
|
4219
5270
|
if (typeof value.agent === "string") info.agent = value.agent;
|
|
4220
|
-
if (
|
|
5271
|
+
if (isRecord3(value.model)) {
|
|
4221
5272
|
if (typeof value.model.id === "string") info.modelID = value.model.id;
|
|
4222
5273
|
if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
|
|
4223
5274
|
}
|
|
@@ -4227,7 +5278,7 @@ function adaptMessageInfo(value, role) {
|
|
|
4227
5278
|
return info;
|
|
4228
5279
|
}
|
|
4229
5280
|
function adaptV2Message(value) {
|
|
4230
|
-
if (!
|
|
5281
|
+
if (!isRecord3(value) || typeof value.id !== "string" || typeof value.type !== "string") {
|
|
4231
5282
|
return null;
|
|
4232
5283
|
}
|
|
4233
5284
|
if (value.type === "user") {
|
|
@@ -4240,7 +5291,7 @@ function adaptV2Message(value) {
|
|
|
4240
5291
|
if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
|
|
4241
5292
|
const parts = [];
|
|
4242
5293
|
for (const content of value.content) {
|
|
4243
|
-
if (!
|
|
5294
|
+
if (!isRecord3(content) || typeof content.type !== "string") return null;
|
|
4244
5295
|
if (content.type === "text") {
|
|
4245
5296
|
if (typeof content.text !== "string") return null;
|
|
4246
5297
|
parts.push({ type: "text", text: content.text });
|
|
@@ -4254,23 +5305,23 @@ function adaptV2Message(value) {
|
|
|
4254
5305
|
};
|
|
4255
5306
|
}
|
|
4256
5307
|
function adaptFormTool(value) {
|
|
4257
|
-
if (!
|
|
5308
|
+
if (!isRecord3(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
|
|
4258
5309
|
return void 0;
|
|
4259
5310
|
}
|
|
4260
5311
|
return { messageID: value.messageID, callID: value.id };
|
|
4261
5312
|
}
|
|
4262
5313
|
function adaptV2FormWire(value) {
|
|
4263
|
-
if (!
|
|
5314
|
+
if (!isRecord3(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
4264
5315
|
return null;
|
|
4265
5316
|
}
|
|
4266
5317
|
return value;
|
|
4267
5318
|
}
|
|
4268
5319
|
function adaptV2FormField(value, header) {
|
|
4269
|
-
if (!
|
|
5320
|
+
if (!isRecord3(value)) return null;
|
|
4270
5321
|
const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
|
|
4271
5322
|
if (!question) return null;
|
|
4272
5323
|
const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
|
|
4273
|
-
if (!
|
|
5324
|
+
if (!isRecord3(option)) return [];
|
|
4274
5325
|
const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
|
|
4275
5326
|
if (!label) return [];
|
|
4276
5327
|
return [
|
|
@@ -4288,7 +5339,7 @@ function adaptV2Form(value) {
|
|
|
4288
5339
|
const header = typeof form.title === "string" ? form.title : "";
|
|
4289
5340
|
const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
|
|
4290
5341
|
if (questions.length === 0) return null;
|
|
4291
|
-
const tool =
|
|
5342
|
+
const tool = isRecord3(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
|
|
4292
5343
|
return {
|
|
4293
5344
|
id: form.id,
|
|
4294
5345
|
sessionID: form.sessionID,
|
|
@@ -4298,7 +5349,7 @@ function adaptV2Form(value) {
|
|
|
4298
5349
|
};
|
|
4299
5350
|
}
|
|
4300
5351
|
function adaptV2FormList(value) {
|
|
4301
|
-
if (!
|
|
5352
|
+
if (!isRecord3(value) || !Array.isArray(value.data)) return null;
|
|
4302
5353
|
return value.data.map(adaptV2Form).filter((form) => form !== null);
|
|
4303
5354
|
}
|
|
4304
5355
|
function adaptPattern(value) {
|
|
@@ -4309,7 +5360,7 @@ function adaptPattern(value) {
|
|
|
4309
5360
|
return void 0;
|
|
4310
5361
|
}
|
|
4311
5362
|
function adaptV2PermissionWire(value) {
|
|
4312
|
-
if (!
|
|
5363
|
+
if (!isRecord3(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
4313
5364
|
return null;
|
|
4314
5365
|
}
|
|
4315
5366
|
if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
|
|
@@ -4321,12 +5372,12 @@ function adaptV2Permission(value) {
|
|
|
4321
5372
|
const type = permission.permission ?? permission.action;
|
|
4322
5373
|
if (!type) return null;
|
|
4323
5374
|
const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
|
|
4324
|
-
const time =
|
|
5375
|
+
const time = isRecord3(permission.time) ? finiteNumber2(permission.time.created) !== void 0 ? { created: finiteNumber2(permission.time.created) } : void 0 : void 0;
|
|
4325
5376
|
return {
|
|
4326
5377
|
id: permission.id,
|
|
4327
5378
|
type,
|
|
4328
5379
|
sessionID: permission.sessionID,
|
|
4329
|
-
metadata:
|
|
5380
|
+
metadata: isRecord3(permission.metadata) ? permission.metadata : {},
|
|
4330
5381
|
raw: permission,
|
|
4331
5382
|
...pattern !== void 0 ? { pattern } : {},
|
|
4332
5383
|
...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
|
|
@@ -4336,12 +5387,12 @@ function adaptV2Permission(value) {
|
|
|
4336
5387
|
};
|
|
4337
5388
|
}
|
|
4338
5389
|
function adaptV2PermissionList(value) {
|
|
4339
|
-
if (!
|
|
5390
|
+
if (!isRecord3(value) || !Array.isArray(value.data)) return null;
|
|
4340
5391
|
return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
|
|
4341
5392
|
}
|
|
4342
5393
|
function adaptV2Session(value) {
|
|
4343
|
-
if (!
|
|
4344
|
-
const time =
|
|
5394
|
+
if (!isRecord3(value) || typeof value.id !== "string" || value.id.length === 0) return null;
|
|
5395
|
+
const time = isRecord3(value.time) ? {
|
|
4345
5396
|
...finiteNumber2(value.time.created) !== void 0 ? { created: finiteNumber2(value.time.created) } : {},
|
|
4346
5397
|
...finiteNumber2(value.time.updated) !== void 0 ? { updated: finiteNumber2(value.time.updated) } : {}
|
|
4347
5398
|
} : void 0;
|
|
@@ -4353,7 +5404,7 @@ function adaptV2Session(value) {
|
|
|
4353
5404
|
};
|
|
4354
5405
|
}
|
|
4355
5406
|
function adaptV2SessionList(value) {
|
|
4356
|
-
if (!
|
|
5407
|
+
if (!isRecord3(value) || !Array.isArray(value.data) || !isRecord3(value.cursor)) return null;
|
|
4357
5408
|
return {
|
|
4358
5409
|
data: value.data.map(adaptV2Session).filter((session) => session !== null),
|
|
4359
5410
|
cursor: value.cursor
|
|
@@ -4362,18 +5413,18 @@ function adaptV2SessionList(value) {
|
|
|
4362
5413
|
function adaptV2Location(value) {
|
|
4363
5414
|
const candidates = [
|
|
4364
5415
|
value,
|
|
4365
|
-
|
|
4366
|
-
|
|
5416
|
+
isRecord3(value) ? value.data : void 0,
|
|
5417
|
+
isRecord3(value) ? value.location : void 0
|
|
4367
5418
|
];
|
|
4368
5419
|
for (const candidate of candidates) {
|
|
4369
|
-
if (!
|
|
5420
|
+
if (!isRecord3(candidate) || typeof candidate.directory !== "string") continue;
|
|
4370
5421
|
const directory = candidate.directory.trim();
|
|
4371
5422
|
if (directory) return directory;
|
|
4372
5423
|
}
|
|
4373
5424
|
return null;
|
|
4374
5425
|
}
|
|
4375
5426
|
function adaptV2Messages(value) {
|
|
4376
|
-
if (!
|
|
5427
|
+
if (!isRecord3(value) || !Array.isArray(value.data)) return [];
|
|
4377
5428
|
return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
|
|
4378
5429
|
}
|
|
4379
5430
|
async function readJson(response) {
|
|
@@ -4388,7 +5439,7 @@ async function readJson(response) {
|
|
|
4388
5439
|
async function readData(client, path, init) {
|
|
4389
5440
|
const response = await client.request(path, init);
|
|
4390
5441
|
const body = await readJson(response);
|
|
4391
|
-
if (!
|
|
5442
|
+
if (!isRecord3(body) || !("data" in body)) {
|
|
4392
5443
|
throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
|
|
4393
5444
|
}
|
|
4394
5445
|
return body.data;
|
|
@@ -4399,6 +5450,14 @@ var OpenCodeV2PromptAckError = class extends Error {
|
|
|
4399
5450
|
this.name = "OpenCodeV2PromptAckError";
|
|
4400
5451
|
}
|
|
4401
5452
|
};
|
|
5453
|
+
var OpenCodeV2ModelSwitchError = class extends Error {
|
|
5454
|
+
statusCode;
|
|
5455
|
+
constructor(error2) {
|
|
5456
|
+
super(error2 instanceof Error ? error2.message : String(error2));
|
|
5457
|
+
this.name = "OpenCodeV2ModelSwitchError";
|
|
5458
|
+
this.statusCode = error2 instanceof OpenCodeRequestError ? error2.statusCode : null;
|
|
5459
|
+
}
|
|
5460
|
+
};
|
|
4402
5461
|
async function getOpenCodeDirectoryV2(client) {
|
|
4403
5462
|
try {
|
|
4404
5463
|
return adaptV2Location(await readJson(await client.request("/api/location")));
|
|
@@ -4415,7 +5474,7 @@ async function createV2Session(client, directory) {
|
|
|
4415
5474
|
headers: { "Content-Type": "application/json" },
|
|
4416
5475
|
body: JSON.stringify({ location: { directory } })
|
|
4417
5476
|
});
|
|
4418
|
-
if (!
|
|
5477
|
+
if (!isRecord3(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
4419
5478
|
throw new Error("OpenCode V2 create session response was missing data.id");
|
|
4420
5479
|
}
|
|
4421
5480
|
return data.id;
|
|
@@ -4426,25 +5485,35 @@ async function getV2Session(client, sessionId) {
|
|
|
4426
5485
|
if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
|
|
4427
5486
|
return session;
|
|
4428
5487
|
}
|
|
4429
|
-
async function listV2SessionPage(client, cursor) {
|
|
5488
|
+
async function listV2SessionPage(client, cursor, signal) {
|
|
5489
|
+
const operation = cursor ? "GET /api/session (next page)" : "GET /api/session";
|
|
4430
5490
|
const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
|
|
4431
5491
|
try {
|
|
4432
|
-
|
|
5492
|
+
const response = await client.request(path, signal ? { signal } : void 0, {
|
|
5493
|
+
allowStatuses: ALL_HTTP_STATUSES2
|
|
5494
|
+
});
|
|
5495
|
+
if (!response.ok) {
|
|
5496
|
+
console.error(
|
|
5497
|
+
`[listV2SessionPage] ${operation} returned HTTP ${response.status} (port ${client.port})`
|
|
5498
|
+
);
|
|
5499
|
+
return null;
|
|
5500
|
+
}
|
|
5501
|
+
return adaptV2SessionList(await readJson(response));
|
|
4433
5502
|
} catch (error2) {
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
);
|
|
5503
|
+
if (signal?.aborted) return null;
|
|
5504
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
5505
|
+
console.error(`[listV2SessionPage] ${operation} failed (port ${client.port}): ${errorName}`);
|
|
4437
5506
|
return null;
|
|
4438
5507
|
}
|
|
4439
5508
|
}
|
|
4440
|
-
async function listV2Sessions(client) {
|
|
5509
|
+
async function listV2Sessions(client, signal) {
|
|
4441
5510
|
const sessions = [];
|
|
4442
5511
|
const seenCursors = /* @__PURE__ */ new Set();
|
|
4443
5512
|
let cursor;
|
|
4444
5513
|
let hasNextPage = true;
|
|
4445
5514
|
try {
|
|
4446
5515
|
while (hasNextPage) {
|
|
4447
|
-
const page = await listV2SessionPage(client, cursor);
|
|
5516
|
+
const page = await listV2SessionPage(client, cursor, signal);
|
|
4448
5517
|
if (!page) return null;
|
|
4449
5518
|
sessions.push(...page.data);
|
|
4450
5519
|
const next = page.cursor.next;
|
|
@@ -4460,8 +5529,9 @@ async function listV2Sessions(client) {
|
|
|
4460
5529
|
}
|
|
4461
5530
|
return sessions;
|
|
4462
5531
|
} catch (error2) {
|
|
5532
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
4463
5533
|
console.error(
|
|
4464
|
-
`[listV2Sessions] session pagination failed
|
|
5534
|
+
`[listV2Sessions] GET /api/session pagination failed (port ${client.port}): ${errorName}`
|
|
4465
5535
|
);
|
|
4466
5536
|
return null;
|
|
4467
5537
|
}
|
|
@@ -4471,7 +5541,7 @@ async function deleteV2Session(client, sessionId) {
|
|
|
4471
5541
|
const response = await client.request(
|
|
4472
5542
|
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
4473
5543
|
{ method: "DELETE" },
|
|
4474
|
-
{ allowStatuses:
|
|
5544
|
+
{ allowStatuses: ALL_HTTP_STATUSES2 }
|
|
4475
5545
|
);
|
|
4476
5546
|
if (response.status >= 200 && response.status < 300) {
|
|
4477
5547
|
return { ok: true, status: response.status };
|
|
@@ -4507,7 +5577,34 @@ async function v2SessionExists(client, sessionId) {
|
|
|
4507
5577
|
return null;
|
|
4508
5578
|
}
|
|
4509
5579
|
}
|
|
4510
|
-
async function sendV2Prompt(client, sessionId, text) {
|
|
5580
|
+
async function sendV2Prompt(client, sessionId, text, options, onInvalidModelSelection) {
|
|
5581
|
+
const modelSelection = options?.model?.trim();
|
|
5582
|
+
const { model, variant } = splitModelVariant(modelSelection);
|
|
5583
|
+
const separator = model?.indexOf("/") ?? -1;
|
|
5584
|
+
const modelReference = model && separator > 0 && separator < model.length - 1 ? {
|
|
5585
|
+
id: model.slice(separator + 1),
|
|
5586
|
+
providerID: model.slice(0, separator),
|
|
5587
|
+
...variant ? { variant } : {}
|
|
5588
|
+
} : void 0;
|
|
5589
|
+
if (modelSelection && !modelReference) onInvalidModelSelection?.();
|
|
5590
|
+
if (options?.agent) {
|
|
5591
|
+
await client.request(`/api/session/${encodeURIComponent(sessionId)}/agent`, {
|
|
5592
|
+
method: "POST",
|
|
5593
|
+
headers: { "Content-Type": "application/json" },
|
|
5594
|
+
body: JSON.stringify({ agent: options.agent })
|
|
5595
|
+
});
|
|
5596
|
+
}
|
|
5597
|
+
if (modelReference) {
|
|
5598
|
+
try {
|
|
5599
|
+
await client.request(`/api/session/${encodeURIComponent(sessionId)}/model`, {
|
|
5600
|
+
method: "POST",
|
|
5601
|
+
headers: { "Content-Type": "application/json" },
|
|
5602
|
+
body: JSON.stringify({ model: modelReference })
|
|
5603
|
+
});
|
|
5604
|
+
} catch (error2) {
|
|
5605
|
+
throw new OpenCodeV2ModelSwitchError(error2);
|
|
5606
|
+
}
|
|
5607
|
+
}
|
|
4511
5608
|
const path = `/api/session/${encodeURIComponent(sessionId)}/prompt`;
|
|
4512
5609
|
const response = await client.request(path, {
|
|
4513
5610
|
method: "POST",
|
|
@@ -4522,8 +5619,8 @@ async function sendV2Prompt(client, sessionId, text) {
|
|
|
4522
5619
|
`OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4523
5620
|
);
|
|
4524
5621
|
}
|
|
4525
|
-
const data =
|
|
4526
|
-
if (!
|
|
5622
|
+
const data = isRecord3(body) && "data" in body ? body.data : void 0;
|
|
5623
|
+
if (!isRecord3(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
4527
5624
|
throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
|
|
4528
5625
|
}
|
|
4529
5626
|
return data.id;
|
|
@@ -4532,7 +5629,7 @@ async function getV2SessionMessages(client, sessionId) {
|
|
|
4532
5629
|
const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
|
|
4533
5630
|
try {
|
|
4534
5631
|
const body = await readJson(await client.request(path));
|
|
4535
|
-
if (!
|
|
5632
|
+
if (!isRecord3(body) || !Array.isArray(body.data) || !isRecord3(body.cursor)) return null;
|
|
4536
5633
|
return adaptV2Messages(body);
|
|
4537
5634
|
} catch (error2) {
|
|
4538
5635
|
console.error(
|
|
@@ -4571,7 +5668,7 @@ async function interruptV2Session(client, sessionId) {
|
|
|
4571
5668
|
async function getV2ActiveSessions(client) {
|
|
4572
5669
|
try {
|
|
4573
5670
|
const body = await readJson(await client.request("/api/session/active"));
|
|
4574
|
-
if (!
|
|
5671
|
+
if (!isRecord3(body) || !isRecord3(body.data)) return null;
|
|
4575
5672
|
return body.data;
|
|
4576
5673
|
} catch (error2) {
|
|
4577
5674
|
console.error(
|
|
@@ -4587,8 +5684,8 @@ async function isV2SessionOngoing(client, sessionId) {
|
|
|
4587
5684
|
}
|
|
4588
5685
|
function sessionErrorReason2(value) {
|
|
4589
5686
|
if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
|
|
4590
|
-
if (
|
|
4591
|
-
const data =
|
|
5687
|
+
if (isRecord3(value)) {
|
|
5688
|
+
const data = isRecord3(value.data) ? value.data : void 0;
|
|
4592
5689
|
const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
|
|
4593
5690
|
if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
4594
5691
|
}
|
|
@@ -4604,17 +5701,17 @@ function adaptV2SessionErrorEvent(value) {
|
|
|
4604
5701
|
return null;
|
|
4605
5702
|
}
|
|
4606
5703
|
}
|
|
4607
|
-
if (!
|
|
5704
|
+
if (!isRecord3(parsed)) return null;
|
|
4608
5705
|
try {
|
|
4609
5706
|
const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
|
|
4610
5707
|
if (establishedShape) return establishedShape;
|
|
4611
5708
|
} catch (error2) {
|
|
4612
5709
|
void error2;
|
|
4613
5710
|
}
|
|
4614
|
-
const candidates = [parsed, parsed.payload, parsed.data].filter(
|
|
5711
|
+
const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord3);
|
|
4615
5712
|
for (const event of candidates) {
|
|
4616
5713
|
if (event.type !== "session.error") continue;
|
|
4617
|
-
const properties = [event.properties, event.data, event].find(
|
|
5714
|
+
const properties = [event.properties, event.data, event].find(isRecord3);
|
|
4618
5715
|
if (!properties) continue;
|
|
4619
5716
|
const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
|
|
4620
5717
|
if (!sessionId) continue;
|
|
@@ -4664,13 +5761,13 @@ async function readV2SessionErrorStream(client, options) {
|
|
|
4664
5761
|
}
|
|
4665
5762
|
|
|
4666
5763
|
// src/lib/opencode/credential-import-v2.ts
|
|
4667
|
-
import { readFileSync as
|
|
4668
|
-
import { join as
|
|
5764
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
5765
|
+
import { join as join8 } from "node:path";
|
|
4669
5766
|
|
|
4670
5767
|
// src/lib/openai-usage.ts
|
|
4671
|
-
import { readFileSync as
|
|
5768
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4672
5769
|
import { homedir as homedir3 } from "node:os";
|
|
4673
|
-
import { join as
|
|
5770
|
+
import { join as join7 } from "node:path";
|
|
4674
5771
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
4675
5772
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
4676
5773
|
var OpenAiUsageError = class extends Error {
|
|
@@ -4684,7 +5781,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
4684
5781
|
}
|
|
4685
5782
|
function readOpenCodeChatGptCredentials() {
|
|
4686
5783
|
try {
|
|
4687
|
-
const raw =
|
|
5784
|
+
const raw = readFileSync7(join7(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
4688
5785
|
let parsed;
|
|
4689
5786
|
try {
|
|
4690
5787
|
parsed = JSON.parse(raw);
|
|
@@ -4750,7 +5847,7 @@ function parseCodexUsageHeaders(headers) {
|
|
|
4750
5847
|
function normalizeProbeModel(model) {
|
|
4751
5848
|
return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
|
|
4752
5849
|
}
|
|
4753
|
-
function
|
|
5850
|
+
function isRecord4(value) {
|
|
4754
5851
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4755
5852
|
}
|
|
4756
5853
|
function unsupportedProbeModels(reason, port) {
|
|
@@ -4761,14 +5858,14 @@ async function resolveV1ProbeModels(client, port) {
|
|
|
4761
5858
|
try {
|
|
4762
5859
|
const response = await client.request("/config/providers");
|
|
4763
5860
|
const body = await response.json();
|
|
4764
|
-
if (!
|
|
5861
|
+
if (!isRecord4(body) || !Array.isArray(body.providers)) {
|
|
4765
5862
|
return unsupportedProbeModels("V1 provider response did not contain a providers array", port);
|
|
4766
5863
|
}
|
|
4767
5864
|
const provider = body.providers.find(
|
|
4768
|
-
(candidate) =>
|
|
5865
|
+
(candidate) => isRecord4(candidate) && candidate.id === "openai"
|
|
4769
5866
|
);
|
|
4770
|
-
if (!provider || !
|
|
4771
|
-
const defaults2 =
|
|
5867
|
+
if (!provider || !isRecord4(provider.models)) return { status: "supported", models: [] };
|
|
5868
|
+
const defaults2 = isRecord4(body.default) ? body.default : void 0;
|
|
4772
5869
|
const candidates = [
|
|
4773
5870
|
...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
|
|
4774
5871
|
...Object.keys(provider.models)
|
|
@@ -4790,14 +5887,14 @@ async function resolveV2ProbeModels(client, port) {
|
|
|
4790
5887
|
try {
|
|
4791
5888
|
const response = await client.request(path);
|
|
4792
5889
|
const body = await response.json();
|
|
4793
|
-
if (!
|
|
5890
|
+
if (!isRecord4(body) || !Array.isArray(body.data)) {
|
|
4794
5891
|
return unsupportedProbeModels(`V2 GET ${path} did not contain a provider data array`, port);
|
|
4795
5892
|
}
|
|
4796
5893
|
const provider = body.data.find(
|
|
4797
|
-
(candidate) =>
|
|
5894
|
+
(candidate) => isRecord4(candidate) && candidate.id === "openai"
|
|
4798
5895
|
);
|
|
4799
5896
|
if (!provider) return { status: "supported", models: [] };
|
|
4800
|
-
if (!
|
|
5897
|
+
if (!isRecord4(provider.models)) {
|
|
4801
5898
|
return unsupportedProbeModels(
|
|
4802
5899
|
"V2 provider response has no safe OpenAI model catalogue",
|
|
4803
5900
|
port
|
|
@@ -4907,7 +6004,7 @@ var CONNECT_KEY_MAX_ATTEMPTS = 3;
|
|
|
4907
6004
|
var CONNECT_KEY_RETRY_DELAY_MS = 250;
|
|
4908
6005
|
var INTEGRATION_LIST_MAX_READS = 10;
|
|
4909
6006
|
var INTEGRATION_LIST_RETRY_DELAY_MS = 1e3;
|
|
4910
|
-
function
|
|
6007
|
+
function isRecord5(value) {
|
|
4911
6008
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4912
6009
|
}
|
|
4913
6010
|
var sleep2 = (milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds));
|
|
@@ -4916,14 +6013,14 @@ function emptyResult(unsupported = [], outcome = "completed") {
|
|
|
4916
6013
|
}
|
|
4917
6014
|
function readOpenCodeCredentials(homeDir, log3) {
|
|
4918
6015
|
try {
|
|
4919
|
-
const raw =
|
|
6016
|
+
const raw = readFileSync8(join8(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
|
|
4920
6017
|
let parsed;
|
|
4921
6018
|
try {
|
|
4922
6019
|
parsed = JSON.parse(raw);
|
|
4923
6020
|
} catch (error2) {
|
|
4924
6021
|
return null;
|
|
4925
6022
|
}
|
|
4926
|
-
return
|
|
6023
|
+
return isRecord5(parsed) ? parsed : null;
|
|
4927
6024
|
} catch (error2) {
|
|
4928
6025
|
const code = error2.code;
|
|
4929
6026
|
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
@@ -4936,7 +6033,7 @@ function classifyOpenCodeCredentials(credentials2) {
|
|
|
4936
6033
|
const importable = [];
|
|
4937
6034
|
const unsupported = [];
|
|
4938
6035
|
for (const [integrationId, value] of Object.entries(credentials2)) {
|
|
4939
|
-
if (!
|
|
6036
|
+
if (!isRecord5(value) || typeof value.type !== "string") {
|
|
4940
6037
|
unsupported.push({ integrationId, reason: "unrecognised credential entry" });
|
|
4941
6038
|
continue;
|
|
4942
6039
|
}
|
|
@@ -4951,7 +6048,7 @@ function classifyOpenCodeCredentials(credentials2) {
|
|
|
4951
6048
|
unsupported.push({ integrationId, reason: "unrecognised credential entry" });
|
|
4952
6049
|
continue;
|
|
4953
6050
|
}
|
|
4954
|
-
if (value.metadata === void 0 ||
|
|
6051
|
+
if (value.metadata === void 0 || isRecord5(value.metadata) && Object.keys(value.metadata).length === 0) {
|
|
4955
6052
|
importable.push({ integrationId, key: value.key });
|
|
4956
6053
|
} else {
|
|
4957
6054
|
unsupported.push({ integrationId, reason: "needs the interactive connect flow" });
|
|
@@ -4964,7 +6061,7 @@ function statusFromRequestError(error2) {
|
|
|
4964
6061
|
const match = /^OpenCode request failed: HTTP (\d{3})/.exec(error2.message);
|
|
4965
6062
|
return match ? Number(match[1]) : void 0;
|
|
4966
6063
|
}
|
|
4967
|
-
function
|
|
6064
|
+
function failureMessage2(status2) {
|
|
4968
6065
|
return status2 === void 0 ? "OpenCode credential import request failed" : `OpenCode credential import request failed with HTTP ${status2}`;
|
|
4969
6066
|
}
|
|
4970
6067
|
function isRetryableStatus(status2) {
|
|
@@ -4991,18 +6088,18 @@ async function importOneCredential(request3, credential, sleepForRetry, log3) {
|
|
|
4991
6088
|
const failure = {
|
|
4992
6089
|
integrationId: credential.integrationId,
|
|
4993
6090
|
status: response.status,
|
|
4994
|
-
message:
|
|
6091
|
+
message: failureMessage2(response.status)
|
|
4995
6092
|
};
|
|
4996
|
-
log3(`${
|
|
6093
|
+
log3(`${failureMessage2(failure.status)} for ${failure.integrationId}`, "warn");
|
|
4997
6094
|
return { ok: false, failure };
|
|
4998
6095
|
} catch (error2) {
|
|
4999
6096
|
const status2 = statusFromRequestError(error2);
|
|
5000
6097
|
const failure = {
|
|
5001
6098
|
integrationId: credential.integrationId,
|
|
5002
6099
|
...status2 === void 0 ? {} : { status: status2 },
|
|
5003
|
-
message:
|
|
6100
|
+
message: failureMessage2(status2)
|
|
5004
6101
|
};
|
|
5005
|
-
log3(`${
|
|
6102
|
+
log3(`${failureMessage2(status2)} for ${failure.integrationId}`, "warn");
|
|
5006
6103
|
return { ok: false, failure };
|
|
5007
6104
|
}
|
|
5008
6105
|
}
|
|
@@ -5028,10 +6125,10 @@ async function importOpenCodeCredentials({
|
|
|
5028
6125
|
return result;
|
|
5029
6126
|
}
|
|
5030
6127
|
function parseIntegrationList(value) {
|
|
5031
|
-
if (!
|
|
6128
|
+
if (!isRecord5(value) || !Array.isArray(value.data)) return null;
|
|
5032
6129
|
const integrationIds = /* @__PURE__ */ new Set();
|
|
5033
6130
|
for (const integration of value.data) {
|
|
5034
|
-
if (!
|
|
6131
|
+
if (!isRecord5(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
5035
6132
|
return null;
|
|
5036
6133
|
}
|
|
5037
6134
|
if (integration.connections.length > 0) integrationIds.add(integration.id);
|
|
@@ -5116,87 +6213,9 @@ async function importOpenCodeV2Credentials({
|
|
|
5116
6213
|
|
|
5117
6214
|
// src/lib/opencode/session-db-boot.ts
|
|
5118
6215
|
import { spawn as spawn2 } from "node:child_process";
|
|
5119
|
-
import { mkdirSync as
|
|
6216
|
+
import { mkdirSync as mkdirSync4, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5120
6217
|
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
|
|
6218
|
+
import { dirname as dirname7, resolve as resolvePath } from "node:path";
|
|
5200
6219
|
var SESSION_DB_RESTORE_TIMEOUT_MS = 3e5;
|
|
5201
6220
|
var SESSION_DB_VERIFY_TIMEOUT_MS = 36e4;
|
|
5202
6221
|
var SESSION_DB_SYNCHRONISER_TIMEOUT_MS = 12e4;
|
|
@@ -5246,8 +6265,8 @@ function clearMarker(options) {
|
|
|
5246
6265
|
function markNoReplicate(options, message) {
|
|
5247
6266
|
if (options.noReplicateMarker) {
|
|
5248
6267
|
try {
|
|
5249
|
-
|
|
5250
|
-
|
|
6268
|
+
mkdirSync4(dirname7(options.noReplicateMarker), { recursive: true });
|
|
6269
|
+
writeFileSync4(options.noReplicateMarker, "");
|
|
5251
6270
|
} catch (error2) {
|
|
5252
6271
|
options.log(
|
|
5253
6272
|
`Could not write session-DB no-replicate marker ${options.noReplicateMarker}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
@@ -5401,8 +6420,8 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
5401
6420
|
return null;
|
|
5402
6421
|
}
|
|
5403
6422
|
try {
|
|
5404
|
-
|
|
5405
|
-
|
|
6423
|
+
mkdirSync4(dirname7(configPath), { recursive: true });
|
|
6424
|
+
writeFileSync4(configPath, rendered.stdout);
|
|
5406
6425
|
} catch (error2) {
|
|
5407
6426
|
options.log(
|
|
5408
6427
|
`Could not write ${configPath}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
@@ -5667,8 +6686,8 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
5667
6686
|
|
|
5668
6687
|
// src/lib/opencode/session-db-provenance.ts
|
|
5669
6688
|
import { createRequire } from "node:module";
|
|
5670
|
-
import { mkdirSync as
|
|
5671
|
-
import { dirname as
|
|
6689
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6690
|
+
import { dirname as dirname8, join as join9 } from "node:path";
|
|
5672
6691
|
var require2 = createRequire(import.meta.url);
|
|
5673
6692
|
function readSessionDbMigrationIds(dbPath) {
|
|
5674
6693
|
let db;
|
|
@@ -5705,12 +6724,12 @@ function readSessionDbMigrationIds(dbPath) {
|
|
|
5705
6724
|
}
|
|
5706
6725
|
function sessionDbProvenanceStatePath(homeDir, env) {
|
|
5707
6726
|
const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
|
|
5708
|
-
return override ||
|
|
6727
|
+
return override || join9(homeDir, ".local", "state", "evident", "session-db-provenance.json");
|
|
5709
6728
|
}
|
|
5710
6729
|
function loadSessionDbProvenanceState(path) {
|
|
5711
6730
|
let value;
|
|
5712
6731
|
try {
|
|
5713
|
-
value = JSON.parse(
|
|
6732
|
+
value = JSON.parse(readFileSync9(path, "utf8"));
|
|
5714
6733
|
} catch (error2) {
|
|
5715
6734
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return {};
|
|
5716
6735
|
console.error(
|
|
@@ -5734,8 +6753,8 @@ function loadSessionDbProvenanceState(path) {
|
|
|
5734
6753
|
}
|
|
5735
6754
|
function saveSessionDbProvenanceState(path, state) {
|
|
5736
6755
|
try {
|
|
5737
|
-
|
|
5738
|
-
|
|
6756
|
+
mkdirSync5(dirname8(path), { recursive: true });
|
|
6757
|
+
writeFileSync5(path, `${JSON.stringify(state, null, 2)}
|
|
5739
6758
|
`, "utf8");
|
|
5740
6759
|
} catch (error2) {
|
|
5741
6760
|
console.error(
|
|
@@ -5790,6 +6809,26 @@ function isSessionDbProvenanceRecord(value) {
|
|
|
5790
6809
|
return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
|
|
5791
6810
|
}
|
|
5792
6811
|
|
|
6812
|
+
// src/lib/opencode/detect.ts
|
|
6813
|
+
function describeProbe(route, result) {
|
|
6814
|
+
if (result.error?.startsWith("HTTP ")) return `${route}: ${result.error}`;
|
|
6815
|
+
if (result.error) return `${route}: no HTTP response (${result.error})`;
|
|
6816
|
+
return `${route}: ${result.healthy ? "healthy" : "no response detail"}`;
|
|
6817
|
+
}
|
|
6818
|
+
async function classifyRunningOpenCode(port) {
|
|
6819
|
+
const v1 = await checkOpenCodeHealth(port);
|
|
6820
|
+
if (v1.healthy) return { kind: "v1", version: v1.version ?? null };
|
|
6821
|
+
const v2 = await checkOpenCode2Health(port);
|
|
6822
|
+
if (v2.healthy) return { kind: "v2", version: v2.version ?? null };
|
|
6823
|
+
const detail = [
|
|
6824
|
+
describeProbe("V1 /global/health", v1),
|
|
6825
|
+
describeProbe("V2 /api/info (or /api/health fallback)", v2)
|
|
6826
|
+
].join("; ");
|
|
6827
|
+
if (v1.okWithNonJsonBody && v2.authFailed) return { kind: "v2", version: null };
|
|
6828
|
+
if (v1.authFailed && v2.authFailed) return { kind: "authenticated-unknown", detail };
|
|
6829
|
+
return { kind: "no-server-identified", detail };
|
|
6830
|
+
}
|
|
6831
|
+
|
|
5793
6832
|
// src/lib/opencode/opencode2-package.ts
|
|
5794
6833
|
var OPENCODE2_PACKAGE_SPEC = "@opencode-ai/cli@0.0.0-beta-17823";
|
|
5795
6834
|
|
|
@@ -6098,26 +7137,28 @@ async function startOpenCode(port, options = {}) {
|
|
|
6098
7137
|
}
|
|
6099
7138
|
async function startOpenCode2(port, options = {}) {
|
|
6100
7139
|
const password = randomBytes(24).toString("hex");
|
|
6101
|
-
let command = "opencode2";
|
|
7140
|
+
let command = options.command ?? "opencode2";
|
|
6102
7141
|
const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
|
|
6103
7142
|
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
|
-
|
|
7143
|
+
if (options.command === void 0) {
|
|
7144
|
+
try {
|
|
7145
|
+
execSync("which opencode2", { stdio: "ignore" });
|
|
7146
|
+
} catch {
|
|
7147
|
+
command = "npx";
|
|
7148
|
+
args = [
|
|
7149
|
+
"-y",
|
|
7150
|
+
"-p",
|
|
7151
|
+
OPENCODE2_PACKAGE_SPEC,
|
|
7152
|
+
"--",
|
|
7153
|
+
"opencode2",
|
|
7154
|
+
"serve",
|
|
7155
|
+
"--port",
|
|
7156
|
+
port.toString(),
|
|
7157
|
+
"--hostname",
|
|
7158
|
+
"127.0.0.1",
|
|
7159
|
+
...logLevel
|
|
7160
|
+
];
|
|
7161
|
+
}
|
|
6121
7162
|
}
|
|
6122
7163
|
const child = spawn3(command, args, {
|
|
6123
7164
|
env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
|
|
@@ -6174,6 +7215,56 @@ function isOpenCode2Installed() {
|
|
|
6174
7215
|
return false;
|
|
6175
7216
|
}
|
|
6176
7217
|
}
|
|
7218
|
+
function readVersionOutput(command) {
|
|
7219
|
+
try {
|
|
7220
|
+
return execSync2(`${command} --version`, {
|
|
7221
|
+
encoding: "utf-8",
|
|
7222
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
7223
|
+
}).trim();
|
|
7224
|
+
} catch (error2) {
|
|
7225
|
+
const output = typeof error2 === "object" && error2 !== null ? [
|
|
7226
|
+
"stdout" in error2 ? String(error2.stdout ?? "").trim() : "",
|
|
7227
|
+
"stderr" in error2 ? String(error2.stderr ?? "").trim() : ""
|
|
7228
|
+
].filter(Boolean).join("\n") : "";
|
|
7229
|
+
return output || (error2 instanceof Error ? error2.message : String(error2));
|
|
7230
|
+
}
|
|
7231
|
+
}
|
|
7232
|
+
function parseOpenCodeVersion(rawVersion) {
|
|
7233
|
+
const output = rawVersion.trim();
|
|
7234
|
+
const prefix = output.match(/^(opencode2|opencode)\s+/i)?.[1]?.toLowerCase();
|
|
7235
|
+
const version2 = (prefix ? output.slice(prefix.length).trim() : output).replace(/^v/i, "");
|
|
7236
|
+
if (prefix === "opencode2") return { major: "v2", version: version2 };
|
|
7237
|
+
const major = version2.match(/^(\d+)(?:\.\d+)*(?:[-+][0-9A-Za-z.-]+)?$/)?.[1];
|
|
7238
|
+
if (!major) return null;
|
|
7239
|
+
if (Number(major) === 1) return { major: "v1", version: version2 };
|
|
7240
|
+
if (Number(major) >= 2) return { major: "v2", version: version2 };
|
|
7241
|
+
return null;
|
|
7242
|
+
}
|
|
7243
|
+
function resolveInstalledOpenCode() {
|
|
7244
|
+
let opencode2Available = false;
|
|
7245
|
+
if (isOpenCodeInstalled()) {
|
|
7246
|
+
const rawVersion = readVersionOutput("opencode");
|
|
7247
|
+
const parsed = parseOpenCodeVersion(rawVersion);
|
|
7248
|
+
if (parsed?.major === "v1") {
|
|
7249
|
+
return { kind: "installed", major: "v1", command: "opencode", version: parsed.version };
|
|
7250
|
+
}
|
|
7251
|
+
if (parsed?.major === "v2") {
|
|
7252
|
+
return { kind: "installed", major: "v2", command: "opencode", version: parsed.version };
|
|
7253
|
+
}
|
|
7254
|
+
opencode2Available = isOpenCode2Installed();
|
|
7255
|
+
if (!opencode2Available) return { kind: "unknown", command: "opencode", rawVersion };
|
|
7256
|
+
}
|
|
7257
|
+
if (opencode2Available || isOpenCode2Installed()) {
|
|
7258
|
+
const parsed = parseOpenCodeVersion(readVersionOutput("opencode2"));
|
|
7259
|
+
return {
|
|
7260
|
+
kind: "installed",
|
|
7261
|
+
major: "v2",
|
|
7262
|
+
command: "opencode2",
|
|
7263
|
+
version: parsed?.version
|
|
7264
|
+
};
|
|
7265
|
+
}
|
|
7266
|
+
return { kind: "none" };
|
|
7267
|
+
}
|
|
6177
7268
|
async function promptOpenCodeInstall(interactive) {
|
|
6178
7269
|
if (!interactive) {
|
|
6179
7270
|
console.log(
|
|
@@ -6268,130 +7359,23 @@ async function promptOpenCodeInstall(interactive) {
|
|
|
6268
7359
|
return action;
|
|
6269
7360
|
}
|
|
6270
7361
|
|
|
6271
|
-
// src/lib/opencode/provider-check.ts
|
|
6272
|
-
function buildNoProviderWarning(hasProvider) {
|
|
6273
|
-
if (hasProvider !== false) return null;
|
|
6274
|
-
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
|
-
}
|
|
6276
|
-
|
|
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 };
|
|
7362
|
+
// src/lib/opencode/provider-check.ts
|
|
7363
|
+
function buildNoProviderWarning(hasProvider) {
|
|
7364
|
+
if (hasProvider !== false) return null;
|
|
7365
|
+
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).";
|
|
6382
7366
|
}
|
|
6383
7367
|
|
|
6384
7368
|
// src/lib/opencode/session-db-size.ts
|
|
6385
7369
|
import { statSync as statSync4 } from "node:fs";
|
|
6386
|
-
import { join as
|
|
7370
|
+
import { join as join10 } from "node:path";
|
|
6387
7371
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
6388
7372
|
function statSessionDbBytes(homeDir) {
|
|
6389
|
-
const dbPath =
|
|
7373
|
+
const dbPath = join10(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
6390
7374
|
try {
|
|
6391
7375
|
return statSync4(dbPath).size;
|
|
6392
7376
|
} catch (err) {
|
|
6393
|
-
const
|
|
6394
|
-
if (!
|
|
7377
|
+
const isMissingFile4 = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
7378
|
+
if (!isMissingFile4) {
|
|
6395
7379
|
console.error(
|
|
6396
7380
|
`[statSessionDbBytes] could not stat ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
6397
7381
|
);
|
|
@@ -6416,12 +7400,12 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
6416
7400
|
// src/lib/opencode/log-tail.ts
|
|
6417
7401
|
import { statSync as statSync5 } from "node:fs";
|
|
6418
7402
|
import { homedir as homedir5 } from "node:os";
|
|
6419
|
-
import { join as
|
|
7403
|
+
import { join as join11 } from "node:path";
|
|
6420
7404
|
import { open as open2, stat } from "node:fs/promises";
|
|
6421
7405
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
6422
7406
|
function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
|
|
6423
|
-
const dataDir = env.XDG_DATA_HOME ||
|
|
6424
|
-
return
|
|
7407
|
+
const dataDir = env.XDG_DATA_HOME || join11(homeDir, ".local", "share");
|
|
7408
|
+
return join11(dataDir, "opencode", "log", "opencode.log");
|
|
6425
7409
|
}
|
|
6426
7410
|
function isEnoent(error2) {
|
|
6427
7411
|
return error2?.code === "ENOENT";
|
|
@@ -6906,8 +7890,8 @@ function connectTunnel(options) {
|
|
|
6906
7890
|
}
|
|
6907
7891
|
forwarder.handleBinaryBodyFrame(frame.sid, frame.payload);
|
|
6908
7892
|
} catch (error2) {
|
|
6909
|
-
const
|
|
6910
|
-
onError?.(`Failed to handle binary message: ${
|
|
7893
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
7894
|
+
onError?.(`Failed to handle binary message: ${errorMessage4}`);
|
|
6911
7895
|
}
|
|
6912
7896
|
return;
|
|
6913
7897
|
}
|
|
@@ -6915,8 +7899,8 @@ function connectTunnel(options) {
|
|
|
6915
7899
|
try {
|
|
6916
7900
|
message = JSON.parse(data.toString());
|
|
6917
7901
|
} catch (error2) {
|
|
6918
|
-
const
|
|
6919
|
-
onError?.(`Failed to handle message: ${
|
|
7902
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
7903
|
+
onError?.(`Failed to handle message: ${errorMessage4}`);
|
|
6920
7904
|
return;
|
|
6921
7905
|
}
|
|
6922
7906
|
if (isStreamFrame(message)) {
|
|
@@ -7088,10 +8072,10 @@ var RunnerConnection = class _RunnerConnection {
|
|
|
7088
8072
|
};
|
|
7089
8073
|
|
|
7090
8074
|
// src/lib/tunnel/ready-marker.ts
|
|
7091
|
-
import { writeFileSync as
|
|
8075
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
7092
8076
|
function writeTunnelReadyMarker(path, agentId) {
|
|
7093
8077
|
try {
|
|
7094
|
-
|
|
8078
|
+
writeFileSync6(path, `${agentId}
|
|
7095
8079
|
`);
|
|
7096
8080
|
return { ok: true };
|
|
7097
8081
|
} catch (error2) {
|
|
@@ -7116,7 +8100,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
7116
8100
|
}
|
|
7117
8101
|
|
|
7118
8102
|
// src/lib/process-liveness.ts
|
|
7119
|
-
import { readFileSync as
|
|
8103
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
7120
8104
|
function isProcessAlive(pid) {
|
|
7121
8105
|
try {
|
|
7122
8106
|
process.kill(pid, 0);
|
|
@@ -7131,7 +8115,7 @@ function isProcessAlive(pid) {
|
|
|
7131
8115
|
}
|
|
7132
8116
|
if (process.platform !== "linux") return true;
|
|
7133
8117
|
try {
|
|
7134
|
-
const status2 =
|
|
8118
|
+
const status2 = readFileSync10(`/proc/${pid}/status`, "utf8");
|
|
7135
8119
|
return !/^State:\s+Z(?:\s|$)/m.test(status2);
|
|
7136
8120
|
} catch (error2) {
|
|
7137
8121
|
console.error(
|
|
@@ -7198,8 +8182,8 @@ function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
|
7198
8182
|
}
|
|
7199
8183
|
|
|
7200
8184
|
// src/lib/resource-usage-reporting.ts
|
|
7201
|
-
var
|
|
7202
|
-
var
|
|
8185
|
+
var ENABLED_VALUES2 = /* @__PURE__ */ new Set(["on", "true", "1"]);
|
|
8186
|
+
var DISABLED_VALUES2 = /* @__PURE__ */ new Set(["off", "false", "0"]);
|
|
7203
8187
|
function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
7204
8188
|
if (flagValue === false) {
|
|
7205
8189
|
return { enabled: false, warnings: [] };
|
|
@@ -7209,10 +8193,10 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
|
7209
8193
|
return { enabled: true, warnings: [] };
|
|
7210
8194
|
}
|
|
7211
8195
|
const normalized = raw.trim().toLowerCase();
|
|
7212
|
-
if (
|
|
8196
|
+
if (DISABLED_VALUES2.has(normalized)) {
|
|
7213
8197
|
return { enabled: false, warnings: [] };
|
|
7214
8198
|
}
|
|
7215
|
-
if (
|
|
8199
|
+
if (ENABLED_VALUES2.has(normalized)) {
|
|
7216
8200
|
return { enabled: true, warnings: [] };
|
|
7217
8201
|
}
|
|
7218
8202
|
return {
|
|
@@ -7490,18 +8474,18 @@ function createSessionStoreHealth(options = {}) {
|
|
|
7490
8474
|
import { homedir as homedir6 } from "node:os";
|
|
7491
8475
|
|
|
7492
8476
|
// src/lib/runner-file-sync.ts
|
|
7493
|
-
import { join as
|
|
8477
|
+
import { join as join13 } from "node:path";
|
|
7494
8478
|
|
|
7495
8479
|
// src/lib/file-push.ts
|
|
7496
8480
|
import { randomUUID } from "node:crypto";
|
|
7497
8481
|
import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
|
|
7498
|
-
import { basename as basename2, dirname as
|
|
8482
|
+
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
8483
|
var FILE_MODE = 384;
|
|
7500
8484
|
var DIRECTORY_MODE = 448;
|
|
7501
8485
|
async function writePushedFile(request3) {
|
|
7502
|
-
const { requestedPath, content, allowedDirectories, homeDir } = request3;
|
|
8486
|
+
const { requestedPath, content, allowedDirectories, allowedFiles, homeDir } = request3;
|
|
7503
8487
|
const bytes = content.byteLength;
|
|
7504
|
-
if (allowedDirectories.length === 0) {
|
|
8488
|
+
if (allowedDirectories.length === 0 && allowedFiles.length === 0) {
|
|
7505
8489
|
return refuse("file_sync_disabled", "File sync is not enabled on this runner.", {
|
|
7506
8490
|
path: requestedPath,
|
|
7507
8491
|
bytes
|
|
@@ -7526,10 +8510,10 @@ async function writePushedFile(request3) {
|
|
|
7526
8510
|
}
|
|
7527
8511
|
try {
|
|
7528
8512
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
7529
|
-
|
|
8513
|
+
dirname9(candidate)
|
|
7530
8514
|
);
|
|
7531
|
-
const realTarget =
|
|
7532
|
-
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
8515
|
+
const realTarget = join12(existingAncestor, ...missingSegments, basename2(candidate));
|
|
8516
|
+
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget) ?? await findMatchingAllowedFile(allowedFiles, realTarget);
|
|
7533
8517
|
if (allowedDirectory === null) {
|
|
7534
8518
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
7535
8519
|
path: realTarget,
|
|
@@ -7538,8 +8522,8 @@ async function writePushedFile(request3) {
|
|
|
7538
8522
|
}
|
|
7539
8523
|
if (missingSegments.length > 0) {
|
|
7540
8524
|
await createMissingDirectories(existingAncestor, missingSegments);
|
|
7541
|
-
const realParent = await realpath(
|
|
7542
|
-
if (realParent !==
|
|
8525
|
+
const realParent = await realpath(dirname9(realTarget));
|
|
8526
|
+
if (realParent !== dirname9(realTarget) || !contains(allowedDirectory, realTarget)) {
|
|
7543
8527
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
7544
8528
|
path: realTarget,
|
|
7545
8529
|
bytes,
|
|
@@ -7564,7 +8548,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
7564
8548
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
7565
8549
|
return null;
|
|
7566
8550
|
}
|
|
7567
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
8551
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join12(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
7568
8552
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
7569
8553
|
return null;
|
|
7570
8554
|
}
|
|
@@ -7582,7 +8566,7 @@ async function resolveNearestExistingAncestor(directory) {
|
|
|
7582
8566
|
try {
|
|
7583
8567
|
return { existingAncestor: await realpath(current), missingSegments };
|
|
7584
8568
|
} catch (err) {
|
|
7585
|
-
const parent =
|
|
8569
|
+
const parent = dirname9(current);
|
|
7586
8570
|
if (err.code !== "ENOENT" || parent === current) {
|
|
7587
8571
|
throw err;
|
|
7588
8572
|
}
|
|
@@ -7604,6 +8588,22 @@ async function findContainingAllowedDirectory(allowedDirectories, realTarget) {
|
|
|
7604
8588
|
}
|
|
7605
8589
|
return null;
|
|
7606
8590
|
}
|
|
8591
|
+
async function findMatchingAllowedFile(allowedFiles, realTarget) {
|
|
8592
|
+
for (const entry of allowedFiles) {
|
|
8593
|
+
if (!isAbsolute2(entry)) {
|
|
8594
|
+
log("warn", "file_push_allowed_directory_skipped", {
|
|
8595
|
+
directory: entry,
|
|
8596
|
+
reason: "not_absolute_file"
|
|
8597
|
+
});
|
|
8598
|
+
continue;
|
|
8599
|
+
}
|
|
8600
|
+
const realParent = await realpathCreatingIfMissing(dirname9(entry));
|
|
8601
|
+
if (realParent !== null && join12(realParent, basename2(entry)) === realTarget) {
|
|
8602
|
+
return realParent;
|
|
8603
|
+
}
|
|
8604
|
+
}
|
|
8605
|
+
return null;
|
|
8606
|
+
}
|
|
7607
8607
|
async function realpathCreatingIfMissing(directory) {
|
|
7608
8608
|
try {
|
|
7609
8609
|
return await realpath(directory);
|
|
@@ -7637,13 +8637,13 @@ function contains(realDirectory, realTarget) {
|
|
|
7637
8637
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
7638
8638
|
let current = existingAncestor;
|
|
7639
8639
|
for (const segment of missingSegments) {
|
|
7640
|
-
current =
|
|
8640
|
+
current = join12(current, segment);
|
|
7641
8641
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
7642
8642
|
await chmod(current, DIRECTORY_MODE);
|
|
7643
8643
|
}
|
|
7644
8644
|
}
|
|
7645
8645
|
async function writeAtomically(realTarget, content) {
|
|
7646
|
-
const temporaryPath =
|
|
8646
|
+
const temporaryPath = join12(dirname9(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
7647
8647
|
let handle;
|
|
7648
8648
|
try {
|
|
7649
8649
|
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
@@ -7773,16 +8773,16 @@ var NOT_APPLIED = {
|
|
|
7773
8773
|
opencodeAuthApplied: false
|
|
7774
8774
|
};
|
|
7775
8775
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
7776
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
7777
|
-
return expanded ===
|
|
8776
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
8777
|
+
return expanded === join13(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
7778
8778
|
}
|
|
7779
8779
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
7780
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
7781
|
-
return expanded ===
|
|
8780
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
8781
|
+
return expanded === join13(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
7782
8782
|
}
|
|
7783
8783
|
async function applyOne(options, file) {
|
|
7784
8784
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
7785
|
-
if (options.allowedDirectories.length === 0) {
|
|
8785
|
+
if (options.allowedDirectories.length === 0 && options.allowedFiles.length === 0) {
|
|
7786
8786
|
options.log({
|
|
7787
8787
|
level: "warn",
|
|
7788
8788
|
message: `Runner file ${label} rejected: file sync is not enabled on this runner (start it with --enable-file-sync-to)`
|
|
@@ -7809,6 +8809,7 @@ async function applyOne(options, file) {
|
|
|
7809
8809
|
requestedPath: file.path,
|
|
7810
8810
|
content: download.content,
|
|
7811
8811
|
allowedDirectories: options.allowedDirectories,
|
|
8812
|
+
allowedFiles: options.allowedFiles,
|
|
7812
8813
|
homeDir: options.homeDir
|
|
7813
8814
|
});
|
|
7814
8815
|
} catch (err) {
|
|
@@ -7916,7 +8917,7 @@ function describe(err) {
|
|
|
7916
8917
|
}
|
|
7917
8918
|
|
|
7918
8919
|
// src/lib/vault-file-sync.ts
|
|
7919
|
-
import { createHash } from "node:crypto";
|
|
8920
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
7920
8921
|
async function syncVaultFiles(options) {
|
|
7921
8922
|
const files = await listVaultFiles(options);
|
|
7922
8923
|
if (files === null) return { applied: 0 };
|
|
@@ -8012,7 +9013,7 @@ async function reconcileVaultFile(options, file) {
|
|
|
8012
9013
|
}
|
|
8013
9014
|
const content = await downloadVaultFile(options, file);
|
|
8014
9015
|
if (content === null) return false;
|
|
8015
|
-
const actualSha256 =
|
|
9016
|
+
const actualSha256 = createHash2("sha256").update(content).digest("hex");
|
|
8016
9017
|
if (actualSha256 !== file.content_sha256) {
|
|
8017
9018
|
options.log({
|
|
8018
9019
|
level: "warn",
|
|
@@ -8060,6 +9061,7 @@ async function applyVaultFile(options, file, content) {
|
|
|
8060
9061
|
requestedPath: file.destination_path,
|
|
8061
9062
|
content,
|
|
8062
9063
|
allowedDirectories: options.allowedDirectories,
|
|
9064
|
+
allowedFiles: options.allowedFiles,
|
|
8063
9065
|
homeDir: options.homeDir
|
|
8064
9066
|
});
|
|
8065
9067
|
if (!outcome.ok) {
|
|
@@ -8212,6 +9214,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8212
9214
|
conversationFilter;
|
|
8213
9215
|
retry;
|
|
8214
9216
|
log;
|
|
9217
|
+
recordBinding;
|
|
8215
9218
|
fetchImpl;
|
|
8216
9219
|
sleep;
|
|
8217
9220
|
pausedPollIntervalMs;
|
|
@@ -8219,6 +9222,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8219
9222
|
stuckQueuedMs;
|
|
8220
9223
|
now;
|
|
8221
9224
|
fileSyncDirectories;
|
|
9225
|
+
fileSyncExactFiles;
|
|
8222
9226
|
homeDir;
|
|
8223
9227
|
maxActiveSessions;
|
|
8224
9228
|
watcherStallMs;
|
|
@@ -8573,6 +9577,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8573
9577
|
this.retry = { ...DEFAULT_RETRY_POLICY, ...config.retry };
|
|
8574
9578
|
this.log = config.log ?? (() => {
|
|
8575
9579
|
});
|
|
9580
|
+
this.recordBinding = config.recordBinding;
|
|
8576
9581
|
this.fetchImpl = withRequestTimeout(
|
|
8577
9582
|
config.fetchImpl ?? fetch,
|
|
8578
9583
|
config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
|
|
@@ -8588,6 +9593,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8588
9593
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
8589
9594
|
this.now = config.now ?? (() => Date.now());
|
|
8590
9595
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
9596
|
+
this.fileSyncExactFiles = config.fileSyncExactFiles ?? [];
|
|
8591
9597
|
this.homeDir = config.homeDir ?? homedir6();
|
|
8592
9598
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
8593
9599
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
@@ -8685,6 +9691,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8685
9691
|
getAuthHeader: this.getAuthHeader,
|
|
8686
9692
|
fetchImpl: this.fetchImpl,
|
|
8687
9693
|
allowedDirectories: this.fileSyncDirectories,
|
|
9694
|
+
allowedFiles: this.fileSyncExactFiles,
|
|
8688
9695
|
homeDir: this.homeDir,
|
|
8689
9696
|
ackFailures: this.fileAckFailures,
|
|
8690
9697
|
log: this.log
|
|
@@ -8695,6 +9702,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8695
9702
|
getAuthHeader: this.getAuthHeader,
|
|
8696
9703
|
fetchImpl: this.fetchImpl,
|
|
8697
9704
|
allowedDirectories: this.fileSyncDirectories,
|
|
9705
|
+
allowedFiles: this.fileSyncExactFiles,
|
|
8698
9706
|
homeDir: this.homeDir,
|
|
8699
9707
|
outcomes: this.vaultFileOutcomes,
|
|
8700
9708
|
log: this.log
|
|
@@ -8757,6 +9765,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8757
9765
|
let cappedSkips = 0;
|
|
8758
9766
|
for (const conv of conversations) {
|
|
8759
9767
|
if (this.stopped) break;
|
|
9768
|
+
if (conv.opencode_session_id) {
|
|
9769
|
+
this.recordSessionBinding(conv.id, conv.opencode_session_id);
|
|
9770
|
+
}
|
|
8760
9771
|
if (this.maxActiveSessions !== void 0) {
|
|
8761
9772
|
const activeSessionIds = this.activeSessionIdsForCap();
|
|
8762
9773
|
const resolvedSessionId = this.sessions.get(conv.id) ?? conv.opencode_session_id;
|
|
@@ -8839,12 +9850,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8839
9850
|
this.sessions.delete(conversationId);
|
|
8840
9851
|
this.supersede(conversationId, sessionId);
|
|
8841
9852
|
const dispatchDescription = source === "readopt" ? "re-dispatched message" : "message";
|
|
8842
|
-
const
|
|
8843
|
-
this.untrackableAck.set(messageId,
|
|
9853
|
+
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.`;
|
|
9854
|
+
this.untrackableAck.set(messageId, errorMessage4);
|
|
8844
9855
|
this.untrackableAckSessionIds.set(messageId, null);
|
|
8845
9856
|
this.log({
|
|
8846
9857
|
level: "error",
|
|
8847
|
-
message:
|
|
9858
|
+
message: errorMessage4,
|
|
8848
9859
|
conversation_id: conversationId,
|
|
8849
9860
|
message_id: messageId
|
|
8850
9861
|
});
|
|
@@ -8852,7 +9863,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8852
9863
|
conversationId,
|
|
8853
9864
|
messageId,
|
|
8854
9865
|
null,
|
|
8855
|
-
|
|
9866
|
+
errorMessage4
|
|
8856
9867
|
);
|
|
8857
9868
|
if (reportError !== null) {
|
|
8858
9869
|
this.log({
|
|
@@ -9033,12 +10044,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9033
10044
|
conversation_id: conv.id,
|
|
9034
10045
|
message_id: message.id
|
|
9035
10046
|
});
|
|
9036
|
-
const
|
|
10047
|
+
const errorMessage4 = this.untrackableAck.get(message.id);
|
|
9037
10048
|
const reportError = await this.reportUntrackableAck(
|
|
9038
10049
|
conv.id,
|
|
9039
10050
|
message.id,
|
|
9040
10051
|
null,
|
|
9041
|
-
|
|
10052
|
+
errorMessage4
|
|
9042
10053
|
);
|
|
9043
10054
|
if (reportError === null) {
|
|
9044
10055
|
continue;
|
|
@@ -9126,7 +10137,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9126
10137
|
const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
|
|
9127
10138
|
const providerId = hasProviderAndModel ? dispatchedModel.slice(0, separator) : null;
|
|
9128
10139
|
const modelId = hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null;
|
|
9129
|
-
const
|
|
10140
|
+
const errorMessage4 = `Provider namespace not loaded on this runner: ${message.opencode_model}. Refused before dispatch; no turn was started.`;
|
|
9130
10141
|
this.log({
|
|
9131
10142
|
level: "error",
|
|
9132
10143
|
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 +10145,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9134
10145
|
message_id: message.id
|
|
9135
10146
|
});
|
|
9136
10147
|
try {
|
|
9137
|
-
await this.markFailed(conv.id, message.id, sessionId,
|
|
10148
|
+
await this.markFailed(conv.id, message.id, sessionId, errorMessage4, void 0, {
|
|
9138
10149
|
classification: {
|
|
9139
10150
|
kind: "model_auth",
|
|
9140
10151
|
providerId,
|
|
@@ -9179,7 +10190,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9179
10190
|
false
|
|
9180
10191
|
);
|
|
9181
10192
|
}
|
|
9182
|
-
opencodeMessageId = this.isV2 ? await sendV2Prompt(
|
|
10193
|
+
opencodeMessageId = this.isV2 ? await sendV2Prompt(
|
|
10194
|
+
this.openCodeClient,
|
|
10195
|
+
sessionId,
|
|
10196
|
+
message.content,
|
|
10197
|
+
options,
|
|
10198
|
+
() => this.logInvalidV2ModelSelection(conv.id, message.id)
|
|
10199
|
+
) : await this.dispatchLocked(
|
|
9183
10200
|
sessionId,
|
|
9184
10201
|
() => sendPromptAsync(
|
|
9185
10202
|
this.port,
|
|
@@ -9206,12 +10223,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9206
10223
|
} catch (err) {
|
|
9207
10224
|
if (err instanceof ChannelAuthError) throw err;
|
|
9208
10225
|
if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
|
|
9209
|
-
const
|
|
9210
|
-
this.untrackableAck.set(message.id,
|
|
10226
|
+
const errorMessage5 = err instanceof Error ? err.message : String(err);
|
|
10227
|
+
this.untrackableAck.set(message.id, errorMessage5);
|
|
9211
10228
|
this.untrackableAckSessionIds.set(message.id, null);
|
|
9212
10229
|
this.log({
|
|
9213
10230
|
level: "error",
|
|
9214
|
-
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${
|
|
10231
|
+
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage5}`,
|
|
9215
10232
|
conversation_id: conv.id,
|
|
9216
10233
|
message_id: message.id
|
|
9217
10234
|
});
|
|
@@ -9219,7 +10236,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9219
10236
|
conv.id,
|
|
9220
10237
|
message.id,
|
|
9221
10238
|
null,
|
|
9222
|
-
|
|
10239
|
+
errorMessage5
|
|
9223
10240
|
);
|
|
9224
10241
|
if (markError !== null) {
|
|
9225
10242
|
this.log({
|
|
@@ -9254,7 +10271,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9254
10271
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
9255
10272
|
break;
|
|
9256
10273
|
}
|
|
9257
|
-
|
|
10274
|
+
if (this.isV2 && err instanceof OpenCodeV2ModelSwitchError) {
|
|
10275
|
+
await this.failV2ModelSwitch(conv, message, sessionId, err);
|
|
10276
|
+
break;
|
|
10277
|
+
}
|
|
10278
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
9258
10279
|
this.sessions.delete(conv.id);
|
|
9259
10280
|
this.supersede(conv.id, sessionId);
|
|
9260
10281
|
this.log({
|
|
@@ -9263,7 +10284,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9263
10284
|
conversation_id: conv.id,
|
|
9264
10285
|
message_id: message.id
|
|
9265
10286
|
});
|
|
9266
|
-
await this.markFailed(conv.id, message.id, null,
|
|
10287
|
+
await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
|
|
9267
10288
|
this.log({
|
|
9268
10289
|
level: "warn",
|
|
9269
10290
|
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 +10295,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9274
10295
|
});
|
|
9275
10296
|
this.log({
|
|
9276
10297
|
level: "error",
|
|
9277
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
10298
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage4}`,
|
|
9278
10299
|
conversation_id: conv.id,
|
|
9279
10300
|
message_id: message.id
|
|
9280
10301
|
});
|
|
@@ -9847,6 +10868,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9847
10868
|
this.dispatchNotStartedSignalled.set(message.id, branch);
|
|
9848
10869
|
void this.postSignal(conv.id, message.id, "dispatch_not_started", { branch });
|
|
9849
10870
|
}
|
|
10871
|
+
logInvalidV2ModelSelection(conversationId, messageId) {
|
|
10872
|
+
this.log({
|
|
10873
|
+
level: "warn",
|
|
10874
|
+
message: `Skipping an invalid OpenCode V2 model selection for message ${messageId.slice(0, 8)}; expected provider/model format`,
|
|
10875
|
+
conversation_id: conversationId,
|
|
10876
|
+
message_id: messageId
|
|
10877
|
+
});
|
|
10878
|
+
}
|
|
9850
10879
|
/**
|
|
9851
10880
|
* Class B (#1340): the runner DECIDED an outcome (reattach/settle/fail_permanent)
|
|
9852
10881
|
* but its own PATCH to record it failed. Fires at most once per (message,
|
|
@@ -10061,6 +11090,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10061
11090
|
return { sessionId: await this.createAndBindSession(conv.id), created: true };
|
|
10062
11091
|
}
|
|
10063
11092
|
this.sessions.set(conv.id, bound);
|
|
11093
|
+
this.recordSessionBinding(conv.id, bound);
|
|
10064
11094
|
return { sessionId: bound, created: false };
|
|
10065
11095
|
}
|
|
10066
11096
|
return { sessionId: await this.createAndBindSession(conv.id), created: true };
|
|
@@ -10074,6 +11104,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10074
11104
|
const directory = await this.resolveOpenCodeDirectory();
|
|
10075
11105
|
const sessionId = await this.createOpenCodeSession(directory);
|
|
10076
11106
|
this.sessions.set(conversationId, sessionId);
|
|
11107
|
+
this.recordSessionBinding(conversationId, sessionId);
|
|
10077
11108
|
await this.persistSession(conversationId, sessionId).catch((err) => {
|
|
10078
11109
|
this.log({
|
|
10079
11110
|
level: "warn",
|
|
@@ -10083,6 +11114,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10083
11114
|
});
|
|
10084
11115
|
return sessionId;
|
|
10085
11116
|
}
|
|
11117
|
+
recordSessionBinding(conversationId, sessionId) {
|
|
11118
|
+
if (!this.recordBinding) return;
|
|
11119
|
+
try {
|
|
11120
|
+
this.recordBinding(conversationId, sessionId);
|
|
11121
|
+
} catch (error2) {
|
|
11122
|
+
this.log({
|
|
11123
|
+
level: "warn",
|
|
11124
|
+
message: `Recording OpenCode session ${sessionId.slice(0, 8)} for conversation ${conversationId.slice(0, 8)} failed (best-effort): ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
11125
|
+
conversation_id: conversationId
|
|
11126
|
+
});
|
|
11127
|
+
}
|
|
11128
|
+
}
|
|
10086
11129
|
/**
|
|
10087
11130
|
* Lazily resolve (and cache) opencode's root directory via the selected client's
|
|
10088
11131
|
* location lookup.
|
|
@@ -12183,12 +13226,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12183
13226
|
conversation_id: row.conversation_id,
|
|
12184
13227
|
message_id: row.id
|
|
12185
13228
|
});
|
|
12186
|
-
const
|
|
13229
|
+
const errorMessage4 = this.untrackableAck.get(row.id);
|
|
12187
13230
|
const reportError = await this.reportUntrackableAck(
|
|
12188
13231
|
row.conversation_id,
|
|
12189
13232
|
row.id,
|
|
12190
13233
|
this.untrackableAckSessionIds.get(row.id) ?? null,
|
|
12191
|
-
|
|
13234
|
+
errorMessage4
|
|
12192
13235
|
);
|
|
12193
13236
|
if (reportError !== null) {
|
|
12194
13237
|
this.log({
|
|
@@ -12473,7 +13516,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12473
13516
|
let ocId;
|
|
12474
13517
|
let preDispatchUserIds = null;
|
|
12475
13518
|
try {
|
|
12476
|
-
ocId = this.isV2 ? await sendV2Prompt(
|
|
13519
|
+
ocId = this.isV2 ? await sendV2Prompt(
|
|
13520
|
+
this.openCodeClient,
|
|
13521
|
+
sessionId,
|
|
13522
|
+
row.content,
|
|
13523
|
+
options,
|
|
13524
|
+
() => this.logInvalidV2ModelSelection(row.conversation_id, row.id)
|
|
13525
|
+
) : await this.dispatchLocked(
|
|
12477
13526
|
sessionId,
|
|
12478
13527
|
() => sendPromptAsync(
|
|
12479
13528
|
this.port,
|
|
@@ -12501,14 +13550,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12501
13550
|
this.awaitingReadopt.delete(row.id);
|
|
12502
13551
|
if (err instanceof ChannelAuthError) throw err;
|
|
12503
13552
|
if (this.isV2) {
|
|
12504
|
-
const
|
|
13553
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
12505
13554
|
const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
|
|
12506
13555
|
if (!invalidPromptAck) {
|
|
12507
13556
|
const exists = await this.sessionExists(sessionId);
|
|
12508
13557
|
if (exists === false) {
|
|
12509
13558
|
this.log({
|
|
12510
13559
|
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: ${
|
|
13560
|
+
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
13561
|
conversation_id: row.conversation_id,
|
|
12513
13562
|
message_id: row.id
|
|
12514
13563
|
});
|
|
@@ -12518,7 +13567,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12518
13567
|
if (exists === null) {
|
|
12519
13568
|
this.log({
|
|
12520
13569
|
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: ${
|
|
13570
|
+
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
13571
|
conversation_id: row.conversation_id,
|
|
12523
13572
|
message_id: row.id
|
|
12524
13573
|
});
|
|
@@ -12526,12 +13575,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12526
13575
|
return;
|
|
12527
13576
|
}
|
|
12528
13577
|
} else {
|
|
12529
|
-
this.untrackableAck.set(row.id,
|
|
13578
|
+
this.untrackableAck.set(row.id, errorMessage4);
|
|
12530
13579
|
this.untrackableAckSessionIds.set(row.id, sessionId);
|
|
12531
13580
|
}
|
|
13581
|
+
if (err instanceof OpenCodeV2ModelSwitchError) {
|
|
13582
|
+
await this.failV2ModelSwitch(readoptConv, readoptMessage, sessionId, err);
|
|
13583
|
+
return;
|
|
13584
|
+
}
|
|
12532
13585
|
this.log({
|
|
12533
13586
|
level: "error",
|
|
12534
|
-
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${
|
|
13587
|
+
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage4}`,
|
|
12535
13588
|
conversation_id: row.conversation_id,
|
|
12536
13589
|
message_id: row.id
|
|
12537
13590
|
});
|
|
@@ -12540,7 +13593,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12540
13593
|
row.conversation_id,
|
|
12541
13594
|
row.id,
|
|
12542
13595
|
sessionId,
|
|
12543
|
-
|
|
13596
|
+
errorMessage4
|
|
12544
13597
|
);
|
|
12545
13598
|
if (markError !== null) {
|
|
12546
13599
|
this.log({
|
|
@@ -12551,7 +13604,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12551
13604
|
});
|
|
12552
13605
|
}
|
|
12553
13606
|
} else {
|
|
12554
|
-
await this.markFailed(row.conversation_id, row.id, sessionId,
|
|
13607
|
+
await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage4).catch(
|
|
12555
13608
|
(markErr) => {
|
|
12556
13609
|
this.log({
|
|
12557
13610
|
level: "warn",
|
|
@@ -13535,9 +14588,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
13535
14588
|
* trackable. Callers use the returned error to keep their path-specific logging
|
|
13536
14589
|
* while the same report payload can be retried on a later drain.
|
|
13537
14590
|
*/
|
|
13538
|
-
async reportUntrackableAck(conversationId, messageId, sessionId,
|
|
14591
|
+
async reportUntrackableAck(conversationId, messageId, sessionId, errorMessage4) {
|
|
13539
14592
|
try {
|
|
13540
|
-
await this.markFailed(conversationId, messageId, sessionId,
|
|
14593
|
+
await this.markFailed(conversationId, messageId, sessionId, errorMessage4);
|
|
13541
14594
|
return null;
|
|
13542
14595
|
} catch (err) {
|
|
13543
14596
|
this.signalUntrackableAck(conversationId, messageId);
|
|
@@ -13606,6 +14659,38 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
13606
14659
|
}
|
|
13607
14660
|
this.clearSubagentInvocationCaches(messageId);
|
|
13608
14661
|
}
|
|
14662
|
+
async failV2ModelSwitch(conv, message, sessionId, error2) {
|
|
14663
|
+
const { model } = splitModelVariant(message.opencode_model ?? void 0);
|
|
14664
|
+
const separator = model?.indexOf("/") ?? -1;
|
|
14665
|
+
const classification = error2.statusCode === 404 && model && separator > 0 && separator < model.length - 1 ? {
|
|
14666
|
+
kind: "model_auth",
|
|
14667
|
+
providerId: model.slice(0, separator),
|
|
14668
|
+
modelId: model.slice(separator + 1),
|
|
14669
|
+
reason: "missing"
|
|
14670
|
+
} : null;
|
|
14671
|
+
this.log({
|
|
14672
|
+
level: "error",
|
|
14673
|
+
message: `OpenCode could not apply the selected model for message ${message.id.slice(0, 8)}: ${error2.message}`,
|
|
14674
|
+
conversation_id: conv.id,
|
|
14675
|
+
message_id: message.id
|
|
14676
|
+
});
|
|
14677
|
+
try {
|
|
14678
|
+
await this.markFailed(conv.id, message.id, sessionId, error2.message, void 0, {
|
|
14679
|
+
classification,
|
|
14680
|
+
errorShape: { name: null, statusCode: error2.statusCode }
|
|
14681
|
+
});
|
|
14682
|
+
} catch (markErr) {
|
|
14683
|
+
this.log({
|
|
14684
|
+
level: "warn",
|
|
14685
|
+
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)}`,
|
|
14686
|
+
conversation_id: conv.id,
|
|
14687
|
+
message_id: message.id
|
|
14688
|
+
});
|
|
14689
|
+
this.signalDispatchNotStarted(conv, message, "failure_unreported");
|
|
14690
|
+
return;
|
|
14691
|
+
}
|
|
14692
|
+
if (classification) await this.repairProviderRegistry(conv.id, message.id);
|
|
14693
|
+
}
|
|
13609
14694
|
/**
|
|
13610
14695
|
* Classify an errored turn and extract its bounded error shape for `markFailed`.
|
|
13611
14696
|
*
|
|
@@ -14049,7 +15134,7 @@ import ora3 from "ora";
|
|
|
14049
15134
|
import { select as select3 } from "@inquirer/prompts";
|
|
14050
15135
|
function unknownPasswordError2(port) {
|
|
14051
15136
|
return new Error(
|
|
14052
|
-
`
|
|
15137
|
+
`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
15138
|
);
|
|
14054
15139
|
}
|
|
14055
15140
|
var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
|
|
@@ -14067,9 +15152,9 @@ async function ensureOpenCode2Running(ctx) {
|
|
|
14067
15152
|
password: null
|
|
14068
15153
|
};
|
|
14069
15154
|
}
|
|
14070
|
-
if (!isOpenCode2Installed()) {
|
|
15155
|
+
if (!ctx.command && !isOpenCode2Installed()) {
|
|
14071
15156
|
throw new Error(
|
|
14072
|
-
|
|
15157
|
+
"OpenCode V2 is not installed. Install it with `npm install -g @opencode/cli` or `curl -fsSL https://opencode.ai/v2/install | bash`."
|
|
14073
15158
|
);
|
|
14074
15159
|
}
|
|
14075
15160
|
let port = ctx.port;
|
|
@@ -14097,7 +15182,8 @@ Port ${port} is already in use.`));
|
|
|
14097
15182
|
if (!ctx.interactive) {
|
|
14098
15183
|
ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
|
|
14099
15184
|
const { child: proc, password } = await startOpenCode2(port, {
|
|
14100
|
-
inheritStdio: ctx.inheritStdio
|
|
15185
|
+
inheritStdio: ctx.inheritStdio,
|
|
15186
|
+
command: ctx.command
|
|
14101
15187
|
});
|
|
14102
15188
|
const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
|
|
14103
15189
|
if (!health.healthy) {
|
|
@@ -14118,13 +15204,14 @@ Port ${port} is already in use.`));
|
|
|
14118
15204
|
password
|
|
14119
15205
|
};
|
|
14120
15206
|
}
|
|
15207
|
+
const command = ctx.command ?? "opencode2";
|
|
14121
15208
|
const action = await select3({
|
|
14122
15209
|
message: "OpenCode V2 is not running. What would you like to do?",
|
|
14123
15210
|
choices: [
|
|
14124
15211
|
{
|
|
14125
15212
|
name: "Start OpenCode V2 for me",
|
|
14126
15213
|
value: "start",
|
|
14127
|
-
description: `Run '
|
|
15214
|
+
description: `Run '${command} serve --port ${port}'`
|
|
14128
15215
|
},
|
|
14129
15216
|
{
|
|
14130
15217
|
name: "Show me the command",
|
|
@@ -14142,14 +15229,15 @@ Port ${port} is already in use.`));
|
|
|
14142
15229
|
blank();
|
|
14143
15230
|
console.log(chalk6.bold("Run this command in another terminal:"));
|
|
14144
15231
|
blank();
|
|
14145
|
-
console.log(` ${chalk6.cyan(
|
|
15232
|
+
console.log(` ${chalk6.cyan(`${command} serve --port ${port}`)}`);
|
|
14146
15233
|
blank();
|
|
14147
15234
|
throw new Error("Please start OpenCode V2 manually");
|
|
14148
15235
|
}
|
|
14149
15236
|
if (action === "start") {
|
|
14150
15237
|
const spinner = ora3("Starting OpenCode V2...").start();
|
|
14151
15238
|
const { child: proc, password } = await startOpenCode2(port, {
|
|
14152
|
-
inheritStdio: ctx.inheritStdio
|
|
15239
|
+
inheritStdio: ctx.inheritStdio,
|
|
15240
|
+
command: ctx.command
|
|
14153
15241
|
});
|
|
14154
15242
|
const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
|
|
14155
15243
|
if (!health.healthy) {
|
|
@@ -14175,7 +15263,7 @@ Port ${port} is already in use.`));
|
|
|
14175
15263
|
}
|
|
14176
15264
|
|
|
14177
15265
|
// src/lib/runner-credentials.ts
|
|
14178
|
-
import { chmodSync as chmodSync2, writeFileSync as
|
|
15266
|
+
import { chmodSync as chmodSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
14179
15267
|
import { spawn as spawn5 } from "node:child_process";
|
|
14180
15268
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
14181
15269
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
@@ -14418,8 +15506,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
14418
15506
|
}
|
|
14419
15507
|
try {
|
|
14420
15508
|
env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
|
|
14421
|
-
|
|
14422
|
-
|
|
15509
|
+
writeFileSync7(GIT_CONFIG_GLOBAL, "");
|
|
15510
|
+
writeFileSync7(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
|
|
14423
15511
|
chmodSync2(GIT_CREDENTIAL_HELPER, 448);
|
|
14424
15512
|
const config = [
|
|
14425
15513
|
["user.name", env.GIT_USER_NAME ?? "evident-bot"],
|
|
@@ -14452,7 +15540,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
14452
15540
|
// src/lib/opencode/config-overlay.ts
|
|
14453
15541
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
14454
15542
|
import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
|
|
14455
|
-
import { isAbsolute as isAbsolute3, join as
|
|
15543
|
+
import { isAbsolute as isAbsolute3, join as join14, resolve as resolve4 } from "node:path";
|
|
14456
15544
|
function parseConfigContent(content) {
|
|
14457
15545
|
if (!content?.trim()) return {};
|
|
14458
15546
|
const parsed = JSON.parse(content);
|
|
@@ -14482,7 +15570,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
14482
15570
|
return;
|
|
14483
15571
|
}
|
|
14484
15572
|
const source = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
|
|
14485
|
-
const target = isFile(
|
|
15573
|
+
const target = isFile(join14(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
14486
15574
|
if (!isFile(source)) {
|
|
14487
15575
|
log3(
|
|
14488
15576
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -14490,7 +15578,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
14490
15578
|
);
|
|
14491
15579
|
return;
|
|
14492
15580
|
}
|
|
14493
|
-
copyFileSync(source,
|
|
15581
|
+
copyFileSync(source, join14(cwd, target));
|
|
14494
15582
|
try {
|
|
14495
15583
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
14496
15584
|
stdio: "ignore"
|
|
@@ -14499,13 +15587,13 @@ function applyRunnerOpenCodeConfig({
|
|
|
14499
15587
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
14500
15588
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
14501
15589
|
}
|
|
14502
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
15590
|
+
log3(`Applied runner OpenCode config ${source} to ${join14(cwd, target)}`);
|
|
14503
15591
|
}
|
|
14504
15592
|
|
|
14505
15593
|
// src/lib/opencode/runtime-provenance.ts
|
|
14506
|
-
import { readFileSync as
|
|
15594
|
+
import { readFileSync as readFileSync11, readdirSync } from "node:fs";
|
|
14507
15595
|
import { homedir as homedir7 } from "node:os";
|
|
14508
|
-
import { join as
|
|
15596
|
+
import { join as join15 } from "node:path";
|
|
14509
15597
|
var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
|
14510
15598
|
var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
|
|
14511
15599
|
function errorKind(error2) {
|
|
@@ -14573,7 +15661,7 @@ function parseJsonc(value) {
|
|
|
14573
15661
|
return JSON.parse(output);
|
|
14574
15662
|
}
|
|
14575
15663
|
function openCodeCacheRoot(env = process.env, homeDir = homedir7()) {
|
|
14576
|
-
return
|
|
15664
|
+
return join15(env.XDG_CACHE_HOME ?? join15(homeDir, ".cache"), "opencode");
|
|
14577
15665
|
}
|
|
14578
15666
|
function readConfiguredPluginSpecs({
|
|
14579
15667
|
cwd = process.cwd(),
|
|
@@ -14582,10 +15670,10 @@ function readConfiguredPluginSpecs({
|
|
|
14582
15670
|
let config;
|
|
14583
15671
|
try {
|
|
14584
15672
|
try {
|
|
14585
|
-
config = parseJsonc(
|
|
15673
|
+
config = parseJsonc(readFileSync11(join15(cwd, "opencode.jsonc"), "utf8"));
|
|
14586
15674
|
} catch (error2) {
|
|
14587
15675
|
if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
|
|
14588
|
-
config = parseJsonc(
|
|
15676
|
+
config = parseJsonc(readFileSync11(join15(cwd, "opencode.json"), "utf8"));
|
|
14589
15677
|
}
|
|
14590
15678
|
} catch (error2) {
|
|
14591
15679
|
log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
|
|
@@ -14613,7 +15701,7 @@ function resolveInstalledPluginVersions({
|
|
|
14613
15701
|
homeDir = homedir7(),
|
|
14614
15702
|
log: log3
|
|
14615
15703
|
}) {
|
|
14616
|
-
const packagesRoot =
|
|
15704
|
+
const packagesRoot = join15(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
|
|
14617
15705
|
let directories;
|
|
14618
15706
|
try {
|
|
14619
15707
|
directories = readdirSync(packagesRoot, { withFileTypes: true });
|
|
@@ -14634,8 +15722,8 @@ function resolveInstalledPluginVersions({
|
|
|
14634
15722
|
let resolvedVersion = null;
|
|
14635
15723
|
try {
|
|
14636
15724
|
const packageJson = JSON.parse(
|
|
14637
|
-
|
|
14638
|
-
|
|
15725
|
+
readFileSync11(
|
|
15726
|
+
join15(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
|
|
14639
15727
|
"utf8"
|
|
14640
15728
|
)
|
|
14641
15729
|
);
|
|
@@ -14656,7 +15744,7 @@ function resolveInstalledPluginVersions({
|
|
|
14656
15744
|
}
|
|
14657
15745
|
|
|
14658
15746
|
// src/lib/credential-sync.ts
|
|
14659
|
-
import { renameSync, writeFileSync as
|
|
15747
|
+
import { renameSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
14660
15748
|
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
14661
15749
|
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
14662
15750
|
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
@@ -14666,7 +15754,7 @@ var MAX_FLUSH_PASSES = 2;
|
|
|
14666
15754
|
function outcomesWith(outcome) {
|
|
14667
15755
|
return { claude: outcome, opencode: outcome };
|
|
14668
15756
|
}
|
|
14669
|
-
function
|
|
15757
|
+
function errorMessage3(error2) {
|
|
14670
15758
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
14671
15759
|
}
|
|
14672
15760
|
function waitForSettlement(promise, timeoutMs) {
|
|
@@ -14690,10 +15778,10 @@ function writeMarker(markerPath, outcomes, log3) {
|
|
|
14690
15778
|
`;
|
|
14691
15779
|
const temporaryPath = `${markerPath}.tmp`;
|
|
14692
15780
|
try {
|
|
14693
|
-
|
|
15781
|
+
writeFileSync8(temporaryPath, body, { mode: 384 });
|
|
14694
15782
|
renameSync(temporaryPath, markerPath);
|
|
14695
15783
|
} catch (error2) {
|
|
14696
|
-
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${
|
|
15784
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage3(error2)}`, "warn");
|
|
14697
15785
|
}
|
|
14698
15786
|
}
|
|
14699
15787
|
function intervalSeconds(env, log3) {
|
|
@@ -14725,7 +15813,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
|
14725
15813
|
},
|
|
14726
15814
|
(error2) => {
|
|
14727
15815
|
failed = true;
|
|
14728
|
-
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${
|
|
15816
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage3(error2)}`, "warn");
|
|
14729
15817
|
}
|
|
14730
15818
|
);
|
|
14731
15819
|
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
@@ -14785,7 +15873,7 @@ function createCredentialSync({
|
|
|
14785
15873
|
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
14786
15874
|
} catch (error2) {
|
|
14787
15875
|
outcomes[store] = "failed";
|
|
14788
|
-
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${
|
|
15876
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage3(error2)}`, "debug");
|
|
14789
15877
|
}
|
|
14790
15878
|
}
|
|
14791
15879
|
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
@@ -14908,14 +15996,21 @@ function request2(options) {
|
|
|
14908
15996
|
function safeFailure(operation, status2) {
|
|
14909
15997
|
return status2 === void 0 ? `${operation} could not reach the managed credential service` : `${operation} returned HTTP ${status2}`;
|
|
14910
15998
|
}
|
|
14911
|
-
function
|
|
14912
|
-
let
|
|
15999
|
+
function redactAuthorizedText(message, authorization) {
|
|
16000
|
+
let redacted = message;
|
|
14913
16001
|
const credential = authorization.split(/\s+/, 2)[1];
|
|
14914
16002
|
for (const secret of [authorization, credential]) {
|
|
14915
|
-
if (secret)
|
|
16003
|
+
if (secret) redacted = redacted.replaceAll(secret, "[redacted]");
|
|
14916
16004
|
}
|
|
16005
|
+
return redactDiagnosticText(redacted);
|
|
16006
|
+
}
|
|
16007
|
+
function logTransportFailure(options, operation, error2, authorization) {
|
|
16008
|
+
const detail = redactAuthorizedText(
|
|
16009
|
+
error2 instanceof Error ? error2.message : String(error2),
|
|
16010
|
+
authorization
|
|
16011
|
+
);
|
|
14917
16012
|
options.log(
|
|
14918
|
-
`MANAGED-INFERENCE-TRANSPORT-FAILED: operation=${operation} detail=${
|
|
16013
|
+
`MANAGED-INFERENCE-TRANSPORT-FAILED: operation=${operation} detail=${detail}`,
|
|
14919
16014
|
"debug"
|
|
14920
16015
|
);
|
|
14921
16016
|
}
|
|
@@ -14935,6 +16030,18 @@ function readSetup(value) {
|
|
|
14935
16030
|
}
|
|
14936
16031
|
return setup;
|
|
14937
16032
|
}
|
|
16033
|
+
function readModelDescriptors(value) {
|
|
16034
|
+
if (!Array.isArray(value)) return null;
|
|
16035
|
+
return value.every(
|
|
16036
|
+
(descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
|
|
16037
|
+
) ? value : null;
|
|
16038
|
+
}
|
|
16039
|
+
function logDescriptorRefreshFallback(options, branch, generation) {
|
|
16040
|
+
options.log(
|
|
16041
|
+
`MANAGED-INFERENCE-DESCRIPTORS-${branch}: runner=${options.agentId} generation=${generation}`,
|
|
16042
|
+
"warn"
|
|
16043
|
+
);
|
|
16044
|
+
}
|
|
14938
16045
|
function clearLocalSetup(options) {
|
|
14939
16046
|
const env = options.env ?? process.env;
|
|
14940
16047
|
const store = options.store ?? defaultStore;
|
|
@@ -14966,7 +16073,14 @@ async function discoverManagedInferenceSetup(options) {
|
|
|
14966
16073
|
return { kind: "none" };
|
|
14967
16074
|
}
|
|
14968
16075
|
if (!response.ok) {
|
|
14969
|
-
|
|
16076
|
+
const body2 = await responseJson(response);
|
|
16077
|
+
const serverMessage = typeof body2?.error === "string" && body2.error.trim() ? redactAuthorizedText(body2.error.trim(), headers.Authorization).slice(0, 500) : void 0;
|
|
16078
|
+
const failure = serverMessage ? `Managed setup discovery returned HTTP ${response.status}: ${serverMessage}` : safeFailure("Managed setup discovery", response.status);
|
|
16079
|
+
const runnerKeyGuidance = response.status === 401 || response.status === 403 ? "Authenticate as this runner with its runner key (EVIDENT_RUNNER_KEY)." : void 0;
|
|
16080
|
+
return {
|
|
16081
|
+
kind: "failed",
|
|
16082
|
+
reason: runnerKeyGuidance ? `${failure}. ${runnerKeyGuidance}` : failure
|
|
16083
|
+
};
|
|
14970
16084
|
}
|
|
14971
16085
|
const body = await responseJson(response);
|
|
14972
16086
|
const setup = readSetup(body?.setup);
|
|
@@ -15066,7 +16180,27 @@ async function reaffirmStored(options, credential) {
|
|
|
15066
16180
|
if (!setup || setup.generation !== credential.generation || setup.token_id !== credential.tokenId) {
|
|
15067
16181
|
return { kind: "failed", reason: "Managed credential reaffirmation returned a mismatch" };
|
|
15068
16182
|
}
|
|
15069
|
-
const
|
|
16183
|
+
const descriptorsValue = body?.model_descriptors;
|
|
16184
|
+
const refreshedDescriptors = readModelDescriptors(descriptorsValue);
|
|
16185
|
+
let credentialToApply = credential;
|
|
16186
|
+
if (descriptorsValue === void 0 || descriptorsValue === null) {
|
|
16187
|
+
logDescriptorRefreshFallback(options, "UNAVAILABLE", credential.generation);
|
|
16188
|
+
} else if (refreshedDescriptors === null) {
|
|
16189
|
+
logDescriptorRefreshFallback(options, "MALFORMED", credential.generation);
|
|
16190
|
+
} else if (refreshedDescriptors.length === 0) {
|
|
16191
|
+
logDescriptorRefreshFallback(options, "EMPTY", credential.generation);
|
|
16192
|
+
} else {
|
|
16193
|
+
const { config, unsupportedProviders } = buildCredentialProxyNativeConfig({
|
|
16194
|
+
inferenceBaseUrl: credential.inferenceBaseUrl,
|
|
16195
|
+
modelDescriptors: refreshedDescriptors
|
|
16196
|
+
});
|
|
16197
|
+
if (unsupportedProviders.length > 0 || Object.keys(config.provider).length === 0) {
|
|
16198
|
+
logDescriptorRefreshFallback(options, "UNSUPPORTED-PROVIDER", credential.generation);
|
|
16199
|
+
} else {
|
|
16200
|
+
credentialToApply = { ...credential, modelDescriptors: refreshedDescriptors };
|
|
16201
|
+
}
|
|
16202
|
+
}
|
|
16203
|
+
const failure = applyCredential(options, credentialToApply);
|
|
15070
16204
|
return failure ? { kind: "failed", reason: failure } : { kind: "ready", generation: credential.generation, tokenId: credential.tokenId };
|
|
15071
16205
|
}
|
|
15072
16206
|
async function prepareManagedInferenceSetup(options, discovery, health) {
|
|
@@ -15100,10 +16234,8 @@ async function prepareManagedInferenceSetup(options, discovery, health) {
|
|
|
15100
16234
|
const setup = readSetup(body?.setup);
|
|
15101
16235
|
const token = body?.inference_token;
|
|
15102
16236
|
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
|
-
)) {
|
|
16237
|
+
const modelDescriptors = readModelDescriptors(body?.model_descriptors);
|
|
16238
|
+
if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" || modelDescriptors === null) {
|
|
15107
16239
|
return { kind: "failed", reason: "Managed credential bootstrap returned an invalid response" };
|
|
15108
16240
|
}
|
|
15109
16241
|
const credential = {
|
|
@@ -15172,33 +16304,49 @@ function resolveLogLevel(options) {
|
|
|
15172
16304
|
}
|
|
15173
16305
|
return "info";
|
|
15174
16306
|
}
|
|
15175
|
-
function
|
|
15176
|
-
const
|
|
16307
|
+
function resolveFileSyncEntry(entry, homeDir, flagName, labels) {
|
|
16308
|
+
const trimmed = entry.trim();
|
|
16309
|
+
if (trimmed === "") {
|
|
16310
|
+
throw new Error(`${flagName} requires a ${labels.noun} path (got an empty value)`);
|
|
16311
|
+
}
|
|
16312
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join16(homeDir, trimmed.slice(2)) : trimmed;
|
|
16313
|
+
if (!isAbsolute4(expanded)) {
|
|
16314
|
+
throw new Error(`${flagName} requires an absolute ${labels.noun} path; got "${entry}"`);
|
|
16315
|
+
}
|
|
16316
|
+
const normalized = resolvePath2(expanded);
|
|
16317
|
+
if (parse(normalized).root === normalized) {
|
|
16318
|
+
throw new Error(
|
|
16319
|
+
`${flagName} will not allow-list the filesystem root ("${entry}"); name the specific ${labels.noun} the credentials belong in (for example ${labels.rootExample})`
|
|
16320
|
+
);
|
|
16321
|
+
}
|
|
16322
|
+
return normalized;
|
|
16323
|
+
}
|
|
16324
|
+
function resolveFileSyncAllowList(raw, homeDir, flagName, labels) {
|
|
16325
|
+
const paths = [];
|
|
15177
16326
|
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
|
-
}
|
|
16327
|
+
const normalized = resolveFileSyncEntry(entry, homeDir, flagName, labels);
|
|
16328
|
+
if (!paths.includes(normalized)) paths.push(normalized);
|
|
15195
16329
|
}
|
|
15196
|
-
if (
|
|
16330
|
+
if (paths.length > MAX_FILE_SYNC_DIRECTORIES) {
|
|
15197
16331
|
throw new Error(
|
|
15198
|
-
|
|
16332
|
+
`${flagName} accepts at most ${MAX_FILE_SYNC_DIRECTORIES} ${labels.plural}; got ${paths.length}`
|
|
15199
16333
|
);
|
|
15200
16334
|
}
|
|
15201
|
-
return
|
|
16335
|
+
return paths;
|
|
16336
|
+
}
|
|
16337
|
+
function resolveFileSyncDirectories(raw, homeDir) {
|
|
16338
|
+
return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to", {
|
|
16339
|
+
noun: "directory",
|
|
16340
|
+
plural: "directories",
|
|
16341
|
+
rootExample: "~/.claude"
|
|
16342
|
+
});
|
|
16343
|
+
}
|
|
16344
|
+
function resolveFileSyncExactFiles(raw, homeDir) {
|
|
16345
|
+
return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to-exact-file", {
|
|
16346
|
+
noun: "file",
|
|
16347
|
+
plural: "files",
|
|
16348
|
+
rootExample: "~/.local/share/opencode/mcp-auth.json"
|
|
16349
|
+
});
|
|
15202
16350
|
}
|
|
15203
16351
|
var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
|
|
15204
16352
|
var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
|
|
@@ -15214,13 +16362,13 @@ function resolveOpenCodeVersion(options, env = process.env) {
|
|
|
15214
16362
|
raw = env[OPENCODE_VERSION_ENV];
|
|
15215
16363
|
source = OPENCODE_VERSION_ENV;
|
|
15216
16364
|
} else {
|
|
15217
|
-
return { version: "
|
|
16365
|
+
return { version: "auto", warnings: [] };
|
|
15218
16366
|
}
|
|
15219
16367
|
const normalized = raw.trim().toLowerCase();
|
|
15220
|
-
if (normalized !== "v1" && normalized !== "v2") {
|
|
16368
|
+
if (normalized !== "v1" && normalized !== "v2" && normalized !== "auto") {
|
|
15221
16369
|
return {
|
|
15222
|
-
version: "
|
|
15223
|
-
warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or
|
|
16370
|
+
version: "auto",
|
|
16371
|
+
warnings: [`Ignoring invalid ${source} "${raw}": expected v1, v2, or auto; using auto`]
|
|
15224
16372
|
};
|
|
15225
16373
|
}
|
|
15226
16374
|
return { version: normalized, warnings: [] };
|
|
@@ -15730,8 +16878,8 @@ async function driveChannels(state, driver) {
|
|
|
15730
16878
|
state.running = false;
|
|
15731
16879
|
break;
|
|
15732
16880
|
}
|
|
15733
|
-
const
|
|
15734
|
-
logActivity(state, { type: "error", error: `Channel processing error: ${
|
|
16881
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : String(error2);
|
|
16882
|
+
logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage4}` });
|
|
15735
16883
|
if (state.interactive) displayStatus(state);
|
|
15736
16884
|
if (driver.hasInFlightWatchers()) {
|
|
15737
16885
|
consecutiveDrainFailures = 0;
|
|
@@ -15821,6 +16969,7 @@ async function driveChannels(state, driver) {
|
|
|
15821
16969
|
}
|
|
15822
16970
|
}
|
|
15823
16971
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
16972
|
+
var SESSION_ARCHIVE_FIRST_RUN_MS = SESSION_CLEANUP_FIRST_SWEEP_MS + 1e3;
|
|
15824
16973
|
var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
|
|
15825
16974
|
var SESSION_CLEANUP_MAX_ISOLATED_DELETE_FAILURES = 2;
|
|
15826
16975
|
var SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES = 2;
|
|
@@ -15835,7 +16984,7 @@ function shouldWarnForReclaimSkip(reason) {
|
|
|
15835
16984
|
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
15836
16985
|
}
|
|
15837
16986
|
function sessionDbPath() {
|
|
15838
|
-
return
|
|
16987
|
+
return join16(homedir8(), ".local", "share", "opencode", "opencode.db");
|
|
15839
16988
|
}
|
|
15840
16989
|
function logSessionCleanupActivity(state, phase, entry) {
|
|
15841
16990
|
logActivity(state, {
|
|
@@ -15844,6 +16993,63 @@ function logSessionCleanupActivity(state, phase, entry) {
|
|
|
15844
16993
|
metadata: { ...entry.metadata, phase }
|
|
15845
16994
|
});
|
|
15846
16995
|
}
|
|
16996
|
+
function logSessionArchiveActivity(state, phase, entry) {
|
|
16997
|
+
logActivity(state, {
|
|
16998
|
+
...entry,
|
|
16999
|
+
source: "cli.session-archive",
|
|
17000
|
+
metadata: { ...entry.metadata, phase }
|
|
17001
|
+
});
|
|
17002
|
+
}
|
|
17003
|
+
function logSessionArchiveSummary(state, phase, summary) {
|
|
17004
|
+
const everyUploadFailed = summary.uploadAttempts > 0 && summary.uploadFailures === summary.uploadAttempts;
|
|
17005
|
+
const staleSince = everyUploadFailed && summary.staleSince !== null ? `; archive stale since ${summary.staleSince}` : "";
|
|
17006
|
+
const neverWatermarked = summary.neverWatermarkedFailures > 0 ? `; ${summary.neverWatermarkedFailures} failed session(s) had no prior successful upload` : "";
|
|
17007
|
+
const warning2 = summary.failed > 0 || summary.failures.length > 0 || everyUploadFailed;
|
|
17008
|
+
logSessionArchiveActivity(state, phase, {
|
|
17009
|
+
type: "info",
|
|
17010
|
+
level: warning2 ? "warn" : "info",
|
|
17011
|
+
message: `Session archive: inspected ${summary.inspected}, uploaded ${summary.uploaded}, unchanged ${summary.unchanged}, failed ${summary.failed}, skipped budget ${summary.skippedBudget}, unmapped ${summary.unmappedIds.length}` + staleSince + neverWatermarked,
|
|
17012
|
+
metadata: {
|
|
17013
|
+
outcome: "archive-run-complete",
|
|
17014
|
+
sessions_inspected: summary.inspected,
|
|
17015
|
+
sessions_uploaded: summary.uploaded,
|
|
17016
|
+
sessions_unchanged: summary.unchanged,
|
|
17017
|
+
sessions_failed: summary.failed,
|
|
17018
|
+
uploads_attempted: summary.uploadAttempts,
|
|
17019
|
+
uploads_failed: summary.uploadFailures,
|
|
17020
|
+
sessions_skipped_budget: summary.skippedBudget,
|
|
17021
|
+
sessions_unmapped: summary.unmappedIds.length,
|
|
17022
|
+
sessions_without_prior_watermark: summary.neverWatermarkedFailures,
|
|
17023
|
+
...everyUploadFailed && summary.staleSince !== null ? { stale_since: summary.staleSince } : {}
|
|
17024
|
+
}
|
|
17025
|
+
});
|
|
17026
|
+
for (const failure of summary.failures) {
|
|
17027
|
+
const permanentSizeFailure = failure.reason === "too-large";
|
|
17028
|
+
const safeMessage = failure.message ? `: ${redactDiagnosticText(failure.message).slice(0, 200)}` : "";
|
|
17029
|
+
logSessionArchiveActivity(state, phase, {
|
|
17030
|
+
type: "info",
|
|
17031
|
+
level: "warn",
|
|
17032
|
+
message: `Session archive: session ${failure.sessionId} ` + (permanentSizeFailure ? "failed permanently (too-large payload)" : `failed (${failure.outcome}`) + (permanentSizeFailure ? "" : failure.reason ? `, reason=${failure.reason})` : ")") + safeMessage,
|
|
17033
|
+
metadata: {
|
|
17034
|
+
outcome: failure.outcome,
|
|
17035
|
+
session_id: failure.sessionId,
|
|
17036
|
+
archived: failure.archived,
|
|
17037
|
+
...failure.reason !== void 0 ? { reason: failure.reason } : {}
|
|
17038
|
+
}
|
|
17039
|
+
});
|
|
17040
|
+
}
|
|
17041
|
+
if (summary.unmappedIds.length > 0) {
|
|
17042
|
+
logSessionArchiveActivity(state, phase, {
|
|
17043
|
+
type: "info",
|
|
17044
|
+
level: "warn",
|
|
17045
|
+
message: `Session archive: ${summary.unmappedIds.length} unmapped session(s): ` + summary.unmappedIds.join(", "),
|
|
17046
|
+
metadata: {
|
|
17047
|
+
outcome: "unmapped-sessions",
|
|
17048
|
+
sessions_unmapped: summary.unmappedIds.length
|
|
17049
|
+
}
|
|
17050
|
+
});
|
|
17051
|
+
}
|
|
17052
|
+
}
|
|
15847
17053
|
async function drainSessionDeletionReports(state) {
|
|
15848
17054
|
const path = sessionDeletionReportPath(homedir8(), process.env);
|
|
15849
17055
|
let delivered = 0;
|
|
@@ -15941,6 +17147,7 @@ async function runSweep(state, driver, config) {
|
|
|
15941
17147
|
let failedOtherStatus = 0;
|
|
15942
17148
|
let failedThrew = 0;
|
|
15943
17149
|
let skippedNewlyActive = 0;
|
|
17150
|
+
let skippedArchive = 0;
|
|
15944
17151
|
let remaining = 0;
|
|
15945
17152
|
const deletionRecords = [];
|
|
15946
17153
|
let protectedNow = driver.protectedSessionIds();
|
|
@@ -15952,6 +17159,7 @@ async function runSweep(state, driver, config) {
|
|
|
15952
17159
|
const lastActivityById = new Map(
|
|
15953
17160
|
snapshots.map(({ id, lastActivityMs }) => [id, lastActivityMs])
|
|
15954
17161
|
);
|
|
17162
|
+
const sessionById = new Map(sessions.map((session) => [session.id, session]));
|
|
15955
17163
|
const toDelete = selectSessionsToDelete(snapshots, {
|
|
15956
17164
|
maxAgeMs: config.maxAgeMs,
|
|
15957
17165
|
maxCount: config.maxCount,
|
|
@@ -15974,6 +17182,42 @@ async function runSweep(state, driver, config) {
|
|
|
15974
17182
|
});
|
|
15975
17183
|
continue;
|
|
15976
17184
|
}
|
|
17185
|
+
if (state.sessionArchiveConfig.enabled) {
|
|
17186
|
+
const candidate = sessionById.get(id);
|
|
17187
|
+
const archiveConfig = state.sessionArchiveConfig;
|
|
17188
|
+
const archiveBudgetMs = Math.min(
|
|
17189
|
+
archiveConfig.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
|
|
17190
|
+
MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
|
|
17191
|
+
);
|
|
17192
|
+
const archived = candidate ? await runSessionArchiveTask(
|
|
17193
|
+
state,
|
|
17194
|
+
"sweep",
|
|
17195
|
+
archiveBudgetMs,
|
|
17196
|
+
(signal) => runSessionArchiveBatch(state, "sweep", [candidate], archiveBudgetMs, signal)
|
|
17197
|
+
) : null;
|
|
17198
|
+
if (!archived?.archivedSessionIds.includes(id)) {
|
|
17199
|
+
skippedArchive++;
|
|
17200
|
+
logSessionArchiveActivity(state, "sweep", {
|
|
17201
|
+
type: "info",
|
|
17202
|
+
level: "warn",
|
|
17203
|
+
message: `Session cleanup: retaining session ${id}; deletion is held back pending successful archival`,
|
|
17204
|
+
metadata: {
|
|
17205
|
+
outcome: "deletion-held-for-archive",
|
|
17206
|
+
session_id: id
|
|
17207
|
+
}
|
|
17208
|
+
});
|
|
17209
|
+
continue;
|
|
17210
|
+
}
|
|
17211
|
+
protectedNow = driver.protectedSessionIds();
|
|
17212
|
+
if (protectedNow.has(id)) {
|
|
17213
|
+
skippedNewlyActive++;
|
|
17214
|
+
logActivity(state, {
|
|
17215
|
+
type: "info",
|
|
17216
|
+
message: `Session cleanup: skipping ${id} \u2014 became active/bound during archival (${mode})`
|
|
17217
|
+
});
|
|
17218
|
+
continue;
|
|
17219
|
+
}
|
|
17220
|
+
}
|
|
15977
17221
|
const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
|
|
15978
17222
|
if (outcome.ok) {
|
|
15979
17223
|
deleted++;
|
|
@@ -16037,10 +17281,11 @@ async function runSweep(state, driver, config) {
|
|
|
16037
17281
|
}
|
|
16038
17282
|
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
16039
17283
|
const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
|
|
17284
|
+
const archiveHeldNote = skippedArchive > 0 ? `, archive held ${skippedArchive}` : "";
|
|
16040
17285
|
const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
|
|
16041
17286
|
logActivity(state, {
|
|
16042
17287
|
type: "info",
|
|
16043
|
-
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
|
|
17288
|
+
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${archiveHeldNote}${remainingNote} (${mode})`
|
|
16044
17289
|
});
|
|
16045
17290
|
}
|
|
16046
17291
|
if (deletionRecords.length > 0) {
|
|
@@ -16078,6 +17323,7 @@ async function runSweep(state, driver, config) {
|
|
|
16078
17323
|
sessions_failed_other: failedOtherStatus,
|
|
16079
17324
|
sessions_failed_threw: failedThrew,
|
|
16080
17325
|
sessions_skipped_active: skippedNewlyActive,
|
|
17326
|
+
sessions_archive_held: skippedArchive,
|
|
16081
17327
|
sessions_remaining: remaining,
|
|
16082
17328
|
...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
|
|
16083
17329
|
};
|
|
@@ -16209,6 +17455,186 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
16209
17455
|
);
|
|
16210
17456
|
state.sessionCleanupTimers.push(interval, firstSweep);
|
|
16211
17457
|
}
|
|
17458
|
+
function runSessionArchiveTask(state, phase, budgetMs, work) {
|
|
17459
|
+
if (state.sessionArchiveInFlight) {
|
|
17460
|
+
logSessionArchiveActivity(state, phase, {
|
|
17461
|
+
type: "info",
|
|
17462
|
+
level: "debug",
|
|
17463
|
+
message: "Session archive skipped \u2014 previous archive run still in progress",
|
|
17464
|
+
metadata: { outcome: "skipped-in-flight" }
|
|
17465
|
+
});
|
|
17466
|
+
return Promise.resolve(null);
|
|
17467
|
+
}
|
|
17468
|
+
const controller = new AbortController();
|
|
17469
|
+
state.sessionArchiveAbortController = controller;
|
|
17470
|
+
const timeout = setTimeout(() => {
|
|
17471
|
+
controller.abort();
|
|
17472
|
+
logSessionArchiveActivity(state, phase, {
|
|
17473
|
+
type: "info",
|
|
17474
|
+
level: "warn",
|
|
17475
|
+
message: `Session archive exceeded ${budgetMs}ms; remaining sessions were not confirmed archived`,
|
|
17476
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17477
|
+
});
|
|
17478
|
+
}, budgetMs);
|
|
17479
|
+
const task = Promise.resolve().then(() => work(controller.signal)).catch((error2) => {
|
|
17480
|
+
logSessionArchiveActivity(state, phase, {
|
|
17481
|
+
type: "info",
|
|
17482
|
+
level: "warn",
|
|
17483
|
+
message: `Session archive run failed: ${redactDiagnosticText(error2 instanceof Error ? error2.message : String(error2)).slice(0, 200)}`,
|
|
17484
|
+
metadata: { outcome: "archive-run-failed" }
|
|
17485
|
+
});
|
|
17486
|
+
return null;
|
|
17487
|
+
}).finally(() => {
|
|
17488
|
+
clearTimeout(timeout);
|
|
17489
|
+
if (state.sessionArchiveInFlight === task) {
|
|
17490
|
+
state.sessionArchiveInFlight = null;
|
|
17491
|
+
state.sessionArchiveAbortController = null;
|
|
17492
|
+
}
|
|
17493
|
+
});
|
|
17494
|
+
state.sessionArchiveInFlight = task;
|
|
17495
|
+
return task;
|
|
17496
|
+
}
|
|
17497
|
+
async function runSessionArchiveBatch(state, phase, sessions, budgetMs, signal) {
|
|
17498
|
+
const config = state.sessionArchiveConfig;
|
|
17499
|
+
const client = state.opencodeClient;
|
|
17500
|
+
const watermarkPath = state.sessionArchiveWatermarkPath;
|
|
17501
|
+
if (!client || !config.keyPrefix || !watermarkPath || !state.agentId) {
|
|
17502
|
+
logSessionArchiveActivity(state, phase, {
|
|
17503
|
+
type: "info",
|
|
17504
|
+
level: "warn",
|
|
17505
|
+
message: "Session archive skipped because runner identity or archive configuration is unavailable",
|
|
17506
|
+
metadata: { outcome: "archive-configuration-unavailable" }
|
|
17507
|
+
});
|
|
17508
|
+
return null;
|
|
17509
|
+
}
|
|
17510
|
+
const summary = await runSessionArchive({
|
|
17511
|
+
sessions,
|
|
17512
|
+
deps: {
|
|
17513
|
+
client,
|
|
17514
|
+
runnerId: state.agentId,
|
|
17515
|
+
keyPrefix: config.keyPrefix,
|
|
17516
|
+
bindings: readSessionBindings(
|
|
17517
|
+
sessionBindingLedgerPath(homedir8(), process.env),
|
|
17518
|
+
(message) => logSessionArchiveActivity(state, phase, {
|
|
17519
|
+
type: "info",
|
|
17520
|
+
level: "warn",
|
|
17521
|
+
message: redactDiagnosticText(message),
|
|
17522
|
+
metadata: { outcome: "binding-ledger-read-failed" }
|
|
17523
|
+
})
|
|
17524
|
+
),
|
|
17525
|
+
watermarks: state.sessionArchiveWatermarks,
|
|
17526
|
+
watermarkPath,
|
|
17527
|
+
signal,
|
|
17528
|
+
upload: uploadSessionArchive,
|
|
17529
|
+
exportMessages: exportSessionMessages,
|
|
17530
|
+
recordWatermark: recordUpload
|
|
17531
|
+
},
|
|
17532
|
+
budgetMs
|
|
17533
|
+
});
|
|
17534
|
+
logSessionArchiveSummary(state, phase, summary);
|
|
17535
|
+
return summary;
|
|
17536
|
+
}
|
|
17537
|
+
async function runSessionArchiveFleet(state, phase, budgetMs, signal) {
|
|
17538
|
+
const startedAt = Date.now();
|
|
17539
|
+
const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0, signal);
|
|
17540
|
+
if (enumeration.sessions === null) {
|
|
17541
|
+
const interrupted = signal.aborted;
|
|
17542
|
+
logSessionArchiveActivity(state, phase, {
|
|
17543
|
+
type: "info",
|
|
17544
|
+
level: "warn",
|
|
17545
|
+
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`,
|
|
17546
|
+
metadata: {
|
|
17547
|
+
outcome: interrupted ? "enumeration-interrupted" : "enumeration-refused",
|
|
17548
|
+
...!interrupted ? { reason: enumeration.reason } : {}
|
|
17549
|
+
}
|
|
17550
|
+
});
|
|
17551
|
+
return null;
|
|
17552
|
+
}
|
|
17553
|
+
return runSessionArchiveBatch(
|
|
17554
|
+
state,
|
|
17555
|
+
phase,
|
|
17556
|
+
enumeration.sessions,
|
|
17557
|
+
Math.max(0, budgetMs - (Date.now() - startedAt)),
|
|
17558
|
+
signal
|
|
17559
|
+
);
|
|
17560
|
+
}
|
|
17561
|
+
async function runSessionArchiveShutdownFlush(state, budgetMs) {
|
|
17562
|
+
const deadline = Date.now() + budgetMs;
|
|
17563
|
+
const previousRun = state.sessionArchiveInFlight;
|
|
17564
|
+
if (previousRun) {
|
|
17565
|
+
state.sessionArchiveAbortController?.abort(
|
|
17566
|
+
new Error("graceful shutdown archive flush requested")
|
|
17567
|
+
);
|
|
17568
|
+
let timeout;
|
|
17569
|
+
const settled = await Promise.race([
|
|
17570
|
+
previousRun.then(() => true),
|
|
17571
|
+
new Promise((resolve5) => {
|
|
17572
|
+
timeout = setTimeout(() => resolve5(false), budgetMs);
|
|
17573
|
+
})
|
|
17574
|
+
]);
|
|
17575
|
+
if (timeout) clearTimeout(timeout);
|
|
17576
|
+
if (!settled) {
|
|
17577
|
+
state.sessionArchiveAbortController?.abort(new Error("session archive flush budget expired"));
|
|
17578
|
+
logSessionArchiveActivity(state, "shutdown", {
|
|
17579
|
+
type: "info",
|
|
17580
|
+
level: "warn",
|
|
17581
|
+
message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
|
|
17582
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17583
|
+
});
|
|
17584
|
+
return null;
|
|
17585
|
+
}
|
|
17586
|
+
}
|
|
17587
|
+
const remainingMs = Math.floor(deadline - Date.now());
|
|
17588
|
+
if (remainingMs <= 0) {
|
|
17589
|
+
logSessionArchiveActivity(state, "shutdown", {
|
|
17590
|
+
type: "info",
|
|
17591
|
+
level: "warn",
|
|
17592
|
+
message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
|
|
17593
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17594
|
+
});
|
|
17595
|
+
return null;
|
|
17596
|
+
}
|
|
17597
|
+
return runSessionArchiveTask(
|
|
17598
|
+
state,
|
|
17599
|
+
"shutdown",
|
|
17600
|
+
remainingMs,
|
|
17601
|
+
(signal) => runSessionArchiveFleet(state, "shutdown", remainingMs, signal)
|
|
17602
|
+
);
|
|
17603
|
+
}
|
|
17604
|
+
function scheduleSessionArchive(state) {
|
|
17605
|
+
const config = state.sessionArchiveConfig;
|
|
17606
|
+
if (!config.enabled) return;
|
|
17607
|
+
state.sessionArchiveWatermarkPath = sessionArchiveWatermarkPath(homedir8(), process.env);
|
|
17608
|
+
state.sessionArchiveWatermarks = readWatermarks(
|
|
17609
|
+
state.sessionArchiveWatermarkPath,
|
|
17610
|
+
(message) => logSessionArchiveActivity(state, "startup", {
|
|
17611
|
+
type: "info",
|
|
17612
|
+
level: "warn",
|
|
17613
|
+
message: redactDiagnosticText(message),
|
|
17614
|
+
metadata: { outcome: "watermark-read-failed" }
|
|
17615
|
+
})
|
|
17616
|
+
);
|
|
17617
|
+
logSessionArchiveActivity(state, "startup", {
|
|
17618
|
+
type: "info",
|
|
17619
|
+
message: `Session archive enabled (interval=${config.intervalMs}ms)`,
|
|
17620
|
+
metadata: { outcome: "armed", interval_ms: config.intervalMs }
|
|
17621
|
+
});
|
|
17622
|
+
const cycleBudgetMs = Math.min(
|
|
17623
|
+
config.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
|
|
17624
|
+
MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
|
|
17625
|
+
);
|
|
17626
|
+
const run2 = (phase = "scheduled") => {
|
|
17627
|
+
void runSessionArchiveTask(
|
|
17628
|
+
state,
|
|
17629
|
+
phase,
|
|
17630
|
+
cycleBudgetMs,
|
|
17631
|
+
(signal) => runSessionArchiveFleet(state, phase, cycleBudgetMs, signal)
|
|
17632
|
+
);
|
|
17633
|
+
};
|
|
17634
|
+
const interval = setInterval(run2, config.intervalMs);
|
|
17635
|
+
const firstRun = setTimeout(() => run2(), SESSION_ARCHIVE_FIRST_RUN_MS);
|
|
17636
|
+
state.sessionArchiveTimers.push(interval, firstRun);
|
|
17637
|
+
}
|
|
16212
17638
|
function scheduleUsageReporting(state, params) {
|
|
16213
17639
|
const { mode, warnings } = params.resolved;
|
|
16214
17640
|
for (const warning2 of warnings) {
|
|
@@ -16483,6 +17909,12 @@ async function cleanup(state, opts = {}) {
|
|
|
16483
17909
|
clearTimeout(timer);
|
|
16484
17910
|
}
|
|
16485
17911
|
state.sessionCleanupTimers = [];
|
|
17912
|
+
for (const timer of state.sessionArchiveTimers) {
|
|
17913
|
+
clearInterval(timer);
|
|
17914
|
+
clearTimeout(timer);
|
|
17915
|
+
}
|
|
17916
|
+
state.sessionArchiveTimers = [];
|
|
17917
|
+
state.sessionArchiveAbortController?.abort(new Error("runner cleanup started"));
|
|
16486
17918
|
state.stopOpenCodeLogTail?.();
|
|
16487
17919
|
state.stopOpenCodeLogTail = null;
|
|
16488
17920
|
if (state.claudeUsageTimer) {
|
|
@@ -16550,6 +17982,15 @@ async function cleanup(state, opts = {}) {
|
|
|
16550
17982
|
await timeShutdownPhase(state, durations, "tunnel_close", () => connection.close());
|
|
16551
17983
|
state.connection = null;
|
|
16552
17984
|
}
|
|
17985
|
+
if (opts.graceful && state.sessionArchiveConfig.enabled) {
|
|
17986
|
+
const budgetMs = state.sessionArchiveConfig.flushBudgetMs;
|
|
17987
|
+
await timeShutdownPhase(
|
|
17988
|
+
state,
|
|
17989
|
+
durations,
|
|
17990
|
+
"session_archive_flush",
|
|
17991
|
+
() => runSessionArchiveShutdownFlush(state, budgetMs)
|
|
17992
|
+
);
|
|
17993
|
+
}
|
|
16553
17994
|
if (state.opencodeProcess) {
|
|
16554
17995
|
const opencodeProcess = state.opencodeProcess;
|
|
16555
17996
|
const result = await timeShutdownPhase(
|
|
@@ -16621,9 +18062,11 @@ async function run(options) {
|
|
|
16621
18062
|
const interactive = isInteractive(options.json);
|
|
16622
18063
|
let logLevel;
|
|
16623
18064
|
let fileSyncDirectories;
|
|
18065
|
+
let fileSyncExactFiles;
|
|
16624
18066
|
try {
|
|
16625
18067
|
logLevel = resolveLogLevel(options);
|
|
16626
18068
|
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir8());
|
|
18069
|
+
fileSyncExactFiles = resolveFileSyncExactFiles(options.enableFileSyncToExactFile, homedir8());
|
|
16627
18070
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
16628
18071
|
throw new Error(
|
|
16629
18072
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -16671,6 +18114,18 @@ async function run(options) {
|
|
|
16671
18114
|
messageCount: 0,
|
|
16672
18115
|
lastProxiedActivityAt: null,
|
|
16673
18116
|
sessionCleanupTimers: [],
|
|
18117
|
+
sessionArchiveConfig: resolveSessionArchiveConfig(
|
|
18118
|
+
{
|
|
18119
|
+
enabled: options.sessionArchive,
|
|
18120
|
+
interval: options.sessionArchiveInterval
|
|
18121
|
+
},
|
|
18122
|
+
process.env
|
|
18123
|
+
),
|
|
18124
|
+
sessionArchiveTimers: [],
|
|
18125
|
+
sessionArchiveWatermarks: /* @__PURE__ */ new Map(),
|
|
18126
|
+
sessionArchiveWatermarkPath: null,
|
|
18127
|
+
sessionArchiveInFlight: null,
|
|
18128
|
+
sessionArchiveAbortController: null,
|
|
16674
18129
|
sessionDeleteFailures: /* @__PURE__ */ new Map(),
|
|
16675
18130
|
claudeUsageTimer: null,
|
|
16676
18131
|
claudeUsageRearm: null,
|
|
@@ -16684,6 +18139,14 @@ async function run(options) {
|
|
|
16684
18139
|
};
|
|
16685
18140
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
16686
18141
|
setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
|
|
18142
|
+
for (const warning2 of state.sessionArchiveConfig.warnings) {
|
|
18143
|
+
logSessionArchiveActivity(state, "startup", {
|
|
18144
|
+
type: "info",
|
|
18145
|
+
level: "warn",
|
|
18146
|
+
message: `Session archive: ${warning2}`,
|
|
18147
|
+
metadata: { outcome: "configuration-warning" }
|
|
18148
|
+
});
|
|
18149
|
+
}
|
|
16687
18150
|
if (options.credentialSyncMarker) {
|
|
16688
18151
|
state.credentialSync = createCredentialSync({
|
|
16689
18152
|
markerPath: options.credentialSyncMarker,
|
|
@@ -16699,7 +18162,11 @@ async function run(options) {
|
|
|
16699
18162
|
}
|
|
16700
18163
|
if (fileSyncDirectories.length > 0) {
|
|
16701
18164
|
log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
|
|
16702
|
-
}
|
|
18165
|
+
}
|
|
18166
|
+
if (fileSyncExactFiles.length > 0) {
|
|
18167
|
+
log2(state, `File sync exact files enabled for: ${fileSyncExactFiles.join(", ")}`);
|
|
18168
|
+
}
|
|
18169
|
+
if (fileSyncDirectories.length === 0 && fileSyncExactFiles.length === 0) {
|
|
16703
18170
|
log2(state, "File sync is disabled (no --enable-file-sync-to given)", "debug");
|
|
16704
18171
|
}
|
|
16705
18172
|
if (!options.runner && options.agent) {
|
|
@@ -17001,13 +18468,75 @@ async function run(options) {
|
|
|
17001
18468
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
17002
18469
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
17003
18470
|
}
|
|
17004
|
-
const { version:
|
|
17005
|
-
options,
|
|
17006
|
-
process.env
|
|
17007
|
-
);
|
|
18471
|
+
const { version: opencodeVersionSelection, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(options, process.env);
|
|
17008
18472
|
for (const warning2 of opencodeVersionWarnings) {
|
|
17009
18473
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
17010
18474
|
}
|
|
18475
|
+
let opencodeMajor;
|
|
18476
|
+
let opencodeV2Command;
|
|
18477
|
+
let opencodeDetectionSource;
|
|
18478
|
+
let opencodeDetectionLevel = "info";
|
|
18479
|
+
let opencodeDetectionMessage;
|
|
18480
|
+
if (opencodeVersionSelection === "v1") {
|
|
18481
|
+
opencodeMajor = "v1";
|
|
18482
|
+
opencodeDetectionSource = "explicit";
|
|
18483
|
+
opencodeDetectionMessage = "OpenCode V1 was selected explicitly.";
|
|
18484
|
+
} else if (opencodeVersionSelection === "v2") {
|
|
18485
|
+
const installed = resolveInstalledOpenCode();
|
|
18486
|
+
opencodeMajor = "v2";
|
|
18487
|
+
opencodeDetectionSource = "explicit";
|
|
18488
|
+
if (installed.kind === "installed" && installed.major === "v2") {
|
|
18489
|
+
opencodeV2Command = installed.command;
|
|
18490
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; using ${installed.command}${installed.version ? ` (${installed.version})` : ""}.`;
|
|
18491
|
+
} else if (installed.kind === "unknown") {
|
|
18492
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported ${JSON.stringify(installed.rawVersion)}, but no V2 command was resolved.`;
|
|
18493
|
+
} else if (installed.kind === "installed") {
|
|
18494
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported V1 ${installed.version ?? "with an unknown version"}, so no V2 command was resolved.`;
|
|
18495
|
+
} else {
|
|
18496
|
+
opencodeDetectionMessage = "OpenCode V2 was selected explicitly, but no V2 command was resolved.";
|
|
18497
|
+
}
|
|
18498
|
+
} else {
|
|
18499
|
+
const running = await classifyRunningOpenCode(state.port);
|
|
18500
|
+
if (running.kind === "v1") {
|
|
18501
|
+
opencodeMajor = "v1";
|
|
18502
|
+
opencodeDetectionSource = "running-server";
|
|
18503
|
+
opencodeDetectionMessage = `OpenCode detection: found V1 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
|
|
18504
|
+
} else if (running.kind === "v2") {
|
|
18505
|
+
opencodeMajor = "v2";
|
|
18506
|
+
opencodeDetectionSource = "running-server";
|
|
18507
|
+
opencodeDetectionMessage = `OpenCode detection: found V2 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
|
|
18508
|
+
} else if (running.kind === "authenticated-unknown") {
|
|
18509
|
+
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.`;
|
|
18510
|
+
logActivity(state, {
|
|
18511
|
+
type: "info",
|
|
18512
|
+
level: "warn",
|
|
18513
|
+
message: `OpenCode detection: authenticated-unknown on port ${state.port}; ${running.detail}. ${message}`
|
|
18514
|
+
});
|
|
18515
|
+
throw new Error(message);
|
|
18516
|
+
} else {
|
|
18517
|
+
const installed = resolveInstalledOpenCode();
|
|
18518
|
+
opencodeDetectionLevel = "warn";
|
|
18519
|
+
if (installed.kind === "installed") {
|
|
18520
|
+
opencodeMajor = installed.major;
|
|
18521
|
+
opencodeDetectionSource = "installed-binary";
|
|
18522
|
+
if (installed.major === "v2") opencodeV2Command = installed.command;
|
|
18523
|
+
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"}.`;
|
|
18524
|
+
} else if (installed.kind === "unknown") {
|
|
18525
|
+
opencodeMajor = "v1";
|
|
18526
|
+
opencodeDetectionSource = "fallback-v1";
|
|
18527
|
+
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.`;
|
|
18528
|
+
} else {
|
|
18529
|
+
opencodeMajor = "v1";
|
|
18530
|
+
opencodeDetectionSource = "fallback-v1";
|
|
18531
|
+
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.`;
|
|
18532
|
+
}
|
|
18533
|
+
}
|
|
18534
|
+
}
|
|
18535
|
+
logActivity(state, {
|
|
18536
|
+
type: "info",
|
|
18537
|
+
level: opencodeDetectionLevel,
|
|
18538
|
+
message: opencodeDetectionMessage
|
|
18539
|
+
});
|
|
17011
18540
|
const managedSetupOptions = {
|
|
17012
18541
|
agentId: state.agentId,
|
|
17013
18542
|
apiUrl: getApiUrlConfig(),
|
|
@@ -17028,7 +18557,7 @@ async function run(options) {
|
|
|
17028
18557
|
managedPreparation = managedDiscovery;
|
|
17029
18558
|
managedSetupOptions.log(`MANAGED-INFERENCE-NOT-READY: ${managedDiscovery.reason}`, "warn");
|
|
17030
18559
|
} else if (managedDiscovery.kind === "setup") {
|
|
17031
|
-
const managedHealth =
|
|
18560
|
+
const managedHealth = opencodeMajor === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
|
|
17032
18561
|
managedPreparation = await prepareManagedInferenceSetup(
|
|
17033
18562
|
managedSetupOptions,
|
|
17034
18563
|
managedDiscovery,
|
|
@@ -17053,13 +18582,14 @@ async function run(options) {
|
|
|
17053
18582
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
17054
18583
|
const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
|
|
17055
18584
|
try {
|
|
17056
|
-
const oc =
|
|
18585
|
+
const oc = opencodeMajor === "v2" ? await ensureOpenCode2Running({
|
|
17057
18586
|
port: state.port,
|
|
17058
18587
|
interactive: state.interactive,
|
|
17059
18588
|
agentId: state.agentId,
|
|
17060
18589
|
log: (message) => log2(state, message),
|
|
17061
18590
|
startTimeoutMs: opencodeStartTimeoutMs,
|
|
17062
|
-
inheritStdio: Boolean(options.opencodePidFile)
|
|
18591
|
+
inheritStdio: Boolean(options.opencodePidFile),
|
|
18592
|
+
command: opencodeV2Command
|
|
17063
18593
|
}) : await ensureOpenCodeRunning({
|
|
17064
18594
|
port: state.port,
|
|
17065
18595
|
interactive: state.interactive,
|
|
@@ -17071,7 +18601,7 @@ async function run(options) {
|
|
|
17071
18601
|
state.port = oc.port;
|
|
17072
18602
|
state.opencodeProcess = options.opencodePidFile ? null : oc.process;
|
|
17073
18603
|
state.opencodeVersion = oc.version;
|
|
17074
|
-
state.opencodeApiVersion =
|
|
18604
|
+
state.opencodeApiVersion = opencodeMajor;
|
|
17075
18605
|
let configuredPlugins = [];
|
|
17076
18606
|
let resolvedPlugins = [];
|
|
17077
18607
|
try {
|
|
@@ -17093,13 +18623,14 @@ async function run(options) {
|
|
|
17093
18623
|
metadata: {
|
|
17094
18624
|
opencode_version: state.opencodeVersion,
|
|
17095
18625
|
opencode_api_version: state.opencodeApiVersion,
|
|
18626
|
+
opencode_detection_source: opencodeDetectionSource,
|
|
17096
18627
|
configured_plugins: JSON.stringify(configuredPlugins),
|
|
17097
18628
|
resolved_plugins: JSON.stringify(resolvedPlugins)
|
|
17098
18629
|
},
|
|
17099
18630
|
source: "cli.runtime-provenance"
|
|
17100
18631
|
});
|
|
17101
18632
|
let opencodePassword = null;
|
|
17102
|
-
if (
|
|
18633
|
+
if (opencodeMajor === "v2" && "password" in oc) {
|
|
17103
18634
|
const value = oc.password;
|
|
17104
18635
|
if (typeof value === "string" || value === null) opencodePassword = value;
|
|
17105
18636
|
}
|
|
@@ -17112,7 +18643,7 @@ async function run(options) {
|
|
|
17112
18643
|
state.opencodeClient = openCodeClient;
|
|
17113
18644
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
17114
18645
|
try {
|
|
17115
|
-
|
|
18646
|
+
writeFileSync9(options.opencodePidFile, `${oc.process.pid}
|
|
17116
18647
|
`, { mode: 384 });
|
|
17117
18648
|
chmodSync3(options.opencodePidFile, 384);
|
|
17118
18649
|
} catch (error2) {
|
|
@@ -17222,7 +18753,7 @@ async function run(options) {
|
|
|
17222
18753
|
let existingPid;
|
|
17223
18754
|
if (existsSync3(options.litestreamPidFile)) {
|
|
17224
18755
|
try {
|
|
17225
|
-
const rawPid =
|
|
18756
|
+
const rawPid = readFileSync12(options.litestreamPidFile, "utf8").trim();
|
|
17226
18757
|
const parsedPid = Number(rawPid);
|
|
17227
18758
|
if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
|
|
17228
18759
|
existingPid = parsedPid;
|
|
@@ -17259,7 +18790,7 @@ async function run(options) {
|
|
|
17259
18790
|
});
|
|
17260
18791
|
try {
|
|
17261
18792
|
if (litestreamProcess.pid !== void 0) {
|
|
17262
|
-
|
|
18793
|
+
writeFileSync9(options.litestreamPidFile, `${litestreamProcess.pid}
|
|
17263
18794
|
`, {
|
|
17264
18795
|
mode: 384
|
|
17265
18796
|
});
|
|
@@ -17309,6 +18840,15 @@ async function run(options) {
|
|
|
17309
18840
|
log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
|
|
17310
18841
|
}
|
|
17311
18842
|
const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
|
|
18843
|
+
const recordBinding = state.sessionArchiveConfig.enabled ? (conversationId, sessionId) => appendSessionBinding(
|
|
18844
|
+
sessionBindingLedgerPath(homedir8(), process.env),
|
|
18845
|
+
{
|
|
18846
|
+
conversation_id: conversationId,
|
|
18847
|
+
opencode_session_id: sessionId,
|
|
18848
|
+
observed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
18849
|
+
},
|
|
18850
|
+
(message) => logActivity(state, { type: "info", level: "warn", message })
|
|
18851
|
+
) : void 0;
|
|
17312
18852
|
const channelDriver = new ChannelDriver({
|
|
17313
18853
|
agentId: state.agentId,
|
|
17314
18854
|
port: state.port,
|
|
@@ -17318,9 +18858,11 @@ async function run(options) {
|
|
|
17318
18858
|
conversationFilter: state.conversationFilter,
|
|
17319
18859
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
17320
18860
|
sessionStoreHealth: state.sessionStoreHealth,
|
|
17321
|
-
|
|
17322
|
-
//
|
|
18861
|
+
recordBinding,
|
|
18862
|
+
// #559: the file-sync allow-lists. Empty ⇒ pending files are REJECTED with
|
|
18863
|
+
// `file_sync_disabled` on the ack, not silently ignored.
|
|
17323
18864
|
fileSyncDirectories,
|
|
18865
|
+
fileSyncExactFiles,
|
|
17324
18866
|
homeDir: homedir8(),
|
|
17325
18867
|
maxActiveSessions,
|
|
17326
18868
|
log: (entry) => (
|
|
@@ -17496,6 +19038,7 @@ async function run(options) {
|
|
|
17496
19038
|
}
|
|
17497
19039
|
if (!state.running) return;
|
|
17498
19040
|
scheduleSessionCleanup(state, channelDriver, options);
|
|
19041
|
+
scheduleSessionArchive(state);
|
|
17499
19042
|
state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
|
|
17500
19043
|
state.openaiUsageRearm = scheduleUsageReporting(state, {
|
|
17501
19044
|
label: "OpenAI",
|
|
@@ -17591,8 +19134,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17591
19134
|
"--opencode-start-timeout <seconds>",
|
|
17592
19135
|
"Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
|
|
17593
19136
|
).option(
|
|
17594
|
-
"--opencode-version <v1|v2>",
|
|
17595
|
-
"
|
|
19137
|
+
"--opencode-version <v1|v2|auto>",
|
|
19138
|
+
"OpenCode major version: auto (default) detects the running server, then installed binary; v1 and v2 skip server-protocol detection. Env: EVIDENT_OPENCODE_VERSION"
|
|
17596
19139
|
).option("--json", "Output in JSON format").option(
|
|
17597
19140
|
"--session-cleanup-max-age <duration>",
|
|
17598
19141
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
@@ -17605,6 +19148,12 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17605
19148
|
).option(
|
|
17606
19149
|
"--session-cleanup-interval <duration>",
|
|
17607
19150
|
"How often the cleanup sweep runs (default: 1h). Env: EVIDENT_SESSION_CLEANUP_INTERVAL"
|
|
19151
|
+
).option(
|
|
19152
|
+
"--session-archive",
|
|
19153
|
+
"Upload full OpenCode transcripts to the runner archive (off by default). Env: EVIDENT_SESSION_ARCHIVE"
|
|
19154
|
+
).option(
|
|
19155
|
+
"--session-archive-interval <duration>",
|
|
19156
|
+
"How often session archives are uploaded (default: 1h). Env: EVIDENT_SESSION_ARCHIVE_INTERVAL"
|
|
17608
19157
|
).option(
|
|
17609
19158
|
"--claude-usage-reporting <mode>",
|
|
17610
19159
|
"Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
@@ -17619,6 +19168,11 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17619
19168
|
"Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.",
|
|
17620
19169
|
(value, previous) => previous.concat([value]),
|
|
17621
19170
|
[]
|
|
19171
|
+
).option(
|
|
19172
|
+
"--enable-file-sync-to-exact-file <path>",
|
|
19173
|
+
"Allow Evident to write files to exactly this path (repeatable).",
|
|
19174
|
+
(value, previous) => previous.concat([value]),
|
|
19175
|
+
[]
|
|
17622
19176
|
).option(
|
|
17623
19177
|
"--tunnel-ready-file <path>",
|
|
17624
19178
|
"Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
|
|
@@ -17671,6 +19225,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17671
19225
|
sessionCleanupMaxCount: options.sessionCleanupMaxCount,
|
|
17672
19226
|
maxActiveSessions: options.maxActiveSessions,
|
|
17673
19227
|
sessionCleanupInterval: options.sessionCleanupInterval,
|
|
19228
|
+
sessionArchive: options.sessionArchive,
|
|
19229
|
+
sessionArchiveInterval: options.sessionArchiveInterval,
|
|
17674
19230
|
// Raw string — the resolver in run.ts single-sources parsing
|
|
17675
19231
|
// (resolveClaudeUsageReportingMode).
|
|
17676
19232
|
claudeUsageReporting: options.claudeUsageReporting,
|
|
@@ -17681,6 +19237,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17681
19237
|
// Raw values — expansion/validation is single-sourced in run.ts's
|
|
17682
19238
|
// resolveFileSyncDirectories.
|
|
17683
19239
|
enableFileSyncTo: options.enableFileSyncTo,
|
|
19240
|
+
// Raw values — expansion/validation is single-sourced in run.ts's
|
|
19241
|
+
// resolveFileSyncExactFiles.
|
|
19242
|
+
enableFileSyncToExactFile: options.enableFileSyncToExactFile,
|
|
17684
19243
|
tunnelReadyFile: options.tunnelReadyFile,
|
|
17685
19244
|
litestreamConfig: options.litestreamConfig,
|
|
17686
19245
|
opencodePidFile: options.opencodePidFile,
|