@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
@@ -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.collections,
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.collections,
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: () => findingsScheduler?.dispose(),
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 { ResidentStatus, RuntimeMode } from "./status-model";
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