@khalilgharbaoui/opencode-claude-code-plugin 0.60.0 → 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.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) {
@@ -2614,9 +2622,12 @@ function withoutAccountMarker(modelId) {
2614
2622
  }
2615
2623
  function qualifyModelName(wanted, referenceModelId) {
2616
2624
  const base = withoutAccountMarker(wanted.trim());
2617
- if (!base || !Object.hasOwn(defaultModels, base)) return null;
2625
+ if (!isRegisteredModelName(base)) return null;
2618
2626
  return `${base}${accountMarker(referenceModelId)}`;
2619
2627
  }
2628
+ function isRegisteredModelName(name) {
2629
+ return name.length > 0 && Object.hasOwn(defaultModels, name);
2630
+ }
2620
2631
  function resolveAgentModel(agent, modelId, overrides) {
2621
2632
  if (!agent) return modelId;
2622
2633
  const record = (overrides?.records ?? registry)[agent];
@@ -3797,13 +3808,27 @@ import { createHash } from "crypto";
3797
3808
  var fingerprints = /* @__PURE__ */ new Map();
3798
3809
  var fingerprintSeq = 0;
3799
3810
  var MAX_FORK_FINGERPRINTS = 64;
3800
- 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) {
3801
3825
  const p = part;
3802
3826
  const type = typeof p?.type === "string" ? p.type : "unknown";
3827
+ const content = (text) => version >= 2 ? withoutDcpRefs(text) : text;
3803
3828
  switch (type) {
3804
3829
  case "text":
3805
3830
  hash.update("text\0");
3806
- hash.update(String(p?.text ?? ""));
3831
+ hash.update(content(String(p?.text ?? "")));
3807
3832
  break;
3808
3833
  case "tool-call":
3809
3834
  hash.update("call\0");
@@ -3815,7 +3840,7 @@ function hashPart(hash, part) {
3815
3840
  hash.update("result\0");
3816
3841
  hash.update(String(p?.toolName ?? ""));
3817
3842
  hash.update("\0");
3818
- hash.update(stableJson(p?.output));
3843
+ hash.update(stableJson(p?.output, version >= 2 ? withoutDcpRefs : void 0));
3819
3844
  break;
3820
3845
  case "file":
3821
3846
  case "image": {
@@ -3837,9 +3862,10 @@ function hashPart(hash, part) {
3837
3862
  }
3838
3863
  hash.update("");
3839
3864
  }
3840
- function stableJson(value) {
3865
+ function stableJson(value, text) {
3841
3866
  const seen = /* @__PURE__ */ new WeakSet();
3842
3867
  const walk = (node) => {
3868
+ if (typeof node === "string") return text ? text(node) : node;
3843
3869
  if (node === null || typeof node !== "object") return node;
3844
3870
  if (seen.has(node)) return "[circular]";
3845
3871
  seen.add(node);
@@ -3856,16 +3882,16 @@ function stableJson(value) {
3856
3882
  return "[unserializable]";
3857
3883
  }
3858
3884
  }
3859
- function messageDigest(msg) {
3885
+ function messageDigest(msg, version) {
3860
3886
  const hash = createHash("sha256");
3861
3887
  hash.update(msg.role);
3862
3888
  hash.update("\0");
3863
3889
  if (typeof msg.content === "string") {
3864
3890
  hash.update("text\0");
3865
- hash.update(msg.content);
3891
+ hash.update(version >= 2 ? withoutDcpRefs(msg.content) : msg.content);
3866
3892
  hash.update("");
3867
3893
  } else if (Array.isArray(msg.content)) {
3868
- for (const part of msg.content) hashPart(hash, part);
3894
+ for (const part of msg.content) hashPart(hash, part, version);
3869
3895
  }
3870
3896
  return hash.digest("hex").slice(0, 32);
3871
3897
  }
@@ -3878,7 +3904,7 @@ function conversationDigests(prompt, opts) {
3878
3904
  }
3879
3905
  out.push({
3880
3906
  role: msg.role,
3881
- 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)
3882
3908
  });
3883
3909
  }
3884
3910
  return out;
@@ -4248,6 +4274,13 @@ var SPAWN_MCP_TIMEOUT_MS = 12e4;
4248
4274
  function spawnMcpTimeoutEnv(inherited = process.env) {
4249
4275
  return inherited.MCP_TIMEOUT === void 0 ? { MCP_TIMEOUT: String(SPAWN_MCP_TIMEOUT_MS) } : {};
4250
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
+ }
4251
4284
  function detectCliVersion(cliPath) {
4252
4285
  const cached2 = cache2.get(cliPath);
4253
4286
  if (cached2) return cached2;
@@ -4793,6 +4826,13 @@ function taskSchemaOffersModel(def) {
4793
4826
  const fields = schema?.properties;
4794
4827
  return !!fields && typeof fields === "object" && "model" in fields;
4795
4828
  }
4829
+ function completeTaskModels(def, input, complete) {
4830
+ if (!complete || !taskSchemaOffersModel(def)) return;
4831
+ const tasks = def.name === TASK_BATCH_TOOL_NAME ? taskBatchTasks(input) : [input];
4832
+ for (const task of tasks) {
4833
+ if (task.model !== void 0) task.model = complete(task.model);
4834
+ }
4835
+ }
4796
4836
  function taskModelInputError(def, input, guard) {
4797
4837
  const tasks = def.name === TASK_BATCH_TOOL_NAME ? taskBatchTasks(input) : [input];
4798
4838
  const offered = taskSchemaOffersModel(def);
@@ -5121,6 +5161,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
5121
5161
  }
5122
5162
  }
5123
5163
  if (toolName === "task" || toolName === TASK_BATCH_TOOL_NAME) {
5164
+ completeTaskModels(tool, input, options.completeTaskModel);
5124
5165
  const problem = taskModelInputError(tool, input, options.taskModelGuard);
5125
5166
  if (problem) {
5126
5167
  writeToolCallResult(res, requestId, { kind: "error", message: problem });
@@ -5894,6 +5935,21 @@ function parseTaskModelReference(value) {
5894
5935
  const [, providerID, modelID, variant] = match;
5895
5936
  return variant ? { providerID, modelID, variant } : { providerID, modelID };
5896
5937
  }
5938
+ function completeTaskModelReference(value, own) {
5939
+ if (typeof value !== "string") return value;
5940
+ const trimmed = value.trim();
5941
+ if (!trimmed || trimmed.includes("/")) return value;
5942
+ const hash = trimmed.indexOf("#");
5943
+ const id = hash === -1 ? trimmed : trimmed.slice(0, hash);
5944
+ const variant = hash === -1 ? void 0 : trimmed.slice(hash + 1);
5945
+ if (variant !== void 0 && (!variant || variant.includes("#"))) return value;
5946
+ const at = id.indexOf("@");
5947
+ const base = at === -1 ? id : id.slice(0, at);
5948
+ if (at !== -1 && id.slice(at + 1) !== own.accountMarker) return value;
5949
+ if (!own.isRegistered(base)) return value;
5950
+ const modelID = own.accountMarker ? `${base}@${own.accountMarker}` : base;
5951
+ return `${own.providerID}/${modelID}${variant ? `#${variant}` : ""}`;
5952
+ }
5897
5953
  function taskModelAccountRefusal(input) {
5898
5954
  if (!input.groups || input.crossGroup) return null;
5899
5955
  const reference = parseTaskModelReference(input.model);
@@ -7192,7 +7248,7 @@ function isDigest(value) {
7192
7248
  }
7193
7249
  function isRecord2(value) {
7194
7250
  const record = value;
7195
- 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");
7196
7252
  }
7197
7253
  function load2() {
7198
7254
  if (records) return records;
@@ -7245,7 +7301,7 @@ function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath, where)
7245
7301
  const configDir = where?.configDir;
7246
7302
  const account = where?.account;
7247
7303
  const current = load2().get(sessionKey2);
7248
- 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)) {
7249
7305
  return;
7250
7306
  }
7251
7307
  save2((merged) => {
@@ -7257,6 +7313,7 @@ function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath, where)
7257
7313
  shape: shape2,
7258
7314
  ...configDir === void 0 ? {} : { configDir },
7259
7315
  ...account === void 0 ? {} : { account },
7316
+ version: DIGEST_VERSION,
7260
7317
  updatedAt: Date.now()
7261
7318
  });
7262
7319
  });
@@ -7291,7 +7348,7 @@ function findResumePoint(opts) {
7291
7348
  return void 0;
7292
7349
  };
7293
7350
  if (record.cliPath !== opts.cliPath) return refuse("another-claude-binary");
7294
- if (!continuesRecordedConversation(record.chain, opts.prompt)) {
7351
+ if (!continuesRecordedConversation(record.chain, opts.prompt, { version: record.version ?? 1 })) {
7295
7352
  return refuse("conversation-changed");
7296
7353
  }
7297
7354
  if (!existsSync2(opts.transcriptPath(record.claudeSessionId))) {
@@ -7329,7 +7386,8 @@ function findSiblingResumePoint(opts) {
7329
7386
  continue;
7330
7387
  }
7331
7388
  if (!record.shape || !continuesRecordedConversation(record.shape, opts.prompt, {
7332
- assistantContent: false
7389
+ assistantContent: false,
7390
+ version: record.version ?? 1
7333
7391
  })) {
7334
7392
  refusal = "sibling-conversation-changed";
7335
7393
  continue;
@@ -7859,6 +7917,18 @@ function isTerminalResultLine(line) {
7859
7917
  return false;
7860
7918
  }
7861
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
+ }
7862
7932
  function settleTurn(ap) {
7863
7933
  ap.turnInFlight = false;
7864
7934
  const waiters = ap.turnIdleWaiters ?? [];
@@ -8220,6 +8290,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
8220
8290
  rl.on("line", (line) => {
8221
8291
  if (dispatchSideQuestionResponse(ap, line)) return;
8222
8292
  noteTurnLine(ap, line);
8293
+ noteInitLine(ap, line);
8223
8294
  if (lineEmitter.listenerCount("line") === 0) {
8224
8295
  bufferUnattendedLine(ap, line);
8225
8296
  return;
@@ -8834,9 +8905,10 @@ function resolveBudgetWarnAt(value) {
8834
8905
  }
8835
8906
  return [...new Set(value)].sort((a, b) => a - b);
8836
8907
  }
8837
- function costDelta(claudeSessionId, totalCostUsd, cumulative) {
8838
- if (!Number.isFinite(totalCostUsd) || totalCostUsd <= 0) return 0;
8839
- 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 };
8840
8912
  const previous = lastTotals.get(claudeSessionId);
8841
8913
  lastTotals.delete(claudeSessionId);
8842
8914
  lastTotals.set(claudeSessionId, totalCostUsd);
@@ -8845,8 +8917,9 @@ function costDelta(claudeSessionId, totalCostUsd, cumulative) {
8845
8917
  if (oldest.done) break;
8846
8918
  lastTotals.delete(oldest.value);
8847
8919
  }
8848
- if (previous === void 0 || totalCostUsd < previous) return totalCostUsd;
8849
- 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 };
8850
8923
  }
8851
8924
  function recordSessionCost(session, deltaUsd) {
8852
8925
  if (!(deltaUsd > 0)) return;
@@ -8905,8 +8978,10 @@ function formatBudgetStopNote(spentUsd, capUsd) {
8905
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.`;
8906
8979
  }
8907
8980
  function recordResultCost(budget, claudeSessionId, totalCostUsd) {
8908
- if (typeof totalCostUsd !== "number") return;
8909
- 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;
8910
8985
  }
8911
8986
  function takeBudgetNote(budget) {
8912
8987
  if (!budget.notes) return null;
@@ -9122,6 +9197,7 @@ var BUNDLE_DATA_ALLOWLIST = {
9122
9197
  // booleans: every one of these is a decision the plugin made
9123
9198
  applied: "flag",
9124
9199
  attached: "flag",
9200
+ beforeInit: "flag",
9125
9201
  bridgeSkipNativeSkills: "flag",
9126
9202
  channelClosed: "flag",
9127
9203
  compaction: "flag",
@@ -9223,9 +9299,11 @@ var BUNDLE_DATA_ALLOWLIST = {
9223
9299
  durationMs: "duration",
9224
9300
  elapsedMs: "duration",
9225
9301
  remainingMs: "duration",
9302
+ startWatchdogMs: "duration",
9226
9303
  timeoutMs: "duration",
9227
9304
  waitedMs: "duration",
9228
9305
  costUsd: "money",
9306
+ turnCostUsd: "money",
9229
9307
  // enums: a fixed vocabulary this package or the CLI defines
9230
9308
  apiKeySource: "enum",
9231
9309
  delivery: "enum",
@@ -11670,6 +11748,11 @@ function extractTurnStats(msg) {
11670
11748
  if (Array.isArray(msg.permission_denials)) stats.permissionDenials = msg.permission_denials;
11671
11749
  return stats;
11672
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
+ }
11673
11756
  function formatCost(costUsd) {
11674
11757
  if (!Number.isFinite(costUsd) || costUsd < 0) return "$0.00";
11675
11758
  return costUsd >= 1 ? `$${costUsd.toFixed(2)}` : `$${costUsd.toFixed(4)}`;
@@ -15226,9 +15309,24 @@ async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCom
15226
15309
  groups,
15227
15310
  crossGroup: config.subagentDispatchCrossGroup === true
15228
15311
  }) : void 0;
15312
+ const ownModelMarker = accountModelSuffix(config.account ?? DEFAULT_ACCOUNT);
15313
+ const completeTaskModel = (model) => {
15314
+ const completed = completeTaskModelReference(model, {
15315
+ providerID: config.provider,
15316
+ accountMarker: ownModelMarker,
15317
+ isRegistered: isRegisteredModelName
15318
+ });
15319
+ if (completed !== model) {
15320
+ log.info("bare task model completed to this conversation's own provider", {
15321
+ model: completed
15322
+ });
15323
+ }
15324
+ return completed;
15325
+ };
15229
15326
  const srv = await createProxyMcpServer(tools, timeoutOverrides, interceptors, {
15230
15327
  admit,
15231
- taskModelGuard
15328
+ taskModelGuard,
15329
+ completeTaskModel
15232
15330
  });
15233
15331
  srv.calls.on("call", (call) => {
15234
15332
  if (call.toolName === "task") noteExplicitTaskModels(callerSessionId, [call.input]);
@@ -15518,6 +15616,8 @@ function createTurnState(init) {
15518
15616
  asideSinkUnregister: null,
15519
15617
  resultFallbackTimer: null,
15520
15618
  startWatchdog: null,
15619
+ startWatchdogArmedMs: 0,
15620
+ startWatchdogBeforeInit: false,
15521
15621
  respawnAttempted: false,
15522
15622
  hasReceivedContent: false,
15523
15623
  hasReceivedProgress: false,
@@ -15893,7 +15993,11 @@ function onStartWatchdogFire(state) {
15893
15993
  if (state.respawnAttempted) {
15894
15994
  log.error(
15895
15995
  "claude process still silent after respawn; ending turn",
15896
- { sessionKey: state.sessionKey }
15996
+ {
15997
+ sessionKey: state.sessionKey,
15998
+ startWatchdogMs: state.startWatchdogArmedMs,
15999
+ beforeInit: state.startWatchdogBeforeInit
16000
+ }
15897
16001
  );
15898
16002
  deleteActiveProcess(state.sessionKey);
15899
16003
  deleteClaudeSessionId(state.sessionKey);
@@ -15914,7 +16018,11 @@ function onStartWatchdogFire(state) {
15914
16018
  state.respawnAttempted = true;
15915
16019
  log.warn(
15916
16020
  "no stdout after envelope write; respawning claude process to resume conversation",
15917
- { sessionKey: state.sessionKey, startWatchdogMs: state.startWatchdogMs }
16021
+ {
16022
+ sessionKey: state.sessionKey,
16023
+ startWatchdogMs: state.startWatchdogArmedMs,
16024
+ beforeInit: state.startWatchdogBeforeInit
16025
+ }
15918
16026
  );
15919
16027
  state.lineEmitter.off("line", state.lineHandler);
15920
16028
  state.lineEmitter.off("close", state.closeHandler);
@@ -15966,14 +16074,29 @@ function onStartWatchdogFire(state) {
15966
16074
  }
15967
16075
  armStartWatchdog(state);
15968
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
+ }
15969
16081
  function armStartWatchdog(state) {
15970
16082
  clearStartWatchdog(state);
15971
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
+ );
15972
16090
  state.startWatchdog = setTimeout(
15973
16091
  () => onStartWatchdogFire(state),
15974
- state.startWatchdogMs
16092
+ state.startWatchdogArmedMs
15975
16093
  );
15976
16094
  }
16095
+ function noteStartWatchdogInit(state) {
16096
+ if (!state.startWatchdog || !state.startWatchdogBeforeInit) return;
16097
+ if (state.activeProcess?.initSeen !== true) return;
16098
+ armStartWatchdog(state);
16099
+ }
15977
16100
  function deliverPendingCompletions(state, force = false) {
15978
16101
  const pending2 = state.activeProcess?.pendingProxyCompletions;
15979
16102
  const entries = [...pending2?.values() ?? []].filter(
@@ -16329,6 +16452,7 @@ function createLineHandler(state, ctx) {
16329
16452
  subtype: msg.subtype
16330
16453
  });
16331
16454
  if (msg.type === "system" && msg.subtype === "init") {
16455
+ noteStartWatchdogInit(state);
16332
16456
  if (msg.session_id) {
16333
16457
  setClaudeSessionId(state.sessionKey, msg.session_id);
16334
16458
  log.info("session initialized", {
@@ -16927,7 +17051,7 @@ ${plan}
16927
17051
  if (msg.session_id) {
16928
17052
  setClaudeSessionId(state.sessionKey, msg.session_id);
16929
17053
  }
16930
- 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;
16931
17055
  if (deliverPendingCompletions(state)) {
16932
17056
  return;
16933
17057
  }
@@ -17020,10 +17144,17 @@ ${plan}
17020
17144
  numTurns: msg.num_turns,
17021
17145
  isError: msg.is_error,
17022
17146
  subtype: msg.subtype,
17023
- ...turnStatsLogPayload(turnStats)
17147
+ ...turnStatsLogPayload(turnStats),
17148
+ turnCostUsd: resultCost?.deltaUsd
17024
17149
  });
17025
17150
  if (ctx.config.turnStats && !ctx.compactionMode && !msg.is_error && !failure) {
17026
- const footer = formatTurnStatsBlock(turnStats);
17151
+ const footer = formatTurnStatsBlock({
17152
+ ...turnStats,
17153
+ costUsd: footerCostUsd(
17154
+ resultCost,
17155
+ state.activeProcess?.cliArgs?.includes("--resume") === true
17156
+ )
17157
+ });
17027
17158
  if (footer) {
17028
17159
  state.controller.enqueue({
17029
17160
  type: "text-delta",
@@ -17098,6 +17229,26 @@ async function isProxyCallStillServed(callId) {
17098
17229
  setProxyDeadlineGuard(({ callId }) => isProxyCallStillServed(callId));
17099
17230
  var FORWARD_SETTLE_MS = 250;
17100
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
+ }
17101
17252
  var ClaudeCodeLanguageModel = class {
17102
17253
  specificationVersion = "v3";
17103
17254
  modelId;
@@ -17585,32 +17736,35 @@ var ClaudeCodeLanguageModel = class {
17585
17736
  * switch, and outside `runModelChain` on purpose: an account switch starts
17586
17737
  * that account's own model chain from the top.
17587
17738
  *
17588
- * Anything but `"auto"` is `runModelChain` and one `await`, so `"off"` and
17589
- * `"ask"` are exactly what they were. With `"auto"`, the attempt that failed
17590
- * decides the switch itself (`completeResult` sets `run.switchTo` and closes
17591
- * its stream without a `finish`), and the next attempt's prologue makes the
17592
- * switch the form's pick would have made. This only splices the attempts
17593
- * into one caller turn: one `stream-start`, and the `finish` of whichever
17594
- * 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.
17595
17745
  *
17596
17746
  * The bound is `run.tried`, as it is `tried` for the model chain: every
17597
17747
  * account spawned or switched to goes in, and none in it is picked again.
17598
17748
  *
17599
- * Cancelling the returned stream is a stop, for every attempt: this loop is
17600
- * the plugin starting more work on the caller's behalf, so a caller that
17601
- * has gone away must reach all of it. Each attempt gets a signal that the
17602
- * caller's own `abortSignal` and a `cancel()` both abort, and the loop
17603
- * 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.
17604
17755
  */
17605
17756
  async runAccountFailover(options, mode) {
17606
- if (this.config.accountFailover !== "auto") return this.runModelChain(options, mode);
17607
- const self = this;
17608
- const run = { tried: /* @__PURE__ */ new Set() };
17609
17757
  const cancelled2 = new AbortController();
17610
17758
  const attemptOptions = {
17611
17759
  ...options,
17612
17760
  abortSignal: options.abortSignal ? AbortSignal.any([options.abortSignal, cancelled2.signal]) : cancelled2.signal
17613
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() };
17614
17768
  const stopped = () => attemptOptions.abortSignal.aborted;
17615
17769
  let inner = await this.runModelChain(attemptOptions, mode, run);
17616
17770
  const request = inner.request;
@@ -17652,9 +17806,8 @@ var ClaudeCodeLanguageModel = class {
17652
17806
  }
17653
17807
  }
17654
17808
  },
17655
- async cancel(reason) {
17809
+ cancel(reason) {
17656
17810
  cancelled2.abort(reason);
17657
- await reader?.cancel(reason).catch(() => void 0);
17658
17811
  }
17659
17812
  });
17660
17813
  return { stream, request };
@@ -18061,16 +18214,19 @@ var ClaudeCodeLanguageModel = class {
18061
18214
  resetsAt: autoSwitch.resetsAt
18062
18215
  } : compactionMode || mode === "generate" ? null : consumeAccountFailoverAnswer(sk, options.prompt, {
18063
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,
18064
18220
  candidates: failoverCandidates(
18065
18221
  this.config.failoverAccounts,
18066
18222
  sourceAccount,
18067
18223
  groups
18068
- )
18224
+ ).filter((account) => account !== (failover.target ?? sourceAccount))
18069
18225
  });
18070
18226
  if (failoverAnswer?.kind === "stop") {
18071
18227
  log.warn("account failover declined; ending the turn", {
18072
18228
  sessionKey: sk,
18073
- account: sourceAccount,
18229
+ account: failover.target ?? sourceAccount,
18074
18230
  reason: failoverAnswer.reason
18075
18231
  });
18076
18232
  const note = formatFailoverStopNote(failoverAnswer.reason);
@@ -18084,7 +18240,10 @@ var ClaudeCodeLanguageModel = class {
18084
18240
  controller.enqueue({
18085
18241
  type: "error",
18086
18242
  error: new Error(
18087
- `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.`
18088
18247
  )
18089
18248
  });
18090
18249
  controller.enqueue({
@@ -18188,13 +18347,6 @@ var ClaudeCodeLanguageModel = class {
18188
18347
  }
18189
18348
  cliPath = failover.cliPath;
18190
18349
  asideTransportRef.cliPath = cliPath;
18191
- if (!autoSwitch) {
18192
- failoverNote = formatFailoverNote({
18193
- sourceAccount: failoverAnswer.sourceAccount,
18194
- target: failoverAnswer.target,
18195
- resetsAt: failoverAnswer.resetsAt
18196
- });
18197
- }
18198
18350
  }
18199
18351
  const spawnConfigDir = this.config.accountInProcess ? this.skillBridgeSpawn(failover).configDir : void 0;
18200
18352
  const processForAccount = getActiveProcess(sk);
@@ -18266,6 +18418,14 @@ var ClaudeCodeLanguageModel = class {
18266
18418
  reason: autoSwitch.reason,
18267
18419
  carried: conversationOnTarget
18268
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
+ });
18269
18429
  }
18270
18430
  let accountGroupNote = null;
18271
18431
  if (!compactionMode && groups && accountProviders.size > 0 && includeHistoryContext && failoverAnswer?.kind !== "switch") {
@@ -18532,11 +18692,12 @@ var ClaudeCodeLanguageModel = class {
18532
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."
18533
18693
  );
18534
18694
  }
18695
+ const spawnedAccount = failover.target ?? sourceAccount;
18535
18696
  const failoverAccounts = failoverCandidates(
18536
18697
  this.config.failoverAccounts,
18537
18698
  sourceAccount,
18538
18699
  groups
18539
- );
18700
+ ).filter((account) => account !== spawnedAccount);
18540
18701
  const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover === "ask" && !compactionMode && // A `doGenerate` caller takes the override and reports the plain
18541
18702
  // rate-limit error, never the form. See `TurnMode`.
18542
18703
  mode !== "generate" && isAccountFailoverQuestionActive({
@@ -18549,7 +18710,6 @@ var ClaudeCodeLanguageModel = class {
18549
18710
  // the operator is usually not even looking at.
18550
18711
  childSession: !!await fetchSessionParentId(affinity)
18551
18712
  });
18552
- const spawnedAccount = failover.target ?? sourceAccount;
18553
18713
  accountRun?.tried.add(spawnedAccount);
18554
18714
  let autoFailoverChoice;
18555
18715
  const autoFailoverTarget = () => {
@@ -18558,7 +18718,7 @@ var ClaudeCodeLanguageModel = class {
18558
18718
  }
18559
18719
  return autoFailoverChoice ?? void 0;
18560
18720
  };
18561
- const limitAccount = accountRun ? spawnedAccount : sourceAccount;
18721
+ const limitAccount = spawnedAccount;
18562
18722
  const usageLimitNoteActive = !compactionMode && mode !== "generate" && !failoverAskActive;
18563
18723
  const usageWarnThresholds = resolveUsageWarnThresholds(this.config.usageWarnThresholds);
18564
18724
  const usageSwitchAt = this.config.usageSwitchAt === null ? null : resolveUsageSwitchAt(this.config.usageSwitchAt);
@@ -19307,7 +19467,11 @@ var ClaudeCodeLanguageModel = class {
19307
19467
  state.activeProcess?.pendingProxyCompletions?.clear();
19308
19468
  if ((state.accountLimitHit || state.accountBlock) && failoverAskActive && msg.is_error === true) {
19309
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.
19310
19473
  sourceAccount,
19474
+ failedAccount: limitAccount,
19311
19475
  candidates: failoverAccounts,
19312
19476
  resetsAt: state.accountLimitHit?.resetsAt,
19313
19477
  window: state.accountLimitHit?.window,
@@ -19315,7 +19479,7 @@ var ClaudeCodeLanguageModel = class {
19315
19479
  carryTranscript: self.config.crossAccountResume !== false
19316
19480
  });
19317
19481
  log.warn(
19318
- `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.`,
19319
19483
  {
19320
19484
  sessionKey: sk,
19321
19485
  candidates: failoverAccounts,
@@ -19406,7 +19570,7 @@ var ClaudeCodeLanguageModel = class {
19406
19570
  state.endTextBlock();
19407
19571
  state.resultFailure ??= "usage_limit";
19408
19572
  }
19409
- if (accountRun && state.accountLimitHit && msg.is_error === true) {
19573
+ if (state.accountLimitHit && msg.is_error === true) {
19410
19574
  state.resultFailure ??= "usage_limit";
19411
19575
  }
19412
19576
  if (runAutoContinue(state, msg)) return;
@@ -19525,7 +19689,11 @@ var ClaudeCodeLanguageModel = class {
19525
19689
  interactive: useInteractive,
19526
19690
  sourceAccount: limitAccount,
19527
19691
  usageAccount: spawnedAccount,
19528
- 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,
19529
19697
  failoverAskActive,
19530
19698
  autoFailoverTarget,
19531
19699
  usageLimitNoteActive,