@aiwg/cli 2026.7.21 → 2026.7.24
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 +14 -3
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/browser-export.js +2 -0
- package/dist/src/artifacts/index-builder.js +44 -8
- package/dist/src/artifacts/query-engine.js +1 -1
- package/dist/src/artifacts/types.js +1 -0
- package/dist/src/cli/handlers/index.js +5 -1
- package/dist/src/cli/handlers/sessions.js +339 -40
- package/dist/src/cli/handlers/setup-manifest.js +800 -0
- package/dist/src/cli/handlers/use.js +127 -17
- package/dist/src/config/aiwg-config.js +18 -2
- package/dist/src/config/cli.js +16 -3
- package/dist/src/extensions/commands/definitions.js +99 -0
- package/dist/src/security/threat-assessment-config.js +296 -0
- package/dist/src/serve/sandbox-registry.js +34 -0
- package/dist/src/sessions/adapters/claude.js +37 -9
- package/dist/src/sessions/adapters/codex.js +38 -11
- package/dist/src/sessions/adapters/cursor.js +166 -10
- package/dist/src/sessions/adapters/factory.js +50 -9
- package/dist/src/sessions/batch-contracts.js +121 -0
- package/dist/src/sessions/batch-import.js +265 -0
- package/dist/src/sessions/contracts.js +32 -5
- package/dist/src/sessions/import-lease.js +152 -0
- package/dist/src/sessions/importer.js +163 -14
- package/dist/src/sessions/index.js +6 -0
- package/dist/src/sessions/origin.js +117 -0
- package/dist/src/sessions/readers.js +1 -1
- package/dist/src/sessions/repository.js +354 -13
- package/dist/src/sessions/timeline.js +148 -0
- package/dist/src/sessions/workspace-discovery.js +319 -0
- package/package.json +1 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import { CandidateReviewReceiptSchema, DeletionReceiptSchema, IntelligenceCandidateSchema, PromotionDependencyDecisionSchema, PromotionReceiptSchema, SessionContractError, sha256, } from './contracts.js';
|
|
3
|
+
import { coverageFromBatchRun, } from './batch-contracts.js';
|
|
3
4
|
import { redactSessionText, sanitizeNativeExtensions } from './policy.js';
|
|
4
5
|
const require = createRequire(import.meta.url);
|
|
6
|
+
const POLICY_PROVIDER_MIGRATION = 'policy-provider-identity:v2';
|
|
7
|
+
const EVENT_ORIGIN_INTENT_MIGRATION = 'event-origin-intent:v1';
|
|
5
8
|
export class SessionRepository {
|
|
6
9
|
db;
|
|
7
10
|
constructor(path = ':memory:') {
|
|
@@ -14,6 +17,7 @@ export class SessionRepository {
|
|
|
14
17
|
}
|
|
15
18
|
this.db.pragma('foreign_keys = ON');
|
|
16
19
|
this.db.pragma('journal_mode = WAL');
|
|
20
|
+
this.db.pragma('busy_timeout = 5000');
|
|
17
21
|
this.initialize();
|
|
18
22
|
}
|
|
19
23
|
initialize() {
|
|
@@ -24,6 +28,7 @@ export class SessionRepository {
|
|
|
24
28
|
CREATE TABLE IF NOT EXISTS import_runs (
|
|
25
29
|
import_run_id TEXT PRIMARY KEY, source_id TEXT NOT NULL, parser_version TEXT NOT NULL,
|
|
26
30
|
status TEXT NOT NULL, checkpoint TEXT NOT NULL, data TEXT NOT NULL,
|
|
31
|
+
batch_run_id TEXT,
|
|
27
32
|
FOREIGN KEY(source_id) REFERENCES session_sources(source_id)
|
|
28
33
|
);
|
|
29
34
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -45,6 +50,12 @@ export class SessionRepository {
|
|
|
45
50
|
outcome TEXT NOT NULL, counts TEXT NOT NULL, checkpoint TEXT NOT NULL,
|
|
46
51
|
FOREIGN KEY(import_run_id) REFERENCES import_runs(import_run_id)
|
|
47
52
|
);
|
|
53
|
+
CREATE TABLE IF NOT EXISTS batch_import_runs (
|
|
54
|
+
batch_run_id TEXT PRIMARY KEY, manifest_id TEXT NOT NULL,
|
|
55
|
+
workspace_id TEXT NOT NULL, status TEXT NOT NULL,
|
|
56
|
+
started_at TEXT NOT NULL, updated_at TEXT NOT NULL, data TEXT NOT NULL,
|
|
57
|
+
UNIQUE(manifest_id, workspace_id)
|
|
58
|
+
);
|
|
48
59
|
CREATE TABLE IF NOT EXISTS mutation_audit (
|
|
49
60
|
operation_id TEXT PRIMARY KEY, operation TEXT NOT NULL, actor TEXT NOT NULL,
|
|
50
61
|
counts TEXT NOT NULL, adapter_version TEXT NOT NULL, policy_version TEXT NOT NULL,
|
|
@@ -89,6 +100,9 @@ export class SessionRepository {
|
|
|
89
100
|
dependent_id TEXT NOT NULL, action TEXT NOT NULL, data TEXT NOT NULL,
|
|
90
101
|
UNIQUE(operation_id, dependent_id)
|
|
91
102
|
);
|
|
103
|
+
CREATE TABLE IF NOT EXISTS session_catalog_meta (
|
|
104
|
+
key TEXT PRIMARY KEY, value TEXT NOT NULL
|
|
105
|
+
);
|
|
92
106
|
CREATE INDEX IF NOT EXISTS idx_session_workspace_provider
|
|
93
107
|
ON sessions(workspace_id, source_id, lifecycle);
|
|
94
108
|
CREATE INDEX IF NOT EXISTS idx_event_session_sequence
|
|
@@ -111,12 +125,20 @@ export class SessionRepository {
|
|
|
111
125
|
if (!mutationColumns.includes('data')) {
|
|
112
126
|
this.db.exec('ALTER TABLE mutation_audit ADD COLUMN data TEXT');
|
|
113
127
|
}
|
|
128
|
+
const importRunColumns = this.db.prepare('PRAGMA table_info(import_runs)').all().map((row) => String(row.name));
|
|
129
|
+
if (!importRunColumns.includes('batch_run_id')) {
|
|
130
|
+
this.db.exec('ALTER TABLE import_runs ADD COLUMN batch_run_id TEXT');
|
|
131
|
+
}
|
|
114
132
|
this.db.exec(`
|
|
115
133
|
CREATE INDEX IF NOT EXISTS idx_mutation_audit_workspace
|
|
116
134
|
ON mutation_audit(workspace_id, occurred_at, operation_id)
|
|
135
|
+
;
|
|
136
|
+
CREATE INDEX IF NOT EXISTS idx_batch_import_workspace
|
|
137
|
+
ON batch_import_runs(workspace_id, updated_at, batch_run_id)
|
|
117
138
|
`);
|
|
118
139
|
this.alignSessionFtsRowids();
|
|
119
140
|
this.migrateSessionPolicyAndProviderIdentity();
|
|
141
|
+
this.migrateEventOriginAndIntent();
|
|
120
142
|
}
|
|
121
143
|
alignSessionFtsRowids() {
|
|
122
144
|
const eventCount = Number(this.db.prepare('SELECT COUNT(*) AS count FROM session_events').get()?.count ?? 0);
|
|
@@ -138,7 +160,7 @@ export class SessionRepository {
|
|
|
138
160
|
rebuild();
|
|
139
161
|
}
|
|
140
162
|
migrateSessionPolicyAndProviderIdentity() {
|
|
141
|
-
|
|
163
|
+
this.runCatalogMigration(POLICY_PROVIDER_MIGRATION, () => {
|
|
142
164
|
for (const row of this.db.prepare('SELECT source_id, provider, data FROM session_sources').all()) {
|
|
143
165
|
const source = JSON.parse(String(row.data));
|
|
144
166
|
const canonicalProvider = source.provider === 'windsurf'
|
|
@@ -153,7 +175,7 @@ export class SessionRepository {
|
|
|
153
175
|
const session = JSON.parse(String(row.data));
|
|
154
176
|
if (session.provider === 'windsurf')
|
|
155
177
|
session.provider = 'devin-desktop';
|
|
156
|
-
session.extensions =
|
|
178
|
+
session.extensions = sanitizeSessionExtensions(renameNativeWindsurfEnvelope(session.extensions), session.provider);
|
|
157
179
|
this.db.prepare('UPDATE sessions SET data=? WHERE session_id=?')
|
|
158
180
|
.run(JSON.stringify(session), String(row.session_id));
|
|
159
181
|
}
|
|
@@ -223,7 +245,52 @@ export class SessionRepository {
|
|
|
223
245
|
WHERE operation_id=?`).run(workspaceId, event.targetClass, JSON.stringify(event), event.operationId);
|
|
224
246
|
}
|
|
225
247
|
});
|
|
226
|
-
|
|
248
|
+
}
|
|
249
|
+
migrateEventOriginAndIntent() {
|
|
250
|
+
this.runCatalogMigration(EVENT_ORIGIN_INTENT_MIGRATION, () => {
|
|
251
|
+
for (const row of this.db.prepare('SELECT event_id, data FROM session_events').all()) {
|
|
252
|
+
const event = JSON.parse(String(row.data));
|
|
253
|
+
let changed = false;
|
|
254
|
+
if (typeof event.origin !== 'string') {
|
|
255
|
+
event.origin = 'unknown';
|
|
256
|
+
event.originRule = 'legacy:unknown';
|
|
257
|
+
event.originClassifierVersion = '1.0.0';
|
|
258
|
+
changed = true;
|
|
259
|
+
}
|
|
260
|
+
if (!Object.hasOwn(event, 'activityBoundary')) {
|
|
261
|
+
event.activityBoundary = null;
|
|
262
|
+
event.activityBoundaryBasis = null;
|
|
263
|
+
event.activityBoundaryConfidence = null;
|
|
264
|
+
changed = true;
|
|
265
|
+
}
|
|
266
|
+
if (changed) {
|
|
267
|
+
this.db.prepare('UPDATE session_events SET data=? WHERE event_id=?').run(JSON.stringify(event), String(row.event_id));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (const row of this.db.prepare('SELECT session_id, data FROM sessions').all()) {
|
|
271
|
+
const session = JSON.parse(String(row.data));
|
|
272
|
+
if (session.intent !== undefined)
|
|
273
|
+
continue;
|
|
274
|
+
session.intent = {
|
|
275
|
+
status: 'unknown',
|
|
276
|
+
eventId: null,
|
|
277
|
+
sequence: null,
|
|
278
|
+
title: null,
|
|
279
|
+
summary: null,
|
|
280
|
+
};
|
|
281
|
+
this.db.prepare('UPDATE sessions SET data=? WHERE session_id=?').run(JSON.stringify(session), String(row.session_id));
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
runCatalogMigration(migration, migrate) {
|
|
286
|
+
const applied = this.db.prepare('SELECT 1 AS applied FROM session_catalog_meta WHERE key=?').get(migration);
|
|
287
|
+
if (applied)
|
|
288
|
+
return;
|
|
289
|
+
const transaction = this.db.transaction(() => {
|
|
290
|
+
migrate();
|
|
291
|
+
this.db.prepare('INSERT INTO session_catalog_meta (key, value) VALUES (?, ?)').run(migration, 'applied');
|
|
292
|
+
});
|
|
293
|
+
transaction();
|
|
227
294
|
}
|
|
228
295
|
emitMutation(input) {
|
|
229
296
|
const unsigned = {
|
|
@@ -344,8 +411,9 @@ export class SessionRepository {
|
|
|
344
411
|
}
|
|
345
412
|
this.db.prepare(`INSERT INTO session_sources(source_id, provider, data) VALUES (?, ?, ?)
|
|
346
413
|
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(
|
|
348
|
-
|
|
414
|
+
this.db.prepare(`INSERT INTO import_runs(
|
|
415
|
+
import_run_id, source_id, parser_version, status, checkpoint, data, batch_run_id
|
|
416
|
+
) VALUES (?, ?, ?, 'running', ?, ?, ?)`).run(batch.run.importRunId, batch.source.sourceId, batch.run.parserVersion, JSON.stringify(checkpoint), JSON.stringify(batch.run), batch.batchRunId ?? null);
|
|
349
417
|
let sessionsInserted = 0;
|
|
350
418
|
let eventsInserted = 0;
|
|
351
419
|
for (const session of batch.sessions) {
|
|
@@ -372,7 +440,25 @@ export class SessionRepository {
|
|
|
372
440
|
const prior = this.db.prepare('SELECT digest FROM session_events WHERE event_id=?')
|
|
373
441
|
.get(event.eventId);
|
|
374
442
|
if (prior && String(prior.digest) !== event.digest) {
|
|
375
|
-
|
|
443
|
+
const priorEvent = this.db.prepare(`SELECT e.data AS event_data, src.provider AS provider, s.native_session_id AS native_session_id
|
|
444
|
+
FROM session_events e
|
|
445
|
+
JOIN sessions s ON s.session_id=e.session_id
|
|
446
|
+
JOIN session_sources src ON src.source_id=e.source_id
|
|
447
|
+
WHERE e.event_id=?`).get(event.eventId);
|
|
448
|
+
const priorCoordinates = priorEvent
|
|
449
|
+
? safeEventCoordinates(String(priorEvent.event_data), {
|
|
450
|
+
provider: String(priorEvent.provider),
|
|
451
|
+
nativeSessionId: String(priorEvent.native_session_id),
|
|
452
|
+
})
|
|
453
|
+
: { provider: '<unknown>', sourceId: '<unknown>', nativeSessionId: '<unknown>', nativeEventId: event.eventId };
|
|
454
|
+
const incomingCoordinates = {
|
|
455
|
+
provider: batch.source.provider,
|
|
456
|
+
sourceId: event.sourceId,
|
|
457
|
+
nativeSessionId: batch.sessions.find((session) => session.sessionId === event.sessionId)
|
|
458
|
+
?.nativeSessionId ?? event.sessionId,
|
|
459
|
+
nativeEventId: event.nativeId ?? `sequence:${event.sequence}`,
|
|
460
|
+
};
|
|
461
|
+
throw new SessionContractError('IMPORT_CONFLICT', `stable event identity changed content: ${event.eventId}; prior=${formatEventCoordinates(priorCoordinates)} incoming=${formatEventCoordinates(incomingCoordinates)}`);
|
|
376
462
|
}
|
|
377
463
|
const inserted = insertEvent.run(event.eventId, event.sessionId, event.sourceId, event.importRunId, event.sequence, event.digest, JSON.stringify(event)).changes;
|
|
378
464
|
eventsInserted += inserted;
|
|
@@ -443,6 +529,53 @@ export class SessionRepository {
|
|
|
443
529
|
});
|
|
444
530
|
return commit();
|
|
445
531
|
}
|
|
532
|
+
commitStagedBatch(batchRunId, sourceIds) {
|
|
533
|
+
if (sourceIds.length === 0)
|
|
534
|
+
return 0;
|
|
535
|
+
const placeholders = sourceIds.map(() => '?').join(', ');
|
|
536
|
+
const commit = this.db.transaction(() => {
|
|
537
|
+
const params = [batchRunId, ...sourceIds];
|
|
538
|
+
const staged = this.db.prepare(`SELECT import_run_id FROM import_runs
|
|
539
|
+
WHERE batch_run_id=? AND source_id IN (${placeholders}) AND status='staged'`).all(...params).map((row) => String(row.import_run_id));
|
|
540
|
+
const result = this.db.prepare(`UPDATE import_runs SET status='committed'
|
|
541
|
+
WHERE batch_run_id=? AND source_id IN (${placeholders}) AND status='staged'`).run(...params);
|
|
542
|
+
if (staged.length > 0) {
|
|
543
|
+
const runPlaceholders = staged.map(() => '?').join(', ');
|
|
544
|
+
this.db.prepare(`UPDATE import_receipts SET outcome='committed'
|
|
545
|
+
WHERE import_run_id IN (${runPlaceholders}) AND outcome='staged'`).run(...staged);
|
|
546
|
+
}
|
|
547
|
+
for (const operationId of staged) {
|
|
548
|
+
const row = this.db.prepare('SELECT data FROM mutation_audit WHERE operation_id=?').get(operationId);
|
|
549
|
+
if (!row?.data)
|
|
550
|
+
continue;
|
|
551
|
+
const prior = JSON.parse(String(row.data));
|
|
552
|
+
const { integrityDigest: _integrityDigest, ...unsigned } = {
|
|
553
|
+
...prior,
|
|
554
|
+
outcome: 'committed',
|
|
555
|
+
observedAt: new Date().toISOString(),
|
|
556
|
+
};
|
|
557
|
+
const updated = {
|
|
558
|
+
...unsigned,
|
|
559
|
+
integrityDigest: sha256(JSON.stringify(unsigned)),
|
|
560
|
+
};
|
|
561
|
+
this.db.prepare(`UPDATE mutation_audit SET outcome='committed', occurred_at=?, data=?
|
|
562
|
+
WHERE operation_id=?`).run(updated.observedAt, JSON.stringify(updated), operationId);
|
|
563
|
+
}
|
|
564
|
+
return result.changes;
|
|
565
|
+
});
|
|
566
|
+
return commit();
|
|
567
|
+
}
|
|
568
|
+
sourceImportTotals(sourceId, batchRunId) {
|
|
569
|
+
const events = Number(this.db.prepare(`SELECT COUNT(*) AS count
|
|
570
|
+
FROM session_events e
|
|
571
|
+
JOIN import_runs r ON r.import_run_id=e.import_run_id
|
|
572
|
+
WHERE e.source_id=? AND r.batch_run_id=?`).get(sourceId, batchRunId)?.count ?? 0);
|
|
573
|
+
const sessions = Number(this.db.prepare(`SELECT COUNT(DISTINCT e.session_id) AS count
|
|
574
|
+
FROM session_events e
|
|
575
|
+
JOIN import_runs r ON r.import_run_id=e.import_run_id
|
|
576
|
+
WHERE e.source_id=? AND r.batch_run_id=?`).get(sourceId, batchRunId)?.count ?? 0);
|
|
577
|
+
return { sessions, events };
|
|
578
|
+
}
|
|
446
579
|
getSession(id, workspaceId) {
|
|
447
580
|
const row = this.db.prepare(`SELECT s.data FROM sessions s
|
|
448
581
|
WHERE s.session_id=? AND s.lifecycle != ?
|
|
@@ -458,6 +591,42 @@ export class SessionRepository {
|
|
|
458
591
|
listSources() {
|
|
459
592
|
return this.db.prepare('SELECT data FROM session_sources ORDER BY provider, source_id').all().map((row) => JSON.parse(String(row.data)));
|
|
460
593
|
}
|
|
594
|
+
listWorkspaceIds() {
|
|
595
|
+
return this.db.prepare('SELECT DISTINCT workspace_id FROM sessions ORDER BY workspace_id').all().map((row) => String(row.workspace_id));
|
|
596
|
+
}
|
|
597
|
+
saveBatchImportRun(run) {
|
|
598
|
+
const save = this.db.transaction(() => {
|
|
599
|
+
const prior = this.db.prepare('SELECT manifest_id, workspace_id FROM batch_import_runs WHERE batch_run_id=?').get(run.runId);
|
|
600
|
+
if (prior && (String(prior.manifest_id) !== run.manifestId
|
|
601
|
+
|| String(prior.workspace_id) !== run.workspaceId)) {
|
|
602
|
+
throw new SessionContractError('IMPORT_CONFLICT', 'batch run identity changed manifest or workspace scope');
|
|
603
|
+
}
|
|
604
|
+
this.db.prepare(`INSERT INTO batch_import_runs(
|
|
605
|
+
batch_run_id, manifest_id, workspace_id, status, started_at, updated_at, data
|
|
606
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
607
|
+
ON CONFLICT(batch_run_id) DO UPDATE SET
|
|
608
|
+
status=excluded.status, updated_at=excluded.updated_at, data=excluded.data`).run(run.runId, run.manifestId, run.workspaceId, run.status, run.startedAt, run.updatedAt, JSON.stringify(run));
|
|
609
|
+
});
|
|
610
|
+
save();
|
|
611
|
+
}
|
|
612
|
+
getBatchImportRun(runId) {
|
|
613
|
+
const row = this.db.prepare('SELECT data FROM batch_import_runs WHERE batch_run_id=?').get(runId);
|
|
614
|
+
return row ? JSON.parse(String(row.data)) : null;
|
|
615
|
+
}
|
|
616
|
+
getBatchImportRunForManifest(manifestId, workspaceId) {
|
|
617
|
+
const row = this.db.prepare(`SELECT data FROM batch_import_runs
|
|
618
|
+
WHERE manifest_id=? AND workspace_id=?`).get(manifestId, workspaceId);
|
|
619
|
+
return row ? JSON.parse(String(row.data)) : null;
|
|
620
|
+
}
|
|
621
|
+
getCoverage(workspaceId, now = new Date()) {
|
|
622
|
+
const row = this.db.prepare(`SELECT data FROM batch_import_runs
|
|
623
|
+
WHERE workspace_id=?
|
|
624
|
+
ORDER BY updated_at DESC, batch_run_id DESC LIMIT 1`).get(workspaceId);
|
|
625
|
+
const coverage = coverageFromBatchRun(row ? JSON.parse(String(row.data)) : null, now);
|
|
626
|
+
return coverage.status === 'unknown'
|
|
627
|
+
? { ...coverage, workspaceId }
|
|
628
|
+
: coverage;
|
|
629
|
+
}
|
|
461
630
|
listSessions(options) {
|
|
462
631
|
const where = [`s.lifecycle != 'tombstoned'`, `EXISTS (
|
|
463
632
|
SELECT 1 FROM session_events e
|
|
@@ -554,6 +723,23 @@ export class SessionRepository {
|
|
|
554
723
|
ORDER BY e.sequence_no, e.event_id`).all(...(workspaceId ? [sessionId, workspaceId] : [sessionId]))
|
|
555
724
|
.map((row) => JSON.parse(String(row.data)));
|
|
556
725
|
}
|
|
726
|
+
listTimelineInputs(workspaceId) {
|
|
727
|
+
return this.db.prepare(`SELECT s.data AS session_data, e.data AS event_data
|
|
728
|
+
FROM session_events e
|
|
729
|
+
JOIN sessions s ON s.session_id=e.session_id
|
|
730
|
+
JOIN import_runs r ON r.import_run_id=e.import_run_id
|
|
731
|
+
WHERE s.workspace_id=? AND s.lifecycle!='tombstoned' AND r.status='committed'
|
|
732
|
+
ORDER BY
|
|
733
|
+
CASE WHEN json_extract(e.data, '$.occurredAt') IS NULL THEN 1 ELSE 0 END,
|
|
734
|
+
json_extract(e.data, '$.occurredAt'),
|
|
735
|
+
json_extract(s.data, '$.provider'),
|
|
736
|
+
e.session_id,
|
|
737
|
+
e.sequence_no,
|
|
738
|
+
e.event_id`).all(workspaceId).map((row) => ({
|
|
739
|
+
session: JSON.parse(String(row.session_data)),
|
|
740
|
+
event: JSON.parse(String(row.event_data)),
|
|
741
|
+
}));
|
|
742
|
+
}
|
|
557
743
|
search(options) {
|
|
558
744
|
const query = options.query.trim();
|
|
559
745
|
if (!query)
|
|
@@ -889,6 +1075,13 @@ export class SessionRepository {
|
|
|
889
1075
|
ORDER BY rowid DESC LIMIT 1`).get(sourceId, parserVersion);
|
|
890
1076
|
return row ? JSON.parse(String(row.checkpoint)) : null;
|
|
891
1077
|
}
|
|
1078
|
+
getBatchCheckpoint(sourceId, parserVersion, batchRunId) {
|
|
1079
|
+
const row = this.db.prepare(`SELECT checkpoint FROM import_runs
|
|
1080
|
+
WHERE source_id=? AND parser_version=? AND batch_run_id=?
|
|
1081
|
+
AND status IN ('staged', 'committed')
|
|
1082
|
+
ORDER BY rowid DESC LIMIT 1`).get(sourceId, parserVersion, batchRunId);
|
|
1083
|
+
return row ? JSON.parse(String(row.checkpoint)) : null;
|
|
1084
|
+
}
|
|
892
1085
|
relocateSource(sourceId, redactedLocator, workspaceId) {
|
|
893
1086
|
const relocate = this.db.transaction(() => {
|
|
894
1087
|
const row = this.db.prepare(`SELECT src.data FROM session_sources src
|
|
@@ -1269,6 +1462,7 @@ function searchHit(row) {
|
|
|
1269
1462
|
const citation = {
|
|
1270
1463
|
provider: session.provider,
|
|
1271
1464
|
sessionId: event.sessionId,
|
|
1465
|
+
origin: event.origin,
|
|
1272
1466
|
eventId: event.eventId,
|
|
1273
1467
|
importRunId: event.importRunId,
|
|
1274
1468
|
sourceId: event.sourceId,
|
|
@@ -1281,6 +1475,7 @@ function searchHit(row) {
|
|
|
1281
1475
|
provider: session.provider,
|
|
1282
1476
|
workspaceId: session.workspaceId,
|
|
1283
1477
|
sessionId: event.sessionId,
|
|
1478
|
+
origin: event.origin,
|
|
1284
1479
|
eventId: event.eventId,
|
|
1285
1480
|
importRunId: event.importRunId,
|
|
1286
1481
|
sourceId: event.sourceId,
|
|
@@ -1417,6 +1612,20 @@ function authorizedDocumentScopeDigest(options) {
|
|
|
1417
1612
|
return sha256(JSON.stringify(scope));
|
|
1418
1613
|
}
|
|
1419
1614
|
function appendMetadataFilters(where, params, options) {
|
|
1615
|
+
const controlEvents = options.controlEvents ?? 'exclude';
|
|
1616
|
+
const controlOrigins = [
|
|
1617
|
+
'provider-bootstrap',
|
|
1618
|
+
'workspace-instruction',
|
|
1619
|
+
'tool-control',
|
|
1620
|
+
];
|
|
1621
|
+
if (controlEvents === 'exclude') {
|
|
1622
|
+
where.push(`COALESCE(json_extract(e.data, '$.origin'), 'unknown') NOT IN (${controlOrigins.map(() => '?').join(',')})`);
|
|
1623
|
+
params.push(...controlOrigins);
|
|
1624
|
+
}
|
|
1625
|
+
else if (controlEvents === 'only') {
|
|
1626
|
+
where.push(`COALESCE(json_extract(e.data, '$.origin'), 'unknown') IN (${controlOrigins.map(() => '?').join(',')})`);
|
|
1627
|
+
params.push(...controlOrigins);
|
|
1628
|
+
}
|
|
1420
1629
|
if (options.sessionIds?.length) {
|
|
1421
1630
|
where.push(`e.session_id IN (${options.sessionIds.map(() => '?').join(',')})`);
|
|
1422
1631
|
params.push(...options.sessionIds);
|
|
@@ -1517,26 +1726,158 @@ function mergeSessionAggregate(prior, incoming) {
|
|
|
1517
1726
|
'consistent-snapshot': 1,
|
|
1518
1727
|
complete: 2,
|
|
1519
1728
|
};
|
|
1520
|
-
const
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
};
|
|
1729
|
+
const lifecycle = mergeLifecycleAggregate(prior, incoming);
|
|
1730
|
+
const nativeKey = `native.${prior.provider}`;
|
|
1731
|
+
const priorNative = objectRecord(prior.extensions[nativeKey]);
|
|
1732
|
+
const incomingNative = objectRecord(incoming.extensions[nativeKey]);
|
|
1525
1733
|
return {
|
|
1526
1734
|
...prior,
|
|
1527
1735
|
startedAt: timestamp(prior.startedAt, incoming.startedAt, 'min'),
|
|
1528
1736
|
updatedAt: timestamp(prior.updatedAt, incoming.updatedAt, 'max'),
|
|
1529
1737
|
consistency: consistencyRank[prior.consistency] >= consistencyRank[incoming.consistency]
|
|
1530
1738
|
? prior.consistency : incoming.consistency,
|
|
1531
|
-
lifecycle:
|
|
1532
|
-
|
|
1739
|
+
lifecycle: lifecycle.lifecycle,
|
|
1740
|
+
intent: mergeSessionIntent(prior.intent, incoming.intent),
|
|
1533
1741
|
sourceDigest: incoming.sourceDigest,
|
|
1534
1742
|
extensions: {
|
|
1535
1743
|
...prior.extensions,
|
|
1536
1744
|
...incoming.extensions,
|
|
1745
|
+
[nativeKey]: {
|
|
1746
|
+
...priorNative,
|
|
1747
|
+
...incomingNative,
|
|
1748
|
+
lifecycleEvidence: lifecycle.evidence,
|
|
1749
|
+
},
|
|
1537
1750
|
},
|
|
1538
1751
|
};
|
|
1539
1752
|
}
|
|
1753
|
+
function mergeLifecycleAggregate(prior, incoming) {
|
|
1754
|
+
const nativeKey = `native.${prior.provider}`;
|
|
1755
|
+
const priorEvidence = objectRecord(objectRecord(prior.extensions[nativeKey]).lifecycleEvidence);
|
|
1756
|
+
const incomingEvidence = objectRecord(objectRecord(incoming.extensions[nativeKey]).lifecycleEvidence);
|
|
1757
|
+
if (prior.lifecycle === 'tombstoned') {
|
|
1758
|
+
return { lifecycle: prior.lifecycle, evidence: priorEvidence };
|
|
1759
|
+
}
|
|
1760
|
+
const rank = {
|
|
1761
|
+
'open-provisional-source': 1,
|
|
1762
|
+
'inactivity-threshold': 1,
|
|
1763
|
+
'complete-source': 2,
|
|
1764
|
+
'provider-explicit-event': 3,
|
|
1765
|
+
};
|
|
1766
|
+
const priorRank = rank[String(priorEvidence.basis)] ?? 0;
|
|
1767
|
+
const incomingRank = rank[String(incomingEvidence.basis)] ?? 0;
|
|
1768
|
+
const incomingIsNewer = lifecycleEvidenceTimestamp(incomingEvidence)
|
|
1769
|
+
> lifecycleEvidenceTimestamp(priorEvidence);
|
|
1770
|
+
return incomingRank > priorRank || (incomingRank === priorRank && incomingIsNewer)
|
|
1771
|
+
? { lifecycle: incoming.lifecycle, evidence: incomingEvidence }
|
|
1772
|
+
: { lifecycle: prior.lifecycle, evidence: priorEvidence };
|
|
1773
|
+
}
|
|
1774
|
+
function objectRecord(value) {
|
|
1775
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
1776
|
+
? value
|
|
1777
|
+
: {};
|
|
1778
|
+
}
|
|
1779
|
+
function sanitizeSessionExtensions(extensions, provider) {
|
|
1780
|
+
const sanitized = sanitizeNativeExtensions(extensions).value;
|
|
1781
|
+
const nativeKey = `native.${provider}`;
|
|
1782
|
+
const evidence = safeLifecycleEvidence(objectRecord(objectRecord(extensions[nativeKey]).lifecycleEvidence));
|
|
1783
|
+
if (!evidence)
|
|
1784
|
+
return sanitized;
|
|
1785
|
+
sanitized[nativeKey] = {
|
|
1786
|
+
...objectRecord(sanitized[nativeKey]),
|
|
1787
|
+
lifecycleEvidence: evidence,
|
|
1788
|
+
};
|
|
1789
|
+
return sanitized;
|
|
1790
|
+
}
|
|
1791
|
+
function safeLifecycleEvidence(evidence) {
|
|
1792
|
+
const bases = new Set([
|
|
1793
|
+
'open-provisional-source',
|
|
1794
|
+
'inactivity-threshold',
|
|
1795
|
+
'complete-source',
|
|
1796
|
+
'provider-explicit-event',
|
|
1797
|
+
]);
|
|
1798
|
+
const states = new Set([
|
|
1799
|
+
'active', 'inactive', 'paused', 'complete', 'interrupted', 'archived',
|
|
1800
|
+
'unknown', 'tombstoned',
|
|
1801
|
+
]);
|
|
1802
|
+
const confidences = new Set(['high', 'medium', 'low', 'provisional']);
|
|
1803
|
+
const basis = String(evidence.basis ?? '');
|
|
1804
|
+
const state = String(evidence.state ?? '');
|
|
1805
|
+
const confidence = String(evidence.confidence ?? '');
|
|
1806
|
+
const observedAt = String(evidence.observedAt ?? '');
|
|
1807
|
+
if (!bases.has(basis)
|
|
1808
|
+
|| !states.has(state)
|
|
1809
|
+
|| !confidences.has(confidence)
|
|
1810
|
+
|| !Number.isFinite(Date.parse(observedAt))) {
|
|
1811
|
+
return null;
|
|
1812
|
+
}
|
|
1813
|
+
const output = {
|
|
1814
|
+
basis,
|
|
1815
|
+
state,
|
|
1816
|
+
observedAt,
|
|
1817
|
+
confidence,
|
|
1818
|
+
};
|
|
1819
|
+
if (typeof evidence.thresholdMs === 'number'
|
|
1820
|
+
&& Number.isFinite(evidence.thresholdMs)
|
|
1821
|
+
&& evidence.thresholdMs >= 0) {
|
|
1822
|
+
output.thresholdMs = evidence.thresholdMs;
|
|
1823
|
+
}
|
|
1824
|
+
return output;
|
|
1825
|
+
}
|
|
1826
|
+
function lifecycleEvidenceTimestamp(value) {
|
|
1827
|
+
const parsed = Date.parse(String(value.observedAt ?? ''));
|
|
1828
|
+
return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
|
|
1829
|
+
}
|
|
1830
|
+
function mergeSessionIntent(prior, incoming) {
|
|
1831
|
+
if (prior.status === 'selected' && incoming.status === 'selected') {
|
|
1832
|
+
const priorKey = [prior.sequence ?? Number.MAX_SAFE_INTEGER, prior.eventId ?? ''];
|
|
1833
|
+
const incomingKey = [incoming.sequence ?? Number.MAX_SAFE_INTEGER, incoming.eventId ?? ''];
|
|
1834
|
+
return priorKey[0] < incomingKey[0]
|
|
1835
|
+
|| (priorKey[0] === incomingKey[0] && String(priorKey[1]) <= String(incomingKey[1]))
|
|
1836
|
+
? prior
|
|
1837
|
+
: incoming;
|
|
1838
|
+
}
|
|
1839
|
+
if (prior.status === 'selected')
|
|
1840
|
+
return prior;
|
|
1841
|
+
if (incoming.status === 'selected')
|
|
1842
|
+
return incoming;
|
|
1843
|
+
if (prior.status === 'unknown' || incoming.status === 'unknown') {
|
|
1844
|
+
return {
|
|
1845
|
+
status: 'unknown',
|
|
1846
|
+
eventId: null,
|
|
1847
|
+
sequence: null,
|
|
1848
|
+
title: null,
|
|
1849
|
+
summary: null,
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
return prior;
|
|
1853
|
+
}
|
|
1854
|
+
function safeEventCoordinates(eventData, fallback) {
|
|
1855
|
+
try {
|
|
1856
|
+
const event = JSON.parse(eventData);
|
|
1857
|
+
return {
|
|
1858
|
+
provider: fallback.provider,
|
|
1859
|
+
sourceId: event.sourceId,
|
|
1860
|
+
nativeSessionId: fallback.nativeSessionId,
|
|
1861
|
+
nativeEventId: event.nativeId ?? `sequence:${event.sequence}`,
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
catch {
|
|
1865
|
+
return {
|
|
1866
|
+
provider: fallback.provider,
|
|
1867
|
+
sourceId: '<unreadable>',
|
|
1868
|
+
nativeSessionId: fallback.nativeSessionId,
|
|
1869
|
+
nativeEventId: '<unreadable>',
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
function formatEventCoordinates(input) {
|
|
1874
|
+
return [
|
|
1875
|
+
`provider=${input.provider}`,
|
|
1876
|
+
`source=${input.sourceId}`,
|
|
1877
|
+
`session=${input.nativeSessionId}`,
|
|
1878
|
+
`event=${input.nativeEventId}`,
|
|
1879
|
+
].join(',');
|
|
1880
|
+
}
|
|
1540
1881
|
function allowedCandidateTransition(from, to) {
|
|
1541
1882
|
const transitions = {
|
|
1542
1883
|
pending: new Set(['accepted', 'rejected', 'deferred']),
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export const TIMELINE_SCHEMA_VERSION = '1.0.0';
|
|
2
|
+
export const DEFAULT_TIMELINE_GAP_MS = 30 * 60 * 1_000;
|
|
3
|
+
export function deriveSessionTimeline(input, gapMs = DEFAULT_TIMELINE_GAP_MS) {
|
|
4
|
+
if (!Number.isFinite(gapMs) || gapMs < 0) {
|
|
5
|
+
throw new Error('timeline gap must be a non-negative duration');
|
|
6
|
+
}
|
|
7
|
+
const grouped = new Map();
|
|
8
|
+
for (const item of input) {
|
|
9
|
+
const group = grouped.get(item.session.sessionId) ?? [];
|
|
10
|
+
group.push(item);
|
|
11
|
+
grouped.set(item.session.sessionId, group);
|
|
12
|
+
}
|
|
13
|
+
const result = [];
|
|
14
|
+
for (const items of grouped.values()) {
|
|
15
|
+
const timed = items
|
|
16
|
+
.filter((item) => item.event.occurredAt !== null)
|
|
17
|
+
.sort(compareTimelineInput);
|
|
18
|
+
const untimed = items
|
|
19
|
+
.filter((item) => item.event.occurredAt === null)
|
|
20
|
+
.sort(compareSequence);
|
|
21
|
+
if (timed.length === 0) {
|
|
22
|
+
const first = items[0];
|
|
23
|
+
result.push({
|
|
24
|
+
schemaVersion: TIMELINE_SCHEMA_VERSION,
|
|
25
|
+
provider: first.session.provider,
|
|
26
|
+
sessionId: first.session.sessionId,
|
|
27
|
+
segmentIndex: 0,
|
|
28
|
+
startAt: null,
|
|
29
|
+
endAt: null,
|
|
30
|
+
durationMs: null,
|
|
31
|
+
eventCount: untimed.length,
|
|
32
|
+
boundaryBasis: 'unknown-time',
|
|
33
|
+
boundaryEvidence: null,
|
|
34
|
+
confidence: 'low',
|
|
35
|
+
minSequence: untimed.at(0)?.event.sequence ?? 0,
|
|
36
|
+
maxSequence: untimed.at(-1)?.event.sequence ?? 0,
|
|
37
|
+
});
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const sessionSegments = [];
|
|
41
|
+
let current = null;
|
|
42
|
+
let previous = null;
|
|
43
|
+
for (const item of timed) {
|
|
44
|
+
const boundary = previous ? segmentBoundary(previous.event, item.event, gapMs) : null;
|
|
45
|
+
if (!current || boundary) {
|
|
46
|
+
current = {
|
|
47
|
+
schemaVersion: TIMELINE_SCHEMA_VERSION,
|
|
48
|
+
provider: item.session.provider,
|
|
49
|
+
sessionId: item.session.sessionId,
|
|
50
|
+
segmentIndex: sessionSegments.length,
|
|
51
|
+
startAt: item.event.occurredAt,
|
|
52
|
+
endAt: item.event.occurredAt,
|
|
53
|
+
durationMs: 0,
|
|
54
|
+
eventCount: 0,
|
|
55
|
+
boundaryBasis: boundary?.basis ?? 'session-start',
|
|
56
|
+
boundaryEvidence: boundary?.evidence ?? null,
|
|
57
|
+
confidence: boundary?.confidence ?? 'high',
|
|
58
|
+
minSequence: item.event.sequence,
|
|
59
|
+
maxSequence: item.event.sequence,
|
|
60
|
+
};
|
|
61
|
+
sessionSegments.push(current);
|
|
62
|
+
}
|
|
63
|
+
current.eventCount += 1;
|
|
64
|
+
current.endAt = item.event.occurredAt;
|
|
65
|
+
current.durationMs = Math.max(0, Date.parse(current.endAt) - Date.parse(current.startAt));
|
|
66
|
+
current.minSequence = Math.min(current.minSequence, item.event.sequence);
|
|
67
|
+
current.maxSequence = Math.max(current.maxSequence, item.event.sequence);
|
|
68
|
+
previous = item;
|
|
69
|
+
}
|
|
70
|
+
for (const item of untimed) {
|
|
71
|
+
const destination = sessionSegments.find((segment) => item.event.sequence <= segment.maxSequence) ?? sessionSegments.at(-1);
|
|
72
|
+
destination.eventCount += 1;
|
|
73
|
+
destination.minSequence = Math.min(destination.minSequence, item.event.sequence);
|
|
74
|
+
destination.maxSequence = Math.max(destination.maxSequence, item.event.sequence);
|
|
75
|
+
}
|
|
76
|
+
result.push(...sessionSegments);
|
|
77
|
+
}
|
|
78
|
+
return result
|
|
79
|
+
.sort((left, right) => compareNullableTime(left.startAt, right.startAt)
|
|
80
|
+
|| left.provider.localeCompare(right.provider)
|
|
81
|
+
|| left.sessionId.localeCompare(right.sessionId)
|
|
82
|
+
|| left.segmentIndex - right.segmentIndex)
|
|
83
|
+
.map(({ minSequence: _min, maxSequence: _max, ...segment }) => segment);
|
|
84
|
+
}
|
|
85
|
+
export function parseTimelineGap(value) {
|
|
86
|
+
if (value === undefined)
|
|
87
|
+
return DEFAULT_TIMELINE_GAP_MS;
|
|
88
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/.exec(value.trim());
|
|
89
|
+
if (!match)
|
|
90
|
+
throw new Error('timeline gap must use ms, s, m, h, or d (for example 30m)');
|
|
91
|
+
const factors = {
|
|
92
|
+
ms: 1,
|
|
93
|
+
s: 1_000,
|
|
94
|
+
m: 60_000,
|
|
95
|
+
h: 3_600_000,
|
|
96
|
+
d: 86_400_000,
|
|
97
|
+
};
|
|
98
|
+
const result = Number(match[1]) * factors[match[2]];
|
|
99
|
+
if (!Number.isSafeInteger(result) || result < 0) {
|
|
100
|
+
throw new Error('timeline gap is outside the supported duration range');
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
function segmentBoundary(previous, current, gapMs) {
|
|
105
|
+
if (current.activityBoundary === 'resume'
|
|
106
|
+
|| current.activityBoundary === 'continuation') {
|
|
107
|
+
return {
|
|
108
|
+
basis: 'provider-explicit',
|
|
109
|
+
evidence: current.activityBoundaryBasis ?? current.activityBoundary,
|
|
110
|
+
confidence: current.activityBoundaryConfidence ?? 'high',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (previous.activityBoundary === 'pause' || previous.activityBoundary === 'end') {
|
|
114
|
+
return {
|
|
115
|
+
basis: 'provider-explicit',
|
|
116
|
+
evidence: previous.activityBoundaryBasis ?? previous.activityBoundary,
|
|
117
|
+
confidence: previous.activityBoundaryConfidence ?? 'high',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const gap = Date.parse(current.occurredAt) - Date.parse(previous.occurredAt);
|
|
121
|
+
if (gap > gapMs) {
|
|
122
|
+
return {
|
|
123
|
+
basis: 'inferred-gap',
|
|
124
|
+
evidence: `inactivity>${gapMs}ms`,
|
|
125
|
+
confidence: 'medium',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
function compareTimelineInput(left, right) {
|
|
131
|
+
return Date.parse(left.event.occurredAt) - Date.parse(right.event.occurredAt)
|
|
132
|
+
|| left.event.sequence - right.event.sequence
|
|
133
|
+
|| left.event.eventId.localeCompare(right.event.eventId);
|
|
134
|
+
}
|
|
135
|
+
function compareSequence(left, right) {
|
|
136
|
+
return left.event.sequence - right.event.sequence
|
|
137
|
+
|| left.event.eventId.localeCompare(right.event.eventId);
|
|
138
|
+
}
|
|
139
|
+
function compareNullableTime(left, right) {
|
|
140
|
+
if (left === right)
|
|
141
|
+
return 0;
|
|
142
|
+
if (left === null)
|
|
143
|
+
return 1;
|
|
144
|
+
if (right === null)
|
|
145
|
+
return -1;
|
|
146
|
+
return Date.parse(left) - Date.parse(right);
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=timeline.js.map
|