@cairnkeep/cli 2.3.1 → 2.5.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 (151) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +232 -6
  3. package/bin/cairn +43 -2
  4. package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
  5. package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
  6. package/claude/hooks/compaction-capture.sh +34 -0
  7. package/claude/hooks/memory-capture.sh +47 -5
  8. package/claude/hooks/memory-wakeup.sh +55 -2
  9. package/docs/building-an-overlay.md +4 -3
  10. package/docs/design/meta-agent.md +439 -0
  11. package/docs/harness-compatibility.md +117 -0
  12. package/docs/learning/CURRICULUM-MAP.md +91 -0
  13. package/docs/learning/FEATURE-GUIDE.md +86 -0
  14. package/docs/learning/PRODUCTION-PLAN.md +45 -14
  15. package/docs/learning/README.md +20 -1
  16. package/docs/learning/instructor-guide.md +6 -0
  17. package/docs/learning/lessons/L00-why-cairnkeep.md +20 -8
  18. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  19. package/docs/learning/lessons/L02-installation.md +23 -5
  20. package/docs/learning/lessons/L03-first-project.md +7 -2
  21. package/docs/learning/lessons/L04-memory-fundamentals.md +4 -0
  22. package/docs/learning/lessons/L05-daily-workflow.md +4 -0
  23. package/docs/learning/lessons/L07-privacy-and-storage.md +3 -0
  24. package/docs/learning/lessons/L10-context-exploration.md +4 -2
  25. package/docs/learning/lessons/L12-managed-overlays.md +2 -0
  26. package/docs/learning/lessons/L13-session-evidence.md +71 -0
  27. package/docs/learning/lessons/L14-typed-memory.md +51 -0
  28. package/docs/learning/lessons/L15-capability-governance.md +56 -0
  29. package/docs/learning/lessons/L16-evaluation.md +67 -0
  30. package/docs/learning/lessons/L17-agent-boundary.md +55 -0
  31. package/docs/learning/templates/lesson-template.md +4 -1
  32. package/docs/learning/tracks/evidence-and-evaluation.md +20 -0
  33. package/docs/learning/tracks/operator.md +8 -2
  34. package/docs/learning/tracks/practitioner.md +6 -1
  35. package/docs/learning/tracks/quickstart.md +3 -0
  36. package/docs/learning/video-scripts/L00-why-cairnkeep.md +21 -17
  37. package/docs/learning/video-scripts/L02-installation.md +9 -2
  38. package/docs/learning/video-scripts/L03-first-project.md +3 -0
  39. package/docs/operating.md +567 -7
  40. package/docs/privacy-and-data-flow.md +417 -0
  41. package/docs/storage.md +252 -0
  42. package/examples/eval/README.md +42 -0
  43. package/examples/eval/adapter.json +12 -0
  44. package/examples/eval/bundled-fake.json +1 -0
  45. package/examples/eval/task-set.json +1 -0
  46. package/mcp-memory-server/dist/artifact-cli.js +184 -0
  47. package/mcp-memory-server/dist/artifact-schema.js +218 -0
  48. package/mcp-memory-server/dist/artifact-store.js +812 -0
  49. package/mcp-memory-server/dist/capability-adapter.js +286 -0
  50. package/mcp-memory-server/dist/capability-cli.js +361 -0
  51. package/mcp-memory-server/dist/capability-config.js +310 -0
  52. package/mcp-memory-server/dist/capability-harness.js +496 -0
  53. package/mcp-memory-server/dist/capability-registry.js +123 -0
  54. package/mcp-memory-server/dist/capability-schema.js +65 -0
  55. package/mcp-memory-server/dist/capability-store.js +431 -0
  56. package/mcp-memory-server/dist/compaction-normalize.js +267 -0
  57. package/mcp-memory-server/dist/eval-cli.js +523 -0
  58. package/mcp-memory-server/dist/eval-plan.js +346 -0
  59. package/mcp-memory-server/dist/eval-process.js +273 -0
  60. package/mcp-memory-server/dist/eval-report.js +609 -0
  61. package/mcp-memory-server/dist/eval-runner.js +827 -0
  62. package/mcp-memory-server/dist/eval-schema.js +368 -0
  63. package/mcp-memory-server/dist/eval-statistics.js +161 -0
  64. package/mcp-memory-server/dist/eval-workspace.js +313 -0
  65. package/mcp-memory-server/dist/failure-signature.js +141 -0
  66. package/mcp-memory-server/dist/index.js +600 -55
  67. package/mcp-memory-server/dist/node-cli.js +84 -0
  68. package/mcp-memory-server/dist/node-schema.js +170 -0
  69. package/mcp-memory-server/dist/node-store.js +455 -0
  70. package/mcp-memory-server/dist/note-cli.js +235 -0
  71. package/mcp-memory-server/dist/note-distiller.js +177 -0
  72. package/mcp-memory-server/dist/note-enrichment.js +93 -0
  73. package/mcp-memory-server/dist/note-schema.js +4 -0
  74. package/mcp-memory-server/dist/note-store.js +985 -0
  75. package/mcp-memory-server/dist/trajectory-cli.js +141 -0
  76. package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
  77. package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
  78. package/mcp-memory-server/dist/trajectory-schema.js +73 -0
  79. package/mcp-memory-server/dist/trajectory-store.js +339 -0
  80. package/mcp-memory-server/package.json +24 -2
  81. package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
  82. package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
  83. package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
  84. package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
  85. package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
  86. package/opencode/capability-contract/plugins/capability-command.ts +319 -0
  87. package/opencode/command/graphify.md +25 -14
  88. package/opencode/plugins/memory-capture.ts +76 -10
  89. package/opencode/plugins/memory-wakeup.ts +58 -6
  90. package/package.json +8 -4
  91. package/pi/extensions/cairnkeep-trajectory.ts +55 -0
  92. package/schemas/artifact.schema.json +133 -0
  93. package/schemas/capability-callback.schema.json +68 -0
  94. package/schemas/capability-contract.schema.json +126 -0
  95. package/schemas/eval-adapter.schema.json +43 -0
  96. package/schemas/eval-protocol.schema.json +114 -0
  97. package/schemas/eval-report.schema.json +423 -0
  98. package/schemas/eval-task-set.schema.json +114 -0
  99. package/schemas/memory-node.schema.json +90 -0
  100. package/schemas/note.schema.json +112 -0
  101. package/schemas/trajectory-redaction.schema.json +25 -0
  102. package/schemas/trajectory.schema.json +82 -0
  103. package/scripts/bootstrap.sh +6 -0
  104. package/scripts/completion.sh +119 -5
  105. package/scripts/doctor.sh +284 -0
  106. package/scripts/fake-eval-adapter.mjs +120 -0
  107. package/scripts/fixtures/capability-harness-contracts.json +408 -0
  108. package/scripts/install-audit-timer.sh +3 -1
  109. package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
  110. package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
  111. package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
  112. package/scripts/memory-wiki-audit.sh +49 -0
  113. package/scripts/smoke-bash32.sh +9 -0
  114. package/scripts/sync-claude-assets.sh +99 -6
  115. package/scripts/sync-opencode-graphify-assets.sh +24 -3
  116. package/scripts/sync-opencode-plugin-assets.sh +31 -5
  117. package/scripts/sync-opencode-security-assets.sh +24 -3
  118. package/scripts/sync-opencode-wiki-assets.sh +24 -3
  119. package/scripts/sync-pi-assets.sh +72 -0
  120. package/scripts/test-audit-timer.sh +98 -0
  121. package/scripts/test-bootstrap-untracked.sh +49 -0
  122. package/scripts/test-cli-dispatch.sh +41 -2
  123. package/scripts/test-compaction-hooks.sh +495 -0
  124. package/scripts/test-completion.sh +77 -0
  125. package/scripts/test-doctor.sh +348 -1
  126. package/scripts/test-eval-cli.sh +175 -0
  127. package/scripts/test-launcher-hooks.sh +630 -3
  128. package/scripts/test-learning-docs.sh +20 -4
  129. package/scripts/test-notes-cli.sh +114 -0
  130. package/scripts/test-package-install.sh +254 -0
  131. package/scripts/test-phase16-node-lifecycle.sh +50 -0
  132. package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
  133. package/scripts/test-phase18-capability-lifecycle.sh +504 -0
  134. package/scripts/test-phase18-harness-boundary.sh +1060 -0
  135. package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
  136. package/scripts/test-trajectory-cli.sh +87 -0
  137. package/scripts/test-trajectory-hooks.sh +142 -0
  138. package/scripts/test-uninstall.sh +231 -5
  139. package/scripts/uninstall.sh +38 -5
  140. package/scripts/verify-dependency-security.mjs +121 -0
  141. package/scripts/verify-docs-parity.sh +527 -1
  142. package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
  143. package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
  144. package/templates/capabilities.json.template +7 -0
  145. package/templates/env.example.template +51 -0
  146. package/templates/memory-wiki-audit.service.template +1 -1
  147. package/templates/start-claude.sh.template +36 -0
  148. package/templates/start-kimi.sh.template +39 -0
  149. package/templates/start-opencode.sh.template +47 -5
  150. package/templates/start-pi.sh.template +30 -0
  151. package/templates/trajectory-redaction.json.template +4 -0
@@ -11,8 +11,12 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
12
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
13
13
  import { z } from "zod";
14
+ import { artifactIdSchema, artifactKindSchema, artifactMediaTypeSchema, artifactNodeRefSchema, artifactProvenanceSchema, artifactSessionRefSchema, compactionSummaryInputContentSchema, diffContentSchema, generatedFileInputContentSchema, isArtifactHttpEnabled, isArtifactStoreEnabled, testOutputContentSchema, } from "./artifact-schema.js";
15
+ import { isCapabilityContractEnabled, resolveCapabilityStatus, } from "./capability-config.js";
14
16
  import { EmbeddingCache, cosineSimilarity, embedTexts, getEmbeddingConfig, hashText, } from "./embeddings.js";
15
17
  import { computeRepoState, exploreCacheKey, normalizeExploreQuery, readExploreCache, writeExploreCache, } from "./explore-cache.js";
18
+ import { canonicalTagsSchema, isTypedMemoryNodesEnabled, memoryImportEnvelopeSchema, noteNodeSchema, nodeTypeSchema, } from "./node-schema.js";
19
+ import { attachNodeMetadata, applyReviewedTypedNode, commitMemoryImport, createTypedNode, deleteTypedNode, getTypedNode, invalidateReviewedTypedNode, listTypedHistory, planMemoryImport, supersedeTypedNode, } from "./node-store.js";
16
20
  const MINIMUM_NODE_MAJOR = 22;
17
21
  const nodeMajor = Number.parseInt(process.versions.node, 10);
18
22
  if (!Number.isInteger(nodeMajor) || nodeMajor < MINIMUM_NODE_MAJOR) {
@@ -33,6 +37,24 @@ const REVIEWED_NAMESPACE = "__reviewed__";
33
37
  const REVIEW_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
34
38
  const PROJECT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
35
39
  const WORKSPACE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
40
+ const NODE_ADDRESS_SPACE_SCHEMA = z.enum(["memory", "project-notes", "shared-notes"]);
41
+ const scopeMutationTails = new Map();
42
+ async function serializeScopeMutation(scopePath, operation) {
43
+ const previous = scopeMutationTails.get(scopePath) ?? Promise.resolve();
44
+ let release = () => { };
45
+ const turn = new Promise((resolveTurn) => { release = resolveTurn; });
46
+ const tail = previous.then(() => turn);
47
+ scopeMutationTails.set(scopePath, tail);
48
+ await previous;
49
+ try {
50
+ return await operation();
51
+ }
52
+ finally {
53
+ release();
54
+ if (scopeMutationTails.get(scopePath) === tail)
55
+ scopeMutationTails.delete(scopePath);
56
+ }
57
+ }
36
58
  function expandHome(value) {
37
59
  if (value === "~") {
38
60
  return homedir();
@@ -127,6 +149,7 @@ function parseServerContext(headers) {
127
149
  return {
128
150
  projectId: rawProjectId || undefined,
129
151
  memoryConfig,
152
+ remote: true,
130
153
  };
131
154
  }
132
155
  function resolveScopePath(scope, options = {}) {
@@ -236,11 +259,17 @@ async function listEntries(scope, prefix = "", options = {}) {
236
259
  }
237
260
  try {
238
261
  const entries = await agent.kv.list(prefix);
239
- return visibleEntries(entries.map(({ key, value }) => ({
262
+ const visible = visibleEntries(entries.map(({ key, value }) => ({
240
263
  scope,
241
264
  key,
242
265
  value: normalizeValue(value),
243
266
  })), options.includeHistory ?? false);
267
+ if (!options.typed)
268
+ return visible;
269
+ const typed = [];
270
+ for (const entry of visible)
271
+ typed.push(await attachNodeMetadata(agent, scope, entry));
272
+ return typed;
244
273
  }
245
274
  finally {
246
275
  await agent.close();
@@ -256,7 +285,8 @@ async function readKey(scope, key, options = {}) {
256
285
  if (value === undefined) {
257
286
  return [];
258
287
  }
259
- return [{ scope, key, value: normalizeValue(value) }];
288
+ const entry = { scope, key, value: normalizeValue(value) };
289
+ return [options.typed ? await attachNodeMetadata(agent, scope, entry) : entry];
260
290
  }
261
291
  finally {
262
292
  await agent.close();
@@ -264,10 +294,39 @@ async function readKey(scope, key, options = {}) {
264
294
  }
265
295
  function searchEntries(entries, query) {
266
296
  const needle = query.toLowerCase();
267
- return entries.filter(({ key, value }) => {
268
- return key.toLowerCase().includes(needle) || value.toLowerCase().includes(needle);
297
+ return entries.filter(({ key, value, node_type, tags }) => key.toLowerCase().includes(needle)
298
+ || value.toLowerCase().includes(needle)
299
+ || node_type?.toLowerCase().includes(needle)
300
+ || tags?.some((tag) => tag.includes(needle)));
301
+ }
302
+ function filterEntries(entries, filters) {
303
+ return entries.filter((entry) => {
304
+ const tags = entry.tags ?? [];
305
+ if (filters.node_types && !filters.node_types.includes(entry.node_type ?? "memory"))
306
+ return false;
307
+ if (filters.tags_all && !filters.tags_all.every((tag) => tags.includes(tag)))
308
+ return false;
309
+ if (filters.tags_any && !filters.tags_any.some((tag) => tags.includes(tag)))
310
+ return false;
311
+ return true;
269
312
  });
270
313
  }
314
+ function memoryFiltersFromInput(input) {
315
+ return {
316
+ node_types: input.node_types === undefined ? undefined : z.array(nodeTypeSchema).min(1).parse(input.node_types),
317
+ tags_all: input.tags_all === undefined ? undefined : canonicalTagsSchema.pipe(z.array(z.string()).min(1)).parse(input.tags_all),
318
+ tags_any: input.tags_any === undefined ? undefined : canonicalTagsSchema.pipe(z.array(z.string()).min(1)).parse(input.tags_any),
319
+ };
320
+ }
321
+ function compareMemoryEntries(left, right) {
322
+ return left.scope.localeCompare(right.scope) || left.key.localeCompare(right.key);
323
+ }
324
+ function isExactTextHit(entry, query) {
325
+ const needle = query.toLowerCase();
326
+ return entry.key.toLowerCase().includes(needle)
327
+ || entry.node_type?.toLowerCase() === needle
328
+ || Boolean(entry.tags?.includes(needle));
329
+ }
271
330
  function asToolText(value) {
272
331
  return JSON.stringify(value, null, 2);
273
332
  }
@@ -464,28 +523,32 @@ async function callAnythingLLM(workspace, query) {
464
523
  return JSON.stringify(payload, null, 2);
465
524
  }
466
525
  function entryText(entry) {
467
- return entry.value ? `${entry.key}\n${entry.value}` : entry.key;
526
+ const metadata = [entry.node_type, ...(entry.tags ?? [])].filter(Boolean).join("\n");
527
+ return [entry.key, entry.value, metadata].filter(Boolean).join("\n");
468
528
  }
469
529
  function embeddingCachePath(scope, projectId) {
470
530
  return join(getBaseDir(), ".embeddings", `${hashText(resolveScopePath(scope, { projectId }))}.json`);
471
531
  }
472
- async function semanticSearch(scope, query, topK, minScore, config = getMemoryConfig(), projectId) {
532
+ async function semanticSearch(scope, query, topK, minScore, config = getMemoryConfig(), projectId, options = {}) {
473
533
  const scopes = getSearchScopes(scope, config);
474
534
  const embeddingConfig = getEmbeddingConfig();
475
535
  const perScopeEntries = await Promise.all(scopes.map(async (candidate) => ({
476
536
  scope: candidate,
477
- entries: await listEntries(candidate, "", { projectId }),
537
+ entries: filterEntries(await listEntries(candidate, "", { projectId, typed: options.typed }), options.filters ?? {}),
478
538
  })));
479
539
  const allEntries = perScopeEntries.flatMap((group) => group.entries);
480
540
  if (allEntries.length === 0) {
481
541
  return { results: [], mode: embeddingConfig ? "semantic" : "substring" };
482
542
  }
483
- const substringFallback = () => ({
484
- results: searchEntries(allEntries, query)
485
- .map((entry) => ({ ...entry, score: 1 }))
486
- .slice(0, topK),
487
- mode: "substring",
488
- });
543
+ const substringFallback = () => {
544
+ const matches = searchEntries(allEntries, query);
545
+ if (options.typed)
546
+ matches.sort(compareMemoryEntries);
547
+ return {
548
+ results: matches.map((entry) => ({ ...entry, score: 1 })).slice(0, topK),
549
+ mode: "substring",
550
+ };
551
+ };
489
552
  if (!embeddingConfig) {
490
553
  return substringFallback();
491
554
  }
@@ -522,15 +585,21 @@ async function semanticSearch(scope, query, topK, minScore, config = getMemoryCo
522
585
  for (const cache of caches) {
523
586
  cache.save();
524
587
  }
525
- const ranked = allEntries
588
+ const exact = options.typed
589
+ ? allEntries.filter((entry) => isExactTextHit(entry, query)).sort(compareMemoryEntries).map((entry) => ({ ...entry, score: 1 }))
590
+ : [];
591
+ const semanticCandidates = options.typed
592
+ ? allEntries.filter((entry) => !isExactTextHit(entry, query))
593
+ : allEntries;
594
+ const rankedSemantic = semanticCandidates
526
595
  .map((entry) => {
527
596
  const vector = vectors.get(entry);
528
597
  const score = vector && queryVector ? cosineSimilarity(queryVector, vector) : 0;
529
598
  return { ...entry, score };
530
599
  })
531
600
  .filter((entry) => entry.score >= minScore)
532
- .sort((left, right) => right.score - left.score)
533
- .slice(0, topK);
601
+ .sort((left, right) => right.score - left.score || (options.typed ? compareMemoryEntries(left, right) : 0));
602
+ const ranked = [...exact, ...rankedSemantic].slice(0, topK);
534
603
  return { results: ranked, mode: "semantic", model: embeddingConfig.model };
535
604
  }
536
605
  catch {
@@ -753,10 +822,197 @@ function gitToplevel(cwd) {
753
822
  // only allows one connected transport per server). All instances share the
754
823
  // module-level helpers + AgentFS below. Enables a single long-lived process to
755
824
  // serve many concurrent clients within one trusted server-side storage domain.
756
- function createMemoryServer(context = {}) {
825
+ function buildMemoryServer(context, capabilitySnapshot, withCapability) {
757
826
  const server = new McpServer({ name: "cairn-memory", version: "0.1.0" });
827
+ const capabilityEnabled = (id) => capabilitySnapshot?.capabilities.find((state) => state.id === id)?.enabled ?? true;
828
+ const registerCapabilityTool = (id, name, config, callback) => {
829
+ if (!capabilityEnabled(id))
830
+ return;
831
+ const handler = capabilitySnapshot && withCapability
832
+ ? withCapability({
833
+ projectRoot: process.cwd(),
834
+ snapshot: capabilitySnapshot,
835
+ capabilityId: id,
836
+ classification: {
837
+ harness: "other",
838
+ source: "mcp",
839
+ transport: context.remote ? "http" : "stdio",
840
+ },
841
+ }, callback)
842
+ : callback;
843
+ server.registerTool(name, config, handler);
844
+ };
758
845
  const memoryConfig = () => context.memoryConfig ?? getMemoryConfig();
759
846
  const scopeOptions = { projectId: context.projectId };
847
+ const typedNodesEnabled = isTypedMemoryNodesEnabled();
848
+ const artifactToolsEnabled = isArtifactStoreEnabled() && (!context.remote || isArtifactHttpEnabled());
849
+ if (artifactToolsEnabled && context.remote && !context.projectId) {
850
+ throw new ClientContextError("Remote artifact access requires X-Cairn-Project.");
851
+ }
852
+ let fallbackArtifactSessionRef;
853
+ const resolveArtifactSessionRef = (sessionRef) => {
854
+ if (sessionRef)
855
+ return sessionRef;
856
+ fallbackArtifactSessionRef ??= `cairn:${randomUUID()}`;
857
+ return fallbackArtifactSessionRef;
858
+ };
859
+ const artifactProjectRoot = async () => {
860
+ if (!context.remote)
861
+ return process.cwd();
862
+ if (!context.projectId) {
863
+ throw new ClientContextError("Remote artifact access requires X-Cairn-Project.");
864
+ }
865
+ const { resolveRemoteArtifactProjectRoot } = await import("./artifact-store.js");
866
+ return resolveRemoteArtifactProjectRoot(getBaseDir(), context.projectId);
867
+ };
868
+ const noteTargetOptions = (scope, address_space) => {
869
+ if (scope !== "project")
870
+ throw new Error("INVALID_SCOPE: note address spaces require scope project.");
871
+ if (context.remote && address_space === "project-notes" && !context.projectId) {
872
+ throw new Error("INVALID_SCOPE: remote project notes require X-Cairn-Project.");
873
+ }
874
+ return {
875
+ address_space,
876
+ ...(context.projectId ? { projectId: context.projectId } : { projectRoot: process.cwd() }),
877
+ };
878
+ };
879
+ if (artifactToolsEnabled) {
880
+ const artifactIdentifierSchema = z.string().min(4).max(40).regex(/^art_[0-9a-f-]*$/i);
881
+ const artifactWriteToolSchema = z.object({
882
+ kind: artifactKindSchema,
883
+ session_ref: artifactSessionRefSchema.optional(),
884
+ node_ref: artifactNodeRefSchema.optional(),
885
+ media_type: artifactMediaTypeSchema,
886
+ provenance: artifactProvenanceSchema,
887
+ supersedes: artifactIdSchema.optional(),
888
+ content: z.union([
889
+ compactionSummaryInputContentSchema,
890
+ diffContentSchema,
891
+ testOutputContentSchema,
892
+ generatedFileInputContentSchema,
893
+ ]),
894
+ }).strict();
895
+ server.registerTool("artifact_write", {
896
+ description: "Write one bounded inline artifact to the immutable project artifact store.",
897
+ inputSchema: artifactWriteToolSchema,
898
+ annotations: {
899
+ readOnlyHint: false,
900
+ idempotentHint: true,
901
+ },
902
+ }, async (input) => {
903
+ const projectRoot = await artifactProjectRoot();
904
+ const sessionRef = resolveArtifactSessionRef(input.session_ref);
905
+ const { putArtifact } = await import("./artifact-store.js");
906
+ const result = await putArtifact(projectRoot, { ...input, session_ref: sessionRef });
907
+ const artifact = result.artifact;
908
+ const payload = {
909
+ schema_version: artifact.schema_version,
910
+ artifact_id: artifact.artifact_id,
911
+ kind: artifact.kind,
912
+ session_ref: artifact.session_ref,
913
+ content_digest: artifact.content_digest,
914
+ logical_bytes: artifact.logical_bytes,
915
+ stored_bytes: artifact.stored_bytes,
916
+ status: result.idempotent ? "existing" : "created",
917
+ };
918
+ return {
919
+ content: [{ type: "text", text: JSON.stringify(payload) }],
920
+ structuredContent: payload,
921
+ };
922
+ });
923
+ server.registerTool("artifact_read", {
924
+ description: "Read one artifact by exact ID or an unambiguous ID prefix.",
925
+ inputSchema: z.object({ artifact_id: artifactIdentifierSchema }).strict(),
926
+ annotations: {
927
+ readOnlyHint: true,
928
+ idempotentHint: true,
929
+ },
930
+ }, async ({ artifact_id }) => {
931
+ const projectRoot = await artifactProjectRoot();
932
+ const { readArtifact } = await import("./artifact-store.js");
933
+ const payload = await readArtifact(artifact_id, projectRoot);
934
+ return {
935
+ content: [{ type: "text", text: asToolText(payload) }],
936
+ structuredContent: payload,
937
+ };
938
+ });
939
+ server.registerTool("artifact_list", {
940
+ description: "List artifact metadata newest first with exact filters and opaque pagination.",
941
+ inputSchema: z.object({
942
+ session_ref: artifactSessionRefSchema.optional(),
943
+ kind: artifactKindSchema.optional(),
944
+ node_ref: artifactNodeRefSchema.optional(),
945
+ limit: z.number().int().min(1).max(100).optional(),
946
+ cursor: z.string().min(1).max(256).optional(),
947
+ }).strict(),
948
+ annotations: {
949
+ readOnlyHint: true,
950
+ idempotentHint: true,
951
+ },
952
+ }, async (filters) => {
953
+ const projectRoot = await artifactProjectRoot();
954
+ const { listArtifacts } = await import("./artifact-store.js");
955
+ const listed = await listArtifacts(projectRoot, filters);
956
+ const payload = {
957
+ schema_version: listed.schema_version,
958
+ artifacts: listed.artifacts.map((artifact) => ({
959
+ schema_version: artifact.schema_version,
960
+ artifact_id: artifact.artifact_id,
961
+ kind: artifact.kind,
962
+ created_at: artifact.created_at,
963
+ session_ref: artifact.session_ref,
964
+ ...(artifact.node_ref ? { node_ref: artifact.node_ref } : {}),
965
+ media_type: artifact.media_type,
966
+ logical_bytes: artifact.logical_bytes,
967
+ stored_bytes: artifact.stored_bytes,
968
+ content_digest: artifact.content_digest,
969
+ ...(artifact.supersedes ? { supersedes: artifact.supersedes } : {}),
970
+ })),
971
+ logical_bytes: listed.logical_bytes,
972
+ ...(listed.next_cursor ? { next_cursor: listed.next_cursor } : {}),
973
+ };
974
+ return {
975
+ content: [{ type: "text", text: JSON.stringify(payload) }],
976
+ structuredContent: payload,
977
+ };
978
+ });
979
+ server.registerTool("artifact_delete", {
980
+ description: "Hard-delete one artifact by exact ID or an unambiguous ID prefix.",
981
+ inputSchema: z.object({ artifact_id: artifactIdentifierSchema }).strict(),
982
+ annotations: {
983
+ readOnlyHint: false,
984
+ destructiveHint: true,
985
+ idempotentHint: true,
986
+ },
987
+ }, async ({ artifact_id }) => {
988
+ const projectRoot = await artifactProjectRoot();
989
+ const { deleteArtifact } = await import("./artifact-store.js");
990
+ let payload;
991
+ try {
992
+ const deleted = await deleteArtifact(artifact_id, projectRoot);
993
+ payload = {
994
+ schema_version: deleted.schema_version,
995
+ artifact_id: deleted.artifact_id,
996
+ deleted: deleted.deleted,
997
+ missing: false,
998
+ };
999
+ }
1000
+ catch (error) {
1001
+ if (!(error instanceof Error) || error.message !== "Artifact not found.")
1002
+ throw error;
1003
+ payload = {
1004
+ schema_version: 1,
1005
+ artifact_id,
1006
+ deleted: false,
1007
+ missing: true,
1008
+ };
1009
+ }
1010
+ return {
1011
+ content: [{ type: "text", text: JSON.stringify(payload) }],
1012
+ structuredContent: payload,
1013
+ };
1014
+ });
1015
+ }
760
1016
  server.registerTool("memory_read", {
761
1017
  description: "Read an exact key or search memory entries across AgentFS scopes.",
762
1018
  // Plain object schema — a .refine() wrapper (ZodEffects) makes the SDK
@@ -765,20 +1021,35 @@ function createMemoryServer(context = {}) {
765
1021
  scope: z.string(),
766
1022
  key: z.string().optional(),
767
1023
  query: z.string().optional(),
1024
+ ...(typedNodesEnabled ? { address_space: NODE_ADDRESS_SPACE_SCHEMA.optional() } : {}),
768
1025
  }),
769
1026
  annotations: {
770
1027
  readOnlyHint: true,
771
1028
  idempotentHint: true,
772
1029
  },
773
- }, async ({ scope, key, query }) => {
1030
+ }, async (toolInput) => {
1031
+ const { scope, key, query } = toolInput;
774
1032
  if (Boolean(key) === Boolean(query)) {
775
1033
  throw new Error("Provide exactly one of key or query.");
776
1034
  }
1035
+ const addressSpace = "address_space" in toolInput && toolInput.address_space !== undefined ? NODE_ADDRESS_SPACE_SCHEMA.parse(toolInput.address_space) : undefined;
1036
+ if (addressSpace && addressSpace !== "memory") {
1037
+ const noteStore = await import("./note-store.js");
1038
+ const target = noteTargetOptions(scope, addressSpace);
1039
+ const noteResults = key
1040
+ ? [noteStore.readAddressedNote({ ...target, key })].filter((entry) => entry !== null)
1041
+ : noteStore.searchAddressedNotes({ ...target, query: query ?? "" });
1042
+ const sortedNotes = noteResults.sort((left, right) => left.key.localeCompare(right.key));
1043
+ return {
1044
+ content: [{ type: "text", text: asToolText(sortedNotes) }],
1045
+ structuredContent: { results: sortedNotes },
1046
+ };
1047
+ }
777
1048
  const config = memoryConfig();
778
1049
  const scopes = getSearchScopes(scope, config);
779
1050
  const results = key
780
- ? (await Promise.all(scopes.map((candidate) => readKey(candidate, key, scopeOptions)))).flat()
781
- : searchEntries((await Promise.all(scopes.map((candidate) => listEntries(candidate, "", scopeOptions)))).flat(), query ?? "");
1051
+ ? (await Promise.all(scopes.map((candidate) => readKey(candidate, key, { ...scopeOptions, typed: typedNodesEnabled })))).flat()
1052
+ : searchEntries((await Promise.all(scopes.map((candidate) => listEntries(candidate, "", { ...scopeOptions, typed: typedNodesEnabled })))).flat(), query ?? "");
782
1053
  const sorted = results.sort((left, right) => {
783
1054
  return `${left.scope}:${left.key}`.localeCompare(`${right.scope}:${right.key}`);
784
1055
  });
@@ -787,16 +1058,40 @@ function createMemoryServer(context = {}) {
787
1058
  structuredContent: { results: sorted },
788
1059
  };
789
1060
  });
790
- server.registerTool("memory_write", {
1061
+ registerCapabilityTool("memory.write", "memory_write", {
791
1062
  description: "Write a memory entry to a scoped AgentFS database and optionally promote it.",
792
1063
  inputSchema: z.object({
793
1064
  scope: z.string(),
794
1065
  key: z.string().min(1),
795
1066
  value: z.string(),
796
1067
  promote_to: z.string().optional(),
1068
+ ...(typedNodesEnabled ? {
1069
+ address_space: NODE_ADDRESS_SPACE_SCHEMA.optional(),
1070
+ node_type: nodeTypeSchema.optional(),
1071
+ tags: canonicalTagsSchema.optional(),
1072
+ note: noteNodeSchema.optional(),
1073
+ } : {}),
797
1074
  }),
798
- }, async ({ scope, key, value, promote_to }) => {
1075
+ }, async (input) => {
1076
+ const { scope, key, value, promote_to } = input;
1077
+ const nodeType = "node_type" in input && input.node_type !== undefined ? nodeTypeSchema.parse(input.node_type) : undefined;
1078
+ const tags = "tags" in input && input.tags !== undefined ? canonicalTagsSchema.parse(input.tags) : undefined;
1079
+ const note = "note" in input ? input.note : undefined;
1080
+ const metadataAware = "address_space" in input || nodeType !== undefined || tags !== undefined || note !== undefined;
799
1081
  assertWritableMemoryKey(key);
1082
+ const addressSpace = "address_space" in input && input.address_space !== undefined ? NODE_ADDRESS_SPACE_SCHEMA.parse(input.address_space) : undefined;
1083
+ if (addressSpace && addressSpace !== "memory") {
1084
+ if (promote_to)
1085
+ throw new Error("INVALID_SCOPE: note writes do not support promote_to.");
1086
+ const noteStore = await import("./note-store.js");
1087
+ const payload = await noteStore.createAddressedNote({ ...noteTargetOptions(scope, addressSpace), key, value, note, node_type: nodeType, tags });
1088
+ return {
1089
+ content: [{ type: "text", text: asToolText(payload) }],
1090
+ structuredContent: payload,
1091
+ };
1092
+ }
1093
+ if (note !== undefined)
1094
+ throw new Error("UNSUPPORTED_TARGET: note payloads require a note address space.");
800
1095
  const targets = promote_to && promote_to !== scope ? [scope, promote_to] : [scope];
801
1096
  // Collision-safe: in the unified store, writes from different repos/machines
802
1097
  // can share a key. If a different value already exists, preserve the old one
@@ -810,6 +1105,28 @@ function createMemoryServer(context = {}) {
810
1105
  throw new Error(`Unable to open scope ${target}.`);
811
1106
  }
812
1107
  try {
1108
+ if (typedNodesEnabled) {
1109
+ const current = await getTypedNode(agent, target, key);
1110
+ if (metadataAware) {
1111
+ await createTypedNode({ agent, scope: target, key, value, node_type: nodeType, tags });
1112
+ }
1113
+ else if (!current) {
1114
+ await createTypedNode({ agent, scope: target, key, value });
1115
+ }
1116
+ else if (current.value !== value) {
1117
+ const changed = await supersedeTypedNode({
1118
+ agent,
1119
+ scope: target,
1120
+ key,
1121
+ value,
1122
+ reason: "collision-safe write in unified store",
1123
+ });
1124
+ if (changed.snapshot_key && changed.previous_value !== undefined) {
1125
+ collisions.push({ scope: target, snapshot_key: changed.snapshot_key, previous_value: changed.previous_value });
1126
+ }
1127
+ }
1128
+ continue;
1129
+ }
813
1130
  const previous = await agent.kv.get(key);
814
1131
  const previousNorm = previous === undefined ? undefined : normalizeValue(previous);
815
1132
  if (previousNorm !== undefined && previousNorm !== value) {
@@ -839,17 +1156,37 @@ function createMemoryServer(context = {}) {
839
1156
  inputSchema: z.object({
840
1157
  scope: z.string(),
841
1158
  prefix: z.string().optional(),
1159
+ ...(typedNodesEnabled ? {
1160
+ address_space: NODE_ADDRESS_SPACE_SCHEMA.optional(),
1161
+ node_types: z.array(nodeTypeSchema).min(1).optional(),
1162
+ tags_all: canonicalTagsSchema.pipe(z.array(z.string()).min(1)).optional(),
1163
+ tags_any: canonicalTagsSchema.pipe(z.array(z.string()).min(1)).optional(),
1164
+ } : {}),
842
1165
  }),
843
1166
  annotations: {
844
1167
  readOnlyHint: true,
845
1168
  idempotentHint: true,
846
1169
  },
847
- }, async ({ scope, prefix }) => {
848
- const entries = await listEntries(scope, prefix ?? "", scopeOptions);
1170
+ }, async (input) => {
1171
+ const { scope, prefix } = input;
1172
+ const filters = typedNodesEnabled ? memoryFiltersFromInput(input) : {};
1173
+ const addressSpace = "address_space" in input && input.address_space !== undefined ? NODE_ADDRESS_SPACE_SCHEMA.parse(input.address_space) : undefined;
1174
+ if (addressSpace && addressSpace !== "memory") {
1175
+ const noteStore = await import("./note-store.js");
1176
+ const entries = noteStore.listAddressedNotes({ ...noteTargetOptions(scope, addressSpace), prefix, ...filters });
1177
+ const keys = entries.map((entry) => entry.key);
1178
+ const nodes = entries.map(({ value: _value, ...entry }) => entry);
1179
+ return {
1180
+ content: [{ type: "text", text: asToolText(keys) }],
1181
+ structuredContent: { keys, nodes },
1182
+ };
1183
+ }
1184
+ const entries = filterEntries(await listEntries(scope, prefix ?? "", { ...scopeOptions, typed: typedNodesEnabled }), filters);
849
1185
  const keys = entries.map((entry) => entry.key).sort();
1186
+ const nodes = entries.map(({ value: _value, ...entry }) => entry).sort((left, right) => left.key.localeCompare(right.key));
850
1187
  return {
851
1188
  content: [{ type: "text", text: asToolText(keys) }],
852
- structuredContent: { keys },
1189
+ structuredContent: typedNodesEnabled ? { keys, nodes } : { keys },
853
1190
  };
854
1191
  });
855
1192
  server.registerTool("memory_delete", {
@@ -857,38 +1194,79 @@ function createMemoryServer(context = {}) {
857
1194
  inputSchema: z.object({
858
1195
  scope: z.string(),
859
1196
  key: z.string().min(1),
1197
+ ...(typedNodesEnabled ? { address_space: NODE_ADDRESS_SPACE_SCHEMA.optional() } : {}),
860
1198
  }),
861
- }, async ({ scope, key }) => {
862
- if (isReviewedKey(key)) {
1199
+ }, async (input) => {
1200
+ const { scope, key } = input;
1201
+ const addressSpace = "address_space" in input && input.address_space !== undefined ? NODE_ADDRESS_SPACE_SCHEMA.parse(input.address_space) : undefined;
1202
+ if (addressSpace && addressSpace !== "memory") {
1203
+ const noteStore = await import("./note-store.js");
1204
+ const payload = await noteStore.deleteAddressedNote({ ...noteTargetOptions(scope, addressSpace), key });
1205
+ return {
1206
+ content: [{ type: "text", text: asToolText(payload) }],
1207
+ structuredContent: payload,
1208
+ };
1209
+ }
1210
+ if (typedNodesEnabled)
1211
+ assertWritableMemoryKey(key);
1212
+ else if (isReviewedKey(key)) {
863
1213
  throw new Error(`Keys under ${REVIEWED_NAMESPACE}/ are reserved for reviewed-memory provenance.`);
864
1214
  }
865
1215
  const agent = await openScope(scope, false, scopeOptions);
1216
+ let typedResult;
866
1217
  if (agent) {
867
1218
  try {
868
- await agent.kv.delete(key);
1219
+ if (typedNodesEnabled)
1220
+ typedResult = await deleteTypedNode({ agent, scope, key });
1221
+ else
1222
+ await agent.kv.delete(key);
869
1223
  }
870
1224
  finally {
871
1225
  await agent.close();
872
1226
  }
873
1227
  }
1228
+ const payload = typedNodesEnabled
1229
+ ? { ok: true, scope, key, deleted: typedResult?.deleted ?? false, missing: typedResult?.missing ?? true, snapshot_key: typedResult?.snapshot_key ?? null, ...(typedResult?.final_snapshot ? { final_snapshot: typedResult.final_snapshot } : {}) }
1230
+ : { ok: true, scope, key };
874
1231
  return {
875
- content: [{ type: "text", text: asToolText({ ok: true, scope, key }) }],
876
- structuredContent: { ok: true, scope, key },
1232
+ content: [{ type: "text", text: asToolText(payload) }],
1233
+ structuredContent: payload,
877
1234
  };
878
1235
  });
879
- server.registerTool("memory_search", {
1236
+ registerCapabilityTool("memory.search", "memory_search", {
880
1237
  description: "Semantic search across AgentFS memory scopes using the configured embedding endpoint, ranked by cosine similarity. Falls back to substring matching when embeddings are unavailable. Use this to find memory by meaning rather than by exact key.",
881
1238
  inputSchema: z.object({
882
1239
  scope: z.string(),
883
1240
  query: z.string().min(1),
884
1241
  top_k: z.number().int().min(1).max(50).optional(),
885
1242
  min_score: z.number().min(0).max(1).optional(),
1243
+ ...(typedNodesEnabled ? {
1244
+ address_space: NODE_ADDRESS_SPACE_SCHEMA.optional(),
1245
+ node_types: z.array(nodeTypeSchema).min(1).optional(),
1246
+ tags_all: canonicalTagsSchema.pipe(z.array(z.string()).min(1)).optional(),
1247
+ tags_any: canonicalTagsSchema.pipe(z.array(z.string()).min(1)).optional(),
1248
+ } : {}),
886
1249
  }),
887
1250
  annotations: {
888
1251
  readOnlyHint: true,
889
1252
  },
890
- }, async ({ scope, query, top_k, min_score }) => {
891
- const { results, mode, model } = await semanticSearch(scope, query, top_k ?? 8, min_score ?? 0, memoryConfig(), context.projectId);
1253
+ }, async (input) => {
1254
+ const { scope, query, top_k, min_score } = input;
1255
+ const addressSpace = "address_space" in input && input.address_space !== undefined ? NODE_ADDRESS_SPACE_SCHEMA.parse(input.address_space) : undefined;
1256
+ if (addressSpace && addressSpace !== "memory") {
1257
+ const noteStore = await import("./note-store.js");
1258
+ const entries = noteStore.searchAddressedNotes({ ...noteTargetOptions(scope, addressSpace), query, ...memoryFiltersFromInput(input) }).slice(0, top_k ?? 8);
1259
+ const results = entries.map((entry) => ({ ...entry, score: 1 }));
1260
+ const payload = { mode: "exact", count: results.length, results };
1261
+ return {
1262
+ content: [{ type: "text", text: asToolText(payload) }],
1263
+ structuredContent: payload,
1264
+ };
1265
+ }
1266
+ const { results, mode, model } = await semanticSearch(scope, query, top_k ?? 8, min_score ?? 0, memoryConfig(), context.projectId, {
1267
+ typed: typedNodesEnabled,
1268
+ filters: typedNodesEnabled ? memoryFiltersFromInput(input) : {},
1269
+ });
892
1270
  const payload = { mode, model, count: results.length, results };
893
1271
  return {
894
1272
  content: [{ type: "text", text: asToolText(payload) }],
@@ -923,14 +1301,51 @@ function createMemoryServer(context = {}) {
923
1301
  key: z.string().min(1),
924
1302
  value: z.string(),
925
1303
  reason: z.string().optional(),
1304
+ ...(typedNodesEnabled ? {
1305
+ address_space: NODE_ADDRESS_SPACE_SCHEMA.optional(),
1306
+ node_type: nodeTypeSchema.optional(),
1307
+ tags: canonicalTagsSchema.optional(),
1308
+ note: noteNodeSchema.optional(),
1309
+ } : {}),
926
1310
  }),
927
- }, async ({ scope, key, value, reason }) => {
1311
+ }, async (input) => {
1312
+ const { scope, key, value, reason } = input;
928
1313
  assertWritableMemoryKey(key);
1314
+ const addressSpace = "address_space" in input && input.address_space !== undefined ? NODE_ADDRESS_SPACE_SCHEMA.parse(input.address_space) : undefined;
1315
+ const note = "note" in input ? input.note : undefined;
1316
+ const nodeType = "node_type" in input && input.node_type !== undefined ? nodeTypeSchema.parse(input.node_type) : undefined;
1317
+ const tags = "tags" in input && input.tags !== undefined ? canonicalTagsSchema.parse(input.tags) : undefined;
1318
+ if (addressSpace && addressSpace !== "memory") {
1319
+ const noteStore = await import("./note-store.js");
1320
+ const payload = await noteStore.supersedeAddressedNote({ ...noteTargetOptions(scope, addressSpace), key, value, reason, note, node_type: nodeType, tags });
1321
+ return {
1322
+ content: [{ type: "text", text: asToolText(payload) }],
1323
+ structuredContent: payload,
1324
+ };
1325
+ }
1326
+ if (note !== undefined)
1327
+ throw new Error("UNSUPPORTED_TARGET: note payloads require a note address space.");
929
1328
  const agent = await openScope(scope, true, scopeOptions);
930
1329
  if (!agent) {
931
1330
  throw new Error(`Unable to open scope ${scope}.`);
932
1331
  }
933
1332
  try {
1333
+ if (typedNodesEnabled) {
1334
+ const changed = await supersedeTypedNode({
1335
+ agent,
1336
+ scope,
1337
+ key,
1338
+ value,
1339
+ node_type: nodeType,
1340
+ tags,
1341
+ reason,
1342
+ });
1343
+ const payload = { ...changed };
1344
+ return {
1345
+ content: [{ type: "text", text: asToolText(payload) }],
1346
+ structuredContent: payload,
1347
+ };
1348
+ }
934
1349
  const previous = await agent.kv.get(key);
935
1350
  if (previous === undefined) {
936
1351
  await agent.kv.set(key, value);
@@ -1015,18 +1430,33 @@ function createMemoryServer(context = {}) {
1015
1430
  });
1016
1431
  }
1017
1432
  }
1018
- const previous = await agent.kv.get(key);
1019
- const previousValue = previous === undefined ? undefined : normalizeValue(previous);
1020
1433
  let snapshotKey = null;
1021
- if (previousValue !== undefined && previousValue !== value) {
1022
- snapshotKey = historySnapshotKey(key, appliedAt);
1023
- await agent.kv.set(snapshotKey, {
1024
- value: previousValue,
1025
- superseded_at: appliedAt,
1026
- superseded_reason: `reviewed memory ${review_id}`,
1434
+ if (typedNodesEnabled) {
1435
+ const changed = await applyReviewedTypedNode({
1436
+ agent,
1437
+ scope,
1438
+ review_id,
1439
+ key,
1440
+ value,
1441
+ node_type: "memory",
1442
+ tags: [],
1443
+ in_transaction: true,
1027
1444
  });
1445
+ snapshotKey = changed.snapshot_key;
1446
+ }
1447
+ else {
1448
+ const previous = await agent.kv.get(key);
1449
+ const previousValue = previous === undefined ? undefined : normalizeValue(previous);
1450
+ if (previousValue !== undefined && previousValue !== value) {
1451
+ snapshotKey = historySnapshotKey(key, appliedAt);
1452
+ await agent.kv.set(snapshotKey, {
1453
+ value: previousValue,
1454
+ superseded_at: appliedAt,
1455
+ superseded_reason: `reviewed memory ${review_id}`,
1456
+ });
1457
+ }
1458
+ await agent.kv.set(key, value);
1028
1459
  }
1029
- await agent.kv.set(key, value);
1030
1460
  await agent.kv.set(recordKey, {
1031
1461
  schema_version: 1,
1032
1462
  review_id,
@@ -1124,13 +1554,26 @@ function createMemoryServer(context = {}) {
1124
1554
  if (current !== undefined) {
1125
1555
  const currentValue = normalizeValue(current);
1126
1556
  if (hashText(currentValue) === record.value_hash) {
1127
- snapshotKey = historySnapshotKey(record.key, invalidatedAt);
1128
- await agent.kv.set(snapshotKey, {
1129
- value: currentValue,
1130
- superseded_at: invalidatedAt,
1131
- superseded_reason: reason ?? `reviewed memory ${review_id} invalidated`,
1132
- });
1133
- await agent.kv.delete(record.key);
1557
+ if (typedNodesEnabled) {
1558
+ const changed = await invalidateReviewedTypedNode({
1559
+ agent,
1560
+ scope,
1561
+ review_id,
1562
+ key: record.key,
1563
+ reason,
1564
+ in_transaction: true,
1565
+ });
1566
+ snapshotKey = changed.snapshot_key;
1567
+ }
1568
+ else {
1569
+ snapshotKey = historySnapshotKey(record.key, invalidatedAt);
1570
+ await agent.kv.set(snapshotKey, {
1571
+ value: currentValue,
1572
+ superseded_at: invalidatedAt,
1573
+ superseded_reason: reason ?? `reviewed memory ${review_id} invalidated`,
1574
+ });
1575
+ await agent.kv.delete(record.key);
1576
+ }
1134
1577
  removed = true;
1135
1578
  }
1136
1579
  else {
@@ -1171,12 +1614,52 @@ function createMemoryServer(context = {}) {
1171
1614
  inputSchema: z.object({
1172
1615
  scope: z.string(),
1173
1616
  key: z.string().min(1),
1617
+ ...(typedNodesEnabled ? { address_space: NODE_ADDRESS_SPACE_SCHEMA.optional() } : {}),
1174
1618
  }),
1175
1619
  annotations: {
1176
1620
  readOnlyHint: true,
1177
1621
  idempotentHint: true,
1178
1622
  },
1179
- }, async ({ scope, key }) => {
1623
+ }, async (input) => {
1624
+ const { scope, key } = input;
1625
+ const addressSpace = "address_space" in input && input.address_space !== undefined ? NODE_ADDRESS_SPACE_SCHEMA.parse(input.address_space) : undefined;
1626
+ if (addressSpace && addressSpace !== "memory") {
1627
+ const noteStore = await import("./note-store.js");
1628
+ const target = noteTargetOptions(scope, addressSpace);
1629
+ const currentNode = noteStore.readAddressedNote({ ...target, key });
1630
+ const history = noteStore.listAddressedNoteHistory({ ...target, key });
1631
+ const payload = { scope, key, current: currentNode?.value ?? null, history, current_node: currentNode, history_nodes: history };
1632
+ return {
1633
+ content: [{ type: "text", text: asToolText(payload) }],
1634
+ structuredContent: payload,
1635
+ };
1636
+ }
1637
+ if (typedNodesEnabled) {
1638
+ const agent = await openScope(scope, false, scopeOptions);
1639
+ let currentNode = null;
1640
+ let historyNodes = [];
1641
+ if (agent) {
1642
+ try {
1643
+ currentNode = await getTypedNode(agent, scope, key);
1644
+ historyNodes = await listTypedHistory(agent, key);
1645
+ }
1646
+ finally {
1647
+ await agent.close();
1648
+ }
1649
+ }
1650
+ const payload = {
1651
+ scope,
1652
+ key,
1653
+ current: currentNode?.value ?? null,
1654
+ history: historyNodes,
1655
+ current_node: currentNode,
1656
+ history_nodes: historyNodes,
1657
+ };
1658
+ return {
1659
+ content: [{ type: "text", text: asToolText(payload) }],
1660
+ structuredContent: payload,
1661
+ };
1662
+ }
1180
1663
  const current = await readKey(scope, key, scopeOptions);
1181
1664
  const history = (await listEntries(scope, historyPrefix(key), {
1182
1665
  includeHistory: true,
@@ -1194,6 +1677,59 @@ function createMemoryServer(context = {}) {
1194
1677
  structuredContent: payload,
1195
1678
  };
1196
1679
  });
1680
+ if (typedNodesEnabled) {
1681
+ server.registerTool("memory_import", {
1682
+ description: "Validate, plan, and atomically import a bounded batch of typed memory nodes.",
1683
+ inputSchema: memoryImportEnvelopeSchema,
1684
+ annotations: {
1685
+ idempotentHint: true,
1686
+ },
1687
+ }, async (input) => {
1688
+ const envelope = memoryImportEnvelopeSchema.parse(input);
1689
+ if (envelope.address_space !== "memory") {
1690
+ const noteStore = await import("./note-store.js");
1691
+ const addressSpace = envelope.address_space;
1692
+ const plan = noteStore.planNoteImport({ input: envelope, ...noteTargetOptions(envelope.scope, addressSpace) });
1693
+ const payload = envelope.dry_run ? plan.result : await noteStore.commitNoteImport(plan);
1694
+ return {
1695
+ content: [{ type: "text", text: asToolText(payload) }],
1696
+ structuredContent: payload,
1697
+ };
1698
+ }
1699
+ // Resolve containment before any database is opened or directory is created.
1700
+ const scopePath = resolveScopePath(envelope.scope, scopeOptions);
1701
+ const existing = await openScope(envelope.scope, false, scopeOptions);
1702
+ let plan;
1703
+ try {
1704
+ plan = await planMemoryImport(existing, envelope);
1705
+ }
1706
+ finally {
1707
+ await existing?.close();
1708
+ }
1709
+ if (envelope.dry_run) {
1710
+ const payload = await commitMemoryImport(null, plan);
1711
+ return {
1712
+ content: [{ type: "text", text: asToolText(payload) }],
1713
+ structuredContent: payload,
1714
+ };
1715
+ }
1716
+ const payload = await serializeScopeMutation(scopePath, async () => {
1717
+ const agent = await openScope(envelope.scope, true, scopeOptions);
1718
+ if (!agent)
1719
+ throw new Error(`Unable to open scope ${envelope.scope}.`);
1720
+ try {
1721
+ return await commitMemoryImport(agent, plan);
1722
+ }
1723
+ finally {
1724
+ await agent.close();
1725
+ }
1726
+ });
1727
+ return {
1728
+ content: [{ type: "text", text: asToolText(payload) }],
1729
+ structuredContent: payload,
1730
+ };
1731
+ });
1732
+ }
1197
1733
  server.registerTool("domain_knowledge_query", {
1198
1734
  description: "Query an AnythingLLM workspace in query mode for domain knowledge.",
1199
1735
  inputSchema: z.object({
@@ -1262,7 +1798,7 @@ function createMemoryServer(context = {}) {
1262
1798
  },
1263
1799
  };
1264
1800
  });
1265
- server.registerTool("context_explore", {
1801
+ registerCapabilityTool("context.explore", "context_explore", {
1266
1802
  description: "Delegate a natural-language repo-exploration query to the external token_miser explore binary (FastContext-backed). Returns compact path:line-range citations. Requires CAIRN_EXPLORE_BINARY (absolute path to the token_miser binary) and a repo_root (per-call param or CAIRN_EXPLORE_REPO_ROOT env). Thin adapter — token_miser owns all exploration logic.",
1267
1803
  inputSchema: z.object({
1268
1804
  query: z.string().min(1),
@@ -1292,7 +1828,7 @@ function createMemoryServer(context = {}) {
1292
1828
  structuredContent: payload,
1293
1829
  };
1294
1830
  });
1295
- server.registerTool("route_check", {
1831
+ registerCapabilityTool("route.check", "route_check", {
1296
1832
  description: "Check reachability of the external token_miser routing/tiering proxy via its /health endpoint. Requires CAIRN_ROUTE_ENDPOINT (base URL of an already-running token_miser instance). Thin adapter — token_miser owns all routing/tiering logic; this tool neither hosts a proxy nor learns which tier serves a request.",
1297
1833
  inputSchema: z.object({
1298
1834
  timeout_seconds: z.number().int().min(1).max(60).optional(),
@@ -1345,6 +1881,15 @@ function createMemoryServer(context = {}) {
1345
1881
  });
1346
1882
  return server;
1347
1883
  }
1884
+ function createMemoryServer(context = {}) {
1885
+ const contractEnabled = isCapabilityContractEnabled();
1886
+ if (!contractEnabled)
1887
+ return buildMemoryServer(context);
1888
+ return Promise.all([
1889
+ resolveCapabilityStatus({ projectRoot: process.cwd() }),
1890
+ import("./capability-adapter.js"),
1891
+ ]).then(([snapshot, adapter]) => buildMemoryServer(context, snapshot, adapter.withCapability));
1892
+ }
1348
1893
  // One-shot CLI: `node dist/index.js wakeup` prints project-scope memory for the
1349
1894
  // SessionStart hook. Reads a file-snapshot copy of ./.agentfs/project.db so it
1350
1895
  // never contends with a running cairn-memory MCP that holds the exclusive lock
@@ -1481,7 +2026,7 @@ if (httpPort > 0) {
1481
2026
  onsessioninitialized: (id) => { sessions.set(id, transport); },
1482
2027
  onsessionclosed: (id) => { sessions.delete(id); },
1483
2028
  });
1484
- const session = createMemoryServer(context);
2029
+ const session = await createMemoryServer(context);
1485
2030
  await session.connect(transport);
1486
2031
  return transport.handleRequest(request);
1487
2032
  };
@@ -1540,7 +2085,7 @@ if (httpPort > 0) {
1540
2085
  } process.exit(0); });
1541
2086
  }
1542
2087
  else {
1543
- const server = createMemoryServer();
2088
+ const server = await createMemoryServer();
1544
2089
  const transport = new StdioServerTransport();
1545
2090
  process.on("SIGINT", async () => {
1546
2091
  await server.close();