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

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) {
@@ -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;
@@ -7840,6 +7872,7 @@ function claudeSpawnEnv(opts) {
7840
7872
  if (opts?.configDir) {
7841
7873
  env.CLAUDE_CONFIG_DIR = opts.configDir;
7842
7874
  }
7875
+ Object.assign(env, todoToolsEnv(opts?.enableTodoTools === true));
7843
7876
  if (opts?.ignoreAnthropicApiKey) {
7844
7877
  delete env.ANTHROPIC_API_KEY;
7845
7878
  delete env.ANTHROPIC_AUTH_TOKEN;
@@ -7849,6 +7882,10 @@ function claudeSpawnEnv(opts) {
7849
7882
  }
7850
7883
  return env;
7851
7884
  }
7885
+ function todoToolsEnv(enabled) {
7886
+ if (!enabled || process.env.CLAUDE_CODE_ENABLE_TODO_TOOLS !== void 0) return {};
7887
+ return { CLAUDE_CODE_ENABLE_TODO_TOOLS: "1" };
7888
+ }
7852
7889
  function touch(key) {
7853
7890
  const existing = activeProcesses.get(key);
7854
7891
  if (existing) {
@@ -7885,6 +7922,18 @@ function isTerminalResultLine(line) {
7885
7922
  return false;
7886
7923
  }
7887
7924
  }
7925
+ function isSystemInitLine(line) {
7926
+ if (!line.includes('"init"')) return false;
7927
+ try {
7928
+ const parsed = JSON.parse(line);
7929
+ return parsed.type === "system" && parsed.subtype === "init";
7930
+ } catch {
7931
+ return false;
7932
+ }
7933
+ }
7934
+ function noteInitLine(ap, line) {
7935
+ if (!ap.initSeen && isSystemInitLine(line)) ap.initSeen = true;
7936
+ }
7888
7937
  function settleTurn(ap) {
7889
7938
  ap.turnInFlight = false;
7890
7939
  const waiters = ap.turnIdleWaiters ?? [];
@@ -8208,7 +8257,7 @@ function invalidateOtherEffortSessions(baseKey, effort) {
8208
8257
  clearBackgroundTasks(key);
8209
8258
  }
8210
8259
  }
8211
- function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile, ignoreAnthropicApiKey, effort, promptCacheTtl, configDir) {
8260
+ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile, ignoreAnthropicApiKey, effort, promptCacheTtl, configDir, enableTodoTools) {
8212
8261
  evictIfNeeded();
8213
8262
  log.info("spawning new claude process", {
8214
8263
  cliPath,
@@ -8217,13 +8266,14 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
8217
8266
  sessionKey: sessionKey2,
8218
8267
  effort,
8219
8268
  promptCacheTtl,
8220
- configDir
8269
+ configDir,
8270
+ ...enableTodoTools ? { enableTodoTools } : {}
8221
8271
  });
8222
8272
  const plan = planClaudeSpawn(cliPath, cliArgs);
8223
8273
  const proc = spawn(plan.file, plan.args, {
8224
8274
  cwd,
8225
8275
  stdio: ["pipe", "pipe", "pipe"],
8226
- env: claudeSpawnEnv({ ignoreAnthropicApiKey, effort, promptCacheTtl, configDir }),
8276
+ env: claudeSpawnEnv({ ignoreAnthropicApiKey, effort, promptCacheTtl, configDir, enableTodoTools }),
8227
8277
  windowsVerbatimArguments: plan.windowsVerbatimArguments
8228
8278
  });
8229
8279
  const lineEmitter = new EventEmitter3();
@@ -8235,6 +8285,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
8235
8285
  systemPromptFile,
8236
8286
  effort,
8237
8287
  promptCacheTtl,
8288
+ enableTodoTools,
8238
8289
  startedAt: Date.now(),
8239
8290
  cliPath,
8240
8291
  cliArgs: [...cliArgs],
@@ -8246,6 +8297,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
8246
8297
  rl.on("line", (line) => {
8247
8298
  if (dispatchSideQuestionResponse(ap, line)) return;
8248
8299
  noteTurnLine(ap, line);
8300
+ noteInitLine(ap, line);
8249
8301
  if (lineEmitter.listenerCount("line") === 0) {
8250
8302
  bufferUnattendedLine(ap, line);
8251
8303
  return;
@@ -8365,7 +8417,8 @@ function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropi
8365
8417
  ignoreAnthropicApiKey,
8366
8418
  old.effort,
8367
8419
  old.promptCacheTtl,
8368
- old.configDir
8420
+ old.configDir,
8421
+ old.enableTodoTools
8369
8422
  );
8370
8423
  replacement.mcpServers = old.mcpServers;
8371
8424
  replacement.pendingProxyCompletions = old.pendingProxyCompletions;
@@ -8860,9 +8913,10 @@ function resolveBudgetWarnAt(value) {
8860
8913
  }
8861
8914
  return [...new Set(value)].sort((a, b) => a - b);
8862
8915
  }
8863
- function costDelta(claudeSessionId, totalCostUsd, cumulative) {
8864
- if (!Number.isFinite(totalCostUsd) || totalCostUsd <= 0) return 0;
8865
- if (!cumulative || !claudeSessionId) return totalCostUsd;
8916
+ function takeResultCost(claudeSessionId, totalCostUsd, cumulative) {
8917
+ if (!Number.isFinite(totalCostUsd) || totalCostUsd <= 0) return { deltaUsd: 0, noBaseline: false };
8918
+ if (!cumulative) return { deltaUsd: totalCostUsd, noBaseline: false };
8919
+ if (!claudeSessionId) return { deltaUsd: totalCostUsd, noBaseline: true };
8866
8920
  const previous = lastTotals.get(claudeSessionId);
8867
8921
  lastTotals.delete(claudeSessionId);
8868
8922
  lastTotals.set(claudeSessionId, totalCostUsd);
@@ -8871,8 +8925,9 @@ function costDelta(claudeSessionId, totalCostUsd, cumulative) {
8871
8925
  if (oldest.done) break;
8872
8926
  lastTotals.delete(oldest.value);
8873
8927
  }
8874
- if (previous === void 0 || totalCostUsd < previous) return totalCostUsd;
8875
- return totalCostUsd - previous;
8928
+ if (previous === void 0) return { deltaUsd: totalCostUsd, noBaseline: true };
8929
+ if (totalCostUsd < previous) return { deltaUsd: totalCostUsd, noBaseline: false };
8930
+ return { deltaUsd: totalCostUsd - previous, noBaseline: false };
8876
8931
  }
8877
8932
  function recordSessionCost(session, deltaUsd) {
8878
8933
  if (!(deltaUsd > 0)) return;
@@ -8931,8 +8986,10 @@ function formatBudgetStopNote(spentUsd, capUsd) {
8931
8986
  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
8987
  }
8933
8988
  function recordResultCost(budget, claudeSessionId, totalCostUsd) {
8934
- if (typeof totalCostUsd !== "number") return;
8935
- recordSessionCost(budget.session, costDelta(claudeSessionId, totalCostUsd, budget.cumulative));
8989
+ if (typeof totalCostUsd !== "number") return void 0;
8990
+ const cost = takeResultCost(claudeSessionId, totalCostUsd, budget.cumulative);
8991
+ recordSessionCost(budget.session, cost.deltaUsd);
8992
+ return cost;
8936
8993
  }
8937
8994
  function takeBudgetNote(budget) {
8938
8995
  if (!budget.notes) return null;
@@ -9148,6 +9205,7 @@ var BUNDLE_DATA_ALLOWLIST = {
9148
9205
  // booleans: every one of these is a decision the plugin made
9149
9206
  applied: "flag",
9150
9207
  attached: "flag",
9208
+ beforeInit: "flag",
9151
9209
  bridgeSkipNativeSkills: "flag",
9152
9210
  channelClosed: "flag",
9153
9211
  compaction: "flag",
@@ -9249,9 +9307,11 @@ var BUNDLE_DATA_ALLOWLIST = {
9249
9307
  durationMs: "duration",
9250
9308
  elapsedMs: "duration",
9251
9309
  remainingMs: "duration",
9310
+ startWatchdogMs: "duration",
9252
9311
  timeoutMs: "duration",
9253
9312
  waitedMs: "duration",
9254
9313
  costUsd: "money",
9314
+ turnCostUsd: "money",
9255
9315
  // enums: a fixed vocabulary this package or the CLI defines
9256
9316
  apiKeySource: "enum",
9257
9317
  delivery: "enum",
@@ -11059,6 +11119,11 @@ function formatDoctorReport(report) {
11059
11119
  `| subagentDispatch | ${report.subagentDispatch}${report.subagentDispatch === "ask" && report.subagentDispatchCrossGroup ? " (cross-group accounts allowed)" : ""} |`
11060
11120
  );
11061
11121
  lines.push(`| turnStats | ${report.turnStats} |`);
11122
+ if (report.todoTools) {
11123
+ lines.push(
11124
+ `| enableTodoTools | ${report.todoTools.option}${report.todoTools.envSet ? " (CLAUDE_CODE_ENABLE_TODO_TOOLS is set in opencode's environment and decides)" : ""} |`
11125
+ );
11126
+ }
11062
11127
  lines.push(`| ANTHROPIC_API_KEY in env | ${report.anthropicApiKeyInEnv ? "yes" : "no"} |`);
11063
11128
  const overriding = report.permissionPresets.filter((row) => row.overrides.length > 0);
11064
11129
  if (overriding.length > 0) {
@@ -11490,6 +11555,10 @@ async function gatherDoctorReport(options) {
11490
11555
  subagentDispatch: base.subagentDispatch,
11491
11556
  subagentDispatchCrossGroup: base.subagentDispatchCrossGroup,
11492
11557
  turnStats: options.turnStats,
11558
+ todoTools: {
11559
+ option: options.enableTodoTools === true,
11560
+ envSet: process.env.CLAUDE_CODE_ENABLE_TODO_TOOLS !== void 0
11561
+ },
11493
11562
  anthropicApiKeyInEnv: base.anthropicApiKeyInEnv,
11494
11563
  processes,
11495
11564
  pendingCalls: snapshotPendingProxyCalls(),
@@ -11696,6 +11765,11 @@ function extractTurnStats(msg) {
11696
11765
  if (Array.isArray(msg.permission_denials)) stats.permissionDenials = msg.permission_denials;
11697
11766
  return stats;
11698
11767
  }
11768
+ function footerCostUsd(cost, resumedSession) {
11769
+ if (!cost) return void 0;
11770
+ if (cost.noBaseline && resumedSession) return void 0;
11771
+ return cost.deltaUsd;
11772
+ }
11699
11773
  function formatCost(costUsd) {
11700
11774
  if (!Number.isFinite(costUsd) || costUsd < 0) return "$0.00";
11701
11775
  return costUsd >= 1 ? `$${costUsd.toFixed(2)}` : `$${costUsd.toFixed(4)}`;
@@ -15559,6 +15633,8 @@ function createTurnState(init) {
15559
15633
  asideSinkUnregister: null,
15560
15634
  resultFallbackTimer: null,
15561
15635
  startWatchdog: null,
15636
+ startWatchdogArmedMs: 0,
15637
+ startWatchdogBeforeInit: false,
15562
15638
  respawnAttempted: false,
15563
15639
  hasReceivedContent: false,
15564
15640
  hasReceivedProgress: false,
@@ -15934,7 +16010,11 @@ function onStartWatchdogFire(state) {
15934
16010
  if (state.respawnAttempted) {
15935
16011
  log.error(
15936
16012
  "claude process still silent after respawn; ending turn",
15937
- { sessionKey: state.sessionKey }
16013
+ {
16014
+ sessionKey: state.sessionKey,
16015
+ startWatchdogMs: state.startWatchdogArmedMs,
16016
+ beforeInit: state.startWatchdogBeforeInit
16017
+ }
15938
16018
  );
15939
16019
  deleteActiveProcess(state.sessionKey);
15940
16020
  deleteClaudeSessionId(state.sessionKey);
@@ -15955,7 +16035,11 @@ function onStartWatchdogFire(state) {
15955
16035
  state.respawnAttempted = true;
15956
16036
  log.warn(
15957
16037
  "no stdout after envelope write; respawning claude process to resume conversation",
15958
- { sessionKey: state.sessionKey, startWatchdogMs: state.startWatchdogMs }
16038
+ {
16039
+ sessionKey: state.sessionKey,
16040
+ startWatchdogMs: state.startWatchdogArmedMs,
16041
+ beforeInit: state.startWatchdogBeforeInit
16042
+ }
15959
16043
  );
15960
16044
  state.lineEmitter.off("line", state.lineHandler);
15961
16045
  state.lineEmitter.off("close", state.closeHandler);
@@ -16007,14 +16091,29 @@ function onStartWatchdogFire(state) {
16007
16091
  }
16008
16092
  armStartWatchdog(state);
16009
16093
  }
16094
+ var START_WATCHDOG_INIT_MARGIN_MS = 3e4;
16095
+ function startWatchdogDelayMs(configuredMs, beforeInit, mcpTimeoutMs = effectiveSpawnMcpTimeoutMs()) {
16096
+ return beforeInit ? Math.max(configuredMs, mcpTimeoutMs + START_WATCHDOG_INIT_MARGIN_MS) : configuredMs;
16097
+ }
16010
16098
  function armStartWatchdog(state) {
16011
16099
  clearStartWatchdog(state);
16012
16100
  if (state.controllerClosed) return;
16101
+ const ap = state.activeProcess;
16102
+ state.startWatchdogBeforeInit = !ap?.interactiveControl && ap?.initSeen !== true;
16103
+ state.startWatchdogArmedMs = startWatchdogDelayMs(
16104
+ state.startWatchdogMs,
16105
+ state.startWatchdogBeforeInit
16106
+ );
16013
16107
  state.startWatchdog = setTimeout(
16014
16108
  () => onStartWatchdogFire(state),
16015
- state.startWatchdogMs
16109
+ state.startWatchdogArmedMs
16016
16110
  );
16017
16111
  }
16112
+ function noteStartWatchdogInit(state) {
16113
+ if (!state.startWatchdog || !state.startWatchdogBeforeInit) return;
16114
+ if (state.activeProcess?.initSeen !== true) return;
16115
+ armStartWatchdog(state);
16116
+ }
16018
16117
  function deliverPendingCompletions(state, force = false) {
16019
16118
  const pending2 = state.activeProcess?.pendingProxyCompletions;
16020
16119
  const entries = [...pending2?.values() ?? []].filter(
@@ -16370,6 +16469,7 @@ function createLineHandler(state, ctx) {
16370
16469
  subtype: msg.subtype
16371
16470
  });
16372
16471
  if (msg.type === "system" && msg.subtype === "init") {
16472
+ noteStartWatchdogInit(state);
16373
16473
  if (msg.session_id) {
16374
16474
  setClaudeSessionId(state.sessionKey, msg.session_id);
16375
16475
  log.info("session initialized", {
@@ -16968,7 +17068,7 @@ ${plan}
16968
17068
  if (msg.session_id) {
16969
17069
  setClaudeSessionId(state.sessionKey, msg.session_id);
16970
17070
  }
16971
- if (ctx.budget) recordResultCost(ctx.budget, msg.session_id, msg.total_cost_usd);
17071
+ 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
17072
  if (deliverPendingCompletions(state)) {
16973
17073
  return;
16974
17074
  }
@@ -17061,10 +17161,17 @@ ${plan}
17061
17161
  numTurns: msg.num_turns,
17062
17162
  isError: msg.is_error,
17063
17163
  subtype: msg.subtype,
17064
- ...turnStatsLogPayload(turnStats)
17164
+ ...turnStatsLogPayload(turnStats),
17165
+ turnCostUsd: resultCost?.deltaUsd
17065
17166
  });
17066
17167
  if (ctx.config.turnStats && !ctx.compactionMode && !msg.is_error && !failure) {
17067
- const footer = formatTurnStatsBlock(turnStats);
17168
+ const footer = formatTurnStatsBlock({
17169
+ ...turnStats,
17170
+ costUsd: footerCostUsd(
17171
+ resultCost,
17172
+ state.activeProcess?.cliArgs?.includes("--resume") === true
17173
+ )
17174
+ });
17068
17175
  if (footer) {
17069
17176
  state.controller.enqueue({
17070
17177
  type: "text-delta",
@@ -17139,6 +17246,26 @@ async function isProxyCallStillServed(callId) {
17139
17246
  setProxyDeadlineGuard(({ callId }) => isProxyCallStillServed(callId));
17140
17247
  var FORWARD_SETTLE_MS = 250;
17141
17248
  var CHILD_EXIT_STATUS_GRACE_MS = 250;
17249
+ function stopOnCancel(stream, cancelled2) {
17250
+ const reader = stream.getReader();
17251
+ return new ReadableStream({
17252
+ async pull(controller) {
17253
+ try {
17254
+ const { done, value } = await reader.read();
17255
+ if (done) controller.close();
17256
+ else controller.enqueue(value);
17257
+ } catch (error) {
17258
+ try {
17259
+ controller.error(error);
17260
+ } catch {
17261
+ }
17262
+ }
17263
+ },
17264
+ cancel(reason) {
17265
+ cancelled2.abort(reason);
17266
+ }
17267
+ });
17268
+ }
17142
17269
  var ClaudeCodeLanguageModel = class {
17143
17270
  specificationVersion = "v3";
17144
17271
  modelId;
@@ -17626,32 +17753,35 @@ var ClaudeCodeLanguageModel = class {
17626
17753
  * switch, and outside `runModelChain` on purpose: an account switch starts
17627
17754
  * that account's own model chain from the top.
17628
17755
  *
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.
17756
+ * Anything but `"auto"` is `runModelChain` behind `stopOnCancel`. With
17757
+ * `"auto"`, the attempt that failed decides the switch itself
17758
+ * (`completeResult` sets `run.switchTo` and closes its stream without a
17759
+ * `finish`), and the next attempt's prologue makes the switch the form's pick
17760
+ * would have made. This only splices the attempts into one caller turn: one
17761
+ * `stream-start`, and the `finish` of whichever attempt did not switch.
17636
17762
  *
17637
17763
  * The bound is `run.tried`, as it is `tried` for the model chain: every
17638
17764
  * account spawned or switched to goes in, and none in it is picked again.
17639
17765
  *
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.
17766
+ * Cancelling the returned stream is a stop, in every mode: a caller that has
17767
+ * gone away must reach the turn it started, or the CLI runs it to the end
17768
+ * and bills it. Each attempt gets a signal that the caller's own
17769
+ * `abortSignal` and a `cancel()` both abort, so a cancel takes the turn's
17770
+ * ordinary abort path, and with `"auto"` the loop starts no further account
17771
+ * once either has happened. Until (h #g257) only `"auto"` did this.
17645
17772
  */
17646
17773
  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
17774
  const cancelled2 = new AbortController();
17651
17775
  const attemptOptions = {
17652
17776
  ...options,
17653
17777
  abortSignal: options.abortSignal ? AbortSignal.any([options.abortSignal, cancelled2.signal]) : cancelled2.signal
17654
17778
  };
17779
+ if (this.config.accountFailover !== "auto") {
17780
+ const inner2 = await this.runModelChain(attemptOptions, mode);
17781
+ return { ...inner2, stream: stopOnCancel(inner2.stream, cancelled2) };
17782
+ }
17783
+ const self = this;
17784
+ const run = { tried: /* @__PURE__ */ new Set() };
17655
17785
  const stopped = () => attemptOptions.abortSignal.aborted;
17656
17786
  let inner = await this.runModelChain(attemptOptions, mode, run);
17657
17787
  const request = inner.request;
@@ -17693,9 +17823,8 @@ var ClaudeCodeLanguageModel = class {
17693
17823
  }
17694
17824
  }
17695
17825
  },
17696
- async cancel(reason) {
17826
+ cancel(reason) {
17697
17827
  cancelled2.abort(reason);
17698
- await reader?.cancel(reason).catch(() => void 0);
17699
17828
  }
17700
17829
  });
17701
17830
  return { stream, request };
@@ -17907,6 +18036,7 @@ var ClaudeCodeLanguageModel = class {
17907
18036
  cliPath,
17908
18037
  interactive: !!useInteractive,
17909
18038
  turnStats: this.config.turnStats === true,
18039
+ enableTodoTools: this.config.enableTodoTools === true,
17910
18040
  ignoreAnthropicApiKey: this.config.ignoreAnthropicApiKey === true,
17911
18041
  // Where the plugin applies the account itself rather than through a
17912
18042
  // wrapper (`accountInProcess`, Windows), the `/cost` spawn needs the
@@ -18102,16 +18232,19 @@ var ClaudeCodeLanguageModel = class {
18102
18232
  resetsAt: autoSwitch.resetsAt
18103
18233
  } : compactionMode || mode === "generate" ? null : consumeAccountFailoverAnswer(sk, options.prompt, {
18104
18234
  sourceAccount,
18235
+ // What the form would offer now: an override still standing is
18236
+ // still the account that failed, and is not on offer (h #g257).
18237
+ failedAccount: failover.target ?? sourceAccount,
18105
18238
  candidates: failoverCandidates(
18106
18239
  this.config.failoverAccounts,
18107
18240
  sourceAccount,
18108
18241
  groups
18109
- )
18242
+ ).filter((account) => account !== (failover.target ?? sourceAccount))
18110
18243
  });
18111
18244
  if (failoverAnswer?.kind === "stop") {
18112
18245
  log.warn("account failover declined; ending the turn", {
18113
18246
  sessionKey: sk,
18114
- account: sourceAccount,
18247
+ account: failover.target ?? sourceAccount,
18115
18248
  reason: failoverAnswer.reason
18116
18249
  });
18117
18250
  const note = formatFailoverStopNote(failoverAnswer.reason);
@@ -18125,7 +18258,10 @@ var ClaudeCodeLanguageModel = class {
18125
18258
  controller.enqueue({
18126
18259
  type: "error",
18127
18260
  error: new Error(
18128
- `Claude account "${sourceAccount}" is out of usage and no other account was picked.`
18261
+ // The account that failed: after a chain of overrides that is
18262
+ // not the provider's own (h #g257). A stop answer also follows
18263
+ // an expired login, so this does not say "out of usage".
18264
+ `Claude account "${failover.target ?? sourceAccount}" cannot serve this turn and no other account was picked.`
18129
18265
  )
18130
18266
  });
18131
18267
  controller.enqueue({
@@ -18229,13 +18365,6 @@ var ClaudeCodeLanguageModel = class {
18229
18365
  }
18230
18366
  cliPath = failover.cliPath;
18231
18367
  asideTransportRef.cliPath = cliPath;
18232
- if (!autoSwitch) {
18233
- failoverNote = formatFailoverNote({
18234
- sourceAccount: failoverAnswer.sourceAccount,
18235
- target: failoverAnswer.target,
18236
- resetsAt: failoverAnswer.resetsAt
18237
- });
18238
- }
18239
18368
  }
18240
18369
  const spawnConfigDir = this.config.accountInProcess ? this.skillBridgeSpawn(failover).configDir : void 0;
18241
18370
  const processForAccount = getActiveProcess(sk);
@@ -18307,6 +18436,14 @@ var ClaudeCodeLanguageModel = class {
18307
18436
  reason: autoSwitch.reason,
18308
18437
  carried: conversationOnTarget
18309
18438
  });
18439
+ } else if (!autoSwitch && failoverAnswer?.kind === "switch") {
18440
+ failoverNote = formatFailoverNote({
18441
+ sourceAccount: failoverAnswer.failedAccount ?? failoverAnswer.sourceAccount,
18442
+ target: failoverAnswer.target,
18443
+ resetsAt: failoverAnswer.resetsAt,
18444
+ reason: failoverAnswer.reason,
18445
+ carried: conversationOnTarget
18446
+ });
18310
18447
  }
18311
18448
  let accountGroupNote = null;
18312
18449
  if (!compactionMode && groups && accountProviders.size > 0 && includeHistoryContext && failoverAnswer?.kind !== "switch") {
@@ -18573,11 +18710,12 @@ var ClaudeCodeLanguageModel = class {
18573
18710
  "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
18711
  );
18575
18712
  }
18713
+ const spawnedAccount = failover.target ?? sourceAccount;
18576
18714
  const failoverAccounts = failoverCandidates(
18577
18715
  this.config.failoverAccounts,
18578
18716
  sourceAccount,
18579
18717
  groups
18580
- );
18718
+ ).filter((account) => account !== spawnedAccount);
18581
18719
  const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover === "ask" && !compactionMode && // A `doGenerate` caller takes the override and reports the plain
18582
18720
  // rate-limit error, never the form. See `TurnMode`.
18583
18721
  mode !== "generate" && isAccountFailoverQuestionActive({
@@ -18590,7 +18728,6 @@ var ClaudeCodeLanguageModel = class {
18590
18728
  // the operator is usually not even looking at.
18591
18729
  childSession: !!await fetchSessionParentId(affinity)
18592
18730
  });
18593
- const spawnedAccount = failover.target ?? sourceAccount;
18594
18731
  accountRun?.tried.add(spawnedAccount);
18595
18732
  let autoFailoverChoice;
18596
18733
  const autoFailoverTarget = () => {
@@ -18599,7 +18736,7 @@ var ClaudeCodeLanguageModel = class {
18599
18736
  }
18600
18737
  return autoFailoverChoice ?? void 0;
18601
18738
  };
18602
- const limitAccount = accountRun ? spawnedAccount : sourceAccount;
18739
+ const limitAccount = spawnedAccount;
18603
18740
  const usageLimitNoteActive = !compactionMode && mode !== "generate" && !failoverAskActive;
18604
18741
  const usageWarnThresholds = resolveUsageWarnThresholds(this.config.usageWarnThresholds);
18605
18742
  const usageSwitchAt = this.config.usageSwitchAt === null ? null : resolveUsageSwitchAt(this.config.usageSwitchAt);
@@ -19100,7 +19237,8 @@ var ClaudeCodeLanguageModel = class {
19100
19237
  claudeSpawnEnv({
19101
19238
  ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
19102
19239
  effort: reasoningEffort,
19103
- promptCacheTtl
19240
+ promptCacheTtl,
19241
+ enableTodoTools: self.config.enableTodoTools === true
19104
19242
  })
19105
19243
  )
19106
19244
  });
@@ -19218,7 +19356,9 @@ var ClaudeCodeLanguageModel = class {
19218
19356
  self.config.ignoreAnthropicApiKey,
19219
19357
  reasoningEffort,
19220
19358
  promptCacheTtl,
19221
- spawnConfigDir
19359
+ spawnConfigDir,
19360
+ // A compaction spawn runs no tools, so it never asks for them.
19361
+ self.config.enableTodoTools === true && !compactionMode
19222
19362
  );
19223
19363
  ap.mcpServers = spawnMcpServers;
19224
19364
  state.proc = ap.proc;
@@ -19348,7 +19488,11 @@ var ClaudeCodeLanguageModel = class {
19348
19488
  state.activeProcess?.pendingProxyCompletions?.clear();
19349
19489
  if ((state.accountLimitHit || state.accountBlock) && failoverAskActive && msg.is_error === true) {
19350
19490
  const call = createAccountFailoverQuestionCall(sk, {
19491
+ // The override a pick sets is keyed on the provider's own
19492
+ // account, the one every turn of this conversation resolves
19493
+ // from; the form names the account that actually failed.
19351
19494
  sourceAccount,
19495
+ failedAccount: limitAccount,
19352
19496
  candidates: failoverAccounts,
19353
19497
  resetsAt: state.accountLimitHit?.resetsAt,
19354
19498
  window: state.accountLimitHit?.window,
@@ -19356,7 +19500,7 @@ var ClaudeCodeLanguageModel = class {
19356
19500
  carryTranscript: self.config.crossAccountResume !== false
19357
19501
  });
19358
19502
  log.warn(
19359
- `Claude account "${sourceAccount}" ${state.accountLimitHit || !state.accountBlock ? "is out of usage" : `cannot serve (${state.accountBlock})`}; asking which account to continue on.`,
19503
+ `Claude account "${limitAccount}" ${state.accountLimitHit || !state.accountBlock ? "is out of usage" : `cannot serve (${state.accountBlock})`}; asking which account to continue on.`,
19360
19504
  {
19361
19505
  sessionKey: sk,
19362
19506
  candidates: failoverAccounts,
@@ -19447,7 +19591,7 @@ var ClaudeCodeLanguageModel = class {
19447
19591
  state.endTextBlock();
19448
19592
  state.resultFailure ??= "usage_limit";
19449
19593
  }
19450
- if (accountRun && state.accountLimitHit && msg.is_error === true) {
19594
+ if (state.accountLimitHit && msg.is_error === true) {
19451
19595
  state.resultFailure ??= "usage_limit";
19452
19596
  }
19453
19597
  if (runAutoContinue(state, msg)) return;
@@ -19566,7 +19710,11 @@ var ClaudeCodeLanguageModel = class {
19566
19710
  interactive: useInteractive,
19567
19711
  sourceAccount: limitAccount,
19568
19712
  usageAccount: spawnedAccount,
19569
- accountConfigDir: accountRun ? self.skillBridgeSpawn(failover).configDir : self.config.configDir,
19713
+ // The config dir of the account that ran, so an account-block note
19714
+ // names that account's login command. The provider's own when no
19715
+ // override moved the turn; until (h #g257) `"off"` and `"ask"`
19716
+ // named the provider's even after one had.
19717
+ accountConfigDir: self.skillBridgeSpawn(failover).configDir,
19570
19718
  failoverAskActive,
19571
19719
  autoFailoverTarget,
19572
19720
  usageLimitNoteActive,
@@ -20943,6 +21091,7 @@ function createClaudeCode(settings = {}) {
20943
21091
  bridgeOpencodeSkills: settings.bridgeOpencodeSkills === true,
20944
21092
  bridgeSkipNativeSkills: settings.bridgeSkipNativeSkills !== false,
20945
21093
  turnStats: settings.turnStats === true,
21094
+ enableTodoTools: settings.enableTodoTools === true,
20946
21095
  forkSessions: settings.forkSessions === true,
20947
21096
  resumeAfterRestart: settings.resumeAfterRestart !== false,
20948
21097
  resumeAcrossModelChanges: settings.resumeAcrossModelChanges !== false,