@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
@@ -9,6 +9,7 @@ import type { Database } from "bun:sqlite";
9
9
 
10
10
  import type { EmbeddingPort } from "../llm/types";
11
11
  import type { VectorIndexPort } from "../store/vector";
12
+ import type { BackgroundIssue } from "./status-model";
12
13
 
13
14
  import { embedBacklog } from "../embed";
14
15
  import {
@@ -24,6 +25,15 @@ import { createVectorStatsPort } from "../store/vector";
24
25
  const DEBOUNCE_MS = 30_000; // 30 seconds
25
26
  const MAX_WAIT_MS = 300_000; // 5 minutes
26
27
  const BATCH_SIZE = 32;
28
+ /** Consecutive failed passes after which automatic retries stop (parked). */
29
+ export const MAX_FAILED_PASSES = 5;
30
+ /** A pass running longer than this is reported as overrunning. */
31
+ export const PASS_OVERRUN_MS = 15 * 60_000;
32
+
33
+ /** Delay before the automatic retry that follows `failures` failed passes. */
34
+ export function failedPassRetryDelayMs(failures: number): number {
35
+ return DEBOUNCE_MS * 2 ** Math.max(0, failures - 1);
36
+ }
27
37
 
28
38
  // ─────────────────────────────────────────────────────────────────────────────
29
39
  // Types
@@ -35,6 +45,12 @@ export interface EmbedSchedulerState {
35
45
  nextRunAt?: number;
36
46
  lastRunAt?: number;
37
47
  lastResult?: EmbedResult;
48
+ /** Start of the pass in flight. */
49
+ runningSince?: number;
50
+ /** Failed passes since the last clean pass (0 when healthy). */
51
+ consecutiveFailures: number;
52
+ /** Automatic retries stopped; chunks stay pending for new work or `gno embed`. */
53
+ parked: boolean;
38
54
  }
39
55
 
40
56
  export interface EmbedResult {
@@ -52,6 +68,47 @@ export interface EmbedSchedulerDeps {
52
68
  getModelUri: () => string;
53
69
  onEmbedded?: (result: EmbedResult) => void;
54
70
  embedBacklogFn?: typeof embedBacklog;
71
+ /**
72
+ * Shared writer lease for one page of background writes; null when another
73
+ * writer holds it, which defers the rest of the pass instead of waiting.
74
+ */
75
+ acquireWriteLease?: () => Promise<(() => Promise<void>) | null>;
76
+ }
77
+
78
+ /** Project scheduler state onto status issues; empty while healthy. */
79
+ export function embedSchedulerIssues(
80
+ state: EmbedSchedulerState,
81
+ now = Date.now()
82
+ ): BackgroundIssue[] {
83
+ const issues: BackgroundIssue[] = [];
84
+ if (state.parked || state.consecutiveFailures > 0) {
85
+ issues.push({
86
+ job: "embed",
87
+ state: state.parked ? "parked" : "failing",
88
+ consecutiveFailures: state.consecutiveFailures,
89
+ runningSeconds: null,
90
+ });
91
+ }
92
+ if (
93
+ state.runningSince !== undefined &&
94
+ now - state.runningSince > PASS_OVERRUN_MS
95
+ ) {
96
+ issues.push({
97
+ job: "embed",
98
+ state: "overrunning",
99
+ consecutiveFailures: state.consecutiveFailures,
100
+ runningSeconds: Math.floor((now - state.runningSince) / 1000),
101
+ });
102
+ }
103
+ return issues;
104
+ }
105
+
106
+ interface PassOutcome {
107
+ result: EmbedResult;
108
+ /** Present when the pass failed; the message is logged once per retry step. */
109
+ failure?: string;
110
+ /** Another writer held the lease; the rest of the backlog waits for a rerun. */
111
+ deferred?: boolean;
55
112
  }
56
113
 
57
114
  // ─────────────────────────────────────────────────────────────────────────────
@@ -86,6 +143,7 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
86
143
  getModelUri,
87
144
  onEmbedded,
88
145
  embedBacklogFn = embedBacklog,
146
+ acquireWriteLease,
89
147
  } = deps;
90
148
 
91
149
  // State
@@ -99,6 +157,9 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
99
157
  let currentRun: Promise<EmbedResult | null> | null = null;
100
158
  let lastRunAt: number | null = null;
101
159
  let lastResult: EmbedResult | null = null;
160
+ let runningSince: number | null = null;
161
+ let consecutiveFailures = 0;
162
+ let parked = false;
102
163
  const controller = new AbortController();
103
164
 
104
165
  const stats = createVectorStatsPort(db);
@@ -110,14 +171,14 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
110
171
  * 2. Filtering by docId would require joining through documents table
111
172
  * 3. Simpler to just embed all backlog when triggered
112
173
  */
113
- async function runEmbed(): Promise<EmbedResult> {
174
+ async function runEmbed(): Promise<PassOutcome> {
114
175
  // Resolve dependencies at execution time (survives context reloads)
115
176
  const embedPort = getEmbedPort();
116
177
  const vectorIndex = getVectorIndex();
117
178
  const modelUri = getModelUri();
118
179
 
119
180
  if (!embedPort || !vectorIndex) {
120
- return { embedded: 0, errors: 0 };
181
+ return { result: { embedded: 0, errors: 0 } };
121
182
  }
122
183
 
123
184
  let result: Awaited<ReturnType<typeof embedBacklogFn>>;
@@ -130,6 +191,7 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
130
191
  vectorIndex,
131
192
  modelUri,
132
193
  batchSize: BATCH_SIZE,
194
+ acquireWriteTurn: acquireWriteLease,
133
195
  identityStillCurrent: () =>
134
196
  getEmbedPort() === embedPort &&
135
197
  getVectorIndex() === vectorIndex &&
@@ -138,14 +200,21 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
138
200
  )
139
201
  );
140
202
  } catch (cause) {
141
- if (controller.signal.aborted) return { embedded: 0, errors: 0 };
142
- throw cause;
203
+ if (controller.signal.aborted)
204
+ return { result: { embedded: 0, errors: 0 } };
205
+ // An inference deadline or worker exit aborts the whole pass. It is a
206
+ // failed pass like any other, never a silently dropped rejection.
207
+ return {
208
+ result: { embedded: 0, errors: 0 },
209
+ failure: cause instanceof Error ? cause.message : String(cause),
210
+ };
143
211
  }
144
212
 
145
213
  if (!result.ok) {
146
- needsRerun = true;
147
- console.error("[embed-scheduler] Embed failed:", result.error.message);
148
- return { embedded: 0, errors: 0 };
214
+ return {
215
+ result: { embedded: 0, errors: 0 },
216
+ failure: result.error.message,
217
+ };
149
218
  }
150
219
  if (
151
220
  getEmbedPort() !== embedPort ||
@@ -153,21 +222,42 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
153
222
  getModelUri() !== modelUri
154
223
  )
155
224
  needsRerun = true;
156
- if ((result.value.contentionErrors ?? 0) > 0 || result.value.errors > 0) {
157
- // Provider failures and contended checkpoints remain durably pending.
225
+ if (result.value.deferred) needsRerun = true;
226
+ if (result.value.embedded > 0) onEmbedded?.(result.value);
227
+ const contended = result.value.contentionErrors ?? 0;
228
+ const deferred = result.value.deferred === true;
229
+ // Provider failures and contended checkpoints remain durably pending.
230
+ return result.value.errors > 0 || contended > 0
231
+ ? {
232
+ result: result.value,
233
+ deferred,
234
+ failure: `${result.value.errors} embedding errors, ${contended} contended writes`,
235
+ }
236
+ : { result: result.value, deferred };
237
+ }
238
+
239
+ /** Count a failed pass and schedule its bounded retry, logging each step once. */
240
+ function recordFailure(message: string): void {
241
+ consecutiveFailures += 1;
242
+ if (parked) return;
243
+ if (consecutiveFailures >= MAX_FAILED_PASSES) {
244
+ parked = true;
158
245
  console.error(
159
- `[embed-scheduler] ${result.value.errors} embedding errors, ${result.value.contentionErrors ?? 0} contended writes; rescheduling`
246
+ `[embed-scheduler] Embed pass failed ${consecutiveFailures} times (${message}); automatic retries parked. Pending chunks stay queued for new changes or \`gno embed\`.`
160
247
  );
161
- needsRerun = true;
248
+ return;
162
249
  }
163
- if (result.value.embedded > 0) onEmbedded?.(result.value);
164
- return result.value;
250
+ const delay = failedPassRetryDelayMs(consecutiveFailures);
251
+ console.error(
252
+ `[embed-scheduler] Embed pass failed (${consecutiveFailures}/${MAX_FAILED_PASSES}): ${message}; retrying in ${Math.round(delay / 1000)}s`
253
+ );
254
+ scheduleRun(delay);
165
255
  }
166
256
 
167
257
  /**
168
258
  * Schedule or reschedule the debounced embed run.
169
259
  */
170
- function scheduleRun(): void {
260
+ function scheduleRun(retryDelay?: number): void {
171
261
  if (disposed) {
172
262
  return;
173
263
  }
@@ -180,7 +270,7 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
180
270
 
181
271
  // Calculate delay
182
272
  const now = Date.now();
183
- let delay = DEBOUNCE_MS;
273
+ let delay = retryDelay ?? DEBOUNCE_MS;
184
274
 
185
275
  // Check max-wait
186
276
  if (firstPendingAt !== null) {
@@ -226,17 +316,38 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
226
316
  pendingCount = 0;
227
317
  firstPendingAt = null;
228
318
 
229
- let result: EmbedResult;
319
+ runningSince = Date.now();
320
+ let outcome: PassOutcome;
230
321
  try {
231
- result = await runEmbed();
322
+ outcome = await runEmbed();
232
323
  lastRunAt = Date.now();
233
- lastResult = result;
324
+ lastResult = outcome.result;
234
325
  } finally {
235
326
  running = false;
327
+ runningSince = null;
236
328
  }
329
+ const result = outcome.result;
237
330
 
238
- // Check if we need to rerun (notifications arrived while running)
239
331
  // Must be AFTER running=false so scheduleRun() actually schedules
332
+ if (outcome.failure !== undefined && !disposed) {
333
+ // A failed pass never reruns immediately: the bounded retry covers work
334
+ // that arrived meanwhile, and once parked only fresh work earns a pass.
335
+ needsRerun = false;
336
+ recordFailure(outcome.failure);
337
+ // Parked stops retries of failed work, never a lease-deferred remainder.
338
+ if (parked && (pendingCount > 0 || outcome.deferred)) {
339
+ firstPendingAt ??= Date.now();
340
+ scheduleRun();
341
+ }
342
+ return result;
343
+ }
344
+ // Only a pass that reached the end of the backlog cleanly clears failures.
345
+ if (outcome.failure === undefined && !outcome.deferred) {
346
+ consecutiveFailures = 0;
347
+ parked = false;
348
+ }
349
+
350
+ // Check if we need to rerun (notifications arrived while running)
240
351
  if ((needsRerun || pendingCount > 0) && !disposed) {
241
352
  needsRerun = false;
242
353
  // Set firstPendingAt if we have pending work
@@ -313,7 +424,10 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
313
424
  const state: EmbedSchedulerState = {
314
425
  pendingDocCount: pendingCount,
315
426
  running,
427
+ consecutiveFailures,
428
+ parked,
316
429
  };
430
+ if (runningSince !== null) state.runningSince = runningSince;
317
431
 
318
432
  // Use accurate nextRunAt from timer scheduling
319
433
  if (nextRunAt !== null) {
@@ -0,0 +1,79 @@
1
+ /**
2
+ * REST boundary for host paths: a caller the request-locality rule judges
3
+ * remote never receives an `absPath` field from any `/api/*` JSON response.
4
+ * Same-host callers (the loopback Web UI) keep them for Reveal and
5
+ * "Open original". Routes that stream original source bytes are exempt:
6
+ * their body is the owner's file, not an API envelope.
7
+ */
8
+
9
+ import { withoutHostPaths } from "../core/host-paths";
10
+ import {
11
+ isLocalClientRequest,
12
+ type RequestPeerServer,
13
+ } from "./request-locality";
14
+
15
+ const HOST_PATH_KEY = '"absPath"';
16
+ const SOURCE_BYTE_ROUTES = new Set(["/api/doc-asset"]);
17
+
18
+ type RouteHandler = (
19
+ req: Request,
20
+ server: RequestPeerServer
21
+ ) => Response | Promise<Response>;
22
+
23
+ /** The JSON response with every `absPath` field removed. */
24
+ export async function redactResponseHostPaths(
25
+ response: Response
26
+ ): Promise<Response> {
27
+ const contentType = response.headers.get("content-type") ?? "";
28
+ if (!contentType.includes("application/json") || response.body === null) {
29
+ return response;
30
+ }
31
+ const text = await response.text();
32
+ const body = text.includes(HOST_PATH_KEY)
33
+ ? JSON.stringify(withoutHostPaths(JSON.parse(text) as unknown))
34
+ : text;
35
+ const headers = new Headers(response.headers);
36
+ headers.delete("content-length");
37
+ return new Response(body, {
38
+ status: response.status,
39
+ statusText: response.statusText,
40
+ headers,
41
+ });
42
+ }
43
+
44
+ const redacting =
45
+ (handler: RouteHandler): RouteHandler =>
46
+ async (req, server) => {
47
+ const response = await handler(req, server);
48
+ return isLocalClientRequest(req, server)
49
+ ? response
50
+ : redactResponseHostPaths(response);
51
+ };
52
+
53
+ const isMethodTable = (route: unknown): route is Record<string, unknown> =>
54
+ route !== null &&
55
+ typeof route === "object" &&
56
+ Object.getPrototypeOf(route) === Object.prototype;
57
+
58
+ /** Wrap every `/api/*` handler in the remote host-path redaction. */
59
+ export function withRemoteHostPathRedaction<T extends Record<string, unknown>>(
60
+ routes: T
61
+ ): T {
62
+ const wrapped: Record<string, unknown> = { ...routes };
63
+ for (const [path, route] of Object.entries(routes)) {
64
+ if (!path.startsWith("/api/") || SOURCE_BYTE_ROUTES.has(path)) continue;
65
+ if (typeof route === "function") {
66
+ wrapped[path] = redacting(route as RouteHandler);
67
+ } else if (isMethodTable(route)) {
68
+ wrapped[path] = Object.fromEntries(
69
+ Object.entries(route).map(([method, handler]) => [
70
+ method,
71
+ typeof handler === "function"
72
+ ? redacting(handler as RouteHandler)
73
+ : handler,
74
+ ])
75
+ );
76
+ }
77
+ }
78
+ return wrapped as T;
79
+ }
@@ -49,6 +49,7 @@ const Ask = lazy(() => import("./pages/Ask"));
49
49
  const GraphView = lazy(() => import("./pages/GraphView"));
50
50
  const CompiledContext = lazy(() => import("./pages/CompiledContext"));
51
51
  const TraceHistory = lazy(() => import("./pages/TraceHistory"));
52
+ const Sessions = lazy(() => import("./pages/Sessions"));
52
53
 
53
54
  type Route =
54
55
  | "/"
@@ -61,7 +62,8 @@ type Route =
61
62
  | "/graph"
62
63
  | "/connectors"
63
64
  | "/context/compiled"
64
- | "/traces";
65
+ | "/traces"
66
+ | "/sessions";
65
67
  type Navigate = (to: string | number) => void;
66
68
 
67
69
  interface RoutePageProps {
@@ -80,6 +82,7 @@ const routes: Record<Route, React.ComponentType<RoutePageProps>> = {
80
82
  "/ask": Ask,
81
83
  "/graph": GraphView,
82
84
  "/traces": TraceHistory,
85
+ "/sessions": Sessions,
83
86
  "/context/compiled": CompiledContext,
84
87
  };
85
88
 
@@ -26,6 +26,13 @@ import {
26
26
  resolveNotePreset,
27
27
  } from "../../../core/note-presets";
28
28
  import { apiFetch } from "../hooks/use-api";
29
+ import {
30
+ clearRequestIntent,
31
+ type RequestIntent,
32
+ requestIdForIntent,
33
+ } from "../lib/request-intent";
34
+
35
+ const CAPTURE_INTENT_KEY = "gno:capture-intent";
29
36
  import { getActiveWikiLinkQuery } from "../lib/wiki-link";
30
37
  import { IndexingProgress } from "./IndexingProgress";
31
38
  import { TagInput } from "./TagInput";
@@ -169,6 +176,7 @@ export function CaptureModal({
169
176
  const [wikiLinkPosition, setWikiLinkPosition] = useState({ x: 24, y: 24 });
170
177
  const [wikiLinkActiveIndex, setWikiLinkActiveIndex] = useState(-1);
171
178
  const contentRef = useRef<HTMLTextAreaElement | null>(null);
179
+ const captureIntentRef = useRef<RequestIntent | null>(null);
172
180
 
173
181
  // Submission state
174
182
  const [state, setState] = useState<ModalState>("form");
@@ -311,19 +319,28 @@ export function CaptureModal({
311
319
  ? selectedPresetId
312
320
  : undefined;
313
321
 
322
+ const payload = {
323
+ collection,
324
+ title,
325
+ folderPath: defaultFolderPath || undefined,
326
+ content: presetOnly ? undefined : content,
327
+ presetId: submitPresetId,
328
+ collisionPolicy: "create_with_suffix",
329
+ source,
330
+ ...(tags.length > 0 && { tags }),
331
+ };
314
332
  const { data, error: err } = await apiFetch<CaptureResponse>(
315
333
  "/api/capture",
316
334
  {
317
335
  method: "POST",
318
336
  body: JSON.stringify({
319
- collection,
320
- title,
321
- folderPath: defaultFolderPath || undefined,
322
- content: presetOnly ? undefined : content,
323
- presetId: submitPresetId,
324
- collisionPolicy: "create_with_suffix",
325
- source,
326
- ...(tags.length > 0 && { tags }),
337
+ ...payload,
338
+ // A retry of this exact capture replays instead of adding a suffix copy.
339
+ requestId: requestIdForIntent(
340
+ captureIntentRef,
341
+ JSON.stringify(payload),
342
+ CAPTURE_INTENT_KEY
343
+ ),
327
344
  }),
328
345
  }
329
346
  );
@@ -335,6 +352,7 @@ export function CaptureModal({
335
352
  }
336
353
 
337
354
  if (data) {
355
+ clearRequestIntent(captureIntentRef, CAPTURE_INTENT_KEY);
338
356
  // Save last used collection
339
357
  localStorage.setItem(STORAGE_KEY, collection);
340
358