@cortexkit/opencode-magic-context 0.49.0 → 0.49.2
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/{announcement-fp14dw0n.js → announcement-hdbkt8v5.js} +1 -1
- package/dist/auto-search-worker.js +3 -3
- package/dist/config/{mural-model-deprecation.d.ts → deprecated-config.d.ts} +4 -1
- package/dist/config/deprecated-config.d.ts.map +1 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +2 -2
- package/dist/config/schema/magic-context.d.ts +4 -4
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-kzx4dmk1.js → conflict-warning-hook-asnt6hfc.js} +3 -3
- package/dist/{context-nj5f293n.js → context-tfyhtdcs.js} +111 -45
- package/dist/{derive-budgets-qz5rrr08.js → derive-budgets-j4ry8j8n.js} +1 -1
- package/dist/{embedding-backfill-aetxv1vt.js → embedding-backfill-wyc3ykyf.js} +7 -7
- package/dist/embedding-worker-process.js +30 -0
- package/dist/embedding-worker.js +5 -5
- package/dist/features/magic-context/dreamer/task-registry.d.ts +2 -3
- package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-worker-client.d.ts +8 -1
- package/dist/features/magic-context/memory/embedding-worker-client.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-worker-process.d.ts +2 -0
- package/dist/features/magic-context/memory/embedding-worker-process.d.ts.map +1 -0
- package/dist/features/magic-context/memory/onnx-telemetry.d.ts +3 -0
- package/dist/features/magic-context/memory/onnx-telemetry.d.ts.map +1 -0
- package/dist/features/magic-context/overflow-detection.d.ts +1 -2
- package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts +1 -0
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +1 -0
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +1 -0
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-measured-request.d.ts +3 -1
- package/dist/hooks/magic-context/lkg-measured-request.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-persist.d.ts +8 -14
- package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-recovery-fence.d.ts +62 -0
- package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -0
- package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts +38 -0
- package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts.map +1 -0
- package/dist/hooks/magic-context/lkg-replay.d.ts +3 -1
- package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/opaque-thinking.d.ts +12 -0
- package/dist/hooks/magic-context/opaque-thinking.d.ts.map +1 -0
- package/dist/hooks/magic-context/pass-outcome.d.ts +2 -2
- package/dist/hooks/magic-context/pending-materialization.d.ts +8 -0
- package/dist/hooks/magic-context/pending-materialization.d.ts.map +1 -0
- package/dist/hooks/magic-context/read-session-db.d.ts +3 -0
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +1 -0
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts +5 -2
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-date-refresh.d.ts +29 -0
- package/dist/hooks/magic-context/system-date-refresh.d.ts.map +1 -0
- package/dist/hooks/magic-context/system-prompt-dates.d.ts +6 -0
- package/dist/hooks/magic-context/system-prompt-dates.d.ts.map +1 -0
- package/dist/hooks/magic-context/system-prompt-delivery.d.ts +47 -0
- package/dist/hooks/magic-context/system-prompt-delivery.d.ts.map +1 -0
- package/dist/hooks/magic-context/system-prompt-hash.d.ts +4 -0
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +8 -0
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +2 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +6 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-9qjqq45k.js → index-0pjvpfkf.js} +21 -1
- package/dist/index-6m7rdv7y.js +733 -0
- package/dist/{index-1r0k9js3.js → index-6tfxmc4n.js} +10 -10
- package/dist/{index-gzcmtmar.js → index-7w18gg78.js} +25 -1
- package/dist/{index-nm6a8xpd.js → index-att4bfan.js} +1 -1
- package/dist/{index-ezmszwbd.js → index-eypn4ba6.js} +7 -4
- package/dist/{index-ha87j7a8.js → index-fdtkt5r1.js} +6 -6
- package/dist/{index-kp5qe690.js → index-fg00p1cg.js} +38 -36
- package/dist/{index-1bc61ans.js → index-fvnzcnww.js} +44 -1
- package/dist/index-gbmqa9x9.js +1170 -0
- package/dist/{index-pxwp2hjk.js → index-gzky5rva.js} +30 -53
- package/dist/{index-kq57tdf0.js → index-k6xvbj2e.js} +255 -1075
- package/dist/{index-0w56v4fm.js → index-mw7c950g.js} +1 -1
- package/dist/{index-1g30axtz.js → index-p0n5ryq8.js} +82 -900
- package/dist/{index-b7mv1jx3.js → index-p1hnwfd8.js} +1 -1
- package/dist/{index-peshmj6h.js → index-r2zcs71c.js} +6 -4
- package/dist/{index-kmpvemph.js → index-rafsaxac.js} +2 -2
- package/dist/{index-stbr4ce6.js → index-s7fc0fkn.js} +1 -1
- package/dist/{index-hj2q50k0.js → index-sfhndpcb.js} +463 -6
- package/dist/index-sv0bb7vg.js +6972 -0
- package/dist/{index-57mrqed8.js → index-w23dh319.js} +17 -258
- package/dist/{index-9rt53q79.js → index-xgrwmj82.js} +1 -1
- package/dist/{index-h2py7207.js → index-xnanvj3w.js} +765 -1255
- package/dist/{index-2jcbj7r1.js → index-zfzdpeq2.js} +2269 -3802
- package/dist/{index-s4j7t92z.js → index-zg8yq4ya.js} +29 -744
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +209 -61
- package/dist/{message-index-bounded-h0mrd4pr.js → message-index-bounded-hqktkhdy.js} +7 -6
- package/dist/migration-worker.js +6 -5
- package/dist/plugin/hooks/create-session-hooks.d.ts +3 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{project-embedding-registry-rtt0djyb.js → project-embedding-registry-0gv3h6y8.js} +6 -6
- package/dist/raw-ordinal-warmup-worker.js +1 -1
- package/dist/{read-session-chunk-ty89v75e.js → read-session-chunk-5mh0bh8y.js} +5 -4
- package/dist/{recomp-orchestrator-4f1yyd1q.js → recomp-orchestrator-kpkhkjhq.js} +20 -19
- package/dist/{search-pz4f4wge.js → search-p9w39qv4.js} +15 -14
- package/dist/{send-session-notification-38cd5dqt.js → send-session-notification-xffh3bbe.js} +2 -2
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/storage-file-mutex.d.ts +9 -0
- package/dist/shared/storage-file-mutex.d.ts.map +1 -0
- package/dist/shared/storage-permissions.d.ts +6 -0
- package/dist/shared/storage-permissions.d.ts.map +1 -1
- package/dist/shared/window-geometry.d.ts +9 -66
- package/dist/shared/window-geometry.d.ts.map +1 -1
- package/dist/standalone-plugin-marker.d.ts +8 -0
- package/dist/standalone-plugin-marker.d.ts.map +1 -0
- package/dist/tools/ctx-reduce/constants.d.ts +1 -2
- package/dist/tools/ctx-reduce/constants.d.ts.map +1 -1
- package/dist/tools/ctx-reduce/tools.d.ts +1 -1
- package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
- package/dist/{transform-postprocess-phase-tck4g20s.js → transform-postprocess-phase-481j5a7z.js} +20 -19
- package/dist/v2/auto-search-worker.js +3 -3
- package/dist/v2/{context-crbt6p1y.js → context-gsdtha5v.js} +873 -2122
- package/dist/v2/{derive-budgets-fgej9j46.js → derive-budgets-enckmg7y.js} +2 -2
- package/dist/v2/{embedding-backfill-ph1qfyn3.js → embedding-backfill-dbvjbcmm.js} +8 -9
- package/dist/v2/embedding-worker-process.js +29 -0
- package/dist/v2/embedding-worker.js +3 -4
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/copied-tag-notation.d.ts +19 -0
- package/dist/v2/hooks/copied-tag-notation.d.ts.map +1 -0
- package/dist/v2/hooks/payload.d.ts.map +1 -1
- package/dist/v2/hooks/system-prompt-response.d.ts +5 -0
- package/dist/v2/hooks/system-prompt-response.d.ts.map +1 -0
- package/dist/v2/{message-index-bounded-xwqdywxg.js → message-index-bounded-p6v0n8qh.js} +8 -7
- package/dist/v2/migration-worker.js +6 -5
- package/dist/v2/{project-embedding-registry-d6m7tyxr.js → project-embedding-registry-nyq7z8y6.js} +7 -8
- package/dist/v2/raw-ordinal-warmup-worker.js +2 -2
- package/dist/v2/{read-session-chunk-8p5mdryx.js → read-session-chunk-fhyfe4qa.js} +6 -5
- package/dist/v2/{recomp-orchestrator-ckfgt6wq.js → recomp-orchestrator-5xy7w136.js} +22 -22
- package/dist/v2/{search-ym8rehhz.js → search-j7bakcwa.js} +17 -17
- package/dist/v2/{send-session-notification-5m0yapqx.js → send-session-notification-f6v698r5.js} +2 -2
- package/dist/v2/{server-jvxzm1jy.js → server-1fszbabv.js} +28 -28
- package/dist/v2/{server-3j4d00kf.js → server-2htmbqbv.js} +25 -1
- package/dist/v2/{server-c5rrgrcd.js → server-4c7jt8gq.js} +25 -1
- package/dist/v2/{server-ycw9qqcy.js → server-4enr7s6x.js} +1 -1
- package/dist/v2/{server-v4mewb78.js → server-6tcrjfw8.js} +1 -1
- package/dist/v2/{server-x4hscabv.js → server-7spkq1y6.js} +2257 -3746
- package/dist/v2/{server-sa8jvrfc.js → server-7vkj6zhb.js} +29 -744
- package/dist/v2/{server-xwz4dqh9.js → server-8xmq9h3f.js} +30 -53
- package/dist/v2/{server-1tk82krw.js → server-bgzy0phk.js} +463 -6
- package/dist/v2/server-c7n2ftng.js +6860 -0
- package/dist/v2/server-cvgn49tr.js +733 -0
- package/dist/v2/{server-zm570tr2.js → server-cw6521wn.js} +232 -7
- package/dist/v2/{server-pj4pjv02.js → server-czb2rkpk.js} +21 -1
- package/dist/v2/{server-19zjmn02.js → server-d6g4whc2.js} +20 -14
- package/dist/v2/{server-1bc61ans.js → server-fvnzcnww.js} +44 -1
- package/dist/v2/{server-w32kmdez.js → server-kh958yj8.js} +3 -3
- package/dist/v2/{server-q4ph9bwn.js → server-mqvtz7wz.js} +7 -7
- package/dist/v2/{server-2s6ayq3t.js → server-ras2sxnc.js} +133 -13
- package/dist/v2/{server-ss95ky04.js → server-s2st631p.js} +1 -1
- package/dist/v2/{server-2s9qz6n7.js → server-t528t8qm.js} +1 -1
- package/dist/v2/{server-8k2agx29.js → server-vb7pbd9p.js} +17 -258
- package/dist/v2/{server-zp53y8js.js → server-zbxjj2mz.js} +2 -2
- package/dist/v2/server.d.ts.map +1 -1
- package/dist/v2/server.js +149 -5
- package/dist/v2/{storage-tags-8vw924zn.js → storage-tags-5yxrvkaf.js} +1 -1
- package/dist/v2/{wrapup-orchestrator-a7efxaax.js → wrapup-orchestrator-yem5422s.js} +28 -28
- package/dist/{wrapup-orchestrator-barbc6h9.js → wrapup-orchestrator-xvrr27ws.js} +21 -20
- package/package.json +4 -4
- package/src/config/{mural-model-deprecation.ts → deprecated-config.ts} +19 -0
- package/src/config/index.test.ts +15 -37
- package/src/config/index.ts +2 -3
- package/src/config/project-security.ts +3 -3
- package/src/config/schema/magic-context.ts +11 -7
- package/src/features/magic-context/dreamer/task-registry.ts +2 -3
- package/src/shared/models-dev-cache.ts +1 -7
- package/src/shared/sqlite-640-review.test.ts +5 -1
- package/src/shared/storage-file-mutex-review.test.ts +162 -0
- package/src/shared/storage-file-mutex.ts +130 -0
- package/src/shared/storage-permissions.ts +46 -0
- package/src/shared/window-geometry.test.ts +102 -236
- package/src/shared/window-geometry.ts +23 -413
- package/dist/config/mural-model-deprecation.d.ts.map +0 -1
- package/dist/index-2jgwttpt.js +0 -3269
- package/dist/index-mfkzp7qj.js +0 -229
- package/dist/v2/server-4ez8mntn.js +0 -3213
- package/dist/v2/server-mfkzp7qj.js +0 -229
- package/src/shared/__fixtures__/window-overlay.v1.json +0 -337
- /package/dist/{index-2pp04pmg.js → index-69vas84q.js} +0 -0
- /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,
|