@cortexkit/opencode-magic-context 0.49.19 → 0.49.20

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 (271) hide show
  1. package/dist/auto-search-worker.js +48 -9
  2. package/dist/chunk-embedding-write-worker.js +30 -23
  3. package/dist/{compaction-marker-candidates-n4krhat4.js → compaction-marker-candidates-b2q30cft.js} +3 -3
  4. package/dist/compaction-marker-candidates-worker.js +2 -2
  5. package/dist/compartment-chunk-embedding-va090as0.js +65 -0
  6. package/dist/config/schema/magic-context.d.ts +6 -6
  7. package/dist/{conflict-warning-hook-4pmz879v.js → conflict-warning-hook-frp3z2wk.js} +9 -8
  8. package/dist/{context-j31m2dv7.js → context-mkece1db.js} +455 -134
  9. package/dist/{derive-budgets-zn7yzd2d.js → derive-budgets-pqybnx09.js} +2 -2
  10. package/dist/embedding-backfill-gy3p78py.js +25 -0
  11. package/dist/embedding-worker-process.js +2 -2
  12. package/dist/embedding-worker.js +5 -5
  13. package/dist/features/magic-context/chunk-embedding-writer.d.ts.map +1 -1
  14. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +44 -1
  15. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  16. package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/task-config.d.ts.map +1 -1
  18. package/dist/features/magic-context/dreamer/task-scheduler.d.ts +2 -0
  19. package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
  20. package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
  21. package/dist/features/magic-context/memory/embedding-local.d.ts +1 -0
  22. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  23. package/dist/features/magic-context/memory/local-embedding-pooling.d.ts +22 -0
  24. package/dist/features/magic-context/memory/local-embedding-pooling.d.ts.map +1 -0
  25. package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
  26. package/dist/features/magic-context/message-index.d.ts +2 -0
  27. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  28. package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
  29. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  30. package/dist/features/magic-context/search.d.ts +27 -0
  31. package/dist/features/magic-context/search.d.ts.map +1 -1
  32. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  33. package/dist/features/magic-context/storage-meta-persisted.d.ts +1 -0
  34. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  35. package/dist/features/magic-context/subagent-token-capture.d.ts +20 -0
  36. package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -1
  37. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  38. package/dist/{history-owner-xp3pngv7.js → history-owner-36wp3mkb.js} +2 -2
  39. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  40. package/dist/hooks/magic-context/auto-embed-session.d.ts.map +1 -1
  41. package/dist/hooks/magic-context/auto-search-hint.d.ts +7 -1
  42. package/dist/hooks/magic-context/auto-search-hint.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  44. package/dist/hooks/magic-context/auto-search-worker-client.d.ts +33 -6
  45. package/dist/hooks/magic-context/auto-search-worker-client.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/cache-busting-signals.d.ts +5 -2
  47. package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
  48. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  49. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  50. package/dist/hooks/magic-context/compartment-runner-types.d.ts +5 -0
  51. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  52. package/dist/hooks/magic-context/compartment-trigger.d.ts +1 -1
  53. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  54. package/dist/hooks/magic-context/diagnostic-log-state.d.ts +4 -0
  55. package/dist/hooks/magic-context/diagnostic-log-state.d.ts.map +1 -0
  56. package/dist/hooks/magic-context/execute-reclaim.d.ts +35 -0
  57. package/dist/hooks/magic-context/execute-reclaim.d.ts.map +1 -0
  58. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  59. package/dist/hooks/magic-context/hook.d.ts +1 -3
  60. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  61. package/dist/hooks/magic-context/latest-assistant-turn.d.ts +8 -0
  62. package/dist/hooks/magic-context/latest-assistant-turn.d.ts.map +1 -1
  63. package/dist/hooks/magic-context/lkg-recovery-fence.d.ts +5 -0
  64. package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -1
  65. package/dist/hooks/magic-context/nudge-evaluation-log.d.ts +3 -0
  66. package/dist/hooks/magic-context/nudge-evaluation-log.d.ts.map +1 -0
  67. package/dist/hooks/magic-context/pending-drop-eligibility.d.ts.map +1 -1
  68. package/dist/hooks/magic-context/protected-tail-boundary.d.ts +4 -0
  69. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  70. package/dist/hooks/magic-context/raw-ordinal-warmup.d.ts.map +1 -1
  71. package/dist/hooks/magic-context/system-date-refresh.d.ts +5 -5
  72. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  73. package/dist/hooks/magic-context/system-prompt-sticky-date.d.ts +10 -0
  74. package/dist/hooks/magic-context/system-prompt-sticky-date.d.ts.map +1 -0
  75. package/dist/hooks/magic-context/tag-messages.d.ts +2 -0
  76. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  78. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +21 -6
  79. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  80. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  81. package/dist/{index-bjcnntmz.js → index-0wthn7fp.js} +468 -18
  82. package/dist/{index-kq82mtg0.js → index-11zf2wwc.js} +3 -3
  83. package/dist/{index-b5vqckbw.js → index-153r2q6z.js} +314 -127
  84. package/dist/{index-3h4dp9wb.js → index-1pefp8ar.js} +5 -5
  85. package/dist/{index-beht37jy.js → index-1zx2pa5d.js} +1 -1
  86. package/dist/{index-ze7mvk54.js → index-2yhtnfcf.js} +75 -57
  87. package/dist/{index-cf74k5ak.js → index-305y133n.js} +4 -4
  88. package/dist/{index-s734jcsr.js → index-35qc6606.js} +9 -3
  89. package/dist/{index-se4m2g6w.js → index-48stjd56.js} +105 -19
  90. package/dist/{index-4911rn0b.js → index-4n7w9rad.js} +6 -6
  91. package/dist/{index-kee4kn5j.js → index-4pnsgvfk.js} +1 -1
  92. package/dist/{v2/server-8cd2j99z.js → index-5kym00ag.js} +32 -13
  93. package/dist/{index-4s3t0nd0.js → index-616rsjrv.js} +2 -2
  94. package/dist/{index-jhghdnsm.js → index-67nyj0gy.js} +485 -202
  95. package/dist/{index-6x57ar45.js → index-adk2z5d8.js} +3 -3
  96. package/dist/{index-c5pytzne.js → index-b88dwwrh.js} +1 -1
  97. package/dist/{index-rr2bb45z.js → index-c4b76d4t.js} +2 -2
  98. package/dist/{index-nj7yykkx.js → index-c7phnbvk.js} +1 -1
  99. package/dist/index-cpjk5jd2.js +82 -0
  100. package/dist/{index-edmpdxcw.js → index-d6rp4dxh.js} +5 -5
  101. package/dist/{index-dxmq29t1.js → index-dc8hy95c.js} +69 -17
  102. package/dist/{index-j75v2476.js → index-h7tgn49j.js} +12 -15
  103. package/dist/{index-ke416mdc.js → index-j76ah6d7.js} +2 -2
  104. package/dist/{index-c1p88z1y.js → index-jfh3cktr.js} +4 -4
  105. package/dist/{index-a6mc0v4d.js → index-jmycsey7.js} +3 -3
  106. package/dist/{index-24fezq97.js → index-jr5na10t.js} +4 -4
  107. package/dist/{index-s74zqahq.js → index-kc9pn495.js} +4 -4
  108. package/dist/{index-p5y3k4g7.js → index-nda0zgna.js} +2 -1
  109. package/dist/{index-jn12fvvh.js → index-neb2jaed.js} +3 -3
  110. package/dist/{index-63k48rv9.js → index-pnjwa6qa.js} +5 -5
  111. package/dist/{index-jrr131e0.js → index-rmd0069m.js} +11 -11
  112. package/dist/{index-y9f76s3f.js → index-rrv26qjz.js} +1 -1
  113. package/dist/{index-v10f1rjv.js → index-spwjbkc7.js} +1 -1
  114. package/dist/{index-k29z4aqh.js → index-t5pck3ss.js} +1 -1
  115. package/dist/{index-3esx8tkf.js → index-t7hx3qcb.js} +318 -104
  116. package/dist/{index-rpxdjkm0.js → index-v7r4td4c.js} +306 -59
  117. package/dist/{index-2vxtcb95.js → index-vkc1ca9p.js} +2 -2
  118. package/dist/{index-9zcsy3hh.js → index-x4k3f4mf.js} +17 -9
  119. package/dist/{index-bp5083x2.js → index-x9k3mqrf.js} +1 -1
  120. package/dist/{index-n37sebck.js → index-y4a3nhtk.js} +1 -1
  121. package/dist/index.js +52 -49
  122. package/dist/{logger-jm3sqqzr.js → logger-94kew5wm.js} +1 -1
  123. package/dist/{message-fts-rowid-map-17s0dfn4.js → message-fts-rowid-map-22tevnnj.js} +6 -5
  124. package/dist/{message-index-gtpqnj8w.js → message-index-5qxcz6h7.js} +16 -15
  125. package/dist/{message-index-bounded-zyc8h32y.js → message-index-bounded-n0m0t7tv.js} +16 -15
  126. package/dist/{message-time-backfill-73mmm76q.js → message-time-backfill-0myx7fz3.js} +3 -3
  127. package/dist/migration-worker.js +13 -12
  128. package/dist/{model-resolution-nx97z3x7.js → model-resolution-280gmqvd.js} +1 -1
  129. package/dist/plugin/embedding-bootstrap.d.ts +3 -1
  130. package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
  131. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  132. package/dist/plugin/rpc-handlers.d.ts +8 -0
  133. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  134. package/dist/plugin/subagent-tool-policy.d.ts +9 -0
  135. package/dist/plugin/subagent-tool-policy.d.ts.map +1 -1
  136. package/dist/{project-embedding-registry-z74mhqkf.js → project-embedding-registry-s2pqgec9.js} +13 -12
  137. package/dist/raw-ordinal-warmup-worker.js +136 -101
  138. package/dist/{read-session-chunk-jjb48bt9.js → read-session-chunk-0a8vdyqp.js} +11 -10
  139. package/dist/{read-session-raw-rsc8c7qy.js → read-session-raw-y5cszf0b.js} +4 -3
  140. package/dist/recomp-orchestrator-61vyha9j.js +62 -0
  141. package/dist/{safe-notification-target-1szv1k5k.js → safe-notification-target-mkr9yzvr.js} +2 -2
  142. package/dist/{search-archive-6vm0e2f5.js → search-archive-8q9s49p5.js} +12 -11
  143. package/dist/{search-xsqk1bj4.js → search-v1y9ejws.js} +25 -24
  144. package/dist/{send-session-notification-6ve0e8p4.js → send-session-notification-0k6qzydk.js} +7 -6
  145. package/dist/shared/logger.d.ts.map +1 -1
  146. package/dist/shared/model-resolution.d.ts +2 -0
  147. package/dist/shared/model-resolution.d.ts.map +1 -1
  148. package/dist/shared/prompt-async-transport.d.ts +3 -0
  149. package/dist/shared/prompt-async-transport.d.ts.map +1 -1
  150. package/dist/shared/rpc-types.d.ts +7 -0
  151. package/dist/shared/rpc-types.d.ts.map +1 -1
  152. package/dist/shared/sqlite.d.ts +1 -1
  153. package/dist/shared/sqlite.d.ts.map +1 -1
  154. package/dist/shared/subagent-runner.d.ts +2 -0
  155. package/dist/shared/subagent-runner.d.ts.map +1 -1
  156. package/dist/shared/worker-shutdown.d.ts +68 -0
  157. package/dist/shared/worker-shutdown.d.ts.map +1 -0
  158. package/dist/shared/worker-shutdown.fixture.d.ts +2 -0
  159. package/dist/shared/worker-shutdown.fixture.d.ts.map +1 -0
  160. package/dist/{sqlite-zf72c52d.js → sqlite-1rpvw2rj.js} +1 -1
  161. package/dist/{storage-memory-y3qmy00r.js → storage-memory-nm7v82mj.js} +2 -2
  162. package/dist/{storage-tags-r7k9tjcv.js → storage-tags-r6nk29n4.js} +5 -5
  163. package/dist/{task-config-x3qmayd2.js → task-config-t6bpyka7.js} +2 -2
  164. package/dist/tools/ctx-search/tools.d.ts.map +1 -1
  165. package/dist/{transform-postprocess-phase-jj99r7z2.js → transform-postprocess-phase-vtrgb7m6.js} +34 -31
  166. package/dist/v2/auto-search-worker.js +48 -9
  167. package/dist/v2/chunk-embedding-write-worker.js +30 -23
  168. package/dist/v2/{compaction-marker-candidates-ebx3388k.js → compaction-marker-candidates-cfab70cs.js} +3 -3
  169. package/dist/v2/compaction-marker-candidates-worker.js +2 -2
  170. package/dist/v2/compartment-chunk-embedding-nbdqx1td.js +64 -0
  171. package/dist/v2/{context-y5yamxak.js → context-cc33tv2z.js} +1189 -386
  172. package/dist/v2/{derive-budgets-2zh4gm2c.js → derive-budgets-knf763tt.js} +3 -3
  173. package/dist/v2/embedding-backfill-yxv2h5mc.js +24 -0
  174. package/dist/v2/embedding-worker-process.js +2 -2
  175. package/dist/v2/embedding-worker.js +5 -5
  176. package/dist/v2/hidden-child-record.d.ts +2 -0
  177. package/dist/v2/hidden-child-record.d.ts.map +1 -1
  178. package/dist/v2/hidden-completion.d.ts +2 -0
  179. package/dist/v2/hidden-completion.d.ts.map +1 -1
  180. package/dist/v2/hidden-model-activity.d.ts +7 -0
  181. package/dist/v2/hidden-model-activity.d.ts.map +1 -0
  182. package/dist/v2/{history-owner-42e16w0n.js → history-owner-ad0crnrn.js} +2 -2
  183. package/dist/v2/hooks/context.d.ts.map +1 -1
  184. package/dist/v2/hooks/generate.d.ts +36 -1
  185. package/dist/v2/hooks/generate.d.ts.map +1 -1
  186. package/dist/v2/hooks/lkg-system.d.ts +35 -4
  187. package/dist/v2/hooks/lkg-system.d.ts.map +1 -1
  188. package/dist/v2/hooks/payload.d.ts +16 -1
  189. package/dist/v2/hooks/payload.d.ts.map +1 -1
  190. package/dist/v2/{logger-7qt02bnb.js → logger-p34mjtja.js} +1 -1
  191. package/dist/v2/{message-fts-rowid-map-wfx67gfd.js → message-fts-rowid-map-dg12wymk.js} +6 -5
  192. package/dist/v2/{message-index-bounded-spd8r7pj.js → message-index-bounded-f4e01xk0.js} +16 -15
  193. package/dist/v2/{message-index-8vz2pq76.js → message-index-ydzrp6y6.js} +16 -15
  194. package/dist/v2/{message-time-backfill-04t0km1z.js → message-time-backfill-ddh8n9gy.js} +3 -3
  195. package/dist/v2/migration-worker.js +13 -12
  196. package/dist/v2/{model-resolution-nx97z3x7.js → model-resolution-280gmqvd.js} +1 -1
  197. package/dist/v2/{project-embedding-registry-gnkj56xp.js → project-embedding-registry-deza9ksw.js} +13 -12
  198. package/dist/v2/raw-ordinal-warmup-worker.js +136 -101
  199. package/dist/v2/{read-session-chunk-cwkxrmns.js → read-session-chunk-8cxfagky.js} +11 -10
  200. package/dist/v2/{read-session-raw-yaegtjzh.js → read-session-raw-1nrrecvx.js} +4 -3
  201. package/dist/v2/recomp-orchestrator-47m4hv5g.js +61 -0
  202. package/dist/v2/{safe-notification-target-qg7n2s77.js → safe-notification-target-khnzg57c.js} +2 -2
  203. package/dist/v2/{search-archive-wve3tq1k.js → search-archive-e80cvc09.js} +12 -11
  204. package/dist/v2/{search-hwg7jfbj.js → search-fjz746zr.js} +25 -24
  205. package/dist/v2/{send-session-notification-3pxn658x.js → send-session-notification-nwvrv34d.js} +7 -6
  206. package/dist/v2/{server-8by8qfec.js → server-0epqaxhd.js} +75 -57
  207. package/dist/v2/{server-pwv4ezb0.js → server-168pb207.js} +1 -1
  208. package/dist/v2/{server-h0410h0v.js → server-1bw1wcbp.js} +1 -1
  209. package/dist/v2/{server-beht37jy.js → server-1zx2pa5d.js} +1 -1
  210. package/dist/v2/{server-a9xqj88f.js → server-230fnbws.js} +4 -4
  211. package/dist/v2/{server-tbe1femw.js → server-29qp8nz4.js} +1 -1
  212. package/dist/v2/{server-tgnbmwah.js → server-3mz2qh5a.js} +1 -1
  213. package/dist/v2/{server-hy3d3vsx.js → server-4nw4fqhr.js} +2 -2
  214. package/dist/v2/{server-45q69hkq.js → server-4sxjs6hn.js} +5 -5
  215. package/dist/v2/{server-34kwfsr4.js → server-53eh1r8r.js} +2 -2
  216. package/dist/v2/{server-xj136kt8.js → server-55rgxfrw.js} +9 -3
  217. package/dist/v2/{server-nj659894.js → server-5arr9ecn.js} +2 -2
  218. package/dist/{index-8cd2j99z.js → v2/server-5kym00ag.js} +32 -13
  219. package/dist/v2/{server-2ar62dyx.js → server-63fqdghs.js} +106 -20
  220. package/dist/v2/{server-f2xf5gcb.js → server-6495nmn9.js} +12 -15
  221. package/dist/v2/{server-qd11g795.js → server-92g7d776.js} +2 -1
  222. package/dist/v2/{server-xq7dfjke.js → server-9x0kmta4.js} +5 -5
  223. package/dist/v2/{server-27gq4mfz.js → server-a3dwxfgd.js} +4 -4
  224. package/dist/v2/{server-6tmj0s7j.js → server-af2pght7.js} +3 -3
  225. package/dist/v2/{server-1wy7b9de.js → server-br1x0tev.js} +3 -3
  226. package/dist/v2/{server-n1prf1g6.js → server-brj8j2gk.js} +2 -2
  227. package/dist/v2/{server-6vdzn1td.js → server-c7dxxsj7.js} +308 -61
  228. package/dist/v2/server-cpjk5jd2.js +82 -0
  229. package/dist/v2/{server-g3qma5c0.js → server-dj7tz4ph.js} +2 -2
  230. package/dist/v2/{server-9hx7g5jp.js → server-ej96a39v.js} +17 -9
  231. package/dist/v2/{server-6pmqg9m4.js → server-gyy4k6mf.js} +1 -1
  232. package/dist/v2/{server-bmymkx73.js → server-hqzyq8n4.js} +11 -11
  233. package/dist/v2/{server-h1v0pyxb.js → server-j4jnp9b5.js} +468 -18
  234. package/dist/v2/{server-da07v4xp.js → server-k3t06fxr.js} +1 -1
  235. package/dist/v2/{server-gsxjv2q2.js → server-m91f9m5s.js} +69 -17
  236. package/dist/v2/{server-rmygf5pn.js → server-ne8arh8t.js} +2 -2
  237. package/dist/v2/{server-hm38q7r0.js → server-vn2nwcv6.js} +5 -5
  238. package/dist/v2/{server-46neh5fa.js → server-wyawsa88.js} +314 -127
  239. package/dist/v2/{server-zzkj2fch.js → server-xsryvyf6.js} +4 -4
  240. package/dist/v2/{server-n37sebck.js → server-y4a3nhtk.js} +1 -1
  241. package/dist/v2/{server-d04vrx0j.js → server-ygtykank.js} +2 -2
  242. package/dist/v2/server.js +6 -6
  243. package/dist/v2/{sqlite-zf72c52d.js → sqlite-1rpvw2rj.js} +1 -1
  244. package/dist/v2/{storage-memory-y3qmy00r.js → storage-memory-nm7v82mj.js} +2 -2
  245. package/dist/v2/{storage-tags-3wdb8zph.js → storage-tags-ss08kj5r.js} +5 -5
  246. package/dist/v2/store-reader.d.ts +8 -0
  247. package/dist/v2/store-reader.d.ts.map +1 -1
  248. package/dist/v2/{task-config-nvz8mapa.js → task-config-7md27rx8.js} +2 -2
  249. package/dist/v2/{wrapup-orchestrator-embdz8gj.js → wrapup-orchestrator-639whfhq.js} +32 -31
  250. package/dist/wrapup-orchestrator-mnvt2g8m.js +49 -0
  251. package/package.json +1 -1
  252. package/src/config/index.test.ts +28 -6
  253. package/src/shared/logger.test.ts +42 -1
  254. package/src/shared/logger.ts +11 -13
  255. package/src/shared/model-resolution.test.ts +67 -12
  256. package/src/shared/model-resolution.ts +43 -16
  257. package/src/shared/model-suggestion-retry.ts +1 -1
  258. package/src/shared/prompt-async-transport.ts +47 -3
  259. package/src/shared/rpc-types.ts +7 -0
  260. package/src/shared/sqlite-busy-budget-review.test.ts +6 -1
  261. package/src/shared/sqlite.ts +4 -2
  262. package/src/shared/subagent-runner.ts +2 -0
  263. package/src/shared/worker-shutdown-fence.test.ts +87 -0
  264. package/src/shared/worker-shutdown.fixture.ts +25 -0
  265. package/src/shared/worker-shutdown.test.ts +127 -0
  266. package/src/shared/worker-shutdown.ts +160 -0
  267. package/dist/embedding-backfill-fmz50tsy.js +0 -24
  268. package/dist/recomp-orchestrator-999jj4bc.js +0 -61
  269. package/dist/v2/embedding-backfill-7c7yr69y.js +0 -23
  270. package/dist/v2/recomp-orchestrator-f6m9jr7p.js +0 -60
  271. package/dist/wrapup-orchestrator-whhbjmaz.js +0 -48
@@ -118,12 +118,22 @@ describe("model-resolution", () => {
118
118
  // Inheritance is deliberately absent because a fallback model may not support the
119
119
  // qualifier selected for the primary model.
120
120
  expect(resolveHistorianModel(config, "opencode")).toEqual({
121
- primary: { model: "open/primary", qualifier: "primary-only" },
122
- fallbacks: [{ model: "open/fallback" }],
121
+ primary: {
122
+ model: "open/primary",
123
+ qualifier: "primary-only",
124
+ sourceConfigKey: "historian.opencode.model",
125
+ },
126
+ fallbacks: [
127
+ { model: "open/fallback", sourceConfigKey: "historian.opencode.fallback_models" },
128
+ ],
123
129
  });
124
130
  expect(resolveHistorianModel(config, "pi")).toEqual({
125
- primary: { model: "pi/primary", qualifier: "high" },
126
- fallbacks: [{ model: "pi/fallback" }],
131
+ primary: {
132
+ model: "pi/primary",
133
+ qualifier: "high",
134
+ sourceConfigKey: "historian.pi.model",
135
+ },
136
+ fallbacks: [{ model: "pi/fallback", sourceConfigKey: "historian.pi.fallback_models" }],
127
137
  });
128
138
  });
129
139
 
@@ -143,12 +153,32 @@ describe("model-resolution", () => {
143
153
  };
144
154
 
145
155
  expect(resolveHistorianModel(config, "opencode")).toEqual({
146
- primary: { model: "open/code", qualifier: "oc-primary" },
147
- fallbacks: [{ model: "open/fallback", qualifier: "oc-fallback" }],
156
+ primary: {
157
+ model: "open/code",
158
+ qualifier: "oc-primary",
159
+ sourceConfigKey: "historian.opencode.model",
160
+ },
161
+ fallbacks: [
162
+ {
163
+ model: "open/fallback",
164
+ qualifier: "oc-fallback",
165
+ sourceConfigKey: "historian.opencode.fallback_models",
166
+ },
167
+ ],
148
168
  });
149
169
  expect(resolveHistorianModel(config, "pi")).toEqual({
150
- primary: { model: "pi/model", qualifier: "high" },
151
- fallbacks: [{ model: "pi/fallback", qualifier: "max" }],
170
+ primary: {
171
+ model: "pi/model",
172
+ qualifier: "high",
173
+ sourceConfigKey: "historian.pi.model",
174
+ },
175
+ fallbacks: [
176
+ {
177
+ model: "pi/fallback",
178
+ qualifier: "max",
179
+ sourceConfigKey: "historian.pi.fallback_models",
180
+ },
181
+ ],
152
182
  });
153
183
  });
154
184
 
@@ -182,8 +212,18 @@ describe("model-resolution", () => {
182
212
  };
183
213
 
184
214
  expect(resolveHistorianModel(configured, "omp")).toEqual({
185
- primary: { model: "omp/historian", qualifier: "auto" },
186
- fallbacks: [{ model: "omp/historian-fallback", qualifier: "max" }],
215
+ primary: {
216
+ model: "omp/historian",
217
+ qualifier: "auto",
218
+ sourceConfigKey: "historian.omp.model",
219
+ },
220
+ fallbacks: [
221
+ {
222
+ model: "omp/historian-fallback",
223
+ qualifier: "max",
224
+ sourceConfigKey: "historian.omp.fallback_models",
225
+ },
226
+ ],
187
227
  });
188
228
  expect(
189
229
  resolveDreamerTaskModel({ config: configured, harness: "omp", task: "verify" }),
@@ -197,6 +237,7 @@ describe("model-resolution", () => {
197
237
  expect(resolveHistorianModel(piFallback, "omp")?.primary).toEqual({
198
238
  model: "pi/historian",
199
239
  qualifier: "high",
240
+ sourceConfigKey: "historian.pi.model",
200
241
  });
201
242
  expect(
202
243
  resolveDreamerTaskModel({ config: piFallback, harness: "omp", task: "verify" }),
@@ -245,8 +286,22 @@ describe("model-resolution", () => {
245
286
  };
246
287
 
247
288
  expect(resolveDreamerTaskModel({ config, harness: "opencode", task: "curate" })).toEqual({
248
- primary: { model: "open/task", qualifier: "task-variant" },
249
- fallbacks: [{ model: "open/bare" }, { model: "open/qualified", qualifier: "fb" }],
289
+ primary: {
290
+ model: "open/task",
291
+ qualifier: "task-variant",
292
+ sourceConfigKey: "dreamer.opencode.tasks.curate.model",
293
+ },
294
+ fallbacks: [
295
+ {
296
+ model: "open/bare",
297
+ sourceConfigKey: "dreamer.opencode.tasks.curate.fallback_models",
298
+ },
299
+ {
300
+ model: "open/qualified",
301
+ qualifier: "fb",
302
+ sourceConfigKey: "dreamer.opencode.tasks.curate.fallback_models",
303
+ },
304
+ ],
250
305
  schedule: "0 4 * * *",
251
306
  timeoutMinutes: 12,
252
307
  promotionThreshold: 3,
@@ -5,6 +5,8 @@ export interface ResolvedModelEntry {
5
5
  model: string;
6
6
  /** OpenCode variant or Pi thinking level, selected for the active entry only. */
7
7
  qualifier?: string;
8
+ /** Config location that supplied this attempt, for accurate failure diagnostics. */
9
+ sourceConfigKey?: string;
8
10
  }
9
11
 
10
12
  export type ModelInput = string | ResolvedModelEntry;
@@ -35,6 +37,10 @@ function resolveHarnessBlock(
35
37
  return asRecord(selected);
36
38
  }
37
39
 
40
+ function sourceHarness(container: ConfigRecord | undefined, harness: ModelHarness): ModelHarness {
41
+ return harness === "omp" && container?.omp == null ? "pi" : harness;
42
+ }
43
+
38
44
  /**
39
45
  * Collapses the string and object spellings of a harness model entry into the
40
46
  * same model identity. Qualifiers stay separate because they alter an attempt,
@@ -116,14 +122,20 @@ export function resolveHistorianModel(
116
122
  const historian = asRecord(asRecord(config)?.historian);
117
123
  const block = resolveHarnessBlock(historian, harness);
118
124
  if (!block) return { fallbacks: [] };
125
+ const source = `historian.${sourceHarness(historian, harness)}`;
126
+ const primary = resolvePrimaryEntry({
127
+ entry: block.model,
128
+ defaultQualifier: block[harness === "opencode" ? "variant" : "thinking_level"],
129
+ harness,
130
+ });
131
+ const fallbacks = resolveFallbackEntries(block.fallback_models, harness);
119
132
 
120
133
  return {
121
- primary: resolvePrimaryEntry({
122
- entry: block.model,
123
- defaultQualifier: block[harness === "opencode" ? "variant" : "thinking_level"],
124
- harness,
125
- }),
126
- fallbacks: resolveFallbackEntries(block.fallback_models, harness),
134
+ primary: primary ? { ...primary, sourceConfigKey: `${source}.model` } : undefined,
135
+ fallbacks: fallbacks.map((entry) => ({
136
+ ...entry,
137
+ sourceConfigKey: `${source}.fallback_models`,
138
+ })),
127
139
  };
128
140
  }
129
141
 
@@ -171,19 +183,34 @@ export function resolveDreamerTaskModel(args: {
171
183
  harnessBlock[args.harness === "opencode" ? "variant" : "thinking_level"] ??
172
184
  normalizeModelEntry(harnessBlock.model, args.harness)?.qualifier;
173
185
  const taskHasModel = taskBlock !== undefined && hasOwn(taskBlock, "model");
186
+ const taskHasFallbacks = taskBlock !== undefined && hasOwn(taskBlock, "fallback_models");
174
187
  const primarySource = taskHasModel ? taskBlock?.model : harnessBlock.model;
175
- const fallbackSource =
176
- taskBlock !== undefined && hasOwn(taskBlock, "fallback_models")
177
- ? taskBlock.fallback_models
178
- : harnessBlock.fallback_models;
188
+ const fallbackSource = taskHasFallbacks
189
+ ? taskBlock?.fallback_models
190
+ : harnessBlock.fallback_models;
191
+ const source = `dreamer.${sourceHarness(dreamer, args.harness)}`;
192
+ const primary = resolvePrimaryEntry({
193
+ entry: primarySource,
194
+ defaultQualifier: taskQualifier ?? harnessQualifier,
195
+ harness: args.harness,
196
+ });
197
+ const fallbacks = resolveFallbackEntries(fallbackSource, args.harness);
179
198
 
180
199
  return {
181
- primary: resolvePrimaryEntry({
182
- entry: primarySource,
183
- defaultQualifier: taskQualifier ?? harnessQualifier,
184
- harness: args.harness,
185
- }),
186
- fallbacks: resolveFallbackEntries(fallbackSource, args.harness),
200
+ primary: primary
201
+ ? {
202
+ ...primary,
203
+ sourceConfigKey: taskHasModel
204
+ ? `${source}.tasks.${args.task}.model`
205
+ : `${source}.model`,
206
+ }
207
+ : undefined,
208
+ fallbacks: fallbacks.map((entry) => ({
209
+ ...entry,
210
+ sourceConfigKey: taskHasFallbacks
211
+ ? `${source}.tasks.${args.task}.fallback_models`
212
+ : `${source}.fallback_models`,
213
+ })),
187
214
  schedule: readString(schedulingTask?.schedule),
188
215
  timeoutMinutes:
189
216
  typeof taskBlock?.timeout_minutes === "number" ? taskBlock.timeout_minutes : undefined,
@@ -357,7 +357,7 @@ async function promptWithTimeout(
357
357
  // Only a transport that watches the child can say the model is silent.
358
358
  if (!activity.monitored || activity.received) return;
359
359
  silent = true;
360
- controller.abort();
360
+ controller.abort(new ModelProducedNoOutputError(firstOutputTimeoutMs));
361
361
  }, firstOutputTimeoutMs)
362
362
  : undefined;
363
363
  const timeoutError = () =>
@@ -7,7 +7,10 @@ import {
7
7
  releaseBudgetFinalizeChild,
8
8
  TOKEN_BUDGET_FINALIZE_MESSAGE,
9
9
  } from "../features/magic-context/dreamer/token-budget";
10
- import { sumTokensFromChildMessages } from "../features/magic-context/subagent-token-capture";
10
+ import {
11
+ annotateSubagentInvocationError,
12
+ sumTokensFromChildMessages,
13
+ } from "../features/magic-context/subagent-token-capture";
11
14
  import { describeAssistantError } from "./assistant-message-extractor";
12
15
  import { sessionLog } from "./logger";
13
16
  import type { PromptArgs, PromptTransport } from "./model-suggestion-retry";
@@ -60,6 +63,7 @@ export interface PromptAsyncWaitOptions {
60
63
  pollIntervalMs?: number;
61
64
  startGraceMs?: number;
62
65
  tokenBudget?: number;
66
+ modelConfigKey?: string;
63
67
  /** Shared across retries of one child session. */
64
68
  budgetGuard?: ReturnType<typeof createDreamTokenBudget>;
65
69
  onBudgetUpdate?: (
@@ -93,6 +97,19 @@ export function isHostSessionError(error: unknown): boolean {
93
97
  );
94
98
  }
95
99
 
100
+ /** Return the original host error carried by a marked child-session failure. */
101
+ export function hostSessionErrorOf(error: unknown): unknown {
102
+ if (!isHostSessionError(error)) return undefined;
103
+ return (error as { hostError?: unknown }).hostError;
104
+ }
105
+
106
+ function requestModelRef(request: PromptArgs): string | undefined {
107
+ const model = request.body?.model;
108
+ if (model && typeof model.providerID === "string" && typeof model.modelID === "string")
109
+ return `${model.providerID}/${model.modelID}`;
110
+ return undefined;
111
+ }
112
+
96
113
  /** True when the host client can send without waiting and report session status. */
97
114
  export function supportsPromptAsync(client: Client): boolean {
98
115
  const session = sessionApi(client);
@@ -339,11 +356,32 @@ export async function promptAsyncAndWaitForIdle(
339
356
  // retains that provider/model error while this child is actively awaited.
340
357
  activeSessionErrors.set(sessionId, undefined);
341
358
  try {
342
- const sent = await session.promptAsync?.(request);
359
+ let sent: unknown;
360
+ try {
361
+ sent = await session.promptAsync?.(request);
362
+ } catch (error) {
363
+ annotateSubagentInvocationError(error, {
364
+ attemptedModel: requestModelRef(request),
365
+ modelConfigKey: options.modelConfigKey,
366
+ hostError: error,
367
+ });
368
+ throw error;
369
+ }
343
370
  if (sent && typeof sent === "object" && "error" in sent) {
344
371
  const rejection = (sent as { error?: unknown }).error;
345
372
  if (rejection) {
346
- throw new Error(`prompt_async was rejected: ${describeRejection(rejection)}`);
373
+ const error = new Error(
374
+ `prompt_async was rejected: ${describeRejection(rejection)}`,
375
+ {
376
+ cause: rejection,
377
+ },
378
+ );
379
+ annotateSubagentInvocationError(error, {
380
+ attemptedModel: requestModelRef(request),
381
+ modelConfigKey: options.modelConfigKey,
382
+ hostError: rejection,
383
+ });
384
+ throw error;
347
385
  }
348
386
  }
349
387
  let sentAt = Date.now();
@@ -364,6 +402,12 @@ export async function promptAsyncAndWaitForIdle(
364
402
  name: "DreamerProviderOutputFailureError",
365
403
  transient: true,
366
404
  [HOST_SESSION_ERROR]: true,
405
+ hostError,
406
+ });
407
+ annotateSubagentInvocationError(error, {
408
+ attemptedModel: requestModelRef(request),
409
+ modelConfigKey: options.modelConfigKey,
410
+ hostError,
367
411
  });
368
412
  throw error;
369
413
  }
@@ -380,6 +380,7 @@ export interface DebugSessionHolderCount {
380
380
  wireMessages: number;
381
381
  wireContentSnapshots: number;
382
382
  wireEstimatedBytes: number;
383
+ generateReplayBytes: number;
383
384
  }
384
385
 
385
386
  export interface DebugMemoryHolders {
@@ -401,6 +402,12 @@ export interface DebugMemoryHolders {
401
402
  compartmentMirrors: {
402
403
  entries: number;
403
404
  };
405
+ /** OpenCode 2 only: kept copies of each session's last served request, which side
406
+ * questions replay. Zero on other hosts. */
407
+ generateReplay: {
408
+ sessionCount: number;
409
+ estimatedBytes: number;
410
+ };
404
411
  messageIndexQueue: {
405
412
  queueLength: number;
406
413
  reconciliationScheduled: number;
@@ -34,6 +34,7 @@ async function lock(path: string, mode: "read" | "write" | "exclusive", holdMs:
34
34
  "--eval",
35
35
  `import { Database } from "bun:sqlite";
36
36
  const db = new Database(process.argv[1]);
37
+ db.exec("PRAGMA busy_timeout = 5000");
37
38
  db.exec(process.argv[2] === "read" ? "BEGIN; SELECT * FROM result"
38
39
  : process.argv[2] === "write" ? "BEGIN IMMEDIATE" : "BEGIN EXCLUSIVE");
39
40
  console.log("LOCKED");
@@ -51,7 +52,11 @@ async function lock(path: string, mode: "read" | "write" | "exclusive", holdMs:
51
52
  while (!output.includes("LOCKED\n")) {
52
53
  const chunk = await reader.read();
53
54
  if (chunk.done) {
54
- throw new Error(`locker failed: ${await new Response(child.stderr).text()}`);
55
+ const [exitCode, stderr] = await Promise.all([
56
+ child.exited,
57
+ new Response(child.stderr).text(),
58
+ ]);
59
+ throw new Error(`locker failed (exit ${exitCode}): ${stderr.trim() || "<no stderr>"}`);
55
60
  }
56
61
  output += new TextDecoder().decode(chunk.value);
57
62
  }
@@ -1283,7 +1283,7 @@ export async function withAsyncPrivilegedWriter<T>(
1283
1283
  operation: () => T,
1284
1284
  options: {
1285
1285
  signal?: AbortSignal;
1286
- beforeRetry?: (error: SqliteAcquisitionBusyError) => void;
1286
+ beforeRetry?: (error: SqliteAcquisitionBusyError) => void | Promise<void>;
1287
1287
  jitter?: boolean;
1288
1288
  budgetMs?: number;
1289
1289
  assertCurrentPass?: () => void;
@@ -1326,7 +1326,9 @@ export async function withAsyncPrivilegedWriter<T>(
1326
1326
  if (entered || !(error instanceof SqliteAcquisitionBusyError)) throw error;
1327
1327
  // Adapters may serve a validated saved request instead of waiting.
1328
1328
  // This runs only after a failed BEGIN, never after callback writes.
1329
- options.beforeRetry?.(error);
1329
+ // It may read asynchronously (OpenCode 2 reads its host store to
1330
+ // rebuild the transform input); the retry waits for it.
1331
+ await options.beforeRetry?.(error);
1330
1332
  const elapsed = performance.now() - started;
1331
1333
  if (elapsed >= budgetMs) {
1332
1334
  reportSqliteDiagnostic(
@@ -103,6 +103,8 @@ export interface SubagentRunOptions {
103
103
 
104
104
  /** Optional token accounting metadata. When present, harness runners persist subagent_invocations. */
105
105
  accountingSessionId?: string | undefined;
106
+ /** Config key supplying the selected model, for host refusal diagnostics. */
107
+ accountingModelConfigKey?: string | undefined;
106
108
  accountingSubagent?:
107
109
  | "historian"
108
110
  | "historian_editor"
@@ -0,0 +1,87 @@
1
+ import { expect, test } from "bun:test";
2
+ import { readdirSync, readFileSync } from "node:fs";
3
+ import { join, relative, resolve } from "node:path";
4
+ import ts from "typescript";
5
+
6
+ // A worker that installs exitOnWorkerClose must close its parent port only
7
+ // through the returned closePort, which removes the close listener first. On
8
+ // Bun 1.3.14 a worker whose port closes with that listener still attached never
9
+ // exits, and the port's own close() cannot be wrapped there (it is read-only),
10
+ // so this fence rejects a direct close in any such file.
11
+
12
+ const root = resolve(import.meta.dir, "../../../..");
13
+ const helper = "packages/plugin/src/shared/worker-shutdown.ts";
14
+
15
+ function files(dir: string): string[] {
16
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
17
+ const path = join(dir, entry.name);
18
+ if (entry.isDirectory()) return files(path);
19
+ return /\.tsx?$/.test(path) && !/\.test\.|\.d\.ts$/.test(path) ? [path] : [];
20
+ });
21
+ }
22
+
23
+ function unwrap(node: ts.Expression): ts.Expression {
24
+ let current = node;
25
+ while (ts.isParenthesizedExpression(current) || ts.isNonNullExpression(current))
26
+ current = current.expression;
27
+ return current;
28
+ }
29
+
30
+ /** Direct closes of the parent port, or of a variable holding it, in a file that installs the close listener. */
31
+ function scan(path: string, text: string): { usesHelper: boolean; violations: string[] } {
32
+ const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true);
33
+ const ports = new Set(["parentPort"]);
34
+ let usesHelper = false;
35
+ const closes: ts.CallExpression[] = [];
36
+ const visit = (node: ts.Node) => {
37
+ if (
38
+ ts.isVariableDeclaration(node) &&
39
+ ts.isIdentifier(node.name) &&
40
+ node.initializer &&
41
+ ts.isIdentifier(unwrap(node.initializer)) &&
42
+ ports.has((unwrap(node.initializer) as ts.Identifier).text)
43
+ )
44
+ ports.add(node.name.text);
45
+ if (ts.isCallExpression(node)) {
46
+ const callee = unwrap(node.expression);
47
+ if (ts.isIdentifier(callee) && callee.text === "exitOnWorkerClose") usesHelper = true;
48
+ if (ts.isPropertyAccessExpression(callee) && callee.name.text === "close")
49
+ closes.push(node);
50
+ }
51
+ ts.forEachChild(node, visit);
52
+ };
53
+ visit(source);
54
+ if (!usesHelper || path === helper) return { usesHelper, violations: [] };
55
+ const violations = closes.flatMap((call) => {
56
+ const target = unwrap((unwrap(call.expression) as ts.PropertyAccessExpression).expression);
57
+ if (!ts.isIdentifier(target) || !ports.has(target.text)) return [];
58
+ const line = source.getLineAndCharacterOfPosition(call.getStart(source)).line + 1;
59
+ return [
60
+ `${path}:${line}: close the port through exitOnWorkerClose's closePort, not ${target.text}.close()`,
61
+ ];
62
+ });
63
+ return { usesHelper, violations };
64
+ }
65
+
66
+ test("workers with the close listener close their port only through closePort", () => {
67
+ const scans = ["packages/plugin/src", "packages/pi-plugin/src", "packages/cli/src"]
68
+ .flatMap((directory) => files(join(root, directory)))
69
+ .map((path) => scan(relative(root, path), readFileSync(path, "utf8")));
70
+ // Pi tag snapshots, raw-ordinal warm-up, auto-search, the chunk writer and the test fixture.
71
+ expect(scans.filter((result) => result.usesHelper).length).toBeGreaterThanOrEqual(5);
72
+ expect(scans.flatMap((result) => result.violations)).toEqual([]);
73
+ });
74
+
75
+ test("the fence rejects direct port closes and accepts closePort", () => {
76
+ const header = 'import { parentPort } from "node:worker_threads";\n';
77
+ const install = "const { closePort } = exitOnWorkerClose(parentPort, () => db.close());\n";
78
+ expect(scan("worker.ts", `${header}${install}parentPort?.close();`).violations).toHaveLength(1);
79
+ expect(
80
+ scan("worker.ts", `${header}const port = parentPort!;\n${install}port.close();`).violations,
81
+ ).toHaveLength(1);
82
+ expect(
83
+ scan("worker.ts", `${header}${install}db.close();\nclosePort();`).violations,
84
+ ).toHaveLength(0);
85
+ // A worker without the close listener may close its port directly.
86
+ expect(scan("worker.ts", `${header}parentPort?.close();`).violations).toHaveLength(0);
87
+ });
@@ -0,0 +1,25 @@
1
+ // Worker for worker-shutdown.test.ts. "cooperative" closes its connection
2
+ // when asked; "deaf" never reads parent messages, so only terminate() stops it.
3
+ // "finish" installs the close listener, finishes its work and closes its own
4
+ // port through the helper's `closePort()`; the thread must then exit by itself.
5
+ import { parentPort, workerData } from "node:worker_threads";
6
+ import { Database } from "./sqlite";
7
+ import { exitOnWorkerClose } from "./worker-shutdown";
8
+
9
+ const { path, mode } = workerData as {
10
+ path: string;
11
+ mode: "cooperative" | "deaf" | "finish";
12
+ };
13
+ const db = new Database(path);
14
+ db.prepare("SELECT 1").get();
15
+ if (mode === "finish") {
16
+ const { closePort } = exitOnWorkerClose(parentPort, () => db.close());
17
+ db.close();
18
+ parentPort?.postMessage("ready");
19
+ closePort();
20
+ } else {
21
+ if (mode === "cooperative") exitOnWorkerClose(parentPort, () => db.close());
22
+ // Keep the thread alive until it is asked to close or is terminated.
23
+ setInterval(() => {}, 1_000);
24
+ parentPort?.postMessage("ready");
25
+ }
@@ -0,0 +1,127 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { afterEach, expect, test } from "bun:test";
3
+ import { EventEmitter } from "node:events";
4
+ import { join } from "node:path";
5
+ import { Worker } from "node:worker_threads";
6
+ import { createTestTempDir } from "./test-temp-dir";
7
+ import {
8
+ createWorkerShutdown,
9
+ exitOnWorkerClose,
10
+ getWorkerShutdownStats,
11
+ type WorkerClosePort,
12
+ } from "./worker-shutdown";
13
+
14
+ const cleanups: (() => void)[] = [];
15
+ afterEach(() => {
16
+ for (const cleanup of cleanups.splice(0)) cleanup();
17
+ });
18
+
19
+ async function start(mode: "cooperative" | "deaf" | "finish") {
20
+ const { dir, cleanup } = createTestTempDir("mc-worker-shutdown-");
21
+ cleanups.push(cleanup);
22
+ const path = join(dir, "store.db");
23
+ const owner = new Database(path);
24
+ owner.exec("PRAGMA journal_mode=WAL");
25
+ cleanups.unshift(() => owner.close());
26
+ const worker = new Worker(new URL("./worker-shutdown.fixture.ts", import.meta.url), {
27
+ workerData: { path, mode },
28
+ });
29
+ const exitCode = new Promise<number>((resolve) => worker.once("exit", resolve));
30
+ await new Promise<void>((resolve) => worker.once("message", () => resolve()));
31
+ return { owner, worker, exitCode };
32
+ }
33
+
34
+ function counts(site: string) {
35
+ return { ...(getWorkerShutdownStats().get(site) ?? { graceful: 0, forced: 0 }) };
36
+ }
37
+
38
+ test("a worker asked to close shuts its connection and exits by itself", async () => {
39
+ const site = "test-cooperative";
40
+ const { owner, worker, exitCode } = await start("cooperative");
41
+ const before = counts(site);
42
+ const shutdown = createWorkerShutdown(worker, site, 10_000);
43
+ const started = performance.now();
44
+ await shutdown.close();
45
+ // Well inside the deadline: the worker exited on its own, it was not terminated.
46
+ expect(performance.now() - started).toBeLessThan(5_000);
47
+ expect(await exitCode).toBe(0);
48
+ expect(counts(site)).toEqual({ graceful: before.graceful + 1, forced: before.forced });
49
+ owner.exec("PRAGMA busy_timeout=0");
50
+ owner.exec("BEGIN IMMEDIATE");
51
+ owner.exec("ROLLBACK");
52
+ });
53
+
54
+ test("a worker that does not exit is terminated at the deadline", async () => {
55
+ const site = "test-deaf";
56
+ const { worker, exitCode } = await start("deaf");
57
+ const before = counts(site);
58
+ const shutdown = createWorkerShutdown(worker, site, 100);
59
+ const started = performance.now();
60
+ await shutdown.close();
61
+ expect(performance.now() - started).toBeGreaterThanOrEqual(90);
62
+ await exitCode;
63
+ expect(counts(site)).toEqual({ graceful: before.graceful, forced: before.forced + 1 });
64
+ });
65
+
66
+ test("closing twice shares one shutdown, and closing an exited worker is a no-op", async () => {
67
+ const site = "test-repeat";
68
+ const { worker, exitCode } = await start("cooperative");
69
+ const before = counts(site);
70
+ const shutdown = createWorkerShutdown(worker, site, 10_000);
71
+ const first = shutdown.close();
72
+ expect(shutdown.close()).toBe(first);
73
+ await first;
74
+ await exitCode;
75
+ expect(counts(site)).toEqual({ graceful: before.graceful + 1, forced: before.forced });
76
+
77
+ const exited = await start("deaf");
78
+ const late = createWorkerShutdown(exited.worker, site, 10_000);
79
+ await exited.worker.terminate();
80
+ await exited.exitCode;
81
+ await late.close();
82
+ expect(counts(site)).toEqual({ graceful: before.graceful + 1, forced: before.forced });
83
+ });
84
+
85
+ /** A port that records how many message listeners were attached when it closed. */
86
+ function recordingPort() {
87
+ const events = new EventEmitter();
88
+ const closedWith: number[] = [];
89
+ const port = {
90
+ on: (event: string, listener: (...args: unknown[]) => void) => {
91
+ events.on(event, listener);
92
+ return port;
93
+ },
94
+ off: (event: string, listener: (...args: unknown[]) => void) => {
95
+ events.off(event, listener);
96
+ return port;
97
+ },
98
+ close: () => {
99
+ closedWith.push(events.listenerCount("message"));
100
+ },
101
+ };
102
+ return { port: port as unknown as WorkerClosePort, events, closedWith };
103
+ }
104
+
105
+ test("closePort removes the close listener before the port closes", async () => {
106
+ const { port, events, closedWith } = recordingPort();
107
+ const { closePort } = exitOnWorkerClose(port, () => {});
108
+ expect(events.listenerCount("message")).toBe(1);
109
+ closePort();
110
+ // Closing again does not close the port twice.
111
+ closePort();
112
+ await new Promise((resolve) => setTimeout(resolve, 10));
113
+ // Bun 1.3.14 never ends a worker whose port closes with a listener attached.
114
+ expect(closedWith).toEqual([0]);
115
+ });
116
+
117
+ test("a worker that closes its own port through closePort exits by itself", async () => {
118
+ const { worker, exitCode } = await start("finish");
119
+ const outcome = await Promise.race([
120
+ exitCode,
121
+ new Promise<string>((resolve) => setTimeout(() => resolve("no exit"), 5_000)),
122
+ ]);
123
+ // Terminate only a worker that is still running: on Bun 1.3.14,
124
+ // terminate() on a worker that has already exited never settles.
125
+ if (outcome === "no exit") await worker.terminate();
126
+ expect(outcome).toBe(0);
127
+ });