@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.
- package/README.md +69 -6
- package/assets/skill/README.md +5 -1
- package/assets/skill/SKILL.md +98 -1
- package/assets/skill/cli-reference.md +140 -0
- package/assets/skill/examples.md +45 -0
- package/assets/skill/mcp-reference.md +62 -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.4.0.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 +358 -24
- package/spec/compiled-context.md +68 -0
- package/spec/mcp.md +223 -2
- package/spec/output-schemas/capture-receipt.schema.json +3 -0
- package/spec/output-schemas/compiled-context-check.schema.json +44 -0
- package/spec/output-schemas/compiled-context-file.schema.json +165 -0
- package/spec/output-schemas/compiled-context-preview.schema.json +142 -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/app/compiled-context-files.ts +361 -0
- package/src/app/compiled-context.ts +240 -0
- package/src/app/context-surface.ts +7 -2
- package/src/cli/commands/ask.ts +14 -2
- package/src/cli/commands/audit.ts +4 -0
- package/src/cli/commands/capture.ts +55 -96
- package/src/cli/commands/context-compiled.ts +136 -0
- 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/errors.ts +10 -3
- package/src/cli/program.ts +458 -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/compiled-context.ts +254 -0
- package/src/core/context-budget.ts +2 -10
- package/src/core/file-lock.ts +22 -5
- package/src/core/folder-setup-planning.ts +2 -1
- package/src/core/memory-remember.ts +233 -122
- package/src/core/memory-types.ts +11 -0
- package/src/core/network-boundary-inventory.ts +16 -0
- package/src/core/request-receipts.ts +671 -0
- package/src/core/setup-receipt.ts +27 -20
- package/src/core/typed-metadata.ts +4 -0
- package/src/core/validation.ts +9 -2
- package/src/core/windows-private-path.ts +96 -0
- package/src/index.ts +11 -2
- package/src/ingestion/compiled-context.ts +15 -0
- package/src/ingestion/sync.ts +40 -8
- package/src/ingestion/walker.ts +5 -4
- package/src/llm/nodeLlamaCpp/simulator-install.ts +6 -2
- package/src/mcp/context.ts +8 -0
- package/src/mcp/http-egress.ts +15 -3
- package/src/mcp/http-transport.ts +2 -0
- package/src/mcp/retrieval-warnings.ts +24 -0
- package/src/mcp/tools/ask.ts +14 -1
- package/src/mcp/tools/capture.ts +87 -83
- package/src/mcp/tools/context.ts +46 -2
- package/src/mcp/tools/index.ts +119 -7
- package/src/mcp/tools/memory-remember.ts +7 -0
- package/src/mcp/tools/memory-shared.ts +7 -1
- package/src/mcp/tools/query.ts +3 -1
- package/src/mcp/tools/request-status.ts +73 -0
- package/src/mcp/tools/search.ts +3 -1
- package/src/mcp/tools/sessions.ts +208 -0
- package/src/mcp/tools/vsearch.ts +3 -1
- package/src/sdk/client.ts +252 -89
- package/src/sdk/index.ts +13 -0
- package/src/sdk/types.ts +82 -3
- package/src/serve/capture-service.ts +98 -32
- package/src/serve/compiled-context.ts +84 -0
- package/src/serve/config-sync.ts +3 -2
- package/src/serve/public/app.tsx +15 -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 +4 -0
- package/src/serve/public/pages/CompiledContext.tsx +364 -0
- package/src/serve/public/pages/Dashboard.tsx +17 -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 +248 -3
- package/src/serve/session-automation.ts +146 -0
- package/src/serve/spa-production-build.ts +6 -5
- 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 +68 -16
- package/src/store/sqlite/scoped-index.ts +9 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.4.0.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 (
|
|
@@ -669,7 +693,20 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
669
693
|
async close(): Promise<void> {
|
|
670
694
|
this.fenceForShutdown();
|
|
671
695
|
if (this.db) {
|
|
672
|
-
|
|
696
|
+
try {
|
|
697
|
+
// Current Bun finalizes all statements and releases Windows handles now.
|
|
698
|
+
this.db.close(true);
|
|
699
|
+
} catch (cause) {
|
|
700
|
+
// Older Bun only finalizes cached statements. Its native SQLITE_BUSY
|
|
701
|
+
// error has no code field; retain deferred close for that exact case.
|
|
702
|
+
if (
|
|
703
|
+
!(cause instanceof Error) ||
|
|
704
|
+
cause.message !== "database is locked"
|
|
705
|
+
) {
|
|
706
|
+
throw cause;
|
|
707
|
+
}
|
|
708
|
+
this.db.close(false);
|
|
709
|
+
}
|
|
673
710
|
this.db = null;
|
|
674
711
|
}
|
|
675
712
|
}
|
|
@@ -798,6 +835,11 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
798
835
|
this.configPath = configPath;
|
|
799
836
|
}
|
|
800
837
|
|
|
838
|
+
/** Path this adapter opened; names the index's request ledger. */
|
|
839
|
+
getDbPath(): string {
|
|
840
|
+
return this.dbPath;
|
|
841
|
+
}
|
|
842
|
+
|
|
801
843
|
/**
|
|
802
844
|
* Get raw SQLite database handle for vector operations.
|
|
803
845
|
* Part of SqliteDbProvider interface - use with isSqliteDbProvider() type guard.
|
|
@@ -1189,9 +1231,12 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1189
1231
|
return ok(null);
|
|
1190
1232
|
}
|
|
1191
1233
|
if (row.fingerprint !== expectedFingerprint) {
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
+
)
|
|
1195
1240
|
);
|
|
1196
1241
|
return ok(null);
|
|
1197
1242
|
}
|
|
@@ -1203,9 +1248,12 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1203
1248
|
receipt.collection !== row.collection ||
|
|
1204
1249
|
(receipt.evidence.connectorTarget ?? "") !== row.connector_target
|
|
1205
1250
|
) {
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
+
)
|
|
1209
1257
|
);
|
|
1210
1258
|
return ok(null);
|
|
1211
1259
|
}
|
|
@@ -1230,8 +1278,11 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1230
1278
|
if (!serialized.ok) {
|
|
1231
1279
|
return err("INVALID_INPUT", serialized.error);
|
|
1232
1280
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
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 (
|
|
1235
1286
|
collection, connector_target, schema_version, fingerprint,
|
|
1236
1287
|
receipt_json, updated_at
|
|
1237
1288
|
) VALUES (?, ?, ?, ?, ?, datetime('now'))
|
|
@@ -1240,13 +1291,14 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1240
1291
|
fingerprint = excluded.fingerprint,
|
|
1241
1292
|
receipt_json = excluded.receipt_json,
|
|
1242
1293
|
updated_at = datetime('now')`,
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1294
|
+
[
|
|
1295
|
+
serialized.projected.collection,
|
|
1296
|
+
serialized.connectorTarget,
|
|
1297
|
+
serialized.projected.schemaVersion,
|
|
1298
|
+
serialized.projected.fingerprint,
|
|
1299
|
+
serialized.json,
|
|
1300
|
+
]
|
|
1301
|
+
)
|
|
1250
1302
|
);
|
|
1251
1303
|
return ok(undefined);
|
|
1252
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
|
-
ae739d8da342a7f932f8097e1d4d852ff7c46b722b8e979303b1213eaf2d76a5 gno-browser-clipper-v2.4.0.zip
|