@khalilgharbaoui/opencode-claude-code-plugin 0.60.1 → 0.60.2

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.d.ts CHANGED
@@ -1634,22 +1634,22 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
1634
1634
  * switch, and outside `runModelChain` on purpose: an account switch starts
1635
1635
  * that account's own model chain from the top.
1636
1636
  *
1637
- * Anything but `"auto"` is `runModelChain` and one `await`, so `"off"` and
1638
- * `"ask"` are exactly what they were. With `"auto"`, the attempt that failed
1639
- * decides the switch itself (`completeResult` sets `run.switchTo` and closes
1640
- * its stream without a `finish`), and the next attempt's prologue makes the
1641
- * switch the form's pick would have made. This only splices the attempts
1642
- * into one caller turn: one `stream-start`, and the `finish` of whichever
1643
- * attempt did not switch.
1637
+ * Anything but `"auto"` is `runModelChain` behind `stopOnCancel`. With
1638
+ * `"auto"`, the attempt that failed decides the switch itself
1639
+ * (`completeResult` sets `run.switchTo` and closes its stream without a
1640
+ * `finish`), and the next attempt's prologue makes the switch the form's pick
1641
+ * would have made. This only splices the attempts into one caller turn: one
1642
+ * `stream-start`, and the `finish` of whichever attempt did not switch.
1644
1643
  *
1645
1644
  * The bound is `run.tried`, as it is `tried` for the model chain: every
1646
1645
  * account spawned or switched to goes in, and none in it is picked again.
1647
1646
  *
1648
- * Cancelling the returned stream is a stop, for every attempt: this loop is
1649
- * the plugin starting more work on the caller's behalf, so a caller that
1650
- * has gone away must reach all of it. Each attempt gets a signal that the
1651
- * caller's own `abortSignal` and a `cancel()` both abort, and the loop
1652
- * starts no further account once either has happened.
1647
+ * Cancelling the returned stream is a stop, in every mode: a caller that has
1648
+ * gone away must reach the turn it started, or the CLI runs it to the end
1649
+ * and bills it. Each attempt gets a signal that the caller's own
1650
+ * `abortSignal` and a `cancel()` both abort, so a cancel takes the turn's
1651
+ * ordinary abort path, and with `"auto"` the loop starts no further account
1652
+ * once either has happened. Until (h #g257) only `"auto"` did this.
1653
1653
  */
1654
1654
  private runAccountFailover;
1655
1655
  /**
package/dist/index.js CHANGED
@@ -1998,17 +1998,21 @@ function describeReset(resetsAt) {
1998
1998
  }
1999
1999
  function createAccountFailoverQuestionCall(sessionKey2, input, toolCallId = `${ACCOUNT_FAILOVER_TOOL_CALL_PREFIX}${Math.random().toString(36).slice(2, 10)}`) {
2000
2000
  const source = normalizeAccountName(input.sourceAccount || DEFAULT_ACCOUNT);
2001
+ const failed = normalizeAccountName(input.failedAccount || source);
2001
2002
  const candidates = input.candidates.map((c) => normalizeAccountName(c));
2002
2003
  const resets = describeReset(input.resetsAt);
2003
2004
  const until = resets ?? "opencode restarts";
2004
- const question = input.reason ? `The Claude account "${source}" ${input.reason}. Continue this task on another configured account? Leaving this unanswered waits, at no cost.` : [
2005
- `The Claude account "${source}" is out of usage`,
2005
+ const question = input.reason ? `The Claude account "${failed}" ${input.reason}. Continue this task on another configured account? Leaving this unanswered waits, at no cost.` : [
2006
+ `The Claude account "${failed}" is out of usage`,
2006
2007
  input.window ? ` in ${input.window}` : "",
2007
2008
  resets ? `, which resets at ${resets}` : "",
2008
2009
  ". Continue this task on another configured account? Leaving this unanswered waits, at no cost."
2009
2010
  ].join("");
2010
2011
  pendingQuestions2.set(pendingKey2(sessionKey2, toolCallId), {
2011
2012
  sourceAccount: source,
2013
+ // Only after a chain of overrides; otherwise the two are one account.
2014
+ ...failed !== source ? { failedAccount: failed } : {},
2015
+ ...input.reason ? { reason: input.reason } : {},
2012
2016
  candidates: [...candidates],
2013
2017
  resetsAt: input.resetsAt,
2014
2018
  question
@@ -2024,7 +2028,7 @@ function createAccountFailoverQuestionCall(sessionKey2, input, toolCallId = `${A
2024
2028
  options: [
2025
2029
  ...candidates.map((candidate) => ({
2026
2030
  label: candidate,
2027
- description: input.carryTranscript === false ? `Run on "${candidate}" until ${until}. The conversation is replayed as a fresh Claude session, and any MCP server configured only in "${source}"'s Claude profile will be missing.` : `Run on "${candidate}" until ${until}. The conversation continues where it stopped if its Claude transcript can be carried across, and is replayed as a fresh Claude session if it cannot; either way, any MCP server configured only in "${source}"'s Claude profile will be missing.`
2031
+ description: input.carryTranscript === false ? `Run on "${candidate}" until ${until}. The conversation is replayed as a fresh Claude session, and any MCP server configured only in "${failed}"'s Claude profile will be missing.` : `Run on "${candidate}" until ${until}. The conversation continues where it stopped if its Claude transcript can be carried across, and is replayed as a fresh Claude session if it cannot; either way, any MCP server configured only in "${failed}"'s Claude profile will be missing.`
2028
2032
  })),
2029
2033
  {
2030
2034
  label: STOP_ANSWER,
@@ -2068,6 +2072,8 @@ function classify(pending2, part) {
2068
2072
  kind: "switch",
2069
2073
  target,
2070
2074
  sourceAccount: pending2.sourceAccount,
2075
+ ...pending2.failedAccount ? { failedAccount: pending2.failedAccount } : {},
2076
+ ...pending2.reason ? { reason: pending2.reason } : {},
2071
2077
  resetsAt: pending2.resetsAt
2072
2078
  };
2073
2079
  }
@@ -2095,6 +2101,7 @@ function consumeAccountFailoverAnswer(sessionKey2, prompt, fallback) {
2095
2101
  return classify(
2096
2102
  {
2097
2103
  sourceAccount: normalizeAccountName(fallback.sourceAccount || DEFAULT_ACCOUNT),
2104
+ ...fallback.failedAccount ? { failedAccount: normalizeAccountName(fallback.failedAccount) } : {},
2098
2105
  candidates: fallback.candidates.map((c) => normalizeAccountName(c))
2099
2106
  },
2100
2107
  orphan
@@ -2105,8 +2112,9 @@ function consumeAccountFailoverAnswer(sessionKey2, prompt, fallback) {
2105
2112
  }
2106
2113
  function formatFailoverNote(input) {
2107
2114
  const resets = describeReset(input.resetsAt);
2115
+ const arrival = input.carried ? "The conversation continues where it stopped." : "The conversation is replayed into a fresh Claude session there.";
2108
2116
  return `
2109
- ${FAILOVER_MARKER} "${input.sourceAccount}" is out of usage, so this conversation continues on "${input.target}" ${resets ? `until ${resets}` : "until opencode restarts"}. Claude cannot resume a session across accounts, so the thread is being replayed into a fresh one.
2117
+ ${FAILOVER_MARKER} "${input.sourceAccount}" ${input.reason ?? "is out of usage"}, so this conversation continues on "${input.target}" ${resets ? `until ${resets}` : "until opencode restarts"}. ${arrival}
2110
2118
  `;
2111
2119
  }
2112
2120
  function formatAutoFailoverNote(input) {
@@ -3800,13 +3808,27 @@ import { createHash } from "crypto";
3800
3808
  var fingerprints = /* @__PURE__ */ new Map();
3801
3809
  var fingerprintSeq = 0;
3802
3810
  var MAX_FORK_FINGERPRINTS = 64;
3803
- function hashPart(hash, part) {
3811
+ var DIGEST_VERSION = 2;
3812
+ var DCP_MESSAGE_ID_TAG = /<dcp-message-id(?:\s[^>]*)?>[^<]*<\/dcp-message-id>/g;
3813
+ var DCP_COMPACT_REF_LINE = /^@(?:\d+|b\d+|blocked)@(?: \[[^\]\n]*\])?$/;
3814
+ function withoutDcpRefs(text) {
3815
+ let out = text.includes("<dcp-message-id") ? text.replace(DCP_MESSAGE_ID_TAG, "") : text;
3816
+ for (; ; ) {
3817
+ const trimmed = out.trimEnd();
3818
+ if (!trimmed.endsWith("@") && !trimmed.endsWith("]")) return out;
3819
+ const lineStart = trimmed.lastIndexOf("\n") + 1;
3820
+ if (!DCP_COMPACT_REF_LINE.test(trimmed.slice(lineStart))) return out;
3821
+ out = trimmed.slice(0, lineStart);
3822
+ }
3823
+ }
3824
+ function hashPart(hash, part, version) {
3804
3825
  const p = part;
3805
3826
  const type = typeof p?.type === "string" ? p.type : "unknown";
3827
+ const content = (text) => version >= 2 ? withoutDcpRefs(text) : text;
3806
3828
  switch (type) {
3807
3829
  case "text":
3808
3830
  hash.update("text\0");
3809
- hash.update(String(p?.text ?? ""));
3831
+ hash.update(content(String(p?.text ?? "")));
3810
3832
  break;
3811
3833
  case "tool-call":
3812
3834
  hash.update("call\0");
@@ -3818,7 +3840,7 @@ function hashPart(hash, part) {
3818
3840
  hash.update("result\0");
3819
3841
  hash.update(String(p?.toolName ?? ""));
3820
3842
  hash.update("\0");
3821
- hash.update(stableJson(p?.output));
3843
+ hash.update(stableJson(p?.output, version >= 2 ? withoutDcpRefs : void 0));
3822
3844
  break;
3823
3845
  case "file":
3824
3846
  case "image": {
@@ -3840,9 +3862,10 @@ function hashPart(hash, part) {
3840
3862
  }
3841
3863
  hash.update("");
3842
3864
  }
3843
- function stableJson(value) {
3865
+ function stableJson(value, text) {
3844
3866
  const seen = /* @__PURE__ */ new WeakSet();
3845
3867
  const walk = (node) => {
3868
+ if (typeof node === "string") return text ? text(node) : node;
3846
3869
  if (node === null || typeof node !== "object") return node;
3847
3870
  if (seen.has(node)) return "[circular]";
3848
3871
  seen.add(node);
@@ -3859,16 +3882,16 @@ function stableJson(value) {
3859
3882
  return "[unserializable]";
3860
3883
  }
3861
3884
  }
3862
- function messageDigest(msg) {
3885
+ function messageDigest(msg, version) {
3863
3886
  const hash = createHash("sha256");
3864
3887
  hash.update(msg.role);
3865
3888
  hash.update("\0");
3866
3889
  if (typeof msg.content === "string") {
3867
3890
  hash.update("text\0");
3868
- hash.update(msg.content);
3891
+ hash.update(version >= 2 ? withoutDcpRefs(msg.content) : msg.content);
3869
3892
  hash.update("");
3870
3893
  } else if (Array.isArray(msg.content)) {
3871
- for (const part of msg.content) hashPart(hash, part);
3894
+ for (const part of msg.content) hashPart(hash, part, version);
3872
3895
  }
3873
3896
  return hash.digest("hex").slice(0, 32);
3874
3897
  }
@@ -3881,7 +3904,7 @@ function conversationDigests(prompt, opts) {
3881
3904
  }
3882
3905
  out.push({
3883
3906
  role: msg.role,
3884
- digest: opts?.assistantContent === false && msg.role === "assistant" ? ASSISTANT_TURN_DIGEST : messageDigest(msg)
3907
+ digest: opts?.assistantContent === false && msg.role === "assistant" ? ASSISTANT_TURN_DIGEST : messageDigest(msg, opts?.version ?? DIGEST_VERSION)
3885
3908
  });
3886
3909
  }
3887
3910
  return out;
@@ -4251,6 +4274,13 @@ var SPAWN_MCP_TIMEOUT_MS = 12e4;
4251
4274
  function spawnMcpTimeoutEnv(inherited = process.env) {
4252
4275
  return inherited.MCP_TIMEOUT === void 0 ? { MCP_TIMEOUT: String(SPAWN_MCP_TIMEOUT_MS) } : {};
4253
4276
  }
4277
+ var CLI_DEFAULT_MCP_TIMEOUT_MS = 3e4;
4278
+ function effectiveSpawnMcpTimeoutMs(inherited = process.env) {
4279
+ const value = inherited.MCP_TIMEOUT;
4280
+ if (value === void 0) return SPAWN_MCP_TIMEOUT_MS;
4281
+ const parsed = Number.parseInt(value, 10);
4282
+ return Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, 2147483647) : CLI_DEFAULT_MCP_TIMEOUT_MS;
4283
+ }
4254
4284
  function detectCliVersion(cliPath) {
4255
4285
  const cached2 = cache2.get(cliPath);
4256
4286
  if (cached2) return cached2;
@@ -7218,7 +7248,7 @@ function isDigest(value) {
7218
7248
  }
7219
7249
  function isRecord2(value) {
7220
7250
  const record = value;
7221
- return !!record && typeof record.claudeSessionId === "string" && typeof record.cliPath === "string" && typeof record.updatedAt === "number" && Array.isArray(record.chain) && record.chain.every(isDigest) && (record.shape === void 0 || Array.isArray(record.shape) && record.shape.every(isDigest)) && (record.configDir === void 0 || typeof record.configDir === "string") && (record.account === void 0 || typeof record.account === "string");
7251
+ return !!record && typeof record.claudeSessionId === "string" && typeof record.cliPath === "string" && typeof record.updatedAt === "number" && Array.isArray(record.chain) && record.chain.every(isDigest) && (record.shape === void 0 || Array.isArray(record.shape) && record.shape.every(isDigest)) && (record.configDir === void 0 || typeof record.configDir === "string") && (record.account === void 0 || typeof record.account === "string") && (record.version === void 0 || typeof record.version === "number");
7222
7252
  }
7223
7253
  function load2() {
7224
7254
  if (records) return records;
@@ -7271,7 +7301,7 @@ function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath, where)
7271
7301
  const configDir = where?.configDir;
7272
7302
  const account = where?.account;
7273
7303
  const current = load2().get(sessionKey2);
7274
- if (current && current.claudeSessionId === claudeSessionId && current.cliPath === cliPath && current.shape !== void 0 && current.configDir === configDir && current.account === account && current.chain.length === chain.length && current.chain.every((entry, i) => entry.digest === chain[i].digest)) {
7304
+ if (current && current.claudeSessionId === claudeSessionId && current.cliPath === cliPath && current.shape !== void 0 && current.configDir === configDir && current.account === account && current.version === DIGEST_VERSION && current.chain.length === chain.length && current.chain.every((entry, i) => entry.digest === chain[i].digest)) {
7275
7305
  return;
7276
7306
  }
7277
7307
  save2((merged) => {
@@ -7283,6 +7313,7 @@ function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath, where)
7283
7313
  shape: shape2,
7284
7314
  ...configDir === void 0 ? {} : { configDir },
7285
7315
  ...account === void 0 ? {} : { account },
7316
+ version: DIGEST_VERSION,
7286
7317
  updatedAt: Date.now()
7287
7318
  });
7288
7319
  });
@@ -7317,7 +7348,7 @@ function findResumePoint(opts) {
7317
7348
  return void 0;
7318
7349
  };
7319
7350
  if (record.cliPath !== opts.cliPath) return refuse("another-claude-binary");
7320
- if (!continuesRecordedConversation(record.chain, opts.prompt)) {
7351
+ if (!continuesRecordedConversation(record.chain, opts.prompt, { version: record.version ?? 1 })) {
7321
7352
  return refuse("conversation-changed");
7322
7353
  }
7323
7354
  if (!existsSync2(opts.transcriptPath(record.claudeSessionId))) {
@@ -7355,7 +7386,8 @@ function findSiblingResumePoint(opts) {
7355
7386
  continue;
7356
7387
  }
7357
7388
  if (!record.shape || !continuesRecordedConversation(record.shape, opts.prompt, {
7358
- assistantContent: false
7389
+ assistantContent: false,
7390
+ version: record.version ?? 1
7359
7391
  })) {
7360
7392
  refusal = "sibling-conversation-changed";
7361
7393
  continue;
@@ -7885,6 +7917,18 @@ function isTerminalResultLine(line) {
7885
7917
  return false;
7886
7918
  }
7887
7919
  }
7920
+ function isSystemInitLine(line) {
7921
+ if (!line.includes('"init"')) return false;
7922
+ try {
7923
+ const parsed = JSON.parse(line);
7924
+ return parsed.type === "system" && parsed.subtype === "init";
7925
+ } catch {
7926
+ return false;
7927
+ }
7928
+ }
7929
+ function noteInitLine(ap, line) {
7930
+ if (!ap.initSeen && isSystemInitLine(line)) ap.initSeen = true;
7931
+ }
7888
7932
  function settleTurn(ap) {
7889
7933
  ap.turnInFlight = false;
7890
7934
  const waiters = ap.turnIdleWaiters ?? [];
@@ -8246,6 +8290,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
8246
8290
  rl.on("line", (line) => {
8247
8291
  if (dispatchSideQuestionResponse(ap, line)) return;
8248
8292
  noteTurnLine(ap, line);
8293
+ noteInitLine(ap, line);
8249
8294
  if (lineEmitter.listenerCount("line") === 0) {
8250
8295
  bufferUnattendedLine(ap, line);
8251
8296
  return;
@@ -8860,9 +8905,10 @@ function resolveBudgetWarnAt(value) {
8860
8905
  }
8861
8906
  return [...new Set(value)].sort((a, b) => a - b);
8862
8907
  }
8863
- function costDelta(claudeSessionId, totalCostUsd, cumulative) {
8864
- if (!Number.isFinite(totalCostUsd) || totalCostUsd <= 0) return 0;
8865
- if (!cumulative || !claudeSessionId) return totalCostUsd;
8908
+ function takeResultCost(claudeSessionId, totalCostUsd, cumulative) {
8909
+ if (!Number.isFinite(totalCostUsd) || totalCostUsd <= 0) return { deltaUsd: 0, noBaseline: false };
8910
+ if (!cumulative) return { deltaUsd: totalCostUsd, noBaseline: false };
8911
+ if (!claudeSessionId) return { deltaUsd: totalCostUsd, noBaseline: true };
8866
8912
  const previous = lastTotals.get(claudeSessionId);
8867
8913
  lastTotals.delete(claudeSessionId);
8868
8914
  lastTotals.set(claudeSessionId, totalCostUsd);
@@ -8871,8 +8917,9 @@ function costDelta(claudeSessionId, totalCostUsd, cumulative) {
8871
8917
  if (oldest.done) break;
8872
8918
  lastTotals.delete(oldest.value);
8873
8919
  }
8874
- if (previous === void 0 || totalCostUsd < previous) return totalCostUsd;
8875
- return totalCostUsd - previous;
8920
+ if (previous === void 0) return { deltaUsd: totalCostUsd, noBaseline: true };
8921
+ if (totalCostUsd < previous) return { deltaUsd: totalCostUsd, noBaseline: false };
8922
+ return { deltaUsd: totalCostUsd - previous, noBaseline: false };
8876
8923
  }
8877
8924
  function recordSessionCost(session, deltaUsd) {
8878
8925
  if (!(deltaUsd > 0)) return;
@@ -8931,8 +8978,10 @@ function formatBudgetStopNote(spentUsd, capUsd) {
8931
8978
  return `${BUDGET_MARKER} this conversation has spent ${formatUsd(spentUsd)} against its ${formatUsd(capUsd)} \`maxBudgetUsd\`, so this turn was not started and nothing was sent to Claude. Raise \`maxBudgetUsd\` in the provider options and relaunch opencode (plugin options are read once at startup), or start a new session.`;
8932
8979
  }
8933
8980
  function recordResultCost(budget, claudeSessionId, totalCostUsd) {
8934
- if (typeof totalCostUsd !== "number") return;
8935
- recordSessionCost(budget.session, costDelta(claudeSessionId, totalCostUsd, budget.cumulative));
8981
+ if (typeof totalCostUsd !== "number") return void 0;
8982
+ const cost = takeResultCost(claudeSessionId, totalCostUsd, budget.cumulative);
8983
+ recordSessionCost(budget.session, cost.deltaUsd);
8984
+ return cost;
8936
8985
  }
8937
8986
  function takeBudgetNote(budget) {
8938
8987
  if (!budget.notes) return null;
@@ -9148,6 +9197,7 @@ var BUNDLE_DATA_ALLOWLIST = {
9148
9197
  // booleans: every one of these is a decision the plugin made
9149
9198
  applied: "flag",
9150
9199
  attached: "flag",
9200
+ beforeInit: "flag",
9151
9201
  bridgeSkipNativeSkills: "flag",
9152
9202
  channelClosed: "flag",
9153
9203
  compaction: "flag",
@@ -9249,9 +9299,11 @@ var BUNDLE_DATA_ALLOWLIST = {
9249
9299
  durationMs: "duration",
9250
9300
  elapsedMs: "duration",
9251
9301
  remainingMs: "duration",
9302
+ startWatchdogMs: "duration",
9252
9303
  timeoutMs: "duration",
9253
9304
  waitedMs: "duration",
9254
9305
  costUsd: "money",
9306
+ turnCostUsd: "money",
9255
9307
  // enums: a fixed vocabulary this package or the CLI defines
9256
9308
  apiKeySource: "enum",
9257
9309
  delivery: "enum",
@@ -11696,6 +11748,11 @@ function extractTurnStats(msg) {
11696
11748
  if (Array.isArray(msg.permission_denials)) stats.permissionDenials = msg.permission_denials;
11697
11749
  return stats;
11698
11750
  }
11751
+ function footerCostUsd(cost, resumedSession) {
11752
+ if (!cost) return void 0;
11753
+ if (cost.noBaseline && resumedSession) return void 0;
11754
+ return cost.deltaUsd;
11755
+ }
11699
11756
  function formatCost(costUsd) {
11700
11757
  if (!Number.isFinite(costUsd) || costUsd < 0) return "$0.00";
11701
11758
  return costUsd >= 1 ? `$${costUsd.toFixed(2)}` : `$${costUsd.toFixed(4)}`;
@@ -15559,6 +15616,8 @@ function createTurnState(init) {
15559
15616
  asideSinkUnregister: null,
15560
15617
  resultFallbackTimer: null,
15561
15618
  startWatchdog: null,
15619
+ startWatchdogArmedMs: 0,
15620
+ startWatchdogBeforeInit: false,
15562
15621
  respawnAttempted: false,
15563
15622
  hasReceivedContent: false,
15564
15623
  hasReceivedProgress: false,
@@ -15934,7 +15993,11 @@ function onStartWatchdogFire(state) {
15934
15993
  if (state.respawnAttempted) {
15935
15994
  log.error(
15936
15995
  "claude process still silent after respawn; ending turn",
15937
- { sessionKey: state.sessionKey }
15996
+ {
15997
+ sessionKey: state.sessionKey,
15998
+ startWatchdogMs: state.startWatchdogArmedMs,
15999
+ beforeInit: state.startWatchdogBeforeInit
16000
+ }
15938
16001
  );
15939
16002
  deleteActiveProcess(state.sessionKey);
15940
16003
  deleteClaudeSessionId(state.sessionKey);
@@ -15955,7 +16018,11 @@ function onStartWatchdogFire(state) {
15955
16018
  state.respawnAttempted = true;
15956
16019
  log.warn(
15957
16020
  "no stdout after envelope write; respawning claude process to resume conversation",
15958
- { sessionKey: state.sessionKey, startWatchdogMs: state.startWatchdogMs }
16021
+ {
16022
+ sessionKey: state.sessionKey,
16023
+ startWatchdogMs: state.startWatchdogArmedMs,
16024
+ beforeInit: state.startWatchdogBeforeInit
16025
+ }
15959
16026
  );
15960
16027
  state.lineEmitter.off("line", state.lineHandler);
15961
16028
  state.lineEmitter.off("close", state.closeHandler);
@@ -16007,14 +16074,29 @@ function onStartWatchdogFire(state) {
16007
16074
  }
16008
16075
  armStartWatchdog(state);
16009
16076
  }
16077
+ var START_WATCHDOG_INIT_MARGIN_MS = 3e4;
16078
+ function startWatchdogDelayMs(configuredMs, beforeInit, mcpTimeoutMs = effectiveSpawnMcpTimeoutMs()) {
16079
+ return beforeInit ? Math.max(configuredMs, mcpTimeoutMs + START_WATCHDOG_INIT_MARGIN_MS) : configuredMs;
16080
+ }
16010
16081
  function armStartWatchdog(state) {
16011
16082
  clearStartWatchdog(state);
16012
16083
  if (state.controllerClosed) return;
16084
+ const ap = state.activeProcess;
16085
+ state.startWatchdogBeforeInit = !ap?.interactiveControl && ap?.initSeen !== true;
16086
+ state.startWatchdogArmedMs = startWatchdogDelayMs(
16087
+ state.startWatchdogMs,
16088
+ state.startWatchdogBeforeInit
16089
+ );
16013
16090
  state.startWatchdog = setTimeout(
16014
16091
  () => onStartWatchdogFire(state),
16015
- state.startWatchdogMs
16092
+ state.startWatchdogArmedMs
16016
16093
  );
16017
16094
  }
16095
+ function noteStartWatchdogInit(state) {
16096
+ if (!state.startWatchdog || !state.startWatchdogBeforeInit) return;
16097
+ if (state.activeProcess?.initSeen !== true) return;
16098
+ armStartWatchdog(state);
16099
+ }
16018
16100
  function deliverPendingCompletions(state, force = false) {
16019
16101
  const pending2 = state.activeProcess?.pendingProxyCompletions;
16020
16102
  const entries = [...pending2?.values() ?? []].filter(
@@ -16370,6 +16452,7 @@ function createLineHandler(state, ctx) {
16370
16452
  subtype: msg.subtype
16371
16453
  });
16372
16454
  if (msg.type === "system" && msg.subtype === "init") {
16455
+ noteStartWatchdogInit(state);
16373
16456
  if (msg.session_id) {
16374
16457
  setClaudeSessionId(state.sessionKey, msg.session_id);
16375
16458
  log.info("session initialized", {
@@ -16968,7 +17051,7 @@ ${plan}
16968
17051
  if (msg.session_id) {
16969
17052
  setClaudeSessionId(state.sessionKey, msg.session_id);
16970
17053
  }
16971
- if (ctx.budget) recordResultCost(ctx.budget, msg.session_id, msg.total_cost_usd);
17054
+ const resultCost = ctx.budget ? recordResultCost(ctx.budget, msg.session_id, msg.total_cost_usd) : ctx.config.turnStats && typeof msg.total_cost_usd === "number" ? takeResultCost(msg.session_id, msg.total_cost_usd, !ctx.interactive) : void 0;
16972
17055
  if (deliverPendingCompletions(state)) {
16973
17056
  return;
16974
17057
  }
@@ -17061,10 +17144,17 @@ ${plan}
17061
17144
  numTurns: msg.num_turns,
17062
17145
  isError: msg.is_error,
17063
17146
  subtype: msg.subtype,
17064
- ...turnStatsLogPayload(turnStats)
17147
+ ...turnStatsLogPayload(turnStats),
17148
+ turnCostUsd: resultCost?.deltaUsd
17065
17149
  });
17066
17150
  if (ctx.config.turnStats && !ctx.compactionMode && !msg.is_error && !failure) {
17067
- const footer = formatTurnStatsBlock(turnStats);
17151
+ const footer = formatTurnStatsBlock({
17152
+ ...turnStats,
17153
+ costUsd: footerCostUsd(
17154
+ resultCost,
17155
+ state.activeProcess?.cliArgs?.includes("--resume") === true
17156
+ )
17157
+ });
17068
17158
  if (footer) {
17069
17159
  state.controller.enqueue({
17070
17160
  type: "text-delta",
@@ -17139,6 +17229,26 @@ async function isProxyCallStillServed(callId) {
17139
17229
  setProxyDeadlineGuard(({ callId }) => isProxyCallStillServed(callId));
17140
17230
  var FORWARD_SETTLE_MS = 250;
17141
17231
  var CHILD_EXIT_STATUS_GRACE_MS = 250;
17232
+ function stopOnCancel(stream, cancelled2) {
17233
+ const reader = stream.getReader();
17234
+ return new ReadableStream({
17235
+ async pull(controller) {
17236
+ try {
17237
+ const { done, value } = await reader.read();
17238
+ if (done) controller.close();
17239
+ else controller.enqueue(value);
17240
+ } catch (error) {
17241
+ try {
17242
+ controller.error(error);
17243
+ } catch {
17244
+ }
17245
+ }
17246
+ },
17247
+ cancel(reason) {
17248
+ cancelled2.abort(reason);
17249
+ }
17250
+ });
17251
+ }
17142
17252
  var ClaudeCodeLanguageModel = class {
17143
17253
  specificationVersion = "v3";
17144
17254
  modelId;
@@ -17626,32 +17736,35 @@ var ClaudeCodeLanguageModel = class {
17626
17736
  * switch, and outside `runModelChain` on purpose: an account switch starts
17627
17737
  * that account's own model chain from the top.
17628
17738
  *
17629
- * Anything but `"auto"` is `runModelChain` and one `await`, so `"off"` and
17630
- * `"ask"` are exactly what they were. With `"auto"`, the attempt that failed
17631
- * decides the switch itself (`completeResult` sets `run.switchTo` and closes
17632
- * its stream without a `finish`), and the next attempt's prologue makes the
17633
- * switch the form's pick would have made. This only splices the attempts
17634
- * into one caller turn: one `stream-start`, and the `finish` of whichever
17635
- * attempt did not switch.
17739
+ * Anything but `"auto"` is `runModelChain` behind `stopOnCancel`. With
17740
+ * `"auto"`, the attempt that failed decides the switch itself
17741
+ * (`completeResult` sets `run.switchTo` and closes its stream without a
17742
+ * `finish`), and the next attempt's prologue makes the switch the form's pick
17743
+ * would have made. This only splices the attempts into one caller turn: one
17744
+ * `stream-start`, and the `finish` of whichever attempt did not switch.
17636
17745
  *
17637
17746
  * The bound is `run.tried`, as it is `tried` for the model chain: every
17638
17747
  * account spawned or switched to goes in, and none in it is picked again.
17639
17748
  *
17640
- * Cancelling the returned stream is a stop, for every attempt: this loop is
17641
- * the plugin starting more work on the caller's behalf, so a caller that
17642
- * has gone away must reach all of it. Each attempt gets a signal that the
17643
- * caller's own `abortSignal` and a `cancel()` both abort, and the loop
17644
- * starts no further account once either has happened.
17749
+ * Cancelling the returned stream is a stop, in every mode: a caller that has
17750
+ * gone away must reach the turn it started, or the CLI runs it to the end
17751
+ * and bills it. Each attempt gets a signal that the caller's own
17752
+ * `abortSignal` and a `cancel()` both abort, so a cancel takes the turn's
17753
+ * ordinary abort path, and with `"auto"` the loop starts no further account
17754
+ * once either has happened. Until (h #g257) only `"auto"` did this.
17645
17755
  */
17646
17756
  async runAccountFailover(options, mode) {
17647
- if (this.config.accountFailover !== "auto") return this.runModelChain(options, mode);
17648
- const self = this;
17649
- const run = { tried: /* @__PURE__ */ new Set() };
17650
17757
  const cancelled2 = new AbortController();
17651
17758
  const attemptOptions = {
17652
17759
  ...options,
17653
17760
  abortSignal: options.abortSignal ? AbortSignal.any([options.abortSignal, cancelled2.signal]) : cancelled2.signal
17654
17761
  };
17762
+ if (this.config.accountFailover !== "auto") {
17763
+ const inner2 = await this.runModelChain(attemptOptions, mode);
17764
+ return { ...inner2, stream: stopOnCancel(inner2.stream, cancelled2) };
17765
+ }
17766
+ const self = this;
17767
+ const run = { tried: /* @__PURE__ */ new Set() };
17655
17768
  const stopped = () => attemptOptions.abortSignal.aborted;
17656
17769
  let inner = await this.runModelChain(attemptOptions, mode, run);
17657
17770
  const request = inner.request;
@@ -17693,9 +17806,8 @@ var ClaudeCodeLanguageModel = class {
17693
17806
  }
17694
17807
  }
17695
17808
  },
17696
- async cancel(reason) {
17809
+ cancel(reason) {
17697
17810
  cancelled2.abort(reason);
17698
- await reader?.cancel(reason).catch(() => void 0);
17699
17811
  }
17700
17812
  });
17701
17813
  return { stream, request };
@@ -18102,16 +18214,19 @@ var ClaudeCodeLanguageModel = class {
18102
18214
  resetsAt: autoSwitch.resetsAt
18103
18215
  } : compactionMode || mode === "generate" ? null : consumeAccountFailoverAnswer(sk, options.prompt, {
18104
18216
  sourceAccount,
18217
+ // What the form would offer now: an override still standing is
18218
+ // still the account that failed, and is not on offer (h #g257).
18219
+ failedAccount: failover.target ?? sourceAccount,
18105
18220
  candidates: failoverCandidates(
18106
18221
  this.config.failoverAccounts,
18107
18222
  sourceAccount,
18108
18223
  groups
18109
- )
18224
+ ).filter((account) => account !== (failover.target ?? sourceAccount))
18110
18225
  });
18111
18226
  if (failoverAnswer?.kind === "stop") {
18112
18227
  log.warn("account failover declined; ending the turn", {
18113
18228
  sessionKey: sk,
18114
- account: sourceAccount,
18229
+ account: failover.target ?? sourceAccount,
18115
18230
  reason: failoverAnswer.reason
18116
18231
  });
18117
18232
  const note = formatFailoverStopNote(failoverAnswer.reason);
@@ -18125,7 +18240,10 @@ var ClaudeCodeLanguageModel = class {
18125
18240
  controller.enqueue({
18126
18241
  type: "error",
18127
18242
  error: new Error(
18128
- `Claude account "${sourceAccount}" is out of usage and no other account was picked.`
18243
+ // The account that failed: after a chain of overrides that is
18244
+ // not the provider's own (h #g257). A stop answer also follows
18245
+ // an expired login, so this does not say "out of usage".
18246
+ `Claude account "${failover.target ?? sourceAccount}" cannot serve this turn and no other account was picked.`
18129
18247
  )
18130
18248
  });
18131
18249
  controller.enqueue({
@@ -18229,13 +18347,6 @@ var ClaudeCodeLanguageModel = class {
18229
18347
  }
18230
18348
  cliPath = failover.cliPath;
18231
18349
  asideTransportRef.cliPath = cliPath;
18232
- if (!autoSwitch) {
18233
- failoverNote = formatFailoverNote({
18234
- sourceAccount: failoverAnswer.sourceAccount,
18235
- target: failoverAnswer.target,
18236
- resetsAt: failoverAnswer.resetsAt
18237
- });
18238
- }
18239
18350
  }
18240
18351
  const spawnConfigDir = this.config.accountInProcess ? this.skillBridgeSpawn(failover).configDir : void 0;
18241
18352
  const processForAccount = getActiveProcess(sk);
@@ -18307,6 +18418,14 @@ var ClaudeCodeLanguageModel = class {
18307
18418
  reason: autoSwitch.reason,
18308
18419
  carried: conversationOnTarget
18309
18420
  });
18421
+ } else if (!autoSwitch && failoverAnswer?.kind === "switch") {
18422
+ failoverNote = formatFailoverNote({
18423
+ sourceAccount: failoverAnswer.failedAccount ?? failoverAnswer.sourceAccount,
18424
+ target: failoverAnswer.target,
18425
+ resetsAt: failoverAnswer.resetsAt,
18426
+ reason: failoverAnswer.reason,
18427
+ carried: conversationOnTarget
18428
+ });
18310
18429
  }
18311
18430
  let accountGroupNote = null;
18312
18431
  if (!compactionMode && groups && accountProviders.size > 0 && includeHistoryContext && failoverAnswer?.kind !== "switch") {
@@ -18573,11 +18692,12 @@ var ClaudeCodeLanguageModel = class {
18573
18692
  "The read-only preset on the interactive transport needs Claude Code 2.1.263 or newer (--restricted and --permission-mode dontAsk). Update claude, or use the headless transport."
18574
18693
  );
18575
18694
  }
18695
+ const spawnedAccount = failover.target ?? sourceAccount;
18576
18696
  const failoverAccounts = failoverCandidates(
18577
18697
  this.config.failoverAccounts,
18578
18698
  sourceAccount,
18579
18699
  groups
18580
- );
18700
+ ).filter((account) => account !== spawnedAccount);
18581
18701
  const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover === "ask" && !compactionMode && // A `doGenerate` caller takes the override and reports the plain
18582
18702
  // rate-limit error, never the form. See `TurnMode`.
18583
18703
  mode !== "generate" && isAccountFailoverQuestionActive({
@@ -18590,7 +18710,6 @@ var ClaudeCodeLanguageModel = class {
18590
18710
  // the operator is usually not even looking at.
18591
18711
  childSession: !!await fetchSessionParentId(affinity)
18592
18712
  });
18593
- const spawnedAccount = failover.target ?? sourceAccount;
18594
18713
  accountRun?.tried.add(spawnedAccount);
18595
18714
  let autoFailoverChoice;
18596
18715
  const autoFailoverTarget = () => {
@@ -18599,7 +18718,7 @@ var ClaudeCodeLanguageModel = class {
18599
18718
  }
18600
18719
  return autoFailoverChoice ?? void 0;
18601
18720
  };
18602
- const limitAccount = accountRun ? spawnedAccount : sourceAccount;
18721
+ const limitAccount = spawnedAccount;
18603
18722
  const usageLimitNoteActive = !compactionMode && mode !== "generate" && !failoverAskActive;
18604
18723
  const usageWarnThresholds = resolveUsageWarnThresholds(this.config.usageWarnThresholds);
18605
18724
  const usageSwitchAt = this.config.usageSwitchAt === null ? null : resolveUsageSwitchAt(this.config.usageSwitchAt);
@@ -19348,7 +19467,11 @@ var ClaudeCodeLanguageModel = class {
19348
19467
  state.activeProcess?.pendingProxyCompletions?.clear();
19349
19468
  if ((state.accountLimitHit || state.accountBlock) && failoverAskActive && msg.is_error === true) {
19350
19469
  const call = createAccountFailoverQuestionCall(sk, {
19470
+ // The override a pick sets is keyed on the provider's own
19471
+ // account, the one every turn of this conversation resolves
19472
+ // from; the form names the account that actually failed.
19351
19473
  sourceAccount,
19474
+ failedAccount: limitAccount,
19352
19475
  candidates: failoverAccounts,
19353
19476
  resetsAt: state.accountLimitHit?.resetsAt,
19354
19477
  window: state.accountLimitHit?.window,
@@ -19356,7 +19479,7 @@ var ClaudeCodeLanguageModel = class {
19356
19479
  carryTranscript: self.config.crossAccountResume !== false
19357
19480
  });
19358
19481
  log.warn(
19359
- `Claude account "${sourceAccount}" ${state.accountLimitHit || !state.accountBlock ? "is out of usage" : `cannot serve (${state.accountBlock})`}; asking which account to continue on.`,
19482
+ `Claude account "${limitAccount}" ${state.accountLimitHit || !state.accountBlock ? "is out of usage" : `cannot serve (${state.accountBlock})`}; asking which account to continue on.`,
19360
19483
  {
19361
19484
  sessionKey: sk,
19362
19485
  candidates: failoverAccounts,
@@ -19447,7 +19570,7 @@ var ClaudeCodeLanguageModel = class {
19447
19570
  state.endTextBlock();
19448
19571
  state.resultFailure ??= "usage_limit";
19449
19572
  }
19450
- if (accountRun && state.accountLimitHit && msg.is_error === true) {
19573
+ if (state.accountLimitHit && msg.is_error === true) {
19451
19574
  state.resultFailure ??= "usage_limit";
19452
19575
  }
19453
19576
  if (runAutoContinue(state, msg)) return;
@@ -19566,7 +19689,11 @@ var ClaudeCodeLanguageModel = class {
19566
19689
  interactive: useInteractive,
19567
19690
  sourceAccount: limitAccount,
19568
19691
  usageAccount: spawnedAccount,
19569
- accountConfigDir: accountRun ? self.skillBridgeSpawn(failover).configDir : self.config.configDir,
19692
+ // The config dir of the account that ran, so an account-block note
19693
+ // names that account's login command. The provider's own when no
19694
+ // override moved the turn; until (h #g257) `"off"` and `"ask"`
19695
+ // named the provider's even after one had.
19696
+ accountConfigDir: self.skillBridgeSpawn(failover).configDir,
19570
19697
  failoverAskActive,
19571
19698
  autoFailoverTarget,
19572
19699
  usageLimitNoteActive,