@evident-ai/cli 3.5.2-dev.2bb04b2 → 3.5.2-dev.3e69672
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 +475 -142
- 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";
|
|
@@ -1227,7 +1322,7 @@ import ora4 from "ora";
|
|
|
1227
1322
|
import { select as select4 } from "@inquirer/prompts";
|
|
1228
1323
|
|
|
1229
1324
|
// src/lib/telemetry.ts
|
|
1230
|
-
var CLI_VERSION = (true ? "3.5.2-dev.
|
|
1325
|
+
var CLI_VERSION = (true ? "3.5.2-dev.3e69672" : void 0) ?? process.env.npm_package_version ?? "unknown";
|
|
1231
1326
|
function getCliVersion() {
|
|
1232
1327
|
return CLI_VERSION;
|
|
1233
1328
|
}
|
|
@@ -2250,8 +2345,12 @@ function errorOf(m) {
|
|
|
2250
2345
|
if (!m || typeof m !== "object") return void 0;
|
|
2251
2346
|
return m.info?.error ?? m.error;
|
|
2252
2347
|
}
|
|
2348
|
+
function isTerminalStopAnswer(m) {
|
|
2349
|
+
return completedOf(m) != null && errorOf(m) == null && finishOf(m) === "stop";
|
|
2350
|
+
}
|
|
2253
2351
|
function isAssistantInFlight(m) {
|
|
2254
2352
|
if (completedOf(m) == null) return true;
|
|
2353
|
+
if (errorOf(m) != null) return false;
|
|
2255
2354
|
return finishOf(m) === "tool-calls";
|
|
2256
2355
|
}
|
|
2257
2356
|
async function getSessionMessages(port, sessionId, client) {
|
|
@@ -2608,11 +2707,31 @@ function messageText(m) {
|
|
|
2608
2707
|
if (!m || !Array.isArray(m.parts)) return "";
|
|
2609
2708
|
return m.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
2610
2709
|
}
|
|
2611
|
-
|
|
2710
|
+
function findDispatchedUserMessageId(messages, content, excludeIds) {
|
|
2711
|
+
if (!messages || messages.length === 0) return null;
|
|
2712
|
+
let best = null;
|
|
2713
|
+
for (const message of messages) {
|
|
2714
|
+
if (roleOf2(message) !== "user") continue;
|
|
2715
|
+
const id = idOf2(message);
|
|
2716
|
+
if (typeof id !== "string" || excludeIds?.has(id)) continue;
|
|
2717
|
+
if (messageText(message) !== content) continue;
|
|
2718
|
+
const created = createdOf(message) ?? 0;
|
|
2719
|
+
if (best === null || created > best.created) best = { id, created };
|
|
2720
|
+
}
|
|
2721
|
+
return best?.id ?? null;
|
|
2722
|
+
}
|
|
2723
|
+
async function sendPromptAsync(port, sessionId, content, options, attachments, client, callbacks) {
|
|
2612
2724
|
const before = await getSessionMessages(port, sessionId, client);
|
|
2613
2725
|
const knownUserIds = new Set(
|
|
2614
2726
|
(before ?? []).filter((m) => roleOf2(m) === "user").map((m) => idOf2(m)).filter((id) => typeof id === "string")
|
|
2615
2727
|
);
|
|
2728
|
+
if (callbacks) {
|
|
2729
|
+
try {
|
|
2730
|
+
callbacks.onKnownUserIds(before === null ? null : knownUserIds);
|
|
2731
|
+
} catch (error2) {
|
|
2732
|
+
callbacks.onKnownUserIdsError(error2);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2616
2735
|
const parts = [{ type: "text", text: content }];
|
|
2617
2736
|
let pendingOutcomes = null;
|
|
2618
2737
|
if (attachments && attachments.inputs.length > 0) {
|
|
@@ -2651,22 +2770,10 @@ async function sendPromptAsync(port, sessionId, content, options, attachments, c
|
|
|
2651
2770
|
const READ_BACK_DELAY_MS = 150;
|
|
2652
2771
|
for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
|
|
2653
2772
|
const after = await getSessionMessages(port, sessionId, client);
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
const id = idOf2(m);
|
|
2659
|
-
if (typeof id !== "string" || knownUserIds.has(id)) continue;
|
|
2660
|
-
if (messageText(m) !== content) continue;
|
|
2661
|
-
const created = createdOf(m) ?? 0;
|
|
2662
|
-
if (best === null || created > best.created) {
|
|
2663
|
-
best = { id, created };
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
if (best) {
|
|
2667
|
-
if (pendingOutcomes && attachments?.onOutcomes) attachments.onOutcomes(pendingOutcomes);
|
|
2668
|
-
return best.id;
|
|
2669
|
-
}
|
|
2773
|
+
const dispatchedUserMessageId = findDispatchedUserMessageId(after, content, knownUserIds);
|
|
2774
|
+
if (dispatchedUserMessageId !== null) {
|
|
2775
|
+
if (pendingOutcomes && attachments?.onOutcomes) attachments.onOutcomes(pendingOutcomes);
|
|
2776
|
+
return dispatchedUserMessageId;
|
|
2670
2777
|
}
|
|
2671
2778
|
if (attempt < READ_BACK_ATTEMPTS - 1) {
|
|
2672
2779
|
await new Promise((resolve4) => setTimeout(resolve4, READ_BACK_DELAY_MS));
|
|
@@ -2690,30 +2797,41 @@ function findAssistantReplyAfter(messages, userMessageId) {
|
|
|
2690
2797
|
function findLastAssistantReplyFor(messages, userMessageId) {
|
|
2691
2798
|
if (!messages || messages.length === 0) return null;
|
|
2692
2799
|
let lastCorrelated = null;
|
|
2693
|
-
let lastNonErrored = null;
|
|
2694
2800
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2695
2801
|
const m = messages[i];
|
|
2696
2802
|
if (roleOf2(m) !== "assistant" || parentIdOf2(m) !== userMessageId) continue;
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2803
|
+
lastCorrelated = m;
|
|
2804
|
+
break;
|
|
2805
|
+
}
|
|
2806
|
+
if (lastCorrelated) {
|
|
2807
|
+
if (errorOf(lastCorrelated) == null) return lastCorrelated;
|
|
2808
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2809
|
+
const m = messages[i];
|
|
2810
|
+
if (roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId && isTerminalStopAnswer(m)) {
|
|
2811
|
+
return m;
|
|
2812
|
+
}
|
|
2701
2813
|
}
|
|
2814
|
+
return lastCorrelated;
|
|
2702
2815
|
}
|
|
2703
|
-
if (lastCorrelated) return lastNonErrored ?? lastCorrelated;
|
|
2704
2816
|
const userIndex = messages.findIndex((m) => idOf2(m) === userMessageId);
|
|
2705
2817
|
if (userIndex === -1) return null;
|
|
2706
2818
|
let last = null;
|
|
2707
|
-
let
|
|
2819
|
+
let lastAssistantIndex = -1;
|
|
2708
2820
|
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
2709
2821
|
const role = roleOf2(messages[i]);
|
|
2710
2822
|
if (role === "user") break;
|
|
2711
2823
|
if (role === "assistant") {
|
|
2712
2824
|
last = messages[i];
|
|
2713
|
-
|
|
2825
|
+
lastAssistantIndex = i;
|
|
2714
2826
|
}
|
|
2715
2827
|
}
|
|
2716
|
-
|
|
2828
|
+
if (last === null || errorOf(last) == null) return last;
|
|
2829
|
+
for (let i = lastAssistantIndex; i > userIndex; i--) {
|
|
2830
|
+
const m = messages[i];
|
|
2831
|
+
if (roleOf2(m) === "user") break;
|
|
2832
|
+
if (roleOf2(m) === "assistant" && isTerminalStopAnswer(m)) return m;
|
|
2833
|
+
}
|
|
2834
|
+
return last;
|
|
2717
2835
|
}
|
|
2718
2836
|
function collectSubagentSessions(messages, userMessageId) {
|
|
2719
2837
|
if (!messages || messages.length === 0) return [];
|
|
@@ -2819,8 +2937,9 @@ function attributeTaskCallUsage(messages, windows) {
|
|
|
2819
2937
|
}
|
|
2820
2938
|
function sumAssistantUsage(messages) {
|
|
2821
2939
|
if (!messages || messages.length === 0) return null;
|
|
2822
|
-
const
|
|
2823
|
-
const
|
|
2940
|
+
const hasTrailingError = errorOf(messages[messages.length - 1]) != null;
|
|
2941
|
+
const hasTerminalStopAnswer = messages.some(isTerminalStopAnswer);
|
|
2942
|
+
const selected = hasTrailingError && hasTerminalStopAnswer ? messages.filter((message) => errorOf(message) == null) : messages;
|
|
2824
2943
|
let sawAnyUsage = false;
|
|
2825
2944
|
let inputSum = 0;
|
|
2826
2945
|
let outputSum = 0;
|
|
@@ -2876,11 +2995,9 @@ function messageUsage(messages, userMessageId) {
|
|
|
2876
2995
|
const byParentAll = messages.filter(
|
|
2877
2996
|
(m) => roleOf2(m) === "assistant" && parentIdOf2(m) === userMessageId
|
|
2878
2997
|
);
|
|
2879
|
-
const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
|
|
2880
|
-
const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
|
|
2881
2998
|
let correlated;
|
|
2882
|
-
if (
|
|
2883
|
-
correlated =
|
|
2999
|
+
if (byParentAll.length > 0) {
|
|
3000
|
+
correlated = byParentAll;
|
|
2884
3001
|
} else {
|
|
2885
3002
|
const reply = findAssistantReplyAfter(messages, userMessageId);
|
|
2886
3003
|
correlated = reply ? [reply] : [];
|
|
@@ -2926,14 +3043,26 @@ function messageError(messages, userMessageId) {
|
|
|
2926
3043
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
2927
3044
|
const error2 = errorOf(reply);
|
|
2928
3045
|
if (error2 == null) return null;
|
|
2929
|
-
|
|
2930
|
-
if (typeof error2 === "
|
|
3046
|
+
let rendered;
|
|
3047
|
+
if (typeof error2 === "string") {
|
|
3048
|
+
rendered = error2;
|
|
3049
|
+
} else if (typeof error2 === "object") {
|
|
2931
3050
|
const e = error2;
|
|
2932
3051
|
const dataMessage = e.data?.message;
|
|
2933
|
-
if (typeof dataMessage === "string")
|
|
2934
|
-
|
|
3052
|
+
if (typeof dataMessage === "string") {
|
|
3053
|
+
rendered = dataMessage;
|
|
3054
|
+
} else if (typeof e.message === "string") {
|
|
3055
|
+
rendered = e.message;
|
|
3056
|
+
} else {
|
|
3057
|
+
rendered = "The agent run failed.";
|
|
3058
|
+
}
|
|
3059
|
+
} else {
|
|
3060
|
+
rendered = "The agent run failed.";
|
|
2935
3061
|
}
|
|
2936
|
-
|
|
3062
|
+
const toolPart = Array.isArray(reply?.parts) ? reply.parts.find(
|
|
3063
|
+
(part) => part && typeof part === "object" && part.type === "tool" && part.state?.status === "error"
|
|
3064
|
+
) : void 0;
|
|
3065
|
+
return typeof toolPart?.tool === "string" && toolPart.tool.length > 0 ? `${rendered} (tool "${toolPart.tool}")` : rendered;
|
|
2937
3066
|
}
|
|
2938
3067
|
function isAbortedTerminalReply(messages, userMessageId) {
|
|
2939
3068
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
@@ -7102,10 +7231,18 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7102
7231
|
* row is `processing` or `pending`; clear it only when absent from both lists.
|
|
7103
7232
|
*/
|
|
7104
7233
|
untrackableAck = /* @__PURE__ */ new Map();
|
|
7234
|
+
/** Session binding used by the first untrackable-ack report, for retries. */
|
|
7235
|
+
untrackableAckSessionIds = /* @__PURE__ */ new Map();
|
|
7105
7236
|
/** Messages whose untrackable-ack failure signal was emitted while fenced. */
|
|
7106
7237
|
untrackableAckSignalled = /* @__PURE__ */ new Set();
|
|
7107
7238
|
/** Messages whose first untrackable-ack fence warning was already logged. */
|
|
7108
7239
|
untrackableAckWarned = /* @__PURE__ */ new Set();
|
|
7240
|
+
/**
|
|
7241
|
+
* Accepted V1 prompts whose assigned ids were not read back; these rows are
|
|
7242
|
+
* re-read instead of POSTed again. A null snapshot cannot safely exclude an older
|
|
7243
|
+
* identical-text turn, so it never produces a correlation.
|
|
7244
|
+
*/
|
|
7245
|
+
awaitingCorrelation = /* @__PURE__ */ new Map();
|
|
7109
7246
|
/** Pending rows seen in the current drain, used to retain terminal dispatch fences. */
|
|
7110
7247
|
pendingMessageIds = /* @__PURE__ */ new Set();
|
|
7111
7248
|
/**
|
|
@@ -7191,21 +7328,6 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7191
7328
|
* re-signal on every one of the 15h of re-dispatch attempts #1110 made.
|
|
7192
7329
|
*/
|
|
7193
7330
|
dispatchNotStartedSignalled = /* @__PURE__ */ new Map();
|
|
7194
|
-
/**
|
|
7195
|
-
* Consecutive-UNCONFIRMED-dispatch streak for a `pending` row with NO stored
|
|
7196
|
-
* `opencode_message_id` yet — i.e. one that has never even reached the
|
|
7197
|
-
* re-drive fence above. `sendPromptAsync`'s POST may 2xx, but its own
|
|
7198
|
-
* read-back retries can never confirm the assigned id when the session's
|
|
7199
|
-
* message list is PERMANENTLY unreadable (e.g. a corrupted local opencode
|
|
7200
|
-
* SQLite DB, #1345/#1348's exact fault, just hit BEFORE the row is ever
|
|
7201
|
-
* dispatched instead of after). Unlike an already-dispatched row, THIS row has
|
|
7202
|
-
* no other safety net at all: the lifecycle cron only reclaims `status =
|
|
7203
|
-
* 'processing'` rows, and a row stuck here never reaches `processing`. Keyed
|
|
7204
|
-
* by message id, carrying `sessionId` so a session change (a fresh one bound
|
|
7205
|
-
* after abandonment) starts a new streak rather than inheriting the old
|
|
7206
|
-
* session's count — same shape as `redrivePollFailures` above.
|
|
7207
|
-
*/
|
|
7208
|
-
unconfirmedDispatchFailures = /* @__PURE__ */ new Map();
|
|
7209
7331
|
/**
|
|
7210
7332
|
* Consecutive give-ups for a message whose OpenCode user row remains queued.
|
|
7211
7333
|
* The session id scopes the count: a fresh session is a new attempt after a
|
|
@@ -7223,8 +7345,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7223
7345
|
* short-circuits while it is present, so a null-id row is re-dispatched AT MOST
|
|
7224
7346
|
* ONCE per outstanding read-back. Cleared on a SUCCESSFUL dispatch+read-back (the
|
|
7225
7347
|
* row is then tracked in `dispatched`, so `readoptOne`'s early skip prevents
|
|
7226
|
-
* re-entry) OR on a failed/unresolved dispatch (the
|
|
7227
|
-
*
|
|
7348
|
+
* re-entry) OR on a failed/unresolved dispatch (the accepted prompt is handed to
|
|
7349
|
+
* the terminal-report fence and is never sent again).
|
|
7228
7350
|
*/
|
|
7229
7351
|
awaitingReadopt = /* @__PURE__ */ new Set();
|
|
7230
7352
|
/**
|
|
@@ -7542,6 +7664,49 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7542
7664
|
}
|
|
7543
7665
|
return ids;
|
|
7544
7666
|
}
|
|
7667
|
+
/** Find an accepted prompt's user row without adopting an older or concurrent turn. */
|
|
7668
|
+
findAwaitingCorrelationId(entry, sessionId, messages) {
|
|
7669
|
+
if (entry.sessionId !== sessionId || entry.knownUserIds === null) return null;
|
|
7670
|
+
const excludeIds = new Set(entry.knownUserIds);
|
|
7671
|
+
for (const inFlight of this.watchers.get(entry.sessionId)?.inFlight.values() ?? []) {
|
|
7672
|
+
excludeIds.add(inFlight.opencodeMessageId);
|
|
7673
|
+
}
|
|
7674
|
+
return findDispatchedUserMessageId(messages, entry.content, excludeIds);
|
|
7675
|
+
}
|
|
7676
|
+
/** Hand an exhausted accepted-but-untrackable prompt to the terminal-report fence. */
|
|
7677
|
+
async handoffUntrackableCorrelation(conversationId, messageId, sessionId, attempts, source) {
|
|
7678
|
+
this.awaitingCorrelation.delete(messageId);
|
|
7679
|
+
this.awaitingReadopt.delete(messageId);
|
|
7680
|
+
this.sessions.delete(conversationId);
|
|
7681
|
+
this.supersede(conversationId, sessionId);
|
|
7682
|
+
const dispatchDescription = source === "readopt" ? "re-dispatched message" : "message";
|
|
7683
|
+
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.`;
|
|
7684
|
+
this.untrackableAck.set(messageId, errorMessage3);
|
|
7685
|
+
this.untrackableAckSessionIds.set(messageId, null);
|
|
7686
|
+
this.log({
|
|
7687
|
+
level: "error",
|
|
7688
|
+
message: errorMessage3,
|
|
7689
|
+
conversation_id: conversationId,
|
|
7690
|
+
message_id: messageId
|
|
7691
|
+
});
|
|
7692
|
+
const reportError = await this.reportUntrackableAck(
|
|
7693
|
+
conversationId,
|
|
7694
|
+
messageId,
|
|
7695
|
+
null,
|
|
7696
|
+
errorMessage3
|
|
7697
|
+
);
|
|
7698
|
+
if (reportError !== null) {
|
|
7699
|
+
this.log({
|
|
7700
|
+
level: "warn",
|
|
7701
|
+
message: `markFailed PATCH for message ${messageId.slice(0, 8)} (conversation ${conversationId.slice(0, 8)}) failed: ${reportError.message}`,
|
|
7702
|
+
conversation_id: conversationId,
|
|
7703
|
+
message_id: messageId
|
|
7704
|
+
});
|
|
7705
|
+
}
|
|
7706
|
+
if (source === "readopt") {
|
|
7707
|
+
void this.postSignal(conversationId, messageId, "readopt_orphan_unsent");
|
|
7708
|
+
}
|
|
7709
|
+
}
|
|
7545
7710
|
/**
|
|
7546
7711
|
* File-pull work, for `run.ts`'s idle accounting (#559).
|
|
7547
7712
|
*
|
|
@@ -7721,6 +7886,56 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7721
7886
|
}
|
|
7722
7887
|
break;
|
|
7723
7888
|
}
|
|
7889
|
+
const correlation = this.awaitingCorrelation.get(message.id);
|
|
7890
|
+
if (correlation) {
|
|
7891
|
+
correlation.attempts += 1;
|
|
7892
|
+
let correlatedId = null;
|
|
7893
|
+
if (correlation.knownUserIds !== null && correlation.sessionId === sessionId) {
|
|
7894
|
+
const polled = await this.pollSessionMessagesForRedrive(
|
|
7895
|
+
conv,
|
|
7896
|
+
message,
|
|
7897
|
+
correlation.sessionId
|
|
7898
|
+
);
|
|
7899
|
+
if (polled.ok)
|
|
7900
|
+
correlatedId = this.findAwaitingCorrelationId(correlation, sessionId, polled.messages);
|
|
7901
|
+
} else if (correlation.knownUserIds === null) {
|
|
7902
|
+
this.log({
|
|
7903
|
+
level: "debug",
|
|
7904
|
+
message: `Message ${message.id.slice(0, 8)} cannot be correlated because its pre-dispatch session list was unreadable`,
|
|
7905
|
+
conversation_id: conv.id,
|
|
7906
|
+
message_id: message.id
|
|
7907
|
+
});
|
|
7908
|
+
}
|
|
7909
|
+
if (correlatedId !== null) {
|
|
7910
|
+
this.awaitingCorrelation.delete(message.id);
|
|
7911
|
+
this.dispatchNotStartedSignalled.delete(message.id);
|
|
7912
|
+
this.releasedOpencodeIds.delete(message.id);
|
|
7913
|
+
this.dispatched.add(message.id);
|
|
7914
|
+
this.registerInFlight(conv, correlation.sessionId, message, correlatedId);
|
|
7915
|
+
dispatched += 1;
|
|
7916
|
+
this.log({
|
|
7917
|
+
level: "info",
|
|
7918
|
+
message: `Matched accepted message ${message.id.slice(0, 8)} to OpenCode turn ${correlatedId.slice(0, 8)} without sending it again`,
|
|
7919
|
+
conversation_id: conv.id,
|
|
7920
|
+
message_id: message.id
|
|
7921
|
+
});
|
|
7922
|
+
void this.postSignal(conv.id, message.id, "ack_correlated", {
|
|
7923
|
+
opencode_message_id: correlatedId
|
|
7924
|
+
});
|
|
7925
|
+
continue;
|
|
7926
|
+
}
|
|
7927
|
+
if (correlation.attempts >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
|
|
7928
|
+
await this.handoffUntrackableCorrelation(
|
|
7929
|
+
conv.id,
|
|
7930
|
+
message.id,
|
|
7931
|
+
correlation.sessionId,
|
|
7932
|
+
correlation.attempts,
|
|
7933
|
+
"fresh"
|
|
7934
|
+
);
|
|
7935
|
+
break;
|
|
7936
|
+
}
|
|
7937
|
+
break;
|
|
7938
|
+
}
|
|
7724
7939
|
const effectiveOpencodeMessageId = message.opencode_message_id ?? this.releasedOpencodeIds.get(message.id)?.opencodeMessageId ?? null;
|
|
7725
7940
|
if (effectiveOpencodeMessageId) {
|
|
7726
7941
|
const outcome = await this.resolveRedrive(
|
|
@@ -7742,6 +7957,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7742
7957
|
model: message.opencode_model ?? void 0
|
|
7743
7958
|
};
|
|
7744
7959
|
let opencodeMessageId;
|
|
7960
|
+
let preDispatchUserIds = null;
|
|
7745
7961
|
try {
|
|
7746
7962
|
this.log({
|
|
7747
7963
|
level: "info",
|
|
@@ -7771,7 +7987,20 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7771
7987
|
message.content,
|
|
7772
7988
|
options,
|
|
7773
7989
|
sendAttachments,
|
|
7774
|
-
this.openCodeClient
|
|
7990
|
+
this.openCodeClient,
|
|
7991
|
+
{
|
|
7992
|
+
onKnownUserIds: (ids) => {
|
|
7993
|
+
preDispatchUserIds = ids;
|
|
7994
|
+
},
|
|
7995
|
+
onKnownUserIdsError: (error2) => {
|
|
7996
|
+
this.log({
|
|
7997
|
+
level: "warn",
|
|
7998
|
+
message: `Pre-dispatch snapshot callback failed for message ${message.id.slice(0, 8)}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
7999
|
+
conversation_id: conv.id,
|
|
8000
|
+
message_id: message.id
|
|
8001
|
+
});
|
|
8002
|
+
}
|
|
8003
|
+
}
|
|
7775
8004
|
)
|
|
7776
8005
|
);
|
|
7777
8006
|
} catch (err) {
|
|
@@ -7779,6 +8008,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7779
8008
|
if (this.isV2 && err instanceof OpenCodeV2PromptAckError) {
|
|
7780
8009
|
const errorMessage4 = err instanceof Error ? err.message : String(err);
|
|
7781
8010
|
this.untrackableAck.set(message.id, errorMessage4);
|
|
8011
|
+
this.untrackableAckSessionIds.set(message.id, null);
|
|
7782
8012
|
this.log({
|
|
7783
8013
|
level: "error",
|
|
7784
8014
|
message: `V2 prompt dispatch for message ${message.id.slice(0, 8)} failed after a positive ack with no usable id: ${errorMessage4}`,
|
|
@@ -7854,39 +8084,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7854
8084
|
if (this.isV2) {
|
|
7855
8085
|
throw new Error("V2 prompt dispatch completed without an acknowledged message id");
|
|
7856
8086
|
}
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
message_id: message.id
|
|
7864
|
-
});
|
|
7865
|
-
this.signalDispatchNotStarted(conv, message, "readback_unconfirmed");
|
|
7866
|
-
continue;
|
|
7867
|
-
}
|
|
7868
|
-
this.unconfirmedDispatchFailures.delete(message.id);
|
|
7869
|
-
this.sessions.delete(conv.id);
|
|
7870
|
-
this.supersede(conv.id, sessionId);
|
|
7871
|
-
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.`;
|
|
8087
|
+
this.awaitingCorrelation.set(message.id, {
|
|
8088
|
+
sessionId,
|
|
8089
|
+
content: message.content,
|
|
8090
|
+
attempts: 0,
|
|
8091
|
+
knownUserIds: preDispatchUserIds
|
|
8092
|
+
});
|
|
7872
8093
|
this.log({
|
|
7873
|
-
level: "
|
|
7874
|
-
message:
|
|
8094
|
+
level: "warn",
|
|
8095
|
+
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`,
|
|
7875
8096
|
conversation_id: conv.id,
|
|
7876
8097
|
message_id: message.id
|
|
7877
8098
|
});
|
|
7878
|
-
|
|
7879
|
-
this.log({
|
|
7880
|
-
level: "warn",
|
|
7881
|
-
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)}`,
|
|
7882
|
-
conversation_id: conv.id,
|
|
7883
|
-
message_id: message.id
|
|
7884
|
-
});
|
|
7885
|
-
this.signalDispatchNotStarted(conv, message, "abandon_unreported");
|
|
7886
|
-
});
|
|
8099
|
+
this.signalDispatchNotStarted(conv, message, "readback_unconfirmed");
|
|
7887
8100
|
break;
|
|
7888
8101
|
}
|
|
7889
|
-
this.unconfirmedDispatchFailures.delete(message.id);
|
|
7890
8102
|
this.dispatchNotStartedSignalled.delete(message.id);
|
|
7891
8103
|
this.releasedOpencodeIds.delete(message.id);
|
|
7892
8104
|
this.dispatched.add(message.id);
|
|
@@ -8491,23 +8703,6 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8491
8703
|
this.redrivePollFailures.set(messageId, { sessionId, signature, count: 1 });
|
|
8492
8704
|
return 1;
|
|
8493
8705
|
}
|
|
8494
|
-
/**
|
|
8495
|
-
* Record one UNCONFIRMED-dispatch outcome (a `pending` row with no stored
|
|
8496
|
-
* `opencode_message_id` whose `sendPromptAsync` returned `null`) toward the
|
|
8497
|
-
* bound in `processConversation`'s dispatch loop, and return the resulting
|
|
8498
|
-
* count. Mirrors `recordRedrivePollFailure`'s session-scoping: a session
|
|
8499
|
-
* change starts a fresh streak at `1` rather than inheriting the old one's
|
|
8500
|
-
* count, since a new session is a genuinely different attempt.
|
|
8501
|
-
*/
|
|
8502
|
-
recordUnconfirmedDispatch(messageId, sessionId) {
|
|
8503
|
-
const existing = this.unconfirmedDispatchFailures.get(messageId);
|
|
8504
|
-
if (existing && existing.sessionId === sessionId) {
|
|
8505
|
-
existing.count += 1;
|
|
8506
|
-
return existing.count;
|
|
8507
|
-
}
|
|
8508
|
-
this.unconfirmedDispatchFailures.set(messageId, { sessionId, count: 1 });
|
|
8509
|
-
return 1;
|
|
8510
|
-
}
|
|
8511
8706
|
/** Record one queued give-up toward the never-started bound. */
|
|
8512
8707
|
recordNeverStartedAttempt(messageId, sessionId) {
|
|
8513
8708
|
const existing = this.neverStartedAttempts.get(messageId);
|
|
@@ -10094,11 +10289,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10094
10289
|
}
|
|
10095
10290
|
}
|
|
10096
10291
|
}
|
|
10097
|
-
if (this.untrackableAck.size > 0 || this.neverStartedAttempts.size > 0) {
|
|
10292
|
+
if (this.untrackableAck.size > 0 || this.neverStartedAttempts.size > 0 || this.awaitingCorrelation.size > 0) {
|
|
10098
10293
|
const stillProcessing = new Set(rows.map((r) => r.id));
|
|
10294
|
+
for (const id of this.awaitingCorrelation.keys()) {
|
|
10295
|
+
if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
|
|
10296
|
+
this.awaitingCorrelation.delete(id);
|
|
10297
|
+
this.log({
|
|
10298
|
+
level: "debug",
|
|
10299
|
+
message: `Re-adopt: message ${id.slice(0, 8)} left processing and pending \u2014 cleared awaiting-correlation fence`,
|
|
10300
|
+
message_id: id
|
|
10301
|
+
});
|
|
10302
|
+
}
|
|
10303
|
+
}
|
|
10099
10304
|
for (const id of [...this.untrackableAck.keys()]) {
|
|
10100
10305
|
if (!stillProcessing.has(id) && !this.pendingMessageIds.has(id)) {
|
|
10101
10306
|
this.untrackableAck.delete(id);
|
|
10307
|
+
this.untrackableAckSessionIds.delete(id);
|
|
10102
10308
|
this.untrackableAckSignalled.delete(id);
|
|
10103
10309
|
this.untrackableAckWarned.delete(id);
|
|
10104
10310
|
this.log({
|
|
@@ -10138,7 +10344,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10138
10344
|
bySession.set(row.opencode_session_id, list);
|
|
10139
10345
|
}
|
|
10140
10346
|
for (const [sessionId, sessionRows] of bySession) {
|
|
10141
|
-
let messages;
|
|
10347
|
+
let messages = null;
|
|
10142
10348
|
try {
|
|
10143
10349
|
if (this.isV2) {
|
|
10144
10350
|
const snapshot = await this.getSessionMessages(sessionId);
|
|
@@ -10147,9 +10353,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10147
10353
|
level: "warn",
|
|
10148
10354
|
message: `Re-adopt: polling session ${sessionId.slice(0, 8)} returned an unreadable message snapshot \u2014 skipping this session this tick`
|
|
10149
10355
|
});
|
|
10150
|
-
|
|
10356
|
+
} else {
|
|
10357
|
+
messages = snapshot;
|
|
10151
10358
|
}
|
|
10152
|
-
messages = snapshot;
|
|
10153
10359
|
} else {
|
|
10154
10360
|
const polled = await pollSessionMessagesForRedrive(
|
|
10155
10361
|
this.port,
|
|
@@ -10162,15 +10368,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10162
10368
|
level: "warn",
|
|
10163
10369
|
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`
|
|
10164
10370
|
});
|
|
10165
|
-
|
|
10371
|
+
} else {
|
|
10372
|
+
messages = polled.messages;
|
|
10166
10373
|
}
|
|
10167
|
-
messages = polled.messages;
|
|
10168
10374
|
}
|
|
10169
10375
|
} catch (err) {
|
|
10170
10376
|
this.log({
|
|
10171
10377
|
level: "warn",
|
|
10172
10378
|
message: `Re-adopt: failed to poll session ${sessionId.slice(0, 8)} (will retry next drain): ${err instanceof Error ? err.message : String(err)}`
|
|
10173
10379
|
});
|
|
10380
|
+
}
|
|
10381
|
+
if (messages === null) {
|
|
10382
|
+
for (const row of sessionRows) {
|
|
10383
|
+
if (this.awaitingCorrelation.has(row.id)) {
|
|
10384
|
+
await this.advanceReadoptCorrelation(sessionId, row, null);
|
|
10385
|
+
}
|
|
10386
|
+
}
|
|
10174
10387
|
continue;
|
|
10175
10388
|
}
|
|
10176
10389
|
const anyUntracked = sessionRows.some((row) => !this.isTracked(sessionId, row.id));
|
|
@@ -10180,6 +10393,59 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10180
10393
|
}
|
|
10181
10394
|
}
|
|
10182
10395
|
}
|
|
10396
|
+
/** Advance a re-adopted accepted prompt through the shared correlation fence. */
|
|
10397
|
+
async advanceReadoptCorrelation(sessionId, row, messages) {
|
|
10398
|
+
const correlation = this.awaitingCorrelation.get(row.id);
|
|
10399
|
+
if (!correlation) return false;
|
|
10400
|
+
correlation.attempts += 1;
|
|
10401
|
+
const correlatedId = messages === null ? null : this.findAwaitingCorrelationId(correlation, sessionId, messages);
|
|
10402
|
+
if (correlatedId !== null) {
|
|
10403
|
+
this.awaitingCorrelation.delete(row.id);
|
|
10404
|
+
const conv = this.convForRow(sessionId, row);
|
|
10405
|
+
const message = this.queuedMessageForRow(row);
|
|
10406
|
+
const processedAtMs = this.processedAtMs(row);
|
|
10407
|
+
this.registerReadopted(conv, sessionId, message, correlatedId, processedAtMs, processedAtMs);
|
|
10408
|
+
this.dispatched.add(row.id);
|
|
10409
|
+
this.readopted.add(row.id);
|
|
10410
|
+
this.ensureWatcherRunning(sessionId);
|
|
10411
|
+
this.log({
|
|
10412
|
+
level: "info",
|
|
10413
|
+
message: `Re-adopt: matched accepted message ${row.id.slice(0, 8)} to OpenCode turn ${correlatedId.slice(0, 8)} without sending it again`,
|
|
10414
|
+
conversation_id: row.conversation_id,
|
|
10415
|
+
message_id: row.id
|
|
10416
|
+
});
|
|
10417
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_redispatched");
|
|
10418
|
+
void this.postSignal(row.conversation_id, row.id, "ack_correlated", {
|
|
10419
|
+
opencode_message_id: correlatedId
|
|
10420
|
+
});
|
|
10421
|
+
return true;
|
|
10422
|
+
}
|
|
10423
|
+
if (correlation.knownUserIds === null) {
|
|
10424
|
+
this.log({
|
|
10425
|
+
level: "debug",
|
|
10426
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} cannot be correlated because its pre-dispatch session list was unreadable`,
|
|
10427
|
+
conversation_id: row.conversation_id,
|
|
10428
|
+
message_id: row.id
|
|
10429
|
+
});
|
|
10430
|
+
} else if (correlation.sessionId !== sessionId) {
|
|
10431
|
+
this.log({
|
|
10432
|
+
level: "warn",
|
|
10433
|
+
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`,
|
|
10434
|
+
conversation_id: row.conversation_id,
|
|
10435
|
+
message_id: row.id
|
|
10436
|
+
});
|
|
10437
|
+
}
|
|
10438
|
+
if (correlation.attempts >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
|
|
10439
|
+
await this.handoffUntrackableCorrelation(
|
|
10440
|
+
row.conversation_id,
|
|
10441
|
+
row.id,
|
|
10442
|
+
correlation.sessionId,
|
|
10443
|
+
correlation.attempts,
|
|
10444
|
+
"readopt"
|
|
10445
|
+
);
|
|
10446
|
+
}
|
|
10447
|
+
return true;
|
|
10448
|
+
}
|
|
10183
10449
|
/**
|
|
10184
10450
|
* Re-adopt ONE `processing` row against the tick's session message snapshot
|
|
10185
10451
|
* (ADR-0046 Decision §1/§2). Idempotent: skips a row already being driven.
|
|
@@ -10187,8 +10453,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10187
10453
|
* Branches on `messageRunState(messages, row.opencode_message_id)` — the
|
|
10188
10454
|
* opencode-assigned user-message id persisted on the first `processing` PATCH
|
|
10189
10455
|
* (#218). A row with a NULL stored id (dispatched but the read-back never landed
|
|
10190
|
-
* before the restart)
|
|
10191
|
-
*
|
|
10456
|
+
* before the restart) is first offered to an awaiting-correlation fence recorded
|
|
10457
|
+
* by this process; without that fence, it has no id to correlate after restart
|
|
10458
|
+
* and is treated as an orphan and re-dispatched (at most once, see
|
|
10459
|
+
* `forceReadoptRun`):
|
|
10192
10460
|
* - `done` → `markDone` now (guarded like the watcher's done branch);
|
|
10193
10461
|
* - `failed` → `markFailed` with the surfaced error (issue #182), so an
|
|
10194
10462
|
* errored turn is reported failed on restart, NOT re-dispatched —
|
|
@@ -10197,7 +10465,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10197
10465
|
* re-dispatched instead (issue #1310, see the branch below);
|
|
10198
10466
|
* - `running`/`queued` → re-attach a watcher via `registerReadopted` (no re-dispatch),
|
|
10199
10467
|
* tracking the stored id so the reply correlates by it;
|
|
10200
|
-
* - `unknown`/null id →
|
|
10468
|
+
* - `unknown`/null id → correlate through an awaiting fence when present;
|
|
10469
|
+
* otherwise re-dispatch (opencode assigns a fresh id) + attach a watcher.
|
|
10201
10470
|
*
|
|
10202
10471
|
* Only `ChannelAuthError` propagates.
|
|
10203
10472
|
*/
|
|
@@ -10211,6 +10480,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10211
10480
|
});
|
|
10212
10481
|
return;
|
|
10213
10482
|
}
|
|
10483
|
+
if (await this.advanceReadoptCorrelation(sessionId, row, messages)) return;
|
|
10214
10484
|
const ocId = row.opencode_message_id;
|
|
10215
10485
|
const state = messageRunState(messages, ocId ?? "");
|
|
10216
10486
|
if (state === "done") {
|
|
@@ -10277,12 +10547,48 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10277
10547
|
await this.reportSubagentAuthFailures(row.conversation_id, ocId ?? "", row.id, messages);
|
|
10278
10548
|
this.dontRedispatch.delete(row.id);
|
|
10279
10549
|
this.untrackableAck.delete(row.id);
|
|
10550
|
+
this.untrackableAckSessionIds.delete(row.id);
|
|
10280
10551
|
this.untrackableAckSignalled.delete(row.id);
|
|
10281
10552
|
this.untrackableAckWarned.delete(row.id);
|
|
10282
10553
|
void this.postSignal(row.conversation_id, row.id, "readopt_failed");
|
|
10283
10554
|
return;
|
|
10284
10555
|
}
|
|
10285
|
-
if (this.
|
|
10556
|
+
if (this.untrackableAck.has(row.id)) {
|
|
10557
|
+
if (this.untrackableAckSignalled.has(row.id)) {
|
|
10558
|
+
const firstFenceWarning = !this.untrackableAckWarned.has(row.id);
|
|
10559
|
+
this.untrackableAckWarned.add(row.id);
|
|
10560
|
+
this.log({
|
|
10561
|
+
level: firstFenceWarning ? "warn" : "debug",
|
|
10562
|
+
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`,
|
|
10563
|
+
conversation_id: row.conversation_id,
|
|
10564
|
+
message_id: row.id
|
|
10565
|
+
});
|
|
10566
|
+
const errorMessage3 = this.untrackableAck.get(row.id);
|
|
10567
|
+
const reportError = await this.reportUntrackableAck(
|
|
10568
|
+
row.conversation_id,
|
|
10569
|
+
row.id,
|
|
10570
|
+
this.untrackableAckSessionIds.get(row.id) ?? null,
|
|
10571
|
+
errorMessage3
|
|
10572
|
+
);
|
|
10573
|
+
if (reportError !== null) {
|
|
10574
|
+
this.log({
|
|
10575
|
+
level: "warn",
|
|
10576
|
+
message: `Re-adopt: failed to report untrackable message ${row.id.slice(0, 8)} (will retry next drain): ${reportError.message}`,
|
|
10577
|
+
conversation_id: row.conversation_id,
|
|
10578
|
+
message_id: row.id
|
|
10579
|
+
});
|
|
10580
|
+
}
|
|
10581
|
+
} else {
|
|
10582
|
+
this.log({
|
|
10583
|
+
level: "debug",
|
|
10584
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
|
|
10585
|
+
conversation_id: row.conversation_id,
|
|
10586
|
+
message_id: row.id
|
|
10587
|
+
});
|
|
10588
|
+
}
|
|
10589
|
+
return;
|
|
10590
|
+
}
|
|
10591
|
+
if (this.dontRedispatch.has(row.id)) {
|
|
10286
10592
|
this.log({
|
|
10287
10593
|
level: "debug",
|
|
10288
10594
|
message: `Re-adopt: message ${row.id.slice(0, 8)} already gave up \u2014 left to the cron; skipping until it leaves processing`,
|
|
@@ -10460,6 +10766,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10460
10766
|
}
|
|
10461
10767
|
this.dontRedispatch.delete(row.id);
|
|
10462
10768
|
this.untrackableAck.delete(row.id);
|
|
10769
|
+
this.untrackableAckSessionIds.delete(row.id);
|
|
10463
10770
|
this.untrackableAckSignalled.delete(row.id);
|
|
10464
10771
|
this.untrackableAckWarned.delete(row.id);
|
|
10465
10772
|
void this.postSignal(row.conversation_id, row.id, "readopt_done");
|
|
@@ -10479,8 +10786,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10479
10786
|
* latch makes a null-id row re-dispatched AT MOST ONCE per outstanding read-back:
|
|
10480
10787
|
* short-circuit while the row is latched; clear it on a successful dispatch (the
|
|
10481
10788
|
* row is then tracked in `dispatched`, so `readoptOne`'s early skip prevents
|
|
10482
|
-
* re-entry)
|
|
10483
|
-
*
|
|
10789
|
+
* re-entry) or when the dispatch itself failed before acceptance. An accepted V1
|
|
10790
|
+
* dispatch whose id is not readable enters the bounded correlation fence instead
|
|
10791
|
+
* of retrying; exhaustion hands it to `untrackableAck`.
|
|
10484
10792
|
*
|
|
10485
10793
|
* `evidentMessageId = row.id` addresses the SERVER row. Deadline anchored to
|
|
10486
10794
|
* `processed_at` (Invariant 1).
|
|
@@ -10543,6 +10851,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10543
10851
|
);
|
|
10544
10852
|
}
|
|
10545
10853
|
let ocId;
|
|
10854
|
+
let preDispatchUserIds = null;
|
|
10546
10855
|
try {
|
|
10547
10856
|
ocId = this.isV2 ? await sendV2Prompt(this.openCodeClient, sessionId, row.content) : await this.dispatchLocked(
|
|
10548
10857
|
sessionId,
|
|
@@ -10552,7 +10861,20 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10552
10861
|
row.content,
|
|
10553
10862
|
options,
|
|
10554
10863
|
sendAttachments,
|
|
10555
|
-
this.openCodeClient
|
|
10864
|
+
this.openCodeClient,
|
|
10865
|
+
{
|
|
10866
|
+
onKnownUserIds: (ids) => {
|
|
10867
|
+
preDispatchUserIds = ids;
|
|
10868
|
+
},
|
|
10869
|
+
onKnownUserIdsError: (error2) => {
|
|
10870
|
+
this.log({
|
|
10871
|
+
level: "warn",
|
|
10872
|
+
message: `Pre-dispatch snapshot callback failed for message ${row.id.slice(0, 8)}: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
10873
|
+
conversation_id: row.conversation_id,
|
|
10874
|
+
message_id: row.id
|
|
10875
|
+
});
|
|
10876
|
+
}
|
|
10877
|
+
}
|
|
10556
10878
|
)
|
|
10557
10879
|
);
|
|
10558
10880
|
} catch (err) {
|
|
@@ -10585,6 +10907,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10585
10907
|
}
|
|
10586
10908
|
} else {
|
|
10587
10909
|
this.untrackableAck.set(row.id, errorMessage3);
|
|
10910
|
+
this.untrackableAckSessionIds.set(row.id, sessionId);
|
|
10588
10911
|
}
|
|
10589
10912
|
this.log({
|
|
10590
10913
|
level: "error",
|
|
@@ -10633,39 +10956,20 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
10633
10956
|
}
|
|
10634
10957
|
if (ocId === null) {
|
|
10635
10958
|
this.awaitingReadopt.delete(row.id);
|
|
10636
|
-
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
|
|
10640
|
-
|
|
10641
|
-
|
|
10642
|
-
this.log({
|
|
10643
|
-
level: "error",
|
|
10644
|
-
message: errorMessage3,
|
|
10645
|
-
conversation_id: row.conversation_id,
|
|
10646
|
-
message_id: row.id
|
|
10647
|
-
});
|
|
10648
|
-
await this.markFailed(row.conversation_id, row.id, null, errorMessage3).catch((markErr) => {
|
|
10649
|
-
this.log({
|
|
10650
|
-
level: "warn",
|
|
10651
|
-
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)}`,
|
|
10652
|
-
conversation_id: row.conversation_id,
|
|
10653
|
-
message_id: row.id
|
|
10654
|
-
});
|
|
10655
|
-
});
|
|
10656
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
10657
|
-
return;
|
|
10658
|
-
}
|
|
10959
|
+
this.awaitingCorrelation.set(row.id, {
|
|
10960
|
+
sessionId,
|
|
10961
|
+
content: row.content,
|
|
10962
|
+
attempts: 0,
|
|
10963
|
+
knownUserIds: preDispatchUserIds
|
|
10964
|
+
});
|
|
10659
10965
|
this.log({
|
|
10660
10966
|
level: "warn",
|
|
10661
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)}
|
|
10967
|
+
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`,
|
|
10662
10968
|
conversation_id: row.conversation_id,
|
|
10663
10969
|
message_id: row.id
|
|
10664
10970
|
});
|
|
10665
|
-
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
10666
10971
|
return;
|
|
10667
10972
|
}
|
|
10668
|
-
this.unconfirmedDispatchFailures.delete(row.id);
|
|
10669
10973
|
const processedAtMs = this.processedAtMs(row);
|
|
10670
10974
|
this.registerReadopted(
|
|
10671
10975
|
readoptConv,
|
|
@@ -14144,6 +14448,34 @@ async function run(options) {
|
|
|
14144
14448
|
} else {
|
|
14145
14449
|
log2(state, "Not running in a MicroVM (MICROVM_ID unset) \u2014 nothing to report", "debug");
|
|
14146
14450
|
}
|
|
14451
|
+
if (options.restoreRunnerCredentials || options.credentialSyncMarker) {
|
|
14452
|
+
const logCredentialVaultResolution = (message, level) => {
|
|
14453
|
+
log2(state, message, level);
|
|
14454
|
+
if (state.interactive && !state.json) {
|
|
14455
|
+
logActivity(state, { type: "info", level, message });
|
|
14456
|
+
}
|
|
14457
|
+
};
|
|
14458
|
+
const vaultResolution = await getRunnerCredentialVault(state.agentId, state.authHeader);
|
|
14459
|
+
if (vaultResolution.error) {
|
|
14460
|
+
delete process.env.CREDENTIAL_VAULT_PREFIX;
|
|
14461
|
+
logCredentialVaultResolution(
|
|
14462
|
+
`CREDENTIAL-VAULT-LOOKUP-FAILED: ${vaultResolution.error}; credentials will be read from and written to this runner's own prefix this boot`,
|
|
14463
|
+
"warn"
|
|
14464
|
+
);
|
|
14465
|
+
} else if (vaultResolution.prefix !== null && vaultResolution.vaultId !== null && vaultResolution.source !== null) {
|
|
14466
|
+
process.env.CREDENTIAL_VAULT_PREFIX = vaultResolution.prefix;
|
|
14467
|
+
logCredentialVaultResolution(
|
|
14468
|
+
`CREDENTIAL-VAULT: source=${vaultResolution.source} vault=${vaultResolution.vaultId} prefix=${vaultResolution.prefix}`,
|
|
14469
|
+
"info"
|
|
14470
|
+
);
|
|
14471
|
+
} else {
|
|
14472
|
+
delete process.env.CREDENTIAL_VAULT_PREFIX;
|
|
14473
|
+
logCredentialVaultResolution(
|
|
14474
|
+
"CREDENTIAL-VAULT-NONE: this runner is not linked to a credential vault; using its own prefix",
|
|
14475
|
+
"debug"
|
|
14476
|
+
);
|
|
14477
|
+
}
|
|
14478
|
+
}
|
|
14147
14479
|
if (options.restoreRunnerCredentials) {
|
|
14148
14480
|
log2(state, "Restoring runner credentials before starting OpenCode");
|
|
14149
14481
|
const credentialContext = {
|
|
@@ -14541,14 +14873,14 @@ async function run(options) {
|
|
|
14541
14873
|
},
|
|
14542
14874
|
onDisconnected: (code, reason, transition) => {
|
|
14543
14875
|
state.connected = false;
|
|
14544
|
-
const
|
|
14876
|
+
const safeReason2 = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
|
|
14545
14877
|
logActivity(state, {
|
|
14546
14878
|
type: "info",
|
|
14547
|
-
message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${
|
|
14879
|
+
message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason2})`
|
|
14548
14880
|
});
|
|
14549
14881
|
emitAgentDisconnected(state.agentId, {
|
|
14550
14882
|
code,
|
|
14551
|
-
reason:
|
|
14883
|
+
reason: safeReason2,
|
|
14552
14884
|
connection_generation: transition.generation
|
|
14553
14885
|
});
|
|
14554
14886
|
if (state.interactive) displayStatus(state);
|
|
@@ -14711,6 +15043,7 @@ program.command("login").description("Authenticate with Evident").option("--toke
|
|
|
14711
15043
|
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 }));
|
|
14712
15044
|
program.command("whoami").description("Show the currently logged in user").action(whoami);
|
|
14713
15045
|
program.command("status").description("Check whether the configured credentials can reach Evident").option("--json", "Output in JSON format").action((options) => status({ json: options.json }));
|
|
15046
|
+
program.command("credential-vault-env").description("Print the shell assignment a runner image evals before starting the synchroniser").action(credentialVaultEnv);
|
|
14714
15047
|
program.command("claude-usage").description("[spike] Show Claude subscription usage (requires a local `claude login`)").action(claudeUsage);
|
|
14715
15048
|
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(
|
|
14716
15049
|
"-a, --agent [id]",
|