@aiwg/cli 2026.7.20 → 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.
- package/README.md +4 -4
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/browser-export.js +7 -0
- package/dist/src/artifacts/citation-parser.js +96 -35
- package/dist/src/artifacts/index-builder.js +54 -17
- package/dist/src/artifacts/state-transfer.js +27 -0
- package/dist/src/artifacts/stats.js +8 -0
- package/dist/src/cli/cli-extension-loader.js +73 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/sessions.js +966 -0
- package/dist/src/cli/handlers/skill-lint.js +49 -45
- package/dist/src/cli/handlers/use.js +143 -60
- package/dist/src/cli/handlers/utilities.js +22 -8
- package/dist/src/cli/skill-usage.js +146 -24
- package/dist/src/extensions/commands/definitions.js +29 -0
- package/dist/src/extensions/manifest.js +29 -0
- package/dist/src/sessions/adapters/claude.js +357 -0
- package/dist/src/sessions/adapters/codex.js +521 -0
- package/dist/src/sessions/adapters/copilot.js +226 -0
- package/dist/src/sessions/adapters/cursor.js +372 -0
- package/dist/src/sessions/adapters/factory.js +345 -0
- package/dist/src/sessions/adapters/generic.js +225 -0
- package/dist/src/sessions/adapters/hermes.js +341 -0
- package/dist/src/sessions/adapters/openclaw.js +381 -0
- package/dist/src/sessions/adapters/opencode.js +454 -0
- package/dist/src/sessions/adapters/openhuman.js +315 -0
- package/dist/src/sessions/adapters/warp.js +160 -0
- package/dist/src/sessions/adapters/windsurf.js +212 -0
- package/dist/src/sessions/candidates.js +210 -0
- package/dist/src/sessions/contracts.js +310 -0
- package/dist/src/sessions/discovery.js +51 -0
- package/dist/src/sessions/fixtures.js +12 -0
- package/dist/src/sessions/importer.js +315 -0
- package/dist/src/sessions/index.js +25 -0
- package/dist/src/sessions/knowledge-shard.js +61 -0
- package/dist/src/sessions/optional-backends.js +238 -0
- package/dist/src/sessions/policy.js +192 -0
- package/dist/src/sessions/ports.js +2 -0
- package/dist/src/sessions/promotion.js +367 -0
- package/dist/src/sessions/readers.js +176 -0
- package/dist/src/sessions/repository.js +1551 -0
- package/dist/src/skills/adapters/agent-skills.js +59 -0
- package/dist/src/skills/adapters/local.js +19 -1
- package/dist/src/skills/agent-skills.js +249 -0
- package/dist/src/skills/cli.js +463 -7
- package/dist/src/skills/deployer.js +554 -0
- package/dist/src/skills/doctor.js +105 -0
- package/dist/src/skills/exporter.js +382 -0
- package/dist/src/skills/importer.js +921 -0
- package/dist/src/skills/registry.js +19 -0
- package/dist/src/skills/validator.js +323 -0
- package/package.json +2 -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
|