@evident-ai/cli 3.6.1-dev.10d71b3 → 3.6.1-dev.22e9703
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 +38 -7
- package/dist/index.js +1780 -443
- 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.22e9703" : 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,6 +3872,9 @@ 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
|
}
|
|
@@ -3078,28 +4058,38 @@ function sessionListPath(options) {
|
|
|
3078
4058
|
async function listSessionPage(port, client, options) {
|
|
3079
4059
|
const path = sessionListPath(options);
|
|
3080
4060
|
try {
|
|
3081
|
-
const res = await requestWithClient(
|
|
3082
|
-
|
|
4061
|
+
const res = await requestWithClient(
|
|
4062
|
+
port,
|
|
4063
|
+
client,
|
|
4064
|
+
path,
|
|
4065
|
+
options?.signal ? { signal: options.signal } : void 0,
|
|
4066
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
4067
|
+
);
|
|
4068
|
+
if (!res.ok) {
|
|
4069
|
+
console.error(`[listSessionPage] GET ${path} returned HTTP ${res.status} (port ${port})`);
|
|
4070
|
+
return null;
|
|
4071
|
+
}
|
|
3083
4072
|
const body = await res.json();
|
|
3084
4073
|
return Array.isArray(body) ? body : null;
|
|
3085
4074
|
} catch (error2) {
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
);
|
|
4075
|
+
if (options?.signal?.aborted) return null;
|
|
4076
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
4077
|
+
console.error(`[listSessionPage] GET ${path} failed (port ${port}): ${errorName}`);
|
|
3089
4078
|
return null;
|
|
3090
4079
|
}
|
|
3091
4080
|
}
|
|
3092
4081
|
async function listSessions(port, client, options) {
|
|
3093
|
-
if (client?.version === "v2") return listV2Sessions(client);
|
|
4082
|
+
if (client?.version === "v2") return listV2Sessions(client, options?.signal);
|
|
3094
4083
|
return listSessionPage(port, client, options);
|
|
3095
4084
|
}
|
|
3096
|
-
async function listAllSessions(port, client) {
|
|
4085
|
+
async function listAllSessions(port, client, signal) {
|
|
3097
4086
|
if (client?.version === "v2") {
|
|
3098
|
-
const sessions2 = await listV2Sessions(client);
|
|
4087
|
+
const sessions2 = await listV2Sessions(client, signal);
|
|
3099
4088
|
return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
|
|
3100
4089
|
}
|
|
3101
4090
|
const sessions = await listSessionPage(port, client, {
|
|
3102
|
-
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
|
|
4091
|
+
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS,
|
|
4092
|
+
signal
|
|
3103
4093
|
});
|
|
3104
4094
|
if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
|
|
3105
4095
|
if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
|
|
@@ -3900,8 +4890,8 @@ function classifyReplyAuthError(reply) {
|
|
|
3900
4890
|
};
|
|
3901
4891
|
}
|
|
3902
4892
|
}
|
|
3903
|
-
const
|
|
3904
|
-
if (
|
|
4893
|
+
const errorMessage4 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
|
|
4894
|
+
if (errorMessage4 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage4.includes(knownMessage))) {
|
|
3905
4895
|
return {
|
|
3906
4896
|
kind: "model_auth",
|
|
3907
4897
|
providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
|
|
@@ -4172,14 +5162,15 @@ async function probeOpenCodeRequestReadiness(port, client) {
|
|
|
4172
5162
|
}
|
|
4173
5163
|
|
|
4174
5164
|
// src/lib/opencode/session-v2.ts
|
|
4175
|
-
|
|
5165
|
+
var ALL_HTTP_STATUSES2 = Array.from({ length: 500 }, (_, index) => index + 100);
|
|
5166
|
+
function isRecord3(value) {
|
|
4176
5167
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4177
5168
|
}
|
|
4178
5169
|
function finiteNumber2(value) {
|
|
4179
5170
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
4180
5171
|
}
|
|
4181
5172
|
function adaptTime(value) {
|
|
4182
|
-
if (!
|
|
5173
|
+
if (!isRecord3(value)) return void 0;
|
|
4183
5174
|
const created = finiteNumber2(value.created);
|
|
4184
5175
|
const completed = finiteNumber2(value.completed);
|
|
4185
5176
|
if (created === void 0 && completed === void 0) return void 0;
|
|
@@ -4189,11 +5180,11 @@ function adaptTime(value) {
|
|
|
4189
5180
|
};
|
|
4190
5181
|
}
|
|
4191
5182
|
function adaptTokens(value) {
|
|
4192
|
-
if (!
|
|
5183
|
+
if (!isRecord3(value)) return void 0;
|
|
4193
5184
|
const input = finiteNumber2(value.input);
|
|
4194
5185
|
const output = finiteNumber2(value.output);
|
|
4195
5186
|
const reasoning = finiteNumber2(value.reasoning);
|
|
4196
|
-
const cache =
|
|
5187
|
+
const cache = isRecord3(value.cache) ? {
|
|
4197
5188
|
...finiteNumber2(value.cache.read) !== void 0 ? { read: finiteNumber2(value.cache.read) } : {},
|
|
4198
5189
|
...finiteNumber2(value.cache.write) !== void 0 ? { write: finiteNumber2(value.cache.write) } : {}
|
|
4199
5190
|
} : void 0;
|
|
@@ -4217,7 +5208,7 @@ function adaptMessageInfo(value, role) {
|
|
|
4217
5208
|
if (typeof value.finish === "string") info.finish = value.finish;
|
|
4218
5209
|
if ("error" in value) info.error = value.error;
|
|
4219
5210
|
if (typeof value.agent === "string") info.agent = value.agent;
|
|
4220
|
-
if (
|
|
5211
|
+
if (isRecord3(value.model)) {
|
|
4221
5212
|
if (typeof value.model.id === "string") info.modelID = value.model.id;
|
|
4222
5213
|
if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
|
|
4223
5214
|
}
|
|
@@ -4227,7 +5218,7 @@ function adaptMessageInfo(value, role) {
|
|
|
4227
5218
|
return info;
|
|
4228
5219
|
}
|
|
4229
5220
|
function adaptV2Message(value) {
|
|
4230
|
-
if (!
|
|
5221
|
+
if (!isRecord3(value) || typeof value.id !== "string" || typeof value.type !== "string") {
|
|
4231
5222
|
return null;
|
|
4232
5223
|
}
|
|
4233
5224
|
if (value.type === "user") {
|
|
@@ -4240,7 +5231,7 @@ function adaptV2Message(value) {
|
|
|
4240
5231
|
if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
|
|
4241
5232
|
const parts = [];
|
|
4242
5233
|
for (const content of value.content) {
|
|
4243
|
-
if (!
|
|
5234
|
+
if (!isRecord3(content) || typeof content.type !== "string") return null;
|
|
4244
5235
|
if (content.type === "text") {
|
|
4245
5236
|
if (typeof content.text !== "string") return null;
|
|
4246
5237
|
parts.push({ type: "text", text: content.text });
|
|
@@ -4254,23 +5245,23 @@ function adaptV2Message(value) {
|
|
|
4254
5245
|
};
|
|
4255
5246
|
}
|
|
4256
5247
|
function adaptFormTool(value) {
|
|
4257
|
-
if (!
|
|
5248
|
+
if (!isRecord3(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
|
|
4258
5249
|
return void 0;
|
|
4259
5250
|
}
|
|
4260
5251
|
return { messageID: value.messageID, callID: value.id };
|
|
4261
5252
|
}
|
|
4262
5253
|
function adaptV2FormWire(value) {
|
|
4263
|
-
if (!
|
|
5254
|
+
if (!isRecord3(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
4264
5255
|
return null;
|
|
4265
5256
|
}
|
|
4266
5257
|
return value;
|
|
4267
5258
|
}
|
|
4268
5259
|
function adaptV2FormField(value, header) {
|
|
4269
|
-
if (!
|
|
5260
|
+
if (!isRecord3(value)) return null;
|
|
4270
5261
|
const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
|
|
4271
5262
|
if (!question) return null;
|
|
4272
5263
|
const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
|
|
4273
|
-
if (!
|
|
5264
|
+
if (!isRecord3(option)) return [];
|
|
4274
5265
|
const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
|
|
4275
5266
|
if (!label) return [];
|
|
4276
5267
|
return [
|
|
@@ -4288,7 +5279,7 @@ function adaptV2Form(value) {
|
|
|
4288
5279
|
const header = typeof form.title === "string" ? form.title : "";
|
|
4289
5280
|
const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
|
|
4290
5281
|
if (questions.length === 0) return null;
|
|
4291
|
-
const tool =
|
|
5282
|
+
const tool = isRecord3(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
|
|
4292
5283
|
return {
|
|
4293
5284
|
id: form.id,
|
|
4294
5285
|
sessionID: form.sessionID,
|
|
@@ -4298,7 +5289,7 @@ function adaptV2Form(value) {
|
|
|
4298
5289
|
};
|
|
4299
5290
|
}
|
|
4300
5291
|
function adaptV2FormList(value) {
|
|
4301
|
-
if (!
|
|
5292
|
+
if (!isRecord3(value) || !Array.isArray(value.data)) return null;
|
|
4302
5293
|
return value.data.map(adaptV2Form).filter((form) => form !== null);
|
|
4303
5294
|
}
|
|
4304
5295
|
function adaptPattern(value) {
|
|
@@ -4309,7 +5300,7 @@ function adaptPattern(value) {
|
|
|
4309
5300
|
return void 0;
|
|
4310
5301
|
}
|
|
4311
5302
|
function adaptV2PermissionWire(value) {
|
|
4312
|
-
if (!
|
|
5303
|
+
if (!isRecord3(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
4313
5304
|
return null;
|
|
4314
5305
|
}
|
|
4315
5306
|
if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
|
|
@@ -4321,12 +5312,12 @@ function adaptV2Permission(value) {
|
|
|
4321
5312
|
const type = permission.permission ?? permission.action;
|
|
4322
5313
|
if (!type) return null;
|
|
4323
5314
|
const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
|
|
4324
|
-
const time =
|
|
5315
|
+
const time = isRecord3(permission.time) ? finiteNumber2(permission.time.created) !== void 0 ? { created: finiteNumber2(permission.time.created) } : void 0 : void 0;
|
|
4325
5316
|
return {
|
|
4326
5317
|
id: permission.id,
|
|
4327
5318
|
type,
|
|
4328
5319
|
sessionID: permission.sessionID,
|
|
4329
|
-
metadata:
|
|
5320
|
+
metadata: isRecord3(permission.metadata) ? permission.metadata : {},
|
|
4330
5321
|
raw: permission,
|
|
4331
5322
|
...pattern !== void 0 ? { pattern } : {},
|
|
4332
5323
|
...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
|
|
@@ -4336,12 +5327,12 @@ function adaptV2Permission(value) {
|
|
|
4336
5327
|
};
|
|
4337
5328
|
}
|
|
4338
5329
|
function adaptV2PermissionList(value) {
|
|
4339
|
-
if (!
|
|
5330
|
+
if (!isRecord3(value) || !Array.isArray(value.data)) return null;
|
|
4340
5331
|
return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
|
|
4341
5332
|
}
|
|
4342
5333
|
function adaptV2Session(value) {
|
|
4343
|
-
if (!
|
|
4344
|
-
const time =
|
|
5334
|
+
if (!isRecord3(value) || typeof value.id !== "string" || value.id.length === 0) return null;
|
|
5335
|
+
const time = isRecord3(value.time) ? {
|
|
4345
5336
|
...finiteNumber2(value.time.created) !== void 0 ? { created: finiteNumber2(value.time.created) } : {},
|
|
4346
5337
|
...finiteNumber2(value.time.updated) !== void 0 ? { updated: finiteNumber2(value.time.updated) } : {}
|
|
4347
5338
|
} : void 0;
|
|
@@ -4353,7 +5344,7 @@ function adaptV2Session(value) {
|
|
|
4353
5344
|
};
|
|
4354
5345
|
}
|
|
4355
5346
|
function adaptV2SessionList(value) {
|
|
4356
|
-
if (!
|
|
5347
|
+
if (!isRecord3(value) || !Array.isArray(value.data) || !isRecord3(value.cursor)) return null;
|
|
4357
5348
|
return {
|
|
4358
5349
|
data: value.data.map(adaptV2Session).filter((session) => session !== null),
|
|
4359
5350
|
cursor: value.cursor
|
|
@@ -4362,18 +5353,18 @@ function adaptV2SessionList(value) {
|
|
|
4362
5353
|
function adaptV2Location(value) {
|
|
4363
5354
|
const candidates = [
|
|
4364
5355
|
value,
|
|
4365
|
-
|
|
4366
|
-
|
|
5356
|
+
isRecord3(value) ? value.data : void 0,
|
|
5357
|
+
isRecord3(value) ? value.location : void 0
|
|
4367
5358
|
];
|
|
4368
5359
|
for (const candidate of candidates) {
|
|
4369
|
-
if (!
|
|
5360
|
+
if (!isRecord3(candidate) || typeof candidate.directory !== "string") continue;
|
|
4370
5361
|
const directory = candidate.directory.trim();
|
|
4371
5362
|
if (directory) return directory;
|
|
4372
5363
|
}
|
|
4373
5364
|
return null;
|
|
4374
5365
|
}
|
|
4375
5366
|
function adaptV2Messages(value) {
|
|
4376
|
-
if (!
|
|
5367
|
+
if (!isRecord3(value) || !Array.isArray(value.data)) return [];
|
|
4377
5368
|
return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
|
|
4378
5369
|
}
|
|
4379
5370
|
async function readJson(response) {
|
|
@@ -4388,7 +5379,7 @@ async function readJson(response) {
|
|
|
4388
5379
|
async function readData(client, path, init) {
|
|
4389
5380
|
const response = await client.request(path, init);
|
|
4390
5381
|
const body = await readJson(response);
|
|
4391
|
-
if (!
|
|
5382
|
+
if (!isRecord3(body) || !("data" in body)) {
|
|
4392
5383
|
throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
|
|
4393
5384
|
}
|
|
4394
5385
|
return body.data;
|
|
@@ -4415,7 +5406,7 @@ async function createV2Session(client, directory) {
|
|
|
4415
5406
|
headers: { "Content-Type": "application/json" },
|
|
4416
5407
|
body: JSON.stringify({ location: { directory } })
|
|
4417
5408
|
});
|
|
4418
|
-
if (!
|
|
5409
|
+
if (!isRecord3(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
4419
5410
|
throw new Error("OpenCode V2 create session response was missing data.id");
|
|
4420
5411
|
}
|
|
4421
5412
|
return data.id;
|
|
@@ -4426,25 +5417,35 @@ async function getV2Session(client, sessionId) {
|
|
|
4426
5417
|
if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
|
|
4427
5418
|
return session;
|
|
4428
5419
|
}
|
|
4429
|
-
async function listV2SessionPage(client, cursor) {
|
|
5420
|
+
async function listV2SessionPage(client, cursor, signal) {
|
|
5421
|
+
const operation = cursor ? "GET /api/session (next page)" : "GET /api/session";
|
|
4430
5422
|
const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
|
|
4431
5423
|
try {
|
|
4432
|
-
|
|
5424
|
+
const response = await client.request(path, signal ? { signal } : void 0, {
|
|
5425
|
+
allowStatuses: ALL_HTTP_STATUSES2
|
|
5426
|
+
});
|
|
5427
|
+
if (!response.ok) {
|
|
5428
|
+
console.error(
|
|
5429
|
+
`[listV2SessionPage] ${operation} returned HTTP ${response.status} (port ${client.port})`
|
|
5430
|
+
);
|
|
5431
|
+
return null;
|
|
5432
|
+
}
|
|
5433
|
+
return adaptV2SessionList(await readJson(response));
|
|
4433
5434
|
} catch (error2) {
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
);
|
|
5435
|
+
if (signal?.aborted) return null;
|
|
5436
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
5437
|
+
console.error(`[listV2SessionPage] ${operation} failed (port ${client.port}): ${errorName}`);
|
|
4437
5438
|
return null;
|
|
4438
5439
|
}
|
|
4439
5440
|
}
|
|
4440
|
-
async function listV2Sessions(client) {
|
|
5441
|
+
async function listV2Sessions(client, signal) {
|
|
4441
5442
|
const sessions = [];
|
|
4442
5443
|
const seenCursors = /* @__PURE__ */ new Set();
|
|
4443
5444
|
let cursor;
|
|
4444
5445
|
let hasNextPage = true;
|
|
4445
5446
|
try {
|
|
4446
5447
|
while (hasNextPage) {
|
|
4447
|
-
const page = await listV2SessionPage(client, cursor);
|
|
5448
|
+
const page = await listV2SessionPage(client, cursor, signal);
|
|
4448
5449
|
if (!page) return null;
|
|
4449
5450
|
sessions.push(...page.data);
|
|
4450
5451
|
const next = page.cursor.next;
|
|
@@ -4460,8 +5461,9 @@ async function listV2Sessions(client) {
|
|
|
4460
5461
|
}
|
|
4461
5462
|
return sessions;
|
|
4462
5463
|
} catch (error2) {
|
|
5464
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
4463
5465
|
console.error(
|
|
4464
|
-
`[listV2Sessions] session pagination failed
|
|
5466
|
+
`[listV2Sessions] GET /api/session pagination failed (port ${client.port}): ${errorName}`
|
|
4465
5467
|
);
|
|
4466
5468
|
return null;
|
|
4467
5469
|
}
|
|
@@ -4471,7 +5473,7 @@ async function deleteV2Session(client, sessionId) {
|
|
|
4471
5473
|
const response = await client.request(
|
|
4472
5474
|
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
4473
5475
|
{ method: "DELETE" },
|
|
4474
|
-
{ allowStatuses:
|
|
5476
|
+
{ allowStatuses: ALL_HTTP_STATUSES2 }
|
|
4475
5477
|
);
|
|
4476
5478
|
if (response.status >= 200 && response.status < 300) {
|
|
4477
5479
|
return { ok: true, status: response.status };
|
|
@@ -4522,8 +5524,8 @@ async function sendV2Prompt(client, sessionId, text) {
|
|
|
4522
5524
|
`OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4523
5525
|
);
|
|
4524
5526
|
}
|
|
4525
|
-
const data =
|
|
4526
|
-
if (!
|
|
5527
|
+
const data = isRecord3(body) && "data" in body ? body.data : void 0;
|
|
5528
|
+
if (!isRecord3(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
4527
5529
|
throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
|
|
4528
5530
|
}
|
|
4529
5531
|
return data.id;
|
|
@@ -4532,7 +5534,7 @@ async function getV2SessionMessages(client, sessionId) {
|
|
|
4532
5534
|
const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
|
|
4533
5535
|
try {
|
|
4534
5536
|
const body = await readJson(await client.request(path));
|
|
4535
|
-
if (!
|
|
5537
|
+
if (!isRecord3(body) || !Array.isArray(body.data) || !isRecord3(body.cursor)) return null;
|
|
4536
5538
|
return adaptV2Messages(body);
|
|
4537
5539
|
} catch (error2) {
|
|
4538
5540
|
console.error(
|
|
@@ -4571,7 +5573,7 @@ async function interruptV2Session(client, sessionId) {
|
|
|
4571
5573
|
async function getV2ActiveSessions(client) {
|
|
4572
5574
|
try {
|
|
4573
5575
|
const body = await readJson(await client.request("/api/session/active"));
|
|
4574
|
-
if (!
|
|
5576
|
+
if (!isRecord3(body) || !isRecord3(body.data)) return null;
|
|
4575
5577
|
return body.data;
|
|
4576
5578
|
} catch (error2) {
|
|
4577
5579
|
console.error(
|
|
@@ -4587,8 +5589,8 @@ async function isV2SessionOngoing(client, sessionId) {
|
|
|
4587
5589
|
}
|
|
4588
5590
|
function sessionErrorReason2(value) {
|
|
4589
5591
|
if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
|
|
4590
|
-
if (
|
|
4591
|
-
const data =
|
|
5592
|
+
if (isRecord3(value)) {
|
|
5593
|
+
const data = isRecord3(value.data) ? value.data : void 0;
|
|
4592
5594
|
const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
|
|
4593
5595
|
if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
4594
5596
|
}
|
|
@@ -4604,17 +5606,17 @@ function adaptV2SessionErrorEvent(value) {
|
|
|
4604
5606
|
return null;
|
|
4605
5607
|
}
|
|
4606
5608
|
}
|
|
4607
|
-
if (!
|
|
5609
|
+
if (!isRecord3(parsed)) return null;
|
|
4608
5610
|
try {
|
|
4609
5611
|
const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
|
|
4610
5612
|
if (establishedShape) return establishedShape;
|
|
4611
5613
|
} catch (error2) {
|
|
4612
5614
|
void error2;
|
|
4613
5615
|
}
|
|
4614
|
-
const candidates = [parsed, parsed.payload, parsed.data].filter(
|
|
5616
|
+
const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord3);
|
|
4615
5617
|
for (const event of candidates) {
|
|
4616
5618
|
if (event.type !== "session.error") continue;
|
|
4617
|
-
const properties = [event.properties, event.data, event].find(
|
|
5619
|
+
const properties = [event.properties, event.data, event].find(isRecord3);
|
|
4618
5620
|
if (!properties) continue;
|
|
4619
5621
|
const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
|
|
4620
5622
|
if (!sessionId) continue;
|
|
@@ -4664,13 +5666,13 @@ async function readV2SessionErrorStream(client, options) {
|
|
|
4664
5666
|
}
|
|
4665
5667
|
|
|
4666
5668
|
// src/lib/opencode/credential-import-v2.ts
|
|
4667
|
-
import { readFileSync as
|
|
4668
|
-
import { join as
|
|
5669
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
5670
|
+
import { join as join8 } from "node:path";
|
|
4669
5671
|
|
|
4670
5672
|
// src/lib/openai-usage.ts
|
|
4671
|
-
import { readFileSync as
|
|
5673
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4672
5674
|
import { homedir as homedir3 } from "node:os";
|
|
4673
|
-
import { join as
|
|
5675
|
+
import { join as join7 } from "node:path";
|
|
4674
5676
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
4675
5677
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
4676
5678
|
var OpenAiUsageError = class extends Error {
|
|
@@ -4684,7 +5686,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
4684
5686
|
}
|
|
4685
5687
|
function readOpenCodeChatGptCredentials() {
|
|
4686
5688
|
try {
|
|
4687
|
-
const raw =
|
|
5689
|
+
const raw = readFileSync7(join7(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
4688
5690
|
let parsed;
|
|
4689
5691
|
try {
|
|
4690
5692
|
parsed = JSON.parse(raw);
|
|
@@ -4750,7 +5752,7 @@ function parseCodexUsageHeaders(headers) {
|
|
|
4750
5752
|
function normalizeProbeModel(model) {
|
|
4751
5753
|
return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
|
|
4752
5754
|
}
|
|
4753
|
-
function
|
|
5755
|
+
function isRecord4(value) {
|
|
4754
5756
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4755
5757
|
}
|
|
4756
5758
|
function unsupportedProbeModels(reason, port) {
|
|
@@ -4761,14 +5763,14 @@ async function resolveV1ProbeModels(client, port) {
|
|
|
4761
5763
|
try {
|
|
4762
5764
|
const response = await client.request("/config/providers");
|
|
4763
5765
|
const body = await response.json();
|
|
4764
|
-
if (!
|
|
5766
|
+
if (!isRecord4(body) || !Array.isArray(body.providers)) {
|
|
4765
5767
|
return unsupportedProbeModels("V1 provider response did not contain a providers array", port);
|
|
4766
5768
|
}
|
|
4767
5769
|
const provider = body.providers.find(
|
|
4768
|
-
(candidate) =>
|
|
5770
|
+
(candidate) => isRecord4(candidate) && candidate.id === "openai"
|
|
4769
5771
|
);
|
|
4770
|
-
if (!provider || !
|
|
4771
|
-
const defaults2 =
|
|
5772
|
+
if (!provider || !isRecord4(provider.models)) return { status: "supported", models: [] };
|
|
5773
|
+
const defaults2 = isRecord4(body.default) ? body.default : void 0;
|
|
4772
5774
|
const candidates = [
|
|
4773
5775
|
...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
|
|
4774
5776
|
...Object.keys(provider.models)
|
|
@@ -4790,14 +5792,14 @@ async function resolveV2ProbeModels(client, port) {
|
|
|
4790
5792
|
try {
|
|
4791
5793
|
const response = await client.request(path);
|
|
4792
5794
|
const body = await response.json();
|
|
4793
|
-
if (!
|
|
5795
|
+
if (!isRecord4(body) || !Array.isArray(body.data)) {
|
|
4794
5796
|
return unsupportedProbeModels(`V2 GET ${path} did not contain a provider data array`, port);
|
|
4795
5797
|
}
|
|
4796
5798
|
const provider = body.data.find(
|
|
4797
|
-
(candidate) =>
|
|
5799
|
+
(candidate) => isRecord4(candidate) && candidate.id === "openai"
|
|
4798
5800
|
);
|
|
4799
5801
|
if (!provider) return { status: "supported", models: [] };
|
|
4800
|
-
if (!
|
|
5802
|
+
if (!isRecord4(provider.models)) {
|
|
4801
5803
|
return unsupportedProbeModels(
|
|
4802
5804
|
"V2 provider response has no safe OpenAI model catalogue",
|
|
4803
5805
|
port
|
|
@@ -4907,7 +5909,7 @@ var CONNECT_KEY_MAX_ATTEMPTS = 3;
|
|
|
4907
5909
|
var CONNECT_KEY_RETRY_DELAY_MS = 250;
|
|
4908
5910
|
var INTEGRATION_LIST_MAX_READS = 10;
|
|
4909
5911
|
var INTEGRATION_LIST_RETRY_DELAY_MS = 1e3;
|
|
4910
|
-
function
|
|
5912
|
+
function isRecord5(value) {
|
|
4911
5913
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4912
5914
|
}
|
|
4913
5915
|
var sleep2 = (milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds));
|
|
@@ -4916,14 +5918,14 @@ function emptyResult(unsupported = [], outcome = "completed") {
|
|
|
4916
5918
|
}
|
|
4917
5919
|
function readOpenCodeCredentials(homeDir, log3) {
|
|
4918
5920
|
try {
|
|
4919
|
-
const raw =
|
|
5921
|
+
const raw = readFileSync8(join8(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
|
|
4920
5922
|
let parsed;
|
|
4921
5923
|
try {
|
|
4922
5924
|
parsed = JSON.parse(raw);
|
|
4923
5925
|
} catch (error2) {
|
|
4924
5926
|
return null;
|
|
4925
5927
|
}
|
|
4926
|
-
return
|
|
5928
|
+
return isRecord5(parsed) ? parsed : null;
|
|
4927
5929
|
} catch (error2) {
|
|
4928
5930
|
const code = error2.code;
|
|
4929
5931
|
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
@@ -4936,7 +5938,7 @@ function classifyOpenCodeCredentials(credentials2) {
|
|
|
4936
5938
|
const importable = [];
|
|
4937
5939
|
const unsupported = [];
|
|
4938
5940
|
for (const [integrationId, value] of Object.entries(credentials2)) {
|
|
4939
|
-
if (!
|
|
5941
|
+
if (!isRecord5(value) || typeof value.type !== "string") {
|
|
4940
5942
|
unsupported.push({ integrationId, reason: "unrecognised credential entry" });
|
|
4941
5943
|
continue;
|
|
4942
5944
|
}
|
|
@@ -4951,7 +5953,7 @@ function classifyOpenCodeCredentials(credentials2) {
|
|
|
4951
5953
|
unsupported.push({ integrationId, reason: "unrecognised credential entry" });
|
|
4952
5954
|
continue;
|
|
4953
5955
|
}
|
|
4954
|
-
if (value.metadata === void 0 ||
|
|
5956
|
+
if (value.metadata === void 0 || isRecord5(value.metadata) && Object.keys(value.metadata).length === 0) {
|
|
4955
5957
|
importable.push({ integrationId, key: value.key });
|
|
4956
5958
|
} else {
|
|
4957
5959
|
unsupported.push({ integrationId, reason: "needs the interactive connect flow" });
|
|
@@ -4964,7 +5966,7 @@ function statusFromRequestError(error2) {
|
|
|
4964
5966
|
const match = /^OpenCode request failed: HTTP (\d{3})/.exec(error2.message);
|
|
4965
5967
|
return match ? Number(match[1]) : void 0;
|
|
4966
5968
|
}
|
|
4967
|
-
function
|
|
5969
|
+
function failureMessage2(status2) {
|
|
4968
5970
|
return status2 === void 0 ? "OpenCode credential import request failed" : `OpenCode credential import request failed with HTTP ${status2}`;
|
|
4969
5971
|
}
|
|
4970
5972
|
function isRetryableStatus(status2) {
|
|
@@ -4991,18 +5993,18 @@ async function importOneCredential(request3, credential, sleepForRetry, log3) {
|
|
|
4991
5993
|
const failure = {
|
|
4992
5994
|
integrationId: credential.integrationId,
|
|
4993
5995
|
status: response.status,
|
|
4994
|
-
message:
|
|
5996
|
+
message: failureMessage2(response.status)
|
|
4995
5997
|
};
|
|
4996
|
-
log3(`${
|
|
5998
|
+
log3(`${failureMessage2(failure.status)} for ${failure.integrationId}`, "warn");
|
|
4997
5999
|
return { ok: false, failure };
|
|
4998
6000
|
} catch (error2) {
|
|
4999
6001
|
const status2 = statusFromRequestError(error2);
|
|
5000
6002
|
const failure = {
|
|
5001
6003
|
integrationId: credential.integrationId,
|
|
5002
6004
|
...status2 === void 0 ? {} : { status: status2 },
|
|
5003
|
-
message:
|
|
6005
|
+
message: failureMessage2(status2)
|
|
5004
6006
|
};
|
|
5005
|
-
log3(`${
|
|
6007
|
+
log3(`${failureMessage2(status2)} for ${failure.integrationId}`, "warn");
|
|
5006
6008
|
return { ok: false, failure };
|
|
5007
6009
|
}
|
|
5008
6010
|
}
|
|
@@ -5028,10 +6030,10 @@ async function importOpenCodeCredentials({
|
|
|
5028
6030
|
return result;
|
|
5029
6031
|
}
|
|
5030
6032
|
function parseIntegrationList(value) {
|
|
5031
|
-
if (!
|
|
6033
|
+
if (!isRecord5(value) || !Array.isArray(value.data)) return null;
|
|
5032
6034
|
const integrationIds = /* @__PURE__ */ new Set();
|
|
5033
6035
|
for (const integration of value.data) {
|
|
5034
|
-
if (!
|
|
6036
|
+
if (!isRecord5(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
5035
6037
|
return null;
|
|
5036
6038
|
}
|
|
5037
6039
|
if (integration.connections.length > 0) integrationIds.add(integration.id);
|
|
@@ -5116,87 +6118,9 @@ async function importOpenCodeV2Credentials({
|
|
|
5116
6118
|
|
|
5117
6119
|
// src/lib/opencode/session-db-boot.ts
|
|
5118
6120
|
import { spawn as spawn2 } from "node:child_process";
|
|
5119
|
-
import { mkdirSync as
|
|
6121
|
+
import { mkdirSync as mkdirSync4, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5120
6122
|
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
|
|
6123
|
+
import { dirname as dirname7, resolve as resolvePath } from "node:path";
|
|
5200
6124
|
var SESSION_DB_RESTORE_TIMEOUT_MS = 3e5;
|
|
5201
6125
|
var SESSION_DB_VERIFY_TIMEOUT_MS = 36e4;
|
|
5202
6126
|
var SESSION_DB_SYNCHRONISER_TIMEOUT_MS = 12e4;
|
|
@@ -5246,8 +6170,8 @@ function clearMarker(options) {
|
|
|
5246
6170
|
function markNoReplicate(options, message) {
|
|
5247
6171
|
if (options.noReplicateMarker) {
|
|
5248
6172
|
try {
|
|
5249
|
-
|
|
5250
|
-
|
|
6173
|
+
mkdirSync4(dirname7(options.noReplicateMarker), { recursive: true });
|
|
6174
|
+
writeFileSync4(options.noReplicateMarker, "");
|
|
5251
6175
|
} catch (error2) {
|
|
5252
6176
|
options.log(
|
|
5253
6177
|
`Could not write session-DB no-replicate marker ${options.noReplicateMarker}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
@@ -5401,8 +6325,8 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
5401
6325
|
return null;
|
|
5402
6326
|
}
|
|
5403
6327
|
try {
|
|
5404
|
-
|
|
5405
|
-
|
|
6328
|
+
mkdirSync4(dirname7(configPath), { recursive: true });
|
|
6329
|
+
writeFileSync4(configPath, rendered.stdout);
|
|
5406
6330
|
} catch (error2) {
|
|
5407
6331
|
options.log(
|
|
5408
6332
|
`Could not write ${configPath}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
@@ -5667,8 +6591,8 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
5667
6591
|
|
|
5668
6592
|
// src/lib/opencode/session-db-provenance.ts
|
|
5669
6593
|
import { createRequire } from "node:module";
|
|
5670
|
-
import { mkdirSync as
|
|
5671
|
-
import { dirname as
|
|
6594
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6595
|
+
import { dirname as dirname8, join as join9 } from "node:path";
|
|
5672
6596
|
var require2 = createRequire(import.meta.url);
|
|
5673
6597
|
function readSessionDbMigrationIds(dbPath) {
|
|
5674
6598
|
let db;
|
|
@@ -5705,12 +6629,12 @@ function readSessionDbMigrationIds(dbPath) {
|
|
|
5705
6629
|
}
|
|
5706
6630
|
function sessionDbProvenanceStatePath(homeDir, env) {
|
|
5707
6631
|
const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
|
|
5708
|
-
return override ||
|
|
6632
|
+
return override || join9(homeDir, ".local", "state", "evident", "session-db-provenance.json");
|
|
5709
6633
|
}
|
|
5710
6634
|
function loadSessionDbProvenanceState(path) {
|
|
5711
6635
|
let value;
|
|
5712
6636
|
try {
|
|
5713
|
-
value = JSON.parse(
|
|
6637
|
+
value = JSON.parse(readFileSync9(path, "utf8"));
|
|
5714
6638
|
} catch (error2) {
|
|
5715
6639
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return {};
|
|
5716
6640
|
console.error(
|
|
@@ -5734,8 +6658,8 @@ function loadSessionDbProvenanceState(path) {
|
|
|
5734
6658
|
}
|
|
5735
6659
|
function saveSessionDbProvenanceState(path, state) {
|
|
5736
6660
|
try {
|
|
5737
|
-
|
|
5738
|
-
|
|
6661
|
+
mkdirSync5(dirname8(path), { recursive: true });
|
|
6662
|
+
writeFileSync5(path, `${JSON.stringify(state, null, 2)}
|
|
5739
6663
|
`, "utf8");
|
|
5740
6664
|
} catch (error2) {
|
|
5741
6665
|
console.error(
|
|
@@ -5790,6 +6714,26 @@ function isSessionDbProvenanceRecord(value) {
|
|
|
5790
6714
|
return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
|
|
5791
6715
|
}
|
|
5792
6716
|
|
|
6717
|
+
// src/lib/opencode/detect.ts
|
|
6718
|
+
function describeProbe(route, result) {
|
|
6719
|
+
if (result.error?.startsWith("HTTP ")) return `${route}: ${result.error}`;
|
|
6720
|
+
if (result.error) return `${route}: no HTTP response (${result.error})`;
|
|
6721
|
+
return `${route}: ${result.healthy ? "healthy" : "no response detail"}`;
|
|
6722
|
+
}
|
|
6723
|
+
async function classifyRunningOpenCode(port) {
|
|
6724
|
+
const v1 = await checkOpenCodeHealth(port);
|
|
6725
|
+
if (v1.healthy) return { kind: "v1", version: v1.version ?? null };
|
|
6726
|
+
const v2 = await checkOpenCode2Health(port);
|
|
6727
|
+
if (v2.healthy) return { kind: "v2", version: v2.version ?? null };
|
|
6728
|
+
const detail = [
|
|
6729
|
+
describeProbe("V1 /global/health", v1),
|
|
6730
|
+
describeProbe("V2 /api/info (or /api/health fallback)", v2)
|
|
6731
|
+
].join("; ");
|
|
6732
|
+
if (v1.okWithNonJsonBody && v2.authFailed) return { kind: "v2", version: null };
|
|
6733
|
+
if (v1.authFailed && v2.authFailed) return { kind: "authenticated-unknown", detail };
|
|
6734
|
+
return { kind: "no-server-identified", detail };
|
|
6735
|
+
}
|
|
6736
|
+
|
|
5793
6737
|
// src/lib/opencode/opencode2-package.ts
|
|
5794
6738
|
var OPENCODE2_PACKAGE_SPEC = "@opencode-ai/cli@0.0.0-beta-17823";
|
|
5795
6739
|
|
|
@@ -6098,26 +7042,28 @@ async function startOpenCode(port, options = {}) {
|
|
|
6098
7042
|
}
|
|
6099
7043
|
async function startOpenCode2(port, options = {}) {
|
|
6100
7044
|
const password = randomBytes(24).toString("hex");
|
|
6101
|
-
let command = "opencode2";
|
|
7045
|
+
let command = options.command ?? "opencode2";
|
|
6102
7046
|
const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
|
|
6103
7047
|
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
|
-
|
|
7048
|
+
if (options.command === void 0) {
|
|
7049
|
+
try {
|
|
7050
|
+
execSync("which opencode2", { stdio: "ignore" });
|
|
7051
|
+
} catch {
|
|
7052
|
+
command = "npx";
|
|
7053
|
+
args = [
|
|
7054
|
+
"-y",
|
|
7055
|
+
"-p",
|
|
7056
|
+
OPENCODE2_PACKAGE_SPEC,
|
|
7057
|
+
"--",
|
|
7058
|
+
"opencode2",
|
|
7059
|
+
"serve",
|
|
7060
|
+
"--port",
|
|
7061
|
+
port.toString(),
|
|
7062
|
+
"--hostname",
|
|
7063
|
+
"127.0.0.1",
|
|
7064
|
+
...logLevel
|
|
7065
|
+
];
|
|
7066
|
+
}
|
|
6121
7067
|
}
|
|
6122
7068
|
const child = spawn3(command, args, {
|
|
6123
7069
|
env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
|
|
@@ -6174,6 +7120,56 @@ function isOpenCode2Installed() {
|
|
|
6174
7120
|
return false;
|
|
6175
7121
|
}
|
|
6176
7122
|
}
|
|
7123
|
+
function readVersionOutput(command) {
|
|
7124
|
+
try {
|
|
7125
|
+
return execSync2(`${command} --version`, {
|
|
7126
|
+
encoding: "utf-8",
|
|
7127
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
7128
|
+
}).trim();
|
|
7129
|
+
} catch (error2) {
|
|
7130
|
+
const output = typeof error2 === "object" && error2 !== null ? [
|
|
7131
|
+
"stdout" in error2 ? String(error2.stdout ?? "").trim() : "",
|
|
7132
|
+
"stderr" in error2 ? String(error2.stderr ?? "").trim() : ""
|
|
7133
|
+
].filter(Boolean).join("\n") : "";
|
|
7134
|
+
return output || (error2 instanceof Error ? error2.message : String(error2));
|
|
7135
|
+
}
|
|
7136
|
+
}
|
|
7137
|
+
function parseOpenCodeVersion(rawVersion) {
|
|
7138
|
+
const output = rawVersion.trim();
|
|
7139
|
+
const prefix = output.match(/^(opencode2|opencode)\s+/i)?.[1]?.toLowerCase();
|
|
7140
|
+
const version2 = (prefix ? output.slice(prefix.length).trim() : output).replace(/^v/i, "");
|
|
7141
|
+
if (prefix === "opencode2") return { major: "v2", version: version2 };
|
|
7142
|
+
const major = version2.match(/^(\d+)(?:\.\d+)*(?:[-+][0-9A-Za-z.-]+)?$/)?.[1];
|
|
7143
|
+
if (!major) return null;
|
|
7144
|
+
if (Number(major) === 1) return { major: "v1", version: version2 };
|
|
7145
|
+
if (Number(major) >= 2) return { major: "v2", version: version2 };
|
|
7146
|
+
return null;
|
|
7147
|
+
}
|
|
7148
|
+
function resolveInstalledOpenCode() {
|
|
7149
|
+
let opencode2Available = false;
|
|
7150
|
+
if (isOpenCodeInstalled()) {
|
|
7151
|
+
const rawVersion = readVersionOutput("opencode");
|
|
7152
|
+
const parsed = parseOpenCodeVersion(rawVersion);
|
|
7153
|
+
if (parsed?.major === "v1") {
|
|
7154
|
+
return { kind: "installed", major: "v1", command: "opencode", version: parsed.version };
|
|
7155
|
+
}
|
|
7156
|
+
if (parsed?.major === "v2") {
|
|
7157
|
+
return { kind: "installed", major: "v2", command: "opencode", version: parsed.version };
|
|
7158
|
+
}
|
|
7159
|
+
opencode2Available = isOpenCode2Installed();
|
|
7160
|
+
if (!opencode2Available) return { kind: "unknown", command: "opencode", rawVersion };
|
|
7161
|
+
}
|
|
7162
|
+
if (opencode2Available || isOpenCode2Installed()) {
|
|
7163
|
+
const parsed = parseOpenCodeVersion(readVersionOutput("opencode2"));
|
|
7164
|
+
return {
|
|
7165
|
+
kind: "installed",
|
|
7166
|
+
major: "v2",
|
|
7167
|
+
command: "opencode2",
|
|
7168
|
+
version: parsed?.version
|
|
7169
|
+
};
|
|
7170
|
+
}
|
|
7171
|
+
return { kind: "none" };
|
|
7172
|
+
}
|
|
6177
7173
|
async function promptOpenCodeInstall(interactive) {
|
|
6178
7174
|
if (!interactive) {
|
|
6179
7175
|
console.log(
|
|
@@ -6253,145 +7249,38 @@ async function promptOpenCodeInstall(interactive) {
|
|
|
6253
7249
|
} else {
|
|
6254
7250
|
console.log(chalk4.yellow("\nOpenCode still not detected in PATH."));
|
|
6255
7251
|
console.log(chalk4.dim("You may need to restart your terminal or add it to your PATH."));
|
|
6256
|
-
const proceed = await select({
|
|
6257
|
-
message: "Continue anyway?",
|
|
6258
|
-
choices: [
|
|
6259
|
-
{ name: "Yes, continue", value: "continue" },
|
|
6260
|
-
{ name: "No, exit", value: "exit" }
|
|
6261
|
-
]
|
|
6262
|
-
});
|
|
6263
|
-
return proceed === "continue" ? "continue" : "exit";
|
|
6264
|
-
}
|
|
6265
|
-
}
|
|
6266
|
-
return "exit";
|
|
6267
|
-
}
|
|
6268
|
-
return action;
|
|
6269
|
-
}
|
|
6270
|
-
|
|
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);
|
|
7252
|
+
const proceed = await select({
|
|
7253
|
+
message: "Continue anyway?",
|
|
7254
|
+
choices: [
|
|
7255
|
+
{ name: "Yes, continue", value: "continue" },
|
|
7256
|
+
{ name: "No, exit", value: "exit" }
|
|
7257
|
+
]
|
|
7258
|
+
});
|
|
7259
|
+
return proceed === "continue" ? "continue" : "exit";
|
|
7260
|
+
}
|
|
6320
7261
|
}
|
|
7262
|
+
return "exit";
|
|
6321
7263
|
}
|
|
6322
|
-
|
|
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;
|
|
7264
|
+
return action;
|
|
6341
7265
|
}
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
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 };
|
|
7266
|
+
|
|
7267
|
+
// src/lib/opencode/provider-check.ts
|
|
7268
|
+
function buildNoProviderWarning(hasProvider) {
|
|
7269
|
+
if (hasProvider !== false) return null;
|
|
7270
|
+
return "Warning: opencode has no authenticated model provider configured, so it won't be able to answer prompts. Run `opencode auth login` to set one up (see https://opencode.ai for details).";
|
|
6382
7271
|
}
|
|
6383
7272
|
|
|
6384
7273
|
// src/lib/opencode/session-db-size.ts
|
|
6385
7274
|
import { statSync as statSync4 } from "node:fs";
|
|
6386
|
-
import { join as
|
|
7275
|
+
import { join as join10 } from "node:path";
|
|
6387
7276
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
6388
7277
|
function statSessionDbBytes(homeDir) {
|
|
6389
|
-
const dbPath =
|
|
7278
|
+
const dbPath = join10(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
6390
7279
|
try {
|
|
6391
7280
|
return statSync4(dbPath).size;
|
|
6392
7281
|
} catch (err) {
|
|
6393
|
-
const
|
|
6394
|
-
if (!
|
|
7282
|
+
const isMissingFile4 = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
7283
|
+
if (!isMissingFile4) {
|
|
6395
7284
|
console.error(
|
|
6396
7285
|
`[statSessionDbBytes] could not stat ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
6397
7286
|
);
|
|
@@ -6416,12 +7305,12 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
6416
7305
|
// src/lib/opencode/log-tail.ts
|
|
6417
7306
|
import { statSync as statSync5 } from "node:fs";
|
|
6418
7307
|
import { homedir as homedir5 } from "node:os";
|
|
6419
|
-
import { join as
|
|
7308
|
+
import { join as join11 } from "node:path";
|
|
6420
7309
|
import { open as open2, stat } from "node:fs/promises";
|
|
6421
7310
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
6422
7311
|
function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
|
|
6423
|
-
const dataDir = env.XDG_DATA_HOME ||
|
|
6424
|
-
return
|
|
7312
|
+
const dataDir = env.XDG_DATA_HOME || join11(homeDir, ".local", "share");
|
|
7313
|
+
return join11(dataDir, "opencode", "log", "opencode.log");
|
|
6425
7314
|
}
|
|
6426
7315
|
function isEnoent(error2) {
|
|
6427
7316
|
return error2?.code === "ENOENT";
|
|
@@ -6906,8 +7795,8 @@ function connectTunnel(options) {
|
|
|
6906
7795
|
}
|
|
6907
7796
|
forwarder.handleBinaryBodyFrame(frame.sid, frame.payload);
|
|
6908
7797
|
} catch (error2) {
|
|
6909
|
-
const
|
|
6910
|
-
onError?.(`Failed to handle binary message: ${
|
|
7798
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
7799
|
+
onError?.(`Failed to handle binary message: ${errorMessage4}`);
|
|
6911
7800
|
}
|
|
6912
7801
|
return;
|
|
6913
7802
|
}
|
|
@@ -6915,8 +7804,8 @@ function connectTunnel(options) {
|
|
|
6915
7804
|
try {
|
|
6916
7805
|
message = JSON.parse(data.toString());
|
|
6917
7806
|
} catch (error2) {
|
|
6918
|
-
const
|
|
6919
|
-
onError?.(`Failed to handle message: ${
|
|
7807
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
7808
|
+
onError?.(`Failed to handle message: ${errorMessage4}`);
|
|
6920
7809
|
return;
|
|
6921
7810
|
}
|
|
6922
7811
|
if (isStreamFrame(message)) {
|
|
@@ -7088,10 +7977,10 @@ var RunnerConnection = class _RunnerConnection {
|
|
|
7088
7977
|
};
|
|
7089
7978
|
|
|
7090
7979
|
// src/lib/tunnel/ready-marker.ts
|
|
7091
|
-
import { writeFileSync as
|
|
7980
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
7092
7981
|
function writeTunnelReadyMarker(path, agentId) {
|
|
7093
7982
|
try {
|
|
7094
|
-
|
|
7983
|
+
writeFileSync6(path, `${agentId}
|
|
7095
7984
|
`);
|
|
7096
7985
|
return { ok: true };
|
|
7097
7986
|
} catch (error2) {
|
|
@@ -7116,7 +8005,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
7116
8005
|
}
|
|
7117
8006
|
|
|
7118
8007
|
// src/lib/process-liveness.ts
|
|
7119
|
-
import { readFileSync as
|
|
8008
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
7120
8009
|
function isProcessAlive(pid) {
|
|
7121
8010
|
try {
|
|
7122
8011
|
process.kill(pid, 0);
|
|
@@ -7131,7 +8020,7 @@ function isProcessAlive(pid) {
|
|
|
7131
8020
|
}
|
|
7132
8021
|
if (process.platform !== "linux") return true;
|
|
7133
8022
|
try {
|
|
7134
|
-
const status2 =
|
|
8023
|
+
const status2 = readFileSync10(`/proc/${pid}/status`, "utf8");
|
|
7135
8024
|
return !/^State:\s+Z(?:\s|$)/m.test(status2);
|
|
7136
8025
|
} catch (error2) {
|
|
7137
8026
|
console.error(
|
|
@@ -7198,8 +8087,8 @@ function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
|
7198
8087
|
}
|
|
7199
8088
|
|
|
7200
8089
|
// src/lib/resource-usage-reporting.ts
|
|
7201
|
-
var
|
|
7202
|
-
var
|
|
8090
|
+
var ENABLED_VALUES2 = /* @__PURE__ */ new Set(["on", "true", "1"]);
|
|
8091
|
+
var DISABLED_VALUES2 = /* @__PURE__ */ new Set(["off", "false", "0"]);
|
|
7203
8092
|
function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
7204
8093
|
if (flagValue === false) {
|
|
7205
8094
|
return { enabled: false, warnings: [] };
|
|
@@ -7209,10 +8098,10 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
|
7209
8098
|
return { enabled: true, warnings: [] };
|
|
7210
8099
|
}
|
|
7211
8100
|
const normalized = raw.trim().toLowerCase();
|
|
7212
|
-
if (
|
|
8101
|
+
if (DISABLED_VALUES2.has(normalized)) {
|
|
7213
8102
|
return { enabled: false, warnings: [] };
|
|
7214
8103
|
}
|
|
7215
|
-
if (
|
|
8104
|
+
if (ENABLED_VALUES2.has(normalized)) {
|
|
7216
8105
|
return { enabled: true, warnings: [] };
|
|
7217
8106
|
}
|
|
7218
8107
|
return {
|
|
@@ -7490,12 +8379,12 @@ function createSessionStoreHealth(options = {}) {
|
|
|
7490
8379
|
import { homedir as homedir6 } from "node:os";
|
|
7491
8380
|
|
|
7492
8381
|
// src/lib/runner-file-sync.ts
|
|
7493
|
-
import { join as
|
|
8382
|
+
import { join as join13 } from "node:path";
|
|
7494
8383
|
|
|
7495
8384
|
// src/lib/file-push.ts
|
|
7496
8385
|
import { randomUUID } from "node:crypto";
|
|
7497
8386
|
import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
|
|
7498
|
-
import { basename as basename2, dirname as
|
|
8387
|
+
import { basename as basename2, dirname as dirname9, isAbsolute as isAbsolute2, join as join12, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
|
7499
8388
|
var FILE_MODE = 384;
|
|
7500
8389
|
var DIRECTORY_MODE = 448;
|
|
7501
8390
|
async function writePushedFile(request3) {
|
|
@@ -7526,9 +8415,9 @@ async function writePushedFile(request3) {
|
|
|
7526
8415
|
}
|
|
7527
8416
|
try {
|
|
7528
8417
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
7529
|
-
|
|
8418
|
+
dirname9(candidate)
|
|
7530
8419
|
);
|
|
7531
|
-
const realTarget =
|
|
8420
|
+
const realTarget = join12(existingAncestor, ...missingSegments, basename2(candidate));
|
|
7532
8421
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget) ?? await findMatchingAllowedFile(allowedFiles, realTarget);
|
|
7533
8422
|
if (allowedDirectory === null) {
|
|
7534
8423
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -7538,8 +8427,8 @@ async function writePushedFile(request3) {
|
|
|
7538
8427
|
}
|
|
7539
8428
|
if (missingSegments.length > 0) {
|
|
7540
8429
|
await createMissingDirectories(existingAncestor, missingSegments);
|
|
7541
|
-
const realParent = await realpath(
|
|
7542
|
-
if (realParent !==
|
|
8430
|
+
const realParent = await realpath(dirname9(realTarget));
|
|
8431
|
+
if (realParent !== dirname9(realTarget) || !contains(allowedDirectory, realTarget)) {
|
|
7543
8432
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
7544
8433
|
path: realTarget,
|
|
7545
8434
|
bytes,
|
|
@@ -7564,7 +8453,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
7564
8453
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
7565
8454
|
return null;
|
|
7566
8455
|
}
|
|
7567
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
8456
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join12(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
7568
8457
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
7569
8458
|
return null;
|
|
7570
8459
|
}
|
|
@@ -7582,7 +8471,7 @@ async function resolveNearestExistingAncestor(directory) {
|
|
|
7582
8471
|
try {
|
|
7583
8472
|
return { existingAncestor: await realpath(current), missingSegments };
|
|
7584
8473
|
} catch (err) {
|
|
7585
|
-
const parent =
|
|
8474
|
+
const parent = dirname9(current);
|
|
7586
8475
|
if (err.code !== "ENOENT" || parent === current) {
|
|
7587
8476
|
throw err;
|
|
7588
8477
|
}
|
|
@@ -7613,8 +8502,8 @@ async function findMatchingAllowedFile(allowedFiles, realTarget) {
|
|
|
7613
8502
|
});
|
|
7614
8503
|
continue;
|
|
7615
8504
|
}
|
|
7616
|
-
const realParent = await realpathCreatingIfMissing(
|
|
7617
|
-
if (realParent !== null &&
|
|
8505
|
+
const realParent = await realpathCreatingIfMissing(dirname9(entry));
|
|
8506
|
+
if (realParent !== null && join12(realParent, basename2(entry)) === realTarget) {
|
|
7618
8507
|
return realParent;
|
|
7619
8508
|
}
|
|
7620
8509
|
}
|
|
@@ -7653,13 +8542,13 @@ function contains(realDirectory, realTarget) {
|
|
|
7653
8542
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
7654
8543
|
let current = existingAncestor;
|
|
7655
8544
|
for (const segment of missingSegments) {
|
|
7656
|
-
current =
|
|
8545
|
+
current = join12(current, segment);
|
|
7657
8546
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
7658
8547
|
await chmod(current, DIRECTORY_MODE);
|
|
7659
8548
|
}
|
|
7660
8549
|
}
|
|
7661
8550
|
async function writeAtomically(realTarget, content) {
|
|
7662
|
-
const temporaryPath =
|
|
8551
|
+
const temporaryPath = join12(dirname9(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
7663
8552
|
let handle;
|
|
7664
8553
|
try {
|
|
7665
8554
|
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
@@ -7789,12 +8678,12 @@ var NOT_APPLIED = {
|
|
|
7789
8678
|
opencodeAuthApplied: false
|
|
7790
8679
|
};
|
|
7791
8680
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
7792
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
7793
|
-
return expanded ===
|
|
8681
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
8682
|
+
return expanded === join13(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
7794
8683
|
}
|
|
7795
8684
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
7796
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
7797
|
-
return expanded ===
|
|
8685
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
8686
|
+
return expanded === join13(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
7798
8687
|
}
|
|
7799
8688
|
async function applyOne(options, file) {
|
|
7800
8689
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -7933,7 +8822,7 @@ function describe(err) {
|
|
|
7933
8822
|
}
|
|
7934
8823
|
|
|
7935
8824
|
// src/lib/vault-file-sync.ts
|
|
7936
|
-
import { createHash } from "node:crypto";
|
|
8825
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
7937
8826
|
async function syncVaultFiles(options) {
|
|
7938
8827
|
const files = await listVaultFiles(options);
|
|
7939
8828
|
if (files === null) return { applied: 0 };
|
|
@@ -8029,7 +8918,7 @@ async function reconcileVaultFile(options, file) {
|
|
|
8029
8918
|
}
|
|
8030
8919
|
const content = await downloadVaultFile(options, file);
|
|
8031
8920
|
if (content === null) return false;
|
|
8032
|
-
const actualSha256 =
|
|
8921
|
+
const actualSha256 = createHash2("sha256").update(content).digest("hex");
|
|
8033
8922
|
if (actualSha256 !== file.content_sha256) {
|
|
8034
8923
|
options.log({
|
|
8035
8924
|
level: "warn",
|
|
@@ -8230,6 +9119,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8230
9119
|
conversationFilter;
|
|
8231
9120
|
retry;
|
|
8232
9121
|
log;
|
|
9122
|
+
recordBinding;
|
|
8233
9123
|
fetchImpl;
|
|
8234
9124
|
sleep;
|
|
8235
9125
|
pausedPollIntervalMs;
|
|
@@ -8592,6 +9482,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8592
9482
|
this.retry = { ...DEFAULT_RETRY_POLICY, ...config.retry };
|
|
8593
9483
|
this.log = config.log ?? (() => {
|
|
8594
9484
|
});
|
|
9485
|
+
this.recordBinding = config.recordBinding;
|
|
8595
9486
|
this.fetchImpl = withRequestTimeout(
|
|
8596
9487
|
config.fetchImpl ?? fetch,
|
|
8597
9488
|
config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
|
|
@@ -8779,6 +9670,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8779
9670
|
let cappedSkips = 0;
|
|
8780
9671
|
for (const conv of conversations) {
|
|
8781
9672
|
if (this.stopped) break;
|
|
9673
|
+
if (conv.opencode_session_id) {
|
|
9674
|
+
this.recordSessionBinding(conv.id, conv.opencode_session_id);
|
|
9675
|
+
}
|
|
8782
9676
|
if (this.maxActiveSessions !== void 0) {
|
|
8783
9677
|
const activeSessionIds = this.activeSessionIdsForCap();
|
|
8784
9678
|
const resolvedSessionId = this.sessions.get(conv.id) ?? conv.opencode_session_id;
|
|
@@ -8861,12 +9755,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8861
9755
|
this.sessions.delete(conversationId);
|
|
8862
9756
|
this.supersede(conversationId, sessionId);
|
|
8863
9757
|
const dispatchDescription = source === "readopt" ? "re-dispatched message" : "message";
|
|
8864
|
-
const
|
|
8865
|
-
this.untrackableAck.set(messageId,
|
|
9758
|
+
const errorMessage4 = `OpenCode accepted this ${dispatchDescription} once, but its assigned id was never confirmed after ${attempts} attempts to match it up \u2014 the message was deliberately not sent again, so no duplicate turn was created; session ${sessionId.slice(0, 8)} was abandoned; a fresh one is used for further messages.`;
|
|
9759
|
+
this.untrackableAck.set(messageId, errorMessage4);
|
|
8866
9760
|
this.untrackableAckSessionIds.set(messageId, null);
|
|
8867
9761
|
this.log({
|
|
8868
9762
|
level: "error",
|
|
8869
|
-
message:
|
|
9763
|
+
message: errorMessage4,
|
|
8870
9764
|
conversation_id: conversationId,
|
|
8871
9765
|
message_id: messageId
|
|
8872
9766
|
});
|
|
@@ -8874,7 +9768,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8874
9768
|
conversationId,
|
|
8875
9769
|
messageId,
|
|
8876
9770
|
null,
|
|
8877
|
-
|
|
9771
|
+
errorMessage4
|
|
8878
9772
|
);
|
|
8879
9773
|
if (reportError !== null) {
|
|
8880
9774
|
this.log({
|
|
@@ -9055,12 +9949,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9055
9949
|
conversation_id: conv.id,
|
|
9056
9950
|
message_id: message.id
|
|
9057
9951
|
});
|
|
9058
|
-
const
|
|
9952
|
+
const errorMessage4 = this.untrackableAck.get(message.id);
|
|
9059
9953
|
const reportError = await this.reportUntrackableAck(
|
|
9060
9954
|
conv.id,
|
|
9061
9955
|
message.id,
|
|
9062
9956
|
null,
|
|
9063
|
-
|
|
9957
|
+
errorMessage4
|
|
9064
9958
|
);
|
|
9065
9959
|
if (reportError === null) {
|
|
9066
9960
|
continue;
|
|
@@ -9148,7 +10042,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9148
10042
|
const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
|
|
9149
10043
|
const providerId = hasProviderAndModel ? dispatchedModel.slice(0, separator) : null;
|
|
9150
10044
|
const modelId = hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null;
|
|
9151
|
-
const
|
|
10045
|
+
const errorMessage4 = `Provider namespace not loaded on this runner: ${message.opencode_model}. Refused before dispatch; no turn was started.`;
|
|
9152
10046
|
this.log({
|
|
9153
10047
|
level: "error",
|
|
9154
10048
|
message: `Refusing message ${message.id.slice(0, 8)} in conversation ${conv.id.slice(0, 8)}: provider namespace ${providerId ?? "(unknown)"} is not loaded; no turn was started`,
|
|
@@ -9156,7 +10050,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9156
10050
|
message_id: message.id
|
|
9157
10051
|
});
|
|
9158
10052
|
try {
|
|
9159
|
-
await this.markFailed(conv.id, message.id, sessionId,
|
|
10053
|
+
await this.markFailed(conv.id, message.id, sessionId, errorMessage4, void 0, {
|
|
9160
10054
|
classification: {
|
|
9161
10055
|
kind: "model_auth",
|
|
9162
10056
|
providerId,
|
|
@@ -9228,12 +10122,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9228
10122
|
} catch (err) {
|
|
9229
10123
|
if (err instanceof ChannelAuthError) throw err;
|
|
9230
10124
|
if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
|
|
9231
|
-
const
|
|
9232
|
-
this.untrackableAck.set(message.id,
|
|
10125
|
+
const errorMessage5 = err instanceof Error ? err.message : String(err);
|
|
10126
|
+
this.untrackableAck.set(message.id, errorMessage5);
|
|
9233
10127
|
this.untrackableAckSessionIds.set(message.id, null);
|
|
9234
10128
|
this.log({
|
|
9235
10129
|
level: "error",
|
|
9236
|
-
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${
|
|
10130
|
+
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage5}`,
|
|
9237
10131
|
conversation_id: conv.id,
|
|
9238
10132
|
message_id: message.id
|
|
9239
10133
|
});
|
|
@@ -9241,7 +10135,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9241
10135
|
conv.id,
|
|
9242
10136
|
message.id,
|
|
9243
10137
|
null,
|
|
9244
|
-
|
|
10138
|
+
errorMessage5
|
|
9245
10139
|
);
|
|
9246
10140
|
if (markError !== null) {
|
|
9247
10141
|
this.log({
|
|
@@ -9276,7 +10170,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9276
10170
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
9277
10171
|
break;
|
|
9278
10172
|
}
|
|
9279
|
-
const
|
|
10173
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
9280
10174
|
this.sessions.delete(conv.id);
|
|
9281
10175
|
this.supersede(conv.id, sessionId);
|
|
9282
10176
|
this.log({
|
|
@@ -9285,7 +10179,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9285
10179
|
conversation_id: conv.id,
|
|
9286
10180
|
message_id: message.id
|
|
9287
10181
|
});
|
|
9288
|
-
await this.markFailed(conv.id, message.id, null,
|
|
10182
|
+
await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
|
|
9289
10183
|
this.log({
|
|
9290
10184
|
level: "warn",
|
|
9291
10185
|
message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -9296,7 +10190,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9296
10190
|
});
|
|
9297
10191
|
this.log({
|
|
9298
10192
|
level: "error",
|
|
9299
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
10193
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage4}`,
|
|
9300
10194
|
conversation_id: conv.id,
|
|
9301
10195
|
message_id: message.id
|
|
9302
10196
|
});
|
|
@@ -10083,6 +10977,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10083
10977
|
return { sessionId: await this.createAndBindSession(conv.id), created: true };
|
|
10084
10978
|
}
|
|
10085
10979
|
this.sessions.set(conv.id, bound);
|
|
10980
|
+
this.recordSessionBinding(conv.id, bound);
|
|
10086
10981
|
return { sessionId: bound, created: false };
|
|
10087
10982
|
}
|
|
10088
10983
|
return { sessionId: await this.createAndBindSession(conv.id), created: true };
|
|
@@ -10096,6 +10991,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10096
10991
|
const directory = await this.resolveOpenCodeDirectory();
|
|
10097
10992
|
const sessionId = await this.createOpenCodeSession(directory);
|
|
10098
10993
|
this.sessions.set(conversationId, sessionId);
|
|
10994
|
+
this.recordSessionBinding(conversationId, sessionId);
|
|
10099
10995
|
await this.persistSession(conversationId, sessionId).catch((err) => {
|
|
10100
10996
|
this.log({
|
|
10101
10997
|
level: "warn",
|
|
@@ -10105,6 +11001,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10105
11001
|
});
|
|
10106
11002
|
return sessionId;
|
|
10107
11003
|
}
|
|
11004
|
+
recordSessionBinding(conversationId, sessionId) {
|
|
11005
|
+
if (!this.recordBinding) return;
|
|
11006
|
+
try {
|
|
11007
|
+
this.recordBinding(conversationId, sessionId);
|
|
11008
|
+
} catch (error2) {
|
|
11009
|
+
this.log({
|
|
11010
|
+
level: "warn",
|
|
11011
|
+
message: `Recording OpenCode session ${sessionId.slice(0, 8)} for conversation ${conversationId.slice(0, 8)} failed (best-effort): ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
11012
|
+
conversation_id: conversationId
|
|
11013
|
+
});
|
|
11014
|
+
}
|
|
11015
|
+
}
|
|
10108
11016
|
/**
|
|
10109
11017
|
* Lazily resolve (and cache) opencode's root directory via the selected client's
|
|
10110
11018
|
* location lookup.
|
|
@@ -12205,12 +13113,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12205
13113
|
conversation_id: row.conversation_id,
|
|
12206
13114
|
message_id: row.id
|
|
12207
13115
|
});
|
|
12208
|
-
const
|
|
13116
|
+
const errorMessage4 = this.untrackableAck.get(row.id);
|
|
12209
13117
|
const reportError = await this.reportUntrackableAck(
|
|
12210
13118
|
row.conversation_id,
|
|
12211
13119
|
row.id,
|
|
12212
13120
|
this.untrackableAckSessionIds.get(row.id) ?? null,
|
|
12213
|
-
|
|
13121
|
+
errorMessage4
|
|
12214
13122
|
);
|
|
12215
13123
|
if (reportError !== null) {
|
|
12216
13124
|
this.log({
|
|
@@ -12523,14 +13431,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12523
13431
|
this.awaitingReadopt.delete(row.id);
|
|
12524
13432
|
if (err instanceof ChannelAuthError) throw err;
|
|
12525
13433
|
if (this.isV2) {
|
|
12526
|
-
const
|
|
13434
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
12527
13435
|
const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
|
|
12528
13436
|
if (!invalidPromptAck) {
|
|
12529
13437
|
const exists = await this.sessionExists(sessionId);
|
|
12530
13438
|
if (exists === false) {
|
|
12531
13439
|
this.log({
|
|
12532
13440
|
level: "warn",
|
|
12533
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch hit a session (${sessionId.slice(0, 8)}) that was deleted mid-dispatch \u2014 deferring to the next drain: ${
|
|
13441
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch hit a session (${sessionId.slice(0, 8)}) that was deleted mid-dispatch \u2014 deferring to the next drain: ${errorMessage4}`,
|
|
12534
13442
|
conversation_id: row.conversation_id,
|
|
12535
13443
|
message_id: row.id
|
|
12536
13444
|
});
|
|
@@ -12540,7 +13448,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12540
13448
|
if (exists === null) {
|
|
12541
13449
|
this.log({
|
|
12542
13450
|
level: "warn",
|
|
12543
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch failed and session (${sessionId.slice(0, 8)}) existence could not be confirmed \u2014 deferring to the next drain: ${
|
|
13451
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch failed and session (${sessionId.slice(0, 8)}) existence could not be confirmed \u2014 deferring to the next drain: ${errorMessage4}`,
|
|
12544
13452
|
conversation_id: row.conversation_id,
|
|
12545
13453
|
message_id: row.id
|
|
12546
13454
|
});
|
|
@@ -12548,12 +13456,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12548
13456
|
return;
|
|
12549
13457
|
}
|
|
12550
13458
|
} else {
|
|
12551
|
-
this.untrackableAck.set(row.id,
|
|
13459
|
+
this.untrackableAck.set(row.id, errorMessage4);
|
|
12552
13460
|
this.untrackableAckSessionIds.set(row.id, sessionId);
|
|
12553
13461
|
}
|
|
12554
13462
|
this.log({
|
|
12555
13463
|
level: "error",
|
|
12556
|
-
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${
|
|
13464
|
+
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage4}`,
|
|
12557
13465
|
conversation_id: row.conversation_id,
|
|
12558
13466
|
message_id: row.id
|
|
12559
13467
|
});
|
|
@@ -12562,7 +13470,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12562
13470
|
row.conversation_id,
|
|
12563
13471
|
row.id,
|
|
12564
13472
|
sessionId,
|
|
12565
|
-
|
|
13473
|
+
errorMessage4
|
|
12566
13474
|
);
|
|
12567
13475
|
if (markError !== null) {
|
|
12568
13476
|
this.log({
|
|
@@ -12573,7 +13481,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12573
13481
|
});
|
|
12574
13482
|
}
|
|
12575
13483
|
} else {
|
|
12576
|
-
await this.markFailed(row.conversation_id, row.id, sessionId,
|
|
13484
|
+
await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage4).catch(
|
|
12577
13485
|
(markErr) => {
|
|
12578
13486
|
this.log({
|
|
12579
13487
|
level: "warn",
|
|
@@ -13557,9 +14465,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
13557
14465
|
* trackable. Callers use the returned error to keep their path-specific logging
|
|
13558
14466
|
* while the same report payload can be retried on a later drain.
|
|
13559
14467
|
*/
|
|
13560
|
-
async reportUntrackableAck(conversationId, messageId, sessionId,
|
|
14468
|
+
async reportUntrackableAck(conversationId, messageId, sessionId, errorMessage4) {
|
|
13561
14469
|
try {
|
|
13562
|
-
await this.markFailed(conversationId, messageId, sessionId,
|
|
14470
|
+
await this.markFailed(conversationId, messageId, sessionId, errorMessage4);
|
|
13563
14471
|
return null;
|
|
13564
14472
|
} catch (err) {
|
|
13565
14473
|
this.signalUntrackableAck(conversationId, messageId);
|
|
@@ -14071,7 +14979,7 @@ import ora3 from "ora";
|
|
|
14071
14979
|
import { select as select3 } from "@inquirer/prompts";
|
|
14072
14980
|
function unknownPasswordError2(port) {
|
|
14073
14981
|
return new Error(
|
|
14074
|
-
`
|
|
14982
|
+
`An assistant on port ${port} requires credentials this runner does not have. Released OpenCode V2 generates a server password even when none is configured. This CLI has no option to supply the password for an already-running server, so it will not join one you started. Free port ${port} so Evident can start its own, or pass --port to choose another.`
|
|
14075
14983
|
);
|
|
14076
14984
|
}
|
|
14077
14985
|
var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
|
|
@@ -14089,9 +14997,9 @@ async function ensureOpenCode2Running(ctx) {
|
|
|
14089
14997
|
password: null
|
|
14090
14998
|
};
|
|
14091
14999
|
}
|
|
14092
|
-
if (!isOpenCode2Installed()) {
|
|
15000
|
+
if (!ctx.command && !isOpenCode2Installed()) {
|
|
14093
15001
|
throw new Error(
|
|
14094
|
-
|
|
15002
|
+
"OpenCode V2 is not installed. Install it with `npm install -g @opencode/cli` or `curl -fsSL https://opencode.ai/v2/install | bash`."
|
|
14095
15003
|
);
|
|
14096
15004
|
}
|
|
14097
15005
|
let port = ctx.port;
|
|
@@ -14119,7 +15027,8 @@ Port ${port} is already in use.`));
|
|
|
14119
15027
|
if (!ctx.interactive) {
|
|
14120
15028
|
ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
|
|
14121
15029
|
const { child: proc, password } = await startOpenCode2(port, {
|
|
14122
|
-
inheritStdio: ctx.inheritStdio
|
|
15030
|
+
inheritStdio: ctx.inheritStdio,
|
|
15031
|
+
command: ctx.command
|
|
14123
15032
|
});
|
|
14124
15033
|
const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
|
|
14125
15034
|
if (!health.healthy) {
|
|
@@ -14140,13 +15049,14 @@ Port ${port} is already in use.`));
|
|
|
14140
15049
|
password
|
|
14141
15050
|
};
|
|
14142
15051
|
}
|
|
15052
|
+
const command = ctx.command ?? "opencode2";
|
|
14143
15053
|
const action = await select3({
|
|
14144
15054
|
message: "OpenCode V2 is not running. What would you like to do?",
|
|
14145
15055
|
choices: [
|
|
14146
15056
|
{
|
|
14147
15057
|
name: "Start OpenCode V2 for me",
|
|
14148
15058
|
value: "start",
|
|
14149
|
-
description: `Run '
|
|
15059
|
+
description: `Run '${command} serve --port ${port}'`
|
|
14150
15060
|
},
|
|
14151
15061
|
{
|
|
14152
15062
|
name: "Show me the command",
|
|
@@ -14164,14 +15074,15 @@ Port ${port} is already in use.`));
|
|
|
14164
15074
|
blank();
|
|
14165
15075
|
console.log(chalk6.bold("Run this command in another terminal:"));
|
|
14166
15076
|
blank();
|
|
14167
|
-
console.log(` ${chalk6.cyan(
|
|
15077
|
+
console.log(` ${chalk6.cyan(`${command} serve --port ${port}`)}`);
|
|
14168
15078
|
blank();
|
|
14169
15079
|
throw new Error("Please start OpenCode V2 manually");
|
|
14170
15080
|
}
|
|
14171
15081
|
if (action === "start") {
|
|
14172
15082
|
const spinner = ora3("Starting OpenCode V2...").start();
|
|
14173
15083
|
const { child: proc, password } = await startOpenCode2(port, {
|
|
14174
|
-
inheritStdio: ctx.inheritStdio
|
|
15084
|
+
inheritStdio: ctx.inheritStdio,
|
|
15085
|
+
command: ctx.command
|
|
14175
15086
|
});
|
|
14176
15087
|
const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
|
|
14177
15088
|
if (!health.healthy) {
|
|
@@ -14197,7 +15108,7 @@ Port ${port} is already in use.`));
|
|
|
14197
15108
|
}
|
|
14198
15109
|
|
|
14199
15110
|
// src/lib/runner-credentials.ts
|
|
14200
|
-
import { chmodSync as chmodSync2, writeFileSync as
|
|
15111
|
+
import { chmodSync as chmodSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
14201
15112
|
import { spawn as spawn5 } from "node:child_process";
|
|
14202
15113
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
14203
15114
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
@@ -14440,8 +15351,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
14440
15351
|
}
|
|
14441
15352
|
try {
|
|
14442
15353
|
env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
|
|
14443
|
-
|
|
14444
|
-
|
|
15354
|
+
writeFileSync7(GIT_CONFIG_GLOBAL, "");
|
|
15355
|
+
writeFileSync7(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
|
|
14445
15356
|
chmodSync2(GIT_CREDENTIAL_HELPER, 448);
|
|
14446
15357
|
const config = [
|
|
14447
15358
|
["user.name", env.GIT_USER_NAME ?? "evident-bot"],
|
|
@@ -14474,7 +15385,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
14474
15385
|
// src/lib/opencode/config-overlay.ts
|
|
14475
15386
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
14476
15387
|
import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
|
|
14477
|
-
import { isAbsolute as isAbsolute3, join as
|
|
15388
|
+
import { isAbsolute as isAbsolute3, join as join14, resolve as resolve4 } from "node:path";
|
|
14478
15389
|
function parseConfigContent(content) {
|
|
14479
15390
|
if (!content?.trim()) return {};
|
|
14480
15391
|
const parsed = JSON.parse(content);
|
|
@@ -14504,7 +15415,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
14504
15415
|
return;
|
|
14505
15416
|
}
|
|
14506
15417
|
const source = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
|
|
14507
|
-
const target = isFile(
|
|
15418
|
+
const target = isFile(join14(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
14508
15419
|
if (!isFile(source)) {
|
|
14509
15420
|
log3(
|
|
14510
15421
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -14512,7 +15423,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
14512
15423
|
);
|
|
14513
15424
|
return;
|
|
14514
15425
|
}
|
|
14515
|
-
copyFileSync(source,
|
|
15426
|
+
copyFileSync(source, join14(cwd, target));
|
|
14516
15427
|
try {
|
|
14517
15428
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
14518
15429
|
stdio: "ignore"
|
|
@@ -14521,13 +15432,13 @@ function applyRunnerOpenCodeConfig({
|
|
|
14521
15432
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
14522
15433
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
14523
15434
|
}
|
|
14524
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
15435
|
+
log3(`Applied runner OpenCode config ${source} to ${join14(cwd, target)}`);
|
|
14525
15436
|
}
|
|
14526
15437
|
|
|
14527
15438
|
// src/lib/opencode/runtime-provenance.ts
|
|
14528
|
-
import { readFileSync as
|
|
15439
|
+
import { readFileSync as readFileSync11, readdirSync } from "node:fs";
|
|
14529
15440
|
import { homedir as homedir7 } from "node:os";
|
|
14530
|
-
import { join as
|
|
15441
|
+
import { join as join15 } from "node:path";
|
|
14531
15442
|
var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
|
14532
15443
|
var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
|
|
14533
15444
|
function errorKind(error2) {
|
|
@@ -14595,7 +15506,7 @@ function parseJsonc(value) {
|
|
|
14595
15506
|
return JSON.parse(output);
|
|
14596
15507
|
}
|
|
14597
15508
|
function openCodeCacheRoot(env = process.env, homeDir = homedir7()) {
|
|
14598
|
-
return
|
|
15509
|
+
return join15(env.XDG_CACHE_HOME ?? join15(homeDir, ".cache"), "opencode");
|
|
14599
15510
|
}
|
|
14600
15511
|
function readConfiguredPluginSpecs({
|
|
14601
15512
|
cwd = process.cwd(),
|
|
@@ -14604,10 +15515,10 @@ function readConfiguredPluginSpecs({
|
|
|
14604
15515
|
let config;
|
|
14605
15516
|
try {
|
|
14606
15517
|
try {
|
|
14607
|
-
config = parseJsonc(
|
|
15518
|
+
config = parseJsonc(readFileSync11(join15(cwd, "opencode.jsonc"), "utf8"));
|
|
14608
15519
|
} catch (error2) {
|
|
14609
15520
|
if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
|
|
14610
|
-
config = parseJsonc(
|
|
15521
|
+
config = parseJsonc(readFileSync11(join15(cwd, "opencode.json"), "utf8"));
|
|
14611
15522
|
}
|
|
14612
15523
|
} catch (error2) {
|
|
14613
15524
|
log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
|
|
@@ -14635,7 +15546,7 @@ function resolveInstalledPluginVersions({
|
|
|
14635
15546
|
homeDir = homedir7(),
|
|
14636
15547
|
log: log3
|
|
14637
15548
|
}) {
|
|
14638
|
-
const packagesRoot =
|
|
15549
|
+
const packagesRoot = join15(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
|
|
14639
15550
|
let directories;
|
|
14640
15551
|
try {
|
|
14641
15552
|
directories = readdirSync(packagesRoot, { withFileTypes: true });
|
|
@@ -14656,8 +15567,8 @@ function resolveInstalledPluginVersions({
|
|
|
14656
15567
|
let resolvedVersion = null;
|
|
14657
15568
|
try {
|
|
14658
15569
|
const packageJson = JSON.parse(
|
|
14659
|
-
|
|
14660
|
-
|
|
15570
|
+
readFileSync11(
|
|
15571
|
+
join15(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
|
|
14661
15572
|
"utf8"
|
|
14662
15573
|
)
|
|
14663
15574
|
);
|
|
@@ -14678,7 +15589,7 @@ function resolveInstalledPluginVersions({
|
|
|
14678
15589
|
}
|
|
14679
15590
|
|
|
14680
15591
|
// src/lib/credential-sync.ts
|
|
14681
|
-
import { renameSync, writeFileSync as
|
|
15592
|
+
import { renameSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
14682
15593
|
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
14683
15594
|
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
14684
15595
|
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
@@ -14688,7 +15599,7 @@ var MAX_FLUSH_PASSES = 2;
|
|
|
14688
15599
|
function outcomesWith(outcome) {
|
|
14689
15600
|
return { claude: outcome, opencode: outcome };
|
|
14690
15601
|
}
|
|
14691
|
-
function
|
|
15602
|
+
function errorMessage3(error2) {
|
|
14692
15603
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
14693
15604
|
}
|
|
14694
15605
|
function waitForSettlement(promise, timeoutMs) {
|
|
@@ -14712,10 +15623,10 @@ function writeMarker(markerPath, outcomes, log3) {
|
|
|
14712
15623
|
`;
|
|
14713
15624
|
const temporaryPath = `${markerPath}.tmp`;
|
|
14714
15625
|
try {
|
|
14715
|
-
|
|
15626
|
+
writeFileSync8(temporaryPath, body, { mode: 384 });
|
|
14716
15627
|
renameSync(temporaryPath, markerPath);
|
|
14717
15628
|
} catch (error2) {
|
|
14718
|
-
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${
|
|
15629
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage3(error2)}`, "warn");
|
|
14719
15630
|
}
|
|
14720
15631
|
}
|
|
14721
15632
|
function intervalSeconds(env, log3) {
|
|
@@ -14747,7 +15658,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
|
14747
15658
|
},
|
|
14748
15659
|
(error2) => {
|
|
14749
15660
|
failed = true;
|
|
14750
|
-
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${
|
|
15661
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage3(error2)}`, "warn");
|
|
14751
15662
|
}
|
|
14752
15663
|
);
|
|
14753
15664
|
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
@@ -14807,7 +15718,7 @@ function createCredentialSync({
|
|
|
14807
15718
|
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
14808
15719
|
} catch (error2) {
|
|
14809
15720
|
outcomes[store] = "failed";
|
|
14810
|
-
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${
|
|
15721
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage3(error2)}`, "debug");
|
|
14811
15722
|
}
|
|
14812
15723
|
}
|
|
14813
15724
|
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
@@ -14957,6 +15868,18 @@ function readSetup(value) {
|
|
|
14957
15868
|
}
|
|
14958
15869
|
return setup;
|
|
14959
15870
|
}
|
|
15871
|
+
function readModelDescriptors(value) {
|
|
15872
|
+
if (!Array.isArray(value)) return null;
|
|
15873
|
+
return value.every(
|
|
15874
|
+
(descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
|
|
15875
|
+
) ? value : null;
|
|
15876
|
+
}
|
|
15877
|
+
function logDescriptorRefreshFallback(options, branch, generation) {
|
|
15878
|
+
options.log(
|
|
15879
|
+
`MANAGED-INFERENCE-DESCRIPTORS-${branch}: runner=${options.agentId} generation=${generation}`,
|
|
15880
|
+
"warn"
|
|
15881
|
+
);
|
|
15882
|
+
}
|
|
14960
15883
|
function clearLocalSetup(options) {
|
|
14961
15884
|
const env = options.env ?? process.env;
|
|
14962
15885
|
const store = options.store ?? defaultStore;
|
|
@@ -15088,7 +16011,27 @@ async function reaffirmStored(options, credential) {
|
|
|
15088
16011
|
if (!setup || setup.generation !== credential.generation || setup.token_id !== credential.tokenId) {
|
|
15089
16012
|
return { kind: "failed", reason: "Managed credential reaffirmation returned a mismatch" };
|
|
15090
16013
|
}
|
|
15091
|
-
const
|
|
16014
|
+
const descriptorsValue = body?.model_descriptors;
|
|
16015
|
+
const refreshedDescriptors = readModelDescriptors(descriptorsValue);
|
|
16016
|
+
let credentialToApply = credential;
|
|
16017
|
+
if (descriptorsValue === void 0 || descriptorsValue === null) {
|
|
16018
|
+
logDescriptorRefreshFallback(options, "UNAVAILABLE", credential.generation);
|
|
16019
|
+
} else if (refreshedDescriptors === null) {
|
|
16020
|
+
logDescriptorRefreshFallback(options, "MALFORMED", credential.generation);
|
|
16021
|
+
} else if (refreshedDescriptors.length === 0) {
|
|
16022
|
+
logDescriptorRefreshFallback(options, "EMPTY", credential.generation);
|
|
16023
|
+
} else {
|
|
16024
|
+
const { config, unsupportedProviders } = buildCredentialProxyNativeConfig({
|
|
16025
|
+
inferenceBaseUrl: credential.inferenceBaseUrl,
|
|
16026
|
+
modelDescriptors: refreshedDescriptors
|
|
16027
|
+
});
|
|
16028
|
+
if (unsupportedProviders.length > 0 || Object.keys(config.provider).length === 0) {
|
|
16029
|
+
logDescriptorRefreshFallback(options, "UNSUPPORTED-PROVIDER", credential.generation);
|
|
16030
|
+
} else {
|
|
16031
|
+
credentialToApply = { ...credential, modelDescriptors: refreshedDescriptors };
|
|
16032
|
+
}
|
|
16033
|
+
}
|
|
16034
|
+
const failure = applyCredential(options, credentialToApply);
|
|
15092
16035
|
return failure ? { kind: "failed", reason: failure } : { kind: "ready", generation: credential.generation, tokenId: credential.tokenId };
|
|
15093
16036
|
}
|
|
15094
16037
|
async function prepareManagedInferenceSetup(options, discovery, health) {
|
|
@@ -15122,10 +16065,8 @@ async function prepareManagedInferenceSetup(options, discovery, health) {
|
|
|
15122
16065
|
const setup = readSetup(body?.setup);
|
|
15123
16066
|
const token = body?.inference_token;
|
|
15124
16067
|
const inferenceBaseUrl = body?.inference_base_url;
|
|
15125
|
-
const modelDescriptors = body?.model_descriptors;
|
|
15126
|
-
if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" ||
|
|
15127
|
-
(descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
|
|
15128
|
-
)) {
|
|
16068
|
+
const modelDescriptors = readModelDescriptors(body?.model_descriptors);
|
|
16069
|
+
if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" || modelDescriptors === null) {
|
|
15129
16070
|
return { kind: "failed", reason: "Managed credential bootstrap returned an invalid response" };
|
|
15130
16071
|
}
|
|
15131
16072
|
const credential = {
|
|
@@ -15199,7 +16140,7 @@ function resolveFileSyncEntry(entry, homeDir, flagName, labels) {
|
|
|
15199
16140
|
if (trimmed === "") {
|
|
15200
16141
|
throw new Error(`${flagName} requires a ${labels.noun} path (got an empty value)`);
|
|
15201
16142
|
}
|
|
15202
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
16143
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join16(homeDir, trimmed.slice(2)) : trimmed;
|
|
15203
16144
|
if (!isAbsolute4(expanded)) {
|
|
15204
16145
|
throw new Error(`${flagName} requires an absolute ${labels.noun} path; got "${entry}"`);
|
|
15205
16146
|
}
|
|
@@ -15252,13 +16193,13 @@ function resolveOpenCodeVersion(options, env = process.env) {
|
|
|
15252
16193
|
raw = env[OPENCODE_VERSION_ENV];
|
|
15253
16194
|
source = OPENCODE_VERSION_ENV;
|
|
15254
16195
|
} else {
|
|
15255
|
-
return { version: "
|
|
16196
|
+
return { version: "auto", warnings: [] };
|
|
15256
16197
|
}
|
|
15257
16198
|
const normalized = raw.trim().toLowerCase();
|
|
15258
|
-
if (normalized !== "v1" && normalized !== "v2") {
|
|
16199
|
+
if (normalized !== "v1" && normalized !== "v2" && normalized !== "auto") {
|
|
15259
16200
|
return {
|
|
15260
|
-
version: "
|
|
15261
|
-
warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or
|
|
16201
|
+
version: "auto",
|
|
16202
|
+
warnings: [`Ignoring invalid ${source} "${raw}": expected v1, v2, or auto; using auto`]
|
|
15262
16203
|
};
|
|
15263
16204
|
}
|
|
15264
16205
|
return { version: normalized, warnings: [] };
|
|
@@ -15768,8 +16709,8 @@ async function driveChannels(state, driver) {
|
|
|
15768
16709
|
state.running = false;
|
|
15769
16710
|
break;
|
|
15770
16711
|
}
|
|
15771
|
-
const
|
|
15772
|
-
logActivity(state, { type: "error", error: `Channel processing error: ${
|
|
16712
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : String(error2);
|
|
16713
|
+
logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage4}` });
|
|
15773
16714
|
if (state.interactive) displayStatus(state);
|
|
15774
16715
|
if (driver.hasInFlightWatchers()) {
|
|
15775
16716
|
consecutiveDrainFailures = 0;
|
|
@@ -15859,6 +16800,7 @@ async function driveChannels(state, driver) {
|
|
|
15859
16800
|
}
|
|
15860
16801
|
}
|
|
15861
16802
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
16803
|
+
var SESSION_ARCHIVE_FIRST_RUN_MS = SESSION_CLEANUP_FIRST_SWEEP_MS + 1e3;
|
|
15862
16804
|
var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
|
|
15863
16805
|
var SESSION_CLEANUP_MAX_ISOLATED_DELETE_FAILURES = 2;
|
|
15864
16806
|
var SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES = 2;
|
|
@@ -15873,7 +16815,7 @@ function shouldWarnForReclaimSkip(reason) {
|
|
|
15873
16815
|
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
15874
16816
|
}
|
|
15875
16817
|
function sessionDbPath() {
|
|
15876
|
-
return
|
|
16818
|
+
return join16(homedir8(), ".local", "share", "opencode", "opencode.db");
|
|
15877
16819
|
}
|
|
15878
16820
|
function logSessionCleanupActivity(state, phase, entry) {
|
|
15879
16821
|
logActivity(state, {
|
|
@@ -15882,6 +16824,63 @@ function logSessionCleanupActivity(state, phase, entry) {
|
|
|
15882
16824
|
metadata: { ...entry.metadata, phase }
|
|
15883
16825
|
});
|
|
15884
16826
|
}
|
|
16827
|
+
function logSessionArchiveActivity(state, phase, entry) {
|
|
16828
|
+
logActivity(state, {
|
|
16829
|
+
...entry,
|
|
16830
|
+
source: "cli.session-archive",
|
|
16831
|
+
metadata: { ...entry.metadata, phase }
|
|
16832
|
+
});
|
|
16833
|
+
}
|
|
16834
|
+
function logSessionArchiveSummary(state, phase, summary) {
|
|
16835
|
+
const everyUploadFailed = summary.uploadAttempts > 0 && summary.uploadFailures === summary.uploadAttempts;
|
|
16836
|
+
const staleSince = everyUploadFailed && summary.staleSince !== null ? `; archive stale since ${summary.staleSince}` : "";
|
|
16837
|
+
const neverWatermarked = summary.neverWatermarkedFailures > 0 ? `; ${summary.neverWatermarkedFailures} failed session(s) had no prior successful upload` : "";
|
|
16838
|
+
const warning2 = summary.failed > 0 || summary.failures.length > 0 || everyUploadFailed;
|
|
16839
|
+
logSessionArchiveActivity(state, phase, {
|
|
16840
|
+
type: "info",
|
|
16841
|
+
level: warning2 ? "warn" : "info",
|
|
16842
|
+
message: `Session archive: inspected ${summary.inspected}, uploaded ${summary.uploaded}, unchanged ${summary.unchanged}, failed ${summary.failed}, skipped budget ${summary.skippedBudget}, unmapped ${summary.unmappedIds.length}` + staleSince + neverWatermarked,
|
|
16843
|
+
metadata: {
|
|
16844
|
+
outcome: "archive-run-complete",
|
|
16845
|
+
sessions_inspected: summary.inspected,
|
|
16846
|
+
sessions_uploaded: summary.uploaded,
|
|
16847
|
+
sessions_unchanged: summary.unchanged,
|
|
16848
|
+
sessions_failed: summary.failed,
|
|
16849
|
+
uploads_attempted: summary.uploadAttempts,
|
|
16850
|
+
uploads_failed: summary.uploadFailures,
|
|
16851
|
+
sessions_skipped_budget: summary.skippedBudget,
|
|
16852
|
+
sessions_unmapped: summary.unmappedIds.length,
|
|
16853
|
+
sessions_without_prior_watermark: summary.neverWatermarkedFailures,
|
|
16854
|
+
...everyUploadFailed && summary.staleSince !== null ? { stale_since: summary.staleSince } : {}
|
|
16855
|
+
}
|
|
16856
|
+
});
|
|
16857
|
+
for (const failure of summary.failures) {
|
|
16858
|
+
const permanentSizeFailure = failure.reason === "too-large";
|
|
16859
|
+
const safeMessage = failure.message ? `: ${redactDiagnosticText(failure.message).slice(0, 200)}` : "";
|
|
16860
|
+
logSessionArchiveActivity(state, phase, {
|
|
16861
|
+
type: "info",
|
|
16862
|
+
level: "warn",
|
|
16863
|
+
message: `Session archive: session ${failure.sessionId} ` + (permanentSizeFailure ? "failed permanently (too-large payload)" : `failed (${failure.outcome}`) + (permanentSizeFailure ? "" : failure.reason ? `, reason=${failure.reason})` : ")") + safeMessage,
|
|
16864
|
+
metadata: {
|
|
16865
|
+
outcome: failure.outcome,
|
|
16866
|
+
session_id: failure.sessionId,
|
|
16867
|
+
archived: failure.archived,
|
|
16868
|
+
...failure.reason !== void 0 ? { reason: failure.reason } : {}
|
|
16869
|
+
}
|
|
16870
|
+
});
|
|
16871
|
+
}
|
|
16872
|
+
if (summary.unmappedIds.length > 0) {
|
|
16873
|
+
logSessionArchiveActivity(state, phase, {
|
|
16874
|
+
type: "info",
|
|
16875
|
+
level: "warn",
|
|
16876
|
+
message: `Session archive: ${summary.unmappedIds.length} unmapped session(s): ` + summary.unmappedIds.join(", "),
|
|
16877
|
+
metadata: {
|
|
16878
|
+
outcome: "unmapped-sessions",
|
|
16879
|
+
sessions_unmapped: summary.unmappedIds.length
|
|
16880
|
+
}
|
|
16881
|
+
});
|
|
16882
|
+
}
|
|
16883
|
+
}
|
|
15885
16884
|
async function drainSessionDeletionReports(state) {
|
|
15886
16885
|
const path = sessionDeletionReportPath(homedir8(), process.env);
|
|
15887
16886
|
let delivered = 0;
|
|
@@ -15979,6 +16978,7 @@ async function runSweep(state, driver, config) {
|
|
|
15979
16978
|
let failedOtherStatus = 0;
|
|
15980
16979
|
let failedThrew = 0;
|
|
15981
16980
|
let skippedNewlyActive = 0;
|
|
16981
|
+
let skippedArchive = 0;
|
|
15982
16982
|
let remaining = 0;
|
|
15983
16983
|
const deletionRecords = [];
|
|
15984
16984
|
let protectedNow = driver.protectedSessionIds();
|
|
@@ -15990,6 +16990,7 @@ async function runSweep(state, driver, config) {
|
|
|
15990
16990
|
const lastActivityById = new Map(
|
|
15991
16991
|
snapshots.map(({ id, lastActivityMs }) => [id, lastActivityMs])
|
|
15992
16992
|
);
|
|
16993
|
+
const sessionById = new Map(sessions.map((session) => [session.id, session]));
|
|
15993
16994
|
const toDelete = selectSessionsToDelete(snapshots, {
|
|
15994
16995
|
maxAgeMs: config.maxAgeMs,
|
|
15995
16996
|
maxCount: config.maxCount,
|
|
@@ -16012,6 +17013,42 @@ async function runSweep(state, driver, config) {
|
|
|
16012
17013
|
});
|
|
16013
17014
|
continue;
|
|
16014
17015
|
}
|
|
17016
|
+
if (state.sessionArchiveConfig.enabled) {
|
|
17017
|
+
const candidate = sessionById.get(id);
|
|
17018
|
+
const archiveConfig = state.sessionArchiveConfig;
|
|
17019
|
+
const archiveBudgetMs = Math.min(
|
|
17020
|
+
archiveConfig.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
|
|
17021
|
+
MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
|
|
17022
|
+
);
|
|
17023
|
+
const archived = candidate ? await runSessionArchiveTask(
|
|
17024
|
+
state,
|
|
17025
|
+
"sweep",
|
|
17026
|
+
archiveBudgetMs,
|
|
17027
|
+
(signal) => runSessionArchiveBatch(state, "sweep", [candidate], archiveBudgetMs, signal)
|
|
17028
|
+
) : null;
|
|
17029
|
+
if (!archived?.archivedSessionIds.includes(id)) {
|
|
17030
|
+
skippedArchive++;
|
|
17031
|
+
logSessionArchiveActivity(state, "sweep", {
|
|
17032
|
+
type: "info",
|
|
17033
|
+
level: "warn",
|
|
17034
|
+
message: `Session cleanup: retaining session ${id}; deletion is held back pending successful archival`,
|
|
17035
|
+
metadata: {
|
|
17036
|
+
outcome: "deletion-held-for-archive",
|
|
17037
|
+
session_id: id
|
|
17038
|
+
}
|
|
17039
|
+
});
|
|
17040
|
+
continue;
|
|
17041
|
+
}
|
|
17042
|
+
protectedNow = driver.protectedSessionIds();
|
|
17043
|
+
if (protectedNow.has(id)) {
|
|
17044
|
+
skippedNewlyActive++;
|
|
17045
|
+
logActivity(state, {
|
|
17046
|
+
type: "info",
|
|
17047
|
+
message: `Session cleanup: skipping ${id} \u2014 became active/bound during archival (${mode})`
|
|
17048
|
+
});
|
|
17049
|
+
continue;
|
|
17050
|
+
}
|
|
17051
|
+
}
|
|
16015
17052
|
const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
|
|
16016
17053
|
if (outcome.ok) {
|
|
16017
17054
|
deleted++;
|
|
@@ -16075,10 +17112,11 @@ async function runSweep(state, driver, config) {
|
|
|
16075
17112
|
}
|
|
16076
17113
|
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
16077
17114
|
const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
|
|
17115
|
+
const archiveHeldNote = skippedArchive > 0 ? `, archive held ${skippedArchive}` : "";
|
|
16078
17116
|
const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
|
|
16079
17117
|
logActivity(state, {
|
|
16080
17118
|
type: "info",
|
|
16081
|
-
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
|
|
17119
|
+
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${archiveHeldNote}${remainingNote} (${mode})`
|
|
16082
17120
|
});
|
|
16083
17121
|
}
|
|
16084
17122
|
if (deletionRecords.length > 0) {
|
|
@@ -16116,6 +17154,7 @@ async function runSweep(state, driver, config) {
|
|
|
16116
17154
|
sessions_failed_other: failedOtherStatus,
|
|
16117
17155
|
sessions_failed_threw: failedThrew,
|
|
16118
17156
|
sessions_skipped_active: skippedNewlyActive,
|
|
17157
|
+
sessions_archive_held: skippedArchive,
|
|
16119
17158
|
sessions_remaining: remaining,
|
|
16120
17159
|
...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
|
|
16121
17160
|
};
|
|
@@ -16247,6 +17286,186 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
16247
17286
|
);
|
|
16248
17287
|
state.sessionCleanupTimers.push(interval, firstSweep);
|
|
16249
17288
|
}
|
|
17289
|
+
function runSessionArchiveTask(state, phase, budgetMs, work) {
|
|
17290
|
+
if (state.sessionArchiveInFlight) {
|
|
17291
|
+
logSessionArchiveActivity(state, phase, {
|
|
17292
|
+
type: "info",
|
|
17293
|
+
level: "debug",
|
|
17294
|
+
message: "Session archive skipped \u2014 previous archive run still in progress",
|
|
17295
|
+
metadata: { outcome: "skipped-in-flight" }
|
|
17296
|
+
});
|
|
17297
|
+
return Promise.resolve(null);
|
|
17298
|
+
}
|
|
17299
|
+
const controller = new AbortController();
|
|
17300
|
+
state.sessionArchiveAbortController = controller;
|
|
17301
|
+
const timeout = setTimeout(() => {
|
|
17302
|
+
controller.abort();
|
|
17303
|
+
logSessionArchiveActivity(state, phase, {
|
|
17304
|
+
type: "info",
|
|
17305
|
+
level: "warn",
|
|
17306
|
+
message: `Session archive exceeded ${budgetMs}ms; remaining sessions were not confirmed archived`,
|
|
17307
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17308
|
+
});
|
|
17309
|
+
}, budgetMs);
|
|
17310
|
+
const task = Promise.resolve().then(() => work(controller.signal)).catch((error2) => {
|
|
17311
|
+
logSessionArchiveActivity(state, phase, {
|
|
17312
|
+
type: "info",
|
|
17313
|
+
level: "warn",
|
|
17314
|
+
message: `Session archive run failed: ${redactDiagnosticText(error2 instanceof Error ? error2.message : String(error2)).slice(0, 200)}`,
|
|
17315
|
+
metadata: { outcome: "archive-run-failed" }
|
|
17316
|
+
});
|
|
17317
|
+
return null;
|
|
17318
|
+
}).finally(() => {
|
|
17319
|
+
clearTimeout(timeout);
|
|
17320
|
+
if (state.sessionArchiveInFlight === task) {
|
|
17321
|
+
state.sessionArchiveInFlight = null;
|
|
17322
|
+
state.sessionArchiveAbortController = null;
|
|
17323
|
+
}
|
|
17324
|
+
});
|
|
17325
|
+
state.sessionArchiveInFlight = task;
|
|
17326
|
+
return task;
|
|
17327
|
+
}
|
|
17328
|
+
async function runSessionArchiveBatch(state, phase, sessions, budgetMs, signal) {
|
|
17329
|
+
const config = state.sessionArchiveConfig;
|
|
17330
|
+
const client = state.opencodeClient;
|
|
17331
|
+
const watermarkPath = state.sessionArchiveWatermarkPath;
|
|
17332
|
+
if (!client || !config.keyPrefix || !watermarkPath || !state.agentId) {
|
|
17333
|
+
logSessionArchiveActivity(state, phase, {
|
|
17334
|
+
type: "info",
|
|
17335
|
+
level: "warn",
|
|
17336
|
+
message: "Session archive skipped because runner identity or archive configuration is unavailable",
|
|
17337
|
+
metadata: { outcome: "archive-configuration-unavailable" }
|
|
17338
|
+
});
|
|
17339
|
+
return null;
|
|
17340
|
+
}
|
|
17341
|
+
const summary = await runSessionArchive({
|
|
17342
|
+
sessions,
|
|
17343
|
+
deps: {
|
|
17344
|
+
client,
|
|
17345
|
+
runnerId: state.agentId,
|
|
17346
|
+
keyPrefix: config.keyPrefix,
|
|
17347
|
+
bindings: readSessionBindings(
|
|
17348
|
+
sessionBindingLedgerPath(homedir8(), process.env),
|
|
17349
|
+
(message) => logSessionArchiveActivity(state, phase, {
|
|
17350
|
+
type: "info",
|
|
17351
|
+
level: "warn",
|
|
17352
|
+
message: redactDiagnosticText(message),
|
|
17353
|
+
metadata: { outcome: "binding-ledger-read-failed" }
|
|
17354
|
+
})
|
|
17355
|
+
),
|
|
17356
|
+
watermarks: state.sessionArchiveWatermarks,
|
|
17357
|
+
watermarkPath,
|
|
17358
|
+
signal,
|
|
17359
|
+
upload: uploadSessionArchive,
|
|
17360
|
+
exportMessages: exportSessionMessages,
|
|
17361
|
+
recordWatermark: recordUpload
|
|
17362
|
+
},
|
|
17363
|
+
budgetMs
|
|
17364
|
+
});
|
|
17365
|
+
logSessionArchiveSummary(state, phase, summary);
|
|
17366
|
+
return summary;
|
|
17367
|
+
}
|
|
17368
|
+
async function runSessionArchiveFleet(state, phase, budgetMs, signal) {
|
|
17369
|
+
const startedAt = Date.now();
|
|
17370
|
+
const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0, signal);
|
|
17371
|
+
if (enumeration.sessions === null) {
|
|
17372
|
+
const interrupted = signal.aborted;
|
|
17373
|
+
logSessionArchiveActivity(state, phase, {
|
|
17374
|
+
type: "info",
|
|
17375
|
+
level: "warn",
|
|
17376
|
+
message: interrupted ? "Session archive: enumeration was interrupted before the complete store was known; no session was marked archived" : `Session archive: could not enumerate the complete session store (${enumeration.reason}); no session was marked archived`,
|
|
17377
|
+
metadata: {
|
|
17378
|
+
outcome: interrupted ? "enumeration-interrupted" : "enumeration-refused",
|
|
17379
|
+
...!interrupted ? { reason: enumeration.reason } : {}
|
|
17380
|
+
}
|
|
17381
|
+
});
|
|
17382
|
+
return null;
|
|
17383
|
+
}
|
|
17384
|
+
return runSessionArchiveBatch(
|
|
17385
|
+
state,
|
|
17386
|
+
phase,
|
|
17387
|
+
enumeration.sessions,
|
|
17388
|
+
Math.max(0, budgetMs - (Date.now() - startedAt)),
|
|
17389
|
+
signal
|
|
17390
|
+
);
|
|
17391
|
+
}
|
|
17392
|
+
async function runSessionArchiveShutdownFlush(state, budgetMs) {
|
|
17393
|
+
const deadline = Date.now() + budgetMs;
|
|
17394
|
+
const previousRun = state.sessionArchiveInFlight;
|
|
17395
|
+
if (previousRun) {
|
|
17396
|
+
state.sessionArchiveAbortController?.abort(
|
|
17397
|
+
new Error("graceful shutdown archive flush requested")
|
|
17398
|
+
);
|
|
17399
|
+
let timeout;
|
|
17400
|
+
const settled = await Promise.race([
|
|
17401
|
+
previousRun.then(() => true),
|
|
17402
|
+
new Promise((resolve5) => {
|
|
17403
|
+
timeout = setTimeout(() => resolve5(false), budgetMs);
|
|
17404
|
+
})
|
|
17405
|
+
]);
|
|
17406
|
+
if (timeout) clearTimeout(timeout);
|
|
17407
|
+
if (!settled) {
|
|
17408
|
+
state.sessionArchiveAbortController?.abort(new Error("session archive flush budget expired"));
|
|
17409
|
+
logSessionArchiveActivity(state, "shutdown", {
|
|
17410
|
+
type: "info",
|
|
17411
|
+
level: "warn",
|
|
17412
|
+
message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
|
|
17413
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17414
|
+
});
|
|
17415
|
+
return null;
|
|
17416
|
+
}
|
|
17417
|
+
}
|
|
17418
|
+
const remainingMs = Math.floor(deadline - Date.now());
|
|
17419
|
+
if (remainingMs <= 0) {
|
|
17420
|
+
logSessionArchiveActivity(state, "shutdown", {
|
|
17421
|
+
type: "info",
|
|
17422
|
+
level: "warn",
|
|
17423
|
+
message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
|
|
17424
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17425
|
+
});
|
|
17426
|
+
return null;
|
|
17427
|
+
}
|
|
17428
|
+
return runSessionArchiveTask(
|
|
17429
|
+
state,
|
|
17430
|
+
"shutdown",
|
|
17431
|
+
remainingMs,
|
|
17432
|
+
(signal) => runSessionArchiveFleet(state, "shutdown", remainingMs, signal)
|
|
17433
|
+
);
|
|
17434
|
+
}
|
|
17435
|
+
function scheduleSessionArchive(state) {
|
|
17436
|
+
const config = state.sessionArchiveConfig;
|
|
17437
|
+
if (!config.enabled) return;
|
|
17438
|
+
state.sessionArchiveWatermarkPath = sessionArchiveWatermarkPath(homedir8(), process.env);
|
|
17439
|
+
state.sessionArchiveWatermarks = readWatermarks(
|
|
17440
|
+
state.sessionArchiveWatermarkPath,
|
|
17441
|
+
(message) => logSessionArchiveActivity(state, "startup", {
|
|
17442
|
+
type: "info",
|
|
17443
|
+
level: "warn",
|
|
17444
|
+
message: redactDiagnosticText(message),
|
|
17445
|
+
metadata: { outcome: "watermark-read-failed" }
|
|
17446
|
+
})
|
|
17447
|
+
);
|
|
17448
|
+
logSessionArchiveActivity(state, "startup", {
|
|
17449
|
+
type: "info",
|
|
17450
|
+
message: `Session archive enabled (interval=${config.intervalMs}ms)`,
|
|
17451
|
+
metadata: { outcome: "armed", interval_ms: config.intervalMs }
|
|
17452
|
+
});
|
|
17453
|
+
const cycleBudgetMs = Math.min(
|
|
17454
|
+
config.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
|
|
17455
|
+
MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
|
|
17456
|
+
);
|
|
17457
|
+
const run2 = (phase = "scheduled") => {
|
|
17458
|
+
void runSessionArchiveTask(
|
|
17459
|
+
state,
|
|
17460
|
+
phase,
|
|
17461
|
+
cycleBudgetMs,
|
|
17462
|
+
(signal) => runSessionArchiveFleet(state, phase, cycleBudgetMs, signal)
|
|
17463
|
+
);
|
|
17464
|
+
};
|
|
17465
|
+
const interval = setInterval(run2, config.intervalMs);
|
|
17466
|
+
const firstRun = setTimeout(() => run2(), SESSION_ARCHIVE_FIRST_RUN_MS);
|
|
17467
|
+
state.sessionArchiveTimers.push(interval, firstRun);
|
|
17468
|
+
}
|
|
16250
17469
|
function scheduleUsageReporting(state, params) {
|
|
16251
17470
|
const { mode, warnings } = params.resolved;
|
|
16252
17471
|
for (const warning2 of warnings) {
|
|
@@ -16521,6 +17740,12 @@ async function cleanup(state, opts = {}) {
|
|
|
16521
17740
|
clearTimeout(timer);
|
|
16522
17741
|
}
|
|
16523
17742
|
state.sessionCleanupTimers = [];
|
|
17743
|
+
for (const timer of state.sessionArchiveTimers) {
|
|
17744
|
+
clearInterval(timer);
|
|
17745
|
+
clearTimeout(timer);
|
|
17746
|
+
}
|
|
17747
|
+
state.sessionArchiveTimers = [];
|
|
17748
|
+
state.sessionArchiveAbortController?.abort(new Error("runner cleanup started"));
|
|
16524
17749
|
state.stopOpenCodeLogTail?.();
|
|
16525
17750
|
state.stopOpenCodeLogTail = null;
|
|
16526
17751
|
if (state.claudeUsageTimer) {
|
|
@@ -16588,6 +17813,15 @@ async function cleanup(state, opts = {}) {
|
|
|
16588
17813
|
await timeShutdownPhase(state, durations, "tunnel_close", () => connection.close());
|
|
16589
17814
|
state.connection = null;
|
|
16590
17815
|
}
|
|
17816
|
+
if (opts.graceful && state.sessionArchiveConfig.enabled) {
|
|
17817
|
+
const budgetMs = state.sessionArchiveConfig.flushBudgetMs;
|
|
17818
|
+
await timeShutdownPhase(
|
|
17819
|
+
state,
|
|
17820
|
+
durations,
|
|
17821
|
+
"session_archive_flush",
|
|
17822
|
+
() => runSessionArchiveShutdownFlush(state, budgetMs)
|
|
17823
|
+
);
|
|
17824
|
+
}
|
|
16591
17825
|
if (state.opencodeProcess) {
|
|
16592
17826
|
const opencodeProcess = state.opencodeProcess;
|
|
16593
17827
|
const result = await timeShutdownPhase(
|
|
@@ -16711,6 +17945,18 @@ async function run(options) {
|
|
|
16711
17945
|
messageCount: 0,
|
|
16712
17946
|
lastProxiedActivityAt: null,
|
|
16713
17947
|
sessionCleanupTimers: [],
|
|
17948
|
+
sessionArchiveConfig: resolveSessionArchiveConfig(
|
|
17949
|
+
{
|
|
17950
|
+
enabled: options.sessionArchive,
|
|
17951
|
+
interval: options.sessionArchiveInterval
|
|
17952
|
+
},
|
|
17953
|
+
process.env
|
|
17954
|
+
),
|
|
17955
|
+
sessionArchiveTimers: [],
|
|
17956
|
+
sessionArchiveWatermarks: /* @__PURE__ */ new Map(),
|
|
17957
|
+
sessionArchiveWatermarkPath: null,
|
|
17958
|
+
sessionArchiveInFlight: null,
|
|
17959
|
+
sessionArchiveAbortController: null,
|
|
16714
17960
|
sessionDeleteFailures: /* @__PURE__ */ new Map(),
|
|
16715
17961
|
claudeUsageTimer: null,
|
|
16716
17962
|
claudeUsageRearm: null,
|
|
@@ -16724,6 +17970,14 @@ async function run(options) {
|
|
|
16724
17970
|
};
|
|
16725
17971
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
16726
17972
|
setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
|
|
17973
|
+
for (const warning2 of state.sessionArchiveConfig.warnings) {
|
|
17974
|
+
logSessionArchiveActivity(state, "startup", {
|
|
17975
|
+
type: "info",
|
|
17976
|
+
level: "warn",
|
|
17977
|
+
message: `Session archive: ${warning2}`,
|
|
17978
|
+
metadata: { outcome: "configuration-warning" }
|
|
17979
|
+
});
|
|
17980
|
+
}
|
|
16727
17981
|
if (options.credentialSyncMarker) {
|
|
16728
17982
|
state.credentialSync = createCredentialSync({
|
|
16729
17983
|
markerPath: options.credentialSyncMarker,
|
|
@@ -17045,13 +18299,75 @@ async function run(options) {
|
|
|
17045
18299
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
17046
18300
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
17047
18301
|
}
|
|
17048
|
-
const { version:
|
|
17049
|
-
options,
|
|
17050
|
-
process.env
|
|
17051
|
-
);
|
|
18302
|
+
const { version: opencodeVersionSelection, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(options, process.env);
|
|
17052
18303
|
for (const warning2 of opencodeVersionWarnings) {
|
|
17053
18304
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
17054
18305
|
}
|
|
18306
|
+
let opencodeMajor;
|
|
18307
|
+
let opencodeV2Command;
|
|
18308
|
+
let opencodeDetectionSource;
|
|
18309
|
+
let opencodeDetectionLevel = "info";
|
|
18310
|
+
let opencodeDetectionMessage;
|
|
18311
|
+
if (opencodeVersionSelection === "v1") {
|
|
18312
|
+
opencodeMajor = "v1";
|
|
18313
|
+
opencodeDetectionSource = "explicit";
|
|
18314
|
+
opencodeDetectionMessage = "OpenCode V1 was selected explicitly.";
|
|
18315
|
+
} else if (opencodeVersionSelection === "v2") {
|
|
18316
|
+
const installed = resolveInstalledOpenCode();
|
|
18317
|
+
opencodeMajor = "v2";
|
|
18318
|
+
opencodeDetectionSource = "explicit";
|
|
18319
|
+
if (installed.kind === "installed" && installed.major === "v2") {
|
|
18320
|
+
opencodeV2Command = installed.command;
|
|
18321
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; using ${installed.command}${installed.version ? ` (${installed.version})` : ""}.`;
|
|
18322
|
+
} else if (installed.kind === "unknown") {
|
|
18323
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported ${JSON.stringify(installed.rawVersion)}, but no V2 command was resolved.`;
|
|
18324
|
+
} else if (installed.kind === "installed") {
|
|
18325
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported V1 ${installed.version ?? "with an unknown version"}, so no V2 command was resolved.`;
|
|
18326
|
+
} else {
|
|
18327
|
+
opencodeDetectionMessage = "OpenCode V2 was selected explicitly, but no V2 command was resolved.";
|
|
18328
|
+
}
|
|
18329
|
+
} else {
|
|
18330
|
+
const running = await classifyRunningOpenCode(state.port);
|
|
18331
|
+
if (running.kind === "v1") {
|
|
18332
|
+
opencodeMajor = "v1";
|
|
18333
|
+
opencodeDetectionSource = "running-server";
|
|
18334
|
+
opencodeDetectionMessage = `OpenCode detection: found V1 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
|
|
18335
|
+
} else if (running.kind === "v2") {
|
|
18336
|
+
opencodeMajor = "v2";
|
|
18337
|
+
opencodeDetectionSource = "running-server";
|
|
18338
|
+
opencodeDetectionMessage = `OpenCode detection: found V2 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
|
|
18339
|
+
} else if (running.kind === "authenticated-unknown") {
|
|
18340
|
+
const message = `A server on port ${state.port} requires credentials this runner does not have. This can be a password-protected V1 or a V2 server, so the protocol cannot be established. Select it explicitly with --opencode-version v1 or --opencode-version v2.`;
|
|
18341
|
+
logActivity(state, {
|
|
18342
|
+
type: "info",
|
|
18343
|
+
level: "warn",
|
|
18344
|
+
message: `OpenCode detection: authenticated-unknown on port ${state.port}; ${running.detail}. ${message}`
|
|
18345
|
+
});
|
|
18346
|
+
throw new Error(message);
|
|
18347
|
+
} else {
|
|
18348
|
+
const installed = resolveInstalledOpenCode();
|
|
18349
|
+
opencodeDetectionLevel = "warn";
|
|
18350
|
+
if (installed.kind === "installed") {
|
|
18351
|
+
opencodeMajor = installed.major;
|
|
18352
|
+
opencodeDetectionSource = "installed-binary";
|
|
18353
|
+
if (installed.major === "v2") opencodeV2Command = installed.command;
|
|
18354
|
+
opencodeDetectionMessage = `OpenCode detection: no server identified on port ${state.port} (${running.detail}); using installed ${installed.major} command ${installed.command}${installed.version ? ` (${installed.version})` : " with an unknown version"}.`;
|
|
18355
|
+
} else if (installed.kind === "unknown") {
|
|
18356
|
+
opencodeMajor = "v1";
|
|
18357
|
+
opencodeDetectionSource = "fallback-v1";
|
|
18358
|
+
opencodeDetectionMessage = `OpenCode detection: no server identified on port ${state.port} (${running.detail}); ${installed.command} reported ${JSON.stringify(installed.rawVersion)}, so assuming V1. Use --opencode-version v2 to select V2 explicitly.`;
|
|
18359
|
+
} else {
|
|
18360
|
+
opencodeMajor = "v1";
|
|
18361
|
+
opencodeDetectionSource = "fallback-v1";
|
|
18362
|
+
opencodeDetectionMessage = `OpenCode detection: no server identified on port ${state.port} (${running.detail}); no recognizable OpenCode binary is installed, so continuing into the V1 install flow.`;
|
|
18363
|
+
}
|
|
18364
|
+
}
|
|
18365
|
+
}
|
|
18366
|
+
logActivity(state, {
|
|
18367
|
+
type: "info",
|
|
18368
|
+
level: opencodeDetectionLevel,
|
|
18369
|
+
message: opencodeDetectionMessage
|
|
18370
|
+
});
|
|
17055
18371
|
const managedSetupOptions = {
|
|
17056
18372
|
agentId: state.agentId,
|
|
17057
18373
|
apiUrl: getApiUrlConfig(),
|
|
@@ -17072,7 +18388,7 @@ async function run(options) {
|
|
|
17072
18388
|
managedPreparation = managedDiscovery;
|
|
17073
18389
|
managedSetupOptions.log(`MANAGED-INFERENCE-NOT-READY: ${managedDiscovery.reason}`, "warn");
|
|
17074
18390
|
} else if (managedDiscovery.kind === "setup") {
|
|
17075
|
-
const managedHealth =
|
|
18391
|
+
const managedHealth = opencodeMajor === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
|
|
17076
18392
|
managedPreparation = await prepareManagedInferenceSetup(
|
|
17077
18393
|
managedSetupOptions,
|
|
17078
18394
|
managedDiscovery,
|
|
@@ -17097,13 +18413,14 @@ async function run(options) {
|
|
|
17097
18413
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
17098
18414
|
const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
|
|
17099
18415
|
try {
|
|
17100
|
-
const oc =
|
|
18416
|
+
const oc = opencodeMajor === "v2" ? await ensureOpenCode2Running({
|
|
17101
18417
|
port: state.port,
|
|
17102
18418
|
interactive: state.interactive,
|
|
17103
18419
|
agentId: state.agentId,
|
|
17104
18420
|
log: (message) => log2(state, message),
|
|
17105
18421
|
startTimeoutMs: opencodeStartTimeoutMs,
|
|
17106
|
-
inheritStdio: Boolean(options.opencodePidFile)
|
|
18422
|
+
inheritStdio: Boolean(options.opencodePidFile),
|
|
18423
|
+
command: opencodeV2Command
|
|
17107
18424
|
}) : await ensureOpenCodeRunning({
|
|
17108
18425
|
port: state.port,
|
|
17109
18426
|
interactive: state.interactive,
|
|
@@ -17115,7 +18432,7 @@ async function run(options) {
|
|
|
17115
18432
|
state.port = oc.port;
|
|
17116
18433
|
state.opencodeProcess = options.opencodePidFile ? null : oc.process;
|
|
17117
18434
|
state.opencodeVersion = oc.version;
|
|
17118
|
-
state.opencodeApiVersion =
|
|
18435
|
+
state.opencodeApiVersion = opencodeMajor;
|
|
17119
18436
|
let configuredPlugins = [];
|
|
17120
18437
|
let resolvedPlugins = [];
|
|
17121
18438
|
try {
|
|
@@ -17137,13 +18454,14 @@ async function run(options) {
|
|
|
17137
18454
|
metadata: {
|
|
17138
18455
|
opencode_version: state.opencodeVersion,
|
|
17139
18456
|
opencode_api_version: state.opencodeApiVersion,
|
|
18457
|
+
opencode_detection_source: opencodeDetectionSource,
|
|
17140
18458
|
configured_plugins: JSON.stringify(configuredPlugins),
|
|
17141
18459
|
resolved_plugins: JSON.stringify(resolvedPlugins)
|
|
17142
18460
|
},
|
|
17143
18461
|
source: "cli.runtime-provenance"
|
|
17144
18462
|
});
|
|
17145
18463
|
let opencodePassword = null;
|
|
17146
|
-
if (
|
|
18464
|
+
if (opencodeMajor === "v2" && "password" in oc) {
|
|
17147
18465
|
const value = oc.password;
|
|
17148
18466
|
if (typeof value === "string" || value === null) opencodePassword = value;
|
|
17149
18467
|
}
|
|
@@ -17156,7 +18474,7 @@ async function run(options) {
|
|
|
17156
18474
|
state.opencodeClient = openCodeClient;
|
|
17157
18475
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
17158
18476
|
try {
|
|
17159
|
-
|
|
18477
|
+
writeFileSync9(options.opencodePidFile, `${oc.process.pid}
|
|
17160
18478
|
`, { mode: 384 });
|
|
17161
18479
|
chmodSync3(options.opencodePidFile, 384);
|
|
17162
18480
|
} catch (error2) {
|
|
@@ -17266,7 +18584,7 @@ async function run(options) {
|
|
|
17266
18584
|
let existingPid;
|
|
17267
18585
|
if (existsSync3(options.litestreamPidFile)) {
|
|
17268
18586
|
try {
|
|
17269
|
-
const rawPid =
|
|
18587
|
+
const rawPid = readFileSync12(options.litestreamPidFile, "utf8").trim();
|
|
17270
18588
|
const parsedPid = Number(rawPid);
|
|
17271
18589
|
if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
|
|
17272
18590
|
existingPid = parsedPid;
|
|
@@ -17303,7 +18621,7 @@ async function run(options) {
|
|
|
17303
18621
|
});
|
|
17304
18622
|
try {
|
|
17305
18623
|
if (litestreamProcess.pid !== void 0) {
|
|
17306
|
-
|
|
18624
|
+
writeFileSync9(options.litestreamPidFile, `${litestreamProcess.pid}
|
|
17307
18625
|
`, {
|
|
17308
18626
|
mode: 384
|
|
17309
18627
|
});
|
|
@@ -17353,6 +18671,15 @@ async function run(options) {
|
|
|
17353
18671
|
log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
|
|
17354
18672
|
}
|
|
17355
18673
|
const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
|
|
18674
|
+
const recordBinding = state.sessionArchiveConfig.enabled ? (conversationId, sessionId) => appendSessionBinding(
|
|
18675
|
+
sessionBindingLedgerPath(homedir8(), process.env),
|
|
18676
|
+
{
|
|
18677
|
+
conversation_id: conversationId,
|
|
18678
|
+
opencode_session_id: sessionId,
|
|
18679
|
+
observed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
18680
|
+
},
|
|
18681
|
+
(message) => logActivity(state, { type: "info", level: "warn", message })
|
|
18682
|
+
) : void 0;
|
|
17356
18683
|
const channelDriver = new ChannelDriver({
|
|
17357
18684
|
agentId: state.agentId,
|
|
17358
18685
|
port: state.port,
|
|
@@ -17362,6 +18689,7 @@ async function run(options) {
|
|
|
17362
18689
|
conversationFilter: state.conversationFilter,
|
|
17363
18690
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
17364
18691
|
sessionStoreHealth: state.sessionStoreHealth,
|
|
18692
|
+
recordBinding,
|
|
17365
18693
|
// #559: the file-sync allow-lists. Empty ⇒ pending files are REJECTED with
|
|
17366
18694
|
// `file_sync_disabled` on the ack, not silently ignored.
|
|
17367
18695
|
fileSyncDirectories,
|
|
@@ -17541,6 +18869,7 @@ async function run(options) {
|
|
|
17541
18869
|
}
|
|
17542
18870
|
if (!state.running) return;
|
|
17543
18871
|
scheduleSessionCleanup(state, channelDriver, options);
|
|
18872
|
+
scheduleSessionArchive(state);
|
|
17544
18873
|
state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
|
|
17545
18874
|
state.openaiUsageRearm = scheduleUsageReporting(state, {
|
|
17546
18875
|
label: "OpenAI",
|
|
@@ -17636,8 +18965,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17636
18965
|
"--opencode-start-timeout <seconds>",
|
|
17637
18966
|
"Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
|
|
17638
18967
|
).option(
|
|
17639
|
-
"--opencode-version <v1|v2>",
|
|
17640
|
-
"
|
|
18968
|
+
"--opencode-version <v1|v2|auto>",
|
|
18969
|
+
"OpenCode major version: auto (default) detects the running server, then installed binary; v1 and v2 skip server-protocol detection. Env: EVIDENT_OPENCODE_VERSION"
|
|
17641
18970
|
).option("--json", "Output in JSON format").option(
|
|
17642
18971
|
"--session-cleanup-max-age <duration>",
|
|
17643
18972
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
@@ -17650,6 +18979,12 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17650
18979
|
).option(
|
|
17651
18980
|
"--session-cleanup-interval <duration>",
|
|
17652
18981
|
"How often the cleanup sweep runs (default: 1h). Env: EVIDENT_SESSION_CLEANUP_INTERVAL"
|
|
18982
|
+
).option(
|
|
18983
|
+
"--session-archive",
|
|
18984
|
+
"Upload full OpenCode transcripts to the runner archive (off by default). Env: EVIDENT_SESSION_ARCHIVE"
|
|
18985
|
+
).option(
|
|
18986
|
+
"--session-archive-interval <duration>",
|
|
18987
|
+
"How often session archives are uploaded (default: 1h). Env: EVIDENT_SESSION_ARCHIVE_INTERVAL"
|
|
17653
18988
|
).option(
|
|
17654
18989
|
"--claude-usage-reporting <mode>",
|
|
17655
18990
|
"Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
@@ -17721,6 +19056,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17721
19056
|
sessionCleanupMaxCount: options.sessionCleanupMaxCount,
|
|
17722
19057
|
maxActiveSessions: options.maxActiveSessions,
|
|
17723
19058
|
sessionCleanupInterval: options.sessionCleanupInterval,
|
|
19059
|
+
sessionArchive: options.sessionArchive,
|
|
19060
|
+
sessionArchiveInterval: options.sessionArchiveInterval,
|
|
17724
19061
|
// Raw string — the resolver in run.ts single-sources parsing
|
|
17725
19062
|
// (resolveClaudeUsageReportingMode).
|
|
17726
19063
|
claudeUsageReporting: options.claudeUsageReporting,
|