@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
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArchiveIcon,
|
|
3
|
+
InfoIcon,
|
|
4
|
+
Loader2Icon,
|
|
5
|
+
MessagesSquareIcon,
|
|
6
|
+
} from "lucide-react";
|
|
7
|
+
import {
|
|
8
|
+
type FormEvent,
|
|
9
|
+
useCallback,
|
|
10
|
+
useEffect,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from "react";
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
SessionImportReceipt,
|
|
17
|
+
SessionsStatus,
|
|
18
|
+
} from "../components/sessions/api";
|
|
19
|
+
|
|
20
|
+
import { sessionsApi } from "../components/sessions/api";
|
|
21
|
+
import { AutomationPanel } from "../components/sessions/AutomationPanel";
|
|
22
|
+
import { SessionSearch } from "../components/sessions/SessionSearch";
|
|
23
|
+
import { SourcesPanel } from "../components/sessions/SourcesPanel";
|
|
24
|
+
import { Badge } from "../components/ui/badge";
|
|
25
|
+
import { Button } from "../components/ui/button";
|
|
26
|
+
import { Card, CardContent } from "../components/ui/card";
|
|
27
|
+
import { Input } from "../components/ui/input";
|
|
28
|
+
import { fetchServerCapabilities } from "../lib/server-capabilities";
|
|
29
|
+
|
|
30
|
+
interface PageProps {
|
|
31
|
+
navigate: (to: string | number) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Codes meaning "this instance is not a usable session archive". */
|
|
35
|
+
const UNBOUND_CODES = new Set([
|
|
36
|
+
"SESSIONS_NOT_CONFIGURED",
|
|
37
|
+
"SESSIONS_BINDING_MISMATCH",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const INIT_COMMAND =
|
|
41
|
+
"gno --config <archive.yml> --index sessions sessions init --archive <dir> --collection <name>";
|
|
42
|
+
const SERVE_COMMAND = "gno --config <archive.yml> --index sessions serve";
|
|
43
|
+
|
|
44
|
+
function CommandLine({ children }: { children: string }) {
|
|
45
|
+
return (
|
|
46
|
+
<code className="block whitespace-pre-wrap break-all rounded-md border border-border/60 bg-muted/40 px-3 py-2 font-mono text-xs">
|
|
47
|
+
{children}
|
|
48
|
+
</code>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function InitForm({ onDone }: { onDone: () => Promise<void> }) {
|
|
53
|
+
const [archive, setArchive] = useState("");
|
|
54
|
+
const [collection, setCollection] = useState("sessions");
|
|
55
|
+
const [error, setError] = useState<string | null>(null);
|
|
56
|
+
const [saving, setSaving] = useState(false);
|
|
57
|
+
|
|
58
|
+
const submit = async (event: FormEvent<HTMLFormElement>) => {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
setSaving(true);
|
|
61
|
+
const result = await sessionsApi("/api/sessions/init", {
|
|
62
|
+
method: "POST",
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
archive: archive.trim(),
|
|
65
|
+
collection: collection.trim(),
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
setSaving(false);
|
|
69
|
+
if (result.error) {
|
|
70
|
+
setError(result.error);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
setError(null);
|
|
74
|
+
await onDone();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<form
|
|
79
|
+
aria-label="Create session archive"
|
|
80
|
+
className="grid gap-3 sm:grid-cols-2"
|
|
81
|
+
onSubmit={(event) => void submit(event)}
|
|
82
|
+
>
|
|
83
|
+
<label className="grid gap-1 text-sm" htmlFor="sessions-init-archive">
|
|
84
|
+
Archive directory (absolute path)
|
|
85
|
+
<Input
|
|
86
|
+
id="sessions-init-archive"
|
|
87
|
+
onChange={(event) => setArchive(event.currentTarget.value)}
|
|
88
|
+
placeholder="/home/me/agent-archive"
|
|
89
|
+
required
|
|
90
|
+
value={archive}
|
|
91
|
+
/>
|
|
92
|
+
</label>
|
|
93
|
+
<label className="grid gap-1 text-sm" htmlFor="sessions-init-collection">
|
|
94
|
+
First archive collection
|
|
95
|
+
<Input
|
|
96
|
+
id="sessions-init-collection"
|
|
97
|
+
onChange={(event) => setCollection(event.currentTarget.value)}
|
|
98
|
+
pattern="[a-z0-9][a-z0-9_-]{0,63}"
|
|
99
|
+
required
|
|
100
|
+
value={collection}
|
|
101
|
+
/>
|
|
102
|
+
</label>
|
|
103
|
+
{error && (
|
|
104
|
+
<p
|
|
105
|
+
className="break-words text-destructive text-sm sm:col-span-2"
|
|
106
|
+
role="alert"
|
|
107
|
+
>
|
|
108
|
+
{error}
|
|
109
|
+
</p>
|
|
110
|
+
)}
|
|
111
|
+
<div className="sm:col-span-2">
|
|
112
|
+
<Button disabled={saving} type="submit">
|
|
113
|
+
{saving && <Loader2Icon className="animate-spin" />}
|
|
114
|
+
Create archive for this instance
|
|
115
|
+
</Button>
|
|
116
|
+
<p className="mt-2 text-muted-foreground text-xs">
|
|
117
|
+
Only works when this server was started with a dedicated archive
|
|
118
|
+
config and a named index; the default config is refused.
|
|
119
|
+
</p>
|
|
120
|
+
</div>
|
|
121
|
+
</form>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default function Sessions({ navigate }: PageProps) {
|
|
126
|
+
const [status, setStatus] = useState<SessionsStatus | null>(null);
|
|
127
|
+
const [unbound, setUnbound] = useState<string | null>(null);
|
|
128
|
+
const [error, setError] = useState<string | null>(null);
|
|
129
|
+
const [loading, setLoading] = useState(true);
|
|
130
|
+
const [localClient, setLocalClient] = useState(false);
|
|
131
|
+
const [busy, setBusy] = useState<string | null>(null);
|
|
132
|
+
const [receipts, setReceipts] = useState<
|
|
133
|
+
Record<string, SessionImportReceipt>
|
|
134
|
+
>({});
|
|
135
|
+
// Bumped when an action (import/remove) fails so focus moves to the alert
|
|
136
|
+
// instead of dropping to <body> from the disabled button.
|
|
137
|
+
const [actionErrorCount, setActionErrorCount] = useState(0);
|
|
138
|
+
const errorRef = useRef<HTMLDivElement>(null);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (actionErrorCount > 0) errorRef.current?.focus();
|
|
142
|
+
}, [actionErrorCount]);
|
|
143
|
+
|
|
144
|
+
const loadStatus = useCallback(async () => {
|
|
145
|
+
const result = await sessionsApi<SessionsStatus>("/api/sessions/status");
|
|
146
|
+
setLoading(false);
|
|
147
|
+
if (result.data) {
|
|
148
|
+
setStatus(result.data);
|
|
149
|
+
setUnbound(null);
|
|
150
|
+
setError(null);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (result.sessionsCode && UNBOUND_CODES.has(result.sessionsCode)) {
|
|
154
|
+
setStatus(null);
|
|
155
|
+
setUnbound(result.error);
|
|
156
|
+
setError(null);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// A transient failure (for example during background polling) keeps the
|
|
160
|
+
// last good status on screen, so run monitoring and controls survive.
|
|
161
|
+
setError(result.error);
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
void loadStatus();
|
|
166
|
+
void fetchServerCapabilities().then(({ data }) => {
|
|
167
|
+
setLocalClient(data?.localClient === true);
|
|
168
|
+
});
|
|
169
|
+
}, [loadStatus]);
|
|
170
|
+
|
|
171
|
+
const runImport = async (sourceId: string, dryRun: boolean) => {
|
|
172
|
+
setBusy(`${sourceId}:${dryRun ? "preview" : "import"}`);
|
|
173
|
+
const result = await sessionsApi<SessionImportReceipt>(
|
|
174
|
+
"/api/sessions/import",
|
|
175
|
+
{
|
|
176
|
+
method: "POST",
|
|
177
|
+
body: JSON.stringify({ sourceId, dryRun }),
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
setBusy(null);
|
|
181
|
+
if (!result.data) {
|
|
182
|
+
setError(result.error);
|
|
183
|
+
setActionErrorCount((count) => count + 1);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const receipt = result.data;
|
|
187
|
+
setError(null);
|
|
188
|
+
setReceipts((current) => ({ ...current, [sourceId]: receipt }));
|
|
189
|
+
if (!dryRun) await loadStatus();
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const removeSource = async (sourceId: string) => {
|
|
193
|
+
setBusy(`${sourceId}:remove`);
|
|
194
|
+
const result = await sessionsApi(
|
|
195
|
+
`/api/sessions/sources/${encodeURIComponent(sourceId)}`,
|
|
196
|
+
{ method: "DELETE" }
|
|
197
|
+
);
|
|
198
|
+
setBusy(null);
|
|
199
|
+
if (result.error) {
|
|
200
|
+
setError(result.error);
|
|
201
|
+
setActionErrorCount((count) => count + 1);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
setReceipts((current) => {
|
|
205
|
+
const next = { ...current };
|
|
206
|
+
delete next[sourceId];
|
|
207
|
+
return next;
|
|
208
|
+
});
|
|
209
|
+
await loadStatus();
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const archiveCollections = status?.collections.map((c) => c.name) ?? [];
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div className="min-h-screen">
|
|
216
|
+
<main className="mx-auto max-w-5xl space-y-8 px-4 py-6 sm:p-8">
|
|
217
|
+
<header className="flex flex-wrap items-start justify-between gap-4">
|
|
218
|
+
<div className="min-w-0 max-w-3xl space-y-3">
|
|
219
|
+
<div className="flex items-center gap-2 text-primary">
|
|
220
|
+
<ArchiveIcon aria-hidden="true" className="size-4" />
|
|
221
|
+
<span className="font-medium text-sm tracking-wide uppercase">
|
|
222
|
+
Agent session archive
|
|
223
|
+
</span>
|
|
224
|
+
</div>
|
|
225
|
+
<h1 className="font-semibold text-2xl tracking-tight sm:text-3xl">
|
|
226
|
+
Agent sessions
|
|
227
|
+
</h1>
|
|
228
|
+
{status && (
|
|
229
|
+
<p className="flex flex-wrap items-center gap-2 text-sm">
|
|
230
|
+
<span className="text-muted-foreground">Archive index</span>
|
|
231
|
+
<Badge className="font-mono" variant="secondary">
|
|
232
|
+
{status.index}
|
|
233
|
+
</Badge>
|
|
234
|
+
<span className="text-muted-foreground">
|
|
235
|
+
· {status.collections.length} collection
|
|
236
|
+
{status.collections.length === 1 ? "" : "s"}
|
|
237
|
+
</span>
|
|
238
|
+
</p>
|
|
239
|
+
)}
|
|
240
|
+
<p className="text-muted-foreground text-sm">
|
|
241
|
+
Import is manual by default. Nothing watches your agents or
|
|
242
|
+
imports on its own unless you switch on a hook or schedule below.
|
|
243
|
+
</p>
|
|
244
|
+
</div>
|
|
245
|
+
<Button onClick={() => navigate("/")} variant="outline">
|
|
246
|
+
Back to Dashboard
|
|
247
|
+
</Button>
|
|
248
|
+
</header>
|
|
249
|
+
|
|
250
|
+
<aside className="flex gap-3 rounded-lg border border-border/60 bg-muted/30 p-4 text-sm">
|
|
251
|
+
<InfoIcon
|
|
252
|
+
aria-hidden="true"
|
|
253
|
+
className="mt-0.5 size-4 shrink-0 text-primary"
|
|
254
|
+
/>
|
|
255
|
+
<p className="min-w-0">
|
|
256
|
+
This archive index is separate from your curated index. Broad search
|
|
257
|
+
in a curated instance does not include archived sessions unless you
|
|
258
|
+
explicitly register an archive collection in that index. Human turns
|
|
259
|
+
are what you wrote; assistant turns are agent output — suggestions,
|
|
260
|
+
not your decisions.
|
|
261
|
+
</p>
|
|
262
|
+
</aside>
|
|
263
|
+
|
|
264
|
+
{error && (
|
|
265
|
+
<Card className="border-destructive bg-destructive/10">
|
|
266
|
+
<CardContent
|
|
267
|
+
className="break-words py-4 text-destructive outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
268
|
+
ref={errorRef}
|
|
269
|
+
role="alert"
|
|
270
|
+
tabIndex={-1}
|
|
271
|
+
>
|
|
272
|
+
{error}
|
|
273
|
+
</CardContent>
|
|
274
|
+
</Card>
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
{loading && (
|
|
278
|
+
<p className="flex items-center gap-2 text-muted-foreground">
|
|
279
|
+
<Loader2Icon className="size-4 animate-spin" />
|
|
280
|
+
Loading archive status…
|
|
281
|
+
</p>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{unbound !== null && (
|
|
285
|
+
<section
|
|
286
|
+
aria-labelledby="sessions-unbound-heading"
|
|
287
|
+
className="space-y-4 rounded-lg border border-dashed border-border/70 p-4 sm:p-6"
|
|
288
|
+
>
|
|
289
|
+
<div className="flex items-center gap-2">
|
|
290
|
+
<MessagesSquareIcon
|
|
291
|
+
aria-hidden="true"
|
|
292
|
+
className="size-5 text-primary"
|
|
293
|
+
/>
|
|
294
|
+
<h2
|
|
295
|
+
className="font-semibold text-xl"
|
|
296
|
+
id="sessions-unbound-heading"
|
|
297
|
+
>
|
|
298
|
+
This server is not a session archive
|
|
299
|
+
</h2>
|
|
300
|
+
</div>
|
|
301
|
+
<p className="break-words text-muted-foreground text-sm">
|
|
302
|
+
{unbound}
|
|
303
|
+
</p>
|
|
304
|
+
<p className="text-sm">
|
|
305
|
+
Session archives live in their own config file and named index.
|
|
306
|
+
Create one, then start a server for it:
|
|
307
|
+
</p>
|
|
308
|
+
<CommandLine>{INIT_COMMAND}</CommandLine>
|
|
309
|
+
<CommandLine>{SERVE_COMMAND}</CommandLine>
|
|
310
|
+
{localClient && <InitForm onDone={loadStatus} />}
|
|
311
|
+
</section>
|
|
312
|
+
)}
|
|
313
|
+
|
|
314
|
+
{status && (
|
|
315
|
+
<>
|
|
316
|
+
{status.warnings.length > 0 && (
|
|
317
|
+
<ul className="list-disc space-y-1 pl-5 text-amber-800 text-sm dark:text-amber-200">
|
|
318
|
+
{status.warnings.map((warning) => (
|
|
319
|
+
<li className="break-words" key={warning}>
|
|
320
|
+
{warning}
|
|
321
|
+
</li>
|
|
322
|
+
))}
|
|
323
|
+
</ul>
|
|
324
|
+
)}
|
|
325
|
+
<SourcesPanel
|
|
326
|
+
archiveCollections={archiveCollections}
|
|
327
|
+
busy={busy}
|
|
328
|
+
localClient={localClient}
|
|
329
|
+
onChanged={loadStatus}
|
|
330
|
+
onImport={(sourceId, dryRun) => void runImport(sourceId, dryRun)}
|
|
331
|
+
onRemove={(sourceId) => void removeSource(sourceId)}
|
|
332
|
+
receipts={receipts}
|
|
333
|
+
sources={status.sources}
|
|
334
|
+
/>
|
|
335
|
+
<AutomationPanel
|
|
336
|
+
automation={status.automation}
|
|
337
|
+
localClient={localClient}
|
|
338
|
+
onChanged={loadStatus}
|
|
339
|
+
sources={status.sources}
|
|
340
|
+
/>
|
|
341
|
+
<SessionSearch
|
|
342
|
+
collections={archiveCollections}
|
|
343
|
+
navigate={(to) => navigate(to)}
|
|
344
|
+
/>
|
|
345
|
+
</>
|
|
346
|
+
)}
|
|
347
|
+
</main>
|
|
348
|
+
</div>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
@@ -9,6 +9,7 @@ import type { SyncResult } from "../ingestion";
|
|
|
9
9
|
import type { ModelManager } from "../llm/nodeLlamaCpp/lifecycle";
|
|
10
10
|
import type { ToolContext } from "../mcp/context";
|
|
11
11
|
import type { HttpMcpTransportStatus } from "../mcp/http-transport";
|
|
12
|
+
import type { SessionAutomationRunResult } from "../sessions/types";
|
|
12
13
|
import type { DocumentEventBus } from "./doc-events";
|
|
13
14
|
import type { EmbedResult, EmbedScheduler } from "./embed-scheduler";
|
|
14
15
|
import type { ContextHolder } from "./routes/api";
|
|
@@ -44,13 +45,16 @@ import { JobManager } from "../core/job-manager";
|
|
|
44
45
|
import { recordContentMutation } from "../core/mutation-generations";
|
|
45
46
|
import {
|
|
46
47
|
shutdownDuration,
|
|
48
|
+
RESIDENT_BUSY_TIMEOUT_MS,
|
|
47
49
|
SHUTDOWN_DRAIN_MS,
|
|
48
50
|
SHUTDOWN_ABORT_MS,
|
|
49
51
|
} from "../core/shutdown-budget";
|
|
52
|
+
import { acquireCliWriteLease } from "../core/write-lease";
|
|
50
53
|
import { defaultSyncService, withContentTypeRules } from "../ingestion";
|
|
51
54
|
import { withOwnedInferenceScope } from "../llm/inference-scope";
|
|
52
55
|
import { getActivePreset } from "../llm/registry";
|
|
53
56
|
import { createToolContext, Mutex } from "../mcp/context";
|
|
57
|
+
import { watchedCollections } from "../sessions/config";
|
|
54
58
|
import { SqliteAdapter } from "../store/sqlite/adapter";
|
|
55
59
|
import {
|
|
56
60
|
createServerContext,
|
|
@@ -58,7 +62,7 @@ import {
|
|
|
58
62
|
disposeServerContext,
|
|
59
63
|
type ServerContext,
|
|
60
64
|
} from "./context";
|
|
61
|
-
import { createEmbedScheduler } from "./embed-scheduler";
|
|
65
|
+
import { createEmbedScheduler, embedSchedulerIssues } from "./embed-scheduler";
|
|
62
66
|
import { FindingsScheduler, type FindingsPassResult } from "./findings-pass";
|
|
63
67
|
import { AdmissionController, ReaderGate } from "./resident-admission";
|
|
64
68
|
import { ResidentBackgroundWork } from "./resident-background-work";
|
|
@@ -67,6 +71,10 @@ import {
|
|
|
67
71
|
createStandaloneResidentStatus,
|
|
68
72
|
buildResidentStatusSnapshot,
|
|
69
73
|
} from "./resident-status";
|
|
74
|
+
import {
|
|
75
|
+
notifyAutomationImport,
|
|
76
|
+
SessionAutomationScheduler,
|
|
77
|
+
} from "./session-automation";
|
|
70
78
|
import { CollectionWatchService as DefaultCollectionWatchService } from "./watch-service";
|
|
71
79
|
|
|
72
80
|
const OWNER_LOCK_TIMEOUT_MS = 0;
|
|
@@ -83,6 +91,9 @@ export interface ResidentRuntimeOptions {
|
|
|
83
91
|
watchCallbacks?: CollectionWatchCallbacks;
|
|
84
92
|
/** Daemon mode only: observes every scheduled findings-pass attempt. */
|
|
85
93
|
onFindingsResult?: (result: FindingsPassResult) => void;
|
|
94
|
+
/** Daemon mode on a session archive: observes every automation run. */
|
|
95
|
+
onSessionAutomationResult?: (result: SessionAutomationRunResult) => void;
|
|
96
|
+
onSessionAutomationError?: (error: unknown) => void;
|
|
86
97
|
readerLimit?: number;
|
|
87
98
|
readerQueueLimit?: number;
|
|
88
99
|
shutdownDeadlineMs?: number;
|
|
@@ -117,6 +128,8 @@ export interface ResidentRuntime {
|
|
|
117
128
|
readonly capsuleReverificationScheduler: SavedCapsuleReverificationScheduler;
|
|
118
129
|
/** Present only when daemon mode runs with `findings.enabled`. */
|
|
119
130
|
readonly findingsScheduler: FindingsScheduler | null;
|
|
131
|
+
/** Present only when daemon mode runs on a session-archive config. */
|
|
132
|
+
readonly sessionAutomation: SessionAutomationScheduler | null;
|
|
120
133
|
readonly modelManager: Pick<
|
|
121
134
|
ModelManager,
|
|
122
135
|
"acquireLease" | "getLifecycleStats" | "disposeAll"
|
|
@@ -178,10 +191,25 @@ export type ResidentRuntimeDeps = {
|
|
|
178
191
|
eventBus?: DocumentEventBus | null;
|
|
179
192
|
callbacks?: CollectionWatchCallbacks;
|
|
180
193
|
syncOptions?: Parameters<typeof withContentTypeRules>[0];
|
|
194
|
+
acquireWriteLease?: () => Promise<(() => Promise<void>) | null>;
|
|
181
195
|
}) => CollectionWatchService;
|
|
182
196
|
modelManagerFactory?: (config: Config) => ModelManager;
|
|
183
197
|
};
|
|
184
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
|
+
|
|
185
213
|
export async function startResidentRuntime(
|
|
186
214
|
options: ResidentRuntimeOptions = {},
|
|
187
215
|
deps: ResidentRuntimeDeps = {}
|
|
@@ -263,6 +291,11 @@ export async function startResidentRuntime(
|
|
|
263
291
|
if (!syncCollections.ok) return failStartup(syncCollections.error.message);
|
|
264
292
|
const syncContexts = await store.syncContexts(initialConfig.contexts ?? []);
|
|
265
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);
|
|
266
299
|
|
|
267
300
|
let ctx: ServerContext;
|
|
268
301
|
try {
|
|
@@ -311,6 +344,7 @@ export async function startResidentRuntime(
|
|
|
311
344
|
onEmbedded: () => {
|
|
312
345
|
generations.index += 1;
|
|
313
346
|
},
|
|
347
|
+
acquireWriteLease: leaseFor(`gno ${mode} (background embed)`),
|
|
314
348
|
});
|
|
315
349
|
ctxHolder.scheduler = scheduler;
|
|
316
350
|
ctxHolder.current.scheduler = scheduler;
|
|
@@ -322,7 +356,7 @@ export async function startResidentRuntime(
|
|
|
322
356
|
deps.watchServiceFactory ??
|
|
323
357
|
((watchOptions) => new DefaultCollectionWatchService(watchOptions))
|
|
324
358
|
)({
|
|
325
|
-
collections: initialConfig
|
|
359
|
+
collections: watchedCollections(initialConfig),
|
|
326
360
|
store,
|
|
327
361
|
scheduler,
|
|
328
362
|
eventBus: options.eventBus ?? null,
|
|
@@ -340,6 +374,7 @@ export async function startResidentRuntime(
|
|
|
340
374
|
},
|
|
341
375
|
},
|
|
342
376
|
syncOptions: withContentTypeRules({}, initialConfig),
|
|
377
|
+
acquireWriteLease: leaseFor(`gno ${mode} (watch sync)`),
|
|
343
378
|
});
|
|
344
379
|
watchService.start();
|
|
345
380
|
ctxHolder.watchService = watchService;
|
|
@@ -393,6 +428,31 @@ export async function startResidentRuntime(
|
|
|
393
428
|
// otherwise keep reporting a schedule that no longer exists.
|
|
394
429
|
await deleteFindingsRunState(findingsStatePath);
|
|
395
430
|
}
|
|
431
|
+
// Session automation drains only in daemon mode on an archive config;
|
|
432
|
+
// `serve` never runs admitted hook work or schedules.
|
|
433
|
+
let sessionAutomation: SessionAutomationScheduler | null = null;
|
|
434
|
+
if (mode === "daemon" && initialConfig.sessions) {
|
|
435
|
+
sessionAutomation = new SessionAutomationScheduler({
|
|
436
|
+
store,
|
|
437
|
+
configPath: actualConfigPath,
|
|
438
|
+
indexName: canonicalizeIndexName(options.index ?? DEFAULT_INDEX_NAME),
|
|
439
|
+
dbPath,
|
|
440
|
+
startBackgroundWork: (operation) => backgroundWork.start(operation),
|
|
441
|
+
onResult: (result) => {
|
|
442
|
+
notifyAutomationImport(result, {
|
|
443
|
+
markMutation: () => {
|
|
444
|
+
generations.content += 1;
|
|
445
|
+
generations.index += 1;
|
|
446
|
+
},
|
|
447
|
+
notifySyncComplete: (collections) =>
|
|
448
|
+
scheduler.notifySyncComplete(collections),
|
|
449
|
+
});
|
|
450
|
+
options.onSessionAutomationResult?.(result);
|
|
451
|
+
},
|
|
452
|
+
onError: options.onSessionAutomationError,
|
|
453
|
+
});
|
|
454
|
+
sessionAutomation.start();
|
|
455
|
+
}
|
|
396
456
|
capsuleReverificationScheduler = new SavedCapsuleReverificationScheduler({
|
|
397
457
|
deps: {
|
|
398
458
|
store,
|
|
@@ -412,7 +472,7 @@ export async function startResidentRuntime(
|
|
|
412
472
|
ctxHolder.config = config;
|
|
413
473
|
ctxHolder.current = { ...ctxHolder.current, config };
|
|
414
474
|
ctxHolder.watchService?.updateCollections(
|
|
415
|
-
config
|
|
475
|
+
watchedCollections(config),
|
|
416
476
|
withContentTypeRules({}, config)
|
|
417
477
|
);
|
|
418
478
|
},
|
|
@@ -475,6 +535,7 @@ export async function startResidentRuntime(
|
|
|
475
535
|
jobManager,
|
|
476
536
|
capsuleReverificationScheduler,
|
|
477
537
|
findingsScheduler,
|
|
538
|
+
sessionAutomation,
|
|
478
539
|
modelManager,
|
|
479
540
|
mcpContext,
|
|
480
541
|
generations,
|
|
@@ -553,6 +614,7 @@ export async function startResidentRuntime(
|
|
|
553
614
|
failed: jobs.recent.filter((job) => job.status === "failed").length,
|
|
554
615
|
},
|
|
555
616
|
generations: { ...generations },
|
|
617
|
+
backgroundIssues: embedSchedulerIssues(scheduler.getState()),
|
|
556
618
|
});
|
|
557
619
|
},
|
|
558
620
|
setListenerPort(port) {
|
|
@@ -621,7 +683,10 @@ export async function startResidentRuntime(
|
|
|
621
683
|
mcpContext,
|
|
622
684
|
modelManager,
|
|
623
685
|
ownerLock,
|
|
624
|
-
stopFindings: () =>
|
|
686
|
+
stopFindings: () => {
|
|
687
|
+
findingsScheduler?.dispose();
|
|
688
|
+
sessionAutomation?.dispose();
|
|
689
|
+
},
|
|
625
690
|
stopCapsules: () => capsuleReverificationScheduler.dispose(),
|
|
626
691
|
closeEvents: () => options.eventBus?.close(),
|
|
627
692
|
disposeContext: deps.disposeServerContext ?? disposeServerContext,
|
|
@@ -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
|
|