@gmickel/gno 2.5.1 → 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.
- package/README.md +60 -5
- package/assets/skill/README.md +5 -1
- package/assets/skill/SKILL.md +75 -1
- package/assets/skill/cli-reference.md +123 -0
- package/assets/skill/examples.md +30 -0
- package/assets/skill/mcp-reference.md +52 -0
- 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.6.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.6.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -1
- package/spec/cli.md +347 -24
- package/spec/mcp.md +198 -2
- package/spec/output-schemas/capture-receipt.schema.json +3 -0
- package/spec/output-schemas/mcp-capture-result.schema.json +3 -0
- package/spec/output-schemas/memory-remember.schema.json +8 -2
- package/spec/output-schemas/request-status.schema.json +113 -0
- 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/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/ls.ts +3 -0
- package/src/cli/commands/memory.ts +12 -3
- 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/program.ts +388 -1
- 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 +3 -0
- package/src/core/memory-remember.ts +233 -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/index.ts +9 -0
- package/src/mcp/context.ts +8 -0
- package/src/mcp/http-egress.ts +4 -0
- package/src/mcp/http-transport.ts +2 -0
- package/src/mcp/tools/capture.ts +87 -83
- package/src/mcp/tools/index.ts +66 -0
- package/src/mcp/tools/memory-remember.ts +7 -0
- 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/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/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/components/sessions/snippet.tsx +53 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/hooks/use-api.ts +10 -2
- package/src/serve/public/lib/request-intent.ts +69 -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 +10 -0
- package/src/serve/public/pages/DocView.tsx +15 -1
- package/src/serve/public/pages/DocumentEditor.tsx +139 -96
- package/src/serve/public/pages/Sessions.tsx +350 -0
- package/src/serve/resident-runtime.ts +43 -3
- 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 +205 -1
- package/src/serve/session-automation.ts +146 -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/sqlite/adapter.ts +54 -15
- package/src/store/sqlite/scoped-index.ts +9 -0
- 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
|
+
}
|
|
@@ -527,6 +527,30 @@ function isDatabaseLockedError(cause: unknown): boolean {
|
|
|
527
527
|
);
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
/**
|
|
531
|
+
* Run a best-effort write without waiting for another writer: when the index
|
|
532
|
+
* is locked (SQLITE_BUSY) the write is skipped instead of blocking the event
|
|
533
|
+
* loop for the configured busy timeout.
|
|
534
|
+
*/
|
|
535
|
+
function withoutBusyWait(db: Database, write: () => void): void {
|
|
536
|
+
const current =
|
|
537
|
+
db.query<{ timeout: number }, []>("PRAGMA busy_timeout").get()?.timeout ??
|
|
538
|
+
0;
|
|
539
|
+
db.exec("PRAGMA busy_timeout = 0");
|
|
540
|
+
try {
|
|
541
|
+
write();
|
|
542
|
+
} catch (cause) {
|
|
543
|
+
const code = (cause as { code?: unknown } | null)?.code;
|
|
544
|
+
const busy =
|
|
545
|
+
code === "SQLITE_BUSY" ||
|
|
546
|
+
code === "SQLITE_LOCKED" ||
|
|
547
|
+
isDatabaseLockedError(cause);
|
|
548
|
+
if (!busy) throw cause;
|
|
549
|
+
} finally {
|
|
550
|
+
db.exec(`PRAGMA busy_timeout = ${current}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
530
554
|
/** Resolve a caller-supplied busy_timeout, defaulting rather than using 0. */
|
|
531
555
|
function resolveBusyTimeoutMs(busyTimeoutMs?: number): number {
|
|
532
556
|
if (
|
|
@@ -811,6 +835,11 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
811
835
|
this.configPath = configPath;
|
|
812
836
|
}
|
|
813
837
|
|
|
838
|
+
/** Path this adapter opened; names the index's request ledger. */
|
|
839
|
+
getDbPath(): string {
|
|
840
|
+
return this.dbPath;
|
|
841
|
+
}
|
|
842
|
+
|
|
814
843
|
/**
|
|
815
844
|
* Get raw SQLite database handle for vector operations.
|
|
816
845
|
* Part of SqliteDbProvider interface - use with isSqliteDbProvider() type guard.
|
|
@@ -1202,9 +1231,12 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1202
1231
|
return ok(null);
|
|
1203
1232
|
}
|
|
1204
1233
|
if (row.fingerprint !== expectedFingerprint) {
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1234
|
+
// Best-effort cleanup: a busy writer leaves the stale row for later.
|
|
1235
|
+
withoutBusyWait(db, () =>
|
|
1236
|
+
db.run(
|
|
1237
|
+
"DELETE FROM activation_receipts WHERE collection = ? AND connector_target = ?",
|
|
1238
|
+
[collection, connectorTarget]
|
|
1239
|
+
)
|
|
1208
1240
|
);
|
|
1209
1241
|
return ok(null);
|
|
1210
1242
|
}
|
|
@@ -1216,9 +1248,12 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1216
1248
|
receipt.collection !== row.collection ||
|
|
1217
1249
|
(receipt.evidence.connectorTarget ?? "") !== row.connector_target
|
|
1218
1250
|
) {
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1251
|
+
// Best-effort cleanup: a busy writer leaves the stale row for later.
|
|
1252
|
+
withoutBusyWait(db, () =>
|
|
1253
|
+
db.run(
|
|
1254
|
+
"DELETE FROM activation_receipts WHERE collection = ? AND connector_target = ?",
|
|
1255
|
+
[collection, connectorTarget]
|
|
1256
|
+
)
|
|
1222
1257
|
);
|
|
1223
1258
|
return ok(null);
|
|
1224
1259
|
}
|
|
@@ -1243,8 +1278,11 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1243
1278
|
if (!serialized.ok) {
|
|
1244
1279
|
return err("INVALID_INPUT", serialized.error);
|
|
1245
1280
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1281
|
+
// A receipt is a cache: status paths on resident event loops must not
|
|
1282
|
+
// busy-wait behind another writer (for example an import child).
|
|
1283
|
+
withoutBusyWait(db, () =>
|
|
1284
|
+
db.run(
|
|
1285
|
+
`INSERT INTO activation_receipts (
|
|
1248
1286
|
collection, connector_target, schema_version, fingerprint,
|
|
1249
1287
|
receipt_json, updated_at
|
|
1250
1288
|
) VALUES (?, ?, ?, ?, ?, datetime('now'))
|
|
@@ -1253,13 +1291,14 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1253
1291
|
fingerprint = excluded.fingerprint,
|
|
1254
1292
|
receipt_json = excluded.receipt_json,
|
|
1255
1293
|
updated_at = datetime('now')`,
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1294
|
+
[
|
|
1295
|
+
serialized.projected.collection,
|
|
1296
|
+
serialized.connectorTarget,
|
|
1297
|
+
serialized.projected.schemaVersion,
|
|
1298
|
+
serialized.projected.fingerprint,
|
|
1299
|
+
serialized.json,
|
|
1300
|
+
]
|
|
1301
|
+
)
|
|
1263
1302
|
);
|
|
1264
1303
|
return ok(undefined);
|
|
1265
1304
|
} catch (cause) {
|
|
@@ -2,6 +2,7 @@ import type { Config } from "../../config/types";
|
|
|
2
2
|
|
|
3
3
|
import { getIndexDbPath } from "../../app/constants";
|
|
4
4
|
import { indexesMatch } from "../../core/indexed-reference";
|
|
5
|
+
import { assertSessionBinding } from "../../sessions/binding";
|
|
5
6
|
import { SqliteAdapter } from "./adapter";
|
|
6
7
|
|
|
7
8
|
export interface ScopedIndexStore {
|
|
@@ -36,6 +37,14 @@ export async function openScopedIndexStore(options: {
|
|
|
36
37
|
);
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
// A session archive index only opens with its own archive config.
|
|
41
|
+
await assertSessionBinding({
|
|
42
|
+
config: options.config,
|
|
43
|
+
configPath: options.configPath ?? "<inline-config>",
|
|
44
|
+
indexName: requestedIndexName ?? "default",
|
|
45
|
+
dbPath,
|
|
46
|
+
});
|
|
47
|
+
|
|
39
48
|
const store = new SqliteAdapter();
|
|
40
49
|
store.setConfigPath(options.configPath ?? "<inline-config>");
|
|
41
50
|
const openResult = await store.open(
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
61f94e89a85a038072704ca3bc398c38c264cf6b9a09e2e891058d1d7795a453 gno-browser-clipper-v2.5.1.zip
|