nexus-agents 8.85.2 → 8.86.0

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 (138) hide show
  1. package/dist/{ast-rule-runner-GDCEI7BW.js → ast-rule-runner-USLLC3XL.js} +4 -4
  2. package/dist/{child-mcp-config-HAXDNULH.js → child-mcp-config-K563PP6S.js} +3 -3
  3. package/dist/{chunk-QLYONNVR.js → chunk-2SQO4FET.js} +2 -2
  4. package/dist/{chunk-L3PGHZH7.js → chunk-4Q753QZM.js} +3 -3
  5. package/dist/{chunk-WIBR5HMY.js → chunk-5U4XTYAB.js} +2 -2
  6. package/dist/{chunk-YGOTLETF.js → chunk-64IJKT4I.js} +537 -402
  7. package/dist/chunk-64IJKT4I.js.map +1 -0
  8. package/dist/{chunk-CDHUCQ44.js → chunk-C4YTAB6K.js} +43 -22
  9. package/dist/chunk-C4YTAB6K.js.map +1 -0
  10. package/dist/{chunk-TYHKIUUX.js → chunk-CBZAMHBA.js} +2 -2
  11. package/dist/{chunk-FOODF33L.js → chunk-DSVMQVJZ.js} +4 -4
  12. package/dist/{chunk-XLYTLQAT.js → chunk-EVYZZL7J.js} +2 -2
  13. package/dist/{chunk-7NLTUPYN.js → chunk-FNL4J77P.js} +2 -2
  14. package/dist/chunk-FNL4J77P.js.map +1 -0
  15. package/dist/{chunk-GFFQHVSC.js → chunk-GKLTEUFU.js} +2 -2
  16. package/dist/{chunk-JHNS5FYV.js → chunk-IXKFIO6O.js} +2 -2
  17. package/dist/{chunk-4RHGZLKP.js → chunk-J6GIJFV4.js} +2 -2
  18. package/dist/{chunk-HSMW4KIV.js → chunk-KX7A23JM.js} +2 -2
  19. package/dist/{chunk-PB4SXWBA.js → chunk-KXFPJOPL.js} +2 -2
  20. package/dist/{chunk-Y2ARXXPF.js → chunk-NNYVXSQF.js} +2 -2
  21. package/dist/{chunk-K6VACGLH.js → chunk-NQA77WXP.js} +3 -3
  22. package/dist/{chunk-7L6KI365.js → chunk-O24ENDHN.js} +3 -3
  23. package/dist/{chunk-M6U6UHFA.js → chunk-OOIZ3IQD.js} +2 -2
  24. package/dist/{chunk-U33OOJOK.js → chunk-P6D7S7CY.js} +2 -2
  25. package/dist/{chunk-B3PR26HR.js → chunk-Q75P5CVJ.js} +2 -2
  26. package/dist/{chunk-P5BZ3DNM.js → chunk-QCDYJYGB.js} +7 -7
  27. package/dist/{chunk-WZRXZ6P2.js → chunk-QW6J2UV4.js} +43 -9
  28. package/dist/chunk-QW6J2UV4.js.map +1 -0
  29. package/dist/{chunk-4UHDDCRB.js → chunk-RQLIPUQK.js} +5 -1
  30. package/dist/{chunk-4UHDDCRB.js.map → chunk-RQLIPUQK.js.map} +1 -1
  31. package/dist/{chunk-3EK4V5JS.js → chunk-S6MNVNQB.js} +2 -2
  32. package/dist/{chunk-IKLDY3HI.js → chunk-SQ4M3JHW.js} +41 -39
  33. package/dist/chunk-SQ4M3JHW.js.map +1 -0
  34. package/dist/{chunk-Z33AE45S.js → chunk-V66MMBO4.js} +5 -5
  35. package/dist/{chunk-WKZSEYV7.js → chunk-XARB7ITD.js} +8 -8
  36. package/dist/{chunk-PBXABL7H.js → chunk-XNNNDQLK.js} +2 -2
  37. package/dist/{chunk-VYZRGIY5.js → chunk-XWPZZEBX.js} +4 -4
  38. package/dist/{chunk-ILGHDIYO.js → chunk-YG73EH43.js} +2 -2
  39. package/dist/{chunk-23DTC2UV.js → chunk-YPJRVA2H.js} +4 -4
  40. package/dist/{chunk-NL7I35TJ.js → chunk-ZE73MT66.js} +5 -5
  41. package/dist/chunk-ZE73MT66.js.map +1 -0
  42. package/dist/{chunk-ARWTQGSY.js → chunk-ZEOHUE2N.js} +10 -10
  43. package/dist/{chunk-KC3AZWVQ.js → chunk-ZSFRB2S4.js} +3 -3
  44. package/dist/{cli-circuit-breaker-TOE7GRYI.js → cli-circuit-breaker-4ASNTBDP.js} +4 -4
  45. package/dist/cli.js +43 -43
  46. package/dist/{composite-router-UD3FGSSS.js → composite-router-V45AK6WD.js} +3 -3
  47. package/dist/{consensus-vote-YW6LAIH7.js → consensus-vote-N2Z7FX3D.js} +17 -17
  48. package/dist/{context-retriever-CH3GEH2Z.js → context-retriever-NZOSGG6S.js} +9 -9
  49. package/dist/{doctor-deep-LMEUFKDA.js → doctor-deep-JRY3D7BP.js} +4 -4
  50. package/dist/{doctor-live-O25NTC5D.js → doctor-live-NSRHMHMG.js} +11 -11
  51. package/dist/{expert-bridge-ZQSYVUC2.js → expert-bridge-Q4SEPVTE.js} +5 -5
  52. package/dist/factory-JBK45P7G.js +15 -0
  53. package/dist/{factory-O4U4BJFT.js → factory-ZV4MR3VM.js} +10 -10
  54. package/dist/{improvement-review-RL6VHCCY.js → improvement-review-KHTIDV4R.js} +6 -6
  55. package/dist/index.d.ts +56 -6
  56. package/dist/index.js +31 -31
  57. package/dist/{init-opencode-RMMHBIQ2.js → init-opencode-6WGN5J6J.js} +6 -6
  58. package/dist/issue-triage-XVDXOZRW.js +16 -0
  59. package/dist/{learning-persistence-7BEPQEGN.js → learning-persistence-ER7E5Q6T.js} +6 -4
  60. package/dist/model-registry.generated.json +2949 -3961
  61. package/dist/{pr-reviewer-helpers-S53B7TYK.js → pr-reviewer-helpers-UMNMOUAO.js} +5 -5
  62. package/dist/{registry-command-TWHRKWDB.js → registry-command-QRBAKWRK.js} +3 -3
  63. package/dist/{repo-security-plan-HYJ7HTOS.js → repo-security-plan-UM6WX5PN.js} +4 -4
  64. package/dist/research-helpers-synthesize-DUOVXI5V.js +14 -0
  65. package/dist/{routing-memory-HQTEZF2S.js → routing-memory-OKYSDPXT.js} +3 -3
  66. package/dist/{session-memory-M5MEGJJZ.js → session-memory-ONUSRMYV.js} +4 -4
  67. package/dist/setup-config-BAZRBFN3.js +11 -0
  68. package/dist/{setup-custom-api-LRL6J7RN.js → setup-custom-api-LSMLWGKW.js} +5 -5
  69. package/dist/setup-data-dir-7PDX3R3Y.js +32 -0
  70. package/dist/{tool-memory-24PMDECK.js → tool-memory-Q5UWNVN3.js} +6 -6
  71. package/dist/{unified-registry-NODUOBMA.js → unified-registry-Y327PN6R.js} +11 -11
  72. package/dist/{weather-report-IGEXNIWL.js → weather-report-C2VIAC2Q.js} +3 -3
  73. package/package.json +1 -1
  74. package/dist/chunk-7NLTUPYN.js.map +0 -1
  75. package/dist/chunk-CDHUCQ44.js.map +0 -1
  76. package/dist/chunk-IKLDY3HI.js.map +0 -1
  77. package/dist/chunk-NL7I35TJ.js.map +0 -1
  78. package/dist/chunk-WZRXZ6P2.js.map +0 -1
  79. package/dist/chunk-YGOTLETF.js.map +0 -1
  80. package/dist/factory-ZOM2QJPC.js +0 -15
  81. package/dist/issue-triage-DQAOBZEP.js +0 -16
  82. package/dist/research-helpers-synthesize-G3WV7AHW.js +0 -14
  83. package/dist/setup-config-L5QGPDH6.js +0 -11
  84. package/dist/setup-data-dir-WZXMTSDN.js +0 -32
  85. /package/dist/{ast-rule-runner-GDCEI7BW.js.map → ast-rule-runner-USLLC3XL.js.map} +0 -0
  86. /package/dist/{child-mcp-config-HAXDNULH.js.map → child-mcp-config-K563PP6S.js.map} +0 -0
  87. /package/dist/{chunk-QLYONNVR.js.map → chunk-2SQO4FET.js.map} +0 -0
  88. /package/dist/{chunk-L3PGHZH7.js.map → chunk-4Q753QZM.js.map} +0 -0
  89. /package/dist/{chunk-WIBR5HMY.js.map → chunk-5U4XTYAB.js.map} +0 -0
  90. /package/dist/{chunk-TYHKIUUX.js.map → chunk-CBZAMHBA.js.map} +0 -0
  91. /package/dist/{chunk-FOODF33L.js.map → chunk-DSVMQVJZ.js.map} +0 -0
  92. /package/dist/{chunk-XLYTLQAT.js.map → chunk-EVYZZL7J.js.map} +0 -0
  93. /package/dist/{chunk-GFFQHVSC.js.map → chunk-GKLTEUFU.js.map} +0 -0
  94. /package/dist/{chunk-JHNS5FYV.js.map → chunk-IXKFIO6O.js.map} +0 -0
  95. /package/dist/{chunk-4RHGZLKP.js.map → chunk-J6GIJFV4.js.map} +0 -0
  96. /package/dist/{chunk-HSMW4KIV.js.map → chunk-KX7A23JM.js.map} +0 -0
  97. /package/dist/{chunk-PB4SXWBA.js.map → chunk-KXFPJOPL.js.map} +0 -0
  98. /package/dist/{chunk-Y2ARXXPF.js.map → chunk-NNYVXSQF.js.map} +0 -0
  99. /package/dist/{chunk-K6VACGLH.js.map → chunk-NQA77WXP.js.map} +0 -0
  100. /package/dist/{chunk-7L6KI365.js.map → chunk-O24ENDHN.js.map} +0 -0
  101. /package/dist/{chunk-M6U6UHFA.js.map → chunk-OOIZ3IQD.js.map} +0 -0
  102. /package/dist/{chunk-U33OOJOK.js.map → chunk-P6D7S7CY.js.map} +0 -0
  103. /package/dist/{chunk-B3PR26HR.js.map → chunk-Q75P5CVJ.js.map} +0 -0
  104. /package/dist/{chunk-P5BZ3DNM.js.map → chunk-QCDYJYGB.js.map} +0 -0
  105. /package/dist/{chunk-3EK4V5JS.js.map → chunk-S6MNVNQB.js.map} +0 -0
  106. /package/dist/{chunk-Z33AE45S.js.map → chunk-V66MMBO4.js.map} +0 -0
  107. /package/dist/{chunk-WKZSEYV7.js.map → chunk-XARB7ITD.js.map} +0 -0
  108. /package/dist/{chunk-PBXABL7H.js.map → chunk-XNNNDQLK.js.map} +0 -0
  109. /package/dist/{chunk-VYZRGIY5.js.map → chunk-XWPZZEBX.js.map} +0 -0
  110. /package/dist/{chunk-ILGHDIYO.js.map → chunk-YG73EH43.js.map} +0 -0
  111. /package/dist/{chunk-23DTC2UV.js.map → chunk-YPJRVA2H.js.map} +0 -0
  112. /package/dist/{chunk-ARWTQGSY.js.map → chunk-ZEOHUE2N.js.map} +0 -0
  113. /package/dist/{chunk-KC3AZWVQ.js.map → chunk-ZSFRB2S4.js.map} +0 -0
  114. /package/dist/{cli-circuit-breaker-TOE7GRYI.js.map → cli-circuit-breaker-4ASNTBDP.js.map} +0 -0
  115. /package/dist/{composite-router-UD3FGSSS.js.map → composite-router-V45AK6WD.js.map} +0 -0
  116. /package/dist/{consensus-vote-YW6LAIH7.js.map → consensus-vote-N2Z7FX3D.js.map} +0 -0
  117. /package/dist/{context-retriever-CH3GEH2Z.js.map → context-retriever-NZOSGG6S.js.map} +0 -0
  118. /package/dist/{doctor-deep-LMEUFKDA.js.map → doctor-deep-JRY3D7BP.js.map} +0 -0
  119. /package/dist/{doctor-live-O25NTC5D.js.map → doctor-live-NSRHMHMG.js.map} +0 -0
  120. /package/dist/{expert-bridge-ZQSYVUC2.js.map → expert-bridge-Q4SEPVTE.js.map} +0 -0
  121. /package/dist/{factory-O4U4BJFT.js.map → factory-JBK45P7G.js.map} +0 -0
  122. /package/dist/{factory-ZOM2QJPC.js.map → factory-ZV4MR3VM.js.map} +0 -0
  123. /package/dist/{improvement-review-RL6VHCCY.js.map → improvement-review-KHTIDV4R.js.map} +0 -0
  124. /package/dist/{init-opencode-RMMHBIQ2.js.map → init-opencode-6WGN5J6J.js.map} +0 -0
  125. /package/dist/{issue-triage-DQAOBZEP.js.map → issue-triage-XVDXOZRW.js.map} +0 -0
  126. /package/dist/{learning-persistence-7BEPQEGN.js.map → learning-persistence-ER7E5Q6T.js.map} +0 -0
  127. /package/dist/{pr-reviewer-helpers-S53B7TYK.js.map → pr-reviewer-helpers-UMNMOUAO.js.map} +0 -0
  128. /package/dist/{registry-command-TWHRKWDB.js.map → registry-command-QRBAKWRK.js.map} +0 -0
  129. /package/dist/{repo-security-plan-HYJ7HTOS.js.map → repo-security-plan-UM6WX5PN.js.map} +0 -0
  130. /package/dist/{research-helpers-synthesize-G3WV7AHW.js.map → research-helpers-synthesize-DUOVXI5V.js.map} +0 -0
  131. /package/dist/{routing-memory-HQTEZF2S.js.map → routing-memory-OKYSDPXT.js.map} +0 -0
  132. /package/dist/{session-memory-M5MEGJJZ.js.map → session-memory-ONUSRMYV.js.map} +0 -0
  133. /package/dist/{setup-config-L5QGPDH6.js.map → setup-config-BAZRBFN3.js.map} +0 -0
  134. /package/dist/{setup-custom-api-LRL6J7RN.js.map → setup-custom-api-LSMLWGKW.js.map} +0 -0
  135. /package/dist/{setup-data-dir-WZXMTSDN.js.map → setup-data-dir-7PDX3R3Y.js.map} +0 -0
  136. /package/dist/{tool-memory-24PMDECK.js.map → tool-memory-Q5UWNVN3.js.map} +0 -0
  137. /package/dist/{unified-registry-NODUOBMA.js.map → unified-registry-Y327PN6R.js.map} +0 -0
  138. /package/dist/{weather-report-IGEXNIWL.js.map → weather-report-C2VIAC2Q.js.map} +0 -0
@@ -2,8 +2,9 @@ import {
2
2
  ensureLearningDir,
3
3
  getDecisionCostFile,
4
4
  getModelSelectionShadowFile,
5
- isPersistenceEnabled
6
- } from "./chunk-4UHDDCRB.js";
5
+ isPersistenceEnabled,
6
+ isStrategyDistillationEnabled
7
+ } from "./chunk-RQLIPUQK.js";
7
8
  import {
8
9
  getNexusDataDir,
9
10
  nexusDataPath,
@@ -1303,9 +1304,9 @@ var DEFAULT_MODEL_CAPABILITIES = {
1303
1304
  },
1304
1305
  {
1305
1306
  id: "codex-5.2",
1306
- displayName: "GPT-5.3 Codex Spark",
1307
+ displayName: "GPT-5.6 Luna",
1307
1308
  provider: "openai",
1308
- contextWindow: 128e3,
1309
+ contextWindow: 105e4,
1309
1310
  outputModalities: ["text", "structured_json", "code"],
1310
1311
  inputModalities: ["text", "image", "pdf", "code"],
1311
1312
  toolCapabilities: [
@@ -1322,33 +1323,41 @@ var DEFAULT_MODEL_CAPABILITIES = {
1322
1323
  "No native image/audio/video generation",
1323
1324
  "Network access restricted in sandbox"
1324
1325
  ],
1325
- // #5091: repointed from gpt-5.2-codex, which codex 0.146.0 no longer
1326
- // serves (~/.codex/models_cache.json visibility=list). Context window,
1327
- // max output and pricing are models.dev's figures for this slug
1328
- // (snapshot 2026-08-31). Quality scores are the codex arm's tier
1329
- // positions carried over from the replaced entry, not a measurement of
1330
- // this slug; `cost` is unchanged because the price is identical.
1326
+ // 2026-09-23: repointed from gpt-5.3-codex-spark, which codex-cli 0.155.1
1327
+ // no longer serves (~/.codex/models_cache.json visibility=list; the
1328
+ // verify Codex Models check reported it). Same shape as #5091 option A:
1329
+ // the id stays (it holds the arm's outcome history), only the slug and
1330
+ // the metadata describing it move.
1331
+ // WHY gpt-5.6-luna: this id is the codex fast/spark tier. The served
1332
+ // fast models are the two "luna" variants (codex catalog: gpt-5.6-luna
1333
+ // "Older fast and efficient model"; gpt-6-luna "Fast and affordable model
1334
+ // for easier tasks"). gpt-6-luna is the cheaper of the two, so it takes
1335
+ // the mini id; this id takes gpt-5.6-luna.
1336
+ // Context window, max output and pricing are models.dev's figures for
1337
+ // this slug (snapshot 2026-09-21); the codex cache reports a 272K default
1338
+ // window. reasoning/codeGeneration/speed are tier positions CARRIED OVER
1339
+ // from the replaced entry, not a measurement of this slug; `cost` follows
1340
+ // the registry's price scale (9, as for $0.3/$1 openrouter-qwen-coder;
1341
+ // only the free model scores 10) — spark's 7 described a $1.75/$14 slug.
1331
1342
  // ROUTING CONSEQUENCE: `resolve-model-for-tier` ranks the balanced tier
1332
1343
  // by codeGeneration and breaks the 10/10/10 tie on the higher `cost`
1333
- // score, so this entry (cost 7) wins the balanced codex tier over
1334
- // codex-5.3 (5) and gpt-5.5 (4). It already did; what changed is that
1335
- // the winner now runs (spark) instead of being rejected (gpt-5.2-codex).
1336
- // `resolve-model-for-tier.test.ts` pins this so a rescore is a visible
1337
- // decision, not a side effect.
1338
- notes: "GPT-5.3 Codex Spark (models.dev 2026-08-31); fast codex-tuned tier below GPT-5.4; 128K context; sandboxed execution environment",
1339
- pricing: { inputPer1M: 1.75, outputPer1M: 14 },
1340
- qualityScores: { reasoning: 9, codeGeneration: 10, speed: 8, cost: 7 },
1341
- maxOutputTokens: 32e3,
1344
+ // score, so this entry (now cost 9) still wins the balanced codex tier
1345
+ // over codex-5.3 (5) and gpt-5.5 (4). `resolve-model-for-tier.test.ts`
1346
+ // pins it so a rescore is a visible decision, not a side effect.
1347
+ notes: 'GPT-5.6 Luna (models.dev 2026-09-21; codex catalog: "older fast and efficient model"); fast codex tier; served by codex-cli 0.155.1; 1.05M context (codex cache default window 272K)',
1348
+ pricing: { inputPer1M: 0.2, outputPer1M: 1.2 },
1349
+ qualityScores: { reasoning: 9, codeGeneration: 10, speed: 8, cost: 9 },
1350
+ maxOutputTokens: 128e3,
1342
1351
  cliName: "codex",
1343
- cliModelName: "gpt-5.3-codex-spark",
1352
+ cliModelName: "gpt-5.6-luna",
1344
1353
  unsupportedParameters: ["temperature"],
1345
1354
  maxTokensParam: "max_completion_tokens"
1346
1355
  },
1347
1356
  {
1348
1357
  id: "codex-5.1-mini",
1349
- displayName: "GPT-5.4 Mini",
1358
+ displayName: "GPT-6 Luna",
1350
1359
  provider: "openai",
1351
- contextWindow: 4e5,
1360
+ contextWindow: 105e4,
1352
1361
  outputModalities: ["text", "structured_json", "code"],
1353
1362
  inputModalities: ["text", "image", "code"],
1354
1363
  toolCapabilities: [
@@ -1360,19 +1369,25 @@ var DEFAULT_MODEL_CAPABILITIES = {
1360
1369
  ],
1361
1370
  specialFeatures: ["streaming"],
1362
1371
  constraints: ["Sandboxed execution only", "No native image/audio/video generation"],
1363
- // #5091: repointed from o3-mini, which codex 0.146.0 no longer serves
1364
- // (~/.codex/models_cache.json visibility=list). Context window, max
1365
- // output and pricing are models.dev's figures for this slug (snapshot
1366
- // 2026-08-31). Quality scores are the codex arm's tier positions carried
1367
- // over from the replaced entry, not a measurement of this slug; `cost`
1368
- // follows the registry's own price scale ($1/$5 haiku and $0.5/$3
1369
- // gemini-3-flash both score 9, and $0.75/$4.5 sits between them).
1370
- notes: "GPT-5.4 Mini (models.dev 2026-08-31); compact codex tier; fast and cost-effective for code tasks; 400K context",
1371
- pricing: { inputPer1M: 0.75, outputPer1M: 4.5 },
1372
+ // 2026-09-23: repointed from gpt-5.4-mini, which codex-cli 0.155.1 no
1373
+ // longer serves (~/.codex/models_cache.json visibility=list); #5091 had
1374
+ // repointed it from o3-mini. WHY gpt-6-luna: it is the smallest served
1375
+ // model — $0.1/$0.5 per 1M, below gpt-5.6-luna's $0.2/$1.2 — and the
1376
+ // codex catalog calls it "Fast and affordable model for easier tasks".
1377
+ // SOURCE: the committed models.dev snapshot (2026-09-21) and generated
1378
+ // catalogue (2026-09-20) predate gpt-6-luna. Context window, max output
1379
+ // and pricing are the live models.dev / LiteLLM figures for
1380
+ // `openai/gpt-6-luna` as read by `pnpm build:registry` on 2026-09-23
1381
+ // (LiteLLM: 922K max input); the codex cache reports a 272K default
1382
+ // window. Quality scores are the codex arm's tier positions carried
1383
+ // over, not a measurement of this slug; `cost` stays 9 on the registry's
1384
+ // price scale (only the free model scores 10).
1385
+ notes: 'GPT-6 Luna (models.dev/LiteLLM 2026-09-23; codex catalog: "fast and affordable model for easier tasks"); compact codex tier; served by codex-cli 0.155.1; 1.05M context (codex cache default window 272K)',
1386
+ pricing: { inputPer1M: 0.1, outputPer1M: 0.5 },
1372
1387
  qualityScores: { reasoning: 7, codeGeneration: 8, speed: 9, cost: 9 },
1373
1388
  maxOutputTokens: 128e3,
1374
1389
  cliName: "codex",
1375
- cliModelName: "gpt-5.4-mini",
1390
+ cliModelName: "gpt-6-luna",
1376
1391
  unsupportedParameters: ["temperature"],
1377
1392
  maxTokensParam: "max_completion_tokens"
1378
1393
  },
@@ -2628,7 +2643,7 @@ function setDefaultRegistry(registry) {
2628
2643
  }
2629
2644
  async function reloadDefaultRegistry() {
2630
2645
  globalRegistry = buildDefaultRegistry();
2631
- const { resetGlobalRegistry } = await import("./unified-registry-NODUOBMA.js");
2646
+ const { resetGlobalRegistry } = await import("./unified-registry-Y327PN6R.js");
2632
2647
  resetGlobalRegistry();
2633
2648
  return globalRegistry;
2634
2649
  }
@@ -9641,20 +9656,40 @@ var DistilledRuleStage = class {
9641
9656
  config;
9642
9657
  logger;
9643
9658
  rulesAppliedCount = 0;
9659
+ firstRunChecked = false;
9644
9660
  constructor(distiller, config, logger15) {
9645
9661
  this.distiller = distiller;
9646
9662
  this.config = { ...DEFAULT_CONFIG5, ...config };
9647
9663
  this.logger = logger15 ?? createLogger({ component: "DistilledRuleStage" });
9648
9664
  }
9649
9665
  canHandle(ctx) {
9666
+ this.checkPersistedTriggerOnce();
9650
9667
  const candidates = getRemainingCandidates(ctx);
9651
9668
  if (candidates.length <= 1) return false;
9652
9669
  const activeRules = this.distiller.getRules("active");
9653
9670
  return activeRules.length > 0;
9654
9671
  }
9655
9672
  route(ctx) {
9673
+ this.checkPersistedTriggerOnce();
9656
9674
  return Promise.resolve(this.routeSync(ctx));
9657
9675
  }
9676
+ /**
9677
+ * The first time this stage runs, let the distiller distill from the
9678
+ * persisted outcome store (#6512). Without it, rules only ever came from 50
9679
+ * outcomes seen inside one process, which short-lived CLI processes never
9680
+ * reach. A failure here is logged and routing continues without rules.
9681
+ */
9682
+ checkPersistedTriggerOnce() {
9683
+ if (this.firstRunChecked) return;
9684
+ this.firstRunChecked = true;
9685
+ try {
9686
+ this.distiller.checkPersistedTrigger();
9687
+ } catch (error) {
9688
+ this.logger.warn("Persisted-store distill trigger failed; routing without new rules", {
9689
+ error: error instanceof Error ? error.message : String(error)
9690
+ });
9691
+ }
9692
+ }
9658
9693
  recordOutcome(outcome) {
9659
9694
  this.distiller.onOutcome();
9660
9695
  this.logger.debug("Forwarded outcome to distiller", {
@@ -10182,367 +10217,58 @@ var DEFAULT_DISTILLER_CONFIG = {
10182
10217
  ruleExpiryMs: 24 * 60 * 60 * 1e3
10183
10218
  };
10184
10219
 
10185
- // src/learning/strategy-distiller.ts
10186
- function sigmoidConfidence(observations, center = 30) {
10187
- const raw = 1 / (1 + Math.exp(-(observations - center) / 5));
10188
- return Math.max(0, Math.min(1, raw));
10189
- }
10190
- function clampUnit(value) {
10191
- if (!Number.isFinite(value)) return 0;
10192
- return Math.max(0, Math.min(1, value));
10193
- }
10194
- function effectFor(patternType, metric, thresholds) {
10195
- switch (patternType) {
10196
- case "failure-rate":
10197
- return clampUnit(
10198
- (metric - thresholds.failureRateThreshold) / (1 - thresholds.failureRateThreshold)
10199
- );
10200
- case "success-rate":
10201
- return clampUnit(
10202
- (metric - thresholds.successRateThreshold) / (1 - thresholds.successRateThreshold)
10203
- );
10204
- case "latency-spike":
10205
- return clampUnit(
10206
- (metric - thresholds.latencyRatioThreshold) / thresholds.latencyRatioThreshold
10207
- );
10208
- }
10209
- }
10210
- function ruleFingerprint(patternType, cli, category) {
10211
- return `${patternType}:${cli}:${category}`;
10212
- }
10213
- function groupOutcomes(outcomes) {
10214
- const map2 = /* @__PURE__ */ new Map();
10215
- for (const o of outcomes) {
10216
- const key = `${o.cli}:${o.category}`;
10217
- const list = map2.get(key);
10218
- if (list !== void 0) {
10219
- list.push(o);
10220
- } else {
10221
- map2.set(key, [o]);
10222
- }
10223
- }
10224
- const groups = [];
10225
- for (const [key, list] of map2) {
10226
- const [cli, category] = key.split(":");
10227
- groups.push({ cli, category, outcomes: list });
10228
- }
10229
- return groups;
10230
- }
10231
- function percentile(sorted, p) {
10232
- if (sorted.length === 0) return 0;
10233
- const idx = Math.ceil(p / 100 * sorted.length) - 1;
10234
- return sorted[Math.max(0, idx)] ?? 0;
10235
- }
10236
- function detectFailurePatterns(groups, threshold) {
10237
- const patterns = [];
10238
- for (const g of groups) {
10239
- const failCount = g.outcomes.filter((o) => !o.success).length;
10240
- const failRate = failCount / g.outcomes.length;
10241
- if (failRate >= threshold) {
10242
- patterns.push({
10243
- cli: g.cli,
10244
- category: g.category,
10245
- patternType: "failure-rate",
10246
- action: failRate >= 0.8 ? "avoid" : "penalize",
10247
- metric: failRate,
10248
- observationCount: g.outcomes.length
10249
- });
10250
- }
10251
- }
10252
- return patterns;
10253
- }
10254
- function detectSuccessPatterns(groups, threshold) {
10255
- const patterns = [];
10256
- for (const g of groups) {
10257
- const successCount = g.outcomes.filter((o) => o.success).length;
10258
- const successRate3 = successCount / g.outcomes.length;
10259
- if (successRate3 >= threshold) {
10260
- patterns.push({
10261
- cli: g.cli,
10262
- category: g.category,
10263
- patternType: "success-rate",
10264
- action: "boost",
10265
- metric: successRate3,
10266
- observationCount: g.outcomes.length
10267
- });
10268
- }
10269
- }
10270
- return patterns;
10271
- }
10272
- function detectLatencyPatterns(groups, threshold) {
10273
- const patterns = [];
10274
- for (const g of groups) {
10275
- const durations = g.outcomes.map((o) => o.durationMs).sort((a, b) => a - b);
10276
- if (durations.length < 3) continue;
10277
- const median = percentile(durations, 50);
10278
- const p90 = percentile(durations, 90);
10279
- if (median <= 0) continue;
10280
- const ratio = p90 / median;
10281
- if (ratio >= threshold) {
10282
- patterns.push({
10283
- cli: g.cli,
10284
- category: g.category,
10285
- patternType: "latency-spike",
10286
- action: "penalize",
10287
- metric: ratio,
10288
- observationCount: g.outcomes.length
10289
- });
10290
- }
10291
- }
10292
- return patterns;
10293
- }
10294
- var StrategyDistiller = class {
10295
- /** Protected so `PersistentStrategyDistiller` hydrates legacy rules under the live thresholds. */
10296
- config;
10297
- outcomeStore;
10298
- logger;
10299
- rules = /* @__PURE__ */ new Map();
10300
- outcomeCounter = 0;
10301
- lastDistillAt;
10302
- constructor(outcomeStore, logger15, config) {
10303
- this.outcomeStore = outcomeStore;
10304
- this.config = { ...DEFAULT_DISTILLER_CONFIG, ...config };
10305
- this.logger = logger15 ?? createLogger({ component: "StrategyDistiller" });
10306
- }
10307
- /** Called for each processed outcome. Triggers distillation at threshold. */
10308
- onOutcome() {
10309
- this.outcomeCounter++;
10310
- if (this.outcomeCounter >= this.config.triggerThreshold) {
10311
- this.distill();
10312
- this.outcomeCounter = 0;
10313
- }
10314
- }
10315
- /** Run distillation on current OutcomeStore data. */
10316
- distill() {
10317
- const now = getTimeProvider().now();
10318
- const outcomes = this.outcomeStore.query();
10319
- const groups = groupOutcomes(outcomes);
10320
- this.expireRules(now);
10321
- const failurePatterns = detectFailurePatterns(groups, this.config.failureRateThreshold);
10322
- const successPatterns = detectSuccessPatterns(groups, this.config.successRateThreshold);
10323
- const latencyPatterns = detectLatencyPatterns(groups, this.config.latencyRatioThreshold);
10324
- const allPatterns = [...failurePatterns, ...successPatterns, ...latencyPatterns];
10325
- for (const pattern of allPatterns) {
10326
- this.upsertRule(pattern, now);
10327
- }
10328
- this.enforceMaxRules();
10329
- this.lastDistillAt = now;
10330
- this.logger.debug("Distillation complete", {
10331
- rulesTotal: this.rules.size,
10332
- patternsFound: allPatterns.length,
10333
- outcomesAnalyzed: outcomes.length
10334
- });
10335
- }
10336
- /** Get rules filtered by status. */
10337
- getRules(status) {
10338
- const all = [...this.rules.values()];
10339
- if (status === void 0) return all;
10340
- return all.filter((r) => r.status === status);
10341
- }
10342
- /** Get distiller statistics. */
10343
- getStats() {
10344
- const countByStatus = {
10345
- draft: 0,
10346
- active: 0,
10347
- promoted: 0,
10348
- expired: 0
10349
- };
10350
- for (const rule of this.rules.values()) {
10351
- countByStatus[rule.status]++;
10352
- }
10353
- return {
10354
- ruleCountByStatus: countByStatus,
10355
- totalRules: this.rules.size,
10356
- lastDistillAt: this.lastDistillAt,
10357
- outcomesSinceLastDistill: this.outcomeCounter
10358
- };
10359
- }
10220
+ // src/learning/distiller-eligibility.ts
10221
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
10222
+
10223
+ // src/orchestration/outcomes/outcome-types.ts
10224
+ import { z as z14 } from "zod";
10225
+ var logger8 = createLogger({ component: "outcome-error-taxonomy" });
10226
+ var OutcomeSourceSchema = z14.enum(["delegate", "consensus", "manual"]);
10227
+ var OutcomeFailureCategorySchema = z14.enum([
10228
+ "timeout",
10229
+ "authentication",
10230
+ "rate_limit",
10231
+ "connection",
10232
+ "crash",
10233
+ "adapter_unavailable",
10234
+ "validation",
10235
+ "parse",
10236
+ "execution",
10237
+ "generic",
10238
+ "unknown"
10239
+ ]);
10240
+ var OutcomeCliSchema = z14.union([CliNameSchema, ApiArmIdSchema, z14.literal("unknown")]);
10241
+ var OutcomeCliSourceSchema = z14.enum(["executed", "category-default"]);
10242
+ var TaskOutcomeSchema = z14.object({
10243
+ id: z14.string().min(1),
10244
+ cli: OutcomeCliSchema,
10245
+ /** How the CLI attribution was obtained. Absent on legacy, unmeasured records. */
10246
+ cliSource: OutcomeCliSourceSchema.optional(),
10247
+ category: TaskCategorySchema,
10248
+ model: z14.string().min(1),
10249
+ success: z14.boolean(),
10250
+ durationMs: z14.number().nonnegative(),
10251
+ timestamp: z14.string().min(1),
10252
+ qualitySignals: z14.array(z14.string()).optional(),
10253
+ failureCategory: OutcomeFailureCategorySchema.optional(),
10254
+ errorMessage: z14.string().max(500).optional(),
10255
+ source: OutcomeSourceSchema,
10256
+ /** Whether this outcome came from a triage-initiated retry (#1506). */
10257
+ wasRetried: z14.boolean().optional(),
10258
+ /** Triage action taken on the failure (#1506). */
10259
+ triageAction: z14.string().max(30).optional(),
10260
+ /** Routing stage that selected this CLI (#1785). */
10261
+ routingStage: z14.string().max(50).optional(),
10262
+ /** Number of retry attempts before this outcome (#1785). */
10263
+ retryCount: z14.number().int().nonnegative().optional(),
10264
+ /** Vendor resolved from `model` via ModelRegistry at write time (#2548). */
10265
+ vendor: z14.string().min(1).max(40).optional(),
10266
+ /** Family resolved from `model` via ModelRegistry at write time (#2548). */
10267
+ family: z14.string().min(1).max(80).optional(),
10360
10268
  /**
10361
- * Promote high-confidence rules to RoutingMemory.
10362
- * Rules must be active, with sufficient observations and confidence.
10363
- *
10364
- * @deprecated No production caller (#5004 finding 4). `DistilledRuleStage`
10365
- * is the single channel by which distilled rules reach routing; this
10366
- * second channel into `RoutingMemory` is kept only so the deprecation is
10367
- * non-breaking. Removal is tracked in #5467. Note the gate now compares
10368
- * `confidence = support × effect` against `promotionConfidence`, so a rule
10369
- * that would have promoted on sample size alone may no longer clear it.
10370
- */
10371
- promote(routingMemory) {
10372
- let promoted = 0;
10373
- for (const [id, rule] of this.rules) {
10374
- if (rule.status !== "active") continue;
10375
- if (rule.observationCount < this.config.minObservationsForActive) continue;
10376
- if (rule.confidence < this.config.promotionConfidence) continue;
10377
- const performance2 = this.ruleToPerformance(rule);
10378
- routingMemory.storePreference(rule.cli, rule.category, performance2);
10379
- this.rules.set(id, { ...rule, status: "promoted", updatedAt: getTimeProvider().now() });
10380
- promoted++;
10381
- this.logger.info("Promoted distilled rule to RoutingMemory", {
10382
- ruleId: id,
10383
- cli: rule.cli,
10384
- category: rule.category,
10385
- confidence: rule.confidence
10386
- });
10387
- }
10388
- return promoted;
10389
- }
10390
- // ==========================================================================
10391
- // Protected — for subclass hydration (Issue #1009)
10392
- // ==========================================================================
10393
- /** Load pre-existing rules (e.g., from disk). Used by PersistentStrategyDistiller. */
10394
- loadRules(rules) {
10395
- for (const rule of rules) {
10396
- this.rules.set(rule.id, rule);
10397
- }
10398
- }
10399
- // ==========================================================================
10400
- // Private
10401
- // ==========================================================================
10402
- upsertRule(pattern, now) {
10403
- const id = ruleFingerprint(pattern.patternType, pattern.cli, pattern.category);
10404
- const existing = this.rules.get(id);
10405
- if (existing === void 0 && pattern.observationCount < this.config.minObservationsForDraft) {
10406
- return;
10407
- }
10408
- const support = sigmoidConfidence(pattern.observationCount);
10409
- const effect = effectFor(pattern.patternType, pattern.metric, this.config);
10410
- const confidence = support * effect;
10411
- const status = this.computeStatus(pattern.observationCount, existing?.status);
10412
- if (existing !== void 0) {
10413
- this.rules.set(id, {
10414
- ...existing,
10415
- action: pattern.action,
10416
- confidence,
10417
- support,
10418
- effect,
10419
- observationCount: pattern.observationCount,
10420
- metric: pattern.metric,
10421
- status,
10422
- updatedAt: now
10423
- });
10424
- } else {
10425
- this.rules.set(id, {
10426
- id,
10427
- patternType: pattern.patternType,
10428
- cli: pattern.cli,
10429
- category: pattern.category,
10430
- action: pattern.action,
10431
- confidence,
10432
- support,
10433
- effect,
10434
- observationCount: pattern.observationCount,
10435
- metric: pattern.metric,
10436
- status,
10437
- createdAt: now,
10438
- updatedAt: now,
10439
- tainted: false
10440
- });
10441
- }
10442
- }
10443
- computeStatus(observations, existing) {
10444
- if (existing === "promoted") return "promoted";
10445
- if (observations >= this.config.minObservationsForActive) return "active";
10446
- return "draft";
10447
- }
10448
- expireRules(now) {
10449
- for (const [id, rule] of this.rules) {
10450
- if (rule.status === "promoted") continue;
10451
- if (now - rule.updatedAt > this.config.ruleExpiryMs) {
10452
- this.rules.set(id, { ...rule, status: "expired", updatedAt: now });
10453
- }
10454
- }
10455
- }
10456
- enforceMaxRules() {
10457
- if (this.rules.size <= this.config.maxRules) return;
10458
- const sorted = [...this.rules.entries()].sort((a, b) => {
10459
- if (a[1].status === "expired" && b[1].status !== "expired") return -1;
10460
- if (b[1].status === "expired" && a[1].status !== "expired") return 1;
10461
- return a[1].confidence - b[1].confidence;
10462
- });
10463
- const excess = this.rules.size - this.config.maxRules;
10464
- for (let i = 0; i < excess; i++) {
10465
- const entry = sorted[i];
10466
- if (entry !== void 0) {
10467
- this.rules.delete(entry[0]);
10468
- }
10469
- }
10470
- }
10471
- /** Convert rule metrics into ModelPerformance for RoutingMemory. */
10472
- ruleToPerformance(rule) {
10473
- const successRate3 = rule.patternType === "success-rate" ? rule.metric : 1 - rule.metric;
10474
- return {
10475
- avgQuality: rule.confidence,
10476
- successRate: Math.max(0, Math.min(1, successRate3)),
10477
- avgLatencyMs: 0,
10478
- avgTokens: 0,
10479
- observations: rule.observationCount
10480
- };
10481
- }
10482
- };
10483
- function createStrategyDistiller(outcomeStore, logger15, config) {
10484
- return new StrategyDistiller(outcomeStore, logger15, config);
10485
- }
10486
- var persistentDistillerFactory;
10487
- function registerPersistentDistillerFactory(factory) {
10488
- persistentDistillerFactory = factory;
10489
- }
10490
- function createPersistentDistillerOrFallback(outcomeStore, logger15) {
10491
- if (persistentDistillerFactory !== void 0) {
10492
- return persistentDistillerFactory(outcomeStore, logger15);
10493
- }
10494
- return new StrategyDistiller(outcomeStore, logger15);
10495
- }
10496
-
10497
- // src/orchestration/outcomes/outcome-types.ts
10498
- import { z as z14 } from "zod";
10499
- var logger8 = createLogger({ component: "outcome-error-taxonomy" });
10500
- var OutcomeSourceSchema = z14.enum(["delegate", "consensus", "manual"]);
10501
- var OutcomeFailureCategorySchema = z14.enum([
10502
- "timeout",
10503
- "authentication",
10504
- "rate_limit",
10505
- "connection",
10506
- "crash",
10507
- "adapter_unavailable",
10508
- "validation",
10509
- "parse",
10510
- "execution",
10511
- "generic",
10512
- "unknown"
10513
- ]);
10514
- var OutcomeCliSchema = z14.union([CliNameSchema, ApiArmIdSchema, z14.literal("unknown")]);
10515
- var OutcomeCliSourceSchema = z14.enum(["executed", "category-default"]);
10516
- var TaskOutcomeSchema = z14.object({
10517
- id: z14.string().min(1),
10518
- cli: OutcomeCliSchema,
10519
- /** How the CLI attribution was obtained. Absent on legacy, unmeasured records. */
10520
- cliSource: OutcomeCliSourceSchema.optional(),
10521
- category: TaskCategorySchema,
10522
- model: z14.string().min(1),
10523
- success: z14.boolean(),
10524
- durationMs: z14.number().nonnegative(),
10525
- timestamp: z14.string().min(1),
10526
- qualitySignals: z14.array(z14.string()).optional(),
10527
- failureCategory: OutcomeFailureCategorySchema.optional(),
10528
- errorMessage: z14.string().max(500).optional(),
10529
- source: OutcomeSourceSchema,
10530
- /** Whether this outcome came from a triage-initiated retry (#1506). */
10531
- wasRetried: z14.boolean().optional(),
10532
- /** Triage action taken on the failure (#1506). */
10533
- triageAction: z14.string().max(30).optional(),
10534
- /** Routing stage that selected this CLI (#1785). */
10535
- routingStage: z14.string().max(50).optional(),
10536
- /** Number of retry attempts before this outcome (#1785). */
10537
- retryCount: z14.number().int().nonnegative().optional(),
10538
- /** Vendor resolved from `model` via ModelRegistry at write time (#2548). */
10539
- vendor: z14.string().min(1).max(40).optional(),
10540
- /** Family resolved from `model` via ModelRegistry at write time (#2548). */
10541
- family: z14.string().min(1).max(80).optional(),
10542
- /**
10543
- * Voter role for `source: 'consensus'` outcomes (#2662) — `architect`,
10544
- * `security`, etc. Absent on non-consensus outcomes. Lets the
10545
- * stratified outcome report break results down by voter role.
10269
+ * Voter role for `source: 'consensus'` outcomes (#2662) — `architect`,
10270
+ * `security`, etc. Absent on non-consensus outcomes. Lets the
10271
+ * stratified outcome report break results down by voter role.
10546
10272
  */
10547
10273
  voterRole: z14.string().min(1).max(40).optional(),
10548
10274
  /**
@@ -10766,6 +10492,411 @@ function categorizeOutcomeErrorMessage(msg) {
10766
10492
  return classifyText(msg.toLowerCase());
10767
10493
  }
10768
10494
 
10495
+ // src/learning/distiller-eligibility.ts
10496
+ var ROUTABLE_CLIS = new Set(CLI_NAMES);
10497
+ function isDistillerEligible(outcome) {
10498
+ if (outcome.source !== "delegate") return false;
10499
+ if (outcome.cliSource !== "executed") return false;
10500
+ if (!(outcome.durationMs > 0)) return false;
10501
+ return ROUTABLE_CLIS.has(outcome.cli);
10502
+ }
10503
+ function countEligibleSince(outcomes, sinceMs) {
10504
+ let count = 0;
10505
+ for (const outcome of outcomes) {
10506
+ if (!isDistillerEligible(outcome)) continue;
10507
+ if (sinceMs !== void 0 && !(Date.parse(outcome.timestamp) > sinceMs)) continue;
10508
+ count++;
10509
+ }
10510
+ return count;
10511
+ }
10512
+ var eligibilityFieldsSchema;
10513
+ function getEligibilityFieldsSchema() {
10514
+ eligibilityFieldsSchema ??= TaskOutcomeSchema.pick({
10515
+ source: true,
10516
+ cli: true,
10517
+ cliSource: true,
10518
+ durationMs: true
10519
+ });
10520
+ return eligibilityFieldsSchema;
10521
+ }
10522
+ function parseLine(line) {
10523
+ try {
10524
+ const parsed = getEligibilityFieldsSchema().safeParse(JSON.parse(line));
10525
+ return parsed.success ? parsed.data : void 0;
10526
+ } catch {
10527
+ return void 0;
10528
+ }
10529
+ }
10530
+ function countEligibleOutcomesInFile(filePath) {
10531
+ if (!existsSync6(filePath)) return 0;
10532
+ let count = 0;
10533
+ for (const line of readFileSync6(filePath, "utf-8").split("\n")) {
10534
+ if (line.trim().length === 0) continue;
10535
+ const fields = parseLine(line);
10536
+ if (fields !== void 0 && isDistillerEligible(fields)) count++;
10537
+ }
10538
+ return count;
10539
+ }
10540
+
10541
+ // src/learning/strategy-distiller.ts
10542
+ function sigmoidConfidence(observations, center = 30) {
10543
+ const raw = 1 / (1 + Math.exp(-(observations - center) / 5));
10544
+ return Math.max(0, Math.min(1, raw));
10545
+ }
10546
+ function clampUnit(value) {
10547
+ if (!Number.isFinite(value)) return 0;
10548
+ return Math.max(0, Math.min(1, value));
10549
+ }
10550
+ function effectFor(patternType, metric, thresholds) {
10551
+ switch (patternType) {
10552
+ case "failure-rate":
10553
+ return clampUnit(
10554
+ (metric - thresholds.failureRateThreshold) / (1 - thresholds.failureRateThreshold)
10555
+ );
10556
+ case "success-rate":
10557
+ return clampUnit(
10558
+ (metric - thresholds.successRateThreshold) / (1 - thresholds.successRateThreshold)
10559
+ );
10560
+ case "latency-spike":
10561
+ return clampUnit(
10562
+ (metric - thresholds.latencyRatioThreshold) / thresholds.latencyRatioThreshold
10563
+ );
10564
+ }
10565
+ }
10566
+ function ruleFingerprint(patternType, cli, category) {
10567
+ return `${patternType}:${cli}:${category}`;
10568
+ }
10569
+ function groupOutcomes(outcomes) {
10570
+ const map2 = /* @__PURE__ */ new Map();
10571
+ for (const o of outcomes) {
10572
+ const key = `${o.cli}:${o.category}`;
10573
+ const list = map2.get(key);
10574
+ if (list !== void 0) {
10575
+ list.push(o);
10576
+ } else {
10577
+ map2.set(key, [o]);
10578
+ }
10579
+ }
10580
+ const groups = [];
10581
+ for (const [key, list] of map2) {
10582
+ const [cli, category] = key.split(":");
10583
+ groups.push({ cli, category, outcomes: list });
10584
+ }
10585
+ return groups;
10586
+ }
10587
+ function percentile(sorted, p) {
10588
+ if (sorted.length === 0) return 0;
10589
+ const idx = Math.ceil(p / 100 * sorted.length) - 1;
10590
+ return sorted[Math.max(0, idx)] ?? 0;
10591
+ }
10592
+ function detectFailurePatterns(groups, threshold) {
10593
+ const patterns = [];
10594
+ for (const g of groups) {
10595
+ const failCount = g.outcomes.filter((o) => !o.success).length;
10596
+ const failRate = failCount / g.outcomes.length;
10597
+ if (failRate >= threshold) {
10598
+ patterns.push({
10599
+ cli: g.cli,
10600
+ category: g.category,
10601
+ patternType: "failure-rate",
10602
+ action: failRate >= 0.8 ? "avoid" : "penalize",
10603
+ metric: failRate,
10604
+ observationCount: g.outcomes.length
10605
+ });
10606
+ }
10607
+ }
10608
+ return patterns;
10609
+ }
10610
+ function detectSuccessPatterns(groups, threshold) {
10611
+ const patterns = [];
10612
+ for (const g of groups) {
10613
+ const successCount = g.outcomes.filter((o) => o.success).length;
10614
+ const successRate3 = successCount / g.outcomes.length;
10615
+ if (successRate3 >= threshold) {
10616
+ patterns.push({
10617
+ cli: g.cli,
10618
+ category: g.category,
10619
+ patternType: "success-rate",
10620
+ action: "boost",
10621
+ metric: successRate3,
10622
+ observationCount: g.outcomes.length
10623
+ });
10624
+ }
10625
+ }
10626
+ return patterns;
10627
+ }
10628
+ function detectLatencyPatterns(groups, threshold) {
10629
+ const patterns = [];
10630
+ for (const g of groups) {
10631
+ const durations = g.outcomes.map((o) => o.durationMs).sort((a, b) => a - b);
10632
+ if (durations.length < 3) continue;
10633
+ const median = percentile(durations, 50);
10634
+ const p90 = percentile(durations, 90);
10635
+ if (median <= 0) continue;
10636
+ const ratio = p90 / median;
10637
+ if (ratio >= threshold) {
10638
+ patterns.push({
10639
+ cli: g.cli,
10640
+ category: g.category,
10641
+ patternType: "latency-spike",
10642
+ action: "penalize",
10643
+ metric: ratio,
10644
+ observationCount: g.outcomes.length
10645
+ });
10646
+ }
10647
+ }
10648
+ return patterns;
10649
+ }
10650
+ var StrategyDistiller = class {
10651
+ /** Protected so `PersistentStrategyDistiller` hydrates legacy rules under the live thresholds. */
10652
+ config;
10653
+ outcomeStore;
10654
+ logger;
10655
+ rules = /* @__PURE__ */ new Map();
10656
+ outcomeCounter = 0;
10657
+ lastDistillAt;
10658
+ eligibleAtLastDistill;
10659
+ persistedTriggerChecked = false;
10660
+ constructor(outcomeStore, logger15, config) {
10661
+ this.outcomeStore = outcomeStore;
10662
+ this.config = { ...DEFAULT_DISTILLER_CONFIG, ...config };
10663
+ this.logger = logger15 ?? createLogger({ component: "StrategyDistiller" });
10664
+ }
10665
+ /**
10666
+ * Called for each processed outcome. Triggers distillation at threshold.
10667
+ * This counter is per-process; `checkPersistedTrigger` covers the
10668
+ * cross-process case (#6512).
10669
+ */
10670
+ onOutcome() {
10671
+ this.outcomeCounter++;
10672
+ if (this.outcomeCounter >= this.config.triggerThreshold) {
10673
+ this.distill();
10674
+ this.outcomeCounter = 0;
10675
+ }
10676
+ }
10677
+ /**
10678
+ * First-route trigger against the persisted store (#6512).
10679
+ *
10680
+ * The in-process counter above only fires after `triggerThreshold` outcomes
10681
+ * in ONE process, which short-lived CLI processes never reach. This checks
10682
+ * the store instead: distill when it holds at least `triggerThreshold`
10683
+ * eligible outcomes newer than the last distill (a hydrated snapshot's
10684
+ * `savedAt`; no snapshot means all of them count). With no snapshot and zero
10685
+ * eligible outcomes it distills anyway, so the empty state is recorded as
10686
+ * "0 eligible, 0 rules, at <time>" rather than left as "never".
10687
+ *
10688
+ * Before deciding, rules past `ruleExpiryMs` are expired, so a hydrated rule
10689
+ * that is out of date is never applied.
10690
+ *
10691
+ * Runs at most once per instance; `DistilledRuleStage` calls it when it first
10692
+ * runs, so constructing a distiller stays cheap. Returns whether it distilled.
10693
+ */
10694
+ checkPersistedTrigger() {
10695
+ if (this.persistedTriggerChecked) return false;
10696
+ this.persistedTriggerChecked = true;
10697
+ this.expireRules(getTimeProvider().now());
10698
+ const newer = countEligibleSince(this.outcomeStore.query(), this.lastDistillAt);
10699
+ const recordEmpty = this.lastDistillAt === void 0 && newer === 0;
10700
+ if (newer < this.config.triggerThreshold && !recordEmpty) return false;
10701
+ this.logger.debug("Persisted-store distill trigger fired", {
10702
+ eligibleSinceLastDistill: newer,
10703
+ triggerThreshold: this.config.triggerThreshold
10704
+ });
10705
+ this.distill();
10706
+ return true;
10707
+ }
10708
+ /** Run distillation on the eligible outcomes in the OutcomeStore (#6512). */
10709
+ distill() {
10710
+ const now = getTimeProvider().now();
10711
+ const outcomes = this.outcomeStore.query().filter(isDistillerEligible);
10712
+ const groups = groupOutcomes(outcomes);
10713
+ this.expireRules(now);
10714
+ const failurePatterns = detectFailurePatterns(groups, this.config.failureRateThreshold);
10715
+ const successPatterns = detectSuccessPatterns(groups, this.config.successRateThreshold);
10716
+ const latencyPatterns = detectLatencyPatterns(groups, this.config.latencyRatioThreshold);
10717
+ const allPatterns = [...failurePatterns, ...successPatterns, ...latencyPatterns];
10718
+ for (const pattern of allPatterns) {
10719
+ this.upsertRule(pattern, now);
10720
+ }
10721
+ this.enforceMaxRules();
10722
+ this.lastDistillAt = now;
10723
+ this.eligibleAtLastDistill = outcomes.length;
10724
+ this.logger.debug("Distillation complete", {
10725
+ rulesTotal: this.rules.size,
10726
+ patternsFound: allPatterns.length,
10727
+ outcomesAnalyzed: outcomes.length
10728
+ });
10729
+ }
10730
+ /** Get rules filtered by status. */
10731
+ getRules(status) {
10732
+ const all = [...this.rules.values()];
10733
+ if (status === void 0) return all;
10734
+ return all.filter((r) => r.status === status);
10735
+ }
10736
+ /** Get distiller statistics. */
10737
+ getStats() {
10738
+ const countByStatus = {
10739
+ draft: 0,
10740
+ active: 0,
10741
+ promoted: 0,
10742
+ expired: 0
10743
+ };
10744
+ for (const rule of this.rules.values()) {
10745
+ countByStatus[rule.status]++;
10746
+ }
10747
+ return {
10748
+ ruleCountByStatus: countByStatus,
10749
+ totalRules: this.rules.size,
10750
+ lastDistillAt: this.lastDistillAt,
10751
+ outcomesSinceLastDistill: this.outcomeCounter,
10752
+ eligibleOutcomesAtLastDistill: this.eligibleAtLastDistill
10753
+ };
10754
+ }
10755
+ /**
10756
+ * Promote high-confidence rules to RoutingMemory.
10757
+ * Rules must be active, with sufficient observations and confidence.
10758
+ *
10759
+ * @deprecated No production caller (#5004 finding 4). `DistilledRuleStage`
10760
+ * is the single channel by which distilled rules reach routing; this
10761
+ * second channel into `RoutingMemory` is kept only so the deprecation is
10762
+ * non-breaking. Removal is tracked in #5467. Note the gate now compares
10763
+ * `confidence = support × effect` against `promotionConfidence`, so a rule
10764
+ * that would have promoted on sample size alone may no longer clear it.
10765
+ */
10766
+ promote(routingMemory) {
10767
+ let promoted = 0;
10768
+ for (const [id, rule] of this.rules) {
10769
+ if (rule.status !== "active") continue;
10770
+ if (rule.observationCount < this.config.minObservationsForActive) continue;
10771
+ if (rule.confidence < this.config.promotionConfidence) continue;
10772
+ const performance2 = this.ruleToPerformance(rule);
10773
+ routingMemory.storePreference(rule.cli, rule.category, performance2);
10774
+ this.rules.set(id, { ...rule, status: "promoted", updatedAt: getTimeProvider().now() });
10775
+ promoted++;
10776
+ this.logger.info("Promoted distilled rule to RoutingMemory", {
10777
+ ruleId: id,
10778
+ cli: rule.cli,
10779
+ category: rule.category,
10780
+ confidence: rule.confidence
10781
+ });
10782
+ }
10783
+ return promoted;
10784
+ }
10785
+ // ==========================================================================
10786
+ // Protected — for subclass hydration (Issue #1009)
10787
+ // ==========================================================================
10788
+ /** Load pre-existing rules (e.g., from disk). Used by PersistentStrategyDistiller. */
10789
+ loadRules(rules) {
10790
+ for (const rule of rules) {
10791
+ this.rules.set(rule.id, rule);
10792
+ }
10793
+ }
10794
+ /**
10795
+ * Restore last-distill state from a persisted snapshot (#6512), so the
10796
+ * first-route trigger counts only outcomes newer than that snapshot.
10797
+ */
10798
+ restoreDistillState(lastDistillAt, eligibleOutcomes) {
10799
+ this.lastDistillAt = lastDistillAt;
10800
+ this.eligibleAtLastDistill = eligibleOutcomes;
10801
+ }
10802
+ // ==========================================================================
10803
+ // Private
10804
+ // ==========================================================================
10805
+ upsertRule(pattern, now) {
10806
+ const id = ruleFingerprint(pattern.patternType, pattern.cli, pattern.category);
10807
+ const existing = this.rules.get(id);
10808
+ if (existing === void 0 && pattern.observationCount < this.config.minObservationsForDraft) {
10809
+ return;
10810
+ }
10811
+ const support = sigmoidConfidence(pattern.observationCount);
10812
+ const effect = effectFor(pattern.patternType, pattern.metric, this.config);
10813
+ const confidence = support * effect;
10814
+ const status = this.computeStatus(pattern.observationCount, existing?.status);
10815
+ if (existing !== void 0) {
10816
+ this.rules.set(id, {
10817
+ ...existing,
10818
+ action: pattern.action,
10819
+ confidence,
10820
+ support,
10821
+ effect,
10822
+ observationCount: pattern.observationCount,
10823
+ metric: pattern.metric,
10824
+ status,
10825
+ updatedAt: now
10826
+ });
10827
+ } else {
10828
+ this.rules.set(id, {
10829
+ id,
10830
+ patternType: pattern.patternType,
10831
+ cli: pattern.cli,
10832
+ category: pattern.category,
10833
+ action: pattern.action,
10834
+ confidence,
10835
+ support,
10836
+ effect,
10837
+ observationCount: pattern.observationCount,
10838
+ metric: pattern.metric,
10839
+ status,
10840
+ createdAt: now,
10841
+ updatedAt: now,
10842
+ tainted: false
10843
+ });
10844
+ }
10845
+ }
10846
+ computeStatus(observations, existing) {
10847
+ if (existing === "promoted") return "promoted";
10848
+ if (observations >= this.config.minObservationsForActive) return "active";
10849
+ return "draft";
10850
+ }
10851
+ expireRules(now) {
10852
+ for (const [id, rule] of this.rules) {
10853
+ if (rule.status === "promoted") continue;
10854
+ if (now - rule.updatedAt > this.config.ruleExpiryMs) {
10855
+ this.rules.set(id, { ...rule, status: "expired", updatedAt: now });
10856
+ }
10857
+ }
10858
+ }
10859
+ enforceMaxRules() {
10860
+ if (this.rules.size <= this.config.maxRules) return;
10861
+ const sorted = [...this.rules.entries()].sort((a, b) => {
10862
+ if (a[1].status === "expired" && b[1].status !== "expired") return -1;
10863
+ if (b[1].status === "expired" && a[1].status !== "expired") return 1;
10864
+ return a[1].confidence - b[1].confidence;
10865
+ });
10866
+ const excess = this.rules.size - this.config.maxRules;
10867
+ for (let i = 0; i < excess; i++) {
10868
+ const entry = sorted[i];
10869
+ if (entry !== void 0) {
10870
+ this.rules.delete(entry[0]);
10871
+ }
10872
+ }
10873
+ }
10874
+ /** Convert rule metrics into ModelPerformance for RoutingMemory. */
10875
+ ruleToPerformance(rule) {
10876
+ const successRate3 = rule.patternType === "success-rate" ? rule.metric : 1 - rule.metric;
10877
+ return {
10878
+ avgQuality: rule.confidence,
10879
+ successRate: Math.max(0, Math.min(1, successRate3)),
10880
+ avgLatencyMs: 0,
10881
+ avgTokens: 0,
10882
+ observations: rule.observationCount
10883
+ };
10884
+ }
10885
+ };
10886
+ function createStrategyDistiller(outcomeStore, logger15, config) {
10887
+ return new StrategyDistiller(outcomeStore, logger15, config);
10888
+ }
10889
+ var persistentDistillerFactory;
10890
+ function registerPersistentDistillerFactory(factory) {
10891
+ persistentDistillerFactory = factory;
10892
+ }
10893
+ function createPersistentDistillerOrFallback(outcomeStore, logger15) {
10894
+ if (persistentDistillerFactory !== void 0) {
10895
+ return persistentDistillerFactory(outcomeStore, logger15);
10896
+ }
10897
+ return new StrategyDistiller(outcomeStore, logger15);
10898
+ }
10899
+
10769
10900
  // src/orchestration/outcomes/outcome-store.ts
10770
10901
  var DEFAULT_MAX_ENTRIES2 = 1e4;
10771
10902
  var DEFAULT_FAMILY_FALLBACK_THRESHOLD = 5;
@@ -12860,7 +12991,7 @@ function aggregateDecisionCosts(records, windowMs) {
12860
12991
  import { z as z18 } from "zod";
12861
12992
 
12862
12993
  // src/config/jsonl-store.ts
12863
- import { appendFileSync as appendFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync } from "node:fs";
12994
+ import { appendFileSync as appendFileSync3, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync } from "node:fs";
12864
12995
  import { dirname as dirname5 } from "node:path";
12865
12996
  var DIR_MODE = 448;
12866
12997
  var JsonlStore = class {
@@ -12923,14 +13054,14 @@ var JsonlStore = class {
12923
13054
  }
12924
13055
  }
12925
13056
  hydrate() {
12926
- if (!existsSync6(this.filePath)) {
13057
+ if (!existsSync7(this.filePath)) {
12927
13058
  this.logger.debug("No JSONL file found, starting fresh", { path: this.filePath });
12928
13059
  return;
12929
13060
  }
12930
13061
  let loaded = 0;
12931
13062
  let skipped = 0;
12932
13063
  try {
12933
- const content = readFileSync6(this.filePath, "utf-8");
13064
+ const content = readFileSync7(this.filePath, "utf-8");
12934
13065
  const lines = content.split("\n").filter((line) => line.trim().length > 0);
12935
13066
  for (const line of lines) {
12936
13067
  try {
@@ -15202,7 +15333,7 @@ function resolveModelForTier(cliName, tier, opts = {}) {
15202
15333
  }
15203
15334
 
15204
15335
  // src/cli-adapters/model-selection-shadow.ts
15205
- import { appendFileSync as appendFileSync4, existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
15336
+ import { appendFileSync as appendFileSync4, existsSync as existsSync8, readFileSync as readFileSync8 } from "node:fs";
15206
15337
  import { z as z22 } from "zod";
15207
15338
  function isRouteModelShadowEnabled() {
15208
15339
  return parseBoolEnv("NEXUS_ROUTE_MODEL_SHADOW", false) && isPersistenceEnabled();
@@ -15253,11 +15384,11 @@ function persistModelSelectionShadowRecord(record) {
15253
15384
  }
15254
15385
  function readModelSelectionShadowRecords() {
15255
15386
  const file = getModelSelectionShadowFile();
15256
- if (!existsSync7(file)) return [];
15387
+ if (!existsSync8(file)) return [];
15257
15388
  const records = [];
15258
15389
  try {
15259
15390
  const cutoff = Date.now() - READ_LOOKBACK_MS;
15260
- const lines = readFileSync7(file, "utf-8").split("\n").filter((l) => l.trim().length > 0);
15391
+ const lines = readFileSync8(file, "utf-8").split("\n").filter((l) => l.trim().length > 0);
15261
15392
  for (const line of lines) {
15262
15393
  let parsed;
15263
15394
  try {
@@ -15612,6 +15743,9 @@ function generateTraceId2() {
15612
15743
  // src/cli-adapters/composite-router.ts
15613
15744
  var FAILURE_QUALITY_SCORE = 0.3;
15614
15745
  var DEFAULT_TOKEN_ESTIMATE = 1e3;
15746
+ function applyDistillationSwitch(config) {
15747
+ return isStrategyDistillationEnabled() ? config : { ...config, enableStrategyDistillation: false };
15748
+ }
15615
15749
  var CompositeRouter = class _CompositeRouter {
15616
15750
  config;
15617
15751
  logger;
@@ -15694,7 +15828,7 @@ var CompositeRouter = class _CompositeRouter {
15694
15828
  availableModelsCache,
15695
15829
  ...baseConfig
15696
15830
  } = config ?? {};
15697
- this.config = CompositeRouterConfigSchema.parse(baseConfig);
15831
+ this.config = applyDistillationSwitch(CompositeRouterConfigSchema.parse(baseConfig));
15698
15832
  this.logger = logger15 ?? createLogger({ component: "CompositeRouter" });
15699
15833
  this.pendingRoutingOutcomes = new PendingRoutingOutcomes(this.logger);
15700
15834
  this.adapters = adapters;
@@ -15880,7 +16014,7 @@ var CompositeRouter = class _CompositeRouter {
15880
16014
  */
15881
16015
  async consultUnifiedContext(task) {
15882
16016
  try {
15883
- const { getContextForTask, inferTaskCategory } = await import("./context-retriever-CH3GEH2Z.js");
16017
+ const { getContextForTask, inferTaskCategory } = await import("./context-retriever-NZOSGG6S.js");
15884
16018
  const ctx = await getContextForTask({
15885
16019
  task: task.content,
15886
16020
  category: inferTaskCategory(task.content),
@@ -17463,6 +17597,7 @@ export {
17463
17597
  RoutingMemory,
17464
17598
  createRoutingMemory,
17465
17599
  DEFAULT_DISTILLER_CONFIG,
17600
+ countEligibleOutcomesInFile,
17466
17601
  sigmoidConfidence,
17467
17602
  effectFor,
17468
17603
  detectFailurePatterns,
@@ -17515,4 +17650,4 @@ export {
17515
17650
  AgentCapability,
17516
17651
  OrchestratorError
17517
17652
  };
17518
- //# sourceMappingURL=chunk-YGOTLETF.js.map
17653
+ //# sourceMappingURL=chunk-64IJKT4I.js.map