@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,457 @@
1
+ /**
2
+ * Shared contracts for native agent-session ingestion.
3
+ *
4
+ * Data flow: native source -> harness parser -> sanitizer -> one durable
5
+ * JSONL archive file per thread -> ordinary collection sync through the
6
+ * JSONL record adapter. Every surface (CLI, MCP, REST, SDK, Web UI) consumes
7
+ * the receipt and status shapes defined here.
8
+ *
9
+ * @module src/sessions/types
10
+ */
11
+
12
+ export const SESSION_HARNESSES = [
13
+ "codex",
14
+ "claude-code",
15
+ "openclaw",
16
+ "hermes",
17
+ ] as const;
18
+ export type SessionHarness = (typeof SESSION_HARNESSES)[number];
19
+
20
+ export const SESSION_HARNESS_LABELS: Record<SessionHarness, string> = {
21
+ codex: "Codex",
22
+ "claude-code": "Claude Code",
23
+ openclaw: "OpenClaw",
24
+ hermes: "Hermes",
25
+ };
26
+
27
+ /** Speaker classes that reach the archive. Everything else is skipped. */
28
+ export type SessionRole = "human" | "assistant";
29
+
30
+ /** Largest `limit` accepted by one import call. */
31
+ export const MAX_IMPORT_LIMIT = 100_000;
32
+
33
+ /** Version of the archive line layout; bumps force a rewrite on import. */
34
+ export const SESSION_ARCHIVE_FORMAT_VERSION = 2;
35
+
36
+ /** Bounds applied while reading sources. */
37
+ export const SESSION_LIMITS = {
38
+ /** Largest native file or database read in one unit. */
39
+ maxSourceBytes: 512 * 1024 * 1024,
40
+ /** Largest physical JSONL line parsed; larger lines are skipped and counted. */
41
+ maxLineBytes: 32 * 1024 * 1024,
42
+ /** Largest dialogue text kept for one turn; larger turns are skipped. */
43
+ maxTurnChars: 256 * 1024,
44
+ /** Largest number of archived turns for one thread. */
45
+ maxTurnsPerThread: 20_000,
46
+ /** Largest archive file written for one thread. */
47
+ maxArchiveBytesPerThread: 64 * 1024 * 1024,
48
+ /** Largest number of threads one database unit may yield. */
49
+ maxThreadsPerUnit: 50_000,
50
+ /** Units listed individually in a receipt. */
51
+ maxReceiptUnits: 200,
52
+ /** Distinct unknown record kinds reported per unit. */
53
+ maxDiagnosticKinds: 32,
54
+ /** Units enumerated below one source root. */
55
+ maxUnitsPerSource: 100_000,
56
+ } as const;
57
+
58
+ /** One archived dialogue turn after structural classification. */
59
+ export interface ParsedTurn {
60
+ /** Native logical turn ID (message/item ID or a stable ordinal locator). */
61
+ turnId: string;
62
+ role: SessionRole;
63
+ text: string;
64
+ /** Normalized ISO-8601 UTC timestamp; absent when unknown or unparseable. */
65
+ timestamp?: string;
66
+ /** Safe native locator inside the unit, e.g. `line:42` or `messages/17`. */
67
+ locator: string;
68
+ /** Working directory recorded for this turn, when the format carries one. */
69
+ cwd?: string;
70
+ }
71
+
72
+ export type SessionThreadKind = "main" | "subagent" | "fork" | "continuation";
73
+
74
+ /** One conversation thread as recovered from a native unit. */
75
+ export interface ParsedThread {
76
+ harness: SessionHarness;
77
+ /** Native thread identity (distinct for forks and subagents). */
78
+ threadId: string;
79
+ /** Native root session identity shared by forks/subagents when known. */
80
+ sessionId: string;
81
+ parentThreadId?: string;
82
+ kind: SessionThreadKind;
83
+ /** Recorded working directory for the thread. */
84
+ cwd?: string;
85
+ turns: ParsedTurn[];
86
+ }
87
+
88
+ /** Bounded, content-free diagnostics for one unit. */
89
+ export interface UnitDiagnostics {
90
+ /** Record kinds the parser does not recognise, with counts. */
91
+ unknownKinds: Record<string, number>;
92
+ malformedRecords: number;
93
+ overLimitRecords: number;
94
+ overLimitTurns: number;
95
+ /** Records skipped because they are injected context, not speech. */
96
+ injectedSkipped: number;
97
+ /** Records copied from a parent thread (fork/continuation history). */
98
+ copiedHistorySkipped: number;
99
+ /** Final line was cut mid-write (a growing file). */
100
+ truncatedTail: boolean;
101
+ /**
102
+ * Single-thread file units: assistant turns exist but no human turn was
103
+ * recognised (possible format drift); the unit stays incomplete.
104
+ */
105
+ humanTurnsMissing: boolean;
106
+ /**
107
+ * Database units: main threads with assistant turns but no human turn.
108
+ * Reported per thread, so one such thread cannot hold the unit back.
109
+ */
110
+ threadsWithoutHuman: number;
111
+ /**
112
+ * Database units: threads left unread because the unit reached
113
+ * `maxThreadsPerUnit`; the unit stays incomplete so its checkpoint does
114
+ * not advance past them.
115
+ */
116
+ threadsOverLimit: number;
117
+ /** Format revision reported by the source, when recorded. */
118
+ formatVersion?: string;
119
+ }
120
+
121
+ export const emptyDiagnostics = (): UnitDiagnostics => ({
122
+ unknownKinds: {},
123
+ malformedRecords: 0,
124
+ overLimitRecords: 0,
125
+ overLimitTurns: 0,
126
+ injectedSkipped: 0,
127
+ copiedHistorySkipped: 0,
128
+ truncatedTail: false,
129
+ humanTurnsMissing: false,
130
+ threadsWithoutHuman: 0,
131
+ threadsOverLimit: 0,
132
+ });
133
+
134
+ const SAFE_KIND = /[^A-Za-z0-9_.:/-]/g;
135
+
136
+ /** Record an unknown kind without retaining source content. */
137
+ export function noteUnknownKind(
138
+ diagnostics: UnitDiagnostics,
139
+ kind: unknown
140
+ ): void {
141
+ const label =
142
+ typeof kind === "string" && kind.length > 0
143
+ ? kind.slice(0, 64).replace(SAFE_KIND, "_")
144
+ : "(none)";
145
+ const known = diagnostics.unknownKinds[label];
146
+ if (
147
+ known === undefined &&
148
+ Object.keys(diagnostics.unknownKinds).length >=
149
+ SESSION_LIMITS.maxDiagnosticKinds
150
+ ) {
151
+ return;
152
+ }
153
+ diagnostics.unknownKinds[label] = (known ?? 0) + 1;
154
+ }
155
+
156
+ export interface ParseUnitResult {
157
+ threads: ParsedThread[];
158
+ diagnostics: UnitDiagnostics;
159
+ /** True when every byte of the unit was read and understood or skipped by policy. */
160
+ complete: boolean;
161
+ /** Parser identity recorded in archive provenance. */
162
+ parser: string;
163
+ }
164
+
165
+ /** Normalize a native timestamp to ISO-8601 UTC or drop it. */
166
+ export function normalizeTimestamp(value: unknown): string | undefined {
167
+ let date: Date | undefined;
168
+ if (typeof value === "string" && value.trim()) {
169
+ const trimmed = value.trim();
170
+ // Numeric strings are epoch seconds or milliseconds.
171
+ if (/^\d+(?:\.\d+)?$/.test(trimmed)) {
172
+ return normalizeTimestamp(Number(trimmed));
173
+ }
174
+ date = new Date(trimmed);
175
+ } else if (typeof value === "number" && Number.isFinite(value)) {
176
+ // Heuristic boundary between epoch seconds and epoch milliseconds.
177
+ date = new Date(value < 1e11 ? value * 1000 : value);
178
+ }
179
+ if (!date || Number.isNaN(date.getTime())) return undefined;
180
+ const year = date.getUTCFullYear();
181
+ if (year < 1990 || year > 2200) return undefined;
182
+ return date.toISOString();
183
+ }
184
+
185
+ // ─────────────────────────────────────────────────────────────────────────────
186
+ // Receipts and status (shared across every surface)
187
+ // ─────────────────────────────────────────────────────────────────────────────
188
+
189
+ export const SESSION_UNIT_OUTCOMES = [
190
+ "imported",
191
+ "updated",
192
+ "unchanged",
193
+ "skipped_policy",
194
+ "unsupported",
195
+ "incomplete",
196
+ "failed",
197
+ ] as const;
198
+ export type SessionUnitOutcome = (typeof SESSION_UNIT_OUTCOMES)[number];
199
+
200
+ export interface SessionImportCounts {
201
+ /** Threads written to the archive for the first time. */
202
+ imported: number;
203
+ /** Threads whose archive file changed. */
204
+ updated: number;
205
+ /** Threads whose archive file was already current. */
206
+ unchanged: number;
207
+ /** Threads withheld by policy (mixed-domain, over-limit). */
208
+ skippedPolicy: number;
209
+ /** Units whose format was not recognised. */
210
+ unsupported: number;
211
+ /** Units read only partially; their checkpoint did not advance. */
212
+ incomplete: number;
213
+ /** Units that could not be read. */
214
+ failed: number;
215
+ }
216
+
217
+ export interface SessionTurnCounts {
218
+ human: number;
219
+ assistant: number;
220
+ redactions: number;
221
+ injectedSkipped: number;
222
+ copiedHistorySkipped: number;
223
+ overLimit: number;
224
+ }
225
+
226
+ export interface SessionUnitReceipt {
227
+ sourceId: string;
228
+ harness: SessionHarness | null;
229
+ /** Safe locator (basename or database table path); never a host path. */
230
+ locator: string;
231
+ outcome: SessionUnitOutcome;
232
+ reason?: string;
233
+ threads: number;
234
+ turns: number;
235
+ /** Destination collections for archived threads of this unit. */
236
+ collections: string[];
237
+ unknownKinds?: Record<string, number>;
238
+ warnings?: string[];
239
+ }
240
+
241
+ export interface SessionImportReceipt {
242
+ schemaVersion: "1";
243
+ dryRun: boolean;
244
+ index: string;
245
+ sourceIds: string[];
246
+ status: "complete" | "partial" | "failed" | "nothing_to_do";
247
+ counts: SessionImportCounts;
248
+ turns: SessionTurnCounts;
249
+ units: SessionUnitReceipt[];
250
+ unitsTruncated: boolean;
251
+ /** Pending units left for a later run because `limit` was reached. */
252
+ deferredUnits: number;
253
+ lexical: {
254
+ status: "ready" | "failed" | "skipped";
255
+ collections: string[];
256
+ error?: string;
257
+ };
258
+ embedding: { backlog: number | null };
259
+ warnings: string[];
260
+ }
261
+
262
+ export interface SessionSourceStatus {
263
+ id: string;
264
+ harness: SessionHarness;
265
+ collection: string;
266
+ available: boolean;
267
+ units: {
268
+ total: number;
269
+ complete: number;
270
+ incomplete: number;
271
+ failed: number;
272
+ pending: number;
273
+ };
274
+ archivedThreads: number;
275
+ staleParser: number;
276
+ sourceUnavailable: number;
277
+ lastImportAt: string | null;
278
+ }
279
+
280
+ export interface SessionsStatus {
281
+ schemaVersion: "1";
282
+ configured: boolean;
283
+ index: string;
284
+ collections: Array<{ name: string; threads: number }>;
285
+ sources: SessionSourceStatus[];
286
+ /** Opt-in automation; `profiles` is empty when nothing is configured. */
287
+ automation: SessionAutomationStatus;
288
+ warnings: string[];
289
+ }
290
+
291
+ // ─────────────────────────────────────────────────────────────────────────────
292
+ // Automation (opt-in hooks and daemon schedules)
293
+ // ─────────────────────────────────────────────────────────────────────────────
294
+
295
+ /** What admitted a run: a host hook, a daemon schedule tick, or an explicit run. */
296
+ export type SessionTriggerKind = "hook" | "schedule" | "manual";
297
+
298
+ /** Outcome of one automation run. `up_to_date` is a verified no-op. */
299
+ export type SessionRunOutcome =
300
+ | "complete"
301
+ | "up_to_date"
302
+ | "partial"
303
+ | "failed";
304
+
305
+ /**
306
+ * Reader-facing profile state: `off` (no trigger enabled, nothing pending),
307
+ * `idle`, `pending`, `running`, `retrying` (a failed run waits for its
308
+ * backoff), `partial` or `failed` (the last run; see `recovery`).
309
+ */
310
+ export type SessionProfileState =
311
+ | "off"
312
+ | "idle"
313
+ | "pending"
314
+ | "running"
315
+ | "retrying"
316
+ | "partial"
317
+ | "failed";
318
+
319
+ export interface SessionRunRecord {
320
+ triggers: SessionTriggerKind[];
321
+ startedAt: string;
322
+ finishedAt: string;
323
+ outcome: SessionRunOutcome;
324
+ /** Stable reason code for partial/failed runs (no content, no paths). */
325
+ reason: string | null;
326
+ threads: { imported: number; updated: number; unchanged: number };
327
+ units: { incomplete: number; failed: number; deferred: number };
328
+ }
329
+
330
+ export interface SessionProfileStatus {
331
+ id: string;
332
+ sources: string[];
333
+ /** Destination archive collections of the selected sources. */
334
+ collections: string[];
335
+ state: SessionProfileState;
336
+ hook: {
337
+ harness: string;
338
+ enabled: boolean;
339
+ /** Owned entry present in the host settings; null when unreadable. */
340
+ installed: boolean | null;
341
+ } | null;
342
+ schedule: {
343
+ enabled: boolean;
344
+ cadence: string;
345
+ nextDueAt: string | null;
346
+ } | null;
347
+ limit: number;
348
+ retries: number;
349
+ pending: {
350
+ since: string;
351
+ triggers: SessionTriggerKind[];
352
+ } | null;
353
+ running: { startedAt: string; triggers: SessionTriggerKind[] } | null;
354
+ lastTrigger: { kind: SessionTriggerKind; at: string } | null;
355
+ lastRun: SessionRunRecord | null;
356
+ lastSuccessAt: string | null;
357
+ retryAt: string | null;
358
+ /** Next action for the owner when the profile needs attention. */
359
+ recovery: string | null;
360
+ }
361
+
362
+ export interface SessionAutomationStatus {
363
+ daemon: {
364
+ /** `running` only with a fresh heartbeat from a live daemon process. */
365
+ state: "running" | "not_running" | "stale";
366
+ heartbeatAt: string | null;
367
+ };
368
+ /** IANA timezone used for human-readable times; stored instants are UTC. */
369
+ timezone: string;
370
+ profiles: SessionProfileStatus[];
371
+ }
372
+
373
+ /** Result of one explicit or daemon-drained automation run. */
374
+ export interface SessionAutomationRunResult {
375
+ schemaVersion: "1";
376
+ profileId: string;
377
+ /** False when the run could not start (busy, disabled, nothing pending). */
378
+ ran: boolean;
379
+ outcome: SessionRunOutcome | "not_started";
380
+ reason: string | null;
381
+ /** True when admitted work is still waiting (deferred units, failure, busy). */
382
+ pending: boolean;
383
+ receipts: SessionImportReceipt[];
384
+ }
385
+
386
+ export interface SessionDiscoveryCandidate {
387
+ harness: SessionHarness;
388
+ /** Absolute host path; only local owner surfaces receive it. */
389
+ path: string;
390
+ units: number;
391
+ bytes: number;
392
+ truncated: boolean;
393
+ formatVersions: string[];
394
+ registeredAs: string | null;
395
+ }
396
+
397
+ export interface SessionsDiscovery {
398
+ schemaVersion: "1";
399
+ candidates: SessionDiscoveryCandidate[];
400
+ warnings: string[];
401
+ }
402
+
403
+ export type SessionsErrorCode =
404
+ | "SESSIONS_NOT_CONFIGURED"
405
+ | "SESSIONS_BINDING_MISMATCH"
406
+ | "SESSIONS_SELECTION_REQUIRED"
407
+ | "SESSIONS_DESTINATION_REQUIRED"
408
+ | "SESSIONS_UNKNOWN_SOURCE"
409
+ | "SESSIONS_UNKNOWN_COLLECTION"
410
+ | "SESSIONS_UNSAFE_PATH"
411
+ | "SESSIONS_SOURCE_UNAVAILABLE"
412
+ | "SESSIONS_UNSUPPORTED_FORMAT"
413
+ | "SESSIONS_INVALID_INPUT"
414
+ | "SESSIONS_BUSY"
415
+ | "SESSIONS_RUNTIME_FAILURE"
416
+ | "SESSIONS_UNKNOWN_PROFILE"
417
+ | "SESSIONS_UNSUPPORTED_INTEGRATION";
418
+
419
+ /** Typed error shared by every sessions surface. */
420
+ export class SessionsError extends Error {
421
+ readonly code: SessionsErrorCode;
422
+
423
+ constructor(code: SessionsErrorCode, message: string) {
424
+ super(message);
425
+ this.name = "SessionsError";
426
+ this.code = code;
427
+ }
428
+ }
429
+
430
+ /** Errors that describe the caller's input rather than runtime state. */
431
+ export const SESSIONS_VALIDATION_CODES: ReadonlySet<SessionsErrorCode> =
432
+ new Set([
433
+ "SESSIONS_NOT_CONFIGURED",
434
+ "SESSIONS_BINDING_MISMATCH",
435
+ "SESSIONS_SELECTION_REQUIRED",
436
+ "SESSIONS_DESTINATION_REQUIRED",
437
+ "SESSIONS_UNKNOWN_SOURCE",
438
+ "SESSIONS_UNKNOWN_COLLECTION",
439
+ "SESSIONS_UNSAFE_PATH",
440
+ "SESSIONS_UNSUPPORTED_FORMAT",
441
+ "SESSIONS_INVALID_INPUT",
442
+ "SESSIONS_UNKNOWN_PROFILE",
443
+ "SESSIONS_UNSUPPORTED_INTEGRATION",
444
+ ]);
445
+
446
+ /**
447
+ * Typed, path-free error for remote surfaces (REST, MCP). Filesystem and
448
+ * store failures carry host paths in their messages; those stay in the
449
+ * server log.
450
+ */
451
+ export function remoteSafeSessionsError(error: unknown): SessionsError {
452
+ if (error instanceof SessionsError) return error;
453
+ return new SessionsError(
454
+ "SESSIONS_RUNTIME_FAILURE",
455
+ "The session operation failed on the server (filesystem or index error); retry, or run the command locally for details."
456
+ );
457
+ }
@@ -0,0 +1,29 @@
1
+ /** Runtime details become provenance; existing partitions await measured re-keying. */
2
+ import type { Migration } from "./runner";
3
+
4
+ export const migration: Migration = {
5
+ version: 31,
6
+ name: "runtime_independent_vectors",
7
+ up(db): void {
8
+ db.exec(`
9
+ ALTER TABLE vector_partitions ADD COLUMN provenance TEXT;
10
+ ALTER TABLE vector_partitions ADD COLUMN legacy INTEGER NOT NULL DEFAULT 0;
11
+ -- Vector-defining key shared by a primary and its confirmed forks.
12
+ ALTER TABLE vector_partitions ADD COLUMN base_fingerprint TEXT;
13
+ -- Runtime fingerprint of a confirmed separate partition; NULL = primary.
14
+ ALTER TABLE vector_partitions ADD COLUMN fork TEXT;
15
+ UPDATE vector_partitions SET legacy = 1;
16
+ CREATE TABLE vector_runtime_verdicts (
17
+ partition_id TEXT NOT NULL,
18
+ runtime TEXT NOT NULL,
19
+ label TEXT NOT NULL,
20
+ verdict TEXT NOT NULL CHECK (verdict IN ('compatible', 'incompatible')),
21
+ min_cosine REAL NOT NULL,
22
+ samples INTEGER NOT NULL CHECK (samples >= 0),
23
+ sample_ms REAL NOT NULL,
24
+ PRIMARY KEY (partition_id, runtime)
25
+ );
26
+
27
+ `);
28
+ },
29
+ };
@@ -0,0 +1,17 @@
1
+ /** Identity each caller (process runtime + env) last resolved, for status. */
2
+ import type { Migration } from "./runner";
3
+
4
+ export const migration: Migration = {
5
+ version: 32,
6
+ name: "vector_runtime_callers",
7
+ up(db): void {
8
+ db.exec(`
9
+ CREATE TABLE IF NOT EXISTS vector_runtime_callers (
10
+ caller TEXT PRIMARY KEY,
11
+ runtime TEXT NOT NULL,
12
+ label TEXT NOT NULL,
13
+ identity TEXT NOT NULL
14
+ );
15
+ `);
16
+ },
17
+ };
@@ -44,6 +44,8 @@ import { migration as m027 } from "./027-memory-scopes";
44
44
  import { migration as m028 } from "./028-vector-variants";
45
45
  import { migration as m029 } from "./029-graph-reference-state";
46
46
  import { migration as m030 } from "./030-typed-metadata";
47
+ import { migration as m031 } from "./031-runtime-independent-vectors";
48
+ import { migration as m032 } from "./032-vector-runtime-callers";
47
49
 
48
50
  /** All migrations in order */
49
51
  export const migrations = [
@@ -77,4 +79,6 @@ export const migrations = [
77
79
  m028,
78
80
  m029,
79
81
  m030,
82
+ m031,
83
+ m032,
80
84
  ];
@@ -158,7 +158,11 @@ import { getSchemaVersion, migrations, runMigrations } from "../migrations";
158
158
  import { err, ok } from "../types";
159
159
  import { getStoredEmbeddingFingerprint } from "../vector/freshness";
160
160
  import { modelTableName } from "../vector/sqlite-vec";
161
- import { getVariantStatus } from "../vector/status";
161
+ import {
162
+ getVariantStatus,
163
+ listVectorPartitions,
164
+ vectorRuntimeStatus,
165
+ } from "../vector/status";
162
166
  import {
163
167
  deleteSavedCapsuleRegistration as deleteStoredSavedCapsuleRegistration,
164
168
  getSavedCapsuleRegistration as getStoredSavedCapsuleRegistration,
@@ -527,6 +531,30 @@ function isDatabaseLockedError(cause: unknown): boolean {
527
531
  );
528
532
  }
529
533
 
534
+ /**
535
+ * Run a best-effort write without waiting for another writer: when the index
536
+ * is locked (SQLITE_BUSY) the write is skipped instead of blocking the event
537
+ * loop for the configured busy timeout.
538
+ */
539
+ function withoutBusyWait(db: Database, write: () => void): void {
540
+ const current =
541
+ db.query<{ timeout: number }, []>("PRAGMA busy_timeout").get()?.timeout ??
542
+ 0;
543
+ db.exec("PRAGMA busy_timeout = 0");
544
+ try {
545
+ write();
546
+ } catch (cause) {
547
+ const code = (cause as { code?: unknown } | null)?.code;
548
+ const busy =
549
+ code === "SQLITE_BUSY" ||
550
+ code === "SQLITE_LOCKED" ||
551
+ isDatabaseLockedError(cause);
552
+ if (!busy) throw cause;
553
+ } finally {
554
+ db.exec(`PRAGMA busy_timeout = ${current}`);
555
+ }
556
+ }
557
+
530
558
  /** Resolve a caller-supplied busy_timeout, defaulting rather than using 0. */
531
559
  function resolveBusyTimeoutMs(busyTimeoutMs?: number): number {
532
560
  if (
@@ -687,6 +715,11 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
687
715
  }
688
716
  }
689
717
 
718
+ /** Set this connection's SQLite busy_timeout (resident event-loop bound). */
719
+ setBusyTimeout(ms: number): void {
720
+ this.db?.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(ms))}`);
721
+ }
722
+
690
723
  /** Cap subsequent SQLite lock waits to the resident settlement deadline. */
691
724
  beginShutdown(deadline: number): void {
692
725
  this.shutdownDeadline = deadline;
@@ -811,6 +844,11 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
811
844
  this.configPath = configPath;
812
845
  }
813
846
 
847
+ /** Path this adapter opened; names the index's request ledger. */
848
+ getDbPath(): string {
849
+ return this.dbPath;
850
+ }
851
+
814
852
  /**
815
853
  * Get raw SQLite database handle for vector operations.
816
854
  * Part of SqliteDbProvider interface - use with isSqliteDbProvider() type guard.
@@ -1202,9 +1240,12 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1202
1240
  return ok(null);
1203
1241
  }
1204
1242
  if (row.fingerprint !== expectedFingerprint) {
1205
- db.run(
1206
- "DELETE FROM activation_receipts WHERE collection = ? AND connector_target = ?",
1207
- [collection, connectorTarget]
1243
+ // Best-effort cleanup: a busy writer leaves the stale row for later.
1244
+ withoutBusyWait(db, () =>
1245
+ db.run(
1246
+ "DELETE FROM activation_receipts WHERE collection = ? AND connector_target = ?",
1247
+ [collection, connectorTarget]
1248
+ )
1208
1249
  );
1209
1250
  return ok(null);
1210
1251
  }
@@ -1216,9 +1257,12 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1216
1257
  receipt.collection !== row.collection ||
1217
1258
  (receipt.evidence.connectorTarget ?? "") !== row.connector_target
1218
1259
  ) {
1219
- db.run(
1220
- "DELETE FROM activation_receipts WHERE collection = ? AND connector_target = ?",
1221
- [collection, connectorTarget]
1260
+ // Best-effort cleanup: a busy writer leaves the stale row for later.
1261
+ withoutBusyWait(db, () =>
1262
+ db.run(
1263
+ "DELETE FROM activation_receipts WHERE collection = ? AND connector_target = ?",
1264
+ [collection, connectorTarget]
1265
+ )
1222
1266
  );
1223
1267
  return ok(null);
1224
1268
  }
@@ -1243,8 +1287,11 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1243
1287
  if (!serialized.ok) {
1244
1288
  return err("INVALID_INPUT", serialized.error);
1245
1289
  }
1246
- db.run(
1247
- `INSERT INTO activation_receipts (
1290
+ // A receipt is a cache: status paths on resident event loops must not
1291
+ // busy-wait behind another writer (for example an import child).
1292
+ withoutBusyWait(db, () =>
1293
+ db.run(
1294
+ `INSERT INTO activation_receipts (
1248
1295
  collection, connector_target, schema_version, fingerprint,
1249
1296
  receipt_json, updated_at
1250
1297
  ) VALUES (?, ?, ?, ?, ?, datetime('now'))
@@ -1253,13 +1300,14 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1253
1300
  fingerprint = excluded.fingerprint,
1254
1301
  receipt_json = excluded.receipt_json,
1255
1302
  updated_at = datetime('now')`,
1256
- [
1257
- serialized.projected.collection,
1258
- serialized.connectorTarget,
1259
- serialized.projected.schemaVersion,
1260
- serialized.projected.fingerprint,
1261
- serialized.json,
1262
- ]
1303
+ [
1304
+ serialized.projected.collection,
1305
+ serialized.connectorTarget,
1306
+ serialized.projected.schemaVersion,
1307
+ serialized.projected.fingerprint,
1308
+ serialized.json,
1309
+ ]
1310
+ )
1263
1311
  );
1264
1312
  return ok(undefined);
1265
1313
  } catch (cause) {
@@ -5854,6 +5902,10 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5854
5902
  (embedModel ? getStoredEmbeddingFingerprint(db, embedModel) : null);
5855
5903
 
5856
5904
  const variantStatus = getVariantStatus(db, options);
5905
+ const vectorPartitions = listVectorPartitions(
5906
+ db,
5907
+ embedModel ?? undefined
5908
+ );
5857
5909
 
5858
5910
  // Get version
5859
5911
  const versionRow = db
@@ -6038,6 +6090,14 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
6038
6090
  ? (variantStatus.embeddedByCollection.get(s.name) ?? 0)
6039
6091
  : s.embedded_count,
6040
6092
  })),
6093
+ ...(vectorPartitions.length
6094
+ ? {
6095
+ vectorPartitions,
6096
+ ...(embedModel
6097
+ ? { vectorRuntime: vectorRuntimeStatus(db, embedModel) }
6098
+ : {}),
6099
+ }
6100
+ : {}),
6041
6101
  totalDocuments: totalsRow?.total ?? 0,
6042
6102
  activeDocuments: totalsRow?.active ?? 0,
6043
6103
  totalChunks: chunkCount,