@remnic/core 9.35.1 → 9.35.3

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 (224) hide show
  1. package/dist/access-admin-ops-surface.d.ts +5 -5
  2. package/dist/access-admin-ops-surface.js +19 -18
  3. package/dist/access-authorization-probe.d.ts +5 -5
  4. package/dist/access-authorization-probe.js +20 -19
  5. package/dist/access-boundary.d.ts +5 -5
  6. package/dist/access-boundary.js +19 -18
  7. package/dist/access-cli.js +43 -42
  8. package/dist/access-cli.js.map +1 -1
  9. package/dist/access-extraction-force-flush.d.ts +5 -5
  10. package/dist/access-extraction-force-flush.js +19 -18
  11. package/dist/access-http-lcm-compaction.d.ts +5 -5
  12. package/dist/access-http-lifecycle-flush.d.ts +5 -5
  13. package/dist/access-http.d.ts +5 -5
  14. package/dist/access-http.js +24 -23
  15. package/dist/access-identity-continuity-surface.d.ts +4 -4
  16. package/dist/access-identity-continuity-surface.js +19 -18
  17. package/dist/access-lcm-surface.d.ts +5 -5
  18. package/dist/access-lcm-surface.js +19 -18
  19. package/dist/access-mcp.d.ts +5 -5
  20. package/dist/access-mcp.js +22 -21
  21. package/dist/access-namespace-preflight.js +19 -18
  22. package/dist/access-observe-write-surface.d.ts +5 -5
  23. package/dist/access-observe-write-surface.js +19 -18
  24. package/dist/access-operations-batch.js +20 -19
  25. package/dist/access-operations.d.ts +8 -8
  26. package/dist/access-operations.js +21 -20
  27. package/dist/access-recall-concurrency.d.ts +5 -5
  28. package/dist/access-recall-concurrency.js +19 -18
  29. package/dist/access-recall-response.d.ts +5 -5
  30. package/dist/access-recall-response.js +19 -18
  31. package/dist/access-recall-surface.d.ts +5 -5
  32. package/dist/access-recall-surface.js +19 -18
  33. package/dist/access-schema.d.ts +88 -88
  34. package/dist/{access-service-D9zIBlpp.d.ts → access-service-C2WYT1pO.d.ts} +3 -3
  35. package/dist/access-service-helpers.d.ts +5 -5
  36. package/dist/access-service.d.ts +5 -5
  37. package/dist/access-service.js +19 -18
  38. package/dist/access-surface-catalog.d.ts +5 -5
  39. package/dist/access-wearables-meetings-surface.d.ts +2 -2
  40. package/dist/bootstrap.d.ts +4 -4
  41. package/dist/briefing.d.ts +1 -1
  42. package/dist/briefing.js +10 -9
  43. package/dist/buffer.d.ts +1 -1
  44. package/dist/{capsule-merge-2H2IUTBT.js → capsule-merge-IDWJPCWK.js} +77 -8
  45. package/dist/capsule-merge-IDWJPCWK.js.map +1 -0
  46. package/dist/causal-consolidation.js +11 -10
  47. package/dist/causal-consolidation.js.map +1 -1
  48. package/dist/{chunk-65ZPVYTA.js → chunk-3CE77VAP.js} +55 -5
  49. package/dist/chunk-3CE77VAP.js.map +1 -0
  50. package/dist/chunk-4J7G5SY4.js +338 -0
  51. package/dist/chunk-4J7G5SY4.js.map +1 -0
  52. package/dist/{chunk-EO7X2LDK.js → chunk-4TAD3B56.js} +2 -2
  53. package/dist/{chunk-QHYB4MJV.js → chunk-5ZGHUWTT.js} +2 -2
  54. package/dist/{chunk-TCIWHK3N.js → chunk-6D5D5A4M.js} +5 -5
  55. package/dist/{chunk-BIXWNHIL.js → chunk-6TF4M2LB.js} +5 -5
  56. package/dist/{chunk-2NUC473W.js → chunk-AKELMJO2.js} +220 -48
  57. package/dist/{chunk-2NUC473W.js.map → chunk-AKELMJO2.js.map} +1 -1
  58. package/dist/{chunk-FBUE6PD3.js → chunk-BEQOZOYZ.js} +21 -4
  59. package/dist/chunk-BEQOZOYZ.js.map +1 -0
  60. package/dist/{chunk-SA3WJJF2.js → chunk-BGLDKAYY.js} +2 -2
  61. package/dist/{chunk-OPRP6Q4Z.js → chunk-DKKZ6SO2.js} +3 -3
  62. package/dist/{chunk-6TAUHL33.js → chunk-DLVPUX7U.js} +10 -2
  63. package/dist/chunk-DLVPUX7U.js.map +1 -0
  64. package/dist/{chunk-QNWXCJ76.js → chunk-ENTCVTQR.js} +18 -1
  65. package/dist/{chunk-QNWXCJ76.js.map → chunk-ENTCVTQR.js.map} +1 -1
  66. package/dist/{chunk-AEPYA55F.js → chunk-FE7MUX7M.js} +2 -2
  67. package/dist/{chunk-NQUF7XKR.js → chunk-FT4RF53O.js} +2 -2
  68. package/dist/{chunk-GHR7WHZX.js → chunk-GORHSN5C.js} +2 -2
  69. package/dist/{chunk-7TEZUUGM.js → chunk-HUS2XN3T.js} +2 -2
  70. package/dist/{chunk-XWDZQIYU.js → chunk-IJAQCBGV.js} +2 -2
  71. package/dist/{chunk-TJLLYV2X.js → chunk-IMEXE4XS.js} +7 -7
  72. package/dist/{chunk-ZCPP5OW6.js → chunk-JQSBRLJ6.js} +31 -27
  73. package/dist/chunk-JQSBRLJ6.js.map +1 -0
  74. package/dist/{chunk-IZXTZ7BF.js → chunk-JYLN6KHH.js} +6 -6
  75. package/dist/{chunk-HEVWWPTL.js → chunk-LH3KXIBJ.js} +17 -17
  76. package/dist/{chunk-EH5U2YCX.js → chunk-M3DBB2ND.js} +2 -2
  77. package/dist/{chunk-XABS455Y.js → chunk-NBAEGFXT.js} +5 -5
  78. package/dist/{chunk-DP7ZV2II.js → chunk-OA6PIPDE.js} +2 -2
  79. package/dist/{chunk-T4HH5Q3U.js → chunk-OJRLTZQM.js} +3 -3
  80. package/dist/{chunk-7ZHGPX56.js → chunk-PRP3M5Y3.js} +2 -2
  81. package/dist/{chunk-Y2ZD2RVG.js → chunk-PYTUBWAS.js} +2 -2
  82. package/dist/{chunk-4TGZCYHH.js → chunk-Q3CVY7LV.js} +1954 -1642
  83. package/dist/chunk-Q3CVY7LV.js.map +1 -0
  84. package/dist/{chunk-PH3ZWL3W.js → chunk-Q53RLEW7.js} +2 -2
  85. package/dist/{chunk-Y6OWJ3V5.js → chunk-RY2XZTFN.js} +2 -2
  86. package/dist/{chunk-2SHIWGY7.js → chunk-RY7CHVGE.js} +2 -2
  87. package/dist/{chunk-7VUAG236.js → chunk-T3FVAHM7.js} +4 -4
  88. package/dist/{chunk-2LTDWSR6.js → chunk-TSH56VPU.js} +2 -2
  89. package/dist/{chunk-J6SLNQRO.js → chunk-UBWOCQ3I.js} +6 -2
  90. package/dist/chunk-UBWOCQ3I.js.map +1 -0
  91. package/dist/{chunk-5FHE4JI7.js → chunk-VEUVON5I.js} +2 -2
  92. package/dist/{chunk-KEWUNL7P.js → chunk-WET4M3R6.js} +2 -2
  93. package/dist/chunk-WKDFJXW5.js +158 -0
  94. package/dist/chunk-WKDFJXW5.js.map +1 -0
  95. package/dist/{chunk-SLPLPGJQ.js → chunk-WL2S2UBB.js} +5 -5
  96. package/dist/{chunk-AHZYNPLX.js → chunk-YVMVVE4B.js} +2 -2
  97. package/dist/{chunk-5HTAJCTO.js → chunk-ZB3YODID.js} +15 -15
  98. package/dist/{cli-mFX-QQvx.d.ts → cli-jkmzdT8P.d.ts} +3 -3
  99. package/dist/cli.d.ts +6 -6
  100. package/dist/cli.js +34 -33
  101. package/dist/compounding/engine.d.ts +1 -1
  102. package/dist/compounding/engine.js +10 -9
  103. package/dist/connectors/codex-materialize-runner.js +10 -9
  104. package/dist/connectors/index.js +10 -9
  105. package/dist/consolidation-provenance-check.d.ts +1 -1
  106. package/dist/consolidation-provenance-check.js +8 -2
  107. package/dist/consolidation-undo.d.ts +1 -1
  108. package/dist/consolidation-undo.js +10 -1
  109. package/dist/consolidation-undo.js.map +1 -1
  110. package/dist/contradiction/index.d.ts +1 -1
  111. package/dist/corpus-watermark.js +12 -11
  112. package/dist/entity-retrieval.d.ts +1 -1
  113. package/dist/entity-retrieval.js +10 -9
  114. package/dist/explicit-capture.d.ts +4 -4
  115. package/dist/explicit-capture.js +6 -3
  116. package/dist/extraction.js +13 -12
  117. package/dist/{forget-4UY2EOKH.js → forget-DNAJ66ZD.js} +2 -2
  118. package/dist/index.d.ts +398 -398
  119. package/dist/index.js +43 -42
  120. package/dist/local-llm-helpers.d.ts +63 -1
  121. package/dist/local-llm-helpers.js +10 -1
  122. package/dist/local-llm.d.ts +4 -1
  123. package/dist/local-llm.js +2 -2
  124. package/dist/maintenance/memory-governance.js +10 -9
  125. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
  126. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -9
  127. package/dist/maintenance/rebuild-memory-projection.d.ts +1 -1
  128. package/dist/maintenance/rebuild-memory-projection.js +11 -10
  129. package/dist/{maintenance-70Wq118b.d.ts → maintenance-Ha9Dg93M.d.ts} +1 -1
  130. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  131. package/dist/memory-worth-outcomes.d.ts +1 -1
  132. package/dist/namespaces/migrate.d.ts +1 -1
  133. package/dist/namespaces/migrate.js +11 -10
  134. package/dist/namespaces/storage.d.ts +1 -1
  135. package/dist/namespaces/storage.js +10 -9
  136. package/dist/offline-sync-impression-drain.js +3 -3
  137. package/dist/operator-doctor-corpus.js +13 -12
  138. package/dist/operator-toolkit.d.ts +1 -1
  139. package/dist/operator-toolkit.js +17 -16
  140. package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
  141. package/dist/orchestration/maintenance.d.ts +2 -2
  142. package/dist/orchestration/maintenance.js +15 -14
  143. package/dist/{orchestrator-BH8LT-NX.d.ts → orchestrator-DV_u9bfx.d.ts} +3 -3
  144. package/dist/orchestrator.d.ts +4 -4
  145. package/dist/orchestrator.js +43 -42
  146. package/dist/page-versioning.js +7 -1
  147. package/dist/schemas.d.ts +76 -76
  148. package/dist/semantic-consolidation.js +11 -10
  149. package/dist/semantic-rule-promotion.js +10 -9
  150. package/dist/semantic-rule-verifier.js +10 -9
  151. package/dist/{service-CrxXHz5T.d.ts → service-JxXXY-Eh.d.ts} +1 -1
  152. package/dist/shared-context/manager.d.ts +8 -8
  153. package/dist/source-agent-qualifier.js +10 -9
  154. package/dist/{storage-CyWa1Jm6.d.ts → storage-BbH4HDUS.d.ts} +30 -26
  155. package/dist/storage.d.ts +1 -1
  156. package/dist/storage.js +9 -8
  157. package/dist/summarizer.js +3 -3
  158. package/dist/temporal-supersession.d.ts +1 -1
  159. package/dist/tier-migration.d.ts +1 -1
  160. package/dist/transfer/capsule-import.js +8 -2
  161. package/dist/transfer/types.d.ts +66 -66
  162. package/dist/verified-recall.js +10 -9
  163. package/package.json +2 -2
  164. package/src/binary-lifecycle/pipeline.ts +52 -1
  165. package/src/consolidation-undo.ts +4 -0
  166. package/src/curation/index.ts +41 -5
  167. package/src/lifecycle/tombstones.ts +40 -0
  168. package/src/local-llm-helpers.ts +171 -0
  169. package/src/local-llm.ts +34 -30
  170. package/src/maintenance/memory-governance.ts +4 -0
  171. package/src/page-versioning.ts +16 -1
  172. package/src/review/index.ts +97 -13
  173. package/src/spaces/index.ts +127 -6
  174. package/src/storage/citation-hash-source.ts +97 -0
  175. package/src/storage/entity-canonical-id-lock.ts +33 -0
  176. package/src/storage/entity-canonical-id-migration-adapter.ts +8 -4
  177. package/src/storage/entity-canonical-id-migration.ts +129 -77
  178. package/src/storage/entity-canonical-id-references.ts +557 -0
  179. package/src/storage/entity-ref-repair.ts +171 -0
  180. package/src/storage/entity-store.ts +163 -2
  181. package/src/storage/tombstone-blocked-capture-sync.ts +18 -0
  182. package/src/storage.ts +363 -396
  183. package/src/transfer/capsule-import.ts +91 -3
  184. package/src/transfer/capsule-merge.ts +128 -10
  185. package/dist/capsule-merge-2H2IUTBT.js.map +0 -1
  186. package/dist/chunk-4RNZMRAV.js +0 -60
  187. package/dist/chunk-4RNZMRAV.js.map +0 -1
  188. package/dist/chunk-4TGZCYHH.js.map +0 -1
  189. package/dist/chunk-65ZPVYTA.js.map +0 -1
  190. package/dist/chunk-6TAUHL33.js.map +0 -1
  191. package/dist/chunk-FBUE6PD3.js.map +0 -1
  192. package/dist/chunk-J6SLNQRO.js.map +0 -1
  193. package/dist/chunk-ZCPP5OW6.js.map +0 -1
  194. /package/dist/{chunk-EO7X2LDK.js.map → chunk-4TAD3B56.js.map} +0 -0
  195. /package/dist/{chunk-QHYB4MJV.js.map → chunk-5ZGHUWTT.js.map} +0 -0
  196. /package/dist/{chunk-TCIWHK3N.js.map → chunk-6D5D5A4M.js.map} +0 -0
  197. /package/dist/{chunk-BIXWNHIL.js.map → chunk-6TF4M2LB.js.map} +0 -0
  198. /package/dist/{chunk-SA3WJJF2.js.map → chunk-BGLDKAYY.js.map} +0 -0
  199. /package/dist/{chunk-OPRP6Q4Z.js.map → chunk-DKKZ6SO2.js.map} +0 -0
  200. /package/dist/{chunk-AEPYA55F.js.map → chunk-FE7MUX7M.js.map} +0 -0
  201. /package/dist/{chunk-NQUF7XKR.js.map → chunk-FT4RF53O.js.map} +0 -0
  202. /package/dist/{chunk-GHR7WHZX.js.map → chunk-GORHSN5C.js.map} +0 -0
  203. /package/dist/{chunk-7TEZUUGM.js.map → chunk-HUS2XN3T.js.map} +0 -0
  204. /package/dist/{chunk-XWDZQIYU.js.map → chunk-IJAQCBGV.js.map} +0 -0
  205. /package/dist/{chunk-TJLLYV2X.js.map → chunk-IMEXE4XS.js.map} +0 -0
  206. /package/dist/{chunk-IZXTZ7BF.js.map → chunk-JYLN6KHH.js.map} +0 -0
  207. /package/dist/{chunk-HEVWWPTL.js.map → chunk-LH3KXIBJ.js.map} +0 -0
  208. /package/dist/{chunk-EH5U2YCX.js.map → chunk-M3DBB2ND.js.map} +0 -0
  209. /package/dist/{chunk-XABS455Y.js.map → chunk-NBAEGFXT.js.map} +0 -0
  210. /package/dist/{chunk-DP7ZV2II.js.map → chunk-OA6PIPDE.js.map} +0 -0
  211. /package/dist/{chunk-T4HH5Q3U.js.map → chunk-OJRLTZQM.js.map} +0 -0
  212. /package/dist/{chunk-7ZHGPX56.js.map → chunk-PRP3M5Y3.js.map} +0 -0
  213. /package/dist/{chunk-Y2ZD2RVG.js.map → chunk-PYTUBWAS.js.map} +0 -0
  214. /package/dist/{chunk-PH3ZWL3W.js.map → chunk-Q53RLEW7.js.map} +0 -0
  215. /package/dist/{chunk-Y6OWJ3V5.js.map → chunk-RY2XZTFN.js.map} +0 -0
  216. /package/dist/{chunk-2SHIWGY7.js.map → chunk-RY7CHVGE.js.map} +0 -0
  217. /package/dist/{chunk-7VUAG236.js.map → chunk-T3FVAHM7.js.map} +0 -0
  218. /package/dist/{chunk-2LTDWSR6.js.map → chunk-TSH56VPU.js.map} +0 -0
  219. /package/dist/{chunk-5FHE4JI7.js.map → chunk-VEUVON5I.js.map} +0 -0
  220. /package/dist/{chunk-KEWUNL7P.js.map → chunk-WET4M3R6.js.map} +0 -0
  221. /package/dist/{chunk-SLPLPGJQ.js.map → chunk-WL2S2UBB.js.map} +0 -0
  222. /package/dist/{chunk-AHZYNPLX.js.map → chunk-YVMVVE4B.js.map} +0 -0
  223. /package/dist/{chunk-5HTAJCTO.js.map → chunk-ZB3YODID.js.map} +0 -0
  224. /package/dist/{forget-4UY2EOKH.js.map → forget-DNAJ66ZD.js.map} +0 -0
@@ -1,8 +1,16 @@
1
- import { lstat, mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
1
+ import { lstat, mkdir, readFile, realpath, stat, unlink, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import { gunzipSync } from "node:zlib";
5
5
  import { bumpMemoryCorpusVersionForDir } from "../memory-corpus-version.js";
6
+ import {
7
+ HistoricalEntityCanonicalIdCache,
8
+ readEntityRefFromFrontmatter,
9
+ canonicalizeEntityRefFrontmatter,
10
+ classifyEntityRefWritePath,
11
+ repairContentAfterJournalMove,
12
+ requestEntityCanonicalIdReconcile,
13
+ } from "../storage/entity-canonical-id-references.js";
6
14
  import {
7
15
  createVersion,
8
16
  type VersioningConfig,
@@ -381,6 +389,26 @@ export async function importCapsule(
381
389
  // atomic (mkdir + writeFile pair).
382
390
  const imported: ImportCapsuleImportedRecord[] = [];
383
391
  const skipped: ImportCapsuleSkippedRecord[] = [];
392
+ // The target's completed migration journal is a write boundary here too
393
+ // (issue #2213): a capsule can carry pre-migration memories whose entityRef
394
+ // the target already renamed, and no recurring reconciliation pass exists
395
+ // to absorb a stale ref after the fact. Read through the identity-keyed
396
+ // cache PER RECORD so a peer migration completing mid-import is picked up
397
+ // for every record written after it (one lstat per record).
398
+ const journalStateDir = path.join(rootReal, "state");
399
+ const historicalIdCache = new HistoricalEntityCanonicalIdCache();
400
+ // Entity pages in THIS capsule can re-establish collisions the target's
401
+ // journal already resolved (Codex P1): once the next migration parks the
402
+ // mapping, a memory canonicalized onto the canonical claimant is
403
+ // permanently misattributed. Memories whose ref targets a capsule-supplied
404
+ // entity id import byte-faithful behind the reconcile marker instead; the
405
+ // migration settles the collision and rewrites references per its verdict.
406
+ const capsuleEntityIds = new Set(
407
+ bundle.records
408
+ .map((record) => computeTargetPath(record.path, mode, capsule.id))
409
+ .filter((rel) => classifyEntityRefWritePath(rootReal, path.join(rootReal, fromPosixRelPath(rel))) === "entity")
410
+ .map((rel) => path.basename(rel, ".md")),
411
+ );
384
412
 
385
413
  // Sort by source path so the imported/skipped lists are deterministic
386
414
  // regardless of bundle order. (`exportCapsule` already sorts; we re-sort
@@ -389,6 +417,11 @@ export async function importCapsule(
389
417
  a.path.localeCompare(b.path),
390
418
  );
391
419
 
420
+ // The reconcile marker is published ONCE, after every record landed
421
+ // (Codex P1, round 21): a per-record marker could be consumed by a peer
422
+ // between a legacy-preserving memory write and its colliding entity page,
423
+ // canonicalizing the memory onto the wrong claimant irreversibly.
424
+ let reconcileNeeded = false;
392
425
  for (const rec of sortedRecords) {
393
426
  const targetRel = computeTargetPath(rec.path, mode, capsule.id);
394
427
  // Use rootReal (realpath-resolved) to stay consistent with phase 1's
@@ -431,13 +464,62 @@ export async function importCapsule(
431
464
  let contentToWrite = rec.content;
432
465
  let rewroteId = false;
433
466
  if (mode === "fork") {
434
- const forked = rewriteFrontmatterIdForFork(rec.content, capsule.id, opts.now);
467
+ const forked = rewriteFrontmatterIdForFork(contentToWrite, capsule.id, opts.now);
435
468
  contentToWrite = forked.content;
436
469
  rewroteId = forked.rewrote;
437
470
  }
438
471
 
439
472
  await mkdir(path.dirname(targetAbs), { recursive: true });
440
- await writeFile(targetAbs, contentToWrite, "utf-8");
473
+ // Resolve at the write itself (mirrors capsule-merge): a peer migration
474
+ // can complete during the snapshot/mkdir awaits, and capsule writes bump
475
+ // only the corpus version, which the migration fingerprint ignores. The
476
+ // post-write REPAIR re-canonicalizes and rewrites when the journal moved
477
+ // across the write itself (a parked mapping cannot be fixed after the
478
+ // fact by the reconcile pass). Only memory-tier records canonicalize;
479
+ // entities/ pages carry relationship TARGETS only
480
+ // rewriteRelationshipTargets understands, so they import byte-faithful
481
+ // with a reconcile marker; everything else imports losslessly.
482
+ const targetKind = classifyEntityRefWritePath(rootReal, targetAbs);
483
+ const rawRef = targetKind === "memory" ? readEntityRefFromFrontmatter(contentToWrite) : null;
484
+ if (targetKind === "memory" && rawRef !== null && capsuleEntityIds.has(rawRef)) {
485
+ // Ref targets a capsule-supplied entity page (potential collision
486
+ // claimant): keep the legacy bytes; the DEFERRED marker below lets the
487
+ // migration settle the collision and rewrite per its verdict (Codex P1).
488
+ await writeFile(targetAbs, contentToWrite, "utf-8");
489
+ reconcileNeeded = true;
490
+ } else if (targetKind === "memory") {
491
+ const idsAtWrite = historicalIdCache.get(journalStateDir);
492
+ const rawRecord = contentToWrite;
493
+ contentToWrite = canonicalizeEntityRefFrontmatter(rawRecord, idsAtWrite);
494
+ // Snapshot the pre-write bytes: a repair failure after the write must
495
+ // restore them (or remove a newly created target), or the import
496
+ // reports failure while an overwrite already destroyed the prior state
497
+ // and the skipped corpus bump leaves warm caches stale (AGENTS.md §14).
498
+ const originalBytes = await readFile(targetAbs, "utf-8").catch(() => null);
499
+ await writeFile(targetAbs, contentToWrite, "utf-8");
500
+ try {
501
+ await repairContentAfterJournalMove({
502
+ stateDir: journalStateDir,
503
+ cache: historicalIdCache,
504
+ idsAtWrite,
505
+ rawContent: rawRecord,
506
+ lastWritten: contentToWrite,
507
+ rewrite: (content) => writeFile(targetAbs, content, "utf-8"),
508
+ });
509
+ } catch (err) {
510
+ if (originalBytes === null) {
511
+ await unlink(targetAbs).catch(() => undefined);
512
+ } else {
513
+ await writeFile(targetAbs, originalBytes, "utf-8").catch(() => undefined);
514
+ }
515
+ throw err;
516
+ }
517
+ } else {
518
+ await writeFile(targetAbs, contentToWrite, "utf-8");
519
+ if (targetKind === "entity") {
520
+ reconcileNeeded = true;
521
+ }
522
+ }
441
523
  // Bump the corpus sentinel per write (Cursor Medium, #1902): a concurrent
442
524
  // readAllMemories during the import must rescan and see records already on
443
525
  // disk, never the pre-import corpus, mid-batch.
@@ -450,6 +532,12 @@ export async function importCapsule(
450
532
  });
451
533
  }
452
534
 
535
+ // Every record — including every collision claimant entity page — is on
536
+ // disk; NOW the reconcile pass can settle collisions safely.
537
+ if (reconcileNeeded) {
538
+ await requestEntityCanonicalIdReconcile(journalStateDir);
539
+ }
540
+
453
541
  // Sort skipped for stable output (see comment above).
454
542
  skipped.sort((a, b) => a.path.localeCompare(b.path));
455
543
 
@@ -1,7 +1,15 @@
1
- import { lstat, mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
1
+ import { lstat, mkdir, readFile, realpath, stat, unlink, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { gunzipSync } from "node:zlib";
4
4
  import { bumpMemoryCorpusVersionForDir } from "../memory-corpus-version.js";
5
+ import {
6
+ HistoricalEntityCanonicalIdCache,
7
+ canonicalizeEntityRefFrontmatter,
8
+ readEntityRefFromFrontmatter,
9
+ classifyEntityRefWritePath,
10
+ repairContentAfterJournalMove,
11
+ requestEntityCanonicalIdReconcile,
12
+ } from "../storage/entity-canonical-id-references.js";
5
13
  import {
6
14
  createVersion,
7
15
  type VersioningConfig,
@@ -349,6 +357,83 @@ export async function mergeCapsule(
349
357
  const merged: MergeCapsuleWrittenRecord[] = [];
350
358
  const skipped: MergeCapsuleSkippedRecord[] = [];
351
359
  const conflicts: MergeCapsuleConflictRecord[] = [];
360
+ // Same write-boundary rule as capsule-import (issue #2213): records land
361
+ // with their entityRef resolved through the target's migration journal,
362
+ // read through the identity-keyed cache PER RECORD so a peer migration
363
+ // completing mid-merge is picked up for every record written after it.
364
+ const journalStateDir = path.join(rootReal, "state");
365
+ const historicalIdCache = new HistoricalEntityCanonicalIdCache();
366
+ // Entity pages in THIS capsule can re-establish collisions the target's
367
+ // journal already resolved (Codex P1): once the next migration parks the
368
+ // mapping, a memory canonicalized onto the canonical claimant is
369
+ // permanently misattributed. Memories whose ref targets a capsule-supplied
370
+ // entity id merge byte-faithful behind the reconcile marker instead.
371
+ const capsuleEntityIds = new Set(
372
+ bundle.records
373
+ .filter((record) =>
374
+ classifyEntityRefWritePath(rootReal, path.join(rootReal, fromPosixRelPath(record.path))) === "entity")
375
+ .map((record) => path.basename(record.path, ".md")),
376
+ );
377
+ // Write a record with its entityRef resolved AT the write, then repair if
378
+ // the journal moved across the write itself — a parked mapping cannot be
379
+ // fixed after the fact by the reconcile pass (issue #2213). Only memory-tier
380
+ // records canonicalize; entities/ pages carry relationship TARGETS only
381
+ // rewriteRelationshipTargets understands, so they merge byte-faithful with
382
+ // a reconcile marker; everything else merges losslessly. The corpus bump
383
+ // lands AFTER the repair so a warm cache rescan never captures pre-repair
384
+ // bytes.
385
+ // The reconcile marker is published ONCE, after every record landed
386
+ // (Codex P1, round 21): a per-record marker could be consumed by a peer
387
+ // between a legacy-preserving memory write and its colliding entity page,
388
+ // canonicalizing the memory onto the wrong claimant irreversibly.
389
+ let reconcileNeeded = false;
390
+ const writeCanonicalRecord = async (targetAbs: string, rawContent: string): Promise<void> => {
391
+ const kind = classifyEntityRefWritePath(rootReal, targetAbs);
392
+ const rawRef = kind === "memory" ? readEntityRefFromFrontmatter(rawContent) : null;
393
+ if (kind === "memory" && rawRef !== null && capsuleEntityIds.has(rawRef)) {
394
+ // Ref targets a capsule-supplied entity page (potential collision
395
+ // claimant): keep the legacy bytes; the deferred marker lets the
396
+ // migration settle the collision and rewrite per its verdict (Codex P1).
397
+ await writeFile(targetAbs, rawContent, "utf-8");
398
+ reconcileNeeded = true;
399
+ bumpMemoryCorpusVersionForDir(rootReal); // per-write bump (Cursor Medium, #1902)
400
+ return;
401
+ }
402
+ if (kind !== "memory") {
403
+ await writeFile(targetAbs, rawContent, "utf-8");
404
+ if (kind === "entity") {
405
+ reconcileNeeded = true;
406
+ }
407
+ bumpMemoryCorpusVersionForDir(rootReal); // per-write bump (Cursor Medium, #1902)
408
+ return;
409
+ }
410
+ const idsAtWrite = historicalIdCache.get(journalStateDir);
411
+ const written = canonicalizeEntityRefFrontmatter(rawContent, idsAtWrite);
412
+ // Snapshot the pre-write bytes: a repair failure after the write must
413
+ // restore them (or remove a newly created target) before propagating —
414
+ // the merge reports failure, so disk must return to its pre-write state
415
+ // and warm caches stay consistent (AGENTS.md §14).
416
+ const originalBytes = await readLocalFile(targetAbs);
417
+ await writeFile(targetAbs, written, "utf-8");
418
+ try {
419
+ await repairContentAfterJournalMove({
420
+ stateDir: journalStateDir,
421
+ cache: historicalIdCache,
422
+ idsAtWrite,
423
+ rawContent,
424
+ lastWritten: written,
425
+ rewrite: (content) => writeFile(targetAbs, content, "utf-8"),
426
+ });
427
+ } catch (err) {
428
+ if (originalBytes === null) {
429
+ await unlink(targetAbs).catch(() => undefined);
430
+ } else {
431
+ await writeFile(targetAbs, originalBytes, "utf-8").catch(() => undefined);
432
+ }
433
+ throw err;
434
+ }
435
+ bumpMemoryCorpusVersionForDir(rootReal); // per-write bump (Cursor Medium, #1902)
436
+ };
352
437
 
353
438
  // Sort by source path for deterministic output (mirrors capsule-import.ts).
354
439
  const sortedRecords = [...bundle.records].sort((a, b) =>
@@ -362,22 +447,48 @@ export async function mergeCapsule(
362
447
  const localContent = await readLocalFile(targetAbs);
363
448
 
364
449
  if (localContent === null) {
365
- // No local copy — always write regardless of mode.
450
+ // No local copy — always write regardless of mode. Resolve at the write
451
+ // itself and verify the journal afterwards: a peer migration completing
452
+ // during any of these awaits (or the write) would otherwise strand the
453
+ // reference — capsule writes bump only the corpus version, which the
454
+ // migration fingerprint deliberately ignores.
366
455
  await mkdir(path.dirname(targetAbs), { recursive: true });
367
- await writeFile(targetAbs, rec.content, "utf-8");
368
- bumpMemoryCorpusVersionForDir(rootReal); // per-write bump (Cursor Medium, #1902)
456
+ await writeCanonicalRecord(targetAbs, rec.content);
369
457
  merged.push({ sourcePath: rec.path, targetPath: rec.path, snapshotted: false });
370
458
  continue;
371
459
  }
372
460
 
373
- // Local file exists. Check if it is byte-identical to the archive entry.
461
+ // Local file exists. A local copy already holding the CANONICALIZED form
462
+ // (computed AFTER the local read, so a journal published during that
463
+ // await is reflected) is done. A local copy matching the RAW archive
464
+ // bytes while a mapping applies is UPGRADED in place — skipping would
465
+ // leave the legacy entityRef on disk forever, since corpus bumps no
466
+ // longer trigger migration rewrites (issue #2213).
467
+ const idsAtCompare = historicalIdCache.get(journalStateDir);
468
+ const compareRef = readEntityRefFromFrontmatter(rec.content);
469
+ const canonicalContent =
470
+ classifyEntityRefWritePath(rootReal, targetAbs) === "memory"
471
+ && !(compareRef !== null && capsuleEntityIds.has(compareRef))
472
+ ? canonicalizeEntityRefFrontmatter(rec.content, idsAtCompare)
473
+ : rec.content;
374
474
  const { sha256: localSha256 } = sha256String(localContent);
375
-
376
- if (localSha256 === entry.sha256) {
377
- // Byte-identical — no write needed.
475
+ const canonicalSha256 =
476
+ canonicalContent === rec.content ? entry.sha256 : sha256String(canonicalContent).sha256;
477
+
478
+ // The identical verdict is only valid for the snapshot it was computed
479
+ // under (Codex P1): a mapping parked/removed mid-compare can make the
480
+ // local copy's formerly-canonical ref stale with nothing left to repair
481
+ // it. A moved journal falls through to the write path, which resolves at
482
+ // the write and repairs after it.
483
+ if (localSha256 === canonicalSha256 && historicalIdCache.get(journalStateDir) === idsAtCompare) {
378
484
  skipped.push({ path: rec.path, reason: "identical" });
379
485
  continue;
380
486
  }
487
+ if (localSha256 === canonicalSha256 || localSha256 === entry.sha256) {
488
+ await writeCanonicalRecord(targetAbs, rec.content);
489
+ merged.push({ sourcePath: rec.path, targetPath: rec.path, snapshotted: false });
490
+ continue;
491
+ }
381
492
 
382
493
  // Content differs → conflict.
383
494
  const { sha256: archiveSha256 } = sha256String(rec.content);
@@ -409,11 +520,18 @@ export async function mergeCapsule(
409
520
  snapshotted = true;
410
521
  }
411
522
 
412
- await writeFile(targetAbs, rec.content, "utf-8");
413
- bumpMemoryCorpusVersionForDir(rootReal); // per-write bump (Cursor Medium, #1902)
523
+ // Same write-time re-resolve + post-write repair as above — the
524
+ // snapshot/read awaits give a peer migration a real window.
525
+ await writeCanonicalRecord(targetAbs, rec.content);
414
526
  merged.push({ sourcePath: rec.path, targetPath: rec.path, snapshotted });
415
527
  }
416
528
 
529
+ // Every record — including every collision claimant entity page — is on
530
+ // disk; NOW the reconcile pass can settle collisions safely.
531
+ if (reconcileNeeded) {
532
+ await requestEntityCanonicalIdReconcile(journalStateDir);
533
+ }
534
+
417
535
  // Sort output lists for determinism.
418
536
  merged.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
419
537
  skipped.sort((a, b) => a.path.localeCompare(b.path));
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/transfer/capsule-merge.ts"],"sourcesContent":["import { lstat, mkdir, readFile, realpath, stat, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { gunzipSync } from \"node:zlib\";\nimport { bumpMemoryCorpusVersionForDir } from \"../memory-corpus-version.js\";\nimport {\n createVersion,\n type VersioningConfig,\n type VersioningLogger,\n} from \"../page-versioning.js\";\nimport {\n assertIsDirectoryNotSymlink,\n assertRealpathInsideRoot,\n fromPosixRelPath,\n isPathInsideRoot,\n sha256String,\n} from \"./fs-utils.js\";\nimport {\n parseExportBundle,\n type CapsuleBlock,\n type ExportManifestV2,\n type ExportMemoryRecordV1,\n} from \"./types.js\";\n\n/**\n * Three-way conflict-resolution mode for {@link mergeCapsule}.\n *\n * A \"conflict\" is defined as: the same memory-file path exists in both the\n * source archive and the target directory AND the content hash of the local\n * file differs from the archive's manifest entry for that path.\n *\n * Files that exist only in the archive (no local counterpart) are always\n * written regardless of mode — there is no conflict to resolve.\n *\n * Files that are byte-identical (same content hash in both locations) are\n * recorded as {@link MergeCapsuleResult.skipped} with reason `\"identical\"` and\n * are never re-written regardless of mode; this is a no-op optimisation rather\n * than a conflict.\n *\n * - `\"skip-conflicts\"` (default) — log the conflict, skip the conflicting\n * archive entries, but continue importing non-conflicting entries. The\n * resulting merge is the union of:\n * - all non-conflicting archive files (written to target)\n * - all pre-existing local files (left unchanged)\n *\n * - `\"prefer-source\"` — for conflicting files, snapshot the local content via\n * page-versioning (gotcha #54: snapshot before overwrite) then overwrite\n * with the archive content.\n *\n * - `\"prefer-local\"` — for conflicting files, keep the local content; the\n * archive entry is skipped.\n */\nexport type MergeCapsuleConflictMode =\n | \"skip-conflicts\"\n | \"prefer-source\"\n | \"prefer-local\";\n\n/**\n * Options accepted by {@link mergeCapsule}.\n *\n * `sourceArchive` — absolute or cwd-relative path to a `.capsule.json.gz`\n * archive produced by `exportCapsule`. Must be a V2 bundle.\n *\n * `targetRoot` — absolute or cwd-relative path to the memory directory that\n * receives the merged records. Must be an existing, non-symlink directory.\n *\n * `conflictMode` — see {@link MergeCapsuleConflictMode}. Defaults to\n * `\"skip-conflicts\"`.\n *\n * `versioning` — optional page-versioning config forwarded to\n * {@link createVersion} in `\"prefer-source\"` mode. When omitted or disabled,\n * overwrites proceed without snapshotting (not recommended for production).\n *\n * `log` — optional logger forwarded to {@link createVersion}.\n */\nexport interface MergeCapsuleOptions {\n sourceArchive: string;\n targetRoot: string;\n conflictMode?: MergeCapsuleConflictMode;\n versioning?: VersioningConfig;\n log?: VersioningLogger;\n}\n\nexport interface MergeCapsuleWrittenRecord {\n /** Capsule-relative posix path. */\n sourcePath: string;\n /** Memory-dir-relative posix path written on disk. */\n targetPath: string;\n /** Whether a page-versioning snapshot was taken before overwriting. */\n snapshotted: boolean;\n}\n\nexport interface MergeCapsuleSkippedRecord {\n /** Capsule-relative posix path. */\n path: string;\n /**\n * Why the archive entry was not written.\n *\n * `\"conflict\"` — the entry existed locally with different content and the\n * active mode did not resolve the conflict with a write (`\"skip-conflicts\"` /\n * `\"prefer-local\"`).\n *\n * `\"identical\"` — the entry's content hash matches what is already on disk;\n * no write is needed.\n */\n reason: \"conflict\" | \"identical\";\n}\n\nexport interface MergeCapsuleConflictRecord {\n /** Capsule-relative posix path of the conflicting entry. */\n path: string;\n /** SHA-256 of the archive's copy. */\n archiveSha256: string;\n /** SHA-256 of the local copy. */\n localSha256: string;\n}\n\nexport interface MergeCapsuleResult {\n /** Records that were written to the target directory. */\n merged: MergeCapsuleWrittenRecord[];\n /**\n * Records that were NOT written (conflict skipped, or byte-identical).\n * Includes conflicts that were resolved by `\"prefer-local\"`.\n */\n skipped: MergeCapsuleSkippedRecord[];\n /**\n * Metadata about every detected conflict, regardless of which mode resolved\n * it. Callers can use this to report \"N conflicts encountered; M overwritten\".\n */\n conflicts: MergeCapsuleConflictRecord[];\n /** The manifest decoded from the archive. */\n manifest: ExportManifestV2;\n}\n\n// ---------------------------------------------------------------------------\n// Main entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Merge a V2 capsule archive into an existing memory directory using\n * three-way conflict semantics.\n *\n * Sequence:\n * 1. Read + gunzip + JSON.parse the archive.\n * 2. Validate through `parseExportBundle` (V1 rejected).\n * 3. Verify every record's content sha256 against the manifest.\n * Any mismatch aborts BEFORE any file is written (gotcha #25).\n * 4. Classify each record as: new (no local copy), identical (same hash),\n * or conflicting (different hash).\n * 5. Apply the selected {@link MergeCapsuleConflictMode} to conflicting\n * entries; always write new entries; always skip identical entries.\n *\n * Determinism: `merged`, `skipped`, and `conflicts` are all returned sorted\n * by `path`/`sourcePath` so callers get stable output regardless of bundle\n * order.\n */\nexport async function mergeCapsule(\n opts: MergeCapsuleOptions,\n): Promise<MergeCapsuleResult> {\n const archiveAbs = path.resolve(opts.sourceArchive);\n const rootAbs = path.resolve(opts.targetRoot);\n\n await assertIsDirectoryNotSymlink(rootAbs, \"mergeCapsule\", \"targetRoot\");\n\n const conflictMode: MergeCapsuleConflictMode =\n opts.conflictMode ?? \"skip-conflicts\";\n\n // Rule 51: reject invalid conflictMode values up-front before any I/O.\n if (\n conflictMode !== \"skip-conflicts\" &&\n conflictMode !== \"prefer-source\" &&\n conflictMode !== \"prefer-local\"\n ) {\n throw new Error(\n `mergeCapsule: unknown conflictMode ${JSON.stringify(conflictMode)}; ` +\n `expected \"skip-conflicts\", \"prefer-source\", or \"prefer-local\"`,\n );\n }\n\n // ---------------------------------------------------------------------------\n // Parse + validate archive\n // ---------------------------------------------------------------------------\n\n const raw = await readFile(archiveAbs);\n let json: string;\n try {\n json = gunzipSync(raw).toString(\"utf-8\");\n } catch (cause) {\n throw new Error(\n `mergeCapsule: archive is not a valid gzip file: ${archiveAbs}`,\n { cause: cause as Error },\n );\n }\n\n let parsedJson: unknown;\n try {\n parsedJson = JSON.parse(json);\n } catch (cause) {\n throw new Error(\n `mergeCapsule: archive is not valid JSON after gunzip: ${archiveAbs}`,\n { cause: cause as Error },\n );\n }\n\n const parsed = parseExportBundle(parsedJson);\n if (parsed.capsuleVersion !== 2) {\n throw new Error(\n \"mergeCapsule: archive is V1; only V2 capsule archives are supported\",\n );\n }\n\n const bundle = parsed.bundle as {\n manifest: ExportManifestV2;\n records: ExportMemoryRecordV1[];\n };\n const manifest = bundle.manifest;\n const capsule = manifest.capsule;\n\n // Build path → manifest entry index for O(1) checksum lookup.\n const manifestIndex = new Map<string, ExportManifestV2[\"files\"][number]>();\n for (const f of manifest.files) {\n manifestIndex.set(f.path, f);\n }\n if (manifestIndex.size !== manifest.files.length) {\n throw new Error(\"mergeCapsule: manifest contains duplicate file paths\");\n }\n\n const recordPaths = new Set<string>();\n for (const rec of bundle.records) {\n if (recordPaths.has(rec.path)) {\n throw new Error(\n `mergeCapsule: bundle contains duplicate record path: ${rec.path}`,\n );\n }\n recordPaths.add(rec.path);\n }\n\n // ---------------------------------------------------------------------------\n // Phase 1: verify checksums + validate paths before ANY filesystem mutation.\n // (gotcha #25: don't destroy old state before confirming new state succeeds)\n // ---------------------------------------------------------------------------\n\n const rootReal = await realpath(rootAbs).catch(() => rootAbs);\n\n // Tracks normalized, case-folded target paths seen so far in phase 1. Maps\n // targetAbs.toLowerCase() → first source path so the collision error can name\n // both offending entries. Two manifest entries whose computed target paths\n // normalise to the same absolute path (e.g. `subdir/file.md` and\n // `subdir/./file.md`, or differing case on case-insensitive filesystems such\n // as macOS and Windows) would both refer to the same inode. In\n // `skip-conflicts`/`prefer-local` modes one entry would be misclassified as a\n // local conflict against the OTHER entry's just-written content; in\n // `prefer-source` the second entry would silently overwrite the first. We\n // reject the import up-front before any write (Codex P2 thread on PR #748,\n // mirroring `capsule-import.ts`).\n const seenTargetPaths = new Map<string, string>();\n\n for (const rec of bundle.records) {\n // Checksum validation.\n const entry = manifestIndex.get(rec.path);\n if (!entry) {\n throw new Error(\n `mergeCapsule: archive checksum mismatch (record without manifest entry: ${rec.path})`,\n );\n }\n const { sha256, bytes } = sha256String(rec.content);\n if (sha256 !== entry.sha256 || bytes !== entry.bytes) {\n throw new Error(\n `mergeCapsule: archive checksum mismatch for ${rec.path}: ` +\n `expected sha256=${entry.sha256} bytes=${entry.bytes}, ` +\n `got sha256=${sha256} bytes=${bytes}`,\n );\n }\n\n // Path-traversal validation (mirrors capsule-import.ts).\n if (rec.path.includes(\"\\\\\")) {\n throw new Error(\n `mergeCapsule: record path contains backslash separators (Windows-style paths are not allowed): ${rec.path}`,\n );\n }\n const posixNormalized = path.posix.normalize(rec.path);\n if (\n rec.path.startsWith(\"/\") ||\n rec.path.split(\"/\").some((seg) => seg === \"..\") ||\n posixNormalized.startsWith(\"..\") ||\n posixNormalized.startsWith(\"/\")\n ) {\n throw new Error(\n `mergeCapsule: record path escapes target root: ${rec.path}`,\n );\n }\n\n // Lexical root containment check.\n const targetAbs = path.join(rootReal, fromPosixRelPath(rec.path));\n if (!isPathInsideRoot(rootReal, targetAbs)) {\n throw new Error(\n `mergeCapsule: record path escapes target root: ${rec.path}`,\n );\n }\n\n // Symlink-aware containment check (shared helper from fs-utils).\n await assertRealpathInsideRoot(rootReal, targetAbs, rec.path, \"mergeCapsule\");\n\n // Target-file symlink guard: if the target already exists as a symlink,\n // reject — writes through symlinks can redirect to unexpected locations.\n const targetLstat = await lstat(targetAbs).catch(() => null);\n if (targetLstat !== null && targetLstat.isSymbolicLink()) {\n throw new Error(\n `mergeCapsule: record target is a symlink and cannot be written to safely: ${rec.path}`,\n );\n }\n\n // Duplicate normalized target path detection (Codex P2 #748, mirrors\n // capsule-import.ts). `path.join` already normalises `.` segments\n // (e.g. `subdir/./file.md` → `subdir/file.md`). On case-insensitive\n // filesystems (macOS default, Windows), two paths that differ only in case\n // would resolve to the same inode. We fold the dedup key to lowercase so\n // that `subdir/File.md` and `subdir/file.md` are detected as duplicates\n // before any write occurs. This is intentionally unconditional: the cost\n // of an extra `.toLowerCase()` on case-sensitive filesystems is negligible,\n // and a defensive lowercase is far simpler than probing filesystem\n // case-sensitivity at runtime. Without this guard, prefer-source mode\n // would silently overwrite one entry with the other, and skip-conflicts /\n // prefer-local would misclassify the second entry as a local conflict\n // against the first entry's freshly written content.\n const dedupKey = targetAbs.toLowerCase();\n const firstSourcePath = seenTargetPaths.get(dedupKey);\n if (firstSourcePath !== undefined) {\n throw new Error(\n `mergeCapsule: manifest contains two entries that resolve to the same target path: ` +\n `\"${firstSourcePath}\" and \"${rec.path}\" both map to \"${rec.path}\"`,\n );\n }\n seenTargetPaths.set(dedupKey, rec.path);\n }\n\n // Detect manifest-only entries (missing record). Treat as corruption.\n for (const f of manifest.files) {\n if (!recordPaths.has(f.path)) {\n throw new Error(\n `mergeCapsule: archive checksum mismatch (manifest entry without record: ${f.path})`,\n );\n }\n }\n\n // ---------------------------------------------------------------------------\n // Phase 2: classify records and apply conflict mode.\n // ---------------------------------------------------------------------------\n\n const merged: MergeCapsuleWrittenRecord[] = [];\n const skipped: MergeCapsuleSkippedRecord[] = [];\n const conflicts: MergeCapsuleConflictRecord[] = [];\n\n // Sort by source path for deterministic output (mirrors capsule-import.ts).\n const sortedRecords = [...bundle.records].sort((a, b) =>\n a.path.localeCompare(b.path),\n );\n\n for (const rec of sortedRecords) {\n const targetAbs = path.join(rootReal, fromPosixRelPath(rec.path));\n const entry = manifestIndex.get(rec.path)!; // validated above\n\n const localContent = await readLocalFile(targetAbs);\n\n if (localContent === null) {\n // No local copy — always write regardless of mode.\n await mkdir(path.dirname(targetAbs), { recursive: true });\n await writeFile(targetAbs, rec.content, \"utf-8\");\n bumpMemoryCorpusVersionForDir(rootReal); // per-write bump (Cursor Medium, #1902)\n merged.push({ sourcePath: rec.path, targetPath: rec.path, snapshotted: false });\n continue;\n }\n\n // Local file exists. Check if it is byte-identical to the archive entry.\n const { sha256: localSha256 } = sha256String(localContent);\n\n if (localSha256 === entry.sha256) {\n // Byte-identical — no write needed.\n skipped.push({ path: rec.path, reason: \"identical\" });\n continue;\n }\n\n // Content differs → conflict.\n const { sha256: archiveSha256 } = sha256String(rec.content);\n conflicts.push({\n path: rec.path,\n archiveSha256,\n localSha256,\n });\n\n if (conflictMode === \"skip-conflicts\" || conflictMode === \"prefer-local\") {\n // Keep local copy, skip archive entry.\n skipped.push({ path: rec.path, reason: \"conflict\" });\n continue;\n }\n\n // conflictMode === \"prefer-source\": snapshot local then overwrite.\n let snapshotted = false;\n if (opts.versioning && opts.versioning.enabled) {\n // Gotcha #54: snapshot BEFORE overwriting.\n await createVersion(\n targetAbs,\n localContent,\n \"manual\",\n opts.versioning,\n opts.log,\n `capsule-merge: ${capsule.id}`,\n rootReal,\n );\n snapshotted = true;\n }\n\n await writeFile(targetAbs, rec.content, \"utf-8\");\n bumpMemoryCorpusVersionForDir(rootReal); // per-write bump (Cursor Medium, #1902)\n merged.push({ sourcePath: rec.path, targetPath: rec.path, snapshotted });\n }\n\n // Sort output lists for determinism.\n merged.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));\n skipped.sort((a, b) => a.path.localeCompare(b.path));\n conflicts.sort((a, b) => a.path.localeCompare(b.path));\n\n // Per-write corpus bumps happen inside the loop above (Cursor Medium, #1902):\n // a concurrent readAllMemories during the merge rescans and never serves the\n // pre-merge corpus mid-batch.\n\n return { merged, skipped, conflicts, manifest };\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\nasync function readLocalFile(absPath: string): Promise<string | null> {\n const st = await stat(absPath).catch(() => null);\n if (!st || !st.isFile()) return null;\n return readFile(absPath, \"utf-8\");\n}\n\n// Re-export CapsuleBlock so callers don't need a deep import from types.ts.\nexport type { CapsuleBlock };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,OAAO,OAAO,UAAU,UAAU,MAAM,iBAAiB;AAClE,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAyJ3B,eAAsB,aACpB,MAC6B;AAC7B,QAAM,aAAa,KAAK,QAAQ,KAAK,aAAa;AAClD,QAAM,UAAU,KAAK,QAAQ,KAAK,UAAU;AAE5C,QAAM,4BAA4B,SAAS,gBAAgB,YAAY;AAEvE,QAAM,eACJ,KAAK,gBAAgB;AAGvB,MACE,iBAAiB,oBACjB,iBAAiB,mBACjB,iBAAiB,gBACjB;AACA,UAAM,IAAI;AAAA,MACR,sCAAsC,KAAK,UAAU,YAAY,CAAC;AAAA,IAEpE;AAAA,EACF;AAMA,QAAM,MAAM,MAAM,SAAS,UAAU;AACrC,MAAI;AACJ,MAAI;AACF,WAAO,WAAW,GAAG,EAAE,SAAS,OAAO;AAAA,EACzC,SAAS,OAAO;AACd,UAAM,IAAI;AAAA,MACR,mDAAmD,UAAU;AAAA,MAC7D,EAAE,MAAsB;AAAA,IAC1B;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,iBAAa,KAAK,MAAM,IAAI;AAAA,EAC9B,SAAS,OAAO;AACd,UAAM,IAAI;AAAA,MACR,yDAAyD,UAAU;AAAA,MACnE,EAAE,MAAsB;AAAA,IAC1B;AAAA,EACF;AAEA,QAAM,SAAS,kBAAkB,UAAU;AAC3C,MAAI,OAAO,mBAAmB,GAAG;AAC/B,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,OAAO;AAItB,QAAM,WAAW,OAAO;AACxB,QAAM,UAAU,SAAS;AAGzB,QAAM,gBAAgB,oBAAI,IAA+C;AACzE,aAAW,KAAK,SAAS,OAAO;AAC9B,kBAAc,IAAI,EAAE,MAAM,CAAC;AAAA,EAC7B;AACA,MAAI,cAAc,SAAS,SAAS,MAAM,QAAQ;AAChD,UAAM,IAAI,MAAM,sDAAsD;AAAA,EACxE;AAEA,QAAM,cAAc,oBAAI,IAAY;AACpC,aAAW,OAAO,OAAO,SAAS;AAChC,QAAI,YAAY,IAAI,IAAI,IAAI,GAAG;AAC7B,YAAM,IAAI;AAAA,QACR,wDAAwD,IAAI,IAAI;AAAA,MAClE;AAAA,IACF;AACA,gBAAY,IAAI,IAAI,IAAI;AAAA,EAC1B;AAOA,QAAM,WAAW,MAAM,SAAS,OAAO,EAAE,MAAM,MAAM,OAAO;AAa5D,QAAM,kBAAkB,oBAAI,IAAoB;AAEhD,aAAW,OAAO,OAAO,SAAS;AAEhC,UAAM,QAAQ,cAAc,IAAI,IAAI,IAAI;AACxC,QAAI,CAAC,OAAO;AACV,YAAM,IAAI;AAAA,QACR,2EAA2E,IAAI,IAAI;AAAA,MACrF;AAAA,IACF;AACA,UAAM,EAAE,QAAQ,MAAM,IAAI,aAAa,IAAI,OAAO;AAClD,QAAI,WAAW,MAAM,UAAU,UAAU,MAAM,OAAO;AACpD,YAAM,IAAI;AAAA,QACR,+CAA+C,IAAI,IAAI,qBAClC,MAAM,MAAM,UAAU,MAAM,KAAK,gBACtC,MAAM,UAAU,KAAK;AAAA,MACvC;AAAA,IACF;AAGA,QAAI,IAAI,KAAK,SAAS,IAAI,GAAG;AAC3B,YAAM,IAAI;AAAA,QACR,kGAAkG,IAAI,IAAI;AAAA,MAC5G;AAAA,IACF;AACA,UAAM,kBAAkB,KAAK,MAAM,UAAU,IAAI,IAAI;AACrD,QACE,IAAI,KAAK,WAAW,GAAG,KACvB,IAAI,KAAK,MAAM,GAAG,EAAE,KAAK,CAAC,QAAQ,QAAQ,IAAI,KAC9C,gBAAgB,WAAW,IAAI,KAC/B,gBAAgB,WAAW,GAAG,GAC9B;AACA,YAAM,IAAI;AAAA,QACR,kDAAkD,IAAI,IAAI;AAAA,MAC5D;AAAA,IACF;AAGA,UAAM,YAAY,KAAK,KAAK,UAAU,iBAAiB,IAAI,IAAI,CAAC;AAChE,QAAI,CAAC,iBAAiB,UAAU,SAAS,GAAG;AAC1C,YAAM,IAAI;AAAA,QACR,kDAAkD,IAAI,IAAI;AAAA,MAC5D;AAAA,IACF;AAGA,UAAM,yBAAyB,UAAU,WAAW,IAAI,MAAM,cAAc;AAI5E,UAAM,cAAc,MAAM,MAAM,SAAS,EAAE,MAAM,MAAM,IAAI;AAC3D,QAAI,gBAAgB,QAAQ,YAAY,eAAe,GAAG;AACxD,YAAM,IAAI;AAAA,QACR,6EAA6E,IAAI,IAAI;AAAA,MACvF;AAAA,IACF;AAeA,UAAM,WAAW,UAAU,YAAY;AACvC,UAAM,kBAAkB,gBAAgB,IAAI,QAAQ;AACpD,QAAI,oBAAoB,QAAW;AACjC,YAAM,IAAI;AAAA,QACR,sFACM,eAAe,UAAU,IAAI,IAAI,kBAAkB,IAAI,IAAI;AAAA,MACnE;AAAA,IACF;AACA,oBAAgB,IAAI,UAAU,IAAI,IAAI;AAAA,EACxC;AAGA,aAAW,KAAK,SAAS,OAAO;AAC9B,QAAI,CAAC,YAAY,IAAI,EAAE,IAAI,GAAG;AAC5B,YAAM,IAAI;AAAA,QACR,2EAA2E,EAAE,IAAI;AAAA,MACnF;AAAA,IACF;AAAA,EACF;AAMA,QAAM,SAAsC,CAAC;AAC7C,QAAM,UAAuC,CAAC;AAC9C,QAAM,YAA0C,CAAC;AAGjD,QAAM,gBAAgB,CAAC,GAAG,OAAO,OAAO,EAAE;AAAA,IAAK,CAAC,GAAG,MACjD,EAAE,KAAK,cAAc,EAAE,IAAI;AAAA,EAC7B;AAEA,aAAW,OAAO,eAAe;AAC/B,UAAM,YAAY,KAAK,KAAK,UAAU,iBAAiB,IAAI,IAAI,CAAC;AAChE,UAAM,QAAQ,cAAc,IAAI,IAAI,IAAI;AAExC,UAAM,eAAe,MAAM,cAAc,SAAS;AAElD,QAAI,iBAAiB,MAAM;AAEzB,YAAM,MAAM,KAAK,QAAQ,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;AACxD,YAAM,UAAU,WAAW,IAAI,SAAS,OAAO;AAC/C,oCAA8B,QAAQ;AACtC,aAAO,KAAK,EAAE,YAAY,IAAI,MAAM,YAAY,IAAI,MAAM,aAAa,MAAM,CAAC;AAC9E;AAAA,IACF;AAGA,UAAM,EAAE,QAAQ,YAAY,IAAI,aAAa,YAAY;AAEzD,QAAI,gBAAgB,MAAM,QAAQ;AAEhC,cAAQ,KAAK,EAAE,MAAM,IAAI,MAAM,QAAQ,YAAY,CAAC;AACpD;AAAA,IACF;AAGA,UAAM,EAAE,QAAQ,cAAc,IAAI,aAAa,IAAI,OAAO;AAC1D,cAAU,KAAK;AAAA,MACb,MAAM,IAAI;AAAA,MACV;AAAA,MACA;AAAA,IACF,CAAC;AAED,QAAI,iBAAiB,oBAAoB,iBAAiB,gBAAgB;AAExE,cAAQ,KAAK,EAAE,MAAM,IAAI,MAAM,QAAQ,WAAW,CAAC;AACnD;AAAA,IACF;AAGA,QAAI,cAAc;AAClB,QAAI,KAAK,cAAc,KAAK,WAAW,SAAS;AAE9C,YAAM;AAAA,QACJ;AAAA,QACA;AAAA,QACA;AAAA,QACA,KAAK;AAAA,QACL,KAAK;AAAA,QACL,kBAAkB,QAAQ,EAAE;AAAA,QAC5B;AAAA,MACF;AACA,oBAAc;AAAA,IAChB;AAEA,UAAM,UAAU,WAAW,IAAI,SAAS,OAAO;AAC/C,kCAA8B,QAAQ;AACtC,WAAO,KAAK,EAAE,YAAY,IAAI,MAAM,YAAY,IAAI,MAAM,YAAY,CAAC;AAAA,EACzE;AAGA,SAAO,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,cAAc,EAAE,UAAU,CAAC;AAC9D,UAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AACnD,YAAU,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAMrD,SAAO,EAAE,QAAQ,SAAS,WAAW,SAAS;AAChD;AAMA,eAAe,cAAc,SAAyC;AACpE,QAAM,KAAK,MAAM,KAAK,OAAO,EAAE,MAAM,MAAM,IAAI;AAC/C,MAAI,CAAC,MAAM,CAAC,GAAG,OAAO,EAAG,QAAO;AAChC,SAAO,SAAS,SAAS,OAAO;AAClC;","names":[]}
@@ -1,60 +0,0 @@
1
- // src/local-llm-helpers.ts
2
- function isAbortError(err) {
3
- if (!err || typeof err !== "object") return false;
4
- const maybe = err;
5
- return maybe.name === "AbortError" || maybe.message === "This operation was aborted" || maybe.message === "The operation was aborted";
6
- }
7
- function waitForRetryBackoff(backoffMs, signal) {
8
- return new Promise((resolve) => {
9
- let settled = false;
10
- let timer;
11
- const onAbort = () => {
12
- if (settled) return;
13
- settled = true;
14
- if (timer) clearTimeout(timer);
15
- signal?.removeEventListener("abort", onAbort);
16
- resolve(false);
17
- };
18
- const onTimer = () => {
19
- if (settled) return;
20
- settled = true;
21
- signal?.removeEventListener("abort", onAbort);
22
- resolve(true);
23
- };
24
- timer = setTimeout(onTimer, backoffMs);
25
- if (signal) {
26
- if (signal.aborted) onAbort();
27
- else signal.addEventListener("abort", onAbort, { once: true });
28
- }
29
- });
30
- }
31
- function normalizeBackendTripReason(reason) {
32
- const cleaned = reason.replace(/\s+/g, " ").replace(/^[-:–—\s]+/, "").trim();
33
- if (!cleaned) return "unknown local backend failure";
34
- return cleaned.length > 160 ? `${cleaned.slice(0, 157)}...` : cleaned;
35
- }
36
- function extractNonRecoverableBackendReason(reason) {
37
- const match = reason.match(
38
- /Failed to load model|Library not loaded|different Team IDs|code signature|llm_engine_mlx_amphibian/i
39
- );
40
- return match?.[0] ?? null;
41
- }
42
- function extractNonRecoverableBackendReasonFromErrorText(errorText) {
43
- const directReason = extractNonRecoverableBackendReason(errorText);
44
- if (directReason) return directReason;
45
- try {
46
- const parsed = JSON.parse(errorText);
47
- return extractNonRecoverableBackendReason(parsed?.error?.message ?? "");
48
- } catch {
49
- return null;
50
- }
51
- }
52
-
53
- export {
54
- isAbortError,
55
- waitForRetryBackoff,
56
- normalizeBackendTripReason,
57
- extractNonRecoverableBackendReason,
58
- extractNonRecoverableBackendReasonFromErrorText
59
- };
60
- //# sourceMappingURL=chunk-4RNZMRAV.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/local-llm-helpers.ts"],"sourcesContent":["export function isAbortError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const maybe = err as { name?: string; message?: string };\n return (\n maybe.name === \"AbortError\" ||\n maybe.message === \"This operation was aborted\" ||\n maybe.message === \"The operation was aborted\"\n );\n}\n\nexport function waitForRetryBackoff(backoffMs: number, signal?: AbortSignal): Promise<boolean> {\n return new Promise((resolve) => {\n let settled = false;\n let timer: NodeJS.Timeout | undefined;\n const onAbort = (): void => {\n if (settled) return;\n settled = true;\n if (timer) clearTimeout(timer);\n signal?.removeEventListener(\"abort\", onAbort);\n resolve(false);\n };\n const onTimer = (): void => {\n if (settled) return;\n settled = true;\n signal?.removeEventListener(\"abort\", onAbort);\n resolve(true);\n };\n timer = setTimeout(onTimer, backoffMs);\n if (signal) {\n if (signal.aborted) onAbort();\n else signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n });\n}\n\nexport function normalizeBackendTripReason(reason: string): string {\n const cleaned = reason.replace(/\\s+/g, \" \").replace(/^[-:–—\\s]+/, \"\").trim();\n if (!cleaned) return \"unknown local backend failure\";\n return cleaned.length > 160 ? `${cleaned.slice(0, 157)}...` : cleaned;\n}\n\nexport function extractNonRecoverableBackendReason(reason: string): string | null {\n const match = reason.match(\n /Failed to load model|Library not loaded|different Team IDs|code signature|llm_engine_mlx_amphibian/i,\n );\n return match?.[0] ?? null;\n}\n\nexport function extractNonRecoverableBackendReasonFromErrorText(errorText: string): string | null {\n const directReason = extractNonRecoverableBackendReason(errorText);\n if (directReason) return directReason;\n try {\n const parsed = JSON.parse(errorText) as { error?: { message?: string } };\n return extractNonRecoverableBackendReason(parsed?.error?.message ?? \"\");\n } catch {\n return null;\n }\n}\n"],"mappings":";AAAO,SAAS,aAAa,KAAuB;AAClD,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,QAAQ;AACd,SACE,MAAM,SAAS,gBACf,MAAM,YAAY,gCAClB,MAAM,YAAY;AAEtB;AAEO,SAAS,oBAAoB,WAAmB,QAAwC;AAC7F,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,QAAI,UAAU;AACd,QAAI;AACJ,UAAM,UAAU,MAAY;AAC1B,UAAI,QAAS;AACb,gBAAU;AACV,UAAI,MAAO,cAAa,KAAK;AAC7B,cAAQ,oBAAoB,SAAS,OAAO;AAC5C,cAAQ,KAAK;AAAA,IACf;AACA,UAAM,UAAU,MAAY;AAC1B,UAAI,QAAS;AACb,gBAAU;AACV,cAAQ,oBAAoB,SAAS,OAAO;AAC5C,cAAQ,IAAI;AAAA,IACd;AACA,YAAQ,WAAW,SAAS,SAAS;AACrC,QAAI,QAAQ;AACV,UAAI,OAAO,QAAS,SAAQ;AAAA,UACvB,QAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAAA,IAC/D;AAAA,EACF,CAAC;AACH;AAEO,SAAS,2BAA2B,QAAwB;AACjE,QAAM,UAAU,OAAO,QAAQ,QAAQ,GAAG,EAAE,QAAQ,cAAc,EAAE,EAAE,KAAK;AAC3E,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,QAAQ,SAAS,MAAM,GAAG,QAAQ,MAAM,GAAG,GAAG,CAAC,QAAQ;AAChE;AAEO,SAAS,mCAAmC,QAA+B;AAChF,QAAM,QAAQ,OAAO;AAAA,IACnB;AAAA,EACF;AACA,SAAO,QAAQ,CAAC,KAAK;AACvB;AAEO,SAAS,gDAAgD,WAAkC;AAChG,QAAM,eAAe,mCAAmC,SAAS;AACjE,MAAI,aAAc,QAAO;AACzB,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,SAAS;AACnC,WAAO,mCAAmC,QAAQ,OAAO,WAAW,EAAE;AAAA,EACxE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}