@remnic/core 1.1.22 → 1.1.24
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/access-cli.js +15 -15
- package/dist/access-http.d.ts +9 -1
- package/dist/access-http.js +9 -9
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +8 -8
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-DT9L2DW4.d.ts → access-service-CEyV8XJ5.d.ts} +19 -2
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +6 -6
- package/dist/briefing.js +3 -3
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-YO3AZEE5.js → chunk-25YQM6XW.js} +3 -3
- package/dist/{chunk-LDJANWTK.js → chunk-2DM72JF3.js} +12 -12
- package/dist/{chunk-TLM762GT.js → chunk-2WIPXV3Y.js} +2 -2
- package/dist/{chunk-QOHBYVZG.js → chunk-3F24QTRI.js} +2 -2
- package/dist/{chunk-5IQC4OG6.js → chunk-4H6DURG6.js} +2 -2
- package/dist/{chunk-26OQECWH.js → chunk-6CB4E7ZV.js} +4 -4
- package/dist/{chunk-NOQ74SJN.js → chunk-7D6O46PF.js} +2 -2
- package/dist/{chunk-FF46Q3SN.js → chunk-AMVN77EU.js} +360 -32
- package/dist/chunk-AMVN77EU.js.map +1 -0
- package/dist/{chunk-7Q2P774N.js → chunk-F33CJ5CH.js} +13 -3
- package/dist/chunk-F33CJ5CH.js.map +1 -0
- package/dist/{chunk-FSODDMR2.js → chunk-IANK6Y5W.js} +2 -2
- package/dist/{chunk-UA6OCL6S.js → chunk-JUYT2J3K.js} +106 -11
- package/dist/chunk-JUYT2J3K.js.map +1 -0
- package/dist/{chunk-NGPO6S3M.js → chunk-LCTP7YRU.js} +42 -5
- package/dist/chunk-LCTP7YRU.js.map +1 -0
- package/dist/{chunk-GGCJ253V.js → chunk-MVAOT247.js} +8 -8
- package/dist/{chunk-SH5S7XYD.js → chunk-MXFBBHJU.js} +72 -2
- package/dist/chunk-MXFBBHJU.js.map +1 -0
- package/dist/{chunk-VMQRBXJ5.js → chunk-NW7JW5GA.js} +2 -2
- package/dist/{chunk-SZKCBLS5.js → chunk-PUXCIHRL.js} +2 -2
- package/dist/{chunk-2IRT26RZ.js → chunk-QYHQ2JHL.js} +2 -2
- package/dist/{chunk-CN4P6SVA.js → chunk-RCZRL5BE.js} +2 -2
- package/dist/{chunk-SGIXDVSF.js → chunk-S27EXIHY.js} +2 -2
- package/dist/{chunk-5ML4TH3E.js → chunk-TFORLO3O.js} +4 -4
- package/dist/{chunk-TOFUTKQN.js → chunk-TR4DK5OH.js} +2 -2
- package/dist/{chunk-6ORWKANA.js → chunk-VYU7PXUS.js} +2 -2
- package/dist/{chunk-FFU4GMST.js → chunk-WNARATI3.js} +2 -2
- package/dist/{chunk-KSFBM6TV.js → chunk-YITUHONZ.js} +2 -2
- package/dist/{cli-BN0CkYzI.d.ts → cli-BguVmIwO.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +18 -18
- package/dist/compounding/engine.js +3 -3
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/index.js +3 -3
- package/dist/entity-retrieval.js +3 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +30 -24
- package/dist/index.js.map +1 -1
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/storage.js +3 -3
- package/dist/offline-sync.d.ts +38 -1
- package/dist/offline-sync.js +8 -2
- package/dist/operator-toolkit.js +6 -6
- package/dist/orchestrator.js +11 -11
- package/dist/schemas.d.ts +22 -22
- package/dist/secure-store/index.js +2 -2
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +2 -2
- package/dist/transfer/types.d.ts +12 -12
- package/dist/verified-recall.js +3 -3
- package/package.json +1 -1
- package/src/access-http.test.ts +239 -0
- package/src/access-http.ts +128 -7
- package/src/access-service-offline-file-content.test.ts +37 -0
- package/src/access-service.ts +70 -0
- package/src/index.ts +4 -0
- package/src/offline-sync.test.ts +395 -79
- package/src/offline-sync.ts +473 -32
- package/src/secure-store/secure-fs.ts +84 -3
- package/src/storage.ts +12 -0
- package/dist/chunk-7Q2P774N.js.map +0 -1
- package/dist/chunk-FF46Q3SN.js.map +0 -1
- package/dist/chunk-NGPO6S3M.js.map +0 -1
- package/dist/chunk-SH5S7XYD.js.map +0 -1
- package/dist/chunk-UA6OCL6S.js.map +0 -1
- /package/dist/{chunk-YO3AZEE5.js.map → chunk-25YQM6XW.js.map} +0 -0
- /package/dist/{chunk-LDJANWTK.js.map → chunk-2DM72JF3.js.map} +0 -0
- /package/dist/{chunk-TLM762GT.js.map → chunk-2WIPXV3Y.js.map} +0 -0
- /package/dist/{chunk-QOHBYVZG.js.map → chunk-3F24QTRI.js.map} +0 -0
- /package/dist/{chunk-5IQC4OG6.js.map → chunk-4H6DURG6.js.map} +0 -0
- /package/dist/{chunk-26OQECWH.js.map → chunk-6CB4E7ZV.js.map} +0 -0
- /package/dist/{chunk-NOQ74SJN.js.map → chunk-7D6O46PF.js.map} +0 -0
- /package/dist/{chunk-FSODDMR2.js.map → chunk-IANK6Y5W.js.map} +0 -0
- /package/dist/{chunk-GGCJ253V.js.map → chunk-MVAOT247.js.map} +0 -0
- /package/dist/{chunk-VMQRBXJ5.js.map → chunk-NW7JW5GA.js.map} +0 -0
- /package/dist/{chunk-SZKCBLS5.js.map → chunk-PUXCIHRL.js.map} +0 -0
- /package/dist/{chunk-2IRT26RZ.js.map → chunk-QYHQ2JHL.js.map} +0 -0
- /package/dist/{chunk-CN4P6SVA.js.map → chunk-RCZRL5BE.js.map} +0 -0
- /package/dist/{chunk-SGIXDVSF.js.map → chunk-S27EXIHY.js.map} +0 -0
- /package/dist/{chunk-5ML4TH3E.js.map → chunk-TFORLO3O.js.map} +0 -0
- /package/dist/{chunk-TOFUTKQN.js.map → chunk-TR4DK5OH.js.map} +0 -0
- /package/dist/{chunk-6ORWKANA.js.map → chunk-VYU7PXUS.js.map} +0 -0
- /package/dist/{chunk-FFU4GMST.js.map → chunk-WNARATI3.js.map} +0 -0
- /package/dist/{chunk-KSFBM6TV.js.map → chunk-YITUHONZ.js.map} +0 -0
package/src/offline-sync.test.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, utimes, writeFile } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
applyOfflineSyncFileContentChunk,
|
|
9
10
|
applyOfflineSyncChangeset,
|
|
10
11
|
applyOfflineSyncSnapshot,
|
|
11
12
|
buildOfflineSyncChangeset,
|
|
@@ -58,7 +59,21 @@ test("offline snapshot captures source-of-truth files and excludes private/inter
|
|
|
58
59
|
|
|
59
60
|
assert.deepEqual(
|
|
60
61
|
snapshot.files.map((file) => file.path),
|
|
61
|
-
[
|
|
62
|
+
[
|
|
63
|
+
"assets/blob.bin",
|
|
64
|
+
"facts/a.md",
|
|
65
|
+
"facts/fact-hashes.txt",
|
|
66
|
+
"state/fact-hashes.ready",
|
|
67
|
+
"state/fact-hashes.txt",
|
|
68
|
+
"state/last_graph_recall.json",
|
|
69
|
+
"state/last_intent.json",
|
|
70
|
+
"state/last_qmd_recall.json",
|
|
71
|
+
"state/last_recall.json",
|
|
72
|
+
"state/lcm.sqlite",
|
|
73
|
+
"state/lcm.sqlite-shm",
|
|
74
|
+
"state/lcm.sqlite-wal",
|
|
75
|
+
"transcripts/session.jsonl",
|
|
76
|
+
],
|
|
62
77
|
);
|
|
63
78
|
const binary = snapshot.files.find((file) => file.path === "assets/blob.bin");
|
|
64
79
|
assert.equal(Buffer.from(binary?.contentBase64 ?? "", "base64")[3], 255);
|
|
@@ -71,14 +86,27 @@ test("offline snapshot captures source-of-truth files and excludes private/inter
|
|
|
71
86
|
});
|
|
72
87
|
assert.deepEqual(
|
|
73
88
|
withoutTranscripts.files.map((file) => file.path),
|
|
74
|
-
[
|
|
89
|
+
[
|
|
90
|
+
"assets/blob.bin",
|
|
91
|
+
"facts/a.md",
|
|
92
|
+
"facts/fact-hashes.txt",
|
|
93
|
+
"state/fact-hashes.ready",
|
|
94
|
+
"state/fact-hashes.txt",
|
|
95
|
+
"state/last_graph_recall.json",
|
|
96
|
+
"state/last_intent.json",
|
|
97
|
+
"state/last_qmd_recall.json",
|
|
98
|
+
"state/last_recall.json",
|
|
99
|
+
"state/lcm.sqlite",
|
|
100
|
+
"state/lcm.sqlite-shm",
|
|
101
|
+
"state/lcm.sqlite-wal",
|
|
102
|
+
],
|
|
75
103
|
);
|
|
76
104
|
} finally {
|
|
77
105
|
await rm(root, { recursive: true, force: true });
|
|
78
106
|
}
|
|
79
107
|
});
|
|
80
108
|
|
|
81
|
-
test("offline sync
|
|
109
|
+
test("offline sync includes retrieval debug snapshots for full-fidelity offline recall", async () => {
|
|
82
110
|
const root = await tempDir("remnic-offline-debug-snapshots");
|
|
83
111
|
try {
|
|
84
112
|
await write(root, "facts/a.md", "alpha");
|
|
@@ -93,25 +121,25 @@ test("offline sync excludes volatile retrieval debug snapshots without deleting
|
|
|
93
121
|
includeContent: true,
|
|
94
122
|
});
|
|
95
123
|
|
|
96
|
-
assert.deepEqual(snapshot.files.map((file) => file.path), [
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
);
|
|
124
|
+
assert.deepEqual(snapshot.files.map((file) => file.path), [
|
|
125
|
+
"facts/a.md",
|
|
126
|
+
"state/last_graph_recall.json",
|
|
127
|
+
"state/last_intent.json",
|
|
128
|
+
"state/last_qmd_recall.json",
|
|
129
|
+
"state/last_recall.json",
|
|
130
|
+
]);
|
|
131
|
+
const focused = await buildOfflineSyncSnapshotForPaths({
|
|
132
|
+
root,
|
|
133
|
+
sourceId: "remote",
|
|
134
|
+
paths: ["state/last_graph_recall.json"],
|
|
135
|
+
includeContent: true,
|
|
136
|
+
});
|
|
137
|
+
assert.deepEqual(focused.files.map((file) => file.path), ["state/last_graph_recall.json"]);
|
|
138
|
+
const chunk = await readOfflineSyncFileContentChunk({
|
|
139
|
+
root,
|
|
140
|
+
path: "state/last_graph_recall.json",
|
|
141
|
+
});
|
|
142
|
+
assert.equal(chunk.content.toString("utf-8"), "graph");
|
|
115
143
|
|
|
116
144
|
const oldGraph = Buffer.from("old graph");
|
|
117
145
|
const pull = await applyOfflineSyncSnapshot({
|
|
@@ -125,14 +153,14 @@ test("offline sync excludes volatile retrieval debug snapshots without deleting
|
|
|
125
153
|
}],
|
|
126
154
|
});
|
|
127
155
|
|
|
128
|
-
assert.equal(pull.
|
|
156
|
+
assert.equal(pull.skipped, 5);
|
|
129
157
|
assert.equal(await readUtf8(root, "state/last_graph_recall.json"), "graph");
|
|
130
158
|
} finally {
|
|
131
159
|
await rm(root, { recursive: true, force: true });
|
|
132
160
|
}
|
|
133
161
|
});
|
|
134
162
|
|
|
135
|
-
test("offline sync
|
|
163
|
+
test("offline sync includes live LCM sqlite artifacts for full-fidelity offline mode", async () => {
|
|
136
164
|
const root = await tempDir("remnic-offline-lcm-sqlite");
|
|
137
165
|
try {
|
|
138
166
|
await write(root, "facts/a.md", "alpha");
|
|
@@ -146,17 +174,19 @@ test("offline sync excludes live LCM sqlite artifacts without deleting existing
|
|
|
146
174
|
includeContent: true,
|
|
147
175
|
});
|
|
148
176
|
|
|
149
|
-
assert.deepEqual(snapshot.files.map((file) => file.path), [
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
177
|
+
assert.deepEqual(snapshot.files.map((file) => file.path), [
|
|
178
|
+
"facts/a.md",
|
|
179
|
+
"state/lcm.sqlite",
|
|
180
|
+
"state/lcm.sqlite-shm",
|
|
181
|
+
"state/lcm.sqlite-wal",
|
|
182
|
+
]);
|
|
183
|
+
const focused = await buildOfflineSyncSnapshotForPaths({
|
|
184
|
+
root,
|
|
185
|
+
sourceId: "remote",
|
|
186
|
+
paths: ["state/lcm.sqlite"],
|
|
187
|
+
includeContent: true,
|
|
188
|
+
});
|
|
189
|
+
assert.deepEqual(focused.files.map((file) => file.path), ["state/lcm.sqlite"]);
|
|
160
190
|
|
|
161
191
|
const oldDb = Buffer.from("old live db");
|
|
162
192
|
const pull = await applyOfflineSyncSnapshot({
|
|
@@ -170,14 +200,14 @@ test("offline sync excludes live LCM sqlite artifacts without deleting existing
|
|
|
170
200
|
}],
|
|
171
201
|
});
|
|
172
202
|
|
|
173
|
-
assert.equal(pull.
|
|
203
|
+
assert.equal(pull.skipped, 4);
|
|
174
204
|
assert.equal(await readUtf8(root, "state/lcm.sqlite"), "live db");
|
|
175
205
|
} finally {
|
|
176
206
|
await rm(root, { recursive: true, force: true });
|
|
177
207
|
}
|
|
178
208
|
});
|
|
179
209
|
|
|
180
|
-
test("offline sync
|
|
210
|
+
test("offline sync includes durable runtime state and excludes only transient sync temp files", async () => {
|
|
181
211
|
const root = await tempDir("remnic-offline-runtime-state");
|
|
182
212
|
try {
|
|
183
213
|
await write(root, "facts/a.md", "alpha");
|
|
@@ -209,17 +239,30 @@ test("offline sync excludes runtime-derived state without deleting existing loca
|
|
|
209
239
|
assert.deepEqual(snapshot.files.map((file) => file.path), [
|
|
210
240
|
"assets/state/fact-hashes.txt",
|
|
211
241
|
"facts/a.md",
|
|
242
|
+
"namespaces/generalist-project-origin-6ebeaa54/state/.memory-status-version.log",
|
|
243
|
+
"namespaces/generalist-project-origin-6ebeaa54/state/entity-mention-index.json",
|
|
244
|
+
"namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json",
|
|
245
|
+
"state/.artifact-write-version.log",
|
|
246
|
+
"state/.memory-status-version.log",
|
|
247
|
+
"state/buffer-surprise-ledger.jsonl",
|
|
248
|
+
"state/buffer.json",
|
|
249
|
+
"state/embeddings.json",
|
|
250
|
+
"state/entity-mention-index.json",
|
|
251
|
+
"state/index_tags.json",
|
|
252
|
+
"state/index_time.json",
|
|
253
|
+
"state/memory-lifecycle-ledger.jsonl",
|
|
254
|
+
"state/memory-projection.sqlite",
|
|
255
|
+
"state/memory-projection.sqlite-shm",
|
|
256
|
+
"state/memory-projection.sqlite-wal",
|
|
257
|
+
"state/recall_impressions.jsonl",
|
|
212
258
|
]);
|
|
213
|
-
await
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}),
|
|
221
|
-
/offline sync snapshot path is excluded: state\/memory-lifecycle-ledger\.jsonl/,
|
|
222
|
-
);
|
|
259
|
+
const focused = await buildOfflineSyncSnapshotForPaths({
|
|
260
|
+
root,
|
|
261
|
+
sourceId: "remote",
|
|
262
|
+
paths: ["state/memory-lifecycle-ledger.jsonl"],
|
|
263
|
+
includeContent: true,
|
|
264
|
+
});
|
|
265
|
+
assert.deepEqual(focused.files.map((file) => file.path), ["state/memory-lifecycle-ledger.jsonl"]);
|
|
223
266
|
await assert.rejects(
|
|
224
267
|
() =>
|
|
225
268
|
readOfflineSyncFileContentChunk({
|
|
@@ -228,16 +271,15 @@ test("offline sync excludes runtime-derived state without deleting existing loca
|
|
|
228
271
|
}),
|
|
229
272
|
/offline sync file content path is excluded: state\/buffer\.json\.tmp-123-456/,
|
|
230
273
|
);
|
|
231
|
-
await
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
);
|
|
274
|
+
const namespaced = await buildOfflineSyncSnapshotForPaths({
|
|
275
|
+
root,
|
|
276
|
+
sourceId: "remote",
|
|
277
|
+
paths: ["namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json"],
|
|
278
|
+
includeContent: true,
|
|
279
|
+
});
|
|
280
|
+
assert.deepEqual(namespaced.files.map((file) => file.path), [
|
|
281
|
+
"namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json",
|
|
282
|
+
]);
|
|
241
283
|
|
|
242
284
|
const oldLedger = Buffer.from("old ledger");
|
|
243
285
|
const pull = await applyOfflineSyncSnapshot({
|
|
@@ -251,14 +293,14 @@ test("offline sync excludes runtime-derived state without deleting existing loca
|
|
|
251
293
|
}],
|
|
252
294
|
});
|
|
253
295
|
|
|
254
|
-
assert.equal(pull.
|
|
296
|
+
assert.equal(pull.skipped, 18);
|
|
255
297
|
assert.equal(await readUtf8(root, "state/memory-lifecycle-ledger.jsonl"), "ledger");
|
|
256
298
|
} finally {
|
|
257
299
|
await rm(root, { recursive: true, force: true });
|
|
258
300
|
}
|
|
259
301
|
});
|
|
260
302
|
|
|
261
|
-
test("offline sync
|
|
303
|
+
test("offline sync accepts durable runtime records from older peers", async () => {
|
|
262
304
|
const root = await tempDir("remnic-offline-legacy-runtime-state");
|
|
263
305
|
try {
|
|
264
306
|
const fact = Buffer.from("alpha");
|
|
@@ -284,6 +326,13 @@ test("offline sync ignores runtime-derived records from older peers", async () =
|
|
|
284
326
|
mtimeMs: 0,
|
|
285
327
|
contentBase64: runtime.toString("base64"),
|
|
286
328
|
},
|
|
329
|
+
{
|
|
330
|
+
path: "state/buffer.json.tmp-123-456",
|
|
331
|
+
sha256: runtimeSha,
|
|
332
|
+
bytes: runtime.byteLength,
|
|
333
|
+
mtimeMs: 0,
|
|
334
|
+
contentBase64: runtime.toString("base64"),
|
|
335
|
+
},
|
|
287
336
|
{
|
|
288
337
|
path: "namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json",
|
|
289
338
|
sha256: runtimeSha,
|
|
@@ -309,15 +358,16 @@ test("offline sync ignores runtime-derived records from older peers", async () =
|
|
|
309
358
|
},
|
|
310
359
|
});
|
|
311
360
|
|
|
312
|
-
assert.equal(pull.upserted,
|
|
361
|
+
assert.equal(pull.upserted, 4);
|
|
313
362
|
assert.equal(await readUtf8(root, "facts/a.md"), "alpha");
|
|
314
363
|
assert.equal(await readUtf8(root, "assets/state/fact-hashes.txt"), "durable asset");
|
|
315
|
-
await
|
|
316
|
-
|
|
317
|
-
/
|
|
364
|
+
assert.equal(await readUtf8(root, "state/buffer.json"), "legacy runtime");
|
|
365
|
+
assert.equal(
|
|
366
|
+
await readUtf8(root, "namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json"),
|
|
367
|
+
"legacy runtime",
|
|
318
368
|
);
|
|
319
369
|
await assert.rejects(
|
|
320
|
-
() => readFile(path.join(root, "
|
|
370
|
+
() => readFile(path.join(root, "state", "buffer.json.tmp-123-456")),
|
|
321
371
|
/ENOENT/,
|
|
322
372
|
);
|
|
323
373
|
|
|
@@ -343,6 +393,17 @@ test("offline sync ignores runtime-derived records from older peers", async () =
|
|
|
343
393
|
contentBase64: runtime.toString("base64"),
|
|
344
394
|
},
|
|
345
395
|
},
|
|
396
|
+
{
|
|
397
|
+
type: "upsert",
|
|
398
|
+
path: "state/buffer.json.tmp-123-456",
|
|
399
|
+
file: {
|
|
400
|
+
path: "state/buffer.json.tmp-123-456",
|
|
401
|
+
sha256: runtimeSha,
|
|
402
|
+
bytes: runtime.byteLength,
|
|
403
|
+
mtimeMs: 0,
|
|
404
|
+
contentBase64: runtime.toString("base64"),
|
|
405
|
+
},
|
|
406
|
+
},
|
|
346
407
|
{
|
|
347
408
|
type: "upsert",
|
|
348
409
|
path: "namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json",
|
|
@@ -380,15 +441,19 @@ test("offline sync ignores runtime-derived records from older peers", async () =
|
|
|
380
441
|
},
|
|
381
442
|
});
|
|
382
443
|
|
|
383
|
-
assert.equal(push.appliedUpserts,
|
|
444
|
+
assert.equal(push.appliedUpserts, 4);
|
|
384
445
|
assert.equal(await readUtf8(remote, "facts/a.md"), "alpha");
|
|
385
446
|
assert.equal(await readUtf8(remote, "assets/state/fact-hashes.txt"), "durable asset");
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
447
|
+
assert.equal(
|
|
448
|
+
await readUtf8(remote, "state/memory-lifecycle-ledger.jsonl"),
|
|
449
|
+
"legacy runtime",
|
|
450
|
+
);
|
|
451
|
+
assert.equal(
|
|
452
|
+
await readUtf8(remote, "namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json"),
|
|
453
|
+
"legacy runtime",
|
|
389
454
|
);
|
|
390
455
|
await assert.rejects(
|
|
391
|
-
() => readFile(path.join(remote, "
|
|
456
|
+
() => readFile(path.join(remote, "state", "buffer.json.tmp-123-456")),
|
|
392
457
|
/ENOENT/,
|
|
393
458
|
);
|
|
394
459
|
} finally {
|
|
@@ -426,6 +491,7 @@ test("offline sync reads bounded file content chunks with metadata", async () =>
|
|
|
426
491
|
const root = await tempDir("remnic-offline-file-content");
|
|
427
492
|
try {
|
|
428
493
|
await write(root, "artifacts/large.txt", "alpha\nbeta\ngamma\n");
|
|
494
|
+
await write(root, "state/lcm.sqlite", "live db");
|
|
429
495
|
|
|
430
496
|
const chunk = await readOfflineSyncFileContentChunk({
|
|
431
497
|
root,
|
|
@@ -440,14 +506,199 @@ test("offline sync reads bounded file content chunks with metadata", async () =>
|
|
|
440
506
|
assert.equal(chunk.content.toString("utf-8"), "beta\n");
|
|
441
507
|
assert.equal(chunk.bytes, Buffer.byteLength("alpha\nbeta\ngamma\n"));
|
|
442
508
|
|
|
443
|
-
await
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
509
|
+
const lcm = await readOfflineSyncFileContentChunk({
|
|
510
|
+
root,
|
|
511
|
+
path: "state/lcm.sqlite",
|
|
512
|
+
});
|
|
513
|
+
assert.equal(lcm.content.toString("utf-8"), "live db");
|
|
514
|
+
} finally {
|
|
515
|
+
await rm(root, { recursive: true, force: true });
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test("offline sync applies chunked file content with base conflict checks", async () => {
|
|
520
|
+
const root = await tempDir("remnic-offline-file-content-apply");
|
|
521
|
+
try {
|
|
522
|
+
await write(root, "state/lcm.sqlite", "old");
|
|
523
|
+
const oldSha = createHash("sha256").update("old").digest("hex");
|
|
524
|
+
const next = Buffer.from("new durable sqlite content");
|
|
525
|
+
const nextSha = createHash("sha256").update(next).digest("hex");
|
|
526
|
+
|
|
527
|
+
const first = await applyOfflineSyncFileContentChunk({
|
|
528
|
+
root,
|
|
529
|
+
sourceId: "laptop",
|
|
530
|
+
path: "state/lcm.sqlite",
|
|
531
|
+
sha256: nextSha,
|
|
532
|
+
bytes: next.byteLength,
|
|
533
|
+
mtimeMs: 123,
|
|
534
|
+
offset: 0,
|
|
535
|
+
baseSha256: oldSha,
|
|
536
|
+
content: next.subarray(0, 8),
|
|
537
|
+
});
|
|
538
|
+
assert.equal(first.done, false);
|
|
539
|
+
|
|
540
|
+
const second = await applyOfflineSyncFileContentChunk({
|
|
541
|
+
root,
|
|
542
|
+
sourceId: "laptop",
|
|
543
|
+
path: "state/lcm.sqlite",
|
|
544
|
+
sha256: nextSha,
|
|
545
|
+
bytes: next.byteLength,
|
|
546
|
+
mtimeMs: 123,
|
|
547
|
+
offset: 8,
|
|
548
|
+
baseSha256: oldSha,
|
|
549
|
+
content: next.subarray(8),
|
|
550
|
+
});
|
|
551
|
+
assert.equal(second.done, true);
|
|
552
|
+
assert.equal(second.applied, true);
|
|
553
|
+
assert.equal(await readUtf8(root, "state/lcm.sqlite"), "new durable sqlite content");
|
|
554
|
+
|
|
555
|
+
const conflictContent = Buffer.from("conflicting local sqlite");
|
|
556
|
+
const conflictSha = createHash("sha256").update(conflictContent).digest("hex");
|
|
557
|
+
const conflict = await applyOfflineSyncFileContentChunk({
|
|
558
|
+
root,
|
|
559
|
+
sourceId: "laptop",
|
|
560
|
+
path: "state/lcm.sqlite",
|
|
561
|
+
sha256: conflictSha,
|
|
562
|
+
bytes: conflictContent.byteLength,
|
|
563
|
+
mtimeMs: 456,
|
|
564
|
+
offset: 0,
|
|
565
|
+
baseSha256: oldSha,
|
|
566
|
+
content: conflictContent,
|
|
567
|
+
});
|
|
568
|
+
assert.equal(conflict.done, true);
|
|
569
|
+
assert.equal(conflict.applied, false);
|
|
570
|
+
assert.equal(conflict.conflict?.reason, "remote_changed_for_local_update");
|
|
571
|
+
assert.equal(await readUtf8(root, "state/lcm.sqlite"), "new durable sqlite content");
|
|
572
|
+
} finally {
|
|
573
|
+
await rm(root, { recursive: true, force: true });
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test("offline sync stages chunked uploads through storage hooks", async () => {
|
|
578
|
+
const root = await tempDir("remnic-offline-file-content-hooks");
|
|
579
|
+
const encode = (content: Buffer) => Buffer.from(`ENC:${content.toString("base64")}`);
|
|
580
|
+
const decode = (content: Buffer) => {
|
|
581
|
+
const text = content.toString("utf-8");
|
|
582
|
+
return text.startsWith("ENC:") ? Buffer.from(text.slice(4), "base64") : content;
|
|
583
|
+
};
|
|
584
|
+
const readHook = async ({ filePath }: { filePath: string }) => decode(await readFile(filePath));
|
|
585
|
+
let stagingWrites = 0;
|
|
586
|
+
let mutationWrites = 0;
|
|
587
|
+
const writeStagingHook = async ({ filePath, content }: { filePath: string; content: Buffer }) => {
|
|
588
|
+
stagingWrites += 1;
|
|
589
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
590
|
+
await writeFile(filePath, encode(content));
|
|
591
|
+
};
|
|
592
|
+
const writeHook = async ({ filePath, content }: { filePath: string; content: Buffer }) => {
|
|
593
|
+
mutationWrites += 1;
|
|
594
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
595
|
+
await writeFile(filePath, encode(content));
|
|
596
|
+
};
|
|
597
|
+
const writeChunksHook = async ({
|
|
598
|
+
filePath,
|
|
599
|
+
chunks,
|
|
600
|
+
}: {
|
|
601
|
+
filePath: string;
|
|
602
|
+
chunks: AsyncIterable<Buffer>;
|
|
603
|
+
}) => {
|
|
604
|
+
const content: Buffer[] = [];
|
|
605
|
+
for await (const chunk of chunks) content.push(chunk);
|
|
606
|
+
await writeHook({ filePath, content: Buffer.concat(content) });
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
await write(root, "state/lcm.sqlite", "old");
|
|
611
|
+
const oldSha = createHash("sha256").update("old").digest("hex");
|
|
612
|
+
const next = Buffer.from("new durable sqlite content");
|
|
613
|
+
const nextSha = createHash("sha256").update(next).digest("hex");
|
|
614
|
+
|
|
615
|
+
const first = await applyOfflineSyncFileContentChunk({
|
|
616
|
+
root,
|
|
617
|
+
sourceId: "laptop",
|
|
618
|
+
path: "state/lcm.sqlite",
|
|
619
|
+
sha256: nextSha,
|
|
620
|
+
bytes: next.byteLength,
|
|
621
|
+
mtimeMs: 123,
|
|
622
|
+
offset: 0,
|
|
623
|
+
baseSha256: oldSha,
|
|
624
|
+
content: next.subarray(0, 8),
|
|
625
|
+
readFile: readHook,
|
|
626
|
+
writeFile: writeHook,
|
|
627
|
+
writeStagingFile: writeStagingHook,
|
|
628
|
+
writeFileChunks: writeChunksHook,
|
|
629
|
+
});
|
|
630
|
+
assert.equal(first.done, false);
|
|
631
|
+
assert.equal(stagingWrites, 1);
|
|
632
|
+
assert.equal(mutationWrites, 0);
|
|
633
|
+
const uploadEntries = await readdir(path.join(root, ".offline-sync", "uploads"));
|
|
634
|
+
assert.equal(uploadEntries.length, 1);
|
|
635
|
+
const uploadChunkEntries = await readdir(path.join(root, ".offline-sync", "uploads", uploadEntries[0]));
|
|
636
|
+
assert.deepEqual(uploadChunkEntries, ["00000000000000000000.part"]);
|
|
637
|
+
const rawUpload = await readFile(path.join(
|
|
638
|
+
root,
|
|
639
|
+
".offline-sync",
|
|
640
|
+
"uploads",
|
|
641
|
+
uploadEntries[0],
|
|
642
|
+
uploadChunkEntries[0],
|
|
643
|
+
));
|
|
644
|
+
assert.match(rawUpload.toString("utf-8"), /^ENC:/);
|
|
645
|
+
assert.equal(rawUpload.includes(next.subarray(0, 8)), false);
|
|
646
|
+
|
|
647
|
+
const second = await applyOfflineSyncFileContentChunk({
|
|
648
|
+
root,
|
|
649
|
+
sourceId: "laptop",
|
|
650
|
+
path: "state/lcm.sqlite",
|
|
651
|
+
sha256: nextSha,
|
|
652
|
+
bytes: next.byteLength,
|
|
653
|
+
mtimeMs: 123,
|
|
654
|
+
offset: 8,
|
|
655
|
+
baseSha256: oldSha,
|
|
656
|
+
content: next.subarray(8),
|
|
657
|
+
readFile: readHook,
|
|
658
|
+
writeFile: writeHook,
|
|
659
|
+
writeStagingFile: writeStagingHook,
|
|
660
|
+
writeFileChunks: writeChunksHook,
|
|
661
|
+
});
|
|
662
|
+
assert.equal(second.applied, true);
|
|
663
|
+
assert.equal(stagingWrites, 2);
|
|
664
|
+
assert.equal(mutationWrites, 1);
|
|
665
|
+
assert.equal((await readdir(path.join(root, ".offline-sync", "uploads"))).length, 0);
|
|
666
|
+
const rawTarget = await readFile(path.join(root, "state/lcm.sqlite"));
|
|
667
|
+
assert.match(rawTarget.toString("utf-8"), /^ENC:/);
|
|
668
|
+
assert.equal(decode(rawTarget).toString("utf-8"), "new durable sqlite content");
|
|
669
|
+
} finally {
|
|
670
|
+
await rm(root, { recursive: true, force: true });
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
test("offline sync prunes stale staged uploads when starting a new upload", async () => {
|
|
675
|
+
const root = await tempDir("remnic-offline-file-content-prune");
|
|
676
|
+
try {
|
|
677
|
+
const staleKey = `${"a".repeat(64)}.part`;
|
|
678
|
+
const staleDir = path.join(root, ".offline-sync", "uploads", staleKey);
|
|
679
|
+
await mkdir(staleDir, { recursive: true });
|
|
680
|
+
await writeFile(path.join(staleDir, "00000000000000000000.part"), "abandoned");
|
|
681
|
+
const staleTime = new Date(Date.now() - 25 * 60 * 60 * 1000);
|
|
682
|
+
await utimes(path.join(staleDir, "00000000000000000000.part"), staleTime, staleTime);
|
|
683
|
+
await utimes(staleDir, staleTime, staleTime);
|
|
684
|
+
|
|
685
|
+
const next = Buffer.from("new durable sqlite content");
|
|
686
|
+
const nextSha = createHash("sha256").update(next).digest("hex");
|
|
687
|
+
const first = await applyOfflineSyncFileContentChunk({
|
|
688
|
+
root,
|
|
689
|
+
sourceId: "laptop",
|
|
690
|
+
path: "state/lcm.sqlite",
|
|
691
|
+
sha256: nextSha,
|
|
692
|
+
bytes: next.byteLength,
|
|
693
|
+
mtimeMs: 123,
|
|
694
|
+
offset: 0,
|
|
695
|
+
content: next.subarray(0, 8),
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
assert.equal(first.done, false);
|
|
699
|
+
const uploadEntries = await readdir(path.join(root, ".offline-sync", "uploads"));
|
|
700
|
+
assert.equal(uploadEntries.includes(staleKey), false);
|
|
701
|
+
assert.equal(uploadEntries.length, 1);
|
|
451
702
|
} finally {
|
|
452
703
|
await rm(root, { recursive: true, force: true });
|
|
453
704
|
}
|
|
@@ -526,6 +777,32 @@ test("offline changeset only carries content for changed local files", async ()
|
|
|
526
777
|
}
|
|
527
778
|
});
|
|
528
779
|
|
|
780
|
+
test("offline changeset can exclude directly pushed large files without reading their content", async () => {
|
|
781
|
+
const local = await tempDir("remnic-offline-changeset-exclude");
|
|
782
|
+
try {
|
|
783
|
+
await write(local, "state/lcm.sqlite", "before");
|
|
784
|
+
await write(local, "facts/small.md", "before");
|
|
785
|
+
const base = await buildOfflineSyncSnapshot({
|
|
786
|
+
root: local,
|
|
787
|
+
sourceId: "remote",
|
|
788
|
+
includeContent: false,
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
await write(local, "state/lcm.sqlite", "after large");
|
|
792
|
+
await write(local, "facts/small.md", "after small");
|
|
793
|
+
const changeset = await buildOfflineSyncChangeset({
|
|
794
|
+
root: local,
|
|
795
|
+
sourceId: "laptop",
|
|
796
|
+
baseFiles: base.files,
|
|
797
|
+
excludePaths: ["state/lcm.sqlite"],
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
assert.deepEqual(changeset.changes.map((change) => change.path), ["facts/small.md"]);
|
|
801
|
+
} finally {
|
|
802
|
+
await rm(local, { recursive: true, force: true });
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
529
806
|
test("offline pull accepts metadata-only snapshots when files are unchanged", async () => {
|
|
530
807
|
const remote = await tempDir("remnic-offline-metadata-remote");
|
|
531
808
|
const local = await tempDir("remnic-offline-metadata-local");
|
|
@@ -960,6 +1237,45 @@ test("offline sync applies and snapshots through secure storage hooks", async ()
|
|
|
960
1237
|
"secret fact",
|
|
961
1238
|
);
|
|
962
1239
|
assert.equal(snapshot.files[0]?.bytes, Buffer.byteLength("secret fact"));
|
|
1240
|
+
|
|
1241
|
+
const sqlite = Buffer.from("streamed durable sqlite content");
|
|
1242
|
+
const sqliteSha = createHash("sha256").update(sqlite).digest("hex");
|
|
1243
|
+
const first = await applyOfflineSyncFileContentChunk({
|
|
1244
|
+
root,
|
|
1245
|
+
sourceId: "laptop",
|
|
1246
|
+
path: "state/lcm.sqlite",
|
|
1247
|
+
sha256: sqliteSha,
|
|
1248
|
+
bytes: sqlite.byteLength,
|
|
1249
|
+
mtimeMs: 321,
|
|
1250
|
+
offset: 0,
|
|
1251
|
+
content: sqlite.subarray(0, 8),
|
|
1252
|
+
readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
|
|
1253
|
+
writeFile: async ({ filePath, content }) => storage.writeOfflineSyncFile(filePath, content),
|
|
1254
|
+
writeStagingFile: async ({ filePath, content }) => storage.writeOfflineSyncStagingFile(filePath, content),
|
|
1255
|
+
writeFileChunks: async ({ filePath, chunks }) => storage.writeOfflineSyncFileChunks(filePath, chunks),
|
|
1256
|
+
});
|
|
1257
|
+
assert.equal(first.done, false);
|
|
1258
|
+
const second = await applyOfflineSyncFileContentChunk({
|
|
1259
|
+
root,
|
|
1260
|
+
sourceId: "laptop",
|
|
1261
|
+
path: "state/lcm.sqlite",
|
|
1262
|
+
sha256: sqliteSha,
|
|
1263
|
+
bytes: sqlite.byteLength,
|
|
1264
|
+
mtimeMs: 321,
|
|
1265
|
+
offset: 8,
|
|
1266
|
+
content: sqlite.subarray(8),
|
|
1267
|
+
readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
|
|
1268
|
+
writeFile: async ({ filePath, content }) => storage.writeOfflineSyncFile(filePath, content),
|
|
1269
|
+
writeStagingFile: async ({ filePath, content }) => storage.writeOfflineSyncStagingFile(filePath, content),
|
|
1270
|
+
writeFileChunks: async ({ filePath, chunks }) => storage.writeOfflineSyncFileChunks(filePath, chunks),
|
|
1271
|
+
});
|
|
1272
|
+
assert.equal(second.applied, true);
|
|
1273
|
+
const rawSqlite = await readFile(path.join(root, "state", "lcm.sqlite"));
|
|
1274
|
+
assert.equal(isEncryptedFile(rawSqlite), true);
|
|
1275
|
+
assert.equal(
|
|
1276
|
+
(await storage.readOfflineSyncFile(path.join(root, "state", "lcm.sqlite"))).toString("utf8"),
|
|
1277
|
+
"streamed durable sqlite content",
|
|
1278
|
+
);
|
|
963
1279
|
} finally {
|
|
964
1280
|
await rm(root, { recursive: true, force: true });
|
|
965
1281
|
await rm(source, { recursive: true, force: true });
|
|
@@ -984,7 +1300,7 @@ test("offline storage writes invalidate fact hash readiness for rebuild", async
|
|
|
984
1300
|
|
|
985
1301
|
assert.equal(
|
|
986
1302
|
sourceChangeset.changes.some((change) => change.path.startsWith("state/fact-hashes")),
|
|
987
|
-
|
|
1303
|
+
true,
|
|
988
1304
|
);
|
|
989
1305
|
|
|
990
1306
|
const factChangeset = {
|