@evident-ai/cli 3.5.2-dev.e67eb8b → 3.5.2-dev.ffa823b
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 +5 -3
- package/dist/index.js +2120 -583
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { createRequire as createRequire2 } from "module";
|
|
5
4
|
import { Command } from "commander";
|
|
6
5
|
|
|
7
6
|
// src/commands/login.ts
|
|
@@ -316,14 +315,14 @@ function blank() {
|
|
|
316
315
|
console.log();
|
|
317
316
|
}
|
|
318
317
|
function waitForEnter(prompt = "Press Enter to continue...") {
|
|
319
|
-
return new Promise((
|
|
318
|
+
return new Promise((resolve5) => {
|
|
320
319
|
process.stdout.write(chalk.dim(prompt));
|
|
321
320
|
const handler = () => {
|
|
322
321
|
process.stdin.removeListener("data", handler);
|
|
323
322
|
process.stdin.setRawMode?.(false);
|
|
324
323
|
process.stdin.pause();
|
|
325
324
|
console.log();
|
|
326
|
-
|
|
325
|
+
resolve5();
|
|
327
326
|
};
|
|
328
327
|
if (process.stdin.isTTY) {
|
|
329
328
|
process.stdin.setRawMode?.(true);
|
|
@@ -333,7 +332,7 @@ function waitForEnter(prompt = "Press Enter to continue...") {
|
|
|
333
332
|
});
|
|
334
333
|
}
|
|
335
334
|
function sleep(ms) {
|
|
336
|
-
return new Promise((
|
|
335
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
337
336
|
}
|
|
338
337
|
|
|
339
338
|
// src/commands/login.ts
|
|
@@ -411,19 +410,19 @@ async function tokenLogin() {
|
|
|
411
410
|
);
|
|
412
411
|
blank();
|
|
413
412
|
process.stdout.write("Paste token: ");
|
|
414
|
-
const token = await new Promise((
|
|
413
|
+
const token = await new Promise((resolve5) => {
|
|
415
414
|
let data = "";
|
|
416
415
|
process.stdin.setEncoding("utf8");
|
|
417
416
|
process.stdin.on("data", (chunk) => {
|
|
418
417
|
data += chunk;
|
|
419
418
|
});
|
|
420
419
|
process.stdin.on("end", () => {
|
|
421
|
-
|
|
420
|
+
resolve5(data.trim());
|
|
422
421
|
});
|
|
423
422
|
if (process.stdin.isTTY) {
|
|
424
423
|
process.stdin.once("data", (chunk) => {
|
|
425
424
|
process.stdin.pause();
|
|
426
|
-
|
|
425
|
+
resolve5(chunk.toString().trim());
|
|
427
426
|
});
|
|
428
427
|
process.stdin.resume();
|
|
429
428
|
}
|
|
@@ -704,6 +703,31 @@ async function reportResourceUsage(agentId, authHeader, usage) {
|
|
|
704
703
|
opencode_db_bytes: usage.opencodeDbBytes
|
|
705
704
|
});
|
|
706
705
|
}
|
|
706
|
+
async function getRunnerCredentialVault(agentId, authHeader) {
|
|
707
|
+
const empty = { prefix: null, vaultId: null, source: null };
|
|
708
|
+
try {
|
|
709
|
+
const apiUrl = getApiUrlConfig();
|
|
710
|
+
const response = await fetch(`${apiUrl}/runners/${agentId}/credential-vault`, {
|
|
711
|
+
headers: { Authorization: authHeader },
|
|
712
|
+
signal: AbortSignal.timeout(1e4)
|
|
713
|
+
});
|
|
714
|
+
if (!response.ok) {
|
|
715
|
+
const serverMessage = await readErrorMessage(response);
|
|
716
|
+
return {
|
|
717
|
+
...empty,
|
|
718
|
+
error: `HTTP ${response.status}${serverMessage ? `: ${serverMessage}` : ""}`
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const data = await response.json();
|
|
722
|
+
return {
|
|
723
|
+
prefix: data.credential_vault_prefix,
|
|
724
|
+
vaultId: data.vault_id,
|
|
725
|
+
source: data.source
|
|
726
|
+
};
|
|
727
|
+
} catch (error2) {
|
|
728
|
+
return { ...empty, error: describeTimeoutError(error2, 1e4) };
|
|
729
|
+
}
|
|
730
|
+
}
|
|
707
731
|
async function getAgentInfo(agentId, authHeader) {
|
|
708
732
|
const apiUrl = getApiUrlConfig();
|
|
709
733
|
try {
|
|
@@ -878,6 +902,76 @@ async function status(options = {}) {
|
|
|
878
902
|
process.exit(result.exitCode);
|
|
879
903
|
}
|
|
880
904
|
|
|
905
|
+
// src/commands/credential-vault-env.ts
|
|
906
|
+
function shellQuote(value) {
|
|
907
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
908
|
+
}
|
|
909
|
+
function printAssignment(prefix) {
|
|
910
|
+
console.log(`CREDENTIAL_VAULT_PREFIX=${shellQuote(prefix ?? "")}`);
|
|
911
|
+
}
|
|
912
|
+
function safeReason(reason, credentials2, authHeader) {
|
|
913
|
+
let safe = reason;
|
|
914
|
+
for (const secret of [
|
|
915
|
+
authHeader,
|
|
916
|
+
credentials2?.token,
|
|
917
|
+
process.env.EVIDENT_RUNNER_KEY,
|
|
918
|
+
process.env.EVIDENT_AGENT_KEY,
|
|
919
|
+
process.env.EVIDENT_TOKEN
|
|
920
|
+
]) {
|
|
921
|
+
if (secret) safe = safe.replaceAll(secret, "[redacted]");
|
|
922
|
+
}
|
|
923
|
+
return safe.replace(/[\r\n]+/g, " ");
|
|
924
|
+
}
|
|
925
|
+
function printFallback(reason, credentials2, authHeader) {
|
|
926
|
+
printAssignment(null);
|
|
927
|
+
console.error(
|
|
928
|
+
`WARNING: ${safeReason(reason, credentials2, authHeader)}; using this runner's own credential prefix.`
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
async function credentialVaultEnv() {
|
|
932
|
+
let credentials2 = null;
|
|
933
|
+
let authHeader;
|
|
934
|
+
try {
|
|
935
|
+
credentials2 = await getAuthCredentials();
|
|
936
|
+
if (!credentials2) {
|
|
937
|
+
printFallback("no runner credentials are configured");
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
authHeader = getAuthHeader(credentials2);
|
|
941
|
+
const runner = await resolveAgentIdFromKey(authHeader);
|
|
942
|
+
if (!runner.agent_id) {
|
|
943
|
+
printFallback(
|
|
944
|
+
runner.error ?? "the runner identity could not be resolved",
|
|
945
|
+
credentials2,
|
|
946
|
+
authHeader
|
|
947
|
+
);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const lookup = await getRunnerCredentialVault(runner.agent_id, authHeader);
|
|
951
|
+
if (lookup.error) {
|
|
952
|
+
printFallback(`credential vault lookup failed: ${lookup.error}`, credentials2, authHeader);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (lookup.prefix === null) {
|
|
956
|
+
printFallback("this runner is not linked to a credential vault", credentials2, authHeader);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (/[\r\n]/.test(lookup.prefix)) {
|
|
960
|
+
printFallback("credential vault lookup returned an invalid prefix", credentials2, authHeader);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
printAssignment(lookup.prefix);
|
|
964
|
+
console.error(
|
|
965
|
+
`VAULT-MODE: credential stores use vault=${lookup.vaultId} source=${lookup.source} prefix=${lookup.prefix}.`
|
|
966
|
+
);
|
|
967
|
+
} catch (error2) {
|
|
968
|
+
const detail = credentials2 === null ? "credential resolution failed" : `credential vault lookup could not be completed: ${error2 instanceof Error ? error2.message : String(error2)}`;
|
|
969
|
+
printFallback(detail, credentials2, authHeader);
|
|
970
|
+
} finally {
|
|
971
|
+
process.exitCode = 0;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
881
975
|
// src/lib/claude-usage.ts
|
|
882
976
|
import { execFileSync } from "node:child_process";
|
|
883
977
|
import { readFileSync } from "node:fs";
|
|
@@ -1064,12 +1158,82 @@ async function claudeUsage() {
|
|
|
1064
1158
|
// src/commands/run.ts
|
|
1065
1159
|
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1066
1160
|
import { homedir as homedir7 } from "node:os";
|
|
1067
|
-
import { isAbsolute as
|
|
1161
|
+
import { isAbsolute as isAbsolute4, join as join12, parse, resolve as resolvePath2 } from "node:path";
|
|
1068
1162
|
import chalk7 from "chalk";
|
|
1069
1163
|
|
|
1070
1164
|
// ../../packages/types/src/agents/index.ts
|
|
1071
1165
|
var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
|
|
1072
1166
|
|
|
1167
|
+
// ../../packages/types/src/opencode/index.ts
|
|
1168
|
+
function roleOf(m) {
|
|
1169
|
+
if (!m || typeof m !== "object") return void 0;
|
|
1170
|
+
if (typeof m.role === "string") return m.role;
|
|
1171
|
+
const infoRole = m.info?.role;
|
|
1172
|
+
return typeof infoRole === "string" ? infoRole : void 0;
|
|
1173
|
+
}
|
|
1174
|
+
function idOf(m) {
|
|
1175
|
+
if (!m || typeof m !== "object") return void 0;
|
|
1176
|
+
if (typeof m.id === "string") return m.id;
|
|
1177
|
+
const infoId = m.info?.id;
|
|
1178
|
+
return typeof infoId === "string" ? infoId : void 0;
|
|
1179
|
+
}
|
|
1180
|
+
function parentIdOf(m) {
|
|
1181
|
+
if (!m || typeof m !== "object") return void 0;
|
|
1182
|
+
if (typeof m.parentID === "string") return m.parentID;
|
|
1183
|
+
const infoParent = m.info?.parentID;
|
|
1184
|
+
return typeof infoParent === "string" ? infoParent : void 0;
|
|
1185
|
+
}
|
|
1186
|
+
function textPartsOf(m) {
|
|
1187
|
+
if (!m || typeof m !== "object") return [];
|
|
1188
|
+
const parts = Array.isArray(m.parts) ? m.parts : [];
|
|
1189
|
+
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text ?? "").filter(Boolean);
|
|
1190
|
+
}
|
|
1191
|
+
function correlatedAssistantRun(messages, userMessageId) {
|
|
1192
|
+
if (!messages || messages.length === 0) return [];
|
|
1193
|
+
const byParent = messages.filter(
|
|
1194
|
+
(m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
|
|
1195
|
+
);
|
|
1196
|
+
if (byParent.length > 0) return byParent;
|
|
1197
|
+
const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
|
|
1198
|
+
if (userIndex === -1) return [];
|
|
1199
|
+
const run2 = [];
|
|
1200
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
1201
|
+
const role = roleOf(messages[i]);
|
|
1202
|
+
if (role === "user") break;
|
|
1203
|
+
if (role === "assistant") run2.push(messages[i]);
|
|
1204
|
+
}
|
|
1205
|
+
return run2;
|
|
1206
|
+
}
|
|
1207
|
+
function lastText(assistantRun) {
|
|
1208
|
+
for (let i = assistantRun.length - 1; i >= 0; i--) {
|
|
1209
|
+
const textParts = textPartsOf(assistantRun[i]);
|
|
1210
|
+
if (textParts.length > 0) return textParts.join("\n").trim() || null;
|
|
1211
|
+
}
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
function effectiveCorrelatedRun(messages, userMessageId) {
|
|
1215
|
+
const run2 = correlatedAssistantRun(messages, userMessageId);
|
|
1216
|
+
if (lastText(run2) !== null) return run2;
|
|
1217
|
+
const userIndex = messages?.findIndex((m) => idOf(m) === userMessageId) ?? -1;
|
|
1218
|
+
if (userIndex === -1) return run2;
|
|
1219
|
+
let continuation;
|
|
1220
|
+
for (let i = userIndex + 1; messages && i < messages.length; i++) {
|
|
1221
|
+
if (roleOf(messages[i]) === "user") {
|
|
1222
|
+
continuation = messages[i];
|
|
1223
|
+
break;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
const continuationId = idOf(continuation);
|
|
1227
|
+
if (roleOf(continuation) === "user" && textPartsOf(continuation).join("\n").trim().toLowerCase() === "continue" && continuationId) {
|
|
1228
|
+
const continuationRun = correlatedAssistantRun(messages, continuationId);
|
|
1229
|
+
if (continuationRun.length > 0) return continuationRun;
|
|
1230
|
+
}
|
|
1231
|
+
return run2;
|
|
1232
|
+
}
|
|
1233
|
+
function extractReplyTextForMessage(messages, opencodeUserMessageId) {
|
|
1234
|
+
return lastText(effectiveCorrelatedRun(messages, opencodeUserMessageId));
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1073
1237
|
// ../../packages/types/src/telemetry/index.ts
|
|
1074
1238
|
var TelemetryEventTypes = {
|
|
1075
1239
|
// Agent activity events (shown in web UI activity log)
|
|
@@ -1158,7 +1322,7 @@ import ora4 from "ora";
|
|
|
1158
1322
|
import { select as select4 } from "@inquirer/prompts";
|
|
1159
1323
|
|
|
1160
1324
|
// src/lib/telemetry.ts
|
|
1161
|
-
var CLI_VERSION = (true ? "3.5.2-dev.
|
|
1325
|
+
var CLI_VERSION = (true ? "3.5.2-dev.ffa823b" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1162
1326
|
function getCliVersion() {
|
|
1163
1327
|
return CLI_VERSION;
|
|
1164
1328
|
}
|
|
@@ -1319,8 +1483,591 @@ var EventTypes = {
|
|
|
1319
1483
|
DEPRECATED_AGENT_KEY_ENV_USED: "cli.deprecated_agent_key_env_used"
|
|
1320
1484
|
};
|
|
1321
1485
|
|
|
1486
|
+
// src/lib/opencode/session-db-reclaim.ts
|
|
1487
|
+
import { statSync as statSync2, statfsSync } from "node:fs";
|
|
1488
|
+
import { dirname as dirname2 } from "node:path";
|
|
1489
|
+
|
|
1490
|
+
// src/lib/diagnostic-redaction.ts
|
|
1491
|
+
import { homedir as homedir2 } from "node:os";
|
|
1492
|
+
function redactHomeDirectory(message) {
|
|
1493
|
+
const home = homedir2();
|
|
1494
|
+
if (!home || home === "/") {
|
|
1495
|
+
return message;
|
|
1496
|
+
}
|
|
1497
|
+
const pathSegmentCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-";
|
|
1498
|
+
let result = "";
|
|
1499
|
+
let searchFrom = 0;
|
|
1500
|
+
let homeIndex;
|
|
1501
|
+
while ((homeIndex = message.indexOf(home, searchFrom)) !== -1) {
|
|
1502
|
+
const homeEnd = homeIndex + home.length;
|
|
1503
|
+
const previousCharacter = message[homeIndex - 1];
|
|
1504
|
+
const nextCharacter = message[homeEnd];
|
|
1505
|
+
const atPathBoundary = (!previousCharacter || !pathSegmentCharacters.includes(previousCharacter)) && (!nextCharacter || !pathSegmentCharacters.includes(nextCharacter));
|
|
1506
|
+
result += message.slice(searchFrom, homeIndex);
|
|
1507
|
+
result += atPathBoundary ? "~" : home;
|
|
1508
|
+
searchFrom = homeEnd;
|
|
1509
|
+
}
|
|
1510
|
+
return result + message.slice(searchFrom);
|
|
1511
|
+
}
|
|
1512
|
+
function redactDiagnosticText(message) {
|
|
1513
|
+
const redacted = message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/(?<![A-Za-z0-9_-])sk-ant-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g, "sk-ant-***").replace(/(?<![A-Za-z0-9_-])sk-proj-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g, "sk-proj-***").replace(/(?<![A-Za-z0-9_-])sk-[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/g, "sk-***").replace(/(?<![A-Za-z0-9_-])(gh[oprsu])_[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/g, "$1_***").replace(/(?<![A-Za-z0-9_-])github_pat_[A-Za-z0-9_]{20,}(?![A-Za-z0-9_-])/g, "github_pat_***").replace(/https?:\/\/\S+/g, "<url>");
|
|
1514
|
+
return redactHomeDirectory(redacted);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// src/lib/opencode/session-db-reclaim.ts
|
|
1518
|
+
var SESSION_DB_RECLAIM_CHUNK_PAGES = 100;
|
|
1519
|
+
var SESSION_DB_RECLAIM_TIME_BUDGET_MS = 500;
|
|
1520
|
+
var SESSION_DB_RECLAIM_PAGE_CEILING = 4e4;
|
|
1521
|
+
var SESSION_DB_RECLAIM_BUSY_TIMEOUT_MS = 5e3;
|
|
1522
|
+
function errorMessage(error2) {
|
|
1523
|
+
if (!(error2 instanceof Error)) return String(error2);
|
|
1524
|
+
return error2.cause instanceof Error ? error2.cause.message : error2.message;
|
|
1525
|
+
}
|
|
1526
|
+
function errorMessages(error2) {
|
|
1527
|
+
const messages = [];
|
|
1528
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1529
|
+
let current = error2;
|
|
1530
|
+
while (current !== null && current !== void 0 && !visited.has(current)) {
|
|
1531
|
+
visited.add(current);
|
|
1532
|
+
if (typeof current === "string") {
|
|
1533
|
+
messages.push(current);
|
|
1534
|
+
break;
|
|
1535
|
+
}
|
|
1536
|
+
if (current instanceof Error) {
|
|
1537
|
+
messages.push(current.name, current.message);
|
|
1538
|
+
current = current.cause;
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
if (typeof current === "object") {
|
|
1542
|
+
const record = current;
|
|
1543
|
+
if (typeof record.code === "string") messages.push(record.code);
|
|
1544
|
+
if (typeof record.message === "string") messages.push(record.message);
|
|
1545
|
+
current = record.cause;
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
break;
|
|
1549
|
+
}
|
|
1550
|
+
return messages;
|
|
1551
|
+
}
|
|
1552
|
+
function isSessionStoreWriteFailure(error2) {
|
|
1553
|
+
return errorMessages(error2).some(
|
|
1554
|
+
(message) => /(?:database or disk is full|disk is full|no space left on device|SQLITE_(?:FULL|IOERR)(?:\b|_))/i.test(
|
|
1555
|
+
message
|
|
1556
|
+
)
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
var SQLITE_CAUSE_TOKEN = /(?:^|cause:\s+)(SQLiteError:\s+[A-Za-z][A-Za-z0-9]*(?:\s+[A-Za-z][A-Za-z0-9]*){0,7})(?=\s*(?:[)\]"',;]|$))/i;
|
|
1560
|
+
var EFFECT_SQL_CAUSE_TOKEN = /(?:^|cause:\s+)(effect\/sql\/SqlError\/[A-Za-z][A-Za-z0-9_]*)/i;
|
|
1561
|
+
var PARAMS_FIELD = /\bparams\s*(?:=|:)/i;
|
|
1562
|
+
function isCauseBearingText(text) {
|
|
1563
|
+
return /^(?:SQLiteError:\s+|effect\/sql\/SqlError\/)/i.test(text.trim()) || /(?:^|\s)cause:\s+(?:SQLiteError:\s+|effect\/sql\/SqlError\/)/i.test(text);
|
|
1564
|
+
}
|
|
1565
|
+
function structuredLogFields(line) {
|
|
1566
|
+
const fields = [];
|
|
1567
|
+
for (let index = 0; index < line.length; index++) {
|
|
1568
|
+
if (line[index] === '"') {
|
|
1569
|
+
index++;
|
|
1570
|
+
while (index < line.length) {
|
|
1571
|
+
if (line[index] === "\\") {
|
|
1572
|
+
index++;
|
|
1573
|
+
} else if (line[index] === '"') {
|
|
1574
|
+
break;
|
|
1575
|
+
}
|
|
1576
|
+
index++;
|
|
1577
|
+
}
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
const atFieldBoundary = index === 0 || /\s/.test(line[index - 1]);
|
|
1581
|
+
const key = line.startsWith("error=", index) ? "error=" : line.startsWith("cause=", index) ? "cause=" : null;
|
|
1582
|
+
if (!atFieldBoundary || key === null) continue;
|
|
1583
|
+
const valueStart = index + key.length;
|
|
1584
|
+
if (line[valueStart] !== '"') {
|
|
1585
|
+
const valueEnd2 = line.indexOf(" ", valueStart);
|
|
1586
|
+
fields.push(line.slice(valueStart, valueEnd2 === -1 ? line.length : valueEnd2));
|
|
1587
|
+
index = valueEnd2 === -1 ? line.length : valueEnd2;
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
let valueEnd = valueStart + 1;
|
|
1591
|
+
for (; valueEnd < line.length; valueEnd++) {
|
|
1592
|
+
if (line[valueEnd] === "\\") {
|
|
1593
|
+
valueEnd++;
|
|
1594
|
+
} else if (line[valueEnd] === '"') {
|
|
1595
|
+
break;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
fields.push(line.slice(valueStart + 1, valueEnd));
|
|
1599
|
+
index = valueEnd;
|
|
1600
|
+
}
|
|
1601
|
+
return fields;
|
|
1602
|
+
}
|
|
1603
|
+
function structuredCauseMessages(text) {
|
|
1604
|
+
const fields = structuredLogFields(text);
|
|
1605
|
+
if (fields.length > 0) return fields;
|
|
1606
|
+
return isCauseBearingText(text) ? [text] : [];
|
|
1607
|
+
}
|
|
1608
|
+
function causeTokenFromText(text) {
|
|
1609
|
+
const paramsIndex = text.search(PARAMS_FIELD);
|
|
1610
|
+
for (const pattern of [SQLITE_CAUSE_TOKEN, EFFECT_SQL_CAUSE_TOKEN]) {
|
|
1611
|
+
const match = pattern.exec(text);
|
|
1612
|
+
if (match?.[1] && (paramsIndex === -1 || match.index < paramsIndex)) {
|
|
1613
|
+
return match[1];
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
return null;
|
|
1617
|
+
}
|
|
1618
|
+
function sessionStoreCauseToken(value) {
|
|
1619
|
+
for (const message of structuredCauseMessages(value)) {
|
|
1620
|
+
const token = causeTokenFromText(message);
|
|
1621
|
+
if (token !== null) return redactDiagnosticText(token).slice(0, 120);
|
|
1622
|
+
}
|
|
1623
|
+
return null;
|
|
1624
|
+
}
|
|
1625
|
+
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
1626
|
+
try {
|
|
1627
|
+
const fsStats = statfsSync(dirname2(dbPath));
|
|
1628
|
+
const availableBytes = fsStats.bavail * fsStats.bsize;
|
|
1629
|
+
if (availableBytes < requiredBytes) {
|
|
1630
|
+
return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
|
|
1631
|
+
}
|
|
1632
|
+
return null;
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
function readLogicalBytes(db) {
|
|
1638
|
+
const pageCount = db.prepare("PRAGMA page_count").get().page_count;
|
|
1639
|
+
const pageSize = db.prepare("PRAGMA page_size").get().page_size;
|
|
1640
|
+
return pageCount * pageSize;
|
|
1641
|
+
}
|
|
1642
|
+
function readFreelistCount(db) {
|
|
1643
|
+
return db.prepare("PRAGMA freelist_count").get().freelist_count;
|
|
1644
|
+
}
|
|
1645
|
+
function readCheckpointResult(db) {
|
|
1646
|
+
db.exec("PRAGMA busy_timeout=0");
|
|
1647
|
+
const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
1648
|
+
return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
|
|
1649
|
+
}
|
|
1650
|
+
async function probeReclaimAvailability(input) {
|
|
1651
|
+
const { dbPath, requiredBytes } = input;
|
|
1652
|
+
let sqlite;
|
|
1653
|
+
try {
|
|
1654
|
+
sqlite = await import("node:sqlite");
|
|
1655
|
+
} catch (err) {
|
|
1656
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
1657
|
+
console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
|
|
1658
|
+
return { reason: "sqlite-unavailable", detail };
|
|
1659
|
+
}
|
|
1660
|
+
let autoVacuum = null;
|
|
1661
|
+
try {
|
|
1662
|
+
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
1663
|
+
try {
|
|
1664
|
+
db.exec(`PRAGMA busy_timeout=${SESSION_DB_RECLAIM_BUSY_TIMEOUT_MS}`);
|
|
1665
|
+
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
1666
|
+
} finally {
|
|
1667
|
+
db.close();
|
|
1668
|
+
}
|
|
1669
|
+
} catch (err) {
|
|
1670
|
+
console.warn(
|
|
1671
|
+
`[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
if (autoVacuum !== 0) return null;
|
|
1675
|
+
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
1676
|
+
}
|
|
1677
|
+
async function reclaimSessionDbSpace(input) {
|
|
1678
|
+
const {
|
|
1679
|
+
dbPath,
|
|
1680
|
+
pageCeiling = SESSION_DB_RECLAIM_PAGE_CEILING,
|
|
1681
|
+
timeBudgetMs = SESSION_DB_RECLAIM_TIME_BUDGET_MS,
|
|
1682
|
+
chunkPages = SESSION_DB_RECLAIM_CHUNK_PAGES,
|
|
1683
|
+
now = () => performance.now(),
|
|
1684
|
+
hasLiveTurn = false,
|
|
1685
|
+
hasProvenanceAnomaly = false
|
|
1686
|
+
} = input;
|
|
1687
|
+
let sqlite;
|
|
1688
|
+
try {
|
|
1689
|
+
sqlite = await import("node:sqlite");
|
|
1690
|
+
} catch (err) {
|
|
1691
|
+
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
1692
|
+
console.warn(
|
|
1693
|
+
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
|
|
1694
|
+
);
|
|
1695
|
+
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
1696
|
+
}
|
|
1697
|
+
const { DatabaseSync } = sqlite;
|
|
1698
|
+
let db;
|
|
1699
|
+
try {
|
|
1700
|
+
db = new DatabaseSync(dbPath);
|
|
1701
|
+
db.exec(`PRAGMA busy_timeout=${SESSION_DB_RECLAIM_BUSY_TIMEOUT_MS}`);
|
|
1702
|
+
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
1703
|
+
if (autoVacuum === 0) {
|
|
1704
|
+
if (hasLiveTurn || hasProvenanceAnomaly) {
|
|
1705
|
+
const detail = [
|
|
1706
|
+
...hasLiveTurn ? ["a session turn is live"] : [],
|
|
1707
|
+
...hasProvenanceAnomaly ? ["session-DB provenance is anomalous"] : []
|
|
1708
|
+
].join(" and ");
|
|
1709
|
+
console.warn(`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${detail}`);
|
|
1710
|
+
return {
|
|
1711
|
+
ok: false,
|
|
1712
|
+
skipped: "full-vacuum-blocked",
|
|
1713
|
+
detail
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
const fileBytesForGuard = statSync2(dbPath).size;
|
|
1717
|
+
const skipReason = insufficientSpaceReason(dbPath, fileBytesForGuard);
|
|
1718
|
+
if (skipReason !== null) {
|
|
1719
|
+
console.warn(
|
|
1720
|
+
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: ${skipReason}`
|
|
1721
|
+
);
|
|
1722
|
+
return { ok: false, skipped: "insufficient-disk-space", detail: skipReason };
|
|
1723
|
+
}
|
|
1724
|
+
const beforeBytes = readLogicalBytes(db);
|
|
1725
|
+
const freelistBefore = readFreelistCount(db);
|
|
1726
|
+
const startedAt = now();
|
|
1727
|
+
db.exec("PRAGMA auto_vacuum=INCREMENTAL");
|
|
1728
|
+
db.exec("VACUUM");
|
|
1729
|
+
const afterBytes = readLogicalBytes(db);
|
|
1730
|
+
const freelistAfter = readFreelistCount(db);
|
|
1731
|
+
const checkpointStartedAt = now();
|
|
1732
|
+
const checkpoint = readCheckpointResult(db);
|
|
1733
|
+
const checkpointElapsedMs = Math.max(0, now() - checkpointStartedAt);
|
|
1734
|
+
return {
|
|
1735
|
+
ok: true,
|
|
1736
|
+
mode: "convert",
|
|
1737
|
+
beforeBytes,
|
|
1738
|
+
afterBytes,
|
|
1739
|
+
freelistBefore,
|
|
1740
|
+
freelistAfter,
|
|
1741
|
+
pagesReclaimed: Math.max(0, freelistBefore - freelistAfter),
|
|
1742
|
+
stoppedBy: "freelist-drained",
|
|
1743
|
+
elapsedMs: Math.max(0, now() - startedAt),
|
|
1744
|
+
checkpointElapsedMs,
|
|
1745
|
+
checkpoint
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
if (autoVacuum === 2) {
|
|
1749
|
+
if (hasLiveTurn) {
|
|
1750
|
+
const detail = "a session turn is live";
|
|
1751
|
+
console.warn(
|
|
1752
|
+
`[reclaimSessionDbSpace] skipping incremental reclaim of ${dbPath}: ${detail}`
|
|
1753
|
+
);
|
|
1754
|
+
return { ok: false, skipped: "live-turn", detail };
|
|
1755
|
+
}
|
|
1756
|
+
const beforeBytes = readLogicalBytes(db);
|
|
1757
|
+
const freelistBefore = readFreelistCount(db);
|
|
1758
|
+
const startedAt = now();
|
|
1759
|
+
const pageLimit = Math.max(0, Math.trunc(pageCeiling));
|
|
1760
|
+
const chunkLimit = Math.max(1, Math.trunc(chunkPages));
|
|
1761
|
+
const budget = Math.max(0, timeBudgetMs);
|
|
1762
|
+
let pagesAttempted = 0;
|
|
1763
|
+
let freelistAfter = freelistBefore;
|
|
1764
|
+
let stoppedBy = "freelist-drained";
|
|
1765
|
+
while (freelistAfter > 0) {
|
|
1766
|
+
if (pagesAttempted >= pageLimit) {
|
|
1767
|
+
stoppedBy = "page-ceiling";
|
|
1768
|
+
break;
|
|
1769
|
+
}
|
|
1770
|
+
if (now() - startedAt >= budget) {
|
|
1771
|
+
stoppedBy = "time-budget";
|
|
1772
|
+
break;
|
|
1773
|
+
}
|
|
1774
|
+
const pages = Math.min(chunkLimit, pageLimit - pagesAttempted, freelistAfter);
|
|
1775
|
+
db.exec(`PRAGMA incremental_vacuum(${pages})`);
|
|
1776
|
+
pagesAttempted += pages;
|
|
1777
|
+
freelistAfter = readFreelistCount(db);
|
|
1778
|
+
}
|
|
1779
|
+
const afterBytes = readLogicalBytes(db);
|
|
1780
|
+
const checkpointStartedAt = now();
|
|
1781
|
+
const checkpoint = readCheckpointResult(db);
|
|
1782
|
+
const checkpointElapsedMs = Math.max(0, now() - checkpointStartedAt);
|
|
1783
|
+
return {
|
|
1784
|
+
ok: true,
|
|
1785
|
+
mode: "incremental",
|
|
1786
|
+
beforeBytes,
|
|
1787
|
+
afterBytes,
|
|
1788
|
+
freelistBefore,
|
|
1789
|
+
freelistAfter,
|
|
1790
|
+
pagesReclaimed: Math.max(0, freelistBefore - freelistAfter),
|
|
1791
|
+
stoppedBy,
|
|
1792
|
+
elapsedMs: Math.max(0, now() - startedAt),
|
|
1793
|
+
checkpointElapsedMs,
|
|
1794
|
+
checkpoint
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
console.warn(
|
|
1798
|
+
`[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
|
|
1799
|
+
);
|
|
1800
|
+
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
1801
|
+
} catch (err) {
|
|
1802
|
+
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
1803
|
+
return {
|
|
1804
|
+
ok: false,
|
|
1805
|
+
skipped: isSessionStoreWriteFailure(err) ? "disk-exhaustion" : "reclaim-error",
|
|
1806
|
+
detail: errorMessage(err)
|
|
1807
|
+
};
|
|
1808
|
+
} finally {
|
|
1809
|
+
db?.close();
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// src/lib/opencode/dependency-install-diagnostic.ts
|
|
1814
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
1815
|
+
import { basename, dirname as dirname3, isAbsolute, join as join2, relative, resolve, sep } from "node:path";
|
|
1816
|
+
var DEPENDENCY_INSTALL_FAILURE_MESSAGE = "background dependency install failed";
|
|
1817
|
+
var MAX_DISTINCT_DIAGNOSTICS = 32;
|
|
1818
|
+
var EFFECT = "OpenCode could not install background dependencies for this scope; its plugins and tools may be unavailable";
|
|
1819
|
+
function readOpenCodeLogField(line, key) {
|
|
1820
|
+
const match = line.match(new RegExp(`(?:^|\\s)${key}=(?:"((?:\\\\.|[^"])*)"|(\\S+))`));
|
|
1821
|
+
if (!match) return void 0;
|
|
1822
|
+
if (match[1] === void 0) return match[2];
|
|
1823
|
+
return match[1].replace(/\\(["\\n])/g, (_match, escaped) => {
|
|
1824
|
+
return escaped === "n" ? "\n" : escaped;
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
function parseTrigger(line) {
|
|
1828
|
+
if (readOpenCodeLogField(line, "message") !== DEPENDENCY_INSTALL_FAILURE_MESSAGE) {
|
|
1829
|
+
return null;
|
|
1830
|
+
}
|
|
1831
|
+
const error2 = readOpenCodeLogField(line, "error");
|
|
1832
|
+
if (!error2 || !/\bEACCES\b/.test(error2)) return null;
|
|
1833
|
+
const errnoMatch = error2.match(/\bEACCES:\s*permission denied,\s*([a-z_]+)\s+'([^']*)'/);
|
|
1834
|
+
const capturedPath = errnoMatch?.[2];
|
|
1835
|
+
return {
|
|
1836
|
+
dir: readOpenCodeLogField(line, "dir"),
|
|
1837
|
+
op: errnoMatch?.[1],
|
|
1838
|
+
path: capturedPath && isAbsolute(capturedPath) ? capturedPath : void 0
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
function getErrorCode(error2) {
|
|
1842
|
+
if (typeof error2 === "object" && error2 !== null && "code" in error2) {
|
|
1843
|
+
const code = error2.code;
|
|
1844
|
+
if (typeof code === "string") return code;
|
|
1845
|
+
}
|
|
1846
|
+
return "UNKNOWN";
|
|
1847
|
+
}
|
|
1848
|
+
function isWithin(path, root) {
|
|
1849
|
+
const candidate = resolve(path);
|
|
1850
|
+
const resolvedRoot = resolve(root);
|
|
1851
|
+
return candidate === resolvedRoot || candidate.startsWith(`${resolvedRoot}${sep}`);
|
|
1852
|
+
}
|
|
1853
|
+
function rootSpecs(context) {
|
|
1854
|
+
return [
|
|
1855
|
+
{ token: "~", configured: resolve(context.homeDir) },
|
|
1856
|
+
{ token: "$WORKSPACE", configured: resolve(context.cwd) }
|
|
1857
|
+
];
|
|
1858
|
+
}
|
|
1859
|
+
function resolveRoots(context) {
|
|
1860
|
+
const resolved = [];
|
|
1861
|
+
const unresolved = [];
|
|
1862
|
+
for (const spec of rootSpecs(context)) {
|
|
1863
|
+
try {
|
|
1864
|
+
resolved.push({ ...spec, real: realpathSync(spec.configured) });
|
|
1865
|
+
} catch (_error) {
|
|
1866
|
+
unresolved.push(spec);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
return { resolved, unresolved };
|
|
1870
|
+
}
|
|
1871
|
+
function resolveNearestAncestor(failingPath, remaining, error2) {
|
|
1872
|
+
let ancestor = dirname3(failingPath);
|
|
1873
|
+
const suffix = [basename(failingPath), ...remaining];
|
|
1874
|
+
while (ancestor) {
|
|
1875
|
+
try {
|
|
1876
|
+
return {
|
|
1877
|
+
path: resolve(realpathSync(ancestor), ...suffix),
|
|
1878
|
+
failure: { path: resolve(failingPath), error: error2 }
|
|
1879
|
+
};
|
|
1880
|
+
} catch (ancestorError) {
|
|
1881
|
+
if (ancestor === dirname3(ancestor) || !getErrorCode(ancestorError).startsWith("E")) {
|
|
1882
|
+
return null;
|
|
1883
|
+
}
|
|
1884
|
+
suffix.unshift(basename(ancestor));
|
|
1885
|
+
ancestor = dirname3(ancestor);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
return null;
|
|
1889
|
+
}
|
|
1890
|
+
function resolveExistingPath(absolutePath) {
|
|
1891
|
+
if (!isAbsolute(absolutePath)) return null;
|
|
1892
|
+
const candidate = resolve(absolutePath);
|
|
1893
|
+
const remaining = [];
|
|
1894
|
+
let current = candidate;
|
|
1895
|
+
while (current) {
|
|
1896
|
+
try {
|
|
1897
|
+
const stat2 = lstatSync(current);
|
|
1898
|
+
if (stat2.isSymbolicLink()) {
|
|
1899
|
+
if (remaining.length === 0) {
|
|
1900
|
+
return { path: join2(realpathSync(dirname3(current)), basename(current)) };
|
|
1901
|
+
}
|
|
1902
|
+
return { path: resolve(realpathSync(current), ...remaining) };
|
|
1903
|
+
}
|
|
1904
|
+
return { path: resolve(realpathSync(current), ...remaining) };
|
|
1905
|
+
} catch (error2) {
|
|
1906
|
+
const code = getErrorCode(error2);
|
|
1907
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
1908
|
+
return resolveNearestAncestor(current, remaining, error2);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
if (current === dirname3(current)) return null;
|
|
1912
|
+
remaining.unshift(basename(current));
|
|
1913
|
+
current = dirname3(current);
|
|
1914
|
+
}
|
|
1915
|
+
return null;
|
|
1916
|
+
}
|
|
1917
|
+
function tokenise(absolutePath, roots) {
|
|
1918
|
+
const pathResolution = resolveExistingPath(absolutePath);
|
|
1919
|
+
if (!pathResolution) return null;
|
|
1920
|
+
const root = roots.reduce((selected, candidate) => {
|
|
1921
|
+
if (!isWithin(pathResolution.path, candidate.real)) return selected;
|
|
1922
|
+
if (!selected || candidate.real.length > selected.real.length || candidate.real.length === selected.real.length && candidate.token === "$WORKSPACE") {
|
|
1923
|
+
return candidate;
|
|
1924
|
+
}
|
|
1925
|
+
return selected;
|
|
1926
|
+
}, null);
|
|
1927
|
+
if (!root) return null;
|
|
1928
|
+
const suffix = relative(root.real, pathResolution.path);
|
|
1929
|
+
return {
|
|
1930
|
+
value: suffix ? `${root.token}/${suffix.split(sep).join("/")}` : root.token,
|
|
1931
|
+
root,
|
|
1932
|
+
resolutionFailure: pathResolution.failure
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
function classifyScope(dir, roots) {
|
|
1936
|
+
if (!dir) return "other";
|
|
1937
|
+
const tokenisedDir = tokenise(dir, roots.resolved);
|
|
1938
|
+
if (tokenisedDir) return tokenisedDir.root.token === "$WORKSPACE" ? "workspace" : "global";
|
|
1939
|
+
for (const spec of roots.unresolved) {
|
|
1940
|
+
if (isWithin(dir, spec.configured)) {
|
|
1941
|
+
return spec.token === "$WORKSPACE" ? "workspace" : "global";
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
return "other";
|
|
1945
|
+
}
|
|
1946
|
+
function unresolvedRootForPath(path, unresolved) {
|
|
1947
|
+
return unresolved.find((spec) => isWithin(path, spec.configured));
|
|
1948
|
+
}
|
|
1949
|
+
function statType(stat2) {
|
|
1950
|
+
if (stat2.isDirectory()) return "d";
|
|
1951
|
+
if (stat2.isFile()) return "f";
|
|
1952
|
+
if (stat2.isSymbolicLink()) return "l";
|
|
1953
|
+
return "o";
|
|
1954
|
+
}
|
|
1955
|
+
function renderStat(name, stat2) {
|
|
1956
|
+
const mode = (stat2.mode & 4095).toString(8).padStart(4, "0");
|
|
1957
|
+
return `${name}:${statType(stat2)}:${stat2.uid}:${stat2.gid}:${mode}`;
|
|
1958
|
+
}
|
|
1959
|
+
function renderStatError(name, error2) {
|
|
1960
|
+
const code = getErrorCode(error2);
|
|
1961
|
+
return `${name}:${renderErrorCode(code)}`;
|
|
1962
|
+
}
|
|
1963
|
+
function renderErrorCode(code) {
|
|
1964
|
+
if (code === "ENOENT") return "missing";
|
|
1965
|
+
return code.startsWith("E") ? code : `E${code}`;
|
|
1966
|
+
}
|
|
1967
|
+
function inspectChain(rootReal, rootToken, absolutePath, resolutionFailure) {
|
|
1968
|
+
const target = resolve(absolutePath);
|
|
1969
|
+
const relativePath = relative(rootReal, target);
|
|
1970
|
+
const components = relativePath ? relativePath.split(sep) : [];
|
|
1971
|
+
const entries = [];
|
|
1972
|
+
let current = rootReal;
|
|
1973
|
+
let status2 = "complete";
|
|
1974
|
+
let finalStat;
|
|
1975
|
+
for (const name of [rootToken, ...components]) {
|
|
1976
|
+
if (name !== rootToken) current = join2(current, name);
|
|
1977
|
+
if (resolutionFailure && resolve(current) === resolve(resolutionFailure.path)) {
|
|
1978
|
+
entries.push(renderStatError(name, resolutionFailure.error));
|
|
1979
|
+
status2 = "stat_failed";
|
|
1980
|
+
break;
|
|
1981
|
+
}
|
|
1982
|
+
try {
|
|
1983
|
+
const stat2 = lstatSync(current);
|
|
1984
|
+
entries.push(renderStat(name, stat2));
|
|
1985
|
+
finalStat = stat2;
|
|
1986
|
+
} catch (error2) {
|
|
1987
|
+
entries.push(renderStatError(name, error2));
|
|
1988
|
+
if (getErrorCode(error2) !== "ENOENT") status2 = "stat_failed";
|
|
1989
|
+
break;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
let symlinkTarget;
|
|
1993
|
+
let symlinkMeta;
|
|
1994
|
+
if (finalStat?.isSymbolicLink() && entries.length === components.length + 1) {
|
|
1995
|
+
try {
|
|
1996
|
+
symlinkTarget = realpathSync(current);
|
|
1997
|
+
try {
|
|
1998
|
+
symlinkMeta = renderStat("", lstatSync(symlinkTarget)).slice(1);
|
|
1999
|
+
} catch (error2) {
|
|
2000
|
+
symlinkMeta = renderErrorCode(getErrorCode(error2));
|
|
2001
|
+
status2 = "stat_failed";
|
|
2002
|
+
}
|
|
2003
|
+
} catch (error2) {
|
|
2004
|
+
symlinkTarget = "<unresolved>";
|
|
2005
|
+
symlinkMeta = renderErrorCode(getErrorCode(error2));
|
|
2006
|
+
status2 = "stat_failed";
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
return { status: status2, chain: entries.join(","), symlinkTarget, symlinkMeta };
|
|
2010
|
+
}
|
|
2011
|
+
function buildDiagnostic(line, context) {
|
|
2012
|
+
const trigger = parseTrigger(line);
|
|
2013
|
+
if (!trigger) return null;
|
|
2014
|
+
const roots = resolveRoots(context);
|
|
2015
|
+
const scope = classifyScope(trigger.dir, roots);
|
|
2016
|
+
const pathSource = trigger.path ? "error" : trigger.dir ? "dir" : "none";
|
|
2017
|
+
const inspectedPath = trigger.path ?? trigger.dir;
|
|
2018
|
+
const microvmId = context.microvmId?.trim() || "unknown";
|
|
2019
|
+
const euid = context.euid === void 0 ? "unavailable" : String(context.euid);
|
|
2020
|
+
const egid = context.egid === void 0 ? "unavailable" : String(context.egid);
|
|
2021
|
+
const op = trigger.op ?? "unknown";
|
|
2022
|
+
if (!inspectedPath) {
|
|
2023
|
+
return {
|
|
2024
|
+
event: `OPENCODE-DEPENDENCY-INSTALL-EACCES: status=no_path path_source=${pathSource} scope=${scope} microvm_id=${microvmId} op=${op} path=unknown cli_euid=${euid} cli_egid=${egid} effect="${EFFECT}" chain=unavailable`,
|
|
2025
|
+
localDetail: null
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
const tokenisedPath = tokenise(inspectedPath, roots.resolved);
|
|
2029
|
+
if (!tokenisedPath) {
|
|
2030
|
+
const unresolvedRoot = unresolvedRootForPath(inspectedPath, roots.unresolved);
|
|
2031
|
+
if (unresolvedRoot) {
|
|
2032
|
+
return {
|
|
2033
|
+
event: `OPENCODE-DEPENDENCY-INSTALL-EACCES: status=root_unresolved path_source=${pathSource} scope=${scope} microvm_id=${microvmId} op=${op} path=unknown cli_euid=${euid} cli_egid=${egid} effect="${EFFECT}" chain=unavailable`,
|
|
2034
|
+
localDetail: null
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
return {
|
|
2038
|
+
event: `OPENCODE-DEPENDENCY-INSTALL-EACCES: status=outside_roots path_source=${pathSource} scope=${scope} microvm_id=${microvmId} op=${op} path=<outside-approved-roots> cli_euid=${euid} cli_egid=${egid} effect="${EFFECT}" chain=unavailable`,
|
|
2039
|
+
localDetail: `OPENCODE-DEPENDENCY-INSTALL-EACCES (local only): outside approved roots: ${inspectedPath}`
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
const inspection = inspectChain(
|
|
2043
|
+
tokenisedPath.root.real,
|
|
2044
|
+
tokenisedPath.root.token,
|
|
2045
|
+
inspectedPath,
|
|
2046
|
+
tokenisedPath.resolutionFailure
|
|
2047
|
+
);
|
|
2048
|
+
const optionalSymlinkFields = inspection.symlinkTarget ? ` symlink_target=${tokenise(inspection.symlinkTarget, roots.resolved)?.value ?? "<outside-approved-roots>"}` + (inspection.symlinkMeta ? ` symlink_meta=${inspection.symlinkMeta}` : "") : "";
|
|
2049
|
+
return {
|
|
2050
|
+
event: `OPENCODE-DEPENDENCY-INSTALL-EACCES: status=${inspection.status} path_source=${pathSource} scope=${scope} microvm_id=${microvmId} op=${op} path=${tokenisedPath.value} cli_euid=${euid} cli_egid=${egid} effect="${EFFECT}" chain=${inspection.chain}${optionalSymlinkFields}`,
|
|
2051
|
+
localDetail: null
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
function createDependencyInstallDiagnosticReporter({
|
|
2055
|
+
context,
|
|
2056
|
+
emitEvent: emitEvent2,
|
|
2057
|
+
emitLocal
|
|
2058
|
+
}) {
|
|
2059
|
+
const emittedEvents = /* @__PURE__ */ new Set();
|
|
2060
|
+
return (line) => {
|
|
2061
|
+
const diagnostic = buildDiagnostic(line, context);
|
|
2062
|
+
if (!diagnostic || emittedEvents.has(diagnostic.event)) return;
|
|
2063
|
+
if (emittedEvents.size >= MAX_DISTINCT_DIAGNOSTICS) return;
|
|
2064
|
+
emittedEvents.add(diagnostic.event);
|
|
2065
|
+
emitEvent2(diagnostic.event);
|
|
2066
|
+
if (diagnostic.localDetail) emitLocal(diagnostic.localDetail);
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
|
|
1322
2070
|
// src/lib/runner-activity-telemetry.ts
|
|
1323
|
-
import { homedir as homedir2 } from "node:os";
|
|
1324
2071
|
var FORWARDED_LEVELS_BY_SOURCE = {
|
|
1325
2072
|
"cli.run": /* @__PURE__ */ new Set(["warn", "error"]),
|
|
1326
2073
|
opencode: /* @__PURE__ */ new Set(["warn", "error"]),
|
|
@@ -1337,11 +2084,13 @@ function parseOpenCodeLogLine(line) {
|
|
|
1337
2084
|
if (!levelMatch) return null;
|
|
1338
2085
|
const level = levelMatch[1].toUpperCase();
|
|
1339
2086
|
if (level !== "WARN" && level !== "ERROR") return null;
|
|
1340
|
-
const sessionMatch = normalisedLine.match(/(?:^|\s)sessionID=(\S+)/);
|
|
2087
|
+
const sessionMatch = normalisedLine.match(/(?:^|\s)(?:sessionID|session\.id)=(\S+)/);
|
|
2088
|
+
const messageIDMatch = normalisedLine.match(/(?:^|\s)messageID=(\S+)/);
|
|
1341
2089
|
const messageMatch = normalisedLine.match(/(?:^|\s)message=(?:"((?:\\.|[^"])*)"|(\S+))/);
|
|
1342
2090
|
const parsed = {
|
|
1343
2091
|
level: level === "WARN" ? "warn" : "error",
|
|
1344
|
-
sessionID: sessionMatch?.[1]
|
|
2092
|
+
sessionID: sessionMatch?.[1],
|
|
2093
|
+
messageID: messageIDMatch?.[1]
|
|
1345
2094
|
};
|
|
1346
2095
|
const message = messageMatch?.[1] ?? messageMatch?.[2];
|
|
1347
2096
|
if (message !== void 0) parsed.message = message;
|
|
@@ -1349,18 +2098,29 @@ function parseOpenCodeLogLine(line) {
|
|
|
1349
2098
|
}
|
|
1350
2099
|
var MAX_LINE_BUFFER_BYTES = 16 * 1024;
|
|
1351
2100
|
var REPEAT_SUPPRESSION_WINDOW_MS = 6e4;
|
|
1352
|
-
function
|
|
2101
|
+
function sanitiseDependencyInstallWarning(line) {
|
|
2102
|
+
const withoutDirectory = line.replace(
|
|
2103
|
+
/((?:^|\s)dir=)(?:"(?:\\.|[^"])*"|\/[^\s)"']+|\S+)/g,
|
|
2104
|
+
"$1<path>"
|
|
2105
|
+
);
|
|
2106
|
+
return withoutDirectory.replace(
|
|
2107
|
+
/((?:^|\s)error=)"((?:\\.|[^"])*)"/,
|
|
2108
|
+
(_match, prefix, error2) => `${prefix}"${error2.replace(/'\/[^']*'/g, "'<path>'")}"`
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
function createOpenCodeActivityForwarder(getContext, onSessionStoreWriteFailure, onDependencyInstallFailure) {
|
|
1353
2112
|
let buffer = Buffer.alloc(0);
|
|
1354
2113
|
const repeatStates = /* @__PURE__ */ new Map();
|
|
1355
2114
|
const forwardLine = (line, parsed, repeatsSuppressed = 0) => {
|
|
1356
2115
|
const metadata = {
|
|
1357
2116
|
...parsed.sessionID ? { sessionID: parsed.sessionID } : {},
|
|
2117
|
+
...parsed.messageID ? { messageID: parsed.messageID } : {},
|
|
1358
2118
|
...repeatsSuppressed > 0 ? { repeats_suppressed: repeatsSuppressed } : {}
|
|
1359
2119
|
};
|
|
1360
2120
|
forwardRunnerActivity(
|
|
1361
2121
|
{
|
|
1362
2122
|
level: parsed.level,
|
|
1363
|
-
error: line,
|
|
2123
|
+
error: parsed.message === DEPENDENCY_INSTALL_FAILURE_MESSAGE ? sanitiseDependencyInstallWarning(line) : line,
|
|
1364
2124
|
metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
|
|
1365
2125
|
source: "opencode"
|
|
1366
2126
|
},
|
|
@@ -1378,11 +2138,9 @@ function createOpenCodeActivityForwarder(getContext) {
|
|
|
1378
2138
|
repeatStates.delete(key);
|
|
1379
2139
|
}
|
|
1380
2140
|
};
|
|
1381
|
-
const
|
|
1382
|
-
const parsed = parseOpenCodeLogLine(line);
|
|
1383
|
-
if (!parsed) return;
|
|
2141
|
+
const forwardWithRepeatSuppression = (line, parsed) => {
|
|
1384
2142
|
const now = Date.now();
|
|
1385
|
-
const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}`;
|
|
2143
|
+
const key = parsed.message === void 0 ? void 0 : `${parsed.level}\0${parsed.message}\0${parsed.sessionID ?? ""}\0${parsed.messageID ?? ""}`;
|
|
1386
2144
|
expireRepeatStates(now, key);
|
|
1387
2145
|
if (key === void 0) {
|
|
1388
2146
|
forwardLine(line, parsed);
|
|
@@ -1403,6 +2161,35 @@ function createOpenCodeActivityForwarder(getContext) {
|
|
|
1403
2161
|
previous.parsed = parsed;
|
|
1404
2162
|
previous.repeatsSuppressed++;
|
|
1405
2163
|
};
|
|
2164
|
+
const flushLine = (line) => {
|
|
2165
|
+
const parsed = parseOpenCodeLogLine(line);
|
|
2166
|
+
const isDiskFailure = isSessionStoreWriteFailure(line);
|
|
2167
|
+
const cause = sessionStoreCauseToken(line);
|
|
2168
|
+
if (onSessionStoreWriteFailure && (isDiskFailure || cause !== null)) {
|
|
2169
|
+
try {
|
|
2170
|
+
onSessionStoreWriteFailure({
|
|
2171
|
+
isDiskFailure,
|
|
2172
|
+
sessionId: parsed?.sessionID ?? null,
|
|
2173
|
+
cause
|
|
2174
|
+
});
|
|
2175
|
+
} catch (err) {
|
|
2176
|
+
reportTelemetryDiagnostic(
|
|
2177
|
+
`Runner activity telemetry could not record session-store write failure: ${err instanceof Error ? err.message : String(err)}`
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
if (!parsed) return;
|
|
2182
|
+
forwardWithRepeatSuppression(line, parsed);
|
|
2183
|
+
if (onDependencyInstallFailure && parsed.message === DEPENDENCY_INSTALL_FAILURE_MESSAGE) {
|
|
2184
|
+
try {
|
|
2185
|
+
onDependencyInstallFailure(line);
|
|
2186
|
+
} catch (err) {
|
|
2187
|
+
reportTelemetryDiagnostic(
|
|
2188
|
+
`Runner activity telemetry could not record dependency-install failure diagnostic: ${err instanceof Error ? err.message : String(err)}`
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
};
|
|
1406
2193
|
const forward = ((chunk) => {
|
|
1407
2194
|
buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8")]);
|
|
1408
2195
|
let newlineIndex;
|
|
@@ -1428,34 +2215,38 @@ function createOpenCodeActivityForwarder(getContext) {
|
|
|
1428
2215
|
var MAX_MESSAGE_LENGTH = 500;
|
|
1429
2216
|
var MAX_METADATA_VALUE_LENGTH = 200;
|
|
1430
2217
|
var MAX_METADATA_ENTRIES = 20;
|
|
1431
|
-
var TRUNCATION_MARKER = "\u2026";
|
|
1432
|
-
function redactHomeDirectory(message) {
|
|
1433
|
-
const home = homedir2();
|
|
1434
|
-
if (!home || home === "/") {
|
|
1435
|
-
return message;
|
|
1436
|
-
}
|
|
1437
|
-
const pathSegmentCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-";
|
|
1438
|
-
let result = "";
|
|
1439
|
-
let searchFrom = 0;
|
|
1440
|
-
let homeIndex;
|
|
1441
|
-
while ((homeIndex = message.indexOf(home, searchFrom)) !== -1) {
|
|
1442
|
-
const homeEnd = homeIndex + home.length;
|
|
1443
|
-
const previousCharacter = message[homeIndex - 1];
|
|
1444
|
-
const nextCharacter = message[homeEnd];
|
|
1445
|
-
const atPathBoundary = home === "/" || (!previousCharacter || !pathSegmentCharacters.includes(previousCharacter)) && (!nextCharacter || !pathSegmentCharacters.includes(nextCharacter));
|
|
1446
|
-
result += message.slice(searchFrom, homeIndex);
|
|
1447
|
-
result += atPathBoundary ? "~" : home;
|
|
1448
|
-
searchFrom = homeEnd;
|
|
1449
|
-
}
|
|
1450
|
-
return result + message.slice(searchFrom);
|
|
1451
|
-
}
|
|
1452
|
-
function redactDiagnosticText(message) {
|
|
1453
|
-
const redacted = message.replace(/esk_[A-Za-z0-9_-]+/g, "esk_***").replace(/ct_[A-Za-z0-9_-]+/g, "ct_***").replace(/(?<![A-Za-z0-9_-])sk-ant-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g, "sk-ant-***").replace(/(?<![A-Za-z0-9_-])sk-proj-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g, "sk-proj-***").replace(/(?<![A-Za-z0-9_-])sk-[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/g, "sk-***").replace(/(?<![A-Za-z0-9_-])(gh[oprsu])_[A-Za-z0-9]{20,}(?![A-Za-z0-9_-])/g, "$1_***").replace(/(?<![A-Za-z0-9_-])github_pat_[A-Za-z0-9_]{20,}(?![A-Za-z0-9_-])/g, "github_pat_***").replace(/https?:\/\/\S+/g, "<url>");
|
|
1454
|
-
return redactHomeDirectory(redacted);
|
|
1455
|
-
}
|
|
1456
2218
|
function truncate(message) {
|
|
1457
2219
|
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
1458
|
-
|
|
2220
|
+
let dropped = message.length - MAX_MESSAGE_LENGTH;
|
|
2221
|
+
let marker = `\u2026${dropped} chars omitted\u2026`;
|
|
2222
|
+
let previousDropped = -1;
|
|
2223
|
+
while (dropped !== previousDropped) {
|
|
2224
|
+
previousDropped = dropped;
|
|
2225
|
+
const nextDropped = message.length - MAX_MESSAGE_LENGTH + marker.length;
|
|
2226
|
+
dropped = nextDropped;
|
|
2227
|
+
marker = `\u2026${dropped} chars omitted\u2026`;
|
|
2228
|
+
}
|
|
2229
|
+
const remaining = MAX_MESSAGE_LENGTH - marker.length;
|
|
2230
|
+
const headLength = Math.ceil(remaining * 0.6);
|
|
2231
|
+
const tailLength = remaining - headLength;
|
|
2232
|
+
const effectMatch = message.startsWith("OPENCODE-DEPENDENCY-INSTALL-EACCES:") ? message.match(/ effect="[^"]*"/) : null;
|
|
2233
|
+
if (effectMatch?.index !== void 0) {
|
|
2234
|
+
const effectEnd = effectMatch.index + effectMatch[0].length;
|
|
2235
|
+
if (effectEnd > headLength && effectEnd < message.length) {
|
|
2236
|
+
const effectLength = effectMatch[0].length;
|
|
2237
|
+
const nonEffectLength = remaining - effectLength;
|
|
2238
|
+
const preservedTailLength = Math.min(
|
|
2239
|
+
message.length - effectEnd,
|
|
2240
|
+
nonEffectLength - Math.min(effectMatch.index, Math.ceil(nonEffectLength * 0.6))
|
|
2241
|
+
);
|
|
2242
|
+
const preservedHeadLength = Math.min(
|
|
2243
|
+
effectMatch.index,
|
|
2244
|
+
nonEffectLength - preservedTailLength
|
|
2245
|
+
);
|
|
2246
|
+
return message.slice(0, preservedHeadLength) + marker + message.slice(effectMatch.index, effectEnd) + message.slice(message.length - preservedTailLength);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
return message.slice(0, headLength) + marker + message.slice(message.length - tailLength);
|
|
1459
2250
|
}
|
|
1460
2251
|
function sanitiseMetadata(metadata) {
|
|
1461
2252
|
if (!metadata) return {};
|
|
@@ -1532,10 +2323,10 @@ function forwardRunnerActivity(entry, context) {
|
|
|
1532
2323
|
|
|
1533
2324
|
// src/lib/opencode/session-db-recovery-report.ts
|
|
1534
2325
|
import { readFileSync as readFileSync2, unlinkSync } from "node:fs";
|
|
1535
|
-
import { join as
|
|
2326
|
+
import { join as join3 } from "node:path";
|
|
1536
2327
|
function sessionDbRecoveryReportPath(homeDir, env) {
|
|
1537
2328
|
const override = env.EVIDENT_SESSION_DB_RECOVERY_REPORT?.trim();
|
|
1538
|
-
return override ||
|
|
2329
|
+
return override || join3(homeDir, ".local", "state", "evident", "session-db-recovery.jsonl");
|
|
1539
2330
|
}
|
|
1540
2331
|
function drainSessionDbRecoveryReport({
|
|
1541
2332
|
homeDir,
|
|
@@ -1665,12 +2456,23 @@ function buildSessionDbRecoveryActivity(record) {
|
|
|
1665
2456
|
metadata: withoutContractFields(record),
|
|
1666
2457
|
message: `This runner started without restoring session history because it could not verify where its session history is backed up. Nothing in the backup was changed; check the runner backup configuration and permissions.${replication}`
|
|
1667
2458
|
};
|
|
1668
|
-
case "session_db_boot_refused":
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
2459
|
+
case "session_db_boot_refused": {
|
|
2460
|
+
const message = (() => {
|
|
2461
|
+
switch (record.reason) {
|
|
2462
|
+
case "local_discard_failed":
|
|
2463
|
+
return `This runner did not come online because its damaged session database could not be safely separated from its active backup or proven removed. Backed-up session history remains readable at ${record.quarantine_destination ?? "its original location or the quarantine destination named in the boot logs"}; any local session-database files that remain were left in place and nothing opened or wrote them. See the runner boot logs for SESSION-DB-LOCAL-DISCARD-FAILED details.`;
|
|
2464
|
+
case "replica_separation_unproven":
|
|
2465
|
+
return `This runner did not come online because its damaged session database could not be safely separated from its active backup. The SESSION-DB-REPLICA-SEPARATION-UNVERIFIED boot log contains the partial-quarantine moved and remaining counts. Backed-up session history remains readable at ${record.quarantine_destination ?? "its original location or the quarantine destination named in the boot logs"}; any local session-database files that remain were left in place and nothing opened or wrote them.`;
|
|
2466
|
+
case "verification_timeout_before_separation":
|
|
2467
|
+
return "This runner did not come online because session database verification timed out after detecting corruption, before replica separation was attempted. No replica progress is claimed. See SESSION-DB-VERIFY-TIMEOUT in the runner boot logs before retrying.";
|
|
2468
|
+
case "verification_timeout_during_separation":
|
|
2469
|
+
return "This runner did not come online because session database verification timed out while replica separation was in progress. Separation completion and object removal were not proven. See SESSION-DB-VERIFY-TIMEOUT in the runner boot logs before retrying.";
|
|
2470
|
+
default:
|
|
2471
|
+
return "This runner did not come online because session database recovery could not be completed. Inspect the runner boot logs for the refusal reason before retrying.";
|
|
2472
|
+
}
|
|
2473
|
+
})();
|
|
2474
|
+
return { level, metadata: withoutContractFields(record), message };
|
|
2475
|
+
}
|
|
1674
2476
|
case "schema_provenance_mismatch":
|
|
1675
2477
|
return {
|
|
1676
2478
|
level,
|
|
@@ -1797,7 +2599,7 @@ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
|
|
|
1797
2599
|
if (health.healthy || health.authFailed) {
|
|
1798
2600
|
return health;
|
|
1799
2601
|
}
|
|
1800
|
-
await new Promise((
|
|
2602
|
+
await new Promise((resolve5) => setTimeout(resolve5, 1e3));
|
|
1801
2603
|
}
|
|
1802
2604
|
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
1803
2605
|
}
|
|
@@ -1808,7 +2610,7 @@ async function waitForOpenCode2Health(port, password, timeoutMs = 3e4) {
|
|
|
1808
2610
|
if (health.healthy || health.authFailed) {
|
|
1809
2611
|
return health;
|
|
1810
2612
|
}
|
|
1811
|
-
await new Promise((
|
|
2613
|
+
await new Promise((resolve5) => setTimeout(resolve5, 1e3));
|
|
1812
2614
|
}
|
|
1813
2615
|
return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
|
|
1814
2616
|
}
|
|
@@ -1920,7 +2722,7 @@ async function getOpenCodeDirectory(port, client) {
|
|
|
1920
2722
|
return null;
|
|
1921
2723
|
}
|
|
1922
2724
|
}
|
|
1923
|
-
function
|
|
2725
|
+
function roleOf2(m) {
|
|
1924
2726
|
if (!m || typeof m !== "object") return void 0;
|
|
1925
2727
|
if (typeof m.role === "string") return m.role;
|
|
1926
2728
|
const infoRole = m.info?.role;
|
|
@@ -1934,13 +2736,13 @@ function createdOf(m) {
|
|
|
1934
2736
|
if (!m || typeof m !== "object") return void 0;
|
|
1935
2737
|
return m.info?.time?.created ?? m.time?.created;
|
|
1936
2738
|
}
|
|
1937
|
-
function
|
|
2739
|
+
function idOf2(m) {
|
|
1938
2740
|
if (!m || typeof m !== "object") return void 0;
|
|
1939
2741
|
if (typeof m.id === "string") return m.id;
|
|
1940
2742
|
const infoId = m.info?.id;
|
|
1941
2743
|
return typeof infoId === "string" ? infoId : void 0;
|
|
1942
2744
|
}
|
|
1943
|
-
function
|
|
2745
|
+
function parentIdOf2(m) {
|
|
1944
2746
|
if (!m || typeof m !== "object") return void 0;
|
|
1945
2747
|
if (typeof m.parentID === "string") return m.parentID;
|
|
1946
2748
|
const infoParent = m.info?.parentID;
|
|
@@ -1956,8 +2758,12 @@ function errorOf(m) {
|
|
|
1956
2758
|
if (!m || typeof m !== "object") return void 0;
|
|
1957
2759
|
return m.info?.error ?? m.error;
|
|
1958
2760
|
}
|
|
2761
|
+
function isTerminalStopAnswer(m) {
|
|
2762
|
+
return completedOf(m) != null && errorOf(m) == null && finishOf(m) === "stop";
|
|
2763
|
+
}
|
|
1959
2764
|
function isAssistantInFlight(m) {
|
|
1960
2765
|
if (completedOf(m) == null) return true;
|
|
2766
|
+
if (errorOf(m) != null) return false;
|
|
1961
2767
|
return finishOf(m) === "tool-calls";
|
|
1962
2768
|
}
|
|
1963
2769
|
async function getSessionMessages(port, sessionId, client) {
|
|
@@ -2005,9 +2811,10 @@ async function pollSessionMessagesForRedrive(port, sessionId, client) {
|
|
|
2005
2811
|
function isSessionActivelyGenerating(messages) {
|
|
2006
2812
|
if (!messages || messages.length === 0) return false;
|
|
2007
2813
|
const last = messages[messages.length - 1];
|
|
2008
|
-
if (
|
|
2814
|
+
if (roleOf2(last) !== "assistant") return false;
|
|
2009
2815
|
return completedOf(last) == null;
|
|
2010
2816
|
}
|
|
2817
|
+
var FULL_SESSION_ENUMERATION_MAX_SESSIONS = 3200;
|
|
2011
2818
|
function sessionLastActivityMs(session) {
|
|
2012
2819
|
const candidates = [
|
|
2013
2820
|
session.time?.updated,
|
|
@@ -2022,30 +2829,71 @@ function sessionLastActivityMs(session) {
|
|
|
2022
2829
|
}
|
|
2023
2830
|
return null;
|
|
2024
2831
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
2832
|
+
function sessionListPath(options) {
|
|
2833
|
+
const params = new URLSearchParams();
|
|
2834
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
2835
|
+
const query = params.toString();
|
|
2836
|
+
return query ? `/session?${query}` : "/session";
|
|
2837
|
+
}
|
|
2838
|
+
async function listSessionPage(port, client, options) {
|
|
2839
|
+
const path = sessionListPath(options);
|
|
2027
2840
|
try {
|
|
2028
|
-
const res = await requestWithClient(port, client,
|
|
2841
|
+
const res = await requestWithClient(port, client, path);
|
|
2029
2842
|
if (!res.ok) return null;
|
|
2030
2843
|
const body = await res.json();
|
|
2031
2844
|
return Array.isArray(body) ? body : null;
|
|
2032
2845
|
} catch (error2) {
|
|
2033
2846
|
console.error(
|
|
2034
|
-
`[
|
|
2847
|
+
`[listSessionPage] GET ${path} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2035
2848
|
);
|
|
2036
2849
|
return null;
|
|
2037
2850
|
}
|
|
2038
2851
|
}
|
|
2852
|
+
async function listSessions(port, client, options) {
|
|
2853
|
+
if (client?.version === "v2") return listV2Sessions(client);
|
|
2854
|
+
return listSessionPage(port, client, options);
|
|
2855
|
+
}
|
|
2856
|
+
async function listAllSessions(port, client) {
|
|
2857
|
+
if (client?.version === "v2") {
|
|
2858
|
+
const sessions2 = await listV2Sessions(client);
|
|
2859
|
+
return sessions2 === null ? { sessions: null, reason: "opencode-unreachable" } : { sessions: sessions2 };
|
|
2860
|
+
}
|
|
2861
|
+
const sessions = await listSessionPage(port, client, {
|
|
2862
|
+
limit: FULL_SESSION_ENUMERATION_MAX_SESSIONS
|
|
2863
|
+
});
|
|
2864
|
+
if (sessions === null) return { sessions: null, reason: "opencode-unreachable" };
|
|
2865
|
+
if (sessions.length >= FULL_SESSION_ENUMERATION_MAX_SESSIONS) {
|
|
2866
|
+
return { sessions: null, reason: "session-enumeration-limit-bound" };
|
|
2867
|
+
}
|
|
2868
|
+
return { sessions };
|
|
2869
|
+
}
|
|
2039
2870
|
async function deleteSession(port, id, client) {
|
|
2040
2871
|
if (client?.version === "v2") return deleteV2Session(client, id);
|
|
2041
2872
|
try {
|
|
2042
|
-
const res = await requestWithClient(
|
|
2043
|
-
|
|
2873
|
+
const res = await requestWithClient(
|
|
2874
|
+
port,
|
|
2875
|
+
client,
|
|
2876
|
+
`/session/${id}`,
|
|
2877
|
+
{ method: "DELETE" },
|
|
2878
|
+
{ allowStatuses: ALL_HTTP_STATUSES }
|
|
2879
|
+
);
|
|
2880
|
+
if (res.status >= 200 && res.status < 300) {
|
|
2881
|
+
return { ok: true, status: res.status };
|
|
2882
|
+
}
|
|
2883
|
+
try {
|
|
2884
|
+
const body = await res.text();
|
|
2885
|
+
return body ? { ok: false, status: res.status, body } : { ok: false, status: res.status };
|
|
2886
|
+
} catch (error2) {
|
|
2887
|
+
console.error(
|
|
2888
|
+
`[deleteSession] DELETE /session/${id} response body read failed for HTTP ${res.status}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2889
|
+
);
|
|
2890
|
+
return { ok: false, status: res.status };
|
|
2891
|
+
}
|
|
2044
2892
|
} catch (error2) {
|
|
2045
2893
|
console.error(
|
|
2046
2894
|
`[deleteSession] DELETE /session/${id} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2047
2895
|
);
|
|
2048
|
-
return false;
|
|
2896
|
+
return { ok: false, status: "threw" };
|
|
2049
2897
|
}
|
|
2050
2898
|
}
|
|
2051
2899
|
async function sessionExists(port, id, client) {
|
|
@@ -2283,11 +3131,31 @@ function messageText(m) {
|
|
|
2283
3131
|
if (!m || !Array.isArray(m.parts)) return "";
|
|
2284
3132
|
return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
2285
3133
|
}
|
|
2286
|
-
|
|
3134
|
+
function findDispatchedUserMessageId(messages, content, excludeIds) {
|
|
3135
|
+
if (!messages || messages.length === 0) return null;
|
|
3136
|
+
let best = null;
|
|
3137
|
+
for (const message of messages) {
|
|
3138
|
+
if (roleOf2(message) !== "user") continue;
|
|
3139
|
+
const id = idOf2(message);
|
|
3140
|
+
if (typeof id !== "string" || excludeIds?.has(id)) continue;
|
|
3141
|
+
if (messageText(message) !== content) continue;
|
|
3142
|
+
const created = createdOf(message) ?? 0;
|
|
3143
|
+
if (best === null || created > best.created) best = { id, created };
|
|
3144
|
+
}
|
|
3145
|
+
return best?.id ?? null;
|
|
3146
|
+
}
|
|
3147
|
+
async function sendPromptAsync(port, sessionId, content, options, attachments, client, callbacks) {
|
|
2287
3148
|
const before = await getSessionMessages(port, sessionId, client);
|
|
2288
3149
|
const knownUserIds = new Set(
|
|
2289
|
-
(before ?? []).filter((m) =>
|
|
3150
|
+
(before ?? []).filter((m) => roleOf2(m) === "user").map((m) => idOf2(m)).filter((id) => typeof id === "string")
|
|
2290
3151
|
);
|
|
3152
|
+
if (callbacks) {
|
|
3153
|
+
try {
|
|
3154
|
+
callbacks.onKnownUserIds(before === null ? null : knownUserIds);
|
|
3155
|
+
} catch (error2) {
|
|
3156
|
+
callbacks.onKnownUserIdsError(error2);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
2291
3159
|
const parts = [{ type: "text", text: content }];
|
|
2292
3160
|
let pendingOutcomes = null;
|
|
2293
3161
|
if (attachments && attachments.inputs.length > 0) {
|
|
@@ -2326,25 +3194,13 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
|
|
|
2326
3194
|
const READ_BACK_DELAY_MS = 150;
|
|
2327
3195
|
for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
|
|
2328
3196
|
const after = await getSessionMessages(port, sessionId, client);
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
const id = idOf(m);
|
|
2334
|
-
if (typeof id !== "string" || knownUserIds.has(id)) continue;
|
|
2335
|
-
if (messageText(m) !== content) continue;
|
|
2336
|
-
const created = createdOf(m) ?? 0;
|
|
2337
|
-
if (best === null || created > best.created) {
|
|
2338
|
-
best = { id, created };
|
|
2339
|
-
}
|
|
2340
|
-
}
|
|
2341
|
-
if (best) {
|
|
2342
|
-
if (pendingOutcomes && attachments?.onOutcomes) attachments.onOutcomes(pendingOutcomes);
|
|
2343
|
-
return best.id;
|
|
2344
|
-
}
|
|
3197
|
+
const dispatchedUserMessageId = findDispatchedUserMessageId(after, content, knownUserIds);
|
|
3198
|
+
if (dispatchedUserMessageId !== null) {
|
|
3199
|
+
if (pendingOutcomes && attachments?.onOutcomes) attachments.onOutcomes(pendingOutcomes);
|
|
3200
|
+
return dispatchedUserMessageId;
|
|
2345
3201
|
}
|
|
2346
3202
|
if (attempt < READ_BACK_ATTEMPTS - 1) {
|
|
2347
|
-
await new Promise((
|
|
3203
|
+
await new Promise((resolve5) => setTimeout(resolve5, READ_BACK_DELAY_MS));
|
|
2348
3204
|
}
|
|
2349
3205
|
}
|
|
2350
3206
|
return null;
|
|
@@ -2352,57 +3208,68 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
|
|
|
2352
3208
|
function findAssistantReplyAfter(messages, userMessageId) {
|
|
2353
3209
|
if (!messages || messages.length === 0) return null;
|
|
2354
3210
|
const byParent = messages.find(
|
|
2355
|
-
(m) =>
|
|
3211
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
|
|
2356
3212
|
);
|
|
2357
3213
|
if (byParent) return byParent;
|
|
2358
|
-
const userIndex = messages.findIndex((m) =>
|
|
3214
|
+
const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
|
|
2359
3215
|
if (userIndex === -1) return null;
|
|
2360
3216
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2361
|
-
if (
|
|
3217
|
+
if (roleOf2(messages[i]) === "assistant") return messages[i];
|
|
2362
3218
|
}
|
|
2363
3219
|
return null;
|
|
2364
3220
|
}
|
|
2365
3221
|
function findLastAssistantReplyFor(messages, userMessageId) {
|
|
2366
3222
|
if (!messages || messages.length === 0) return null;
|
|
2367
3223
|
let lastCorrelated = null;
|
|
2368
|
-
let lastNonErrored = null;
|
|
2369
3224
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2370
3225
|
const m = messages[i];
|
|
2371
|
-
if (
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
3226
|
+
if (roleOf2(m) !== "assistant" || parentIdOf2(m) !== userMessageId) continue;
|
|
3227
|
+
lastCorrelated = m;
|
|
3228
|
+
break;
|
|
3229
|
+
}
|
|
3230
|
+
if (lastCorrelated) {
|
|
3231
|
+
if (errorOf(lastCorrelated) == null) return lastCorrelated;
|
|
3232
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3233
|
+
const m = messages[i];
|
|
3234
|
+
if (roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId && isTerminalStopAnswer(m)) {
|
|
3235
|
+
return m;
|
|
3236
|
+
}
|
|
2376
3237
|
}
|
|
3238
|
+
return lastCorrelated;
|
|
2377
3239
|
}
|
|
2378
|
-
|
|
2379
|
-
const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
|
|
3240
|
+
const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
|
|
2380
3241
|
if (userIndex === -1) return null;
|
|
2381
3242
|
let last = null;
|
|
2382
|
-
let
|
|
3243
|
+
let lastAssistantIndex = -1;
|
|
2383
3244
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2384
|
-
const role =
|
|
3245
|
+
const role = roleOf2(messages[i]);
|
|
2385
3246
|
if (role === "user") break;
|
|
2386
3247
|
if (role === "assistant") {
|
|
2387
3248
|
last = messages[i];
|
|
2388
|
-
|
|
3249
|
+
lastAssistantIndex = i;
|
|
2389
3250
|
}
|
|
2390
3251
|
}
|
|
2391
|
-
|
|
3252
|
+
if (last === null || errorOf(last) == null) return last;
|
|
3253
|
+
for (let i = lastAssistantIndex; i > userIndex; i--) {
|
|
3254
|
+
const m = messages[i];
|
|
3255
|
+
if (roleOf2(m) === "user") break;
|
|
3256
|
+
if (roleOf2(m) === "assistant" && isTerminalStopAnswer(m)) return m;
|
|
3257
|
+
}
|
|
3258
|
+
return last;
|
|
2392
3259
|
}
|
|
2393
3260
|
function collectSubagentSessions(messages, userMessageId) {
|
|
2394
3261
|
if (!messages || messages.length === 0) return [];
|
|
2395
3262
|
const byParent = messages.filter(
|
|
2396
|
-
(message) =>
|
|
3263
|
+
(message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
|
|
2397
3264
|
);
|
|
2398
3265
|
const assistants = byParent.length > 0 ? byParent : [];
|
|
2399
3266
|
if (assistants.length === 0) {
|
|
2400
|
-
const userIndex = messages.findIndex((message) =>
|
|
3267
|
+
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
2401
3268
|
if (userIndex === -1) return [];
|
|
2402
3269
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2403
3270
|
const message = messages[i];
|
|
2404
|
-
if (
|
|
2405
|
-
if (
|
|
3271
|
+
if (roleOf2(message) === "user") break;
|
|
3272
|
+
if (roleOf2(message) === "assistant") assistants.push(message);
|
|
2406
3273
|
}
|
|
2407
3274
|
}
|
|
2408
3275
|
const refs = [];
|
|
@@ -2442,7 +3309,7 @@ function collectTaskCalls(messages, userMessageId) {
|
|
|
2442
3309
|
if (!messages || messages.length === 0) return [];
|
|
2443
3310
|
const calls = [];
|
|
2444
3311
|
for (const message of messages) {
|
|
2445
|
-
if (
|
|
3312
|
+
if (roleOf2(message) !== "assistant" || parentIdOf2(message) !== userMessageId) continue;
|
|
2446
3313
|
for (const part of message.parts ?? []) {
|
|
2447
3314
|
if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
|
|
2448
3315
|
continue;
|
|
@@ -2472,7 +3339,7 @@ function attributeTaskCallUsage(messages, windows) {
|
|
|
2472
3339
|
for (const window of eligibleWindows) assignments.set(window.callID, []);
|
|
2473
3340
|
const unattributed = [];
|
|
2474
3341
|
for (const message of messages ?? []) {
|
|
2475
|
-
if (
|
|
3342
|
+
if (roleOf2(message) !== "assistant") continue;
|
|
2476
3343
|
const created = finiteNumber(createdOf(message));
|
|
2477
3344
|
const matching = created === null ? [] : eligibleWindows.filter(
|
|
2478
3345
|
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
@@ -2494,8 +3361,9 @@ function attributeTaskCallUsage(messages, windows) {
|
|
|
2494
3361
|
}
|
|
2495
3362
|
function sumAssistantUsage(messages) {
|
|
2496
3363
|
if (!messages || messages.length === 0) return null;
|
|
2497
|
-
const
|
|
2498
|
-
const
|
|
3364
|
+
const hasTrailingError = errorOf(messages[messages.length - 1]) != null;
|
|
3365
|
+
const hasTerminalStopAnswer = messages.some(isTerminalStopAnswer);
|
|
3366
|
+
const selected = hasTrailingError && hasTerminalStopAnswer ? messages.filter((message) => errorOf(message) == null) : messages;
|
|
2499
3367
|
let sawAnyUsage = false;
|
|
2500
3368
|
let inputSum = 0;
|
|
2501
3369
|
let outputSum = 0;
|
|
@@ -2504,9 +3372,11 @@ function sumAssistantUsage(messages) {
|
|
|
2504
3372
|
let cacheWriteSum = 0;
|
|
2505
3373
|
let costSum = 0;
|
|
2506
3374
|
let sawCost = false;
|
|
3375
|
+
let toolCallCount = 0;
|
|
2507
3376
|
let modelId = null;
|
|
2508
3377
|
let providerId = null;
|
|
2509
3378
|
for (const m of selected) {
|
|
3379
|
+
toolCallCount += m.parts?.filter((part) => part.type === "tool").length ?? 0;
|
|
2510
3380
|
const info = m.info;
|
|
2511
3381
|
if (!info) continue;
|
|
2512
3382
|
const tokens = info.tokens;
|
|
@@ -2543,19 +3413,18 @@ function sumAssistantUsage(messages) {
|
|
|
2543
3413
|
usage_tokens_cache_write: cacheWriteSum,
|
|
2544
3414
|
// NULL means OpenCode never reported a cost; it is distinct from a genuine
|
|
2545
3415
|
// zero-cost message, which sets `sawCost` with `costSum === 0`.
|
|
2546
|
-
usage_cost_usd: sawCost ? costSum : null
|
|
3416
|
+
usage_cost_usd: sawCost ? costSum : null,
|
|
3417
|
+
turn_tool_calls: toolCallCount
|
|
2547
3418
|
};
|
|
2548
3419
|
}
|
|
2549
3420
|
function messageUsage(messages, userMessageId) {
|
|
2550
3421
|
if (!messages || messages.length === 0) return null;
|
|
2551
3422
|
const byParentAll = messages.filter(
|
|
2552
|
-
(m) =>
|
|
3423
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
|
|
2553
3424
|
);
|
|
2554
|
-
const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
|
|
2555
|
-
const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
|
|
2556
3425
|
let correlated;
|
|
2557
|
-
if (
|
|
2558
|
-
correlated =
|
|
3426
|
+
if (byParentAll.length > 0) {
|
|
3427
|
+
correlated = byParentAll;
|
|
2559
3428
|
} else {
|
|
2560
3429
|
const reply = findAssistantReplyAfter(messages, userMessageId);
|
|
2561
3430
|
correlated = reply ? [reply] : [];
|
|
@@ -2565,7 +3434,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
2565
3434
|
}
|
|
2566
3435
|
function messageRunState(messages, userMessageId) {
|
|
2567
3436
|
if (!messages || messages.length === 0) return "unknown";
|
|
2568
|
-
const hasUser = messages.some((m) =>
|
|
3437
|
+
const hasUser = messages.some((m) => idOf2(m) === userMessageId);
|
|
2569
3438
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
2570
3439
|
if (!hasUser) {
|
|
2571
3440
|
if (!reply) return "unknown";
|
|
@@ -2601,14 +3470,38 @@ function messageError(messages, userMessageId) {
|
|
|
2601
3470
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
2602
3471
|
const error2 = errorOf(reply);
|
|
2603
3472
|
if (error2 == null) return null;
|
|
2604
|
-
|
|
2605
|
-
if (typeof error2 === "
|
|
3473
|
+
let rendered;
|
|
3474
|
+
if (typeof error2 === "string") {
|
|
3475
|
+
rendered = error2;
|
|
3476
|
+
} else if (typeof error2 === "object") {
|
|
2606
3477
|
const e = error2;
|
|
2607
3478
|
const dataMessage = e.data?.message;
|
|
2608
|
-
if (typeof dataMessage === "string")
|
|
2609
|
-
|
|
3479
|
+
if (typeof dataMessage === "string") {
|
|
3480
|
+
rendered = dataMessage;
|
|
3481
|
+
} else if (typeof e.message === "string") {
|
|
3482
|
+
rendered = e.message;
|
|
3483
|
+
} else {
|
|
3484
|
+
rendered = "The agent run failed.";
|
|
3485
|
+
}
|
|
3486
|
+
} else {
|
|
3487
|
+
rendered = "The agent run failed.";
|
|
2610
3488
|
}
|
|
2611
|
-
|
|
3489
|
+
const toolPart = Array.isArray(reply?.parts) ? reply.parts.find(
|
|
3490
|
+
(part) => part && typeof part === "object" && part.type === "tool" && part.state?.status === "error"
|
|
3491
|
+
) : void 0;
|
|
3492
|
+
return typeof toolPart?.tool === "string" && toolPart.tool.length > 0 ? `${rendered} (tool "${toolPart.tool}")` : rendered;
|
|
3493
|
+
}
|
|
3494
|
+
function describeAbandonedReplyShape(messages, userMessageId) {
|
|
3495
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
3496
|
+
if (!reply) return null;
|
|
3497
|
+
const parts = Array.isArray(reply.parts) ? reply.parts : [];
|
|
3498
|
+
const lastTool = [...parts].reverse().find((part) => part?.type === "tool");
|
|
3499
|
+
return {
|
|
3500
|
+
partTypes: parts.slice(-12).map((part) => typeof part?.type === "string" ? part.type : "(unknown)"),
|
|
3501
|
+
droppedPartCount: Math.max(0, parts.length - 12),
|
|
3502
|
+
lastToolName: typeof lastTool?.tool === "string" && lastTool.tool.length > 0 ? lastTool.tool : null,
|
|
3503
|
+
lastToolStatus: typeof lastTool?.state?.status === "string" && lastTool.state.status.length > 0 ? lastTool.state.status : null
|
|
3504
|
+
};
|
|
2612
3505
|
}
|
|
2613
3506
|
function isAbortedTerminalReply(messages, userMessageId) {
|
|
2614
3507
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
@@ -2657,7 +3550,7 @@ function findLatestSubagentAuthOutcome(messages, sinceMs) {
|
|
|
2657
3550
|
if (!messages || messages.length === 0) return null;
|
|
2658
3551
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2659
3552
|
const message = messages[i];
|
|
2660
|
-
if (
|
|
3553
|
+
if (roleOf2(message) !== "assistant") continue;
|
|
2661
3554
|
const created = createdOf(message);
|
|
2662
3555
|
if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
|
|
2663
3556
|
const failure = classifyReplyAuthError(message);
|
|
@@ -2689,25 +3582,25 @@ function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProvi
|
|
|
2689
3582
|
function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
2690
3583
|
if (!messages || messages.length === 0) return false;
|
|
2691
3584
|
return messages.some(
|
|
2692
|
-
(m) =>
|
|
3585
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
2693
3586
|
);
|
|
2694
3587
|
}
|
|
2695
3588
|
function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
|
|
2696
3589
|
if (!messages || messages.length === 0) return false;
|
|
2697
|
-
const userIndex = messages.findIndex((message) =>
|
|
3590
|
+
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
2698
3591
|
if (userIndex === -1) return false;
|
|
2699
3592
|
let hasLaterUser = false;
|
|
2700
3593
|
let hasStartedLaterUser = false;
|
|
2701
3594
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2702
3595
|
const message = messages[i];
|
|
2703
|
-
if (
|
|
3596
|
+
if (roleOf2(message) !== "user") continue;
|
|
2704
3597
|
hasLaterUser = true;
|
|
2705
|
-
const laterUserMessageId =
|
|
3598
|
+
const laterUserMessageId = idOf2(message);
|
|
2706
3599
|
if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
|
|
2707
3600
|
return false;
|
|
2708
3601
|
}
|
|
2709
3602
|
if (messages.some(
|
|
2710
|
-
(candidate) =>
|
|
3603
|
+
(candidate) => roleOf2(candidate) === "assistant" && parentIdOf2(candidate) === laterUserMessageId
|
|
2711
3604
|
)) {
|
|
2712
3605
|
hasStartedLaterUser = true;
|
|
2713
3606
|
}
|
|
@@ -3178,13 +4071,28 @@ async function listV2Sessions(client) {
|
|
|
3178
4071
|
}
|
|
3179
4072
|
async function deleteV2Session(client, sessionId) {
|
|
3180
4073
|
try {
|
|
3181
|
-
await client.request(
|
|
3182
|
-
|
|
4074
|
+
const response = await client.request(
|
|
4075
|
+
`/api/session/${encodeURIComponent(sessionId)}`,
|
|
4076
|
+
{ method: "DELETE" },
|
|
4077
|
+
{ allowStatuses: Array.from({ length: 500 }, (_, index) => index + 100) }
|
|
4078
|
+
);
|
|
4079
|
+
if (response.status >= 200 && response.status < 300) {
|
|
4080
|
+
return { ok: true, status: response.status };
|
|
4081
|
+
}
|
|
4082
|
+
try {
|
|
4083
|
+
const body = await response.text();
|
|
4084
|
+
return body ? { ok: false, status: response.status, body } : { ok: false, status: response.status };
|
|
4085
|
+
} catch (error2) {
|
|
4086
|
+
console.error(
|
|
4087
|
+
`[deleteV2Session] DELETE /api/session/${sessionId} response body read failed for HTTP ${response.status}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
4088
|
+
);
|
|
4089
|
+
return { ok: false, status: response.status };
|
|
4090
|
+
}
|
|
3183
4091
|
} catch (error2) {
|
|
3184
4092
|
console.error(
|
|
3185
4093
|
`[deleteV2Session] DELETE /api/session/${sessionId} failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
3186
4094
|
);
|
|
3187
|
-
return false;
|
|
4095
|
+
return { ok: false, status: "threw" };
|
|
3188
4096
|
}
|
|
3189
4097
|
}
|
|
3190
4098
|
async function v2SessionExists(client, sessionId) {
|
|
@@ -3355,12 +4263,12 @@ async function readV2SessionErrorStream(client, options) {
|
|
|
3355
4263
|
|
|
3356
4264
|
// src/lib/opencode/credential-import-v2.ts
|
|
3357
4265
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
3358
|
-
import { join as
|
|
4266
|
+
import { join as join5 } from "node:path";
|
|
3359
4267
|
|
|
3360
4268
|
// src/lib/openai-usage.ts
|
|
3361
4269
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
3362
4270
|
import { homedir as homedir3 } from "node:os";
|
|
3363
|
-
import { join as
|
|
4271
|
+
import { join as join4 } from "node:path";
|
|
3364
4272
|
var CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
3365
4273
|
var OPENCODE_AUTH_SEGMENTS = [".local", "share", "opencode", "auth.json"];
|
|
3366
4274
|
var OpenAiUsageError = class extends Error {
|
|
@@ -3374,7 +4282,7 @@ function isLocalCredentialProblem2(err) {
|
|
|
3374
4282
|
}
|
|
3375
4283
|
function readOpenCodeChatGptCredentials() {
|
|
3376
4284
|
try {
|
|
3377
|
-
const raw = readFileSync3(
|
|
4285
|
+
const raw = readFileSync3(join4(homedir3(), ...OPENCODE_AUTH_SEGMENTS), "utf-8");
|
|
3378
4286
|
let parsed;
|
|
3379
4287
|
try {
|
|
3380
4288
|
parsed = JSON.parse(raw);
|
|
@@ -3600,13 +4508,13 @@ var INTEGRATION_LIST_RETRY_DELAY_MS = 1e3;
|
|
|
3600
4508
|
function isRecord3(value) {
|
|
3601
4509
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3602
4510
|
}
|
|
3603
|
-
var sleep2 = (milliseconds) => new Promise((
|
|
4511
|
+
var sleep2 = (milliseconds) => new Promise((resolve5) => setTimeout(resolve5, milliseconds));
|
|
3604
4512
|
function emptyResult(unsupported = [], outcome = "completed") {
|
|
3605
4513
|
return { outcome, imported: [], alreadyConnected: [], unsupported, failed: [] };
|
|
3606
4514
|
}
|
|
3607
4515
|
function readOpenCodeCredentials(homeDir, log3) {
|
|
3608
4516
|
try {
|
|
3609
|
-
const raw = readFileSync4(
|
|
4517
|
+
const raw = readFileSync4(join5(homeDir, ...OPENCODE_AUTH_SEGMENTS), "utf8");
|
|
3610
4518
|
let parsed;
|
|
3611
4519
|
try {
|
|
3612
4520
|
parsed = JSON.parse(raw);
|
|
@@ -3806,9 +4714,9 @@ async function importOpenCodeV2Credentials({
|
|
|
3806
4714
|
|
|
3807
4715
|
// src/lib/opencode/session-db-boot.ts
|
|
3808
4716
|
import { spawn as spawn2 } from "node:child_process";
|
|
3809
|
-
import { mkdirSync, statSync as
|
|
4717
|
+
import { mkdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
|
|
3810
4718
|
import { homedir as homedir4 } from "node:os";
|
|
3811
|
-
import { dirname as
|
|
4719
|
+
import { dirname as dirname4, resolve as resolvePath } from "node:path";
|
|
3812
4720
|
|
|
3813
4721
|
// src/lib/runner-synchroniser.ts
|
|
3814
4722
|
import { spawn } from "node:child_process";
|
|
@@ -3818,7 +4726,7 @@ function appendError(stderr, error2) {
|
|
|
3818
4726
|
${message}`;
|
|
3819
4727
|
}
|
|
3820
4728
|
function runSynchroniser(args, opts) {
|
|
3821
|
-
return new Promise((
|
|
4729
|
+
return new Promise((resolve5) => {
|
|
3822
4730
|
let child;
|
|
3823
4731
|
let stdout = "";
|
|
3824
4732
|
let stderr = "";
|
|
@@ -3832,7 +4740,7 @@ function runSynchroniser(args, opts) {
|
|
|
3832
4740
|
if (timer.handle) clearTimeout(timer.handle);
|
|
3833
4741
|
if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
|
|
3834
4742
|
if (spawnListener) child.removeListener("spawn", spawnListener);
|
|
3835
|
-
|
|
4743
|
+
resolve5(result);
|
|
3836
4744
|
};
|
|
3837
4745
|
try {
|
|
3838
4746
|
child = spawn("runner-synchroniser", args, {
|
|
@@ -3888,9 +4796,12 @@ function runSynchroniser(args, opts) {
|
|
|
3888
4796
|
|
|
3889
4797
|
// src/lib/opencode/session-db-boot.ts
|
|
3890
4798
|
var SESSION_DB_RESTORE_TIMEOUT_MS = 3e5;
|
|
3891
|
-
var SESSION_DB_VERIFY_TIMEOUT_MS =
|
|
4799
|
+
var SESSION_DB_VERIFY_TIMEOUT_MS = 36e4;
|
|
3892
4800
|
var SESSION_DB_SYNCHRONISER_TIMEOUT_MS = 12e4;
|
|
3893
|
-
var SESSION_DB_VERIFY_WALKBACK_BUDGET_SECONDS =
|
|
4801
|
+
var SESSION_DB_VERIFY_WALKBACK_BUDGET_SECONDS = 40;
|
|
4802
|
+
var SESSION_DB_VERIFY_RESTORE_POINTS_TIMEOUT_SECONDS = 30;
|
|
4803
|
+
var SESSION_DB_VERIFY_RESTORE_CANDIDATE_TIMEOUT_SECONDS = 120;
|
|
4804
|
+
var SESSION_DB_VERIFY_DISPOSAL_BUDGET_SECONDS = 90;
|
|
3894
4805
|
function commandError(result) {
|
|
3895
4806
|
return result.stderr.trim() || `process exited with code ${result.code ?? "null"}`;
|
|
3896
4807
|
}
|
|
@@ -3933,7 +4844,7 @@ function clearMarker(options) {
|
|
|
3933
4844
|
function markNoReplicate(options, message) {
|
|
3934
4845
|
if (options.noReplicateMarker) {
|
|
3935
4846
|
try {
|
|
3936
|
-
mkdirSync(
|
|
4847
|
+
mkdirSync(dirname4(options.noReplicateMarker), { recursive: true });
|
|
3937
4848
|
writeFileSync(options.noReplicateMarker, "");
|
|
3938
4849
|
} catch (error2) {
|
|
3939
4850
|
options.log(
|
|
@@ -3992,7 +4903,7 @@ function parseSynchroniserEnv(stdout) {
|
|
|
3992
4903
|
return values;
|
|
3993
4904
|
}
|
|
3994
4905
|
function runCommand(command, args, options) {
|
|
3995
|
-
return new Promise((
|
|
4906
|
+
return new Promise((resolve5) => {
|
|
3996
4907
|
let child;
|
|
3997
4908
|
let stdout = "";
|
|
3998
4909
|
let stderr = "";
|
|
@@ -4001,7 +4912,7 @@ function runCommand(command, args, options) {
|
|
|
4001
4912
|
if (settled) return;
|
|
4002
4913
|
settled = true;
|
|
4003
4914
|
if (timer) clearTimeout(timer);
|
|
4004
|
-
|
|
4915
|
+
resolve5(result);
|
|
4005
4916
|
};
|
|
4006
4917
|
try {
|
|
4007
4918
|
child = spawn2(command, args, {
|
|
@@ -4009,7 +4920,7 @@ function runCommand(command, args, options) {
|
|
|
4009
4920
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4010
4921
|
});
|
|
4011
4922
|
} catch (error2) {
|
|
4012
|
-
|
|
4923
|
+
resolve5({
|
|
4013
4924
|
code: null,
|
|
4014
4925
|
stdout,
|
|
4015
4926
|
stderr: error2 instanceof Error ? error2.message : String(error2),
|
|
@@ -4058,7 +4969,7 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
4058
4969
|
return null;
|
|
4059
4970
|
}
|
|
4060
4971
|
try {
|
|
4061
|
-
if (
|
|
4972
|
+
if (statSync3(configPath).size > 0) return configPath;
|
|
4062
4973
|
} catch (error2) {
|
|
4063
4974
|
if (!(error2 instanceof Error && "code" in error2 && error2.code === "ENOENT")) {
|
|
4064
4975
|
options.log(
|
|
@@ -4088,7 +4999,7 @@ async function ensureLitestreamConfig(options, env) {
|
|
|
4088
4999
|
return null;
|
|
4089
5000
|
}
|
|
4090
5001
|
try {
|
|
4091
|
-
mkdirSync(
|
|
5002
|
+
mkdirSync(dirname4(configPath), { recursive: true });
|
|
4092
5003
|
writeFileSync(configPath, rendered.stdout);
|
|
4093
5004
|
} catch (error2) {
|
|
4094
5005
|
options.log(
|
|
@@ -4201,20 +5112,37 @@ async function verifySessionDb(options, configPath, env) {
|
|
|
4201
5112
|
timeoutMs: SESSION_DB_VERIFY_TIMEOUT_MS,
|
|
4202
5113
|
env: {
|
|
4203
5114
|
...env,
|
|
4204
|
-
// The synchroniser reads
|
|
4205
|
-
// 120_000, so the walkback gives up before the outer process bound.
|
|
5115
|
+
// The synchroniser reads all four values in SECONDS.
|
|
4206
5116
|
EVIDENT_SESSION_DB_WALKBACK_BUDGET_SECONDS: String(
|
|
4207
5117
|
SESSION_DB_VERIFY_WALKBACK_BUDGET_SECONDS
|
|
5118
|
+
),
|
|
5119
|
+
EVIDENT_SESSION_DB_RESTORE_POINTS_TIMEOUT_SECONDS: String(
|
|
5120
|
+
SESSION_DB_VERIFY_RESTORE_POINTS_TIMEOUT_SECONDS
|
|
5121
|
+
),
|
|
5122
|
+
EVIDENT_SESSION_DB_RESTORE_CANDIDATE_TIMEOUT_SECONDS: String(
|
|
5123
|
+
SESSION_DB_VERIFY_RESTORE_CANDIDATE_TIMEOUT_SECONDS
|
|
5124
|
+
),
|
|
5125
|
+
EVIDENT_SESSION_DB_DISPOSAL_BUDGET_SECONDS: String(
|
|
5126
|
+
SESSION_DB_VERIFY_DISPOSAL_BUDGET_SECONDS
|
|
4208
5127
|
)
|
|
4209
5128
|
}
|
|
4210
5129
|
});
|
|
4211
5130
|
logSynchroniserDiagnostics(result, options);
|
|
4212
5131
|
if (result.timedOut || result.code === 124 || result.code === 137) {
|
|
5132
|
+
if (result.stderr.includes("SESSION-DB-INTEGRITY-FAILED")) {
|
|
5133
|
+
const reason = result.stderr.includes("SESSION-DB-REPLICA-SIZE") ? "verification_timeout_during_separation" : "verification_timeout_before_separation";
|
|
5134
|
+
options.log(
|
|
5135
|
+
`SESSION-DB-VERIFY-TIMEOUT: verification did not finish within its ${SESSION_DB_VERIFY_TIMEOUT_MS}ms deadline; refusing to start with the restored opencode.db (${reason})`,
|
|
5136
|
+
"error"
|
|
5137
|
+
);
|
|
5138
|
+
reportRecord("verify", "session_db_boot_refused", reason, null, options);
|
|
5139
|
+
return { verifyFatal: true, verifyFatalReason: reason };
|
|
5140
|
+
}
|
|
4213
5141
|
options.log(
|
|
4214
5142
|
`SESSION-DB-VERIFY-TIMEOUT: verification did not finish within its ${SESSION_DB_VERIFY_TIMEOUT_MS}ms deadline; continuing with the restored opencode.db as-is, unverified`,
|
|
4215
5143
|
"warn"
|
|
4216
5144
|
);
|
|
4217
|
-
return false;
|
|
5145
|
+
return { verifyFatal: false };
|
|
4218
5146
|
}
|
|
4219
5147
|
if (result.code === 34) {
|
|
4220
5148
|
reportRecord(
|
|
@@ -4224,14 +5152,14 @@ async function verifySessionDb(options, configPath, env) {
|
|
|
4224
5152
|
null,
|
|
4225
5153
|
options
|
|
4226
5154
|
);
|
|
4227
|
-
return true;
|
|
5155
|
+
return { verifyFatal: true };
|
|
4228
5156
|
}
|
|
4229
5157
|
if (result.code === 33) {
|
|
4230
5158
|
options.log(
|
|
4231
5159
|
"SESSION-DB-INTEGRITY-EXHAUSTED: booting continues and litestream still replicates, starting an empty backup chain after the corrupt replica was separated.",
|
|
4232
5160
|
"warn"
|
|
4233
5161
|
);
|
|
4234
|
-
return false;
|
|
5162
|
+
return { verifyFatal: false };
|
|
4235
5163
|
}
|
|
4236
5164
|
if (result.code !== 0) {
|
|
4237
5165
|
options.log(
|
|
@@ -4239,17 +5167,17 @@ async function verifySessionDb(options, configPath, env) {
|
|
|
4239
5167
|
"warn"
|
|
4240
5168
|
);
|
|
4241
5169
|
}
|
|
4242
|
-
return false;
|
|
5170
|
+
return { verifyFatal: false };
|
|
4243
5171
|
}
|
|
4244
5172
|
options.log(
|
|
4245
5173
|
"skipping session-DB verification: this boot's session DB was not proven safe to replicate",
|
|
4246
5174
|
"debug"
|
|
4247
5175
|
);
|
|
4248
|
-
return false;
|
|
5176
|
+
return { verifyFatal: false };
|
|
4249
5177
|
}
|
|
4250
5178
|
function fileExists(path) {
|
|
4251
5179
|
try {
|
|
4252
|
-
|
|
5180
|
+
statSync3(path);
|
|
4253
5181
|
return true;
|
|
4254
5182
|
} catch (error2) {
|
|
4255
5183
|
if (error2 instanceof Error && "code" in error2 && error2.code === "ENOENT") return false;
|
|
@@ -4332,13 +5260,13 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
4332
5260
|
if (options.noReplicateMarker && fileExists(options.noReplicateMarker)) {
|
|
4333
5261
|
return { verifyFatal: false };
|
|
4334
5262
|
}
|
|
4335
|
-
return
|
|
5263
|
+
return verifySessionDb(options, configPath, env);
|
|
4336
5264
|
}
|
|
4337
5265
|
|
|
4338
5266
|
// src/lib/opencode/session-db-provenance.ts
|
|
4339
5267
|
import { createRequire } from "node:module";
|
|
4340
5268
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4341
|
-
import { dirname as
|
|
5269
|
+
import { dirname as dirname5, join as join6 } from "node:path";
|
|
4342
5270
|
var require2 = createRequire(import.meta.url);
|
|
4343
5271
|
function readSessionDbMigrationIds(dbPath) {
|
|
4344
5272
|
let db;
|
|
@@ -4375,7 +5303,7 @@ function readSessionDbMigrationIds(dbPath) {
|
|
|
4375
5303
|
}
|
|
4376
5304
|
function sessionDbProvenanceStatePath(homeDir, env) {
|
|
4377
5305
|
const override = env.EVIDENT_SESSION_DB_PROVENANCE_STATE?.trim();
|
|
4378
|
-
return override ||
|
|
5306
|
+
return override || join6(homeDir, ".local", "state", "evident", "session-db-provenance.json");
|
|
4379
5307
|
}
|
|
4380
5308
|
function loadSessionDbProvenanceState(path) {
|
|
4381
5309
|
let value;
|
|
@@ -4404,7 +5332,7 @@ function loadSessionDbProvenanceState(path) {
|
|
|
4404
5332
|
}
|
|
4405
5333
|
function saveSessionDbProvenanceState(path, state) {
|
|
4406
5334
|
try {
|
|
4407
|
-
mkdirSync2(
|
|
5335
|
+
mkdirSync2(dirname5(path), { recursive: true });
|
|
4408
5336
|
writeFileSync2(path, `${JSON.stringify(state, null, 2)}
|
|
4409
5337
|
`, "utf8");
|
|
4410
5338
|
} catch (error2) {
|
|
@@ -4495,7 +5423,7 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
4495
5423
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
4496
5424
|
return { outcome: "exited", code: child.exitCode, signal: child.signalCode };
|
|
4497
5425
|
}
|
|
4498
|
-
return new Promise((
|
|
5426
|
+
return new Promise((resolve5, reject) => {
|
|
4499
5427
|
let forced = false;
|
|
4500
5428
|
let settled = false;
|
|
4501
5429
|
const timer = setTimeout(() => {
|
|
@@ -4515,7 +5443,7 @@ async function stopProcessAndWait(child, timeoutMs, sendTerm, sendKill) {
|
|
|
4515
5443
|
settled = true;
|
|
4516
5444
|
clearTimeout(timer);
|
|
4517
5445
|
child.removeListener("exit", onExit);
|
|
4518
|
-
|
|
5446
|
+
resolve5(result);
|
|
4519
5447
|
};
|
|
4520
5448
|
const fail = (error2) => {
|
|
4521
5449
|
if (settled) return;
|
|
@@ -4985,10 +5913,13 @@ function selectSessionsToDelete(sessions, opts) {
|
|
|
4985
5913
|
toDelete.push(s.id);
|
|
4986
5914
|
}
|
|
4987
5915
|
}
|
|
4988
|
-
|
|
5916
|
+
const activityById = new Map(sessions.map((session) => [session.id, session.lastActivityMs]));
|
|
5917
|
+
return toDelete.sort(
|
|
5918
|
+
(a, b) => (activityById.get(a) ?? -Infinity) - (activityById.get(b) ?? -Infinity)
|
|
5919
|
+
);
|
|
4989
5920
|
}
|
|
4990
5921
|
var DEFAULT_INTERVAL = "1h";
|
|
4991
|
-
function
|
|
5922
|
+
function resolve2(flag, envValue, fallback) {
|
|
4992
5923
|
return flag ?? envValue ?? fallback;
|
|
4993
5924
|
}
|
|
4994
5925
|
function parseMaxCount(input) {
|
|
@@ -5004,9 +5935,9 @@ function parseMaxCount(input) {
|
|
|
5004
5935
|
}
|
|
5005
5936
|
function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
5006
5937
|
const warnings = [];
|
|
5007
|
-
const maxAgeRaw =
|
|
5008
|
-
const maxCountRaw =
|
|
5009
|
-
const intervalRaw =
|
|
5938
|
+
const maxAgeRaw = resolve2(flags.maxAge, env.EVIDENT_SESSION_CLEANUP_MAX_AGE);
|
|
5939
|
+
const maxCountRaw = resolve2(flags.maxCount, env.EVIDENT_SESSION_CLEANUP_MAX_COUNT);
|
|
5940
|
+
const intervalRaw = resolve2(
|
|
5010
5941
|
flags.interval,
|
|
5011
5942
|
env.EVIDENT_SESSION_CLEANUP_INTERVAL,
|
|
5012
5943
|
DEFAULT_INTERVAL
|
|
@@ -5045,13 +5976,13 @@ function resolveSessionCleanupConfig(flags, env = process.env) {
|
|
|
5045
5976
|
}
|
|
5046
5977
|
|
|
5047
5978
|
// src/lib/opencode/session-db-size.ts
|
|
5048
|
-
import { statSync as
|
|
5049
|
-
import { join as
|
|
5979
|
+
import { statSync as statSync4 } from "node:fs";
|
|
5980
|
+
import { join as join7 } from "node:path";
|
|
5050
5981
|
var LARGE_DB_THRESHOLD_BYTES = 268435456;
|
|
5051
5982
|
function statSessionDbBytes(homeDir) {
|
|
5052
|
-
const dbPath =
|
|
5983
|
+
const dbPath = join7(homeDir, ".local", "share", "opencode", "opencode.db");
|
|
5053
5984
|
try {
|
|
5054
|
-
return
|
|
5985
|
+
return statSync4(dbPath).size;
|
|
5055
5986
|
} catch (err) {
|
|
5056
5987
|
const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
5057
5988
|
if (!isMissingFile) {
|
|
@@ -5077,14 +6008,14 @@ function buildSessionStoreSizeWarning(input) {
|
|
|
5077
6008
|
}
|
|
5078
6009
|
|
|
5079
6010
|
// src/lib/opencode/log-tail.ts
|
|
5080
|
-
import { statSync as
|
|
6011
|
+
import { statSync as statSync5 } from "node:fs";
|
|
5081
6012
|
import { homedir as homedir5 } from "node:os";
|
|
5082
|
-
import { join as
|
|
6013
|
+
import { join as join8 } from "node:path";
|
|
5083
6014
|
import { open as open2, stat } from "node:fs/promises";
|
|
5084
6015
|
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
5085
6016
|
function resolveOpenCodeLogPath(homeDir = homedir5(), env = process.env) {
|
|
5086
|
-
const dataDir = env.XDG_DATA_HOME ||
|
|
5087
|
-
return
|
|
6017
|
+
const dataDir = env.XDG_DATA_HOME || join8(homeDir, ".local", "share");
|
|
6018
|
+
return join8(dataDir, "opencode", "log", "opencode.log");
|
|
5088
6019
|
}
|
|
5089
6020
|
function isEnoent(error2) {
|
|
5090
6021
|
return error2?.code === "ENOENT";
|
|
@@ -5100,7 +6031,7 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
|
5100
6031
|
let inode = null;
|
|
5101
6032
|
let baselineReady = true;
|
|
5102
6033
|
try {
|
|
5103
|
-
const initial =
|
|
6034
|
+
const initial = statSync5(logPath);
|
|
5104
6035
|
offset = initial.size;
|
|
5105
6036
|
inode = initial.ino;
|
|
5106
6037
|
} catch (error2) {
|
|
@@ -5120,166 +6051,43 @@ function tailOpenCodeLogFile(logPath, onChunk, opts = {}) {
|
|
|
5120
6051
|
current = await stat(logPath);
|
|
5121
6052
|
} catch (error2) {
|
|
5122
6053
|
if (!isEnoent(error2)) reportFailure("stat", logPath, error2);
|
|
5123
|
-
return;
|
|
5124
|
-
}
|
|
5125
|
-
if (!baselineReady) {
|
|
5126
|
-
offset = current.size;
|
|
5127
|
-
inode = current.ino;
|
|
5128
|
-
baselineReady = true;
|
|
5129
|
-
return;
|
|
5130
|
-
}
|
|
5131
|
-
if (inode !== null && current.ino !== inode || current.size < offset) {
|
|
5132
|
-
offset = 0;
|
|
5133
|
-
}
|
|
5134
|
-
inode = current.ino;
|
|
5135
|
-
if (current.size === offset) return;
|
|
5136
|
-
const length = current.size - offset;
|
|
5137
|
-
const fh = await open2(logPath, "r");
|
|
5138
|
-
try {
|
|
5139
|
-
const buf = Buffer.alloc(length);
|
|
5140
|
-
const { bytesRead } = await fh.read(buf, 0, length, offset);
|
|
5141
|
-
offset += bytesRead;
|
|
5142
|
-
if (bytesRead > 0) onChunk(buf.subarray(0, bytesRead));
|
|
5143
|
-
} finally {
|
|
5144
|
-
await fh.close();
|
|
5145
|
-
}
|
|
5146
|
-
} catch (error2) {
|
|
5147
|
-
if (!isEnoent(error2)) reportFailure("poll", logPath, error2);
|
|
5148
|
-
} finally {
|
|
5149
|
-
polling = false;
|
|
5150
|
-
}
|
|
5151
|
-
};
|
|
5152
|
-
const interval = setInterval(() => void poll(), pollIntervalMs);
|
|
5153
|
-
void poll();
|
|
5154
|
-
return {
|
|
5155
|
-
stop: () => {
|
|
5156
|
-
stopped = true;
|
|
5157
|
-
clearInterval(interval);
|
|
5158
|
-
}
|
|
5159
|
-
};
|
|
5160
|
-
}
|
|
5161
|
-
|
|
5162
|
-
// src/lib/opencode/session-db-reclaim.ts
|
|
5163
|
-
import { statSync as statSync5, statfsSync } from "node:fs";
|
|
5164
|
-
import { dirname as dirname4 } from "node:path";
|
|
5165
|
-
function errorMessage(error2) {
|
|
5166
|
-
if (!(error2 instanceof Error)) return String(error2);
|
|
5167
|
-
return error2.cause instanceof Error ? error2.cause.message : error2.message;
|
|
5168
|
-
}
|
|
5169
|
-
function insufficientSpaceReason(dbPath, requiredBytes) {
|
|
5170
|
-
try {
|
|
5171
|
-
const fsStats = statfsSync(dirname4(dbPath));
|
|
5172
|
-
const availableBytes = fsStats.bavail * fsStats.bsize;
|
|
5173
|
-
if (availableBytes < requiredBytes) {
|
|
5174
|
-
return `only ${availableBytes} bytes free, need ${requiredBytes} for a second copy`;
|
|
5175
|
-
}
|
|
5176
|
-
return null;
|
|
5177
|
-
} catch (err) {
|
|
5178
|
-
return `could not check free space (${err instanceof Error ? err.message : String(err)}); refusing to guess`;
|
|
5179
|
-
}
|
|
5180
|
-
}
|
|
5181
|
-
function readLogicalBytes(db) {
|
|
5182
|
-
const pageCount = db.prepare("PRAGMA page_count").get().page_count;
|
|
5183
|
-
const pageSize = db.prepare("PRAGMA page_size").get().page_size;
|
|
5184
|
-
return pageCount * pageSize;
|
|
5185
|
-
}
|
|
5186
|
-
function readCheckpointResult(db) {
|
|
5187
|
-
const row = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
5188
|
-
return { busy: row.busy !== 0, log: row.log, checkpointed: row.checkpointed };
|
|
5189
|
-
}
|
|
5190
|
-
async function probeReclaimAvailability(input) {
|
|
5191
|
-
const { dbPath, requiredBytes } = input;
|
|
5192
|
-
let sqlite;
|
|
5193
|
-
try {
|
|
5194
|
-
sqlite = await import("node:sqlite");
|
|
5195
|
-
} catch (err) {
|
|
5196
|
-
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
5197
|
-
console.warn(`[probeReclaimAvailability] node:sqlite unavailable for ${dbPath}: ` + detail);
|
|
5198
|
-
return { reason: "sqlite-unavailable", detail };
|
|
5199
|
-
}
|
|
5200
|
-
let autoVacuum = null;
|
|
5201
|
-
try {
|
|
5202
|
-
const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
5203
|
-
try {
|
|
5204
|
-
db.exec("PRAGMA busy_timeout=5000");
|
|
5205
|
-
autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
5206
|
-
} finally {
|
|
5207
|
-
db.close();
|
|
5208
|
-
}
|
|
5209
|
-
} catch (err) {
|
|
5210
|
-
console.warn(
|
|
5211
|
-
`[probeReclaimAvailability] could not read auto_vacuum mode for ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
5212
|
-
);
|
|
5213
|
-
}
|
|
5214
|
-
if (autoVacuum !== 0) return null;
|
|
5215
|
-
return insufficientSpaceReason(dbPath, requiredBytes) !== null ? { reason: "insufficient-disk-space" } : null;
|
|
5216
|
-
}
|
|
5217
|
-
async function reclaimSessionDbSpace(input) {
|
|
5218
|
-
const { dbPath, maxPages, allowFullVacuum = true } = input;
|
|
5219
|
-
let sqlite;
|
|
5220
|
-
try {
|
|
5221
|
-
sqlite = await import("node:sqlite");
|
|
5222
|
-
} catch (err) {
|
|
5223
|
-
const detail = `Node ${process.version}: ${errorMessage(err)}`;
|
|
5224
|
-
console.warn(
|
|
5225
|
-
`[reclaimSessionDbSpace] node:sqlite unavailable (need Node >=22.5, >=22.13 unflagged) for ${dbPath}: ${detail}`
|
|
5226
|
-
);
|
|
5227
|
-
return { ok: false, skipped: "sqlite-unavailable", detail };
|
|
5228
|
-
}
|
|
5229
|
-
const { DatabaseSync } = sqlite;
|
|
5230
|
-
let db;
|
|
5231
|
-
try {
|
|
5232
|
-
db = new DatabaseSync(dbPath);
|
|
5233
|
-
db.exec("PRAGMA busy_timeout=5000");
|
|
5234
|
-
const autoVacuum = db.prepare("PRAGMA auto_vacuum").get().auto_vacuum;
|
|
5235
|
-
if (autoVacuum === 0) {
|
|
5236
|
-
if (!allowFullVacuum) {
|
|
5237
|
-
console.warn(
|
|
5238
|
-
`[reclaimSessionDbSpace] skipping VACUUM conversion of ${dbPath}: a session turn is live`
|
|
5239
|
-
);
|
|
5240
|
-
return {
|
|
5241
|
-
ok: false,
|
|
5242
|
-
skipped: "full-vacuum-blocked",
|
|
5243
|
-
detail: "a session turn is live"
|
|
5244
|
-
};
|
|
6054
|
+
return;
|
|
5245
6055
|
}
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
);
|
|
5252
|
-
return { ok: false, skipped: "insufficient-disk-space", detail: skipReason };
|
|
6056
|
+
if (!baselineReady) {
|
|
6057
|
+
offset = current.size;
|
|
6058
|
+
inode = current.ino;
|
|
6059
|
+
baselineReady = true;
|
|
6060
|
+
return;
|
|
5253
6061
|
}
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
6062
|
+
if (inode !== null && current.ino !== inode || current.size < offset) {
|
|
6063
|
+
offset = 0;
|
|
6064
|
+
}
|
|
6065
|
+
inode = current.ino;
|
|
6066
|
+
if (current.size === offset) return;
|
|
6067
|
+
const length = current.size - offset;
|
|
6068
|
+
const fh = await open2(logPath, "r");
|
|
6069
|
+
try {
|
|
6070
|
+
const buf = Buffer.alloc(length);
|
|
6071
|
+
const { bytesRead } = await fh.read(buf, 0, length, offset);
|
|
6072
|
+
offset += bytesRead;
|
|
6073
|
+
if (bytesRead > 0) onChunk(buf.subarray(0, bytesRead));
|
|
6074
|
+
} finally {
|
|
6075
|
+
await fh.close();
|
|
6076
|
+
}
|
|
6077
|
+
} catch (error2) {
|
|
6078
|
+
if (!isEnoent(error2)) reportFailure("poll", logPath, error2);
|
|
6079
|
+
} finally {
|
|
6080
|
+
polling = false;
|
|
5260
6081
|
}
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
6082
|
+
};
|
|
6083
|
+
const interval = setInterval(() => void poll(), pollIntervalMs);
|
|
6084
|
+
void poll();
|
|
6085
|
+
return {
|
|
6086
|
+
stop: () => {
|
|
6087
|
+
stopped = true;
|
|
6088
|
+
clearInterval(interval);
|
|
5268
6089
|
}
|
|
5269
|
-
|
|
5270
|
-
`[reclaimSessionDbSpace] ${dbPath} has auto_vacuum=${autoVacuum} (neither NONE nor INCREMENTAL); nothing to reclaim`
|
|
5271
|
-
);
|
|
5272
|
-
return { ok: false, skipped: "auto-vacuum-not-applicable" };
|
|
5273
|
-
} catch (err) {
|
|
5274
|
-
console.error(`[reclaimSessionDbSpace] reclaim failed for ${dbPath}: ` + errorMessage(err));
|
|
5275
|
-
return {
|
|
5276
|
-
ok: false,
|
|
5277
|
-
skipped: "reclaim-error",
|
|
5278
|
-
detail: errorMessage(err)
|
|
5279
|
-
};
|
|
5280
|
-
} finally {
|
|
5281
|
-
db?.close();
|
|
5282
|
-
}
|
|
6090
|
+
};
|
|
5283
6091
|
}
|
|
5284
6092
|
|
|
5285
6093
|
// src/lib/tunnel/connection.ts
|
|
@@ -5362,8 +6170,8 @@ var StreamForwarder = class {
|
|
|
5362
6170
|
pauseStream(sid, stream) {
|
|
5363
6171
|
if (!stream.paused) {
|
|
5364
6172
|
stream.paused = true;
|
|
5365
|
-
stream.pausePromise = new Promise((
|
|
5366
|
-
stream.resume =
|
|
6173
|
+
stream.pausePromise = new Promise((resolve5) => {
|
|
6174
|
+
stream.resume = resolve5;
|
|
5367
6175
|
});
|
|
5368
6176
|
}
|
|
5369
6177
|
if (stream.pauseTimer !== null) clearTimeout(stream.pauseTimer);
|
|
@@ -5430,12 +6238,12 @@ var StreamForwarder = class {
|
|
|
5430
6238
|
let endBody;
|
|
5431
6239
|
if (has_body) {
|
|
5432
6240
|
const chunks = [];
|
|
5433
|
-
bodyPromise = new Promise((
|
|
6241
|
+
bodyPromise = new Promise((resolve5) => {
|
|
5434
6242
|
pushBody = (buf) => {
|
|
5435
6243
|
chunks.push(buf);
|
|
5436
6244
|
};
|
|
5437
6245
|
endBody = () => {
|
|
5438
|
-
|
|
6246
|
+
resolve5(Buffer.concat(chunks));
|
|
5439
6247
|
};
|
|
5440
6248
|
});
|
|
5441
6249
|
}
|
|
@@ -5511,6 +6319,8 @@ var StreamForwarder = class {
|
|
|
5511
6319
|
if (done) break;
|
|
5512
6320
|
const chunk = Buffer.from(value);
|
|
5513
6321
|
for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
|
|
6322
|
+
await stream.pausePromise;
|
|
6323
|
+
if (ac.signal.aborted) break;
|
|
5514
6324
|
const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
|
|
5515
6325
|
if (this.binaryFramesSupported) {
|
|
5516
6326
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
@@ -5553,6 +6363,7 @@ function classifyUpgradeRejection(headers) {
|
|
|
5553
6363
|
}
|
|
5554
6364
|
var MAX_RECONNECT_DELAY = 3e4;
|
|
5555
6365
|
var BASE_RECONNECT_DELAY = 500;
|
|
6366
|
+
var RELAY_SILENCE_TIMEOUT_MS = 9e4;
|
|
5556
6367
|
function getReconnectDelay(attempt) {
|
|
5557
6368
|
const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
|
|
5558
6369
|
const jitter = Math.random() * 1e3;
|
|
@@ -5603,7 +6414,7 @@ function connectTunnel(options) {
|
|
|
5603
6414
|
} = options;
|
|
5604
6415
|
const tunnelUrl = getTunnelUrlConfig();
|
|
5605
6416
|
const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
|
|
5606
|
-
return new Promise((
|
|
6417
|
+
return new Promise((resolve5, reject) => {
|
|
5607
6418
|
const ws = new WebSocket2(url, {
|
|
5608
6419
|
headers: {
|
|
5609
6420
|
Authorization: authHeader,
|
|
@@ -5624,10 +6435,27 @@ function connectTunnel(options) {
|
|
|
5624
6435
|
ws.close();
|
|
5625
6436
|
reject(new Error("Connection timeout"));
|
|
5626
6437
|
}, 3e4);
|
|
6438
|
+
let relaySilenceTimeout;
|
|
6439
|
+
let relaySilenceTimedOut = false;
|
|
6440
|
+
const clearRelaySilenceTimeout = () => {
|
|
6441
|
+
if (relaySilenceTimeout === void 0) return;
|
|
6442
|
+
clearTimeout(relaySilenceTimeout);
|
|
6443
|
+
relaySilenceTimeout = void 0;
|
|
6444
|
+
};
|
|
6445
|
+
const armRelaySilenceTimeout = () => {
|
|
6446
|
+
clearRelaySilenceTimeout();
|
|
6447
|
+
relaySilenceTimeout = setTimeout(() => {
|
|
6448
|
+
relaySilenceTimeout = void 0;
|
|
6449
|
+
relaySilenceTimedOut = true;
|
|
6450
|
+
ws.terminate();
|
|
6451
|
+
}, RELAY_SILENCE_TIMEOUT_MS);
|
|
6452
|
+
relaySilenceTimeout.unref?.();
|
|
6453
|
+
};
|
|
5627
6454
|
let upgradeRejection = null;
|
|
5628
6455
|
let upgradeRejectionReason = null;
|
|
5629
6456
|
ws.on("unexpected-response", (_req, res) => {
|
|
5630
6457
|
clearTimeout(connectionTimeout);
|
|
6458
|
+
clearRelaySilenceTimeout();
|
|
5631
6459
|
const reason = classifyUpgradeRejection(res.headers);
|
|
5632
6460
|
upgradeRejectionReason = reason;
|
|
5633
6461
|
const chunks = [];
|
|
@@ -5657,6 +6485,7 @@ function connectTunnel(options) {
|
|
|
5657
6485
|
onInfo?.("WebSocket connection established");
|
|
5658
6486
|
});
|
|
5659
6487
|
ws.on("message", (data, isBinary) => {
|
|
6488
|
+
if (relaySilenceTimeout !== void 0) armRelaySilenceTimeout();
|
|
5660
6489
|
if (isBinary) {
|
|
5661
6490
|
try {
|
|
5662
6491
|
const frame = decodeBinaryBodyFrame(toUint8Array(data));
|
|
@@ -5690,10 +6519,11 @@ function connectTunnel(options) {
|
|
|
5690
6519
|
switch (message.type) {
|
|
5691
6520
|
case "connected": {
|
|
5692
6521
|
clearTimeout(connectionTimeout);
|
|
6522
|
+
armRelaySilenceTimeout();
|
|
5693
6523
|
forwarder.setBinaryFramesSupported(message.binary_frames === true);
|
|
5694
6524
|
const connectedAgentId = message.agent_id ?? agentId;
|
|
5695
6525
|
onConnected?.(connectedAgentId);
|
|
5696
|
-
|
|
6526
|
+
resolve5({
|
|
5697
6527
|
ws,
|
|
5698
6528
|
close: () => ws.close(1e3, "CLI shutdown")
|
|
5699
6529
|
});
|
|
@@ -5714,6 +6544,7 @@ function connectTunnel(options) {
|
|
|
5714
6544
|
});
|
|
5715
6545
|
ws.on("error", (error2) => {
|
|
5716
6546
|
clearTimeout(connectionTimeout);
|
|
6547
|
+
clearRelaySilenceTimeout();
|
|
5717
6548
|
const detail = upgradeRejection ?? describeSocketError(error2, url);
|
|
5718
6549
|
if (upgradeRejectionReason === "do_code_updated") {
|
|
5719
6550
|
onWarning?.("Relay redeployed \u2014 reconnecting");
|
|
@@ -5725,7 +6556,8 @@ function connectTunnel(options) {
|
|
|
5725
6556
|
);
|
|
5726
6557
|
});
|
|
5727
6558
|
ws.on("close", (code, reason) => {
|
|
5728
|
-
|
|
6559
|
+
clearRelaySilenceTimeout();
|
|
6560
|
+
const reasonStr = (relaySilenceTimedOut ? "no relay heartbeat for 90s \u2014 reconnecting" : void 0) || reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
|
|
5729
6561
|
forwarder.abortAll();
|
|
5730
6562
|
onDisconnected?.(code, reasonStr);
|
|
5731
6563
|
});
|
|
@@ -5733,7 +6565,7 @@ function connectTunnel(options) {
|
|
|
5733
6565
|
}
|
|
5734
6566
|
|
|
5735
6567
|
// src/lib/tunnel/runner-connection.ts
|
|
5736
|
-
var RunnerConnection = class {
|
|
6568
|
+
var RunnerConnection = class _RunnerConnection {
|
|
5737
6569
|
opts;
|
|
5738
6570
|
sleep;
|
|
5739
6571
|
now;
|
|
@@ -5747,6 +6579,10 @@ var RunnerConnection = class {
|
|
|
5747
6579
|
reconnectAttempt = 0;
|
|
5748
6580
|
generation = 0;
|
|
5749
6581
|
disconnectedAtMs = null;
|
|
6582
|
+
static RELAY_RECONNECT_CLOSE_REASONS = /* @__PURE__ */ new Set([
|
|
6583
|
+
"No pong received",
|
|
6584
|
+
"Orphaned socket"
|
|
6585
|
+
]);
|
|
5750
6586
|
constructor(opts) {
|
|
5751
6587
|
this.opts = opts;
|
|
5752
6588
|
this.resolvedAgentId = opts.agentId;
|
|
@@ -5803,7 +6639,8 @@ var RunnerConnection = class {
|
|
|
5803
6639
|
onDisconnected: (code, reason) => {
|
|
5804
6640
|
this.disconnectedAtMs = this.now();
|
|
5805
6641
|
events.onDisconnected(code, reason, { generation: this.generation });
|
|
5806
|
-
|
|
6642
|
+
const relayRequestedReconnect = code === 1e3 && _RunnerConnection.RELAY_RECONNECT_CLOSE_REASONS.has(reason);
|
|
6643
|
+
if (this.opts.isRunning() && (code !== 1e3 || relayRequestedReconnect) && !this.reconnecting) {
|
|
5807
6644
|
this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
|
|
5808
6645
|
events.onError?.(`Reconnection failed: ${err.message}`);
|
|
5809
6646
|
});
|
|
@@ -6142,16 +6979,111 @@ function createResourceUsageCollector(homeDir) {
|
|
|
6142
6979
|
};
|
|
6143
6980
|
}
|
|
6144
6981
|
|
|
6982
|
+
// src/lib/resource-usage-low-disk.ts
|
|
6983
|
+
var RUNNER_DISK_ALERT_ENTRY_FREE_BYTES = 4 * 1024 ** 3;
|
|
6984
|
+
var RUNNER_DISK_ALERT_ENTRY_FREE_FRACTION = 0.25;
|
|
6985
|
+
var RUNNER_DISK_ALERT_VERY_LOW_FREE_BYTES = 1 * 1024 ** 3;
|
|
6986
|
+
var RUNNER_DISK_ALERT_CRITICAL_FREE_BYTES = 256 * 1024 ** 2;
|
|
6987
|
+
var RUNNER_DISK_ALERT_RECOVERY_FREE_BYTES = 6 * 1024 ** 3;
|
|
6988
|
+
var RUNNER_DISK_ALERT_RECOVERY_FREE_FRACTION = 0.375;
|
|
6989
|
+
var LOCAL_DISK_WARNING_REASSERTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
6990
|
+
var BAND_RANK = {
|
|
6991
|
+
low: 1,
|
|
6992
|
+
very_low: 2,
|
|
6993
|
+
critical: 3
|
|
6994
|
+
};
|
|
6995
|
+
function bandFor(freeBytes) {
|
|
6996
|
+
if (freeBytes < RUNNER_DISK_ALERT_CRITICAL_FREE_BYTES) return "critical";
|
|
6997
|
+
if (freeBytes < RUNNER_DISK_ALERT_VERY_LOW_FREE_BYTES) return "very_low";
|
|
6998
|
+
return "low";
|
|
6999
|
+
}
|
|
7000
|
+
function formatBytes(bytes) {
|
|
7001
|
+
if (bytes === null) return "unavailable";
|
|
7002
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
7003
|
+
const unitIndex = Math.min(Math.floor(Math.log2(Math.max(bytes, 1)) / 10), units.length - 1);
|
|
7004
|
+
const value = bytes / 1024 ** unitIndex;
|
|
7005
|
+
const digits = unitIndex === 0 || value >= 100 ? 0 : value >= 10 ? 1 : 2;
|
|
7006
|
+
return `${value.toFixed(digits)} ${units[unitIndex]}`;
|
|
7007
|
+
}
|
|
7008
|
+
function warningMessage(band, diskFreeBytes, diskTotalBytes, opencodeDbBytes) {
|
|
7009
|
+
return `Low disk warning (${band} severity): ${formatBytes(diskFreeBytes)} free of ${formatBytes(diskTotalBytes)}; opencode.db is ${formatBytes(opencodeDbBytes)}. Clear space on the machine running this runner before session history or new work can no longer be saved.`;
|
|
7010
|
+
}
|
|
7011
|
+
function evaluateLowDiskWarning(input) {
|
|
7012
|
+
const { diskFreeBytes, diskTotalBytes, opencodeDbBytes, nowMs, state } = input;
|
|
7013
|
+
if (diskFreeBytes === null || diskTotalBytes === null || diskTotalBytes <= 0 || diskFreeBytes >= diskTotalBytes) {
|
|
7014
|
+
return { warning: null, state };
|
|
7015
|
+
}
|
|
7016
|
+
const recovered = diskFreeBytes >= RUNNER_DISK_ALERT_RECOVERY_FREE_BYTES || diskFreeBytes >= diskTotalBytes * RUNNER_DISK_ALERT_RECOVERY_FREE_FRACTION;
|
|
7017
|
+
if (recovered) {
|
|
7018
|
+
return { warning: null, state: { worstBand: null, warnedAtMs: null } };
|
|
7019
|
+
}
|
|
7020
|
+
const low = diskFreeBytes < RUNNER_DISK_ALERT_ENTRY_FREE_BYTES && diskFreeBytes < diskTotalBytes * RUNNER_DISK_ALERT_ENTRY_FREE_FRACTION;
|
|
7021
|
+
if (!low) return { warning: null, state };
|
|
7022
|
+
const currentBand = bandFor(diskFreeBytes);
|
|
7023
|
+
const currentRank = BAND_RANK[currentBand];
|
|
7024
|
+
const previousRank = state.worstBand === null ? 0 : BAND_RANK[state.worstBand];
|
|
7025
|
+
const worse = currentRank > previousRank;
|
|
7026
|
+
const reassert = state.warnedAtMs !== null && nowMs - state.warnedAtMs >= LOCAL_DISK_WARNING_REASSERTION_INTERVAL_MS;
|
|
7027
|
+
const nextState = {
|
|
7028
|
+
worstBand: worse ? currentBand : state.worstBand ?? currentBand,
|
|
7029
|
+
warnedAtMs: worse || state.warnedAtMs === null || reassert ? nowMs : state.warnedAtMs
|
|
7030
|
+
};
|
|
7031
|
+
return {
|
|
7032
|
+
warning: worse || state.warnedAtMs === null || reassert ? warningMessage(nextState.worstBand, diskFreeBytes, diskTotalBytes, opencodeDbBytes) : null,
|
|
7033
|
+
state: nextState
|
|
7034
|
+
};
|
|
7035
|
+
}
|
|
7036
|
+
|
|
7037
|
+
// src/lib/opencode/session-store-health.ts
|
|
7038
|
+
var SESSION_STORE_HEALTH_STALENESS_MS = 5 * 6e4;
|
|
7039
|
+
function createSessionStoreHealth(options = {}) {
|
|
7040
|
+
const now = options.now ?? (() => Date.now());
|
|
7041
|
+
const stalenessMs = Math.max(0, options.stalenessMs ?? SESSION_STORE_HEALTH_STALENESS_MS);
|
|
7042
|
+
let lastUnwritableAt = null;
|
|
7043
|
+
let lastWritableAt = null;
|
|
7044
|
+
const causes = /* @__PURE__ */ new Map();
|
|
7045
|
+
return {
|
|
7046
|
+
observeUnwritable: () => {
|
|
7047
|
+
lastUnwritableAt = now();
|
|
7048
|
+
},
|
|
7049
|
+
observeWritable: () => {
|
|
7050
|
+
lastWritableAt = now();
|
|
7051
|
+
},
|
|
7052
|
+
isUnwritable: () => {
|
|
7053
|
+
if (lastUnwritableAt === null) return false;
|
|
7054
|
+
if (lastWritableAt !== null && lastWritableAt >= lastUnwritableAt) return false;
|
|
7055
|
+
return now() - lastUnwritableAt < stalenessMs;
|
|
7056
|
+
},
|
|
7057
|
+
observeCause: (sessionId, cause) => {
|
|
7058
|
+
if (sessionId === null) return;
|
|
7059
|
+
causes.delete(sessionId);
|
|
7060
|
+
causes.set(sessionId, { cause, observedAt: now() });
|
|
7061
|
+
while (causes.size > 256) {
|
|
7062
|
+
const oldest = causes.keys().next().value;
|
|
7063
|
+
if (typeof oldest !== "string") break;
|
|
7064
|
+
causes.delete(oldest);
|
|
7065
|
+
}
|
|
7066
|
+
},
|
|
7067
|
+
readCause: (sessionId) => {
|
|
7068
|
+
const observed = causes.get(sessionId);
|
|
7069
|
+
if (!observed) return null;
|
|
7070
|
+
causes.delete(sessionId);
|
|
7071
|
+
if (now() - observed.observedAt >= stalenessMs) return null;
|
|
7072
|
+
return observed.cause;
|
|
7073
|
+
}
|
|
7074
|
+
};
|
|
7075
|
+
}
|
|
7076
|
+
|
|
6145
7077
|
// src/lib/channels/driver.ts
|
|
6146
7078
|
import { homedir as homedir6 } from "node:os";
|
|
6147
7079
|
|
|
6148
7080
|
// src/lib/runner-file-sync.ts
|
|
6149
|
-
import { join as
|
|
7081
|
+
import { join as join10 } from "node:path";
|
|
6150
7082
|
|
|
6151
7083
|
// src/lib/file-push.ts
|
|
6152
7084
|
import { randomUUID } from "node:crypto";
|
|
6153
7085
|
import { chmod, mkdir, open as open3, realpath, rename, unlink } from "node:fs/promises";
|
|
6154
|
-
import { basename, dirname as
|
|
7086
|
+
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute2, join as join9, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
|
6155
7087
|
var FILE_MODE = 384;
|
|
6156
7088
|
var DIRECTORY_MODE = 448;
|
|
6157
7089
|
async function writePushedFile(request2) {
|
|
@@ -6182,9 +7114,9 @@ async function writePushedFile(request2) {
|
|
|
6182
7114
|
}
|
|
6183
7115
|
try {
|
|
6184
7116
|
const { existingAncestor, missingSegments } = await resolveNearestExistingAncestor(
|
|
6185
|
-
|
|
7117
|
+
dirname6(candidate)
|
|
6186
7118
|
);
|
|
6187
|
-
const realTarget =
|
|
7119
|
+
const realTarget = join9(existingAncestor, ...missingSegments, basename2(candidate));
|
|
6188
7120
|
const allowedDirectory = await findContainingAllowedDirectory(allowedDirectories, realTarget);
|
|
6189
7121
|
if (allowedDirectory === null) {
|
|
6190
7122
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
@@ -6194,8 +7126,8 @@ async function writePushedFile(request2) {
|
|
|
6194
7126
|
}
|
|
6195
7127
|
if (missingSegments.length > 0) {
|
|
6196
7128
|
await createMissingDirectories(existingAncestor, missingSegments);
|
|
6197
|
-
const realParent = await realpath(
|
|
6198
|
-
if (realParent !==
|
|
7129
|
+
const realParent = await realpath(dirname6(realTarget));
|
|
7130
|
+
if (realParent !== dirname6(realTarget) || !contains(allowedDirectory, realTarget)) {
|
|
6199
7131
|
return refuse("path_not_allowed", "The runner does not allow writing to that location.", {
|
|
6200
7132
|
path: realTarget,
|
|
6201
7133
|
bytes,
|
|
@@ -6220,15 +7152,15 @@ function expandAndValidate(requestedPath, homeDir) {
|
|
|
6220
7152
|
if (requestedPath.trim() === "" || requestedPath.includes("\0")) {
|
|
6221
7153
|
return null;
|
|
6222
7154
|
}
|
|
6223
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
7155
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join9(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
6224
7156
|
if (expanded.split(/[/\\]/).includes("..")) {
|
|
6225
7157
|
return null;
|
|
6226
7158
|
}
|
|
6227
|
-
if (!
|
|
7159
|
+
if (!isAbsolute2(expanded)) {
|
|
6228
7160
|
return null;
|
|
6229
7161
|
}
|
|
6230
|
-
const candidate =
|
|
6231
|
-
const name =
|
|
7162
|
+
const candidate = resolve3(expanded);
|
|
7163
|
+
const name = basename2(candidate);
|
|
6232
7164
|
return name === "" || name === "." || name === ".." ? null : candidate;
|
|
6233
7165
|
}
|
|
6234
7166
|
async function resolveNearestExistingAncestor(directory) {
|
|
@@ -6238,18 +7170,18 @@ async function resolveNearestExistingAncestor(directory) {
|
|
|
6238
7170
|
try {
|
|
6239
7171
|
return { existingAncestor: await realpath(current), missingSegments };
|
|
6240
7172
|
} catch (err) {
|
|
6241
|
-
const parent =
|
|
7173
|
+
const parent = dirname6(current);
|
|
6242
7174
|
if (err.code !== "ENOENT" || parent === current) {
|
|
6243
7175
|
throw err;
|
|
6244
7176
|
}
|
|
6245
|
-
missingSegments.unshift(
|
|
7177
|
+
missingSegments.unshift(basename2(current));
|
|
6246
7178
|
current = parent;
|
|
6247
7179
|
}
|
|
6248
7180
|
}
|
|
6249
7181
|
}
|
|
6250
7182
|
async function findContainingAllowedDirectory(allowedDirectories, realTarget) {
|
|
6251
7183
|
for (const directory of allowedDirectories) {
|
|
6252
|
-
if (!
|
|
7184
|
+
if (!isAbsolute2(directory)) {
|
|
6253
7185
|
log("warn", "file_push_allowed_directory_skipped", { directory, reason: "not_absolute" });
|
|
6254
7186
|
continue;
|
|
6255
7187
|
}
|
|
@@ -6287,19 +7219,19 @@ async function realpathCreatingIfMissing(directory) {
|
|
|
6287
7219
|
}
|
|
6288
7220
|
}
|
|
6289
7221
|
function contains(realDirectory, realTarget) {
|
|
6290
|
-
const rel =
|
|
6291
|
-
return rel !== "" && rel !== ".." && !rel.startsWith(`..${
|
|
7222
|
+
const rel = relative2(realDirectory, realTarget);
|
|
7223
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute2(rel);
|
|
6292
7224
|
}
|
|
6293
7225
|
async function createMissingDirectories(existingAncestor, missingSegments) {
|
|
6294
7226
|
let current = existingAncestor;
|
|
6295
7227
|
for (const segment of missingSegments) {
|
|
6296
|
-
current =
|
|
7228
|
+
current = join9(current, segment);
|
|
6297
7229
|
await mkdir(current, { recursive: true, mode: DIRECTORY_MODE });
|
|
6298
7230
|
await chmod(current, DIRECTORY_MODE);
|
|
6299
7231
|
}
|
|
6300
7232
|
}
|
|
6301
7233
|
async function writeAtomically(realTarget, content) {
|
|
6302
|
-
const temporaryPath =
|
|
7234
|
+
const temporaryPath = join9(dirname6(realTarget), `.evident-push-${randomUUID()}.tmp`);
|
|
6303
7235
|
let handle;
|
|
6304
7236
|
try {
|
|
6305
7237
|
handle = await open3(temporaryPath, "wx", FILE_MODE);
|
|
@@ -6429,12 +7361,12 @@ var NOT_APPLIED = {
|
|
|
6429
7361
|
opencodeAuthApplied: false
|
|
6430
7362
|
};
|
|
6431
7363
|
function isClaudeCredentialPath(requestedPath, homeDir) {
|
|
6432
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
6433
|
-
return expanded ===
|
|
7364
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
7365
|
+
return expanded === join10(homeDir, ...CLAUDE_CREDENTIALS_SEGMENTS);
|
|
6434
7366
|
}
|
|
6435
7367
|
function isOpenCodeAuthPath(requestedPath, homeDir) {
|
|
6436
|
-
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ?
|
|
6437
|
-
return expanded ===
|
|
7368
|
+
const expanded = requestedPath === "~" ? homeDir : requestedPath.startsWith("~/") ? join10(homeDir, requestedPath.slice(2)) : requestedPath;
|
|
7369
|
+
return expanded === join10(homeDir, ...OPENCODE_AUTH_SEGMENTS);
|
|
6438
7370
|
}
|
|
6439
7371
|
async function applyOne(options, file) {
|
|
6440
7372
|
const label = `${file.id.slice(0, 8)} (${file.path})`;
|
|
@@ -6578,6 +7510,11 @@ function messageIdOf(m) {
|
|
|
6578
7510
|
const infoId = m.info?.id;
|
|
6579
7511
|
return typeof infoId === "string" ? infoId : void 0;
|
|
6580
7512
|
}
|
|
7513
|
+
function composeAbandonmentNote(finish, replyShape, outcome) {
|
|
7514
|
+
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.");
|
|
7515
|
+
if (replyShape === null) return base;
|
|
7516
|
+
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)` : ""}.`;
|
|
7517
|
+
}
|
|
6581
7518
|
function cleanImageMime(contentType) {
|
|
6582
7519
|
if (!contentType) return null;
|
|
6583
7520
|
const media = contentType.split(";")[0].trim().toLowerCase();
|
|
@@ -6598,8 +7535,13 @@ var SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS = 2e3;
|
|
|
6598
7535
|
var SESSION_ERROR_STREAM_HEALTHY_MS = 5e3;
|
|
6599
7536
|
var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
6600
7537
|
var MAX_BUFFERED_SESSION_ERRORS = 256;
|
|
7538
|
+
var SESSION_ERROR_RETENTION_TTL_MS = 6e4;
|
|
7539
|
+
var SESSION_STORE_UNWRITABLE_ERROR = "The runner session database could not be written, so this turn did not finish. Nothing was lost from the queue. Clear disk space and retry.";
|
|
7540
|
+
var SESSION_ERROR_RETAINED_DETAIL = "The runner's session database could not be written, so this turn stopped part-way. Runner-observed session error: ";
|
|
7541
|
+
var SESSION_ERROR_OBSERVED_CAUSE = "Runner-observed OpenCode session-store cause: ";
|
|
6601
7542
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
6602
7543
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
7544
|
+
var DRIVER_SESSION_ENUMERATION_LIMIT = 20;
|
|
6603
7545
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
6604
7546
|
var HEARTBEAT_MS = 6e4;
|
|
6605
7547
|
var ALIVE_WITHHELD_THRESHOLD_MS = 3 * HEARTBEAT_MS;
|
|
@@ -6661,6 +7603,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6661
7603
|
maxActiveSessions;
|
|
6662
7604
|
watcherStallMs;
|
|
6663
7605
|
wedgeWarningIntervalMs;
|
|
7606
|
+
sessionStoreHealth;
|
|
6664
7607
|
openCodeClient;
|
|
6665
7608
|
/** Cache of conversationId → opencode sessionId. */
|
|
6666
7609
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -6753,6 +7696,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6753
7696
|
* with a short TTL so an unmatched session cannot retain an event indefinitely.
|
|
6754
7697
|
*/
|
|
6755
7698
|
bufferedSessionErrors = /* @__PURE__ */ new Map();
|
|
7699
|
+
retainedSessionErrors = /* @__PURE__ */ new Map();
|
|
6756
7700
|
/**
|
|
6757
7701
|
* AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
|
|
6758
7702
|
* dispatched and are still in-flight. A message in this set is never
|
|
@@ -6785,10 +7729,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6785
7729
|
* row is `processing` or `pending`; clear it only when absent from both lists.
|
|
6786
7730
|
*/
|
|
6787
7731
|
untrackableAck = /* @__PURE__ */ new Map();
|
|
7732
|
+
/** Session binding used by the first untrackable-ack report, for retries. */
|
|
7733
|
+
untrackableAckSessionIds = /* @__PURE__ */ new Map();
|
|
6788
7734
|
/** Messages whose untrackable-ack failure signal was emitted while fenced. */
|
|
6789
7735
|
untrackableAckSignalled = /* @__PURE__ */ new Set();
|
|
6790
7736
|
/** Messages whose first untrackable-ack fence warning was already logged. */
|
|
6791
7737
|
untrackableAckWarned = /* @__PURE__ */ new Set();
|
|
7738
|
+
/**
|
|
7739
|
+
* Accepted V1 prompts whose assigned ids were not read back; these rows are
|
|
7740
|
+
* re-read instead of POSTed again. A null snapshot cannot safely exclude an older
|
|
7741
|
+
* identical-text turn, so it never produces a correlation.
|
|
7742
|
+
*/
|
|
7743
|
+
awaitingCorrelation = /* @__PURE__ */ new Map();
|
|
6792
7744
|
/** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
|
|
6793
7745
|
pendingMessageIds = /* @__PURE__ */ new Set();
|
|
6794
7746
|
/**
|
|
@@ -6874,21 +7826,6 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6874
7826
|
* re-signal on every one of the 15h of re-dispatch attempts #1110 made.
|
|
6875
7827
|
*/
|
|
6876
7828
|
dispatchNotStartedSignalled = /* @__PURE__ */ new Map();
|
|
6877
|
-
/**
|
|
6878
|
-
* Consecutive-UNCONFIRMED-dispatch streak for a `pending` row with NO stored
|
|
6879
|
-
* `opencode_message_id` yet — i.e. one that has never even reached the
|
|
6880
|
-
* re-drive fence above. `sendPromptAsync`'s POST may 2xx, but its own
|
|
6881
|
-
* read-back retries can never confirm the assigned id when the session's
|
|
6882
|
-
* message list is PERMANENTLY unreadable (e.g. a corrupted local opencode
|
|
6883
|
-
* SQLite DB, #1345/#1348's exact fault, just hit BEFORE the row is ever
|
|
6884
|
-
* dispatched instead of after). Unlike an already-dispatched row, THIS row has
|
|
6885
|
-
* no other safety net at all: the lifecycle cron only reclaims `status =
|
|
6886
|
-
* 'processing'` rows, and a row stuck here never reaches `processing`. Keyed
|
|
6887
|
-
* by message id, carrying `sessionId` so a session change (a fresh one bound
|
|
6888
|
-
* after abandonment) starts a new streak rather than inheriting the old
|
|
6889
|
-
* session's count — same shape as `redrivePollFailures` above.
|
|
6890
|
-
*/
|
|
6891
|
-
unconfirmedDispatchFailures = /* @__PURE__ */ new Map();
|
|
6892
7829
|
/**
|
|
6893
7830
|
* Consecutive give-ups for a message whose OpenCode user row remains queued.
|
|
6894
7831
|
* The session id scopes the count: a fresh session is a new attempt after a
|
|
@@ -6906,8 +7843,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6906
7843
|
* short-circuits while it is present, so a null-id row is re-dispatched AT MOST
|
|
6907
7844
|
* ONCE per outstanding read-back. Cleared on a SUCCESSFUL dispatch+read-back (the
|
|
6908
7845
|
* row is then tracked in `dispatched`, so `readoptOne`'s early skip prevents
|
|
6909
|
-
* re-entry) OR on a failed/unresolved dispatch (the
|
|
6910
|
-
*
|
|
7846
|
+
* re-entry) OR on a failed/unresolved dispatch (the accepted prompt is handed to
|
|
7847
|
+
* the terminal-report fence and is never sent again).
|
|
6911
7848
|
*/
|
|
6912
7849
|
awaitingReadopt = /* @__PURE__ */ new Set();
|
|
6913
7850
|
/**
|
|
@@ -7030,6 +7967,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7030
7967
|
this.maxActiveSessions = config.maxActiveSessions;
|
|
7031
7968
|
this.watcherStallMs = config.watcherStallMs ?? WATCHER_STALL_MS;
|
|
7032
7969
|
this.wedgeWarningIntervalMs = config.wedgeWarningIntervalMs ?? WEDGE_WARNING_INTERVAL_MS;
|
|
7970
|
+
this.sessionStoreHealth = config.sessionStoreHealth;
|
|
7033
7971
|
}
|
|
7034
7972
|
get isV2() {
|
|
7035
7973
|
return this.openCodeClient.version === "v2";
|
|
@@ -7045,7 +7983,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7045
7983
|
return fetchSessionMessages(this.port, sessionId, this.openCodeClient);
|
|
7046
7984
|
}
|
|
7047
7985
|
async listSessions() {
|
|
7048
|
-
return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient
|
|
7986
|
+
return this.isV2 ? listV2Sessions(this.openCodeClient) : listSessions(this.port, this.openCodeClient, {
|
|
7987
|
+
limit: DRIVER_SESSION_ENUMERATION_LIMIT
|
|
7988
|
+
});
|
|
7049
7989
|
}
|
|
7050
7990
|
async sessionExists(sessionId) {
|
|
7051
7991
|
return this.isV2 ? v2SessionExists(this.openCodeClient, sessionId) : sessionExists(this.port, sessionId, this.openCodeClient);
|
|
@@ -7222,6 +8162,49 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7222
8162
|
}
|
|
7223
8163
|
return ids;
|
|
7224
8164
|
}
|
|
8165
|
+
/** Find an accepted prompt's user row without adopting an older or concurrent turn. */
|
|
8166
|
+
findAwaitingCorrelationId(entry, sessionId, messages) {
|
|
8167
|
+
if (entry.sessionId !== sessionId || entry.knownUserIds === null) return null;
|
|
8168
|
+
const excludeIds = new Set(entry.knownUserIds);
|
|
8169
|
+
for (const inFlight of this.watchers.get(entry.sessionId)?.inFlight.values() ?? []) {
|
|
8170
|
+
excludeIds.add(inFlight.opencodeMessageId);
|
|
8171
|
+
}
|
|
8172
|
+
return findDispatchedUserMessageId(messages, entry.content, excludeIds);
|
|
8173
|
+
}
|
|
8174
|
+
/** Hand an exhausted accepted-but-untrackable prompt to the terminal-report fence. */
|
|
8175
|
+
async handoffUntrackableCorrelation(conversationId, messageId, sessionId, attempts, source) {
|
|
8176
|
+
this.awaitingCorrelation.delete(messageId);
|
|
8177
|
+
this.awaitingReadopt.delete(messageId);
|
|
8178
|
+
this.sessions.delete(conversationId);
|
|
8179
|
+
this.supersede(conversationId, sessionId);
|
|
8180
|
+
const dispatchDescription = source === "readopt" ? "re-dispatched message" : "message";
|
|
8181
|
+
const errorMessage3 = `OpenCode accepted this ${dispatchDescription} once, but its assigned id was never confirmed after ${attempts} attempts to match it up \u2014 the message was deliberately not sent again, so no duplicate turn was created; session ${sessionId.slice(0, 8)} was abandoned; a fresh one is used for further messages.`;
|
|
8182
|
+
this.untrackableAck.set(messageId, errorMessage3);
|
|
8183
|
+
this.untrackableAckSessionIds.set(messageId, null);
|
|
8184
|
+
this.log({
|
|
8185
|
+
level: "error",
|
|
8186
|
+
message: errorMessage3,
|
|
8187
|
+
conversation_id: conversationId,
|
|
8188
|
+
message_id: messageId
|
|
8189
|
+
});
|
|
8190
|
+
const reportError = await this.reportUntrackableAck(
|
|
8191
|
+
conversationId,
|
|
8192
|
+
messageId,
|
|
8193
|
+
null,
|
|
8194
|
+
errorMessage3
|
|
8195
|
+
);
|
|
8196
|
+
if (reportError !== null) {
|
|
8197
|
+
this.log({
|
|
8198
|
+
level: "warn",
|
|
8199
|
+
message: `markFailed PATCH for message ${messageId.slice(0, 8)} (conversation ${conversationId.slice(0, 8)}) failed: ${reportError.message}`,
|
|
8200
|
+
conversation_id: conversationId,
|
|
8201
|
+
message_id: messageId
|
|
8202
|
+
});
|
|
8203
|
+
}
|
|
8204
|
+
if (source === "readopt") {
|
|
8205
|
+
void this.postSignal(conversationId, messageId, "readopt_orphan_unsent");
|
|
8206
|
+
}
|
|
8207
|
+
}
|
|
7225
8208
|
/**
|
|
7226
8209
|
* File-pull work, for `run.ts`'s idle accounting (#559).
|
|
7227
8210
|
*
|
|
@@ -7401,6 +8384,56 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7401
8384
|
}
|
|
7402
8385
|
break;
|
|
7403
8386
|
}
|
|
8387
|
+
const correlation = this.awaitingCorrelation.get(message.id);
|
|
8388
|
+
if (correlation) {
|
|
8389
|
+
correlation.attempts += 1;
|
|
8390
|
+
let correlatedId = null;
|
|
8391
|
+
if (correlation.knownUserIds !== null && correlation.sessionId === sessionId) {
|
|
8392
|
+
const polled = await this.pollSessionMessagesForRedrive(
|
|
8393
|
+
conv,
|
|
8394
|
+
message,
|
|
8395
|
+
correlation.sessionId
|
|
8396
|
+
);
|
|
8397
|
+
if (polled.ok)
|
|
8398
|
+
correlatedId = this.findAwaitingCorrelationId(correlation, sessionId, polled.messages);
|
|
8399
|
+
} else if (correlation.knownUserIds === null) {
|
|
8400
|
+
this.log({
|
|
8401
|
+
level: "debug",
|
|
8402
|
+
message: `Message ${message.id.slice(0, 8)} cannot be correlated because its pre-dispatch session list was unreadable`,
|
|
8403
|
+
conversation_id: conv.id,
|
|
8404
|
+
message_id: message.id
|
|
8405
|
+
});
|
|
8406
|
+
}
|
|
8407
|
+
if (correlatedId !== null) {
|
|
8408
|
+
this.awaitingCorrelation.delete(message.id);
|
|
8409
|
+
this.dispatchNotStartedSignalled.delete(message.id);
|
|
8410
|
+
this.releasedOpencodeIds.delete(message.id);
|
|
8411
|
+
this.dispatched.add(message.id);
|
|
8412
|
+
this.registerInFlight(conv, correlation.sessionId, message, correlatedId);
|
|
8413
|
+
dispatched += 1;
|
|
8414
|
+
this.log({
|
|
8415
|
+
level: "info",
|
|
8416
|
+
message: `Matched accepted message ${message.id.slice(0, 8)} to OpenCode turn ${correlatedId.slice(0, 8)} without sending it again`,
|
|
8417
|
+
conversation_id: conv.id,
|
|
8418
|
+
message_id: message.id
|
|
8419
|
+
});
|
|
8420
|
+
void this.postSignal(conv.id, message.id, "ack_correlated", {
|
|
8421
|
+
opencode_message_id: correlatedId
|
|
8422
|
+
});
|
|
8423
|
+
continue;
|
|
8424
|
+
}
|
|
8425
|
+
if (correlation.attempts >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
|
|
8426
|
+
await this.handoffUntrackableCorrelation(
|
|
8427
|
+
conv.id,
|
|
8428
|
+
message.id,
|
|
8429
|
+
correlation.sessionId,
|
|
8430
|
+
correlation.attempts,
|
|
8431
|
+
"fresh"
|
|
8432
|
+
);
|
|
8433
|
+
break;
|
|
8434
|
+
}
|
|
8435
|
+
break;
|
|
8436
|
+
}
|
|
7404
8437
|
const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
|
|
7405
8438
|
if (effectiveOpencodeMessageId) {
|
|
7406
8439
|
const outcome = await this.resolveRedrive(
|
|
@@ -7422,6 +8455,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7422
8455
|
model: message.opencode_model ?? void 0
|
|
7423
8456
|
};
|
|
7424
8457
|
let opencodeMessageId;
|
|
8458
|
+
let preDispatchUserIds = null;
|
|
7425
8459
|
try {
|
|
7426
8460
|
this.log({
|
|
7427
8461
|
level: "info",
|
|
@@ -7451,7 +8485,20 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7451
8485
|
message.content,
|
|
7452
8486
|
options,
|
|
7453
8487
|
sendAttachments,
|
|
7454
|
-
this.openCodeClient
|
|
8488
|
+
this.openCodeClient,
|
|
8489
|
+
{
|
|
8490
|
+
onKnownUserIds: (ids) => {
|
|
8491
|
+
preDispatchUserIds = ids;
|
|
8492
|
+
},
|
|
8493
|
+
onKnownUserIdsError: (error2) => {
|
|
8494
|
+
this.log({
|
|
8495
|
+
level: "warn",
|
|
8496
|
+
message: `Pre-dispatch snapshot callback failed for message ${message.id.slice(0, 8)}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
8497
|
+
conversation_id: conv.id,
|
|
8498
|
+
message_id: message.id
|
|
8499
|
+
});
|
|
8500
|
+
}
|
|
8501
|
+
}
|
|
7455
8502
|
)
|
|
7456
8503
|
);
|
|
7457
8504
|
} catch (err) {
|
|
@@ -7459,6 +8506,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7459
8506
|
if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
|
|
7460
8507
|
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
7461
8508
|
this.untrackableAck.set(message.id, errorMessage4);
|
|
8509
|
+
this.untrackableAckSessionIds.set(message.id, null);
|
|
7462
8510
|
this.log({
|
|
7463
8511
|
level: "error",
|
|
7464
8512
|
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
|
|
@@ -7534,39 +8582,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7534
8582
|
if (this.isV2) {
|
|
7535
8583
|
throw new Error("V2 prompt dispatch completed without an acknowledged message id");
|
|
7536
8584
|
}
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
message_id: message.id
|
|
7544
|
-
});
|
|
7545
|
-
this.signalDispatchNotStarted(conv, message, "readback_unconfirmed");
|
|
7546
|
-
continue;
|
|
7547
|
-
}
|
|
7548
|
-
this.unconfirmedDispatchFailures.delete(message.id);
|
|
7549
|
-
this.sessions.delete(conv.id);
|
|
7550
|
-
this.supersede(conv.id, sessionId);
|
|
7551
|
-
const errorMessage3 = `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
|
|
8585
|
+
this.awaitingCorrelation.set(message.id, {
|
|
8586
|
+
sessionId,
|
|
8587
|
+
content: message.content,
|
|
8588
|
+
attempts: 0,
|
|
8589
|
+
knownUserIds: preDispatchUserIds
|
|
8590
|
+
});
|
|
7552
8591
|
this.log({
|
|
7553
|
-
level: "
|
|
7554
|
-
message:
|
|
8592
|
+
level: "warn",
|
|
8593
|
+
message: `OpenCode accepted message ${message.id.slice(0, 8)}, but its assigned id could not be read back \u2014 the message will not be sent again while the runner matches it to the turn already running`,
|
|
7555
8594
|
conversation_id: conv.id,
|
|
7556
8595
|
message_id: message.id
|
|
7557
8596
|
});
|
|
7558
|
-
|
|
7559
|
-
this.log({
|
|
7560
|
-
level: "warn",
|
|
7561
|
-
message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
7562
|
-
conversation_id: conv.id,
|
|
7563
|
-
message_id: message.id
|
|
7564
|
-
});
|
|
7565
|
-
this.signalDispatchNotStarted(conv, message, "abandon_unreported");
|
|
7566
|
-
});
|
|
8597
|
+
this.signalDispatchNotStarted(conv, message, "readback_unconfirmed");
|
|
7567
8598
|
break;
|
|
7568
8599
|
}
|
|
7569
|
-
this.unconfirmedDispatchFailures.delete(message.id);
|
|
7570
8600
|
this.dispatchNotStartedSignalled.delete(message.id);
|
|
7571
8601
|
this.releasedOpencodeIds.delete(message.id);
|
|
7572
8602
|
this.dispatched.add(message.id);
|
|
@@ -8171,23 +9201,6 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8171
9201
|
this.redrivePollFailures.set(messageId, { sessionId, signature, count: 1 });
|
|
8172
9202
|
return 1;
|
|
8173
9203
|
}
|
|
8174
|
-
/**
|
|
8175
|
-
* Record one UNCONFIRMED-dispatch outcome (a `pending` row with no stored
|
|
8176
|
-
* `opencode_message_id` whose `sendPromptAsync` returned `null`) toward the
|
|
8177
|
-
* bound in `processConversation`'s dispatch loop, and return the resulting
|
|
8178
|
-
* count. Mirrors `recordRedrivePollFailure`'s session-scoping: a session
|
|
8179
|
-
* change starts a fresh streak at `1` rather than inheriting the old one's
|
|
8180
|
-
* count, since a new session is a genuinely different attempt.
|
|
8181
|
-
*/
|
|
8182
|
-
recordUnconfirmedDispatch(messageId, sessionId) {
|
|
8183
|
-
const existing = this.unconfirmedDispatchFailures.get(messageId);
|
|
8184
|
-
if (existing && existing.sessionId === sessionId) {
|
|
8185
|
-
existing.count += 1;
|
|
8186
|
-
return existing.count;
|
|
8187
|
-
}
|
|
8188
|
-
this.unconfirmedDispatchFailures.set(messageId, { sessionId, count: 1 });
|
|
8189
|
-
return 1;
|
|
8190
|
-
}
|
|
8191
9204
|
/** Record one queued give-up toward the never-started bound. */
|
|
8192
9205
|
recordNeverStartedAttempt(messageId, sessionId) {
|
|
8193
9206
|
const existing = this.neverStartedAttempts.get(messageId);
|
|
@@ -8499,8 +9512,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8499
9512
|
b2LastDescendantCheckMs: 0,
|
|
8500
9513
|
b2RootOngoingHeldLogged: false,
|
|
8501
9514
|
b2AbandonedSignalled: false,
|
|
9515
|
+
b2AbandonedWithoutAnswer: false,
|
|
8502
9516
|
ambiguousPinnedSinceMs: 0,
|
|
8503
9517
|
ambiguousResolved: false,
|
|
9518
|
+
sessionStoreFailurePending: false,
|
|
8504
9519
|
neverStartedResolved: false
|
|
8505
9520
|
});
|
|
8506
9521
|
const buffered = this.bufferedSessionErrors.get(sessionId);
|
|
@@ -8522,6 +9537,28 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8522
9537
|
this.bufferedSessionErrors.delete(oldest);
|
|
8523
9538
|
}
|
|
8524
9539
|
}
|
|
9540
|
+
retainSessionError(event) {
|
|
9541
|
+
this.retainedSessionErrors.delete(event.sessionId);
|
|
9542
|
+
this.retainedSessionErrors.set(event.sessionId, {
|
|
9543
|
+
reason: event.reason,
|
|
9544
|
+
observedAt: this.now()
|
|
9545
|
+
});
|
|
9546
|
+
while (this.retainedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
|
|
9547
|
+
const oldest = this.retainedSessionErrors.keys().next().value;
|
|
9548
|
+
if (typeof oldest !== "string") break;
|
|
9549
|
+
this.retainedSessionErrors.delete(oldest);
|
|
9550
|
+
}
|
|
9551
|
+
}
|
|
9552
|
+
readRetainedSessionError(sessionId) {
|
|
9553
|
+
const retained = this.retainedSessionErrors.get(sessionId);
|
|
9554
|
+
if (!retained) return null;
|
|
9555
|
+
if (this.now() - retained.observedAt >= SESSION_ERROR_RETENTION_TTL_MS) {
|
|
9556
|
+
this.retainedSessionErrors.delete(sessionId);
|
|
9557
|
+
return null;
|
|
9558
|
+
}
|
|
9559
|
+
this.retainedSessionErrors.delete(sessionId);
|
|
9560
|
+
return retained.reason;
|
|
9561
|
+
}
|
|
8525
9562
|
/**
|
|
8526
9563
|
* Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
|
|
8527
9564
|
* EQUAL (#1618) so a watcher whose loop has not started ticking yet is never
|
|
@@ -8617,8 +9654,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8617
9654
|
b2LastDescendantCheckMs: 0,
|
|
8618
9655
|
b2RootOngoingHeldLogged: false,
|
|
8619
9656
|
b2AbandonedSignalled: false,
|
|
9657
|
+
b2AbandonedWithoutAnswer: false,
|
|
8620
9658
|
ambiguousPinnedSinceMs: 0,
|
|
8621
9659
|
ambiguousResolved: false,
|
|
9660
|
+
sessionStoreFailurePending: false,
|
|
8622
9661
|
neverStartedResolved: false
|
|
8623
9662
|
});
|
|
8624
9663
|
}
|
|
@@ -8846,9 +9885,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8846
9885
|
return;
|
|
8847
9886
|
}
|
|
8848
9887
|
if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
|
|
9888
|
+
this.retainSessionError(event);
|
|
8849
9889
|
this.log({
|
|
8850
|
-
level: "
|
|
8851
|
-
message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
|
|
9890
|
+
level: "warn",
|
|
9891
|
+
message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 retaining the session error and deferring to transcript polling: ${event.reason}`,
|
|
8852
9892
|
conversation_id: watcher.conv.id
|
|
8853
9893
|
});
|
|
8854
9894
|
return;
|
|
@@ -8878,9 +9918,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8878
9918
|
const messages = await this.getSessionMessages(event.sessionId);
|
|
8879
9919
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
8880
9920
|
if (state !== "queued") {
|
|
9921
|
+
this.retainSessionError(event);
|
|
8881
9922
|
this.log({
|
|
8882
|
-
level: "
|
|
8883
|
-
message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state};
|
|
9923
|
+
level: "warn",
|
|
9924
|
+
message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} in session ${event.sessionId.slice(0, 8)} observed state ${state}; retaining the reason for transcript polling: ${event.reason}`,
|
|
8884
9925
|
conversation_id: watcher.conv.id,
|
|
8885
9926
|
message_id: inFlight.evidentMessageId
|
|
8886
9927
|
});
|
|
@@ -9032,6 +10073,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9032
10073
|
if (state === "running" || state === "done" || state === "failed") {
|
|
9033
10074
|
this.neverStartedAttempts.delete(id);
|
|
9034
10075
|
}
|
|
10076
|
+
if (inFlight.sessionStoreFailurePending) {
|
|
10077
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
10078
|
+
return;
|
|
10079
|
+
}
|
|
9035
10080
|
if (inFlight.neverStartedResolved) {
|
|
9036
10081
|
if (state === "running" || state === "done" || state === "failed") {
|
|
9037
10082
|
inFlight.neverStartedResolved = false;
|
|
@@ -9105,7 +10150,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9105
10150
|
if (state === "failed") {
|
|
9106
10151
|
this.anchorDeliveryDeadline(inFlight);
|
|
9107
10152
|
if (!inFlight.done) {
|
|
9108
|
-
const
|
|
10153
|
+
const messageErrorText = messageError(messages, inFlight.opencodeMessageId);
|
|
10154
|
+
const retainedReason = this.readRetainedSessionError(sessionId);
|
|
10155
|
+
const retainedError = retainedReason === null ? null : `${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}`;
|
|
10156
|
+
const observedCause = this.sessionStoreHealth?.readCause(sessionId);
|
|
10157
|
+
const causeError = observedCause === null || observedCause === void 0 ? null : `${SESSION_ERROR_OBSERVED_CAUSE}${observedCause}`;
|
|
10158
|
+
const storeError = this.sessionStoreHealth?.isUnwritable() ? SESSION_STORE_UNWRITABLE_ERROR : null;
|
|
10159
|
+
const runnerDetails = [retainedError, causeError, storeError].filter(
|
|
10160
|
+
(detail) => detail !== null
|
|
10161
|
+
);
|
|
10162
|
+
const error2 = messageErrorText === null ? runnerDetails.length === 0 ? void 0 : runnerDetails.join(" ") : [messageErrorText, ...runnerDetails].join(" ");
|
|
9109
10163
|
this.log({
|
|
9110
10164
|
level: "error",
|
|
9111
10165
|
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} errored \u2014 marking failed: ${error2 ?? "(no error text)"}`,
|
|
@@ -9191,10 +10245,33 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9191
10245
|
inFlight.b2LastDescendantCheckMs = 0;
|
|
9192
10246
|
inFlight.b2RootOngoingHeldLogged = false;
|
|
9193
10247
|
inFlight.b2AbandonedSignalled = false;
|
|
10248
|
+
inFlight.b2AbandonedWithoutAnswer = false;
|
|
9194
10249
|
}
|
|
9195
10250
|
} else {
|
|
9196
10251
|
if (inFlight.b2AbandonedSignalled) {
|
|
9197
|
-
|
|
10252
|
+
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
10253
|
+
const finish = reply?.info?.finish ?? reply?.finish;
|
|
10254
|
+
if (inFlight.b2AbandonedWithoutAnswer) {
|
|
10255
|
+
await this.settleMessageAbandonedWithoutAnswer(
|
|
10256
|
+
sessionId,
|
|
10257
|
+
watcher,
|
|
10258
|
+
inFlight,
|
|
10259
|
+
messages,
|
|
10260
|
+
finish
|
|
10261
|
+
);
|
|
10262
|
+
} else {
|
|
10263
|
+
await this.settleMessageDone(
|
|
10264
|
+
sessionId,
|
|
10265
|
+
watcher,
|
|
10266
|
+
inFlight,
|
|
10267
|
+
messages,
|
|
10268
|
+
composeAbandonmentNote(
|
|
10269
|
+
finish,
|
|
10270
|
+
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
10271
|
+
"with-answer"
|
|
10272
|
+
)
|
|
10273
|
+
);
|
|
10274
|
+
}
|
|
9198
10275
|
return;
|
|
9199
10276
|
}
|
|
9200
10277
|
if (inFlight.b2PinnedSinceMs === 0) inFlight.b2PinnedSinceMs = this.now();
|
|
@@ -9211,22 +10288,62 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9211
10288
|
descendantOngoing,
|
|
9212
10289
|
rootOngoing
|
|
9213
10290
|
})) {
|
|
10291
|
+
if (this.sessionStoreHealth?.isUnwritable()) {
|
|
10292
|
+
inFlight.sessionStoreFailurePending = true;
|
|
10293
|
+
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
10294
|
+
watched_for_ms: pinnedForMs,
|
|
10295
|
+
run_state: "failed",
|
|
10296
|
+
opencode_message_id: inFlight.opencodeMessageId
|
|
10297
|
+
});
|
|
10298
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
10299
|
+
return;
|
|
10300
|
+
}
|
|
9214
10301
|
inFlight.b2AbandonedSignalled = true;
|
|
10302
|
+
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
10303
|
+
const finish = reply?.info?.finish ?? reply?.finish;
|
|
10304
|
+
if (extractReplyTextForMessage(messages, inFlight.opencodeMessageId) === null) {
|
|
10305
|
+
inFlight.b2AbandonedWithoutAnswer = true;
|
|
10306
|
+
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
10307
|
+
watched_for_ms: pinnedForMs,
|
|
10308
|
+
run_state: "failed",
|
|
10309
|
+
finish,
|
|
10310
|
+
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
10311
|
+
...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
|
|
10312
|
+
opencode_message_id: inFlight.opencodeMessageId
|
|
10313
|
+
});
|
|
10314
|
+
await this.settleMessageAbandonedWithoutAnswer(
|
|
10315
|
+
sessionId,
|
|
10316
|
+
watcher,
|
|
10317
|
+
inFlight,
|
|
10318
|
+
messages,
|
|
10319
|
+
finish
|
|
10320
|
+
);
|
|
10321
|
+
return;
|
|
10322
|
+
}
|
|
9215
10323
|
this.log({
|
|
9216
10324
|
level: "warn",
|
|
9217
10325
|
message: `Message ${id.slice(0, 8)} b2-pinned for ${Math.round(pinnedForMs / 1e3)}s with no ongoing descendant sub-agent session (status-map confirmed) \u2014 treating the delegated/tool turn as abandoned, resolving done`,
|
|
9218
10326
|
conversation_id: conv.id,
|
|
9219
10327
|
message_id: id
|
|
9220
10328
|
});
|
|
9221
|
-
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
9222
10329
|
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
9223
10330
|
watched_for_ms: pinnedForMs,
|
|
9224
|
-
finish
|
|
10331
|
+
finish,
|
|
9225
10332
|
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
9226
10333
|
...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
|
|
9227
10334
|
opencode_message_id: inFlight.opencodeMessageId
|
|
9228
10335
|
});
|
|
9229
|
-
await this.settleMessageDone(
|
|
10336
|
+
await this.settleMessageDone(
|
|
10337
|
+
sessionId,
|
|
10338
|
+
watcher,
|
|
10339
|
+
inFlight,
|
|
10340
|
+
messages,
|
|
10341
|
+
composeAbandonmentNote(
|
|
10342
|
+
finish,
|
|
10343
|
+
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
10344
|
+
"with-answer"
|
|
10345
|
+
)
|
|
10346
|
+
);
|
|
9230
10347
|
return;
|
|
9231
10348
|
}
|
|
9232
10349
|
if (rootOngoing === true && !inFlight.b2RootOngoingHeldLogged) {
|
|
@@ -9269,6 +10386,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9269
10386
|
maxPinnedMs: AMBIGUOUS_FINISH_MAX_PINNED_MS,
|
|
9270
10387
|
sessionOngoing: ongoing
|
|
9271
10388
|
})) {
|
|
10389
|
+
if (this.sessionStoreHealth?.isUnwritable()) {
|
|
10390
|
+
inFlight.sessionStoreFailurePending = true;
|
|
10391
|
+
void this.postSignal(conv.id, id, "ambiguous_finish_resolved", {
|
|
10392
|
+
watched_for_ms: pinnedForMs,
|
|
10393
|
+
run_state: "failed",
|
|
10394
|
+
opencode_message_id: inFlight.opencodeMessageId
|
|
10395
|
+
});
|
|
10396
|
+
await this.settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight);
|
|
10397
|
+
return;
|
|
10398
|
+
}
|
|
9272
10399
|
inFlight.ambiguousResolved = true;
|
|
9273
10400
|
this.log({
|
|
9274
10401
|
level: "warn",
|
|
@@ -9416,15 +10543,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9416
10543
|
}
|
|
9417
10544
|
}
|
|
9418
10545
|
/**
|
|
9419
|
-
* Settle a message whose run-state has resolved `'done'
|
|
9420
|
-
*
|
|
9421
|
-
*
|
|
9422
|
-
* b2-abandonment resolution) can reach the exact same completion behavior
|
|
9423
|
-
* (delivery-deadline anchoring, title resolution, usage extraction, and
|
|
9424
|
-
* `markDone`'s auth/terminal/transient-retry discipline) without duplicating it
|
|
9425
|
-
* and risking the two copies silently drifting apart.
|
|
10546
|
+
* Settle a message whose run-state has resolved `'done'`. Normal completion and
|
|
10547
|
+
* confirmed b2 abandonment share the same delivery, usage, and terminal handling.
|
|
10548
|
+
* Normal completions omit the optional terminal diagnostic.
|
|
9426
10549
|
*/
|
|
9427
|
-
async settleMessageDone(sessionId, watcher, inFlight, messages) {
|
|
10550
|
+
async settleMessageDone(sessionId, watcher, inFlight, messages, note) {
|
|
9428
10551
|
const conv = watcher.conv;
|
|
9429
10552
|
this.anchorDeliveryDeadline(inFlight);
|
|
9430
10553
|
if (!inFlight.done) {
|
|
@@ -9451,7 +10574,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9451
10574
|
title,
|
|
9452
10575
|
usage,
|
|
9453
10576
|
usageAgentName,
|
|
9454
|
-
subagentInvocations
|
|
10577
|
+
subagentInvocations,
|
|
10578
|
+
note
|
|
9455
10579
|
);
|
|
9456
10580
|
} catch (err) {
|
|
9457
10581
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -9493,6 +10617,78 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9493
10617
|
}
|
|
9494
10618
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9495
10619
|
}
|
|
10620
|
+
/** Report a confirmed b2 abandonment that produced no answer text. */
|
|
10621
|
+
async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages, finish) {
|
|
10622
|
+
const conv = watcher.conv;
|
|
10623
|
+
const error2 = composeAbandonmentNote(
|
|
10624
|
+
finish,
|
|
10625
|
+
describeAbandonedReplyShape(messages, inFlight.opencodeMessageId),
|
|
10626
|
+
"without-answer"
|
|
10627
|
+
);
|
|
10628
|
+
this.anchorDeliveryDeadline(inFlight);
|
|
10629
|
+
this.log({
|
|
10630
|
+
level: "warn",
|
|
10631
|
+
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} was abandoned without an answer \u2014 ${error2}`,
|
|
10632
|
+
conversation_id: conv.id,
|
|
10633
|
+
message_id: inFlight.evidentMessageId
|
|
10634
|
+
});
|
|
10635
|
+
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
10636
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
10637
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
10638
|
+
messages,
|
|
10639
|
+
inFlight.opencodeMessageId,
|
|
10640
|
+
inFlight.evidentMessageId
|
|
10641
|
+
);
|
|
10642
|
+
try {
|
|
10643
|
+
await this.markFailed(
|
|
10644
|
+
conv.id,
|
|
10645
|
+
inFlight.evidentMessageId,
|
|
10646
|
+
sessionId,
|
|
10647
|
+
error2,
|
|
10648
|
+
usage,
|
|
10649
|
+
void 0,
|
|
10650
|
+
usageAgentName,
|
|
10651
|
+
subagentInvocations
|
|
10652
|
+
);
|
|
10653
|
+
} catch (err) {
|
|
10654
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
10655
|
+
if (err instanceof ChannelTerminalError) {
|
|
10656
|
+
this.log({
|
|
10657
|
+
level: "warn",
|
|
10658
|
+
message: `Failed to report abandoned no-answer message ${inFlight.evidentMessageId.slice(0, 8)} as failed (terminal HTTP ${err.status}) \u2014 leaving it for the cron safety net: ${err.message}`,
|
|
10659
|
+
conversation_id: conv.id,
|
|
10660
|
+
message_id: inFlight.evidentMessageId
|
|
10661
|
+
});
|
|
10662
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
10663
|
+
return;
|
|
10664
|
+
}
|
|
10665
|
+
if (this.now() >= inFlight.deadline) {
|
|
10666
|
+
this.log({
|
|
10667
|
+
level: "warn",
|
|
10668
|
+
message: `Failed to report abandoned no-answer message ${inFlight.evidentMessageId.slice(0, 8)} as failed within the delivery window \u2014 leaving it for the cron safety net: ${err instanceof Error ? err.message : String(err)}`,
|
|
10669
|
+
conversation_id: conv.id,
|
|
10670
|
+
message_id: inFlight.evidentMessageId
|
|
10671
|
+
});
|
|
10672
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
10673
|
+
return;
|
|
10674
|
+
}
|
|
10675
|
+
this.log({
|
|
10676
|
+
level: "warn",
|
|
10677
|
+
message: `Failed to report abandoned no-answer message ${inFlight.evidentMessageId.slice(0, 8)} as failed (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
10678
|
+
conversation_id: conv.id,
|
|
10679
|
+
message_id: inFlight.evidentMessageId
|
|
10680
|
+
});
|
|
10681
|
+
return;
|
|
10682
|
+
}
|
|
10683
|
+
inFlight.done = true;
|
|
10684
|
+
await this.reportSubagentAuthFailures(
|
|
10685
|
+
watcher.conv.id,
|
|
10686
|
+
inFlight.opencodeMessageId,
|
|
10687
|
+
inFlight.evidentMessageId,
|
|
10688
|
+
messages
|
|
10689
|
+
);
|
|
10690
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
10691
|
+
}
|
|
9496
10692
|
/**
|
|
9497
10693
|
* Report a queued message as failed after the never-started bound. The accepted
|
|
9498
10694
|
* OpenCode copy is not cancelled, so the local fence remains on unsuccessful
|
|
@@ -9548,6 +10744,66 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9548
10744
|
this.neverStartedAttempts.delete(inFlight.evidentMessageId);
|
|
9549
10745
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9550
10746
|
}
|
|
10747
|
+
/**
|
|
10748
|
+
* Report a turn as failed when the session database was observed unwritable.
|
|
10749
|
+
* The queue row is retained as failed, while the OpenCode copy stays fenced so
|
|
10750
|
+
* a later drain cannot blindly prompt the same turn again.
|
|
10751
|
+
*/
|
|
10752
|
+
async settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight) {
|
|
10753
|
+
const conv = watcher.conv;
|
|
10754
|
+
const error2 = SESSION_STORE_UNWRITABLE_ERROR;
|
|
10755
|
+
this.log({
|
|
10756
|
+
level: "error",
|
|
10757
|
+
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} was not resolved as done: ${error2}`,
|
|
10758
|
+
conversation_id: conv.id,
|
|
10759
|
+
message_id: inFlight.evidentMessageId
|
|
10760
|
+
});
|
|
10761
|
+
this.anchorDeliveryDeadline(inFlight);
|
|
10762
|
+
try {
|
|
10763
|
+
await this.markFailed(conv.id, inFlight.evidentMessageId, sessionId, error2);
|
|
10764
|
+
} catch (err) {
|
|
10765
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
10766
|
+
if (err instanceof ChannelTerminalError) {
|
|
10767
|
+
this.log({
|
|
10768
|
+
level: "warn",
|
|
10769
|
+
message: `Failed to report session-store failure for message ${inFlight.evidentMessageId.slice(0, 8)} as failed (terminal HTTP ${err.status}) \u2014 keeping its OpenCode copy fenced: ${err.message}`,
|
|
10770
|
+
conversation_id: conv.id,
|
|
10771
|
+
message_id: inFlight.evidentMessageId
|
|
10772
|
+
});
|
|
10773
|
+
this.recordReleasedOpencodeId(
|
|
10774
|
+
inFlight.evidentMessageId,
|
|
10775
|
+
sessionId,
|
|
10776
|
+
inFlight.opencodeMessageId
|
|
10777
|
+
);
|
|
10778
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
10779
|
+
return;
|
|
10780
|
+
}
|
|
10781
|
+
if (this.now() >= inFlight.deadline) {
|
|
10782
|
+
this.log({
|
|
10783
|
+
level: "warn",
|
|
10784
|
+
message: `Failed to report session-store failure for message ${inFlight.evidentMessageId.slice(0, 8)} as failed within the delivery window \u2014 keeping its OpenCode copy fenced: ${err instanceof Error ? err.message : String(err)}`,
|
|
10785
|
+
conversation_id: conv.id,
|
|
10786
|
+
message_id: inFlight.evidentMessageId
|
|
10787
|
+
});
|
|
10788
|
+
this.recordReleasedOpencodeId(
|
|
10789
|
+
inFlight.evidentMessageId,
|
|
10790
|
+
sessionId,
|
|
10791
|
+
inFlight.opencodeMessageId
|
|
10792
|
+
);
|
|
10793
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
10794
|
+
return;
|
|
10795
|
+
}
|
|
10796
|
+
this.log({
|
|
10797
|
+
level: "warn",
|
|
10798
|
+
message: `Failed to report session-store failure for message ${inFlight.evidentMessageId.slice(0, 8)} as failed (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
10799
|
+
conversation_id: conv.id,
|
|
10800
|
+
message_id: inFlight.evidentMessageId
|
|
10801
|
+
});
|
|
10802
|
+
return;
|
|
10803
|
+
}
|
|
10804
|
+
inFlight.done = true;
|
|
10805
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
10806
|
+
}
|
|
9551
10807
|
// Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
|
|
9552
10808
|
/**
|
|
9553
10809
|
* Re-adopt this agent's `processing` messages on drain (ADR-0046 Decision §1).
|
|
@@ -9586,11 +10842,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9586
10842
|
}
|
|
9587
10843
|
}
|
|
9588
10844
|
}
|
|
9589
|
-
if (this.untrackableAck.size > 0 || this.neverStartedAttempts.size > 0) {
|
|
10845
|
+
if (this.untrackableAck.size > 0 || this.neverStartedAttempts.size > 0 || this.awaitingCorrelation.size > 0) {
|
|
9590
10846
|
const stillProcessing = new Set(rows.map((r) => r.id));
|
|
10847
|
+
for (const id of this.awaitingCorrelation.keys()) {
|
|
10848
|
+
if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
|
|
10849
|
+
this.awaitingCorrelation.delete(id);
|
|
10850
|
+
this.log({
|
|
10851
|
+
level: "debug",
|
|
10852
|
+
message: `Re-adopt: message ${id.slice(0, 8)} left processing and pending \u2014 cleared awaiting-correlation fence`,
|
|
10853
|
+
message_id: id
|
|
10854
|
+
});
|
|
10855
|
+
}
|
|
10856
|
+
}
|
|
9591
10857
|
for (const id of [...this.untrackableAck.keys()]) {
|
|
9592
10858
|
if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
|
|
9593
10859
|
this.untrackableAck.delete(id);
|
|
10860
|
+
this.untrackableAckSessionIds.delete(id);
|
|
9594
10861
|
this.untrackableAckSignalled.delete(id);
|
|
9595
10862
|
this.untrackableAckWarned.delete(id);
|
|
9596
10863
|
this.log({
|
|
@@ -9630,7 +10897,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9630
10897
|
bySession.set(row.opencode_session_id, list);
|
|
9631
10898
|
}
|
|
9632
10899
|
for (const [sessionId, sessionRows] of bySession) {
|
|
9633
|
-
let messages;
|
|
10900
|
+
let messages = null;
|
|
9634
10901
|
try {
|
|
9635
10902
|
if (this.isV2) {
|
|
9636
10903
|
const snapshot = await this.getSessionMessages(sessionId);
|
|
@@ -9639,9 +10906,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9639
10906
|
level: "warn",
|
|
9640
10907
|
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned an unreadable message snapshot \u2014 skipping this session this tick`
|
|
9641
10908
|
});
|
|
9642
|
-
|
|
10909
|
+
} else {
|
|
10910
|
+
messages = snapshot;
|
|
9643
10911
|
}
|
|
9644
|
-
messages = snapshot;
|
|
9645
10912
|
} else {
|
|
9646
10913
|
const polled = await pollSessionMessagesForRedrive(
|
|
9647
10914
|
this.port,
|
|
@@ -9654,15 +10921,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9654
10921
|
level: "warn",
|
|
9655
10922
|
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned ${polled.malformed ? "a non-array message body" : `HTTP ${polled.status ?? "a network error"}${normalized ? `: ${normalized}` : ""}`} \u2014 skipping this session this tick`
|
|
9656
10923
|
});
|
|
9657
|
-
|
|
10924
|
+
} else {
|
|
10925
|
+
messages = polled.messages;
|
|
9658
10926
|
}
|
|
9659
|
-
messages = polled.messages;
|
|
9660
10927
|
}
|
|
9661
10928
|
} catch (err) {
|
|
9662
10929
|
this.log({
|
|
9663
10930
|
level: "warn",
|
|
9664
10931
|
message: `Re-adopt: failed to poll session ${sessionId.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`
|
|
9665
10932
|
});
|
|
10933
|
+
}
|
|
10934
|
+
if (messages === null) {
|
|
10935
|
+
for (const row of sessionRows) {
|
|
10936
|
+
if (this.awaitingCorrelation.has(row.id)) {
|
|
10937
|
+
await this.advanceReadoptCorrelation(sessionId, row, null);
|
|
10938
|
+
}
|
|
10939
|
+
}
|
|
9666
10940
|
continue;
|
|
9667
10941
|
}
|
|
9668
10942
|
const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
|
|
@@ -9672,6 +10946,59 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9672
10946
|
}
|
|
9673
10947
|
}
|
|
9674
10948
|
}
|
|
10949
|
+
/** Advance a re-adopted accepted prompt through the shared correlation fence. */
|
|
10950
|
+
async advanceReadoptCorrelation(sessionId, row, messages) {
|
|
10951
|
+
const correlation = this.awaitingCorrelation.get(row.id);
|
|
10952
|
+
if (!correlation) return false;
|
|
10953
|
+
correlation.attempts += 1;
|
|
10954
|
+
const correlatedId = messages === null ? null : this.findAwaitingCorrelationId(correlation, sessionId, messages);
|
|
10955
|
+
if (correlatedId !== null) {
|
|
10956
|
+
this.awaitingCorrelation.delete(row.id);
|
|
10957
|
+
const conv = this.convForRow(sessionId, row);
|
|
10958
|
+
const message = this.queuedMessageForRow(row);
|
|
10959
|
+
const processedAtMs = this.processedAtMs(row);
|
|
10960
|
+
this.registerReadopted(conv, sessionId, message, correlatedId, processedAtMs, processedAtMs);
|
|
10961
|
+
this.dispatched.add(row.id);
|
|
10962
|
+
this.readopted.add(row.id);
|
|
10963
|
+
this.ensureWatcherRunning(sessionId);
|
|
10964
|
+
this.log({
|
|
10965
|
+
level: "info",
|
|
10966
|
+
message: `Re-adopt: matched accepted message ${row.id.slice(0, 8)} to OpenCode turn ${correlatedId.slice(0, 8)} without sending it again`,
|
|
10967
|
+
conversation_id: row.conversation_id,
|
|
10968
|
+
message_id: row.id
|
|
10969
|
+
});
|
|
10970
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_redispatched");
|
|
10971
|
+
void this.postSignal(row.conversation_id, row.id, "ack_correlated", {
|
|
10972
|
+
opencode_message_id: correlatedId
|
|
10973
|
+
});
|
|
10974
|
+
return true;
|
|
10975
|
+
}
|
|
10976
|
+
if (correlation.knownUserIds === null) {
|
|
10977
|
+
this.log({
|
|
10978
|
+
level: "debug",
|
|
10979
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} cannot be correlated because its pre-dispatch session list was unreadable`,
|
|
10980
|
+
conversation_id: row.conversation_id,
|
|
10981
|
+
message_id: row.id
|
|
10982
|
+
});
|
|
10983
|
+
} else if (correlation.sessionId !== sessionId) {
|
|
10984
|
+
this.log({
|
|
10985
|
+
level: "warn",
|
|
10986
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} moved from original session ${correlation.sessionId.slice(0, 8)} to ${sessionId.slice(0, 8)} while awaiting correlation \u2014 refusing to adopt a turn from the rebound session`,
|
|
10987
|
+
conversation_id: row.conversation_id,
|
|
10988
|
+
message_id: row.id
|
|
10989
|
+
});
|
|
10990
|
+
}
|
|
10991
|
+
if (correlation.attempts >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
|
|
10992
|
+
await this.handoffUntrackableCorrelation(
|
|
10993
|
+
row.conversation_id,
|
|
10994
|
+
row.id,
|
|
10995
|
+
correlation.sessionId,
|
|
10996
|
+
correlation.attempts,
|
|
10997
|
+
"readopt"
|
|
10998
|
+
);
|
|
10999
|
+
}
|
|
11000
|
+
return true;
|
|
11001
|
+
}
|
|
9675
11002
|
/**
|
|
9676
11003
|
* Re-adopt ONE `processing` row against the tick's session message snapshot
|
|
9677
11004
|
* (ADR-0046 Decision §1/§2). Idempotent: skips a row already being driven.
|
|
@@ -9679,8 +11006,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9679
11006
|
* Branches on `messageRunState(messages, row.opencode_message_id)` — the
|
|
9680
11007
|
* opencode-assigned user-message id persisted on the first `processing` PATCH
|
|
9681
11008
|
* (#218). A row with a NULL stored id (dispatched but the read-back never landed
|
|
9682
|
-
* before the restart)
|
|
9683
|
-
*
|
|
11009
|
+
* before the restart) is first offered to an awaiting-correlation fence recorded
|
|
11010
|
+
* by this process; without that fence, it has no id to correlate after restart
|
|
11011
|
+
* and is treated as an orphan and re-dispatched (at most once, see
|
|
11012
|
+
* `forceReadoptRun`):
|
|
9684
11013
|
* - `done` → `markDone` now (guarded like the watcher's done branch);
|
|
9685
11014
|
* - `failed` → `markFailed` with the surfaced error (issue #182), so an
|
|
9686
11015
|
* errored turn is reported failed on restart, NOT re-dispatched —
|
|
@@ -9689,7 +11018,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9689
11018
|
* re-dispatched instead (issue #1310, see the branch below);
|
|
9690
11019
|
* - `running`/`queued` → re-attach a watcher via `registerReadopted` (no re-dispatch),
|
|
9691
11020
|
* tracking the stored id so the reply correlates by it;
|
|
9692
|
-
* - `unknown`/null id →
|
|
11021
|
+
* - `unknown`/null id → correlate through an awaiting fence when present;
|
|
11022
|
+
* otherwise re-dispatch (opencode assigns a fresh id) + attach a watcher.
|
|
9693
11023
|
*
|
|
9694
11024
|
* Only `ChannelAuthError` propagates.
|
|
9695
11025
|
*/
|
|
@@ -9703,6 +11033,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9703
11033
|
});
|
|
9704
11034
|
return;
|
|
9705
11035
|
}
|
|
11036
|
+
if (await this.advanceReadoptCorrelation(sessionId, row, messages)) return;
|
|
9706
11037
|
const ocId = row.opencode_message_id;
|
|
9707
11038
|
const state = messageRunState(messages, ocId ?? "");
|
|
9708
11039
|
if (state === "done") {
|
|
@@ -9769,12 +11100,48 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9769
11100
|
await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
|
|
9770
11101
|
this.dontRedispatch.delete(row.id);
|
|
9771
11102
|
this.untrackableAck.delete(row.id);
|
|
11103
|
+
this.untrackableAckSessionIds.delete(row.id);
|
|
9772
11104
|
this.untrackableAckSignalled.delete(row.id);
|
|
9773
11105
|
this.untrackableAckWarned.delete(row.id);
|
|
9774
11106
|
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
9775
11107
|
return;
|
|
9776
11108
|
}
|
|
9777
|
-
if (this.
|
|
11109
|
+
if (this.untrackableAck.has(row.id)) {
|
|
11110
|
+
if (this.untrackableAckSignalled.has(row.id)) {
|
|
11111
|
+
const firstFenceWarning = !this.untrackableAckWarned.has(row.id);
|
|
11112
|
+
this.untrackableAckWarned.add(row.id);
|
|
11113
|
+
this.log({
|
|
11114
|
+
level: firstFenceWarning ? "warn" : "debug",
|
|
11115
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} is fenced after an untrackable OpenCode turn \u2014 skipping re-dispatch and retrying the terminal report`,
|
|
11116
|
+
conversation_id: row.conversation_id,
|
|
11117
|
+
message_id: row.id
|
|
11118
|
+
});
|
|
11119
|
+
const errorMessage3 = this.untrackableAck.get(row.id);
|
|
11120
|
+
const reportError = await this.reportUntrackableAck(
|
|
11121
|
+
row.conversation_id,
|
|
11122
|
+
row.id,
|
|
11123
|
+
this.untrackableAckSessionIds.get(row.id) ?? null,
|
|
11124
|
+
errorMessage3
|
|
11125
|
+
);
|
|
11126
|
+
if (reportError !== null) {
|
|
11127
|
+
this.log({
|
|
11128
|
+
level: "warn",
|
|
11129
|
+
message: `Re-adopt: failed to report untrackable message ${row.id.slice(0, 8)} (will retry next drain): ${reportError.message}`,
|
|
11130
|
+
conversation_id: row.conversation_id,
|
|
11131
|
+
message_id: row.id
|
|
11132
|
+
});
|
|
11133
|
+
}
|
|
11134
|
+
} else {
|
|
11135
|
+
this.log({
|
|
11136
|
+
level: "debug",
|
|
11137
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
|
|
11138
|
+
conversation_id: row.conversation_id,
|
|
11139
|
+
message_id: row.id
|
|
11140
|
+
});
|
|
11141
|
+
}
|
|
11142
|
+
return;
|
|
11143
|
+
}
|
|
11144
|
+
if (this.dontRedispatch.has(row.id)) {
|
|
9778
11145
|
this.log({
|
|
9779
11146
|
level: "debug",
|
|
9780
11147
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
|
|
@@ -9952,6 +11319,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9952
11319
|
}
|
|
9953
11320
|
this.dontRedispatch.delete(row.id);
|
|
9954
11321
|
this.untrackableAck.delete(row.id);
|
|
11322
|
+
this.untrackableAckSessionIds.delete(row.id);
|
|
9955
11323
|
this.untrackableAckSignalled.delete(row.id);
|
|
9956
11324
|
this.untrackableAckWarned.delete(row.id);
|
|
9957
11325
|
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
@@ -9971,8 +11339,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9971
11339
|
* latch makes a null-id row re-dispatched AT MOST ONCE per outstanding read-back:
|
|
9972
11340
|
* short-circuit while the row is latched; clear it on a successful dispatch (the
|
|
9973
11341
|
* row is then tracked in `dispatched`, so `readoptOne`'s early skip prevents
|
|
9974
|
-
* re-entry)
|
|
9975
|
-
*
|
|
11342
|
+
* re-entry) or when the dispatch itself failed before acceptance. An accepted V1
|
|
11343
|
+
* dispatch whose id is not readable enters the bounded correlation fence instead
|
|
11344
|
+
* of retrying; exhaustion hands it to `untrackableAck`.
|
|
9976
11345
|
*
|
|
9977
11346
|
* `evidentMessageId = row.id` addresses the SERVER row. Deadline anchored to
|
|
9978
11347
|
* `processed_at` (Invariant 1).
|
|
@@ -10035,6 +11404,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10035
11404
|
);
|
|
10036
11405
|
}
|
|
10037
11406
|
let ocId;
|
|
11407
|
+
let preDispatchUserIds = null;
|
|
10038
11408
|
try {
|
|
10039
11409
|
ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
|
|
10040
11410
|
sessionId,
|
|
@@ -10044,7 +11414,20 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10044
11414
|
row.content,
|
|
10045
11415
|
options,
|
|
10046
11416
|
sendAttachments,
|
|
10047
|
-
this.openCodeClient
|
|
11417
|
+
this.openCodeClient,
|
|
11418
|
+
{
|
|
11419
|
+
onKnownUserIds: (ids) => {
|
|
11420
|
+
preDispatchUserIds = ids;
|
|
11421
|
+
},
|
|
11422
|
+
onKnownUserIdsError: (error2) => {
|
|
11423
|
+
this.log({
|
|
11424
|
+
level: "warn",
|
|
11425
|
+
message: `Pre-dispatch snapshot callback failed for message ${row.id.slice(0, 8)}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
11426
|
+
conversation_id: row.conversation_id,
|
|
11427
|
+
message_id: row.id
|
|
11428
|
+
});
|
|
11429
|
+
}
|
|
11430
|
+
}
|
|
10048
11431
|
)
|
|
10049
11432
|
);
|
|
10050
11433
|
} catch (err) {
|
|
@@ -10077,6 +11460,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10077
11460
|
}
|
|
10078
11461
|
} else {
|
|
10079
11462
|
this.untrackableAck.set(row.id, errorMessage3);
|
|
11463
|
+
this.untrackableAckSessionIds.set(row.id, sessionId);
|
|
10080
11464
|
}
|
|
10081
11465
|
this.log({
|
|
10082
11466
|
level: "error",
|
|
@@ -10125,39 +11509,20 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10125
11509
|
}
|
|
10126
11510
|
if (ocId === null) {
|
|
10127
11511
|
this.awaitingReadopt.delete(row.id);
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
this.log({
|
|
10135
|
-
level: "error",
|
|
10136
|
-
message: errorMessage3,
|
|
10137
|
-
conversation_id: row.conversation_id,
|
|
10138
|
-
message_id: row.id
|
|
10139
|
-
});
|
|
10140
|
-
await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
|
|
10141
|
-
this.log({
|
|
10142
|
-
level: "warn",
|
|
10143
|
-
message: `markFailed PATCH for message ${row.id.slice(0, 8)} (conversation ${row.conversation_id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
10144
|
-
conversation_id: row.conversation_id,
|
|
10145
|
-
message_id: row.id
|
|
10146
|
-
});
|
|
10147
|
-
});
|
|
10148
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
10149
|
-
return;
|
|
10150
|
-
}
|
|
11512
|
+
this.awaitingCorrelation.set(row.id, {
|
|
11513
|
+
sessionId,
|
|
11514
|
+
content: row.content,
|
|
11515
|
+
attempts: 0,
|
|
11516
|
+
knownUserIds: preDispatchUserIds
|
|
11517
|
+
});
|
|
10151
11518
|
this.log({
|
|
10152
11519
|
level: "warn",
|
|
10153
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)}
|
|
11520
|
+
message: `Re-adopt: OpenCode accepted message ${row.id.slice(0, 8)}, but its assigned id could not be read back \u2014 the message will not be sent again while the runner matches it to the turn already running`,
|
|
10154
11521
|
conversation_id: row.conversation_id,
|
|
10155
11522
|
message_id: row.id
|
|
10156
11523
|
});
|
|
10157
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
10158
11524
|
return;
|
|
10159
11525
|
}
|
|
10160
|
-
this.unconfirmedDispatchFailures.delete(row.id);
|
|
10161
11526
|
const processedAtMs = this.processedAtMs(row);
|
|
10162
11527
|
this.registerReadopted(
|
|
10163
11528
|
readoptConv,
|
|
@@ -10496,14 +11861,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10496
11861
|
const fetchChildWithoutBlocking = async (sessionId) => {
|
|
10497
11862
|
const pending = fetchChildMessages(sessionId);
|
|
10498
11863
|
let timer;
|
|
10499
|
-
const timeout = new Promise((
|
|
11864
|
+
const timeout = new Promise((resolve5) => {
|
|
10500
11865
|
timer = setTimeout(() => {
|
|
10501
11866
|
this.log({
|
|
10502
11867
|
level: "warn",
|
|
10503
11868
|
message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} was slow \u2014 omitting invocation telemetry without delaying completion`,
|
|
10504
11869
|
message_id: messageId
|
|
10505
11870
|
});
|
|
10506
|
-
|
|
11871
|
+
resolve5(null);
|
|
10507
11872
|
}, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
|
|
10508
11873
|
});
|
|
10509
11874
|
try {
|
|
@@ -11063,7 +12428,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
11063
12428
|
* watcher retries next tick within the
|
|
11064
12429
|
* deadline, Finding 4).
|
|
11065
12430
|
*/
|
|
11066
|
-
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
|
|
12431
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations, error2) {
|
|
11067
12432
|
const res = await this.fetchImpl(
|
|
11068
12433
|
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
11069
12434
|
{
|
|
@@ -11081,7 +12446,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
11081
12446
|
...title ? { title } : {},
|
|
11082
12447
|
...usage ? usage : {},
|
|
11083
12448
|
...usageAgentName ? { usage_agent_name: usageAgentName } : {},
|
|
11084
|
-
...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
|
|
12449
|
+
...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {},
|
|
12450
|
+
...error2 ? { error: error2 } : {}
|
|
11085
12451
|
})
|
|
11086
12452
|
}
|
|
11087
12453
|
);
|
|
@@ -11744,7 +13110,7 @@ function commandError2(result) {
|
|
|
11744
13110
|
return result.stderr.trim() || `process exited with code ${result.code ?? "null"}`;
|
|
11745
13111
|
}
|
|
11746
13112
|
var runCommand2 = (command, args, opts) => {
|
|
11747
|
-
return new Promise((
|
|
13113
|
+
return new Promise((resolve5) => {
|
|
11748
13114
|
let child;
|
|
11749
13115
|
let stdout = "";
|
|
11750
13116
|
let stderr = "";
|
|
@@ -11754,7 +13120,7 @@ var runCommand2 = (command, args, opts) => {
|
|
|
11754
13120
|
if (settled) return;
|
|
11755
13121
|
settled = true;
|
|
11756
13122
|
if (timer.handle) clearTimeout(timer.handle);
|
|
11757
|
-
|
|
13123
|
+
resolve5(result);
|
|
11758
13124
|
};
|
|
11759
13125
|
try {
|
|
11760
13126
|
child = spawn5(command, args, { env: opts.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -11809,7 +13175,9 @@ async function installRunnerSecret({
|
|
|
11809
13175
|
}) {
|
|
11810
13176
|
const arn = env.RUNNER_SECRET_ARN?.trim();
|
|
11811
13177
|
if (!arn) {
|
|
11812
|
-
|
|
13178
|
+
if (!env.GH_TOKEN) {
|
|
13179
|
+
log3("runner secret is not configured; continuing without GitHub and MCP credentials");
|
|
13180
|
+
}
|
|
11813
13181
|
return false;
|
|
11814
13182
|
}
|
|
11815
13183
|
const result = await (commandRunner ?? runCommand2)(
|
|
@@ -12008,7 +13376,7 @@ async function configureGitHubAccess({ env, log: log3 }) {
|
|
|
12008
13376
|
// src/lib/opencode/config-overlay.ts
|
|
12009
13377
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
12010
13378
|
import { copyFileSync, existsSync as existsSync2, statSync as statSync6 } from "node:fs";
|
|
12011
|
-
import { isAbsolute as
|
|
13379
|
+
import { isAbsolute as isAbsolute3, join as join11, resolve as resolve4 } from "node:path";
|
|
12012
13380
|
function isFile(filePath) {
|
|
12013
13381
|
return existsSync2(filePath) && statSync6(filePath).isFile();
|
|
12014
13382
|
}
|
|
@@ -12021,8 +13389,8 @@ function applyRunnerOpenCodeConfig({
|
|
|
12021
13389
|
log3("runner OpenCode config is not configured; using the baked project config", "debug");
|
|
12022
13390
|
return;
|
|
12023
13391
|
}
|
|
12024
|
-
const source =
|
|
12025
|
-
const target = isFile(
|
|
13392
|
+
const source = isAbsolute3(overlayPath) ? overlayPath : resolve4(cwd, overlayPath);
|
|
13393
|
+
const target = isFile(join11(cwd, "opencode.jsonc")) ? "opencode.jsonc" : "opencode.json";
|
|
12026
13394
|
if (!isFile(source)) {
|
|
12027
13395
|
log3(
|
|
12028
13396
|
`RUNNER-OPENCODE-CONFIG-MISSING: ${source} is not a file; headless turns will wedge on the first external-directory permission prompt (#563)`,
|
|
@@ -12030,7 +13398,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
12030
13398
|
);
|
|
12031
13399
|
return;
|
|
12032
13400
|
}
|
|
12033
|
-
copyFileSync(source,
|
|
13401
|
+
copyFileSync(source, join11(cwd, target));
|
|
12034
13402
|
try {
|
|
12035
13403
|
execFileSync2("git", ["-C", cwd, "update-index", "--skip-worktree", target], {
|
|
12036
13404
|
stdio: "ignore"
|
|
@@ -12039,7 +13407,7 @@ function applyRunnerOpenCodeConfig({
|
|
|
12039
13407
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
12040
13408
|
log3(`could not mark ${target} skip-worktree: ${detail}; it may show as a local change`, "warn");
|
|
12041
13409
|
}
|
|
12042
|
-
log3(`Applied runner OpenCode config ${source} to ${
|
|
13410
|
+
log3(`Applied runner OpenCode config ${source} to ${join11(cwd, target)}`);
|
|
12043
13411
|
}
|
|
12044
13412
|
|
|
12045
13413
|
// src/lib/credential-sync.ts
|
|
@@ -12057,14 +13425,14 @@ function errorMessage2(error2) {
|
|
|
12057
13425
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
12058
13426
|
}
|
|
12059
13427
|
function waitForSettlement(promise, timeoutMs) {
|
|
12060
|
-
return new Promise((
|
|
13428
|
+
return new Promise((resolve5) => {
|
|
12061
13429
|
let settled = false;
|
|
12062
13430
|
const timer = setTimeout(() => finish(false), Math.max(0, timeoutMs));
|
|
12063
13431
|
const finish = (value) => {
|
|
12064
13432
|
if (settled) return;
|
|
12065
13433
|
settled = true;
|
|
12066
13434
|
clearTimeout(timer);
|
|
12067
|
-
|
|
13435
|
+
resolve5(value);
|
|
12068
13436
|
};
|
|
12069
13437
|
promise.then(
|
|
12070
13438
|
() => finish(true),
|
|
@@ -12315,8 +13683,8 @@ function resolveFileSyncDirectories(raw, homeDir) {
|
|
|
12315
13683
|
if (trimmed === "") {
|
|
12316
13684
|
throw new Error("--enable-file-sync-to requires a directory path (got an empty value)");
|
|
12317
13685
|
}
|
|
12318
|
-
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ?
|
|
12319
|
-
if (!
|
|
13686
|
+
const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") ? join12(homeDir, trimmed.slice(2)) : trimmed;
|
|
13687
|
+
if (!isAbsolute4(expanded)) {
|
|
12320
13688
|
throw new Error(`--enable-file-sync-to requires an absolute directory path; got "${entry}"`);
|
|
12321
13689
|
}
|
|
12322
13690
|
const normalized = resolvePath2(expanded);
|
|
@@ -12416,8 +13784,7 @@ function resolveMaxActiveSessions(options, env = process.env) {
|
|
|
12416
13784
|
function meetsThreshold(state, level) {
|
|
12417
13785
|
return LOG_LEVELS[level] >= LOG_LEVELS[state.logLevel];
|
|
12418
13786
|
}
|
|
12419
|
-
function
|
|
12420
|
-
if (!meetsThreshold(state, level)) return;
|
|
13787
|
+
function writeConsoleLog(state, message, level) {
|
|
12421
13788
|
if (state.json) {
|
|
12422
13789
|
console.log(
|
|
12423
13790
|
JSON.stringify({
|
|
@@ -12426,10 +13793,18 @@ function log2(state, message, level = "info") {
|
|
|
12426
13793
|
message
|
|
12427
13794
|
})
|
|
12428
13795
|
);
|
|
12429
|
-
|
|
12430
|
-
const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
|
|
12431
|
-
console.log(`${prefix} ${message}`);
|
|
13796
|
+
return;
|
|
12432
13797
|
}
|
|
13798
|
+
const prefix = level === "error" ? chalk7.red("\u2717") : level === "warn" ? chalk7.yellow("!") : level === "debug" ? chalk7.dim("\xB7") : chalk7.green("\u2022");
|
|
13799
|
+
console.log(`${prefix} ${message}`);
|
|
13800
|
+
}
|
|
13801
|
+
function log2(state, message, level = "info") {
|
|
13802
|
+
if (!meetsThreshold(state, level)) return;
|
|
13803
|
+
if (state.json || !state.interactive) writeConsoleLog(state, message, level);
|
|
13804
|
+
}
|
|
13805
|
+
function logLocalOnly(state, message, level = "info") {
|
|
13806
|
+
if (!meetsThreshold(state, level)) return;
|
|
13807
|
+
writeConsoleLog(state, message, level);
|
|
12433
13808
|
}
|
|
12434
13809
|
function logActivity(state, entry) {
|
|
12435
13810
|
const level = entry.level ?? (entry.type === "error" ? "error" : "info");
|
|
@@ -12731,7 +14106,7 @@ async function driveChannels(state, driver) {
|
|
|
12731
14106
|
}
|
|
12732
14107
|
}
|
|
12733
14108
|
}
|
|
12734
|
-
await new Promise((
|
|
14109
|
+
await new Promise((resolve5) => setTimeout(resolve5, CHANNEL_POLL_INTERVAL_MS));
|
|
12735
14110
|
const cycleMs = performance.now() - cycleStartedAtMs;
|
|
12736
14111
|
if (idleThisCycle) idleMs += cycleMs;
|
|
12737
14112
|
if (unreachableThisCycle) unreachableMs += cycleMs;
|
|
@@ -12752,9 +14127,12 @@ async function driveChannels(state, driver) {
|
|
|
12752
14127
|
}
|
|
12753
14128
|
}
|
|
12754
14129
|
var SESSION_CLEANUP_FIRST_SWEEP_MS = 1e4;
|
|
12755
|
-
var
|
|
14130
|
+
var SESSION_CLEANUP_MAX_DELETES_PER_SWEEP = 100;
|
|
14131
|
+
var SESSION_CLEANUP_MAX_ISOLATED_DELETE_FAILURES = 2;
|
|
14132
|
+
var SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES = 2;
|
|
14133
|
+
var MAX_SESSION_DELETE_BODY_LENGTH = 200;
|
|
12756
14134
|
function shouldWarnForReclaimSkip(reason) {
|
|
12757
|
-
if (reason !== "sqlite-unavailable") return false;
|
|
14135
|
+
if (reason !== "sqlite-unavailable" && reason !== "disk-exhaustion") return false;
|
|
12758
14136
|
const version2 = /^v(\d+)\.(\d+)\.(\d+)$/.exec(process.version);
|
|
12759
14137
|
if (!version2) return false;
|
|
12760
14138
|
const major = Number(version2[1]);
|
|
@@ -12763,7 +14141,7 @@ function shouldWarnForReclaimSkip(reason) {
|
|
|
12763
14141
|
return major > 22 || major === 22 && (minor > 13 || minor === 13 && patch >= 0);
|
|
12764
14142
|
}
|
|
12765
14143
|
function sessionDbPath() {
|
|
12766
|
-
return
|
|
14144
|
+
return join12(homedir7(), ".local", "share", "opencode", "opencode.db");
|
|
12767
14145
|
}
|
|
12768
14146
|
function logSessionCleanupActivity(state, phase, entry) {
|
|
12769
14147
|
logActivity(state, {
|
|
@@ -12811,61 +14189,146 @@ function logSessionDbProvenanceMismatch(state, provenance, currentVersion, preBo
|
|
|
12811
14189
|
async function runSweep(state, driver, config) {
|
|
12812
14190
|
const mode = `age=${config.maxAgeMs ?? "\u2014"} count=${config.maxCount ?? "\u2014"}`;
|
|
12813
14191
|
try {
|
|
12814
|
-
const
|
|
12815
|
-
|
|
12816
|
-
|
|
14192
|
+
const enumeration = await listAllSessions(state.port, state.opencodeClient ?? void 0);
|
|
14193
|
+
const sessions = enumeration.sessions;
|
|
14194
|
+
const enumerationReason = sessions === null ? enumeration.reason : null;
|
|
14195
|
+
if (enumerationReason !== null) {
|
|
14196
|
+
logActivity(state, {
|
|
12817
14197
|
type: "info",
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
outcome: "sweep-skipped",
|
|
12821
|
-
reason: "opencode-unreachable"
|
|
12822
|
-
}
|
|
14198
|
+
level: "warn",
|
|
14199
|
+
message: `Session cleanup: could not enumerate sessions (${enumerationReason}); skipping deletes and continuing with space reclaim (${mode})`
|
|
12823
14200
|
});
|
|
12824
|
-
return;
|
|
12825
14201
|
}
|
|
12826
|
-
const toDelete = selectSessionsToDelete(
|
|
12827
|
-
sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
|
|
12828
|
-
{
|
|
12829
|
-
maxAgeMs: config.maxAgeMs,
|
|
12830
|
-
maxCount: config.maxCount,
|
|
12831
|
-
nowMs: Date.now(),
|
|
12832
|
-
protectedIds: driver.protectedSessionIds()
|
|
12833
|
-
}
|
|
12834
|
-
);
|
|
12835
|
-
const protectedNow = driver.protectedSessionIds();
|
|
12836
14202
|
let deleted = 0;
|
|
12837
14203
|
let failed = 0;
|
|
14204
|
+
let failed4xx = 0;
|
|
14205
|
+
let failed5xx = 0;
|
|
14206
|
+
let failedOtherStatus = 0;
|
|
14207
|
+
let failedThrew = 0;
|
|
12838
14208
|
let skippedNewlyActive = 0;
|
|
12839
|
-
|
|
12840
|
-
|
|
12841
|
-
|
|
12842
|
-
|
|
14209
|
+
let remaining = 0;
|
|
14210
|
+
let protectedNow = driver.protectedSessionIds();
|
|
14211
|
+
if (sessions !== null) {
|
|
14212
|
+
const toDelete = selectSessionsToDelete(
|
|
14213
|
+
sessions.map((s) => ({ id: s.id, lastActivityMs: sessionLastActivityMs(s) })),
|
|
14214
|
+
{
|
|
14215
|
+
maxAgeMs: config.maxAgeMs,
|
|
14216
|
+
maxCount: config.maxCount,
|
|
14217
|
+
nowMs: Date.now(),
|
|
14218
|
+
protectedIds: protectedNow
|
|
14219
|
+
}
|
|
14220
|
+
);
|
|
14221
|
+
const eligibleToDelete = toDelete.filter(
|
|
14222
|
+
(id) => (state.sessionDeleteFailures.get(id)?.isolatedFailures ?? 0) < SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES
|
|
14223
|
+
);
|
|
14224
|
+
const deleteCandidates = eligibleToDelete.slice(0, SESSION_CLEANUP_MAX_DELETES_PER_SWEEP);
|
|
14225
|
+
remaining = eligibleToDelete.length - deleteCandidates.length;
|
|
14226
|
+
const answeredFailures = [];
|
|
14227
|
+
protectedNow = driver.protectedSessionIds();
|
|
14228
|
+
for (const id of deleteCandidates) {
|
|
14229
|
+
if (protectedNow.has(id)) {
|
|
14230
|
+
skippedNewlyActive++;
|
|
14231
|
+
logActivity(state, {
|
|
14232
|
+
type: "info",
|
|
14233
|
+
message: `Session cleanup: skipping ${id} \u2014 became active/bound after selection (${mode})`
|
|
14234
|
+
});
|
|
14235
|
+
continue;
|
|
14236
|
+
}
|
|
14237
|
+
const outcome = await deleteSession(state.port, id, state.opencodeClient ?? void 0);
|
|
14238
|
+
if (outcome.ok) {
|
|
14239
|
+
deleted++;
|
|
14240
|
+
} else if (outcome.status === "threw") {
|
|
14241
|
+
failed++;
|
|
14242
|
+
failedThrew++;
|
|
14243
|
+
} else {
|
|
14244
|
+
failed++;
|
|
14245
|
+
const statusClass = Math.floor(outcome.status / 100);
|
|
14246
|
+
if (statusClass === 4) failed4xx++;
|
|
14247
|
+
else if (statusClass === 5) failed5xx++;
|
|
14248
|
+
else failedOtherStatus++;
|
|
14249
|
+
answeredFailures.push({
|
|
14250
|
+
id,
|
|
14251
|
+
status: outcome.status,
|
|
14252
|
+
...outcome.body !== void 0 ? { body: outcome.body } : {}
|
|
14253
|
+
});
|
|
14254
|
+
}
|
|
14255
|
+
}
|
|
14256
|
+
const givenUpFailures = [];
|
|
14257
|
+
if (failed > SESSION_CLEANUP_MAX_ISOLATED_DELETE_FAILURES) {
|
|
14258
|
+
for (const { id } of answeredFailures) state.sessionDeleteFailures.delete(id);
|
|
14259
|
+
} else {
|
|
14260
|
+
for (const failure of answeredFailures) {
|
|
14261
|
+
const previous = state.sessionDeleteFailures.get(failure.id);
|
|
14262
|
+
const record = previous?.status === failure.status ? { status: failure.status, isolatedFailures: previous.isolatedFailures + 1 } : { status: failure.status, isolatedFailures: 1 };
|
|
14263
|
+
state.sessionDeleteFailures.set(failure.id, record);
|
|
14264
|
+
if (record.isolatedFailures >= SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES) {
|
|
14265
|
+
givenUpFailures.push(failure);
|
|
14266
|
+
}
|
|
14267
|
+
}
|
|
14268
|
+
}
|
|
14269
|
+
for (const failure of givenUpFailures) {
|
|
14270
|
+
const excerpt = redactDiagnosticText(failure.body ?? "(no response body)").slice(
|
|
14271
|
+
0,
|
|
14272
|
+
MAX_SESSION_DELETE_BODY_LENGTH
|
|
14273
|
+
);
|
|
14274
|
+
logSessionCleanupActivity(state, "sweep", {
|
|
12843
14275
|
type: "info",
|
|
12844
|
-
|
|
14276
|
+
level: "warn",
|
|
14277
|
+
message: `Session cleanup: gave up on session ${failure.id} after ${SESSION_CLEANUP_GIVE_UP_AFTER_FAILURES} isolated delete failures (status ${failure.status}): ${excerpt}`,
|
|
14278
|
+
metadata: {
|
|
14279
|
+
outcome: "session-delete-given-up",
|
|
14280
|
+
session_id: failure.id,
|
|
14281
|
+
status: failure.status,
|
|
14282
|
+
body_excerpt: excerpt,
|
|
14283
|
+
given_up_count: givenUpFailures.length
|
|
14284
|
+
}
|
|
12845
14285
|
});
|
|
12846
|
-
continue;
|
|
12847
14286
|
}
|
|
12848
|
-
|
|
12849
|
-
|
|
14287
|
+
const failedNote = failed > 0 ? `, failed ${failed}` : "";
|
|
14288
|
+
const skippedNote = skippedNewlyActive > 0 ? `, skipped ${skippedNewlyActive} newly-active` : "";
|
|
14289
|
+
const remainingNote = remaining > 0 ? `, remaining ${remaining}` : "";
|
|
14290
|
+
logActivity(state, {
|
|
14291
|
+
type: "info",
|
|
14292
|
+
message: `Session cleanup: inspected ${sessions.length}, deleted ${deleted}${failedNote}${skippedNote}${remainingNote} (${mode})`
|
|
14293
|
+
});
|
|
12850
14294
|
}
|
|
12851
|
-
const
|
|
12852
|
-
|
|
12853
|
-
|
|
12854
|
-
|
|
12855
|
-
|
|
12856
|
-
|
|
14295
|
+
const sessionMetadata = {
|
|
14296
|
+
sessions_inspected: sessions?.length ?? null,
|
|
14297
|
+
sessions_deleted: deleted,
|
|
14298
|
+
sessions_failed: failed,
|
|
14299
|
+
sessions_failed_4xx: failed4xx,
|
|
14300
|
+
sessions_failed_5xx: failed5xx,
|
|
14301
|
+
sessions_failed_other: failedOtherStatus,
|
|
14302
|
+
sessions_failed_threw: failedThrew,
|
|
14303
|
+
sessions_skipped_active: skippedNewlyActive,
|
|
14304
|
+
sessions_remaining: remaining,
|
|
14305
|
+
...enumerationReason !== null ? { session_enumeration_reason: enumerationReason } : {}
|
|
14306
|
+
};
|
|
14307
|
+
const protectedForReclaim = driver.protectedSessionIds();
|
|
12857
14308
|
const reclaimResult = await reclaimSessionDbSpace({
|
|
12858
14309
|
dbPath: sessionDbPath(),
|
|
12859
|
-
|
|
12860
|
-
|
|
14310
|
+
chunkPages: SESSION_DB_RECLAIM_CHUNK_PAGES,
|
|
14311
|
+
pageCeiling: SESSION_DB_RECLAIM_PAGE_CEILING,
|
|
14312
|
+
timeBudgetMs: SESSION_DB_RECLAIM_TIME_BUDGET_MS,
|
|
14313
|
+
hasLiveTurn: protectedForReclaim.size > 0,
|
|
14314
|
+
hasProvenanceAnomaly: state.sessionDbProvenanceAnomaly
|
|
12861
14315
|
});
|
|
12862
14316
|
if (reclaimResult.ok) {
|
|
14317
|
+
state.sessionStoreHealth.observeWritable();
|
|
12863
14318
|
const beforeMib = (reclaimResult.beforeBytes / 1024 / 1024).toFixed(1);
|
|
12864
14319
|
const afterMib = (reclaimResult.afterBytes / 1024 / 1024).toFixed(1);
|
|
12865
14320
|
const checkpointNote = reclaimResult.checkpoint.busy ? ` (on-disk file truncation deferred: checkpoint busy, ${reclaimResult.checkpoint.log} WAL frames pending)` : "";
|
|
14321
|
+
const loopElapsedMs = Math.max(
|
|
14322
|
+
0,
|
|
14323
|
+
reclaimResult.elapsedMs - reclaimResult.checkpointElapsedMs
|
|
14324
|
+
);
|
|
14325
|
+
const reclaimIsSlow = reclaimResult.elapsedMs >= 4 * SESSION_DB_RECLAIM_TIME_BUDGET_MS;
|
|
14326
|
+
const perPageNote = reclaimResult.pagesReclaimed > 0 ? `, per-page ${(loopElapsedMs / reclaimResult.pagesReclaimed).toFixed(2)} ms/page` : "";
|
|
14327
|
+
const slowReclaimNote = reclaimIsSlow ? ` (slow reclaim: total elapsed ${reclaimResult.elapsedMs} ms, checkpoint ${reclaimResult.checkpointElapsedMs} ms, loop ${loopElapsedMs} ms, budget ${SESSION_DB_RECLAIM_TIME_BUDGET_MS} ms${perPageNote})` : "";
|
|
12866
14328
|
logSessionCleanupActivity(state, "sweep", {
|
|
12867
14329
|
type: "info",
|
|
12868
|
-
|
|
14330
|
+
level: reclaimIsSlow ? "warn" : "info",
|
|
14331
|
+
message: `Session cleanup: reclaimed session-db space (${reclaimResult.mode}): ${beforeMib} MiB -> ${afterMib} MiB (stopped by ${reclaimResult.stoppedBy}; freelist ${reclaimResult.freelistBefore} -> ${reclaimResult.freelistAfter} pages)` + checkpointNote + slowReclaimNote,
|
|
12869
14332
|
metadata: {
|
|
12870
14333
|
outcome: "reclaimed",
|
|
12871
14334
|
mode: reclaimResult.mode,
|
|
@@ -12873,25 +14336,28 @@ async function runSweep(state, driver, config) {
|
|
|
12873
14336
|
after_bytes: reclaimResult.afterBytes,
|
|
12874
14337
|
checkpoint_busy: reclaimResult.checkpoint.busy,
|
|
12875
14338
|
wal_frames_pending: reclaimResult.checkpoint.log,
|
|
12876
|
-
|
|
12877
|
-
|
|
12878
|
-
|
|
12879
|
-
|
|
14339
|
+
stopped_by: reclaimResult.stoppedBy,
|
|
14340
|
+
freelist_before: reclaimResult.freelistBefore,
|
|
14341
|
+
freelist_after: reclaimResult.freelistAfter,
|
|
14342
|
+
pages_reclaimed: reclaimResult.pagesReclaimed,
|
|
14343
|
+
reclaim_elapsed_ms: reclaimResult.elapsedMs,
|
|
14344
|
+
checkpoint_elapsed_ms: reclaimResult.checkpointElapsedMs,
|
|
14345
|
+
...sessionMetadata
|
|
12880
14346
|
}
|
|
12881
14347
|
});
|
|
12882
14348
|
} else {
|
|
14349
|
+
if (reclaimResult.skipped === "disk-exhaustion") {
|
|
14350
|
+
state.sessionStoreHealth.observeUnwritable();
|
|
14351
|
+
}
|
|
12883
14352
|
logSessionCleanupActivity(state, "sweep", {
|
|
12884
14353
|
type: "info",
|
|
12885
|
-
level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
|
|
14354
|
+
level: reclaimResult.skipped === "reclaim-error" || reclaimResult.skipped === "disk-exhaustion" || reclaimResult.skipped === "insufficient-disk-space" ? "warn" : "info",
|
|
12886
14355
|
message: `Session cleanup: session-db space reclaim skipped (${reclaimResult.skipped})` + (reclaimResult.detail ? `: ${reclaimResult.detail}` : ""),
|
|
12887
14356
|
metadata: {
|
|
12888
14357
|
outcome: "reclaim-skipped",
|
|
12889
14358
|
reason: reclaimResult.skipped,
|
|
12890
14359
|
...reclaimResult.detail !== void 0 ? { detail: reclaimResult.detail } : {},
|
|
12891
|
-
|
|
12892
|
-
sessions_deleted: deleted,
|
|
12893
|
-
sessions_failed: failed,
|
|
12894
|
-
sessions_skipped_active: skippedNewlyActive
|
|
14360
|
+
...sessionMetadata
|
|
12895
14361
|
}
|
|
12896
14362
|
});
|
|
12897
14363
|
}
|
|
@@ -13148,6 +14614,19 @@ function scheduleResourceUsageReporting(state, options) {
|
|
|
13148
14614
|
message: `Resource usage collection: ${warning2}`
|
|
13149
14615
|
});
|
|
13150
14616
|
}
|
|
14617
|
+
const lowDiskDecision = evaluateLowDiskWarning({
|
|
14618
|
+
...usage,
|
|
14619
|
+
nowMs: Date.now(),
|
|
14620
|
+
state: state.lowDiskWarningState
|
|
14621
|
+
});
|
|
14622
|
+
state.lowDiskWarningState = lowDiskDecision.state;
|
|
14623
|
+
if (lowDiskDecision.warning !== null) {
|
|
14624
|
+
logActivity(state, {
|
|
14625
|
+
type: "info",
|
|
14626
|
+
level: "warn",
|
|
14627
|
+
message: lowDiskDecision.warning
|
|
14628
|
+
});
|
|
14629
|
+
}
|
|
13151
14630
|
const result = await reportResourceUsage(state.agentId, state.authHeader, usage);
|
|
13152
14631
|
if (result.ok) {
|
|
13153
14632
|
if (consecutiveFailures > 0) {
|
|
@@ -13342,8 +14821,8 @@ async function beginGracefulShutdown(state, trigger) {
|
|
|
13342
14821
|
return true;
|
|
13343
14822
|
}
|
|
13344
14823
|
);
|
|
13345
|
-
const timedOut = new Promise((
|
|
13346
|
-
timer = setTimeout(() =>
|
|
14824
|
+
const timedOut = new Promise((resolve5) => {
|
|
14825
|
+
timer = setTimeout(() => resolve5(false), telemetryBudgetMs);
|
|
13347
14826
|
});
|
|
13348
14827
|
if (!await Promise.race([flushed, timedOut])) {
|
|
13349
14828
|
log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
|
|
@@ -13394,6 +14873,7 @@ async function run(options) {
|
|
|
13394
14873
|
opencodeClient: null,
|
|
13395
14874
|
opencodeV2CredentialImportGuard: { inFlight: false },
|
|
13396
14875
|
sessionDbProvenanceAnomaly: false,
|
|
14876
|
+
sessionStoreHealth: createSessionStoreHealth(),
|
|
13397
14877
|
opencodeProcess: null,
|
|
13398
14878
|
stopOpenCodeLogTail: null,
|
|
13399
14879
|
litestreamProcess: null,
|
|
@@ -13405,12 +14885,14 @@ async function run(options) {
|
|
|
13405
14885
|
messageCount: 0,
|
|
13406
14886
|
lastProxiedActivityAt: null,
|
|
13407
14887
|
sessionCleanupTimers: [],
|
|
14888
|
+
sessionDeleteFailures: /* @__PURE__ */ new Map(),
|
|
13408
14889
|
claudeUsageTimer: null,
|
|
13409
14890
|
claudeUsageRearm: null,
|
|
13410
14891
|
openaiUsageTimer: null,
|
|
13411
14892
|
openaiUsageRearm: null,
|
|
13412
14893
|
resourceUsageTimer: null,
|
|
13413
14894
|
stopResourceUsageSampling: null,
|
|
14895
|
+
lowDiskWarningState: { worstBand: null, warnedAtMs: null },
|
|
13414
14896
|
credentialSync: null,
|
|
13415
14897
|
authHeader: ""
|
|
13416
14898
|
};
|
|
@@ -13584,27 +15066,74 @@ async function run(options) {
|
|
|
13584
15066
|
} else {
|
|
13585
15067
|
log2(state, "Not running in a MicroVM (MICROVM_ID unset) \u2014 nothing to report", "debug");
|
|
13586
15068
|
}
|
|
13587
|
-
if (options.restoreRunnerCredentials) {
|
|
13588
|
-
|
|
13589
|
-
|
|
13590
|
-
|
|
13591
|
-
|
|
13592
|
-
if (level === "error") {
|
|
13593
|
-
logActivity(state, { type: "error", error: message });
|
|
13594
|
-
} else {
|
|
13595
|
-
logActivity(state, { type: "info", level, message });
|
|
13596
|
-
}
|
|
15069
|
+
if (options.restoreRunnerCredentials || options.credentialSyncMarker) {
|
|
15070
|
+
const logCredentialVaultResolution = (message, level) => {
|
|
15071
|
+
log2(state, message, level);
|
|
15072
|
+
if (state.interactive && !state.json) {
|
|
15073
|
+
logActivity(state, { type: "info", level, message });
|
|
13597
15074
|
}
|
|
13598
15075
|
};
|
|
13599
|
-
const
|
|
15076
|
+
const vaultResolution = await getRunnerCredentialVault(state.agentId, state.authHeader);
|
|
15077
|
+
if (vaultResolution.error) {
|
|
15078
|
+
delete process.env.CREDENTIAL_VAULT_PREFIX;
|
|
15079
|
+
logCredentialVaultResolution(
|
|
15080
|
+
`CREDENTIAL-VAULT-LOOKUP-FAILED: ${vaultResolution.error}; credentials will be read from and written to this runner's own prefix this boot`,
|
|
15081
|
+
"warn"
|
|
15082
|
+
);
|
|
15083
|
+
} else if (vaultResolution.prefix !== null && vaultResolution.vaultId !== null && vaultResolution.source !== null) {
|
|
15084
|
+
process.env.CREDENTIAL_VAULT_PREFIX = vaultResolution.prefix;
|
|
15085
|
+
logCredentialVaultResolution(
|
|
15086
|
+
`CREDENTIAL-VAULT: source=${vaultResolution.source} vault=${vaultResolution.vaultId} prefix=${vaultResolution.prefix}`,
|
|
15087
|
+
"info"
|
|
15088
|
+
);
|
|
15089
|
+
} else {
|
|
15090
|
+
delete process.env.CREDENTIAL_VAULT_PREFIX;
|
|
15091
|
+
logCredentialVaultResolution(
|
|
15092
|
+
"CREDENTIAL-VAULT-NONE: this runner is not linked to a credential vault; using its own prefix",
|
|
15093
|
+
"debug"
|
|
15094
|
+
);
|
|
15095
|
+
}
|
|
15096
|
+
}
|
|
15097
|
+
const credentialContext = {
|
|
15098
|
+
env: process.env,
|
|
15099
|
+
log: (message, level = "info") => {
|
|
15100
|
+
if (level === "error") {
|
|
15101
|
+
logActivity(state, { type: "error", error: message });
|
|
15102
|
+
} else {
|
|
15103
|
+
logActivity(state, { type: "info", level, message });
|
|
15104
|
+
}
|
|
15105
|
+
}
|
|
15106
|
+
};
|
|
15107
|
+
const githubTokenPopulated = await installRunnerSecret(credentialContext);
|
|
15108
|
+
if (options.restoreRunnerCredentials) {
|
|
13600
15109
|
await restoreCredentialStores(credentialContext);
|
|
13601
|
-
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
13602
15110
|
}
|
|
15111
|
+
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
13603
15112
|
state.credentialSync?.arm();
|
|
13604
|
-
const
|
|
13605
|
-
|
|
13606
|
-
|
|
13607
|
-
|
|
15113
|
+
const dependencyInstallDiagnosticReporter = createDependencyInstallDiagnosticReporter({
|
|
15114
|
+
context: {
|
|
15115
|
+
homeDir: homedir7(),
|
|
15116
|
+
cwd: process.cwd(),
|
|
15117
|
+
microvmId: process.env.MICROVM_ID?.trim(),
|
|
15118
|
+
euid: process.getuid?.(),
|
|
15119
|
+
egid: process.getgid?.()
|
|
15120
|
+
},
|
|
15121
|
+
emitEvent: (message) => logActivity(state, { type: "info", level: "warn", message }),
|
|
15122
|
+
emitLocal: (message) => logLocalOnly(state, message, "warn")
|
|
15123
|
+
});
|
|
15124
|
+
const openCodeActivityForwarder = createOpenCodeActivityForwarder(
|
|
15125
|
+
() => ({
|
|
15126
|
+
agentId: state.agentId,
|
|
15127
|
+
authHeader: state.authHeader
|
|
15128
|
+
}),
|
|
15129
|
+
({ isDiskFailure, sessionId, cause }) => {
|
|
15130
|
+
if (isDiskFailure) state.sessionStoreHealth.observeUnwritable();
|
|
15131
|
+
if (cause !== null && sessionId !== null) {
|
|
15132
|
+
state.sessionStoreHealth.observeCause(sessionId, cause);
|
|
15133
|
+
}
|
|
15134
|
+
},
|
|
15135
|
+
dependencyInstallDiagnosticReporter
|
|
15136
|
+
);
|
|
13608
15137
|
const openCodeLogTail = tailOpenCodeLogFile(
|
|
13609
15138
|
resolveOpenCodeLogPath(homedir7(), process.env),
|
|
13610
15139
|
openCodeActivityForwarder
|
|
@@ -13614,22 +15143,27 @@ async function run(options) {
|
|
|
13614
15143
|
openCodeLogTail.stop();
|
|
13615
15144
|
};
|
|
13616
15145
|
let sessionDbVerifyFatal = false;
|
|
15146
|
+
let sessionDbVerifyFatalReason;
|
|
13617
15147
|
if (!options.restoreSessionDb) {
|
|
13618
|
-
|
|
15148
|
+
logActivity(state, {
|
|
15149
|
+
type: "info",
|
|
15150
|
+
level: "warn",
|
|
15151
|
+
message: "SESSION-DB-VERIFY-SKIPPED: reason=flag-absent \u2014 Skipping session-DB restore: --restore-session-db was not passed"
|
|
15152
|
+
});
|
|
13619
15153
|
} else {
|
|
13620
15154
|
const health = await checkOpenCodeHealth(state.port);
|
|
13621
15155
|
if (health.healthy) {
|
|
13622
|
-
|
|
13623
|
-
|
|
13624
|
-
|
|
13625
|
-
"
|
|
13626
|
-
);
|
|
15156
|
+
logActivity(state, {
|
|
15157
|
+
type: "info",
|
|
15158
|
+
level: "warn",
|
|
15159
|
+
message: "SESSION-DB-VERIFY-SKIPPED: reason=opencode-serving \u2014 Skipping session-DB restore: OpenCode is already serving this database"
|
|
15160
|
+
});
|
|
13627
15161
|
} else if (health.authFailed) {
|
|
13628
|
-
|
|
13629
|
-
|
|
13630
|
-
|
|
13631
|
-
"
|
|
13632
|
-
);
|
|
15162
|
+
logActivity(state, {
|
|
15163
|
+
type: "info",
|
|
15164
|
+
level: "warn",
|
|
15165
|
+
message: "SESSION-DB-VERIFY-SKIPPED: reason=opencode-auth-challenge \u2014 Skipping session-DB restore: OpenCode answered with an authentication challenge; assuming this database is in use"
|
|
15166
|
+
});
|
|
13633
15167
|
} else {
|
|
13634
15168
|
const result = await restoreAndVerifySessionDb({
|
|
13635
15169
|
dbPath: sessionDbPath(),
|
|
@@ -13646,12 +15180,13 @@ async function run(options) {
|
|
|
13646
15180
|
reportRecovery: (record) => reportSessionDbRecoveryRecord(state, record)
|
|
13647
15181
|
});
|
|
13648
15182
|
sessionDbVerifyFatal = result.verifyFatal;
|
|
15183
|
+
sessionDbVerifyFatalReason = result.verifyFatalReason;
|
|
13649
15184
|
}
|
|
13650
15185
|
}
|
|
13651
15186
|
reportSessionDbRecovery(state);
|
|
13652
15187
|
if (sessionDbVerifyFatal) {
|
|
13653
15188
|
throw new Error(
|
|
13654
|
-
"SESSION-DB-INTEGRITY-EXHAUSTED: the corrupt session DB could not be proven separated from the active backup prefix or removed from disk"
|
|
15189
|
+
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`
|
|
13655
15190
|
);
|
|
13656
15191
|
}
|
|
13657
15192
|
applyRunnerOpenCodeConfig({
|
|
@@ -13896,6 +15431,7 @@ async function run(options) {
|
|
|
13896
15431
|
getAuthHeader: () => state.authHeader,
|
|
13897
15432
|
conversationFilter: state.conversationFilter,
|
|
13898
15433
|
stuckQueuedMs: CHANNEL_STUCK_QUEUED_MS,
|
|
15434
|
+
sessionStoreHealth: state.sessionStoreHealth,
|
|
13899
15435
|
// #559: the `--enable-file-sync-to` allow-list. Empty ⇒ pending files are
|
|
13900
15436
|
// REJECTED with `file_sync_disabled` on the ack, not silently ignored.
|
|
13901
15437
|
fileSyncDirectories,
|
|
@@ -13977,14 +15513,14 @@ async function run(options) {
|
|
|
13977
15513
|
},
|
|
13978
15514
|
onDisconnected: (code, reason, transition) => {
|
|
13979
15515
|
state.connected = false;
|
|
13980
|
-
const
|
|
15516
|
+
const safeReason2 = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
|
|
13981
15517
|
logActivity(state, {
|
|
13982
15518
|
type: "info",
|
|
13983
|
-
message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${
|
|
15519
|
+
message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason2})`
|
|
13984
15520
|
});
|
|
13985
15521
|
emitAgentDisconnected(state.agentId, {
|
|
13986
15522
|
code,
|
|
13987
|
-
reason:
|
|
15523
|
+
reason: safeReason2,
|
|
13988
15524
|
connection_generation: transition.generation
|
|
13989
15525
|
});
|
|
13990
15526
|
if (state.interactive) displayStatus(state);
|
|
@@ -14129,7 +15665,7 @@ async function run(options) {
|
|
|
14129
15665
|
}
|
|
14130
15666
|
|
|
14131
15667
|
// src/index.ts
|
|
14132
|
-
var
|
|
15668
|
+
var version = getCliVersion();
|
|
14133
15669
|
var program = new Command();
|
|
14134
15670
|
program.name("evident").description("Run OpenCode locally and connect it to Evident").version(version).option(
|
|
14135
15671
|
"--endpoint <url>",
|
|
@@ -14147,6 +15683,7 @@ program.command("login").description("Authenticate with Evident").option("--toke
|
|
|
14147
15683
|
program.command("logout").description("Remove stored credentials for the current endpoint").option("--all", "Remove stored credentials for all endpoints").action((options) => logout({ all: options.all }));
|
|
14148
15684
|
program.command("whoami").description("Show the currently logged in user").action(whoami);
|
|
14149
15685
|
program.command("status").description("Check whether the configured credentials can reach Evident").option("--json", "Output in JSON format").action((options) => status({ json: options.json }));
|
|
15686
|
+
program.command("credential-vault-env").description("Print the shell assignment a runner image evals before starting the synchroniser").action(credentialVaultEnv);
|
|
14150
15687
|
program.command("claude-usage").description("[spike] Show Claude subscription usage (requires a local `claude login`)").action(claudeUsage);
|
|
14151
15688
|
program.command("run").description("Connect to Evident and process messages").option("--runner [id]", "Runner ID to connect to (optional when EVIDENT_RUNNER_KEY is set)").option(
|
|
14152
15689
|
"-a, --agent [id]",
|