@khalilgharbaoui/opencode-claude-code-plugin 0.54.1 → 0.55.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.d.ts +38 -10
- package/dist/index.js +442 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +64 -6
package/dist/index.js
CHANGED
|
@@ -1623,6 +1623,9 @@ function resolveAccountOverride(source, now = Date.now()) {
|
|
|
1623
1623
|
}
|
|
1624
1624
|
return entry.target;
|
|
1625
1625
|
}
|
|
1626
|
+
function clearAccountOverride(source) {
|
|
1627
|
+
accountOverrides.delete(normalizeAccountName(source || DEFAULT_ACCOUNT));
|
|
1628
|
+
}
|
|
1626
1629
|
function stripAccountSuffix(modelId) {
|
|
1627
1630
|
const at = modelId.indexOf("@");
|
|
1628
1631
|
return at === -1 ? modelId : modelId.slice(0, at);
|
|
@@ -1671,6 +1674,15 @@ function failoverCandidates(accounts, source, groups) {
|
|
|
1671
1674
|
}
|
|
1672
1675
|
return out;
|
|
1673
1676
|
}
|
|
1677
|
+
function isAccountKnownUnavailable(account, now = Date.now()) {
|
|
1678
|
+
if (resolveAccountOverride(account, now) !== void 0) return true;
|
|
1679
|
+
return recallAccountLimit(account, now).resetsAt !== void 0;
|
|
1680
|
+
}
|
|
1681
|
+
function nextAutoFailoverAccount(candidates, tried, now = Date.now()) {
|
|
1682
|
+
return candidates.find(
|
|
1683
|
+
(account) => !tried.has(account) && !isAccountKnownUnavailable(account, now)
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1674
1686
|
var pendingQuestions2 = /* @__PURE__ */ new Map();
|
|
1675
1687
|
function pendingKey2(sessionKey2, toolCallId) {
|
|
1676
1688
|
return `${sessionKey2}${KEY_SEPARATOR2}${toolCallId}`;
|
|
@@ -1797,6 +1809,13 @@ function formatFailoverNote(input) {
|
|
|
1797
1809
|
${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.
|
|
1798
1810
|
`;
|
|
1799
1811
|
}
|
|
1812
|
+
function formatAutoFailoverNote(input) {
|
|
1813
|
+
const resets = describeReset(input.resetsAt);
|
|
1814
|
+
const arrival = input.carried ? "The conversation continues where it stopped." : "The conversation is replayed into a fresh Claude session there.";
|
|
1815
|
+
return `
|
|
1816
|
+
${FAILOVER_MARKER} "${input.failedAccount}" ${input.reason ?? "is out of usage"}, so this turn moved to "${input.target}" automatically and stays there until ${resets ?? "opencode restarts"}. ${arrival}
|
|
1817
|
+
`;
|
|
1818
|
+
}
|
|
1800
1819
|
function formatFailoverStopNote(reason) {
|
|
1801
1820
|
return `
|
|
1802
1821
|
${FAILOVER_MARKER} Staying on this account (${reason}). The turn ends here; the usage limit is unchanged.
|
|
@@ -3564,6 +3583,10 @@ function cliHygieneEnv(inherited = process.env) {
|
|
|
3564
3583
|
}
|
|
3565
3584
|
return filled;
|
|
3566
3585
|
}
|
|
3586
|
+
var SPAWN_MCP_TIMEOUT_MS = 12e4;
|
|
3587
|
+
function spawnMcpTimeoutEnv(inherited = process.env) {
|
|
3588
|
+
return inherited.MCP_TIMEOUT === void 0 ? { MCP_TIMEOUT: String(SPAWN_MCP_TIMEOUT_MS) } : {};
|
|
3589
|
+
}
|
|
3567
3590
|
function detectCliVersion(cliPath) {
|
|
3568
3591
|
const cached = cache.get(cliPath);
|
|
3569
3592
|
if (cached) return cached;
|
|
@@ -6387,6 +6410,79 @@ function tailIsReplyOnly(history, from) {
|
|
|
6387
6410
|
return true;
|
|
6388
6411
|
}
|
|
6389
6412
|
|
|
6413
|
+
// src/conversation-gap.ts
|
|
6414
|
+
var served = /* @__PURE__ */ new Map();
|
|
6415
|
+
var servedSeq = 0;
|
|
6416
|
+
var MAX_SERVED_CONVERSATIONS = 64;
|
|
6417
|
+
function recordServedConversation(sessionKey2, prompt) {
|
|
6418
|
+
const chain = conversationDigests(prompt, { assistantContent: false });
|
|
6419
|
+
if (chain.length === 0) return;
|
|
6420
|
+
served.delete(sessionKey2);
|
|
6421
|
+
served.set(sessionKey2, { chain, seq: ++servedSeq });
|
|
6422
|
+
while (served.size > MAX_SERVED_CONVERSATIONS) {
|
|
6423
|
+
const oldest = served.keys().next();
|
|
6424
|
+
if (oldest.done) break;
|
|
6425
|
+
served.delete(oldest.value);
|
|
6426
|
+
}
|
|
6427
|
+
}
|
|
6428
|
+
function forgetServedConversation(sessionKey2) {
|
|
6429
|
+
served.delete(sessionKey2);
|
|
6430
|
+
}
|
|
6431
|
+
function moveServedConversation(fromKey, toKey) {
|
|
6432
|
+
const record = served.get(fromKey);
|
|
6433
|
+
if (!record) return;
|
|
6434
|
+
served.delete(fromKey);
|
|
6435
|
+
served.delete(toKey);
|
|
6436
|
+
served.set(toKey, { ...record, seq: ++servedSeq });
|
|
6437
|
+
}
|
|
6438
|
+
function findMissedMessages(sessionKey2, prompt) {
|
|
6439
|
+
const record = served.get(sessionKey2);
|
|
6440
|
+
if (!record) return null;
|
|
6441
|
+
const messages = prompt;
|
|
6442
|
+
let lastAssistant = -1;
|
|
6443
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
6444
|
+
if (messages[i].role === "assistant") {
|
|
6445
|
+
lastAssistant = i;
|
|
6446
|
+
break;
|
|
6447
|
+
}
|
|
6448
|
+
}
|
|
6449
|
+
if (lastAssistant < 0) return null;
|
|
6450
|
+
const history = [];
|
|
6451
|
+
for (let i = 0; i <= lastAssistant; i++) {
|
|
6452
|
+
const msg = messages[i];
|
|
6453
|
+
if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "tool") continue;
|
|
6454
|
+
const [entry] = conversationDigests([msg], { assistantContent: false });
|
|
6455
|
+
history.push({ index: i, role: entry.role, digest: entry.digest });
|
|
6456
|
+
}
|
|
6457
|
+
const recorded = record.chain;
|
|
6458
|
+
if (recorded.length === 0 || recorded.length > history.length) return null;
|
|
6459
|
+
for (let i = 0; i < recorded.length; i++) {
|
|
6460
|
+
if (recorded[i].role !== history[i].role) return null;
|
|
6461
|
+
if (recorded[i].digest !== history[i].digest) return null;
|
|
6462
|
+
}
|
|
6463
|
+
let firstUser = -1;
|
|
6464
|
+
for (let i = recorded.length; i < history.length; i++) {
|
|
6465
|
+
if (history[i].role === "user") {
|
|
6466
|
+
firstUser = i;
|
|
6467
|
+
break;
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
6470
|
+
if (firstUser < 0) return null;
|
|
6471
|
+
const from = history[firstUser].index;
|
|
6472
|
+
const missed = messages.slice(from, lastAssistant + 1);
|
|
6473
|
+
let userMessages = 0;
|
|
6474
|
+
for (let i = firstUser; i < history.length; i++) {
|
|
6475
|
+
if (history[i].role === "user") userMessages++;
|
|
6476
|
+
}
|
|
6477
|
+
log.debug("this claude session missed part of the conversation", {
|
|
6478
|
+
sessionKey: sessionKey2,
|
|
6479
|
+
recorded: recorded.length,
|
|
6480
|
+
missed: missed.length,
|
|
6481
|
+
userMessages
|
|
6482
|
+
});
|
|
6483
|
+
return { messages: missed, userMessages };
|
|
6484
|
+
}
|
|
6485
|
+
|
|
6390
6486
|
// src/session-resume-store.ts
|
|
6391
6487
|
import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
6392
6488
|
import { homedir as homedir6 } from "os";
|
|
@@ -7014,7 +7110,10 @@ function claudeSpawnEnv(opts) {
|
|
|
7014
7110
|
// Pin the child to the binary whose version we detected, and keep it off
|
|
7015
7111
|
// non-essential network calls. Fills gaps only, so an explicit shell value
|
|
7016
7112
|
// survives: see `cliHygieneEnv` for why the version has to hold still.
|
|
7017
|
-
...cliHygieneEnv()
|
|
7113
|
+
...cliHygieneEnv(),
|
|
7114
|
+
// 120 s for MCP servers to start, so a subagent outlasts an opencode
|
|
7115
|
+
// stall instead of running without the proxy: see `spawnMcpTimeoutEnv`.
|
|
7116
|
+
...spawnMcpTimeoutEnv()
|
|
7018
7117
|
};
|
|
7019
7118
|
if (opts?.effort) {
|
|
7020
7119
|
env.CLAUDE_CODE_EFFORT_LEVEL = cliEffortLevel(opts.effort);
|
|
@@ -7314,6 +7413,7 @@ function deleteClaudeSessionId(key) {
|
|
|
7314
7413
|
if (claudeSessionId) clearLedger(claudeSessionId);
|
|
7315
7414
|
claudeSessions.delete(key);
|
|
7316
7415
|
forgetForkFingerprint(key);
|
|
7416
|
+
forgetServedConversation(key);
|
|
7317
7417
|
forgetResumePoint(key);
|
|
7318
7418
|
}
|
|
7319
7419
|
function transferClaudeSession(fromKey, toKey) {
|
|
@@ -7325,6 +7425,7 @@ function transferClaudeSession(fromKey, toKey) {
|
|
|
7325
7425
|
setClaudeSessionId(toKey, claudeSessionId);
|
|
7326
7426
|
moveResumePoint(fromKey, toKey);
|
|
7327
7427
|
moveForkFingerprint(fromKey, toKey);
|
|
7428
|
+
moveServedConversation(fromKey, toKey);
|
|
7328
7429
|
clearExitPlanModeQuestions(fromKey);
|
|
7329
7430
|
clearAccountFailoverQuestions(fromKey);
|
|
7330
7431
|
clearSubagentDispatchQuestions(fromKey);
|
|
@@ -7968,12 +8069,14 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
7968
8069
|
"agent prompt cache ttl refused: unknown value",
|
|
7969
8070
|
"auto-continuation stopped",
|
|
7970
8071
|
"auto-continuing incomplete claude result",
|
|
8072
|
+
"automatic account switch skipped an account whose runtime could not be prepared",
|
|
7971
8073
|
"btw: a turn is running but no claude process appeared for it",
|
|
7972
8074
|
"btw: early aside failed; the message will ask again",
|
|
7973
8075
|
"cannot interrupt this transport; waiting for the turn to end",
|
|
7974
8076
|
"carried this conversation's claude transcript to the other account",
|
|
7975
8077
|
"carrying the claude conversation over to the new session key",
|
|
7976
8078
|
"claude --version output unparseable",
|
|
8079
|
+
"claude account cannot serve this turn; continuing it on the next account automatically",
|
|
7977
8080
|
"claude account is out of usage; the turn ends with a note",
|
|
7978
8081
|
"claude already registers a different skill under this name; not bridging ours",
|
|
7979
8082
|
"claude cli < 2.1.142 detected; Opus 4.7 thinking summaries unavailable. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
@@ -8047,6 +8150,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
8047
8150
|
"interactiveBypass is ignored: skipPermissions governs the interactive transport too",
|
|
8048
8151
|
"malformed data URI, skipping file part",
|
|
8049
8152
|
"no active process to respawn (start watchdog); ending turn",
|
|
8153
|
+
"no configured account could take this turn; it ends here",
|
|
8050
8154
|
"no proxyTools entry was recognised; nothing will be proxied this turn",
|
|
8051
8155
|
"no stdout after envelope write; respawning claude process to resume conversation",
|
|
8052
8156
|
"opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction",
|
|
@@ -8076,6 +8180,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
8076
8180
|
"remote URL images are not supported by Claude CLI, skipping",
|
|
8077
8181
|
"replaying stdout the child emitted between turns",
|
|
8078
8182
|
"replaying the conversation as text: no claude session was available to continue",
|
|
8183
|
+
"replaying the messages this claude session missed: the conversation continued under another session key",
|
|
8079
8184
|
"replaying this conversation as text on the other account",
|
|
8080
8185
|
"replaying this conversation rather than carrying it across accounts",
|
|
8081
8186
|
"result fallback timer fired \u2014 closing stream without result event",
|
|
@@ -8527,8 +8632,8 @@ function redactLogLine(raw, context) {
|
|
|
8527
8632
|
if (!parsed.data || Object.keys(parsed.data).length === 0) return head;
|
|
8528
8633
|
return `${head} ${JSON.stringify(sanitizeLogData(parsed.data, context))}`;
|
|
8529
8634
|
}
|
|
8530
|
-
function readLogTail(
|
|
8531
|
-
const fd = openSync(
|
|
8635
|
+
function readLogTail(path14, maxBytes = BUNDLE_TAIL_BYTES) {
|
|
8636
|
+
const fd = openSync(path14, "r");
|
|
8532
8637
|
try {
|
|
8533
8638
|
const size = fstatSync(fd).size;
|
|
8534
8639
|
const length = Math.min(size, maxBytes);
|
|
@@ -9830,8 +9935,10 @@ function clipWithMarker(text, max) {
|
|
|
9830
9935
|
\u2026[truncated ${text.length - max} chars]`;
|
|
9831
9936
|
}
|
|
9832
9937
|
var MAX_REPLAY_MESSAGE_CHARS = 2e3;
|
|
9833
|
-
var MAX_REPLAY_LAST_REPLY_CHARS = 12e3;
|
|
9834
9938
|
var MAX_REPLAY_HEAD_CHARS = 1e3;
|
|
9939
|
+
var MAX_REPLAY_RECENT_MESSAGES = 6;
|
|
9940
|
+
var MAX_REPLAY_RECENT_MESSAGE_CHARS = 12e3;
|
|
9941
|
+
var MAX_REPLAY_TOTAL_CHARS = 18e4;
|
|
9835
9942
|
function clipMiddle(text, max) {
|
|
9836
9943
|
if (text.length <= max) return text;
|
|
9837
9944
|
const head = Math.min(Math.floor(max / 4), MAX_REPLAY_HEAD_CHARS);
|
|
@@ -9895,39 +10002,58 @@ ${clipWithMarker(
|
|
|
9895
10002
|
}
|
|
9896
10003
|
return { text: lines.join("\n"), toolResultCount };
|
|
9897
10004
|
}
|
|
10005
|
+
function isConversationMessage(msg) {
|
|
10006
|
+
return msg.role === "user" || msg.role === "assistant" || msg.role === "tool";
|
|
10007
|
+
}
|
|
9898
10008
|
function compactConversationHistory(prompt, opts = {}) {
|
|
9899
10009
|
const mode = opts.mode ?? "fresh-session";
|
|
9900
10010
|
prompt = filterSideQuestionHistory(prompt);
|
|
9901
10011
|
if (mode === "compaction") {
|
|
9902
10012
|
return buildCompactionHistory(prompt);
|
|
9903
10013
|
}
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
10014
|
+
if (opts.messages) {
|
|
10015
|
+
const run = filterSideQuestionHistory(opts.messages).filter(isConversationMessage);
|
|
10016
|
+
return run.length === 0 ? null : renderReplay(run);
|
|
10017
|
+
}
|
|
10018
|
+
const conversationMessages = prompt.filter(isConversationMessage);
|
|
9907
10019
|
if (conversationMessages.length <= 1) {
|
|
9908
10020
|
return null;
|
|
9909
10021
|
}
|
|
9910
|
-
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
const msg = conversationMessages[i];
|
|
10022
|
+
return renderReplay(conversationMessages.slice(0, -1));
|
|
10023
|
+
}
|
|
10024
|
+
function renderReplay(messages) {
|
|
10025
|
+
const entries = [];
|
|
10026
|
+
let total = 0;
|
|
10027
|
+
let rank = 0;
|
|
10028
|
+
let omitted = 0;
|
|
10029
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
10030
|
+
const msg = messages[i];
|
|
9920
10031
|
const role = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "Tool";
|
|
9921
10032
|
const { text } = renderMessageContentForCompaction(msg);
|
|
9922
|
-
if (text.trim())
|
|
9923
|
-
|
|
9924
|
-
|
|
10033
|
+
if (!text.trim()) continue;
|
|
10034
|
+
const max = rank < MAX_REPLAY_RECENT_MESSAGES ? MAX_REPLAY_RECENT_MESSAGE_CHARS : MAX_REPLAY_MESSAGE_CHARS;
|
|
10035
|
+
rank++;
|
|
10036
|
+
const entry = `${role}: ${clipMiddle(text, max)}`;
|
|
10037
|
+
if (total + entry.length > MAX_REPLAY_TOTAL_CHARS) {
|
|
10038
|
+
omitted = i + 1;
|
|
10039
|
+
break;
|
|
9925
10040
|
}
|
|
10041
|
+
entries.push(entry);
|
|
10042
|
+
total += entry.length + 2;
|
|
9926
10043
|
}
|
|
9927
|
-
if (
|
|
9928
|
-
|
|
10044
|
+
if (entries.length === 0) return null;
|
|
10045
|
+
entries.reverse();
|
|
10046
|
+
if (omitted > 0) {
|
|
10047
|
+
log.info("replay bounded: the oldest messages were dropped", {
|
|
10048
|
+
omitted,
|
|
10049
|
+
kept: entries.length,
|
|
10050
|
+
chars: total
|
|
10051
|
+
});
|
|
10052
|
+
entries.unshift(
|
|
10053
|
+
`[${omitted} earlier message${omitted === 1 ? "" : "s"} omitted: the replay is capped at ${MAX_REPLAY_TOTAL_CHARS} characters]`
|
|
10054
|
+
);
|
|
9929
10055
|
}
|
|
9930
|
-
return
|
|
10056
|
+
return entries.join("\n\n");
|
|
9931
10057
|
}
|
|
9932
10058
|
function buildCompactionHistory(prompt) {
|
|
9933
10059
|
const entries = [];
|
|
@@ -10031,6 +10157,27 @@ ${historyContext}
|
|
|
10031
10157
|
|
|
10032
10158
|
Now continuing with the current message:
|
|
10033
10159
|
|
|
10160
|
+
`
|
|
10161
|
+
});
|
|
10162
|
+
}
|
|
10163
|
+
} else if (opts.missed && opts.missed.length > 0) {
|
|
10164
|
+
const missed = compactConversationHistory(prompt, { messages: opts.missed });
|
|
10165
|
+
if (missed) {
|
|
10166
|
+
log.info("including the messages this claude session missed", {
|
|
10167
|
+
messages: opts.missed.length,
|
|
10168
|
+
chars: missed.length
|
|
10169
|
+
});
|
|
10170
|
+
content.push({
|
|
10171
|
+
type: "text",
|
|
10172
|
+
text: `<missed_conversation>
|
|
10173
|
+
The conversation continued under another opencode agent or model while this session was not asked. These are the messages exchanged since your last reply, replayed as text. They are part of this conversation: decisions and plans made there stand, and work done there is done.
|
|
10174
|
+
|
|
10175
|
+
${missed}
|
|
10176
|
+
|
|
10177
|
+
</missed_conversation>
|
|
10178
|
+
|
|
10179
|
+
Now continuing with the current message:
|
|
10180
|
+
|
|
10034
10181
|
`
|
|
10035
10182
|
});
|
|
10036
10183
|
}
|
|
@@ -10668,6 +10815,7 @@ function interactiveSpawnEnv(opts) {
|
|
|
10668
10815
|
// detected version the flag gates read, and skip non-essential traffic.
|
|
10669
10816
|
// Fills gaps only, so a var the user exported survives untouched.
|
|
10670
10817
|
...cliHygieneEnv(),
|
|
10818
|
+
...spawnMcpTimeoutEnv(),
|
|
10671
10819
|
...opts.ignoreAnthropicApiKey ? { ANTHROPIC_API_KEY: void 0, ANTHROPIC_AUTH_TOKEN: void 0 } : {},
|
|
10672
10820
|
...opts.effort ? { CLAUDE_CODE_EFFORT_LEVEL: opts.effort } : {}
|
|
10673
10821
|
};
|
|
@@ -12305,9 +12453,9 @@ import { readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "
|
|
|
12305
12453
|
import { homedir as homedir11 } from "os";
|
|
12306
12454
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
12307
12455
|
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join15 } from "path";
|
|
12308
|
-
function readPromptFileIfPresent(
|
|
12456
|
+
function readPromptFileIfPresent(path14) {
|
|
12309
12457
|
try {
|
|
12310
|
-
const content = readFileSync9(
|
|
12458
|
+
const content = readFileSync9(path14, "utf8").trim();
|
|
12311
12459
|
return content || void 0;
|
|
12312
12460
|
} catch {
|
|
12313
12461
|
return void 0;
|
|
@@ -12425,9 +12573,9 @@ function extractSystemMessages(prompt) {
|
|
|
12425
12573
|
}
|
|
12426
12574
|
var CLAUDE_NATIVE_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md"]);
|
|
12427
12575
|
function userAuthoredInstructions(forwarded, opts = {}) {
|
|
12428
|
-
const readFile3 = opts.readFile ?? ((
|
|
12576
|
+
const readFile3 = opts.readFile ?? ((path14) => {
|
|
12429
12577
|
try {
|
|
12430
|
-
return readFileSync9(
|
|
12578
|
+
return readFileSync9(path14, "utf8");
|
|
12431
12579
|
} catch {
|
|
12432
12580
|
return null;
|
|
12433
12581
|
}
|
|
@@ -12439,16 +12587,16 @@ function userAuthoredInstructions(forwarded, opts = {}) {
|
|
|
12439
12587
|
}
|
|
12440
12588
|
const seen = /* @__PURE__ */ new Set();
|
|
12441
12589
|
for (const match of forwarded.matchAll(/^Instructions from: (.+)$/gm)) {
|
|
12442
|
-
const
|
|
12443
|
-
if (seen.has(
|
|
12444
|
-
if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(
|
|
12445
|
-
seen.add(
|
|
12446
|
-
const file = readFile3(
|
|
12590
|
+
const path14 = match[1].trim();
|
|
12591
|
+
if (seen.has(path14) || !isAbsolute3(path14)) continue;
|
|
12592
|
+
if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(path14))) continue;
|
|
12593
|
+
seen.add(path14);
|
|
12594
|
+
const file = readFile3(path14);
|
|
12447
12595
|
if (!file?.trim()) continue;
|
|
12448
12596
|
const start = match.index + match[0].length + 1;
|
|
12449
12597
|
const content = forwarded.startsWith(file, start) ? file : forwarded.startsWith(file.trimEnd(), start) ? file.trimEnd() : null;
|
|
12450
12598
|
if (content === null) continue;
|
|
12451
|
-
out.push({ text: `Instructions from: ${
|
|
12599
|
+
out.push({ text: `Instructions from: ${path14}
|
|
12452
12600
|
${content}`, content });
|
|
12453
12601
|
}
|
|
12454
12602
|
return out;
|
|
@@ -12485,10 +12633,10 @@ ${options.compressionSummary.trim()}`
|
|
|
12485
12633
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
12486
12634
|
const content = parts.join("\n\n");
|
|
12487
12635
|
if (!content) return void 0;
|
|
12488
|
-
const
|
|
12636
|
+
const path14 = join15(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
|
|
12489
12637
|
try {
|
|
12490
|
-
writeFileSync8(
|
|
12491
|
-
return
|
|
12638
|
+
writeFileSync8(path14, content, { encoding: "utf8", mode: 384 });
|
|
12639
|
+
return path14;
|
|
12492
12640
|
} catch (err) {
|
|
12493
12641
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
12494
12642
|
return void 0;
|
|
@@ -14506,6 +14654,7 @@ ${plan}
|
|
|
14506
14654
|
resultText: msg.message.content.filter((b) => b.type === "text" && b.text).map((b) => String(b.text)).join("\n")
|
|
14507
14655
|
}));
|
|
14508
14656
|
const limitErrorReply = limitFromReply && ctx.usageLimitNoteActive;
|
|
14657
|
+
const autoFailoverReply = (limitFromReply || accountBlockKind(msg) !== null) && ctx.autoFailoverTarget() !== void 0;
|
|
14509
14658
|
const compactionApiError = ctx.compactionMode && (apiErrorReply || typeof msg.error === "string");
|
|
14510
14659
|
if (limitFromReply) {
|
|
14511
14660
|
state.accountLimitHit ??= recallAccountLimit(ctx.sourceAccount);
|
|
@@ -14527,7 +14676,7 @@ ${plan}
|
|
|
14527
14676
|
}
|
|
14528
14677
|
for (const block2 of msg.message.content) {
|
|
14529
14678
|
if (block2.type === "text" && block2.text) {
|
|
14530
|
-
if (limitErrorReply) continue;
|
|
14679
|
+
if (limitErrorReply || autoFailoverReply) continue;
|
|
14531
14680
|
if (compactionApiError) {
|
|
14532
14681
|
state.compactionFailureText ??= block2.text;
|
|
14533
14682
|
continue;
|
|
@@ -14755,9 +14904,11 @@ ${plan}
|
|
|
14755
14904
|
})) {
|
|
14756
14905
|
state.accountLimitHit = recallAccountLimit(ctx.sourceAccount);
|
|
14757
14906
|
}
|
|
14907
|
+
const autoFailoverResult = msg.is_error === true && (!!state.accountLimitHit || !!state.accountBlock) && ctx.autoFailoverTarget() !== void 0;
|
|
14758
14908
|
state.usageLimitNote = ctx.usageLimitNoteActive && !!state.accountLimitHit && // An expired login or a billing hold writes its own note just below
|
|
14759
14909
|
// and says what to run; two notes about one failure is one too many.
|
|
14760
|
-
!state.accountBlock && msg.is_error === true
|
|
14910
|
+
!state.accountBlock && msg.is_error === true && // Moved to another account: that account's answer is this turn's.
|
|
14911
|
+
!autoFailoverResult;
|
|
14761
14912
|
const compactionFailure = ctx.compactionMode && (msg.is_error === true || typeof msg.subtype === "string" && msg.subtype !== "success");
|
|
14762
14913
|
if (compactionFailure) {
|
|
14763
14914
|
if (typeof msg.result === "string" && msg.result.trim()) {
|
|
@@ -14766,7 +14917,7 @@ ${plan}
|
|
|
14766
14917
|
state.resultFailure ??= state.accountBlock ?? (state.accountLimitHit ? "usage_limit" : void 0) ?? (typeof msg.subtype === "string" && msg.subtype !== "success" ? msg.subtype : "error");
|
|
14767
14918
|
}
|
|
14768
14919
|
if (!state.currentTextId && msg.is_error && !compactionFailure && // Already on screen from the reply frame, word for word.
|
|
14769
|
-
!(typeof msg.result === "string" && state.apiErrorTextShown !== null && msg.result.trim() === state.apiErrorTextShown.trim()) && !state.usageLimitNote && typeof msg.result === "string" && msg.result.trim().length > 0) {
|
|
14920
|
+
!(typeof msg.result === "string" && state.apiErrorTextShown !== null && msg.result.trim() === state.apiErrorTextShown.trim()) && !state.usageLimitNote && !autoFailoverResult && typeof msg.result === "string" && msg.result.trim().length > 0) {
|
|
14770
14921
|
const errId = state.startTextBlock();
|
|
14771
14922
|
state.controller.enqueue({
|
|
14772
14923
|
type: "text-delta",
|
|
@@ -14780,14 +14931,14 @@ ${plan}
|
|
|
14780
14931
|
if (state.accountBlock && msg.is_error) {
|
|
14781
14932
|
state.resultFailure ??= state.accountBlock;
|
|
14782
14933
|
const offeringSwitch = ctx.failoverAskActive;
|
|
14783
|
-
if (!compactionFailure) {
|
|
14934
|
+
if (!compactionFailure && !autoFailoverResult) {
|
|
14784
14935
|
state.controller.enqueue({
|
|
14785
14936
|
type: "text-delta",
|
|
14786
14937
|
id: state.startTextBlock(),
|
|
14787
14938
|
delta: formatAccountBlockNote({
|
|
14788
14939
|
kind: state.accountBlock,
|
|
14789
14940
|
account: ctx.sourceAccount,
|
|
14790
|
-
configDir: ctx.
|
|
14941
|
+
configDir: ctx.accountConfigDir,
|
|
14791
14942
|
offeringSwitch,
|
|
14792
14943
|
// Read only when the form is not taking the turn, which is the
|
|
14793
14944
|
// default: moving the work by hand is then the operator's only
|
|
@@ -14806,7 +14957,7 @@ ${plan}
|
|
|
14806
14957
|
const failure = describeResultFailure(msg);
|
|
14807
14958
|
if (failure) {
|
|
14808
14959
|
state.resultFailure = msg.subtype;
|
|
14809
|
-
if (!compactionFailure) {
|
|
14960
|
+
if (!compactionFailure && !autoFailoverResult) {
|
|
14810
14961
|
state.controller.enqueue({
|
|
14811
14962
|
type: "text-delta",
|
|
14812
14963
|
id: state.startTextBlock(),
|
|
@@ -15304,12 +15455,93 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15304
15455
|
* because which method opencode used is not in them.
|
|
15305
15456
|
*/
|
|
15306
15457
|
async streamTurn(options, mode) {
|
|
15307
|
-
const result = await this.
|
|
15458
|
+
const result = await this.runAccountFailover(options, mode);
|
|
15308
15459
|
return {
|
|
15309
15460
|
...result,
|
|
15310
15461
|
stream: translateStreamForHost(result.stream, this.config.hostApi ?? "v1")
|
|
15311
15462
|
};
|
|
15312
15463
|
}
|
|
15464
|
+
/**
|
|
15465
|
+
* Run the turn, moving the same request to the next eligible configured
|
|
15466
|
+
* account when the one it ran on is out of usage or cannot serve
|
|
15467
|
+
* (`accountFailover: "auto"`). The single wiring point for the automatic
|
|
15468
|
+
* switch, and outside `runModelChain` on purpose: an account switch starts
|
|
15469
|
+
* that account's own model chain from the top.
|
|
15470
|
+
*
|
|
15471
|
+
* Anything but `"auto"` is `runModelChain` and one `await`, so `"off"` and
|
|
15472
|
+
* `"ask"` are exactly what they were. With `"auto"`, the attempt that failed
|
|
15473
|
+
* decides the switch itself (`completeResult` sets `run.switchTo` and closes
|
|
15474
|
+
* its stream without a `finish`), and the next attempt's prologue makes the
|
|
15475
|
+
* switch the form's pick would have made. This only splices the attempts
|
|
15476
|
+
* into one caller turn: one `stream-start`, and the `finish` of whichever
|
|
15477
|
+
* attempt did not switch.
|
|
15478
|
+
*
|
|
15479
|
+
* The bound is `run.tried`, as it is `tried` for the model chain: every
|
|
15480
|
+
* account spawned or switched to goes in, and none in it is picked again.
|
|
15481
|
+
*
|
|
15482
|
+
* Cancelling the returned stream is a stop, for every attempt: this loop is
|
|
15483
|
+
* the plugin starting more work on the caller's behalf, so a caller that
|
|
15484
|
+
* has gone away must reach all of it. Each attempt gets a signal that the
|
|
15485
|
+
* caller's own `abortSignal` and a `cancel()` both abort, and the loop
|
|
15486
|
+
* starts no further account once either has happened.
|
|
15487
|
+
*/
|
|
15488
|
+
async runAccountFailover(options, mode) {
|
|
15489
|
+
if (this.config.accountFailover !== "auto") return this.runModelChain(options, mode);
|
|
15490
|
+
const self = this;
|
|
15491
|
+
const run = { tried: /* @__PURE__ */ new Set() };
|
|
15492
|
+
const cancelled2 = new AbortController();
|
|
15493
|
+
const attemptOptions = {
|
|
15494
|
+
...options,
|
|
15495
|
+
abortSignal: options.abortSignal ? AbortSignal.any([options.abortSignal, cancelled2.signal]) : cancelled2.signal
|
|
15496
|
+
};
|
|
15497
|
+
const stopped = () => attemptOptions.abortSignal.aborted;
|
|
15498
|
+
let inner = await this.runModelChain(attemptOptions, mode, run);
|
|
15499
|
+
const request = inner.request;
|
|
15500
|
+
let reader;
|
|
15501
|
+
const stream = new ReadableStream({
|
|
15502
|
+
async start(controller) {
|
|
15503
|
+
let started2 = false;
|
|
15504
|
+
try {
|
|
15505
|
+
for (; ; ) {
|
|
15506
|
+
reader = inner.stream.getReader();
|
|
15507
|
+
try {
|
|
15508
|
+
for (; ; ) {
|
|
15509
|
+
const { done, value } = await reader.read();
|
|
15510
|
+
if (done) break;
|
|
15511
|
+
if (value.type === "stream-start") {
|
|
15512
|
+
if (started2) continue;
|
|
15513
|
+
started2 = true;
|
|
15514
|
+
}
|
|
15515
|
+
controller.enqueue(value);
|
|
15516
|
+
}
|
|
15517
|
+
} finally {
|
|
15518
|
+
reader.releaseLock();
|
|
15519
|
+
}
|
|
15520
|
+
if (!run.switchTo) break;
|
|
15521
|
+
if (stopped()) {
|
|
15522
|
+
log.info("turn stopped before the automatic account switch; not switching", {
|
|
15523
|
+
target: run.switchTo.target
|
|
15524
|
+
});
|
|
15525
|
+
break;
|
|
15526
|
+
}
|
|
15527
|
+
inner = await self.runModelChain(attemptOptions, mode, run);
|
|
15528
|
+
}
|
|
15529
|
+
} catch (error) {
|
|
15530
|
+
if (!stopped()) controller.enqueue({ type: "error", error });
|
|
15531
|
+
} finally {
|
|
15532
|
+
try {
|
|
15533
|
+
controller.close();
|
|
15534
|
+
} catch {
|
|
15535
|
+
}
|
|
15536
|
+
}
|
|
15537
|
+
},
|
|
15538
|
+
async cancel(reason) {
|
|
15539
|
+
cancelled2.abort(reason);
|
|
15540
|
+
await reader?.cancel(reason).catch(() => void 0);
|
|
15541
|
+
}
|
|
15542
|
+
});
|
|
15543
|
+
return { stream, request };
|
|
15544
|
+
}
|
|
15313
15545
|
/**
|
|
15314
15546
|
* Run the turn, moving to the next model in the fallback chain if the one
|
|
15315
15547
|
* it started on is refused. The single wiring point for `src/model-fallback.ts`.
|
|
@@ -15328,14 +15560,14 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15328
15560
|
* and an exhausted chain leaves the last attempt un-armed so its error
|
|
15329
15561
|
* surfaces exactly as it does today.
|
|
15330
15562
|
*/
|
|
15331
|
-
async runModelChain(options, mode) {
|
|
15563
|
+
async runModelChain(options, mode, accountRun) {
|
|
15332
15564
|
const agent = this.getOpencodeAgent(options);
|
|
15333
15565
|
const chain = this.isCompactionCall(options) ? [] : resolveFallbackChain(agent, resolveAgentModel(agent, this.modelId));
|
|
15334
|
-
if (chain.length === 0) return this.doStreamForHost(options, void 0, mode);
|
|
15566
|
+
if (chain.length === 0) return this.doStreamForHost(options, void 0, mode, accountRun);
|
|
15335
15567
|
const self = this;
|
|
15336
15568
|
const tried = /* @__PURE__ */ new Set();
|
|
15337
15569
|
let attempt = { armed: true };
|
|
15338
|
-
let inner = await this.doStreamForHost(options, attempt, mode);
|
|
15570
|
+
let inner = await this.doStreamForHost(options, attempt, mode, accountRun);
|
|
15339
15571
|
const request = inner.request;
|
|
15340
15572
|
const stream = new ReadableStream({
|
|
15341
15573
|
async start(controller) {
|
|
@@ -15405,7 +15637,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15405
15637
|
modelOverride: next,
|
|
15406
15638
|
armed: nextFallbackModel(chain, tried) !== void 0
|
|
15407
15639
|
};
|
|
15408
|
-
inner = await self.doStreamForHost(options, attempt, mode);
|
|
15640
|
+
inner = await self.doStreamForHost(options, attempt, mode, accountRun);
|
|
15409
15641
|
}
|
|
15410
15642
|
} catch (error) {
|
|
15411
15643
|
controller.enqueue({ type: "error", error });
|
|
@@ -15419,8 +15651,10 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15419
15651
|
});
|
|
15420
15652
|
return { stream, request };
|
|
15421
15653
|
}
|
|
15422
|
-
async doStreamForHost(options, attempt, mode = "stream") {
|
|
15654
|
+
async doStreamForHost(options, attempt, mode = "stream", accountRun) {
|
|
15423
15655
|
const warnings = [];
|
|
15656
|
+
const autoSwitch = accountRun?.switchTo;
|
|
15657
|
+
if (accountRun) accountRun.switchTo = void 0;
|
|
15424
15658
|
const turnAbort = watchTurnAbort(options.abortSignal);
|
|
15425
15659
|
const abortedBeforeWork = (stage) => {
|
|
15426
15660
|
log.info("abort before the turn asked claude for work; nothing spawned", {
|
|
@@ -15660,12 +15894,17 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15660
15894
|
});
|
|
15661
15895
|
}
|
|
15662
15896
|
const hasPriorConversation = options.prompt.filter((m) => m.role === "user" || m.role === "assistant").length > 1;
|
|
15663
|
-
if (!hasPriorConversation) {
|
|
15897
|
+
if (!hasPriorConversation && !autoSwitch) {
|
|
15664
15898
|
deleteClaudeSessionId(sk);
|
|
15665
15899
|
deleteActiveProcess(sk);
|
|
15666
15900
|
clearCompression(sk);
|
|
15667
15901
|
}
|
|
15668
|
-
const failoverAnswer =
|
|
15902
|
+
const failoverAnswer = autoSwitch ? {
|
|
15903
|
+
kind: "switch",
|
|
15904
|
+
target: autoSwitch.target,
|
|
15905
|
+
sourceAccount: autoSwitch.sourceAccount,
|
|
15906
|
+
resetsAt: autoSwitch.resetsAt
|
|
15907
|
+
} : compactionMode || mode === "generate" ? null : consumeAccountFailoverAnswer(sk, options.prompt, {
|
|
15669
15908
|
sourceAccount,
|
|
15670
15909
|
candidates: failoverCandidates(
|
|
15671
15910
|
this.config.failoverAccounts,
|
|
@@ -15723,13 +15962,84 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15723
15962
|
cliPath: this.config.cliPath,
|
|
15724
15963
|
modelId: effectiveModelId
|
|
15725
15964
|
});
|
|
15965
|
+
while (autoSwitch && accountRun && failover.target !== failoverAnswer.target) {
|
|
15966
|
+
log.notice("automatic account switch skipped an account whose runtime could not be prepared", {
|
|
15967
|
+
sessionKey: sk,
|
|
15968
|
+
target: failoverAnswer.target
|
|
15969
|
+
});
|
|
15970
|
+
const next = nextAutoFailoverAccount(
|
|
15971
|
+
failoverCandidates(this.config.failoverAccounts, sourceAccount, groups),
|
|
15972
|
+
accountRun.tried
|
|
15973
|
+
);
|
|
15974
|
+
if (!next) {
|
|
15975
|
+
clearAccountOverride(failoverAnswer.sourceAccount);
|
|
15976
|
+
log.warn("no configured account could take this turn; it ends here", {
|
|
15977
|
+
sessionKey: sk,
|
|
15978
|
+
account: autoSwitch.failedAccount,
|
|
15979
|
+
cause: autoSwitch.cause
|
|
15980
|
+
});
|
|
15981
|
+
const note = mode === "generate" ? null : autoSwitch.cause === "usage_limit" ? formatUsageLimitNote({
|
|
15982
|
+
sourceAccount: autoSwitch.failedAccount,
|
|
15983
|
+
candidates: [],
|
|
15984
|
+
resetsAt: autoSwitch.resetsAt,
|
|
15985
|
+
window: autoSwitch.window
|
|
15986
|
+
}) : formatAccountBlockNote({
|
|
15987
|
+
kind: autoSwitch.cause,
|
|
15988
|
+
account: autoSwitch.failedAccount,
|
|
15989
|
+
configDir: autoSwitch.failedConfigDir,
|
|
15990
|
+
offeringSwitch: false,
|
|
15991
|
+
candidates: []
|
|
15992
|
+
});
|
|
15993
|
+
const cause = autoSwitch.cause;
|
|
15994
|
+
const exhausted = new ReadableStream({
|
|
15995
|
+
start(controller) {
|
|
15996
|
+
controller.enqueue({ type: "stream-start", warnings });
|
|
15997
|
+
if (note) {
|
|
15998
|
+
const id = generateId();
|
|
15999
|
+
controller.enqueue({ type: "text-start", id });
|
|
16000
|
+
controller.enqueue({ type: "text-delta", id, delta: note });
|
|
16001
|
+
controller.enqueue({ type: "text-end", id });
|
|
16002
|
+
}
|
|
16003
|
+
controller.enqueue({
|
|
16004
|
+
type: "finish",
|
|
16005
|
+
finishReason: { unified: "error", raw: cause },
|
|
16006
|
+
usage: toUsage2({ input_tokens: 0, output_tokens: 0 }),
|
|
16007
|
+
providerMetadata: {
|
|
16008
|
+
"claude-code": {
|
|
16009
|
+
path: "account-failover-exhausted",
|
|
16010
|
+
synthetic: true,
|
|
16011
|
+
usageUnavailable: true
|
|
16012
|
+
}
|
|
16013
|
+
}
|
|
16014
|
+
});
|
|
16015
|
+
controller.close();
|
|
16016
|
+
}
|
|
16017
|
+
});
|
|
16018
|
+
return { stream: exhausted, request: { body: { text: "" } } };
|
|
16019
|
+
}
|
|
16020
|
+
accountRun.tried.add(next);
|
|
16021
|
+
failoverAnswer.target = next;
|
|
16022
|
+
setAccountOverride(
|
|
16023
|
+
failoverAnswer.sourceAccount,
|
|
16024
|
+
next,
|
|
16025
|
+
failoverUntil(failoverAnswer.resetsAt)
|
|
16026
|
+
);
|
|
16027
|
+
failover = await resolveFailoverSpawn({
|
|
16028
|
+
account: sourceAccount,
|
|
16029
|
+
baseCliPath,
|
|
16030
|
+
cliPath: this.config.cliPath,
|
|
16031
|
+
modelId: effectiveModelId
|
|
16032
|
+
});
|
|
16033
|
+
}
|
|
15726
16034
|
cliPath = failover.cliPath;
|
|
15727
16035
|
asideTransportRef.cliPath = cliPath;
|
|
15728
|
-
|
|
15729
|
-
|
|
15730
|
-
|
|
15731
|
-
|
|
15732
|
-
|
|
16036
|
+
if (!autoSwitch) {
|
|
16037
|
+
failoverNote = formatFailoverNote({
|
|
16038
|
+
sourceAccount: failoverAnswer.sourceAccount,
|
|
16039
|
+
target: failoverAnswer.target,
|
|
16040
|
+
resetsAt: failoverAnswer.resetsAt
|
|
16041
|
+
});
|
|
16042
|
+
}
|
|
15733
16043
|
}
|
|
15734
16044
|
const spawnConfigDir = this.config.accountInProcess ? this.skillBridgeSpawn(failover).configDir : void 0;
|
|
15735
16045
|
const processForAccount = getActiveProcess(sk);
|
|
@@ -15793,6 +16103,15 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15793
16103
|
if (failoverAnswer?.kind === "switch" && hasPriorConversation && !conversationOnTarget) {
|
|
15794
16104
|
includeHistoryContext = true;
|
|
15795
16105
|
}
|
|
16106
|
+
if (autoSwitch && failoverAnswer?.kind === "switch" && mode !== "generate") {
|
|
16107
|
+
failoverNote = formatAutoFailoverNote({
|
|
16108
|
+
failedAccount: autoSwitch.failedAccount,
|
|
16109
|
+
target: failoverAnswer.target,
|
|
16110
|
+
resetsAt: autoSwitch.resetsAt,
|
|
16111
|
+
reason: autoSwitch.reason,
|
|
16112
|
+
carried: conversationOnTarget
|
|
16113
|
+
});
|
|
16114
|
+
}
|
|
15796
16115
|
let accountGroupNote = null;
|
|
15797
16116
|
if (!compactionMode && groups && accountProviders.size > 0 && includeHistoryContext && failoverAnswer?.kind !== "switch") {
|
|
15798
16117
|
const foreign = findForeignAccountSibling({
|
|
@@ -15965,6 +16284,14 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15965
16284
|
messages: options.prompt.length
|
|
15966
16285
|
});
|
|
15967
16286
|
}
|
|
16287
|
+
const missed = !compactionMode && !includeHistoryContext && failoverAnswer?.kind !== "switch" && !accountGroupNote && (!!getClaudeSessionId(sk) || !!getActiveProcess(sk)) ? findMissedMessages(sk, filterSideQuestionHistory(options.prompt)) : null;
|
|
16288
|
+
if (missed) {
|
|
16289
|
+
log.notice("replaying the messages this claude session missed: the conversation continued under another session key", {
|
|
16290
|
+
sessionKey: sk,
|
|
16291
|
+
messages: missed.messages.length,
|
|
16292
|
+
userMessages: missed.userMessages
|
|
16293
|
+
});
|
|
16294
|
+
}
|
|
15968
16295
|
if (this.config.forkSessions === true && !compactionMode) {
|
|
15969
16296
|
recordForkFingerprint(sk, options.prompt, cliPath);
|
|
15970
16297
|
}
|
|
@@ -15985,7 +16312,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15985
16312
|
compactionMode,
|
|
15986
16313
|
compactionInstructions: useInteractive && compactionMode ? extractSystemMessages(effectivePrompt).join("\n\n") : void 0,
|
|
15987
16314
|
cliToolCallIds: new Set(previousPendingProxyCalls.map((c) => c.toolCallId)),
|
|
15988
|
-
stripContextReminders: this.stripContextRemindersEnabled()
|
|
16315
|
+
stripContextReminders: this.stripContextRemindersEnabled(),
|
|
16316
|
+
missed: missed?.messages
|
|
15989
16317
|
});
|
|
15990
16318
|
const ptyPlanMode = useInteractive && !compactionMode && this.config.permissionMode === "plan";
|
|
15991
16319
|
const resolvedProxy = compactionMode ? null : ptyPlanMode ? this.resolvedProxyTools()?.filter((def) => def.name !== "write" && def.name !== "edit") ?? null : this.resolvedProxyTools();
|
|
@@ -16050,6 +16378,16 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16050
16378
|
// the operator is usually not even looking at.
|
|
16051
16379
|
childSession: !!await fetchSessionParentId(affinity)
|
|
16052
16380
|
});
|
|
16381
|
+
const spawnedAccount = failover.target ?? sourceAccount;
|
|
16382
|
+
accountRun?.tried.add(spawnedAccount);
|
|
16383
|
+
let autoFailoverChoice;
|
|
16384
|
+
const autoFailoverTarget = () => {
|
|
16385
|
+
if (autoFailoverChoice === void 0) {
|
|
16386
|
+
autoFailoverChoice = accountRun && !compactionMode ? nextAutoFailoverAccount(failoverAccounts, accountRun.tried) ?? null : null;
|
|
16387
|
+
}
|
|
16388
|
+
return autoFailoverChoice ?? void 0;
|
|
16389
|
+
};
|
|
16390
|
+
const limitAccount = accountRun ? spawnedAccount : sourceAccount;
|
|
16053
16391
|
const usageLimitNoteActive = !compactionMode && mode !== "generate" && !failoverAskActive;
|
|
16054
16392
|
let staleBuild = null;
|
|
16055
16393
|
if (!compactionMode && mode !== "generate" && affinity !== "default") {
|
|
@@ -16690,6 +17028,9 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16690
17028
|
account: failover.target ?? sourceAccount
|
|
16691
17029
|
});
|
|
16692
17030
|
}
|
|
17031
|
+
if (!compactionMode) {
|
|
17032
|
+
recordServedConversation(sk, filterSideQuestionHistory(options.prompt));
|
|
17033
|
+
}
|
|
16693
17034
|
if (deliverPendingCompletions(state)) {
|
|
16694
17035
|
if (state.drainBuffer.length > 0) drainNow(state);
|
|
16695
17036
|
return;
|
|
@@ -16727,6 +17068,36 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16727
17068
|
finishWithQuestionCall(state, call);
|
|
16728
17069
|
return;
|
|
16729
17070
|
}
|
|
17071
|
+
const autoTarget = (state.accountLimitHit || state.accountBlock) && msg.is_error === true ? autoFailoverTarget() : void 0;
|
|
17072
|
+
if (autoTarget && accountRun) {
|
|
17073
|
+
const target = autoTarget;
|
|
17074
|
+
accountRun.tried.add(target);
|
|
17075
|
+
accountRun.switchTo = {
|
|
17076
|
+
target,
|
|
17077
|
+
sourceAccount,
|
|
17078
|
+
failedAccount: limitAccount,
|
|
17079
|
+
cause: state.accountLimitHit ? "usage_limit" : state.accountBlock,
|
|
17080
|
+
resetsAt: state.accountLimitHit?.resetsAt,
|
|
17081
|
+
window: state.accountLimitHit?.window,
|
|
17082
|
+
failedConfigDir: self.skillBridgeSpawn(failover).configDir,
|
|
17083
|
+
reason: state.accountLimitHit || !state.accountBlock ? void 0 : describeAccountBlock(state.accountBlock)
|
|
17084
|
+
};
|
|
17085
|
+
log.warn("claude account cannot serve this turn; continuing it on the next account automatically", {
|
|
17086
|
+
sessionKey: sk,
|
|
17087
|
+
account: limitAccount,
|
|
17088
|
+
target,
|
|
17089
|
+
cause: state.accountLimitHit ? "usage_limit" : state.accountBlock,
|
|
17090
|
+
resetsAt: state.accountLimitHit?.resetsAt ?? null
|
|
17091
|
+
});
|
|
17092
|
+
state.endTextBlock();
|
|
17093
|
+
state.controllerClosed = true;
|
|
17094
|
+
cleanupTurn();
|
|
17095
|
+
try {
|
|
17096
|
+
controller.close();
|
|
17097
|
+
} catch {
|
|
17098
|
+
}
|
|
17099
|
+
return;
|
|
17100
|
+
}
|
|
16730
17101
|
if (modelFallbackArmed && attempt && msg.is_error === true) {
|
|
16731
17102
|
const refusal = state.modelRefusal ?? (state.accountLimitHit && !failoverAskActive && !state.accountBlock ? { kind: "account_limit" } : null);
|
|
16732
17103
|
if (refusal) {
|
|
@@ -16755,19 +17126,20 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16755
17126
|
}
|
|
16756
17127
|
}
|
|
16757
17128
|
if (state.usageLimitNote) {
|
|
17129
|
+
const noteCandidates = accountRun ? [] : failoverAccounts;
|
|
16758
17130
|
log.warn("claude account is out of usage; the turn ends with a note", {
|
|
16759
17131
|
sessionKey: sk,
|
|
16760
|
-
account:
|
|
17132
|
+
account: limitAccount,
|
|
16761
17133
|
window: state.accountLimitHit?.window ?? null,
|
|
16762
17134
|
resetsAt: state.accountLimitHit?.resetsAt ?? null,
|
|
16763
|
-
candidates:
|
|
17135
|
+
candidates: noteCandidates
|
|
16764
17136
|
});
|
|
16765
17137
|
controller.enqueue({
|
|
16766
17138
|
type: "text-delta",
|
|
16767
17139
|
id: state.startTextBlock(),
|
|
16768
17140
|
delta: formatUsageLimitNote({
|
|
16769
|
-
sourceAccount,
|
|
16770
|
-
candidates:
|
|
17141
|
+
sourceAccount: limitAccount,
|
|
17142
|
+
candidates: noteCandidates,
|
|
16771
17143
|
resetsAt: state.accountLimitHit?.resetsAt,
|
|
16772
17144
|
window: state.accountLimitHit?.window,
|
|
16773
17145
|
resetsText: state.accountLimitHit?.resetsText
|
|
@@ -16776,6 +17148,9 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16776
17148
|
state.endTextBlock();
|
|
16777
17149
|
state.resultFailure ??= "usage_limit";
|
|
16778
17150
|
}
|
|
17151
|
+
if (accountRun && state.accountLimitHit && msg.is_error === true) {
|
|
17152
|
+
state.resultFailure ??= "usage_limit";
|
|
17153
|
+
}
|
|
16779
17154
|
if (runAutoContinue(state, msg)) return;
|
|
16780
17155
|
for (const [idx, reasoningId] of state.reasoningIds) {
|
|
16781
17156
|
if (state.reasoningStarted.get(idx)) {
|
|
@@ -16860,10 +17235,14 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16860
17235
|
fastMode,
|
|
16861
17236
|
planModeQuestionActive,
|
|
16862
17237
|
interactive: useInteractive,
|
|
16863
|
-
sourceAccount,
|
|
17238
|
+
sourceAccount: limitAccount,
|
|
17239
|
+
accountConfigDir: accountRun ? self.skillBridgeSpawn(failover).configDir : self.config.configDir,
|
|
16864
17240
|
failoverAskActive,
|
|
17241
|
+
autoFailoverTarget,
|
|
16865
17242
|
usageLimitNoteActive,
|
|
16866
|
-
|
|
17243
|
+
// Read only when no switch takes the turn, which under `"auto"`
|
|
17244
|
+
// means no account is left to offer.
|
|
17245
|
+
failoverAccounts: accountRun ? [] : failoverAccounts,
|
|
16867
17246
|
modelFallbackArmed,
|
|
16868
17247
|
attempt,
|
|
16869
17248
|
handleControlRequest: handleControlRequest2,
|
|
@@ -17506,7 +17885,6 @@ function registerOpencodeLogSink(client) {
|
|
|
17506
17885
|
}
|
|
17507
17886
|
|
|
17508
17887
|
// src/v2-client.ts
|
|
17509
|
-
import path14 from "path";
|
|
17510
17888
|
function formatAgentTypeList(agents) {
|
|
17511
17889
|
const lines = agents.flatMap((agent) => {
|
|
17512
17890
|
if (agent.hidden === true || agent.mode === "primary") return [];
|
|
@@ -17534,8 +17912,7 @@ function describe(value) {
|
|
|
17534
17912
|
function toV1Session(info) {
|
|
17535
17913
|
if (!info || typeof info !== "object") return void 0;
|
|
17536
17914
|
const session = info;
|
|
17537
|
-
const
|
|
17538
|
-
const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path14.resolve(root, session.subpath) : root : void 0;
|
|
17915
|
+
const directory = typeof session.location?.directory === "string" ? session.location.directory : void 0;
|
|
17539
17916
|
return { ...info, directory, parentID: session.parentID };
|
|
17540
17917
|
}
|
|
17541
17918
|
function toV1Messages(messages) {
|