@evident-ai/cli 3.6.1-dev.10d71b3 → 3.6.1-dev.2106dfe
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 +1999 -493
- 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.2106dfe" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1449
1449
|
function getCliVersion() {
|
|
1450
1450
|
return CLI_VERSION;
|
|
1451
1451
|
}
|
|
@@ -2195,6 +2195,7 @@ var FORWARDED_LEVELS_BY_SOURCE = {
|
|
|
2195
2195
|
"cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
|
|
2196
2196
|
opencode: /* @__PURE__ */ new Set(["warn", "error"]),
|
|
2197
2197
|
"cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"]),
|
|
2198
|
+
"cli.session-archive": /* @__PURE__ */ new Set(["info", "warn", "error"]),
|
|
2198
2199
|
"cli.runtime-provenance": /* @__PURE__ */ new Set(["info", "warn", "error"])
|
|
2199
2200
|
};
|
|
2200
2201
|
var SEVERITY_BY_LEVEL = {
|
|
@@ -2757,6 +2758,943 @@ function boundedError(error2) {
|
|
|
2757
2758
|
return message.slice(0, MAX_DIAGNOSTIC_LENGTH);
|
|
2758
2759
|
}
|
|
2759
2760
|
|
|
2761
|
+
// src/lib/opencode/session-binding-ledger.ts
|
|
2762
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2763
|
+
import { dirname as dirname5, join as join5 } from "node:path";
|
|
2764
|
+
var SESSION_BINDING_LEDGER_MAX_RECORDS = 1e3;
|
|
2765
|
+
var MAX_DIAGNOSTIC_LENGTH2 = 200;
|
|
2766
|
+
function sessionBindingLedgerPath(homeDir, env) {
|
|
2767
|
+
const override = env.EVIDENT_SESSION_BINDING_LEDGER?.trim();
|
|
2768
|
+
return override || join5(homeDir, ".local", "state", "evident", "session-bindings.jsonl");
|
|
2769
|
+
}
|
|
2770
|
+
function appendSessionBinding(path, record, log3) {
|
|
2771
|
+
mkdirSync2(dirname5(path), { recursive: true });
|
|
2772
|
+
const current = readSessionBindingRecords(path, log3);
|
|
2773
|
+
if (current.readError !== null) {
|
|
2774
|
+
throw new Error(`cannot enforce the ledger limit after reading ${path}: ${current.readError}`);
|
|
2775
|
+
}
|
|
2776
|
+
if (current.records.some(
|
|
2777
|
+
(existing) => existing.conversation_id === record.conversation_id && existing.opencode_session_id === record.opencode_session_id
|
|
2778
|
+
)) {
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
const kept = [...current.records, record].slice(-SESSION_BINDING_LEDGER_MAX_RECORDS);
|
|
2782
|
+
if (kept.length > current.records.length) {
|
|
2783
|
+
appendFileSync2(path, `${JSON.stringify(record)}
|
|
2784
|
+
`, "utf8");
|
|
2785
|
+
} else {
|
|
2786
|
+
writeFileSync2(path, `${kept.map((entry) => JSON.stringify(entry)).join("\n")}
|
|
2787
|
+
`, "utf8");
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
function readSessionBindings(path, log3) {
|
|
2791
|
+
const result = readSessionBindingRecords(path, log3);
|
|
2792
|
+
if (result.readError !== null) {
|
|
2793
|
+
log3(`[session-binding-ledger] could not read ${path}: ${result.readError}`);
|
|
2794
|
+
}
|
|
2795
|
+
return new Map(
|
|
2796
|
+
result.records.map((record) => [record.opencode_session_id, record.conversation_id])
|
|
2797
|
+
);
|
|
2798
|
+
}
|
|
2799
|
+
function readSessionBindingRecords(path, log3) {
|
|
2800
|
+
let content;
|
|
2801
|
+
try {
|
|
2802
|
+
content = readFileSync4(path, "utf8");
|
|
2803
|
+
} catch (error2) {
|
|
2804
|
+
if (isMissingFile2(error2)) return { records: [], readError: null };
|
|
2805
|
+
const readError = boundedError2(error2);
|
|
2806
|
+
return { records: [], readError };
|
|
2807
|
+
}
|
|
2808
|
+
const records = content.split("\n").flatMap((line) => {
|
|
2809
|
+
if (!line.trim()) return [];
|
|
2810
|
+
const record = parseRecord2(line, path, log3);
|
|
2811
|
+
return record ? [record] : [];
|
|
2812
|
+
});
|
|
2813
|
+
return { records, readError: null };
|
|
2814
|
+
}
|
|
2815
|
+
function parseRecord2(line, path, log3) {
|
|
2816
|
+
try {
|
|
2817
|
+
const value = JSON.parse(line);
|
|
2818
|
+
if (isSessionBindingRecord(value)) return value;
|
|
2819
|
+
log3(`[session-binding-ledger] skipped malformed record in ${path}: invalid shape`);
|
|
2820
|
+
} catch (error2) {
|
|
2821
|
+
log3(`[session-binding-ledger] skipped malformed record in ${path}: ${boundedError2(error2)}`);
|
|
2822
|
+
}
|
|
2823
|
+
return null;
|
|
2824
|
+
}
|
|
2825
|
+
function isSessionBindingRecord(value) {
|
|
2826
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2827
|
+
const record = value;
|
|
2828
|
+
return typeof record.conversation_id === "string" && record.conversation_id.length > 0 && typeof record.opencode_session_id === "string" && record.opencode_session_id.length > 0 && typeof record.observed_at === "string";
|
|
2829
|
+
}
|
|
2830
|
+
function isMissingFile2(error2) {
|
|
2831
|
+
return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
|
|
2832
|
+
}
|
|
2833
|
+
function boundedError2(error2) {
|
|
2834
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2835
|
+
return message.slice(0, MAX_DIAGNOSTIC_LENGTH2);
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
// src/lib/opencode/session-archive-config.ts
|
|
2839
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2840
|
+
|
|
2841
|
+
// src/lib/opencode/session-cleanup.ts
|
|
2842
|
+
var DURATION_UNIT_MS = {
|
|
2843
|
+
s: 1e3,
|
|
2844
|
+
m: 60 * 1e3,
|
|
2845
|
+
h: 60 * 60 * 1e3,
|
|
2846
|
+
d: 24 * 60 * 60 * 1e3
|
|
2847
|
+
};
|
|
2848
|
+
function parseDurationMs(input) {
|
|
2849
|
+
const trimmed = input.trim();
|
|
2850
|
+
const match = /^(\d+)([smhd])$/.exec(trimmed);
|
|
2851
|
+
if (!match) {
|
|
2852
|
+
throw new Error(
|
|
2853
|
+
`Invalid duration "${input}": expected <number><unit> where unit is one of s, m, h, d (e.g. "7d", "24h", "30m", "90s").`
|
|
2854
|
+
);
|
|
2855
|
+
}
|
|
2856
|
+
const value = Number(match[1]);
|
|
2857
|
+
if (value <= 0) {
|
|
2858
|
+
throw new Error(`Invalid duration "${input}": must be a positive value.`);
|
|
2859
|
+
}
|
|
2860
|
+
return value * DURATION_UNIT_MS[match[2]];
|
|
2861
|
+
}
|
|
2862
|
+
function selectSessionsToDelete(sessions, opts) {
|
|
2863
|
+
const { maxAgeMs, maxCount, nowMs, protectedIds } = opts;
|
|
2864
|
+
if (maxAgeMs === void 0 && maxCount === void 0) return [];
|
|
2865
|
+
const ageEligible = (s) => {
|
|
2866
|
+
if (maxAgeMs === void 0) return false;
|
|
2867
|
+
if (s.lastActivityMs === null) return true;
|
|
2868
|
+
return nowMs - s.lastActivityMs > maxAgeMs;
|
|
2869
|
+
};
|
|
2870
|
+
const countEligibleIds = /* @__PURE__ */ new Set();
|
|
2871
|
+
if (maxCount !== void 0) {
|
|
2872
|
+
const byActivityDesc = [...sessions].sort(
|
|
2873
|
+
(a, b) => (b.lastActivityMs ?? -Infinity) - (a.lastActivityMs ?? -Infinity)
|
|
2874
|
+
);
|
|
2875
|
+
for (const s of byActivityDesc.slice(maxCount)) {
|
|
2876
|
+
countEligibleIds.add(s.id);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
const toDelete = [];
|
|
2880
|
+
for (const s of sessions) {
|
|
2881
|
+
if (protectedIds.has(s.id)) continue;
|
|
2882
|
+
if (ageEligible(s) || countEligibleIds.has(s.id)) {
|
|
2883
|
+
toDelete.push(s.id);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
|
|
2887
|
+
return toDelete.sort(
|
|
2888
|
+
(a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2891
|
+
var DEFAULT_INTERVAL = "1h";
|
|
2892
|
+
function resolve2(flag, envValue, fallback) {
|
|
2893
|
+
return flag ?? envValue ?? fallback;
|
|
2894
|
+
}
|
|
2895
|
+
function parseMaxCount(input) {
|
|
2896
|
+
const trimmed = input.trim();
|
|
2897
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
2898
|
+
throw new Error(`Invalid max-count "${input}": expected a positive integer.`);
|
|
2899
|
+
}
|
|
2900
|
+
const value = Number(trimmed);
|
|
2901
|
+
if (value <= 0) {
|
|
2902
|
+
throw new Error(`Invalid max-count "${input}": must be greater than 0.`);
|
|
2903
|
+
}
|
|
2904
|
+
return value;
|
|
2905
|
+
}
|
|
2906
|
+
function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
2907
|
+
const warnings = [];
|
|
2908
|
+
const maxAgeRaw = resolve2(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
|
|
2909
|
+
const maxCountRaw = resolve2(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
|
|
2910
|
+
const intervalRaw = resolve2(
|
|
2911
|
+
flags.interval,
|
|
2912
|
+
env.EVIDENT_SESSION_CLEANUP_INTERVAL,
|
|
2913
|
+
DEFAULT_INTERVAL
|
|
2914
|
+
);
|
|
2915
|
+
let maxAgeMs;
|
|
2916
|
+
if (maxAgeRaw !== void 0) {
|
|
2917
|
+
try {
|
|
2918
|
+
maxAgeMs = parseDurationMs(maxAgeRaw);
|
|
2919
|
+
} catch (err) {
|
|
2920
|
+
warnings.push(
|
|
2921
|
+
`Ignoring invalid --session-cleanup-max-age: ${err instanceof Error ? err.message : String(err)}`
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
let maxCount;
|
|
2926
|
+
if (maxCountRaw !== void 0) {
|
|
2927
|
+
try {
|
|
2928
|
+
maxCount = parseMaxCount(maxCountRaw);
|
|
2929
|
+
} catch (err) {
|
|
2930
|
+
warnings.push(
|
|
2931
|
+
`Ignoring invalid --session-cleanup-max-count: ${err instanceof Error ? err.message : String(err)}`
|
|
2932
|
+
);
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
let intervalMs;
|
|
2936
|
+
try {
|
|
2937
|
+
intervalMs = parseDurationMs(intervalRaw ?? DEFAULT_INTERVAL);
|
|
2938
|
+
} catch (err) {
|
|
2939
|
+
warnings.push(
|
|
2940
|
+
`Ignoring invalid --session-cleanup-interval, using default ${DEFAULT_INTERVAL}: ${err instanceof Error ? err.message : String(err)}`
|
|
2941
|
+
);
|
|
2942
|
+
intervalMs = parseDurationMs(DEFAULT_INTERVAL);
|
|
2943
|
+
}
|
|
2944
|
+
const enabled = maxAgeMs !== void 0 || maxCount !== void 0;
|
|
2945
|
+
return { enabled, maxAgeMs, maxCount, intervalMs, warnings };
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
// src/lib/opencode/session-archive-config.ts
|
|
2949
|
+
var DEFAULT_INTERVAL2 = "1h";
|
|
2950
|
+
var DEFAULT_FLUSH_BUDGET_MS = 2e3;
|
|
2951
|
+
var MAX_SESSION_ARCHIVE_TIMER_DELAY_MS = 2147483647;
|
|
2952
|
+
var STATE_PREFIX_FILE = "/dev/shm/evident-state-prefix";
|
|
2953
|
+
var ENABLED_VALUES = /* @__PURE__ */ new Set(["on", "true", "1"]);
|
|
2954
|
+
var DISABLED_VALUES = /* @__PURE__ */ new Set(["off", "false", "0"]);
|
|
2955
|
+
function nonEmpty(value) {
|
|
2956
|
+
const trimmed = value?.trim();
|
|
2957
|
+
return trimmed ? trimmed : null;
|
|
2958
|
+
}
|
|
2959
|
+
function parseFlushBudgetMs(value) {
|
|
2960
|
+
const trimmed = value.trim();
|
|
2961
|
+
const budgetMs = Number(trimmed);
|
|
2962
|
+
if (!/^\d+$/.test(trimmed) || !Number.isSafeInteger(budgetMs) || budgetMs <= 0 || budgetMs > MAX_SESSION_ARCHIVE_TIMER_DELAY_MS) {
|
|
2963
|
+
throw new Error(
|
|
2964
|
+
`Invalid flush budget "${value}": expected a positive integer number of milliseconds no greater than ${MAX_SESSION_ARCHIVE_TIMER_DELAY_MS}.`
|
|
2965
|
+
);
|
|
2966
|
+
}
|
|
2967
|
+
return budgetMs;
|
|
2968
|
+
}
|
|
2969
|
+
function resolveSessionArchiveConfig(flags, env = process.env) {
|
|
2970
|
+
const warnings = [];
|
|
2971
|
+
const archiveRaw = env.EVIDENT_SESSION_ARCHIVE;
|
|
2972
|
+
let enabled = flags.enabled ?? false;
|
|
2973
|
+
if (flags.enabled === void 0 && archiveRaw !== void 0 && archiveRaw.trim() !== "") {
|
|
2974
|
+
const normalized = archiveRaw.trim().toLowerCase();
|
|
2975
|
+
if (ENABLED_VALUES.has(normalized)) {
|
|
2976
|
+
enabled = true;
|
|
2977
|
+
} else if (DISABLED_VALUES.has(normalized)) {
|
|
2978
|
+
enabled = false;
|
|
2979
|
+
} else {
|
|
2980
|
+
warnings.push(
|
|
2981
|
+
`Ignoring invalid EVIDENT_SESSION_ARCHIVE "${archiveRaw}": expected on or off; session archiving remains off.`
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
let intervalMs;
|
|
2986
|
+
const intervalRaw = flags.interval ?? env.EVIDENT_SESSION_ARCHIVE_INTERVAL ?? DEFAULT_INTERVAL2;
|
|
2987
|
+
try {
|
|
2988
|
+
intervalMs = parseDurationMs(intervalRaw);
|
|
2989
|
+
if (intervalMs > MAX_SESSION_ARCHIVE_TIMER_DELAY_MS) {
|
|
2990
|
+
throw new Error(
|
|
2991
|
+
`Duration "${intervalRaw}" exceeds Node's maximum timer delay of ${MAX_SESSION_ARCHIVE_TIMER_DELAY_MS}ms.`
|
|
2992
|
+
);
|
|
2993
|
+
}
|
|
2994
|
+
} catch (err) {
|
|
2995
|
+
warnings.push(
|
|
2996
|
+
`Ignoring invalid session archive interval, using default ${DEFAULT_INTERVAL2}: ${err instanceof Error ? err.message : String(err)}`
|
|
2997
|
+
);
|
|
2998
|
+
intervalMs = parseDurationMs(DEFAULT_INTERVAL2);
|
|
2999
|
+
}
|
|
3000
|
+
let flushBudgetMs;
|
|
3001
|
+
const flushBudgetRaw = env.EVIDENT_SESSION_ARCHIVE_FLUSH_MS ?? String(DEFAULT_FLUSH_BUDGET_MS);
|
|
3002
|
+
try {
|
|
3003
|
+
flushBudgetMs = parseFlushBudgetMs(flushBudgetRaw);
|
|
3004
|
+
} catch (err) {
|
|
3005
|
+
warnings.push(
|
|
3006
|
+
`Ignoring invalid EVIDENT_SESSION_ARCHIVE_FLUSH_MS, using default ${DEFAULT_FLUSH_BUDGET_MS}: ${err instanceof Error ? err.message : String(err)}`
|
|
3007
|
+
);
|
|
3008
|
+
flushBudgetMs = DEFAULT_FLUSH_BUDGET_MS;
|
|
3009
|
+
}
|
|
3010
|
+
const bucket = nonEmpty(env.PERSISTENCE_BUCKET);
|
|
3011
|
+
let keyPrefix = nonEmpty(env.LITESTREAM_PREFIX);
|
|
3012
|
+
if (enabled && bucket === null) {
|
|
3013
|
+
warnings.push("Session archiving is off because PERSISTENCE_BUCKET is unset or empty.");
|
|
3014
|
+
enabled = false;
|
|
3015
|
+
}
|
|
3016
|
+
if (enabled && keyPrefix === null) {
|
|
3017
|
+
let fallbackReadError = null;
|
|
3018
|
+
try {
|
|
3019
|
+
keyPrefix = nonEmpty(readFileSync5(STATE_PREFIX_FILE, "utf8"));
|
|
3020
|
+
} catch (err) {
|
|
3021
|
+
fallbackReadError = err instanceof Error ? err.message : String(err);
|
|
3022
|
+
}
|
|
3023
|
+
if (keyPrefix === null) {
|
|
3024
|
+
const readFailure = fallbackReadError ? ` Could not read ${STATE_PREFIX_FILE}: ${fallbackReadError}.` : "";
|
|
3025
|
+
warnings.push(
|
|
3026
|
+
`Session archiving is off because neither LITESTREAM_PREFIX nor ${STATE_PREFIX_FILE} contains a prefix.${readFailure}`
|
|
3027
|
+
);
|
|
3028
|
+
enabled = false;
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
return { enabled, bucket, keyPrefix, intervalMs, flushBudgetMs, warnings };
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
// src/lib/opencode/session-archive-snapshot.ts
|
|
3035
|
+
import { createHash } from "node:crypto";
|
|
3036
|
+
var SessionArchiveExportError = class extends Error {
|
|
3037
|
+
constructor(code, message) {
|
|
3038
|
+
super(message);
|
|
3039
|
+
this.code = code;
|
|
3040
|
+
this.name = "SessionArchiveExportError";
|
|
3041
|
+
}
|
|
3042
|
+
};
|
|
3043
|
+
function canonicalize(value) {
|
|
3044
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
3045
|
+
if (typeof value !== "object" || value === null) return value;
|
|
3046
|
+
return Object.fromEntries(
|
|
3047
|
+
Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, nested]) => [key, canonicalize(nested)])
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
3050
|
+
function contentForFingerprint(archive) {
|
|
3051
|
+
return JSON.stringify(
|
|
3052
|
+
canonicalize({
|
|
3053
|
+
opencode_api_version: archive.opencode_api_version,
|
|
3054
|
+
opencode_session_id: archive.opencode_session_id,
|
|
3055
|
+
evident_conversation_id: archive.evident_conversation_id,
|
|
3056
|
+
messages: archive.messages
|
|
3057
|
+
})
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
function fingerprintArchive(archive) {
|
|
3061
|
+
return createHash("sha256").update(contentForFingerprint(archive)).digest("hex");
|
|
3062
|
+
}
|
|
3063
|
+
function buildSessionArchive(input) {
|
|
3064
|
+
const archive = {
|
|
3065
|
+
schema_version: 1,
|
|
3066
|
+
opencode_api_version: input.apiVersion,
|
|
3067
|
+
opencode_session_id: input.session.id,
|
|
3068
|
+
evident_conversation_id: input.conversationId,
|
|
3069
|
+
evident_runner_id: input.runnerId,
|
|
3070
|
+
binding_source: input.bindingSource,
|
|
3071
|
+
session: input.session,
|
|
3072
|
+
messages: input.messages,
|
|
3073
|
+
message_count: input.messages.length,
|
|
3074
|
+
archived_at: input.archivedAt
|
|
3075
|
+
};
|
|
3076
|
+
return { ...archive, content_fingerprint: fingerprintArchive(archive) };
|
|
3077
|
+
}
|
|
3078
|
+
function isRecord(value) {
|
|
3079
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3080
|
+
}
|
|
3081
|
+
function errorMessage2(error2) {
|
|
3082
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
3083
|
+
}
|
|
3084
|
+
function invalidResponse(version2, sessionId, path, reason) {
|
|
3085
|
+
return new SessionArchiveExportError(
|
|
3086
|
+
"invalid-response",
|
|
3087
|
+
`OpenCode ${version2} session ${sessionId} returned an invalid response from GET ${path}: ${reason}`
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
async function requestPage(client, sessionId, path, signal) {
|
|
3091
|
+
let response;
|
|
3092
|
+
try {
|
|
3093
|
+
response = await client.request(path, signal ? { signal } : void 0, {
|
|
3094
|
+
allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100)
|
|
3095
|
+
});
|
|
3096
|
+
} catch (error2) {
|
|
3097
|
+
throw new SessionArchiveExportError(
|
|
3098
|
+
"request-failed",
|
|
3099
|
+
`OpenCode ${client.version} session ${sessionId} request GET ${path} failed: ${errorMessage2(error2)}`
|
|
3100
|
+
);
|
|
3101
|
+
}
|
|
3102
|
+
if (response.status === 404) {
|
|
3103
|
+
throw new SessionArchiveExportError(
|
|
3104
|
+
"not-found",
|
|
3105
|
+
`OpenCode ${client.version} session ${sessionId} was not found at GET ${path} (HTTP 404)`
|
|
3106
|
+
);
|
|
3107
|
+
}
|
|
3108
|
+
if (!response.ok) {
|
|
3109
|
+
throw new SessionArchiveExportError(
|
|
3110
|
+
"request-failed",
|
|
3111
|
+
`OpenCode ${client.version} session ${sessionId} request GET ${path} returned HTTP ${response.status}`
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
let bodyText;
|
|
3115
|
+
try {
|
|
3116
|
+
bodyText = await response.text();
|
|
3117
|
+
} catch (error2) {
|
|
3118
|
+
throw new SessionArchiveExportError(
|
|
3119
|
+
"request-failed",
|
|
3120
|
+
`OpenCode ${client.version} session ${sessionId} response body from GET ${path} could not be read: ${errorMessage2(error2)}`
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
try {
|
|
3124
|
+
return { response, body: JSON.parse(bodyText) };
|
|
3125
|
+
} catch (error2) {
|
|
3126
|
+
if (!(error2 instanceof SyntaxError)) throw error2;
|
|
3127
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
3128
|
+
const looksLikeHtml = contentType.toLowerCase().includes("text/html") || /^\s*<!doctype\s+html|^\s*<html/i.test(bodyText);
|
|
3129
|
+
throw invalidResponse(
|
|
3130
|
+
client.version,
|
|
3131
|
+
sessionId,
|
|
3132
|
+
path,
|
|
3133
|
+
looksLikeHtml ? "received text/html, expected JSON" : "received non-JSON content, expected JSON"
|
|
3134
|
+
);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
function v1NextCursor(response, sessionId, path) {
|
|
3138
|
+
const headerCursor = response.headers.get("x-next-cursor")?.trim() || null;
|
|
3139
|
+
const linkHeader = response.headers.get("link");
|
|
3140
|
+
const nextLink = linkHeader?.split(",").map((link) => link.trim()).find((link) => /;\s*rel\s*=\s*"?next"?/i.test(link));
|
|
3141
|
+
let linkCursor = null;
|
|
3142
|
+
if (nextLink) {
|
|
3143
|
+
const target = nextLink.match(/<([^>]+)>/)?.[1];
|
|
3144
|
+
if (!target) {
|
|
3145
|
+
throw new SessionArchiveExportError(
|
|
3146
|
+
"pagination-failed",
|
|
3147
|
+
`OpenCode v1 session ${sessionId} returned a next Link without a URL for GET ${path}`
|
|
3148
|
+
);
|
|
3149
|
+
}
|
|
3150
|
+
try {
|
|
3151
|
+
linkCursor = new URL(target, "http://127.0.0.1").searchParams.get("before");
|
|
3152
|
+
} catch (error2) {
|
|
3153
|
+
throw new SessionArchiveExportError(
|
|
3154
|
+
"pagination-failed",
|
|
3155
|
+
`OpenCode v1 session ${sessionId} returned an invalid next Link for GET ${path}: ${errorMessage2(error2)}`
|
|
3156
|
+
);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
if (headerCursor && linkCursor && headerCursor !== linkCursor) {
|
|
3160
|
+
throw new SessionArchiveExportError(
|
|
3161
|
+
"pagination-failed",
|
|
3162
|
+
`OpenCode v1 session ${sessionId} returned conflicting next cursors for GET ${path}`
|
|
3163
|
+
);
|
|
3164
|
+
}
|
|
3165
|
+
const cursor = headerCursor ?? linkCursor;
|
|
3166
|
+
if ((headerCursor || nextLink) && !cursor) {
|
|
3167
|
+
throw new SessionArchiveExportError(
|
|
3168
|
+
"pagination-failed",
|
|
3169
|
+
`OpenCode v1 session ${sessionId} returned a next page without a usable cursor for GET ${path}`
|
|
3170
|
+
);
|
|
3171
|
+
}
|
|
3172
|
+
return cursor;
|
|
3173
|
+
}
|
|
3174
|
+
function v1Messages(body, sessionId, path) {
|
|
3175
|
+
if (!Array.isArray(body)) {
|
|
3176
|
+
throw invalidResponse("v1", sessionId, path, "expected an array of messages");
|
|
3177
|
+
}
|
|
3178
|
+
return body;
|
|
3179
|
+
}
|
|
3180
|
+
async function exportV1SessionMessages(client, sessionId, signal) {
|
|
3181
|
+
const basePath = `/session/${encodeURIComponent(sessionId)}/message`;
|
|
3182
|
+
const firstPage = await requestPage(client, sessionId, basePath, signal);
|
|
3183
|
+
let messages = v1Messages(firstPage.body, sessionId, basePath);
|
|
3184
|
+
let cursor = v1NextCursor(firstPage.response, sessionId, basePath);
|
|
3185
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
3186
|
+
while (cursor !== null) {
|
|
3187
|
+
if (seenCursors.has(cursor)) {
|
|
3188
|
+
throw new SessionArchiveExportError(
|
|
3189
|
+
"pagination-failed",
|
|
3190
|
+
`OpenCode v1 session ${sessionId} repeated cursor while reading GET ${basePath}`
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
seenCursors.add(cursor);
|
|
3194
|
+
const path = `${basePath}?${new URLSearchParams({ before: cursor }).toString()}`;
|
|
3195
|
+
const page = await requestPage(client, sessionId, path, signal);
|
|
3196
|
+
messages = [...v1Messages(page.body, sessionId, path), ...messages];
|
|
3197
|
+
cursor = v1NextCursor(page.response, sessionId, path);
|
|
3198
|
+
}
|
|
3199
|
+
return messages;
|
|
3200
|
+
}
|
|
3201
|
+
async function exportV2SessionMessages(client, sessionId, signal) {
|
|
3202
|
+
const basePath = `/api/session/${encodeURIComponent(sessionId)}/message`;
|
|
3203
|
+
const messages = [];
|
|
3204
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
3205
|
+
let cursor;
|
|
3206
|
+
let hasNextPage = true;
|
|
3207
|
+
while (hasNextPage) {
|
|
3208
|
+
const params = new URLSearchParams({ limit: "100" });
|
|
3209
|
+
if (cursor === void 0) {
|
|
3210
|
+
params.set("order", "asc");
|
|
3211
|
+
} else {
|
|
3212
|
+
params.set("cursor", cursor);
|
|
3213
|
+
}
|
|
3214
|
+
const path = `${basePath}?${params.toString()}`;
|
|
3215
|
+
const { body } = await requestPage(client, sessionId, path, signal);
|
|
3216
|
+
if (!isRecord(body) || !Array.isArray(body.data) || !isRecord(body.cursor)) {
|
|
3217
|
+
throw invalidResponse("v2", sessionId, path, "expected a {data: [], cursor: {\u2026}} envelope");
|
|
3218
|
+
}
|
|
3219
|
+
messages.push(...body.data);
|
|
3220
|
+
const next = body.cursor.next;
|
|
3221
|
+
if (next === void 0 || next === null) {
|
|
3222
|
+
hasNextPage = false;
|
|
3223
|
+
continue;
|
|
3224
|
+
}
|
|
3225
|
+
if (typeof next !== "string" || next.length === 0 || seenCursors.has(next)) {
|
|
3226
|
+
throw new SessionArchiveExportError(
|
|
3227
|
+
"pagination-failed",
|
|
3228
|
+
`OpenCode v2 session ${sessionId} returned an invalid or repeated cursor from GET ${path}`
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
seenCursors.add(next);
|
|
3232
|
+
cursor = next;
|
|
3233
|
+
}
|
|
3234
|
+
return messages;
|
|
3235
|
+
}
|
|
3236
|
+
async function exportSessionMessages(client, sessionId, signal) {
|
|
3237
|
+
if (client.version === "v1") return exportV1SessionMessages(client, sessionId, signal);
|
|
3238
|
+
if (client.version === "v2") return exportV2SessionMessages(client, sessionId, signal);
|
|
3239
|
+
throw new SessionArchiveExportError(
|
|
3240
|
+
"unsupported-dialect",
|
|
3241
|
+
`OpenCode session ${sessionId} cannot be archived: unsupported API dialect ${String(client.version)}`
|
|
3242
|
+
);
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
// src/lib/opencode/session-archive-watermarks.ts
|
|
3246
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3247
|
+
import { dirname as dirname6, join as join6 } from "node:path";
|
|
3248
|
+
function sessionArchiveWatermarkPath(homeDir, env) {
|
|
3249
|
+
const override = env.EVIDENT_SESSION_ARCHIVE_WATERMARK?.trim();
|
|
3250
|
+
return override || join6(homeDir, ".local", "state", "evident", "session-archive-watermarks.json");
|
|
3251
|
+
}
|
|
3252
|
+
function readWatermarks(path, log3) {
|
|
3253
|
+
let content;
|
|
3254
|
+
try {
|
|
3255
|
+
content = readFileSync6(path, "utf8");
|
|
3256
|
+
} catch (error2) {
|
|
3257
|
+
if (isMissingFile3(error2)) return /* @__PURE__ */ new Map();
|
|
3258
|
+
log3(`[session-archive-watermarks] could not read ${path}; treating every session as changed`);
|
|
3259
|
+
return /* @__PURE__ */ new Map();
|
|
3260
|
+
}
|
|
3261
|
+
try {
|
|
3262
|
+
const parsed = JSON.parse(content);
|
|
3263
|
+
if (!isRecord2(parsed)) throw new Error("invalid watermark object");
|
|
3264
|
+
const entries = Object.entries(parsed);
|
|
3265
|
+
if (!entries.every(([sessionId, value]) => sessionId.length > 0 && isWatermark(value))) {
|
|
3266
|
+
throw new Error("invalid watermark entry");
|
|
3267
|
+
}
|
|
3268
|
+
return new Map(entries);
|
|
3269
|
+
} catch (error2) {
|
|
3270
|
+
log3(`[session-archive-watermarks] could not parse ${path}; treating every session as changed`);
|
|
3271
|
+
return /* @__PURE__ */ new Map();
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
function recordUpload(path, sessionId, entry, watermarks) {
|
|
3275
|
+
const updated = new Map(watermarks);
|
|
3276
|
+
updated.set(sessionId, entry);
|
|
3277
|
+
mkdirSync3(dirname6(path), { recursive: true });
|
|
3278
|
+
writeFileSync3(path, `${JSON.stringify(Object.fromEntries(updated))}
|
|
3279
|
+
`, {
|
|
3280
|
+
encoding: "utf8",
|
|
3281
|
+
mode: 384
|
|
3282
|
+
});
|
|
3283
|
+
watermarks.clear();
|
|
3284
|
+
for (const [id, watermark] of updated) watermarks.set(id, watermark);
|
|
3285
|
+
}
|
|
3286
|
+
function isRecord2(value) {
|
|
3287
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3288
|
+
}
|
|
3289
|
+
function isWatermark(value) {
|
|
3290
|
+
if (!isRecord2(value)) return false;
|
|
3291
|
+
return typeof value.fingerprint === "string" && typeof value.uploaded_at === "string" && typeof value.key === "string";
|
|
3292
|
+
}
|
|
3293
|
+
function isMissingFile3(error2) {
|
|
3294
|
+
return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
// src/lib/runner-synchroniser.ts
|
|
3298
|
+
import { spawn } from "node:child_process";
|
|
3299
|
+
|
|
3300
|
+
// ../../runner/synchroniser/src/shell-contract.json
|
|
3301
|
+
var shell_contract_default = {
|
|
3302
|
+
commands: [
|
|
3303
|
+
"env",
|
|
3304
|
+
"litestream-config",
|
|
3305
|
+
"restore",
|
|
3306
|
+
"sync-once",
|
|
3307
|
+
"model-auth-ready",
|
|
3308
|
+
"self-stop",
|
|
3309
|
+
"session-db-classify",
|
|
3310
|
+
"session-db-verify",
|
|
3311
|
+
"archive-put"
|
|
3312
|
+
],
|
|
3313
|
+
archiveStdinMaxBytes: 16777216
|
|
3314
|
+
};
|
|
3315
|
+
|
|
3316
|
+
// src/lib/runner-synchroniser.ts
|
|
3317
|
+
var MAX_ARCHIVE_STDIN_BYTES = shell_contract_default.archiveStdinMaxBytes;
|
|
3318
|
+
var ERROR_CLOSE_FALLBACK_MS = 1e3;
|
|
3319
|
+
function isPipeClosed(error2) {
|
|
3320
|
+
return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "EPIPE";
|
|
3321
|
+
}
|
|
3322
|
+
function appendError(stderr, error2) {
|
|
3323
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
3324
|
+
return stderr === "" ? message : `${stderr}
|
|
3325
|
+
${message}`;
|
|
3326
|
+
}
|
|
3327
|
+
function runSynchroniser(args, opts) {
|
|
3328
|
+
return new Promise((resolve5) => {
|
|
3329
|
+
let child;
|
|
3330
|
+
let stdout = "";
|
|
3331
|
+
let stderr = "";
|
|
3332
|
+
let settled = false;
|
|
3333
|
+
let shuttingDown = false;
|
|
3334
|
+
let timedOut = false;
|
|
3335
|
+
let stdinFailure;
|
|
3336
|
+
let childFailure;
|
|
3337
|
+
const timer = {};
|
|
3338
|
+
const closeFallbackTimer = {};
|
|
3339
|
+
let abortListener;
|
|
3340
|
+
let spawnListener;
|
|
3341
|
+
const finish = (result) => {
|
|
3342
|
+
if (settled) return;
|
|
3343
|
+
settled = true;
|
|
3344
|
+
if (timer.handle) clearTimeout(timer.handle);
|
|
3345
|
+
if (closeFallbackTimer.handle) clearTimeout(closeFallbackTimer.handle);
|
|
3346
|
+
if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
|
|
3347
|
+
if (spawnListener) child.removeListener("spawn", spawnListener);
|
|
3348
|
+
resolve5(result);
|
|
3349
|
+
};
|
|
3350
|
+
const armErrorCloseFallback = () => {
|
|
3351
|
+
if (closeFallbackTimer.handle) return;
|
|
3352
|
+
closeFallbackTimer.handle = setTimeout(() => {
|
|
3353
|
+
const failure = stdinFailure ?? childFailure;
|
|
3354
|
+
finish({
|
|
3355
|
+
code: null,
|
|
3356
|
+
stdout,
|
|
3357
|
+
stderr: failure === void 0 ? stderr : appendError(stderr, failure),
|
|
3358
|
+
timedOut
|
|
3359
|
+
});
|
|
3360
|
+
}, ERROR_CLOSE_FALLBACK_MS);
|
|
3361
|
+
};
|
|
3362
|
+
const terminateChild = () => {
|
|
3363
|
+
if (!shuttingDown) {
|
|
3364
|
+
shuttingDown = true;
|
|
3365
|
+
child.stdin?.destroy();
|
|
3366
|
+
}
|
|
3367
|
+
if (child.pid !== void 0) {
|
|
3368
|
+
child.kill("SIGKILL");
|
|
3369
|
+
} else if (!spawnListener) {
|
|
3370
|
+
spawnListener = () => {
|
|
3371
|
+
if (child.pid !== void 0) child.kill("SIGKILL");
|
|
3372
|
+
};
|
|
3373
|
+
child.once("spawn", spawnListener);
|
|
3374
|
+
}
|
|
3375
|
+
};
|
|
3376
|
+
try {
|
|
3377
|
+
child = spawn("runner-synchroniser", args, {
|
|
3378
|
+
env: opts.env ?? process.env,
|
|
3379
|
+
stdio: [opts.stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"]
|
|
3380
|
+
});
|
|
3381
|
+
} catch (error2) {
|
|
3382
|
+
finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
child.stdout?.setEncoding("utf8");
|
|
3386
|
+
child.stdout?.on("data", (chunk) => {
|
|
3387
|
+
stdout += chunk;
|
|
3388
|
+
});
|
|
3389
|
+
child.stderr?.setEncoding("utf8");
|
|
3390
|
+
child.stderr?.on("data", (chunk) => {
|
|
3391
|
+
stderr += chunk;
|
|
3392
|
+
});
|
|
3393
|
+
child.once("error", (error2) => {
|
|
3394
|
+
childFailure = error2;
|
|
3395
|
+
if (child.pid !== void 0) terminateChild();
|
|
3396
|
+
armErrorCloseFallback();
|
|
3397
|
+
});
|
|
3398
|
+
child.once("close", (code) => {
|
|
3399
|
+
const failure = stdinFailure ?? childFailure;
|
|
3400
|
+
finish({
|
|
3401
|
+
code: failure === void 0 ? code : null,
|
|
3402
|
+
stdout,
|
|
3403
|
+
stderr: failure === void 0 ? stderr : appendError(stderr, failure),
|
|
3404
|
+
timedOut
|
|
3405
|
+
});
|
|
3406
|
+
});
|
|
3407
|
+
const stdin = child.stdin;
|
|
3408
|
+
const failStdin = (error2) => {
|
|
3409
|
+
if (settled || shuttingDown || isPipeClosed(error2)) return;
|
|
3410
|
+
stdinFailure = error2;
|
|
3411
|
+
terminateChild();
|
|
3412
|
+
armErrorCloseFallback();
|
|
3413
|
+
};
|
|
3414
|
+
stdin?.once("error", failStdin);
|
|
3415
|
+
if (opts.stdin !== void 0 && stdin === null) {
|
|
3416
|
+
failStdin(new Error("synchroniser stdin is unavailable"));
|
|
3417
|
+
} else if (opts.stdin !== void 0 && stdin !== null) {
|
|
3418
|
+
const drained = stdin.write(opts.stdin, (error2) => {
|
|
3419
|
+
if (error2) failStdin(error2);
|
|
3420
|
+
});
|
|
3421
|
+
if (!drained) {
|
|
3422
|
+
stdin.once("drain", () => stdin.end());
|
|
3423
|
+
} else {
|
|
3424
|
+
stdin.end();
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
if (opts.signal) {
|
|
3428
|
+
const killChild = () => {
|
|
3429
|
+
terminateChild();
|
|
3430
|
+
};
|
|
3431
|
+
abortListener = killChild;
|
|
3432
|
+
if (opts.signal.aborted) {
|
|
3433
|
+
abortListener();
|
|
3434
|
+
} else {
|
|
3435
|
+
opts.signal.addEventListener("abort", abortListener, { once: true });
|
|
3436
|
+
if (opts.signal.aborted) abortListener();
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
timer.handle = setTimeout(
|
|
3440
|
+
() => {
|
|
3441
|
+
timedOut = true;
|
|
3442
|
+
terminateChild();
|
|
3443
|
+
},
|
|
3444
|
+
Math.max(0, opts.timeoutMs)
|
|
3445
|
+
);
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
// src/lib/opencode/session-archive-upload.ts
|
|
3450
|
+
var MAX_SESSION_ARCHIVE_BYTES = MAX_ARCHIVE_STDIN_BYTES;
|
|
3451
|
+
var EXIT_ARCHIVE_CONFIG_MISSING = 50;
|
|
3452
|
+
var EXIT_ARCHIVE_ACCESS_DENIED = 51;
|
|
3453
|
+
function transportFailure(bytes, reason) {
|
|
3454
|
+
const message = reason === "missing-executable" ? "runner-synchroniser is unavailable; session archival requires a hosted runner." : reason === "timeout" ? "runner-synchroniser timed out before the archive upload completed." : "session archive upload did not complete.";
|
|
3455
|
+
return { kind: "transport-failed", bytes, reason, message };
|
|
3456
|
+
}
|
|
3457
|
+
function isJson(body) {
|
|
3458
|
+
try {
|
|
3459
|
+
JSON.parse(body.toString("utf8"));
|
|
3460
|
+
return true;
|
|
3461
|
+
} catch (error2) {
|
|
3462
|
+
void error2;
|
|
3463
|
+
return false;
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
async function uploadSessionArchive(key, body, options) {
|
|
3467
|
+
const bytes = Buffer.from(body);
|
|
3468
|
+
if (bytes.byteLength > MAX_SESSION_ARCHIVE_BYTES) {
|
|
3469
|
+
return transportFailure(bytes.byteLength, "too-large");
|
|
3470
|
+
}
|
|
3471
|
+
if (!isJson(bytes)) return transportFailure(bytes.byteLength, "invalid-json");
|
|
3472
|
+
const run2 = options.runSynchroniser ?? runSynchroniser;
|
|
3473
|
+
let result;
|
|
3474
|
+
try {
|
|
3475
|
+
result = await run2(["archive-put", key], {
|
|
3476
|
+
timeoutMs: options.timeoutMs,
|
|
3477
|
+
env: options.env,
|
|
3478
|
+
signal: options.signal,
|
|
3479
|
+
stdin: bytes
|
|
3480
|
+
});
|
|
3481
|
+
} catch (error2) {
|
|
3482
|
+
return transportFailure(
|
|
3483
|
+
bytes.byteLength,
|
|
3484
|
+
error2 instanceof Error && /ENOENT|runner-synchroniser/i.test(error2.message) ? "missing-executable" : "spawn-failed"
|
|
3485
|
+
);
|
|
3486
|
+
}
|
|
3487
|
+
if (result.timedOut) return transportFailure(bytes.byteLength, "timeout");
|
|
3488
|
+
if (result.code === 0) return { kind: "ok", bytes: bytes.byteLength };
|
|
3489
|
+
if (result.code === EXIT_ARCHIVE_CONFIG_MISSING) {
|
|
3490
|
+
return { kind: "config-missing", bytes: bytes.byteLength };
|
|
3491
|
+
}
|
|
3492
|
+
if (result.code === EXIT_ARCHIVE_ACCESS_DENIED) {
|
|
3493
|
+
return { kind: "access-denied", bytes: bytes.byteLength };
|
|
3494
|
+
}
|
|
3495
|
+
if (result.code === null && /ENOENT|runner-synchroniser/i.test(result.stderr)) {
|
|
3496
|
+
return transportFailure(bytes.byteLength, "missing-executable");
|
|
3497
|
+
}
|
|
3498
|
+
return transportFailure(
|
|
3499
|
+
bytes.byteLength,
|
|
3500
|
+
result.code === null ? "spawn-failed" : "process-failed"
|
|
3501
|
+
);
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
// src/lib/opencode/session-archive-run.ts
|
|
3505
|
+
var SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS = 6e4;
|
|
3506
|
+
function updatedAt(session) {
|
|
3507
|
+
const updated = session.time?.updated;
|
|
3508
|
+
return typeof updated === "number" && Number.isFinite(updated) ? updated : -Infinity;
|
|
3509
|
+
}
|
|
3510
|
+
function sessionArchiveKey(prefix, sessionId) {
|
|
3511
|
+
return `${prefix.replace(/\/+$/, "")}/conversation-archives/${encodeURIComponent(sessionId)}.json`;
|
|
3512
|
+
}
|
|
3513
|
+
function failureMessage(error2) {
|
|
3514
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
3515
|
+
}
|
|
3516
|
+
function isBudgetExpired(deadline, now, signal) {
|
|
3517
|
+
return signal?.aborted === true || now() >= deadline;
|
|
3518
|
+
}
|
|
3519
|
+
function createBudgetSignal(parent, timeoutMs) {
|
|
3520
|
+
const controller = new AbortController();
|
|
3521
|
+
const forwardAbort = () => controller.abort(parent?.reason);
|
|
3522
|
+
if (parent?.aborted) forwardAbort();
|
|
3523
|
+
else parent?.addEventListener("abort", forwardAbort, { once: true });
|
|
3524
|
+
const timer = setTimeout(
|
|
3525
|
+
() => controller.abort(new Error("session archive budget exceeded")),
|
|
3526
|
+
timeoutMs
|
|
3527
|
+
);
|
|
3528
|
+
return {
|
|
3529
|
+
signal: controller.signal,
|
|
3530
|
+
dispose: () => {
|
|
3531
|
+
clearTimeout(timer);
|
|
3532
|
+
parent?.removeEventListener("abort", forwardAbort);
|
|
3533
|
+
}
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
async function runSessionArchive({
|
|
3537
|
+
sessions,
|
|
3538
|
+
deps,
|
|
3539
|
+
budgetMs,
|
|
3540
|
+
now = Date.now
|
|
3541
|
+
}) {
|
|
3542
|
+
const startedAt = now();
|
|
3543
|
+
const deadline = startedAt + Math.max(0, budgetMs);
|
|
3544
|
+
const summary = {
|
|
3545
|
+
inspected: sessions.length,
|
|
3546
|
+
uploaded: 0,
|
|
3547
|
+
unchanged: 0,
|
|
3548
|
+
failed: 0,
|
|
3549
|
+
uploadAttempts: 0,
|
|
3550
|
+
uploadFailures: 0,
|
|
3551
|
+
skippedBudget: 0,
|
|
3552
|
+
skippedBudgetIds: [],
|
|
3553
|
+
neverWatermarkedFailures: 0,
|
|
3554
|
+
unmappedIds: [],
|
|
3555
|
+
archivedSessionIds: [],
|
|
3556
|
+
failures: [],
|
|
3557
|
+
staleSince: null
|
|
3558
|
+
};
|
|
3559
|
+
const ordered = [...sessions].sort((left, right) => updatedAt(right) - updatedAt(left));
|
|
3560
|
+
const upload = deps.upload ?? uploadSessionArchive;
|
|
3561
|
+
const exportMessages = deps.exportMessages ?? exportSessionMessages;
|
|
3562
|
+
const writeWatermark = deps.recordWatermark ?? recordUpload;
|
|
3563
|
+
const failedWatermarks = [];
|
|
3564
|
+
const recordFailedWatermark = (watermark) => {
|
|
3565
|
+
if (watermark) failedWatermarks.push(watermark.uploaded_at);
|
|
3566
|
+
else summary.neverWatermarkedFailures++;
|
|
3567
|
+
};
|
|
3568
|
+
for (let index = 0; index < ordered.length; index++) {
|
|
3569
|
+
const session = ordered[index];
|
|
3570
|
+
const remainingMs = Math.floor(deadline - now());
|
|
3571
|
+
if (remainingMs <= 0 || deps.signal?.aborted) {
|
|
3572
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3573
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3574
|
+
break;
|
|
3575
|
+
}
|
|
3576
|
+
const conversationId = deps.bindings.get(session.id) ?? null;
|
|
3577
|
+
if (conversationId === null) summary.unmappedIds.push(session.id);
|
|
3578
|
+
const exportBudget = createBudgetSignal(deps.signal, remainingMs);
|
|
3579
|
+
let messages;
|
|
3580
|
+
try {
|
|
3581
|
+
messages = await exportMessages(deps.client, session.id, exportBudget.signal);
|
|
3582
|
+
} catch (error2) {
|
|
3583
|
+
if (isBudgetExpired(deadline, now, deps.signal) || exportBudget.signal.aborted) {
|
|
3584
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3585
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3586
|
+
break;
|
|
3587
|
+
}
|
|
3588
|
+
summary.failed++;
|
|
3589
|
+
const reason = error2 instanceof SessionArchiveExportError ? error2.code : "export-failed";
|
|
3590
|
+
summary.failures.push({
|
|
3591
|
+
sessionId: session.id,
|
|
3592
|
+
outcome: "export-failed",
|
|
3593
|
+
reason,
|
|
3594
|
+
message: failureMessage(error2),
|
|
3595
|
+
archived: false
|
|
3596
|
+
});
|
|
3597
|
+
recordFailedWatermark(deps.watermarks.get(session.id));
|
|
3598
|
+
continue;
|
|
3599
|
+
} finally {
|
|
3600
|
+
exportBudget.dispose();
|
|
3601
|
+
}
|
|
3602
|
+
if (isBudgetExpired(deadline, now, deps.signal) || exportBudget.signal.aborted) {
|
|
3603
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3604
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3605
|
+
break;
|
|
3606
|
+
}
|
|
3607
|
+
try {
|
|
3608
|
+
const archivedAt = new Date(now()).toISOString();
|
|
3609
|
+
const snapshot = buildSessionArchive({
|
|
3610
|
+
session,
|
|
3611
|
+
messages,
|
|
3612
|
+
apiVersion: deps.client.version,
|
|
3613
|
+
conversationId,
|
|
3614
|
+
bindingSource: conversationId === null ? "unmapped" : "ledger",
|
|
3615
|
+
runnerId: deps.runnerId,
|
|
3616
|
+
archivedAt
|
|
3617
|
+
});
|
|
3618
|
+
const key = sessionArchiveKey(deps.keyPrefix, session.id);
|
|
3619
|
+
const existing = deps.watermarks.get(session.id);
|
|
3620
|
+
if (existing?.fingerprint === snapshot.content_fingerprint) {
|
|
3621
|
+
summary.unchanged++;
|
|
3622
|
+
summary.archivedSessionIds.push(session.id);
|
|
3623
|
+
continue;
|
|
3624
|
+
}
|
|
3625
|
+
const uploadRemainingMs = Math.floor(deadline - now());
|
|
3626
|
+
if (uploadRemainingMs <= 0 || deps.signal?.aborted) {
|
|
3627
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3628
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3629
|
+
break;
|
|
3630
|
+
}
|
|
3631
|
+
summary.uploadAttempts++;
|
|
3632
|
+
const uploadBudget = createBudgetSignal(deps.signal, uploadRemainingMs);
|
|
3633
|
+
let outcome;
|
|
3634
|
+
try {
|
|
3635
|
+
outcome = await upload(key, JSON.stringify(snapshot), {
|
|
3636
|
+
timeoutMs: Math.min(uploadRemainingMs, SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS),
|
|
3637
|
+
signal: uploadBudget.signal
|
|
3638
|
+
});
|
|
3639
|
+
} finally {
|
|
3640
|
+
uploadBudget.dispose();
|
|
3641
|
+
}
|
|
3642
|
+
if (outcome.kind !== "ok") {
|
|
3643
|
+
summary.failed++;
|
|
3644
|
+
summary.uploadFailures++;
|
|
3645
|
+
const reason = "reason" in outcome ? outcome.reason : void 0;
|
|
3646
|
+
summary.failures.push({
|
|
3647
|
+
sessionId: session.id,
|
|
3648
|
+
outcome: outcome.kind,
|
|
3649
|
+
...reason !== void 0 ? { reason } : {},
|
|
3650
|
+
..."message" in outcome ? { message: outcome.message } : {},
|
|
3651
|
+
archived: false
|
|
3652
|
+
});
|
|
3653
|
+
recordFailedWatermark(existing);
|
|
3654
|
+
if (uploadBudget.signal.aborted) {
|
|
3655
|
+
summary.skippedBudgetIds.push(...ordered.slice(index + 1).map(({ id }) => id));
|
|
3656
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3657
|
+
break;
|
|
3658
|
+
}
|
|
3659
|
+
continue;
|
|
3660
|
+
}
|
|
3661
|
+
summary.uploaded++;
|
|
3662
|
+
summary.archivedSessionIds.push(session.id);
|
|
3663
|
+
const watermark = {
|
|
3664
|
+
fingerprint: snapshot.content_fingerprint,
|
|
3665
|
+
uploaded_at: new Date(now()).toISOString(),
|
|
3666
|
+
key
|
|
3667
|
+
};
|
|
3668
|
+
try {
|
|
3669
|
+
writeWatermark(deps.watermarkPath, session.id, watermark, deps.watermarks);
|
|
3670
|
+
} catch (error2) {
|
|
3671
|
+
summary.failures.push({
|
|
3672
|
+
sessionId: session.id,
|
|
3673
|
+
outcome: "watermark-write-failed",
|
|
3674
|
+
message: failureMessage(error2),
|
|
3675
|
+
archived: true
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
3678
|
+
} catch (error2) {
|
|
3679
|
+
if (isBudgetExpired(deadline, now, deps.signal)) {
|
|
3680
|
+
summary.skippedBudgetIds.push(...ordered.slice(index).map(({ id }) => id));
|
|
3681
|
+
summary.skippedBudget = summary.skippedBudgetIds.length;
|
|
3682
|
+
break;
|
|
3683
|
+
}
|
|
3684
|
+
summary.failed++;
|
|
3685
|
+
summary.failures.push({
|
|
3686
|
+
sessionId: session.id,
|
|
3687
|
+
outcome: "archive-failed",
|
|
3688
|
+
message: failureMessage(error2),
|
|
3689
|
+
archived: false
|
|
3690
|
+
});
|
|
3691
|
+
recordFailedWatermark(deps.watermarks.get(session.id));
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
summary.staleSince = failedWatermarks.sort()[0] ?? null;
|
|
3695
|
+
return summary;
|
|
3696
|
+
}
|
|
3697
|
+
|
|
2760
3698
|
// src/lib/opencode/auth.ts
|
|
2761
3699
|
function buildOpenCodeBasicAuthHeader(password) {
|
|
2762
3700
|
return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
|
|
@@ -2775,8 +3713,26 @@ async function checkOpenCodeHealth(port) {
|
|
|
2775
3713
|
if (!response.ok) {
|
|
2776
3714
|
return { healthy: false, error: `HTTP ${response.status}` };
|
|
2777
3715
|
}
|
|
2778
|
-
const
|
|
2779
|
-
|
|
3716
|
+
const body = await response.text();
|
|
3717
|
+
let data;
|
|
3718
|
+
try {
|
|
3719
|
+
data = JSON.parse(body);
|
|
3720
|
+
} catch (error2) {
|
|
3721
|
+
return {
|
|
3722
|
+
healthy: false,
|
|
3723
|
+
okWithNonJsonBody: true,
|
|
3724
|
+
error: `HTTP ${response.status}: invalid JSON health response${bodyExcerpt(body)}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3725
|
+
};
|
|
3726
|
+
}
|
|
3727
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
3728
|
+
return {
|
|
3729
|
+
healthy: false,
|
|
3730
|
+
okWithNonJsonBody: true,
|
|
3731
|
+
error: `HTTP ${response.status}: invalid health response${bodyExcerpt(body)}: expected JSON object`
|
|
3732
|
+
};
|
|
3733
|
+
}
|
|
3734
|
+
const version2 = data.version;
|
|
3735
|
+
return typeof version2 === "string" ? { healthy: true, version: version2 } : { healthy: true };
|
|
2780
3736
|
} catch (error2) {
|
|
2781
3737
|
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
2782
3738
|
return { healthy: false, error: message };
|
|
@@ -2792,10 +3748,17 @@ async function checkOpenCode2Health(port, password) {
|
|
|
2792
3748
|
if (password !== void 0) {
|
|
2793
3749
|
headers.Authorization = buildOpenCodeBasicAuthHeader(password);
|
|
2794
3750
|
}
|
|
2795
|
-
|
|
3751
|
+
let response = await fetch(`http://127.0.0.1:${port}/api/info`, {
|
|
2796
3752
|
headers,
|
|
2797
3753
|
signal: AbortSignal.timeout(2e3)
|
|
2798
3754
|
});
|
|
3755
|
+
const usesHealthFallback = response.status === 404;
|
|
3756
|
+
if (usesHealthFallback) {
|
|
3757
|
+
response = await fetch(`http://127.0.0.1:${port}/api/health`, {
|
|
3758
|
+
headers,
|
|
3759
|
+
signal: AbortSignal.timeout(2e3)
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
2799
3762
|
if (response.status === 401) {
|
|
2800
3763
|
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
2801
3764
|
}
|
|
@@ -2810,23 +3773,29 @@ async function checkOpenCode2Health(port, password) {
|
|
|
2810
3773
|
} catch (error2) {
|
|
2811
3774
|
return {
|
|
2812
3775
|
healthy: false,
|
|
2813
|
-
error: `
|
|
3776
|
+
error: `HTTP ${response.status}: invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2814
3777
|
};
|
|
2815
3778
|
}
|
|
2816
3779
|
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
2817
3780
|
return {
|
|
2818
3781
|
healthy: false,
|
|
2819
|
-
error: `
|
|
3782
|
+
error: `HTTP ${response.status}: invalid health response${excerpt}: expected healthy=true`
|
|
2820
3783
|
};
|
|
2821
3784
|
}
|
|
2822
|
-
const
|
|
2823
|
-
if (
|
|
3785
|
+
const bodyObject = data;
|
|
3786
|
+
if (usesHealthFallback && bodyObject.healthy !== true) {
|
|
2824
3787
|
return {
|
|
2825
3788
|
healthy: false,
|
|
2826
3789
|
error: `Invalid health response${excerpt}: expected healthy=true`
|
|
2827
3790
|
};
|
|
2828
3791
|
}
|
|
2829
|
-
|
|
3792
|
+
if (!usesHealthFallback) {
|
|
3793
|
+
return typeof bodyObject.version === "string" && bodyObject.version.length > 0 ? { healthy: true, version: bodyObject.version } : {
|
|
3794
|
+
healthy: false,
|
|
3795
|
+
error: `HTTP ${response.status}: invalid info response${excerpt}: expected a non-empty version`
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3798
|
+
return typeof bodyObject.version === "string" ? { healthy: true, version: bodyObject.version } : { healthy: true };
|
|
2830
3799
|
} catch (error2) {
|
|
2831
3800
|
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
2832
3801
|
return { healthy: false, error: message };
|
|
@@ -2884,10 +3853,18 @@ function describeRequestOperation(input, init) {
|
|
|
2884
3853
|
function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
2885
3854
|
return (async (input, init) => {
|
|
2886
3855
|
const startedAt = Date.now();
|
|
3856
|
+
const controller = new AbortController();
|
|
3857
|
+
const abortFromCaller = () => controller.abort(init?.signal?.reason);
|
|
3858
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
3859
|
+
const abortFromTimeout = () => controller.abort(timeoutSignal.reason);
|
|
3860
|
+
if (init?.signal?.aborted) abortFromCaller();
|
|
3861
|
+
else init?.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
3862
|
+
if (timeoutSignal.aborted) abortFromTimeout();
|
|
3863
|
+
else timeoutSignal.addEventListener("abort", abortFromTimeout, { once: true });
|
|
2887
3864
|
try {
|
|
2888
|
-
return await fetchImpl(input, { ...init, signal:
|
|
3865
|
+
return await fetchImpl(input, { ...init, signal: controller.signal });
|
|
2889
3866
|
} catch (error2) {
|
|
2890
|
-
if (
|
|
3867
|
+
if (timeoutSignal.aborted) {
|
|
2891
3868
|
throw new RequestTimeoutError(
|
|
2892
3869
|
describeRequestOperation(input, init),
|
|
2893
3870
|
timeoutMs,
|
|
@@ -2895,11 +3872,21 @@ function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
|
2895
3872
|
);
|
|
2896
3873
|
}
|
|
2897
3874
|
throw error2;
|
|
3875
|
+
} finally {
|
|
3876
|
+
init?.signal?.removeEventListener("abort", abortFromCaller);
|
|
3877
|
+
timeoutSignal.removeEventListener("abort", abortFromTimeout);
|
|
2898
3878
|
}
|
|
2899
3879
|
});
|
|
2900
3880
|
}
|
|
2901
3881
|
|
|
2902
3882
|
// src/lib/opencode/client.ts
|
|
3883
|
+
var OpenCodeRequestError = class extends Error {
|
|
3884
|
+
constructor(statusCode, message) {
|
|
3885
|
+
super(message);
|
|
3886
|
+
this.statusCode = statusCode;
|
|
3887
|
+
this.name = "OpenCodeRequestError";
|
|
3888
|
+
}
|
|
3889
|
+
};
|
|
2903
3890
|
function redactPassword(message, password) {
|
|
2904
3891
|
return message.replaceAll(password, "[redacted]");
|
|
2905
3892
|
}
|
|
@@ -2921,7 +3908,8 @@ function createOpenCodeClient(options) {
|
|
|
2921
3908
|
const response = await fetchImpl(`${baseUrl}${path}`, requestInit);
|
|
2922
3909
|
if (!response.ok && !requestOptions?.allowStatuses?.includes(response.status)) {
|
|
2923
3910
|
const body = await response.text();
|
|
2924
|
-
throw new
|
|
3911
|
+
throw new OpenCodeRequestError(
|
|
3912
|
+
response.status,
|
|
2925
3913
|
`OpenCode request failed: HTTP ${response.status}${body ? `: ${body}` : ""}`
|
|
2926
3914
|
);
|
|
2927
3915
|
}
|
|
@@ -2929,7 +3917,11 @@ function createOpenCodeClient(options) {
|
|
|
2929
3917
|
} catch (error2) {
|
|
2930
3918
|
if (options.version === "v2" && password !== null) {
|
|
2931
3919
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2932
|
-
|
|
3920
|
+
const redactedMessage = redactPassword(message, password);
|
|
3921
|
+
if (error2 instanceof OpenCodeRequestError) {
|
|
3922
|
+
throw new OpenCodeRequestError(error2.statusCode, redactedMessage);
|
|
3923
|
+
}
|
|
3924
|
+
throw new Error(redactedMessage);
|
|
2933
3925
|
}
|
|
2934
3926
|
throw error2;
|
|
2935
3927
|
}
|
|
@@ -3078,28 +4070,38 @@ function sessionListPath(options) {
|
|
|
3078
4070
|
async function listSessionPage(port, client, options) {
|
|
3079
4071
|
const path = sessionListPath(options);
|
|
3080
4072
|
try {
|
|
3081
|
-
const res = await requestWithClient(
|
|
3082
|
-
|
|
4073
|
+
const res = await requestWithClient(
|
|
4074
|
+
port,
|
|
4075
|
+
client,
|
|
4076
|
+
path,
|
|
4077
|
+
options?.signal ? { signal: options.signal } : void 0,
|
|
4078
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
4079
|
+
);
|
|
4080
|
+
if (!res.ok) {
|
|
4081
|
+
console.error(`[listSessionPage] GET ${path} returned HTTP ${res.status} (port ${port})`);
|
|
4082
|
+
return null;
|
|
4083
|
+
}
|
|
3083
4084
|
const body = await res.json();
|
|
3084
4085
|
return Array.isArray(body) ? body : null;
|
|
3085
4086
|
} catch (error2) {
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
);
|
|
4087
|
+
if (options?.signal?.aborted) return null;
|
|
4088
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
4089
|
+
console.error(`[listSessionPage] GET ${path} failed (port ${port}): ${errorName}`);
|
|
3089
4090
|
return null;
|
|
3090
4091
|
}
|
|
3091
4092
|
}
|
|
3092
4093
|
async function listSessions(port, client, options) {
|
|
3093
|
-
if (client?.version === "v2") return listV2Sessions(client);
|
|
4094
|
+
if (client?.version === "v2") return listV2Sessions(client, options?.signal);
|
|
3094
4095
|
return listSessionPage(port, client, options);
|
|
3095
4096
|
}
|
|
3096
|
-
async function listAllSessions(port, client) {
|
|
4097
|
+
async function listAllSessions(port, client, signal) {
|
|
3097
4098
|
if (client?.version === "v2") {
|
|
3098
|
-
const sessions2 = await listV2Sessions(client);
|
|
4099
|
+
const sessions2 = await listV2Sessions(client, signal);
|
|
3099
4100
|
return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
|
|
3100
4101
|
}
|
|
3101
4102
|
const sessions = await listSessionPage(port, client, {
|
|
3102
|
-
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
|
|
4103
|
+
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS,
|
|
4104
|
+
signal
|
|
3103
4105
|
});
|
|
3104
4106
|
if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
|
|
3105
4107
|
if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
|
|
@@ -3595,21 +4597,24 @@ function findLastAssistantReplyFor(messages, userMessageId) {
|
|
|
3595
4597
|
}
|
|
3596
4598
|
return last;
|
|
3597
4599
|
}
|
|
4600
|
+
function assistantMessagesInTurnBlock(messages, userMessageId) {
|
|
4601
|
+
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
4602
|
+
if (userIndex === -1) return [];
|
|
4603
|
+
const assistants = [];
|
|
4604
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
4605
|
+
const message = messages[i];
|
|
4606
|
+
const role = roleOf2(message);
|
|
4607
|
+
if (role === "user") break;
|
|
4608
|
+
if (role === "assistant") assistants.push(message);
|
|
4609
|
+
}
|
|
4610
|
+
return assistants;
|
|
4611
|
+
}
|
|
3598
4612
|
function collectSubagentSessions(messages, userMessageId) {
|
|
3599
4613
|
if (!messages || messages.length === 0) return [];
|
|
3600
4614
|
const byParent = messages.filter(
|
|
3601
4615
|
(message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
|
|
3602
4616
|
);
|
|
3603
|
-
const assistants = byParent.length > 0 ? byParent :
|
|
3604
|
-
if (assistants.length === 0) {
|
|
3605
|
-
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
3606
|
-
if (userIndex === -1) return [];
|
|
3607
|
-
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
3608
|
-
const message = messages[i];
|
|
3609
|
-
if (roleOf2(message) === "user") break;
|
|
3610
|
-
if (roleOf2(message) === "assistant") assistants.push(message);
|
|
3611
|
-
}
|
|
3612
|
-
}
|
|
4617
|
+
const assistants = byParent.length > 0 ? byParent : assistantMessagesInTurnBlock(messages, userMessageId);
|
|
3613
4618
|
const refs = [];
|
|
3614
4619
|
const seen = /* @__PURE__ */ new Set();
|
|
3615
4620
|
for (const message of assistants) {
|
|
@@ -3764,8 +4769,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
3764
4769
|
if (byParentAll.length > 0) {
|
|
3765
4770
|
correlated = byParentAll;
|
|
3766
4771
|
} else {
|
|
3767
|
-
|
|
3768
|
-
correlated = reply ? [reply] : [];
|
|
4772
|
+
correlated = assistantMessagesInTurnBlock(messages, userMessageId);
|
|
3769
4773
|
}
|
|
3770
4774
|
if (correlated.length === 0) return null;
|
|
3771
4775
|
return sumAssistantUsage(correlated);
|
|
@@ -3900,8 +4904,8 @@ function classifyReplyAuthError(reply) {
|
|
|
3900
4904
|
};
|
|
3901
4905
|
}
|
|
3902
4906
|
}
|
|
3903
|
-
const
|
|
3904
|
-
if (
|
|
4907
|
+
const errorMessage4 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
|
|
4908
|
+
if (errorMessage4 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage4.includes(knownMessage))) {
|
|
3905
4909
|
return {
|
|
3906
4910
|
kind: "model_auth",
|
|
3907
4911
|
providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
|
|
@@ -3987,39 +4991,85 @@ async function hasAnyConfiguredProvider(port, client) {
|
|
|
3987
4991
|
);
|
|
3988
4992
|
return null;
|
|
3989
4993
|
}
|
|
3990
|
-
const
|
|
4994
|
+
const query = `?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
|
|
4995
|
+
const integrationPath = `/api/integration${query}`;
|
|
4996
|
+
let integrationReady = null;
|
|
3991
4997
|
try {
|
|
3992
|
-
const res = await client.request(
|
|
4998
|
+
const res = await client.request(integrationPath);
|
|
3993
4999
|
if (!res.ok) {
|
|
3994
5000
|
console.error(
|
|
3995
|
-
`[hasAnyConfiguredProvider] GET ${
|
|
5001
|
+
`[hasAnyConfiguredProvider] GET ${integrationPath} returned HTTP ${res.status} (port ${port})`
|
|
3996
5002
|
);
|
|
3997
|
-
|
|
5003
|
+
} else {
|
|
5004
|
+
const body = await res.json();
|
|
5005
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
|
|
5006
|
+
console.error(
|
|
5007
|
+
`[hasAnyConfiguredProvider] GET ${integrationPath} body had no integration data array (port ${port})`
|
|
5008
|
+
);
|
|
5009
|
+
} else {
|
|
5010
|
+
let valid = true;
|
|
5011
|
+
for (const integration of body.data) {
|
|
5012
|
+
if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
5013
|
+
console.error(
|
|
5014
|
+
`[hasAnyConfiguredProvider] GET ${integrationPath} body contained an invalid integration (port ${port})`
|
|
5015
|
+
);
|
|
5016
|
+
valid = false;
|
|
5017
|
+
break;
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
if (valid) {
|
|
5021
|
+
integrationReady = body.data.some(
|
|
5022
|
+
(integration) => integration.connections.length > 0
|
|
5023
|
+
);
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
3998
5026
|
}
|
|
3999
|
-
|
|
4000
|
-
|
|
5027
|
+
} catch (error2) {
|
|
5028
|
+
console.error(
|
|
5029
|
+
`[hasAnyConfiguredProvider] GET ${integrationPath} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
5030
|
+
);
|
|
5031
|
+
}
|
|
5032
|
+
if (integrationReady === true) return true;
|
|
5033
|
+
const providerPath = `/api/provider${query}`;
|
|
5034
|
+
let providerReady = null;
|
|
5035
|
+
try {
|
|
5036
|
+
const res = await client.request(providerPath);
|
|
5037
|
+
if (!res.ok) {
|
|
4001
5038
|
console.error(
|
|
4002
|
-
`[hasAnyConfiguredProvider] GET ${
|
|
5039
|
+
`[hasAnyConfiguredProvider] GET ${providerPath} returned HTTP ${res.status} (port ${port})`
|
|
4003
5040
|
);
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
if (!integration || typeof integration !== "object" || Array.isArray(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
5041
|
+
} else {
|
|
5042
|
+
const body = await res.json();
|
|
5043
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.data)) {
|
|
4008
5044
|
console.error(
|
|
4009
|
-
`[hasAnyConfiguredProvider] GET ${
|
|
5045
|
+
`[hasAnyConfiguredProvider] GET ${providerPath} body had no provider data array (port ${port})`
|
|
4010
5046
|
);
|
|
4011
|
-
|
|
5047
|
+
} else {
|
|
5048
|
+
let valid = true;
|
|
5049
|
+
for (const provider of body.data) {
|
|
5050
|
+
if (!provider || typeof provider !== "object" || Array.isArray(provider) || typeof provider.id !== "string") {
|
|
5051
|
+
console.error(
|
|
5052
|
+
`[hasAnyConfiguredProvider] GET ${providerPath} body contained an invalid provider (port ${port})`
|
|
5053
|
+
);
|
|
5054
|
+
valid = false;
|
|
5055
|
+
break;
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
if (valid) {
|
|
5059
|
+
providerReady = body.data.some(
|
|
5060
|
+
(provider) => provider.activation === "enabled"
|
|
5061
|
+
);
|
|
5062
|
+
}
|
|
4012
5063
|
}
|
|
4013
5064
|
}
|
|
4014
|
-
return body.data.some(
|
|
4015
|
-
(integration) => Array.isArray(integration.connections) && integration.connections.length > 0
|
|
4016
|
-
);
|
|
4017
5065
|
} catch (error2) {
|
|
4018
5066
|
console.error(
|
|
4019
|
-
`[hasAnyConfiguredProvider] GET ${
|
|
5067
|
+
`[hasAnyConfiguredProvider] GET ${providerPath} failed (port ${port}): ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4020
5068
|
);
|
|
4021
|
-
return null;
|
|
4022
5069
|
}
|
|
5070
|
+
if (providerReady === true) return true;
|
|
5071
|
+
if (integrationReady === null || providerReady === null) return null;
|
|
5072
|
+
return false;
|
|
4023
5073
|
}
|
|
4024
5074
|
try {
|
|
4025
5075
|
const res = await requestWithClient(port, client, "/config/providers");
|
|
@@ -4172,14 +5222,15 @@ async function probeOpenCodeRequestReadiness(port, client) {
|
|
|
4172
5222
|
}
|
|
4173
5223
|
|
|
4174
5224
|
// src/lib/opencode/session-v2.ts
|
|
4175
|
-
|
|
5225
|
+
var ALL_HTTP_STATUSES2 = Array.from({ length: 500 }, (_, index) => index + 100);
|
|
5226
|
+
function isRecord3(value) {
|
|
4176
5227
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4177
5228
|
}
|
|
4178
5229
|
function finiteNumber2(value) {
|
|
4179
5230
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
4180
5231
|
}
|
|
4181
5232
|
function adaptTime(value) {
|
|
4182
|
-
if (!
|
|
5233
|
+
if (!isRecord3(value)) return void 0;
|
|
4183
5234
|
const created = finiteNumber2(value.created);
|
|
4184
5235
|
const completed = finiteNumber2(value.completed);
|
|
4185
5236
|
if (created === void 0 && completed === void 0) return void 0;
|
|
@@ -4189,11 +5240,11 @@ function adaptTime(value) {
|
|
|
4189
5240
|
};
|
|
4190
5241
|
}
|
|
4191
5242
|
function adaptTokens(value) {
|
|
4192
|
-
if (!
|
|
5243
|
+
if (!isRecord3(value)) return void 0;
|
|
4193
5244
|
const input = finiteNumber2(value.input);
|
|
4194
5245
|
const output = finiteNumber2(value.output);
|
|
4195
5246
|
const reasoning = finiteNumber2(value.reasoning);
|
|
4196
|
-
const cache =
|
|
5247
|
+
const cache = isRecord3(value.cache) ? {
|
|
4197
5248
|
...finiteNumber2(value.cache.read) !== void 0 ? { read: finiteNumber2(value.cache.read) } : {},
|
|
4198
5249
|
...finiteNumber2(value.cache.write) !== void 0 ? { write: finiteNumber2(value.cache.write) } : {}
|
|
4199
5250
|
} : void 0;
|
|
@@ -4217,7 +5268,7 @@ function adaptMessageInfo(value, role) {
|
|
|
4217
5268
|
if (typeof value.finish === "string") info.finish = value.finish;
|
|
4218
5269
|
if ("error" in value) info.error = value.error;
|
|
4219
5270
|
if (typeof value.agent === "string") info.agent = value.agent;
|
|
4220
|
-
if (
|
|
5271
|
+
if (isRecord3(value.model)) {
|
|
4221
5272
|
if (typeof value.model.id === "string") info.modelID = value.model.id;
|
|
4222
5273
|
if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
|
|
4223
5274
|
}
|
|
@@ -4227,7 +5278,7 @@ function adaptMessageInfo(value, role) {
|
|
|
4227
5278
|
return info;
|
|
4228
5279
|
}
|
|
4229
5280
|
function adaptV2Message(value) {
|
|
4230
|
-
if (!
|
|
5281
|
+
if (!isRecord3(value) || typeof value.id !== "string" || typeof value.type !== "string") {
|
|
4231
5282
|
return null;
|
|
4232
5283
|
}
|
|
4233
5284
|
if (value.type === "user") {
|
|
@@ -4240,7 +5291,7 @@ function adaptV2Message(value) {
|
|
|
4240
5291
|
if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
|
|
4241
5292
|
const parts = [];
|
|
4242
5293
|
for (const content of value.content) {
|
|
4243
|
-
if (!
|
|
5294
|
+
if (!isRecord3(content) || typeof content.type !== "string") return null;
|
|
4244
5295
|
if (content.type === "text") {
|
|
4245
5296
|
if (typeof content.text !== "string") return null;
|
|
4246
5297
|
parts.push({ type: "text", text: content.text });
|
|
@@ -4254,23 +5305,23 @@ function adaptV2Message(value) {
|
|
|
4254
5305
|
};
|
|
4255
5306
|
}
|
|
4256
5307
|
function adaptFormTool(value) {
|
|
4257
|
-
if (!
|
|
5308
|
+
if (!isRecord3(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
|
|
4258
5309
|
return void 0;
|
|
4259
5310
|
}
|
|
4260
5311
|
return { messageID: value.messageID, callID: value.id };
|
|
4261
5312
|
}
|
|
4262
5313
|
function adaptV2FormWire(value) {
|
|
4263
|
-
if (!
|
|
5314
|
+
if (!isRecord3(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
4264
5315
|
return null;
|
|
4265
5316
|
}
|
|
4266
5317
|
return value;
|
|
4267
5318
|
}
|
|
4268
5319
|
function adaptV2FormField(value, header) {
|
|
4269
|
-
if (!
|
|
5320
|
+
if (!isRecord3(value)) return null;
|
|
4270
5321
|
const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
|
|
4271
5322
|
if (!question) return null;
|
|
4272
5323
|
const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
|
|
4273
|
-
if (!
|
|
5324
|
+
if (!isRecord3(option)) return [];
|
|
4274
5325
|
const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
|
|
4275
5326
|
if (!label) return [];
|
|
4276
5327
|
return [
|
|
@@ -4288,7 +5339,7 @@ function adaptV2Form(value) {
|
|
|
4288
5339
|
const header = typeof form.title === "string" ? form.title : "";
|
|
4289
5340
|
const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
|
|
4290
5341
|
if (questions.length === 0) return null;
|
|
4291
|
-
const tool =
|
|
5342
|
+
const tool = isRecord3(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
|
|
4292
5343
|
return {
|
|
4293
5344
|
id: form.id,
|
|
4294
5345
|
sessionID: form.sessionID,
|
|
@@ -4298,7 +5349,7 @@ function adaptV2Form(value) {
|
|
|
4298
5349
|
};
|
|
4299
5350
|
}
|
|
4300
5351
|
function adaptV2FormList(value) {
|
|
4301
|
-
if (!
|
|
5352
|
+
if (!isRecord3(value) || !Array.isArray(value.data)) return null;
|
|
4302
5353
|
return value.data.map(adaptV2Form).filter((form) => form !== null);
|
|
4303
5354
|
}
|
|
4304
5355
|
function adaptPattern(value) {
|
|
@@ -4309,7 +5360,7 @@ function adaptPattern(value) {
|
|
|
4309
5360
|
return void 0;
|
|
4310
5361
|
}
|
|
4311
5362
|
function adaptV2PermissionWire(value) {
|
|
4312
|
-
if (!
|
|
5363
|
+
if (!isRecord3(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
4313
5364
|
return null;
|
|
4314
5365
|
}
|
|
4315
5366
|
if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
|
|
@@ -4321,12 +5372,12 @@ function adaptV2Permission(value) {
|
|
|
4321
5372
|
const type = permission.permission ?? permission.action;
|
|
4322
5373
|
if (!type) return null;
|
|
4323
5374
|
const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
|
|
4324
|
-
const time =
|
|
5375
|
+
const time = isRecord3(permission.time) ? finiteNumber2(permission.time.created) !== void 0 ? { created: finiteNumber2(permission.time.created) } : void 0 : void 0;
|
|
4325
5376
|
return {
|
|
4326
5377
|
id: permission.id,
|
|
4327
5378
|
type,
|
|
4328
5379
|
sessionID: permission.sessionID,
|
|
4329
|
-
metadata:
|
|
5380
|
+
metadata: isRecord3(permission.metadata) ? permission.metadata : {},
|
|
4330
5381
|
raw: permission,
|
|
4331
5382
|
...pattern !== void 0 ? { pattern } : {},
|
|
4332
5383
|
...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
|
|
@@ -4336,12 +5387,12 @@ function adaptV2Permission(value) {
|
|
|
4336
5387
|
};
|
|
4337
5388
|
}
|
|
4338
5389
|
function adaptV2PermissionList(value) {
|
|
4339
|
-
if (!
|
|
5390
|
+
if (!isRecord3(value) || !Array.isArray(value.data)) return null;
|
|
4340
5391
|
return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
|
|
4341
5392
|
}
|
|
4342
5393
|
function adaptV2Session(value) {
|
|
4343
|
-
if (!
|
|
4344
|
-
const time =
|
|
5394
|
+
if (!isRecord3(value) || typeof value.id !== "string" || value.id.length === 0) return null;
|
|
5395
|
+
const time = isRecord3(value.time) ? {
|
|
4345
5396
|
...finiteNumber2(value.time.created) !== void 0 ? { created: finiteNumber2(value.time.created) } : {},
|
|
4346
5397
|
...finiteNumber2(value.time.updated) !== void 0 ? { updated: finiteNumber2(value.time.updated) } : {}
|
|
4347
5398
|
} : void 0;
|
|
@@ -4353,7 +5404,7 @@ function adaptV2Session(value) {
|
|
|
4353
5404
|
};
|
|
4354
5405
|
}
|
|
4355
5406
|
function adaptV2SessionList(value) {
|
|
4356
|
-
if (!
|
|
5407
|
+
if (!isRecord3(value) || !Array.isArray(value.data) || !isRecord3(value.cursor)) return null;
|
|
4357
5408
|
return {
|
|
4358
5409
|
data: value.data.map(adaptV2Session).filter((session) => session !== null),
|
|
4359
5410
|
cursor: value.cursor
|
|
@@ -4362,18 +5413,18 @@ function adaptV2SessionList(value) {
|
|
|
4362
5413
|
function adaptV2Location(value) {
|
|
4363
5414
|
const candidates = [
|
|
4364
5415
|
value,
|
|
4365
|
-
|
|
4366
|
-
|
|
5416
|
+
isRecord3(value) ? value.data : void 0,
|
|
5417
|
+
isRecord3(value) ? value.location : void 0
|
|
4367
5418
|
];
|
|
4368
5419
|
for (const candidate of candidates) {
|
|
4369
|
-
if (!
|
|
5420
|
+
if (!isRecord3(candidate) || typeof candidate.directory !== "string") continue;
|
|
4370
5421
|
const directory = candidate.directory.trim();
|
|
4371
5422
|
if (directory) return directory;
|
|
4372
5423
|
}
|
|
4373
5424
|
return null;
|
|
4374
5425
|
}
|
|
4375
5426
|
function adaptV2Messages(value) {
|
|
4376
|
-
if (!
|
|
5427
|
+
if (!isRecord3(value) || !Array.isArray(value.data)) return [];
|
|
4377
5428
|
return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
|
|
4378
5429
|
}
|
|
4379
5430
|
async function readJson(response) {
|
|
@@ -4388,7 +5439,7 @@ async function readJson(response) {
|
|
|
4388
5439
|
async function readData(client, path, init) {
|
|
4389
5440
|
const response = await client.request(path, init);
|
|
4390
5441
|
const body = await readJson(response);
|
|
4391
|
-
if (!
|
|
5442
|
+
if (!isRecord3(body) || !("data" in body)) {
|
|
4392
5443
|
throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
|
|
4393
5444
|
}
|
|
4394
5445
|
return body.data;
|
|
@@ -4399,6 +5450,14 @@ var OpenCodeV2PromptAckError = class extends Error {
|
|
|
4399
5450
|
this.name = "OpenCodeV2PromptAckError";
|
|
4400
5451
|
}
|
|
4401
5452
|
};
|
|
5453
|
+
var OpenCodeV2ModelSwitchError = class extends Error {
|
|
5454
|
+
statusCode;
|
|
5455
|
+
constructor(error2) {
|
|
5456
|
+
super(error2 instanceof Error ? error2.message : String(error2));
|
|
5457
|
+
this.name = "OpenCodeV2ModelSwitchError";
|
|
5458
|
+
this.statusCode = error2 instanceof OpenCodeRequestError ? error2.statusCode : null;
|
|
5459
|
+
}
|
|
5460
|
+
};
|
|
4402
5461
|
async function getOpenCodeDirectoryV2(client) {
|
|
4403
5462
|
try {
|
|
4404
5463
|
return adaptV2Location(await readJson(await client.request("/api/location")));
|
|
@@ -4415,7 +5474,7 @@ async function createV2Session(client, directory) {
|
|
|
4415
5474
|
headers: { "Content-Type": "application/json" },
|
|
4416
5475
|
body: JSON.stringify({ location: { directory } })
|
|
4417
5476
|
});
|
|
4418
|
-
if (!
|
|
5477
|
+
if (!isRecord3(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
4419
5478
|
throw new Error("OpenCode V2 create session response was missing data.id");
|
|
4420
5479
|
}
|
|
4421
5480
|
return data.id;
|
|
@@ -4426,25 +5485,35 @@ async function getV2Session(client, sessionId) {
|
|
|
4426
5485
|
if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
|
|
4427
5486
|
return session;
|
|
4428
5487
|
}
|
|
4429
|
-
async function listV2SessionPage(client, cursor) {
|
|
5488
|
+
async function listV2SessionPage(client, cursor, signal) {
|
|
5489
|
+
const operation = cursor ? "GET /api/session (next page)" : "GET /api/session";
|
|
4430
5490
|
const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
|
|
4431
5491
|
try {
|
|
4432
|
-
|
|
5492
|
+
const response = await client.request(path, signal ? { signal } : void 0, {
|
|
5493
|
+
allowStatuses: ALL_HTTP_STATUSES2
|
|
5494
|
+
});
|
|
5495
|
+
if (!response.ok) {
|
|
5496
|
+
console.error(
|
|
5497
|
+
`[listV2SessionPage] ${operation} returned HTTP ${response.status} (port ${client.port})`
|
|
5498
|
+
);
|
|
5499
|
+
return null;
|
|
5500
|
+
}
|
|
5501
|
+
return adaptV2SessionList(await readJson(response));
|
|
4433
5502
|
} catch (error2) {
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
);
|
|
5503
|
+
if (signal?.aborted) return null;
|
|
5504
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
5505
|
+
console.error(`[listV2SessionPage] ${operation} failed (port ${client.port}): ${errorName}`);
|
|
4437
5506
|
return null;
|
|
4438
5507
|
}
|
|
4439
5508
|
}
|
|
4440
|
-
async function listV2Sessions(client) {
|
|
5509
|
+
async function listV2Sessions(client, signal) {
|
|
4441
5510
|
const sessions = [];
|
|
4442
5511
|
const seenCursors = /* @__PURE__ */ new Set();
|
|
4443
5512
|
let cursor;
|
|
4444
5513
|
let hasNextPage = true;
|
|
4445
5514
|
try {
|
|
4446
5515
|
while (hasNextPage) {
|
|
4447
|
-
const page = await listV2SessionPage(client, cursor);
|
|
5516
|
+
const page = await listV2SessionPage(client, cursor, signal);
|
|
4448
5517
|
if (!page) return null;
|
|
4449
5518
|
sessions.push(...page.data);
|
|
4450
5519
|
const next = page.cursor.next;
|
|
@@ -4460,8 +5529,9 @@ async function listV2Sessions(client) {
|
|
|
4460
5529
|
}
|
|
4461
5530
|
return sessions;
|
|
4462
5531
|
} catch (error2) {
|
|
5532
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
4463
5533
|
console.error(
|
|
4464
|
-
`[listV2Sessions] session pagination failed
|
|
5534
|
+
`[listV2Sessions] GET /api/session pagination failed (port ${client.port}): ${errorName}`
|
|
4465
5535
|
);
|
|
4466
5536
|
return null;
|
|
4467
5537
|
}
|
|
@@ -4471,7 +5541,7 @@ async function deleteV2Session(client, sessionId) {
|
|
|
4471
5541
|
const response = await client.request(
|
|
4472
5542
|
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
4473
5543
|
{ method: "DELETE" },
|
|
4474
|
-
{ allowStatuses:
|
|
5544
|
+
{ allowStatuses: ALL_HTTP_STATUSES2 }
|
|
4475
5545
|
);
|
|
4476
5546
|
if (response.status >= 200 && response.status < 300) {
|
|
4477
5547
|
return { ok: true, status: response.status };
|
|
@@ -4507,7 +5577,34 @@ async function v2SessionExists(client, sessionId) {
|
|
|
4507
5577
|
return null;
|
|
4508
5578
|
}
|
|
4509
5579
|
}
|
|
4510
|
-
async function sendV2Prompt(client, sessionId, text) {
|
|
5580
|
+
async function sendV2Prompt(client, sessionId, text, options, onInvalidModelSelection) {
|
|
5581
|
+
const modelSelection = options?.model?.trim();
|
|
5582
|
+
const { model, variant } = splitModelVariant(modelSelection);
|
|
5583
|
+
const separator = model?.indexOf("/") ?? -1;
|
|
5584
|
+
const modelReference = model && separator > 0 && separator < model.length - 1 ? {
|
|
5585
|
+
id: model.slice(separator + 1),
|
|
5586
|
+
providerID: model.slice(0, separator),
|
|
5587
|
+
...variant ? { variant } : {}
|
|
5588
|
+
} : void 0;
|
|
5589
|
+
if (modelSelection && !modelReference) onInvalidModelSelection?.();
|
|
5590
|
+
if (options?.agent) {
|
|
5591
|
+
await client.request(`/api/session/${encodeURIComponent(sessionId)}/agent`, {
|
|
5592
|
+
method: "POST",
|
|
5593
|
+
headers: { "Content-Type": "application/json" },
|
|
5594
|
+
body: JSON.stringify({ agent: options.agent })
|
|
5595
|
+
});
|
|
5596
|
+
}
|
|
5597
|
+
if (modelReference) {
|
|
5598
|
+
try {
|
|
5599
|
+
await client.request(`/api/session/${encodeURIComponent(sessionId)}/model`, {
|
|
5600
|
+
method: "POST",
|
|
5601
|
+
headers: { "Content-Type": "application/json" },
|
|
5602
|
+
body: JSON.stringify({ model: modelReference })
|
|
5603
|
+
});
|
|
5604
|
+
} catch (error2) {
|
|
5605
|
+
throw new OpenCodeV2ModelSwitchError(error2);
|
|
5606
|
+
}
|
|
5607
|
+
}
|
|
4511
5608
|
const path = `/api/session/${encodeURIComponent(sessionId)}/prompt`;
|
|
4512
5609
|
const response = await client.request(path, {
|
|
4513
5610
|
method: "POST",
|
|
@@ -4522,8 +5619,8 @@ async function sendV2Prompt(client, sessionId, text) {
|
|
|
4522
5619
|
`OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4523
5620
|
);
|
|
4524
5621
|
}
|
|
4525
|
-
const data =
|
|
4526
|
-
if (!
|
|
5622
|
+
const data = isRecord3(body) && "data" in body ? body.data : void 0;
|
|
5623
|
+
if (!isRecord3(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
4527
5624
|
throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
|
|
4528
5625
|
}
|
|
4529
5626
|
return data.id;
|
|
@@ -4532,7 +5629,7 @@ async function getV2SessionMessages(client, sessionId) {
|
|
|
4532
5629
|
const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
|
|
4533
5630
|
try {
|
|
4534
5631
|
const body = await readJson(await client.request(path));
|
|
4535
|
-
if (!
|
|
5632
|
+
if (!isRecord3(body) || !Array.isArray(body.data) || !isRecord3(body.cursor)) return null;
|
|
4536
5633
|
return adaptV2Messages(body);
|
|
4537
5634
|
} catch (error2) {
|
|
4538
5635
|
console.error(
|
|
@@ -4571,7 +5668,7 @@ async function interruptV2Session(client, sessionId) {
|
|
|
4571
5668
|
async function getV2ActiveSessions(client) {
|
|
4572
5669
|
try {
|
|
4573
5670
|
const body = await readJson(await client.request("/api/session/active"));
|
|
4574
|
-
if (!
|
|
5671
|
+
if (!isRecord3(body) || !isRecord3(body.data)) return null;
|
|
4575
5672
|
return body.data;
|
|
4576
5673
|
} catch (error2) {
|
|
4577
5674
|
console.error(
|
|
@@ -4587,8 +5684,8 @@ async function isV2SessionOngoing(client, sessionId) {
|
|
|
4587
5684
|
}
|
|
4588
5685
|
function sessionErrorReason2(value) {
|
|
4589
5686
|
if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
|
|
4590
|
-
if (
|
|
4591
|
-
const data =
|
|
5687
|
+
if (isRecord3(value)) {
|
|
5688
|
+
const data = isRecord3(value.data) ? value.data : void 0;
|
|
4592
5689
|
const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
|
|
4593
5690
|
if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
4594
5691
|
}
|
|
@@ -4604,17 +5701,17 @@ function adaptV2SessionErrorEvent(value) {
|
|
|
4604
5701
|
return null;
|
|
4605
5702
|
}
|
|
4606
5703
|
}
|
|
4607
|
-
if (!
|
|
5704
|
+
if (!isRecord3(parsed)) return null;
|
|
4608
5705
|
try {
|
|
4609
5706
|
const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
|
|
4610
5707
|
if (establishedShape) return establishedShape;
|
|
4611
5708
|
} catch (error2) {
|
|
4612
5709
|
void error2;
|
|
4613
5710
|
}
|
|
4614
|
-
const candidates = [parsed, parsed.payload, parsed.data].filter(
|
|
5711
|
+
const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord3);
|
|
4615
5712
|
for (const event of candidates) {
|
|
4616
5713
|
if (event.type !== "session.error") continue;
|
|
4617
|
-
const properties = [event.properties, event.data, event].find(
|
|
5714
|
+
const properties = [event.properties, event.data, event].find(isRecord3);
|
|
4618
5715
|
if (!properties) continue;
|
|
4619
5716
|
const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
|
|
4620
5717
|
if (!sessionId) continue;
|
|
@@ -4664,13 +5761,13 @@ async function readV2SessionErrorStream(client, options) {
|
|
|
4664
5761
|
}
|
|
4665
5762
|
|
|
4666
5763
|
// src/lib/opencode/credential-import-v2.ts
|
|
4667
|
-
import { readFileSync as
|
|
4668
|
-
import { join as
|
|
5764
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
5765
|
+
import { join as join8 } from "node:path";
|
|
4669
5766
|
|
|
4670
5767
|
// src/lib/openai-usage.ts
|
|
4671
|
-
import { readFileSync as
|
|
5768
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4672
5769
|
import { homedir as homedir3 } from "node:os";
|
|
4673
|
-
import { join as
|
|
5770
|
+
import { join as join7 } from "node:path";
|
|
4674
5771
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
4675
5772
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
4676
5773
|
var OpenAiUsageError = class extends Error {
|
|
@@ -4684,7 +5781,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
4684
5781
|
}
|
|
4685
5782
|
function readOpenCodeChatGptCredentials() {
|
|
4686
5783
|
try {
|
|
4687
|
-
const raw =
|
|
5784
|
+
const raw = readFileSync7(join7(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
4688
5785
|
let parsed;
|
|
4689
5786
|
try {
|
|
4690
5787
|
parsed = JSON.parse(raw);
|
|
@@ -4750,7 +5847,7 @@ function parseCodexUsageHeaders(headers) {
|
|
|
4750
5847
|
function normalizeProbeModel(model) {
|
|
4751
5848
|
return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
|
|
4752
5849
|
}
|
|
4753
|
-
function
|
|
5850
|
+
function isRecord4(value) {
|
|
4754
5851
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4755
5852
|
}
|
|
4756
5853
|
function unsupportedProbeModels(reason, port) {
|
|
@@ -4761,14 +5858,14 @@ async function resolveV1ProbeModels(client, port) {
|
|
|
4761
5858
|
try {
|
|
4762
5859
|
const response = await client.request("/config/providers");
|
|
4763
5860
|
const body = await response.json();
|
|
4764
|
-
if (!
|
|
5861
|
+
if (!isRecord4(body) || !Array.isArray(body.providers)) {
|
|
4765
5862
|
return unsupportedProbeModels("V1 provider response did not contain a providers array", port);
|
|
4766
5863
|
}
|
|
4767
5864
|
const provider = body.providers.find(
|
|
4768
|
-
(candidate) =>
|
|
5865
|
+
(candidate) => isRecord4(candidate) && candidate.id === "openai"
|
|
4769
5866
|
);
|
|
4770
|
-
if (!provider || !
|
|
4771
|
-
const defaults2 =
|
|
5867
|
+
if (!provider || !isRecord4(provider.models)) return { status: "supported", models: [] };
|
|
5868
|
+
const defaults2 = isRecord4(body.default) ? body.default : void 0;
|
|
4772
5869
|
const candidates = [
|
|
4773
5870
|
...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
|
|
4774
5871
|
...Object.keys(provider.models)
|
|
@@ -4790,14 +5887,14 @@ async function resolveV2ProbeModels(client, port) {
|
|
|
4790
5887
|
try {
|
|
4791
5888
|
const response = await client.request(path);
|
|
4792
5889
|
const body = await response.json();
|
|
4793
|
-
if (!
|
|
5890
|
+
if (!isRecord4(body) || !Array.isArray(body.data)) {
|
|
4794
5891
|
return unsupportedProbeModels(`V2 GET ${path} did not contain a provider data array`, port);
|
|
4795
5892
|
}
|
|
4796
5893
|
const provider = body.data.find(
|
|
4797
|
-
(candidate) =>
|
|
5894
|
+
(candidate) => isRecord4(candidate) && candidate.id === "openai"
|
|
4798
5895
|
);
|
|
4799
5896
|
if (!provider) return { status: "supported", models: [] };
|
|
4800
|
-
if (!
|
|
5897
|
+
if (!isRecord4(provider.models)) {
|
|
4801
5898
|
return unsupportedProbeModels(
|
|
4802
5899
|
"V2 provider response has no safe OpenAI model catalogue",
|
|
4803
5900
|
port
|
|
@@ -4907,7 +6004,7 @@ var CONNECT_KEY_MAX_ATTEMPTS = 3;
|
|
|
4907
6004
|
var CONNECT_KEY_RETRY_DELAY_MS = 250;
|
|
4908
6005
|
var INTEGRATION_LIST_MAX_READS = 10;
|
|
4909
6006
|
var INTEGRATION_LIST_RETRY_DELAY_MS = 1e3;
|
|
4910
|
-
function
|
|
6007
|
+
function isRecord5(value) {
|
|
4911
6008
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4912
6009
|
}
|
|
4913
6010
|
var sleep2 = (milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds));
|
|
@@ -4916,14 +6013,14 @@ function emptyResult(unsupported = [], outcome = "completed") {
|
|
|
4916
6013
|
}
|
|
4917
6014
|
function readOpenCodeCredentials(homeDir, log3) {
|
|
4918
6015
|
try {
|
|
4919
|
-
const raw =
|
|
6016
|
+
const raw = readFileSync8(join8(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
|
|
4920
6017
|
let parsed;
|
|
4921
6018
|
try {
|
|
4922
6019
|
parsed = JSON.parse(raw);
|
|
4923
6020
|
} catch (error2) {
|
|
4924
6021
|
return null;
|
|
4925
6022
|
}
|
|
4926
|
-
return
|
|
6023
|
+
return isRecord5(parsed) ? parsed : null;
|
|
4927
6024
|
} catch (error2) {
|
|
4928
6025
|
const code = error2.code;
|
|
4929
6026
|
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
@@ -4936,7 +6033,7 @@ function classifyOpenCodeCredentials(credentials2) {
|
|
|
4936
6033
|
const importable = [];
|
|
4937
6034
|
const unsupported = [];
|
|
4938
6035
|
for (const [integrationId, value] of Object.entries(credentials2)) {
|
|
4939
|
-
if (!
|
|
6036
|
+
if (!isRecord5(value) || typeof value.type !== "string") {
|
|
4940
6037
|
unsupported.push({ integrationId, reason: "unrecognised credential entry" });
|
|
4941
6038
|
continue;
|
|
4942
6039
|
}
|
|
@@ -4951,7 +6048,7 @@ function classifyOpenCodeCredentials(credentials2) {
|
|
|
4951
6048
|
unsupported.push({ integrationId, reason: "unrecognised credential entry" });
|
|
4952
6049
|
continue;
|
|
4953
6050
|
}
|
|
4954
|
-
if (value.metadata === void 0 ||
|
|
6051
|
+
if (value.metadata === void 0 || isRecord5(value.metadata) && Object.keys(value.metadata).length === 0) {
|
|
4955
6052
|
importable.push({ integrationId, key: value.key });
|
|
4956
6053
|
} else {
|
|
4957
6054
|
unsupported.push({ integrationId, reason: "needs the interactive connect flow" });
|
|
@@ -4964,7 +6061,7 @@ function statusFromRequestError(error2) {
|
|
|
4964
6061
|
const match = /^OpenCode request failed: HTTP (\d{3})/.exec(error2.message);
|
|
4965
6062
|
return match ? Number(match[1]) : void 0;
|
|
4966
6063
|
}
|
|
4967
|
-
function
|
|
6064
|
+
function failureMessage2(status2) {
|
|
4968
6065
|
return status2 === void 0 ? "OpenCode credential import request failed" : `OpenCode credential import request failed with HTTP ${status2}`;
|
|
4969
6066
|
}
|
|
4970
6067
|
function isRetryableStatus(status2) {
|
|
@@ -4991,18 +6088,18 @@ async function importOneCredential(request3, credential, sleepForRetry, log3) {
|
|
|
4991
6088
|
const failure = {
|
|
4992
6089
|
integrationId: credential.integrationId,
|
|
4993
6090
|
status: response.status,
|
|
4994
|
-
message:
|
|
6091
|
+
message: failureMessage2(response.status)
|
|
4995
6092
|
};
|
|
4996
|
-
log3(`${
|
|
6093
|
+
log3(`${failureMessage2(failure.status)} for ${failure.integrationId}`, "warn");
|
|
4997
6094
|
return { ok: false, failure };
|
|
4998
6095
|
} catch (error2) {
|
|
4999
6096
|
const status2 = statusFromRequestError(error2);
|
|
5000
6097
|
const failure = {
|
|
5001
6098
|
integrationId: credential.integrationId,
|
|
5002
6099
|
...status2 === void 0 ? {} : { status: status2 },
|
|
5003
|
-
message:
|
|
6100
|
+
message: failureMessage2(status2)
|
|
5004
6101
|
};
|
|
5005
|
-
log3(`${
|
|
6102
|
+
log3(`${failureMessage2(status2)} for ${failure.integrationId}`, "warn");
|
|
5006
6103
|
return { ok: false, failure };
|
|
5007
6104
|
}
|
|
5008
6105
|
}
|
|
@@ -5028,10 +6125,10 @@ async function importOpenCodeCredentials({
|
|
|
5028
6125
|
return result;
|
|
5029
6126
|
}
|
|
5030
6127
|
function parseIntegrationList(value) {
|
|
5031
|
-
if (!
|
|
6128
|
+
if (!isRecord5(value) || !Array.isArray(value.data)) return null;
|
|
5032
6129
|
const integrationIds = /* @__PURE__ */ new Set();
|
|
5033
6130
|
for (const integration of value.data) {
|
|
5034
|
-
if (!
|
|
6131
|
+
if (!isRecord5(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
5035
6132
|
return null;
|
|
5036
6133
|
}
|
|
5037
6134
|
if (integration.connections.length > 0) integrationIds.add(integration.id);
|
|
@@ -5116,87 +6213,9 @@ async function importOpenCodeV2Credentials({
|
|
|
5116
6213
|
|
|
5117
6214
|
// src/lib/opencode/session-db-boot.ts
|
|
5118
6215
|
import { spawn as spawn2 } from "node:child_process";
|
|
5119
|
-
import { mkdirSync as
|
|
6216
|
+
import { mkdirSync as mkdirSync4, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5120
6217
|
import { homedir as homedir4 } from "node:os";
|
|
5121
|
-
import { dirname as
|
|
5122
|
-
|
|
5123
|
-
// src/lib/runner-synchroniser.ts
|
|
5124
|
-
import { spawn } from "node:child_process";
|
|
5125
|
-
function appendError(stderr, error2) {
|
|
5126
|
-
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
5127
|
-
return stderr === "" ? message : `${stderr}
|
|
5128
|
-
${message}`;
|
|
5129
|
-
}
|
|
5130
|
-
function runSynchroniser(args, opts) {
|
|
5131
|
-
return new Promise((resolve5) => {
|
|
5132
|
-
let child;
|
|
5133
|
-
let stdout = "";
|
|
5134
|
-
let stderr = "";
|
|
5135
|
-
let settled = false;
|
|
5136
|
-
const timer = {};
|
|
5137
|
-
let abortListener;
|
|
5138
|
-
let spawnListener;
|
|
5139
|
-
const finish = (result) => {
|
|
5140
|
-
if (settled) return;
|
|
5141
|
-
settled = true;
|
|
5142
|
-
if (timer.handle) clearTimeout(timer.handle);
|
|
5143
|
-
if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
|
|
5144
|
-
if (spawnListener) child.removeListener("spawn", spawnListener);
|
|
5145
|
-
resolve5(result);
|
|
5146
|
-
};
|
|
5147
|
-
try {
|
|
5148
|
-
child = spawn("runner-synchroniser", args, {
|
|
5149
|
-
env: opts.env ?? process.env,
|
|
5150
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5151
|
-
});
|
|
5152
|
-
} catch (error2) {
|
|
5153
|
-
finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
|
|
5154
|
-
return;
|
|
5155
|
-
}
|
|
5156
|
-
child.stdout?.setEncoding("utf8");
|
|
5157
|
-
child.stdout?.on("data", (chunk) => {
|
|
5158
|
-
stdout += chunk;
|
|
5159
|
-
});
|
|
5160
|
-
child.stderr?.setEncoding("utf8");
|
|
5161
|
-
child.stderr?.on("data", (chunk) => {
|
|
5162
|
-
stderr += chunk;
|
|
5163
|
-
});
|
|
5164
|
-
child.once("error", (error2) => {
|
|
5165
|
-
finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
|
|
5166
|
-
});
|
|
5167
|
-
child.once("close", (code) => {
|
|
5168
|
-
finish({ code, stdout, stderr, timedOut: false });
|
|
5169
|
-
});
|
|
5170
|
-
if (opts.signal) {
|
|
5171
|
-
const killChild = () => {
|
|
5172
|
-
if (child.pid === void 0) {
|
|
5173
|
-
if (!spawnListener) {
|
|
5174
|
-
spawnListener = killChild;
|
|
5175
|
-
child.once("spawn", spawnListener);
|
|
5176
|
-
}
|
|
5177
|
-
return;
|
|
5178
|
-
}
|
|
5179
|
-
child.kill("SIGKILL");
|
|
5180
|
-
};
|
|
5181
|
-
abortListener = killChild;
|
|
5182
|
-
if (opts.signal.aborted) {
|
|
5183
|
-
abortListener();
|
|
5184
|
-
} else {
|
|
5185
|
-
opts.signal.addEventListener("abort", abortListener, { once: true });
|
|
5186
|
-
if (opts.signal.aborted) abortListener();
|
|
5187
|
-
}
|
|
5188
|
-
}
|
|
5189
|
-
timer.handle = setTimeout(
|
|
5190
|
-
() => {
|
|
5191
|
-
child.kill("SIGKILL");
|
|
5192
|
-
finish({ code: null, stdout, stderr, timedOut: true });
|
|
5193
|
-
},
|
|
5194
|
-
Math.max(0, opts.timeoutMs)
|
|
5195
|
-
);
|
|
5196
|
-
});
|
|
5197
|
-
}
|
|
5198
|
-
|
|
5199
|
-
// src/lib/opencode/session-db-boot.ts
|
|
6218
|
+
import { dirname as dirname7, resolve as resolvePath } from "node:path";
|
|
5200
6219
|
var SESSION_DB_RESTORE_TIMEOUT_MS = 3e5;
|
|
5201
6220
|
var SESSION_DB_VERIFY_TIMEOUT_MS = 36e4;
|
|
5202
6221
|
var SESSION_DB_SYNCHRONISER_TIMEOUT_MS = 12e4;
|
|
@@ -5246,8 +6265,8 @@ function clearMarker(options) {
|
|
|
5246
6265
|
function markNoReplicate(options, message) {
|
|
5247
6266
|
if (options.noReplicateMarker) {
|
|
5248
6267
|
try {
|
|
5249
|
-
|
|
5250
|
-
|
|
6268
|
+
mkdirSync4(dirname7(options.noReplicateMarker), { recursive: true });
|
|
6269
|
+
writeFileSync4(options.noReplicateMarker, "");
|
|
5251
6270
|
} catch (error2) {
|
|
5252
6271
|
options.log(
|
|
5253
6272
|
`Could not write session-DB no-replicate marker ${options.noReplicateMarker}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
@@ -5401,8 +6420,8 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
5401
6420
|
return null;
|
|
5402
6421
|
}
|
|
5403
6422
|
try {
|
|
5404
|
-
|
|
5405
|
-
|
|
6423
|
+
mkdirSync4(dirname7(configPath), { recursive: true });
|
|
6424
|
+
writeFileSync4(configPath, rendered.stdout);
|
|
5406
6425
|
} catch (error2) {
|
|
5407
6426
|
options.log(
|
|
5408
6427
|
`Could not write ${configPath}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
@@ -5667,8 +6686,8 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
5667
6686
|
|
|
5668
6687
|
// src/lib/opencode/session-db-provenance.ts
|
|
5669
6688
|
import { createRequire } from "node:module";
|
|
5670
|
-
import { mkdirSync as
|
|
5671
|
-
import { dirname as
|
|
6689
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6690
|
+
import { dirname as dirname8, join as join9 } from "node:path";
|
|
5672
6691
|
var require2 = createRequire(import.meta.url);
|
|
5673
6692
|
function readSessionDbMigrationIds(dbPath) {
|
|
5674
6693
|
let db;
|
|
@@ -5705,12 +6724,12 @@ function readSessionDbMigrationIds(dbPath) {
|
|
|
5705
6724
|
}
|
|
5706
6725
|
function sessionDbProvenanceStatePath(homeDir, env) {
|
|
5707
6726
|
const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
|
|
5708
|
-
return override ||
|
|
6727
|
+
return override || join9(homeDir, ".local", "state", "evident", "session-db-provenance.json");
|
|
5709
6728
|
}
|
|
5710
6729
|
function loadSessionDbProvenanceState(path) {
|
|
5711
6730
|
let value;
|
|
5712
6731
|
try {
|
|
5713
|
-
value = JSON.parse(
|
|
6732
|
+
value = JSON.parse(readFileSync9(path, "utf8"));
|
|
5714
6733
|
} catch (error2) {
|
|
5715
6734
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return {};
|
|
5716
6735
|
console.error(
|
|
@@ -5734,8 +6753,8 @@ function loadSessionDbProvenanceState(path) {
|
|
|
5734
6753
|
}
|
|
5735
6754
|
function saveSessionDbProvenanceState(path, state) {
|
|
5736
6755
|
try {
|
|
5737
|
-
|
|
5738
|
-
|
|
6756
|
+
mkdirSync5(dirname8(path), { recursive: true });
|
|
6757
|
+
writeFileSync5(path, `${JSON.stringify(state, null, 2)}
|
|
5739
6758
|
`, "utf8");
|
|
5740
6759
|
} catch (error2) {
|
|
5741
6760
|
console.error(
|
|
@@ -5790,6 +6809,26 @@ function isSessionDbProvenanceRecord(value) {
|
|
|
5790
6809
|
return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
|
|
5791
6810
|
}
|
|
5792
6811
|
|
|
6812
|
+
// src/lib/opencode/detect.ts
|
|
6813
|
+
function describeProbe(route, result) {
|
|
6814
|
+
if (result.error?.startsWith("HTTP ")) return `${route}: ${result.error}`;
|
|
6815
|
+
if (result.error) return `${route}: no HTTP response (${result.error})`;
|
|
6816
|
+
return `${route}: ${result.healthy ? "healthy" : "no response detail"}`;
|
|
6817
|
+
}
|
|
6818
|
+
async function classifyRunningOpenCode(port) {
|
|
6819
|
+
const v1 = await checkOpenCodeHealth(port);
|
|
6820
|
+
if (v1.healthy) return { kind: "v1", version: v1.version ?? null };
|
|
6821
|
+
const v2 = await checkOpenCode2Health(port);
|
|
6822
|
+
if (v2.healthy) return { kind: "v2", version: v2.version ?? null };
|
|
6823
|
+
const detail = [
|
|
6824
|
+
describeProbe("V1 /global/health", v1),
|
|
6825
|
+
describeProbe("V2 /api/info (or /api/health fallback)", v2)
|
|
6826
|
+
].join("; ");
|
|
6827
|
+
if (v1.okWithNonJsonBody && v2.authFailed) return { kind: "v2", version: null };
|
|
6828
|
+
if (v1.authFailed && v2.authFailed) return { kind: "authenticated-unknown", detail };
|
|
6829
|
+
return { kind: "no-server-identified", detail };
|
|
6830
|
+
}
|
|
6831
|
+
|
|
5793
6832
|
// src/lib/opencode/opencode2-package.ts
|
|
5794
6833
|
var OPENCODE2_PACKAGE_SPEC = "@opencode-ai/cli@0.0.0-beta-17823";
|
|
5795
6834
|
|
|
@@ -6098,26 +7137,28 @@ async function startOpenCode(port, options = {}) {
|
|
|
6098
7137
|
}
|
|
6099
7138
|
async function startOpenCode2(port, options = {}) {
|
|
6100
7139
|
const password = randomBytes(24).toString("hex");
|
|
6101
|
-
let command = "opencode2";
|
|
7140
|
+
let command = options.command ?? "opencode2";
|
|
6102
7141
|
const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
|
|
6103
7142
|
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...logLevel];
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
7143
|
+
if (options.command === void 0) {
|
|
7144
|
+
try {
|
|
7145
|
+
execSync("which opencode2", { stdio: "ignore" });
|
|
7146
|
+
} catch {
|
|
7147
|
+
command = "npx";
|
|
7148
|
+
args = [
|
|
7149
|
+
"-y",
|
|
7150
|
+
"-p",
|
|
7151
|
+
OPENCODE2_PACKAGE_SPEC,
|
|
7152
|
+
"--",
|
|
7153
|
+
"opencode2",
|
|
7154
|
+
"serve",
|
|
7155
|
+
"--port",
|
|
7156
|
+
port.toString(),
|
|
7157
|
+
"--hostname",
|
|
7158
|
+
"127.0.0.1",
|
|
7159
|
+
...logLevel
|
|
7160
|
+
];
|
|
7161
|
+
}
|
|
6121
7162
|
}
|
|
6122
7163
|
const child = spawn3(command, args, {
|
|
6123
7164
|
env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
|
|
@@ -6174,6 +7215,56 @@ function isOpenCode2Installed() {
|
|
|
6174
7215
|
return false;
|
|
6175
7216
|
}
|
|
6176
7217
|
}
|
|
7218
|
+
function readVersionOutput(command) {
|
|
7219
|
+
try {
|
|
7220
|
+
return execSync2(`${command} --version`, {
|
|
7221
|
+
encoding: "utf-8",
|
|
7222
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
7223
|
+
}).trim();
|
|
7224
|
+
} catch (error2) {
|
|
7225
|
+
const output = typeof error2 === "object" && error2 !== null ? [
|
|
7226
|
+
"stdout" in error2 ? String(error2.stdout ?? "").trim() : "",
|
|
7227
|
+
"stderr" in error2 ? String(error2.stderr ?? "").trim() : ""
|
|
7228
|
+
].filter(Boolean).join("\n") : "";
|
|
7229
|
+
return output || (error2 instanceof Error ? error2.message : String(error2));
|
|
7230
|
+
}
|
|
7231
|
+
}
|
|
7232
|
+
function parseOpenCodeVersion(rawVersion) {
|
|
7233
|
+
const output = rawVersion.trim();
|
|
7234
|
+
const prefix = output.match(/^(opencode2|opencode)\s+/i)?.[1]?.toLowerCase();
|
|
7235
|
+
const version2 = (prefix ? output.slice(prefix.length).trim() : output).replace(/^v/i, "");
|
|
7236
|
+
if (prefix === "opencode2") return { major: "v2", version: version2 };
|
|
7237
|
+
const major = version2.match(/^(\d+)(?:\.\d+)*(?:[-+][0-9A-Za-z.-]+)?$/)?.[1];
|
|
7238
|
+
if (!major) return null;
|
|
7239
|
+
if (Number(major) === 1) return { major: "v1", version: version2 };
|
|
7240
|
+
if (Number(major) >= 2) return { major: "v2", version: version2 };
|
|
7241
|
+
return null;
|
|
7242
|
+
}
|
|
7243
|
+
function resolveInstalledOpenCode() {
|
|
7244
|
+
let opencode2Available = false;
|
|
7245
|
+
if (isOpenCodeInstalled()) {
|
|
7246
|
+
const rawVersion = readVersionOutput("opencode");
|
|
7247
|
+
const parsed = parseOpenCodeVersion(rawVersion);
|
|
7248
|
+
if (parsed?.major === "v1") {
|
|
7249
|
+
return { kind: "installed", major: "v1", command: "opencode", version: parsed.version };
|
|
7250
|
+
}
|
|
7251
|
+
if (parsed?.major === "v2") {
|
|
7252
|
+
return { kind: "installed", major: "v2", command: "opencode", version: parsed.version };
|
|
7253
|
+
}
|
|
7254
|
+
opencode2Available = isOpenCode2Installed();
|
|
7255
|
+
if (!opencode2Available) return { kind: "unknown", command: "opencode", rawVersion };
|
|
7256
|
+
}
|
|
7257
|
+
if (opencode2Available || isOpenCode2Installed()) {
|
|
7258
|
+
const parsed = parseOpenCodeVersion(readVersionOutput("opencode2"));
|
|
7259
|
+
return {
|
|
7260
|
+
kind: "installed",
|
|
7261
|
+
major: "v2",
|
|
7262
|
+
command: "opencode2",
|
|
7263
|
+
version: parsed?.version
|
|
7264
|
+
};
|
|
7265
|
+
}
|
|
7266
|
+
return { kind: "none" };
|
|
7267
|
+
}
|
|
6177
7268
|
async function promptOpenCodeInstall(interactive) {
|
|
6178
7269
|
if (!interactive) {
|
|
6179
7270
|
console.log(
|
|
@@ -6244,154 +7335,47 @@ async function promptOpenCodeInstall(interactive) {
|
|
|
6244
7335
|
value: "exit",
|
|
6245
7336
|
description: "Exit now and run the command again later"
|
|
6246
7337
|
}
|
|
6247
|
-
]
|
|
6248
|
-
});
|
|
6249
|
-
if (afterInstall === "continue") {
|
|
6250
|
-
if (isOpenCodeInstalled()) {
|
|
6251
|
-
console.log(chalk4.green("\n\u2713 OpenCode detected!"));
|
|
6252
|
-
return "installed";
|
|
6253
|
-
} else {
|
|
6254
|
-
console.log(chalk4.yellow("\nOpenCode still not detected in PATH."));
|
|
6255
|
-
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);
|
|
6320
|
-
}
|
|
6321
|
-
}
|
|
6322
|
-
const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
|
|
6323
|
-
return toDelete.sort(
|
|
6324
|
-
(a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
|
|
6325
|
-
);
|
|
6326
|
-
}
|
|
6327
|
-
var DEFAULT_INTERVAL = "1h";
|
|
6328
|
-
function resolve2(flag, envValue, fallback) {
|
|
6329
|
-
return flag ?? envValue ?? fallback;
|
|
6330
|
-
}
|
|
6331
|
-
function parseMaxCount(input) {
|
|
6332
|
-
const trimmed = input.trim();
|
|
6333
|
-
if (!/^\d+$/.test(trimmed)) {
|
|
6334
|
-
throw new Error(`Invalid max-count "${input}": expected a positive integer.`);
|
|
6335
|
-
}
|
|
6336
|
-
const value = Number(trimmed);
|
|
6337
|
-
if (value <= 0) {
|
|
6338
|
-
throw new Error(`Invalid max-count "${input}": must be greater than 0.`);
|
|
6339
|
-
}
|
|
6340
|
-
return value;
|
|
6341
|
-
}
|
|
6342
|
-
function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
6343
|
-
const warnings = [];
|
|
6344
|
-
const maxAgeRaw = resolve2(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
|
|
6345
|
-
const maxCountRaw = resolve2(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
|
|
6346
|
-
const intervalRaw = resolve2(
|
|
6347
|
-
flags.interval,
|
|
6348
|
-
env.EVIDENT_SESSION_CLEANUP_INTERVAL,
|
|
6349
|
-
DEFAULT_INTERVAL
|
|
6350
|
-
);
|
|
6351
|
-
let maxAgeMs;
|
|
6352
|
-
if (maxAgeRaw !== void 0) {
|
|
6353
|
-
try {
|
|
6354
|
-
maxAgeMs = parseDurationMs(maxAgeRaw);
|
|
6355
|
-
} catch (err) {
|
|
6356
|
-
warnings.push(
|
|
6357
|
-
`Ignoring invalid --session-cleanup-max-age: ${err instanceof Error ? err.message : String(err)}`
|
|
6358
|
-
);
|
|
6359
|
-
}
|
|
6360
|
-
}
|
|
6361
|
-
let maxCount;
|
|
6362
|
-
if (maxCountRaw !== void 0) {
|
|
6363
|
-
try {
|
|
6364
|
-
maxCount = parseMaxCount(maxCountRaw);
|
|
6365
|
-
} catch (err) {
|
|
6366
|
-
warnings.push(
|
|
6367
|
-
`Ignoring invalid --session-cleanup-max-count: ${err instanceof Error ? err.message : String(err)}`
|
|
6368
|
-
);
|
|
7338
|
+
]
|
|
7339
|
+
});
|
|
7340
|
+
if (afterInstall === "continue") {
|
|
7341
|
+
if (isOpenCodeInstalled()) {
|
|
7342
|
+
console.log(chalk4.green("\n\u2713 OpenCode detected!"));
|
|
7343
|
+
return "installed";
|
|
7344
|
+
} else {
|
|
7345
|
+
console.log(chalk4.yellow("\nOpenCode still not detected in PATH."));
|
|
7346
|
+
console.log(chalk4.dim("You may need to restart your terminal or add it to your PATH."));
|
|
7347
|
+
const proceed = await select({
|
|
7348
|
+
message: "Continue anyway?",
|
|
7349
|
+
choices: [
|
|
7350
|
+
{ name: "Yes, continue", value: "continue" },
|
|
7351
|
+
{ name: "No, exit", value: "exit" }
|
|
7352
|
+
]
|
|
7353
|
+
});
|
|
7354
|
+
return proceed === "continue" ? "continue" : "exit";
|
|
7355
|
+
}
|
|
6369
7356
|
}
|
|
7357
|
+
return "exit";
|
|
6370
7358
|
}
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
intervalMs = parseDurationMs(DEFAULT_INTERVAL);
|
|
6379
|
-
}
|
|
6380
|
-
const enabled = maxAgeMs !== void 0 || maxCount !== void 0;
|
|
6381
|
-
return { enabled, maxAgeMs, maxCount, intervalMs, warnings };
|
|
7359
|
+
return action;
|
|
7360
|
+
}
|
|
7361
|
+
|
|
7362
|
+
// src/lib/opencode/provider-check.ts
|
|
7363
|
+
function buildNoProviderWarning(hasProvider) {
|
|
7364
|
+
if (hasProvider !== false) return null;
|
|
7365
|
+
return "Warning: opencode has no authenticated model provider configured, so it won't be able to answer prompts. Run `opencode auth login` to set one up (see https://opencode.ai for details).";
|
|
6382
7366
|
}
|
|
6383
7367
|
|
|
6384
7368
|
// src/lib/opencode/session-db-size.ts
|
|
6385
7369
|
import { statSync as statSync4 } from "node:fs";
|
|
6386
|
-
import { join as
|
|
7370
|
+
import { join as join10 } from "node:path";
|
|
6387
7371
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
6388
7372
|
function statSessionDbBytes(homeDir) {
|
|
6389
|
-
const dbPath =
|
|
7373
|
+
const dbPath = join10(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
6390
7374
|
try {
|
|
6391
7375
|
return statSync4(dbPath).size;
|
|
6392
7376
|
} catch (err) {
|
|
6393
|
-
const
|
|
6394
|
-
if (!
|
|
7377
|
+
const isMissingFile4 = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
7378
|
+
if (!isMissingFile4) {
|
|
6395
7379
|
console.error(
|
|
6396
7380
|
`[statSessionDbBytes] could not stat ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
6397
7381
|
);
|
|
@@ -6416,12 +7400,12 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
6416
7400
|
// src/lib/opencode/log-tail.ts
|
|
6417
7401
|
import { statSync as statSync5 } from "node:fs";
|
|
6418
7402
|
import { homedir as homedir5 } from "node:os";
|
|
6419
|
-
import { join as
|
|
7403
|
+
import { join as join11 } from "node:path";
|
|
6420
7404
|
import { open as open2, stat } from "node:fs/promises";
|
|
6421
7405
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
6422
7406
|
function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
|
|
6423
|
-
const dataDir = env.XDG_DATA_HOME ||
|
|
6424
|
-
return
|
|
7407
|
+
const dataDir = env.XDG_DATA_HOME || join11(homeDir, ".local", "share");
|
|
7408
|
+
return join11(dataDir, "opencode", "log", "opencode.log");
|
|
6425
7409
|
}
|
|
6426
7410
|
function isEnoent(error2) {
|
|
6427
7411
|
return error2?.code === "ENOENT";
|
|
@@ -6906,8 +7890,8 @@ function connectTunnel(options) {
|
|
|
6906
7890
|
}
|
|
6907
7891
|
forwarder.handleBinaryBodyFrame(frame.sid, frame.payload);
|
|
6908
7892
|
} catch (error2) {
|
|
6909
|
-
const
|
|
6910
|
-
onError?.(`Failed to handle binary message: ${
|
|
7893
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
7894
|
+
onError?.(`Failed to handle binary message: ${errorMessage4}`);
|
|
6911
7895
|
}
|
|
6912
7896
|
return;
|
|
6913
7897
|
}
|
|
@@ -6915,8 +7899,8 @@ function connectTunnel(options) {
|
|
|
6915
7899
|
try {
|
|
6916
7900
|
message = JSON.parse(data.toString());
|
|
6917
7901
|
} catch (error2) {
|
|
6918
|
-
const
|
|
6919
|
-
onError?.(`Failed to handle message: ${
|
|
7902
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
7903
|
+
onError?.(`Failed to handle message: ${errorMessage4}`);
|
|
6920
7904
|
return;
|
|
6921
7905
|
}
|
|
6922
7906
|
if (isStreamFrame(message)) {
|
|
@@ -7088,10 +8072,10 @@ var RunnerConnection = class _RunnerConnection {
|
|
|
7088
8072
|
};
|
|
7089
8073
|
|
|
7090
8074
|
// src/lib/tunnel/ready-marker.ts
|
|
7091
|
-
import { writeFileSync as
|
|
8075
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
7092
8076
|
function writeTunnelReadyMarker(path, agentId) {
|
|
7093
8077
|
try {
|
|
7094
|
-
|
|
8078
|
+
writeFileSync6(path, `${agentId}
|
|
7095
8079
|
`);
|
|
7096
8080
|
return { ok: true };
|
|
7097
8081
|
} catch (error2) {
|
|
@@ -7116,7 +8100,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
7116
8100
|
}
|
|
7117
8101
|
|
|
7118
8102
|
// src/lib/process-liveness.ts
|
|
7119
|
-
import { readFileSync as
|
|
8103
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
7120
8104
|
function isProcessAlive(pid) {
|
|
7121
8105
|
try {
|
|
7122
8106
|
process.kill(pid, 0);
|
|
@@ -7131,7 +8115,7 @@ function isProcessAlive(pid) {
|
|
|
7131
8115
|
}
|
|
7132
8116
|
if (process.platform !== "linux") return true;
|
|
7133
8117
|
try {
|
|
7134
|
-
const status2 =
|
|
8118
|
+
const status2 = readFileSync10(`/proc/${pid}/status`, "utf8");
|
|
7135
8119
|
return !/^State:\s+Z(?:\s|$)/m.test(status2);
|
|
7136
8120
|
} catch (error2) {
|
|
7137
8121
|
console.error(
|
|
@@ -7198,8 +8182,8 @@ function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
|
7198
8182
|
}
|
|
7199
8183
|
|
|
7200
8184
|
// src/lib/resource-usage-reporting.ts
|
|
7201
|
-
var
|
|
7202
|
-
var
|
|
8185
|
+
var ENABLED_VALUES2 = /* @__PURE__ */ new Set(["on", "true", "1"]);
|
|
8186
|
+
var DISABLED_VALUES2 = /* @__PURE__ */ new Set(["off", "false", "0"]);
|
|
7203
8187
|
function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
7204
8188
|
if (flagValue === false) {
|
|
7205
8189
|
return { enabled: false, warnings: [] };
|
|
@@ -7209,10 +8193,10 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
|
7209
8193
|
return { enabled: true, warnings: [] };
|
|
7210
8194
|
}
|
|
7211
8195
|
const normalized = raw.trim().toLowerCase();
|
|
7212
|
-
if (
|
|
8196
|
+
if (DISABLED_VALUES2.has(normalized)) {
|
|
7213
8197
|
return { enabled: false, warnings: [] };
|
|
7214
8198
|
}
|
|
7215
|
-
if (
|
|
8199
|
+
if (ENABLED_VALUES2.has(normalized)) {
|
|
7216
8200
|
return { enabled: true, warnings: [] };
|
|
7217
8201
|
}
|
|
7218
8202
|
return {
|
|
@@ -7490,12 +8474,12 @@ function createSessionStoreHealth(options = {}) {
|
|
|
7490
8474
|
import { homedir as homedir6 } from "node:os";
|
|
7491
8475
|
|
|
7492
8476
|
// src/lib/runner-file-sync.ts
|
|
7493
|
-
import { join as
|
|
8477
|
+
import { join as join13 } from "node:path";
|
|
7494
8478
|
|
|
7495
8479
|
// src/lib/file-push.ts
|
|
7496
8480
|
import { randomUUID } from "node:crypto";
|
|
7497
8481
|
import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
|
|
7498
|
-
import { basename as basename2, dirname as
|
|
8482
|
+
import { basename as basename2, dirname as dirname9, isAbsolute as isAbsolute2, join as join12, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
|
7499
8483
|
var FILE_MODE = 384;
|
|
7500
8484
|
var DIRECTORY_MODE = 448;
|
|
7501
8485
|
async function writePushedFile(request3) {
|
|
@@ -7526,9 +8510,9 @@ async function writePushedFile(request3) {
|
|
|
7526
8510
|
}
|
|
7527
8511
|
try {
|
|
7528
8512
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
7529
|
-
|
|
8513
|
+
dirname9(candidate)
|
|
7530
8514
|
);
|
|
7531
|
-
const realTarget =
|
|
8515
|
+
const realTarget = join12(existingAncestor, ...missingSegments, basename2(candidate));
|
|
7532
8516
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget) ?? await findMatchingAllowedFile(allowedFiles, realTarget);
|
|
7533
8517
|
if (allowedDirectory === null) {
|
|
7534
8518
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -7538,8 +8522,8 @@ async function writePushedFile(request3) {
|
|
|
7538
8522
|
}
|
|
7539
8523
|
if (missingSegments.length > 0) {
|
|
7540
8524
|
await createMissingDirectories(existingAncestor, missingSegments);
|
|
7541
|
-
const realParent = await realpath(
|
|
7542
|
-
if (realParent !==
|
|
8525
|
+
const realParent = await realpath(dirname9(realTarget));
|
|
8526
|
+
if (realParent !== dirname9(realTarget) || !contains(allowedDirectory, realTarget)) {
|
|
7543
8527
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
7544
8528
|
path: realTarget,
|
|
7545
8529
|
bytes,
|
|
@@ -7564,7 +8548,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
7564
8548
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
7565
8549
|
return null;
|
|
7566
8550
|
}
|
|
7567
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
8551
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join12(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
7568
8552
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
7569
8553
|
return null;
|
|
7570
8554
|
}
|
|
@@ -7582,7 +8566,7 @@ async function resolveNearestExistingAncestor(directory) {
|
|
|
7582
8566
|
try {
|
|
7583
8567
|
return { existingAncestor: await realpath(current), missingSegments };
|
|
7584
8568
|
} catch (err) {
|
|
7585
|
-
const parent =
|
|
8569
|
+
const parent = dirname9(current);
|
|
7586
8570
|
if (err.code !== "ENOENT" || parent === current) {
|
|
7587
8571
|
throw err;
|
|
7588
8572
|
}
|
|
@@ -7613,8 +8597,8 @@ async function findMatchingAllowedFile(allowedFiles, realTarget) {
|
|
|
7613
8597
|
});
|
|
7614
8598
|
continue;
|
|
7615
8599
|
}
|
|
7616
|
-
const realParent = await realpathCreatingIfMissing(
|
|
7617
|
-
if (realParent !== null &&
|
|
8600
|
+
const realParent = await realpathCreatingIfMissing(dirname9(entry));
|
|
8601
|
+
if (realParent !== null && join12(realParent, basename2(entry)) === realTarget) {
|
|
7618
8602
|
return realParent;
|
|
7619
8603
|
}
|
|
7620
8604
|
}
|
|
@@ -7653,13 +8637,13 @@ function contains(realDirectory, realTarget) {
|
|
|
7653
8637
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
7654
8638
|
let current = existingAncestor;
|
|
7655
8639
|
for (const segment of missingSegments) {
|
|
7656
|
-
current =
|
|
8640
|
+
current = join12(current, segment);
|
|
7657
8641
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
7658
8642
|
await chmod(current, DIRECTORY_MODE);
|
|
7659
8643
|
}
|
|
7660
8644
|
}
|
|
7661
8645
|
async function writeAtomically(realTarget, content) {
|
|
7662
|
-
const temporaryPath =
|
|
8646
|
+
const temporaryPath = join12(dirname9(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
7663
8647
|
let handle;
|
|
7664
8648
|
try {
|
|
7665
8649
|
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
@@ -7789,12 +8773,12 @@ var NOT_APPLIED = {
|
|
|
7789
8773
|
opencodeAuthApplied: false
|
|
7790
8774
|
};
|
|
7791
8775
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
7792
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
7793
|
-
return expanded ===
|
|
8776
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
8777
|
+
return expanded === join13(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
7794
8778
|
}
|
|
7795
8779
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
7796
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
7797
|
-
return expanded ===
|
|
8780
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
8781
|
+
return expanded === join13(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
7798
8782
|
}
|
|
7799
8783
|
async function applyOne(options, file) {
|
|
7800
8784
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -7933,7 +8917,7 @@ function describe(err) {
|
|
|
7933
8917
|
}
|
|
7934
8918
|
|
|
7935
8919
|
// src/lib/vault-file-sync.ts
|
|
7936
|
-
import { createHash } from "node:crypto";
|
|
8920
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
7937
8921
|
async function syncVaultFiles(options) {
|
|
7938
8922
|
const files = await listVaultFiles(options);
|
|
7939
8923
|
if (files === null) return { applied: 0 };
|
|
@@ -8029,7 +9013,7 @@ async function reconcileVaultFile(options, file) {
|
|
|
8029
9013
|
}
|
|
8030
9014
|
const content = await downloadVaultFile(options, file);
|
|
8031
9015
|
if (content === null) return false;
|
|
8032
|
-
const actualSha256 =
|
|
9016
|
+
const actualSha256 = createHash2("sha256").update(content).digest("hex");
|
|
8033
9017
|
if (actualSha256 !== file.content_sha256) {
|
|
8034
9018
|
options.log({
|
|
8035
9019
|
level: "warn",
|
|
@@ -8230,6 +9214,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8230
9214
|
conversationFilter;
|
|
8231
9215
|
retry;
|
|
8232
9216
|
log;
|
|
9217
|
+
recordBinding;
|
|
8233
9218
|
fetchImpl;
|
|
8234
9219
|
sleep;
|
|
8235
9220
|
pausedPollIntervalMs;
|
|
@@ -8592,6 +9577,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8592
9577
|
this.retry = { ...DEFAULT_RETRY_POLICY, ...config.retry };
|
|
8593
9578
|
this.log = config.log ?? (() => {
|
|
8594
9579
|
});
|
|
9580
|
+
this.recordBinding = config.recordBinding;
|
|
8595
9581
|
this.fetchImpl = withRequestTimeout(
|
|
8596
9582
|
config.fetchImpl ?? fetch,
|
|
8597
9583
|
config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
|
|
@@ -8779,6 +9765,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8779
9765
|
let cappedSkips = 0;
|
|
8780
9766
|
for (const conv of conversations) {
|
|
8781
9767
|
if (this.stopped) break;
|
|
9768
|
+
if (conv.opencode_session_id) {
|
|
9769
|
+
this.recordSessionBinding(conv.id, conv.opencode_session_id);
|
|
9770
|
+
}
|
|
8782
9771
|
if (this.maxActiveSessions !== void 0) {
|
|
8783
9772
|
const activeSessionIds = this.activeSessionIdsForCap();
|
|
8784
9773
|
const resolvedSessionId = this.sessions.get(conv.id) ?? conv.opencode_session_id;
|
|
@@ -8861,12 +9850,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8861
9850
|
this.sessions.delete(conversationId);
|
|
8862
9851
|
this.supersede(conversationId, sessionId);
|
|
8863
9852
|
const dispatchDescription = source === "readopt" ? "re-dispatched message" : "message";
|
|
8864
|
-
const
|
|
8865
|
-
this.untrackableAck.set(messageId,
|
|
9853
|
+
const errorMessage4 = `OpenCode accepted this ${dispatchDescription} once, but its assigned id was never confirmed after ${attempts} attempts to match it up \u2014 the message was deliberately not sent again, so no duplicate turn was created; session ${sessionId.slice(0, 8)} was abandoned; a fresh one is used for further messages.`;
|
|
9854
|
+
this.untrackableAck.set(messageId, errorMessage4);
|
|
8866
9855
|
this.untrackableAckSessionIds.set(messageId, null);
|
|
8867
9856
|
this.log({
|
|
8868
9857
|
level: "error",
|
|
8869
|
-
message:
|
|
9858
|
+
message: errorMessage4,
|
|
8870
9859
|
conversation_id: conversationId,
|
|
8871
9860
|
message_id: messageId
|
|
8872
9861
|
});
|
|
@@ -8874,7 +9863,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8874
9863
|
conversationId,
|
|
8875
9864
|
messageId,
|
|
8876
9865
|
null,
|
|
8877
|
-
|
|
9866
|
+
errorMessage4
|
|
8878
9867
|
);
|
|
8879
9868
|
if (reportError !== null) {
|
|
8880
9869
|
this.log({
|
|
@@ -9055,12 +10044,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9055
10044
|
conversation_id: conv.id,
|
|
9056
10045
|
message_id: message.id
|
|
9057
10046
|
});
|
|
9058
|
-
const
|
|
10047
|
+
const errorMessage4 = this.untrackableAck.get(message.id);
|
|
9059
10048
|
const reportError = await this.reportUntrackableAck(
|
|
9060
10049
|
conv.id,
|
|
9061
10050
|
message.id,
|
|
9062
10051
|
null,
|
|
9063
|
-
|
|
10052
|
+
errorMessage4
|
|
9064
10053
|
);
|
|
9065
10054
|
if (reportError === null) {
|
|
9066
10055
|
continue;
|
|
@@ -9148,7 +10137,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9148
10137
|
const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
|
|
9149
10138
|
const providerId = hasProviderAndModel ? dispatchedModel.slice(0, separator) : null;
|
|
9150
10139
|
const modelId = hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null;
|
|
9151
|
-
const
|
|
10140
|
+
const errorMessage4 = `Provider namespace not loaded on this runner: ${message.opencode_model}. Refused before dispatch; no turn was started.`;
|
|
9152
10141
|
this.log({
|
|
9153
10142
|
level: "error",
|
|
9154
10143
|
message: `Refusing message ${message.id.slice(0, 8)} in conversation ${conv.id.slice(0, 8)}: provider namespace ${providerId ?? "(unknown)"} is not loaded; no turn was started`,
|
|
@@ -9156,7 +10145,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9156
10145
|
message_id: message.id
|
|
9157
10146
|
});
|
|
9158
10147
|
try {
|
|
9159
|
-
await this.markFailed(conv.id, message.id, sessionId,
|
|
10148
|
+
await this.markFailed(conv.id, message.id, sessionId, errorMessage4, void 0, {
|
|
9160
10149
|
classification: {
|
|
9161
10150
|
kind: "model_auth",
|
|
9162
10151
|
providerId,
|
|
@@ -9201,7 +10190,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9201
10190
|
false
|
|
9202
10191
|
);
|
|
9203
10192
|
}
|
|
9204
|
-
opencodeMessageId = this.isV2 ? await sendV2Prompt(
|
|
10193
|
+
opencodeMessageId = this.isV2 ? await sendV2Prompt(
|
|
10194
|
+
this.openCodeClient,
|
|
10195
|
+
sessionId,
|
|
10196
|
+
message.content,
|
|
10197
|
+
options,
|
|
10198
|
+
() => this.logInvalidV2ModelSelection(conv.id, message.id)
|
|
10199
|
+
) : await this.dispatchLocked(
|
|
9205
10200
|
sessionId,
|
|
9206
10201
|
() => sendPromptAsync(
|
|
9207
10202
|
this.port,
|
|
@@ -9228,12 +10223,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9228
10223
|
} catch (err) {
|
|
9229
10224
|
if (err instanceof ChannelAuthError) throw err;
|
|
9230
10225
|
if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
|
|
9231
|
-
const
|
|
9232
|
-
this.untrackableAck.set(message.id,
|
|
10226
|
+
const errorMessage5 = err instanceof Error ? err.message : String(err);
|
|
10227
|
+
this.untrackableAck.set(message.id, errorMessage5);
|
|
9233
10228
|
this.untrackableAckSessionIds.set(message.id, null);
|
|
9234
10229
|
this.log({
|
|
9235
10230
|
level: "error",
|
|
9236
|
-
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${
|
|
10231
|
+
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage5}`,
|
|
9237
10232
|
conversation_id: conv.id,
|
|
9238
10233
|
message_id: message.id
|
|
9239
10234
|
});
|
|
@@ -9241,7 +10236,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9241
10236
|
conv.id,
|
|
9242
10237
|
message.id,
|
|
9243
10238
|
null,
|
|
9244
|
-
|
|
10239
|
+
errorMessage5
|
|
9245
10240
|
);
|
|
9246
10241
|
if (markError !== null) {
|
|
9247
10242
|
this.log({
|
|
@@ -9276,7 +10271,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9276
10271
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
9277
10272
|
break;
|
|
9278
10273
|
}
|
|
9279
|
-
|
|
10274
|
+
if (this.isV2 && err instanceof OpenCodeV2ModelSwitchError) {
|
|
10275
|
+
await this.failV2ModelSwitch(conv, message, sessionId, err);
|
|
10276
|
+
break;
|
|
10277
|
+
}
|
|
10278
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
9280
10279
|
this.sessions.delete(conv.id);
|
|
9281
10280
|
this.supersede(conv.id, sessionId);
|
|
9282
10281
|
this.log({
|
|
@@ -9285,7 +10284,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9285
10284
|
conversation_id: conv.id,
|
|
9286
10285
|
message_id: message.id
|
|
9287
10286
|
});
|
|
9288
|
-
await this.markFailed(conv.id, message.id, null,
|
|
10287
|
+
await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
|
|
9289
10288
|
this.log({
|
|
9290
10289
|
level: "warn",
|
|
9291
10290
|
message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -9296,7 +10295,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9296
10295
|
});
|
|
9297
10296
|
this.log({
|
|
9298
10297
|
level: "error",
|
|
9299
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
10298
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage4}`,
|
|
9300
10299
|
conversation_id: conv.id,
|
|
9301
10300
|
message_id: message.id
|
|
9302
10301
|
});
|
|
@@ -9869,6 +10868,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9869
10868
|
this.dispatchNotStartedSignalled.set(message.id, branch);
|
|
9870
10869
|
void this.postSignal(conv.id, message.id, "dispatch_not_started", { branch });
|
|
9871
10870
|
}
|
|
10871
|
+
logInvalidV2ModelSelection(conversationId, messageId) {
|
|
10872
|
+
this.log({
|
|
10873
|
+
level: "warn",
|
|
10874
|
+
message: `Skipping an invalid OpenCode V2 model selection for message ${messageId.slice(0, 8)}; expected provider/model format`,
|
|
10875
|
+
conversation_id: conversationId,
|
|
10876
|
+
message_id: messageId
|
|
10877
|
+
});
|
|
10878
|
+
}
|
|
9872
10879
|
/**
|
|
9873
10880
|
* Class B (#1340): the runner DECIDED an outcome (reattach/settle/fail_permanent)
|
|
9874
10881
|
* but its own PATCH to record it failed. Fires at most once per (message,
|
|
@@ -10083,6 +11090,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10083
11090
|
return { sessionId: await this.createAndBindSession(conv.id), created: true };
|
|
10084
11091
|
}
|
|
10085
11092
|
this.sessions.set(conv.id, bound);
|
|
11093
|
+
this.recordSessionBinding(conv.id, bound);
|
|
10086
11094
|
return { sessionId: bound, created: false };
|
|
10087
11095
|
}
|
|
10088
11096
|
return { sessionId: await this.createAndBindSession(conv.id), created: true };
|
|
@@ -10096,6 +11104,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10096
11104
|
const directory = await this.resolveOpenCodeDirectory();
|
|
10097
11105
|
const sessionId = await this.createOpenCodeSession(directory);
|
|
10098
11106
|
this.sessions.set(conversationId, sessionId);
|
|
11107
|
+
this.recordSessionBinding(conversationId, sessionId);
|
|
10099
11108
|
await this.persistSession(conversationId, sessionId).catch((err) => {
|
|
10100
11109
|
this.log({
|
|
10101
11110
|
level: "warn",
|
|
@@ -10105,6 +11114,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10105
11114
|
});
|
|
10106
11115
|
return sessionId;
|
|
10107
11116
|
}
|
|
11117
|
+
recordSessionBinding(conversationId, sessionId) {
|
|
11118
|
+
if (!this.recordBinding) return;
|
|
11119
|
+
try {
|
|
11120
|
+
this.recordBinding(conversationId, sessionId);
|
|
11121
|
+
} catch (error2) {
|
|
11122
|
+
this.log({
|
|
11123
|
+
level: "warn",
|
|
11124
|
+
message: `Recording OpenCode session ${sessionId.slice(0, 8)} for conversation ${conversationId.slice(0, 8)} failed (best-effort): ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
11125
|
+
conversation_id: conversationId
|
|
11126
|
+
});
|
|
11127
|
+
}
|
|
11128
|
+
}
|
|
10108
11129
|
/**
|
|
10109
11130
|
* Lazily resolve (and cache) opencode's root directory via the selected client's
|
|
10110
11131
|
* location lookup.
|
|
@@ -12205,12 +13226,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12205
13226
|
conversation_id: row.conversation_id,
|
|
12206
13227
|
message_id: row.id
|
|
12207
13228
|
});
|
|
12208
|
-
const
|
|
13229
|
+
const errorMessage4 = this.untrackableAck.get(row.id);
|
|
12209
13230
|
const reportError = await this.reportUntrackableAck(
|
|
12210
13231
|
row.conversation_id,
|
|
12211
13232
|
row.id,
|
|
12212
13233
|
this.untrackableAckSessionIds.get(row.id) ?? null,
|
|
12213
|
-
|
|
13234
|
+
errorMessage4
|
|
12214
13235
|
);
|
|
12215
13236
|
if (reportError !== null) {
|
|
12216
13237
|
this.log({
|
|
@@ -12495,7 +13516,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12495
13516
|
let ocId;
|
|
12496
13517
|
let preDispatchUserIds = null;
|
|
12497
13518
|
try {
|
|
12498
|
-
ocId = this.isV2 ? await sendV2Prompt(
|
|
13519
|
+
ocId = this.isV2 ? await sendV2Prompt(
|
|
13520
|
+
this.openCodeClient,
|
|
13521
|
+
sessionId,
|
|
13522
|
+
row.content,
|
|
13523
|
+
options,
|
|
13524
|
+
() => this.logInvalidV2ModelSelection(row.conversation_id, row.id)
|
|
13525
|
+
) : await this.dispatchLocked(
|
|
12499
13526
|
sessionId,
|
|
12500
13527
|
() => sendPromptAsync(
|
|
12501
13528
|
this.port,
|
|
@@ -12523,14 +13550,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12523
13550
|
this.awaitingReadopt.delete(row.id);
|
|
12524
13551
|
if (err instanceof ChannelAuthError) throw err;
|
|
12525
13552
|
if (this.isV2) {
|
|
12526
|
-
const
|
|
13553
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
12527
13554
|
const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
|
|
12528
13555
|
if (!invalidPromptAck) {
|
|
12529
13556
|
const exists = await this.sessionExists(sessionId);
|
|
12530
13557
|
if (exists === false) {
|
|
12531
13558
|
this.log({
|
|
12532
13559
|
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: ${
|
|
13560
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch hit a session (${sessionId.slice(0, 8)}) that was deleted mid-dispatch \u2014 deferring to the next drain: ${errorMessage4}`,
|
|
12534
13561
|
conversation_id: row.conversation_id,
|
|
12535
13562
|
message_id: row.id
|
|
12536
13563
|
});
|
|
@@ -12540,7 +13567,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12540
13567
|
if (exists === null) {
|
|
12541
13568
|
this.log({
|
|
12542
13569
|
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: ${
|
|
13570
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch failed and session (${sessionId.slice(0, 8)}) existence could not be confirmed \u2014 deferring to the next drain: ${errorMessage4}`,
|
|
12544
13571
|
conversation_id: row.conversation_id,
|
|
12545
13572
|
message_id: row.id
|
|
12546
13573
|
});
|
|
@@ -12548,12 +13575,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12548
13575
|
return;
|
|
12549
13576
|
}
|
|
12550
13577
|
} else {
|
|
12551
|
-
this.untrackableAck.set(row.id,
|
|
13578
|
+
this.untrackableAck.set(row.id, errorMessage4);
|
|
12552
13579
|
this.untrackableAckSessionIds.set(row.id, sessionId);
|
|
12553
13580
|
}
|
|
13581
|
+
if (err instanceof OpenCodeV2ModelSwitchError) {
|
|
13582
|
+
await this.failV2ModelSwitch(readoptConv, readoptMessage, sessionId, err);
|
|
13583
|
+
return;
|
|
13584
|
+
}
|
|
12554
13585
|
this.log({
|
|
12555
13586
|
level: "error",
|
|
12556
|
-
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${
|
|
13587
|
+
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage4}`,
|
|
12557
13588
|
conversation_id: row.conversation_id,
|
|
12558
13589
|
message_id: row.id
|
|
12559
13590
|
});
|
|
@@ -12562,7 +13593,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12562
13593
|
row.conversation_id,
|
|
12563
13594
|
row.id,
|
|
12564
13595
|
sessionId,
|
|
12565
|
-
|
|
13596
|
+
errorMessage4
|
|
12566
13597
|
);
|
|
12567
13598
|
if (markError !== null) {
|
|
12568
13599
|
this.log({
|
|
@@ -12573,7 +13604,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12573
13604
|
});
|
|
12574
13605
|
}
|
|
12575
13606
|
} else {
|
|
12576
|
-
await this.markFailed(row.conversation_id, row.id, sessionId,
|
|
13607
|
+
await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage4).catch(
|
|
12577
13608
|
(markErr) => {
|
|
12578
13609
|
this.log({
|
|
12579
13610
|
level: "warn",
|
|
@@ -13557,9 +14588,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
13557
14588
|
* trackable. Callers use the returned error to keep their path-specific logging
|
|
13558
14589
|
* while the same report payload can be retried on a later drain.
|
|
13559
14590
|
*/
|
|
13560
|
-
async reportUntrackableAck(conversationId, messageId, sessionId,
|
|
14591
|
+
async reportUntrackableAck(conversationId, messageId, sessionId, errorMessage4) {
|
|
13561
14592
|
try {
|
|
13562
|
-
await this.markFailed(conversationId, messageId, sessionId,
|
|
14593
|
+
await this.markFailed(conversationId, messageId, sessionId, errorMessage4);
|
|
13563
14594
|
return null;
|
|
13564
14595
|
} catch (err) {
|
|
13565
14596
|
this.signalUntrackableAck(conversationId, messageId);
|
|
@@ -13628,6 +14659,38 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
13628
14659
|
}
|
|
13629
14660
|
this.clearSubagentInvocationCaches(messageId);
|
|
13630
14661
|
}
|
|
14662
|
+
async failV2ModelSwitch(conv, message, sessionId, error2) {
|
|
14663
|
+
const { model } = splitModelVariant(message.opencode_model ?? void 0);
|
|
14664
|
+
const separator = model?.indexOf("/") ?? -1;
|
|
14665
|
+
const classification = error2.statusCode === 404 && model && separator > 0 && separator < model.length - 1 ? {
|
|
14666
|
+
kind: "model_auth",
|
|
14667
|
+
providerId: model.slice(0, separator),
|
|
14668
|
+
modelId: model.slice(separator + 1),
|
|
14669
|
+
reason: "missing"
|
|
14670
|
+
} : null;
|
|
14671
|
+
this.log({
|
|
14672
|
+
level: "error",
|
|
14673
|
+
message: `OpenCode could not apply the selected model for message ${message.id.slice(0, 8)}: ${error2.message}`,
|
|
14674
|
+
conversation_id: conv.id,
|
|
14675
|
+
message_id: message.id
|
|
14676
|
+
});
|
|
14677
|
+
try {
|
|
14678
|
+
await this.markFailed(conv.id, message.id, sessionId, error2.message, void 0, {
|
|
14679
|
+
classification,
|
|
14680
|
+
errorShape: { name: null, statusCode: error2.statusCode }
|
|
14681
|
+
});
|
|
14682
|
+
} catch (markErr) {
|
|
14683
|
+
this.log({
|
|
14684
|
+
level: "warn",
|
|
14685
|
+
message: `markFailed PATCH for V2 model switch failure on message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
14686
|
+
conversation_id: conv.id,
|
|
14687
|
+
message_id: message.id
|
|
14688
|
+
});
|
|
14689
|
+
this.signalDispatchNotStarted(conv, message, "failure_unreported");
|
|
14690
|
+
return;
|
|
14691
|
+
}
|
|
14692
|
+
if (classification) await this.repairProviderRegistry(conv.id, message.id);
|
|
14693
|
+
}
|
|
13631
14694
|
/**
|
|
13632
14695
|
* Classify an errored turn and extract its bounded error shape for `markFailed`.
|
|
13633
14696
|
*
|
|
@@ -14071,7 +15134,7 @@ import ora3 from "ora";
|
|
|
14071
15134
|
import { select as select3 } from "@inquirer/prompts";
|
|
14072
15135
|
function unknownPasswordError2(port) {
|
|
14073
15136
|
return new Error(
|
|
14074
|
-
`
|
|
15137
|
+
`An assistant on port ${port} requires credentials this runner does not have. Released OpenCode V2 generates a server password even when none is configured. This CLI has no option to supply the password for an already-running server, so it will not join one you started. Free port ${port} so Evident can start its own, or pass --port to choose another.`
|
|
14075
15138
|
);
|
|
14076
15139
|
}
|
|
14077
15140
|
var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
|
|
@@ -14089,9 +15152,9 @@ async function ensureOpenCode2Running(ctx) {
|
|
|
14089
15152
|
password: null
|
|
14090
15153
|
};
|
|
14091
15154
|
}
|
|
14092
|
-
if (!isOpenCode2Installed()) {
|
|
15155
|
+
if (!ctx.command && !isOpenCode2Installed()) {
|
|
14093
15156
|
throw new Error(
|
|
14094
|
-
|
|
15157
|
+
"OpenCode V2 is not installed. Install it with `npm install -g @opencode/cli` or `curl -fsSL https://opencode.ai/v2/install | bash`."
|
|
14095
15158
|
);
|
|
14096
15159
|
}
|
|
14097
15160
|
let port = ctx.port;
|
|
@@ -14119,7 +15182,8 @@ Port ${port} is already in use.`));
|
|
|
14119
15182
|
if (!ctx.interactive) {
|
|
14120
15183
|
ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
|
|
14121
15184
|
const { child: proc, password } = await startOpenCode2(port, {
|
|
14122
|
-
inheritStdio: ctx.inheritStdio
|
|
15185
|
+
inheritStdio: ctx.inheritStdio,
|
|
15186
|
+
command: ctx.command
|
|
14123
15187
|
});
|
|
14124
15188
|
const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
|
|
14125
15189
|
if (!health.healthy) {
|
|
@@ -14140,13 +15204,14 @@ Port ${port} is already in use.`));
|
|
|
14140
15204
|
password
|
|
14141
15205
|
};
|
|
14142
15206
|
}
|
|
15207
|
+
const command = ctx.command ?? "opencode2";
|
|
14143
15208
|
const action = await select3({
|
|
14144
15209
|
message: "OpenCode V2 is not running. What would you like to do?",
|
|
14145
15210
|
choices: [
|
|
14146
15211
|
{
|
|
14147
15212
|
name: "Start OpenCode V2 for me",
|
|
14148
15213
|
value: "start",
|
|
14149
|
-
description: `Run '
|
|
15214
|
+
description: `Run '${command} serve --port ${port}'`
|
|
14150
15215
|
},
|
|
14151
15216
|
{
|
|
14152
15217
|
name: "Show me the command",
|
|
@@ -14164,14 +15229,15 @@ Port ${port} is already in use.`));
|
|
|
14164
15229
|
blank();
|
|
14165
15230
|
console.log(chalk6.bold("Run this command in another terminal:"));
|
|
14166
15231
|
blank();
|
|
14167
|
-
console.log(` ${chalk6.cyan(
|
|
15232
|
+
console.log(` ${chalk6.cyan(`${command} serve --port ${port}`)}`);
|
|
14168
15233
|
blank();
|
|
14169
15234
|
throw new Error("Please start OpenCode V2 manually");
|
|
14170
15235
|
}
|
|
14171
15236
|
if (action === "start") {
|
|
14172
15237
|
const spinner = ora3("Starting OpenCode V2...").start();
|
|
14173
15238
|
const { child: proc, password } = await startOpenCode2(port, {
|
|
14174
|
-
inheritStdio: ctx.inheritStdio
|
|
15239
|
+
inheritStdio: ctx.inheritStdio,
|
|
15240
|
+
command: ctx.command
|
|
14175
15241
|
});
|
|
14176
15242
|
const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
|
|
14177
15243
|
if (!health.healthy) {
|
|
@@ -14197,7 +15263,7 @@ Port ${port} is already in use.`));
|
|
|
14197
15263
|
}
|
|
14198
15264
|
|
|
14199
15265
|
// src/lib/runner-credentials.ts
|
|
14200
|
-
import { chmodSync as chmodSync2, writeFileSync as
|
|
15266
|
+
import { chmodSync as chmodSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
14201
15267
|
import { spawn as spawn5 } from "node:child_process";
|
|
14202
15268
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
14203
15269
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
@@ -14440,8 +15506,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
14440
15506
|
}
|
|
14441
15507
|
try {
|
|
14442
15508
|
env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
|
|
14443
|
-
|
|
14444
|
-
|
|
15509
|
+
writeFileSync7(GIT_CONFIG_GLOBAL, "");
|
|
15510
|
+
writeFileSync7(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
|
|
14445
15511
|
chmodSync2(GIT_CREDENTIAL_HELPER, 448);
|
|
14446
15512
|
const config = [
|
|
14447
15513
|
["user.name", env.GIT_USER_NAME ?? "evident-bot"],
|
|
@@ -14474,7 +15540,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
14474
15540
|
// src/lib/opencode/config-overlay.ts
|
|
14475
15541
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
14476
15542
|
import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
|
|
14477
|
-
import { isAbsolute as isAbsolute3, join as
|
|
15543
|
+
import { isAbsolute as isAbsolute3, join as join14, resolve as resolve4 } from "node:path";
|
|
14478
15544
|
function parseConfigContent(content) {
|
|
14479
15545
|
if (!content?.trim()) return {};
|
|
14480
15546
|
const parsed = JSON.parse(content);
|
|
@@ -14504,7 +15570,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
14504
15570
|
return;
|
|
14505
15571
|
}
|
|
14506
15572
|
const source = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
|
|
14507
|
-
const target = isFile(
|
|
15573
|
+
const target = isFile(join14(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
14508
15574
|
if (!isFile(source)) {
|
|
14509
15575
|
log3(
|
|
14510
15576
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -14512,7 +15578,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
14512
15578
|
);
|
|
14513
15579
|
return;
|
|
14514
15580
|
}
|
|
14515
|
-
copyFileSync(source,
|
|
15581
|
+
copyFileSync(source, join14(cwd, target));
|
|
14516
15582
|
try {
|
|
14517
15583
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
14518
15584
|
stdio: "ignore"
|
|
@@ -14521,13 +15587,13 @@ function applyRunnerOpenCodeConfig({
|
|
|
14521
15587
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
14522
15588
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
14523
15589
|
}
|
|
14524
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
15590
|
+
log3(`Applied runner OpenCode config ${source} to ${join14(cwd, target)}`);
|
|
14525
15591
|
}
|
|
14526
15592
|
|
|
14527
15593
|
// src/lib/opencode/runtime-provenance.ts
|
|
14528
|
-
import { readFileSync as
|
|
15594
|
+
import { readFileSync as readFileSync11, readdirSync } from "node:fs";
|
|
14529
15595
|
import { homedir as homedir7 } from "node:os";
|
|
14530
|
-
import { join as
|
|
15596
|
+
import { join as join15 } from "node:path";
|
|
14531
15597
|
var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
|
14532
15598
|
var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
|
|
14533
15599
|
function errorKind(error2) {
|
|
@@ -14595,7 +15661,7 @@ function parseJsonc(value) {
|
|
|
14595
15661
|
return JSON.parse(output);
|
|
14596
15662
|
}
|
|
14597
15663
|
function openCodeCacheRoot(env = process.env, homeDir = homedir7()) {
|
|
14598
|
-
return
|
|
15664
|
+
return join15(env.XDG_CACHE_HOME ?? join15(homeDir, ".cache"), "opencode");
|
|
14599
15665
|
}
|
|
14600
15666
|
function readConfiguredPluginSpecs({
|
|
14601
15667
|
cwd = process.cwd(),
|
|
@@ -14604,10 +15670,10 @@ function readConfiguredPluginSpecs({
|
|
|
14604
15670
|
let config;
|
|
14605
15671
|
try {
|
|
14606
15672
|
try {
|
|
14607
|
-
config = parseJsonc(
|
|
15673
|
+
config = parseJsonc(readFileSync11(join15(cwd, "opencode.jsonc"), "utf8"));
|
|
14608
15674
|
} catch (error2) {
|
|
14609
15675
|
if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
|
|
14610
|
-
config = parseJsonc(
|
|
15676
|
+
config = parseJsonc(readFileSync11(join15(cwd, "opencode.json"), "utf8"));
|
|
14611
15677
|
}
|
|
14612
15678
|
} catch (error2) {
|
|
14613
15679
|
log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
|
|
@@ -14635,7 +15701,7 @@ function resolveInstalledPluginVersions({
|
|
|
14635
15701
|
homeDir = homedir7(),
|
|
14636
15702
|
log: log3
|
|
14637
15703
|
}) {
|
|
14638
|
-
const packagesRoot =
|
|
15704
|
+
const packagesRoot = join15(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
|
|
14639
15705
|
let directories;
|
|
14640
15706
|
try {
|
|
14641
15707
|
directories = readdirSync(packagesRoot, { withFileTypes: true });
|
|
@@ -14656,8 +15722,8 @@ function resolveInstalledPluginVersions({
|
|
|
14656
15722
|
let resolvedVersion = null;
|
|
14657
15723
|
try {
|
|
14658
15724
|
const packageJson = JSON.parse(
|
|
14659
|
-
|
|
14660
|
-
|
|
15725
|
+
readFileSync11(
|
|
15726
|
+
join15(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
|
|
14661
15727
|
"utf8"
|
|
14662
15728
|
)
|
|
14663
15729
|
);
|
|
@@ -14678,7 +15744,7 @@ function resolveInstalledPluginVersions({
|
|
|
14678
15744
|
}
|
|
14679
15745
|
|
|
14680
15746
|
// src/lib/credential-sync.ts
|
|
14681
|
-
import { renameSync, writeFileSync as
|
|
15747
|
+
import { renameSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
14682
15748
|
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
14683
15749
|
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
14684
15750
|
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
@@ -14688,7 +15754,7 @@ var MAX_FLUSH_PASSES = 2;
|
|
|
14688
15754
|
function outcomesWith(outcome) {
|
|
14689
15755
|
return { claude: outcome, opencode: outcome };
|
|
14690
15756
|
}
|
|
14691
|
-
function
|
|
15757
|
+
function errorMessage3(error2) {
|
|
14692
15758
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
14693
15759
|
}
|
|
14694
15760
|
function waitForSettlement(promise, timeoutMs) {
|
|
@@ -14712,10 +15778,10 @@ function writeMarker(markerPath, outcomes, log3) {
|
|
|
14712
15778
|
`;
|
|
14713
15779
|
const temporaryPath = `${markerPath}.tmp`;
|
|
14714
15780
|
try {
|
|
14715
|
-
|
|
15781
|
+
writeFileSync8(temporaryPath, body, { mode: 384 });
|
|
14716
15782
|
renameSync(temporaryPath, markerPath);
|
|
14717
15783
|
} catch (error2) {
|
|
14718
|
-
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${
|
|
15784
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage3(error2)}`, "warn");
|
|
14719
15785
|
}
|
|
14720
15786
|
}
|
|
14721
15787
|
function intervalSeconds(env, log3) {
|
|
@@ -14747,7 +15813,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
|
14747
15813
|
},
|
|
14748
15814
|
(error2) => {
|
|
14749
15815
|
failed = true;
|
|
14750
|
-
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${
|
|
15816
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage3(error2)}`, "warn");
|
|
14751
15817
|
}
|
|
14752
15818
|
);
|
|
14753
15819
|
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
@@ -14807,7 +15873,7 @@ function createCredentialSync({
|
|
|
14807
15873
|
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
14808
15874
|
} catch (error2) {
|
|
14809
15875
|
outcomes[store] = "failed";
|
|
14810
|
-
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${
|
|
15876
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage3(error2)}`, "debug");
|
|
14811
15877
|
}
|
|
14812
15878
|
}
|
|
14813
15879
|
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
@@ -14930,14 +15996,21 @@ function request2(options) {
|
|
|
14930
15996
|
function safeFailure(operation, status2) {
|
|
14931
15997
|
return status2 === void 0 ? `${operation} could not reach the managed credential service` : `${operation} returned HTTP ${status2}`;
|
|
14932
15998
|
}
|
|
14933
|
-
function
|
|
14934
|
-
let
|
|
15999
|
+
function redactAuthorizedText(message, authorization) {
|
|
16000
|
+
let redacted = message;
|
|
14935
16001
|
const credential = authorization.split(/\s+/, 2)[1];
|
|
14936
16002
|
for (const secret of [authorization, credential]) {
|
|
14937
|
-
if (secret)
|
|
16003
|
+
if (secret) redacted = redacted.replaceAll(secret, "[redacted]");
|
|
14938
16004
|
}
|
|
16005
|
+
return redactDiagnosticText(redacted);
|
|
16006
|
+
}
|
|
16007
|
+
function logTransportFailure(options, operation, error2, authorization) {
|
|
16008
|
+
const detail = redactAuthorizedText(
|
|
16009
|
+
error2 instanceof Error ? error2.message : String(error2),
|
|
16010
|
+
authorization
|
|
16011
|
+
);
|
|
14939
16012
|
options.log(
|
|
14940
|
-
`MANAGED-INFERENCE-TRANSPORT-FAILED: operation=${operation} detail=${
|
|
16013
|
+
`MANAGED-INFERENCE-TRANSPORT-FAILED: operation=${operation} detail=${detail}`,
|
|
14941
16014
|
"debug"
|
|
14942
16015
|
);
|
|
14943
16016
|
}
|
|
@@ -14957,6 +16030,18 @@ function readSetup(value) {
|
|
|
14957
16030
|
}
|
|
14958
16031
|
return setup;
|
|
14959
16032
|
}
|
|
16033
|
+
function readModelDescriptors(value) {
|
|
16034
|
+
if (!Array.isArray(value)) return null;
|
|
16035
|
+
return value.every(
|
|
16036
|
+
(descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
|
|
16037
|
+
) ? value : null;
|
|
16038
|
+
}
|
|
16039
|
+
function logDescriptorRefreshFallback(options, branch, generation) {
|
|
16040
|
+
options.log(
|
|
16041
|
+
`MANAGED-INFERENCE-DESCRIPTORS-${branch}: runner=${options.agentId} generation=${generation}`,
|
|
16042
|
+
"warn"
|
|
16043
|
+
);
|
|
16044
|
+
}
|
|
14960
16045
|
function clearLocalSetup(options) {
|
|
14961
16046
|
const env = options.env ?? process.env;
|
|
14962
16047
|
const store = options.store ?? defaultStore;
|
|
@@ -14988,7 +16073,14 @@ async function discoverManagedInferenceSetup(options) {
|
|
|
14988
16073
|
return { kind: "none" };
|
|
14989
16074
|
}
|
|
14990
16075
|
if (!response.ok) {
|
|
14991
|
-
|
|
16076
|
+
const body2 = await responseJson(response);
|
|
16077
|
+
const serverMessage = typeof body2?.error === "string" && body2.error.trim() ? redactAuthorizedText(body2.error.trim(), headers.Authorization).slice(0, 500) : void 0;
|
|
16078
|
+
const failure = serverMessage ? `Managed setup discovery returned HTTP ${response.status}: ${serverMessage}` : safeFailure("Managed setup discovery", response.status);
|
|
16079
|
+
const runnerKeyGuidance = response.status === 401 || response.status === 403 ? "Authenticate as this runner with its runner key (EVIDENT_RUNNER_KEY)." : void 0;
|
|
16080
|
+
return {
|
|
16081
|
+
kind: "failed",
|
|
16082
|
+
reason: runnerKeyGuidance ? `${failure}. ${runnerKeyGuidance}` : failure
|
|
16083
|
+
};
|
|
14992
16084
|
}
|
|
14993
16085
|
const body = await responseJson(response);
|
|
14994
16086
|
const setup = readSetup(body?.setup);
|
|
@@ -15088,7 +16180,27 @@ async function reaffirmStored(options, credential) {
|
|
|
15088
16180
|
if (!setup || setup.generation !== credential.generation || setup.token_id !== credential.tokenId) {
|
|
15089
16181
|
return { kind: "failed", reason: "Managed credential reaffirmation returned a mismatch" };
|
|
15090
16182
|
}
|
|
15091
|
-
const
|
|
16183
|
+
const descriptorsValue = body?.model_descriptors;
|
|
16184
|
+
const refreshedDescriptors = readModelDescriptors(descriptorsValue);
|
|
16185
|
+
let credentialToApply = credential;
|
|
16186
|
+
if (descriptorsValue === void 0 || descriptorsValue === null) {
|
|
16187
|
+
logDescriptorRefreshFallback(options, "UNAVAILABLE", credential.generation);
|
|
16188
|
+
} else if (refreshedDescriptors === null) {
|
|
16189
|
+
logDescriptorRefreshFallback(options, "MALFORMED", credential.generation);
|
|
16190
|
+
} else if (refreshedDescriptors.length === 0) {
|
|
16191
|
+
logDescriptorRefreshFallback(options, "EMPTY", credential.generation);
|
|
16192
|
+
} else {
|
|
16193
|
+
const { config, unsupportedProviders } = buildCredentialProxyNativeConfig({
|
|
16194
|
+
inferenceBaseUrl: credential.inferenceBaseUrl,
|
|
16195
|
+
modelDescriptors: refreshedDescriptors
|
|
16196
|
+
});
|
|
16197
|
+
if (unsupportedProviders.length > 0 || Object.keys(config.provider).length === 0) {
|
|
16198
|
+
logDescriptorRefreshFallback(options, "UNSUPPORTED-PROVIDER", credential.generation);
|
|
16199
|
+
} else {
|
|
16200
|
+
credentialToApply = { ...credential, modelDescriptors: refreshedDescriptors };
|
|
16201
|
+
}
|
|
16202
|
+
}
|
|
16203
|
+
const failure = applyCredential(options, credentialToApply);
|
|
15092
16204
|
return failure ? { kind: "failed", reason: failure } : { kind: "ready", generation: credential.generation, tokenId: credential.tokenId };
|
|
15093
16205
|
}
|
|
15094
16206
|
async function prepareManagedInferenceSetup(options, discovery, health) {
|
|
@@ -15122,10 +16234,8 @@ async function prepareManagedInferenceSetup(options, discovery, health) {
|
|
|
15122
16234
|
const setup = readSetup(body?.setup);
|
|
15123
16235
|
const token = body?.inference_token;
|
|
15124
16236
|
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
|
-
)) {
|
|
16237
|
+
const modelDescriptors = readModelDescriptors(body?.model_descriptors);
|
|
16238
|
+
if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" || modelDescriptors === null) {
|
|
15129
16239
|
return { kind: "failed", reason: "Managed credential bootstrap returned an invalid response" };
|
|
15130
16240
|
}
|
|
15131
16241
|
const credential = {
|
|
@@ -15199,7 +16309,7 @@ function resolveFileSyncEntry(entry, homeDir, flagName, labels) {
|
|
|
15199
16309
|
if (trimmed === "") {
|
|
15200
16310
|
throw new Error(`${flagName} requires a ${labels.noun} path (got an empty value)`);
|
|
15201
16311
|
}
|
|
15202
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
16312
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join16(homeDir, trimmed.slice(2)) : trimmed;
|
|
15203
16313
|
if (!isAbsolute4(expanded)) {
|
|
15204
16314
|
throw new Error(`${flagName} requires an absolute ${labels.noun} path; got "${entry}"`);
|
|
15205
16315
|
}
|
|
@@ -15252,13 +16362,13 @@ function resolveOpenCodeVersion(options, env = process.env) {
|
|
|
15252
16362
|
raw = env[OPENCODE_VERSION_ENV];
|
|
15253
16363
|
source = OPENCODE_VERSION_ENV;
|
|
15254
16364
|
} else {
|
|
15255
|
-
return { version: "
|
|
16365
|
+
return { version: "auto", warnings: [] };
|
|
15256
16366
|
}
|
|
15257
16367
|
const normalized = raw.trim().toLowerCase();
|
|
15258
|
-
if (normalized !== "v1" && normalized !== "v2") {
|
|
16368
|
+
if (normalized !== "v1" && normalized !== "v2" && normalized !== "auto") {
|
|
15259
16369
|
return {
|
|
15260
|
-
version: "
|
|
15261
|
-
warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or
|
|
16370
|
+
version: "auto",
|
|
16371
|
+
warnings: [`Ignoring invalid ${source} "${raw}": expected v1, v2, or auto; using auto`]
|
|
15262
16372
|
};
|
|
15263
16373
|
}
|
|
15264
16374
|
return { version: normalized, warnings: [] };
|
|
@@ -15768,8 +16878,8 @@ async function driveChannels(state, driver) {
|
|
|
15768
16878
|
state.running = false;
|
|
15769
16879
|
break;
|
|
15770
16880
|
}
|
|
15771
|
-
const
|
|
15772
|
-
logActivity(state, { type: "error", error: `Channel processing error: ${
|
|
16881
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : String(error2);
|
|
16882
|
+
logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage4}` });
|
|
15773
16883
|
if (state.interactive) displayStatus(state);
|
|
15774
16884
|
if (driver.hasInFlightWatchers()) {
|
|
15775
16885
|
consecutiveDrainFailures = 0;
|
|
@@ -15859,6 +16969,7 @@ async function driveChannels(state, driver) {
|
|
|
15859
16969
|
}
|
|
15860
16970
|
}
|
|
15861
16971
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
16972
|
+
var SESSION_ARCHIVE_FIRST_RUN_MS = SESSION_CLEANUP_FIRST_SWEEP_MS + 1e3;
|
|
15862
16973
|
var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
|
|
15863
16974
|
var SESSION_CLEANUP_MAX_ISOLATED_DELETE_FAILURES = 2;
|
|
15864
16975
|
var SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES = 2;
|
|
@@ -15873,7 +16984,7 @@ function shouldWarnForReclaimSkip(reason) {
|
|
|
15873
16984
|
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
15874
16985
|
}
|
|
15875
16986
|
function sessionDbPath() {
|
|
15876
|
-
return
|
|
16987
|
+
return join16(homedir8(), ".local", "share", "opencode", "opencode.db");
|
|
15877
16988
|
}
|
|
15878
16989
|
function logSessionCleanupActivity(state, phase, entry) {
|
|
15879
16990
|
logActivity(state, {
|
|
@@ -15882,6 +16993,63 @@ function logSessionCleanupActivity(state, phase, entry) {
|
|
|
15882
16993
|
metadata: { ...entry.metadata, phase }
|
|
15883
16994
|
});
|
|
15884
16995
|
}
|
|
16996
|
+
function logSessionArchiveActivity(state, phase, entry) {
|
|
16997
|
+
logActivity(state, {
|
|
16998
|
+
...entry,
|
|
16999
|
+
source: "cli.session-archive",
|
|
17000
|
+
metadata: { ...entry.metadata, phase }
|
|
17001
|
+
});
|
|
17002
|
+
}
|
|
17003
|
+
function logSessionArchiveSummary(state, phase, summary) {
|
|
17004
|
+
const everyUploadFailed = summary.uploadAttempts > 0 && summary.uploadFailures === summary.uploadAttempts;
|
|
17005
|
+
const staleSince = everyUploadFailed && summary.staleSince !== null ? `; archive stale since ${summary.staleSince}` : "";
|
|
17006
|
+
const neverWatermarked = summary.neverWatermarkedFailures > 0 ? `; ${summary.neverWatermarkedFailures} failed session(s) had no prior successful upload` : "";
|
|
17007
|
+
const warning2 = summary.failed > 0 || summary.failures.length > 0 || everyUploadFailed;
|
|
17008
|
+
logSessionArchiveActivity(state, phase, {
|
|
17009
|
+
type: "info",
|
|
17010
|
+
level: warning2 ? "warn" : "info",
|
|
17011
|
+
message: `Session archive: inspected ${summary.inspected}, uploaded ${summary.uploaded}, unchanged ${summary.unchanged}, failed ${summary.failed}, skipped budget ${summary.skippedBudget}, unmapped ${summary.unmappedIds.length}` + staleSince + neverWatermarked,
|
|
17012
|
+
metadata: {
|
|
17013
|
+
outcome: "archive-run-complete",
|
|
17014
|
+
sessions_inspected: summary.inspected,
|
|
17015
|
+
sessions_uploaded: summary.uploaded,
|
|
17016
|
+
sessions_unchanged: summary.unchanged,
|
|
17017
|
+
sessions_failed: summary.failed,
|
|
17018
|
+
uploads_attempted: summary.uploadAttempts,
|
|
17019
|
+
uploads_failed: summary.uploadFailures,
|
|
17020
|
+
sessions_skipped_budget: summary.skippedBudget,
|
|
17021
|
+
sessions_unmapped: summary.unmappedIds.length,
|
|
17022
|
+
sessions_without_prior_watermark: summary.neverWatermarkedFailures,
|
|
17023
|
+
...everyUploadFailed && summary.staleSince !== null ? { stale_since: summary.staleSince } : {}
|
|
17024
|
+
}
|
|
17025
|
+
});
|
|
17026
|
+
for (const failure of summary.failures) {
|
|
17027
|
+
const permanentSizeFailure = failure.reason === "too-large";
|
|
17028
|
+
const safeMessage = failure.message ? `: ${redactDiagnosticText(failure.message).slice(0, 200)}` : "";
|
|
17029
|
+
logSessionArchiveActivity(state, phase, {
|
|
17030
|
+
type: "info",
|
|
17031
|
+
level: "warn",
|
|
17032
|
+
message: `Session archive: session ${failure.sessionId} ` + (permanentSizeFailure ? "failed permanently (too-large payload)" : `failed (${failure.outcome}`) + (permanentSizeFailure ? "" : failure.reason ? `, reason=${failure.reason})` : ")") + safeMessage,
|
|
17033
|
+
metadata: {
|
|
17034
|
+
outcome: failure.outcome,
|
|
17035
|
+
session_id: failure.sessionId,
|
|
17036
|
+
archived: failure.archived,
|
|
17037
|
+
...failure.reason !== void 0 ? { reason: failure.reason } : {}
|
|
17038
|
+
}
|
|
17039
|
+
});
|
|
17040
|
+
}
|
|
17041
|
+
if (summary.unmappedIds.length > 0) {
|
|
17042
|
+
logSessionArchiveActivity(state, phase, {
|
|
17043
|
+
type: "info",
|
|
17044
|
+
level: "warn",
|
|
17045
|
+
message: `Session archive: ${summary.unmappedIds.length} unmapped session(s): ` + summary.unmappedIds.join(", "),
|
|
17046
|
+
metadata: {
|
|
17047
|
+
outcome: "unmapped-sessions",
|
|
17048
|
+
sessions_unmapped: summary.unmappedIds.length
|
|
17049
|
+
}
|
|
17050
|
+
});
|
|
17051
|
+
}
|
|
17052
|
+
}
|
|
15885
17053
|
async function drainSessionDeletionReports(state) {
|
|
15886
17054
|
const path = sessionDeletionReportPath(homedir8(), process.env);
|
|
15887
17055
|
let delivered = 0;
|
|
@@ -15979,6 +17147,7 @@ async function runSweep(state, driver, config) {
|
|
|
15979
17147
|
let failedOtherStatus = 0;
|
|
15980
17148
|
let failedThrew = 0;
|
|
15981
17149
|
let skippedNewlyActive = 0;
|
|
17150
|
+
let skippedArchive = 0;
|
|
15982
17151
|
let remaining = 0;
|
|
15983
17152
|
const deletionRecords = [];
|
|
15984
17153
|
let protectedNow = driver.protectedSessionIds();
|
|
@@ -15990,6 +17159,7 @@ async function runSweep(state, driver, config) {
|
|
|
15990
17159
|
const lastActivityById = new Map(
|
|
15991
17160
|
snapshots.map(({ id, lastActivityMs }) => [id, lastActivityMs])
|
|
15992
17161
|
);
|
|
17162
|
+
const sessionById = new Map(sessions.map((session) => [session.id, session]));
|
|
15993
17163
|
const toDelete = selectSessionsToDelete(snapshots, {
|
|
15994
17164
|
maxAgeMs: config.maxAgeMs,
|
|
15995
17165
|
maxCount: config.maxCount,
|
|
@@ -16012,6 +17182,42 @@ async function runSweep(state, driver, config) {
|
|
|
16012
17182
|
});
|
|
16013
17183
|
continue;
|
|
16014
17184
|
}
|
|
17185
|
+
if (state.sessionArchiveConfig.enabled) {
|
|
17186
|
+
const candidate = sessionById.get(id);
|
|
17187
|
+
const archiveConfig = state.sessionArchiveConfig;
|
|
17188
|
+
const archiveBudgetMs = Math.min(
|
|
17189
|
+
archiveConfig.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
|
|
17190
|
+
MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
|
|
17191
|
+
);
|
|
17192
|
+
const archived = candidate ? await runSessionArchiveTask(
|
|
17193
|
+
state,
|
|
17194
|
+
"sweep",
|
|
17195
|
+
archiveBudgetMs,
|
|
17196
|
+
(signal) => runSessionArchiveBatch(state, "sweep", [candidate], archiveBudgetMs, signal)
|
|
17197
|
+
) : null;
|
|
17198
|
+
if (!archived?.archivedSessionIds.includes(id)) {
|
|
17199
|
+
skippedArchive++;
|
|
17200
|
+
logSessionArchiveActivity(state, "sweep", {
|
|
17201
|
+
type: "info",
|
|
17202
|
+
level: "warn",
|
|
17203
|
+
message: `Session cleanup: retaining session ${id}; deletion is held back pending successful archival`,
|
|
17204
|
+
metadata: {
|
|
17205
|
+
outcome: "deletion-held-for-archive",
|
|
17206
|
+
session_id: id
|
|
17207
|
+
}
|
|
17208
|
+
});
|
|
17209
|
+
continue;
|
|
17210
|
+
}
|
|
17211
|
+
protectedNow = driver.protectedSessionIds();
|
|
17212
|
+
if (protectedNow.has(id)) {
|
|
17213
|
+
skippedNewlyActive++;
|
|
17214
|
+
logActivity(state, {
|
|
17215
|
+
type: "info",
|
|
17216
|
+
message: `Session cleanup: skipping ${id} \u2014 became active/bound during archival (${mode})`
|
|
17217
|
+
});
|
|
17218
|
+
continue;
|
|
17219
|
+
}
|
|
17220
|
+
}
|
|
16015
17221
|
const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
|
|
16016
17222
|
if (outcome.ok) {
|
|
16017
17223
|
deleted++;
|
|
@@ -16075,10 +17281,11 @@ async function runSweep(state, driver, config) {
|
|
|
16075
17281
|
}
|
|
16076
17282
|
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
16077
17283
|
const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
|
|
17284
|
+
const archiveHeldNote = skippedArchive > 0 ? `, archive held ${skippedArchive}` : "";
|
|
16078
17285
|
const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
|
|
16079
17286
|
logActivity(state, {
|
|
16080
17287
|
type: "info",
|
|
16081
|
-
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
|
|
17288
|
+
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${archiveHeldNote}${remainingNote} (${mode})`
|
|
16082
17289
|
});
|
|
16083
17290
|
}
|
|
16084
17291
|
if (deletionRecords.length > 0) {
|
|
@@ -16116,6 +17323,7 @@ async function runSweep(state, driver, config) {
|
|
|
16116
17323
|
sessions_failed_other: failedOtherStatus,
|
|
16117
17324
|
sessions_failed_threw: failedThrew,
|
|
16118
17325
|
sessions_skipped_active: skippedNewlyActive,
|
|
17326
|
+
sessions_archive_held: skippedArchive,
|
|
16119
17327
|
sessions_remaining: remaining,
|
|
16120
17328
|
...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
|
|
16121
17329
|
};
|
|
@@ -16247,6 +17455,186 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
16247
17455
|
);
|
|
16248
17456
|
state.sessionCleanupTimers.push(interval, firstSweep);
|
|
16249
17457
|
}
|
|
17458
|
+
function runSessionArchiveTask(state, phase, budgetMs, work) {
|
|
17459
|
+
if (state.sessionArchiveInFlight) {
|
|
17460
|
+
logSessionArchiveActivity(state, phase, {
|
|
17461
|
+
type: "info",
|
|
17462
|
+
level: "debug",
|
|
17463
|
+
message: "Session archive skipped \u2014 previous archive run still in progress",
|
|
17464
|
+
metadata: { outcome: "skipped-in-flight" }
|
|
17465
|
+
});
|
|
17466
|
+
return Promise.resolve(null);
|
|
17467
|
+
}
|
|
17468
|
+
const controller = new AbortController();
|
|
17469
|
+
state.sessionArchiveAbortController = controller;
|
|
17470
|
+
const timeout = setTimeout(() => {
|
|
17471
|
+
controller.abort();
|
|
17472
|
+
logSessionArchiveActivity(state, phase, {
|
|
17473
|
+
type: "info",
|
|
17474
|
+
level: "warn",
|
|
17475
|
+
message: `Session archive exceeded ${budgetMs}ms; remaining sessions were not confirmed archived`,
|
|
17476
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17477
|
+
});
|
|
17478
|
+
}, budgetMs);
|
|
17479
|
+
const task = Promise.resolve().then(() => work(controller.signal)).catch((error2) => {
|
|
17480
|
+
logSessionArchiveActivity(state, phase, {
|
|
17481
|
+
type: "info",
|
|
17482
|
+
level: "warn",
|
|
17483
|
+
message: `Session archive run failed: ${redactDiagnosticText(error2 instanceof Error ? error2.message : String(error2)).slice(0, 200)}`,
|
|
17484
|
+
metadata: { outcome: "archive-run-failed" }
|
|
17485
|
+
});
|
|
17486
|
+
return null;
|
|
17487
|
+
}).finally(() => {
|
|
17488
|
+
clearTimeout(timeout);
|
|
17489
|
+
if (state.sessionArchiveInFlight === task) {
|
|
17490
|
+
state.sessionArchiveInFlight = null;
|
|
17491
|
+
state.sessionArchiveAbortController = null;
|
|
17492
|
+
}
|
|
17493
|
+
});
|
|
17494
|
+
state.sessionArchiveInFlight = task;
|
|
17495
|
+
return task;
|
|
17496
|
+
}
|
|
17497
|
+
async function runSessionArchiveBatch(state, phase, sessions, budgetMs, signal) {
|
|
17498
|
+
const config = state.sessionArchiveConfig;
|
|
17499
|
+
const client = state.opencodeClient;
|
|
17500
|
+
const watermarkPath = state.sessionArchiveWatermarkPath;
|
|
17501
|
+
if (!client || !config.keyPrefix || !watermarkPath || !state.agentId) {
|
|
17502
|
+
logSessionArchiveActivity(state, phase, {
|
|
17503
|
+
type: "info",
|
|
17504
|
+
level: "warn",
|
|
17505
|
+
message: "Session archive skipped because runner identity or archive configuration is unavailable",
|
|
17506
|
+
metadata: { outcome: "archive-configuration-unavailable" }
|
|
17507
|
+
});
|
|
17508
|
+
return null;
|
|
17509
|
+
}
|
|
17510
|
+
const summary = await runSessionArchive({
|
|
17511
|
+
sessions,
|
|
17512
|
+
deps: {
|
|
17513
|
+
client,
|
|
17514
|
+
runnerId: state.agentId,
|
|
17515
|
+
keyPrefix: config.keyPrefix,
|
|
17516
|
+
bindings: readSessionBindings(
|
|
17517
|
+
sessionBindingLedgerPath(homedir8(), process.env),
|
|
17518
|
+
(message) => logSessionArchiveActivity(state, phase, {
|
|
17519
|
+
type: "info",
|
|
17520
|
+
level: "warn",
|
|
17521
|
+
message: redactDiagnosticText(message),
|
|
17522
|
+
metadata: { outcome: "binding-ledger-read-failed" }
|
|
17523
|
+
})
|
|
17524
|
+
),
|
|
17525
|
+
watermarks: state.sessionArchiveWatermarks,
|
|
17526
|
+
watermarkPath,
|
|
17527
|
+
signal,
|
|
17528
|
+
upload: uploadSessionArchive,
|
|
17529
|
+
exportMessages: exportSessionMessages,
|
|
17530
|
+
recordWatermark: recordUpload
|
|
17531
|
+
},
|
|
17532
|
+
budgetMs
|
|
17533
|
+
});
|
|
17534
|
+
logSessionArchiveSummary(state, phase, summary);
|
|
17535
|
+
return summary;
|
|
17536
|
+
}
|
|
17537
|
+
async function runSessionArchiveFleet(state, phase, budgetMs, signal) {
|
|
17538
|
+
const startedAt = Date.now();
|
|
17539
|
+
const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0, signal);
|
|
17540
|
+
if (enumeration.sessions === null) {
|
|
17541
|
+
const interrupted = signal.aborted;
|
|
17542
|
+
logSessionArchiveActivity(state, phase, {
|
|
17543
|
+
type: "info",
|
|
17544
|
+
level: "warn",
|
|
17545
|
+
message: interrupted ? "Session archive: enumeration was interrupted before the complete store was known; no session was marked archived" : `Session archive: could not enumerate the complete session store (${enumeration.reason}); no session was marked archived`,
|
|
17546
|
+
metadata: {
|
|
17547
|
+
outcome: interrupted ? "enumeration-interrupted" : "enumeration-refused",
|
|
17548
|
+
...!interrupted ? { reason: enumeration.reason } : {}
|
|
17549
|
+
}
|
|
17550
|
+
});
|
|
17551
|
+
return null;
|
|
17552
|
+
}
|
|
17553
|
+
return runSessionArchiveBatch(
|
|
17554
|
+
state,
|
|
17555
|
+
phase,
|
|
17556
|
+
enumeration.sessions,
|
|
17557
|
+
Math.max(0, budgetMs - (Date.now() - startedAt)),
|
|
17558
|
+
signal
|
|
17559
|
+
);
|
|
17560
|
+
}
|
|
17561
|
+
async function runSessionArchiveShutdownFlush(state, budgetMs) {
|
|
17562
|
+
const deadline = Date.now() + budgetMs;
|
|
17563
|
+
const previousRun = state.sessionArchiveInFlight;
|
|
17564
|
+
if (previousRun) {
|
|
17565
|
+
state.sessionArchiveAbortController?.abort(
|
|
17566
|
+
new Error("graceful shutdown archive flush requested")
|
|
17567
|
+
);
|
|
17568
|
+
let timeout;
|
|
17569
|
+
const settled = await Promise.race([
|
|
17570
|
+
previousRun.then(() => true),
|
|
17571
|
+
new Promise((resolve5) => {
|
|
17572
|
+
timeout = setTimeout(() => resolve5(false), budgetMs);
|
|
17573
|
+
})
|
|
17574
|
+
]);
|
|
17575
|
+
if (timeout) clearTimeout(timeout);
|
|
17576
|
+
if (!settled) {
|
|
17577
|
+
state.sessionArchiveAbortController?.abort(new Error("session archive flush budget expired"));
|
|
17578
|
+
logSessionArchiveActivity(state, "shutdown", {
|
|
17579
|
+
type: "info",
|
|
17580
|
+
level: "warn",
|
|
17581
|
+
message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
|
|
17582
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17583
|
+
});
|
|
17584
|
+
return null;
|
|
17585
|
+
}
|
|
17586
|
+
}
|
|
17587
|
+
const remainingMs = Math.floor(deadline - Date.now());
|
|
17588
|
+
if (remainingMs <= 0) {
|
|
17589
|
+
logSessionArchiveActivity(state, "shutdown", {
|
|
17590
|
+
type: "info",
|
|
17591
|
+
level: "warn",
|
|
17592
|
+
message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
|
|
17593
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17594
|
+
});
|
|
17595
|
+
return null;
|
|
17596
|
+
}
|
|
17597
|
+
return runSessionArchiveTask(
|
|
17598
|
+
state,
|
|
17599
|
+
"shutdown",
|
|
17600
|
+
remainingMs,
|
|
17601
|
+
(signal) => runSessionArchiveFleet(state, "shutdown", remainingMs, signal)
|
|
17602
|
+
);
|
|
17603
|
+
}
|
|
17604
|
+
function scheduleSessionArchive(state) {
|
|
17605
|
+
const config = state.sessionArchiveConfig;
|
|
17606
|
+
if (!config.enabled) return;
|
|
17607
|
+
state.sessionArchiveWatermarkPath = sessionArchiveWatermarkPath(homedir8(), process.env);
|
|
17608
|
+
state.sessionArchiveWatermarks = readWatermarks(
|
|
17609
|
+
state.sessionArchiveWatermarkPath,
|
|
17610
|
+
(message) => logSessionArchiveActivity(state, "startup", {
|
|
17611
|
+
type: "info",
|
|
17612
|
+
level: "warn",
|
|
17613
|
+
message: redactDiagnosticText(message),
|
|
17614
|
+
metadata: { outcome: "watermark-read-failed" }
|
|
17615
|
+
})
|
|
17616
|
+
);
|
|
17617
|
+
logSessionArchiveActivity(state, "startup", {
|
|
17618
|
+
type: "info",
|
|
17619
|
+
message: `Session archive enabled (interval=${config.intervalMs}ms)`,
|
|
17620
|
+
metadata: { outcome: "armed", interval_ms: config.intervalMs }
|
|
17621
|
+
});
|
|
17622
|
+
const cycleBudgetMs = Math.min(
|
|
17623
|
+
config.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
|
|
17624
|
+
MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
|
|
17625
|
+
);
|
|
17626
|
+
const run2 = (phase = "scheduled") => {
|
|
17627
|
+
void runSessionArchiveTask(
|
|
17628
|
+
state,
|
|
17629
|
+
phase,
|
|
17630
|
+
cycleBudgetMs,
|
|
17631
|
+
(signal) => runSessionArchiveFleet(state, phase, cycleBudgetMs, signal)
|
|
17632
|
+
);
|
|
17633
|
+
};
|
|
17634
|
+
const interval = setInterval(run2, config.intervalMs);
|
|
17635
|
+
const firstRun = setTimeout(() => run2(), SESSION_ARCHIVE_FIRST_RUN_MS);
|
|
17636
|
+
state.sessionArchiveTimers.push(interval, firstRun);
|
|
17637
|
+
}
|
|
16250
17638
|
function scheduleUsageReporting(state, params) {
|
|
16251
17639
|
const { mode, warnings } = params.resolved;
|
|
16252
17640
|
for (const warning2 of warnings) {
|
|
@@ -16521,6 +17909,12 @@ async function cleanup(state, opts = {}) {
|
|
|
16521
17909
|
clearTimeout(timer);
|
|
16522
17910
|
}
|
|
16523
17911
|
state.sessionCleanupTimers = [];
|
|
17912
|
+
for (const timer of state.sessionArchiveTimers) {
|
|
17913
|
+
clearInterval(timer);
|
|
17914
|
+
clearTimeout(timer);
|
|
17915
|
+
}
|
|
17916
|
+
state.sessionArchiveTimers = [];
|
|
17917
|
+
state.sessionArchiveAbortController?.abort(new Error("runner cleanup started"));
|
|
16524
17918
|
state.stopOpenCodeLogTail?.();
|
|
16525
17919
|
state.stopOpenCodeLogTail = null;
|
|
16526
17920
|
if (state.claudeUsageTimer) {
|
|
@@ -16588,6 +17982,15 @@ async function cleanup(state, opts = {}) {
|
|
|
16588
17982
|
await timeShutdownPhase(state, durations, "tunnel_close", () => connection.close());
|
|
16589
17983
|
state.connection = null;
|
|
16590
17984
|
}
|
|
17985
|
+
if (opts.graceful && state.sessionArchiveConfig.enabled) {
|
|
17986
|
+
const budgetMs = state.sessionArchiveConfig.flushBudgetMs;
|
|
17987
|
+
await timeShutdownPhase(
|
|
17988
|
+
state,
|
|
17989
|
+
durations,
|
|
17990
|
+
"session_archive_flush",
|
|
17991
|
+
() => runSessionArchiveShutdownFlush(state, budgetMs)
|
|
17992
|
+
);
|
|
17993
|
+
}
|
|
16591
17994
|
if (state.opencodeProcess) {
|
|
16592
17995
|
const opencodeProcess = state.opencodeProcess;
|
|
16593
17996
|
const result = await timeShutdownPhase(
|
|
@@ -16711,6 +18114,18 @@ async function run(options) {
|
|
|
16711
18114
|
messageCount: 0,
|
|
16712
18115
|
lastProxiedActivityAt: null,
|
|
16713
18116
|
sessionCleanupTimers: [],
|
|
18117
|
+
sessionArchiveConfig: resolveSessionArchiveConfig(
|
|
18118
|
+
{
|
|
18119
|
+
enabled: options.sessionArchive,
|
|
18120
|
+
interval: options.sessionArchiveInterval
|
|
18121
|
+
},
|
|
18122
|
+
process.env
|
|
18123
|
+
),
|
|
18124
|
+
sessionArchiveTimers: [],
|
|
18125
|
+
sessionArchiveWatermarks: /* @__PURE__ */ new Map(),
|
|
18126
|
+
sessionArchiveWatermarkPath: null,
|
|
18127
|
+
sessionArchiveInFlight: null,
|
|
18128
|
+
sessionArchiveAbortController: null,
|
|
16714
18129
|
sessionDeleteFailures: /* @__PURE__ */ new Map(),
|
|
16715
18130
|
claudeUsageTimer: null,
|
|
16716
18131
|
claudeUsageRearm: null,
|
|
@@ -16724,6 +18139,14 @@ async function run(options) {
|
|
|
16724
18139
|
};
|
|
16725
18140
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
16726
18141
|
setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
|
|
18142
|
+
for (const warning2 of state.sessionArchiveConfig.warnings) {
|
|
18143
|
+
logSessionArchiveActivity(state, "startup", {
|
|
18144
|
+
type: "info",
|
|
18145
|
+
level: "warn",
|
|
18146
|
+
message: `Session archive: ${warning2}`,
|
|
18147
|
+
metadata: { outcome: "configuration-warning" }
|
|
18148
|
+
});
|
|
18149
|
+
}
|
|
16727
18150
|
if (options.credentialSyncMarker) {
|
|
16728
18151
|
state.credentialSync = createCredentialSync({
|
|
16729
18152
|
markerPath: options.credentialSyncMarker,
|
|
@@ -17045,13 +18468,75 @@ async function run(options) {
|
|
|
17045
18468
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
17046
18469
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
17047
18470
|
}
|
|
17048
|
-
const { version:
|
|
17049
|
-
options,
|
|
17050
|
-
process.env
|
|
17051
|
-
);
|
|
18471
|
+
const { version: opencodeVersionSelection, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(options, process.env);
|
|
17052
18472
|
for (const warning2 of opencodeVersionWarnings) {
|
|
17053
18473
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
17054
18474
|
}
|
|
18475
|
+
let opencodeMajor;
|
|
18476
|
+
let opencodeV2Command;
|
|
18477
|
+
let opencodeDetectionSource;
|
|
18478
|
+
let opencodeDetectionLevel = "info";
|
|
18479
|
+
let opencodeDetectionMessage;
|
|
18480
|
+
if (opencodeVersionSelection === "v1") {
|
|
18481
|
+
opencodeMajor = "v1";
|
|
18482
|
+
opencodeDetectionSource = "explicit";
|
|
18483
|
+
opencodeDetectionMessage = "OpenCode V1 was selected explicitly.";
|
|
18484
|
+
} else if (opencodeVersionSelection === "v2") {
|
|
18485
|
+
const installed = resolveInstalledOpenCode();
|
|
18486
|
+
opencodeMajor = "v2";
|
|
18487
|
+
opencodeDetectionSource = "explicit";
|
|
18488
|
+
if (installed.kind === "installed" && installed.major === "v2") {
|
|
18489
|
+
opencodeV2Command = installed.command;
|
|
18490
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; using ${installed.command}${installed.version ? ` (${installed.version})` : ""}.`;
|
|
18491
|
+
} else if (installed.kind === "unknown") {
|
|
18492
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported ${JSON.stringify(installed.rawVersion)}, but no V2 command was resolved.`;
|
|
18493
|
+
} else if (installed.kind === "installed") {
|
|
18494
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported V1 ${installed.version ?? "with an unknown version"}, so no V2 command was resolved.`;
|
|
18495
|
+
} else {
|
|
18496
|
+
opencodeDetectionMessage = "OpenCode V2 was selected explicitly, but no V2 command was resolved.";
|
|
18497
|
+
}
|
|
18498
|
+
} else {
|
|
18499
|
+
const running = await classifyRunningOpenCode(state.port);
|
|
18500
|
+
if (running.kind === "v1") {
|
|
18501
|
+
opencodeMajor = "v1";
|
|
18502
|
+
opencodeDetectionSource = "running-server";
|
|
18503
|
+
opencodeDetectionMessage = `OpenCode detection: found V1 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
|
|
18504
|
+
} else if (running.kind === "v2") {
|
|
18505
|
+
opencodeMajor = "v2";
|
|
18506
|
+
opencodeDetectionSource = "running-server";
|
|
18507
|
+
opencodeDetectionMessage = `OpenCode detection: found V2 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
|
|
18508
|
+
} else if (running.kind === "authenticated-unknown") {
|
|
18509
|
+
const message = `A server on port ${state.port} requires credentials this runner does not have. This can be a password-protected V1 or a V2 server, so the protocol cannot be established. Select it explicitly with --opencode-version v1 or --opencode-version v2.`;
|
|
18510
|
+
logActivity(state, {
|
|
18511
|
+
type: "info",
|
|
18512
|
+
level: "warn",
|
|
18513
|
+
message: `OpenCode detection: authenticated-unknown on port ${state.port}; ${running.detail}. ${message}`
|
|
18514
|
+
});
|
|
18515
|
+
throw new Error(message);
|
|
18516
|
+
} else {
|
|
18517
|
+
const installed = resolveInstalledOpenCode();
|
|
18518
|
+
opencodeDetectionLevel = "warn";
|
|
18519
|
+
if (installed.kind === "installed") {
|
|
18520
|
+
opencodeMajor = installed.major;
|
|
18521
|
+
opencodeDetectionSource = "installed-binary";
|
|
18522
|
+
if (installed.major === "v2") opencodeV2Command = installed.command;
|
|
18523
|
+
opencodeDetectionMessage = `OpenCode detection: no server identified on port ${state.port} (${running.detail}); using installed ${installed.major} command ${installed.command}${installed.version ? ` (${installed.version})` : " with an unknown version"}.`;
|
|
18524
|
+
} else if (installed.kind === "unknown") {
|
|
18525
|
+
opencodeMajor = "v1";
|
|
18526
|
+
opencodeDetectionSource = "fallback-v1";
|
|
18527
|
+
opencodeDetectionMessage = `OpenCode detection: no server identified on port ${state.port} (${running.detail}); ${installed.command} reported ${JSON.stringify(installed.rawVersion)}, so assuming V1. Use --opencode-version v2 to select V2 explicitly.`;
|
|
18528
|
+
} else {
|
|
18529
|
+
opencodeMajor = "v1";
|
|
18530
|
+
opencodeDetectionSource = "fallback-v1";
|
|
18531
|
+
opencodeDetectionMessage = `OpenCode detection: no server identified on port ${state.port} (${running.detail}); no recognizable OpenCode binary is installed, so continuing into the V1 install flow.`;
|
|
18532
|
+
}
|
|
18533
|
+
}
|
|
18534
|
+
}
|
|
18535
|
+
logActivity(state, {
|
|
18536
|
+
type: "info",
|
|
18537
|
+
level: opencodeDetectionLevel,
|
|
18538
|
+
message: opencodeDetectionMessage
|
|
18539
|
+
});
|
|
17055
18540
|
const managedSetupOptions = {
|
|
17056
18541
|
agentId: state.agentId,
|
|
17057
18542
|
apiUrl: getApiUrlConfig(),
|
|
@@ -17072,7 +18557,7 @@ async function run(options) {
|
|
|
17072
18557
|
managedPreparation = managedDiscovery;
|
|
17073
18558
|
managedSetupOptions.log(`MANAGED-INFERENCE-NOT-READY: ${managedDiscovery.reason}`, "warn");
|
|
17074
18559
|
} else if (managedDiscovery.kind === "setup") {
|
|
17075
|
-
const managedHealth =
|
|
18560
|
+
const managedHealth = opencodeMajor === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
|
|
17076
18561
|
managedPreparation = await prepareManagedInferenceSetup(
|
|
17077
18562
|
managedSetupOptions,
|
|
17078
18563
|
managedDiscovery,
|
|
@@ -17097,13 +18582,14 @@ async function run(options) {
|
|
|
17097
18582
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
17098
18583
|
const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
|
|
17099
18584
|
try {
|
|
17100
|
-
const oc =
|
|
18585
|
+
const oc = opencodeMajor === "v2" ? await ensureOpenCode2Running({
|
|
17101
18586
|
port: state.port,
|
|
17102
18587
|
interactive: state.interactive,
|
|
17103
18588
|
agentId: state.agentId,
|
|
17104
18589
|
log: (message) => log2(state, message),
|
|
17105
18590
|
startTimeoutMs: opencodeStartTimeoutMs,
|
|
17106
|
-
inheritStdio: Boolean(options.opencodePidFile)
|
|
18591
|
+
inheritStdio: Boolean(options.opencodePidFile),
|
|
18592
|
+
command: opencodeV2Command
|
|
17107
18593
|
}) : await ensureOpenCodeRunning({
|
|
17108
18594
|
port: state.port,
|
|
17109
18595
|
interactive: state.interactive,
|
|
@@ -17115,7 +18601,7 @@ async function run(options) {
|
|
|
17115
18601
|
state.port = oc.port;
|
|
17116
18602
|
state.opencodeProcess = options.opencodePidFile ? null : oc.process;
|
|
17117
18603
|
state.opencodeVersion = oc.version;
|
|
17118
|
-
state.opencodeApiVersion =
|
|
18604
|
+
state.opencodeApiVersion = opencodeMajor;
|
|
17119
18605
|
let configuredPlugins = [];
|
|
17120
18606
|
let resolvedPlugins = [];
|
|
17121
18607
|
try {
|
|
@@ -17137,13 +18623,14 @@ async function run(options) {
|
|
|
17137
18623
|
metadata: {
|
|
17138
18624
|
opencode_version: state.opencodeVersion,
|
|
17139
18625
|
opencode_api_version: state.opencodeApiVersion,
|
|
18626
|
+
opencode_detection_source: opencodeDetectionSource,
|
|
17140
18627
|
configured_plugins: JSON.stringify(configuredPlugins),
|
|
17141
18628
|
resolved_plugins: JSON.stringify(resolvedPlugins)
|
|
17142
18629
|
},
|
|
17143
18630
|
source: "cli.runtime-provenance"
|
|
17144
18631
|
});
|
|
17145
18632
|
let opencodePassword = null;
|
|
17146
|
-
if (
|
|
18633
|
+
if (opencodeMajor === "v2" && "password" in oc) {
|
|
17147
18634
|
const value = oc.password;
|
|
17148
18635
|
if (typeof value === "string" || value === null) opencodePassword = value;
|
|
17149
18636
|
}
|
|
@@ -17156,7 +18643,7 @@ async function run(options) {
|
|
|
17156
18643
|
state.opencodeClient = openCodeClient;
|
|
17157
18644
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
17158
18645
|
try {
|
|
17159
|
-
|
|
18646
|
+
writeFileSync9(options.opencodePidFile, `${oc.process.pid}
|
|
17160
18647
|
`, { mode: 384 });
|
|
17161
18648
|
chmodSync3(options.opencodePidFile, 384);
|
|
17162
18649
|
} catch (error2) {
|
|
@@ -17266,7 +18753,7 @@ async function run(options) {
|
|
|
17266
18753
|
let existingPid;
|
|
17267
18754
|
if (existsSync3(options.litestreamPidFile)) {
|
|
17268
18755
|
try {
|
|
17269
|
-
const rawPid =
|
|
18756
|
+
const rawPid = readFileSync12(options.litestreamPidFile, "utf8").trim();
|
|
17270
18757
|
const parsedPid = Number(rawPid);
|
|
17271
18758
|
if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
|
|
17272
18759
|
existingPid = parsedPid;
|
|
@@ -17303,7 +18790,7 @@ async function run(options) {
|
|
|
17303
18790
|
});
|
|
17304
18791
|
try {
|
|
17305
18792
|
if (litestreamProcess.pid !== void 0) {
|
|
17306
|
-
|
|
18793
|
+
writeFileSync9(options.litestreamPidFile, `${litestreamProcess.pid}
|
|
17307
18794
|
`, {
|
|
17308
18795
|
mode: 384
|
|
17309
18796
|
});
|
|
@@ -17353,6 +18840,15 @@ async function run(options) {
|
|
|
17353
18840
|
log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
|
|
17354
18841
|
}
|
|
17355
18842
|
const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
|
|
18843
|
+
const recordBinding = state.sessionArchiveConfig.enabled ? (conversationId, sessionId) => appendSessionBinding(
|
|
18844
|
+
sessionBindingLedgerPath(homedir8(), process.env),
|
|
18845
|
+
{
|
|
18846
|
+
conversation_id: conversationId,
|
|
18847
|
+
opencode_session_id: sessionId,
|
|
18848
|
+
observed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
18849
|
+
},
|
|
18850
|
+
(message) => logActivity(state, { type: "info", level: "warn", message })
|
|
18851
|
+
) : void 0;
|
|
17356
18852
|
const channelDriver = new ChannelDriver({
|
|
17357
18853
|
agentId: state.agentId,
|
|
17358
18854
|
port: state.port,
|
|
@@ -17362,6 +18858,7 @@ async function run(options) {
|
|
|
17362
18858
|
conversationFilter: state.conversationFilter,
|
|
17363
18859
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
17364
18860
|
sessionStoreHealth: state.sessionStoreHealth,
|
|
18861
|
+
recordBinding,
|
|
17365
18862
|
// #559: the file-sync allow-lists. Empty ⇒ pending files are REJECTED with
|
|
17366
18863
|
// `file_sync_disabled` on the ack, not silently ignored.
|
|
17367
18864
|
fileSyncDirectories,
|
|
@@ -17541,6 +19038,7 @@ async function run(options) {
|
|
|
17541
19038
|
}
|
|
17542
19039
|
if (!state.running) return;
|
|
17543
19040
|
scheduleSessionCleanup(state, channelDriver, options);
|
|
19041
|
+
scheduleSessionArchive(state);
|
|
17544
19042
|
state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
|
|
17545
19043
|
state.openaiUsageRearm = scheduleUsageReporting(state, {
|
|
17546
19044
|
label: "OpenAI",
|
|
@@ -17636,8 +19134,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17636
19134
|
"--opencode-start-timeout <seconds>",
|
|
17637
19135
|
"Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
|
|
17638
19136
|
).option(
|
|
17639
|
-
"--opencode-version <v1|v2>",
|
|
17640
|
-
"
|
|
19137
|
+
"--opencode-version <v1|v2|auto>",
|
|
19138
|
+
"OpenCode major version: auto (default) detects the running server, then installed binary; v1 and v2 skip server-protocol detection. Env: EVIDENT_OPENCODE_VERSION"
|
|
17641
19139
|
).option("--json", "Output in JSON format").option(
|
|
17642
19140
|
"--session-cleanup-max-age <duration>",
|
|
17643
19141
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
@@ -17650,6 +19148,12 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17650
19148
|
).option(
|
|
17651
19149
|
"--session-cleanup-interval <duration>",
|
|
17652
19150
|
"How often the cleanup sweep runs (default: 1h). Env: EVIDENT_SESSION_CLEANUP_INTERVAL"
|
|
19151
|
+
).option(
|
|
19152
|
+
"--session-archive",
|
|
19153
|
+
"Upload full OpenCode transcripts to the runner archive (off by default). Env: EVIDENT_SESSION_ARCHIVE"
|
|
19154
|
+
).option(
|
|
19155
|
+
"--session-archive-interval <duration>",
|
|
19156
|
+
"How often session archives are uploaded (default: 1h). Env: EVIDENT_SESSION_ARCHIVE_INTERVAL"
|
|
17653
19157
|
).option(
|
|
17654
19158
|
"--claude-usage-reporting <mode>",
|
|
17655
19159
|
"Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
@@ -17721,6 +19225,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17721
19225
|
sessionCleanupMaxCount: options.sessionCleanupMaxCount,
|
|
17722
19226
|
maxActiveSessions: options.maxActiveSessions,
|
|
17723
19227
|
sessionCleanupInterval: options.sessionCleanupInterval,
|
|
19228
|
+
sessionArchive: options.sessionArchive,
|
|
19229
|
+
sessionArchiveInterval: options.sessionArchiveInterval,
|
|
17724
19230
|
// Raw string — the resolver in run.ts single-sources parsing
|
|
17725
19231
|
// (resolveClaudeUsageReportingMode).
|
|
17726
19232
|
claudeUsageReporting: options.claudeUsageReporting,
|