@wrongstack/core 0.306.2 → 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.
Files changed (41) hide show
  1. package/dist/coordination/index.js +184 -86
  2. package/dist/coordination/mail-tools.d.ts +1 -1
  3. package/dist/coordination/mailbox-project-server.js +38 -16
  4. package/dist/coordination/sqlite-mailbox.d.ts +1 -0
  5. package/dist/core/conversation-state.d.ts +1 -2
  6. package/dist/core/fallback-model.d.ts +19 -1
  7. package/dist/core/fallback-profile-manager.d.ts +21 -3
  8. package/dist/core/index.d.ts +1 -1
  9. package/dist/core/index.js +152 -46
  10. package/dist/defaults/index.js +347 -139
  11. package/dist/execution/auto-compaction-middleware.d.ts +64 -0
  12. package/dist/execution/compaction-core.d.ts +4 -0
  13. package/dist/execution/index.d.ts +1 -1
  14. package/dist/execution/index.js +234 -91
  15. package/dist/execution/retry-policy.d.ts +27 -0
  16. package/dist/hq/index.js +50 -15
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +642 -184
  19. package/dist/infrastructure/index.js +21 -0
  20. package/dist/plugin/index.js +195 -19
  21. package/dist/security/index.js +52 -16
  22. package/dist/security/kanban-boundary.d.ts +3 -1
  23. package/dist/session-catalog/index.js +50 -15
  24. package/dist/session-catalog/project-server.js +50 -15
  25. package/dist/storage/cloud-config-sync/sanitize.d.ts +18 -0
  26. package/dist/storage/cloud-config-sync.d.ts +1 -1
  27. package/dist/storage/index.js +326 -71
  28. package/dist/storage/provider-config-watcher.d.ts +9 -0
  29. package/dist/storage/session-store/strict-empty-check.d.ts +7 -0
  30. package/dist/storage/session-store.d.ts +1 -0
  31. package/dist/tools/index.d.ts +1 -1
  32. package/dist/tools/index.js +83 -22
  33. package/dist/types/blocks.d.ts +9 -0
  34. package/dist/types/config/root.d.ts +14 -0
  35. package/dist/types/session.d.ts +7 -0
  36. package/instructions/agents/code-reviewer.md +3 -0
  37. package/instructions/coordination/subagent-baseline.md +10 -1
  38. package/instructions/system-lite.md +8 -5
  39. package/instructions/system-pro.md +26 -0
  40. package/instructions/system.md +21 -0
  41. package/package.json +3 -3
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
  /**
@@ -32528,6 +32618,44 @@ function userInputTitle(content) {
32528
32618
  return sessionContentPreview(content, 60);
32529
32619
  }
32530
32620
 
32621
+ // src/storage/session-writer-scrubber.ts
32622
+ function scrubSessionWriterEvent(event, secretScrubber) {
32623
+ const persistMessage = (message) => {
32624
+ const { _estTokens: _ignored, ...persisted } = message;
32625
+ return {
32626
+ ...persisted,
32627
+ content: typeof persisted.content === "string" ? secretScrubber?.scrub(persisted.content) ?? persisted.content : secretScrubber?.scrubObject(persisted.content) ?? persisted.content
32628
+ };
32629
+ };
32630
+ if (event.type === "context_snapshot" || event.type === "messages_replaced") {
32631
+ return { ...event, messages: event.messages.map(persistMessage) };
32632
+ }
32633
+ if (event.type === "message_appended" || event.type === "message_updated") {
32634
+ return { ...event, message: persistMessage(event.message) };
32635
+ }
32636
+ if (!secretScrubber) return event;
32637
+ if (event.type === "user_input") {
32638
+ return {
32639
+ ...event,
32640
+ content: typeof event.content === "string" ? secretScrubber.scrub(event.content) : secretScrubber.scrubObject(event.content)
32641
+ };
32642
+ }
32643
+ if (event.type === "llm_response") {
32644
+ return { ...event, content: secretScrubber.scrubObject(event.content) };
32645
+ }
32646
+ if (event.type === "file_snapshot") {
32647
+ return {
32648
+ ...event,
32649
+ files: event.files.map((f) => ({
32650
+ ...f,
32651
+ before: f.before !== null ? secretScrubber.scrub(f.before) : null,
32652
+ after: f.after !== null ? secretScrubber.scrub(f.after) : null
32653
+ }))
32654
+ };
32655
+ }
32656
+ return event;
32657
+ }
32658
+
32531
32659
  // src/storage/session-writer-truncate.ts
32532
32660
  import * as fsp11 from "node:fs/promises";
32533
32661
  var CHUNK_SIZE = 65536;
@@ -32672,45 +32800,11 @@ async function rewriteSessionToCheckpoint(filePath, checkpointByteOffset) {
32672
32800
  }
32673
32801
  }
32674
32802
 
32675
- // src/storage/session-writer-scrubber.ts
32676
- function scrubSessionWriterEvent(event, secretScrubber) {
32677
- const persistMessage = (message) => {
32678
- const { _estTokens: _ignored, ...persisted } = message;
32679
- return {
32680
- ...persisted,
32681
- content: typeof persisted.content === "string" ? secretScrubber?.scrub(persisted.content) ?? persisted.content : secretScrubber?.scrubObject(persisted.content) ?? persisted.content
32682
- };
32683
- };
32684
- if (event.type === "context_snapshot" || event.type === "messages_replaced") {
32685
- return { ...event, messages: event.messages.map(persistMessage) };
32686
- }
32687
- if (event.type === "message_appended" || event.type === "message_updated") {
32688
- return { ...event, message: persistMessage(event.message) };
32689
- }
32690
- if (!secretScrubber) return event;
32691
- if (event.type === "user_input") {
32692
- return {
32693
- ...event,
32694
- content: typeof event.content === "string" ? secretScrubber.scrub(event.content) : secretScrubber.scrubObject(event.content)
32695
- };
32696
- }
32697
- if (event.type === "llm_response") {
32698
- return { ...event, content: secretScrubber.scrubObject(event.content) };
32699
- }
32700
- if (event.type === "file_snapshot") {
32701
- return {
32702
- ...event,
32703
- files: event.files.map((f) => ({
32704
- ...f,
32705
- before: f.before !== null ? secretScrubber.scrub(f.before) : null,
32706
- after: f.after !== null ? secretScrubber.scrub(f.after) : null
32707
- }))
32708
- };
32709
- }
32710
- return event;
32711
- }
32712
-
32713
32803
  // src/storage/file-session-writer.ts
32804
+ function isClosedHandleError(err) {
32805
+ const code = err?.code;
32806
+ return code === "EBADF" || code === "ERR_CLOSED_RESOURCE" || code === "ERR_INVALID_HANDLE";
32807
+ }
32714
32808
  var FileSessionWriter = class _FileSessionWriter {
32715
32809
  constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
32716
32810
  this.id = id;
@@ -32882,8 +32976,7 @@ var FileSessionWriter = class _FileSessionWriter {
32882
32976
  try {
32883
32977
  return await this.handle.appendFile(data, "utf8");
32884
32978
  } catch (err) {
32885
- const nodeErr = err;
32886
- if (nodeErr?.code === "EBADF") {
32979
+ if (isClosedHandleError(err)) {
32887
32980
  this.handle = await fsp12.open(this.filePath, "a", 384);
32888
32981
  return await this.handle.appendFile(data, "utf8");
32889
32982
  }
@@ -32923,8 +33016,8 @@ var FileSessionWriter = class _FileSessionWriter {
32923
33016
  bufferSynchronousEvent(event) {
32924
33017
  if (this.closed) return;
32925
33018
  void this.ensureInit();
32926
- this.observeForSummary(event);
32927
- const appendEvent = event.type === "file_snapshot" ? scrubSessionWriterEvent(event, this.secretScrubber) : event;
33019
+ const appendEvent = scrubSessionWriterEvent(event, this.secretScrubber);
33020
+ this.observeForSummary(appendEvent);
32928
33021
  try {
32929
33022
  this._onAppend?.(appendEvent);
32930
33023
  } catch {
@@ -33077,8 +33170,7 @@ var FileSessionWriter = class _FileSessionWriter {
33077
33170
  try {
33078
33171
  await this.handle.datasync();
33079
33172
  } catch (err) {
33080
- const nodeErr = err;
33081
- if (nodeErr?.code === "EBADF") {
33173
+ if (isClosedHandleError(err)) {
33082
33174
  this.handle = await fsp12.open(this.filePath, "a", 384);
33083
33175
  return;
33084
33176
  }
@@ -33315,6 +33407,7 @@ var FileSessionWriter = class _FileSessionWriter {
33315
33407
  return this.closePromise;
33316
33408
  }
33317
33409
  async doClose() {
33410
+ await this.ensureInit();
33318
33411
  if (this.pendingFileSnapshots.length > 0) {
33319
33412
  await this.writeFileSnapshot(this.activePromptIndex ?? 0, [...this.pendingFileSnapshots]);
33320
33413
  this.pendingFileSnapshots = [];
@@ -33330,8 +33423,7 @@ var FileSessionWriter = class _FileSessionWriter {
33330
33423
  try {
33331
33424
  await this.handle.datasync();
33332
33425
  } catch (err) {
33333
- const nodeErr = err;
33334
- if (nodeErr?.code !== "EBADF") throw err;
33426
+ if (!isClosedHandleError(err)) throw err;
33335
33427
  }
33336
33428
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
33337
33429
  const observedActivityMs = Date.parse(this.lastActivityAt);
@@ -35033,9 +35125,40 @@ async function readOrBuildShardManifestEntry(opts) {
35033
35125
  return entry;
35034
35126
  }
35035
35127
 
35036
- // src/storage/session-store/summary-builder.ts
35128
+ // src/storage/session-store/strict-empty-check.ts
35037
35129
  import { createReadStream as createReadStream7 } from "node:fs";
35038
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";
35039
35162
  async function summarizeSessionFile(opts) {
35040
35163
  return summarizeSessionEventSequence({
35041
35164
  id: opts.id,
@@ -35152,8 +35275,8 @@ async function summarizeSessionEventSequence(opts) {
35152
35275
  }
35153
35276
  }
35154
35277
  async function* iterateSessionEvents(file, secretScrubber) {
35155
- const stream = createReadStream7(file, { encoding: "utf8" });
35156
- const lines = createInterface7({ input: stream, crlfDelay: Infinity });
35278
+ const stream = createReadStream8(file, { encoding: "utf8" });
35279
+ const lines = createInterface8({ input: stream, crlfDelay: Infinity });
35157
35280
  try {
35158
35281
  for await (const line of lines) {
35159
35282
  if (!line.trim()) continue;
@@ -36049,6 +36172,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
36049
36172
  await deleteSessionArtifacts({ rootDir: this.dir, id, jsonlPath });
36050
36173
  await this.writeTombstone(id);
36051
36174
  }
36175
+ async isEmpty(id) {
36176
+ const canonicalId = await this.resolveId(id);
36177
+ return isStrictlyEmptySessionFile(this.sessionPath(canonicalId, ".jsonl"));
36178
+ }
36052
36179
  async delete(id) {
36053
36180
  if (this.catalogClient) {
36054
36181
  const canonical = await this.resolveId(id);
@@ -36703,6 +36830,12 @@ function normalizeModelRef(ref, defaultProvider) {
36703
36830
  function hasText(value) {
36704
36831
  return typeof value === "string" && value.trim().length > 0;
36705
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
+ }
36706
36839
  function providerHasKey(entry) {
36707
36840
  if (!entry) return false;
36708
36841
  if (hasText(entry.apiKey)) return true;
@@ -36713,7 +36846,7 @@ function providerHasKey(entry) {
36713
36846
  }
36714
36847
  function visibleProviderModels(config, providerId, providerModels) {
36715
36848
  const entry = config.providers?.[providerId];
36716
- return entry?.models !== void 0 ? [...entry.models] : providerModels;
36849
+ return Array.isArray(entry?.models) ? [...entry.models] : providerModels;
36717
36850
  }
36718
36851
  function buildProfiles(config) {
36719
36852
  const entries = /* @__PURE__ */ new Map();
@@ -36750,13 +36883,34 @@ var FallbackProfileManager = class {
36750
36883
  listProfiles() {
36751
36884
  return Object.freeze([...this.profiles.keys()]);
36752
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
+ }
36753
36900
  // ── Resolution ─────────────────────────────────────────────────────────
36754
36901
  /**
36755
36902
  * Resolve a named fallback profile to a validated, provider-filtered chain.
36756
36903
  *
36757
- * Returns an empty chain when:
36758
- * - The profile doesn't exist.
36759
- * - 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.
36760
36914
  *
36761
36915
  * @param name - Profile name from config.fallbackProfiles.
36762
36916
  * @param defaultProvider - Used when an entry has no explicit provider.
@@ -36777,13 +36931,9 @@ var FallbackProfileManager = class {
36777
36931
  if (seen.has(key)) continue;
36778
36932
  seen.add(key);
36779
36933
  if (excludeKey && key === excludeKey) continue;
36780
- const health = this.checkProvider(providerId);
36781
- if (!health.usable) continue;
36782
36934
  if (this.statusTracker && !this.statusTracker.isAvailable(providerId, parsed.model)) continue;
36783
36935
  if (!evaluateModelCalendar(this.config.modelAvailabilitySchedule, providerId, parsed.model).allowed)
36784
36936
  continue;
36785
- const allowedModels = this.config.providers?.[providerId]?.models;
36786
- if (allowedModels && !allowedModels.includes(parsed.model)) continue;
36787
36937
  resolved.push({
36788
36938
  providerId,
36789
36939
  model: parsed.model,
@@ -36801,12 +36951,14 @@ var FallbackProfileManager = class {
36801
36951
  resolveEffective(opts = {}) {
36802
36952
  const bridge = this.resolveBridge(opts.exclude);
36803
36953
  let selected = FREEZER_EMPTY;
36804
- if (opts.fallbackModels && opts.fallbackModels.length > 0) {
36805
- 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);
36806
36958
  if (resolved.length > 0) selected = resolved;
36807
36959
  }
36808
- if (selected.length === 0 && opts.fallbackProfile) {
36809
- const resolved = this.resolve(opts.fallbackProfile, { exclude: opts.exclude });
36960
+ if (selected.length === 0 && profileName) {
36961
+ const resolved = this.resolve(profileName, { exclude: opts.exclude });
36810
36962
  if (resolved.length > 0) selected = resolved;
36811
36963
  }
36812
36964
  if (selected.length === 0 && opts.fallbackAuto !== false) {
@@ -36882,13 +37034,14 @@ var FallbackProfileManager = class {
36882
37034
  };
36883
37035
  const configFallbackAuto = this.config.fallbackAuto;
36884
37036
  const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
36885
- 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();
36886
37039
  const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
36887
- 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;
36888
37041
  const fromExplicitSource = explicitUsable || profileUsable;
36889
- 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({
36890
37043
  fallbackModels: explicitRefs,
36891
- fallbackProfile: opts.fallbackProfile,
37044
+ fallbackProfile: profileName,
36892
37045
  fallbackAuto: effectiveFallbackAuto,
36893
37046
  exclude: current
36894
37047
  });
@@ -36905,7 +37058,7 @@ var FallbackProfileManager = class {
36905
37058
  });
36906
37059
  }
36907
37060
  candidates.push(...selectedChain);
36908
- if (!fromExplicitSource && effectiveFallbackAuto && opts.fallbackProfile !== "default") {
37061
+ if (!fromExplicitSource && effectiveFallbackAuto && profileName !== "default") {
36909
37062
  candidates.push(...this.resolve("default", { exclude: current }));
36910
37063
  }
36911
37064
  if (!fromExplicitSource && effectiveFallbackAuto) {
@@ -36983,7 +37136,7 @@ var FallbackProfileManager = class {
36983
37136
  const leaderModel = this.config.model;
36984
37137
  const providers = this.config.providers ?? {};
36985
37138
  const favoriteSet = new Set(
36986
- (this.config.favoriteModels ?? []).map((ref) => {
37139
+ (asRefList(this.config.favoriteModels) ?? []).map((ref) => {
36987
37140
  const p = parseModelRef(ref);
36988
37141
  return `${p.provider ?? leaderProvider}/${p.model}`;
36989
37142
  })
@@ -37094,9 +37247,15 @@ function effectiveFallbackChain(config) {
37094
37247
  const mgr = new FallbackProfileManager(config);
37095
37248
  return mgr.resolveEffective({
37096
37249
  fallbackModels: config.fallbackModels,
37250
+ fallbackProfile: config.fallbackProfile,
37097
37251
  fallbackAuto: config.fallbackAuto
37098
37252
  }).map((e) => `${e.providerId}/${e.model}`);
37099
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
+ }
37100
37259
  var DEFAULT_PRIMARY_COOLDOWN_MS = 6e4;
37101
37260
  var DEFAULT_PRIMARY_COOLDOWN_MAX_MS = 10 * 6e4;
37102
37261
  var DEFAULT_PRIMARY_RECOVERY_SUCCESSES = 2;
@@ -37136,7 +37295,11 @@ function createFallbackModelExtension(deps) {
37136
37295
  let blockedPrimary;
37137
37296
  let primaryBlockedUntil = 0;
37138
37297
  const now = () => deps.now?.() ?? Date.now();
37139
- 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
+ );
37140
37303
  const cooldownMax = () => Math.max(cooldownBase(), deps.primaryCooldownMaxMs ?? DEFAULT_PRIMARY_COOLDOWN_MAX_MS);
37141
37304
  const selectedPrimary = (cfg) => deps.getPrimaryTarget?.() ?? primaryTarget(cfg);
37142
37305
  const primaryInCooldown = (cfg) => sameTarget(blockedPrimary, selectedPrimary(cfg)) && now() < primaryBlockedUntil;
@@ -37155,7 +37318,7 @@ function createFallbackModelExtension(deps) {
37155
37318
  primaryBlockedUntil = now() + Math.min(cooldownMax(), base * multiplier);
37156
37319
  };
37157
37320
  const recoveryTarget = () => Math.max(1, deps.primaryRecoverySuccesses ?? DEFAULT_PRIMARY_RECOVERY_SUCCESSES);
37158
- const stickyTarget = () => Math.max(0, deps.stickyFallbackTurns ?? 0);
37321
+ const stickyTarget = () => Math.max(0, deps.stickyFallbackTurns ?? liveStickiness()?.stickyFallbackTurns ?? 0);
37159
37322
  const inStickyWindow = () => stickyTurnsElapsed < stickyTarget();
37160
37323
  const onPrimarySuccess = (cfg) => {
37161
37324
  if (!sameTarget(blockedPrimary, selectedPrimary(cfg))) return;
@@ -46891,10 +47054,10 @@ var AdaptiveConcurrencyController = class {
46891
47054
 
46892
47055
  // src/coordination/agent-monitor.ts
46893
47056
  init_file_permissions();
46894
- import { createReadStream as createReadStream8 } from "node:fs";
47057
+ import { createReadStream as createReadStream9 } from "node:fs";
46895
47058
  import * as fs28 from "node:fs/promises";
46896
47059
  import * as path69 from "node:path";
46897
- import { createInterface as createInterface8 } from "node:readline";
47060
+ import { createInterface as createInterface9 } from "node:readline";
46898
47061
  var AgentMonitorService = class _AgentMonitorService {
46899
47062
  _fleetBus;
46900
47063
  _events;
@@ -47001,8 +47164,8 @@ var AgentMonitorService = class _AgentMonitorService {
47001
47164
  const accessible = await fs28.access(file).then(() => true).catch(() => false);
47002
47165
  if (!accessible) return [];
47003
47166
  const out = [];
47004
- const input = createReadStream8(file, { encoding: "utf8" });
47005
- const lines = createInterface8({ input, crlfDelay: Number.POSITIVE_INFINITY });
47167
+ const input = createReadStream9(file, { encoding: "utf8" });
47168
+ const lines = createInterface9({ input, crlfDelay: Number.POSITIVE_INFINITY });
47006
47169
  try {
47007
47170
  for await (const line of lines) {
47008
47171
  const trimmed = line.trim();
@@ -56651,6 +56814,12 @@ function findExchangeStart(messages, userIndex) {
56651
56814
 
56652
56815
  // src/execution/auto-compaction-middleware.ts
56653
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
+ }
56654
56823
  var MAX_DIGEST_LOG_CHARS = 4e3;
56655
56824
  function truncateDigest(digest2) {
56656
56825
  if (digest2.length <= MAX_DIGEST_LOG_CHARS) return digest2;
@@ -56696,8 +56865,19 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
56696
56865
  * 1 / 2.5 = 0.4.
56697
56866
  */
56698
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;
56699
56877
  /** Tracks the most recent no-op attempt so we can avoid re-firing per turn. */
56700
56878
  lastNoopAttempt = null;
56879
+ /** Context size at the last hygiene pass; anchors the growth interval. */
56880
+ lastHygieneTokens = null;
56701
56881
  /**
56702
56882
  * Cached token estimate from the last handler() invocation. When the
56703
56883
  * message count and tool count haven't changed since the last estimate
@@ -56757,55 +56937,9 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
56757
56937
  handler() {
56758
56938
  return async (ctx, next) => {
56759
56939
  if (!this._enabled) return next(ctx);
56760
- const rawHygiene = eliseAcknowledgedToolResults(ctx.messages, {
56761
- maxRetainedTokens: this.resolveToolResultRetention(ctx)
56762
- });
56763
- const receiptHygiene = collapseAcknowledgedToolReceipts(rawHygiene.messages, {
56764
- maxPairs: this.resolveToolReceiptRetention(ctx)
56765
- });
56766
- if (rawHygiene.changed || receiptHygiene.changed) {
56767
- ctx.state.replaceMessages(receiptHygiene.messages);
56768
- ctx.clearFileTracking();
56769
- this.invalidateTokenCaches(ctx);
56770
- }
56771
- const msgCount = ctx.messages.length;
56772
- const toolCount = (ctx.tools ?? []).length;
56773
- const revision = ctx.state?.revision ?? -1;
56774
- let tokens;
56775
- const anchorAt = typeof ctx.meta?.["realAnchorMsgCount"] === "number" ? ctx.meta["realAnchorMsgCount"] : void 0;
56776
- const anchored = realAnchoredInputTokens(ctx.messages, ctx.lastRealInputTokens, anchorAt);
56777
- if (anchored !== null) {
56778
- tokens = anchored;
56779
- } else if (this._estimator) {
56780
- tokens = this._estimator(ctx);
56781
- } else if (msgCount === this._cachedMsgCount && toolCount === this._cachedToolCount && revision === this._cachedRevision && ctx.systemPrompt === this._cachedSystemRef && ctx.tools === this._cachedToolsRef && this._cachedTokens >= 0) {
56782
- tokens = this._cachedTokens;
56783
- } else if (this.tryStashedTokens(ctx, msgCount, toolCount, revision) !== null) {
56784
- const stashed = this.tryStashedTokens(ctx, msgCount, toolCount, revision);
56785
- const cal = getCalibrationState(`${ctx.provider?.id ?? "unknown"}/${ctx.model}`);
56786
- tokens = cal.calibrated ? Math.round(stashed * Math.min(1.5, Math.max(0.5, cal.ratio))) : stashed;
56787
- this._cachedTokens = tokens;
56788
- this._cachedMsgCount = msgCount;
56789
- this._cachedToolCount = toolCount;
56790
- this._cachedRevision = revision;
56791
- this._cachedSystemRef = ctx.systemPrompt;
56792
- this._cachedToolsRef = ctx.tools;
56793
- } else {
56794
- tokens = estimateRequestTokensCalibrated(
56795
- ctx.messages,
56796
- ctx.systemPrompt,
56797
- ctx.tools ?? [],
56798
- `${ctx.provider?.id ?? "unknown"}/${ctx.model}`
56799
- ).total;
56800
- this._cachedTokens = tokens;
56801
- this._cachedMsgCount = msgCount;
56802
- this._cachedToolCount = toolCount;
56803
- this._cachedRevision = revision;
56804
- this._cachedSystemRef = ctx.systemPrompt;
56805
- this._cachedToolsRef = ctx.tools;
56806
- }
56940
+ let tokens = this.estimateContextTokens(ctx);
56807
56941
  const runtimeMaxContext = effectiveMaxContext(ctx, this._maxContext);
56808
- const budget = computeContextWindowBudget(ctx, tokens, runtimeMaxContext);
56942
+ let budget = computeContextWindowBudget(ctx, tokens, runtimeMaxContext);
56809
56943
  const calibratedLoad = budget.load;
56810
56944
  const policy = this.policyProvider?.(ctx);
56811
56945
  const thresholds = policy?.thresholds ?? {
@@ -56819,22 +56953,27 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
56819
56953
  });
56820
56954
  const aggressiveOn = policy?.aggressiveOn ?? this.aggressiveOn;
56821
56955
  const targetLoad = normalizeTargetLoad(policy?.targetLoad, adaptiveThresholds);
56822
- let load = calibratedLoad;
56823
- if (calibratedLoad >= _AutoCompactionMiddleware.GUARD_GATE_LOAD) {
56824
- const guardTotal = estimateRequestTokensUpperBound(
56825
- ctx.messages,
56826
- ctx.systemPrompt,
56827
- ctx.tools ?? [],
56828
- `${ctx.provider?.id ?? "unknown"}/${ctx.model}`
56829
- ).total;
56830
- const guardLoad = guardTotal / budget.availableInputTokens;
56831
- if (guardLoad > load) load = guardLoad;
56832
- }
56833
- 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);
56834
56958
  if (!level) {
56835
56959
  this.lastNoopAttempt = null;
56836
56960
  return next(ctx);
56837
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
+ }
56838
56977
  if (this.shouldSkipNoopRetry(level, tokens)) {
56839
56978
  return next(ctx);
56840
56979
  }
@@ -56851,6 +56990,123 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
56851
56990
  return next(ctx);
56852
56991
  };
56853
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
+ }
56854
57110
  /**
56855
57111
  * H1: try to read a pre-computed token total from `ctx.lastRequestTokens`
56856
57112
  * (set by the agent loop's pre-flight or its restash in emitContextPct).
@@ -61663,26 +61919,29 @@ var DefaultProviderRunner = class {
61663
61919
  // src/execution/retry-policy.ts
61664
61920
  import { randomInt } from "node:crypto";
61665
61921
  var MAX_RETRY_AFTER_MS = 6e4;
61922
+ var MODEL_RETRIES = 3;
61666
61923
  var MAX_ATTEMPTS_BY_KIND = {
61667
- 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,
61668
61930
  quota_exhausted: 0,
61669
- stream_hang: 2,
61670
- // proxy-level timeout — retrying 5x wastes ~40s before fallback kicks in
61671
- overloaded: 3,
61672
- server: 3,
61673
- timeout: 2,
61674
- network: 2,
61675
61931
  auth: 0,
61676
61932
  invalid_request: 0,
61677
61933
  context_overflow: 0,
61678
61934
  content_filter: 0,
61679
61935
  unknown: 0
61680
61936
  };
61937
+ var FAILOVER_RETRY_AFTER_MS = 15e3;
61681
61938
  var DefaultRetryPolicy = class {
61682
61939
  shouldRetry(err, attempt) {
61683
61940
  const isProviderErr = err instanceof ProviderError || ProviderError.isProviderError(err);
61684
61941
  if (isProviderErr) {
61685
61942
  if (!err.retryable) return false;
61943
+ const hint = retryAfterMsFromError(err);
61944
+ if (hint !== void 0 && hint >= FAILOVER_RETRY_AFTER_MS) return false;
61686
61945
  return attempt < this.maxAttempts(err);
61687
61946
  }
61688
61947
  const msg = err.message ?? "";
@@ -63237,7 +63496,8 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
63237
63496
  decision: "block",
63238
63497
  reason: "Active card is not implementation-ready: " + readiness.issues.map((issue) => issue.message).join(" | "),
63239
63498
  boardId: board.id,
63240
- taskId: task.id
63499
+ taskId: task.id,
63500
+ readinessIssues: readiness.issues
63241
63501
  };
63242
63502
  }
63243
63503
  if (task.lifecycle?.currentStage !== "running" || task.assignment?.status !== "running") {
@@ -64047,7 +64307,8 @@ ${errorDetails}`,
64047
64307
  type: "tool_result",
64048
64308
  tool_use_id: use.id,
64049
64309
  content: `Tool "${tool.name}" blocked by Kanban boundary. ${boundary.reason ?? ""}`.trim(),
64050
- is_error: true
64310
+ is_error: true,
64311
+ _kanbanBoundary: boundary
64051
64312
  };
64052
64313
  budget = this.budgetForString(result.content, budget);
64053
64314
  return { result, tool, durationMs: Date.now() - start };
@@ -67090,7 +67351,7 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
67090
67351
  };
67091
67352
 
67092
67353
  // src/security/permission-helpers.ts
67093
- import { realpathSync as realpathSync2 } from "node:fs";
67354
+ import { realpathSync as realpathSync3 } from "node:fs";
67094
67355
  import * as path86 from "node:path";
67095
67356
  function matchesTrust(patterns, subject2) {
67096
67357
  return patterns.includes(subject2) || matchAny(patterns, subject2);
@@ -67175,7 +67436,7 @@ function realpathOfNearestExisting(p) {
67175
67436
  const tail = [];
67176
67437
  for (; ; ) {
67177
67438
  try {
67178
- return tail.length === 0 ? realpathSync2(probe2) : path86.join(realpathSync2(probe2), ...tail);
67439
+ return tail.length === 0 ? realpathSync3(probe2) : path86.join(realpathSync3(probe2), ...tail);
67179
67440
  } catch {
67180
67441
  const parent = path86.dirname(probe2);
67181
67442
  if (parent === probe2) return p;
@@ -83344,6 +83605,156 @@ var CLOUD_SYNC_CONTRACT = {
83344
83605
  "extensions.plugins": EXTENSIONS_PLUGINS_TREE
83345
83606
  };
83346
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
+ })();
83347
83758
  var NAMESPACE_SCHEMA_VERSIONS = {
83348
83759
  "core.runtime": 1,
83349
83760
  "ui.preferences": 1,
@@ -83445,7 +83856,7 @@ function mergeAtContract(local, incoming, tree) {
83445
83856
  return base;
83446
83857
  }
83447
83858
  function applyNamespacePayload(config, namespace, payload) {
83448
- const tree = CLOUD_SYNC_CONTRACT[namespace];
83859
+ const tree = INBOUND_CONTRACT[namespace];
83449
83860
  if (!tree || tree === true) return config;
83450
83861
  const next = { ...config };
83451
83862
  for (const [key, incoming] of Object.entries(payload)) {
@@ -89672,8 +90083,8 @@ var SessionRegistry = class {
89672
90083
  if (id !== entry.sessionId) delete registry2[id];
89673
90084
  continue;
89674
90085
  }
89675
- const heartbeatAge = now - new Date(existing.lastHeartbeatAt).getTime();
89676
- if (heartbeatAge > PID_CHECK_AFTER_MS && !pidAlive2(existing.pid)) {
90086
+ const heartbeatAt = Date.parse(existing.lastHeartbeatAt);
90087
+ if (!Number.isFinite(heartbeatAt) || now - heartbeatAt > PID_CHECK_AFTER_MS && !pidAlive2(existing.pid)) {
89677
90088
  delete registry2[id];
89678
90089
  }
89679
90090
  }
@@ -91569,6 +91980,15 @@ async function readProviderSnapshot(configPath, vault, warn) {
91569
91980
  snapshot.fallbackBridge = decrypted.fallbackBridge.trim();
91570
91981
  }
91571
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
+ }
91572
91992
  if (Array.isArray(decrypted.favoriteModels)) snapshot.favoriteModels = decrypted.favoriteModels;
91573
91993
  if (typeof decrypted.favoriteModelsOnly === "boolean")
91574
91994
  snapshot.favoriteModelsOnly = decrypted.favoriteModelsOnly;
@@ -91587,10 +92007,13 @@ function serializeSnapshot(s) {
91587
92007
  fallbackModels: s.fallbackModels ?? null,
91588
92008
  fallbackBridge: s.fallbackBridge ?? null,
91589
92009
  fallbackProfiles: s.fallbackProfiles ?? null,
92010
+ fallbackProfile: s.fallbackProfile ?? null,
91590
92011
  favoriteModels: s.favoriteModels ?? null,
91591
92012
  favoriteModelsOnly: s.favoriteModelsOnly ?? null,
91592
92013
  modelMatrix: s.modelMatrix ?? null,
91593
92014
  fallbackAuto: s.fallbackAuto ?? null,
92015
+ fallbackStickiness: s.fallbackStickiness ?? null,
92016
+ fallbackMaxLastResortCandidates: s.fallbackMaxLastResortCandidates ?? null,
91594
92017
  modelAvailabilitySchedule: s.modelAvailabilitySchedule ?? null
91595
92018
  });
91596
92019
  }
@@ -92076,10 +92499,10 @@ var ReplayLogStore = class _ReplayLogStore {
92076
92499
  };
92077
92500
 
92078
92501
  // src/storage/session-recovery.ts
92079
- import { createReadStream as createReadStream9 } from "node:fs";
92502
+ import { createReadStream as createReadStream10 } from "node:fs";
92080
92503
  import * as fs67 from "node:fs/promises";
92081
92504
  import * as path122 from "node:path";
92082
- import { createInterface as createInterface9 } from "node:readline";
92505
+ import { createInterface as createInterface10 } from "node:readline";
92083
92506
  var SessionRecovery = class _SessionRecovery {
92084
92507
  constructor(dir) {
92085
92508
  this.dir = dir;
@@ -92156,8 +92579,8 @@ var SessionRecovery = class _SessionRecovery {
92156
92579
  let lastCheckpoint = null;
92157
92580
  let latestBoundary = null;
92158
92581
  let sawEvent = false;
92159
- const stream = createReadStream9(fp, { encoding: "utf8" });
92160
- const lines = createInterface9({ input: stream, crlfDelay: Infinity });
92582
+ const stream = createReadStream10(fp, { encoding: "utf8" });
92583
+ const lines = createInterface10({ input: stream, crlfDelay: Infinity });
92161
92584
  try {
92162
92585
  for await (const line of lines) {
92163
92586
  if (!line.trim()) continue;
@@ -92316,10 +92739,10 @@ async function applyRewindToConversation(opts) {
92316
92739
  init_errors();
92317
92740
  init_atomic_write();
92318
92741
  init_error();
92319
- import { createReadStream as createReadStream10 } from "node:fs";
92742
+ import { createReadStream as createReadStream11 } from "node:fs";
92320
92743
  import * as fsp44 from "node:fs/promises";
92321
92744
  import * as path123 from "node:path";
92322
- import { createInterface as createInterface10 } from "node:readline";
92745
+ import { createInterface as createInterface11 } from "node:readline";
92323
92746
  var DefaultSessionRewinder = class {
92324
92747
  constructor(sessionsDir, projectRoot) {
92325
92748
  this.sessionsDir = sessionsDir;
@@ -92331,8 +92754,8 @@ var DefaultSessionRewinder = class {
92331
92754
  return sessionScopedPath(this.sessionsDir, sessionId, ".jsonl");
92332
92755
  }
92333
92756
  async *readEvents(file) {
92334
- const stream = createReadStream10(file, { encoding: "utf8" });
92335
- const lines = createInterface10({ input: stream, crlfDelay: Infinity });
92757
+ const stream = createReadStream11(file, { encoding: "utf8" });
92758
+ const lines = createInterface11({ input: stream, crlfDelay: Infinity });
92336
92759
  try {
92337
92760
  for await (const line of lines) {
92338
92761
  if (!line.trim()) continue;
@@ -93964,6 +94387,30 @@ var PROVIDER_MANAGE_SCHEMA = {
93964
94387
  required: ["action"],
93965
94388
  additionalProperties: false
93966
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
+ }
93967
94414
  function validateProviderBaseUrl(raw) {
93968
94415
  let url;
93969
94416
  try {
@@ -94034,6 +94481,8 @@ ${msg}`,
94034
94481
  const invalid2 = validateProviderBaseUrl(input.baseUrl);
94035
94482
  if (invalid2) return { status: "error", message: invalid2 };
94036
94483
  }
94484
+ const borrowed = rejectBorrowedEnvVars(providers, input.provider, input.envVars);
94485
+ if (borrowed) return { status: "error", message: borrowed };
94037
94486
  const entry = { type: input.type };
94038
94487
  if (input.models) entry.models = input.models;
94039
94488
  if (input.baseUrl) entry.baseUrl = input.baseUrl;
@@ -94067,14 +94516,22 @@ ${msg}`,
94067
94516
  if (input.autoDiscoverModels !== void 0) entry.autoDiscoverModels = input.autoDiscoverModels;
94068
94517
  if (input.apiKey !== void 0) entry.apiKey = input.apiKey || void 0;
94069
94518
  const endpointChanged = input.baseUrl !== void 0 && (entry.baseUrl ?? void 0) !== (previous.baseUrl ?? void 0);
94070
- const keyDropped = endpointChanged && input.apiKey === void 0 && previous.apiKey !== void 0;
94071
- 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 };
94072
94529
  providers[input.provider] = entry;
94073
94530
  await opts.updateConfig((cfg) => {
94074
94531
  cfg.providers = providers;
94075
94532
  });
94076
94533
  const updated = Object.keys({ ...entry }).filter((k) => k !== "apiKey").join(", ");
94077
- 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` : "";
94078
94535
  return { status: "ok", message: `\u2713 Updated ${input.provider}: ${updated}${keyNote}` };
94079
94536
  }
94080
94537
  if (input.action === "remove") {
@@ -97015,6 +97472,7 @@ export {
97015
97472
  runShellHook,
97016
97473
  runWithNetworkTelemetry,
97017
97474
  runWithProcessTelemetry,
97475
+ runtimeFallbackChain,
97018
97476
  safeEmit,
97019
97477
  safeParse,
97020
97478
  safeProfileName,