@remnic/core 9.42.0 → 9.44.0

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 (216) hide show
  1. package/dist/access-admin-ops-surface.d.ts +2 -2
  2. package/dist/access-admin-ops-surface.js +40 -40
  3. package/dist/access-authorization-probe.d.ts +2 -2
  4. package/dist/access-authorization-probe.js +41 -41
  5. package/dist/access-boundary.d.ts +2 -2
  6. package/dist/access-boundary.js +40 -40
  7. package/dist/access-cli.js +101 -99
  8. package/dist/access-cli.js.map +1 -1
  9. package/dist/access-extraction-force-flush.d.ts +2 -2
  10. package/dist/access-extraction-force-flush.js +40 -40
  11. package/dist/access-http-lcm-compaction.d.ts +2 -2
  12. package/dist/access-http-lifecycle-flush.d.ts +2 -2
  13. package/dist/access-http-query.d.ts +8 -0
  14. package/dist/access-http-query.js +179 -0
  15. package/dist/access-http.d.ts +2 -2
  16. package/dist/access-http.js +48 -47
  17. package/dist/access-identity-continuity-surface.d.ts +1 -1
  18. package/dist/access-identity-continuity-surface.js +40 -40
  19. package/dist/access-lcm-surface.d.ts +2 -2
  20. package/dist/access-lcm-surface.js +40 -40
  21. package/dist/access-mcp.d.ts +2 -2
  22. package/dist/access-mcp.js +44 -44
  23. package/dist/access-namespace-preflight.js +40 -40
  24. package/dist/access-observe-write-surface.d.ts +2 -2
  25. package/dist/access-observe-write-surface.js +40 -40
  26. package/dist/access-operations-batch.js +42 -42
  27. package/dist/access-operations.d.ts +2 -2
  28. package/dist/access-operations.js +43 -43
  29. package/dist/access-recall-concurrency.d.ts +2 -2
  30. package/dist/access-recall-concurrency.js +40 -40
  31. package/dist/access-recall-response.d.ts +2 -2
  32. package/dist/access-recall-response.js +40 -40
  33. package/dist/access-recall-surface.d.ts +2 -2
  34. package/dist/access-recall-surface.js +40 -40
  35. package/dist/{access-service-D1BkxwSf.d.ts → access-service-BCZJbX0f.d.ts} +12 -2
  36. package/dist/access-service-helpers.d.ts +2 -2
  37. package/dist/access-service.d.ts +2 -2
  38. package/dist/access-service.js +40 -40
  39. package/dist/access-surface-catalog.d.ts +2 -2
  40. package/dist/access-surface-catalog.js +5 -0
  41. package/dist/access-surface-catalog.js.map +1 -1
  42. package/dist/{auto-sync-UPTUVE4X.js → auto-sync-5OE5QOZB.js} +3 -3
  43. package/dist/bootstrap.d.ts +1 -1
  44. package/dist/briefing.js +8 -8
  45. package/dist/buffer.js +6 -6
  46. package/dist/causal-behavior.js +2 -2
  47. package/dist/causal-chain.js +2 -2
  48. package/dist/causal-consolidation.js +12 -12
  49. package/dist/causal-retrieval.js +2 -2
  50. package/dist/causal-trajectory-graph.js +2 -2
  51. package/dist/causal-trajectory.js +1 -1
  52. package/dist/{chunk-LEPOP46W.js → chunk-227UFSEQ.js} +4 -4
  53. package/dist/{chunk-BEQOZOYZ.js → chunk-2FTRRQKZ.js} +5 -5
  54. package/dist/{chunk-CMENJQ6I.js → chunk-2QR2RRH6.js} +2 -2
  55. package/dist/{chunk-3C2TGBDB.js → chunk-35IC3EVP.js} +2 -2
  56. package/dist/{chunk-L2DPF4DF.js → chunk-3SHB4YAW.js} +4 -4
  57. package/dist/{chunk-UV72DEZD.js → chunk-4B77XSG5.js} +5 -5
  58. package/dist/{chunk-GUQYMOPU.js → chunk-4LMA77JH.js} +5 -5
  59. package/dist/{chunk-AX2J2LC4.js → chunk-5ZE7QHTK.js} +2 -2
  60. package/dist/{chunk-YFHMW37V.js → chunk-6CZPDBJS.js} +5 -5
  61. package/dist/chunk-6ZYQ36IB.js +30 -0
  62. package/dist/chunk-6ZYQ36IB.js.map +1 -0
  63. package/dist/{chunk-QY66HJ4X.js → chunk-7PLHD7EO.js} +5 -5
  64. package/dist/{chunk-NRV4Y3PM.js → chunk-CAGPP5NB.js} +1 -1
  65. package/dist/{chunk-5TOV3D6U.js → chunk-CH2WTP6R.js} +6 -6
  66. package/dist/{chunk-AXUU7SG3.js → chunk-E6JYVPBK.js} +2 -2
  67. package/dist/{chunk-NF3U3MPG.js → chunk-ECK75GDJ.js} +2 -2
  68. package/dist/{chunk-NHRNYJPJ.js → chunk-EMC5GT4H.js} +7 -7
  69. package/dist/{chunk-Q4XL7LJW.js → chunk-EUVPB5TA.js} +55 -37
  70. package/dist/chunk-EUVPB5TA.js.map +1 -0
  71. package/dist/{chunk-PWKZV4A3.js → chunk-G24HJJ67.js} +4 -4
  72. package/dist/{chunk-OEXKYYVH.js → chunk-HGFILS2Q.js} +6 -6
  73. package/dist/{chunk-IOQFPVGY.js → chunk-HLPD7B7P.js} +2 -2
  74. package/dist/{chunk-CWPDM66T.js → chunk-HXTUA7QM.js} +28 -28
  75. package/dist/chunk-HXTUA7QM.js.map +1 -0
  76. package/dist/{chunk-WGCTFGLW.js → chunk-I22U5YYD.js} +3 -3
  77. package/dist/{chunk-MSXWUXFE.js → chunk-IADUAH35.js} +2 -2
  78. package/dist/{chunk-FHEJ22B7.js → chunk-IXFI6E3Q.js} +3 -3
  79. package/dist/{chunk-6I2C3EOH.js → chunk-J43XN7N3.js} +90 -88
  80. package/dist/chunk-J43XN7N3.js.map +1 -0
  81. package/dist/{chunk-F2KMAFEB.js → chunk-JDDTNK5P.js} +2 -2
  82. package/dist/{chunk-UGCUVLOL.js → chunk-MUKLQDJP.js} +50 -50
  83. package/dist/{chunk-5N26WBPQ.js → chunk-MYFFOPO2.js} +5 -5
  84. package/dist/{chunk-Q53RLEW7.js → chunk-N7LPS4Z3.js} +2 -2
  85. package/dist/{chunk-UZQCNOPS.js → chunk-NVRYW264.js} +12 -12
  86. package/dist/{chunk-MVW234LO.js → chunk-O4BADU6R.js} +2 -2
  87. package/dist/{chunk-ZERSIPFC.js → chunk-OHLNFI6Z.js} +17 -17
  88. package/dist/{chunk-KXGN6G2Y.js → chunk-OPVOUBHT.js} +2 -2
  89. package/dist/{chunk-JDXDA5OO.js → chunk-OZL7KB6B.js} +2 -2
  90. package/dist/{chunk-XRWVXPEM.js → chunk-PJUHQC4T.js} +2 -2
  91. package/dist/{chunk-CQ5MR2HQ.js → chunk-PLQCPMJA.js} +14 -14
  92. package/dist/{chunk-MU7ZHSZE.js → chunk-PO5LSKQO.js} +2 -2
  93. package/dist/{chunk-LRGSLV7M.js → chunk-PYHOAXTI.js} +4 -4
  94. package/dist/{chunk-ONHRRJZM.js → chunk-QJAQNXWI.js} +6 -6
  95. package/dist/{chunk-UQVGDEAJ.js → chunk-TRBIJS2T.js} +2 -2
  96. package/dist/{chunk-6GIMMNVK.js → chunk-U5SX74DV.js} +4 -4
  97. package/dist/{chunk-KPJQTSRJ.js → chunk-UUQLB2HJ.js} +2 -2
  98. package/dist/{chunk-JYIFGGEM.js → chunk-UZGJFIIG.js} +2 -2
  99. package/dist/{chunk-HNZKVLAN.js → chunk-W6ROK2KU.js} +2 -2
  100. package/dist/{chunk-7VIB7PAE.js → chunk-WMHO3WLC.js} +2 -2
  101. package/dist/{chunk-OYFDEJZB.js → chunk-WUDWRUGK.js} +2 -2
  102. package/dist/{chunk-4VXGRIH6.js → chunk-WUNIWKY4.js} +5 -5
  103. package/dist/{chunk-BI6RE2ZS.js → chunk-WX6KOVVK.js} +2 -2
  104. package/dist/chunk-YUOKZ3LJ.js +24 -0
  105. package/dist/chunk-YUOKZ3LJ.js.map +1 -0
  106. package/dist/{cli-IUhV-lmk.d.ts → cli-U30A3jyx.d.ts} +2 -2
  107. package/dist/cli.d.ts +3 -3
  108. package/dist/cli.js +70 -69
  109. package/dist/compounding/engine.js +8 -8
  110. package/dist/connectors/codex-materialize-runner.js +8 -8
  111. package/dist/connectors/index.js +8 -8
  112. package/dist/convergence-refresh.d.ts +24 -0
  113. package/dist/convergence-refresh.js +74 -0
  114. package/dist/convergence-refresh.js.map +1 -0
  115. package/dist/conversation-index/backend.js +2 -2
  116. package/dist/corpus-watermark.js +20 -20
  117. package/dist/entity-retrieval.js +8 -8
  118. package/dist/explicit-capture.d.ts +1 -1
  119. package/dist/explicit-capture.js +3 -3
  120. package/dist/extraction.js +14 -14
  121. package/dist/{graph-edge-decay-Z6ENALU2.js → graph-edge-decay-GF3ZSGSP.js} +4 -4
  122. package/dist/graph-edge-decay-GF3ZSGSP.js.map +1 -0
  123. package/dist/graph-snapshot.js +3 -3
  124. package/dist/graph.js +2 -2
  125. package/dist/index.d.ts +4 -4
  126. package/dist/index.js +161 -159
  127. package/dist/lcm/index.js +3 -3
  128. package/dist/maintenance/memory-governance.js +9 -9
  129. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +8 -8
  130. package/dist/maintenance/rebuild-memory-projection.js +10 -10
  131. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  132. package/dist/namespaces/migrate.js +19 -19
  133. package/dist/namespaces/search.js +10 -10
  134. package/dist/namespaces/storage.js +8 -8
  135. package/dist/offline-sync-impression-drain.js +4 -4
  136. package/dist/operator-doctor-corpus.js +21 -21
  137. package/dist/operator-toolkit.js +30 -30
  138. package/dist/orchestration/maintenance.js +15 -15
  139. package/dist/{orchestrator-fYTncnLy.d.ts → orchestrator-DUxbhhqO.d.ts} +1 -1
  140. package/dist/orchestrator.d.ts +1 -1
  141. package/dist/orchestrator.js +102 -100
  142. package/dist/qmd.js +4 -4
  143. package/dist/reconcile/cursor.d.ts +21 -0
  144. package/dist/reconcile/cursor.js +104 -0
  145. package/dist/reconcile/cursor.js.map +1 -0
  146. package/dist/resume-bundles.js +1 -1
  147. package/dist/search/factory.js +9 -9
  148. package/dist/search/index.js +17 -17
  149. package/dist/semantic-consolidation.js +10 -10
  150. package/dist/semantic-rule-promotion.js +8 -8
  151. package/dist/semantic-rule-verifier.js +9 -9
  152. package/dist/source-agent-qualifier.js +13 -13
  153. package/dist/storage.js +7 -7
  154. package/dist/transfer/import-sqlite.js +2 -2
  155. package/dist/verified-recall.js +9 -9
  156. package/package.json +12 -2
  157. package/src/access-http-query.ts +25 -0
  158. package/src/access-http.test.ts +136 -0
  159. package/src/access-http.ts +26 -26
  160. package/src/access-service-offline-file-content.test.ts +40 -0
  161. package/src/access-service.ts +34 -0
  162. package/src/access-surface-catalog.ts +5 -0
  163. package/src/convergence-refresh.ts +28 -0
  164. package/src/orchestrator-convergence-refresh.test.ts +114 -0
  165. package/src/orchestrator.ts +5 -4
  166. package/src/reconcile/cursor.test.ts +123 -0
  167. package/src/reconcile/cursor.ts +130 -0
  168. package/dist/chunk-6I2C3EOH.js.map +0 -1
  169. package/dist/chunk-CWPDM66T.js.map +0 -1
  170. package/dist/chunk-Q4XL7LJW.js.map +0 -1
  171. /package/dist/{graph-edge-decay-Z6ENALU2.js.map → access-http-query.js.map} +0 -0
  172. /package/dist/{auto-sync-UPTUVE4X.js.map → auto-sync-5OE5QOZB.js.map} +0 -0
  173. /package/dist/{chunk-LEPOP46W.js.map → chunk-227UFSEQ.js.map} +0 -0
  174. /package/dist/{chunk-BEQOZOYZ.js.map → chunk-2FTRRQKZ.js.map} +0 -0
  175. /package/dist/{chunk-CMENJQ6I.js.map → chunk-2QR2RRH6.js.map} +0 -0
  176. /package/dist/{chunk-3C2TGBDB.js.map → chunk-35IC3EVP.js.map} +0 -0
  177. /package/dist/{chunk-L2DPF4DF.js.map → chunk-3SHB4YAW.js.map} +0 -0
  178. /package/dist/{chunk-UV72DEZD.js.map → chunk-4B77XSG5.js.map} +0 -0
  179. /package/dist/{chunk-GUQYMOPU.js.map → chunk-4LMA77JH.js.map} +0 -0
  180. /package/dist/{chunk-AX2J2LC4.js.map → chunk-5ZE7QHTK.js.map} +0 -0
  181. /package/dist/{chunk-YFHMW37V.js.map → chunk-6CZPDBJS.js.map} +0 -0
  182. /package/dist/{chunk-QY66HJ4X.js.map → chunk-7PLHD7EO.js.map} +0 -0
  183. /package/dist/{chunk-NRV4Y3PM.js.map → chunk-CAGPP5NB.js.map} +0 -0
  184. /package/dist/{chunk-5TOV3D6U.js.map → chunk-CH2WTP6R.js.map} +0 -0
  185. /package/dist/{chunk-AXUU7SG3.js.map → chunk-E6JYVPBK.js.map} +0 -0
  186. /package/dist/{chunk-NF3U3MPG.js.map → chunk-ECK75GDJ.js.map} +0 -0
  187. /package/dist/{chunk-NHRNYJPJ.js.map → chunk-EMC5GT4H.js.map} +0 -0
  188. /package/dist/{chunk-PWKZV4A3.js.map → chunk-G24HJJ67.js.map} +0 -0
  189. /package/dist/{chunk-OEXKYYVH.js.map → chunk-HGFILS2Q.js.map} +0 -0
  190. /package/dist/{chunk-IOQFPVGY.js.map → chunk-HLPD7B7P.js.map} +0 -0
  191. /package/dist/{chunk-WGCTFGLW.js.map → chunk-I22U5YYD.js.map} +0 -0
  192. /package/dist/{chunk-MSXWUXFE.js.map → chunk-IADUAH35.js.map} +0 -0
  193. /package/dist/{chunk-FHEJ22B7.js.map → chunk-IXFI6E3Q.js.map} +0 -0
  194. /package/dist/{chunk-F2KMAFEB.js.map → chunk-JDDTNK5P.js.map} +0 -0
  195. /package/dist/{chunk-UGCUVLOL.js.map → chunk-MUKLQDJP.js.map} +0 -0
  196. /package/dist/{chunk-5N26WBPQ.js.map → chunk-MYFFOPO2.js.map} +0 -0
  197. /package/dist/{chunk-Q53RLEW7.js.map → chunk-N7LPS4Z3.js.map} +0 -0
  198. /package/dist/{chunk-UZQCNOPS.js.map → chunk-NVRYW264.js.map} +0 -0
  199. /package/dist/{chunk-MVW234LO.js.map → chunk-O4BADU6R.js.map} +0 -0
  200. /package/dist/{chunk-ZERSIPFC.js.map → chunk-OHLNFI6Z.js.map} +0 -0
  201. /package/dist/{chunk-KXGN6G2Y.js.map → chunk-OPVOUBHT.js.map} +0 -0
  202. /package/dist/{chunk-JDXDA5OO.js.map → chunk-OZL7KB6B.js.map} +0 -0
  203. /package/dist/{chunk-XRWVXPEM.js.map → chunk-PJUHQC4T.js.map} +0 -0
  204. /package/dist/{chunk-CQ5MR2HQ.js.map → chunk-PLQCPMJA.js.map} +0 -0
  205. /package/dist/{chunk-MU7ZHSZE.js.map → chunk-PO5LSKQO.js.map} +0 -0
  206. /package/dist/{chunk-LRGSLV7M.js.map → chunk-PYHOAXTI.js.map} +0 -0
  207. /package/dist/{chunk-ONHRRJZM.js.map → chunk-QJAQNXWI.js.map} +0 -0
  208. /package/dist/{chunk-UQVGDEAJ.js.map → chunk-TRBIJS2T.js.map} +0 -0
  209. /package/dist/{chunk-6GIMMNVK.js.map → chunk-U5SX74DV.js.map} +0 -0
  210. /package/dist/{chunk-KPJQTSRJ.js.map → chunk-UUQLB2HJ.js.map} +0 -0
  211. /package/dist/{chunk-JYIFGGEM.js.map → chunk-UZGJFIIG.js.map} +0 -0
  212. /package/dist/{chunk-HNZKVLAN.js.map → chunk-W6ROK2KU.js.map} +0 -0
  213. /package/dist/{chunk-7VIB7PAE.js.map → chunk-WMHO3WLC.js.map} +0 -0
  214. /package/dist/{chunk-OYFDEJZB.js.map → chunk-WUDWRUGK.js.map} +0 -0
  215. /package/dist/{chunk-4VXGRIH6.js.map → chunk-WUNIWKY4.js.map} +0 -0
  216. /package/dist/{chunk-BI6RE2ZS.js.map → chunk-WX6KOVVK.js.map} +0 -0
@@ -18,6 +18,7 @@ import {
18
18
  type EngramAccessWriteResponse,
19
19
  } from "./access-service.js";
20
20
  import { maybeHandleLifecycleFlush, type LifecycleFlushHttpDeps } from "./access-http-lifecycle-flush.js";
21
+ import { nonEmptyQueryParam, optionalQueryString, positiveIntQueryParam } from "./access-http-query.js";
21
22
  import { CorrectionContractError } from "./correction/correction-contract.js";
22
23
  import { WearablesInputError } from "./wearables/errors.js"; import { respondMeetingsList, respondMeetingsGet, respondMeetingsBuild } from "./meetings/http-glue.js";
23
24
  import { EngramMcpServer, MCP_SUPPORTED_PROTOCOL_VERSIONS } from "./access-mcp.js";
@@ -1322,6 +1323,31 @@ export class EngramAccessHttpServer {
1322
1323
  return;
1323
1324
  }
1324
1325
 
1326
+ if (
1327
+ req.method === "POST" &&
1328
+ (
1329
+ pathname === "/engram/v1/offline-sync/convergence-complete" ||
1330
+ pathname === "/remnic/v1/offline-sync/convergence-complete"
1331
+ )
1332
+ ) {
1333
+ this.enforceTokenOp("offline_sync_apply_file_content");
1334
+ const namespaceParams = parsed.searchParams.getAll("namespace").filter(Boolean);
1335
+ const requestedNamespaces: Array<string | undefined> =
1336
+ namespaceParams.length > 0 ? namespaceParams : [undefined];
1337
+ const namespaces = requestedNamespaces
1338
+ .map((namespace) => this.resolveNamespace(req, namespace))
1339
+ .filter((namespace): namespace is string => namespace !== undefined);
1340
+ this.ensureWriteRateLimitAvailable(req);
1341
+ const result = await this.service.offlineSyncFinalizeConvergence({
1342
+ ...(namespaces.length > 0 ? { namespaces } : {}),
1343
+ principal: this.resolveRequestPrincipal(req),
1344
+ sourceId: this.readRequiredDecodedHeader(req, "x-remnic-source-id"),
1345
+ });
1346
+ this.recordWriteRateLimitHit(req);
1347
+ this.respondJson(res, 200, result);
1348
+ return;
1349
+ }
1350
+
1325
1351
  if (
1326
1352
  req.method === "POST" &&
1327
1353
  (pathname === "/engram/v1/offline-sync/apply" || pathname === "/remnic/v1/offline-sync/apply")
@@ -3925,29 +3951,3 @@ export class EngramAccessHttpServer {
3925
3951
  return false;
3926
3952
  }
3927
3953
  }
3928
-
3929
- /** Optional string field from a JSON body: absent/null/"" → undefined. */
3930
- function optionalQueryString(value: unknown, label: string): string | undefined {
3931
- if (value === undefined || value === null || value === "") return undefined;
3932
- if (typeof value !== "string") {
3933
- throw new EngramAccessInputError(
3934
- `${label} must be a string (got ${JSON.stringify(value)})`,
3935
- );
3936
- }
3937
- return value;
3938
- }
3939
-
3940
- /** Optional non-empty query param: null/"" → undefined. */
3941
- function nonEmptyQueryParam(value: string | null): string | undefined {
3942
- return value !== null && value.length > 0 ? value : undefined;
3943
- }
3944
-
3945
- /** Optional positive-integer query param; rejects invalid values. */
3946
- function positiveIntQueryParam(value: string | null, label: string): number | undefined {
3947
- if (value === null || value.length === 0) return undefined;
3948
- const parsed = Number(value);
3949
- if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
3950
- throw new EngramAccessInputError(`${label} expects a positive integer`);
3951
- }
3952
- return parsed;
3953
- }
@@ -52,3 +52,43 @@ test("offline apply-file-content reports invalid metadata as input errors", asyn
52
52
  /mtimeMs must be within JavaScript Date range/.test(error.message),
53
53
  );
54
54
  });
55
+
56
+ test("offline convergence completion refreshes authorized namespaces as one batch and rejects other sources", async () => {
57
+ const refreshed: string[][] = [];
58
+ const orchestratorStub = {
59
+ config: {
60
+ memoryDir: "/tmp/remnic-access-service-convergence-test",
61
+ namespacesEnabled: false,
62
+ defaultNamespace: "global",
63
+ sharedNamespace: "shared",
64
+ },
65
+ refreshNamespacesAfterConvergence: async (namespaces: readonly string[]) => {
66
+ refreshed.push([...namespaces]);
67
+ },
68
+ };
69
+ const service = new EngramAccessService(
70
+ orchestratorStub as unknown as ConstructorParameters<typeof EngramAccessService>[0],
71
+ );
72
+ const convergenceService = service as unknown as {
73
+ offlineSyncFinalizeConvergence(options: {
74
+ namespaces?: string[];
75
+ principal?: string;
76
+ sourceId: string;
77
+ }): Promise<{ namespaces: string[]; refreshed: true }>;
78
+ };
79
+
80
+ await assert.rejects(
81
+ () => convergenceService.offlineSyncFinalizeConvergence({ sourceId: "laptop" }),
82
+ (error) =>
83
+ error instanceof EngramAccessInputError
84
+ && /sourceId must be remnic-converge/.test(error.message),
85
+ );
86
+ assert.deepEqual(refreshed, []);
87
+
88
+ const result = await convergenceService.offlineSyncFinalizeConvergence({
89
+ sourceId: "remnic-converge",
90
+ });
91
+
92
+ assert.deepEqual(result, { namespaces: ["global"], refreshed: true });
93
+ assert.deepEqual(refreshed, [["global"]]);
94
+ });
@@ -818,6 +818,12 @@ export interface EngramAccessOfflineSyncApplyRequest {
818
818
  returnCurrentFiles?: boolean;
819
819
  }
820
820
 
821
+ export interface EngramAccessOfflineSyncFinalizeConvergenceRequest {
822
+ namespaces?: string[];
823
+ principal?: string;
824
+ sourceId: string;
825
+ }
826
+
821
827
  export interface EngramAccessOfflineSyncSnapshotResponse extends OfflineSyncSnapshot {
822
828
  namespace: string;
823
829
  }
@@ -843,6 +849,11 @@ export interface EngramAccessOfflineSyncApplyResponse extends OfflineSyncApplyCh
843
849
  namespace: string;
844
850
  }
845
851
 
852
+ export interface EngramAccessOfflineSyncFinalizeConvergenceResponse {
853
+ namespaces: string[];
854
+ refreshed: true;
855
+ }
856
+
846
857
  export type EngramAccessActionConfidenceRequest = ActionConfidenceInput;
847
858
  export type EngramAccessActionConfidenceResponse = ActionConfidenceResult;
848
859
 
@@ -5621,6 +5632,29 @@ export class EngramAccessService {
5621
5632
  }
5622
5633
  }
5623
5634
 
5635
+ async offlineSyncFinalizeConvergence(
5636
+ options: EngramAccessOfflineSyncFinalizeConvergenceRequest,
5637
+ ): Promise<EngramAccessOfflineSyncFinalizeConvergenceResponse> {
5638
+ if (options.sourceId !== "remnic-converge") {
5639
+ throw new EngramAccessInputError("sourceId must be remnic-converge");
5640
+ }
5641
+ const requestedNamespaces = options.namespaces?.length
5642
+ ? options.namespaces
5643
+ : [undefined];
5644
+ const resolvedNamespaces = Array.from(new Set(
5645
+ requestedNamespaces.map((namespace) => this.writableNamespaceFor(
5646
+ namespace,
5647
+ undefined,
5648
+ options.principal,
5649
+ )),
5650
+ ));
5651
+ await this.orchestrator.refreshNamespacesAfterConvergence(resolvedNamespaces);
5652
+ return {
5653
+ namespaces: resolvedNamespaces,
5654
+ refreshed: true,
5655
+ };
5656
+ }
5657
+
5624
5658
  async offlineSyncApply(
5625
5659
  options: EngramAccessOfflineSyncApplyRequest,
5626
5660
  ): Promise<EngramAccessOfflineSyncApplyResponse> {
@@ -183,6 +183,11 @@ export const HTTP_ROUTES: readonly HttpRouteEntry[] = [
183
183
  { method: "POST", pathname: "/engram/v1/offline-sync/files", operation: "offline_sync_files" },
184
184
  { method: "POST", pathname: "/engram/v1/offline-sync/file-content", operation: "offline_sync_file_content" },
185
185
  { method: "POST", pathname: "/engram/v1/offline-sync/apply-file-content", operation: "offline_sync_apply_file_content" },
186
+ {
187
+ method: "POST",
188
+ pathname: "/engram/v1/offline-sync/convergence-complete",
189
+ operation: "offline_sync_apply_file_content",
190
+ },
186
191
  { method: "POST", pathname: "/engram/v1/offline-sync/apply", operation: "offline_sync_apply" },
187
192
  { method: "POST", pathname: "/engram/v1/recall/explain", operation: "recall_explain" },
188
193
  { method: "POST", pathname: "/engram/v1/action-confidence", operation: "action_confidence" },
@@ -0,0 +1,28 @@
1
+ import {
2
+ type RebuildMemoryProjectionOptions,
3
+ rebuildMemoryProjection,
4
+ } from "./maintenance/rebuild-memory-projection.js";
5
+ import type { NamespaceSearchRouter } from "./namespaces/search.js";
6
+
7
+ type ConvergenceStorage = NonNullable<RebuildMemoryProjectionOptions["storage"]>;
8
+
9
+ export async function refreshConvergedNamespaces(
10
+ namespaces: readonly string[],
11
+ namespaceSearchRouter: Pick<NamespaceSearchRouter, "updateNamespacesDetailed">,
12
+ getStorage: (namespace: string) => Promise<ConvergenceStorage>
13
+ ): Promise<void> {
14
+ const targetNamespaces = Array.from(new Set(namespaces.map((namespace) => namespace.trim()).filter(Boolean)));
15
+ if (targetNamespaces.length === 0) return;
16
+
17
+ await namespaceSearchRouter.updateNamespacesDetailed(targetNamespaces, undefined, { strict: true });
18
+
19
+ for (const namespace of targetNamespaces) {
20
+ const storage = await getStorage(namespace);
21
+ await rebuildMemoryProjection({
22
+ memoryDir: storage.dir,
23
+ storage,
24
+ defaultNamespace: namespace,
25
+ dryRun: false,
26
+ });
27
+ }
28
+ }
@@ -0,0 +1,114 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+
7
+ import { readProjectedMemoryBrowse } from "./memory-projection-store.js";
8
+ import { Orchestrator } from "./orchestrator.js";
9
+ import { StorageManager } from "./storage.js";
10
+
11
+ test("refreshNamespacesAfterConvergence runs one strict QMD batch and rebuilds each namespace projection once", async () => {
12
+ const teamDir = await mkdtemp(path.join(os.tmpdir(), "remnic-convergence-team-"));
13
+ const sharedDir = await mkdtemp(path.join(os.tmpdir(), "remnic-convergence-shared-"));
14
+ try {
15
+ const teamStorage = new StorageManager(teamDir);
16
+ const sharedStorage = new StorageManager(sharedDir);
17
+ const teamMemory = await teamStorage.writeMemory("fact", "A received team fact.", {
18
+ source: "test",
19
+ });
20
+ const sharedMemory = await sharedStorage.writeMemory("fact", "A received shared fact.", {
21
+ source: "test",
22
+ });
23
+ const qmdCalls: Array<{
24
+ namespaces: string[];
25
+ options: { strict?: boolean } | undefined;
26
+ }> = [];
27
+ const storageCalls: string[] = [];
28
+ const orchestrator = Object.create(Orchestrator.prototype) as Orchestrator;
29
+ const internals = orchestrator as unknown as {
30
+ config: { defaultNamespace: string };
31
+ namespaceSearchRouter: {
32
+ updateNamespacesDetailed(
33
+ namespaces: string[],
34
+ execution?: unknown,
35
+ options?: { strict?: boolean }
36
+ ): Promise<{ backendCount: number; eligibleNamespaces: string[] }>;
37
+ };
38
+ getStorage(namespace: string): Promise<StorageManager>;
39
+ };
40
+ internals.config = { defaultNamespace: "default" };
41
+ internals.namespaceSearchRouter = {
42
+ async updateNamespacesDetailed(namespaces, _execution, options) {
43
+ qmdCalls.push({ namespaces, options });
44
+ return { backendCount: 1, eligibleNamespaces: namespaces };
45
+ },
46
+ };
47
+ const storageByNamespace = new Map([
48
+ ["team", teamStorage],
49
+ ["shared", sharedStorage],
50
+ ]);
51
+ internals.getStorage = async (namespace) => {
52
+ storageCalls.push(namespace);
53
+ const storage = storageByNamespace.get(namespace);
54
+ assert.ok(storage);
55
+ return storage;
56
+ };
57
+ const refreshSurface = orchestrator as unknown as {
58
+ refreshNamespacesAfterConvergence(namespaces: readonly string[]): Promise<void>;
59
+ };
60
+
61
+ await refreshSurface.refreshNamespacesAfterConvergence(["team", "shared", "team"]);
62
+
63
+ assert.deepEqual(qmdCalls, [
64
+ {
65
+ namespaces: ["team", "shared"],
66
+ options: { strict: true },
67
+ },
68
+ ]);
69
+ assert.deepEqual(storageCalls, ["team", "shared"]);
70
+ const teamProjection = readProjectedMemoryBrowse(teamDir, { limit: 10, offset: 0 });
71
+ const sharedProjection = readProjectedMemoryBrowse(sharedDir, { limit: 10, offset: 0 });
72
+ assert.equal(teamProjection?.memories[0]?.id, teamMemory.id);
73
+ assert.equal(sharedProjection?.memories[0]?.id, sharedMemory.id);
74
+ } finally {
75
+ await Promise.all([rm(teamDir, { recursive: true, force: true }), rm(sharedDir, { recursive: true, force: true })]);
76
+ }
77
+ });
78
+
79
+ test("refreshNamespacesAfterConvergence rebuilds projection when search is disabled", async () => {
80
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-convergence-no-search-"));
81
+ try {
82
+ const storage = new StorageManager(memoryDir);
83
+ const memory = await storage.writeMemory("fact", "A received fact without search enabled.", {
84
+ source: "test",
85
+ });
86
+ const orchestrator = Object.create(Orchestrator.prototype) as Orchestrator;
87
+ const internals = orchestrator as unknown as {
88
+ namespaceSearchRouter: {
89
+ updateNamespacesDetailed(
90
+ namespaces: string[],
91
+ execution?: unknown,
92
+ options?: { strict?: boolean }
93
+ ): Promise<{ backendCount: number; eligibleNamespaces: string[] }>;
94
+ };
95
+ getStorage(namespace: string): Promise<StorageManager>;
96
+ };
97
+ internals.namespaceSearchRouter = {
98
+ async updateNamespacesDetailed() {
99
+ return { backendCount: 0, eligibleNamespaces: [] };
100
+ },
101
+ };
102
+ internals.getStorage = async () => storage;
103
+ const refreshSurface = orchestrator as unknown as {
104
+ refreshNamespacesAfterConvergence(namespaces: readonly string[]): Promise<void>;
105
+ };
106
+
107
+ await refreshSurface.refreshNamespacesAfterConvergence(["default"]);
108
+
109
+ const projection = readProjectedMemoryBrowse(memoryDir, { limit: 10, offset: 0 });
110
+ assert.equal(projection?.memories[0]?.id, memory.id);
111
+ } finally {
112
+ await rm(memoryDir, { recursive: true, force: true });
113
+ }
114
+ });
@@ -218,6 +218,7 @@ import {
218
218
  runPatternReinforcement,
219
219
  type PatternReinforcementResult,
220
220
  } from "./maintenance/pattern-reinforcement.js";
221
+ import { refreshConvergedNamespaces } from "./convergence-refresh.js";
221
222
  import { ModelRegistry } from "./model-registry.js";
222
223
  import { applyRuntimeRetrievalPolicy, expandQuery } from "./retrieval.js";
223
224
  import {
@@ -1278,11 +1279,11 @@ export class Orchestrator {
1278
1279
  return await this.namespaceSearchRouter.healthForNamespace(namespace, execution);
1279
1280
  }
1280
1281
 
1281
- private isSearchAvailableForNamespaceRouting(): boolean {
1282
- if (resolveNamespaceCapabilities(this.config).namespaces) return true;
1283
- return this.qmd.isAvailable();
1282
+ async refreshNamespacesAfterConvergence(namespaces: readonly string[]): Promise<void> {
1283
+ return refreshConvergedNamespaces(namespaces, this.namespaceSearchRouter, this.getStorage.bind(this));
1284
1284
  }
1285
1285
 
1286
+
1286
1287
  invalidateLiveContentHashIndex(): void {
1287
1288
  this.contentHashIndex = null;
1288
1289
  this.contentHashIndexesByStorageDir.clear();
@@ -1530,7 +1531,7 @@ export class Orchestrator {
1530
1531
  });
1531
1532
  this.contradictionLinkingCoordinator = new ContradictionLinkingCoordinator({
1532
1533
  getConfig: () => this.config,
1533
- isSearchAvailable: () => this.isSearchAvailableForNamespaceRouting(),
1534
+ isSearchAvailable: () => resolveNamespaceCapabilities(this.config).namespaces || this.qmd.isAvailable(),
1534
1535
  searchAcrossNamespaces: (options) => this.searchAcrossNamespaces(options),
1535
1536
  extractMemoryIdsFromResults: (results) => this.extractMemoryIdsFromResults(results),
1536
1537
  namespaceFromPath: (p) => this.namespaceFromPath(p),
@@ -0,0 +1,123 @@
1
+ import * as assert from "node:assert/strict";
2
+ import * as fs from "node:fs/promises";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { test } from "node:test";
6
+ import {
7
+ defaultConvergeCursorPath,
8
+ hashPeerNamespace,
9
+ normalizeConvergeCursor,
10
+ readConvergeCursor,
11
+ writeConvergeCursor,
12
+ type ConvergeCursorState,
13
+ } from "./cursor.js";
14
+
15
+ const shaA = "a".repeat(64);
16
+
17
+ test("hashPeerNamespace: deterministic key normalization", () => {
18
+ const k1 = hashPeerNamespace("http://peer.example.com/", "default");
19
+ const k2 = hashPeerNamespace("http://peer.example.com", "DEFAULT ");
20
+ assert.equal(k1, k2);
21
+ assert.equal(k1.length, 16);
22
+ });
23
+
24
+ test("hashPeerNamespace: accepts non-URL local peer identifiers", () => {
25
+ assert.equal(hashPeerNamespace("local", "default").length, 16);
26
+ });
27
+
28
+ test("hashPeerNamespace: preserves path case while normalizing URL authority and trailing slash", () => {
29
+ const uppercasePath = hashPeerNamespace("HTTP://PEER.EXAMPLE.COM/Memory/", "default");
30
+ const lowercasePath = hashPeerNamespace("http://peer.example.com/memory", "default");
31
+
32
+ assert.notEqual(uppercasePath, lowercasePath);
33
+ assert.equal(
34
+ uppercasePath,
35
+ hashPeerNamespace("http://peer.example.com/Memory", "DEFAULT "),
36
+ );
37
+ assert.equal(
38
+ lowercasePath,
39
+ hashPeerNamespace("HTTP://PEER.EXAMPLE.COM/memory/", "default"),
40
+ );
41
+ });
42
+
43
+ test("defaultConvergeCursorPath: constructs path under memoryDir state", () => {
44
+ const p = defaultConvergeCursorPath("/tmp/mem", "http://localhost:4318", "general");
45
+ assert.ok(p.includes(path.join(".remnic", "state", "converge-cursors")));
46
+ assert.ok(p.endsWith(".json"));
47
+ });
48
+
49
+ test("normalizeConvergeCursor: validates envelope shape", () => {
50
+ assert.throws(() => normalizeConvergeCursor(null), /must be an object/);
51
+ assert.throws(() => normalizeConvergeCursor({ version: 2 }), /version must be 1/);
52
+ assert.throws(
53
+ () => normalizeConvergeCursor({ version: 1, peerUrl: "" }),
54
+ /missing peerUrl/,
55
+ );
56
+ assert.throws(
57
+ () => normalizeConvergeCursor({ version: 1, peerUrl: "http://peer", namespace: "" }),
58
+ /missing namespace/,
59
+ );
60
+
61
+ const valid = normalizeConvergeCursor({
62
+ version: 1,
63
+ peerUrl: "http://peer",
64
+ namespace: "default",
65
+ baseFiles: [{ path: "a.md", sha256: shaA }],
66
+ });
67
+ assert.equal(valid.peerUrl, "http://peer");
68
+ assert.equal(valid.namespace, "default");
69
+ assert.equal(valid.baseFiles.length, 1);
70
+ assert.equal(valid.baseFiles[0]?.path, "a.md");
71
+ });
72
+
73
+ test("readConvergeCursor: returns null when file is missing or invalid", async () => {
74
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "remnic-cursor-test-"));
75
+ try {
76
+ const missing = await readConvergeCursor(path.join(tmpDir, "missing.json"));
77
+ assert.equal(missing, null);
78
+
79
+ const invalidPath = path.join(tmpDir, "invalid.json");
80
+ await fs.writeFile(invalidPath, "{ invalid json", "utf-8");
81
+ const invalid = await readConvergeCursor(invalidPath);
82
+ assert.equal(invalid, null);
83
+ } finally {
84
+ await fs.rm(tmpDir, { recursive: true, force: true });
85
+ }
86
+ });
87
+
88
+ test("writeConvergeCursor & readConvergeCursor: atomic write roundtrip", async () => {
89
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "remnic-cursor-test-"));
90
+ const cursorPath = path.join(tmpDir, "state", "cursor.json");
91
+ try {
92
+ const cursor: ConvergeCursorState = {
93
+ version: 1,
94
+ peerUrl: "http://localhost:4318",
95
+ namespace: "default",
96
+ lastConvergedAt: new Date().toISOString(),
97
+ baseFiles: [{ path: "facts/a.md", sha256: shaA, bytes: 120, mtimeMs: 1000 }],
98
+ completedPaths: ["facts/a.md"],
99
+ };
100
+
101
+ await writeConvergeCursor(cursorPath, cursor);
102
+ const read = await readConvergeCursor(cursorPath);
103
+ assert.ok(read);
104
+ assert.equal(read.peerUrl, "http://localhost:4318");
105
+ assert.equal(read.namespace, "default");
106
+ assert.equal(read.baseFiles.length, 1);
107
+ assert.equal(read.baseFiles[0]?.sha256, shaA);
108
+ assert.deepEqual(read.completedPaths, ["facts/a.md"]);
109
+
110
+ // Update existing cursor atomically
111
+ const updated: ConvergeCursorState = {
112
+ ...read,
113
+ lastConvergedAt: new Date().toISOString(),
114
+ completedPaths: ["facts/a.md", "facts/b.md"],
115
+ };
116
+ await writeConvergeCursor(cursorPath, updated);
117
+ const readUpdated = await readConvergeCursor(cursorPath);
118
+ assert.ok(readUpdated);
119
+ assert.equal(readUpdated.completedPaths?.length, 2);
120
+ } finally {
121
+ await fs.rm(tmpDir, { recursive: true, force: true });
122
+ }
123
+ });
@@ -0,0 +1,130 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+
5
+ export interface ConvergeCursorFileState {
6
+ path: string;
7
+ sha256: string;
8
+ mtimeMs?: number;
9
+ bytes?: number;
10
+ }
11
+
12
+ export interface ConvergeCursorState {
13
+ version: 1;
14
+ peerUrl: string;
15
+ namespace: string;
16
+ lastConvergedAt?: string;
17
+ baseFiles: ConvergeCursorFileState[];
18
+ completedPaths?: string[];
19
+ }
20
+
21
+ export function hashPeerNamespace(peerUrl: string, namespace: string): string {
22
+ let normalizedUrl: string;
23
+ try {
24
+ const url = new URL(peerUrl);
25
+ const credentials =
26
+ url.username || url.password
27
+ ? `${url.username}${url.password ? `:${url.password}` : ""}@`
28
+ : "";
29
+ normalizedUrl =
30
+ `${url.protocol.toLowerCase()}//${credentials}${url.hostname.toLowerCase()}` +
31
+ `${url.port ? `:${url.port}` : ""}${url.pathname.replace(/\/+$/, "")}${url.search}${url.hash}`;
32
+ } catch {
33
+ normalizedUrl = peerUrl.trim().replace(/\/+$/, "").toLowerCase();
34
+ }
35
+ const normalizedNs = namespace.trim().toLowerCase();
36
+ return createHash("sha256")
37
+ .update(`${normalizedUrl}\0${normalizedNs}`)
38
+ .digest("hex")
39
+ .slice(0, 16);
40
+ }
41
+
42
+ export function defaultConvergeCursorPath(
43
+ memoryDir: string,
44
+ peerUrl: string,
45
+ namespace: string,
46
+ ): string {
47
+ const key = hashPeerNamespace(peerUrl, namespace);
48
+ return path.join(path.resolve(memoryDir), ".remnic", "state", "converge-cursors", `${key}.json`);
49
+ }
50
+
51
+ export function normalizeConvergeCursor(input: unknown): ConvergeCursorState {
52
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
53
+ throw new Error("converge cursor must be an object");
54
+ }
55
+ const obj = input as Record<string, unknown>;
56
+ if (obj.version !== 1) {
57
+ throw new Error("converge cursor version must be 1");
58
+ }
59
+ if (typeof obj.peerUrl !== "string" || !obj.peerUrl.trim()) {
60
+ throw new Error("converge cursor missing peerUrl");
61
+ }
62
+ if (typeof obj.namespace !== "string" || !obj.namespace.trim()) {
63
+ throw new Error("converge cursor missing namespace");
64
+ }
65
+ const baseFiles: ConvergeCursorFileState[] = [];
66
+ if (Array.isArray(obj.baseFiles)) {
67
+ for (const item of obj.baseFiles) {
68
+ if (item && typeof item === "object") {
69
+ const fileItem = item as Record<string, unknown>;
70
+ if (typeof fileItem.path === "string" && typeof fileItem.sha256 === "string") {
71
+ baseFiles.push({
72
+ path: fileItem.path,
73
+ sha256: fileItem.sha256,
74
+ mtimeMs: typeof fileItem.mtimeMs === "number" ? fileItem.mtimeMs : undefined,
75
+ bytes: typeof fileItem.bytes === "number" ? fileItem.bytes : undefined,
76
+ });
77
+ }
78
+ }
79
+ }
80
+ }
81
+ const completedPaths: string[] = [];
82
+ if (Array.isArray(obj.completedPaths)) {
83
+ for (const item of obj.completedPaths) {
84
+ if (typeof item === "string") {
85
+ completedPaths.push(item);
86
+ }
87
+ }
88
+ }
89
+ return {
90
+ version: 1,
91
+ peerUrl: obj.peerUrl.trim(),
92
+ namespace: obj.namespace.trim(),
93
+ lastConvergedAt: typeof obj.lastConvergedAt === "string" ? obj.lastConvergedAt : undefined,
94
+ baseFiles,
95
+ completedPaths,
96
+ };
97
+ }
98
+
99
+ export async function readConvergeCursor(
100
+ cursorPath: string,
101
+ ): Promise<ConvergeCursorState | null> {
102
+ try {
103
+ const raw = await fs.readFile(path.resolve(cursorPath), "utf-8");
104
+ const parsed = JSON.parse(raw);
105
+ return normalizeConvergeCursor(parsed);
106
+ } catch (error) {
107
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
108
+ return null;
109
+ }
110
+ }
111
+
112
+ export async function writeConvergeCursor(
113
+ cursorPath: string,
114
+ cursor: ConvergeCursorState,
115
+ ): Promise<void> {
116
+ const normalized = normalizeConvergeCursor(cursor);
117
+ const target = path.resolve(cursorPath);
118
+ await fs.mkdir(path.dirname(target), { recursive: true });
119
+ const tmp = path.join(
120
+ path.dirname(target),
121
+ `.converge-cursor.${process.pid}.${randomUUID()}.tmp`,
122
+ );
123
+ await fs.writeFile(tmp, JSON.stringify(normalized, null, 2) + "\n", "utf-8");
124
+ try {
125
+ await fs.rename(tmp, target);
126
+ } catch (error) {
127
+ await fs.unlink(tmp).catch(() => {});
128
+ throw error;
129
+ }
130
+ }