@remnic/core 9.3.769 → 9.3.770

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 (141) hide show
  1. package/dist/access-admin-ops-surface.js +29 -29
  2. package/dist/access-boundary.d.ts +2 -1
  3. package/dist/access-boundary.js +30 -30
  4. package/dist/access-cli.js +83 -81
  5. package/dist/access-cli.js.map +1 -1
  6. package/dist/access-errors.d.ts +5 -0
  7. package/dist/access-errors.js +8 -0
  8. package/dist/access-errors.js.map +1 -0
  9. package/dist/access-http.js +38 -36
  10. package/dist/access-identity-continuity-surface.js +29 -29
  11. package/dist/access-lcm-surface.js +29 -29
  12. package/dist/access-mcp.js +37 -35
  13. package/dist/access-observe-write-surface.js +29 -29
  14. package/dist/access-operations-batch.js +34 -32
  15. package/dist/access-operations.js +36 -34
  16. package/dist/access-recall-surface.js +29 -29
  17. package/dist/access-service.js +29 -29
  18. package/dist/access-surface-catalog.js +1 -0
  19. package/dist/access-surface-catalog.js.map +1 -1
  20. package/dist/active-recall.js +5 -5
  21. package/dist/auto-sync-2SOUW4JW.js +1 -1
  22. package/dist/briefing.js +7 -7
  23. package/dist/causal-behavior.js +5 -5
  24. package/dist/causal-chain.js +3 -3
  25. package/dist/causal-consolidation.js +11 -11
  26. package/dist/causal-retrieval.js +3 -3
  27. package/dist/causal-trajectory.js +1 -1
  28. package/dist/{chunk-R3K4CXPR.js → chunk-3JOFWTQD.js} +2 -2
  29. package/dist/{chunk-QOGXUNWL.js → chunk-3O3BZNSH.js} +4 -4
  30. package/dist/{chunk-SFNQDXXL.js → chunk-3YYWUHA7.js} +3 -3
  31. package/dist/{chunk-GLERZDM7.js → chunk-52TEPNL7.js} +29 -7
  32. package/dist/chunk-52TEPNL7.js.map +1 -0
  33. package/dist/{chunk-N4ZA2I7M.js → chunk-5ACKQLGA.js} +2 -2
  34. package/dist/{chunk-YDYC6PIS.js → chunk-5QJZLJ7Q.js} +2 -2
  35. package/dist/{chunk-ORAK3LNZ.js → chunk-6SHAUM5S.js} +5 -5
  36. package/dist/{chunk-CKNAVW34.js → chunk-7OSULYN2.js} +2 -2
  37. package/dist/{chunk-PA5ALD3S.js → chunk-7XGF5HCC.js} +106 -86
  38. package/dist/chunk-7XGF5HCC.js.map +1 -0
  39. package/dist/{chunk-6T4LTI2F.js → chunk-7XH7VJN4.js} +4 -4
  40. package/dist/{chunk-OX272PBW.js → chunk-CSHUSYUM.js} +2 -2
  41. package/dist/{chunk-EVXI2I6G.js → chunk-E6LXM2GE.js} +41 -41
  42. package/dist/{chunk-OAREUC7N.js → chunk-EWGMA5W5.js} +28 -28
  43. package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
  44. package/dist/chunk-FTYDWKO6.js +101 -0
  45. package/dist/chunk-FTYDWKO6.js.map +1 -0
  46. package/dist/{chunk-2RLECZ6J.js → chunk-IOENSSZC.js} +2 -2
  47. package/dist/{chunk-VKHZFF5P.js → chunk-KFIAFHOC.js} +2 -2
  48. package/dist/{chunk-NDXJJKT2.js → chunk-KMKBD66B.js} +2 -2
  49. package/dist/{chunk-SQNED75R.js → chunk-KYC3RLYJ.js} +1 -1
  50. package/dist/{chunk-V5RVMULT.js → chunk-MLDAD27I.js} +7 -7
  51. package/dist/{chunk-IGVKWKR2.js → chunk-O5EKTMOJ.js} +6 -6
  52. package/dist/{chunk-CNXMWYLA.js → chunk-OP43P53G.js} +6 -6
  53. package/dist/{chunk-4HYFKIGH.js → chunk-PSNMICC2.js} +4 -4
  54. package/dist/{chunk-IVFIYAIP.js → chunk-QKUWPSU3.js} +5 -5
  55. package/dist/chunk-RANHFI3P.js +8 -0
  56. package/dist/chunk-RANHFI3P.js.map +1 -0
  57. package/dist/{chunk-AS2NRB45.js → chunk-RZ3KJZYE.js} +2 -2
  58. package/dist/{chunk-4JG55YV3.js → chunk-SRSPVFSJ.js} +3 -3
  59. package/dist/{chunk-DTTVX6EO.js → chunk-STN5KMSS.js} +12 -12
  60. package/dist/{chunk-SIPZ5UMK.js → chunk-TGROC2UP.js} +25 -4
  61. package/dist/chunk-TGROC2UP.js.map +1 -0
  62. package/dist/{chunk-QPTP6SKC.js → chunk-U72WWTLR.js} +2 -2
  63. package/dist/{chunk-U5GFYMV7.js → chunk-W7VA5667.js} +3 -3
  64. package/dist/{chunk-X7HTU4HO.js → chunk-WB4CFKZ4.js} +2 -2
  65. package/dist/{chunk-NFWTMM2M.js → chunk-XDRJQMYL.js} +2 -2
  66. package/dist/{chunk-CVINPTKG.js → chunk-ZG4FT6IT.js} +5 -5
  67. package/dist/{chunk-MSGPLITO.js → chunk-ZT34X56N.js} +2 -2
  68. package/dist/{chunk-MSGPLITO.js.map → chunk-ZT34X56N.js.map} +1 -1
  69. package/dist/cli.js +58 -56
  70. package/dist/compounding/engine.js +7 -7
  71. package/dist/config.js +2 -2
  72. package/dist/connectors/codex-materialize-runner.js +7 -7
  73. package/dist/connectors/index.js +7 -7
  74. package/dist/consolidation-provenance-check.js +2 -2
  75. package/dist/dashboard-runtime.js +2 -2
  76. package/dist/entity-retrieval.js +7 -7
  77. package/dist/extraction.js +3 -3
  78. package/dist/index.js +133 -131
  79. package/dist/lcm/engine.js +3 -3
  80. package/dist/lcm/index.js +11 -11
  81. package/dist/maintenance/memory-governance.js +8 -8
  82. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
  83. package/dist/maintenance/rebuild-memory-projection.js +9 -9
  84. package/dist/namespaces/migrate.js +12 -12
  85. package/dist/namespaces/search.js +4 -4
  86. package/dist/namespaces/storage.js +7 -7
  87. package/dist/operator-toolkit.js +17 -17
  88. package/dist/orchestration/maintenance.js +9 -9
  89. package/dist/orchestrator.js +83 -81
  90. package/dist/recall-timings.d.ts +26 -0
  91. package/dist/recall-timings.js +16 -0
  92. package/dist/recall-timings.js.map +1 -0
  93. package/dist/resume-bundles.js +3 -3
  94. package/dist/search/factory.js +3 -3
  95. package/dist/search/index.js +7 -7
  96. package/dist/semantic-consolidation.js +10 -10
  97. package/dist/semantic-rule-promotion.js +7 -7
  98. package/dist/semantic-rule-verifier.js +8 -8
  99. package/dist/storage.js +8 -8
  100. package/dist/verified-recall.js +8 -8
  101. package/package.json +2 -2
  102. package/src/access-boundary.ts +2 -0
  103. package/src/access-errors.ts +2 -0
  104. package/src/access-http.ts +32 -1
  105. package/src/access-operations-batch.ts +19 -0
  106. package/src/access-surface-catalog.ts +1 -0
  107. package/src/orchestration/recall-internal.ts +21 -3
  108. package/src/recall-timings-access.test.ts +301 -0
  109. package/src/recall-timings.ts +141 -0
  110. package/dist/chunk-GLERZDM7.js.map +0 -1
  111. package/dist/chunk-PA5ALD3S.js.map +0 -1
  112. package/dist/chunk-SIPZ5UMK.js.map +0 -1
  113. /package/dist/{chunk-R3K4CXPR.js.map → chunk-3JOFWTQD.js.map} +0 -0
  114. /package/dist/{chunk-QOGXUNWL.js.map → chunk-3O3BZNSH.js.map} +0 -0
  115. /package/dist/{chunk-SFNQDXXL.js.map → chunk-3YYWUHA7.js.map} +0 -0
  116. /package/dist/{chunk-N4ZA2I7M.js.map → chunk-5ACKQLGA.js.map} +0 -0
  117. /package/dist/{chunk-YDYC6PIS.js.map → chunk-5QJZLJ7Q.js.map} +0 -0
  118. /package/dist/{chunk-ORAK3LNZ.js.map → chunk-6SHAUM5S.js.map} +0 -0
  119. /package/dist/{chunk-CKNAVW34.js.map → chunk-7OSULYN2.js.map} +0 -0
  120. /package/dist/{chunk-6T4LTI2F.js.map → chunk-7XH7VJN4.js.map} +0 -0
  121. /package/dist/{chunk-OX272PBW.js.map → chunk-CSHUSYUM.js.map} +0 -0
  122. /package/dist/{chunk-EVXI2I6G.js.map → chunk-E6LXM2GE.js.map} +0 -0
  123. /package/dist/{chunk-OAREUC7N.js.map → chunk-EWGMA5W5.js.map} +0 -0
  124. /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
  125. /package/dist/{chunk-2RLECZ6J.js.map → chunk-IOENSSZC.js.map} +0 -0
  126. /package/dist/{chunk-VKHZFF5P.js.map → chunk-KFIAFHOC.js.map} +0 -0
  127. /package/dist/{chunk-NDXJJKT2.js.map → chunk-KMKBD66B.js.map} +0 -0
  128. /package/dist/{chunk-SQNED75R.js.map → chunk-KYC3RLYJ.js.map} +0 -0
  129. /package/dist/{chunk-V5RVMULT.js.map → chunk-MLDAD27I.js.map} +0 -0
  130. /package/dist/{chunk-IGVKWKR2.js.map → chunk-O5EKTMOJ.js.map} +0 -0
  131. /package/dist/{chunk-CNXMWYLA.js.map → chunk-OP43P53G.js.map} +0 -0
  132. /package/dist/{chunk-4HYFKIGH.js.map → chunk-PSNMICC2.js.map} +0 -0
  133. /package/dist/{chunk-IVFIYAIP.js.map → chunk-QKUWPSU3.js.map} +0 -0
  134. /package/dist/{chunk-AS2NRB45.js.map → chunk-RZ3KJZYE.js.map} +0 -0
  135. /package/dist/{chunk-4JG55YV3.js.map → chunk-SRSPVFSJ.js.map} +0 -0
  136. /package/dist/{chunk-DTTVX6EO.js.map → chunk-STN5KMSS.js.map} +0 -0
  137. /package/dist/{chunk-QPTP6SKC.js.map → chunk-U72WWTLR.js.map} +0 -0
  138. /package/dist/{chunk-U5GFYMV7.js.map → chunk-W7VA5667.js.map} +0 -0
  139. /package/dist/{chunk-X7HTU4HO.js.map → chunk-WB4CFKZ4.js.map} +0 -0
  140. /package/dist/{chunk-NFWTMM2M.js.map → chunk-XDRJQMYL.js.map} +0 -0
  141. /package/dist/{chunk-CVINPTKG.js.map → chunk-ZG4FT6IT.js.map} +0 -0
package/dist/storage.js CHANGED
@@ -16,19 +16,19 @@ import {
16
16
  parseFrontmatter,
17
17
  parseFrontmatterStringValue,
18
18
  serializeEntityFile
19
- } from "./chunk-DTTVX6EO.js";
20
- import "./chunk-M7XQSUBB.js";
19
+ } from "./chunk-STN5KMSS.js";
21
20
  import "./chunk-L2B3EE7B.js";
21
+ import "./chunk-M7XQSUBB.js";
22
22
  import "./chunk-5UZXUTVO.js";
23
23
  import "./chunk-5GPPACXK.js";
24
+ import {
25
+ stripAttributesSuffix
26
+ } from "./chunk-SVOZFLIQ.js";
24
27
  import "./chunk-LTJAMRGI.js";
25
28
  import "./chunk-TKZ3AL5V.js";
26
29
  import "./chunk-6CIAGZ6X.js";
27
30
  import "./chunk-J6A3CX5N.js";
28
31
  import "./chunk-DQC3ZZYM.js";
29
- import {
30
- stripAttributesSuffix
31
- } from "./chunk-SVOZFLIQ.js";
32
32
  import "./chunk-JS2HKKOK.js";
33
33
  import "./chunk-AWJ2FHCF.js";
34
34
  import "./chunk-42JKGUFJ.js";
@@ -40,9 +40,9 @@ import "./chunk-DM2T26WE.js";
40
40
  import "./chunk-LDXUBPMO.js";
41
41
  import "./chunk-YTU7WKYN.js";
42
42
  import "./chunk-AAD2ZS6P.js";
43
+ import "./chunk-G7D6GZ5J.js";
43
44
  import "./chunk-VF4XKTX3.js";
44
45
  import "./chunk-W5H6ZRPT.js";
45
- import "./chunk-G7D6GZ5J.js";
46
46
  import "./chunk-VUA4S7P6.js";
47
47
  import "./chunk-IQVQJJL7.js";
48
48
  import "./chunk-L2EXJQJP.js";
@@ -55,13 +55,13 @@ import "./chunk-5GKPFL3S.js";
55
55
  import "./chunk-4DJQYKMN.js";
56
56
  import "./chunk-EYIEWJNI.js";
57
57
  import "./chunk-JUC24CTX.js";
58
- import "./chunk-6MKAMLQL.js";
59
- import "./chunk-JAKIDQB2.js";
60
58
  import "./chunk-NU3CSQ4H.js";
61
59
  import "./chunk-J2HSAU72.js";
62
60
  import "./chunk-A6XUJE5D.js";
63
61
  import "./chunk-P7FMDTKL.js";
64
62
  import "./chunk-VS2IYZRU.js";
63
+ import "./chunk-6MKAMLQL.js";
64
+ import "./chunk-JAKIDQB2.js";
65
65
  import "./chunk-PZ5AY32C.js";
66
66
  export {
67
67
  ContentHashIndex,
@@ -1,19 +1,19 @@
1
1
  import {
2
2
  compareVerifiedEpisodeResults,
3
3
  searchVerifiedEpisodes
4
- } from "./chunk-N4ZA2I7M.js";
4
+ } from "./chunk-5ACKQLGA.js";
5
5
  import "./chunk-7FL4CNPV.js";
6
- import "./chunk-DTTVX6EO.js";
7
- import "./chunk-M7XQSUBB.js";
6
+ import "./chunk-STN5KMSS.js";
8
7
  import "./chunk-L2B3EE7B.js";
8
+ import "./chunk-M7XQSUBB.js";
9
9
  import "./chunk-5UZXUTVO.js";
10
10
  import "./chunk-5GPPACXK.js";
11
+ import "./chunk-SVOZFLIQ.js";
11
12
  import "./chunk-LTJAMRGI.js";
12
13
  import "./chunk-TKZ3AL5V.js";
13
14
  import "./chunk-6CIAGZ6X.js";
14
15
  import "./chunk-J6A3CX5N.js";
15
16
  import "./chunk-DQC3ZZYM.js";
16
- import "./chunk-SVOZFLIQ.js";
17
17
  import "./chunk-JS2HKKOK.js";
18
18
  import "./chunk-AWJ2FHCF.js";
19
19
  import "./chunk-42JKGUFJ.js";
@@ -25,9 +25,9 @@ import "./chunk-DM2T26WE.js";
25
25
  import "./chunk-LDXUBPMO.js";
26
26
  import "./chunk-YTU7WKYN.js";
27
27
  import "./chunk-AAD2ZS6P.js";
28
+ import "./chunk-G7D6GZ5J.js";
28
29
  import "./chunk-VF4XKTX3.js";
29
30
  import "./chunk-W5H6ZRPT.js";
30
- import "./chunk-G7D6GZ5J.js";
31
31
  import "./chunk-VUA4S7P6.js";
32
32
  import "./chunk-IQVQJJL7.js";
33
33
  import "./chunk-L2EXJQJP.js";
@@ -40,14 +40,14 @@ import "./chunk-5GKPFL3S.js";
40
40
  import "./chunk-4DJQYKMN.js";
41
41
  import "./chunk-EYIEWJNI.js";
42
42
  import "./chunk-JUC24CTX.js";
43
- import "./chunk-6MKAMLQL.js";
44
- import "./chunk-JAKIDQB2.js";
45
- import "./chunk-ZBJMUXZH.js";
46
43
  import "./chunk-NU3CSQ4H.js";
47
44
  import "./chunk-J2HSAU72.js";
48
45
  import "./chunk-A6XUJE5D.js";
49
46
  import "./chunk-P7FMDTKL.js";
50
47
  import "./chunk-VS2IYZRU.js";
48
+ import "./chunk-6MKAMLQL.js";
49
+ import "./chunk-JAKIDQB2.js";
50
+ import "./chunk-ZBJMUXZH.js";
51
51
  import "./chunk-PZ5AY32C.js";
52
52
  export {
53
53
  compareVerifiedEpisodeResults,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.3.769",
3
+ "version": "9.3.770",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3000,7 +3000,7 @@
3000
3000
  "core"
3001
3001
  ],
3002
3002
  "peerDependencies": {
3003
- "@remnic/coding-graph": "^9.3.769"
3003
+ "@remnic/coding-graph": "^9.3.770"
3004
3004
  },
3005
3005
  "peerDependenciesMeta": {
3006
3006
  "@remnic/coding-graph": {
@@ -147,6 +147,7 @@ export type OperationName =
147
147
  | "offline_sync_apply"
148
148
  | "lcm_status"
149
149
  | "memory_list"
150
+ | "recall_timings"
150
151
  | "entity_list"
151
152
  | "maintenance_status"
152
153
  | "quality_status"
@@ -178,6 +179,7 @@ export type OperationName =
178
179
  export interface OperationContext {
179
180
  readonly service: EngramAccessService;
180
181
  readonly authenticatedPrincipal?: string;
182
+ readonly operatorPrincipal?: string;
181
183
  readonly hooks?: OperationHooks;
182
184
  }
183
185
 
@@ -0,0 +1,2 @@
1
+ /** Authenticated caller lacks the required authorization (HTTP 403). */
2
+ export class EngramAccessForbiddenError extends Error {}
@@ -8,7 +8,13 @@ import { fileURLToPath, URL } from "node:url";
8
8
  import { gunzipSync } from "node:zlib";
9
9
  import { log } from "./logger.js";
10
10
  import { abortError, isAbortError } from "./abort-error.js";
11
- import { EngramAccessInputError, type EngramAccessService, type EngramAccessMemoryResponse, type EngramAccessWriteResponse } from "./access-service.js";
11
+ import { EngramAccessForbiddenError } from "./access-errors.js";
12
+ import {
13
+ EngramAccessInputError,
14
+ type EngramAccessService,
15
+ type EngramAccessMemoryResponse,
16
+ type EngramAccessWriteResponse,
17
+ } from "./access-service.js";
12
18
  import { CorrectionContractError } from "./correction/correction-contract.js";
13
19
  import { WearablesInputError } from "./wearables/errors.js";
14
20
  import { EngramMcpServer, MCP_SUPPORTED_PROTOCOL_VERSIONS } from "./access-mcp.js";
@@ -451,6 +457,10 @@ export class EngramAccessHttpServer {
451
457
  this.respondJson(res, 400, { error: err.message, code: "input_error" });
452
458
  return;
453
459
  }
460
+ if (err instanceof EngramAccessForbiddenError) {
461
+ this.respondJson(res, 403, { error: err.message, code: "forbidden" });
462
+ return;
463
+ }
454
464
  if (err instanceof CorrectionContractError) {
455
465
  this.respondJson(res, 400, { error: err.message, code: "correction_contract_error" });
456
466
  return;
@@ -1769,6 +1779,27 @@ export class EngramAccessHttpServer {
1769
1779
  return;
1770
1780
  }
1771
1781
 
1782
+ if (req.method === "GET" && pathname === "/engram/v1/recall/timings") {
1783
+ const op = getOperation("recall_timings"); // boundary dispatch (issue #1830)
1784
+ if (!op) {
1785
+ throw new Error("access-boundary: operation not registered: recall_timings");
1786
+ }
1787
+ // Live diagnostic route; no response (success or denial) is cacheable.
1788
+ // Set before dispatch so the 403 path carries the header too.
1789
+ res.setHeader("cache-control", "no-store");
1790
+ // Dispatch through the registered operation so the HTTP route and the
1791
+ // boundary registration share one gate. The server's own principal is
1792
+ // the transport-level operator fallback; a configured
1793
+ // agentAccessHttp.principal outranks it.
1794
+ const output = (await op.run({}, {
1795
+ service: this.service,
1796
+ authenticatedPrincipal: this.resolveRequestPrincipal(req),
1797
+ operatorPrincipal: this.authenticatedPrincipal,
1798
+ })) as { result: unknown };
1799
+ this.respondJson(res, 200, output.result);
1800
+ return;
1801
+ }
1802
+
1772
1803
  const memoryMatch = pathname.match(/^\/engram\/v1\/memories\/([^/]+)$/);
1773
1804
  if (req.method === "GET" && memoryMatch) {
1774
1805
  const memoryId = decodeURIComponent(memoryMatch[1] ?? "");
@@ -14,10 +14,12 @@ import { z } from "zod";
14
14
  import type { DreamsPhase, RecallDisclosure, RecallPlanMode } from "./types.js";
15
15
  import type { RemnicChatGptMemoryInspectorInput } from "./mcp-memory-inspector-app.js";
16
16
  import { defineOperation } from "./access-boundary.js";
17
+ import { EngramAccessForbiddenError } from "./access-errors.js";
17
18
  import { EngramAccessInputError, type EngramAccessService } from "./access-service.js";
18
19
  import { projectTagProjectId } from "./coding/coding-namespace.js";
19
20
  import { expandTildePath } from "./utils/path.js";
20
21
  import { resolvePrincipal } from "./namespaces/principal.js";
22
+ import { getRecallTimingStatus, isRecallTimingsOperator } from "./recall-timings.js";
21
23
  import { validateBriefingFormat } from "./briefing.js";
22
24
  import {
23
25
  buildChatGptMemoryInspectorActionRequest,
@@ -254,6 +256,23 @@ defineOperation({ name: "offline_sync_apply_file_content", description: "Apply f
254
256
  defineOperation({ name: "offline_sync_apply", description: "Apply sync.", schema: strictSchema({ changeset: z.unknown().optional(), namespace: S.str }), handler: async (input, ctx) => ({ result: await ctx.service.offlineSyncApply({ changeset: input.changeset, namespace: optStr(input.namespace), principal: ctx.authenticatedPrincipal }) }) });
255
257
  defineOperation({ name: "lcm_status", description: "LCM status.", schema: strictSchema({}), handler: async (_i, ctx) => ({ result: await ctx.service.lcmStatus() }) });
256
258
  defineOperation({ name: "memory_list", description: "List memories.", schema: strictSchema({}), handler: async (_i, ctx) => ({ result: await ctx.service.memoryBrowse() }) });
259
+ defineOperation({
260
+ name: "recall_timings",
261
+ description: "List recent recall timings.",
262
+ schema: strictSchema({}),
263
+ handler: async (_input, ctx) => {
264
+ if (
265
+ !isRecallTimingsOperator(
266
+ ctx.service.configRef,
267
+ ctx.authenticatedPrincipal,
268
+ ctx.operatorPrincipal,
269
+ )
270
+ ) {
271
+ throw new EngramAccessForbiddenError("recall timings require the configured operator principal");
272
+ }
273
+ return { result: getRecallTimingStatus(ctx.service.configRef) };
274
+ },
275
+ });
257
276
  defineOperation({ name: "entity_list", description: "List entities.", schema: strictSchema({}), handler: async (_i, ctx) => ({ result: await ctx.service.entityList() }) });
258
277
  defineOperation({ name: "maintenance_status", description: "Maintenance status.", schema: strictSchema({}), handler: async (_i, ctx) => ({ result: await ctx.service.maintenance() }) });
259
278
  defineOperation({ name: "quality_status", description: "Quality status.", schema: strictSchema({}), handler: async (_i, ctx) => ({ result: await ctx.service.quality() }) });
@@ -200,6 +200,7 @@ export const HTTP_ROUTES: readonly HttpRouteEntry[] = [
200
200
  { method: "POST", pathname: "/engram/v1/coding/delta", operation: "coding_delta" },
201
201
  { method: "POST", pathname: "/engram/v1/suggestions", operation: "suggestion_submit" },
202
202
  { method: "GET", pathname: "/engram/v1/memories", operation: "memory_list" },
203
+ { method: "GET", pathname: "/engram/v1/recall/timings", operation: "recall_timings" },
203
204
  { method: "GET", pathname: "/engram/v1/memories/:id", operation: "memory_get" },
204
205
  { method: "GET", pathname: "/engram/v1/memories/:id/timeline", operation: "memory_timeline" },
205
206
  { method: "GET", pathname: "/engram/v1/entities", operation: "entity_list" },
@@ -51,6 +51,7 @@ import { createRecallSectionMetricRecorder } from "../recall-qos.js";
51
51
  import { buildRecallQueryPolicy } from "../recall-query-policy.js";
52
52
  import { type GraphRecallExpandedEntry, type LastRecallBudgetSummary, type LastRecallSnapshot, LastRecallStore, RecallHandleHistoryStore } from "../recall-state.js";
53
53
  import { type RecallFilterTrace, type RecallXrayResult, type RecallXrayScoreDecomposition, type RecallXraySnapshot, buildXraySnapshot } from "../recall-xray.js";
54
+ import { recordRecallTiming } from "../recall-timings.js";
54
55
  import { findUnresolvedEntityRefs } from "../reconstruct.js";
55
56
  import { RerankCache, rerankLocalOrNoop } from "../rerank.js";
56
57
  import { buildResponseGuidanceRecallSection, shouldRecallResponseGuidance } from "../response-guidance-recall.js";
@@ -752,6 +753,9 @@ export class RecallInternalCoordinator {
752
753
  // Catalog read touch (issue #1499) is recorded LATER — after the Phase 1
753
754
  // abort gate — so it fires only when retrieval actually runs, not for
754
755
  // aborted / short-circuited recalls.
756
+ const codingContext = sessionKey
757
+ ? this.deps.getCodingContextForSession(sessionKey)
758
+ : null;
755
759
  const scopePlan = resolveScopePlan({
756
760
  config: this.deps.config,
757
761
  sessionKey,
@@ -761,9 +765,7 @@ export class RecallInternalCoordinator {
761
765
  && options.principalOverride.length > 0
762
766
  ? options.principalOverride
763
767
  : undefined,
764
- codingContext: sessionKey
765
- ? this.deps.getCodingContextForSession(sessionKey)
766
- : null,
768
+ codingContext,
767
769
  namespacesEnabled,
768
770
  });
769
771
  const {
@@ -867,6 +869,14 @@ export class RecallInternalCoordinator {
867
869
  const earlySessionKey = sessionKey ?? "default";
868
870
  this.deps._recallWorkspaceOverrides.delete(earlySessionKey);
869
871
  timings.total = `${Date.now() - recallStart}ms`;
872
+ recordRecallTiming(this.deps.config, {
873
+ ...timings,
874
+ timestamp: new Date().toISOString(),
875
+ namespace: selfNamespace,
876
+ total: timings.total,
877
+ recallPlan: timings.recallPlan,
878
+ queryPolicy: timings.queryPolicy,
879
+ });
870
880
  // X-ray capture for the `no_recall` early-return path
871
881
  // (issue #570 PR 1). `no_recall` skips retrieval entirely, so
872
882
  // the snapshot carries zero results and an empty-budget accounting
@@ -5146,6 +5156,14 @@ export class RecallInternalCoordinator {
5146
5156
  .map(([k, v]) => `${k}=${v}`)
5147
5157
  .join(", ");
5148
5158
  log.info(`recall timings: ${timingParts}`);
5159
+ recordRecallTiming(this.deps.config, {
5160
+ ...timings,
5161
+ timestamp: new Date().toISOString(),
5162
+ namespace: selfNamespace,
5163
+ total: timings.total,
5164
+ recallPlan: timings.recallPlan,
5165
+ queryPolicy: timings.queryPolicy,
5166
+ });
5149
5167
 
5150
5168
  const assembledRecall = this.deps.assembleRecallSections(
5151
5169
  sectionBuckets,
@@ -0,0 +1,301 @@
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 { getOperation } from "./access-boundary.js";
8
+ import { EngramAccessHttpServer } from "./access-http.js";
9
+ import { EngramAccessService } from "./access-service.js";
10
+ import { parseConfig } from "./config.js";
11
+ import { Orchestrator } from "./orchestrator.js";
12
+ import {
13
+ getRecallTimings,
14
+ getRecallTimingStatus,
15
+ recordRecallTiming,
16
+ resolveRecallTimingsOperatorPrincipal,
17
+ type RecallTimingRecord,
18
+ } from "./recall-timings.js";
19
+
20
+ function makeConfig(memoryDir: string, overrides: Record<string, unknown> = {}) {
21
+ return parseConfig({
22
+ openaiApiKey: "sk-test",
23
+ memoryDir,
24
+ workspaceDir: path.join(memoryDir, "workspace"),
25
+ qmdEnabled: false,
26
+ embeddingFallbackEnabled: false,
27
+ recallPlannerEnabled: false,
28
+ sharedContextEnabled: false,
29
+ initGateTimeoutMs: 1000,
30
+ ...overrides,
31
+ });
32
+ }
33
+
34
+ test("authenticated recall timings route returns two recalls without user-correlatable inputs", async () => {
35
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-recall-timings-"));
36
+ const config = makeConfig(memoryDir);
37
+ const orchestrator = new Orchestrator(config);
38
+ const service = new EngramAccessService(orchestrator);
39
+ const server = new EngramAccessHttpServer({
40
+ service,
41
+ host: "127.0.0.1",
42
+ port: 0,
43
+ authToken: "secret-token",
44
+ principal: "operator",
45
+ trustPrincipalHeader: true,
46
+ maxBodyBytes: 1024,
47
+ });
48
+ const started = await server.start();
49
+ const url = `http://${started.host}:${started.port}/engram/v1/recall/timings`;
50
+ try {
51
+ await orchestrator.recall("private first query", "private:first:session");
52
+ await orchestrator.recall("private second query", "private:second:session", { mode: "no_recall" });
53
+
54
+ const denied = await fetch(url);
55
+ assert.equal(denied.status, 401);
56
+
57
+ const nonOperator = await fetch(url, {
58
+ headers: {
59
+ Authorization: "Bearer secret-token",
60
+ "X-Engram-Principal": "reader",
61
+ },
62
+ });
63
+ assert.equal(nonOperator.status, 403);
64
+ assert.equal(nonOperator.headers.get("cache-control"), "no-store");
65
+
66
+ const response = await fetch(url, {
67
+ headers: {
68
+ Authorization: "Bearer secret-token",
69
+ "X-Engram-Principal": "operator",
70
+ },
71
+ });
72
+ assert.equal(response.status, 200);
73
+ assert.equal(response.headers.get("cache-control"), "no-store");
74
+ const payload = await response.json() as {
75
+ generatedAt: string;
76
+ processStartedAt: string;
77
+ capacity: number;
78
+ count: number;
79
+ order: string;
80
+ records: RecallTimingRecord[];
81
+ };
82
+ assert.ok(Number.isFinite(Date.parse(payload.generatedAt)));
83
+ assert.ok(Number.isFinite(Date.parse(payload.processStartedAt)));
84
+ assert.ok(Date.parse(payload.processStartedAt) <= Date.parse(payload.generatedAt));
85
+ assert.equal(payload.capacity, 50);
86
+ assert.equal(payload.order, "newest-first");
87
+ assert.equal(payload.count, 2);
88
+ assert.equal(payload.records.length, 2);
89
+ const operation = getOperation("recall_timings");
90
+ assert.ok(operation);
91
+ await assert.rejects(
92
+ operation.run({}, {
93
+ service,
94
+ authenticatedPrincipal: "reader",
95
+ operatorPrincipal: "operator",
96
+ }),
97
+ /configured operator principal/,
98
+ );
99
+ const operationOutput = await operation.run({}, {
100
+ service,
101
+ authenticatedPrincipal: "operator",
102
+ operatorPrincipal: "operator",
103
+ }) as { result: { count: number; records: RecallTimingRecord[] } };
104
+ // generatedAt legitimately differs between the two calls; the data must not.
105
+ assert.equal(operationOutput.result.count, payload.count);
106
+ assert.deepEqual(operationOutput.result.records, payload.records);
107
+ for (const record of payload.records) {
108
+ assert.equal(record.namespace, "default");
109
+ assert.ok(Number.isFinite(Date.parse(record.timestamp)));
110
+ assert.equal(typeof record.timingsMs.total, "number");
111
+ assert.ok(record.timingsMs.total >= 0);
112
+ assert.equal(typeof record.recallPlan, "string");
113
+ assert.equal(typeof record.queryPolicy, "string");
114
+ assert.equal("query" in record, false);
115
+ assert.equal("prompt" in record, false);
116
+ assert.equal("sessionKey" in record, false);
117
+ assert.equal("retrievalQuery" in record, false);
118
+ assert.equal("query" in record.timingsMs, false);
119
+ assert.equal("prompt" in record.timingsMs, false);
120
+ }
121
+ } finally {
122
+ await server.stop();
123
+ await rm(memoryDir, { recursive: true, force: true });
124
+ }
125
+ });
126
+
127
+ test("recall timing history keeps only the newest 50 records", () => {
128
+ const config = makeConfig(path.join(os.tmpdir(), "remnic-recall-timing-ring"));
129
+ for (let index = 0; index < 51; index += 1) {
130
+ recordRecallTiming(config, {
131
+ timestamp: new Date(index * 1000).toISOString(),
132
+ namespace: "default",
133
+ total: index,
134
+ recallPlan: "full",
135
+ queryPolicy: "general/full",
136
+ });
137
+ }
138
+
139
+ const records = getRecallTimings(config);
140
+ assert.equal(records.length, 50);
141
+ assert.equal(records[0]?.timingsMs.total, 50);
142
+ assert.equal(records[49]?.timingsMs.total, 1);
143
+ });
144
+
145
+ test("operator principal resolution agrees for standalone and embedded access", () => {
146
+ const standaloneConfig = makeConfig(path.join(os.tmpdir(), "remnic-recall-timing-standalone"));
147
+ assert.equal(
148
+ resolveRecallTimingsOperatorPrincipal(standaloneConfig, "standalone-operator"),
149
+ "standalone-operator",
150
+ );
151
+
152
+ const embeddedConfig = makeConfig(path.join(os.tmpdir(), "remnic-recall-timing-embedded"), {
153
+ agentAccessHttp: {
154
+ enabled: true,
155
+ host: "127.0.0.1",
156
+ port: 0,
157
+ principal: "embedded-operator",
158
+ maxBodyBytes: 1024,
159
+ },
160
+ });
161
+ assert.equal(
162
+ resolveRecallTimingsOperatorPrincipal(embeddedConfig),
163
+ "embedded-operator",
164
+ );
165
+ assert.equal(
166
+ resolveRecallTimingsOperatorPrincipal(
167
+ embeddedConfig,
168
+ "fallback-transport-principal",
169
+ ),
170
+ "embedded-operator",
171
+ );
172
+ });
173
+
174
+ test("recall timing records expose only the fixed telemetry schema", () => {
175
+ const config = makeConfig(path.join(os.tmpdir(), "remnic-recall-timing-schema"));
176
+ recordRecallTiming(config, {
177
+ timestamp: new Date(0).toISOString(),
178
+ namespace: "default",
179
+ total: "7ms",
180
+ qmd: "3ms",
181
+ qmdPost: "2ms-cache",
182
+ recallPlan: "full",
183
+ queryPolicy: "general/full",
184
+ queryAware: "1ms;helped=private prompt detail",
185
+ prompt: "private prompt",
186
+ sessionKey: "user-session",
187
+ unknownTiming: "9ms",
188
+ });
189
+
190
+ const record = getRecallTimings(config)[0];
191
+ assert.deepEqual(record, {
192
+ timestamp: new Date(0).toISOString(),
193
+ namespace: "default",
194
+ recallPlan: "full",
195
+ queryPolicy: "general/full",
196
+ timingsMs: {
197
+ total: 7,
198
+ qmd: 3,
199
+ qmdPost: 2,
200
+ },
201
+ });
202
+ });
203
+
204
+ test("phases that did not run are omitted while measured zeros survive", () => {
205
+ const config = makeConfig(path.join(os.tmpdir(), "remnic-recall-timing-phases"));
206
+ recordRecallTiming(config, {
207
+ timestamp: new Date(0).toISOString(),
208
+ namespace: "default",
209
+ total: "5ms",
210
+ qmd: "0ms",
211
+ recallPlan: "full",
212
+ queryPolicy: "general/full",
213
+ });
214
+
215
+ const record = getRecallTimings(config)[0];
216
+ assert.ok(record);
217
+ assert.equal(record.timingsMs.qmd, 0);
218
+ assert.equal("qmdPost" in record.timingsMs, false);
219
+ assert.equal("ki" in record.timingsMs, false);
220
+ });
221
+
222
+ test("status envelope reports a stable process start and live generation time", () => {
223
+ const config = makeConfig(path.join(os.tmpdir(), "remnic-recall-timing-envelope"));
224
+ const first = getRecallTimingStatus(config);
225
+ const second = getRecallTimingStatus(config);
226
+ assert.equal(first.processStartedAt, second.processStartedAt);
227
+ assert.ok(Number.isFinite(Date.parse(first.processStartedAt)));
228
+ assert.ok(Date.parse(first.processStartedAt) <= Date.now());
229
+ assert.equal(first.capacity, 50);
230
+ assert.equal(first.order, "newest-first");
231
+ assert.equal(first.count, 0);
232
+ assert.deepEqual(first.records, []);
233
+ });
234
+
235
+ test("configured operator principal outranks a divergent server principal override", async () => {
236
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-recall-timings-div-"));
237
+ const config = makeConfig(memoryDir, {
238
+ agentAccessHttp: {
239
+ enabled: true,
240
+ host: "127.0.0.1",
241
+ port: 0,
242
+ principal: "config-operator",
243
+ maxBodyBytes: 1024,
244
+ },
245
+ });
246
+ const orchestrator = new Orchestrator(config);
247
+ const service = new EngramAccessService(orchestrator);
248
+ const server = new EngramAccessHttpServer({
249
+ service,
250
+ host: "127.0.0.1",
251
+ port: 0,
252
+ authToken: "secret-token",
253
+ principal: "cli-override",
254
+ trustPrincipalHeader: true,
255
+ maxBodyBytes: 1024,
256
+ });
257
+ const started = await server.start();
258
+ const url = `http://${started.host}:${started.port}/engram/v1/recall/timings`;
259
+ try {
260
+ // The server's own default identity is NOT the operator when the config
261
+ // explicitly names a different one. Settled precedence: config first.
262
+ const overrideOnly = await fetch(url, {
263
+ headers: { Authorization: "Bearer secret-token" },
264
+ });
265
+ assert.equal(overrideOnly.status, 403);
266
+
267
+ const configOperator = await fetch(url, {
268
+ headers: {
269
+ Authorization: "Bearer secret-token",
270
+ "X-Engram-Principal": "config-operator",
271
+ },
272
+ });
273
+ assert.equal(configOperator.status, 200);
274
+ } finally {
275
+ await server.stop();
276
+ await rm(memoryDir, { recursive: true, force: true });
277
+ }
278
+ });
279
+
280
+ test("batch context without transport operator fails closed when config names no operator", async () => {
281
+ const config = makeConfig(path.join(os.tmpdir(), "remnic-recall-timing-batch-closed"));
282
+ const orchestrator = new Orchestrator(config);
283
+ const service = new EngramAccessService(orchestrator);
284
+ const operation = getOperation("recall_timings");
285
+ assert.ok(operation);
286
+ // No config principal and no transport operator: nobody can pass the gate,
287
+ // not even a caller whose authenticated principal would match elsewhere.
288
+ await assert.rejects(
289
+ operation.run({}, { service, authenticatedPrincipal: "operator" }),
290
+ /configured operator principal/,
291
+ );
292
+ // The same caller passes once the transport supplies the operator context,
293
+ // as the HTTP route does by dispatching with operatorPrincipal set to the
294
+ // server's own principal.
295
+ const output = await operation.run({}, {
296
+ service,
297
+ authenticatedPrincipal: "operator",
298
+ operatorPrincipal: "operator",
299
+ }) as { result: { count: number } };
300
+ assert.equal(output.result.count, 0);
301
+ });