@gmickel/gno 2.6.0 → 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 +1 -1
- package/assets/skill/SKILL.md +5 -3
- package/assets/skill/cli-reference.md +9 -2
- package/assets/skill/mcp-reference.md +2 -1
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.6.0.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 +1 -1
- package/spec/cli.md +109 -18
- package/spec/mcp.md +36 -2
- package/spec/output-schemas/ask.schema.json +1 -1
- package/spec/output-schemas/capture-receipt.schema.json +1 -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 +1 -2
- package/spec/output-schemas/memory-remember.schema.json +2 -2
- package/spec/output-schemas/multi-get.schema.json +4 -1
- package/spec/output-schemas/peek.schema.json +2 -9
- 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/status.schema.json +98 -0
- package/src/cli/commands/doctor.ts +54 -20
- package/src/cli/commands/embed.ts +41 -3
- package/src/cli/commands/query.ts +5 -0
- 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 +53 -1
- package/src/core/capture-sync.ts +9 -2
- package/src/core/host-paths.ts +31 -0
- package/src/core/memory-remember.ts +4 -3
- 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 +21 -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 +9 -0
- package/src/mcp/resources/index.ts +6 -5
- package/src/mcp/tool-descriptions-core.ts +1 -1
- package/src/mcp/tools/capture.ts +1 -3
- package/src/mcp/tools/index.ts +11 -4
- package/src/mcp/tools/memory-remember.ts +1 -1
- package/src/mcp/tools/status.ts +4 -0
- package/src/pipeline/hybrid.ts +37 -7
- package/src/pipeline/vsearch.ts +14 -2
- package/src/serve/embed-scheduler.ts +133 -19
- package/src/serve/host-path-redaction.ts +79 -0
- package/src/serve/public/components/sessions/SessionSearch.tsx +2 -2
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/hooks/use-api.ts +17 -2
- package/src/serve/public/lib/request-intent.ts +8 -0
- package/src/serve/public/{components/sessions → lib}/snippet.tsx +2 -3
- package/src/serve/public/pages/Dashboard.tsx +12 -9
- package/src/serve/public/pages/DocView.tsx +10 -5
- package/src/serve/public/pages/DocumentEditor.tsx +91 -14
- package/src/serve/public/pages/Search.tsx +1 -41
- package/src/serve/resident-runtime.ts +26 -1
- package/src/serve/resident-status.ts +13 -1
- package/src/serve/server.ts +10 -9
- 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/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 +22 -1
- 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.6.0.zip.sha256 +0 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useCallback, useState } from "react";
|
|
2
2
|
|
|
3
|
+
import { writeOutcomeUnknown } from "../lib/request-intent";
|
|
4
|
+
|
|
3
5
|
interface ApiState<T> {
|
|
4
6
|
data: T | null;
|
|
5
7
|
loading: boolean;
|
|
@@ -86,6 +88,11 @@ export async function apiFetch<T>(
|
|
|
86
88
|
error: string | null;
|
|
87
89
|
/** `error.details` of the JSON error envelope, when the server sent one. */
|
|
88
90
|
details?: Record<string, unknown>;
|
|
91
|
+
/**
|
|
92
|
+
* A write may or may not have committed: no readable response, a server
|
|
93
|
+
* error, or the request is still pending under its request ID.
|
|
94
|
+
*/
|
|
95
|
+
outcomeUnknown?: true;
|
|
89
96
|
}> {
|
|
90
97
|
try {
|
|
91
98
|
const res = await fetch(endpoint, {
|
|
@@ -96,17 +103,24 @@ export async function apiFetch<T>(
|
|
|
96
103
|
const { json, parseError } = await parseJsonSafe(res);
|
|
97
104
|
|
|
98
105
|
if (parseError) {
|
|
99
|
-
return { data: null, error: parseError };
|
|
106
|
+
return { data: null, error: parseError, outcomeUnknown: true };
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
if (!res.ok) {
|
|
103
110
|
const apiError = json as {
|
|
104
|
-
error?: {
|
|
111
|
+
error?: {
|
|
112
|
+
code?: string;
|
|
113
|
+
message?: string;
|
|
114
|
+
details?: Record<string, unknown>;
|
|
115
|
+
};
|
|
105
116
|
};
|
|
106
117
|
return {
|
|
107
118
|
data: null,
|
|
108
119
|
error: apiError.error?.message || `Request failed: ${res.status}`,
|
|
109
120
|
details: apiError.error?.details,
|
|
121
|
+
...(writeOutcomeUnknown(res.status, apiError.error?.code)
|
|
122
|
+
? { outcomeUnknown: true as const }
|
|
123
|
+
: {}),
|
|
110
124
|
};
|
|
111
125
|
}
|
|
112
126
|
|
|
@@ -115,6 +129,7 @@ export async function apiFetch<T>(
|
|
|
115
129
|
return {
|
|
116
130
|
data: null,
|
|
117
131
|
error: err instanceof Error ? err.message : "Network error",
|
|
132
|
+
outcomeUnknown: true,
|
|
118
133
|
};
|
|
119
134
|
}
|
|
120
135
|
}
|
|
@@ -67,3 +67,11 @@ export function clearRequestIntent(
|
|
|
67
67
|
// Nothing persisted to clear.
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether a failed write's HTTP answer leaves its commit unknown: a server
|
|
73
|
+
* error, or the request ID's earlier attempt is still in progress.
|
|
74
|
+
*/
|
|
75
|
+
export function writeOutcomeUnknown(status: number, code?: string): boolean {
|
|
76
|
+
return status >= 500 || code === "REQUEST_PENDING";
|
|
77
|
+
}
|
|
@@ -14,10 +14,9 @@ export function unescapeMarkdown(text: string): string {
|
|
|
14
14
|
/**
|
|
15
15
|
* Render a search snippet: FTS `<mark>` highlights become real <mark>
|
|
16
16
|
* elements, everything else stays React text (never parsed as HTML).
|
|
17
|
-
*
|
|
18
|
-
* archived turns are indexed as escaped Markdown.
|
|
17
|
+
* Markdown backslash escapes are dropped because indexed text keeps them.
|
|
19
18
|
*/
|
|
20
|
-
export function
|
|
19
|
+
export function renderSnippet(snippet: string): ReactNode[] {
|
|
21
20
|
const parts: ReactNode[] = [];
|
|
22
21
|
let remaining = snippet;
|
|
23
22
|
let key = 0;
|
|
@@ -788,25 +788,28 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
788
788
|
}
|
|
789
789
|
style={{ animationDelay: `${0.4 + index * 0.1}s` }}
|
|
790
790
|
>
|
|
791
|
-
<CardContent className="flex items-center justify-between py-4">
|
|
792
|
-
<div className="flex items-center gap-3">
|
|
791
|
+
<CardContent className="flex flex-wrap items-center justify-between gap-3 py-4">
|
|
792
|
+
<div className="flex min-w-0 flex-1 basis-48 items-center gap-3">
|
|
793
793
|
{syncing ? (
|
|
794
|
-
<Loader2Icon className="size-4 animate-spin text-amber-500" />
|
|
794
|
+
<Loader2Icon className="size-4 shrink-0 animate-spin text-amber-500" />
|
|
795
795
|
) : collection.embeddedCount >= collection.chunkCount ? (
|
|
796
|
-
<CheckCircle2Icon className="size-4 text-green-500" />
|
|
796
|
+
<CheckCircle2Icon className="size-4 shrink-0 text-green-500" />
|
|
797
797
|
) : (
|
|
798
|
-
<div className="size-4 rounded-full border-2 border-amber-500" />
|
|
798
|
+
<div className="size-4 shrink-0 rounded-full border-2 border-amber-500" />
|
|
799
799
|
)}
|
|
800
|
-
<div>
|
|
801
|
-
<div className="font-medium text-lg transition-colors group-hover:text-primary">
|
|
800
|
+
<div className="min-w-0">
|
|
801
|
+
<div className="truncate font-medium text-lg transition-colors group-hover:text-primary">
|
|
802
802
|
{collection.name}
|
|
803
803
|
</div>
|
|
804
|
-
<div
|
|
804
|
+
<div
|
|
805
|
+
className="truncate font-mono text-muted-foreground text-sm"
|
|
806
|
+
title={collection.path}
|
|
807
|
+
>
|
|
805
808
|
{collection.path}
|
|
806
809
|
</div>
|
|
807
810
|
</div>
|
|
808
811
|
</div>
|
|
809
|
-
<div className="flex items-center gap-3 text-right">
|
|
812
|
+
<div className="flex shrink-0 items-center gap-3 text-right">
|
|
810
813
|
<Button
|
|
811
814
|
onClick={(event) => {
|
|
812
815
|
event.stopPropagation();
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
type FileRefactorPreviewPlan,
|
|
38
38
|
} from "../../../core/file-refactor-contract";
|
|
39
39
|
import { extractSections } from "../../../core/sections";
|
|
40
|
+
import { updateFrontmatterTags } from "../../../ingestion/frontmatter";
|
|
40
41
|
import {
|
|
41
42
|
CodeBlock,
|
|
42
43
|
CodeBlockCopyButton,
|
|
@@ -583,11 +584,10 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
583
584
|
return buildDocAssetUrl(doc.uri, doc.relPath);
|
|
584
585
|
}, [doc, isPdf]);
|
|
585
586
|
|
|
586
|
-
// Remote "Open original" target
|
|
587
|
-
//
|
|
588
|
-
// previous file:// scope (every read-only source) for remote clients.
|
|
587
|
+
// Remote "Open original" target: /api/doc-asset serves any collection file
|
|
588
|
+
// inline by URI, and remote clients never receive the host absPath.
|
|
589
589
|
const sourceAssetUrl = useMemo(() => {
|
|
590
|
-
if (!doc
|
|
590
|
+
if (!doc) {
|
|
591
591
|
return null;
|
|
592
592
|
}
|
|
593
593
|
return buildDocAssetUrl(doc.uri, doc.relPath);
|
|
@@ -1243,10 +1243,15 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
1243
1243
|
}
|
|
1244
1244
|
|
|
1245
1245
|
clearRequestIntent(tagIntentRef, TAG_INTENT_KEY);
|
|
1246
|
-
//
|
|
1246
|
+
// Mirror the committed write-back so the frontmatter tag list is current
|
|
1247
|
+
// without waiting for the index to resync.
|
|
1247
1248
|
setDoc({
|
|
1248
1249
|
...doc,
|
|
1249
1250
|
tags: editedTags,
|
|
1251
|
+
content:
|
|
1252
|
+
data?.writeBack === "applied" && doc.content !== null
|
|
1253
|
+
? updateFrontmatterTags(doc.content, editedTags)
|
|
1254
|
+
: doc.content,
|
|
1250
1255
|
source: {
|
|
1251
1256
|
...doc.source,
|
|
1252
1257
|
sourceHash: data?.version.sourceHash ?? doc.source.sourceHash,
|
|
@@ -207,8 +207,12 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
207
207
|
const [saveError, setSaveError] = useState<string | null>(null);
|
|
208
208
|
const [creatingCopy, setCreatingCopy] = useState(false);
|
|
209
209
|
const [copyError, setCopyError] = useState<string | null>(null);
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
/**
|
|
211
|
+
* `outside`: the file changed under the editor. `unconfirmed`: a save's
|
|
212
|
+
* response was lost, so a change on disk may be that save's own commit.
|
|
213
|
+
*/
|
|
214
|
+
const [changeNotice, setChangeNotice] = useState<
|
|
215
|
+
"outside" | "unconfirmed" | null
|
|
212
216
|
>(null);
|
|
213
217
|
const [historyEntries, setHistoryEntries] = useState<LocalHistoryEntry[]>([]);
|
|
214
218
|
const [historyDialogOpen, setHistoryDialogOpen] = useState(false);
|
|
@@ -322,12 +326,21 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
322
326
|
// read the current revision and last committed content from refs.
|
|
323
327
|
const docRef = useRef(doc);
|
|
324
328
|
const committedContentRef = useRef(originalContent);
|
|
329
|
+
const draftRef = useRef(content);
|
|
325
330
|
useEffect(() => {
|
|
326
331
|
docRef.current = doc;
|
|
327
332
|
}, [doc]);
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
draftRef.current = content;
|
|
335
|
+
}, [content]);
|
|
328
336
|
// One request ID per (document revision, content) save intent: retrying a
|
|
329
337
|
// save whose response was lost replays it instead of reporting a conflict.
|
|
330
338
|
const saveIntentRef = useRef<RequestIntent | null>(null);
|
|
339
|
+
// Content of a save that is in flight or whose response was lost: its
|
|
340
|
+
// commit is unknown, so change events are held until a response says whose
|
|
341
|
+
// change they were. Retry save re-sends exactly this content.
|
|
342
|
+
const unknownSaveRef = useRef<string | null>(null);
|
|
343
|
+
const changedWhileUnknownRef = useRef(false);
|
|
331
344
|
|
|
332
345
|
/** Save `contentToSave`; resolves true once it is committed on disk. */
|
|
333
346
|
const persistContent = useCallback(
|
|
@@ -341,8 +354,13 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
341
354
|
|
|
342
355
|
setSaveStatus("saving");
|
|
343
356
|
setSaveError(null);
|
|
357
|
+
unknownSaveRef.current = contentToSave;
|
|
344
358
|
|
|
345
|
-
const {
|
|
359
|
+
const {
|
|
360
|
+
data,
|
|
361
|
+
error: err,
|
|
362
|
+
outcomeUnknown,
|
|
363
|
+
} = await apiFetch<UpdateDocResponse>(
|
|
346
364
|
`/api/docs/${encodeURIComponent(current.docid)}`,
|
|
347
365
|
{
|
|
348
366
|
method: "PUT",
|
|
@@ -363,23 +381,45 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
363
381
|
if (err) {
|
|
364
382
|
setSaveStatus("error");
|
|
365
383
|
setSaveError(err);
|
|
384
|
+
if (outcomeUnknown) {
|
|
385
|
+
// Never downgrade a warning about a change from elsewhere.
|
|
386
|
+
setChangeNotice((notice) =>
|
|
387
|
+
notice === "outside" ? notice : "unconfirmed"
|
|
388
|
+
);
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
// A definitive rejection: this save did not commit, so a change seen
|
|
392
|
+
// meanwhile came from elsewhere.
|
|
393
|
+
unknownSaveRef.current = null;
|
|
394
|
+
if (changedWhileUnknownRef.current) {
|
|
395
|
+
changedWhileUnknownRef.current = false;
|
|
396
|
+
setChangeNotice("outside");
|
|
397
|
+
} else {
|
|
398
|
+
setChangeNotice((notice) =>
|
|
399
|
+
notice === "unconfirmed" ? null : notice
|
|
400
|
+
);
|
|
401
|
+
}
|
|
366
402
|
return false;
|
|
367
403
|
}
|
|
368
404
|
|
|
405
|
+
unknownSaveRef.current = null;
|
|
406
|
+
changedWhileUnknownRef.current = false;
|
|
369
407
|
if (data?.request?.replayed) {
|
|
370
408
|
// A replay reports an earlier commit; disk may have moved on since.
|
|
371
|
-
//
|
|
409
|
+
// Only a read showing a different hash is evidence of another writer.
|
|
372
410
|
const { data: latest } = await apiFetch<DocData>(
|
|
373
411
|
`/api/doc?uri=${encodeURIComponent(current.uri)}`
|
|
374
412
|
);
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
413
|
+
setChangeNotice(
|
|
414
|
+
latest && latest.source.sourceHash !== data.version.sourceHash
|
|
415
|
+
? "outside"
|
|
416
|
+
: null
|
|
417
|
+
);
|
|
378
418
|
} else {
|
|
379
419
|
// Written now against the loaded revision: the next change event is
|
|
380
420
|
// this save's own sync, and any earlier notice is stale.
|
|
381
421
|
ignoreDocEventsUntilRef.current = Date.now() + 5_000;
|
|
382
|
-
|
|
422
|
+
setChangeNotice(null);
|
|
383
423
|
}
|
|
384
424
|
const previous = committedContentRef.current;
|
|
385
425
|
if (previous !== contentToSave) {
|
|
@@ -589,6 +629,18 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
589
629
|
}
|
|
590
630
|
}, [cancelAutosave, content, hasUnsavedChanges, persistContent]);
|
|
591
631
|
|
|
632
|
+
// Retry the unconfirmed save itself (same content, same request ID), even
|
|
633
|
+
// when the draft has since changed or returned to the loaded text; once it
|
|
634
|
+
// is confirmed, the draft as it is then is saved on top of it.
|
|
635
|
+
const retryUnconfirmedSave = useCallback(async () => {
|
|
636
|
+
const pending = unknownSaveRef.current;
|
|
637
|
+
if (pending === null) return;
|
|
638
|
+
cancelAutosave();
|
|
639
|
+
if (await persistContent(pending)) {
|
|
640
|
+
await persistContent(draftRef.current);
|
|
641
|
+
}
|
|
642
|
+
}, [cancelAutosave, persistContent]);
|
|
643
|
+
|
|
592
644
|
const loadDocument = useCallback(() => {
|
|
593
645
|
const uri = currentTarget.uri;
|
|
594
646
|
|
|
@@ -637,16 +689,20 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
637
689
|
return;
|
|
638
690
|
}
|
|
639
691
|
handledDocEventRef.current = latestDocEvent.changedAt;
|
|
692
|
+
if (unknownSaveRef.current !== null) {
|
|
693
|
+
changedWhileUnknownRef.current = true;
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
640
696
|
if (Date.now() < ignoreDocEventsUntilRef.current) {
|
|
641
697
|
return;
|
|
642
698
|
}
|
|
643
|
-
|
|
644
|
-
"This document changed on disk. Reload before continuing."
|
|
645
|
-
);
|
|
699
|
+
setChangeNotice("outside");
|
|
646
700
|
}, [doc, latestDocEvent?.changedAt, latestDocEvent?.uri]);
|
|
647
701
|
|
|
648
702
|
const reloadDocument = useCallback(() => {
|
|
649
|
-
|
|
703
|
+
unknownSaveRef.current = null;
|
|
704
|
+
changedWhileUnknownRef.current = false;
|
|
705
|
+
setChangeNotice(null);
|
|
650
706
|
loadDocument();
|
|
651
707
|
}, [loadDocument]);
|
|
652
708
|
|
|
@@ -1136,10 +1192,31 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
1136
1192
|
</p>
|
|
1137
1193
|
)}
|
|
1138
1194
|
|
|
1139
|
-
{
|
|
1195
|
+
{changeNotice === "unconfirmed" && (
|
|
1196
|
+
<div className="border-amber-500/30 border-b bg-amber-500/10 px-4 py-3">
|
|
1197
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
1198
|
+
<p className="text-amber-500 text-sm">
|
|
1199
|
+
The save may have completed, but its response was lost. Retry to
|
|
1200
|
+
confirm it.
|
|
1201
|
+
</p>
|
|
1202
|
+
<Button
|
|
1203
|
+
disabled={saveStatus === "saving"}
|
|
1204
|
+
onClick={retryUnconfirmedSave}
|
|
1205
|
+
size="sm"
|
|
1206
|
+
variant="outline"
|
|
1207
|
+
>
|
|
1208
|
+
Retry save
|
|
1209
|
+
</Button>
|
|
1210
|
+
</div>
|
|
1211
|
+
</div>
|
|
1212
|
+
)}
|
|
1213
|
+
|
|
1214
|
+
{changeNotice === "outside" && (
|
|
1140
1215
|
<div className="border-amber-500/30 border-b bg-amber-500/10 px-4 py-3">
|
|
1141
1216
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
1142
|
-
<p className="text-amber-500 text-sm">
|
|
1217
|
+
<p className="text-amber-500 text-sm">
|
|
1218
|
+
This document changed on disk. Reload before continuing.
|
|
1219
|
+
</p>
|
|
1143
1220
|
<Button onClick={reloadDocument} size="sm" variant="outline">
|
|
1144
1221
|
Reload
|
|
1145
1222
|
</Button>
|
|
@@ -50,50 +50,10 @@ import {
|
|
|
50
50
|
fetchServerCapabilities,
|
|
51
51
|
type ServerCapabilities,
|
|
52
52
|
} from "../lib/server-capabilities";
|
|
53
|
+
import { renderSnippet } from "../lib/snippet";
|
|
53
54
|
import { cn } from "../lib/utils";
|
|
54
55
|
import { AIModelSelector, TagFacets } from "./search-page-widgets";
|
|
55
56
|
|
|
56
|
-
/**
|
|
57
|
-
* Render snippet with <mark> tags as highlighted spans.
|
|
58
|
-
* Only allows mark tags - strips all other HTML for safety.
|
|
59
|
-
*/
|
|
60
|
-
function renderSnippet(snippet: string): React.ReactNode {
|
|
61
|
-
const parts: React.ReactNode[] = [];
|
|
62
|
-
let remaining = snippet;
|
|
63
|
-
let key = 0;
|
|
64
|
-
|
|
65
|
-
while (remaining.length > 0) {
|
|
66
|
-
const markStart = remaining.indexOf("<mark>");
|
|
67
|
-
if (markStart === -1) {
|
|
68
|
-
parts.push(remaining);
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (markStart > 0) {
|
|
73
|
-
parts.push(remaining.slice(0, markStart));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const markEnd = remaining.indexOf("</mark>", markStart);
|
|
77
|
-
if (markEnd === -1) {
|
|
78
|
-
parts.push(remaining.slice(markStart));
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const highlighted = remaining.slice(markStart + 6, markEnd);
|
|
83
|
-
parts.push(
|
|
84
|
-
<mark
|
|
85
|
-
className="rounded bg-primary/20 px-0.5 font-medium text-primary"
|
|
86
|
-
key={key++}
|
|
87
|
-
>
|
|
88
|
-
{highlighted}
|
|
89
|
-
</mark>
|
|
90
|
-
);
|
|
91
|
-
remaining = remaining.slice(markEnd + 7);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return parts;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
57
|
interface PageProps {
|
|
98
58
|
navigate: (to: string | number) => void;
|
|
99
59
|
}
|
|
@@ -45,9 +45,11 @@ import { JobManager } from "../core/job-manager";
|
|
|
45
45
|
import { recordContentMutation } from "../core/mutation-generations";
|
|
46
46
|
import {
|
|
47
47
|
shutdownDuration,
|
|
48
|
+
RESIDENT_BUSY_TIMEOUT_MS,
|
|
48
49
|
SHUTDOWN_DRAIN_MS,
|
|
49
50
|
SHUTDOWN_ABORT_MS,
|
|
50
51
|
} from "../core/shutdown-budget";
|
|
52
|
+
import { acquireCliWriteLease } from "../core/write-lease";
|
|
51
53
|
import { defaultSyncService, withContentTypeRules } from "../ingestion";
|
|
52
54
|
import { withOwnedInferenceScope } from "../llm/inference-scope";
|
|
53
55
|
import { getActivePreset } from "../llm/registry";
|
|
@@ -60,7 +62,7 @@ import {
|
|
|
60
62
|
disposeServerContext,
|
|
61
63
|
type ServerContext,
|
|
62
64
|
} from "./context";
|
|
63
|
-
import { createEmbedScheduler } from "./embed-scheduler";
|
|
65
|
+
import { createEmbedScheduler, embedSchedulerIssues } from "./embed-scheduler";
|
|
64
66
|
import { FindingsScheduler, type FindingsPassResult } from "./findings-pass";
|
|
65
67
|
import { AdmissionController, ReaderGate } from "./resident-admission";
|
|
66
68
|
import { ResidentBackgroundWork } from "./resident-background-work";
|
|
@@ -189,10 +191,25 @@ export type ResidentRuntimeDeps = {
|
|
|
189
191
|
eventBus?: DocumentEventBus | null;
|
|
190
192
|
callbacks?: CollectionWatchCallbacks;
|
|
191
193
|
syncOptions?: Parameters<typeof withContentTypeRules>[0];
|
|
194
|
+
acquireWriteLease?: () => Promise<(() => Promise<void>) | null>;
|
|
192
195
|
}) => CollectionWatchService;
|
|
193
196
|
modelManagerFactory?: (config: Config) => ModelManager;
|
|
194
197
|
};
|
|
195
198
|
|
|
199
|
+
/** No-wait shared writer lease for background writes; null when held elsewhere. */
|
|
200
|
+
async function acquireBackgroundWriteLease(
|
|
201
|
+
dbPath: string,
|
|
202
|
+
command: string
|
|
203
|
+
): Promise<(() => Promise<void>) | null> {
|
|
204
|
+
const lease = await acquireCliWriteLease({
|
|
205
|
+
dbPath,
|
|
206
|
+
waitMs: 0,
|
|
207
|
+
noWait: true,
|
|
208
|
+
command,
|
|
209
|
+
});
|
|
210
|
+
return lease.ok ? lease.release : null;
|
|
211
|
+
}
|
|
212
|
+
|
|
196
213
|
export async function startResidentRuntime(
|
|
197
214
|
options: ResidentRuntimeOptions = {},
|
|
198
215
|
deps: ResidentRuntimeDeps = {}
|
|
@@ -274,6 +291,11 @@ export async function startResidentRuntime(
|
|
|
274
291
|
if (!syncCollections.ok) return failStartup(syncCollections.error.message);
|
|
275
292
|
const syncContexts = await store.syncContexts(initialConfig.contexts ?? []);
|
|
276
293
|
if (!syncContexts.ok) return failStartup(syncContexts.error.message);
|
|
294
|
+
// SQLite busy waits block this event loop; keep each one short enough that
|
|
295
|
+
// a stop signal is still handled within the stop grace.
|
|
296
|
+
store.setBusyTimeout?.(RESIDENT_BUSY_TIMEOUT_MS);
|
|
297
|
+
const leaseFor = (command: string) => () =>
|
|
298
|
+
acquireBackgroundWriteLease(dbPath, command);
|
|
277
299
|
|
|
278
300
|
let ctx: ServerContext;
|
|
279
301
|
try {
|
|
@@ -322,6 +344,7 @@ export async function startResidentRuntime(
|
|
|
322
344
|
onEmbedded: () => {
|
|
323
345
|
generations.index += 1;
|
|
324
346
|
},
|
|
347
|
+
acquireWriteLease: leaseFor(`gno ${mode} (background embed)`),
|
|
325
348
|
});
|
|
326
349
|
ctxHolder.scheduler = scheduler;
|
|
327
350
|
ctxHolder.current.scheduler = scheduler;
|
|
@@ -351,6 +374,7 @@ export async function startResidentRuntime(
|
|
|
351
374
|
},
|
|
352
375
|
},
|
|
353
376
|
syncOptions: withContentTypeRules({}, initialConfig),
|
|
377
|
+
acquireWriteLease: leaseFor(`gno ${mode} (watch sync)`),
|
|
354
378
|
});
|
|
355
379
|
watchService.start();
|
|
356
380
|
ctxHolder.watchService = watchService;
|
|
@@ -590,6 +614,7 @@ export async function startResidentRuntime(
|
|
|
590
614
|
failed: jobs.recent.filter((job) => job.status === "failed").length,
|
|
591
615
|
},
|
|
592
616
|
generations: { ...generations },
|
|
617
|
+
backgroundIssues: embedSchedulerIssues(scheduler.getState()),
|
|
593
618
|
});
|
|
594
619
|
},
|
|
595
620
|
setListenerPort(port) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/** Safe, transport-neutral resident lifecycle status projection. */
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
BackgroundIssue,
|
|
5
|
+
ResidentStatus,
|
|
6
|
+
RuntimeMode,
|
|
7
|
+
} from "./status-model";
|
|
4
8
|
|
|
5
9
|
const EMPTY_MODELS: ResidentStatus["models"] = {
|
|
6
10
|
activeLeases: 0,
|
|
@@ -50,6 +54,7 @@ export interface ResidentStatusSnapshotInput {
|
|
|
50
54
|
models: ResidentStatus["models"];
|
|
51
55
|
jobs: ResidentStatus["jobs"];
|
|
52
56
|
generations: ResidentStatus["generations"];
|
|
57
|
+
backgroundIssues?: BackgroundIssue[];
|
|
53
58
|
now?: number;
|
|
54
59
|
}
|
|
55
60
|
|
|
@@ -72,6 +77,13 @@ export function buildResidentStatusSnapshot(
|
|
|
72
77
|
models: { ...input.models },
|
|
73
78
|
jobs: { ...input.jobs },
|
|
74
79
|
generations: { ...input.generations },
|
|
80
|
+
...(input.backgroundIssues?.length
|
|
81
|
+
? {
|
|
82
|
+
backgroundIssues: input.backgroundIssues.map((issue) => ({
|
|
83
|
+
...issue,
|
|
84
|
+
})),
|
|
85
|
+
}
|
|
86
|
+
: {}),
|
|
75
87
|
};
|
|
76
88
|
}
|
|
77
89
|
|
package/src/serve/server.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import type { HttpGatewayOverrides } from "../mcp/http-security";
|
|
2
|
-
/**
|
|
3
|
-
* Bun.serve() web server for GNO web UI.
|
|
4
|
-
* Uses Bun's fullstack dev server with HTML imports.
|
|
5
|
-
* Opens DB once at startup, closes on shutdown.
|
|
6
|
-
*
|
|
7
|
-
* @module src/serve/server
|
|
8
|
-
*/
|
|
9
2
|
import type { RequestPeerServer } from "./request-locality";
|
|
10
3
|
import type { ResidentRuntime } from "./resident-runtime";
|
|
11
4
|
import type { ContextHolder } from "./routes/api";
|
|
@@ -23,6 +16,14 @@ import {
|
|
|
23
16
|
handlePdfjsVendorRequest,
|
|
24
17
|
isPdfjsVendorPath,
|
|
25
18
|
} from "./fn112-routes";
|
|
19
|
+
/**
|
|
20
|
+
* Bun.serve() web server for GNO web UI.
|
|
21
|
+
* Uses Bun's fullstack dev server with HTML imports.
|
|
22
|
+
* Opens DB once at startup, closes on shutdown.
|
|
23
|
+
*
|
|
24
|
+
* @module src/serve/server
|
|
25
|
+
*/
|
|
26
|
+
import { withRemoteHostPathRedaction } from "./host-path-redaction";
|
|
26
27
|
import { PDFJS_ASSET_CACHE_CONTROL } from "./pdfjs-assets";
|
|
27
28
|
// HTML import - Bun handles bundling TSX/CSS automatically via routes
|
|
28
29
|
import homepage from "./public/index.html";
|
|
@@ -571,7 +572,7 @@ export async function startServer(
|
|
|
571
572
|
development: isDev,
|
|
572
573
|
|
|
573
574
|
// Static routes - Bun handles HTML bundling and /_bun/* assets automatically
|
|
574
|
-
routes: {
|
|
575
|
+
routes: withRemoteHostPathRedaction({
|
|
575
576
|
"/mcp": gateway.route,
|
|
576
577
|
...clipperRoutesForBind(
|
|
577
578
|
isHttpGatewayLoopbackBind(gatewayConfig.host),
|
|
@@ -1743,7 +1744,7 @@ export async function startServer(
|
|
|
1743
1744
|
);
|
|
1744
1745
|
},
|
|
1745
1746
|
},
|
|
1746
|
-
},
|
|
1747
|
+
}),
|
|
1747
1748
|
// Production catch-all: /vendor/pdfjs prefix, then hashed SPA chunks
|
|
1748
1749
|
// (gzip + immutable) and the private SPA source — the same factory the
|
|
1749
1750
|
// tests mount (no test-only fallback path).
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { ContentTypeBoostStatus } from "../config/content-types";
|
|
2
2
|
import type { ActivationStatus } from "../core/activation-status";
|
|
3
3
|
import type { ChunkingStatus } from "../store/chunking";
|
|
4
|
+
import type {
|
|
5
|
+
VectorPartitionStatus,
|
|
6
|
+
VectorRuntimeStatus,
|
|
7
|
+
} from "../store/vector/status";
|
|
4
8
|
|
|
5
9
|
export type HealthCheckStatus = "ok" | "warn" | "error";
|
|
6
10
|
|
|
@@ -109,6 +113,16 @@ export interface ResidentStatus {
|
|
|
109
113
|
content: number;
|
|
110
114
|
index: number;
|
|
111
115
|
};
|
|
116
|
+
/** Present only while a background job is in trouble. */
|
|
117
|
+
backgroundIssues?: BackgroundIssue[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A resident background job that keeps failing, has stopped retrying, or overruns. */
|
|
121
|
+
export interface BackgroundIssue {
|
|
122
|
+
job: "embed" | "resident";
|
|
123
|
+
state: "failing" | "parked" | "overrunning" | "unresponsive";
|
|
124
|
+
consecutiveFailures: number;
|
|
125
|
+
runningSeconds: number | null;
|
|
112
126
|
}
|
|
113
127
|
|
|
114
128
|
export interface BackgroundServiceState {
|
|
@@ -186,6 +200,8 @@ export interface AppStatusResponse {
|
|
|
186
200
|
totalDocuments: number;
|
|
187
201
|
totalChunks: number;
|
|
188
202
|
embeddingBacklog: number;
|
|
203
|
+
vectorPartitions?: VectorPartitionStatus[];
|
|
204
|
+
vectorRuntime?: VectorRuntimeStatus;
|
|
189
205
|
lastUpdated: string | null;
|
|
190
206
|
recentErrors: number;
|
|
191
207
|
healthy: boolean;
|
package/src/serve/status.ts
CHANGED
|
@@ -734,6 +734,8 @@ export async function buildAppStatus(
|
|
|
734
734
|
totalDocuments: status.activeDocuments,
|
|
735
735
|
totalChunks: status.totalChunks,
|
|
736
736
|
embeddingBacklog: status.embeddingBacklog,
|
|
737
|
+
vectorPartitions: status.vectorPartitions,
|
|
738
|
+
vectorRuntime: status.vectorRuntime,
|
|
737
739
|
chunking: status.chunking,
|
|
738
740
|
lastUpdated: status.lastUpdatedAt,
|
|
739
741
|
recentErrors: status.recentErrors,
|
|
@@ -45,6 +45,9 @@ export const WATCHER_MAX_SUPPRESSION_ENTRIES = 4_096;
|
|
|
45
45
|
/** Bounded retry delay after failed classification/sync. */
|
|
46
46
|
export const WATCHER_RETRY_BACKOFF_MS = 500;
|
|
47
47
|
|
|
48
|
+
/** Retry delay while another writer holds the shared writer lease. */
|
|
49
|
+
export const WATCHER_LEASE_RETRY_MS = 5_000;
|
|
50
|
+
|
|
48
51
|
/**
|
|
49
52
|
* Single fixed budget for fallback classification across visited directories,
|
|
50
53
|
* candidates, removals, dirty dirs, and aggregate store rows.
|
|
@@ -219,7 +219,8 @@ export function requeueAfterFailure(
|
|
|
219
219
|
collectionName: string,
|
|
220
220
|
exact: string[],
|
|
221
221
|
dirty: string[],
|
|
222
|
-
forceFlags?: PendingForceFlags
|
|
222
|
+
forceFlags?: PendingForceFlags,
|
|
223
|
+
delayMs = WATCHER_RETRY_BACKOFF_MS
|
|
223
224
|
): void {
|
|
224
225
|
queueWithoutSchedule(host, collectionName, exact, dirty, forceFlags);
|
|
225
226
|
if (host.disposed()) {
|
|
@@ -241,7 +242,7 @@ export function requeueAfterFailure(
|
|
|
241
242
|
host.timers.delete(collectionName);
|
|
242
243
|
host.retryScheduled.delete(collectionName);
|
|
243
244
|
startFlush(host, collectionName);
|
|
244
|
-
},
|
|
245
|
+
}, delayMs)
|
|
245
246
|
);
|
|
246
247
|
}
|
|
247
248
|
|