@cortexkit/opencode-magic-context 0.49.15 → 0.49.17

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 (235) hide show
  1. package/dist/auto-search-worker.js +76 -63
  2. package/dist/chunk-embedding-write-worker.js +12 -10
  3. package/dist/{compaction-marker-candidates-v5c6997n.js → compaction-marker-candidates-n4krhat4.js} +2 -2
  4. package/dist/compaction-marker-candidates-worker.js +2 -2
  5. package/dist/config/schema/magic-context.d.ts +343 -343
  6. package/dist/config/schema/magic-context.d.ts.map +1 -1
  7. package/dist/{conflict-warning-hook-mgq17yr3.js → conflict-warning-hook-4pmz879v.js} +6 -6
  8. package/dist/{context-0m46day0.js → context-yhetp0kn.js} +297 -141
  9. package/dist/embedding-backfill-fmz50tsy.js +24 -0
  10. package/dist/embedding-worker-process.js +1 -1
  11. package/dist/embedding-worker.js +5 -5
  12. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +1 -1
  13. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  14. package/dist/features/magic-context/git-commits/indexer.d.ts.map +1 -1
  15. package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
  16. package/dist/features/magic-context/memory/project-identity-cache.d.ts +2 -0
  17. package/dist/features/magic-context/memory/project-identity-cache.d.ts.map +1 -1
  18. package/dist/features/magic-context/message-fts-rowid-map.d.ts +2 -2
  19. package/dist/features/magic-context/message-fts-rowid-map.d.ts.map +1 -1
  20. package/dist/features/magic-context/message-fts-session-filter.d.ts +2 -0
  21. package/dist/features/magic-context/message-fts-session-filter.d.ts.map +1 -1
  22. package/dist/features/magic-context/message-index-async.d.ts +1 -1
  23. package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
  24. package/dist/features/magic-context/message-index.d.ts +5 -0
  25. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  26. package/dist/features/magic-context/message-time-backfill.d.ts +2 -2
  27. package/dist/features/magic-context/message-time-backfill.d.ts.map +1 -1
  28. package/dist/features/magic-context/search.d.ts +6 -1
  29. package/dist/features/magic-context/search.d.ts.map +1 -1
  30. package/dist/features/magic-context/storage-db.d.ts +2 -1
  31. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  32. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  33. package/dist/features/magic-context/storage-meta-shared.d.ts +2 -2
  34. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  35. package/dist/features/magic-context/storage-session-tables.d.ts +9 -1
  36. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  37. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  38. package/dist/hooks/magic-context/auto-search-deadline.d.ts +2 -1
  39. package/dist/hooks/magic-context/auto-search-deadline.d.ts.map +1 -1
  40. package/dist/hooks/magic-context/auto-search-hint.d.ts +3 -0
  41. package/dist/hooks/magic-context/auto-search-hint.d.ts.map +1 -1
  42. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/auto-search-worker-client.d.ts +10 -2
  44. package/dist/hooks/magic-context/auto-search-worker-client.d.ts.map +1 -1
  45. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +4 -2
  46. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  47. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  48. package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
  49. package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
  50. package/dist/hooks/magic-context/historian-host-runner.d.ts.map +1 -1
  51. package/dist/hooks/magic-context/historian-prompt-fit.d.ts +2 -1
  52. package/dist/hooks/magic-context/historian-prompt-fit.d.ts.map +1 -1
  53. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  54. package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -1
  55. package/dist/hooks/magic-context/raw-ordinal-warmup.d.ts +26 -0
  56. package/dist/hooks/magic-context/raw-ordinal-warmup.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  58. package/dist/hooks/magic-context/recomp-orchestrator.d.ts +3 -3
  59. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
  60. package/dist/hooks/magic-context/system-prompt-diagnostics.d.ts +8 -0
  61. package/dist/hooks/magic-context/system-prompt-diagnostics.d.ts.map +1 -0
  62. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  63. package/dist/hooks/magic-context/tag-messages.d.ts +6 -1
  64. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  65. package/dist/hooks/magic-context/todo-view.d.ts +2 -2
  66. package/dist/hooks/magic-context/tool-drop-target.d.ts +3 -2
  67. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  68. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  69. package/dist/hooks/magic-context/transform-stage-logger.d.ts +3 -0
  70. package/dist/hooks/magic-context/transform-stage-logger.d.ts.map +1 -1
  71. package/dist/hooks/magic-context/transform.d.ts +1 -0
  72. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  73. package/dist/{index-3kdm78zp.js → index-24fezq97.js} +4 -4
  74. package/dist/{index-a8bvhwqs.js → index-2vxtcb95.js} +32 -3
  75. package/dist/{index-f71thw8j.js → index-4s3t0nd0.js} +1 -1
  76. package/dist/{index-ts1hjk1h.js → index-4yt7dtf4.js} +453 -227
  77. package/dist/{index-mc1cqza6.js → index-5a1ex6x8.js} +5 -5
  78. package/dist/{index-fkx2qnvw.js → index-6x57ar45.js} +2 -2
  79. package/dist/{index-dyvbsvga.js → index-a6mc0v4d.js} +25 -11
  80. package/dist/{index-snp5wbge.js → index-a7c4r5re.js} +160 -749
  81. package/dist/{index-42vcjcwe.js → index-aabzay3h.js} +180 -103
  82. package/dist/{index-3kx0kg0f.js → index-b5vqckbw.js} +14 -8
  83. package/dist/{v2/server-wafzvyhg.js → index-beht37jy.js} +8 -0
  84. package/dist/{index-e20m7284.js → index-bjcnntmz.js} +3 -3
  85. package/dist/{index-hqkczed7.js → index-c5pytzne.js} +22 -2
  86. package/dist/{index-thvnpfsv.js → index-cwzfcbtq.js} +967 -88
  87. package/dist/{index-yxycg7fs.js → index-fb2qxs2y.js} +26 -18
  88. package/dist/{index-mht7bq0z.js → index-hqhm3wke.js} +32 -8
  89. package/dist/{index-e4dsjj1p.js → index-jn12fvvh.js} +1 -1
  90. package/dist/index-kaqs6h31.js +58 -0
  91. package/dist/{index-n6m03s3n.js → index-ke416mdc.js} +1 -1
  92. package/dist/index-kmdj7dsq.js +96 -0
  93. package/dist/{index-1d289pq2.js → index-kq82mtg0.js} +2 -2
  94. package/dist/{index-n78avt2p.js → index-n37sebck.js} +1 -1
  95. package/dist/{index-1vc7jrkg.js → index-nkpmm9q8.js} +3 -3
  96. package/dist/{index-944w16m0.js → index-p7m7j0cp.js} +25 -17
  97. package/dist/{index-wk0n1cgt.js → index-qn7xstt2.js} +2 -2
  98. package/dist/{index-saews2s3.js → index-rr2bb45z.js} +1 -1
  99. package/dist/{index-mmfa86mt.js → index-s734jcsr.js} +169 -149
  100. package/dist/{index-dgs16axy.js → index-s74zqahq.js} +1 -1
  101. package/dist/{index-bxdg2nbt.js → index-v8awdgvd.js} +4 -4
  102. package/dist/{index-xn6q9pg6.js → index-vrzjhftj.js} +36 -76
  103. package/dist/{index-1sarmj9z.js → index-wgypt3cs.js} +6 -6
  104. package/dist/{index-y1qysm3q.js → index-ze7mvk54.js} +11 -8
  105. package/dist/index.js +47 -45
  106. package/dist/message-fts-rowid-map-17s0dfn4.js +25 -0
  107. package/dist/{message-index-bounded-vtgpq6b4.js → message-index-bounded-1633vc7f.js} +17 -17
  108. package/dist/{message-index-a6gztaqx.js → message-index-dprebmtt.js} +15 -15
  109. package/dist/{message-time-backfill-qjge9044.js → message-time-backfill-73mmm76q.js} +5 -5
  110. package/dist/migration-worker.js +9 -7
  111. package/dist/plugin/dream-timer.d.ts +11 -0
  112. package/dist/plugin/dream-timer.d.ts.map +1 -1
  113. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  114. package/dist/{project-embedding-registry-x45bv3qr.js → project-embedding-registry-z74mhqkf.js} +12 -10
  115. package/dist/raw-ordinal-warmup-worker.js +40 -10
  116. package/dist/{read-session-chunk-3ae3nfdn.js → read-session-chunk-4zp09ejb.js} +9 -9
  117. package/dist/{read-session-raw-m56hc7v2.js → read-session-raw-rsc8c7qy.js} +2 -2
  118. package/dist/{recomp-orchestrator-nwzcfhx4.js → recomp-orchestrator-z3ecszx6.js} +26 -26
  119. package/dist/{search-deajrd90.js → search-6mee3vty.js} +23 -22
  120. package/dist/search-archive-znyy44ve.js +25 -0
  121. package/dist/{send-session-notification-24r4esbh.js → send-session-notification-6ve0e8p4.js} +5 -5
  122. package/dist/shared/historian-cache.d.ts +8 -0
  123. package/dist/shared/historian-cache.d.ts.map +1 -0
  124. package/dist/shared/model-suggestion-retry.d.ts +4 -0
  125. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  126. package/dist/shared/resolve-fallbacks.d.ts +3 -3
  127. package/dist/shared/rpc-discovery-cleanup.d.ts +3 -0
  128. package/dist/shared/rpc-discovery-cleanup.d.ts.map +1 -0
  129. package/dist/shared/rpc-server.d.ts +2 -0
  130. package/dist/shared/rpc-server.d.ts.map +1 -1
  131. package/dist/shared/search-text.d.ts +8 -0
  132. package/dist/shared/search-text.d.ts.map +1 -0
  133. package/dist/shared/sqlite.d.ts +4 -2
  134. package/dist/shared/sqlite.d.ts.map +1 -1
  135. package/dist/shared/storage-file-mutex.d.ts.map +1 -1
  136. package/dist/shared/subagent-runner.d.ts +2 -0
  137. package/dist/shared/subagent-runner.d.ts.map +1 -1
  138. package/dist/shared/system-directive.d.ts +2 -0
  139. package/dist/shared/system-directive.d.ts.map +1 -1
  140. package/dist/{sqlite-4pqyynrs.js → sqlite-zf72c52d.js} +1 -1
  141. package/dist/{storage-memory-j3v3hc83.js → storage-memory-y3qmy00r.js} +2 -2
  142. package/dist/{storage-tags-s5bg1ahx.js → storage-tags-8qns3012.js} +8 -4
  143. package/dist/tools/ctx-expand/search-archive.d.ts +5 -1
  144. package/dist/tools/ctx-expand/search-archive.d.ts.map +1 -1
  145. package/dist/{transform-postprocess-phase-ne3z1zmt.js → transform-postprocess-phase-3ydzxa62.js} +26 -26
  146. package/dist/v2/auto-search-worker.js +76 -63
  147. package/dist/v2/chunk-embedding-write-worker.js +12 -10
  148. package/dist/v2/{compaction-marker-candidates-1vqwr3s5.js → compaction-marker-candidates-ebx3388k.js} +2 -2
  149. package/dist/v2/compaction-marker-candidates-worker.js +2 -2
  150. package/dist/v2/{context-4qczzck7.js → context-jn3zpeqh.js} +909 -454
  151. package/dist/v2/embedding-backfill-7c7yr69y.js +23 -0
  152. package/dist/v2/embedding-worker-process.js +1 -1
  153. package/dist/v2/embedding-worker.js +5 -5
  154. package/dist/v2/fold/native-replay.d.ts +11 -2
  155. package/dist/v2/fold/native-replay.d.ts.map +1 -1
  156. package/dist/v2/fold/policy.d.ts.map +1 -1
  157. package/dist/v2/fold/restore.d.ts.map +1 -1
  158. package/dist/v2/hooks/context.d.ts.map +1 -1
  159. package/dist/v2/hooks/hidden-child.d.ts +8 -0
  160. package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
  161. package/dist/v2/hooks/types.d.ts +17 -0
  162. package/dist/v2/hooks/types.d.ts.map +1 -1
  163. package/dist/v2/message-fts-rowid-map-wfx67gfd.js +24 -0
  164. package/dist/v2/{message-index-bounded-rxxznd34.js → message-index-bounded-hpejqyd8.js} +17 -17
  165. package/dist/v2/{message-index-f947qtbz.js → message-index-pxehjpzx.js} +15 -15
  166. package/dist/v2/{message-time-backfill-bsteyc4d.js → message-time-backfill-04t0km1z.js} +5 -5
  167. package/dist/v2/migration-worker.js +9 -7
  168. package/dist/v2/{project-embedding-registry-0msp56p8.js → project-embedding-registry-gnkj56xp.js} +12 -10
  169. package/dist/v2/raw-ordinal-warmup-worker.js +40 -10
  170. package/dist/v2/{read-session-chunk-rq9nx1nn.js → read-session-chunk-zdppn4bj.js} +9 -9
  171. package/dist/v2/{read-session-raw-b3tf6ra1.js → read-session-raw-yaegtjzh.js} +2 -2
  172. package/dist/v2/{recomp-orchestrator-5zhxtwha.js → recomp-orchestrator-cmqnfpp3.js} +26 -26
  173. package/dist/v2/{search-n8wqxb1j.js → search-66842dya.js} +23 -22
  174. package/dist/v2/search-archive-gfh11vs0.js +24 -0
  175. package/dist/v2/{send-session-notification-8ghn9tnv.js → send-session-notification-3pxn658x.js} +5 -5
  176. package/dist/v2/{server-yrvqaswb.js → server-1wy7b9de.js} +25 -11
  177. package/dist/v2/{server-th4y11y7.js → server-271qxsf5.js} +6 -6
  178. package/dist/v2/{server-e298hswg.js → server-3rhpvy84.js} +967 -88
  179. package/dist/v2/{server-qhfvvfxj.js → server-46neh5fa.js} +14 -8
  180. package/dist/v2/{server-rrqpvwqd.js → server-6tmj0s7j.js} +2 -2
  181. package/dist/v2/{server-hek6z3w6.js → server-8by8qfec.js} +11 -8
  182. package/dist/v2/{server-vbe4se57.js → server-a9xqj88f.js} +1 -1
  183. package/dist/{index-wafzvyhg.js → v2/server-beht37jy.js} +8 -0
  184. package/dist/v2/{server-05xjtq6c.js → server-d04vrx0j.js} +1 -1
  185. package/dist/v2/server-ec4ca92p.js +96 -0
  186. package/dist/v2/{server-dd0tz6cn.js → server-fw7wrk01.js} +160 -750
  187. package/dist/v2/{server-zs8d0s3b.js → server-g3qma5c0.js} +1 -1
  188. package/dist/v2/{server-k2bzedxk.js → server-gpvay0xz.js} +2 -2
  189. package/dist/v2/{server-kmbarfmn.js → server-gtfhttbr.js} +26 -18
  190. package/dist/v2/{server-g10mdym6.js → server-h1v0pyxb.js} +3 -3
  191. package/dist/v2/{server-4gn3z5an.js → server-hy3d3vsx.js} +1 -1
  192. package/dist/v2/server-kaqs6h31.js +58 -0
  193. package/dist/v2/{server-ehyava47.js → server-kh7dpa1a.js} +25 -17
  194. package/dist/v2/{server-6cdy4rc0.js → server-my7k4eds.js} +4 -4
  195. package/dist/v2/{server-d8tj10db.js → server-n1prf1g6.js} +1 -1
  196. package/dist/v2/{server-n78avt2p.js → server-n37sebck.js} +1 -1
  197. package/dist/v2/{server-zc3e54vy.js → server-nj659894.js} +6 -6
  198. package/dist/v2/{server-gvkm8xaj.js → server-rmygf5pn.js} +32 -3
  199. package/dist/v2/{server-0ppsweh6.js → server-tgnbmwah.js} +22 -2
  200. package/dist/v2/{server-cfbr1s2x.js → server-vsw8tmm6.js} +36 -76
  201. package/dist/v2/{server-prg55b7k.js → server-xj136kt8.js} +23 -3
  202. package/dist/v2/{server-atbc19ab.js → server-zyzawhdw.js} +32 -8
  203. package/dist/v2/{server-ef89gdsx.js → server-zzkj2fch.js} +4 -4
  204. package/dist/v2/server.js +5 -5
  205. package/dist/v2/{sqlite-4pqyynrs.js → sqlite-zf72c52d.js} +1 -1
  206. package/dist/v2/{storage-memory-j3v3hc83.js → storage-memory-y3qmy00r.js} +2 -2
  207. package/dist/v2/{storage-tags-hcc2adpf.js → storage-tags-1h8k6ra9.js} +8 -4
  208. package/dist/v2/store-reader.d.ts +2 -0
  209. package/dist/v2/store-reader.d.ts.map +1 -1
  210. package/dist/v2/{wrapup-orchestrator-65pwye0d.js → wrapup-orchestrator-xs8redz9.js} +26 -26
  211. package/dist/wrapup-orchestrator-j7d7ry5b.js +48 -0
  212. package/package.json +1 -1
  213. package/src/config/latch-permanence-guard.test.ts +4 -0
  214. package/src/config/schema/magic-context.ts +14 -6
  215. package/src/shared/historian-cache.test.ts +69 -0
  216. package/src/shared/historian-cache.ts +55 -0
  217. package/src/shared/model-resolution.test.ts +22 -0
  218. package/src/shared/model-suggestion-retry.ts +11 -3
  219. package/src/shared/resolve-fallbacks.test.ts +2 -3
  220. package/src/shared/resolve-fallbacks.ts +3 -3
  221. package/src/shared/rpc-discovery-cleanup.test.ts +57 -0
  222. package/src/shared/rpc-discovery-cleanup.ts +57 -0
  223. package/src/shared/rpc-server.ts +36 -13
  224. package/src/shared/search-text.test.ts +47 -0
  225. package/src/shared/search-text.ts +63 -0
  226. package/src/shared/sqlite.test.ts +56 -0
  227. package/src/shared/sqlite.ts +51 -2
  228. package/src/shared/storage-file-mutex-review.test.ts +22 -2
  229. package/src/shared/storage-file-mutex.ts +11 -1
  230. package/src/shared/subagent-runner.ts +2 -0
  231. package/src/shared/system-directive.ts +19 -1
  232. package/src/shared/tag-transcript.ts +12 -4
  233. package/dist/embedding-backfill-5e1t5wha.js +0 -22
  234. package/dist/v2/embedding-backfill-jmaf8zrr.js +0 -21
  235. package/dist/wrapup-orchestrator-28xzsj5q.js +0 -48
@@ -0,0 +1,48 @@
1
+ import {
2
+ runManagedWrapup2
3
+ } from "./index-5a1ex6x8.js";
4
+ import"./index-nkpmm9q8.js";
5
+ import"./index-a7c4r5re.js";
6
+ import"./index-4s3t0nd0.js";
7
+ import"./index-v8awdgvd.js";
8
+ import"./index-cwzfcbtq.js";
9
+ import"./index-sd3egckz.js";
10
+ import"./index-24fezq97.js";
11
+ import"./index-ze7mvk54.js";
12
+ import"./index-n37sebck.js";
13
+ import"./index-4dhv7mqz.js";
14
+ import"./index-v10f1rjv.js";
15
+ import"./index-s74zqahq.js";
16
+ import"./index-qt1qqasc.js";
17
+ import"./index-p7m7j0cp.js";
18
+ import"./index-hqhm3wke.js";
19
+ import"./index-1zx87sdm.js";
20
+ import"./index-kq82mtg0.js";
21
+ import"./index-x8a7jmkh.js";
22
+ import"./index-y9f76s3f.js";
23
+ import"./index-bjcnntmz.js";
24
+ import"./index-wgypt3cs.js";
25
+ import"./index-rr2bb45z.js";
26
+ import"./index-b5vqckbw.js";
27
+ import"./index-6x57ar45.js";
28
+ import"./index-fb2qxs2y.js";
29
+ import"./index-fb3zxn06.js";
30
+ import"./index-qn7xstt2.js";
31
+ import"./index-2vxtcb95.js";
32
+ import"./index-a6mc0v4d.js";
33
+ import"./index-fr0wbhdn.js";
34
+ import"./index-nj7yykkx.js";
35
+ import"./index-ke416mdc.js";
36
+ import"./index-bp5083x2.js";
37
+ import"./index-58e3y90n.js";
38
+ import"./index-s734jcsr.js";
39
+ import"./index-v0p37hm2.js";
40
+ import"./index-k29z4aqh.js";
41
+ import"./index-c5pytzne.js";
42
+ import"./index-beht37jy.js";
43
+ import"./index-j75v2476.js";
44
+ import"./index-g2q4htwp.js";
45
+ import"./index-pavzsdc6.js";
46
+ export {
47
+ runManagedWrapup2 as runManagedWrapup
48
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-magic-context",
3
- "version": "0.49.15",
3
+ "version": "0.49.17",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",
@@ -188,6 +188,10 @@ const KNOWN_SLOTS: Record<string, KnownSlot> = {
188
188
  classification: "DIAGNOSTIC",
189
189
  reason: "Most-recent diagnostic: every open attempt clears or overwrites it.",
190
190
  },
191
+ "packages/pi-plugin/src/pi-append-compaction.ts:loggedIncompatibleHost": {
192
+ classification: "DIAGNOSTIC",
193
+ reason: "Log-once flag for the incompatible-host error line only: the binding and getEntry checks run again on every adaptPiAppendCompaction call, so a host that becomes compatible gets markers without a restart.",
194
+ },
191
195
  "packages/plugin/src/shared/models-dev-cache.ts:apiCache": {
192
196
  classification: "PUBLICATION",
193
197
  reason: "Publishes last-known-good model metadata, not a failure verdict; refresh writes a later successful value.",
@@ -205,12 +205,14 @@ export type OmpEntry = z.infer<typeof OmpEntrySchema>;
205
205
  /** Strict model-resolution block used by historian.opencode. */
206
206
  export const OpenCodeHarnessBlockSchema = z
207
207
  .object({
208
- model: OcEntrySchema.optional().describe("Primary OpenCode model entry."),
208
+ model: OcEntrySchema.optional().describe(
209
+ "Primary historian OpenCode model entry. When set, only this model and fallback_models are used; exhaustion reports failure, never the session model. When absent, OpenCode inherits the session model.",
210
+ ),
209
211
  fallback_models: z
210
212
  .array(OcEntrySchema)
211
213
  .optional()
212
214
  .describe(
213
- "Ordered fallback OpenCode entries. New-shape configuration requires an array; legacy singleton values migrate to a one-element array.",
215
+ "Ordered historian fallback OpenCode entries. A configured primary never adds the session model after this list. Requires an array; legacy singleton values migrate to a one-element array.",
214
216
  ),
215
217
  variant: z
216
218
  .string()
@@ -226,12 +228,14 @@ export type OpenCodeHarnessBlock = z.infer<typeof OpenCodeHarnessBlockSchema>;
226
228
  /** Strict model-resolution block used by historian.pi. */
227
229
  export const PiHarnessBlockSchema = z
228
230
  .object({
229
- model: PiEntrySchema.optional().describe("Primary Pi model entry."),
231
+ model: PiEntrySchema.optional().describe(
232
+ "Primary historian Pi model entry. When set, only this model and fallback_models are used; exhaustion reports failure, never the session model. When absent, Pi does not run the historian.",
233
+ ),
230
234
  fallback_models: z
231
235
  .array(PiEntrySchema)
232
236
  .optional()
233
237
  .describe(
234
- "Ordered fallback Pi entries. New-shape configuration requires an array; legacy singleton values migrate to a one-element array.",
238
+ "Ordered historian fallback Pi entries. The session model is never appended. Requires an array; legacy singleton values migrate to a one-element array.",
235
239
  ),
236
240
  thinking_level: PiThinkingLevelSchema.describe(
237
241
  "Pi thinking level for the primary entry when it declares none. Fallback entries declare thinking levels per-entry.",
@@ -244,11 +248,15 @@ export type PiHarnessBlock = z.infer<typeof PiHarnessBlockSchema>;
244
248
  /** Strict model-resolution block used by historian.omp. */
245
249
  export const OmpHarnessBlockSchema = z
246
250
  .object({
247
- model: OmpEntrySchema.optional().describe("Primary OMP model entry."),
251
+ model: OmpEntrySchema.optional().describe(
252
+ "Primary historian OMP model entry. When set, only this model and fallback_models are used; exhaustion reports failure, never the session model. An absent OMP block inherits the Pi block; a selected block without a model does not run the historian.",
253
+ ),
248
254
  fallback_models: z
249
255
  .array(OmpEntrySchema)
250
256
  .optional()
251
- .describe("Ordered fallback OMP entries."),
257
+ .describe(
258
+ "Ordered historian fallback OMP entries. The session model is never appended.",
259
+ ),
252
260
  thinking_level: OmpThinkingLevelSchema.describe(
253
261
  "OMP thinking level for the primary entry when it declares none. Fallback entries declare thinking levels per-entry.",
254
262
  ),
@@ -0,0 +1,69 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { withoutHistorianTailCache } from "./historian-cache";
3
+
4
+ // Deliberately non-canonical JSON catches accidental parse/stringify rewrites.
5
+ const anthropicBody = String.raw`{ "model":"claude-test", "max_tokens":1e3, "system":[{"type":"text","text":"stable \\n prefix","cache_control":{"type":"ephemeral"}}], "messages":[{"role":"user","content":[{"type":"text", "text":"seed \\u0041 / chunk \ud83d\udc4b", "cache_control":{"type":"ephemeral","ttl":"5m"}}]}], "temperature":0.00 }`;
6
+ const systemOnlyBody = anthropicBody.replace(
7
+ ', "cache_control":{"type":"ephemeral","ttl":"5m"}',
8
+ "",
9
+ );
10
+
11
+ describe("historian wire cache markers", () => {
12
+ test("retry retains an existing whole-tail read and fresh repair retains the system read", () => {
13
+ const initial = JSON.parse(anthropicBody);
14
+ const prefixes = (request: typeof initial): string[] => {
15
+ let prefix = "";
16
+ const marked: string[] = [];
17
+ for (const block of [
18
+ ...request.system,
19
+ ...request.messages.flatMap((message: { content: unknown[] }) => message.content),
20
+ ]) {
21
+ prefix += block.text;
22
+ if (block.cache_control) marked.push(prefix);
23
+ }
24
+ return marked;
25
+ };
26
+ const written = new Set(prefixes(initial));
27
+ const retry = JSON.parse(anthropicBody);
28
+ expect(prefixes(retry).filter((prefix) => written.has(prefix))).toHaveLength(2);
29
+ const repair = JSON.parse(withoutHistorianTailCache(anthropicBody));
30
+ repair.messages[0].content[0].text += "\n\nValidation error and invalid XML";
31
+ expect(prefixes(repair).filter((prefix) => written.has(prefix))).toEqual([
32
+ initial.system[0].text,
33
+ ]);
34
+ expect(repair.messages[0].content[0].cache_control).toBeUndefined();
35
+ });
36
+ test("splices only tail marker bytes without changing prompt or generation bytes", () => {
37
+ expect(withoutHistorianTailCache(anthropicBody)).toBe(systemOnlyBody);
38
+ expect(JSON.parse(systemOnlyBody).system).toEqual(JSON.parse(anthropicBody).system);
39
+ expect(JSON.parse(systemOnlyBody).messages[0].content[0].text).toBe(
40
+ JSON.parse(anthropicBody).messages[0].content[0].text,
41
+ );
42
+ });
43
+ test("removes first, middle and sole properties, preserving other bytes", () => {
44
+ for (const [block, expected] of [
45
+ ['{"cache_control":{}, "type":"text","text":"x"}', '{"type":"text","text":"x"}'],
46
+ ['{"type":"text", "cache_control":{}, "text":"x"}', '{"type":"text", "text":"x"}'],
47
+ ['{"cache_control":{}}', "{}"],
48
+ ]) {
49
+ const body = JSON.stringify({
50
+ model: "claude",
51
+ max_tokens: 100,
52
+ system: [{ type: "text", text: "system", cache_control: { type: "ephemeral" } }],
53
+ messages: [{ role: "user", content: [] }],
54
+ }).replace('"content":[]', `"content":[${block}]`);
55
+ expect(withoutHistorianTailCache(body)).toBe(body.replace(block, expected));
56
+ }
57
+ });
58
+ test("unknown, wrapped and automatic-cache envelopes are byte-identical", () => {
59
+ for (const body of [
60
+ '{"model":"claude","messages":[],"max_tokens":100}',
61
+ '{"model":"gpt","messages":[{"role":"user","content":"x"}],"max_tokens":100}',
62
+ '{"anthropic_version":"bedrock-2023-05-31","messages":[],"system":[],"max_tokens":100}',
63
+ '{"instances":[{"messages":[]}]}',
64
+ anthropicBody.replace(',"cache_control":{"type":"ephemeral"}', ""),
65
+ anthropicBody.replace('"temperature":0.00', '"cache_control":{"type":"ephemeral"}'),
66
+ ])
67
+ expect(withoutHistorianTailCache(body)).toBe(body);
68
+ });
69
+ });
@@ -0,0 +1,55 @@
1
+ import { type Node, parseTree } from "jsonc-parser/lib/esm/main.js";
2
+
3
+ export type HistorianCacheTail = "write" | "skip";
4
+
5
+ /**
6
+ * Remove only message-block cache markers from a recognized Anthropic Messages
7
+ * body. Splicing property spans preserves every other wire byte, including
8
+ * number spelling, escaping and whitespace. Unknown envelopes stay untouched.
9
+ */
10
+ export function withoutHistorianTailCache(body: string): string {
11
+ const value = JSON.parse(body);
12
+ if (
13
+ !value ||
14
+ typeof value.model !== "string" ||
15
+ typeof value.max_tokens !== "number" ||
16
+ !Array.isArray(value.system) ||
17
+ value.system.length === 0 ||
18
+ !value.system.every(
19
+ (part: { type?: unknown; text?: unknown }) =>
20
+ part?.type === "text" && typeof part.text === "string",
21
+ ) ||
22
+ !value.system.some((part: { cache_control?: unknown }) => part.cache_control) ||
23
+ !Array.isArray(value.messages) ||
24
+ value.cache_control !== undefined
25
+ )
26
+ return body;
27
+
28
+ const root = parseTree(body);
29
+ const property = (node: Node | undefined, key: string): Node | undefined =>
30
+ node?.children?.find((child) => child.children?.[0]?.value === key)?.children?.[1];
31
+ const removals: Array<[number, number]> = [];
32
+ for (const message of property(root, "messages")?.children ?? []) {
33
+ for (const block of property(message, "content")?.children ?? []) {
34
+ if (block.type !== "object") continue;
35
+ const fields = block.children ?? [];
36
+ fields.forEach((field, index) => {
37
+ if (field.children?.[0]?.value !== "cache_control") return;
38
+ const next = fields[index + 1];
39
+ const previous = fields[index - 1];
40
+ removals.push(
41
+ next
42
+ ? [field.offset, next.offset]
43
+ : [
44
+ previous ? previous.offset + previous.length : field.offset,
45
+ field.offset + field.length,
46
+ ],
47
+ );
48
+ });
49
+ }
50
+ }
51
+ for (const [start, end] of removals.sort((a, b) => b[0] - a[0])) {
52
+ body = body.slice(0, start) + body.slice(end);
53
+ }
54
+ return body;
55
+ }
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { parsePluginConfig } from "../config";
2
3
 
3
4
  import {
4
5
  normalizeModelEntry,
@@ -9,6 +10,27 @@ import {
9
10
  } from "./model-resolution";
10
11
 
11
12
  describe("model-resolution", () => {
13
+ test("loader defaults and historian metadata do not configure a primary model", () => {
14
+ for (const harness of ["opencode", "pi", "omp"] as const) {
15
+ for (const input of [
16
+ {},
17
+ { historian: {} },
18
+ { historian: { temperature: 0, [harness]: {} } },
19
+ ]) {
20
+ expect(
21
+ resolveHistorianModel(parsePluginConfig(input), harness).primary,
22
+ ).toBeUndefined();
23
+ }
24
+ for (const model of [" provider/explicit ", { model: "provider/explicit" }]) {
25
+ expect(
26
+ resolveHistorianModel(
27
+ parsePluginConfig({ historian: { [harness]: { model } } }),
28
+ harness,
29
+ ).primary?.model,
30
+ ).toBe("provider/explicit");
31
+ }
32
+ }
33
+ });
12
34
  test("keeps historian temperature opt-in while preserving explicit overrides", () => {
13
35
  const fixtures = [
14
36
  { temperature: undefined, expected: undefined },
@@ -41,6 +41,8 @@ export type PromptBody = {
41
41
  export type PromptArgs = {
42
42
  path: { id: string };
43
43
  body: PromptBody;
44
+ /** Controls hidden historian message-block cache markers; never serialized in the prompt body. */
45
+ cacheTail?: import("./historian-cache").HistorianCacheTail;
44
46
  signal?: AbortSignal;
45
47
  [key: string]: unknown;
46
48
  };
@@ -72,6 +74,8 @@ export interface PromptAttemptInfo {
72
74
  export interface PromptRetryOptions {
73
75
  transport?: PromptTransport;
74
76
  timeoutMs?: number;
77
+ /** False for callers whose configured model chain must not accept provider suggestions. Defaults to true. */
78
+ allowModelSuggestions?: boolean;
75
79
  /** External abort signal — cancels the in-flight LLM prompt immediately when aborted */
76
80
  signal?: AbortSignal;
77
81
  /**
@@ -508,6 +512,7 @@ async function attemptOnce(
508
512
  callContext: string,
509
513
  label: string,
510
514
  transport?: PromptTransport,
515
+ allowModelSuggestions = true,
511
516
  ): Promise<void> {
512
517
  // Keep this snapshot separate from the object passed to the client. A
513
518
  // failed prompt facade may rewrite its request body before rejecting; the
@@ -518,9 +523,9 @@ async function attemptOnce(
518
523
  await promptWithTimeout(client, attemptArgs, timeoutMs, signal, transport);
519
524
  return;
520
525
  } catch (error) {
521
- // If non-retryable (abort, overflow, timeout), bubble up immediately.
522
- // Don't even try suggestion retry — caller needs the original error.
523
- if (isNonRetryable(error, signal)) throw error;
526
+ // Non-retryable failures and callers with model suggestions disabled
527
+ // must surface the original error without accepting a provider suggestion.
528
+ if (isNonRetryable(error, signal) || !allowModelSuggestions) throw error;
524
529
 
525
530
  const suggestion = parseModelSuggestion(error);
526
531
  if (!suggestion || !originalBody.model) {
@@ -596,6 +601,7 @@ export async function promptSyncWithModelSuggestionRetry(
596
601
  callContext,
597
602
  explicitPrimaryLabel,
598
603
  options.transport,
604
+ options.allowModelSuggestions,
599
605
  );
600
606
  return;
601
607
  } catch (error) {
@@ -640,6 +646,7 @@ export async function promptSyncWithModelSuggestionRetry(
640
646
  callContext,
641
647
  label,
642
648
  options.transport,
649
+ options.allowModelSuggestions,
643
650
  );
644
651
  log(
645
652
  `[${callContext}] fallback succeeded with ${label} (attempt ${i + 2}/${fallbacks.length + 1})`,
@@ -685,6 +692,7 @@ async function attemptAndValidate<TOutput, TValidated>(
685
692
  callContext,
686
693
  attempt.label,
687
694
  options.transport,
695
+ options.allowModelSuggestions,
688
696
  );
689
697
  } catch (error) {
690
698
  throw {
@@ -4,9 +4,8 @@ import { modelBodyField, parseProviderModel, resolveFallbackChain } from "./reso
4
4
 
5
5
  describe("resolveFallbackChain", () => {
6
6
  // Policy: user-config-only. There is NO builtin provider-agnostic chain —
7
- // when the user configures no fallback_models the result is EMPTY, and the
8
- // runner's session-model last resort (a model the user actually has) is the
9
- // only fallback. A hardcoded chain named providers the user may not have and
7
+ // when the user configures no fallback_models the result is empty.
8
+ // A hardcoded chain named providers the user may not have and
10
9
  // produced `Model not found` retry storms.
11
10
  test("returns empty when user provides nothing (no builtin chain)", () => {
12
11
  expect(resolveFallbackChain(undefined)).toEqual([]);
@@ -9,9 +9,9 @@ import type { ModelInput, ResolvedModelEntry } from "./model-resolution";
9
9
  * inevitably names providers the user doesn't have (e.g. a metapi-only user got
10
10
  * a chain of google/github-copilot/opencode entries, every one a
11
11
  * `Model not found` retry), which produced confusing errors and wasted
12
- * attempts. If the user configured nothing, this returns an empty list and the
13
- * runner's session-model last resort (the model the user is actually using) is
14
- * the only fallback.
12
+ * attempts. If the user configured no alternates, this returns an empty list.
13
+ * A configured historian never adds the session model to this list; only an
14
+ * unconfigured OpenCode historian inherits the session model.
15
15
  *
16
16
  * The returned list does NOT include the primary model — it's the ordered list
17
17
  * of *alternates* to try after the primary fails. Each entry is
@@ -0,0 +1,57 @@
1
+ import { expect, spyOn, test } from "bun:test";
2
+ import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { MagicContextRpcServer } from "./rpc-server";
6
+ import * as utils from "./rpc-utils";
7
+ import { createTestTempDirFromPath } from "./test-temp-dir";
8
+
9
+ test("RPC startup reaps dead records across projects but keeps live and inconclusive records", async () => {
10
+ const root = createTestTempDirFromPath(join(tmpdir(), "rpc-reap-"));
11
+ const server = new MagicContextRpcServer(root, "/current-project");
12
+ const dir = utils.rpcPortDir(root, "/other-project");
13
+ const empty = utils.rpcPortDir(root, "/empty-project");
14
+ mkdirSync(dir, { recursive: true });
15
+ mkdirSync(empty, { recursive: true });
16
+ for (const pid of [999999, 888888, process.pid])
17
+ writeFileSync(
18
+ join(dir, `port-${pid}.json`),
19
+ JSON.stringify({ port: 1234, pid, token: "do-not-log" }),
20
+ );
21
+ const probe = spyOn(utils, "inspectProcessesAsync").mockResolvedValue({
22
+ pi: { state: "absent", processIds: [] },
23
+ evidence: () => ({ processStartTime: null, commandLine: null, imageName: null }),
24
+ liveness: (pid: number) =>
25
+ pid === 999999 ? "dead" : pid === 888888 ? "inconclusive" : "alive",
26
+ } as Awaited<ReturnType<typeof utils.inspectProcessesAsync>>);
27
+ try {
28
+ await server.start();
29
+ expect(readdirSync(dir).sort()).toEqual(
30
+ [`port-${process.pid}.json`, "port-888888.json"].sort(),
31
+ );
32
+ expect(existsSync(empty)).toBe(false);
33
+ } finally {
34
+ server.stop();
35
+ probe.mockRestore();
36
+ rmSync(root, { recursive: true, force: true });
37
+ }
38
+ });
39
+
40
+ test("RPC stop removes its empty discovery directory but preserves other instances", async () => {
41
+ const root = createTestTempDirFromPath(join(tmpdir(), "rpc-stop-reap-"));
42
+ const first = new MagicContextRpcServer(root, root);
43
+ const second = new MagicContextRpcServer(root, root);
44
+ const directory = utils.rpcPortDir(root, root);
45
+ try {
46
+ await first.start();
47
+ await second.start();
48
+ first.stop();
49
+ expect(readdirSync(directory)).toHaveLength(1);
50
+ second.stop();
51
+ expect(existsSync(directory)).toBe(false);
52
+ } finally {
53
+ first.stop();
54
+ second.stop();
55
+ rmSync(root, { recursive: true, force: true });
56
+ }
57
+ });
@@ -0,0 +1,57 @@
1
+ import { type Dirent, promises as fs } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { inspectProcessesAsync, parseRpcPortFile } from "./rpc-utils";
4
+
5
+ /** Reap only records proven dead; live, unreadable and uncertain records stay. */
6
+ export async function reapDeadRpcDiscovery(storageDir: string): Promise<void> {
7
+ const root = join(storageDir, "rpc");
8
+ let directories: Dirent[];
9
+ try {
10
+ directories = await fs.readdir(root, { withFileTypes: true });
11
+ } catch {
12
+ return;
13
+ }
14
+ const records: { path: string; content: string; pid: number }[] = [];
15
+ const projectDirs: string[] = [];
16
+ for (const entry of directories) {
17
+ if (!entry.isDirectory() || !/^[0-9a-f]{16}$/.test(entry.name)) continue;
18
+ const directory = join(root, entry.name);
19
+ projectDirs.push(directory);
20
+ try {
21
+ for (const name of await fs.readdir(directory)) {
22
+ const match = /^port-(\d+)(?:-[0-9a-f]+)?\.json$/.exec(name);
23
+ if (!match) continue;
24
+ const path = join(directory, name);
25
+ try {
26
+ const content = await fs.readFile(path, "utf8");
27
+ const record = parseRpcPortFile(content, Number(match[1]));
28
+ if (record && record.pid === Number(match[1]))
29
+ records.push({ path, content, pid: record.pid });
30
+ } catch {
31
+ // Another host may be publishing or stopping this record.
32
+ }
33
+ }
34
+ } catch {
35
+ // An inaccessible project must not prevent cleanup of other projects.
36
+ }
37
+ }
38
+ // Read records before a fresh process snapshot so a recently started host
39
+ // cannot be mistaken for a PID missing from an older cached snapshot.
40
+ const processes = records.length ? await inspectProcessesAsync(true) : undefined;
41
+ for (const record of records) {
42
+ if (processes?.liveness(record.pid) !== "dead") continue;
43
+ try {
44
+ if ((await fs.readFile(record.path, "utf8")) === record.content)
45
+ await fs.unlink(record.path);
46
+ } catch {
47
+ // Leave the record for a later pass; never recursively remove its directory.
48
+ }
49
+ }
50
+ for (const directory of projectDirs) {
51
+ try {
52
+ await fs.rmdir(directory);
53
+ } catch {
54
+ // Leave non-empty directories untouched; they may still contain records for hosts not proven dead.
55
+ }
56
+ }
57
+ }
@@ -1,7 +1,8 @@
1
1
  import { randomBytes, timingSafeEqual } from "node:crypto";
2
- import { readdirSync, readFileSync, unlinkSync } from "node:fs";
2
+ import { readdirSync, readFileSync, rmdirSync, unlinkSync } from "node:fs";
3
3
  import type { Server, ServerWebSocket } from "bun";
4
4
  import { log } from "./logger";
5
+ import { reapDeadRpcDiscovery } from "./rpc-discovery-cleanup";
5
6
  import {
6
7
  acknowledgeNotifications,
7
8
  drainNotifications,
@@ -82,6 +83,7 @@ export class MagicContextRpcServer {
82
83
  private portFilePath: string;
83
84
  private portDir: string;
84
85
  private startedAt = Date.now();
86
+ private stopped = false;
85
87
  private readonly instanceId = randomBytes(8).toString("hex");
86
88
  private unregisterOwnInstance: (() => void) | null = null;
87
89
  /** Every authenticated WS socket, so dispose can close them all. */
@@ -93,7 +95,10 @@ export class MagicContextRpcServer {
93
95
  // discovers/guesses the port.
94
96
  private readonly token = randomBytes(32).toString("hex");
95
97
 
96
- constructor(storageDir: string, directory: string) {
98
+ constructor(
99
+ private storageDir: string,
100
+ directory: string,
101
+ ) {
97
102
  this.portFilePath = rpcPortFilePath(storageDir, directory, process.pid, this.instanceId);
98
103
  this.portDir = rpcPortDir(storageDir, directory);
99
104
  }
@@ -131,6 +136,9 @@ export class MagicContextRpcServer {
131
136
  log("rpc server skipped: Bun runtime not available (no TUI consumer)");
132
137
  return 0;
133
138
  }
139
+ this.stopped = false;
140
+ await reapDeadRpcDiscovery(this.storageDir);
141
+ if (this.stopped) return 0;
134
142
  this.startedAt = Date.now();
135
143
  const self = this;
136
144
  const server = inRootAsyncContext(() =>
@@ -172,17 +180,26 @@ export class MagicContextRpcServer {
172
180
  // The discovery file carries the RPC bearer token. The shared writer
173
181
  // creates its directory and temporary file with owner-only modes.
174
182
  this.unregisterOwnInstance ??= registerOwnRpcServerInstance(this.instanceId);
175
- writeStorageFileAtomicSync(
176
- this.portFilePath,
177
- JSON.stringify({
178
- port: this.port,
179
- pid: process.pid,
180
- started_at: this.startedAt,
181
- kind: "OpenCode server",
182
- token: this.token,
183
- instance_id: this.instanceId,
184
- }),
185
- );
183
+ const publish = () =>
184
+ writeStorageFileAtomicSync(
185
+ this.portFilePath,
186
+ JSON.stringify({
187
+ port: this.port,
188
+ pid: process.pid,
189
+ started_at: this.startedAt,
190
+ kind: "OpenCode server",
191
+ token: this.token,
192
+ instance_id: this.instanceId,
193
+ }),
194
+ );
195
+ try {
196
+ publish();
197
+ } catch (error) {
198
+ // An empty-directory reaper can race the writer before its temp
199
+ // file exists. Recreate the directory and retry that case only.
200
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
201
+ publish();
202
+ }
186
203
  log(`[rpc] server listening on 127.0.0.1:${this.port}`);
187
204
  } catch (err) {
188
205
  log(`[rpc] failed to write port file: ${err}`);
@@ -193,6 +210,7 @@ export class MagicContextRpcServer {
193
210
 
194
211
  /** Stop the server: close every socket, stop accepting, remove port file. */
195
212
  stop(): void {
213
+ this.stopped = true;
196
214
  for (const ws of this.sockets) {
197
215
  try {
198
216
  if (ws.data.authTimer) clearTimeout(ws.data.authTimer);
@@ -213,6 +231,11 @@ export class MagicContextRpcServer {
213
231
  } catch {
214
232
  // Intentional: port file may already be gone
215
233
  }
234
+ try {
235
+ rmdirSync(this.portDir);
236
+ } catch {
237
+ // Other instances' discovery records must survive this server's stop.
238
+ }
216
239
  this.unregisterOwnInstance?.();
217
240
  this.unregisterOwnInstance = null;
218
241
  }
@@ -0,0 +1,47 @@
1
+ import { expect, test } from "bun:test";
2
+ import { cleanSearchText, keywordRelevance, matchedSearchSnippet } from "./search-text";
3
+ import { removeSystemReminders } from "./system-directive";
4
+
5
+ test("nested reminder contents stay quarantined through the outer closing tag", () => {
6
+ expect(
7
+ removeSystemReminders(
8
+ 'before <SYSTEM-REMINDER origin="host">outer <system-reminder>inner</system-reminder> leaked tail</SYSTEM-REMINDER> after',
9
+ ),
10
+ ).toBe("before after");
11
+ expect(removeSystemReminders("before <system-reminder>unfinished")).toBe("before");
12
+ expect(removeSystemReminders("before </system-reminder> after")).toBe("before after");
13
+ });
14
+
15
+ test("mismatched notification closes cannot release hidden content", () => {
16
+ expect(cleanSearchText("kept <task>hidden </task_cohort> still hidden</task> kept")).toBe(
17
+ "kept kept",
18
+ );
19
+ expect(cleanSearchText("kept <task id='finished'/> kept")).toBe("kept kept");
20
+ });
21
+
22
+ test("task notification trees and cut legacy XML never become hint text", () => {
23
+ expect(
24
+ cleanSearchText(
25
+ '<task_cohort id="parent"><task id="child">background result</task>status</task_cohort> actual answer',
26
+ ),
27
+ ).toBe("actual answer");
28
+ expect(cleanSearchText('<task id="truncated">background result…')).toBe("");
29
+ });
30
+
31
+ test("keyword confidence measures absolute distinct term coverage, not result rank", () => {
32
+ expect(keywordRelevance("explain historian cache wiring", "cache icons")).toBeCloseTo(0.2);
33
+ expect(
34
+ keywordRelevance("historian cache wiring", "historian cache wiring details"),
35
+ ).toBeCloseTo(0.8);
36
+ expect(keywordRelevance("historian cache wiring", "<task>historian cache wiring</task>")).toBe(
37
+ 0,
38
+ );
39
+ });
40
+
41
+ test("matched snippets prefer a dense passage over an irrelevant first line", () => {
42
+ const content = `Header cache. ${"padding ".repeat(150)}Historian cache wiring uses incremental pools. ${"trailing ".repeat(100)}`;
43
+ const snippet = matchedSearchSnippet(content, "historian cache wiring", 80);
44
+ expect(snippet).toContain("Historian cache wiring");
45
+ expect(snippet).not.toContain("Header");
46
+ expect(matchedSearchSnippet(content, "unrelated deployment")).toBe("");
47
+ });