@evident-ai/cli 3.6.1-dev.bbdfbd6 → 3.6.1-dev.c852a6f
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 +14 -5
- package/dist/index.js +1059 -170
- 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 readFileSync9, writeFileSync as writeFileSync7 } 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 join14, 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.c852a6f" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1446
1449
|
function getCliVersion() {
|
|
1447
1450
|
return CLI_VERSION;
|
|
1448
1451
|
}
|
|
@@ -2638,6 +2641,122 @@ function isSessionDbRecoveryRecord(value) {
|
|
|
2638
2641
|
);
|
|
2639
2642
|
}
|
|
2640
2643
|
|
|
2644
|
+
// src/lib/opencode/session-deletion-report.ts
|
|
2645
|
+
import { appendFileSync, mkdirSync, readFileSync as readFileSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
|
|
2646
|
+
import { dirname as dirname4, join as join4 } from "node:path";
|
|
2647
|
+
var SESSION_DELETION_SPOOL_MAX_RECORDS = 1e3;
|
|
2648
|
+
var MAX_DIAGNOSTIC_LENGTH = 200;
|
|
2649
|
+
function sessionDeletionReportPath(homeDir, env) {
|
|
2650
|
+
const override = env.EVIDENT_SESSION_DELETION_REPORT?.trim();
|
|
2651
|
+
return override || join4(homeDir, ".local", "state", "evident", "session-deletions.jsonl");
|
|
2652
|
+
}
|
|
2653
|
+
function appendSessionDeletionRecords(path, records) {
|
|
2654
|
+
if (records.length === 0) return 0;
|
|
2655
|
+
mkdirSync(dirname4(path), { recursive: true });
|
|
2656
|
+
const current = readSessionDeletionRecords(path);
|
|
2657
|
+
if (current.readError !== null) {
|
|
2658
|
+
throw new Error(`cannot enforce the spool limit after reading ${path}: ${current.readError}`);
|
|
2659
|
+
}
|
|
2660
|
+
const combined = [...current.records, ...records];
|
|
2661
|
+
const kept = combined.slice(-SESSION_DELETION_SPOOL_MAX_RECORDS);
|
|
2662
|
+
const dropped = combined.length - kept.length;
|
|
2663
|
+
if (dropped > 0) {
|
|
2664
|
+
writeFileSync(path, `${kept.map((record) => JSON.stringify(record)).join("\n")}
|
|
2665
|
+
`, "utf8");
|
|
2666
|
+
} else {
|
|
2667
|
+
appendFileSync(path, `${records.map((record) => JSON.stringify(record)).join("\n")}
|
|
2668
|
+
`, "utf8");
|
|
2669
|
+
}
|
|
2670
|
+
return dropped;
|
|
2671
|
+
}
|
|
2672
|
+
function readSessionDeletionRecords(path) {
|
|
2673
|
+
let content;
|
|
2674
|
+
try {
|
|
2675
|
+
content = readFileSync3(path, "utf8");
|
|
2676
|
+
} catch (error2) {
|
|
2677
|
+
if (isMissingFile(error2)) return { records: [], skippedLines: 0, readError: null };
|
|
2678
|
+
const readError = boundedError(error2);
|
|
2679
|
+
console.error(`[session-deletion-report] could not read ${path}: ${readError}`);
|
|
2680
|
+
return { records: [], skippedLines: 0, readError };
|
|
2681
|
+
}
|
|
2682
|
+
let skippedLines = 0;
|
|
2683
|
+
const records = content.split("\n").flatMap((line) => {
|
|
2684
|
+
if (!line.trim()) return [];
|
|
2685
|
+
const record = parseRecord(line, path);
|
|
2686
|
+
if (record) return [record];
|
|
2687
|
+
skippedLines++;
|
|
2688
|
+
return [];
|
|
2689
|
+
});
|
|
2690
|
+
return { records, skippedLines, readError: null };
|
|
2691
|
+
}
|
|
2692
|
+
function removeDeliveredSessionDeletionRecords(path, delivered) {
|
|
2693
|
+
let content;
|
|
2694
|
+
try {
|
|
2695
|
+
content = readFileSync3(path, "utf8");
|
|
2696
|
+
} catch (error2) {
|
|
2697
|
+
if (isMissingFile(error2)) return;
|
|
2698
|
+
console.error(
|
|
2699
|
+
`[session-deletion-report] could not read ${path} before removing delivered records: ${boundedError(error2)}`
|
|
2700
|
+
);
|
|
2701
|
+
throw error2;
|
|
2702
|
+
}
|
|
2703
|
+
const toRemove = /* @__PURE__ */ new Map();
|
|
2704
|
+
for (const record of delivered) {
|
|
2705
|
+
toRemove.set(record.opencode_session_id, (toRemove.get(record.opencode_session_id) ?? 0) + 1);
|
|
2706
|
+
}
|
|
2707
|
+
const remaining = [];
|
|
2708
|
+
for (const line of content.split("\n")) {
|
|
2709
|
+
if (!line.trim()) continue;
|
|
2710
|
+
const record = parseRecord(line, path);
|
|
2711
|
+
const count = record ? toRemove.get(record.opencode_session_id) ?? 0 : 0;
|
|
2712
|
+
if (record && count > 0) {
|
|
2713
|
+
if (count === 1) toRemove.delete(record.opencode_session_id);
|
|
2714
|
+
else toRemove.set(record.opencode_session_id, count - 1);
|
|
2715
|
+
} else {
|
|
2716
|
+
remaining.push(line);
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
try {
|
|
2720
|
+
if (remaining.length === 0) unlinkSync2(path);
|
|
2721
|
+
else writeFileSync(path, `${remaining.join("\n")}
|
|
2722
|
+
`, "utf8");
|
|
2723
|
+
} catch (error2) {
|
|
2724
|
+
if (isMissingFile(error2)) return;
|
|
2725
|
+
console.error(
|
|
2726
|
+
`[session-deletion-report] could not write ${path} after removing delivered records: ${boundedError(error2)}`
|
|
2727
|
+
);
|
|
2728
|
+
throw error2;
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
function parseRecord(line, path) {
|
|
2732
|
+
try {
|
|
2733
|
+
const value = JSON.parse(line);
|
|
2734
|
+
if (isSessionDeletionRecord(value)) return value;
|
|
2735
|
+
console.error(`[session-deletion-report] skipped malformed record in ${path}: invalid shape`);
|
|
2736
|
+
return null;
|
|
2737
|
+
} catch (error2) {
|
|
2738
|
+
console.error(
|
|
2739
|
+
`[session-deletion-report] skipped malformed record in ${path}: ${boundedError(error2)}`
|
|
2740
|
+
);
|
|
2741
|
+
return null;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
function isSessionDeletionRecord(value) {
|
|
2745
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2746
|
+
const record = value;
|
|
2747
|
+
const policy = record.policy;
|
|
2748
|
+
if (!policy || typeof policy !== "object" || Array.isArray(policy)) return false;
|
|
2749
|
+
const fields = policy;
|
|
2750
|
+
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");
|
|
2751
|
+
}
|
|
2752
|
+
function isMissingFile(error2) {
|
|
2753
|
+
return error2 instanceof Error && "code" in error2 && error2.code === "ENOENT";
|
|
2754
|
+
}
|
|
2755
|
+
function boundedError(error2) {
|
|
2756
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2757
|
+
return message.slice(0, MAX_DIAGNOSTIC_LENGTH);
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2641
2760
|
// src/lib/opencode/auth.ts
|
|
2642
2761
|
function buildOpenCodeBasicAuthHeader(password) {
|
|
2643
2762
|
return `Basic ${Buffer.from(["opencode", password].join(":")).toString("base64")}`;
|
|
@@ -2656,8 +2775,26 @@ async function checkOpenCodeHealth(port) {
|
|
|
2656
2775
|
if (!response.ok) {
|
|
2657
2776
|
return { healthy: false, error: `HTTP ${response.status}` };
|
|
2658
2777
|
}
|
|
2659
|
-
const
|
|
2660
|
-
|
|
2778
|
+
const body = await response.text();
|
|
2779
|
+
let data;
|
|
2780
|
+
try {
|
|
2781
|
+
data = JSON.parse(body);
|
|
2782
|
+
} catch (error2) {
|
|
2783
|
+
return {
|
|
2784
|
+
healthy: false,
|
|
2785
|
+
okWithNonJsonBody: true,
|
|
2786
|
+
error: `HTTP ${response.status}: invalid JSON health response${bodyExcerpt(body)}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
2790
|
+
return {
|
|
2791
|
+
healthy: false,
|
|
2792
|
+
okWithNonJsonBody: true,
|
|
2793
|
+
error: `HTTP ${response.status}: invalid health response${bodyExcerpt(body)}: expected JSON object`
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
const version2 = data.version;
|
|
2797
|
+
return typeof version2 === "string" ? { healthy: true, version: version2 } : { healthy: true };
|
|
2661
2798
|
} catch (error2) {
|
|
2662
2799
|
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
2663
2800
|
return { healthy: false, error: message };
|
|
@@ -2673,10 +2810,17 @@ async function checkOpenCode2Health(port, password) {
|
|
|
2673
2810
|
if (password !== void 0) {
|
|
2674
2811
|
headers.Authorization = buildOpenCodeBasicAuthHeader(password);
|
|
2675
2812
|
}
|
|
2676
|
-
|
|
2813
|
+
let response = await fetch(`http://127.0.0.1:${port}/api/info`, {
|
|
2677
2814
|
headers,
|
|
2678
2815
|
signal: AbortSignal.timeout(2e3)
|
|
2679
2816
|
});
|
|
2817
|
+
const usesHealthFallback = response.status === 404;
|
|
2818
|
+
if (usesHealthFallback) {
|
|
2819
|
+
response = await fetch(`http://127.0.0.1:${port}/api/health`, {
|
|
2820
|
+
headers,
|
|
2821
|
+
signal: AbortSignal.timeout(2e3)
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2680
2824
|
if (response.status === 401) {
|
|
2681
2825
|
return { healthy: false, authFailed: true, error: "HTTP 401" };
|
|
2682
2826
|
}
|
|
@@ -2691,23 +2835,29 @@ async function checkOpenCode2Health(port, password) {
|
|
|
2691
2835
|
} catch (error2) {
|
|
2692
2836
|
return {
|
|
2693
2837
|
healthy: false,
|
|
2694
|
-
error: `
|
|
2838
|
+
error: `HTTP ${response.status}: invalid JSON health response${excerpt}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2695
2839
|
};
|
|
2696
2840
|
}
|
|
2697
2841
|
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
2698
2842
|
return {
|
|
2699
2843
|
healthy: false,
|
|
2700
|
-
error: `
|
|
2844
|
+
error: `HTTP ${response.status}: invalid health response${excerpt}: expected healthy=true`
|
|
2701
2845
|
};
|
|
2702
2846
|
}
|
|
2703
|
-
const
|
|
2704
|
-
if (
|
|
2847
|
+
const bodyObject = data;
|
|
2848
|
+
if (usesHealthFallback && bodyObject.healthy !== true) {
|
|
2705
2849
|
return {
|
|
2706
2850
|
healthy: false,
|
|
2707
2851
|
error: `Invalid health response${excerpt}: expected healthy=true`
|
|
2708
2852
|
};
|
|
2709
2853
|
}
|
|
2710
|
-
|
|
2854
|
+
if (!usesHealthFallback) {
|
|
2855
|
+
return typeof bodyObject.version === "string" && bodyObject.version.length > 0 ? { healthy: true, version: bodyObject.version } : {
|
|
2856
|
+
healthy: false,
|
|
2857
|
+
error: `HTTP ${response.status}: invalid info response${excerpt}: expected a non-empty version`
|
|
2858
|
+
};
|
|
2859
|
+
}
|
|
2860
|
+
return typeof bodyObject.version === "string" ? { healthy: true, version: bodyObject.version } : { healthy: true };
|
|
2711
2861
|
} catch (error2) {
|
|
2712
2862
|
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
2713
2863
|
return { healthy: false, error: message };
|
|
@@ -3209,6 +3359,52 @@ async function getModelAttachmentCapability(port, model, client) {
|
|
|
3209
3359
|
return null;
|
|
3210
3360
|
}
|
|
3211
3361
|
}
|
|
3362
|
+
async function isPinnedProviderLoaded(port, model, client) {
|
|
3363
|
+
if (client?.version === "v2") return null;
|
|
3364
|
+
const { model: baseModel } = splitModelVariant(model);
|
|
3365
|
+
if (!baseModel) return null;
|
|
3366
|
+
const slash = baseModel.indexOf("/");
|
|
3367
|
+
if (slash <= 0) return null;
|
|
3368
|
+
const providerId = baseModel.slice(0, slash);
|
|
3369
|
+
try {
|
|
3370
|
+
const res = await requestWithClient(port, client, "/config/providers");
|
|
3371
|
+
if (!res.ok) {
|
|
3372
|
+
console.error(
|
|
3373
|
+
`[isPinnedProviderLoaded] GET /config/providers returned HTTP ${res.status} (port ${port})`
|
|
3374
|
+
);
|
|
3375
|
+
return null;
|
|
3376
|
+
}
|
|
3377
|
+
const body = await res.json();
|
|
3378
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
3379
|
+
console.error(
|
|
3380
|
+
`[isPinnedProviderLoaded] GET /config/providers body was not a plain object (port ${port})`
|
|
3381
|
+
);
|
|
3382
|
+
return null;
|
|
3383
|
+
}
|
|
3384
|
+
const providers = body.providers;
|
|
3385
|
+
if (!Array.isArray(providers)) {
|
|
3386
|
+
console.error(
|
|
3387
|
+
`[isPinnedProviderLoaded] GET /config/providers body had no providers array (port ${port})`
|
|
3388
|
+
);
|
|
3389
|
+
return null;
|
|
3390
|
+
}
|
|
3391
|
+
const providerIds = providers.filter(
|
|
3392
|
+
(provider) => provider !== null && typeof provider === "object" && typeof provider.id === "string"
|
|
3393
|
+
).map((provider) => provider.id);
|
|
3394
|
+
if (providerIds.length === 0) {
|
|
3395
|
+
console.error(
|
|
3396
|
+
`[isPinnedProviderLoaded] GET /config/providers had no provider entries with string ids (port ${port})`
|
|
3397
|
+
);
|
|
3398
|
+
return null;
|
|
3399
|
+
}
|
|
3400
|
+
return providerIds.includes(providerId);
|
|
3401
|
+
} catch (err) {
|
|
3402
|
+
console.error(
|
|
3403
|
+
`[isPinnedProviderLoaded] GET /config/providers failed (port ${port}): ${err instanceof Error ? err.message : String(err)}`
|
|
3404
|
+
);
|
|
3405
|
+
return null;
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3212
3408
|
async function buildFileParts(attachments, capable) {
|
|
3213
3409
|
const outcomes = [];
|
|
3214
3410
|
const parts = [];
|
|
@@ -3623,9 +3819,17 @@ function isPreamblePinnedRunning(messages, userMessageId) {
|
|
|
3623
3819
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
3624
3820
|
return completedOf(reply) != null && finishOf(reply) === "tool-calls";
|
|
3625
3821
|
}
|
|
3822
|
+
function isMidFlightPinnedRunning(messages, userMessageId) {
|
|
3823
|
+
if (messageRunState(messages, userMessageId) !== "running") return false;
|
|
3824
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
3825
|
+
return reply !== null && isAssistantInFlight(reply) && completedOf(reply) == null;
|
|
3826
|
+
}
|
|
3626
3827
|
function isB2AbandonmentConfirmed(params) {
|
|
3627
3828
|
return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false && params.rootOngoing === false;
|
|
3628
3829
|
}
|
|
3830
|
+
function isMidFlightOrphanConfirmed(params) {
|
|
3831
|
+
return params.pinnedForMs >= params.minPinnedMs && params.notOngoingStreak >= params.minNotOngoingStreak;
|
|
3832
|
+
}
|
|
3629
3833
|
function isAmbiguousTerminalFinish(m) {
|
|
3630
3834
|
if (completedOf(m) == null) return false;
|
|
3631
3835
|
if (errorOf(m) != null) return false;
|
|
@@ -4491,13 +4695,13 @@ async function readV2SessionErrorStream(client, options) {
|
|
|
4491
4695
|
}
|
|
4492
4696
|
|
|
4493
4697
|
// src/lib/opencode/credential-import-v2.ts
|
|
4494
|
-
import { readFileSync as
|
|
4495
|
-
import { join as
|
|
4698
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
4699
|
+
import { join as join6 } from "node:path";
|
|
4496
4700
|
|
|
4497
4701
|
// src/lib/openai-usage.ts
|
|
4498
|
-
import { readFileSync as
|
|
4702
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
4499
4703
|
import { homedir as homedir3 } from "node:os";
|
|
4500
|
-
import { join as
|
|
4704
|
+
import { join as join5 } from "node:path";
|
|
4501
4705
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
4502
4706
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
4503
4707
|
var OpenAiUsageError = class extends Error {
|
|
@@ -4511,7 +4715,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
4511
4715
|
}
|
|
4512
4716
|
function readOpenCodeChatGptCredentials() {
|
|
4513
4717
|
try {
|
|
4514
|
-
const raw =
|
|
4718
|
+
const raw = readFileSync4(join5(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
4515
4719
|
let parsed;
|
|
4516
4720
|
try {
|
|
4517
4721
|
parsed = JSON.parse(raw);
|
|
@@ -4743,7 +4947,7 @@ function emptyResult(unsupported = [], outcome = "completed") {
|
|
|
4743
4947
|
}
|
|
4744
4948
|
function readOpenCodeCredentials(homeDir, log3) {
|
|
4745
4949
|
try {
|
|
4746
|
-
const raw =
|
|
4950
|
+
const raw = readFileSync5(join6(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
|
|
4747
4951
|
let parsed;
|
|
4748
4952
|
try {
|
|
4749
4953
|
parsed = JSON.parse(raw);
|
|
@@ -4943,9 +5147,9 @@ async function importOpenCodeV2Credentials({
|
|
|
4943
5147
|
|
|
4944
5148
|
// src/lib/opencode/session-db-boot.ts
|
|
4945
5149
|
import { spawn as spawn2 } from "node:child_process";
|
|
4946
|
-
import { mkdirSync, statSync as statSync3, unlinkSync as
|
|
5150
|
+
import { mkdirSync as mkdirSync2, statSync as statSync3, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4947
5151
|
import { homedir as homedir4 } from "node:os";
|
|
4948
|
-
import { dirname as
|
|
5152
|
+
import { dirname as dirname5, resolve as resolvePath } from "node:path";
|
|
4949
5153
|
|
|
4950
5154
|
// src/lib/runner-synchroniser.ts
|
|
4951
5155
|
import { spawn } from "node:child_process";
|
|
@@ -5061,7 +5265,7 @@ function reportRecord(stage, outcome, reason, litestreamExitCode, options) {
|
|
|
5061
5265
|
function clearMarker(options) {
|
|
5062
5266
|
if (!options.noReplicateMarker) return;
|
|
5063
5267
|
try {
|
|
5064
|
-
|
|
5268
|
+
unlinkSync3(options.noReplicateMarker);
|
|
5065
5269
|
} catch (error2) {
|
|
5066
5270
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return;
|
|
5067
5271
|
options.log(
|
|
@@ -5073,8 +5277,8 @@ function clearMarker(options) {
|
|
|
5073
5277
|
function markNoReplicate(options, message) {
|
|
5074
5278
|
if (options.noReplicateMarker) {
|
|
5075
5279
|
try {
|
|
5076
|
-
|
|
5077
|
-
|
|
5280
|
+
mkdirSync2(dirname5(options.noReplicateMarker), { recursive: true });
|
|
5281
|
+
writeFileSync2(options.noReplicateMarker, "");
|
|
5078
5282
|
} catch (error2) {
|
|
5079
5283
|
options.log(
|
|
5080
5284
|
`Could not write session-DB no-replicate marker ${options.noReplicateMarker}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
@@ -5087,7 +5291,7 @@ function markNoReplicate(options, message) {
|
|
|
5087
5291
|
function discardSessionDbDebris(options) {
|
|
5088
5292
|
for (const path of [options.dbPath, `${options.dbPath}-wal`, `${options.dbPath}-shm`]) {
|
|
5089
5293
|
try {
|
|
5090
|
-
|
|
5294
|
+
unlinkSync3(path);
|
|
5091
5295
|
} catch (error2) {
|
|
5092
5296
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") continue;
|
|
5093
5297
|
options.log(
|
|
@@ -5228,8 +5432,8 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
5228
5432
|
return null;
|
|
5229
5433
|
}
|
|
5230
5434
|
try {
|
|
5231
|
-
|
|
5232
|
-
|
|
5435
|
+
mkdirSync2(dirname5(configPath), { recursive: true });
|
|
5436
|
+
writeFileSync2(configPath, rendered.stdout);
|
|
5233
5437
|
} catch (error2) {
|
|
5234
5438
|
options.log(
|
|
5235
5439
|
`Could not write ${configPath}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
@@ -5494,8 +5698,8 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
5494
5698
|
|
|
5495
5699
|
// src/lib/opencode/session-db-provenance.ts
|
|
5496
5700
|
import { createRequire } from "node:module";
|
|
5497
|
-
import { mkdirSync as
|
|
5498
|
-
import { dirname as
|
|
5701
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5702
|
+
import { dirname as dirname6, join as join7 } from "node:path";
|
|
5499
5703
|
var require2 = createRequire(import.meta.url);
|
|
5500
5704
|
function readSessionDbMigrationIds(dbPath) {
|
|
5501
5705
|
let db;
|
|
@@ -5532,12 +5736,12 @@ function readSessionDbMigrationIds(dbPath) {
|
|
|
5532
5736
|
}
|
|
5533
5737
|
function sessionDbProvenanceStatePath(homeDir, env) {
|
|
5534
5738
|
const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
|
|
5535
|
-
return override ||
|
|
5739
|
+
return override || join7(homeDir, ".local", "state", "evident", "session-db-provenance.json");
|
|
5536
5740
|
}
|
|
5537
5741
|
function loadSessionDbProvenanceState(path) {
|
|
5538
5742
|
let value;
|
|
5539
5743
|
try {
|
|
5540
|
-
value = JSON.parse(
|
|
5744
|
+
value = JSON.parse(readFileSync6(path, "utf8"));
|
|
5541
5745
|
} catch (error2) {
|
|
5542
5746
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return {};
|
|
5543
5747
|
console.error(
|
|
@@ -5561,8 +5765,8 @@ function loadSessionDbProvenanceState(path) {
|
|
|
5561
5765
|
}
|
|
5562
5766
|
function saveSessionDbProvenanceState(path, state) {
|
|
5563
5767
|
try {
|
|
5564
|
-
|
|
5565
|
-
|
|
5768
|
+
mkdirSync3(dirname6(path), { recursive: true });
|
|
5769
|
+
writeFileSync3(path, `${JSON.stringify(state, null, 2)}
|
|
5566
5770
|
`, "utf8");
|
|
5567
5771
|
} catch (error2) {
|
|
5568
5772
|
console.error(
|
|
@@ -5617,6 +5821,26 @@ function isSessionDbProvenanceRecord(value) {
|
|
|
5617
5821
|
return typeof record.opencodeVersion === "string" && Array.isArray(record.migrationIds) && record.migrationIds.every((id) => typeof id === "string") && typeof record.updatedAt === "string";
|
|
5618
5822
|
}
|
|
5619
5823
|
|
|
5824
|
+
// src/lib/opencode/detect.ts
|
|
5825
|
+
function describeProbe(route, result) {
|
|
5826
|
+
if (result.error?.startsWith("HTTP ")) return `${route}: ${result.error}`;
|
|
5827
|
+
if (result.error) return `${route}: no HTTP response (${result.error})`;
|
|
5828
|
+
return `${route}: ${result.healthy ? "healthy" : "no response detail"}`;
|
|
5829
|
+
}
|
|
5830
|
+
async function classifyRunningOpenCode(port) {
|
|
5831
|
+
const v1 = await checkOpenCodeHealth(port);
|
|
5832
|
+
if (v1.healthy) return { kind: "v1", version: v1.version ?? null };
|
|
5833
|
+
const v2 = await checkOpenCode2Health(port);
|
|
5834
|
+
if (v2.healthy) return { kind: "v2", version: v2.version ?? null };
|
|
5835
|
+
const detail = [
|
|
5836
|
+
describeProbe("V1 /global/health", v1),
|
|
5837
|
+
describeProbe("V2 /api/info (or /api/health fallback)", v2)
|
|
5838
|
+
].join("; ");
|
|
5839
|
+
if (v1.okWithNonJsonBody && v2.authFailed) return { kind: "v2", version: null };
|
|
5840
|
+
if (v1.authFailed && v2.authFailed) return { kind: "authenticated-unknown", detail };
|
|
5841
|
+
return { kind: "no-server-identified", detail };
|
|
5842
|
+
}
|
|
5843
|
+
|
|
5620
5844
|
// src/lib/opencode/opencode2-package.ts
|
|
5621
5845
|
var OPENCODE2_PACKAGE_SPEC = "@opencode-ai/cli@0.0.0-beta-17823";
|
|
5622
5846
|
|
|
@@ -5925,26 +6149,28 @@ async function startOpenCode(port, options = {}) {
|
|
|
5925
6149
|
}
|
|
5926
6150
|
async function startOpenCode2(port, options = {}) {
|
|
5927
6151
|
const password = randomBytes(24).toString("hex");
|
|
5928
|
-
let command = "opencode2";
|
|
6152
|
+
let command = options.command ?? "opencode2";
|
|
5929
6153
|
const logLevel = options.inheritStdio ? ["--log-level", resolveOpenCode2LogLevel(process.env)] : [];
|
|
5930
6154
|
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1", ...logLevel];
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
6155
|
+
if (options.command === void 0) {
|
|
6156
|
+
try {
|
|
6157
|
+
execSync("which opencode2", { stdio: "ignore" });
|
|
6158
|
+
} catch {
|
|
6159
|
+
command = "npx";
|
|
6160
|
+
args = [
|
|
6161
|
+
"-y",
|
|
6162
|
+
"-p",
|
|
6163
|
+
OPENCODE2_PACKAGE_SPEC,
|
|
6164
|
+
"--",
|
|
6165
|
+
"opencode2",
|
|
6166
|
+
"serve",
|
|
6167
|
+
"--port",
|
|
6168
|
+
port.toString(),
|
|
6169
|
+
"--hostname",
|
|
6170
|
+
"127.0.0.1",
|
|
6171
|
+
...logLevel
|
|
6172
|
+
];
|
|
6173
|
+
}
|
|
5948
6174
|
}
|
|
5949
6175
|
const child = spawn3(command, args, {
|
|
5950
6176
|
env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
|
|
@@ -6001,6 +6227,56 @@ function isOpenCode2Installed() {
|
|
|
6001
6227
|
return false;
|
|
6002
6228
|
}
|
|
6003
6229
|
}
|
|
6230
|
+
function readVersionOutput(command) {
|
|
6231
|
+
try {
|
|
6232
|
+
return execSync2(`${command} --version`, {
|
|
6233
|
+
encoding: "utf-8",
|
|
6234
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
6235
|
+
}).trim();
|
|
6236
|
+
} catch (error2) {
|
|
6237
|
+
const output = typeof error2 === "object" && error2 !== null ? [
|
|
6238
|
+
"stdout" in error2 ? String(error2.stdout ?? "").trim() : "",
|
|
6239
|
+
"stderr" in error2 ? String(error2.stderr ?? "").trim() : ""
|
|
6240
|
+
].filter(Boolean).join("\n") : "";
|
|
6241
|
+
return output || (error2 instanceof Error ? error2.message : String(error2));
|
|
6242
|
+
}
|
|
6243
|
+
}
|
|
6244
|
+
function parseOpenCodeVersion(rawVersion) {
|
|
6245
|
+
const output = rawVersion.trim();
|
|
6246
|
+
const prefix = output.match(/^(opencode2|opencode)\s+/i)?.[1]?.toLowerCase();
|
|
6247
|
+
const version2 = (prefix ? output.slice(prefix.length).trim() : output).replace(/^v/i, "");
|
|
6248
|
+
if (prefix === "opencode2") return { major: "v2", version: version2 };
|
|
6249
|
+
const major = version2.match(/^(\d+)(?:\.\d+)*(?:[-+][0-9A-Za-z.-]+)?$/)?.[1];
|
|
6250
|
+
if (!major) return null;
|
|
6251
|
+
if (Number(major) === 1) return { major: "v1", version: version2 };
|
|
6252
|
+
if (Number(major) >= 2) return { major: "v2", version: version2 };
|
|
6253
|
+
return null;
|
|
6254
|
+
}
|
|
6255
|
+
function resolveInstalledOpenCode() {
|
|
6256
|
+
let opencode2Available = false;
|
|
6257
|
+
if (isOpenCodeInstalled()) {
|
|
6258
|
+
const rawVersion = readVersionOutput("opencode");
|
|
6259
|
+
const parsed = parseOpenCodeVersion(rawVersion);
|
|
6260
|
+
if (parsed?.major === "v1") {
|
|
6261
|
+
return { kind: "installed", major: "v1", command: "opencode", version: parsed.version };
|
|
6262
|
+
}
|
|
6263
|
+
if (parsed?.major === "v2") {
|
|
6264
|
+
return { kind: "installed", major: "v2", command: "opencode", version: parsed.version };
|
|
6265
|
+
}
|
|
6266
|
+
opencode2Available = isOpenCode2Installed();
|
|
6267
|
+
if (!opencode2Available) return { kind: "unknown", command: "opencode", rawVersion };
|
|
6268
|
+
}
|
|
6269
|
+
if (opencode2Available || isOpenCode2Installed()) {
|
|
6270
|
+
const parsed = parseOpenCodeVersion(readVersionOutput("opencode2"));
|
|
6271
|
+
return {
|
|
6272
|
+
kind: "installed",
|
|
6273
|
+
major: "v2",
|
|
6274
|
+
command: "opencode2",
|
|
6275
|
+
version: parsed?.version
|
|
6276
|
+
};
|
|
6277
|
+
}
|
|
6278
|
+
return { kind: "none" };
|
|
6279
|
+
}
|
|
6004
6280
|
async function promptOpenCodeInstall(interactive) {
|
|
6005
6281
|
if (!interactive) {
|
|
6006
6282
|
console.log(
|
|
@@ -6210,15 +6486,15 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
6210
6486
|
|
|
6211
6487
|
// src/lib/opencode/session-db-size.ts
|
|
6212
6488
|
import { statSync as statSync4 } from "node:fs";
|
|
6213
|
-
import { join as
|
|
6489
|
+
import { join as join8 } from "node:path";
|
|
6214
6490
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
6215
6491
|
function statSessionDbBytes(homeDir) {
|
|
6216
|
-
const dbPath =
|
|
6492
|
+
const dbPath = join8(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
6217
6493
|
try {
|
|
6218
6494
|
return statSync4(dbPath).size;
|
|
6219
6495
|
} catch (err) {
|
|
6220
|
-
const
|
|
6221
|
-
if (!
|
|
6496
|
+
const isMissingFile2 = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
6497
|
+
if (!isMissingFile2) {
|
|
6222
6498
|
console.error(
|
|
6223
6499
|
`[statSessionDbBytes] could not stat ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
6224
6500
|
);
|
|
@@ -6243,12 +6519,12 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
6243
6519
|
// src/lib/opencode/log-tail.ts
|
|
6244
6520
|
import { statSync as statSync5 } from "node:fs";
|
|
6245
6521
|
import { homedir as homedir5 } from "node:os";
|
|
6246
|
-
import { join as
|
|
6522
|
+
import { join as join9 } from "node:path";
|
|
6247
6523
|
import { open as open2, stat } from "node:fs/promises";
|
|
6248
6524
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
6249
6525
|
function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
|
|
6250
|
-
const dataDir = env.XDG_DATA_HOME ||
|
|
6251
|
-
return
|
|
6526
|
+
const dataDir = env.XDG_DATA_HOME || join9(homeDir, ".local", "share");
|
|
6527
|
+
return join9(dataDir, "opencode", "log", "opencode.log");
|
|
6252
6528
|
}
|
|
6253
6529
|
function isEnoent(error2) {
|
|
6254
6530
|
return error2?.code === "ENOENT";
|
|
@@ -6915,10 +7191,10 @@ var RunnerConnection = class _RunnerConnection {
|
|
|
6915
7191
|
};
|
|
6916
7192
|
|
|
6917
7193
|
// src/lib/tunnel/ready-marker.ts
|
|
6918
|
-
import { writeFileSync as
|
|
7194
|
+
import { writeFileSync as writeFileSync4 } from "node:fs";
|
|
6919
7195
|
function writeTunnelReadyMarker(path, agentId) {
|
|
6920
7196
|
try {
|
|
6921
|
-
|
|
7197
|
+
writeFileSync4(path, `${agentId}
|
|
6922
7198
|
`);
|
|
6923
7199
|
return { ok: true };
|
|
6924
7200
|
} catch (error2) {
|
|
@@ -6943,7 +7219,7 @@ async function stopSessionDbReplication(child, timeoutMs) {
|
|
|
6943
7219
|
}
|
|
6944
7220
|
|
|
6945
7221
|
// src/lib/process-liveness.ts
|
|
6946
|
-
import { readFileSync as
|
|
7222
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
6947
7223
|
function isProcessAlive(pid) {
|
|
6948
7224
|
try {
|
|
6949
7225
|
process.kill(pid, 0);
|
|
@@ -6958,7 +7234,7 @@ function isProcessAlive(pid) {
|
|
|
6958
7234
|
}
|
|
6959
7235
|
if (process.platform !== "linux") return true;
|
|
6960
7236
|
try {
|
|
6961
|
-
const status2 =
|
|
7237
|
+
const status2 = readFileSync7(`/proc/${pid}/status`, "utf8");
|
|
6962
7238
|
return !/^State:\s+Z(?:\s|$)/m.test(status2);
|
|
6963
7239
|
} catch (error2) {
|
|
6964
7240
|
console.error(
|
|
@@ -7317,18 +7593,18 @@ function createSessionStoreHealth(options = {}) {
|
|
|
7317
7593
|
import { homedir as homedir6 } from "node:os";
|
|
7318
7594
|
|
|
7319
7595
|
// src/lib/runner-file-sync.ts
|
|
7320
|
-
import { join as
|
|
7596
|
+
import { join as join11 } from "node:path";
|
|
7321
7597
|
|
|
7322
7598
|
// src/lib/file-push.ts
|
|
7323
7599
|
import { randomUUID } from "node:crypto";
|
|
7324
7600
|
import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
|
|
7325
|
-
import { basename as basename2, dirname as
|
|
7601
|
+
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute2, join as join10, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
|
7326
7602
|
var FILE_MODE = 384;
|
|
7327
7603
|
var DIRECTORY_MODE = 448;
|
|
7328
7604
|
async function writePushedFile(request3) {
|
|
7329
|
-
const { requestedPath, content, allowedDirectories, homeDir } = request3;
|
|
7605
|
+
const { requestedPath, content, allowedDirectories, allowedFiles, homeDir } = request3;
|
|
7330
7606
|
const bytes = content.byteLength;
|
|
7331
|
-
if (allowedDirectories.length === 0) {
|
|
7607
|
+
if (allowedDirectories.length === 0 && allowedFiles.length === 0) {
|
|
7332
7608
|
return refuse("file_sync_disabled", "File sync is not enabled on this runner.", {
|
|
7333
7609
|
path: requestedPath,
|
|
7334
7610
|
bytes
|
|
@@ -7353,10 +7629,10 @@ async function writePushedFile(request3) {
|
|
|
7353
7629
|
}
|
|
7354
7630
|
try {
|
|
7355
7631
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
7356
|
-
|
|
7632
|
+
dirname7(candidate)
|
|
7357
7633
|
);
|
|
7358
|
-
const realTarget =
|
|
7359
|
-
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
7634
|
+
const realTarget = join10(existingAncestor, ...missingSegments, basename2(candidate));
|
|
7635
|
+
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget) ?? await findMatchingAllowedFile(allowedFiles, realTarget);
|
|
7360
7636
|
if (allowedDirectory === null) {
|
|
7361
7637
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
7362
7638
|
path: realTarget,
|
|
@@ -7365,8 +7641,8 @@ async function writePushedFile(request3) {
|
|
|
7365
7641
|
}
|
|
7366
7642
|
if (missingSegments.length > 0) {
|
|
7367
7643
|
await createMissingDirectories(existingAncestor, missingSegments);
|
|
7368
|
-
const realParent = await realpath(
|
|
7369
|
-
if (realParent !==
|
|
7644
|
+
const realParent = await realpath(dirname7(realTarget));
|
|
7645
|
+
if (realParent !== dirname7(realTarget) || !contains(allowedDirectory, realTarget)) {
|
|
7370
7646
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
7371
7647
|
path: realTarget,
|
|
7372
7648
|
bytes,
|
|
@@ -7391,7 +7667,7 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
7391
7667
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
7392
7668
|
return null;
|
|
7393
7669
|
}
|
|
7394
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
7670
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
7395
7671
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
7396
7672
|
return null;
|
|
7397
7673
|
}
|
|
@@ -7409,7 +7685,7 @@ async function resolveNearestExistingAncestor(directory) {
|
|
|
7409
7685
|
try {
|
|
7410
7686
|
return { existingAncestor: await realpath(current), missingSegments };
|
|
7411
7687
|
} catch (err) {
|
|
7412
|
-
const parent =
|
|
7688
|
+
const parent = dirname7(current);
|
|
7413
7689
|
if (err.code !== "ENOENT" || parent === current) {
|
|
7414
7690
|
throw err;
|
|
7415
7691
|
}
|
|
@@ -7431,6 +7707,22 @@ async function findContainingAllowedDirectory(allowedDirectories, realTarget) {
|
|
|
7431
7707
|
}
|
|
7432
7708
|
return null;
|
|
7433
7709
|
}
|
|
7710
|
+
async function findMatchingAllowedFile(allowedFiles, realTarget) {
|
|
7711
|
+
for (const entry of allowedFiles) {
|
|
7712
|
+
if (!isAbsolute2(entry)) {
|
|
7713
|
+
log("warn", "file_push_allowed_directory_skipped", {
|
|
7714
|
+
directory: entry,
|
|
7715
|
+
reason: "not_absolute_file"
|
|
7716
|
+
});
|
|
7717
|
+
continue;
|
|
7718
|
+
}
|
|
7719
|
+
const realParent = await realpathCreatingIfMissing(dirname7(entry));
|
|
7720
|
+
if (realParent !== null && join10(realParent, basename2(entry)) === realTarget) {
|
|
7721
|
+
return realParent;
|
|
7722
|
+
}
|
|
7723
|
+
}
|
|
7724
|
+
return null;
|
|
7725
|
+
}
|
|
7434
7726
|
async function realpathCreatingIfMissing(directory) {
|
|
7435
7727
|
try {
|
|
7436
7728
|
return await realpath(directory);
|
|
@@ -7464,13 +7756,13 @@ function contains(realDirectory, realTarget) {
|
|
|
7464
7756
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
7465
7757
|
let current = existingAncestor;
|
|
7466
7758
|
for (const segment of missingSegments) {
|
|
7467
|
-
current =
|
|
7759
|
+
current = join10(current, segment);
|
|
7468
7760
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
7469
7761
|
await chmod(current, DIRECTORY_MODE);
|
|
7470
7762
|
}
|
|
7471
7763
|
}
|
|
7472
7764
|
async function writeAtomically(realTarget, content) {
|
|
7473
|
-
const temporaryPath =
|
|
7765
|
+
const temporaryPath = join10(dirname7(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
7474
7766
|
let handle;
|
|
7475
7767
|
try {
|
|
7476
7768
|
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
@@ -7600,16 +7892,16 @@ var NOT_APPLIED = {
|
|
|
7600
7892
|
opencodeAuthApplied: false
|
|
7601
7893
|
};
|
|
7602
7894
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
7603
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
7604
|
-
return expanded ===
|
|
7895
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join11(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
7896
|
+
return expanded === join11(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
7605
7897
|
}
|
|
7606
7898
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
7607
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
7608
|
-
return expanded ===
|
|
7899
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join11(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
7900
|
+
return expanded === join11(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
7609
7901
|
}
|
|
7610
7902
|
async function applyOne(options, file) {
|
|
7611
7903
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
7612
|
-
if (options.allowedDirectories.length === 0) {
|
|
7904
|
+
if (options.allowedDirectories.length === 0 && options.allowedFiles.length === 0) {
|
|
7613
7905
|
options.log({
|
|
7614
7906
|
level: "warn",
|
|
7615
7907
|
message: `Runner file ${label} rejected: file sync is not enabled on this runner (start it with --enable-file-sync-to)`
|
|
@@ -7636,6 +7928,7 @@ async function applyOne(options, file) {
|
|
|
7636
7928
|
requestedPath: file.path,
|
|
7637
7929
|
content: download.content,
|
|
7638
7930
|
allowedDirectories: options.allowedDirectories,
|
|
7931
|
+
allowedFiles: options.allowedFiles,
|
|
7639
7932
|
homeDir: options.homeDir
|
|
7640
7933
|
});
|
|
7641
7934
|
} catch (err) {
|
|
@@ -7742,6 +8035,212 @@ function describe(err) {
|
|
|
7742
8035
|
return err instanceof Error ? err.message : String(err);
|
|
7743
8036
|
}
|
|
7744
8037
|
|
|
8038
|
+
// src/lib/vault-file-sync.ts
|
|
8039
|
+
import { createHash } from "node:crypto";
|
|
8040
|
+
async function syncVaultFiles(options) {
|
|
8041
|
+
const files = await listVaultFiles(options);
|
|
8042
|
+
if (files === null) return { applied: 0 };
|
|
8043
|
+
const listedIds = new Set(files.map((file) => file.id));
|
|
8044
|
+
for (const fileId of options.outcomes.keys()) {
|
|
8045
|
+
if (!listedIds.has(fileId)) options.outcomes.delete(fileId);
|
|
8046
|
+
}
|
|
8047
|
+
let applied = 0;
|
|
8048
|
+
for (const file of files) {
|
|
8049
|
+
try {
|
|
8050
|
+
if (await reconcileVaultFile(options, file)) applied += 1;
|
|
8051
|
+
} catch (err) {
|
|
8052
|
+
options.log({
|
|
8053
|
+
level: "error",
|
|
8054
|
+
message: `Vault file ${file.id} reconciliation failed unexpectedly (message processing is unaffected): ${describe2(err)}`
|
|
8055
|
+
});
|
|
8056
|
+
}
|
|
8057
|
+
}
|
|
8058
|
+
return { applied };
|
|
8059
|
+
}
|
|
8060
|
+
async function listVaultFiles(options) {
|
|
8061
|
+
let response;
|
|
8062
|
+
try {
|
|
8063
|
+
response = await options.fetchImpl(`${options.apiUrl}/runners/${options.agentId}/vault-files`, {
|
|
8064
|
+
headers: { Authorization: options.getAuthHeader() }
|
|
8065
|
+
});
|
|
8066
|
+
} catch (err) {
|
|
8067
|
+
options.log({
|
|
8068
|
+
level: "warn",
|
|
8069
|
+
message: `Could not list vault files \u2014 retrying on the next drain: ${describe2(err)}`
|
|
8070
|
+
});
|
|
8071
|
+
return null;
|
|
8072
|
+
}
|
|
8073
|
+
if (!response.ok) {
|
|
8074
|
+
options.log({
|
|
8075
|
+
level: response.status === 404 ? "debug" : "warn",
|
|
8076
|
+
message: `Listing vault files returned HTTP ${response.status} \u2014 retrying on the next drain`
|
|
8077
|
+
});
|
|
8078
|
+
return null;
|
|
8079
|
+
}
|
|
8080
|
+
let body;
|
|
8081
|
+
try {
|
|
8082
|
+
body = await response.json();
|
|
8083
|
+
} catch (err) {
|
|
8084
|
+
options.log({
|
|
8085
|
+
level: "warn",
|
|
8086
|
+
message: `Vault file list was not readable JSON \u2014 retrying on the next drain: ${describe2(err)}`
|
|
8087
|
+
});
|
|
8088
|
+
return null;
|
|
8089
|
+
}
|
|
8090
|
+
if (body === null || typeof body !== "object" || !Array.isArray(body.files)) {
|
|
8091
|
+
options.log({
|
|
8092
|
+
level: "warn",
|
|
8093
|
+
message: "Vault file list was not an object with a files array \u2014 ignoring it for this drain"
|
|
8094
|
+
});
|
|
8095
|
+
return null;
|
|
8096
|
+
}
|
|
8097
|
+
const files = [];
|
|
8098
|
+
for (const entry of body.files) {
|
|
8099
|
+
const file = asVaultFile(entry);
|
|
8100
|
+
if (file === null) {
|
|
8101
|
+
options.log({
|
|
8102
|
+
level: "warn",
|
|
8103
|
+
message: "Ignoring a malformed vault file entry"
|
|
8104
|
+
});
|
|
8105
|
+
continue;
|
|
8106
|
+
}
|
|
8107
|
+
files.push(file);
|
|
8108
|
+
}
|
|
8109
|
+
return files;
|
|
8110
|
+
}
|
|
8111
|
+
function asVaultFile(entry) {
|
|
8112
|
+
if (entry === null || typeof entry !== "object") return null;
|
|
8113
|
+
const value = entry;
|
|
8114
|
+
if (typeof value.id !== "string" || value.id === "" || typeof value.destination_path !== "string" || value.destination_path === "" || typeof value.size_bytes !== "number" || !Number.isFinite(value.size_bytes) || value.size_bytes < 0 || typeof value.content_sha256 !== "string" || value.content_sha256 === "" || typeof value.created_at !== "string" || typeof value.updated_at !== "string") {
|
|
8115
|
+
return null;
|
|
8116
|
+
}
|
|
8117
|
+
return {
|
|
8118
|
+
id: value.id,
|
|
8119
|
+
destination_path: value.destination_path,
|
|
8120
|
+
size_bytes: value.size_bytes,
|
|
8121
|
+
content_sha256: value.content_sha256,
|
|
8122
|
+
created_at: value.created_at,
|
|
8123
|
+
updated_at: value.updated_at
|
|
8124
|
+
};
|
|
8125
|
+
}
|
|
8126
|
+
async function reconcileVaultFile(options, file) {
|
|
8127
|
+
const existing = options.outcomes.get(file.id);
|
|
8128
|
+
if (existing?.sha256 === file.content_sha256) {
|
|
8129
|
+
if (existing.reported || existing.reportAttempts >= MAX_ACK_ATTEMPTS) return false;
|
|
8130
|
+
await reportOutcome(options, file, existing);
|
|
8131
|
+
return false;
|
|
8132
|
+
}
|
|
8133
|
+
const content = await downloadVaultFile(options, file);
|
|
8134
|
+
if (content === null) return false;
|
|
8135
|
+
const actualSha256 = createHash("sha256").update(content).digest("hex");
|
|
8136
|
+
if (actualSha256 !== file.content_sha256) {
|
|
8137
|
+
options.log({
|
|
8138
|
+
level: "warn",
|
|
8139
|
+
message: `Vault file ${file.id} content hash mismatch: expected ${file.content_sha256}, received ${actualSha256} \u2014 retrying on the next drain`
|
|
8140
|
+
});
|
|
8141
|
+
return false;
|
|
8142
|
+
}
|
|
8143
|
+
const outcome = await applyVaultFile(options, file, content);
|
|
8144
|
+
const entry = {
|
|
8145
|
+
sha256: file.content_sha256,
|
|
8146
|
+
outcome: outcome.ok ? "applied" : "refused",
|
|
8147
|
+
reason: outcome.ok ? null : outcome.code,
|
|
8148
|
+
reported: false,
|
|
8149
|
+
reportAttempts: 0
|
|
8150
|
+
};
|
|
8151
|
+
options.outcomes.set(file.id, entry);
|
|
8152
|
+
await reportOutcome(options, file, entry);
|
|
8153
|
+
return outcome.ok;
|
|
8154
|
+
}
|
|
8155
|
+
async function downloadVaultFile(options, file) {
|
|
8156
|
+
try {
|
|
8157
|
+
const response = await options.fetchImpl(
|
|
8158
|
+
`${options.apiUrl}/runners/${options.agentId}/vault-files/${file.id}/content`,
|
|
8159
|
+
{ headers: { Authorization: options.getAuthHeader() } }
|
|
8160
|
+
);
|
|
8161
|
+
if (!response.ok) {
|
|
8162
|
+
options.log({
|
|
8163
|
+
level: "warn",
|
|
8164
|
+
message: `Downloading vault file ${file.id} returned HTTP ${response.status} \u2014 retrying on the next drain`
|
|
8165
|
+
});
|
|
8166
|
+
return null;
|
|
8167
|
+
}
|
|
8168
|
+
return Buffer.from(await response.arrayBuffer());
|
|
8169
|
+
} catch (err) {
|
|
8170
|
+
options.log({
|
|
8171
|
+
level: "warn",
|
|
8172
|
+
message: `Downloading vault file ${file.id} failed \u2014 retrying on the next drain: ${describe2(err)}`
|
|
8173
|
+
});
|
|
8174
|
+
return null;
|
|
8175
|
+
}
|
|
8176
|
+
}
|
|
8177
|
+
async function applyVaultFile(options, file, content) {
|
|
8178
|
+
try {
|
|
8179
|
+
const outcome = await writePushedFile({
|
|
8180
|
+
requestedPath: file.destination_path,
|
|
8181
|
+
content,
|
|
8182
|
+
allowedDirectories: options.allowedDirectories,
|
|
8183
|
+
allowedFiles: options.allowedFiles,
|
|
8184
|
+
homeDir: options.homeDir
|
|
8185
|
+
});
|
|
8186
|
+
if (!outcome.ok) {
|
|
8187
|
+
options.log({
|
|
8188
|
+
level: "warn",
|
|
8189
|
+
message: `Vault file ${file.id} refused (${outcome.code}): ${outcome.message}`
|
|
8190
|
+
});
|
|
8191
|
+
} else {
|
|
8192
|
+
options.log({
|
|
8193
|
+
level: "info",
|
|
8194
|
+
message: `Vault file ${file.id} applied (${content.byteLength} bytes)`
|
|
8195
|
+
});
|
|
8196
|
+
}
|
|
8197
|
+
return outcome;
|
|
8198
|
+
} catch (err) {
|
|
8199
|
+
options.log({
|
|
8200
|
+
level: "error",
|
|
8201
|
+
message: `Vault file ${file.id} could not be written: ${describe2(err)}`
|
|
8202
|
+
});
|
|
8203
|
+
return { ok: false, code: "write_failed", message: "The runner could not write the file." };
|
|
8204
|
+
}
|
|
8205
|
+
}
|
|
8206
|
+
async function reportOutcome(options, file, entry) {
|
|
8207
|
+
const body = {
|
|
8208
|
+
status: entry.outcome === "applied" ? "applied" : "rejected",
|
|
8209
|
+
reported_sha256: entry.sha256
|
|
8210
|
+
};
|
|
8211
|
+
if (entry.outcome === "refused" && entry.reason !== null) body.reason = entry.reason;
|
|
8212
|
+
try {
|
|
8213
|
+
const response = await options.fetchImpl(
|
|
8214
|
+
`${options.apiUrl}/runners/${options.agentId}/vault-files/${file.id}/status`,
|
|
8215
|
+
{
|
|
8216
|
+
method: "POST",
|
|
8217
|
+
headers: {
|
|
8218
|
+
Authorization: options.getAuthHeader(),
|
|
8219
|
+
"Content-Type": "application/json"
|
|
8220
|
+
},
|
|
8221
|
+
body: JSON.stringify(body)
|
|
8222
|
+
}
|
|
8223
|
+
);
|
|
8224
|
+
if (response.ok) {
|
|
8225
|
+
entry.reported = true;
|
|
8226
|
+
return;
|
|
8227
|
+
}
|
|
8228
|
+
recordReportFailure(options, file, entry, `HTTP ${response.status}`);
|
|
8229
|
+
} catch (err) {
|
|
8230
|
+
recordReportFailure(options, file, entry, describe2(err));
|
|
8231
|
+
}
|
|
8232
|
+
}
|
|
8233
|
+
function recordReportFailure(options, file, entry, reason) {
|
|
8234
|
+
entry.reportAttempts += 1;
|
|
8235
|
+
options.log({
|
|
8236
|
+
level: "error",
|
|
8237
|
+
message: entry.reportAttempts >= MAX_ACK_ATTEMPTS ? `Reporting vault file ${file.id} as ${entry.outcome} failed (${reason}) \u2014 giving up after ${entry.reportAttempts} attempts` : `Reporting vault file ${file.id} as ${entry.outcome} failed (${reason}) \u2014 retrying on the next drain (attempt ${entry.reportAttempts} of ${MAX_ACK_ATTEMPTS})`
|
|
8238
|
+
});
|
|
8239
|
+
}
|
|
8240
|
+
function describe2(err) {
|
|
8241
|
+
return err instanceof Error ? err.message : String(err);
|
|
8242
|
+
}
|
|
8243
|
+
|
|
7745
8244
|
// src/lib/channels/driver.ts
|
|
7746
8245
|
function messageIdOf(m) {
|
|
7747
8246
|
if (!m || typeof m !== "object") return void 0;
|
|
@@ -7750,7 +8249,7 @@ function messageIdOf(m) {
|
|
|
7750
8249
|
return typeof infoId === "string" ? infoId : void 0;
|
|
7751
8250
|
}
|
|
7752
8251
|
function composeAbandonmentNote(finish, replyShape, outcome) {
|
|
7753
|
-
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.");
|
|
8252
|
+
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.");
|
|
7754
8253
|
if (replyShape === null) return base;
|
|
7755
8254
|
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)` : ""}.`;
|
|
7756
8255
|
}
|
|
@@ -7787,6 +8286,9 @@ var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
|
|
|
7787
8286
|
var ABSOLUTE_MAX_PROCESSING_MS = 6 * 60 * 60 * 1e3;
|
|
7788
8287
|
var B2_ABANDONMENT_MIN_PINNED_MS = 3 * 6e4;
|
|
7789
8288
|
var AMBIGUOUS_FINISH_MAX_PINNED_MS = 3 * 6e4;
|
|
8289
|
+
var MIDFLIGHT_ORPHAN_MIN_PINNED_MS = 3 * 6e4;
|
|
8290
|
+
var MIDFLIGHT_ORPHAN_RECHECK_MS = 6e4;
|
|
8291
|
+
var MIDFLIGHT_ORPHAN_MIN_NOT_ONGOING_STREAK = 2;
|
|
7790
8292
|
var B2_ABANDONMENT_RECHECK_MS = HEARTBEAT_MS;
|
|
7791
8293
|
var POLL_MISS_GRACE_MS = HEARTBEAT_MS;
|
|
7792
8294
|
var WATCHER_STALL_MS = 3 * POLL_MISS_GRACE_MS;
|
|
@@ -7838,6 +8340,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7838
8340
|
stuckQueuedMs;
|
|
7839
8341
|
now;
|
|
7840
8342
|
fileSyncDirectories;
|
|
8343
|
+
fileSyncExactFiles;
|
|
7841
8344
|
homeDir;
|
|
7842
8345
|
maxActiveSessions;
|
|
7843
8346
|
watcherStallMs;
|
|
@@ -8150,6 +8653,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8150
8653
|
* re-downloaded and re-written every ~2s until the server expires it.
|
|
8151
8654
|
*/
|
|
8152
8655
|
fileAckFailures = /* @__PURE__ */ new Map();
|
|
8656
|
+
vaultFileOutcomes = /* @__PURE__ */ new Map();
|
|
8153
8657
|
/**
|
|
8154
8658
|
* Monotonic count of files this runner has pulled and written (#559). Only
|
|
8155
8659
|
* ever increases, so `run.ts` detects work by comparing it against the value
|
|
@@ -8206,6 +8710,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8206
8710
|
this.stuckQueuedMs = config.stuckQueuedMs ?? DEFAULT_STUCK_QUEUED_MS;
|
|
8207
8711
|
this.now = config.now ?? (() => Date.now());
|
|
8208
8712
|
this.fileSyncDirectories = config.fileSyncDirectories ?? [];
|
|
8713
|
+
this.fileSyncExactFiles = config.fileSyncExactFiles ?? [];
|
|
8209
8714
|
this.homeDir = config.homeDir ?? homedir6();
|
|
8210
8715
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
8211
8716
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
@@ -8303,14 +8808,27 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8303
8808
|
getAuthHeader: this.getAuthHeader,
|
|
8304
8809
|
fetchImpl: this.fetchImpl,
|
|
8305
8810
|
allowedDirectories: this.fileSyncDirectories,
|
|
8811
|
+
allowedFiles: this.fileSyncExactFiles,
|
|
8306
8812
|
homeDir: this.homeDir,
|
|
8307
8813
|
ackFailures: this.fileAckFailures,
|
|
8308
8814
|
log: this.log
|
|
8309
8815
|
});
|
|
8310
|
-
|
|
8816
|
+
const vaultResult = await syncVaultFiles({
|
|
8817
|
+
agentId: this.agentId,
|
|
8818
|
+
apiUrl: this.apiUrl,
|
|
8819
|
+
getAuthHeader: this.getAuthHeader,
|
|
8820
|
+
fetchImpl: this.fetchImpl,
|
|
8821
|
+
allowedDirectories: this.fileSyncDirectories,
|
|
8822
|
+
allowedFiles: this.fileSyncExactFiles,
|
|
8823
|
+
homeDir: this.homeDir,
|
|
8824
|
+
outcomes: this.vaultFileOutcomes,
|
|
8825
|
+
log: this.log
|
|
8826
|
+
});
|
|
8827
|
+
const applied = result.applied + vaultResult.applied;
|
|
8828
|
+
this.appliedFileCount += applied;
|
|
8311
8829
|
if (result.claudeCredentialApplied) this.claudeCredentialApplyCount += 1;
|
|
8312
8830
|
if (result.opencodeAuthApplied) this.opencodeAuthApplyCount += 1;
|
|
8313
|
-
return
|
|
8831
|
+
return applied;
|
|
8314
8832
|
} catch (err) {
|
|
8315
8833
|
this.log({
|
|
8316
8834
|
level: "error",
|
|
@@ -8722,6 +9240,47 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8722
9240
|
agent: message.opencode_agent ?? void 0,
|
|
8723
9241
|
model: message.opencode_model ?? void 0
|
|
8724
9242
|
};
|
|
9243
|
+
const pinnedProviderLoaded = await isPinnedProviderLoaded(
|
|
9244
|
+
this.port,
|
|
9245
|
+
options.model,
|
|
9246
|
+
this.openCodeClient
|
|
9247
|
+
);
|
|
9248
|
+
if (pinnedProviderLoaded === false) {
|
|
9249
|
+
const dispatchedModel = message.opencode_model;
|
|
9250
|
+
const separator = dispatchedModel?.indexOf("/") ?? -1;
|
|
9251
|
+
const hasProviderAndModel = dispatchedModel !== null && separator > 0 && separator < dispatchedModel.length - 1;
|
|
9252
|
+
const providerId = hasProviderAndModel ? dispatchedModel.slice(0, separator) : null;
|
|
9253
|
+
const modelId = hasProviderAndModel ? dispatchedModel.slice(separator + 1) : null;
|
|
9254
|
+
const errorMessage3 = `Provider namespace not loaded on this runner: ${message.opencode_model}. Refused before dispatch; no turn was started.`;
|
|
9255
|
+
this.log({
|
|
9256
|
+
level: "error",
|
|
9257
|
+
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`,
|
|
9258
|
+
conversation_id: conv.id,
|
|
9259
|
+
message_id: message.id
|
|
9260
|
+
});
|
|
9261
|
+
try {
|
|
9262
|
+
await this.markFailed(conv.id, message.id, sessionId, errorMessage3, void 0, {
|
|
9263
|
+
classification: {
|
|
9264
|
+
kind: "model_auth",
|
|
9265
|
+
providerId,
|
|
9266
|
+
modelId,
|
|
9267
|
+
reason: "missing"
|
|
9268
|
+
},
|
|
9269
|
+
errorShape: { name: null, statusCode: null }
|
|
9270
|
+
});
|
|
9271
|
+
this.signalDispatchNotStarted(conv, message, "model_provider_absent");
|
|
9272
|
+
} catch (markErr) {
|
|
9273
|
+
this.log({
|
|
9274
|
+
level: "warn",
|
|
9275
|
+
message: `markFailed PATCH for refused message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed: ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
9276
|
+
conversation_id: conv.id,
|
|
9277
|
+
message_id: message.id
|
|
9278
|
+
});
|
|
9279
|
+
this.signalDispatchNotStarted(conv, message, "failure_unreported");
|
|
9280
|
+
}
|
|
9281
|
+
await this.repairProviderRegistry(conv.id, message.id);
|
|
9282
|
+
break;
|
|
9283
|
+
}
|
|
8725
9284
|
let opencodeMessageId;
|
|
8726
9285
|
let preDispatchUserIds = null;
|
|
8727
9286
|
try {
|
|
@@ -9846,6 +10405,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9846
10405
|
b2AbandonedWithoutAnswer: false,
|
|
9847
10406
|
ambiguousPinnedSinceMs: 0,
|
|
9848
10407
|
ambiguousResolved: false,
|
|
10408
|
+
midFlightPinnedSinceMs: 0,
|
|
10409
|
+
midFlightNotOngoingStreak: 0,
|
|
10410
|
+
midFlightLastStatusCheckMs: 0,
|
|
10411
|
+
midFlightOrphanSignalled: false,
|
|
10412
|
+
midFlightOrphanWithoutAnswer: false,
|
|
10413
|
+
midFlightHeldLogged: false,
|
|
9849
10414
|
sessionStoreFailurePending: false,
|
|
9850
10415
|
neverStartedResolved: false
|
|
9851
10416
|
});
|
|
@@ -9985,6 +10550,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9985
10550
|
b2AbandonedWithoutAnswer: false,
|
|
9986
10551
|
ambiguousPinnedSinceMs: 0,
|
|
9987
10552
|
ambiguousResolved: false,
|
|
10553
|
+
midFlightPinnedSinceMs: 0,
|
|
10554
|
+
midFlightNotOngoingStreak: 0,
|
|
10555
|
+
midFlightLastStatusCheckMs: 0,
|
|
10556
|
+
midFlightOrphanSignalled: false,
|
|
10557
|
+
midFlightOrphanWithoutAnswer: false,
|
|
10558
|
+
midFlightHeldLogged: false,
|
|
9988
10559
|
sessionStoreFailurePending: false,
|
|
9989
10560
|
neverStartedResolved: false
|
|
9990
10561
|
});
|
|
@@ -10756,7 +11327,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10756
11327
|
watcher,
|
|
10757
11328
|
inFlight,
|
|
10758
11329
|
messages,
|
|
10759
|
-
|
|
11330
|
+
composeAbandonmentNote(
|
|
11331
|
+
finish,
|
|
11332
|
+
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
11333
|
+
"without-answer"
|
|
11334
|
+
)
|
|
10760
11335
|
);
|
|
10761
11336
|
} else {
|
|
10762
11337
|
await this.settleMessageDone(
|
|
@@ -10815,7 +11390,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10815
11390
|
watcher,
|
|
10816
11391
|
inFlight,
|
|
10817
11392
|
messages,
|
|
10818
|
-
|
|
11393
|
+
composeAbandonmentNote(
|
|
11394
|
+
finish,
|
|
11395
|
+
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
11396
|
+
"without-answer"
|
|
11397
|
+
)
|
|
10819
11398
|
);
|
|
10820
11399
|
return;
|
|
10821
11400
|
}
|
|
@@ -10909,6 +11488,106 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10909
11488
|
return;
|
|
10910
11489
|
}
|
|
10911
11490
|
}
|
|
11491
|
+
const midFlightPinnedNow = activelyRunning && isMidFlightPinnedRunning(messages, inFlight.opencodeMessageId);
|
|
11492
|
+
if (!midFlightPinnedNow) {
|
|
11493
|
+
if (snapshotReadable) {
|
|
11494
|
+
inFlight.midFlightPinnedSinceMs = 0;
|
|
11495
|
+
inFlight.midFlightNotOngoingStreak = 0;
|
|
11496
|
+
inFlight.midFlightLastStatusCheckMs = 0;
|
|
11497
|
+
inFlight.midFlightOrphanSignalled = false;
|
|
11498
|
+
inFlight.midFlightOrphanWithoutAnswer = false;
|
|
11499
|
+
inFlight.midFlightHeldLogged = false;
|
|
11500
|
+
}
|
|
11501
|
+
} else {
|
|
11502
|
+
if (inFlight.midFlightOrphanSignalled) {
|
|
11503
|
+
const note = composeAbandonmentNote(
|
|
11504
|
+
void 0,
|
|
11505
|
+
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
11506
|
+
inFlight.midFlightOrphanWithoutAnswer ? "midflight-without-answer" : "midflight-with-answer"
|
|
11507
|
+
);
|
|
11508
|
+
if (inFlight.midFlightOrphanWithoutAnswer) {
|
|
11509
|
+
await this.settleMessageAbandonedWithoutAnswer(
|
|
11510
|
+
sessionId,
|
|
11511
|
+
watcher,
|
|
11512
|
+
inFlight,
|
|
11513
|
+
messages,
|
|
11514
|
+
note
|
|
11515
|
+
);
|
|
11516
|
+
} else {
|
|
11517
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages, note);
|
|
11518
|
+
}
|
|
11519
|
+
return;
|
|
11520
|
+
}
|
|
11521
|
+
if (inFlight.midFlightPinnedSinceMs === 0) {
|
|
11522
|
+
inFlight.midFlightPinnedSinceMs = this.now();
|
|
11523
|
+
}
|
|
11524
|
+
const pinnedForMs = this.now() - inFlight.midFlightPinnedSinceMs;
|
|
11525
|
+
if (pinnedForMs >= MIDFLIGHT_ORPHAN_MIN_PINNED_MS && this.now() - inFlight.midFlightLastStatusCheckMs >= MIDFLIGHT_ORPHAN_RECHECK_MS) {
|
|
11526
|
+
inFlight.midFlightLastStatusCheckMs = this.now();
|
|
11527
|
+
const rootOngoing = await this.isSessionOngoing(sessionId);
|
|
11528
|
+
inFlight.midFlightNotOngoingStreak = rootOngoing === false ? inFlight.midFlightNotOngoingStreak + 1 : 0;
|
|
11529
|
+
if (isMidFlightOrphanConfirmed({
|
|
11530
|
+
pinnedForMs,
|
|
11531
|
+
minPinnedMs: MIDFLIGHT_ORPHAN_MIN_PINNED_MS,
|
|
11532
|
+
notOngoingStreak: inFlight.midFlightNotOngoingStreak,
|
|
11533
|
+
minNotOngoingStreak: MIDFLIGHT_ORPHAN_MIN_NOT_ONGOING_STREAK
|
|
11534
|
+
})) {
|
|
11535
|
+
if (this.sessionStoreHealth?.isUnwritable()) {
|
|
11536
|
+
inFlight.sessionStoreFailurePending = true;
|
|
11537
|
+
void this.postSignal(conv.id, id, "midflight_orphan_resolved", {
|
|
11538
|
+
watched_for_ms: pinnedForMs,
|
|
11539
|
+
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
11540
|
+
opencode_message_id: inFlight.opencodeMessageId,
|
|
11541
|
+
run_state: "failed"
|
|
11542
|
+
});
|
|
11543
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
11544
|
+
return;
|
|
11545
|
+
}
|
|
11546
|
+
inFlight.midFlightOrphanSignalled = true;
|
|
11547
|
+
const noteOutcome = extractReplyTextForMessage(messages, inFlight.opencodeMessageId) === null ? "midflight-without-answer" : "midflight-with-answer";
|
|
11548
|
+
inFlight.midFlightOrphanWithoutAnswer = noteOutcome === "midflight-without-answer";
|
|
11549
|
+
void this.postSignal(conv.id, id, "midflight_orphan_resolved", {
|
|
11550
|
+
watched_for_ms: pinnedForMs,
|
|
11551
|
+
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
11552
|
+
opencode_message_id: inFlight.opencodeMessageId,
|
|
11553
|
+
...inFlight.midFlightOrphanWithoutAnswer ? { run_state: "failed" } : {}
|
|
11554
|
+
});
|
|
11555
|
+
const note = composeAbandonmentNote(
|
|
11556
|
+
void 0,
|
|
11557
|
+
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
11558
|
+
noteOutcome
|
|
11559
|
+
);
|
|
11560
|
+
this.log({
|
|
11561
|
+
level: "warn",
|
|
11562
|
+
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`,
|
|
11563
|
+
conversation_id: conv.id,
|
|
11564
|
+
message_id: id
|
|
11565
|
+
});
|
|
11566
|
+
if (inFlight.midFlightOrphanWithoutAnswer) {
|
|
11567
|
+
await this.settleMessageAbandonedWithoutAnswer(
|
|
11568
|
+
sessionId,
|
|
11569
|
+
watcher,
|
|
11570
|
+
inFlight,
|
|
11571
|
+
messages,
|
|
11572
|
+
note
|
|
11573
|
+
);
|
|
11574
|
+
} else {
|
|
11575
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages, note);
|
|
11576
|
+
}
|
|
11577
|
+
return;
|
|
11578
|
+
}
|
|
11579
|
+
if (rootOngoing === true && !inFlight.midFlightHeldLogged) {
|
|
11580
|
+
inFlight.midFlightHeldLogged = true;
|
|
11581
|
+
const replyShape = describeAbandonedReplyShape(messages, inFlight.opencodeMessageId);
|
|
11582
|
+
this.log({
|
|
11583
|
+
level: "warn",
|
|
11584
|
+
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)"}`,
|
|
11585
|
+
conversation_id: conv.id,
|
|
11586
|
+
message_id: id
|
|
11587
|
+
});
|
|
11588
|
+
}
|
|
11589
|
+
}
|
|
11590
|
+
}
|
|
10912
11591
|
if (watcher.inFlight.has(inFlight.evidentMessageId) && activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
|
|
10913
11592
|
await this.settleAbsoluteCeiling(sessionId, watcher, messages, ceilingAttemptsThisTick);
|
|
10914
11593
|
return;
|
|
@@ -11108,13 +11787,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
11108
11787
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
11109
11788
|
}
|
|
11110
11789
|
/** Report a confirmed b2 abandonment that produced no answer text. */
|
|
11111
|
-
async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages,
|
|
11790
|
+
async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages, error2) {
|
|
11112
11791
|
const conv = watcher.conv;
|
|
11113
|
-
const error2 = composeAbandonmentNote(
|
|
11114
|
-
finish,
|
|
11115
|
-
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
11116
|
-
"without-answer"
|
|
11117
|
-
);
|
|
11118
11792
|
this.anchorDeliveryDeadline(inFlight);
|
|
11119
11793
|
this.log({
|
|
11120
11794
|
level: "warn",
|
|
@@ -13500,7 +14174,7 @@ import ora3 from "ora";
|
|
|
13500
14174
|
import { select as select3 } from "@inquirer/prompts";
|
|
13501
14175
|
function unknownPasswordError2(port) {
|
|
13502
14176
|
return new Error(
|
|
13503
|
-
`
|
|
14177
|
+
`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.`
|
|
13504
14178
|
);
|
|
13505
14179
|
}
|
|
13506
14180
|
var INTERACTIVE_START_TIMEOUT_MS2 = 3e4;
|
|
@@ -13518,9 +14192,9 @@ async function ensureOpenCode2Running(ctx) {
|
|
|
13518
14192
|
password: null
|
|
13519
14193
|
};
|
|
13520
14194
|
}
|
|
13521
|
-
if (!isOpenCode2Installed()) {
|
|
14195
|
+
if (!ctx.command && !isOpenCode2Installed()) {
|
|
13522
14196
|
throw new Error(
|
|
13523
|
-
|
|
14197
|
+
"OpenCode V2 is not installed. Install it with `npm install -g @opencode/cli` or `curl -fsSL https://opencode.ai/v2/install | bash`."
|
|
13524
14198
|
);
|
|
13525
14199
|
}
|
|
13526
14200
|
let port = ctx.port;
|
|
@@ -13548,7 +14222,8 @@ Port ${port} is already in use.`));
|
|
|
13548
14222
|
if (!ctx.interactive) {
|
|
13549
14223
|
ctx.log(`OpenCode V2 is not running on port ${port}. Starting it automatically...`);
|
|
13550
14224
|
const { child: proc, password } = await startOpenCode2(port, {
|
|
13551
|
-
inheritStdio: ctx.inheritStdio
|
|
14225
|
+
inheritStdio: ctx.inheritStdio,
|
|
14226
|
+
command: ctx.command
|
|
13552
14227
|
});
|
|
13553
14228
|
const health = await waitForOpenCode2Health(port, password, ctx.startTimeoutMs);
|
|
13554
14229
|
if (!health.healthy) {
|
|
@@ -13569,13 +14244,14 @@ Port ${port} is already in use.`));
|
|
|
13569
14244
|
password
|
|
13570
14245
|
};
|
|
13571
14246
|
}
|
|
14247
|
+
const command = ctx.command ?? "opencode2";
|
|
13572
14248
|
const action = await select3({
|
|
13573
14249
|
message: "OpenCode V2 is not running. What would you like to do?",
|
|
13574
14250
|
choices: [
|
|
13575
14251
|
{
|
|
13576
14252
|
name: "Start OpenCode V2 for me",
|
|
13577
14253
|
value: "start",
|
|
13578
|
-
description: `Run '
|
|
14254
|
+
description: `Run '${command} serve --port ${port}'`
|
|
13579
14255
|
},
|
|
13580
14256
|
{
|
|
13581
14257
|
name: "Show me the command",
|
|
@@ -13593,14 +14269,15 @@ Port ${port} is already in use.`));
|
|
|
13593
14269
|
blank();
|
|
13594
14270
|
console.log(chalk6.bold("Run this command in another terminal:"));
|
|
13595
14271
|
blank();
|
|
13596
|
-
console.log(` ${chalk6.cyan(
|
|
14272
|
+
console.log(` ${chalk6.cyan(`${command} serve --port ${port}`)}`);
|
|
13597
14273
|
blank();
|
|
13598
14274
|
throw new Error("Please start OpenCode V2 manually");
|
|
13599
14275
|
}
|
|
13600
14276
|
if (action === "start") {
|
|
13601
14277
|
const spinner = ora3("Starting OpenCode V2...").start();
|
|
13602
14278
|
const { child: proc, password } = await startOpenCode2(port, {
|
|
13603
|
-
inheritStdio: ctx.inheritStdio
|
|
14279
|
+
inheritStdio: ctx.inheritStdio,
|
|
14280
|
+
command: ctx.command
|
|
13604
14281
|
});
|
|
13605
14282
|
const health = await waitForOpenCode2Health(port, password, INTERACTIVE_START_TIMEOUT_MS2);
|
|
13606
14283
|
if (!health.healthy) {
|
|
@@ -13626,7 +14303,7 @@ Port ${port} is already in use.`));
|
|
|
13626
14303
|
}
|
|
13627
14304
|
|
|
13628
14305
|
// src/lib/runner-credentials.ts
|
|
13629
|
-
import { chmodSync as chmodSync2, writeFileSync as
|
|
14306
|
+
import { chmodSync as chmodSync2, writeFileSync as writeFileSync5 } from "node:fs";
|
|
13630
14307
|
import { spawn as spawn5 } from "node:child_process";
|
|
13631
14308
|
var RUNNER_SECRET_FETCH_TIMEOUT_MS = 6e4;
|
|
13632
14309
|
var CREDENTIAL_RESTORE_TIMEOUT_MS = 6e4;
|
|
@@ -13869,8 +14546,8 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
13869
14546
|
}
|
|
13870
14547
|
try {
|
|
13871
14548
|
env.GIT_CONFIG_GLOBAL = GIT_CONFIG_GLOBAL;
|
|
13872
|
-
|
|
13873
|
-
|
|
14549
|
+
writeFileSync5(GIT_CONFIG_GLOBAL, "");
|
|
14550
|
+
writeFileSync5(GIT_CREDENTIAL_HELPER, GIT_CREDENTIAL_HELPER_CONTENT, { mode: 448 });
|
|
13874
14551
|
chmodSync2(GIT_CREDENTIAL_HELPER, 448);
|
|
13875
14552
|
const config = [
|
|
13876
14553
|
["user.name", env.GIT_USER_NAME ?? "evident-bot"],
|
|
@@ -13903,7 +14580,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
13903
14580
|
// src/lib/opencode/config-overlay.ts
|
|
13904
14581
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
13905
14582
|
import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
|
|
13906
|
-
import { isAbsolute as isAbsolute3, join as
|
|
14583
|
+
import { isAbsolute as isAbsolute3, join as join12, resolve as resolve4 } from "node:path";
|
|
13907
14584
|
function parseConfigContent(content) {
|
|
13908
14585
|
if (!content?.trim()) return {};
|
|
13909
14586
|
const parsed = JSON.parse(content);
|
|
@@ -13933,7 +14610,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
13933
14610
|
return;
|
|
13934
14611
|
}
|
|
13935
14612
|
const source = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
|
|
13936
|
-
const target = isFile(
|
|
14613
|
+
const target = isFile(join12(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
13937
14614
|
if (!isFile(source)) {
|
|
13938
14615
|
log3(
|
|
13939
14616
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -13941,7 +14618,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
13941
14618
|
);
|
|
13942
14619
|
return;
|
|
13943
14620
|
}
|
|
13944
|
-
copyFileSync(source,
|
|
14621
|
+
copyFileSync(source, join12(cwd, target));
|
|
13945
14622
|
try {
|
|
13946
14623
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
13947
14624
|
stdio: "ignore"
|
|
@@ -13950,13 +14627,13 @@ function applyRunnerOpenCodeConfig({
|
|
|
13950
14627
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
13951
14628
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
13952
14629
|
}
|
|
13953
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
14630
|
+
log3(`Applied runner OpenCode config ${source} to ${join12(cwd, target)}`);
|
|
13954
14631
|
}
|
|
13955
14632
|
|
|
13956
14633
|
// src/lib/opencode/runtime-provenance.ts
|
|
13957
|
-
import { readFileSync as
|
|
14634
|
+
import { readFileSync as readFileSync8, readdirSync } from "node:fs";
|
|
13958
14635
|
import { homedir as homedir7 } from "node:os";
|
|
13959
|
-
import { join as
|
|
14636
|
+
import { join as join13 } from "node:path";
|
|
13960
14637
|
var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
|
|
13961
14638
|
var REGISTRY_SPEC = /^[a-z0-9.*+^~<>=|-]+$/i;
|
|
13962
14639
|
function errorKind(error2) {
|
|
@@ -14024,7 +14701,7 @@ function parseJsonc(value) {
|
|
|
14024
14701
|
return JSON.parse(output);
|
|
14025
14702
|
}
|
|
14026
14703
|
function openCodeCacheRoot(env = process.env, homeDir = homedir7()) {
|
|
14027
|
-
return
|
|
14704
|
+
return join13(env.XDG_CACHE_HOME ?? join13(homeDir, ".cache"), "opencode");
|
|
14028
14705
|
}
|
|
14029
14706
|
function readConfiguredPluginSpecs({
|
|
14030
14707
|
cwd = process.cwd(),
|
|
@@ -14033,10 +14710,10 @@ function readConfiguredPluginSpecs({
|
|
|
14033
14710
|
let config;
|
|
14034
14711
|
try {
|
|
14035
14712
|
try {
|
|
14036
|
-
config = parseJsonc(
|
|
14713
|
+
config = parseJsonc(readFileSync8(join13(cwd, "opencode.jsonc"), "utf8"));
|
|
14037
14714
|
} catch (error2) {
|
|
14038
14715
|
if (!("code" in Object(error2)) || Object(error2).code !== "ENOENT") throw error2;
|
|
14039
|
-
config = parseJsonc(
|
|
14716
|
+
config = parseJsonc(readFileSync8(join13(cwd, "opencode.json"), "utf8"));
|
|
14040
14717
|
}
|
|
14041
14718
|
} catch (error2) {
|
|
14042
14719
|
log3(`Could not read OpenCode plugin configuration: ${errorKind(error2)}`);
|
|
@@ -14064,7 +14741,7 @@ function resolveInstalledPluginVersions({
|
|
|
14064
14741
|
homeDir = homedir7(),
|
|
14065
14742
|
log: log3
|
|
14066
14743
|
}) {
|
|
14067
|
-
const packagesRoot =
|
|
14744
|
+
const packagesRoot = join13(cacheRoot ?? openCodeCacheRoot(env, homeDir), "packages");
|
|
14068
14745
|
let directories;
|
|
14069
14746
|
try {
|
|
14070
14747
|
directories = readdirSync(packagesRoot, { withFileTypes: true });
|
|
@@ -14085,8 +14762,8 @@ function resolveInstalledPluginVersions({
|
|
|
14085
14762
|
let resolvedVersion = null;
|
|
14086
14763
|
try {
|
|
14087
14764
|
const packageJson = JSON.parse(
|
|
14088
|
-
|
|
14089
|
-
|
|
14765
|
+
readFileSync8(
|
|
14766
|
+
join13(packagesRoot, directory.name, "node_modules", parsed.name, "package.json"),
|
|
14090
14767
|
"utf8"
|
|
14091
14768
|
)
|
|
14092
14769
|
);
|
|
@@ -14107,7 +14784,7 @@ function resolveInstalledPluginVersions({
|
|
|
14107
14784
|
}
|
|
14108
14785
|
|
|
14109
14786
|
// src/lib/credential-sync.ts
|
|
14110
|
-
import { renameSync, writeFileSync as
|
|
14787
|
+
import { renameSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
14111
14788
|
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
14112
14789
|
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
14113
14790
|
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
@@ -14141,7 +14818,7 @@ function writeMarker(markerPath, outcomes, log3) {
|
|
|
14141
14818
|
`;
|
|
14142
14819
|
const temporaryPath = `${markerPath}.tmp`;
|
|
14143
14820
|
try {
|
|
14144
|
-
|
|
14821
|
+
writeFileSync6(temporaryPath, body, { mode: 384 });
|
|
14145
14822
|
renameSync(temporaryPath, markerPath);
|
|
14146
14823
|
} catch (error2) {
|
|
14147
14824
|
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage2(error2)}`, "warn");
|
|
@@ -14381,15 +15058,25 @@ async function responseJson(response) {
|
|
|
14381
15058
|
function readSetup(value) {
|
|
14382
15059
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
14383
15060
|
const setup = value;
|
|
14384
|
-
if (typeof setup.runner_id !== "string" || typeof setup.generation !== "string" || typeof setup.status !== "string" || setup.token_id !== null && typeof setup.token_id !== "string") {
|
|
15061
|
+
if (typeof setup.runner_id !== "string" || typeof setup.generation !== "string" || typeof setup.status !== "string" || setup.token_id !== null && typeof setup.token_id !== "string" || typeof setup.acknowledged_at !== "string" && setup.acknowledged_at !== null) {
|
|
14385
15062
|
return null;
|
|
14386
15063
|
}
|
|
14387
15064
|
return setup;
|
|
14388
15065
|
}
|
|
14389
15066
|
function clearLocalSetup(options) {
|
|
14390
15067
|
const env = options.env ?? process.env;
|
|
14391
|
-
|
|
14392
|
-
|
|
15068
|
+
const store = options.store ?? defaultStore;
|
|
15069
|
+
const stored = store.get(options.agentId);
|
|
15070
|
+
store.clear(options.agentId);
|
|
15071
|
+
const token = env.EVIDENT_PROXY_TOKEN;
|
|
15072
|
+
if (token !== void 0 && token === stored?.token) {
|
|
15073
|
+
delete env.EVIDENT_PROXY_TOKEN;
|
|
15074
|
+
} else if (token !== void 0) {
|
|
15075
|
+
options.log(
|
|
15076
|
+
`MANAGED-INFERENCE-CLEANUP-PRESERVED-EXTERNAL-TOKEN: runner=${options.agentId}`,
|
|
15077
|
+
"debug"
|
|
15078
|
+
);
|
|
15079
|
+
}
|
|
14393
15080
|
}
|
|
14394
15081
|
async function discoverManagedInferenceSetup(options) {
|
|
14395
15082
|
const headers = requestHeaders(options);
|
|
@@ -14413,6 +15100,25 @@ async function discoverManagedInferenceSetup(options) {
|
|
|
14413
15100
|
const setup = readSetup(body?.setup);
|
|
14414
15101
|
return setup ? { kind: "setup", setup } : { kind: "failed", reason: "Managed setup discovery returned an invalid response" };
|
|
14415
15102
|
}
|
|
15103
|
+
async function enrollManagedInferenceSetup(options) {
|
|
15104
|
+
const response = await post(options, "enroll", {});
|
|
15105
|
+
if (!response) {
|
|
15106
|
+
return { kind: "failed", reason: safeFailure("Managed credential enrollment") };
|
|
15107
|
+
}
|
|
15108
|
+
if (response.status === 404) {
|
|
15109
|
+
return { kind: "none" };
|
|
15110
|
+
}
|
|
15111
|
+
const body = await responseJson(response);
|
|
15112
|
+
if (!response.ok) {
|
|
15113
|
+
const serverError = body?.error;
|
|
15114
|
+
return {
|
|
15115
|
+
kind: "failed",
|
|
15116
|
+
reason: typeof serverError === "string" && serverError.trim() ? redactDiagnosticText(serverError).slice(0, 500) : safeFailure("Managed credential enrollment", response.status)
|
|
15117
|
+
};
|
|
15118
|
+
}
|
|
15119
|
+
const setup = readSetup(body?.setup);
|
|
15120
|
+
return setup ? { kind: "setup", setup } : { kind: "failed", reason: "Managed credential enrollment returned an invalid response" };
|
|
15121
|
+
}
|
|
14416
15122
|
async function post(options, operation, body) {
|
|
14417
15123
|
const headers = requestHeaders(options);
|
|
14418
15124
|
try {
|
|
@@ -14508,7 +15214,7 @@ async function prepareManagedInferenceSetup(options, discovery, health) {
|
|
|
14508
15214
|
}
|
|
14509
15215
|
return reaffirmStored(options, stored);
|
|
14510
15216
|
}
|
|
14511
|
-
if (discovery.setup.
|
|
15217
|
+
if (discovery.setup.acknowledged_at !== null && stored?.generation === discovery.setup.generation && stored.tokenId === discovery.setup.token_id) {
|
|
14512
15218
|
return reaffirmStored(options, stored);
|
|
14513
15219
|
}
|
|
14514
15220
|
const response = await post(options, "bootstrap", { generation: discovery.setup.generation });
|
|
@@ -14594,33 +15300,49 @@ function resolveLogLevel(options) {
|
|
|
14594
15300
|
}
|
|
14595
15301
|
return "info";
|
|
14596
15302
|
}
|
|
14597
|
-
function
|
|
14598
|
-
const
|
|
15303
|
+
function resolveFileSyncEntry(entry, homeDir, flagName, labels) {
|
|
15304
|
+
const trimmed = entry.trim();
|
|
15305
|
+
if (trimmed === "") {
|
|
15306
|
+
throw new Error(`${flagName} requires a ${labels.noun} path (got an empty value)`);
|
|
15307
|
+
}
|
|
15308
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join14(homeDir, trimmed.slice(2)) : trimmed;
|
|
15309
|
+
if (!isAbsolute4(expanded)) {
|
|
15310
|
+
throw new Error(`${flagName} requires an absolute ${labels.noun} path; got "${entry}"`);
|
|
15311
|
+
}
|
|
15312
|
+
const normalized = resolvePath2(expanded);
|
|
15313
|
+
if (parse(normalized).root === normalized) {
|
|
15314
|
+
throw new Error(
|
|
15315
|
+
`${flagName} will not allow-list the filesystem root ("${entry}"); name the specific ${labels.noun} the credentials belong in (for example ${labels.rootExample})`
|
|
15316
|
+
);
|
|
15317
|
+
}
|
|
15318
|
+
return normalized;
|
|
15319
|
+
}
|
|
15320
|
+
function resolveFileSyncAllowList(raw, homeDir, flagName, labels) {
|
|
15321
|
+
const paths = [];
|
|
14599
15322
|
for (const entry of raw ?? []) {
|
|
14600
|
-
const
|
|
14601
|
-
if (
|
|
14602
|
-
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
14603
|
-
}
|
|
14604
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join13(homeDir, trimmed.slice(2)) : trimmed;
|
|
14605
|
-
if (!isAbsolute4(expanded)) {
|
|
14606
|
-
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
14607
|
-
}
|
|
14608
|
-
const normalized = resolvePath2(expanded);
|
|
14609
|
-
if (parse(normalized).root === normalized) {
|
|
14610
|
-
throw new Error(
|
|
14611
|
-
`--enable-file-sync-to will not allow-list the filesystem root ("${entry}"); name the specific directory the credentials belong in (for example ~/.claude)`
|
|
14612
|
-
);
|
|
14613
|
-
}
|
|
14614
|
-
if (!directories.includes(normalized)) {
|
|
14615
|
-
directories.push(normalized);
|
|
14616
|
-
}
|
|
15323
|
+
const normalized = resolveFileSyncEntry(entry, homeDir, flagName, labels);
|
|
15324
|
+
if (!paths.includes(normalized)) paths.push(normalized);
|
|
14617
15325
|
}
|
|
14618
|
-
if (
|
|
15326
|
+
if (paths.length > MAX_FILE_SYNC_DIRECTORIES) {
|
|
14619
15327
|
throw new Error(
|
|
14620
|
-
|
|
15328
|
+
`${flagName} accepts at most ${MAX_FILE_SYNC_DIRECTORIES} ${labels.plural}; got ${paths.length}`
|
|
14621
15329
|
);
|
|
14622
15330
|
}
|
|
14623
|
-
return
|
|
15331
|
+
return paths;
|
|
15332
|
+
}
|
|
15333
|
+
function resolveFileSyncDirectories(raw, homeDir) {
|
|
15334
|
+
return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to", {
|
|
15335
|
+
noun: "directory",
|
|
15336
|
+
plural: "directories",
|
|
15337
|
+
rootExample: "~/.claude"
|
|
15338
|
+
});
|
|
15339
|
+
}
|
|
15340
|
+
function resolveFileSyncExactFiles(raw, homeDir) {
|
|
15341
|
+
return resolveFileSyncAllowList(raw, homeDir, "--enable-file-sync-to-exact-file", {
|
|
15342
|
+
noun: "file",
|
|
15343
|
+
plural: "files",
|
|
15344
|
+
rootExample: "~/.local/share/opencode/mcp-auth.json"
|
|
15345
|
+
});
|
|
14624
15346
|
}
|
|
14625
15347
|
var DEFAULT_OPENCODE_START_TIMEOUT_SECONDS = 180;
|
|
14626
15348
|
var MAX_OPENCODE_START_TIMEOUT_SECONDS = 3600;
|
|
@@ -14636,13 +15358,13 @@ function resolveOpenCodeVersion(options, env = process.env) {
|
|
|
14636
15358
|
raw = env[OPENCODE_VERSION_ENV];
|
|
14637
15359
|
source = OPENCODE_VERSION_ENV;
|
|
14638
15360
|
} else {
|
|
14639
|
-
return { version: "
|
|
15361
|
+
return { version: "auto", warnings: [] };
|
|
14640
15362
|
}
|
|
14641
15363
|
const normalized = raw.trim().toLowerCase();
|
|
14642
|
-
if (normalized !== "v1" && normalized !== "v2") {
|
|
15364
|
+
if (normalized !== "v1" && normalized !== "v2" && normalized !== "auto") {
|
|
14643
15365
|
return {
|
|
14644
|
-
version: "
|
|
14645
|
-
warnings: [`Ignoring invalid ${source} "${raw}": expected v1 or
|
|
15366
|
+
version: "auto",
|
|
15367
|
+
warnings: [`Ignoring invalid ${source} "${raw}": expected v1, v2, or auto; using auto`]
|
|
14646
15368
|
};
|
|
14647
15369
|
}
|
|
14648
15370
|
return { version: normalized, warnings: [] };
|
|
@@ -15257,7 +15979,7 @@ function shouldWarnForReclaimSkip(reason) {
|
|
|
15257
15979
|
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
15258
15980
|
}
|
|
15259
15981
|
function sessionDbPath() {
|
|
15260
|
-
return
|
|
15982
|
+
return join14(homedir8(), ".local", "share", "opencode", "opencode.db");
|
|
15261
15983
|
}
|
|
15262
15984
|
function logSessionCleanupActivity(state, phase, entry) {
|
|
15263
15985
|
logActivity(state, {
|
|
@@ -15266,6 +15988,47 @@ function logSessionCleanupActivity(state, phase, entry) {
|
|
|
15266
15988
|
metadata: { ...entry.metadata, phase }
|
|
15267
15989
|
});
|
|
15268
15990
|
}
|
|
15991
|
+
async function drainSessionDeletionReports(state) {
|
|
15992
|
+
const path = sessionDeletionReportPath(homedir8(), process.env);
|
|
15993
|
+
let delivered = 0;
|
|
15994
|
+
let failure = null;
|
|
15995
|
+
try {
|
|
15996
|
+
const spool = readSessionDeletionRecords(path);
|
|
15997
|
+
if (spool.readError !== null) {
|
|
15998
|
+
failure = `could not read spool: ${spool.readError}`;
|
|
15999
|
+
} else if (spool.records.length === 0) {
|
|
16000
|
+
return;
|
|
16001
|
+
} else if (!state.agentId || !state.authHeader) {
|
|
16002
|
+
failure = "runner authentication is not available";
|
|
16003
|
+
} else {
|
|
16004
|
+
for (let offset = 0; offset < spool.records.length; offset += 200) {
|
|
16005
|
+
const batch = spool.records.slice(offset, offset + 200);
|
|
16006
|
+
const result = await reportSessionDeletions(state.agentId, state.authHeader, batch);
|
|
16007
|
+
if (!result.ok) {
|
|
16008
|
+
failure = result.error ?? "unknown API error";
|
|
16009
|
+
break;
|
|
16010
|
+
}
|
|
16011
|
+
removeDeliveredSessionDeletionRecords(path, batch);
|
|
16012
|
+
delivered += batch.length;
|
|
16013
|
+
}
|
|
16014
|
+
}
|
|
16015
|
+
} catch (error2) {
|
|
16016
|
+
failure = error2 instanceof Error ? error2.message : String(error2);
|
|
16017
|
+
}
|
|
16018
|
+
const pending = readSessionDeletionRecords(path);
|
|
16019
|
+
const pendingCount = pending.readError === null ? pending.records.length : null;
|
|
16020
|
+
const message = `Session deletion reports: delivered ${delivered}, ${pendingCount === null ? "pending count unavailable" : `${pendingCount} pending`}` + (failure === null ? "." : `; delivery failed: ${failure}.`);
|
|
16021
|
+
logActivity(state, {
|
|
16022
|
+
type: "info",
|
|
16023
|
+
level: failure === null ? "info" : "warn",
|
|
16024
|
+
message,
|
|
16025
|
+
metadata: {
|
|
16026
|
+
outcome: failure === null ? "session-deletion-reports-drained" : "session-deletion-reports-pending",
|
|
16027
|
+
delivered,
|
|
16028
|
+
pending: pendingCount
|
|
16029
|
+
}
|
|
16030
|
+
});
|
|
16031
|
+
}
|
|
15269
16032
|
function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBootMigrationCount) {
|
|
15270
16033
|
const record = {
|
|
15271
16034
|
v: 1,
|
|
@@ -15323,17 +16086,22 @@ async function runSweep(state, driver, config) {
|
|
|
15323
16086
|
let failedThrew = 0;
|
|
15324
16087
|
let skippedNewlyActive = 0;
|
|
15325
16088
|
let remaining = 0;
|
|
16089
|
+
const deletionRecords = [];
|
|
15326
16090
|
let protectedNow = driver.protectedSessionIds();
|
|
15327
16091
|
if (sessions !== null) {
|
|
15328
|
-
const
|
|
15329
|
-
|
|
15330
|
-
|
|
15331
|
-
|
|
15332
|
-
|
|
15333
|
-
|
|
15334
|
-
protectedIds: protectedNow
|
|
15335
|
-
}
|
|
16092
|
+
const snapshots = sessions.map((session) => ({
|
|
16093
|
+
id: session.id,
|
|
16094
|
+
lastActivityMs: sessionLastActivityMs(session)
|
|
16095
|
+
}));
|
|
16096
|
+
const lastActivityById = new Map(
|
|
16097
|
+
snapshots.map(({ id, lastActivityMs }) => [id, lastActivityMs])
|
|
15336
16098
|
);
|
|
16099
|
+
const toDelete = selectSessionsToDelete(snapshots, {
|
|
16100
|
+
maxAgeMs: config.maxAgeMs,
|
|
16101
|
+
maxCount: config.maxCount,
|
|
16102
|
+
nowMs: Date.now(),
|
|
16103
|
+
protectedIds: protectedNow
|
|
16104
|
+
});
|
|
15337
16105
|
const eligibleToDelete = toDelete.filter(
|
|
15338
16106
|
(id) => (state.sessionDeleteFailures.get(id)?.isolatedFailures ?? 0) < SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES
|
|
15339
16107
|
);
|
|
@@ -15353,6 +16121,17 @@ async function runSweep(state, driver, config) {
|
|
|
15353
16121
|
const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
|
|
15354
16122
|
if (outcome.ok) {
|
|
15355
16123
|
deleted++;
|
|
16124
|
+
const lastActivityMs = lastActivityById.get(id) ?? null;
|
|
16125
|
+
deletionRecords.push({
|
|
16126
|
+
opencode_session_id: id,
|
|
16127
|
+
deleted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16128
|
+
reason: "auto_cleanup",
|
|
16129
|
+
policy: {
|
|
16130
|
+
max_age_ms: config.maxAgeMs ?? null,
|
|
16131
|
+
max_count: config.maxCount ?? null,
|
|
16132
|
+
last_activity_at: lastActivityMs === null ? null : new Date(lastActivityMs).toISOString()
|
|
16133
|
+
}
|
|
16134
|
+
});
|
|
15356
16135
|
} else if (outcome.status === "threw") {
|
|
15357
16136
|
failed++;
|
|
15358
16137
|
failedThrew++;
|
|
@@ -15408,6 +16187,32 @@ async function runSweep(state, driver, config) {
|
|
|
15408
16187
|
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
|
|
15409
16188
|
});
|
|
15410
16189
|
}
|
|
16190
|
+
if (deletionRecords.length > 0) {
|
|
16191
|
+
try {
|
|
16192
|
+
const dropped = appendSessionDeletionRecords(
|
|
16193
|
+
sessionDeletionReportPath(homedir8(), process.env),
|
|
16194
|
+
deletionRecords
|
|
16195
|
+
);
|
|
16196
|
+
if (dropped > 0) {
|
|
16197
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
16198
|
+
type: "info",
|
|
16199
|
+
level: "warn",
|
|
16200
|
+
message: `Session cleanup: dropped ${dropped} oldest pending deletion report(s) after reaching the spool limit`,
|
|
16201
|
+
metadata: { outcome: "deletion-reports-dropped", dropped_count: dropped }
|
|
16202
|
+
});
|
|
16203
|
+
}
|
|
16204
|
+
} catch (error2) {
|
|
16205
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
16206
|
+
type: "info",
|
|
16207
|
+
level: "warn",
|
|
16208
|
+
message: `Session cleanup: could not persist ${deletionRecords.length} confirmed deletion report(s): ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
16209
|
+
metadata: {
|
|
16210
|
+
outcome: "deletion-report-spool-write-failed",
|
|
16211
|
+
records: deletionRecords.length
|
|
16212
|
+
}
|
|
16213
|
+
});
|
|
16214
|
+
}
|
|
16215
|
+
}
|
|
15411
16216
|
const sessionMetadata = {
|
|
15412
16217
|
sessions_inspected: sessions?.length ?? null,
|
|
15413
16218
|
sessions_deleted: deleted,
|
|
@@ -15477,6 +16282,7 @@ async function runSweep(state, driver, config) {
|
|
|
15477
16282
|
}
|
|
15478
16283
|
});
|
|
15479
16284
|
}
|
|
16285
|
+
await drainSessionDeletionReports(state);
|
|
15480
16286
|
} catch (error2) {
|
|
15481
16287
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
15482
16288
|
logSessionCleanupActivity(state, "sweep", {
|
|
@@ -15959,9 +16765,11 @@ async function run(options) {
|
|
|
15959
16765
|
const interactive = isInteractive(options.json);
|
|
15960
16766
|
let logLevel;
|
|
15961
16767
|
let fileSyncDirectories;
|
|
16768
|
+
let fileSyncExactFiles;
|
|
15962
16769
|
try {
|
|
15963
16770
|
logLevel = resolveLogLevel(options);
|
|
15964
16771
|
fileSyncDirectories = resolveFileSyncDirectories(options.enableFileSyncTo, homedir8());
|
|
16772
|
+
fileSyncExactFiles = resolveFileSyncExactFiles(options.enableFileSyncToExactFile, homedir8());
|
|
15965
16773
|
if (options.restoreSessionDb && !options.sessionDbNoReplicateMarker) {
|
|
15966
16774
|
throw new Error(
|
|
15967
16775
|
"--restore-session-db requires --session-db-no-replicate-marker <path>; restore failures must block Litestream replication"
|
|
@@ -16037,7 +16845,11 @@ async function run(options) {
|
|
|
16037
16845
|
}
|
|
16038
16846
|
if (fileSyncDirectories.length > 0) {
|
|
16039
16847
|
log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
|
|
16040
|
-
}
|
|
16848
|
+
}
|
|
16849
|
+
if (fileSyncExactFiles.length > 0) {
|
|
16850
|
+
log2(state, `File sync exact files enabled for: ${fileSyncExactFiles.join(", ")}`);
|
|
16851
|
+
}
|
|
16852
|
+
if (fileSyncDirectories.length === 0 && fileSyncExactFiles.length === 0) {
|
|
16041
16853
|
log2(state, "File sync is disabled (no --enable-file-sync-to given)", "debug");
|
|
16042
16854
|
}
|
|
16043
16855
|
if (!options.runner && options.agent) {
|
|
@@ -16319,6 +17131,7 @@ async function run(options) {
|
|
|
16319
17131
|
}
|
|
16320
17132
|
}
|
|
16321
17133
|
reportSessionDbRecovery(state);
|
|
17134
|
+
await drainSessionDeletionReports(state);
|
|
16322
17135
|
if (sessionDbVerifyFatal) {
|
|
16323
17136
|
throw new Error(
|
|
16324
17137
|
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`
|
|
@@ -16338,13 +17151,75 @@ async function run(options) {
|
|
|
16338
17151
|
for (const warning2 of opencodeStartTimeoutWarnings) {
|
|
16339
17152
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
16340
17153
|
}
|
|
16341
|
-
const { version:
|
|
16342
|
-
options,
|
|
16343
|
-
process.env
|
|
16344
|
-
);
|
|
17154
|
+
const { version: opencodeVersionSelection, warnings: opencodeVersionWarnings } = resolveOpenCodeVersion(options, process.env);
|
|
16345
17155
|
for (const warning2 of opencodeVersionWarnings) {
|
|
16346
17156
|
logActivity(state, { type: "info", level: "warn", message: warning2 });
|
|
16347
17157
|
}
|
|
17158
|
+
let opencodeMajor;
|
|
17159
|
+
let opencodeV2Command;
|
|
17160
|
+
let opencodeDetectionSource;
|
|
17161
|
+
let opencodeDetectionLevel = "info";
|
|
17162
|
+
let opencodeDetectionMessage;
|
|
17163
|
+
if (opencodeVersionSelection === "v1") {
|
|
17164
|
+
opencodeMajor = "v1";
|
|
17165
|
+
opencodeDetectionSource = "explicit";
|
|
17166
|
+
opencodeDetectionMessage = "OpenCode V1 was selected explicitly.";
|
|
17167
|
+
} else if (opencodeVersionSelection === "v2") {
|
|
17168
|
+
const installed = resolveInstalledOpenCode();
|
|
17169
|
+
opencodeMajor = "v2";
|
|
17170
|
+
opencodeDetectionSource = "explicit";
|
|
17171
|
+
if (installed.kind === "installed" && installed.major === "v2") {
|
|
17172
|
+
opencodeV2Command = installed.command;
|
|
17173
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; using ${installed.command}${installed.version ? ` (${installed.version})` : ""}.`;
|
|
17174
|
+
} else if (installed.kind === "unknown") {
|
|
17175
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported ${JSON.stringify(installed.rawVersion)}, but no V2 command was resolved.`;
|
|
17176
|
+
} else if (installed.kind === "installed") {
|
|
17177
|
+
opencodeDetectionMessage = `OpenCode V2 was selected explicitly; ${installed.command} reported V1 ${installed.version ?? "with an unknown version"}, so no V2 command was resolved.`;
|
|
17178
|
+
} else {
|
|
17179
|
+
opencodeDetectionMessage = "OpenCode V2 was selected explicitly, but no V2 command was resolved.";
|
|
17180
|
+
}
|
|
17181
|
+
} else {
|
|
17182
|
+
const running = await classifyRunningOpenCode(state.port);
|
|
17183
|
+
if (running.kind === "v1") {
|
|
17184
|
+
opencodeMajor = "v1";
|
|
17185
|
+
opencodeDetectionSource = "running-server";
|
|
17186
|
+
opencodeDetectionMessage = `OpenCode detection: found V1 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
|
|
17187
|
+
} else if (running.kind === "v2") {
|
|
17188
|
+
opencodeMajor = "v2";
|
|
17189
|
+
opencodeDetectionSource = "running-server";
|
|
17190
|
+
opencodeDetectionMessage = `OpenCode detection: found V2 on port ${state.port}${running.version ? ` (${running.version})` : " (version unknown)"}.`;
|
|
17191
|
+
} else if (running.kind === "authenticated-unknown") {
|
|
17192
|
+
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.`;
|
|
17193
|
+
logActivity(state, {
|
|
17194
|
+
type: "info",
|
|
17195
|
+
level: "warn",
|
|
17196
|
+
message: `OpenCode detection: authenticated-unknown on port ${state.port}; ${running.detail}. ${message}`
|
|
17197
|
+
});
|
|
17198
|
+
throw new Error(message);
|
|
17199
|
+
} else {
|
|
17200
|
+
const installed = resolveInstalledOpenCode();
|
|
17201
|
+
opencodeDetectionLevel = "warn";
|
|
17202
|
+
if (installed.kind === "installed") {
|
|
17203
|
+
opencodeMajor = installed.major;
|
|
17204
|
+
opencodeDetectionSource = "installed-binary";
|
|
17205
|
+
if (installed.major === "v2") opencodeV2Command = installed.command;
|
|
17206
|
+
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"}.`;
|
|
17207
|
+
} else if (installed.kind === "unknown") {
|
|
17208
|
+
opencodeMajor = "v1";
|
|
17209
|
+
opencodeDetectionSource = "fallback-v1";
|
|
17210
|
+
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.`;
|
|
17211
|
+
} else {
|
|
17212
|
+
opencodeMajor = "v1";
|
|
17213
|
+
opencodeDetectionSource = "fallback-v1";
|
|
17214
|
+
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.`;
|
|
17215
|
+
}
|
|
17216
|
+
}
|
|
17217
|
+
}
|
|
17218
|
+
logActivity(state, {
|
|
17219
|
+
type: "info",
|
|
17220
|
+
level: opencodeDetectionLevel,
|
|
17221
|
+
message: opencodeDetectionMessage
|
|
17222
|
+
});
|
|
16348
17223
|
const managedSetupOptions = {
|
|
16349
17224
|
agentId: state.agentId,
|
|
16350
17225
|
apiUrl: getApiUrlConfig(),
|
|
@@ -16356,13 +17231,16 @@ async function run(options) {
|
|
|
16356
17231
|
...level === "error" ? { error: message } : { message }
|
|
16357
17232
|
})
|
|
16358
17233
|
};
|
|
16359
|
-
|
|
17234
|
+
let managedDiscovery = await discoverManagedInferenceSetup(managedSetupOptions);
|
|
17235
|
+
if (managedDiscovery.kind === "none") {
|
|
17236
|
+
managedDiscovery = await enrollManagedInferenceSetup(managedSetupOptions);
|
|
17237
|
+
}
|
|
16360
17238
|
let managedPreparation = { kind: "none" };
|
|
16361
17239
|
if (managedDiscovery.kind === "failed") {
|
|
16362
17240
|
managedPreparation = managedDiscovery;
|
|
16363
17241
|
managedSetupOptions.log(`MANAGED-INFERENCE-NOT-READY: ${managedDiscovery.reason}`, "warn");
|
|
16364
17242
|
} else if (managedDiscovery.kind === "setup") {
|
|
16365
|
-
const managedHealth =
|
|
17243
|
+
const managedHealth = opencodeMajor === "v2" ? await checkOpenCode2Health(state.port) : await checkOpenCodeHealth(state.port);
|
|
16366
17244
|
managedPreparation = await prepareManagedInferenceSetup(
|
|
16367
17245
|
managedSetupOptions,
|
|
16368
17246
|
managedDiscovery,
|
|
@@ -16387,13 +17265,14 @@ async function run(options) {
|
|
|
16387
17265
|
const preBootMigrationIds = readSessionDbMigrationIds(sessionDbPath());
|
|
16388
17266
|
const ocSpinner = interactive && !state.json ? ora4("Checking OpenCode...").start() : null;
|
|
16389
17267
|
try {
|
|
16390
|
-
const oc =
|
|
17268
|
+
const oc = opencodeMajor === "v2" ? await ensureOpenCode2Running({
|
|
16391
17269
|
port: state.port,
|
|
16392
17270
|
interactive: state.interactive,
|
|
16393
17271
|
agentId: state.agentId,
|
|
16394
17272
|
log: (message) => log2(state, message),
|
|
16395
17273
|
startTimeoutMs: opencodeStartTimeoutMs,
|
|
16396
|
-
inheritStdio: Boolean(options.opencodePidFile)
|
|
17274
|
+
inheritStdio: Boolean(options.opencodePidFile),
|
|
17275
|
+
command: opencodeV2Command
|
|
16397
17276
|
}) : await ensureOpenCodeRunning({
|
|
16398
17277
|
port: state.port,
|
|
16399
17278
|
interactive: state.interactive,
|
|
@@ -16405,7 +17284,7 @@ async function run(options) {
|
|
|
16405
17284
|
state.port = oc.port;
|
|
16406
17285
|
state.opencodeProcess = options.opencodePidFile ? null : oc.process;
|
|
16407
17286
|
state.opencodeVersion = oc.version;
|
|
16408
|
-
state.opencodeApiVersion =
|
|
17287
|
+
state.opencodeApiVersion = opencodeMajor;
|
|
16409
17288
|
let configuredPlugins = [];
|
|
16410
17289
|
let resolvedPlugins = [];
|
|
16411
17290
|
try {
|
|
@@ -16427,13 +17306,14 @@ async function run(options) {
|
|
|
16427
17306
|
metadata: {
|
|
16428
17307
|
opencode_version: state.opencodeVersion,
|
|
16429
17308
|
opencode_api_version: state.opencodeApiVersion,
|
|
17309
|
+
opencode_detection_source: opencodeDetectionSource,
|
|
16430
17310
|
configured_plugins: JSON.stringify(configuredPlugins),
|
|
16431
17311
|
resolved_plugins: JSON.stringify(resolvedPlugins)
|
|
16432
17312
|
},
|
|
16433
17313
|
source: "cli.runtime-provenance"
|
|
16434
17314
|
});
|
|
16435
17315
|
let opencodePassword = null;
|
|
16436
|
-
if (
|
|
17316
|
+
if (opencodeMajor === "v2" && "password" in oc) {
|
|
16437
17317
|
const value = oc.password;
|
|
16438
17318
|
if (typeof value === "string" || value === null) opencodePassword = value;
|
|
16439
17319
|
}
|
|
@@ -16446,7 +17326,7 @@ async function run(options) {
|
|
|
16446
17326
|
state.opencodeClient = openCodeClient;
|
|
16447
17327
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
16448
17328
|
try {
|
|
16449
|
-
|
|
17329
|
+
writeFileSync7(options.opencodePidFile, `${oc.process.pid}
|
|
16450
17330
|
`, { mode: 384 });
|
|
16451
17331
|
chmodSync3(options.opencodePidFile, 384);
|
|
16452
17332
|
} catch (error2) {
|
|
@@ -16556,7 +17436,7 @@ async function run(options) {
|
|
|
16556
17436
|
let existingPid;
|
|
16557
17437
|
if (existsSync3(options.litestreamPidFile)) {
|
|
16558
17438
|
try {
|
|
16559
|
-
const rawPid =
|
|
17439
|
+
const rawPid = readFileSync9(options.litestreamPidFile, "utf8").trim();
|
|
16560
17440
|
const parsedPid = Number(rawPid);
|
|
16561
17441
|
if (/^\d+$/.test(rawPid) && Number.isSafeInteger(parsedPid) && parsedPid > 0) {
|
|
16562
17442
|
existingPid = parsedPid;
|
|
@@ -16593,7 +17473,7 @@ async function run(options) {
|
|
|
16593
17473
|
});
|
|
16594
17474
|
try {
|
|
16595
17475
|
if (litestreamProcess.pid !== void 0) {
|
|
16596
|
-
|
|
17476
|
+
writeFileSync7(options.litestreamPidFile, `${litestreamProcess.pid}
|
|
16597
17477
|
`, {
|
|
16598
17478
|
mode: 384
|
|
16599
17479
|
});
|
|
@@ -16652,9 +17532,10 @@ async function run(options) {
|
|
|
16652
17532
|
conversationFilter: state.conversationFilter,
|
|
16653
17533
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
16654
17534
|
sessionStoreHealth: state.sessionStoreHealth,
|
|
16655
|
-
// #559: the
|
|
16656
|
-
//
|
|
17535
|
+
// #559: the file-sync allow-lists. Empty ⇒ pending files are REJECTED with
|
|
17536
|
+
// `file_sync_disabled` on the ack, not silently ignored.
|
|
16657
17537
|
fileSyncDirectories,
|
|
17538
|
+
fileSyncExactFiles,
|
|
16658
17539
|
homeDir: homedir8(),
|
|
16659
17540
|
maxActiveSessions,
|
|
16660
17541
|
log: (entry) => (
|
|
@@ -16925,8 +17806,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
16925
17806
|
"--opencode-start-timeout <seconds>",
|
|
16926
17807
|
"Seconds to wait for OpenCode to become healthy when the runner starts it (default: 180). Env: EVIDENT_OPENCODE_START_TIMEOUT"
|
|
16927
17808
|
).option(
|
|
16928
|
-
"--opencode-version <v1|v2>",
|
|
16929
|
-
"
|
|
17809
|
+
"--opencode-version <v1|v2|auto>",
|
|
17810
|
+
"OpenCode major version: auto (default) detects the running server, then installed binary; v1 and v2 skip server-protocol detection. Env: EVIDENT_OPENCODE_VERSION"
|
|
16930
17811
|
).option("--json", "Output in JSON format").option(
|
|
16931
17812
|
"--session-cleanup-max-age <duration>",
|
|
16932
17813
|
"Delete OpenCode sessions idle longer than this (e.g. 7d, 24h). Enables cleanup. Env: EVIDENT_SESSION_CLEANUP_MAX_AGE"
|
|
@@ -16953,6 +17834,11 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
16953
17834
|
"Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.",
|
|
16954
17835
|
(value, previous) => previous.concat([value]),
|
|
16955
17836
|
[]
|
|
17837
|
+
).option(
|
|
17838
|
+
"--enable-file-sync-to-exact-file <path>",
|
|
17839
|
+
"Allow Evident to write files to exactly this path (repeatable).",
|
|
17840
|
+
(value, previous) => previous.concat([value]),
|
|
17841
|
+
[]
|
|
16956
17842
|
).option(
|
|
16957
17843
|
"--tunnel-ready-file <path>",
|
|
16958
17844
|
"Path to write once the tunnel is connected (opt-in; unused unless an operator/image sets it)"
|
|
@@ -17015,6 +17901,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
17015
17901
|
// Raw values — expansion/validation is single-sourced in run.ts's
|
|
17016
17902
|
// resolveFileSyncDirectories.
|
|
17017
17903
|
enableFileSyncTo: options.enableFileSyncTo,
|
|
17904
|
+
// Raw values — expansion/validation is single-sourced in run.ts's
|
|
17905
|
+
// resolveFileSyncExactFiles.
|
|
17906
|
+
enableFileSyncToExactFile: options.enableFileSyncToExactFile,
|
|
17018
17907
|
tunnelReadyFile: options.tunnelReadyFile,
|
|
17019
17908
|
litestreamConfig: options.litestreamConfig,
|
|
17020
17909
|
opencodePidFile: options.opencodePidFile,
|