@khalilgharbaoui/opencode-claude-code-plugin 0.54.2 → 0.56.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/README.md +9 -1
- package/dist/index.d.ts +159 -10
- package/dist/index.js +2177 -603
- package/dist/index.js.map +1 -1
- package/dist/tui.js +206 -8
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +105 -9
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/claude-code-language-model.ts
|
|
2
|
-
import { join as
|
|
2
|
+
import { join as join17 } from "path";
|
|
3
3
|
|
|
4
4
|
// src/ids.ts
|
|
5
5
|
import { randomInt } from "crypto";
|
|
@@ -151,7 +151,7 @@ function translateStreamForHost(stream, dialect = "v1") {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
// src/account-failover.ts
|
|
154
|
-
import { homedir as
|
|
154
|
+
import { homedir as homedir3 } from "os";
|
|
155
155
|
|
|
156
156
|
// src/accounts.ts
|
|
157
157
|
import {
|
|
@@ -705,6 +705,198 @@ ${ACCOUNT_GROUP_MARKER} this conversation was running on the Claude account "${s
|
|
|
705
705
|
`;
|
|
706
706
|
}
|
|
707
707
|
|
|
708
|
+
// src/account-usage.ts
|
|
709
|
+
import { mkdirSync as mkdirSync2, readFileSync, renameSync as renameSync2, statSync as statSync2, writeFileSync } from "fs";
|
|
710
|
+
import { homedir as homedir2 } from "os";
|
|
711
|
+
import { join as join2 } from "path";
|
|
712
|
+
var DEFAULT_USAGE_WARN_THRESHOLDS = [50, 75, 90, 95];
|
|
713
|
+
var DEFAULT_USAGE_SWITCH_AT = 90;
|
|
714
|
+
var UNTIMED_READING_TTL_MS = 5 * 60 * 60 * 1e3;
|
|
715
|
+
var USAGE_READING_MAX_AGE_MS = 8 * 24 * 60 * 60 * 1e3;
|
|
716
|
+
var MAX_USAGE_ACCOUNTS = 32;
|
|
717
|
+
var MAX_USAGE_WINDOWS = 8;
|
|
718
|
+
var NAME = /^[a-z0-9_-]{1,64}$/;
|
|
719
|
+
var STATUS = /^[a-z_]{1,32}$/;
|
|
720
|
+
var MAX_RESETS_TEXT = 80;
|
|
721
|
+
var pathOverride = null;
|
|
722
|
+
var cache = null;
|
|
723
|
+
function accountUsageStorePath() {
|
|
724
|
+
if (pathOverride) return pathOverride;
|
|
725
|
+
const stateRoot = process.env.XDG_STATE_HOME || join2(homedir2(), ".local", "state");
|
|
726
|
+
return join2(stateRoot, "opencode-claude-code-plugin", "account-usage.json");
|
|
727
|
+
}
|
|
728
|
+
function finite(value) {
|
|
729
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
730
|
+
}
|
|
731
|
+
function validEntry(value) {
|
|
732
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
733
|
+
const entry = value;
|
|
734
|
+
if (!finite(entry.observedAt)) return null;
|
|
735
|
+
if (typeof entry.status !== "string" || !STATUS.test(entry.status)) return null;
|
|
736
|
+
if (entry.utilization !== null && !finite(entry.utilization)) return null;
|
|
737
|
+
if (entry.resetsAt !== void 0 && !finite(entry.resetsAt)) return null;
|
|
738
|
+
if (entry.resetsText !== void 0 && (typeof entry.resetsText !== "string" || entry.resetsText.length > MAX_RESETS_TEXT)) {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
const noted = Array.isArray(entry.noted) ? [...new Set(entry.noted.filter((t) => finite(t) && t > 0 && t <= 100))].sort(
|
|
742
|
+
(a, b) => a - b
|
|
743
|
+
) : [];
|
|
744
|
+
return {
|
|
745
|
+
utilization: entry.utilization,
|
|
746
|
+
...entry.resetsAt === void 0 ? {} : { resetsAt: entry.resetsAt },
|
|
747
|
+
...entry.resetsText === void 0 ? {} : { resetsText: entry.resetsText },
|
|
748
|
+
status: entry.status,
|
|
749
|
+
observedAt: entry.observedAt,
|
|
750
|
+
noted
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function readStore(file, now) {
|
|
754
|
+
const out = /* @__PURE__ */ new Map();
|
|
755
|
+
try {
|
|
756
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
757
|
+
const accounts = parsed && typeof parsed === "object" ? parsed.accounts : null;
|
|
758
|
+
if (!accounts || typeof accounts !== "object" || Array.isArray(accounts)) return out;
|
|
759
|
+
const cutoff = now - USAGE_READING_MAX_AGE_MS;
|
|
760
|
+
for (const [account, windows] of Object.entries(accounts)) {
|
|
761
|
+
if (!NAME.test(account) || !windows || typeof windows !== "object") continue;
|
|
762
|
+
const kept = /* @__PURE__ */ new Map();
|
|
763
|
+
for (const [window, raw] of Object.entries(windows)) {
|
|
764
|
+
if (!NAME.test(window)) continue;
|
|
765
|
+
const entry = validEntry(raw);
|
|
766
|
+
if (entry && entry.observedAt >= cutoff) kept.set(window, entry);
|
|
767
|
+
}
|
|
768
|
+
if (kept.size > 0) out.set(account, kept);
|
|
769
|
+
}
|
|
770
|
+
} catch {
|
|
771
|
+
}
|
|
772
|
+
return out;
|
|
773
|
+
}
|
|
774
|
+
function newestObserved(windows) {
|
|
775
|
+
let newest = 0;
|
|
776
|
+
for (const entry of windows.values()) newest = Math.max(newest, entry.observedAt);
|
|
777
|
+
return newest;
|
|
778
|
+
}
|
|
779
|
+
function bounded(usage, now) {
|
|
780
|
+
const cutoff = now - USAGE_READING_MAX_AGE_MS;
|
|
781
|
+
const accounts = [...usage.entries()].map(([account, windows]) => {
|
|
782
|
+
const kept = [...windows.entries()].filter(([, entry]) => entry.observedAt >= cutoff).sort((a, b) => a[1].observedAt - b[1].observedAt).slice(-MAX_USAGE_WINDOWS);
|
|
783
|
+
return [account, new Map(kept)];
|
|
784
|
+
}).filter(([, windows]) => windows.size > 0).sort((a, b) => newestObserved(a[1]) - newestObserved(b[1])).slice(-MAX_USAGE_ACCOUNTS);
|
|
785
|
+
return new Map(accounts);
|
|
786
|
+
}
|
|
787
|
+
function writeStore(file, usage) {
|
|
788
|
+
const accounts = {};
|
|
789
|
+
for (const [account, windows] of usage) accounts[account] = Object.fromEntries(windows);
|
|
790
|
+
try {
|
|
791
|
+
mkdirSync2(join2(file, ".."), { recursive: true, mode: 448 });
|
|
792
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
793
|
+
writeFileSync(tmp, JSON.stringify({ version: 1, accounts }), { mode: 384 });
|
|
794
|
+
renameSync2(tmp, file);
|
|
795
|
+
return true;
|
|
796
|
+
} catch {
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
function cached(now) {
|
|
801
|
+
const file = accountUsageStorePath();
|
|
802
|
+
if (!cache || cache.path !== file) cache = { path: file, usage: readStore(file, now) };
|
|
803
|
+
return cache.usage;
|
|
804
|
+
}
|
|
805
|
+
function refreshed(now) {
|
|
806
|
+
const file = accountUsageStorePath();
|
|
807
|
+
cache = { path: file, usage: readStore(file, now) };
|
|
808
|
+
return cache.usage;
|
|
809
|
+
}
|
|
810
|
+
function isReadingCurrent(entry, now = Date.now()) {
|
|
811
|
+
if (entry.resetsAt !== void 0) return entry.resetsAt > now;
|
|
812
|
+
return now - entry.observedAt < UNTIMED_READING_TTL_MS;
|
|
813
|
+
}
|
|
814
|
+
function applyReading(previous, reading, now = Date.now()) {
|
|
815
|
+
const carried = previous && isReadingCurrent(previous, now) ? previous.noted : [];
|
|
816
|
+
return {
|
|
817
|
+
utilization: finite(reading.utilization) ? reading.utilization : null,
|
|
818
|
+
...finite(reading.resetsAt) ? { resetsAt: reading.resetsAt } : {},
|
|
819
|
+
...reading.resetsText && !finite(reading.resetsAt) ? { resetsText: reading.resetsText.slice(0, MAX_RESETS_TEXT) } : {},
|
|
820
|
+
status: STATUS.test(reading.status) ? reading.status : "unknown",
|
|
821
|
+
observedAt: now,
|
|
822
|
+
noted: [...carried]
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
function usagePercent(utilization) {
|
|
826
|
+
return Math.round(utilization * 1e3) / 10;
|
|
827
|
+
}
|
|
828
|
+
function pendingCrossings(entry, thresholds, now = Date.now()) {
|
|
829
|
+
if (!isReadingCurrent(entry, now)) return [];
|
|
830
|
+
if (entry.utilization === null || entry.status === "rejected") return [];
|
|
831
|
+
const percent = usagePercent(entry.utilization);
|
|
832
|
+
return [...new Set(thresholds)].filter((threshold) => percent >= threshold && !entry.noted.includes(threshold)).sort((a, b) => a - b);
|
|
833
|
+
}
|
|
834
|
+
function resolveUsageWarnThresholds(value) {
|
|
835
|
+
if (!Array.isArray(value)) return [...DEFAULT_USAGE_WARN_THRESHOLDS];
|
|
836
|
+
return [...new Set(value.filter((t) => finite(t) && t > 0 && t <= 100))].sort(
|
|
837
|
+
(a, b) => a - b
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
function resolveUsageSwitchAt(value) {
|
|
841
|
+
if (value === false || value === 0) return null;
|
|
842
|
+
if (finite(value) && value > 0 && value <= 100) return value;
|
|
843
|
+
return DEFAULT_USAGE_SWITCH_AT;
|
|
844
|
+
}
|
|
845
|
+
function recordUsageReading(account, window, reading, now = Date.now()) {
|
|
846
|
+
if (!NAME.test(account) || !NAME.test(window)) return null;
|
|
847
|
+
const file = accountUsageStorePath();
|
|
848
|
+
const merged = readStore(file, now);
|
|
849
|
+
const previous = merged.get(account)?.get(window) ?? cached(now).get(account)?.get(window);
|
|
850
|
+
const entry = applyReading(previous, reading, now);
|
|
851
|
+
const windows = merged.get(account) ?? /* @__PURE__ */ new Map();
|
|
852
|
+
windows.set(window, entry);
|
|
853
|
+
merged.set(account, windows);
|
|
854
|
+
const kept = bounded(merged, now);
|
|
855
|
+
cache = { path: file, usage: kept };
|
|
856
|
+
return writeStore(file, kept) ? entry : null;
|
|
857
|
+
}
|
|
858
|
+
function markUsageNoted(account, window, thresholds, now = Date.now()) {
|
|
859
|
+
if (!NAME.test(account) || !NAME.test(window) || thresholds.length === 0) return false;
|
|
860
|
+
const file = accountUsageStorePath();
|
|
861
|
+
const merged = readStore(file, now);
|
|
862
|
+
const entry = merged.get(account)?.get(window) ?? cached(now).get(account)?.get(window);
|
|
863
|
+
if (!entry) return false;
|
|
864
|
+
const updated = {
|
|
865
|
+
...entry,
|
|
866
|
+
noted: [.../* @__PURE__ */ new Set([...entry.noted, ...thresholds])].sort((a, b) => a - b)
|
|
867
|
+
};
|
|
868
|
+
const windows = merged.get(account) ?? /* @__PURE__ */ new Map();
|
|
869
|
+
windows.set(window, updated);
|
|
870
|
+
merged.set(account, windows);
|
|
871
|
+
const kept = bounded(merged, now);
|
|
872
|
+
cache = { path: file, usage: kept };
|
|
873
|
+
return writeStore(file, kept);
|
|
874
|
+
}
|
|
875
|
+
function usageCrossings(account, thresholds, now = Date.now()) {
|
|
876
|
+
if (thresholds.length === 0) return [];
|
|
877
|
+
const collect = (usage) => {
|
|
878
|
+
const out = [];
|
|
879
|
+
for (const [window, entry] of usage.get(account) ?? []) {
|
|
880
|
+
const crossed = pendingCrossings(entry, thresholds, now);
|
|
881
|
+
if (crossed.length > 0) out.push({ window, entry, crossed });
|
|
882
|
+
}
|
|
883
|
+
return out;
|
|
884
|
+
};
|
|
885
|
+
if (collect(cached(now)).length === 0) return [];
|
|
886
|
+
return collect(refreshed(now));
|
|
887
|
+
}
|
|
888
|
+
function accountUsageAtOrOver(account, percent, now = Date.now()) {
|
|
889
|
+
for (const entry of cached(now).get(account)?.values() ?? []) {
|
|
890
|
+
if (!isReadingCurrent(entry, now)) continue;
|
|
891
|
+
if (entry.status === "rejected") return true;
|
|
892
|
+
if (entry.utilization !== null && usagePercent(entry.utilization) >= percent) return true;
|
|
893
|
+
}
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
function snapshotAccountUsage(now = Date.now()) {
|
|
897
|
+
return refreshed(now);
|
|
898
|
+
}
|
|
899
|
+
|
|
708
900
|
// src/types.ts
|
|
709
901
|
var READ_ONLY_PERMISSION_MODE = "read-only";
|
|
710
902
|
var DEFAULT_PROXY_TOOL_NAMES = [
|
|
@@ -1079,9 +1271,9 @@ function reportConversationReset(msg) {
|
|
|
1079
1271
|
}
|
|
1080
1272
|
var RESULT_ERROR_MARKER = "\u258C **claude code error:**";
|
|
1081
1273
|
var RESULT_SUBTYPES = {
|
|
1082
|
-
error_max_turns: "it
|
|
1274
|
+
error_max_turns: "it reached its `--max-turns` cap (the agent's `maxTurns`) before finishing this message",
|
|
1083
1275
|
error_during_execution: "it failed while running the turn",
|
|
1084
|
-
error_max_budget_usd: "it
|
|
1276
|
+
error_max_budget_usd: "it reached its `--max-budget-usd` cap (the agent's `maxBudgetUsd`), which counts this process's whole life",
|
|
1085
1277
|
error_max_structured_output_retries: "it could not produce valid structured output"
|
|
1086
1278
|
};
|
|
1087
1279
|
function describeResultFailure(msg) {
|
|
@@ -1529,7 +1721,7 @@ function accountBlockKind(msg) {
|
|
|
1529
1721
|
function describeAccountBlock(kind) {
|
|
1530
1722
|
return ACCOUNT_BLOCKS[kind].what;
|
|
1531
1723
|
}
|
|
1532
|
-
function loginCommandFor(configDir, home =
|
|
1724
|
+
function loginCommandFor(configDir, home = homedir3()) {
|
|
1533
1725
|
if (!configDir) return "claude auth login";
|
|
1534
1726
|
const shown = configDir === home || configDir.startsWith(`${home}/`) ? `~${configDir.slice(home.length)}` : configDir;
|
|
1535
1727
|
return `CLAUDE_CONFIG_DIR=${shown} claude auth login`;
|
|
@@ -1560,6 +1752,28 @@ function formatUsageLimitNote(input) {
|
|
|
1560
1752
|
${USAGE_LIMIT_MARKER} ${what} ${advice}
|
|
1561
1753
|
`;
|
|
1562
1754
|
}
|
|
1755
|
+
var USAGE_MARKER = "\u258C **usage:**";
|
|
1756
|
+
function formatUsageNote(input) {
|
|
1757
|
+
const account = normalizeAccountName(input.account || DEFAULT_ACCOUNT);
|
|
1758
|
+
const clauses = input.windows.map((entry) => {
|
|
1759
|
+
const window = describeRateLimitWindow(entry.window) ?? "a usage window";
|
|
1760
|
+
const resets = formatResetsAtLocal(entry.resetsAt);
|
|
1761
|
+
const when = resets ? `, which resets at ${resets}` : entry.resetsText ? `, which resets ${entry.resetsText}` : "";
|
|
1762
|
+
return `${entry.percent}% of ${window}${when}`;
|
|
1763
|
+
});
|
|
1764
|
+
const used = clauses.length <= 1 ? clauses.join("") : `${clauses.slice(0, -1).join("; ")}; and ${clauses[clauses.length - 1]}`;
|
|
1765
|
+
let then = "";
|
|
1766
|
+
if (input.movedTo) {
|
|
1767
|
+
const until = formatResetsAtLocal(input.movedTo.until);
|
|
1768
|
+
then = ` New turns move to the "${normalizeAccountName(input.movedTo.target)}" account ${until ? `until ${until}` : "until opencode restarts"}.`;
|
|
1769
|
+
} else {
|
|
1770
|
+
const others = describeOtherAccounts(input.candidates ?? []);
|
|
1771
|
+
if (others) then = ` To keep working past the limit, pick a model from ${others}.`;
|
|
1772
|
+
}
|
|
1773
|
+
return `
|
|
1774
|
+
${USAGE_MARKER} the Claude account "${account}" has used ${used}.${then}
|
|
1775
|
+
`;
|
|
1776
|
+
}
|
|
1563
1777
|
function isAccountLimitError(input) {
|
|
1564
1778
|
if (input.rateLimit && isRateLimitRejected(input.rateLimit)) return true;
|
|
1565
1779
|
const text = input.resultText;
|
|
@@ -1591,7 +1805,7 @@ function recallAccountLimit(account, now = Date.now()) {
|
|
|
1591
1805
|
};
|
|
1592
1806
|
}
|
|
1593
1807
|
var accountOverrides = /* @__PURE__ */ new Map();
|
|
1594
|
-
function setAccountOverride(source, target, until, now = Date.now()) {
|
|
1808
|
+
function setAccountOverride(source, target, until, now = Date.now(), cause = "usage-limit") {
|
|
1595
1809
|
const from = normalizeAccountName(source || DEFAULT_ACCOUNT);
|
|
1596
1810
|
const to = normalizeAccountName(target);
|
|
1597
1811
|
if (!to || to === from) return;
|
|
@@ -1604,6 +1818,14 @@ function setAccountOverride(source, target, until, now = Date.now()) {
|
|
|
1604
1818
|
until = void 0;
|
|
1605
1819
|
}
|
|
1606
1820
|
accountOverrides.set(from, { target: to, until });
|
|
1821
|
+
if (cause === "usage-threshold") {
|
|
1822
|
+
log.warn("claude account is near its usage limit; new turns move to another account until the window resets", {
|
|
1823
|
+
source: from,
|
|
1824
|
+
target: to,
|
|
1825
|
+
until: until ?? null
|
|
1826
|
+
});
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1607
1829
|
log.warn(
|
|
1608
1830
|
`Claude account "${from}" is out of usage; this and every other session on it now runs on "${to}"${until ? ` until ${new Date(until).toISOString()}` : " until opencode restarts"}.`,
|
|
1609
1831
|
{ source: from, target: to, until: until ?? null }
|
|
@@ -1623,6 +1845,9 @@ function resolveAccountOverride(source, now = Date.now()) {
|
|
|
1623
1845
|
}
|
|
1624
1846
|
return entry.target;
|
|
1625
1847
|
}
|
|
1848
|
+
function clearAccountOverride(source) {
|
|
1849
|
+
accountOverrides.delete(normalizeAccountName(source || DEFAULT_ACCOUNT));
|
|
1850
|
+
}
|
|
1626
1851
|
function stripAccountSuffix(modelId) {
|
|
1627
1852
|
const at = modelId.indexOf("@");
|
|
1628
1853
|
return at === -1 ? modelId : modelId.slice(0, at);
|
|
@@ -1671,6 +1896,20 @@ function failoverCandidates(accounts, source, groups) {
|
|
|
1671
1896
|
}
|
|
1672
1897
|
return out;
|
|
1673
1898
|
}
|
|
1899
|
+
function isAccountKnownUnavailable(account, now = Date.now()) {
|
|
1900
|
+
if (resolveAccountOverride(account, now) !== void 0) return true;
|
|
1901
|
+
return recallAccountLimit(account, now).resetsAt !== void 0;
|
|
1902
|
+
}
|
|
1903
|
+
function nextAutoFailoverAccount(candidates, tried, now = Date.now()) {
|
|
1904
|
+
return candidates.find(
|
|
1905
|
+
(account) => !tried.has(account) && !isAccountKnownUnavailable(account, now)
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
function nextUsageSwitchAccount(candidates, exclude, switchAt, now = Date.now()) {
|
|
1909
|
+
return candidates.find(
|
|
1910
|
+
(account) => !exclude.has(account) && !isAccountKnownUnavailable(account, now) && !accountUsageAtOrOver(account, switchAt, now)
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1674
1913
|
var pendingQuestions2 = /* @__PURE__ */ new Map();
|
|
1675
1914
|
function pendingKey2(sessionKey2, toolCallId) {
|
|
1676
1915
|
return `${sessionKey2}${KEY_SEPARATOR2}${toolCallId}`;
|
|
@@ -1797,6 +2036,13 @@ function formatFailoverNote(input) {
|
|
|
1797
2036
|
${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
2037
|
`;
|
|
1799
2038
|
}
|
|
2039
|
+
function formatAutoFailoverNote(input) {
|
|
2040
|
+
const resets = describeReset(input.resetsAt);
|
|
2041
|
+
const arrival = input.carried ? "The conversation continues where it stopped." : "The conversation is replayed into a fresh Claude session there.";
|
|
2042
|
+
return `
|
|
2043
|
+
${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}
|
|
2044
|
+
`;
|
|
2045
|
+
}
|
|
1800
2046
|
function formatFailoverStopNote(reason) {
|
|
1801
2047
|
return `
|
|
1802
2048
|
${FAILOVER_MARKER} Staying on this account (${reason}). The turn ends here; the usage limit is unchanged.
|
|
@@ -1845,7 +2091,7 @@ function failoverUntil(resetsAt) {
|
|
|
1845
2091
|
}
|
|
1846
2092
|
|
|
1847
2093
|
// src/subagent-dispatch.ts
|
|
1848
|
-
import { join as
|
|
2094
|
+
import { join as join5 } from "path";
|
|
1849
2095
|
|
|
1850
2096
|
// src/agent-models.ts
|
|
1851
2097
|
import { readFile, readdir } from "fs/promises";
|
|
@@ -2253,6 +2499,7 @@ var REASONING_EFFORTS = [
|
|
|
2253
2499
|
var registry = {};
|
|
2254
2500
|
var defaultSubagentModel;
|
|
2255
2501
|
var defaultSubagentCacheTtl;
|
|
2502
|
+
var defaultSubagentCliCaps = {};
|
|
2256
2503
|
var providerFallbackModels = [];
|
|
2257
2504
|
function setAgentRegistry(records2) {
|
|
2258
2505
|
registry = records2;
|
|
@@ -2269,6 +2516,15 @@ function getDefaultSubagentModel() {
|
|
|
2269
2516
|
function setDefaultSubagentCacheTtl(ttl) {
|
|
2270
2517
|
defaultSubagentCacheTtl = ttl?.trim() || void 0;
|
|
2271
2518
|
}
|
|
2519
|
+
function setDefaultSubagentCliCaps(caps) {
|
|
2520
|
+
defaultSubagentCliCaps = {
|
|
2521
|
+
...caps.maxTurns !== void 0 ? { maxTurns: caps.maxTurns } : {},
|
|
2522
|
+
...caps.maxBudgetUsd !== void 0 ? { maxBudgetUsd: caps.maxBudgetUsd } : {}
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
function getDefaultSubagentCliCaps() {
|
|
2526
|
+
return { ...defaultSubagentCliCaps };
|
|
2527
|
+
}
|
|
2272
2528
|
function setProviderFallbackModels(models) {
|
|
2273
2529
|
providerFallbackModels = models ?? [];
|
|
2274
2530
|
}
|
|
@@ -2352,6 +2608,45 @@ function resolveAgentCacheTtl(agent, overrides) {
|
|
|
2352
2608
|
log.debug("agent prompt cache ttl", { agent, ttl: wanted });
|
|
2353
2609
|
return wanted;
|
|
2354
2610
|
}
|
|
2611
|
+
var refusedMainAgentBudgets = /* @__PURE__ */ new Set();
|
|
2612
|
+
function resolveAgentCliCaps(agent, overrides) {
|
|
2613
|
+
if (!agent) return {};
|
|
2614
|
+
const record = (overrides?.records ?? registry)[agent];
|
|
2615
|
+
if (!record) return {};
|
|
2616
|
+
const defaults = overrides?.defaults ?? defaultSubagentCliCaps;
|
|
2617
|
+
const isSubagent = record.mode === "subagent";
|
|
2618
|
+
const caps = {};
|
|
2619
|
+
const turns = parseCap(record.maxTurns, true);
|
|
2620
|
+
if (record.maxTurns !== void 0 && turns === void 0) {
|
|
2621
|
+
log.warn("agent maxTurns refused: not a positive whole number", { agent });
|
|
2622
|
+
}
|
|
2623
|
+
const maxTurns = turns ?? (isSubagent ? defaults.maxTurns : void 0);
|
|
2624
|
+
if (maxTurns !== void 0) caps.maxTurns = maxTurns;
|
|
2625
|
+
const budget = parseCap(record.maxBudgetUsd, false);
|
|
2626
|
+
if (record.maxBudgetUsd !== void 0 && budget === void 0) {
|
|
2627
|
+
log.warn("agent maxBudgetUsd refused: not a positive number of dollars", { agent });
|
|
2628
|
+
}
|
|
2629
|
+
if (budget !== void 0 && !isSubagent) {
|
|
2630
|
+
if (!refusedMainAgentBudgets.has(agent)) {
|
|
2631
|
+
refusedMainAgentBudgets.add(agent);
|
|
2632
|
+
log.warn(
|
|
2633
|
+
"agent maxBudgetUsd ignored: the CLI counts it over a reused process's whole life, so it applies to subagents only",
|
|
2634
|
+
{ agent }
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2637
|
+
} else {
|
|
2638
|
+
const maxBudgetUsd = budget ?? (isSubagent ? defaults.maxBudgetUsd : void 0);
|
|
2639
|
+
if (maxBudgetUsd !== void 0) caps.maxBudgetUsd = maxBudgetUsd;
|
|
2640
|
+
}
|
|
2641
|
+
return caps;
|
|
2642
|
+
}
|
|
2643
|
+
function parseCap(value, integer) {
|
|
2644
|
+
if (value === void 0) return void 0;
|
|
2645
|
+
const parsed = typeof value === "string" ? value.trim() ? Number(value) : NaN : value;
|
|
2646
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
|
|
2647
|
+
if (integer && !Number.isInteger(parsed)) return void 0;
|
|
2648
|
+
return parsed;
|
|
2649
|
+
}
|
|
2355
2650
|
function parseAgentFrontmatter(text) {
|
|
2356
2651
|
const record = {};
|
|
2357
2652
|
if (!text.startsWith("---")) return record;
|
|
@@ -2384,6 +2679,8 @@ function parseAgentFrontmatter(text) {
|
|
|
2384
2679
|
else if (key === "forceModel") record.forceModel = value;
|
|
2385
2680
|
else if (key === "reasoningEffort") record.reasoningEffort = value;
|
|
2386
2681
|
else if (key === "cacheTtl") record.cacheTtl = value;
|
|
2682
|
+
else if (key === "maxTurns") record.maxTurns = value;
|
|
2683
|
+
else if (key === "maxBudgetUsd") record.maxBudgetUsd = value;
|
|
2387
2684
|
}
|
|
2388
2685
|
return record;
|
|
2389
2686
|
}
|
|
@@ -2445,18 +2742,18 @@ function agentDirectories(home, projectDirectory) {
|
|
|
2445
2742
|
}
|
|
2446
2743
|
|
|
2447
2744
|
// src/dispatch-choice-store.ts
|
|
2448
|
-
import { mkdirSync as
|
|
2449
|
-
import { homedir as
|
|
2450
|
-
import { join as
|
|
2745
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, renameSync as renameSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2746
|
+
import { homedir as homedir4 } from "os";
|
|
2747
|
+
import { join as join3 } from "path";
|
|
2451
2748
|
var MAX_DISPATCH_CHOICE_SESSIONS = 128;
|
|
2452
2749
|
var MAX_DISPATCH_CHOICES_PER_SESSION = 32;
|
|
2453
2750
|
var DISPATCH_CHOICE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
2454
2751
|
var sessions = null;
|
|
2455
|
-
var
|
|
2752
|
+
var pathOverride2 = null;
|
|
2456
2753
|
function dispatchChoiceStorePath() {
|
|
2457
|
-
if (
|
|
2458
|
-
const stateRoot = process.env.XDG_STATE_HOME ||
|
|
2459
|
-
return
|
|
2754
|
+
if (pathOverride2) return pathOverride2;
|
|
2755
|
+
const stateRoot = process.env.XDG_STATE_HOME || join3(homedir4(), ".local", "state");
|
|
2756
|
+
return join3(stateRoot, "opencode-claude-code-plugin", "subagent-dispatch.json");
|
|
2460
2757
|
}
|
|
2461
2758
|
function isChoice(value) {
|
|
2462
2759
|
const choice = value;
|
|
@@ -2482,7 +2779,7 @@ function trimChoices(choices) {
|
|
|
2482
2779
|
function readFile2(file) {
|
|
2483
2780
|
const out = /* @__PURE__ */ new Map();
|
|
2484
2781
|
try {
|
|
2485
|
-
const parsed = JSON.parse(
|
|
2782
|
+
const parsed = JSON.parse(readFileSync2(file, "utf8"));
|
|
2486
2783
|
const entries = parsed && typeof parsed === "object" ? parsed.sessions : null;
|
|
2487
2784
|
if (!entries || typeof entries !== "object") return out;
|
|
2488
2785
|
const cutoff = Date.now() - DISPATCH_CHOICE_MAX_AGE_MS;
|
|
@@ -2504,12 +2801,12 @@ function save(update) {
|
|
|
2504
2801
|
const kept = [...merged.entries()].filter(([, record]) => record.updatedAt >= cutoff).filter(([, record]) => Object.keys(record.choices).length > 0).sort((a, b) => a[1].updatedAt - b[1].updatedAt).slice(-MAX_DISPATCH_CHOICE_SESSIONS);
|
|
2505
2802
|
sessions = new Map(kept);
|
|
2506
2803
|
try {
|
|
2507
|
-
|
|
2804
|
+
mkdirSync3(join3(file, ".."), { recursive: true, mode: 448 });
|
|
2508
2805
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
2509
|
-
|
|
2806
|
+
writeFileSync2(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
|
|
2510
2807
|
mode: 384
|
|
2511
2808
|
});
|
|
2512
|
-
|
|
2809
|
+
renameSync3(tmp, file);
|
|
2513
2810
|
} catch (err) {
|
|
2514
2811
|
log.debug("could not write the subagent dispatch choice store", {
|
|
2515
2812
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -3419,97 +3716,404 @@ function stripSubagentDispatchParts(prompt) {
|
|
|
3419
3716
|
return changed ? out : prompt;
|
|
3420
3717
|
}
|
|
3421
3718
|
|
|
3422
|
-
// src/
|
|
3423
|
-
import {
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3719
|
+
// src/session-fork.ts
|
|
3720
|
+
import { createHash } from "crypto";
|
|
3721
|
+
var fingerprints = /* @__PURE__ */ new Map();
|
|
3722
|
+
var fingerprintSeq = 0;
|
|
3723
|
+
var MAX_FORK_FINGERPRINTS = 64;
|
|
3724
|
+
function hashPart(hash, part) {
|
|
3725
|
+
const p = part;
|
|
3726
|
+
const type = typeof p?.type === "string" ? p.type : "unknown";
|
|
3727
|
+
switch (type) {
|
|
3728
|
+
case "text":
|
|
3729
|
+
hash.update("text\0");
|
|
3730
|
+
hash.update(String(p?.text ?? ""));
|
|
3731
|
+
break;
|
|
3732
|
+
case "tool-call":
|
|
3733
|
+
hash.update("call\0");
|
|
3734
|
+
hash.update(String(p?.toolName ?? ""));
|
|
3735
|
+
hash.update("\0");
|
|
3736
|
+
hash.update(stableJson(p?.input));
|
|
3737
|
+
break;
|
|
3738
|
+
case "tool-result":
|
|
3739
|
+
hash.update("result\0");
|
|
3740
|
+
hash.update(String(p?.toolName ?? ""));
|
|
3741
|
+
hash.update("\0");
|
|
3742
|
+
hash.update(stableJson(p?.output));
|
|
3743
|
+
break;
|
|
3744
|
+
case "file":
|
|
3745
|
+
case "image": {
|
|
3746
|
+
hash.update(type);
|
|
3747
|
+
hash.update("\0");
|
|
3748
|
+
hash.update(String(p?.mediaType ?? ""));
|
|
3749
|
+
hash.update("\0");
|
|
3750
|
+
const data = p?.data ?? p?.image;
|
|
3751
|
+
if (typeof data === "string") hash.update(data);
|
|
3752
|
+
else if (data instanceof Uint8Array) hash.update(data);
|
|
3753
|
+
else if (data instanceof URL) hash.update(data.href);
|
|
3754
|
+
else hash.update(stableJson(data));
|
|
3755
|
+
break;
|
|
3756
|
+
}
|
|
3757
|
+
default:
|
|
3758
|
+
hash.update("other\0");
|
|
3759
|
+
hash.update(type);
|
|
3760
|
+
break;
|
|
3761
|
+
}
|
|
3762
|
+
hash.update("");
|
|
3435
3763
|
}
|
|
3436
|
-
function
|
|
3764
|
+
function stableJson(value) {
|
|
3765
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
3766
|
+
const walk = (node) => {
|
|
3767
|
+
if (node === null || typeof node !== "object") return node;
|
|
3768
|
+
if (seen.has(node)) return "[circular]";
|
|
3769
|
+
seen.add(node);
|
|
3770
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
3771
|
+
const out = {};
|
|
3772
|
+
for (const key of Object.keys(node).sort()) {
|
|
3773
|
+
out[key] = walk(node[key]);
|
|
3774
|
+
}
|
|
3775
|
+
return out;
|
|
3776
|
+
};
|
|
3437
3777
|
try {
|
|
3438
|
-
return
|
|
3778
|
+
return JSON.stringify(walk(value)) ?? "null";
|
|
3439
3779
|
} catch {
|
|
3440
|
-
return
|
|
3780
|
+
return "[unserializable]";
|
|
3441
3781
|
}
|
|
3442
3782
|
}
|
|
3443
|
-
function
|
|
3444
|
-
const
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
const isFile = deps.isFile ?? defaultIsFile;
|
|
3454
|
-
const extensions = executableExtensions(env);
|
|
3455
|
-
const candidatesFor = (base) => {
|
|
3456
|
-
const ext = path4.win32.extname(base).toLowerCase();
|
|
3457
|
-
if (ext && extensions.includes(ext)) return [base];
|
|
3458
|
-
return [...extensions.map((candidate) => base + candidate), base];
|
|
3459
|
-
};
|
|
3460
|
-
if (command.includes("\\") || command.includes("/")) {
|
|
3461
|
-
for (const candidate of candidatesFor(command)) {
|
|
3462
|
-
if (isFile(candidate)) return candidate;
|
|
3463
|
-
}
|
|
3464
|
-
return null;
|
|
3465
|
-
}
|
|
3466
|
-
const pathValue = envValue(env, "PATH") ?? "";
|
|
3467
|
-
for (const entry of pathValue.split(path4.win32.delimiter)) {
|
|
3468
|
-
const directory = entry.trim().replace(/^"(.*)"$/, "$1");
|
|
3469
|
-
if (!directory) continue;
|
|
3470
|
-
for (const candidate of candidatesFor(path4.win32.join(directory, command))) {
|
|
3471
|
-
if (isFile(candidate)) return candidate;
|
|
3472
|
-
}
|
|
3783
|
+
function messageDigest(msg) {
|
|
3784
|
+
const hash = createHash("sha256");
|
|
3785
|
+
hash.update(msg.role);
|
|
3786
|
+
hash.update("\0");
|
|
3787
|
+
if (typeof msg.content === "string") {
|
|
3788
|
+
hash.update("text\0");
|
|
3789
|
+
hash.update(msg.content);
|
|
3790
|
+
hash.update("");
|
|
3791
|
+
} else if (Array.isArray(msg.content)) {
|
|
3792
|
+
for (const part of msg.content) hashPart(hash, part);
|
|
3473
3793
|
}
|
|
3474
|
-
return
|
|
3794
|
+
return hash.digest("hex").slice(0, 32);
|
|
3475
3795
|
}
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
let backslashes = 0;
|
|
3482
|
-
for (const char of arg) {
|
|
3483
|
-
if (char === "\\") {
|
|
3484
|
-
backslashes += 1;
|
|
3485
|
-
continue;
|
|
3486
|
-
}
|
|
3487
|
-
if (char === '"') {
|
|
3488
|
-
out += "\\".repeat(backslashes * 2) + '""';
|
|
3489
|
-
backslashes = 0;
|
|
3796
|
+
var ASSISTANT_TURN_DIGEST = "assistant-turn";
|
|
3797
|
+
function conversationDigests(prompt, opts) {
|
|
3798
|
+
const out = [];
|
|
3799
|
+
for (const msg of prompt) {
|
|
3800
|
+
if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "tool") {
|
|
3490
3801
|
continue;
|
|
3491
3802
|
}
|
|
3492
|
-
out
|
|
3493
|
-
|
|
3803
|
+
out.push({
|
|
3804
|
+
role: msg.role,
|
|
3805
|
+
digest: opts?.assistantContent === false && msg.role === "assistant" ? ASSISTANT_TURN_DIGEST : messageDigest(msg)
|
|
3806
|
+
});
|
|
3494
3807
|
}
|
|
3495
|
-
return
|
|
3496
|
-
}
|
|
3497
|
-
function
|
|
3498
|
-
const
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3808
|
+
return out;
|
|
3809
|
+
}
|
|
3810
|
+
function recordForkFingerprint(sessionKey2, prompt, cliPath) {
|
|
3811
|
+
const entries = conversationDigests(prompt);
|
|
3812
|
+
if (entries.length === 0) return;
|
|
3813
|
+
fingerprints.delete(sessionKey2);
|
|
3814
|
+
fingerprints.set(sessionKey2, { entries, cliPath, seq: ++fingerprintSeq });
|
|
3815
|
+
while (fingerprints.size > MAX_FORK_FINGERPRINTS) {
|
|
3816
|
+
const oldest = fingerprints.keys().next();
|
|
3817
|
+
if (oldest.done) break;
|
|
3818
|
+
fingerprints.delete(oldest.value);
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
function forgetForkFingerprint(sessionKey2) {
|
|
3822
|
+
fingerprints.delete(sessionKey2);
|
|
3823
|
+
}
|
|
3824
|
+
function forkSiblingSignature(sessionKey2) {
|
|
3825
|
+
const parts = sessionKey2.split("::");
|
|
3826
|
+
if (parts.length < 5) return null;
|
|
3827
|
+
if (parts[2] === "compaction") return null;
|
|
3828
|
+
const blanked = [...parts];
|
|
3829
|
+
blanked[3] = "*";
|
|
3830
|
+
return blanked.join("::");
|
|
3831
|
+
}
|
|
3832
|
+
function modelSiblingSignature(sessionKey2, accountProviders) {
|
|
3833
|
+
const parts = sessionKey2.split("::");
|
|
3834
|
+
if (parts.length < 5) return null;
|
|
3835
|
+
if (parts[2] === "compaction") return null;
|
|
3836
|
+
const withoutEffort = parts[parts.length - 1].startsWith("effort=") ? parts.slice(0, -1) : parts;
|
|
3837
|
+
if (withoutEffort.length < 5) return null;
|
|
3838
|
+
const blanked = [...withoutEffort];
|
|
3839
|
+
blanked[1] = "*";
|
|
3840
|
+
const signature = blanked.join("::");
|
|
3841
|
+
return accountProviders && accountProviders.size > 0 ? blankAccountProvider(signature, accountProviders) : signature;
|
|
3842
|
+
}
|
|
3843
|
+
function moveForkFingerprint(fromKey, toKey) {
|
|
3844
|
+
const record = fingerprints.get(fromKey);
|
|
3845
|
+
if (!record) return;
|
|
3846
|
+
fingerprints.delete(fromKey);
|
|
3847
|
+
fingerprints.delete(toKey);
|
|
3848
|
+
fingerprints.set(toKey, { ...record, seq: ++fingerprintSeq });
|
|
3849
|
+
}
|
|
3850
|
+
function splitForkHistory(prompt, opts) {
|
|
3851
|
+
const messages = prompt;
|
|
3852
|
+
let lastAssistant = -1;
|
|
3853
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3854
|
+
if (messages[i].role === "assistant") {
|
|
3855
|
+
lastAssistant = i;
|
|
3856
|
+
break;
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
if (lastAssistant < 0) return { history: [], forkable: false };
|
|
3860
|
+
for (let i = lastAssistant + 1; i < messages.length; i++) {
|
|
3861
|
+
const msg = messages[i];
|
|
3862
|
+
if (msg.role === "tool") return { history: [], forkable: false };
|
|
3863
|
+
if (!Array.isArray(msg.content)) continue;
|
|
3864
|
+
for (const part of msg.content) {
|
|
3865
|
+
if (part?.type === "tool-result") return { history: [], forkable: false };
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
return {
|
|
3869
|
+
history: conversationDigests(messages.slice(0, lastAssistant + 1), opts),
|
|
3870
|
+
forkable: true
|
|
3871
|
+
};
|
|
3872
|
+
}
|
|
3873
|
+
function findForkParent(opts) {
|
|
3874
|
+
const signature = forkSiblingSignature(opts.sessionKey);
|
|
3875
|
+
if (!signature) return null;
|
|
3876
|
+
const { history, forkable } = splitForkHistory(opts.prompt);
|
|
3877
|
+
if (!forkable || history.length === 0) return null;
|
|
3878
|
+
let best = null;
|
|
3879
|
+
let bestLength = 0;
|
|
3880
|
+
let bestSeq = -1;
|
|
3881
|
+
for (const [key, fingerprint] of fingerprints) {
|
|
3882
|
+
if (key === opts.sessionKey) continue;
|
|
3883
|
+
if (fingerprint.cliPath !== opts.cliPath) continue;
|
|
3884
|
+
if (forkSiblingSignature(key) !== signature) continue;
|
|
3885
|
+
const recorded = fingerprint.entries;
|
|
3886
|
+
if (recorded.length === 0 || recorded.length > history.length) continue;
|
|
3887
|
+
if (!isPrefix(recorded, history)) continue;
|
|
3888
|
+
if (!tailIsReplyOnly(history, recorded.length)) continue;
|
|
3889
|
+
const claudeSessionId = opts.lookupClaudeSessionId(key);
|
|
3890
|
+
if (!claudeSessionId) continue;
|
|
3891
|
+
if (opts.isBusy(key)) continue;
|
|
3892
|
+
if (recorded.length > bestLength || recorded.length === bestLength && fingerprint.seq > bestSeq) {
|
|
3893
|
+
best = { parentKey: key, claudeSessionId, matched: recorded.length };
|
|
3894
|
+
bestLength = recorded.length;
|
|
3895
|
+
bestSeq = fingerprint.seq;
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
if (best) {
|
|
3899
|
+
log.info("found a fork parent for this conversation", {
|
|
3900
|
+
sessionKey: opts.sessionKey,
|
|
3901
|
+
parentKey: best.parentKey,
|
|
3902
|
+
matchedMessages: best.matched,
|
|
3903
|
+
historyMessages: history.length
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3906
|
+
return best;
|
|
3907
|
+
}
|
|
3908
|
+
function continuesRecordedConversation(recorded, prompt, opts) {
|
|
3909
|
+
const { history, forkable } = splitForkHistory(prompt, opts);
|
|
3910
|
+
if (!forkable || recorded.length === 0 || recorded.length > history.length) return false;
|
|
3911
|
+
return isPrefix(recorded, history) && tailIsReplyOnly(history, recorded.length);
|
|
3912
|
+
}
|
|
3913
|
+
function isPrefix(recorded, history) {
|
|
3914
|
+
for (let i = 0; i < recorded.length; i++) {
|
|
3915
|
+
if (recorded[i].role !== history[i].role) return false;
|
|
3916
|
+
if (recorded[i].digest !== history[i].digest) return false;
|
|
3917
|
+
}
|
|
3918
|
+
return true;
|
|
3919
|
+
}
|
|
3920
|
+
function tailIsReplyOnly(history, from) {
|
|
3921
|
+
for (let i = from; i < history.length; i++) {
|
|
3922
|
+
if (history[i].role === "user") return false;
|
|
3923
|
+
}
|
|
3924
|
+
return true;
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
// src/conversation-gap.ts
|
|
3928
|
+
var served = /* @__PURE__ */ new Map();
|
|
3929
|
+
var servedSeq = 0;
|
|
3930
|
+
var MAX_SERVED_CONVERSATIONS = 64;
|
|
3931
|
+
function recordServedConversation(sessionKey2, prompt) {
|
|
3932
|
+
const chain = conversationDigests(prompt, { assistantContent: false });
|
|
3933
|
+
if (chain.length === 0) return;
|
|
3934
|
+
served.delete(sessionKey2);
|
|
3935
|
+
served.set(sessionKey2, { chain, seq: ++servedSeq });
|
|
3936
|
+
while (served.size > MAX_SERVED_CONVERSATIONS) {
|
|
3937
|
+
const oldest = served.keys().next();
|
|
3938
|
+
if (oldest.done) break;
|
|
3939
|
+
served.delete(oldest.value);
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
function forgetServedConversation(sessionKey2) {
|
|
3943
|
+
served.delete(sessionKey2);
|
|
3944
|
+
}
|
|
3945
|
+
function moveServedConversation(fromKey, toKey) {
|
|
3946
|
+
const record = served.get(fromKey);
|
|
3947
|
+
if (!record) return;
|
|
3948
|
+
served.delete(fromKey);
|
|
3949
|
+
served.delete(toKey);
|
|
3950
|
+
served.set(toKey, { ...record, seq: ++servedSeq });
|
|
3951
|
+
}
|
|
3952
|
+
function classifyConversationChange(sessionKey2, prompt) {
|
|
3953
|
+
const record = served.get(sessionKey2);
|
|
3954
|
+
if (!record) return null;
|
|
3955
|
+
const recorded = record.chain;
|
|
3956
|
+
if (recorded.length === 0) return null;
|
|
3957
|
+
const messages = prompt;
|
|
3958
|
+
let lastAssistant = -1;
|
|
3959
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3960
|
+
if (messages[i].role === "assistant") {
|
|
3961
|
+
lastAssistant = i;
|
|
3962
|
+
break;
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
if (lastAssistant < 0) return null;
|
|
3966
|
+
const history = [];
|
|
3967
|
+
for (let i = 0; i <= lastAssistant; i++) {
|
|
3968
|
+
const msg = messages[i];
|
|
3969
|
+
if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "tool") continue;
|
|
3970
|
+
const [entry] = conversationDigests([msg], { assistantContent: false });
|
|
3971
|
+
history.push({ index: i, role: entry.role, digest: entry.digest });
|
|
3972
|
+
}
|
|
3973
|
+
let shared = 0;
|
|
3974
|
+
while (shared < recorded.length && shared < history.length && recorded[shared].role === history[shared].role && recorded[shared].digest === history[shared].digest) {
|
|
3975
|
+
shared++;
|
|
3976
|
+
}
|
|
3977
|
+
if (shared < recorded.length) {
|
|
3978
|
+
if (shared === 0) return null;
|
|
3979
|
+
let removedUserMessages = 0;
|
|
3980
|
+
for (let i = shared; i < recorded.length; i++) {
|
|
3981
|
+
if (recorded[i].role === "user") removedUserMessages++;
|
|
3982
|
+
}
|
|
3983
|
+
if (removedUserMessages === 0) return null;
|
|
3984
|
+
log.debug("this claude session holds turns the conversation no longer has", {
|
|
3985
|
+
sessionKey: sessionKey2,
|
|
3986
|
+
recorded: recorded.length,
|
|
3987
|
+
shared,
|
|
3988
|
+
removedUserMessages
|
|
3989
|
+
});
|
|
3990
|
+
return {
|
|
3991
|
+
kind: "rewound",
|
|
3992
|
+
sharedMessages: shared,
|
|
3993
|
+
removedMessages: recorded.length - shared,
|
|
3994
|
+
removedUserMessages
|
|
3995
|
+
};
|
|
3996
|
+
}
|
|
3997
|
+
let firstUser = -1;
|
|
3998
|
+
for (let i = recorded.length; i < history.length; i++) {
|
|
3999
|
+
if (history[i].role === "user") {
|
|
4000
|
+
firstUser = i;
|
|
4001
|
+
break;
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
if (firstUser < 0) return null;
|
|
4005
|
+
const from = history[firstUser].index;
|
|
4006
|
+
const missed = messages.slice(from, lastAssistant + 1);
|
|
4007
|
+
let userMessages = 0;
|
|
4008
|
+
for (let i = firstUser; i < history.length; i++) {
|
|
4009
|
+
if (history[i].role === "user") userMessages++;
|
|
4010
|
+
}
|
|
4011
|
+
log.debug("this claude session missed part of the conversation", {
|
|
4012
|
+
sessionKey: sessionKey2,
|
|
4013
|
+
recorded: recorded.length,
|
|
4014
|
+
missed: missed.length,
|
|
4015
|
+
userMessages
|
|
4016
|
+
});
|
|
4017
|
+
return { kind: "missed", messages: missed, userMessages };
|
|
4018
|
+
}
|
|
4019
|
+
var CONVERSATION_REWOUND_MARKER = "\u258C **conversation rewound:**";
|
|
4020
|
+
function formatConversationRewoundNote() {
|
|
4021
|
+
return `
|
|
4022
|
+
${CONVERSATION_REWOUND_MARKER} this conversation was edited or undone, so the Claude session answering it still held the turns that were removed. This turn replays the thread as it stands now into a fresh Claude session, which no longer has them.
|
|
4023
|
+
`;
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
// src/cli-version.ts
|
|
4027
|
+
import { execFile } from "child_process";
|
|
4028
|
+
import { promisify } from "util";
|
|
4029
|
+
|
|
4030
|
+
// src/windows-spawn.ts
|
|
4031
|
+
import * as fs2 from "fs";
|
|
4032
|
+
import * as path4 from "path";
|
|
4033
|
+
var DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
|
|
4034
|
+
function envValue(env, name) {
|
|
4035
|
+
const direct = env[name];
|
|
4036
|
+
if (direct !== void 0) return direct;
|
|
4037
|
+
const match = Object.keys(env).find((key) => key.toLowerCase() === name.toLowerCase());
|
|
4038
|
+
return match === void 0 ? void 0 : env[match];
|
|
4039
|
+
}
|
|
4040
|
+
function defaultIsFile(candidate) {
|
|
4041
|
+
try {
|
|
4042
|
+
return fs2.statSync(candidate).isFile();
|
|
4043
|
+
} catch {
|
|
4044
|
+
return false;
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
function executableExtensions(env = process.env) {
|
|
4048
|
+
const raw = envValue(env, "PATHEXT") ?? DEFAULT_PATHEXT;
|
|
4049
|
+
return raw.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean).map((ext) => ext.startsWith(".") ? ext : `.${ext}`);
|
|
4050
|
+
}
|
|
4051
|
+
function isBatchFile(file) {
|
|
4052
|
+
const ext = path4.win32.extname(file).toLowerCase();
|
|
4053
|
+
return ext === ".cmd" || ext === ".bat";
|
|
4054
|
+
}
|
|
4055
|
+
function resolveWindowsCommand(command, deps = {}) {
|
|
4056
|
+
const env = deps.env ?? process.env;
|
|
4057
|
+
const isFile = deps.isFile ?? defaultIsFile;
|
|
4058
|
+
const extensions = executableExtensions(env);
|
|
4059
|
+
const candidatesFor = (base) => {
|
|
4060
|
+
const ext = path4.win32.extname(base).toLowerCase();
|
|
4061
|
+
if (ext && extensions.includes(ext)) return [base];
|
|
4062
|
+
return [...extensions.map((candidate) => base + candidate), base];
|
|
4063
|
+
};
|
|
4064
|
+
if (command.includes("\\") || command.includes("/")) {
|
|
4065
|
+
for (const candidate of candidatesFor(command)) {
|
|
4066
|
+
if (isFile(candidate)) return candidate;
|
|
4067
|
+
}
|
|
4068
|
+
return null;
|
|
4069
|
+
}
|
|
4070
|
+
const pathValue = envValue(env, "PATH") ?? "";
|
|
4071
|
+
for (const entry of pathValue.split(path4.win32.delimiter)) {
|
|
4072
|
+
const directory = entry.trim().replace(/^"(.*)"$/, "$1");
|
|
4073
|
+
if (!directory) continue;
|
|
4074
|
+
for (const candidate of candidatesFor(path4.win32.join(directory, command))) {
|
|
4075
|
+
if (isFile(candidate)) return candidate;
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
return null;
|
|
4079
|
+
}
|
|
4080
|
+
function quoteBatchArgument(arg) {
|
|
4081
|
+
if (/[\r\n]/.test(arg)) {
|
|
4082
|
+
throw new Error("cmd.exe cannot carry a newline in an argument");
|
|
4083
|
+
}
|
|
4084
|
+
let out = '"';
|
|
4085
|
+
let backslashes = 0;
|
|
4086
|
+
for (const char of arg) {
|
|
4087
|
+
if (char === "\\") {
|
|
4088
|
+
backslashes += 1;
|
|
4089
|
+
continue;
|
|
4090
|
+
}
|
|
4091
|
+
if (char === '"') {
|
|
4092
|
+
out += "\\".repeat(backslashes * 2) + '""';
|
|
4093
|
+
backslashes = 0;
|
|
4094
|
+
continue;
|
|
4095
|
+
}
|
|
4096
|
+
out += "\\".repeat(backslashes) + char;
|
|
4097
|
+
backslashes = 0;
|
|
4098
|
+
}
|
|
4099
|
+
return `${out}${"\\".repeat(backslashes * 2)}"`;
|
|
4100
|
+
}
|
|
4101
|
+
function planCmdInvocation(file, args, env = process.env) {
|
|
4102
|
+
const comspec = envValue(env, "ComSpec") ?? "cmd.exe";
|
|
4103
|
+
const line = [`"${path4.win32.normalize(file)}"`, ...args.map(quoteBatchArgument)];
|
|
4104
|
+
return {
|
|
4105
|
+
file: comspec,
|
|
4106
|
+
args: ["/d", "/s", "/c", `"${line.join(" ")}"`],
|
|
4107
|
+
windowsVerbatimArguments: true
|
|
4108
|
+
};
|
|
4109
|
+
}
|
|
4110
|
+
function planClaudeSpawn(command, args, deps = {}) {
|
|
4111
|
+
const platform2 = deps.platform ?? process.platform;
|
|
4112
|
+
if (platform2 !== "win32") return { file: command, args: [...args] };
|
|
4113
|
+
const env = deps.env ?? process.env;
|
|
4114
|
+
const resolved = resolveWindowsCommand(command, deps) ?? command;
|
|
4115
|
+
if (isBatchFile(resolved)) return planCmdInvocation(resolved, args, env);
|
|
4116
|
+
return { file: resolved, args: [...args] };
|
|
3513
4117
|
}
|
|
3514
4118
|
|
|
3515
4119
|
// src/cli-version.ts
|
|
@@ -3524,7 +4128,7 @@ function probeExec(cliPath, args, options) {
|
|
|
3524
4128
|
windowsVerbatimArguments: plan.windowsVerbatimArguments
|
|
3525
4129
|
});
|
|
3526
4130
|
}
|
|
3527
|
-
var
|
|
4131
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
3528
4132
|
var PROBE_TIMEOUT_MS = 5e3;
|
|
3529
4133
|
var probeTimeoutMs = PROBE_TIMEOUT_MS;
|
|
3530
4134
|
var MAX_DEADLINE_REPROBES = 2;
|
|
@@ -3569,8 +4173,8 @@ function spawnMcpTimeoutEnv(inherited = process.env) {
|
|
|
3569
4173
|
return inherited.MCP_TIMEOUT === void 0 ? { MCP_TIMEOUT: String(SPAWN_MCP_TIMEOUT_MS) } : {};
|
|
3570
4174
|
}
|
|
3571
4175
|
function detectCliVersion(cliPath) {
|
|
3572
|
-
const
|
|
3573
|
-
if (
|
|
4176
|
+
const cached2 = cache2.get(cliPath);
|
|
4177
|
+
if (cached2) return cached2;
|
|
3574
4178
|
let deadlineKill = false;
|
|
3575
4179
|
const promise = (async () => {
|
|
3576
4180
|
try {
|
|
@@ -3606,8 +4210,8 @@ function detectCliVersion(cliPath) {
|
|
|
3606
4210
|
return null;
|
|
3607
4211
|
}
|
|
3608
4212
|
})();
|
|
3609
|
-
|
|
3610
|
-
forgetDeadlineKill(
|
|
4213
|
+
cache2.set(cliPath, promise);
|
|
4214
|
+
forgetDeadlineKill(cache2, cliPath, promise, () => deadlineKill);
|
|
3611
4215
|
return promise;
|
|
3612
4216
|
}
|
|
3613
4217
|
function gte(v, target) {
|
|
@@ -3639,6 +4243,10 @@ function cliSupportsInteractiveBypass(v) {
|
|
|
3639
4243
|
if (!v) return false;
|
|
3640
4244
|
return gte(v, { major: 2, minor: 1, patch: 263 });
|
|
3641
4245
|
}
|
|
4246
|
+
function cliSupportsMaxTurns(v) {
|
|
4247
|
+
if (!v) return false;
|
|
4248
|
+
return gte(v, { major: 2, minor: 1, patch: 280 });
|
|
4249
|
+
}
|
|
3642
4250
|
var INTERACTIVE_MEASURED_CLI = "2.1.293";
|
|
3643
4251
|
var INTERACTIVE_MEASURED = { major: 2, minor: 1, patch: 293 };
|
|
3644
4252
|
function isUnmeasuredInteractiveCli(v) {
|
|
@@ -3666,8 +4274,8 @@ var flagSupport = /* @__PURE__ */ new Map();
|
|
|
3666
4274
|
var headlessSupport = /* @__PURE__ */ new Map();
|
|
3667
4275
|
function detectHeadlessSupport(cliPath) {
|
|
3668
4276
|
const key = `${cliPath}\0headless`;
|
|
3669
|
-
const
|
|
3670
|
-
if (
|
|
4277
|
+
const cached2 = headlessSupport.get(key);
|
|
4278
|
+
if (cached2) return cached2;
|
|
3671
4279
|
let deadlineKill = false;
|
|
3672
4280
|
const promise = (async () => {
|
|
3673
4281
|
try {
|
|
@@ -3702,8 +4310,8 @@ function detectHeadlessSupport(cliPath) {
|
|
|
3702
4310
|
}
|
|
3703
4311
|
function detectCliSupportsFlag(cliPath, flag) {
|
|
3704
4312
|
const key = `${cliPath}\0${flag}`;
|
|
3705
|
-
const
|
|
3706
|
-
if (
|
|
4313
|
+
const cached2 = flagSupport.get(key);
|
|
4314
|
+
if (cached2) return cached2;
|
|
3707
4315
|
let deadlineKill = false;
|
|
3708
4316
|
const promise = (async () => {
|
|
3709
4317
|
try {
|
|
@@ -4383,6 +4991,21 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
4383
4991
|
writeToolCallResult(res, requestId, intercepted);
|
|
4384
4992
|
return;
|
|
4385
4993
|
}
|
|
4994
|
+
if (options.admit) {
|
|
4995
|
+
let refusal = null;
|
|
4996
|
+
try {
|
|
4997
|
+
refusal = await options.admit(toolName, input);
|
|
4998
|
+
} catch (admitError) {
|
|
4999
|
+
log.warn("proxy-mcp admission check failed; letting the call through", {
|
|
5000
|
+
toolName,
|
|
5001
|
+
error: admitError instanceof Error ? admitError.message : String(admitError)
|
|
5002
|
+
});
|
|
5003
|
+
}
|
|
5004
|
+
if (refusal) {
|
|
5005
|
+
writeToolCallResult(res, requestId, refusal);
|
|
5006
|
+
return;
|
|
5007
|
+
}
|
|
5008
|
+
}
|
|
4386
5009
|
const callId = crypto.randomUUID();
|
|
4387
5010
|
log.info("proxy-mcp tool call received", {
|
|
4388
5011
|
callId,
|
|
@@ -4403,12 +5026,12 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
4403
5026
|
});
|
|
4404
5027
|
let timer = null;
|
|
4405
5028
|
const result = await new Promise(
|
|
4406
|
-
(
|
|
5029
|
+
(resolve7, reject2) => {
|
|
4407
5030
|
const entry = {
|
|
4408
5031
|
id: callId,
|
|
4409
5032
|
toolName,
|
|
4410
5033
|
input,
|
|
4411
|
-
resolve:
|
|
5034
|
+
resolve: resolve7,
|
|
4412
5035
|
reject: reject2,
|
|
4413
5036
|
channel
|
|
4414
5037
|
};
|
|
@@ -4497,11 +5120,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
4497
5120
|
}
|
|
4498
5121
|
}
|
|
4499
5122
|
});
|
|
4500
|
-
await new Promise((
|
|
5123
|
+
await new Promise((resolve7, reject2) => {
|
|
4501
5124
|
server2.once("error", reject2);
|
|
4502
5125
|
server2.listen(0, "127.0.0.1", () => {
|
|
4503
5126
|
server2.off("error", reject2);
|
|
4504
|
-
|
|
5127
|
+
resolve7();
|
|
4505
5128
|
});
|
|
4506
5129
|
});
|
|
4507
5130
|
const addr = server2.address();
|
|
@@ -4558,8 +5181,8 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
4558
5181
|
entry.reject(new Error(SERVER_CLOSED_MESSAGE));
|
|
4559
5182
|
}
|
|
4560
5183
|
pending2.clear();
|
|
4561
|
-
await new Promise((
|
|
4562
|
-
server2.close(() =>
|
|
5184
|
+
await new Promise((resolve7) => {
|
|
5185
|
+
server2.close(() => resolve7());
|
|
4563
5186
|
});
|
|
4564
5187
|
if (configFilePath) {
|
|
4565
5188
|
try {
|
|
@@ -4615,6 +5238,7 @@ function resolveDisallowedTools(options) {
|
|
|
4615
5238
|
for (const name of disallowedToolFlags(options.proxyTools ?? [])) push(name);
|
|
4616
5239
|
for (const name of options.extraDisallowedTools ?? []) push(String(name));
|
|
4617
5240
|
if (options.disableWebSearch) push("WebSearch");
|
|
5241
|
+
for (const rule of options.readRules ?? []) push(String(rule));
|
|
4618
5242
|
return out;
|
|
4619
5243
|
}
|
|
4620
5244
|
function resolveProxyOpencodeToolDefs(options) {
|
|
@@ -4731,10 +5355,10 @@ function resolveMcpProxyToolDefs(options) {
|
|
|
4731
5355
|
return { defs, coveredServers, matchedServers };
|
|
4732
5356
|
}
|
|
4733
5357
|
function readBody(req) {
|
|
4734
|
-
return new Promise((
|
|
5358
|
+
return new Promise((resolve7, reject) => {
|
|
4735
5359
|
const chunks = [];
|
|
4736
5360
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
4737
|
-
req.on("end", () =>
|
|
5361
|
+
req.on("end", () => resolve7(Buffer.concat(chunks).toString("utf8")));
|
|
4738
5362
|
req.on("error", reject);
|
|
4739
5363
|
});
|
|
4740
5364
|
}
|
|
@@ -5623,6 +6247,7 @@ var opencodeClient = null;
|
|
|
5623
6247
|
function setOpencodeClient(client) {
|
|
5624
6248
|
if (client && typeof client === "object") {
|
|
5625
6249
|
opencodeClient = client;
|
|
6250
|
+
childSessionAnswers.clear();
|
|
5626
6251
|
}
|
|
5627
6252
|
}
|
|
5628
6253
|
function getOpencodeClient() {
|
|
@@ -5690,6 +6315,20 @@ async function fetchSessionParentId(sessionID) {
|
|
|
5690
6315
|
return void 0;
|
|
5691
6316
|
}
|
|
5692
6317
|
}
|
|
6318
|
+
var childSessionAnswers = /* @__PURE__ */ new Map();
|
|
6319
|
+
var MAX_CHILD_SESSION_ANSWERS = 256;
|
|
6320
|
+
function watchChildSession(sessionID) {
|
|
6321
|
+
if (!sessionID || sessionID === "default") return () => false;
|
|
6322
|
+
const known = childSessionAnswers.get(sessionID);
|
|
6323
|
+
if (known !== void 0) return () => known;
|
|
6324
|
+
let answer;
|
|
6325
|
+
void fetchSessionParentId(sessionID).then((parentID) => {
|
|
6326
|
+
answer = !!parentID;
|
|
6327
|
+
if (childSessionAnswers.size >= MAX_CHILD_SESSION_ANSWERS) childSessionAnswers.clear();
|
|
6328
|
+
childSessionAnswers.set(sessionID, answer);
|
|
6329
|
+
});
|
|
6330
|
+
return () => answer;
|
|
6331
|
+
}
|
|
5693
6332
|
async function fetchSessionRunState(sessionID, directory) {
|
|
5694
6333
|
if (!sessionID || sessionID === "default") return "unknown";
|
|
5695
6334
|
const status = opencodeClient?.session?.status;
|
|
@@ -5726,7 +6365,7 @@ async function settleSessionRunState(sessionID, options = {}) {
|
|
|
5726
6365
|
if (state === "busy") return "busy";
|
|
5727
6366
|
if (state !== "unknown") last = state;
|
|
5728
6367
|
if (Date.now() - started2 >= windowMs) return last;
|
|
5729
|
-
await new Promise((
|
|
6368
|
+
await new Promise((resolve7) => setTimeout(resolve7, pollMs));
|
|
5730
6369
|
}
|
|
5731
6370
|
}
|
|
5732
6371
|
var DEFAULT_MCP_CONNECT_WAIT_MS = 3e3;
|
|
@@ -5779,7 +6418,7 @@ async function waitForPendingMcpServers(first, budgetMs, signal) {
|
|
|
5779
6418
|
for (; ; ) {
|
|
5780
6419
|
if (Date.now() - started2 >= budgetMs) break;
|
|
5781
6420
|
if (signal?.aborted) break;
|
|
5782
|
-
await new Promise((
|
|
6421
|
+
await new Promise((resolve7) => setTimeout(resolve7, MCP_CONNECT_POLL_MS));
|
|
5783
6422
|
if (signal?.aborted) break;
|
|
5784
6423
|
const next = await fetchRuntimeMcpStatus();
|
|
5785
6424
|
if (!next) break;
|
|
@@ -6008,8 +6647,8 @@ async function countOtherRunningWithin(exclude, budgetMs) {
|
|
|
6008
6647
|
worker
|
|
6009
6648
|
);
|
|
6010
6649
|
let timer;
|
|
6011
|
-
const deadline = new Promise((
|
|
6012
|
-
timer = setTimeout(() =>
|
|
6650
|
+
const deadline = new Promise((resolve7) => {
|
|
6651
|
+
timer = setTimeout(() => resolve7("over"), budgetMs);
|
|
6013
6652
|
timer.unref?.();
|
|
6014
6653
|
});
|
|
6015
6654
|
try {
|
|
@@ -6040,6 +6679,41 @@ async function withBackgroundRunningCount(result, budgetMs = BACKGROUND_COUNT_BU
|
|
|
6040
6679
|
}
|
|
6041
6680
|
return { ...result, text: result.text + formatBackgroundRunningLine(others, dispatched.length) };
|
|
6042
6681
|
}
|
|
6682
|
+
var BACKGROUND_CAP_BUDGET_MS = 2e3;
|
|
6683
|
+
function requestedBackgroundChildren(toolName, input) {
|
|
6684
|
+
const isBackground = (value) => value === true || value === "true";
|
|
6685
|
+
if (toolName === "task") return isBackground(input.background) ? 1 : 0;
|
|
6686
|
+
if (toolName === "task_batch" && Array.isArray(input.tasks)) {
|
|
6687
|
+
return input.tasks.filter(
|
|
6688
|
+
(task) => !!task && typeof task === "object" && isBackground(task.background)
|
|
6689
|
+
).length;
|
|
6690
|
+
}
|
|
6691
|
+
return 0;
|
|
6692
|
+
}
|
|
6693
|
+
async function admitBackgroundDispatch(toolName, input, cap, budgetMs = BACKGROUND_CAP_BUDGET_MS) {
|
|
6694
|
+
const requested = requestedBackgroundChildren(toolName, input);
|
|
6695
|
+
if (requested === 0) return null;
|
|
6696
|
+
const running = await countOtherRunningWithin(/* @__PURE__ */ new Set(), budgetMs);
|
|
6697
|
+
if (running !== void 0 && running + requested <= cap) return null;
|
|
6698
|
+
log.notice("background dispatch refused by maxBackgroundSubagents", {
|
|
6699
|
+
toolName,
|
|
6700
|
+
cap,
|
|
6701
|
+
requested,
|
|
6702
|
+
...running === void 0 ? { countUnreadable: true } : { running }
|
|
6703
|
+
});
|
|
6704
|
+
return {
|
|
6705
|
+
kind: "error",
|
|
6706
|
+
message: formatBackgroundCapRefusal({ toolName, cap, running, requested, budgetMs })
|
|
6707
|
+
};
|
|
6708
|
+
}
|
|
6709
|
+
function formatBackgroundCapRefusal(opts) {
|
|
6710
|
+
const batch = opts.toolName === "task_batch";
|
|
6711
|
+
const what = batch ? `This task_batch was not started, none of its tasks: a batch is refused whole.` : `This background task was not started.`;
|
|
6712
|
+
const why = opts.running === void 0 ? `\`maxBackgroundSubagents\` allows ${opts.cap} background subagent${opts.cap === 1 ? "" : "s"} at once, and the running count could not be read within ${Math.round(
|
|
6713
|
+
(opts.budgetMs ?? BACKGROUND_CAP_BUDGET_MS) / 1e3
|
|
6714
|
+
)} s, so the cap could not be checked.` : `\`maxBackgroundSubagents\` allows ${opts.cap} background subagent${opts.cap === 1 ? "" : "s"} at once, ${opts.running} ${opts.running === 1 ? "is" : "are"} running now, and this ${batch ? "batch" : "dispatch"} would start ${opts.requested} more.`;
|
|
6715
|
+
return `Background dispatch refused. ${why} ${what} Run this work in the foreground instead (omit \`background\`), which is never capped, or keep working and dispatch it later: a running subagent's result arrives as a new message when it finishes. Do not sleep or poll for room.`;
|
|
6716
|
+
}
|
|
6043
6717
|
function formatBackgroundRunningLine(others, dispatched) {
|
|
6044
6718
|
const total = others + dispatched;
|
|
6045
6719
|
const including = dispatched === 1 ? "this one" : `the ${dispatched} just started`;
|
|
@@ -6121,288 +6795,80 @@ opencode has no assistant reply for this task yet and it is not running. It may
|
|
|
6121
6795
|
};
|
|
6122
6796
|
}
|
|
6123
6797
|
if (reply.error) {
|
|
6124
|
-
markCollected(options.sessionKey, taskId);
|
|
6125
|
-
return {
|
|
6126
|
-
kind: "text",
|
|
6127
|
-
isError: true,
|
|
6128
|
-
text: `<task id="${taskId}" state="error">
|
|
6129
|
-
${reply.error}
|
|
6130
|
-
</task>`
|
|
6131
|
-
};
|
|
6132
|
-
}
|
|
6133
|
-
if (hasCollectedBackgroundTask(options.sessionKey, taskId)) {
|
|
6134
|
-
return {
|
|
6135
|
-
kind: "text",
|
|
6136
|
-
text: `<task id="${taskId}" state="completed">
|
|
6137
|
-
Already delivered to this conversation earlier (by this tool or by opencode's completion notification). Scroll back for the result rather than collecting it again.
|
|
6138
|
-
</task>`
|
|
6139
|
-
};
|
|
6140
|
-
}
|
|
6141
|
-
markCollected(options.sessionKey, taskId);
|
|
6142
|
-
log.info("collected background subagent result", {
|
|
6143
|
-
sessionKey: options.sessionKey,
|
|
6144
|
-
taskId,
|
|
6145
|
-
textLength: reply.text.length
|
|
6146
|
-
});
|
|
6147
|
-
return {
|
|
6148
|
-
kind: "text",
|
|
6149
|
-
text: `<task id="${taskId}" state="completed">
|
|
6150
|
-
<task_result>
|
|
6151
|
-
${reply.text || "(the subagent produced no text)"}
|
|
6152
|
-
</task_result>
|
|
6153
|
-
</task>`
|
|
6154
|
-
};
|
|
6155
|
-
}
|
|
6156
|
-
async function cancelBackgroundTask(input, options) {
|
|
6157
|
-
const taskId = readTaskId(input);
|
|
6158
|
-
if (!taskId) return { kind: "error", message: MISSING_TASK_ID };
|
|
6159
|
-
const refused = await guardParent(taskId, options.callerSessionId);
|
|
6160
|
-
if (refused) return { kind: "error", message: refused };
|
|
6161
|
-
const running = isBackgroundTaskRunning(
|
|
6162
|
-
await fetchSessionRunState(taskId),
|
|
6163
|
-
await fetchSessionReplies(taskId) ?? []
|
|
6164
|
-
);
|
|
6165
|
-
const aborted = await abortSession(taskId);
|
|
6166
|
-
if (!aborted) {
|
|
6167
|
-
return {
|
|
6168
|
-
kind: "error",
|
|
6169
|
-
message: `Could not cancel background task ${taskId}: opencode did not accept the abort. It may have already finished, or this opencode build does not expose session abort.`
|
|
6170
|
-
};
|
|
6171
|
-
}
|
|
6172
|
-
collected.get(options.sessionKey)?.delete(taskId);
|
|
6173
|
-
mark(cancelled, options.sessionKey, taskId);
|
|
6174
|
-
log.info("cancelled background subagent", {
|
|
6175
|
-
sessionKey: options.sessionKey,
|
|
6176
|
-
taskId,
|
|
6177
|
-
wasRunning: running
|
|
6178
|
-
});
|
|
6179
|
-
return {
|
|
6180
|
-
kind: "text",
|
|
6181
|
-
text: `<task id="${taskId}" state="cancelled">
|
|
6182
|
-
` + (running ? "Stopped. No completion notification will arrive for it." : "It was not running when the cancel was sent, so nothing was interrupted. No completion notification will arrive for it.") + "\n</task>"
|
|
6183
|
-
};
|
|
6184
|
-
}
|
|
6185
|
-
|
|
6186
|
-
// src/session-fork.ts
|
|
6187
|
-
import { createHash as createHash3 } from "crypto";
|
|
6188
|
-
var fingerprints = /* @__PURE__ */ new Map();
|
|
6189
|
-
var fingerprintSeq = 0;
|
|
6190
|
-
var MAX_FORK_FINGERPRINTS = 64;
|
|
6191
|
-
function hashPart(hash, part) {
|
|
6192
|
-
const p = part;
|
|
6193
|
-
const type = typeof p?.type === "string" ? p.type : "unknown";
|
|
6194
|
-
switch (type) {
|
|
6195
|
-
case "text":
|
|
6196
|
-
hash.update("text\0");
|
|
6197
|
-
hash.update(String(p?.text ?? ""));
|
|
6198
|
-
break;
|
|
6199
|
-
case "tool-call":
|
|
6200
|
-
hash.update("call\0");
|
|
6201
|
-
hash.update(String(p?.toolName ?? ""));
|
|
6202
|
-
hash.update("\0");
|
|
6203
|
-
hash.update(stableJson(p?.input));
|
|
6204
|
-
break;
|
|
6205
|
-
case "tool-result":
|
|
6206
|
-
hash.update("result\0");
|
|
6207
|
-
hash.update(String(p?.toolName ?? ""));
|
|
6208
|
-
hash.update("\0");
|
|
6209
|
-
hash.update(stableJson(p?.output));
|
|
6210
|
-
break;
|
|
6211
|
-
case "file":
|
|
6212
|
-
case "image": {
|
|
6213
|
-
hash.update(type);
|
|
6214
|
-
hash.update("\0");
|
|
6215
|
-
hash.update(String(p?.mediaType ?? ""));
|
|
6216
|
-
hash.update("\0");
|
|
6217
|
-
const data = p?.data ?? p?.image;
|
|
6218
|
-
if (typeof data === "string") hash.update(data);
|
|
6219
|
-
else if (data instanceof Uint8Array) hash.update(data);
|
|
6220
|
-
else if (data instanceof URL) hash.update(data.href);
|
|
6221
|
-
else hash.update(stableJson(data));
|
|
6222
|
-
break;
|
|
6223
|
-
}
|
|
6224
|
-
default:
|
|
6225
|
-
hash.update("other\0");
|
|
6226
|
-
hash.update(type);
|
|
6227
|
-
break;
|
|
6228
|
-
}
|
|
6229
|
-
hash.update("");
|
|
6230
|
-
}
|
|
6231
|
-
function stableJson(value) {
|
|
6232
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
6233
|
-
const walk = (node) => {
|
|
6234
|
-
if (node === null || typeof node !== "object") return node;
|
|
6235
|
-
if (seen.has(node)) return "[circular]";
|
|
6236
|
-
seen.add(node);
|
|
6237
|
-
if (Array.isArray(node)) return node.map(walk);
|
|
6238
|
-
const out = {};
|
|
6239
|
-
for (const key of Object.keys(node).sort()) {
|
|
6240
|
-
out[key] = walk(node[key]);
|
|
6241
|
-
}
|
|
6242
|
-
return out;
|
|
6243
|
-
};
|
|
6244
|
-
try {
|
|
6245
|
-
return JSON.stringify(walk(value)) ?? "null";
|
|
6246
|
-
} catch {
|
|
6247
|
-
return "[unserializable]";
|
|
6248
|
-
}
|
|
6249
|
-
}
|
|
6250
|
-
function messageDigest(msg) {
|
|
6251
|
-
const hash = createHash3("sha256");
|
|
6252
|
-
hash.update(msg.role);
|
|
6253
|
-
hash.update("\0");
|
|
6254
|
-
if (typeof msg.content === "string") {
|
|
6255
|
-
hash.update("text\0");
|
|
6256
|
-
hash.update(msg.content);
|
|
6257
|
-
hash.update("");
|
|
6258
|
-
} else if (Array.isArray(msg.content)) {
|
|
6259
|
-
for (const part of msg.content) hashPart(hash, part);
|
|
6260
|
-
}
|
|
6261
|
-
return hash.digest("hex").slice(0, 32);
|
|
6262
|
-
}
|
|
6263
|
-
var ASSISTANT_TURN_DIGEST = "assistant-turn";
|
|
6264
|
-
function conversationDigests(prompt, opts) {
|
|
6265
|
-
const out = [];
|
|
6266
|
-
for (const msg of prompt) {
|
|
6267
|
-
if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "tool") {
|
|
6268
|
-
continue;
|
|
6269
|
-
}
|
|
6270
|
-
out.push({
|
|
6271
|
-
role: msg.role,
|
|
6272
|
-
digest: opts?.assistantContent === false && msg.role === "assistant" ? ASSISTANT_TURN_DIGEST : messageDigest(msg)
|
|
6273
|
-
});
|
|
6274
|
-
}
|
|
6275
|
-
return out;
|
|
6276
|
-
}
|
|
6277
|
-
function recordForkFingerprint(sessionKey2, prompt, cliPath) {
|
|
6278
|
-
const entries = conversationDigests(prompt);
|
|
6279
|
-
if (entries.length === 0) return;
|
|
6280
|
-
fingerprints.delete(sessionKey2);
|
|
6281
|
-
fingerprints.set(sessionKey2, { entries, cliPath, seq: ++fingerprintSeq });
|
|
6282
|
-
while (fingerprints.size > MAX_FORK_FINGERPRINTS) {
|
|
6283
|
-
const oldest = fingerprints.keys().next();
|
|
6284
|
-
if (oldest.done) break;
|
|
6285
|
-
fingerprints.delete(oldest.value);
|
|
6286
|
-
}
|
|
6287
|
-
}
|
|
6288
|
-
function forgetForkFingerprint(sessionKey2) {
|
|
6289
|
-
fingerprints.delete(sessionKey2);
|
|
6290
|
-
}
|
|
6291
|
-
function forkSiblingSignature(sessionKey2) {
|
|
6292
|
-
const parts = sessionKey2.split("::");
|
|
6293
|
-
if (parts.length < 5) return null;
|
|
6294
|
-
if (parts[2] === "compaction") return null;
|
|
6295
|
-
const blanked = [...parts];
|
|
6296
|
-
blanked[3] = "*";
|
|
6297
|
-
return blanked.join("::");
|
|
6298
|
-
}
|
|
6299
|
-
function modelSiblingSignature(sessionKey2, accountProviders) {
|
|
6300
|
-
const parts = sessionKey2.split("::");
|
|
6301
|
-
if (parts.length < 5) return null;
|
|
6302
|
-
if (parts[2] === "compaction") return null;
|
|
6303
|
-
const withoutEffort = parts[parts.length - 1].startsWith("effort=") ? parts.slice(0, -1) : parts;
|
|
6304
|
-
if (withoutEffort.length < 5) return null;
|
|
6305
|
-
const blanked = [...withoutEffort];
|
|
6306
|
-
blanked[1] = "*";
|
|
6307
|
-
const signature = blanked.join("::");
|
|
6308
|
-
return accountProviders && accountProviders.size > 0 ? blankAccountProvider(signature, accountProviders) : signature;
|
|
6309
|
-
}
|
|
6310
|
-
function moveForkFingerprint(fromKey, toKey) {
|
|
6311
|
-
const record = fingerprints.get(fromKey);
|
|
6312
|
-
if (!record) return;
|
|
6313
|
-
fingerprints.delete(fromKey);
|
|
6314
|
-
fingerprints.delete(toKey);
|
|
6315
|
-
fingerprints.set(toKey, { ...record, seq: ++fingerprintSeq });
|
|
6316
|
-
}
|
|
6317
|
-
function splitForkHistory(prompt, opts) {
|
|
6318
|
-
const messages = prompt;
|
|
6319
|
-
let lastAssistant = -1;
|
|
6320
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
6321
|
-
if (messages[i].role === "assistant") {
|
|
6322
|
-
lastAssistant = i;
|
|
6323
|
-
break;
|
|
6324
|
-
}
|
|
6798
|
+
markCollected(options.sessionKey, taskId);
|
|
6799
|
+
return {
|
|
6800
|
+
kind: "text",
|
|
6801
|
+
isError: true,
|
|
6802
|
+
text: `<task id="${taskId}" state="error">
|
|
6803
|
+
${reply.error}
|
|
6804
|
+
</task>`
|
|
6805
|
+
};
|
|
6325
6806
|
}
|
|
6326
|
-
if (
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
}
|
|
6807
|
+
if (hasCollectedBackgroundTask(options.sessionKey, taskId)) {
|
|
6808
|
+
return {
|
|
6809
|
+
kind: "text",
|
|
6810
|
+
text: `<task id="${taskId}" state="completed">
|
|
6811
|
+
Already delivered to this conversation earlier (by this tool or by opencode's completion notification). Scroll back for the result rather than collecting it again.
|
|
6812
|
+
</task>`
|
|
6813
|
+
};
|
|
6334
6814
|
}
|
|
6815
|
+
markCollected(options.sessionKey, taskId);
|
|
6816
|
+
log.info("collected background subagent result", {
|
|
6817
|
+
sessionKey: options.sessionKey,
|
|
6818
|
+
taskId,
|
|
6819
|
+
textLength: reply.text.length
|
|
6820
|
+
});
|
|
6335
6821
|
return {
|
|
6336
|
-
|
|
6337
|
-
|
|
6822
|
+
kind: "text",
|
|
6823
|
+
text: `<task id="${taskId}" state="completed">
|
|
6824
|
+
<task_result>
|
|
6825
|
+
${reply.text || "(the subagent produced no text)"}
|
|
6826
|
+
</task_result>
|
|
6827
|
+
</task>`
|
|
6338
6828
|
};
|
|
6339
6829
|
}
|
|
6340
|
-
function
|
|
6341
|
-
const
|
|
6342
|
-
if (!
|
|
6343
|
-
const
|
|
6344
|
-
if (
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
if (!tailIsReplyOnly(history, recorded.length)) continue;
|
|
6356
|
-
const claudeSessionId = opts.lookupClaudeSessionId(key);
|
|
6357
|
-
if (!claudeSessionId) continue;
|
|
6358
|
-
if (opts.isBusy(key)) continue;
|
|
6359
|
-
if (recorded.length > bestLength || recorded.length === bestLength && fingerprint.seq > bestSeq) {
|
|
6360
|
-
best = { parentKey: key, claudeSessionId, matched: recorded.length };
|
|
6361
|
-
bestLength = recorded.length;
|
|
6362
|
-
bestSeq = fingerprint.seq;
|
|
6363
|
-
}
|
|
6364
|
-
}
|
|
6365
|
-
if (best) {
|
|
6366
|
-
log.info("found a fork parent for this conversation", {
|
|
6367
|
-
sessionKey: opts.sessionKey,
|
|
6368
|
-
parentKey: best.parentKey,
|
|
6369
|
-
matchedMessages: best.matched,
|
|
6370
|
-
historyMessages: history.length
|
|
6371
|
-
});
|
|
6372
|
-
}
|
|
6373
|
-
return best;
|
|
6374
|
-
}
|
|
6375
|
-
function continuesRecordedConversation(recorded, prompt, opts) {
|
|
6376
|
-
const { history, forkable } = splitForkHistory(prompt, opts);
|
|
6377
|
-
if (!forkable || recorded.length === 0 || recorded.length > history.length) return false;
|
|
6378
|
-
return isPrefix(recorded, history) && tailIsReplyOnly(history, recorded.length);
|
|
6379
|
-
}
|
|
6380
|
-
function isPrefix(recorded, history) {
|
|
6381
|
-
for (let i = 0; i < recorded.length; i++) {
|
|
6382
|
-
if (recorded[i].role !== history[i].role) return false;
|
|
6383
|
-
if (recorded[i].digest !== history[i].digest) return false;
|
|
6384
|
-
}
|
|
6385
|
-
return true;
|
|
6386
|
-
}
|
|
6387
|
-
function tailIsReplyOnly(history, from) {
|
|
6388
|
-
for (let i = from; i < history.length; i++) {
|
|
6389
|
-
if (history[i].role === "user") return false;
|
|
6830
|
+
async function cancelBackgroundTask(input, options) {
|
|
6831
|
+
const taskId = readTaskId(input);
|
|
6832
|
+
if (!taskId) return { kind: "error", message: MISSING_TASK_ID };
|
|
6833
|
+
const refused = await guardParent(taskId, options.callerSessionId);
|
|
6834
|
+
if (refused) return { kind: "error", message: refused };
|
|
6835
|
+
const running = isBackgroundTaskRunning(
|
|
6836
|
+
await fetchSessionRunState(taskId),
|
|
6837
|
+
await fetchSessionReplies(taskId) ?? []
|
|
6838
|
+
);
|
|
6839
|
+
const aborted = await abortSession(taskId);
|
|
6840
|
+
if (!aborted) {
|
|
6841
|
+
return {
|
|
6842
|
+
kind: "error",
|
|
6843
|
+
message: `Could not cancel background task ${taskId}: opencode did not accept the abort. It may have already finished, or this opencode build does not expose session abort.`
|
|
6844
|
+
};
|
|
6390
6845
|
}
|
|
6391
|
-
|
|
6846
|
+
collected.get(options.sessionKey)?.delete(taskId);
|
|
6847
|
+
mark(cancelled, options.sessionKey, taskId);
|
|
6848
|
+
log.info("cancelled background subagent", {
|
|
6849
|
+
sessionKey: options.sessionKey,
|
|
6850
|
+
taskId,
|
|
6851
|
+
wasRunning: running
|
|
6852
|
+
});
|
|
6853
|
+
return {
|
|
6854
|
+
kind: "text",
|
|
6855
|
+
text: `<task id="${taskId}" state="cancelled">
|
|
6856
|
+
` + (running ? "Stopped. No completion notification will arrive for it." : "It was not running when the cancel was sent, so nothing was interrupted. No completion notification will arrive for it.") + "\n</task>"
|
|
6857
|
+
};
|
|
6392
6858
|
}
|
|
6393
6859
|
|
|
6394
6860
|
// src/session-resume-store.ts
|
|
6395
|
-
import { existsSync as existsSync2, mkdirSync as
|
|
6396
|
-
import { homedir as
|
|
6397
|
-
import { join as
|
|
6861
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
6862
|
+
import { homedir as homedir7 } from "os";
|
|
6863
|
+
import { join as join9 } from "path";
|
|
6398
6864
|
var MAX_RESUME_RECORDS = 256;
|
|
6399
6865
|
var RESUME_RECORD_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
6400
6866
|
var records = null;
|
|
6401
|
-
var
|
|
6867
|
+
var pathOverride3 = null;
|
|
6402
6868
|
function resumeStorePath() {
|
|
6403
|
-
if (
|
|
6404
|
-
const stateRoot = process.env.XDG_STATE_HOME ||
|
|
6405
|
-
return
|
|
6869
|
+
if (pathOverride3) return pathOverride3;
|
|
6870
|
+
const stateRoot = process.env.XDG_STATE_HOME || join9(homedir7(), ".local", "state");
|
|
6871
|
+
return join9(stateRoot, "opencode-claude-code-plugin", "claude-sessions.json");
|
|
6406
6872
|
}
|
|
6407
6873
|
function isDigest(value) {
|
|
6408
6874
|
const entry = value;
|
|
@@ -6416,7 +6882,7 @@ function load2() {
|
|
|
6416
6882
|
if (records) return records;
|
|
6417
6883
|
records = /* @__PURE__ */ new Map();
|
|
6418
6884
|
try {
|
|
6419
|
-
const parsed = JSON.parse(
|
|
6885
|
+
const parsed = JSON.parse(readFileSync5(resumeStorePath(), "utf8"));
|
|
6420
6886
|
const entries = parsed && typeof parsed === "object" ? parsed.sessions : null;
|
|
6421
6887
|
if (entries && typeof entries === "object") {
|
|
6422
6888
|
const cutoff = Date.now() - RESUME_RECORD_MAX_AGE_MS;
|
|
@@ -6431,7 +6897,7 @@ function save2(update) {
|
|
|
6431
6897
|
const file = resumeStorePath();
|
|
6432
6898
|
const merged = /* @__PURE__ */ new Map();
|
|
6433
6899
|
try {
|
|
6434
|
-
const onDisk = JSON.parse(
|
|
6900
|
+
const onDisk = JSON.parse(readFileSync5(file, "utf8"))?.sessions;
|
|
6435
6901
|
if (onDisk && typeof onDisk === "object") {
|
|
6436
6902
|
for (const [key, record] of Object.entries(onDisk)) {
|
|
6437
6903
|
if (isRecord2(record)) merged.set(key, record);
|
|
@@ -6444,12 +6910,12 @@ function save2(update) {
|
|
|
6444
6910
|
const kept = [...merged.entries()].filter(([, record]) => record.updatedAt >= cutoff).sort((a, b) => a[1].updatedAt - b[1].updatedAt).slice(-MAX_RESUME_RECORDS);
|
|
6445
6911
|
records = new Map(kept);
|
|
6446
6912
|
try {
|
|
6447
|
-
|
|
6913
|
+
mkdirSync5(join9(file, ".."), { recursive: true, mode: 448 });
|
|
6448
6914
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
6449
|
-
|
|
6915
|
+
writeFileSync5(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
|
|
6450
6916
|
mode: 384
|
|
6451
6917
|
});
|
|
6452
|
-
|
|
6918
|
+
renameSync4(tmp, file);
|
|
6453
6919
|
} catch (err) {
|
|
6454
6920
|
log.debug("could not write the claude session resume store", {
|
|
6455
6921
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -6867,7 +7333,7 @@ async function requestSideQuestion(activeProcess, question, options) {
|
|
|
6867
7333
|
}
|
|
6868
7334
|
});
|
|
6869
7335
|
pendingProcesses.add(proc);
|
|
6870
|
-
return new Promise((
|
|
7336
|
+
return new Promise((resolve7, reject) => {
|
|
6871
7337
|
const event = `side-question:${requestId}`;
|
|
6872
7338
|
let settled = false;
|
|
6873
7339
|
let sent = false;
|
|
@@ -6922,7 +7388,7 @@ async function requestSideQuestion(activeProcess, question, options) {
|
|
|
6922
7388
|
}
|
|
6923
7389
|
settled = true;
|
|
6924
7390
|
cleanup();
|
|
6925
|
-
|
|
7391
|
+
resolve7({ response: result.response, synthetic: result.synthetic });
|
|
6926
7392
|
};
|
|
6927
7393
|
const timer = setTimeout(() => {
|
|
6928
7394
|
fail(new Error(`/btw timed out after ${timeoutMs}ms.`), true);
|
|
@@ -7097,16 +7563,16 @@ function isTurnInFlight(ap) {
|
|
|
7097
7563
|
}
|
|
7098
7564
|
function awaitTurnIdle(ap, timeoutMs) {
|
|
7099
7565
|
if (!ap.turnInFlight) return Promise.resolve(true);
|
|
7100
|
-
return new Promise((
|
|
7566
|
+
return new Promise((resolve7) => {
|
|
7101
7567
|
const wake = () => {
|
|
7102
7568
|
clearTimeout(timer);
|
|
7103
|
-
|
|
7569
|
+
resolve7(true);
|
|
7104
7570
|
};
|
|
7105
7571
|
const timer = setTimeout(() => {
|
|
7106
7572
|
const waiters = ap.turnIdleWaiters ?? [];
|
|
7107
7573
|
const at = waiters.indexOf(wake);
|
|
7108
7574
|
if (at >= 0) waiters.splice(at, 1);
|
|
7109
|
-
|
|
7575
|
+
resolve7(false);
|
|
7110
7576
|
}, timeoutMs);
|
|
7111
7577
|
(ap.turnIdleWaiters ??= []).push(wake);
|
|
7112
7578
|
});
|
|
@@ -7247,14 +7713,14 @@ function hasProcessExited(proc) {
|
|
|
7247
7713
|
}
|
|
7248
7714
|
function waitForProcessExit(proc, timeoutMs) {
|
|
7249
7715
|
if (hasProcessExited(proc)) return Promise.resolve(true);
|
|
7250
|
-
return new Promise((
|
|
7716
|
+
return new Promise((resolve7) => {
|
|
7251
7717
|
const onExit = () => {
|
|
7252
7718
|
clearTimeout(timer);
|
|
7253
|
-
|
|
7719
|
+
resolve7(true);
|
|
7254
7720
|
};
|
|
7255
7721
|
const timer = setTimeout(() => {
|
|
7256
7722
|
proc.off("exit", onExit);
|
|
7257
|
-
|
|
7723
|
+
resolve7(hasProcessExited(proc));
|
|
7258
7724
|
}, timeoutMs);
|
|
7259
7725
|
proc.once("exit", onExit);
|
|
7260
7726
|
});
|
|
@@ -7321,6 +7787,7 @@ function deleteClaudeSessionId(key) {
|
|
|
7321
7787
|
if (claudeSessionId) clearLedger(claudeSessionId);
|
|
7322
7788
|
claudeSessions.delete(key);
|
|
7323
7789
|
forgetForkFingerprint(key);
|
|
7790
|
+
forgetServedConversation(key);
|
|
7324
7791
|
forgetResumePoint(key);
|
|
7325
7792
|
}
|
|
7326
7793
|
function transferClaudeSession(fromKey, toKey) {
|
|
@@ -7332,6 +7799,7 @@ function transferClaudeSession(fromKey, toKey) {
|
|
|
7332
7799
|
setClaudeSessionId(toKey, claudeSessionId);
|
|
7333
7800
|
moveResumePoint(fromKey, toKey);
|
|
7334
7801
|
moveForkFingerprint(fromKey, toKey);
|
|
7802
|
+
moveServedConversation(fromKey, toKey);
|
|
7335
7803
|
clearExitPlanModeQuestions(fromKey);
|
|
7336
7804
|
clearAccountFailoverQuestions(fromKey);
|
|
7337
7805
|
clearSubagentDispatchQuestions(fromKey);
|
|
@@ -7557,7 +8025,9 @@ function buildCliArgs(opts) {
|
|
|
7557
8025
|
thinking,
|
|
7558
8026
|
thinkingDisplay,
|
|
7559
8027
|
fastMode,
|
|
7560
|
-
cliVersion
|
|
8028
|
+
cliVersion,
|
|
8029
|
+
maxTurns,
|
|
8030
|
+
maxBudgetUsd
|
|
7561
8031
|
} = opts;
|
|
7562
8032
|
const args = [
|
|
7563
8033
|
"--print",
|
|
@@ -7628,6 +8098,8 @@ function buildCliArgs(opts) {
|
|
|
7628
8098
|
if (fastMode && cliSupportsFastMode(cliVersion ?? null)) {
|
|
7629
8099
|
args.push("--settings", JSON.stringify({ fastMode: true }));
|
|
7630
8100
|
}
|
|
8101
|
+
if (maxTurns !== void 0) args.push("--max-turns", String(maxTurns));
|
|
8102
|
+
if (maxBudgetUsd !== void 0) args.push("--max-budget-usd", String(maxBudgetUsd));
|
|
7631
8103
|
if (skipPermissions && permissionMode !== "plan" && !readOnly) {
|
|
7632
8104
|
args.push("--dangerously-skip-permissions");
|
|
7633
8105
|
}
|
|
@@ -7781,7 +8253,7 @@ async function waitForSessionIdle(client, sessionID, options = {}) {
|
|
|
7781
8253
|
if (options.stop?.()) return true;
|
|
7782
8254
|
if (await sessionStatus(client, sessionID) !== "busy") return true;
|
|
7783
8255
|
if (Date.now() - started2 >= timeoutMs) return false;
|
|
7784
|
-
await new Promise((
|
|
8256
|
+
await new Promise((resolve7) => setTimeout(resolve7, pollMs));
|
|
7785
8257
|
}
|
|
7786
8258
|
}
|
|
7787
8259
|
async function deliverAsideInline(client, sessionID, text, options = {}) {
|
|
@@ -7792,7 +8264,7 @@ async function deliverAsideInline(client, sessionID, text, options = {}) {
|
|
|
7792
8264
|
if (emitAsideInline(sessionID, text)) return true;
|
|
7793
8265
|
if (Date.now() - started2 >= timeoutMs) return false;
|
|
7794
8266
|
if (await sessionStatus(client, sessionID) !== "busy") return false;
|
|
7795
|
-
await new Promise((
|
|
8267
|
+
await new Promise((resolve7) => setTimeout(resolve7, pollMs));
|
|
7796
8268
|
}
|
|
7797
8269
|
}
|
|
7798
8270
|
async function waitForAsideProcess(client, sessionID, options = {}) {
|
|
@@ -7813,7 +8285,7 @@ async function waitForAsideProcess(client, sessionID, options = {}) {
|
|
|
7813
8285
|
log.warn("btw: a turn is running but no claude process appeared for it", { sessionID, waitedMs });
|
|
7814
8286
|
return void 0;
|
|
7815
8287
|
}
|
|
7816
|
-
await new Promise((
|
|
8288
|
+
await new Promise((resolve7) => setTimeout(resolve7, pollMs));
|
|
7817
8289
|
}
|
|
7818
8290
|
}
|
|
7819
8291
|
async function settleSessionBusy(client, sessionID, active, options = {}) {
|
|
@@ -7825,7 +8297,7 @@ async function settleSessionBusy(client, sessionID, active, options = {}) {
|
|
|
7825
8297
|
if (status === "busy") return true;
|
|
7826
8298
|
if (status === "unknown") return isProcessBusy(active);
|
|
7827
8299
|
if (Date.now() - started2 >= settleMs) return false;
|
|
7828
|
-
await new Promise((
|
|
8300
|
+
await new Promise((resolve7) => setTimeout(resolve7, pollMs));
|
|
7829
8301
|
}
|
|
7830
8302
|
}
|
|
7831
8303
|
function errorText(error) {
|
|
@@ -7872,10 +8344,10 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
7872
8344
|
let inlineDone = false;
|
|
7873
8345
|
let markInlineDelivered = () => {
|
|
7874
8346
|
};
|
|
7875
|
-
const inlineDelivered = new Promise((
|
|
8347
|
+
const inlineDelivered = new Promise((resolve7) => {
|
|
7876
8348
|
markInlineDelivered = () => {
|
|
7877
8349
|
inlineDone = true;
|
|
7878
|
-
|
|
8350
|
+
resolve7("inline");
|
|
7879
8351
|
};
|
|
7880
8352
|
});
|
|
7881
8353
|
if (isSideQuestionPending(active)) {
|
|
@@ -7953,10 +8425,147 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
7953
8425
|
}
|
|
7954
8426
|
}
|
|
7955
8427
|
|
|
8428
|
+
// src/budget-ledger.ts
|
|
8429
|
+
var BUDGET_MARKER = "\u258C **budget:**";
|
|
8430
|
+
var DEFAULT_BUDGET_WARN_AT = [50, 80];
|
|
8431
|
+
var MAX_SESSIONS = 256;
|
|
8432
|
+
var MAX_CLAUDE_SESSIONS = 512;
|
|
8433
|
+
var sessions2 = /* @__PURE__ */ new Map();
|
|
8434
|
+
var lastTotals = /* @__PURE__ */ new Map();
|
|
8435
|
+
function touch2(session) {
|
|
8436
|
+
let entry = sessions2.get(session);
|
|
8437
|
+
if (entry) {
|
|
8438
|
+
sessions2.delete(session);
|
|
8439
|
+
} else {
|
|
8440
|
+
entry = { own: 0, notedPercent: 0 };
|
|
8441
|
+
}
|
|
8442
|
+
sessions2.set(session, entry);
|
|
8443
|
+
while (sessions2.size > MAX_SESSIONS) {
|
|
8444
|
+
const oldest = sessions2.keys().next();
|
|
8445
|
+
if (oldest.done) break;
|
|
8446
|
+
sessions2.delete(oldest.value);
|
|
8447
|
+
}
|
|
8448
|
+
return entry;
|
|
8449
|
+
}
|
|
8450
|
+
function resolveBudgetUsd(value, option) {
|
|
8451
|
+
if (value === void 0 || value === null) return void 0;
|
|
8452
|
+
const parsed = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
|
|
8453
|
+
if (typeof parsed === "number" && Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
8454
|
+
log.warn("budget option refused: not a positive number of dollars", { option });
|
|
8455
|
+
return void 0;
|
|
8456
|
+
}
|
|
8457
|
+
function resolvePositiveInteger(value, option) {
|
|
8458
|
+
if (value === void 0 || value === null) return void 0;
|
|
8459
|
+
const parsed = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
|
|
8460
|
+
if (typeof parsed === "number" && Number.isInteger(parsed) && parsed > 0) return parsed;
|
|
8461
|
+
log.warn("cap option refused: not a positive whole number", { option });
|
|
8462
|
+
return void 0;
|
|
8463
|
+
}
|
|
8464
|
+
function resolveBudgetWarnAt(value) {
|
|
8465
|
+
if (value === void 0 || value === null) return [...DEFAULT_BUDGET_WARN_AT];
|
|
8466
|
+
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "number" && Number.isFinite(entry) && entry > 0 && entry < 100)) {
|
|
8467
|
+
log.warn("budgetWarnAt refused: expected percentages between 0 and 100", {
|
|
8468
|
+
option: "budgetWarnAt"
|
|
8469
|
+
});
|
|
8470
|
+
return [...DEFAULT_BUDGET_WARN_AT];
|
|
8471
|
+
}
|
|
8472
|
+
return [...new Set(value)].sort((a, b) => a - b);
|
|
8473
|
+
}
|
|
8474
|
+
function costDelta(claudeSessionId, totalCostUsd, cumulative) {
|
|
8475
|
+
if (!Number.isFinite(totalCostUsd) || totalCostUsd <= 0) return 0;
|
|
8476
|
+
if (!cumulative || !claudeSessionId) return totalCostUsd;
|
|
8477
|
+
const previous = lastTotals.get(claudeSessionId);
|
|
8478
|
+
lastTotals.delete(claudeSessionId);
|
|
8479
|
+
lastTotals.set(claudeSessionId, totalCostUsd);
|
|
8480
|
+
while (lastTotals.size > MAX_CLAUDE_SESSIONS) {
|
|
8481
|
+
const oldest = lastTotals.keys().next();
|
|
8482
|
+
if (oldest.done) break;
|
|
8483
|
+
lastTotals.delete(oldest.value);
|
|
8484
|
+
}
|
|
8485
|
+
if (previous === void 0 || totalCostUsd < previous) return totalCostUsd;
|
|
8486
|
+
return totalCostUsd - previous;
|
|
8487
|
+
}
|
|
8488
|
+
function recordSessionCost(session, deltaUsd) {
|
|
8489
|
+
if (!(deltaUsd > 0)) return;
|
|
8490
|
+
touch2(session).own += deltaUsd;
|
|
8491
|
+
}
|
|
8492
|
+
function sessionParentKnown(session) {
|
|
8493
|
+
return sessions2.get(session)?.parent !== void 0;
|
|
8494
|
+
}
|
|
8495
|
+
function noteSessionParent(session, parent) {
|
|
8496
|
+
touch2(session).parent = parent && parent !== session ? parent : null;
|
|
8497
|
+
}
|
|
8498
|
+
function isChildSession(session) {
|
|
8499
|
+
return typeof sessions2.get(session)?.parent === "string";
|
|
8500
|
+
}
|
|
8501
|
+
function budgetRoot(session) {
|
|
8502
|
+
let current = session;
|
|
8503
|
+
const seen = /* @__PURE__ */ new Set([current]);
|
|
8504
|
+
for (; ; ) {
|
|
8505
|
+
const parent = sessions2.get(current)?.parent;
|
|
8506
|
+
if (typeof parent !== "string" || seen.has(parent)) return current;
|
|
8507
|
+
seen.add(parent);
|
|
8508
|
+
current = parent;
|
|
8509
|
+
}
|
|
8510
|
+
}
|
|
8511
|
+
function sessionSpend(root) {
|
|
8512
|
+
let total = 0;
|
|
8513
|
+
for (const [session, entry] of sessions2) {
|
|
8514
|
+
if (entry.own > 0 && budgetRoot(session) === root) total += entry.own;
|
|
8515
|
+
}
|
|
8516
|
+
return total;
|
|
8517
|
+
}
|
|
8518
|
+
function takeCrossedThreshold(root, capUsd, warnAt) {
|
|
8519
|
+
const spentUsd = sessionSpend(root);
|
|
8520
|
+
const percentSpent = spentUsd / capUsd * 100;
|
|
8521
|
+
const entry = touch2(root);
|
|
8522
|
+
const crossed = [...warnAt, 100].filter(
|
|
8523
|
+
(threshold) => threshold <= percentSpent && threshold > entry.notedPercent
|
|
8524
|
+
);
|
|
8525
|
+
if (crossed.length === 0) return null;
|
|
8526
|
+
const percent = Math.max(...crossed);
|
|
8527
|
+
entry.notedPercent = percent;
|
|
8528
|
+
return { percent, spentUsd };
|
|
8529
|
+
}
|
|
8530
|
+
function formatUsd(value) {
|
|
8531
|
+
if (value >= 100) return `$${value.toFixed(0)}`;
|
|
8532
|
+
if (value >= 1) return `$${value.toFixed(2)}`;
|
|
8533
|
+
return `$${value.toFixed(4)}`;
|
|
8534
|
+
}
|
|
8535
|
+
function formatBudgetWarnNote(percent, spentUsd, capUsd) {
|
|
8536
|
+
if (percent >= 100) {
|
|
8537
|
+
return `${BUDGET_MARKER} this conversation has spent ${formatUsd(spentUsd)} of its ${formatUsd(capUsd)} \`maxBudgetUsd\`, so the next message will not start a turn. Raise \`maxBudgetUsd\` and relaunch opencode, or start a new session.`;
|
|
8538
|
+
}
|
|
8539
|
+
return `${BUDGET_MARKER} this conversation has spent ${formatUsd(spentUsd)}, ${percent}% of its ${formatUsd(capUsd)} \`maxBudgetUsd\`, counting the subagents it started.`;
|
|
8540
|
+
}
|
|
8541
|
+
function formatBudgetStopNote(spentUsd, capUsd) {
|
|
8542
|
+
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.`;
|
|
8543
|
+
}
|
|
8544
|
+
function recordResultCost(budget, claudeSessionId, totalCostUsd) {
|
|
8545
|
+
if (typeof totalCostUsd !== "number") return;
|
|
8546
|
+
recordSessionCost(budget.session, costDelta(claudeSessionId, totalCostUsd, budget.cumulative));
|
|
8547
|
+
}
|
|
8548
|
+
function takeBudgetNote(budget) {
|
|
8549
|
+
if (!budget.notes) return null;
|
|
8550
|
+
const crossed = takeCrossedThreshold(budgetRoot(budget.session), budget.capUsd, budget.warnAt);
|
|
8551
|
+
return crossed ? formatBudgetWarnNote(crossed.percent, crossed.spentUsd, budget.capUsd) : null;
|
|
8552
|
+
}
|
|
8553
|
+
function snapshotBudgets() {
|
|
8554
|
+
const roots = /* @__PURE__ */ new Map();
|
|
8555
|
+
for (const [session, entry] of sessions2) {
|
|
8556
|
+
const root = budgetRoot(session);
|
|
8557
|
+
const row = roots.get(root) ?? { session: root, spentUsd: 0, children: 0 };
|
|
8558
|
+
row.spentUsd += entry.own;
|
|
8559
|
+
if (root !== session && entry.own > 0) row.children++;
|
|
8560
|
+
roots.set(root, row);
|
|
8561
|
+
}
|
|
8562
|
+
return [...roots.values()].filter((row) => row.spentUsd > 0).sort((a, b) => b.spentUsd - a.spentUsd);
|
|
8563
|
+
}
|
|
8564
|
+
|
|
7956
8565
|
// src/diagnostic-bundle.ts
|
|
7957
8566
|
import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
|
|
7958
8567
|
import { openSync, readSync, closeSync, fstatSync, realpathSync } from "fs";
|
|
7959
|
-
import { homedir as
|
|
8568
|
+
import { homedir as homedir8 } from "os";
|
|
7960
8569
|
|
|
7961
8570
|
// src/log-messages.ts
|
|
7962
8571
|
var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
@@ -7970,18 +8579,32 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
7970
8579
|
"account failover declined; ending the turn",
|
|
7971
8580
|
"accountGroups must be an object mapping an account name to a group name; ignoring it",
|
|
7972
8581
|
"accountGroups names an account that is not configured; ignoring it",
|
|
8582
|
+
"agent cap not applied: this claude CLI does not take the flag",
|
|
7973
8583
|
"agent effort override refused: unknown level",
|
|
8584
|
+
"agent maxBudgetUsd ignored: the CLI counts it over a reused process's whole life, so it applies to subagents only",
|
|
8585
|
+
"agent maxBudgetUsd refused: not a positive number of dollars",
|
|
8586
|
+
"agent maxTurns / maxBudgetUsd not applied: the interactive transport cannot pass print-only flags",
|
|
8587
|
+
"agent maxTurns refused: not a positive whole number",
|
|
7974
8588
|
"agent model override refused: unknown model",
|
|
7975
8589
|
"agent prompt cache ttl refused: unknown value",
|
|
8590
|
+
"an opencode read deny rule could not be mapped onto Claude's own Read; that path stays readable by the CLI's own file tools",
|
|
7976
8591
|
"auto-continuation stopped",
|
|
7977
8592
|
"auto-continuing incomplete claude result",
|
|
8593
|
+
"automatic account switch skipped an account whose runtime could not be prepared",
|
|
8594
|
+
"background dispatch refused by maxBackgroundSubagents",
|
|
7978
8595
|
"btw: a turn is running but no claude process appeared for it",
|
|
7979
8596
|
"btw: early aside failed; the message will ask again",
|
|
8597
|
+
"budget option refused: not a positive number of dollars",
|
|
8598
|
+
"budgetWarnAt refused: expected percentages between 0 and 100",
|
|
7980
8599
|
"cannot interrupt this transport; waiting for the turn to end",
|
|
8600
|
+
"cap option refused: not a positive whole number",
|
|
7981
8601
|
"carried this conversation's claude transcript to the other account",
|
|
7982
8602
|
"carrying the claude conversation over to the new session key",
|
|
7983
8603
|
"claude --version output unparseable",
|
|
8604
|
+
"claude account cannot serve this turn; continuing it on the next account automatically",
|
|
8605
|
+
"claude account is near its usage limit; new turns move to another account until the window resets",
|
|
7984
8606
|
"claude account is out of usage; the turn ends with a note",
|
|
8607
|
+
"claude account usage crossed a warning threshold",
|
|
7985
8608
|
"claude already registers a different skill under this name; not bridging ours",
|
|
7986
8609
|
"claude cli < 2.1.142 detected; Opus 4.7 thinking summaries unavailable. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
7987
8610
|
"claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
@@ -8004,6 +8627,8 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
8004
8627
|
"cleanup-stale: error processing cache root",
|
|
8005
8628
|
"cleanup-stale: rmSync failed",
|
|
8006
8629
|
"continuing this conversation's claude session under the new model or effort instead of replaying it",
|
|
8630
|
+
"conversation budget reached; turn not started",
|
|
8631
|
+
"conversation budget threshold crossed",
|
|
8007
8632
|
"copied a shared Claude capability into the account config dir instead of linking it; later edits to the original will not be picked up",
|
|
8008
8633
|
"could not share a Claude capability with the account",
|
|
8009
8634
|
"delivering proxy results after interrupted continuation",
|
|
@@ -8054,6 +8679,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
8054
8679
|
"interactiveBypass is ignored: skipPermissions governs the interactive transport too",
|
|
8055
8680
|
"malformed data URI, skipping file part",
|
|
8056
8681
|
"no active process to respawn (start watchdog); ending turn",
|
|
8682
|
+
"no configured account could take this turn; it ends here",
|
|
8057
8683
|
"no proxyTools entry was recognised; nothing will be proxied this turn",
|
|
8058
8684
|
"no stdout after envelope write; respawning claude process to resume conversation",
|
|
8059
8685
|
"opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction",
|
|
@@ -8069,6 +8695,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
8069
8695
|
"proxy call past its deadline, but opencode is still serving it; waiting",
|
|
8070
8696
|
"proxy call still waiting, deadline approaching",
|
|
8071
8697
|
"proxy call still waiting, no deadline",
|
|
8698
|
+
"proxy-mcp admission check failed; letting the call through",
|
|
8072
8699
|
"proxy-mcp client closed a tool call before its result",
|
|
8073
8700
|
"proxy-mcp dropping result for a closed tool call",
|
|
8074
8701
|
"proxy-mcp interceptor failed",
|
|
@@ -8083,6 +8710,8 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
8083
8710
|
"remote URL images are not supported by Claude CLI, skipping",
|
|
8084
8711
|
"replaying stdout the child emitted between turns",
|
|
8085
8712
|
"replaying the conversation as text: no claude session was available to continue",
|
|
8713
|
+
"replaying the conversation as text: the thread was edited or undone",
|
|
8714
|
+
"replaying the messages this claude session missed: the conversation continued under another session key",
|
|
8086
8715
|
"replaying this conversation as text on the other account",
|
|
8087
8716
|
"replaying this conversation rather than carrying it across accounts",
|
|
8088
8717
|
"result fallback timer fired \u2014 closing stream without result event",
|
|
@@ -8153,6 +8782,20 @@ var BUNDLE_DATA_ALLOWLIST = {
|
|
|
8153
8782
|
isError: "flag",
|
|
8154
8783
|
planModeQuestion: "flag",
|
|
8155
8784
|
subagentDispatchCrossGroup: "flag",
|
|
8785
|
+
// Whether opencode's read deny rules are carried onto Claude's own Read,
|
|
8786
|
+
// and how many were. A count and a flag only: the patterns themselves are
|
|
8787
|
+
// the operator's config text and are never logged (h #g243).
|
|
8788
|
+
mapReadRules: "flag",
|
|
8789
|
+
readRuleDenies: "count",
|
|
8790
|
+
// Budgets and caps (h #g245): numbers and option names only, never spend
|
|
8791
|
+
// per conversation beyond what the doctor table already prints.
|
|
8792
|
+
child: "flag",
|
|
8793
|
+
countUnreadable: "flag",
|
|
8794
|
+
cap: "count",
|
|
8795
|
+
requested: "count",
|
|
8796
|
+
running: "count",
|
|
8797
|
+
option: "name",
|
|
8798
|
+
cliFlag: "name",
|
|
8156
8799
|
kept: "flag",
|
|
8157
8800
|
listsAgentTypes: "flag",
|
|
8158
8801
|
opencodeHasQuestion: "flag",
|
|
@@ -8160,6 +8803,8 @@ var BUNDLE_DATA_ALLOWLIST = {
|
|
|
8160
8803
|
sse: "flag",
|
|
8161
8804
|
strictMcpConfig: "flag",
|
|
8162
8805
|
supported: "flag",
|
|
8806
|
+
// Whether a usage threshold moved new turns to another account (h #g244).
|
|
8807
|
+
switched: "flag",
|
|
8163
8808
|
turnStats: "flag",
|
|
8164
8809
|
// counts and sizes: a length is a shape, never content
|
|
8165
8810
|
attempts: "count",
|
|
@@ -8185,12 +8830,22 @@ var BUNDLE_DATA_ALLOWLIST = {
|
|
|
8185
8830
|
preTokens: "count",
|
|
8186
8831
|
released: "count",
|
|
8187
8832
|
removed: "count",
|
|
8833
|
+
// The three counts of a rewound thread: how much of what this key's Claude
|
|
8834
|
+
// session was answering is still in the conversation, and how much of it is
|
|
8835
|
+
// not (h #g242).
|
|
8836
|
+
sharedMessages: "count",
|
|
8837
|
+
removedMessages: "count",
|
|
8838
|
+
removedUserMessages: "count",
|
|
8188
8839
|
// The three counts of a released subagent dispatch: how many subagents, how
|
|
8189
8840
|
// many of them carried a choice, and which task a refused answer was for.
|
|
8190
8841
|
claims: "count",
|
|
8191
8842
|
tasks: "count",
|
|
8192
8843
|
taskIndex: "count",
|
|
8193
8844
|
skills: "count",
|
|
8845
|
+
// A usage reading crossing a warning threshold (h #g244): the percent used
|
|
8846
|
+
// and the thresholds it crossed, numbers only.
|
|
8847
|
+
utilization: "count",
|
|
8848
|
+
crossed: "count",
|
|
8194
8849
|
stderrBytes: "count",
|
|
8195
8850
|
textLength: "count",
|
|
8196
8851
|
tools: "count",
|
|
@@ -8349,7 +9004,7 @@ function createIdHasher(salt = randomBytes2(16).toString("hex")) {
|
|
|
8349
9004
|
return (raw) => createHash4("sha256").update(salt).update("\0").update(raw).digest("hex").slice(0, 8);
|
|
8350
9005
|
}
|
|
8351
9006
|
function createRedactionContext(options = {}) {
|
|
8352
|
-
const home = options.home ??
|
|
9007
|
+
const home = options.home ?? homedir8();
|
|
8353
9008
|
const homes = /* @__PURE__ */ new Set();
|
|
8354
9009
|
if (home) homes.add(home);
|
|
8355
9010
|
if (home) {
|
|
@@ -8643,6 +9298,202 @@ function buildLogBundleSection(input) {
|
|
|
8643
9298
|
return lines.join("\n");
|
|
8644
9299
|
}
|
|
8645
9300
|
|
|
9301
|
+
// src/read-rules.ts
|
|
9302
|
+
import * as path8 from "path";
|
|
9303
|
+
import * as os4 from "os";
|
|
9304
|
+
var OPENCODE_DEFAULT_READ_RULES = [
|
|
9305
|
+
{ pattern: "*", effect: "allow", source: "default" },
|
|
9306
|
+
{ pattern: "*.env", effect: "ask", source: "default" },
|
|
9307
|
+
{ pattern: "*.env.*", effect: "ask", source: "default" },
|
|
9308
|
+
{ pattern: "*.env.example", effect: "allow", source: "default" }
|
|
9309
|
+
];
|
|
9310
|
+
var READ_PERMISSION = "read";
|
|
9311
|
+
function wildcardMatch(input, pattern) {
|
|
9312
|
+
const normalized = input.replaceAll("\\", "/");
|
|
9313
|
+
let escaped = pattern.replaceAll("\\", "/").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
9314
|
+
if (escaped.endsWith(" .*")) escaped = `${escaped.slice(0, -3)}( .*)?`;
|
|
9315
|
+
return new RegExp(`^${escaped}$`, process.platform === "win32" ? "si" : "s").test(normalized);
|
|
9316
|
+
}
|
|
9317
|
+
function expandHome2(pattern, home) {
|
|
9318
|
+
if (pattern === "~" || pattern === "$HOME") return home;
|
|
9319
|
+
if (pattern.startsWith("~/")) return home + pattern.slice(1);
|
|
9320
|
+
if (pattern.startsWith("$HOME/") || pattern.startsWith("$HOME\\")) return home + pattern.slice(5);
|
|
9321
|
+
return pattern;
|
|
9322
|
+
}
|
|
9323
|
+
function isPlainObject2(value) {
|
|
9324
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9325
|
+
}
|
|
9326
|
+
function isEffect(value) {
|
|
9327
|
+
return value === "allow" || value === "ask" || value === "deny";
|
|
9328
|
+
}
|
|
9329
|
+
function mergePermissionBlock(base, incoming) {
|
|
9330
|
+
const out = { ...base };
|
|
9331
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
9332
|
+
const existing = out[key];
|
|
9333
|
+
out[key] = isPlainObject2(existing) && isPlainObject2(value) ? mergePermissionBlock(existing, value) : value;
|
|
9334
|
+
}
|
|
9335
|
+
return out;
|
|
9336
|
+
}
|
|
9337
|
+
function v1PermissionBlock(layers) {
|
|
9338
|
+
let permission = {};
|
|
9339
|
+
let tools = {};
|
|
9340
|
+
for (const layer of layers) {
|
|
9341
|
+
const block = layer.permission;
|
|
9342
|
+
if (typeof block === "string") permission = mergePermissionBlock(permission, { "*": block });
|
|
9343
|
+
else if (isPlainObject2(block)) permission = mergePermissionBlock(permission, block);
|
|
9344
|
+
if (isPlainObject2(layer.tools)) tools = { ...tools, ...layer.tools };
|
|
9345
|
+
}
|
|
9346
|
+
const fromEnv = process.env.OPENCODE_PERMISSION;
|
|
9347
|
+
if (fromEnv) {
|
|
9348
|
+
try {
|
|
9349
|
+
const parsed = JSON.parse(fromEnv);
|
|
9350
|
+
if (isPlainObject2(parsed)) permission = mergePermissionBlock(permission, parsed);
|
|
9351
|
+
} catch {
|
|
9352
|
+
}
|
|
9353
|
+
}
|
|
9354
|
+
const derived = {};
|
|
9355
|
+
if (tools.read === false) derived[READ_PERMISSION] = "deny";
|
|
9356
|
+
else if (tools.read === true) derived[READ_PERMISSION] = "allow";
|
|
9357
|
+
return mergePermissionBlock(derived, permission);
|
|
9358
|
+
}
|
|
9359
|
+
function rulesFromV1Block(block, home) {
|
|
9360
|
+
const rules = [];
|
|
9361
|
+
for (const [key, value] of Object.entries(block)) {
|
|
9362
|
+
if (!wildcardMatch(READ_PERMISSION, key)) continue;
|
|
9363
|
+
if (isEffect(value)) {
|
|
9364
|
+
rules.push({ pattern: "*", effect: value, source: "config" });
|
|
9365
|
+
continue;
|
|
9366
|
+
}
|
|
9367
|
+
if (!isPlainObject2(value)) continue;
|
|
9368
|
+
for (const [pattern, effect] of Object.entries(value)) {
|
|
9369
|
+
if (!isEffect(effect)) continue;
|
|
9370
|
+
rules.push({ pattern: expandHome2(String(pattern), home), effect, source: "config" });
|
|
9371
|
+
}
|
|
9372
|
+
}
|
|
9373
|
+
return rules;
|
|
9374
|
+
}
|
|
9375
|
+
function rulesFromV2Array(value, home) {
|
|
9376
|
+
if (!Array.isArray(value)) return [];
|
|
9377
|
+
const rules = [];
|
|
9378
|
+
for (const entry of value) {
|
|
9379
|
+
if (!isPlainObject2(entry)) continue;
|
|
9380
|
+
const { action, resource, effect } = entry;
|
|
9381
|
+
if (typeof action !== "string" || !wildcardMatch(READ_PERMISSION, action)) continue;
|
|
9382
|
+
if (!isEffect(effect)) continue;
|
|
9383
|
+
const pattern = typeof resource === "string" && resource.length > 0 ? resource : "*";
|
|
9384
|
+
rules.push({ pattern: expandHome2(pattern, home), effect, source: "config" });
|
|
9385
|
+
}
|
|
9386
|
+
return rules;
|
|
9387
|
+
}
|
|
9388
|
+
function resolveReadRules(layers, hostApi = "v1", options = {}) {
|
|
9389
|
+
const home = options.home ?? os4.homedir();
|
|
9390
|
+
const rules = options.includeDefaults === false ? [] : [...OPENCODE_DEFAULT_READ_RULES];
|
|
9391
|
+
if (hostApi === "v2") {
|
|
9392
|
+
for (const layer of layers) {
|
|
9393
|
+
const block = typeof layer.permission === "string" ? { "*": layer.permission } : isPlainObject2(layer.permission) ? layer.permission : null;
|
|
9394
|
+
if (block) rules.push(...rulesFromV1Block(block, home));
|
|
9395
|
+
rules.push(...rulesFromV2Array(layer.permissions, home));
|
|
9396
|
+
}
|
|
9397
|
+
return rules;
|
|
9398
|
+
}
|
|
9399
|
+
rules.push(...rulesFromV1Block(v1PermissionBlock(layers), home));
|
|
9400
|
+
for (const layer of layers) rules.push(...rulesFromV2Array(layer.permissions, home));
|
|
9401
|
+
return rules;
|
|
9402
|
+
}
|
|
9403
|
+
function describeReadPatternRefusal(reason) {
|
|
9404
|
+
switch (reason) {
|
|
9405
|
+
case "empty-pattern":
|
|
9406
|
+
return "the pattern is empty";
|
|
9407
|
+
case "contains-dotdot":
|
|
9408
|
+
return "it contains `..`, which leaves the worktree the two matchers agree on";
|
|
9409
|
+
}
|
|
9410
|
+
}
|
|
9411
|
+
function translateReadPattern(pattern, options = {}) {
|
|
9412
|
+
const raw = pattern.replaceAll("\\", "/").trim();
|
|
9413
|
+
if (raw.length === 0) return { refused: "empty-pattern" };
|
|
9414
|
+
if (raw.split("/").includes("..")) return { refused: "contains-dotdot" };
|
|
9415
|
+
if (/^\*+$/.test(raw)) return { rule: "**" };
|
|
9416
|
+
const widenSubtree = (value) => value.endsWith("/*") ? `${value}*` : value;
|
|
9417
|
+
if (path8.posix.isAbsolute(raw)) {
|
|
9418
|
+
return { rule: `/${widenSubtree(raw)}` };
|
|
9419
|
+
}
|
|
9420
|
+
if (!raw.includes("/")) return { rule: raw };
|
|
9421
|
+
const { worktree, cwd } = options;
|
|
9422
|
+
if (worktree && cwd && path8.resolve(worktree) !== path8.resolve(cwd)) {
|
|
9423
|
+
const anchored = path8.posix.join(path8.resolve(worktree).replaceAll("\\", "/"), widenSubtree(raw));
|
|
9424
|
+
return { rule: `/${anchored}` };
|
|
9425
|
+
}
|
|
9426
|
+
return { rule: widenSubtree(raw) };
|
|
9427
|
+
}
|
|
9428
|
+
function emptyMapping(enabled) {
|
|
9429
|
+
return { enabled, deny: [], ask: [], unmapped: [], shadowedAllows: [] };
|
|
9430
|
+
}
|
|
9431
|
+
function mapReadRules(rules, options = {}) {
|
|
9432
|
+
const deny = [];
|
|
9433
|
+
const ask = [];
|
|
9434
|
+
const unmapped = [];
|
|
9435
|
+
const shadowedAllows = [];
|
|
9436
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9437
|
+
const supersededBy = (index, pattern, by) => rules.slice(index + 1).some((later) => by(later) && wildcardMatch(pattern, later.pattern));
|
|
9438
|
+
rules.forEach((rule, index) => {
|
|
9439
|
+
if (rule.effect === "ask") {
|
|
9440
|
+
if (supersededBy(index, rule.pattern, () => true)) return;
|
|
9441
|
+
if (!ask.includes(rule.pattern)) ask.push(rule.pattern);
|
|
9442
|
+
return;
|
|
9443
|
+
}
|
|
9444
|
+
if (rule.effect !== "deny") return;
|
|
9445
|
+
if (supersededBy(index, rule.pattern, (later) => later.effect !== "deny")) return;
|
|
9446
|
+
const translated = translateReadPattern(rule.pattern, options);
|
|
9447
|
+
if ("refused" in translated) {
|
|
9448
|
+
unmapped.push({ pattern: rule.pattern, reason: translated.refused });
|
|
9449
|
+
return;
|
|
9450
|
+
}
|
|
9451
|
+
const entry = `Read(${translated.rule})`;
|
|
9452
|
+
if (!seen.has(entry)) {
|
|
9453
|
+
seen.add(entry);
|
|
9454
|
+
deny.push(entry);
|
|
9455
|
+
}
|
|
9456
|
+
for (const later of rules.slice(index + 1)) {
|
|
9457
|
+
if (later.effect !== "allow") continue;
|
|
9458
|
+
if (!wildcardMatch(later.pattern, rule.pattern)) continue;
|
|
9459
|
+
if (!shadowedAllows.includes(later.pattern)) shadowedAllows.push(later.pattern);
|
|
9460
|
+
}
|
|
9461
|
+
});
|
|
9462
|
+
return { deny, ask, unmapped, shadowedAllows };
|
|
9463
|
+
}
|
|
9464
|
+
function resolveReadRuleMapping(input) {
|
|
9465
|
+
if (input.enabled === false) return emptyMapping(false);
|
|
9466
|
+
try {
|
|
9467
|
+
const rules = resolveReadRules(input.layers, input.hostApi ?? "v1", { home: input.home });
|
|
9468
|
+
const mapped = mapReadRules(rules, { worktree: input.worktree, cwd: input.cwd });
|
|
9469
|
+
for (const refused of mapped.unmapped) {
|
|
9470
|
+
log.warn(
|
|
9471
|
+
"an opencode read deny rule could not be mapped onto Claude's own Read; that path stays readable by the CLI's own file tools",
|
|
9472
|
+
{ reason: refused.reason }
|
|
9473
|
+
);
|
|
9474
|
+
}
|
|
9475
|
+
if (mapped.deny.length > 0) {
|
|
9476
|
+
log.info("mapped opencode read deny rules onto the claude spawn", {
|
|
9477
|
+
rules: mapped.deny,
|
|
9478
|
+
askNotEnforced: mapped.ask.length
|
|
9479
|
+
});
|
|
9480
|
+
}
|
|
9481
|
+
return { enabled: true, ...mapped };
|
|
9482
|
+
} catch (err) {
|
|
9483
|
+
log.debug("could not resolve opencode read rules", {
|
|
9484
|
+
error: err instanceof Error ? err.message : String(err)
|
|
9485
|
+
});
|
|
9486
|
+
return emptyMapping(true);
|
|
9487
|
+
}
|
|
9488
|
+
}
|
|
9489
|
+
var lastReadRuleMapping;
|
|
9490
|
+
function recordReadRuleMapping(mapping) {
|
|
9491
|
+
lastReadRuleMapping = mapping;
|
|
9492
|
+
}
|
|
9493
|
+
function snapshotReadRuleMapping() {
|
|
9494
|
+
return lastReadRuleMapping;
|
|
9495
|
+
}
|
|
9496
|
+
|
|
8646
9497
|
// src/plan-usage.ts
|
|
8647
9498
|
import { execFile as execFile2 } from "child_process";
|
|
8648
9499
|
var PLAN_USAGE_ARGUMENTS = /* @__PURE__ */ new Set(["usage", "cost", "stats", "limits"]);
|
|
@@ -8686,7 +9537,7 @@ function parsePlanUsage(stdout) {
|
|
|
8686
9537
|
}
|
|
8687
9538
|
function runCli(cliPath, args, timeoutMs, env) {
|
|
8688
9539
|
const plan = planClaudeSpawn(cliPath, args);
|
|
8689
|
-
return new Promise((
|
|
9540
|
+
return new Promise((resolve7, reject) => {
|
|
8690
9541
|
execFile2(
|
|
8691
9542
|
plan.file,
|
|
8692
9543
|
plan.args,
|
|
@@ -8700,9 +9551,9 @@ function runCli(cliPath, args, timeoutMs, env) {
|
|
|
8700
9551
|
windowsVerbatimArguments: plan.windowsVerbatimArguments
|
|
8701
9552
|
},
|
|
8702
9553
|
(error, stdout) => {
|
|
8703
|
-
if (stdout && stdout.trim()) return
|
|
9554
|
+
if (stdout && stdout.trim()) return resolve7(stdout);
|
|
8704
9555
|
if (error) return reject(error);
|
|
8705
|
-
|
|
9556
|
+
resolve7(stdout ?? "");
|
|
8706
9557
|
}
|
|
8707
9558
|
);
|
|
8708
9559
|
});
|
|
@@ -8741,13 +9592,13 @@ async function fetchPlanUsage(cliPath, options = {}) {
|
|
|
8741
9592
|
|
|
8742
9593
|
// src/stale-build.ts
|
|
8743
9594
|
import * as fs7 from "fs";
|
|
8744
|
-
import * as
|
|
9595
|
+
import * as path10 from "path";
|
|
8745
9596
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8746
9597
|
|
|
8747
9598
|
// src/startup-diagnostics.ts
|
|
8748
9599
|
import { execFile as execFile3 } from "child_process";
|
|
8749
9600
|
import * as fs6 from "fs";
|
|
8750
|
-
import * as
|
|
9601
|
+
import * as path9 from "path";
|
|
8751
9602
|
import { promisify as promisify2 } from "util";
|
|
8752
9603
|
import { fileURLToPath } from "url";
|
|
8753
9604
|
|
|
@@ -8782,8 +9633,8 @@ var cachedPluginVersion;
|
|
|
8782
9633
|
function pluginVersion() {
|
|
8783
9634
|
if (cachedPluginVersion) return cachedPluginVersion;
|
|
8784
9635
|
try {
|
|
8785
|
-
const here =
|
|
8786
|
-
const raw = fs6.readFileSync(
|
|
9636
|
+
const here = path9.dirname(fileURLToPath(import.meta.url));
|
|
9637
|
+
const raw = fs6.readFileSync(path9.join(here, "..", "package.json"), "utf8");
|
|
8787
9638
|
const version = JSON.parse(raw).version;
|
|
8788
9639
|
cachedPluginVersion = typeof version === "string" ? version : "unknown";
|
|
8789
9640
|
} catch {
|
|
@@ -8807,7 +9658,7 @@ var opencodeVersionProbe;
|
|
|
8807
9658
|
function detectOpencodeVersion(execPath = process.execPath) {
|
|
8808
9659
|
if (opencodeVersionProbe) return opencodeVersionProbe;
|
|
8809
9660
|
opencodeVersionProbe = (async () => {
|
|
8810
|
-
if (!
|
|
9661
|
+
if (!path9.basename(execPath).toLowerCase().includes("opencode")) {
|
|
8811
9662
|
log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
|
|
8812
9663
|
return void 0;
|
|
8813
9664
|
}
|
|
@@ -8860,6 +9711,23 @@ function collectStartupDiagnostics(providers, opencodeVersion) {
|
|
|
8860
9711
|
error: err instanceof Error ? err.message : String(err)
|
|
8861
9712
|
});
|
|
8862
9713
|
}
|
|
9714
|
+
const mapReadRules2 = firstOption(providers, "mapReadRules") !== false;
|
|
9715
|
+
let readRuleDenies = 0;
|
|
9716
|
+
if (mapReadRules2) {
|
|
9717
|
+
try {
|
|
9718
|
+
const hostApi = firstOption(providers, "hostApi") === "v2" ? "v2" : "v1";
|
|
9719
|
+
readRuleDenies = resolveReadRuleMapping({
|
|
9720
|
+
layers: opencodeConfigLayers(cwd.resolved, hostApi),
|
|
9721
|
+
hostApi,
|
|
9722
|
+
worktree: detectWorktree(cwd.resolved),
|
|
9723
|
+
cwd: cwd.resolved
|
|
9724
|
+
}).deny.length;
|
|
9725
|
+
} catch (err) {
|
|
9726
|
+
log.debug("startup diagnostics could not resolve opencode read rules", {
|
|
9727
|
+
error: err instanceof Error ? err.message : String(err)
|
|
9728
|
+
});
|
|
9729
|
+
}
|
|
9730
|
+
}
|
|
8863
9731
|
let transport;
|
|
8864
9732
|
try {
|
|
8865
9733
|
transport = requestedTransport({
|
|
@@ -8895,6 +9763,8 @@ function collectStartupDiagnostics(providers, opencodeVersion) {
|
|
|
8895
9763
|
planModeQuestion: firstOption(providers, "planModeQuestion") === true,
|
|
8896
9764
|
subagentDispatch: firstOption(providers, "subagentDispatch") === "ask" ? "ask" : "off",
|
|
8897
9765
|
subagentDispatchCrossGroup: firstOption(providers, "subagentDispatchCrossGroup") === true,
|
|
9766
|
+
mapReadRules: mapReadRules2,
|
|
9767
|
+
readRuleDenies,
|
|
8898
9768
|
anthropicApiKeyInEnv: Boolean(
|
|
8899
9769
|
process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
|
|
8900
9770
|
)
|
|
@@ -8988,7 +9858,7 @@ function createStaleBuildWatch(io) {
|
|
|
8988
9858
|
const claimed = /* @__PURE__ */ new Set();
|
|
8989
9859
|
const warned = /* @__PURE__ */ new Set();
|
|
8990
9860
|
let checkedAt;
|
|
8991
|
-
let
|
|
9861
|
+
let cached2 = null;
|
|
8992
9862
|
const read = () => {
|
|
8993
9863
|
let onDisk;
|
|
8994
9864
|
try {
|
|
@@ -9006,10 +9876,10 @@ function createStaleBuildWatch(io) {
|
|
|
9006
9876
|
},
|
|
9007
9877
|
check() {
|
|
9008
9878
|
const now = io.now();
|
|
9009
|
-
if (checkedAt !== void 0 && now - checkedAt < throttleMs) return
|
|
9879
|
+
if (checkedAt !== void 0 && now - checkedAt < throttleMs) return cached2;
|
|
9010
9880
|
checkedAt = now;
|
|
9011
9881
|
const status = read();
|
|
9012
|
-
|
|
9882
|
+
cached2 = status.stale;
|
|
9013
9883
|
if (status.stale && status.onDisk) {
|
|
9014
9884
|
const identity = `${status.stale.kind}:${status.onDisk.version}:${status.onDisk.mtimeMs}:${status.onDisk.size}`;
|
|
9015
9885
|
if (!warned.has(identity)) {
|
|
@@ -9022,7 +9892,7 @@ function createStaleBuildWatch(io) {
|
|
|
9022
9892
|
});
|
|
9023
9893
|
}
|
|
9024
9894
|
}
|
|
9025
|
-
return
|
|
9895
|
+
return cached2;
|
|
9026
9896
|
},
|
|
9027
9897
|
describe() {
|
|
9028
9898
|
return read();
|
|
@@ -9047,7 +9917,7 @@ function defaultEntryPath() {
|
|
|
9047
9917
|
}
|
|
9048
9918
|
function readOnDiskBuild(entryPath) {
|
|
9049
9919
|
try {
|
|
9050
|
-
const raw = fs7.readFileSync(
|
|
9920
|
+
const raw = fs7.readFileSync(path10.join(path10.dirname(entryPath), "..", "package.json"), "utf8");
|
|
9051
9921
|
const version = JSON.parse(raw).version;
|
|
9052
9922
|
if (typeof version !== "string" || version.length === 0) return void 0;
|
|
9053
9923
|
const stat = fs7.statSync(entryPath);
|
|
@@ -9272,10 +10142,24 @@ function formatDoctorReport(report) {
|
|
|
9272
10142
|
);
|
|
9273
10143
|
}
|
|
9274
10144
|
lines.push("");
|
|
10145
|
+
lines.push("**opencode read rules on Claude's own Read**");
|
|
10146
|
+
lines.push("");
|
|
10147
|
+
lines.push(...formatReadRules(report.readRules));
|
|
10148
|
+
if (report.accountUsage) {
|
|
10149
|
+
lines.push("");
|
|
10150
|
+
lines.push("**Account usage**");
|
|
10151
|
+
lines.push("");
|
|
10152
|
+
lines.push(...formatAccountUsage(report.accountUsage));
|
|
10153
|
+
}
|
|
10154
|
+
lines.push("");
|
|
9275
10155
|
lines.push("**Background subagents**");
|
|
9276
10156
|
lines.push("");
|
|
9277
10157
|
lines.push(...formatBackgroundSubagents(report.backgroundSubagents));
|
|
9278
10158
|
lines.push("");
|
|
10159
|
+
lines.push("**Budgets and caps**");
|
|
10160
|
+
lines.push("");
|
|
10161
|
+
lines.push(...formatBudgets(report.budgets));
|
|
10162
|
+
lines.push("");
|
|
9279
10163
|
lines.push("**Plan usage**");
|
|
9280
10164
|
lines.push("");
|
|
9281
10165
|
switch (report.planUsage.status) {
|
|
@@ -9306,7 +10190,82 @@ function formatDoctorReport(report) {
|
|
|
9306
10190
|
lines.push("```");
|
|
9307
10191
|
}
|
|
9308
10192
|
}
|
|
9309
|
-
return lines.join("\n");
|
|
10193
|
+
return lines.join("\n");
|
|
10194
|
+
}
|
|
10195
|
+
function formatReadRules(mapping) {
|
|
10196
|
+
const lines = [];
|
|
10197
|
+
if (!mapping) {
|
|
10198
|
+
lines.push(
|
|
10199
|
+
"Not mapped yet: no `claude` spawn has been planned in this process. The next message in a Claude Code session resolves them."
|
|
10200
|
+
);
|
|
10201
|
+
return lines;
|
|
10202
|
+
}
|
|
10203
|
+
if (!mapping.enabled) {
|
|
10204
|
+
lines.push(
|
|
10205
|
+
"Off (`mapReadRules: false`). opencode's `read` rules reach its own tools only, so a file opencode would refuse is still readable by the Read the CLI runs itself."
|
|
10206
|
+
);
|
|
10207
|
+
return lines;
|
|
10208
|
+
}
|
|
10209
|
+
if (mapping.deny.length === 0) {
|
|
10210
|
+
lines.push(
|
|
10211
|
+
"No `deny` rule to map. opencode's defaults only `ask` for `.env` files, and an ask cannot be carried onto a spawn that has nobody to ask (see below)."
|
|
10212
|
+
);
|
|
10213
|
+
} else {
|
|
10214
|
+
lines.push(`Denied on Claude's own Read (${mapping.deny.length}):`);
|
|
10215
|
+
lines.push("");
|
|
10216
|
+
for (const rule of mapping.deny) lines.push(`- \`${rule}\``);
|
|
10217
|
+
}
|
|
10218
|
+
if (mapping.ask.length > 0) {
|
|
10219
|
+
lines.push("");
|
|
10220
|
+
lines.push(`Not enforced, because they are \`ask\` rules (${mapping.ask.length}):`);
|
|
10221
|
+
lines.push("");
|
|
10222
|
+
for (const pattern of mapping.ask) lines.push(`- \`${pattern}\``);
|
|
10223
|
+
lines.push("");
|
|
10224
|
+
lines.push(
|
|
10225
|
+
"A headless spawn runs with the skip flag and there is no one for the CLI to ask, so an `ask` is reported rather than silently promoted to a deny. To close one, make it `deny` in opencode's `permission.read`, or add `Read` to `proxyTools` so the read runs in opencode and its own prompt applies."
|
|
10226
|
+
);
|
|
10227
|
+
}
|
|
10228
|
+
if (mapping.unmapped.length > 0) {
|
|
10229
|
+
lines.push("");
|
|
10230
|
+
lines.push(`Deny rules that could NOT be mapped (${mapping.unmapped.length}):`);
|
|
10231
|
+
lines.push("");
|
|
10232
|
+
for (const refused of mapping.unmapped) {
|
|
10233
|
+
lines.push(`- \`${refused.pattern}\`: ${describeReadPatternRefusal(refused.reason)}`);
|
|
10234
|
+
}
|
|
10235
|
+
}
|
|
10236
|
+
if (mapping.shadowedAllows.length > 0) {
|
|
10237
|
+
lines.push("");
|
|
10238
|
+
lines.push(`Allow rules a mapped deny now shadows (${mapping.shadowedAllows.length}):`);
|
|
10239
|
+
lines.push("");
|
|
10240
|
+
for (const pattern of mapping.shadowedAllows) lines.push(`- \`${pattern}\``);
|
|
10241
|
+
lines.push("");
|
|
10242
|
+
lines.push(
|
|
10243
|
+
"Claude Code's deny beats its allow, so an exception opencode would carve out of a deny cannot be expressed: Claude refuses these where opencode would allow them."
|
|
10244
|
+
);
|
|
10245
|
+
}
|
|
10246
|
+
return lines;
|
|
10247
|
+
}
|
|
10248
|
+
function formatAccountUsage(usage, now = Date.now()) {
|
|
10249
|
+
const rows = [];
|
|
10250
|
+
for (const account of [...usage.keys()].sort()) {
|
|
10251
|
+
for (const [window, entry] of usage.get(account)) {
|
|
10252
|
+
const used = entry.status === "rejected" && entry.utilization === null ? "out" : entry.utilization === null ? "unknown" : `${usagePercent(entry.utilization)}%`;
|
|
10253
|
+
const resets = entry.resetsAt !== void 0 ? formatResetsAtLocal(entry.resetsAt) ?? "unknown" : entry.resetsText ? tableCell(entry.resetsText) : "unknown";
|
|
10254
|
+
rows.push(
|
|
10255
|
+
`| ${account} | ${window} | ${used} | ${entry.status} | ${resets}${isReadingCurrent(entry, now) ? "" : " (window over)"} | ${formatAge(Math.max(0, now - entry.observedAt))} ago | ${entry.noted.length > 0 ? entry.noted.map((t) => `${t}%`).join(", ") : "none"} |`
|
|
10256
|
+
);
|
|
10257
|
+
}
|
|
10258
|
+
}
|
|
10259
|
+
if (rows.length === 0) {
|
|
10260
|
+
return [
|
|
10261
|
+
"No reading yet. Claude Code reports an account's usage on the stream when its view of the limits changes, usually on the first turn of a process; the interactive transport reports nothing until a limit."
|
|
10262
|
+
];
|
|
10263
|
+
}
|
|
10264
|
+
return [
|
|
10265
|
+
"| account | window | used | status | resets | observed | noted at |",
|
|
10266
|
+
"|---|---|---|---|---|---|---|",
|
|
10267
|
+
...rows
|
|
10268
|
+
];
|
|
9310
10269
|
}
|
|
9311
10270
|
function formatBackgroundSubagents(state) {
|
|
9312
10271
|
const lines = [];
|
|
@@ -9314,6 +10273,10 @@ function formatBackgroundSubagents(state) {
|
|
|
9314
10273
|
lines.push(...formatRunningCount(state.running));
|
|
9315
10274
|
lines.push("");
|
|
9316
10275
|
}
|
|
10276
|
+
lines.push(
|
|
10277
|
+
state.cap === void 0 ? "maxBackgroundSubagents: off (no cap)" : `maxBackgroundSubagents: ${state.cap} at once; a background dispatch over it is refused whole`
|
|
10278
|
+
);
|
|
10279
|
+
lines.push("");
|
|
9317
10280
|
const gate = state.gate;
|
|
9318
10281
|
if (!gate) {
|
|
9319
10282
|
lines.push(
|
|
@@ -9372,6 +10335,39 @@ function formatRunningCount(count) {
|
|
|
9372
10335
|
}
|
|
9373
10336
|
return lines;
|
|
9374
10337
|
}
|
|
10338
|
+
function formatBudgets(budgets) {
|
|
10339
|
+
const lines = [];
|
|
10340
|
+
lines.push("| Field | Value |");
|
|
10341
|
+
lines.push("|---|---|");
|
|
10342
|
+
lines.push(
|
|
10343
|
+
`| maxBudgetUsd | ${budgets.maxBudgetUsd === void 0 ? "off (no cap)" : formatUsd(budgets.maxBudgetUsd)} |`
|
|
10344
|
+
);
|
|
10345
|
+
if (budgets.maxBudgetUsd !== void 0) {
|
|
10346
|
+
const warnAt = budgets.warnAt.map((percent) => `${percent}%`).join(", ");
|
|
10347
|
+
lines.push(`| budgetWarnAt | ${warnAt || "none"} |`);
|
|
10348
|
+
}
|
|
10349
|
+
lines.push(`| defaultSubagentMaxTurns | ${budgets.subagentDefaults.maxTurns ?? "unset"} |`);
|
|
10350
|
+
lines.push(
|
|
10351
|
+
`| defaultSubagentMaxBudgetUsd | ${budgets.subagentDefaults.maxBudgetUsd === void 0 ? "unset" : formatUsd(budgets.subagentDefaults.maxBudgetUsd)} |`
|
|
10352
|
+
);
|
|
10353
|
+
if (budgets.maxBudgetUsd === void 0) return lines;
|
|
10354
|
+
lines.push("");
|
|
10355
|
+
if (budgets.sessions.length === 0) {
|
|
10356
|
+
lines.push("No spend recorded by this process yet.");
|
|
10357
|
+
} else {
|
|
10358
|
+
lines.push("| session | spent | of cap | subagents counted |");
|
|
10359
|
+
lines.push("|---|---|---|---|");
|
|
10360
|
+
for (const row of budgets.sessions) {
|
|
10361
|
+
const percent = Math.round(row.spentUsd / budgets.maxBudgetUsd * 100);
|
|
10362
|
+
lines.push(`| ${row.session} | ${formatUsd(row.spentUsd)} | ${percent}% | ${row.children} |`);
|
|
10363
|
+
}
|
|
10364
|
+
}
|
|
10365
|
+
lines.push("");
|
|
10366
|
+
lines.push(
|
|
10367
|
+
"In memory only: an opencode restart starts every conversation's ledger again (a resumed Claude session's first result restores its own earlier total). A conversation at its cap ends each new turn before anything is spawned."
|
|
10368
|
+
);
|
|
10369
|
+
return lines;
|
|
10370
|
+
}
|
|
9375
10371
|
function describeBackgroundGateSource(gate) {
|
|
9376
10372
|
if (gate.hostApi === "v2") {
|
|
9377
10373
|
return "opencode 2 offers `background` unconditionally, so the registry is not consulted";
|
|
@@ -9450,14 +10446,23 @@ async function gatherDoctorReport(options) {
|
|
|
9450
10446
|
processes,
|
|
9451
10447
|
pendingCalls: snapshotPendingProxyCalls(),
|
|
9452
10448
|
proxyServers,
|
|
10449
|
+
readRules: snapshotReadRuleMapping(),
|
|
9453
10450
|
mcpServerErrors: snapshotMcpServerErrors(),
|
|
9454
10451
|
pluginLoadFailures: snapshotPluginLoadFailures(),
|
|
9455
10452
|
hookFailures: snapshotHookFailures(),
|
|
9456
10453
|
planUsage,
|
|
10454
|
+
accountUsage: snapshotAccountUsage(),
|
|
9457
10455
|
backgroundSubagents: {
|
|
9458
10456
|
gate: snapshotBackgroundSubagentGate(),
|
|
9459
10457
|
ledgers: snapshotBackgroundTasks(),
|
|
9460
|
-
running: await countRunningBackgroundTasks().catch(() => void 0)
|
|
10458
|
+
running: await countRunningBackgroundTasks().catch(() => void 0),
|
|
10459
|
+
cap: options.maxBackgroundSubagents
|
|
10460
|
+
},
|
|
10461
|
+
budgets: {
|
|
10462
|
+
maxBudgetUsd: options.maxBudgetUsd,
|
|
10463
|
+
warnAt: options.budgetWarnAt ?? [],
|
|
10464
|
+
subagentDefaults: getDefaultSubagentCliCaps(),
|
|
10465
|
+
sessions: snapshotBudgets()
|
|
9461
10466
|
}
|
|
9462
10467
|
};
|
|
9463
10468
|
}
|
|
@@ -9490,7 +10495,32 @@ function withholdFreeText(report) {
|
|
|
9490
10495
|
hookFailures: report.hookFailures.map((failure) => ({
|
|
9491
10496
|
...failure,
|
|
9492
10497
|
stderr: withhold(failure.stderr)
|
|
9493
|
-
}))
|
|
10498
|
+
})),
|
|
10499
|
+
// The reset phrase the TUI printed is Claude Code's text, not the plugin's.
|
|
10500
|
+
accountUsage: report.accountUsage && new Map(
|
|
10501
|
+
[...report.accountUsage].map(([account, windows]) => [
|
|
10502
|
+
account,
|
|
10503
|
+
new Map(
|
|
10504
|
+
[...windows].map(([window, entry]) => [
|
|
10505
|
+
window,
|
|
10506
|
+
entry.resetsText ? { ...entry, resetsText: withhold(entry.resetsText) } : entry
|
|
10507
|
+
])
|
|
10508
|
+
)
|
|
10509
|
+
])
|
|
10510
|
+
),
|
|
10511
|
+
// The operator's own glob patterns. Not plugin-authored, so they follow
|
|
10512
|
+
// the same rule as a hook's stderr: the shape and the counts survive a
|
|
10513
|
+
// bundle, the text does not.
|
|
10514
|
+
readRules: report.readRules && {
|
|
10515
|
+
...report.readRules,
|
|
10516
|
+
deny: report.readRules.deny.map(withhold),
|
|
10517
|
+
ask: report.readRules.ask.map(withhold),
|
|
10518
|
+
unmapped: report.readRules.unmapped.map((refused) => ({
|
|
10519
|
+
...refused,
|
|
10520
|
+
pattern: withhold(refused.pattern)
|
|
10521
|
+
})),
|
|
10522
|
+
shadowedAllows: report.readRules.shadowedAllows.map(withhold)
|
|
10523
|
+
}
|
|
9494
10524
|
};
|
|
9495
10525
|
}
|
|
9496
10526
|
function renderDoctorReport(report, options) {
|
|
@@ -9687,11 +10717,14 @@ var PLUGIN_NOTE_MARKERS = [
|
|
|
9687
10717
|
FAILOVER_MARKER,
|
|
9688
10718
|
ACCOUNT_BLOCK_MARKER,
|
|
9689
10719
|
ACCOUNT_GROUP_MARKER,
|
|
10720
|
+
CONVERSATION_REWOUND_MARKER,
|
|
9690
10721
|
USAGE_LIMIT_MARKER,
|
|
10722
|
+
USAGE_MARKER,
|
|
9691
10723
|
MODEL_FALLBACK_MARKER,
|
|
9692
10724
|
UNATTENDED_REPLAY_MARKER,
|
|
9693
10725
|
STALE_BUILD_MARKER,
|
|
9694
|
-
SUBAGENT_DISPATCH_MARKER
|
|
10726
|
+
SUBAGENT_DISPATCH_MARKER,
|
|
10727
|
+
BUDGET_MARKER
|
|
9695
10728
|
];
|
|
9696
10729
|
function isPluginNote(part) {
|
|
9697
10730
|
if (!part || part.type !== "text" || typeof part.text !== "string") return false;
|
|
@@ -9837,8 +10870,10 @@ function clipWithMarker(text, max) {
|
|
|
9837
10870
|
\u2026[truncated ${text.length - max} chars]`;
|
|
9838
10871
|
}
|
|
9839
10872
|
var MAX_REPLAY_MESSAGE_CHARS = 2e3;
|
|
9840
|
-
var MAX_REPLAY_LAST_REPLY_CHARS = 12e3;
|
|
9841
10873
|
var MAX_REPLAY_HEAD_CHARS = 1e3;
|
|
10874
|
+
var MAX_REPLAY_RECENT_MESSAGES = 6;
|
|
10875
|
+
var MAX_REPLAY_RECENT_MESSAGE_CHARS = 12e3;
|
|
10876
|
+
var MAX_REPLAY_TOTAL_CHARS = 18e4;
|
|
9842
10877
|
function clipMiddle(text, max) {
|
|
9843
10878
|
if (text.length <= max) return text;
|
|
9844
10879
|
const head = Math.min(Math.floor(max / 4), MAX_REPLAY_HEAD_CHARS);
|
|
@@ -9902,39 +10937,58 @@ ${clipWithMarker(
|
|
|
9902
10937
|
}
|
|
9903
10938
|
return { text: lines.join("\n"), toolResultCount };
|
|
9904
10939
|
}
|
|
10940
|
+
function isConversationMessage(msg) {
|
|
10941
|
+
return msg.role === "user" || msg.role === "assistant" || msg.role === "tool";
|
|
10942
|
+
}
|
|
9905
10943
|
function compactConversationHistory(prompt, opts = {}) {
|
|
9906
10944
|
const mode = opts.mode ?? "fresh-session";
|
|
9907
10945
|
prompt = filterSideQuestionHistory(prompt);
|
|
9908
10946
|
if (mode === "compaction") {
|
|
9909
10947
|
return buildCompactionHistory(prompt);
|
|
9910
10948
|
}
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
10949
|
+
if (opts.messages) {
|
|
10950
|
+
const run = filterSideQuestionHistory(opts.messages).filter(isConversationMessage);
|
|
10951
|
+
return run.length === 0 ? null : renderReplay(run);
|
|
10952
|
+
}
|
|
10953
|
+
const conversationMessages = prompt.filter(isConversationMessage);
|
|
9914
10954
|
if (conversationMessages.length <= 1) {
|
|
9915
10955
|
return null;
|
|
9916
10956
|
}
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
const msg = conversationMessages[i];
|
|
10957
|
+
return renderReplay(conversationMessages.slice(0, -1));
|
|
10958
|
+
}
|
|
10959
|
+
function renderReplay(messages) {
|
|
10960
|
+
const entries = [];
|
|
10961
|
+
let total = 0;
|
|
10962
|
+
let rank = 0;
|
|
10963
|
+
let omitted = 0;
|
|
10964
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
10965
|
+
const msg = messages[i];
|
|
9927
10966
|
const role = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "Tool";
|
|
9928
10967
|
const { text } = renderMessageContentForCompaction(msg);
|
|
9929
|
-
if (text.trim())
|
|
9930
|
-
|
|
9931
|
-
|
|
10968
|
+
if (!text.trim()) continue;
|
|
10969
|
+
const max = rank < MAX_REPLAY_RECENT_MESSAGES ? MAX_REPLAY_RECENT_MESSAGE_CHARS : MAX_REPLAY_MESSAGE_CHARS;
|
|
10970
|
+
rank++;
|
|
10971
|
+
const entry = `${role}: ${clipMiddle(text, max)}`;
|
|
10972
|
+
if (total + entry.length > MAX_REPLAY_TOTAL_CHARS) {
|
|
10973
|
+
omitted = i + 1;
|
|
10974
|
+
break;
|
|
9932
10975
|
}
|
|
10976
|
+
entries.push(entry);
|
|
10977
|
+
total += entry.length + 2;
|
|
9933
10978
|
}
|
|
9934
|
-
if (
|
|
9935
|
-
|
|
10979
|
+
if (entries.length === 0) return null;
|
|
10980
|
+
entries.reverse();
|
|
10981
|
+
if (omitted > 0) {
|
|
10982
|
+
log.info("replay bounded: the oldest messages were dropped", {
|
|
10983
|
+
omitted,
|
|
10984
|
+
kept: entries.length,
|
|
10985
|
+
chars: total
|
|
10986
|
+
});
|
|
10987
|
+
entries.unshift(
|
|
10988
|
+
`[${omitted} earlier message${omitted === 1 ? "" : "s"} omitted: the replay is capped at ${MAX_REPLAY_TOTAL_CHARS} characters]`
|
|
10989
|
+
);
|
|
9936
10990
|
}
|
|
9937
|
-
return
|
|
10991
|
+
return entries.join("\n\n");
|
|
9938
10992
|
}
|
|
9939
10993
|
function buildCompactionHistory(prompt) {
|
|
9940
10994
|
const entries = [];
|
|
@@ -10038,6 +11092,27 @@ ${historyContext}
|
|
|
10038
11092
|
|
|
10039
11093
|
Now continuing with the current message:
|
|
10040
11094
|
|
|
11095
|
+
`
|
|
11096
|
+
});
|
|
11097
|
+
}
|
|
11098
|
+
} else if (opts.missed && opts.missed.length > 0) {
|
|
11099
|
+
const missed = compactConversationHistory(prompt, { messages: opts.missed });
|
|
11100
|
+
if (missed) {
|
|
11101
|
+
log.info("including the messages this claude session missed", {
|
|
11102
|
+
messages: opts.missed.length,
|
|
11103
|
+
chars: missed.length
|
|
11104
|
+
});
|
|
11105
|
+
content.push({
|
|
11106
|
+
type: "text",
|
|
11107
|
+
text: `<missed_conversation>
|
|
11108
|
+
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.
|
|
11109
|
+
|
|
11110
|
+
${missed}
|
|
11111
|
+
|
|
11112
|
+
</missed_conversation>
|
|
11113
|
+
|
|
11114
|
+
Now continuing with the current message:
|
|
11115
|
+
|
|
10041
11116
|
`
|
|
10042
11117
|
});
|
|
10043
11118
|
}
|
|
@@ -10139,13 +11214,13 @@ function getTrailingUserMessages(prompt, opts = {}) {
|
|
|
10139
11214
|
// src/skill-bridge.ts
|
|
10140
11215
|
import * as crypto3 from "crypto";
|
|
10141
11216
|
import * as fs8 from "fs";
|
|
10142
|
-
import * as
|
|
10143
|
-
import * as
|
|
11217
|
+
import * as os5 from "os";
|
|
11218
|
+
import * as path11 from "path";
|
|
10144
11219
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
10145
11220
|
function bundledSkillsDir() {
|
|
10146
11221
|
try {
|
|
10147
11222
|
const here = fileURLToPath3(import.meta.url);
|
|
10148
|
-
const dir =
|
|
11223
|
+
const dir = path11.resolve(path11.dirname(here), "..", "skills");
|
|
10149
11224
|
return dirExists2(dir) ? dir : null;
|
|
10150
11225
|
} catch {
|
|
10151
11226
|
return null;
|
|
@@ -10162,7 +11237,7 @@ function realPath(p) {
|
|
|
10162
11237
|
try {
|
|
10163
11238
|
return fs8.realpathSync(p);
|
|
10164
11239
|
} catch {
|
|
10165
|
-
return
|
|
11240
|
+
return path11.resolve(p);
|
|
10166
11241
|
}
|
|
10167
11242
|
}
|
|
10168
11243
|
function envEnabled(name) {
|
|
@@ -10183,10 +11258,10 @@ function declaredSkillName(source) {
|
|
|
10183
11258
|
return SAFE_SKILL_NAME.test(name) ? name : null;
|
|
10184
11259
|
}
|
|
10185
11260
|
function readSkill(root, entry) {
|
|
10186
|
-
const dir =
|
|
11261
|
+
const dir = path11.join(root, entry);
|
|
10187
11262
|
let source;
|
|
10188
11263
|
try {
|
|
10189
|
-
source = fs8.readFileSync(
|
|
11264
|
+
source = fs8.readFileSync(path11.join(dir, "SKILL.md"), "utf8");
|
|
10190
11265
|
} catch {
|
|
10191
11266
|
return null;
|
|
10192
11267
|
}
|
|
@@ -10204,40 +11279,40 @@ function skillRoots(cwd) {
|
|
|
10204
11279
|
const roots = [];
|
|
10205
11280
|
const seen = /* @__PURE__ */ new Set();
|
|
10206
11281
|
const push = (p) => {
|
|
10207
|
-
const abs =
|
|
11282
|
+
const abs = path11.resolve(p);
|
|
10208
11283
|
if (seen.has(abs)) return;
|
|
10209
11284
|
seen.add(abs);
|
|
10210
11285
|
if (dirExists2(abs)) roots.push(abs);
|
|
10211
11286
|
};
|
|
10212
|
-
const home =
|
|
10213
|
-
const homeDir = home ?
|
|
10214
|
-
let current =
|
|
11287
|
+
const home = os5.homedir();
|
|
11288
|
+
const homeDir = home ? path11.resolve(home) : null;
|
|
11289
|
+
let current = path11.resolve(cwd);
|
|
10215
11290
|
while (true) {
|
|
10216
|
-
push(
|
|
11291
|
+
push(path11.join(current, ".opencode", "skills"));
|
|
10217
11292
|
if (current !== homeDir) {
|
|
10218
|
-
push(
|
|
10219
|
-
push(
|
|
11293
|
+
push(path11.join(current, ".claude", "skills"));
|
|
11294
|
+
push(path11.join(current, ".agents", "skills"));
|
|
10220
11295
|
}
|
|
10221
|
-
const parent =
|
|
11296
|
+
const parent = path11.dirname(current);
|
|
10222
11297
|
if (parent === current) break;
|
|
10223
11298
|
current = parent;
|
|
10224
11299
|
}
|
|
10225
|
-
if (homeDir) push(
|
|
11300
|
+
if (homeDir) push(path11.join(homeDir, ".opencode", "skills"));
|
|
10226
11301
|
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
10227
11302
|
if (envDir) {
|
|
10228
|
-
push(
|
|
10229
|
-
push(
|
|
11303
|
+
push(path11.join(envDir, "skills"));
|
|
11304
|
+
push(path11.join(envDir, "skill"));
|
|
10230
11305
|
}
|
|
10231
|
-
const xdg = process.env.XDG_CONFIG_HOME ?? (homeDir ?
|
|
11306
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? (homeDir ? path11.join(homeDir, ".config") : null);
|
|
10232
11307
|
if (xdg) {
|
|
10233
|
-
push(
|
|
10234
|
-
push(
|
|
11308
|
+
push(path11.join(xdg, "opencode", "skills"));
|
|
11309
|
+
push(path11.join(xdg, "opencode", "skill"));
|
|
10235
11310
|
}
|
|
10236
11311
|
if (homeDir && !envEnabled("OPENCODE_DISABLE_EXTERNAL_SKILLS")) {
|
|
10237
11312
|
if (!envEnabled("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")) {
|
|
10238
|
-
push(
|
|
11313
|
+
push(path11.join(homeDir, ".claude", "skills"));
|
|
10239
11314
|
}
|
|
10240
|
-
push(
|
|
11315
|
+
push(path11.join(homeDir, ".agents", "skills"));
|
|
10241
11316
|
}
|
|
10242
11317
|
return roots;
|
|
10243
11318
|
}
|
|
@@ -10284,23 +11359,23 @@ function discoverBundledSkills() {
|
|
|
10284
11359
|
}
|
|
10285
11360
|
function claudeConfigDir(configDir) {
|
|
10286
11361
|
const explicit = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
10287
|
-
if (explicit) return
|
|
10288
|
-
const home =
|
|
10289
|
-
return home ?
|
|
11362
|
+
if (explicit) return path11.resolve(expandHome(explicit));
|
|
11363
|
+
const home = os5.homedir();
|
|
11364
|
+
return home ? path11.resolve(home, ".claude") : null;
|
|
10290
11365
|
}
|
|
10291
11366
|
function installedPluginSkillRoots(configDir, cwd) {
|
|
10292
11367
|
if (!configDir) return [];
|
|
10293
11368
|
let parsed;
|
|
10294
11369
|
try {
|
|
10295
11370
|
parsed = JSON.parse(
|
|
10296
|
-
fs8.readFileSync(
|
|
11371
|
+
fs8.readFileSync(path11.join(configDir, "plugins", "installed_plugins.json"), "utf8")
|
|
10297
11372
|
);
|
|
10298
11373
|
} catch {
|
|
10299
11374
|
return [];
|
|
10300
11375
|
}
|
|
10301
11376
|
const plugins = parsed?.plugins;
|
|
10302
11377
|
if (!plugins || typeof plugins !== "object") return [];
|
|
10303
|
-
const workspace =
|
|
11378
|
+
const workspace = path11.resolve(cwd);
|
|
10304
11379
|
const roots = [];
|
|
10305
11380
|
for (const installs of Object.values(plugins)) {
|
|
10306
11381
|
if (!Array.isArray(installs)) continue;
|
|
@@ -10308,23 +11383,23 @@ function installedPluginSkillRoots(configDir, cwd) {
|
|
|
10308
11383
|
const entry = install;
|
|
10309
11384
|
if (typeof entry?.installPath !== "string" || !entry.installPath) continue;
|
|
10310
11385
|
if (entry.scope !== "user" && !coversWorkspace(entry.projectPath, workspace)) continue;
|
|
10311
|
-
roots.push(
|
|
11386
|
+
roots.push(path11.join(entry.installPath, "skills"));
|
|
10312
11387
|
}
|
|
10313
11388
|
}
|
|
10314
11389
|
return roots;
|
|
10315
11390
|
}
|
|
10316
11391
|
function coversWorkspace(projectPath, workspace) {
|
|
10317
11392
|
if (typeof projectPath !== "string" || !projectPath) return false;
|
|
10318
|
-
const root =
|
|
11393
|
+
const root = path11.resolve(projectPath);
|
|
10319
11394
|
if (root === workspace) return true;
|
|
10320
|
-
const rel =
|
|
10321
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
11395
|
+
const rel = path11.relative(root, workspace);
|
|
11396
|
+
return rel !== "" && !rel.startsWith("..") && !path11.isAbsolute(rel);
|
|
10322
11397
|
}
|
|
10323
11398
|
function discoverNativeClaudeSkills(opts) {
|
|
10324
11399
|
const found = [];
|
|
10325
11400
|
const scanned = /* @__PURE__ */ new Set();
|
|
10326
11401
|
const scan = (root, scope) => {
|
|
10327
|
-
const abs =
|
|
11402
|
+
const abs = path11.resolve(root);
|
|
10328
11403
|
if (scanned.has(abs)) return;
|
|
10329
11404
|
scanned.add(abs);
|
|
10330
11405
|
let entries;
|
|
@@ -10341,13 +11416,13 @@ function discoverNativeClaudeSkills(opts) {
|
|
|
10341
11416
|
}
|
|
10342
11417
|
};
|
|
10343
11418
|
const configDir = claudeConfigDir(opts.configDir);
|
|
10344
|
-
if (configDir) scan(
|
|
10345
|
-
const home =
|
|
10346
|
-
const homeDir = home ?
|
|
10347
|
-
let current =
|
|
11419
|
+
if (configDir) scan(path11.join(configDir, "skills"), "user");
|
|
11420
|
+
const home = os5.homedir();
|
|
11421
|
+
const homeDir = home ? path11.resolve(home) : null;
|
|
11422
|
+
let current = path11.resolve(opts.cwd);
|
|
10348
11423
|
while (true) {
|
|
10349
|
-
if (current !== homeDir) scan(
|
|
10350
|
-
const parent =
|
|
11424
|
+
if (current !== homeDir) scan(path11.join(current, ".claude", "skills"), "project");
|
|
11425
|
+
const parent = path11.dirname(current);
|
|
10351
11426
|
if (parent === current) break;
|
|
10352
11427
|
current = parent;
|
|
10353
11428
|
}
|
|
@@ -10381,7 +11456,7 @@ function dropNativelyLoadedSkills(skills, native) {
|
|
|
10381
11456
|
}
|
|
10382
11457
|
function readBundledSkillInfos() {
|
|
10383
11458
|
return discoverBundledSkills().flatMap((skill) => {
|
|
10384
|
-
const file =
|
|
11459
|
+
const file = path11.join(skill.dir, "SKILL.md");
|
|
10385
11460
|
let text;
|
|
10386
11461
|
try {
|
|
10387
11462
|
text = fs8.readFileSync(file, "utf8");
|
|
@@ -10425,13 +11500,13 @@ function buildSkillPluginDir(skills) {
|
|
|
10425
11500
|
if (skills.length === 0) return null;
|
|
10426
11501
|
const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
|
|
10427
11502
|
const hash = crypto3.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
|
|
10428
|
-
const root =
|
|
10429
|
-
const manifest =
|
|
11503
|
+
const root = path11.join(pluginTmpDir(), `skills-${hash}`);
|
|
11504
|
+
const manifest = path11.join(root, ".claude-plugin", "plugin.json");
|
|
10430
11505
|
if (fileExists2(manifest)) return root;
|
|
10431
11506
|
try {
|
|
10432
11507
|
fs8.rmSync(root, { recursive: true, force: true });
|
|
10433
|
-
fs8.mkdirSync(
|
|
10434
|
-
fs8.mkdirSync(
|
|
11508
|
+
fs8.mkdirSync(path11.join(root, ".claude-plugin"), { recursive: true });
|
|
11509
|
+
fs8.mkdirSync(path11.join(root, "skills"), { recursive: true });
|
|
10435
11510
|
fs8.writeFileSync(
|
|
10436
11511
|
manifest,
|
|
10437
11512
|
JSON.stringify(
|
|
@@ -10445,7 +11520,7 @@ function buildSkillPluginDir(skills) {
|
|
|
10445
11520
|
{ encoding: "utf8", mode: 384 }
|
|
10446
11521
|
);
|
|
10447
11522
|
for (const skill of skills) {
|
|
10448
|
-
linkSkill(skill.dir,
|
|
11523
|
+
linkSkill(skill.dir, path11.join(root, "skills", skill.name));
|
|
10449
11524
|
}
|
|
10450
11525
|
} catch (err) {
|
|
10451
11526
|
log.warn("failed to stage opencode skill plugin dir", {
|
|
@@ -10461,7 +11536,7 @@ function registerBundledSkillPath(config) {
|
|
|
10461
11536
|
if (!dir) return false;
|
|
10462
11537
|
config.skills ??= {};
|
|
10463
11538
|
const paths = config.skills.paths ??= [];
|
|
10464
|
-
if (paths.some((p) =>
|
|
11539
|
+
if (paths.some((p) => path11.resolve(p) === dir)) return false;
|
|
10465
11540
|
paths.push(dir);
|
|
10466
11541
|
return true;
|
|
10467
11542
|
}
|
|
@@ -10509,22 +11584,22 @@ async function resolveSkillPluginDirs(opts) {
|
|
|
10509
11584
|
// src/claude-session-wrapper.ts
|
|
10510
11585
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
10511
11586
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
10512
|
-
import { existsSync as existsSync4, writeFileSync as
|
|
11587
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
10513
11588
|
import { unlink as unlink2 } from "fs/promises";
|
|
10514
|
-
import { join as
|
|
11589
|
+
import { join as join14 } from "path";
|
|
10515
11590
|
|
|
10516
11591
|
// src/claude-session-bun.ts
|
|
10517
|
-
import * as
|
|
11592
|
+
import * as os6 from "os";
|
|
10518
11593
|
import * as fs9 from "fs";
|
|
10519
|
-
import * as
|
|
11594
|
+
import * as path12 from "path";
|
|
10520
11595
|
import { execFileSync } from "child_process";
|
|
10521
11596
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
10522
11597
|
import { StringDecoder } from "string_decoder";
|
|
10523
11598
|
function resolveClaude(cmd = "claude") {
|
|
10524
|
-
if (
|
|
11599
|
+
if (path12.isAbsolute(cmd) && fs9.existsSync(cmd)) return cmd;
|
|
10525
11600
|
const viaBun = Bun.which(cmd);
|
|
10526
11601
|
if (viaBun) return viaBun;
|
|
10527
|
-
const isWin =
|
|
11602
|
+
const isWin = os6.platform() === "win32";
|
|
10528
11603
|
try {
|
|
10529
11604
|
const out = execFileSync(isWin ? "where" : "which", [cmd], {
|
|
10530
11605
|
encoding: "utf8",
|
|
@@ -10537,7 +11612,7 @@ function resolveClaude(cmd = "claude") {
|
|
|
10537
11612
|
throw new Error(`Could not resolve command on PATH: ${cmd}`);
|
|
10538
11613
|
}
|
|
10539
11614
|
function encodeCwd(cwd) {
|
|
10540
|
-
const resolved =
|
|
11615
|
+
const resolved = path12.resolve(cwd);
|
|
10541
11616
|
let real = resolved;
|
|
10542
11617
|
try {
|
|
10543
11618
|
real = fs9.realpathSync.native(resolved);
|
|
@@ -10548,7 +11623,7 @@ function encodeCwd(cwd) {
|
|
|
10548
11623
|
var bunPtySpawner = (argv, opts) => {
|
|
10549
11624
|
const [command, ...args] = argv;
|
|
10550
11625
|
const resolved = resolveClaude(command ?? "claude");
|
|
10551
|
-
if (
|
|
11626
|
+
if (os6.platform() === "win32" && isBatchFile(resolved)) {
|
|
10552
11627
|
throw new Error(
|
|
10553
11628
|
`The interactive transport cannot run a Windows batch shim (${resolved}). Point \`cliPath\` at claude.exe, or use the default headless transport.`
|
|
10554
11629
|
);
|
|
@@ -10656,10 +11731,10 @@ function classifyScreen(text) {
|
|
|
10656
11731
|
return null;
|
|
10657
11732
|
}
|
|
10658
11733
|
function interactiveTranscriptPath(opts) {
|
|
10659
|
-
return
|
|
11734
|
+
return path12.join(
|
|
10660
11735
|
resolveConfigDir(opts.configDir),
|
|
10661
11736
|
"projects",
|
|
10662
|
-
encodeCwd(
|
|
11737
|
+
encodeCwd(path12.resolve(opts.cwd)),
|
|
10663
11738
|
`${opts.sessionId}.jsonl`
|
|
10664
11739
|
);
|
|
10665
11740
|
}
|
|
@@ -10901,12 +11976,12 @@ function isOwnPromptRecord(rec, normalized) {
|
|
|
10901
11976
|
}
|
|
10902
11977
|
function resolveConfigDir(configDir) {
|
|
10903
11978
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
10904
|
-
if (!value) return
|
|
10905
|
-
if (value === "~") return
|
|
11979
|
+
if (!value) return path12.join(os6.homedir(), ".claude");
|
|
11980
|
+
if (value === "~") return os6.homedir();
|
|
10906
11981
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
10907
|
-
return
|
|
11982
|
+
return path12.join(os6.homedir(), value.slice(2));
|
|
10908
11983
|
}
|
|
10909
|
-
return
|
|
11984
|
+
return path12.resolve(value);
|
|
10910
11985
|
}
|
|
10911
11986
|
function fatalScreenMessage(kind, configDir, cwd) {
|
|
10912
11987
|
if (kind === "trust") {
|
|
@@ -10970,7 +12045,7 @@ var ClaudeSession = class {
|
|
|
10970
12045
|
abandonTurn = false;
|
|
10971
12046
|
o;
|
|
10972
12047
|
constructor(opts = {}) {
|
|
10973
|
-
this.cwd =
|
|
12048
|
+
this.cwd = path12.resolve(opts.cwd ?? process.cwd());
|
|
10974
12049
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
10975
12050
|
this.explicitConfigDir = opts.configDir ? this.configDir : void 0;
|
|
10976
12051
|
this.signal = opts.signal;
|
|
@@ -10982,7 +12057,7 @@ var ClaudeSession = class {
|
|
|
10982
12057
|
this.onExit = opts.onExit;
|
|
10983
12058
|
this.spawnPty = opts.spawnPty ?? bunPtySpawner;
|
|
10984
12059
|
this.sessionId = opts.resumeSessionId ?? randomUUID4();
|
|
10985
|
-
this.jsonlPath =
|
|
12060
|
+
this.jsonlPath = path12.join(
|
|
10986
12061
|
this.configDir,
|
|
10987
12062
|
"projects",
|
|
10988
12063
|
encodeCwd(this.cwd),
|
|
@@ -11278,7 +12353,7 @@ var ClaudeSession = class {
|
|
|
11278
12353
|
throw new Error("session not started or already exited");
|
|
11279
12354
|
if (this.turn) throw new Error("a turn is already running in this session");
|
|
11280
12355
|
let settle;
|
|
11281
|
-
this.turn = new Promise((
|
|
12356
|
+
this.turn = new Promise((resolve7) => settle = resolve7);
|
|
11282
12357
|
this.turnDenied = [];
|
|
11283
12358
|
this.abandonTurn = false;
|
|
11284
12359
|
this.transcript.skipToEnd();
|
|
@@ -11693,8 +12768,8 @@ ${text2}`
|
|
|
11693
12768
|
}
|
|
11694
12769
|
function stageImage(data, extension) {
|
|
11695
12770
|
try {
|
|
11696
|
-
const file =
|
|
11697
|
-
|
|
12771
|
+
const file = join14(pluginTmpDir(), `attachment-${randomUUID5()}.${extension}`);
|
|
12772
|
+
writeFileSync7(file, data, { mode: 384, flag: "wx" });
|
|
11698
12773
|
return file;
|
|
11699
12774
|
} catch (err) {
|
|
11700
12775
|
log.warn("interactive transport could not stage an image", {
|
|
@@ -12175,17 +13250,17 @@ function spawnInteractiveProcess(opts) {
|
|
|
12175
13250
|
}
|
|
12176
13251
|
|
|
12177
13252
|
// src/spawn-record-store.ts
|
|
12178
|
-
import { mkdirSync as
|
|
12179
|
-
import { homedir as
|
|
12180
|
-
import { join as
|
|
13253
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, renameSync as renameSync5, statSync as statSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
13254
|
+
import { homedir as homedir12 } from "os";
|
|
13255
|
+
import { join as join15 } from "path";
|
|
12181
13256
|
var MAX_SPAWN_RECORDS = 256;
|
|
12182
13257
|
var SPAWN_RECORD_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
12183
|
-
var
|
|
13258
|
+
var pathOverride4 = null;
|
|
12184
13259
|
var lastWritten = /* @__PURE__ */ new Map();
|
|
12185
13260
|
function spawnRecordStorePath() {
|
|
12186
|
-
if (
|
|
12187
|
-
const stateRoot = process.env.XDG_STATE_HOME ||
|
|
12188
|
-
return
|
|
13261
|
+
if (pathOverride4) return pathOverride4;
|
|
13262
|
+
const stateRoot = process.env.XDG_STATE_HOME || join15(homedir12(), ".local", "state");
|
|
13263
|
+
return join15(stateRoot, "opencode-claude-code-plugin", "session-spawns.json");
|
|
12189
13264
|
}
|
|
12190
13265
|
function isRecord5(value) {
|
|
12191
13266
|
const record = value;
|
|
@@ -12198,7 +13273,7 @@ function isRecord5(value) {
|
|
|
12198
13273
|
function readEntries(file) {
|
|
12199
13274
|
const out = /* @__PURE__ */ new Map();
|
|
12200
13275
|
try {
|
|
12201
|
-
const parsed = JSON.parse(
|
|
13276
|
+
const parsed = JSON.parse(readFileSync9(file, "utf8"));
|
|
12202
13277
|
const entries = parsed && typeof parsed === "object" ? parsed.sessions : null;
|
|
12203
13278
|
if (!entries || typeof entries !== "object") return out;
|
|
12204
13279
|
const cutoff = Date.now() - SPAWN_RECORD_MAX_AGE_MS;
|
|
@@ -12234,12 +13309,12 @@ ${record.account ?? ""}`;
|
|
|
12234
13309
|
const cutoff = now - SPAWN_RECORD_MAX_AGE_MS;
|
|
12235
13310
|
const kept = [...merged.entries()].filter(([, entry]) => entry.at >= cutoff).sort((a, b) => a[1].at - b[1].at).slice(-MAX_SPAWN_RECORDS);
|
|
12236
13311
|
try {
|
|
12237
|
-
|
|
13312
|
+
mkdirSync7(join15(file, ".."), { recursive: true, mode: 448 });
|
|
12238
13313
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
12239
|
-
|
|
13314
|
+
writeFileSync8(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
|
|
12240
13315
|
mode: 384
|
|
12241
13316
|
});
|
|
12242
|
-
|
|
13317
|
+
renameSync5(tmp, file);
|
|
12243
13318
|
} catch {
|
|
12244
13319
|
return false;
|
|
12245
13320
|
}
|
|
@@ -12252,7 +13327,7 @@ ${record.account ?? ""}`;
|
|
|
12252
13327
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
12253
13328
|
import { constants as fsConstants } from "fs";
|
|
12254
13329
|
import { chmod as chmod2, copyFile as copyFile2, lstat as lstat2, mkdir as mkdir2 } from "fs/promises";
|
|
12255
|
-
import
|
|
13330
|
+
import path13 from "path";
|
|
12256
13331
|
var transcriptPath = interactiveTranscriptPath;
|
|
12257
13332
|
function configDirForAccount(account) {
|
|
12258
13333
|
const normalized = normalizeAccountName(account || DEFAULT_ACCOUNT);
|
|
@@ -12280,7 +13355,7 @@ async function carryTranscriptToAccount(input) {
|
|
|
12280
13355
|
const carriedId = renamed ? (input.newSessionId ?? randomUUID6)() : sessionId;
|
|
12281
13356
|
const target = renamed ? transcriptPath({ configDir: targetConfigDir, cwd, sessionId: carriedId }) : targetOwn;
|
|
12282
13357
|
try {
|
|
12283
|
-
await mkdir2(
|
|
13358
|
+
await mkdir2(path13.dirname(target), { recursive: true, mode: 448 });
|
|
12284
13359
|
await copyFile2(live.file, target, fsConstants.COPYFILE_EXCL);
|
|
12285
13360
|
await chmod2(target, 384);
|
|
12286
13361
|
} catch (err) {
|
|
@@ -12300,7 +13375,7 @@ function dedupe2(dirs) {
|
|
|
12300
13375
|
const seen = /* @__PURE__ */ new Set();
|
|
12301
13376
|
const out = [];
|
|
12302
13377
|
for (const dir of dirs) {
|
|
12303
|
-
const resolved =
|
|
13378
|
+
const resolved = path13.resolve(dir);
|
|
12304
13379
|
if (seen.has(resolved)) continue;
|
|
12305
13380
|
seen.add(resolved);
|
|
12306
13381
|
out.push(resolved);
|
|
@@ -12309,13 +13384,13 @@ function dedupe2(dirs) {
|
|
|
12309
13384
|
}
|
|
12310
13385
|
|
|
12311
13386
|
// src/prompts.ts
|
|
12312
|
-
import { readFileSync as
|
|
12313
|
-
import { homedir as
|
|
13387
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
|
|
13388
|
+
import { homedir as homedir13 } from "os";
|
|
12314
13389
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
12315
|
-
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as
|
|
13390
|
+
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join16 } from "path";
|
|
12316
13391
|
function readPromptFileIfPresent(path15) {
|
|
12317
13392
|
try {
|
|
12318
|
-
const content =
|
|
13393
|
+
const content = readFileSync10(path15, "utf8").trim();
|
|
12319
13394
|
return content || void 0;
|
|
12320
13395
|
} catch {
|
|
12321
13396
|
return void 0;
|
|
@@ -12324,7 +13399,7 @@ function readPromptFileIfPresent(path15) {
|
|
|
12324
13399
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
12325
13400
|
let dir = cwd;
|
|
12326
13401
|
while (true) {
|
|
12327
|
-
const content = readPromptFileIfPresent(
|
|
13402
|
+
const content = readPromptFileIfPresent(join16(dir, "AGENTS.md"));
|
|
12328
13403
|
if (content) return content;
|
|
12329
13404
|
const parent = dirname6(dir);
|
|
12330
13405
|
if (parent === dir) return void 0;
|
|
@@ -12435,7 +13510,7 @@ var CLAUDE_NATIVE_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLA
|
|
|
12435
13510
|
function userAuthoredInstructions(forwarded, opts = {}) {
|
|
12436
13511
|
const readFile3 = opts.readFile ?? ((path15) => {
|
|
12437
13512
|
try {
|
|
12438
|
-
return
|
|
13513
|
+
return readFileSync10(path15, "utf8");
|
|
12439
13514
|
} catch {
|
|
12440
13515
|
return null;
|
|
12441
13516
|
}
|
|
@@ -12476,8 +13551,8 @@ ${options.compressionSummary.trim()}`
|
|
|
12476
13551
|
for (const s of extraSystemContent) {
|
|
12477
13552
|
if (s.trim()) parts.push(s.trim());
|
|
12478
13553
|
}
|
|
12479
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
12480
|
-
const globalAgents = readPromptFileIfPresent(
|
|
13554
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join16(homedir13(), ".config");
|
|
13555
|
+
const globalAgents = readPromptFileIfPresent(join16(configRoot, "opencode", "AGENTS.md"));
|
|
12481
13556
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
12482
13557
|
const forwarded = extraSystemContent.join("\n\n");
|
|
12483
13558
|
const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
|
|
@@ -12493,9 +13568,9 @@ ${options.compressionSummary.trim()}`
|
|
|
12493
13568
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
12494
13569
|
const content = parts.join("\n\n");
|
|
12495
13570
|
if (!content) return void 0;
|
|
12496
|
-
const path15 =
|
|
13571
|
+
const path15 = join16(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
|
|
12497
13572
|
try {
|
|
12498
|
-
|
|
13573
|
+
writeFileSync9(path15, content, { encoding: "utf8", mode: 384 });
|
|
12499
13574
|
return path15;
|
|
12500
13575
|
} catch (err) {
|
|
12501
13576
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
@@ -13113,12 +14188,43 @@ async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCom
|
|
|
13113
14188
|
};
|
|
13114
14189
|
});
|
|
13115
14190
|
}
|
|
13116
|
-
const
|
|
14191
|
+
const cap = config.maxBackgroundSubagents;
|
|
14192
|
+
const admit = cap !== void 0 && tools.some((t) => t.name === "task" || t.name === TASK_BATCH_TOOL_NAME) ? (toolName, input) => admitBackgroundDispatch(toolName, input, cap) : void 0;
|
|
14193
|
+
const srv = await createProxyMcpServer(tools, timeoutOverrides, interceptors, { admit });
|
|
13117
14194
|
srv.calls.on("call", (call) => {
|
|
13118
14195
|
queuePendingProxyCall(sessionKeyForCalls, call, timeoutOverrides);
|
|
13119
14196
|
});
|
|
13120
14197
|
return srv;
|
|
13121
14198
|
}
|
|
14199
|
+
async function resolveSpawnCliCaps(caps, cliPath, cliVersion, whenAborted) {
|
|
14200
|
+
const result = {};
|
|
14201
|
+
if (caps.maxTurns !== void 0) {
|
|
14202
|
+
if (cliSupportsMaxTurns(cliVersion)) result.maxTurns = caps.maxTurns;
|
|
14203
|
+
else warnCapDropped("max-turns");
|
|
14204
|
+
}
|
|
14205
|
+
if (caps.maxBudgetUsd !== void 0) {
|
|
14206
|
+
const probe = detectCliSupportsFlag(cliPath, "--max-budget-usd");
|
|
14207
|
+
const supported = whenAborted ? await Promise.race([probe, whenAborted.then(() => false)]) : await probe;
|
|
14208
|
+
if (supported) result.maxBudgetUsd = caps.maxBudgetUsd;
|
|
14209
|
+
else warnCapDropped("max-budget-usd");
|
|
14210
|
+
}
|
|
14211
|
+
return result;
|
|
14212
|
+
}
|
|
14213
|
+
var droppedCapWarnings = /* @__PURE__ */ new Set();
|
|
14214
|
+
function warnCapDropped(cliFlag) {
|
|
14215
|
+
if (droppedCapWarnings.has(cliFlag)) return;
|
|
14216
|
+
droppedCapWarnings.add(cliFlag);
|
|
14217
|
+
log.warn("agent cap not applied: this claude CLI does not take the flag", { cliFlag });
|
|
14218
|
+
}
|
|
14219
|
+
var interactiveCapsWarned = false;
|
|
14220
|
+
function warnCliCapsOnInteractive(caps) {
|
|
14221
|
+
if (interactiveCapsWarned) return;
|
|
14222
|
+
if (caps.maxTurns === void 0 && caps.maxBudgetUsd === void 0) return;
|
|
14223
|
+
interactiveCapsWarned = true;
|
|
14224
|
+
log.warn(
|
|
14225
|
+
"agent maxTurns / maxBudgetUsd not applied: the interactive transport cannot pass print-only flags"
|
|
14226
|
+
);
|
|
14227
|
+
}
|
|
13122
14228
|
|
|
13123
14229
|
// src/mcp-hot-reload.ts
|
|
13124
14230
|
var DEFAULT_MCP_HOT_RELOAD_COOLDOWN_MS = 6e4;
|
|
@@ -13437,8 +14543,8 @@ function watchTurnAbort(signal) {
|
|
|
13437
14543
|
let aborted = signal?.aborted === true;
|
|
13438
14544
|
let listeners = [];
|
|
13439
14545
|
let settle;
|
|
13440
|
-
const whenAborted = new Promise((
|
|
13441
|
-
settle =
|
|
14546
|
+
const whenAborted = new Promise((resolve7) => {
|
|
14547
|
+
settle = resolve7;
|
|
13442
14548
|
});
|
|
13443
14549
|
const fire = () => {
|
|
13444
14550
|
if (aborted && listeners === null) return;
|
|
@@ -14227,6 +15333,13 @@ function createLineHandler(state, ctx) {
|
|
|
14227
15333
|
};
|
|
14228
15334
|
rememberAccountLimit(ctx.sourceAccount, state.accountLimitHit);
|
|
14229
15335
|
}
|
|
15336
|
+
if (info?.rateLimitType && info.status) {
|
|
15337
|
+
recordUsageReading(ctx.usageAccount, info.rateLimitType, {
|
|
15338
|
+
utilization: info.utilization ?? null,
|
|
15339
|
+
resetsAt: resetsAtToMs(info.resetsAt),
|
|
15340
|
+
status: info.status
|
|
15341
|
+
});
|
|
15342
|
+
}
|
|
14230
15343
|
const note = reportRateLimitEvent(msg);
|
|
14231
15344
|
if (note) {
|
|
14232
15345
|
state.controller.enqueue({ type: "text-delta", id: state.startTextBlock(), delta: note });
|
|
@@ -14514,6 +15627,7 @@ ${plan}
|
|
|
14514
15627
|
resultText: msg.message.content.filter((b) => b.type === "text" && b.text).map((b) => String(b.text)).join("\n")
|
|
14515
15628
|
}));
|
|
14516
15629
|
const limitErrorReply = limitFromReply && ctx.usageLimitNoteActive;
|
|
15630
|
+
const autoFailoverReply = (limitFromReply || accountBlockKind(msg) !== null) && ctx.autoFailoverTarget() !== void 0;
|
|
14517
15631
|
const compactionApiError = ctx.compactionMode && (apiErrorReply || typeof msg.error === "string");
|
|
14518
15632
|
if (limitFromReply) {
|
|
14519
15633
|
state.accountLimitHit ??= recallAccountLimit(ctx.sourceAccount);
|
|
@@ -14526,6 +15640,15 @@ ${plan}
|
|
|
14526
15640
|
window: known.window ?? said.window,
|
|
14527
15641
|
...known.resetsAt === void 0 && said.resetsText ? { resetsText: said.resetsText } : {}
|
|
14528
15642
|
};
|
|
15643
|
+
if (ctx.interactive) {
|
|
15644
|
+
const hit = state.accountLimitHit;
|
|
15645
|
+
recordUsageReading(ctx.usageAccount, hit.window ?? "unknown", {
|
|
15646
|
+
utilization: null,
|
|
15647
|
+
resetsAt: resetsAtToMs(hit.resetsAt),
|
|
15648
|
+
resetsText: hit.resetsText,
|
|
15649
|
+
status: "rejected"
|
|
15650
|
+
});
|
|
15651
|
+
}
|
|
14529
15652
|
}
|
|
14530
15653
|
if (hasText && !hasToolUse) {
|
|
14531
15654
|
startResultFallback(state);
|
|
@@ -14535,7 +15658,7 @@ ${plan}
|
|
|
14535
15658
|
}
|
|
14536
15659
|
for (const block2 of msg.message.content) {
|
|
14537
15660
|
if (block2.type === "text" && block2.text) {
|
|
14538
|
-
if (limitErrorReply) continue;
|
|
15661
|
+
if (limitErrorReply || autoFailoverReply) continue;
|
|
14539
15662
|
if (compactionApiError) {
|
|
14540
15663
|
state.compactionFailureText ??= block2.text;
|
|
14541
15664
|
continue;
|
|
@@ -14755,6 +15878,7 @@ ${plan}
|
|
|
14755
15878
|
if (msg.session_id) {
|
|
14756
15879
|
setClaudeSessionId(state.sessionKey, msg.session_id);
|
|
14757
15880
|
}
|
|
15881
|
+
if (ctx.budget) recordResultCost(ctx.budget, msg.session_id, msg.total_cost_usd);
|
|
14758
15882
|
if (deliverPendingCompletions(state)) {
|
|
14759
15883
|
return;
|
|
14760
15884
|
}
|
|
@@ -14763,9 +15887,11 @@ ${plan}
|
|
|
14763
15887
|
})) {
|
|
14764
15888
|
state.accountLimitHit = recallAccountLimit(ctx.sourceAccount);
|
|
14765
15889
|
}
|
|
15890
|
+
const autoFailoverResult = msg.is_error === true && (!!state.accountLimitHit || !!state.accountBlock) && ctx.autoFailoverTarget() !== void 0;
|
|
14766
15891
|
state.usageLimitNote = ctx.usageLimitNoteActive && !!state.accountLimitHit && // An expired login or a billing hold writes its own note just below
|
|
14767
15892
|
// and says what to run; two notes about one failure is one too many.
|
|
14768
|
-
!state.accountBlock && msg.is_error === true
|
|
15893
|
+
!state.accountBlock && msg.is_error === true && // Moved to another account: that account's answer is this turn's.
|
|
15894
|
+
!autoFailoverResult;
|
|
14769
15895
|
const compactionFailure = ctx.compactionMode && (msg.is_error === true || typeof msg.subtype === "string" && msg.subtype !== "success");
|
|
14770
15896
|
if (compactionFailure) {
|
|
14771
15897
|
if (typeof msg.result === "string" && msg.result.trim()) {
|
|
@@ -14774,7 +15900,7 @@ ${plan}
|
|
|
14774
15900
|
state.resultFailure ??= state.accountBlock ?? (state.accountLimitHit ? "usage_limit" : void 0) ?? (typeof msg.subtype === "string" && msg.subtype !== "success" ? msg.subtype : "error");
|
|
14775
15901
|
}
|
|
14776
15902
|
if (!state.currentTextId && msg.is_error && !compactionFailure && // Already on screen from the reply frame, word for word.
|
|
14777
|
-
!(typeof msg.result === "string" && state.apiErrorTextShown !== null && msg.result.trim() === state.apiErrorTextShown.trim()) && !state.usageLimitNote && typeof msg.result === "string" && msg.result.trim().length > 0) {
|
|
15903
|
+
!(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) {
|
|
14778
15904
|
const errId = state.startTextBlock();
|
|
14779
15905
|
state.controller.enqueue({
|
|
14780
15906
|
type: "text-delta",
|
|
@@ -14788,14 +15914,14 @@ ${plan}
|
|
|
14788
15914
|
if (state.accountBlock && msg.is_error) {
|
|
14789
15915
|
state.resultFailure ??= state.accountBlock;
|
|
14790
15916
|
const offeringSwitch = ctx.failoverAskActive;
|
|
14791
|
-
if (!compactionFailure) {
|
|
15917
|
+
if (!compactionFailure && !autoFailoverResult) {
|
|
14792
15918
|
state.controller.enqueue({
|
|
14793
15919
|
type: "text-delta",
|
|
14794
15920
|
id: state.startTextBlock(),
|
|
14795
15921
|
delta: formatAccountBlockNote({
|
|
14796
15922
|
kind: state.accountBlock,
|
|
14797
15923
|
account: ctx.sourceAccount,
|
|
14798
|
-
configDir: ctx.
|
|
15924
|
+
configDir: ctx.accountConfigDir,
|
|
14799
15925
|
offeringSwitch,
|
|
14800
15926
|
// Read only when the form is not taking the turn, which is the
|
|
14801
15927
|
// default: moving the work by hand is then the operator's only
|
|
@@ -14814,7 +15940,7 @@ ${plan}
|
|
|
14814
15940
|
const failure = describeResultFailure(msg);
|
|
14815
15941
|
if (failure) {
|
|
14816
15942
|
state.resultFailure = msg.subtype;
|
|
14817
|
-
if (!compactionFailure) {
|
|
15943
|
+
if (!compactionFailure && !autoFailoverResult) {
|
|
14818
15944
|
state.controller.enqueue({
|
|
14819
15945
|
type: "text-delta",
|
|
14820
15946
|
id: state.startTextBlock(),
|
|
@@ -14857,6 +15983,19 @@ ${plan}
|
|
|
14857
15983
|
});
|
|
14858
15984
|
}
|
|
14859
15985
|
}
|
|
15986
|
+
if (ctx.budget && !autoFailoverResult && !state.modelRefusal) {
|
|
15987
|
+
const note = takeBudgetNote(ctx.budget);
|
|
15988
|
+
if (note) {
|
|
15989
|
+
state.controller.enqueue({
|
|
15990
|
+
type: "text-delta",
|
|
15991
|
+
id: state.startTextBlock(),
|
|
15992
|
+
delta: note
|
|
15993
|
+
});
|
|
15994
|
+
log.notice("conversation budget threshold crossed", {
|
|
15995
|
+
sessionId: msg.session_id
|
|
15996
|
+
});
|
|
15997
|
+
}
|
|
15998
|
+
}
|
|
14860
15999
|
state.turnCompleted = true;
|
|
14861
16000
|
state.endTextBlock();
|
|
14862
16001
|
const shouldDeferResult = !msg.is_error && !state.autoContinueState.aborted && !state.autoContinueState.sawAskUserQuestion;
|
|
@@ -15023,6 +16162,66 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15023
16162
|
async resolvePlanModeQuestion(compactionMode, loadLiveToolInfo = () => this.fetchLiveToolInfo()) {
|
|
15024
16163
|
return resolvePlanModeQuestion(this.config, compactionMode, loadLiveToolInfo);
|
|
15025
16164
|
}
|
|
16165
|
+
/**
|
|
16166
|
+
* The ledger this turn keeps under `maxBudgetUsd` (h #g245), or null for
|
|
16167
|
+
* none. The parent lookup runs once per session per process and only with a
|
|
16168
|
+
* cap set, so a default install never pays for it. A lookup that cannot
|
|
16169
|
+
* answer reads as "no parent", which makes the session its own conversation:
|
|
16170
|
+
* its spend is still counted, against its own budget.
|
|
16171
|
+
*/
|
|
16172
|
+
async resolveTurnBudget(affinity, compactionMode, mode, interactive) {
|
|
16173
|
+
const capUsd = this.config.maxBudgetUsd;
|
|
16174
|
+
if (capUsd === void 0 || !affinity || affinity === "default") return null;
|
|
16175
|
+
if (!sessionParentKnown(affinity)) {
|
|
16176
|
+
noteSessionParent(affinity, await fetchSessionParentId(affinity) ?? null);
|
|
16177
|
+
}
|
|
16178
|
+
return {
|
|
16179
|
+
session: affinity,
|
|
16180
|
+
capUsd,
|
|
16181
|
+
warnAt: this.config.budgetWarnAt ?? [],
|
|
16182
|
+
notes: !compactionMode && mode !== "generate" && !isChildSession(affinity),
|
|
16183
|
+
// The interactive transport synthesizes a per-turn `result` (h #g208);
|
|
16184
|
+
// headless reports the Claude session's running total.
|
|
16185
|
+
cumulative: !interactive
|
|
16186
|
+
};
|
|
16187
|
+
}
|
|
16188
|
+
/**
|
|
16189
|
+
* A turn the session budget stopped before anything was spawned: one
|
|
16190
|
+
* `▌ **budget:**` note (when the turn may write one) and an error finish,
|
|
16191
|
+
* the shape a usage limit has had since h #g214. A child session gets an
|
|
16192
|
+
* `error` part instead of the note, so the parent's `task` result says why
|
|
16193
|
+
* without the note becoming the subagent's answer.
|
|
16194
|
+
*/
|
|
16195
|
+
budgetStopStream(opts) {
|
|
16196
|
+
log.notice("conversation budget reached; turn not started", {
|
|
16197
|
+
child: opts.child
|
|
16198
|
+
});
|
|
16199
|
+
const toUsage2 = this.toUsage.bind(this);
|
|
16200
|
+
const text = formatBudgetStopNote(opts.spentUsd, opts.capUsd);
|
|
16201
|
+
const stream = new ReadableStream({
|
|
16202
|
+
start(controller) {
|
|
16203
|
+
controller.enqueue({ type: "stream-start", warnings: opts.warnings });
|
|
16204
|
+
if (opts.note) {
|
|
16205
|
+
const id = generateId();
|
|
16206
|
+
controller.enqueue({ type: "text-start", id });
|
|
16207
|
+
controller.enqueue({ type: "text-delta", id, delta: text });
|
|
16208
|
+
controller.enqueue({ type: "text-end", id });
|
|
16209
|
+
} else if (opts.child) {
|
|
16210
|
+
controller.enqueue({ type: "error", error: new Error(text) });
|
|
16211
|
+
}
|
|
16212
|
+
controller.enqueue({
|
|
16213
|
+
type: "finish",
|
|
16214
|
+
finishReason: { unified: "error", raw: "budget_exceeded" },
|
|
16215
|
+
usage: toUsage2({ input_tokens: 0, output_tokens: 0 }),
|
|
16216
|
+
providerMetadata: {
|
|
16217
|
+
"claude-code": { path: "budget-stop", synthetic: true, usageUnavailable: true }
|
|
16218
|
+
}
|
|
16219
|
+
});
|
|
16220
|
+
controller.close();
|
|
16221
|
+
}
|
|
16222
|
+
});
|
|
16223
|
+
return { stream, request: { body: { text: "" } } };
|
|
16224
|
+
}
|
|
15026
16225
|
/**
|
|
15027
16226
|
* A dispatch form, as a turn: one synthetic `question` tool call and a
|
|
15028
16227
|
* `tool-calls` finish, which is how the operator's answer comes back on the
|
|
@@ -15312,12 +16511,93 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15312
16511
|
* because which method opencode used is not in them.
|
|
15313
16512
|
*/
|
|
15314
16513
|
async streamTurn(options, mode) {
|
|
15315
|
-
const result = await this.
|
|
16514
|
+
const result = await this.runAccountFailover(options, mode);
|
|
15316
16515
|
return {
|
|
15317
16516
|
...result,
|
|
15318
16517
|
stream: translateStreamForHost(result.stream, this.config.hostApi ?? "v1")
|
|
15319
16518
|
};
|
|
15320
16519
|
}
|
|
16520
|
+
/**
|
|
16521
|
+
* Run the turn, moving the same request to the next eligible configured
|
|
16522
|
+
* account when the one it ran on is out of usage or cannot serve
|
|
16523
|
+
* (`accountFailover: "auto"`). The single wiring point for the automatic
|
|
16524
|
+
* switch, and outside `runModelChain` on purpose: an account switch starts
|
|
16525
|
+
* that account's own model chain from the top.
|
|
16526
|
+
*
|
|
16527
|
+
* Anything but `"auto"` is `runModelChain` and one `await`, so `"off"` and
|
|
16528
|
+
* `"ask"` are exactly what they were. With `"auto"`, the attempt that failed
|
|
16529
|
+
* decides the switch itself (`completeResult` sets `run.switchTo` and closes
|
|
16530
|
+
* its stream without a `finish`), and the next attempt's prologue makes the
|
|
16531
|
+
* switch the form's pick would have made. This only splices the attempts
|
|
16532
|
+
* into one caller turn: one `stream-start`, and the `finish` of whichever
|
|
16533
|
+
* attempt did not switch.
|
|
16534
|
+
*
|
|
16535
|
+
* The bound is `run.tried`, as it is `tried` for the model chain: every
|
|
16536
|
+
* account spawned or switched to goes in, and none in it is picked again.
|
|
16537
|
+
*
|
|
16538
|
+
* Cancelling the returned stream is a stop, for every attempt: this loop is
|
|
16539
|
+
* the plugin starting more work on the caller's behalf, so a caller that
|
|
16540
|
+
* has gone away must reach all of it. Each attempt gets a signal that the
|
|
16541
|
+
* caller's own `abortSignal` and a `cancel()` both abort, and the loop
|
|
16542
|
+
* starts no further account once either has happened.
|
|
16543
|
+
*/
|
|
16544
|
+
async runAccountFailover(options, mode) {
|
|
16545
|
+
if (this.config.accountFailover !== "auto") return this.runModelChain(options, mode);
|
|
16546
|
+
const self = this;
|
|
16547
|
+
const run = { tried: /* @__PURE__ */ new Set() };
|
|
16548
|
+
const cancelled2 = new AbortController();
|
|
16549
|
+
const attemptOptions = {
|
|
16550
|
+
...options,
|
|
16551
|
+
abortSignal: options.abortSignal ? AbortSignal.any([options.abortSignal, cancelled2.signal]) : cancelled2.signal
|
|
16552
|
+
};
|
|
16553
|
+
const stopped = () => attemptOptions.abortSignal.aborted;
|
|
16554
|
+
let inner = await this.runModelChain(attemptOptions, mode, run);
|
|
16555
|
+
const request = inner.request;
|
|
16556
|
+
let reader;
|
|
16557
|
+
const stream = new ReadableStream({
|
|
16558
|
+
async start(controller) {
|
|
16559
|
+
let started2 = false;
|
|
16560
|
+
try {
|
|
16561
|
+
for (; ; ) {
|
|
16562
|
+
reader = inner.stream.getReader();
|
|
16563
|
+
try {
|
|
16564
|
+
for (; ; ) {
|
|
16565
|
+
const { done, value } = await reader.read();
|
|
16566
|
+
if (done) break;
|
|
16567
|
+
if (value.type === "stream-start") {
|
|
16568
|
+
if (started2) continue;
|
|
16569
|
+
started2 = true;
|
|
16570
|
+
}
|
|
16571
|
+
controller.enqueue(value);
|
|
16572
|
+
}
|
|
16573
|
+
} finally {
|
|
16574
|
+
reader.releaseLock();
|
|
16575
|
+
}
|
|
16576
|
+
if (!run.switchTo) break;
|
|
16577
|
+
if (stopped()) {
|
|
16578
|
+
log.info("turn stopped before the automatic account switch; not switching", {
|
|
16579
|
+
target: run.switchTo.target
|
|
16580
|
+
});
|
|
16581
|
+
break;
|
|
16582
|
+
}
|
|
16583
|
+
inner = await self.runModelChain(attemptOptions, mode, run);
|
|
16584
|
+
}
|
|
16585
|
+
} catch (error) {
|
|
16586
|
+
if (!stopped()) controller.enqueue({ type: "error", error });
|
|
16587
|
+
} finally {
|
|
16588
|
+
try {
|
|
16589
|
+
controller.close();
|
|
16590
|
+
} catch {
|
|
16591
|
+
}
|
|
16592
|
+
}
|
|
16593
|
+
},
|
|
16594
|
+
async cancel(reason) {
|
|
16595
|
+
cancelled2.abort(reason);
|
|
16596
|
+
await reader?.cancel(reason).catch(() => void 0);
|
|
16597
|
+
}
|
|
16598
|
+
});
|
|
16599
|
+
return { stream, request };
|
|
16600
|
+
}
|
|
15321
16601
|
/**
|
|
15322
16602
|
* Run the turn, moving to the next model in the fallback chain if the one
|
|
15323
16603
|
* it started on is refused. The single wiring point for `src/model-fallback.ts`.
|
|
@@ -15336,14 +16616,14 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15336
16616
|
* and an exhausted chain leaves the last attempt un-armed so its error
|
|
15337
16617
|
* surfaces exactly as it does today.
|
|
15338
16618
|
*/
|
|
15339
|
-
async runModelChain(options, mode) {
|
|
16619
|
+
async runModelChain(options, mode, accountRun) {
|
|
15340
16620
|
const agent = this.getOpencodeAgent(options);
|
|
15341
16621
|
const chain = this.isCompactionCall(options) ? [] : resolveFallbackChain(agent, resolveAgentModel(agent, this.modelId));
|
|
15342
|
-
if (chain.length === 0) return this.doStreamForHost(options, void 0, mode);
|
|
16622
|
+
if (chain.length === 0) return this.doStreamForHost(options, void 0, mode, accountRun);
|
|
15343
16623
|
const self = this;
|
|
15344
16624
|
const tried = /* @__PURE__ */ new Set();
|
|
15345
16625
|
let attempt = { armed: true };
|
|
15346
|
-
let inner = await this.doStreamForHost(options, attempt, mode);
|
|
16626
|
+
let inner = await this.doStreamForHost(options, attempt, mode, accountRun);
|
|
15347
16627
|
const request = inner.request;
|
|
15348
16628
|
const stream = new ReadableStream({
|
|
15349
16629
|
async start(controller) {
|
|
@@ -15413,7 +16693,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15413
16693
|
modelOverride: next,
|
|
15414
16694
|
armed: nextFallbackModel(chain, tried) !== void 0
|
|
15415
16695
|
};
|
|
15416
|
-
inner = await self.doStreamForHost(options, attempt, mode);
|
|
16696
|
+
inner = await self.doStreamForHost(options, attempt, mode, accountRun);
|
|
15417
16697
|
}
|
|
15418
16698
|
} catch (error) {
|
|
15419
16699
|
controller.enqueue({ type: "error", error });
|
|
@@ -15427,8 +16707,10 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15427
16707
|
});
|
|
15428
16708
|
return { stream, request };
|
|
15429
16709
|
}
|
|
15430
|
-
async doStreamForHost(options, attempt, mode = "stream") {
|
|
16710
|
+
async doStreamForHost(options, attempt, mode = "stream", accountRun) {
|
|
15431
16711
|
const warnings = [];
|
|
16712
|
+
const autoSwitch = accountRun?.switchTo;
|
|
16713
|
+
if (accountRun) accountRun.switchTo = void 0;
|
|
15432
16714
|
const turnAbort = watchTurnAbort(options.abortSignal);
|
|
15433
16715
|
const abortedBeforeWork = (stage) => {
|
|
15434
16716
|
log.info("abort before the turn asked claude for work; nothing spawned", {
|
|
@@ -15466,6 +16748,11 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15466
16748
|
this.getOpencodeAgent(options) ?? null
|
|
15467
16749
|
];
|
|
15468
16750
|
if (promptCacheTtl) context.push(promptCacheTtl);
|
|
16751
|
+
const agentCliCaps = compactionMode ? {} : resolveAgentCliCaps(this.getOpencodeAgent(options));
|
|
16752
|
+
if (agentCliCaps.maxTurns !== void 0) context.push(`maxTurns=${agentCliCaps.maxTurns}`);
|
|
16753
|
+
if (agentCliCaps.maxBudgetUsd !== void 0) {
|
|
16754
|
+
context.push(`maxBudgetUsd=${agentCliCaps.maxBudgetUsd}`);
|
|
16755
|
+
}
|
|
15469
16756
|
const baseKey = sessionKey(
|
|
15470
16757
|
cwd,
|
|
15471
16758
|
`${effectiveModelId}::${scope}::${affinity}::context=${JSON.stringify(context)}`
|
|
@@ -15523,7 +16810,10 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15523
16810
|
configDir: this.config.accountInProcess ? this.config.configDir : void 0,
|
|
15524
16811
|
// `/claude-code-doctor usage` opts into the CLI's own plan-usage
|
|
15525
16812
|
// report; anything else here is ignored, as it always has been.
|
|
15526
|
-
argument: doctor.rest
|
|
16813
|
+
argument: doctor.rest,
|
|
16814
|
+
maxBudgetUsd: this.config.maxBudgetUsd,
|
|
16815
|
+
budgetWarnAt: this.config.budgetWarnAt,
|
|
16816
|
+
maxBackgroundSubagents: this.config.maxBackgroundSubagents
|
|
15527
16817
|
};
|
|
15528
16818
|
const stream2 = new ReadableStream({
|
|
15529
16819
|
async start(controller) {
|
|
@@ -15659,6 +16949,23 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15659
16949
|
if (dispatchStep?.kind === "release") {
|
|
15660
16950
|
return this.subagentDispatchReleaseStream(dispatchStep, sk, warnings);
|
|
15661
16951
|
}
|
|
16952
|
+
const budget = await this.resolveTurnBudget(affinity, compactionMode, mode, useInteractive);
|
|
16953
|
+
if (budget && !compactionMode) {
|
|
16954
|
+
const root = budgetRoot(budget.session);
|
|
16955
|
+
const spentUsd = sessionSpend(root);
|
|
16956
|
+
if (spentUsd >= budget.capUsd && getPendingProxyCalls(sk).length === 0) {
|
|
16957
|
+
turnAbort.dispose();
|
|
16958
|
+
return this.budgetStopStream({
|
|
16959
|
+
spentUsd,
|
|
16960
|
+
capUsd: budget.capUsd,
|
|
16961
|
+
// The note is the operator's; a `doGenerate` turn's text is a return
|
|
16962
|
+
// value and a child's can become its parent's `task` result.
|
|
16963
|
+
note: mode !== "generate" && !isChildSession(budget.session),
|
|
16964
|
+
child: isChildSession(budget.session),
|
|
16965
|
+
warnings
|
|
16966
|
+
});
|
|
16967
|
+
}
|
|
16968
|
+
}
|
|
15662
16969
|
if (!compactionMode) invalidateOtherEffortSessions(baseKey, reasoningEffort);
|
|
15663
16970
|
if (!compactionMode && affinity !== "default") {
|
|
15664
16971
|
recordSessionSpawn(affinity, {
|
|
@@ -15668,12 +16975,17 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15668
16975
|
});
|
|
15669
16976
|
}
|
|
15670
16977
|
const hasPriorConversation = options.prompt.filter((m) => m.role === "user" || m.role === "assistant").length > 1;
|
|
15671
|
-
if (!hasPriorConversation) {
|
|
16978
|
+
if (!hasPriorConversation && !autoSwitch) {
|
|
15672
16979
|
deleteClaudeSessionId(sk);
|
|
15673
16980
|
deleteActiveProcess(sk);
|
|
15674
16981
|
clearCompression(sk);
|
|
15675
16982
|
}
|
|
15676
|
-
const failoverAnswer =
|
|
16983
|
+
const failoverAnswer = autoSwitch ? {
|
|
16984
|
+
kind: "switch",
|
|
16985
|
+
target: autoSwitch.target,
|
|
16986
|
+
sourceAccount: autoSwitch.sourceAccount,
|
|
16987
|
+
resetsAt: autoSwitch.resetsAt
|
|
16988
|
+
} : compactionMode || mode === "generate" ? null : consumeAccountFailoverAnswer(sk, options.prompt, {
|
|
15677
16989
|
sourceAccount,
|
|
15678
16990
|
candidates: failoverCandidates(
|
|
15679
16991
|
this.config.failoverAccounts,
|
|
@@ -15731,13 +17043,84 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15731
17043
|
cliPath: this.config.cliPath,
|
|
15732
17044
|
modelId: effectiveModelId
|
|
15733
17045
|
});
|
|
17046
|
+
while (autoSwitch && accountRun && failover.target !== failoverAnswer.target) {
|
|
17047
|
+
log.notice("automatic account switch skipped an account whose runtime could not be prepared", {
|
|
17048
|
+
sessionKey: sk,
|
|
17049
|
+
target: failoverAnswer.target
|
|
17050
|
+
});
|
|
17051
|
+
const next = nextAutoFailoverAccount(
|
|
17052
|
+
failoverCandidates(this.config.failoverAccounts, sourceAccount, groups),
|
|
17053
|
+
accountRun.tried
|
|
17054
|
+
);
|
|
17055
|
+
if (!next) {
|
|
17056
|
+
clearAccountOverride(failoverAnswer.sourceAccount);
|
|
17057
|
+
log.warn("no configured account could take this turn; it ends here", {
|
|
17058
|
+
sessionKey: sk,
|
|
17059
|
+
account: autoSwitch.failedAccount,
|
|
17060
|
+
cause: autoSwitch.cause
|
|
17061
|
+
});
|
|
17062
|
+
const note = mode === "generate" ? null : autoSwitch.cause === "usage_limit" ? formatUsageLimitNote({
|
|
17063
|
+
sourceAccount: autoSwitch.failedAccount,
|
|
17064
|
+
candidates: [],
|
|
17065
|
+
resetsAt: autoSwitch.resetsAt,
|
|
17066
|
+
window: autoSwitch.window
|
|
17067
|
+
}) : formatAccountBlockNote({
|
|
17068
|
+
kind: autoSwitch.cause,
|
|
17069
|
+
account: autoSwitch.failedAccount,
|
|
17070
|
+
configDir: autoSwitch.failedConfigDir,
|
|
17071
|
+
offeringSwitch: false,
|
|
17072
|
+
candidates: []
|
|
17073
|
+
});
|
|
17074
|
+
const cause = autoSwitch.cause;
|
|
17075
|
+
const exhausted = new ReadableStream({
|
|
17076
|
+
start(controller) {
|
|
17077
|
+
controller.enqueue({ type: "stream-start", warnings });
|
|
17078
|
+
if (note) {
|
|
17079
|
+
const id = generateId();
|
|
17080
|
+
controller.enqueue({ type: "text-start", id });
|
|
17081
|
+
controller.enqueue({ type: "text-delta", id, delta: note });
|
|
17082
|
+
controller.enqueue({ type: "text-end", id });
|
|
17083
|
+
}
|
|
17084
|
+
controller.enqueue({
|
|
17085
|
+
type: "finish",
|
|
17086
|
+
finishReason: { unified: "error", raw: cause },
|
|
17087
|
+
usage: toUsage2({ input_tokens: 0, output_tokens: 0 }),
|
|
17088
|
+
providerMetadata: {
|
|
17089
|
+
"claude-code": {
|
|
17090
|
+
path: "account-failover-exhausted",
|
|
17091
|
+
synthetic: true,
|
|
17092
|
+
usageUnavailable: true
|
|
17093
|
+
}
|
|
17094
|
+
}
|
|
17095
|
+
});
|
|
17096
|
+
controller.close();
|
|
17097
|
+
}
|
|
17098
|
+
});
|
|
17099
|
+
return { stream: exhausted, request: { body: { text: "" } } };
|
|
17100
|
+
}
|
|
17101
|
+
accountRun.tried.add(next);
|
|
17102
|
+
failoverAnswer.target = next;
|
|
17103
|
+
setAccountOverride(
|
|
17104
|
+
failoverAnswer.sourceAccount,
|
|
17105
|
+
next,
|
|
17106
|
+
failoverUntil(failoverAnswer.resetsAt)
|
|
17107
|
+
);
|
|
17108
|
+
failover = await resolveFailoverSpawn({
|
|
17109
|
+
account: sourceAccount,
|
|
17110
|
+
baseCliPath,
|
|
17111
|
+
cliPath: this.config.cliPath,
|
|
17112
|
+
modelId: effectiveModelId
|
|
17113
|
+
});
|
|
17114
|
+
}
|
|
15734
17115
|
cliPath = failover.cliPath;
|
|
15735
17116
|
asideTransportRef.cliPath = cliPath;
|
|
15736
|
-
|
|
15737
|
-
|
|
15738
|
-
|
|
15739
|
-
|
|
15740
|
-
|
|
17117
|
+
if (!autoSwitch) {
|
|
17118
|
+
failoverNote = formatFailoverNote({
|
|
17119
|
+
sourceAccount: failoverAnswer.sourceAccount,
|
|
17120
|
+
target: failoverAnswer.target,
|
|
17121
|
+
resetsAt: failoverAnswer.resetsAt
|
|
17122
|
+
});
|
|
17123
|
+
}
|
|
15741
17124
|
}
|
|
15742
17125
|
const spawnConfigDir = this.config.accountInProcess ? this.skillBridgeSpawn(failover).configDir : void 0;
|
|
15743
17126
|
const processForAccount = getActiveProcess(sk);
|
|
@@ -15801,6 +17184,15 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15801
17184
|
if (failoverAnswer?.kind === "switch" && hasPriorConversation && !conversationOnTarget) {
|
|
15802
17185
|
includeHistoryContext = true;
|
|
15803
17186
|
}
|
|
17187
|
+
if (autoSwitch && failoverAnswer?.kind === "switch" && mode !== "generate") {
|
|
17188
|
+
failoverNote = formatAutoFailoverNote({
|
|
17189
|
+
failedAccount: autoSwitch.failedAccount,
|
|
17190
|
+
target: failoverAnswer.target,
|
|
17191
|
+
resetsAt: autoSwitch.resetsAt,
|
|
17192
|
+
reason: autoSwitch.reason,
|
|
17193
|
+
carried: conversationOnTarget
|
|
17194
|
+
});
|
|
17195
|
+
}
|
|
15804
17196
|
let accountGroupNote = null;
|
|
15805
17197
|
if (!compactionMode && groups && accountProviders.size > 0 && includeHistoryContext && failoverAnswer?.kind !== "switch") {
|
|
15806
17198
|
const foreign = findForeignAccountSibling({
|
|
@@ -15870,7 +17262,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15870
17262
|
let replayReason = "no-session-for-key";
|
|
15871
17263
|
if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
|
|
15872
17264
|
const configDir = resolveConfigDir(this.config.configDir);
|
|
15873
|
-
const transcriptPath2 = (id, dir) =>
|
|
17265
|
+
const transcriptPath2 = (id, dir) => join17(dir ?? configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
|
|
15874
17266
|
const resumePoint = findResumePoint({
|
|
15875
17267
|
sessionKey: sk,
|
|
15876
17268
|
prompt: options.prompt,
|
|
@@ -15973,6 +17365,30 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15973
17365
|
messages: options.prompt.length
|
|
15974
17366
|
});
|
|
15975
17367
|
}
|
|
17368
|
+
const conversationChange = !compactionMode && !includeHistoryContext && failoverAnswer?.kind !== "switch" && !accountGroupNote && (!!getClaudeSessionId(sk) || !!getActiveProcess(sk)) ? classifyConversationChange(sk, filterSideQuestionHistory(options.prompt)) : null;
|
|
17369
|
+
const missed = conversationChange?.kind === "missed" ? conversationChange : null;
|
|
17370
|
+
if (missed) {
|
|
17371
|
+
log.notice("replaying the messages this claude session missed: the conversation continued under another session key", {
|
|
17372
|
+
sessionKey: sk,
|
|
17373
|
+
messages: missed.messages.length,
|
|
17374
|
+
userMessages: missed.userMessages
|
|
17375
|
+
});
|
|
17376
|
+
}
|
|
17377
|
+
let rewoundNote = null;
|
|
17378
|
+
if (conversationChange?.kind === "rewound") {
|
|
17379
|
+
await deleteActiveProcessAndWait(sk);
|
|
17380
|
+
deleteClaudeSessionId(sk);
|
|
17381
|
+
includeHistoryContext = true;
|
|
17382
|
+
replayReason = "history-rewound";
|
|
17383
|
+
log.notice("replaying the conversation as text: the thread was edited or undone", {
|
|
17384
|
+
sessionKey: sk,
|
|
17385
|
+
reason: "history-rewound",
|
|
17386
|
+
sharedMessages: conversationChange.sharedMessages,
|
|
17387
|
+
removedMessages: conversationChange.removedMessages,
|
|
17388
|
+
removedUserMessages: conversationChange.removedUserMessages
|
|
17389
|
+
});
|
|
17390
|
+
if (mode !== "generate") rewoundNote = formatConversationRewoundNote();
|
|
17391
|
+
}
|
|
15976
17392
|
if (this.config.forkSessions === true && !compactionMode) {
|
|
15977
17393
|
recordForkFingerprint(sk, options.prompt, cliPath);
|
|
15978
17394
|
}
|
|
@@ -15993,7 +17409,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
15993
17409
|
compactionMode,
|
|
15994
17410
|
compactionInstructions: useInteractive && compactionMode ? extractSystemMessages(effectivePrompt).join("\n\n") : void 0,
|
|
15995
17411
|
cliToolCallIds: new Set(previousPendingProxyCalls.map((c) => c.toolCallId)),
|
|
15996
|
-
stripContextReminders: this.stripContextRemindersEnabled()
|
|
17412
|
+
stripContextReminders: this.stripContextRemindersEnabled(),
|
|
17413
|
+
missed: missed?.messages
|
|
15997
17414
|
});
|
|
15998
17415
|
const ptyPlanMode = useInteractive && !compactionMode && this.config.permissionMode === "plan";
|
|
15999
17416
|
const resolvedProxy = compactionMode ? null : ptyPlanMode ? this.resolvedProxyTools()?.filter((def) => def.name !== "write" && def.name !== "edit") ?? null : this.resolvedProxyTools();
|
|
@@ -16058,7 +17475,24 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16058
17475
|
// the operator is usually not even looking at.
|
|
16059
17476
|
childSession: !!await fetchSessionParentId(affinity)
|
|
16060
17477
|
});
|
|
17478
|
+
const spawnedAccount = failover.target ?? sourceAccount;
|
|
17479
|
+
accountRun?.tried.add(spawnedAccount);
|
|
17480
|
+
let autoFailoverChoice;
|
|
17481
|
+
const autoFailoverTarget = () => {
|
|
17482
|
+
if (autoFailoverChoice === void 0) {
|
|
17483
|
+
autoFailoverChoice = accountRun && !compactionMode ? nextAutoFailoverAccount(failoverAccounts, accountRun.tried) ?? null : null;
|
|
17484
|
+
}
|
|
17485
|
+
return autoFailoverChoice ?? void 0;
|
|
17486
|
+
};
|
|
17487
|
+
const limitAccount = accountRun ? spawnedAccount : sourceAccount;
|
|
16061
17488
|
const usageLimitNoteActive = !compactionMode && mode !== "generate" && !failoverAskActive;
|
|
17489
|
+
const usageWarnThresholds = resolveUsageWarnThresholds(this.config.usageWarnThresholds);
|
|
17490
|
+
const usageSwitchAt = this.config.usageSwitchAt === null ? null : resolveUsageSwitchAt(this.config.usageSwitchAt);
|
|
17491
|
+
const usageNoteThresholds = [
|
|
17492
|
+
.../* @__PURE__ */ new Set([...usageWarnThresholds, ...usageSwitchAt === null ? [] : [usageSwitchAt]])
|
|
17493
|
+
];
|
|
17494
|
+
const usageNotesPossible = usageNoteThresholds.length > 0 && !compactionMode && mode !== "generate";
|
|
17495
|
+
const usageChildSession = usageNotesPossible ? watchChildSession(affinity) : () => true;
|
|
16062
17496
|
let staleBuild = null;
|
|
16063
17497
|
if (!compactionMode && mode !== "generate" && affinity !== "default") {
|
|
16064
17498
|
staleBuild = staleBuildWatch().check();
|
|
@@ -16385,11 +17819,20 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16385
17819
|
);
|
|
16386
17820
|
}
|
|
16387
17821
|
const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;
|
|
17822
|
+
const readRules = resolveReadRuleMapping({
|
|
17823
|
+
layers: opencodeConfigLayers(cwd, self.config.hostApi === "v2" ? "v2" : "v1"),
|
|
17824
|
+
hostApi: self.config.hostApi === "v2" ? "v2" : "v1",
|
|
17825
|
+
enabled: self.config.mapReadRules !== false,
|
|
17826
|
+
worktree: detectWorktree(cwd),
|
|
17827
|
+
cwd
|
|
17828
|
+
});
|
|
16388
17829
|
const allDisallowed = resolveDisallowedTools({
|
|
16389
17830
|
proxyTools: enrichedProxy,
|
|
16390
17831
|
extraDisallowedTools: self.config.extraDisallowedTools,
|
|
16391
|
-
disableWebSearch: self.config.webSearch === "disabled"
|
|
17832
|
+
disableWebSearch: self.config.webSearch === "disabled",
|
|
17833
|
+
readRules: readRules.deny
|
|
16392
17834
|
});
|
|
17835
|
+
recordReadRuleMapping(readRules);
|
|
16393
17836
|
return {
|
|
16394
17837
|
excludeServers,
|
|
16395
17838
|
taskProxyEnabled,
|
|
@@ -16500,6 +17943,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16500
17943
|
supportsBypass: ptyBypassSupported
|
|
16501
17944
|
});
|
|
16502
17945
|
if (!posture) throw new Error("the interactive transport cannot hold this permission posture");
|
|
17946
|
+
warnCliCapsOnInteractive(agentCliCaps);
|
|
16503
17947
|
const ap = spawnInteractiveProcess({
|
|
16504
17948
|
cwd,
|
|
16505
17949
|
cliPath,
|
|
@@ -16623,7 +18067,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16623
18067
|
pluginDirs: skillPluginDirs,
|
|
16624
18068
|
...self.thinkingCliOptions(),
|
|
16625
18069
|
fastMode,
|
|
16626
|
-
cliVersion
|
|
18070
|
+
cliVersion,
|
|
18071
|
+
...await resolveSpawnCliCaps(agentCliCaps, cliPath, cliVersion, turnAbort.whenAborted)
|
|
16627
18072
|
});
|
|
16628
18073
|
spawnSystemPromptFile = systemPromptFile;
|
|
16629
18074
|
spawnProxyServer = state.proxyServer;
|
|
@@ -16673,6 +18118,14 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16673
18118
|
});
|
|
16674
18119
|
state.endTextBlock();
|
|
16675
18120
|
}
|
|
18121
|
+
if (rewoundNote) {
|
|
18122
|
+
controller.enqueue({
|
|
18123
|
+
type: "text-delta",
|
|
18124
|
+
id: state.startTextBlock(),
|
|
18125
|
+
delta: rewoundNote
|
|
18126
|
+
});
|
|
18127
|
+
state.endTextBlock();
|
|
18128
|
+
}
|
|
16676
18129
|
if (accountGroupNote) {
|
|
16677
18130
|
controller.enqueue({
|
|
16678
18131
|
type: "text-delta",
|
|
@@ -16690,6 +18143,56 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16690
18143
|
state.endTextBlock();
|
|
16691
18144
|
}
|
|
16692
18145
|
state.autoContinueState.startedAt = Date.now();
|
|
18146
|
+
const writeUsageNote = () => {
|
|
18147
|
+
const crossings = usageCrossings(spawnedAccount, usageNoteThresholds);
|
|
18148
|
+
if (crossings.length === 0) return;
|
|
18149
|
+
const switching = usageSwitchAt !== null && crossings.some((crossing) => crossing.crossed.includes(usageSwitchAt));
|
|
18150
|
+
let movedTo;
|
|
18151
|
+
let candidates = [];
|
|
18152
|
+
if (switching && usageSwitchAt !== null) {
|
|
18153
|
+
if (accountRun) {
|
|
18154
|
+
const target = nextUsageSwitchAccount(
|
|
18155
|
+
failoverAccounts,
|
|
18156
|
+
/* @__PURE__ */ new Set([spawnedAccount, sourceAccount]),
|
|
18157
|
+
usageSwitchAt
|
|
18158
|
+
);
|
|
18159
|
+
if (target) {
|
|
18160
|
+
const until = Math.max(
|
|
18161
|
+
...crossings.filter((crossing) => crossing.crossed.includes(usageSwitchAt)).map((crossing) => crossing.entry.resetsAt ?? 0)
|
|
18162
|
+
);
|
|
18163
|
+
movedTo = { target, ...until > 0 ? { until } : {} };
|
|
18164
|
+
setAccountOverride(sourceAccount, target, movedTo.until, Date.now(), "usage-threshold");
|
|
18165
|
+
}
|
|
18166
|
+
} else {
|
|
18167
|
+
candidates = failoverAccounts.filter(
|
|
18168
|
+
(account) => account !== spawnedAccount && !accountUsageAtOrOver(account, usageSwitchAt)
|
|
18169
|
+
);
|
|
18170
|
+
}
|
|
18171
|
+
}
|
|
18172
|
+
const windows = crossings.map((crossing) => ({
|
|
18173
|
+
window: crossing.window,
|
|
18174
|
+
percent: usagePercent(crossing.entry.utilization ?? 0),
|
|
18175
|
+
resetsAt: crossing.entry.resetsAt,
|
|
18176
|
+
resetsText: crossing.entry.resetsText
|
|
18177
|
+
}));
|
|
18178
|
+
for (const crossing of crossings) {
|
|
18179
|
+
log.notice("claude account usage crossed a warning threshold", {
|
|
18180
|
+
sessionKey: sk,
|
|
18181
|
+
account: spawnedAccount,
|
|
18182
|
+
rateLimitType: crossing.window,
|
|
18183
|
+
utilization: usagePercent(crossing.entry.utilization ?? 0),
|
|
18184
|
+
crossed: crossing.crossed,
|
|
18185
|
+
switched: movedTo !== void 0
|
|
18186
|
+
});
|
|
18187
|
+
markUsageNoted(spawnedAccount, crossing.window, crossing.crossed);
|
|
18188
|
+
}
|
|
18189
|
+
controller.enqueue({
|
|
18190
|
+
type: "text-delta",
|
|
18191
|
+
id: state.startTextBlock(),
|
|
18192
|
+
delta: formatUsageNote({ account: spawnedAccount, windows, movedTo, candidates })
|
|
18193
|
+
});
|
|
18194
|
+
state.endTextBlock();
|
|
18195
|
+
};
|
|
16693
18196
|
const completeResult = (msg) => {
|
|
16694
18197
|
if (state.controllerClosed) return;
|
|
16695
18198
|
if (self.config.resumeAfterRestart !== false && !compactionMode && msg.is_error !== true && typeof msg.session_id === "string") {
|
|
@@ -16698,6 +18201,9 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16698
18201
|
account: failover.target ?? sourceAccount
|
|
16699
18202
|
});
|
|
16700
18203
|
}
|
|
18204
|
+
if (!compactionMode) {
|
|
18205
|
+
recordServedConversation(sk, filterSideQuestionHistory(options.prompt));
|
|
18206
|
+
}
|
|
16701
18207
|
if (deliverPendingCompletions(state)) {
|
|
16702
18208
|
if (state.drainBuffer.length > 0) drainNow(state);
|
|
16703
18209
|
return;
|
|
@@ -16735,6 +18241,36 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16735
18241
|
finishWithQuestionCall(state, call);
|
|
16736
18242
|
return;
|
|
16737
18243
|
}
|
|
18244
|
+
const autoTarget = (state.accountLimitHit || state.accountBlock) && msg.is_error === true ? autoFailoverTarget() : void 0;
|
|
18245
|
+
if (autoTarget && accountRun) {
|
|
18246
|
+
const target = autoTarget;
|
|
18247
|
+
accountRun.tried.add(target);
|
|
18248
|
+
accountRun.switchTo = {
|
|
18249
|
+
target,
|
|
18250
|
+
sourceAccount,
|
|
18251
|
+
failedAccount: limitAccount,
|
|
18252
|
+
cause: state.accountLimitHit ? "usage_limit" : state.accountBlock,
|
|
18253
|
+
resetsAt: state.accountLimitHit?.resetsAt,
|
|
18254
|
+
window: state.accountLimitHit?.window,
|
|
18255
|
+
failedConfigDir: self.skillBridgeSpawn(failover).configDir,
|
|
18256
|
+
reason: state.accountLimitHit || !state.accountBlock ? void 0 : describeAccountBlock(state.accountBlock)
|
|
18257
|
+
};
|
|
18258
|
+
log.warn("claude account cannot serve this turn; continuing it on the next account automatically", {
|
|
18259
|
+
sessionKey: sk,
|
|
18260
|
+
account: limitAccount,
|
|
18261
|
+
target,
|
|
18262
|
+
cause: state.accountLimitHit ? "usage_limit" : state.accountBlock,
|
|
18263
|
+
resetsAt: state.accountLimitHit?.resetsAt ?? null
|
|
18264
|
+
});
|
|
18265
|
+
state.endTextBlock();
|
|
18266
|
+
state.controllerClosed = true;
|
|
18267
|
+
cleanupTurn();
|
|
18268
|
+
try {
|
|
18269
|
+
controller.close();
|
|
18270
|
+
} catch {
|
|
18271
|
+
}
|
|
18272
|
+
return;
|
|
18273
|
+
}
|
|
16738
18274
|
if (modelFallbackArmed && attempt && msg.is_error === true) {
|
|
16739
18275
|
const refusal = state.modelRefusal ?? (state.accountLimitHit && !failoverAskActive && !state.accountBlock ? { kind: "account_limit" } : null);
|
|
16740
18276
|
if (refusal) {
|
|
@@ -16763,19 +18299,20 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16763
18299
|
}
|
|
16764
18300
|
}
|
|
16765
18301
|
if (state.usageLimitNote) {
|
|
18302
|
+
const noteCandidates = accountRun ? [] : failoverAccounts;
|
|
16766
18303
|
log.warn("claude account is out of usage; the turn ends with a note", {
|
|
16767
18304
|
sessionKey: sk,
|
|
16768
|
-
account:
|
|
18305
|
+
account: limitAccount,
|
|
16769
18306
|
window: state.accountLimitHit?.window ?? null,
|
|
16770
18307
|
resetsAt: state.accountLimitHit?.resetsAt ?? null,
|
|
16771
|
-
candidates:
|
|
18308
|
+
candidates: noteCandidates
|
|
16772
18309
|
});
|
|
16773
18310
|
controller.enqueue({
|
|
16774
18311
|
type: "text-delta",
|
|
16775
18312
|
id: state.startTextBlock(),
|
|
16776
18313
|
delta: formatUsageLimitNote({
|
|
16777
|
-
sourceAccount,
|
|
16778
|
-
candidates:
|
|
18314
|
+
sourceAccount: limitAccount,
|
|
18315
|
+
candidates: noteCandidates,
|
|
16779
18316
|
resetsAt: state.accountLimitHit?.resetsAt,
|
|
16780
18317
|
window: state.accountLimitHit?.window,
|
|
16781
18318
|
resetsText: state.accountLimitHit?.resetsText
|
|
@@ -16784,6 +18321,9 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16784
18321
|
state.endTextBlock();
|
|
16785
18322
|
state.resultFailure ??= "usage_limit";
|
|
16786
18323
|
}
|
|
18324
|
+
if (accountRun && state.accountLimitHit && msg.is_error === true) {
|
|
18325
|
+
state.resultFailure ??= "usage_limit";
|
|
18326
|
+
}
|
|
16787
18327
|
if (runAutoContinue(state, msg)) return;
|
|
16788
18328
|
for (const [idx, reasoningId] of state.reasoningIds) {
|
|
16789
18329
|
if (state.reasoningStarted.get(idx)) {
|
|
@@ -16816,6 +18356,9 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16816
18356
|
});
|
|
16817
18357
|
state.endTextBlock();
|
|
16818
18358
|
}
|
|
18359
|
+
if (usageNotesPossible && msg.is_error !== true && !state.resultFailure && usageChildSession() === false) {
|
|
18360
|
+
writeUsageNote();
|
|
18361
|
+
}
|
|
16819
18362
|
if (compactionMode && state.resultFailure) {
|
|
16820
18363
|
log.warn("claude could not compact the conversation; no summary was stored", {
|
|
16821
18364
|
sessionKey: sk,
|
|
@@ -16868,10 +18411,16 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16868
18411
|
fastMode,
|
|
16869
18412
|
planModeQuestionActive,
|
|
16870
18413
|
interactive: useInteractive,
|
|
16871
|
-
sourceAccount,
|
|
18414
|
+
sourceAccount: limitAccount,
|
|
18415
|
+
usageAccount: spawnedAccount,
|
|
18416
|
+
accountConfigDir: accountRun ? self.skillBridgeSpawn(failover).configDir : self.config.configDir,
|
|
16872
18417
|
failoverAskActive,
|
|
18418
|
+
autoFailoverTarget,
|
|
16873
18419
|
usageLimitNoteActive,
|
|
16874
|
-
|
|
18420
|
+
// Read only when no switch takes the turn, which under `"auto"`
|
|
18421
|
+
// means no account is left to offer.
|
|
18422
|
+
failoverAccounts: accountRun ? [] : failoverAccounts,
|
|
18423
|
+
budget,
|
|
16875
18424
|
modelFallbackArmed,
|
|
16876
18425
|
attempt,
|
|
16877
18426
|
handleControlRequest: handleControlRequest2,
|
|
@@ -17122,7 +18671,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
17122
18671
|
return fresh.length;
|
|
17123
18672
|
};
|
|
17124
18673
|
if (!state.unattendedTurnEnded && await forwardTrailingUserMessages() > 0) {
|
|
17125
|
-
await new Promise((
|
|
18674
|
+
await new Promise((resolve7) => setTimeout(resolve7, FORWARD_SETTLE_MS));
|
|
17126
18675
|
if (state.controllerClosed) return;
|
|
17127
18676
|
}
|
|
17128
18677
|
for (const { call, result: opencodeResult } of previousPendingProxyMatches) {
|
|
@@ -17226,8 +18775,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
17226
18775
|
};
|
|
17227
18776
|
|
|
17228
18777
|
// src/account-tier.ts
|
|
17229
|
-
import { readFileSync as
|
|
17230
|
-
import
|
|
18778
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
18779
|
+
import path14 from "path";
|
|
17231
18780
|
var ORGANIZATION_TYPE_LABELS = {
|
|
17232
18781
|
claude_max: "Max",
|
|
17233
18782
|
claude_pro: "Pro",
|
|
@@ -17249,14 +18798,14 @@ function accountTierConfigDir(account) {
|
|
|
17249
18798
|
function readAccountTier(account) {
|
|
17250
18799
|
const normalized = normalizeAccountName(account) || DEFAULT_ACCOUNT;
|
|
17251
18800
|
const configDir = accountTierConfigDir(normalized);
|
|
17252
|
-
const
|
|
17253
|
-
if (
|
|
18801
|
+
const cached2 = tierCache.get(configDir);
|
|
18802
|
+
if (cached2 !== void 0) return cached2 ?? void 0;
|
|
17254
18803
|
const label = readTierFromConfigDir(configDir, normalized === DEFAULT_ACCOUNT);
|
|
17255
18804
|
tierCache.set(configDir, label ?? null);
|
|
17256
18805
|
return label;
|
|
17257
18806
|
}
|
|
17258
18807
|
function readTierFromConfigDir(configDir, allowLegacyFallback) {
|
|
17259
|
-
const candidates = [
|
|
18808
|
+
const candidates = [path14.join(configDir, ".claude.json")];
|
|
17260
18809
|
if (allowLegacyFallback) candidates.push(expandHome("~/.claude.json"));
|
|
17261
18810
|
for (const candidate of candidates) {
|
|
17262
18811
|
const oauth = readOauthAccount(candidate);
|
|
@@ -17269,7 +18818,7 @@ function readTierFromConfigDir(configDir, allowLegacyFallback) {
|
|
|
17269
18818
|
function readOauthAccount(file) {
|
|
17270
18819
|
let raw;
|
|
17271
18820
|
try {
|
|
17272
|
-
raw =
|
|
18821
|
+
raw = readFileSync11(file, "utf8");
|
|
17273
18822
|
} catch {
|
|
17274
18823
|
return void 0;
|
|
17275
18824
|
}
|
|
@@ -17297,14 +18846,14 @@ function tierLabel(oauthAccount) {
|
|
|
17297
18846
|
// src/cleanup-stale.ts
|
|
17298
18847
|
import {
|
|
17299
18848
|
existsSync as existsSync5,
|
|
17300
|
-
mkdirSync as
|
|
17301
|
-
readFileSync as
|
|
18849
|
+
mkdirSync as mkdirSync8,
|
|
18850
|
+
readFileSync as readFileSync12,
|
|
17302
18851
|
realpathSync as realpathSync4,
|
|
17303
18852
|
rmSync as rmSync3,
|
|
17304
|
-
writeFileSync as
|
|
18853
|
+
writeFileSync as writeFileSync10
|
|
17305
18854
|
} from "fs";
|
|
17306
|
-
import { homedir as
|
|
17307
|
-
import { dirname as dirname7, join as
|
|
18855
|
+
import { homedir as homedir14 } from "os";
|
|
18856
|
+
import { dirname as dirname7, join as join18, resolve as resolve6 } from "path";
|
|
17308
18857
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
17309
18858
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
17310
18859
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -17312,20 +18861,20 @@ var alreadyRan = false;
|
|
|
17312
18861
|
function candidateCacheRoots() {
|
|
17313
18862
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
17314
18863
|
return [
|
|
17315
|
-
xdg ?
|
|
17316
|
-
|
|
17317
|
-
|
|
18864
|
+
xdg ? join18(xdg, "opencode") : null,
|
|
18865
|
+
join18(homedir14(), ".cache", "opencode"),
|
|
18866
|
+
join18(homedir14(), "Library", "Caches", "opencode")
|
|
17318
18867
|
].filter((p) => Boolean(p));
|
|
17319
18868
|
}
|
|
17320
18869
|
function userOpencodeJsonPath() {
|
|
17321
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
17322
|
-
return
|
|
18870
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join18(homedir14(), ".config");
|
|
18871
|
+
return join18(xdgConfig, "opencode", "opencode.json");
|
|
17323
18872
|
}
|
|
17324
18873
|
function userIntendsToUseUnscoped() {
|
|
17325
18874
|
const cfg = userOpencodeJsonPath();
|
|
17326
18875
|
if (!existsSync5(cfg)) return false;
|
|
17327
18876
|
try {
|
|
17328
|
-
const json = JSON.parse(
|
|
18877
|
+
const json = JSON.parse(readFileSync12(cfg, "utf8"));
|
|
17329
18878
|
const plugins = json.plugin;
|
|
17330
18879
|
if (!Array.isArray(plugins)) return false;
|
|
17331
18880
|
return plugins.some(
|
|
@@ -17338,7 +18887,7 @@ function userIntendsToUseUnscoped() {
|
|
|
17338
18887
|
function ourLoadedDir() {
|
|
17339
18888
|
try {
|
|
17340
18889
|
const filePath = fileURLToPath4(import.meta.url);
|
|
17341
|
-
return realpathSync4(
|
|
18890
|
+
return realpathSync4(resolve6(filePath, "..", ".."));
|
|
17342
18891
|
} catch {
|
|
17343
18892
|
return null;
|
|
17344
18893
|
}
|
|
@@ -17346,19 +18895,19 @@ function ourLoadedDir() {
|
|
|
17346
18895
|
function ourVersion(dir) {
|
|
17347
18896
|
if (!dir) return null;
|
|
17348
18897
|
try {
|
|
17349
|
-
const pkg = JSON.parse(
|
|
18898
|
+
const pkg = JSON.parse(readFileSync12(join18(dir, "package.json"), "utf8"));
|
|
17350
18899
|
return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
|
|
17351
18900
|
} catch {
|
|
17352
18901
|
return null;
|
|
17353
18902
|
}
|
|
17354
18903
|
}
|
|
17355
18904
|
function markerPath() {
|
|
17356
|
-
const stateRoot = process.env.XDG_STATE_HOME ??
|
|
17357
|
-
return
|
|
18905
|
+
const stateRoot = process.env.XDG_STATE_HOME ?? join18(homedir14(), ".local", "state");
|
|
18906
|
+
return join18(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
|
|
17358
18907
|
}
|
|
17359
18908
|
function markerVersion() {
|
|
17360
18909
|
try {
|
|
17361
|
-
const marker = JSON.parse(
|
|
18910
|
+
const marker = JSON.parse(readFileSync12(markerPath(), "utf8"));
|
|
17362
18911
|
return typeof marker.version === "string" ? marker.version : null;
|
|
17363
18912
|
} catch {
|
|
17364
18913
|
return null;
|
|
@@ -17367,8 +18916,8 @@ function markerVersion() {
|
|
|
17367
18916
|
function writeMarker(version) {
|
|
17368
18917
|
const file = markerPath();
|
|
17369
18918
|
try {
|
|
17370
|
-
|
|
17371
|
-
|
|
18919
|
+
mkdirSync8(dirname7(file), { recursive: true, mode: 448 });
|
|
18920
|
+
writeFileSync10(
|
|
17372
18921
|
file,
|
|
17373
18922
|
JSON.stringify({ version, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
|
|
17374
18923
|
"utf8"
|
|
@@ -17403,7 +18952,7 @@ function cleanupStaleUnscopedInstall() {
|
|
|
17403
18952
|
}
|
|
17404
18953
|
function cleanupOne(cacheRoot, ourDir) {
|
|
17405
18954
|
if (!existsSync5(cacheRoot)) return;
|
|
17406
|
-
const stalePath =
|
|
18955
|
+
const stalePath = join18(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
17407
18956
|
if (!existsSync5(stalePath)) return;
|
|
17408
18957
|
let realStalePath = stalePath;
|
|
17409
18958
|
try {
|
|
@@ -17411,11 +18960,11 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
17411
18960
|
} catch {
|
|
17412
18961
|
}
|
|
17413
18962
|
if (ourDir && realStalePath === ourDir) return;
|
|
17414
|
-
const pkgJsonPath =
|
|
18963
|
+
const pkgJsonPath = join18(stalePath, "package.json");
|
|
17415
18964
|
if (!existsSync5(pkgJsonPath)) return;
|
|
17416
18965
|
let pkg = {};
|
|
17417
18966
|
try {
|
|
17418
|
-
pkg = JSON.parse(
|
|
18967
|
+
pkg = JSON.parse(readFileSync12(pkgJsonPath, "utf8"));
|
|
17419
18968
|
} catch {
|
|
17420
18969
|
return;
|
|
17421
18970
|
}
|
|
@@ -17431,13 +18980,13 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
17431
18980
|
});
|
|
17432
18981
|
return;
|
|
17433
18982
|
}
|
|
17434
|
-
const cachePkgJson =
|
|
18983
|
+
const cachePkgJson = join18(cacheRoot, "package.json");
|
|
17435
18984
|
if (!existsSync5(cachePkgJson)) return;
|
|
17436
18985
|
try {
|
|
17437
|
-
const cfg = JSON.parse(
|
|
18986
|
+
const cfg = JSON.parse(readFileSync12(cachePkgJson, "utf8"));
|
|
17438
18987
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
17439
18988
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
17440
|
-
|
|
18989
|
+
writeFileSync10(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
17441
18990
|
log.info("cleanup-stale: pruned dep from cache package.json");
|
|
17442
18991
|
}
|
|
17443
18992
|
} catch (err) {
|
|
@@ -17514,7 +19063,6 @@ function registerOpencodeLogSink(client) {
|
|
|
17514
19063
|
}
|
|
17515
19064
|
|
|
17516
19065
|
// src/v2-client.ts
|
|
17517
|
-
import path14 from "path";
|
|
17518
19066
|
function formatAgentTypeList(agents) {
|
|
17519
19067
|
const lines = agents.flatMap((agent) => {
|
|
17520
19068
|
if (agent.hidden === true || agent.mode === "primary") return [];
|
|
@@ -17542,8 +19090,7 @@ function describe(value) {
|
|
|
17542
19090
|
function toV1Session(info) {
|
|
17543
19091
|
if (!info || typeof info !== "object") return void 0;
|
|
17544
19092
|
const session = info;
|
|
17545
|
-
const
|
|
17546
|
-
const directory = typeof root === "string" ? typeof session.subpath === "string" && session.subpath.length > 0 ? path14.resolve(root, session.subpath) : root : void 0;
|
|
19093
|
+
const directory = typeof session.location?.directory === "string" ? session.location.directory : void 0;
|
|
17547
19094
|
return { ...info, directory, parentID: session.parentID };
|
|
17548
19095
|
}
|
|
17549
19096
|
function toV1Messages(messages) {
|
|
@@ -18062,6 +19609,12 @@ function createClaudeCode(settings = {}) {
|
|
|
18062
19609
|
const cliPath = settings.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude";
|
|
18063
19610
|
const providerName = settings.providerID ?? settings.name ?? "claude-code";
|
|
18064
19611
|
const proxyTools = preset?.proxyTools ?? settings.proxyTools ?? [...DEFAULT_PROXY_TOOL_NAMES];
|
|
19612
|
+
const maxBudgetUsd = resolveBudgetUsd(settings.maxBudgetUsd, "maxBudgetUsd");
|
|
19613
|
+
const budgetWarnAt = maxBudgetUsd === void 0 ? void 0 : resolveBudgetWarnAt(settings.budgetWarnAt);
|
|
19614
|
+
const maxBackgroundSubagents = resolvePositiveInteger(
|
|
19615
|
+
settings.maxBackgroundSubagents,
|
|
19616
|
+
"maxBackgroundSubagents"
|
|
19617
|
+
);
|
|
18065
19618
|
const createModel = (modelId) => {
|
|
18066
19619
|
return new ClaudeCodeLanguageModel(modelId, {
|
|
18067
19620
|
provider: providerName,
|
|
@@ -18104,6 +19657,7 @@ function createClaudeCode(settings = {}) {
|
|
|
18104
19657
|
proxyOpencodeTools: settings.proxyOpencodeTools,
|
|
18105
19658
|
stripContextReminders: settings.stripContextReminders === true,
|
|
18106
19659
|
extraDisallowedTools: preset?.extraDisallowedTools ?? settings.extraDisallowedTools,
|
|
19660
|
+
mapReadRules: settings.mapReadRules,
|
|
18107
19661
|
proxyToolTimeoutMs: settings.proxyToolTimeoutMs,
|
|
18108
19662
|
planModeQuestion: settings.planModeQuestion ?? false,
|
|
18109
19663
|
webSearch: settings.webSearch,
|
|
@@ -18122,6 +19676,13 @@ function createClaudeCode(settings = {}) {
|
|
|
18122
19676
|
resumeAfterRestart: settings.resumeAfterRestart !== false,
|
|
18123
19677
|
resumeAcrossModelChanges: settings.resumeAcrossModelChanges !== false,
|
|
18124
19678
|
crossAccountResume: settings.crossAccountResume !== false,
|
|
19679
|
+
// Resolved once per model, not per turn, for the reason `accountGroups`
|
|
19680
|
+
// is: an invalid value WARNs at creation and a turn reads a checked one.
|
|
19681
|
+
maxBudgetUsd,
|
|
19682
|
+
budgetWarnAt,
|
|
19683
|
+
maxBackgroundSubagents,
|
|
19684
|
+
usageWarnThresholds: resolveUsageWarnThresholds(settings.usageWarnThresholds),
|
|
19685
|
+
usageSwitchAt: resolveUsageSwitchAt(settings.usageSwitchAt),
|
|
18125
19686
|
transport: settings.transport,
|
|
18126
19687
|
interactive: settings.interactive,
|
|
18127
19688
|
interactiveBypass: settings.interactiveBypass,
|
|
@@ -18325,6 +19886,13 @@ async function buildAgentRegistry(config) {
|
|
|
18325
19886
|
typeof configuredTtl === "string" ? configuredTtl : void 0
|
|
18326
19887
|
);
|
|
18327
19888
|
setProviderFallbackModels(parseFallbackModelList(options?.fallbackModels));
|
|
19889
|
+
setDefaultSubagentCliCaps({
|
|
19890
|
+
maxTurns: resolvePositiveInteger(options?.defaultSubagentMaxTurns, "defaultSubagentMaxTurns"),
|
|
19891
|
+
maxBudgetUsd: resolveBudgetUsd(
|
|
19892
|
+
options?.defaultSubagentMaxBudgetUsd,
|
|
19893
|
+
"defaultSubagentMaxBudgetUsd"
|
|
19894
|
+
)
|
|
19895
|
+
});
|
|
18328
19896
|
const records2 = await readAgentMarkdownRecords(
|
|
18329
19897
|
agentDirectories(
|
|
18330
19898
|
process.env.HOME ?? process.env.USERPROFILE,
|
|
@@ -18337,6 +19905,10 @@ async function buildAgentRegistry(config) {
|
|
|
18337
19905
|
const value = agent[key] ?? bag[key];
|
|
18338
19906
|
return typeof value === "string" ? value : void 0;
|
|
18339
19907
|
};
|
|
19908
|
+
const pickNumber = (key) => {
|
|
19909
|
+
const value = agent[key] ?? bag[key];
|
|
19910
|
+
return typeof value === "number" || typeof value === "string" ? value : void 0;
|
|
19911
|
+
};
|
|
18340
19912
|
const declaredChain = parseFallbackModelList(
|
|
18341
19913
|
agent.fallbackModels ?? bag.fallbackModels
|
|
18342
19914
|
);
|
|
@@ -18346,6 +19918,8 @@ async function buildAgentRegistry(config) {
|
|
|
18346
19918
|
forceModel: pick("forceModel") ?? records2[name]?.forceModel,
|
|
18347
19919
|
reasoningEffort: pick("reasoningEffort") ?? records2[name]?.reasoningEffort,
|
|
18348
19920
|
cacheTtl: pick("cacheTtl") ?? records2[name]?.cacheTtl,
|
|
19921
|
+
maxTurns: pickNumber("maxTurns") ?? records2[name]?.maxTurns,
|
|
19922
|
+
maxBudgetUsd: pickNumber("maxBudgetUsd") ?? records2[name]?.maxBudgetUsd,
|
|
18349
19923
|
fallbackModels: declaredChain.length ? declaredChain : records2[name]?.fallbackModels,
|
|
18350
19924
|
prompt: pick("prompt") ?? records2[name]?.prompt
|
|
18351
19925
|
};
|