@remnic/core 10.0.66 → 10.0.67

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 (239) hide show
  1. package/dist/access-admin-ops-surface.js +16 -16
  2. package/dist/access-authorization-probe.js +17 -17
  3. package/dist/access-boundary.js +16 -16
  4. package/dist/access-cli.js +60 -60
  5. package/dist/access-extraction-force-flush.js +16 -16
  6. package/dist/access-http-briefing.js +17 -17
  7. package/dist/access-http-query.js +18 -18
  8. package/dist/access-http.js +33 -33
  9. package/dist/access-identity-continuity-surface.js +16 -16
  10. package/dist/access-lcm-surface.js +16 -16
  11. package/dist/access-mcp.js +26 -26
  12. package/dist/access-namespace-preflight.js +16 -16
  13. package/dist/access-observe-write-surface.js +16 -16
  14. package/dist/access-operations-batch.js +17 -17
  15. package/dist/access-operations.js +24 -24
  16. package/dist/access-recall-concurrency.js +16 -16
  17. package/dist/access-recall-diagnostics.js +2 -2
  18. package/dist/access-recall-response.js +16 -16
  19. package/dist/access-recall-surface.js +16 -16
  20. package/dist/access-service.js +16 -16
  21. package/dist/access-surface-catalog.js +18 -18
  22. package/dist/active-recall.js +3 -3
  23. package/dist/briefing.js +3 -3
  24. package/dist/buffer.js +2 -2
  25. package/dist/causal-consolidation.js +4 -4
  26. package/dist/{chunk-KISOMGY2.js → chunk-3RHU5MD7.js} +2 -2
  27. package/dist/{chunk-3H25IRJJ.js → chunk-4MPAKYZU.js} +2 -2
  28. package/dist/{chunk-IPCSZLLF.js → chunk-4NV7O76Q.js} +2 -2
  29. package/dist/{chunk-BH7NVSEC.js → chunk-4SOWMA55.js} +3 -3
  30. package/dist/{chunk-GROHOWR5.js → chunk-5MGKGEL7.js} +2 -2
  31. package/dist/{chunk-ZPLQ27MY.js → chunk-5UM4H64E.js} +2 -2
  32. package/dist/{chunk-2DNFRLLL.js → chunk-5Y6VA6X2.js} +2 -2
  33. package/dist/{chunk-RAU25Q6J.js → chunk-6WKWL53V.js} +13 -13
  34. package/dist/{chunk-KFPJIKUW.js → chunk-75DCOS6I.js} +23 -23
  35. package/dist/{chunk-25QK3LR2.js → chunk-7O2MC42R.js} +2 -2
  36. package/dist/{chunk-7ETWIBTO.js → chunk-ADYP7HXE.js} +3 -3
  37. package/dist/{chunk-FUDKGJDN.js → chunk-AVGDYYMA.js} +2 -2
  38. package/dist/{chunk-ECF627KC.js → chunk-AYJQ644F.js} +2 -2
  39. package/dist/{chunk-T763ADEK.js → chunk-B3MWELZ3.js} +2 -2
  40. package/dist/{chunk-DRTSPRWL.js → chunk-B4DXREWC.js} +2 -2
  41. package/dist/{chunk-5UEGL5BF.js → chunk-CI3MAAIY.js} +2 -2
  42. package/dist/{chunk-JYXFIILI.js → chunk-CTH2CQCB.js} +2 -2
  43. package/dist/{chunk-UVHXLVFJ.js → chunk-EURPSF4Y.js} +2 -2
  44. package/dist/{chunk-VPEA63FC.js → chunk-EWJZZDRG.js} +2 -2
  45. package/dist/{chunk-BOZQAUAJ.js → chunk-EXQJMFRF.js} +2 -2
  46. package/dist/{chunk-VC65GNUS.js → chunk-GGD2YDS6.js} +2 -2
  47. package/dist/{chunk-5JABRT5G.js → chunk-GHHQLSSY.js} +3 -3
  48. package/dist/{chunk-FMYAU5MP.js → chunk-GJ4HU6LK.js} +2 -2
  49. package/dist/{chunk-3NJJQAL3.js → chunk-H5DIRCZP.js} +12 -12
  50. package/dist/{chunk-434E4BTO.js → chunk-HH73M65H.js} +3 -3
  51. package/dist/{chunk-LIGZO7XN.js → chunk-HPB5TF55.js} +2 -2
  52. package/dist/{chunk-E4QIX2OB.js → chunk-IAGVM7EY.js} +2 -2
  53. package/dist/{chunk-VC7GKR2V.js → chunk-IG3EZ3TF.js} +2 -2
  54. package/dist/{chunk-QAN5ZDRS.js → chunk-IVUVQPTQ.js} +2 -2
  55. package/dist/{chunk-XOFIJGB2.js → chunk-IZBIDPMY.js} +2 -2
  56. package/dist/{chunk-HFEGRMFG.js → chunk-JLGBJ4AX.js} +7 -7
  57. package/dist/{chunk-BBHKN2CN.js → chunk-JLMOH5JA.js} +2 -2
  58. package/dist/{chunk-TPCRDPUY.js → chunk-KZSGBWOJ.js} +2 -2
  59. package/dist/{chunk-SHHZWSSE.js → chunk-KZZ5EZ7O.js} +2 -2
  60. package/dist/{chunk-XMBDI725.js → chunk-LFYEQK45.js} +674 -518
  61. package/dist/chunk-LFYEQK45.js.map +1 -0
  62. package/dist/{chunk-A5FOXI3B.js → chunk-LN4X2NDA.js} +2 -2
  63. package/dist/{chunk-5ZHFPUBC.js → chunk-MOIJWWNA.js} +8 -8
  64. package/dist/{chunk-KO224R36.js → chunk-NCZGGGWI.js} +2 -2
  65. package/dist/{chunk-XVQVF2TK.js → chunk-NHWDTOBN.js} +269 -230
  66. package/dist/chunk-NHWDTOBN.js.map +1 -0
  67. package/dist/{chunk-7ZTGMEXF.js → chunk-O5ZINWW3.js} +2 -2
  68. package/dist/{chunk-WWH7GJ42.js → chunk-OCK4D5PC.js} +2 -2
  69. package/dist/{chunk-G6L3REL3.js → chunk-OPFVXM2D.js} +2 -2
  70. package/dist/{chunk-FR7PK7ED.js → chunk-P4TV3TQ2.js} +2 -2
  71. package/dist/{chunk-ZGHPELZ6.js → chunk-PMDOGXA2.js} +2 -2
  72. package/dist/{chunk-NM2II57L.js → chunk-PRKNAIGZ.js} +2 -1
  73. package/dist/chunk-PRKNAIGZ.js.map +1 -0
  74. package/dist/{chunk-F32JRMX2.js → chunk-QGFR73SN.js} +2 -2
  75. package/dist/{chunk-QNZPYH36.js → chunk-R2BIVU2Z.js} +352 -290
  76. package/dist/chunk-R2BIVU2Z.js.map +1 -0
  77. package/dist/{chunk-HG3CG557.js → chunk-RLVI2M5Q.js} +2 -2
  78. package/dist/{chunk-SH6ZYUTX.js → chunk-SXTVZ6JL.js} +2 -2
  79. package/dist/{chunk-CLSKGTAT.js → chunk-TECK5IVY.js} +2 -2
  80. package/dist/{chunk-SNS3DWG6.js → chunk-TVE7W67N.js} +2 -2
  81. package/dist/{chunk-KYSSUY66.js → chunk-U2CAGMUD.js} +2 -2
  82. package/dist/{chunk-XLCEFUAU.js → chunk-UVVFPCPX.js} +2 -2
  83. package/dist/{chunk-PL75KQGF.js → chunk-UW6LBFYN.js} +5 -5
  84. package/dist/{chunk-SEA56G6S.js → chunk-V4XE263I.js} +1 -1
  85. package/dist/chunk-V4XE263I.js.map +1 -0
  86. package/dist/{chunk-VSZNPAGL.js → chunk-V6SBA5O7.js} +3 -3
  87. package/dist/{chunk-B7VD36TI.js → chunk-WHROVQ5Q.js} +3 -3
  88. package/dist/{chunk-LDHR465K.js → chunk-Y3XPSGNF.js} +2 -2
  89. package/dist/{chunk-E6VQJ75T.js → chunk-ZORS5BG5.js} +4 -4
  90. package/dist/{chunk-KXY4HW2Y.js → chunk-ZXF3O7P4.js} +2 -2
  91. package/dist/cli.js +46 -46
  92. package/dist/coding/export-okf-codegraph.js +2 -2
  93. package/dist/compounding/engine.js +4 -4
  94. package/dist/config.d.ts.map +1 -1
  95. package/dist/config.js +3 -3
  96. package/dist/connectors/codex-materialize-runner.js +3 -3
  97. package/dist/connectors/index.js +3 -3
  98. package/dist/convergence-refresh.js +5 -5
  99. package/dist/corpus-watermark.js +5 -5
  100. package/dist/entity-retrieval.js +3 -3
  101. package/dist/external-wiki-access.js +17 -17
  102. package/dist/extraction.js +6 -6
  103. package/dist/harmonic-retrieval.js +3 -3
  104. package/dist/index.js +60 -60
  105. package/dist/location/index.js +5 -5
  106. package/dist/maintenance/memory-governance.js +3 -3
  107. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  108. package/dist/maintenance/rebuild-memory-projection-runner.js +5 -5
  109. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  110. package/dist/memory-browse-operations.js +17 -17
  111. package/dist/namespaces/migrate.js +4 -4
  112. package/dist/namespaces/storage.js +3 -3
  113. package/dist/operator-doctor-corpus.js +6 -6
  114. package/dist/operator-toolkit.js +12 -12
  115. package/dist/orchestration/buffer-drain-runner.js +60 -60
  116. package/dist/orchestration/consolidation-run.d.ts.map +1 -1
  117. package/dist/orchestration/extraction-drain.d.ts +4 -4
  118. package/dist/orchestration/extraction-drain.d.ts.map +1 -1
  119. package/dist/orchestration/extraction-persist.d.ts.map +1 -1
  120. package/dist/orchestration/extraction-queue-coordinator.d.ts +67 -33
  121. package/dist/orchestration/extraction-queue-coordinator.d.ts.map +1 -1
  122. package/dist/orchestration/extraction-run.d.ts +1 -8
  123. package/dist/orchestration/extraction-run.d.ts.map +1 -1
  124. package/dist/orchestration/fact-dedup-window.d.ts +49 -0
  125. package/dist/orchestration/fact-dedup-window.d.ts.map +1 -0
  126. package/dist/orchestration/maintenance.js +13 -13
  127. package/dist/orchestration/provider-breaker.d.ts +58 -0
  128. package/dist/orchestration/provider-breaker.d.ts.map +1 -0
  129. package/dist/orchestrator.js +60 -60
  130. package/dist/recall-explain-renderer.js +3 -3
  131. package/dist/recall-navigate-access.js +2 -2
  132. package/dist/recall-navigate-disclosure.js +2 -2
  133. package/dist/recall-navigate.js +2 -2
  134. package/dist/recall-navigation-operations.js +17 -17
  135. package/dist/recall-navigation.js +16 -16
  136. package/dist/recall-xray-cli.js +4 -4
  137. package/dist/recall-xray-renderer.js +3 -3
  138. package/dist/recall-xray.js +2 -2
  139. package/dist/resume-bundles.js +4 -4
  140. package/dist/semantic-consolidation.js +4 -4
  141. package/dist/semantic-rule-promotion.js +3 -3
  142. package/dist/semantic-rule-verifier.js +3 -3
  143. package/dist/source-agent-qualifier.js +5 -5
  144. package/dist/standup/build.js +3 -3
  145. package/dist/storage/profile-mutation.d.ts +25 -0
  146. package/dist/storage/profile-mutation.d.ts.map +1 -0
  147. package/dist/storage.d.ts +4 -6
  148. package/dist/storage.d.ts.map +1 -1
  149. package/dist/storage.js +2 -2
  150. package/dist/support-passport/index.js +19 -19
  151. package/dist/transfer/export-okf.js +3 -3
  152. package/dist/types.d.ts +2 -0
  153. package/dist/types.d.ts.map +1 -1
  154. package/dist/types.js +1 -1
  155. package/dist/verified-recall.js +3 -3
  156. package/dist/who-knows-cli.js +4 -4
  157. package/dist/who-knows.js +3 -3
  158. package/package.json +2 -2
  159. package/src/buffer-drain-handover.test.ts +3 -3
  160. package/src/buffer-drop-extraction-chunk.test.ts +0 -1
  161. package/src/config.test.ts +12 -0
  162. package/src/config.ts +2 -0
  163. package/src/fact-dedup-window.test.ts +116 -0
  164. package/src/orchestration/consolidation-profile-race.test.ts +178 -0
  165. package/src/orchestration/consolidation-run.ts +19 -7
  166. package/src/orchestration/extraction-drain.ts +10 -3
  167. package/src/orchestration/extraction-persist.ts +141 -138
  168. package/src/orchestration/extraction-queue-coordinator.ts +167 -56
  169. package/src/orchestration/extraction-run.ts +50 -93
  170. package/src/orchestration/fact-dedup-window.ts +122 -0
  171. package/src/orchestration/provider-breaker.test.ts +89 -0
  172. package/src/orchestration/provider-breaker.ts +158 -0
  173. package/src/orchestration/turn-ingestion.ts +5 -5
  174. package/src/orchestrator-extraction-queue.test.ts +179 -8
  175. package/src/orchestrator.ts +1 -1
  176. package/src/storage/profile-mutation.ts +119 -0
  177. package/src/storage-append-profile.test.ts +114 -0
  178. package/src/storage.ts +17 -48
  179. package/src/types.ts +2 -0
  180. package/dist/chunk-NM2II57L.js.map +0 -1
  181. package/dist/chunk-QNZPYH36.js.map +0 -1
  182. package/dist/chunk-SEA56G6S.js.map +0 -1
  183. package/dist/chunk-XMBDI725.js.map +0 -1
  184. package/dist/chunk-XVQVF2TK.js.map +0 -1
  185. /package/dist/{chunk-KISOMGY2.js.map → chunk-3RHU5MD7.js.map} +0 -0
  186. /package/dist/{chunk-3H25IRJJ.js.map → chunk-4MPAKYZU.js.map} +0 -0
  187. /package/dist/{chunk-IPCSZLLF.js.map → chunk-4NV7O76Q.js.map} +0 -0
  188. /package/dist/{chunk-BH7NVSEC.js.map → chunk-4SOWMA55.js.map} +0 -0
  189. /package/dist/{chunk-GROHOWR5.js.map → chunk-5MGKGEL7.js.map} +0 -0
  190. /package/dist/{chunk-ZPLQ27MY.js.map → chunk-5UM4H64E.js.map} +0 -0
  191. /package/dist/{chunk-2DNFRLLL.js.map → chunk-5Y6VA6X2.js.map} +0 -0
  192. /package/dist/{chunk-RAU25Q6J.js.map → chunk-6WKWL53V.js.map} +0 -0
  193. /package/dist/{chunk-KFPJIKUW.js.map → chunk-75DCOS6I.js.map} +0 -0
  194. /package/dist/{chunk-25QK3LR2.js.map → chunk-7O2MC42R.js.map} +0 -0
  195. /package/dist/{chunk-7ETWIBTO.js.map → chunk-ADYP7HXE.js.map} +0 -0
  196. /package/dist/{chunk-FUDKGJDN.js.map → chunk-AVGDYYMA.js.map} +0 -0
  197. /package/dist/{chunk-ECF627KC.js.map → chunk-AYJQ644F.js.map} +0 -0
  198. /package/dist/{chunk-T763ADEK.js.map → chunk-B3MWELZ3.js.map} +0 -0
  199. /package/dist/{chunk-DRTSPRWL.js.map → chunk-B4DXREWC.js.map} +0 -0
  200. /package/dist/{chunk-5UEGL5BF.js.map → chunk-CI3MAAIY.js.map} +0 -0
  201. /package/dist/{chunk-JYXFIILI.js.map → chunk-CTH2CQCB.js.map} +0 -0
  202. /package/dist/{chunk-UVHXLVFJ.js.map → chunk-EURPSF4Y.js.map} +0 -0
  203. /package/dist/{chunk-VPEA63FC.js.map → chunk-EWJZZDRG.js.map} +0 -0
  204. /package/dist/{chunk-BOZQAUAJ.js.map → chunk-EXQJMFRF.js.map} +0 -0
  205. /package/dist/{chunk-VC65GNUS.js.map → chunk-GGD2YDS6.js.map} +0 -0
  206. /package/dist/{chunk-5JABRT5G.js.map → chunk-GHHQLSSY.js.map} +0 -0
  207. /package/dist/{chunk-FMYAU5MP.js.map → chunk-GJ4HU6LK.js.map} +0 -0
  208. /package/dist/{chunk-3NJJQAL3.js.map → chunk-H5DIRCZP.js.map} +0 -0
  209. /package/dist/{chunk-434E4BTO.js.map → chunk-HH73M65H.js.map} +0 -0
  210. /package/dist/{chunk-LIGZO7XN.js.map → chunk-HPB5TF55.js.map} +0 -0
  211. /package/dist/{chunk-E4QIX2OB.js.map → chunk-IAGVM7EY.js.map} +0 -0
  212. /package/dist/{chunk-VC7GKR2V.js.map → chunk-IG3EZ3TF.js.map} +0 -0
  213. /package/dist/{chunk-QAN5ZDRS.js.map → chunk-IVUVQPTQ.js.map} +0 -0
  214. /package/dist/{chunk-XOFIJGB2.js.map → chunk-IZBIDPMY.js.map} +0 -0
  215. /package/dist/{chunk-HFEGRMFG.js.map → chunk-JLGBJ4AX.js.map} +0 -0
  216. /package/dist/{chunk-BBHKN2CN.js.map → chunk-JLMOH5JA.js.map} +0 -0
  217. /package/dist/{chunk-TPCRDPUY.js.map → chunk-KZSGBWOJ.js.map} +0 -0
  218. /package/dist/{chunk-SHHZWSSE.js.map → chunk-KZZ5EZ7O.js.map} +0 -0
  219. /package/dist/{chunk-A5FOXI3B.js.map → chunk-LN4X2NDA.js.map} +0 -0
  220. /package/dist/{chunk-5ZHFPUBC.js.map → chunk-MOIJWWNA.js.map} +0 -0
  221. /package/dist/{chunk-KO224R36.js.map → chunk-NCZGGGWI.js.map} +0 -0
  222. /package/dist/{chunk-7ZTGMEXF.js.map → chunk-O5ZINWW3.js.map} +0 -0
  223. /package/dist/{chunk-WWH7GJ42.js.map → chunk-OCK4D5PC.js.map} +0 -0
  224. /package/dist/{chunk-G6L3REL3.js.map → chunk-OPFVXM2D.js.map} +0 -0
  225. /package/dist/{chunk-FR7PK7ED.js.map → chunk-P4TV3TQ2.js.map} +0 -0
  226. /package/dist/{chunk-ZGHPELZ6.js.map → chunk-PMDOGXA2.js.map} +0 -0
  227. /package/dist/{chunk-F32JRMX2.js.map → chunk-QGFR73SN.js.map} +0 -0
  228. /package/dist/{chunk-HG3CG557.js.map → chunk-RLVI2M5Q.js.map} +0 -0
  229. /package/dist/{chunk-SH6ZYUTX.js.map → chunk-SXTVZ6JL.js.map} +0 -0
  230. /package/dist/{chunk-CLSKGTAT.js.map → chunk-TECK5IVY.js.map} +0 -0
  231. /package/dist/{chunk-SNS3DWG6.js.map → chunk-TVE7W67N.js.map} +0 -0
  232. /package/dist/{chunk-KYSSUY66.js.map → chunk-U2CAGMUD.js.map} +0 -0
  233. /package/dist/{chunk-XLCEFUAU.js.map → chunk-UVVFPCPX.js.map} +0 -0
  234. /package/dist/{chunk-PL75KQGF.js.map → chunk-UW6LBFYN.js.map} +0 -0
  235. /package/dist/{chunk-VSZNPAGL.js.map → chunk-V6SBA5O7.js.map} +0 -0
  236. /package/dist/{chunk-B7VD36TI.js.map → chunk-WHROVQ5Q.js.map} +0 -0
  237. /package/dist/{chunk-LDHR465K.js.map → chunk-Y3XPSGNF.js.map} +0 -0
  238. /package/dist/{chunk-E6VQJ75T.js.map → chunk-ZORS5BG5.js.map} +0 -0
  239. /package/dist/{chunk-KXY4HW2Y.js.map → chunk-ZXF3O7P4.js.map} +0 -0
@@ -0,0 +1,116 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { StorageManager } from "./storage.js";
7
+ import { withHeldFileLock } from "./utils/serialize-mutations.js";
8
+ import { confirmActiveFactDuplicate, persistUnderFactDedupWindow } from "./orchestration/fact-dedup-window.js";
9
+
10
+ function probe(storage: StorageManager) {
11
+ return { hasContentHashDedup: (s: StorageManager, key: string) => s.hasFactContentHash(key) };
12
+ }
13
+
14
+ async function makeStorage(): Promise<{ storage: StorageManager; cleanup: () => Promise<void> }> {
15
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-fact-dedup-window-"));
16
+ return { storage: new StorageManager(root), cleanup: () => rm(root, { recursive: true, force: true }) };
17
+ }
18
+
19
+ test("confirmActiveFactDuplicate sees a fact another session committed into the same namespace", async () => {
20
+ const { storage, cleanup } = await makeStorage();
21
+ try {
22
+ const recheck = () =>
23
+ confirmActiveFactDuplicate(probe(storage), storage, {
24
+ category: "fact",
25
+ contentHashDedupKey: "the sky is blue",
26
+ connector: undefined,
27
+ citationEnabled: false,
28
+ citationTemplate: "",
29
+ });
30
+ assert.equal(await recheck(), false, "clean namespace must not report a duplicate");
31
+
32
+ // A concurrent session (separate manager, same dir) commits the fact.
33
+ const peer = new StorageManager(storage["baseDir"]);
34
+ await peer.writeMemory("fact", "The sky is BLUE!");
35
+
36
+ assert.equal(await recheck(), true, "the committed fact must confirm as an active duplicate");
37
+ } finally {
38
+ await cleanup();
39
+ }
40
+ });
41
+
42
+ test("persistUnderFactDedupWindow suppresses on recheck hit and writes on a miss", async () => {
43
+ const { storage, cleanup } = await makeStorage();
44
+ try {
45
+ let writes = 0;
46
+ const write = async () => {
47
+ writes += 1;
48
+ await storage.writeMemory("fact", "the sky is blue");
49
+ return "written";
50
+ };
51
+ const recheck = () =>
52
+ confirmActiveFactDuplicate(probe(storage), storage, {
53
+ category: "fact",
54
+ contentHashDedupKey: "the sky is blue",
55
+ connector: undefined,
56
+ citationEnabled: false,
57
+ citationTemplate: "",
58
+ });
59
+
60
+ // First session: recheck misses, write proceeds.
61
+ const first = await persistUnderFactDedupWindow(storage["baseDir"], recheck, write);
62
+ assert.deepEqual(first, { suppressed: false, result: "written" });
63
+ assert.equal(writes, 1);
64
+
65
+ // Second session racing the same content: recheck hits, write suppressed.
66
+ const second = await persistUnderFactDedupWindow(storage["baseDir"], recheck, write);
67
+ assert.deepEqual(second, { suppressed: true });
68
+ assert.equal(writes, 1, "the raced duplicate must not be written");
69
+ assert.equal(await storage.hasFactContentHash("the sky is blue"), true);
70
+ } finally {
71
+ await cleanup();
72
+ }
73
+ });
74
+
75
+ test("persistUnderFactDedupWindow fails closed when a peer holds fact-dedup.lock", async () => {
76
+ const { storage, cleanup } = await makeStorage();
77
+ try {
78
+ const holderEntered = Promise.withResolvers<void>();
79
+ const holderRelease = Promise.withResolvers<void>();
80
+ const holder = withHeldFileLock(
81
+ path.join(storage["baseDir"], "fact-dedup.lock"),
82
+ { staleMs: 60_000 },
83
+ async (acquired) => {
84
+ assert.ok(acquired, "test holder must acquire the lock");
85
+ holderEntered.resolve();
86
+ await holderRelease.promise;
87
+ },
88
+ );
89
+ await holderEntered.promise;
90
+
91
+ let wrote = false;
92
+ await assert.rejects(
93
+ persistUnderFactDedupWindow(
94
+ storage["baseDir"],
95
+ async () => false,
96
+ async () => {
97
+ wrote = true;
98
+ return "written";
99
+ },
100
+ ),
101
+ /fact-dedup\.lock not acquired/,
102
+ );
103
+ assert.equal(wrote, false, "a locked-out writer must not write unlocked");
104
+
105
+ holderRelease.resolve();
106
+ await holder;
107
+ const outcome = await persistUnderFactDedupWindow(
108
+ storage["baseDir"],
109
+ async () => false,
110
+ async () => "written",
111
+ );
112
+ assert.deepEqual(outcome, { suppressed: false, result: "written" });
113
+ } finally {
114
+ await cleanup();
115
+ }
116
+ });
@@ -0,0 +1,178 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { StorageManager } from "../storage.js";
7
+ import { ConsolidationRunCoordinator } from "./consolidation-run.js";
8
+ import type { ExtractionEngine } from "../extraction.js";
9
+ import type { PluginConfig } from "../types.js";
10
+
11
+ function asPluginConfig(fake: Partial<PluginConfig>): PluginConfig {
12
+ // SAFETY: the coordinator reads only the three members raceConfig sets.
13
+ return fake as PluginConfig;
14
+ }
15
+
16
+ function raceConfig(): PluginConfig {
17
+ return asPluginConfig({
18
+ memoryDir: "/synthetic/remnic-profile-race-tests",
19
+ queryAwareIndexingEnabled: false,
20
+ dependencyPropagation: { enabled: false, maxDependents: 0, linkTypes: [], timeoutMs: 0, dryRun: true },
21
+ });
22
+ }
23
+
24
+ const PROFILE_SECTION = { consolidateTriggerLines: 2, consolidateTargetLines: 5 };
25
+
26
+ function makeRaceCoordinator(
27
+ storage: StorageManager,
28
+ extraction: ExtractionEngine,
29
+ ): ConsolidationRunCoordinator {
30
+ return new ConsolidationRunCoordinator({
31
+ config: raceConfig(),
32
+ getStorage: () => storage,
33
+ // SAFETY: records nothing in this test.
34
+ getStorageRouter: () => ({ recordWrite: async () => {} }) as never,
35
+ getExtraction: () => extraction,
36
+ storageDirNamespace: () => "namespace-a",
37
+ // SAFETY: propagation is disabled in this config; the port is never used.
38
+ getDependencyPropagationDelivery: () =>
39
+ ({
40
+ prepare: async () => null,
41
+ afterMutation: async () => {},
42
+ cancel: () => {},
43
+ deferPrepared: async () => {},
44
+ }) as never,
45
+ // SAFETY: index removal unused in this test.
46
+ embeddingFallback: { removeFromIndex: async () => {} } as never,
47
+ // SAFETY: node rebuild unused in this test.
48
+ tmtBuilder: { maybeRebuildNodes: async () => {} } as never,
49
+ consolidationObservers: new Set(),
50
+ getAccessTrackingBuffer: () => new Map(),
51
+ // SAFETY: unused passes in this config.
52
+ lifecyclePolicyCoordinator: {
53
+ runFactArchival: async () => 0,
54
+ runSummarization: async () => {},
55
+ runTopicExtraction: async () => {},
56
+ } as never,
57
+ // SAFETY: unused pass in this config.
58
+ compressionGuidelineCoordinator: {
59
+ runCompressionGuidelineLearningPass: async () => {},
60
+ } as never,
61
+ // SAFETY: unused pass in this config.
62
+ semanticConsolidationCoordinator: {} as never,
63
+ // SAFETY: unused pass in this config.
64
+ entitySynthesisCoordinator: {} as never,
65
+ // SAFETY: profile entry only.
66
+ recallSectionCoordinator: {
67
+ getRecallSectionEntry: (sectionId: string) =>
68
+ sectionId === "profile" ? PROFILE_SECTION : undefined,
69
+ } as never,
70
+ // SAFETY: unused pass in this config.
71
+ tierMigrationCoordinator: {
72
+ runCycle: async () => ({ migrated: 0 }),
73
+ } as never,
74
+ flushAccessTracking: async () => {},
75
+ indexPersistedMemory: async () => {},
76
+ autoConsolidateIdentity: async () => {},
77
+ fastChatCompletion: async () => null,
78
+ });
79
+ }
80
+
81
+ /** Extraction fake whose consolidateProfile gates AFTER receiving the old
82
+ * profile, exactly like a slow LLM call, so the test can interleave an append
83
+ * inside the real await window (no fake timing). */
84
+ // SAFETY: the stub implements exactly the two members the profile branch
85
+ // calls; the tests assert their effects instead of the stub's other surface.
86
+ function asExtractionEngine(
87
+ fake: Pick<ExtractionEngine, "consolidate" | "consolidateProfile">,
88
+ ): ExtractionEngine {
89
+ // SAFETY: callers stub only the members this Pick names.
90
+ return fake as ExtractionEngine;
91
+ }
92
+
93
+ function makeGatedConsolidationExtraction(
94
+ profilePassedToModel: PromiseWithResolvers<string>,
95
+ releaseConsolidation: PromiseWithResolvers<void>,
96
+ consolidatedProfile: string,
97
+ ): ExtractionEngine {
98
+ return asExtractionEngine({
99
+ async consolidate() {
100
+ return { items: [], profileUpdates: [], entityUpdates: [] };
101
+ },
102
+ async consolidateProfile(fullProfileContent: string) {
103
+ profilePassedToModel.resolve(fullProfileContent);
104
+ await releaseConsolidation.promise;
105
+ return { consolidatedProfile, removedCount: 2, summary: "test consolidation" };
106
+ },
107
+ });
108
+ }
109
+
110
+ async function makeStorage(): Promise<{ storage: StorageManager; cleanup: () => Promise<void> }> {
111
+ const root = await mkdtemp(path.join(os.tmpdir(), "remnic-profile-consolidation-race-"));
112
+ const storage = new StorageManager(root);
113
+ // The consolidation pass early-returns below five memories; seed enough to
114
+ // reach the profile branch.
115
+ for (let i = 0; i < 5; i++) await storage.writeMemory("fact", `seed claim ${i}`);
116
+ return { storage, cleanup: () => rm(root, { recursive: true, force: true }) };
117
+ }
118
+
119
+ const BASE_PROFILE_LINES = ["# Behavioral Profile", "", "- trait one", "- trait two", "- trait three", ""];
120
+
121
+ test("profile consolidation keeps a concurrent append instead of clobbering it", async () => {
122
+ const { storage, cleanup } = await makeStorage();
123
+ try {
124
+ await storage.writeProfile(BASE_PROFILE_LINES.join("\n"));
125
+
126
+ const profilePassedToModel = Promise.withResolvers<string>();
127
+ const releaseConsolidation = Promise.withResolvers<void>();
128
+ const extraction = makeGatedConsolidationExtraction(
129
+ profilePassedToModel,
130
+ releaseConsolidation,
131
+ "# Behavioral Profile\n\n- consolidated summary\n",
132
+ );
133
+ const coordinator = makeRaceCoordinator(storage, extraction);
134
+
135
+ const run = coordinator.run();
136
+ const baseline = await profilePassedToModel.promise;
137
+ assert.equal(baseline, await storage.readProfile(), "model must receive the pre-consolidation profile");
138
+
139
+ // Concurrent extractor appends during the real LLM await window.
140
+ await storage.appendToProfile(["unique concurrent bullet"]);
141
+
142
+ releaseConsolidation.resolve();
143
+ await run;
144
+
145
+ const profile = await storage.readProfile();
146
+ assert.match(profile, /unique concurrent bullet/, "append made during consolidation must survive");
147
+ assert.doesNotMatch(profile, /consolidated summary/, "stale consolidation must not be committed");
148
+ } finally {
149
+ await cleanup();
150
+ }
151
+ });
152
+
153
+ test("profile consolidation commits when the profile is unchanged during the LLM call", async () => {
154
+ const { storage, cleanup } = await makeStorage();
155
+ try {
156
+ await storage.writeProfile(BASE_PROFILE_LINES.join("\n"));
157
+
158
+ const profilePassedToModel = Promise.withResolvers<string>();
159
+ const releaseConsolidation = Promise.withResolvers<void>();
160
+ const extraction = makeGatedConsolidationExtraction(
161
+ profilePassedToModel,
162
+ releaseConsolidation,
163
+ "# Behavioral Profile\n\n- consolidated summary\n",
164
+ );
165
+ const coordinator = makeRaceCoordinator(storage, extraction);
166
+
167
+ const run = coordinator.run();
168
+ await profilePassedToModel.promise;
169
+ releaseConsolidation.resolve();
170
+ await run;
171
+
172
+ const profile = await storage.readProfile();
173
+ assert.match(profile, /consolidated summary/, "unchanged profile must take the consolidation");
174
+ assert.doesNotMatch(profile, /trait one/, "consolidation replaces the old bullets");
175
+ } finally {
176
+ await cleanup();
177
+ }
178
+ });
@@ -930,14 +930,26 @@ export class ConsolidationRunCoordinator {
930
930
  profileConsolidationTargetLines,
931
931
  );
932
932
  if (profileResult) {
933
- await storage.writeProfile(profileResult.consolidatedProfile);
934
- // Profile consolidation rewrites profile.md — a durable namespace
935
- // mutation; record it for the catalog touch even when it is the only
936
- // change in the pass (codex). Otherwise lastWriteAt goes stale.
937
- memoryItemMutated = true;
938
- log.info(
939
- `profile.md consolidated: removed ${profileResult.removedCount} items — ${profileResult.summary}`,
933
+ const outcome = await storage.commitConsolidatedProfile(
934
+ currentProfile,
935
+ profileResult.consolidatedProfile,
940
936
  );
937
+ if (outcome === "conflict") {
938
+ // A concurrent append landed during the LLM call. Committing the
939
+ // stale rewrite would drop it, so skip; the line cap still trips
940
+ // next pass, so consolidation retries against the fresh profile.
941
+ log.info(
942
+ "profile.md changed during consolidation — skipping stale rewrite (retries next pass)",
943
+ );
944
+ } else {
945
+ // Profile consolidation rewrites profile.md — a durable namespace
946
+ // mutation; record it for the catalog touch even when it is the only
947
+ // change in the pass (codex). Otherwise lastWriteAt goes stale.
948
+ memoryItemMutated = true;
949
+ log.info(
950
+ `profile.md consolidated: removed ${profileResult.removedCount} items — ${profileResult.summary}`,
951
+ );
952
+ }
941
953
  }
942
954
  }
943
955
  }
@@ -13,6 +13,7 @@ import { describeError } from "../buffer-turn-helpers.js";
13
13
  import { chunkExtractionWindow, classifyDrainResult, mergeDrainResults } from "../extraction-window-drain.js";
14
14
  import { log } from "../logger.js";
15
15
  import type { BufferTurn } from "../types.js";
16
+ import type { ExtractionPreemptHandle } from "./extraction-queue-coordinator.js";
16
17
  import type { ExtractionRunCoordinator, ExtractionRunResult } from "./extraction-run.js";
17
18
 
18
19
  type RunOptions = Parameters<ExtractionRunCoordinator["runExtraction"]>[1];
@@ -42,7 +43,6 @@ export interface ExtractionDrainDeps {
42
43
  buffer: {
43
44
  dropExtractionChunk(bufferKey: string, turns: readonly BufferTurn[], options?: { allowNonPrefix?: boolean }): Promise<number>;
44
45
  };
45
- extractionQueueCoordinator: { beginPreemptable(abort: () => void): void };
46
46
  }
47
47
 
48
48
  /** Identity of a chunk: its failure count must not follow a buffer key to different content. */
@@ -111,8 +111,15 @@ export class ExtractionDrain {
111
111
  * persist step ends the host's preemption registration, so the drain registers
112
112
  * it again before every later chunk; a fail-closed flush can then abort the
113
113
  * next chunk's model request instead of waiting behind the whole window.
114
+ * `preemptHandle`: the queue task's own preemption slot (per task, so two
115
+ * concurrent automatic extractions cannot clobber each other's registration).
114
116
  */
115
- async run(turns: BufferTurn[], runOptions: DrainRunOptions, preemptable = false): Promise<ExtractionRunResult> {
117
+ async run(
118
+ turns: BufferTurn[],
119
+ runOptions: DrainRunOptions,
120
+ preemptable = false,
121
+ preemptHandle?: ExtractionPreemptHandle,
122
+ ): Promise<ExtractionRunResult> {
116
123
  const budgetChars = this.deps.extractionWindowBudgetChars?.() ?? Number.POSITIVE_INFINITY;
117
124
  const chunkable =
118
125
  runOptions.clearBufferAfterExtraction &&
@@ -155,7 +162,7 @@ export class ExtractionDrain {
155
162
  if (preemptable && position > 0) {
156
163
  const local = new AbortController();
157
164
  preempt = local;
158
- this.deps.extractionQueueCoordinator.beginPreemptable(() => local.abort(preemptedExtractionError()));
165
+ preemptHandle?.arm(() => local.abort(preemptedExtractionError()));
159
166
  chunkOptions = {
160
167
  ...chunkOptions,
161
168
  abortSignal: runOptions.abortSignal ? AbortSignal.any([runOptions.abortSignal, local.signal]) : local.signal,