@aiwg/cli 2026.7.19 → 2026.7.21

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