@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
@@ -4,7 +4,7 @@ export { buildBtwBlock, consumeBtwNotes, pendingBtwCount, setBtwNote, } from './
4
4
  export { Context, type ContextInit, type ProviderMemoryEvidence, type RunOptions, type TodoItem, } from './context.js';
5
5
  export { type ContinuationInput, type ContinuationSource, detectContinueIntent, type ResolvedContinuation, resolveContinuation, } from './continue-intent.js';
6
6
  export { ConversationState, type ReadonlyConversationState, type StateChange, type StateChangeHandler, wrapAsState, } from './conversation-state.js';
7
- export { createFallbackModelExtension, effectiveFallbackChain, type FallbackGateFn, type FallbackModelDeps, fallbackProfileChain, smartDefaultFallbackChain, } from './fallback-model.js';
7
+ export { createFallbackModelExtension, effectiveFallbackChain, type FallbackGateFn, type FallbackModelDeps, fallbackProfileChain, runtimeFallbackChain, smartDefaultFallbackChain, } from './fallback-model.js';
8
8
  export { FallbackProfileManager } from './fallback-profile-manager.js';
9
9
  export { InputBuilder, type InputBuilderEvent, type InputBuilderOptions, } from './input-builder.js';
10
10
  export { type InstructionBundle, type InstructionBundlePaths, loadInstructionBundle, type SystemInstructionVariant, } from './instruction-bundle.js';
@@ -1722,6 +1722,24 @@ import { randomUUID } from "node:crypto";
1722
1722
  import * as v8 from "node:v8";
1723
1723
 
1724
1724
  // src/security/secret-scrubber.ts
1725
+ var JSON_CREDENTIAL_KEY_ANCHORS = [
1726
+ 'Key"',
1727
+ 'key"',
1728
+ 'KEY"',
1729
+ 'token"',
1730
+ 'Token"',
1731
+ 'TOKEN"',
1732
+ 'secret"',
1733
+ 'Secret"',
1734
+ 'SECRET"',
1735
+ 'password"',
1736
+ 'Password"',
1737
+ 'PASSWORD"',
1738
+ 'authorization"',
1739
+ 'Authorization"',
1740
+ 'bearer"',
1741
+ 'Bearer"'
1742
+ ];
1725
1743
  var PATTERNS = [
1726
1744
  // Anchored at the start where possible so partial matches inside larger
1727
1745
  // strings don't trigger false positives.
@@ -1824,6 +1842,30 @@ var PATTERNS = [
1824
1842
  regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
1825
1843
  anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
1826
1844
  },
1845
+ {
1846
+ type: "json_credential_key",
1847
+ // The JSON counterpart to `high_entropy_env`, and the pattern that the
1848
+ // now-deleted `JSON_KEY_ANCHORS` list was written for. Without it those
1849
+ // anchors only widened the cheap pre-scan — `hasCredentialAnchors` said
1850
+ // "this text may hold a secret", every pattern then declined to match, and
1851
+ // the value went out verbatim. `high_entropy_env` cannot cover these: it
1852
+ // requires an UPPERCASE unquoted key (`API_KEY=…`), so `{"apiKey":"…"}`
1853
+ // never matched.
1854
+ //
1855
+ // Tool results are routinely serialised as JSON, and a credential with no
1856
+ // recognisable prefix (Azure, self-hosted gateways, Anthropic/Codex OAuth)
1857
+ // has no other pattern that can catch it — this is the only thing standing
1858
+ // between such a value and the session JSONL, chronicle, HQ broadcast and
1859
+ // the model's own context.
1860
+ //
1861
+ // The key may carry a prefix (`"anthropicApiKey"`), but the credential word
1862
+ // must END the key: `"tokenCount"` and `"maxTokens"` do not match, because
1863
+ // the closing quote has to follow the word immediately.
1864
+ // Value floor of 8 chars keeps enum-ish values (`"authorization":"none"`)
1865
+ // out. Capture groups: 1=key + punctuation, 2=value, 3=closing quote.
1866
+ 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,
1867
+ anchor: JSON_CREDENTIAL_KEY_ANCHORS
1868
+ },
1827
1869
  // ── Ported from packages/plugins credential-patterns.ts (WS-034) ─────────
1828
1870
  // The plugin runtime carried 37 patterns while this scrubber — the one that
1829
1871
  // guards session JSONL, chronicle, HQ broadcast, WebUI events and the auth
@@ -1906,9 +1948,12 @@ var PATTERNS = [
1906
1948
  anchor: "GOCSPX-"
1907
1949
  }
1908
1950
  ];
1909
- var SIMPLE_PATTERNS = PATTERNS.filter((p) => p.type !== "high_entropy_env");
1951
+ var SIMPLE_PATTERNS = PATTERNS.filter(
1952
+ (p) => p.type !== "high_entropy_env" && p.type !== "json_credential_key"
1953
+ );
1910
1954
  var COMBINED_REGEX = new RegExp(SIMPLE_PATTERNS.map((p) => `(${p.regex.source})`).join("|"), "g");
1911
1955
  var HIGH_ENTROPY_REGEX = PATTERNS.find((p) => p.type === "high_entropy_env").regex;
1956
+ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key").regex;
1912
1957
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
1913
1958
  var SCRUB_CHUNK_BYTES = 64 * 1024;
1914
1959
  var SCRUB_OVERLAP_BYTES = 1024;
@@ -1919,20 +1964,7 @@ var PATTERN_ANCHORS = [
1919
1964
  )
1920
1965
  )
1921
1966
  ];
1922
- var JSON_KEY_ANCHORS = [
1923
- '"apiKey"',
1924
- '"api_key"',
1925
- '"token"',
1926
- '"secret"',
1927
- '"password"',
1928
- '"authorization"',
1929
- '"bearer"',
1930
- '"private_key"',
1931
- '"access_token"',
1932
- '"refresh_token"',
1933
- '"client_secret"'
1934
- ];
1935
- var ALL_ANCHORS = [...PATTERN_ANCHORS, ...JSON_KEY_ANCHORS];
1967
+ var ALL_ANCHORS = PATTERN_ANCHORS;
1936
1968
  function hasCredentialAnchors(text) {
1937
1969
  for (const anchor of ALL_ANCHORS) {
1938
1970
  if (text.includes(anchor)) return true;
@@ -1981,6 +2013,9 @@ var DefaultSecretScrubber = class {
1981
2013
  out = out.replace(HIGH_ENTROPY_REGEX, (_match, lead, key, _value) => {
1982
2014
  return `${lead}${key}=[REDACTED:high_entropy_env]`;
1983
2015
  });
2016
+ out = out.replace(JSON_CREDENTIAL_REGEX, (_match, keyPrefix, _value, closingQuote) => {
2017
+ return `${keyPrefix}[REDACTED:json_credential_key]${closingQuote}`;
2018
+ });
1984
2019
  return out;
1985
2020
  }
1986
2021
  /**
@@ -4669,6 +4704,7 @@ function getCalibrationState(calibrationKey = CALIBRATION_GLOBAL_KEY) {
4669
4704
  }
4670
4705
 
4671
4706
  // src/core/context.ts
4707
+ import { realpathSync } from "node:fs";
4672
4708
  import * as path10 from "node:path";
4673
4709
 
4674
4710
  // src/core/conversation-state.ts
@@ -4744,12 +4780,15 @@ var ConversationState = class {
4744
4780
  * cap determines the starting index for the sum but does not gate it.
4745
4781
  */
4746
4782
  overflowCount(arr) {
4747
- let drop = Context.MAX_MESSAGES > 0 ? Math.max(0, arr.length - Context.MAX_MESSAGES) : 0;
4748
- if (Context.MAX_MESSAGE_TOKENS <= 0) return this.protocolSafeDropCount(arr, drop);
4783
+ const contextClass = this.ctx.constructor;
4784
+ const maxMessages = contextClass.MAX_MESSAGES;
4785
+ const maxMessageTokens = contextClass.MAX_MESSAGE_TOKENS;
4786
+ let drop = maxMessages > 0 ? Math.max(0, arr.length - maxMessages) : 0;
4787
+ if (maxMessageTokens <= 0) return this.protocolSafeDropCount(arr, drop);
4749
4788
  let total = 0;
4750
4789
  for (let i = drop; i < arr.length; i++) total += arr[i]?._estTokens ?? 0;
4751
- if (total <= Context.MAX_MESSAGE_TOKENS) return this.protocolSafeDropCount(arr, drop);
4752
- while (drop < arr.length - 1 && total > Context.MAX_MESSAGE_TOKENS) {
4790
+ if (total <= maxMessageTokens) return this.protocolSafeDropCount(arr, drop);
4791
+ while (drop < arr.length - 1 && total > maxMessageTokens) {
4753
4792
  total -= arr[drop]?._estTokens ?? 0;
4754
4793
  drop++;
4755
4794
  }
@@ -5575,6 +5614,19 @@ var Context = class _Context {
5575
5614
  if (rel.startsWith("..") || path10.isAbsolute(rel)) {
5576
5615
  throw new Error(`Working directory "${resolved}" is outside project root "${root}"`);
5577
5616
  }
5617
+ let realTarget = resolved;
5618
+ let realRoot = root;
5619
+ try {
5620
+ realTarget = realpathSync.native(resolved);
5621
+ realRoot = realpathSync.native(root);
5622
+ } catch {
5623
+ }
5624
+ const realRel = path10.relative(realRoot, realTarget);
5625
+ if (realRel.startsWith("..") || path10.isAbsolute(realRel)) {
5626
+ throw new Error(
5627
+ `Working directory "${resolved}" resolves to "${realTarget}", outside project root "${realRoot}"`
5628
+ );
5629
+ }
5578
5630
  }
5579
5631
  const old = this.workingDir;
5580
5632
  this.workingDir = resolved;
@@ -8119,26 +8171,41 @@ function stripNextStepsFromMessage(msg) {
8119
8171
  strippedNextStepsCache.set(msg, clone);
8120
8172
  return clone;
8121
8173
  }
8122
- function composeRequestMessages(history, tail) {
8174
+ function markCacheBoundary(msg) {
8175
+ if (typeof msg.content === "string") return void 0;
8176
+ const blocks = msg.content.slice();
8177
+ const boundary = blocks[blocks.length - 1];
8178
+ if (!boundary || boundary.type !== "text" && boundary.type !== "tool_result") return void 0;
8179
+ blocks[blocks.length - 1] = { ...boundary, cache_control: { type: "ephemeral" } };
8180
+ return { ...msg, content: blocks };
8181
+ }
8182
+ function composeRequestMessages(history, tail, previous) {
8123
8183
  if (history.length === 0) return null;
8124
8184
  const out = history.slice();
8125
8185
  const lastIdx = out.length - 1;
8126
8186
  const last = out[lastIdx];
8187
+ if (previous && previous.index < lastIdx && history[previous.index] === previous.message) {
8188
+ const marked2 = markCacheBoundary(previous.message);
8189
+ if (marked2) out[previous.index] = marked2;
8190
+ }
8127
8191
  const blocks = typeof last.content === "string" ? [{ type: "text", text: last.content }] : last.content.slice();
8128
- const boundary = blocks[blocks.length - 1];
8129
- if (boundary && (boundary.type === "text" || boundary.type === "tool_result")) {
8130
- blocks[blocks.length - 1] = { ...boundary, cache_control: { type: "ephemeral" } };
8192
+ const tailBlock = blocks[blocks.length - 1];
8193
+ const marked = tailBlock && (tailBlock.type === "text" || tailBlock.type === "tool_result");
8194
+ if (marked) {
8195
+ blocks[blocks.length - 1] = { ...tailBlock, cache_control: { type: "ephemeral" } };
8131
8196
  }
8197
+ const boundary = marked ? { message: last, index: lastIdx } : void 0;
8132
8198
  if (tail.length === 0 || last.role !== "user") {
8133
8199
  out[lastIdx] = { ...last, content: blocks };
8134
8200
  if (tail.length > 0) out.push({ role: "user", content: [LIVE_CONTEXT_HEADER, ...tail] });
8135
- return out;
8201
+ return { messages: out, boundary };
8136
8202
  }
8137
8203
  out[lastIdx] = { ...last, content: [...blocks, LIVE_CONTEXT_HEADER, ...tail] };
8138
- return out;
8204
+ return { messages: out, boundary };
8139
8205
  }
8140
8206
  function createAgentResponseHandler(a) {
8141
8207
  const stabilizedPromptEpochs = /* @__PURE__ */ new WeakSet();
8208
+ let previousBoundary;
8142
8209
  function stabilizePromptEpoch() {
8143
8210
  const prompt = a.ctx.systemPrompt;
8144
8211
  if (stabilizedPromptEpochs.has(prompt)) return;
@@ -8179,7 +8246,9 @@ function createAgentResponseHandler(a) {
8179
8246
  ...memoryEvidence
8180
8247
  ].filter((block) => block !== void 0);
8181
8248
  const requestHistory = stripDeliveredNextSteps(a.ctx.messages);
8182
- const composedMessages = composeRequestMessages(requestHistory, liveContextTail);
8249
+ const composed = composeRequestMessages(requestHistory, liveContextTail, previousBoundary);
8250
+ if (composed) previousBoundary = composed.boundary;
8251
+ const composedMessages = composed?.messages ?? null;
8183
8252
  const system = composedMessages ? stableSystem : liveContextTail.length > 0 ? [...stableSystem, ...liveContextTail] : stableSystem;
8184
8253
  await a.ctx.waitForModelTransition();
8185
8254
  const provider = a.ctx.provider;
@@ -9326,6 +9395,12 @@ function normalizeModelRef(ref, defaultProvider) {
9326
9395
  function hasText(value) {
9327
9396
  return typeof value === "string" && value.trim().length > 0;
9328
9397
  }
9398
+ function asRefList(value) {
9399
+ return Array.isArray(value) ? value : void 0;
9400
+ }
9401
+ function asProfileName(value) {
9402
+ return hasText(value) ? value : void 0;
9403
+ }
9329
9404
  function providerHasKey(entry) {
9330
9405
  if (!entry) return false;
9331
9406
  if (hasText(entry.apiKey)) return true;
@@ -9336,7 +9411,7 @@ function providerHasKey(entry) {
9336
9411
  }
9337
9412
  function visibleProviderModels(config, providerId, providerModels) {
9338
9413
  const entry = config.providers?.[providerId];
9339
- return entry?.models !== void 0 ? [...entry.models] : providerModels;
9414
+ return Array.isArray(entry?.models) ? [...entry.models] : providerModels;
9340
9415
  }
9341
9416
  function buildProfiles(config) {
9342
9417
  const entries = /* @__PURE__ */ new Map();
@@ -9373,13 +9448,34 @@ var FallbackProfileManager = class {
9373
9448
  listProfiles() {
9374
9449
  return Object.freeze([...this.profiles.keys()]);
9375
9450
  }
9451
+ /**
9452
+ * The profile the session has selected (`config.fallbackProfile`, set by
9453
+ * `/fallback profile use <name>`), or undefined when none is selected or the
9454
+ * name no longer resolves to a defined profile.
9455
+ *
9456
+ * Consulted by every resolution entry point when the caller does not name a
9457
+ * profile itself. Without this the leader — which passes no profile — could
9458
+ * never use a named profile at all: `config.fallbackProfiles` was reachable
9459
+ * only by copying a chain into `fallbackModels`.
9460
+ */
9461
+ activeProfileName() {
9462
+ const name = asProfileName(this.config.fallbackProfile);
9463
+ return name && this.profiles.has(name) ? name : void 0;
9464
+ }
9376
9465
  // ── Resolution ─────────────────────────────────────────────────────────
9377
9466
  /**
9378
9467
  * Resolve a named fallback profile to a validated, provider-filtered chain.
9379
9468
  *
9380
- * Returns an empty chain when:
9381
- * - The profile doesn't exist.
9382
- * - Every entry's provider is missing, has no key, or has no matching model.
9469
+ * Returns an empty chain when the profile doesn't exist, or when every entry
9470
+ * is excluded, quarantined, or blacked out.
9471
+ *
9472
+ * Filtering is intentionally identical to {@link resolveRefs} (the explicit
9473
+ * `fallbackModels` path): the self-exclusion, the runtime status tracker,
9474
+ * and the availability calendar — nothing else. Anything a named profile
9475
+ * drops, an explicit chain drops too, and vice versa. Profiles used to apply
9476
+ * two extra filters (provider "usability" and the `providers[].models`
9477
+ * snapshot) that the explicit path did not, which silently rerouted roles
9478
+ * pinned to a profile onto a different model than the one configured.
9383
9479
  *
9384
9480
  * @param name - Profile name from config.fallbackProfiles.
9385
9481
  * @param defaultProvider - Used when an entry has no explicit provider.
@@ -9400,13 +9496,9 @@ var FallbackProfileManager = class {
9400
9496
  if (seen.has(key)) continue;
9401
9497
  seen.add(key);
9402
9498
  if (excludeKey && key === excludeKey) continue;
9403
- const health = this.checkProvider(providerId);
9404
- if (!health.usable) continue;
9405
9499
  if (this.statusTracker && !this.statusTracker.isAvailable(providerId, parsed.model)) continue;
9406
9500
  if (!evaluateModelCalendar(this.config.modelAvailabilitySchedule, providerId, parsed.model).allowed)
9407
9501
  continue;
9408
- const allowedModels = this.config.providers?.[providerId]?.models;
9409
- if (allowedModels && !allowedModels.includes(parsed.model)) continue;
9410
9502
  resolved.push({
9411
9503
  providerId,
9412
9504
  model: parsed.model,
@@ -9424,12 +9516,14 @@ var FallbackProfileManager = class {
9424
9516
  resolveEffective(opts = {}) {
9425
9517
  const bridge = this.resolveBridge(opts.exclude);
9426
9518
  let selected = FREEZER_EMPTY;
9427
- if (opts.fallbackModels && opts.fallbackModels.length > 0) {
9428
- const resolved = this.resolveRefs(opts.fallbackModels, opts.exclude);
9519
+ const explicitRefs = asRefList(opts.fallbackModels);
9520
+ const profileName = asProfileName(opts.fallbackProfile) ?? this.activeProfileName();
9521
+ if (explicitRefs && explicitRefs.length > 0) {
9522
+ const resolved = this.resolveRefs(explicitRefs, opts.exclude);
9429
9523
  if (resolved.length > 0) selected = resolved;
9430
9524
  }
9431
- if (selected.length === 0 && opts.fallbackProfile) {
9432
- const resolved = this.resolve(opts.fallbackProfile, { exclude: opts.exclude });
9525
+ if (selected.length === 0 && profileName) {
9526
+ const resolved = this.resolve(profileName, { exclude: opts.exclude });
9433
9527
  if (resolved.length > 0) selected = resolved;
9434
9528
  }
9435
9529
  if (selected.length === 0 && opts.fallbackAuto !== false) {
@@ -9505,13 +9599,14 @@ var FallbackProfileManager = class {
9505
9599
  };
9506
9600
  const configFallbackAuto = this.config.fallbackAuto;
9507
9601
  const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
9508
- const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
9602
+ const explicitRefs = asRefList(opts.fallbackModels) ?? asRefList(this.config.fallbackModels);
9603
+ const profileName = asProfileName(opts.fallbackProfile) ?? this.activeProfileName();
9509
9604
  const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
9510
- const profileUsable = opts.fallbackProfile !== void 0 && this.hasProfile(opts.fallbackProfile) && this.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
9605
+ const profileUsable = profileName !== void 0 && this.hasProfile(profileName) && this.resolve(profileName, { exclude: current }).length > 0;
9511
9606
  const fromExplicitSource = explicitUsable || profileUsable;
9512
- const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? this.resolve(opts.fallbackProfile, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
9607
+ const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : profileName ? this.resolve(profileName, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
9513
9608
  fallbackModels: explicitRefs,
9514
- fallbackProfile: opts.fallbackProfile,
9609
+ fallbackProfile: profileName,
9515
9610
  fallbackAuto: effectiveFallbackAuto,
9516
9611
  exclude: current
9517
9612
  });
@@ -9528,7 +9623,7 @@ var FallbackProfileManager = class {
9528
9623
  });
9529
9624
  }
9530
9625
  candidates.push(...selectedChain);
9531
- if (!fromExplicitSource && effectiveFallbackAuto && opts.fallbackProfile !== "default") {
9626
+ if (!fromExplicitSource && effectiveFallbackAuto && profileName !== "default") {
9532
9627
  candidates.push(...this.resolve("default", { exclude: current }));
9533
9628
  }
9534
9629
  if (!fromExplicitSource && effectiveFallbackAuto) {
@@ -9606,7 +9701,7 @@ var FallbackProfileManager = class {
9606
9701
  const leaderModel = this.config.model;
9607
9702
  const providers = this.config.providers ?? {};
9608
9703
  const favoriteSet = new Set(
9609
- (this.config.favoriteModels ?? []).map((ref) => {
9704
+ (asRefList(this.config.favoriteModels) ?? []).map((ref) => {
9610
9705
  const p = parseModelRef(ref);
9611
9706
  return `${p.provider ?? leaderProvider}/${p.model}`;
9612
9707
  })
@@ -9717,9 +9812,15 @@ function effectiveFallbackChain(config) {
9717
9812
  const mgr = new FallbackProfileManager(config);
9718
9813
  return mgr.resolveEffective({
9719
9814
  fallbackModels: config.fallbackModels,
9815
+ fallbackProfile: config.fallbackProfile,
9720
9816
  fallbackAuto: config.fallbackAuto
9721
9817
  }).map((e) => `${e.providerId}/${e.model}`);
9722
9818
  }
9819
+ function runtimeFallbackChain(config) {
9820
+ const mgr = new FallbackProfileManager(config);
9821
+ const current = primaryTarget(config);
9822
+ return mgr.resolveCandidates(current, {}).map((e) => `${e.providerId}/${e.model}`);
9823
+ }
9723
9824
  var DEFAULT_PRIMARY_COOLDOWN_MS = 6e4;
9724
9825
  var DEFAULT_PRIMARY_COOLDOWN_MAX_MS = 10 * 6e4;
9725
9826
  var DEFAULT_PRIMARY_RECOVERY_SUCCESSES = 2;
@@ -9759,7 +9860,11 @@ function createFallbackModelExtension(deps) {
9759
9860
  let blockedPrimary;
9760
9861
  let primaryBlockedUntil = 0;
9761
9862
  const now = () => deps.now?.() ?? Date.now();
9762
- const cooldownBase = () => Math.max(0, deps.primaryCooldownMs ?? DEFAULT_PRIMARY_COOLDOWN_MS);
9863
+ const liveStickiness = () => deps.getConfig().fallbackStickiness;
9864
+ const cooldownBase = () => Math.max(
9865
+ 0,
9866
+ deps.primaryCooldownMs ?? liveStickiness()?.primaryProbeInterval ?? DEFAULT_PRIMARY_COOLDOWN_MS
9867
+ );
9763
9868
  const cooldownMax = () => Math.max(cooldownBase(), deps.primaryCooldownMaxMs ?? DEFAULT_PRIMARY_COOLDOWN_MAX_MS);
9764
9869
  const selectedPrimary = (cfg) => deps.getPrimaryTarget?.() ?? primaryTarget(cfg);
9765
9870
  const primaryInCooldown = (cfg) => sameTarget(blockedPrimary, selectedPrimary(cfg)) && now() < primaryBlockedUntil;
@@ -9778,7 +9883,7 @@ function createFallbackModelExtension(deps) {
9778
9883
  primaryBlockedUntil = now() + Math.min(cooldownMax(), base * multiplier);
9779
9884
  };
9780
9885
  const recoveryTarget = () => Math.max(1, deps.primaryRecoverySuccesses ?? DEFAULT_PRIMARY_RECOVERY_SUCCESSES);
9781
- const stickyTarget = () => Math.max(0, deps.stickyFallbackTurns ?? 0);
9886
+ const stickyTarget = () => Math.max(0, deps.stickyFallbackTurns ?? liveStickiness()?.stickyFallbackTurns ?? 0);
9782
9887
  const inStickyWindow = () => stickyTurnsElapsed < stickyTarget();
9783
9888
  const onPrimarySuccess = (cfg) => {
9784
9889
  if (!sameTarget(blockedPrimary, selectedPrimary(cfg))) return;
@@ -11816,6 +11921,7 @@ export {
11816
11921
  renderNextStepsBlock,
11817
11922
  resolveContinuation,
11818
11923
  runProviderWithRetry,
11924
+ runtimeFallbackChain,
11819
11925
  setBtwNote,
11820
11926
  setQueuedMessagesSnapshot,
11821
11927
  smartDefaultFallbackChain,