@gmickel/gno 2.4.0 → 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 (136) hide show
  1. package/README.md +69 -6
  2. package/assets/skill/README.md +5 -1
  3. package/assets/skill/SKILL.md +98 -1
  4. package/assets/skill/cli-reference.md +140 -0
  5. package/assets/skill/examples.md +45 -0
  6. package/assets/skill/mcp-reference.md +62 -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.4.0.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 +358 -24
  17. package/spec/compiled-context.md +68 -0
  18. package/spec/mcp.md +223 -2
  19. package/spec/output-schemas/capture-receipt.schema.json +3 -0
  20. package/spec/output-schemas/compiled-context-check.schema.json +44 -0
  21. package/spec/output-schemas/compiled-context-file.schema.json +165 -0
  22. package/spec/output-schemas/compiled-context-preview.schema.json +142 -0
  23. package/spec/output-schemas/mcp-capture-result.schema.json +3 -0
  24. package/spec/output-schemas/memory-remember.schema.json +8 -2
  25. package/spec/output-schemas/request-status.schema.json +113 -0
  26. package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
  27. package/spec/output-schemas/sessions-discovery.schema.json +38 -0
  28. package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
  29. package/spec/output-schemas/sessions-status.schema.json +432 -0
  30. package/src/app/compiled-context-files.ts +361 -0
  31. package/src/app/compiled-context.ts +240 -0
  32. package/src/app/context-surface.ts +7 -2
  33. package/src/cli/commands/ask.ts +14 -2
  34. package/src/cli/commands/audit.ts +4 -0
  35. package/src/cli/commands/capture.ts +55 -96
  36. package/src/cli/commands/context-compiled.ts +136 -0
  37. package/src/cli/commands/daemon.ts +41 -0
  38. package/src/cli/commands/ls.ts +3 -0
  39. package/src/cli/commands/memory.ts +12 -3
  40. package/src/cli/commands/request-status.ts +59 -0
  41. package/src/cli/commands/reset.ts +39 -5
  42. package/src/cli/commands/sessions.ts +713 -0
  43. package/src/cli/commands/shared.ts +14 -1
  44. package/src/cli/errors.ts +10 -3
  45. package/src/cli/program.ts +458 -1
  46. package/src/cli/session-binding.ts +49 -0
  47. package/src/config/types.ts +8 -0
  48. package/src/core/capture-publish.ts +239 -0
  49. package/src/core/capture-sync.ts +3 -0
  50. package/src/core/compiled-context.ts +254 -0
  51. package/src/core/context-budget.ts +2 -10
  52. package/src/core/file-lock.ts +22 -5
  53. package/src/core/folder-setup-planning.ts +2 -1
  54. package/src/core/memory-remember.ts +233 -122
  55. package/src/core/memory-types.ts +11 -0
  56. package/src/core/network-boundary-inventory.ts +16 -0
  57. package/src/core/request-receipts.ts +671 -0
  58. package/src/core/setup-receipt.ts +27 -20
  59. package/src/core/typed-metadata.ts +4 -0
  60. package/src/core/validation.ts +9 -2
  61. package/src/core/windows-private-path.ts +96 -0
  62. package/src/index.ts +11 -2
  63. package/src/ingestion/compiled-context.ts +15 -0
  64. package/src/ingestion/sync.ts +40 -8
  65. package/src/ingestion/walker.ts +5 -4
  66. package/src/llm/nodeLlamaCpp/simulator-install.ts +6 -2
  67. package/src/mcp/context.ts +8 -0
  68. package/src/mcp/http-egress.ts +15 -3
  69. package/src/mcp/http-transport.ts +2 -0
  70. package/src/mcp/retrieval-warnings.ts +24 -0
  71. package/src/mcp/tools/ask.ts +14 -1
  72. package/src/mcp/tools/capture.ts +87 -83
  73. package/src/mcp/tools/context.ts +46 -2
  74. package/src/mcp/tools/index.ts +119 -7
  75. package/src/mcp/tools/memory-remember.ts +7 -0
  76. package/src/mcp/tools/memory-shared.ts +7 -1
  77. package/src/mcp/tools/query.ts +3 -1
  78. package/src/mcp/tools/request-status.ts +73 -0
  79. package/src/mcp/tools/search.ts +3 -1
  80. package/src/mcp/tools/sessions.ts +208 -0
  81. package/src/mcp/tools/vsearch.ts +3 -1
  82. package/src/sdk/client.ts +252 -89
  83. package/src/sdk/index.ts +13 -0
  84. package/src/sdk/types.ts +82 -3
  85. package/src/serve/capture-service.ts +98 -32
  86. package/src/serve/compiled-context.ts +84 -0
  87. package/src/serve/config-sync.ts +3 -2
  88. package/src/serve/public/app.tsx +15 -1
  89. package/src/serve/public/components/CaptureModal.tsx +26 -8
  90. package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
  91. package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
  92. package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
  93. package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
  94. package/src/serve/public/components/sessions/api.ts +40 -0
  95. package/src/serve/public/components/sessions/snippet.tsx +53 -0
  96. package/src/serve/public/globals.built.css +1 -1
  97. package/src/serve/public/hooks/use-api.ts +10 -2
  98. package/src/serve/public/lib/request-intent.ts +69 -0
  99. package/src/serve/public/lib/workspace-actions.ts +12 -1
  100. package/src/serve/public/lib/workspace-tabs.ts +4 -0
  101. package/src/serve/public/pages/CompiledContext.tsx +364 -0
  102. package/src/serve/public/pages/Dashboard.tsx +17 -0
  103. package/src/serve/public/pages/DocView.tsx +15 -1
  104. package/src/serve/public/pages/DocumentEditor.tsx +139 -96
  105. package/src/serve/public/pages/Sessions.tsx +350 -0
  106. package/src/serve/resident-runtime.ts +43 -3
  107. package/src/serve/routes/api.ts +476 -147
  108. package/src/serve/routes/sessions.ts +766 -0
  109. package/src/serve/security.ts +9 -0
  110. package/src/serve/server.ts +248 -3
  111. package/src/serve/session-automation.ts +146 -0
  112. package/src/serve/spa-production-build.ts +6 -5
  113. package/src/sessions/archive.ts +348 -0
  114. package/src/sessions/automation-state.ts +444 -0
  115. package/src/sessions/automation-status.ts +239 -0
  116. package/src/sessions/automation.ts +1169 -0
  117. package/src/sessions/binding.ts +105 -0
  118. package/src/sessions/claude-hook.ts +240 -0
  119. package/src/sessions/config.ts +176 -0
  120. package/src/sessions/format.ts +191 -0
  121. package/src/sessions/import-child-env.ts +8 -0
  122. package/src/sessions/import-child.ts +152 -0
  123. package/src/sessions/parsers/claude-code.ts +259 -0
  124. package/src/sessions/parsers/codex.ts +303 -0
  125. package/src/sessions/parsers/hermes.ts +248 -0
  126. package/src/sessions/parsers/openclaw.ts +496 -0
  127. package/src/sessions/parsers/shared.ts +184 -0
  128. package/src/sessions/sanitize.ts +222 -0
  129. package/src/sessions/service.ts +1533 -0
  130. package/src/sessions/setup.ts +477 -0
  131. package/src/sessions/sources.ts +518 -0
  132. package/src/sessions/state.ts +118 -0
  133. package/src/sessions/types.ts +457 -0
  134. package/src/store/sqlite/adapter.ts +68 -16
  135. package/src/store/sqlite/scoped-index.ts +9 -0
  136. package/browser-extension/artifacts/gno-browser-clipper-v2.4.0.zip.sha256 +0 -1
@@ -4,30 +4,28 @@
4
4
  * @module src/cli/commands/capture
5
5
  */
6
6
 
7
- // node:fs/promises for mkdir (no Bun equivalent for recursive dir creation)
8
- import { mkdir } from "node:fs/promises";
9
- // node:path has no Bun path utilities
10
- import { dirname, join } from "node:path";
11
-
12
7
  import type {
13
- CapturePlan,
8
+ CaptureInput,
14
9
  CaptureReceipt,
15
10
  CaptureSourceKind,
16
11
  } from "../../core/capture";
17
12
  import type { NoteCollisionPolicy } from "../../core/note-creation";
18
13
  import type { NotePresetId } from "../../core/note-presets";
14
+ import type { RequestReceiptInfo } from "../../core/request-receipts";
19
15
 
20
16
  import { getIndexDbPath } from "../../app/constants";
21
17
  import {
22
- buildCaptureReceipt,
23
18
  listCaptureDiskRelPaths,
24
19
  planCapture,
25
20
  serializeCaptureReceipt,
26
21
  } from "../../core/capture";
27
- import { writeCapturePlanFile } from "../../core/capture-write";
28
- import { withWriteLock } from "../../core/file-lock";
29
- import { defaultSyncService, withContentTypeRules } from "../../ingestion";
22
+ import { publishCapture } from "../../core/capture-publish";
23
+ import {
24
+ formatRequestReceiptLine,
25
+ localRequestLedger,
26
+ } from "../../core/request-receipts";
30
27
  import { CliError } from "../errors";
28
+ import { requestErrorToCli } from "./request-status";
31
29
  import { initStore } from "./shared";
32
30
 
33
31
  export interface CaptureCliOptions {
@@ -49,6 +47,7 @@ export interface CaptureCliOptions {
49
47
  sourceAuthor?: string;
50
48
  sourceDate?: string;
51
49
  sourceId?: string;
50
+ requestId?: string;
52
51
  }
53
52
 
54
53
  const SOURCE_KINDS = new Set<CaptureSourceKind>([
@@ -130,7 +129,7 @@ function validateCollisionPolicy(
130
129
 
131
130
  export async function capture(
132
131
  options: CaptureCliOptions
133
- ): Promise<CaptureReceipt> {
132
+ ): Promise<CaptureReceipt & { request?: RequestReceiptInfo }> {
134
133
  const storeInit = await initStore({
135
134
  configPath: options.configPath,
136
135
  indexName: options.indexName,
@@ -160,102 +159,61 @@ export async function capture(
160
159
 
161
160
  validateCollisionPolicy(options.collisionPolicy);
162
161
  const content = await readContent(options);
163
- const existingDocs = await store.listDocuments(collection.name);
164
- if (!existingDocs.ok) {
165
- throw new Error(existingDocs.error.message);
166
- }
167
- const diskRelPaths = await listCaptureDiskRelPaths(collection.path);
168
- let plan: CapturePlan;
162
+ const input: CaptureInput = {
163
+ collection: collection.name,
164
+ content,
165
+ title: options.title,
166
+ relPath: options.path,
167
+ folderPath: options.folder,
168
+ collisionPolicy: options.collisionPolicy,
169
+ presetId: options.preset,
170
+ tags: parseTags(options.tags),
171
+ source: buildSource(options),
172
+ };
173
+ const ledger = localRequestLedger(getIndexDbPath(options.indexName));
169
174
  try {
170
- plan = planCapture({
171
- input: {
172
- collection: collection.name,
173
- content,
174
- title: options.title,
175
- relPath: options.path,
176
- folderPath: options.folder,
177
- collisionPolicy: options.collisionPolicy,
178
- presetId: options.preset,
179
- tags: parseTags(options.tags),
180
- source: buildSource(options),
175
+ const published = await publishCapture({
176
+ collection,
177
+ store,
178
+ lockPath: ledger.lockPath,
179
+ reportSyncFailure: true,
180
+ config,
181
+ plan: async () => {
182
+ const existingDocs = await store.listDocuments(collection.name);
183
+ if (!existingDocs.ok) {
184
+ throw new Error(existingDocs.error.message);
185
+ }
186
+ try {
187
+ return planCapture({
188
+ input,
189
+ existingRelPaths: existingDocs.value.map((doc) => doc.relPath),
190
+ diskRelPaths: await listCaptureDiskRelPaths(collection.path),
191
+ });
192
+ } catch (error) {
193
+ throw new CliError(
194
+ "VALIDATION",
195
+ error instanceof Error ? error.message : String(error)
196
+ );
197
+ }
181
198
  },
182
- existingRelPaths: existingDocs.value.map((doc) => doc.relPath),
183
- diskRelPaths,
199
+ request:
200
+ options.requestId === undefined
201
+ ? undefined
202
+ : { ...ledger, requestId: options.requestId, input },
184
203
  });
204
+ return published.request
205
+ ? { ...published.receipt, request: published.request }
206
+ : published.receipt;
185
207
  } catch (error) {
186
- throw new CliError(
187
- "VALIDATION",
188
- error instanceof Error ? error.message : String(error)
189
- );
208
+ throw requestErrorToCli(error);
190
209
  }
191
- const absPath = join(collection.path, plan.relPath);
192
-
193
- const lockPath = join(
194
- dirname(getIndexDbPath(options.indexName)),
195
- ".mcp-write.lock"
196
- );
197
- return await withWriteLock(lockPath, async () => {
198
- if (plan.openedExisting) {
199
- const existingDoc = await store.getDocument(
200
- collection.name,
201
- plan.relPath
202
- );
203
- if (!existingDoc.ok) {
204
- throw new Error(existingDoc.error.message);
205
- }
206
- return buildCaptureReceipt({
207
- plan,
208
- absPath,
209
- docid: existingDoc.value?.docid,
210
- sync: existingDoc.value
211
- ? { status: "completed" }
212
- : {
213
- status: "skipped",
214
- reason: "Existing file is not indexed yet.",
215
- },
216
- });
217
- }
218
-
219
- await mkdir(dirname(absPath), { recursive: true });
220
- await writeCapturePlanFile(plan, absPath);
221
- const syncResults = await defaultSyncService.syncFiles(
222
- collection,
223
- store,
224
- [plan.relPath],
225
- withContentTypeRules(
226
- {
227
- runUpdateCmd: false,
228
- gitPull: false,
229
- },
230
- config
231
- )
232
- );
233
- const syncResult = syncResults[0];
234
- const docResult = await store.getDocument(collection.name, plan.relPath);
235
- const docid = docResult.ok ? docResult.value?.docid : undefined;
236
- return buildCaptureReceipt({
237
- plan,
238
- absPath,
239
- docid: syncResult?.docid ?? docid,
240
- sync:
241
- syncResult?.status === "error"
242
- ? {
243
- status: "failed",
244
- error:
245
- syncResult.errorMessage ??
246
- syncResult.errorCode ??
247
- "Unknown sync error",
248
- }
249
- : { status: "completed" },
250
- });
251
- });
252
210
  } finally {
253
211
  await store.close();
254
212
  }
255
213
  }
256
214
 
257
215
  export function formatCaptureReceipt(
258
- receipt: CaptureReceipt,
216
+ receipt: CaptureReceipt & { request?: RequestReceiptInfo },
259
217
  options: { json?: boolean; quiet?: boolean } = {}
260
218
  ): string {
261
219
  if (options.json) {
@@ -278,5 +236,6 @@ export function formatCaptureReceipt(
278
236
  if (receipt.source.url) {
279
237
  lines.push(`Source: ${receipt.source.url}`);
280
238
  }
239
+ if (receipt.request) lines.push(formatRequestReceiptLine(receipt.request));
281
240
  return lines.join("\n");
282
241
  }
@@ -0,0 +1,136 @@
1
+ import { previewCompiledContext } from "../../app/compiled-context";
2
+ import {
3
+ compileContextFile,
4
+ checkContextFile,
5
+ refreshContextFile,
6
+ } from "../../app/compiled-context-files";
7
+ /** Explicit local compiled context operations. */
8
+ import { createGnoClient } from "../../sdk/client";
9
+ import { CliError } from "../errors";
10
+ import { contextCliError } from "./context-build";
11
+ import { initStore } from "./shared";
12
+
13
+ export interface CompiledCommandOptions {
14
+ configPath?: string;
15
+ indexName?: string;
16
+ capsulePath?: string;
17
+ capsuleOutputPath?: string;
18
+ outputPath?: string;
19
+ budgetTokens?: number;
20
+ budgetBytes?: number;
21
+ json?: boolean;
22
+ }
23
+
24
+ export async function contextCompiled(
25
+ operation: "preview" | "compile" | "check" | "refresh",
26
+ options: CompiledCommandOptions
27
+ ): Promise<{
28
+ output: string;
29
+ status?: "current" | "stale" | "conflict" | "unverifiable";
30
+ }> {
31
+ const initialized = await initStore({
32
+ configPath: options.configPath,
33
+ indexName: options.indexName,
34
+ syncConfig: false,
35
+ allowEmptyCollections: true,
36
+ });
37
+ if (!initialized.ok) throw new CliError("RUNTIME", initialized.error);
38
+ const deps = {
39
+ store: initialized.store,
40
+ config: initialized.config,
41
+ indexName: options.indexName,
42
+ };
43
+ try {
44
+ if (operation === "check") {
45
+ if (!options.outputPath)
46
+ throw new CliError("VALIDATION", "Artifact path required");
47
+ const result = await checkContextFile(
48
+ { outputPath: options.outputPath, capsulePath: options.capsulePath },
49
+ deps
50
+ );
51
+ return {
52
+ output: options.json
53
+ ? JSON.stringify(result)
54
+ : `${result.status}: ${result.reasons.join("; ") || "verified current"}`,
55
+ status: result.status,
56
+ };
57
+ }
58
+ if (operation === "refresh") {
59
+ if (!options.outputPath || !options.capsuleOutputPath)
60
+ throw new CliError(
61
+ "VALIDATION",
62
+ "Artifact and Capsule output paths required"
63
+ );
64
+ const result = await refreshContextFile(
65
+ {
66
+ outputPath: options.outputPath,
67
+ capsuleOutputPath: options.capsuleOutputPath,
68
+ },
69
+ deps,
70
+ async (request) => {
71
+ const client = await createGnoClient({
72
+ configPath: options.configPath,
73
+ indexName: options.indexName,
74
+ });
75
+ try {
76
+ return await client.context(request);
77
+ } finally {
78
+ await client.close();
79
+ }
80
+ }
81
+ );
82
+ return {
83
+ output: options.json
84
+ ? JSON.stringify(result)
85
+ : `${result.status}: ${result.outputPath}`,
86
+ };
87
+ }
88
+ if (!options.capsulePath || !options.budgetTokens)
89
+ throw new CliError("VALIDATION", "Capsule path and budget required");
90
+ if (operation === "compile") {
91
+ if (!options.outputPath)
92
+ throw new CliError("VALIDATION", "Output path required");
93
+ const result = await compileContextFile(
94
+ {
95
+ capsulePath: options.capsulePath,
96
+ outputPath: options.outputPath,
97
+ budgetTokens: options.budgetTokens,
98
+ budgetBytes: options.budgetBytes,
99
+ },
100
+ deps
101
+ );
102
+ return {
103
+ output: options.json
104
+ ? JSON.stringify(result)
105
+ : `${result.status}: ${result.outputPath}`,
106
+ };
107
+ }
108
+ const source = Bun.file(options.capsulePath);
109
+ if (source.size > 4 * 1024 * 1024)
110
+ throw new CliError("VALIDATION", "Capsule exceeds byte limit");
111
+ const capsule: unknown = await source.json();
112
+ const result = await previewCompiledContext(
113
+ {
114
+ capsule,
115
+ budgetTokens: options.budgetTokens,
116
+ budgetBytes: options.budgetBytes,
117
+ },
118
+ deps
119
+ );
120
+ return { output: options.json ? JSON.stringify(result) : result.markdown };
121
+ } catch (error) {
122
+ if (error instanceof CliError) throw error;
123
+ if (
124
+ error instanceof SyntaxError ||
125
+ (error instanceof Error && error.name === "ZodError")
126
+ ) {
127
+ throw new CliError(
128
+ "VALIDATION",
129
+ "Invalid compiled context input; provide a valid Capsule and budget"
130
+ );
131
+ }
132
+ throw contextCliError(error);
133
+ } finally {
134
+ await initialized.store.close();
135
+ }
136
+ }
@@ -3,6 +3,7 @@ import type { HttpGatewayOverrides } from "../../mcp/http-security";
3
3
  import type { BackgroundRuntimeResult } from "../../serve/background-runtime";
4
4
  import type { FindingsPassResult } from "../../serve/findings-pass";
5
5
  import type { ResidentRuntime } from "../../serve/resident-runtime";
6
+ import type { SessionAutomationRunResult } from "../../sessions/types";
6
7
 
7
8
  import {
8
9
  enforceCollectionEgress,
@@ -17,6 +18,7 @@ import {
17
18
  import { startBackgroundRuntime } from "../../serve/background-runtime";
18
19
  import { handleResidentStatus, handleStatus } from "../../serve/routes/api";
19
20
  import { createMcpHttpGateway } from "../../serve/routes/mcp";
21
+ import { SessionsError } from "../../sessions/types";
20
22
 
21
23
  export interface DaemonOptions extends HttpGatewayOverrides {
22
24
  configPath?: string;
@@ -67,6 +69,31 @@ export function logFindingsPassResult(
67
69
  );
68
70
  }
69
71
 
72
+ /** Content-free: counts and reason codes only; clean no-ops stay silent. */
73
+ export function logSessionAutomationResult(
74
+ result: SessionAutomationRunResult,
75
+ logger: DaemonLogger,
76
+ options: { quiet?: boolean }
77
+ ): void {
78
+ if (!result.ran) return;
79
+ if (result.outcome === "failed") {
80
+ logger.error(
81
+ `session automation ${result.profileId}: failed (${result.reason ?? "unknown"})`
82
+ );
83
+ return;
84
+ }
85
+ if (result.outcome === "up_to_date" || options.quiet) return;
86
+ let imported = 0;
87
+ let updated = 0;
88
+ for (const receipt of result.receipts) {
89
+ imported += receipt.counts.imported;
90
+ updated += receipt.counts.updated;
91
+ }
92
+ logger.log(
93
+ `session automation ${result.profileId}: ${result.outcome}${result.reason ? ` (${result.reason})` : ""}; ${imported} threads imported, ${updated} updated`
94
+ );
95
+ }
96
+
70
97
  function formatCollectionSyncSummary(result: CollectionSyncResult): string {
71
98
  return `${result.collection}: ${result.filesAdded} added, ${result.filesUpdated} updated, ${result.filesUnchanged} unchanged, ${result.filesErrored} errors`;
72
99
  }
@@ -175,6 +202,20 @@ export async function daemon(
175
202
  quiet: options.quiet,
176
203
  verbose: options.verbose,
177
204
  }),
205
+ onSessionAutomationResult: (result) =>
206
+ logSessionAutomationResult(result, logger, { quiet: options.quiet }),
207
+ onSessionAutomationError: (error) => {
208
+ // Busy state (a run or state change in progress) is retried next tick.
209
+ if (error instanceof SessionsError && error.code === "SESSIONS_BUSY") {
210
+ if (options.verbose) {
211
+ logger.log("session automation: run in progress; next tick retries");
212
+ }
213
+ return;
214
+ }
215
+ logger.error(
216
+ `session automation tick failed: ${error instanceof SessionsError ? error.code : "runtime error"}`
217
+ );
218
+ },
178
219
  watchCallbacks: {
179
220
  onSyncStart: ({ collection, relPaths }) => {
180
221
  if (!options.quiet) {
@@ -14,6 +14,8 @@ import { initStore } from "./shared";
14
14
  // ─────────────────────────────────────────────────────────────────────────────
15
15
 
16
16
  export interface LsCommandOptions {
17
+ /** Index name (defaults to "default"). */
18
+ indexName?: string;
17
19
  /** Override config path */
18
20
  configPath?: string;
19
21
  /** Max results (default 20) */
@@ -111,6 +113,7 @@ export async function ls(
111
113
 
112
114
  const initResult = await initStore({
113
115
  configPath: options.configPath,
116
+ indexName: options.indexName,
114
117
  syncConfig: false,
115
118
  });
116
119
  if (!initResult.ok) {
@@ -24,11 +24,15 @@ import {
24
24
  type RememberInput,
25
25
  type RememberResult,
26
26
  } from "../../core/memory";
27
- import { writeLeasePath } from "../../core/write-lease";
27
+ import {
28
+ formatRequestReceiptLine,
29
+ localRequestLedger,
30
+ } from "../../core/request-receipts";
28
31
  import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
29
32
  import { resolveModelUri } from "../../llm/registry";
30
33
  import { createVectorIndexPort } from "../../store/vector";
31
34
  import { CliError, type CliErrorCode } from "../errors";
35
+ import { requestErrorToCli } from "./request-status";
32
36
  import { initStore } from "./shared";
33
37
 
34
38
  /** Environment overrides for the identity defaults. */
@@ -62,6 +66,7 @@ export interface RememberCliOptions
62
66
  receipt?: string;
63
67
  derivedFrom?: string[];
64
68
  source?: string;
69
+ requestId?: string;
65
70
  }
66
71
 
67
72
  export interface RecallCliOptions
@@ -254,7 +259,7 @@ const MEMORY_ERROR_TO_CLI: Record<MemoryErrorCode, CliErrorCode> = {
254
259
 
255
260
  /** Map a core `MemoryError` onto the CLI error model (code carried in details). */
256
261
  export function toCliError(error: unknown): unknown {
257
- if (!(error instanceof MemoryError)) return error;
262
+ if (!(error instanceof MemoryError)) return requestErrorToCli(error);
258
263
  return new CliError(MEMORY_ERROR_TO_CLI[error.code], error.message, {
259
264
  details: { memoryCode: error.code },
260
265
  });
@@ -322,11 +327,13 @@ async function openMemoryRuntime(
322
327
  await embedResult.value.dispose();
323
328
  }
324
329
  }
330
+ const ledger = localRequestLedger(getIndexDbPath(options.indexName));
325
331
  const service = new MemoryService({
326
332
  store,
327
333
  config,
328
334
  collections,
329
- lockPath: writeLeasePath(getIndexDbPath(options.indexName)),
335
+ lockPath: ledger.lockPath,
336
+ requests: ledger,
330
337
  embedPort,
331
338
  vectorIndex,
332
339
  });
@@ -361,6 +368,7 @@ export async function remember(
361
368
  ? options.derivedFrom
362
369
  : undefined,
363
370
  source: options.source,
371
+ requestId: options.requestId,
364
372
  };
365
373
  return await runtime.service.remember(input);
366
374
  } catch (error) {
@@ -457,6 +465,7 @@ export function formatRememberResult(
457
465
  lines.push(`Sync: ${result.sync.status}`);
458
466
  }
459
467
  lines.push(`Matching: ${formatMatching(result.matching)}`);
468
+ if (result.request) lines.push(formatRequestReceiptLine(result.request));
460
469
  return lines.join("\n");
461
470
  }
462
471
 
@@ -0,0 +1,59 @@
1
+ /**
2
+ * gno request-status: look up a write request ID before retrying it, plus
3
+ * the CLI mapping for request receipt errors.
4
+ *
5
+ * @module src/cli/commands/request-status
6
+ */
7
+
8
+ import { getIndexDbPath } from "../../app/constants";
9
+ import {
10
+ formatRequestStatus,
11
+ localRequestLedger,
12
+ readRequestStatus,
13
+ RequestReceiptError,
14
+ type RequestReceiptErrorCode,
15
+ type RequestStatusResult,
16
+ } from "../../core/request-receipts";
17
+ import { CliError, type CliErrorCode } from "../errors";
18
+
19
+ const REQUEST_ERROR_TO_CLI: Record<RequestReceiptErrorCode, CliErrorCode> = {
20
+ REQUEST_ID_INVALID: "VALIDATION",
21
+ REQUEST_ID_CONFLICT: "VALIDATION",
22
+ REQUEST_EXPIRED: "VALIDATION",
23
+ // Accepted and still in progress elsewhere: exit 4 like lease contention.
24
+ REQUEST_PENDING: "BUSY",
25
+ REQUEST_RECOVERY_CONFLICT: "RUNTIME",
26
+ REQUEST_CAPACITY_EXHAUSTED: "RUNTIME",
27
+ REQUEST_LEDGER_UNAVAILABLE: "RUNTIME",
28
+ };
29
+
30
+ /** Map a request receipt error onto the CLI error model (code in details). */
31
+ export function requestErrorToCli(error: unknown): unknown {
32
+ if (!(error instanceof RequestReceiptError)) return error;
33
+ return new CliError(REQUEST_ERROR_TO_CLI[error.code], error.message, {
34
+ details: { requestCode: error.code },
35
+ });
36
+ }
37
+
38
+ export async function requestStatus(options: {
39
+ requestId: string;
40
+ indexName?: string;
41
+ }): Promise<RequestStatusResult> {
42
+ try {
43
+ return await readRequestStatus({
44
+ ...localRequestLedger(getIndexDbPath(options.indexName)),
45
+ requestId: options.requestId,
46
+ });
47
+ } catch (error) {
48
+ throw requestErrorToCli(error);
49
+ }
50
+ }
51
+
52
+ export function formatRequestStatusOutput(
53
+ result: RequestStatusResult,
54
+ options: { json?: boolean } = {}
55
+ ): string {
56
+ return options.json
57
+ ? JSON.stringify(result, null, 2)
58
+ : formatRequestStatus(result);
59
+ }
@@ -1,17 +1,20 @@
1
1
  /**
2
2
  * gno reset - Reset GNO to fresh state
3
3
  *
4
- * Deletes all config, data, and cache directories.
4
+ * Deletes all config, data, and cache directories. The request ledger
5
+ * (`<dataDir>/write-receipts/`) is kept so request IDs that already ran
6
+ * can never run again.
5
7
  */
6
8
 
7
9
  // node:fs/promises: rm and stat for recursive directory deletion (no Bun equivalent)
8
- import { rm, stat } from "node:fs/promises";
10
+ import { readdir, rm, stat } from "node:fs/promises";
9
11
  // node:os: homedir for platform-agnostic home directory (no Bun equivalent)
10
12
  import { homedir } from "node:os";
11
13
  // node:path: path manipulation utilities (no Bun equivalent)
12
- import { isAbsolute, normalize, sep } from "node:path";
14
+ import { isAbsolute, join, normalize, sep } from "node:path";
13
15
 
14
16
  import { resolveDirs } from "../../app/constants";
17
+ import { REQUEST_LEDGER_DIR } from "../../core/request-receipts";
15
18
  import { CliError } from "../errors";
16
19
 
17
20
  interface ResetOptions {
@@ -103,8 +106,8 @@ export async function reset(options: ResetOptions): Promise<ResetResult> {
103
106
  assertSafePath(dirs.cache, "Cache");
104
107
  }
105
108
 
106
- // Delete data directory (always, contains index DB)
107
- results.push(await rmDir(dirs.data));
109
+ // Empty the data directory (index DBs, caches) but keep the request ledger.
110
+ results.push(...(await clearDataDir(dirs.data)));
108
111
 
109
112
  // Delete config unless --keep-config
110
113
  if (options.keepConfig) {
@@ -134,6 +137,37 @@ export async function reset(options: ResetOptions): Promise<ResetResult> {
134
137
  return { results, errors };
135
138
  }
136
139
 
140
+ /** Empty the data directory except the request ledger (what reset runs). */
141
+ export async function clearDataDir(path: string): Promise<DirResult[]> {
142
+ let entries: string[];
143
+ try {
144
+ entries = await readdir(path);
145
+ } catch (e) {
146
+ const err = e as NodeJS.ErrnoException;
147
+ return [
148
+ err.code === "ENOENT"
149
+ ? { path, status: "missing" }
150
+ : { path, status: "missing", error: err.message },
151
+ ];
152
+ }
153
+ if (!entries.includes(REQUEST_LEDGER_DIR)) return [await rmDir(path)];
154
+ const failures: string[] = [];
155
+ for (const entry of entries) {
156
+ if (entry === REQUEST_LEDGER_DIR) continue;
157
+ try {
158
+ await rm(join(path, entry), { recursive: true, force: true });
159
+ } catch (e) {
160
+ failures.push(`${entry}: ${(e as Error).message}`);
161
+ }
162
+ }
163
+ return [
164
+ failures.length > 0
165
+ ? { path, status: "missing", error: failures.join("; ") }
166
+ : { path, status: "deleted" },
167
+ { path: join(path, REQUEST_LEDGER_DIR), status: "kept" },
168
+ ];
169
+ }
170
+
137
171
  async function rmDir(path: string): Promise<DirResult> {
138
172
  try {
139
173
  // Check if exists first