@cortexkit/opencode-magic-context 0.49.0 → 0.49.1

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 (195) hide show
  1. package/dist/{announcement-fp14dw0n.js → announcement-hdbkt8v5.js} +1 -1
  2. package/dist/auto-search-worker.js +3 -3
  3. package/dist/config/{mural-model-deprecation.d.ts → deprecated-config.d.ts} +4 -1
  4. package/dist/config/deprecated-config.d.ts.map +1 -0
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/project-security.d.ts +2 -2
  7. package/dist/config/schema/magic-context.d.ts +4 -4
  8. package/dist/config/schema/magic-context.d.ts.map +1 -1
  9. package/dist/{conflict-warning-hook-kzx4dmk1.js → conflict-warning-hook-asnt6hfc.js} +3 -3
  10. package/dist/{context-nj5f293n.js → context-tfyhtdcs.js} +111 -45
  11. package/dist/{derive-budgets-qz5rrr08.js → derive-budgets-j4ry8j8n.js} +1 -1
  12. package/dist/{embedding-backfill-aetxv1vt.js → embedding-backfill-wyc3ykyf.js} +7 -7
  13. package/dist/embedding-worker-process.js +30 -0
  14. package/dist/embedding-worker.js +5 -5
  15. package/dist/features/magic-context/dreamer/task-registry.d.ts +2 -3
  16. package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
  17. package/dist/features/magic-context/memory/embedding-worker-client.d.ts +8 -1
  18. package/dist/features/magic-context/memory/embedding-worker-client.d.ts.map +1 -1
  19. package/dist/features/magic-context/memory/embedding-worker-process.d.ts +2 -0
  20. package/dist/features/magic-context/memory/embedding-worker-process.d.ts.map +1 -0
  21. package/dist/features/magic-context/memory/onnx-telemetry.d.ts +3 -0
  22. package/dist/features/magic-context/memory/onnx-telemetry.d.ts.map +1 -0
  23. package/dist/features/magic-context/overflow-detection.d.ts +1 -2
  24. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  25. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  26. package/dist/features/magic-context/transform-decision-log.d.ts +1 -1
  27. package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
  28. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts +1 -0
  29. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
  30. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  31. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +1 -0
  32. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  33. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  34. package/dist/hooks/magic-context/hook.d.ts +1 -0
  35. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  37. package/dist/hooks/magic-context/lkg-measured-request.d.ts +3 -1
  38. package/dist/hooks/magic-context/lkg-measured-request.d.ts.map +1 -1
  39. package/dist/hooks/magic-context/lkg-persist.d.ts +8 -14
  40. package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -1
  41. package/dist/hooks/magic-context/lkg-recovery-fence.d.ts +62 -0
  42. package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -0
  43. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts +38 -0
  44. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts.map +1 -0
  45. package/dist/hooks/magic-context/lkg-replay.d.ts +3 -1
  46. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  47. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
  48. package/dist/hooks/magic-context/opaque-thinking.d.ts +12 -0
  49. package/dist/hooks/magic-context/opaque-thinking.d.ts.map +1 -0
  50. package/dist/hooks/magic-context/pass-outcome.d.ts +2 -2
  51. package/dist/hooks/magic-context/pending-materialization.d.ts +8 -0
  52. package/dist/hooks/magic-context/pending-materialization.d.ts.map +1 -0
  53. package/dist/hooks/magic-context/read-session-db.d.ts +3 -0
  54. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  55. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
  56. package/dist/hooks/magic-context/rust-mode-transform.d.ts +1 -0
  57. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  58. package/dist/hooks/magic-context/strip-content.d.ts +5 -2
  59. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  60. package/dist/hooks/magic-context/system-date-refresh.d.ts +29 -0
  61. package/dist/hooks/magic-context/system-date-refresh.d.ts.map +1 -0
  62. package/dist/hooks/magic-context/system-prompt-dates.d.ts +6 -0
  63. package/dist/hooks/magic-context/system-prompt-dates.d.ts.map +1 -0
  64. package/dist/hooks/magic-context/system-prompt-delivery.d.ts +47 -0
  65. package/dist/hooks/magic-context/system-prompt-delivery.d.ts.map +1 -0
  66. package/dist/hooks/magic-context/system-prompt-hash.d.ts +4 -0
  67. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  68. package/dist/hooks/magic-context/tag-messages.d.ts +8 -0
  69. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  70. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +2 -0
  71. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  72. package/dist/hooks/magic-context/transform.d.ts +6 -0
  73. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  74. package/dist/{index-9qjqq45k.js → index-0pjvpfkf.js} +21 -1
  75. package/dist/index-6m7rdv7y.js +733 -0
  76. package/dist/{index-1r0k9js3.js → index-6tfxmc4n.js} +10 -10
  77. package/dist/{index-gzcmtmar.js → index-7w18gg78.js} +25 -1
  78. package/dist/{index-nm6a8xpd.js → index-att4bfan.js} +1 -1
  79. package/dist/{index-ezmszwbd.js → index-eypn4ba6.js} +7 -4
  80. package/dist/{index-ha87j7a8.js → index-fdtkt5r1.js} +6 -6
  81. package/dist/{index-kp5qe690.js → index-fg00p1cg.js} +38 -36
  82. package/dist/{index-1bc61ans.js → index-fvnzcnww.js} +44 -1
  83. package/dist/index-gbmqa9x9.js +1170 -0
  84. package/dist/{index-pxwp2hjk.js → index-gzky5rva.js} +30 -53
  85. package/dist/{index-kq57tdf0.js → index-k6xvbj2e.js} +255 -1075
  86. package/dist/{index-0w56v4fm.js → index-mw7c950g.js} +1 -1
  87. package/dist/{index-1g30axtz.js → index-p0n5ryq8.js} +82 -900
  88. package/dist/{index-b7mv1jx3.js → index-p1hnwfd8.js} +1 -1
  89. package/dist/{index-peshmj6h.js → index-r2zcs71c.js} +6 -4
  90. package/dist/{index-kmpvemph.js → index-rafsaxac.js} +2 -2
  91. package/dist/{index-stbr4ce6.js → index-s7fc0fkn.js} +1 -1
  92. package/dist/{index-hj2q50k0.js → index-sfhndpcb.js} +463 -6
  93. package/dist/index-sv0bb7vg.js +6972 -0
  94. package/dist/{index-57mrqed8.js → index-w23dh319.js} +17 -258
  95. package/dist/{index-9rt53q79.js → index-xgrwmj82.js} +1 -1
  96. package/dist/{index-h2py7207.js → index-xnanvj3w.js} +765 -1255
  97. package/dist/{index-2jcbj7r1.js → index-zfzdpeq2.js} +2269 -3802
  98. package/dist/{index-s4j7t92z.js → index-zg8yq4ya.js} +29 -744
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +209 -61
  101. package/dist/{message-index-bounded-h0mrd4pr.js → message-index-bounded-hqktkhdy.js} +7 -6
  102. package/dist/migration-worker.js +6 -5
  103. package/dist/plugin/hooks/create-session-hooks.d.ts +3 -1
  104. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  105. package/dist/plugin/messages-transform.d.ts.map +1 -1
  106. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  107. package/dist/{project-embedding-registry-rtt0djyb.js → project-embedding-registry-0gv3h6y8.js} +6 -6
  108. package/dist/raw-ordinal-warmup-worker.js +1 -1
  109. package/dist/{read-session-chunk-ty89v75e.js → read-session-chunk-5mh0bh8y.js} +5 -4
  110. package/dist/{recomp-orchestrator-4f1yyd1q.js → recomp-orchestrator-kpkhkjhq.js} +20 -19
  111. package/dist/{search-pz4f4wge.js → search-p9w39qv4.js} +15 -14
  112. package/dist/{send-session-notification-38cd5dqt.js → send-session-notification-xffh3bbe.js} +2 -2
  113. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  114. package/dist/shared/storage-file-mutex.d.ts +9 -0
  115. package/dist/shared/storage-file-mutex.d.ts.map +1 -0
  116. package/dist/shared/storage-permissions.d.ts +6 -0
  117. package/dist/shared/storage-permissions.d.ts.map +1 -1
  118. package/dist/shared/window-geometry.d.ts +9 -66
  119. package/dist/shared/window-geometry.d.ts.map +1 -1
  120. package/dist/standalone-plugin-marker.d.ts +8 -0
  121. package/dist/standalone-plugin-marker.d.ts.map +1 -0
  122. package/dist/tools/ctx-reduce/constants.d.ts +1 -2
  123. package/dist/tools/ctx-reduce/constants.d.ts.map +1 -1
  124. package/dist/tools/ctx-reduce/tools.d.ts +1 -1
  125. package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
  126. package/dist/{transform-postprocess-phase-tck4g20s.js → transform-postprocess-phase-481j5a7z.js} +20 -19
  127. package/dist/v2/auto-search-worker.js +3 -3
  128. package/dist/v2/{context-crbt6p1y.js → context-gsdtha5v.js} +873 -2122
  129. package/dist/v2/{derive-budgets-fgej9j46.js → derive-budgets-enckmg7y.js} +2 -2
  130. package/dist/v2/{embedding-backfill-ph1qfyn3.js → embedding-backfill-dbvjbcmm.js} +8 -9
  131. package/dist/v2/embedding-worker-process.js +29 -0
  132. package/dist/v2/embedding-worker.js +3 -4
  133. package/dist/v2/hooks/context.d.ts.map +1 -1
  134. package/dist/v2/hooks/copied-tag-notation.d.ts +19 -0
  135. package/dist/v2/hooks/copied-tag-notation.d.ts.map +1 -0
  136. package/dist/v2/hooks/payload.d.ts.map +1 -1
  137. package/dist/v2/hooks/system-prompt-response.d.ts +5 -0
  138. package/dist/v2/hooks/system-prompt-response.d.ts.map +1 -0
  139. package/dist/v2/{message-index-bounded-xwqdywxg.js → message-index-bounded-p6v0n8qh.js} +8 -7
  140. package/dist/v2/migration-worker.js +6 -5
  141. package/dist/v2/{project-embedding-registry-d6m7tyxr.js → project-embedding-registry-nyq7z8y6.js} +7 -8
  142. package/dist/v2/raw-ordinal-warmup-worker.js +2 -2
  143. package/dist/v2/{read-session-chunk-8p5mdryx.js → read-session-chunk-fhyfe4qa.js} +6 -5
  144. package/dist/v2/{recomp-orchestrator-ckfgt6wq.js → recomp-orchestrator-5xy7w136.js} +22 -22
  145. package/dist/v2/{search-ym8rehhz.js → search-j7bakcwa.js} +17 -17
  146. package/dist/v2/{send-session-notification-5m0yapqx.js → send-session-notification-f6v698r5.js} +2 -2
  147. package/dist/v2/{server-jvxzm1jy.js → server-1fszbabv.js} +28 -28
  148. package/dist/v2/{server-3j4d00kf.js → server-2htmbqbv.js} +25 -1
  149. package/dist/v2/{server-c5rrgrcd.js → server-4c7jt8gq.js} +25 -1
  150. package/dist/v2/{server-ycw9qqcy.js → server-4enr7s6x.js} +1 -1
  151. package/dist/v2/{server-v4mewb78.js → server-6tcrjfw8.js} +1 -1
  152. package/dist/v2/{server-x4hscabv.js → server-7spkq1y6.js} +2257 -3746
  153. package/dist/v2/{server-sa8jvrfc.js → server-7vkj6zhb.js} +29 -744
  154. package/dist/v2/{server-xwz4dqh9.js → server-8xmq9h3f.js} +30 -53
  155. package/dist/v2/{server-1tk82krw.js → server-bgzy0phk.js} +463 -6
  156. package/dist/v2/server-c7n2ftng.js +6860 -0
  157. package/dist/v2/server-cvgn49tr.js +733 -0
  158. package/dist/v2/{server-zm570tr2.js → server-cw6521wn.js} +232 -7
  159. package/dist/v2/{server-pj4pjv02.js → server-czb2rkpk.js} +21 -1
  160. package/dist/v2/{server-19zjmn02.js → server-d6g4whc2.js} +20 -14
  161. package/dist/v2/{server-1bc61ans.js → server-fvnzcnww.js} +44 -1
  162. package/dist/v2/{server-w32kmdez.js → server-kh958yj8.js} +3 -3
  163. package/dist/v2/{server-q4ph9bwn.js → server-mqvtz7wz.js} +7 -7
  164. package/dist/v2/{server-2s6ayq3t.js → server-ras2sxnc.js} +133 -13
  165. package/dist/v2/{server-ss95ky04.js → server-s2st631p.js} +1 -1
  166. package/dist/v2/{server-2s9qz6n7.js → server-t528t8qm.js} +1 -1
  167. package/dist/v2/{server-8k2agx29.js → server-vb7pbd9p.js} +17 -258
  168. package/dist/v2/{server-zp53y8js.js → server-zbxjj2mz.js} +2 -2
  169. package/dist/v2/server.d.ts.map +1 -1
  170. package/dist/v2/server.js +149 -5
  171. package/dist/v2/{storage-tags-8vw924zn.js → storage-tags-5yxrvkaf.js} +1 -1
  172. package/dist/v2/{wrapup-orchestrator-a7efxaax.js → wrapup-orchestrator-yem5422s.js} +28 -28
  173. package/dist/{wrapup-orchestrator-barbc6h9.js → wrapup-orchestrator-xvrr27ws.js} +21 -20
  174. package/package.json +4 -4
  175. package/src/config/{mural-model-deprecation.ts → deprecated-config.ts} +19 -0
  176. package/src/config/index.test.ts +15 -37
  177. package/src/config/index.ts +2 -3
  178. package/src/config/project-security.ts +3 -3
  179. package/src/config/schema/magic-context.ts +11 -7
  180. package/src/features/magic-context/dreamer/task-registry.ts +2 -3
  181. package/src/shared/models-dev-cache.ts +1 -7
  182. package/src/shared/sqlite-640-review.test.ts +5 -1
  183. package/src/shared/storage-file-mutex-review.test.ts +162 -0
  184. package/src/shared/storage-file-mutex.ts +130 -0
  185. package/src/shared/storage-permissions.ts +46 -0
  186. package/src/shared/window-geometry.test.ts +102 -236
  187. package/src/shared/window-geometry.ts +23 -413
  188. package/dist/config/mural-model-deprecation.d.ts.map +0 -1
  189. package/dist/index-2jgwttpt.js +0 -3269
  190. package/dist/index-mfkzp7qj.js +0 -229
  191. package/dist/v2/server-4ez8mntn.js +0 -3213
  192. package/dist/v2/server-mfkzp7qj.js +0 -229
  193. package/src/shared/__fixtures__/window-overlay.v1.json +0 -337
  194. /package/dist/{index-2pp04pmg.js → index-69vas84q.js} +0 -0
  195. /package/dist/v2/{index-2pp04pmg.js → index-69vas84q.js} +0 -0
@@ -2,9 +2,10 @@ import { expect, spyOn, test } from "bun:test";
2
2
  import { openDatabase } from "../features/magic-context/storage-db";
3
3
  import { getOrCreateSessionMeta, updateSessionMeta } from "../features/magic-context/storage-meta";
4
4
  import { recordToolDefinition } from "../features/magic-context/tool-definition-tokens";
5
+ import { saveLkgSlotToDb } from "../hooks/magic-context/lkg-persist";
5
6
  import { captureLkgSlot } from "../hooks/magic-context/lkg-replay";
6
7
  import { lkgReplayFits } from "../hooks/magic-context/lkg-replay-fit";
7
- import { resetLkgSlotsForTest } from "../hooks/magic-context/lkg-slot";
8
+ import { getSlot, resetLkgSlotsForTest } from "../hooks/magic-context/lkg-slot";
8
9
  import type { MessageLike } from "../hooks/magic-context/transform-operations";
9
10
  import { createMessagesTransformHandler } from "../plugin/messages-transform";
10
11
  import { refreshModelLimitsFromApi } from "./models-dev-cache";
@@ -141,6 +142,9 @@ test("review: OpenCode tries a valid saved request before backed-off writer retr
141
142
  providerKey: "openai",
142
143
  }),
143
144
  ).toBe(true);
145
+ expect(saveLkgSlotToDb(db, sessionId, getSlot(sessionId)!, undefined, { rebuild: true })).toBe(
146
+ true,
147
+ );
144
148
  expect(
145
149
  lkgReplayFits({
146
150
  db,
@@ -0,0 +1,162 @@
1
+ import { expect, test } from "bun:test";
2
+ import { spawnSync } from "node:child_process";
3
+ import { readdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { __resetRpcIdentityTestHooks, __setRpcIdentityTestHooks } from "./rpc-utils";
6
+ import { withStorageFileMutex } from "./storage-file-mutex";
7
+ import { createTestTempDir } from "./test-temp-dir";
8
+
9
+ const mutex = join(import.meta.dir, "storage-file-mutex.ts");
10
+
11
+ async function holder(root: string) {
12
+ const script = `
13
+ import { existsSync } from 'node:fs';
14
+ import { withStorageFileMutex } from ${JSON.stringify(mutex)};
15
+ withStorageFileMutex(${JSON.stringify(join(root, "locks"))}, () => {
16
+ console.log('HELD');
17
+ const pause = new Int32Array(new SharedArrayBuffer(4));
18
+ while (!existsSync(${JSON.stringify(join(root, "release"))})) Atomics.wait(pause, 0, 0, 10);
19
+ });`;
20
+ const child = Bun.spawn([process.execPath, "--eval", script], {
21
+ env: {
22
+ ...process.env,
23
+ XDG_DATA_HOME: root,
24
+ XDG_CONFIG_HOME: root,
25
+ MAGIC_CONTEXT_STORAGE_DIR: root,
26
+ MAGIC_CONTEXT_TEST_DATA_DIR: root,
27
+ },
28
+ stdout: "pipe",
29
+ stderr: "pipe",
30
+ windowsHide: true,
31
+ });
32
+ const reader = child.stdout.getReader();
33
+ const first = await reader.read();
34
+ expect(new TextDecoder().decode(first.value)).toContain("HELD");
35
+ reader.releaseLock();
36
+ if (process.platform === "darwin") {
37
+ const files = spawnSync("/usr/sbin/lsof", ["-p", String(child.pid), "-Fn"], {
38
+ encoding: "utf8",
39
+ windowsHide: true,
40
+ });
41
+ expect(files.status).toBe(0);
42
+ const names = files.stdout.split("\n").filter((line) => line.startsWith("n"));
43
+ expect(
44
+ names.some((line) =>
45
+ /\/(?:\.local\/share\/opencode|\.local\/share\/cortexkit\/magic-context|\.config\/(?:opencode|cortexkit))\//.test(
46
+ line,
47
+ ),
48
+ ),
49
+ ).toBe(false);
50
+ console.log(`REVIEW_LSOF pid=${child.pid} paths=${names.length} forbidden=0`);
51
+ }
52
+ return child;
53
+ }
54
+
55
+ test.skipIf(process.platform === "win32")(
56
+ "independent mutex: paused live holder is not taken over, dead holder is reclaimed",
57
+ async () => {
58
+ const { dir: root, cleanup } = createTestTempDir("mc-mutex-review-");
59
+ const child = await holder(root);
60
+ try {
61
+ process.kill(child.pid, "SIGSTOP");
62
+ const start = performance.now();
63
+ expect(() =>
64
+ withStorageFileMutex(
65
+ join(root, "locks"),
66
+ () => {
67
+ throw new Error("LIVE HOLDER WAS TAKEN OVER");
68
+ },
69
+ 200,
70
+ ),
71
+ ).toThrow("Storage safety publication lock is unavailable");
72
+ expect(performance.now() - start).toBeLessThan(2500);
73
+ process.kill(child.pid, "SIGKILL");
74
+ await child.exited;
75
+ let reached = false;
76
+ withStorageFileMutex(
77
+ join(root, "locks"),
78
+ () => {
79
+ reached = true;
80
+ },
81
+ 200,
82
+ );
83
+ expect(reached).toBe(true);
84
+ expect(readdirSync(join(root, "locks"))).toEqual([]);
85
+ } finally {
86
+ if (child.exitCode === null) {
87
+ child.kill("SIGKILL");
88
+ await child.exited;
89
+ }
90
+ cleanup();
91
+ }
92
+ },
93
+ 15000,
94
+ );
95
+
96
+ test.skipIf(process.platform === "win32")(
97
+ "independent mutex: corrupt live-owner identity refuses within a bound",
98
+ async () => {
99
+ const { dir: root, cleanup } = createTestTempDir("mc-mutex-review-");
100
+ const child = await holder(root);
101
+ try {
102
+ const locks = join(root, "locks");
103
+ const ticket = join(locks, readdirSync(locks)[0]!);
104
+ expect(JSON.parse(readFileSync(ticket, "utf8")).birth).toBeString();
105
+ writeFileSync(ticket, "inconclusive owner identity", { mode: 0o600 });
106
+ const start = performance.now();
107
+ let entered = false;
108
+ expect(() =>
109
+ withStorageFileMutex(
110
+ locks,
111
+ () => {
112
+ entered = true;
113
+ },
114
+ 200,
115
+ ),
116
+ ).toThrow("Storage safety publication lock is unavailable");
117
+ expect(entered).toBe(false);
118
+ expect(performance.now() - start).toBeLessThan(2500);
119
+ expect(readdirSync(locks)).toHaveLength(1);
120
+ } finally {
121
+ child.kill("SIGKILL");
122
+ await child.exited;
123
+ cleanup();
124
+ }
125
+ },
126
+ 15000,
127
+ );
128
+
129
+ test.skipIf(process.platform === "win32")(
130
+ "independent mutex: denied PID probe is inconclusive and refuses within a bound",
131
+ async () => {
132
+ const { dir: root, cleanup } = createTestTempDir("mc-mutex-inconclusive-review-");
133
+ const child = await holder(root);
134
+ try {
135
+ __setRpcIdentityTestHooks({
136
+ processKill: () => {
137
+ throw Object.assign(new Error("Denied PID probe"), { code: "EPERM" });
138
+ },
139
+ });
140
+ const start = performance.now();
141
+ let entered = false;
142
+ expect(() =>
143
+ withStorageFileMutex(
144
+ join(root, "locks"),
145
+ () => {
146
+ entered = true;
147
+ },
148
+ 200,
149
+ ),
150
+ ).toThrow("Storage safety publication lock is unavailable");
151
+ expect(entered).toBe(false);
152
+ expect(performance.now() - start).toBeLessThan(2500);
153
+ expect(readdirSync(join(root, "locks"))).toHaveLength(1);
154
+ } finally {
155
+ __resetRpcIdentityTestHooks();
156
+ child.kill("SIGKILL");
157
+ await child.exited;
158
+ cleanup();
159
+ }
160
+ },
161
+ 15000,
162
+ );
@@ -0,0 +1,130 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { readdirSync, readFileSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { isPidAlive, readProcessStartTime } from "./rpc-utils";
6
+ import {
7
+ ensurePrivateStorageDirectorySync,
8
+ writeStorageFileAtomicSync,
9
+ writeStorageFileSync,
10
+ } from "./storage-permissions";
11
+
12
+ interface Ticket {
13
+ choosing: boolean;
14
+ number: number;
15
+ birth: string | null;
16
+ checksum: string;
17
+ }
18
+ const ticketChecksum = (name: string, ticket: Ticket) =>
19
+ createHash("sha256")
20
+ .update(JSON.stringify([name, ticket.choosing, ticket.number, ticket.birth]))
21
+ .digest("hex");
22
+ function processBirth(pid: number): string | null {
23
+ if (process.platform === "linux") {
24
+ try {
25
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
26
+ const ticks = stat
27
+ .slice(stat.lastIndexOf(")") + 2)
28
+ .trim()
29
+ .split(/\s+/)[19];
30
+ if (!ticks || !/^\d+$/.test(ticks)) return null;
31
+ return `${readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim()}:${ticks}`;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+ if (process.platform === "darwin") {
37
+ try {
38
+ return (
39
+ execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
40
+ encoding: "utf8",
41
+ timeout: 1000,
42
+ windowsHide: true,
43
+ env: { ...process.env, TZ: "UTC", LC_ALL: "C" },
44
+ stdio: ["ignore", "pipe", "pipe"],
45
+ }).trim() || null
46
+ );
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ const start = readProcessStartTime(pid);
52
+ return start === null ? null : String(start);
53
+ }
54
+ const pause = new Int32Array(new SharedArrayBuffer(4));
55
+
56
+ /**
57
+ * Lamport's bakery mutex using unique contender files on a local filesystem.
58
+ * Exclusive creation and atomic replacement work on POSIX and Windows. A dead
59
+ * owner is removed by PID/start-time evidence, never by age: a paused live
60
+ * publisher must not lose its lock. Uncertain liveness refuses after the budget.
61
+ * Unique names mean reclaiming a dead ticket cannot unlink a successor's lock.
62
+ */
63
+ export function withStorageFileMutex<T>(directory: string, work: () => T, timeoutMs = 1000): T {
64
+ ensurePrivateStorageDirectorySync(directory);
65
+ const name = `${process.pid}-${randomUUID()}.lock`;
66
+ const ownPath = join(directory, name);
67
+ const birth = processBirth(process.pid);
68
+ const ticket: Ticket = { choosing: true, number: 0, birth, checksum: "" };
69
+ const encodedTicket = () =>
70
+ JSON.stringify({ ...ticket, checksum: ticketChecksum(name, ticket) });
71
+ writeStorageFileSync(ownPath, encodedTicket(), { flag: "wx", forcePrivate: true });
72
+ const deadline = performance.now() + timeoutMs;
73
+ const peers = (): [string, Ticket | null][] => {
74
+ const found: [string, Ticket | null][] = [];
75
+ for (const other of readdirSync(directory)) {
76
+ if (other === name || !/^\d+-[\da-f-]+\.lock$/.test(other)) continue;
77
+ const pid = Number(other.split("-")[0]);
78
+ const otherPath = join(directory, other);
79
+ let value: Ticket | null = null;
80
+ try {
81
+ const parsed = JSON.parse(readFileSync(otherPath, "utf8")) as Ticket;
82
+ if (
83
+ typeof parsed.choosing === "boolean" &&
84
+ Number.isSafeInteger(parsed.number) &&
85
+ parsed.number >= 0 &&
86
+ (parsed.birth === null || typeof parsed.birth === "string") &&
87
+ parsed.checksum === ticketChecksum(other, parsed)
88
+ )
89
+ value = parsed;
90
+ } catch (error) {
91
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
92
+ }
93
+ const alive = isPidAlive(pid);
94
+ const actualBirth = alive === "alive" && value?.birth ? processBirth(pid) : null;
95
+ if (
96
+ alive === "dead" ||
97
+ (actualBirth !== null && value?.birth && actualBirth !== value.birth)
98
+ ) {
99
+ rmSync(otherPath, { force: true });
100
+ continue;
101
+ }
102
+ found.push([other, value]);
103
+ }
104
+ return found;
105
+ };
106
+ try {
107
+ ticket.number = 1 + Math.max(0, ...peers().map(([, value]) => value?.number ?? 0));
108
+ ticket.choosing = false;
109
+ writeStorageFileAtomicSync(ownPath, encodedTicket(), true);
110
+ while (
111
+ peers().some(
112
+ ([other, value]) =>
113
+ !value ||
114
+ value.choosing ||
115
+ value.number < ticket.number ||
116
+ (value.number === ticket.number && other < name),
117
+ )
118
+ ) {
119
+ if (performance.now() >= deadline)
120
+ throw new Error("Storage safety publication lock is unavailable");
121
+ Atomics.wait(pause, 0, 0, 10);
122
+ }
123
+ const result = work();
124
+ if (result && typeof (result as { then?: unknown }).then === "function")
125
+ throw new TypeError("Storage publication mutex requires synchronous work");
126
+ return result;
127
+ } finally {
128
+ rmSync(ownPath, { force: true });
129
+ }
130
+ }
@@ -10,6 +10,7 @@ import {
10
10
  chmodSync,
11
11
  closeSync,
12
12
  createWriteStream,
13
+ fsyncSync,
13
14
  lstatSync,
14
15
  mkdirSync,
15
16
  openSync,
@@ -54,6 +55,15 @@ export function ensureStorageDirectorySync(
54
55
  });
55
56
  }
56
57
 
58
+ /** Replay-authorizing files require a private real directory, even after a permissive restore. */
59
+ export function ensurePrivateStorageDirectorySync(directory: string): void {
60
+ ensureStorageDirectorySync(directory, true);
61
+ const stat = lstatSync(directory);
62
+ if (!stat.isDirectory() || stat.isSymbolicLink())
63
+ throw new Error("Private storage directory is not a real directory");
64
+ if (process.platform !== "win32") chmodSync(directory, 0o700);
65
+ }
66
+
57
67
  /** Create or replace a storage file using a private mode from the first byte. */
58
68
  export function writeStorageFileSync(
59
69
  filePath: string,
@@ -85,6 +95,42 @@ export function writeStorageFileAtomicSync(
85
95
  }
86
96
  }
87
97
 
98
+ /** Flush the namespace containing a safety-critical sidecar (POSIX only). */
99
+ export function syncStorageDirectorySync(directory: string): void {
100
+ const fd = openSync(directory, "r");
101
+ try {
102
+ fsyncSync(fd);
103
+ } finally {
104
+ closeSync(fd);
105
+ }
106
+ }
107
+
108
+ /** Publish a safety record only after both its bytes and directory entry are durable. */
109
+ export function writeStorageFileDurableAtomicSync(
110
+ filePath: string,
111
+ content: string,
112
+ afterTempSync?: () => void,
113
+ ): void {
114
+ const directory = dirname(filePath);
115
+ ensurePrivateStorageDirectorySync(directory);
116
+ const tempPath = `${filePath}.${process.pid}.${randomBytes(12).toString("hex")}.tmp`;
117
+ let fd: number | undefined;
118
+ try {
119
+ fd = openStorageFileSync(tempPath, "wx", true);
120
+ writeFileSync(fd, content, "utf8");
121
+ fsyncSync(fd);
122
+ closeSync(fd);
123
+ fd = undefined;
124
+ afterTempSync?.();
125
+ renameSync(tempPath, filePath);
126
+ syncStorageDirectorySync(directory);
127
+ syncStorageDirectorySync(dirname(directory));
128
+ } finally {
129
+ if (fd !== undefined) closeSync(fd);
130
+ rmSync(tempPath, { force: true });
131
+ }
132
+ }
133
+
88
134
  export function copyStorageFileSync(
89
135
  sourcePath: string,
90
136
  destinationPath: string,