@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 +12 -12
- package/dist/index.js +189 -62
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +14 -4
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`
|
|
1638
|
-
* `"
|
|
1639
|
-
*
|
|
1640
|
-
*
|
|
1641
|
-
*
|
|
1642
|
-
*
|
|
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,
|
|
1649
|
-
* the
|
|
1650
|
-
*
|
|
1651
|
-
*
|
|
1652
|
-
* starts no further account
|
|
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 "${
|
|
2005
|
-
`The Claude account "${
|
|
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 "${
|
|
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"}.
|
|
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
|
-
|
|
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
|
|
8864
|
-
if (!Number.isFinite(totalCostUsd) || totalCostUsd <= 0) return 0;
|
|
8865
|
-
if (!cumulative
|
|
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
|
|
8875
|
-
return totalCostUsd
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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`
|
|
17630
|
-
* `"
|
|
17631
|
-
*
|
|
17632
|
-
*
|
|
17633
|
-
*
|
|
17634
|
-
*
|
|
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,
|
|
17641
|
-
* the
|
|
17642
|
-
*
|
|
17643
|
-
*
|
|
17644
|
-
* starts no further account
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 "${
|
|
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 (
|
|
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
|
-
|
|
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,
|