@remnic/core 9.3.755 → 9.3.756

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 (181) hide show
  1. package/dist/access-admin-ops-surface.d.ts +1 -1
  2. package/dist/access-admin-ops-surface.js +34 -34
  3. package/dist/access-boundary.d.ts +1 -1
  4. package/dist/access-boundary.js +35 -35
  5. package/dist/access-cli.js +83 -83
  6. package/dist/access-http.d.ts +1 -1
  7. package/dist/access-http.js +44 -44
  8. package/dist/access-lcm-surface.d.ts +1 -1
  9. package/dist/access-lcm-surface.js +34 -34
  10. package/dist/access-mcp.d.ts +1 -1
  11. package/dist/access-mcp.js +41 -41
  12. package/dist/access-observe-write-surface.d.ts +225 -0
  13. package/dist/access-observe-write-surface.js +128 -0
  14. package/dist/access-operations-batch.js +38 -38
  15. package/dist/access-operations.d.ts +1 -1
  16. package/dist/access-operations.js +40 -40
  17. package/dist/access-recall-surface.d.ts +1 -1
  18. package/dist/access-recall-surface.js +34 -34
  19. package/dist/access-schema.d.ts +2 -2
  20. package/dist/access-schema.js +3 -3
  21. package/dist/{access-service-C3jkLi2U.d.ts → access-service-CJwcDoQp.d.ts} +4 -5
  22. package/dist/access-service.d.ts +1 -1
  23. package/dist/access-service.js +34 -34
  24. package/dist/access-surface-catalog.d.ts +1 -1
  25. package/dist/active-recall.js +1 -1
  26. package/dist/adapters/index.js +4 -4
  27. package/dist/adapters/registry.js +2 -2
  28. package/dist/{auto-sync-G4YKBPOU.js → auto-sync-RZO5ZPHR.js} +3 -3
  29. package/dist/briefing.js +7 -7
  30. package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-FFWJ4X4A.js} +2 -2
  31. package/dist/causal-behavior.js +2 -2
  32. package/dist/causal-chain.js +2 -2
  33. package/dist/causal-consolidation.js +15 -15
  34. package/dist/causal-retrieval.js +2 -2
  35. package/dist/causal-trajectory.js +1 -1
  36. package/dist/{chunk-M5QKGHCR.js → chunk-3I2IASNK.js} +2 -2
  37. package/dist/{chunk-OCFITZRV.js → chunk-4AC57RCM.js} +4 -4
  38. package/dist/{chunk-3DOD6ICJ.js → chunk-4I45UZ4V.js} +5 -5
  39. package/dist/{chunk-TAPSNIMA.js → chunk-5CEJH5ZN.js} +4 -4
  40. package/dist/{chunk-I6IAJCJS.js → chunk-5VWRYFQE.js} +7 -7
  41. package/dist/{chunk-4LQUYBZM.js → chunk-5ZKT7WTR.js} +7 -7
  42. package/dist/{chunk-VL5QHTJI.js → chunk-6DA5BBLS.js} +117 -117
  43. package/dist/{chunk-LFKEFIJA.js → chunk-6DXXIXED.js} +4 -4
  44. package/dist/{chunk-XBM5DG4O.js → chunk-7APVFQ34.js} +680 -595
  45. package/dist/chunk-7APVFQ34.js.map +1 -0
  46. package/dist/{chunk-NCNOYM6F.js → chunk-7CFGXWYX.js} +2 -2
  47. package/dist/{chunk-TJATYB3F.js → chunk-A6XCHI53.js} +5 -5
  48. package/dist/{chunk-UQJ6YTKR.js → chunk-BTH6MQJT.js} +7 -7
  49. package/dist/{chunk-G7LCG56O.js → chunk-CXW6Y7SI.js} +4 -4
  50. package/dist/{chunk-GCEV6SXP.js → chunk-DWDDQLSZ.js} +2298 -2218
  51. package/dist/chunk-DWDDQLSZ.js.map +1 -0
  52. package/dist/{chunk-DS4QM6I7.js → chunk-EEKDRAWX.js} +4 -4
  53. package/dist/{chunk-I3UWEZTK.js → chunk-EF3VFQC4.js} +2 -2
  54. package/dist/{chunk-437S3G37.js → chunk-EJJQDP3H.js} +1 -1
  55. package/dist/{chunk-FQLM6ZUP.js → chunk-EVVLGYHR.js} +3 -3
  56. package/dist/{chunk-GGYUR4M2.js → chunk-GBXP62CL.js} +10 -10
  57. package/dist/{chunk-N4AGSRVA.js → chunk-GG6G2RYR.js} +9 -9
  58. package/dist/{chunk-JITAVROY.js → chunk-H2T4OJNI.js} +4 -4
  59. package/dist/{chunk-2MR3MFQB.js → chunk-JTYM4HPA.js} +2 -2
  60. package/dist/{chunk-LD53WPMU.js → chunk-LQ6JI4VH.js} +4 -4
  61. package/dist/{chunk-GPFXPC2V.js → chunk-NG2NHB4C.js} +7 -7
  62. package/dist/{chunk-CIDGUFAL.js → chunk-NLXKSM46.js} +2 -2
  63. package/dist/{chunk-T6WHI3JW.js → chunk-OLAVQS6S.js} +6 -6
  64. package/dist/{chunk-ER3CCWK2.js → chunk-ORAK3LNZ.js} +2 -2
  65. package/dist/{chunk-PJ7DIEFC.js → chunk-PBZQVVLG.js} +5 -5
  66. package/dist/{chunk-PJQMISPD.js → chunk-PPAEJXVF.js} +5 -5
  67. package/dist/{chunk-GMWKWA6N.js → chunk-PRX77NPC.js} +5 -5
  68. package/dist/{chunk-24463PIV.js → chunk-PS572XYO.js} +2 -2
  69. package/dist/{chunk-KKOCFUI5.js → chunk-REZ4TUD2.js} +10 -10
  70. package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
  71. package/dist/{chunk-V5UV7BOA.js → chunk-SQNED75R.js} +1 -1
  72. package/dist/{chunk-FLVL4L3L.js → chunk-TCKP4EHY.js} +2 -2
  73. package/dist/{chunk-SP42M3XP.js → chunk-TXV3I2O6.js} +7 -7
  74. package/dist/{chunk-S6UCPBPL.js → chunk-UNV6ZGTS.js} +2 -2
  75. package/dist/{chunk-2BFLDH5F.js → chunk-WEPWY4HB.js} +2 -2
  76. package/dist/{chunk-YQGHW5ML.js → chunk-WRFKZEO6.js} +4 -4
  77. package/dist/{chunk-XCXN3D2U.js → chunk-Y4TAFMUE.js} +6 -6
  78. package/dist/{chunk-H6S3DJVE.js → chunk-YMGQXBPS.js} +2 -2
  79. package/dist/{chunk-T422SYM6.js → chunk-ZUDM75KG.js} +5 -5
  80. package/dist/{chunk-XD6JVK5P.js → chunk-ZUHD6XZZ.js} +43 -43
  81. package/dist/{chunk-FOJY75BL.js → chunk-ZW5SZ3TH.js} +12 -12
  82. package/dist/{cli-BJQ2ahrh.d.ts → cli-cZyANcmY.d.ts} +1 -1
  83. package/dist/cli.d.ts +2 -2
  84. package/dist/cli.js +59 -59
  85. package/dist/compounding/engine.js +8 -8
  86. package/dist/config.js +1 -1
  87. package/dist/connectors/codex-materialize-runner.js +8 -8
  88. package/dist/connectors/index.js +9 -9
  89. package/dist/contradiction/index.js +2 -2
  90. package/dist/{contradiction-scan-2SODRNUF.js → contradiction-scan-3FJYWS3G.js} +3 -3
  91. package/dist/contradiction-scan-3FJYWS3G.js.map +1 -0
  92. package/dist/conversation-index/backend.js +2 -2
  93. package/dist/entity-retrieval.js +8 -8
  94. package/dist/extraction.js +2 -2
  95. package/dist/index.d.ts +2 -2
  96. package/dist/index.js +126 -126
  97. package/dist/lcm/engine.js +2 -2
  98. package/dist/lcm/index.js +2 -2
  99. package/dist/maintenance/memory-governance.js +8 -8
  100. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
  101. package/dist/maintenance/rebuild-memory-projection.js +9 -9
  102. package/dist/mcp-memory-inspector-app.d.ts +1 -1
  103. package/dist/namespaces/migrate.js +18 -18
  104. package/dist/namespaces/search.js +9 -9
  105. package/dist/namespaces/storage.js +9 -9
  106. package/dist/operator-toolkit.js +24 -24
  107. package/dist/orchestration/maintenance.js +12 -12
  108. package/dist/orchestrator.js +82 -82
  109. package/dist/resume-bundles.js +2 -2
  110. package/dist/schemas.d.ts +10 -10
  111. package/dist/search/factory.js +8 -8
  112. package/dist/search/index.js +8 -8
  113. package/dist/search/lancedb-backend.js +2 -2
  114. package/dist/search/meilisearch-backend.js +2 -2
  115. package/dist/search/orama-backend.js +2 -2
  116. package/dist/semantic-consolidation.js +11 -11
  117. package/dist/semantic-rule-promotion.js +7 -7
  118. package/dist/semantic-rule-verifier.js +8 -8
  119. package/dist/shared-context/manager.d.ts +2 -2
  120. package/dist/storage.d.ts +60 -78
  121. package/dist/storage.js +14 -6
  122. package/dist/summarizer.js +2 -2
  123. package/dist/transfer/backup.js +2 -2
  124. package/dist/transfer/capsule-export.js +2 -2
  125. package/dist/transfer/capsule-import.js +2 -2
  126. package/dist/transfer/types.d.ts +12 -12
  127. package/dist/verified-recall.js +8 -8
  128. package/package.json +2 -2
  129. package/src/access-observe-write-surface.ts +1007 -0
  130. package/src/access-service.ts +48 -794
  131. package/src/orchestration/self-deps.ts +9 -1
  132. package/src/storage/entity-store.ts +4 -2
  133. package/src/storage/memory-read-store.ts +756 -0
  134. package/src/storage.ts +67 -627
  135. package/dist/chunk-GCEV6SXP.js.map +0 -1
  136. package/dist/chunk-XBM5DG4O.js.map +0 -1
  137. /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-observe-write-surface.js.map} +0 -0
  138. /package/dist/{auto-sync-G4YKBPOU.js.map → auto-sync-RZO5ZPHR.js.map} +0 -0
  139. /package/dist/{contradiction-scan-2SODRNUF.js.map → capsule-crypto-FFWJ4X4A.js.map} +0 -0
  140. /package/dist/{chunk-M5QKGHCR.js.map → chunk-3I2IASNK.js.map} +0 -0
  141. /package/dist/{chunk-OCFITZRV.js.map → chunk-4AC57RCM.js.map} +0 -0
  142. /package/dist/{chunk-3DOD6ICJ.js.map → chunk-4I45UZ4V.js.map} +0 -0
  143. /package/dist/{chunk-TAPSNIMA.js.map → chunk-5CEJH5ZN.js.map} +0 -0
  144. /package/dist/{chunk-I6IAJCJS.js.map → chunk-5VWRYFQE.js.map} +0 -0
  145. /package/dist/{chunk-4LQUYBZM.js.map → chunk-5ZKT7WTR.js.map} +0 -0
  146. /package/dist/{chunk-VL5QHTJI.js.map → chunk-6DA5BBLS.js.map} +0 -0
  147. /package/dist/{chunk-LFKEFIJA.js.map → chunk-6DXXIXED.js.map} +0 -0
  148. /package/dist/{chunk-NCNOYM6F.js.map → chunk-7CFGXWYX.js.map} +0 -0
  149. /package/dist/{chunk-TJATYB3F.js.map → chunk-A6XCHI53.js.map} +0 -0
  150. /package/dist/{chunk-UQJ6YTKR.js.map → chunk-BTH6MQJT.js.map} +0 -0
  151. /package/dist/{chunk-G7LCG56O.js.map → chunk-CXW6Y7SI.js.map} +0 -0
  152. /package/dist/{chunk-DS4QM6I7.js.map → chunk-EEKDRAWX.js.map} +0 -0
  153. /package/dist/{chunk-I3UWEZTK.js.map → chunk-EF3VFQC4.js.map} +0 -0
  154. /package/dist/{chunk-437S3G37.js.map → chunk-EJJQDP3H.js.map} +0 -0
  155. /package/dist/{chunk-FQLM6ZUP.js.map → chunk-EVVLGYHR.js.map} +0 -0
  156. /package/dist/{chunk-GGYUR4M2.js.map → chunk-GBXP62CL.js.map} +0 -0
  157. /package/dist/{chunk-N4AGSRVA.js.map → chunk-GG6G2RYR.js.map} +0 -0
  158. /package/dist/{chunk-JITAVROY.js.map → chunk-H2T4OJNI.js.map} +0 -0
  159. /package/dist/{chunk-2MR3MFQB.js.map → chunk-JTYM4HPA.js.map} +0 -0
  160. /package/dist/{chunk-LD53WPMU.js.map → chunk-LQ6JI4VH.js.map} +0 -0
  161. /package/dist/{chunk-GPFXPC2V.js.map → chunk-NG2NHB4C.js.map} +0 -0
  162. /package/dist/{chunk-CIDGUFAL.js.map → chunk-NLXKSM46.js.map} +0 -0
  163. /package/dist/{chunk-T6WHI3JW.js.map → chunk-OLAVQS6S.js.map} +0 -0
  164. /package/dist/{chunk-ER3CCWK2.js.map → chunk-ORAK3LNZ.js.map} +0 -0
  165. /package/dist/{chunk-PJ7DIEFC.js.map → chunk-PBZQVVLG.js.map} +0 -0
  166. /package/dist/{chunk-PJQMISPD.js.map → chunk-PPAEJXVF.js.map} +0 -0
  167. /package/dist/{chunk-GMWKWA6N.js.map → chunk-PRX77NPC.js.map} +0 -0
  168. /package/dist/{chunk-24463PIV.js.map → chunk-PS572XYO.js.map} +0 -0
  169. /package/dist/{chunk-KKOCFUI5.js.map → chunk-REZ4TUD2.js.map} +0 -0
  170. /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
  171. /package/dist/{chunk-V5UV7BOA.js.map → chunk-SQNED75R.js.map} +0 -0
  172. /package/dist/{chunk-FLVL4L3L.js.map → chunk-TCKP4EHY.js.map} +0 -0
  173. /package/dist/{chunk-SP42M3XP.js.map → chunk-TXV3I2O6.js.map} +0 -0
  174. /package/dist/{chunk-S6UCPBPL.js.map → chunk-UNV6ZGTS.js.map} +0 -0
  175. /package/dist/{chunk-2BFLDH5F.js.map → chunk-WEPWY4HB.js.map} +0 -0
  176. /package/dist/{chunk-YQGHW5ML.js.map → chunk-WRFKZEO6.js.map} +0 -0
  177. /package/dist/{chunk-XCXN3D2U.js.map → chunk-Y4TAFMUE.js.map} +0 -0
  178. /package/dist/{chunk-H6S3DJVE.js.map → chunk-YMGQXBPS.js.map} +0 -0
  179. /package/dist/{chunk-T422SYM6.js.map → chunk-ZUDM75KG.js.map} +0 -0
  180. /package/dist/{chunk-XD6JVK5P.js.map → chunk-ZUHD6XZZ.js.map} +0 -0
  181. /package/dist/{chunk-FOJY75BL.js.map → chunk-ZW5SZ3TH.js.map} +0 -0
@@ -251,6 +251,7 @@ import {
251
251
  import { formatProfileTraceAscii } from "./profiling.js";
252
252
  import { resolveAccessSetupCapabilities, resolveGraphConstructionCapabilities, resolveIndexingCapabilities } from "./capabilities.js";
253
253
  import { resolveRecallEnhancementCapabilities } from "./capabilities.js";
254
+ import { AccessObserveWriteSurface } from "./access-observe-write-surface.js";
254
255
  import { AccessLcmSurface } from "./access-lcm-surface.js";
255
256
  import { AccessAdminOpsSurface } from "./access-admin-ops-surface.js";
256
257
  import { AccessRecallSurface } from "./access-recall-surface.js";
@@ -1309,6 +1310,18 @@ export class EngramAccessService {
1309
1310
  private readonly budget: CrossNamespaceBudget;
1310
1311
  private readonly auditAdapter: AccessAuditAdapter | null;
1311
1312
 
1313
+ /** AccessObserveWriteSurface (access-service decomposition). Lazy; selfDeps live wiring. */
1314
+ private _accessObserveWriteSurface: AccessObserveWriteSurface | undefined;
1315
+
1316
+ private get accessObserveWriteSurface(): AccessObserveWriteSurface {
1317
+ if (!this._accessObserveWriteSurface) {
1318
+ this._accessObserveWriteSurface = new AccessObserveWriteSurface(
1319
+ selfDeps<ConstructorParameters<typeof AccessObserveWriteSurface>[0]>(this),
1320
+ );
1321
+ }
1322
+ return this._accessObserveWriteSurface;
1323
+ }
1324
+
1312
1325
  /** AccessLcmSurface (access-service decomposition). Lazy; selfDeps live wiring. */
1313
1326
  private _accessLcmSurface: AccessLcmSurface | undefined;
1314
1327
 
@@ -2468,89 +2481,12 @@ export class EngramAccessService {
2468
2481
  namespace: string,
2469
2482
  collection: string,
2470
2483
  ): Promise<EngramAccessQmdHealthResponse> {
2471
- if (searchBackend !== "qmd" || !qmdEnabled) {
2472
- return {
2473
- enabled: qmdEnabled,
2474
- active: false,
2475
- degraded: false,
2476
- mode: searchBackend !== "qmd" ? "not-selected" : "disabled",
2477
- collection,
2478
- collectionState: "skipped",
2479
- installedVersion: null,
2480
- supportedVersion: null,
2481
- supported: null,
2482
- upgradeAvailable: null,
2483
- doctorAvailable: null,
2484
- debugStatus: searchBackend !== "qmd" ? `backend=${searchBackend}` : "backend=disabled",
2485
- };
2486
- }
2487
-
2488
- if (resolveNamespaceCapabilities(this.orchestrator.config).namespaces === true) {
2489
- const namespaceHealth = await this.namespaceQmdHealth(searchBackend, qmdEnabled, namespace, collection);
2490
- if (namespaceHealth) return namespaceHealth;
2491
- }
2492
-
2493
- const qmd = this.orchestrator.qmd;
2494
- if (!qmd) {
2495
- return {
2496
- enabled: true,
2497
- active: false,
2498
- degraded: true,
2499
- mode: "fallback",
2500
- collection,
2501
- collectionState: "unknown",
2502
- installedVersion: null,
2503
- supportedVersion: null,
2504
- supported: null,
2505
- upgradeAvailable: null,
2506
- doctorAvailable: null,
2507
- debugStatus: "backend=unavailable",
2508
- };
2509
- }
2510
- const diagnosticAvailable = await this.qmdProbeAvailable(searchBackend, qmdEnabled);
2511
- const operationalAvailable = diagnosticAvailable || qmd.isAvailable();
2512
- const collectionState = diagnosticAvailable
2513
- ? await this.qmdCollectionState(searchBackend, qmdEnabled, collection)
2514
- : "unknown";
2515
- const active = operationalAvailable && collectionState !== "missing";
2516
- const degraded =
2517
- searchBackend === "qmd" &&
2518
- qmdEnabled &&
2519
- (!active || !diagnosticAvailable || collectionState === "unknown");
2520
- const debugStatus = qmd.debugStatus();
2521
- const versionStatus =
2522
- "getVersionStatus" in qmd && typeof qmd.getVersionStatus === "function"
2523
- ? qmd.getVersionStatus()
2524
- : null;
2525
- const daemonMode =
2526
- "isDaemonMode" in qmd && typeof qmd.isDaemonMode === "function"
2527
- ? qmd.isDaemonMode() === true
2528
- : false;
2529
- const mode =
2530
- searchBackend !== "qmd"
2531
- ? "not-selected"
2532
- : !qmdEnabled
2533
- ? "disabled"
2534
- : !active
2535
- ? "fallback"
2536
- : daemonMode
2537
- ? "daemon"
2538
- : "cli";
2539
-
2540
- return {
2541
- enabled: qmdEnabled,
2542
- active,
2543
- degraded,
2544
- mode,
2484
+ return this.accessObserveWriteSurface.qmdHealth(
2485
+ searchBackend,
2486
+ qmdEnabled,
2487
+ namespace,
2545
2488
  collection,
2546
- collectionState,
2547
- installedVersion: versionStatus?.installedVersion ?? null,
2548
- supportedVersion: versionStatus?.supportedVersion ?? null,
2549
- supported: versionStatus?.supported ?? null,
2550
- upgradeAvailable: versionStatus?.upgradeAvailable ?? null,
2551
- doctorAvailable: versionStatus?.capabilities?.doctor ?? null,
2552
- debugStatus,
2553
- };
2489
+ );
2554
2490
  }
2555
2491
 
2556
2492
  private async namespaceQmdHealth(
@@ -2559,77 +2495,12 @@ export class EngramAccessService {
2559
2495
  namespace: string,
2560
2496
  fallbackCollection: string,
2561
2497
  ): Promise<EngramAccessQmdHealthResponse | null> {
2562
- if (searchBackend !== "qmd" || !qmdEnabled) return null;
2563
- const searchHealthForNamespace = (
2564
- this.orchestrator as Orchestrator & {
2565
- searchHealthForNamespace?: (
2566
- namespace: string,
2567
- execution?: { signal?: AbortSignal },
2568
- ) => Promise<{
2569
- collection: string;
2570
- available: boolean;
2571
- collectionState: EngramAccessQmdCollectionState;
2572
- debugStatus: string;
2573
- installedVersion: string | null;
2574
- supportedVersion: string | null;
2575
- supported: boolean | null;
2576
- upgradeAvailable: boolean | null;
2577
- doctorAvailable: boolean | null;
2578
- daemonMode: boolean | null;
2579
- }>;
2580
- }
2581
- ).searchHealthForNamespace;
2582
- if (typeof searchHealthForNamespace !== "function") return null;
2583
-
2584
- const controller = new AbortController();
2585
- const timer = setTimeout(() => controller.abort(), 2_000);
2586
- timer.unref?.();
2587
- try {
2588
- const health = await searchHealthForNamespace.call(this.orchestrator, namespace, {
2589
- signal: controller.signal,
2590
- });
2591
- const active = health.available && health.collectionState !== "missing";
2592
- const degraded = !active || health.collectionState === "unknown";
2593
- const mode =
2594
- !active
2595
- ? "fallback"
2596
- : health.daemonMode === true
2597
- ? "daemon"
2598
- : "cli";
2599
-
2600
- return {
2601
- enabled: true,
2602
- active,
2603
- degraded,
2604
- mode,
2605
- collection: health.collection || fallbackCollection,
2606
- collectionState: health.collectionState,
2607
- installedVersion: health.installedVersion,
2608
- supportedVersion: health.supportedVersion,
2609
- supported: health.supported,
2610
- upgradeAvailable: health.upgradeAvailable,
2611
- doctorAvailable: health.doctorAvailable,
2612
- debugStatus: health.debugStatus,
2613
- };
2614
- } catch (error) {
2615
- const detail = displayErrorDetail(error) || "unknown";
2616
- return {
2617
- enabled: true,
2618
- active: false,
2619
- degraded: true,
2620
- mode: "fallback",
2621
- collection: fallbackCollection,
2622
- collectionState: "unknown",
2623
- installedVersion: null,
2624
- supportedVersion: null,
2625
- supported: null,
2626
- upgradeAvailable: null,
2627
- doctorAvailable: null,
2628
- debugStatus: `backend=namespace-unavailable error=${detail}`,
2629
- };
2630
- } finally {
2631
- clearTimeout(timer);
2632
- }
2498
+ return this.accessObserveWriteSurface.namespaceQmdHealth(
2499
+ searchBackend,
2500
+ qmdEnabled,
2501
+ namespace,
2502
+ fallbackCollection,
2503
+ );
2633
2504
  }
2634
2505
 
2635
2506
  private async qmdCollectionState(
@@ -2714,100 +2585,12 @@ export class EngramAccessService {
2714
2585
  return this.orchestrator.generateDaySummary(memories);
2715
2586
  }
2716
2587
 
2717
- /**
2718
- * Build a daily context briefing. Gracefully degrades when the OpenAI key
2719
- * or Responses API is unavailable — never throws for LLM-related problems.
2720
- */
2721
2588
  async briefing(
2722
2589
  request: EngramAccessBriefingRequest,
2723
2590
  ): Promise<EngramAccessBriefingResponse> {
2724
- const config = this.orchestrator.config;
2725
- if (!config.briefing.enabled) {
2726
- throw new EngramAccessInputError("briefing is disabled");
2727
- }
2728
-
2729
- const namespace = this.resolveReadableNamespace(request.namespace, request.principal);
2730
- const storage = await this.orchestrator.getStorage(namespace);
2731
-
2732
- const token = typeof request.since === "string" && request.since.trim().length > 0
2733
- ? request.since.trim()
2734
- : config.briefing.defaultWindow;
2735
- const window = parseBriefingWindow(token);
2736
- if (!window) {
2737
- throw new EngramAccessInputError(`invalid briefing window: ${token}`);
2738
- }
2739
-
2740
- // Validate focus: only treat undefined / empty strings as "no filter".
2741
- // Anything else that parses to null (e.g. "project:", "topic:") is malformed
2742
- // and must be rejected so a templating miss never silently broadens the
2743
- // briefing from a targeted project view to all memories.
2744
- const rawFocus = typeof request.focus === "string" ? request.focus.trim() : "";
2745
- let focus = null;
2746
- if (rawFocus.length > 0) {
2747
- focus = parseBriefingFocus(rawFocus);
2748
- if (!focus) {
2749
- throw new EngramAccessInputError(
2750
- `invalid briefing focus filter: ${request.focus}`,
2751
- );
2752
- }
2753
- }
2754
-
2755
- // Reject unsupported format values explicitly. Programmatic callers that
2756
- // bypass CLI/MCP pre-validation (which already use validateBriefingFormat)
2757
- // could otherwise send a typo like "jsno" and silently receive a response
2758
- // in the default format, masking the client bug and breaking format-dependent
2759
- // automation. Only undefined / absent format falls through to the default.
2760
- const SUPPORTED_FORMATS = ["markdown", "json"] as const;
2761
- if (
2762
- typeof request.format === "string" &&
2763
- !(SUPPORTED_FORMATS as readonly string[]).includes(request.format)
2764
- ) {
2765
- throw new EngramAccessInputError(
2766
- `unsupported briefing format: "${request.format}". Accepted: ${SUPPORTED_FORMATS.join(", ")}.`,
2767
- );
2768
- }
2769
- const format: "markdown" | "json" = request.format === "json"
2770
- ? "json"
2771
- : request.format === "markdown"
2772
- ? "markdown"
2773
- : config.briefing.defaultFormat;
2774
-
2775
- const maxFollowups = typeof request.maxFollowups === "number" && Number.isFinite(request.maxFollowups)
2776
- ? Math.max(0, Math.min(10, Math.floor(request.maxFollowups)))
2777
- : config.briefing.maxFollowups;
2778
-
2779
- const calendarSource = config.briefing.calendarSource
2780
- ? new FileCalendarSource(config.briefing.calendarSource)
2781
- : undefined;
2782
-
2783
- const result = await buildBriefing({
2784
- storage,
2785
- namespace,
2786
- window,
2787
- focus,
2788
- calendarSource,
2789
- maxFollowups,
2790
- allowLlm: config.briefing.llmFollowups,
2791
- openaiApiKey: config.openaiApiKey,
2792
- openaiBaseUrl: config.openaiBaseUrl,
2793
- model: config.model,
2794
- // Without a direct OpenAI key, route follow-ups through the configured
2795
- // LLM chain (gateway model source or local LLM) — same fallback every
2796
- // other LLM feature uses. A configured key keeps its precedence so
2797
- // existing deployments are unchanged.
2798
- followupGenerator: config.openaiApiKey
2799
- ? undefined
2800
- : this.orchestrator.briefingChainFollowupGenerator,
2801
- });
2802
-
2803
- return {
2804
- format,
2805
- window: result.window,
2806
- namespace,
2807
- markdown: result.markdown,
2808
- json: result.json,
2809
- followupsUnavailableReason: result.followupsUnavailableReason,
2810
- };
2591
+ return this.accessObserveWriteSurface.briefing(
2592
+ request,
2593
+ );
2811
2594
  }
2812
2595
 
2813
2596
  /**
@@ -3141,73 +2924,10 @@ export class EngramAccessService {
3141
2924
  request: EngramAccessMemoryStoreRequest,
3142
2925
  hooks?: { enforceWriteQuota?: () => void | Promise<void> },
3143
2926
  ): Promise<EngramAccessWriteResponse> {
3144
- const namespace = await this.resolveCodingScopedWriteNamespace(request);
3145
- const schemaVersion = request.schemaVersion ?? ENGRAM_ACCESS_WRITE_SCHEMA_VERSION;
3146
- if (schemaVersion !== ENGRAM_ACCESS_WRITE_SCHEMA_VERSION) {
3147
- throw new EngramAccessInputError(`unsupported schemaVersion: ${schemaVersion}`);
3148
- }
3149
- const execute = async (): Promise<EngramAccessWriteResponse> => {
3150
- const candidate = this.validateWriteCandidate(request, namespace);
3151
- if (request.dryRun === true) {
3152
- return {
3153
- schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
3154
- operation: "memory_store",
3155
- namespace,
3156
- dryRun: true,
3157
- accepted: true,
3158
- queued: false,
3159
- status: "validated",
3160
- idempotencyKey: request.idempotencyKey?.trim() || undefined,
3161
- };
3162
- }
3163
- const result = await persistExplicitCapture(this.orchestrator, candidate, "memory_store");
3164
- // Seed the session's coding binding ONLY after a real write commits, and
3165
- // only when the namespace came from project scoping (no explicit
3166
- // namespace). This mirrors recall's maybeAttachCodingContext so a LATER
3167
- // bare recall/write on the same session is scoped to the same project —
3168
- // but never binds the session on a dryRun, replay/conflict, quota
3169
- // rejection, or an explicit-namespace write (which bypasses the overlay),
3170
- // since those don't reach this point or aren't project-scoped (Codex review).
3171
- await this.attachCodingContextAfterScopedWrite(request);
3172
- // #1645 (review thread yG-): a tombstone-blocked capture is pending_review
3173
- // (no active copy) — report it as queued_for_review so the HTTP/MCP caller
3174
- // doesn't read it as a successfully stored active memory.
3175
- const blocked = result.tombstoneBlocked === true;
3176
- const response: EngramAccessWriteResponse = {
3177
- schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
3178
- operation: "memory_store",
3179
- namespace,
3180
- dryRun: false,
3181
- accepted: true,
3182
- queued: blocked,
3183
- status: blocked ? "queued_for_review" : result.duplicateOf ? "duplicate" : "stored",
3184
- memoryId: result.id,
3185
- duplicateOf: result.duplicateOf,
3186
- idempotencyKey: request.idempotencyKey?.trim() || undefined,
3187
- };
3188
- log.info(
3189
- `access-write op=memory_store namespace=${namespace} dryRun=false status=${response.status} memoryId=${response.memoryId ?? "-"} idempotency=${response.idempotencyKey ? "yes" : "no"}`,
3190
- );
3191
- return response;
3192
- };
3193
- return this.handleIdempotentWrite({
3194
- operation: "memory_store",
3195
- idempotencyKey: request.idempotencyKey,
3196
- requestFingerprint: {
3197
- schemaVersion,
3198
- content: request.content,
3199
- category: request.category,
3200
- confidence: request.confidence,
3201
- namespace,
3202
- tags: request.tags,
3203
- entityRef: request.entityRef,
3204
- ttl: request.ttl,
3205
- sourceReason: request.sourceReason,
3206
- },
3207
- skip: request.dryRun === true,
3208
- beforeExecute: hooks?.enforceWriteQuota,
3209
- execute,
3210
- });
2927
+ return this.accessObserveWriteSurface.memoryStore(
2928
+ request,
2929
+ hooks,
2930
+ );
3211
2931
  }
3212
2932
 
3213
2933
  async peekMemoryStoreIdempotency(request: EngramAccessMemoryStoreRequest): Promise<EngramAccessIdempotencyStatus> {
@@ -3238,70 +2958,10 @@ export class EngramAccessService {
3238
2958
  request: EngramAccessSuggestionSubmitRequest,
3239
2959
  hooks?: { enforceWriteQuota?: () => void | Promise<void> },
3240
2960
  ): Promise<EngramAccessWriteResponse> {
3241
- const namespace = await this.resolveCodingScopedWriteNamespace(request);
3242
- const schemaVersion = request.schemaVersion ?? ENGRAM_ACCESS_WRITE_SCHEMA_VERSION;
3243
- if (schemaVersion !== ENGRAM_ACCESS_WRITE_SCHEMA_VERSION) {
3244
- throw new EngramAccessInputError(`unsupported schemaVersion: ${schemaVersion}`);
3245
- }
3246
- const execute = async (): Promise<EngramAccessWriteResponse> => {
3247
- const candidate = this.validateWriteCandidate(request, namespace);
3248
- if (request.dryRun === true) {
3249
- return {
3250
- schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
3251
- operation: "suggestion_submit",
3252
- namespace,
3253
- dryRun: true,
3254
- accepted: true,
3255
- queued: true,
3256
- status: "validated",
3257
- idempotencyKey: request.idempotencyKey?.trim() || undefined,
3258
- };
3259
- }
3260
- const result = await queueExplicitCaptureForReview(
3261
- this.orchestrator,
3262
- candidate,
3263
- "suggestion_submit",
3264
- new Error(request.sourceReason?.trim() || "submitted via engram suggestion_submit"),
3265
- );
3266
- // Seed the session binding only after a real, project-scoped submit commits
3267
- // (mirrors memory_store / recall; skips dryRun, replay, quota-reject, and
3268
- // explicit-namespace writes — Codex review).
3269
- await this.attachCodingContextAfterScopedWrite(request);
3270
- const response: EngramAccessWriteResponse = {
3271
- schemaVersion: ENGRAM_ACCESS_WRITE_SCHEMA_VERSION,
3272
- operation: "suggestion_submit",
3273
- namespace,
3274
- dryRun: false,
3275
- accepted: true,
3276
- queued: true,
3277
- status: "queued_for_review",
3278
- memoryId: result.id,
3279
- duplicateOf: result.duplicateOf,
3280
- idempotencyKey: request.idempotencyKey?.trim() || undefined,
3281
- };
3282
- log.info(
3283
- `access-write op=suggestion_submit namespace=${namespace} dryRun=false status=${response.status} memoryId=${response.memoryId ?? "-"} idempotency=${response.idempotencyKey ? "yes" : "no"}`,
3284
- );
3285
- return response;
3286
- };
3287
- return this.handleIdempotentWrite({
3288
- operation: "suggestion_submit",
3289
- idempotencyKey: request.idempotencyKey,
3290
- requestFingerprint: {
3291
- schemaVersion,
3292
- content: request.content,
3293
- category: request.category,
3294
- confidence: request.confidence,
3295
- namespace,
3296
- tags: request.tags,
3297
- entityRef: request.entityRef,
3298
- ttl: request.ttl,
3299
- sourceReason: request.sourceReason,
3300
- },
3301
- skip: request.dryRun === true,
3302
- beforeExecute: hooks?.enforceWriteQuota,
3303
- execute,
3304
- });
2961
+ return this.accessObserveWriteSurface.suggestionSubmit(
2962
+ request,
2963
+ hooks,
2964
+ );
3305
2965
  }
3306
2966
 
3307
2967
  async peekSuggestionSubmitIdempotency(
@@ -4391,228 +4051,9 @@ export class EngramAccessService {
4391
4051
  }
4392
4052
 
4393
4053
  private async runObserve(request: EngramAccessObserveRequest): Promise<EngramAccessObserveResponse> {
4394
- if (!request.sessionKey || typeof request.sessionKey !== "string" || request.sessionKey.trim().length === 0) {
4395
- throw new EngramAccessInputError("sessionKey is required and must be a non-empty string");
4396
- }
4397
- if (!Array.isArray(request.messages) || request.messages.length === 0) {
4398
- throw new EngramAccessInputError("messages is required and must be a non-empty array");
4399
- }
4400
- for (const msg of request.messages) {
4401
- if (!msg || typeof msg !== "object" || typeof msg.role !== "string" || typeof msg.content !== "string") {
4402
- throw new EngramAccessInputError("each message must have a string 'role' and 'content'");
4403
- }
4404
- if (msg.role !== "user" && msg.role !== "assistant") {
4405
- throw new EngramAccessInputError(`invalid message role: ${msg.role} (expected 'user' or 'assistant')`);
4406
- }
4407
- }
4408
-
4409
- // 1. Resolve the FULL effective scope plan BEFORE any session mutation
4410
- // (Codex P2 / Cursor "orphan context after overlay auth"). The plan is
4411
- // read-only and re-runs the SAME authorization as
4412
- // memory_store/suggestion_submit (rule 39): the explicit-namespace check
4413
- // AND the coding-overlay self-base `canWriteNamespace` check both run
4414
- // here. Because `maybeAttachCodingContext` has NOT run yet, the plan's
4415
- // overlay resolves from the per-call `cwd`/`projectTag` fallback
4416
- // (`resolveCodingContextFromOptions`) — identical to the context that
4417
- // would be attached — so the scope is the same either way. Running the
4418
- // plan first means an `observe` that ultimately throws on a non-writable
4419
- // self base leaves NO coding context bound to the session, matching how
4420
- // `memory_store` resolves its full scoped write namespace before any
4421
- // session mutation.
4422
- const scope = await this.resolveMemoryScopePlan(request);
4423
- const writeNamespace = scope.writeNamespace;
4424
-
4425
- // Backward-compatible BASE writable namespace (pre-#1495 response semantics)
4426
- // for the legacy `namespace` response field. DERIVED from the already-resolved
4427
- // scope plan — NOT a second writable-namespace resolution call
4428
- // call (#1505 thread jvO). The fresh call re-authorized `undefined ⇒
4429
- // config.defaultNamespace` a SECOND time; under a restrictive default-namespace
4430
- // write policy that re-auth could REJECT an otherwise valid project-scoped
4431
- // observe whose effective self/project write target the scope plan already
4432
- // authorized (the same target memory_store/suggestion_submit accept). Worse,
4433
- // that post-plan rejection fired AFTER `resolveMemoryScopePlan` may have seeded
4434
- // the coding context, leaving an orphaned session binding behind. The plan is
4435
- // the single authorization point (rule 22 / 39); the legacy field must reuse it
4436
- // and never re-authorize. Pre-#1495 semantics were exactly
4437
- // the writable-namespace resolver (overlay-agnostic): the explicit
4438
- // namespace when supplied, else `config.defaultNamespace` for user-project
4439
- // coding overlays. Hosted scope-profile layers such as `teamProject` report
4440
- // their effective profile write namespace because there is no legacy
4441
- // overlay-compatible base namespace for those writes.
4442
- const namespace = this.legacyResponseNamespaceForScope(scope);
4443
- const shouldWriteObjectiveState =
4444
- resolveObjectiveStateCapabilities(this.orchestrator.config).objectiveStateMemory === true &&
4445
- resolveObjectiveStateCapabilities(this.orchestrator.config).objectiveStateSnapshotWrites === true;
4446
-
4447
- // 2. Auto-resolve coding context from cwd/projectTag so a LATER bare recall
4448
- // on the same session is project-scoped (rule 42: same namespace layer as
4449
- // recall). Done AFTER the scope plan authorized the write, so a rejected
4450
- // request never leaves orphaned context on the session.
4451
- await this.maybeAttachCodingContext(request.sessionKey, {
4452
- cwd: request.cwd,
4453
- projectTag: request.projectTag,
4454
- });
4455
-
4456
- // Prefix sessionKey with the EFFECTIVE write namespace for LCM archival so
4457
- // observed turns are scoped to the same namespace project-scoped recall
4458
- // reads. The SAME `lcmSessionKeyForNamespace` helper is used by the
4459
- // orchestrator recall readers and by compaction flush/record, so the LCM
4460
- // write key and every read/flush key agree (#1495, rule 42). Only prefixes
4461
- // when the namespace diverges from the default store; a single-store
4462
- // deployment keeps the raw sessionKey unchanged.
4463
- const lcmSessionKey =
4464
- lcmSessionKeyForNamespace(
4465
- writeNamespace,
4466
- request.sessionKey,
4467
- this.orchestrator.config.defaultNamespace,
4468
- ) ?? request.sessionKey;
4469
-
4470
- // 4. Objective-state snapshots → the scope plan's objective-state namespace.
4471
- // For explicit-namespace and coding-overlay writes this equals
4472
- // writeNamespace; for an IMPLICIT write it is the principal SELF base
4473
- // (#928 contract, already auth-checked inside the scope plan), not the
4474
- // general default-store write namespace.
4475
- if (shouldWriteObjectiveState) {
4476
- try {
4477
- const objectiveStateLocation =
4478
- await this.objectiveStateStoreLocationForNamespace(
4479
- scope.objectiveStateNamespace,
4480
- );
4481
- await recordObjectiveStateSnapshotsFromObservedMessages({
4482
- memoryDir: objectiveStateLocation.memoryDir,
4483
- objectiveStateStoreDir: objectiveStateLocation.objectiveStateStoreDir,
4484
- objectiveStateMemoryEnabled: resolveObjectiveStateCapabilities(this.orchestrator.config).objectiveStateMemory,
4485
- objectiveStateSnapshotWritesEnabled:
4486
- resolveObjectiveStateCapabilities(this.orchestrator.config).objectiveStateSnapshotWrites,
4487
- sessionKey: request.sessionKey,
4488
- recordedAt: new Date().toISOString(),
4489
- messages: request.messages,
4490
- });
4491
- } catch (err) {
4492
- log.error(`access-observe objective-state snapshot write failed: ${err}`);
4493
- }
4494
- }
4495
-
4496
- // 5. LCM archival → effective write namespace.
4497
- // lcmArchived in the response means "LCM archival was queued" (not
4498
- // "completed"), matching extractionQueued semantics. Both run async.
4499
- let lcmArchived = false;
4500
- if (this.orchestrator.lcmEngine && this.orchestrator.lcmEngine.enabled) {
4501
- // Fire-and-forget: LCM archival writes to SQLite and builds summary
4502
- // DAGs, which can take tens of seconds for large sessions. Don't
4503
- // block the HTTP response — the caller only needs acknowledgment.
4504
- try {
4505
- this.orchestrator.lcmEngine.enqueueObserveMessages(lcmSessionKey, request.messages);
4506
- lcmArchived = true;
4507
- } catch (err) {
4508
- log.error(`access-observe LCM enqueue failed: ${err}`);
4509
- }
4510
- }
4511
-
4512
- // 6. Extraction/replay → effective write namespace for STORAGE, ORIGINAL
4513
- // sessionKey for IDENTITY (provenance + threading).
4514
- let extractionQueued = false;
4515
- if (request.skipExtraction !== true) {
4516
- const turns = request.messages.map((m) => ({
4517
- source: "openclaw" as const,
4518
- // Identity-vs-routing separation (#1505 thread 1, cursor): extraction
4519
- // derives the provenance principal via `resolvePrincipal(turn.sessionKey)`
4520
- // and threads `turn.sessionKey` into conversation threading. Feeding the
4521
- // namespace-PREFIXED `lcmSessionKey` here mis-derived the principal to
4522
- // `default` (a `<ns>:<key>` string matches no prefix/map rule and fails
4523
- // the `agent:` heuristic). Pass the ORIGINAL sessionKey so identity is
4524
- // correct; storage routing is pinned separately via
4525
- // writeNamespaceOverride below, and the authenticated principal is pinned
4526
- // via principalOverride.
4527
- sessionKey: request.sessionKey,
4528
- role: m.role,
4529
- content: m.content,
4530
- parts: m.parts,
4531
- rawContent: m.rawContent,
4532
- sourceFormat: m.sourceFormat,
4533
- timestamp: new Date().toISOString(),
4534
- }));
4535
- // Pin extraction STORAGE to the effective namespace rather than letting the
4536
- // orchestrator re-derive one from the session key + coding overlay — that
4537
- // re-derivation would have to reparse identity and could miss the overlay
4538
- // (the #1495 drift). Passing writeNamespaceOverride makes the extraction
4539
- // target deterministic and identical to LCM/objective-state (rule 39).
4540
- //
4541
- // Pin WHENEVER namespaces are enabled, not only when writeNamespace differs
4542
- // from the default store (#1505 round 3, codex "Pin default-store extraction
4543
- // writes too"). For an unqualified/no-overlay observe by a principal that
4544
- // HAS a self namespace, writeNamespace is `config.defaultNamespace` but an
4545
- // unpinned `runExtraction` would fall back to
4546
- // `defaultNamespaceForPrincipal(principal)` = the SELF namespace — diverging
4547
- // from where LCM/objective-state/response wrote (`default`). Pinning the
4548
- // resolved writeNamespace forces all side effects onto the one scope-plan
4549
- // namespace. When namespaces are DISABLED the router collapses every
4550
- // namespace to one store, so leaving the override undefined preserves the
4551
- // existing single-store routing byte-for-byte.
4552
- const writeNamespaceOverride =
4553
- resolveNamespaceCapabilities(this.orchestrator.config).namespaces === true
4554
- ? writeNamespace
4555
- : undefined;
4556
- // Pin provenance PRINCIPAL to the scope plan's resolved principal (#1505
4557
- // thread 1). The scope plan already applied auth precedence
4558
- // (authenticatedPrincipal/principalOverride > resolvePrincipal(original
4559
- // sessionKey)), so this is the same identity the surface authorized — never
4560
- // a `default` fallback parsed from a prefixed key. Omitted when no principal
4561
- // resolved (namespaces-disabled / unauthenticated single-store), preserving
4562
- // existing behavior.
4563
- const principalOverride =
4564
- typeof scope.principal === "string" && scope.principal.length > 0
4565
- ? scope.principal
4566
- : undefined;
4567
- // Fire-and-forget: queue extraction in the background so the HTTP
4568
- // response returns immediately. LCM archival (above) is also
4569
- // enqueue-only; extraction involves LLM calls that can take
4570
- // minutes under load and should not block the caller.
4571
- //
4572
- // Backpressure: the orchestrator's own extraction queue already
4573
- // limits concurrency (one extraction at a time per session via
4574
- // queueBufferedExtraction). Fire-and-forget here just decouples
4575
- // the HTTP response from the queue drain.
4576
- try {
4577
- const extractionPromise = this.orchestrator.ingestReplayBatch(turns, {
4578
- archiveLcm: false,
4579
- writeNamespaceOverride,
4580
- principalOverride,
4581
- });
4582
- extractionPromise.catch((err) => {
4583
- log.error(`access-observe background extraction failed: ${err}`);
4584
- });
4585
- extractionQueued = true;
4586
- } catch (err) {
4587
- // Synchronous enqueue failure (e.g. orchestrator disposed)
4588
- log.error(`access-observe extraction enqueue failed: ${err}`);
4589
- }
4590
- }
4591
-
4592
- log.info(
4593
- `access-observe namespace=${namespace} effectiveNamespace=${writeNamespace} sessionKey=${request.sessionKey} messages=${request.messages.length} lcm=${lcmArchived} extraction=${extractionQueued}`,
4054
+ return this.accessObserveWriteSurface.runObserve(
4055
+ request,
4594
4056
  );
4595
-
4596
- return {
4597
- accepted: request.messages.length,
4598
- sessionKey: request.sessionKey,
4599
- namespace,
4600
- effectiveNamespace: writeNamespace,
4601
- scopeDebug: {
4602
- principal: scope.principal,
4603
- explicitNamespace: scope.explicitNamespace,
4604
- baseNamespace: scope.baseNamespace,
4605
- writeNamespace: scope.writeNamespace,
4606
- codingOverlayApplied: scope.codingOverlayApplied,
4607
- readNamespaces: scope.readNamespaces,
4608
- scopeProfile: scope.scopeProfile,
4609
- writeLayer: scope.writeLayer,
4610
- layers: scope.layers,
4611
- promotionTargets: scope.promotionTargets,
4612
- },
4613
- lcmArchived,
4614
- extractionQueued,
4615
- };
4616
4057
  }
4617
4058
 
4618
4059
  async lcmSearch(request: EngramAccessLcmSearchRequest): Promise<EngramAccessLcmSearchResponse> {
@@ -5205,68 +4646,9 @@ export class EngramAccessService {
5205
4646
  tags?: string[];
5206
4647
  dueAt?: string;
5207
4648
  }): Promise<unknown> {
5208
- const STATUSES = new Set(["todo", "in_progress", "blocked", "done", "cancelled"]);
5209
- const PRIORITIES = new Set(["low", "medium", "high"]);
5210
- const asStatus = (v?: string) => (v && STATUSES.has(v) ? v as "todo" | "in_progress" | "blocked" | "done" | "cancelled" : undefined);
5211
- const asPriority = (v?: string) => (v && PRIORITIES.has(v) ? v as "low" | "medium" | "high" : undefined);
5212
-
5213
- const storage = new WorkStorage(this.orchestrator.config.memoryDir);
5214
- await storage.ensureDirectories();
5215
- const action = request.action;
5216
-
5217
- if (action === "create") {
5218
- if (!request.title?.trim()) throw new EngramAccessInputError("title is required for create");
5219
- const task = await storage.createTask({
5220
- title: request.title,
5221
- description: request.description,
5222
- status: asStatus(request.status),
5223
- priority: asPriority(request.priority),
5224
- owner: request.owner?.trim() || undefined,
5225
- assignee: request.assignee?.trim() || undefined,
5226
- projectId: request.projectId?.trim() || undefined,
5227
- tags: request.tags,
5228
- dueAt: request.dueAt?.trim() || undefined,
5229
- });
5230
- return { action, task };
5231
- }
5232
- if (action === "get") {
5233
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for get");
5234
- return { action, task: await storage.getTask(request.id) };
5235
- }
5236
- if (action === "list") {
5237
- const tasks = await storage.listTasks({
5238
- status: asStatus(request.status),
5239
- owner: request.owner?.trim() || undefined,
5240
- assignee: request.assignee?.trim() || undefined,
5241
- projectId: request.projectId?.trim() || undefined,
5242
- });
5243
- return { action, count: tasks.length, tasks };
5244
- }
5245
- if (action === "update") {
5246
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for update");
5247
- const patch: Record<string, unknown> = {};
5248
- if (request.title !== undefined) patch.title = request.title;
5249
- if (request.description !== undefined) patch.description = request.description;
5250
- const st = asStatus(request.status); if (st) patch.status = st;
5251
- const pr = asPriority(request.priority); if (pr) patch.priority = pr;
5252
- if (request.owner !== undefined) patch.owner = request.owner || null;
5253
- if (request.assignee !== undefined) patch.assignee = request.assignee || null;
5254
- if (request.projectId !== undefined) patch.projectId = request.projectId || null;
5255
- if (request.tags) patch.tags = request.tags;
5256
- if (request.dueAt !== undefined) patch.dueAt = request.dueAt || null;
5257
- return { action, task: await storage.updateTask(request.id, patch as any) };
5258
- }
5259
- if (action === "transition") {
5260
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for transition");
5261
- const st = asStatus(request.status);
5262
- if (!st) throw new EngramAccessInputError("valid status is required for transition");
5263
- return { action, task: await storage.transitionTask(request.id, st) };
5264
- }
5265
- if (action === "delete") {
5266
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for delete");
5267
- return { action, deleted: await storage.deleteTask(request.id) };
5268
- }
5269
- throw new EngramAccessInputError(`Unsupported work_task action: ${action}`);
4649
+ return this.accessObserveWriteSurface.workTask(
4650
+ request,
4651
+ );
5270
4652
  }
5271
4653
 
5272
4654
  async workProject(request: {
@@ -5280,53 +4662,9 @@ export class EngramAccessService {
5280
4662
  taskId?: string;
5281
4663
  projectId?: string;
5282
4664
  }): Promise<unknown> {
5283
- const STATUSES = new Set(["active", "on_hold", "completed", "archived"]);
5284
- const asStatus = (v?: string) => (v && STATUSES.has(v) ? v as "active" | "on_hold" | "completed" | "archived" : undefined);
5285
-
5286
- const storage = new WorkStorage(this.orchestrator.config.memoryDir);
5287
- await storage.ensureDirectories();
5288
- const action = request.action;
5289
-
5290
- if (action === "create") {
5291
- if (!request.name?.trim()) throw new EngramAccessInputError("name is required for create");
5292
- const project = await storage.createProject({
5293
- name: request.name,
5294
- description: request.description,
5295
- status: asStatus(request.status),
5296
- owner: request.owner?.trim() || undefined,
5297
- tags: request.tags,
5298
- });
5299
- return { action, project };
5300
- }
5301
- if (action === "get") {
5302
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for get");
5303
- return { action, project: await storage.getProject(request.id) };
5304
- }
5305
- if (action === "list") {
5306
- const projects = await storage.listProjects();
5307
- return { action, count: projects.length, projects };
5308
- }
5309
- if (action === "update") {
5310
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for update");
5311
- const patch: Record<string, unknown> = {};
5312
- if (request.name !== undefined) patch.name = request.name;
5313
- if (request.description !== undefined) patch.description = request.description;
5314
- const st = asStatus(request.status); if (st) patch.status = st;
5315
- if (request.owner !== undefined) patch.owner = request.owner || null;
5316
- if (request.tags) patch.tags = request.tags;
5317
- return { action, project: await storage.updateProject(request.id, patch as any) };
5318
- }
5319
- if (action === "delete") {
5320
- if (!request.id?.trim()) throw new EngramAccessInputError("id is required for delete");
5321
- return { action, deleted: await storage.deleteProject(request.id) };
5322
- }
5323
- if (action === "link_task") {
5324
- if (!request.taskId?.trim() || !request.projectId?.trim()) {
5325
- throw new EngramAccessInputError("taskId and projectId are required for link_task");
5326
- }
5327
- return { action, linked: await storage.linkTaskToProject(request.taskId, request.projectId) };
5328
- }
5329
- throw new EngramAccessInputError(`Unsupported work_project action: ${action}`);
4665
+ return this.accessObserveWriteSurface.workProject(
4666
+ request,
4667
+ );
5330
4668
  }
5331
4669
 
5332
4670
  async workBoard(request: {
@@ -5335,26 +4673,9 @@ export class EngramAccessService {
5335
4673
  snapshotJson?: string;
5336
4674
  linkToMemory?: boolean;
5337
4675
  }): Promise<unknown> {
5338
- const memoryDir = this.orchestrator.config.memoryDir;
5339
- await new WorkStorage(memoryDir).ensureDirectories();
5340
- const action = request.action;
5341
- const projectId = request.projectId?.trim() || undefined;
5342
-
5343
- if (action === "export_markdown") {
5344
- const markdown = await exportWorkBoardMarkdown({ memoryDir, projectId });
5345
- return { action, markdown: wrapWorkLayerContext(markdown, { linkToMemory: request.linkToMemory === true }) };
5346
- }
5347
- if (action === "export_snapshot") {
5348
- const snapshot = await exportWorkBoardSnapshot({ memoryDir, projectId });
5349
- return { action, snapshot };
5350
- }
5351
- if (action === "import_snapshot") {
5352
- if (!request.snapshotJson?.trim()) throw new EngramAccessInputError("snapshotJson is required for import_snapshot");
5353
- const snapshot = JSON.parse(request.snapshotJson);
5354
- const result = await importWorkBoardSnapshot({ memoryDir, snapshot, projectId });
5355
- return { action, result };
5356
- }
5357
- throw new EngramAccessInputError(`Unsupported work_board action: ${action}`);
4676
+ return this.accessObserveWriteSurface.workBoard(
4677
+ request,
4678
+ );
5358
4679
  }
5359
4680
 
5360
4681
  // ── Shared Context / Compounding ────────────────────────────────────────
@@ -5767,76 +5088,9 @@ export class EngramAccessService {
5767
5088
  sourcePrompt?: string;
5768
5089
  dryRun?: boolean;
5769
5090
  }): Promise<unknown> {
5770
- const actionTypes = new Set<MemoryActionType>([
5771
- "store_episode",
5772
- "store_note",
5773
- "update_note",
5774
- "create_artifact",
5775
- "summarize_node",
5776
- "discard",
5777
- "link_graph",
5778
- ]);
5779
- if (!actionTypes.has(request.action as MemoryActionType)) {
5780
- throw new EngramAccessInputError(
5781
- `memory_action_apply: invalid action ${JSON.stringify(request.action)}`,
5782
- );
5783
- }
5784
-
5785
- if (resolveCompressionCapabilities(this.orchestrator.config).contextCompressionActions !== true) {
5786
- throw new EngramAccessInputError(
5787
- "memory_action_apply is disabled; enable contextCompressionActionsEnabled to use this tool",
5788
- );
5789
- }
5790
-
5791
- const outcome = request.outcome ?? "skipped";
5792
- if (outcome !== "applied" && outcome !== "skipped" && outcome !== "failed") {
5793
- throw new EngramAccessInputError(
5794
- `memory_action_apply: outcome must be "applied", "skipped", or "failed"; got ${JSON.stringify(outcome)}`,
5795
- );
5796
- }
5797
-
5798
- const resolvedNs = this.writableNamespaceFor(
5799
- request.namespace,
5800
- request.sessionKey,
5801
- request.principal,
5091
+ return this.accessObserveWriteSurface.memoryActionApply(
5092
+ request,
5802
5093
  );
5803
- const inputSummaryParts = [
5804
- request.content,
5805
- request.category ? `category=${request.category}` : undefined,
5806
- request.linkTargetId ? `linkTargetId=${request.linkTargetId}` : undefined,
5807
- request.linkType ? `linkType=${request.linkType}` : undefined,
5808
- typeof request.linkStrength === "number"
5809
- ? `linkStrength=${request.linkStrength}`
5810
- : undefined,
5811
- request.artifactType ? `artifactType=${request.artifactType}` : undefined,
5812
- typeof request.execute === "boolean" ? `execute=${request.execute}` : undefined,
5813
- ].filter((part): part is string => typeof part === "string" && part.length > 0);
5814
-
5815
- const event = {
5816
- action: request.action as MemoryActionType,
5817
- outcome: outcome as MemoryActionOutcome,
5818
- namespace: resolvedNs,
5819
- actor: "access.memory_action_apply",
5820
- subsystem: "access.memory_action_apply",
5821
- reason: request.reason,
5822
- memoryId: request.memoryId,
5823
- sourceSessionKey: request.sessionKey,
5824
- inputSummary: inputSummaryParts.length > 0
5825
- ? inputSummaryParts.join(" | ").slice(0, 500)
5826
- : undefined,
5827
- dryRun: request.dryRun === true,
5828
- promptHash:
5829
- typeof request.sourcePrompt === "string" && request.sourcePrompt.length > 0
5830
- ? createHash("sha256").update(request.sourcePrompt).digest("hex")
5831
- : undefined,
5832
- };
5833
- const preview = this.orchestrator.previewMemoryActionEvent(event);
5834
- if (request.dryRun === true) {
5835
- return { recorded: false, dryRun: true, event: preview };
5836
- }
5837
-
5838
- const recorded = await this.orchestrator.appendMemoryActionEvent(event);
5839
- return { recorded, event: preview };
5840
5094
  }
5841
5095
 
5842
5096
  async contextCheckpoint(request: {