@remnic/core 9.3.600 → 9.3.602

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 (48) hide show
  1. package/dist/access-cli.js +6 -6
  2. package/dist/access-http.js +6 -6
  3. package/dist/access-mcp.js +5 -5
  4. package/dist/access-service.js +4 -4
  5. package/dist/causal-behavior.js +2 -2
  6. package/dist/causal-chain.js +2 -2
  7. package/dist/causal-consolidation.js +2 -2
  8. package/dist/causal-retrieval.js +2 -2
  9. package/dist/causal-trajectory-graph.js +1 -1
  10. package/dist/causal-trajectory.js +1 -1
  11. package/dist/{chunk-ELKI4BB6.js → chunk-2ESBDLT5.js} +3 -3
  12. package/dist/{chunk-WZA5Y6AC.js → chunk-2QANQKSQ.js} +3 -3
  13. package/dist/{chunk-BDCCWRHR.js → chunk-5RPTH6AU.js} +3 -3
  14. package/dist/{chunk-JKV57BTN.js → chunk-7V2SGZ3H.js} +2 -2
  15. package/dist/{chunk-D4KJ74JJ.js → chunk-EWC6W6AB.js} +2 -2
  16. package/dist/{chunk-V67GWXM2.js → chunk-IP73YCZP.js} +1 -1
  17. package/dist/{chunk-KDUVQU6Y.js → chunk-MTGOAU7A.js} +4 -4
  18. package/dist/{chunk-65JSA4MP.js → chunk-RUYYYLDT.js} +7 -7
  19. package/dist/{chunk-CL3MWNNQ.js → chunk-TH67Q46T.js} +3 -3
  20. package/dist/{chunk-ZZYF3BUL.js → chunk-TQOU3VAT.js} +1 -1
  21. package/dist/{chunk-4JRRISLU.js → chunk-XL7FK7PJ.js} +61 -43
  22. package/dist/chunk-XL7FK7PJ.js.map +1 -0
  23. package/dist/cli.js +8 -8
  24. package/dist/compounding/engine.js +1 -1
  25. package/dist/{graph-edge-decay-MUP5J7CC.js → graph-edge-decay-5ZOK7ZNO.js} +2 -2
  26. package/dist/graph-snapshot.js +2 -2
  27. package/dist/graph.js +1 -1
  28. package/dist/index.js +403 -116
  29. package/dist/index.js.map +1 -1
  30. package/dist/operator-toolkit.js +2 -2
  31. package/dist/orchestrator.js +4 -4
  32. package/package.json +1 -1
  33. package/src/graph.test.ts +76 -11
  34. package/src/graph.ts +101 -88
  35. package/src/spaces/index.test.ts +205 -18
  36. package/src/spaces/index.ts +453 -139
  37. package/dist/chunk-4JRRISLU.js.map +0 -1
  38. /package/dist/{chunk-ELKI4BB6.js.map → chunk-2ESBDLT5.js.map} +0 -0
  39. /package/dist/{chunk-WZA5Y6AC.js.map → chunk-2QANQKSQ.js.map} +0 -0
  40. /package/dist/{chunk-BDCCWRHR.js.map → chunk-5RPTH6AU.js.map} +0 -0
  41. /package/dist/{chunk-JKV57BTN.js.map → chunk-7V2SGZ3H.js.map} +0 -0
  42. /package/dist/{chunk-D4KJ74JJ.js.map → chunk-EWC6W6AB.js.map} +0 -0
  43. /package/dist/{chunk-V67GWXM2.js.map → chunk-IP73YCZP.js.map} +0 -0
  44. /package/dist/{chunk-KDUVQU6Y.js.map → chunk-MTGOAU7A.js.map} +0 -0
  45. /package/dist/{chunk-65JSA4MP.js.map → chunk-RUYYYLDT.js.map} +0 -0
  46. /package/dist/{chunk-CL3MWNNQ.js.map → chunk-TH67Q46T.js.map} +0 -0
  47. /package/dist/{chunk-ZZYF3BUL.js.map → chunk-TQOU3VAT.js.map} +0 -0
  48. /package/dist/{graph-edge-decay-MUP5J7CC.js.map → graph-edge-decay-5ZOK7ZNO.js.map} +0 -0
@@ -1,11 +1,32 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdtemp, rm } from "node:fs/promises";
2
+ import { spawn } from "node:child_process";
3
+ import fs, { existsSync } from "node:fs";
4
+ import { mkdir, mkdtemp, rm, utimes, writeFile } from "node:fs/promises";
3
5
  import os from "node:os";
4
6
  import path from "node:path";
5
7
  import test from "node:test";
8
+ import { setTimeout as sleep } from "node:timers/promises";
9
+ import { fileURLToPath } from "node:url";
6
10
 
7
11
  import { createSpace, loadManifest } from "./index.js";
8
12
 
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ const repoRoot = path.resolve(__dirname, "../../../..");
15
+ const tsxCli = path.join(repoRoot, "node_modules", "tsx", "dist", "cli.mjs");
16
+ const spacesSourcePath = path.join(__dirname, "index.ts");
17
+
18
+ function unsetEnv(name: string): void {
19
+ Reflect.deleteProperty(process.env, name);
20
+ }
21
+
22
+ function restoreEnv(name: string, value: string | undefined): void {
23
+ if (value === undefined) {
24
+ unsetEnv(name);
25
+ } else {
26
+ process.env[name] = value;
27
+ }
28
+ }
29
+
9
30
  test("personal space bootstrap prefers REMNIC_MEMORY_DIR over legacy ENGRAM_MEMORY_DIR", async () => {
10
31
  const baseDir = await mkdtemp(path.join(os.tmpdir(), "remnic-spaces-current-"));
11
32
  const remnicMemoryDir = path.join(baseDir, "remnic-memory");
@@ -19,10 +40,8 @@ test("personal space bootstrap prefers REMNIC_MEMORY_DIR over legacy ENGRAM_MEMO
19
40
  const manifest = loadManifest(baseDir);
20
41
  assert.equal(manifest.spaces[0]?.memoryDir, remnicMemoryDir);
21
42
  } finally {
22
- if (previousRemnic === undefined) delete process.env.REMNIC_MEMORY_DIR;
23
- else process.env.REMNIC_MEMORY_DIR = previousRemnic;
24
- if (previousEngram === undefined) delete process.env.ENGRAM_MEMORY_DIR;
25
- else process.env.ENGRAM_MEMORY_DIR = previousEngram;
43
+ restoreEnv("REMNIC_MEMORY_DIR", previousRemnic);
44
+ restoreEnv("ENGRAM_MEMORY_DIR", previousEngram);
26
45
  await rm(baseDir, { recursive: true, force: true });
27
46
  }
28
47
  });
@@ -33,16 +52,14 @@ test("personal space bootstrap keeps ENGRAM_MEMORY_DIR as a legacy fallback", as
33
52
  const previousRemnic = process.env.REMNIC_MEMORY_DIR;
34
53
  const previousEngram = process.env.ENGRAM_MEMORY_DIR;
35
54
 
36
- delete process.env.REMNIC_MEMORY_DIR;
55
+ unsetEnv("REMNIC_MEMORY_DIR");
37
56
  process.env.ENGRAM_MEMORY_DIR = legacyMemoryDir;
38
57
  try {
39
58
  const manifest = loadManifest(baseDir);
40
59
  assert.equal(manifest.spaces[0]?.memoryDir, legacyMemoryDir);
41
60
  } finally {
42
- if (previousRemnic === undefined) delete process.env.REMNIC_MEMORY_DIR;
43
- else process.env.REMNIC_MEMORY_DIR = previousRemnic;
44
- if (previousEngram === undefined) delete process.env.ENGRAM_MEMORY_DIR;
45
- else process.env.ENGRAM_MEMORY_DIR = previousEngram;
61
+ restoreEnv("REMNIC_MEMORY_DIR", previousRemnic);
62
+ restoreEnv("ENGRAM_MEMORY_DIR", previousEngram);
46
63
  await rm(baseDir, { recursive: true, force: true });
47
64
  }
48
65
  });
@@ -56,8 +73,7 @@ test("personal space bootstrap normalizes relative memoryDir env values", async
56
73
  const manifest = loadManifest(baseDir);
57
74
  assert.equal(manifest.spaces[0]?.memoryDir, path.resolve("relative-personal-memory"));
58
75
  } finally {
59
- if (previousRemnic === undefined) delete process.env.REMNIC_MEMORY_DIR;
60
- else process.env.REMNIC_MEMORY_DIR = previousRemnic;
76
+ restoreEnv("REMNIC_MEMORY_DIR", previousRemnic);
61
77
  await rm(baseDir, { recursive: true, force: true });
62
78
  }
63
79
  });
@@ -68,8 +84,8 @@ test("createSpace normalizes caller-provided memoryDir before saving manifest",
68
84
  const previousRemnic = process.env.REMNIC_MEMORY_DIR;
69
85
  const previousEngram = process.env.ENGRAM_MEMORY_DIR;
70
86
 
71
- delete process.env.REMNIC_MEMORY_DIR;
72
- delete process.env.ENGRAM_MEMORY_DIR;
87
+ unsetEnv("REMNIC_MEMORY_DIR");
88
+ unsetEnv("ENGRAM_MEMORY_DIR");
73
89
  try {
74
90
  const created = createSpace({
75
91
  baseDir,
@@ -83,11 +99,182 @@ test("createSpace normalizes caller-provided memoryDir before saving manifest",
83
99
  const saved = manifest.spaces.find((space) => space.id === "project");
84
100
  assert.equal(saved?.memoryDir, projectMemoryDir);
85
101
  } finally {
86
- if (previousRemnic === undefined) delete process.env.REMNIC_MEMORY_DIR;
87
- else process.env.REMNIC_MEMORY_DIR = previousRemnic;
88
- if (previousEngram === undefined) delete process.env.ENGRAM_MEMORY_DIR;
89
- else process.env.ENGRAM_MEMORY_DIR = previousEngram;
102
+ restoreEnv("REMNIC_MEMORY_DIR", previousRemnic);
103
+ restoreEnv("ENGRAM_MEMORY_DIR", previousEngram);
90
104
  await rm(baseDir, { recursive: true, force: true });
91
105
  await rm(projectMemoryDir, { recursive: true, force: true });
92
106
  }
93
107
  });
108
+
109
+ test("concurrent createSpace calls preserve both manifest updates", async () => {
110
+ const baseDir = await mkdtemp(path.join(os.tmpdir(), "remnic-spaces-concurrent-"));
111
+ const childScript = path.join(baseDir, "create-space-child.mjs");
112
+ const goPath = path.join(baseDir, "go");
113
+ const manifestPath = path.join(baseDir, ".config", "engram", "spaces", "manifest.json");
114
+
115
+ await writeFile(
116
+ childScript,
117
+ `
118
+ import fs from "node:fs";
119
+ import path from "node:path";
120
+ import { pathToFileURL } from "node:url";
121
+
122
+ const [baseDir, name, manifestPath, goPath, spacesSourcePath] = process.argv.slice(2);
123
+ const readyPath = path.join(baseDir, \`\${name}.ready\`);
124
+ const originalReadFileSync = fs.readFileSync.bind(fs);
125
+ const sleepBuffer = new Int32Array(new SharedArrayBuffer(4));
126
+ let paused = false;
127
+
128
+ fs.readFileSync = function patchedReadFileSync(filePath, ...args) {
129
+ const result = originalReadFileSync(filePath, ...args);
130
+ if (!paused && path.resolve(String(filePath)) === manifestPath) {
131
+ paused = true;
132
+ fs.writeFileSync(readyPath, "ready\\n");
133
+ const deadline = Date.now() + 10_000;
134
+ while (!fs.existsSync(goPath)) {
135
+ if (Date.now() > deadline) {
136
+ throw new Error(\`Timed out waiting for manifest race release: \${goPath}\`);
137
+ }
138
+ Atomics.wait(sleepBuffer, 0, 0, 20);
139
+ }
140
+ }
141
+ return result;
142
+ };
143
+
144
+ const { createSpace } = await import(pathToFileURL(spacesSourcePath).href);
145
+ createSpace({ baseDir, name, kind: "project" });
146
+ `,
147
+ "utf8"
148
+ );
149
+
150
+ try {
151
+ loadManifest(baseDir);
152
+
153
+ const first = runSpaceChild(baseDir, "Alpha", manifestPath, goPath, childScript);
154
+ const second = runSpaceChild(baseDir, "Beta", manifestPath, goPath, childScript);
155
+
156
+ await waitForAnyFile([path.join(baseDir, "Alpha.ready"), path.join(baseDir, "Beta.ready")]);
157
+ await sleep(300);
158
+ await writeFile(goPath, "go\n", "utf8");
159
+ await Promise.all([first, second]);
160
+
161
+ const manifest = loadManifest(baseDir);
162
+ const ids = manifest.spaces.map((space) => space.id).sort();
163
+ assert.ok(ids.includes("alpha"), `expected alpha in ${ids.join(", ")}`);
164
+ assert.ok(ids.includes("beta"), `expected beta in ${ids.join(", ")}`);
165
+ } finally {
166
+ await rm(baseDir, { recursive: true, force: true });
167
+ }
168
+ });
169
+
170
+ test("createSpace reclaims stale manifest locks only after the owner exits", async () => {
171
+ const baseDir = await mkdtemp(path.join(os.tmpdir(), "remnic-spaces-stale-lock-"));
172
+ const manifestPath = path.join(baseDir, ".config", "engram", "spaces", "manifest.json");
173
+ const lockDir = `${manifestPath}.lock`;
174
+ const staleTime = new Date(Date.now() - 31_000);
175
+
176
+ try {
177
+ loadManifest(baseDir);
178
+ await mkdir(lockDir);
179
+ await writeFile(path.join(lockDir, "owner"), "999999:stale-owner\n", "utf8");
180
+ await utimes(lockDir, staleTime, staleTime);
181
+
182
+ createSpace({ baseDir, name: "Recovered", kind: "project" });
183
+
184
+ const manifest = loadManifest(baseDir);
185
+ assert.ok(manifest.spaces.some((space) => space.id === "recovered"));
186
+ assert.equal(existsSync(lockDir), false);
187
+ assert.equal(existsSync(`${lockDir}.reclaim`), false);
188
+ } finally {
189
+ await rm(baseDir, { recursive: true, force: true });
190
+ }
191
+ });
192
+
193
+ test("createSpace reclaims stale manifest reclaim locks", async () => {
194
+ const baseDir = await mkdtemp(path.join(os.tmpdir(), "remnic-spaces-stale-reclaim-lock-"));
195
+ const manifestPath = path.join(baseDir, ".config", "engram", "spaces", "manifest.json");
196
+ const lockDir = `${manifestPath}.lock`;
197
+ const reclaimDir = `${lockDir}.reclaim`;
198
+ const staleTime = new Date(Date.now() - 31_000);
199
+
200
+ try {
201
+ loadManifest(baseDir);
202
+ await mkdir(reclaimDir);
203
+ await utimes(reclaimDir, staleTime, staleTime);
204
+
205
+ createSpace({ baseDir, name: "Recovered", kind: "project" });
206
+
207
+ const manifest = loadManifest(baseDir);
208
+ assert.ok(manifest.spaces.some((space) => space.id === "recovered"));
209
+ assert.equal(existsSync(reclaimDir), false);
210
+ } finally {
211
+ await rm(baseDir, { recursive: true, force: true });
212
+ }
213
+ });
214
+
215
+ test("loadManifest persists bootstrap when manifest disappears between existence check and read", async () => {
216
+ const baseDir = await mkdtemp(path.join(os.tmpdir(), "remnic-spaces-vanishing-manifest-"));
217
+ const manifestPath = path.join(baseDir, ".config", "engram", "spaces", "manifest.json");
218
+ const originalExistsSync = fs.existsSync.bind(fs);
219
+ let forceInitialExists = true;
220
+
221
+ fs.existsSync = ((filePath) => {
222
+ if (forceInitialExists && path.resolve(String(filePath)) === manifestPath) {
223
+ forceInitialExists = false;
224
+ return true;
225
+ }
226
+ return originalExistsSync(filePath);
227
+ }) as typeof fs.existsSync;
228
+
229
+ try {
230
+ const manifest = loadManifest(baseDir);
231
+
232
+ assert.equal(manifest.activeSpaceId, "personal");
233
+ assert.equal(manifest.spaces[0]?.id, "personal");
234
+ assert.equal(originalExistsSync(manifestPath), true);
235
+ } finally {
236
+ fs.existsSync = originalExistsSync;
237
+ await rm(baseDir, { recursive: true, force: true });
238
+ }
239
+ });
240
+
241
+ async function runSpaceChild(
242
+ baseDir: string,
243
+ name: string,
244
+ manifestPath: string,
245
+ goPath: string,
246
+ childScript: string
247
+ ): Promise<void> {
248
+ const child = spawn(process.execPath, [tsxCli, childScript, baseDir, name, manifestPath, goPath, spacesSourcePath], {
249
+ cwd: repoRoot,
250
+ env: process.env,
251
+ stdio: ["ignore", "pipe", "pipe"],
252
+ });
253
+
254
+ let stdout = "";
255
+ let stderr = "";
256
+ child.stdout.setEncoding("utf8");
257
+ child.stderr.setEncoding("utf8");
258
+ child.stdout.on("data", (chunk) => {
259
+ stdout += chunk;
260
+ });
261
+ child.stderr.on("data", (chunk) => {
262
+ stderr += chunk;
263
+ });
264
+
265
+ const code = await new Promise<number | null>((resolve, reject) => {
266
+ child.on("error", reject);
267
+ child.on("close", resolve);
268
+ });
269
+ assert.equal(code, 0, `child ${name} failed\nstdout:\n${stdout}\nstderr:\n${stderr}`);
270
+ }
271
+
272
+ async function waitForAnyFile(filePaths: string[]): Promise<void> {
273
+ const deadline = Date.now() + 5_000;
274
+ while (!filePaths.some((filePath) => existsSync(filePath))) {
275
+ if (Date.now() > deadline) {
276
+ throw new Error(`Timed out waiting for one of: ${filePaths.join(", ")}`);
277
+ }
278
+ await sleep(20);
279
+ }
280
+ }