@wrongstack/core 0.306.3 → 0.306.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3884,6 +3884,10 @@ var IN_PROJECT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
3884
3884
  "fallbackModels",
3885
3885
  "fallbackBridge",
3886
3886
  "fallbackProfiles",
3887
+ // The profile SELECTOR. No broader than its siblings: a repo that can write
3888
+ // `fallbackModels` and `fallbackProfiles` already controls the chain outright,
3889
+ // and this one can only name a profile the user already defined.
3890
+ "fallbackProfile",
3887
3891
  "favoriteModels",
3888
3892
  "favoriteModelsOnly",
3889
3893
  "modelAvailabilitySchedule",
@@ -3931,6 +3935,10 @@ var KNOWN_DENIED_IN_PROJECT = [
3931
3935
  {
3932
3936
  key: "git",
3933
3937
  reason: "Carries git.identity (GIT_AUTHOR_*/GIT_COMMITTER_* injection): a repo-committed config could spoof the author identity written into the victim's commit history (impersonation)."
3938
+ },
3939
+ {
3940
+ key: "fallbackMaxLastResortCandidates",
3941
+ reason: "Bounds how many of the user's OTHER configured providers may be swept in as last-resort failover. Setting it to 0 from a repo-committed config would silently strip that depth during an outage. It was already stripped in practice (absent from the allow-list) but was missing from the key registry, so this gate never checked it."
3934
3942
  }
3935
3943
  ];
3936
3944
  var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
@@ -3951,11 +3959,13 @@ var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
3951
3959
  "fallbackModels",
3952
3960
  "fallbackBridge",
3953
3961
  "fallbackProfiles",
3962
+ "fallbackProfile",
3954
3963
  "favoriteModels",
3955
3964
  "favoriteModelsOnly",
3956
3965
  "modelAvailabilitySchedule",
3957
3966
  "fallbackAuto",
3958
3967
  "fallbackStickiness",
3968
+ "fallbackMaxLastResortCandidates",
3959
3969
  "hooks",
3960
3970
  "plugins",
3961
3971
  "pluginManager",
@@ -4041,6 +4051,17 @@ var IN_PROJECT_DENIED_PATHS = [
4041
4051
  // operator owns, not the checked-out repository.
4042
4052
  path: "tools.kanbanGovernance",
4043
4053
  reason: "Repo-committed config could disable a Kanban governance gate the operator switched on, letting product mutations run outside any managed card."
4054
+ },
4055
+ {
4056
+ // The bridge spawn path resolves the CLI entry by walking UP from the
4057
+ // project root, so a repo that ships its own `packages/cli/dist/index.js`
4058
+ // gets that file spawned with `process.execPath` on WebUI boot — no
4059
+ // prompt, no banner. Turning the feature on is therefore equivalent to
4060
+ // arbitrary code execution for a hostile checkout, which makes this an
4061
+ // operator-owned switch and never a repo-owned one.
4062
+ // See discover-mailbox-bridge.ts:findWorkspaceCliEntry.
4063
+ path: "features.mailboxBridge",
4064
+ reason: "Enables the mailbox bridge, whose CLI-entry resolution walks up from the project root \u2014 a repo-supplied packages/cli/dist/index.js would be spawned on WebUI boot."
4044
4065
  }
4045
4066
  ];
4046
4067
  function deleteNestedPath(target, path124) {
@@ -8622,6 +8643,7 @@ function hasOpenTodos(todos) {
8622
8643
  }
8623
8644
 
8624
8645
  // src/core/context.ts
8646
+ import { realpathSync as realpathSync2 } from "node:fs";
8625
8647
  import * as path21 from "node:path";
8626
8648
 
8627
8649
  // src/utils/tool-wire-compact.ts
@@ -9073,12 +9095,15 @@ var ConversationState = class {
9073
9095
  * cap determines the starting index for the sum but does not gate it.
9074
9096
  */
9075
9097
  overflowCount(arr) {
9076
- let drop = Context.MAX_MESSAGES > 0 ? Math.max(0, arr.length - Context.MAX_MESSAGES) : 0;
9077
- if (Context.MAX_MESSAGE_TOKENS <= 0) return this.protocolSafeDropCount(arr, drop);
9098
+ const contextClass = this.ctx.constructor;
9099
+ const maxMessages = contextClass.MAX_MESSAGES;
9100
+ const maxMessageTokens = contextClass.MAX_MESSAGE_TOKENS;
9101
+ let drop = maxMessages > 0 ? Math.max(0, arr.length - maxMessages) : 0;
9102
+ if (maxMessageTokens <= 0) return this.protocolSafeDropCount(arr, drop);
9078
9103
  let total = 0;
9079
9104
  for (let i = drop; i < arr.length; i++) total += arr[i]?._estTokens ?? 0;
9080
- if (total <= Context.MAX_MESSAGE_TOKENS) return this.protocolSafeDropCount(arr, drop);
9081
- while (drop < arr.length - 1 && total > Context.MAX_MESSAGE_TOKENS) {
9105
+ if (total <= maxMessageTokens) return this.protocolSafeDropCount(arr, drop);
9106
+ while (drop < arr.length - 1 && total > maxMessageTokens) {
9082
9107
  total -= arr[drop]?._estTokens ?? 0;
9083
9108
  drop++;
9084
9109
  }
@@ -9904,6 +9929,19 @@ var Context = class _Context {
9904
9929
  if (rel.startsWith("..") || path21.isAbsolute(rel)) {
9905
9930
  throw new Error(`Working directory "${resolved}" is outside project root "${root}"`);
9906
9931
  }
9932
+ let realTarget = resolved;
9933
+ let realRoot = root;
9934
+ try {
9935
+ realTarget = realpathSync2.native(resolved);
9936
+ realRoot = realpathSync2.native(root);
9937
+ } catch {
9938
+ }
9939
+ const realRel = path21.relative(realRoot, realTarget);
9940
+ if (realRel.startsWith("..") || path21.isAbsolute(realRel)) {
9941
+ throw new Error(
9942
+ `Working directory "${resolved}" resolves to "${realTarget}", outside project root "${realRoot}"`
9943
+ );
9944
+ }
9907
9945
  }
9908
9946
  const old = this.workingDir;
9909
9947
  this.workingDir = resolved;
@@ -10655,26 +10693,41 @@ function stripNextStepsFromMessage(msg) {
10655
10693
  strippedNextStepsCache.set(msg, clone);
10656
10694
  return clone;
10657
10695
  }
10658
- function composeRequestMessages(history, tail) {
10696
+ function markCacheBoundary(msg) {
10697
+ if (typeof msg.content === "string") return void 0;
10698
+ const blocks = msg.content.slice();
10699
+ const boundary = blocks[blocks.length - 1];
10700
+ if (!boundary || boundary.type !== "text" && boundary.type !== "tool_result") return void 0;
10701
+ blocks[blocks.length - 1] = { ...boundary, cache_control: { type: "ephemeral" } };
10702
+ return { ...msg, content: blocks };
10703
+ }
10704
+ function composeRequestMessages(history, tail, previous) {
10659
10705
  if (history.length === 0) return null;
10660
10706
  const out = history.slice();
10661
10707
  const lastIdx = out.length - 1;
10662
10708
  const last = out[lastIdx];
10709
+ if (previous && previous.index < lastIdx && history[previous.index] === previous.message) {
10710
+ const marked2 = markCacheBoundary(previous.message);
10711
+ if (marked2) out[previous.index] = marked2;
10712
+ }
10663
10713
  const blocks = typeof last.content === "string" ? [{ type: "text", text: last.content }] : last.content.slice();
10664
- const boundary = blocks[blocks.length - 1];
10665
- if (boundary && (boundary.type === "text" || boundary.type === "tool_result")) {
10666
- blocks[blocks.length - 1] = { ...boundary, cache_control: { type: "ephemeral" } };
10714
+ const tailBlock = blocks[blocks.length - 1];
10715
+ const marked = tailBlock && (tailBlock.type === "text" || tailBlock.type === "tool_result");
10716
+ if (marked) {
10717
+ blocks[blocks.length - 1] = { ...tailBlock, cache_control: { type: "ephemeral" } };
10667
10718
  }
10719
+ const boundary = marked ? { message: last, index: lastIdx } : void 0;
10668
10720
  if (tail.length === 0 || last.role !== "user") {
10669
10721
  out[lastIdx] = { ...last, content: blocks };
10670
10722
  if (tail.length > 0) out.push({ role: "user", content: [LIVE_CONTEXT_HEADER, ...tail] });
10671
- return out;
10723
+ return { messages: out, boundary };
10672
10724
  }
10673
10725
  out[lastIdx] = { ...last, content: [...blocks, LIVE_CONTEXT_HEADER, ...tail] };
10674
- return out;
10726
+ return { messages: out, boundary };
10675
10727
  }
10676
10728
  function createAgentResponseHandler(a) {
10677
10729
  const stabilizedPromptEpochs = /* @__PURE__ */ new WeakSet();
10730
+ let previousBoundary;
10678
10731
  function stabilizePromptEpoch() {
10679
10732
  const prompt = a.ctx.systemPrompt;
10680
10733
  if (stabilizedPromptEpochs.has(prompt)) return;
@@ -10715,7 +10768,9 @@ function createAgentResponseHandler(a) {
10715
10768
  ...memoryEvidence
10716
10769
  ].filter((block) => block !== void 0);
10717
10770
  const requestHistory = stripDeliveredNextSteps(a.ctx.messages);
10718
- const composedMessages = composeRequestMessages(requestHistory, liveContextTail);
10771
+ const composed = composeRequestMessages(requestHistory, liveContextTail, previousBoundary);
10772
+ if (composed) previousBoundary = composed.boundary;
10773
+ const composedMessages = composed?.messages ?? null;
10719
10774
  const system = composedMessages ? stableSystem : liveContextTail.length > 0 ? [...stableSystem, ...liveContextTail] : stableSystem;
10720
10775
  await a.ctx.waitForModelTransition();
10721
10776
  const provider = a.ctx.provider;
@@ -27608,6 +27663,24 @@ import { createInterface as createInterface4 } from "node:readline";
27608
27663
  import { homedir as homedir5 } from "node:os";
27609
27664
 
27610
27665
  // src/security/secret-scrubber.ts
27666
+ var JSON_CREDENTIAL_KEY_ANCHORS = [
27667
+ 'Key"',
27668
+ 'key"',
27669
+ 'KEY"',
27670
+ 'token"',
27671
+ 'Token"',
27672
+ 'TOKEN"',
27673
+ 'secret"',
27674
+ 'Secret"',
27675
+ 'SECRET"',
27676
+ 'password"',
27677
+ 'Password"',
27678
+ 'PASSWORD"',
27679
+ 'authorization"',
27680
+ 'Authorization"',
27681
+ 'bearer"',
27682
+ 'Bearer"'
27683
+ ];
27611
27684
  var PATTERNS = [
27612
27685
  // Anchored at the start where possible so partial matches inside larger
27613
27686
  // strings don't trigger false positives.
@@ -27710,6 +27783,30 @@ var PATTERNS = [
27710
27783
  regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
27711
27784
  anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
27712
27785
  },
27786
+ {
27787
+ type: "json_credential_key",
27788
+ // The JSON counterpart to `high_entropy_env`, and the pattern that the
27789
+ // now-deleted `JSON_KEY_ANCHORS` list was written for. Without it those
27790
+ // anchors only widened the cheap pre-scan — `hasCredentialAnchors` said
27791
+ // "this text may hold a secret", every pattern then declined to match, and
27792
+ // the value went out verbatim. `high_entropy_env` cannot cover these: it
27793
+ // requires an UPPERCASE unquoted key (`API_KEY=…`), so `{"apiKey":"…"}`
27794
+ // never matched.
27795
+ //
27796
+ // Tool results are routinely serialised as JSON, and a credential with no
27797
+ // recognisable prefix (Azure, self-hosted gateways, Anthropic/Codex OAuth)
27798
+ // has no other pattern that can catch it — this is the only thing standing
27799
+ // between such a value and the session JSONL, chronicle, HQ broadcast and
27800
+ // the model's own context.
27801
+ //
27802
+ // The key may carry a prefix (`"anthropicApiKey"`), but the credential word
27803
+ // must END the key: `"tokenCount"` and `"maxTokens"` do not match, because
27804
+ // the closing quote has to follow the word immediately.
27805
+ // Value floor of 8 chars keeps enum-ish values (`"authorization":"none"`)
27806
+ // out. Capture groups: 1=key + punctuation, 2=value, 3=closing quote.
27807
+ regex: /("[A-Za-z0-9_]*(?:apiKey|api_key|token|secret|password|authorization|bearer|private_key|access_token|refresh_token|client_secret)"\s*:\s*")([^"\\]{8,512})(")/gi,
27808
+ anchor: JSON_CREDENTIAL_KEY_ANCHORS
27809
+ },
27713
27810
  // ── Ported from packages/plugins credential-patterns.ts (WS-034) ─────────
27714
27811
  // The plugin runtime carried 37 patterns while this scrubber — the one that
27715
27812
  // guards session JSONL, chronicle, HQ broadcast, WebUI events and the auth
@@ -27792,9 +27889,12 @@ var PATTERNS = [
27792
27889
  anchor: "GOCSPX-"
27793
27890
  }
27794
27891
  ];
27795
- var SIMPLE_PATTERNS = PATTERNS.filter((p) => p.type !== "high_entropy_env");
27892
+ var SIMPLE_PATTERNS = PATTERNS.filter(
27893
+ (p) => p.type !== "high_entropy_env" && p.type !== "json_credential_key"
27894
+ );
27796
27895
  var COMBINED_REGEX = new RegExp(SIMPLE_PATTERNS.map((p) => `(${p.regex.source})`).join("|"), "g");
27797
27896
  var HIGH_ENTROPY_REGEX = PATTERNS.find((p) => p.type === "high_entropy_env").regex;
27897
+ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key").regex;
27798
27898
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
27799
27899
  var SCRUB_CHUNK_BYTES = 64 * 1024;
27800
27900
  var SCRUB_OVERLAP_BYTES = 1024;
@@ -27805,20 +27905,7 @@ var PATTERN_ANCHORS = [
27805
27905
  )
27806
27906
  )
27807
27907
  ];
27808
- var JSON_KEY_ANCHORS = [
27809
- '"apiKey"',
27810
- '"api_key"',
27811
- '"token"',
27812
- '"secret"',
27813
- '"password"',
27814
- '"authorization"',
27815
- '"bearer"',
27816
- '"private_key"',
27817
- '"access_token"',
27818
- '"refresh_token"',
27819
- '"client_secret"'
27820
- ];
27821
- var ALL_ANCHORS = [...PATTERN_ANCHORS, ...JSON_KEY_ANCHORS];
27908
+ var ALL_ANCHORS = PATTERN_ANCHORS;
27822
27909
  function hasCredentialAnchors(text2) {
27823
27910
  for (const anchor of ALL_ANCHORS) {
27824
27911
  if (text2.includes(anchor)) return true;
@@ -27867,6 +27954,9 @@ var DefaultSecretScrubber = class {
27867
27954
  out = out.replace(HIGH_ENTROPY_REGEX, (_match, lead, key, _value) => {
27868
27955
  return `${lead}${key}=[REDACTED:high_entropy_env]`;
27869
27956
  });
27957
+ out = out.replace(JSON_CREDENTIAL_REGEX, (_match, keyPrefix, _value, closingQuote) => {
27958
+ return `${keyPrefix}[REDACTED:json_credential_key]${closingQuote}`;
27959
+ });
27870
27960
  return out;
27871
27961
  }
27872
27962
  /**
@@ -35035,9 +35125,40 @@ async function readOrBuildShardManifestEntry(opts) {
35035
35125
  return entry;
35036
35126
  }
35037
35127
 
35038
- // src/storage/session-store/summary-builder.ts
35128
+ // src/storage/session-store/strict-empty-check.ts
35039
35129
  import { createReadStream as createReadStream7 } from "node:fs";
35040
35130
  import { createInterface as createInterface7 } from "node:readline";
35131
+ var EMPTY_SESSION_EVENT_TYPES = /* @__PURE__ */ new Set(["session_start", "session_resumed", "session_end"]);
35132
+ async function isStrictlyEmptySessionFile(file) {
35133
+ const input = createReadStream7(file, { encoding: "utf8" });
35134
+ const lines = createInterface7({ input, crlfDelay: Infinity });
35135
+ let sawSessionStart = false;
35136
+ try {
35137
+ for await (const line of lines) {
35138
+ if (!line.trim()) continue;
35139
+ let event;
35140
+ try {
35141
+ event = JSON.parse(line);
35142
+ } catch {
35143
+ return false;
35144
+ }
35145
+ if (event === null || typeof event !== "object" || Array.isArray(event)) return false;
35146
+ const type = event.type;
35147
+ if (typeof type !== "string" || !EMPTY_SESSION_EVENT_TYPES.has(type)) return false;
35148
+ if (type === "session_start") sawSessionStart = true;
35149
+ }
35150
+ } catch {
35151
+ return false;
35152
+ } finally {
35153
+ lines.close();
35154
+ input.destroy();
35155
+ }
35156
+ return sawSessionStart;
35157
+ }
35158
+
35159
+ // src/storage/session-store/summary-builder.ts
35160
+ import { createReadStream as createReadStream8 } from "node:fs";
35161
+ import { createInterface as createInterface8 } from "node:readline";
35041
35162
  async function summarizeSessionFile(opts) {
35042
35163
  return summarizeSessionEventSequence({
35043
35164
  id: opts.id,
@@ -35154,8 +35275,8 @@ async function summarizeSessionEventSequence(opts) {
35154
35275
  }
35155
35276
  }
35156
35277
  async function* iterateSessionEvents(file, secretScrubber) {
35157
- const stream = createReadStream7(file, { encoding: "utf8" });
35158
- const lines = createInterface7({ input: stream, crlfDelay: Infinity });
35278
+ const stream = createReadStream8(file, { encoding: "utf8" });
35279
+ const lines = createInterface8({ input: stream, crlfDelay: Infinity });
35159
35280
  try {
35160
35281
  for await (const line of lines) {
35161
35282
  if (!line.trim()) continue;
@@ -36051,6 +36172,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
36051
36172
  await deleteSessionArtifacts({ rootDir: this.dir, id, jsonlPath });
36052
36173
  await this.writeTombstone(id);
36053
36174
  }
36175
+ async isEmpty(id) {
36176
+ const canonicalId = await this.resolveId(id);
36177
+ return isStrictlyEmptySessionFile(this.sessionPath(canonicalId, ".jsonl"));
36178
+ }
36054
36179
  async delete(id) {
36055
36180
  if (this.catalogClient) {
36056
36181
  const canonical = await this.resolveId(id);
@@ -36705,6 +36830,12 @@ function normalizeModelRef(ref, defaultProvider) {
36705
36830
  function hasText(value) {
36706
36831
  return typeof value === "string" && value.trim().length > 0;
36707
36832
  }
36833
+ function asRefList(value) {
36834
+ return Array.isArray(value) ? value : void 0;
36835
+ }
36836
+ function asProfileName(value) {
36837
+ return hasText(value) ? value : void 0;
36838
+ }
36708
36839
  function providerHasKey(entry) {
36709
36840
  if (!entry) return false;
36710
36841
  if (hasText(entry.apiKey)) return true;
@@ -36715,7 +36846,7 @@ function providerHasKey(entry) {
36715
36846
  }
36716
36847
  function visibleProviderModels(config, providerId, providerModels) {
36717
36848
  const entry = config.providers?.[providerId];
36718
- return entry?.models !== void 0 ? [...entry.models] : providerModels;
36849
+ return Array.isArray(entry?.models) ? [...entry.models] : providerModels;
36719
36850
  }
36720
36851
  function buildProfiles(config) {
36721
36852
  const entries = /* @__PURE__ */ new Map();
@@ -36752,13 +36883,34 @@ var FallbackProfileManager = class {
36752
36883
  listProfiles() {
36753
36884
  return Object.freeze([...this.profiles.keys()]);
36754
36885
  }
36886
+ /**
36887
+ * The profile the session has selected (`config.fallbackProfile`, set by
36888
+ * `/fallback profile use <name>`), or undefined when none is selected or the
36889
+ * name no longer resolves to a defined profile.
36890
+ *
36891
+ * Consulted by every resolution entry point when the caller does not name a
36892
+ * profile itself. Without this the leader — which passes no profile — could
36893
+ * never use a named profile at all: `config.fallbackProfiles` was reachable
36894
+ * only by copying a chain into `fallbackModels`.
36895
+ */
36896
+ activeProfileName() {
36897
+ const name = asProfileName(this.config.fallbackProfile);
36898
+ return name && this.profiles.has(name) ? name : void 0;
36899
+ }
36755
36900
  // ── Resolution ─────────────────────────────────────────────────────────
36756
36901
  /**
36757
36902
  * Resolve a named fallback profile to a validated, provider-filtered chain.
36758
36903
  *
36759
- * Returns an empty chain when:
36760
- * - The profile doesn't exist.
36761
- * - Every entry's provider is missing, has no key, or has no matching model.
36904
+ * Returns an empty chain when the profile doesn't exist, or when every entry
36905
+ * is excluded, quarantined, or blacked out.
36906
+ *
36907
+ * Filtering is intentionally identical to {@link resolveRefs} (the explicit
36908
+ * `fallbackModels` path): the self-exclusion, the runtime status tracker,
36909
+ * and the availability calendar — nothing else. Anything a named profile
36910
+ * drops, an explicit chain drops too, and vice versa. Profiles used to apply
36911
+ * two extra filters (provider "usability" and the `providers[].models`
36912
+ * snapshot) that the explicit path did not, which silently rerouted roles
36913
+ * pinned to a profile onto a different model than the one configured.
36762
36914
  *
36763
36915
  * @param name - Profile name from config.fallbackProfiles.
36764
36916
  * @param defaultProvider - Used when an entry has no explicit provider.
@@ -36779,13 +36931,9 @@ var FallbackProfileManager = class {
36779
36931
  if (seen.has(key)) continue;
36780
36932
  seen.add(key);
36781
36933
  if (excludeKey && key === excludeKey) continue;
36782
- const health = this.checkProvider(providerId);
36783
- if (!health.usable) continue;
36784
36934
  if (this.statusTracker && !this.statusTracker.isAvailable(providerId, parsed.model)) continue;
36785
36935
  if (!evaluateModelCalendar(this.config.modelAvailabilitySchedule, providerId, parsed.model).allowed)
36786
36936
  continue;
36787
- const allowedModels = this.config.providers?.[providerId]?.models;
36788
- if (allowedModels && !allowedModels.includes(parsed.model)) continue;
36789
36937
  resolved.push({
36790
36938
  providerId,
36791
36939
  model: parsed.model,
@@ -36803,12 +36951,14 @@ var FallbackProfileManager = class {
36803
36951
  resolveEffective(opts = {}) {
36804
36952
  const bridge = this.resolveBridge(opts.exclude);
36805
36953
  let selected = FREEZER_EMPTY;
36806
- if (opts.fallbackModels && opts.fallbackModels.length > 0) {
36807
- const resolved = this.resolveRefs(opts.fallbackModels, opts.exclude);
36954
+ const explicitRefs = asRefList(opts.fallbackModels);
36955
+ const profileName = asProfileName(opts.fallbackProfile) ?? this.activeProfileName();
36956
+ if (explicitRefs && explicitRefs.length > 0) {
36957
+ const resolved = this.resolveRefs(explicitRefs, opts.exclude);
36808
36958
  if (resolved.length > 0) selected = resolved;
36809
36959
  }
36810
- if (selected.length === 0 && opts.fallbackProfile) {
36811
- const resolved = this.resolve(opts.fallbackProfile, { exclude: opts.exclude });
36960
+ if (selected.length === 0 && profileName) {
36961
+ const resolved = this.resolve(profileName, { exclude: opts.exclude });
36812
36962
  if (resolved.length > 0) selected = resolved;
36813
36963
  }
36814
36964
  if (selected.length === 0 && opts.fallbackAuto !== false) {
@@ -36884,13 +37034,14 @@ var FallbackProfileManager = class {
36884
37034
  };
36885
37035
  const configFallbackAuto = this.config.fallbackAuto;
36886
37036
  const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
36887
- const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
37037
+ const explicitRefs = asRefList(opts.fallbackModels) ?? asRefList(this.config.fallbackModels);
37038
+ const profileName = asProfileName(opts.fallbackProfile) ?? this.activeProfileName();
36888
37039
  const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
36889
- const profileUsable = opts.fallbackProfile !== void 0 && this.hasProfile(opts.fallbackProfile) && this.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
37040
+ const profileUsable = profileName !== void 0 && this.hasProfile(profileName) && this.resolve(profileName, { exclude: current }).length > 0;
36890
37041
  const fromExplicitSource = explicitUsable || profileUsable;
36891
- const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? this.resolve(opts.fallbackProfile, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
37042
+ const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : profileName ? this.resolve(profileName, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
36892
37043
  fallbackModels: explicitRefs,
36893
- fallbackProfile: opts.fallbackProfile,
37044
+ fallbackProfile: profileName,
36894
37045
  fallbackAuto: effectiveFallbackAuto,
36895
37046
  exclude: current
36896
37047
  });
@@ -36907,7 +37058,7 @@ var FallbackProfileManager = class {
36907
37058
  });
36908
37059
  }
36909
37060
  candidates.push(...selectedChain);
36910
- if (!fromExplicitSource && effectiveFallbackAuto && opts.fallbackProfile !== "default") {
37061
+ if (!fromExplicitSource && effectiveFallbackAuto && profileName !== "default") {
36911
37062
  candidates.push(...this.resolve("default", { exclude: current }));
36912
37063
  }
36913
37064
  if (!fromExplicitSource && effectiveFallbackAuto) {
@@ -36985,7 +37136,7 @@ var FallbackProfileManager = class {
36985
37136
  const leaderModel = this.config.model;
36986
37137
  const providers = this.config.providers ?? {};
36987
37138
  const favoriteSet = new Set(
36988
- (this.config.favoriteModels ?? []).map((ref) => {
37139
+ (asRefList(this.config.favoriteModels) ?? []).map((ref) => {
36989
37140
  const p = parseModelRef(ref);
36990
37141
  return `${p.provider ?? leaderProvider}/${p.model}`;
36991
37142
  })
@@ -37096,9 +37247,15 @@ function effectiveFallbackChain(config) {
37096
37247
  const mgr = new FallbackProfileManager(config);
37097
37248
  return mgr.resolveEffective({
37098
37249
  fallbackModels: config.fallbackModels,
37250
+ fallbackProfile: config.fallbackProfile,
37099
37251
  fallbackAuto: config.fallbackAuto
37100
37252
  }).map((e) => `${e.providerId}/${e.model}`);
37101
37253
  }
37254
+ function runtimeFallbackChain(config) {
37255
+ const mgr = new FallbackProfileManager(config);
37256
+ const current = primaryTarget(config);
37257
+ return mgr.resolveCandidates(current, {}).map((e) => `${e.providerId}/${e.model}`);
37258
+ }
37102
37259
  var DEFAULT_PRIMARY_COOLDOWN_MS = 6e4;
37103
37260
  var DEFAULT_PRIMARY_COOLDOWN_MAX_MS = 10 * 6e4;
37104
37261
  var DEFAULT_PRIMARY_RECOVERY_SUCCESSES = 2;
@@ -37138,7 +37295,11 @@ function createFallbackModelExtension(deps) {
37138
37295
  let blockedPrimary;
37139
37296
  let primaryBlockedUntil = 0;
37140
37297
  const now = () => deps.now?.() ?? Date.now();
37141
- const cooldownBase = () => Math.max(0, deps.primaryCooldownMs ?? DEFAULT_PRIMARY_COOLDOWN_MS);
37298
+ const liveStickiness = () => deps.getConfig().fallbackStickiness;
37299
+ const cooldownBase = () => Math.max(
37300
+ 0,
37301
+ deps.primaryCooldownMs ?? liveStickiness()?.primaryProbeInterval ?? DEFAULT_PRIMARY_COOLDOWN_MS
37302
+ );
37142
37303
  const cooldownMax = () => Math.max(cooldownBase(), deps.primaryCooldownMaxMs ?? DEFAULT_PRIMARY_COOLDOWN_MAX_MS);
37143
37304
  const selectedPrimary = (cfg) => deps.getPrimaryTarget?.() ?? primaryTarget(cfg);
37144
37305
  const primaryInCooldown = (cfg) => sameTarget(blockedPrimary, selectedPrimary(cfg)) && now() < primaryBlockedUntil;
@@ -37157,7 +37318,7 @@ function createFallbackModelExtension(deps) {
37157
37318
  primaryBlockedUntil = now() + Math.min(cooldownMax(), base * multiplier);
37158
37319
  };
37159
37320
  const recoveryTarget = () => Math.max(1, deps.primaryRecoverySuccesses ?? DEFAULT_PRIMARY_RECOVERY_SUCCESSES);
37160
- const stickyTarget = () => Math.max(0, deps.stickyFallbackTurns ?? 0);
37321
+ const stickyTarget = () => Math.max(0, deps.stickyFallbackTurns ?? liveStickiness()?.stickyFallbackTurns ?? 0);
37161
37322
  const inStickyWindow = () => stickyTurnsElapsed < stickyTarget();
37162
37323
  const onPrimarySuccess = (cfg) => {
37163
37324
  if (!sameTarget(blockedPrimary, selectedPrimary(cfg))) return;
@@ -46893,10 +47054,10 @@ var AdaptiveConcurrencyController = class {
46893
47054
 
46894
47055
  // src/coordination/agent-monitor.ts
46895
47056
  init_file_permissions();
46896
- import { createReadStream as createReadStream8 } from "node:fs";
47057
+ import { createReadStream as createReadStream9 } from "node:fs";
46897
47058
  import * as fs28 from "node:fs/promises";
46898
47059
  import * as path69 from "node:path";
46899
- import { createInterface as createInterface8 } from "node:readline";
47060
+ import { createInterface as createInterface9 } from "node:readline";
46900
47061
  var AgentMonitorService = class _AgentMonitorService {
46901
47062
  _fleetBus;
46902
47063
  _events;
@@ -47003,8 +47164,8 @@ var AgentMonitorService = class _AgentMonitorService {
47003
47164
  const accessible = await fs28.access(file).then(() => true).catch(() => false);
47004
47165
  if (!accessible) return [];
47005
47166
  const out = [];
47006
- const input = createReadStream8(file, { encoding: "utf8" });
47007
- const lines = createInterface8({ input, crlfDelay: Number.POSITIVE_INFINITY });
47167
+ const input = createReadStream9(file, { encoding: "utf8" });
47168
+ const lines = createInterface9({ input, crlfDelay: Number.POSITIVE_INFINITY });
47008
47169
  try {
47009
47170
  for await (const line of lines) {
47010
47171
  const trimmed = line.trim();
@@ -56653,6 +56814,12 @@ function findExchangeStart(messages, userIndex) {
56653
56814
 
56654
56815
  // src/execution/auto-compaction-middleware.ts
56655
56816
  var LEVEL_RANK2 = { warn: 0, soft: 1, hard: 2 };
56817
+ function pressureLevelFor(load, thresholds) {
56818
+ if (load >= thresholds.hard) return "hard";
56819
+ if (load >= thresholds.soft) return "soft";
56820
+ if (load >= thresholds.warn) return "warn";
56821
+ return null;
56822
+ }
56656
56823
  var MAX_DIGEST_LOG_CHARS = 4e3;
56657
56824
  function truncateDigest(digest2) {
56658
56825
  if (digest2.length <= MAX_DIGEST_LOG_CHARS) return digest2;
@@ -56698,8 +56865,19 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
56698
56865
  * 1 / 2.5 = 0.4.
56699
56866
  */
56700
56867
  static GUARD_GATE_LOAD = 0.4;
56868
+ /**
56869
+ * How much the context must grow between two history-rewriting hygiene
56870
+ * passes, as a fraction of the available input window and as an absolute
56871
+ * floor. Every pass rewrites already-transmitted messages, which forces the
56872
+ * provider to re-cache the whole prompt; spacing the passes out is what lets
56873
+ * the conversation prefix stay cached for the turns in between.
56874
+ */
56875
+ static HYGIENE_GROWTH_RATIO = 0.15;
56876
+ static HYGIENE_MIN_GROWTH_TOKENS = 2e4;
56701
56877
  /** Tracks the most recent no-op attempt so we can avoid re-firing per turn. */
56702
56878
  lastNoopAttempt = null;
56879
+ /** Context size at the last hygiene pass; anchors the growth interval. */
56880
+ lastHygieneTokens = null;
56703
56881
  /**
56704
56882
  * Cached token estimate from the last handler() invocation. When the
56705
56883
  * message count and tool count haven't changed since the last estimate
@@ -56759,55 +56937,9 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
56759
56937
  handler() {
56760
56938
  return async (ctx, next) => {
56761
56939
  if (!this._enabled) return next(ctx);
56762
- const rawHygiene = eliseAcknowledgedToolResults(ctx.messages, {
56763
- maxRetainedTokens: this.resolveToolResultRetention(ctx)
56764
- });
56765
- const receiptHygiene = collapseAcknowledgedToolReceipts(rawHygiene.messages, {
56766
- maxPairs: this.resolveToolReceiptRetention(ctx)
56767
- });
56768
- if (rawHygiene.changed || receiptHygiene.changed) {
56769
- ctx.state.replaceMessages(receiptHygiene.messages);
56770
- ctx.clearFileTracking();
56771
- this.invalidateTokenCaches(ctx);
56772
- }
56773
- const msgCount = ctx.messages.length;
56774
- const toolCount = (ctx.tools ?? []).length;
56775
- const revision = ctx.state?.revision ?? -1;
56776
- let tokens;
56777
- const anchorAt = typeof ctx.meta?.["realAnchorMsgCount"] === "number" ? ctx.meta["realAnchorMsgCount"] : void 0;
56778
- const anchored = realAnchoredInputTokens(ctx.messages, ctx.lastRealInputTokens, anchorAt);
56779
- if (anchored !== null) {
56780
- tokens = anchored;
56781
- } else if (this._estimator) {
56782
- tokens = this._estimator(ctx);
56783
- } else if (msgCount === this._cachedMsgCount && toolCount === this._cachedToolCount && revision === this._cachedRevision && ctx.systemPrompt === this._cachedSystemRef && ctx.tools === this._cachedToolsRef && this._cachedTokens >= 0) {
56784
- tokens = this._cachedTokens;
56785
- } else if (this.tryStashedTokens(ctx, msgCount, toolCount, revision) !== null) {
56786
- const stashed = this.tryStashedTokens(ctx, msgCount, toolCount, revision);
56787
- const cal = getCalibrationState(`${ctx.provider?.id ?? "unknown"}/${ctx.model}`);
56788
- tokens = cal.calibrated ? Math.round(stashed * Math.min(1.5, Math.max(0.5, cal.ratio))) : stashed;
56789
- this._cachedTokens = tokens;
56790
- this._cachedMsgCount = msgCount;
56791
- this._cachedToolCount = toolCount;
56792
- this._cachedRevision = revision;
56793
- this._cachedSystemRef = ctx.systemPrompt;
56794
- this._cachedToolsRef = ctx.tools;
56795
- } else {
56796
- tokens = estimateRequestTokensCalibrated(
56797
- ctx.messages,
56798
- ctx.systemPrompt,
56799
- ctx.tools ?? [],
56800
- `${ctx.provider?.id ?? "unknown"}/${ctx.model}`
56801
- ).total;
56802
- this._cachedTokens = tokens;
56803
- this._cachedMsgCount = msgCount;
56804
- this._cachedToolCount = toolCount;
56805
- this._cachedRevision = revision;
56806
- this._cachedSystemRef = ctx.systemPrompt;
56807
- this._cachedToolsRef = ctx.tools;
56808
- }
56940
+ let tokens = this.estimateContextTokens(ctx);
56809
56941
  const runtimeMaxContext = effectiveMaxContext(ctx, this._maxContext);
56810
- const budget = computeContextWindowBudget(ctx, tokens, runtimeMaxContext);
56942
+ let budget = computeContextWindowBudget(ctx, tokens, runtimeMaxContext);
56811
56943
  const calibratedLoad = budget.load;
56812
56944
  const policy = this.policyProvider?.(ctx);
56813
56945
  const thresholds = policy?.thresholds ?? {
@@ -56821,22 +56953,27 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
56821
56953
  });
56822
56954
  const aggressiveOn = policy?.aggressiveOn ?? this.aggressiveOn;
56823
56955
  const targetLoad = normalizeTargetLoad(policy?.targetLoad, adaptiveThresholds);
56824
- let load = calibratedLoad;
56825
- if (calibratedLoad >= _AutoCompactionMiddleware.GUARD_GATE_LOAD) {
56826
- const guardTotal = estimateRequestTokensUpperBound(
56827
- ctx.messages,
56828
- ctx.systemPrompt,
56829
- ctx.tools ?? [],
56830
- `${ctx.provider?.id ?? "unknown"}/${ctx.model}`
56831
- ).total;
56832
- const guardLoad = guardTotal / budget.availableInputTokens;
56833
- if (guardLoad > load) load = guardLoad;
56834
- }
56835
- const level = load >= adaptiveThresholds.hard ? "hard" : load >= adaptiveThresholds.soft ? "soft" : load >= adaptiveThresholds.warn ? "warn" : null;
56956
+ let load = this.applySendGuard(ctx, calibratedLoad, budget.availableInputTokens);
56957
+ let level = pressureLevelFor(load, adaptiveThresholds);
56836
56958
  if (!level) {
56837
56959
  this.lastNoopAttempt = null;
56838
56960
  return next(ctx);
56839
56961
  }
56962
+ if (this.shouldRunHygiene(level, tokens, budget.availableInputTokens)) {
56963
+ const changed = this.runHistoryHygiene(ctx);
56964
+ tokens = changed ? this.estimateContextTokens(ctx) : tokens;
56965
+ this.lastHygieneTokens = tokens;
56966
+ if (changed) {
56967
+ budget = computeContextWindowBudget(ctx, tokens, runtimeMaxContext);
56968
+ load = this.applySendGuard(ctx, budget.load, budget.availableInputTokens);
56969
+ const relevelled = pressureLevelFor(load, adaptiveThresholds);
56970
+ if (!relevelled) {
56971
+ this.lastNoopAttempt = null;
56972
+ return next(ctx);
56973
+ }
56974
+ level = relevelled;
56975
+ }
56976
+ }
56840
56977
  if (this.shouldSkipNoopRetry(level, tokens)) {
56841
56978
  return next(ctx);
56842
56979
  }
@@ -56853,6 +56990,123 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
56853
56990
  return next(ctx);
56854
56991
  };
56855
56992
  }
56993
+ /**
56994
+ * Full-request token total for the current context.
56995
+ *
56996
+ * Reuses the last estimate when the context hasn't grown since the previous
56997
+ * check — common in autonomous idle loops. The cached value is invalidated
56998
+ * whenever messages or tools change.
56999
+ *
57000
+ * IMPORTANT: the cache is only valid for the deterministic
57001
+ * `estimateRequestTokensCalibrated` path (messages+system+tools → fixed
57002
+ * output). When a custom `_estimator` is provided (e.g. in tests with a
57003
+ * mutable closure, or a dynamic policy provider), always call it fresh — the
57004
+ * estimator owns its own semantics and the middleware cannot safely cache its
57005
+ * result across calls.
57006
+ */
57007
+ estimateContextTokens(ctx) {
57008
+ const msgCount = ctx.messages.length;
57009
+ const toolCount = (ctx.tools ?? []).length;
57010
+ const revision = ctx.state?.revision ?? -1;
57011
+ const anchorAt = typeof ctx.meta?.["realAnchorMsgCount"] === "number" ? ctx.meta["realAnchorMsgCount"] : void 0;
57012
+ const anchored = realAnchoredInputTokens(ctx.messages, ctx.lastRealInputTokens, anchorAt);
57013
+ if (anchored !== null) return anchored;
57014
+ if (this._estimator) return this._estimator(ctx);
57015
+ if (msgCount === this._cachedMsgCount && toolCount === this._cachedToolCount && revision === this._cachedRevision && ctx.systemPrompt === this._cachedSystemRef && ctx.tools === this._cachedToolsRef && this._cachedTokens >= 0) {
57016
+ return this._cachedTokens;
57017
+ }
57018
+ const stashed = this.tryStashedTokens(ctx, msgCount, toolCount, revision);
57019
+ let tokens;
57020
+ if (stashed !== null) {
57021
+ const cal = getCalibrationState(`${ctx.provider?.id ?? "unknown"}/${ctx.model}`);
57022
+ tokens = cal.calibrated ? Math.round(stashed * Math.min(1.5, Math.max(0.5, cal.ratio))) : stashed;
57023
+ } else {
57024
+ tokens = estimateRequestTokensCalibrated(
57025
+ ctx.messages,
57026
+ ctx.systemPrompt,
57027
+ ctx.tools ?? [],
57028
+ `${ctx.provider?.id ?? "unknown"}/${ctx.model}`
57029
+ ).total;
57030
+ }
57031
+ this._cachedTokens = tokens;
57032
+ this._cachedMsgCount = msgCount;
57033
+ this._cachedToolCount = toolCount;
57034
+ this._cachedRevision = revision;
57035
+ this._cachedSystemRef = ctx.systemPrompt;
57036
+ this._cachedToolsRef = ctx.tools;
57037
+ return tokens;
57038
+ }
57039
+ /**
57040
+ * Never-undercount send guard.
57041
+ *
57042
+ * The calibrated estimate can under-count dense content (CJK, base64,
57043
+ * minified) by >1.5×, which would let an over-limit request slip past the
57044
+ * thresholds and reach the provider. Once the calibrated load is high enough
57045
+ * that even the max density factor (2.5×) *could* overflow (load ≥ 1/2.5 =
57046
+ * 0.4), re-check with the upper-bound estimator and escalate to whichever
57047
+ * load is larger. Below 0.4 an overflow is arithmetically impossible, so the
57048
+ * extra scan is skipped.
57049
+ */
57050
+ applySendGuard(ctx, calibratedLoad, availableInputTokens) {
57051
+ if (calibratedLoad < _AutoCompactionMiddleware.GUARD_GATE_LOAD) return calibratedLoad;
57052
+ const guardTotal = estimateRequestTokensUpperBound(
57053
+ ctx.messages,
57054
+ ctx.systemPrompt,
57055
+ ctx.tools ?? [],
57056
+ `${ctx.provider?.id ?? "unknown"}/${ctx.model}`
57057
+ ).total;
57058
+ const guardLoad = guardTotal / availableInputTokens;
57059
+ return guardLoad > calibratedLoad ? guardLoad : calibratedLoad;
57060
+ }
57061
+ /**
57062
+ * Rewrite acknowledged tool protocol in place.
57063
+ *
57064
+ * Tool results are protocol inputs for the immediately following model
57065
+ * response, not unlimited durable prompt memory. Once a later assistant
57066
+ * message proves the provider consumed them, keep a mode-sized raw window and
57067
+ * replace the rest with semantic head/tail receipts, then fold fully-aged
57068
+ * pairs into the history digest.
57069
+ *
57070
+ * Every one of those edits touches a message the provider has already seen,
57071
+ * so each call costs a full prompt re-cache — it belongs behind
57072
+ * {@link shouldRunHygiene}, never on the per-turn path. Content staleness is
57073
+ * not a reason to bypass that gate: the edit/replace tools guard against
57074
+ * acting on an out-of-date read with their own mtime + sha-256 check.
57075
+ *
57076
+ * @returns whether the conversation was rewritten.
57077
+ */
57078
+ runHistoryHygiene(ctx) {
57079
+ const raw = eliseAcknowledgedToolResults(ctx.messages, {
57080
+ maxRetainedTokens: this.resolveToolResultRetention(ctx)
57081
+ });
57082
+ const receipts = collapseAcknowledgedToolReceipts(raw.messages, {
57083
+ maxPairs: this.resolveToolReceiptRetention(ctx)
57084
+ });
57085
+ if (!raw.changed && !receipts.changed) return false;
57086
+ ctx.state.replaceMessages(receipts.messages);
57087
+ ctx.clearFileTracking();
57088
+ this.invalidateTokenCaches(ctx);
57089
+ return true;
57090
+ }
57091
+ /**
57092
+ * Whether the history-rewriting hygiene pass may run this turn.
57093
+ *
57094
+ * Hard pressure always runs it — staying under the window outranks caching.
57095
+ * Otherwise it runs at most once per growth interval, so the conversation
57096
+ * stays append-only (and therefore cacheable by the provider) in between.
57097
+ */
57098
+ shouldRunHygiene(level, tokens, availableInputTokens) {
57099
+ if (level === "hard") return true;
57100
+ const last = this.lastHygieneTokens;
57101
+ if (last === null) return true;
57102
+ const anchor = Math.min(last, tokens);
57103
+ this.lastHygieneTokens = anchor;
57104
+ const interval = Math.max(
57105
+ _AutoCompactionMiddleware.HYGIENE_MIN_GROWTH_TOKENS,
57106
+ Math.floor(availableInputTokens * _AutoCompactionMiddleware.HYGIENE_GROWTH_RATIO)
57107
+ );
57108
+ return tokens - anchor >= interval;
57109
+ }
56856
57110
  /**
56857
57111
  * H1: try to read a pre-computed token total from `ctx.lastRequestTokens`
56858
57112
  * (set by the agent loop's pre-flight or its restash in emitContextPct).
@@ -61665,26 +61919,29 @@ var DefaultProviderRunner = class {
61665
61919
  // src/execution/retry-policy.ts
61666
61920
  import { randomInt } from "node:crypto";
61667
61921
  var MAX_RETRY_AFTER_MS = 6e4;
61922
+ var MODEL_RETRIES = 3;
61668
61923
  var MAX_ATTEMPTS_BY_KIND = {
61669
- rate_limit: 5,
61924
+ rate_limit: MODEL_RETRIES,
61925
+ overloaded: MODEL_RETRIES,
61926
+ server: MODEL_RETRIES,
61927
+ timeout: MODEL_RETRIES,
61928
+ network: MODEL_RETRIES,
61929
+ stream_hang: MODEL_RETRIES,
61670
61930
  quota_exhausted: 0,
61671
- stream_hang: 2,
61672
- // proxy-level timeout — retrying 5x wastes ~40s before fallback kicks in
61673
- overloaded: 3,
61674
- server: 3,
61675
- timeout: 2,
61676
- network: 2,
61677
61931
  auth: 0,
61678
61932
  invalid_request: 0,
61679
61933
  context_overflow: 0,
61680
61934
  content_filter: 0,
61681
61935
  unknown: 0
61682
61936
  };
61937
+ var FAILOVER_RETRY_AFTER_MS = 15e3;
61683
61938
  var DefaultRetryPolicy = class {
61684
61939
  shouldRetry(err, attempt) {
61685
61940
  const isProviderErr = err instanceof ProviderError || ProviderError.isProviderError(err);
61686
61941
  if (isProviderErr) {
61687
61942
  if (!err.retryable) return false;
61943
+ const hint = retryAfterMsFromError(err);
61944
+ if (hint !== void 0 && hint >= FAILOVER_RETRY_AFTER_MS) return false;
61688
61945
  return attempt < this.maxAttempts(err);
61689
61946
  }
61690
61947
  const msg = err.message ?? "";
@@ -67094,7 +67351,7 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
67094
67351
  };
67095
67352
 
67096
67353
  // src/security/permission-helpers.ts
67097
- import { realpathSync as realpathSync2 } from "node:fs";
67354
+ import { realpathSync as realpathSync3 } from "node:fs";
67098
67355
  import * as path86 from "node:path";
67099
67356
  function matchesTrust(patterns, subject2) {
67100
67357
  return patterns.includes(subject2) || matchAny(patterns, subject2);
@@ -67179,7 +67436,7 @@ function realpathOfNearestExisting(p) {
67179
67436
  const tail = [];
67180
67437
  for (; ; ) {
67181
67438
  try {
67182
- return tail.length === 0 ? realpathSync2(probe2) : path86.join(realpathSync2(probe2), ...tail);
67439
+ return tail.length === 0 ? realpathSync3(probe2) : path86.join(realpathSync3(probe2), ...tail);
67183
67440
  } catch {
67184
67441
  const parent = path86.dirname(probe2);
67185
67442
  if (parent === probe2) return p;
@@ -83348,6 +83605,156 @@ var CLOUD_SYNC_CONTRACT = {
83348
83605
  "extensions.plugins": EXTENSIONS_PLUGINS_TREE
83349
83606
  };
83350
83607
  var CLOUD_SYNC_NAMESPACES = Object.keys(CLOUD_SYNC_CONTRACT);
83608
+ var INBOUND_DENIED_PATHS = [
83609
+ // ── Code execution ──────────────────────────────────────────────────────
83610
+ {
83611
+ namespace: "mcp.servers",
83612
+ path: "mcpServers.*.command",
83613
+ reason: "Executable spawned for a stdio MCP server."
83614
+ },
83615
+ {
83616
+ namespace: "mcp.servers",
83617
+ path: "mcpServers.*.args",
83618
+ reason: "Argv for that executable."
83619
+ },
83620
+ {
83621
+ namespace: "mcp.servers",
83622
+ path: "mcpServers.*.transport",
83623
+ reason: "Switching transport to stdio selects the spawning code path."
83624
+ },
83625
+ {
83626
+ namespace: "extensions.plugins",
83627
+ path: "plugins",
83628
+ reason: "Plugin list is resolved and `await import`ed."
83629
+ },
83630
+ // `extensions` is deliberately NOT denied: it is per-plugin settings read via
83631
+ // `ConfigStore.getExtension(name)`, not a loader list, so it grants no import.
83632
+ // Syncing it is the feature (`extensions.telegram.notifyChatId` and friends).
83633
+ // Residual risk accepted: a plugin whose own settings include a URL can have
83634
+ // that URL rewritten by the portal. Constrain that in the plugin's
83635
+ // `configSchema`, which is where the loader validates it.
83636
+ // ── Credential redirection / exfiltration ───────────────────────────────
83637
+ {
83638
+ namespace: "providers.catalog",
83639
+ path: "providers.*.baseUrl",
83640
+ reason: "Repoints the provider endpoint; reinjectLocalSecrets keeps the local apiKey, so the real key follows the redirect."
83641
+ },
83642
+ {
83643
+ namespace: "providers.catalog",
83644
+ path: "providers.*.envVars",
83645
+ reason: "Chooses which environment variable is read for the key."
83646
+ },
83647
+ {
83648
+ namespace: "providers.catalog",
83649
+ path: "providers.*.activeKey",
83650
+ reason: "Selects which stored key is sent."
83651
+ },
83652
+ {
83653
+ namespace: "mcp.servers",
83654
+ path: "mcpServers.*.url",
83655
+ reason: "Remote MCP endpoint; receives whatever the transport carries."
83656
+ },
83657
+ {
83658
+ namespace: "mcp.servers",
83659
+ path: "mcpServers.*.envVars",
83660
+ reason: "Names of environment variables forwarded to the server process."
83661
+ },
83662
+ // ── Operator-owned safety switches ──────────────────────────────────────
83663
+ {
83664
+ namespace: "mcp.servers",
83665
+ path: "mcpServers.*.permission",
83666
+ reason: "Approval requirement for that server\u2019s tools."
83667
+ },
83668
+ { namespace: "core.runtime", path: "yolo", reason: "Disables every permission prompt." },
83669
+ {
83670
+ namespace: "core.runtime",
83671
+ path: "features.allowOutsideProjectRoot",
83672
+ reason: "Short-circuits project-root containment and the symlink realpath check."
83673
+ },
83674
+ {
83675
+ namespace: "core.runtime",
83676
+ path: "tools.restrictToProjectRoot",
83677
+ reason: "The other half of the filesystem confinement switch."
83678
+ },
83679
+ {
83680
+ namespace: "core.runtime",
83681
+ path: "features.developerMode",
83682
+ reason: "Loosens guardrails; an operator opt-in, not a synced preference."
83683
+ },
83684
+ {
83685
+ namespace: "core.runtime",
83686
+ path: "tools.disabledTools",
83687
+ reason: "Could re-enable a tool the operator deliberately switched off."
83688
+ },
83689
+ {
83690
+ namespace: "ui.preferences",
83691
+ path: "autonomy.defaultMode",
83692
+ reason: "Autonomy is user-owned, never remote-owned."
83693
+ },
83694
+ {
83695
+ namespace: "ui.preferences",
83696
+ path: "autonomy.yolo",
83697
+ reason: "Alias for the denied top-level `yolo`, and it wins over the user setting."
83698
+ },
83699
+ {
83700
+ namespace: "ui.preferences",
83701
+ path: "launch.autonomy",
83702
+ reason: "Launch-time autonomy mode; same user-owned boundary."
83703
+ },
83704
+ {
83705
+ namespace: "models.routing",
83706
+ path: "brain.mode",
83707
+ reason: "Selects the policy/LLM/human decision ladder."
83708
+ },
83709
+ {
83710
+ namespace: "models.routing",
83711
+ path: "brain.maxAutoRisk",
83712
+ reason: "The risk ceiling below which actions proceed without asking."
83713
+ }
83714
+ ];
83715
+ function contractHasPath(tree, segments) {
83716
+ if (segments.length === 0) return true;
83717
+ if (tree === true) return false;
83718
+ const [head, ...rest] = segments;
83719
+ if (head === void 0 || !Object.hasOwn(tree, head)) return false;
83720
+ const child = tree[head];
83721
+ return child === void 0 ? false : contractHasPath(child, rest);
83722
+ }
83723
+ function pruneContractPath(tree, segments) {
83724
+ if (tree === true || segments.length === 0) return tree;
83725
+ const [head, ...rest] = segments;
83726
+ if (head === void 0 || !Object.hasOwn(tree, head)) return tree;
83727
+ const next = { ...tree };
83728
+ if (rest.length === 0) {
83729
+ delete next[head];
83730
+ return next;
83731
+ }
83732
+ const child = next[head];
83733
+ if (child === void 0) return tree;
83734
+ next[head] = pruneContractPath(child, rest);
83735
+ return next;
83736
+ }
83737
+ function assertInboundDenyListResolves() {
83738
+ const unresolved = INBOUND_DENIED_PATHS.filter((entry) => {
83739
+ const tree = CLOUD_SYNC_CONTRACT[entry.namespace];
83740
+ return tree === void 0 || !contractHasPath(tree, entry.path.split("."));
83741
+ });
83742
+ if (unresolved.length > 0) {
83743
+ throw new Error(
83744
+ "INBOUND_DENIED_PATHS entr(ies) no longer resolve against CLOUD_SYNC_CONTRACT \u2014 a rename would silently re-open them: " + unresolved.map((entry) => `${entry.namespace}:${entry.path}`).join(", ")
83745
+ );
83746
+ }
83747
+ }
83748
+ var INBOUND_CONTRACT = (() => {
83749
+ assertInboundDenyListResolves();
83750
+ const out = { ...CLOUD_SYNC_CONTRACT };
83751
+ for (const entry of INBOUND_DENIED_PATHS) {
83752
+ const tree = out[entry.namespace];
83753
+ if (tree === void 0) continue;
83754
+ out[entry.namespace] = pruneContractPath(tree, entry.path.split("."));
83755
+ }
83756
+ return out;
83757
+ })();
83351
83758
  var NAMESPACE_SCHEMA_VERSIONS = {
83352
83759
  "core.runtime": 1,
83353
83760
  "ui.preferences": 1,
@@ -83449,7 +83856,7 @@ function mergeAtContract(local, incoming, tree) {
83449
83856
  return base;
83450
83857
  }
83451
83858
  function applyNamespacePayload(config, namespace, payload) {
83452
- const tree = CLOUD_SYNC_CONTRACT[namespace];
83859
+ const tree = INBOUND_CONTRACT[namespace];
83453
83860
  if (!tree || tree === true) return config;
83454
83861
  const next = { ...config };
83455
83862
  for (const [key, incoming] of Object.entries(payload)) {
@@ -91573,6 +91980,15 @@ async function readProviderSnapshot(configPath, vault, warn) {
91573
91980
  snapshot.fallbackBridge = decrypted.fallbackBridge.trim();
91574
91981
  }
91575
91982
  if (decrypted.fallbackProfiles) snapshot.fallbackProfiles = decrypted.fallbackProfiles;
91983
+ if (typeof decrypted.fallbackProfile === "string" && decrypted.fallbackProfile.trim()) {
91984
+ snapshot.fallbackProfile = decrypted.fallbackProfile.trim();
91985
+ }
91986
+ if (decrypted.fallbackStickiness && typeof decrypted.fallbackStickiness === "object") {
91987
+ snapshot.fallbackStickiness = decrypted.fallbackStickiness;
91988
+ }
91989
+ if (typeof decrypted.fallbackMaxLastResortCandidates === "number" && Number.isFinite(decrypted.fallbackMaxLastResortCandidates)) {
91990
+ snapshot.fallbackMaxLastResortCandidates = decrypted.fallbackMaxLastResortCandidates;
91991
+ }
91576
91992
  if (Array.isArray(decrypted.favoriteModels)) snapshot.favoriteModels = decrypted.favoriteModels;
91577
91993
  if (typeof decrypted.favoriteModelsOnly === "boolean")
91578
91994
  snapshot.favoriteModelsOnly = decrypted.favoriteModelsOnly;
@@ -91591,10 +92007,13 @@ function serializeSnapshot(s) {
91591
92007
  fallbackModels: s.fallbackModels ?? null,
91592
92008
  fallbackBridge: s.fallbackBridge ?? null,
91593
92009
  fallbackProfiles: s.fallbackProfiles ?? null,
92010
+ fallbackProfile: s.fallbackProfile ?? null,
91594
92011
  favoriteModels: s.favoriteModels ?? null,
91595
92012
  favoriteModelsOnly: s.favoriteModelsOnly ?? null,
91596
92013
  modelMatrix: s.modelMatrix ?? null,
91597
92014
  fallbackAuto: s.fallbackAuto ?? null,
92015
+ fallbackStickiness: s.fallbackStickiness ?? null,
92016
+ fallbackMaxLastResortCandidates: s.fallbackMaxLastResortCandidates ?? null,
91598
92017
  modelAvailabilitySchedule: s.modelAvailabilitySchedule ?? null
91599
92018
  });
91600
92019
  }
@@ -92080,10 +92499,10 @@ var ReplayLogStore = class _ReplayLogStore {
92080
92499
  };
92081
92500
 
92082
92501
  // src/storage/session-recovery.ts
92083
- import { createReadStream as createReadStream9 } from "node:fs";
92502
+ import { createReadStream as createReadStream10 } from "node:fs";
92084
92503
  import * as fs67 from "node:fs/promises";
92085
92504
  import * as path122 from "node:path";
92086
- import { createInterface as createInterface9 } from "node:readline";
92505
+ import { createInterface as createInterface10 } from "node:readline";
92087
92506
  var SessionRecovery = class _SessionRecovery {
92088
92507
  constructor(dir) {
92089
92508
  this.dir = dir;
@@ -92160,8 +92579,8 @@ var SessionRecovery = class _SessionRecovery {
92160
92579
  let lastCheckpoint = null;
92161
92580
  let latestBoundary = null;
92162
92581
  let sawEvent = false;
92163
- const stream = createReadStream9(fp, { encoding: "utf8" });
92164
- const lines = createInterface9({ input: stream, crlfDelay: Infinity });
92582
+ const stream = createReadStream10(fp, { encoding: "utf8" });
92583
+ const lines = createInterface10({ input: stream, crlfDelay: Infinity });
92165
92584
  try {
92166
92585
  for await (const line of lines) {
92167
92586
  if (!line.trim()) continue;
@@ -92320,10 +92739,10 @@ async function applyRewindToConversation(opts) {
92320
92739
  init_errors();
92321
92740
  init_atomic_write();
92322
92741
  init_error();
92323
- import { createReadStream as createReadStream10 } from "node:fs";
92742
+ import { createReadStream as createReadStream11 } from "node:fs";
92324
92743
  import * as fsp44 from "node:fs/promises";
92325
92744
  import * as path123 from "node:path";
92326
- import { createInterface as createInterface10 } from "node:readline";
92745
+ import { createInterface as createInterface11 } from "node:readline";
92327
92746
  var DefaultSessionRewinder = class {
92328
92747
  constructor(sessionsDir, projectRoot) {
92329
92748
  this.sessionsDir = sessionsDir;
@@ -92335,8 +92754,8 @@ var DefaultSessionRewinder = class {
92335
92754
  return sessionScopedPath(this.sessionsDir, sessionId, ".jsonl");
92336
92755
  }
92337
92756
  async *readEvents(file) {
92338
- const stream = createReadStream10(file, { encoding: "utf8" });
92339
- const lines = createInterface10({ input: stream, crlfDelay: Infinity });
92757
+ const stream = createReadStream11(file, { encoding: "utf8" });
92758
+ const lines = createInterface11({ input: stream, crlfDelay: Infinity });
92340
92759
  try {
92341
92760
  for await (const line of lines) {
92342
92761
  if (!line.trim()) continue;
@@ -93968,6 +94387,30 @@ var PROVIDER_MANAGE_SCHEMA = {
93968
94387
  required: ["action"],
93969
94388
  additionalProperties: false
93970
94389
  };
94390
+ var CREDENTIAL_SELECTOR_FIELDS = ["apiKey", "apiKeys", "activeKey", "envVars"];
94391
+ function envVarsClaimedByOtherProviders(providers, exceptProvider) {
94392
+ const claimed = /* @__PURE__ */ new Map();
94393
+ for (const [id, entry] of Object.entries(providers)) {
94394
+ if (id === exceptProvider) continue;
94395
+ const names = entry?.["envVars"];
94396
+ if (!Array.isArray(names)) continue;
94397
+ for (const name of names) {
94398
+ if (typeof name === "string" && !claimed.has(name)) claimed.set(name, id);
94399
+ }
94400
+ }
94401
+ return claimed;
94402
+ }
94403
+ function rejectBorrowedEnvVars(providers, provider, requested) {
94404
+ if (!requested) return null;
94405
+ const claimed = envVarsClaimedByOtherProviders(providers, provider);
94406
+ for (const name of requested) {
94407
+ const owner = typeof name === "string" ? claimed.get(name) : void 0;
94408
+ if (owner !== void 0) {
94409
+ return `Environment variable "${String(name)}" already supplies the key for provider "${owner}". Reading another provider's credential from "${provider}" is not allowed; use provider_key_set to give "${provider}" its own key.`;
94410
+ }
94411
+ }
94412
+ return null;
94413
+ }
93971
94414
  function validateProviderBaseUrl(raw) {
93972
94415
  let url;
93973
94416
  try {
@@ -94038,6 +94481,8 @@ ${msg}`,
94038
94481
  const invalid2 = validateProviderBaseUrl(input.baseUrl);
94039
94482
  if (invalid2) return { status: "error", message: invalid2 };
94040
94483
  }
94484
+ const borrowed = rejectBorrowedEnvVars(providers, input.provider, input.envVars);
94485
+ if (borrowed) return { status: "error", message: borrowed };
94041
94486
  const entry = { type: input.type };
94042
94487
  if (input.models) entry.models = input.models;
94043
94488
  if (input.baseUrl) entry.baseUrl = input.baseUrl;
@@ -94071,14 +94516,22 @@ ${msg}`,
94071
94516
  if (input.autoDiscoverModels !== void 0) entry.autoDiscoverModels = input.autoDiscoverModels;
94072
94517
  if (input.apiKey !== void 0) entry.apiKey = input.apiKey || void 0;
94073
94518
  const endpointChanged = input.baseUrl !== void 0 && (entry.baseUrl ?? void 0) !== (previous.baseUrl ?? void 0);
94074
- const keyDropped = endpointChanged && input.apiKey === void 0 && previous.apiKey !== void 0;
94075
- if (keyDropped) entry.apiKey = void 0;
94519
+ const explicitlySupplied = /* @__PURE__ */ new Set([
94520
+ ...input.apiKey !== void 0 ? ["apiKey"] : [],
94521
+ ...input.envVars !== void 0 ? ["envVars"] : []
94522
+ ]);
94523
+ const droppedFields = endpointChanged ? CREDENTIAL_SELECTOR_FIELDS.filter(
94524
+ (field) => !explicitlySupplied.has(field) && previous[field] !== void 0
94525
+ ) : [];
94526
+ for (const field of droppedFields) entry[field] = void 0;
94527
+ const borrowed = rejectBorrowedEnvVars(providers, input.provider, input.envVars);
94528
+ if (borrowed) return { status: "error", message: borrowed };
94076
94529
  providers[input.provider] = entry;
94077
94530
  await opts.updateConfig((cfg) => {
94078
94531
  cfg.providers = providers;
94079
94532
  });
94080
94533
  const updated = Object.keys({ ...entry }).filter((k) => k !== "apiKey").join(", ");
94081
- const keyNote = keyDropped ? " \u2014 stored API key cleared because the base URL changed; set it again with provider_key_set" : "";
94534
+ const keyNote = droppedFields.length > 0 ? ` \u2014 cleared ${droppedFields.join(", ")} because the base URL changed; set the key again with provider_key_set` : "";
94082
94535
  return { status: "ok", message: `\u2713 Updated ${input.provider}: ${updated}${keyNote}` };
94083
94536
  }
94084
94537
  if (input.action === "remove") {
@@ -97019,6 +97472,7 @@ export {
97019
97472
  runShellHook,
97020
97473
  runWithNetworkTelemetry,
97021
97474
  runWithProcessTelemetry,
97475
+ runtimeFallbackChain,
97022
97476
  safeEmit,
97023
97477
  safeParse,
97024
97478
  safeProfileName,