@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.
- package/dist/auto-search-worker.js +76 -63
- package/dist/chunk-embedding-write-worker.js +12 -10
- package/dist/{compaction-marker-candidates-v5c6997n.js → compaction-marker-candidates-n4krhat4.js} +2 -2
- package/dist/compaction-marker-candidates-worker.js +2 -2
- package/dist/config/schema/magic-context.d.ts +343 -343
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-mgq17yr3.js → conflict-warning-hook-4pmz879v.js} +6 -6
- package/dist/{context-0m46day0.js → context-yhetp0kn.js} +297 -141
- package/dist/embedding-backfill-fmz50tsy.js +24 -0
- package/dist/embedding-worker-process.js +1 -1
- package/dist/embedding-worker.js +5 -5
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts +1 -1
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/indexer.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity-cache.d.ts +2 -0
- package/dist/features/magic-context/memory/project-identity-cache.d.ts.map +1 -1
- package/dist/features/magic-context/message-fts-rowid-map.d.ts +2 -2
- package/dist/features/magic-context/message-fts-rowid-map.d.ts.map +1 -1
- package/dist/features/magic-context/message-fts-session-filter.d.ts +2 -0
- package/dist/features/magic-context/message-fts-session-filter.d.ts.map +1 -1
- package/dist/features/magic-context/message-index-async.d.ts +1 -1
- package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts +5 -0
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/message-time-backfill.d.ts +2 -2
- package/dist/features/magic-context/message-time-backfill.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts +6 -1
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +2 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-shared.d.ts +2 -2
- package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts +9 -1
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-deadline.d.ts +2 -1
- package/dist/hooks/magic-context/auto-search-deadline.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-hint.d.ts +3 -0
- 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 +10 -2
- package/dist/hooks/magic-context/auto-search-worker-client.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts +4 -2
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
- package/dist/hooks/magic-context/historian-host-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/historian-prompt-fit.d.ts +2 -1
- package/dist/hooks/magic-context/historian-prompt-fit.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -1
- package/dist/hooks/magic-context/raw-ordinal-warmup.d.ts +26 -0
- package/dist/hooks/magic-context/raw-ordinal-warmup.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts +3 -3
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-diagnostics.d.ts +8 -0
- package/dist/hooks/magic-context/system-prompt-diagnostics.d.ts.map +1 -0
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +6 -1
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/todo-view.d.ts +2 -2
- package/dist/hooks/magic-context/tool-drop-target.d.ts +3 -2
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-stage-logger.d.ts +3 -0
- package/dist/hooks/magic-context/transform-stage-logger.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +1 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-3kdm78zp.js → index-24fezq97.js} +4 -4
- package/dist/{index-a8bvhwqs.js → index-2vxtcb95.js} +32 -3
- package/dist/{index-f71thw8j.js → index-4s3t0nd0.js} +1 -1
- package/dist/{index-ts1hjk1h.js → index-4yt7dtf4.js} +453 -227
- package/dist/{index-mc1cqza6.js → index-5a1ex6x8.js} +5 -5
- package/dist/{index-fkx2qnvw.js → index-6x57ar45.js} +2 -2
- package/dist/{index-dyvbsvga.js → index-a6mc0v4d.js} +25 -11
- package/dist/{index-snp5wbge.js → index-a7c4r5re.js} +160 -749
- package/dist/{index-42vcjcwe.js → index-aabzay3h.js} +180 -103
- package/dist/{index-3kx0kg0f.js → index-b5vqckbw.js} +14 -8
- package/dist/{v2/server-wafzvyhg.js → index-beht37jy.js} +8 -0
- package/dist/{index-e20m7284.js → index-bjcnntmz.js} +3 -3
- package/dist/{index-hqkczed7.js → index-c5pytzne.js} +22 -2
- package/dist/{index-thvnpfsv.js → index-cwzfcbtq.js} +967 -88
- package/dist/{index-yxycg7fs.js → index-fb2qxs2y.js} +26 -18
- package/dist/{index-mht7bq0z.js → index-hqhm3wke.js} +32 -8
- package/dist/{index-e4dsjj1p.js → index-jn12fvvh.js} +1 -1
- package/dist/index-kaqs6h31.js +58 -0
- package/dist/{index-n6m03s3n.js → index-ke416mdc.js} +1 -1
- package/dist/index-kmdj7dsq.js +96 -0
- package/dist/{index-1d289pq2.js → index-kq82mtg0.js} +2 -2
- package/dist/{index-n78avt2p.js → index-n37sebck.js} +1 -1
- package/dist/{index-1vc7jrkg.js → index-nkpmm9q8.js} +3 -3
- package/dist/{index-944w16m0.js → index-p7m7j0cp.js} +25 -17
- package/dist/{index-wk0n1cgt.js → index-qn7xstt2.js} +2 -2
- package/dist/{index-saews2s3.js → index-rr2bb45z.js} +1 -1
- package/dist/{index-mmfa86mt.js → index-s734jcsr.js} +169 -149
- package/dist/{index-dgs16axy.js → index-s74zqahq.js} +1 -1
- package/dist/{index-bxdg2nbt.js → index-v8awdgvd.js} +4 -4
- package/dist/{index-xn6q9pg6.js → index-vrzjhftj.js} +36 -76
- package/dist/{index-1sarmj9z.js → index-wgypt3cs.js} +6 -6
- package/dist/{index-y1qysm3q.js → index-ze7mvk54.js} +11 -8
- package/dist/index.js +47 -45
- package/dist/message-fts-rowid-map-17s0dfn4.js +25 -0
- package/dist/{message-index-bounded-vtgpq6b4.js → message-index-bounded-1633vc7f.js} +17 -17
- package/dist/{message-index-a6gztaqx.js → message-index-dprebmtt.js} +15 -15
- package/dist/{message-time-backfill-qjge9044.js → message-time-backfill-73mmm76q.js} +5 -5
- package/dist/migration-worker.js +9 -7
- package/dist/plugin/dream-timer.d.ts +11 -0
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/{project-embedding-registry-x45bv3qr.js → project-embedding-registry-z74mhqkf.js} +12 -10
- package/dist/raw-ordinal-warmup-worker.js +40 -10
- package/dist/{read-session-chunk-3ae3nfdn.js → read-session-chunk-4zp09ejb.js} +9 -9
- package/dist/{read-session-raw-m56hc7v2.js → read-session-raw-rsc8c7qy.js} +2 -2
- package/dist/{recomp-orchestrator-nwzcfhx4.js → recomp-orchestrator-z3ecszx6.js} +26 -26
- package/dist/{search-deajrd90.js → search-6mee3vty.js} +23 -22
- package/dist/search-archive-znyy44ve.js +25 -0
- package/dist/{send-session-notification-24r4esbh.js → send-session-notification-6ve0e8p4.js} +5 -5
- package/dist/shared/historian-cache.d.ts +8 -0
- package/dist/shared/historian-cache.d.ts.map +1 -0
- package/dist/shared/model-suggestion-retry.d.ts +4 -0
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/resolve-fallbacks.d.ts +3 -3
- package/dist/shared/rpc-discovery-cleanup.d.ts +3 -0
- package/dist/shared/rpc-discovery-cleanup.d.ts.map +1 -0
- package/dist/shared/rpc-server.d.ts +2 -0
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/search-text.d.ts +8 -0
- package/dist/shared/search-text.d.ts.map +1 -0
- package/dist/shared/sqlite.d.ts +4 -2
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/storage-file-mutex.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/system-directive.d.ts +2 -0
- package/dist/shared/system-directive.d.ts.map +1 -1
- package/dist/{sqlite-4pqyynrs.js → sqlite-zf72c52d.js} +1 -1
- package/dist/{storage-memory-j3v3hc83.js → storage-memory-y3qmy00r.js} +2 -2
- package/dist/{storage-tags-s5bg1ahx.js → storage-tags-8qns3012.js} +8 -4
- package/dist/tools/ctx-expand/search-archive.d.ts +5 -1
- package/dist/tools/ctx-expand/search-archive.d.ts.map +1 -1
- package/dist/{transform-postprocess-phase-ne3z1zmt.js → transform-postprocess-phase-3ydzxa62.js} +26 -26
- package/dist/v2/auto-search-worker.js +76 -63
- package/dist/v2/chunk-embedding-write-worker.js +12 -10
- package/dist/v2/{compaction-marker-candidates-1vqwr3s5.js → compaction-marker-candidates-ebx3388k.js} +2 -2
- package/dist/v2/compaction-marker-candidates-worker.js +2 -2
- package/dist/v2/{context-4qczzck7.js → context-jn3zpeqh.js} +909 -454
- package/dist/v2/embedding-backfill-7c7yr69y.js +23 -0
- package/dist/v2/embedding-worker-process.js +1 -1
- package/dist/v2/embedding-worker.js +5 -5
- package/dist/v2/fold/native-replay.d.ts +11 -2
- package/dist/v2/fold/native-replay.d.ts.map +1 -1
- package/dist/v2/fold/policy.d.ts.map +1 -1
- package/dist/v2/fold/restore.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +8 -0
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
- package/dist/v2/hooks/types.d.ts +17 -0
- package/dist/v2/hooks/types.d.ts.map +1 -1
- package/dist/v2/message-fts-rowid-map-wfx67gfd.js +24 -0
- package/dist/v2/{message-index-bounded-rxxznd34.js → message-index-bounded-hpejqyd8.js} +17 -17
- package/dist/v2/{message-index-f947qtbz.js → message-index-pxehjpzx.js} +15 -15
- package/dist/v2/{message-time-backfill-bsteyc4d.js → message-time-backfill-04t0km1z.js} +5 -5
- package/dist/v2/migration-worker.js +9 -7
- package/dist/v2/{project-embedding-registry-0msp56p8.js → project-embedding-registry-gnkj56xp.js} +12 -10
- package/dist/v2/raw-ordinal-warmup-worker.js +40 -10
- package/dist/v2/{read-session-chunk-rq9nx1nn.js → read-session-chunk-zdppn4bj.js} +9 -9
- package/dist/v2/{read-session-raw-b3tf6ra1.js → read-session-raw-yaegtjzh.js} +2 -2
- package/dist/v2/{recomp-orchestrator-5zhxtwha.js → recomp-orchestrator-cmqnfpp3.js} +26 -26
- package/dist/v2/{search-n8wqxb1j.js → search-66842dya.js} +23 -22
- package/dist/v2/search-archive-gfh11vs0.js +24 -0
- package/dist/v2/{send-session-notification-8ghn9tnv.js → send-session-notification-3pxn658x.js} +5 -5
- package/dist/v2/{server-yrvqaswb.js → server-1wy7b9de.js} +25 -11
- package/dist/v2/{server-th4y11y7.js → server-271qxsf5.js} +6 -6
- package/dist/v2/{server-e298hswg.js → server-3rhpvy84.js} +967 -88
- package/dist/v2/{server-qhfvvfxj.js → server-46neh5fa.js} +14 -8
- package/dist/v2/{server-rrqpvwqd.js → server-6tmj0s7j.js} +2 -2
- package/dist/v2/{server-hek6z3w6.js → server-8by8qfec.js} +11 -8
- package/dist/v2/{server-vbe4se57.js → server-a9xqj88f.js} +1 -1
- package/dist/{index-wafzvyhg.js → v2/server-beht37jy.js} +8 -0
- package/dist/v2/{server-05xjtq6c.js → server-d04vrx0j.js} +1 -1
- package/dist/v2/server-ec4ca92p.js +96 -0
- package/dist/v2/{server-dd0tz6cn.js → server-fw7wrk01.js} +160 -750
- package/dist/v2/{server-zs8d0s3b.js → server-g3qma5c0.js} +1 -1
- package/dist/v2/{server-k2bzedxk.js → server-gpvay0xz.js} +2 -2
- package/dist/v2/{server-kmbarfmn.js → server-gtfhttbr.js} +26 -18
- package/dist/v2/{server-g10mdym6.js → server-h1v0pyxb.js} +3 -3
- package/dist/v2/{server-4gn3z5an.js → server-hy3d3vsx.js} +1 -1
- package/dist/v2/server-kaqs6h31.js +58 -0
- package/dist/v2/{server-ehyava47.js → server-kh7dpa1a.js} +25 -17
- package/dist/v2/{server-6cdy4rc0.js → server-my7k4eds.js} +4 -4
- package/dist/v2/{server-d8tj10db.js → server-n1prf1g6.js} +1 -1
- package/dist/v2/{server-n78avt2p.js → server-n37sebck.js} +1 -1
- package/dist/v2/{server-zc3e54vy.js → server-nj659894.js} +6 -6
- package/dist/v2/{server-gvkm8xaj.js → server-rmygf5pn.js} +32 -3
- package/dist/v2/{server-0ppsweh6.js → server-tgnbmwah.js} +22 -2
- package/dist/v2/{server-cfbr1s2x.js → server-vsw8tmm6.js} +36 -76
- package/dist/v2/{server-prg55b7k.js → server-xj136kt8.js} +23 -3
- package/dist/v2/{server-atbc19ab.js → server-zyzawhdw.js} +32 -8
- package/dist/v2/{server-ef89gdsx.js → server-zzkj2fch.js} +4 -4
- package/dist/v2/server.js +5 -5
- package/dist/v2/{sqlite-4pqyynrs.js → sqlite-zf72c52d.js} +1 -1
- package/dist/v2/{storage-memory-j3v3hc83.js → storage-memory-y3qmy00r.js} +2 -2
- package/dist/v2/{storage-tags-hcc2adpf.js → storage-tags-1h8k6ra9.js} +8 -4
- package/dist/v2/store-reader.d.ts +2 -0
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/v2/{wrapup-orchestrator-65pwye0d.js → wrapup-orchestrator-xs8redz9.js} +26 -26
- package/dist/wrapup-orchestrator-j7d7ry5b.js +48 -0
- package/package.json +1 -1
- package/src/config/latch-permanence-guard.test.ts +4 -0
- package/src/config/schema/magic-context.ts +14 -6
- package/src/shared/historian-cache.test.ts +69 -0
- package/src/shared/historian-cache.ts +55 -0
- package/src/shared/model-resolution.test.ts +22 -0
- package/src/shared/model-suggestion-retry.ts +11 -3
- package/src/shared/resolve-fallbacks.test.ts +2 -3
- package/src/shared/resolve-fallbacks.ts +3 -3
- package/src/shared/rpc-discovery-cleanup.test.ts +57 -0
- package/src/shared/rpc-discovery-cleanup.ts +57 -0
- package/src/shared/rpc-server.ts +36 -13
- package/src/shared/search-text.test.ts +47 -0
- package/src/shared/search-text.ts +63 -0
- package/src/shared/sqlite.test.ts +56 -0
- package/src/shared/sqlite.ts +51 -2
- package/src/shared/storage-file-mutex-review.test.ts +22 -2
- package/src/shared/storage-file-mutex.ts +11 -1
- package/src/shared/subagent-runner.ts +2 -0
- package/src/shared/system-directive.ts +19 -1
- package/src/shared/tag-transcript.ts +12 -4
- package/dist/embedding-backfill-5e1t5wha.js +0 -22
- package/dist/v2/embedding-backfill-jmaf8zrr.js +0 -21
- 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
|
@@ -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(
|
|
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.
|
|
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(
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
522
|
-
//
|
|
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
|
|
8
|
-
//
|
|
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
|
|
13
|
-
*
|
|
14
|
-
* the
|
|
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
|
+
}
|
package/src/shared/rpc-server.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
});
|