@gmickel/gno 2.4.0 → 2.6.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 (136) hide show
  1. package/README.md +69 -6
  2. package/assets/skill/README.md +5 -1
  3. package/assets/skill/SKILL.md +98 -1
  4. package/assets/skill/cli-reference.md +140 -0
  5. package/assets/skill/examples.md +45 -0
  6. package/assets/skill/mcp-reference.md +62 -0
  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.4.0.zip → gno-browser-clipper-v2.6.0.zip} +0 -0
  13. package/browser-extension/artifacts/gno-browser-clipper-v2.6.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 +358 -24
  17. package/spec/compiled-context.md +68 -0
  18. package/spec/mcp.md +223 -2
  19. package/spec/output-schemas/capture-receipt.schema.json +3 -0
  20. package/spec/output-schemas/compiled-context-check.schema.json +44 -0
  21. package/spec/output-schemas/compiled-context-file.schema.json +165 -0
  22. package/spec/output-schemas/compiled-context-preview.schema.json +142 -0
  23. package/spec/output-schemas/mcp-capture-result.schema.json +3 -0
  24. package/spec/output-schemas/memory-remember.schema.json +8 -2
  25. package/spec/output-schemas/request-status.schema.json +113 -0
  26. package/spec/output-schemas/sessions-automation-run.schema.json +46 -0
  27. package/spec/output-schemas/sessions-discovery.schema.json +38 -0
  28. package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
  29. package/spec/output-schemas/sessions-status.schema.json +432 -0
  30. package/src/app/compiled-context-files.ts +361 -0
  31. package/src/app/compiled-context.ts +240 -0
  32. package/src/app/context-surface.ts +7 -2
  33. package/src/cli/commands/ask.ts +14 -2
  34. package/src/cli/commands/audit.ts +4 -0
  35. package/src/cli/commands/capture.ts +55 -96
  36. package/src/cli/commands/context-compiled.ts +136 -0
  37. package/src/cli/commands/daemon.ts +41 -0
  38. package/src/cli/commands/ls.ts +3 -0
  39. package/src/cli/commands/memory.ts +12 -3
  40. package/src/cli/commands/request-status.ts +59 -0
  41. package/src/cli/commands/reset.ts +39 -5
  42. package/src/cli/commands/sessions.ts +713 -0
  43. package/src/cli/commands/shared.ts +14 -1
  44. package/src/cli/errors.ts +10 -3
  45. package/src/cli/program.ts +458 -1
  46. package/src/cli/session-binding.ts +49 -0
  47. package/src/config/types.ts +8 -0
  48. package/src/core/capture-publish.ts +239 -0
  49. package/src/core/capture-sync.ts +3 -0
  50. package/src/core/compiled-context.ts +254 -0
  51. package/src/core/context-budget.ts +2 -10
  52. package/src/core/file-lock.ts +22 -5
  53. package/src/core/folder-setup-planning.ts +2 -1
  54. package/src/core/memory-remember.ts +233 -122
  55. package/src/core/memory-types.ts +11 -0
  56. package/src/core/network-boundary-inventory.ts +16 -0
  57. package/src/core/request-receipts.ts +671 -0
  58. package/src/core/setup-receipt.ts +27 -20
  59. package/src/core/typed-metadata.ts +4 -0
  60. package/src/core/validation.ts +9 -2
  61. package/src/core/windows-private-path.ts +96 -0
  62. package/src/index.ts +11 -2
  63. package/src/ingestion/compiled-context.ts +15 -0
  64. package/src/ingestion/sync.ts +40 -8
  65. package/src/ingestion/walker.ts +5 -4
  66. package/src/llm/nodeLlamaCpp/simulator-install.ts +6 -2
  67. package/src/mcp/context.ts +8 -0
  68. package/src/mcp/http-egress.ts +15 -3
  69. package/src/mcp/http-transport.ts +2 -0
  70. package/src/mcp/retrieval-warnings.ts +24 -0
  71. package/src/mcp/tools/ask.ts +14 -1
  72. package/src/mcp/tools/capture.ts +87 -83
  73. package/src/mcp/tools/context.ts +46 -2
  74. package/src/mcp/tools/index.ts +119 -7
  75. package/src/mcp/tools/memory-remember.ts +7 -0
  76. package/src/mcp/tools/memory-shared.ts +7 -1
  77. package/src/mcp/tools/query.ts +3 -1
  78. package/src/mcp/tools/request-status.ts +73 -0
  79. package/src/mcp/tools/search.ts +3 -1
  80. package/src/mcp/tools/sessions.ts +208 -0
  81. package/src/mcp/tools/vsearch.ts +3 -1
  82. package/src/sdk/client.ts +252 -89
  83. package/src/sdk/index.ts +13 -0
  84. package/src/sdk/types.ts +82 -3
  85. package/src/serve/capture-service.ts +98 -32
  86. package/src/serve/compiled-context.ts +84 -0
  87. package/src/serve/config-sync.ts +3 -2
  88. package/src/serve/public/app.tsx +15 -1
  89. package/src/serve/public/components/CaptureModal.tsx +26 -8
  90. package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
  91. package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
  92. package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
  93. package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
  94. package/src/serve/public/components/sessions/api.ts +40 -0
  95. package/src/serve/public/components/sessions/snippet.tsx +53 -0
  96. package/src/serve/public/globals.built.css +1 -1
  97. package/src/serve/public/hooks/use-api.ts +10 -2
  98. package/src/serve/public/lib/request-intent.ts +69 -0
  99. package/src/serve/public/lib/workspace-actions.ts +12 -1
  100. package/src/serve/public/lib/workspace-tabs.ts +4 -0
  101. package/src/serve/public/pages/CompiledContext.tsx +364 -0
  102. package/src/serve/public/pages/Dashboard.tsx +17 -0
  103. package/src/serve/public/pages/DocView.tsx +15 -1
  104. package/src/serve/public/pages/DocumentEditor.tsx +139 -96
  105. package/src/serve/public/pages/Sessions.tsx +350 -0
  106. package/src/serve/resident-runtime.ts +43 -3
  107. package/src/serve/routes/api.ts +476 -147
  108. package/src/serve/routes/sessions.ts +766 -0
  109. package/src/serve/security.ts +9 -0
  110. package/src/serve/server.ts +248 -3
  111. package/src/serve/session-automation.ts +146 -0
  112. package/src/serve/spa-production-build.ts +6 -5
  113. package/src/sessions/archive.ts +348 -0
  114. package/src/sessions/automation-state.ts +444 -0
  115. package/src/sessions/automation-status.ts +239 -0
  116. package/src/sessions/automation.ts +1169 -0
  117. package/src/sessions/binding.ts +105 -0
  118. package/src/sessions/claude-hook.ts +240 -0
  119. package/src/sessions/config.ts +176 -0
  120. package/src/sessions/format.ts +191 -0
  121. package/src/sessions/import-child-env.ts +8 -0
  122. package/src/sessions/import-child.ts +152 -0
  123. package/src/sessions/parsers/claude-code.ts +259 -0
  124. package/src/sessions/parsers/codex.ts +303 -0
  125. package/src/sessions/parsers/hermes.ts +248 -0
  126. package/src/sessions/parsers/openclaw.ts +496 -0
  127. package/src/sessions/parsers/shared.ts +184 -0
  128. package/src/sessions/sanitize.ts +222 -0
  129. package/src/sessions/service.ts +1533 -0
  130. package/src/sessions/setup.ts +477 -0
  131. package/src/sessions/sources.ts +518 -0
  132. package/src/sessions/state.ts +118 -0
  133. package/src/sessions/types.ts +457 -0
  134. package/src/store/sqlite/adapter.ts +68 -16
  135. package/src/store/sqlite/scoped-index.ts +9 -0
  136. package/browser-extension/artifacts/gno-browser-clipper-v2.4.0.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";
@@ -51,6 +52,7 @@ import { defaultSyncService, withContentTypeRules } from "../ingestion";
51
52
  import { withOwnedInferenceScope } from "../llm/inference-scope";
52
53
  import { getActivePreset } from "../llm/registry";
53
54
  import { createToolContext, Mutex } from "../mcp/context";
55
+ import { watchedCollections } from "../sessions/config";
54
56
  import { SqliteAdapter } from "../store/sqlite/adapter";
55
57
  import {
56
58
  createServerContext,
@@ -67,6 +69,10 @@ import {
67
69
  createStandaloneResidentStatus,
68
70
  buildResidentStatusSnapshot,
69
71
  } from "./resident-status";
72
+ import {
73
+ notifyAutomationImport,
74
+ SessionAutomationScheduler,
75
+ } from "./session-automation";
70
76
  import { CollectionWatchService as DefaultCollectionWatchService } from "./watch-service";
71
77
 
72
78
  const OWNER_LOCK_TIMEOUT_MS = 0;
@@ -83,6 +89,9 @@ export interface ResidentRuntimeOptions {
83
89
  watchCallbacks?: CollectionWatchCallbacks;
84
90
  /** Daemon mode only: observes every scheduled findings-pass attempt. */
85
91
  onFindingsResult?: (result: FindingsPassResult) => void;
92
+ /** Daemon mode on a session archive: observes every automation run. */
93
+ onSessionAutomationResult?: (result: SessionAutomationRunResult) => void;
94
+ onSessionAutomationError?: (error: unknown) => void;
86
95
  readerLimit?: number;
87
96
  readerQueueLimit?: number;
88
97
  shutdownDeadlineMs?: number;
@@ -117,6 +126,8 @@ export interface ResidentRuntime {
117
126
  readonly capsuleReverificationScheduler: SavedCapsuleReverificationScheduler;
118
127
  /** Present only when daemon mode runs with `findings.enabled`. */
119
128
  readonly findingsScheduler: FindingsScheduler | null;
129
+ /** Present only when daemon mode runs on a session-archive config. */
130
+ readonly sessionAutomation: SessionAutomationScheduler | null;
120
131
  readonly modelManager: Pick<
121
132
  ModelManager,
122
133
  "acquireLease" | "getLifecycleStats" | "disposeAll"
@@ -322,7 +333,7 @@ export async function startResidentRuntime(
322
333
  deps.watchServiceFactory ??
323
334
  ((watchOptions) => new DefaultCollectionWatchService(watchOptions))
324
335
  )({
325
- collections: initialConfig.collections,
336
+ collections: watchedCollections(initialConfig),
326
337
  store,
327
338
  scheduler,
328
339
  eventBus: options.eventBus ?? null,
@@ -393,6 +404,31 @@ export async function startResidentRuntime(
393
404
  // otherwise keep reporting a schedule that no longer exists.
394
405
  await deleteFindingsRunState(findingsStatePath);
395
406
  }
407
+ // Session automation drains only in daemon mode on an archive config;
408
+ // `serve` never runs admitted hook work or schedules.
409
+ let sessionAutomation: SessionAutomationScheduler | null = null;
410
+ if (mode === "daemon" && initialConfig.sessions) {
411
+ sessionAutomation = new SessionAutomationScheduler({
412
+ store,
413
+ configPath: actualConfigPath,
414
+ indexName: canonicalizeIndexName(options.index ?? DEFAULT_INDEX_NAME),
415
+ dbPath,
416
+ startBackgroundWork: (operation) => backgroundWork.start(operation),
417
+ onResult: (result) => {
418
+ notifyAutomationImport(result, {
419
+ markMutation: () => {
420
+ generations.content += 1;
421
+ generations.index += 1;
422
+ },
423
+ notifySyncComplete: (collections) =>
424
+ scheduler.notifySyncComplete(collections),
425
+ });
426
+ options.onSessionAutomationResult?.(result);
427
+ },
428
+ onError: options.onSessionAutomationError,
429
+ });
430
+ sessionAutomation.start();
431
+ }
396
432
  capsuleReverificationScheduler = new SavedCapsuleReverificationScheduler({
397
433
  deps: {
398
434
  store,
@@ -412,7 +448,7 @@ export async function startResidentRuntime(
412
448
  ctxHolder.config = config;
413
449
  ctxHolder.current = { ...ctxHolder.current, config };
414
450
  ctxHolder.watchService?.updateCollections(
415
- config.collections,
451
+ watchedCollections(config),
416
452
  withContentTypeRules({}, config)
417
453
  );
418
454
  },
@@ -475,6 +511,7 @@ export async function startResidentRuntime(
475
511
  jobManager,
476
512
  capsuleReverificationScheduler,
477
513
  findingsScheduler,
514
+ sessionAutomation,
478
515
  modelManager,
479
516
  mcpContext,
480
517
  generations,
@@ -621,7 +658,10 @@ export async function startResidentRuntime(
621
658
  mcpContext,
622
659
  modelManager,
623
660
  ownerLock,
624
- stopFindings: () => findingsScheduler?.dispose(),
661
+ stopFindings: () => {
662
+ findingsScheduler?.dispose();
663
+ sessionAutomation?.dispose();
664
+ },
625
665
  stopCapsules: () => capsuleReverificationScheduler.dispose(),
626
666
  closeEvents: () => options.eventBus?.close(),
627
667
  disposeContext: deps.disposeServerContext ?? disposeServerContext,