@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.
- package/README.md +60 -5
- package/assets/skill/README.md +5 -1
- package/assets/skill/SKILL.md +80 -4
- package/assets/skill/cli-reference.md +132 -2
- package/assets/skill/examples.md +30 -0
- package/assets/skill/mcp-reference.md +54 -1
- package/assets/skill/recipes/capture-and-file.md +6 -0
- package/assets/skill/recipes/memory-file-decision.md +6 -0
- package/assets/skill/recipes/memory-supersede-fact.md +5 -0
- package/assets/skill/recipes/session-evidence-lookup.md +98 -0
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.5.1.zip → gno-browser-clipper-v2.7.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -1
- package/spec/cli.md +449 -35
- package/spec/mcp.md +234 -4
- package/spec/output-schemas/ask.schema.json +1 -1
- package/spec/output-schemas/capture-receipt.schema.json +4 -1
- package/spec/output-schemas/doctor.schema.json +88 -0
- package/spec/output-schemas/error.schema.json +11 -2
- package/spec/output-schemas/get.schema.json +1 -1
- package/spec/output-schemas/mcp-capture-result.schema.json +4 -2
- package/spec/output-schemas/memory-remember.schema.json +10 -4
- package/spec/output-schemas/multi-get.schema.json +4 -1
- package/spec/output-schemas/peek.schema.json +2 -9
- package/spec/output-schemas/request-status.schema.json +113 -0
- package/spec/output-schemas/resident-status.schema.json +22 -0
- package/spec/output-schemas/search-result.schema.json +1 -1
- package/spec/output-schemas/search-results.schema.json +1 -1
- package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
- package/spec/output-schemas/sessions-discovery.schema.json +38 -0
- package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
- package/spec/output-schemas/sessions-status.schema.json +432 -0
- package/spec/output-schemas/status.schema.json +98 -0
- package/src/cli/commands/ask.ts +14 -2
- package/src/cli/commands/capture.ts +55 -96
- package/src/cli/commands/daemon.ts +41 -0
- package/src/cli/commands/doctor.ts +54 -20
- package/src/cli/commands/embed.ts +41 -3
- package/src/cli/commands/ls.ts +3 -0
- package/src/cli/commands/memory.ts +12 -3
- package/src/cli/commands/query.ts +5 -0
- package/src/cli/commands/request-status.ts +59 -0
- package/src/cli/commands/reset.ts +39 -5
- package/src/cli/commands/sessions.ts +713 -0
- package/src/cli/commands/shared.ts +14 -1
- package/src/cli/commands/status.ts +63 -5
- package/src/cli/commands/vec.ts +54 -0
- package/src/cli/detach.ts +29 -1
- package/src/cli/errors.ts +13 -9
- package/src/cli/program.ts +441 -2
- package/src/cli/session-binding.ts +49 -0
- package/src/config/types.ts +8 -0
- package/src/core/capture-publish.ts +239 -0
- package/src/core/capture-sync.ts +12 -2
- package/src/core/host-paths.ts +31 -0
- package/src/core/memory-remember.ts +234 -122
- package/src/core/memory-types.ts +11 -0
- package/src/core/network-boundary-inventory.ts +8 -0
- package/src/core/request-receipts.ts +671 -0
- package/src/core/shutdown-budget.ts +6 -0
- package/src/core/vector-partition-status.ts +52 -0
- package/src/embed/backlog.ts +124 -18
- package/src/embed/fingerprint.ts +6 -3
- package/src/embed/retry.ts +66 -27
- package/src/embed/variant-backlog.ts +15 -10
- package/src/embed/variant-retry.ts +31 -22
- package/src/index.ts +30 -2
- package/src/llm/native-worker/dispatcher.ts +2 -0
- package/src/llm/native-worker/embedding-identity.ts +42 -0
- package/src/llm/native-worker/protocol.ts +1 -0
- package/src/llm/types.ts +3 -0
- package/src/mcp/context.ts +17 -0
- package/src/mcp/http-egress.ts +4 -0
- package/src/mcp/http-transport.ts +2 -0
- package/src/mcp/resources/index.ts +6 -5
- package/src/mcp/tool-descriptions-core.ts +1 -1
- package/src/mcp/tools/capture.ts +87 -85
- package/src/mcp/tools/index.ts +77 -4
- package/src/mcp/tools/memory-remember.ts +8 -1
- package/src/mcp/tools/memory-shared.ts +7 -1
- package/src/mcp/tools/request-status.ts +73 -0
- package/src/mcp/tools/sessions.ts +208 -0
- package/src/mcp/tools/status.ts +4 -0
- package/src/pipeline/hybrid.ts +37 -7
- package/src/pipeline/vsearch.ts +14 -2
- package/src/sdk/client.ts +180 -84
- package/src/sdk/index.ts +6 -0
- package/src/sdk/types.ts +54 -2
- package/src/serve/capture-service.ts +98 -32
- package/src/serve/config-sync.ts +3 -2
- package/src/serve/embed-scheduler.ts +133 -19
- package/src/serve/host-path-redaction.ts +79 -0
- package/src/serve/public/app.tsx +4 -1
- package/src/serve/public/components/CaptureModal.tsx +26 -8
- package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
- package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
- package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
- package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
- package/src/serve/public/components/sessions/api.ts +40 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/hooks/use-api.ts +26 -3
- package/src/serve/public/lib/request-intent.ts +77 -0
- package/src/serve/public/lib/snippet.tsx +52 -0
- package/src/serve/public/lib/workspace-actions.ts +12 -1
- package/src/serve/public/lib/workspace-tabs.ts +2 -0
- package/src/serve/public/pages/Dashboard.tsx +22 -9
- package/src/serve/public/pages/DocView.tsx +25 -6
- package/src/serve/public/pages/DocumentEditor.tsx +224 -104
- package/src/serve/public/pages/Search.tsx +1 -41
- package/src/serve/public/pages/Sessions.tsx +350 -0
- package/src/serve/resident-runtime.ts +69 -4
- package/src/serve/resident-status.ts +13 -1
- package/src/serve/routes/api.ts +476 -147
- package/src/serve/routes/sessions.ts +766 -0
- package/src/serve/security.ts +9 -0
- package/src/serve/server.ts +215 -10
- package/src/serve/session-automation.ts +146 -0
- package/src/serve/status-model.ts +16 -0
- package/src/serve/status.ts +2 -0
- package/src/serve/watch-reconciliation-shared.ts +3 -0
- package/src/serve/watch-service-events.ts +3 -2
- package/src/serve/watch-service-run-flush.ts +35 -2
- package/src/serve/watch-service.ts +5 -0
- package/src/sessions/archive.ts +348 -0
- package/src/sessions/automation-state.ts +444 -0
- package/src/sessions/automation-status.ts +239 -0
- package/src/sessions/automation.ts +1169 -0
- package/src/sessions/binding.ts +105 -0
- package/src/sessions/claude-hook.ts +240 -0
- package/src/sessions/config.ts +176 -0
- package/src/sessions/format.ts +191 -0
- package/src/sessions/import-child-env.ts +8 -0
- package/src/sessions/import-child.ts +152 -0
- package/src/sessions/parsers/claude-code.ts +259 -0
- package/src/sessions/parsers/codex.ts +303 -0
- package/src/sessions/parsers/hermes.ts +248 -0
- package/src/sessions/parsers/openclaw.ts +496 -0
- package/src/sessions/parsers/shared.ts +184 -0
- package/src/sessions/sanitize.ts +222 -0
- package/src/sessions/service.ts +1533 -0
- package/src/sessions/setup.ts +477 -0
- package/src/sessions/sources.ts +518 -0
- package/src/sessions/state.ts +118 -0
- package/src/sessions/types.ts +457 -0
- package/src/store/migrations/031-runtime-independent-vectors.ts +29 -0
- package/src/store/migrations/032-vector-runtime-callers.ts +17 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +76 -16
- package/src/store/sqlite/scoped-index.ts +9 -0
- package/src/store/types.ts +11 -1
- package/src/store/vector/lazy.ts +46 -43
- package/src/store/vector/runtime-compat.ts +651 -0
- package/src/store/vector/sqlite-vec.ts +20 -2
- package/src/store/vector/status.ts +276 -35
- package/src/store/vector/types.ts +2 -0
- package/src/store/vector/variant-search.ts +71 -23
- package/src/store/vector/variants.ts +49 -14
- 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<
|
|
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)
|
|
142
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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 (
|
|
157
|
-
|
|
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] ${
|
|
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
|
-
|
|
248
|
+
return;
|
|
162
249
|
}
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
319
|
+
runningSince = Date.now();
|
|
320
|
+
let outcome: PassOutcome;
|
|
230
321
|
try {
|
|
231
|
-
|
|
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
|
+
}
|
package/src/serve/public/app.tsx
CHANGED
|
@@ -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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|