@remnic/core 10.0.73 → 10.0.75

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 (326) hide show
  1. package/dist/access-admin-ops-surface.js +49 -48
  2. package/dist/access-authorization-probe.js +50 -49
  3. package/dist/access-boundary.js +49 -48
  4. package/dist/access-cli.js +105 -104
  5. package/dist/access-cli.js.map +1 -1
  6. package/dist/access-extraction-force-flush.js +49 -48
  7. package/dist/access-http-briefing.js +50 -49
  8. package/dist/access-http-query.js +51 -50
  9. package/dist/access-http.js +68 -67
  10. package/dist/access-identity-continuity-surface.js +49 -48
  11. package/dist/access-lcm-surface.js +49 -48
  12. package/dist/access-mcp.js +58 -57
  13. package/dist/access-namespace-preflight.js +49 -48
  14. package/dist/access-observe-helpers.d.ts.map +1 -1
  15. package/dist/access-observe-helpers.js +1 -1
  16. package/dist/access-observe-write-category.js +6 -6
  17. package/dist/access-observe-write-surface.js +49 -48
  18. package/dist/access-offline-manifest.js +5 -5
  19. package/dist/access-operations-batch.js +50 -49
  20. package/dist/access-operations.js +57 -56
  21. package/dist/access-recall-concurrency.js +49 -48
  22. package/dist/access-recall-response.js +49 -48
  23. package/dist/access-recall-surface.js +49 -48
  24. package/dist/access-schema.js +5 -5
  25. package/dist/access-service.js +49 -48
  26. package/dist/access-surface-catalog.js +51 -50
  27. package/dist/access-surface-catalog.js.map +1 -1
  28. package/dist/active-recall.js +18 -18
  29. package/dist/adapters/index.js +4 -4
  30. package/dist/adapters/registry.js +2 -2
  31. package/dist/{auto-sync-ANOJDQD4.js → auto-sync-XHSS3XPL.js} +5 -5
  32. package/dist/briefing.js +15 -15
  33. package/dist/buffer-turn-helpers.d.ts +18 -0
  34. package/dist/buffer-turn-helpers.d.ts.map +1 -1
  35. package/dist/buffer-turn-helpers.js +5 -1
  36. package/dist/buffer.js +4 -4
  37. package/dist/calibration.js +5 -4
  38. package/dist/calibration.js.map +1 -1
  39. package/dist/causal-consolidation.js +16 -16
  40. package/dist/{chunk-7WFY2L5J.js → chunk-233S3MXC.js} +3 -3
  41. package/dist/{chunk-N7NAGECH.js → chunk-36B5VPRU.js} +2 -2
  42. package/dist/{chunk-5ORJHH43.js → chunk-3GGCMRO7.js} +2 -2
  43. package/dist/{chunk-RZED7FAU.js → chunk-3M7YP42P.js} +2 -2
  44. package/dist/{chunk-ICBUTKLL.js → chunk-3MBSD53O.js} +2 -2
  45. package/dist/{chunk-J72ZY7NU.js → chunk-4DLZ2ITE.js} +2 -2
  46. package/dist/{chunk-OUBEJWVI.js → chunk-4KLQI3L2.js} +42 -1
  47. package/dist/chunk-4KLQI3L2.js.map +1 -0
  48. package/dist/{chunk-WN2DASTS.js → chunk-5BE3FLKL.js} +4 -4
  49. package/dist/{chunk-S73ZVU37.js → chunk-5IQL4N37.js} +10 -10
  50. package/dist/{chunk-CIL4GHMD.js → chunk-6Q6AQ36B.js} +2 -2
  51. package/dist/{chunk-W4V7P5HP.js → chunk-6RPPZL7W.js} +2 -2
  52. package/dist/{chunk-EQ6EMJRG.js → chunk-6Y5ANLDH.js} +2 -2
  53. package/dist/{chunk-YPYSC3TX.js → chunk-7FFGWIBN.js} +2 -2
  54. package/dist/{chunk-VNTQU6KB.js → chunk-7PMOCQOL.js} +8 -13
  55. package/dist/chunk-7PMOCQOL.js.map +1 -0
  56. package/dist/{chunk-5RW247F5.js → chunk-AY2UK5XK.js} +14 -9
  57. package/dist/chunk-AY2UK5XK.js.map +1 -0
  58. package/dist/{chunk-5QV6CE6K.js → chunk-B36ACJG7.js} +2 -2
  59. package/dist/{chunk-ZBET3W26.js → chunk-BZTA7DR4.js} +3 -3
  60. package/dist/{chunk-JY4LXMZR.js → chunk-C44DRDZM.js} +2 -2
  61. package/dist/{chunk-UIFWZMO7.js → chunk-CLVILSBR.js} +7 -7
  62. package/dist/{chunk-OGQGDOYK.js → chunk-COIQ5N4J.js} +2 -2
  63. package/dist/{chunk-UAOJVQXC.js → chunk-DANFOVQG.js} +2 -2
  64. package/dist/{chunk-2R44BBPK.js → chunk-DEGE32C3.js} +2 -2
  65. package/dist/{chunk-YIJECSIX.js → chunk-DJDJBMFX.js} +9 -2
  66. package/dist/chunk-DJDJBMFX.js.map +1 -0
  67. package/dist/{chunk-LHMCZ6VW.js → chunk-E22CQZ3H.js} +14 -14
  68. package/dist/{chunk-JGLZ46JS.js → chunk-ELUG6NKQ.js} +2 -2
  69. package/dist/{chunk-X4DHAXFO.js → chunk-F3FNXE7C.js} +3 -3
  70. package/dist/{chunk-YIXWY4OZ.js → chunk-FETPTCQZ.js} +2 -2
  71. package/dist/{chunk-N2Y6COEM.js → chunk-FGUWCE5I.js} +2 -2
  72. package/dist/{chunk-FDUCIUQ5.js → chunk-FJ2VJTGA.js} +2 -2
  73. package/dist/{chunk-ENPJX3BB.js → chunk-FMFCQRDM.js} +3 -3
  74. package/dist/{chunk-BNU5TQLR.js → chunk-FOQ7TW2T.js} +2 -2
  75. package/dist/{chunk-4PMFSYH7.js → chunk-FP2AT4WL.js} +2 -2
  76. package/dist/{chunk-DLZGSZVC.js → chunk-FYQUUIPI.js} +4 -4
  77. package/dist/{chunk-XTN54NML.js → chunk-G3T6YMBO.js} +4 -4
  78. package/dist/{chunk-MUH7RR6T.js → chunk-G5NH3SSX.js} +5 -5
  79. package/dist/{chunk-DC3DWF2M.js → chunk-GCJA5KKT.js} +2 -2
  80. package/dist/{chunk-U6IL4724.js → chunk-GWOFCHPN.js} +2 -2
  81. package/dist/{chunk-TJ5YGCPV.js → chunk-GXVK754R.js} +2 -2
  82. package/dist/{chunk-FGGLVCHI.js → chunk-HB5TNQMM.js} +2 -2
  83. package/dist/{chunk-GXAZ4Q2S.js → chunk-HCFNZWFB.js} +2 -2
  84. package/dist/{chunk-F4ZHKDBW.js → chunk-HLWXCDUQ.js} +2 -2
  85. package/dist/{chunk-IQXQ3KWR.js → chunk-HTDXNIO6.js} +2 -2
  86. package/dist/{chunk-4RASUAN3.js → chunk-HXFIILGF.js} +4 -4
  87. package/dist/{chunk-S37O4S6E.js → chunk-I4LJJ44E.js} +2 -2
  88. package/dist/{chunk-USX5YIKB.js → chunk-IBQ7LGUJ.js} +6 -6
  89. package/dist/{chunk-UKQFCOGT.js → chunk-IPHIIRDN.js} +3 -3
  90. package/dist/chunk-IPHIIRDN.js.map +1 -0
  91. package/dist/{chunk-VSJ3ARFS.js → chunk-IRFQKR4Y.js} +2 -2
  92. package/dist/{chunk-BCBY5GYZ.js → chunk-IVXOYE43.js} +2 -2
  93. package/dist/{chunk-WXPBDEMW.js → chunk-JDQEQE2R.js} +2 -2
  94. package/dist/{chunk-2WPWEVAH.js → chunk-KAED7IBB.js} +2 -2
  95. package/dist/{chunk-OWZXDBV4.js → chunk-KNZDGZSY.js} +2 -2
  96. package/dist/{chunk-6KBCUIZP.js → chunk-L4QYW2HO.js} +2 -2
  97. package/dist/{chunk-LYOCAUI6.js → chunk-LJUQPT5G.js} +3 -3
  98. package/dist/{chunk-XPZM7GP6.js → chunk-MBENRPMY.js} +10 -10
  99. package/dist/chunk-MJFL75J6.js +42 -0
  100. package/dist/chunk-MJFL75J6.js.map +1 -0
  101. package/dist/{chunk-VK5HKVQ7.js → chunk-N6GEGRCT.js} +2 -2
  102. package/dist/{chunk-U2A56LLF.js → chunk-OBQ2NZAR.js} +10 -10
  103. package/dist/{chunk-WGRAB3KB.js → chunk-ORVPS4ZW.js} +2 -2
  104. package/dist/{chunk-PFUXPTBT.js → chunk-P3AQ2YSX.js} +4 -4
  105. package/dist/{chunk-DE4U5PTV.js → chunk-P4TEQSIZ.js} +2 -2
  106. package/dist/{chunk-UE5JYPU6.js → chunk-P7HNMNN2.js} +7 -7
  107. package/dist/{chunk-ZG4GNPRE.js → chunk-PBE7NFB5.js} +5 -5
  108. package/dist/{chunk-LN7CWLAU.js → chunk-PLGBPFEO.js} +2 -2
  109. package/dist/{chunk-5MA7ZLPD.js → chunk-QNUNAXBE.js} +35 -1
  110. package/dist/chunk-QNUNAXBE.js.map +1 -0
  111. package/dist/{chunk-23BA3USG.js → chunk-R24RYSQX.js} +43 -43
  112. package/dist/{chunk-WYOHQJKC.js → chunk-RXGFITWS.js} +4 -4
  113. package/dist/{chunk-JEX2JL3U.js → chunk-SHVLNI4L.js} +2 -2
  114. package/dist/{chunk-QUPTDKSX.js → chunk-SY4AFWUZ.js} +31 -31
  115. package/dist/{chunk-H2X32F2K.js → chunk-SYDPJUFR.js} +2 -2
  116. package/dist/{chunk-QIOQI7QZ.js → chunk-SZBZOUHR.js} +2 -2
  117. package/dist/{chunk-EGKDONTC.js → chunk-T6C3G67T.js} +10 -10
  118. package/dist/chunk-T6C3G67T.js.map +1 -0
  119. package/dist/{chunk-E3NBMTTX.js → chunk-TLTBACYX.js} +2 -2
  120. package/dist/{chunk-AVVCPA7U.js → chunk-UCPH56YX.js} +2 -2
  121. package/dist/{chunk-ZODKV27J.js → chunk-URS56HGW.js} +6 -6
  122. package/dist/{chunk-PJAMFIDE.js → chunk-VEQVXNWR.js} +4 -4
  123. package/dist/{chunk-AGRMXXVA.js → chunk-VEWTHVX2.js} +2 -2
  124. package/dist/{chunk-WNQ3B3Q5.js → chunk-VPEMGBTI.js} +2 -2
  125. package/dist/{chunk-DSSTOV5M.js → chunk-VYOAYFDV.js} +66 -49
  126. package/dist/chunk-VYOAYFDV.js.map +1 -0
  127. package/dist/{chunk-5JBVPJCT.js → chunk-WIAXVBQH.js} +2 -2
  128. package/dist/{chunk-BTU77RB5.js → chunk-X53HJENJ.js} +2 -2
  129. package/dist/{chunk-KY3KJUNN.js → chunk-XC7OYOQC.js} +6 -6
  130. package/dist/{chunk-43FLJLT2.js → chunk-Y5HZFIQO.js} +1 -1
  131. package/dist/{chunk-DOJBV6WR.js → chunk-YK6SFDP2.js} +4 -4
  132. package/dist/{chunk-QVZZE6VH.js → chunk-Z3CKK5RR.js} +3 -3
  133. package/dist/{chunk-AZNE2H6D.js → chunk-ZG57BEUS.js} +2 -2
  134. package/dist/{chunk-7P5VBRBS.js → chunk-ZZSFRWKD.js} +2 -2
  135. package/dist/cli.js +81 -80
  136. package/dist/coding/export-okf-codegraph.js +14 -14
  137. package/dist/compounding/engine.js +16 -16
  138. package/dist/config.js +18 -18
  139. package/dist/connectors/codex-materialize-runner.js +15 -15
  140. package/dist/connectors/index.js +15 -15
  141. package/dist/convergence-refresh.js +17 -17
  142. package/dist/corpus-watermark.js +17 -17
  143. package/dist/deep-recall-http-glue.js +6 -6
  144. package/dist/deep-recall-policy-llm.js +3 -3
  145. package/dist/deep-recall.js +2 -2
  146. package/dist/entity-retrieval.js +15 -15
  147. package/dist/external-wiki-access.js +50 -49
  148. package/dist/extraction-faithfulness.js +3 -3
  149. package/dist/extraction-judge.js +3 -3
  150. package/dist/extraction.js +23 -22
  151. package/dist/fallback-llm-codex-subscription.d.ts.map +1 -1
  152. package/dist/fallback-llm-codex-subscription.js +1 -1
  153. package/dist/fallback-llm.js +2 -2
  154. package/dist/{first-start-migration-A6PHIBUQ.js → first-start-migration-QVD22WWS.js} +4 -4
  155. package/dist/harmonic-retrieval.js +15 -15
  156. package/dist/index.js +130 -129
  157. package/dist/lcm/engine.js +2 -2
  158. package/dist/lcm/index.js +5 -5
  159. package/dist/local-llm-native-schema.d.ts +50 -0
  160. package/dist/local-llm-native-schema.d.ts.map +1 -0
  161. package/dist/local-llm-native-schema.js +8 -0
  162. package/dist/local-llm-native-schema.js.map +1 -0
  163. package/dist/local-llm.d.ts +1 -0
  164. package/dist/local-llm.d.ts.map +1 -1
  165. package/dist/local-llm.js +4 -3
  166. package/dist/location/index.js +17 -17
  167. package/dist/maintenance/memory-governance.js +15 -15
  168. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +15 -15
  169. package/dist/maintenance/rebuild-memory-projection-runner.js +17 -17
  170. package/dist/maintenance/rebuild-memory-projection.js +16 -16
  171. package/dist/memory-browse-operations.js +50 -49
  172. package/dist/memory-browse.js +3 -3
  173. package/dist/model-registry.d.ts +11 -0
  174. package/dist/model-registry.d.ts.map +1 -1
  175. package/dist/model-registry.js +1 -1
  176. package/dist/namespaces/migrate.js +16 -16
  177. package/dist/namespaces/search.js +7 -7
  178. package/dist/namespaces/storage.js +15 -15
  179. package/dist/offline-sync-changeset-generations.js +3 -3
  180. package/dist/offline-sync-embedding-generation.js +2 -2
  181. package/dist/offline-sync-snapshot-stream.js +5 -5
  182. package/dist/offline-sync.js +7 -7
  183. package/dist/operator-doctor-corpus.js +18 -18
  184. package/dist/operator-toolkit.js +27 -27
  185. package/dist/orchestration/backlog-scheduler.d.ts +8 -0
  186. package/dist/orchestration/backlog-scheduler.d.ts.map +1 -1
  187. package/dist/orchestration/backlog-scheduler.js +118 -106
  188. package/dist/orchestration/backlog-scheduler.js.map +1 -1
  189. package/dist/orchestration/buffer-drain-runner.js +106 -105
  190. package/dist/orchestration/extraction-queue-coordinator.d.ts +6 -0
  191. package/dist/orchestration/extraction-queue-coordinator.d.ts.map +1 -1
  192. package/dist/orchestration/maintenance.js +28 -27
  193. package/dist/orchestrator.js +105 -104
  194. package/dist/recall-navigation-operations.js +50 -49
  195. package/dist/recall-navigation.js +49 -48
  196. package/dist/recall-planner-llm.js +2 -2
  197. package/dist/recall-state-view-admission.js +3 -3
  198. package/dist/recall-why-service.js +3 -3
  199. package/dist/resume-bundles.js +18 -18
  200. package/dist/search/factory.js +6 -6
  201. package/dist/search/index.js +11 -11
  202. package/dist/search/lancedb-backend.js +2 -2
  203. package/dist/search/meilisearch-backend.js +2 -2
  204. package/dist/search/orama-backend.js +2 -2
  205. package/dist/semantic-consolidation.js +16 -16
  206. package/dist/semantic-rule-promotion.js +15 -15
  207. package/dist/semantic-rule-verifier.js +15 -15
  208. package/dist/source-agent-qualifier.js +18 -18
  209. package/dist/standup/build.js +15 -15
  210. package/dist/storage/buffer-drain-claims.d.ts.map +1 -1
  211. package/dist/storage/buffer-drain-claims.js +2 -2
  212. package/dist/storage/buffer-session-files.js +2 -2
  213. package/dist/storage.js +14 -14
  214. package/dist/summarizer.js +7 -6
  215. package/dist/support-passport/index.js +52 -51
  216. package/dist/transfer/export-okf.js +15 -15
  217. package/dist/verified-recall.js +15 -15
  218. package/dist/who-knows-cli.js +16 -16
  219. package/dist/who-knows.js +15 -15
  220. package/package.json +2 -2
  221. package/src/access-observe-acceptance-boundary.test.ts +24 -18
  222. package/src/access-observe-helpers.test.ts +72 -0
  223. package/src/access-observe-helpers.ts +5 -2
  224. package/src/access-service-observe-lcm-parity.test.ts +9 -2
  225. package/src/access-service-observe-scope.test.ts +8 -1
  226. package/src/backlog-scheduler.test.ts +104 -0
  227. package/src/buffer-record-snapshots.test.ts +195 -0
  228. package/src/buffer-turn-helpers.test.ts +205 -0
  229. package/src/buffer-turn-helpers.ts +62 -0
  230. package/src/buffer.ts +8 -8
  231. package/src/fallback-llm-codex-subscription.ts +10 -1
  232. package/src/local-llm-json-schema-fallback.test.ts +315 -0
  233. package/src/local-llm-native-schema.ts +78 -0
  234. package/src/local-llm.ts +8 -9
  235. package/src/model-registry.ts +40 -0
  236. package/src/orchestration/backlog-scheduler.ts +32 -0
  237. package/src/orchestration/extraction-queue-coordinator.ts +18 -1
  238. package/src/orchestrator-extraction-queue.test.ts +142 -0
  239. package/src/storage/buffer-drain-claims.ts +5 -11
  240. package/dist/chunk-5MA7ZLPD.js.map +0 -1
  241. package/dist/chunk-5RW247F5.js.map +0 -1
  242. package/dist/chunk-DSSTOV5M.js.map +0 -1
  243. package/dist/chunk-EGKDONTC.js.map +0 -1
  244. package/dist/chunk-OUBEJWVI.js.map +0 -1
  245. package/dist/chunk-UKQFCOGT.js.map +0 -1
  246. package/dist/chunk-VNTQU6KB.js.map +0 -1
  247. package/dist/chunk-YIJECSIX.js.map +0 -1
  248. /package/dist/{auto-sync-ANOJDQD4.js.map → auto-sync-XHSS3XPL.js.map} +0 -0
  249. /package/dist/{chunk-7WFY2L5J.js.map → chunk-233S3MXC.js.map} +0 -0
  250. /package/dist/{chunk-N7NAGECH.js.map → chunk-36B5VPRU.js.map} +0 -0
  251. /package/dist/{chunk-5ORJHH43.js.map → chunk-3GGCMRO7.js.map} +0 -0
  252. /package/dist/{chunk-RZED7FAU.js.map → chunk-3M7YP42P.js.map} +0 -0
  253. /package/dist/{chunk-ICBUTKLL.js.map → chunk-3MBSD53O.js.map} +0 -0
  254. /package/dist/{chunk-J72ZY7NU.js.map → chunk-4DLZ2ITE.js.map} +0 -0
  255. /package/dist/{chunk-WN2DASTS.js.map → chunk-5BE3FLKL.js.map} +0 -0
  256. /package/dist/{chunk-S73ZVU37.js.map → chunk-5IQL4N37.js.map} +0 -0
  257. /package/dist/{chunk-CIL4GHMD.js.map → chunk-6Q6AQ36B.js.map} +0 -0
  258. /package/dist/{chunk-W4V7P5HP.js.map → chunk-6RPPZL7W.js.map} +0 -0
  259. /package/dist/{chunk-EQ6EMJRG.js.map → chunk-6Y5ANLDH.js.map} +0 -0
  260. /package/dist/{chunk-YPYSC3TX.js.map → chunk-7FFGWIBN.js.map} +0 -0
  261. /package/dist/{chunk-5QV6CE6K.js.map → chunk-B36ACJG7.js.map} +0 -0
  262. /package/dist/{chunk-ZBET3W26.js.map → chunk-BZTA7DR4.js.map} +0 -0
  263. /package/dist/{chunk-JY4LXMZR.js.map → chunk-C44DRDZM.js.map} +0 -0
  264. /package/dist/{chunk-UIFWZMO7.js.map → chunk-CLVILSBR.js.map} +0 -0
  265. /package/dist/{chunk-OGQGDOYK.js.map → chunk-COIQ5N4J.js.map} +0 -0
  266. /package/dist/{chunk-UAOJVQXC.js.map → chunk-DANFOVQG.js.map} +0 -0
  267. /package/dist/{chunk-2R44BBPK.js.map → chunk-DEGE32C3.js.map} +0 -0
  268. /package/dist/{chunk-LHMCZ6VW.js.map → chunk-E22CQZ3H.js.map} +0 -0
  269. /package/dist/{chunk-JGLZ46JS.js.map → chunk-ELUG6NKQ.js.map} +0 -0
  270. /package/dist/{chunk-X4DHAXFO.js.map → chunk-F3FNXE7C.js.map} +0 -0
  271. /package/dist/{chunk-YIXWY4OZ.js.map → chunk-FETPTCQZ.js.map} +0 -0
  272. /package/dist/{chunk-N2Y6COEM.js.map → chunk-FGUWCE5I.js.map} +0 -0
  273. /package/dist/{chunk-FDUCIUQ5.js.map → chunk-FJ2VJTGA.js.map} +0 -0
  274. /package/dist/{chunk-ENPJX3BB.js.map → chunk-FMFCQRDM.js.map} +0 -0
  275. /package/dist/{chunk-BNU5TQLR.js.map → chunk-FOQ7TW2T.js.map} +0 -0
  276. /package/dist/{chunk-4PMFSYH7.js.map → chunk-FP2AT4WL.js.map} +0 -0
  277. /package/dist/{chunk-DLZGSZVC.js.map → chunk-FYQUUIPI.js.map} +0 -0
  278. /package/dist/{chunk-XTN54NML.js.map → chunk-G3T6YMBO.js.map} +0 -0
  279. /package/dist/{chunk-MUH7RR6T.js.map → chunk-G5NH3SSX.js.map} +0 -0
  280. /package/dist/{chunk-DC3DWF2M.js.map → chunk-GCJA5KKT.js.map} +0 -0
  281. /package/dist/{chunk-U6IL4724.js.map → chunk-GWOFCHPN.js.map} +0 -0
  282. /package/dist/{chunk-TJ5YGCPV.js.map → chunk-GXVK754R.js.map} +0 -0
  283. /package/dist/{chunk-FGGLVCHI.js.map → chunk-HB5TNQMM.js.map} +0 -0
  284. /package/dist/{chunk-GXAZ4Q2S.js.map → chunk-HCFNZWFB.js.map} +0 -0
  285. /package/dist/{chunk-F4ZHKDBW.js.map → chunk-HLWXCDUQ.js.map} +0 -0
  286. /package/dist/{chunk-IQXQ3KWR.js.map → chunk-HTDXNIO6.js.map} +0 -0
  287. /package/dist/{chunk-4RASUAN3.js.map → chunk-HXFIILGF.js.map} +0 -0
  288. /package/dist/{chunk-S37O4S6E.js.map → chunk-I4LJJ44E.js.map} +0 -0
  289. /package/dist/{chunk-USX5YIKB.js.map → chunk-IBQ7LGUJ.js.map} +0 -0
  290. /package/dist/{chunk-VSJ3ARFS.js.map → chunk-IRFQKR4Y.js.map} +0 -0
  291. /package/dist/{chunk-BCBY5GYZ.js.map → chunk-IVXOYE43.js.map} +0 -0
  292. /package/dist/{chunk-WXPBDEMW.js.map → chunk-JDQEQE2R.js.map} +0 -0
  293. /package/dist/{chunk-2WPWEVAH.js.map → chunk-KAED7IBB.js.map} +0 -0
  294. /package/dist/{chunk-OWZXDBV4.js.map → chunk-KNZDGZSY.js.map} +0 -0
  295. /package/dist/{chunk-6KBCUIZP.js.map → chunk-L4QYW2HO.js.map} +0 -0
  296. /package/dist/{chunk-LYOCAUI6.js.map → chunk-LJUQPT5G.js.map} +0 -0
  297. /package/dist/{chunk-XPZM7GP6.js.map → chunk-MBENRPMY.js.map} +0 -0
  298. /package/dist/{chunk-VK5HKVQ7.js.map → chunk-N6GEGRCT.js.map} +0 -0
  299. /package/dist/{chunk-U2A56LLF.js.map → chunk-OBQ2NZAR.js.map} +0 -0
  300. /package/dist/{chunk-WGRAB3KB.js.map → chunk-ORVPS4ZW.js.map} +0 -0
  301. /package/dist/{chunk-PFUXPTBT.js.map → chunk-P3AQ2YSX.js.map} +0 -0
  302. /package/dist/{chunk-DE4U5PTV.js.map → chunk-P4TEQSIZ.js.map} +0 -0
  303. /package/dist/{chunk-UE5JYPU6.js.map → chunk-P7HNMNN2.js.map} +0 -0
  304. /package/dist/{chunk-ZG4GNPRE.js.map → chunk-PBE7NFB5.js.map} +0 -0
  305. /package/dist/{chunk-LN7CWLAU.js.map → chunk-PLGBPFEO.js.map} +0 -0
  306. /package/dist/{chunk-23BA3USG.js.map → chunk-R24RYSQX.js.map} +0 -0
  307. /package/dist/{chunk-WYOHQJKC.js.map → chunk-RXGFITWS.js.map} +0 -0
  308. /package/dist/{chunk-JEX2JL3U.js.map → chunk-SHVLNI4L.js.map} +0 -0
  309. /package/dist/{chunk-QUPTDKSX.js.map → chunk-SY4AFWUZ.js.map} +0 -0
  310. /package/dist/{chunk-H2X32F2K.js.map → chunk-SYDPJUFR.js.map} +0 -0
  311. /package/dist/{chunk-QIOQI7QZ.js.map → chunk-SZBZOUHR.js.map} +0 -0
  312. /package/dist/{chunk-E3NBMTTX.js.map → chunk-TLTBACYX.js.map} +0 -0
  313. /package/dist/{chunk-AVVCPA7U.js.map → chunk-UCPH56YX.js.map} +0 -0
  314. /package/dist/{chunk-ZODKV27J.js.map → chunk-URS56HGW.js.map} +0 -0
  315. /package/dist/{chunk-PJAMFIDE.js.map → chunk-VEQVXNWR.js.map} +0 -0
  316. /package/dist/{chunk-AGRMXXVA.js.map → chunk-VEWTHVX2.js.map} +0 -0
  317. /package/dist/{chunk-WNQ3B3Q5.js.map → chunk-VPEMGBTI.js.map} +0 -0
  318. /package/dist/{chunk-5JBVPJCT.js.map → chunk-WIAXVBQH.js.map} +0 -0
  319. /package/dist/{chunk-BTU77RB5.js.map → chunk-X53HJENJ.js.map} +0 -0
  320. /package/dist/{chunk-KY3KJUNN.js.map → chunk-XC7OYOQC.js.map} +0 -0
  321. /package/dist/{chunk-43FLJLT2.js.map → chunk-Y5HZFIQO.js.map} +0 -0
  322. /package/dist/{chunk-DOJBV6WR.js.map → chunk-YK6SFDP2.js.map} +0 -0
  323. /package/dist/{chunk-QVZZE6VH.js.map → chunk-Z3CKK5RR.js.map} +0 -0
  324. /package/dist/{chunk-AZNE2H6D.js.map → chunk-ZG57BEUS.js.map} +0 -0
  325. /package/dist/{chunk-7P5VBRBS.js.map → chunk-ZZSFRWKD.js.map} +0 -0
  326. /package/dist/{first-start-migration-A6PHIBUQ.js.map → first-start-migration-QVD22WWS.js.map} +0 -0
@@ -148,12 +148,15 @@ export class PendingObserveExtractionTracker {
148
148
  controllers: new Set(previous ? [...previous.controllers, controller] : [controller]),
149
149
  };
150
150
  this.entries.set(key, entry);
151
+ // Any settle (success OR rejection — preemption is just one rejection
152
+ // kind) ends the pending entry; leaving a settled failure tracked wedged
153
+ // the key and kept every controller ever registered for it alive.
151
154
  void promise
152
155
  .then(() => {
153
156
  if (this.entries.get(key) === entry) this.entries.delete(key);
154
157
  })
155
- .catch((error) => {
156
- if (error instanceof Error && isPreemptedExtraction(error) && this.entries.get(key) === entry) this.entries.delete(key);
158
+ .catch(() => {
159
+ if (this.entries.get(key) === entry) this.entries.delete(key);
157
160
  });
158
161
  }
159
162
 
@@ -639,11 +639,18 @@ test("#2128: a failed observe extraction is consumed after force-flush reports i
639
639
  const failure = new Error("provider failed");
640
640
 
641
641
  await service.observe(observeRequest({ sessionKey, skipExtraction: false }));
642
- rejectExtraction(failure);
643
- await assert.rejects(
642
+ // The flush parks on the pending extraction and reports its failure
643
+ // (identity-preserved) when it rejects; with the last-write-wins bug the
644
+ // failure was never tracked at all.
645
+ const reported = assert.rejects(
644
646
  service.extractionForceFlush({ sessionKey }),
645
647
  (error: unknown) => error === failure,
646
648
  );
649
+ const parked = Promise.withResolvers<void>();
650
+ setImmediate(parked.resolve);
651
+ await parked.promise;
652
+ rejectExtraction(failure);
653
+ await reported;
647
654
 
648
655
  await service.observe(observeRequest({ sessionKey, skipExtraction: false }));
649
656
  await assert.doesNotReject(() => service.extractionForceFlush({ sessionKey }));
@@ -256,8 +256,15 @@ test("a preempted observe extraction does not block the next force-flush", async
256
256
  const key = tracker.key("session-a", "alice", "default");
257
257
  tracker.track(key, Promise.reject(preemptedExtractionError()), new AbortController());
258
258
  await tracker.wait("session-a", "alice", "default");
259
+ // A settled failure is no longer pending: the entry is removed on ANY
260
+ // settle, so the next flush is neither blocked nor failed by work that
261
+ // already finished. (A flush overlapping the failing extraction still
262
+ // surfaces its error — covered in access-observe-helpers.test.ts.)
259
263
  tracker.track(key, Promise.reject(new Error("extraction failed")), new AbortController());
260
- await assert.rejects(() => tracker.wait("session-a", "alice", "default"), /extraction failed/);
264
+ const { promise, resolve } = Promise.withResolvers<void>();
265
+ setImmediate(resolve);
266
+ await promise;
267
+ await tracker.wait("session-a", "alice", "default");
261
268
  });
262
269
 
263
270
  test("cancelling a tracked extraction with a preemption reason does not block the next flush", async () => {
@@ -804,3 +804,107 @@ test("claims past the concurrency bound are re-listed on the next tick, in page
804
804
  await scheduler.stop();
805
805
  }
806
806
  });
807
+
808
+ // ── Provider-breaker churn guard ────────────────────────────────────────────
809
+ //
810
+ // While the shared provider breaker is OPEN inside its cooldown, a full
811
+ // submission reads the claim, rebuilds the buffer/coordinator and re-renders
812
+ // chunks before the breaker fails the attempt fast — pure heap pressure per
813
+ // poll. The scheduler must skip submission entirely; when the window lapses,
814
+ // submissions resume and the breaker's own half-open probe keeps recovery
815
+ // automatic.
816
+
817
+ function breakerInstrumentedDeps(
818
+ state: StubState,
819
+ queue: ExtractionQueueCoordinator,
820
+ ): BacklogSchedulerDeps & { claimReads(): number } {
821
+ const deps = makeDeps(state, queue);
822
+ const originalClaim = deps.claims.claimFromSessionFile;
823
+ let claimReads = 0;
824
+ deps.claims.claimFromSessionFile = async (key, lease) => {
825
+ claimReads += 1;
826
+ return originalClaim(key, lease);
827
+ };
828
+ return { ...deps, claimReads: () => claimReads };
829
+ }
830
+
831
+ test("an open provider breaker suppresses submission: no drain, no claim read", async () => {
832
+ const queue = new ExtractionQueueCoordinator(2);
833
+ const state: StubState = {
834
+ counts: new Map([["live-open", { turns: 4, retainedTurns: 0 }]]),
835
+ claimed: new Set(["pending-claim"]),
836
+ claimTurns: new Map([["pending-claim", 7]]),
837
+ claimedThisRun: new Set(),
838
+ outcomes: [],
839
+ drained: [],
840
+ };
841
+ const deps = breakerInstrumentedDeps(state, queue);
842
+ const originalListPage = deps.claims.listPage;
843
+ let listPageCalls = 0;
844
+ deps.claims.listPage = async (...args) => {
845
+ listPageCalls += 1;
846
+ return originalListPage(...args);
847
+ };
848
+ const scheduler = new BacklogScheduler({
849
+ ...deps,
850
+ breakerSnapshot: () => ({
851
+ state: "open",
852
+ openUntilMs: Date.now() + 300_000,
853
+ consecutiveFailures: 5,
854
+ lastReason: "provider 400s",
855
+ }),
856
+ }, { pollMs: 3_600_000 });
857
+ scheduler.start();
858
+ try {
859
+ for (let tick = 1; tick <= 2; tick += 1) {
860
+ await scheduler.tickForTest();
861
+ await scheduler.settleForTest();
862
+ }
863
+ assert.equal(state.drained.length, 0, "no attempt is built while the breaker is open");
864
+ assert.equal(deps.claimReads(), 0, "no claim is read while the breaker is open");
865
+ assert.equal(listPageCalls, 0, "claim discovery I/O is skipped while the breaker is open");
866
+ assert.equal(scheduler.snapshot().counters.claimed, 0);
867
+ } finally {
868
+ await scheduler.stop();
869
+ }
870
+ });
871
+
872
+ test("a lapsed breaker window resumes submission and drains the backlog", async () => {
873
+ const queue = new ExtractionQueueCoordinator(2);
874
+ const state: StubState = {
875
+ counts: new Map([["live-open", { turns: 4, retainedTurns: 0 }]]),
876
+ claimed: new Set(["pending-claim"]),
877
+ claimTurns: new Map([["pending-claim", 7]]),
878
+ claimedThisRun: new Set(),
879
+ outcomes: [
880
+ emptyOutcome("pending-claim", { turnsAtStart: 7 }),
881
+ emptyOutcome("live-open", { turnsAtStart: 4 }),
882
+ ],
883
+ drained: [],
884
+ };
885
+ const deps = breakerInstrumentedDeps(state, queue);
886
+ // Window already lapsed: the next submission IS the half-open probe path.
887
+ const scheduler = new BacklogScheduler({
888
+ ...deps,
889
+ breakerSnapshot: () => ({
890
+ state: "open",
891
+ openUntilMs: Date.now() - 1,
892
+ consecutiveFailures: 5,
893
+ lastReason: "provider 400s",
894
+ }),
895
+ }, { pollMs: 3_600_000 });
896
+ scheduler.start();
897
+ try {
898
+ await scheduler.tickForTest();
899
+ await scheduler.settleForTest();
900
+ await scheduler.tickForTest();
901
+ await scheduler.settleForTest();
902
+ assert.deepEqual(
903
+ state.drained.map((entry) => entry.key).sort(),
904
+ ["live-open", "pending-claim"],
905
+ "a lapsed window lets submissions (the probe path) through"
906
+ );
907
+ } finally {
908
+ await scheduler.stop();
909
+ }
910
+ });
@@ -0,0 +1,195 @@
1
+ import assert from "node:assert/strict";
2
+ /**
3
+ * recordTurnUnlocked snapshot retention — issue: the per-turn mutation built
4
+ * `priorTurns` + `activeTurnsSnapshot` + `retainedTurnsSnapshot` (two full
5
+ * `copyBufferTurn` maps) on EVERY admitted turn, even when the decision path
6
+ * consumes none of them: a plain `keep_buffering` turn with no surprise probe
7
+ * eligible branch discards all three. The mutation now builds the snapshots
8
+ * only when the decision path needs them (`keep_buffering` under an
9
+ * eligible surprise branch, or any extraction decision), so steady-state
10
+ * buffering performs no per-turn snapshot copies.
11
+ *
12
+ * Harness: an instance-level spy wraps the (private) recordTurnUnlocked and
13
+ * captures each AddTurnMutationResult, so tests assert on exactly what the
14
+ * mutation returned — no production instrumentation.
15
+ */
16
+ import test from "node:test";
17
+
18
+ import { type BufferSurpriseProbe, SmartBuffer } from "./buffer.js";
19
+ import { parseConfig } from "./config.js";
20
+ import type { BufferState, BufferTurn } from "./types.js";
21
+
22
+ class FakeStorage {
23
+ public saved: BufferState | null = null;
24
+ public saveCount = 0;
25
+
26
+ constructor(private readonly initial: BufferState) {}
27
+
28
+ async loadBuffer(): Promise<BufferState> {
29
+ return structuredClone(this.initial);
30
+ }
31
+
32
+ async saveBuffer(state: BufferState): Promise<void> {
33
+ this.saveCount += 1;
34
+ this.saved = structuredClone(state);
35
+ }
36
+ }
37
+
38
+ function makeTurn(sessionKey: string, content: string): BufferTurn {
39
+ return {
40
+ role: "user",
41
+ content,
42
+ timestamp: "2026-04-20T12:00:00.000Z",
43
+ sessionKey,
44
+ };
45
+ }
46
+
47
+ function emptyBuffer(): BufferState {
48
+ return { turns: [], lastExtractionAt: null, extractionCount: 0 };
49
+ }
50
+
51
+ type MutationCapture = {
52
+ priorTurns: BufferTurn[];
53
+ activeTurnsSnapshot: BufferTurn[];
54
+ retainedTurnsSnapshot: BufferTurn[];
55
+ decision: string;
56
+ };
57
+
58
+ type RecordSpy = (bufferKey: string, turn: BufferTurn) => Promise<MutationCapture>;
59
+
60
+ function spyMutations(buffer: SmartBuffer): MutationCapture[] {
61
+ const captures: MutationCapture[] = [];
62
+ // SAFETY: recordTurnUnlocked is a TS-private prototype method with a stable
63
+ // runtime shape; the spy re-exposes the identical signature on the instance.
64
+ const original = Object.getPrototypeOf(buffer).recordTurnUnlocked as RecordSpy;
65
+ Object.defineProperty(buffer, "recordTurnUnlocked", {
66
+ value: async (bufferKey: string, turn: BufferTurn): Promise<MutationCapture> => {
67
+ const result = await original.call(buffer, bufferKey, turn);
68
+ captures.push(result);
69
+ return result;
70
+ },
71
+ configurable: true,
72
+ writable: true,
73
+ });
74
+ return captures;
75
+ }
76
+
77
+ function fixedScoreProbe(score: number | null): BufferSurpriseProbe & { calls: number } {
78
+ const probe = {
79
+ calls: 0,
80
+ async scoreTurn(_key: string, _turn: BufferTurn, _prior: readonly BufferTurn[]): Promise<number | null> {
81
+ probe.calls += 1;
82
+ return score;
83
+ },
84
+ };
85
+ return probe;
86
+ }
87
+
88
+ test("plain keep_buffering turn performs no snapshot copies", async () => {
89
+ const storage = new FakeStorage(emptyBuffer());
90
+ const config = parseConfig({
91
+ bufferMaxTurns: 5,
92
+ triggerMode: "smart",
93
+ bufferSaveDebounceMs: 0,
94
+ });
95
+ const buffer = new SmartBuffer(config, storage);
96
+ const captures = spyMutations(buffer);
97
+
98
+ const decision = await buffer.addTurn("sess-1", makeTurn("sess-1", "ordinary content"));
99
+
100
+ assert.equal(decision, "keep_buffering");
101
+ assert.equal(captures.length, 1);
102
+ assert.equal(captures[0].priorTurns.length, 0, "no priorTurns copy for a decision that never reads it");
103
+ assert.equal(captures[0].activeTurnsSnapshot.length, 0, "no active-turn snapshot copies while buffering");
104
+ assert.equal(captures[0].retainedTurnsSnapshot.length, 0, "no retained-turn snapshot copies while buffering");
105
+ });
106
+
107
+ test("extraction decisions still carry full snapshots", async () => {
108
+ const storage = new FakeStorage(emptyBuffer());
109
+ const config = parseConfig({
110
+ bufferMaxTurns: 2,
111
+ triggerMode: "every_n",
112
+ bufferSaveDebounceMs: 0,
113
+ });
114
+ const buffer = new SmartBuffer(config, storage);
115
+ const captures = spyMutations(buffer);
116
+
117
+ await buffer.addTurn("sess-1", makeTurn("sess-1", "one"));
118
+ const outcome = await buffer.addTurnWithOutcome("sess-1", makeTurn("sess-1", "two"));
119
+
120
+ assert.equal(outcome.decision, "extract_batch");
121
+ assert.notEqual(outcome.extractionTurns, undefined);
122
+ assert.equal(outcome.extractionTurns?.length, 2);
123
+ const last = captures[captures.length - 1];
124
+ assert.equal(last.decision, "extract_batch");
125
+ assert.equal(last.priorTurns.length, 1, "prior turn kept for extraction decisions");
126
+ assert.equal(last.activeTurnsSnapshot.length, 2, "active snapshot kept for extraction decisions");
127
+ assert.deepEqual(
128
+ last.activeTurnsSnapshot.map((t) => t.content),
129
+ ["one", "two"]
130
+ );
131
+ });
132
+
133
+ test("surprise-eligible keep_buffering still builds the snapshots the probe path reads", async () => {
134
+ const storage = new FakeStorage(emptyBuffer());
135
+ const probe = fixedScoreProbe(null);
136
+ const config = parseConfig({
137
+ bufferSurpriseTriggerEnabled: true,
138
+ bufferMaxTurns: 5,
139
+ triggerMode: "smart",
140
+ bufferSaveDebounceMs: 0,
141
+ });
142
+ const buffer = new SmartBuffer(config, storage, probe);
143
+ const captures = spyMutations(buffer);
144
+
145
+ const decision = await buffer.addTurn("sess-1", makeTurn("sess-1", "ordinary content"));
146
+
147
+ assert.equal(decision, "keep_buffering");
148
+ assert.equal(probe.calls, 1, "the surprise probe must still be consulted");
149
+ const capture = captures[0];
150
+ assert.equal(capture.priorTurns.length, 0, "first turn has no priors");
151
+ assert.equal(capture.activeTurnsSnapshot.length, 1, "snapshot kept while the surprise branch may promote");
152
+ assert.equal(capture.retainedTurnsSnapshot.length, 0, "nothing retained yet");
153
+ });
154
+
155
+ test("surprise promotion still extracts from a consistent snapshot", async () => {
156
+ const storage = new FakeStorage(emptyBuffer());
157
+ const probe = fixedScoreProbe(0.99);
158
+ const config = parseConfig({
159
+ bufferSurpriseTriggerEnabled: true,
160
+ bufferMaxTurns: 5,
161
+ triggerMode: "smart",
162
+ bufferSurpriseThreshold: 0.5,
163
+ bufferSaveDebounceMs: 0,
164
+ });
165
+ const buffer = new SmartBuffer(config, storage, probe);
166
+
167
+ const outcome = await buffer.addTurnWithOutcome("sess-1", makeTurn("sess-1", "novel"));
168
+
169
+ assert.equal(outcome.decision, "extract_now");
170
+ assert.deepEqual(
171
+ outcome.extractionTurns?.map((t) => t.content),
172
+ ["novel"]
173
+ );
174
+ });
175
+
176
+ test("keep_buffering under every_n never builds snapshots even with a probe present", async () => {
177
+ const storage = new FakeStorage(emptyBuffer());
178
+ const probe = fixedScoreProbe(0.99);
179
+ const config = parseConfig({
180
+ bufferSurpriseTriggerEnabled: true,
181
+ bufferMaxTurns: 5,
182
+ triggerMode: "every_n",
183
+ bufferSaveDebounceMs: 0,
184
+ });
185
+ const buffer = new SmartBuffer(config, storage, probe);
186
+ const captures = spyMutations(buffer);
187
+
188
+ const decision = await buffer.addTurn("sess-1", makeTurn("sess-1", "ordinary content"));
189
+
190
+ assert.equal(decision, "keep_buffering");
191
+ assert.equal(probe.calls, 0, "surprise branch is smart-mode only");
192
+ assert.equal(captures[0].activeTurnsSnapshot.length, 0);
193
+ assert.equal(captures[0].retainedTurnsSnapshot.length, 0);
194
+ assert.equal(captures[0].priorTurns.length, 0);
195
+ });
@@ -0,0 +1,205 @@
1
+ import assert from "node:assert/strict";
2
+ /**
3
+ * mergeTurns identity comparison — issue: the claim-merge hot path
4
+ * (buffer-drain-claims.ts `mergeIntoClaim`) deduped incoming turns with
5
+ * `merged.some(isEqual)`: O(existing × incoming) per observe while a claim
6
+ * only grows during a stalled drain. The identity comparison now goes
7
+ * through a Set of identity keys built from the exact `bufferTurnsEqual`
8
+ * fields, so the merge is O(n + m) with byte-identical merge semantics:
9
+ * dedupe identical turns (including duplicates within `incoming`), append
10
+ * genuinely-new turns in arrival order, existing entries always win.
11
+ *
12
+ * The identity key must be injective over `bufferTurnsEqual` equality
13
+ * classes — including the undefined-vs-null distinctions on optional fields
14
+ * (`providerThreadId`) and the plan-identity triple (#3251).
15
+ */
16
+ import test from "node:test";
17
+
18
+ import { bufferTurnIdentityKey, bufferTurnsEqual, mergeBufferTurnsByIdentity } from "./buffer-turn-helpers.js";
19
+ import type { ScopeProfileLayerResolution } from "./namespaces/scope-profiles.js";
20
+ import type { BufferTurn } from "./types.js";
21
+
22
+ function turn(overrides: Partial<BufferTurn> & { content: string }): BufferTurn {
23
+ return { role: "user", timestamp: "2026-11-01T00:00:00.000Z", ...overrides };
24
+ }
25
+
26
+ test("bufferTurnIdentityKey matches bufferTurnsEqual exactly (same key ⇔ equal)", () => {
27
+ const base = turn({ content: "hello", sessionKey: "s1" });
28
+ const same = turn({ content: "hello", sessionKey: "s1" });
29
+ const different = [
30
+ turn({ content: "hello", sessionKey: "s2" }),
31
+ turn({ content: "world", sessionKey: "s1" }),
32
+ turn({ content: "hello", sessionKey: "s1", role: "assistant" as const }),
33
+ turn({ content: "hello", sessionKey: "s1", timestamp: "2026-11-02T00:00:00.000Z" }),
34
+ turn({ content: "hello", sessionKey: "s1", providerThreadId: null }),
35
+ turn({ content: "hello", sessionKey: "s1", providerThreadId: "t1" }),
36
+ turn({ content: "hello", sessionKey: "s1", turnFingerprint: "fp" }),
37
+ turn({ content: "hello", sessionKey: "s1", persistProcessedFingerprint: true }),
38
+ turn({ content: "hello", sessionKey: "s1", persistProcessedFingerprint: false }),
39
+ turn({ content: "hello", sessionKey: "s1", sourceConnector: "cli" }),
40
+ turn({ content: "hello", sessionKey: "s1", originRole: "user" as const }),
41
+ turn({ content: "hello", sessionKey: "s1", ambientCapture: true }),
42
+ turn({ content: "hello", sessionKey: "s1", ambientCapture: false }),
43
+ turn({ content: "hello", sessionKey: "s1", sourceValidAt: "2026-01-01T00:00:00.000Z" }),
44
+ turn({ content: "hello", sessionKey: "s1", acceptedCapture: true }),
45
+ turn({ content: "hello", sessionKey: "s1", acceptedCapture: false }),
46
+ turn({ content: "hello", sessionKey: "s1", writeNamespaceOverride: "ns" }),
47
+ turn({ content: "hello", sessionKey: "s1", principalOverride: "p1" }),
48
+ turn({
49
+ content: "hello",
50
+ sessionKey: "s1",
51
+ codingContext: { projectId: "proj", branch: "main", rootPath: "/r", defaultBranch: "main" },
52
+ }),
53
+ turn({
54
+ content: "hello",
55
+ sessionKey: "s1",
56
+ codingContext: { projectId: "proj", branch: "dev", rootPath: "/r", defaultBranch: "main" },
57
+ }),
58
+ ];
59
+
60
+ assert.equal(bufferTurnIdentityKey(base), bufferTurnIdentityKey(same));
61
+ assert.equal(bufferTurnsEqual(base, same), true);
62
+ for (const other of different) {
63
+ assert.notEqual(
64
+ bufferTurnIdentityKey(base),
65
+ bufferTurnIdentityKey(other),
66
+ `key must differ for ${JSON.stringify(other)}`
67
+ );
68
+ assert.equal(bufferTurnsEqual(base, other), false);
69
+ }
70
+ });
71
+
72
+ test("bufferTurnIdentityKey: undefined and null providerThreadId are distinct classes", () => {
73
+ const absent = turn({ content: "x" });
74
+ const explicitNull = turn({ content: "x", providerThreadId: null });
75
+ const present = turn({ content: "x", providerThreadId: "t" });
76
+ const keys = new Set([
77
+ bufferTurnIdentityKey(absent),
78
+ bufferTurnIdentityKey(explicitNull),
79
+ bufferTurnIdentityKey(present),
80
+ ]);
81
+ assert.equal(keys.size, 3);
82
+ assert.equal(bufferTurnsEqual(absent, explicitNull), false);
83
+ assert.equal(bufferTurnsEqual(absent, present), false);
84
+ });
85
+
86
+ test("bufferTurnIdentityKey: null and undefined scope plan share the empty identity (#3251)", () => {
87
+ const noPlan = turn({ content: "x" });
88
+ const nullPlan = turn({ content: "x", scopeProfileWritePlan: null });
89
+ assert.equal(bufferTurnIdentityKey(noPlan), bufferTurnIdentityKey(nullPlan));
90
+ assert.equal(bufferTurnsEqual(noPlan, nullPlan), true);
91
+ });
92
+
93
+ test("mergeBufferTurnsByIdentity appends genuinely-new turns in arrival order", () => {
94
+ const existing = [turn({ content: "a" }), turn({ content: "b" })];
95
+ const incoming = [turn({ content: "c" }), turn({ content: "d" })];
96
+ const merged = mergeBufferTurnsByIdentity(existing, incoming);
97
+ assert.deepEqual(
98
+ merged.map((t) => t.content),
99
+ ["a", "b", "c", "d"]
100
+ );
101
+ });
102
+
103
+ test("mergeBufferTurnsByIdentity dedupes turns identical to an existing entry", () => {
104
+ const existing = [turn({ content: "a" }), turn({ content: "b" })];
105
+ const incoming = [turn({ content: "b" }), turn({ content: "c" })];
106
+ const merged = mergeBufferTurnsByIdentity(existing, incoming);
107
+ assert.deepEqual(
108
+ merged.map((t) => t.content),
109
+ ["a", "b", "c"]
110
+ );
111
+ // The kept entry is the EXISTING object, not the incoming copy.
112
+ assert.equal(merged[1], existing[1]);
113
+ });
114
+
115
+ test("mergeBufferTurnsByIdentity dedupes duplicates within incoming (first wins)", () => {
116
+ const existing: BufferTurn[] = [];
117
+ const first = turn({ content: "dup" });
118
+ const second = turn({ content: "dup" });
119
+ const merged = mergeBufferTurnsByIdentity(existing, [first, second]);
120
+ assert.equal(merged.length, 1);
121
+ assert.equal(merged[0], first);
122
+ });
123
+
124
+ test("mergeBufferTurnsByIdentity keeps existing entries verbatim, duplicates included", () => {
125
+ const dupA = turn({ content: "a" });
126
+ const dupB = turn({ content: "a" });
127
+ const existing = [dupA, dupB];
128
+ const merged = mergeBufferTurnsByIdentity(existing, [turn({ content: "a" }), turn({ content: "b" })]);
129
+ assert.equal(merged.length, 3, "existing duplicates are preserved; incoming duplicate dropped");
130
+ assert.equal(merged[0], dupA);
131
+ assert.equal(merged[1], dupB);
132
+ assert.equal(merged[2].content, "b");
133
+ });
134
+
135
+ test("mergeBufferTurnsByIdentity keeps existing order and does not mutate inputs", () => {
136
+ const existing = [turn({ content: "b" }), turn({ content: "a" })];
137
+ const incoming = [turn({ content: "a" }), turn({ content: "c" })];
138
+ const merged = mergeBufferTurnsByIdentity(existing, incoming);
139
+ assert.deepEqual(
140
+ merged.map((t) => t.content),
141
+ ["b", "a", "c"]
142
+ );
143
+ assert.equal(existing.length, 2);
144
+ assert.deepEqual(
145
+ existing.map((t) => t.content),
146
+ ["b", "a"]
147
+ );
148
+ });
149
+
150
+ // Identity fields only: scopeProfilePlanIdentity reads profileId, writeLayer
151
+ // and writeNamespace; the remaining members are inert for identity tests.
152
+ function pinnedPlan(namespace: string): NonNullable<BufferTurn["scopeProfileWritePlan"]> {
153
+ const layer: ScopeProfileLayerResolution = {
154
+ id: "userProject",
155
+ kind: "user-project",
156
+ namespace,
157
+ readable: true,
158
+ writable: true,
159
+ promotable: false,
160
+ reason: "identity-test layer",
161
+ };
162
+ return {
163
+ profileId: "p",
164
+ profile: {
165
+ readOrder: ["userProject"],
166
+ writeDefault: "userProject",
167
+ promotionTargets: [],
168
+ autoPromote: { enabled: false, targets: [], categories: [], minConfidenceTier: "speculative" },
169
+ },
170
+ baseNamespace: namespace,
171
+ writeLayer: "userProject",
172
+ writeNamespace: namespace,
173
+ readNamespaces: [namespace],
174
+ layers: [layer],
175
+ promotionTargets: [],
176
+ warnings: [],
177
+ };
178
+ }
179
+
180
+ test("mergeBufferTurnsByIdentity stays consistent with bufferTurnsEqual on scope-pinned turns", () => {
181
+ const pinned = turn({
182
+ content: "cap",
183
+ acceptedCapture: true,
184
+ writeNamespaceOverride: "ns-a",
185
+ scopeProfileWritePlan: pinnedPlan("ns-a"),
186
+ });
187
+ const samePinned = turn({
188
+ content: "cap",
189
+ acceptedCapture: true,
190
+ writeNamespaceOverride: "ns-a",
191
+ scopeProfileWritePlan: pinnedPlan("ns-a"),
192
+ });
193
+ const otherPinned = turn({
194
+ content: "cap",
195
+ acceptedCapture: true,
196
+ writeNamespaceOverride: "ns-b",
197
+ scopeProfileWritePlan: pinnedPlan("ns-b"),
198
+ });
199
+ assert.equal(bufferTurnsEqual(pinned, samePinned), true);
200
+ assert.equal(bufferTurnsEqual(pinned, otherPinned), false);
201
+ const merged = mergeBufferTurnsByIdentity([pinned], [samePinned, otherPinned]);
202
+ assert.equal(merged.length, 2);
203
+ assert.equal(merged[0], pinned);
204
+ assert.equal(merged[1], otherPinned);
205
+ });
@@ -169,6 +169,68 @@ export function bufferTurnsEqual(left: BufferTurn | undefined, right: BufferTurn
169
169
  );
170
170
  }
171
171
 
172
+ /**
173
+ * Canonical identity key for a turn, injective over `bufferTurnsEqual`
174
+ * equality classes. Every compared field is a primitive (or the #3251 plan
175
+ * identity string), so set-membership replaces pairwise comparison and turn
176
+ * merges run O(n + m) instead of O(n × m). JSON.stringify of a string is
177
+ * injective (it escapes control characters), and the raw `\u0001` sentinels
178
+ * distinguish undefined from null — a distinction `===` makes and plain
179
+ * stringification would lose.
180
+ */
181
+ export function bufferTurnIdentityKey(turn: BufferTurn): string {
182
+ const opt = (value: string | null | undefined): string =>
183
+ value === undefined ? "\u0001undefined" : value === null ? "\u0001null" : JSON.stringify(value);
184
+ const optFlag = (value: boolean | undefined): string =>
185
+ value === undefined ? "\u0001undefined" : String(value);
186
+ const ctx = turn.codingContext;
187
+ return JSON.stringify([
188
+ turn.role,
189
+ turn.content,
190
+ turn.timestamp,
191
+ opt(turn.sessionKey),
192
+ opt(turn.sessionOwnerPrincipal),
193
+ opt(turn.logicalSessionKey),
194
+ turn.providerThreadId === undefined ? "\u0001undefined" : turn.providerThreadId === null ? "\u0001null" : JSON.stringify(turn.providerThreadId),
195
+ opt(turn.turnFingerprint),
196
+ optFlag(turn.persistProcessedFingerprint),
197
+ opt(turn.sourceConnector),
198
+ opt(turn.originRole),
199
+ optFlag(turn.ambientCapture),
200
+ opt(turn.sourceValidAt),
201
+ optFlag(turn.acceptedCapture),
202
+ opt(ctx?.projectId),
203
+ opt(ctx?.rootPath),
204
+ opt(ctx?.branch),
205
+ opt(ctx?.defaultBranch),
206
+ opt(turn.writeNamespaceOverride),
207
+ opt(turn.principalOverride),
208
+ opt(turn.scopeProfileWritePlan ? scopeProfilePlanIdentity(turn.scopeProfileWritePlan) : undefined),
209
+ ]);
210
+ }
211
+
212
+ /**
213
+ * Identity-dedupe `incoming` turns into `existing` (same contract as the
214
+ * claim-merge `mergeTurns`): identical turns are dropped — the EXISTING
215
+ * entry is kept — genuinely-new turns append in arrival order, and
216
+ * duplicates within `incoming` collapse to their first occurrence. O(n + m)
217
+ * via `bufferTurnIdentityKey` membership instead of pairwise equality.
218
+ */
219
+ export function mergeBufferTurnsByIdentity(
220
+ existing: readonly BufferTurn[],
221
+ incoming: readonly BufferTurn[],
222
+ ): BufferTurn[] {
223
+ const merged = [...existing];
224
+ const seen = new Set(merged.map(bufferTurnIdentityKey));
225
+ for (const turn of incoming) {
226
+ const key = bufferTurnIdentityKey(turn);
227
+ if (seen.has(key)) continue;
228
+ seen.add(key);
229
+ merged.push(turn);
230
+ }
231
+ return merged;
232
+ }
233
+
172
234
  /** Replace only the processed snapshot; callers without one retain legacy whole-slot semantics. */
173
235
  export function selectRetainedDeferredTurns(
174
236
  existing: readonly BufferTurn[],
package/src/buffer.ts CHANGED
@@ -646,16 +646,16 @@ export class SmartBuffer {
646
646
  private async recordTurnUnlocked(bufferKey: string, turn: BufferTurn): Promise<AddTurnMutationResult> {
647
647
  await this.loadUnlocked();
648
648
  const entry = this.entryFor(bufferKey);
649
- const priorTurns = entry.turns.slice();
650
- entry.turns.push(turn);
651
- const activeTurnsSnapshot = entry.turns.map(copyBufferTurn);
652
- const retainedTurnsSnapshot = (entry.retainedTurns ?? []).map(copyBufferTurn);
653
- if (bufferKey === "default") {
654
- this.state.turns = entry.turns;
655
- }
656
-
657
649
  const signal = scanSignals(turn.content, this.config.highSignalPatterns);
650
+ entry.turns.push(turn);
658
651
  const decision = this.evaluate(entry, signal.level);
652
+ // Snapshots serve only extraction decisions and surprise-eligible keep_buffering turns (probe, smart mode, sub-high signal); steady-state buffering copies nothing.
653
+ const surpriseEligible = this.surpriseProbe !== null && this.config.triggerMode === "smart" && signal.level !== "high";
654
+ const snapshotsNeeded = decision !== "keep_buffering" || surpriseEligible;
655
+ const priorTurns = snapshotsNeeded ? entry.turns.slice(0, -1) : [];
656
+ const activeTurnsSnapshot = snapshotsNeeded ? entry.turns.map(copyBufferTurn) : [];
657
+ const retainedTurnsSnapshot = snapshotsNeeded ? (entry.retainedTurns ?? []).map(copyBufferTurn) : [];
658
+ if (bufferKey === "default") this.state.turns = entry.turns;
659
659
  const turnCountInWindow = entry.turns.length;
660
660
 
661
661
  this.persistence.markDirty(bufferKey);
@@ -201,7 +201,16 @@ export function isUnsupportedJsonSchemaError(error: unknown): boolean {
201
201
  return (
202
202
  /(?:response[_ ]?format|json[_ ]?schema|structured[_ ]?output)\b[^;,\n]{0,60}\b(?:unsupported|not supported|not implemented|not available)\b/i.test(message) ||
203
203
  /\b(?:unsupported|unknown|unrecognized)(?:[\s:"'=]|parameter|argument|request|supplied|field|type|value){1,40}\b(?:response[_ ]?format|json[_ ]?schema|structured[_ ]?output|schema|format)\b/i.test(message) ||
204
- /\b(?:does not|doesn't|cannot|can't) support(?:[\s:"'=]|the|native|type){1,20}\b(?:response[_ ]?format|json[_ ]?schema|structured[_ ]?output|schema|format)\b/i.test(message)
204
+ /\b(?:does not|doesn't|cannot|can't) support(?:[\s:"'=]|the|native|type){1,20}\b(?:response[_ ]?format|json[_ ]?schema|structured[_ ]?output|schema|format)\b/i.test(message) ||
205
+ // A strict provider parser rejecting the SCHEMA CONTENT itself (2026-11-05
206
+ // MiniMax-M3.1-Flash: "invalid params, Mismatch type string with value
207
+ // array ... \"episodeTitle\": {\"type\": [\"string\",\"null\"]}"). The
208
+ // provider never names response_format here; it dies parsing our payload.
209
+ // Evidence is the observed signature only — a quoted (JSON-escaped)
210
+ // type-array fragment. A generic "mismatch type" naming an unrelated
211
+ // parameter, or a bare echo of the format name, is not enough to degrade.
212
+ (/invalid\s+(?:params|parameters)/i.test(message) &&
213
+ /\\{0,4}"\s{0,8}type\s{0,8}\\{0,4}"\s{0,8}:\s{0,8}\[/.test(message))
205
214
  );
206
215
  }
207
216