@remnic/core 1.1.28 → 1.1.30
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 +17 -17
- package/dist/access-http.d.ts +1 -1
- package/dist/access-http.js +8 -8
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +7 -7
- package/dist/access-schema.d.ts +55 -5
- package/dist/access-schema.js +4 -2
- package/dist/{access-service-CEyV8XJ5.d.ts → access-service-B5hgZPCN.d.ts} +4 -1
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +5 -5
- package/dist/active-recall.js +1 -1
- package/dist/briefing.js +2 -2
- package/dist/causal-consolidation.js +3 -3
- package/dist/{chunk-25YQM6XW.js → chunk-2IWUMAES.js} +3 -3
- package/dist/{chunk-JUYT2J3K.js → chunk-3OWUCDKH.js} +39 -7
- package/dist/chunk-3OWUCDKH.js.map +1 -0
- package/dist/{chunk-BJ3KMYTB.js → chunk-3TNBOMQT.js} +21 -10
- package/dist/chunk-3TNBOMQT.js.map +1 -0
- package/dist/{chunk-QYHQ2JHL.js → chunk-43PJZYGL.js} +2 -2
- package/dist/{chunk-YITUHONZ.js → chunk-4KGVTPGD.js} +2 -2
- package/dist/{chunk-W7DK3CYM.js → chunk-575RMLWN.js} +2 -2
- package/dist/{chunk-TR4DK5OH.js → chunk-76FLAAUC.js} +2 -2
- package/dist/{chunk-S27EXIHY.js → chunk-77H5NU3M.js} +3 -3
- package/dist/{chunk-IANK6Y5W.js → chunk-A6KTB5R6.js} +2 -2
- package/dist/{chunk-7D6O46PF.js → chunk-BVF3AGJP.js} +2 -2
- package/dist/{chunk-NTUNYIF7.js → chunk-I5GLV3VE.js} +2 -2
- package/dist/{chunk-4H6DURG6.js → chunk-JA3AK3PT.js} +2 -2
- package/dist/{chunk-2DM72JF3.js → chunk-KRBK4BQH.js} +14 -14
- package/dist/{chunk-AMVN77EU.js → chunk-MG7NA5H3.js} +365 -90
- package/dist/chunk-MG7NA5H3.js.map +1 -0
- package/dist/{chunk-RCZRL5BE.js → chunk-MRILGULB.js} +2 -2
- package/dist/{chunk-2QR3XXIC.js → chunk-MZH6EHNR.js} +3 -3
- package/dist/{chunk-2QR3XXIC.js.map → chunk-MZH6EHNR.js.map} +1 -1
- package/dist/{chunk-NW7JW5GA.js → chunk-OC7KHOOX.js} +41 -6
- package/dist/chunk-OC7KHOOX.js.map +1 -0
- package/dist/{chunk-LCTP7YRU.js → chunk-QKZGQIPJ.js} +16 -7
- package/dist/chunk-QKZGQIPJ.js.map +1 -0
- package/dist/{chunk-MVAOT247.js → chunk-QLLBRHAT.js} +11 -11
- package/dist/{chunk-2WIPXV3Y.js → chunk-RR2PKP3I.js} +2 -2
- package/dist/{chunk-3F24QTRI.js → chunk-SAZS2QZB.js} +2 -2
- package/dist/{chunk-VYU7PXUS.js → chunk-SIC6U3GZ.js} +2 -2
- package/dist/{chunk-6CB4E7ZV.js → chunk-UL2NNBUL.js} +4 -4
- package/dist/{chunk-F33CJ5CH.js → chunk-VBJ7V5SK.js} +40 -8
- package/dist/chunk-VBJ7V5SK.js.map +1 -0
- package/dist/{chunk-TFORLO3O.js → chunk-W6AQJ2PY.js} +5 -5
- package/dist/{chunk-PUXCIHRL.js → chunk-XSZEP4SF.js} +2 -2
- package/dist/{chunk-4CRG46BG.js → chunk-ZK7I7JYV.js} +2 -2
- package/dist/{cli-BguVmIwO.d.ts → cli-CJKI2JIe.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +22 -22
- package/dist/compounding/engine.js +2 -2
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.js +2 -2
- package/dist/connectors/index.js +2 -2
- package/dist/entity-retrieval.js +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +37 -27
- package/dist/index.js.map +1 -1
- package/dist/maintenance/memory-governance.js +2 -2
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +2 -2
- package/dist/maintenance/rebuild-memory-projection.js +3 -3
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/namespaces/migrate.js +6 -6
- package/dist/namespaces/search.js +3 -3
- package/dist/namespaces/storage.js +2 -2
- package/dist/offline-sync.d.ts +49 -1
- package/dist/offline-sync.js +13 -1
- package/dist/operator-toolkit.js +9 -9
- package/dist/orchestrator.js +12 -12
- package/dist/qmd.d.ts +3 -1
- package/dist/qmd.js +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/schemas.d.ts +22 -22
- package/dist/search/factory.js +2 -2
- package/dist/search/index.js +2 -2
- package/dist/semantic-consolidation.js +3 -3
- package/dist/semantic-rule-promotion.js +2 -2
- package/dist/semantic-rule-verifier.js +2 -2
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/verified-recall.js +2 -2
- package/package.json +1 -1
- package/src/access-http.test.ts +184 -0
- package/src/access-http.ts +37 -0
- package/src/access-schema.ts +58 -3
- package/src/access-service-namespace.test.ts +56 -1
- package/src/access-service-offline-file-content.test.ts +17 -0
- package/src/access-service.ts +16 -1
- package/src/config.ts +2 -2
- package/src/index.ts +6 -0
- package/src/offline-sync.test.ts +1055 -1
- package/src/offline-sync.ts +453 -96
- package/src/qmd.test.ts +65 -10
- package/src/qmd.ts +22 -9
- package/src/storage.ts +36 -2
- package/dist/chunk-AMVN77EU.js.map +0 -1
- package/dist/chunk-BJ3KMYTB.js.map +0 -1
- package/dist/chunk-F33CJ5CH.js.map +0 -1
- package/dist/chunk-JUYT2J3K.js.map +0 -1
- package/dist/chunk-LCTP7YRU.js.map +0 -1
- package/dist/chunk-NW7JW5GA.js.map +0 -1
- /package/dist/{chunk-25YQM6XW.js.map → chunk-2IWUMAES.js.map} +0 -0
- /package/dist/{chunk-QYHQ2JHL.js.map → chunk-43PJZYGL.js.map} +0 -0
- /package/dist/{chunk-YITUHONZ.js.map → chunk-4KGVTPGD.js.map} +0 -0
- /package/dist/{chunk-W7DK3CYM.js.map → chunk-575RMLWN.js.map} +0 -0
- /package/dist/{chunk-TR4DK5OH.js.map → chunk-76FLAAUC.js.map} +0 -0
- /package/dist/{chunk-S27EXIHY.js.map → chunk-77H5NU3M.js.map} +0 -0
- /package/dist/{chunk-IANK6Y5W.js.map → chunk-A6KTB5R6.js.map} +0 -0
- /package/dist/{chunk-7D6O46PF.js.map → chunk-BVF3AGJP.js.map} +0 -0
- /package/dist/{chunk-NTUNYIF7.js.map → chunk-I5GLV3VE.js.map} +0 -0
- /package/dist/{chunk-4H6DURG6.js.map → chunk-JA3AK3PT.js.map} +0 -0
- /package/dist/{chunk-2DM72JF3.js.map → chunk-KRBK4BQH.js.map} +0 -0
- /package/dist/{chunk-RCZRL5BE.js.map → chunk-MRILGULB.js.map} +0 -0
- /package/dist/{chunk-MVAOT247.js.map → chunk-QLLBRHAT.js.map} +0 -0
- /package/dist/{chunk-2WIPXV3Y.js.map → chunk-RR2PKP3I.js.map} +0 -0
- /package/dist/{chunk-3F24QTRI.js.map → chunk-SAZS2QZB.js.map} +0 -0
- /package/dist/{chunk-VYU7PXUS.js.map → chunk-SIC6U3GZ.js.map} +0 -0
- /package/dist/{chunk-6CB4E7ZV.js.map → chunk-UL2NNBUL.js.map} +0 -0
- /package/dist/{chunk-TFORLO3O.js.map → chunk-W6AQJ2PY.js.map} +0 -0
- /package/dist/{chunk-PUXCIHRL.js.map → chunk-XSZEP4SF.js.map} +0 -0
- /package/dist/{chunk-4CRG46BG.js.map → chunk-ZK7I7JYV.js.map} +0 -0
package/src/access-http.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { validateRequest, type SchemaName, type SchemaTypeFor } from "./access-s
|
|
|
12
12
|
import {
|
|
13
13
|
OFFLINE_SYNC_APPLY_MAX_BODY_BYTES,
|
|
14
14
|
OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
|
|
15
|
+
OFFLINE_SYNC_SNAPSHOT_BASE_MAX_BODY_BYTES,
|
|
15
16
|
} from "./offline-sync.js";
|
|
16
17
|
import type { RecallDisclosure, RecallPlanMode } from "./types.js";
|
|
17
18
|
import { isRecallDisclosure } from "./types.js";
|
|
@@ -125,6 +126,16 @@ function parseTrustZoneFilter(raw: string | null): TrustZoneName | undefined {
|
|
|
125
126
|
throw new HttpError(400, "zone must be one of quarantine|working|trusted", "invalid_zone_filter");
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
function summarizeHttpRequest(req: IncomingMessage): string {
|
|
130
|
+
const method = req.method ?? "UNKNOWN";
|
|
131
|
+
try {
|
|
132
|
+
const parsed = new URL(req.url ?? "/", "http://localhost");
|
|
133
|
+
return `${method} ${parsed.pathname}`;
|
|
134
|
+
} catch {
|
|
135
|
+
return `${method} ${(req.url ?? "/").split("?")[0]}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
128
139
|
/**
|
|
129
140
|
* Decode a `:peerId` URL path segment, converting malformed percent-encoded
|
|
130
141
|
* input (e.g., `%E0%A4%A`) into a 400 client error rather than letting
|
|
@@ -217,6 +228,10 @@ export class EngramAccessHttpServer {
|
|
|
217
228
|
res.destroy(err as Error);
|
|
218
229
|
return;
|
|
219
230
|
}
|
|
231
|
+
log.error(
|
|
232
|
+
`engram access HTTP internal error [${correlationId}] ${summarizeHttpRequest(req)}`,
|
|
233
|
+
err,
|
|
234
|
+
);
|
|
220
235
|
this.respondJson(res, 500, { error: "internal_error", code: "internal_error" });
|
|
221
236
|
});
|
|
222
237
|
});
|
|
@@ -630,6 +645,27 @@ export class EngramAccessHttpServer {
|
|
|
630
645
|
return;
|
|
631
646
|
}
|
|
632
647
|
|
|
648
|
+
if (
|
|
649
|
+
req.method === "POST" &&
|
|
650
|
+
(pathname === "/engram/v1/offline-sync/snapshot" || pathname === "/remnic/v1/offline-sync/snapshot")
|
|
651
|
+
) {
|
|
652
|
+
const body = await this.readValidatedBody(
|
|
653
|
+
req,
|
|
654
|
+
"offlineSyncSnapshot",
|
|
655
|
+
OFFLINE_SYNC_SNAPSHOT_BASE_MAX_BODY_BYTES,
|
|
656
|
+
);
|
|
657
|
+
const result = await this.service.offlineSyncSnapshot({
|
|
658
|
+
namespace: this.resolveNamespace(req, body.namespace),
|
|
659
|
+
principal: this.resolveRequestPrincipal(req),
|
|
660
|
+
includeTranscripts: body.includeTranscripts,
|
|
661
|
+
includeContent: body.includeContent,
|
|
662
|
+
baseFiles: body.baseFiles,
|
|
663
|
+
...(body.baseCapturedAt ? { baseCapturedAt: new Date(body.baseCapturedAt) } : {}),
|
|
664
|
+
});
|
|
665
|
+
this.respondJson(res, 200, result);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
633
669
|
if (
|
|
634
670
|
req.method === "POST" &&
|
|
635
671
|
(pathname === "/engram/v1/offline-sync/files" || pathname === "/remnic/v1/offline-sync/files")
|
|
@@ -717,6 +753,7 @@ export class EngramAccessHttpServer {
|
|
|
717
753
|
namespace: this.resolveNamespace(req, body.namespace),
|
|
718
754
|
principal: this.resolveRequestPrincipal(req),
|
|
719
755
|
changeset: body.changeset,
|
|
756
|
+
returnCurrentFiles: body.returnCurrentFiles,
|
|
720
757
|
});
|
|
721
758
|
this.respondJson(res, 200, result);
|
|
722
759
|
return;
|
package/src/access-schema.ts
CHANGED
|
@@ -9,8 +9,12 @@ import {
|
|
|
9
9
|
ACTION_CONFIDENCE_RULE_KINDS,
|
|
10
10
|
} from "./action-confidence.js";
|
|
11
11
|
import { isValidCapsuleSince } from "./transfer/capsule-export.js";
|
|
12
|
+
import { validateArchiveRelativePath } from "./transfer/fs-utils.js";
|
|
12
13
|
import { CAPSULE_ID_PATTERN } from "./transfer/types.js";
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
|
|
16
|
+
OFFLINE_SYNC_MAX_MTIME_MS,
|
|
17
|
+
} from "./offline-sync.js";
|
|
14
18
|
|
|
15
19
|
// ---------------------------------------------------------------------------
|
|
16
20
|
// Error formatting
|
|
@@ -369,10 +373,57 @@ export const capsuleListRequestSchema = z
|
|
|
369
373
|
// Offline sync
|
|
370
374
|
// ---------------------------------------------------------------------------
|
|
371
375
|
|
|
376
|
+
function isValidOfflineSyncPath(value: string): boolean {
|
|
377
|
+
try {
|
|
378
|
+
validateArchiveRelativePath(value, "path");
|
|
379
|
+
return true;
|
|
380
|
+
} catch {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const offlineSyncPathSchema = z
|
|
386
|
+
.string()
|
|
387
|
+
.trim()
|
|
388
|
+
.min(1, "path must be non-empty")
|
|
389
|
+
.max(4096)
|
|
390
|
+
.refine(
|
|
391
|
+
isValidOfflineSyncPath,
|
|
392
|
+
"path must be a POSIX relative path without unsafe segments",
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const offlineSyncFileStateSchema = z.object({
|
|
396
|
+
path: offlineSyncPathSchema,
|
|
397
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/i, "sha256 must be a 64-character hex digest"),
|
|
398
|
+
bytes: z.number().int().min(0),
|
|
399
|
+
mtimeMs: z.number().finite().min(0).max(OFFLINE_SYNC_MAX_MTIME_MS),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const offlineSyncBaseCapturedAtSchema = z
|
|
403
|
+
.string()
|
|
404
|
+
.trim()
|
|
405
|
+
.min(1, "baseCapturedAt must be non-empty when provided")
|
|
406
|
+
.max(64)
|
|
407
|
+
.refine((value) => Number.isFinite(Date.parse(value)), {
|
|
408
|
+
message: "baseCapturedAt must be a valid ISO 8601 timestamp",
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
export const offlineSyncSnapshotRequestSchema = z.object({
|
|
412
|
+
namespace: namespaceSchema,
|
|
413
|
+
includeTranscripts: z.boolean().optional(),
|
|
414
|
+
includeContent: z.boolean().optional(),
|
|
415
|
+
baseCapturedAt: offlineSyncBaseCapturedAtSchema.optional(),
|
|
416
|
+
baseFiles: z
|
|
417
|
+
.array(offlineSyncFileStateSchema)
|
|
418
|
+
.max(300_000, "baseFiles must contain 300000 or fewer entries")
|
|
419
|
+
.optional(),
|
|
420
|
+
});
|
|
421
|
+
|
|
372
422
|
export const offlineSyncApplyRequestSchema = z
|
|
373
423
|
.object({
|
|
374
424
|
namespace: namespaceSchema,
|
|
375
425
|
changeset: z.unknown(),
|
|
426
|
+
returnCurrentFiles: z.boolean().optional(),
|
|
376
427
|
})
|
|
377
428
|
.refine((value) => value.changeset !== undefined && value.changeset !== null, {
|
|
378
429
|
message: "changeset is required",
|
|
@@ -383,14 +434,14 @@ export const offlineSyncFilesRequestSchema = z.object({
|
|
|
383
434
|
namespace: namespaceSchema,
|
|
384
435
|
includeTranscripts: z.boolean().optional(),
|
|
385
436
|
paths: z
|
|
386
|
-
.array(
|
|
437
|
+
.array(offlineSyncPathSchema)
|
|
387
438
|
.max(5000, "paths must contain 5000 or fewer entries"),
|
|
388
439
|
});
|
|
389
440
|
|
|
390
441
|
export const offlineSyncFileContentRequestSchema = z.object({
|
|
391
442
|
namespace: namespaceSchema,
|
|
392
443
|
includeTranscripts: z.boolean().optional(),
|
|
393
|
-
path:
|
|
444
|
+
path: offlineSyncPathSchema,
|
|
394
445
|
offset: z.number().int().min(0).optional(),
|
|
395
446
|
length: z.number().int().min(1).max(OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES).optional(),
|
|
396
447
|
});
|
|
@@ -461,6 +512,7 @@ export type CapsuleExportRequest = z.infer<typeof capsuleExportRequestSchema>;
|
|
|
461
512
|
export type CapsuleImportRequest = z.infer<typeof capsuleImportRequestSchema>;
|
|
462
513
|
export type CapsuleListRequest = z.infer<typeof capsuleListRequestSchema>;
|
|
463
514
|
export type OfflineSyncApplyRequest = z.infer<typeof offlineSyncApplyRequestSchema>;
|
|
515
|
+
export type OfflineSyncSnapshotRequest = z.infer<typeof offlineSyncSnapshotRequestSchema>;
|
|
464
516
|
export type OfflineSyncFilesRequest = z.infer<typeof offlineSyncFilesRequestSchema>;
|
|
465
517
|
export type OfflineSyncFileContentRequest = z.infer<typeof offlineSyncFileContentRequestSchema>;
|
|
466
518
|
export type ActionConfidenceRequest = z.infer<typeof actionConfidenceRequestSchema>;
|
|
@@ -486,6 +538,7 @@ export type SchemaName =
|
|
|
486
538
|
| "capsuleExport"
|
|
487
539
|
| "capsuleImport"
|
|
488
540
|
| "capsuleList"
|
|
541
|
+
| "offlineSyncSnapshot"
|
|
489
542
|
| "offlineSyncFiles"
|
|
490
543
|
| "offlineSyncFileContent"
|
|
491
544
|
| "offlineSyncApply"
|
|
@@ -508,6 +561,7 @@ export type SchemaTypeFor<N extends SchemaName> =
|
|
|
508
561
|
: N extends "capsuleExport" ? CapsuleExportRequest
|
|
509
562
|
: N extends "capsuleImport" ? CapsuleImportRequest
|
|
510
563
|
: N extends "capsuleList" ? CapsuleListRequest
|
|
564
|
+
: N extends "offlineSyncSnapshot" ? OfflineSyncSnapshotRequest
|
|
511
565
|
: N extends "offlineSyncFiles" ? OfflineSyncFilesRequest
|
|
512
566
|
: N extends "offlineSyncFileContent" ? OfflineSyncFileContentRequest
|
|
513
567
|
: N extends "offlineSyncApply" ? OfflineSyncApplyRequest
|
|
@@ -531,6 +585,7 @@ const schemas: Record<SchemaName, z.ZodTypeAny> = {
|
|
|
531
585
|
capsuleExport: capsuleExportRequestSchema,
|
|
532
586
|
capsuleImport: capsuleImportRequestSchema,
|
|
533
587
|
capsuleList: capsuleListRequestSchema,
|
|
588
|
+
offlineSyncSnapshot: offlineSyncSnapshotRequestSchema,
|
|
534
589
|
offlineSyncFiles: offlineSyncFilesRequestSchema,
|
|
535
590
|
offlineSyncFileContent: offlineSyncFileContentRequestSchema,
|
|
536
591
|
offlineSyncApply: offlineSyncApplyRequestSchema,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, symlink, utimes, writeFile } from "node:fs/promises";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import test from "node:test";
|
|
@@ -184,3 +185,57 @@ test("offlineSyncFiles reports symlink requested paths as input errors", async (
|
|
|
184
185
|
await rm(root, { recursive: true, force: true });
|
|
185
186
|
}
|
|
186
187
|
});
|
|
188
|
+
|
|
189
|
+
test("offlineSyncSnapshot ignores client capture time for server fast-base scans", async () => {
|
|
190
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-offline-snapshot-client-clock-"));
|
|
191
|
+
try {
|
|
192
|
+
const relPath = "facts/a.md";
|
|
193
|
+
const filePath = path.join(root, relPath);
|
|
194
|
+
const oldContent = Buffer.from("alpha");
|
|
195
|
+
const newContent = Buffer.from("bravo");
|
|
196
|
+
const mtimeMs = 1_700_000_000_000;
|
|
197
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
198
|
+
await writeFile(filePath, oldContent);
|
|
199
|
+
await utimes(filePath, mtimeMs / 1000, mtimeMs / 1000);
|
|
200
|
+
const baseFile = {
|
|
201
|
+
path: relPath,
|
|
202
|
+
sha256: createHash("sha256").update(oldContent).digest("hex"),
|
|
203
|
+
bytes: oldContent.byteLength,
|
|
204
|
+
mtimeMs,
|
|
205
|
+
};
|
|
206
|
+
await writeFile(filePath, newContent);
|
|
207
|
+
await utimes(filePath, mtimeMs / 1000, mtimeMs / 1000);
|
|
208
|
+
|
|
209
|
+
const { service } = makeService();
|
|
210
|
+
(service as unknown as {
|
|
211
|
+
orchestrator: {
|
|
212
|
+
config: PluginConfig;
|
|
213
|
+
getStorage(namespace: string): Promise<StorageManager>;
|
|
214
|
+
};
|
|
215
|
+
}).orchestrator.getStorage = async () => ({
|
|
216
|
+
dir: root,
|
|
217
|
+
async readOfflineSyncFile(targetPath: string) {
|
|
218
|
+
return readFile(targetPath);
|
|
219
|
+
},
|
|
220
|
+
async digestOfflineSyncFile(targetPath: string) {
|
|
221
|
+
const content = await readFile(targetPath);
|
|
222
|
+
return {
|
|
223
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
224
|
+
bytes: content.byteLength,
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
} as unknown as StorageManager);
|
|
228
|
+
|
|
229
|
+
const snapshot = await service.offlineSyncSnapshot({
|
|
230
|
+
namespace: "team",
|
|
231
|
+
principal: "reader",
|
|
232
|
+
includeContent: false,
|
|
233
|
+
baseFiles: [baseFile],
|
|
234
|
+
baseCapturedAt: new Date(Date.now() + 60_000),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
assert.equal(snapshot.files[0]?.sha256, createHash("sha256").update(newContent).digest("hex"));
|
|
238
|
+
} finally {
|
|
239
|
+
await rm(root, { recursive: true, force: true });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
|
|
4
4
|
import { EngramAccessInputError, EngramAccessService } from "./access-service.js";
|
|
5
|
+
import { OFFLINE_SYNC_MAX_MTIME_MS } from "./offline-sync.js";
|
|
5
6
|
|
|
6
7
|
function createOfflineService(): EngramAccessService {
|
|
7
8
|
return new EngramAccessService({
|
|
@@ -34,4 +35,20 @@ test("offline apply-file-content reports invalid metadata as input errors", asyn
|
|
|
34
35
|
error instanceof EngramAccessInputError &&
|
|
35
36
|
/sha256 must be a 64-character sha256/.test(error.message),
|
|
36
37
|
);
|
|
38
|
+
|
|
39
|
+
await assert.rejects(
|
|
40
|
+
() => service.offlineSyncApplyFileContent({
|
|
41
|
+
includeTranscripts: true,
|
|
42
|
+
sourceId: "laptop",
|
|
43
|
+
path: "state/lcm.sqlite",
|
|
44
|
+
sha256: "a".repeat(64),
|
|
45
|
+
bytes: 0,
|
|
46
|
+
mtimeMs: OFFLINE_SYNC_MAX_MTIME_MS + 1,
|
|
47
|
+
offset: 0,
|
|
48
|
+
content: Buffer.alloc(0),
|
|
49
|
+
}),
|
|
50
|
+
(error) =>
|
|
51
|
+
error instanceof EngramAccessInputError &&
|
|
52
|
+
/mtimeMs must be within JavaScript Date range/.test(error.message),
|
|
53
|
+
);
|
|
37
54
|
});
|
package/src/access-service.ts
CHANGED
|
@@ -139,11 +139,13 @@ import {
|
|
|
139
139
|
applyOfflineSyncFileContentChunk,
|
|
140
140
|
applyOfflineSyncChangeset,
|
|
141
141
|
buildOfflineSyncSnapshot,
|
|
142
|
+
buildOfflineSyncSnapshotFromBase,
|
|
142
143
|
buildOfflineSyncSnapshotForPaths,
|
|
143
144
|
readOfflineSyncFileContentChunk,
|
|
144
145
|
type OfflineSyncApplyFileContentChunkResult,
|
|
145
146
|
type OfflineSyncApplyChangesetResult,
|
|
146
147
|
type OfflineSyncFileContentChunk,
|
|
148
|
+
type OfflineSyncFileState,
|
|
147
149
|
type OfflineSyncSnapshot,
|
|
148
150
|
} from "./offline-sync.js";
|
|
149
151
|
import {
|
|
@@ -619,6 +621,8 @@ export interface EngramAccessOfflineSyncSnapshotRequest {
|
|
|
619
621
|
principal?: string;
|
|
620
622
|
includeTranscripts?: boolean;
|
|
621
623
|
includeContent?: boolean;
|
|
624
|
+
baseCapturedAt?: Date;
|
|
625
|
+
baseFiles?: OfflineSyncFileState[];
|
|
622
626
|
}
|
|
623
627
|
|
|
624
628
|
export interface EngramAccessOfflineSyncFilesRequest {
|
|
@@ -655,6 +659,7 @@ export interface EngramAccessOfflineSyncApplyRequest {
|
|
|
655
659
|
namespace?: string;
|
|
656
660
|
principal?: string;
|
|
657
661
|
changeset: unknown;
|
|
662
|
+
returnCurrentFiles?: boolean;
|
|
658
663
|
}
|
|
659
664
|
|
|
660
665
|
export interface EngramAccessOfflineSyncSnapshotResponse extends OfflineSyncSnapshot {
|
|
@@ -5602,12 +5607,19 @@ export class EngramAccessService {
|
|
|
5602
5607
|
const resolvedNamespace = this.resolveReadableNamespace(options.namespace, options.principal);
|
|
5603
5608
|
const storage = await this.orchestrator.getStorage(resolvedNamespace);
|
|
5604
5609
|
const storageHash = createHash("sha256").update(storage.dir).digest("hex").slice(0, 16);
|
|
5605
|
-
const
|
|
5610
|
+
const snapshotBuilder = options.includeContent === false && options.baseFiles && options.baseFiles.length > 0
|
|
5611
|
+
? buildOfflineSyncSnapshotFromBase
|
|
5612
|
+
: buildOfflineSyncSnapshot;
|
|
5613
|
+
const snapshot = await snapshotBuilder({
|
|
5606
5614
|
root: storage.dir,
|
|
5607
5615
|
sourceId: `remnic:${resolvedNamespace}:${storageHash}`,
|
|
5616
|
+
...(options.baseFiles && options.baseFiles.length > 0 ? { baseFiles: options.baseFiles } : {}),
|
|
5617
|
+
// Client clocks are not authoritative for server-side ctime reuse. A
|
|
5618
|
+
// future client timestamp can hide same-size, preserved-mtime rewrites.
|
|
5608
5619
|
includeContent: options.includeContent !== false,
|
|
5609
5620
|
includeTranscripts: options.includeTranscripts !== false,
|
|
5610
5621
|
readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
|
|
5622
|
+
readFileDigest: async ({ filePath }) => storage.digestOfflineSyncFile(filePath),
|
|
5611
5623
|
});
|
|
5612
5624
|
return {
|
|
5613
5625
|
namespace: resolvedNamespace,
|
|
@@ -5702,6 +5714,7 @@ export class EngramAccessService {
|
|
|
5702
5714
|
content: options.content,
|
|
5703
5715
|
includeTranscripts: options.includeTranscripts !== false,
|
|
5704
5716
|
readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
|
|
5717
|
+
readFileDigest: async ({ filePath }) => storage.digestOfflineSyncFile(filePath),
|
|
5705
5718
|
writeFile: async ({ filePath, content }) => storage.writeOfflineSyncFile(filePath, content),
|
|
5706
5719
|
writeStagingFile: async ({ filePath, content }) => storage.writeOfflineSyncStagingFile(filePath, content),
|
|
5707
5720
|
writeFileChunks: async ({ filePath, chunks }) => storage.writeOfflineSyncFileChunks(filePath, chunks),
|
|
@@ -5743,7 +5756,9 @@ export class EngramAccessService {
|
|
|
5743
5756
|
const result = await applyOfflineSyncChangeset({
|
|
5744
5757
|
root: storage.dir,
|
|
5745
5758
|
changeset: options.changeset,
|
|
5759
|
+
returnCurrentFiles: options.returnCurrentFiles,
|
|
5746
5760
|
readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
|
|
5761
|
+
readFileDigest: async ({ filePath }) => storage.digestOfflineSyncFile(filePath),
|
|
5747
5762
|
writeFile: async ({ filePath, content }) => storage.writeOfflineSyncFile(filePath, content),
|
|
5748
5763
|
deleteFile: async ({ filePath }) => storage.deleteOfflineSyncFile(filePath),
|
|
5749
5764
|
});
|
package/src/config.ts
CHANGED
|
@@ -96,14 +96,14 @@ function parseBoundedPositiveInteger(
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function parseQmdSupportedVersion(value: unknown): string {
|
|
99
|
-
if (value === undefined || value === null) return "2.5.
|
|
99
|
+
if (value === undefined || value === null) return "2.5.3";
|
|
100
100
|
if (typeof value !== "string") {
|
|
101
101
|
throw new Error(`qmdSupportedVersion must be a semantic version string; got ${JSON.stringify(value)}`);
|
|
102
102
|
}
|
|
103
103
|
const normalized = value.trim();
|
|
104
104
|
if (!/^\d+\.\d+\.\d+$/.test(normalized)) {
|
|
105
105
|
throw new Error(
|
|
106
|
-
`qmdSupportedVersion must be a semantic version string like "2.5.
|
|
106
|
+
`qmdSupportedVersion must be a semantic version string like "2.5.3"; got ${JSON.stringify(value)}`,
|
|
107
107
|
);
|
|
108
108
|
}
|
|
109
109
|
return normalized;
|
package/src/index.ts
CHANGED
|
@@ -683,13 +683,16 @@ export {
|
|
|
683
683
|
OFFLINE_SYNC_CHANGESET_FORMAT,
|
|
684
684
|
OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
|
|
685
685
|
OFFLINE_SYNC_FILE_CONTENT_TRANSFER_CHUNK_BYTES,
|
|
686
|
+
OFFLINE_SYNC_SNAPSHOT_BASE_MAX_BODY_BYTES,
|
|
686
687
|
OFFLINE_SYNC_SNAPSHOT_FORMAT,
|
|
687
688
|
OFFLINE_SYNC_STATE_VERSION,
|
|
688
689
|
applyOfflineSyncFileContentChunk,
|
|
689
690
|
applyOfflineSyncChangeset,
|
|
690
691
|
applyOfflineSyncSnapshot,
|
|
691
692
|
buildOfflineSyncChangeset,
|
|
693
|
+
buildOfflineSyncChangesetFromSnapshot,
|
|
692
694
|
buildOfflineSyncSnapshot,
|
|
695
|
+
buildOfflineSyncSnapshotFromBase,
|
|
693
696
|
buildOfflineSyncSnapshotForPaths,
|
|
694
697
|
defaultOfflineSyncStatePath,
|
|
695
698
|
fileStatesFromSnapshot,
|
|
@@ -698,8 +701,10 @@ export {
|
|
|
698
701
|
offlineSyncStateFromSnapshot,
|
|
699
702
|
readOfflineSyncFileContentChunk,
|
|
700
703
|
readOfflineSyncState,
|
|
704
|
+
shouldPreferIncomingOfflineRuntimeFile,
|
|
701
705
|
summarizeOfflineSyncChangeset,
|
|
702
706
|
summarizeOfflineSyncPendingChanges,
|
|
707
|
+
summarizeOfflineSyncPendingFiles,
|
|
703
708
|
writeOfflineSyncState,
|
|
704
709
|
type OfflineSyncApplyFileContentChunkResult,
|
|
705
710
|
type OfflineSyncApplyChangesetResult,
|
|
@@ -707,6 +712,7 @@ export {
|
|
|
707
712
|
type OfflineSyncChange,
|
|
708
713
|
type OfflineSyncChangeset,
|
|
709
714
|
type OfflineSyncConflict,
|
|
715
|
+
type OfflineSyncFileDigest,
|
|
710
716
|
type OfflineSyncFileRecord,
|
|
711
717
|
type OfflineSyncFileContentChunk,
|
|
712
718
|
type OfflineSyncFileState,
|