@remnic/core 9.69.97 → 9.69.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/access-admin-ops-surface.js +10 -10
  2. package/dist/access-authorization-probe.js +11 -11
  3. package/dist/access-boundary.js +10 -10
  4. package/dist/access-cli.js +29 -29
  5. package/dist/access-extraction-force-flush.js +10 -10
  6. package/dist/access-http-briefing.js +11 -11
  7. package/dist/access-http-query.js +10 -10
  8. package/dist/access-http.js +23 -23
  9. package/dist/access-identity-continuity-surface.js +10 -10
  10. package/dist/access-lcm-surface.js +10 -10
  11. package/dist/access-mcp.js +17 -17
  12. package/dist/access-namespace-preflight.js +10 -10
  13. package/dist/access-observe-write-category.js +7 -7
  14. package/dist/access-observe-write-surface.js +10 -10
  15. package/dist/access-offline-manifest.js +6 -6
  16. package/dist/access-operations-batch.js +11 -11
  17. package/dist/access-operations.js +16 -16
  18. package/dist/access-recall-concurrency.js +10 -10
  19. package/dist/access-recall-response.js +10 -10
  20. package/dist/access-recall-surface.js +10 -10
  21. package/dist/access-schema.js +6 -6
  22. package/dist/access-service.js +10 -10
  23. package/dist/access-surface-catalog.js +12 -12
  24. package/dist/active-recall.js +6 -6
  25. package/dist/{chunk-ETUZFFPU.js → chunk-3E6DGSGZ.js} +1 -1
  26. package/dist/chunk-3E6DGSGZ.js.map +1 -0
  27. package/dist/{chunk-UG5V3Z3R.js → chunk-3MK4OYYH.js} +3 -3
  28. package/dist/{chunk-MSAI3Q7D.js → chunk-3QH4NT72.js} +2 -2
  29. package/dist/{chunk-CTEUPJMV.js → chunk-42YP6FMP.js} +7 -7
  30. package/dist/{chunk-C6HPNPNK.js → chunk-53X6CL3A.js} +4 -4
  31. package/dist/{chunk-U36GYZ2P.js → chunk-55NZVUNI.js} +2 -2
  32. package/dist/{chunk-OEBUAC3O.js → chunk-76GQBIVX.js} +2 -2
  33. package/dist/{chunk-4NSLQ7EU.js → chunk-ANPRNTPT.js} +3 -3
  34. package/dist/{chunk-4NSLQ7EU.js.map → chunk-ANPRNTPT.js.map} +1 -1
  35. package/dist/{chunk-5S7SNRSJ.js → chunk-BCQU44UV.js} +2 -2
  36. package/dist/{chunk-ZQTYWSO3.js → chunk-GLJNGCCM.js} +2 -2
  37. package/dist/{chunk-SMLKHXPF.js → chunk-HGG63H3A.js} +2 -2
  38. package/dist/{chunk-P62DSJUK.js → chunk-IAKC3KEJ.js} +4 -4
  39. package/dist/{chunk-3BTHXB7Z.js → chunk-JWG43U5K.js} +2 -2
  40. package/dist/{chunk-IVEHY2VT.js → chunk-KJKTDMAD.js} +5 -5
  41. package/dist/chunk-KJKTDMAD.js.map +1 -0
  42. package/dist/{chunk-AXGJMDBS.js → chunk-KO4X4WRJ.js} +2 -2
  43. package/dist/{chunk-HJ3CBZIG.js → chunk-N4JZ7J2I.js} +7 -7
  44. package/dist/{chunk-VOZNBQNL.js → chunk-NHFWYPXN.js} +2 -2
  45. package/dist/{chunk-GXETU7AE.js → chunk-NI5MWLM3.js} +7 -7
  46. package/dist/{chunk-MZAH3CBT.js → chunk-NWBYJ4VH.js} +2 -2
  47. package/dist/{chunk-6BG364RN.js → chunk-ORVH5S7R.js} +2 -2
  48. package/dist/{chunk-ANGZWYL2.js → chunk-PGJYLYPK.js} +3 -3
  49. package/dist/{chunk-A3K2KKSG.js → chunk-PYYQCHJC.js} +2 -2
  50. package/dist/{chunk-WHIGRW7T.js → chunk-RA3OID2N.js} +2 -2
  51. package/dist/{chunk-RMV2JYOT.js → chunk-SZIJJGA2.js} +11 -11
  52. package/dist/{chunk-PH2V7EXZ.js → chunk-TK7G5ABN.js} +2 -2
  53. package/dist/{chunk-KYOZ5E6V.js → chunk-UEU5IURP.js} +2 -2
  54. package/dist/{chunk-NSKXRTVZ.js → chunk-UGEFYQWU.js} +2 -2
  55. package/dist/{chunk-AF5PW35O.js → chunk-XK6GP7QO.js} +2 -2
  56. package/dist/{chunk-YHZWVDP7.js → chunk-YB5GVH36.js} +2 -2
  57. package/dist/cli.js +27 -27
  58. package/dist/config.js +6 -6
  59. package/dist/deep-recall-http-glue.js +7 -7
  60. package/dist/external-wiki-access.js +11 -11
  61. package/dist/index.js +29 -29
  62. package/dist/memory-browse-operations.js +11 -11
  63. package/dist/offline-sync-changeset-generations.js +3 -3
  64. package/dist/offline-sync-embedding-generation.js +2 -2
  65. package/dist/offline-sync-exclude-globs.d.ts +1 -0
  66. package/dist/offline-sync-exclude-globs.d.ts.map +1 -1
  67. package/dist/offline-sync-exclude-globs.js +1 -1
  68. package/dist/offline-sync-snapshot-stream.js +6 -6
  69. package/dist/offline-sync-upload-staging.d.ts +2 -2
  70. package/dist/offline-sync-upload-staging.d.ts.map +1 -1
  71. package/dist/offline-sync-upload-staging.js +1 -1
  72. package/dist/offline-sync.d.ts.map +1 -1
  73. package/dist/offline-sync.js +5 -5
  74. package/dist/operator-toolkit.js +7 -7
  75. package/dist/orchestrator.js +29 -29
  76. package/dist/recall-navigation-operations.js +11 -11
  77. package/dist/recall-navigation.js +10 -10
  78. package/dist/resume-bundles.js +7 -7
  79. package/dist/support-passport/index.js +13 -13
  80. package/package.json +2 -2
  81. package/src/offline-sync-exclude-globs.ts +3 -2
  82. package/src/offline-sync-upload-staging.test.ts +324 -0
  83. package/src/offline-sync-upload-staging.ts +2 -1
  84. package/src/offline-sync.test.ts +12 -0
  85. package/src/offline-sync.ts +1 -6
  86. package/dist/chunk-ETUZFFPU.js.map +0 -1
  87. package/dist/chunk-IVEHY2VT.js.map +0 -1
  88. /package/dist/{chunk-UG5V3Z3R.js.map → chunk-3MK4OYYH.js.map} +0 -0
  89. /package/dist/{chunk-MSAI3Q7D.js.map → chunk-3QH4NT72.js.map} +0 -0
  90. /package/dist/{chunk-CTEUPJMV.js.map → chunk-42YP6FMP.js.map} +0 -0
  91. /package/dist/{chunk-C6HPNPNK.js.map → chunk-53X6CL3A.js.map} +0 -0
  92. /package/dist/{chunk-U36GYZ2P.js.map → chunk-55NZVUNI.js.map} +0 -0
  93. /package/dist/{chunk-OEBUAC3O.js.map → chunk-76GQBIVX.js.map} +0 -0
  94. /package/dist/{chunk-5S7SNRSJ.js.map → chunk-BCQU44UV.js.map} +0 -0
  95. /package/dist/{chunk-ZQTYWSO3.js.map → chunk-GLJNGCCM.js.map} +0 -0
  96. /package/dist/{chunk-SMLKHXPF.js.map → chunk-HGG63H3A.js.map} +0 -0
  97. /package/dist/{chunk-P62DSJUK.js.map → chunk-IAKC3KEJ.js.map} +0 -0
  98. /package/dist/{chunk-3BTHXB7Z.js.map → chunk-JWG43U5K.js.map} +0 -0
  99. /package/dist/{chunk-AXGJMDBS.js.map → chunk-KO4X4WRJ.js.map} +0 -0
  100. /package/dist/{chunk-HJ3CBZIG.js.map → chunk-N4JZ7J2I.js.map} +0 -0
  101. /package/dist/{chunk-VOZNBQNL.js.map → chunk-NHFWYPXN.js.map} +0 -0
  102. /package/dist/{chunk-GXETU7AE.js.map → chunk-NI5MWLM3.js.map} +0 -0
  103. /package/dist/{chunk-MZAH3CBT.js.map → chunk-NWBYJ4VH.js.map} +0 -0
  104. /package/dist/{chunk-6BG364RN.js.map → chunk-ORVH5S7R.js.map} +0 -0
  105. /package/dist/{chunk-ANGZWYL2.js.map → chunk-PGJYLYPK.js.map} +0 -0
  106. /package/dist/{chunk-A3K2KKSG.js.map → chunk-PYYQCHJC.js.map} +0 -0
  107. /package/dist/{chunk-WHIGRW7T.js.map → chunk-RA3OID2N.js.map} +0 -0
  108. /package/dist/{chunk-RMV2JYOT.js.map → chunk-SZIJJGA2.js.map} +0 -0
  109. /package/dist/{chunk-PH2V7EXZ.js.map → chunk-TK7G5ABN.js.map} +0 -0
  110. /package/dist/{chunk-KYOZ5E6V.js.map → chunk-UEU5IURP.js.map} +0 -0
  111. /package/dist/{chunk-NSKXRTVZ.js.map → chunk-UGEFYQWU.js.map} +0 -0
  112. /package/dist/{chunk-AF5PW35O.js.map → chunk-XK6GP7QO.js.map} +0 -0
  113. /package/dist/{chunk-YHZWVDP7.js.map → chunk-YB5GVH36.js.map} +0 -0
@@ -4,7 +4,7 @@ import {
4
4
  SUPPORT_PASSPORT_MODEL_RESULT_PATH,
5
5
  SupportPassportModelBridge,
6
6
  parseSupportPassportModelJob
7
- } from "../chunk-KYOZ5E6V.js";
7
+ } from "../chunk-UEU5IURP.js";
8
8
  import {
9
9
  DEFAULT_SUPPORT_PASSPORT_CONFIG,
10
10
  parseSupportPassportConfig
@@ -14,8 +14,8 @@ import {
14
14
  buildSupportPassportPublicRequestHandler,
15
15
  composeSupportPassportExternalRequestHandlers,
16
16
  normalizeSupportPassportNetworkAddress
17
- } from "../chunk-VOZNBQNL.js";
18
- import "../chunk-SMLKHXPF.js";
17
+ } from "../chunk-NHFWYPXN.js";
18
+ import "../chunk-HGG63H3A.js";
19
19
  import {
20
20
  SUPPORT_PASSPORT_DRAFT_MAX_CONTENT_CHARACTERS,
21
21
  SUPPORT_PASSPORT_DRAFT_MAX_MEMORIES,
@@ -56,7 +56,7 @@ import {
56
56
  notifySupportPassportCommitted,
57
57
  resolveSupportPassportModelRoutePlan,
58
58
  syncDirectoryForDurability
59
- } from "../chunk-GXETU7AE.js";
59
+ } from "../chunk-NI5MWLM3.js";
60
60
  import "../chunk-W4F3C7N7.js";
61
61
  import "../chunk-MPC7TYTZ.js";
62
62
  import "../chunk-ZHXSOMYH.js";
@@ -88,7 +88,7 @@ import "../chunk-32HO62I4.js";
88
88
  import "../chunk-SOBJ6NEY.js";
89
89
  import "../chunk-QPWUG2S3.js";
90
90
  import "../chunk-R36IBI2M.js";
91
- import "../chunk-C6HPNPNK.js";
91
+ import "../chunk-53X6CL3A.js";
92
92
  import "../chunk-E6UNM67F.js";
93
93
  import "../chunk-6WHJ2EAB.js";
94
94
  import "../chunk-AQEJKOKI.js";
@@ -227,13 +227,13 @@ import "../chunk-Z7L72ZHL.js";
227
227
  import "../chunk-4O4TKIXQ.js";
228
228
  import "../chunk-RMMA44HE.js";
229
229
  import "../chunk-SYOIVK7K.js";
230
- import "../chunk-PH2V7EXZ.js";
230
+ import "../chunk-TK7G5ABN.js";
231
231
  import "../chunk-MY7B3HYC.js";
232
232
  import "../chunk-353C5UNV.js";
233
233
  import "../chunk-5AYUHSAU.js";
234
234
  import "../chunk-BT7NVCML.js";
235
235
  import "../chunk-PO7NXNGC.js";
236
- import "../chunk-3BTHXB7Z.js";
236
+ import "../chunk-JWG43U5K.js";
237
237
  import "../chunk-KQAFEZQX.js";
238
238
  import "../chunk-WEHSQBFR.js";
239
239
  import "../chunk-X7Y7WX73.js";
@@ -244,7 +244,7 @@ import "../chunk-SOAU2OE2.js";
244
244
  import "../chunk-BJMBJZ2Y.js";
245
245
  import "../chunk-UKJAGEXH.js";
246
246
  import "../chunk-FP2373TW.js";
247
- import "../chunk-AF5PW35O.js";
247
+ import "../chunk-XK6GP7QO.js";
248
248
  import "../chunk-N7QV3CMX.js";
249
249
  import "../chunk-PAAJWQH5.js";
250
250
  import "../chunk-2JRB3Z5N.js";
@@ -262,13 +262,13 @@ import "../chunk-C7HJ3NEI.js";
262
262
  import "../chunk-N4PYB235.js";
263
263
  import "../chunk-AW4QUFQF.js";
264
264
  import "../chunk-5FVVY2EB.js";
265
- import "../chunk-IVEHY2VT.js";
265
+ import "../chunk-KJKTDMAD.js";
266
266
  import "../chunk-UI3NYK34.js";
267
- import "../chunk-ETUZFFPU.js";
268
- import "../chunk-MSAI3Q7D.js";
269
- import "../chunk-AXGJMDBS.js";
267
+ import "../chunk-3E6DGSGZ.js";
268
+ import "../chunk-3QH4NT72.js";
269
+ import "../chunk-KO4X4WRJ.js";
270
270
  import "../chunk-TYT4TEQV.js";
271
- import "../chunk-4NSLQ7EU.js";
271
+ import "../chunk-ANPRNTPT.js";
272
272
  import "../chunk-EFHNFTWB.js";
273
273
  import "../chunk-GCGJW34D.js";
274
274
  import "../chunk-75PT4N7Q.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.69.97",
3
+ "version": "9.69.99",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3327,7 +3327,7 @@
3327
3327
  "core"
3328
3328
  ],
3329
3329
  "peerDependencies": {
3330
- "@remnic/coding-graph": "^9.69.97"
3330
+ "@remnic/coding-graph": "^9.69.99"
3331
3331
  },
3332
3332
  "peerDependenciesMeta": {
3333
3333
  "@remnic/coding-graph": {
@@ -104,11 +104,11 @@ export function globToRegExp(glob: string): RegExp {
104
104
  // (Cursor review on PR #1793: trailing `scratch/**` must match
105
105
  // nested `scratch/a/b.md`, matching the offline-mode guide.)
106
106
  if (glob[i + 2] === "/") {
107
- source += "(?:.*/)?";
107
+ source += "(?:[\\s\\S]*/)?";
108
108
  i += 2;
109
109
  continue;
110
110
  }
111
- source += ".*";
111
+ source += "[\\s\\S]*";
112
112
  i += 1;
113
113
  continue;
114
114
  }
@@ -128,6 +128,7 @@ export function globToRegExp(glob: string): RegExp {
128
128
  return new RegExp(`^${source}$`);
129
129
  }
130
130
 
131
+ /** Match relative POSIX paths: `*` stays within a segment; `**` crosses separators and matches every path alone. */
131
132
  export function compileOfflineSyncExcludeGlobs(
132
133
  globs: readonly unknown[],
133
134
  ): RegExp[] {
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Unit coverage for offline-sync-upload-staging.ts (issue #3164): the real
3
+ * on-disk staging layout under a temp archive root — content-keyed spools,
4
+ * chunk offset contiguity, crash debris reclamation, symlink defenses, and
5
+ * the mid-flight reset a second writer performs at offset 0. These functions
6
+ * are also the write path for remote-authoritative runtime state (#3165).
7
+ */
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import {
13
+ lstat,
14
+ mkdir,
15
+ mkdtemp,
16
+ readdir,
17
+ readFile,
18
+ rm,
19
+ symlink,
20
+ utimes,
21
+ writeFile,
22
+ } from "node:fs/promises";
23
+ import { tmpdir } from "node:os";
24
+
25
+ import { prepareSafeArchiveRoot, sha256Bytes, type SafeArchiveRoot } from "./transfer/fs-utils.js";
26
+ import { SYNC_INTERNAL_DIR } from "./offline-sync-file-io.js";
27
+ import { applyOfflineSyncFileContentChunk } from "./offline-sync.js";
28
+ import {
29
+ cleanupOfflineUpload,
30
+ digestOfflineUploadStagingContent,
31
+ hashText,
32
+ pruneOfflineUploadStaging,
33
+ writeOfflineUploadChunk,
34
+ writeSafeFileFromUpload,
35
+ type OfflineUploadStaging,
36
+ } from "./offline-sync-upload-staging.js";
37
+
38
+ const SHA_A = "a".repeat(64);
39
+
40
+ let rootDirCounter = 0;
41
+
42
+ async function newRoot(): Promise<{ root: SafeArchiveRoot; abs: string }> {
43
+ const abs = await mkdtemp(path.join(tmpdir(), `upload-staging-${process.pid}-${rootDirCounter++}-`));
44
+ const root = await prepareSafeArchiveRoot(abs, "test-root", "root");
45
+ return { root, abs };
46
+ }
47
+
48
+ function uploadsDir(abs: string): string {
49
+ return path.join(abs, SYNC_INTERNAL_DIR, "uploads");
50
+ }
51
+
52
+ /** Drive an upload the way applyOfflineSyncFileContentChunk does, with a fixed chunk size. */
53
+ async function stagedUpload(options: {
54
+ root: SafeArchiveRoot;
55
+ abs: string;
56
+ relPath?: string;
57
+ content: Buffer;
58
+ chunkSize: number;
59
+ sourceId?: string;
60
+ }): Promise<string> {
61
+ const relPath = options.relPath ?? "namespaces/alpha/facts/fact-1.md";
62
+ const sourceId = options.sourceId ?? "source-1";
63
+ const sha256 = sha256Bytes(options.content).sha256;
64
+ let upload: OfflineUploadStaging | null = null;
65
+ try {
66
+ if (options.content.length === 0) {
67
+ upload = await writeOfflineUploadChunk({
68
+ root: options.root,
69
+ sourceId,
70
+ relPath,
71
+ sha256,
72
+ bytes: 0,
73
+ offset: 0,
74
+ content: Buffer.alloc(0),
75
+ });
76
+ }
77
+ for (let offset = 0; offset < options.content.length; offset += options.chunkSize) {
78
+ const end = Math.min(offset + options.chunkSize, options.content.length);
79
+ upload = await writeOfflineUploadChunk({
80
+ root: options.root,
81
+ sourceId,
82
+ relPath,
83
+ sha256,
84
+ bytes: options.content.length,
85
+ offset,
86
+ content: options.content.subarray(offset, end),
87
+ });
88
+ }
89
+ if (upload === null) throw new Error("unreachable: the empty-content path still writes chunk 0");
90
+ const digest = await digestOfflineUploadStagingContent({ root: options.root, upload });
91
+ assert.equal(digest.sha256, sha256);
92
+ assert.equal(digest.bytes, options.content.length);
93
+ await writeSafeFileFromUpload(options.root, relPath, upload, undefined, undefined, undefined);
94
+ return path.join(options.abs, relPath);
95
+ } finally {
96
+ if (upload) await cleanupOfflineUpload(upload);
97
+ }
98
+ }
99
+
100
+ test("multi-chunk upload writes exact bytes and reclaims its staging", async (t) => {
101
+ const { root, abs } = await newRoot();
102
+ t.after(() => rm(abs, { recursive: true, force: true }));
103
+ const content = Buffer.from("abcdefghij", "utf8");
104
+ const target = await stagedUpload({ root, abs, content, chunkSize: 4 });
105
+ assert.equal(await readFile(target, "utf8"), "abcdefghij");
106
+ assert.deepEqual(await readdir(uploadsDir(abs)), [], "staging reclaimed after finalize");
107
+ });
108
+
109
+ test("empty upload (bytes 0) still creates an empty target file", async (t) => {
110
+ const { root, abs } = await newRoot();
111
+ t.after(() => rm(abs, { recursive: true, force: true }));
112
+ const target = await stagedUpload({ root, abs, content: Buffer.alloc(0), chunkSize: 4 });
113
+ const stat = await lstat(target);
114
+ assert.equal(stat.size, 0, "empty upload creates a zero-byte target");
115
+ });
116
+
117
+ test("chunk above offset 0 without the initial chunk fails loudly", async (t) => {
118
+ const { root, abs } = await newRoot();
119
+ t.after(() => rm(abs, { recursive: true, force: true }));
120
+ await assert.rejects(
121
+ writeOfflineUploadChunk({
122
+ root,
123
+ sourceId: "source-1",
124
+ relPath: "namespaces/alpha/facts/fact-1.md",
125
+ sha256: SHA_A,
126
+ bytes: 100,
127
+ offset: 4,
128
+ content: Buffer.from("abcd"),
129
+ }),
130
+ /missing initial chunk/,
131
+ );
132
+ });
133
+
134
+ test("digest refuses a gapped chunk sequence instead of skipping bytes", async (t) => {
135
+ const { root, abs } = await newRoot();
136
+ t.after(() => rm(abs, { recursive: true, force: true }));
137
+ const relPath = "namespaces/alpha/facts/fact-1.md";
138
+ const upload = await writeOfflineUploadChunk({
139
+ root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 100, offset: 0,
140
+ content: Buffer.from("aaaa"),
141
+ });
142
+ await writeOfflineUploadChunk({
143
+ root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 100, offset: 9,
144
+ content: Buffer.from("bbbb"),
145
+ });
146
+ await assert.rejects(
147
+ digestOfflineUploadStagingContent({ root, upload }),
148
+ /offset mismatch .* expected 4, got 9/,
149
+ );
150
+ await cleanupOfflineUpload(upload);
151
+ });
152
+
153
+ test("a second writer's offset-0 reset converges identical content and corrupts nothing", async (t) => {
154
+ const { root, abs } = await newRoot();
155
+ t.after(() => rm(abs, { recursive: true, force: true }));
156
+ const relPath = "namespaces/alpha/facts/fact-1.md";
157
+ const declared = sha256Bytes(Buffer.from("12345678")).sha256;
158
+ const shared = { root, sourceId: "source-1", relPath, sha256: declared, bytes: 8 };
159
+ const first = await writeOfflineUploadChunk({ ...shared, offset: 0, content: Buffer.from("1234") });
160
+ await writeOfflineUploadChunk({ ...shared, offset: 0, content: Buffer.from("1234") });
161
+ await writeOfflineUploadChunk({ ...shared, offset: 4, content: Buffer.from("5678") });
162
+ const digest = await digestOfflineUploadStagingContent({ root, upload: first });
163
+ assert.deepEqual(digest, { sha256: declared, bytes: 8 });
164
+ await writeSafeFileFromUpload(root, relPath, first, undefined, undefined, undefined);
165
+ assert.equal(await readFile(path.join(abs, relPath), "utf8"), "12345678");
166
+ await cleanupOfflineUpload(first);
167
+ });
168
+
169
+ test("a reset carrying different bytes fails the real consumer's checksum gate", async (t) => {
170
+ const { abs } = await newRoot();
171
+ t.after(() => rm(abs, { recursive: true, force: true }));
172
+ const relPath = "namespaces/alpha/facts/fact-1.md";
173
+ const declared = sha256Bytes(Buffer.from("12345678")).sha256;
174
+ const shared = {
175
+ root: abs,
176
+ sourceId: "source-1",
177
+ path: relPath,
178
+ sha256: declared,
179
+ bytes: 8,
180
+ mtimeMs: 1_700_000_000_000,
181
+ };
182
+ await applyOfflineSyncFileContentChunk({ ...shared, offset: 0, content: Buffer.from("1234") });
183
+ await applyOfflineSyncFileContentChunk({ ...shared, offset: 0, content: Buffer.from("9999") });
184
+ await assert.rejects(
185
+ applyOfflineSyncFileContentChunk({ ...shared, offset: 4, content: Buffer.from("5678") }),
186
+ /checksum mismatch/,
187
+ );
188
+ assert.deepEqual(
189
+ await readdir(uploadsDir(abs)),
190
+ [],
191
+ "the consumer reclaimed the failed spool",
192
+ );
193
+ assert.equal(fs.existsSync(path.join(abs, relPath)), false, "target never written from a bad spool");
194
+ });
195
+
196
+ test("crash debris: abandoned staging is reclaimed only after the 24h age, by pattern", async (t) => {
197
+ const { root, abs } = await newRoot();
198
+ t.after(() => rm(abs, { recursive: true, force: true }));
199
+ const relPath = "namespaces/alpha/facts/fact-1.md";
200
+ const stagingKey = hashText(["source-1", relPath, SHA_A, "8"].join("\0"));
201
+ const stage = path.join(uploadsDir(abs), `${stagingKey}.part`);
202
+ await writeOfflineUploadChunk({
203
+ root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 8, offset: 0, content: Buffer.from("1234"),
204
+ });
205
+ assert.equal((await readdir(uploadsDir(abs))).length, 1);
206
+
207
+ await pruneOfflineUploadStaging(root);
208
+ assert.equal((await readdir(uploadsDir(abs))).length, 1, "fresh abandoned staging is kept");
209
+
210
+ const stale = new Date(Date.now() - 25 * 60 * 60 * 1000);
211
+ await utimes(stage, stale, stale);
212
+ await pruneOfflineUploadStaging(root);
213
+ assert.equal((await readdir(uploadsDir(abs))).length, 0, "aged-out staging is reclaimed");
214
+
215
+ await writeFile(path.join(uploadsDir(abs), "not-a-part"), "keep me");
216
+ await writeFile(path.join(uploadsDir(abs), `${"z".repeat(64)}.part`), "keep me too");
217
+ await utimes(path.join(uploadsDir(abs), "not-a-part"), stale, stale);
218
+ await utimes(path.join(uploadsDir(abs), `${"z".repeat(64)}.part`), stale, stale);
219
+ await pruneOfflineUploadStaging(root);
220
+ assert.deepEqual((await readdir(uploadsDir(abs))).sort(), [
221
+ "not-a-part",
222
+ `${"z".repeat(64)}.part`,
223
+ ]);
224
+ });
225
+
226
+ test("symlinked chunk path is rejected before write", async (t) => {
227
+ const { root, abs } = await newRoot();
228
+ t.after(() => rm(abs, { recursive: true, force: true }));
229
+ const relPath = "namespaces/alpha/facts/fact-1.md";
230
+ const stagingKey = hashText(["source-1", relPath, SHA_A, "8"].join("\0"));
231
+ const stage = path.join(uploadsDir(abs), `${stagingKey}.part`);
232
+ await writeOfflineUploadChunk({
233
+ root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 8, offset: 0, content: Buffer.from("1234"),
234
+ });
235
+ const victim = path.join(abs, "victim.txt");
236
+ await writeFile(victim, "do not touch");
237
+ await symlink(victim, path.join(stage, `${String(4).padStart(20, "0")}.part`));
238
+ await assert.rejects(
239
+ writeOfflineUploadChunk({
240
+ root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 8, offset: 4, content: Buffer.from("5678"),
241
+ }),
242
+ /symlink/,
243
+ );
244
+ assert.equal(await readFile(victim, "utf8"), "do not touch", "symlink target untouched");
245
+ });
246
+
247
+ test("symlinked final target is rejected and no tmp file is left behind", async (t) => {
248
+ const { root, abs } = await newRoot();
249
+ t.after(() => rm(abs, { recursive: true, force: true }));
250
+ const relPath = "namespaces/alpha/facts/fact-1.md";
251
+ const upload = await writeOfflineUploadChunk({
252
+ root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 8, offset: 0, content: Buffer.from("1234"),
253
+ });
254
+ const realTarget = path.join(abs, "elsewhere.md");
255
+ await writeFile(realTarget, "existing file");
256
+ await mkdir(path.dirname(path.join(abs, relPath)), { recursive: true });
257
+ await symlink(realTarget, path.join(abs, relPath));
258
+ await assert.rejects(
259
+ writeSafeFileFromUpload(root, relPath, upload, undefined, undefined, undefined),
260
+ /symlink/,
261
+ );
262
+ assert.equal(await readFile(realTarget, "utf8"), "existing file", "symlink target not overwritten");
263
+ const leftovers = (await readdir(path.dirname(path.join(abs, relPath))))
264
+ .filter((name) => name.startsWith(".remnic-sync."));
265
+ assert.deepEqual(leftovers, [], "atomic-write tmp cleaned up");
266
+ await cleanupOfflineUpload(upload);
267
+ });
268
+
269
+ test("writeFileChunks hook receives every chunk in order without a local tmp", async (t) => {
270
+ const { root, abs } = await newRoot();
271
+ t.after(() => rm(abs, { recursive: true, force: true }));
272
+ const relPath = "namespaces/alpha/facts/fact-1.md";
273
+ const upload = await writeOfflineUploadChunk({
274
+ root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 12, offset: 0, content: Buffer.from("1234"),
275
+ });
276
+ await writeOfflineUploadChunk({
277
+ root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 12, offset: 4, content: Buffer.from("5678"),
278
+ });
279
+ await writeOfflineUploadChunk({
280
+ root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 12, offset: 8, content: Buffer.from("90ab"),
281
+ });
282
+ const streamed: Buffer[] = [];
283
+ const mtimeMs = 1_700_000_000_000;
284
+ await writeSafeFileFromUpload(root, relPath, upload, undefined, async (target) => {
285
+ assert.equal(target.path, relPath);
286
+ for await (const chunk of target.chunks) streamed.push(Buffer.from(chunk));
287
+ }, mtimeMs);
288
+ assert.equal(Buffer.concat(streamed).toString("utf8"), "1234567890ab");
289
+ assert.equal(
290
+ fs.existsSync(path.join(abs, relPath)),
291
+ false,
292
+ "hook path does not also write a local file",
293
+ );
294
+ await cleanupOfflineUpload(upload);
295
+ });
296
+
297
+ test("concurrent staging keeps identical content at different paths isolated", async (t) => {
298
+ const { root, abs } = await newRoot();
299
+ t.after(() => rm(abs, { recursive: true, force: true }));
300
+ const content = Buffer.from("12345678");
301
+ const shared = { root, sourceId: "source-1", ...sha256Bytes(content) };
302
+ const firstPath = "namespaces/alpha/facts/one.md";
303
+ const secondPath = "namespaces/alpha/facts/two.md";
304
+ const first = await writeOfflineUploadChunk({
305
+ ...shared, relPath: firstPath, offset: 0, content: content.subarray(0, 4),
306
+ });
307
+ const second = await writeOfflineUploadChunk({
308
+ ...shared, relPath: secondPath, offset: 0, content: content.subarray(0, 4),
309
+ });
310
+ await writeOfflineUploadChunk({
311
+ ...shared, relPath: firstPath, offset: 4, content: content.subarray(4),
312
+ });
313
+ assert.deepEqual(await digestOfflineUploadStagingContent({ root, upload: first }), sha256Bytes(content));
314
+ await writeSafeFileFromUpload(root, firstPath, first);
315
+ await cleanupOfflineUpload(first);
316
+ await writeOfflineUploadChunk({
317
+ ...shared, relPath: secondPath, offset: 4, content: content.subarray(4),
318
+ });
319
+ assert.deepEqual(await digestOfflineUploadStagingContent({ root, upload: second }), sha256Bytes(content));
320
+ await writeSafeFileFromUpload(root, secondPath, second);
321
+ await cleanupOfflineUpload(second);
322
+ assert.deepEqual(await readFile(path.join(abs, firstPath)), content);
323
+ assert.deepEqual(await readFile(path.join(abs, secondPath)), content);
324
+ });
@@ -19,7 +19,8 @@ export function hashText(value: string): string {
19
19
  return createHash("sha256").update(value).digest("hex");
20
20
  }
21
21
 
22
- interface OfflineUploadStaging {
22
+ /** A staged upload spool returned by writeOfflineUploadChunk; consumed by digest/cleanup/finalize. */
23
+ export interface OfflineUploadStaging {
23
24
  kind: "single" | "chunks";
24
25
  relPath: string;
25
26
  filePath: string;
@@ -3077,6 +3077,18 @@ test("globToRegExp treats ** as cross-segment in trailing and embedded positions
3077
3077
  assert.equal(embedded.test("state/a/b/other.json"), false);
3078
3078
  });
3079
3079
 
3080
+ test("globToRegExp ** spans line terminators that valid archive paths may contain (#3172)", () => {
3081
+ const trailing = globToRegExp("scratch/**");
3082
+ assert.equal(trailing.test("scratch/a\nb.md"), true);
3083
+ assert.equal(trailing.test("scratch/a\nb/c.md"), true);
3084
+ const embedded = globToRegExp("state/**/runs.json");
3085
+ assert.equal(embedded.test("state/a\nb/runs.json"), true);
3086
+ assert.equal(globToRegExp("**/state/*.sqlite").test("a\nb/state/lcm.sqlite"), true);
3087
+ const single = globToRegExp("state/*.sqlite");
3088
+ assert.equal(single.test("state/a\nb.sqlite"), true);
3089
+ assert.equal(single.test("state/a/b.sqlite"), false);
3090
+ });
3091
+
3080
3092
  test("default excludes cover per-namespace state dirs (#1793 review)", () => {
3081
3093
  const sqlite = globToRegExp("**/state/*.sqlite");
3082
3094
  assert.equal(sqlite.test("state/lcm.sqlite"), true);
@@ -76,6 +76,7 @@ import {
76
76
  writeOfflineUploadChunk,
77
77
  writeSafeFileFromUpload,
78
78
  hashText,
79
+ type OfflineUploadStaging,
79
80
  } from "./offline-sync-upload-staging.js";
80
81
  export type { OfflineSyncExcludeFile, OfflineSyncFileTarget } from "./offline-sync-file-io.js";
81
82
 
@@ -265,12 +266,6 @@ export interface OfflineSyncApplyFileContentChunkResult {
265
266
  currentFile?: OfflineSyncFileState;
266
267
  }
267
268
 
268
- interface OfflineUploadStaging {
269
- kind: "single" | "chunks";
270
- relPath: string;
271
- filePath: string;
272
- }
273
-
274
269
  interface OfflineSyncFileRecordOptions {
275
270
  root: SafeArchiveRoot;
276
271
  relPath: string;
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/offline-sync-upload-staging.ts"],"sourcesContent":["// Offline-sync upload staging: chunked upload spool under\n// `.offline-sync/uploads/` (issue #2033). Extracted from offline-sync.ts so\n// the god-file line-count ratchet does not grow; behavior is unchanged.\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { lstat, mkdir, open, readFile, readdir, rename, rm, stat, unlink, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { SYNC_INTERNAL_DIR, setSafeFileMtime } from \"./offline-sync-file-io.js\";\nimport type { OfflineSyncFileTarget } from \"./offline-sync-file-io.js\";\nimport type {\n OfflineSyncFileStagingWriteTarget,\n OfflineSyncFileWriteChunksTarget,\n OfflineSyncFileWriteTarget,\n} from \"./offline-sync.js\";\nimport { type SafeArchiveRoot, resolveSafeArchiveTarget } from \"./transfer/fs-utils.js\";\n\nconst OFFLINE_SYNC_UPLOAD_STAGING_MAX_AGE_MS = 24 * 60 * 60 * 1000;\n\nexport function hashText(value: string): string {\n return createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\ninterface OfflineUploadStaging {\n kind: \"single\" | \"chunks\";\n relPath: string;\n filePath: string;\n}\n\nfunction offlineUploadRelPath(options: {\n sourceId: string;\n relPath: string;\n sha256: string;\n bytes: number;\n}): string {\n const key = hashText([options.sourceId, options.relPath, options.sha256, String(options.bytes)].join(\"\\0\"));\n return `${SYNC_INTERNAL_DIR}/uploads/${key}.part`;\n}\n\nasync function offlineUploadPath(\n root: SafeArchiveRoot,\n options: {\n sourceId: string;\n relPath: string;\n sha256: string;\n bytes: number;\n }\n): Promise<OfflineUploadStaging> {\n const relPath = offlineUploadRelPath(options);\n return {\n kind: \"single\",\n relPath,\n filePath: await resolveSafeArchiveTarget(root, relPath),\n };\n}\n\nasync function offlineUploadChunkPath(\n root: SafeArchiveRoot,\n options: {\n sourceId: string;\n relPath: string;\n sha256: string;\n bytes: number;\n offset: number;\n }\n): Promise<OfflineUploadStaging> {\n const uploadRelPath = offlineUploadRelPath(options);\n const relPath = `${uploadRelPath}/${String(options.offset).padStart(20, \"0\")}.part`;\n return {\n kind: \"chunks\",\n relPath,\n filePath: await resolveSafeArchiveTarget(root, relPath),\n };\n}\n\nexport async function writeOfflineUploadChunk(options: {\n root: SafeArchiveRoot;\n sourceId: string;\n relPath: string;\n sha256: string;\n bytes: number;\n offset: number;\n content: Buffer;\n readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;\n writeFile?: (target: OfflineSyncFileWriteTarget) => Promise<void>;\n writeStagingFile?: (target: OfflineSyncFileStagingWriteTarget) => Promise<void>;\n}): Promise<OfflineUploadStaging> {\n if ((options.writeFile || options.writeStagingFile) && !options.readFile) {\n throw new Error(\"offline sync upload chunk storage hooks require readFile\");\n }\n const uploadRoot = {\n ...(await offlineUploadPath(options.root, options)),\n kind: \"chunks\" as const,\n };\n if (options.offset === 0) {\n await rm(uploadRoot.filePath, { recursive: true, force: true }).catch(() => {});\n } else {\n const existing = await stat(uploadRoot.filePath).catch((error: unknown) => {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw error;\n });\n if (!existing || !existing.isDirectory()) {\n throw new Error(`offline sync upload is missing initial chunk for ${options.relPath}`);\n }\n }\n const chunk = await offlineUploadChunkPath(options.root, { ...options, offset: options.offset });\n\n const writeStagingFile = options.writeStagingFile ?? options.writeFile;\n if (writeStagingFile) {\n // Storage-backed services provide these hooks so secure-store deployments\n // keep staged partial uploads encrypted at rest without mutating indexes.\n await writeOfflineUploadContent({\n root: options.root,\n relPath: chunk.relPath,\n filePath: chunk.filePath,\n content: options.content,\n writeFile: writeStagingFile,\n });\n return uploadRoot;\n }\n\n await mkdir(path.dirname(chunk.filePath), { recursive: true });\n const existingChunk = await lstat(chunk.filePath).catch((error: unknown) => {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw error;\n });\n if (existingChunk?.isSymbolicLink()) {\n throw new Error(`offline sync upload chunk is a symlink: ${chunk.relPath}`);\n }\n await writeFile(chunk.filePath, options.content, { mode: 0o600 });\n return uploadRoot;\n}\n\nexport async function pruneOfflineUploadStaging(root: SafeArchiveRoot): Promise<void> {\n const uploadsRelPath = `${SYNC_INTERNAL_DIR}/uploads`;\n const uploadsPath = await resolveSafeArchiveTarget(root, uploadsRelPath);\n const entries = await readdir(uploadsPath, { withFileTypes: true }).catch((error: unknown) => {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n throw error;\n });\n const now = Date.now();\n await Promise.all(\n entries.map(async (entry) => {\n if (!/^[a-f0-9]{64}\\.part$/i.test(entry.name)) return;\n const relPath = `${uploadsRelPath}/${entry.name}`;\n const filePath = await resolveSafeArchiveTarget(root, relPath);\n const info = await lstat(filePath).catch((error: unknown) => {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw error;\n });\n if (!info) return;\n if (now - info.mtimeMs <= OFFLINE_SYNC_UPLOAD_STAGING_MAX_AGE_MS) return;\n await rm(filePath, { recursive: true, force: true });\n })\n );\n}\n\nasync function* readOfflineUploadStagingChunks(options: {\n root: SafeArchiveRoot;\n upload: OfflineUploadStaging;\n readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;\n}): AsyncGenerator<Buffer> {\n if (options.upload.kind === \"single\") {\n yield await readOfflineUploadContent({\n root: options.root,\n relPath: options.upload.relPath,\n filePath: options.upload.filePath,\n readFile: options.readFile,\n });\n return;\n }\n\n const entries = await readdir(options.upload.filePath);\n const chunkNames = entries.filter((entry) => /^\\d{20}\\.part$/.test(entry)).sort();\n if (chunkNames.length === 0) {\n throw new Error(`offline sync upload is missing chunks for ${options.upload.relPath}`);\n }\n let expectedOffset = 0;\n for (const chunkName of chunkNames) {\n const offset = Number(chunkName.slice(0, 20));\n if (!Number.isSafeInteger(offset) || offset !== expectedOffset) {\n throw new Error(\n `offline sync upload offset mismatch for ${options.upload.relPath}: expected ${expectedOffset}, got ${offset}`\n );\n }\n const relPath = `${options.upload.relPath}/${chunkName}`;\n const filePath = await resolveSafeArchiveTarget(options.root, relPath);\n const content = await readOfflineUploadContent({\n root: options.root,\n relPath,\n filePath,\n readFile: options.readFile,\n });\n expectedOffset += content.length;\n yield content;\n }\n}\n\nexport async function digestOfflineUploadStagingContent(options: {\n root: SafeArchiveRoot;\n upload: OfflineUploadStaging;\n readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;\n}): Promise<{ sha256: string; bytes: number }> {\n const hash = createHash(\"sha256\");\n let bytes = 0;\n for await (const chunk of readOfflineUploadStagingChunks(options)) {\n hash.update(chunk);\n bytes += chunk.length;\n }\n return { sha256: hash.digest(\"hex\"), bytes };\n}\n\nexport async function writeSafeFileFromUpload(\n root: SafeArchiveRoot,\n relPath: string,\n upload: OfflineUploadStaging,\n readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>,\n writeFileChunks?: (target: OfflineSyncFileWriteChunksTarget) => Promise<void>,\n mtimeMs?: number\n): Promise<void> {\n const target = await resolveSafeArchiveTarget(root, relPath);\n const chunks = readOfflineUploadStagingChunks({ root, upload, readFile });\n if (writeFileChunks) {\n await writeFileChunks({ root: root.abs, path: relPath, filePath: target, chunks });\n await setSafeFileMtime(root, relPath, mtimeMs);\n return;\n }\n\n await mkdir(path.dirname(target), { recursive: true });\n const tmp = path.join(path.dirname(target), `.remnic-sync.${process.pid}.${randomUUID()}.tmp`);\n const handle = await open(tmp, \"w\", 0o600);\n try {\n for await (const chunk of chunks) {\n if (chunk.length > 0) await handle.write(chunk);\n }\n await handle.close();\n const targetStat = await lstat(target).catch((error: unknown) => {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw error;\n });\n if (targetStat?.isSymbolicLink()) {\n throw new Error(`offline sync target is a symlink: ${relPath}`);\n }\n await rename(tmp, target);\n await setSafeFileMtime(root, relPath, mtimeMs);\n } catch (error) {\n await handle.close().catch(() => {});\n await unlink(tmp).catch(() => {});\n throw error;\n }\n}\n\nexport async function cleanupOfflineUpload(upload: OfflineUploadStaging): Promise<void> {\n if (upload.kind === \"chunks\") {\n await rm(upload.filePath, { recursive: true, force: true });\n return;\n }\n await unlink(upload.filePath).catch((error: unknown) => {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return;\n throw error;\n });\n}\n\nasync function readOfflineUploadContent(options: {\n root: SafeArchiveRoot;\n relPath: string;\n filePath: string;\n readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;\n}): Promise<Buffer> {\n if (options.readFile) {\n return options.readFile({\n root: options.root.abs,\n path: options.relPath,\n filePath: options.filePath,\n });\n }\n return readFile(options.filePath);\n}\n\nasync function writeOfflineUploadContent(options: {\n root: SafeArchiveRoot;\n relPath: string;\n filePath: string;\n content: Buffer;\n writeFile: (target: OfflineSyncFileWriteTarget) => Promise<void>;\n}): Promise<void> {\n await options.writeFile({\n root: options.root.abs,\n path: options.relPath,\n filePath: options.filePath,\n content: options.content,\n });\n}\n"],"mappings":"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","names":["readFile"]}