@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 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.2bb04b2" : void 0) ?? process.env.npm_package_version ?? "unknown";
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
- async function sendPromptAsync(port, sessionId, content, options, attachments, client) {
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
- if (after) {
2655
- let best = null;
2656
- for (const m of after) {
2657
- if (roleOf2(m) !== "user") continue;
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
- if (lastCorrelated === null) lastCorrelated = m;
2698
- if (errorOf(m) == null) {
2699
- lastNonErrored = m;
2700
- break;
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 lastOk = null;
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
- if (errorOf(messages[i]) == null) lastOk = messages[i];
2825
+ lastAssistantIndex = i;
2714
2826
  }
2715
2827
  }
2716
- return lastOk ?? last;
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 nonErrored = messages.filter((message) => errorOf(message) == null);
2823
- const selected = nonErrored.length > 0 ? nonErrored : messages;
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 (byParent.length > 0) {
2883
- correlated = byParent;
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
- if (typeof error2 === "string") return error2;
2930
- if (typeof error2 === "object") {
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") return dataMessage;
2934
- if (typeof e.message === "string") return e.message;
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
- return "The agent run failed.";
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 message is genuinely un-sent,
7227
- * so the NEXT tick may retry exactly once more).
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
- const streak = this.recordUnconfirmedDispatch(message.id, sessionId);
7858
- if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
7859
- this.log({
7860
- level: "warn",
7861
- message: `Message ${message.id.slice(0, 8)} dispatched but its opencode id could not be read back (${streak}/${MAX_IDENTICAL_REDRIVE_POLL_FAILURES}) \u2014 leaving un-tracked to retry next tick`,
7862
- conversation_id: conv.id,
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: "error",
7874
- message: errorMessage3,
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
- await this.markFailed(conv.id, message.id, null, errorMessage3).catch((markErr) => {
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
- continue;
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
- continue;
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) has no id to correlate → treated as an orphan and
10191
- * re-dispatched (at most once, see `forceReadoptRun`):
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 → re-dispatch (opencode assigns a fresh id) + attach a watcher.
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.dontRedispatch.has(row.id) || this.untrackableAck.has(row.id)) {
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) OR on a failed/unresolved dispatch (genuinely un-sent → the next tick
10483
- * may retry exactly once more).
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
- const streak = this.recordUnconfirmedDispatch(row.id, sessionId);
10637
- if (streak >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
10638
- this.unconfirmedDispatchFailures.delete(row.id);
10639
- this.sessions.delete(readoptConv.id);
10640
- this.supersede(readoptConv.id, sessionId);
10641
- const errorMessage3 = `OpenCode accepted this re-dispatched 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.`;
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)} re-dispatched but its opencode id could not be read back (${streak}/${MAX_IDENTICAL_REDRIVE_POLL_FAILURES}) \u2014 leaving un-tracked to retry next drain`,
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 safeReason = redactDiagnosticText(reason).slice(0, MAX_DISCONNECT_REASON_LENGTH);
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: ${safeReason})`
14879
+ message: `Tunnel disconnected (generation: ${transition.generation}, code: ${code}, reason: ${safeReason2})`
14548
14880
  });
14549
14881
  emitAgentDisconnected(state.agentId, {
14550
14882
  code,
14551
- reason: safeReason,
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]",