@warmdrift/kgauto-compiler 2.0.0-alpha.83 → 2.0.0-alpha.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  LIBRARY_VERSION,
20
20
  createKeyHealthRoute,
21
21
  keyFingerprint
22
- } from "./chunk-QEFIAAE7.mjs";
22
+ } from "./chunk-V4T2CBRI.mjs";
23
23
  import {
24
24
  ABSOLUTE_FLOOR,
25
25
  ARCHETYPE_FLOOR_DEFAULT,
@@ -27,6 +27,7 @@ import {
27
27
  COST_RANKING_REFERENCE_SHAPE,
28
28
  MEASURED_GROUNDING_MIN_N,
29
29
  PROVIDER_ENV_KEYS,
30
+ canonicalPolicySet,
30
31
  chainProviderSpread,
31
32
  configureBrainQuery,
32
33
  createBrainQueryCache,
@@ -52,9 +53,10 @@ import {
52
53
  loadArchetypePerfFromBrain,
53
54
  loadArchetypePerfNFromBrain,
54
55
  loadChainsFromBrain,
56
+ policySetHas,
55
57
  readBrainReadEnv,
56
58
  resolveProviderKey
57
- } from "./chunk-XPD4I3Q5.mjs";
59
+ } from "./chunk-QDWOMQYN.mjs";
58
60
  import {
59
61
  ALIASES,
60
62
  LATENCY_TIER_MS,
@@ -64,8 +66,9 @@ import {
64
66
  getProfile,
65
67
  latencyTierOf,
66
68
  profilesByProvider,
69
+ resolveModelAlias,
67
70
  tryGetProfile
68
- } from "./chunk-VVRDFE6T.mjs";
71
+ } from "./chunk-2MQIVVEU.mjs";
69
72
  import {
70
73
  emitAdvisoryFired,
71
74
  emitCompileDone,
@@ -680,9 +683,12 @@ function passApplyCliffs(ir, profile, estimatedInputTokens) {
680
683
  if (!triggered) continue;
681
684
  switch (cliff.action) {
682
685
  case "drop_to_top_relevant": {
683
- const targetCount = Math.min(
684
- Math.floor(cliff.threshold * 0.75),
685
- Math.max(1, Math.floor((nextIR.tools?.length ?? 0) / 2))
686
+ const targetCount = Math.max(
687
+ 1,
688
+ Math.min(
689
+ Math.floor(cliff.threshold * 0.75),
690
+ Math.floor((nextIR.tools?.length ?? 0) / 2)
691
+ )
686
692
  );
687
693
  if (nextIR.tools && nextIR.tools.length > targetCount) {
688
694
  const intent = nextIR.intent.archetype;
@@ -767,8 +773,8 @@ function effectiveConventions(profile) {
767
773
  function passScoreTargets(ir, opts) {
768
774
  const constraints = ir.constraints ?? {};
769
775
  const policy = opts.policy ?? {};
770
- const blockedSet = new Set(policy.blockedModels ?? []);
771
- const preferredSet = new Set(policy.preferredModels ?? []);
776
+ const blockedSet = canonicalPolicySet(policy.blockedModels);
777
+ const preferredSet = canonicalPolicySet(policy.preferredModels);
772
778
  const scores = [];
773
779
  const policyMutations = [];
774
780
  const rawPromotion = opts.promotion;
@@ -791,7 +797,7 @@ function passScoreTargets(ir, opts) {
791
797
  continue;
792
798
  }
793
799
  const reasons = [];
794
- if (blockedSet.has(modelId)) {
800
+ if (policySetHas(blockedSet, modelId)) {
795
801
  reasons.push(`blocked_by_policy (consumer gated this model \u2014 see CompilePolicy.blockedModels)`);
796
802
  }
797
803
  if (opts.estimatedInputTokens > profile.maxContextTokens * 0.9) {
@@ -821,7 +827,7 @@ function passScoreTargets(ir, opts) {
821
827
  const qualityScore = Math.max(0, baseQuality - qualityPenalty);
822
828
  const callerOrderBoost = (modelIds.length - modelIds.indexOf(modelId)) * 0.1;
823
829
  const costPenalty = estimatedCostUsd * 5;
824
- const preferredBoost = preferredSet.has(modelId) ? 0.5 : 0;
830
+ const preferredBoost = policySetHas(preferredSet, modelId) ? 0.5 : 0;
825
831
  let latencyPenalty = 0;
826
832
  const maxLatencyMs = constraints.maxLatencyMs;
827
833
  if (typeof maxLatencyMs === "number" && maxLatencyMs > 0) {
@@ -878,7 +884,7 @@ function passScoreTargets(ir, opts) {
878
884
  description: `Model ${modelId} excluded \u2014 estimated cost $${estimatedCostUsd.toFixed(4)} exceeds policy ceiling $${policy.maxCostPerCallUsd.toFixed(4)}`
879
885
  });
880
886
  }
881
- if (preferredSet.has(modelId) && reasons.length === 0) {
887
+ if (policySetHas(preferredSet, modelId) && reasons.length === 0) {
882
888
  policyMutations.push({
883
889
  id: `policy-preferred-${modelId}`,
884
890
  source: "compile_policy",
@@ -1892,6 +1898,40 @@ function defaultOnError3(err) {
1892
1898
  err
1893
1899
  );
1894
1900
  }
1901
+ function prefetchPromotions(appId) {
1902
+ const rt = runtime3;
1903
+ if (!rt || !appId) return void 0;
1904
+ let snap = snapshots3.get(appId);
1905
+ if (!snap) {
1906
+ snap = { data: [], expiresAt: 0, refreshing: false };
1907
+ snapshots3.set(appId, snap);
1908
+ }
1909
+ if (snap.expiresAt > Date.now()) return void 0;
1910
+ const inflight = pendingRefreshes3.get(appId);
1911
+ if (inflight) return inflight;
1912
+ if (snap.refreshing) return void 0;
1913
+ snap.refreshing = true;
1914
+ void asyncRefresh3(rt, appId);
1915
+ return pendingRefreshes3.get(appId);
1916
+ }
1917
+ async function awaitPromotionsReady(appId, timeoutMs) {
1918
+ if (!runtime3 || !appId) return;
1919
+ const pending = prefetchPromotions(appId) ?? pendingRefreshes3.get(appId);
1920
+ if (!(timeoutMs > 0)) return;
1921
+ if (!pending) return;
1922
+ let timer;
1923
+ try {
1924
+ await Promise.race([
1925
+ pending,
1926
+ new Promise((resolve) => {
1927
+ timer = setTimeout(resolve, timeoutMs);
1928
+ })
1929
+ ]);
1930
+ } catch {
1931
+ } finally {
1932
+ if (timer) clearTimeout(timer);
1933
+ }
1934
+ }
1895
1935
  function _testResetPromotions() {
1896
1936
  runtime3 = void 0;
1897
1937
  snapshots3.clear();
@@ -2361,6 +2401,92 @@ function advisorRuleCrossFamilyFit(ctx) {
2361
2401
  ];
2362
2402
  }
2363
2403
 
2404
+ // src/advisor-rules/blocked-model-drift.ts
2405
+ var BLOCKED_MODEL_NOT_IN_ROSTER_CODE = "blocked-model-not-in-roster";
2406
+ var BLOCKED_MODEL_FAMILY_SIBLING_SERVED_CODE = "blocked-model-family-sibling-served";
2407
+ var DOCS_URL = "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories";
2408
+ function familyOf2(modelId, profile) {
2409
+ return profile?.family ?? deriveFamilyFromModelId(modelId);
2410
+ }
2411
+ function isOnTheWayOut(status) {
2412
+ return status === "legacy" || status === "deprecated";
2413
+ }
2414
+ function advisorRuleBlockedModelDrift(ctx) {
2415
+ const blocked = ctx.policy?.blockedModels;
2416
+ if (!blocked || blocked.length === 0) return [];
2417
+ const resolve = ctx.resolveProfile ?? tryGetProfile;
2418
+ const out = [];
2419
+ const entries = [...new Set(blocked)].sort();
2420
+ const selectedProfile = resolve(ctx.selectedModelId);
2421
+ const selectedFamily = familyOf2(ctx.selectedModelId, selectedProfile);
2422
+ const orphans = entries.filter((e) => resolve(e) === void 0);
2423
+ if (orphans.length > 0) {
2424
+ const list = orphans.map((o) => `\`${o}\``).join(", ");
2425
+ const plural = orphans.length === 1 ? "entry" : "entries";
2426
+ const verb = orphans.length === 1 ? "matches" : "match";
2427
+ out.push({
2428
+ level: "warn",
2429
+ code: BLOCKED_MODEL_NOT_IN_ROSTER_CODE,
2430
+ message: `CompilePolicy.blockedModels ${plural} ${list} ${verb} no model in the current roster, so ${orphans.length === 1 ? "it is" : "they are"} inert \u2014 \`blockedModels\` is matched by exact model id, and nothing kgauto can select carries ${orphans.length === 1 ? "that id" : "those ids"}. The block will never fire.`,
2431
+ suggestion: `Check for a typo, or for an id that was retired from the roster since the block was written. This is how a block goes quiet without an error: tt-intel (2026-07-29) carried \`KGAUTO_BLOCKED_MODELS="claude-sonnet-4-6"\` across a roster retarget and the block stopped covering the traffic they believed it covered. Resolve the intended model id against the live roster \u2014 \`getRecommendedPrimary({ family, fallback })\` returns the id the family currently resolves to \u2014 and block that id, or drop the entry if it is no longer needed.`,
2432
+ docsUrl: DOCS_URL
2433
+ });
2434
+ }
2435
+ if (selectedFamily !== null) {
2436
+ const siblings = entries.filter((e) => {
2437
+ if (resolveModelAlias(e) === resolveModelAlias(ctx.selectedModelId)) return false;
2438
+ const p = resolve(e);
2439
+ if (familyOf2(e, p) !== selectedFamily) return false;
2440
+ if (p && selectedProfile && p.provider !== selectedProfile.provider) {
2441
+ return false;
2442
+ }
2443
+ return true;
2444
+ });
2445
+ if (siblings.length > 0) {
2446
+ const list = siblings.map((s) => `\`${s}\``).join(", ");
2447
+ const plural = siblings.length === 1 ? "" : "s";
2448
+ const retargetShaped = siblings.some((s) => {
2449
+ const p = resolve(s);
2450
+ return isOnTheWayOut(p?.status) && selectedProfile?.status === "current";
2451
+ });
2452
+ const retargetNote = retargetShaped ? ` The blocked id${plural} ${siblings.length === 1 ? "is" : "are"} legacy/deprecated while \`${ctx.selectedModelId}\` is current \u2014 that is the signature of a roster lifecycle move rather than a deliberate per-generation block, so this is more likely to be drift than intent.` : "";
2453
+ out.push({
2454
+ level: "warn",
2455
+ code: BLOCKED_MODEL_FAMILY_SIBLING_SERVED_CODE,
2456
+ message: `\`${ctx.selectedModelId}\` was selected for this call. It is in the same family (\`${selectedFamily}\`) as blocked entr${siblings.length === 1 ? "y" : "ies"} ${list}, but its exact id differs \u2014 and \`CompilePolicy.blockedModels\` matches by exact id, so the block does not cover it.${retargetNote}`,
2457
+ suggestion: `Two readings, and kgauto cannot tell them apart: (1) intentional \u2014 you meant to gate that specific id and \`${ctx.selectedModelId}\` is fine, in which case nothing needs doing and you can filter this code; (2) drift \u2014 you meant to gate the family, and a roster change moved traffic to a sibling your block never named. This happened to tt-intel: when the \`claude-sonnet\` family primary retargeted from \`claude-sonnet-4-6\` to \`claude-sonnet-5\`, their literal-id gate stopped matching the family's routed traffic (their fix was local family resolution). If you meant the family, add \`${ctx.selectedModelId}\` to \`blockedModels\` \u2014 kgauto deliberately does NOT widen exact-id blocks into family globs, because that would silently change what every existing block covers.`,
2458
+ docsUrl: DOCS_URL
2459
+ });
2460
+ }
2461
+ }
2462
+ return out;
2463
+ }
2464
+
2465
+ // src/advisor-rules/preferred-blocked-overlap.ts
2466
+ var PREFERRED_MODEL_BLOCKED_CODE = "preferred-model-blocked";
2467
+ var DOCS_URL2 = "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories";
2468
+ function advisorRulePreferredBlockedOverlap(ctx) {
2469
+ const preferred = ctx.policy?.preferredModels;
2470
+ const blocked = ctx.policy?.blockedModels;
2471
+ if (!preferred?.length || !blocked?.length) return [];
2472
+ const blockedSet = canonicalPolicySet(blocked);
2473
+ const deadPins = [...new Set(preferred)].filter((p) => policySetHas(blockedSet, p)).sort();
2474
+ if (deadPins.length === 0) return [];
2475
+ const list = deadPins.map((p) => `\`${p}\``).join(", ");
2476
+ const one = deadPins.length === 1;
2477
+ const preferredSet = canonicalPolicySet(preferred);
2478
+ const servedIsPinned = policySetHas(preferredSet, ctx.selectedModelId);
2479
+ return [
2480
+ {
2481
+ level: "warn",
2482
+ code: PREFERRED_MODEL_BLOCKED_CODE,
2483
+ message: `CompilePolicy.preferredModels entr${one ? "y" : "ies"} ${list} ${one ? "is" : "are"} also in \`blockedModels\` (matched canonically, aliases included). A blocked model is hard-rejected before preference boosts apply, so ${one ? "this pin" : "these pins"} can never serve. ` + (servedIsPinned ? `This call was served by \`${ctx.selectedModelId}\`, which is itself a live pin \u2014 the dead entr${one ? "y is" : "ies are"} latent, not currently rerouting traffic.` : `Substitution is your steady state: this call landed on \`${ctx.selectedModelId}\`, which you did not pin.`),
2484
+ suggestion: `Two readings, and kgauto cannot tell them apart: (1) intentional \u2014 your spend gate deliberately outranks the pin, in which case nothing needs doing and you can filter this code; (2) misconfiguration \u2014 the pin and the block were written at different times and the overlap is an accident. tt-intel hit reading (2) on 2026-08-01: a summarize site pinned \`claude-sonnet\` while \`KGAUTO_BLOCKED_MODELS\` carried the same family, and every call silently substituted a reasoning model whose reasoning burn exceeded the site's \`maxOutputTokens\` \u2014 100% empty payloads under HTTP 200. If the block should win, remove the pin so the policy says what it does. If the pin should win, remove ${one ? "the blocking entry" : "the blocking entries"} or re-scope the block to the exact ids you mean. Check what the substitute costs at your input shape while you are here \u2014 a spend gate that lands traffic on a pricier model than the one it blocked is a cost inversion, not a saving.`,
2485
+ docsUrl: DOCS_URL2
2486
+ }
2487
+ ];
2488
+ }
2489
+
2364
2490
  // src/advisor.ts
2365
2491
  var QUALITY_FLOOR_FOR_RECOMMENDATION = 6;
2366
2492
  var TIER_DOWN_COST_RATIO = 0.5;
@@ -2379,6 +2505,18 @@ function runAdvisor(ir, result, profile, policy, phase2) {
2379
2505
  out.push(...detectToolBloat(ir, result));
2380
2506
  out.push(...detectHistoryUncached(ir, profile));
2381
2507
  out.push(...detectSingleModelArray(ir, policy));
2508
+ out.push(
2509
+ ...advisorRuleBlockedModelDrift({
2510
+ policy,
2511
+ selectedModelId: profile.id
2512
+ })
2513
+ );
2514
+ out.push(
2515
+ ...advisorRulePreferredBlockedOverlap({
2516
+ policy,
2517
+ selectedModelId: profile.id
2518
+ })
2519
+ );
2382
2520
  if (policy?.posture !== "locked") {
2383
2521
  out.push(...detectCostMismatchedArchetype(ir, profile, phase2));
2384
2522
  out.push(...detectModelStaleEvidence(ir, profile));
@@ -2490,18 +2628,27 @@ function detectHistoryUncached(ir, profile) {
2490
2628
  function detectSingleModelArray(ir, policy) {
2491
2629
  if (ir.models.length !== 1) return [];
2492
2630
  if (policy?.posture === "locked") return [];
2493
- const only = ir.models[0];
2631
+ const entry = ir.models[0];
2632
+ const only = typeof entry === "string" ? entry : `family:${entry.family}`;
2633
+ const blocked = canonicalPolicySet(policy?.blockedModels);
2634
+ let alternatives = [];
2635
+ try {
2636
+ alternatives = getDefaultFallbackChain({
2637
+ archetype: ir.intent.archetype,
2638
+ primary: only,
2639
+ posture: "preferred",
2640
+ policy
2641
+ }).filter((id) => resolveModelAlias(id) !== resolveModelAlias(only)).filter((id) => !policySetHas(blocked, id)).filter((id) => getModelCompatibility(id, { archetype: ir.intent.archetype }).status !== "reject");
2642
+ } catch {
2643
+ }
2644
+ const hasAlternative = alternatives.length > 0;
2645
+ const remedy = "Widen the chain AND pin your primary: `compile({ ...ir, models: getDefaultFallbackChain({ archetype: ir.intent.archetype, primary: '" + only + "', posture: 'preferred' }) }, { policy: { preferredModels: ['" + only + "'] } })`. The chain is a CANDIDATE SET \u2014 compile() scores it and does not honour its order, so without the `preferredModels` pin the extra entries can retarget your primary (and with it your cost profile). With the pin, `" + only + "` stays primary and the added entries serve only as the safety net. Note `posture` has no effect once `primary` is passed. If you gate models by spend, re-verify `policy.blockedModels` against the widened set before shipping. If single-model is intentional (compliance/brand promise), set `policy.posture = 'locked'` to silence this rule.";
2494
2646
  return [
2495
2647
  {
2496
- level: "warn",
2648
+ level: hasAlternative ? "critical" : "warn",
2497
2649
  code: "single-model-array",
2498
- message: `\`ir.models\` has length 1 (only "${only}") and posture is not 'locked'. A single-model chain has no safety net \u2014 the first 429 / 5xx / cliff hits the user as a failure. Master plan \xA71.2 closes the reliability gap with a 2-step minimum.`,
2499
- // alpha.80: `posture: 'preferred'` keeps the consumer's current model at
2500
- // position 0 and is therefore cost-neutral; `'open'` re-picks the
2501
- // primary and is NOT. This rule is about reliability, not cost, so both
2502
- // stay on offer — but the cost consequence of the second is now stated,
2503
- // since a reliability fix should not silently become a repricing.
2504
- suggestion: "Use `getDefaultFallbackChain({ archetype: ir.intent.archetype, primary: '" + only + "', posture: 'preferred' })` for a user-anchored chain \u2014 this keeps `" + only + "` as your primary and only adds fallbacks, so your cost profile is unchanged. `getDefaultFallbackChain({ archetype, posture: 'open' })` instead lets the library pick the PRIMARY, ordered by archetype performance rather than cost \u2014 check what it returns before adopting it. If single-model is intentional (compliance/brand promise), set `policy.posture = 'locked'` to silence this rule.",
2650
+ message: hasAlternative ? `\`ir.models\` has length 1 (only "${only}") for archetype "${ir.intent.archetype}" and posture is not 'locked'. A single-model chain has no safety net \u2014 the first 429 / 5xx / cliff hits the user as a failure \u2014 and a compatible, non-blocked alternative exists in the roster today (${alternatives[0]}), so this is critical: the missing safety net is adoptable now.` : `\`ir.models\` has length 1 (only "${only}") for archetype "${ir.intent.archetype}" and posture is not 'locked'. A single-model chain has no safety net \u2014 the first 429 / 5xx / cliff hits the user as a failure. No compatible non-blocked alternative is visible in the roster for this archetype, so this stays a warning.`,
2651
+ suggestion: remedy,
2505
2652
  docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#single-model-array"
2506
2653
  }
2507
2654
  ];
@@ -3055,6 +3202,15 @@ function compile(ir, opts = {}) {
3055
3202
  sectionRewritesApplied
3056
3203
  }
3057
3204
  );
3205
+ if (ir["policy"] !== void 0 && opts.policy === void 0) {
3206
+ rawAdvisories.push({
3207
+ level: "critical",
3208
+ code: "policy-in-ir-ignored",
3209
+ message: "The IR passed to compile() carries a `policy` field. `policy` belongs in the SECOND argument \u2014 `compile(ir, { policy })` \u2014 and inside the IR it is an unknown field that is completely ignored. If that policy names blockedModels, no block is being enforced on this call.",
3210
+ suggestion: "Move it: `compile(ir, { policy: { ... } })`. If you also configure policy correctly elsewhere on this path, remove the IR copy so the next reader is not misled.",
3211
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#public-api"
3212
+ });
3213
+ }
3058
3214
  const advisories = rawAdvisories.map((a) => ({
3059
3215
  ...a,
3060
3216
  kgautoRequestId: handle,
@@ -3281,6 +3437,12 @@ function configureBrain(config) {
3281
3437
  fetchImpl: config.fetchImpl ?? fetch,
3282
3438
  onError: config.onError
3283
3439
  });
3440
+ if (config.appId) {
3441
+ try {
3442
+ void prefetchPromotions(config.appId);
3443
+ } catch {
3444
+ }
3445
+ }
3284
3446
  } else {
3285
3447
  configurePromotionsBrain(void 0);
3286
3448
  }
@@ -3652,6 +3814,7 @@ function buildPayload(input, reg) {
3652
3814
  latency_ms: input.latencyMs,
3653
3815
  success: input.success,
3654
3816
  empty_response: input.emptyResponse ?? input.tokensOut === 0,
3817
+ source: input.source,
3655
3818
  error_type: input.errorType,
3656
3819
  tools_called: input.toolsCalled,
3657
3820
  oracle_score: input.oracleScore?.score,
@@ -4750,7 +4913,11 @@ async function call(ir, opts = {}) {
4750
4913
  )
4751
4914
  })
4752
4915
  );
4753
- await awaitMeasuredFailureReady(ir.appId, resolveGateWarmupMs(opts));
4916
+ const warmupMs = resolveGateWarmupMs(opts);
4917
+ await Promise.all([
4918
+ awaitMeasuredFailureReady(ir.appId, warmupMs),
4919
+ awaitPromotionsReady(ir.appId, warmupMs)
4920
+ ]);
4754
4921
  const initial = compileAndRegister(ir, opts);
4755
4922
  safeEmit(
4756
4923
  () => emitCompileDone(traceId, ir.appId, {
@@ -4790,6 +4957,7 @@ async function call(ir, opts = {}) {
4790
4957
  const latencyMs2 = Date.now() - start;
4791
4958
  await record({
4792
4959
  handle: initial.handle,
4960
+ source: opts.source,
4793
4961
  tokensIn: 0,
4794
4962
  tokensOut: 0,
4795
4963
  latencyMs: latencyMs2,
@@ -4824,11 +4992,11 @@ async function call(ir, opts = {}) {
4824
4992
  }
4825
4993
  let policyBlockedFiltered;
4826
4994
  if (opts.policy?.blockedModels && opts.policy.blockedModels.length > 0) {
4827
- const blocked = new Set(opts.policy.blockedModels);
4995
+ const blocked = canonicalPolicySet(opts.policy.blockedModels);
4828
4996
  const filtered = [];
4829
4997
  const dropped = [];
4830
4998
  for (const t of targetsToTry) {
4831
- if (blocked.has(t)) {
4999
+ if (policySetHas(blocked, t)) {
4832
5000
  dropped.push(t);
4833
5001
  } else {
4834
5002
  filtered.push(t);
@@ -4842,6 +5010,7 @@ async function call(ir, opts = {}) {
4842
5010
  const latencyMs2 = Date.now() - start;
4843
5011
  await record({
4844
5012
  handle: initial.handle,
5013
+ source: opts.source,
4845
5014
  tokensIn: 0,
4846
5015
  tokensOut: 0,
4847
5016
  latencyMs: latencyMs2,
@@ -4868,10 +5037,18 @@ async function call(ir, opts = {}) {
4868
5037
  const failedProviders = /* @__PURE__ */ new Map();
4869
5038
  const sameModelRetryEnabled = opts.sameModelRetry ?? isSameModelRetryEnabledFromEnv();
4870
5039
  let retriedSameModel = false;
5040
+ const pushAttempt = (attempt) => {
5041
+ attempts.push(attempt);
5042
+ if (attempt.status === "success") return;
5043
+ try {
5044
+ opts.onFailedAttempt?.(attempt);
5045
+ } catch {
5046
+ }
5047
+ };
4871
5048
  for (let i = 0; i < targetsToTry.length; i++) {
4872
5049
  const targetModel = targetsToTry[i];
4873
5050
  if (opts.abortSignal?.aborted) {
4874
- attempts.push({
5051
+ pushAttempt({
4875
5052
  model: targetModel,
4876
5053
  status: "terminal",
4877
5054
  errorCode: "aborted",
@@ -4882,7 +5059,7 @@ async function call(ir, opts = {}) {
4882
5059
  const targetProfile = tryGetProfile(targetModel);
4883
5060
  const providerFailReason = targetProfile ? failedProviders.get(targetProfile.provider) : void 0;
4884
5061
  if (targetProfile && providerFailReason && !opts.noFallback) {
4885
- attempts.push({
5062
+ pushAttempt({
4886
5063
  model: targetModel,
4887
5064
  status: "terminal",
4888
5065
  errorCode: `${providerFailReason}_inferred`,
@@ -4901,7 +5078,7 @@ async function call(ir, opts = {}) {
4901
5078
  opts
4902
5079
  );
4903
5080
  } catch (err) {
4904
- attempts.push({
5081
+ pushAttempt({
4905
5082
  model: targetModel,
4906
5083
  status: "terminal",
4907
5084
  errorCode: "compile_error",
@@ -4939,7 +5116,7 @@ async function call(ir, opts = {}) {
4939
5116
  }
4940
5117
  if (!validated.ok && isStructuredContractViolation(validated.errorCode) && sameModelRetryEnabled && !retriedSameModel && !retrySuppressionNote) {
4941
5118
  retriedSameModel = true;
4942
- attempts.push({
5119
+ pushAttempt({
4943
5120
  model: targetModel,
4944
5121
  status: validated.errorType,
4945
5122
  errorCode: validated.errorCode,
@@ -4954,7 +5131,7 @@ async function call(ir, opts = {}) {
4954
5131
  servedByRetry = true;
4955
5132
  }
4956
5133
  if (validated.ok) {
4957
- attempts.push({
5134
+ pushAttempt({
4958
5135
  model: targetModel,
4959
5136
  status: "success",
4960
5137
  ...servedByRetry ? { sameModelRetry: true } : {}
@@ -4975,6 +5152,7 @@ async function call(ir, opts = {}) {
4975
5152
  const goldenShapeKey = goldenRate > 0 ? peekRegisteredShapeKey(initial.handle) : void 0;
4976
5153
  await record({
4977
5154
  handle: initial.handle,
5155
+ source: opts.source,
4978
5156
  tokensIn: validated.response.tokens.input,
4979
5157
  tokensOut: validated.response.tokens.output,
4980
5158
  latencyMs: latencyMs2,
@@ -5072,7 +5250,7 @@ async function call(ir, opts = {}) {
5072
5250
  advisories: activeCompile.advisories
5073
5251
  };
5074
5252
  }
5075
- attempts.push({
5253
+ pushAttempt({
5076
5254
  model: targetModel,
5077
5255
  status: validated.errorType,
5078
5256
  errorCode: validated.errorCode,
@@ -5097,6 +5275,7 @@ async function call(ir, opts = {}) {
5097
5275
  const latencyMs = Date.now() - start;
5098
5276
  await record({
5099
5277
  handle: initial.handle,
5278
+ source: opts.source,
5100
5279
  tokensIn: lastErr?.tokens?.input ?? 0,
5101
5280
  tokensOut: lastErr?.tokens?.output ?? 0,
5102
5281
  latencyMs,
@@ -7216,6 +7395,8 @@ export {
7216
7395
  ALT_STRATEGY_IDS,
7217
7396
  ARCHETYPE_FAMILY_FITS,
7218
7397
  ARCHETYPE_FLOOR_DEFAULT,
7398
+ BLOCKED_MODEL_FAMILY_SIBLING_SERVED_CODE,
7399
+ BLOCKED_MODEL_NOT_IN_ROSTER_CODE,
7219
7400
  BRAIN_READ_ENV_NAMES,
7220
7401
  BURST_SPAN_MS,
7221
7402
  COACH_CFG,
@@ -7259,6 +7440,7 @@ export {
7259
7440
  applySectionRewrites,
7260
7441
  attachCacheControlToStreamTextInput,
7261
7442
  awaitMeasuredFailureReady,
7443
+ awaitPromotionsReady,
7262
7444
  brainHealth,
7263
7445
  bucketContext,
7264
7446
  bucketHistory,
@@ -7345,6 +7527,7 @@ export {
7345
7527
  peekBrainDeadLetter,
7346
7528
  planDecomposition,
7347
7529
  prefetchMeasuredFailure,
7530
+ prefetchPromotions,
7348
7531
  probeShadow,
7349
7532
  profileToRow,
7350
7533
  profilesByProvider,
@@ -7357,6 +7540,7 @@ export {
7357
7540
  resetTokenizer,
7358
7541
  resolveConventionsForProfile,
7359
7542
  resolveGoldenCaptureRate,
7543
+ resolveModelAlias,
7360
7544
  resolvePricingAt,
7361
7545
  resolveProviderKey,
7362
7546
  rowToAdvisory,
@@ -305,6 +305,17 @@ interface CompilePolicy {
305
305
  * Model IDs the consumer has gated. Compile() will never select these.
306
306
  * Use for: cost caps, account-level rate limits, "this model is broken
307
307
  * for our workload" decisions.
308
+ *
309
+ * Matching is by EXACT id after alias resolution (alpha.85): an alias
310
+ * and its canonical id are the same model, so blocking either blocks
311
+ * both (`deepseek-chat` ≡ `deepseek-v4-flash`). Matching does NOT widen
312
+ * to family siblings — blocking `deepseek-v4-flash` says nothing about
313
+ * `deepseek-v4-pro` — and a bare family key (`'deepseek'`,
314
+ * `'claude-sonnet'`) is INERT: it matches no model and no warning fires
315
+ * today. To gate a family, list its concrete ids. Beware roster
316
+ * retargets: when a family primary moves to a new id, a literal-id
317
+ * block does not follow it (tt-intel s113 built consumer-side family
318
+ * resolution for exactly this).
308
319
  */
309
320
  blockedModels?: string[];
310
321
  /**
@@ -1073,6 +1084,51 @@ interface ShadowProbeConfig {
1073
1084
  skipSlowTierInSync?: boolean;
1074
1085
  }
1075
1086
  interface CallOptions {
1087
+ /**
1088
+ * alpha.86 — self-mark for synthetic callers (canaries, smokes, eval
1089
+ * replays, probes). Passed through to every outcome row this call
1090
+ * records, including failure rows. OMIT for real traffic. See
1091
+ * {@link OutcomeSource}.
1092
+ */
1093
+ source?: OutcomeSource;
1094
+ /**
1095
+ * alpha.84 — fires once per FAILED attempt during the fallback walk, before
1096
+ * the walk continues. IC-Cairn's filing, 2026-07-29.
1097
+ *
1098
+ * ## The gap this closes
1099
+ *
1100
+ * `call()` records exactly ONE outcome row per call. An attempt that fails
1101
+ * and then walks to a successful fallback survives only as
1102
+ * `fellOverFrom`/`fallbackReason` metadata ON the success row — so a
1103
+ * `call()`-only consumer's corpus contains **no failure rows at all**, by
1104
+ * construction, no matter how carefully they wired `record()`.
1105
+ *
1106
+ * That is not hypothetical. kgauto's own `corpus-implausible-success`
1107
+ * detector fired on inspire-central and told them to "wire record() on the
1108
+ * FAILURE path" — which they had already done, in five places. Measured:
1109
+ * 89 rows, 0 failure rows, 2 fellover-bearing. **The detector demanded
1110
+ * output the API could not produce.** Streaming consumers have had
1111
+ * `onFailedAttempt` since alpha.48 via `streamWithFallover`; `call()`
1112
+ * consumers had nothing. This restores parity.
1113
+ *
1114
+ * ## Contract
1115
+ *
1116
+ * Fires for every attempt whose `status` is `'retryable'` or `'terminal'`,
1117
+ * including skipped ones (`*_inferred`, `aborted`) — a skip is a real
1118
+ * datum about the chain, not an absence. Never fires for `'success'`.
1119
+ *
1120
+ * Called synchronously and **its throw is swallowed**: a consumer's
1121
+ * telemetry bug must never take down the call it is observing. It is a
1122
+ * notification, not a hook that can veto or alter the walk.
1123
+ *
1124
+ * Typical use — emit a failure row per attempt so the corpus can contain
1125
+ * failures at all:
1126
+ *
1127
+ * call(ir, { onFailedAttempt: (a) => void recordOutcome({
1128
+ * ...base, success: false, errorType: a.errorCode, model: a.model,
1129
+ * }) })
1130
+ */
1131
+ onFailedAttempt?: (attempt: CallAttempt) => void;
1076
1132
  /** Forwarded to compile(). */
1077
1133
  policy?: CompilePolicy;
1078
1134
  /**
@@ -1409,9 +1465,34 @@ interface OracleScore {
1409
1465
  /** Free-form explanation for debugging. */
1410
1466
  rationale?: string;
1411
1467
  }
1468
+ /**
1469
+ * alpha.86 — who originated an outcome row (migration 056).
1470
+ *
1471
+ * Absent/undefined means ORGANIC: a real consumer call on behalf of a real
1472
+ * user. Consumers change nothing. Synthetic writers — canaries, smoke
1473
+ * gates, eval replays, probe harnesses — self-mark, so liveness and volume
1474
+ * rules can compute over consumer-originated rows only.
1475
+ *
1476
+ * The incident this closes (cost-watch 2026-08-06 → 08-08): playbacksam's
1477
+ * daily canary wrote exactly 8 rows/day for 13 straight days while PB's
1478
+ * organic traffic was near-zero, and the liveness rule scored PB the
1479
+ * healthiest consumer in the portfolio. On 08-08 EVERY row the brain
1480
+ * received in 24h was canary, and no rule could say so — distinguishing a
1481
+ * replay from real traffic took a hand-reconstructed time-window +
1482
+ * token-fingerprint argument, three mornings running. A consumer whose
1483
+ * only rows are written by kgauto's own machinery is dark, not healthy;
1484
+ * this column is what lets a rule print that sentence.
1485
+ */
1486
+ type OutcomeSource = 'canary' | 'smoke' | 'eval' | 'probe' | 'synthetic';
1412
1487
  interface RecordInput {
1413
1488
  /** Handle from CompileResult. */
1414
1489
  handle: string;
1490
+ /**
1491
+ * alpha.86 — self-mark for synthetic writers (migration 056). OMIT for
1492
+ * real traffic; never write an explicit "organic" value. See
1493
+ * {@link OutcomeSource}.
1494
+ */
1495
+ source?: OutcomeSource;
1415
1496
  /** Actual tokens consumed (post-call). */
1416
1497
  tokensIn: number;
1417
1498
  tokensOut: number;
@@ -305,6 +305,17 @@ interface CompilePolicy {
305
305
  * Model IDs the consumer has gated. Compile() will never select these.
306
306
  * Use for: cost caps, account-level rate limits, "this model is broken
307
307
  * for our workload" decisions.
308
+ *
309
+ * Matching is by EXACT id after alias resolution (alpha.85): an alias
310
+ * and its canonical id are the same model, so blocking either blocks
311
+ * both (`deepseek-chat` ≡ `deepseek-v4-flash`). Matching does NOT widen
312
+ * to family siblings — blocking `deepseek-v4-flash` says nothing about
313
+ * `deepseek-v4-pro` — and a bare family key (`'deepseek'`,
314
+ * `'claude-sonnet'`) is INERT: it matches no model and no warning fires
315
+ * today. To gate a family, list its concrete ids. Beware roster
316
+ * retargets: when a family primary moves to a new id, a literal-id
317
+ * block does not follow it (tt-intel s113 built consumer-side family
318
+ * resolution for exactly this).
308
319
  */
309
320
  blockedModels?: string[];
310
321
  /**
@@ -1073,6 +1084,51 @@ interface ShadowProbeConfig {
1073
1084
  skipSlowTierInSync?: boolean;
1074
1085
  }
1075
1086
  interface CallOptions {
1087
+ /**
1088
+ * alpha.86 — self-mark for synthetic callers (canaries, smokes, eval
1089
+ * replays, probes). Passed through to every outcome row this call
1090
+ * records, including failure rows. OMIT for real traffic. See
1091
+ * {@link OutcomeSource}.
1092
+ */
1093
+ source?: OutcomeSource;
1094
+ /**
1095
+ * alpha.84 — fires once per FAILED attempt during the fallback walk, before
1096
+ * the walk continues. IC-Cairn's filing, 2026-07-29.
1097
+ *
1098
+ * ## The gap this closes
1099
+ *
1100
+ * `call()` records exactly ONE outcome row per call. An attempt that fails
1101
+ * and then walks to a successful fallback survives only as
1102
+ * `fellOverFrom`/`fallbackReason` metadata ON the success row — so a
1103
+ * `call()`-only consumer's corpus contains **no failure rows at all**, by
1104
+ * construction, no matter how carefully they wired `record()`.
1105
+ *
1106
+ * That is not hypothetical. kgauto's own `corpus-implausible-success`
1107
+ * detector fired on inspire-central and told them to "wire record() on the
1108
+ * FAILURE path" — which they had already done, in five places. Measured:
1109
+ * 89 rows, 0 failure rows, 2 fellover-bearing. **The detector demanded
1110
+ * output the API could not produce.** Streaming consumers have had
1111
+ * `onFailedAttempt` since alpha.48 via `streamWithFallover`; `call()`
1112
+ * consumers had nothing. This restores parity.
1113
+ *
1114
+ * ## Contract
1115
+ *
1116
+ * Fires for every attempt whose `status` is `'retryable'` or `'terminal'`,
1117
+ * including skipped ones (`*_inferred`, `aborted`) — a skip is a real
1118
+ * datum about the chain, not an absence. Never fires for `'success'`.
1119
+ *
1120
+ * Called synchronously and **its throw is swallowed**: a consumer's
1121
+ * telemetry bug must never take down the call it is observing. It is a
1122
+ * notification, not a hook that can veto or alter the walk.
1123
+ *
1124
+ * Typical use — emit a failure row per attempt so the corpus can contain
1125
+ * failures at all:
1126
+ *
1127
+ * call(ir, { onFailedAttempt: (a) => void recordOutcome({
1128
+ * ...base, success: false, errorType: a.errorCode, model: a.model,
1129
+ * }) })
1130
+ */
1131
+ onFailedAttempt?: (attempt: CallAttempt) => void;
1076
1132
  /** Forwarded to compile(). */
1077
1133
  policy?: CompilePolicy;
1078
1134
  /**
@@ -1409,9 +1465,34 @@ interface OracleScore {
1409
1465
  /** Free-form explanation for debugging. */
1410
1466
  rationale?: string;
1411
1467
  }
1468
+ /**
1469
+ * alpha.86 — who originated an outcome row (migration 056).
1470
+ *
1471
+ * Absent/undefined means ORGANIC: a real consumer call on behalf of a real
1472
+ * user. Consumers change nothing. Synthetic writers — canaries, smoke
1473
+ * gates, eval replays, probe harnesses — self-mark, so liveness and volume
1474
+ * rules can compute over consumer-originated rows only.
1475
+ *
1476
+ * The incident this closes (cost-watch 2026-08-06 → 08-08): playbacksam's
1477
+ * daily canary wrote exactly 8 rows/day for 13 straight days while PB's
1478
+ * organic traffic was near-zero, and the liveness rule scored PB the
1479
+ * healthiest consumer in the portfolio. On 08-08 EVERY row the brain
1480
+ * received in 24h was canary, and no rule could say so — distinguishing a
1481
+ * replay from real traffic took a hand-reconstructed time-window +
1482
+ * token-fingerprint argument, three mornings running. A consumer whose
1483
+ * only rows are written by kgauto's own machinery is dark, not healthy;
1484
+ * this column is what lets a rule print that sentence.
1485
+ */
1486
+ type OutcomeSource = 'canary' | 'smoke' | 'eval' | 'probe' | 'synthetic';
1412
1487
  interface RecordInput {
1413
1488
  /** Handle from CompileResult. */
1414
1489
  handle: string;
1490
+ /**
1491
+ * alpha.86 — self-mark for synthetic writers (migration 056). OMIT for
1492
+ * real traffic; never write an explicit "organic" value. See
1493
+ * {@link OutcomeSource}.
1494
+ */
1495
+ source?: OutcomeSource;
1415
1496
  /** Actual tokens consumed (post-call). */
1416
1497
  tokensIn: number;
1417
1498
  tokensOut: number;