@evident-ai/cli 3.6.1-dev.98128d1 → 3.6.1-dev.981b738
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -7
- package/dist/index.js +2264 -544
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -723,6 +723,9 @@ async function reportResourceUsage(agentId, authHeader, usage) {
|
|
|
723
723
|
opencode_db_bytes: usage.opencodeDbBytes
|
|
724
724
|
});
|
|
725
725
|
}
|
|
726
|
+
async function reportSessionDeletions(agentId, authHeader, deletions) {
|
|
727
|
+
return postBestEffort(`/runners/${agentId}/session-deletions`, authHeader, { deletions });
|
|
728
|
+
}
|
|
726
729
|
async function getRunnerCredentialVault(agentId, authHeader) {
|
|
727
730
|
const empty = { prefix: null, vaultId: null, source: null };
|
|
728
731
|
try {
|
|
@@ -1176,9 +1179,9 @@ async function claudeUsage() {
|
|
|
1176
1179
|
}
|
|
1177
1180
|
|
|
1178
1181
|
// src/commands/run.ts
|
|
1179
|
-
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as
|
|
1182
|
+
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "node:fs";
|
|
1180
1183
|
import { homedir as homedir8 } from "node:os";
|
|
1181
|
-
import { isAbsolute as isAbsolute4, join as
|
|
1184
|
+
import { isAbsolute as isAbsolute4, join as join16, parse, resolve as resolvePath2 } from "node:path";
|
|
1182
1185
|
import chalk7 from "chalk";
|
|
1183
1186
|
|
|
1184
1187
|
// ../../packages/types/src/agents/index.ts
|
|
@@ -1442,7 +1445,7 @@ import ora4 from "ora";
|
|
|
1442
1445
|
import { select as select4 } from "@inquirer/prompts";
|
|
1443
1446
|
|
|
1444
1447
|
// src/lib/telemetry.ts
|
|
1445
|
-
var CLI_VERSION = (true ? "3.6.1-dev.
|
|
1448
|
+
var CLI_VERSION = (true ? "3.6.1-dev.981b738" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1446
1449
|
function getCliVersion() {
|
|
1447
1450
|
return CLI_VERSION;
|
|
1448
1451
|
}
|
|
@@ -2192,6 +2195,7 @@ var FORWARDED_LEVELS_BY_SOURCE = {
|
|
|
2192
2195
|
"cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
|
|
2193
2196
|
opencode: /* @__PURE__ */ new Set(["warn", "error"]),
|
|
2194
2197
|
"cli.session-cleanup": /* @__PURE__ */ new Set(["info", "warn", "error"]),
|
|
2198
|
+
"cli.session-archive": /* @__PURE__ */ new Set(["info", "warn", "error"]),
|
|
2195
2199
|
"cli.runtime-provenance": /* @__PURE__ */ new Set(["info", "warn", "error"])
|
|
2196
2200
|
};
|
|
2197
2201
|
var SEVERITY_BY_LEVEL = {
|
|
@@ -2594,48 +2598,1101 @@ function buildSessionDbRecoveryActivity(record) {
|
|
|
2594
2598
|
})();
|
|
2595
2599
|
return { level, metadata: withoutContractFields(record), message };
|
|
2596
2600
|
}
|
|
2597
|
-
case "schema_provenance_mismatch":
|
|
2598
|
-
return {
|
|
2599
|
-
level,
|
|
2600
|
-
metadata: withoutContractFields(record),
|
|
2601
|
-
message: `Session database schema provenance mismatch for ${record.dbPath ?? record.db_path ?? "unknown"}: recorded version=${record.recorded_version ?? "unknown"}, current version=${record.current_version ?? "unknown"}, reason=${record.provenance_reason ?? "unknown"}, migration delta=${record.provenance_migration_delta ?? "unknown"}. Inspect the session database and runner backup before continuing.`
|
|
2601
|
+
case "schema_provenance_mismatch":
|
|
2602
|
+
return {
|
|
2603
|
+
level,
|
|
2604
|
+
metadata: withoutContractFields(record),
|
|
2605
|
+
message: `Session database schema provenance mismatch for ${record.dbPath ?? record.db_path ?? "unknown"}: recorded version=${record.recorded_version ?? "unknown"}, current version=${record.current_version ?? "unknown"}, reason=${record.provenance_reason ?? "unknown"}, migration delta=${record.provenance_migration_delta ?? "unknown"}. Inspect the session database and runner backup before continuing.`
|
|
2606
|
+
};
|
|
2607
|
+
default:
|
|
2608
|
+
return null;
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
function withoutContractFields(record) {
|
|
2612
|
+
const { v: _v, event: _event, ...metadata } = record;
|
|
2613
|
+
return metadata;
|
|
2614
|
+
}
|
|
2615
|
+
var OUTCOMES = /* @__PURE__ */ new Set([
|
|
2616
|
+
"replica_recovered",
|
|
2617
|
+
"restore_retried",
|
|
2618
|
+
"fresh_session_db",
|
|
2619
|
+
"history_rolled_back",
|
|
2620
|
+
"restore_misconfigured",
|
|
2621
|
+
"session_db_boot_refused",
|
|
2622
|
+
"schema_provenance_mismatch"
|
|
2623
|
+
]);
|
|
2624
|
+
var STAGES = /* @__PURE__ */ new Set(["restore", "verify"]);
|
|
2625
|
+
var SEVERITIES = /* @__PURE__ */ new Set(["warning", "error"]);
|
|
2626
|
+
var NUMBER_FIELDS = [
|
|
2627
|
+
"litestream_exit_code",
|
|
2628
|
+
"attempt",
|
|
2629
|
+
"replica_objects",
|
|
2630
|
+
"replica_bytes",
|
|
2631
|
+
"quarantined_objects",
|
|
2632
|
+
"quarantine_failed_objects",
|
|
2633
|
+
"quarantined_bytes",
|
|
2634
|
+
"restore_points_tried"
|
|
2635
|
+
];
|
|
2636
|
+
var STRING_OR_NULL_FIELDS = ["quarantine_destination", "verified_restore_point"];
|
|
2637
|
+
function isSessionDbRecoveryRecord(value) {
|
|
2638
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2639
|
+
const record = value;
|
|
2640
|
+
return record.v === 1 && record.event === "session_db_recovery" && typeof record.at === "string" && STAGES.has(record.stage) && OUTCOMES.has(record.outcome) && SEVERITIES.has(record.severity) && typeof record.reason === "string" && (record.replication_suspended === void 0 || typeof record.replication_suspended === "boolean") && (record.provenance_reason === void 0 || record.provenance_reason === null || typeof record.provenance_reason === "string") && (record.provenance_migration_delta === void 0 || record.provenance_migration_delta === null || Number.isInteger(record.provenance_migration_delta)) && NUMBER_FIELDS.every((field) => record[field] === null || Number.isInteger(record[field])) && STRING_OR_NULL_FIELDS.every(
|
|
2641
|
+
(field) => record[field] === null || typeof record[field] === "string"
|
|
2642
|
+
);
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
// src/lib/opencode/session-deletion-report.ts
|
|
2646
|
+
import { appendFileSync, mkdirSync, readFileSync as readFileSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
|
|
2647
|
+
import { dirname as dirname4, join as join4 } from "node:path";
|
|
2648
|
+
var SESSION_DELETION_SPOOL_MAX_RECORDS = 1e3;
|
|
2649
|
+
var MAX_DIAGNOSTIC_LENGTH = 200;
|
|
2650
|
+
function sessionDeletionReportPath(homeDir, env) {
|
|
2651
|
+
const override = env.EVIDENT_SESSION_DELETION_REPORT?.trim();
|
|
2652
|
+
return override || join4(homeDir, ".local", "state", "evident", "session-deletions.jsonl");
|
|
2653
|
+
}
|
|
2654
|
+
function appendSessionDeletionRecords(path, records) {
|
|
2655
|
+
if (records.length === 0) return 0;
|
|
2656
|
+
mkdirSync(dirname4(path), { recursive: true });
|
|
2657
|
+
const current = readSessionDeletionRecords(path);
|
|
2658
|
+
if (current.readError !== null) {
|
|
2659
|
+
throw new Error(`cannot enforce the spool limit after reading ${path}: ${current.readError}`);
|
|
2660
|
+
}
|
|
2661
|
+
const combined = [...current.records, ...records];
|
|
2662
|
+
const kept = combined.slice(-SESSION_DELETION_SPOOL_MAX_RECORDS);
|
|
2663
|
+
const dropped = combined.length - kept.length;
|
|
2664
|
+
if (dropped > 0) {
|
|
2665
|
+
writeFileSync(path, `${kept.map((record) => JSON.stringify(record)).join("\n")}
|
|
2666
|
+
`, "utf8");
|
|
2667
|
+
} else {
|
|
2668
|
+
appendFileSync(path, `${records.map((record) => JSON.stringify(record)).join("\n")}
|
|
2669
|
+
`, "utf8");
|
|
2670
|
+
}
|
|
2671
|
+
return dropped;
|
|
2672
|
+
}
|
|
2673
|
+
function readSessionDeletionRecords(path) {
|
|
2674
|
+
let content;
|
|
2675
|
+
try {
|
|
2676
|
+
content = readFileSync3(path, "utf8");
|
|
2677
|
+
} catch (error2) {
|
|
2678
|
+
if (isMissingFile(error2)) return { records: [], skippedLines: 0, readError: null };
|
|
2679
|
+
const readError = boundedError(error2);
|
|
2680
|
+
console.error(`[session-deletion-report] could not read ${path}: ${readError}`);
|
|
2681
|
+
return { records: [], skippedLines: 0, readError };
|
|
2682
|
+
}
|
|
2683
|
+
let skippedLines = 0;
|
|
2684
|
+
const records = content.split("\n").flatMap((line) => {
|
|
2685
|
+
if (!line.trim()) return [];
|
|
2686
|
+
const record = parseRecord(line, path);
|
|
2687
|
+
if (record) return [record];
|
|
2688
|
+
skippedLines++;
|
|
2689
|
+
return [];
|
|
2690
|
+
});
|
|
2691
|
+
return { records, skippedLines, readError: null };
|
|
2692
|
+
}
|
|
2693
|
+
function removeDeliveredSessionDeletionRecords(path, delivered) {
|
|
2694
|
+
let content;
|
|
2695
|
+
try {
|
|
2696
|
+
content = readFileSync3(path, "utf8");
|
|
2697
|
+
} catch (error2) {
|
|
2698
|
+
if (isMissingFile(error2)) return;
|
|
2699
|
+
console.error(
|
|
2700
|
+
`[session-deletion-report] could not read ${path} before removing delivered records: ${boundedError(error2)}`
|
|
2701
|
+
);
|
|
2702
|
+
throw error2;
|
|
2703
|
+
}
|
|
2704
|
+
const toRemove = /* @__PURE__ */ new Map();
|
|
2705
|
+
for (const record of delivered) {
|
|
2706
|
+
toRemove.set(record.opencode_session_id, (toRemove.get(record.opencode_session_id) ?? 0) + 1);
|
|
2707
|
+
}
|
|
2708
|
+
const remaining = [];
|
|
2709
|
+
for (const line of content.split("\n")) {
|
|
2710
|
+
if (!line.trim()) continue;
|
|
2711
|
+
const record = parseRecord(line, path);
|
|
2712
|
+
const count = record ? toRemove.get(record.opencode_session_id) ?? 0 : 0;
|
|
2713
|
+
if (record && count > 0) {
|
|
2714
|
+
if (count === 1) toRemove.delete(record.opencode_session_id);
|
|
2715
|
+
else toRemove.set(record.opencode_session_id, count - 1);
|
|
2716
|
+
} else {
|
|
2717
|
+
remaining.push(line);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
try {
|
|
2721
|
+
if (remaining.length === 0) unlinkSync2(path);
|
|
2722
|
+
else writeFileSync(path, `${remaining.join("\n")}
|
|
2723
|
+
`, "utf8");
|
|
2724
|
+
} catch (error2) {
|
|
2725
|
+
if (isMissingFile(error2)) return;
|
|
2726
|
+
console.error(
|
|
2727
|
+
`[session-deletion-report] could not write ${path} after removing delivered records: ${boundedError(error2)}`
|
|
2728
|
+
);
|
|
2729
|
+
throw error2;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
function parseRecord(line, path) {
|
|
2733
|
+
try {
|
|
2734
|
+
const value = JSON.parse(line);
|
|
2735
|
+
if (isSessionDeletionRecord(value)) return value;
|
|
2736
|
+
console.error(`[session-deletion-report] skipped malformed record in ${path}: invalid shape`);
|
|
2737
|
+
return null;
|
|
2738
|
+
} catch (error2) {
|
|
2739
|
+
console.error(
|
|
2740
|
+
`[session-deletion-report] skipped malformed record in ${path}: ${boundedError(error2)}`
|
|
2741
|
+
);
|
|
2742
|
+
return null;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
function isSessionDeletionRecord(value) {
|
|
2746
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2747
|
+
const record = value;
|
|
2748
|
+
const policy = record.policy;
|
|
2749
|
+
if (!policy || typeof policy !== "object" || Array.isArray(policy)) return false;
|
|
2750
|
+
const fields = policy;
|
|
2751
|
+
return typeof record.opencode_session_id === "string" && record.opencode_session_id.length > 0 && typeof record.deleted_at === "string" && record.reason === "auto_cleanup" && (fields.max_age_ms === null || typeof fields.max_age_ms === "number") && (fields.max_count === null || typeof fields.max_count === "number") && (fields.last_activity_at === null || typeof fields.last_activity_at === "string");
|
|
2752
|
+
}
|
|
2753
|
+
function isMissingFile(error2) {
|
|
2754
|
+
return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
|
|
2755
|
+
}
|
|
2756
|
+
function boundedError(error2) {
|
|
2757
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2758
|
+
return message.slice(0, MAX_DIAGNOSTIC_LENGTH);
|
|
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();
|
|
2602
3430
|
};
|
|
2603
|
-
|
|
2604
|
-
|
|
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;
|
|
2605
3464
|
}
|
|
2606
3465
|
}
|
|
2607
|
-
function
|
|
2608
|
-
const
|
|
2609
|
-
|
|
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
|
+
);
|
|
2610
3502
|
}
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
"
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
const record = value;
|
|
2636
|
-
return record.v === 1 && record.event === "session_db_recovery" && typeof record.at === "string" && STAGES.has(record.stage) && OUTCOMES.has(record.outcome) && SEVERITIES.has(record.severity) && typeof record.reason === "string" && (record.replication_suspended === void 0 || typeof record.replication_suspended === "boolean") && (record.provenance_reason === void 0 || record.provenance_reason === null || typeof record.provenance_reason === "string") && (record.provenance_migration_delta === void 0 || record.provenance_migration_delta === null || Number.isInteger(record.provenance_migration_delta)) && NUMBER_FIELDS.every((field) => record[field] === null || Number.isInteger(record[field])) && STRING_OR_NULL_FIELDS.every(
|
|
2637
|
-
(field) => record[field] === null || typeof record[field] === "string"
|
|
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
|
|
2638
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;
|
|
2639
3696
|
}
|
|
2640
3697
|
|
|
2641
3698
|
// src/lib/opencode/auth.ts
|
|
@@ -2656,8 +3713,26 @@ async function checkOpenCodeHealth(port) {
|
|
|
2656
3713
|
if (!response.ok) {
|
|
2657
3714
|
return { healthy: false, error: `HTTP ${response.status}` };
|
|
2658
3715
|
}
|
|
2659
|
-
const
|
|
2660
|
-
|
|
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 };
|
|
2661
3736
|
} catch (error2) {
|
|
2662
3737
|
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
2663
3738
|
return { healthy: false, error: message };
|
|
@@ -2673,10 +3748,17 @@ async function checkOpenCode2Health(port, password) {
|
|
|
2673
3748
|
if (password !== void 0) {
|
|
2674
3749
|
headers.Authorization = buildOpenCodeBasicAuthHeader(password);
|
|
2675
3750
|
}
|
|
2676
|
-
|
|
3751
|
+
let response = await fetch(`http://127.0.0.1:${port}/api/info`, {
|
|
2677
3752
|
headers,
|
|
2678
3753
|
signal: AbortSignal.timeout(2e3)
|
|
2679
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
|
+
}
|
|
2680
3762
|
if (response.status === 401) {
|
|
2681
3763
|
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
2682
3764
|
}
|
|
@@ -2691,23 +3773,29 @@ async function checkOpenCode2Health(port, password) {
|
|
|
2691
3773
|
} catch (error2) {
|
|
2692
3774
|
return {
|
|
2693
3775
|
healthy: false,
|
|
2694
|
-
error: `
|
|
3776
|
+
error: `HTTP ${response.status}: invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2695
3777
|
};
|
|
2696
3778
|
}
|
|
2697
3779
|
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
2698
3780
|
return {
|
|
2699
3781
|
healthy: false,
|
|
2700
|
-
error: `
|
|
3782
|
+
error: `HTTP ${response.status}: invalid health response${excerpt}: expected healthy=true`
|
|
2701
3783
|
};
|
|
2702
3784
|
}
|
|
2703
|
-
const
|
|
2704
|
-
if (
|
|
3785
|
+
const bodyObject = data;
|
|
3786
|
+
if (usesHealthFallback && bodyObject.healthy !== true) {
|
|
2705
3787
|
return {
|
|
2706
3788
|
healthy: false,
|
|
2707
3789
|
error: `Invalid health response${excerpt}: expected healthy=true`
|
|
2708
3790
|
};
|
|
2709
3791
|
}
|
|
2710
|
-
|
|
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 };
|
|
2711
3799
|
} catch (error2) {
|
|
2712
3800
|
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
2713
3801
|
return { healthy: false, error: message };
|
|
@@ -2765,10 +3853,18 @@ function describeRequestOperation(input, init) {
|
|
|
2765
3853
|
function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
2766
3854
|
return (async (input, init) => {
|
|
2767
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 });
|
|
2768
3864
|
try {
|
|
2769
|
-
return await fetchImpl(input, { ...init, signal:
|
|
3865
|
+
return await fetchImpl(input, { ...init, signal: controller.signal });
|
|
2770
3866
|
} catch (error2) {
|
|
2771
|
-
if (
|
|
3867
|
+
if (timeoutSignal.aborted) {
|
|
2772
3868
|
throw new RequestTimeoutError(
|
|
2773
3869
|
describeRequestOperation(input, init),
|
|
2774
3870
|
timeoutMs,
|
|
@@ -2776,6 +3872,9 @@ function withRequestTimeout(fetchImpl, timeoutMs) {
|
|
|
2776
3872
|
);
|
|
2777
3873
|
}
|
|
2778
3874
|
throw error2;
|
|
3875
|
+
} finally {
|
|
3876
|
+
init?.signal?.removeEventListener("abort", abortFromCaller);
|
|
3877
|
+
timeoutSignal.removeEventListener("abort", abortFromTimeout);
|
|
2779
3878
|
}
|
|
2780
3879
|
});
|
|
2781
3880
|
}
|
|
@@ -2959,28 +4058,38 @@ function sessionListPath(options) {
|
|
|
2959
4058
|
async function listSessionPage(port, client, options) {
|
|
2960
4059
|
const path = sessionListPath(options);
|
|
2961
4060
|
try {
|
|
2962
|
-
const res = await requestWithClient(
|
|
2963
|
-
|
|
4061
|
+
const res = await requestWithClient(
|
|
4062
|
+
port,
|
|
4063
|
+
client,
|
|
4064
|
+
path,
|
|
4065
|
+
options?.signal ? { signal: options.signal } : void 0,
|
|
4066
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
4067
|
+
);
|
|
4068
|
+
if (!res.ok) {
|
|
4069
|
+
console.error(`[listSessionPage] GET ${path} returned HTTP ${res.status} (port ${port})`);
|
|
4070
|
+
return null;
|
|
4071
|
+
}
|
|
2964
4072
|
const body = await res.json();
|
|
2965
4073
|
return Array.isArray(body) ? body : null;
|
|
2966
4074
|
} catch (error2) {
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
);
|
|
4075
|
+
if (options?.signal?.aborted) return null;
|
|
4076
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
4077
|
+
console.error(`[listSessionPage] GET ${path} failed (port ${port}): ${errorName}`);
|
|
2970
4078
|
return null;
|
|
2971
4079
|
}
|
|
2972
4080
|
}
|
|
2973
4081
|
async function listSessions(port, client, options) {
|
|
2974
|
-
if (client?.version === "v2") return listV2Sessions(client);
|
|
4082
|
+
if (client?.version === "v2") return listV2Sessions(client, options?.signal);
|
|
2975
4083
|
return listSessionPage(port, client, options);
|
|
2976
4084
|
}
|
|
2977
|
-
async function listAllSessions(port, client) {
|
|
4085
|
+
async function listAllSessions(port, client, signal) {
|
|
2978
4086
|
if (client?.version === "v2") {
|
|
2979
|
-
const sessions2 = await listV2Sessions(client);
|
|
4087
|
+
const sessions2 = await listV2Sessions(client, signal);
|
|
2980
4088
|
return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
|
|
2981
4089
|
}
|
|
2982
4090
|
const sessions = await listSessionPage(port, client, {
|
|
2983
|
-
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
|
|
4091
|
+
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS,
|
|
4092
|
+
signal
|
|
2984
4093
|
});
|
|
2985
4094
|
if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
|
|
2986
4095
|
if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
|
|
@@ -3669,9 +4778,17 @@ function isPreamblePinnedRunning(messages, userMessageId) {
|
|
|
3669
4778
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
3670
4779
|
return completedOf(reply) != null && finishOf(reply) === "tool-calls";
|
|
3671
4780
|
}
|
|
4781
|
+
function isMidFlightPinnedRunning(messages, userMessageId) {
|
|
4782
|
+
if (messageRunState(messages, userMessageId) !== "running") return false;
|
|
4783
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
4784
|
+
return reply !== null && isAssistantInFlight(reply) && completedOf(reply) == null;
|
|
4785
|
+
}
|
|
3672
4786
|
function isB2AbandonmentConfirmed(params) {
|
|
3673
4787
|
return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false && params.rootOngoing === false;
|
|
3674
4788
|
}
|
|
4789
|
+
function isMidFlightOrphanConfirmed(params) {
|
|
4790
|
+
return params.pinnedForMs >= params.minPinnedMs && params.notOngoingStreak >= params.minNotOngoingStreak;
|
|
4791
|
+
}
|
|
3675
4792
|
function isAmbiguousTerminalFinish(m) {
|
|
3676
4793
|
if (completedOf(m) == null) return false;
|
|
3677
4794
|
if (errorOf(m) != null) return false;
|
|
@@ -3773,8 +4890,8 @@ function classifyReplyAuthError(reply) {
|
|
|
3773
4890
|
};
|
|
3774
4891
|
}
|
|
3775
4892
|
}
|
|
3776
|
-
const
|
|
3777
|
-
if (
|
|
4893
|
+
const errorMessage4 = typeof data?.message === "string" ? data.message : typeof e.message === "string" ? e.message : null;
|
|
4894
|
+
if (errorMessage4 !== null && CLAUDE_AUTH_EXPIRY_MESSAGES.some((knownMessage) => errorMessage4.includes(knownMessage))) {
|
|
3778
4895
|
return {
|
|
3779
4896
|
kind: "model_auth",
|
|
3780
4897
|
providerId: typeof data?.providerID === "string" && data.providerID || replyProviderId,
|
|
@@ -4045,14 +5162,15 @@ async function probeOpenCodeRequestReadiness(port, client) {
|
|
|
4045
5162
|
}
|
|
4046
5163
|
|
|
4047
5164
|
// src/lib/opencode/session-v2.ts
|
|
4048
|
-
|
|
5165
|
+
var ALL_HTTP_STATUSES2 = Array.from({ length: 500 }, (_, index) => index + 100);
|
|
5166
|
+
function isRecord3(value) {
|
|
4049
5167
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4050
5168
|
}
|
|
4051
5169
|
function finiteNumber2(value) {
|
|
4052
5170
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
4053
5171
|
}
|
|
4054
5172
|
function adaptTime(value) {
|
|
4055
|
-
if (!
|
|
5173
|
+
if (!isRecord3(value)) return void 0;
|
|
4056
5174
|
const created = finiteNumber2(value.created);
|
|
4057
5175
|
const completed = finiteNumber2(value.completed);
|
|
4058
5176
|
if (created === void 0 && completed === void 0) return void 0;
|
|
@@ -4062,11 +5180,11 @@ function adaptTime(value) {
|
|
|
4062
5180
|
};
|
|
4063
5181
|
}
|
|
4064
5182
|
function adaptTokens(value) {
|
|
4065
|
-
if (!
|
|
5183
|
+
if (!isRecord3(value)) return void 0;
|
|
4066
5184
|
const input = finiteNumber2(value.input);
|
|
4067
5185
|
const output = finiteNumber2(value.output);
|
|
4068
5186
|
const reasoning = finiteNumber2(value.reasoning);
|
|
4069
|
-
const cache =
|
|
5187
|
+
const cache = isRecord3(value.cache) ? {
|
|
4070
5188
|
...finiteNumber2(value.cache.read) !== void 0 ? { read: finiteNumber2(value.cache.read) } : {},
|
|
4071
5189
|
...finiteNumber2(value.cache.write) !== void 0 ? { write: finiteNumber2(value.cache.write) } : {}
|
|
4072
5190
|
} : void 0;
|
|
@@ -4090,7 +5208,7 @@ function adaptMessageInfo(value, role) {
|
|
|
4090
5208
|
if (typeof value.finish === "string") info.finish = value.finish;
|
|
4091
5209
|
if ("error" in value) info.error = value.error;
|
|
4092
5210
|
if (typeof value.agent === "string") info.agent = value.agent;
|
|
4093
|
-
if (
|
|
5211
|
+
if (isRecord3(value.model)) {
|
|
4094
5212
|
if (typeof value.model.id === "string") info.modelID = value.model.id;
|
|
4095
5213
|
if (typeof value.model.providerID === "string") info.providerID = value.model.providerID;
|
|
4096
5214
|
}
|
|
@@ -4100,7 +5218,7 @@ function adaptMessageInfo(value, role) {
|
|
|
4100
5218
|
return info;
|
|
4101
5219
|
}
|
|
4102
5220
|
function adaptV2Message(value) {
|
|
4103
|
-
if (!
|
|
5221
|
+
if (!isRecord3(value) || typeof value.id !== "string" || typeof value.type !== "string") {
|
|
4104
5222
|
return null;
|
|
4105
5223
|
}
|
|
4106
5224
|
if (value.type === "user") {
|
|
@@ -4113,7 +5231,7 @@ function adaptV2Message(value) {
|
|
|
4113
5231
|
if (value.type !== "assistant" || !Array.isArray(value.content)) return null;
|
|
4114
5232
|
const parts = [];
|
|
4115
5233
|
for (const content of value.content) {
|
|
4116
|
-
if (!
|
|
5234
|
+
if (!isRecord3(content) || typeof content.type !== "string") return null;
|
|
4117
5235
|
if (content.type === "text") {
|
|
4118
5236
|
if (typeof content.text !== "string") return null;
|
|
4119
5237
|
parts.push({ type: "text", text: content.text });
|
|
@@ -4127,23 +5245,23 @@ function adaptV2Message(value) {
|
|
|
4127
5245
|
};
|
|
4128
5246
|
}
|
|
4129
5247
|
function adaptFormTool(value) {
|
|
4130
|
-
if (!
|
|
5248
|
+
if (!isRecord3(value) || typeof value.messageID !== "string" || typeof value.id !== "string") {
|
|
4131
5249
|
return void 0;
|
|
4132
5250
|
}
|
|
4133
5251
|
return { messageID: value.messageID, callID: value.id };
|
|
4134
5252
|
}
|
|
4135
5253
|
function adaptV2FormWire(value) {
|
|
4136
|
-
if (!
|
|
5254
|
+
if (!isRecord3(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
4137
5255
|
return null;
|
|
4138
5256
|
}
|
|
4139
5257
|
return value;
|
|
4140
5258
|
}
|
|
4141
5259
|
function adaptV2FormField(value, header) {
|
|
4142
|
-
if (!
|
|
5260
|
+
if (!isRecord3(value)) return null;
|
|
4143
5261
|
const question = typeof value.title === "string" ? value.title : typeof value.question === "string" ? value.question : typeof value.key === "string" ? value.key : null;
|
|
4144
5262
|
if (!question) return null;
|
|
4145
5263
|
const options = Array.isArray(value.options) ? value.options.flatMap((option) => {
|
|
4146
|
-
if (!
|
|
5264
|
+
if (!isRecord3(option)) return [];
|
|
4147
5265
|
const label = typeof option.label === "string" ? option.label : typeof option.value === "string" ? option.value : null;
|
|
4148
5266
|
if (!label) return [];
|
|
4149
5267
|
return [
|
|
@@ -4161,7 +5279,7 @@ function adaptV2Form(value) {
|
|
|
4161
5279
|
const header = typeof form.title === "string" ? form.title : "";
|
|
4162
5280
|
const questions = form.fields.map((field) => adaptV2FormField(field, header)).filter((question) => question !== null);
|
|
4163
5281
|
if (questions.length === 0) return null;
|
|
4164
|
-
const tool =
|
|
5282
|
+
const tool = isRecord3(form.metadata) ? adaptFormTool(form.metadata.tool) : void 0;
|
|
4165
5283
|
return {
|
|
4166
5284
|
id: form.id,
|
|
4167
5285
|
sessionID: form.sessionID,
|
|
@@ -4171,7 +5289,7 @@ function adaptV2Form(value) {
|
|
|
4171
5289
|
};
|
|
4172
5290
|
}
|
|
4173
5291
|
function adaptV2FormList(value) {
|
|
4174
|
-
if (!
|
|
5292
|
+
if (!isRecord3(value) || !Array.isArray(value.data)) return null;
|
|
4175
5293
|
return value.data.map(adaptV2Form).filter((form) => form !== null);
|
|
4176
5294
|
}
|
|
4177
5295
|
function adaptPattern(value) {
|
|
@@ -4182,7 +5300,7 @@ function adaptPattern(value) {
|
|
|
4182
5300
|
return void 0;
|
|
4183
5301
|
}
|
|
4184
5302
|
function adaptV2PermissionWire(value) {
|
|
4185
|
-
if (!
|
|
5303
|
+
if (!isRecord3(value) || typeof value.id !== "string" || typeof value.sessionID !== "string") {
|
|
4186
5304
|
return null;
|
|
4187
5305
|
}
|
|
4188
5306
|
if (typeof value.permission !== "string" && typeof value.action !== "string") return null;
|
|
@@ -4194,12 +5312,12 @@ function adaptV2Permission(value) {
|
|
|
4194
5312
|
const type = permission.permission ?? permission.action;
|
|
4195
5313
|
if (!type) return null;
|
|
4196
5314
|
const pattern = adaptPattern(permission.pattern) ?? adaptPattern(permission.patterns) ?? adaptPattern(permission.resources);
|
|
4197
|
-
const time =
|
|
5315
|
+
const time = isRecord3(permission.time) ? finiteNumber2(permission.time.created) !== void 0 ? { created: finiteNumber2(permission.time.created) } : void 0 : void 0;
|
|
4198
5316
|
return {
|
|
4199
5317
|
id: permission.id,
|
|
4200
5318
|
type,
|
|
4201
5319
|
sessionID: permission.sessionID,
|
|
4202
|
-
metadata:
|
|
5320
|
+
metadata: isRecord3(permission.metadata) ? permission.metadata : {},
|
|
4203
5321
|
raw: permission,
|
|
4204
5322
|
...pattern !== void 0 ? { pattern } : {},
|
|
4205
5323
|
...typeof permission.messageID === "string" ? { messageID: permission.messageID } : {},
|
|
@@ -4209,12 +5327,12 @@ function adaptV2Permission(value) {
|
|
|
4209
5327
|
};
|
|
4210
5328
|
}
|
|
4211
5329
|
function adaptV2PermissionList(value) {
|
|
4212
|
-
if (!
|
|
5330
|
+
if (!isRecord3(value) || !Array.isArray(value.data)) return null;
|
|
4213
5331
|
return value.data.map(adaptV2Permission).filter((permission) => permission !== null);
|
|
4214
5332
|
}
|
|
4215
5333
|
function adaptV2Session(value) {
|
|
4216
|
-
if (!
|
|
4217
|
-
const time =
|
|
5334
|
+
if (!isRecord3(value) || typeof value.id !== "string" || value.id.length === 0) return null;
|
|
5335
|
+
const time = isRecord3(value.time) ? {
|
|
4218
5336
|
...finiteNumber2(value.time.created) !== void 0 ? { created: finiteNumber2(value.time.created) } : {},
|
|
4219
5337
|
...finiteNumber2(value.time.updated) !== void 0 ? { updated: finiteNumber2(value.time.updated) } : {}
|
|
4220
5338
|
} : void 0;
|
|
@@ -4226,7 +5344,7 @@ function adaptV2Session(value) {
|
|
|
4226
5344
|
};
|
|
4227
5345
|
}
|
|
4228
5346
|
function adaptV2SessionList(value) {
|
|
4229
|
-
if (!
|
|
5347
|
+
if (!isRecord3(value) || !Array.isArray(value.data) || !isRecord3(value.cursor)) return null;
|
|
4230
5348
|
return {
|
|
4231
5349
|
data: value.data.map(adaptV2Session).filter((session) => session !== null),
|
|
4232
5350
|
cursor: value.cursor
|
|
@@ -4235,18 +5353,18 @@ function adaptV2SessionList(value) {
|
|
|
4235
5353
|
function adaptV2Location(value) {
|
|
4236
5354
|
const candidates = [
|
|
4237
5355
|
value,
|
|
4238
|
-
|
|
4239
|
-
|
|
5356
|
+
isRecord3(value) ? value.data : void 0,
|
|
5357
|
+
isRecord3(value) ? value.location : void 0
|
|
4240
5358
|
];
|
|
4241
5359
|
for (const candidate of candidates) {
|
|
4242
|
-
if (!
|
|
5360
|
+
if (!isRecord3(candidate) || typeof candidate.directory !== "string") continue;
|
|
4243
5361
|
const directory = candidate.directory.trim();
|
|
4244
5362
|
if (directory) return directory;
|
|
4245
5363
|
}
|
|
4246
5364
|
return null;
|
|
4247
5365
|
}
|
|
4248
5366
|
function adaptV2Messages(value) {
|
|
4249
|
-
if (!
|
|
5367
|
+
if (!isRecord3(value) || !Array.isArray(value.data)) return [];
|
|
4250
5368
|
return value.data.slice().reverse().map(adaptV2Message).filter((message) => message !== null);
|
|
4251
5369
|
}
|
|
4252
5370
|
async function readJson(response) {
|
|
@@ -4261,7 +5379,7 @@ async function readJson(response) {
|
|
|
4261
5379
|
async function readData(client, path, init) {
|
|
4262
5380
|
const response = await client.request(path, init);
|
|
4263
5381
|
const body = await readJson(response);
|
|
4264
|
-
if (!
|
|
5382
|
+
if (!isRecord3(body) || !("data" in body)) {
|
|
4265
5383
|
throw new Error(`OpenCode V2 response for ${path} was missing its data envelope`);
|
|
4266
5384
|
}
|
|
4267
5385
|
return body.data;
|
|
@@ -4288,7 +5406,7 @@ async function createV2Session(client, directory) {
|
|
|
4288
5406
|
headers: { "Content-Type": "application/json" },
|
|
4289
5407
|
body: JSON.stringify({ location: { directory } })
|
|
4290
5408
|
});
|
|
4291
|
-
if (!
|
|
5409
|
+
if (!isRecord3(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
4292
5410
|
throw new Error("OpenCode V2 create session response was missing data.id");
|
|
4293
5411
|
}
|
|
4294
5412
|
return data.id;
|
|
@@ -4299,25 +5417,35 @@ async function getV2Session(client, sessionId) {
|
|
|
4299
5417
|
if (!session) throw new Error("OpenCode V2 get session response contained an invalid session");
|
|
4300
5418
|
return session;
|
|
4301
5419
|
}
|
|
4302
|
-
async function listV2SessionPage(client, cursor) {
|
|
5420
|
+
async function listV2SessionPage(client, cursor, signal) {
|
|
5421
|
+
const operation = cursor ? "GET /api/session (next page)" : "GET /api/session";
|
|
4303
5422
|
const path = cursor ? `/api/session?cursor=${encodeURIComponent(cursor)}` : "/api/session";
|
|
4304
5423
|
try {
|
|
4305
|
-
|
|
5424
|
+
const response = await client.request(path, signal ? { signal } : void 0, {
|
|
5425
|
+
allowStatuses: ALL_HTTP_STATUSES2
|
|
5426
|
+
});
|
|
5427
|
+
if (!response.ok) {
|
|
5428
|
+
console.error(
|
|
5429
|
+
`[listV2SessionPage] ${operation} returned HTTP ${response.status} (port ${client.port})`
|
|
5430
|
+
);
|
|
5431
|
+
return null;
|
|
5432
|
+
}
|
|
5433
|
+
return adaptV2SessionList(await readJson(response));
|
|
4306
5434
|
} catch (error2) {
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
);
|
|
5435
|
+
if (signal?.aborted) return null;
|
|
5436
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
5437
|
+
console.error(`[listV2SessionPage] ${operation} failed (port ${client.port}): ${errorName}`);
|
|
4310
5438
|
return null;
|
|
4311
5439
|
}
|
|
4312
5440
|
}
|
|
4313
|
-
async function listV2Sessions(client) {
|
|
5441
|
+
async function listV2Sessions(client, signal) {
|
|
4314
5442
|
const sessions = [];
|
|
4315
5443
|
const seenCursors = /* @__PURE__ */ new Set();
|
|
4316
5444
|
let cursor;
|
|
4317
5445
|
let hasNextPage = true;
|
|
4318
5446
|
try {
|
|
4319
5447
|
while (hasNextPage) {
|
|
4320
|
-
const page = await listV2SessionPage(client, cursor);
|
|
5448
|
+
const page = await listV2SessionPage(client, cursor, signal);
|
|
4321
5449
|
if (!page) return null;
|
|
4322
5450
|
sessions.push(...page.data);
|
|
4323
5451
|
const next = page.cursor.next;
|
|
@@ -4333,8 +5461,9 @@ async function listV2Sessions(client) {
|
|
|
4333
5461
|
}
|
|
4334
5462
|
return sessions;
|
|
4335
5463
|
} catch (error2) {
|
|
5464
|
+
const errorName = error2 instanceof Error && ["AbortError", "Error", "TimeoutError", "TypeError"].includes(error2.name) ? error2.name : "unknown error";
|
|
4336
5465
|
console.error(
|
|
4337
|
-
`[listV2Sessions] session pagination failed
|
|
5466
|
+
`[listV2Sessions] GET /api/session pagination failed (port ${client.port}): ${errorName}`
|
|
4338
5467
|
);
|
|
4339
5468
|
return null;
|
|
4340
5469
|
}
|
|
@@ -4344,7 +5473,7 @@ async function deleteV2Session(client, sessionId) {
|
|
|
4344
5473
|
const response = await client.request(
|
|
4345
5474
|
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
4346
5475
|
{ method: "DELETE" },
|
|
4347
|
-
{ allowStatuses:
|
|
5476
|
+
{ allowStatuses: ALL_HTTP_STATUSES2 }
|
|
4348
5477
|
);
|
|
4349
5478
|
if (response.status >= 200 && response.status < 300) {
|
|
4350
5479
|
return { ok: true, status: response.status };
|
|
@@ -4395,8 +5524,8 @@ async function sendV2Prompt(client, sessionId, text) {
|
|
|
4395
5524
|
`OpenCode V2 prompt response could not be read: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4396
5525
|
);
|
|
4397
5526
|
}
|
|
4398
|
-
const data =
|
|
4399
|
-
if (!
|
|
5527
|
+
const data = isRecord3(body) && "data" in body ? body.data : void 0;
|
|
5528
|
+
if (!isRecord3(data) || typeof data.id !== "string" || data.id.length === 0) {
|
|
4400
5529
|
throw new OpenCodeV2PromptAckError("OpenCode V2 prompt response was missing data.id");
|
|
4401
5530
|
}
|
|
4402
5531
|
return data.id;
|
|
@@ -4405,7 +5534,7 @@ async function getV2SessionMessages(client, sessionId) {
|
|
|
4405
5534
|
const path = `/api/session/${encodeURIComponent(sessionId)}/message?order=desc&limit=200`;
|
|
4406
5535
|
try {
|
|
4407
5536
|
const body = await readJson(await client.request(path));
|
|
4408
|
-
if (!
|
|
5537
|
+
if (!isRecord3(body) || !Array.isArray(body.data) || !isRecord3(body.cursor)) return null;
|
|
4409
5538
|
return adaptV2Messages(body);
|
|
4410
5539
|
} catch (error2) {
|
|
4411
5540
|
console.error(
|
|
@@ -4444,7 +5573,7 @@ async function interruptV2Session(client, sessionId) {
|
|
|
4444
5573
|
async function getV2ActiveSessions(client) {
|
|
4445
5574
|
try {
|
|
4446
5575
|
const body = await readJson(await client.request("/api/session/active"));
|
|
4447
|
-
if (!
|
|
5576
|
+
if (!isRecord3(body) || !isRecord3(body.data)) return null;
|
|
4448
5577
|
return body.data;
|
|
4449
5578
|
} catch (error2) {
|
|
4450
5579
|
console.error(
|
|
@@ -4460,8 +5589,8 @@ async function isV2SessionOngoing(client, sessionId) {
|
|
|
4460
5589
|
}
|
|
4461
5590
|
function sessionErrorReason2(value) {
|
|
4462
5591
|
if (typeof value === "string" && value.trim()) return value.trim().slice(0, 500);
|
|
4463
|
-
if (
|
|
4464
|
-
const data =
|
|
5592
|
+
if (isRecord3(value)) {
|
|
5593
|
+
const data = isRecord3(value.data) ? value.data : void 0;
|
|
4465
5594
|
const reason = typeof data?.message === "string" && data.message || typeof value.message === "string" && value.message || typeof value.name === "string" && value.name;
|
|
4466
5595
|
if (reason) return reason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
4467
5596
|
}
|
|
@@ -4477,17 +5606,17 @@ function adaptV2SessionErrorEvent(value) {
|
|
|
4477
5606
|
return null;
|
|
4478
5607
|
}
|
|
4479
5608
|
}
|
|
4480
|
-
if (!
|
|
5609
|
+
if (!isRecord3(parsed)) return null;
|
|
4481
5610
|
try {
|
|
4482
5611
|
const establishedShape = parseSessionErrorFrame(JSON.stringify(parsed));
|
|
4483
5612
|
if (establishedShape) return establishedShape;
|
|
4484
5613
|
} catch (error2) {
|
|
4485
5614
|
void error2;
|
|
4486
5615
|
}
|
|
4487
|
-
const candidates = [parsed, parsed.payload, parsed.data].filter(
|
|
5616
|
+
const candidates = [parsed, parsed.payload, parsed.data].filter(isRecord3);
|
|
4488
5617
|
for (const event of candidates) {
|
|
4489
5618
|
if (event.type !== "session.error") continue;
|
|
4490
|
-
const properties = [event.properties, event.data, event].find(
|
|
5619
|
+
const properties = [event.properties, event.data, event].find(isRecord3);
|
|
4491
5620
|
if (!properties) continue;
|
|
4492
5621
|
const sessionId = typeof properties.sessionID === "string" && properties.sessionID || typeof properties.sessionId === "string" && properties.sessionId;
|
|
4493
5622
|
if (!sessionId) continue;
|
|
@@ -4537,13 +5666,13 @@ async function readV2SessionErrorStream(client, options) {
|
|
|
4537
5666
|
}
|
|
4538
5667
|
|
|
4539
5668
|
// src/lib/opencode/credential-import-v2.ts
|
|
4540
|
-
import { readFileSync as
|
|
4541
|
-
import { join as
|
|
5669
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
5670
|
+
import { join as join8 } from "node:path";
|
|
4542
5671
|
|
|
4543
5672
|
// src/lib/openai-usage.ts
|
|
4544
|
-
import { readFileSync as
|
|
5673
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4545
5674
|
import { homedir as homedir3 } from "node:os";
|
|
4546
|
-
import { join as
|
|
5675
|
+
import { join as join7 } from "node:path";
|
|
4547
5676
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
4548
5677
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
4549
5678
|
var OpenAiUsageError = class extends Error {
|
|
@@ -4557,7 +5686,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
4557
5686
|
}
|
|
4558
5687
|
function readOpenCodeChatGptCredentials() {
|
|
4559
5688
|
try {
|
|
4560
|
-
const raw =
|
|
5689
|
+
const raw = readFileSync7(join7(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
4561
5690
|
let parsed;
|
|
4562
5691
|
try {
|
|
4563
5692
|
parsed = JSON.parse(raw);
|
|
@@ -4623,7 +5752,7 @@ function parseCodexUsageHeaders(headers) {
|
|
|
4623
5752
|
function normalizeProbeModel(model) {
|
|
4624
5753
|
return model.endsWith("-fast") ? model.slice(0, -"-fast".length) : model;
|
|
4625
5754
|
}
|
|
4626
|
-
function
|
|
5755
|
+
function isRecord4(value) {
|
|
4627
5756
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4628
5757
|
}
|
|
4629
5758
|
function unsupportedProbeModels(reason, port) {
|
|
@@ -4634,14 +5763,14 @@ async function resolveV1ProbeModels(client, port) {
|
|
|
4634
5763
|
try {
|
|
4635
5764
|
const response = await client.request("/config/providers");
|
|
4636
5765
|
const body = await response.json();
|
|
4637
|
-
if (!
|
|
5766
|
+
if (!isRecord4(body) || !Array.isArray(body.providers)) {
|
|
4638
5767
|
return unsupportedProbeModels("V1 provider response did not contain a providers array", port);
|
|
4639
5768
|
}
|
|
4640
5769
|
const provider = body.providers.find(
|
|
4641
|
-
(candidate) =>
|
|
5770
|
+
(candidate) => isRecord4(candidate) && candidate.id === "openai"
|
|
4642
5771
|
);
|
|
4643
|
-
if (!provider || !
|
|
4644
|
-
const defaults2 =
|
|
5772
|
+
if (!provider || !isRecord4(provider.models)) return { status: "supported", models: [] };
|
|
5773
|
+
const defaults2 = isRecord4(body.default) ? body.default : void 0;
|
|
4645
5774
|
const candidates = [
|
|
4646
5775
|
...typeof defaults2?.openai === "string" ? [defaults2.openai] : [],
|
|
4647
5776
|
...Object.keys(provider.models)
|
|
@@ -4663,14 +5792,14 @@ async function resolveV2ProbeModels(client, port) {
|
|
|
4663
5792
|
try {
|
|
4664
5793
|
const response = await client.request(path);
|
|
4665
5794
|
const body = await response.json();
|
|
4666
|
-
if (!
|
|
5795
|
+
if (!isRecord4(body) || !Array.isArray(body.data)) {
|
|
4667
5796
|
return unsupportedProbeModels(`V2 GET ${path} did not contain a provider data array`, port);
|
|
4668
5797
|
}
|
|
4669
5798
|
const provider = body.data.find(
|
|
4670
|
-
(candidate) =>
|
|
5799
|
+
(candidate) => isRecord4(candidate) && candidate.id === "openai"
|
|
4671
5800
|
);
|
|
4672
5801
|
if (!provider) return { status: "supported", models: [] };
|
|
4673
|
-
if (!
|
|
5802
|
+
if (!isRecord4(provider.models)) {
|
|
4674
5803
|
return unsupportedProbeModels(
|
|
4675
5804
|
"V2 provider response has no safe OpenAI model catalogue",
|
|
4676
5805
|
port
|
|
@@ -4780,7 +5909,7 @@ var CONNECT_KEY_MAX_ATTEMPTS = 3;
|
|
|
4780
5909
|
var CONNECT_KEY_RETRY_DELAY_MS = 250;
|
|
4781
5910
|
var INTEGRATION_LIST_MAX_READS = 10;
|
|
4782
5911
|
var INTEGRATION_LIST_RETRY_DELAY_MS = 1e3;
|
|
4783
|
-
function
|
|
5912
|
+
function isRecord5(value) {
|
|
4784
5913
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4785
5914
|
}
|
|
4786
5915
|
var sleep2 = (milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds));
|
|
@@ -4789,14 +5918,14 @@ function emptyResult(unsupported = [], outcome = "completed") {
|
|
|
4789
5918
|
}
|
|
4790
5919
|
function readOpenCodeCredentials(homeDir, log3) {
|
|
4791
5920
|
try {
|
|
4792
|
-
const raw =
|
|
5921
|
+
const raw = readFileSync8(join8(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
|
|
4793
5922
|
let parsed;
|
|
4794
5923
|
try {
|
|
4795
5924
|
parsed = JSON.parse(raw);
|
|
4796
5925
|
} catch (error2) {
|
|
4797
5926
|
return null;
|
|
4798
5927
|
}
|
|
4799
|
-
return
|
|
5928
|
+
return isRecord5(parsed) ? parsed : null;
|
|
4800
5929
|
} catch (error2) {
|
|
4801
5930
|
const code = error2.code;
|
|
4802
5931
|
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
@@ -4809,7 +5938,7 @@ function classifyOpenCodeCredentials(credentials2) {
|
|
|
4809
5938
|
const importable = [];
|
|
4810
5939
|
const unsupported = [];
|
|
4811
5940
|
for (const [integrationId, value] of Object.entries(credentials2)) {
|
|
4812
|
-
if (!
|
|
5941
|
+
if (!isRecord5(value) || typeof value.type !== "string") {
|
|
4813
5942
|
unsupported.push({ integrationId, reason: "unrecognised credential entry" });
|
|
4814
5943
|
continue;
|
|
4815
5944
|
}
|
|
@@ -4824,7 +5953,7 @@ function classifyOpenCodeCredentials(credentials2) {
|
|
|
4824
5953
|
unsupported.push({ integrationId, reason: "unrecognised credential entry" });
|
|
4825
5954
|
continue;
|
|
4826
5955
|
}
|
|
4827
|
-
if (value.metadata === void 0 ||
|
|
5956
|
+
if (value.metadata === void 0 || isRecord5(value.metadata) && Object.keys(value.metadata).length === 0) {
|
|
4828
5957
|
importable.push({ integrationId, key: value.key });
|
|
4829
5958
|
} else {
|
|
4830
5959
|
unsupported.push({ integrationId, reason: "needs the interactive connect flow" });
|
|
@@ -4837,7 +5966,7 @@ function statusFromRequestError(error2) {
|
|
|
4837
5966
|
const match = /^OpenCode request failed: HTTP (\d{3})/.exec(error2.message);
|
|
4838
5967
|
return match ? Number(match[1]) : void 0;
|
|
4839
5968
|
}
|
|
4840
|
-
function
|
|
5969
|
+
function failureMessage2(status2) {
|
|
4841
5970
|
return status2 === void 0 ? "OpenCode credential import request failed" : `OpenCode credential import request failed with HTTP ${status2}`;
|
|
4842
5971
|
}
|
|
4843
5972
|
function isRetryableStatus(status2) {
|
|
@@ -4864,18 +5993,18 @@ async function importOneCredential(request3, credential, sleepForRetry, log3) {
|
|
|
4864
5993
|
const failure = {
|
|
4865
5994
|
integrationId: credential.integrationId,
|
|
4866
5995
|
status: response.status,
|
|
4867
|
-
message:
|
|
5996
|
+
message: failureMessage2(response.status)
|
|
4868
5997
|
};
|
|
4869
|
-
log3(`${
|
|
5998
|
+
log3(`${failureMessage2(failure.status)} for ${failure.integrationId}`, "warn");
|
|
4870
5999
|
return { ok: false, failure };
|
|
4871
6000
|
} catch (error2) {
|
|
4872
6001
|
const status2 = statusFromRequestError(error2);
|
|
4873
6002
|
const failure = {
|
|
4874
6003
|
integrationId: credential.integrationId,
|
|
4875
6004
|
...status2 === void 0 ? {} : { status: status2 },
|
|
4876
|
-
message:
|
|
6005
|
+
message: failureMessage2(status2)
|
|
4877
6006
|
};
|
|
4878
|
-
log3(`${
|
|
6007
|
+
log3(`${failureMessage2(status2)} for ${failure.integrationId}`, "warn");
|
|
4879
6008
|
return { ok: false, failure };
|
|
4880
6009
|
}
|
|
4881
6010
|
}
|
|
@@ -4901,10 +6030,10 @@ async function importOpenCodeCredentials({
|
|
|
4901
6030
|
return result;
|
|
4902
6031
|
}
|
|
4903
6032
|
function parseIntegrationList(value) {
|
|
4904
|
-
if (!
|
|
6033
|
+
if (!isRecord5(value) || !Array.isArray(value.data)) return null;
|
|
4905
6034
|
const integrationIds = /* @__PURE__ */ new Set();
|
|
4906
6035
|
for (const integration of value.data) {
|
|
4907
|
-
if (!
|
|
6036
|
+
if (!isRecord5(integration) || typeof integration.id !== "string" || !Array.isArray(integration.connections)) {
|
|
4908
6037
|
return null;
|
|
4909
6038
|
}
|
|
4910
6039
|
if (integration.connections.length > 0) integrationIds.add(integration.id);
|
|
@@ -4963,113 +6092,35 @@ async function importOpenCodeV2Credentials({
|
|
|
4963
6092
|
guard.inFlight = true;
|
|
4964
6093
|
try {
|
|
4965
6094
|
const credentials2 = readOpenCodeCredentials(homeDir, log3);
|
|
4966
|
-
if (credentials2 === null) return emptyResult();
|
|
4967
|
-
const classified = classifyOpenCodeCredentials(credentials2);
|
|
4968
|
-
if (classified.importable.length === 0) return emptyResult(classified.unsupported);
|
|
4969
|
-
const connectedIntegrationIds = await getConnectedOpenCodeIntegrationIds({
|
|
4970
|
-
request: request3,
|
|
4971
|
-
directory,
|
|
4972
|
-
sleepForRetry,
|
|
4973
|
-
log: log3
|
|
4974
|
-
});
|
|
4975
|
-
if (connectedIntegrationIds === null) {
|
|
4976
|
-
return emptyResult(classified.unsupported, "connection-list-unavailable");
|
|
4977
|
-
}
|
|
4978
|
-
return importOpenCodeCredentials({
|
|
4979
|
-
request: request3,
|
|
4980
|
-
classified,
|
|
4981
|
-
connectedIntegrationIds,
|
|
4982
|
-
sleepForRetry,
|
|
4983
|
-
log: log3
|
|
4984
|
-
});
|
|
4985
|
-
} finally {
|
|
4986
|
-
guard.inFlight = false;
|
|
4987
|
-
}
|
|
4988
|
-
}
|
|
4989
|
-
|
|
4990
|
-
// src/lib/opencode/session-db-boot.ts
|
|
4991
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
4992
|
-
import { mkdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
|
|
4993
|
-
import { homedir as homedir4 } from "node:os";
|
|
4994
|
-
import { dirname as dirname4, resolve as resolvePath } from "node:path";
|
|
4995
|
-
|
|
4996
|
-
// src/lib/runner-synchroniser.ts
|
|
4997
|
-
import { spawn } from "node:child_process";
|
|
4998
|
-
function appendError(stderr, error2) {
|
|
4999
|
-
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
5000
|
-
return stderr === "" ? message : `${stderr}
|
|
5001
|
-
${message}`;
|
|
5002
|
-
}
|
|
5003
|
-
function runSynchroniser(args, opts) {
|
|
5004
|
-
return new Promise((resolve5) => {
|
|
5005
|
-
let child;
|
|
5006
|
-
let stdout = "";
|
|
5007
|
-
let stderr = "";
|
|
5008
|
-
let settled = false;
|
|
5009
|
-
const timer = {};
|
|
5010
|
-
let abortListener;
|
|
5011
|
-
let spawnListener;
|
|
5012
|
-
const finish = (result) => {
|
|
5013
|
-
if (settled) return;
|
|
5014
|
-
settled = true;
|
|
5015
|
-
if (timer.handle) clearTimeout(timer.handle);
|
|
5016
|
-
if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
|
|
5017
|
-
if (spawnListener) child.removeListener("spawn", spawnListener);
|
|
5018
|
-
resolve5(result);
|
|
5019
|
-
};
|
|
5020
|
-
try {
|
|
5021
|
-
child = spawn("runner-synchroniser", args, {
|
|
5022
|
-
env: opts.env ?? process.env,
|
|
5023
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5024
|
-
});
|
|
5025
|
-
} catch (error2) {
|
|
5026
|
-
finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
|
|
5027
|
-
return;
|
|
5028
|
-
}
|
|
5029
|
-
child.stdout?.setEncoding("utf8");
|
|
5030
|
-
child.stdout?.on("data", (chunk) => {
|
|
5031
|
-
stdout += chunk;
|
|
5032
|
-
});
|
|
5033
|
-
child.stderr?.setEncoding("utf8");
|
|
5034
|
-
child.stderr?.on("data", (chunk) => {
|
|
5035
|
-
stderr += chunk;
|
|
5036
|
-
});
|
|
5037
|
-
child.once("error", (error2) => {
|
|
5038
|
-
finish({ code: null, stdout, stderr: appendError(stderr, error2), timedOut: false });
|
|
5039
|
-
});
|
|
5040
|
-
child.once("close", (code) => {
|
|
5041
|
-
finish({ code, stdout, stderr, timedOut: false });
|
|
6095
|
+
if (credentials2 === null) return emptyResult();
|
|
6096
|
+
const classified = classifyOpenCodeCredentials(credentials2);
|
|
6097
|
+
if (classified.importable.length === 0) return emptyResult(classified.unsupported);
|
|
6098
|
+
const connectedIntegrationIds = await getConnectedOpenCodeIntegrationIds({
|
|
6099
|
+
request: request3,
|
|
6100
|
+
directory,
|
|
6101
|
+
sleepForRetry,
|
|
6102
|
+
log: log3
|
|
5042
6103
|
});
|
|
5043
|
-
if (
|
|
5044
|
-
|
|
5045
|
-
if (child.pid === void 0) {
|
|
5046
|
-
if (!spawnListener) {
|
|
5047
|
-
spawnListener = killChild;
|
|
5048
|
-
child.once("spawn", spawnListener);
|
|
5049
|
-
}
|
|
5050
|
-
return;
|
|
5051
|
-
}
|
|
5052
|
-
child.kill("SIGKILL");
|
|
5053
|
-
};
|
|
5054
|
-
abortListener = killChild;
|
|
5055
|
-
if (opts.signal.aborted) {
|
|
5056
|
-
abortListener();
|
|
5057
|
-
} else {
|
|
5058
|
-
opts.signal.addEventListener("abort", abortListener, { once: true });
|
|
5059
|
-
if (opts.signal.aborted) abortListener();
|
|
5060
|
-
}
|
|
6104
|
+
if (connectedIntegrationIds === null) {
|
|
6105
|
+
return emptyResult(classified.unsupported, "connection-list-unavailable");
|
|
5061
6106
|
}
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
);
|
|
5069
|
-
}
|
|
6107
|
+
return importOpenCodeCredentials({
|
|
6108
|
+
request: request3,
|
|
6109
|
+
classified,
|
|
6110
|
+
connectedIntegrationIds,
|
|
6111
|
+
sleepForRetry,
|
|
6112
|
+
log: log3
|
|
6113
|
+
});
|
|
6114
|
+
} finally {
|
|
6115
|
+
guard.inFlight = false;
|
|
6116
|
+
}
|
|
5070
6117
|
}
|
|
5071
6118
|
|
|
5072
6119
|
// src/lib/opencode/session-db-boot.ts
|
|
6120
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
6121
|
+
import { mkdirSync as mkdirSync4, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
6122
|
+
import { homedir as homedir4 } from "node:os";
|
|
6123
|
+
import { dirname as dirname7, resolve as resolvePath } from "node:path";
|
|
5073
6124
|
var SESSION_DB_RESTORE_TIMEOUT_MS = 3e5;
|
|
5074
6125
|
var SESSION_DB_VERIFY_TIMEOUT_MS = 36e4;
|
|
5075
6126
|
var SESSION_DB_SYNCHRONISER_TIMEOUT_MS = 12e4;
|
|
@@ -5107,7 +6158,7 @@ function reportRecord(stage, outcome, reason, litestreamExitCode, options) {
|
|
|
5107
6158
|
function clearMarker(options) {
|
|
5108
6159
|
if (!options.noReplicateMarker) return;
|
|
5109
6160
|
try {
|
|
5110
|
-
|
|
6161
|
+
unlinkSync3(options.noReplicateMarker);
|
|
5111
6162
|
} catch (error2) {
|
|
5112
6163
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return;
|
|
5113
6164
|
options.log(
|
|
@@ -5119,8 +6170,8 @@ function clearMarker(options) {
|
|
|
5119
6170
|
function markNoReplicate(options, message) {
|
|
5120
6171
|
if (options.noReplicateMarker) {
|
|
5121
6172
|
try {
|
|
5122
|
-
|
|
5123
|
-
|
|
6173
|
+
mkdirSync4(dirname7(options.noReplicateMarker), { recursive: true });
|
|
6174
|
+
writeFileSync4(options.noReplicateMarker, "");
|
|
5124
6175
|
} catch (error2) {
|
|
5125
6176
|
options.log(
|
|
5126
6177
|
`Could not write session-DB no-replicate marker ${options.noReplicateMarker}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
@@ -5133,7 +6184,7 @@ function markNoReplicate(options, message) {
|
|
|
5133
6184
|
function discardSessionDbDebris(options) {
|
|
5134
6185
|
for (const path of [options.dbPath, `${options.dbPath}-wal`, `${options.dbPath}-shm`]) {
|
|
5135
6186
|
try {
|
|
5136
|
-
|
|
6187
|
+
unlinkSync3(path);
|
|
5137
6188
|
} catch (error2) {
|
|
5138
6189
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") continue;
|
|
5139
6190
|
options.log(
|
|
@@ -5274,8 +6325,8 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
5274
6325
|
return null;
|
|
5275
6326
|
}
|
|
5276
6327
|
try {
|
|
5277
|
-
|
|
5278
|
-
|
|
6328
|
+
mkdirSync4(dirname7(configPath), { recursive: true });
|
|
6329
|
+
writeFileSync4(configPath, rendered.stdout);
|
|
5279
6330
|
} catch (error2) {
|
|
5280
6331
|
options.log(
|
|
5281
6332
|
`Could not write ${configPath}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
@@ -5540,8 +6591,8 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
5540
6591
|
|
|
5541
6592
|
// src/lib/opencode/session-db-provenance.ts
|
|
5542
6593
|
import { createRequire } from "node:module";
|
|
5543
|
-
import { mkdirSync as
|
|
5544
|
-
import { dirname as
|
|
6594
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6595
|
+
import { dirname as dirname8, join as join9 } from "node:path";
|
|
5545
6596
|
var require2 = createRequire(import.meta.url);
|
|
5546
6597
|
function readSessionDbMigrationIds(dbPath) {
|
|
5547
6598
|
let db;
|
|
@@ -5578,12 +6629,12 @@ function readSessionDbMigrationIds(dbPath) {
|
|
|
5578
6629
|
}
|
|
5579
6630
|
function sessionDbProvenanceStatePath(homeDir, env) {
|
|
5580
6631
|
const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
|
|
5581
|
-
return override ||
|
|
6632
|
+
return override || join9(homeDir, ".local", "state", "evident", "session-db-provenance.json");
|
|
5582
6633
|
}
|
|
5583
6634
|
function loadSessionDbProvenanceState(path) {
|
|
5584
6635
|
let value;
|
|
5585
6636
|
try {
|
|
5586
|
-
value = JSON.parse(
|
|
6637
|
+
value = JSON.parse(readFileSync9(path, "utf8"));
|
|
5587
6638
|
} catch (error2) {
|
|
5588
6639
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return {};
|
|
5589
6640
|
console.error(
|
|
@@ -5607,8 +6658,8 @@ function loadSessionDbProvenanceState(path) {
|
|
|
5607
6658
|
}
|
|
5608
6659
|
function saveSessionDbProvenanceState(path, state) {
|
|
5609
6660
|
try {
|
|
5610
|
-
|
|
5611
|
-
|
|
6661
|
+
mkdirSync5(dirname8(path), { recursive: true });
|
|
6662
|
+
writeFileSync5(path, `${JSON.stringify(state, null, 2)}
|
|
5612
6663
|
`, "utf8");
|
|
5613
6664
|
} catch (error2) {
|
|
5614
6665
|
console.error(
|
|
@@ -5663,6 +6714,26 @@ function isSessionDbProvenanceRecord(value) {
|
|
|
5663
6714
|
return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
|
|
5664
6715
|
}
|
|
5665
6716
|
|
|
6717
|
+
// src/lib/opencode/detect.ts
|
|
6718
|
+
function describeProbe(route, result) {
|
|
6719
|
+
if (result.error?.startsWith("HTTP ")) return `${route}: ${result.error}`;
|
|
6720
|
+
if (result.error) return `${route}: no HTTP response (${result.error})`;
|
|
6721
|
+
return `${route}: ${result.healthy ? "healthy" : "no response detail"}`;
|
|
6722
|
+
}
|
|
6723
|
+
async function classifyRunningOpenCode(port) {
|
|
6724
|
+
const v1 = await checkOpenCodeHealth(port);
|
|
6725
|
+
if (v1.healthy) return { kind: "v1", version: v1.version ?? null };
|
|
6726
|
+
const v2 = await checkOpenCode2Health(port);
|
|
6727
|
+
if (v2.healthy) return { kind: "v2", version: v2.version ?? null };
|
|
6728
|
+
const detail = [
|
|
6729
|
+
describeProbe("V1 /global/health", v1),
|
|
6730
|
+
describeProbe("V2 /api/info (or /api/health fallback)", v2)
|
|
6731
|
+
].join("; ");
|
|
6732
|
+
if (v1.okWithNonJsonBody && v2.authFailed) return { kind: "v2", version: null };
|
|
6733
|
+
if (v1.authFailed && v2.authFailed) return { kind: "authenticated-unknown", detail };
|
|
6734
|
+
return { kind: "no-server-identified", detail };
|
|
6735
|
+
}
|
|
6736
|
+
|
|
5666
6737
|
// src/lib/opencode/opencode2-package.ts
|
|
5667
6738
|
var OPENCODE2_PACKAGE_SPEC = "@opencode-ai/cli@0.0.0-beta-17823";
|
|
5668
6739
|
|
|
@@ -5971,26 +7042,28 @@ async function startOpenCode(port, options = {}) {
|
|
|
5971
7042
|
}
|
|
5972
7043
|
async function startOpenCode2(port, options = {}) {
|
|
5973
7044
|
const password = randomBytes(24).toString("hex");
|
|
5974
|
-
let command = "opencode2";
|
|
7045
|
+
let command = options.command ?? "opencode2";
|
|
5975
7046
|
const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
|
|
5976
7047
|
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...logLevel];
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
7048
|
+
if (options.command === void 0) {
|
|
7049
|
+
try {
|
|
7050
|
+
execSync("which opencode2", { stdio: "ignore" });
|
|
7051
|
+
} catch {
|
|
7052
|
+
command = "npx";
|
|
7053
|
+
args = [
|
|
7054
|
+
"-y",
|
|
7055
|
+
"-p",
|
|
7056
|
+
OPENCODE2_PACKAGE_SPEC,
|
|
7057
|
+
"--",
|
|
7058
|
+
"opencode2",
|
|
7059
|
+
"serve",
|
|
7060
|
+
"--port",
|
|
7061
|
+
port.toString(),
|
|
7062
|
+
"--hostname",
|
|
7063
|
+
"127.0.0.1",
|
|
7064
|
+
...logLevel
|
|
7065
|
+
];
|
|
7066
|
+
}
|
|
5994
7067
|
}
|
|
5995
7068
|
const child = spawn3(command, args, {
|
|
5996
7069
|
env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
|
|
@@ -6047,6 +7120,56 @@ function isOpenCode2Installed() {
|
|
|
6047
7120
|
return false;
|
|
6048
7121
|
}
|
|
6049
7122
|
}
|
|
7123
|
+
function readVersionOutput(command) {
|
|
7124
|
+
try {
|
|
7125
|
+
return execSync2(`${command} --version`, {
|
|
7126
|
+
encoding: "utf-8",
|
|
7127
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
7128
|
+
}).trim();
|
|
7129
|
+
} catch (error2) {
|
|
7130
|
+
const output = typeof error2 === "object" && error2 !== null ? [
|
|
7131
|
+
"stdout" in error2 ? String(error2.stdout ?? "").trim() : "",
|
|
7132
|
+
"stderr" in error2 ? String(error2.stderr ?? "").trim() : ""
|
|
7133
|
+
].filter(Boolean).join("\n") : "";
|
|
7134
|
+
return output || (error2 instanceof Error ? error2.message : String(error2));
|
|
7135
|
+
}
|
|
7136
|
+
}
|
|
7137
|
+
function parseOpenCodeVersion(rawVersion) {
|
|
7138
|
+
const output = rawVersion.trim();
|
|
7139
|
+
const prefix = output.match(/^(opencode2|opencode)\s+/i)?.[1]?.toLowerCase();
|
|
7140
|
+
const version2 = (prefix ? output.slice(prefix.length).trim() : output).replace(/^v/i, "");
|
|
7141
|
+
if (prefix === "opencode2") return { major: "v2", version: version2 };
|
|
7142
|
+
const major = version2.match(/^(\d+)(?:\.\d+)*(?:[-+][0-9A-Za-z.-]+)?$/)?.[1];
|
|
7143
|
+
if (!major) return null;
|
|
7144
|
+
if (Number(major) === 1) return { major: "v1", version: version2 };
|
|
7145
|
+
if (Number(major) >= 2) return { major: "v2", version: version2 };
|
|
7146
|
+
return null;
|
|
7147
|
+
}
|
|
7148
|
+
function resolveInstalledOpenCode() {
|
|
7149
|
+
let opencode2Available = false;
|
|
7150
|
+
if (isOpenCodeInstalled()) {
|
|
7151
|
+
const rawVersion = readVersionOutput("opencode");
|
|
7152
|
+
const parsed = parseOpenCodeVersion(rawVersion);
|
|
7153
|
+
if (parsed?.major === "v1") {
|
|
7154
|
+
return { kind: "installed", major: "v1", command: "opencode", version: parsed.version };
|
|
7155
|
+
}
|
|
7156
|
+
if (parsed?.major === "v2") {
|
|
7157
|
+
return { kind: "installed", major: "v2", command: "opencode", version: parsed.version };
|
|
7158
|
+
}
|
|
7159
|
+
opencode2Available = isOpenCode2Installed();
|
|
7160
|
+
if (!opencode2Available) return { kind: "unknown", command: "opencode", rawVersion };
|
|
7161
|
+
}
|
|
7162
|
+
if (opencode2Available || isOpenCode2Installed()) {
|
|
7163
|
+
const parsed = parseOpenCodeVersion(readVersionOutput("opencode2"));
|
|
7164
|
+
return {
|
|
7165
|
+
kind: "installed",
|
|
7166
|
+
major: "v2",
|
|
7167
|
+
command: "opencode2",
|
|
7168
|
+
version: parsed?.version
|
|
7169
|
+
};
|
|
7170
|
+
}
|
|
7171
|
+
return { kind: "none" };
|
|
7172
|
+
}
|
|
6050
7173
|
async function promptOpenCodeInstall(interactive) {
|
|
6051
7174
|
if (!interactive) {
|
|
6052
7175
|
console.log(
|
|
@@ -6147,124 +7270,17 @@ function buildNoProviderWarning(hasProvider) {
|
|
|
6147
7270
|
return "Warning: opencode has no authenticated model provider configured, so it won't be able to answer prompts. Run `opencode auth login` to set one up (see https://opencode.ai for details).";
|
|
6148
7271
|
}
|
|
6149
7272
|
|
|
6150
|
-
// src/lib/opencode/session-cleanup.ts
|
|
6151
|
-
var DURATION_UNIT_MS = {
|
|
6152
|
-
s: 1e3,
|
|
6153
|
-
m: 60 * 1e3,
|
|
6154
|
-
h: 60 * 60 * 1e3,
|
|
6155
|
-
d: 24 * 60 * 60 * 1e3
|
|
6156
|
-
};
|
|
6157
|
-
function parseDurationMs(input) {
|
|
6158
|
-
const trimmed = input.trim();
|
|
6159
|
-
const match = /^(\d+)([smhd])$/.exec(trimmed);
|
|
6160
|
-
if (!match) {
|
|
6161
|
-
throw new Error(
|
|
6162
|
-
`Invalid duration "${input}": expected <number><unit> where unit is one of s, m, h, d (e.g. "7d", "24h", "30m", "90s").`
|
|
6163
|
-
);
|
|
6164
|
-
}
|
|
6165
|
-
const value = Number(match[1]);
|
|
6166
|
-
if (value <= 0) {
|
|
6167
|
-
throw new Error(`Invalid duration "${input}": must be a positive value.`);
|
|
6168
|
-
}
|
|
6169
|
-
return value * DURATION_UNIT_MS[match[2]];
|
|
6170
|
-
}
|
|
6171
|
-
function selectSessionsToDelete(sessions, opts) {
|
|
6172
|
-
const { maxAgeMs, maxCount, nowMs, protectedIds } = opts;
|
|
6173
|
-
if (maxAgeMs === void 0 && maxCount === void 0) return [];
|
|
6174
|
-
const ageEligible = (s) => {
|
|
6175
|
-
if (maxAgeMs === void 0) return false;
|
|
6176
|
-
if (s.lastActivityMs === null) return true;
|
|
6177
|
-
return nowMs - s.lastActivityMs > maxAgeMs;
|
|
6178
|
-
};
|
|
6179
|
-
const countEligibleIds = /* @__PURE__ */ new Set();
|
|
6180
|
-
if (maxCount !== void 0) {
|
|
6181
|
-
const byActivityDesc = [...sessions].sort(
|
|
6182
|
-
(a, b) => (b.lastActivityMs ?? -Infinity) - (a.lastActivityMs ?? -Infinity)
|
|
6183
|
-
);
|
|
6184
|
-
for (const s of byActivityDesc.slice(maxCount)) {
|
|
6185
|
-
countEligibleIds.add(s.id);
|
|
6186
|
-
}
|
|
6187
|
-
}
|
|
6188
|
-
const toDelete = [];
|
|
6189
|
-
for (const s of sessions) {
|
|
6190
|
-
if (protectedIds.has(s.id)) continue;
|
|
6191
|
-
if (ageEligible(s) || countEligibleIds.has(s.id)) {
|
|
6192
|
-
toDelete.push(s.id);
|
|
6193
|
-
}
|
|
6194
|
-
}
|
|
6195
|
-
const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
|
|
6196
|
-
return toDelete.sort(
|
|
6197
|
-
(a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
|
|
6198
|
-
);
|
|
6199
|
-
}
|
|
6200
|
-
var DEFAULT_INTERVAL = "1h";
|
|
6201
|
-
function resolve2(flag, envValue, fallback) {
|
|
6202
|
-
return flag ?? envValue ?? fallback;
|
|
6203
|
-
}
|
|
6204
|
-
function parseMaxCount(input) {
|
|
6205
|
-
const trimmed = input.trim();
|
|
6206
|
-
if (!/^\d+$/.test(trimmed)) {
|
|
6207
|
-
throw new Error(`Invalid max-count "${input}": expected a positive integer.`);
|
|
6208
|
-
}
|
|
6209
|
-
const value = Number(trimmed);
|
|
6210
|
-
if (value <= 0) {
|
|
6211
|
-
throw new Error(`Invalid max-count "${input}": must be greater than 0.`);
|
|
6212
|
-
}
|
|
6213
|
-
return value;
|
|
6214
|
-
}
|
|
6215
|
-
function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
6216
|
-
const warnings = [];
|
|
6217
|
-
const maxAgeRaw = resolve2(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
|
|
6218
|
-
const maxCountRaw = resolve2(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
|
|
6219
|
-
const intervalRaw = resolve2(
|
|
6220
|
-
flags.interval,
|
|
6221
|
-
env.EVIDENT_SESSION_CLEANUP_INTERVAL,
|
|
6222
|
-
DEFAULT_INTERVAL
|
|
6223
|
-
);
|
|
6224
|
-
let maxAgeMs;
|
|
6225
|
-
if (maxAgeRaw !== void 0) {
|
|
6226
|
-
try {
|
|
6227
|
-
maxAgeMs = parseDurationMs(maxAgeRaw);
|
|
6228
|
-
} catch (err) {
|
|
6229
|
-
warnings.push(
|
|
6230
|
-
`Ignoring invalid --session-cleanup-max-age: ${err instanceof Error ? err.message : String(err)}`
|
|
6231
|
-
);
|
|
6232
|
-
}
|
|
6233
|
-
}
|
|
6234
|
-
let maxCount;
|
|
6235
|
-
if (maxCountRaw !== void 0) {
|
|
6236
|
-
try {
|
|
6237
|
-
maxCount = parseMaxCount(maxCountRaw);
|
|
6238
|
-
} catch (err) {
|
|
6239
|
-
warnings.push(
|
|
6240
|
-
`Ignoring invalid --session-cleanup-max-count: ${err instanceof Error ? err.message : String(err)}`
|
|
6241
|
-
);
|
|
6242
|
-
}
|
|
6243
|
-
}
|
|
6244
|
-
let intervalMs;
|
|
6245
|
-
try {
|
|
6246
|
-
intervalMs = parseDurationMs(intervalRaw ?? DEFAULT_INTERVAL);
|
|
6247
|
-
} catch (err) {
|
|
6248
|
-
warnings.push(
|
|
6249
|
-
`Ignoring invalid --session-cleanup-interval, using default ${DEFAULT_INTERVAL}: ${err instanceof Error ? err.message : String(err)}`
|
|
6250
|
-
);
|
|
6251
|
-
intervalMs = parseDurationMs(DEFAULT_INTERVAL);
|
|
6252
|
-
}
|
|
6253
|
-
const enabled = maxAgeMs !== void 0 || maxCount !== void 0;
|
|
6254
|
-
return { enabled, maxAgeMs, maxCount, intervalMs, warnings };
|
|
6255
|
-
}
|
|
6256
|
-
|
|
6257
7273
|
// src/lib/opencode/session-db-size.ts
|
|
6258
7274
|
import { statSync as statSync4 } from "node:fs";
|
|
6259
|
-
import { join as
|
|
7275
|
+
import { join as join10 } from "node:path";
|
|
6260
7276
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
6261
7277
|
function statSessionDbBytes(homeDir) {
|
|
6262
|
-
const dbPath =
|
|
7278
|
+
const dbPath = join10(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
6263
7279
|
try {
|
|
6264
7280
|
return statSync4(dbPath).size;
|
|
6265
7281
|
} catch (err) {
|
|
6266
|
-
const
|
|
6267
|
-
if (!
|
|
7282
|
+
const isMissingFile4 = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
7283
|
+
if (!isMissingFile4) {
|
|
6268
7284
|
console.error(
|
|
6269
7285
|
`[statSessionDbBytes] could not stat ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
6270
7286
|
);
|
|
@@ -6289,12 +7305,12 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
6289
7305
|
// src/lib/opencode/log-tail.ts
|
|
6290
7306
|
import { statSync as statSync5 } from "node:fs";
|
|
6291
7307
|
import { homedir as homedir5 } from "node:os";
|
|
6292
|
-
import { join as
|
|
7308
|
+
import { join as join11 } from "node:path";
|
|
6293
7309
|
import { open as open2, stat } from "node:fs/promises";
|
|
6294
7310
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
6295
7311
|
function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
|
|
6296
|
-
const dataDir = env.XDG_DATA_HOME ||
|
|
6297
|
-
return
|
|
7312
|
+
const dataDir = env.XDG_DATA_HOME || join11(homeDir, ".local", "share");
|
|
7313
|
+
return join11(dataDir, "opencode", "log", "opencode.log");
|
|
6298
7314
|
}
|
|
6299
7315
|
function isEnoent(error2) {
|
|
6300
7316
|
return error2?.code === "ENOENT";
|
|
@@ -6779,8 +7795,8 @@ function connectTunnel(options) {
|
|
|
6779
7795
|
}
|
|
6780
7796
|
forwarder.handleBinaryBodyFrame(frame.sid, frame.payload);
|
|
6781
7797
|
} catch (error2) {
|
|
6782
|
-
const
|
|
6783
|
-
onError?.(`Failed to handle binary message: ${
|
|
7798
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
7799
|
+
onError?.(`Failed to handle binary message: ${errorMessage4}`);
|
|
6784
7800
|
}
|
|
6785
7801
|
return;
|
|
6786
7802
|
}
|
|
@@ -6788,8 +7804,8 @@ function connectTunnel(options) {
|
|
|
6788
7804
|
try {
|
|
6789
7805
|
message = JSON.parse(data.toString());
|
|
6790
7806
|
} catch (error2) {
|
|
6791
|
-
const
|
|
6792
|
-
onError?.(`Failed to handle message: ${
|
|
7807
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
7808
|
+
onError?.(`Failed to handle message: ${errorMessage4}`);
|
|
6793
7809
|
return;
|
|
6794
7810
|
}
|
|
6795
7811
|
if (isStreamFrame(message)) {
|
|
@@ -6961,10 +7977,10 @@ var RunnerConnection = class _RunnerConnection {
|
|
|
6961
7977
|
};
|
|
6962
7978
|
|
|
6963
7979
|
// src/lib/tunnel/ready-marker.ts
|
|
6964
|
-
import { writeFileSync as
|
|
7980
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
6965
7981
|
function writeTunnelReadyMarker(path, agentId) {
|
|
6966
7982
|
try {
|
|
6967
|
-
|
|
7983
|
+
writeFileSync6(path, `${agentId}
|
|
6968
7984
|
`);
|
|
6969
7985
|
return { ok: true };
|
|
6970
7986
|
} catch (error2) {
|
|
@@ -6989,7 +8005,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
6989
8005
|
}
|
|
6990
8006
|
|
|
6991
8007
|
// src/lib/process-liveness.ts
|
|
6992
|
-
import { readFileSync as
|
|
8008
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
6993
8009
|
function isProcessAlive(pid) {
|
|
6994
8010
|
try {
|
|
6995
8011
|
process.kill(pid, 0);
|
|
@@ -7004,7 +8020,7 @@ function isProcessAlive(pid) {
|
|
|
7004
8020
|
}
|
|
7005
8021
|
if (process.platform !== "linux") return true;
|
|
7006
8022
|
try {
|
|
7007
|
-
const status2 =
|
|
8023
|
+
const status2 = readFileSync10(`/proc/${pid}/status`, "utf8");
|
|
7008
8024
|
return !/^State:\s+Z(?:\s|$)/m.test(status2);
|
|
7009
8025
|
} catch (error2) {
|
|
7010
8026
|
console.error(
|
|
@@ -7071,8 +8087,8 @@ function resolveOpenAiUsageReportingMode(flagValue, env) {
|
|
|
7071
8087
|
}
|
|
7072
8088
|
|
|
7073
8089
|
// src/lib/resource-usage-reporting.ts
|
|
7074
|
-
var
|
|
7075
|
-
var
|
|
8090
|
+
var ENABLED_VALUES2 = /* @__PURE__ */ new Set(["on", "true", "1"]);
|
|
8091
|
+
var DISABLED_VALUES2 = /* @__PURE__ */ new Set(["off", "false", "0"]);
|
|
7076
8092
|
function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
7077
8093
|
if (flagValue === false) {
|
|
7078
8094
|
return { enabled: false, warnings: [] };
|
|
@@ -7082,10 +8098,10 @@ function resolveResourceUsageReportingEnabled(flagValue, env) {
|
|
|
7082
8098
|
return { enabled: true, warnings: [] };
|
|
7083
8099
|
}
|
|
7084
8100
|
const normalized = raw.trim().toLowerCase();
|
|
7085
|
-
if (
|
|
8101
|
+
if (DISABLED_VALUES2.has(normalized)) {
|
|
7086
8102
|
return { enabled: false, warnings: [] };
|
|
7087
8103
|
}
|
|
7088
|
-
if (
|
|
8104
|
+
if (ENABLED_VALUES2.has(normalized)) {
|
|
7089
8105
|
return { enabled: true, warnings: [] };
|
|
7090
8106
|
}
|
|
7091
8107
|
return {
|
|
@@ -7363,18 +8379,18 @@ function createSessionStoreHealth(options = {}) {
|
|
|
7363
8379
|
import { homedir as homedir6 } from "node:os";
|
|
7364
8380
|
|
|
7365
8381
|
// src/lib/runner-file-sync.ts
|
|
7366
|
-
import { join as
|
|
8382
|
+
import { join as join13 } from "node:path";
|
|
7367
8383
|
|
|
7368
8384
|
// src/lib/file-push.ts
|
|
7369
8385
|
import { randomUUID } from "node:crypto";
|
|
7370
8386
|
import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
|
|
7371
|
-
import { basename as basename2, dirname as
|
|
8387
|
+
import { basename as basename2, dirname as dirname9, isAbsolute as isAbsolute2, join as join12, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
|
7372
8388
|
var FILE_MODE = 384;
|
|
7373
8389
|
var DIRECTORY_MODE = 448;
|
|
7374
8390
|
async function writePushedFile(request3) {
|
|
7375
|
-
const { requestedPath, content, allowedDirectories, homeDir } = request3;
|
|
8391
|
+
const { requestedPath, content, allowedDirectories, allowedFiles, homeDir } = request3;
|
|
7376
8392
|
const bytes = content.byteLength;
|
|
7377
|
-
if (allowedDirectories.length === 0) {
|
|
8393
|
+
if (allowedDirectories.length === 0 && allowedFiles.length === 0) {
|
|
7378
8394
|
return refuse("file_sync_disabled", "File sync is not enabled on this runner.", {
|
|
7379
8395
|
path: requestedPath,
|
|
7380
8396
|
bytes
|
|
@@ -7399,10 +8415,10 @@ async function writePushedFile(request3) {
|
|
|
7399
8415
|
}
|
|
7400
8416
|
try {
|
|
7401
8417
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
7402
|
-
|
|
8418
|
+
dirname9(candidate)
|
|
7403
8419
|
);
|
|
7404
|
-
const realTarget =
|
|
7405
|
-
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
8420
|
+
const realTarget = join12(existingAncestor, ...missingSegments, basename2(candidate));
|
|
8421
|
+
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget) ?? await findMatchingAllowedFile(allowedFiles, realTarget);
|
|
7406
8422
|
if (allowedDirectory === null) {
|
|
7407
8423
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
7408
8424
|
path: realTarget,
|
|
@@ -7411,8 +8427,8 @@ async function writePushedFile(request3) {
|
|
|
7411
8427
|
}
|
|
7412
8428
|
if (missingSegments.length > 0) {
|
|
7413
8429
|
await createMissingDirectories(existingAncestor, missingSegments);
|
|
7414
|
-
const realParent = await realpath(
|
|
7415
|
-
if (realParent !==
|
|
8430
|
+
const realParent = await realpath(dirname9(realTarget));
|
|
8431
|
+
if (realParent !== dirname9(realTarget) || !contains(allowedDirectory, realTarget)) {
|
|
7416
8432
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
7417
8433
|
path: realTarget,
|
|
7418
8434
|
bytes,
|
|
@@ -7437,7 +8453,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
7437
8453
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
7438
8454
|
return null;
|
|
7439
8455
|
}
|
|
7440
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
8456
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join12(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
7441
8457
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
7442
8458
|
return null;
|
|
7443
8459
|
}
|
|
@@ -7455,7 +8471,7 @@ async function resolveNearestExistingAncestor(directory) {
|
|
|
7455
8471
|
try {
|
|
7456
8472
|
return { existingAncestor: await realpath(current), missingSegments };
|
|
7457
8473
|
} catch (err) {
|
|
7458
|
-
const parent =
|
|
8474
|
+
const parent = dirname9(current);
|
|
7459
8475
|
if (err.code !== "ENOENT" || parent === current) {
|
|
7460
8476
|
throw err;
|
|
7461
8477
|
}
|
|
@@ -7477,6 +8493,22 @@ async function findContainingAllowedDirectory(allowedDirectories, realTarget) {
|
|
|
7477
8493
|
}
|
|
7478
8494
|
return null;
|
|
7479
8495
|
}
|
|
8496
|
+
async function findMatchingAllowedFile(allowedFiles, realTarget) {
|
|
8497
|
+
for (const entry of allowedFiles) {
|
|
8498
|
+
if (!isAbsolute2(entry)) {
|
|
8499
|
+
log("warn", "file_push_allowed_directory_skipped", {
|
|
8500
|
+
directory: entry,
|
|
8501
|
+
reason: "not_absolute_file"
|
|
8502
|
+
});
|
|
8503
|
+
continue;
|
|
8504
|
+
}
|
|
8505
|
+
const realParent = await realpathCreatingIfMissing(dirname9(entry));
|
|
8506
|
+
if (realParent !== null && join12(realParent, basename2(entry)) === realTarget) {
|
|
8507
|
+
return realParent;
|
|
8508
|
+
}
|
|
8509
|
+
}
|
|
8510
|
+
return null;
|
|
8511
|
+
}
|
|
7480
8512
|
async function realpathCreatingIfMissing(directory) {
|
|
7481
8513
|
try {
|
|
7482
8514
|
return await realpath(directory);
|
|
@@ -7510,13 +8542,13 @@ function contains(realDirectory, realTarget) {
|
|
|
7510
8542
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
7511
8543
|
let current = existingAncestor;
|
|
7512
8544
|
for (const segment of missingSegments) {
|
|
7513
|
-
current =
|
|
8545
|
+
current = join12(current, segment);
|
|
7514
8546
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
7515
8547
|
await chmod(current, DIRECTORY_MODE);
|
|
7516
8548
|
}
|
|
7517
8549
|
}
|
|
7518
8550
|
async function writeAtomically(realTarget, content) {
|
|
7519
|
-
const temporaryPath =
|
|
8551
|
+
const temporaryPath = join12(dirname9(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
7520
8552
|
let handle;
|
|
7521
8553
|
try {
|
|
7522
8554
|
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
@@ -7646,16 +8678,16 @@ var NOT_APPLIED = {
|
|
|
7646
8678
|
opencodeAuthApplied: false
|
|
7647
8679
|
};
|
|
7648
8680
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
7649
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
7650
|
-
return expanded ===
|
|
8681
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
8682
|
+
return expanded === join13(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
7651
8683
|
}
|
|
7652
8684
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
7653
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
7654
|
-
return expanded ===
|
|
8685
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join13(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
8686
|
+
return expanded === join13(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
7655
8687
|
}
|
|
7656
8688
|
async function applyOne(options, file) {
|
|
7657
8689
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
7658
|
-
if (options.allowedDirectories.length === 0) {
|
|
8690
|
+
if (options.allowedDirectories.length === 0 && options.allowedFiles.length === 0) {
|
|
7659
8691
|
options.log({
|
|
7660
8692
|
level: "warn",
|
|
7661
8693
|
message: `Runner file ${label} rejected: file sync is not enabled on this runner (start it with --enable-file-sync-to)`
|
|
@@ -7682,6 +8714,7 @@ async function applyOne(options, file) {
|
|
|
7682
8714
|
requestedPath: file.path,
|
|
7683
8715
|
content: download.content,
|
|
7684
8716
|
allowedDirectories: options.allowedDirectories,
|
|
8717
|
+
allowedFiles: options.allowedFiles,
|
|
7685
8718
|
homeDir: options.homeDir
|
|
7686
8719
|
});
|
|
7687
8720
|
} catch (err) {
|
|
@@ -7789,7 +8822,7 @@ function describe(err) {
|
|
|
7789
8822
|
}
|
|
7790
8823
|
|
|
7791
8824
|
// src/lib/vault-file-sync.ts
|
|
7792
|
-
import { createHash } from "node:crypto";
|
|
8825
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
7793
8826
|
async function syncVaultFiles(options) {
|
|
7794
8827
|
const files = await listVaultFiles(options);
|
|
7795
8828
|
if (files === null) return { applied: 0 };
|
|
@@ -7885,7 +8918,7 @@ async function reconcileVaultFile(options, file) {
|
|
|
7885
8918
|
}
|
|
7886
8919
|
const content = await downloadVaultFile(options, file);
|
|
7887
8920
|
if (content === null) return false;
|
|
7888
|
-
const actualSha256 =
|
|
8921
|
+
const actualSha256 = createHash2("sha256").update(content).digest("hex");
|
|
7889
8922
|
if (actualSha256 !== file.content_sha256) {
|
|
7890
8923
|
options.log({
|
|
7891
8924
|
level: "warn",
|
|
@@ -7933,6 +8966,7 @@ async function applyVaultFile(options, file, content) {
|
|
|
7933
8966
|
requestedPath: file.destination_path,
|
|
7934
8967
|
content,
|
|
7935
8968
|
allowedDirectories: options.allowedDirectories,
|
|
8969
|
+
allowedFiles: options.allowedFiles,
|
|
7936
8970
|
homeDir: options.homeDir
|
|
7937
8971
|
});
|
|
7938
8972
|
if (!outcome.ok) {
|
|
@@ -8001,7 +9035,7 @@ function messageIdOf(m) {
|
|
|
8001
9035
|
return typeof infoId === "string" ? infoId : void 0;
|
|
8002
9036
|
}
|
|
8003
9037
|
function composeAbandonmentNote(finish, replyShape, outcome) {
|
|
8004
|
-
const base = `OpenCode ended this turn after a tool call (finish="${finish ?? "(unknown)"}") ` + (outcome === "without-answer" ? "without writing an answer, and confirmed it is no longer working on this conversation. Evident is reporting the run as failed so the failure and its reason are visible in failed runs." : "after writing only part of an answer, and confirmed it is no longer working on this conversation. Evident delivered what was written and is reporting the run as finished.");
|
|
9038
|
+
const base = outcome === "midflight-without-answer" || outcome === "midflight-with-answer" ? "OpenCode stopped this turn without ever finishing, and confirmed the session is no longer running. " + (outcome === "midflight-without-answer" ? "Evident is reporting the run as failed so the failure and its reason are visible in failed runs." : "Evident delivered what was written and is reporting the run as finished.") : `OpenCode ended this turn after a tool call (finish="${finish ?? "(unknown)"}") ` + (outcome === "without-answer" ? "without writing an answer, and confirmed it is no longer working on this conversation. Evident is reporting the run as failed so the failure and its reason are visible in failed runs." : "after writing only part of an answer, and confirmed it is no longer working on this conversation. Evident delivered what was written and is reporting the run as finished.");
|
|
8005
9039
|
if (replyShape === null) return base;
|
|
8006
9040
|
return `${base} ${replyShape.lastToolName === null ? "No tool call was recorded" : `The last tool call was "${replyShape.lastToolName}" with status "${replyShape.lastToolStatus ?? "(unknown)"}"`}; the reply's final part types were [${replyShape.partTypes.join(", ")}]${replyShape.droppedPartCount > 0 ? ` (${replyShape.droppedPartCount} earlier part types omitted)` : ""}.`;
|
|
8007
9041
|
}
|
|
@@ -8038,6 +9072,9 @@ var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
|
|
|
8038
9072
|
var ABSOLUTE_MAX_PROCESSING_MS = 6 * 60 * 60 * 1e3;
|
|
8039
9073
|
var B2_ABANDONMENT_MIN_PINNED_MS = 3 * 6e4;
|
|
8040
9074
|
var AMBIGUOUS_FINISH_MAX_PINNED_MS = 3 * 6e4;
|
|
9075
|
+
var MIDFLIGHT_ORPHAN_MIN_PINNED_MS = 3 * 6e4;
|
|
9076
|
+
var MIDFLIGHT_ORPHAN_RECHECK_MS = 6e4;
|
|
9077
|
+
var MIDFLIGHT_ORPHAN_MIN_NOT_ONGOING_STREAK = 2;
|
|
8041
9078
|
var B2_ABANDONMENT_RECHECK_MS = HEARTBEAT_MS;
|
|
8042
9079
|
var POLL_MISS_GRACE_MS = HEARTBEAT_MS;
|
|
8043
9080
|
var WATCHER_STALL_MS = 3 * POLL_MISS_GRACE_MS;
|
|
@@ -8082,6 +9119,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8082
9119
|
conversationFilter;
|
|
8083
9120
|
retry;
|
|
8084
9121
|
log;
|
|
9122
|
+
recordBinding;
|
|
8085
9123
|
fetchImpl;
|
|
8086
9124
|
sleep;
|
|
8087
9125
|
pausedPollIntervalMs;
|
|
@@ -8089,6 +9127,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8089
9127
|
stuckQueuedMs;
|
|
8090
9128
|
now;
|
|
8091
9129
|
fileSyncDirectories;
|
|
9130
|
+
fileSyncExactFiles;
|
|
8092
9131
|
homeDir;
|
|
8093
9132
|
maxActiveSessions;
|
|
8094
9133
|
watcherStallMs;
|
|
@@ -8443,6 +9482,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8443
9482
|
this.retry = { ...DEFAULT_RETRY_POLICY, ...config.retry };
|
|
8444
9483
|
this.log = config.log ?? (() => {
|
|
8445
9484
|
});
|
|
9485
|
+
this.recordBinding = config.recordBinding;
|
|
8446
9486
|
this.fetchImpl = withRequestTimeout(
|
|
8447
9487
|
config.fetchImpl ?? fetch,
|
|
8448
9488
|
config.requestTimeoutMs ?? REQUEST_TIMEOUT_MS
|
|
@@ -8458,6 +9498,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8458
9498
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
8459
9499
|
this.now = config.now ?? (() => Date.now());
|
|
8460
9500
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
9501
|
+
this.fileSyncExactFiles = config.fileSyncExactFiles ?? [];
|
|
8461
9502
|
this.homeDir = config.homeDir ?? homedir6();
|
|
8462
9503
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
8463
9504
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
@@ -8555,6 +9596,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8555
9596
|
getAuthHeader: this.getAuthHeader,
|
|
8556
9597
|
fetchImpl: this.fetchImpl,
|
|
8557
9598
|
allowedDirectories: this.fileSyncDirectories,
|
|
9599
|
+
allowedFiles: this.fileSyncExactFiles,
|
|
8558
9600
|
homeDir: this.homeDir,
|
|
8559
9601
|
ackFailures: this.fileAckFailures,
|
|
8560
9602
|
log: this.log
|
|
@@ -8565,6 +9607,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8565
9607
|
getAuthHeader: this.getAuthHeader,
|
|
8566
9608
|
fetchImpl: this.fetchImpl,
|
|
8567
9609
|
allowedDirectories: this.fileSyncDirectories,
|
|
9610
|
+
allowedFiles: this.fileSyncExactFiles,
|
|
8568
9611
|
homeDir: this.homeDir,
|
|
8569
9612
|
outcomes: this.vaultFileOutcomes,
|
|
8570
9613
|
log: this.log
|
|
@@ -8627,6 +9670,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8627
9670
|
let cappedSkips = 0;
|
|
8628
9671
|
for (const conv of conversations) {
|
|
8629
9672
|
if (this.stopped) break;
|
|
9673
|
+
if (conv.opencode_session_id) {
|
|
9674
|
+
this.recordSessionBinding(conv.id, conv.opencode_session_id);
|
|
9675
|
+
}
|
|
8630
9676
|
if (this.maxActiveSessions !== void 0) {
|
|
8631
9677
|
const activeSessionIds = this.activeSessionIdsForCap();
|
|
8632
9678
|
const resolvedSessionId = this.sessions.get(conv.id) ?? conv.opencode_session_id;
|
|
@@ -8709,12 +9755,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8709
9755
|
this.sessions.delete(conversationId);
|
|
8710
9756
|
this.supersede(conversationId, sessionId);
|
|
8711
9757
|
const dispatchDescription = source === "readopt" ? "re-dispatched message" : "message";
|
|
8712
|
-
const
|
|
8713
|
-
this.untrackableAck.set(messageId,
|
|
9758
|
+
const errorMessage4 = `OpenCode accepted this ${dispatchDescription} once, but its assigned id was never confirmed after ${attempts} attempts to match it up \u2014 the message was deliberately not sent again, so no duplicate turn was created; session ${sessionId.slice(0, 8)} was abandoned; a fresh one is used for further messages.`;
|
|
9759
|
+
this.untrackableAck.set(messageId, errorMessage4);
|
|
8714
9760
|
this.untrackableAckSessionIds.set(messageId, null);
|
|
8715
9761
|
this.log({
|
|
8716
9762
|
level: "error",
|
|
8717
|
-
message:
|
|
9763
|
+
message: errorMessage4,
|
|
8718
9764
|
conversation_id: conversationId,
|
|
8719
9765
|
message_id: messageId
|
|
8720
9766
|
});
|
|
@@ -8722,7 +9768,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8722
9768
|
conversationId,
|
|
8723
9769
|
messageId,
|
|
8724
9770
|
null,
|
|
8725
|
-
|
|
9771
|
+
errorMessage4
|
|
8726
9772
|
);
|
|
8727
9773
|
if (reportError !== null) {
|
|
8728
9774
|
this.log({
|
|
@@ -8903,12 +9949,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8903
9949
|
conversation_id: conv.id,
|
|
8904
9950
|
message_id: message.id
|
|
8905
9951
|
});
|
|
8906
|
-
const
|
|
9952
|
+
const errorMessage4 = this.untrackableAck.get(message.id);
|
|
8907
9953
|
const reportError = await this.reportUntrackableAck(
|
|
8908
9954
|
conv.id,
|
|
8909
9955
|
message.id,
|
|
8910
9956
|
null,
|
|
8911
|
-
|
|
9957
|
+
errorMessage4
|
|
8912
9958
|
);
|
|
8913
9959
|
if (reportError === null) {
|
|
8914
9960
|
continue;
|
|
@@ -8996,7 +10042,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8996
10042
|
const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
|
|
8997
10043
|
const providerId = hasProviderAndModel ? dispatchedModel.slice(0, separator) : null;
|
|
8998
10044
|
const modelId = hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null;
|
|
8999
|
-
const
|
|
10045
|
+
const errorMessage4 = `Provider namespace not loaded on this runner: ${message.opencode_model}. Refused before dispatch; no turn was started.`;
|
|
9000
10046
|
this.log({
|
|
9001
10047
|
level: "error",
|
|
9002
10048
|
message: `Refusing message ${message.id.slice(0, 8)} in conversation ${conv.id.slice(0, 8)}: provider namespace ${providerId ?? "(unknown)"} is not loaded; no turn was started`,
|
|
@@ -9004,7 +10050,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9004
10050
|
message_id: message.id
|
|
9005
10051
|
});
|
|
9006
10052
|
try {
|
|
9007
|
-
await this.markFailed(conv.id, message.id, sessionId,
|
|
10053
|
+
await this.markFailed(conv.id, message.id, sessionId, errorMessage4, void 0, {
|
|
9008
10054
|
classification: {
|
|
9009
10055
|
kind: "model_auth",
|
|
9010
10056
|
providerId,
|
|
@@ -9076,12 +10122,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9076
10122
|
} catch (err) {
|
|
9077
10123
|
if (err instanceof ChannelAuthError) throw err;
|
|
9078
10124
|
if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
|
|
9079
|
-
const
|
|
9080
|
-
this.untrackableAck.set(message.id,
|
|
10125
|
+
const errorMessage5 = err instanceof Error ? err.message : String(err);
|
|
10126
|
+
this.untrackableAck.set(message.id, errorMessage5);
|
|
9081
10127
|
this.untrackableAckSessionIds.set(message.id, null);
|
|
9082
10128
|
this.log({
|
|
9083
10129
|
level: "error",
|
|
9084
|
-
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${
|
|
10130
|
+
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage5}`,
|
|
9085
10131
|
conversation_id: conv.id,
|
|
9086
10132
|
message_id: message.id
|
|
9087
10133
|
});
|
|
@@ -9089,7 +10135,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9089
10135
|
conv.id,
|
|
9090
10136
|
message.id,
|
|
9091
10137
|
null,
|
|
9092
|
-
|
|
10138
|
+
errorMessage5
|
|
9093
10139
|
);
|
|
9094
10140
|
if (markError !== null) {
|
|
9095
10141
|
this.log({
|
|
@@ -9124,7 +10170,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9124
10170
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
9125
10171
|
break;
|
|
9126
10172
|
}
|
|
9127
|
-
const
|
|
10173
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
9128
10174
|
this.sessions.delete(conv.id);
|
|
9129
10175
|
this.supersede(conv.id, sessionId);
|
|
9130
10176
|
this.log({
|
|
@@ -9133,7 +10179,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9133
10179
|
conversation_id: conv.id,
|
|
9134
10180
|
message_id: message.id
|
|
9135
10181
|
});
|
|
9136
|
-
await this.markFailed(conv.id, message.id, null,
|
|
10182
|
+
await this.markFailed(conv.id, message.id, null, errorMessage4).catch((markErr) => {
|
|
9137
10183
|
this.log({
|
|
9138
10184
|
level: "warn",
|
|
9139
10185
|
message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -9144,7 +10190,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9144
10190
|
});
|
|
9145
10191
|
this.log({
|
|
9146
10192
|
level: "error",
|
|
9147
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
10193
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage4}`,
|
|
9148
10194
|
conversation_id: conv.id,
|
|
9149
10195
|
message_id: message.id
|
|
9150
10196
|
});
|
|
@@ -9931,6 +10977,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9931
10977
|
return { sessionId: await this.createAndBindSession(conv.id), created: true };
|
|
9932
10978
|
}
|
|
9933
10979
|
this.sessions.set(conv.id, bound);
|
|
10980
|
+
this.recordSessionBinding(conv.id, bound);
|
|
9934
10981
|
return { sessionId: bound, created: false };
|
|
9935
10982
|
}
|
|
9936
10983
|
return { sessionId: await this.createAndBindSession(conv.id), created: true };
|
|
@@ -9944,6 +10991,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9944
10991
|
const directory = await this.resolveOpenCodeDirectory();
|
|
9945
10992
|
const sessionId = await this.createOpenCodeSession(directory);
|
|
9946
10993
|
this.sessions.set(conversationId, sessionId);
|
|
10994
|
+
this.recordSessionBinding(conversationId, sessionId);
|
|
9947
10995
|
await this.persistSession(conversationId, sessionId).catch((err) => {
|
|
9948
10996
|
this.log({
|
|
9949
10997
|
level: "warn",
|
|
@@ -9953,6 +11001,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9953
11001
|
});
|
|
9954
11002
|
return sessionId;
|
|
9955
11003
|
}
|
|
11004
|
+
recordSessionBinding(conversationId, sessionId) {
|
|
11005
|
+
if (!this.recordBinding) return;
|
|
11006
|
+
try {
|
|
11007
|
+
this.recordBinding(conversationId, sessionId);
|
|
11008
|
+
} catch (error2) {
|
|
11009
|
+
this.log({
|
|
11010
|
+
level: "warn",
|
|
11011
|
+
message: `Recording OpenCode session ${sessionId.slice(0, 8)} for conversation ${conversationId.slice(0, 8)} failed (best-effort): ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
11012
|
+
conversation_id: conversationId
|
|
11013
|
+
});
|
|
11014
|
+
}
|
|
11015
|
+
}
|
|
9956
11016
|
/**
|
|
9957
11017
|
* Lazily resolve (and cache) opencode's root directory via the selected client's
|
|
9958
11018
|
* location lookup.
|
|
@@ -10150,6 +11210,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10150
11210
|
b2AbandonedWithoutAnswer: false,
|
|
10151
11211
|
ambiguousPinnedSinceMs: 0,
|
|
10152
11212
|
ambiguousResolved: false,
|
|
11213
|
+
midFlightPinnedSinceMs: 0,
|
|
11214
|
+
midFlightNotOngoingStreak: 0,
|
|
11215
|
+
midFlightLastStatusCheckMs: 0,
|
|
11216
|
+
midFlightOrphanSignalled: false,
|
|
11217
|
+
midFlightOrphanWithoutAnswer: false,
|
|
11218
|
+
midFlightHeldLogged: false,
|
|
10153
11219
|
sessionStoreFailurePending: false,
|
|
10154
11220
|
neverStartedResolved: false
|
|
10155
11221
|
});
|
|
@@ -10289,6 +11355,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10289
11355
|
b2AbandonedWithoutAnswer: false,
|
|
10290
11356
|
ambiguousPinnedSinceMs: 0,
|
|
10291
11357
|
ambiguousResolved: false,
|
|
11358
|
+
midFlightPinnedSinceMs: 0,
|
|
11359
|
+
midFlightNotOngoingStreak: 0,
|
|
11360
|
+
midFlightLastStatusCheckMs: 0,
|
|
11361
|
+
midFlightOrphanSignalled: false,
|
|
11362
|
+
midFlightOrphanWithoutAnswer: false,
|
|
11363
|
+
midFlightHeldLogged: false,
|
|
10292
11364
|
sessionStoreFailurePending: false,
|
|
10293
11365
|
neverStartedResolved: false
|
|
10294
11366
|
});
|
|
@@ -11060,7 +12132,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
11060
12132
|
watcher,
|
|
11061
12133
|
inFlight,
|
|
11062
12134
|
messages,
|
|
11063
|
-
|
|
12135
|
+
composeAbandonmentNote(
|
|
12136
|
+
finish,
|
|
12137
|
+
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
12138
|
+
"without-answer"
|
|
12139
|
+
)
|
|
11064
12140
|
);
|
|
11065
12141
|
} else {
|
|
11066
12142
|
await this.settleMessageDone(
|
|
@@ -11119,7 +12195,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
11119
12195
|
watcher,
|
|
11120
12196
|
inFlight,
|
|
11121
12197
|
messages,
|
|
11122
|
-
|
|
12198
|
+
composeAbandonmentNote(
|
|
12199
|
+
finish,
|
|
12200
|
+
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
12201
|
+
"without-answer"
|
|
12202
|
+
)
|
|
11123
12203
|
);
|
|
11124
12204
|
return;
|
|
11125
12205
|
}
|
|
@@ -11196,21 +12276,121 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
11196
12276
|
run_state: "failed",
|
|
11197
12277
|
opencode_message_id: inFlight.opencodeMessageId
|
|
11198
12278
|
});
|
|
11199
|
-
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
12279
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
12280
|
+
return;
|
|
12281
|
+
}
|
|
12282
|
+
inFlight.ambiguousResolved = true;
|
|
12283
|
+
this.log({
|
|
12284
|
+
level: "warn",
|
|
12285
|
+
message: `Message ${id.slice(0, 8)} ambiguous-finish-pinned for ${Math.round(pinnedForMs / 1e3)}s \u2014 resolved (${ongoing === false ? "session confirmed not-ongoing" : "pin exceeded the no-hang cap"}) \u2014 settling done`,
|
|
12286
|
+
conversation_id: conv.id,
|
|
12287
|
+
message_id: id
|
|
12288
|
+
});
|
|
12289
|
+
void this.postSignal(conv.id, id, "ambiguous_finish_resolved", {
|
|
12290
|
+
watched_for_ms: pinnedForMs
|
|
12291
|
+
});
|
|
12292
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
12293
|
+
return;
|
|
12294
|
+
}
|
|
12295
|
+
}
|
|
12296
|
+
const midFlightPinnedNow = activelyRunning && isMidFlightPinnedRunning(messages, inFlight.opencodeMessageId);
|
|
12297
|
+
if (!midFlightPinnedNow) {
|
|
12298
|
+
if (snapshotReadable) {
|
|
12299
|
+
inFlight.midFlightPinnedSinceMs = 0;
|
|
12300
|
+
inFlight.midFlightNotOngoingStreak = 0;
|
|
12301
|
+
inFlight.midFlightLastStatusCheckMs = 0;
|
|
12302
|
+
inFlight.midFlightOrphanSignalled = false;
|
|
12303
|
+
inFlight.midFlightOrphanWithoutAnswer = false;
|
|
12304
|
+
inFlight.midFlightHeldLogged = false;
|
|
12305
|
+
}
|
|
12306
|
+
} else {
|
|
12307
|
+
if (inFlight.midFlightOrphanSignalled) {
|
|
12308
|
+
const note = composeAbandonmentNote(
|
|
12309
|
+
void 0,
|
|
12310
|
+
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
12311
|
+
inFlight.midFlightOrphanWithoutAnswer ? "midflight-without-answer" : "midflight-with-answer"
|
|
12312
|
+
);
|
|
12313
|
+
if (inFlight.midFlightOrphanWithoutAnswer) {
|
|
12314
|
+
await this.settleMessageAbandonedWithoutAnswer(
|
|
12315
|
+
sessionId,
|
|
12316
|
+
watcher,
|
|
12317
|
+
inFlight,
|
|
12318
|
+
messages,
|
|
12319
|
+
note
|
|
12320
|
+
);
|
|
12321
|
+
} else {
|
|
12322
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages, note);
|
|
12323
|
+
}
|
|
12324
|
+
return;
|
|
12325
|
+
}
|
|
12326
|
+
if (inFlight.midFlightPinnedSinceMs === 0) {
|
|
12327
|
+
inFlight.midFlightPinnedSinceMs = this.now();
|
|
12328
|
+
}
|
|
12329
|
+
const pinnedForMs = this.now() - inFlight.midFlightPinnedSinceMs;
|
|
12330
|
+
if (pinnedForMs >= MIDFLIGHT_ORPHAN_MIN_PINNED_MS && this.now() - inFlight.midFlightLastStatusCheckMs >= MIDFLIGHT_ORPHAN_RECHECK_MS) {
|
|
12331
|
+
inFlight.midFlightLastStatusCheckMs = this.now();
|
|
12332
|
+
const rootOngoing = await this.isSessionOngoing(sessionId);
|
|
12333
|
+
inFlight.midFlightNotOngoingStreak = rootOngoing === false ? inFlight.midFlightNotOngoingStreak + 1 : 0;
|
|
12334
|
+
if (isMidFlightOrphanConfirmed({
|
|
12335
|
+
pinnedForMs,
|
|
12336
|
+
minPinnedMs: MIDFLIGHT_ORPHAN_MIN_PINNED_MS,
|
|
12337
|
+
notOngoingStreak: inFlight.midFlightNotOngoingStreak,
|
|
12338
|
+
minNotOngoingStreak: MIDFLIGHT_ORPHAN_MIN_NOT_ONGOING_STREAK
|
|
12339
|
+
})) {
|
|
12340
|
+
if (this.sessionStoreHealth?.isUnwritable()) {
|
|
12341
|
+
inFlight.sessionStoreFailurePending = true;
|
|
12342
|
+
void this.postSignal(conv.id, id, "midflight_orphan_resolved", {
|
|
12343
|
+
watched_for_ms: pinnedForMs,
|
|
12344
|
+
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
12345
|
+
opencode_message_id: inFlight.opencodeMessageId,
|
|
12346
|
+
run_state: "failed"
|
|
12347
|
+
});
|
|
12348
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
12349
|
+
return;
|
|
12350
|
+
}
|
|
12351
|
+
inFlight.midFlightOrphanSignalled = true;
|
|
12352
|
+
const noteOutcome = extractReplyTextForMessage(messages, inFlight.opencodeMessageId) === null ? "midflight-without-answer" : "midflight-with-answer";
|
|
12353
|
+
inFlight.midFlightOrphanWithoutAnswer = noteOutcome === "midflight-without-answer";
|
|
12354
|
+
void this.postSignal(conv.id, id, "midflight_orphan_resolved", {
|
|
12355
|
+
watched_for_ms: pinnedForMs,
|
|
12356
|
+
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
12357
|
+
opencode_message_id: inFlight.opencodeMessageId,
|
|
12358
|
+
...inFlight.midFlightOrphanWithoutAnswer ? { run_state: "failed" } : {}
|
|
12359
|
+
});
|
|
12360
|
+
const note = composeAbandonmentNote(
|
|
12361
|
+
void 0,
|
|
12362
|
+
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
12363
|
+
noteOutcome
|
|
12364
|
+
);
|
|
12365
|
+
this.log({
|
|
12366
|
+
level: "warn",
|
|
12367
|
+
message: `Message ${id.slice(0, 8)} was pinned mid-flight for ${Math.round(pinnedForMs / 1e3)}s with root_ongoing=${rootOngoing} \u2014 resolving the orphaned turn`,
|
|
12368
|
+
conversation_id: conv.id,
|
|
12369
|
+
message_id: id
|
|
12370
|
+
});
|
|
12371
|
+
if (inFlight.midFlightOrphanWithoutAnswer) {
|
|
12372
|
+
await this.settleMessageAbandonedWithoutAnswer(
|
|
12373
|
+
sessionId,
|
|
12374
|
+
watcher,
|
|
12375
|
+
inFlight,
|
|
12376
|
+
messages,
|
|
12377
|
+
note
|
|
12378
|
+
);
|
|
12379
|
+
} else {
|
|
12380
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages, note);
|
|
12381
|
+
}
|
|
11200
12382
|
return;
|
|
11201
12383
|
}
|
|
11202
|
-
inFlight.
|
|
11203
|
-
|
|
11204
|
-
|
|
11205
|
-
|
|
11206
|
-
|
|
11207
|
-
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
|
|
11211
|
-
}
|
|
11212
|
-
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
11213
|
-
return;
|
|
12384
|
+
if (rootOngoing === true && !inFlight.midFlightHeldLogged) {
|
|
12385
|
+
inFlight.midFlightHeldLogged = true;
|
|
12386
|
+
const replyShape = describeAbandonedReplyShape(messages, inFlight.opencodeMessageId);
|
|
12387
|
+
this.log({
|
|
12388
|
+
level: "warn",
|
|
12389
|
+
message: `Message ${id.slice(0, 8)} was pinned mid-flight for ${Math.round(pinnedForMs / 1e3)}s with root_ongoing=true \u2014 holding; last tool ${replyShape?.lastToolName ?? "(unknown)"} status ${replyShape?.lastToolStatus ?? "(unknown)"}`,
|
|
12390
|
+
conversation_id: conv.id,
|
|
12391
|
+
message_id: id
|
|
12392
|
+
});
|
|
12393
|
+
}
|
|
11214
12394
|
}
|
|
11215
12395
|
}
|
|
11216
12396
|
if (watcher.inFlight.has(inFlight.evidentMessageId) && activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
|
|
@@ -11412,13 +12592,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
11412
12592
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
11413
12593
|
}
|
|
11414
12594
|
/** Report a confirmed b2 abandonment that produced no answer text. */
|
|
11415
|
-
async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages,
|
|
12595
|
+
async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages, error2) {
|
|
11416
12596
|
const conv = watcher.conv;
|
|
11417
|
-
const error2 = composeAbandonmentNote(
|
|
11418
|
-
finish,
|
|
11419
|
-
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
11420
|
-
"without-answer"
|
|
11421
|
-
);
|
|
11422
12597
|
this.anchorDeliveryDeadline(inFlight);
|
|
11423
12598
|
this.log({
|
|
11424
12599
|
level: "warn",
|
|
@@ -11938,12 +13113,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
11938
13113
|
conversation_id: row.conversation_id,
|
|
11939
13114
|
message_id: row.id
|
|
11940
13115
|
});
|
|
11941
|
-
const
|
|
13116
|
+
const errorMessage4 = this.untrackableAck.get(row.id);
|
|
11942
13117
|
const reportError = await this.reportUntrackableAck(
|
|
11943
13118
|
row.conversation_id,
|
|
11944
13119
|
row.id,
|
|
11945
13120
|
this.untrackableAckSessionIds.get(row.id) ?? null,
|
|
11946
|
-
|
|
13121
|
+
errorMessage4
|
|
11947
13122
|
);
|
|
11948
13123
|
if (reportError !== null) {
|
|
11949
13124
|
this.log({
|
|
@@ -12256,14 +13431,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12256
13431
|
this.awaitingReadopt.delete(row.id);
|
|
12257
13432
|
if (err instanceof ChannelAuthError) throw err;
|
|
12258
13433
|
if (this.isV2) {
|
|
12259
|
-
const
|
|
13434
|
+
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
12260
13435
|
const invalidPromptAck = err instanceof OpenCodeV2PromptAckError;
|
|
12261
13436
|
if (!invalidPromptAck) {
|
|
12262
13437
|
const exists = await this.sessionExists(sessionId);
|
|
12263
13438
|
if (exists === false) {
|
|
12264
13439
|
this.log({
|
|
12265
13440
|
level: "warn",
|
|
12266
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch hit a session (${sessionId.slice(0, 8)}) that was deleted mid-dispatch \u2014 deferring to the next drain: ${
|
|
13441
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch hit a session (${sessionId.slice(0, 8)}) that was deleted mid-dispatch \u2014 deferring to the next drain: ${errorMessage4}`,
|
|
12267
13442
|
conversation_id: row.conversation_id,
|
|
12268
13443
|
message_id: row.id
|
|
12269
13444
|
});
|
|
@@ -12273,7 +13448,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12273
13448
|
if (exists === null) {
|
|
12274
13449
|
this.log({
|
|
12275
13450
|
level: "warn",
|
|
12276
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch failed and session (${sessionId.slice(0, 8)}) existence could not be confirmed \u2014 deferring to the next drain: ${
|
|
13451
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} dispatch failed and session (${sessionId.slice(0, 8)}) existence could not be confirmed \u2014 deferring to the next drain: ${errorMessage4}`,
|
|
12277
13452
|
conversation_id: row.conversation_id,
|
|
12278
13453
|
message_id: row.id
|
|
12279
13454
|
});
|
|
@@ -12281,12 +13456,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12281
13456
|
return;
|
|
12282
13457
|
}
|
|
12283
13458
|
} else {
|
|
12284
|
-
this.untrackableAck.set(row.id,
|
|
13459
|
+
this.untrackableAck.set(row.id, errorMessage4);
|
|
12285
13460
|
this.untrackableAckSessionIds.set(row.id, sessionId);
|
|
12286
13461
|
}
|
|
12287
13462
|
this.log({
|
|
12288
13463
|
level: "error",
|
|
12289
|
-
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${
|
|
13464
|
+
message: `V2 re-adopt dispatch for message ${row.id.slice(0, 8)} failed: ${errorMessage4}`,
|
|
12290
13465
|
conversation_id: row.conversation_id,
|
|
12291
13466
|
message_id: row.id
|
|
12292
13467
|
});
|
|
@@ -12295,7 +13470,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12295
13470
|
row.conversation_id,
|
|
12296
13471
|
row.id,
|
|
12297
13472
|
sessionId,
|
|
12298
|
-
|
|
13473
|
+
errorMessage4
|
|
12299
13474
|
);
|
|
12300
13475
|
if (markError !== null) {
|
|
12301
13476
|
this.log({
|
|
@@ -12306,7 +13481,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
12306
13481
|
});
|
|
12307
13482
|
}
|
|
12308
13483
|
} else {
|
|
12309
|
-
await this.markFailed(row.conversation_id, row.id, sessionId,
|
|
13484
|
+
await this.markFailed(row.conversation_id, row.id, sessionId, errorMessage4).catch(
|
|
12310
13485
|
(markErr) => {
|
|
12311
13486
|
this.log({
|
|
12312
13487
|
level: "warn",
|
|
@@ -13290,9 +14465,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
13290
14465
|
* trackable. Callers use the returned error to keep their path-specific logging
|
|
13291
14466
|
* while the same report payload can be retried on a later drain.
|
|
13292
14467
|
*/
|
|
13293
|
-
async reportUntrackableAck(conversationId, messageId, sessionId,
|
|
14468
|
+
async reportUntrackableAck(conversationId, messageId, sessionId, errorMessage4) {
|
|
13294
14469
|
try {
|
|
13295
|
-
await this.markFailed(conversationId, messageId, sessionId,
|
|
14470
|
+
await this.markFailed(conversationId, messageId, sessionId, errorMessage4);
|
|
13296
14471
|
return null;
|
|
13297
14472
|
} catch (err) {
|
|
13298
14473
|
this.signalUntrackableAck(conversationId, messageId);
|
|
@@ -13804,7 +14979,7 @@ import ora3 from "ora";
|
|
|
13804
14979
|
import { select as select3 } from "@inquirer/prompts";
|
|
13805
14980
|
function unknownPasswordError2(port) {
|
|
13806
14981
|
return new Error(
|
|
13807
|
-
`
|
|
14982
|
+
`An assistant on port ${port} requires credentials this runner does not have. Released OpenCode V2 generates a server password even when none is configured. This CLI has no option to supply the password for an already-running server, so it will not join one you started. Free port ${port} so Evident can start its own, or pass --port to choose another.`
|
|
13808
14983
|
);
|
|
13809
14984
|
}
|
|
13810
14985
|
var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
|
|
@@ -13822,9 +14997,9 @@ async function ensureOpenCode2Running(ctx) {
|
|
|
13822
14997
|
password: null
|
|
13823
14998
|
};
|
|
13824
14999
|
}
|
|
13825
|
-
if (!isOpenCode2Installed()) {
|
|
15000
|
+
if (!ctx.command && !isOpenCode2Installed()) {
|
|
13826
15001
|
throw new Error(
|
|
13827
|
-
|
|
15002
|
+
"OpenCode V2 is not installed. Install it with `npm install -g @opencode/cli` or `curl -fsSL https://opencode.ai/v2/install | bash`."
|
|
13828
15003
|
);
|
|
13829
15004
|
}
|
|
13830
15005
|
let port = ctx.port;
|
|
@@ -13852,7 +15027,8 @@ Port ${port} is already in use.`));
|
|
|
13852
15027
|
if (!ctx.interactive) {
|
|
13853
15028
|
ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
|
|
13854
15029
|
const { child: proc, password } = await startOpenCode2(port, {
|
|
13855
|
-
inheritStdio: ctx.inheritStdio
|
|
15030
|
+
inheritStdio: ctx.inheritStdio,
|
|
15031
|
+
command: ctx.command
|
|
13856
15032
|
});
|
|
13857
15033
|
const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
|
|
13858
15034
|
if (!health.healthy) {
|
|
@@ -13873,13 +15049,14 @@ Port ${port} is already in use.`));
|
|
|
13873
15049
|
password
|
|
13874
15050
|
};
|
|
13875
15051
|
}
|
|
15052
|
+
const command = ctx.command ?? "opencode2";
|
|
13876
15053
|
const action = await select3({
|
|
13877
15054
|
message: "OpenCode V2 is not running. What would you like to do?",
|
|
13878
15055
|
choices: [
|
|
13879
15056
|
{
|
|
13880
15057
|
name: "Start OpenCode V2 for me",
|
|
13881
15058
|
value: "start",
|
|
13882
|
-
description: `Run '
|
|
15059
|
+
description: `Run '${command} serve --port ${port}'`
|
|
13883
15060
|
},
|
|
13884
15061
|
{
|
|
13885
15062
|
name: "Show me the command",
|
|
@@ -13897,14 +15074,15 @@ Port ${port} is already in use.`));
|
|
|
13897
15074
|
blank();
|
|
13898
15075
|
console.log(chalk6.bold("Run this command in another terminal:"));
|
|
13899
15076
|
blank();
|
|
13900
|
-
console.log(` ${chalk6.cyan(
|
|
15077
|
+
console.log(` ${chalk6.cyan(`${command} serve --port ${port}`)}`);
|
|
13901
15078
|
blank();
|
|
13902
15079
|
throw new Error("Please start OpenCode V2 manually");
|
|
13903
15080
|
}
|
|
13904
15081
|
if (action === "start") {
|
|
13905
15082
|
const spinner = ora3("Starting OpenCode V2...").start();
|
|
13906
15083
|
const { child: proc, password } = await startOpenCode2(port, {
|
|
13907
|
-
inheritStdio: ctx.inheritStdio
|
|
15084
|
+
inheritStdio: ctx.inheritStdio,
|
|
15085
|
+
command: ctx.command
|
|
13908
15086
|
});
|
|
13909
15087
|
const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
|
|
13910
15088
|
if (!health.healthy) {
|
|
@@ -13930,7 +15108,7 @@ Port ${port} is already in use.`));
|
|
|
13930
15108
|
}
|
|
13931
15109
|
|
|
13932
15110
|
// src/lib/runner-credentials.ts
|
|
13933
|
-
import { chmodSync as chmodSync2, writeFileSync as
|
|
15111
|
+
import { chmodSync as chmodSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
13934
15112
|
import { spawn as spawn5 } from "node:child_process";
|
|
13935
15113
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
13936
15114
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
@@ -14173,8 +15351,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
14173
15351
|
}
|
|
14174
15352
|
try {
|
|
14175
15353
|
env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
|
|
14176
|
-
|
|
14177
|
-
|
|
15354
|
+
writeFileSync7(GIT_CONFIG_GLOBAL, "");
|
|
15355
|
+
writeFileSync7(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
|
|
14178
15356
|
chmodSync2(GIT_CREDENTIAL_HELPER, 448);
|
|
14179
15357
|
const config = [
|
|
14180
15358
|
["user.name", env.GIT_USER_NAME ?? "evident-bot"],
|
|
@@ -14207,7 +15385,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
14207
15385
|
// src/lib/opencode/config-overlay.ts
|
|
14208
15386
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
14209
15387
|
import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
|
|
14210
|
-
import { isAbsolute as isAbsolute3, join as
|
|
15388
|
+
import { isAbsolute as isAbsolute3, join as join14, resolve as resolve4 } from "node:path";
|
|
14211
15389
|
function parseConfigContent(content) {
|
|
14212
15390
|
if (!content?.trim()) return {};
|
|
14213
15391
|
const parsed = JSON.parse(content);
|
|
@@ -14237,7 +15415,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
14237
15415
|
return;
|
|
14238
15416
|
}
|
|
14239
15417
|
const source = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
|
|
14240
|
-
const target = isFile(
|
|
15418
|
+
const target = isFile(join14(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
14241
15419
|
if (!isFile(source)) {
|
|
14242
15420
|
log3(
|
|
14243
15421
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -14245,7 +15423,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
14245
15423
|
);
|
|
14246
15424
|
return;
|
|
14247
15425
|
}
|
|
14248
|
-
copyFileSync(source,
|
|
15426
|
+
copyFileSync(source, join14(cwd, target));
|
|
14249
15427
|
try {
|
|
14250
15428
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
14251
15429
|
stdio: "ignore"
|
|
@@ -14254,13 +15432,13 @@ function applyRunnerOpenCodeConfig({
|
|
|
14254
15432
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
14255
15433
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
14256
15434
|
}
|
|
14257
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
15435
|
+
log3(`Applied runner OpenCode config ${source} to ${join14(cwd, target)}`);
|
|
14258
15436
|
}
|
|
14259
15437
|
|
|
14260
15438
|
// src/lib/opencode/runtime-provenance.ts
|
|
14261
|
-
import { readFileSync as
|
|
15439
|
+
import { readFileSync as readFileSync11, readdirSync } from "node:fs";
|
|
14262
15440
|
import { homedir as homedir7 } from "node:os";
|
|
14263
|
-
import { join as
|
|
15441
|
+
import { join as join15 } from "node:path";
|
|
14264
15442
|
var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
|
14265
15443
|
var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
|
|
14266
15444
|
function errorKind(error2) {
|
|
@@ -14328,7 +15506,7 @@ function parseJsonc(value) {
|
|
|
14328
15506
|
return JSON.parse(output);
|
|
14329
15507
|
}
|
|
14330
15508
|
function openCodeCacheRoot(env = process.env, homeDir = homedir7()) {
|
|
14331
|
-
return
|
|
15509
|
+
return join15(env.XDG_CACHE_HOME ?? join15(homeDir, ".cache"), "opencode");
|
|
14332
15510
|
}
|
|
14333
15511
|
function readConfiguredPluginSpecs({
|
|
14334
15512
|
cwd = process.cwd(),
|
|
@@ -14337,10 +15515,10 @@ function readConfiguredPluginSpecs({
|
|
|
14337
15515
|
let config;
|
|
14338
15516
|
try {
|
|
14339
15517
|
try {
|
|
14340
|
-
config = parseJsonc(
|
|
15518
|
+
config = parseJsonc(readFileSync11(join15(cwd, "opencode.jsonc"), "utf8"));
|
|
14341
15519
|
} catch (error2) {
|
|
14342
15520
|
if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
|
|
14343
|
-
config = parseJsonc(
|
|
15521
|
+
config = parseJsonc(readFileSync11(join15(cwd, "opencode.json"), "utf8"));
|
|
14344
15522
|
}
|
|
14345
15523
|
} catch (error2) {
|
|
14346
15524
|
log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
|
|
@@ -14368,7 +15546,7 @@ function resolveInstalledPluginVersions({
|
|
|
14368
15546
|
homeDir = homedir7(),
|
|
14369
15547
|
log: log3
|
|
14370
15548
|
}) {
|
|
14371
|
-
const packagesRoot =
|
|
15549
|
+
const packagesRoot = join15(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
|
|
14372
15550
|
let directories;
|
|
14373
15551
|
try {
|
|
14374
15552
|
directories = readdirSync(packagesRoot, { withFileTypes: true });
|
|
@@ -14389,8 +15567,8 @@ function resolveInstalledPluginVersions({
|
|
|
14389
15567
|
let resolvedVersion = null;
|
|
14390
15568
|
try {
|
|
14391
15569
|
const packageJson = JSON.parse(
|
|
14392
|
-
|
|
14393
|
-
|
|
15570
|
+
readFileSync11(
|
|
15571
|
+
join15(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
|
|
14394
15572
|
"utf8"
|
|
14395
15573
|
)
|
|
14396
15574
|
);
|
|
@@ -14411,7 +15589,7 @@ function resolveInstalledPluginVersions({
|
|
|
14411
15589
|
}
|
|
14412
15590
|
|
|
14413
15591
|
// src/lib/credential-sync.ts
|
|
14414
|
-
import { renameSync, writeFileSync as
|
|
15592
|
+
import { renameSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
14415
15593
|
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
14416
15594
|
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
14417
15595
|
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
@@ -14421,7 +15599,7 @@ var MAX_FLUSH_PASSES = 2;
|
|
|
14421
15599
|
function outcomesWith(outcome) {
|
|
14422
15600
|
return { claude: outcome, opencode: outcome };
|
|
14423
15601
|
}
|
|
14424
|
-
function
|
|
15602
|
+
function errorMessage3(error2) {
|
|
14425
15603
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
14426
15604
|
}
|
|
14427
15605
|
function waitForSettlement(promise, timeoutMs) {
|
|
@@ -14445,10 +15623,10 @@ function writeMarker(markerPath, outcomes, log3) {
|
|
|
14445
15623
|
`;
|
|
14446
15624
|
const temporaryPath = `${markerPath}.tmp`;
|
|
14447
15625
|
try {
|
|
14448
|
-
|
|
15626
|
+
writeFileSync8(temporaryPath, body, { mode: 384 });
|
|
14449
15627
|
renameSync(temporaryPath, markerPath);
|
|
14450
15628
|
} catch (error2) {
|
|
14451
|
-
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${
|
|
15629
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage3(error2)}`, "warn");
|
|
14452
15630
|
}
|
|
14453
15631
|
}
|
|
14454
15632
|
function intervalSeconds(env, log3) {
|
|
@@ -14480,7 +15658,7 @@ async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
|
14480
15658
|
},
|
|
14481
15659
|
(error2) => {
|
|
14482
15660
|
failed = true;
|
|
14483
|
-
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${
|
|
15661
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage3(error2)}`, "warn");
|
|
14484
15662
|
}
|
|
14485
15663
|
);
|
|
14486
15664
|
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
@@ -14540,7 +15718,7 @@ function createCredentialSync({
|
|
|
14540
15718
|
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
14541
15719
|
} catch (error2) {
|
|
14542
15720
|
outcomes[store] = "failed";
|
|
14543
|
-
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${
|
|
15721
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage3(error2)}`, "debug");
|
|
14544
15722
|
}
|
|
14545
15723
|
}
|
|
14546
15724
|
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
@@ -14690,6 +15868,18 @@ function readSetup(value) {
|
|
|
14690
15868
|
}
|
|
14691
15869
|
return setup;
|
|
14692
15870
|
}
|
|
15871
|
+
function readModelDescriptors(value) {
|
|
15872
|
+
if (!Array.isArray(value)) return null;
|
|
15873
|
+
return value.every(
|
|
15874
|
+
(descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
|
|
15875
|
+
) ? value : null;
|
|
15876
|
+
}
|
|
15877
|
+
function logDescriptorRefreshFallback(options, branch, generation) {
|
|
15878
|
+
options.log(
|
|
15879
|
+
`MANAGED-INFERENCE-DESCRIPTORS-${branch}: runner=${options.agentId} generation=${generation}`,
|
|
15880
|
+
"warn"
|
|
15881
|
+
);
|
|
15882
|
+
}
|
|
14693
15883
|
function clearLocalSetup(options) {
|
|
14694
15884
|
const env = options.env ?? process.env;
|
|
14695
15885
|
const store = options.store ?? defaultStore;
|
|
@@ -14821,7 +16011,27 @@ async function reaffirmStored(options, credential) {
|
|
|
14821
16011
|
if (!setup || setup.generation !== credential.generation || setup.token_id !== credential.tokenId) {
|
|
14822
16012
|
return { kind: "failed", reason: "Managed credential reaffirmation returned a mismatch" };
|
|
14823
16013
|
}
|
|
14824
|
-
const
|
|
16014
|
+
const descriptorsValue = body?.model_descriptors;
|
|
16015
|
+
const refreshedDescriptors = readModelDescriptors(descriptorsValue);
|
|
16016
|
+
let credentialToApply = credential;
|
|
16017
|
+
if (descriptorsValue === void 0 || descriptorsValue === null) {
|
|
16018
|
+
logDescriptorRefreshFallback(options, "UNAVAILABLE", credential.generation);
|
|
16019
|
+
} else if (refreshedDescriptors === null) {
|
|
16020
|
+
logDescriptorRefreshFallback(options, "MALFORMED", credential.generation);
|
|
16021
|
+
} else if (refreshedDescriptors.length === 0) {
|
|
16022
|
+
logDescriptorRefreshFallback(options, "EMPTY", credential.generation);
|
|
16023
|
+
} else {
|
|
16024
|
+
const { config, unsupportedProviders } = buildCredentialProxyNativeConfig({
|
|
16025
|
+
inferenceBaseUrl: credential.inferenceBaseUrl,
|
|
16026
|
+
modelDescriptors: refreshedDescriptors
|
|
16027
|
+
});
|
|
16028
|
+
if (unsupportedProviders.length > 0 || Object.keys(config.provider).length === 0) {
|
|
16029
|
+
logDescriptorRefreshFallback(options, "UNSUPPORTED-PROVIDER", credential.generation);
|
|
16030
|
+
} else {
|
|
16031
|
+
credentialToApply = { ...credential, modelDescriptors: refreshedDescriptors };
|
|
16032
|
+
}
|
|
16033
|
+
}
|
|
16034
|
+
const failure = applyCredential(options, credentialToApply);
|
|
14825
16035
|
return failure ? { kind: "failed", reason: failure } : { kind: "ready", generation: credential.generation, tokenId: credential.tokenId };
|
|
14826
16036
|
}
|
|
14827
16037
|
async function prepareManagedInferenceSetup(options, discovery, health) {
|
|
@@ -14855,10 +16065,8 @@ async function prepareManagedInferenceSetup(options, discovery, health) {
|
|
|
14855
16065
|
const setup = readSetup(body?.setup);
|
|
14856
16066
|
const token = body?.inference_token;
|
|
14857
16067
|
const inferenceBaseUrl = body?.inference_base_url;
|
|
14858
|
-
const modelDescriptors = body?.model_descriptors;
|
|
14859
|
-
if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" ||
|
|
14860
|
-
(descriptor) => typeof descriptor === "object" && descriptor !== null && typeof descriptor.id === "string" && typeof descriptor.provider === "string"
|
|
14861
|
-
)) {
|
|
16068
|
+
const modelDescriptors = readModelDescriptors(body?.model_descriptors);
|
|
16069
|
+
if (!setup || setup.generation !== discovery.setup.generation || typeof setup.token_id !== "string" || typeof token !== "string" || typeof inferenceBaseUrl !== "string" || modelDescriptors === null) {
|
|
14862
16070
|
return { kind: "failed", reason: "Managed credential bootstrap returned an invalid response" };
|
|
14863
16071
|
}
|
|
14864
16072
|
const credential = {
|
|
@@ -14927,33 +16135,49 @@ function resolveLogLevel(options) {
|
|
|
14927
16135
|
}
|
|
14928
16136
|
return "info";
|
|
14929
16137
|
}
|
|
14930
|
-
function
|
|
14931
|
-
const
|
|
16138
|
+
function resolveFileSyncEntry(entry, homeDir, flagName, labels) {
|
|
16139
|
+
const trimmed = entry.trim();
|
|
16140
|
+
if (trimmed === "") {
|
|
16141
|
+
throw new Error(`${flagName} requires a ${labels.noun} path (got an empty value)`);
|
|
16142
|
+
}
|
|
16143
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join16(homeDir, trimmed.slice(2)) : trimmed;
|
|
16144
|
+
if (!isAbsolute4(expanded)) {
|
|
16145
|
+
throw new Error(`${flagName} requires an absolute ${labels.noun} path; got "${entry}"`);
|
|
16146
|
+
}
|
|
16147
|
+
const normalized = resolvePath2(expanded);
|
|
16148
|
+
if (parse(normalized).root === normalized) {
|
|
16149
|
+
throw new Error(
|
|
16150
|
+
`${flagName} will not allow-list the filesystem root ("${entry}"); name the specific ${labels.noun} the credentials belong in (for example ${labels.rootExample})`
|
|
16151
|
+
);
|
|
16152
|
+
}
|
|
16153
|
+
return normalized;
|
|
16154
|
+
}
|
|
16155
|
+
function resolveFileSyncAllowList(raw, homeDir, flagName, labels) {
|
|
16156
|
+
const paths = [];
|
|
14932
16157
|
for (const entry of raw ?? []) {
|
|
14933
|
-
const
|
|
14934
|
-
if (
|
|
14935
|
-
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
14936
|
-
}
|
|
14937
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join13(homeDir, trimmed.slice(2)) : trimmed;
|
|
14938
|
-
if (!isAbsolute4(expanded)) {
|
|
14939
|
-
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
14940
|
-
}
|
|
14941
|
-
const normalized = resolvePath2(expanded);
|
|
14942
|
-
if (parse(normalized).root === normalized) {
|
|
14943
|
-
throw new Error(
|
|
14944
|
-
`--enable-file-sync-to will not allow-list the filesystem root ("${entry}"); name the specific directory the credentials belong in (for example ~/.claude)`
|
|
14945
|
-
);
|
|
14946
|
-
}
|
|
14947
|
-
if (!directories.includes(normalized)) {
|
|
14948
|
-
directories.push(normalized);
|
|
14949
|
-
}
|
|
16158
|
+
const normalized = resolveFileSyncEntry(entry, homeDir, flagName, labels);
|
|
16159
|
+
if (!paths.includes(normalized)) paths.push(normalized);
|
|
14950
16160
|
}
|
|
14951
|
-
if (
|
|
16161
|
+
if (paths.length > MAX_FILE_SYNC_DIRECTORIES) {
|
|
14952
16162
|
throw new Error(
|
|
14953
|
-
|
|
16163
|
+
`${flagName} accepts at most ${MAX_FILE_SYNC_DIRECTORIES} ${labels.plural}; got ${paths.length}`
|
|
14954
16164
|
);
|
|
14955
16165
|
}
|
|
14956
|
-
return
|
|
16166
|
+
return paths;
|
|
16167
|
+
}
|
|
16168
|
+
function resolveFileSyncDirectories(raw, homeDir) {
|
|
16169
|
+
return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to", {
|
|
16170
|
+
noun: "directory",
|
|
16171
|
+
plural: "directories",
|
|
16172
|
+
rootExample: "~/.claude"
|
|
16173
|
+
});
|
|
16174
|
+
}
|
|
16175
|
+
function resolveFileSyncExactFiles(raw, homeDir) {
|
|
16176
|
+
return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to-exact-file", {
|
|
16177
|
+
noun: "file",
|
|
16178
|
+
plural: "files",
|
|
16179
|
+
rootExample: "~/.local/share/opencode/mcp-auth.json"
|
|
16180
|
+
});
|
|
14957
16181
|
}
|
|
14958
16182
|
var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
|
|
14959
16183
|
var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
|
|
@@ -14969,13 +16193,13 @@ function resolveOpenCodeVersion(options, env = process.env) {
|
|
|
14969
16193
|
raw = env[OPENCODE_VERSION_ENV];
|
|
14970
16194
|
source = OPENCODE_VERSION_ENV;
|
|
14971
16195
|
} else {
|
|
14972
|
-
return { version: "
|
|
16196
|
+
return { version: "auto", warnings: [] };
|
|
14973
16197
|
}
|
|
14974
16198
|
const normalized = raw.trim().toLowerCase();
|
|
14975
|
-
if (normalized !== "v1" && normalized !== "v2") {
|
|
16199
|
+
if (normalized !== "v1" && normalized !== "v2" && normalized !== "auto") {
|
|
14976
16200
|
return {
|
|
14977
|
-
version: "
|
|
14978
|
-
warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or
|
|
16201
|
+
version: "auto",
|
|
16202
|
+
warnings: [`Ignoring invalid ${source} "${raw}": expected v1, v2, or auto; using auto`]
|
|
14979
16203
|
};
|
|
14980
16204
|
}
|
|
14981
16205
|
return { version: normalized, warnings: [] };
|
|
@@ -15485,8 +16709,8 @@ async function driveChannels(state, driver) {
|
|
|
15485
16709
|
state.running = false;
|
|
15486
16710
|
break;
|
|
15487
16711
|
}
|
|
15488
|
-
const
|
|
15489
|
-
logActivity(state, { type: "error", error: `Channel processing error: ${
|
|
16712
|
+
const errorMessage4 = error2 instanceof Error ? error2.message : String(error2);
|
|
16713
|
+
logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage4}` });
|
|
15490
16714
|
if (state.interactive) displayStatus(state);
|
|
15491
16715
|
if (driver.hasInFlightWatchers()) {
|
|
15492
16716
|
consecutiveDrainFailures = 0;
|
|
@@ -15576,6 +16800,7 @@ async function driveChannels(state, driver) {
|
|
|
15576
16800
|
}
|
|
15577
16801
|
}
|
|
15578
16802
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
16803
|
+
var SESSION_ARCHIVE_FIRST_RUN_MS = SESSION_CLEANUP_FIRST_SWEEP_MS + 1e3;
|
|
15579
16804
|
var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
|
|
15580
16805
|
var SESSION_CLEANUP_MAX_ISOLATED_DELETE_FAILURES = 2;
|
|
15581
16806
|
var SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES = 2;
|
|
@@ -15590,7 +16815,7 @@ function shouldWarnForReclaimSkip(reason) {
|
|
|
15590
16815
|
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
15591
16816
|
}
|
|
15592
16817
|
function sessionDbPath() {
|
|
15593
|
-
return
|
|
16818
|
+
return join16(homedir8(), ".local", "share", "opencode", "opencode.db");
|
|
15594
16819
|
}
|
|
15595
16820
|
function logSessionCleanupActivity(state, phase, entry) {
|
|
15596
16821
|
logActivity(state, {
|
|
@@ -15599,6 +16824,104 @@ function logSessionCleanupActivity(state, phase, entry) {
|
|
|
15599
16824
|
metadata: { ...entry.metadata, phase }
|
|
15600
16825
|
});
|
|
15601
16826
|
}
|
|
16827
|
+
function logSessionArchiveActivity(state, phase, entry) {
|
|
16828
|
+
logActivity(state, {
|
|
16829
|
+
...entry,
|
|
16830
|
+
source: "cli.session-archive",
|
|
16831
|
+
metadata: { ...entry.metadata, phase }
|
|
16832
|
+
});
|
|
16833
|
+
}
|
|
16834
|
+
function logSessionArchiveSummary(state, phase, summary) {
|
|
16835
|
+
const everyUploadFailed = summary.uploadAttempts > 0 && summary.uploadFailures === summary.uploadAttempts;
|
|
16836
|
+
const staleSince = everyUploadFailed && summary.staleSince !== null ? `; archive stale since ${summary.staleSince}` : "";
|
|
16837
|
+
const neverWatermarked = summary.neverWatermarkedFailures > 0 ? `; ${summary.neverWatermarkedFailures} failed session(s) had no prior successful upload` : "";
|
|
16838
|
+
const warning2 = summary.failed > 0 || summary.failures.length > 0 || everyUploadFailed;
|
|
16839
|
+
logSessionArchiveActivity(state, phase, {
|
|
16840
|
+
type: "info",
|
|
16841
|
+
level: warning2 ? "warn" : "info",
|
|
16842
|
+
message: `Session archive: inspected ${summary.inspected}, uploaded ${summary.uploaded}, unchanged ${summary.unchanged}, failed ${summary.failed}, skipped budget ${summary.skippedBudget}, unmapped ${summary.unmappedIds.length}` + staleSince + neverWatermarked,
|
|
16843
|
+
metadata: {
|
|
16844
|
+
outcome: "archive-run-complete",
|
|
16845
|
+
sessions_inspected: summary.inspected,
|
|
16846
|
+
sessions_uploaded: summary.uploaded,
|
|
16847
|
+
sessions_unchanged: summary.unchanged,
|
|
16848
|
+
sessions_failed: summary.failed,
|
|
16849
|
+
uploads_attempted: summary.uploadAttempts,
|
|
16850
|
+
uploads_failed: summary.uploadFailures,
|
|
16851
|
+
sessions_skipped_budget: summary.skippedBudget,
|
|
16852
|
+
sessions_unmapped: summary.unmappedIds.length,
|
|
16853
|
+
sessions_without_prior_watermark: summary.neverWatermarkedFailures,
|
|
16854
|
+
...everyUploadFailed && summary.staleSince !== null ? { stale_since: summary.staleSince } : {}
|
|
16855
|
+
}
|
|
16856
|
+
});
|
|
16857
|
+
for (const failure of summary.failures) {
|
|
16858
|
+
const permanentSizeFailure = failure.reason === "too-large";
|
|
16859
|
+
const safeMessage = failure.message ? `: ${redactDiagnosticText(failure.message).slice(0, 200)}` : "";
|
|
16860
|
+
logSessionArchiveActivity(state, phase, {
|
|
16861
|
+
type: "info",
|
|
16862
|
+
level: "warn",
|
|
16863
|
+
message: `Session archive: session ${failure.sessionId} ` + (permanentSizeFailure ? "failed permanently (too-large payload)" : `failed (${failure.outcome}`) + (permanentSizeFailure ? "" : failure.reason ? `, reason=${failure.reason})` : ")") + safeMessage,
|
|
16864
|
+
metadata: {
|
|
16865
|
+
outcome: failure.outcome,
|
|
16866
|
+
session_id: failure.sessionId,
|
|
16867
|
+
archived: failure.archived,
|
|
16868
|
+
...failure.reason !== void 0 ? { reason: failure.reason } : {}
|
|
16869
|
+
}
|
|
16870
|
+
});
|
|
16871
|
+
}
|
|
16872
|
+
if (summary.unmappedIds.length > 0) {
|
|
16873
|
+
logSessionArchiveActivity(state, phase, {
|
|
16874
|
+
type: "info",
|
|
16875
|
+
level: "warn",
|
|
16876
|
+
message: `Session archive: ${summary.unmappedIds.length} unmapped session(s): ` + summary.unmappedIds.join(", "),
|
|
16877
|
+
metadata: {
|
|
16878
|
+
outcome: "unmapped-sessions",
|
|
16879
|
+
sessions_unmapped: summary.unmappedIds.length
|
|
16880
|
+
}
|
|
16881
|
+
});
|
|
16882
|
+
}
|
|
16883
|
+
}
|
|
16884
|
+
async function drainSessionDeletionReports(state) {
|
|
16885
|
+
const path = sessionDeletionReportPath(homedir8(), process.env);
|
|
16886
|
+
let delivered = 0;
|
|
16887
|
+
let failure = null;
|
|
16888
|
+
try {
|
|
16889
|
+
const spool = readSessionDeletionRecords(path);
|
|
16890
|
+
if (spool.readError !== null) {
|
|
16891
|
+
failure = `could not read spool: ${spool.readError}`;
|
|
16892
|
+
} else if (spool.records.length === 0) {
|
|
16893
|
+
return;
|
|
16894
|
+
} else if (!state.agentId || !state.authHeader) {
|
|
16895
|
+
failure = "runner authentication is not available";
|
|
16896
|
+
} else {
|
|
16897
|
+
for (let offset = 0; offset < spool.records.length; offset += 200) {
|
|
16898
|
+
const batch = spool.records.slice(offset, offset + 200);
|
|
16899
|
+
const result = await reportSessionDeletions(state.agentId, state.authHeader, batch);
|
|
16900
|
+
if (!result.ok) {
|
|
16901
|
+
failure = result.error ?? "unknown API error";
|
|
16902
|
+
break;
|
|
16903
|
+
}
|
|
16904
|
+
removeDeliveredSessionDeletionRecords(path, batch);
|
|
16905
|
+
delivered += batch.length;
|
|
16906
|
+
}
|
|
16907
|
+
}
|
|
16908
|
+
} catch (error2) {
|
|
16909
|
+
failure = error2 instanceof Error ? error2.message : String(error2);
|
|
16910
|
+
}
|
|
16911
|
+
const pending = readSessionDeletionRecords(path);
|
|
16912
|
+
const pendingCount = pending.readError === null ? pending.records.length : null;
|
|
16913
|
+
const message = `Session deletion reports: delivered ${delivered}, ${pendingCount === null ? "pending count unavailable" : `${pendingCount} pending`}` + (failure === null ? "." : `; delivery failed: ${failure}.`);
|
|
16914
|
+
logActivity(state, {
|
|
16915
|
+
type: "info",
|
|
16916
|
+
level: failure === null ? "info" : "warn",
|
|
16917
|
+
message,
|
|
16918
|
+
metadata: {
|
|
16919
|
+
outcome: failure === null ? "session-deletion-reports-drained" : "session-deletion-reports-pending",
|
|
16920
|
+
delivered,
|
|
16921
|
+
pending: pendingCount
|
|
16922
|
+
}
|
|
16923
|
+
});
|
|
16924
|
+
}
|
|
15602
16925
|
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
15603
16926
|
const record = {
|
|
15604
16927
|
v: 1,
|
|
@@ -15655,18 +16978,25 @@ async function runSweep(state, driver, config) {
|
|
|
15655
16978
|
let failedOtherStatus = 0;
|
|
15656
16979
|
let failedThrew = 0;
|
|
15657
16980
|
let skippedNewlyActive = 0;
|
|
16981
|
+
let skippedArchive = 0;
|
|
15658
16982
|
let remaining = 0;
|
|
16983
|
+
const deletionRecords = [];
|
|
15659
16984
|
let protectedNow = driver.protectedSessionIds();
|
|
15660
16985
|
if (sessions !== null) {
|
|
15661
|
-
const
|
|
15662
|
-
|
|
15663
|
-
|
|
15664
|
-
|
|
15665
|
-
|
|
15666
|
-
|
|
15667
|
-
protectedIds: protectedNow
|
|
15668
|
-
}
|
|
16986
|
+
const snapshots = sessions.map((session) => ({
|
|
16987
|
+
id: session.id,
|
|
16988
|
+
lastActivityMs: sessionLastActivityMs(session)
|
|
16989
|
+
}));
|
|
16990
|
+
const lastActivityById = new Map(
|
|
16991
|
+
snapshots.map(({ id, lastActivityMs }) => [id, lastActivityMs])
|
|
15669
16992
|
);
|
|
16993
|
+
const sessionById = new Map(sessions.map((session) => [session.id, session]));
|
|
16994
|
+
const toDelete = selectSessionsToDelete(snapshots, {
|
|
16995
|
+
maxAgeMs: config.maxAgeMs,
|
|
16996
|
+
maxCount: config.maxCount,
|
|
16997
|
+
nowMs: Date.now(),
|
|
16998
|
+
protectedIds: protectedNow
|
|
16999
|
+
});
|
|
15670
17000
|
const eligibleToDelete = toDelete.filter(
|
|
15671
17001
|
(id) => (state.sessionDeleteFailures.get(id)?.isolatedFailures ?? 0) < SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES
|
|
15672
17002
|
);
|
|
@@ -15683,9 +17013,56 @@ async function runSweep(state, driver, config) {
|
|
|
15683
17013
|
});
|
|
15684
17014
|
continue;
|
|
15685
17015
|
}
|
|
17016
|
+
if (state.sessionArchiveConfig.enabled) {
|
|
17017
|
+
const candidate = sessionById.get(id);
|
|
17018
|
+
const archiveConfig = state.sessionArchiveConfig;
|
|
17019
|
+
const archiveBudgetMs = Math.min(
|
|
17020
|
+
archiveConfig.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
|
|
17021
|
+
MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
|
|
17022
|
+
);
|
|
17023
|
+
const archived = candidate ? await runSessionArchiveTask(
|
|
17024
|
+
state,
|
|
17025
|
+
"sweep",
|
|
17026
|
+
archiveBudgetMs,
|
|
17027
|
+
(signal) => runSessionArchiveBatch(state, "sweep", [candidate], archiveBudgetMs, signal)
|
|
17028
|
+
) : null;
|
|
17029
|
+
if (!archived?.archivedSessionIds.includes(id)) {
|
|
17030
|
+
skippedArchive++;
|
|
17031
|
+
logSessionArchiveActivity(state, "sweep", {
|
|
17032
|
+
type: "info",
|
|
17033
|
+
level: "warn",
|
|
17034
|
+
message: `Session cleanup: retaining session ${id}; deletion is held back pending successful archival`,
|
|
17035
|
+
metadata: {
|
|
17036
|
+
outcome: "deletion-held-for-archive",
|
|
17037
|
+
session_id: id
|
|
17038
|
+
}
|
|
17039
|
+
});
|
|
17040
|
+
continue;
|
|
17041
|
+
}
|
|
17042
|
+
protectedNow = driver.protectedSessionIds();
|
|
17043
|
+
if (protectedNow.has(id)) {
|
|
17044
|
+
skippedNewlyActive++;
|
|
17045
|
+
logActivity(state, {
|
|
17046
|
+
type: "info",
|
|
17047
|
+
message: `Session cleanup: skipping ${id} \u2014 became active/bound during archival (${mode})`
|
|
17048
|
+
});
|
|
17049
|
+
continue;
|
|
17050
|
+
}
|
|
17051
|
+
}
|
|
15686
17052
|
const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
|
|
15687
17053
|
if (outcome.ok) {
|
|
15688
17054
|
deleted++;
|
|
17055
|
+
const lastActivityMs = lastActivityById.get(id) ?? null;
|
|
17056
|
+
deletionRecords.push({
|
|
17057
|
+
opencode_session_id: id,
|
|
17058
|
+
deleted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17059
|
+
reason: "auto_cleanup",
|
|
17060
|
+
policy: {
|
|
17061
|
+
max_age_ms: config.maxAgeMs ?? null,
|
|
17062
|
+
max_count: config.maxCount ?? null,
|
|
17063
|
+
last_activity_at: lastActivityMs === null ? null : new Date(lastActivityMs).toISOString()
|
|
17064
|
+
}
|
|
17065
|
+
});
|
|
15689
17066
|
} else if (outcome.status === "threw") {
|
|
15690
17067
|
failed++;
|
|
15691
17068
|
failedThrew++;
|
|
@@ -15735,12 +17112,39 @@ async function runSweep(state, driver, config) {
|
|
|
15735
17112
|
}
|
|
15736
17113
|
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
15737
17114
|
const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
|
|
17115
|
+
const archiveHeldNote = skippedArchive > 0 ? `, archive held ${skippedArchive}` : "";
|
|
15738
17116
|
const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
|
|
15739
17117
|
logActivity(state, {
|
|
15740
17118
|
type: "info",
|
|
15741
|
-
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
|
|
17119
|
+
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${archiveHeldNote}${remainingNote} (${mode})`
|
|
15742
17120
|
});
|
|
15743
17121
|
}
|
|
17122
|
+
if (deletionRecords.length > 0) {
|
|
17123
|
+
try {
|
|
17124
|
+
const dropped = appendSessionDeletionRecords(
|
|
17125
|
+
sessionDeletionReportPath(homedir8(), process.env),
|
|
17126
|
+
deletionRecords
|
|
17127
|
+
);
|
|
17128
|
+
if (dropped > 0) {
|
|
17129
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
17130
|
+
type: "info",
|
|
17131
|
+
level: "warn",
|
|
17132
|
+
message: `Session cleanup: dropped ${dropped} oldest pending deletion report(s) after reaching the spool limit`,
|
|
17133
|
+
metadata: { outcome: "deletion-reports-dropped", dropped_count: dropped }
|
|
17134
|
+
});
|
|
17135
|
+
}
|
|
17136
|
+
} catch (error2) {
|
|
17137
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
17138
|
+
type: "info",
|
|
17139
|
+
level: "warn",
|
|
17140
|
+
message: `Session cleanup: could not persist ${deletionRecords.length} confirmed deletion report(s): ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
17141
|
+
metadata: {
|
|
17142
|
+
outcome: "deletion-report-spool-write-failed",
|
|
17143
|
+
records: deletionRecords.length
|
|
17144
|
+
}
|
|
17145
|
+
});
|
|
17146
|
+
}
|
|
17147
|
+
}
|
|
15744
17148
|
const sessionMetadata = {
|
|
15745
17149
|
sessions_inspected: sessions?.length ?? null,
|
|
15746
17150
|
sessions_deleted: deleted,
|
|
@@ -15750,6 +17154,7 @@ async function runSweep(state, driver, config) {
|
|
|
15750
17154
|
sessions_failed_other: failedOtherStatus,
|
|
15751
17155
|
sessions_failed_threw: failedThrew,
|
|
15752
17156
|
sessions_skipped_active: skippedNewlyActive,
|
|
17157
|
+
sessions_archive_held: skippedArchive,
|
|
15753
17158
|
sessions_remaining: remaining,
|
|
15754
17159
|
...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
|
|
15755
17160
|
};
|
|
@@ -15810,6 +17215,7 @@ async function runSweep(state, driver, config) {
|
|
|
15810
17215
|
}
|
|
15811
17216
|
});
|
|
15812
17217
|
}
|
|
17218
|
+
await drainSessionDeletionReports(state);
|
|
15813
17219
|
} catch (error2) {
|
|
15814
17220
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
15815
17221
|
logSessionCleanupActivity(state, "sweep", {
|
|
@@ -15880,6 +17286,186 @@ function scheduleSessionCleanup(state, driver, options) {
|
|
|
15880
17286
|
);
|
|
15881
17287
|
state.sessionCleanupTimers.push(interval, firstSweep);
|
|
15882
17288
|
}
|
|
17289
|
+
function runSessionArchiveTask(state, phase, budgetMs, work) {
|
|
17290
|
+
if (state.sessionArchiveInFlight) {
|
|
17291
|
+
logSessionArchiveActivity(state, phase, {
|
|
17292
|
+
type: "info",
|
|
17293
|
+
level: "debug",
|
|
17294
|
+
message: "Session archive skipped \u2014 previous archive run still in progress",
|
|
17295
|
+
metadata: { outcome: "skipped-in-flight" }
|
|
17296
|
+
});
|
|
17297
|
+
return Promise.resolve(null);
|
|
17298
|
+
}
|
|
17299
|
+
const controller = new AbortController();
|
|
17300
|
+
state.sessionArchiveAbortController = controller;
|
|
17301
|
+
const timeout = setTimeout(() => {
|
|
17302
|
+
controller.abort();
|
|
17303
|
+
logSessionArchiveActivity(state, phase, {
|
|
17304
|
+
type: "info",
|
|
17305
|
+
level: "warn",
|
|
17306
|
+
message: `Session archive exceeded ${budgetMs}ms; remaining sessions were not confirmed archived`,
|
|
17307
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17308
|
+
});
|
|
17309
|
+
}, budgetMs);
|
|
17310
|
+
const task = Promise.resolve().then(() => work(controller.signal)).catch((error2) => {
|
|
17311
|
+
logSessionArchiveActivity(state, phase, {
|
|
17312
|
+
type: "info",
|
|
17313
|
+
level: "warn",
|
|
17314
|
+
message: `Session archive run failed: ${redactDiagnosticText(error2 instanceof Error ? error2.message : String(error2)).slice(0, 200)}`,
|
|
17315
|
+
metadata: { outcome: "archive-run-failed" }
|
|
17316
|
+
});
|
|
17317
|
+
return null;
|
|
17318
|
+
}).finally(() => {
|
|
17319
|
+
clearTimeout(timeout);
|
|
17320
|
+
if (state.sessionArchiveInFlight === task) {
|
|
17321
|
+
state.sessionArchiveInFlight = null;
|
|
17322
|
+
state.sessionArchiveAbortController = null;
|
|
17323
|
+
}
|
|
17324
|
+
});
|
|
17325
|
+
state.sessionArchiveInFlight = task;
|
|
17326
|
+
return task;
|
|
17327
|
+
}
|
|
17328
|
+
async function runSessionArchiveBatch(state, phase, sessions, budgetMs, signal) {
|
|
17329
|
+
const config = state.sessionArchiveConfig;
|
|
17330
|
+
const client = state.opencodeClient;
|
|
17331
|
+
const watermarkPath = state.sessionArchiveWatermarkPath;
|
|
17332
|
+
if (!client || !config.keyPrefix || !watermarkPath || !state.agentId) {
|
|
17333
|
+
logSessionArchiveActivity(state, phase, {
|
|
17334
|
+
type: "info",
|
|
17335
|
+
level: "warn",
|
|
17336
|
+
message: "Session archive skipped because runner identity or archive configuration is unavailable",
|
|
17337
|
+
metadata: { outcome: "archive-configuration-unavailable" }
|
|
17338
|
+
});
|
|
17339
|
+
return null;
|
|
17340
|
+
}
|
|
17341
|
+
const summary = await runSessionArchive({
|
|
17342
|
+
sessions,
|
|
17343
|
+
deps: {
|
|
17344
|
+
client,
|
|
17345
|
+
runnerId: state.agentId,
|
|
17346
|
+
keyPrefix: config.keyPrefix,
|
|
17347
|
+
bindings: readSessionBindings(
|
|
17348
|
+
sessionBindingLedgerPath(homedir8(), process.env),
|
|
17349
|
+
(message) => logSessionArchiveActivity(state, phase, {
|
|
17350
|
+
type: "info",
|
|
17351
|
+
level: "warn",
|
|
17352
|
+
message: redactDiagnosticText(message),
|
|
17353
|
+
metadata: { outcome: "binding-ledger-read-failed" }
|
|
17354
|
+
})
|
|
17355
|
+
),
|
|
17356
|
+
watermarks: state.sessionArchiveWatermarks,
|
|
17357
|
+
watermarkPath,
|
|
17358
|
+
signal,
|
|
17359
|
+
upload: uploadSessionArchive,
|
|
17360
|
+
exportMessages: exportSessionMessages,
|
|
17361
|
+
recordWatermark: recordUpload
|
|
17362
|
+
},
|
|
17363
|
+
budgetMs
|
|
17364
|
+
});
|
|
17365
|
+
logSessionArchiveSummary(state, phase, summary);
|
|
17366
|
+
return summary;
|
|
17367
|
+
}
|
|
17368
|
+
async function runSessionArchiveFleet(state, phase, budgetMs, signal) {
|
|
17369
|
+
const startedAt = Date.now();
|
|
17370
|
+
const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0, signal);
|
|
17371
|
+
if (enumeration.sessions === null) {
|
|
17372
|
+
const interrupted = signal.aborted;
|
|
17373
|
+
logSessionArchiveActivity(state, phase, {
|
|
17374
|
+
type: "info",
|
|
17375
|
+
level: "warn",
|
|
17376
|
+
message: interrupted ? "Session archive: enumeration was interrupted before the complete store was known; no session was marked archived" : `Session archive: could not enumerate the complete session store (${enumeration.reason}); no session was marked archived`,
|
|
17377
|
+
metadata: {
|
|
17378
|
+
outcome: interrupted ? "enumeration-interrupted" : "enumeration-refused",
|
|
17379
|
+
...!interrupted ? { reason: enumeration.reason } : {}
|
|
17380
|
+
}
|
|
17381
|
+
});
|
|
17382
|
+
return null;
|
|
17383
|
+
}
|
|
17384
|
+
return runSessionArchiveBatch(
|
|
17385
|
+
state,
|
|
17386
|
+
phase,
|
|
17387
|
+
enumeration.sessions,
|
|
17388
|
+
Math.max(0, budgetMs - (Date.now() - startedAt)),
|
|
17389
|
+
signal
|
|
17390
|
+
);
|
|
17391
|
+
}
|
|
17392
|
+
async function runSessionArchiveShutdownFlush(state, budgetMs) {
|
|
17393
|
+
const deadline = Date.now() + budgetMs;
|
|
17394
|
+
const previousRun = state.sessionArchiveInFlight;
|
|
17395
|
+
if (previousRun) {
|
|
17396
|
+
state.sessionArchiveAbortController?.abort(
|
|
17397
|
+
new Error("graceful shutdown archive flush requested")
|
|
17398
|
+
);
|
|
17399
|
+
let timeout;
|
|
17400
|
+
const settled = await Promise.race([
|
|
17401
|
+
previousRun.then(() => true),
|
|
17402
|
+
new Promise((resolve5) => {
|
|
17403
|
+
timeout = setTimeout(() => resolve5(false), budgetMs);
|
|
17404
|
+
})
|
|
17405
|
+
]);
|
|
17406
|
+
if (timeout) clearTimeout(timeout);
|
|
17407
|
+
if (!settled) {
|
|
17408
|
+
state.sessionArchiveAbortController?.abort(new Error("session archive flush budget expired"));
|
|
17409
|
+
logSessionArchiveActivity(state, "shutdown", {
|
|
17410
|
+
type: "info",
|
|
17411
|
+
level: "warn",
|
|
17412
|
+
message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
|
|
17413
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17414
|
+
});
|
|
17415
|
+
return null;
|
|
17416
|
+
}
|
|
17417
|
+
}
|
|
17418
|
+
const remainingMs = Math.floor(deadline - Date.now());
|
|
17419
|
+
if (remainingMs <= 0) {
|
|
17420
|
+
logSessionArchiveActivity(state, "shutdown", {
|
|
17421
|
+
type: "info",
|
|
17422
|
+
level: "warn",
|
|
17423
|
+
message: `Session archive exceeded ${budgetMs}ms while stopping a prior run; remaining sessions were not confirmed archived`,
|
|
17424
|
+
metadata: { outcome: "archive-budget-exceeded", budget_ms: budgetMs }
|
|
17425
|
+
});
|
|
17426
|
+
return null;
|
|
17427
|
+
}
|
|
17428
|
+
return runSessionArchiveTask(
|
|
17429
|
+
state,
|
|
17430
|
+
"shutdown",
|
|
17431
|
+
remainingMs,
|
|
17432
|
+
(signal) => runSessionArchiveFleet(state, "shutdown", remainingMs, signal)
|
|
17433
|
+
);
|
|
17434
|
+
}
|
|
17435
|
+
function scheduleSessionArchive(state) {
|
|
17436
|
+
const config = state.sessionArchiveConfig;
|
|
17437
|
+
if (!config.enabled) return;
|
|
17438
|
+
state.sessionArchiveWatermarkPath = sessionArchiveWatermarkPath(homedir8(), process.env);
|
|
17439
|
+
state.sessionArchiveWatermarks = readWatermarks(
|
|
17440
|
+
state.sessionArchiveWatermarkPath,
|
|
17441
|
+
(message) => logSessionArchiveActivity(state, "startup", {
|
|
17442
|
+
type: "info",
|
|
17443
|
+
level: "warn",
|
|
17444
|
+
message: redactDiagnosticText(message),
|
|
17445
|
+
metadata: { outcome: "watermark-read-failed" }
|
|
17446
|
+
})
|
|
17447
|
+
);
|
|
17448
|
+
logSessionArchiveActivity(state, "startup", {
|
|
17449
|
+
type: "info",
|
|
17450
|
+
message: `Session archive enabled (interval=${config.intervalMs}ms)`,
|
|
17451
|
+
metadata: { outcome: "armed", interval_ms: config.intervalMs }
|
|
17452
|
+
});
|
|
17453
|
+
const cycleBudgetMs = Math.min(
|
|
17454
|
+
config.intervalMs + SESSION_ARCHIVE_UPLOAD_TIMEOUT_MS,
|
|
17455
|
+
MAX_SESSION_ARCHIVE_TIMER_DELAY_MS
|
|
17456
|
+
);
|
|
17457
|
+
const run2 = (phase = "scheduled") => {
|
|
17458
|
+
void runSessionArchiveTask(
|
|
17459
|
+
state,
|
|
17460
|
+
phase,
|
|
17461
|
+
cycleBudgetMs,
|
|
17462
|
+
(signal) => runSessionArchiveFleet(state, phase, cycleBudgetMs, signal)
|
|
17463
|
+
);
|
|
17464
|
+
};
|
|
17465
|
+
const interval = setInterval(run2, config.intervalMs);
|
|
17466
|
+
const firstRun = setTimeout(() => run2(), SESSION_ARCHIVE_FIRST_RUN_MS);
|
|
17467
|
+
state.sessionArchiveTimers.push(interval, firstRun);
|
|
17468
|
+
}
|
|
15883
17469
|
function scheduleUsageReporting(state, params) {
|
|
15884
17470
|
const { mode, warnings } = params.resolved;
|
|
15885
17471
|
for (const warning2 of warnings) {
|
|
@@ -16154,6 +17740,12 @@ async function cleanup(state, opts = {}) {
|
|
|
16154
17740
|
clearTimeout(timer);
|
|
16155
17741
|
}
|
|
16156
17742
|
state.sessionCleanupTimers = [];
|
|
17743
|
+
for (const timer of state.sessionArchiveTimers) {
|
|
17744
|
+
clearInterval(timer);
|
|
17745
|
+
clearTimeout(timer);
|
|
17746
|
+
}
|
|
17747
|
+
state.sessionArchiveTimers = [];
|
|
17748
|
+
state.sessionArchiveAbortController?.abort(new Error("runner cleanup started"));
|
|
16157
17749
|
state.stopOpenCodeLogTail?.();
|
|
16158
17750
|
state.stopOpenCodeLogTail = null;
|
|
16159
17751
|
if (state.claudeUsageTimer) {
|
|
@@ -16221,6 +17813,15 @@ async function cleanup(state, opts = {}) {
|
|
|
16221
17813
|
await timeShutdownPhase(state, durations, "tunnel_close", () => connection.close());
|
|
16222
17814
|
state.connection = null;
|
|
16223
17815
|
}
|
|
17816
|
+
if (opts.graceful && state.sessionArchiveConfig.enabled) {
|
|
17817
|
+
const budgetMs = state.sessionArchiveConfig.flushBudgetMs;
|
|
17818
|
+
await timeShutdownPhase(
|
|
17819
|
+
state,
|
|
17820
|
+
durations,
|
|
17821
|
+
"session_archive_flush",
|
|
17822
|
+
() => runSessionArchiveShutdownFlush(state, budgetMs)
|
|
17823
|
+
);
|
|
17824
|
+
}
|
|
16224
17825
|
if (state.opencodeProcess) {
|
|
16225
17826
|
const opencodeProcess = state.opencodeProcess;
|
|
16226
17827
|
const result = await timeShutdownPhase(
|
|
@@ -16292,9 +17893,11 @@ async function run(options) {
|
|
|
16292
17893
|
const interactive = isInteractive(options.json);
|
|
16293
17894
|
let logLevel;
|
|
16294
17895
|
let fileSyncDirectories;
|
|
17896
|
+
let fileSyncExactFiles;
|
|
16295
17897
|
try {
|
|
16296
17898
|
logLevel = resolveLogLevel(options);
|
|
16297
17899
|
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir8());
|
|
17900
|
+
fileSyncExactFiles = resolveFileSyncExactFiles(options.enableFileSyncToExactFile, homedir8());
|
|
16298
17901
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
16299
17902
|
throw new Error(
|
|
16300
17903
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -16342,6 +17945,18 @@ async function run(options) {
|
|
|
16342
17945
|
messageCount: 0,
|
|
16343
17946
|
lastProxiedActivityAt: null,
|
|
16344
17947
|
sessionCleanupTimers: [],
|
|
17948
|
+
sessionArchiveConfig: resolveSessionArchiveConfig(
|
|
17949
|
+
{
|
|
17950
|
+
enabled: options.sessionArchive,
|
|
17951
|
+
interval: options.sessionArchiveInterval
|
|
17952
|
+
},
|
|
17953
|
+
process.env
|
|
17954
|
+
),
|
|
17955
|
+
sessionArchiveTimers: [],
|
|
17956
|
+
sessionArchiveWatermarks: /* @__PURE__ */ new Map(),
|
|
17957
|
+
sessionArchiveWatermarkPath: null,
|
|
17958
|
+
sessionArchiveInFlight: null,
|
|
17959
|
+
sessionArchiveAbortController: null,
|
|
16345
17960
|
sessionDeleteFailures: /* @__PURE__ */ new Map(),
|
|
16346
17961
|
claudeUsageTimer: null,
|
|
16347
17962
|
claudeUsageRearm: null,
|
|
@@ -16355,6 +17970,14 @@ async function run(options) {
|
|
|
16355
17970
|
};
|
|
16356
17971
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
16357
17972
|
setTelemetryDiagnosticsReporter((message) => log2(state, message, "warn"));
|
|
17973
|
+
for (const warning2 of state.sessionArchiveConfig.warnings) {
|
|
17974
|
+
logSessionArchiveActivity(state, "startup", {
|
|
17975
|
+
type: "info",
|
|
17976
|
+
level: "warn",
|
|
17977
|
+
message: `Session archive: ${warning2}`,
|
|
17978
|
+
metadata: { outcome: "configuration-warning" }
|
|
17979
|
+
});
|
|
17980
|
+
}
|
|
16358
17981
|
if (options.credentialSyncMarker) {
|
|
16359
17982
|
state.credentialSync = createCredentialSync({
|
|
16360
17983
|
markerPath: options.credentialSyncMarker,
|
|
@@ -16370,7 +17993,11 @@ async function run(options) {
|
|
|
16370
17993
|
}
|
|
16371
17994
|
if (fileSyncDirectories.length > 0) {
|
|
16372
17995
|
log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
|
|
16373
|
-
}
|
|
17996
|
+
}
|
|
17997
|
+
if (fileSyncExactFiles.length > 0) {
|
|
17998
|
+
log2(state, `File sync exact files enabled for: ${fileSyncExactFiles.join(", ")}`);
|
|
17999
|
+
}
|
|
18000
|
+
if (fileSyncDirectories.length === 0 && fileSyncExactFiles.length === 0) {
|
|
16374
18001
|
log2(state, "File sync is disabled (no --enable-file-sync-to given)", "debug");
|
|
16375
18002
|
}
|
|
16376
18003
|
if (!options.runner && options.agent) {
|
|
@@ -16652,6 +18279,7 @@ async function run(options) {
|
|
|
16652
18279
|
}
|
|
16653
18280
|
}
|
|
16654
18281
|
reportSessionDbRecovery(state);
|
|
18282
|
+
await drainSessionDeletionReports(state);
|
|
16655
18283
|
if (sessionDbVerifyFatal) {
|
|
16656
18284
|
throw new Error(
|
|
16657
18285
|
sessionDbVerifyFatalReason === void 0 ? "SESSION-DB-INTEGRITY-EXHAUSTED: the corrupt session DB could not be proven separated from the active backup prefix or removed from disk" : `SESSION-DB-VERIFY-TIMEOUT: session DB verification timed out (${sessionDbVerifyFatalReason}); refusing to start`
|
|
@@ -16671,13 +18299,75 @@ async function run(options) {
|
|
|
16671
18299
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
16672
18300
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
16673
18301
|
}
|
|
16674
|
-
const { version:
|
|
16675
|
-
options,
|
|
16676
|
-
process.env
|
|
16677
|
-
);
|
|
18302
|
+
const { version: opencodeVersionSelection, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(options, process.env);
|
|
16678
18303
|
for (const warning2 of opencodeVersionWarnings) {
|
|
16679
18304
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
16680
18305
|
}
|
|
18306
|
+
let opencodeMajor;
|
|
18307
|
+
let opencodeV2Command;
|
|
18308
|
+
let opencodeDetectionSource;
|
|
18309
|
+
let opencodeDetectionLevel = "info";
|
|
18310
|
+
let opencodeDetectionMessage;
|
|
18311
|
+
if (opencodeVersionSelection === "v1") {
|
|
18312
|
+
opencodeMajor = "v1";
|
|
18313
|
+
opencodeDetectionSource = "explicit";
|
|
18314
|
+
opencodeDetectionMessage = "OpenCode V1 was selected explicitly.";
|
|
18315
|
+
} else if (opencodeVersionSelection === "v2") {
|
|
18316
|
+
const installed = resolveInstalledOpenCode();
|
|
18317
|
+
opencodeMajor = "v2";
|
|
18318
|
+
opencodeDetectionSource = "explicit";
|
|
18319
|
+
if (installed.kind === "installed" && installed.major === "v2") {
|
|
18320
|
+
opencodeV2Command = installed.command;
|
|
18321
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; using ${installed.command}${installed.version ? ` (${installed.version})` : ""}.`;
|
|
18322
|
+
} else if (installed.kind === "unknown") {
|
|
18323
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported ${JSON.stringify(installed.rawVersion)}, but no V2 command was resolved.`;
|
|
18324
|
+
} else if (installed.kind === "installed") {
|
|
18325
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported V1 ${installed.version ?? "with an unknown version"}, so no V2 command was resolved.`;
|
|
18326
|
+
} else {
|
|
18327
|
+
opencodeDetectionMessage = "OpenCode V2 was selected explicitly, but no V2 command was resolved.";
|
|
18328
|
+
}
|
|
18329
|
+
} else {
|
|
18330
|
+
const running = await classifyRunningOpenCode(state.port);
|
|
18331
|
+
if (running.kind === "v1") {
|
|
18332
|
+
opencodeMajor = "v1";
|
|
18333
|
+
opencodeDetectionSource = "running-server";
|
|
18334
|
+
opencodeDetectionMessage = `OpenCode detection: found V1 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
|
|
18335
|
+
} else if (running.kind === "v2") {
|
|
18336
|
+
opencodeMajor = "v2";
|
|
18337
|
+
opencodeDetectionSource = "running-server";
|
|
18338
|
+
opencodeDetectionMessage = `OpenCode detection: found V2 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
|
|
18339
|
+
} else if (running.kind === "authenticated-unknown") {
|
|
18340
|
+
const message = `A server on port ${state.port} requires credentials this runner does not have. This can be a password-protected V1 or a V2 server, so the protocol cannot be established. Select it explicitly with --opencode-version v1 or --opencode-version v2.`;
|
|
18341
|
+
logActivity(state, {
|
|
18342
|
+
type: "info",
|
|
18343
|
+
level: "warn",
|
|
18344
|
+
message: `OpenCode detection: authenticated-unknown on port ${state.port}; ${running.detail}. ${message}`
|
|
18345
|
+
});
|
|
18346
|
+
throw new Error(message);
|
|
18347
|
+
} else {
|
|
18348
|
+
const installed = resolveInstalledOpenCode();
|
|
18349
|
+
opencodeDetectionLevel = "warn";
|
|
18350
|
+
if (installed.kind === "installed") {
|
|
18351
|
+
opencodeMajor = installed.major;
|
|
18352
|
+
opencodeDetectionSource = "installed-binary";
|
|
18353
|
+
if (installed.major === "v2") opencodeV2Command = installed.command;
|
|
18354
|
+
opencodeDetectionMessage = `OpenCode detection: no server identified on port ${state.port} (${running.detail}); using installed ${installed.major} command ${installed.command}${installed.version ? ` (${installed.version})` : " with an unknown version"}.`;
|
|
18355
|
+
} else if (installed.kind === "unknown") {
|
|
18356
|
+
opencodeMajor = "v1";
|
|
18357
|
+
opencodeDetectionSource = "fallback-v1";
|
|
18358
|
+
opencodeDetectionMessage = `OpenCode detection: no server identified on port ${state.port} (${running.detail}); ${installed.command} reported ${JSON.stringify(installed.rawVersion)}, so assuming V1. Use --opencode-version v2 to select V2 explicitly.`;
|
|
18359
|
+
} else {
|
|
18360
|
+
opencodeMajor = "v1";
|
|
18361
|
+
opencodeDetectionSource = "fallback-v1";
|
|
18362
|
+
opencodeDetectionMessage = `OpenCode detection: no server identified on port ${state.port} (${running.detail}); no recognizable OpenCode binary is installed, so continuing into the V1 install flow.`;
|
|
18363
|
+
}
|
|
18364
|
+
}
|
|
18365
|
+
}
|
|
18366
|
+
logActivity(state, {
|
|
18367
|
+
type: "info",
|
|
18368
|
+
level: opencodeDetectionLevel,
|
|
18369
|
+
message: opencodeDetectionMessage
|
|
18370
|
+
});
|
|
16681
18371
|
const managedSetupOptions = {
|
|
16682
18372
|
agentId: state.agentId,
|
|
16683
18373
|
apiUrl: getApiUrlConfig(),
|
|
@@ -16698,7 +18388,7 @@ async function run(options) {
|
|
|
16698
18388
|
managedPreparation = managedDiscovery;
|
|
16699
18389
|
managedSetupOptions.log(`MANAGED-INFERENCE-NOT-READY: ${managedDiscovery.reason}`, "warn");
|
|
16700
18390
|
} else if (managedDiscovery.kind === "setup") {
|
|
16701
|
-
const managedHealth =
|
|
18391
|
+
const managedHealth = opencodeMajor === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
|
|
16702
18392
|
managedPreparation = await prepareManagedInferenceSetup(
|
|
16703
18393
|
managedSetupOptions,
|
|
16704
18394
|
managedDiscovery,
|
|
@@ -16723,13 +18413,14 @@ async function run(options) {
|
|
|
16723
18413
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
16724
18414
|
const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
|
|
16725
18415
|
try {
|
|
16726
|
-
const oc =
|
|
18416
|
+
const oc = opencodeMajor === "v2" ? await ensureOpenCode2Running({
|
|
16727
18417
|
port: state.port,
|
|
16728
18418
|
interactive: state.interactive,
|
|
16729
18419
|
agentId: state.agentId,
|
|
16730
18420
|
log: (message) => log2(state, message),
|
|
16731
18421
|
startTimeoutMs: opencodeStartTimeoutMs,
|
|
16732
|
-
inheritStdio: Boolean(options.opencodePidFile)
|
|
18422
|
+
inheritStdio: Boolean(options.opencodePidFile),
|
|
18423
|
+
command: opencodeV2Command
|
|
16733
18424
|
}) : await ensureOpenCodeRunning({
|
|
16734
18425
|
port: state.port,
|
|
16735
18426
|
interactive: state.interactive,
|
|
@@ -16741,7 +18432,7 @@ async function run(options) {
|
|
|
16741
18432
|
state.port = oc.port;
|
|
16742
18433
|
state.opencodeProcess = options.opencodePidFile ? null : oc.process;
|
|
16743
18434
|
state.opencodeVersion = oc.version;
|
|
16744
|
-
state.opencodeApiVersion =
|
|
18435
|
+
state.opencodeApiVersion = opencodeMajor;
|
|
16745
18436
|
let configuredPlugins = [];
|
|
16746
18437
|
let resolvedPlugins = [];
|
|
16747
18438
|
try {
|
|
@@ -16763,13 +18454,14 @@ async function run(options) {
|
|
|
16763
18454
|
metadata: {
|
|
16764
18455
|
opencode_version: state.opencodeVersion,
|
|
16765
18456
|
opencode_api_version: state.opencodeApiVersion,
|
|
18457
|
+
opencode_detection_source: opencodeDetectionSource,
|
|
16766
18458
|
configured_plugins: JSON.stringify(configuredPlugins),
|
|
16767
18459
|
resolved_plugins: JSON.stringify(resolvedPlugins)
|
|
16768
18460
|
},
|
|
16769
18461
|
source: "cli.runtime-provenance"
|
|
16770
18462
|
});
|
|
16771
18463
|
let opencodePassword = null;
|
|
16772
|
-
if (
|
|
18464
|
+
if (opencodeMajor === "v2" && "password" in oc) {
|
|
16773
18465
|
const value = oc.password;
|
|
16774
18466
|
if (typeof value === "string" || value === null) opencodePassword = value;
|
|
16775
18467
|
}
|
|
@@ -16782,7 +18474,7 @@ async function run(options) {
|
|
|
16782
18474
|
state.opencodeClient = openCodeClient;
|
|
16783
18475
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
16784
18476
|
try {
|
|
16785
|
-
|
|
18477
|
+
writeFileSync9(options.opencodePidFile, `${oc.process.pid}
|
|
16786
18478
|
`, { mode: 384 });
|
|
16787
18479
|
chmodSync3(options.opencodePidFile, 384);
|
|
16788
18480
|
} catch (error2) {
|
|
@@ -16892,7 +18584,7 @@ async function run(options) {
|
|
|
16892
18584
|
let existingPid;
|
|
16893
18585
|
if (existsSync3(options.litestreamPidFile)) {
|
|
16894
18586
|
try {
|
|
16895
|
-
const rawPid =
|
|
18587
|
+
const rawPid = readFileSync12(options.litestreamPidFile, "utf8").trim();
|
|
16896
18588
|
const parsedPid = Number(rawPid);
|
|
16897
18589
|
if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
|
|
16898
18590
|
existingPid = parsedPid;
|
|
@@ -16929,7 +18621,7 @@ async function run(options) {
|
|
|
16929
18621
|
});
|
|
16930
18622
|
try {
|
|
16931
18623
|
if (litestreamProcess.pid !== void 0) {
|
|
16932
|
-
|
|
18624
|
+
writeFileSync9(options.litestreamPidFile, `${litestreamProcess.pid}
|
|
16933
18625
|
`, {
|
|
16934
18626
|
mode: 384
|
|
16935
18627
|
});
|
|
@@ -16979,6 +18671,15 @@ async function run(options) {
|
|
|
16979
18671
|
log2(state, `Started litestream replication with config ${options.litestreamConfig}`);
|
|
16980
18672
|
}
|
|
16981
18673
|
const tunnelSpinner = interactive && !state.json ? ora4("Connecting tunnel...").start() : null;
|
|
18674
|
+
const recordBinding = state.sessionArchiveConfig.enabled ? (conversationId, sessionId) => appendSessionBinding(
|
|
18675
|
+
sessionBindingLedgerPath(homedir8(), process.env),
|
|
18676
|
+
{
|
|
18677
|
+
conversation_id: conversationId,
|
|
18678
|
+
opencode_session_id: sessionId,
|
|
18679
|
+
observed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
18680
|
+
},
|
|
18681
|
+
(message) => logActivity(state, { type: "info", level: "warn", message })
|
|
18682
|
+
) : void 0;
|
|
16982
18683
|
const channelDriver = new ChannelDriver({
|
|
16983
18684
|
agentId: state.agentId,
|
|
16984
18685
|
port: state.port,
|
|
@@ -16988,9 +18689,11 @@ async function run(options) {
|
|
|
16988
18689
|
conversationFilter: state.conversationFilter,
|
|
16989
18690
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
16990
18691
|
sessionStoreHealth: state.sessionStoreHealth,
|
|
16991
|
-
|
|
16992
|
-
//
|
|
18692
|
+
recordBinding,
|
|
18693
|
+
// #559: the file-sync allow-lists. Empty ⇒ pending files are REJECTED with
|
|
18694
|
+
// `file_sync_disabled` on the ack, not silently ignored.
|
|
16993
18695
|
fileSyncDirectories,
|
|
18696
|
+
fileSyncExactFiles,
|
|
16994
18697
|
homeDir: homedir8(),
|
|
16995
18698
|
maxActiveSessions,
|
|
16996
18699
|
log: (entry) => (
|
|
@@ -17166,6 +18869,7 @@ async function run(options) {
|
|
|
17166
18869
|
}
|
|
17167
18870
|
if (!state.running) return;
|
|
17168
18871
|
scheduleSessionCleanup(state, channelDriver, options);
|
|
18872
|
+
scheduleSessionArchive(state);
|
|
17169
18873
|
state.claudeUsageRearm = scheduleClaudeUsageReporting(state, options);
|
|
17170
18874
|
state.openaiUsageRearm = scheduleUsageReporting(state, {
|
|
17171
18875
|
label: "OpenAI",
|
|
@@ -17261,8 +18965,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17261
18965
|
"--opencode-start-timeout <seconds>",
|
|
17262
18966
|
"Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
|
|
17263
18967
|
).option(
|
|
17264
|
-
"--opencode-version <v1|v2>",
|
|
17265
|
-
"
|
|
18968
|
+
"--opencode-version <v1|v2|auto>",
|
|
18969
|
+
"OpenCode major version: auto (default) detects the running server, then installed binary; v1 and v2 skip server-protocol detection. Env: EVIDENT_OPENCODE_VERSION"
|
|
17266
18970
|
).option("--json", "Output in JSON format").option(
|
|
17267
18971
|
"--session-cleanup-max-age <duration>",
|
|
17268
18972
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
@@ -17275,6 +18979,12 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17275
18979
|
).option(
|
|
17276
18980
|
"--session-cleanup-interval <duration>",
|
|
17277
18981
|
"How often the cleanup sweep runs (default: 1h). Env: EVIDENT_SESSION_CLEANUP_INTERVAL"
|
|
18982
|
+
).option(
|
|
18983
|
+
"--session-archive",
|
|
18984
|
+
"Upload full OpenCode transcripts to the runner archive (off by default). Env: EVIDENT_SESSION_ARCHIVE"
|
|
18985
|
+
).option(
|
|
18986
|
+
"--session-archive-interval <duration>",
|
|
18987
|
+
"How often session archives are uploaded (default: 1h). Env: EVIDENT_SESSION_ARCHIVE_INTERVAL"
|
|
17278
18988
|
).option(
|
|
17279
18989
|
"--claude-usage-reporting <mode>",
|
|
17280
18990
|
"Report Claude subscription usage to Evident: auto | on | off (default: auto). Env: EVIDENT_CLAUDE_USAGE_REPORTING"
|
|
@@ -17289,6 +18999,11 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17289
18999
|
"Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.",
|
|
17290
19000
|
(value, previous) => previous.concat([value]),
|
|
17291
19001
|
[]
|
|
19002
|
+
).option(
|
|
19003
|
+
"--enable-file-sync-to-exact-file <path>",
|
|
19004
|
+
"Allow Evident to write files to exactly this path (repeatable).",
|
|
19005
|
+
(value, previous) => previous.concat([value]),
|
|
19006
|
+
[]
|
|
17292
19007
|
).option(
|
|
17293
19008
|
"--tunnel-ready-file <path>",
|
|
17294
19009
|
"Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
|
|
@@ -17341,6 +19056,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17341
19056
|
sessionCleanupMaxCount: options.sessionCleanupMaxCount,
|
|
17342
19057
|
maxActiveSessions: options.maxActiveSessions,
|
|
17343
19058
|
sessionCleanupInterval: options.sessionCleanupInterval,
|
|
19059
|
+
sessionArchive: options.sessionArchive,
|
|
19060
|
+
sessionArchiveInterval: options.sessionArchiveInterval,
|
|
17344
19061
|
// Raw string — the resolver in run.ts single-sources parsing
|
|
17345
19062
|
// (resolveClaudeUsageReportingMode).
|
|
17346
19063
|
claudeUsageReporting: options.claudeUsageReporting,
|
|
@@ -17351,6 +19068,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17351
19068
|
// Raw values — expansion/validation is single-sourced in run.ts's
|
|
17352
19069
|
// resolveFileSyncDirectories.
|
|
17353
19070
|
enableFileSyncTo: options.enableFileSyncTo,
|
|
19071
|
+
// Raw values — expansion/validation is single-sourced in run.ts's
|
|
19072
|
+
// resolveFileSyncExactFiles.
|
|
19073
|
+
enableFileSyncToExactFile: options.enableFileSyncToExactFile,
|
|
17354
19074
|
tunnelReadyFile: options.tunnelReadyFile,
|
|
17355
19075
|
litestreamConfig: options.litestreamConfig,
|
|
17356
19076
|
opencodePidFile: options.opencodePidFile,
|