@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.
- package/dist/auto-search-worker.js +48 -9
- package/dist/chunk-embedding-write-worker.js +30 -23
- package/dist/{compaction-marker-candidates-n4krhat4.js → compaction-marker-candidates-b2q30cft.js} +3 -3
- package/dist/compaction-marker-candidates-worker.js +2 -2
- package/dist/compartment-chunk-embedding-va090as0.js +65 -0
- package/dist/config/schema/magic-context.d.ts +6 -6
- package/dist/{conflict-warning-hook-4pmz879v.js → conflict-warning-hook-frp3z2wk.js} +9 -8
- package/dist/{context-j31m2dv7.js → context-mkece1db.js} +455 -134
- package/dist/{derive-budgets-zn7yzd2d.js → derive-budgets-pqybnx09.js} +2 -2
- package/dist/embedding-backfill-gy3p78py.js +25 -0
- package/dist/embedding-worker-process.js +2 -2
- package/dist/embedding-worker.js +5 -5
- package/dist/features/magic-context/chunk-embedding-writer.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts +44 -1
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-config.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts +2 -0
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts +1 -0
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/memory/local-embedding-pooling.d.ts +22 -0
- package/dist/features/magic-context/memory/local-embedding-pooling.d.ts.map +1 -0
- package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts +2 -0
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts +27 -0
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +1 -0
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/subagent-token-capture.d.ts +20 -0
- package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
- package/dist/{history-owner-xp3pngv7.js → history-owner-36wp3mkb.js} +2 -2
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-embed-session.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-hint.d.ts +7 -1
- package/dist/hooks/magic-context/auto-search-hint.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-worker-client.d.ts +33 -6
- package/dist/hooks/magic-context/auto-search-worker-client.d.ts.map +1 -1
- package/dist/hooks/magic-context/cache-busting-signals.d.ts +5 -2
- package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
- package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +5 -0
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-trigger.d.ts +1 -1
- package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
- package/dist/hooks/magic-context/diagnostic-log-state.d.ts +4 -0
- package/dist/hooks/magic-context/diagnostic-log-state.d.ts.map +1 -0
- package/dist/hooks/magic-context/execute-reclaim.d.ts +35 -0
- package/dist/hooks/magic-context/execute-reclaim.d.ts.map +1 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +1 -3
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/latest-assistant-turn.d.ts +8 -0
- package/dist/hooks/magic-context/latest-assistant-turn.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-recovery-fence.d.ts +5 -0
- package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -1
- package/dist/hooks/magic-context/nudge-evaluation-log.d.ts +3 -0
- package/dist/hooks/magic-context/nudge-evaluation-log.d.ts.map +1 -0
- package/dist/hooks/magic-context/pending-drop-eligibility.d.ts.map +1 -1
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts +4 -0
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
- package/dist/hooks/magic-context/raw-ordinal-warmup.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-date-refresh.d.ts +5 -5
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-sticky-date.d.ts +10 -0
- package/dist/hooks/magic-context/system-prompt-sticky-date.d.ts.map +1 -0
- package/dist/hooks/magic-context/tag-messages.d.ts +2 -0
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +21 -6
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-bjcnntmz.js → index-0wthn7fp.js} +468 -18
- package/dist/{index-kq82mtg0.js → index-11zf2wwc.js} +3 -3
- package/dist/{index-b5vqckbw.js → index-153r2q6z.js} +314 -127
- package/dist/{index-3h4dp9wb.js → index-1pefp8ar.js} +5 -5
- package/dist/{index-beht37jy.js → index-1zx2pa5d.js} +1 -1
- package/dist/{index-ze7mvk54.js → index-2yhtnfcf.js} +75 -57
- package/dist/{index-cf74k5ak.js → index-305y133n.js} +4 -4
- package/dist/{index-s734jcsr.js → index-35qc6606.js} +9 -3
- package/dist/{index-se4m2g6w.js → index-48stjd56.js} +105 -19
- package/dist/{index-4911rn0b.js → index-4n7w9rad.js} +6 -6
- package/dist/{index-kee4kn5j.js → index-4pnsgvfk.js} +1 -1
- package/dist/{v2/server-8cd2j99z.js → index-5kym00ag.js} +32 -13
- package/dist/{index-4s3t0nd0.js → index-616rsjrv.js} +2 -2
- package/dist/{index-jhghdnsm.js → index-67nyj0gy.js} +485 -202
- package/dist/{index-6x57ar45.js → index-adk2z5d8.js} +3 -3
- package/dist/{index-c5pytzne.js → index-b88dwwrh.js} +1 -1
- package/dist/{index-rr2bb45z.js → index-c4b76d4t.js} +2 -2
- package/dist/{index-nj7yykkx.js → index-c7phnbvk.js} +1 -1
- package/dist/index-cpjk5jd2.js +82 -0
- package/dist/{index-edmpdxcw.js → index-d6rp4dxh.js} +5 -5
- package/dist/{index-dxmq29t1.js → index-dc8hy95c.js} +69 -17
- package/dist/{index-j75v2476.js → index-h7tgn49j.js} +12 -15
- package/dist/{index-ke416mdc.js → index-j76ah6d7.js} +2 -2
- package/dist/{index-c1p88z1y.js → index-jfh3cktr.js} +4 -4
- package/dist/{index-a6mc0v4d.js → index-jmycsey7.js} +3 -3
- package/dist/{index-24fezq97.js → index-jr5na10t.js} +4 -4
- package/dist/{index-s74zqahq.js → index-kc9pn495.js} +4 -4
- package/dist/{index-p5y3k4g7.js → index-nda0zgna.js} +2 -1
- package/dist/{index-jn12fvvh.js → index-neb2jaed.js} +3 -3
- package/dist/{index-63k48rv9.js → index-pnjwa6qa.js} +5 -5
- package/dist/{index-jrr131e0.js → index-rmd0069m.js} +11 -11
- package/dist/{index-y9f76s3f.js → index-rrv26qjz.js} +1 -1
- package/dist/{index-v10f1rjv.js → index-spwjbkc7.js} +1 -1
- package/dist/{index-k29z4aqh.js → index-t5pck3ss.js} +1 -1
- package/dist/{index-3esx8tkf.js → index-t7hx3qcb.js} +318 -104
- package/dist/{index-rpxdjkm0.js → index-v7r4td4c.js} +306 -59
- package/dist/{index-2vxtcb95.js → index-vkc1ca9p.js} +2 -2
- package/dist/{index-9zcsy3hh.js → index-x4k3f4mf.js} +17 -9
- package/dist/{index-bp5083x2.js → index-x9k3mqrf.js} +1 -1
- package/dist/{index-n37sebck.js → index-y4a3nhtk.js} +1 -1
- package/dist/index.js +52 -49
- package/dist/{logger-jm3sqqzr.js → logger-94kew5wm.js} +1 -1
- package/dist/{message-fts-rowid-map-17s0dfn4.js → message-fts-rowid-map-22tevnnj.js} +6 -5
- package/dist/{message-index-gtpqnj8w.js → message-index-5qxcz6h7.js} +16 -15
- package/dist/{message-index-bounded-zyc8h32y.js → message-index-bounded-n0m0t7tv.js} +16 -15
- package/dist/{message-time-backfill-73mmm76q.js → message-time-backfill-0myx7fz3.js} +3 -3
- package/dist/migration-worker.js +13 -12
- package/dist/{model-resolution-nx97z3x7.js → model-resolution-280gmqvd.js} +1 -1
- package/dist/plugin/embedding-bootstrap.d.ts +3 -1
- package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +8 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/subagent-tool-policy.d.ts +9 -0
- package/dist/plugin/subagent-tool-policy.d.ts.map +1 -1
- package/dist/{project-embedding-registry-z74mhqkf.js → project-embedding-registry-s2pqgec9.js} +13 -12
- package/dist/raw-ordinal-warmup-worker.js +136 -101
- package/dist/{read-session-chunk-jjb48bt9.js → read-session-chunk-0a8vdyqp.js} +11 -10
- package/dist/{read-session-raw-rsc8c7qy.js → read-session-raw-y5cszf0b.js} +4 -3
- package/dist/recomp-orchestrator-61vyha9j.js +62 -0
- package/dist/{safe-notification-target-1szv1k5k.js → safe-notification-target-mkr9yzvr.js} +2 -2
- package/dist/{search-archive-6vm0e2f5.js → search-archive-8q9s49p5.js} +12 -11
- package/dist/{search-xsqk1bj4.js → search-v1y9ejws.js} +25 -24
- package/dist/{send-session-notification-6ve0e8p4.js → send-session-notification-0k6qzydk.js} +7 -6
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/model-resolution.d.ts +2 -0
- package/dist/shared/model-resolution.d.ts.map +1 -1
- package/dist/shared/prompt-async-transport.d.ts +3 -0
- package/dist/shared/prompt-async-transport.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +7 -0
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/sqlite.d.ts +1 -1
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/subagent-runner.d.ts +2 -0
- package/dist/shared/subagent-runner.d.ts.map +1 -1
- package/dist/shared/worker-shutdown.d.ts +68 -0
- package/dist/shared/worker-shutdown.d.ts.map +1 -0
- package/dist/shared/worker-shutdown.fixture.d.ts +2 -0
- package/dist/shared/worker-shutdown.fixture.d.ts.map +1 -0
- package/dist/{sqlite-zf72c52d.js → sqlite-1rpvw2rj.js} +1 -1
- package/dist/{storage-memory-y3qmy00r.js → storage-memory-nm7v82mj.js} +2 -2
- package/dist/{storage-tags-r7k9tjcv.js → storage-tags-r6nk29n4.js} +5 -5
- package/dist/{task-config-x3qmayd2.js → task-config-t6bpyka7.js} +2 -2
- package/dist/tools/ctx-search/tools.d.ts.map +1 -1
- package/dist/{transform-postprocess-phase-jj99r7z2.js → transform-postprocess-phase-vtrgb7m6.js} +34 -31
- package/dist/v2/auto-search-worker.js +48 -9
- package/dist/v2/chunk-embedding-write-worker.js +30 -23
- package/dist/v2/{compaction-marker-candidates-ebx3388k.js → compaction-marker-candidates-cfab70cs.js} +3 -3
- package/dist/v2/compaction-marker-candidates-worker.js +2 -2
- package/dist/v2/compartment-chunk-embedding-nbdqx1td.js +64 -0
- package/dist/v2/{context-y5yamxak.js → context-cc33tv2z.js} +1189 -386
- package/dist/v2/{derive-budgets-2zh4gm2c.js → derive-budgets-knf763tt.js} +3 -3
- package/dist/v2/embedding-backfill-yxv2h5mc.js +24 -0
- package/dist/v2/embedding-worker-process.js +2 -2
- package/dist/v2/embedding-worker.js +5 -5
- package/dist/v2/hidden-child-record.d.ts +2 -0
- package/dist/v2/hidden-child-record.d.ts.map +1 -1
- package/dist/v2/hidden-completion.d.ts +2 -0
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hidden-model-activity.d.ts +7 -0
- package/dist/v2/hidden-model-activity.d.ts.map +1 -0
- package/dist/v2/{history-owner-42e16w0n.js → history-owner-ad0crnrn.js} +2 -2
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/generate.d.ts +36 -1
- package/dist/v2/hooks/generate.d.ts.map +1 -1
- package/dist/v2/hooks/lkg-system.d.ts +35 -4
- package/dist/v2/hooks/lkg-system.d.ts.map +1 -1
- package/dist/v2/hooks/payload.d.ts +16 -1
- package/dist/v2/hooks/payload.d.ts.map +1 -1
- package/dist/v2/{logger-7qt02bnb.js → logger-p34mjtja.js} +1 -1
- package/dist/v2/{message-fts-rowid-map-wfx67gfd.js → message-fts-rowid-map-dg12wymk.js} +6 -5
- package/dist/v2/{message-index-bounded-spd8r7pj.js → message-index-bounded-f4e01xk0.js} +16 -15
- package/dist/v2/{message-index-8vz2pq76.js → message-index-ydzrp6y6.js} +16 -15
- package/dist/v2/{message-time-backfill-04t0km1z.js → message-time-backfill-ddh8n9gy.js} +3 -3
- package/dist/v2/migration-worker.js +13 -12
- package/dist/v2/{model-resolution-nx97z3x7.js → model-resolution-280gmqvd.js} +1 -1
- package/dist/v2/{project-embedding-registry-gnkj56xp.js → project-embedding-registry-deza9ksw.js} +13 -12
- package/dist/v2/raw-ordinal-warmup-worker.js +136 -101
- package/dist/v2/{read-session-chunk-cwkxrmns.js → read-session-chunk-8cxfagky.js} +11 -10
- package/dist/v2/{read-session-raw-yaegtjzh.js → read-session-raw-1nrrecvx.js} +4 -3
- package/dist/v2/recomp-orchestrator-47m4hv5g.js +61 -0
- package/dist/v2/{safe-notification-target-qg7n2s77.js → safe-notification-target-khnzg57c.js} +2 -2
- package/dist/v2/{search-archive-wve3tq1k.js → search-archive-e80cvc09.js} +12 -11
- package/dist/v2/{search-hwg7jfbj.js → search-fjz746zr.js} +25 -24
- package/dist/v2/{send-session-notification-3pxn658x.js → send-session-notification-nwvrv34d.js} +7 -6
- package/dist/v2/{server-8by8qfec.js → server-0epqaxhd.js} +75 -57
- package/dist/v2/{server-pwv4ezb0.js → server-168pb207.js} +1 -1
- package/dist/v2/{server-h0410h0v.js → server-1bw1wcbp.js} +1 -1
- package/dist/v2/{server-beht37jy.js → server-1zx2pa5d.js} +1 -1
- package/dist/v2/{server-a9xqj88f.js → server-230fnbws.js} +4 -4
- package/dist/v2/{server-tbe1femw.js → server-29qp8nz4.js} +1 -1
- package/dist/v2/{server-tgnbmwah.js → server-3mz2qh5a.js} +1 -1
- package/dist/v2/{server-hy3d3vsx.js → server-4nw4fqhr.js} +2 -2
- package/dist/v2/{server-45q69hkq.js → server-4sxjs6hn.js} +5 -5
- package/dist/v2/{server-34kwfsr4.js → server-53eh1r8r.js} +2 -2
- package/dist/v2/{server-xj136kt8.js → server-55rgxfrw.js} +9 -3
- package/dist/v2/{server-nj659894.js → server-5arr9ecn.js} +2 -2
- package/dist/{index-8cd2j99z.js → v2/server-5kym00ag.js} +32 -13
- package/dist/v2/{server-2ar62dyx.js → server-63fqdghs.js} +106 -20
- package/dist/v2/{server-f2xf5gcb.js → server-6495nmn9.js} +12 -15
- package/dist/v2/{server-qd11g795.js → server-92g7d776.js} +2 -1
- package/dist/v2/{server-xq7dfjke.js → server-9x0kmta4.js} +5 -5
- package/dist/v2/{server-27gq4mfz.js → server-a3dwxfgd.js} +4 -4
- package/dist/v2/{server-6tmj0s7j.js → server-af2pght7.js} +3 -3
- package/dist/v2/{server-1wy7b9de.js → server-br1x0tev.js} +3 -3
- package/dist/v2/{server-n1prf1g6.js → server-brj8j2gk.js} +2 -2
- package/dist/v2/{server-6vdzn1td.js → server-c7dxxsj7.js} +308 -61
- package/dist/v2/server-cpjk5jd2.js +82 -0
- package/dist/v2/{server-g3qma5c0.js → server-dj7tz4ph.js} +2 -2
- package/dist/v2/{server-9hx7g5jp.js → server-ej96a39v.js} +17 -9
- package/dist/v2/{server-6pmqg9m4.js → server-gyy4k6mf.js} +1 -1
- package/dist/v2/{server-bmymkx73.js → server-hqzyq8n4.js} +11 -11
- package/dist/v2/{server-h1v0pyxb.js → server-j4jnp9b5.js} +468 -18
- package/dist/v2/{server-da07v4xp.js → server-k3t06fxr.js} +1 -1
- package/dist/v2/{server-gsxjv2q2.js → server-m91f9m5s.js} +69 -17
- package/dist/v2/{server-rmygf5pn.js → server-ne8arh8t.js} +2 -2
- package/dist/v2/{server-hm38q7r0.js → server-vn2nwcv6.js} +5 -5
- package/dist/v2/{server-46neh5fa.js → server-wyawsa88.js} +314 -127
- package/dist/v2/{server-zzkj2fch.js → server-xsryvyf6.js} +4 -4
- package/dist/v2/{server-n37sebck.js → server-y4a3nhtk.js} +1 -1
- package/dist/v2/{server-d04vrx0j.js → server-ygtykank.js} +2 -2
- package/dist/v2/server.js +6 -6
- package/dist/v2/{sqlite-zf72c52d.js → sqlite-1rpvw2rj.js} +1 -1
- package/dist/v2/{storage-memory-y3qmy00r.js → storage-memory-nm7v82mj.js} +2 -2
- package/dist/v2/{storage-tags-3wdb8zph.js → storage-tags-ss08kj5r.js} +5 -5
- package/dist/v2/store-reader.d.ts +8 -0
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/v2/{task-config-nvz8mapa.js → task-config-7md27rx8.js} +2 -2
- package/dist/v2/{wrapup-orchestrator-embdz8gj.js → wrapup-orchestrator-639whfhq.js} +32 -31
- package/dist/wrapup-orchestrator-mnvt2g8m.js +49 -0
- package/package.json +1 -1
- package/src/config/index.test.ts +28 -6
- package/src/shared/logger.test.ts +42 -1
- package/src/shared/logger.ts +11 -13
- package/src/shared/model-resolution.test.ts +67 -12
- package/src/shared/model-resolution.ts +43 -16
- package/src/shared/model-suggestion-retry.ts +1 -1
- package/src/shared/prompt-async-transport.ts +47 -3
- package/src/shared/rpc-types.ts +7 -0
- package/src/shared/sqlite-busy-budget-review.test.ts +6 -1
- package/src/shared/sqlite.ts +4 -2
- package/src/shared/subagent-runner.ts +2 -0
- package/src/shared/worker-shutdown-fence.test.ts +87 -0
- package/src/shared/worker-shutdown.fixture.ts +25 -0
- package/src/shared/worker-shutdown.test.ts +127 -0
- package/src/shared/worker-shutdown.ts +160 -0
- package/dist/embedding-backfill-fmz50tsy.js +0 -24
- package/dist/recomp-orchestrator-999jj4bc.js +0 -61
- package/dist/v2/embedding-backfill-7c7yr69y.js +0 -23
- package/dist/v2/recomp-orchestrator-f6m9jr7p.js +0 -60
- 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: {
|
|
122
|
-
|
|
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: {
|
|
126
|
-
|
|
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: {
|
|
147
|
-
|
|
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: {
|
|
151
|
-
|
|
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: {
|
|
186
|
-
|
|
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: {
|
|
249
|
-
|
|
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:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/shared/rpc-types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/shared/sqlite.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
});
|