@remnic/core 1.1.21 → 1.1.23
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-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-NOQ74SJN.js → chunk-7D6O46PF.js} +2 -2
- package/dist/{chunk-SLKSC522.js → chunk-7E7SZRPP.js} +2 -2
- package/dist/{chunk-7Q2P774N.js → chunk-F33CJ5CH.js} +13 -3
- package/dist/chunk-F33CJ5CH.js.map +1 -0
- package/dist/{chunk-APW7AQOJ.js → chunk-FHXVW3L4.js} +4 -4
- package/dist/{chunk-PFFKUJM2.js → chunk-HWF42K6J.js} +103 -4
- package/dist/chunk-HWF42K6J.js.map +1 -0
- package/dist/{chunk-FSODDMR2.js → chunk-IANK6Y5W.js} +2 -2
- package/dist/{chunk-BYYIIXIJ.js → chunk-JKXFF3NT.js} +361 -29
- package/dist/chunk-JKXFF3NT.js.map +1 -0
- package/dist/{chunk-P7F6DJPA.js → chunk-MM5EBZVW.js} +42 -5
- package/dist/chunk-MM5EBZVW.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-SZKCBLS5.js → chunk-PUXCIHRL.js} +2 -2
- package/dist/{chunk-2IRT26RZ.js → chunk-QYHQ2JHL.js} +2 -2
- package/dist/{chunk-73DAPA62.js → chunk-RA73CTVY.js} +12 -12
- 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 +26 -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 +36 -1
- package/dist/offline-sync.js +4 -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 +176 -0
- package/src/access-http.ts +116 -0
- package/src/access-service-offline-file-content.test.ts +37 -0
- package/src/access-service.ts +70 -0
- package/src/index.ts +2 -0
- package/src/offline-sync.test.ts +448 -64
- package/src/offline-sync.ts +477 -29
- 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-BYYIIXIJ.js.map +0 -1
- package/dist/chunk-P7F6DJPA.js.map +0 -1
- package/dist/chunk-PFFKUJM2.js.map +0 -1
- package/dist/chunk-SH5S7XYD.js.map +0 -1
- /package/dist/{chunk-YO3AZEE5.js.map → chunk-25YQM6XW.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-NOQ74SJN.js.map → chunk-7D6O46PF.js.map} +0 -0
- /package/dist/{chunk-SLKSC522.js.map → chunk-7E7SZRPP.js.map} +0 -0
- /package/dist/{chunk-APW7AQOJ.js.map → chunk-FHXVW3L4.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-SZKCBLS5.js.map → chunk-PUXCIHRL.js.map} +0 -0
- /package/dist/{chunk-2IRT26RZ.js.map → chunk-QYHQ2JHL.js.map} +0 -0
- /package/dist/{chunk-73DAPA62.js.map → chunk-RA73CTVY.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,28 +200,35 @@ 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");
|
|
214
|
+
await write(root, "assets/state/fact-hashes.txt", "durable asset");
|
|
184
215
|
await write(root, "state/buffer-surprise-ledger.jsonl", "surprise");
|
|
185
216
|
await write(root, "state/buffer.json", "buffer");
|
|
186
217
|
await write(root, "state/buffer.json.tmp-123-456", "tmp");
|
|
187
218
|
await write(root, "state/embeddings.json", "embeddings");
|
|
219
|
+
await write(root, "state/entity-mention-index.json", "entities");
|
|
188
220
|
await write(root, "state/index_tags.json", "tags");
|
|
189
221
|
await write(root, "state/index_time.json", "time");
|
|
190
222
|
await write(root, "state/memory-lifecycle-ledger.jsonl", "ledger");
|
|
223
|
+
await write(root, "state/.artifact-write-version.log", "version");
|
|
224
|
+
await write(root, "state/.memory-status-version.log", "version");
|
|
191
225
|
await write(root, "state/memory-projection.sqlite", "projection");
|
|
192
226
|
await write(root, "state/memory-projection.sqlite-shm", "projection-shm");
|
|
193
227
|
await write(root, "state/memory-projection.sqlite-wal", "projection-wal");
|
|
194
228
|
await write(root, "state/recall_impressions.jsonl", "impressions");
|
|
229
|
+
await write(root, "namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json", "intent");
|
|
230
|
+
await write(root, "namespaces/generalist-project-origin-6ebeaa54/state/entity-mention-index.json", "entities");
|
|
231
|
+
await write(root, "namespaces/generalist-project-origin-6ebeaa54/state/.memory-status-version.log", "version");
|
|
195
232
|
|
|
196
233
|
const snapshot = await buildOfflineSyncSnapshot({
|
|
197
234
|
root,
|
|
@@ -199,17 +236,33 @@ test("offline sync excludes runtime-derived state without deleting existing loca
|
|
|
199
236
|
includeContent: true,
|
|
200
237
|
});
|
|
201
238
|
|
|
202
|
-
assert.deepEqual(snapshot.files.map((file) => file.path), [
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
/
|
|
212
|
-
|
|
239
|
+
assert.deepEqual(snapshot.files.map((file) => file.path), [
|
|
240
|
+
"assets/state/fact-hashes.txt",
|
|
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",
|
|
258
|
+
]);
|
|
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"]);
|
|
213
266
|
await assert.rejects(
|
|
214
267
|
() =>
|
|
215
268
|
readOfflineSyncFileContentChunk({
|
|
@@ -218,6 +271,15 @@ test("offline sync excludes runtime-derived state without deleting existing loca
|
|
|
218
271
|
}),
|
|
219
272
|
/offline sync file content path is excluded: state\/buffer\.json\.tmp-123-456/,
|
|
220
273
|
);
|
|
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
|
+
]);
|
|
221
283
|
|
|
222
284
|
const oldLedger = Buffer.from("old ledger");
|
|
223
285
|
const pull = await applyOfflineSyncSnapshot({
|
|
@@ -231,20 +293,22 @@ test("offline sync excludes runtime-derived state without deleting existing loca
|
|
|
231
293
|
}],
|
|
232
294
|
});
|
|
233
295
|
|
|
234
|
-
assert.equal(pull.
|
|
296
|
+
assert.equal(pull.skipped, 18);
|
|
235
297
|
assert.equal(await readUtf8(root, "state/memory-lifecycle-ledger.jsonl"), "ledger");
|
|
236
298
|
} finally {
|
|
237
299
|
await rm(root, { recursive: true, force: true });
|
|
238
300
|
}
|
|
239
301
|
});
|
|
240
302
|
|
|
241
|
-
test("offline sync
|
|
303
|
+
test("offline sync accepts durable runtime records from older peers", async () => {
|
|
242
304
|
const root = await tempDir("remnic-offline-legacy-runtime-state");
|
|
243
305
|
try {
|
|
244
306
|
const fact = Buffer.from("alpha");
|
|
245
307
|
const runtime = Buffer.from("legacy runtime");
|
|
308
|
+
const asset = Buffer.from("durable asset");
|
|
246
309
|
const runtimeSha = createHash("sha256").update(runtime).digest("hex");
|
|
247
310
|
const factSha = createHash("sha256").update(fact).digest("hex");
|
|
311
|
+
const assetSha = createHash("sha256").update(asset).digest("hex");
|
|
248
312
|
|
|
249
313
|
const pull = await applyOfflineSyncSnapshot({
|
|
250
314
|
root,
|
|
@@ -262,6 +326,20 @@ test("offline sync ignores runtime-derived records from older peers", async () =
|
|
|
262
326
|
mtimeMs: 0,
|
|
263
327
|
contentBase64: runtime.toString("base64"),
|
|
264
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
|
+
},
|
|
336
|
+
{
|
|
337
|
+
path: "namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json",
|
|
338
|
+
sha256: runtimeSha,
|
|
339
|
+
bytes: runtime.byteLength,
|
|
340
|
+
mtimeMs: 0,
|
|
341
|
+
contentBase64: runtime.toString("base64"),
|
|
342
|
+
},
|
|
265
343
|
{
|
|
266
344
|
path: "facts/a.md",
|
|
267
345
|
sha256: factSha,
|
|
@@ -269,14 +347,27 @@ test("offline sync ignores runtime-derived records from older peers", async () =
|
|
|
269
347
|
mtimeMs: 0,
|
|
270
348
|
contentBase64: fact.toString("base64"),
|
|
271
349
|
},
|
|
350
|
+
{
|
|
351
|
+
path: "assets/state/fact-hashes.txt",
|
|
352
|
+
sha256: assetSha,
|
|
353
|
+
bytes: asset.byteLength,
|
|
354
|
+
mtimeMs: 0,
|
|
355
|
+
contentBase64: asset.toString("base64"),
|
|
356
|
+
},
|
|
272
357
|
],
|
|
273
358
|
},
|
|
274
359
|
});
|
|
275
360
|
|
|
276
|
-
assert.equal(pull.upserted,
|
|
361
|
+
assert.equal(pull.upserted, 4);
|
|
277
362
|
assert.equal(await readUtf8(root, "facts/a.md"), "alpha");
|
|
363
|
+
assert.equal(await readUtf8(root, "assets/state/fact-hashes.txt"), "durable asset");
|
|
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",
|
|
368
|
+
);
|
|
278
369
|
await assert.rejects(
|
|
279
|
-
() => readFile(path.join(root, "state", "buffer.json")),
|
|
370
|
+
() => readFile(path.join(root, "state", "buffer.json.tmp-123-456")),
|
|
280
371
|
/ENOENT/,
|
|
281
372
|
);
|
|
282
373
|
|
|
@@ -302,6 +393,28 @@ test("offline sync ignores runtime-derived records from older peers", async () =
|
|
|
302
393
|
contentBase64: runtime.toString("base64"),
|
|
303
394
|
},
|
|
304
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
|
+
},
|
|
407
|
+
{
|
|
408
|
+
type: "upsert",
|
|
409
|
+
path: "namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json",
|
|
410
|
+
file: {
|
|
411
|
+
path: "namespaces/generalist-project-origin-6ebeaa54/state/last_intent.json",
|
|
412
|
+
sha256: runtimeSha,
|
|
413
|
+
bytes: runtime.byteLength,
|
|
414
|
+
mtimeMs: 0,
|
|
415
|
+
contentBase64: runtime.toString("base64"),
|
|
416
|
+
},
|
|
417
|
+
},
|
|
305
418
|
{
|
|
306
419
|
type: "upsert",
|
|
307
420
|
path: "facts/a.md",
|
|
@@ -313,14 +426,34 @@ test("offline sync ignores runtime-derived records from older peers", async () =
|
|
|
313
426
|
contentBase64: fact.toString("base64"),
|
|
314
427
|
},
|
|
315
428
|
},
|
|
429
|
+
{
|
|
430
|
+
type: "upsert",
|
|
431
|
+
path: "assets/state/fact-hashes.txt",
|
|
432
|
+
file: {
|
|
433
|
+
path: "assets/state/fact-hashes.txt",
|
|
434
|
+
sha256: assetSha,
|
|
435
|
+
bytes: asset.byteLength,
|
|
436
|
+
mtimeMs: 0,
|
|
437
|
+
contentBase64: asset.toString("base64"),
|
|
438
|
+
},
|
|
439
|
+
},
|
|
316
440
|
],
|
|
317
441
|
},
|
|
318
442
|
});
|
|
319
443
|
|
|
320
|
-
assert.equal(push.appliedUpserts,
|
|
444
|
+
assert.equal(push.appliedUpserts, 4);
|
|
321
445
|
assert.equal(await readUtf8(remote, "facts/a.md"), "alpha");
|
|
446
|
+
assert.equal(await readUtf8(remote, "assets/state/fact-hashes.txt"), "durable asset");
|
|
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",
|
|
454
|
+
);
|
|
322
455
|
await assert.rejects(
|
|
323
|
-
() => readFile(path.join(remote, "state", "
|
|
456
|
+
() => readFile(path.join(remote, "state", "buffer.json.tmp-123-456")),
|
|
324
457
|
/ENOENT/,
|
|
325
458
|
);
|
|
326
459
|
} finally {
|
|
@@ -358,6 +491,7 @@ test("offline sync reads bounded file content chunks with metadata", async () =>
|
|
|
358
491
|
const root = await tempDir("remnic-offline-file-content");
|
|
359
492
|
try {
|
|
360
493
|
await write(root, "artifacts/large.txt", "alpha\nbeta\ngamma\n");
|
|
494
|
+
await write(root, "state/lcm.sqlite", "live db");
|
|
361
495
|
|
|
362
496
|
const chunk = await readOfflineSyncFileContentChunk({
|
|
363
497
|
root,
|
|
@@ -372,14 +506,199 @@ test("offline sync reads bounded file content chunks with metadata", async () =>
|
|
|
372
506
|
assert.equal(chunk.content.toString("utf-8"), "beta\n");
|
|
373
507
|
assert.equal(chunk.bytes, Buffer.byteLength("alpha\nbeta\ngamma\n"));
|
|
374
508
|
|
|
375
|
-
await
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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);
|
|
383
702
|
} finally {
|
|
384
703
|
await rm(root, { recursive: true, force: true });
|
|
385
704
|
}
|
|
@@ -458,6 +777,32 @@ test("offline changeset only carries content for changed local files", async ()
|
|
|
458
777
|
}
|
|
459
778
|
});
|
|
460
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
|
+
|
|
461
806
|
test("offline pull accepts metadata-only snapshots when files are unchanged", async () => {
|
|
462
807
|
const remote = await tempDir("remnic-offline-metadata-remote");
|
|
463
808
|
const local = await tempDir("remnic-offline-metadata-local");
|
|
@@ -892,6 +1237,45 @@ test("offline sync applies and snapshots through secure storage hooks", async ()
|
|
|
892
1237
|
"secret fact",
|
|
893
1238
|
);
|
|
894
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
|
+
);
|
|
895
1279
|
} finally {
|
|
896
1280
|
await rm(root, { recursive: true, force: true });
|
|
897
1281
|
await rm(source, { recursive: true, force: true });
|
|
@@ -916,7 +1300,7 @@ test("offline storage writes invalidate fact hash readiness for rebuild", async
|
|
|
916
1300
|
|
|
917
1301
|
assert.equal(
|
|
918
1302
|
sourceChangeset.changes.some((change) => change.path.startsWith("state/fact-hashes")),
|
|
919
|
-
|
|
1303
|
+
true,
|
|
920
1304
|
);
|
|
921
1305
|
|
|
922
1306
|
const factChangeset = {
|