@gmickel/gno 1.40.0 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +1 -0
  2. package/assets/skill/SKILL.md +17 -0
  3. package/assets/skill/cli-reference.md +48 -0
  4. package/assets/skill/mcp-reference.md +24 -0
  5. package/browser-extension/artifacts/{gno-browser-clipper-v1.40.0.zip → gno-browser-clipper-v1.41.0.zip} +0 -0
  6. package/browser-extension/artifacts/gno-browser-clipper-v1.41.0.zip.sha256 +1 -0
  7. package/browser-extension/dist/manifest.json +1 -1
  8. package/package.json +1 -1
  9. package/spec/cli.md +146 -7
  10. package/spec/db/schema.sql +17 -0
  11. package/spec/mcp.md +194 -0
  12. package/spec/output-schemas/memory-recall.schema.json +159 -0
  13. package/spec/output-schemas/memory-remember.schema.json +164 -0
  14. package/spec/output-schemas/status.schema.json +269 -54
  15. package/src/cli/commands/memory.ts +491 -0
  16. package/src/cli/commands/status.ts +23 -4
  17. package/src/cli/options.ts +4 -0
  18. package/src/cli/program.ts +127 -0
  19. package/src/config/types.ts +7 -0
  20. package/src/core/audit-provenance.ts +91 -0
  21. package/src/core/audit-workspace.ts +17 -0
  22. package/src/core/memory-diagnostics.ts +144 -0
  23. package/src/core/memory-fence.ts +239 -0
  24. package/src/core/memory-recall.ts +269 -0
  25. package/src/core/memory-record.ts +435 -0
  26. package/src/core/memory-remember.ts +425 -0
  27. package/src/core/memory-types.ts +211 -0
  28. package/src/core/memory.ts +87 -0
  29. package/src/ingestion/sync.ts +17 -0
  30. package/src/mcp/http-egress.ts +2 -0
  31. package/src/mcp/tools/index.ts +43 -0
  32. package/src/mcp/tools/memory-recall.ts +122 -0
  33. package/src/mcp/tools/memory-remember.ts +177 -0
  34. package/src/mcp/tools/memory-shared.ts +80 -0
  35. package/src/pipeline/search.ts +2 -0
  36. package/src/pipeline/types.ts +8 -0
  37. package/src/sdk/client.ts +94 -1
  38. package/src/sdk/index.ts +13 -0
  39. package/src/sdk/types.ts +28 -0
  40. package/src/serve/routes/api.ts +167 -0
  41. package/src/serve/server.ts +26 -0
  42. package/src/store/migrations/027-memory-scopes.ts +37 -0
  43. package/src/store/migrations/index.ts +2 -0
  44. package/src/store/sqlite/adapter.ts +127 -3
  45. package/src/store/types.ts +54 -0
  46. package/browser-extension/artifacts/gno-browser-clipper-v1.40.0.zip.sha256 +0 -1
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Transport-neutral memory service: `remember()` and `recall()`.
3
+ *
4
+ * Every surface (CLI, MCP, REST, SDK) is a thin adapter over this module.
5
+ * The service owns the shared write lease for every write path; adapters
6
+ * never take `.mcp-write.lock` themselves (single acquisition point, no
7
+ * nesting — a caller that already holds the lease deadlocks/fails fast).
8
+ *
9
+ * This file is the facade: the contracts live in `memory-types`, validation,
10
+ * the context fence, and receipts in `memory-fence`, the write path in
11
+ * `memory-remember`, and retrieval in `memory-recall`. Import from here.
12
+ *
13
+ * @module src/core/memory
14
+ */
15
+
16
+ import type {
17
+ MemoryCandidate,
18
+ MemoryMatchDiagnostics,
19
+ MemoryServiceDeps,
20
+ RecallInput,
21
+ RecallResult,
22
+ RememberInput,
23
+ RememberResult,
24
+ } from "./memory-types";
25
+
26
+ import { recallFacts } from "./memory-recall";
27
+ import {
28
+ type CandidateQuery,
29
+ findMemoryCandidates,
30
+ rememberFact,
31
+ } from "./memory-remember";
32
+
33
+ export {
34
+ MEMORY_CANDIDATE_POOL,
35
+ MEMORY_EMPTY_RECALL_HINT,
36
+ MEMORY_LEXICAL_LIKELY_THRESHOLD,
37
+ MEMORY_RECALL_MAX_FACTS,
38
+ MEMORY_RECALL_MAX_TOKENS,
39
+ MEMORY_SEMANTIC_LIKELY_THRESHOLD,
40
+ MemoryError,
41
+ } from "./memory-types";
42
+ export type {
43
+ MemoryCandidate,
44
+ MemoryCandidateMatch,
45
+ MemoryDecision,
46
+ MemoryErrorCode,
47
+ MemoryFact,
48
+ MemoryIdentity,
49
+ MemoryMatchDiagnostics,
50
+ MemoryMatchMode,
51
+ MemoryRecallReceipt,
52
+ MemoryServiceDeps,
53
+ MemorySyncState,
54
+ RecalledFact,
55
+ RecallInput,
56
+ RecallResult,
57
+ RememberInput,
58
+ RememberResult,
59
+ } from "./memory-types";
60
+
61
+ export class MemoryService {
62
+ private readonly deps: MemoryServiceDeps;
63
+
64
+ constructor(deps: MemoryServiceDeps) {
65
+ this.deps = deps;
66
+ }
67
+
68
+ /**
69
+ * Candidate pool: BM25 top-16 (any-term) within the scope intersection,
70
+ * current facts only. Similarity by cosine when semantic is ready, else by
71
+ * normalized-token Jaccard. Ordered by similarity desc, ties by recordId.
72
+ */
73
+ findCandidates(input: CandidateQuery): Promise<{
74
+ candidates: MemoryCandidate[];
75
+ matching: MemoryMatchDiagnostics;
76
+ }> {
77
+ return findMemoryCandidates(this.deps, input);
78
+ }
79
+
80
+ remember(input: RememberInput): Promise<RememberResult> {
81
+ return rememberFact(this.deps, input);
82
+ }
83
+
84
+ recall(input: RecallInput): Promise<RecallResult> {
85
+ return recallFacts(this.deps, input);
86
+ }
87
+ }
@@ -60,6 +60,7 @@ import {
60
60
  parseLinks,
61
61
  parseTargetParts,
62
62
  } from "../core/links";
63
+ import { extractMemoryScopes } from "../core/memory-record";
63
64
  import { normalizeTag, validateTag } from "../core/tags";
64
65
  import { defaultChunker } from "./chunker";
65
66
  import {
@@ -1074,6 +1075,22 @@ export class SyncService {
1074
1075
  tagCount: extractedTags.length,
1075
1076
  });
1076
1077
 
1078
+ // 13b. Index managed-memory scopes, memory-managed collections only.
1079
+ // A record that passes the memory validator gets scope rows; a
1080
+ // malformed file clears them and therefore drops out of managed
1081
+ // recall while staying searchable.
1082
+ if (collection.memoryManaged === true) {
1083
+ const memoryScopes = extractMemoryScopes(artifact.markdown);
1084
+ const scopesResult = await store.setDocMemoryScopes(
1085
+ docId,
1086
+ memoryScopes
1087
+ );
1088
+ mustOk(scopesResult, "setDocMemoryScopes", {
1089
+ docId,
1090
+ scopeCount: memoryScopes.length,
1091
+ });
1092
+ }
1093
+
1077
1094
  // 14. Extract and store links (wiki and markdown links)
1078
1095
  const excludedRanges = getExcludedRanges(artifact.markdown);
1079
1096
  const lineOffsets = buildLineOffsets(artifact.markdown);
@@ -51,6 +51,8 @@ export const MCP_HTTP_EGRESS_TOOLS = {
51
51
  gno_multi_get: "source",
52
52
  gno_query: "snippet",
53
53
  gno_query_diagnose: "metadata",
54
+ gno_recall: "source",
55
+ gno_remember: "source",
54
56
  gno_remove_collection: "metadata",
55
57
  gno_rename_note: "metadata",
56
58
  gno_search: "snippet",
@@ -64,6 +64,16 @@ import {
64
64
  } from "./links";
65
65
  import { handleListJobs } from "./list-jobs";
66
66
  import { handleListTags } from "./list-tags";
67
+ import {
68
+ handleRecall,
69
+ RECALL_MCP_ANNOTATIONS,
70
+ recallInputSchema,
71
+ } from "./memory-recall";
72
+ import {
73
+ handleRemember,
74
+ REMEMBER_MCP_ANNOTATIONS,
75
+ rememberInputSchema,
76
+ } from "./memory-remember";
67
77
  import { handleMultiGet } from "./multi-get";
68
78
  import { handlePeek, PEEK_MCP_ANNOTATIONS } from "./peek";
69
79
  import { handleQuery, handleQueryDiagnose } from "./query";
@@ -140,11 +150,16 @@ export const MCP_TOOL_DESCRIPTIONS = {
140
150
  contextVerify:
141
151
  "Verify a saved Context Capsule without rebuilding or mutating it. Reports unchanged, stale, missing, reranked, and fingerprint drift states against the active index.",
142
152
  ask: "Generate one answer from a deterministic Context Capsule, verify every substantive claim against exact retained spans, and abstain unless support coverage is complete. Read-only; returns the Capsule, freshness receipt, claim verdicts, gaps, and evidence IDs.",
153
+ recall:
154
+ "Recall current facts from a memory-managed collection for explicit scopes. Call before answering about the user's preferences, decisions, people, or prior work, and before gno_remember to find the predecessor of a changed fact. Returns at most 8 facts within 512 tokens by default, each with text, scopes, provenance, gno:// cite, and content hash, plus a content-free receipt; superseded facts are excluded. Pass the receipt to gno_remember when a stored fact derives from this recall. An empty result names the command that stores the first fact.",
155
+ remember:
156
+ "Store one fact in a memory-managed collection under explicit scopes. Call when the user states a durable preference, decision, or fact worth recalling later; use gno_capture for documents and file edits for existing notes. Without decision it returns likely matches and writes nothing; decision=add writes a new fact; decision=supersede replaces predecessorUri after a hash check, one successor per fact. Exact duplicates return the existing record. Text that replays a recall receipt span or declares a gno:// origin is rejected. The fact is lexically searchable when the call returns.",
143
157
  } as const;
144
158
 
145
159
  /** Tool names whose execution mutates disk, config, or index state. */
146
160
  export const MCP_WRITE_TOOL_NAMES = new Set([
147
161
  "gno_capture",
162
+ "gno_remember",
148
163
  "gno_add_collection",
149
164
  "gno_sync",
150
165
  "gno_embed",
@@ -1037,6 +1052,20 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
1037
1052
  (args) => handleAsk(args, ctx)
1038
1053
  );
1039
1054
 
1055
+ server.registerTool(
1056
+ "gno_recall",
1057
+ {
1058
+ description: MCP_TOOL_DESCRIPTIONS.recall,
1059
+ inputSchema: recallInputSchema,
1060
+ annotations: RECALL_MCP_ANNOTATIONS,
1061
+ },
1062
+ (args, extra) =>
1063
+ handleRecall(args, ctx, {
1064
+ clientName: server.server.getClientVersion()?.name,
1065
+ sessionId: extra.sessionId,
1066
+ })
1067
+ );
1068
+
1040
1069
  server.tool(
1041
1070
  "gno_search",
1042
1071
  MCP_TOOL_DESCRIPTIONS.search,
@@ -1315,6 +1344,20 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
1315
1344
  (args) => handleTracePurge(args, ctx)
1316
1345
  );
1317
1346
 
1347
+ server.registerTool(
1348
+ "gno_remember",
1349
+ {
1350
+ description: MCP_TOOL_DESCRIPTIONS.remember,
1351
+ inputSchema: rememberInputSchema,
1352
+ annotations: REMEMBER_MCP_ANNOTATIONS,
1353
+ },
1354
+ (args, extra) =>
1355
+ handleRemember(args, ctx, {
1356
+ clientName: server.server.getClientVersion()?.name,
1357
+ sessionId: extra.sessionId,
1358
+ })
1359
+ );
1360
+
1318
1361
  server.tool(
1319
1362
  "gno_capture",
1320
1363
  "Create a new document in a collection. Writes to disk. Does NOT auto-embed; run gno_index after to make it searchable via vector search.",
@@ -0,0 +1,122 @@
1
+ /**
2
+ * MCP gno_recall tool - budgeted, cited, current-state memory recall.
3
+ *
4
+ * Thin adapter over the core `MemoryService`. Identity is mapped from the
5
+ * server session (MCP client name + transport session), never from tool
6
+ * arguments; see `memory-shared` for the mapping and service construction.
7
+ *
8
+ * @module src/mcp/tools/memory-recall
9
+ */
10
+
11
+ import { z } from "zod";
12
+
13
+ import type { ToolContext } from "../server";
14
+
15
+ import {
16
+ MEMORY_RECALL_MAX_FACTS,
17
+ MEMORY_RECALL_MAX_TOKENS,
18
+ type RecallResult,
19
+ } from "../../core/memory";
20
+ import { runTool, type ToolResult } from "./index";
21
+ import {
22
+ createMcpMemoryService,
23
+ type McpMemorySessionInfo,
24
+ memoryScopesInputSchema,
25
+ resolveMcpMemoryIdentity,
26
+ rethrowMemoryError,
27
+ } from "./memory-shared";
28
+
29
+ export const RECALL_MCP_ANNOTATIONS = {
30
+ readOnlyHint: true,
31
+ destructiveHint: false,
32
+ idempotentHint: true,
33
+ openWorldHint: false,
34
+ } as const;
35
+
36
+ export const recallInputSchema = z.object({
37
+ query: z
38
+ .string()
39
+ .trim()
40
+ .min(1, "Query cannot be empty")
41
+ .describe("What you need to know, phrased as the fact would be stated"),
42
+ collection: z
43
+ .string()
44
+ .trim()
45
+ .min(1, "Collection cannot be empty")
46
+ .describe("Memory-managed collection to recall from"),
47
+ scopes: memoryScopesInputSchema,
48
+ maxFacts: z
49
+ .number()
50
+ .int()
51
+ .min(1)
52
+ .max(64)
53
+ .optional()
54
+ .describe(`Fact budget (default ${MEMORY_RECALL_MAX_FACTS})`),
55
+ maxTokens: z
56
+ .number()
57
+ .int()
58
+ .min(1)
59
+ .max(8192)
60
+ .optional()
61
+ .describe(`Payload token budget (default ${MEMORY_RECALL_MAX_TOKENS})`),
62
+ });
63
+
64
+ export type RecallToolInput = z.infer<typeof recallInputSchema>;
65
+
66
+ export function formatRecallResult(result: RecallResult): string {
67
+ const lines: string[] = [];
68
+ lines.push(
69
+ `Facts: ${result.facts.length} (budget ${result.budget.maxFacts} facts / ${result.budget.maxTokens} tokens, used ${result.budget.usedTokens}, omitted ${result.budget.omitted})`
70
+ );
71
+ lines.push(
72
+ `Retrieval: ${result.retrieval.mode}${
73
+ result.retrieval.semanticUnavailable
74
+ ? ` (${result.retrieval.semanticUnavailable})`
75
+ : ""
76
+ }`
77
+ );
78
+ for (const fact of result.facts) {
79
+ lines.push("");
80
+ lines.push(`- ${fact.text}`);
81
+ lines.push(` cite: ${fact.uri}`);
82
+ lines.push(
83
+ ` scopes: ${fact.scopes.join(", ")} | hash: ${fact.contentHash} | created: ${fact.createdAt}`
84
+ );
85
+ }
86
+ if (result.hint) {
87
+ lines.push("");
88
+ lines.push(result.hint);
89
+ }
90
+ lines.push("");
91
+ lines.push(
92
+ `Receipt: ${result.receipt.digest} (caller ${result.receipt.caller}, session ${result.receipt.session}, ${result.receipt.memoryIds.length} ids)`
93
+ );
94
+ return lines.join("\n");
95
+ }
96
+
97
+ export function handleRecall(
98
+ args: RecallToolInput,
99
+ ctx: ToolContext,
100
+ info: McpMemorySessionInfo = {}
101
+ ): Promise<ToolResult> {
102
+ return runTool(
103
+ ctx,
104
+ "gno_recall",
105
+ async () => {
106
+ const service = createMcpMemoryService(ctx);
107
+ try {
108
+ return await service.recall({
109
+ ...resolveMcpMemoryIdentity(ctx, info),
110
+ query: args.query,
111
+ collection: args.collection,
112
+ scopes: args.scopes,
113
+ maxFacts: args.maxFacts,
114
+ maxTokens: args.maxTokens,
115
+ });
116
+ } catch (error) {
117
+ return rethrowMemoryError(error);
118
+ }
119
+ },
120
+ formatRecallResult
121
+ );
122
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * MCP gno_remember tool - store one fact with supersession semantics.
3
+ *
4
+ * Thin adapter over the core `MemoryService`, registered only with
5
+ * `--enable-write`. The service acquires the shared write lease itself; this
6
+ * module performs no lock acquisition.
7
+ *
8
+ * @module src/mcp/tools/memory-remember
9
+ */
10
+
11
+ import { z } from "zod";
12
+
13
+ import type { ToolContext } from "../server";
14
+
15
+ import { type RememberResult } from "../../core/memory";
16
+ import { MEMORY_MAX_FACT_BYTES } from "../../core/memory-record";
17
+ import { runTool, type ToolResult } from "./index";
18
+ import {
19
+ createMcpMemoryService,
20
+ type McpMemorySessionInfo,
21
+ memoryScopesInputSchema,
22
+ resolveMcpMemoryIdentity,
23
+ rethrowMemoryError,
24
+ } from "./memory-shared";
25
+
26
+ export const REMEMBER_MCP_ANNOTATIONS = {
27
+ readOnlyHint: false,
28
+ destructiveHint: false,
29
+ idempotentHint: false,
30
+ openWorldHint: false,
31
+ } as const;
32
+
33
+ export const memoryReceiptInputSchema = z
34
+ .object({
35
+ caller: z.string(),
36
+ session: z.string(),
37
+ issuedAt: z.string(),
38
+ memoryIds: z.array(z.string()),
39
+ spanHashes: z.array(z.string()),
40
+ digest: z.string(),
41
+ })
42
+ .describe(
43
+ "The receipt from the gno_recall response the fact was derived from, if any; replayed recalled spans are rejected"
44
+ );
45
+
46
+ export const rememberInputSchema = z.object({
47
+ text: z
48
+ .string()
49
+ .trim()
50
+ .min(1, "Fact text cannot be empty")
51
+ .max(MEMORY_MAX_FACT_BYTES)
52
+ .describe(
53
+ "One fact, stated in full (single statement, not a document; use gno_capture for documents)"
54
+ ),
55
+ collection: z
56
+ .string()
57
+ .trim()
58
+ .min(1, "Collection cannot be empty")
59
+ .describe("Memory-managed collection to write into"),
60
+ scopes: memoryScopesInputSchema,
61
+ decision: z
62
+ .enum(["add", "supersede"])
63
+ .optional()
64
+ .describe(
65
+ "Omit to receive candidates without writing; add creates a new fact; supersede replaces predecessorUri"
66
+ ),
67
+ predecessorUri: z
68
+ .string()
69
+ .trim()
70
+ .min(1)
71
+ .optional()
72
+ .describe("gno:// URI of the fact being superseded (supersede only)"),
73
+ predecessorHash: z
74
+ .string()
75
+ .trim()
76
+ .min(1)
77
+ .optional()
78
+ .describe(
79
+ "contentHash of the predecessor as returned by gno_recall (supersede only)"
80
+ ),
81
+ receipt: memoryReceiptInputSchema.optional(),
82
+ derivedFrom: z
83
+ .array(z.string().trim().min(1))
84
+ .max(32)
85
+ .optional()
86
+ .describe(
87
+ "Declared origins of the fact; any gno:// origin is rejected as GNO-derived"
88
+ ),
89
+ source: z
90
+ .string()
91
+ .trim()
92
+ .min(1)
93
+ .optional()
94
+ .describe("Free-text evidence for the fact (where it came from)"),
95
+ });
96
+
97
+ export type RememberToolInput = z.infer<typeof rememberInputSchema>;
98
+
99
+ export function formatRememberResult(result: RememberResult): string {
100
+ const lines: string[] = [];
101
+ const matching = `matching: ${result.matching.mode} (threshold ${result.matching.threshold}${
102
+ result.matching.semanticUnavailable
103
+ ? `, ${result.matching.semanticUnavailable}`
104
+ : ""
105
+ })`;
106
+ switch (result.outcome) {
107
+ case "existing":
108
+ lines.push(`Outcome: existing (exact duplicate, nothing written)`);
109
+ lines.push(`URI: ${result.record.uri}`);
110
+ lines.push(`Hash: ${result.record.contentHash}`);
111
+ break;
112
+ case "candidates":
113
+ lines.push(
114
+ `Outcome: candidates (${result.candidates.length}, nothing written). Decide: decision=add for a new fact, or decision=supersede with predecessorUri + predecessorHash.`
115
+ );
116
+ for (const candidate of result.candidates) {
117
+ lines.push(
118
+ `- [${candidate.match} ${candidate.similarity.toFixed(2)}] ${candidate.text}`
119
+ );
120
+ lines.push(` uri: ${candidate.uri} | hash: ${candidate.contentHash}`);
121
+ }
122
+ break;
123
+ default:
124
+ lines.push(`Outcome: ${result.outcome}`);
125
+ lines.push(`URI: ${result.record.uri}`);
126
+ lines.push(`Hash: ${result.record.contentHash}`);
127
+ lines.push(`Path: ${result.absPath}`);
128
+ lines.push(`Sync: ${result.sync.status}`);
129
+ if (result.record.supersedes.length > 0) {
130
+ lines.push(`Supersedes: ${result.record.supersedes.join(", ")}`);
131
+ }
132
+ break;
133
+ }
134
+ lines.push(matching);
135
+ return lines.join("\n");
136
+ }
137
+
138
+ export function handleRemember(
139
+ args: RememberToolInput,
140
+ ctx: ToolContext,
141
+ info: McpMemorySessionInfo = {}
142
+ ): Promise<ToolResult> {
143
+ return runTool(
144
+ ctx,
145
+ "gno_remember",
146
+ async () => {
147
+ if (!ctx.enableWrite) {
148
+ throw new Error(
149
+ "WRITE_DISABLED: gno_remember requires --enable-write or GNO_MCP_ENABLE_WRITE=1"
150
+ );
151
+ }
152
+ const service = createMcpMemoryService(ctx);
153
+ let result: RememberResult;
154
+ try {
155
+ result = await service.remember({
156
+ ...resolveMcpMemoryIdentity(ctx, info),
157
+ text: args.text,
158
+ collection: args.collection,
159
+ scopes: args.scopes,
160
+ decision: args.decision,
161
+ predecessorUri: args.predecessorUri,
162
+ predecessorHash: args.predecessorHash,
163
+ receipt: args.receipt,
164
+ derivedFrom: args.derivedFrom,
165
+ source: args.source,
166
+ });
167
+ } catch (error) {
168
+ return rethrowMemoryError(error);
169
+ }
170
+ if (result.outcome === "added" || result.outcome === "superseded") {
171
+ ctx.markContentMutation?.();
172
+ }
173
+ return result;
174
+ },
175
+ formatRememberResult
176
+ );
177
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Helpers shared by the MCP gno_recall / gno_remember tools: identity
3
+ * mapping from the server session, service construction, error rethrow in
4
+ * the shape runTool parses, and the common scopes input schema.
5
+ *
6
+ * The service owns the shared write lease; this module never touches
7
+ * `ctx.writeLockPath` beyond naming the lease file for the service.
8
+ *
9
+ * @module src/mcp/tools/memory-shared
10
+ */
11
+
12
+ import { z } from "zod";
13
+
14
+ import type { ToolContext } from "../server";
15
+
16
+ import {
17
+ MemoryError,
18
+ type MemoryIdentity,
19
+ MemoryService,
20
+ } from "../../core/memory";
21
+ import { MEMORY_MAX_SCOPES } from "../../core/memory-record";
22
+
23
+ /** Caller name when the MCP client sent no implementation name. */
24
+ const DEFAULT_MCP_CALLER = "mcp";
25
+
26
+ /** Server-side identity inputs resolved at dispatch time by the registry. */
27
+ export interface McpMemorySessionInfo {
28
+ /** `clientInfo.name` from the MCP initialize handshake. */
29
+ clientName?: string;
30
+ /** Transport session id (Streamable HTTP); absent on stdio. */
31
+ sessionId?: string;
32
+ }
33
+
34
+ export const memoryScopesInputSchema = z
35
+ .array(z.string().trim().min(1))
36
+ .min(1, "At least one explicit scope is required")
37
+ .max(MEMORY_MAX_SCOPES)
38
+ .describe(
39
+ `Explicit scopes (1-${MEMORY_MAX_SCOPES}, e.g. "project:gno"). Visibility is any-intersection; there is no implicit global scope`
40
+ );
41
+
42
+ /**
43
+ * Map the MCP server session to the core identity contract.
44
+ *
45
+ * caller = MCP client implementation name; session = transport session id
46
+ * when the transport has one (Streamable HTTP), else the per-process server
47
+ * instance id (stdio: one process is one session).
48
+ */
49
+ export function resolveMcpMemoryIdentity(
50
+ ctx: ToolContext,
51
+ info: McpMemorySessionInfo
52
+ ): MemoryIdentity {
53
+ const caller = info.clientName?.trim() || DEFAULT_MCP_CALLER;
54
+ const session = info.sessionId?.trim() || ctx.serverInstanceId;
55
+ return { caller, session };
56
+ }
57
+
58
+ /**
59
+ * Construct the core service for one MCP call.
60
+ *
61
+ * The lease path names the same `.mcp-write.lock` file the rest of the MCP
62
+ * write surface uses, so a memory write and a capture serialise on one lease.
63
+ * Acquisition happens inside the service only.
64
+ */
65
+ export function createMcpMemoryService(ctx: ToolContext): MemoryService {
66
+ return new MemoryService({
67
+ store: ctx.store,
68
+ config: ctx.config,
69
+ collections: ctx.collections,
70
+ lockPath: ctx.writeLockPath,
71
+ });
72
+ }
73
+
74
+ /** Re-throw a core memory error in the `CODE: message` shape runTool parses. */
75
+ export function rethrowMemoryError(error: unknown): never {
76
+ if (error instanceof MemoryError) {
77
+ throw new Error(`${error.code}: ${error.message}`);
78
+ }
79
+ throw error;
80
+ }
@@ -211,6 +211,8 @@ export async function searchBm25(
211
211
  until: temporalRange.until,
212
212
  categories: options.categories,
213
213
  author: options.author,
214
+ memoryScopesAny: options.memoryFilter?.scopes,
215
+ excludeSuperseded: options.memoryFilter?.excludeSuperseded,
214
216
  });
215
217
 
216
218
  if (!ftsResult.ok) {
@@ -212,6 +212,14 @@ export interface SearchOptions {
212
212
  intent?: string;
213
213
  /** Explicit exclusion terms for hard candidate pruning */
214
214
  exclude?: string[];
215
+ /**
216
+ * Internal managed-memory filter: scope any-intersection plus superseded
217
+ * exclusion, executed inside the retrieval query (never post-hoc).
218
+ */
219
+ memoryFilter?: {
220
+ scopes: string[];
221
+ excludeSuperseded: boolean;
222
+ };
215
223
  }
216
224
 
217
225
  /** Structured query mode identifier */