@remnic/core 9.3.757 → 9.3.759

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 (174) hide show
  1. package/dist/access-admin-ops-surface.d.ts +2 -2
  2. package/dist/access-admin-ops-surface.js +20 -20
  3. package/dist/access-audit.js +2 -2
  4. package/dist/access-boundary.d.ts +2 -2
  5. package/dist/access-boundary.js +21 -21
  6. package/dist/access-cli.js +68 -68
  7. package/dist/access-http.d.ts +2 -2
  8. package/dist/access-http.js +28 -28
  9. package/dist/access-identity-continuity-surface.d.ts +166 -0
  10. package/dist/access-identity-continuity-surface.js +128 -0
  11. package/dist/access-lcm-surface.d.ts +2 -2
  12. package/dist/access-lcm-surface.js +20 -20
  13. package/dist/access-mcp.d.ts +2 -2
  14. package/dist/access-mcp.js +27 -27
  15. package/dist/access-observe-write-surface.d.ts +2 -2
  16. package/dist/access-observe-write-surface.js +20 -20
  17. package/dist/access-operations-batch.js +24 -24
  18. package/dist/access-operations.d.ts +2 -2
  19. package/dist/access-operations.js +26 -26
  20. package/dist/access-recall-surface.d.ts +2 -2
  21. package/dist/access-recall-surface.js +20 -20
  22. package/dist/access-schema.js +5 -5
  23. package/dist/{access-service-CJwcDoQp.d.ts → access-service-BW-gb3DL.d.ts} +4 -3
  24. package/dist/access-service.d.ts +2 -2
  25. package/dist/access-service.js +20 -20
  26. package/dist/access-surface-catalog.d.ts +2 -2
  27. package/dist/bootstrap.d.ts +1 -1
  28. package/dist/briefing.js +3 -3
  29. package/dist/{capsule-crypto-RAARDY3M.js → capsule-crypto-CZJSLEFG.js} +2 -2
  30. package/dist/causal-behavior.js +2 -2
  31. package/dist/causal-chain.js +2 -2
  32. package/dist/causal-consolidation.js +8 -8
  33. package/dist/causal-retrieval.js +2 -2
  34. package/dist/causal-trajectory-graph.js +2 -2
  35. package/dist/causal-trajectory.js +1 -1
  36. package/dist/{chunk-EEIROWEG.js → chunk-2BFLDH5F.js} +2 -2
  37. package/dist/{chunk-UPSAT4PY.js → chunk-2MR3MFQB.js} +2 -2
  38. package/dist/{chunk-MQQO56LN.js → chunk-2XE4F3VZ.js} +30 -30
  39. package/dist/{chunk-6A76LSX6.js → chunk-3JBW3GHN.js} +2 -2
  40. package/dist/{chunk-IZP62CEF.js → chunk-3QLPBCFG.js} +4 -4
  41. package/dist/{chunk-LNQUAJNC.js → chunk-437S3G37.js} +1 -1
  42. package/dist/{chunk-GI7PRVMV.js → chunk-6CUUFFU7.js} +2 -2
  43. package/dist/{chunk-6FYRCQ7K.js → chunk-7HWOEYHW.js} +2 -2
  44. package/dist/{chunk-AGNBY3VG.js → chunk-APJQ6UEA.js} +4 -4
  45. package/dist/{chunk-PXHY2E3S.js → chunk-AZSSKOG6.js} +4 -4
  46. package/dist/{chunk-XKUYMM34.js → chunk-DTSOB2YF.js} +2 -2
  47. package/dist/{chunk-6DMTYBNY.js → chunk-EGQ3AVZV.js} +2 -2
  48. package/dist/{chunk-XROP4ZHB.js → chunk-EHQ5HO5T.js} +4 -4
  49. package/dist/{chunk-KTULX4YO.js → chunk-FLVL4L3L.js} +4 -4
  50. package/dist/{chunk-2NTWULEQ.js → chunk-FYJWEGYP.js} +2 -2
  51. package/dist/{chunk-7HJ3XBMD.js → chunk-ILVF6TIY.js} +2 -2
  52. package/dist/{chunk-L5DU7THE.js → chunk-IOZZG75J.js} +2 -2
  53. package/dist/{chunk-4L2KHKLY.js → chunk-IZLGHKWA.js} +271 -197
  54. package/dist/chunk-IZLGHKWA.js.map +1 -0
  55. package/dist/{chunk-AINK3KDE.js → chunk-LLVKDFNU.js} +2 -2
  56. package/dist/{chunk-GIJDW2YO.js → chunk-M2AAYJVI.js} +3 -3
  57. package/dist/{chunk-CYWCKFCL.js → chunk-M5QKGHCR.js} +5 -5
  58. package/dist/{chunk-ZL3Y3ZTQ.js → chunk-OLEBPUZ7.js} +5 -5
  59. package/dist/{chunk-ZG2WP425.js → chunk-ORAK3LNZ.js} +2 -2
  60. package/dist/{chunk-SUN6XVNI.js → chunk-P26O6R3Q.js} +3 -3
  61. package/dist/{chunk-FFPH47IX.js → chunk-PKRRBRY2.js} +2 -2
  62. package/dist/{chunk-PHWBKQHK.js → chunk-QOM3F6YW.js} +8 -8
  63. package/dist/{chunk-FBS6V6LJ.js → chunk-QQJMVF6P.js} +4 -4
  64. package/dist/{chunk-T3UW6NRQ.js → chunk-QUGPXPFJ.js} +7 -7
  65. package/dist/{chunk-LVMVYLNB.js → chunk-SLAZZXBU.js} +2 -2
  66. package/dist/{chunk-RQXVVENY.js → chunk-SMLMH7S5.js} +174 -135
  67. package/dist/chunk-SMLMH7S5.js.map +1 -0
  68. package/dist/{chunk-IR6ATBUI.js → chunk-SN2EWIAI.js} +2 -2
  69. package/dist/{chunk-ZE7GJ76H.js → chunk-SQNED75R.js} +1 -1
  70. package/dist/{chunk-PMBWVQ7D.js → chunk-TQT3USCG.js} +2 -2
  71. package/dist/{chunk-7K5Q6COX.js → chunk-TVVEYCNW.js} +4 -4
  72. package/dist/{chunk-I65IQD7Y.js → chunk-WIXQ3F5P.js} +2 -2
  73. package/dist/{chunk-IF6BTZCB.js → chunk-XQT5GSWU.js} +2 -2
  74. package/dist/{chunk-5PHPNUZN.js → chunk-YP23FQPT.js} +288 -233
  75. package/dist/chunk-YP23FQPT.js.map +1 -0
  76. package/dist/{chunk-QVQG7W2H.js → chunk-ZEQVS6AZ.js} +2 -2
  77. package/dist/{chunk-CTGK5VK3.js → chunk-ZQNCHPTZ.js} +2 -2
  78. package/dist/{cli-cZyANcmY.d.ts → cli-D551dnva.d.ts} +2 -2
  79. package/dist/cli.d.ts +3 -3
  80. package/dist/cli.js +46 -46
  81. package/dist/compounding/engine.js +3 -3
  82. package/dist/connectors/codex-materialize-runner.js +3 -3
  83. package/dist/connectors/index.js +3 -3
  84. package/dist/contradiction/index.js +4 -4
  85. package/dist/entity-retrieval.js +3 -3
  86. package/dist/explicit-capture.d.ts +1 -1
  87. package/dist/{first-start-migration-RYY7PPGF.js → first-start-migration-DY6YCRC2.js} +4 -4
  88. package/dist/{graph-edge-decay-U2GKDAVM.js → graph-edge-decay-RXUCJIN4.js} +4 -4
  89. package/dist/graph-edge-decay-RXUCJIN4.js.map +1 -0
  90. package/dist/graph-snapshot.js +3 -3
  91. package/dist/graph.js +2 -2
  92. package/dist/index.d.ts +4 -4
  93. package/dist/index.js +85 -85
  94. package/dist/lcm/index.js +3 -3
  95. package/dist/maintenance/memory-governance.js +3 -3
  96. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  97. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  98. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  99. package/dist/namespaces/migrate.js +7 -7
  100. package/dist/namespaces/search.js +3 -3
  101. package/dist/namespaces/storage.js +3 -3
  102. package/dist/operator-toolkit.js +12 -12
  103. package/dist/orchestration/maintenance.js +5 -5
  104. package/dist/{orchestrator-Xm6sK5jw.d.ts → orchestrator-BnM187cb.d.ts} +2 -1
  105. package/dist/orchestrator.d.ts +1 -1
  106. package/dist/orchestrator.js +70 -70
  107. package/dist/search/factory.js +2 -2
  108. package/dist/search/index.js +4 -4
  109. package/dist/semantic-consolidation.js +4 -4
  110. package/dist/semantic-rule-promotion.js +3 -3
  111. package/dist/semantic-rule-verifier.js +3 -3
  112. package/dist/storage.d.ts +3 -4
  113. package/dist/storage.js +2 -2
  114. package/dist/transfer/backup.js +3 -3
  115. package/dist/transfer/capsule-export.js +4 -4
  116. package/dist/transfer/capsule-import.js +3 -3
  117. package/dist/transfer/import-sqlite.js +2 -2
  118. package/dist/verified-recall.js +3 -3
  119. package/package.json +2 -2
  120. package/src/access-identity-continuity-surface.test.ts +374 -0
  121. package/src/access-identity-continuity-surface.ts +322 -0
  122. package/src/access-service.ts +23 -171
  123. package/src/orchestration/recall-background-writes.ts +24 -0
  124. package/src/orchestration/recall-entry.ts +5 -0
  125. package/src/orchestration/recall-internal.ts +15 -11
  126. package/src/orchestration/recall-introspection.ts +5 -0
  127. package/src/orchestrator-background-writes.test.ts +101 -0
  128. package/src/orchestrator.ts +13 -13
  129. package/src/storage/identity-continuity-store.ts +223 -0
  130. package/src/storage-contract/round-trip.test.ts +5 -0
  131. package/src/storage.ts +27 -145
  132. package/src/trust-score-recall-paths.test.ts +2 -0
  133. package/dist/chunk-4L2KHKLY.js.map +0 -1
  134. package/dist/chunk-5PHPNUZN.js.map +0 -1
  135. package/dist/chunk-RQXVVENY.js.map +0 -1
  136. /package/dist/{capsule-crypto-RAARDY3M.js.map → access-identity-continuity-surface.js.map} +0 -0
  137. /package/dist/{graph-edge-decay-U2GKDAVM.js.map → capsule-crypto-CZJSLEFG.js.map} +0 -0
  138. /package/dist/{chunk-EEIROWEG.js.map → chunk-2BFLDH5F.js.map} +0 -0
  139. /package/dist/{chunk-UPSAT4PY.js.map → chunk-2MR3MFQB.js.map} +0 -0
  140. /package/dist/{chunk-MQQO56LN.js.map → chunk-2XE4F3VZ.js.map} +0 -0
  141. /package/dist/{chunk-6A76LSX6.js.map → chunk-3JBW3GHN.js.map} +0 -0
  142. /package/dist/{chunk-IZP62CEF.js.map → chunk-3QLPBCFG.js.map} +0 -0
  143. /package/dist/{chunk-LNQUAJNC.js.map → chunk-437S3G37.js.map} +0 -0
  144. /package/dist/{chunk-GI7PRVMV.js.map → chunk-6CUUFFU7.js.map} +0 -0
  145. /package/dist/{chunk-6FYRCQ7K.js.map → chunk-7HWOEYHW.js.map} +0 -0
  146. /package/dist/{chunk-AGNBY3VG.js.map → chunk-APJQ6UEA.js.map} +0 -0
  147. /package/dist/{chunk-PXHY2E3S.js.map → chunk-AZSSKOG6.js.map} +0 -0
  148. /package/dist/{chunk-XKUYMM34.js.map → chunk-DTSOB2YF.js.map} +0 -0
  149. /package/dist/{chunk-6DMTYBNY.js.map → chunk-EGQ3AVZV.js.map} +0 -0
  150. /package/dist/{chunk-XROP4ZHB.js.map → chunk-EHQ5HO5T.js.map} +0 -0
  151. /package/dist/{chunk-KTULX4YO.js.map → chunk-FLVL4L3L.js.map} +0 -0
  152. /package/dist/{chunk-2NTWULEQ.js.map → chunk-FYJWEGYP.js.map} +0 -0
  153. /package/dist/{chunk-7HJ3XBMD.js.map → chunk-ILVF6TIY.js.map} +0 -0
  154. /package/dist/{chunk-L5DU7THE.js.map → chunk-IOZZG75J.js.map} +0 -0
  155. /package/dist/{chunk-AINK3KDE.js.map → chunk-LLVKDFNU.js.map} +0 -0
  156. /package/dist/{chunk-GIJDW2YO.js.map → chunk-M2AAYJVI.js.map} +0 -0
  157. /package/dist/{chunk-CYWCKFCL.js.map → chunk-M5QKGHCR.js.map} +0 -0
  158. /package/dist/{chunk-ZL3Y3ZTQ.js.map → chunk-OLEBPUZ7.js.map} +0 -0
  159. /package/dist/{chunk-ZG2WP425.js.map → chunk-ORAK3LNZ.js.map} +0 -0
  160. /package/dist/{chunk-SUN6XVNI.js.map → chunk-P26O6R3Q.js.map} +0 -0
  161. /package/dist/{chunk-FFPH47IX.js.map → chunk-PKRRBRY2.js.map} +0 -0
  162. /package/dist/{chunk-PHWBKQHK.js.map → chunk-QOM3F6YW.js.map} +0 -0
  163. /package/dist/{chunk-FBS6V6LJ.js.map → chunk-QQJMVF6P.js.map} +0 -0
  164. /package/dist/{chunk-T3UW6NRQ.js.map → chunk-QUGPXPFJ.js.map} +0 -0
  165. /package/dist/{chunk-LVMVYLNB.js.map → chunk-SLAZZXBU.js.map} +0 -0
  166. /package/dist/{chunk-IR6ATBUI.js.map → chunk-SN2EWIAI.js.map} +0 -0
  167. /package/dist/{chunk-ZE7GJ76H.js.map → chunk-SQNED75R.js.map} +0 -0
  168. /package/dist/{chunk-PMBWVQ7D.js.map → chunk-TQT3USCG.js.map} +0 -0
  169. /package/dist/{chunk-7K5Q6COX.js.map → chunk-TVVEYCNW.js.map} +0 -0
  170. /package/dist/{chunk-I65IQD7Y.js.map → chunk-WIXQ3F5P.js.map} +0 -0
  171. /package/dist/{chunk-IF6BTZCB.js.map → chunk-XQT5GSWU.js.map} +0 -0
  172. /package/dist/{chunk-QVQG7W2H.js.map → chunk-ZEQVS6AZ.js.map} +0 -0
  173. /package/dist/{chunk-CTGK5VK3.js.map → chunk-ZQNCHPTZ.js.map} +0 -0
  174. /package/dist/{first-start-migration-RYY7PPGF.js.map → first-start-migration-DY6YCRC2.js.map} +0 -0
@@ -326,6 +326,7 @@ export interface RecallInternalDeps {
326
326
  ): number | undefined;
327
327
  getStorage(namespace?: string): Promise<StorageManager>;
328
328
  readonly handleHistory: RecallHandleHistoryStore;
329
+ trackRecallBackgroundWrite(promise: Promise<void>, label: string): void;
329
330
  isRecallSectionEnabled(
330
331
  sectionId: string,
331
332
  defaultEnabled?: boolean,
@@ -908,8 +909,8 @@ export class RecallInternalCoordinator {
908
909
  }
909
910
  }
910
911
  if (sessionKey) {
911
- this.deps.lastRecall
912
- .record({
912
+ this.deps.trackRecallBackgroundWrite(
913
+ this.deps.lastRecall.record({
913
914
  sessionKey,
914
915
  query: retrievalQuery,
915
916
  memoryIds: [],
@@ -935,8 +936,9 @@ export class RecallInternalCoordinator {
935
936
  injectedChars: identityInjectedChars,
936
937
  truncated: identityInjectionTruncated,
937
938
  },
938
- })
939
- .catch((err) => log.debug(`last recall record failed: ${err}`));
939
+ }),
940
+ "last recall record",
941
+ );
940
942
  }
941
943
  if (sessionKey) {
942
944
  this.deps.queueEvalShadowRecall({
@@ -5364,8 +5366,8 @@ export class RecallInternalCoordinator {
5364
5366
 
5365
5367
  if (sessionKey) {
5366
5368
  throwIfRecallAborted(options.abortSignal);
5367
- this.deps.lastRecall
5368
- .record({
5369
+ this.deps.trackRecallBackgroundWrite(
5370
+ this.deps.lastRecall.record({
5369
5371
  sessionKey,
5370
5372
  query: retrievalQuery,
5371
5373
  memoryIds: recalledMemoryIds,
@@ -5397,8 +5399,9 @@ export class RecallInternalCoordinator {
5397
5399
  // reviews on #1544).
5398
5400
  backendDegradations:
5399
5401
  backendDegradations.length > 0 ? backendDegradations : undefined,
5400
- })
5401
- .catch((err) => log.debug(`last recall record failed: ${err}`));
5402
+ }),
5403
+ "last recall record",
5404
+ );
5402
5405
  // Issue #1582 — record the admitted memory-id set for handle resolution.
5403
5406
  // Only when handles are enabled: if injection is off, no handle is ever
5404
5407
  // rendered, so there is nothing to resolve and we skip the write.
@@ -5410,9 +5413,10 @@ export class RecallInternalCoordinator {
5410
5413
  MEMORY_ID_PATTERN.test(id),
5411
5414
  );
5412
5415
  if (handleEligibleIds.length > 0) {
5413
- this.deps.handleHistory
5414
- .record(sessionKey, handleEligibleIds)
5415
- .catch((err) => log.debug(`handle history record failed: ${err}`));
5416
+ this.deps.trackRecallBackgroundWrite(
5417
+ this.deps.handleHistory.record(sessionKey, handleEligibleIds),
5418
+ "handle history record",
5419
+ );
5416
5420
  }
5417
5421
  }
5418
5422
  }
@@ -66,6 +66,7 @@ export interface RecallIntrospectionDeps {
66
66
  getStorage(namespace?: string): Promise<StorageManager>;
67
67
  readonly graphRecallCoordinator: GraphRecallCoordinator;
68
68
  readonly lastRecall: LastRecallStore;
69
+ trackRecallBackgroundWrite(promise: Promise<void>, label: string): void;
69
70
  resolveStateDirForNamespace(
70
71
  namespace: string,
71
72
  ): Promise<string>;
@@ -500,6 +501,10 @@ export class RecallIntrospectionCoordinator {
500
501
  log.debug(`direct-answer observation chain error: ${err}`);
501
502
  }
502
503
  });
504
+ this.deps.trackRecallBackgroundWrite(
505
+ this.deps.directAnswerObservationChain,
506
+ "direct-answer observation",
507
+ );
503
508
  }
504
509
 
505
510
  async annotateDirectAnswerTier(
@@ -0,0 +1,101 @@
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
+ import { parseConfig } from "./config.js";
7
+ import { Orchestrator } from "./orchestrator.js";
8
+
9
+ test("destroy cancels maintenance and waits for tracked writes before disposing search backends", async () => {
10
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-background-writes-"));
11
+ const orchestrator = new Orchestrator(
12
+ parseConfig({
13
+ openaiApiKey: "sk-test",
14
+ memoryDir,
15
+ workspaceDir: path.join(memoryDir, "workspace"),
16
+ qmdEnabled: false,
17
+ knowledgeIndexEnabled: false,
18
+ identityContinuityEnabled: false,
19
+ transcriptEnabled: false,
20
+ hourlySummariesEnabled: false,
21
+ compoundingEnabled: false,
22
+ })
23
+ );
24
+ const maintenanceScheduler = (orchestrator as unknown as { maintenanceScheduler: { dispose(): void } })
25
+ .maintenanceScheduler;
26
+ const originalMaintenanceDispose = maintenanceScheduler.dispose.bind(maintenanceScheduler);
27
+ let maintenanceDisposed = false;
28
+ maintenanceScheduler.dispose = () => {
29
+ maintenanceDisposed = true;
30
+ originalMaintenanceDispose();
31
+ };
32
+ const writeGate = Promise.withResolvers<void>();
33
+ let destroySettled = false;
34
+ let destroyPromise: Promise<void> | undefined;
35
+
36
+ try {
37
+ orchestrator.trackRecallBackgroundWrite(writeGate.promise, "test background write");
38
+ destroyPromise = orchestrator.destroy().then(() => {
39
+ destroySettled = true;
40
+ });
41
+
42
+ await new Promise<void>((resolve) => setImmediate(resolve));
43
+ assert.equal(destroySettled, false, "destroy must remain pending while a tracked write is pending");
44
+ assert.equal(maintenanceDisposed, true, "destroy must cancel maintenance before waiting for writes");
45
+
46
+ writeGate.resolve();
47
+ await destroyPromise;
48
+ assert.equal(destroySettled, true);
49
+ } finally {
50
+ writeGate.resolve();
51
+ await (destroyPromise ?? orchestrator.destroy());
52
+ await rm(memoryDir, { recursive: true, force: true });
53
+ }
54
+ });
55
+
56
+ test("destroy waits for a recall-initiated last-recall write", async () => {
57
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-recall-background-write-"));
58
+ const orchestrator = new Orchestrator(
59
+ parseConfig({
60
+ openaiApiKey: "sk-test",
61
+ memoryDir,
62
+ workspaceDir: path.join(memoryDir, "workspace"),
63
+ qmdEnabled: false,
64
+ knowledgeIndexEnabled: false,
65
+ identityContinuityEnabled: false,
66
+ transcriptEnabled: false,
67
+ hourlySummariesEnabled: false,
68
+ compoundingEnabled: false,
69
+ })
70
+ );
71
+ const writeStarted = Promise.withResolvers<void>();
72
+ const writeGate = Promise.withResolvers<void>();
73
+ const originalRecord = orchestrator.lastRecall.record.bind(orchestrator.lastRecall);
74
+ orchestrator.lastRecall.record = async (...args: Parameters<typeof originalRecord>) => {
75
+ writeStarted.resolve();
76
+ await writeGate.promise;
77
+ await originalRecord(...args);
78
+ };
79
+ let destroySettled = false;
80
+ let destroyPromise: Promise<void> | undefined;
81
+
82
+ try {
83
+ await orchestrator.initialize();
84
+ await orchestrator.recall("recall state drain contract", "session-background-write");
85
+ await writeStarted.promise;
86
+ destroyPromise = orchestrator.destroy().then(() => {
87
+ destroySettled = true;
88
+ });
89
+
90
+ await new Promise<void>((resolve) => setImmediate(resolve));
91
+ assert.equal(destroySettled, false, "destroy must wait for the recall-initiated state write");
92
+
93
+ writeGate.resolve();
94
+ await destroyPromise;
95
+ assert.equal(destroySettled, true);
96
+ } finally {
97
+ writeGate.resolve();
98
+ await (destroyPromise ?? orchestrator.destroy());
99
+ await rm(memoryDir, { recursive: true, force: true });
100
+ }
101
+ });
@@ -120,6 +120,7 @@ import { NamespaceReadFanoutCoordinator } from "./orchestration/namespace-read-f
120
120
  import { selfDeps } from "./orchestration/self-deps.js";
121
121
  import { RecallEntryCoordinator } from "./orchestration/recall-entry.js";
122
122
  import { SessionContextCoordinator } from "./orchestration/session-context.js";
123
+ import { drainRecallWrites, trackRecallWrite } from "./orchestration/recall-background-writes.js";
123
124
  import {
124
125
  abortRecallError,
125
126
  buildCompressionGuidelinesMarkdown,
@@ -135,9 +136,8 @@ import {
135
136
  type QueryAwarePrefilter,
136
137
  type RecallInvocationOptions,
137
138
  } from "./orchestration/orchestrator-helpers.js";
138
- // Issue #1526 seam 25: module-level helpers moved to
139
- // orchestration/orchestrator-helpers.ts; re-exported here so existing
140
- // importers (coordinators, tests, root shims) keep working.
139
+ // Issue #1526 seam 25: helpers moved to orchestration/orchestrator-helpers.ts and are
140
+ // re-exported here so existing importers (coordinators, tests, root shims) keep working.
141
141
  export {
142
142
  BulkImportBatchPartialFailureError,
143
143
  COMPACTION_SIGNAL_MAX_AGE_MS,
@@ -701,8 +701,7 @@ export class Orchestrator {
701
701
  /** Read by NamespaceReadFanoutCoordinator (seam 26), hence not `private`. */
702
702
  static readonly ARTIFACT_STATUS_CACHE_TTL_MS = 60_000;
703
703
 
704
- // Access tracking buffer (Phase 1A)
705
- // Maps memoryId -> {count, lastAccessed} for batched updates
704
+ // Batched access-tracking counts and timestamps (Phase 1A).
706
705
  private accessTrackingBuffer: Map<
707
706
  string,
708
707
  { count: number; lastAccessed: string }
@@ -887,9 +886,7 @@ export class Orchestrator {
887
886
  private utilityRuntimeValues: UtilityRuntimeValues | null = null;
888
887
  private evalShadowWriteChain: Promise<void> = Promise.resolve();
889
888
 
890
- // Pending background observation-mode direct-answer annotations (#518).
891
- // Tracks fire-and-forget `annotateDirectAnswerTier` calls so callers (tests,
892
- // waitForDirectAnswerObservationIdle) can await settlement.
889
+ // Pending fire-and-forget direct-answer tier annotations (#518).
893
890
  private directAnswerObservationChain: Promise<void> = Promise.resolve();
894
891
 
895
892
  // Initialization gate: recall() awaits this before proceeding
@@ -934,8 +931,9 @@ export class Orchestrator {
934
931
  }
935
932
  }
936
933
 
937
- private async disposeSearchBackendIfNeeded(): Promise<void> {
938
- await (this.qmd as { dispose?: () => void | Promise<void> }).dispose?.();
934
+ /** Track a recall-side write so destroy() can wait for it before teardown. @internal */
935
+ trackRecallBackgroundWrite(promise: Promise<void>, label: string): void {
936
+ trackRecallWrite(this, promise, label);
939
937
  }
940
938
 
941
939
  /**
@@ -952,8 +950,9 @@ export class Orchestrator {
952
950
  this.wearablesAutoSyncHandle = null;
953
951
  }
954
952
  this.maintenanceScheduler.dispose();
953
+ await drainRecallWrites(this);
955
954
  await this.namespaceSearchRouter.dispose();
956
- await this.disposeSearchBackendIfNeeded();
955
+ await (this.qmd as { dispose?: () => void | Promise<void> }).dispose?.();
957
956
  if (this.conversationQmd && this.conversationQmd !== this.qmd) {
958
957
  await (this.conversationQmd as { dispose?: () => void | Promise<void> }).dispose?.();
959
958
  }
@@ -3713,8 +3712,9 @@ export class Orchestrator {
3713
3712
  if (
3714
3713
  this.accessTrackingBuffer.size >= this.config.accessTrackingBufferMaxSize
3715
3714
  ) {
3716
- this.flushAccessTracking().catch((err) =>
3717
- log.debug(`background access tracking flush failed: ${err}`),
3715
+ this.trackRecallBackgroundWrite(
3716
+ this.flushAccessTracking(),
3717
+ "background access tracking flush",
3718
3718
  );
3719
3719
  }
3720
3720
  }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Identity-continuity persistence extracted from StorageManager.
3
+ *
4
+ * The host keeps the public API while this store owns anchors, incidents,
5
+ * audits, and improvement-loop files. Dependencies stay live through
6
+ * `selfDeps`, preserving secure-store and catalog-write behavior.
7
+ */
8
+
9
+ import { readdir } from "node:fs/promises";
10
+ import path from "node:path";
11
+ import {
12
+ closeContinuityIncidentRecord,
13
+ createContinuityIncidentRecord,
14
+ parseContinuityImprovementLoops,
15
+ parseContinuityIncident,
16
+ reviewContinuityLoopInMarkdown,
17
+ serializeContinuityIncident,
18
+ upsertContinuityLoopInMarkdown,
19
+ } from "../identity-continuity.js";
20
+ import { SecureStoreLockedError } from "../secure-store/secure-fs.js";
21
+ import type {
22
+ ContinuityImprovementLoop,
23
+ ContinuityIncidentCloseInput,
24
+ ContinuityIncidentOpenInput,
25
+ ContinuityIncidentRecord,
26
+ ContinuityLoopReviewInput,
27
+ ContinuityLoopUpsertInput,
28
+ } from "../types.js";
29
+ import { isErrnoCode } from "../utils/errno.js";
30
+
31
+ export interface IdentityContinuityStoreDeps {
32
+ ensureDirectories(): Promise<void>;
33
+ generateId(prefix?: string): string;
34
+ readonly identityAnchorPath: string;
35
+ readonly identityIncidentsDir: string;
36
+ readonly identityAuditsWeeklyDir: string;
37
+ readonly identityAuditsMonthlyDir: string;
38
+ readonly identityImprovementLoopsPath: string;
39
+ readStorageSecureFile(filePath: string): Promise<string>;
40
+ writeStorageSecureFile(filePath: string, content: string | Buffer): Promise<void>;
41
+ }
42
+
43
+ export class IdentityContinuityStore {
44
+ constructor(private readonly deps: IdentityContinuityStoreDeps) {}
45
+
46
+ async writeIdentityAnchor(content: string): Promise<void> {
47
+ await this.deps.ensureDirectories();
48
+ await this.deps.writeStorageSecureFile(this.deps.identityAnchorPath, content);
49
+ }
50
+
51
+ async readIdentityAnchor(): Promise<string | null> {
52
+ try {
53
+ return await this.deps.readStorageSecureFile(this.deps.identityAnchorPath);
54
+ } catch (err) {
55
+ if (!isErrnoCode(err, "ENOENT")) throw err;
56
+ return null;
57
+ }
58
+ }
59
+
60
+ async appendContinuityIncident(input: ContinuityIncidentOpenInput): Promise<ContinuityIncidentRecord> {
61
+ await this.deps.ensureDirectories();
62
+ const now = new Date();
63
+ const nowIso = now.toISOString();
64
+ const date = nowIso.slice(0, 10);
65
+ const id = this.deps.generateId("incident");
66
+ const incident = createContinuityIncidentRecord(id, input, nowIso);
67
+ const filePath = path.join(this.deps.identityIncidentsDir, `${date}-${id}.md`);
68
+ await this.deps.writeStorageSecureFile(filePath, serializeContinuityIncident(incident));
69
+ return { ...incident, filePath };
70
+ }
71
+
72
+ async readContinuityIncidents(
73
+ limit = 200,
74
+ state: "open" | "closed" | "all" = "all"
75
+ ): Promise<ContinuityIncidentRecord[]> {
76
+ const normalizedLimit = Number.isFinite(limit) ? Math.floor(limit) : 0;
77
+ const cappedLimit = Math.max(0, normalizedLimit);
78
+ if (cappedLimit === 0) return [];
79
+
80
+ try {
81
+ const candidates = await this.readContinuityIncidentFileNames();
82
+ const incidents: ContinuityIncidentRecord[] = [];
83
+
84
+ for (const file of candidates) {
85
+ if (incidents.length >= cappedLimit) break;
86
+ const filePath = path.join(this.deps.identityIncidentsDir, file);
87
+ try {
88
+ const raw = await this.deps.readStorageSecureFile(filePath);
89
+ const parsed = parseContinuityIncident(raw);
90
+ if (!parsed) continue;
91
+ if (state !== "all" && parsed.state !== state) continue;
92
+ incidents.push({ ...parsed, filePath });
93
+ } catch (err) {
94
+ if (err instanceof SecureStoreLockedError) throw err;
95
+ // Fail-open on malformed/missing files.
96
+ }
97
+ }
98
+ return incidents;
99
+ } catch (err) {
100
+ if (err instanceof SecureStoreLockedError) throw err;
101
+ if (!isErrnoCode(err, "ENOENT")) throw err;
102
+ return [];
103
+ }
104
+ }
105
+
106
+ async closeContinuityIncident(
107
+ id: string,
108
+ closure: ContinuityIncidentCloseInput
109
+ ): Promise<ContinuityIncidentRecord | null> {
110
+ const directFilePath = await this.findContinuityIncidentFilePathById(id);
111
+ const target = directFilePath ? await this.readContinuityIncidentFile(directFilePath) : null;
112
+ if (!target || !directFilePath) return null;
113
+ if (target.state === "closed") return target;
114
+
115
+ const closed = closeContinuityIncidentRecord(target, closure, new Date().toISOString());
116
+ await this.deps.writeStorageSecureFile(directFilePath, serializeContinuityIncident(closed));
117
+ return { ...closed, filePath: directFilePath };
118
+ }
119
+
120
+ async writeIdentityAudit(period: "weekly" | "monthly", key: string, content: string): Promise<string> {
121
+ await this.deps.ensureDirectories();
122
+ const safeKey = this.sanitizeIdentityAuditKey(key);
123
+ const dir = period === "weekly" ? this.deps.identityAuditsWeeklyDir : this.deps.identityAuditsMonthlyDir;
124
+ const filePath = path.join(dir, `${safeKey}.md`);
125
+ await this.deps.writeStorageSecureFile(filePath, content);
126
+ return filePath;
127
+ }
128
+
129
+ async readIdentityAudit(period: "weekly" | "monthly", key: string): Promise<string | null> {
130
+ try {
131
+ const safeKey = this.sanitizeIdentityAuditKey(key);
132
+ const dir = period === "weekly" ? this.deps.identityAuditsWeeklyDir : this.deps.identityAuditsMonthlyDir;
133
+ return await this.deps.readStorageSecureFile(path.join(dir, `${safeKey}.md`));
134
+ } catch (err) {
135
+ if (err instanceof Error && err.message === "Invalid identity audit key") return null;
136
+ if (!isErrnoCode(err, "ENOENT")) throw err;
137
+ return null;
138
+ }
139
+ }
140
+
141
+ async writeIdentityImprovementLoops(content: string): Promise<void> {
142
+ await this.deps.ensureDirectories();
143
+ await this.deps.writeStorageSecureFile(this.deps.identityImprovementLoopsPath, content);
144
+ }
145
+
146
+ async readIdentityImprovementLoops(): Promise<string | null> {
147
+ try {
148
+ return await this.deps.readStorageSecureFile(this.deps.identityImprovementLoopsPath);
149
+ } catch (err) {
150
+ if (!isErrnoCode(err, "ENOENT")) throw err;
151
+ return null;
152
+ }
153
+ }
154
+
155
+ async readIdentityImprovementLoopRegister(): Promise<ContinuityImprovementLoop[]> {
156
+ const raw = await this.readIdentityImprovementLoops();
157
+ if (!raw) return [];
158
+ return parseContinuityImprovementLoops(raw);
159
+ }
160
+
161
+ async upsertIdentityImprovementLoop(input: ContinuityLoopUpsertInput): Promise<ContinuityImprovementLoop> {
162
+ const nowIso = new Date().toISOString();
163
+ const raw = await this.readIdentityImprovementLoops();
164
+ const { markdown, loop } = upsertContinuityLoopInMarkdown(raw, input, nowIso);
165
+ await this.writeIdentityImprovementLoops(markdown);
166
+ return loop;
167
+ }
168
+
169
+ async reviewIdentityImprovementLoop(
170
+ id: string,
171
+ input: ContinuityLoopReviewInput
172
+ ): Promise<ContinuityImprovementLoop | null> {
173
+ const raw = await this.readIdentityImprovementLoops();
174
+ const { markdown, loop } = reviewContinuityLoopInMarkdown(raw, id, input, new Date().toISOString());
175
+ if (!loop) return null;
176
+ await this.writeIdentityImprovementLoops(markdown);
177
+ return loop;
178
+ }
179
+
180
+ private async readContinuityIncidentFileNames(): Promise<string[]> {
181
+ const files = await readdir(this.deps.identityIncidentsDir);
182
+ return files
183
+ .filter((file) => file.endsWith(".md"))
184
+ .sort()
185
+ .reverse();
186
+ }
187
+
188
+ private async readContinuityIncidentFile(filePath: string): Promise<ContinuityIncidentRecord | null> {
189
+ try {
190
+ const raw = await this.deps.readStorageSecureFile(filePath);
191
+ const parsed = parseContinuityIncident(raw);
192
+ return parsed ? { ...parsed, filePath } : null;
193
+ } catch (err) {
194
+ if (err instanceof SecureStoreLockedError) throw err;
195
+ return null;
196
+ }
197
+ }
198
+
199
+ private async findContinuityIncidentFilePathById(id: string): Promise<string | null> {
200
+ const fileNames = await this.readContinuityIncidentFileNames();
201
+ const directMatch = fileNames.find((name) => name.endsWith(`-${id}.md`));
202
+ if (directMatch) {
203
+ const directPath = path.join(this.deps.identityIncidentsDir, directMatch);
204
+ const parsed = await this.readContinuityIncidentFile(directPath);
205
+ if (parsed?.id === id) return directPath;
206
+ }
207
+
208
+ for (const fileName of fileNames) {
209
+ const filePath = path.join(this.deps.identityIncidentsDir, fileName);
210
+ const parsed = await this.readContinuityIncidentFile(filePath);
211
+ if (parsed?.id === id) return filePath;
212
+ }
213
+ return null;
214
+ }
215
+
216
+ private sanitizeIdentityAuditKey(key: string): string {
217
+ const trimmed = key.trim();
218
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(trimmed) || trimmed.includes("..")) {
219
+ throw new Error("Invalid identity audit key");
220
+ }
221
+ return trimmed;
222
+ }
223
+ }
@@ -12,6 +12,8 @@
12
12
  */
13
13
 
14
14
  import assert from "node:assert/strict";
15
+ import { utimes } from "node:fs/promises";
16
+ import path from "node:path";
15
17
  import type { MemoryCategory } from "../types.js";
16
18
  import test from "node:test";
17
19
 
@@ -212,6 +214,9 @@ test("question-queue: writeQuestion → readQuestions → resolveQuestion round-
212
214
  // resolve marks it resolved
213
215
  const resolved = await storage.resolveQuestion(id);
214
216
  assert.equal(resolved, true);
217
+ // Directory mtimes are only a cross-process hint and may be coarser than
218
+ // the cache timestamp. Same-process resolution must invalidate directly.
219
+ await utimes(path.join(storage.dir, "questions"), new Date(0), new Date(0));
215
220
 
216
221
  const afterResolve = await storage.readQuestions({ unresolvedOnly: true });
217
222
  assert.ok(!afterResolve.some((x) => x.id === id), "resolved question must not appear in unresolvedOnly");