opencode-lcm 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -3
- package/dist/store.d.ts +20 -4
- package/dist/store.js +456 -314
- package/package.json +1 -1
- package/src/store.ts +556 -360
package/dist/store.js
CHANGED
|
@@ -167,6 +167,14 @@ function getDeferredPartUpdateKey(event) {
|
|
|
167
167
|
function compareMessages(a, b) {
|
|
168
168
|
return a.info.time.created - b.info.time.created;
|
|
169
169
|
}
|
|
170
|
+
function emptySession(sessionID) {
|
|
171
|
+
return {
|
|
172
|
+
sessionID,
|
|
173
|
+
updatedAt: 0,
|
|
174
|
+
eventCount: 0,
|
|
175
|
+
messages: [],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
170
178
|
function buildSummaryNodeID(sessionID, level, slot) {
|
|
171
179
|
return `sum_${hashContent(`summary:${sessionID}`).slice(0, 12)}_l${level}_p${slot}`;
|
|
172
180
|
}
|
|
@@ -328,6 +336,11 @@ function logStartupPhase(phase, context) {
|
|
|
328
336
|
return;
|
|
329
337
|
getLogger().info(`startup phase: ${phase}`, context);
|
|
330
338
|
}
|
|
339
|
+
function unrefTimer(timer) {
|
|
340
|
+
if (typeof timer === 'object' && timer && 'unref' in timer && typeof timer.unref === 'function') {
|
|
341
|
+
timer.unref();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
331
344
|
function normalizeSqliteRuntimeOverride(value) {
|
|
332
345
|
const normalized = value?.trim().toLowerCase();
|
|
333
346
|
if (normalized === 'bun' || normalized === 'node')
|
|
@@ -347,6 +360,15 @@ export function resolveSqliteRuntimeCandidates(options) {
|
|
|
347
360
|
export function resolveSqliteRuntime(options) {
|
|
348
361
|
return resolveSqliteRuntimeCandidates(options)[0];
|
|
349
362
|
}
|
|
363
|
+
export function resolveCaptureHydrationMode(options) {
|
|
364
|
+
const isBunRuntime = options?.isBunRuntime ?? (typeof globalThis === 'object' && 'Bun' in globalThis);
|
|
365
|
+
const platform = options?.platform ?? process.platform;
|
|
366
|
+
// The targeted fresh-tail capture path is safe under Node, but the bundled
|
|
367
|
+
// Bun runtime on Windows has been the only environment where users have
|
|
368
|
+
// reported native crashes in this hot path. Keep the older full-session
|
|
369
|
+
// hydration there until Bun/Windows is proven stable again.
|
|
370
|
+
return isBunRuntime && platform === 'win32' ? 'full' : 'targeted';
|
|
371
|
+
}
|
|
350
372
|
function isSqliteRuntimeImportError(runtime, error) {
|
|
351
373
|
const code = typeof error === 'object' && error && 'code' in error && typeof error.code === 'string'
|
|
352
374
|
? error.code
|
|
@@ -446,6 +468,10 @@ export class SqliteLcmStore {
|
|
|
446
468
|
workspaceDirectory;
|
|
447
469
|
db;
|
|
448
470
|
dbReadyPromise;
|
|
471
|
+
deferredInitTimer;
|
|
472
|
+
deferredInitPromise;
|
|
473
|
+
deferredInitRequested = false;
|
|
474
|
+
activeOperationCount = 0;
|
|
449
475
|
pendingPartUpdates = new Map();
|
|
450
476
|
pendingPartUpdateTimer;
|
|
451
477
|
pendingPartUpdateFlushPromise;
|
|
@@ -459,8 +485,27 @@ export class SqliteLcmStore {
|
|
|
459
485
|
async init() {
|
|
460
486
|
await mkdir(this.baseDir, { recursive: true });
|
|
461
487
|
}
|
|
488
|
+
// Keep deferred SQLite maintenance off the active connection while a store operation is running.
|
|
489
|
+
async withStoreActivity(operation) {
|
|
490
|
+
this.activeOperationCount += 1;
|
|
491
|
+
try {
|
|
492
|
+
return await operation();
|
|
493
|
+
}
|
|
494
|
+
finally {
|
|
495
|
+
this.activeOperationCount -= 1;
|
|
496
|
+
if (this.activeOperationCount === 0 && this.deferredInitRequested) {
|
|
497
|
+
this.scheduleDeferredInit();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async waitForDeferredInitIfRunning() {
|
|
502
|
+
if (!this.deferredInitPromise)
|
|
503
|
+
return;
|
|
504
|
+
await this.deferredInitPromise;
|
|
505
|
+
}
|
|
462
506
|
async prepareForRead() {
|
|
463
507
|
await this.ensureDbReady();
|
|
508
|
+
await this.waitForDeferredInitIfRunning();
|
|
464
509
|
await this.flushDeferredPartUpdates();
|
|
465
510
|
}
|
|
466
511
|
scheduleDeferredPartUpdateFlush() {
|
|
@@ -470,12 +515,7 @@ export class SqliteLcmStore {
|
|
|
470
515
|
this.pendingPartUpdateTimer = undefined;
|
|
471
516
|
void this.flushDeferredPartUpdates();
|
|
472
517
|
}, SqliteLcmStore.deferredPartUpdateDelayMs);
|
|
473
|
-
|
|
474
|
-
this.pendingPartUpdateTimer &&
|
|
475
|
-
'unref' in this.pendingPartUpdateTimer &&
|
|
476
|
-
typeof this.pendingPartUpdateTimer.unref === 'function') {
|
|
477
|
-
this.pendingPartUpdateTimer.unref();
|
|
478
|
-
}
|
|
518
|
+
unrefTimer(this.pendingPartUpdateTimer);
|
|
479
519
|
}
|
|
480
520
|
clearDeferredPartUpdateTimer() {
|
|
481
521
|
if (!this.pendingPartUpdateTimer)
|
|
@@ -519,57 +559,64 @@ export class SqliteLcmStore {
|
|
|
519
559
|
this.clearDeferredPartUpdateTimer();
|
|
520
560
|
}
|
|
521
561
|
async captureDeferred(event) {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
562
|
+
return this.withStoreActivity(async () => {
|
|
563
|
+
switch (event.type) {
|
|
564
|
+
case 'message.part.updated': {
|
|
565
|
+
const key = getDeferredPartUpdateKey(event);
|
|
566
|
+
if (!key)
|
|
567
|
+
return await this.capture(event);
|
|
568
|
+
this.pendingPartUpdates.set(key, event);
|
|
569
|
+
this.scheduleDeferredPartUpdateFlush();
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
case 'message.part.removed':
|
|
573
|
+
this.clearDeferredPartUpdateForPart(event.properties.sessionID, event.properties.messageID, event.properties.partID);
|
|
574
|
+
break;
|
|
575
|
+
case 'message.removed':
|
|
576
|
+
this.clearDeferredPartUpdatesForMessage(event.properties.sessionID, event.properties.messageID);
|
|
577
|
+
break;
|
|
578
|
+
case 'session.deleted':
|
|
579
|
+
this.clearDeferredPartUpdatesForSession(extractSessionID(event));
|
|
580
|
+
break;
|
|
581
|
+
default:
|
|
582
|
+
break;
|
|
530
583
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
break;
|
|
534
|
-
case 'message.removed':
|
|
535
|
-
this.clearDeferredPartUpdatesForMessage(event.properties.sessionID, event.properties.messageID);
|
|
536
|
-
break;
|
|
537
|
-
case 'session.deleted':
|
|
538
|
-
this.clearDeferredPartUpdatesForSession(extractSessionID(event));
|
|
539
|
-
break;
|
|
540
|
-
default:
|
|
541
|
-
break;
|
|
542
|
-
}
|
|
543
|
-
await this.capture(event);
|
|
584
|
+
await this.capture(event);
|
|
585
|
+
});
|
|
544
586
|
}
|
|
545
587
|
async flushDeferredPartUpdates() {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
588
|
+
return this.withStoreActivity(async () => {
|
|
589
|
+
if (this.pendingPartUpdateFlushPromise)
|
|
590
|
+
return this.pendingPartUpdateFlushPromise;
|
|
591
|
+
if (this.pendingPartUpdates.size === 0)
|
|
592
|
+
return;
|
|
593
|
+
this.clearDeferredPartUpdateTimer();
|
|
594
|
+
this.pendingPartUpdateFlushPromise = (async () => {
|
|
595
|
+
while (this.pendingPartUpdates.size > 0) {
|
|
596
|
+
const batch = [...this.pendingPartUpdates.values()];
|
|
597
|
+
this.pendingPartUpdates.clear();
|
|
598
|
+
for (const event of batch) {
|
|
599
|
+
await this.capture(event);
|
|
600
|
+
}
|
|
557
601
|
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
602
|
+
})().finally(() => {
|
|
603
|
+
this.pendingPartUpdateFlushPromise = undefined;
|
|
604
|
+
if (this.pendingPartUpdates.size > 0)
|
|
605
|
+
this.scheduleDeferredPartUpdateFlush();
|
|
606
|
+
});
|
|
607
|
+
return this.pendingPartUpdateFlushPromise;
|
|
563
608
|
});
|
|
564
|
-
return this.pendingPartUpdateFlushPromise;
|
|
565
609
|
}
|
|
566
610
|
async ensureDbReady() {
|
|
567
|
-
if (this.
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
611
|
+
if (!this.dbReadyPromise) {
|
|
612
|
+
if (this.db) {
|
|
613
|
+
this.scheduleDeferredInit();
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
this.dbReadyPromise = this.openAndInitializeDb();
|
|
617
|
+
}
|
|
618
|
+
await this.dbReadyPromise;
|
|
619
|
+
this.scheduleDeferredInit();
|
|
573
620
|
}
|
|
574
621
|
async openAndInitializeDb() {
|
|
575
622
|
logStartupPhase('open-db:start', { dbPath: this.dbPath });
|
|
@@ -738,8 +785,6 @@ export class SqliteLcmStore {
|
|
|
738
785
|
await this.migrateLegacyArtifacts();
|
|
739
786
|
logStartupPhase('open-db:write-schema-version', { schemaVersion: STORE_SCHEMA_VERSION });
|
|
740
787
|
this.writeSchemaVersionSync(STORE_SCHEMA_VERSION);
|
|
741
|
-
logStartupPhase('open-db:deferred-init:start');
|
|
742
|
-
this.completeDeferredInit();
|
|
743
788
|
logStartupPhase('open-db:ready');
|
|
744
789
|
}
|
|
745
790
|
catch (error) {
|
|
@@ -753,6 +798,54 @@ export class SqliteLcmStore {
|
|
|
753
798
|
}
|
|
754
799
|
}
|
|
755
800
|
deferredInitCompleted = false;
|
|
801
|
+
runDeferredInit() {
|
|
802
|
+
if (this.deferredInitCompleted)
|
|
803
|
+
return Promise.resolve();
|
|
804
|
+
if (this.deferredInitPromise)
|
|
805
|
+
return this.deferredInitPromise;
|
|
806
|
+
this.deferredInitPromise = this.withStoreActivity(async () => {
|
|
807
|
+
this.deferredInitRequested = false;
|
|
808
|
+
logStartupPhase('deferred-init:start');
|
|
809
|
+
this.completeDeferredInit();
|
|
810
|
+
})
|
|
811
|
+
.catch((error) => {
|
|
812
|
+
getLogger().warn('Deferred LCM maintenance failed', {
|
|
813
|
+
message: error instanceof Error ? error.message : String(error),
|
|
814
|
+
});
|
|
815
|
+
})
|
|
816
|
+
.finally(() => {
|
|
817
|
+
this.deferredInitPromise = undefined;
|
|
818
|
+
});
|
|
819
|
+
return this.deferredInitPromise;
|
|
820
|
+
}
|
|
821
|
+
scheduleDeferredInit() {
|
|
822
|
+
if (!this.db || this.deferredInitCompleted || this.deferredInitPromise) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
this.deferredInitRequested = true;
|
|
826
|
+
if (this.activeOperationCount > 0 || this.deferredInitTimer)
|
|
827
|
+
return;
|
|
828
|
+
logStartupPhase('deferred-init:scheduled');
|
|
829
|
+
this.deferredInitTimer = setTimeout(() => {
|
|
830
|
+
this.deferredInitTimer = undefined;
|
|
831
|
+
if (this.activeOperationCount > 0) {
|
|
832
|
+
this.scheduleDeferredInit();
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
void this.runDeferredInit();
|
|
836
|
+
}, 0);
|
|
837
|
+
unrefTimer(this.deferredInitTimer);
|
|
838
|
+
}
|
|
839
|
+
async ensureDeferredInitComplete() {
|
|
840
|
+
await this.ensureDbReady();
|
|
841
|
+
if (this.deferredInitCompleted)
|
|
842
|
+
return;
|
|
843
|
+
if (this.deferredInitTimer) {
|
|
844
|
+
clearTimeout(this.deferredInitTimer);
|
|
845
|
+
this.deferredInitTimer = undefined;
|
|
846
|
+
}
|
|
847
|
+
await this.runDeferredInit();
|
|
848
|
+
}
|
|
756
849
|
readSchemaVersionSync() {
|
|
757
850
|
return firstFiniteNumber(this.getDb().prepare('PRAGMA user_version').get()) ?? 0;
|
|
758
851
|
}
|
|
@@ -802,6 +895,10 @@ export class SqliteLcmStore {
|
|
|
802
895
|
close() {
|
|
803
896
|
this.clearDeferredPartUpdateTimer();
|
|
804
897
|
this.pendingPartUpdates.clear();
|
|
898
|
+
if (this.deferredInitTimer) {
|
|
899
|
+
clearTimeout(this.deferredInitTimer);
|
|
900
|
+
this.deferredInitTimer = undefined;
|
|
901
|
+
}
|
|
805
902
|
if (!this.db)
|
|
806
903
|
return;
|
|
807
904
|
this.db.close();
|
|
@@ -809,51 +906,56 @@ export class SqliteLcmStore {
|
|
|
809
906
|
this.dbReadyPromise = undefined;
|
|
810
907
|
}
|
|
811
908
|
async capture(event) {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
this.
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const session = this.readSessionSync(normalized.sessionID, {
|
|
824
|
-
artifactMessageIDs: this.captureArtifactHydrationMessageIDs(normalized),
|
|
825
|
-
});
|
|
826
|
-
const previousParentSessionID = session.parentSessionID;
|
|
827
|
-
let next = this.applyEvent(session, normalized);
|
|
828
|
-
next.updatedAt = Math.max(next.updatedAt, normalized.timestamp);
|
|
829
|
-
next.eventCount += 1;
|
|
830
|
-
next = this.prepareSessionForPersistence(next);
|
|
831
|
-
const shouldSyncDerivedState = this.shouldSyncDerivedSessionStateForEvent(session, next, normalized);
|
|
832
|
-
await this.persistCapturedSession(next, normalized);
|
|
833
|
-
if (this.shouldRefreshLineageForEvent(normalized.type)) {
|
|
834
|
-
this.refreshAllLineageSync();
|
|
835
|
-
const refreshed = this.readSessionHeaderSync(normalized.sessionID);
|
|
836
|
-
if (refreshed) {
|
|
837
|
-
next = {
|
|
838
|
-
...next,
|
|
839
|
-
parentSessionID: refreshed.parentSessionID,
|
|
840
|
-
rootSessionID: refreshed.rootSessionID,
|
|
841
|
-
lineageDepth: refreshed.lineageDepth,
|
|
842
|
-
};
|
|
909
|
+
return this.withStoreActivity(async () => {
|
|
910
|
+
const normalized = normalizeEvent(event);
|
|
911
|
+
if (!normalized)
|
|
912
|
+
return;
|
|
913
|
+
const shouldRecord = this.shouldRecordEvent(normalized.type);
|
|
914
|
+
const shouldPersistSession = Boolean(normalized.sessionID) && this.shouldPersistSessionForEvent(normalized.type);
|
|
915
|
+
if (!shouldRecord && !shouldPersistSession)
|
|
916
|
+
return;
|
|
917
|
+
await this.ensureDeferredInitComplete();
|
|
918
|
+
if (shouldRecord) {
|
|
919
|
+
this.writeEvent(normalized);
|
|
843
920
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
921
|
+
if (!normalized.sessionID || !shouldPersistSession)
|
|
922
|
+
return;
|
|
923
|
+
const session = resolveCaptureHydrationMode() === 'targeted'
|
|
924
|
+
? this.readSessionForCaptureSync(normalized)
|
|
925
|
+
: this.readSessionSync(normalized.sessionID);
|
|
926
|
+
const previousParentSessionID = session.parentSessionID;
|
|
927
|
+
const shouldSyncDerivedState = this.shouldSyncDerivedSessionStateForEvent(session, normalized);
|
|
928
|
+
let next = this.applyEvent(session, normalized);
|
|
929
|
+
next.updatedAt = Math.max(next.updatedAt, normalized.timestamp);
|
|
930
|
+
next.eventCount += 1;
|
|
931
|
+
next = this.prepareSessionForPersistence(next);
|
|
932
|
+
await this.persistCapturedSession(next, normalized);
|
|
933
|
+
if (this.shouldRefreshLineageForEvent(normalized.type)) {
|
|
934
|
+
this.refreshAllLineageSync();
|
|
935
|
+
const refreshed = this.readSessionHeaderSync(normalized.sessionID);
|
|
936
|
+
if (refreshed) {
|
|
937
|
+
next = {
|
|
938
|
+
...next,
|
|
939
|
+
parentSessionID: refreshed.parentSessionID,
|
|
940
|
+
rootSessionID: refreshed.rootSessionID,
|
|
941
|
+
lineageDepth: refreshed.lineageDepth,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (shouldSyncDerivedState) {
|
|
946
|
+
this.syncDerivedSessionStateSync(this.readSessionSync(normalized.sessionID));
|
|
947
|
+
}
|
|
948
|
+
if (this.shouldSyncDerivedLineageSubtree(normalized.type, previousParentSessionID, next.parentSessionID)) {
|
|
949
|
+
this.syncDerivedLineageSubtreeSync(normalized.sessionID, true);
|
|
950
|
+
}
|
|
951
|
+
if (this.shouldCleanupOrphanBlobsForEvent(normalized.type)) {
|
|
952
|
+
this.deleteOrphanArtifactBlobsSync();
|
|
953
|
+
}
|
|
954
|
+
});
|
|
854
955
|
}
|
|
855
956
|
async stats() {
|
|
856
957
|
await this.prepareForRead();
|
|
958
|
+
await this.ensureDeferredInitComplete();
|
|
857
959
|
const db = this.getDb();
|
|
858
960
|
const totalRow = validateRow(db.prepare('SELECT COUNT(*) AS count, MAX(ts) AS latest FROM events').get(), { count: 'number', latest: 'nullable' }, 'stats.totalEvents');
|
|
859
961
|
const sessionRow = validateRow(db.prepare('SELECT COUNT(*) AS count FROM sessions').get(), { count: 'number' }, 'stats.sessionCount');
|
|
@@ -903,13 +1005,13 @@ export class SqliteLcmStore {
|
|
|
903
1005
|
};
|
|
904
1006
|
}
|
|
905
1007
|
async grep(input) {
|
|
906
|
-
await this.prepareForRead();
|
|
907
1008
|
const resolvedScope = this.resolveConfiguredScope('grep', input.scope, input.sessionID);
|
|
908
|
-
const sessionIDs = this.resolveScopeSessionIDs(resolvedScope, input.sessionID);
|
|
909
1009
|
const limit = input.limit ?? 5;
|
|
910
1010
|
const needle = input.query.trim();
|
|
911
1011
|
if (!needle)
|
|
912
1012
|
return [];
|
|
1013
|
+
await this.prepareForRead();
|
|
1014
|
+
const sessionIDs = this.resolveScopeSessionIDs(resolvedScope, input.sessionID);
|
|
913
1015
|
const ftsResults = this.searchWithFts(needle, sessionIDs, limit);
|
|
914
1016
|
if (ftsResults.length > 0)
|
|
915
1017
|
return ftsResults;
|
|
@@ -1206,54 +1308,33 @@ export class SqliteLcmStore {
|
|
|
1206
1308
|
eventType === 'message.part.updated' ||
|
|
1207
1309
|
eventType === 'message.part.removed');
|
|
1208
1310
|
}
|
|
1209
|
-
|
|
1210
|
-
const payload = event.payload;
|
|
1211
|
-
switch (payload.type) {
|
|
1212
|
-
case 'message.updated':
|
|
1213
|
-
return [payload.properties.info.id];
|
|
1214
|
-
case 'message.part.updated':
|
|
1215
|
-
return [payload.properties.part.messageID];
|
|
1216
|
-
case 'message.part.removed':
|
|
1217
|
-
return [payload.properties.messageID];
|
|
1218
|
-
default:
|
|
1219
|
-
return [];
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
archivedMessageIDs(messages) {
|
|
1223
|
-
return this.getArchivedMessages(messages).map((message) => message.info.id);
|
|
1224
|
-
}
|
|
1225
|
-
didArchivedMessagesChange(before, after) {
|
|
1226
|
-
const beforeIDs = this.archivedMessageIDs(before);
|
|
1227
|
-
const afterIDs = this.archivedMessageIDs(after);
|
|
1228
|
-
if (beforeIDs.length !== afterIDs.length)
|
|
1229
|
-
return true;
|
|
1230
|
-
return beforeIDs.some((messageID, index) => messageID !== afterIDs[index]);
|
|
1231
|
-
}
|
|
1232
|
-
isArchivedMessage(messages, messageID) {
|
|
1233
|
-
if (!messageID)
|
|
1234
|
-
return false;
|
|
1235
|
-
return this.archivedMessageIDs(messages).includes(messageID);
|
|
1236
|
-
}
|
|
1237
|
-
shouldSyncDerivedSessionStateForEvent(previous, next, event) {
|
|
1311
|
+
shouldSyncDerivedSessionStateForEvent(session, event) {
|
|
1238
1312
|
const payload = event.payload;
|
|
1239
1313
|
switch (payload.type) {
|
|
1240
1314
|
case 'message.updated': {
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1243
|
-
this.
|
|
1244
|
-
|
|
1315
|
+
const existing = session.messages.find((message) => message.info.id === payload.properties.info.id);
|
|
1316
|
+
if (existing) {
|
|
1317
|
+
return this.isMessageArchivedSync(session.sessionID, existing.info.id, existing.info.time.created);
|
|
1318
|
+
}
|
|
1319
|
+
return this.readMessageCountSync(session.sessionID) >= this.options.freshTailMessages;
|
|
1320
|
+
}
|
|
1321
|
+
case 'message.removed': {
|
|
1322
|
+
const existing = safeQueryOne(this.getDb().prepare('SELECT created_at FROM messages WHERE session_id = ? AND message_id = ?'), [session.sessionID, payload.properties.messageID], 'shouldSyncDerivedSessionStateForEvent.messageRemoved');
|
|
1323
|
+
if (!existing)
|
|
1324
|
+
return false;
|
|
1325
|
+
return this.readMessageCountSync(session.sessionID) > this.options.freshTailMessages;
|
|
1245
1326
|
}
|
|
1246
|
-
case 'message.removed':
|
|
1247
|
-
return this.didArchivedMessagesChange(previous.messages, next.messages);
|
|
1248
1327
|
case 'message.part.updated': {
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1251
|
-
|
|
1328
|
+
const message = session.messages.find((entry) => entry.info.id === payload.properties.part.messageID);
|
|
1329
|
+
if (!message)
|
|
1330
|
+
return false;
|
|
1331
|
+
return this.isMessageArchivedSync(session.sessionID, message.info.id, message.info.time.created);
|
|
1252
1332
|
}
|
|
1253
1333
|
case 'message.part.removed': {
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
|
|
1334
|
+
const message = session.messages.find((entry) => entry.info.id === payload.properties.messageID);
|
|
1335
|
+
if (!message)
|
|
1336
|
+
return false;
|
|
1337
|
+
return this.isMessageArchivedSync(session.sessionID, message.info.id, message.info.time.created);
|
|
1257
1338
|
}
|
|
1258
1339
|
default:
|
|
1259
1340
|
return false;
|
|
@@ -1868,66 +1949,68 @@ export class SqliteLcmStore {
|
|
|
1868
1949
|
};
|
|
1869
1950
|
}
|
|
1870
1951
|
async compactEventLog(input) {
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1952
|
+
return this.withStoreActivity(async () => {
|
|
1953
|
+
await this.prepareForRead();
|
|
1954
|
+
const apply = input?.apply ?? false;
|
|
1955
|
+
const vacuum = input?.vacuum ?? true;
|
|
1956
|
+
const limit = clamp(input?.limit ?? 10, 1, 50);
|
|
1957
|
+
const candidates = this.readPrunableEventTypeCountsSync();
|
|
1958
|
+
const candidateEvents = candidates.reduce((sum, row) => sum + row.count, 0);
|
|
1959
|
+
const beforeSizes = await this.readStoreFileSizes();
|
|
1960
|
+
if (!apply || candidateEvents === 0) {
|
|
1961
|
+
return [
|
|
1962
|
+
`candidate_events=${candidateEvents}`,
|
|
1963
|
+
`apply=false`,
|
|
1964
|
+
`vacuum_requested=${vacuum}`,
|
|
1965
|
+
`db_bytes=${beforeSizes.dbBytes}`,
|
|
1966
|
+
`wal_bytes=${beforeSizes.walBytes}`,
|
|
1967
|
+
`shm_bytes=${beforeSizes.shmBytes}`,
|
|
1968
|
+
`total_bytes=${beforeSizes.totalBytes}`,
|
|
1969
|
+
...(candidates.length > 0
|
|
1970
|
+
? [
|
|
1971
|
+
'candidate_event_types:',
|
|
1972
|
+
...candidates.slice(0, limit).map((row) => `- ${row.eventType} count=${row.count}`),
|
|
1973
|
+
]
|
|
1974
|
+
: ['candidate_event_types:', '- none']),
|
|
1975
|
+
].join('\n');
|
|
1976
|
+
}
|
|
1977
|
+
const eventTypes = candidates.map((row) => row.eventType);
|
|
1978
|
+
if (eventTypes.length > 0) {
|
|
1979
|
+
const placeholders = eventTypes.map(() => '?').join(', ');
|
|
1980
|
+
this.getDb()
|
|
1981
|
+
.prepare(`DELETE FROM events WHERE event_type IN (${placeholders})`)
|
|
1982
|
+
.run(...eventTypes);
|
|
1983
|
+
}
|
|
1984
|
+
let vacuumApplied = false;
|
|
1985
|
+
this.getDb().exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
1986
|
+
if (vacuum) {
|
|
1987
|
+
this.getDb().exec('VACUUM');
|
|
1988
|
+
this.getDb().exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
1989
|
+
vacuumApplied = true;
|
|
1990
|
+
}
|
|
1991
|
+
const afterSizes = await this.readStoreFileSizes();
|
|
1879
1992
|
return [
|
|
1880
1993
|
`candidate_events=${candidateEvents}`,
|
|
1881
|
-
`
|
|
1994
|
+
`deleted_events=${candidateEvents}`,
|
|
1995
|
+
`apply=true`,
|
|
1882
1996
|
`vacuum_requested=${vacuum}`,
|
|
1883
|
-
`
|
|
1884
|
-
`
|
|
1885
|
-
`
|
|
1886
|
-
`
|
|
1997
|
+
`vacuum_applied=${vacuumApplied}`,
|
|
1998
|
+
`db_bytes_before=${beforeSizes.dbBytes}`,
|
|
1999
|
+
`wal_bytes_before=${beforeSizes.walBytes}`,
|
|
2000
|
+
`shm_bytes_before=${beforeSizes.shmBytes}`,
|
|
2001
|
+
`total_bytes_before=${beforeSizes.totalBytes}`,
|
|
2002
|
+
`db_bytes_after=${afterSizes.dbBytes}`,
|
|
2003
|
+
`wal_bytes_after=${afterSizes.walBytes}`,
|
|
2004
|
+
`shm_bytes_after=${afterSizes.shmBytes}`,
|
|
2005
|
+
`total_bytes_after=${afterSizes.totalBytes}`,
|
|
1887
2006
|
...(candidates.length > 0
|
|
1888
2007
|
? [
|
|
1889
|
-
'
|
|
2008
|
+
'deleted_event_types:',
|
|
1890
2009
|
...candidates.slice(0, limit).map((row) => `- ${row.eventType} count=${row.count}`),
|
|
1891
2010
|
]
|
|
1892
|
-
: ['
|
|
2011
|
+
: ['deleted_event_types:', '- none']),
|
|
1893
2012
|
].join('\n');
|
|
1894
|
-
}
|
|
1895
|
-
const eventTypes = candidates.map((row) => row.eventType);
|
|
1896
|
-
if (eventTypes.length > 0) {
|
|
1897
|
-
const placeholders = eventTypes.map(() => '?').join(', ');
|
|
1898
|
-
this.getDb()
|
|
1899
|
-
.prepare(`DELETE FROM events WHERE event_type IN (${placeholders})`)
|
|
1900
|
-
.run(...eventTypes);
|
|
1901
|
-
}
|
|
1902
|
-
let vacuumApplied = false;
|
|
1903
|
-
this.getDb().exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
1904
|
-
if (vacuum) {
|
|
1905
|
-
this.getDb().exec('VACUUM');
|
|
1906
|
-
this.getDb().exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
1907
|
-
vacuumApplied = true;
|
|
1908
|
-
}
|
|
1909
|
-
const afterSizes = await this.readStoreFileSizes();
|
|
1910
|
-
return [
|
|
1911
|
-
`candidate_events=${candidateEvents}`,
|
|
1912
|
-
`deleted_events=${candidateEvents}`,
|
|
1913
|
-
`apply=true`,
|
|
1914
|
-
`vacuum_requested=${vacuum}`,
|
|
1915
|
-
`vacuum_applied=${vacuumApplied}`,
|
|
1916
|
-
`db_bytes_before=${beforeSizes.dbBytes}`,
|
|
1917
|
-
`wal_bytes_before=${beforeSizes.walBytes}`,
|
|
1918
|
-
`shm_bytes_before=${beforeSizes.shmBytes}`,
|
|
1919
|
-
`total_bytes_before=${beforeSizes.totalBytes}`,
|
|
1920
|
-
`db_bytes_after=${afterSizes.dbBytes}`,
|
|
1921
|
-
`wal_bytes_after=${afterSizes.walBytes}`,
|
|
1922
|
-
`shm_bytes_after=${afterSizes.shmBytes}`,
|
|
1923
|
-
`total_bytes_after=${afterSizes.totalBytes}`,
|
|
1924
|
-
...(candidates.length > 0
|
|
1925
|
-
? [
|
|
1926
|
-
'deleted_event_types:',
|
|
1927
|
-
...candidates.slice(0, limit).map((row) => `- ${row.eventType} count=${row.count}`),
|
|
1928
|
-
]
|
|
1929
|
-
: ['deleted_event_types:', '- none']),
|
|
1930
|
-
].join('\n');
|
|
2013
|
+
});
|
|
1931
2014
|
}
|
|
1932
2015
|
async retentionReport(input) {
|
|
1933
2016
|
await this.prepareForRead();
|
|
@@ -2055,44 +2138,50 @@ export class SqliteLcmStore {
|
|
|
2055
2138
|
].join('\n');
|
|
2056
2139
|
}
|
|
2057
2140
|
async exportSnapshot(input) {
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2141
|
+
return this.withStoreActivity(async () => {
|
|
2142
|
+
await this.prepareForRead();
|
|
2143
|
+
return exportStoreSnapshot({
|
|
2144
|
+
workspaceDirectory: this.workspaceDirectory,
|
|
2145
|
+
normalizeScope: this.normalizeScope.bind(this),
|
|
2146
|
+
resolveScopeSessionIDs: this.resolveScopeSessionIDs.bind(this),
|
|
2147
|
+
readScopedSessionRowsSync: this.readScopedSessionRowsSync.bind(this),
|
|
2148
|
+
readScopedMessageRowsSync: this.readScopedMessageRowsSync.bind(this),
|
|
2149
|
+
readScopedPartRowsSync: this.readScopedPartRowsSync.bind(this),
|
|
2150
|
+
readScopedResumeRowsSync: this.readScopedResumeRowsSync.bind(this),
|
|
2151
|
+
readScopedArtifactRowsSync: this.readScopedArtifactRowsSync.bind(this),
|
|
2152
|
+
readScopedArtifactBlobRowsSync: this.readScopedArtifactBlobRowsSync.bind(this),
|
|
2153
|
+
readScopedSummaryRowsSync: this.readScopedSummaryRowsSync.bind(this),
|
|
2154
|
+
readScopedSummaryEdgeRowsSync: this.readScopedSummaryEdgeRowsSync.bind(this),
|
|
2155
|
+
readScopedSummaryStateRowsSync: this.readScopedSummaryStateRowsSync.bind(this),
|
|
2156
|
+
}, input);
|
|
2157
|
+
});
|
|
2073
2158
|
}
|
|
2074
2159
|
async importSnapshot(input) {
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2160
|
+
return this.withStoreActivity(async () => {
|
|
2161
|
+
await this.prepareForRead();
|
|
2162
|
+
return importStoreSnapshot({
|
|
2163
|
+
workspaceDirectory: this.workspaceDirectory,
|
|
2164
|
+
getDb: () => this.getDb(),
|
|
2165
|
+
clearSessionDataSync: this.clearSessionDataSync.bind(this),
|
|
2166
|
+
backfillArtifactBlobsSync: this.backfillArtifactBlobsSync.bind(this),
|
|
2167
|
+
refreshAllLineageSync: this.refreshAllLineageSync.bind(this),
|
|
2168
|
+
syncAllDerivedSessionStateSync: this.syncAllDerivedSessionStateSync.bind(this),
|
|
2169
|
+
refreshSearchIndexesSync: this.refreshSearchIndexesSync.bind(this),
|
|
2170
|
+
}, input);
|
|
2171
|
+
});
|
|
2085
2172
|
}
|
|
2086
2173
|
async resume(sessionID) {
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2174
|
+
return this.withStoreActivity(async () => {
|
|
2175
|
+
await this.prepareForRead();
|
|
2176
|
+
const resolvedSessionID = sessionID ?? this.latestSessionIDSync();
|
|
2177
|
+
if (!resolvedSessionID)
|
|
2178
|
+
return 'No stored resume snapshots yet.';
|
|
2179
|
+
const existing = this.getResumeSync(resolvedSessionID);
|
|
2180
|
+
if (existing && !this.isManagedResumeNote(existing))
|
|
2181
|
+
return existing;
|
|
2182
|
+
const generated = await this.buildCompactionContext(resolvedSessionID);
|
|
2183
|
+
return generated ?? existing ?? 'No stored resume snapshot for that session.';
|
|
2184
|
+
});
|
|
2096
2185
|
}
|
|
2097
2186
|
async expand(input) {
|
|
2098
2187
|
await this.prepareForRead();
|
|
@@ -2149,45 +2238,47 @@ export class SqliteLcmStore {
|
|
|
2149
2238
|
return note;
|
|
2150
2239
|
}
|
|
2151
2240
|
async transformMessages(messages) {
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2241
|
+
return this.withStoreActivity(async () => {
|
|
2242
|
+
if (messages.length < this.options.minMessagesForTransform)
|
|
2243
|
+
return false;
|
|
2244
|
+
const window = resolveArchiveTransformWindow(messages, this.options.freshTailMessages);
|
|
2245
|
+
if (!window)
|
|
2246
|
+
return false;
|
|
2247
|
+
await this.prepareForRead();
|
|
2248
|
+
const { anchor, archived, recent } = window;
|
|
2249
|
+
const roots = this.ensureSummaryGraphSync(anchor.info.sessionID, archived);
|
|
2250
|
+
if (roots.length === 0)
|
|
2251
|
+
return false;
|
|
2252
|
+
const summary = buildActiveSummaryText(roots, archived.length, this.options.summaryCharBudget);
|
|
2253
|
+
const retrieval = await this.buildAutomaticRetrievalContext(anchor.info.sessionID, recent, anchor);
|
|
2254
|
+
for (const message of archived) {
|
|
2255
|
+
this.compactMessageInPlace(message);
|
|
2256
|
+
}
|
|
2257
|
+
anchor.parts = anchor.parts.filter((part) => !isSyntheticLcmTextPart(part, ['archive-summary', 'retrieved-context']));
|
|
2258
|
+
const syntheticParts = [];
|
|
2259
|
+
if (retrieval) {
|
|
2260
|
+
syntheticParts.push({
|
|
2261
|
+
id: `lcm-memory-${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
|
2262
|
+
sessionID: anchor.info.sessionID,
|
|
2263
|
+
messageID: anchor.info.id,
|
|
2264
|
+
type: 'text',
|
|
2265
|
+
text: retrieval,
|
|
2266
|
+
synthetic: true,
|
|
2267
|
+
metadata: { opencodeLcm: 'retrieved-context' },
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2170
2270
|
syntheticParts.push({
|
|
2171
|
-
id: `lcm-
|
|
2271
|
+
id: `lcm-summary-${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
|
2172
2272
|
sessionID: anchor.info.sessionID,
|
|
2173
2273
|
messageID: anchor.info.id,
|
|
2174
2274
|
type: 'text',
|
|
2175
|
-
text:
|
|
2275
|
+
text: summary,
|
|
2176
2276
|
synthetic: true,
|
|
2177
|
-
metadata: { opencodeLcm: '
|
|
2277
|
+
metadata: { opencodeLcm: 'archive-summary' },
|
|
2178
2278
|
});
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
id: `lcm-summary-${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
|
2182
|
-
sessionID: anchor.info.sessionID,
|
|
2183
|
-
messageID: anchor.info.id,
|
|
2184
|
-
type: 'text',
|
|
2185
|
-
text: summary,
|
|
2186
|
-
synthetic: true,
|
|
2187
|
-
metadata: { opencodeLcm: 'archive-summary' },
|
|
2279
|
+
anchor.parts.push(...syntheticParts);
|
|
2280
|
+
return true;
|
|
2188
2281
|
});
|
|
2189
|
-
anchor.parts.push(...syntheticParts);
|
|
2190
|
-
return true;
|
|
2191
2282
|
}
|
|
2192
2283
|
systemHint() {
|
|
2193
2284
|
if (!this.options.systemHint)
|
|
@@ -3136,15 +3227,29 @@ export class SqliteLcmStore {
|
|
|
3136
3227
|
resolveLineageSync(sessionID, parentSessionID) {
|
|
3137
3228
|
if (!parentSessionID)
|
|
3138
3229
|
return { rootSessionID: sessionID, lineageDepth: 0 };
|
|
3139
|
-
const
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3230
|
+
const seen = new Set([sessionID]);
|
|
3231
|
+
let currentSessionID = parentSessionID;
|
|
3232
|
+
let lineageDepth = 1;
|
|
3233
|
+
while (currentSessionID && !seen.has(currentSessionID)) {
|
|
3234
|
+
seen.add(currentSessionID);
|
|
3235
|
+
const parent = this.getDb()
|
|
3236
|
+
.prepare('SELECT parent_session_id, root_session_id, lineage_depth FROM sessions WHERE session_id = ?')
|
|
3237
|
+
.get(currentSessionID);
|
|
3238
|
+
if (!parent)
|
|
3239
|
+
return { rootSessionID: currentSessionID, lineageDepth };
|
|
3240
|
+
if (parent.root_session_id && parent.lineage_depth !== null) {
|
|
3241
|
+
return {
|
|
3242
|
+
rootSessionID: parent.root_session_id,
|
|
3243
|
+
lineageDepth: parent.lineage_depth + lineageDepth,
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
if (!parent.parent_session_id) {
|
|
3247
|
+
return { rootSessionID: currentSessionID, lineageDepth };
|
|
3248
|
+
}
|
|
3249
|
+
currentSessionID = parent.parent_session_id;
|
|
3250
|
+
lineageDepth += 1;
|
|
3251
|
+
}
|
|
3252
|
+
return { rootSessionID: parentSessionID, lineageDepth };
|
|
3148
3253
|
}
|
|
3149
3254
|
applyEvent(session, event) {
|
|
3150
3255
|
const payload = event.payload;
|
|
@@ -3204,26 +3309,33 @@ export class SqliteLcmStore {
|
|
|
3204
3309
|
const row = safeQueryOne(this.getDb().prepare('SELECT note FROM resumes WHERE session_id = ?'), [sessionID], 'getResumeSync');
|
|
3205
3310
|
return row?.note;
|
|
3206
3311
|
}
|
|
3207
|
-
|
|
3208
|
-
const
|
|
3209
|
-
|
|
3210
|
-
|
|
3312
|
+
materializeSessionRow(row, messages = []) {
|
|
3313
|
+
const parentSessionID = row.parent_session_id ?? undefined;
|
|
3314
|
+
const derivedLineage = row.root_session_id === null || row.lineage_depth === null
|
|
3315
|
+
? this.resolveLineageSync(row.session_id, parentSessionID)
|
|
3316
|
+
: undefined;
|
|
3211
3317
|
return {
|
|
3212
3318
|
sessionID: row.session_id,
|
|
3213
3319
|
title: row.title ?? undefined,
|
|
3214
3320
|
directory: row.session_directory ?? undefined,
|
|
3215
|
-
parentSessionID
|
|
3216
|
-
rootSessionID: row.root_session_id ??
|
|
3217
|
-
lineageDepth: row.lineage_depth ??
|
|
3321
|
+
parentSessionID,
|
|
3322
|
+
rootSessionID: row.root_session_id ?? derivedLineage?.rootSessionID,
|
|
3323
|
+
lineageDepth: row.lineage_depth ?? derivedLineage?.lineageDepth,
|
|
3218
3324
|
pinned: Boolean(row.pinned),
|
|
3219
3325
|
pinReason: row.pin_reason ?? undefined,
|
|
3220
3326
|
updatedAt: row.updated_at,
|
|
3221
3327
|
compactedAt: row.compacted_at ?? undefined,
|
|
3222
3328
|
deleted: Boolean(row.deleted),
|
|
3223
3329
|
eventCount: row.event_count,
|
|
3224
|
-
messages
|
|
3330
|
+
messages,
|
|
3225
3331
|
};
|
|
3226
3332
|
}
|
|
3333
|
+
readSessionHeaderSync(sessionID) {
|
|
3334
|
+
const row = safeQueryOne(this.getDb().prepare('SELECT * FROM sessions WHERE session_id = ?'), [sessionID], 'readSessionHeaderSync');
|
|
3335
|
+
if (!row)
|
|
3336
|
+
return undefined;
|
|
3337
|
+
return this.materializeSessionRow(row);
|
|
3338
|
+
}
|
|
3227
3339
|
clearSessionDataSync(sessionID) {
|
|
3228
3340
|
const db = this.getDb();
|
|
3229
3341
|
db.prepare('DELETE FROM message_fts WHERE session_id = ?').run(sessionID);
|
|
@@ -3361,26 +3473,12 @@ export class SqliteLcmStore {
|
|
|
3361
3473
|
const row = sessionMap.get(sessionID);
|
|
3362
3474
|
const messages = messagesBySession.get(sessionID) ?? [];
|
|
3363
3475
|
if (!row) {
|
|
3364
|
-
return { sessionID,
|
|
3476
|
+
return { ...emptySession(sessionID), messages };
|
|
3365
3477
|
}
|
|
3366
|
-
return
|
|
3367
|
-
sessionID: row.session_id,
|
|
3368
|
-
title: row.title ?? undefined,
|
|
3369
|
-
directory: row.session_directory ?? undefined,
|
|
3370
|
-
parentSessionID: row.parent_session_id ?? undefined,
|
|
3371
|
-
rootSessionID: row.root_session_id ?? undefined,
|
|
3372
|
-
lineageDepth: row.lineage_depth ?? undefined,
|
|
3373
|
-
pinned: Boolean(row.pinned),
|
|
3374
|
-
pinReason: row.pin_reason ?? undefined,
|
|
3375
|
-
updatedAt: row.updated_at,
|
|
3376
|
-
compactedAt: row.compacted_at ?? undefined,
|
|
3377
|
-
deleted: Boolean(row.deleted),
|
|
3378
|
-
eventCount: row.event_count,
|
|
3379
|
-
messages,
|
|
3380
|
-
};
|
|
3478
|
+
return this.materializeSessionRow(row, messages);
|
|
3381
3479
|
});
|
|
3382
3480
|
}
|
|
3383
|
-
readSessionSync(sessionID
|
|
3481
|
+
readSessionSync(sessionID) {
|
|
3384
3482
|
const db = this.getDb();
|
|
3385
3483
|
const row = safeQueryOne(db.prepare('SELECT * FROM sessions WHERE session_id = ?'), [sessionID], 'readSessionSync');
|
|
3386
3484
|
const messageRows = db
|
|
@@ -3390,11 +3488,7 @@ export class SqliteLcmStore {
|
|
|
3390
3488
|
.prepare('SELECT * FROM parts WHERE session_id = ? ORDER BY message_id ASC, sort_key ASC, part_id ASC')
|
|
3391
3489
|
.all(sessionID);
|
|
3392
3490
|
const artifactsByPart = new Map();
|
|
3393
|
-
const
|
|
3394
|
-
const artifacts = artifactMessageIDs === undefined
|
|
3395
|
-
? this.readArtifactsForSessionSync(sessionID)
|
|
3396
|
-
: [...new Set(artifactMessageIDs)].flatMap((messageID) => this.readArtifactsForMessageSync(messageID));
|
|
3397
|
-
for (const artifact of artifacts) {
|
|
3491
|
+
for (const artifact of this.readArtifactsForSessionSync(sessionID)) {
|
|
3398
3492
|
const list = artifactsByPart.get(artifact.partID) ?? [];
|
|
3399
3493
|
list.push(artifact);
|
|
3400
3494
|
artifactsByPart.set(artifact.partID, list);
|
|
@@ -3413,28 +3507,9 @@ export class SqliteLcmStore {
|
|
|
3413
3507
|
parts: partsByMessage.get(messageRow.message_id) ?? [],
|
|
3414
3508
|
}));
|
|
3415
3509
|
if (!row) {
|
|
3416
|
-
return {
|
|
3417
|
-
sessionID,
|
|
3418
|
-
updatedAt: 0,
|
|
3419
|
-
eventCount: 0,
|
|
3420
|
-
messages,
|
|
3421
|
-
};
|
|
3510
|
+
return { ...emptySession(sessionID), messages };
|
|
3422
3511
|
}
|
|
3423
|
-
return
|
|
3424
|
-
sessionID: row.session_id,
|
|
3425
|
-
title: row.title ?? undefined,
|
|
3426
|
-
directory: row.session_directory ?? undefined,
|
|
3427
|
-
parentSessionID: row.parent_session_id ?? undefined,
|
|
3428
|
-
rootSessionID: row.root_session_id ?? undefined,
|
|
3429
|
-
lineageDepth: row.lineage_depth ?? undefined,
|
|
3430
|
-
pinned: Boolean(row.pinned),
|
|
3431
|
-
pinReason: row.pin_reason ?? undefined,
|
|
3432
|
-
updatedAt: row.updated_at,
|
|
3433
|
-
compactedAt: row.compacted_at ?? undefined,
|
|
3434
|
-
deleted: Boolean(row.deleted),
|
|
3435
|
-
eventCount: row.event_count,
|
|
3436
|
-
messages,
|
|
3437
|
-
};
|
|
3512
|
+
return this.materializeSessionRow(row, messages);
|
|
3438
3513
|
}
|
|
3439
3514
|
prepareSessionForPersistence(session) {
|
|
3440
3515
|
const parentSessionID = this.sanitizeParentSessionIDSync(session.sessionID, session.parentSessionID);
|
|
@@ -3462,6 +3537,73 @@ export class SqliteLcmStore {
|
|
|
3462
3537
|
}
|
|
3463
3538
|
return parentSessionID;
|
|
3464
3539
|
}
|
|
3540
|
+
readSessionForCaptureSync(event) {
|
|
3541
|
+
const sessionID = event.sessionID;
|
|
3542
|
+
if (!sessionID)
|
|
3543
|
+
return emptySession('');
|
|
3544
|
+
const session = this.readSessionHeaderSync(sessionID) ?? emptySession(sessionID);
|
|
3545
|
+
const payload = event.payload;
|
|
3546
|
+
switch (payload.type) {
|
|
3547
|
+
case 'message.updated': {
|
|
3548
|
+
const message = this.readMessageSync(sessionID, payload.properties.info.id);
|
|
3549
|
+
if (message)
|
|
3550
|
+
session.messages = [message];
|
|
3551
|
+
return session;
|
|
3552
|
+
}
|
|
3553
|
+
case 'message.part.updated': {
|
|
3554
|
+
const message = this.readMessageSync(sessionID, payload.properties.part.messageID);
|
|
3555
|
+
if (message)
|
|
3556
|
+
session.messages = [message];
|
|
3557
|
+
return session;
|
|
3558
|
+
}
|
|
3559
|
+
case 'message.part.removed': {
|
|
3560
|
+
const message = this.readMessageSync(sessionID, payload.properties.messageID);
|
|
3561
|
+
if (message)
|
|
3562
|
+
session.messages = [message];
|
|
3563
|
+
return session;
|
|
3564
|
+
}
|
|
3565
|
+
default:
|
|
3566
|
+
return session;
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
readMessageSync(sessionID, messageID) {
|
|
3570
|
+
const db = this.getDb();
|
|
3571
|
+
const row = safeQueryOne(db.prepare('SELECT * FROM messages WHERE session_id = ? AND message_id = ?'), [sessionID, messageID], 'readMessageSync');
|
|
3572
|
+
if (!row)
|
|
3573
|
+
return undefined;
|
|
3574
|
+
const artifactsByPart = new Map();
|
|
3575
|
+
for (const artifact of this.readArtifactsForMessageSync(messageID)) {
|
|
3576
|
+
const list = artifactsByPart.get(artifact.partID) ?? [];
|
|
3577
|
+
list.push(artifact);
|
|
3578
|
+
artifactsByPart.set(artifact.partID, list);
|
|
3579
|
+
}
|
|
3580
|
+
const parts = db
|
|
3581
|
+
.prepare('SELECT * FROM parts WHERE session_id = ? AND message_id = ? ORDER BY sort_key ASC, part_id ASC')
|
|
3582
|
+
.all(sessionID, messageID);
|
|
3583
|
+
return {
|
|
3584
|
+
info: parseJson(row.info_json),
|
|
3585
|
+
parts: parts.map((partRow) => {
|
|
3586
|
+
const part = parseJson(partRow.part_json);
|
|
3587
|
+
hydratePartFromArtifacts(part, artifactsByPart.get(part.id) ?? []);
|
|
3588
|
+
return part;
|
|
3589
|
+
}),
|
|
3590
|
+
};
|
|
3591
|
+
}
|
|
3592
|
+
readMessageCountSync(sessionID) {
|
|
3593
|
+
const row = this.getDb()
|
|
3594
|
+
.prepare('SELECT COUNT(*) AS count FROM messages WHERE session_id = ?')
|
|
3595
|
+
.get(sessionID);
|
|
3596
|
+
return row.count;
|
|
3597
|
+
}
|
|
3598
|
+
isMessageArchivedSync(sessionID, messageID, createdAt) {
|
|
3599
|
+
const row = this.getDb()
|
|
3600
|
+
.prepare(`SELECT COUNT(*) AS count
|
|
3601
|
+
FROM messages
|
|
3602
|
+
WHERE session_id = ?
|
|
3603
|
+
AND (created_at > ? OR (created_at = ? AND message_id > ?))`)
|
|
3604
|
+
.get(sessionID, createdAt, createdAt, messageID);
|
|
3605
|
+
return row.count >= this.options.freshTailMessages;
|
|
3606
|
+
}
|
|
3465
3607
|
async persistCapturedSession(session, event) {
|
|
3466
3608
|
const payload = event.payload;
|
|
3467
3609
|
switch (payload.type) {
|