@gmickel/gno 2.5.1 → 2.6.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 (105) hide show
  1. package/README.md +60 -5
  2. package/assets/skill/README.md +5 -1
  3. package/assets/skill/SKILL.md +75 -1
  4. package/assets/skill/cli-reference.md +123 -0
  5. package/assets/skill/examples.md +30 -0
  6. package/assets/skill/mcp-reference.md +52 -0
  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.6.0.zip} +0 -0
  13. package/browser-extension/artifacts/gno-browser-clipper-v2.6.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 +347 -24
  17. package/spec/mcp.md +198 -2
  18. package/spec/output-schemas/capture-receipt.schema.json +3 -0
  19. package/spec/output-schemas/mcp-capture-result.schema.json +3 -0
  20. package/spec/output-schemas/memory-remember.schema.json +8 -2
  21. package/spec/output-schemas/request-status.schema.json +113 -0
  22. package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
  23. package/spec/output-schemas/sessions-discovery.schema.json +38 -0
  24. package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
  25. package/spec/output-schemas/sessions-status.schema.json +432 -0
  26. package/src/cli/commands/ask.ts +14 -2
  27. package/src/cli/commands/capture.ts +55 -96
  28. package/src/cli/commands/daemon.ts +41 -0
  29. package/src/cli/commands/ls.ts +3 -0
  30. package/src/cli/commands/memory.ts +12 -3
  31. package/src/cli/commands/request-status.ts +59 -0
  32. package/src/cli/commands/reset.ts +39 -5
  33. package/src/cli/commands/sessions.ts +713 -0
  34. package/src/cli/commands/shared.ts +14 -1
  35. package/src/cli/program.ts +388 -1
  36. package/src/cli/session-binding.ts +49 -0
  37. package/src/config/types.ts +8 -0
  38. package/src/core/capture-publish.ts +239 -0
  39. package/src/core/capture-sync.ts +3 -0
  40. package/src/core/memory-remember.ts +233 -122
  41. package/src/core/memory-types.ts +11 -0
  42. package/src/core/network-boundary-inventory.ts +8 -0
  43. package/src/core/request-receipts.ts +671 -0
  44. package/src/index.ts +9 -0
  45. package/src/mcp/context.ts +8 -0
  46. package/src/mcp/http-egress.ts +4 -0
  47. package/src/mcp/http-transport.ts +2 -0
  48. package/src/mcp/tools/capture.ts +87 -83
  49. package/src/mcp/tools/index.ts +66 -0
  50. package/src/mcp/tools/memory-remember.ts +7 -0
  51. package/src/mcp/tools/memory-shared.ts +7 -1
  52. package/src/mcp/tools/request-status.ts +73 -0
  53. package/src/mcp/tools/sessions.ts +208 -0
  54. package/src/sdk/client.ts +180 -84
  55. package/src/sdk/index.ts +6 -0
  56. package/src/sdk/types.ts +54 -2
  57. package/src/serve/capture-service.ts +98 -32
  58. package/src/serve/config-sync.ts +3 -2
  59. package/src/serve/public/app.tsx +4 -1
  60. package/src/serve/public/components/CaptureModal.tsx +26 -8
  61. package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
  62. package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
  63. package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
  64. package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
  65. package/src/serve/public/components/sessions/api.ts +40 -0
  66. package/src/serve/public/components/sessions/snippet.tsx +53 -0
  67. package/src/serve/public/globals.built.css +1 -1
  68. package/src/serve/public/hooks/use-api.ts +10 -2
  69. package/src/serve/public/lib/request-intent.ts +69 -0
  70. package/src/serve/public/lib/workspace-actions.ts +12 -1
  71. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  72. package/src/serve/public/pages/Dashboard.tsx +10 -0
  73. package/src/serve/public/pages/DocView.tsx +15 -1
  74. package/src/serve/public/pages/DocumentEditor.tsx +139 -96
  75. package/src/serve/public/pages/Sessions.tsx +350 -0
  76. package/src/serve/resident-runtime.ts +43 -3
  77. package/src/serve/routes/api.ts +476 -147
  78. package/src/serve/routes/sessions.ts +766 -0
  79. package/src/serve/security.ts +9 -0
  80. package/src/serve/server.ts +205 -1
  81. package/src/serve/session-automation.ts +146 -0
  82. package/src/sessions/archive.ts +348 -0
  83. package/src/sessions/automation-state.ts +444 -0
  84. package/src/sessions/automation-status.ts +239 -0
  85. package/src/sessions/automation.ts +1169 -0
  86. package/src/sessions/binding.ts +105 -0
  87. package/src/sessions/claude-hook.ts +240 -0
  88. package/src/sessions/config.ts +176 -0
  89. package/src/sessions/format.ts +191 -0
  90. package/src/sessions/import-child-env.ts +8 -0
  91. package/src/sessions/import-child.ts +152 -0
  92. package/src/sessions/parsers/claude-code.ts +259 -0
  93. package/src/sessions/parsers/codex.ts +303 -0
  94. package/src/sessions/parsers/hermes.ts +248 -0
  95. package/src/sessions/parsers/openclaw.ts +496 -0
  96. package/src/sessions/parsers/shared.ts +184 -0
  97. package/src/sessions/sanitize.ts +222 -0
  98. package/src/sessions/service.ts +1533 -0
  99. package/src/sessions/setup.ts +477 -0
  100. package/src/sessions/sources.ts +518 -0
  101. package/src/sessions/state.ts +118 -0
  102. package/src/sessions/types.ts +457 -0
  103. package/src/store/sqlite/adapter.ts +54 -15
  104. package/src/store/sqlite/scoped-index.ts +9 -0
  105. 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>,
@@ -216,6 +221,7 @@ export function createToolContext(
216
221
  requestSnapshot.getStore()?.egress?.authorizationEpoch?.value,
217
222
  getEgressContext: () => requestSnapshot.getStore()?.egress,
218
223
  getRequestIdentity: () => requestSnapshot.getStore()?.requestIdentity,
224
+ getRequestNamespace: () => requestSnapshot.getStore()?.requestNamespace,
219
225
  runWithEgressContext<T>(
220
226
  egress: NonNullable<ToolContextSnapshot["egress"]>,
221
227
  operation: () => Promise<T>,
@@ -228,6 +234,7 @@ export function createToolContext(
228
234
  collections: config.collections,
229
235
  egress,
230
236
  requestIdentity: scope?.requestIdentity,
237
+ requestNamespace: scope?.requestNamespace,
231
238
  },
232
239
  operation
233
240
  );
@@ -241,6 +248,7 @@ export function createToolContext(
241
248
  collections: config.collections,
242
249
  egress: current?.egress,
243
250
  requestIdentity: current?.requestIdentity,
251
+ requestNamespace: current?.requestNamespace,
244
252
  },
245
253
  operation
246
254
  );
@@ -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();
@@ -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,6 +42,7 @@ 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 & {
@@ -46,6 +50,7 @@ type McpCaptureResult = CaptureReceipt & {
46
50
  absPath: string;
47
51
  overwritten: boolean;
48
52
  serverInstanceId: string;
53
+ request?: RequestReceiptInfo;
49
54
  };
50
55
 
51
56
  const SENSITIVE_SUBPATHS = new Set([
@@ -86,6 +91,7 @@ function formatCaptureResult(result: McpCaptureResult): string {
86
91
  if (result.tags.length > 0) {
87
92
  lines.push(`Tags: ${result.tags.join(", ")}`);
88
93
  }
94
+ if (result.request) lines.push(formatRequestReceiptLine(result.request));
89
95
  return lines.join("\n");
90
96
  }
91
97
 
@@ -112,7 +118,7 @@ function rethrowCaptureError(error: unknown): never {
112
118
  if (error instanceof CaptureSyncError) {
113
119
  throw new Error(`${error.code}: ${error.message}`);
114
120
  }
115
- throw error;
121
+ return rethrowRequestError(error);
116
122
  }
117
123
 
118
124
  export function handleCapture(
@@ -140,83 +146,81 @@ export function handleCapture(
140
146
  // Write + lexical sync complete under the shared write lease: the tool
141
147
  // succeeds only once the capture is retrievable (v1.38 contention
142
148
  // 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);
149
+ const input = buildSharedInput(args, collection.name);
150
+ let published: PublishedCapture;
151
+ try {
152
+ published = await publishCapture({
153
+ collection,
154
+ store: ctx.store,
155
+ lockPath: ctx.writeLockPath,
156
+ lockWaitMs: DEFAULT_LOCK_WAIT_MS,
157
+ config: ctx.config,
158
+ plan: async () => {
159
+ const existingDocs = await ctx.store.listDocuments(collectionName);
160
+ if (!existingDocs.ok) {
161
+ throw new Error(existingDocs.error.message);
186
162
  }
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}`
163
+ let plan: CapturePlan;
164
+ try {
165
+ plan = planCapture({
166
+ input,
167
+ existingRelPaths: existingDocs.value.map((doc) => doc.relPath),
168
+ diskRelPaths: await listCaptureDiskRelPaths(collection.path),
169
+ });
170
+ } catch (error) {
171
+ const message =
172
+ error instanceof Error ? error.message : String(error);
173
+ throw new Error(`${MCP_ERRORS.INVALID_INPUT.code}: ${message}`);
174
+ }
175
+ assertNotSensitive(plan.relPath);
176
+ return plan;
177
+ },
178
+ afterSync: async (synced, receipt) => {
179
+ if (synced.result) {
180
+ recordContentMutation(synced.result, ctx.markContentMutation);
181
+ }
182
+ const isMarkdown =
183
+ receipt.relPath.endsWith(".md") ||
184
+ receipt.relPath.endsWith(".markdown");
185
+ if (
186
+ !isMarkdown &&
187
+ !receipt.openedExisting &&
188
+ receipt.tags.length > 0
189
+ ) {
190
+ const tagResult = await ctx.store.setDocTags(
191
+ synced.documentId,
192
+ receipt.tags,
193
+ "user"
205
194
  );
195
+ if (!tagResult.ok) {
196
+ console.error(
197
+ `[MCP] Warning: Document created but tags not stored: ${tagResult.error.message}`
198
+ );
199
+ }
206
200
  }
207
- }
201
+ },
202
+ request:
203
+ args.requestId === undefined
204
+ ? undefined
205
+ : {
206
+ ledgerPath: requestLedgerPath(ctx.store.getDbPath()),
207
+ namespace: mcpRequestNamespace(ctx),
208
+ requestId: args.requestId,
209
+ input,
210
+ },
211
+ });
212
+ } catch (error) {
213
+ rethrowCaptureError(error);
214
+ }
208
215
 
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
- );
216
+ return {
217
+ ...published.receipt,
218
+ docid: published.receipt.docid ?? "",
219
+ absPath: published.receipt.absPath ?? "",
220
+ overwritten: published.receipt.overwritten ?? false,
221
+ serverInstanceId: ctx.serverInstanceId,
222
+ ...(published.request ? { request: published.request } : {}),
223
+ } as McpCaptureResult;
220
224
  },
221
225
  formatCaptureResult
222
226
  );
@@ -101,6 +101,12 @@ import { handleMultiGet } from "./multi-get";
101
101
  import { handlePeek, PEEK_MCP_ANNOTATIONS } from "./peek";
102
102
  import { handleQuery, handleQueryDiagnose } from "./query";
103
103
  import { handleRemoveCollection } from "./remove-collection";
104
+ import {
105
+ handleRequestStatus,
106
+ REQUEST_STATUS_MCP_ANNOTATIONS,
107
+ requestIdInputSchema,
108
+ requestStatusInputSchema,
109
+ } from "./request-status";
104
110
  import { handleSearch } from "./search";
105
111
  import {
106
112
  handleSection,
@@ -108,6 +114,17 @@ import {
108
114
  sectionInputSchema,
109
115
  sectionOutputSchema,
110
116
  } from "./sections";
117
+ import {
118
+ handleSessionsAutomationRun,
119
+ handleSessionsImport,
120
+ handleSessionsStatus,
121
+ SESSIONS_AUTOMATION_RUN_MCP_ANNOTATIONS,
122
+ SESSIONS_IMPORT_MCP_ANNOTATIONS,
123
+ SESSIONS_STATUS_MCP_ANNOTATIONS,
124
+ sessionsAutomationRunInputSchema,
125
+ sessionsImportInputSchema,
126
+ sessionsStatusInputSchema,
127
+ } from "./sessions";
111
128
  import { handleStatus } from "./status";
112
129
  import { handleSync } from "./sync";
113
130
  import {
@@ -175,6 +192,8 @@ export const MCP_TOOL_DESCRIPTIONS = {
175
192
  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
193
  recall:
177
194
  "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.",
195
+ requestStatus:
196
+ "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
197
  remember:
179
198
  "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
199
  } as const;
@@ -366,6 +385,7 @@ export const captureInputSchema = z.object({
366
385
  .describe(
367
386
  "Structured provenance metadata written under source frontmatter"
368
387
  ),
388
+ requestId: requestIdInputSchema.optional(),
369
389
  });
370
390
 
371
391
  const addCollectionInputSchema = z.object({
@@ -1141,6 +1161,17 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
1141
1161
  })
1142
1162
  );
1143
1163
 
1164
+ registerTool(
1165
+ "gno_sessions_status",
1166
+ {
1167
+ description:
1168
+ "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.",
1169
+ inputSchema: sessionsStatusInputSchema,
1170
+ annotations: SESSIONS_STATUS_MCP_ANNOTATIONS,
1171
+ },
1172
+ () => handleSessionsStatus(ctx)
1173
+ );
1174
+
1144
1175
  registerTool(
1145
1176
  "gno_search",
1146
1177
  {
@@ -1501,6 +1532,28 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
1501
1532
  })
1502
1533
  );
1503
1534
 
1535
+ registerTool(
1536
+ "gno_sessions_import",
1537
+ {
1538
+ description:
1539
+ "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.",
1540
+ inputSchema: sessionsImportInputSchema,
1541
+ annotations: SESSIONS_IMPORT_MCP_ANNOTATIONS,
1542
+ },
1543
+ (args) => handleSessionsImport(args, ctx)
1544
+ );
1545
+
1546
+ registerTool(
1547
+ "gno_sessions_automation_run",
1548
+ {
1549
+ description:
1550
+ "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.",
1551
+ inputSchema: sessionsAutomationRunInputSchema,
1552
+ annotations: SESSIONS_AUTOMATION_RUN_MCP_ANNOTATIONS,
1553
+ },
1554
+ (args) => handleSessionsAutomationRun(args, ctx)
1555
+ );
1556
+
1504
1557
  registerTool(
1505
1558
  "gno_capture",
1506
1559
  {
@@ -1513,6 +1566,19 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
1513
1566
  (args) => handleCapture(args, ctx)
1514
1567
  );
1515
1568
 
1569
+ registerTool(
1570
+ "gno_request_status",
1571
+ {
1572
+ description: describe(
1573
+ "gno_request_status",
1574
+ MCP_TOOL_DESCRIPTIONS.requestStatus
1575
+ ),
1576
+ inputSchema: requestStatusInputSchema,
1577
+ annotations: REQUEST_STATUS_MCP_ANNOTATIONS,
1578
+ },
1579
+ (args) => handleRequestStatus(args, ctx)
1580
+ );
1581
+
1516
1582
  registerTool(
1517
1583
  "gno_add_collection",
1518
1584
  {
@@ -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>;
@@ -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
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * MCP request receipts: the caller's namespace, error rethrow, and the
3
+ * bounded `gno_request_status` lookup.
4
+ *
5
+ * @module src/mcp/tools/request-status
6
+ */
7
+
8
+ import { z } from "zod";
9
+
10
+ import type { ToolContext } from "../server";
11
+
12
+ import {
13
+ formatRequestStatus,
14
+ LOCAL_OWNER_NAMESPACE,
15
+ readRequestStatus,
16
+ RequestReceiptError,
17
+ requestLedgerPath,
18
+ } from "../../core/request-receipts";
19
+ import { runTool, type ToolResult } from "./index";
20
+
21
+ export const REQUEST_STATUS_MCP_ANNOTATIONS = {
22
+ readOnlyHint: true,
23
+ destructiveHint: false,
24
+ idempotentHint: true,
25
+ openWorldHint: false,
26
+ } as const;
27
+
28
+ export const requestIdInputSchema = z
29
+ .string()
30
+ .describe(
31
+ "Optional caller request ID (1-128 of A-Z a-z 0-9 . _ : -). Reuse the same ID when retrying the same write after a lost response; a new intent needs a new ID"
32
+ );
33
+
34
+ export const requestStatusInputSchema = z.object({
35
+ requestId: z
36
+ .string()
37
+ .describe("Request ID previously sent with gno_capture or gno_remember"),
38
+ });
39
+
40
+ /** HTTP callers get their authorized identity's namespace; stdio is the local owner. */
41
+ export function mcpRequestNamespace(ctx: ToolContext): string {
42
+ return ctx.getRequestNamespace?.() ?? LOCAL_OWNER_NAMESPACE;
43
+ }
44
+
45
+ /** Re-throw a request receipt error in the `CODE: message` shape runTool parses. */
46
+ export function rethrowRequestError(error: unknown): never {
47
+ if (error instanceof RequestReceiptError) {
48
+ throw new Error(`${error.code}: ${error.message}`);
49
+ }
50
+ throw error;
51
+ }
52
+
53
+ export function handleRequestStatus(
54
+ args: z.infer<typeof requestStatusInputSchema>,
55
+ ctx: ToolContext
56
+ ): Promise<ToolResult> {
57
+ return runTool(
58
+ ctx,
59
+ "gno_request_status",
60
+ async () => {
61
+ try {
62
+ return await readRequestStatus({
63
+ ledgerPath: requestLedgerPath(ctx.store.getDbPath()),
64
+ namespace: mcpRequestNamespace(ctx),
65
+ requestId: args.requestId,
66
+ });
67
+ } catch (error) {
68
+ return rethrowRequestError(error);
69
+ }
70
+ },
71
+ formatRequestStatus
72
+ );
73
+ }