@remnic/core 9.3.768 → 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.
- package/dist/access-admin-ops-surface.js +29 -29
- package/dist/access-boundary.d.ts +2 -1
- package/dist/access-boundary.js +30 -30
- package/dist/access-cli.js +85 -83
- package/dist/access-cli.js.map +1 -1
- package/dist/access-errors.d.ts +5 -0
- package/dist/access-errors.js +8 -0
- package/dist/access-errors.js.map +1 -0
- package/dist/access-http.d.ts +51 -1
- package/dist/access-http.js +38 -36
- package/dist/access-identity-continuity-surface.js +29 -29
- package/dist/access-lcm-surface.js +29 -29
- package/dist/access-mcp.d.ts +7 -1
- package/dist/access-mcp.js +41 -37
- package/dist/access-observe-write-surface.js +29 -29
- package/dist/access-operations-batch.js +34 -32
- package/dist/access-operations.d.ts +4 -4
- package/dist/access-operations.js +36 -34
- package/dist/access-recall-surface.js +29 -29
- package/dist/access-schema.d.ts +68 -68
- package/dist/access-service.js +29 -29
- package/dist/access-surface-catalog.js +1 -0
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/active-recall.js +5 -5
- package/dist/auto-sync-2SOUW4JW.js +1 -1
- package/dist/briefing.js +7 -7
- package/dist/causal-behavior.js +5 -5
- package/dist/causal-chain.js +3 -3
- package/dist/causal-consolidation.js +11 -11
- package/dist/causal-retrieval.js +3 -3
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-R3K4CXPR.js → chunk-3JOFWTQD.js} +2 -2
- package/dist/{chunk-QOGXUNWL.js → chunk-3O3BZNSH.js} +4 -4
- package/dist/{chunk-SFNQDXXL.js → chunk-3YYWUHA7.js} +3 -3
- package/dist/{chunk-GLERZDM7.js → chunk-52TEPNL7.js} +29 -7
- package/dist/chunk-52TEPNL7.js.map +1 -0
- package/dist/{chunk-N4ZA2I7M.js → chunk-5ACKQLGA.js} +2 -2
- package/dist/{chunk-YDYC6PIS.js → chunk-5QJZLJ7Q.js} +2 -2
- package/dist/{chunk-ORAK3LNZ.js → chunk-6SHAUM5S.js} +5 -5
- package/dist/{chunk-CKNAVW34.js → chunk-7OSULYN2.js} +2 -2
- package/dist/{chunk-PA5ALD3S.js → chunk-7XGF5HCC.js} +106 -86
- package/dist/chunk-7XGF5HCC.js.map +1 -0
- package/dist/{chunk-6T4LTI2F.js → chunk-7XH7VJN4.js} +4 -4
- package/dist/{chunk-OX272PBW.js → chunk-CSHUSYUM.js} +2 -2
- package/dist/{chunk-UG274TNV.js → chunk-E2SPGGUI.js} +13 -8
- package/dist/chunk-E2SPGGUI.js.map +1 -0
- package/dist/{chunk-HBOPSFQQ.js → chunk-E6LXM2GE.js} +41 -41
- package/dist/{chunk-OAREUC7N.js → chunk-EWGMA5W5.js} +28 -28
- package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
- package/dist/chunk-FTYDWKO6.js +101 -0
- package/dist/chunk-FTYDWKO6.js.map +1 -0
- package/dist/{chunk-2RLECZ6J.js → chunk-IOENSSZC.js} +2 -2
- package/dist/{chunk-VKHZFF5P.js → chunk-KFIAFHOC.js} +2 -2
- package/dist/{chunk-NDXJJKT2.js → chunk-KMKBD66B.js} +2 -2
- package/dist/{chunk-SQNED75R.js → chunk-KYC3RLYJ.js} +1 -1
- package/dist/{chunk-V5RVMULT.js → chunk-MLDAD27I.js} +7 -7
- package/dist/{chunk-IGVKWKR2.js → chunk-O5EKTMOJ.js} +6 -6
- package/dist/{chunk-TVLN5EZZ.js → chunk-OP43P53G.js} +83 -8
- package/dist/chunk-OP43P53G.js.map +1 -0
- package/dist/{chunk-4HYFKIGH.js → chunk-PSNMICC2.js} +4 -4
- package/dist/{chunk-IVFIYAIP.js → chunk-QKUWPSU3.js} +5 -5
- package/dist/{chunk-4DLFJJOQ.js → chunk-QMN3CIFS.js} +2 -2
- package/dist/chunk-RANHFI3P.js +8 -0
- package/dist/chunk-RANHFI3P.js.map +1 -0
- package/dist/{chunk-AS2NRB45.js → chunk-RZ3KJZYE.js} +2 -2
- package/dist/{chunk-4JG55YV3.js → chunk-SRSPVFSJ.js} +3 -3
- package/dist/{chunk-DTTVX6EO.js → chunk-STN5KMSS.js} +12 -12
- package/dist/{chunk-J3UJJZKI.js → chunk-TGROC2UP.js} +108 -7
- package/dist/chunk-TGROC2UP.js.map +1 -0
- package/dist/{chunk-QPTP6SKC.js → chunk-U72WWTLR.js} +2 -2
- package/dist/{chunk-U5GFYMV7.js → chunk-W7VA5667.js} +3 -3
- package/dist/{chunk-X7HTU4HO.js → chunk-WB4CFKZ4.js} +2 -2
- package/dist/{chunk-NFWTMM2M.js → chunk-XDRJQMYL.js} +2 -2
- package/dist/{chunk-CVINPTKG.js → chunk-ZG4FT6IT.js} +5 -5
- package/dist/{chunk-MSGPLITO.js → chunk-ZT34X56N.js} +2 -2
- package/dist/{chunk-MSGPLITO.js.map → chunk-ZT34X56N.js.map} +1 -1
- package/dist/cli.js +58 -56
- package/dist/compounding/engine.js +7 -7
- package/dist/config.js +2 -2
- package/dist/connectors/codex-materialize-runner.js +7 -7
- package/dist/connectors/index.js +9 -9
- package/dist/consolidation-provenance-check.js +2 -2
- package/dist/dashboard-runtime.js +2 -2
- package/dist/entity-retrieval.js +7 -7
- package/dist/extraction.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +141 -133
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +11 -11
- package/dist/maintenance/memory-governance.js +8 -8
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
- package/dist/maintenance/rebuild-memory-projection.js +9 -9
- package/dist/namespaces/migrate.js +12 -12
- package/dist/namespaces/search.js +4 -4
- package/dist/namespaces/storage.js +7 -7
- package/dist/operator-toolkit.js +17 -17
- package/dist/orchestration/maintenance.js +9 -9
- package/dist/orchestrator.js +85 -83
- package/dist/recall-timings.d.ts +26 -0
- package/dist/recall-timings.js +16 -0
- package/dist/recall-timings.js.map +1 -0
- package/dist/resume-bundles.js +3 -3
- package/dist/schemas.d.ts +84 -84
- package/dist/search/factory.js +3 -3
- package/dist/search/index.js +7 -7
- package/dist/semantic-consolidation.js +10 -10
- package/dist/semantic-rule-promotion.js +7 -7
- package/dist/semantic-rule-verifier.js +8 -8
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/storage.js +8 -8
- package/dist/tokens.d.ts +3 -1
- package/dist/tokens.js +3 -1
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +8 -8
- package/package.json +2 -2
- package/src/access-boundary.ts +2 -0
- package/src/access-errors.ts +2 -0
- package/src/access-http.test.ts +350 -0
- package/src/access-http.ts +192 -7
- package/src/access-mcp.ts +120 -3
- package/src/access-operations-batch.ts +19 -0
- package/src/access-surface-catalog.ts +1 -0
- package/src/index.ts +3 -0
- package/src/orchestration/recall-internal.ts +21 -3
- package/src/recall-timings-access.test.ts +301 -0
- package/src/recall-timings.ts +141 -0
- package/src/tokens.test.ts +58 -0
- package/src/tokens.ts +19 -8
- package/dist/chunk-GLERZDM7.js.map +0 -1
- package/dist/chunk-J3UJJZKI.js.map +0 -1
- package/dist/chunk-PA5ALD3S.js.map +0 -1
- package/dist/chunk-TVLN5EZZ.js.map +0 -1
- package/dist/chunk-UG274TNV.js.map +0 -1
- /package/dist/{chunk-R3K4CXPR.js.map → chunk-3JOFWTQD.js.map} +0 -0
- /package/dist/{chunk-QOGXUNWL.js.map → chunk-3O3BZNSH.js.map} +0 -0
- /package/dist/{chunk-SFNQDXXL.js.map → chunk-3YYWUHA7.js.map} +0 -0
- /package/dist/{chunk-N4ZA2I7M.js.map → chunk-5ACKQLGA.js.map} +0 -0
- /package/dist/{chunk-YDYC6PIS.js.map → chunk-5QJZLJ7Q.js.map} +0 -0
- /package/dist/{chunk-ORAK3LNZ.js.map → chunk-6SHAUM5S.js.map} +0 -0
- /package/dist/{chunk-CKNAVW34.js.map → chunk-7OSULYN2.js.map} +0 -0
- /package/dist/{chunk-6T4LTI2F.js.map → chunk-7XH7VJN4.js.map} +0 -0
- /package/dist/{chunk-OX272PBW.js.map → chunk-CSHUSYUM.js.map} +0 -0
- /package/dist/{chunk-HBOPSFQQ.js.map → chunk-E6LXM2GE.js.map} +0 -0
- /package/dist/{chunk-OAREUC7N.js.map → chunk-EWGMA5W5.js.map} +0 -0
- /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
- /package/dist/{chunk-2RLECZ6J.js.map → chunk-IOENSSZC.js.map} +0 -0
- /package/dist/{chunk-VKHZFF5P.js.map → chunk-KFIAFHOC.js.map} +0 -0
- /package/dist/{chunk-NDXJJKT2.js.map → chunk-KMKBD66B.js.map} +0 -0
- /package/dist/{chunk-SQNED75R.js.map → chunk-KYC3RLYJ.js.map} +0 -0
- /package/dist/{chunk-V5RVMULT.js.map → chunk-MLDAD27I.js.map} +0 -0
- /package/dist/{chunk-IGVKWKR2.js.map → chunk-O5EKTMOJ.js.map} +0 -0
- /package/dist/{chunk-4HYFKIGH.js.map → chunk-PSNMICC2.js.map} +0 -0
- /package/dist/{chunk-IVFIYAIP.js.map → chunk-QKUWPSU3.js.map} +0 -0
- /package/dist/{chunk-4DLFJJOQ.js.map → chunk-QMN3CIFS.js.map} +0 -0
- /package/dist/{chunk-AS2NRB45.js.map → chunk-RZ3KJZYE.js.map} +0 -0
- /package/dist/{chunk-4JG55YV3.js.map → chunk-SRSPVFSJ.js.map} +0 -0
- /package/dist/{chunk-DTTVX6EO.js.map → chunk-STN5KMSS.js.map} +0 -0
- /package/dist/{chunk-QPTP6SKC.js.map → chunk-U72WWTLR.js.map} +0 -0
- /package/dist/{chunk-U5GFYMV7.js.map → chunk-W7VA5667.js.map} +0 -0
- /package/dist/{chunk-X7HTU4HO.js.map → chunk-WB4CFKZ4.js.map} +0 -0
- /package/dist/{chunk-NFWTMM2M.js.map → chunk-XDRJQMYL.js.map} +0 -0
- /package/dist/{chunk-CVINPTKG.js.map → chunk-ZG4FT6IT.js.map} +0 -0
package/src/access-mcp.ts
CHANGED
|
@@ -78,7 +78,79 @@ type McpResource = {
|
|
|
78
78
|
_meta?: Record<string, unknown>;
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Conservative allowlist of canonical MCP tool suffixes that are
|
|
83
|
+
* unambiguously read-only. Tools in this set are tagged with
|
|
84
|
+
* `annotations: { readOnlyHint: true }` so ChatGPT (and other MCP
|
|
85
|
+
* clients that honor the hint) can skip per-call confirmation.
|
|
86
|
+
*
|
|
87
|
+
* The list is suffix-based so it covers both the `remnic.*` and
|
|
88
|
+
* `engram.*` naming forms. Anything not on it stays unannotated:
|
|
89
|
+
* uncertainty is resolved as "might mutate".
|
|
90
|
+
*
|
|
91
|
+
* Excluded by construction: anything that writes, runs a pipeline,
|
|
92
|
+
* flushes, applies, records, imports, or destructively deletes.
|
|
93
|
+
*/
|
|
94
|
+
const MCP_READ_ONLY_TOOL_SUFFIXES: Readonly<Record<string, true>> = {
|
|
95
|
+
recall: true,
|
|
96
|
+
recall_explain: true,
|
|
97
|
+
recall_tier_explain: true,
|
|
98
|
+
recall_xray: true,
|
|
99
|
+
briefing: true,
|
|
100
|
+
wearables_status: true,
|
|
101
|
+
transcript_day: true,
|
|
102
|
+
transcript_search: true,
|
|
103
|
+
transcript_memories: true,
|
|
104
|
+
action_confidence: true,
|
|
105
|
+
capsule_list: true,
|
|
106
|
+
procedural_stats: true,
|
|
107
|
+
memory_get: true,
|
|
108
|
+
memory_timeline: true,
|
|
109
|
+
entity_get: true,
|
|
110
|
+
review_queue_list: true,
|
|
111
|
+
lcm_search: true,
|
|
112
|
+
continuity_incident_list: true,
|
|
113
|
+
identity_anchor_get: true,
|
|
114
|
+
memory_identity: true,
|
|
115
|
+
memory_search: true,
|
|
116
|
+
memory_profile: true,
|
|
117
|
+
memory_entities_list: true,
|
|
118
|
+
memory_questions: true,
|
|
119
|
+
memory_last_recall: true,
|
|
120
|
+
memory_intent_debug: true,
|
|
121
|
+
memory_qmd_debug: true,
|
|
122
|
+
memory_graph_explain: true,
|
|
123
|
+
graph_snapshot: true,
|
|
124
|
+
review_list: true,
|
|
125
|
+
profiling_report: true,
|
|
126
|
+
peer_list: true,
|
|
127
|
+
peer_get: true,
|
|
128
|
+
peer_profile_get: true,
|
|
129
|
+
console_state: true,
|
|
130
|
+
dreams_status: true,
|
|
131
|
+
codegraph_list_projects: true,
|
|
132
|
+
codegraph_index_status: true,
|
|
133
|
+
codegraph_search_graph: true,
|
|
134
|
+
codegraph_trace_path: true,
|
|
135
|
+
codegraph_detect_changes: true,
|
|
136
|
+
codegraph_query_graph: true,
|
|
137
|
+
codegraph_get_schema: true,
|
|
138
|
+
codegraph_get_snippet: true,
|
|
139
|
+
codegraph_get_architecture: true,
|
|
140
|
+
codegraph_search_code: true,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* MCP protocol versions this server understands, ordered newest → oldest.
|
|
145
|
+
* Exported so the HTTP transport can validate the `MCP-Protocol-Version`
|
|
146
|
+
* header and reject requests advertising an unknown version.
|
|
147
|
+
*/
|
|
148
|
+
export const MCP_SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = [
|
|
149
|
+
"2025-06-18",
|
|
150
|
+
"2025-03-26",
|
|
151
|
+
"2024-11-05",
|
|
152
|
+
];
|
|
153
|
+
const MCP_DEFAULT_PROTOCOL_VERSION: string = MCP_SUPPORTED_PROTOCOL_VERSIONS[0] ?? "2025-06-18";
|
|
82
154
|
const LEGACY_MCP_PREFIX = "engram.";
|
|
83
155
|
const CANONICAL_MCP_PREFIX = "remnic.";
|
|
84
156
|
|
|
@@ -87,13 +159,27 @@ function toCanonicalToolName(name: string): string {
|
|
|
87
159
|
? `${CANONICAL_MCP_PREFIX}${name.slice(LEGACY_MCP_PREFIX.length)}`
|
|
88
160
|
: name;
|
|
89
161
|
}
|
|
90
|
-
|
|
91
162
|
function toLegacyToolName(name: string): string {
|
|
92
163
|
return name.startsWith(CANONICAL_MCP_PREFIX)
|
|
93
164
|
? `${LEGACY_MCP_PREFIX}${name.slice(CANONICAL_MCP_PREFIX.length)}`
|
|
94
165
|
: name;
|
|
95
166
|
}
|
|
96
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Suffix-based allowlist matcher. Returns true for tools whose canonical
|
|
170
|
+
* suffix (after stripping `remnic.` or `engram.`) is in
|
|
171
|
+
* {@link MCP_READ_ONLY_TOOL_SUFFIXES}. Unprefixed names are treated as
|
|
172
|
+
* unannotated.
|
|
173
|
+
*/
|
|
174
|
+
function isReadOnlyToolName(name: string): boolean {
|
|
175
|
+
for (const prefix of [CANONICAL_MCP_PREFIX, LEGACY_MCP_PREFIX]) {
|
|
176
|
+
if (name.startsWith(prefix)) {
|
|
177
|
+
return MCP_READ_ONLY_TOOL_SUFFIXES[name.slice(prefix.length)] === true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
97
183
|
function withToolAliases(tool: McpTool, emitLegacyTools = true): McpTool[] {
|
|
98
184
|
const canonicalName = toCanonicalToolName(tool.name);
|
|
99
185
|
const canonicalTool = canonicalName === tool.name ? tool : { ...tool, name: canonicalName };
|
|
@@ -2342,6 +2428,17 @@ export class EngramMcpServer {
|
|
|
2342
2428
|
);
|
|
2343
2429
|
this.tools = [...this.tools, ...chatTools];
|
|
2344
2430
|
}
|
|
2431
|
+
// Apply `readOnlyHint` annotations to the conservative read-only
|
|
2432
|
+
// allowlist. Done as a final pass so every spread (chat, codegraph,
|
|
2433
|
+
// coding_*, correction, etc.) inherits the annotation without
|
|
2434
|
+
// scattering the same logic across every `withToolAliases` call site.
|
|
2435
|
+
// Suffix-based matching covers both the `remnic.*` and `engram.*`
|
|
2436
|
+
// naming forms emitted by `withToolAliases`.
|
|
2437
|
+
this.tools = this.tools.map((tool) =>
|
|
2438
|
+
isReadOnlyToolName(tool.name) && tool.annotations?.readOnlyHint !== true
|
|
2439
|
+
? { ...tool, annotations: { ...(tool.annotations ?? {}), readOnlyHint: true } }
|
|
2440
|
+
: tool,
|
|
2441
|
+
);
|
|
2345
2442
|
}
|
|
2346
2443
|
|
|
2347
2444
|
/** Get clientInfo for a specific MCP session. Returns undefined for non-MCP requests. */
|
|
@@ -2369,6 +2466,24 @@ export class EngramMcpServer {
|
|
|
2369
2466
|
}
|
|
2370
2467
|
if (method === "initialize") {
|
|
2371
2468
|
const params = request.params ?? {};
|
|
2469
|
+
// MCP initialize REQUIRES params.protocolVersion (string). Reject a
|
|
2470
|
+
// missing/mistyped field with JSON-RPC invalid params instead of
|
|
2471
|
+
// silently negotiating (repo rule: never reinterpret invalid input).
|
|
2472
|
+
// An unsupported-but-well-formed version gets the spec-mandated
|
|
2473
|
+
// counter-offer below instead: the server answers with the newest
|
|
2474
|
+
// version it supports and the client decides whether to proceed.
|
|
2475
|
+
if (typeof params.protocolVersion !== "string" || params.protocolVersion.length === 0) {
|
|
2476
|
+
return {
|
|
2477
|
+
jsonrpc: "2.0",
|
|
2478
|
+
id,
|
|
2479
|
+
error: {
|
|
2480
|
+
code: -32602,
|
|
2481
|
+
message:
|
|
2482
|
+
"initialize requires params.protocolVersion (string); " +
|
|
2483
|
+
`supported versions: ${MCP_SUPPORTED_PROTOCOL_VERSIONS.join(", ")}`,
|
|
2484
|
+
},
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2372
2487
|
const rawClientInfo = params.clientInfo as { name?: string; version?: string } | undefined;
|
|
2373
2488
|
// Generate a server-side session ID for this MCP session.
|
|
2374
2489
|
// The caller should send this back as Mcp-Session-Id on subsequent requests.
|
|
@@ -2391,7 +2506,9 @@ export class EngramMcpServer {
|
|
|
2391
2506
|
jsonrpc: "2.0",
|
|
2392
2507
|
id,
|
|
2393
2508
|
result: {
|
|
2394
|
-
protocolVersion:
|
|
2509
|
+
protocolVersion: MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(params.protocolVersion)
|
|
2510
|
+
? params.protocolVersion
|
|
2511
|
+
: MCP_DEFAULT_PROTOCOL_VERSION,
|
|
2395
2512
|
capabilities: {
|
|
2396
2513
|
tools: {},
|
|
2397
2514
|
resources: {},
|
|
@@ -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" },
|
package/src/index.ts
CHANGED
|
@@ -977,11 +977,14 @@ export {
|
|
|
977
977
|
// ---------------------------------------------------------------------------
|
|
978
978
|
|
|
979
979
|
export {
|
|
980
|
+
buildTokenEntry,
|
|
981
|
+
commitTokenEntry,
|
|
980
982
|
generateToken,
|
|
981
983
|
listTokens,
|
|
982
984
|
revokeToken,
|
|
983
985
|
getAllValidTokens,
|
|
984
986
|
getAllValidTokensCached,
|
|
987
|
+
getAllValidTokenEntriesCached,
|
|
985
988
|
resolveConnectorFromToken,
|
|
986
989
|
loadTokenStore,
|
|
987
990
|
saveTokenStore,
|
|
@@ -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
|
|
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
|
+
});
|