@gmickel/gno 2.5.1 → 2.7.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 (160) hide show
  1. package/README.md +60 -5
  2. package/assets/skill/README.md +5 -1
  3. package/assets/skill/SKILL.md +80 -4
  4. package/assets/skill/cli-reference.md +132 -2
  5. package/assets/skill/examples.md +30 -0
  6. package/assets/skill/mcp-reference.md +54 -1
  7. package/assets/skill/recipes/capture-and-file.md +6 -0
  8. package/assets/skill/recipes/memory-file-decision.md +6 -0
  9. package/assets/skill/recipes/memory-supersede-fact.md +5 -0
  10. package/assets/skill/recipes/session-evidence-lookup.md +98 -0
  11. package/assets/spa-production.json.gz +0 -0
  12. package/browser-extension/artifacts/{gno-browser-clipper-v2.5.1.zip → gno-browser-clipper-v2.7.0.zip} +0 -0
  13. package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +1 -0
  14. package/browser-extension/dist/manifest.json +1 -1
  15. package/package.json +2 -1
  16. package/spec/cli.md +449 -35
  17. package/spec/mcp.md +234 -4
  18. package/spec/output-schemas/ask.schema.json +1 -1
  19. package/spec/output-schemas/capture-receipt.schema.json +4 -1
  20. package/spec/output-schemas/doctor.schema.json +88 -0
  21. package/spec/output-schemas/error.schema.json +11 -2
  22. package/spec/output-schemas/get.schema.json +1 -1
  23. package/spec/output-schemas/mcp-capture-result.schema.json +4 -2
  24. package/spec/output-schemas/memory-remember.schema.json +10 -4
  25. package/spec/output-schemas/multi-get.schema.json +4 -1
  26. package/spec/output-schemas/peek.schema.json +2 -9
  27. package/spec/output-schemas/request-status.schema.json +113 -0
  28. package/spec/output-schemas/resident-status.schema.json +22 -0
  29. package/spec/output-schemas/search-result.schema.json +1 -1
  30. package/spec/output-schemas/search-results.schema.json +1 -1
  31. package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
  32. package/spec/output-schemas/sessions-discovery.schema.json +38 -0
  33. package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
  34. package/spec/output-schemas/sessions-status.schema.json +432 -0
  35. package/spec/output-schemas/status.schema.json +98 -0
  36. package/src/cli/commands/ask.ts +14 -2
  37. package/src/cli/commands/capture.ts +55 -96
  38. package/src/cli/commands/daemon.ts +41 -0
  39. package/src/cli/commands/doctor.ts +54 -20
  40. package/src/cli/commands/embed.ts +41 -3
  41. package/src/cli/commands/ls.ts +3 -0
  42. package/src/cli/commands/memory.ts +12 -3
  43. package/src/cli/commands/query.ts +5 -0
  44. package/src/cli/commands/request-status.ts +59 -0
  45. package/src/cli/commands/reset.ts +39 -5
  46. package/src/cli/commands/sessions.ts +713 -0
  47. package/src/cli/commands/shared.ts +14 -1
  48. package/src/cli/commands/status.ts +63 -5
  49. package/src/cli/commands/vec.ts +54 -0
  50. package/src/cli/detach.ts +29 -1
  51. package/src/cli/errors.ts +13 -9
  52. package/src/cli/program.ts +441 -2
  53. package/src/cli/session-binding.ts +49 -0
  54. package/src/config/types.ts +8 -0
  55. package/src/core/capture-publish.ts +239 -0
  56. package/src/core/capture-sync.ts +12 -2
  57. package/src/core/host-paths.ts +31 -0
  58. package/src/core/memory-remember.ts +234 -122
  59. package/src/core/memory-types.ts +11 -0
  60. package/src/core/network-boundary-inventory.ts +8 -0
  61. package/src/core/request-receipts.ts +671 -0
  62. package/src/core/shutdown-budget.ts +6 -0
  63. package/src/core/vector-partition-status.ts +52 -0
  64. package/src/embed/backlog.ts +124 -18
  65. package/src/embed/fingerprint.ts +6 -3
  66. package/src/embed/retry.ts +66 -27
  67. package/src/embed/variant-backlog.ts +15 -10
  68. package/src/embed/variant-retry.ts +31 -22
  69. package/src/index.ts +30 -2
  70. package/src/llm/native-worker/dispatcher.ts +2 -0
  71. package/src/llm/native-worker/embedding-identity.ts +42 -0
  72. package/src/llm/native-worker/protocol.ts +1 -0
  73. package/src/llm/types.ts +3 -0
  74. package/src/mcp/context.ts +17 -0
  75. package/src/mcp/http-egress.ts +4 -0
  76. package/src/mcp/http-transport.ts +2 -0
  77. package/src/mcp/resources/index.ts +6 -5
  78. package/src/mcp/tool-descriptions-core.ts +1 -1
  79. package/src/mcp/tools/capture.ts +87 -85
  80. package/src/mcp/tools/index.ts +77 -4
  81. package/src/mcp/tools/memory-remember.ts +8 -1
  82. package/src/mcp/tools/memory-shared.ts +7 -1
  83. package/src/mcp/tools/request-status.ts +73 -0
  84. package/src/mcp/tools/sessions.ts +208 -0
  85. package/src/mcp/tools/status.ts +4 -0
  86. package/src/pipeline/hybrid.ts +37 -7
  87. package/src/pipeline/vsearch.ts +14 -2
  88. package/src/sdk/client.ts +180 -84
  89. package/src/sdk/index.ts +6 -0
  90. package/src/sdk/types.ts +54 -2
  91. package/src/serve/capture-service.ts +98 -32
  92. package/src/serve/config-sync.ts +3 -2
  93. package/src/serve/embed-scheduler.ts +133 -19
  94. package/src/serve/host-path-redaction.ts +79 -0
  95. package/src/serve/public/app.tsx +4 -1
  96. package/src/serve/public/components/CaptureModal.tsx +26 -8
  97. package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
  98. package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
  99. package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
  100. package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
  101. package/src/serve/public/components/sessions/api.ts +40 -0
  102. package/src/serve/public/globals.built.css +1 -1
  103. package/src/serve/public/hooks/use-api.ts +26 -3
  104. package/src/serve/public/lib/request-intent.ts +77 -0
  105. package/src/serve/public/lib/snippet.tsx +52 -0
  106. package/src/serve/public/lib/workspace-actions.ts +12 -1
  107. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  108. package/src/serve/public/pages/Dashboard.tsx +22 -9
  109. package/src/serve/public/pages/DocView.tsx +25 -6
  110. package/src/serve/public/pages/DocumentEditor.tsx +224 -104
  111. package/src/serve/public/pages/Search.tsx +1 -41
  112. package/src/serve/public/pages/Sessions.tsx +350 -0
  113. package/src/serve/resident-runtime.ts +69 -4
  114. package/src/serve/resident-status.ts +13 -1
  115. package/src/serve/routes/api.ts +476 -147
  116. package/src/serve/routes/sessions.ts +766 -0
  117. package/src/serve/security.ts +9 -0
  118. package/src/serve/server.ts +215 -10
  119. package/src/serve/session-automation.ts +146 -0
  120. package/src/serve/status-model.ts +16 -0
  121. package/src/serve/status.ts +2 -0
  122. package/src/serve/watch-reconciliation-shared.ts +3 -0
  123. package/src/serve/watch-service-events.ts +3 -2
  124. package/src/serve/watch-service-run-flush.ts +35 -2
  125. package/src/serve/watch-service.ts +5 -0
  126. package/src/sessions/archive.ts +348 -0
  127. package/src/sessions/automation-state.ts +444 -0
  128. package/src/sessions/automation-status.ts +239 -0
  129. package/src/sessions/automation.ts +1169 -0
  130. package/src/sessions/binding.ts +105 -0
  131. package/src/sessions/claude-hook.ts +240 -0
  132. package/src/sessions/config.ts +176 -0
  133. package/src/sessions/format.ts +191 -0
  134. package/src/sessions/import-child-env.ts +8 -0
  135. package/src/sessions/import-child.ts +152 -0
  136. package/src/sessions/parsers/claude-code.ts +259 -0
  137. package/src/sessions/parsers/codex.ts +303 -0
  138. package/src/sessions/parsers/hermes.ts +248 -0
  139. package/src/sessions/parsers/openclaw.ts +496 -0
  140. package/src/sessions/parsers/shared.ts +184 -0
  141. package/src/sessions/sanitize.ts +222 -0
  142. package/src/sessions/service.ts +1533 -0
  143. package/src/sessions/setup.ts +477 -0
  144. package/src/sessions/sources.ts +518 -0
  145. package/src/sessions/state.ts +118 -0
  146. package/src/sessions/types.ts +457 -0
  147. package/src/store/migrations/031-runtime-independent-vectors.ts +29 -0
  148. package/src/store/migrations/032-vector-runtime-callers.ts +17 -0
  149. package/src/store/migrations/index.ts +4 -0
  150. package/src/store/sqlite/adapter.ts +76 -16
  151. package/src/store/sqlite/scoped-index.ts +9 -0
  152. package/src/store/types.ts +11 -1
  153. package/src/store/vector/lazy.ts +46 -43
  154. package/src/store/vector/runtime-compat.ts +651 -0
  155. package/src/store/vector/sqlite-vec.ts +20 -2
  156. package/src/store/vector/status.ts +276 -35
  157. package/src/store/vector/types.ts +2 -0
  158. package/src/store/vector/variant-search.ts +71 -23
  159. package/src/store/vector/variants.ts +49 -14
  160. package/browser-extension/artifacts/gno-browser-clipper-v2.5.1.zip.sha256 +0 -1
@@ -66,11 +66,14 @@ export interface ToolContextSnapshot {
66
66
  * session id, so this is what keeps two modern callers apart.
67
67
  */
68
68
  requestIdentity?: string;
69
+ /** Stable request-receipt namespace of the authorized HTTP identity. */
70
+ requestNamespace?: string;
69
71
  }
70
72
 
71
73
  export interface RequestScope {
72
74
  egress: NonNullable<ToolContextSnapshot["egress"]>;
73
75
  requestIdentity?: string;
76
+ requestNamespace?: string;
74
77
  }
75
78
 
76
79
  export interface ToolContext {
@@ -107,6 +110,8 @@ export interface ToolContext {
107
110
  getEgressContext?: () => ToolContextSnapshot["egress"];
108
111
  /** Per-caller identity of the current request; absent on stdio. */
109
112
  getRequestIdentity?: () => string | undefined;
113
+ /** Request-receipt namespace of the current HTTP caller; absent on stdio. */
114
+ getRequestNamespace?: () => string | undefined;
110
115
  runWithEgressContext?<T>(
111
116
  egress: NonNullable<ToolContextSnapshot["egress"]>,
112
117
  operation: () => Promise<T>,
@@ -115,6 +120,15 @@ export interface ToolContext {
115
120
  runWithSnapshot?<T>(operation: () => Promise<T>): Promise<T>;
116
121
  }
117
122
 
123
+ /**
124
+ * Host absolute paths reach only stdio callers, which run on the owner's
125
+ * machine. Every Streamable HTTP request runs inside an egress context,
126
+ * whatever its peer, so HTTP callers get URIs and relative paths only.
127
+ */
128
+ export const exposesHostPaths = (
129
+ ctx: Pick<ToolContext, "getEgressContext">
130
+ ): boolean => ctx.getEgressContext?.() === undefined;
131
+
118
132
  export interface CreateToolContextOptions {
119
133
  store: SqliteAdapter;
120
134
  getConfig: () => Config;
@@ -216,6 +230,7 @@ export function createToolContext(
216
230
  requestSnapshot.getStore()?.egress?.authorizationEpoch?.value,
217
231
  getEgressContext: () => requestSnapshot.getStore()?.egress,
218
232
  getRequestIdentity: () => requestSnapshot.getStore()?.requestIdentity,
233
+ getRequestNamespace: () => requestSnapshot.getStore()?.requestNamespace,
219
234
  runWithEgressContext<T>(
220
235
  egress: NonNullable<ToolContextSnapshot["egress"]>,
221
236
  operation: () => Promise<T>,
@@ -228,6 +243,7 @@ export function createToolContext(
228
243
  collections: config.collections,
229
244
  egress,
230
245
  requestIdentity: scope?.requestIdentity,
246
+ requestNamespace: scope?.requestNamespace,
231
247
  },
232
248
  operation
233
249
  );
@@ -241,6 +257,7 @@ export function createToolContext(
241
257
  collections: config.collections,
242
258
  egress: current?.egress,
243
259
  requestIdentity: current?.requestIdentity,
260
+ requestNamespace: current?.requestNamespace,
244
261
  },
245
262
  operation
246
263
  );
@@ -57,8 +57,12 @@ export const MCP_HTTP_EGRESS_TOOLS = {
57
57
  gno_remember: "source",
58
58
  gno_remove_collection: "metadata",
59
59
  gno_rename_note: "metadata",
60
+ gno_request_status: "metadata",
60
61
  gno_search: "snippet",
61
62
  gno_section: "metadata",
63
+ gno_sessions_automation_run: "metadata",
64
+ gno_sessions_import: "metadata",
65
+ gno_sessions_status: "metadata",
62
66
  gno_similar: "snippet",
63
67
  gno_peek: "metadata",
64
68
  gno_status: "metadata",
@@ -15,6 +15,7 @@ import type {
15
15
 
16
16
  import { MCP_SERVER_NAME, VERSION } from "../app/constants";
17
17
  import { EgressDeniedError } from "../core/egress-enforcement";
18
+ import { httpMcpRequestNamespace } from "../core/request-receipts";
18
19
  import { withInferenceScope } from "../llm/inference-scope";
19
20
  import { createMcpServerSurface, type ToolContext } from "./context";
20
21
  import {
@@ -445,6 +446,7 @@ export class HttpMcpTransport {
445
446
  this.#runtime.mcpContext.serverInstanceId,
446
447
  context.identity
447
448
  ),
449
+ requestNamespace: httpMcpRequestNamespace(context.identity),
448
450
  }
449
451
  )
450
452
  : await handle();
@@ -21,6 +21,7 @@ import { resolveEffectiveIndex } from "../../core/indexed-reference";
21
21
  import { normalizeTag, validateTag } from "../../core/tags";
22
22
  import { normalizeCollectionName } from "../../core/validation";
23
23
  import { openScopedIndexStore } from "../../store/sqlite/scoped-index";
24
+ import { exposesHostPaths } from "../context";
24
25
 
25
26
  // Tags resource URI prefix
26
27
  const TAGS_URI = `${URI_PREFIX}tags`;
@@ -53,15 +54,15 @@ function formatResourceContent(
53
54
  ctx: ToolContext,
54
55
  indexName = ctx.indexName
55
56
  ): string {
56
- // Find collection for absPath
57
+ // Host path for stdio callers; HTTP callers see the relative path.
57
58
  const uriParsed = parseUri(doc.uri);
58
- let absPath = doc.relPath;
59
- if (uriParsed) {
59
+ let source = doc.relPath;
60
+ if (uriParsed && exposesHostPaths(ctx)) {
60
61
  const collection = ctx.collections.find(
61
62
  (c) => c.name === uriParsed.collection
62
63
  );
63
64
  if (collection) {
64
- absPath = pathJoin(collection.path, doc.relPath);
65
+ source = pathJoin(collection.path, doc.relPath);
65
66
  }
66
67
  }
67
68
 
@@ -72,7 +73,7 @@ function formatResourceContent(
72
73
  const displayUri = decorateUriForIndex(doc.uri, indexName);
73
74
  const header = `<!-- ${displayUri}
74
75
  docid: ${doc.docid}
75
- source: ${absPath}
76
+ source: ${source}
76
77
  mime: ${doc.sourceMime}${langLine}
77
78
  -->
78
79
 
@@ -32,7 +32,7 @@ export const MCP_CORE_TOOL_DESCRIPTIONS: Readonly<Record<string, string>> = {
32
32
  gno_recall:
33
33
  "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. Retrieves current facts from a memory-managed collection for the explicit scopes you pass; superseded facts are excluded. 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. Pass that receipt to gno_remember when a stored fact derives from this recall. An empty result names the command that stores the first fact.",
34
34
  gno_capture:
35
- "Call to create a new note from text the user wants kept: pass collection and content (or a presetId scaffold), optionally title, path or folderPath, tags, and source provenance. Writes the file to disk with source: frontmatter, syncs it for keyword search, and returns a receipt with uri, docid, relPath, absPath, contentHash, collisionPolicyResult, and sync and embed status. Embedding is a separate step (embed.status stays short of completed until gno_index or gno_embed runs), so vector search sees the note later. An existing target follows collisionPolicy: error, open_existing, or create_with_suffix.",
35
+ "Call to create a new note from text the user wants kept: pass collection and content (or a presetId scaffold), optionally title, path or folderPath, tags, and source provenance. Writes the file to disk with source: frontmatter, syncs it for keyword search, and returns a receipt with uri, docid, relPath, absPath (stdio only), contentHash, collisionPolicyResult, and sync and embed status. Embedding is a separate step (embed.status stays short of completed until gno_index or gno_embed runs), so vector search sees the note later. An existing target follows collisionPolicy: error, open_existing, or create_with_suffix.",
36
36
  gno_remember:
37
37
  "Call when the user states a durable preference, decision, or fact worth recalling later; documents go through gno_capture and existing notes through file edits. Stores one fact in a memory-managed collection under the explicit scopes you pass. 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. Returns outcome (candidates, existing, added, or superseded) with the stored record; an exact duplicate returns 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.",
38
38
  };
@@ -4,33 +4,36 @@
4
4
  * @module src/mcp/tools/capture
5
5
  */
6
6
 
7
- // node:fs/promises for mkdir (no Bun equivalent for structure ops)
8
- import { mkdir } from "node:fs/promises";
9
7
  // node:path for path utils (no Bun path utils)
10
- import { dirname, extname, join } from "node:path";
8
+ import { extname } from "node:path";
11
9
 
12
10
  import type { NoteCollisionPolicy } from "../../core/note-creation";
13
11
  import type { NotePresetId } from "../../core/note-presets";
14
12
  import type { ToolContext } from "../server";
15
13
 
16
14
  import {
17
- buildCaptureReceipt,
18
15
  CaptureSyncError,
19
- ensureCapturedFileIndexed,
20
16
  listCaptureDiskRelPaths,
21
17
  planCapture,
22
- syncCapturedFile,
23
18
  type CaptureInput as SharedCaptureInput,
19
+ type CapturePlan,
24
20
  type CaptureReceipt,
25
- type SyncCapturedFileResult,
26
21
  } from "../../core/capture";
27
- import { writeCapturePlanFile } from "../../core/capture-write";
22
+ import {
23
+ publishCapture,
24
+ type PublishedCapture,
25
+ } from "../../core/capture-publish";
28
26
  import { MCP_ERRORS } from "../../core/errors";
29
- import { withWriteLock } from "../../core/file-lock";
30
27
  import { recordContentMutation } from "../../core/mutation-generations";
28
+ import {
29
+ formatRequestReceiptLine,
30
+ type RequestReceiptInfo,
31
+ requestLedgerPath,
32
+ } from "../../core/request-receipts";
31
33
  import { normalizeCollectionName } from "../../core/validation";
32
34
  import { DEFAULT_LOCK_WAIT_MS } from "../../core/write-lease";
33
35
  import { runTool, type ToolResult } from "./index";
36
+ import { mcpRequestNamespace, rethrowRequestError } from "./request-status";
34
37
 
35
38
  interface CaptureInput extends Omit<
36
39
  SharedCaptureInput,
@@ -39,13 +42,14 @@ interface CaptureInput extends Omit<
39
42
  path?: string;
40
43
  collisionPolicy?: NoteCollisionPolicy;
41
44
  presetId?: NotePresetId;
45
+ requestId?: string;
42
46
  }
43
47
 
44
48
  type McpCaptureResult = CaptureReceipt & {
45
49
  docid: string;
46
- absPath: string;
47
50
  overwritten: boolean;
48
51
  serverInstanceId: string;
52
+ request?: RequestReceiptInfo;
49
53
  };
50
54
 
51
55
  const SENSITIVE_SUBPATHS = new Set([
@@ -75,7 +79,7 @@ function formatCaptureResult(result: McpCaptureResult): string {
75
79
  const lines: string[] = [];
76
80
  lines.push(`Doc: ${result.docid}`);
77
81
  lines.push(`URI: ${result.uri}`);
78
- lines.push(`Path: ${result.absPath}`);
82
+ if (result.absPath) lines.push(`Path: ${result.absPath}`);
79
83
  lines.push(`Created: ${result.created ? "yes" : "no"}`);
80
84
  lines.push(`Opened existing: ${result.openedExisting ? "yes" : "no"}`);
81
85
  lines.push(`Overwritten: ${result.overwritten ? "yes" : "no"}`);
@@ -86,6 +90,7 @@ function formatCaptureResult(result: McpCaptureResult): string {
86
90
  if (result.tags.length > 0) {
87
91
  lines.push(`Tags: ${result.tags.join(", ")}`);
88
92
  }
93
+ if (result.request) lines.push(formatRequestReceiptLine(result.request));
89
94
  return lines.join("\n");
90
95
  }
91
96
 
@@ -112,7 +117,7 @@ function rethrowCaptureError(error: unknown): never {
112
117
  if (error instanceof CaptureSyncError) {
113
118
  throw new Error(`${error.code}: ${error.message}`);
114
119
  }
115
- throw error;
120
+ return rethrowRequestError(error);
116
121
  }
117
122
 
118
123
  export function handleCapture(
@@ -140,83 +145,80 @@ export function handleCapture(
140
145
  // Write + lexical sync complete under the shared write lease: the tool
141
146
  // succeeds only once the capture is retrievable (v1.38 contention
142
147
  // contract: wait for the lease, LOCKED when it stays busy).
143
- return await withWriteLock(
144
- ctx.writeLockPath,
145
- async () => {
146
- const existingDocs = await ctx.store.listDocuments(collectionName);
147
- if (!existingDocs.ok) {
148
- throw new Error(existingDocs.error.message);
149
- }
150
-
151
- let plan;
152
- try {
153
- plan = planCapture({
154
- input: buildSharedInput(args, collection.name),
155
- existingRelPaths: existingDocs.value.map((doc) => doc.relPath),
156
- diskRelPaths: await listCaptureDiskRelPaths(collection.path),
157
- });
158
- } catch (error) {
159
- const message =
160
- error instanceof Error ? error.message : String(error);
161
- throw new Error(`${MCP_ERRORS.INVALID_INPUT.code}: ${message}`);
162
- }
163
-
164
- assertNotSensitive(plan.relPath);
165
-
166
- const absPath = join(collection.path, plan.relPath);
167
- const syncInput = {
168
- collection,
169
- store: ctx.store,
170
- relPath: plan.relPath,
171
- absPath,
172
- config: ctx.config,
173
- };
174
-
175
- let synced: SyncCapturedFileResult;
176
- let overwritten = false;
177
- try {
178
- if (plan.openedExisting) {
179
- synced = await ensureCapturedFileIndexed(syncInput);
180
- } else {
181
- overwritten =
182
- (await Bun.file(absPath).exists()) && args.overwrite === true;
183
- await mkdir(dirname(absPath), { recursive: true });
184
- await writeCapturePlanFile(plan, absPath);
185
- synced = await syncCapturedFile(syncInput);
148
+ const input = buildSharedInput(args, collection.name);
149
+ let published: PublishedCapture;
150
+ try {
151
+ published = await publishCapture({
152
+ collection,
153
+ store: ctx.store,
154
+ lockPath: ctx.writeLockPath,
155
+ lockWaitMs: DEFAULT_LOCK_WAIT_MS,
156
+ config: ctx.config,
157
+ plan: async () => {
158
+ const existingDocs = await ctx.store.listDocuments(collectionName);
159
+ if (!existingDocs.ok) {
160
+ throw new Error(existingDocs.error.message);
186
161
  }
187
- } catch (error) {
188
- rethrowCaptureError(error);
189
- }
190
- if (synced.result) {
191
- recordContentMutation(synced.result, ctx.markContentMutation);
192
- }
193
-
194
- const isMarkdown =
195
- plan.relPath.endsWith(".md") || plan.relPath.endsWith(".markdown");
196
- if (!isMarkdown && !plan.openedExisting && plan.tags.length > 0) {
197
- const tagResult = await ctx.store.setDocTags(
198
- synced.documentId,
199
- plan.tags,
200
- "user"
201
- );
202
- if (!tagResult.ok) {
203
- console.error(
204
- `[MCP] Warning: Document created but tags not stored: ${tagResult.error.message}`
162
+ let plan: CapturePlan;
163
+ try {
164
+ plan = planCapture({
165
+ input,
166
+ existingRelPaths: existingDocs.value.map((doc) => doc.relPath),
167
+ diskRelPaths: await listCaptureDiskRelPaths(collection.path),
168
+ });
169
+ } catch (error) {
170
+ const message =
171
+ error instanceof Error ? error.message : String(error);
172
+ throw new Error(`${MCP_ERRORS.INVALID_INPUT.code}: ${message}`);
173
+ }
174
+ assertNotSensitive(plan.relPath);
175
+ return plan;
176
+ },
177
+ afterSync: async (synced, receipt) => {
178
+ if (synced.result) {
179
+ recordContentMutation(synced.result, ctx.markContentMutation);
180
+ }
181
+ const isMarkdown =
182
+ receipt.relPath.endsWith(".md") ||
183
+ receipt.relPath.endsWith(".markdown");
184
+ if (
185
+ !isMarkdown &&
186
+ !receipt.openedExisting &&
187
+ receipt.tags.length > 0
188
+ ) {
189
+ const tagResult = await ctx.store.setDocTags(
190
+ synced.documentId,
191
+ receipt.tags,
192
+ "user"
205
193
  );
194
+ if (!tagResult.ok) {
195
+ console.error(
196
+ `[MCP] Warning: Document created but tags not stored: ${tagResult.error.message}`
197
+ );
198
+ }
206
199
  }
207
- }
200
+ },
201
+ request:
202
+ args.requestId === undefined
203
+ ? undefined
204
+ : {
205
+ ledgerPath: requestLedgerPath(ctx.store.getDbPath()),
206
+ namespace: mcpRequestNamespace(ctx),
207
+ requestId: args.requestId,
208
+ input,
209
+ },
210
+ });
211
+ } catch (error) {
212
+ rethrowCaptureError(error);
213
+ }
208
214
 
209
- return buildCaptureReceipt({
210
- plan,
211
- absPath,
212
- docid: synced.docid,
213
- sync: synced.sync,
214
- overwritten,
215
- serverInstanceId: ctx.serverInstanceId,
216
- }) as McpCaptureResult;
217
- },
218
- DEFAULT_LOCK_WAIT_MS
219
- );
215
+ return {
216
+ ...published.receipt,
217
+ docid: published.receipt.docid ?? "",
218
+ overwritten: published.receipt.overwritten ?? false,
219
+ serverInstanceId: ctx.serverInstanceId,
220
+ ...(published.request ? { request: published.request } : {}),
221
+ } as McpCaptureResult;
220
222
  },
221
223
  formatCaptureResult
222
224
  );
@@ -20,6 +20,7 @@ import {
20
20
  compiledContextPreviewSchema,
21
21
  compiledContextCheckSchema,
22
22
  } from "../../core/compiled-context";
23
+ import { withoutHostPaths } from "../../core/host-paths";
23
24
  import { NOTE_PRESETS, type NotePresetId } from "../../core/note-presets";
24
25
  import { RETRIEVAL_TRACE_METADATA } from "../../core/retrieval-trace-session";
25
26
  import { normalizeTag } from "../../core/tags";
@@ -31,6 +32,7 @@ import {
31
32
  assertInferenceActive,
32
33
  acquireInferencePermit,
33
34
  } from "../../llm/inference-scope";
35
+ import { exposesHostPaths } from "../context";
34
36
  import { profileToolDescription } from "../tool-descriptions-core";
35
37
  import {
36
38
  createProfileToolRegistrar,
@@ -101,6 +103,12 @@ import { handleMultiGet } from "./multi-get";
101
103
  import { handlePeek, PEEK_MCP_ANNOTATIONS } from "./peek";
102
104
  import { handleQuery, handleQueryDiagnose } from "./query";
103
105
  import { handleRemoveCollection } from "./remove-collection";
106
+ import {
107
+ handleRequestStatus,
108
+ REQUEST_STATUS_MCP_ANNOTATIONS,
109
+ requestIdInputSchema,
110
+ requestStatusInputSchema,
111
+ } from "./request-status";
104
112
  import { handleSearch } from "./search";
105
113
  import {
106
114
  handleSection,
@@ -108,6 +116,17 @@ import {
108
116
  sectionInputSchema,
109
117
  sectionOutputSchema,
110
118
  } from "./sections";
119
+ import {
120
+ handleSessionsAutomationRun,
121
+ handleSessionsImport,
122
+ handleSessionsStatus,
123
+ SESSIONS_AUTOMATION_RUN_MCP_ANNOTATIONS,
124
+ SESSIONS_IMPORT_MCP_ANNOTATIONS,
125
+ SESSIONS_STATUS_MCP_ANNOTATIONS,
126
+ sessionsAutomationRunInputSchema,
127
+ sessionsImportInputSchema,
128
+ sessionsStatusInputSchema,
129
+ } from "./sessions";
111
130
  import { handleStatus } from "./status";
112
131
  import { handleSync } from "./sync";
113
132
  import {
@@ -175,6 +194,8 @@ export const MCP_TOOL_DESCRIPTIONS = {
175
194
  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.",
176
195
  recall:
177
196
  "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.",
197
+ requestStatus:
198
+ "Look up a write request ID sent with gno_capture or gno_remember before retrying it. Returns pending, committed (with the result uri and hashes), expired, or not_found within this caller's own namespace; never returns note or fact content. Committed: do not resend. Pending: retry the same request ID later. not_found: nothing was accepted under that ID.",
178
199
  remember:
179
200
  "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.",
180
201
  } as const;
@@ -366,6 +387,7 @@ export const captureInputSchema = z.object({
366
387
  .describe(
367
388
  "Structured provenance metadata written under source frontmatter"
368
389
  ),
390
+ requestId: requestIdInputSchema.optional(),
369
391
  });
370
392
 
371
393
  const addCollectionInputSchema = z.object({
@@ -952,6 +974,10 @@ export interface ToolResult {
952
974
  // DRY Helper: Exception Firewall + Mutex + Response Shaping
953
975
  // ─────────────────────────────────────────────────────────────────────────────
954
976
 
977
+ /** Tool data as the current caller may see it (no host paths over HTTP). */
978
+ const forCaller = <T>(ctx: ToolContext, data: T): T =>
979
+ exposesHostPaths(ctx) ? data : withoutHostPaths(data);
980
+
955
981
  export async function runTool<T>(
956
982
  ctx: ToolContext,
957
983
  name: string,
@@ -970,12 +996,13 @@ export async function runTool<T>(
970
996
  const release = await acquireInferencePermit(() => ctx.toolMutex.acquire());
971
997
  try {
972
998
  assertInferenceActive();
973
- const data = await (ctx.runWithSnapshot?.(fn) ?? fn());
999
+ const raw = await (ctx.runWithSnapshot?.(fn) ?? fn());
974
1000
  assertInferenceActive();
975
1001
  const traceMetadata =
976
- data !== null && typeof data === "object"
977
- ? (data as Record<PropertyKey, unknown>)[RETRIEVAL_TRACE_METADATA]
1002
+ raw !== null && typeof raw === "object"
1003
+ ? (raw as Record<PropertyKey, unknown>)[RETRIEVAL_TRACE_METADATA]
978
1004
  : undefined;
1005
+ const data = forCaller(ctx, raw);
979
1006
  return {
980
1007
  content: [{ type: "text", text: formatText(data) }],
981
1008
  structuredContent: data as { [x: string]: unknown },
@@ -1019,7 +1046,7 @@ export async function runToolNoMutex<T>(
1019
1046
 
1020
1047
  try {
1021
1048
  assertInferenceActive();
1022
- const data = await (ctx.runWithSnapshot?.(fn) ?? fn());
1049
+ const data = forCaller(ctx, await (ctx.runWithSnapshot?.(fn) ?? fn()));
1023
1050
  assertInferenceActive();
1024
1051
  return {
1025
1052
  content: [{ type: "text", text: formatText(data) }],
@@ -1141,6 +1168,17 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
1141
1168
  })
1142
1169
  );
1143
1170
 
1171
+ registerTool(
1172
+ "gno_sessions_status",
1173
+ {
1174
+ description:
1175
+ "Status of this session-archive instance: archive collections, owner-registered session sources (by ID, no host paths), pending/incomplete/failed units, last import, and opt-in automation profiles (enabled hook/schedule, daemon availability, pending/running/partial/failed, last success, next due, recovery action). Only available when the server runs on a dedicated session-archive config/index pair; nothing is imported unless the owner enabled it.",
1176
+ inputSchema: sessionsStatusInputSchema,
1177
+ annotations: SESSIONS_STATUS_MCP_ANNOTATIONS,
1178
+ },
1179
+ () => handleSessionsStatus(ctx)
1180
+ );
1181
+
1144
1182
  registerTool(
1145
1183
  "gno_search",
1146
1184
  {
@@ -1501,6 +1539,28 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
1501
1539
  })
1502
1540
  );
1503
1541
 
1542
+ registerTool(
1543
+ "gno_sessions_import",
1544
+ {
1545
+ description:
1546
+ "Manually import one owner-registered session source into the session archive and sync it for search. Takes a source ID only (never a host path); dryRun=true reports without writing. Returns a receipt with imported/updated/unchanged/skipped/incomplete/failed counts, lexical readiness and embedding backlog; partial imports stay visible and are retried by the next call.",
1547
+ inputSchema: sessionsImportInputSchema,
1548
+ annotations: SESSIONS_IMPORT_MCP_ANNOTATIONS,
1549
+ },
1550
+ (args) => handleSessionsImport(args, ctx)
1551
+ );
1552
+
1553
+ registerTool(
1554
+ "gno_sessions_automation_run",
1555
+ {
1556
+ description:
1557
+ "Run one owner-configured session automation profile now: imports its registered sources through the manual importer and records the outcome in automation status. Takes a profile ID only; it cannot enable hooks or schedules, add sources or change destinations. Returns the run outcome (complete, up_to_date, partial, failed, or not_started with a reason such as busy) and one import receipt per source.",
1558
+ inputSchema: sessionsAutomationRunInputSchema,
1559
+ annotations: SESSIONS_AUTOMATION_RUN_MCP_ANNOTATIONS,
1560
+ },
1561
+ (args) => handleSessionsAutomationRun(args, ctx)
1562
+ );
1563
+
1504
1564
  registerTool(
1505
1565
  "gno_capture",
1506
1566
  {
@@ -1513,6 +1573,19 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
1513
1573
  (args) => handleCapture(args, ctx)
1514
1574
  );
1515
1575
 
1576
+ registerTool(
1577
+ "gno_request_status",
1578
+ {
1579
+ description: describe(
1580
+ "gno_request_status",
1581
+ MCP_TOOL_DESCRIPTIONS.requestStatus
1582
+ ),
1583
+ inputSchema: requestStatusInputSchema,
1584
+ annotations: REQUEST_STATUS_MCP_ANNOTATIONS,
1585
+ },
1586
+ (args) => handleRequestStatus(args, ctx)
1587
+ );
1588
+
1516
1589
  registerTool(
1517
1590
  "gno_add_collection",
1518
1591
  {
@@ -14,6 +14,7 @@ import type { ToolContext } from "../server";
14
14
 
15
15
  import { type RememberResult } from "../../core/memory";
16
16
  import { MEMORY_MAX_FACT_BYTES } from "../../core/memory-record";
17
+ import { formatRequestReceiptLine } from "../../core/request-receipts";
17
18
  import { runTool, type ToolResult } from "./index";
18
19
  import {
19
20
  createMcpMemoryService,
@@ -22,6 +23,7 @@ import {
22
23
  resolveMcpMemoryIdentity,
23
24
  rethrowMemoryError,
24
25
  } from "./memory-shared";
26
+ import { requestIdInputSchema } from "./request-status";
25
27
 
26
28
  export const REMEMBER_MCP_ANNOTATIONS = {
27
29
  readOnlyHint: false,
@@ -92,6 +94,7 @@ export const rememberInputSchema = z.object({
92
94
  .min(1)
93
95
  .optional()
94
96
  .describe("Free-text evidence for the fact (where it came from)"),
97
+ requestId: requestIdInputSchema.optional(),
95
98
  });
96
99
 
97
100
  export type RememberToolInput = z.infer<typeof rememberInputSchema>;
@@ -124,7 +127,7 @@ export function formatRememberResult(result: RememberResult): string {
124
127
  lines.push(`Outcome: ${result.outcome}`);
125
128
  lines.push(`URI: ${result.record.uri}`);
126
129
  lines.push(`Hash: ${result.record.contentHash}`);
127
- lines.push(`Path: ${result.absPath}`);
130
+ if (result.absPath) lines.push(`Path: ${result.absPath}`);
128
131
  lines.push(`Sync: ${result.sync.status}`);
129
132
  if (result.record.supersedes.length > 0) {
130
133
  lines.push(`Supersedes: ${result.record.supersedes.join(", ")}`);
@@ -132,6 +135,9 @@ export function formatRememberResult(result: RememberResult): string {
132
135
  break;
133
136
  }
134
137
  lines.push(matching);
138
+ if ("request" in result && result.request) {
139
+ lines.push(formatRequestReceiptLine(result.request));
140
+ }
135
141
  return lines.join("\n");
136
142
  }
137
143
 
@@ -163,6 +169,7 @@ export function handleRemember(
163
169
  receipt: args.receipt,
164
170
  derivedFrom: args.derivedFrom,
165
171
  source: args.source,
172
+ requestId: args.requestId,
166
173
  });
167
174
  } catch (error) {
168
175
  return rethrowMemoryError(error);
@@ -19,6 +19,8 @@ import {
19
19
  MemoryService,
20
20
  } from "../../core/memory";
21
21
  import { MEMORY_MAX_SCOPES } from "../../core/memory-record";
22
+ import { requestLedgerPath } from "../../core/request-receipts";
23
+ import { mcpRequestNamespace, rethrowRequestError } from "./request-status";
22
24
 
23
25
  /** Caller name when the MCP client sent no implementation name. */
24
26
  const DEFAULT_MCP_CALLER = "mcp";
@@ -74,6 +76,10 @@ export function createMcpMemoryService(ctx: ToolContext): MemoryService {
74
76
  config: ctx.config,
75
77
  collections: ctx.collections,
76
78
  lockPath: ctx.writeLockPath,
79
+ requests: {
80
+ ledgerPath: requestLedgerPath(ctx.store.getDbPath()),
81
+ namespace: mcpRequestNamespace(ctx),
82
+ },
77
83
  });
78
84
  }
79
85
 
@@ -82,5 +88,5 @@ export function rethrowMemoryError(error: unknown): never {
82
88
  if (error instanceof MemoryError) {
83
89
  throw new Error(`${error.code}: ${error.message}`);
84
90
  }
85
- throw error;
91
+ return rethrowRequestError(error);
86
92
  }