@aiwg/cli 2026.7.20 → 2026.7.23

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 (62) hide show
  1. package/README.md +18 -7
  2. package/dist/src/api/index.d.ts +2 -0
  3. package/dist/src/api/index.js +2 -0
  4. package/dist/src/artifacts/browser-export.js +7 -0
  5. package/dist/src/artifacts/citation-parser.js +96 -35
  6. package/dist/src/artifacts/index-builder.js +54 -17
  7. package/dist/src/artifacts/state-transfer.js +27 -0
  8. package/dist/src/artifacts/stats.js +8 -0
  9. package/dist/src/cli/cli-extension-loader.js +73 -0
  10. package/dist/src/cli/handlers/index.js +3 -1
  11. package/dist/src/cli/handlers/sessions.js +1265 -0
  12. package/dist/src/cli/handlers/skill-lint.js +49 -45
  13. package/dist/src/cli/handlers/use.js +143 -60
  14. package/dist/src/cli/handlers/utilities.js +22 -8
  15. package/dist/src/cli/skill-usage.js +146 -24
  16. package/dist/src/config/aiwg-config.js +12 -0
  17. package/dist/src/config/cli.js +16 -3
  18. package/dist/src/extensions/commands/definitions.js +29 -0
  19. package/dist/src/extensions/manifest.js +29 -0
  20. package/dist/src/security/threat-assessment-config.js +296 -0
  21. package/dist/src/sessions/adapters/claude.js +385 -0
  22. package/dist/src/sessions/adapters/codex.js +548 -0
  23. package/dist/src/sessions/adapters/copilot.js +226 -0
  24. package/dist/src/sessions/adapters/cursor.js +528 -0
  25. package/dist/src/sessions/adapters/factory.js +386 -0
  26. package/dist/src/sessions/adapters/generic.js +225 -0
  27. package/dist/src/sessions/adapters/hermes.js +341 -0
  28. package/dist/src/sessions/adapters/openclaw.js +381 -0
  29. package/dist/src/sessions/adapters/opencode.js +454 -0
  30. package/dist/src/sessions/adapters/openhuman.js +315 -0
  31. package/dist/src/sessions/adapters/warp.js +160 -0
  32. package/dist/src/sessions/adapters/windsurf.js +212 -0
  33. package/dist/src/sessions/batch-contracts.js +121 -0
  34. package/dist/src/sessions/batch-import.js +265 -0
  35. package/dist/src/sessions/candidates.js +210 -0
  36. package/dist/src/sessions/contracts.js +337 -0
  37. package/dist/src/sessions/discovery.js +51 -0
  38. package/dist/src/sessions/fixtures.js +12 -0
  39. package/dist/src/sessions/import-lease.js +152 -0
  40. package/dist/src/sessions/importer.js +464 -0
  41. package/dist/src/sessions/index.js +31 -0
  42. package/dist/src/sessions/knowledge-shard.js +61 -0
  43. package/dist/src/sessions/optional-backends.js +238 -0
  44. package/dist/src/sessions/origin.js +117 -0
  45. package/dist/src/sessions/policy.js +192 -0
  46. package/dist/src/sessions/ports.js +2 -0
  47. package/dist/src/sessions/promotion.js +367 -0
  48. package/dist/src/sessions/readers.js +176 -0
  49. package/dist/src/sessions/repository.js +1892 -0
  50. package/dist/src/sessions/timeline.js +148 -0
  51. package/dist/src/sessions/workspace-discovery.js +319 -0
  52. package/dist/src/skills/adapters/agent-skills.js +59 -0
  53. package/dist/src/skills/adapters/local.js +19 -1
  54. package/dist/src/skills/agent-skills.js +249 -0
  55. package/dist/src/skills/cli.js +463 -7
  56. package/dist/src/skills/deployer.js +554 -0
  57. package/dist/src/skills/doctor.js +105 -0
  58. package/dist/src/skills/exporter.js +382 -0
  59. package/dist/src/skills/importer.js +921 -0
  60. package/dist/src/skills/registry.js +19 -0
  61. package/dist/src/skills/validator.js +323 -0
  62. package/package.json +2 -2
@@ -0,0 +1,1892 @@
1
+ import { createRequire } from 'node:module';
2
+ import { CandidateReviewReceiptSchema, DeletionReceiptSchema, IntelligenceCandidateSchema, PromotionDependencyDecisionSchema, PromotionReceiptSchema, SessionContractError, sha256, } from './contracts.js';
3
+ import { coverageFromBatchRun, } from './batch-contracts.js';
4
+ import { redactSessionText, sanitizeNativeExtensions } from './policy.js';
5
+ const require = createRequire(import.meta.url);
6
+ const POLICY_PROVIDER_MIGRATION = 'policy-provider-identity:v2';
7
+ const EVENT_ORIGIN_INTENT_MIGRATION = 'event-origin-intent:v1';
8
+ export class SessionRepository {
9
+ db;
10
+ constructor(path = ':memory:') {
11
+ try {
12
+ const Database = require('better-sqlite3');
13
+ this.db = new Database(path);
14
+ }
15
+ catch {
16
+ throw new Error('session SQLite repository requires optional peer dependency better-sqlite3');
17
+ }
18
+ this.db.pragma('foreign_keys = ON');
19
+ this.db.pragma('journal_mode = WAL');
20
+ this.db.pragma('busy_timeout = 5000');
21
+ this.initialize();
22
+ }
23
+ initialize() {
24
+ this.db.exec(`
25
+ CREATE TABLE IF NOT EXISTS session_sources (
26
+ source_id TEXT PRIMARY KEY, provider TEXT NOT NULL, data TEXT NOT NULL
27
+ );
28
+ CREATE TABLE IF NOT EXISTS import_runs (
29
+ import_run_id TEXT PRIMARY KEY, source_id TEXT NOT NULL, parser_version TEXT NOT NULL,
30
+ status TEXT NOT NULL, checkpoint TEXT NOT NULL, data TEXT NOT NULL,
31
+ batch_run_id TEXT,
32
+ FOREIGN KEY(source_id) REFERENCES session_sources(source_id)
33
+ );
34
+ CREATE TABLE IF NOT EXISTS sessions (
35
+ session_id TEXT PRIMARY KEY, source_id TEXT NOT NULL, native_session_id TEXT NOT NULL,
36
+ workspace_id TEXT NOT NULL, source_digest TEXT NOT NULL, lifecycle TEXT NOT NULL, data TEXT NOT NULL,
37
+ UNIQUE(source_id, native_session_id, source_digest),
38
+ FOREIGN KEY(source_id) REFERENCES session_sources(source_id)
39
+ );
40
+ CREATE TABLE IF NOT EXISTS session_events (
41
+ event_id TEXT PRIMARY KEY, session_id TEXT NOT NULL, source_id TEXT NOT NULL,
42
+ import_run_id TEXT NOT NULL, sequence_no INTEGER NOT NULL, digest TEXT NOT NULL, data TEXT NOT NULL,
43
+ UNIQUE(session_id, event_id, digest),
44
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id),
45
+ FOREIGN KEY(source_id) REFERENCES session_sources(source_id),
46
+ FOREIGN KEY(import_run_id) REFERENCES import_runs(import_run_id)
47
+ );
48
+ CREATE TABLE IF NOT EXISTS import_receipts (
49
+ operation_id TEXT PRIMARY KEY, import_run_id TEXT NOT NULL UNIQUE,
50
+ outcome TEXT NOT NULL, counts TEXT NOT NULL, checkpoint TEXT NOT NULL,
51
+ FOREIGN KEY(import_run_id) REFERENCES import_runs(import_run_id)
52
+ );
53
+ CREATE TABLE IF NOT EXISTS batch_import_runs (
54
+ batch_run_id TEXT PRIMARY KEY, manifest_id TEXT NOT NULL,
55
+ workspace_id TEXT NOT NULL, status TEXT NOT NULL,
56
+ started_at TEXT NOT NULL, updated_at TEXT NOT NULL, data TEXT NOT NULL,
57
+ UNIQUE(manifest_id, workspace_id)
58
+ );
59
+ CREATE TABLE IF NOT EXISTS mutation_audit (
60
+ operation_id TEXT PRIMARY KEY, operation TEXT NOT NULL, actor TEXT NOT NULL,
61
+ counts TEXT NOT NULL, adapter_version TEXT NOT NULL, policy_version TEXT NOT NULL,
62
+ outcome TEXT NOT NULL, occurred_at TEXT NOT NULL,
63
+ workspace_id TEXT, target_class TEXT, data TEXT
64
+ );
65
+ CREATE TABLE IF NOT EXISTS session_tags (
66
+ session_id TEXT NOT NULL, tag TEXT NOT NULL,
67
+ PRIMARY KEY(session_id, tag),
68
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
69
+ );
70
+ CREATE TABLE IF NOT EXISTS intelligence_candidates (
71
+ candidate_id TEXT NOT NULL, version INTEGER NOT NULL,
72
+ review_state TEXT NOT NULL, data TEXT NOT NULL,
73
+ PRIMARY KEY(candidate_id, version)
74
+ );
75
+ CREATE TABLE IF NOT EXISTS candidate_review_receipts (
76
+ receipt_id TEXT PRIMARY KEY, candidate_id TEXT NOT NULL,
77
+ candidate_version INTEGER NOT NULL, data TEXT NOT NULL,
78
+ FOREIGN KEY(candidate_id, candidate_version)
79
+ REFERENCES intelligence_candidates(candidate_id, version)
80
+ );
81
+ CREATE TABLE IF NOT EXISTS promotion_receipts (
82
+ receipt_id TEXT PRIMARY KEY, candidate_id TEXT NOT NULL,
83
+ candidate_version INTEGER NOT NULL, consumer TEXT NOT NULL,
84
+ data TEXT NOT NULL,
85
+ UNIQUE(candidate_id, candidate_version, consumer),
86
+ FOREIGN KEY(candidate_id, candidate_version)
87
+ REFERENCES intelligence_candidates(candidate_id, version)
88
+ );
89
+ CREATE TABLE IF NOT EXISTS deletion_receipts (
90
+ operation_id TEXT PRIMARY KEY, scope_id TEXT NOT NULL,
91
+ workspace_id TEXT, data TEXT NOT NULL
92
+ );
93
+ CREATE TABLE IF NOT EXISTS promotion_dependency_decisions (
94
+ operation_id TEXT NOT NULL, dependent_id TEXT NOT NULL,
95
+ action TEXT NOT NULL, basis TEXT NOT NULL,
96
+ PRIMARY KEY(operation_id, dependent_id)
97
+ );
98
+ CREATE TABLE IF NOT EXISTS promotion_provenance_receipts (
99
+ receipt_id TEXT PRIMARY KEY, operation_id TEXT NOT NULL,
100
+ dependent_id TEXT NOT NULL, action TEXT NOT NULL, data TEXT NOT NULL,
101
+ UNIQUE(operation_id, dependent_id)
102
+ );
103
+ CREATE TABLE IF NOT EXISTS session_catalog_meta (
104
+ key TEXT PRIMARY KEY, value TEXT NOT NULL
105
+ );
106
+ CREATE INDEX IF NOT EXISTS idx_session_workspace_provider
107
+ ON sessions(workspace_id, source_id, lifecycle);
108
+ CREATE INDEX IF NOT EXISTS idx_event_session_sequence
109
+ ON session_events(session_id, sequence_no);
110
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_event_fts USING fts5(
111
+ event_id UNINDEXED, searchable_text, tokenize='unicode61'
112
+ );
113
+ `);
114
+ const deletionReceiptColumns = this.db.prepare('PRAGMA table_info(deletion_receipts)').all().map((row) => String(row.name));
115
+ if (!deletionReceiptColumns.includes('workspace_id')) {
116
+ this.db.exec('ALTER TABLE deletion_receipts ADD COLUMN workspace_id TEXT');
117
+ }
118
+ const mutationColumns = this.db.prepare('PRAGMA table_info(mutation_audit)').all().map((row) => String(row.name));
119
+ if (!mutationColumns.includes('workspace_id')) {
120
+ this.db.exec('ALTER TABLE mutation_audit ADD COLUMN workspace_id TEXT');
121
+ }
122
+ if (!mutationColumns.includes('target_class')) {
123
+ this.db.exec('ALTER TABLE mutation_audit ADD COLUMN target_class TEXT');
124
+ }
125
+ if (!mutationColumns.includes('data')) {
126
+ this.db.exec('ALTER TABLE mutation_audit ADD COLUMN data TEXT');
127
+ }
128
+ const importRunColumns = this.db.prepare('PRAGMA table_info(import_runs)').all().map((row) => String(row.name));
129
+ if (!importRunColumns.includes('batch_run_id')) {
130
+ this.db.exec('ALTER TABLE import_runs ADD COLUMN batch_run_id TEXT');
131
+ }
132
+ this.db.exec(`
133
+ CREATE INDEX IF NOT EXISTS idx_mutation_audit_workspace
134
+ ON mutation_audit(workspace_id, occurred_at, operation_id)
135
+ ;
136
+ CREATE INDEX IF NOT EXISTS idx_batch_import_workspace
137
+ ON batch_import_runs(workspace_id, updated_at, batch_run_id)
138
+ `);
139
+ this.alignSessionFtsRowids();
140
+ this.migrateSessionPolicyAndProviderIdentity();
141
+ this.migrateEventOriginAndIntent();
142
+ }
143
+ alignSessionFtsRowids() {
144
+ const eventCount = Number(this.db.prepare('SELECT COUNT(*) AS count FROM session_events').get()?.count ?? 0);
145
+ const ftsCount = Number(this.db.prepare('SELECT COUNT(*) AS count FROM session_event_fts').get()?.count ?? 0);
146
+ const mismatches = Number(this.db.prepare(`SELECT COUNT(*) AS count
147
+ FROM session_event_fts f
148
+ LEFT JOIN session_events e ON e.rowid=f.rowid
149
+ WHERE e.rowid IS NULL OR e.event_id!=f.event_id`).get()?.count ?? 0);
150
+ if (eventCount === ftsCount && mismatches === 0)
151
+ return;
152
+ const rebuild = this.db.transaction(() => {
153
+ this.db.exec('DELETE FROM session_event_fts');
154
+ this.db.exec(`
155
+ INSERT INTO session_event_fts(rowid, event_id, searchable_text)
156
+ SELECT rowid, event_id, json_extract(data, '$.searchableText')
157
+ FROM session_events
158
+ `);
159
+ });
160
+ rebuild();
161
+ }
162
+ migrateSessionPolicyAndProviderIdentity() {
163
+ this.runCatalogMigration(POLICY_PROVIDER_MIGRATION, () => {
164
+ for (const row of this.db.prepare('SELECT source_id, provider, data FROM session_sources').all()) {
165
+ const source = JSON.parse(String(row.data));
166
+ const canonicalProvider = source.provider === 'windsurf'
167
+ ? 'devin-desktop' : source.provider;
168
+ const extensions = renameNativeWindsurfEnvelope(source.extensions);
169
+ source.provider = canonicalProvider;
170
+ source.locatorClass = canonicalDevinLocator(source.locatorClass);
171
+ source.extensions = sanitizeNativeExtensions(extensions).value;
172
+ this.db.prepare('UPDATE session_sources SET provider=?, data=? WHERE source_id=?').run(canonicalProvider, JSON.stringify(source), String(row.source_id));
173
+ }
174
+ for (const row of this.db.prepare('SELECT session_id, data FROM sessions').all()) {
175
+ const session = JSON.parse(String(row.data));
176
+ if (session.provider === 'windsurf')
177
+ session.provider = 'devin-desktop';
178
+ session.extensions = sanitizeSessionExtensions(renameNativeWindsurfEnvelope(session.extensions), session.provider);
179
+ this.db.prepare('UPDATE sessions SET data=? WHERE session_id=?')
180
+ .run(JSON.stringify(session), String(row.session_id));
181
+ }
182
+ for (const row of this.db.prepare('SELECT rowid AS event_rowid, event_id, data FROM session_events').all()) {
183
+ const event = JSON.parse(String(row.data));
184
+ const text = redactSessionText(event.searchableText);
185
+ const native = sanitizeNativeExtensions(renameNativeWindsurfEnvelope(event.extensions));
186
+ event.searchableText = text.text;
187
+ event.kind = event.kind.startsWith('windsurf.')
188
+ ? `devin-desktop.${event.kind.slice('windsurf.'.length)}` : event.kind;
189
+ event.rawReference.locatorClass = canonicalDevinLocator(event.rawReference.locatorClass);
190
+ event.extensions = native.value;
191
+ event.sensitivity = {
192
+ classification: text.sensitivity === 'sensitive' || native.sensitivity === 'sensitive'
193
+ ? 'sensitive' : 'none',
194
+ classes: [...new Set([
195
+ ...event.sensitivity.classes,
196
+ ...text.classes,
197
+ ...native.classes,
198
+ ])].sort(),
199
+ };
200
+ this.db.prepare('UPDATE session_events SET data=? WHERE event_id=?')
201
+ .run(JSON.stringify(event), String(row.event_id));
202
+ this.db.prepare('UPDATE session_event_fts SET searchable_text=? WHERE rowid=?').run(event.searchableText, Number(row.event_rowid));
203
+ }
204
+ for (const row of this.db.prepare(`SELECT m.operation_id, m.operation, m.actor, m.counts,
205
+ m.adapter_version, m.policy_version, m.outcome, m.occurred_at,
206
+ COALESCE(
207
+ (SELECT s.workspace_id
208
+ FROM import_runs r
209
+ JOIN sessions s ON s.source_id=r.source_id
210
+ WHERE r.import_run_id=m.operation_id
211
+ LIMIT 1),
212
+ 'legacy'
213
+ ) AS inferred_workspace
214
+ FROM mutation_audit m
215
+ WHERE m.data IS NULL`).all()) {
216
+ const workspaceId = String(row.inferred_workspace);
217
+ const unsigned = {
218
+ contractVersion: '1.0.0',
219
+ operationId: String(row.operation_id),
220
+ correlationId: String(row.operation_id),
221
+ eventName: String(row.operation),
222
+ eventTime: String(row.occurred_at),
223
+ observedAt: String(row.occurred_at),
224
+ actorClass: String(row.actor),
225
+ authorizationClass: 'legacy-local-operator',
226
+ workspaceId,
227
+ targetClass: 'session-catalog',
228
+ outcome: String(row.outcome),
229
+ counts: JSON.parse(String(row.counts)),
230
+ adapterVersion: String(row.adapter_version),
231
+ policyVersion: String(row.policy_version),
232
+ schemaVersion: 'legacy-1.0.0',
233
+ resource: { service: 'aiwg.sessions', workspaceId },
234
+ instrumentationScope: {
235
+ name: 'aiwg.sessions.repository',
236
+ version: '1.0.0',
237
+ },
238
+ };
239
+ const event = {
240
+ ...unsigned,
241
+ integrityDigest: sha256(JSON.stringify(unsigned)),
242
+ };
243
+ this.db.prepare(`UPDATE mutation_audit
244
+ SET workspace_id=?, target_class=?, data=?
245
+ WHERE operation_id=?`).run(workspaceId, event.targetClass, JSON.stringify(event), event.operationId);
246
+ }
247
+ });
248
+ }
249
+ migrateEventOriginAndIntent() {
250
+ this.runCatalogMigration(EVENT_ORIGIN_INTENT_MIGRATION, () => {
251
+ for (const row of this.db.prepare('SELECT event_id, data FROM session_events').all()) {
252
+ const event = JSON.parse(String(row.data));
253
+ let changed = false;
254
+ if (typeof event.origin !== 'string') {
255
+ event.origin = 'unknown';
256
+ event.originRule = 'legacy:unknown';
257
+ event.originClassifierVersion = '1.0.0';
258
+ changed = true;
259
+ }
260
+ if (!Object.hasOwn(event, 'activityBoundary')) {
261
+ event.activityBoundary = null;
262
+ event.activityBoundaryBasis = null;
263
+ event.activityBoundaryConfidence = null;
264
+ changed = true;
265
+ }
266
+ if (changed) {
267
+ this.db.prepare('UPDATE session_events SET data=? WHERE event_id=?').run(JSON.stringify(event), String(row.event_id));
268
+ }
269
+ }
270
+ for (const row of this.db.prepare('SELECT session_id, data FROM sessions').all()) {
271
+ const session = JSON.parse(String(row.data));
272
+ if (session.intent !== undefined)
273
+ continue;
274
+ session.intent = {
275
+ status: 'unknown',
276
+ eventId: null,
277
+ sequence: null,
278
+ title: null,
279
+ summary: null,
280
+ };
281
+ this.db.prepare('UPDATE sessions SET data=? WHERE session_id=?').run(JSON.stringify(session), String(row.session_id));
282
+ }
283
+ });
284
+ }
285
+ runCatalogMigration(migration, migrate) {
286
+ const applied = this.db.prepare('SELECT 1 AS applied FROM session_catalog_meta WHERE key=?').get(migration);
287
+ if (applied)
288
+ return;
289
+ const transaction = this.db.transaction(() => {
290
+ migrate();
291
+ this.db.prepare('INSERT INTO session_catalog_meta (key, value) VALUES (?, ?)').run(migration, 'applied');
292
+ });
293
+ transaction();
294
+ }
295
+ emitMutation(input) {
296
+ const unsigned = {
297
+ contractVersion: '1.0.0',
298
+ ...input,
299
+ counts: Object.fromEntries(Object.entries(input.counts)
300
+ .slice(0, 32)
301
+ .map(([key, value]) => [key, Math.max(0, Math.min(Number.MAX_SAFE_INTEGER, Number.isFinite(value) ? Math.trunc(value) : 0))])),
302
+ resource: { service: 'aiwg.sessions', workspaceId: input.workspaceId },
303
+ instrumentationScope: {
304
+ name: 'aiwg.sessions.repository',
305
+ version: '1.0.0',
306
+ },
307
+ };
308
+ const event = {
309
+ ...unsigned,
310
+ integrityDigest: sha256(JSON.stringify(unsigned)),
311
+ };
312
+ this.db.prepare(`INSERT OR IGNORE INTO mutation_audit
313
+ (operation_id, operation, actor, counts, adapter_version, policy_version,
314
+ outcome, occurred_at, workspace_id, target_class, data)
315
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(event.operationId, event.eventName, event.actorClass, JSON.stringify(event.counts), event.adapterVersion, event.policyVersion, event.outcome, event.observedAt, event.workspaceId, event.targetClass, JSON.stringify(event));
316
+ return event;
317
+ }
318
+ emitRepositoryMutation(input) {
319
+ const observedAt = new Date().toISOString();
320
+ return this.emitMutation({
321
+ operationId: input.operationId,
322
+ correlationId: input.operationId,
323
+ eventName: input.eventName,
324
+ eventTime: input.eventTime ?? observedAt,
325
+ observedAt,
326
+ actorClass: input.actorClass ?? 'local-operator',
327
+ authorizationClass: 'workspace-owner',
328
+ workspaceId: input.workspaceId,
329
+ targetClass: input.targetClass,
330
+ outcome: input.outcome ?? 'committed',
331
+ counts: input.counts ?? { affected: 1 },
332
+ adapterVersion: 'repository-1.0.0',
333
+ policyVersion: '1.0.0',
334
+ schemaVersion: '1.0.0',
335
+ });
336
+ }
337
+ workspaceForSession(sessionId) {
338
+ const row = this.db.prepare('SELECT workspace_id FROM sessions WHERE session_id=?').get(sessionId);
339
+ return row ? String(row.workspace_id) : null;
340
+ }
341
+ workspaceForCandidate(candidate) {
342
+ const eventId = candidate.evidence[0]?.eventId;
343
+ if (!eventId)
344
+ return null;
345
+ const row = this.db.prepare(`SELECT s.workspace_id FROM session_events e
346
+ JOIN sessions s ON s.session_id=e.session_id WHERE e.event_id=?`).get(eventId);
347
+ return row ? String(row.workspace_id) : null;
348
+ }
349
+ listMutationEvents(input) {
350
+ if (!input.workspaceId || !Number.isInteger(input.limit) || input.limit < 1 || input.limit > 500) {
351
+ throw new SessionContractError('OPERATION_NOT_AUTHORIZED', 'audit reads require a workspace and a limit from 1 to 500');
352
+ }
353
+ const after = decodeMutationCursor(input.cursor, input.workspaceId);
354
+ const rows = this.db.prepare(`SELECT rowid, data FROM mutation_audit
355
+ WHERE workspace_id=? AND rowid>?
356
+ ORDER BY rowid LIMIT ?`).all(input.workspaceId, after, input.limit + 1);
357
+ const page = rows.slice(0, input.limit);
358
+ const items = page.map((row) => {
359
+ const event = JSON.parse(String(row.data));
360
+ const { integrityDigest, ...unsigned } = event;
361
+ if (integrityDigest !== sha256(JSON.stringify(unsigned))) {
362
+ throw new SessionContractError('IMPORT_CONFLICT', 'mutation audit integrity check failed');
363
+ }
364
+ return event;
365
+ });
366
+ const last = page.at(-1);
367
+ return {
368
+ items,
369
+ nextCursor: rows.length > input.limit && last
370
+ ? encodeMutationCursor(Number(last.rowid), input.workspaceId) : null,
371
+ };
372
+ }
373
+ exportMutationEventsOtel(input) {
374
+ const page = this.listMutationEvents(input);
375
+ return {
376
+ records: page.items.map((event) => ({
377
+ Timestamp: event.eventTime,
378
+ ObservedTimestamp: event.observedAt,
379
+ EventName: event.eventName,
380
+ Body: {},
381
+ Resource: event.resource,
382
+ InstrumentationScope: event.instrumentationScope,
383
+ Attributes: {
384
+ operationId: event.operationId,
385
+ correlationId: event.correlationId,
386
+ actorClass: event.actorClass,
387
+ authorizationClass: event.authorizationClass,
388
+ targetClass: event.targetClass,
389
+ outcome: event.outcome,
390
+ counts: event.counts,
391
+ adapterVersion: event.adapterVersion,
392
+ policyVersion: event.policyVersion,
393
+ schemaVersion: event.schemaVersion,
394
+ },
395
+ })),
396
+ nextCursor: page.nextCursor,
397
+ };
398
+ }
399
+ applyImport(batch, checkpoint, publish = true) {
400
+ const apply = this.db.transaction(() => {
401
+ const existing = this.db.prepare('SELECT outcome, counts, checkpoint FROM import_receipts WHERE import_run_id = ?').get(batch.run.importRunId);
402
+ if (existing) {
403
+ const counts = JSON.parse(String(existing.counts));
404
+ return {
405
+ operationId: batch.run.importRunId,
406
+ outcome: 'duplicate',
407
+ sessionsInserted: counts.sessions,
408
+ eventsInserted: counts.events,
409
+ checkpoint: JSON.parse(String(existing.checkpoint)),
410
+ };
411
+ }
412
+ this.db.prepare(`INSERT INTO session_sources(source_id, provider, data) VALUES (?, ?, ?)
413
+ ON CONFLICT(source_id) DO UPDATE SET data=excluded.data`).run(batch.source.sourceId, batch.source.provider, JSON.stringify(batch.source));
414
+ this.db.prepare(`INSERT INTO import_runs(
415
+ import_run_id, source_id, parser_version, status, checkpoint, data, batch_run_id
416
+ ) VALUES (?, ?, ?, 'running', ?, ?, ?)`).run(batch.run.importRunId, batch.source.sourceId, batch.run.parserVersion, JSON.stringify(checkpoint), JSON.stringify(batch.run), batch.batchRunId ?? null);
417
+ let sessionsInserted = 0;
418
+ let eventsInserted = 0;
419
+ for (const session of batch.sessions) {
420
+ const priorRow = this.db.prepare('SELECT data FROM sessions WHERE session_id=?').get(session.sessionId);
421
+ if (!priorRow) {
422
+ sessionsInserted += this.db.prepare(`INSERT INTO sessions
423
+ (session_id, source_id, native_session_id, workspace_id, source_digest, lifecycle, data)
424
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(session.sessionId, session.sourceId, session.nativeSessionId, session.workspaceId, session.sourceDigest, session.lifecycle, JSON.stringify(session)).changes;
425
+ continue;
426
+ }
427
+ const prior = JSON.parse(String(priorRow.data));
428
+ if (prior.workspaceId !== session.workspaceId
429
+ || prior.sourceId !== session.sourceId
430
+ || prior.nativeSessionId !== session.nativeSessionId) {
431
+ throw new SessionContractError('IMPORT_CONFLICT', 'stable session identity changed authorization or source scope');
432
+ }
433
+ const merged = mergeSessionAggregate(prior, session);
434
+ this.db.prepare(`UPDATE sessions SET source_digest=?, lifecycle=?, data=? WHERE session_id=?`).run(merged.sourceDigest, merged.lifecycle, JSON.stringify(merged), merged.sessionId);
435
+ }
436
+ const insertEvent = this.db.prepare(`INSERT OR IGNORE INTO session_events
437
+ (event_id, session_id, source_id, import_run_id, sequence_no, digest, data)
438
+ VALUES (?, ?, ?, ?, ?, ?, ?)`);
439
+ for (const event of batch.events) {
440
+ const prior = this.db.prepare('SELECT digest FROM session_events WHERE event_id=?')
441
+ .get(event.eventId);
442
+ if (prior && String(prior.digest) !== event.digest) {
443
+ const priorEvent = this.db.prepare(`SELECT e.data AS event_data, src.provider AS provider, s.native_session_id AS native_session_id
444
+ FROM session_events e
445
+ JOIN sessions s ON s.session_id=e.session_id
446
+ JOIN session_sources src ON src.source_id=e.source_id
447
+ WHERE e.event_id=?`).get(event.eventId);
448
+ const priorCoordinates = priorEvent
449
+ ? safeEventCoordinates(String(priorEvent.event_data), {
450
+ provider: String(priorEvent.provider),
451
+ nativeSessionId: String(priorEvent.native_session_id),
452
+ })
453
+ : { provider: '<unknown>', sourceId: '<unknown>', nativeSessionId: '<unknown>', nativeEventId: event.eventId };
454
+ const incomingCoordinates = {
455
+ provider: batch.source.provider,
456
+ sourceId: event.sourceId,
457
+ nativeSessionId: batch.sessions.find((session) => session.sessionId === event.sessionId)
458
+ ?.nativeSessionId ?? event.sessionId,
459
+ nativeEventId: event.nativeId ?? `sequence:${event.sequence}`,
460
+ };
461
+ throw new SessionContractError('IMPORT_CONFLICT', `stable event identity changed content: ${event.eventId}; prior=${formatEventCoordinates(priorCoordinates)} incoming=${formatEventCoordinates(incomingCoordinates)}`);
462
+ }
463
+ const inserted = insertEvent.run(event.eventId, event.sessionId, event.sourceId, event.importRunId, event.sequence, event.digest, JSON.stringify(event)).changes;
464
+ eventsInserted += inserted;
465
+ if (inserted > 0) {
466
+ const eventRowid = Number(this.db.prepare('SELECT rowid AS value FROM session_events WHERE event_id=?').get(event.eventId)?.value);
467
+ this.db.prepare(`INSERT INTO session_event_fts(rowid, event_id, searchable_text)
468
+ VALUES (?, ?, ?)`).run(eventRowid, event.eventId, event.searchableText);
469
+ }
470
+ }
471
+ const outcome = publish ? 'committed' : 'staged';
472
+ this.db.prepare(`UPDATE import_runs SET status=?, checkpoint=? WHERE import_run_id=?`).run(outcome, JSON.stringify(checkpoint), batch.run.importRunId);
473
+ const counts = { sessions: sessionsInserted, events: eventsInserted };
474
+ this.db.prepare(`INSERT INTO import_receipts(operation_id, import_run_id, outcome, counts, checkpoint)
475
+ VALUES (?, ?, ?, ?, ?)`).run(batch.run.importRunId, batch.run.importRunId, outcome, JSON.stringify(counts), JSON.stringify(checkpoint));
476
+ const observedAt = new Date().toISOString();
477
+ this.emitMutation({
478
+ operationId: batch.run.importRunId,
479
+ correlationId: batch.run.importRunId,
480
+ eventName: 'session.import',
481
+ eventTime: batch.run.startedAt,
482
+ observedAt,
483
+ actorClass: 'local-operator',
484
+ authorizationClass: 'workspace-owner',
485
+ workspaceId: batch.sessions[0]?.workspaceId ?? 'unscoped',
486
+ targetClass: 'session-source',
487
+ outcome,
488
+ counts,
489
+ adapterVersion: batch.source.adapterVersion,
490
+ policyVersion: batch.run.policyVersion,
491
+ schemaVersion: batch.source.sourceSchemaVersion,
492
+ });
493
+ return {
494
+ operationId: batch.run.importRunId, outcome,
495
+ sessionsInserted, eventsInserted, checkpoint,
496
+ };
497
+ });
498
+ return apply();
499
+ }
500
+ commitStagedImports(sourceId, parserVersion) {
501
+ const commit = this.db.transaction(() => {
502
+ const staged = this.db.prepare(`SELECT import_run_id FROM import_runs
503
+ WHERE source_id=? AND parser_version=? AND status='staged'`).all(sourceId, parserVersion).map((row) => String(row.import_run_id));
504
+ const result = this.db.prepare(`UPDATE import_runs SET status='committed'
505
+ WHERE source_id=? AND parser_version=? AND status='staged'`).run(sourceId, parserVersion);
506
+ this.db.prepare(`UPDATE import_receipts SET outcome='committed'
507
+ WHERE import_run_id IN (
508
+ SELECT import_run_id FROM import_runs
509
+ WHERE source_id=? AND parser_version=? AND status='committed'
510
+ ) AND outcome='staged'`).run(sourceId, parserVersion);
511
+ for (const operationId of staged) {
512
+ const row = this.db.prepare('SELECT data FROM mutation_audit WHERE operation_id=?').get(operationId);
513
+ if (!row?.data)
514
+ continue;
515
+ const prior = JSON.parse(String(row.data));
516
+ const { integrityDigest: _integrityDigest, ...unsigned } = {
517
+ ...prior,
518
+ outcome: 'committed',
519
+ observedAt: new Date().toISOString(),
520
+ };
521
+ const updated = {
522
+ ...unsigned,
523
+ integrityDigest: sha256(JSON.stringify(unsigned)),
524
+ };
525
+ this.db.prepare(`UPDATE mutation_audit SET outcome='committed', occurred_at=?, data=?
526
+ WHERE operation_id=?`).run(updated.observedAt, JSON.stringify(updated), operationId);
527
+ }
528
+ return result.changes;
529
+ });
530
+ return commit();
531
+ }
532
+ commitStagedBatch(batchRunId, sourceIds) {
533
+ if (sourceIds.length === 0)
534
+ return 0;
535
+ const placeholders = sourceIds.map(() => '?').join(', ');
536
+ const commit = this.db.transaction(() => {
537
+ const params = [batchRunId, ...sourceIds];
538
+ const staged = this.db.prepare(`SELECT import_run_id FROM import_runs
539
+ WHERE batch_run_id=? AND source_id IN (${placeholders}) AND status='staged'`).all(...params).map((row) => String(row.import_run_id));
540
+ const result = this.db.prepare(`UPDATE import_runs SET status='committed'
541
+ WHERE batch_run_id=? AND source_id IN (${placeholders}) AND status='staged'`).run(...params);
542
+ if (staged.length > 0) {
543
+ const runPlaceholders = staged.map(() => '?').join(', ');
544
+ this.db.prepare(`UPDATE import_receipts SET outcome='committed'
545
+ WHERE import_run_id IN (${runPlaceholders}) AND outcome='staged'`).run(...staged);
546
+ }
547
+ for (const operationId of staged) {
548
+ const row = this.db.prepare('SELECT data FROM mutation_audit WHERE operation_id=?').get(operationId);
549
+ if (!row?.data)
550
+ continue;
551
+ const prior = JSON.parse(String(row.data));
552
+ const { integrityDigest: _integrityDigest, ...unsigned } = {
553
+ ...prior,
554
+ outcome: 'committed',
555
+ observedAt: new Date().toISOString(),
556
+ };
557
+ const updated = {
558
+ ...unsigned,
559
+ integrityDigest: sha256(JSON.stringify(unsigned)),
560
+ };
561
+ this.db.prepare(`UPDATE mutation_audit SET outcome='committed', occurred_at=?, data=?
562
+ WHERE operation_id=?`).run(updated.observedAt, JSON.stringify(updated), operationId);
563
+ }
564
+ return result.changes;
565
+ });
566
+ return commit();
567
+ }
568
+ sourceImportTotals(sourceId, batchRunId) {
569
+ const events = Number(this.db.prepare(`SELECT COUNT(*) AS count
570
+ FROM session_events e
571
+ JOIN import_runs r ON r.import_run_id=e.import_run_id
572
+ WHERE e.source_id=? AND r.batch_run_id=?`).get(sourceId, batchRunId)?.count ?? 0);
573
+ const sessions = Number(this.db.prepare(`SELECT COUNT(DISTINCT e.session_id) AS count
574
+ FROM session_events e
575
+ JOIN import_runs r ON r.import_run_id=e.import_run_id
576
+ WHERE e.source_id=? AND r.batch_run_id=?`).get(sourceId, batchRunId)?.count ?? 0);
577
+ return { sessions, events };
578
+ }
579
+ getSession(id, workspaceId) {
580
+ const row = this.db.prepare(`SELECT s.data FROM sessions s
581
+ WHERE s.session_id=? AND s.lifecycle != ?
582
+ ${workspaceId ? 'AND s.workspace_id=?' : ''}
583
+ AND EXISTS (
584
+ SELECT 1 FROM session_events e
585
+ JOIN import_runs r ON r.import_run_id=e.import_run_id
586
+ WHERE e.session_id=s.session_id AND r.status='committed'
587
+ )`)
588
+ .get(...(workspaceId ? [id, 'tombstoned', workspaceId] : [id, 'tombstoned']));
589
+ return row ? JSON.parse(String(row.data)) : null;
590
+ }
591
+ listSources() {
592
+ return this.db.prepare('SELECT data FROM session_sources ORDER BY provider, source_id').all().map((row) => JSON.parse(String(row.data)));
593
+ }
594
+ listWorkspaceIds() {
595
+ return this.db.prepare('SELECT DISTINCT workspace_id FROM sessions ORDER BY workspace_id').all().map((row) => String(row.workspace_id));
596
+ }
597
+ saveBatchImportRun(run) {
598
+ const save = this.db.transaction(() => {
599
+ const prior = this.db.prepare('SELECT manifest_id, workspace_id FROM batch_import_runs WHERE batch_run_id=?').get(run.runId);
600
+ if (prior && (String(prior.manifest_id) !== run.manifestId
601
+ || String(prior.workspace_id) !== run.workspaceId)) {
602
+ throw new SessionContractError('IMPORT_CONFLICT', 'batch run identity changed manifest or workspace scope');
603
+ }
604
+ this.db.prepare(`INSERT INTO batch_import_runs(
605
+ batch_run_id, manifest_id, workspace_id, status, started_at, updated_at, data
606
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
607
+ ON CONFLICT(batch_run_id) DO UPDATE SET
608
+ status=excluded.status, updated_at=excluded.updated_at, data=excluded.data`).run(run.runId, run.manifestId, run.workspaceId, run.status, run.startedAt, run.updatedAt, JSON.stringify(run));
609
+ });
610
+ save();
611
+ }
612
+ getBatchImportRun(runId) {
613
+ const row = this.db.prepare('SELECT data FROM batch_import_runs WHERE batch_run_id=?').get(runId);
614
+ return row ? JSON.parse(String(row.data)) : null;
615
+ }
616
+ getBatchImportRunForManifest(manifestId, workspaceId) {
617
+ const row = this.db.prepare(`SELECT data FROM batch_import_runs
618
+ WHERE manifest_id=? AND workspace_id=?`).get(manifestId, workspaceId);
619
+ return row ? JSON.parse(String(row.data)) : null;
620
+ }
621
+ getCoverage(workspaceId, now = new Date()) {
622
+ const row = this.db.prepare(`SELECT data FROM batch_import_runs
623
+ WHERE workspace_id=?
624
+ ORDER BY updated_at DESC, batch_run_id DESC LIMIT 1`).get(workspaceId);
625
+ const coverage = coverageFromBatchRun(row ? JSON.parse(String(row.data)) : null, now);
626
+ return coverage.status === 'unknown'
627
+ ? { ...coverage, workspaceId }
628
+ : coverage;
629
+ }
630
+ listSessions(options) {
631
+ const where = [`s.lifecycle != 'tombstoned'`, `EXISTS (
632
+ SELECT 1 FROM session_events e
633
+ JOIN import_runs r ON r.import_run_id=e.import_run_id
634
+ WHERE e.session_id=s.session_id AND r.status='committed'
635
+ )`];
636
+ const scopeDigest = sessionListScopeDigest(options);
637
+ const cursor = decodeSessionListCursor(options.cursor);
638
+ if (cursor && cursor.scopeDigest !== scopeDigest) {
639
+ throw new SessionContractError('SCHEMA_DRIFT', 'session list cursor does not match the requested scope');
640
+ }
641
+ const snapshotRowid = cursor?.snapshotRowid ?? Number(this.db.prepare('SELECT COALESCE(MAX(rowid), 0) AS value FROM sessions').get()?.value ?? 0);
642
+ where.push('s.rowid <= ?');
643
+ const params = [snapshotRowid];
644
+ if (options.provider) {
645
+ where.push('json_extract(s.data, \'$.provider\') = ?');
646
+ params.push(options.provider);
647
+ }
648
+ if (options.workspaceId) {
649
+ where.push('s.workspace_id = ?');
650
+ params.push(options.workspaceId);
651
+ }
652
+ if (options.tag) {
653
+ where.push('EXISTS (SELECT 1 FROM session_tags t WHERE t.session_id=s.session_id AND t.tag=?)');
654
+ params.push(options.tag);
655
+ }
656
+ const clause = where.join(' AND ');
657
+ const totalRow = this.db.prepare(`SELECT COUNT(*) AS count FROM sessions s WHERE ${clause}`)
658
+ .get(...params);
659
+ const pageWhere = [...where];
660
+ const pageParams = [...params];
661
+ if (cursor) {
662
+ pageWhere.push(`(
663
+ COALESCE(json_extract(s.data, '$.updatedAt'), '') > ?
664
+ OR (
665
+ COALESCE(json_extract(s.data, '$.updatedAt'), '') = ?
666
+ AND s.session_id > ?
667
+ )
668
+ )`);
669
+ pageParams.push(cursor.updatedAt, cursor.updatedAt, cursor.sessionId);
670
+ }
671
+ const items = this.db.prepare(`SELECT s.data FROM sessions s WHERE ${pageWhere.join(' AND ')}
672
+ ORDER BY COALESCE(json_extract(s.data, '$.updatedAt'), ''), s.session_id
673
+ LIMIT ?${options.offset ? ' OFFSET ?' : ''}`).all(...pageParams, options.limit + 1, ...(options.offset ? [options.offset] : []))
674
+ .map((row) => JSON.parse(String(row.data)));
675
+ const page = items.slice(0, options.limit);
676
+ const last = page.at(-1);
677
+ return {
678
+ items: page,
679
+ total: Number(totalRow?.count ?? 0),
680
+ nextCursor: items.length > options.limit && last
681
+ ? encodeSessionListCursor({
682
+ snapshotRowid,
683
+ updatedAt: last.updatedAt ?? '',
684
+ sessionId: last.sessionId,
685
+ scopeDigest,
686
+ })
687
+ : null,
688
+ snapshotRowid,
689
+ };
690
+ }
691
+ listTags(sessionId, workspaceId) {
692
+ return this.db.prepare(`SELECT t.tag FROM session_tags t
693
+ JOIN sessions s ON s.session_id=t.session_id
694
+ WHERE t.session_id=? ${workspaceId ? 'AND s.workspace_id=?' : ''}
695
+ ORDER BY t.tag`).all(...(workspaceId ? [sessionId, workspaceId] : [sessionId]))
696
+ .map((row) => String(row.tag));
697
+ }
698
+ tagSession(sessionId, tag, workspaceId) {
699
+ const mutate = this.db.transaction(() => {
700
+ if (!this.getSession(sessionId, workspaceId))
701
+ return false;
702
+ const changed = this.db.prepare('INSERT OR IGNORE INTO session_tags(session_id, tag) VALUES (?, ?)').run(sessionId, tag).changes > 0;
703
+ if (changed) {
704
+ const scope = this.workspaceForSession(sessionId);
705
+ this.emitRepositoryMutation({
706
+ operationId: sha256(['session.tag', sessionId, tag].join('\0')),
707
+ eventName: 'session.tag',
708
+ workspaceId: scope,
709
+ targetClass: 'session',
710
+ counts: { tags: 1 },
711
+ });
712
+ }
713
+ return changed;
714
+ });
715
+ return mutate();
716
+ }
717
+ listEvents(sessionId, workspaceId) {
718
+ return this.db.prepare(`SELECT e.data FROM session_events e
719
+ JOIN sessions s ON s.session_id=e.session_id
720
+ JOIN import_runs r ON r.import_run_id=e.import_run_id
721
+ WHERE e.session_id=? ${workspaceId ? 'AND s.workspace_id=?' : ''}
722
+ AND r.status='committed'
723
+ ORDER BY e.sequence_no, e.event_id`).all(...(workspaceId ? [sessionId, workspaceId] : [sessionId]))
724
+ .map((row) => JSON.parse(String(row.data)));
725
+ }
726
+ listTimelineInputs(workspaceId) {
727
+ return this.db.prepare(`SELECT s.data AS session_data, e.data AS event_data
728
+ FROM session_events e
729
+ JOIN sessions s ON s.session_id=e.session_id
730
+ JOIN import_runs r ON r.import_run_id=e.import_run_id
731
+ WHERE s.workspace_id=? AND s.lifecycle!='tombstoned' AND r.status='committed'
732
+ ORDER BY
733
+ CASE WHEN json_extract(e.data, '$.occurredAt') IS NULL THEN 1 ELSE 0 END,
734
+ json_extract(e.data, '$.occurredAt'),
735
+ json_extract(s.data, '$.provider'),
736
+ e.session_id,
737
+ e.sequence_no,
738
+ e.event_id`).all(workspaceId).map((row) => ({
739
+ session: JSON.parse(String(row.session_data)),
740
+ event: JSON.parse(String(row.event_data)),
741
+ }));
742
+ }
743
+ search(options) {
744
+ const query = options.query.trim();
745
+ if (!query)
746
+ throw new SessionContractError('MALFORMED_SOURCE', 'search query must not be empty');
747
+ if (options.limit < 1 || options.limit > 500) {
748
+ throw new SessionContractError('RESOURCE_LIMIT_EXCEEDED', 'search limit must be between 1 and 500');
749
+ }
750
+ const scopeDigest = searchScopeDigest(options);
751
+ const cursor = decodeSearchCursor(options.cursor);
752
+ if (cursor && cursor.scopeDigest !== scopeDigest) {
753
+ throw new SessionContractError('SCHEMA_DRIFT', 'search cursor does not match the requested scope');
754
+ }
755
+ const snapshotRowid = cursor?.snapshotRowid ?? Number(this.db.prepare('SELECT COALESCE(MAX(rowid), 0) AS value FROM session_events').get()?.value ?? 0);
756
+ const where = [
757
+ `session_event_fts.searchable_text MATCH ?`,
758
+ `e.rowid <= ?`,
759
+ `s.workspace_id = ?`,
760
+ `s.lifecycle != 'tombstoned'`,
761
+ `r.status = 'committed'`,
762
+ ];
763
+ const params = [query, snapshotRowid, options.workspaceId];
764
+ appendMetadataFilters(where, params, options);
765
+ if (cursor) {
766
+ where.push(`(
767
+ session_event_fts.rank > ?
768
+ OR (
769
+ session_event_fts.rank = ?
770
+ AND e.event_id > ?
771
+ )
772
+ )`);
773
+ params.push(cursor.rank, cursor.rank, cursor.eventId);
774
+ }
775
+ let rows;
776
+ try {
777
+ rows = this.db.prepare(`
778
+ SELECT
779
+ e.rowid AS event_rowid, e.event_id, e.session_id, e.source_id,
780
+ e.import_run_id, e.sequence_no, e.data AS event_data,
781
+ s.workspace_id, s.data AS session_data, src.data AS source_data,
782
+ session_event_fts.searchable_text,
783
+ session_event_fts.rank AS stable_rank,
784
+ snippet(session_event_fts, 1, '⟦', '⟧', ' … ', 32) AS matched_snippet
785
+ FROM session_event_fts
786
+ -- FTS matches must drive this join. SQLite may otherwise scan the
787
+ -- authorization tables first and execute MATCH once per candidate.
788
+ CROSS JOIN session_events e ON e.rowid=session_event_fts.rowid
789
+ CROSS JOIN sessions s ON s.session_id=e.session_id
790
+ CROSS JOIN import_runs r ON r.import_run_id=e.import_run_id
791
+ CROSS JOIN session_sources src ON src.source_id=e.source_id
792
+ WHERE ${where.join(' AND ')}
793
+ ORDER BY session_event_fts.rank, e.event_id
794
+ LIMIT ?
795
+ `).all(...params, options.limit + 1);
796
+ }
797
+ catch (error) {
798
+ if (error instanceof Error && /fts5|syntax|unterminated|column/i.test(error.message)) {
799
+ throw new SessionContractError('INVALID_SEARCH_QUERY', 'search query syntax is invalid');
800
+ }
801
+ throw error;
802
+ }
803
+ const page = rows.slice(0, options.limit);
804
+ const items = page.map(searchHit);
805
+ const last = page.at(-1);
806
+ return {
807
+ items,
808
+ nextCursor: rows.length > options.limit && last
809
+ ? encodeSearchCursor({
810
+ snapshotRowid,
811
+ rank: Number(last.stable_rank),
812
+ eventId: String(last.event_id),
813
+ scopeDigest,
814
+ })
815
+ : null,
816
+ };
817
+ }
818
+ authorizedSearchDocuments(options) {
819
+ return this.authorizedSearchDocumentPage(options).items;
820
+ }
821
+ authorizedSearchDocumentPage(options) {
822
+ if (options.limit < 1 || options.limit > 500) {
823
+ throw new SessionContractError('RESOURCE_LIMIT_EXCEEDED', 'semantic candidate limit must be between 1 and 500');
824
+ }
825
+ const scopeDigest = authorizedDocumentScopeDigest(options);
826
+ const cursor = decodeAuthorizedDocumentCursor(options.cursor);
827
+ if (cursor && cursor.scopeDigest !== scopeDigest) {
828
+ throw new SessionContractError('SCHEMA_DRIFT', 'authorized document cursor does not match the requested scope');
829
+ }
830
+ const snapshotRowid = cursor?.snapshotRowid ?? Number(this.db.prepare('SELECT COALESCE(MAX(rowid), 0) AS value FROM session_events').get()?.value ?? 0);
831
+ const where = [
832
+ `e.rowid <= ?`,
833
+ `s.workspace_id = ?`,
834
+ `s.lifecycle != 'tombstoned'`,
835
+ `r.status = 'committed'`,
836
+ ];
837
+ const params = [snapshotRowid, options.workspaceId];
838
+ appendMetadataFilters(where, params, options);
839
+ if (cursor) {
840
+ where.push(`e.event_id > ?`);
841
+ params.push(cursor.eventId);
842
+ }
843
+ const rows = this.db.prepare(`
844
+ SELECT
845
+ e.event_id, e.session_id, e.source_id, e.import_run_id,
846
+ e.sequence_no, e.data AS event_data, s.workspace_id,
847
+ s.data AS session_data, src.data AS source_data,
848
+ json_extract(e.data, '$.searchableText') AS searchable_text,
849
+ length(json_extract(e.data, '$.searchableText')) AS stable_rank
850
+ FROM session_events e
851
+ -- Preserve event-id keyset order as the outer loop. Without this planner
852
+ -- fence SQLite may materialize and sort the entire authorized workspace
853
+ -- before applying the bounded semantic-candidate limit.
854
+ CROSS JOIN sessions s ON s.session_id=e.session_id
855
+ CROSS JOIN import_runs r ON r.import_run_id=e.import_run_id
856
+ CROSS JOIN session_sources src ON src.source_id=e.source_id
857
+ WHERE ${where.join(' AND ')}
858
+ ORDER BY e.event_id
859
+ LIMIT ?
860
+ `).all(...params, options.limit + 1);
861
+ const page = rows.slice(0, options.limit);
862
+ const items = page.map((row) => ({
863
+ ...searchHit(row),
864
+ searchableText: String(row.searchable_text),
865
+ }));
866
+ const last = page.at(-1);
867
+ return {
868
+ items,
869
+ nextCursor: rows.length > options.limit && last
870
+ ? encodeAuthorizedDocumentCursor({
871
+ snapshotRowid,
872
+ eventId: String(last.event_id),
873
+ scopeDigest,
874
+ })
875
+ : null,
876
+ snapshotRowid,
877
+ };
878
+ }
879
+ saveCandidates(candidates) {
880
+ const save = this.db.transaction(() => {
881
+ let inserted = 0;
882
+ const incomingIds = new Set(candidates.map((candidate) => candidate.candidateId));
883
+ for (const candidate of candidates) {
884
+ for (const linkedId of [...candidate.conflictsWith, ...candidate.supersedes]) {
885
+ if (!incomingIds.has(linkedId) && !this.getCandidate(linkedId)) {
886
+ throw new SessionContractError('MALFORMED_SOURCE', 'candidate conflict or supersession link does not resolve');
887
+ }
888
+ }
889
+ }
890
+ const saved = candidates.map((input) => {
891
+ const parsed = IntelligenceCandidateSchema.parse(input);
892
+ this.assertCandidateEvidence(parsed);
893
+ const priorRow = this.db.prepare(`SELECT data FROM intelligence_candidates
894
+ WHERE candidate_id=? ORDER BY version DESC LIMIT 1`).get(parsed.candidateId);
895
+ if (priorRow) {
896
+ const prior = JSON.parse(String(priorRow.data));
897
+ if (candidateContentDigest(prior) === candidateContentDigest(parsed))
898
+ return prior;
899
+ parsed.version = prior.version + 1;
900
+ parsed.reviewState = 'pending';
901
+ }
902
+ else {
903
+ parsed.version = 1;
904
+ parsed.reviewState = 'pending';
905
+ }
906
+ this.db.prepare(`INSERT INTO intelligence_candidates(candidate_id, version, review_state, data)
907
+ VALUES (?, ?, ?, ?)`).run(parsed.candidateId, parsed.version, parsed.reviewState, JSON.stringify(parsed));
908
+ inserted += 1;
909
+ return parsed;
910
+ });
911
+ if (inserted > 0 && saved[0]) {
912
+ const workspaceId = this.workspaceForCandidate(saved[0]);
913
+ if (!workspaceId) {
914
+ throw new SessionContractError('OPERATION_NOT_AUTHORIZED', 'candidate mutation lacks an authorized workspace');
915
+ }
916
+ this.emitRepositoryMutation({
917
+ operationId: sha256([
918
+ 'candidate.save',
919
+ ...saved.map((item) => `${item.candidateId}:${item.version}`).sort(),
920
+ ].join('\0')),
921
+ eventName: 'candidate.save',
922
+ workspaceId,
923
+ targetClass: 'intelligence-candidate',
924
+ counts: { candidates: inserted },
925
+ });
926
+ }
927
+ return saved;
928
+ });
929
+ return save();
930
+ }
931
+ getCandidate(candidateId, version, workspaceId) {
932
+ const workspaceClause = workspaceId
933
+ ? `AND ${candidateWorkspacePredicate('intelligence_candidates')}` : '';
934
+ const row = version === undefined
935
+ ? this.db.prepare(`SELECT data FROM intelligence_candidates
936
+ WHERE candidate_id=? ${workspaceClause} ORDER BY version DESC LIMIT 1`).get(...(workspaceId ? [candidateId, workspaceId, workspaceId] : [candidateId]))
937
+ : this.db.prepare(`SELECT data FROM intelligence_candidates
938
+ WHERE candidate_id=? AND version=? ${workspaceClause}`).get(...(workspaceId
939
+ ? [candidateId, version, workspaceId, workspaceId]
940
+ : [candidateId, version]));
941
+ return row
942
+ ? IntelligenceCandidateSchema.parse(JSON.parse(String(row.data)))
943
+ : null;
944
+ }
945
+ listCandidates(reviewState, workspaceId) {
946
+ const predicate = candidateWorkspacePredicate('intelligence_candidates');
947
+ const rows = reviewState
948
+ ? this.db.prepare(`SELECT data FROM intelligence_candidates WHERE review_state=?
949
+ ${workspaceId ? `AND ${predicate}` : ''}
950
+ ORDER BY candidate_id, version`).all(...(workspaceId
951
+ ? [reviewState, workspaceId, workspaceId]
952
+ : [reviewState]))
953
+ : this.db.prepare(`SELECT data FROM intelligence_candidates
954
+ ${workspaceId ? `WHERE ${predicate}` : ''}
955
+ ORDER BY candidate_id, version`).all(...(workspaceId ? [workspaceId, workspaceId] : []));
956
+ return rows.map((row) => IntelligenceCandidateSchema.parse(JSON.parse(String(row.data))));
957
+ }
958
+ reviewCandidate(input) {
959
+ const review = this.db.transaction(() => {
960
+ const candidate = this.getCandidate(input.candidateId, input.version, input.workspaceId);
961
+ if (!candidate) {
962
+ throw new SessionContractError('MALFORMED_SOURCE', 'candidate version does not exist');
963
+ }
964
+ if (!allowedCandidateTransition(candidate.reviewState, input.toState)) {
965
+ throw new SessionContractError('UNSUPPORTED_OPERATION', `candidate transition ${candidate.reviewState} -> ${input.toState} is not allowed`);
966
+ }
967
+ if (input.toState === 'accepted'
968
+ && candidate.security.requiresAcknowledgement
969
+ && !input.securityAcknowledged) {
970
+ throw new SessionContractError('OPERATION_NOT_AUTHORIZED', 'suspicious candidate acceptance requires explicit security acknowledgment');
971
+ }
972
+ const occurredAt = new Date().toISOString();
973
+ const receipt = CandidateReviewReceiptSchema.parse({
974
+ contractVersion: '1.0.0',
975
+ receiptId: sha256([
976
+ input.candidateId, input.version, candidate.reviewState,
977
+ input.toState, input.reviewer, input.reason,
978
+ ].join('\0')),
979
+ candidateId: input.candidateId,
980
+ candidateVersion: input.version,
981
+ fromState: candidate.reviewState,
982
+ toState: input.toState,
983
+ reviewer: input.reviewer,
984
+ reason: input.reason,
985
+ securityWarnings: candidate.security.warnings,
986
+ securityAcknowledged: Boolean(input.securityAcknowledged),
987
+ occurredAt,
988
+ });
989
+ candidate.reviewState = input.toState;
990
+ candidate.security.acknowledged = input.toState === 'accepted'
991
+ && Boolean(input.securityAcknowledged);
992
+ this.db.prepare(`UPDATE intelligence_candidates SET review_state=?, data=?
993
+ WHERE candidate_id=? AND version=?`).run(input.toState, JSON.stringify(candidate), input.candidateId, input.version);
994
+ this.db.prepare(`INSERT OR IGNORE INTO candidate_review_receipts
995
+ (receipt_id, candidate_id, candidate_version, data) VALUES (?, ?, ?, ?)`).run(receipt.receiptId, input.candidateId, input.version, JSON.stringify(receipt));
996
+ const workspaceId = this.workspaceForCandidate(candidate);
997
+ if (!workspaceId) {
998
+ throw new SessionContractError('OPERATION_NOT_AUTHORIZED', 'candidate review lacks an authorized workspace');
999
+ }
1000
+ this.emitRepositoryMutation({
1001
+ operationId: receipt.receiptId,
1002
+ eventName: 'candidate.review',
1003
+ workspaceId,
1004
+ targetClass: 'intelligence-candidate',
1005
+ actorClass: 'reviewer',
1006
+ eventTime: receipt.occurredAt,
1007
+ counts: { candidates: 1, receipts: 1 },
1008
+ });
1009
+ return receipt;
1010
+ });
1011
+ return review();
1012
+ }
1013
+ getPromotionReceipt(candidateId, version, consumer) {
1014
+ const row = this.db.prepare(`SELECT data FROM promotion_receipts
1015
+ WHERE candidate_id=? AND candidate_version=? AND consumer=?`).get(candidateId, version, consumer);
1016
+ return row ? JSON.parse(String(row.data)) : null;
1017
+ }
1018
+ recordPromotion(receiptInput) {
1019
+ const record = this.db.transaction(() => {
1020
+ const receipt = PromotionReceiptSchema.parse(receiptInput);
1021
+ const existing = this.getPromotionReceipt(receipt.candidateId, receipt.candidateVersion, receipt.consumer);
1022
+ if (existing)
1023
+ return { ...existing, duplicate: true };
1024
+ const candidate = this.getCandidate(receipt.candidateId, receipt.candidateVersion);
1025
+ if (!candidate || candidate.reviewState !== 'accepted') {
1026
+ throw new SessionContractError('OPERATION_NOT_AUTHORIZED', 'promotion requires an accepted exact candidate version');
1027
+ }
1028
+ const evidenceIds = [...new Set(candidate.evidence.map((item) => item.eventId))].sort();
1029
+ if (JSON.stringify(evidenceIds) !== JSON.stringify([...receipt.evidenceEventIds].sort())) {
1030
+ throw new SessionContractError('IMPORT_CONFLICT', 'promotion lineage does not match candidate evidence');
1031
+ }
1032
+ this.db.prepare(`INSERT INTO promotion_receipts
1033
+ (receipt_id, candidate_id, candidate_version, consumer, data)
1034
+ VALUES (?, ?, ?, ?, ?)`).run(receipt.receiptId, receipt.candidateId, receipt.candidateVersion, receipt.consumer, JSON.stringify(receipt));
1035
+ candidate.reviewState = 'promoted';
1036
+ this.db.prepare(`UPDATE intelligence_candidates SET review_state='promoted', data=?
1037
+ WHERE candidate_id=? AND version=?`).run(JSON.stringify(candidate), candidate.candidateId, candidate.version);
1038
+ const workspaceId = this.workspaceForCandidate(candidate);
1039
+ if (!workspaceId) {
1040
+ throw new SessionContractError('OPERATION_NOT_AUTHORIZED', 'promotion lacks an authorized workspace');
1041
+ }
1042
+ this.emitRepositoryMutation({
1043
+ operationId: receipt.receiptId,
1044
+ eventName: 'candidate.promote',
1045
+ workspaceId,
1046
+ targetClass: 'promotion',
1047
+ eventTime: receipt.approvedAt,
1048
+ counts: { candidates: 1, promotions: 1, evidence: evidenceIds.length },
1049
+ });
1050
+ return receipt;
1051
+ });
1052
+ return record();
1053
+ }
1054
+ assertCandidateEvidence(candidate) {
1055
+ for (const evidence of candidate.evidence) {
1056
+ const row = this.db.prepare(`SELECT e.data FROM session_events e
1057
+ JOIN sessions s ON s.session_id=e.session_id
1058
+ JOIN import_runs r ON r.import_run_id=e.import_run_id
1059
+ WHERE e.event_id=? AND s.lifecycle!='tombstoned' AND r.status='committed'`).get(evidence.eventId);
1060
+ if (!row) {
1061
+ throw new SessionContractError('MALFORMED_SOURCE', 'candidate evidence does not resolve to a visible committed event');
1062
+ }
1063
+ const event = JSON.parse(String(row.data));
1064
+ if (evidence.end > event.searchableText.length || evidence.start >= evidence.end) {
1065
+ throw new SessionContractError('MALFORMED_SOURCE', 'candidate evidence span is invalid');
1066
+ }
1067
+ if (sha256(event.searchableText.slice(evidence.start, evidence.end)) !== evidence.quoteDigest) {
1068
+ throw new SessionContractError('IMPORT_CONFLICT', 'candidate evidence digest does not match');
1069
+ }
1070
+ }
1071
+ }
1072
+ getCheckpoint(sourceId, parserVersion) {
1073
+ const row = this.db.prepare(`SELECT checkpoint FROM import_runs
1074
+ WHERE source_id=? AND parser_version=? AND status='committed'
1075
+ ORDER BY rowid DESC LIMIT 1`).get(sourceId, parserVersion);
1076
+ return row ? JSON.parse(String(row.checkpoint)) : null;
1077
+ }
1078
+ getBatchCheckpoint(sourceId, parserVersion, batchRunId) {
1079
+ const row = this.db.prepare(`SELECT checkpoint FROM import_runs
1080
+ WHERE source_id=? AND parser_version=? AND batch_run_id=?
1081
+ AND status IN ('staged', 'committed')
1082
+ ORDER BY rowid DESC LIMIT 1`).get(sourceId, parserVersion, batchRunId);
1083
+ return row ? JSON.parse(String(row.checkpoint)) : null;
1084
+ }
1085
+ relocateSource(sourceId, redactedLocator, workspaceId) {
1086
+ const relocate = this.db.transaction(() => {
1087
+ const row = this.db.prepare(`SELECT src.data FROM session_sources src
1088
+ WHERE src.source_id=? ${workspaceId ? `AND EXISTS (
1089
+ SELECT 1 FROM sessions s
1090
+ WHERE s.source_id=src.source_id AND s.workspace_id=?
1091
+ ) AND NOT EXISTS (
1092
+ SELECT 1 FROM sessions s
1093
+ WHERE s.source_id=src.source_id AND s.workspace_id!=?
1094
+ )` : ''}`).get(...(workspaceId ? [sourceId, workspaceId, workspaceId] : [sourceId]));
1095
+ if (!row)
1096
+ return;
1097
+ const scopeRows = this.db.prepare('SELECT DISTINCT workspace_id FROM sessions WHERE source_id=?').all(sourceId);
1098
+ if (scopeRows.length !== 1) {
1099
+ throw new SessionContractError('OPERATION_NOT_AUTHORIZED', 'source relocation requires one authorized workspace');
1100
+ }
1101
+ const source = JSON.parse(String(row.data));
1102
+ source.redactedLocator = redactedLocator;
1103
+ this.db.prepare('UPDATE session_sources SET data=? WHERE source_id=?')
1104
+ .run(JSON.stringify(source), sourceId);
1105
+ this.emitRepositoryMutation({
1106
+ operationId: sha256(['source.relocate', sourceId, redactedLocator].join('\0')),
1107
+ eventName: 'source.relocate',
1108
+ workspaceId: String(scopeRows[0].workspace_id),
1109
+ targetClass: 'session-source',
1110
+ counts: { sources: 1 },
1111
+ });
1112
+ });
1113
+ relocate();
1114
+ }
1115
+ getSource(sourceId, workspaceId) {
1116
+ const row = this.db.prepare(`SELECT src.data FROM session_sources src
1117
+ WHERE src.source_id=? ${workspaceId ? `AND EXISTS (
1118
+ SELECT 1 FROM sessions s
1119
+ WHERE s.source_id=src.source_id AND s.workspace_id=?
1120
+ ) AND NOT EXISTS (
1121
+ SELECT 1 FROM sessions s
1122
+ WHERE s.source_id=src.source_id AND s.workspace_id!=?
1123
+ )` : ''}`).get(...(workspaceId ? [sourceId, workspaceId, workspaceId] : [sourceId]));
1124
+ return row ? JSON.parse(String(row.data)) : null;
1125
+ }
1126
+ deletionPreview(sessionId, workspaceId) {
1127
+ const session = this.db.prepare(`SELECT COUNT(*) AS count FROM sessions
1128
+ WHERE session_id=? AND lifecycle!='tombstoned'
1129
+ ${workspaceId ? 'AND workspace_id=?' : ''}`).get(...(workspaceId ? [sessionId, workspaceId] : [sessionId]));
1130
+ const events = this.db.prepare(`SELECT COUNT(*) AS count FROM session_events e
1131
+ JOIN sessions s ON s.session_id=e.session_id
1132
+ WHERE e.session_id=? ${workspaceId ? 'AND s.workspace_id=?' : ''}`).get(...(workspaceId ? [sessionId, workspaceId] : [sessionId]));
1133
+ const tags = this.db.prepare(`SELECT COUNT(*) AS count FROM session_tags t
1134
+ JOIN sessions s ON s.session_id=t.session_id
1135
+ WHERE t.session_id=? ${workspaceId ? 'AND s.workspace_id=?' : ''}`).get(...(workspaceId ? [sessionId, workspaceId] : [sessionId]));
1136
+ return {
1137
+ sessions: Number(session?.count ?? 0),
1138
+ events: Number(events?.count ?? 0),
1139
+ tags: Number(tags?.count ?? 0),
1140
+ };
1141
+ }
1142
+ tombstoneSession(sessionId, workspaceId) {
1143
+ const tombstone = this.db.transaction(() => {
1144
+ const row = this.db.prepare(`SELECT data, workspace_id FROM sessions
1145
+ WHERE session_id=? ${workspaceId ? 'AND workspace_id=?' : ''}`).get(...(workspaceId ? [sessionId, workspaceId] : [sessionId]));
1146
+ if (!row)
1147
+ return false;
1148
+ const session = JSON.parse(String(row.data));
1149
+ session.lifecycle = 'tombstoned';
1150
+ const changed = this.db.prepare(`UPDATE sessions SET lifecycle='tombstoned', data=?
1151
+ WHERE session_id=? AND lifecycle!='tombstoned'
1152
+ ${workspaceId ? 'AND workspace_id=?' : ''}`).run(...(workspaceId
1153
+ ? [JSON.stringify(session), sessionId, workspaceId]
1154
+ : [JSON.stringify(session), sessionId])).changes > 0;
1155
+ if (changed) {
1156
+ this.emitRepositoryMutation({
1157
+ operationId: sha256(['session.tombstone', sessionId].join('\0')),
1158
+ eventName: 'session.tombstone',
1159
+ workspaceId: String(row.workspace_id),
1160
+ targetClass: 'session',
1161
+ counts: { sessions: 1 },
1162
+ });
1163
+ }
1164
+ return changed;
1165
+ });
1166
+ return tombstone();
1167
+ }
1168
+ restoreSession(sessionId, workspaceId) {
1169
+ const restore = this.db.transaction(() => {
1170
+ const row = this.db.prepare(`SELECT data, workspace_id FROM sessions
1171
+ WHERE session_id=? ${workspaceId ? 'AND workspace_id=?' : ''}`).get(...(workspaceId ? [sessionId, workspaceId] : [sessionId]));
1172
+ if (!row)
1173
+ return false;
1174
+ const session = JSON.parse(String(row.data));
1175
+ session.lifecycle = 'active';
1176
+ const changed = this.db.prepare(`UPDATE sessions SET lifecycle='active', data=?
1177
+ WHERE session_id=? AND lifecycle='tombstoned'
1178
+ ${workspaceId ? 'AND workspace_id=?' : ''}`).run(...(workspaceId
1179
+ ? [JSON.stringify(session), sessionId, workspaceId]
1180
+ : [JSON.stringify(session), sessionId])).changes > 0;
1181
+ if (changed) {
1182
+ this.emitRepositoryMutation({
1183
+ operationId: sha256(['session.restore', sessionId].join('\0')),
1184
+ eventName: 'session.restore',
1185
+ workspaceId: String(row.workspace_id),
1186
+ targetClass: 'session',
1187
+ counts: { sessions: 1 },
1188
+ });
1189
+ }
1190
+ return changed;
1191
+ });
1192
+ return restore();
1193
+ }
1194
+ previewPurge(sessionId, workspaceId) {
1195
+ const session = this.db.prepare(`SELECT data FROM sessions
1196
+ WHERE session_id=? ${workspaceId ? 'AND workspace_id=?' : ''}`).get(...(workspaceId ? [sessionId, workspaceId] : [sessionId]));
1197
+ if (!session) {
1198
+ throw new SessionContractError('MALFORMED_SOURCE', 'session does not exist');
1199
+ }
1200
+ const eventIds = this.db.prepare('SELECT event_id FROM session_events WHERE session_id=? ORDER BY event_id').all(sessionId).map((row) => String(row.event_id));
1201
+ const eventSet = new Set(eventIds);
1202
+ const candidates = this.listCandidates()
1203
+ .filter((item) => item.evidence.some((evidence) => eventSet.has(evidence.eventId)));
1204
+ const candidateKeys = new Set(candidates.map((item) => `${item.candidateId}\0${item.version}`));
1205
+ const dependents = this.db.prepare('SELECT data FROM promotion_receipts ORDER BY candidate_id, candidate_version, consumer').all()
1206
+ .map((row) => JSON.parse(String(row.data)))
1207
+ .filter((receipt) => candidateKeys.has(`${receipt.candidateId}\0${receipt.candidateVersion}`))
1208
+ .map((receipt) => ({
1209
+ dependentId: receipt.receiptId,
1210
+ candidateId: receipt.candidateId,
1211
+ candidateVersion: receipt.candidateVersion,
1212
+ consumer: receipt.consumer,
1213
+ destinationRef: receipt.destinationRef,
1214
+ }));
1215
+ const candidateIds = candidates.map((item) => `${item.candidateId}:${item.version}`).sort();
1216
+ const counts = {
1217
+ sessions: 1,
1218
+ events: eventIds.length,
1219
+ indexes: eventIds.length,
1220
+ embeddings: 0,
1221
+ candidates: candidates.length,
1222
+ snapshots: 0,
1223
+ tags: Number(this.db.prepare('SELECT COUNT(*) AS count FROM session_tags WHERE session_id=?').get(sessionId)?.count ?? 0),
1224
+ promotedDependents: dependents.length,
1225
+ };
1226
+ const operationId = sha256(JSON.stringify({
1227
+ scopeClass: 'session',
1228
+ sessionId,
1229
+ workspaceId: workspaceId ?? null,
1230
+ eventIds,
1231
+ candidateIds,
1232
+ dependentIds: dependents.map((item) => item.dependentId),
1233
+ counts,
1234
+ }));
1235
+ const preview = {
1236
+ contractVersion: '1.0.0',
1237
+ operationId,
1238
+ scopeClass: 'session',
1239
+ sessionId,
1240
+ workspaceId,
1241
+ counts,
1242
+ promotedDependents: dependents,
1243
+ confirmationRequired: true,
1244
+ };
1245
+ const sessionValue = JSON.parse(String(session.data));
1246
+ this.emitRepositoryMutation({
1247
+ operationId: sha256(`${operationId}\0preview`),
1248
+ eventName: 'session.purge.preview',
1249
+ workspaceId: sessionValue.workspaceId,
1250
+ targetClass: 'session',
1251
+ outcome: 'preview',
1252
+ counts,
1253
+ });
1254
+ return preview;
1255
+ }
1256
+ getCompletedPurge(sessionId, workspaceId) {
1257
+ if (this.db.prepare('SELECT 1 AS present FROM sessions WHERE session_id=?').get(sessionId)) {
1258
+ return null;
1259
+ }
1260
+ const row = this.db.prepare(`SELECT data FROM deletion_receipts WHERE scope_id=?
1261
+ ${workspaceId ? 'AND workspace_id=?' : ''}
1262
+ ORDER BY rowid DESC LIMIT 1`).get(...(workspaceId ? [sessionId, workspaceId] : [sessionId]));
1263
+ return row ? JSON.parse(String(row.data)) : null;
1264
+ }
1265
+ listPromotionDependencyDecisions(operationId) {
1266
+ return this.db.prepare(`SELECT dependent_id, action, basis FROM promotion_dependency_decisions
1267
+ WHERE operation_id=? ORDER BY dependent_id`).all(operationId).map((row) => PromotionDependencyDecisionSchema.parse({
1268
+ dependentId: String(row.dependent_id),
1269
+ action: String(row.action),
1270
+ basis: String(row.basis),
1271
+ }));
1272
+ }
1273
+ listPromotionProvenanceReceipts(operationId) {
1274
+ const rows = operationId
1275
+ ? this.db.prepare(`SELECT data FROM promotion_provenance_receipts
1276
+ WHERE operation_id=? ORDER BY dependent_id`).all(operationId)
1277
+ : this.db.prepare('SELECT data FROM promotion_provenance_receipts ORDER BY operation_id, dependent_id').all();
1278
+ return rows.map((row) => JSON.parse(String(row.data)));
1279
+ }
1280
+ purgeSession(input) {
1281
+ const existing = this.db.prepare('SELECT data FROM deletion_receipts WHERE operation_id=?').get(input.preview.operationId);
1282
+ if (existing)
1283
+ return JSON.parse(String(existing.data));
1284
+ const current = this.previewPurge(input.preview.sessionId, input.preview.workspaceId);
1285
+ if (current.operationId !== input.preview.operationId) {
1286
+ throw new SessionContractError('IMPORT_CONFLICT', 'purge scope changed after preview');
1287
+ }
1288
+ const validatedDecisions = input.decisions.map((item) => PromotionDependencyDecisionSchema.parse(item));
1289
+ const expected = new Set(current.promotedDependents.map((item) => item.dependentId));
1290
+ const decisions = new Map(validatedDecisions.map((item) => [item.dependentId, item]));
1291
+ if (validatedDecisions.length !== expected.size
1292
+ || decisions.size !== expected.size
1293
+ || [...expected].some((dependentId) => !decisions.has(dependentId))) {
1294
+ throw new SessionContractError('OPERATION_NOT_AUTHORIZED', 'every promoted dependent requires one explicit disposition');
1295
+ }
1296
+ const mutationWorkspaceId = this.workspaceForSession(input.preview.sessionId);
1297
+ if (!mutationWorkspaceId) {
1298
+ throw new SessionContractError('OPERATION_NOT_AUTHORIZED', 'purge mutation lacks an authorized workspace');
1299
+ }
1300
+ const apply = this.db.transaction(() => {
1301
+ for (const decision of validatedDecisions) {
1302
+ this.db.prepare(`INSERT INTO promotion_dependency_decisions
1303
+ (operation_id, dependent_id, action, basis) VALUES (?, ?, ?, ?)`).run(input.preview.operationId, decision.dependentId, decision.action, decision.basis);
1304
+ const dependent = current.promotedDependents.find((item) => item.dependentId === decision.dependentId);
1305
+ const promotion = this.db.prepare('SELECT data FROM promotion_receipts WHERE receipt_id=?').get(decision.dependentId);
1306
+ if (!promotion) {
1307
+ throw new SessionContractError('IMPORT_CONFLICT', 'promoted dependency disappeared before durable provenance handling');
1308
+ }
1309
+ const promotionReceipt = JSON.parse(String(promotion.data));
1310
+ const provenance = {
1311
+ contractVersion: '1.0.0',
1312
+ receiptId: sha256([
1313
+ input.preview.operationId,
1314
+ decision.dependentId,
1315
+ decision.action,
1316
+ ].join('\0')),
1317
+ operationId: input.preview.operationId,
1318
+ dependentId: decision.dependentId,
1319
+ candidateId: dependent.candidateId,
1320
+ candidateVersion: dependent.candidateVersion,
1321
+ consumer: dependent.consumer,
1322
+ destinationRef: dependent.destinationRef,
1323
+ evidenceEventIds: [...promotionReceipt.evidenceEventIds].sort(),
1324
+ action: decision.action,
1325
+ basis: decision.basis,
1326
+ originAvailable: false,
1327
+ occurredAt: new Date().toISOString(),
1328
+ };
1329
+ this.db.prepare(`INSERT INTO promotion_provenance_receipts
1330
+ (receipt_id, operation_id, dependent_id, action, data)
1331
+ VALUES (?, ?, ?, ?, ?)`).run(provenance.receiptId, provenance.operationId, provenance.dependentId, provenance.action, JSON.stringify(provenance));
1332
+ }
1333
+ const eventRows = this.db.prepare('SELECT rowid AS event_rowid, event_id FROM session_events WHERE session_id=?').all(input.preview.sessionId);
1334
+ for (const row of eventRows) {
1335
+ this.db.prepare('DELETE FROM session_event_fts WHERE rowid=?')
1336
+ .run(Number(row.event_rowid));
1337
+ }
1338
+ const eventIds = new Set(eventRows.map((row) => String(row.event_id)));
1339
+ const affectedCandidates = this.listCandidates()
1340
+ .filter((item) => item.evidence.some((evidence) => eventIds.has(evidence.eventId)));
1341
+ for (const candidate of affectedCandidates) {
1342
+ this.db.prepare('DELETE FROM candidate_review_receipts WHERE candidate_id=? AND candidate_version=?').run(candidate.candidateId, candidate.version);
1343
+ this.db.prepare('DELETE FROM promotion_receipts WHERE candidate_id=? AND candidate_version=?').run(candidate.candidateId, candidate.version);
1344
+ this.db.prepare('DELETE FROM intelligence_candidates WHERE candidate_id=? AND version=?').run(candidate.candidateId, candidate.version);
1345
+ }
1346
+ this.db.prepare('DELETE FROM session_tags WHERE session_id=?').run(input.preview.sessionId);
1347
+ this.db.prepare('DELETE FROM session_events WHERE session_id=?').run(input.preview.sessionId);
1348
+ this.db.prepare('DELETE FROM sessions WHERE session_id=?').run(input.preview.sessionId);
1349
+ const orphanCounts = {
1350
+ sessions: Number(this.db.prepare('SELECT COUNT(*) AS count FROM sessions WHERE session_id=?').get(input.preview.sessionId)?.count ?? 0),
1351
+ events: Number(this.db.prepare('SELECT COUNT(*) AS count FROM session_events WHERE session_id=?').get(input.preview.sessionId)?.count ?? 0),
1352
+ indexes: eventRows.reduce((count, row) => count + Number(this.db.prepare('SELECT COUNT(*) AS count FROM session_event_fts WHERE rowid=?').get(Number(row.event_rowid))?.count ?? 0), 0),
1353
+ candidates: affectedCandidates.reduce((count, candidate) => count + Number(this.db.prepare('SELECT COUNT(*) AS count FROM intelligence_candidates WHERE candidate_id=? AND version=?').get(candidate.candidateId, candidate.version)?.count ?? 0), 0),
1354
+ };
1355
+ if (Object.values(orphanCounts).some((count) => count !== 0)) {
1356
+ throw new SessionContractError('IMPORT_CONFLICT', 'purge orphan check failed');
1357
+ }
1358
+ const receipt = DeletionReceiptSchema.parse({
1359
+ contractVersion: '1.0.0',
1360
+ receiptId: sha256(`${input.preview.operationId}\0terminal`),
1361
+ operationId: input.preview.operationId,
1362
+ scopeClass: 'session',
1363
+ counts: input.preview.counts,
1364
+ survivingDependentIds: validatedDecisions
1365
+ .filter((item) => !['revoke', 'delete'].includes(item.action))
1366
+ .map((item) => item.dependentId)
1367
+ .sort(),
1368
+ actorClass: input.actorClass,
1369
+ reasonCode: input.reasonCode,
1370
+ orphanCounts,
1371
+ outcome: 'committed',
1372
+ occurredAt: new Date().toISOString(),
1373
+ });
1374
+ this.db.prepare(`INSERT INTO deletion_receipts(operation_id, scope_id, workspace_id, data)
1375
+ VALUES (?, ?, ?, ?)`).run(receipt.operationId, input.preview.sessionId, input.preview.workspaceId ?? null, JSON.stringify(receipt));
1376
+ this.emitRepositoryMutation({
1377
+ operationId: sha256(`${receipt.operationId}\0commit`),
1378
+ eventName: 'session.purge',
1379
+ workspaceId: mutationWorkspaceId,
1380
+ targetClass: 'session',
1381
+ actorClass: input.actorClass,
1382
+ eventTime: receipt.occurredAt,
1383
+ counts: {
1384
+ ...receipt.counts,
1385
+ dependencyDispositions: validatedDecisions.length,
1386
+ },
1387
+ });
1388
+ return receipt;
1389
+ });
1390
+ return apply();
1391
+ }
1392
+ reindex(workspaceId) {
1393
+ const rebuild = this.db.transaction(() => {
1394
+ let indexed = 0;
1395
+ if (!workspaceId) {
1396
+ this.db.exec('REINDEX');
1397
+ this.db.exec('DELETE FROM session_event_fts');
1398
+ this.db.exec(`
1399
+ INSERT INTO session_event_fts(rowid, event_id, searchable_text)
1400
+ SELECT rowid, event_id, json_extract(data, '$.searchableText') FROM session_events
1401
+ `);
1402
+ indexed = Number(this.db.prepare('SELECT COUNT(*) AS count FROM session_events').get()?.count ?? 0);
1403
+ }
1404
+ else {
1405
+ this.db.prepare(`DELETE FROM session_event_fts WHERE rowid IN (
1406
+ SELECT e.rowid FROM session_events e
1407
+ JOIN sessions s ON s.session_id=e.session_id
1408
+ WHERE s.workspace_id=?
1409
+ )`).run(workspaceId);
1410
+ indexed = this.db.prepare(`INSERT INTO session_event_fts(rowid, event_id, searchable_text)
1411
+ SELECT e.rowid, e.event_id, json_extract(e.data, '$.searchableText')
1412
+ FROM session_events e
1413
+ JOIN sessions s ON s.session_id=e.session_id
1414
+ WHERE s.workspace_id=?`).run(workspaceId).changes;
1415
+ }
1416
+ this.emitRepositoryMutation({
1417
+ operationId: sha256([
1418
+ 'session.reindex',
1419
+ workspaceId ?? 'global',
1420
+ String(Date.now()),
1421
+ ].join('\0')),
1422
+ eventName: 'session.reindex',
1423
+ workspaceId: workspaceId ?? 'global',
1424
+ targetClass: 'search-index',
1425
+ counts: { events: indexed },
1426
+ });
1427
+ });
1428
+ rebuild();
1429
+ }
1430
+ doctor() {
1431
+ const integrity = this.db.pragma('integrity_check');
1432
+ const count = (table, where = '') => {
1433
+ const row = this.db.prepare(`SELECT COUNT(*) AS count FROM ${table} ${where}`).get();
1434
+ return Number(row?.count ?? 0);
1435
+ };
1436
+ let indexIntegrity = 'ok';
1437
+ try {
1438
+ this.db.prepare(`INSERT INTO session_event_fts(session_event_fts) VALUES ('integrity-check')`).run();
1439
+ if (count('session_event_fts') !== count('session_events'))
1440
+ indexIntegrity = 'failed';
1441
+ }
1442
+ catch {
1443
+ indexIntegrity = 'failed';
1444
+ }
1445
+ return {
1446
+ integrity: integrity[0]?.integrity_check === 'ok' ? 'ok' : 'failed',
1447
+ indexIntegrity,
1448
+ sources: count('session_sources'),
1449
+ sessions: count('sessions', `WHERE lifecycle!='tombstoned'`),
1450
+ events: count('session_events'),
1451
+ stagedImports: count('import_runs', `WHERE status='staged'`),
1452
+ };
1453
+ }
1454
+ close() {
1455
+ this.db.close();
1456
+ }
1457
+ }
1458
+ function searchHit(row) {
1459
+ const event = JSON.parse(String(row.event_data));
1460
+ const session = JSON.parse(String(row.session_data));
1461
+ const source = JSON.parse(String(row.source_data));
1462
+ const citation = {
1463
+ provider: session.provider,
1464
+ sessionId: event.sessionId,
1465
+ origin: event.origin,
1466
+ eventId: event.eventId,
1467
+ importRunId: event.importRunId,
1468
+ sourceId: event.sourceId,
1469
+ locatorClass: source.locatorClass,
1470
+ ...(event.nativeId ? { nativeEventId: event.nativeId } : {}),
1471
+ };
1472
+ return {
1473
+ score: Math.max(0, -Number(row.stable_rank)),
1474
+ snippet: boundedSnippet(String(row.matched_snippet ?? row.searchable_text), 240),
1475
+ provider: session.provider,
1476
+ workspaceId: session.workspaceId,
1477
+ sessionId: event.sessionId,
1478
+ origin: event.origin,
1479
+ eventId: event.eventId,
1480
+ importRunId: event.importRunId,
1481
+ sourceId: event.sourceId,
1482
+ locatorClass: source.locatorClass,
1483
+ sequence: event.sequence,
1484
+ role: event.role,
1485
+ nativeEventId: event.nativeId,
1486
+ occurredAt: event.occurredAt,
1487
+ sensitivity: event.sensitivity.classification,
1488
+ citation,
1489
+ };
1490
+ }
1491
+ function boundedSnippet(value, max) {
1492
+ return value.length <= max ? value : `${value.slice(0, max - 1)}…`;
1493
+ }
1494
+ function renameNativeWindsurfEnvelope(value) {
1495
+ if (!Object.hasOwn(value, 'native.windsurf'))
1496
+ return value;
1497
+ const output = { ...value };
1498
+ if (!Object.hasOwn(output, 'native.devin-desktop')) {
1499
+ output['native.devin-desktop'] = output['native.windsurf'];
1500
+ }
1501
+ delete output['native.windsurf'];
1502
+ return output;
1503
+ }
1504
+ function canonicalDevinLocator(value) {
1505
+ return value.startsWith('windsurf-')
1506
+ ? `devin-desktop-${value.slice('windsurf-'.length)}`
1507
+ : value;
1508
+ }
1509
+ function encodeMutationCursor(rowid, workspaceId) {
1510
+ const unsigned = { rowid, workspaceId };
1511
+ return Buffer.from(JSON.stringify({
1512
+ ...unsigned,
1513
+ checksum: sha256(JSON.stringify(unsigned)),
1514
+ }), 'utf8').toString('base64url');
1515
+ }
1516
+ function decodeMutationCursor(value, workspaceId) {
1517
+ if (!value)
1518
+ return 0;
1519
+ try {
1520
+ const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
1521
+ const unsigned = { rowid: parsed.rowid, workspaceId: parsed.workspaceId };
1522
+ if (!Number.isSafeInteger(parsed.rowid)
1523
+ || parsed.rowid < 0
1524
+ || parsed.workspaceId !== workspaceId
1525
+ || parsed.checksum !== sha256(JSON.stringify(unsigned))) {
1526
+ throw new Error('invalid cursor');
1527
+ }
1528
+ return parsed.rowid;
1529
+ }
1530
+ catch {
1531
+ throw new SessionContractError('SCHEMA_DRIFT', 'mutation audit cursor is invalid');
1532
+ }
1533
+ }
1534
+ function encodeSearchCursor(cursor) {
1535
+ const payload = { ...cursor, checksum: sha256(JSON.stringify(cursor)) };
1536
+ return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
1537
+ }
1538
+ function decodeSearchCursor(value) {
1539
+ if (!value)
1540
+ return null;
1541
+ try {
1542
+ const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
1543
+ const { checksum, ...unsigned } = parsed;
1544
+ if (!Number.isSafeInteger(parsed.snapshotRowid)
1545
+ || !Number.isFinite(parsed.rank)
1546
+ || typeof parsed.eventId !== 'string'
1547
+ || typeof parsed.scopeDigest !== 'string'
1548
+ || checksum !== sha256(JSON.stringify(unsigned)))
1549
+ throw new Error('invalid cursor');
1550
+ return parsed;
1551
+ }
1552
+ catch {
1553
+ throw new SessionContractError('SCHEMA_DRIFT', 'search cursor is invalid');
1554
+ }
1555
+ }
1556
+ function encodeSessionListCursor(cursor) {
1557
+ const payload = { ...cursor, checksum: sha256(JSON.stringify(cursor)) };
1558
+ return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
1559
+ }
1560
+ function decodeSessionListCursor(value) {
1561
+ if (!value)
1562
+ return null;
1563
+ try {
1564
+ const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
1565
+ const { checksum, ...unsigned } = parsed;
1566
+ if (!Number.isSafeInteger(parsed.snapshotRowid)
1567
+ || typeof parsed.updatedAt !== 'string'
1568
+ || typeof parsed.sessionId !== 'string'
1569
+ || typeof parsed.scopeDigest !== 'string'
1570
+ || checksum !== sha256(JSON.stringify(unsigned)))
1571
+ throw new Error('invalid cursor');
1572
+ return parsed;
1573
+ }
1574
+ catch {
1575
+ throw new SessionContractError('SCHEMA_DRIFT', 'session list cursor is invalid');
1576
+ }
1577
+ }
1578
+ function sessionListScopeDigest(options) {
1579
+ return sha256(JSON.stringify({
1580
+ provider: options.provider,
1581
+ workspaceId: options.workspaceId,
1582
+ tag: options.tag,
1583
+ }));
1584
+ }
1585
+ function searchScopeDigest(options) {
1586
+ const { cursor: _cursor, limit: _limit, ...scope } = options;
1587
+ return sha256(JSON.stringify(scope));
1588
+ }
1589
+ function encodeAuthorizedDocumentCursor(cursor) {
1590
+ const payload = { ...cursor, checksum: sha256(JSON.stringify(cursor)) };
1591
+ return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
1592
+ }
1593
+ function decodeAuthorizedDocumentCursor(value) {
1594
+ if (!value)
1595
+ return null;
1596
+ try {
1597
+ const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
1598
+ const { checksum, ...cursor } = parsed;
1599
+ if (!Number.isSafeInteger(cursor.snapshotRowid)
1600
+ || typeof cursor.eventId !== 'string'
1601
+ || typeof cursor.scopeDigest !== 'string'
1602
+ || checksum !== sha256(JSON.stringify(cursor)))
1603
+ throw new Error('invalid cursor');
1604
+ return parsed;
1605
+ }
1606
+ catch {
1607
+ throw new SessionContractError('SCHEMA_DRIFT', 'authorized document cursor is invalid');
1608
+ }
1609
+ }
1610
+ function authorizedDocumentScopeDigest(options) {
1611
+ const { cursor: _cursor, limit: _limit, ...scope } = options;
1612
+ return sha256(JSON.stringify(scope));
1613
+ }
1614
+ function appendMetadataFilters(where, params, options) {
1615
+ const controlEvents = options.controlEvents ?? 'exclude';
1616
+ const controlOrigins = [
1617
+ 'provider-bootstrap',
1618
+ 'workspace-instruction',
1619
+ 'tool-control',
1620
+ ];
1621
+ if (controlEvents === 'exclude') {
1622
+ where.push(`COALESCE(json_extract(e.data, '$.origin'), 'unknown') NOT IN (${controlOrigins.map(() => '?').join(',')})`);
1623
+ params.push(...controlOrigins);
1624
+ }
1625
+ else if (controlEvents === 'only') {
1626
+ where.push(`COALESCE(json_extract(e.data, '$.origin'), 'unknown') IN (${controlOrigins.map(() => '?').join(',')})`);
1627
+ params.push(...controlOrigins);
1628
+ }
1629
+ if (options.sessionIds?.length) {
1630
+ where.push(`e.session_id IN (${options.sessionIds.map(() => '?').join(',')})`);
1631
+ params.push(...options.sessionIds);
1632
+ }
1633
+ if (options.providers?.length) {
1634
+ where.push(`json_extract(s.data, '$.provider') IN (${options.providers.map(() => '?').join(',')})`);
1635
+ params.push(...options.providers);
1636
+ }
1637
+ if (options.dateFrom) {
1638
+ where.push(`json_extract(e.data, '$.occurredAt') >= ?`);
1639
+ params.push(options.dateFrom);
1640
+ }
1641
+ if (options.dateTo) {
1642
+ where.push(`json_extract(e.data, '$.occurredAt') <= ?`);
1643
+ params.push(options.dateTo);
1644
+ }
1645
+ if (options.role) {
1646
+ where.push(`json_extract(e.data, '$.role') = ?`);
1647
+ params.push(options.role);
1648
+ }
1649
+ if (options.participant) {
1650
+ where.push(`json_extract(e.data, '$.participant') = ?`);
1651
+ params.push(options.participant);
1652
+ }
1653
+ if (options.tool) {
1654
+ where.push(`(json_extract(e.data, '$.toolName') = ?
1655
+ OR json_extract(e.data, '$.toolCallId') = ?)`);
1656
+ params.push(options.tool, options.tool);
1657
+ }
1658
+ if (options.tag) {
1659
+ where.push(`EXISTS (
1660
+ SELECT 1 FROM session_tags t WHERE t.session_id=s.session_id AND t.tag=?
1661
+ )`);
1662
+ params.push(options.tag);
1663
+ }
1664
+ if (options.sensitivity) {
1665
+ where.push(`json_extract(e.data, '$.sensitivity.classification') = ?`);
1666
+ params.push(options.sensitivity);
1667
+ }
1668
+ if (options.model) {
1669
+ where.push(`json_extract(e.data, '$.model') = ?`);
1670
+ params.push(options.model);
1671
+ }
1672
+ if (options.entity) {
1673
+ where.push(`EXISTS (
1674
+ SELECT 1 FROM json_each(json_extract(e.data, '$.entities'))
1675
+ WHERE json_each.value = ?
1676
+ )`);
1677
+ params.push(options.entity);
1678
+ }
1679
+ if (options.extractionState) {
1680
+ where.push(`json_extract(e.data, '$.extractionState') = ?`);
1681
+ params.push(options.extractionState);
1682
+ }
1683
+ }
1684
+ function candidateWorkspacePredicate(alias) {
1685
+ return `EXISTS (
1686
+ SELECT 1
1687
+ FROM json_each(json_extract(${alias}.data, '$.evidence')) evidence
1688
+ JOIN session_events e
1689
+ ON e.event_id=json_extract(evidence.value, '$.eventId')
1690
+ JOIN sessions s ON s.session_id=e.session_id
1691
+ WHERE s.workspace_id=?
1692
+ ) AND NOT EXISTS (
1693
+ SELECT 1
1694
+ FROM json_each(json_extract(${alias}.data, '$.evidence')) evidence
1695
+ JOIN session_events e
1696
+ ON e.event_id=json_extract(evidence.value, '$.eventId')
1697
+ JOIN sessions s ON s.session_id=e.session_id
1698
+ WHERE s.workspace_id!=?
1699
+ )`;
1700
+ }
1701
+ function candidateContentDigest(candidate) {
1702
+ return sha256(JSON.stringify({
1703
+ ...candidate,
1704
+ security: {
1705
+ ...candidate.security,
1706
+ acknowledged: false,
1707
+ },
1708
+ version: undefined,
1709
+ reviewState: undefined,
1710
+ createdAt: undefined,
1711
+ }));
1712
+ }
1713
+ function mergeSessionAggregate(prior, incoming) {
1714
+ const timestamp = (left, right, direction) => {
1715
+ if (!left)
1716
+ return right;
1717
+ if (!right)
1718
+ return left;
1719
+ const comparison = Date.parse(left) - Date.parse(right);
1720
+ return direction === 'min'
1721
+ ? (comparison <= 0 ? left : right)
1722
+ : (comparison >= 0 ? left : right);
1723
+ };
1724
+ const consistencyRank = {
1725
+ provisional: 0,
1726
+ 'consistent-snapshot': 1,
1727
+ complete: 2,
1728
+ };
1729
+ const lifecycle = mergeLifecycleAggregate(prior, incoming);
1730
+ const nativeKey = `native.${prior.provider}`;
1731
+ const priorNative = objectRecord(prior.extensions[nativeKey]);
1732
+ const incomingNative = objectRecord(incoming.extensions[nativeKey]);
1733
+ return {
1734
+ ...prior,
1735
+ startedAt: timestamp(prior.startedAt, incoming.startedAt, 'min'),
1736
+ updatedAt: timestamp(prior.updatedAt, incoming.updatedAt, 'max'),
1737
+ consistency: consistencyRank[prior.consistency] >= consistencyRank[incoming.consistency]
1738
+ ? prior.consistency : incoming.consistency,
1739
+ lifecycle: lifecycle.lifecycle,
1740
+ intent: mergeSessionIntent(prior.intent, incoming.intent),
1741
+ sourceDigest: incoming.sourceDigest,
1742
+ extensions: {
1743
+ ...prior.extensions,
1744
+ ...incoming.extensions,
1745
+ [nativeKey]: {
1746
+ ...priorNative,
1747
+ ...incomingNative,
1748
+ lifecycleEvidence: lifecycle.evidence,
1749
+ },
1750
+ },
1751
+ };
1752
+ }
1753
+ function mergeLifecycleAggregate(prior, incoming) {
1754
+ const nativeKey = `native.${prior.provider}`;
1755
+ const priorEvidence = objectRecord(objectRecord(prior.extensions[nativeKey]).lifecycleEvidence);
1756
+ const incomingEvidence = objectRecord(objectRecord(incoming.extensions[nativeKey]).lifecycleEvidence);
1757
+ if (prior.lifecycle === 'tombstoned') {
1758
+ return { lifecycle: prior.lifecycle, evidence: priorEvidence };
1759
+ }
1760
+ const rank = {
1761
+ 'open-provisional-source': 1,
1762
+ 'inactivity-threshold': 1,
1763
+ 'complete-source': 2,
1764
+ 'provider-explicit-event': 3,
1765
+ };
1766
+ const priorRank = rank[String(priorEvidence.basis)] ?? 0;
1767
+ const incomingRank = rank[String(incomingEvidence.basis)] ?? 0;
1768
+ const incomingIsNewer = lifecycleEvidenceTimestamp(incomingEvidence)
1769
+ > lifecycleEvidenceTimestamp(priorEvidence);
1770
+ return incomingRank > priorRank || (incomingRank === priorRank && incomingIsNewer)
1771
+ ? { lifecycle: incoming.lifecycle, evidence: incomingEvidence }
1772
+ : { lifecycle: prior.lifecycle, evidence: priorEvidence };
1773
+ }
1774
+ function objectRecord(value) {
1775
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
1776
+ ? value
1777
+ : {};
1778
+ }
1779
+ function sanitizeSessionExtensions(extensions, provider) {
1780
+ const sanitized = sanitizeNativeExtensions(extensions).value;
1781
+ const nativeKey = `native.${provider}`;
1782
+ const evidence = safeLifecycleEvidence(objectRecord(objectRecord(extensions[nativeKey]).lifecycleEvidence));
1783
+ if (!evidence)
1784
+ return sanitized;
1785
+ sanitized[nativeKey] = {
1786
+ ...objectRecord(sanitized[nativeKey]),
1787
+ lifecycleEvidence: evidence,
1788
+ };
1789
+ return sanitized;
1790
+ }
1791
+ function safeLifecycleEvidence(evidence) {
1792
+ const bases = new Set([
1793
+ 'open-provisional-source',
1794
+ 'inactivity-threshold',
1795
+ 'complete-source',
1796
+ 'provider-explicit-event',
1797
+ ]);
1798
+ const states = new Set([
1799
+ 'active', 'inactive', 'paused', 'complete', 'interrupted', 'archived',
1800
+ 'unknown', 'tombstoned',
1801
+ ]);
1802
+ const confidences = new Set(['high', 'medium', 'low', 'provisional']);
1803
+ const basis = String(evidence.basis ?? '');
1804
+ const state = String(evidence.state ?? '');
1805
+ const confidence = String(evidence.confidence ?? '');
1806
+ const observedAt = String(evidence.observedAt ?? '');
1807
+ if (!bases.has(basis)
1808
+ || !states.has(state)
1809
+ || !confidences.has(confidence)
1810
+ || !Number.isFinite(Date.parse(observedAt))) {
1811
+ return null;
1812
+ }
1813
+ const output = {
1814
+ basis,
1815
+ state,
1816
+ observedAt,
1817
+ confidence,
1818
+ };
1819
+ if (typeof evidence.thresholdMs === 'number'
1820
+ && Number.isFinite(evidence.thresholdMs)
1821
+ && evidence.thresholdMs >= 0) {
1822
+ output.thresholdMs = evidence.thresholdMs;
1823
+ }
1824
+ return output;
1825
+ }
1826
+ function lifecycleEvidenceTimestamp(value) {
1827
+ const parsed = Date.parse(String(value.observedAt ?? ''));
1828
+ return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
1829
+ }
1830
+ function mergeSessionIntent(prior, incoming) {
1831
+ if (prior.status === 'selected' && incoming.status === 'selected') {
1832
+ const priorKey = [prior.sequence ?? Number.MAX_SAFE_INTEGER, prior.eventId ?? ''];
1833
+ const incomingKey = [incoming.sequence ?? Number.MAX_SAFE_INTEGER, incoming.eventId ?? ''];
1834
+ return priorKey[0] < incomingKey[0]
1835
+ || (priorKey[0] === incomingKey[0] && String(priorKey[1]) <= String(incomingKey[1]))
1836
+ ? prior
1837
+ : incoming;
1838
+ }
1839
+ if (prior.status === 'selected')
1840
+ return prior;
1841
+ if (incoming.status === 'selected')
1842
+ return incoming;
1843
+ if (prior.status === 'unknown' || incoming.status === 'unknown') {
1844
+ return {
1845
+ status: 'unknown',
1846
+ eventId: null,
1847
+ sequence: null,
1848
+ title: null,
1849
+ summary: null,
1850
+ };
1851
+ }
1852
+ return prior;
1853
+ }
1854
+ function safeEventCoordinates(eventData, fallback) {
1855
+ try {
1856
+ const event = JSON.parse(eventData);
1857
+ return {
1858
+ provider: fallback.provider,
1859
+ sourceId: event.sourceId,
1860
+ nativeSessionId: fallback.nativeSessionId,
1861
+ nativeEventId: event.nativeId ?? `sequence:${event.sequence}`,
1862
+ };
1863
+ }
1864
+ catch {
1865
+ return {
1866
+ provider: fallback.provider,
1867
+ sourceId: '<unreadable>',
1868
+ nativeSessionId: fallback.nativeSessionId,
1869
+ nativeEventId: '<unreadable>',
1870
+ };
1871
+ }
1872
+ }
1873
+ function formatEventCoordinates(input) {
1874
+ return [
1875
+ `provider=${input.provider}`,
1876
+ `source=${input.sourceId}`,
1877
+ `session=${input.nativeSessionId}`,
1878
+ `event=${input.nativeEventId}`,
1879
+ ].join(',');
1880
+ }
1881
+ function allowedCandidateTransition(from, to) {
1882
+ const transitions = {
1883
+ pending: new Set(['accepted', 'rejected', 'deferred']),
1884
+ deferred: new Set(['pending', 'accepted', 'rejected']),
1885
+ accepted: new Set(['superseded']),
1886
+ promoted: new Set(['superseded']),
1887
+ rejected: new Set(),
1888
+ superseded: new Set(),
1889
+ };
1890
+ return transitions[from].has(to);
1891
+ }
1892
+ //# sourceMappingURL=repository.js.map