@wrongstack/core 0.306.3 → 0.306.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2024,6 +2024,12 @@ function normalizeModelRef(ref, defaultProvider) {
2024
2024
  function hasText(value) {
2025
2025
  return typeof value === "string" && value.trim().length > 0;
2026
2026
  }
2027
+ function asRefList(value) {
2028
+ return Array.isArray(value) ? value : void 0;
2029
+ }
2030
+ function asProfileName(value) {
2031
+ return hasText(value) ? value : void 0;
2032
+ }
2027
2033
  function providerHasKey(entry) {
2028
2034
  if (!entry) return false;
2029
2035
  if (hasText(entry.apiKey)) return true;
@@ -2034,7 +2040,7 @@ function providerHasKey(entry) {
2034
2040
  }
2035
2041
  function visibleProviderModels(config, providerId, providerModels) {
2036
2042
  const entry = config.providers?.[providerId];
2037
- return entry?.models !== void 0 ? [...entry.models] : providerModels;
2043
+ return Array.isArray(entry?.models) ? [...entry.models] : providerModels;
2038
2044
  }
2039
2045
  function buildProfiles(config) {
2040
2046
  const entries = /* @__PURE__ */ new Map();
@@ -2071,13 +2077,34 @@ var FallbackProfileManager = class {
2071
2077
  listProfiles() {
2072
2078
  return Object.freeze([...this.profiles.keys()]);
2073
2079
  }
2080
+ /**
2081
+ * The profile the session has selected (`config.fallbackProfile`, set by
2082
+ * `/fallback profile use <name>`), or undefined when none is selected or the
2083
+ * name no longer resolves to a defined profile.
2084
+ *
2085
+ * Consulted by every resolution entry point when the caller does not name a
2086
+ * profile itself. Without this the leader — which passes no profile — could
2087
+ * never use a named profile at all: `config.fallbackProfiles` was reachable
2088
+ * only by copying a chain into `fallbackModels`.
2089
+ */
2090
+ activeProfileName() {
2091
+ const name = asProfileName(this.config.fallbackProfile);
2092
+ return name && this.profiles.has(name) ? name : void 0;
2093
+ }
2074
2094
  // ── Resolution ─────────────────────────────────────────────────────────
2075
2095
  /**
2076
2096
  * Resolve a named fallback profile to a validated, provider-filtered chain.
2077
2097
  *
2078
- * Returns an empty chain when:
2079
- * - The profile doesn't exist.
2080
- * - Every entry's provider is missing, has no key, or has no matching model.
2098
+ * Returns an empty chain when the profile doesn't exist, or when every entry
2099
+ * is excluded, quarantined, or blacked out.
2100
+ *
2101
+ * Filtering is intentionally identical to {@link resolveRefs} (the explicit
2102
+ * `fallbackModels` path): the self-exclusion, the runtime status tracker,
2103
+ * and the availability calendar — nothing else. Anything a named profile
2104
+ * drops, an explicit chain drops too, and vice versa. Profiles used to apply
2105
+ * two extra filters (provider "usability" and the `providers[].models`
2106
+ * snapshot) that the explicit path did not, which silently rerouted roles
2107
+ * pinned to a profile onto a different model than the one configured.
2081
2108
  *
2082
2109
  * @param name - Profile name from config.fallbackProfiles.
2083
2110
  * @param defaultProvider - Used when an entry has no explicit provider.
@@ -2098,13 +2125,9 @@ var FallbackProfileManager = class {
2098
2125
  if (seen.has(key)) continue;
2099
2126
  seen.add(key);
2100
2127
  if (excludeKey && key === excludeKey) continue;
2101
- const health = this.checkProvider(providerId);
2102
- if (!health.usable) continue;
2103
2128
  if (this.statusTracker && !this.statusTracker.isAvailable(providerId, parsed.model)) continue;
2104
2129
  if (!evaluateModelCalendar(this.config.modelAvailabilitySchedule, providerId, parsed.model).allowed)
2105
2130
  continue;
2106
- const allowedModels = this.config.providers?.[providerId]?.models;
2107
- if (allowedModels && !allowedModels.includes(parsed.model)) continue;
2108
2131
  resolved.push({
2109
2132
  providerId,
2110
2133
  model: parsed.model,
@@ -2122,12 +2145,14 @@ var FallbackProfileManager = class {
2122
2145
  resolveEffective(opts = {}) {
2123
2146
  const bridge = this.resolveBridge(opts.exclude);
2124
2147
  let selected = FREEZER_EMPTY;
2125
- if (opts.fallbackModels && opts.fallbackModels.length > 0) {
2126
- const resolved = this.resolveRefs(opts.fallbackModels, opts.exclude);
2148
+ const explicitRefs = asRefList(opts.fallbackModels);
2149
+ const profileName = asProfileName(opts.fallbackProfile) ?? this.activeProfileName();
2150
+ if (explicitRefs && explicitRefs.length > 0) {
2151
+ const resolved = this.resolveRefs(explicitRefs, opts.exclude);
2127
2152
  if (resolved.length > 0) selected = resolved;
2128
2153
  }
2129
- if (selected.length === 0 && opts.fallbackProfile) {
2130
- const resolved = this.resolve(opts.fallbackProfile, { exclude: opts.exclude });
2154
+ if (selected.length === 0 && profileName) {
2155
+ const resolved = this.resolve(profileName, { exclude: opts.exclude });
2131
2156
  if (resolved.length > 0) selected = resolved;
2132
2157
  }
2133
2158
  if (selected.length === 0 && opts.fallbackAuto !== false) {
@@ -2203,13 +2228,14 @@ var FallbackProfileManager = class {
2203
2228
  };
2204
2229
  const configFallbackAuto = this.config.fallbackAuto;
2205
2230
  const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
2206
- const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
2231
+ const explicitRefs = asRefList(opts.fallbackModels) ?? asRefList(this.config.fallbackModels);
2232
+ const profileName = asProfileName(opts.fallbackProfile) ?? this.activeProfileName();
2207
2233
  const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
2208
- const profileUsable = opts.fallbackProfile !== void 0 && this.hasProfile(opts.fallbackProfile) && this.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
2234
+ const profileUsable = profileName !== void 0 && this.hasProfile(profileName) && this.resolve(profileName, { exclude: current }).length > 0;
2209
2235
  const fromExplicitSource = explicitUsable || profileUsable;
2210
- const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? this.resolve(opts.fallbackProfile, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
2236
+ const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : profileName ? this.resolve(profileName, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
2211
2237
  fallbackModels: explicitRefs,
2212
- fallbackProfile: opts.fallbackProfile,
2238
+ fallbackProfile: profileName,
2213
2239
  fallbackAuto: effectiveFallbackAuto,
2214
2240
  exclude: current
2215
2241
  });
@@ -2226,7 +2252,7 @@ var FallbackProfileManager = class {
2226
2252
  });
2227
2253
  }
2228
2254
  candidates.push(...selectedChain);
2229
- if (!fromExplicitSource && effectiveFallbackAuto && opts.fallbackProfile !== "default") {
2255
+ if (!fromExplicitSource && effectiveFallbackAuto && profileName !== "default") {
2230
2256
  candidates.push(...this.resolve("default", { exclude: current }));
2231
2257
  }
2232
2258
  if (!fromExplicitSource && effectiveFallbackAuto) {
@@ -2304,7 +2330,7 @@ var FallbackProfileManager = class {
2304
2330
  const leaderModel = this.config.model;
2305
2331
  const providers = this.config.providers ?? {};
2306
2332
  const favoriteSet = new Set(
2307
- (this.config.favoriteModels ?? []).map((ref) => {
2333
+ (asRefList(this.config.favoriteModels) ?? []).map((ref) => {
2308
2334
  const p = parseModelRef(ref);
2309
2335
  return `${p.provider ?? leaderProvider}/${p.model}`;
2310
2336
  })
@@ -7478,6 +7504,30 @@ var PROVIDER_MANAGE_SCHEMA = {
7478
7504
  required: ["action"],
7479
7505
  additionalProperties: false
7480
7506
  };
7507
+ var CREDENTIAL_SELECTOR_FIELDS = ["apiKey", "apiKeys", "activeKey", "envVars"];
7508
+ function envVarsClaimedByOtherProviders(providers, exceptProvider) {
7509
+ const claimed = /* @__PURE__ */ new Map();
7510
+ for (const [id, entry] of Object.entries(providers)) {
7511
+ if (id === exceptProvider) continue;
7512
+ const names = entry?.["envVars"];
7513
+ if (!Array.isArray(names)) continue;
7514
+ for (const name of names) {
7515
+ if (typeof name === "string" && !claimed.has(name)) claimed.set(name, id);
7516
+ }
7517
+ }
7518
+ return claimed;
7519
+ }
7520
+ function rejectBorrowedEnvVars(providers, provider, requested) {
7521
+ if (!requested) return null;
7522
+ const claimed = envVarsClaimedByOtherProviders(providers, provider);
7523
+ for (const name of requested) {
7524
+ const owner = typeof name === "string" ? claimed.get(name) : void 0;
7525
+ if (owner !== void 0) {
7526
+ 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.`;
7527
+ }
7528
+ }
7529
+ return null;
7530
+ }
7481
7531
  function validateProviderBaseUrl(raw) {
7482
7532
  let url;
7483
7533
  try {
@@ -7548,6 +7598,8 @@ ${msg}`,
7548
7598
  const invalid = validateProviderBaseUrl(input.baseUrl);
7549
7599
  if (invalid) return { status: "error", message: invalid };
7550
7600
  }
7601
+ const borrowed = rejectBorrowedEnvVars(providers, input.provider, input.envVars);
7602
+ if (borrowed) return { status: "error", message: borrowed };
7551
7603
  const entry = { type: input.type };
7552
7604
  if (input.models) entry.models = input.models;
7553
7605
  if (input.baseUrl) entry.baseUrl = input.baseUrl;
@@ -7581,14 +7633,22 @@ ${msg}`,
7581
7633
  if (input.autoDiscoverModels !== void 0) entry.autoDiscoverModels = input.autoDiscoverModels;
7582
7634
  if (input.apiKey !== void 0) entry.apiKey = input.apiKey || void 0;
7583
7635
  const endpointChanged = input.baseUrl !== void 0 && (entry.baseUrl ?? void 0) !== (previous.baseUrl ?? void 0);
7584
- const keyDropped = endpointChanged && input.apiKey === void 0 && previous.apiKey !== void 0;
7585
- if (keyDropped) entry.apiKey = void 0;
7636
+ const explicitlySupplied = /* @__PURE__ */ new Set([
7637
+ ...input.apiKey !== void 0 ? ["apiKey"] : [],
7638
+ ...input.envVars !== void 0 ? ["envVars"] : []
7639
+ ]);
7640
+ const droppedFields = endpointChanged ? CREDENTIAL_SELECTOR_FIELDS.filter(
7641
+ (field) => !explicitlySupplied.has(field) && previous[field] !== void 0
7642
+ ) : [];
7643
+ for (const field of droppedFields) entry[field] = void 0;
7644
+ const borrowed = rejectBorrowedEnvVars(providers, input.provider, input.envVars);
7645
+ if (borrowed) return { status: "error", message: borrowed };
7586
7646
  providers[input.provider] = entry;
7587
7647
  await opts.updateConfig((cfg) => {
7588
7648
  cfg.providers = providers;
7589
7649
  });
7590
7650
  const updated = Object.keys({ ...entry }).filter((k) => k !== "apiKey").join(", ");
7591
- const keyNote = keyDropped ? " \u2014 stored API key cleared because the base URL changed; set it again with provider_key_set" : "";
7651
+ const keyNote = droppedFields.length > 0 ? ` \u2014 cleared ${droppedFields.join(", ")} because the base URL changed; set the key again with provider_key_set` : "";
7592
7652
  return { status: "ok", message: `\u2713 Updated ${input.provider}: ${updated}${keyNote}` };
7593
7653
  }
7594
7654
  if (input.action === "remove") {
@@ -9494,6 +9554,7 @@ export {
9494
9554
  createMcpControlTool,
9495
9555
  createMcpUseTool,
9496
9556
  createOneShotLLMTool,
9497
- createPluginManagerTool
9557
+ createPluginManagerTool,
9558
+ validateProviderBaseUrl
9498
9559
  };
9499
9560
  //# sourceMappingURL=index.js.map
@@ -115,6 +115,20 @@ export interface Config {
115
115
  * model by `/setmodel`, while the whole ordered list is used for failover.
116
116
  */
117
117
  fallbackProfiles?: Record<string, string[]> | undefined;
118
+ /**
119
+ * The named profile from {@link Config.fallbackProfiles} currently selected
120
+ * for failover, set by `/fallback profile use <name>` and cleared by
121
+ * `/fallback profile none`.
122
+ *
123
+ * Resolution order is `fallbackModels` → this profile → smart default, so an
124
+ * explicit chain still wins. Selecting a profile does NOT overwrite
125
+ * `fallbackModels`: before this field existed, `profile use` was implemented
126
+ * by copying the profile's entries over the explicit chain, which destroyed
127
+ * whatever the user had configured and left no way back.
128
+ *
129
+ * A name that no longer matches a defined profile is ignored.
130
+ */
131
+ fallbackProfile?: string | undefined;
118
132
  /**
119
133
  * When `true` (the default) and `fallbackModels` is empty, a fallback chain
120
134
  * is derived automatically from the other keyed providers/models so 429s
@@ -594,6 +594,13 @@ export interface SessionStore {
594
594
  * Returns the refreshed summary. Throws if the session does not exist.
595
595
  */
596
596
  rename(id: string, name: string): Promise<SessionSummary>;
597
+ /**
598
+ * Return true only when the persisted journal is strictly readable and
599
+ * contains lifecycle envelope events but no messages or other session content.
600
+ * Implementations should fail closed (false) for malformed or unknown events.
601
+ * Optional stores that cannot make this guarantee must omit the method.
602
+ */
603
+ isEmpty?(id: string): Promise<boolean>;
597
604
  delete(id: string): Promise<void>;
598
605
  /**
599
606
  * Rewrite the session JSONL file to contain only a fresh session_start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/core",
3
- "version": "0.306.3",
3
+ "version": "0.306.4",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack core: kernel, types, defaults, and shared utilities for the WrongStack CLI agent.",
6
6
  "repository": {
@@ -177,8 +177,8 @@
177
177
  "wrongstackApiVersion": "0.1.10",
178
178
  "dependencies": {
179
179
  "zod": "4.4.3",
180
- "@wrongstack/persistence": "0.306.3",
181
- "@wrongstack/kanban": "0.306.3"
180
+ "@wrongstack/kanban": "0.306.4",
181
+ "@wrongstack/persistence": "0.306.4"
182
182
  },
183
183
  "devDependencies": {
184
184
  "@types/node": "^26.1.2",