@remnic/core 9.3.749 → 9.3.751

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 (271) hide show
  1. package/dist/access-boundary.d.ts +8 -8
  2. package/dist/access-boundary.js +15 -15
  3. package/dist/access-cli.js +39 -39
  4. package/dist/access-http.d.ts +8 -8
  5. package/dist/access-http.js +20 -20
  6. package/dist/access-mcp.d.ts +8 -8
  7. package/dist/access-mcp.js +19 -19
  8. package/dist/access-operations-batch.js +16 -16
  9. package/dist/access-operations.d.ts +8 -8
  10. package/dist/access-operations.js +18 -18
  11. package/dist/access-schema.d.ts +2 -2
  12. package/dist/access-schema.js +5 -5
  13. package/dist/{access-service-5-EVpt3v.d.ts → access-service-DmAEOJdJ.d.ts} +11 -3
  14. package/dist/access-service.d.ts +5 -5
  15. package/dist/access-service.js +14 -14
  16. package/dist/access-surface-catalog.d.ts +8 -8
  17. package/dist/action-confidence.d.ts +1 -1
  18. package/dist/active-memory-bridge.d.ts +1 -1
  19. package/dist/active-recall.d.ts +1 -1
  20. package/dist/active-recall.js +8 -1
  21. package/dist/active-recall.js.map +1 -1
  22. package/dist/behavior-learner.d.ts +1 -1
  23. package/dist/behavior-signals.d.ts +1 -1
  24. package/dist/bootstrap.d.ts +7 -7
  25. package/dist/briefing.d.ts +1 -1
  26. package/dist/briefing.js +7 -7
  27. package/dist/buffer-surprise-report.d.ts +1 -1
  28. package/dist/buffer.d.ts +1 -1
  29. package/dist/calibration.d.ts +1 -1
  30. package/dist/capabilities.d.ts +1 -1
  31. package/dist/{catalog-D_ZDxu8Y.d.ts → catalog-BpOcwHdE.d.ts} +1 -1
  32. package/dist/causal-behavior.d.ts +1 -1
  33. package/dist/causal-consolidation.d.ts +1 -1
  34. package/dist/causal-consolidation.js +8 -8
  35. package/dist/causal-trajectory-graph.d.ts +1 -1
  36. package/dist/{chunk-C5WN74HH.js → chunk-2HAG7JOI.js} +8 -2
  37. package/dist/chunk-2HAG7JOI.js.map +1 -0
  38. package/dist/{chunk-GKRLPHPU.js → chunk-2KK5WNQB.js} +2 -2
  39. package/dist/{chunk-JRDPT6DA.js → chunk-4NWIGAIC.js} +2 -2
  40. package/dist/{chunk-OBJUKYDO.js → chunk-4XL3CVII.js} +2 -2
  41. package/dist/{chunk-WWVXXSLF.js → chunk-5PZCDUJ6.js} +4 -4
  42. package/dist/{chunk-URQLO7BX.js → chunk-6FNZ3NMS.js} +2 -2
  43. package/dist/{chunk-DRUDC2UX.js → chunk-6MMONPHM.js} +2 -2
  44. package/dist/{chunk-RB6A7OUN.js → chunk-6SXNQFHB.js} +2 -2
  45. package/dist/{chunk-ZGZNLRKF.js → chunk-7TWA7DKP.js} +5 -5
  46. package/dist/{chunk-KMNYXLPL.js → chunk-B35L45HZ.js} +2 -2
  47. package/dist/{chunk-SIV3JZPP.js → chunk-CGZWGVQF.js} +5 -5
  48. package/dist/{chunk-CKFLLAIS.js → chunk-DANV2HTH.js} +3 -3
  49. package/dist/{chunk-NVYF5OB6.js → chunk-DBRVU5PR.js} +3 -3
  50. package/dist/{chunk-LMFS64GY.js → chunk-E4PFDD3N.js} +4 -4
  51. package/dist/{chunk-DIHCFJVV.js → chunk-HYI2GP3F.js} +4 -4
  52. package/dist/{chunk-UIKZZ242.js → chunk-IKHKHADN.js} +2 -2
  53. package/dist/{chunk-PUR2CKX2.js → chunk-JS2HKKOK.js} +1 -1
  54. package/dist/{chunk-PUR2CKX2.js.map → chunk-JS2HKKOK.js.map} +1 -1
  55. package/dist/{chunk-R2OAN6IP.js → chunk-K2JM4DZZ.js} +2 -2
  56. package/dist/{chunk-3IJGF6K2.js → chunk-K2R3DEEH.js} +2 -2
  57. package/dist/{chunk-HIHCB2S6.js → chunk-QRRBXD24.js} +2 -2
  58. package/dist/{chunk-PNUOQYRJ.js → chunk-RWZB6MAY.js} +1953 -2172
  59. package/dist/chunk-RWZB6MAY.js.map +1 -0
  60. package/dist/{chunk-ZPZ4VVLT.js → chunk-SD34EK4Z.js} +2 -2
  61. package/dist/{chunk-WK3NK6SP.js → chunk-SNEXVX3U.js} +6 -6
  62. package/dist/{chunk-FVZ2ISIX.js → chunk-TPLE6SNY.js} +7 -7
  63. package/dist/{chunk-24YDB5QP.js → chunk-UETBEHBA.js} +44 -24
  64. package/dist/chunk-UETBEHBA.js.map +1 -0
  65. package/dist/{chunk-5IN26V6W.js → chunk-UU32EAIY.js} +5 -5
  66. package/dist/{chunk-ZGAJB74V.js → chunk-W3BFT5OJ.js} +2 -2
  67. package/dist/{chunk-L4N7SVNN.js → chunk-WA7FECSG.js} +11 -11
  68. package/dist/{chunk-Q3X7GUAQ.js → chunk-WOX2UW4K.js} +2 -2
  69. package/dist/{chunk-XTEWVF3C.js → chunk-WWSXVOGY.js} +2 -2
  70. package/dist/{chunk-PWWWLD7D.js → chunk-X64YLC24.js} +153 -27
  71. package/dist/chunk-X64YLC24.js.map +1 -0
  72. package/dist/{chunk-P4FW3ZU5.js → chunk-YGGXUNS4.js} +20 -20
  73. package/dist/{chunk-JHQUIH3W.js → chunk-ZBVQ4ZM4.js} +19 -19
  74. package/dist/{cli-DuWWjaKT.d.ts → cli-lmf5RGyQ.d.ts} +3 -3
  75. package/dist/cli.d.ts +9 -9
  76. package/dist/cli.js +33 -33
  77. package/dist/compounding/engine.d.ts +1 -1
  78. package/dist/compounding/engine.js +7 -7
  79. package/dist/compounding/preference-consolidator.d.ts +1 -1
  80. package/dist/compression-optimizer.d.ts +1 -1
  81. package/dist/config.d.ts +1 -1
  82. package/dist/config.js +8 -1
  83. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  84. package/dist/connectors/codex-materialize-runner.js +7 -7
  85. package/dist/connectors/codex-materialize.d.ts +1 -1
  86. package/dist/connectors/index.d.ts +1 -1
  87. package/dist/connectors/index.js +7 -7
  88. package/dist/consolidation-provenance-check.d.ts +1 -1
  89. package/dist/consolidation-undo.d.ts +1 -1
  90. package/dist/contradiction/index.d.ts +1 -1
  91. package/dist/conversation-index/backend.d.ts +1 -1
  92. package/dist/conversation-index/chunker.d.ts +1 -1
  93. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  94. package/dist/conversation-index/indexer.d.ts +1 -1
  95. package/dist/conversation-index/search.d.ts +1 -1
  96. package/dist/day-summary.d.ts +1 -1
  97. package/dist/delinearize.d.ts +1 -1
  98. package/dist/direct-answer-wiring.d.ts +1 -1
  99. package/dist/direct-answer.d.ts +1 -1
  100. package/dist/embedding-fallback.d.ts +1 -1
  101. package/dist/enrichment/index.d.ts +1 -1
  102. package/dist/entity-retrieval.d.ts +1 -1
  103. package/dist/entity-retrieval.js +7 -7
  104. package/dist/entity-schema.d.ts +1 -1
  105. package/dist/explicit-capture.d.ts +7 -7
  106. package/dist/extraction-faithfulness.d.ts +1 -1
  107. package/dist/extraction-judge-telemetry.d.ts +1 -1
  108. package/dist/extraction-judge-training.d.ts +1 -1
  109. package/dist/extraction-judge.d.ts +1 -1
  110. package/dist/extraction.d.ts +1 -1
  111. package/dist/fallback-llm.d.ts +1 -1
  112. package/dist/graph-dashboard-diff.d.ts +1 -1
  113. package/dist/graph-dashboard-key.d.ts +1 -1
  114. package/dist/graph-dashboard-parser.d.ts +1 -1
  115. package/dist/graph-edge-reinforcement.d.ts +1 -1
  116. package/dist/graph-snapshot.d.ts +1 -1
  117. package/dist/graph.d.ts +1 -1
  118. package/dist/identity-continuity.d.ts +1 -1
  119. package/dist/importance.d.ts +1 -1
  120. package/dist/index.d.ts +10 -10
  121. package/dist/index.js +76 -72
  122. package/dist/intent.d.ts +1 -1
  123. package/dist/lcm/engine.d.ts +1 -1
  124. package/dist/lcm/index.d.ts +1 -1
  125. package/dist/lcm/tools.d.ts +1 -1
  126. package/dist/lifecycle.d.ts +1 -1
  127. package/dist/live-connectors-runner.d.ts +1 -1
  128. package/dist/local-llm.d.ts +1 -1
  129. package/dist/local-model-endpoint.d.ts +1 -1
  130. package/dist/maintenance/memory-governance.d.ts +1 -1
  131. package/dist/maintenance/memory-governance.js +7 -7
  132. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
  133. package/dist/maintenance/rebuild-memory-projection.js +8 -8
  134. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  135. package/dist/memory-action-policy.d.ts +1 -1
  136. package/dist/memory-cache.d.ts +1 -1
  137. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  138. package/dist/memory-projection-store.d.ts +1 -1
  139. package/dist/memory-provenance.d.ts +1 -1
  140. package/dist/memory-worth-outcomes.d.ts +1 -1
  141. package/dist/models-json.d.ts +1 -1
  142. package/dist/namespaces/migrate.d.ts +2 -2
  143. package/dist/namespaces/migrate.js +8 -8
  144. package/dist/namespaces/principal.d.ts +1 -1
  145. package/dist/namespaces/search.d.ts +1 -1
  146. package/dist/namespaces/search.js +1 -1
  147. package/dist/namespaces/storage.d.ts +2 -2
  148. package/dist/namespaces/storage.js +7 -7
  149. package/dist/native-knowledge.d.ts +1 -1
  150. package/dist/offline-sync.d.ts +108 -1
  151. package/dist/offline-sync.js +7 -1
  152. package/dist/operator-toolkit.d.ts +1 -1
  153. package/dist/operator-toolkit.js +15 -12
  154. package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
  155. package/dist/orchestration/maintenance.d.ts +2 -2
  156. package/dist/orchestration/maintenance.js +9 -9
  157. package/dist/{orchestrator-CZI6hO_R.d.ts → orchestrator-D7bb8ZGp.d.ts} +193 -201
  158. package/dist/orchestrator.d.ts +5 -5
  159. package/dist/orchestrator.js +51 -41
  160. package/dist/patterns-cli.d.ts +1 -1
  161. package/dist/policy-runtime.d.ts +1 -1
  162. package/dist/provenance.d.ts +1 -1
  163. package/dist/qmd-recall-cache.d.ts +1 -1
  164. package/dist/qmd.d.ts +1 -1
  165. package/dist/recall-disclosure-escalation.d.ts +1 -1
  166. package/dist/recall-explain-renderer.d.ts +1 -1
  167. package/dist/recall-explain-renderer.js +3 -3
  168. package/dist/recall-planner-llm.d.ts +1 -1
  169. package/dist/recall-state.d.ts +1 -1
  170. package/dist/recall-tag-filter.d.ts +1 -1
  171. package/dist/recall-xray-cli.d.ts +1 -1
  172. package/dist/recall-xray-cli.js +4 -4
  173. package/dist/recall-xray-renderer.d.ts +1 -1
  174. package/dist/recall-xray-renderer.js +3 -3
  175. package/dist/recall-xray.d.ts +1 -1
  176. package/dist/recall-xray.js +2 -2
  177. package/dist/resolve-auth-token.d.ts +1 -1
  178. package/dist/resume-bundles.js +9 -2
  179. package/dist/retrieval-agents.d.ts +1 -1
  180. package/dist/retrieval-tiers.d.ts +1 -1
  181. package/dist/routing/engine.d.ts +1 -1
  182. package/dist/routing/store.d.ts +1 -1
  183. package/dist/schemas.d.ts +22 -22
  184. package/dist/search/embed-helper.d.ts +1 -1
  185. package/dist/search/factory.d.ts +1 -1
  186. package/dist/search/factory.js +1 -1
  187. package/dist/search/index.d.ts +1 -1
  188. package/dist/search/index.js +1 -1
  189. package/dist/search/lancedb-backend.d.ts +1 -1
  190. package/dist/search/lancedb-backend.js +1 -1
  191. package/dist/search/meilisearch-backend.d.ts +1 -1
  192. package/dist/search/meilisearch-backend.js +1 -1
  193. package/dist/search/noop-backend.d.ts +1 -1
  194. package/dist/search/orama-backend.d.ts +1 -1
  195. package/dist/search/orama-backend.js +1 -1
  196. package/dist/search/port.d.ts +1 -1
  197. package/dist/search/remote-backend.d.ts +1 -1
  198. package/dist/{semantic-consolidation-D3aSiu9t.d.ts → semantic-consolidation-CTfbVwCA.d.ts} +1 -1
  199. package/dist/semantic-consolidation.d.ts +2 -2
  200. package/dist/semantic-consolidation.js +8 -8
  201. package/dist/semantic-rule-promotion.js +7 -7
  202. package/dist/semantic-rule-verifier.d.ts +1 -1
  203. package/dist/semantic-rule-verifier.js +7 -7
  204. package/dist/session-observer-bands.d.ts +1 -1
  205. package/dist/session-observer-state.d.ts +1 -1
  206. package/dist/shared-context/manager.d.ts +1 -1
  207. package/dist/signal.d.ts +1 -1
  208. package/dist/storage.d.ts +1 -1
  209. package/dist/storage.js +6 -6
  210. package/dist/summarizer.d.ts +1 -1
  211. package/dist/summary-snapshot.d.ts +1 -1
  212. package/dist/temporal-supersession.d.ts +1 -1
  213. package/dist/temporal-validity.d.ts +1 -1
  214. package/dist/threading.d.ts +1 -1
  215. package/dist/tier-migration.d.ts +1 -1
  216. package/dist/tier-routing.d.ts +1 -1
  217. package/dist/topics.d.ts +1 -1
  218. package/dist/transcript.d.ts +1 -1
  219. package/dist/transfer/types.d.ts +12 -12
  220. package/dist/trust-score-stage.d.ts +1 -1
  221. package/dist/trust-score.d.ts +1 -1
  222. package/dist/{types-CZm6rx9-.d.ts → types-D4NYDtXI.d.ts} +7 -0
  223. package/dist/types.d.ts +1 -1
  224. package/dist/types.js +1 -1
  225. package/dist/utility-runtime.d.ts +1 -1
  226. package/dist/verified-recall.js +7 -7
  227. package/package.json +2 -2
  228. package/src/access-service.ts +22 -0
  229. package/src/config.ts +5 -0
  230. package/src/index.ts +3 -1
  231. package/src/offline-sync.test.ts +354 -52
  232. package/src/offline-sync.ts +332 -24
  233. package/src/orchestration/extraction-run.ts +4 -34
  234. package/src/orchestration/namespace-read-fanout.ts +484 -0
  235. package/src/orchestration/orchestrator-helpers.ts +1130 -0
  236. package/src/orchestration/self-deps.ts +50 -0
  237. package/src/orchestration/workspace-ops.ts +789 -0
  238. package/src/orchestrator.ts +177 -2296
  239. package/src/types.ts +9 -0
  240. package/dist/chunk-24YDB5QP.js.map +0 -1
  241. package/dist/chunk-C5WN74HH.js.map +0 -1
  242. package/dist/chunk-PNUOQYRJ.js.map +0 -1
  243. package/dist/chunk-PWWWLD7D.js.map +0 -1
  244. /package/dist/{chunk-GKRLPHPU.js.map → chunk-2KK5WNQB.js.map} +0 -0
  245. /package/dist/{chunk-JRDPT6DA.js.map → chunk-4NWIGAIC.js.map} +0 -0
  246. /package/dist/{chunk-OBJUKYDO.js.map → chunk-4XL3CVII.js.map} +0 -0
  247. /package/dist/{chunk-WWVXXSLF.js.map → chunk-5PZCDUJ6.js.map} +0 -0
  248. /package/dist/{chunk-URQLO7BX.js.map → chunk-6FNZ3NMS.js.map} +0 -0
  249. /package/dist/{chunk-DRUDC2UX.js.map → chunk-6MMONPHM.js.map} +0 -0
  250. /package/dist/{chunk-RB6A7OUN.js.map → chunk-6SXNQFHB.js.map} +0 -0
  251. /package/dist/{chunk-ZGZNLRKF.js.map → chunk-7TWA7DKP.js.map} +0 -0
  252. /package/dist/{chunk-KMNYXLPL.js.map → chunk-B35L45HZ.js.map} +0 -0
  253. /package/dist/{chunk-SIV3JZPP.js.map → chunk-CGZWGVQF.js.map} +0 -0
  254. /package/dist/{chunk-CKFLLAIS.js.map → chunk-DANV2HTH.js.map} +0 -0
  255. /package/dist/{chunk-NVYF5OB6.js.map → chunk-DBRVU5PR.js.map} +0 -0
  256. /package/dist/{chunk-LMFS64GY.js.map → chunk-E4PFDD3N.js.map} +0 -0
  257. /package/dist/{chunk-DIHCFJVV.js.map → chunk-HYI2GP3F.js.map} +0 -0
  258. /package/dist/{chunk-UIKZZ242.js.map → chunk-IKHKHADN.js.map} +0 -0
  259. /package/dist/{chunk-R2OAN6IP.js.map → chunk-K2JM4DZZ.js.map} +0 -0
  260. /package/dist/{chunk-3IJGF6K2.js.map → chunk-K2R3DEEH.js.map} +0 -0
  261. /package/dist/{chunk-HIHCB2S6.js.map → chunk-QRRBXD24.js.map} +0 -0
  262. /package/dist/{chunk-ZPZ4VVLT.js.map → chunk-SD34EK4Z.js.map} +0 -0
  263. /package/dist/{chunk-WK3NK6SP.js.map → chunk-SNEXVX3U.js.map} +0 -0
  264. /package/dist/{chunk-FVZ2ISIX.js.map → chunk-TPLE6SNY.js.map} +0 -0
  265. /package/dist/{chunk-5IN26V6W.js.map → chunk-UU32EAIY.js.map} +0 -0
  266. /package/dist/{chunk-ZGAJB74V.js.map → chunk-W3BFT5OJ.js.map} +0 -0
  267. /package/dist/{chunk-L4N7SVNN.js.map → chunk-WA7FECSG.js.map} +0 -0
  268. /package/dist/{chunk-Q3X7GUAQ.js.map → chunk-WOX2UW4K.js.map} +0 -0
  269. /package/dist/{chunk-XTEWVF3C.js.map → chunk-WWSXVOGY.js.map} +0 -0
  270. /package/dist/{chunk-P4FW3ZU5.js.map → chunk-YGGXUNS4.js.map} +0 -0
  271. /package/dist/{chunk-JHQUIH3W.js.map → chunk-ZBVQ4ZM4.js.map} +0 -0
@@ -198,6 +198,73 @@ const EXCLUDED_FILE_PREFIXES = [
198
198
  ".remnic-sync.",
199
199
  ".remnic-sync-state.",
200
200
  ];
201
+ // Issue #1786: node-local runtime state that each node rebuilds from synced
202
+ // records. Pushing these wastes bandwidth, corrupts the remote's live state
203
+ // dir, and trips the large-file push retry loop on a live SQLite database.
204
+ const DEFAULT_OFFLINE_SYNC_EXCLUDE_GLOBS: readonly string[] = [
205
+ // Leading `**/` matches zero or more segments, so each pattern covers both
206
+ // the root `state/` dir AND per-namespace `namespaces/<ns>/state/` dirs
207
+ // (Cursor review on PR #1793: multi-namespace deployments previously kept
208
+ // pushing their namespaced live sqlite files).
209
+ "**/state/*.sqlite",
210
+ "**/state/*.sqlite-*",
211
+ "**/state/index_tags.json",
212
+ "**/state/entity-mention-index.json",
213
+ "**/state/memory-governance/runs/**",
214
+ ];
215
+
216
+ /**
217
+ * Convert a tiny subset of glob syntax (`*`, `**`, `?`, literal text) into a
218
+ * regular expression anchored at the start and end of the input. The matcher
219
+ * is intentionally narrow — it is only used for operator-supplied
220
+ * `offlineSyncExcludes` entries, not for full shell-style globbing.
221
+ * `*` and `?` match within a single path segment. `**` is cross-segment
222
+ * wherever it appears: a `star-star-slash` prefix (leading or embedded)
223
+ * matches zero or more whole segments, and a trailing `dir/star-star`
224
+ * matches everything under `dir/` at any depth.
225
+ */
226
+ export function globToRegExp(glob: string): RegExp {
227
+ if (typeof glob !== "string" || glob.length === 0) {
228
+ throw new Error("offlineSyncExcludes entry must be a non-empty string");
229
+ }
230
+ if (glob.includes("\0")) {
231
+ throw new Error("offlineSyncExcludes entry must not contain NUL bytes");
232
+ }
233
+ let source = "";
234
+ for (let i = 0; i < glob.length; i += 1) {
235
+ const ch = glob[i];
236
+ if (ch === "*") {
237
+ if (glob[i + 1] === "*") {
238
+ // `**` is cross-segment wherever it appears:
239
+ // leading `**/` -> zero or more whole segments
240
+ // `/**` at end -> everything under the directory
241
+ // `a/**/b` -> any depth between segments
242
+ // (Cursor review on PR #1793: trailing `scratch/**` must match
243
+ // nested `scratch/a/b.md`, matching the offline-mode guide.)
244
+ if (glob[i + 2] === "/") {
245
+ source += "(?:.*/)?";
246
+ i += 2;
247
+ continue;
248
+ }
249
+ source += ".*";
250
+ i += 1;
251
+ continue;
252
+ }
253
+ source += "[^/]*";
254
+ continue;
255
+ }
256
+ if (ch === "?") {
257
+ source += "[^/]";
258
+ continue;
259
+ }
260
+ if (ch === "/") {
261
+ source += "/";
262
+ continue;
263
+ }
264
+ source += ch.replace(/[\\^$.+()|{}\[\]]/g, "\\$&");
265
+ }
266
+ return new RegExp(`^${source}$`);
267
+ }
201
268
 
202
269
  function hashText(value: string): string {
203
270
  return createHash("sha256").update(value).digest("hex");
@@ -354,7 +421,9 @@ export function normalizeOfflineSyncSnapshot(
354
421
  };
355
422
  }
356
423
 
357
- export function normalizeOfflineSyncChangeset(input: unknown): OfflineSyncChangeset {
424
+ export function normalizeOfflineSyncChangeset(
425
+ input: unknown,
426
+ ): OfflineSyncChangeset {
358
427
  if (!input || typeof input !== "object" || Array.isArray(input)) {
359
428
  throw new Error("offline sync changeset must be an object");
360
429
  }
@@ -467,7 +536,95 @@ function shouldExcludeRelPath(relPosix: string, includeTranscripts: boolean): bo
467
536
  const basename = parts[parts.length - 1] ?? "";
468
537
  if (isCanonicalRuntimeStatePath(parts) && basename.includes(".tmp-")) return true;
469
538
  if (EXCLUDED_FILE_NAMES.has(basename)) return true;
470
- return EXCLUDED_FILE_PREFIXES.some((prefix) => basename.startsWith(prefix));
539
+ if (EXCLUDED_FILE_PREFIXES.some((prefix) => basename.startsWith(prefix))) return true;
540
+ return false;
541
+ }
542
+
543
+ /**
544
+ * Push-side variant of `shouldExcludeRelPath` for issue #1786. Combines the
545
+ * legacy structural exclude (private dirs, sync internal, tmp runtime state,
546
+ * excluded file names/prefixes) with the new push-only exclusion layers:
547
+ * - the `state/*.sqlite`, `state/index_tags.json`, etc. defaults that
548
+ * describe node-local runtime state each node rebuilds from synced
549
+ * records, and
550
+ * - operator-supplied `offlineSyncExcludes` regexps.
551
+ *
552
+ * Used ONLY by push-side enumeration/validation paths. Apply-side callers
553
+ * must use the legacy `shouldExcludeRelPath` so a remote snapshot containing
554
+ * `state/lcm.sqlite` can still be accepted on first sync -- the remote's
555
+ * view is authoritative for that file until the local LCM is bootstrapped.
556
+ */
557
+ function shouldExcludePushRelPath(
558
+ relPosix: string,
559
+ includeTranscripts: boolean,
560
+ userExcludeRegexps?: readonly RegExp[],
561
+ ): boolean {
562
+ if (shouldExcludeRelPath(relPosix, includeTranscripts)) return true;
563
+ // Issue #1786: node-local runtime state (live sqlite, derived indexes,
564
+ // per-node governance run logs) is rebuilt by each node, never synced.
565
+ if (matchesOfflineSyncDefaultExclude(relPosix)) return true;
566
+ if (userExcludeRegexps && userExcludeRegexps.some((re) => re.test(relPosix))) return true;
567
+ return false;
568
+ }
569
+
570
+ // Precompiled once at module load — this check sits on the hot
571
+ // enumeration path for every walked file (Kilo review, PR #1793).
572
+ const DEFAULT_OFFLINE_SYNC_EXCLUDE_REGEXPS: readonly RegExp[] =
573
+ DEFAULT_OFFLINE_SYNC_EXCLUDE_GLOBS.map((glob) => globToRegExp(glob));
574
+
575
+ function matchesOfflineSyncDefaultExclude(relPosix: string): boolean {
576
+ for (const regexp of DEFAULT_OFFLINE_SYNC_EXCLUDE_REGEXPS) {
577
+ if (regexp.test(relPosix)) return true;
578
+ }
579
+ return false;
580
+ }
581
+
582
+ /**
583
+ * Compile operator-supplied `offlineSyncExcludes` glob strings into a
584
+ * pre-validated array of regular expressions. Empty input is allowed and
585
+ * returns an empty list. Throws on any invalid entry — callers should treat
586
+ * a thrown error as a fatal configuration mistake and refuse to start the
587
+ * sync run rather than silently dropping the bad entry.
588
+ */
589
+ export function compileOfflineSyncExcludeGlobs(
590
+ globs: readonly unknown[],
591
+ ): RegExp[] {
592
+ const out: RegExp[] = [];
593
+ for (const entry of globs) {
594
+ if (typeof entry !== "string" || entry.length === 0) {
595
+ throw new Error("offlineSyncExcludes must contain only non-empty strings");
596
+ }
597
+ out.push(globToRegExp(entry));
598
+ }
599
+ return out;
600
+ }
601
+
602
+ /**
603
+ * Validate the operator-supplied offline-sync exclude list (#1786).
604
+ * Rejects loudly instead of silently defaulting (CLAUDE.md rule 39):
605
+ * a misspelled key value must fail config parse, not be ignored.
606
+ * Lives next to the glob compiler so config.ts only carries the call.
607
+ */
608
+ export function parseOfflineSyncExcludes(raw: unknown): string[] {
609
+ if (raw === undefined || raw === null) return [];
610
+ if (!Array.isArray(raw)) {
611
+ throw new Error(
612
+ `offlineSyncExcludes must be an array of non-empty glob strings; got ${typeof raw}`,
613
+ );
614
+ }
615
+ for (const entry of raw) {
616
+ if (typeof entry !== "string" || entry.trim().length === 0) {
617
+ throw new Error(
618
+ "offlineSyncExcludes must contain only non-empty glob strings",
619
+ );
620
+ }
621
+ }
622
+ const globs = raw.map((entry) => (entry as string).trim());
623
+ // Compile-check every glob now so a bad pattern fails at parse time
624
+ // rather than mid-sync. compileOfflineSyncExcludeGlobs throws with a
625
+ // per-entry message.
626
+ compileOfflineSyncExcludeGlobs(globs);
627
+ return globs;
471
628
  }
472
629
 
473
630
  function shouldIgnoreIncomingRuntimePath(relPosix: string): boolean {
@@ -629,6 +786,15 @@ export async function* iterateOfflineSyncSnapshotFileRecords(options: {
629
786
  includeTranscripts?: boolean;
630
787
  readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
631
788
  readFileDigest?: (target: OfflineSyncFileTarget) => Promise<OfflineSyncFileDigest>;
789
+ userExcludeRegexps?: readonly RegExp[];
790
+ /**
791
+ * When false, enumeration uses only the legacy structural excludes —
792
+ * the apply/pull-side view of local files. Push-side callers keep the
793
+ * default (true): built-in node-local state excludes + user excludes.
794
+ * (Cursor review on PR #1793: apply-side local enumeration must see
795
+ * files like state/lcm.sqlite or merges misclassify them.)
796
+ */
797
+ excludeNodeLocalState?: boolean;
632
798
  signal?: AbortSignal;
633
799
  }): AsyncIterable<OfflineSyncFileRecord> {
634
800
  throwIfOfflineSyncAborted(options.signal);
@@ -641,10 +807,13 @@ export async function* iterateOfflineSyncSnapshotFileRecords(options: {
641
807
  let entries = await readdir(dirAbs, { withFileTypes: true });
642
808
  entries = entries.sort((left, right) => left.name.localeCompare(right.name));
643
809
  for (const entry of entries) {
644
- throwIfOfflineSyncAborted(options.signal);
645
810
  const abs = path.join(dirAbs, entry.name);
646
811
  const relPosix = path.relative(root.abs, abs).split(path.sep).join("/");
647
- if (shouldExcludeRelPath(relPosix, includeTranscripts)) continue;
812
+ if (
813
+ options.excludeNodeLocalState === false
814
+ ? shouldExcludeRelPath(relPosix, includeTranscripts)
815
+ : shouldExcludePushRelPath(relPosix, includeTranscripts, options.userExcludeRegexps)
816
+ ) continue;
648
817
  if (entry.isSymbolicLink()) continue;
649
818
  if (entry.isDirectory()) {
650
819
  yield* walk(abs);
@@ -674,6 +843,15 @@ export async function buildOfflineSyncSnapshot(options: {
674
843
  now?: Date;
675
844
  readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
676
845
  readFileDigest?: (target: OfflineSyncFileTarget) => Promise<OfflineSyncFileDigest>;
846
+ userExcludeRegexps?: readonly RegExp[];
847
+ /**
848
+ * When false, enumeration uses only the legacy structural excludes —
849
+ * the apply/pull-side view of local files. Push-side callers keep the
850
+ * default (true): built-in node-local state excludes + user excludes.
851
+ * (Cursor review on PR #1793: apply-side local enumeration must see
852
+ * files like state/lcm.sqlite or merges misclassify them.)
853
+ */
854
+ excludeNodeLocalState?: boolean;
677
855
  signal?: AbortSignal;
678
856
  }): Promise<OfflineSyncSnapshot> {
679
857
  throwIfOfflineSyncAborted(options.signal);
@@ -702,6 +880,15 @@ export async function buildOfflineSyncSnapshotFromBase(options: {
702
880
  now?: Date;
703
881
  readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
704
882
  readFileDigest?: (target: OfflineSyncFileTarget) => Promise<OfflineSyncFileDigest>;
883
+ userExcludeRegexps?: readonly RegExp[];
884
+ /**
885
+ * When false, enumeration uses only the legacy structural excludes —
886
+ * the apply/pull-side view of local files. Push-side callers keep the
887
+ * default (true): built-in node-local state excludes + user excludes.
888
+ * (Cursor review on PR #1793: apply-side local enumeration must see
889
+ * files like state/lcm.sqlite or merges misclassify them.)
890
+ */
891
+ excludeNodeLocalState?: boolean;
705
892
  signal?: AbortSignal;
706
893
  }): Promise<OfflineSyncSnapshot> {
707
894
  throwIfOfflineSyncAborted(options.signal);
@@ -726,7 +913,11 @@ export async function buildOfflineSyncSnapshotFromBase(options: {
726
913
  throwIfOfflineSyncAborted(options.signal);
727
914
  const abs = path.join(dirAbs, entry.name);
728
915
  const relPosix = path.relative(root.abs, abs).split(path.sep).join("/");
729
- if (shouldExcludeRelPath(relPosix, includeTranscripts)) continue;
916
+ if (
917
+ options.excludeNodeLocalState === false
918
+ ? shouldExcludeRelPath(relPosix, includeTranscripts)
919
+ : shouldExcludePushRelPath(relPosix, includeTranscripts, options.userExcludeRegexps)
920
+ ) continue;
730
921
  if (entry.isSymbolicLink()) continue;
731
922
  if (entry.isDirectory()) {
732
923
  await walk(abs);
@@ -778,6 +969,15 @@ export async function buildOfflineSyncSnapshotForPaths(options: {
778
969
  now?: Date;
779
970
  readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
780
971
  readFileDigest?: (target: OfflineSyncFileTarget) => Promise<OfflineSyncFileDigest>;
972
+ userExcludeRegexps?: readonly RegExp[];
973
+ /**
974
+ * When false, enumeration uses only the legacy structural excludes —
975
+ * the apply/pull-side view of local files. Push-side callers keep the
976
+ * default (true): built-in node-local state excludes + user excludes.
977
+ * (Cursor review on PR #1793: apply-side local enumeration must see
978
+ * files like state/lcm.sqlite or merges misclassify them.)
979
+ */
980
+ excludeNodeLocalState?: boolean;
781
981
  signal?: AbortSignal;
782
982
  }): Promise<OfflineSyncSnapshot> {
783
983
  throwIfOfflineSyncAborted(options.signal);
@@ -792,7 +992,11 @@ export async function buildOfflineSyncSnapshotForPaths(options: {
792
992
  const relPath = normalizeRelativePath(rawPath, "paths[]");
793
993
  if (seen.has(relPath)) continue;
794
994
  seen.add(relPath);
795
- if (shouldExcludeRelPath(relPath, includeTranscripts)) {
995
+ if (
996
+ options.excludeNodeLocalState === false
997
+ ? shouldExcludeRelPath(relPath, includeTranscripts)
998
+ : shouldExcludePushRelPath(relPath, includeTranscripts, options.userExcludeRegexps)
999
+ ) {
796
1000
  throw new Error(`offline sync snapshot path is excluded: ${relPath}`);
797
1001
  }
798
1002
  const filePath = await resolveSafeArchiveTarget(root, relPath);
@@ -830,12 +1034,25 @@ export async function readOfflineSyncFileContentChunk(options: {
830
1034
  length?: number;
831
1035
  includeTranscripts?: boolean;
832
1036
  readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
1037
+ userExcludeRegexps?: readonly RegExp[];
1038
+ /**
1039
+ * When false, enumeration uses only the legacy structural excludes —
1040
+ * the apply/pull-side view of local files. Push-side callers keep the
1041
+ * default (true): built-in node-local state excludes + user excludes.
1042
+ * (Cursor review on PR #1793: apply-side local enumeration must see
1043
+ * files like state/lcm.sqlite or merges misclassify them.)
1044
+ */
1045
+ excludeNodeLocalState?: boolean;
833
1046
  }): Promise<OfflineSyncFileContentChunk> {
834
1047
  const rootAbs = path.resolve(options.root);
835
1048
  const root = await prepareSafeArchiveRoot(rootAbs, "readOfflineSyncFileContentChunk", "root");
836
1049
  const includeTranscripts = options.includeTranscripts !== false;
837
1050
  const relPath = normalizeRelativePath(options.path, "path");
838
- if (shouldExcludeRelPath(relPath, includeTranscripts)) {
1051
+ if (
1052
+ options.excludeNodeLocalState === false
1053
+ ? shouldExcludeRelPath(relPath, includeTranscripts)
1054
+ : shouldExcludePushRelPath(relPath, includeTranscripts, options.userExcludeRegexps)
1055
+ ) {
839
1056
  throw new Error(`offline sync file content path is excluded: ${relPath}`);
840
1057
  }
841
1058
  const offset = options.offset === undefined
@@ -906,6 +1123,15 @@ export async function buildOfflineSyncChangeset(options: {
906
1123
  excludePaths?: readonly string[];
907
1124
  includeTranscripts?: boolean;
908
1125
  now?: Date;
1126
+ userExcludeRegexps?: readonly RegExp[];
1127
+ /**
1128
+ * When false, enumeration uses only the legacy structural excludes —
1129
+ * the apply/pull-side view of local files. Push-side callers keep the
1130
+ * default (true): built-in node-local state excludes + user excludes.
1131
+ * (Cursor review on PR #1793: apply-side local enumeration must see
1132
+ * files like state/lcm.sqlite or merges misclassify them.)
1133
+ */
1134
+ excludeNodeLocalState?: boolean;
909
1135
  readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
910
1136
  readFileDigest?: (target: OfflineSyncFileTarget) => Promise<OfflineSyncFileDigest>;
911
1137
  }): Promise<OfflineSyncChangeset> {
@@ -918,6 +1144,7 @@ export async function buildOfflineSyncChangeset(options: {
918
1144
  includeContent: false,
919
1145
  includeTranscripts,
920
1146
  now: options.now,
1147
+ userExcludeRegexps: options.userExcludeRegexps,
921
1148
  readFile: options.readFile,
922
1149
  readFileDigest: options.readFileDigest,
923
1150
  });
@@ -929,6 +1156,7 @@ export async function buildOfflineSyncChangeset(options: {
929
1156
  excludePaths: options.excludePaths,
930
1157
  includeTranscripts,
931
1158
  now: options.now,
1159
+ userExcludeRegexps: options.userExcludeRegexps,
932
1160
  readFile: options.readFile,
933
1161
  });
934
1162
  }
@@ -941,6 +1169,15 @@ export async function buildOfflineSyncChangesetFromSnapshot(options: {
941
1169
  excludePaths?: readonly string[];
942
1170
  includeTranscripts?: boolean;
943
1171
  now?: Date;
1172
+ userExcludeRegexps?: readonly RegExp[];
1173
+ /**
1174
+ * When false, enumeration uses only the legacy structural excludes —
1175
+ * the apply/pull-side view of local files. Push-side callers keep the
1176
+ * default (true): built-in node-local state excludes + user excludes.
1177
+ * (Cursor review on PR #1793: apply-side local enumeration must see
1178
+ * files like state/lcm.sqlite or merges misclassify them.)
1179
+ */
1180
+ excludeNodeLocalState?: boolean;
944
1181
  readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
945
1182
  }): Promise<OfflineSyncChangeset> {
946
1183
  const includeTranscripts = options.includeTranscripts !== false;
@@ -959,6 +1196,9 @@ export async function buildOfflineSyncChangesetFromSnapshot(options: {
959
1196
 
960
1197
  for (const relPath of unionPaths(base, currentMap)) {
961
1198
  if (excludedPaths.has(relPath)) continue;
1199
+ // Issue #1786: node-local runtime state must never appear in a push-side
1200
+ // changeset — each node rebuilds it from synced records.
1201
+ if (shouldExcludePushRelPath(relPath, includeTranscripts, options.userExcludeRegexps)) continue;
962
1202
  // Runtime state is remote-authoritative in offline sync: local edits and
963
1203
  // deletes are not pushed; the pull phase restores or removes these files
964
1204
  // from the remote snapshot.
@@ -973,6 +1213,7 @@ export async function buildOfflineSyncChangesetFromSnapshot(options: {
973
1213
  includeContent: true,
974
1214
  includeTranscripts,
975
1215
  now: options.now,
1216
+ userExcludeRegexps: options.userExcludeRegexps,
976
1217
  readFile: options.readFile,
977
1218
  });
978
1219
  const record = file.files[0];
@@ -1025,6 +1266,15 @@ export async function summarizeOfflineSyncPendingChanges(options: {
1025
1266
  baseCapturedAt?: Date;
1026
1267
  includeTranscripts?: boolean;
1027
1268
  now?: Date;
1269
+ userExcludeRegexps?: readonly RegExp[];
1270
+ /**
1271
+ * When false, enumeration uses only the legacy structural excludes —
1272
+ * the apply/pull-side view of local files. Push-side callers keep the
1273
+ * default (true): built-in node-local state excludes + user excludes.
1274
+ * (Cursor review on PR #1793: apply-side local enumeration must see
1275
+ * files like state/lcm.sqlite or merges misclassify them.)
1276
+ */
1277
+ excludeNodeLocalState?: boolean;
1028
1278
  readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
1029
1279
  readFileDigest?: (target: OfflineSyncFileTarget) => Promise<OfflineSyncFileDigest>;
1030
1280
  }): Promise<OfflineSyncChangesetSummary> {
@@ -1037,6 +1287,7 @@ export async function summarizeOfflineSyncPendingChanges(options: {
1037
1287
  includeContent: false,
1038
1288
  includeTranscripts,
1039
1289
  now: options.now,
1290
+ userExcludeRegexps: options.userExcludeRegexps,
1040
1291
  readFile: options.readFile,
1041
1292
  readFileDigest: options.readFileDigest,
1042
1293
  });
@@ -1044,6 +1295,7 @@ export async function summarizeOfflineSyncPendingChanges(options: {
1044
1295
  baseFiles: options.baseFiles,
1045
1296
  currentFiles: current.files,
1046
1297
  includeTranscripts,
1298
+ userExcludeRegexps: options.userExcludeRegexps,
1047
1299
  });
1048
1300
  }
1049
1301
 
@@ -1051,6 +1303,15 @@ export function summarizeOfflineSyncPendingFiles(options: {
1051
1303
  baseFiles?: readonly OfflineSyncFileState[];
1052
1304
  currentFiles: readonly OfflineSyncFileState[];
1053
1305
  includeTranscripts?: boolean;
1306
+ userExcludeRegexps?: readonly RegExp[];
1307
+ /**
1308
+ * When false, enumeration uses only the legacy structural excludes —
1309
+ * the apply/pull-side view of local files. Push-side callers keep the
1310
+ * default (true): built-in node-local state excludes + user excludes.
1311
+ * (Cursor review on PR #1793: apply-side local enumeration must see
1312
+ * files like state/lcm.sqlite or merges misclassify them.)
1313
+ */
1314
+ excludeNodeLocalState?: boolean;
1054
1315
  }): OfflineSyncChangesetSummary {
1055
1316
  const includeTranscripts = options.includeTranscripts !== false;
1056
1317
  const base = byPath(filterBaseFilesForMode(
@@ -1063,9 +1324,9 @@ export function summarizeOfflineSyncPendingFiles(options: {
1063
1324
  ));
1064
1325
  let upserts = 0;
1065
1326
  let deletes = 0;
1066
-
1067
1327
  for (const relPath of unionPaths(base, currentMap)) {
1068
1328
  if (shouldPreferIncomingOfflineRuntimeFile(relPath)) continue;
1329
+ if (shouldExcludePushRelPath(relPath, includeTranscripts, options.userExcludeRegexps)) continue;
1069
1330
  const baseEntry = base.get(relPath);
1070
1331
  const currentEntry = currentMap.get(relPath);
1071
1332
  if (currentEntry && currentEntry.sha256 !== baseEntry?.sha256) {
@@ -1116,6 +1377,7 @@ export async function applyOfflineSyncSnapshot(options: {
1116
1377
  includeTranscripts: snapshot.includeTranscripts,
1117
1378
  readFile: options.readFile,
1118
1379
  readFileDigest: options.readFileDigest,
1380
+ excludeNodeLocalState: false,
1119
1381
  })).files;
1120
1382
  const currentMap = byPath(currentFiles);
1121
1383
  const deferredPaths = new Set(options.deferredPaths ?? []);
@@ -1222,6 +1484,16 @@ export async function applyOfflineSyncSnapshot(options: {
1222
1484
  continue;
1223
1485
  }
1224
1486
 
1487
+ // Node-local runtime state (#1786): a push-filtered remote snapshot
1488
+ // never carries these paths, so their absence is NOT a delete
1489
+ // instruction. Drop any stale base entry (pre-#1786 state files still
1490
+ // carry them) and leave the local file untouched — deleting a live
1491
+ // sqlite WAL/SHM here would corrupt the local LCM store.
1492
+ if (matchesOfflineSyncDefaultExclude(relPath)) {
1493
+ nextBase.delete(relPath);
1494
+ skipped += 1;
1495
+ continue;
1496
+ }
1225
1497
  if (!currentEntry) {
1226
1498
  nextBase.delete(relPath);
1227
1499
  skipped += 1;
@@ -1308,6 +1580,7 @@ export async function applyOfflineSyncChangeset(options: {
1308
1580
  includeTranscripts: changeset.includeTranscripts,
1309
1581
  readFile: options.readFile,
1310
1582
  readFileDigest: options.readFileDigest,
1583
+ excludeNodeLocalState: false,
1311
1584
  })).files;
1312
1585
  const currentMap = byPath(currentFiles);
1313
1586
  const conflicts: OfflineSyncConflict[] = [];
@@ -1401,6 +1674,7 @@ export async function applyOfflineSyncChangeset(options: {
1401
1674
  includeTranscripts: changeset.includeTranscripts,
1402
1675
  readFile: options.readFile,
1403
1676
  readFileDigest: options.readFileDigest,
1677
+ excludeNodeLocalState: false,
1404
1678
  })).files,
1405
1679
  ...(options.returnCurrentFiles === false ? { currentFilesComplete: false } : {}),
1406
1680
  };
@@ -1527,6 +1801,48 @@ async function setSafeFileMtime(
1527
1801
  return true;
1528
1802
  }
1529
1803
 
1804
+ /**
1805
+ * Read the local file digest for a single relative path without applying
1806
+ * push-side exclude rules. Used by apply-side helpers that need to check
1807
+ * the current local state of a path that the push-side exclude would
1808
+ * reject (e.g. `state/lcm.sqlite`). Returns `null` if the file does not
1809
+ * exist, is not a regular file, or is a symlink.
1810
+ */
1811
+ async function readLocalFileState(options: {
1812
+ rootAbs: string;
1813
+ relPath: string;
1814
+ readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;
1815
+ readFileDigest?: (target: OfflineSyncFileTarget) => Promise<OfflineSyncFileDigest>;
1816
+ signal?: AbortSignal;
1817
+ }): Promise<OfflineSyncFileState | null> {
1818
+ const filePath = await resolveSafeArchiveTarget(
1819
+ await prepareSafeArchiveRoot(options.rootAbs, "readLocalFileState", "rootAbs"),
1820
+ options.relPath,
1821
+ );
1822
+ const st = await lstat(filePath).catch((error: unknown) => {
1823
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
1824
+ throw error;
1825
+ });
1826
+ if (!st || st.isSymbolicLink() || !st.isFile()) return null;
1827
+ throwIfOfflineSyncAborted(options.signal);
1828
+ let digest: OfflineSyncFileDigest;
1829
+ if (options.readFileDigest) {
1830
+ digest = await options.readFileDigest({ root: options.rootAbs, path: options.relPath, filePath });
1831
+ } else if (options.readFile) {
1832
+ const content = await options.readFile({ root: options.rootAbs, path: options.relPath, filePath });
1833
+ digest = sha256Buffer(content);
1834
+ } else {
1835
+ digest = await sha256File(filePath, options.signal);
1836
+ }
1837
+ throwIfOfflineSyncAborted(options.signal);
1838
+ return {
1839
+ path: options.relPath,
1840
+ sha256: digest.sha256,
1841
+ bytes: digest.bytes,
1842
+ mtimeMs: st.mtimeMs,
1843
+ };
1844
+ }
1845
+
1530
1846
  export async function applyOfflineSyncFileContentChunk(options: {
1531
1847
  root: string;
1532
1848
  sourceId: string;
@@ -1630,17 +1946,13 @@ export async function applyOfflineSyncFileContentChunk(options: {
1630
1946
  };
1631
1947
  if (offset === 0) {
1632
1948
  await pruneOfflineUploadStaging(root);
1633
- const currentSnapshot = await buildOfflineSyncSnapshotForPaths({
1634
- root: root.abs,
1635
- sourceId: "local",
1636
- paths: [relPath],
1637
- includeContent: false,
1638
- includeTranscripts,
1949
+ const currentFile = await readLocalFileState({
1950
+ rootAbs: root.abs,
1951
+ relPath,
1639
1952
  readFile: options.readFile,
1640
1953
  readFileDigest: options.readFileDigest,
1641
1954
  });
1642
- const currentFile = currentSnapshot.files[0];
1643
- if (currentFile?.sha256 === sha256) {
1955
+ if (currentFile && currentFile.sha256 === sha256) {
1644
1956
  await setSafeFileMtime(root, relPath, mtimeMs);
1645
1957
  return {
1646
1958
  ...baseResult,
@@ -1695,16 +2007,12 @@ export async function applyOfflineSyncFileContentChunk(options: {
1695
2007
  throw new Error(`offline sync upload checksum mismatch for ${relPath}`);
1696
2008
  }
1697
2009
 
1698
- const currentSnapshot = await buildOfflineSyncSnapshotForPaths({
1699
- root: root.abs,
1700
- sourceId: "local",
1701
- paths: [relPath],
1702
- includeContent: false,
1703
- includeTranscripts,
2010
+ const currentFile = await readLocalFileState({
2011
+ rootAbs: root.abs,
2012
+ relPath,
1704
2013
  readFile: options.readFile,
1705
2014
  readFileDigest: options.readFileDigest,
1706
2015
  });
1707
- const currentFile = currentSnapshot.files[0];
1708
2016
  const uploadedState: OfflineSyncFileState = {
1709
2017
  path: relPath,
1710
2018
  sha256,
@@ -1713,7 +2021,7 @@ export async function applyOfflineSyncFileContentChunk(options: {
1713
2021
  };
1714
2022
 
1715
2023
  try {
1716
- if (currentFile?.sha256 === sha256) {
2024
+ if (currentFile && currentFile.sha256 === sha256) {
1717
2025
  await setSafeFileMtime(root, relPath, mtimeMs);
1718
2026
  return {
1719
2027
  ...baseResult,
@@ -44,7 +44,6 @@ import {
44
44
  type GraphConstructionCapabilitySet,
45
45
  type MemoryLifecycleCapabilitySet,
46
46
  } from "../capabilities.js";
47
- import { throwIfAborted as sharedThrowIfAborted, abortError as sharedAbortError } from "../abort-error.js";
48
47
  import { parseFlexibleIsoTimestamp } from "../utils/iso-timestamp.js";
49
48
  import { log } from "../logger.js";
50
49
  import type {
@@ -122,39 +121,10 @@ export interface ExtractionRunCoordinatorDeps {
122
121
  ) => Promise<void>;
123
122
  }
124
123
 
125
- // Recall/extraction-specific abort helpers. Thin wrappers over the shared
126
- // `abort-error.ts` module.
127
- const abortRecallError = sharedAbortError;
128
-
129
- function throwIfRecallAborted(
130
- signal?: AbortSignal,
131
- message = "recall aborted",
132
- ): void {
133
- sharedThrowIfAborted(signal, message);
134
- }
135
-
136
- async function raceRecallAbort<T>(
137
- promise: Promise<T>,
138
- signal?: AbortSignal,
139
- message = "recall aborted",
140
- ): Promise<T> {
141
- throwIfRecallAborted(signal, message);
142
- if (!signal) return promise;
143
-
144
- let onAbort: (() => void) | null = null;
145
- const abortPromise = new Promise<T>((_resolve, reject) => {
146
- onAbort = () => reject(abortRecallError(message));
147
- signal.addEventListener("abort", onAbort, { once: true });
148
- });
149
-
150
- try {
151
- return await Promise.race([promise, abortPromise]);
152
- } finally {
153
- if (onAbort) {
154
- signal.removeEventListener("abort", onAbort);
155
- }
156
- }
157
- }
124
+ // Recall/extraction abort helpers live in orchestrator-helpers.ts since
125
+ // #1526 seam 25; this file previously carried duplicate copies
126
+ // (AGENTS.md rule 8 — no duplicated helpers).
127
+ import { raceRecallAbort, throwIfRecallAborted } from "./orchestrator-helpers.js";
158
128
 
159
129
  export function deriveTopicsFromExtraction(result: ExtractionResult): string[] {
160
130
  const topics = new Set<string>();