@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.
- package/README.md +1 -0
- package/assets/skill/SKILL.md +17 -0
- package/assets/skill/cli-reference.md +48 -0
- package/assets/skill/mcp-reference.md +24 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.40.0.zip → gno-browser-clipper-v1.41.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.41.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +146 -7
- package/spec/db/schema.sql +17 -0
- package/spec/mcp.md +194 -0
- package/spec/output-schemas/memory-recall.schema.json +159 -0
- package/spec/output-schemas/memory-remember.schema.json +164 -0
- package/spec/output-schemas/status.schema.json +269 -54
- package/src/cli/commands/memory.ts +491 -0
- package/src/cli/commands/status.ts +23 -4
- package/src/cli/options.ts +4 -0
- package/src/cli/program.ts +127 -0
- package/src/config/types.ts +7 -0
- package/src/core/audit-provenance.ts +91 -0
- package/src/core/audit-workspace.ts +17 -0
- package/src/core/memory-diagnostics.ts +144 -0
- package/src/core/memory-fence.ts +239 -0
- package/src/core/memory-recall.ts +269 -0
- package/src/core/memory-record.ts +435 -0
- package/src/core/memory-remember.ts +425 -0
- package/src/core/memory-types.ts +211 -0
- package/src/core/memory.ts +87 -0
- package/src/ingestion/sync.ts +17 -0
- package/src/mcp/http-egress.ts +2 -0
- package/src/mcp/tools/index.ts +43 -0
- package/src/mcp/tools/memory-recall.ts +122 -0
- package/src/mcp/tools/memory-remember.ts +177 -0
- package/src/mcp/tools/memory-shared.ts +80 -0
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +8 -0
- package/src/sdk/client.ts +94 -1
- package/src/sdk/index.ts +13 -0
- package/src/sdk/types.ts +28 -0
- package/src/serve/routes/api.ts +167 -0
- package/src/serve/server.ts +26 -0
- package/src/store/migrations/027-memory-scopes.ts +37 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +127 -3
- package/src/store/types.ts +54 -0
- 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
|
+
}
|
package/src/ingestion/sync.ts
CHANGED
|
@@ -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);
|
package/src/mcp/http-egress.ts
CHANGED
|
@@ -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",
|
package/src/mcp/tools/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/pipeline/search.ts
CHANGED
|
@@ -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) {
|
package/src/pipeline/types.ts
CHANGED
|
@@ -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 */
|