@evident-ai/cli 3.5.2-dev.07822ab → 3.5.2-dev.1ca1af3
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/dist/index.js +780 -193
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -703,6 +703,31 @@ async function reportResourceUsage(agentId, authHeader, usage) {
|
|
|
703
703
|
opencode_db_bytes: usage.opencodeDbBytes
|
|
704
704
|
});
|
|
705
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
|
+
}
|
|
706
731
|
async function getAgentInfo(agentId, authHeader) {
|
|
707
732
|
const apiUrl = getApiUrlConfig();
|
|
708
733
|
try {
|
|
@@ -877,6 +902,76 @@ async function status(options = {}) {
|
|
|
877
902
|
process.exit(result.exitCode);
|
|
878
903
|
}
|
|
879
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
|
+
|
|
880
975
|
// src/lib/claude-usage.ts
|
|
881
976
|
import { execFileSync } from "node:child_process";
|
|
882
977
|
import { readFileSync } from "node:fs";
|
|
@@ -1069,6 +1164,76 @@ import chalk7 from "chalk";
|
|
|
1069
1164
|
// ../../packages/types/src/agents/index.ts
|
|
1070
1165
|
var MICROVM_MAX_LIFETIME_MS = 8 * 60 * 6e4;
|
|
1071
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
|
+
|
|
1072
1237
|
// ../../packages/types/src/telemetry/index.ts
|
|
1073
1238
|
var TelemetryEventTypes = {
|
|
1074
1239
|
// Agent activity events (shown in web UI activity log)
|
|
@@ -1157,7 +1322,7 @@ import ora4 from "ora";
|
|
|
1157
1322
|
import { select as select4 } from "@inquirer/prompts";
|
|
1158
1323
|
|
|
1159
1324
|
// src/lib/telemetry.ts
|
|
1160
|
-
var CLI_VERSION = (true ? "3.5.2-dev.
|
|
1325
|
+
var CLI_VERSION = (true ? "3.5.2-dev.1ca1af3" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1161
1326
|
function getCliVersion() {
|
|
1162
1327
|
return CLI_VERSION;
|
|
1163
1328
|
}
|
|
@@ -1889,12 +2054,23 @@ function buildSessionDbRecoveryActivity(record) {
|
|
|
1889
2054
|
metadata: withoutContractFields(record),
|
|
1890
2055
|
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}`
|
|
1891
2056
|
};
|
|
1892
|
-
case "session_db_boot_refused":
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
2057
|
+
case "session_db_boot_refused": {
|
|
2058
|
+
const message = (() => {
|
|
2059
|
+
switch (record.reason) {
|
|
2060
|
+
case "local_discard_failed":
|
|
2061
|
+
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.`;
|
|
2062
|
+
case "replica_separation_unproven":
|
|
2063
|
+
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.`;
|
|
2064
|
+
case "verification_timeout_before_separation":
|
|
2065
|
+
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.";
|
|
2066
|
+
case "verification_timeout_during_separation":
|
|
2067
|
+
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.";
|
|
2068
|
+
default:
|
|
2069
|
+
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.";
|
|
2070
|
+
}
|
|
2071
|
+
})();
|
|
2072
|
+
return { level, metadata: withoutContractFields(record), message };
|
|
2073
|
+
}
|
|
1898
2074
|
case "schema_provenance_mismatch":
|
|
1899
2075
|
return {
|
|
1900
2076
|
level,
|
|
@@ -2144,7 +2320,7 @@ async function getOpenCodeDirectory(port, client) {
|
|
|
2144
2320
|
return null;
|
|
2145
2321
|
}
|
|
2146
2322
|
}
|
|
2147
|
-
function
|
|
2323
|
+
function roleOf2(m) {
|
|
2148
2324
|
if (!m || typeof m !== "object") return void 0;
|
|
2149
2325
|
if (typeof m.role === "string") return m.role;
|
|
2150
2326
|
const infoRole = m.info?.role;
|
|
@@ -2158,13 +2334,13 @@ function createdOf(m) {
|
|
|
2158
2334
|
if (!m || typeof m !== "object") return void 0;
|
|
2159
2335
|
return m.info?.time?.created ?? m.time?.created;
|
|
2160
2336
|
}
|
|
2161
|
-
function
|
|
2337
|
+
function idOf2(m) {
|
|
2162
2338
|
if (!m || typeof m !== "object") return void 0;
|
|
2163
2339
|
if (typeof m.id === "string") return m.id;
|
|
2164
2340
|
const infoId = m.info?.id;
|
|
2165
2341
|
return typeof infoId === "string" ? infoId : void 0;
|
|
2166
2342
|
}
|
|
2167
|
-
function
|
|
2343
|
+
function parentIdOf2(m) {
|
|
2168
2344
|
if (!m || typeof m !== "object") return void 0;
|
|
2169
2345
|
if (typeof m.parentID === "string") return m.parentID;
|
|
2170
2346
|
const infoParent = m.info?.parentID;
|
|
@@ -2180,8 +2356,12 @@ function errorOf(m) {
|
|
|
2180
2356
|
if (!m || typeof m !== "object") return void 0;
|
|
2181
2357
|
return m.info?.error ?? m.error;
|
|
2182
2358
|
}
|
|
2359
|
+
function isTerminalStopAnswer(m) {
|
|
2360
|
+
return completedOf(m) != null && errorOf(m) == null && finishOf(m) === "stop";
|
|
2361
|
+
}
|
|
2183
2362
|
function isAssistantInFlight(m) {
|
|
2184
2363
|
if (completedOf(m) == null) return true;
|
|
2364
|
+
if (errorOf(m) != null) return false;
|
|
2185
2365
|
return finishOf(m) === "tool-calls";
|
|
2186
2366
|
}
|
|
2187
2367
|
async function getSessionMessages(port, sessionId, client) {
|
|
@@ -2229,7 +2409,7 @@ async function pollSessionMessagesForRedrive(port, sessionId, client) {
|
|
|
2229
2409
|
function isSessionActivelyGenerating(messages) {
|
|
2230
2410
|
if (!messages || messages.length === 0) return false;
|
|
2231
2411
|
const last = messages[messages.length - 1];
|
|
2232
|
-
if (
|
|
2412
|
+
if (roleOf2(last) !== "assistant") return false;
|
|
2233
2413
|
return completedOf(last) == null;
|
|
2234
2414
|
}
|
|
2235
2415
|
var FULL_SESSION_ENUMERATION_MAX_SESSIONS = 3200;
|
|
@@ -2538,11 +2718,31 @@ function messageText(m) {
|
|
|
2538
2718
|
if (!m || !Array.isArray(m.parts)) return "";
|
|
2539
2719
|
return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
2540
2720
|
}
|
|
2541
|
-
|
|
2721
|
+
function findDispatchedUserMessageId(messages, content, excludeIds) {
|
|
2722
|
+
if (!messages || messages.length === 0) return null;
|
|
2723
|
+
let best = null;
|
|
2724
|
+
for (const message of messages) {
|
|
2725
|
+
if (roleOf2(message) !== "user") continue;
|
|
2726
|
+
const id = idOf2(message);
|
|
2727
|
+
if (typeof id !== "string" || excludeIds?.has(id)) continue;
|
|
2728
|
+
if (messageText(message) !== content) continue;
|
|
2729
|
+
const created = createdOf(message) ?? 0;
|
|
2730
|
+
if (best === null || created > best.created) best = { id, created };
|
|
2731
|
+
}
|
|
2732
|
+
return best?.id ?? null;
|
|
2733
|
+
}
|
|
2734
|
+
async function sendPromptAsync(port, sessionId, content, options, attachments, client, callbacks) {
|
|
2542
2735
|
const before = await getSessionMessages(port, sessionId, client);
|
|
2543
2736
|
const knownUserIds = new Set(
|
|
2544
|
-
(before ?? []).filter((m) =>
|
|
2737
|
+
(before ?? []).filter((m) => roleOf2(m) === "user").map((m) => idOf2(m)).filter((id) => typeof id === "string")
|
|
2545
2738
|
);
|
|
2739
|
+
if (callbacks) {
|
|
2740
|
+
try {
|
|
2741
|
+
callbacks.onKnownUserIds(before === null ? null : knownUserIds);
|
|
2742
|
+
} catch (error2) {
|
|
2743
|
+
callbacks.onKnownUserIdsError(error2);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2546
2746
|
const parts = [{ type: "text", text: content }];
|
|
2547
2747
|
let pendingOutcomes = null;
|
|
2548
2748
|
if (attachments && attachments.inputs.length > 0) {
|
|
@@ -2581,22 +2781,10 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
|
|
|
2581
2781
|
const READ_BACK_DELAY_MS = 150;
|
|
2582
2782
|
for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
|
|
2583
2783
|
const after = await getSessionMessages(port, sessionId, client);
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
const id = idOf(m);
|
|
2589
|
-
if (typeof id !== "string" || knownUserIds.has(id)) continue;
|
|
2590
|
-
if (messageText(m) !== content) continue;
|
|
2591
|
-
const created = createdOf(m) ?? 0;
|
|
2592
|
-
if (best === null || created > best.created) {
|
|
2593
|
-
best = { id, created };
|
|
2594
|
-
}
|
|
2595
|
-
}
|
|
2596
|
-
if (best) {
|
|
2597
|
-
if (pendingOutcomes && attachments?.onOutcomes) attachments.onOutcomes(pendingOutcomes);
|
|
2598
|
-
return best.id;
|
|
2599
|
-
}
|
|
2784
|
+
const dispatchedUserMessageId = findDispatchedUserMessageId(after, content, knownUserIds);
|
|
2785
|
+
if (dispatchedUserMessageId !== null) {
|
|
2786
|
+
if (pendingOutcomes && attachments?.onOutcomes) attachments.onOutcomes(pendingOutcomes);
|
|
2787
|
+
return dispatchedUserMessageId;
|
|
2600
2788
|
}
|
|
2601
2789
|
if (attempt < READ_BACK_ATTEMPTS - 1) {
|
|
2602
2790
|
await new Promise((resolve4) => setTimeout(resolve4, READ_BACK_DELAY_MS));
|
|
@@ -2607,57 +2795,68 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
|
|
|
2607
2795
|
function findAssistantReplyAfter(messages, userMessageId) {
|
|
2608
2796
|
if (!messages || messages.length === 0) return null;
|
|
2609
2797
|
const byParent = messages.find(
|
|
2610
|
-
(m) =>
|
|
2798
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
|
|
2611
2799
|
);
|
|
2612
2800
|
if (byParent) return byParent;
|
|
2613
|
-
const userIndex = messages.findIndex((m) =>
|
|
2801
|
+
const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
|
|
2614
2802
|
if (userIndex === -1) return null;
|
|
2615
2803
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2616
|
-
if (
|
|
2804
|
+
if (roleOf2(messages[i]) === "assistant") return messages[i];
|
|
2617
2805
|
}
|
|
2618
2806
|
return null;
|
|
2619
2807
|
}
|
|
2620
2808
|
function findLastAssistantReplyFor(messages, userMessageId) {
|
|
2621
2809
|
if (!messages || messages.length === 0) return null;
|
|
2622
2810
|
let lastCorrelated = null;
|
|
2623
|
-
let lastNonErrored = null;
|
|
2624
2811
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2625
2812
|
const m = messages[i];
|
|
2626
|
-
if (
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2813
|
+
if (roleOf2(m) !== "assistant" || parentIdOf2(m) !== userMessageId) continue;
|
|
2814
|
+
lastCorrelated = m;
|
|
2815
|
+
break;
|
|
2816
|
+
}
|
|
2817
|
+
if (lastCorrelated) {
|
|
2818
|
+
if (errorOf(lastCorrelated) == null) return lastCorrelated;
|
|
2819
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2820
|
+
const m = messages[i];
|
|
2821
|
+
if (roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId && isTerminalStopAnswer(m)) {
|
|
2822
|
+
return m;
|
|
2823
|
+
}
|
|
2631
2824
|
}
|
|
2825
|
+
return lastCorrelated;
|
|
2632
2826
|
}
|
|
2633
|
-
|
|
2634
|
-
const userIndex = messages.findIndex((m) => idOf(m) === userMessageId);
|
|
2827
|
+
const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
|
|
2635
2828
|
if (userIndex === -1) return null;
|
|
2636
2829
|
let last = null;
|
|
2637
|
-
let
|
|
2830
|
+
let lastAssistantIndex = -1;
|
|
2638
2831
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2639
|
-
const role =
|
|
2832
|
+
const role = roleOf2(messages[i]);
|
|
2640
2833
|
if (role === "user") break;
|
|
2641
2834
|
if (role === "assistant") {
|
|
2642
2835
|
last = messages[i];
|
|
2643
|
-
|
|
2836
|
+
lastAssistantIndex = i;
|
|
2644
2837
|
}
|
|
2645
2838
|
}
|
|
2646
|
-
|
|
2839
|
+
if (last === null || errorOf(last) == null) return last;
|
|
2840
|
+
for (let i = lastAssistantIndex; i > userIndex; i--) {
|
|
2841
|
+
const m = messages[i];
|
|
2842
|
+
if (roleOf2(m) === "user") break;
|
|
2843
|
+
if (roleOf2(m) === "assistant" && isTerminalStopAnswer(m)) return m;
|
|
2844
|
+
}
|
|
2845
|
+
return last;
|
|
2647
2846
|
}
|
|
2648
2847
|
function collectSubagentSessions(messages, userMessageId) {
|
|
2649
2848
|
if (!messages || messages.length === 0) return [];
|
|
2650
2849
|
const byParent = messages.filter(
|
|
2651
|
-
(message) =>
|
|
2850
|
+
(message) => roleOf2(message) === "assistant" && parentIdOf2(message) === userMessageId
|
|
2652
2851
|
);
|
|
2653
2852
|
const assistants = byParent.length > 0 ? byParent : [];
|
|
2654
2853
|
if (assistants.length === 0) {
|
|
2655
|
-
const userIndex = messages.findIndex((message) =>
|
|
2854
|
+
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
2656
2855
|
if (userIndex === -1) return [];
|
|
2657
2856
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2658
2857
|
const message = messages[i];
|
|
2659
|
-
if (
|
|
2660
|
-
if (
|
|
2858
|
+
if (roleOf2(message) === "user") break;
|
|
2859
|
+
if (roleOf2(message) === "assistant") assistants.push(message);
|
|
2661
2860
|
}
|
|
2662
2861
|
}
|
|
2663
2862
|
const refs = [];
|
|
@@ -2697,7 +2896,7 @@ function collectTaskCalls(messages, userMessageId) {
|
|
|
2697
2896
|
if (!messages || messages.length === 0) return [];
|
|
2698
2897
|
const calls = [];
|
|
2699
2898
|
for (const message of messages) {
|
|
2700
|
-
if (
|
|
2899
|
+
if (roleOf2(message) !== "assistant" || parentIdOf2(message) !== userMessageId) continue;
|
|
2701
2900
|
for (const part of message.parts ?? []) {
|
|
2702
2901
|
if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
|
|
2703
2902
|
continue;
|
|
@@ -2727,7 +2926,7 @@ function attributeTaskCallUsage(messages, windows) {
|
|
|
2727
2926
|
for (const window of eligibleWindows) assignments.set(window.callID, []);
|
|
2728
2927
|
const unattributed = [];
|
|
2729
2928
|
for (const message of messages ?? []) {
|
|
2730
|
-
if (
|
|
2929
|
+
if (roleOf2(message) !== "assistant") continue;
|
|
2731
2930
|
const created = finiteNumber(createdOf(message));
|
|
2732
2931
|
const matching = created === null ? [] : eligibleWindows.filter(
|
|
2733
2932
|
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
@@ -2749,8 +2948,9 @@ function attributeTaskCallUsage(messages, windows) {
|
|
|
2749
2948
|
}
|
|
2750
2949
|
function sumAssistantUsage(messages) {
|
|
2751
2950
|
if (!messages || messages.length === 0) return null;
|
|
2752
|
-
const
|
|
2753
|
-
const
|
|
2951
|
+
const hasTrailingError = errorOf(messages[messages.length - 1]) != null;
|
|
2952
|
+
const hasTerminalStopAnswer = messages.some(isTerminalStopAnswer);
|
|
2953
|
+
const selected = hasTrailingError && hasTerminalStopAnswer ? messages.filter((message) => errorOf(message) == null) : messages;
|
|
2754
2954
|
let sawAnyUsage = false;
|
|
2755
2955
|
let inputSum = 0;
|
|
2756
2956
|
let outputSum = 0;
|
|
@@ -2804,13 +3004,11 @@ function sumAssistantUsage(messages) {
|
|
|
2804
3004
|
function messageUsage(messages, userMessageId) {
|
|
2805
3005
|
if (!messages || messages.length === 0) return null;
|
|
2806
3006
|
const byParentAll = messages.filter(
|
|
2807
|
-
(m) =>
|
|
3007
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
|
|
2808
3008
|
);
|
|
2809
|
-
const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
|
|
2810
|
-
const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
|
|
2811
3009
|
let correlated;
|
|
2812
|
-
if (
|
|
2813
|
-
correlated =
|
|
3010
|
+
if (byParentAll.length > 0) {
|
|
3011
|
+
correlated = byParentAll;
|
|
2814
3012
|
} else {
|
|
2815
3013
|
const reply = findAssistantReplyAfter(messages, userMessageId);
|
|
2816
3014
|
correlated = reply ? [reply] : [];
|
|
@@ -2820,7 +3018,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
2820
3018
|
}
|
|
2821
3019
|
function messageRunState(messages, userMessageId) {
|
|
2822
3020
|
if (!messages || messages.length === 0) return "unknown";
|
|
2823
|
-
const hasUser = messages.some((m) =>
|
|
3021
|
+
const hasUser = messages.some((m) => idOf2(m) === userMessageId);
|
|
2824
3022
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
2825
3023
|
if (!hasUser) {
|
|
2826
3024
|
if (!reply) return "unknown";
|
|
@@ -2856,14 +3054,38 @@ function messageError(messages, userMessageId) {
|
|
|
2856
3054
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
2857
3055
|
const error2 = errorOf(reply);
|
|
2858
3056
|
if (error2 == null) return null;
|
|
2859
|
-
|
|
2860
|
-
if (typeof error2 === "
|
|
3057
|
+
let rendered;
|
|
3058
|
+
if (typeof error2 === "string") {
|
|
3059
|
+
rendered = error2;
|
|
3060
|
+
} else if (typeof error2 === "object") {
|
|
2861
3061
|
const e = error2;
|
|
2862
3062
|
const dataMessage = e.data?.message;
|
|
2863
|
-
if (typeof dataMessage === "string")
|
|
2864
|
-
|
|
3063
|
+
if (typeof dataMessage === "string") {
|
|
3064
|
+
rendered = dataMessage;
|
|
3065
|
+
} else if (typeof e.message === "string") {
|
|
3066
|
+
rendered = e.message;
|
|
3067
|
+
} else {
|
|
3068
|
+
rendered = "The agent run failed.";
|
|
3069
|
+
}
|
|
3070
|
+
} else {
|
|
3071
|
+
rendered = "The agent run failed.";
|
|
2865
3072
|
}
|
|
2866
|
-
|
|
3073
|
+
const toolPart = Array.isArray(reply?.parts) ? reply.parts.find(
|
|
3074
|
+
(part) => part && typeof part === "object" && part.type === "tool" && part.state?.status === "error"
|
|
3075
|
+
) : void 0;
|
|
3076
|
+
return typeof toolPart?.tool === "string" && toolPart.tool.length > 0 ? `${rendered} (tool "${toolPart.tool}")` : rendered;
|
|
3077
|
+
}
|
|
3078
|
+
function describeAbandonedReplyShape(messages, userMessageId) {
|
|
3079
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
3080
|
+
if (!reply) return null;
|
|
3081
|
+
const parts = Array.isArray(reply.parts) ? reply.parts : [];
|
|
3082
|
+
const lastTool = [...parts].reverse().find((part) => part?.type === "tool");
|
|
3083
|
+
return {
|
|
3084
|
+
partTypes: parts.slice(-12).map((part) => typeof part?.type === "string" ? part.type : "(unknown)"),
|
|
3085
|
+
droppedPartCount: Math.max(0, parts.length - 12),
|
|
3086
|
+
lastToolName: typeof lastTool?.tool === "string" && lastTool.tool.length > 0 ? lastTool.tool : null,
|
|
3087
|
+
lastToolStatus: typeof lastTool?.state?.status === "string" && lastTool.state.status.length > 0 ? lastTool.state.status : null
|
|
3088
|
+
};
|
|
2867
3089
|
}
|
|
2868
3090
|
function isAbortedTerminalReply(messages, userMessageId) {
|
|
2869
3091
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
@@ -2912,7 +3134,7 @@ function findLatestSubagentAuthOutcome(messages, sinceMs) {
|
|
|
2912
3134
|
if (!messages || messages.length === 0) return null;
|
|
2913
3135
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2914
3136
|
const message = messages[i];
|
|
2915
|
-
if (
|
|
3137
|
+
if (roleOf2(message) !== "assistant") continue;
|
|
2916
3138
|
const created = createdOf(message);
|
|
2917
3139
|
if (sinceMs !== null && typeof created === "number" && created < sinceMs) continue;
|
|
2918
3140
|
const failure = classifyReplyAuthError(message);
|
|
@@ -2944,25 +3166,25 @@ function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProvi
|
|
|
2944
3166
|
function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
2945
3167
|
if (!messages || messages.length === 0) return false;
|
|
2946
3168
|
return messages.some(
|
|
2947
|
-
(m) =>
|
|
3169
|
+
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
2948
3170
|
);
|
|
2949
3171
|
}
|
|
2950
3172
|
function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
|
|
2951
3173
|
if (!messages || messages.length === 0) return false;
|
|
2952
|
-
const userIndex = messages.findIndex((message) =>
|
|
3174
|
+
const userIndex = messages.findIndex((message) => idOf2(message) === userMessageId);
|
|
2953
3175
|
if (userIndex === -1) return false;
|
|
2954
3176
|
let hasLaterUser = false;
|
|
2955
3177
|
let hasStartedLaterUser = false;
|
|
2956
3178
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2957
3179
|
const message = messages[i];
|
|
2958
|
-
if (
|
|
3180
|
+
if (roleOf2(message) !== "user") continue;
|
|
2959
3181
|
hasLaterUser = true;
|
|
2960
|
-
const laterUserMessageId =
|
|
3182
|
+
const laterUserMessageId = idOf2(message);
|
|
2961
3183
|
if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
|
|
2962
3184
|
return false;
|
|
2963
3185
|
}
|
|
2964
3186
|
if (messages.some(
|
|
2965
|
-
(candidate) =>
|
|
3187
|
+
(candidate) => roleOf2(candidate) === "assistant" && parentIdOf2(candidate) === laterUserMessageId
|
|
2966
3188
|
)) {
|
|
2967
3189
|
hasStartedLaterUser = true;
|
|
2968
3190
|
}
|
|
@@ -4147,9 +4369,12 @@ function runSynchroniser(args, opts) {
|
|
|
4147
4369
|
|
|
4148
4370
|
// src/lib/opencode/session-db-boot.ts
|
|
4149
4371
|
var SESSION_DB_RESTORE_TIMEOUT_MS = 3e5;
|
|
4150
|
-
var SESSION_DB_VERIFY_TIMEOUT_MS =
|
|
4372
|
+
var SESSION_DB_VERIFY_TIMEOUT_MS = 36e4;
|
|
4151
4373
|
var SESSION_DB_SYNCHRONISER_TIMEOUT_MS = 12e4;
|
|
4152
|
-
var SESSION_DB_VERIFY_WALKBACK_BUDGET_SECONDS =
|
|
4374
|
+
var SESSION_DB_VERIFY_WALKBACK_BUDGET_SECONDS = 40;
|
|
4375
|
+
var SESSION_DB_VERIFY_RESTORE_POINTS_TIMEOUT_SECONDS = 30;
|
|
4376
|
+
var SESSION_DB_VERIFY_RESTORE_CANDIDATE_TIMEOUT_SECONDS = 120;
|
|
4377
|
+
var SESSION_DB_VERIFY_DISPOSAL_BUDGET_SECONDS = 90;
|
|
4153
4378
|
function commandError(result) {
|
|
4154
4379
|
return result.stderr.trim() || `process exited with code ${result.code ?? "null"}`;
|
|
4155
4380
|
}
|
|
@@ -4460,20 +4685,37 @@ async function verifySessionDb(options, configPath, env) {
|
|
|
4460
4685
|
timeoutMs: SESSION_DB_VERIFY_TIMEOUT_MS,
|
|
4461
4686
|
env: {
|
|
4462
4687
|
...env,
|
|
4463
|
-
// The synchroniser reads
|
|
4464
|
-
// 120_000, so the walkback gives up before the outer process bound.
|
|
4688
|
+
// The synchroniser reads all four values in SECONDS.
|
|
4465
4689
|
EVIDENT_SESSION_DB_WALKBACK_BUDGET_SECONDS: String(
|
|
4466
4690
|
SESSION_DB_VERIFY_WALKBACK_BUDGET_SECONDS
|
|
4691
|
+
),
|
|
4692
|
+
EVIDENT_SESSION_DB_RESTORE_POINTS_TIMEOUT_SECONDS: String(
|
|
4693
|
+
SESSION_DB_VERIFY_RESTORE_POINTS_TIMEOUT_SECONDS
|
|
4694
|
+
),
|
|
4695
|
+
EVIDENT_SESSION_DB_RESTORE_CANDIDATE_TIMEOUT_SECONDS: String(
|
|
4696
|
+
SESSION_DB_VERIFY_RESTORE_CANDIDATE_TIMEOUT_SECONDS
|
|
4697
|
+
),
|
|
4698
|
+
EVIDENT_SESSION_DB_DISPOSAL_BUDGET_SECONDS: String(
|
|
4699
|
+
SESSION_DB_VERIFY_DISPOSAL_BUDGET_SECONDS
|
|
4467
4700
|
)
|
|
4468
4701
|
}
|
|
4469
4702
|
});
|
|
4470
4703
|
logSynchroniserDiagnostics(result, options);
|
|
4471
4704
|
if (result.timedOut || result.code === 124 || result.code === 137) {
|
|
4705
|
+
if (result.stderr.includes("SESSION-DB-INTEGRITY-FAILED")) {
|
|
4706
|
+
const reason = result.stderr.includes("SESSION-DB-REPLICA-SIZE") ? "verification_timeout_during_separation" : "verification_timeout_before_separation";
|
|
4707
|
+
options.log(
|
|
4708
|
+
`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})`,
|
|
4709
|
+
"error"
|
|
4710
|
+
);
|
|
4711
|
+
reportRecord("verify", "session_db_boot_refused", reason, null, options);
|
|
4712
|
+
return { verifyFatal: true, verifyFatalReason: reason };
|
|
4713
|
+
}
|
|
4472
4714
|
options.log(
|
|
4473
4715
|
`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`,
|
|
4474
4716
|
"warn"
|
|
4475
4717
|
);
|
|
4476
|
-
return false;
|
|
4718
|
+
return { verifyFatal: false };
|
|
4477
4719
|
}
|
|
4478
4720
|
if (result.code === 34) {
|
|
4479
4721
|
reportRecord(
|
|
@@ -4483,14 +4725,14 @@ async function verifySessionDb(options, configPath, env) {
|
|
|
4483
4725
|
null,
|
|
4484
4726
|
options
|
|
4485
4727
|
);
|
|
4486
|
-
return true;
|
|
4728
|
+
return { verifyFatal: true };
|
|
4487
4729
|
}
|
|
4488
4730
|
if (result.code === 33) {
|
|
4489
4731
|
options.log(
|
|
4490
4732
|
"SESSION-DB-INTEGRITY-EXHAUSTED: booting continues and litestream still replicates, starting an empty backup chain after the corrupt replica was separated.",
|
|
4491
4733
|
"warn"
|
|
4492
4734
|
);
|
|
4493
|
-
return false;
|
|
4735
|
+
return { verifyFatal: false };
|
|
4494
4736
|
}
|
|
4495
4737
|
if (result.code !== 0) {
|
|
4496
4738
|
options.log(
|
|
@@ -4498,13 +4740,13 @@ async function verifySessionDb(options, configPath, env) {
|
|
|
4498
4740
|
"warn"
|
|
4499
4741
|
);
|
|
4500
4742
|
}
|
|
4501
|
-
return false;
|
|
4743
|
+
return { verifyFatal: false };
|
|
4502
4744
|
}
|
|
4503
4745
|
options.log(
|
|
4504
4746
|
"skipping session-DB verification: this boot's session DB was not proven safe to replicate",
|
|
4505
4747
|
"debug"
|
|
4506
4748
|
);
|
|
4507
|
-
return false;
|
|
4749
|
+
return { verifyFatal: false };
|
|
4508
4750
|
}
|
|
4509
4751
|
function fileExists(path) {
|
|
4510
4752
|
try {
|
|
@@ -4591,7 +4833,7 @@ async function restoreAndVerifySessionDb(options) {
|
|
|
4591
4833
|
if (options.noReplicateMarker && fileExists(options.noReplicateMarker)) {
|
|
4592
4834
|
return { verifyFatal: false };
|
|
4593
4835
|
}
|
|
4594
|
-
return
|
|
4836
|
+
return verifySessionDb(options, configPath, env);
|
|
4595
4837
|
}
|
|
4596
4838
|
|
|
4597
4839
|
// src/lib/opencode/session-db-provenance.ts
|
|
@@ -6843,6 +7085,9 @@ var SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS = 2e3;
|
|
|
6843
7085
|
var SESSION_ERROR_STREAM_HEALTHY_MS = 5e3;
|
|
6844
7086
|
var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
6845
7087
|
var MAX_BUFFERED_SESSION_ERRORS = 256;
|
|
7088
|
+
var SESSION_ERROR_RETENTION_TTL_MS = 6e4;
|
|
7089
|
+
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.";
|
|
7090
|
+
var SESSION_ERROR_RETAINED_DETAIL = "The runner's session database could not be written, so this turn stopped part-way. Runner-observed session error: ";
|
|
6846
7091
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
6847
7092
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
6848
7093
|
var DRIVER_SESSION_ENUMERATION_LIMIT = 20;
|
|
@@ -7000,6 +7245,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7000
7245
|
* with a short TTL so an unmatched session cannot retain an event indefinitely.
|
|
7001
7246
|
*/
|
|
7002
7247
|
bufferedSessionErrors = /* @__PURE__ */ new Map();
|
|
7248
|
+
retainedSessionErrors = /* @__PURE__ */ new Map();
|
|
7003
7249
|
/**
|
|
7004
7250
|
* AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
|
|
7005
7251
|
* dispatched and are still in-flight. A message in this set is never
|
|
@@ -7032,10 +7278,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7032
7278
|
* row is `processing` or `pending`; clear it only when absent from both lists.
|
|
7033
7279
|
*/
|
|
7034
7280
|
untrackableAck = /* @__PURE__ */ new Map();
|
|
7281
|
+
/** Session binding used by the first untrackable-ack report, for retries. */
|
|
7282
|
+
untrackableAckSessionIds = /* @__PURE__ */ new Map();
|
|
7035
7283
|
/** Messages whose untrackable-ack failure signal was emitted while fenced. */
|
|
7036
7284
|
untrackableAckSignalled = /* @__PURE__ */ new Set();
|
|
7037
7285
|
/** Messages whose first untrackable-ack fence warning was already logged. */
|
|
7038
7286
|
untrackableAckWarned = /* @__PURE__ */ new Set();
|
|
7287
|
+
/**
|
|
7288
|
+
* Accepted V1 prompts whose assigned ids were not read back; these rows are
|
|
7289
|
+
* re-read instead of POSTed again. A null snapshot cannot safely exclude an older
|
|
7290
|
+
* identical-text turn, so it never produces a correlation.
|
|
7291
|
+
*/
|
|
7292
|
+
awaitingCorrelation = /* @__PURE__ */ new Map();
|
|
7039
7293
|
/** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
|
|
7040
7294
|
pendingMessageIds = /* @__PURE__ */ new Set();
|
|
7041
7295
|
/**
|
|
@@ -7121,21 +7375,6 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7121
7375
|
* re-signal on every one of the 15h of re-dispatch attempts #1110 made.
|
|
7122
7376
|
*/
|
|
7123
7377
|
dispatchNotStartedSignalled = /* @__PURE__ */ new Map();
|
|
7124
|
-
/**
|
|
7125
|
-
* Consecutive-UNCONFIRMED-dispatch streak for a `pending` row with NO stored
|
|
7126
|
-
* `opencode_message_id` yet — i.e. one that has never even reached the
|
|
7127
|
-
* re-drive fence above. `sendPromptAsync`'s POST may 2xx, but its own
|
|
7128
|
-
* read-back retries can never confirm the assigned id when the session's
|
|
7129
|
-
* message list is PERMANENTLY unreadable (e.g. a corrupted local opencode
|
|
7130
|
-
* SQLite DB, #1345/#1348's exact fault, just hit BEFORE the row is ever
|
|
7131
|
-
* dispatched instead of after). Unlike an already-dispatched row, THIS row has
|
|
7132
|
-
* no other safety net at all: the lifecycle cron only reclaims `status =
|
|
7133
|
-
* 'processing'` rows, and a row stuck here never reaches `processing`. Keyed
|
|
7134
|
-
* by message id, carrying `sessionId` so a session change (a fresh one bound
|
|
7135
|
-
* after abandonment) starts a new streak rather than inheriting the old
|
|
7136
|
-
* session's count — same shape as `redrivePollFailures` above.
|
|
7137
|
-
*/
|
|
7138
|
-
unconfirmedDispatchFailures = /* @__PURE__ */ new Map();
|
|
7139
7378
|
/**
|
|
7140
7379
|
* Consecutive give-ups for a message whose OpenCode user row remains queued.
|
|
7141
7380
|
* The session id scopes the count: a fresh session is a new attempt after a
|
|
@@ -7153,8 +7392,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7153
7392
|
* short-circuits while it is present, so a null-id row is re-dispatched AT MOST
|
|
7154
7393
|
* ONCE per outstanding read-back. Cleared on a SUCCESSFUL dispatch+read-back (the
|
|
7155
7394
|
* row is then tracked in `dispatched`, so `readoptOne`'s early skip prevents
|
|
7156
|
-
* re-entry) OR on a failed/unresolved dispatch (the
|
|
7157
|
-
*
|
|
7395
|
+
* re-entry) OR on a failed/unresolved dispatch (the accepted prompt is handed to
|
|
7396
|
+
* the terminal-report fence and is never sent again).
|
|
7158
7397
|
*/
|
|
7159
7398
|
awaitingReadopt = /* @__PURE__ */ new Set();
|
|
7160
7399
|
/**
|
|
@@ -7472,6 +7711,49 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7472
7711
|
}
|
|
7473
7712
|
return ids;
|
|
7474
7713
|
}
|
|
7714
|
+
/** Find an accepted prompt's user row without adopting an older or concurrent turn. */
|
|
7715
|
+
findAwaitingCorrelationId(entry, sessionId, messages) {
|
|
7716
|
+
if (entry.sessionId !== sessionId || entry.knownUserIds === null) return null;
|
|
7717
|
+
const excludeIds = new Set(entry.knownUserIds);
|
|
7718
|
+
for (const inFlight of this.watchers.get(entry.sessionId)?.inFlight.values() ?? []) {
|
|
7719
|
+
excludeIds.add(inFlight.opencodeMessageId);
|
|
7720
|
+
}
|
|
7721
|
+
return findDispatchedUserMessageId(messages, entry.content, excludeIds);
|
|
7722
|
+
}
|
|
7723
|
+
/** Hand an exhausted accepted-but-untrackable prompt to the terminal-report fence. */
|
|
7724
|
+
async handoffUntrackableCorrelation(conversationId, messageId, sessionId, attempts, source) {
|
|
7725
|
+
this.awaitingCorrelation.delete(messageId);
|
|
7726
|
+
this.awaitingReadopt.delete(messageId);
|
|
7727
|
+
this.sessions.delete(conversationId);
|
|
7728
|
+
this.supersede(conversationId, sessionId);
|
|
7729
|
+
const dispatchDescription = source === "readopt" ? "re-dispatched message" : "message";
|
|
7730
|
+
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.`;
|
|
7731
|
+
this.untrackableAck.set(messageId, errorMessage3);
|
|
7732
|
+
this.untrackableAckSessionIds.set(messageId, null);
|
|
7733
|
+
this.log({
|
|
7734
|
+
level: "error",
|
|
7735
|
+
message: errorMessage3,
|
|
7736
|
+
conversation_id: conversationId,
|
|
7737
|
+
message_id: messageId
|
|
7738
|
+
});
|
|
7739
|
+
const reportError = await this.reportUntrackableAck(
|
|
7740
|
+
conversationId,
|
|
7741
|
+
messageId,
|
|
7742
|
+
null,
|
|
7743
|
+
errorMessage3
|
|
7744
|
+
);
|
|
7745
|
+
if (reportError !== null) {
|
|
7746
|
+
this.log({
|
|
7747
|
+
level: "warn",
|
|
7748
|
+
message: `markFailed PATCH for message ${messageId.slice(0, 8)} (conversation ${conversationId.slice(0, 8)}) failed: ${reportError.message}`,
|
|
7749
|
+
conversation_id: conversationId,
|
|
7750
|
+
message_id: messageId
|
|
7751
|
+
});
|
|
7752
|
+
}
|
|
7753
|
+
if (source === "readopt") {
|
|
7754
|
+
void this.postSignal(conversationId, messageId, "readopt_orphan_unsent");
|
|
7755
|
+
}
|
|
7756
|
+
}
|
|
7475
7757
|
/**
|
|
7476
7758
|
* File-pull work, for `run.ts`'s idle accounting (#559).
|
|
7477
7759
|
*
|
|
@@ -7651,6 +7933,56 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7651
7933
|
}
|
|
7652
7934
|
break;
|
|
7653
7935
|
}
|
|
7936
|
+
const correlation = this.awaitingCorrelation.get(message.id);
|
|
7937
|
+
if (correlation) {
|
|
7938
|
+
correlation.attempts += 1;
|
|
7939
|
+
let correlatedId = null;
|
|
7940
|
+
if (correlation.knownUserIds !== null && correlation.sessionId === sessionId) {
|
|
7941
|
+
const polled = await this.pollSessionMessagesForRedrive(
|
|
7942
|
+
conv,
|
|
7943
|
+
message,
|
|
7944
|
+
correlation.sessionId
|
|
7945
|
+
);
|
|
7946
|
+
if (polled.ok)
|
|
7947
|
+
correlatedId = this.findAwaitingCorrelationId(correlation, sessionId, polled.messages);
|
|
7948
|
+
} else if (correlation.knownUserIds === null) {
|
|
7949
|
+
this.log({
|
|
7950
|
+
level: "debug",
|
|
7951
|
+
message: `Message ${message.id.slice(0, 8)} cannot be correlated because its pre-dispatch session list was unreadable`,
|
|
7952
|
+
conversation_id: conv.id,
|
|
7953
|
+
message_id: message.id
|
|
7954
|
+
});
|
|
7955
|
+
}
|
|
7956
|
+
if (correlatedId !== null) {
|
|
7957
|
+
this.awaitingCorrelation.delete(message.id);
|
|
7958
|
+
this.dispatchNotStartedSignalled.delete(message.id);
|
|
7959
|
+
this.releasedOpencodeIds.delete(message.id);
|
|
7960
|
+
this.dispatched.add(message.id);
|
|
7961
|
+
this.registerInFlight(conv, correlation.sessionId, message, correlatedId);
|
|
7962
|
+
dispatched += 1;
|
|
7963
|
+
this.log({
|
|
7964
|
+
level: "info",
|
|
7965
|
+
message: `Matched accepted message ${message.id.slice(0, 8)} to OpenCode turn ${correlatedId.slice(0, 8)} without sending it again`,
|
|
7966
|
+
conversation_id: conv.id,
|
|
7967
|
+
message_id: message.id
|
|
7968
|
+
});
|
|
7969
|
+
void this.postSignal(conv.id, message.id, "ack_correlated", {
|
|
7970
|
+
opencode_message_id: correlatedId
|
|
7971
|
+
});
|
|
7972
|
+
continue;
|
|
7973
|
+
}
|
|
7974
|
+
if (correlation.attempts >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
|
|
7975
|
+
await this.handoffUntrackableCorrelation(
|
|
7976
|
+
conv.id,
|
|
7977
|
+
message.id,
|
|
7978
|
+
correlation.sessionId,
|
|
7979
|
+
correlation.attempts,
|
|
7980
|
+
"fresh"
|
|
7981
|
+
);
|
|
7982
|
+
break;
|
|
7983
|
+
}
|
|
7984
|
+
break;
|
|
7985
|
+
}
|
|
7654
7986
|
const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
|
|
7655
7987
|
if (effectiveOpencodeMessageId) {
|
|
7656
7988
|
const outcome = await this.resolveRedrive(
|
|
@@ -7672,6 +8004,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7672
8004
|
model: message.opencode_model ?? void 0
|
|
7673
8005
|
};
|
|
7674
8006
|
let opencodeMessageId;
|
|
8007
|
+
let preDispatchUserIds = null;
|
|
7675
8008
|
try {
|
|
7676
8009
|
this.log({
|
|
7677
8010
|
level: "info",
|
|
@@ -7701,7 +8034,20 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7701
8034
|
message.content,
|
|
7702
8035
|
options,
|
|
7703
8036
|
sendAttachments,
|
|
7704
|
-
this.openCodeClient
|
|
8037
|
+
this.openCodeClient,
|
|
8038
|
+
{
|
|
8039
|
+
onKnownUserIds: (ids) => {
|
|
8040
|
+
preDispatchUserIds = ids;
|
|
8041
|
+
},
|
|
8042
|
+
onKnownUserIdsError: (error2) => {
|
|
8043
|
+
this.log({
|
|
8044
|
+
level: "warn",
|
|
8045
|
+
message: `Pre-dispatch snapshot callback failed for message ${message.id.slice(0, 8)}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
8046
|
+
conversation_id: conv.id,
|
|
8047
|
+
message_id: message.id
|
|
8048
|
+
});
|
|
8049
|
+
}
|
|
8050
|
+
}
|
|
7705
8051
|
)
|
|
7706
8052
|
);
|
|
7707
8053
|
} catch (err) {
|
|
@@ -7709,6 +8055,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7709
8055
|
if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
|
|
7710
8056
|
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
7711
8057
|
this.untrackableAck.set(message.id, errorMessage4);
|
|
8058
|
+
this.untrackableAckSessionIds.set(message.id, null);
|
|
7712
8059
|
this.log({
|
|
7713
8060
|
level: "error",
|
|
7714
8061
|
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
|
|
@@ -7784,39 +8131,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7784
8131
|
if (this.isV2) {
|
|
7785
8132
|
throw new Error("V2 prompt dispatch completed without an acknowledged message id");
|
|
7786
8133
|
}
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
message_id: message.id
|
|
7794
|
-
});
|
|
7795
|
-
this.signalDispatchNotStarted(conv, message, "readback_unconfirmed");
|
|
7796
|
-
continue;
|
|
7797
|
-
}
|
|
7798
|
-
this.unconfirmedDispatchFailures.delete(message.id);
|
|
7799
|
-
this.sessions.delete(conv.id);
|
|
7800
|
-
this.supersede(conv.id, sessionId);
|
|
7801
|
-
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.`;
|
|
8134
|
+
this.awaitingCorrelation.set(message.id, {
|
|
8135
|
+
sessionId,
|
|
8136
|
+
content: message.content,
|
|
8137
|
+
attempts: 0,
|
|
8138
|
+
knownUserIds: preDispatchUserIds
|
|
8139
|
+
});
|
|
7802
8140
|
this.log({
|
|
7803
|
-
level: "
|
|
7804
|
-
message:
|
|
8141
|
+
level: "warn",
|
|
8142
|
+
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`,
|
|
7805
8143
|
conversation_id: conv.id,
|
|
7806
8144
|
message_id: message.id
|
|
7807
8145
|
});
|
|
7808
|
-
|
|
7809
|
-
this.log({
|
|
7810
|
-
level: "warn",
|
|
7811
|
-
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)}`,
|
|
7812
|
-
conversation_id: conv.id,
|
|
7813
|
-
message_id: message.id
|
|
7814
|
-
});
|
|
7815
|
-
this.signalDispatchNotStarted(conv, message, "abandon_unreported");
|
|
7816
|
-
});
|
|
8146
|
+
this.signalDispatchNotStarted(conv, message, "readback_unconfirmed");
|
|
7817
8147
|
break;
|
|
7818
8148
|
}
|
|
7819
|
-
this.unconfirmedDispatchFailures.delete(message.id);
|
|
7820
8149
|
this.dispatchNotStartedSignalled.delete(message.id);
|
|
7821
8150
|
this.releasedOpencodeIds.delete(message.id);
|
|
7822
8151
|
this.dispatched.add(message.id);
|
|
@@ -8421,23 +8750,6 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8421
8750
|
this.redrivePollFailures.set(messageId, { sessionId, signature, count: 1 });
|
|
8422
8751
|
return 1;
|
|
8423
8752
|
}
|
|
8424
|
-
/**
|
|
8425
|
-
* Record one UNCONFIRMED-dispatch outcome (a `pending` row with no stored
|
|
8426
|
-
* `opencode_message_id` whose `sendPromptAsync` returned `null`) toward the
|
|
8427
|
-
* bound in `processConversation`'s dispatch loop, and return the resulting
|
|
8428
|
-
* count. Mirrors `recordRedrivePollFailure`'s session-scoping: a session
|
|
8429
|
-
* change starts a fresh streak at `1` rather than inheriting the old one's
|
|
8430
|
-
* count, since a new session is a genuinely different attempt.
|
|
8431
|
-
*/
|
|
8432
|
-
recordUnconfirmedDispatch(messageId, sessionId) {
|
|
8433
|
-
const existing = this.unconfirmedDispatchFailures.get(messageId);
|
|
8434
|
-
if (existing && existing.sessionId === sessionId) {
|
|
8435
|
-
existing.count += 1;
|
|
8436
|
-
return existing.count;
|
|
8437
|
-
}
|
|
8438
|
-
this.unconfirmedDispatchFailures.set(messageId, { sessionId, count: 1 });
|
|
8439
|
-
return 1;
|
|
8440
|
-
}
|
|
8441
8753
|
/** Record one queued give-up toward the never-started bound. */
|
|
8442
8754
|
recordNeverStartedAttempt(messageId, sessionId) {
|
|
8443
8755
|
const existing = this.neverStartedAttempts.get(messageId);
|
|
@@ -8749,6 +9061,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8749
9061
|
b2LastDescendantCheckMs: 0,
|
|
8750
9062
|
b2RootOngoingHeldLogged: false,
|
|
8751
9063
|
b2AbandonedSignalled: false,
|
|
9064
|
+
b2AbandonedWithoutAnswer: false,
|
|
8752
9065
|
ambiguousPinnedSinceMs: 0,
|
|
8753
9066
|
ambiguousResolved: false,
|
|
8754
9067
|
sessionStoreFailurePending: false,
|
|
@@ -8773,6 +9086,28 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8773
9086
|
this.bufferedSessionErrors.delete(oldest);
|
|
8774
9087
|
}
|
|
8775
9088
|
}
|
|
9089
|
+
retainSessionError(event) {
|
|
9090
|
+
this.retainedSessionErrors.delete(event.sessionId);
|
|
9091
|
+
this.retainedSessionErrors.set(event.sessionId, {
|
|
9092
|
+
reason: event.reason,
|
|
9093
|
+
observedAt: this.now()
|
|
9094
|
+
});
|
|
9095
|
+
while (this.retainedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
|
|
9096
|
+
const oldest = this.retainedSessionErrors.keys().next().value;
|
|
9097
|
+
if (typeof oldest !== "string") break;
|
|
9098
|
+
this.retainedSessionErrors.delete(oldest);
|
|
9099
|
+
}
|
|
9100
|
+
}
|
|
9101
|
+
readRetainedSessionError(sessionId) {
|
|
9102
|
+
const retained = this.retainedSessionErrors.get(sessionId);
|
|
9103
|
+
if (!retained) return null;
|
|
9104
|
+
if (this.now() - retained.observedAt >= SESSION_ERROR_RETENTION_TTL_MS) {
|
|
9105
|
+
this.retainedSessionErrors.delete(sessionId);
|
|
9106
|
+
return null;
|
|
9107
|
+
}
|
|
9108
|
+
this.retainedSessionErrors.delete(sessionId);
|
|
9109
|
+
return retained.reason;
|
|
9110
|
+
}
|
|
8776
9111
|
/**
|
|
8777
9112
|
* Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
|
|
8778
9113
|
* EQUAL (#1618) so a watcher whose loop has not started ticking yet is never
|
|
@@ -8868,6 +9203,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8868
9203
|
b2LastDescendantCheckMs: 0,
|
|
8869
9204
|
b2RootOngoingHeldLogged: false,
|
|
8870
9205
|
b2AbandonedSignalled: false,
|
|
9206
|
+
b2AbandonedWithoutAnswer: false,
|
|
8871
9207
|
ambiguousPinnedSinceMs: 0,
|
|
8872
9208
|
ambiguousResolved: false,
|
|
8873
9209
|
sessionStoreFailurePending: false,
|
|
@@ -9098,9 +9434,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9098
9434
|
return;
|
|
9099
9435
|
}
|
|
9100
9436
|
if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
|
|
9437
|
+
this.retainSessionError(event);
|
|
9101
9438
|
this.log({
|
|
9102
|
-
level: "
|
|
9103
|
-
message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
|
|
9439
|
+
level: "warn",
|
|
9440
|
+
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}`,
|
|
9104
9441
|
conversation_id: watcher.conv.id
|
|
9105
9442
|
});
|
|
9106
9443
|
return;
|
|
@@ -9130,9 +9467,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9130
9467
|
const messages = await this.getSessionMessages(event.sessionId);
|
|
9131
9468
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
9132
9469
|
if (state !== "queued") {
|
|
9470
|
+
this.retainSessionError(event);
|
|
9133
9471
|
this.log({
|
|
9134
|
-
level: "
|
|
9135
|
-
message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state};
|
|
9472
|
+
level: "warn",
|
|
9473
|
+
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}`,
|
|
9136
9474
|
conversation_id: watcher.conv.id,
|
|
9137
9475
|
message_id: inFlight.evidentMessageId
|
|
9138
9476
|
});
|
|
@@ -9361,7 +9699,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9361
9699
|
if (state === "failed") {
|
|
9362
9700
|
this.anchorDeliveryDeadline(inFlight);
|
|
9363
9701
|
if (!inFlight.done) {
|
|
9364
|
-
const
|
|
9702
|
+
const messageErrorText = messageError(messages, inFlight.opencodeMessageId);
|
|
9703
|
+
const retainedReason = this.readRetainedSessionError(sessionId);
|
|
9704
|
+
const retainedError = retainedReason === null ? null : `${SESSION_ERROR_RETAINED_DETAIL}${retainedReason}`;
|
|
9705
|
+
const storeError = this.sessionStoreHealth?.isUnwritable() ? SESSION_STORE_UNWRITABLE_ERROR : null;
|
|
9706
|
+
const runnerDetails = [retainedError, storeError].filter(
|
|
9707
|
+
(detail) => detail !== null
|
|
9708
|
+
);
|
|
9709
|
+
const error2 = messageErrorText === null ? runnerDetails.length === 0 ? void 0 : runnerDetails.join(" ") : [messageErrorText, ...runnerDetails].join(" ");
|
|
9365
9710
|
this.log({
|
|
9366
9711
|
level: "error",
|
|
9367
9712
|
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} errored \u2014 marking failed: ${error2 ?? "(no error text)"}`,
|
|
@@ -9447,10 +9792,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9447
9792
|
inFlight.b2LastDescendantCheckMs = 0;
|
|
9448
9793
|
inFlight.b2RootOngoingHeldLogged = false;
|
|
9449
9794
|
inFlight.b2AbandonedSignalled = false;
|
|
9795
|
+
inFlight.b2AbandonedWithoutAnswer = false;
|
|
9450
9796
|
}
|
|
9451
9797
|
} else {
|
|
9452
9798
|
if (inFlight.b2AbandonedSignalled) {
|
|
9453
|
-
|
|
9799
|
+
if (inFlight.b2AbandonedWithoutAnswer) {
|
|
9800
|
+
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
9801
|
+
await this.settleMessageAbandonedWithoutAnswer(
|
|
9802
|
+
sessionId,
|
|
9803
|
+
watcher,
|
|
9804
|
+
inFlight,
|
|
9805
|
+
messages,
|
|
9806
|
+
reply?.info?.finish ?? reply?.finish
|
|
9807
|
+
);
|
|
9808
|
+
} else {
|
|
9809
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
9810
|
+
}
|
|
9454
9811
|
return;
|
|
9455
9812
|
}
|
|
9456
9813
|
if (inFlight.b2PinnedSinceMs === 0) inFlight.b2PinnedSinceMs = this.now();
|
|
@@ -9478,16 +9835,36 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9478
9835
|
return;
|
|
9479
9836
|
}
|
|
9480
9837
|
inFlight.b2AbandonedSignalled = true;
|
|
9838
|
+
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
9839
|
+
const finish = reply?.info?.finish ?? reply?.finish;
|
|
9840
|
+
if (extractReplyTextForMessage(messages, inFlight.opencodeMessageId) === null) {
|
|
9841
|
+
inFlight.b2AbandonedWithoutAnswer = true;
|
|
9842
|
+
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
9843
|
+
watched_for_ms: pinnedForMs,
|
|
9844
|
+
run_state: "failed",
|
|
9845
|
+
finish,
|
|
9846
|
+
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
9847
|
+
...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
|
|
9848
|
+
opencode_message_id: inFlight.opencodeMessageId
|
|
9849
|
+
});
|
|
9850
|
+
await this.settleMessageAbandonedWithoutAnswer(
|
|
9851
|
+
sessionId,
|
|
9852
|
+
watcher,
|
|
9853
|
+
inFlight,
|
|
9854
|
+
messages,
|
|
9855
|
+
finish
|
|
9856
|
+
);
|
|
9857
|
+
return;
|
|
9858
|
+
}
|
|
9481
9859
|
this.log({
|
|
9482
9860
|
level: "warn",
|
|
9483
9861
|
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`,
|
|
9484
9862
|
conversation_id: conv.id,
|
|
9485
9863
|
message_id: id
|
|
9486
9864
|
});
|
|
9487
|
-
const reply = findLastAssistantReplyFor(messages, inFlight.opencodeMessageId);
|
|
9488
9865
|
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
9489
9866
|
watched_for_ms: pinnedForMs,
|
|
9490
|
-
finish
|
|
9867
|
+
finish,
|
|
9491
9868
|
...rootOngoing != null ? { root_ongoing: rootOngoing } : {},
|
|
9492
9869
|
...descendantOngoing != null ? { descendant_ongoing: descendantOngoing } : {},
|
|
9493
9870
|
opencode_message_id: inFlight.opencodeMessageId
|
|
@@ -9769,6 +10146,76 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9769
10146
|
}
|
|
9770
10147
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
9771
10148
|
}
|
|
10149
|
+
/** Report a confirmed b2 abandonment that produced no answer text. */
|
|
10150
|
+
async settleMessageAbandonedWithoutAnswer(sessionId, watcher, inFlight, messages, finish) {
|
|
10151
|
+
const conv = watcher.conv;
|
|
10152
|
+
const legacyError = `OpenCode ended this turn after a tool call (finish="${finish ?? "(unknown)"}") 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.`;
|
|
10153
|
+
const replyShape = describeAbandonedReplyShape(messages, inFlight.opencodeMessageId);
|
|
10154
|
+
const error2 = replyShape === null ? legacyError : `${legacyError} ${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)` : ""}.`;
|
|
10155
|
+
this.anchorDeliveryDeadline(inFlight);
|
|
10156
|
+
this.log({
|
|
10157
|
+
level: "warn",
|
|
10158
|
+
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} was abandoned without an answer \u2014 ${error2}`,
|
|
10159
|
+
conversation_id: conv.id,
|
|
10160
|
+
message_id: inFlight.evidentMessageId
|
|
10161
|
+
});
|
|
10162
|
+
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
10163
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
10164
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
10165
|
+
messages,
|
|
10166
|
+
inFlight.opencodeMessageId,
|
|
10167
|
+
inFlight.evidentMessageId
|
|
10168
|
+
);
|
|
10169
|
+
try {
|
|
10170
|
+
await this.markFailed(
|
|
10171
|
+
conv.id,
|
|
10172
|
+
inFlight.evidentMessageId,
|
|
10173
|
+
sessionId,
|
|
10174
|
+
error2,
|
|
10175
|
+
usage,
|
|
10176
|
+
void 0,
|
|
10177
|
+
usageAgentName,
|
|
10178
|
+
subagentInvocations
|
|
10179
|
+
);
|
|
10180
|
+
} catch (err) {
|
|
10181
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
10182
|
+
if (err instanceof ChannelTerminalError) {
|
|
10183
|
+
this.log({
|
|
10184
|
+
level: "warn",
|
|
10185
|
+
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}`,
|
|
10186
|
+
conversation_id: conv.id,
|
|
10187
|
+
message_id: inFlight.evidentMessageId
|
|
10188
|
+
});
|
|
10189
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
10190
|
+
return;
|
|
10191
|
+
}
|
|
10192
|
+
if (this.now() >= inFlight.deadline) {
|
|
10193
|
+
this.log({
|
|
10194
|
+
level: "warn",
|
|
10195
|
+
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)}`,
|
|
10196
|
+
conversation_id: conv.id,
|
|
10197
|
+
message_id: inFlight.evidentMessageId
|
|
10198
|
+
});
|
|
10199
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
10200
|
+
return;
|
|
10201
|
+
}
|
|
10202
|
+
this.log({
|
|
10203
|
+
level: "warn",
|
|
10204
|
+
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)}`,
|
|
10205
|
+
conversation_id: conv.id,
|
|
10206
|
+
message_id: inFlight.evidentMessageId
|
|
10207
|
+
});
|
|
10208
|
+
return;
|
|
10209
|
+
}
|
|
10210
|
+
inFlight.done = true;
|
|
10211
|
+
await this.reportSubagentAuthFailures(
|
|
10212
|
+
watcher.conv.id,
|
|
10213
|
+
inFlight.opencodeMessageId,
|
|
10214
|
+
inFlight.evidentMessageId,
|
|
10215
|
+
messages
|
|
10216
|
+
);
|
|
10217
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
10218
|
+
}
|
|
9772
10219
|
/**
|
|
9773
10220
|
* Report a queued message as failed after the never-started bound. The accepted
|
|
9774
10221
|
* OpenCode copy is not cancelled, so the local fence remains on unsuccessful
|
|
@@ -9831,7 +10278,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9831
10278
|
*/
|
|
9832
10279
|
async settleMessageSessionStoreUnwritable(sessionId, watcher, inFlight) {
|
|
9833
10280
|
const conv = watcher.conv;
|
|
9834
|
-
const error2 =
|
|
10281
|
+
const error2 = SESSION_STORE_UNWRITABLE_ERROR;
|
|
9835
10282
|
this.log({
|
|
9836
10283
|
level: "error",
|
|
9837
10284
|
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} was not resolved as done: ${error2}`,
|
|
@@ -9922,11 +10369,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9922
10369
|
}
|
|
9923
10370
|
}
|
|
9924
10371
|
}
|
|
9925
|
-
if (this.untrackableAck.size > 0 || this.neverStartedAttempts.size > 0) {
|
|
10372
|
+
if (this.untrackableAck.size > 0 || this.neverStartedAttempts.size > 0 || this.awaitingCorrelation.size > 0) {
|
|
9926
10373
|
const stillProcessing = new Set(rows.map((r) => r.id));
|
|
10374
|
+
for (const id of this.awaitingCorrelation.keys()) {
|
|
10375
|
+
if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
|
|
10376
|
+
this.awaitingCorrelation.delete(id);
|
|
10377
|
+
this.log({
|
|
10378
|
+
level: "debug",
|
|
10379
|
+
message: `Re-adopt: message ${id.slice(0, 8)} left processing and pending \u2014 cleared awaiting-correlation fence`,
|
|
10380
|
+
message_id: id
|
|
10381
|
+
});
|
|
10382
|
+
}
|
|
10383
|
+
}
|
|
9927
10384
|
for (const id of [...this.untrackableAck.keys()]) {
|
|
9928
10385
|
if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
|
|
9929
10386
|
this.untrackableAck.delete(id);
|
|
10387
|
+
this.untrackableAckSessionIds.delete(id);
|
|
9930
10388
|
this.untrackableAckSignalled.delete(id);
|
|
9931
10389
|
this.untrackableAckWarned.delete(id);
|
|
9932
10390
|
this.log({
|
|
@@ -9966,7 +10424,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9966
10424
|
bySession.set(row.opencode_session_id, list);
|
|
9967
10425
|
}
|
|
9968
10426
|
for (const [sessionId, sessionRows] of bySession) {
|
|
9969
|
-
let messages;
|
|
10427
|
+
let messages = null;
|
|
9970
10428
|
try {
|
|
9971
10429
|
if (this.isV2) {
|
|
9972
10430
|
const snapshot = await this.getSessionMessages(sessionId);
|
|
@@ -9975,9 +10433,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9975
10433
|
level: "warn",
|
|
9976
10434
|
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned an unreadable message snapshot \u2014 skipping this session this tick`
|
|
9977
10435
|
});
|
|
9978
|
-
|
|
10436
|
+
} else {
|
|
10437
|
+
messages = snapshot;
|
|
9979
10438
|
}
|
|
9980
|
-
messages = snapshot;
|
|
9981
10439
|
} else {
|
|
9982
10440
|
const polled = await pollSessionMessagesForRedrive(
|
|
9983
10441
|
this.port,
|
|
@@ -9990,15 +10448,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
9990
10448
|
level: "warn",
|
|
9991
10449
|
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`
|
|
9992
10450
|
});
|
|
9993
|
-
|
|
10451
|
+
} else {
|
|
10452
|
+
messages = polled.messages;
|
|
9994
10453
|
}
|
|
9995
|
-
messages = polled.messages;
|
|
9996
10454
|
}
|
|
9997
10455
|
} catch (err) {
|
|
9998
10456
|
this.log({
|
|
9999
10457
|
level: "warn",
|
|
10000
10458
|
message: `Re-adopt: failed to poll session ${sessionId.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`
|
|
10001
10459
|
});
|
|
10460
|
+
}
|
|
10461
|
+
if (messages === null) {
|
|
10462
|
+
for (const row of sessionRows) {
|
|
10463
|
+
if (this.awaitingCorrelation.has(row.id)) {
|
|
10464
|
+
await this.advanceReadoptCorrelation(sessionId, row, null);
|
|
10465
|
+
}
|
|
10466
|
+
}
|
|
10002
10467
|
continue;
|
|
10003
10468
|
}
|
|
10004
10469
|
const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
|
|
@@ -10008,6 +10473,59 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10008
10473
|
}
|
|
10009
10474
|
}
|
|
10010
10475
|
}
|
|
10476
|
+
/** Advance a re-adopted accepted prompt through the shared correlation fence. */
|
|
10477
|
+
async advanceReadoptCorrelation(sessionId, row, messages) {
|
|
10478
|
+
const correlation = this.awaitingCorrelation.get(row.id);
|
|
10479
|
+
if (!correlation) return false;
|
|
10480
|
+
correlation.attempts += 1;
|
|
10481
|
+
const correlatedId = messages === null ? null : this.findAwaitingCorrelationId(correlation, sessionId, messages);
|
|
10482
|
+
if (correlatedId !== null) {
|
|
10483
|
+
this.awaitingCorrelation.delete(row.id);
|
|
10484
|
+
const conv = this.convForRow(sessionId, row);
|
|
10485
|
+
const message = this.queuedMessageForRow(row);
|
|
10486
|
+
const processedAtMs = this.processedAtMs(row);
|
|
10487
|
+
this.registerReadopted(conv, sessionId, message, correlatedId, processedAtMs, processedAtMs);
|
|
10488
|
+
this.dispatched.add(row.id);
|
|
10489
|
+
this.readopted.add(row.id);
|
|
10490
|
+
this.ensureWatcherRunning(sessionId);
|
|
10491
|
+
this.log({
|
|
10492
|
+
level: "info",
|
|
10493
|
+
message: `Re-adopt: matched accepted message ${row.id.slice(0, 8)} to OpenCode turn ${correlatedId.slice(0, 8)} without sending it again`,
|
|
10494
|
+
conversation_id: row.conversation_id,
|
|
10495
|
+
message_id: row.id
|
|
10496
|
+
});
|
|
10497
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_redispatched");
|
|
10498
|
+
void this.postSignal(row.conversation_id, row.id, "ack_correlated", {
|
|
10499
|
+
opencode_message_id: correlatedId
|
|
10500
|
+
});
|
|
10501
|
+
return true;
|
|
10502
|
+
}
|
|
10503
|
+
if (correlation.knownUserIds === null) {
|
|
10504
|
+
this.log({
|
|
10505
|
+
level: "debug",
|
|
10506
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} cannot be correlated because its pre-dispatch session list was unreadable`,
|
|
10507
|
+
conversation_id: row.conversation_id,
|
|
10508
|
+
message_id: row.id
|
|
10509
|
+
});
|
|
10510
|
+
} else if (correlation.sessionId !== sessionId) {
|
|
10511
|
+
this.log({
|
|
10512
|
+
level: "warn",
|
|
10513
|
+
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`,
|
|
10514
|
+
conversation_id: row.conversation_id,
|
|
10515
|
+
message_id: row.id
|
|
10516
|
+
});
|
|
10517
|
+
}
|
|
10518
|
+
if (correlation.attempts >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
|
|
10519
|
+
await this.handoffUntrackableCorrelation(
|
|
10520
|
+
row.conversation_id,
|
|
10521
|
+
row.id,
|
|
10522
|
+
correlation.sessionId,
|
|
10523
|
+
correlation.attempts,
|
|
10524
|
+
"readopt"
|
|
10525
|
+
);
|
|
10526
|
+
}
|
|
10527
|
+
return true;
|
|
10528
|
+
}
|
|
10011
10529
|
/**
|
|
10012
10530
|
* Re-adopt ONE `processing` row against the tick's session message snapshot
|
|
10013
10531
|
* (ADR-0046 Decision §1/§2). Idempotent: skips a row already being driven.
|
|
@@ -10015,8 +10533,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10015
10533
|
* Branches on `messageRunState(messages, row.opencode_message_id)` — the
|
|
10016
10534
|
* opencode-assigned user-message id persisted on the first `processing` PATCH
|
|
10017
10535
|
* (#218). A row with a NULL stored id (dispatched but the read-back never landed
|
|
10018
|
-
* before the restart)
|
|
10019
|
-
*
|
|
10536
|
+
* before the restart) is first offered to an awaiting-correlation fence recorded
|
|
10537
|
+
* by this process; without that fence, it has no id to correlate after restart
|
|
10538
|
+
* and is treated as an orphan and re-dispatched (at most once, see
|
|
10539
|
+
* `forceReadoptRun`):
|
|
10020
10540
|
* - `done` → `markDone` now (guarded like the watcher's done branch);
|
|
10021
10541
|
* - `failed` → `markFailed` with the surfaced error (issue #182), so an
|
|
10022
10542
|
* errored turn is reported failed on restart, NOT re-dispatched —
|
|
@@ -10025,7 +10545,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10025
10545
|
* re-dispatched instead (issue #1310, see the branch below);
|
|
10026
10546
|
* - `running`/`queued` → re-attach a watcher via `registerReadopted` (no re-dispatch),
|
|
10027
10547
|
* tracking the stored id so the reply correlates by it;
|
|
10028
|
-
* - `unknown`/null id →
|
|
10548
|
+
* - `unknown`/null id → correlate through an awaiting fence when present;
|
|
10549
|
+
* otherwise re-dispatch (opencode assigns a fresh id) + attach a watcher.
|
|
10029
10550
|
*
|
|
10030
10551
|
* Only `ChannelAuthError` propagates.
|
|
10031
10552
|
*/
|
|
@@ -10039,6 +10560,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10039
10560
|
});
|
|
10040
10561
|
return;
|
|
10041
10562
|
}
|
|
10563
|
+
if (await this.advanceReadoptCorrelation(sessionId, row, messages)) return;
|
|
10042
10564
|
const ocId = row.opencode_message_id;
|
|
10043
10565
|
const state = messageRunState(messages, ocId ?? "");
|
|
10044
10566
|
if (state === "done") {
|
|
@@ -10105,12 +10627,48 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10105
10627
|
await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
|
|
10106
10628
|
this.dontRedispatch.delete(row.id);
|
|
10107
10629
|
this.untrackableAck.delete(row.id);
|
|
10630
|
+
this.untrackableAckSessionIds.delete(row.id);
|
|
10108
10631
|
this.untrackableAckSignalled.delete(row.id);
|
|
10109
10632
|
this.untrackableAckWarned.delete(row.id);
|
|
10110
10633
|
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
10111
10634
|
return;
|
|
10112
10635
|
}
|
|
10113
|
-
if (this.
|
|
10636
|
+
if (this.untrackableAck.has(row.id)) {
|
|
10637
|
+
if (this.untrackableAckSignalled.has(row.id)) {
|
|
10638
|
+
const firstFenceWarning = !this.untrackableAckWarned.has(row.id);
|
|
10639
|
+
this.untrackableAckWarned.add(row.id);
|
|
10640
|
+
this.log({
|
|
10641
|
+
level: firstFenceWarning ? "warn" : "debug",
|
|
10642
|
+
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`,
|
|
10643
|
+
conversation_id: row.conversation_id,
|
|
10644
|
+
message_id: row.id
|
|
10645
|
+
});
|
|
10646
|
+
const errorMessage3 = this.untrackableAck.get(row.id);
|
|
10647
|
+
const reportError = await this.reportUntrackableAck(
|
|
10648
|
+
row.conversation_id,
|
|
10649
|
+
row.id,
|
|
10650
|
+
this.untrackableAckSessionIds.get(row.id) ?? null,
|
|
10651
|
+
errorMessage3
|
|
10652
|
+
);
|
|
10653
|
+
if (reportError !== null) {
|
|
10654
|
+
this.log({
|
|
10655
|
+
level: "warn",
|
|
10656
|
+
message: `Re-adopt: failed to report untrackable message ${row.id.slice(0, 8)} (will retry next drain): ${reportError.message}`,
|
|
10657
|
+
conversation_id: row.conversation_id,
|
|
10658
|
+
message_id: row.id
|
|
10659
|
+
});
|
|
10660
|
+
}
|
|
10661
|
+
} else {
|
|
10662
|
+
this.log({
|
|
10663
|
+
level: "debug",
|
|
10664
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
|
|
10665
|
+
conversation_id: row.conversation_id,
|
|
10666
|
+
message_id: row.id
|
|
10667
|
+
});
|
|
10668
|
+
}
|
|
10669
|
+
return;
|
|
10670
|
+
}
|
|
10671
|
+
if (this.dontRedispatch.has(row.id)) {
|
|
10114
10672
|
this.log({
|
|
10115
10673
|
level: "debug",
|
|
10116
10674
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
|
|
@@ -10288,6 +10846,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10288
10846
|
}
|
|
10289
10847
|
this.dontRedispatch.delete(row.id);
|
|
10290
10848
|
this.untrackableAck.delete(row.id);
|
|
10849
|
+
this.untrackableAckSessionIds.delete(row.id);
|
|
10291
10850
|
this.untrackableAckSignalled.delete(row.id);
|
|
10292
10851
|
this.untrackableAckWarned.delete(row.id);
|
|
10293
10852
|
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
@@ -10307,8 +10866,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10307
10866
|
* latch makes a null-id row re-dispatched AT MOST ONCE per outstanding read-back:
|
|
10308
10867
|
* short-circuit while the row is latched; clear it on a successful dispatch (the
|
|
10309
10868
|
* row is then tracked in `dispatched`, so `readoptOne`'s early skip prevents
|
|
10310
|
-
* re-entry)
|
|
10311
|
-
*
|
|
10869
|
+
* re-entry) or when the dispatch itself failed before acceptance. An accepted V1
|
|
10870
|
+
* dispatch whose id is not readable enters the bounded correlation fence instead
|
|
10871
|
+
* of retrying; exhaustion hands it to `untrackableAck`.
|
|
10312
10872
|
*
|
|
10313
10873
|
* `evidentMessageId = row.id` addresses the SERVER row. Deadline anchored to
|
|
10314
10874
|
* `processed_at` (Invariant 1).
|
|
@@ -10371,6 +10931,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10371
10931
|
);
|
|
10372
10932
|
}
|
|
10373
10933
|
let ocId;
|
|
10934
|
+
let preDispatchUserIds = null;
|
|
10374
10935
|
try {
|
|
10375
10936
|
ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
|
|
10376
10937
|
sessionId,
|
|
@@ -10380,7 +10941,20 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10380
10941
|
row.content,
|
|
10381
10942
|
options,
|
|
10382
10943
|
sendAttachments,
|
|
10383
|
-
this.openCodeClient
|
|
10944
|
+
this.openCodeClient,
|
|
10945
|
+
{
|
|
10946
|
+
onKnownUserIds: (ids) => {
|
|
10947
|
+
preDispatchUserIds = ids;
|
|
10948
|
+
},
|
|
10949
|
+
onKnownUserIdsError: (error2) => {
|
|
10950
|
+
this.log({
|
|
10951
|
+
level: "warn",
|
|
10952
|
+
message: `Pre-dispatch snapshot callback failed for message ${row.id.slice(0, 8)}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
10953
|
+
conversation_id: row.conversation_id,
|
|
10954
|
+
message_id: row.id
|
|
10955
|
+
});
|
|
10956
|
+
}
|
|
10957
|
+
}
|
|
10384
10958
|
)
|
|
10385
10959
|
);
|
|
10386
10960
|
} catch (err) {
|
|
@@ -10413,6 +10987,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10413
10987
|
}
|
|
10414
10988
|
} else {
|
|
10415
10989
|
this.untrackableAck.set(row.id, errorMessage3);
|
|
10990
|
+
this.untrackableAckSessionIds.set(row.id, sessionId);
|
|
10416
10991
|
}
|
|
10417
10992
|
this.log({
|
|
10418
10993
|
level: "error",
|
|
@@ -10461,39 +11036,20 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10461
11036
|
}
|
|
10462
11037
|
if (ocId === null) {
|
|
10463
11038
|
this.awaitingReadopt.delete(row.id);
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
this.log({
|
|
10471
|
-
level: "error",
|
|
10472
|
-
message: errorMessage3,
|
|
10473
|
-
conversation_id: row.conversation_id,
|
|
10474
|
-
message_id: row.id
|
|
10475
|
-
});
|
|
10476
|
-
await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
|
|
10477
|
-
this.log({
|
|
10478
|
-
level: "warn",
|
|
10479
|
-
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)}`,
|
|
10480
|
-
conversation_id: row.conversation_id,
|
|
10481
|
-
message_id: row.id
|
|
10482
|
-
});
|
|
10483
|
-
});
|
|
10484
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
10485
|
-
return;
|
|
10486
|
-
}
|
|
11039
|
+
this.awaitingCorrelation.set(row.id, {
|
|
11040
|
+
sessionId,
|
|
11041
|
+
content: row.content,
|
|
11042
|
+
attempts: 0,
|
|
11043
|
+
knownUserIds: preDispatchUserIds
|
|
11044
|
+
});
|
|
10487
11045
|
this.log({
|
|
10488
11046
|
level: "warn",
|
|
10489
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)}
|
|
11047
|
+
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`,
|
|
10490
11048
|
conversation_id: row.conversation_id,
|
|
10491
11049
|
message_id: row.id
|
|
10492
11050
|
});
|
|
10493
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
10494
11051
|
return;
|
|
10495
11052
|
}
|
|
10496
|
-
this.unconfirmedDispatchFailures.delete(row.id);
|
|
10497
11053
|
const processedAtMs = this.processedAtMs(row);
|
|
10498
11054
|
this.registerReadopted(
|
|
10499
11055
|
readoptConv,
|
|
@@ -13972,6 +14528,34 @@ async function run(options) {
|
|
|
13972
14528
|
} else {
|
|
13973
14529
|
log2(state, "Not running in a MicroVM (MICROVM_ID unset) \u2014 nothing to report", "debug");
|
|
13974
14530
|
}
|
|
14531
|
+
if (options.restoreRunnerCredentials || options.credentialSyncMarker) {
|
|
14532
|
+
const logCredentialVaultResolution = (message, level) => {
|
|
14533
|
+
log2(state, message, level);
|
|
14534
|
+
if (state.interactive && !state.json) {
|
|
14535
|
+
logActivity(state, { type: "info", level, message });
|
|
14536
|
+
}
|
|
14537
|
+
};
|
|
14538
|
+
const vaultResolution = await getRunnerCredentialVault(state.agentId, state.authHeader);
|
|
14539
|
+
if (vaultResolution.error) {
|
|
14540
|
+
delete process.env.CREDENTIAL_VAULT_PREFIX;
|
|
14541
|
+
logCredentialVaultResolution(
|
|
14542
|
+
`CREDENTIAL-VAULT-LOOKUP-FAILED: ${vaultResolution.error}; credentials will be read from and written to this runner's own prefix this boot`,
|
|
14543
|
+
"warn"
|
|
14544
|
+
);
|
|
14545
|
+
} else if (vaultResolution.prefix !== null && vaultResolution.vaultId !== null && vaultResolution.source !== null) {
|
|
14546
|
+
process.env.CREDENTIAL_VAULT_PREFIX = vaultResolution.prefix;
|
|
14547
|
+
logCredentialVaultResolution(
|
|
14548
|
+
`CREDENTIAL-VAULT: source=${vaultResolution.source} vault=${vaultResolution.vaultId} prefix=${vaultResolution.prefix}`,
|
|
14549
|
+
"info"
|
|
14550
|
+
);
|
|
14551
|
+
} else {
|
|
14552
|
+
delete process.env.CREDENTIAL_VAULT_PREFIX;
|
|
14553
|
+
logCredentialVaultResolution(
|
|
14554
|
+
"CREDENTIAL-VAULT-NONE: this runner is not linked to a credential vault; using its own prefix",
|
|
14555
|
+
"debug"
|
|
14556
|
+
);
|
|
14557
|
+
}
|
|
14558
|
+
}
|
|
13975
14559
|
if (options.restoreRunnerCredentials) {
|
|
13976
14560
|
log2(state, "Restoring runner credentials before starting OpenCode");
|
|
13977
14561
|
const credentialContext = {
|
|
@@ -14005,6 +14589,7 @@ async function run(options) {
|
|
|
14005
14589
|
openCodeLogTail.stop();
|
|
14006
14590
|
};
|
|
14007
14591
|
let sessionDbVerifyFatal = false;
|
|
14592
|
+
let sessionDbVerifyFatalReason;
|
|
14008
14593
|
if (!options.restoreSessionDb) {
|
|
14009
14594
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
14010
14595
|
} else {
|
|
@@ -14037,12 +14622,13 @@ async function run(options) {
|
|
|
14037
14622
|
reportRecovery: (record) => reportSessionDbRecoveryRecord(state, record)
|
|
14038
14623
|
});
|
|
14039
14624
|
sessionDbVerifyFatal = result.verifyFatal;
|
|
14625
|
+
sessionDbVerifyFatalReason = result.verifyFatalReason;
|
|
14040
14626
|
}
|
|
14041
14627
|
}
|
|
14042
14628
|
reportSessionDbRecovery(state);
|
|
14043
14629
|
if (sessionDbVerifyFatal) {
|
|
14044
14630
|
throw new Error(
|
|
14045
|
-
"SESSION-DB-INTEGRITY-EXHAUSTED: the corrupt session DB could not be proven separated from the active backup prefix or removed from disk"
|
|
14631
|
+
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`
|
|
14046
14632
|
);
|
|
14047
14633
|
}
|
|
14048
14634
|
applyRunnerOpenCodeConfig({
|
|
@@ -14369,14 +14955,14 @@ async function run(options) {
|
|
|
14369
14955
|
},
|
|
14370
14956
|
onDisconnected: (code, reason, transition) => {
|
|
14371
14957
|
state.connected = false;
|
|
14372
|
-
const
|
|
14958
|
+
const safeReason2 = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
|
|
14373
14959
|
logActivity(state, {
|
|
14374
14960
|
type: "info",
|
|
14375
|
-
message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${
|
|
14961
|
+
message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason2})`
|
|
14376
14962
|
});
|
|
14377
14963
|
emitAgentDisconnected(state.agentId, {
|
|
14378
14964
|
code,
|
|
14379
|
-
reason:
|
|
14965
|
+
reason: safeReason2,
|
|
14380
14966
|
connection_generation: transition.generation
|
|
14381
14967
|
});
|
|
14382
14968
|
if (state.interactive) displayStatus(state);
|
|
@@ -14539,6 +15125,7 @@ program.command("login").description("Authenticate with Evident").option("--toke
|
|
|
14539
15125
|
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 }));
|
|
14540
15126
|
program.command("whoami").description("Show the currently logged in user").action(whoami);
|
|
14541
15127
|
program.command("status").description("Check whether the configured credentials can reach Evident").option("--json", "Output in JSON format").action((options) => status({ json: options.json }));
|
|
15128
|
+
program.command("credential-vault-env").description("Print the shell assignment a runner image evals before starting the synchroniser").action(credentialVaultEnv);
|
|
14542
15129
|
program.command("claude-usage").description("[spike] Show Claude subscription usage (requires a local `claude login`)").action(claudeUsage);
|
|
14543
15130
|
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(
|
|
14544
15131
|
"-a, --agent [id]",
|