opencode-lcm 0.12.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/dist/store.d.ts +10 -0
- package/dist/store.js +348 -260
- package/package.json +1 -1
- package/src/store.ts +406 -286
package/dist/store.js
CHANGED
|
@@ -336,6 +336,11 @@ function logStartupPhase(phase, context) {
|
|
|
336
336
|
return;
|
|
337
337
|
getLogger().info(`startup phase: ${phase}`, context);
|
|
338
338
|
}
|
|
339
|
+
function unrefTimer(timer) {
|
|
340
|
+
if (typeof timer === 'object' && timer && 'unref' in timer && typeof timer.unref === 'function') {
|
|
341
|
+
timer.unref();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
339
344
|
function normalizeSqliteRuntimeOverride(value) {
|
|
340
345
|
const normalized = value?.trim().toLowerCase();
|
|
341
346
|
if (normalized === 'bun' || normalized === 'node')
|
|
@@ -463,6 +468,10 @@ export class SqliteLcmStore {
|
|
|
463
468
|
workspaceDirectory;
|
|
464
469
|
db;
|
|
465
470
|
dbReadyPromise;
|
|
471
|
+
deferredInitTimer;
|
|
472
|
+
deferredInitPromise;
|
|
473
|
+
deferredInitRequested = false;
|
|
474
|
+
activeOperationCount = 0;
|
|
466
475
|
pendingPartUpdates = new Map();
|
|
467
476
|
pendingPartUpdateTimer;
|
|
468
477
|
pendingPartUpdateFlushPromise;
|
|
@@ -476,8 +485,27 @@ export class SqliteLcmStore {
|
|
|
476
485
|
async init() {
|
|
477
486
|
await mkdir(this.baseDir, { recursive: true });
|
|
478
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
|
+
}
|
|
479
506
|
async prepareForRead() {
|
|
480
507
|
await this.ensureDbReady();
|
|
508
|
+
await this.waitForDeferredInitIfRunning();
|
|
481
509
|
await this.flushDeferredPartUpdates();
|
|
482
510
|
}
|
|
483
511
|
scheduleDeferredPartUpdateFlush() {
|
|
@@ -487,12 +515,7 @@ export class SqliteLcmStore {
|
|
|
487
515
|
this.pendingPartUpdateTimer = undefined;
|
|
488
516
|
void this.flushDeferredPartUpdates();
|
|
489
517
|
}, SqliteLcmStore.deferredPartUpdateDelayMs);
|
|
490
|
-
|
|
491
|
-
this.pendingPartUpdateTimer &&
|
|
492
|
-
'unref' in this.pendingPartUpdateTimer &&
|
|
493
|
-
typeof this.pendingPartUpdateTimer.unref === 'function') {
|
|
494
|
-
this.pendingPartUpdateTimer.unref();
|
|
495
|
-
}
|
|
518
|
+
unrefTimer(this.pendingPartUpdateTimer);
|
|
496
519
|
}
|
|
497
520
|
clearDeferredPartUpdateTimer() {
|
|
498
521
|
if (!this.pendingPartUpdateTimer)
|
|
@@ -536,57 +559,64 @@ export class SqliteLcmStore {
|
|
|
536
559
|
this.clearDeferredPartUpdateTimer();
|
|
537
560
|
}
|
|
538
561
|
async captureDeferred(event) {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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;
|
|
547
583
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
break;
|
|
551
|
-
case 'message.removed':
|
|
552
|
-
this.clearDeferredPartUpdatesForMessage(event.properties.sessionID, event.properties.messageID);
|
|
553
|
-
break;
|
|
554
|
-
case 'session.deleted':
|
|
555
|
-
this.clearDeferredPartUpdatesForSession(extractSessionID(event));
|
|
556
|
-
break;
|
|
557
|
-
default:
|
|
558
|
-
break;
|
|
559
|
-
}
|
|
560
|
-
await this.capture(event);
|
|
584
|
+
await this.capture(event);
|
|
585
|
+
});
|
|
561
586
|
}
|
|
562
587
|
async flushDeferredPartUpdates() {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
+
}
|
|
574
601
|
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
602
|
+
})().finally(() => {
|
|
603
|
+
this.pendingPartUpdateFlushPromise = undefined;
|
|
604
|
+
if (this.pendingPartUpdates.size > 0)
|
|
605
|
+
this.scheduleDeferredPartUpdateFlush();
|
|
606
|
+
});
|
|
607
|
+
return this.pendingPartUpdateFlushPromise;
|
|
580
608
|
});
|
|
581
|
-
return this.pendingPartUpdateFlushPromise;
|
|
582
609
|
}
|
|
583
610
|
async ensureDbReady() {
|
|
584
|
-
if (this.
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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();
|
|
590
620
|
}
|
|
591
621
|
async openAndInitializeDb() {
|
|
592
622
|
logStartupPhase('open-db:start', { dbPath: this.dbPath });
|
|
@@ -755,8 +785,6 @@ export class SqliteLcmStore {
|
|
|
755
785
|
await this.migrateLegacyArtifacts();
|
|
756
786
|
logStartupPhase('open-db:write-schema-version', { schemaVersion: STORE_SCHEMA_VERSION });
|
|
757
787
|
this.writeSchemaVersionSync(STORE_SCHEMA_VERSION);
|
|
758
|
-
logStartupPhase('open-db:deferred-init:start');
|
|
759
|
-
this.completeDeferredInit();
|
|
760
788
|
logStartupPhase('open-db:ready');
|
|
761
789
|
}
|
|
762
790
|
catch (error) {
|
|
@@ -770,6 +798,54 @@ export class SqliteLcmStore {
|
|
|
770
798
|
}
|
|
771
799
|
}
|
|
772
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
|
+
}
|
|
773
849
|
readSchemaVersionSync() {
|
|
774
850
|
return firstFiniteNumber(this.getDb().prepare('PRAGMA user_version').get()) ?? 0;
|
|
775
851
|
}
|
|
@@ -819,6 +895,10 @@ export class SqliteLcmStore {
|
|
|
819
895
|
close() {
|
|
820
896
|
this.clearDeferredPartUpdateTimer();
|
|
821
897
|
this.pendingPartUpdates.clear();
|
|
898
|
+
if (this.deferredInitTimer) {
|
|
899
|
+
clearTimeout(this.deferredInitTimer);
|
|
900
|
+
this.deferredInitTimer = undefined;
|
|
901
|
+
}
|
|
822
902
|
if (!this.db)
|
|
823
903
|
return;
|
|
824
904
|
this.db.close();
|
|
@@ -826,51 +906,56 @@ export class SqliteLcmStore {
|
|
|
826
906
|
this.dbReadyPromise = undefined;
|
|
827
907
|
}
|
|
828
908
|
async capture(event) {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
this.
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const session = resolveCaptureHydrationMode() === 'targeted'
|
|
841
|
-
? this.readSessionForCaptureSync(normalized)
|
|
842
|
-
: this.readSessionSync(normalized.sessionID);
|
|
843
|
-
const previousParentSessionID = session.parentSessionID;
|
|
844
|
-
const shouldSyncDerivedState = this.shouldSyncDerivedSessionStateForEvent(session, normalized);
|
|
845
|
-
let next = this.applyEvent(session, normalized);
|
|
846
|
-
next.updatedAt = Math.max(next.updatedAt, normalized.timestamp);
|
|
847
|
-
next.eventCount += 1;
|
|
848
|
-
next = this.prepareSessionForPersistence(next);
|
|
849
|
-
await this.persistCapturedSession(next, normalized);
|
|
850
|
-
if (this.shouldRefreshLineageForEvent(normalized.type)) {
|
|
851
|
-
this.refreshAllLineageSync();
|
|
852
|
-
const refreshed = this.readSessionHeaderSync(normalized.sessionID);
|
|
853
|
-
if (refreshed) {
|
|
854
|
-
next = {
|
|
855
|
-
...next,
|
|
856
|
-
parentSessionID: refreshed.parentSessionID,
|
|
857
|
-
rootSessionID: refreshed.rootSessionID,
|
|
858
|
-
lineageDepth: refreshed.lineageDepth,
|
|
859
|
-
};
|
|
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);
|
|
860
920
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
+
});
|
|
871
955
|
}
|
|
872
956
|
async stats() {
|
|
873
957
|
await this.prepareForRead();
|
|
958
|
+
await this.ensureDeferredInitComplete();
|
|
874
959
|
const db = this.getDb();
|
|
875
960
|
const totalRow = validateRow(db.prepare('SELECT COUNT(*) AS count, MAX(ts) AS latest FROM events').get(), { count: 'number', latest: 'nullable' }, 'stats.totalEvents');
|
|
876
961
|
const sessionRow = validateRow(db.prepare('SELECT COUNT(*) AS count FROM sessions').get(), { count: 'number' }, 'stats.sessionCount');
|
|
@@ -920,13 +1005,13 @@ export class SqliteLcmStore {
|
|
|
920
1005
|
};
|
|
921
1006
|
}
|
|
922
1007
|
async grep(input) {
|
|
923
|
-
await this.prepareForRead();
|
|
924
1008
|
const resolvedScope = this.resolveConfiguredScope('grep', input.scope, input.sessionID);
|
|
925
|
-
const sessionIDs = this.resolveScopeSessionIDs(resolvedScope, input.sessionID);
|
|
926
1009
|
const limit = input.limit ?? 5;
|
|
927
1010
|
const needle = input.query.trim();
|
|
928
1011
|
if (!needle)
|
|
929
1012
|
return [];
|
|
1013
|
+
await this.prepareForRead();
|
|
1014
|
+
const sessionIDs = this.resolveScopeSessionIDs(resolvedScope, input.sessionID);
|
|
930
1015
|
const ftsResults = this.searchWithFts(needle, sessionIDs, limit);
|
|
931
1016
|
if (ftsResults.length > 0)
|
|
932
1017
|
return ftsResults;
|
|
@@ -1864,66 +1949,68 @@ export class SqliteLcmStore {
|
|
|
1864
1949
|
};
|
|
1865
1950
|
}
|
|
1866
1951
|
async compactEventLog(input) {
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
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();
|
|
1875
1992
|
return [
|
|
1876
1993
|
`candidate_events=${candidateEvents}`,
|
|
1877
|
-
`
|
|
1994
|
+
`deleted_events=${candidateEvents}`,
|
|
1995
|
+
`apply=true`,
|
|
1878
1996
|
`vacuum_requested=${vacuum}`,
|
|
1879
|
-
`
|
|
1880
|
-
`
|
|
1881
|
-
`
|
|
1882
|
-
`
|
|
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}`,
|
|
1883
2006
|
...(candidates.length > 0
|
|
1884
2007
|
? [
|
|
1885
|
-
'
|
|
2008
|
+
'deleted_event_types:',
|
|
1886
2009
|
...candidates.slice(0, limit).map((row) => `- ${row.eventType} count=${row.count}`),
|
|
1887
2010
|
]
|
|
1888
|
-
: ['
|
|
2011
|
+
: ['deleted_event_types:', '- none']),
|
|
1889
2012
|
].join('\n');
|
|
1890
|
-
}
|
|
1891
|
-
const eventTypes = candidates.map((row) => row.eventType);
|
|
1892
|
-
if (eventTypes.length > 0) {
|
|
1893
|
-
const placeholders = eventTypes.map(() => '?').join(', ');
|
|
1894
|
-
this.getDb()
|
|
1895
|
-
.prepare(`DELETE FROM events WHERE event_type IN (${placeholders})`)
|
|
1896
|
-
.run(...eventTypes);
|
|
1897
|
-
}
|
|
1898
|
-
let vacuumApplied = false;
|
|
1899
|
-
this.getDb().exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
1900
|
-
if (vacuum) {
|
|
1901
|
-
this.getDb().exec('VACUUM');
|
|
1902
|
-
this.getDb().exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
1903
|
-
vacuumApplied = true;
|
|
1904
|
-
}
|
|
1905
|
-
const afterSizes = await this.readStoreFileSizes();
|
|
1906
|
-
return [
|
|
1907
|
-
`candidate_events=${candidateEvents}`,
|
|
1908
|
-
`deleted_events=${candidateEvents}`,
|
|
1909
|
-
`apply=true`,
|
|
1910
|
-
`vacuum_requested=${vacuum}`,
|
|
1911
|
-
`vacuum_applied=${vacuumApplied}`,
|
|
1912
|
-
`db_bytes_before=${beforeSizes.dbBytes}`,
|
|
1913
|
-
`wal_bytes_before=${beforeSizes.walBytes}`,
|
|
1914
|
-
`shm_bytes_before=${beforeSizes.shmBytes}`,
|
|
1915
|
-
`total_bytes_before=${beforeSizes.totalBytes}`,
|
|
1916
|
-
`db_bytes_after=${afterSizes.dbBytes}`,
|
|
1917
|
-
`wal_bytes_after=${afterSizes.walBytes}`,
|
|
1918
|
-
`shm_bytes_after=${afterSizes.shmBytes}`,
|
|
1919
|
-
`total_bytes_after=${afterSizes.totalBytes}`,
|
|
1920
|
-
...(candidates.length > 0
|
|
1921
|
-
? [
|
|
1922
|
-
'deleted_event_types:',
|
|
1923
|
-
...candidates.slice(0, limit).map((row) => `- ${row.eventType} count=${row.count}`),
|
|
1924
|
-
]
|
|
1925
|
-
: ['deleted_event_types:', '- none']),
|
|
1926
|
-
].join('\n');
|
|
2013
|
+
});
|
|
1927
2014
|
}
|
|
1928
2015
|
async retentionReport(input) {
|
|
1929
2016
|
await this.prepareForRead();
|
|
@@ -2051,44 +2138,50 @@ export class SqliteLcmStore {
|
|
|
2051
2138
|
].join('\n');
|
|
2052
2139
|
}
|
|
2053
2140
|
async exportSnapshot(input) {
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
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
|
+
});
|
|
2069
2158
|
}
|
|
2070
2159
|
async importSnapshot(input) {
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
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
|
+
});
|
|
2081
2172
|
}
|
|
2082
2173
|
async resume(sessionID) {
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
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
|
+
});
|
|
2092
2185
|
}
|
|
2093
2186
|
async expand(input) {
|
|
2094
2187
|
await this.prepareForRead();
|
|
@@ -2145,45 +2238,47 @@ export class SqliteLcmStore {
|
|
|
2145
2238
|
return note;
|
|
2146
2239
|
}
|
|
2147
2240
|
async transformMessages(messages) {
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
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
|
+
}
|
|
2166
2270
|
syntheticParts.push({
|
|
2167
|
-
id: `lcm-
|
|
2271
|
+
id: `lcm-summary-${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
|
2168
2272
|
sessionID: anchor.info.sessionID,
|
|
2169
2273
|
messageID: anchor.info.id,
|
|
2170
2274
|
type: 'text',
|
|
2171
|
-
text:
|
|
2275
|
+
text: summary,
|
|
2172
2276
|
synthetic: true,
|
|
2173
|
-
metadata: { opencodeLcm: '
|
|
2277
|
+
metadata: { opencodeLcm: 'archive-summary' },
|
|
2174
2278
|
});
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
id: `lcm-summary-${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
|
2178
|
-
sessionID: anchor.info.sessionID,
|
|
2179
|
-
messageID: anchor.info.id,
|
|
2180
|
-
type: 'text',
|
|
2181
|
-
text: summary,
|
|
2182
|
-
synthetic: true,
|
|
2183
|
-
metadata: { opencodeLcm: 'archive-summary' },
|
|
2279
|
+
anchor.parts.push(...syntheticParts);
|
|
2280
|
+
return true;
|
|
2184
2281
|
});
|
|
2185
|
-
anchor.parts.push(...syntheticParts);
|
|
2186
|
-
return true;
|
|
2187
2282
|
}
|
|
2188
2283
|
systemHint() {
|
|
2189
2284
|
if (!this.options.systemHint)
|
|
@@ -3132,15 +3227,29 @@ export class SqliteLcmStore {
|
|
|
3132
3227
|
resolveLineageSync(sessionID, parentSessionID) {
|
|
3133
3228
|
if (!parentSessionID)
|
|
3134
3229
|
return { rootSessionID: sessionID, lineageDepth: 0 };
|
|
3135
|
-
const
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
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 };
|
|
3144
3253
|
}
|
|
3145
3254
|
applyEvent(session, event) {
|
|
3146
3255
|
const payload = event.payload;
|
|
@@ -3200,26 +3309,33 @@ export class SqliteLcmStore {
|
|
|
3200
3309
|
const row = safeQueryOne(this.getDb().prepare('SELECT note FROM resumes WHERE session_id = ?'), [sessionID], 'getResumeSync');
|
|
3201
3310
|
return row?.note;
|
|
3202
3311
|
}
|
|
3203
|
-
|
|
3204
|
-
const
|
|
3205
|
-
|
|
3206
|
-
|
|
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;
|
|
3207
3317
|
return {
|
|
3208
3318
|
sessionID: row.session_id,
|
|
3209
3319
|
title: row.title ?? undefined,
|
|
3210
3320
|
directory: row.session_directory ?? undefined,
|
|
3211
|
-
parentSessionID
|
|
3212
|
-
rootSessionID: row.root_session_id ??
|
|
3213
|
-
lineageDepth: row.lineage_depth ??
|
|
3321
|
+
parentSessionID,
|
|
3322
|
+
rootSessionID: row.root_session_id ?? derivedLineage?.rootSessionID,
|
|
3323
|
+
lineageDepth: row.lineage_depth ?? derivedLineage?.lineageDepth,
|
|
3214
3324
|
pinned: Boolean(row.pinned),
|
|
3215
3325
|
pinReason: row.pin_reason ?? undefined,
|
|
3216
3326
|
updatedAt: row.updated_at,
|
|
3217
3327
|
compactedAt: row.compacted_at ?? undefined,
|
|
3218
3328
|
deleted: Boolean(row.deleted),
|
|
3219
3329
|
eventCount: row.event_count,
|
|
3220
|
-
messages
|
|
3330
|
+
messages,
|
|
3221
3331
|
};
|
|
3222
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
|
+
}
|
|
3223
3339
|
clearSessionDataSync(sessionID) {
|
|
3224
3340
|
const db = this.getDb();
|
|
3225
3341
|
db.prepare('DELETE FROM message_fts WHERE session_id = ?').run(sessionID);
|
|
@@ -3359,21 +3475,7 @@ export class SqliteLcmStore {
|
|
|
3359
3475
|
if (!row) {
|
|
3360
3476
|
return { ...emptySession(sessionID), messages };
|
|
3361
3477
|
}
|
|
3362
|
-
return
|
|
3363
|
-
sessionID: row.session_id,
|
|
3364
|
-
title: row.title ?? undefined,
|
|
3365
|
-
directory: row.session_directory ?? undefined,
|
|
3366
|
-
parentSessionID: row.parent_session_id ?? undefined,
|
|
3367
|
-
rootSessionID: row.root_session_id ?? undefined,
|
|
3368
|
-
lineageDepth: row.lineage_depth ?? undefined,
|
|
3369
|
-
pinned: Boolean(row.pinned),
|
|
3370
|
-
pinReason: row.pin_reason ?? undefined,
|
|
3371
|
-
updatedAt: row.updated_at,
|
|
3372
|
-
compactedAt: row.compacted_at ?? undefined,
|
|
3373
|
-
deleted: Boolean(row.deleted),
|
|
3374
|
-
eventCount: row.event_count,
|
|
3375
|
-
messages,
|
|
3376
|
-
};
|
|
3478
|
+
return this.materializeSessionRow(row, messages);
|
|
3377
3479
|
});
|
|
3378
3480
|
}
|
|
3379
3481
|
readSessionSync(sessionID) {
|
|
@@ -3407,21 +3509,7 @@ export class SqliteLcmStore {
|
|
|
3407
3509
|
if (!row) {
|
|
3408
3510
|
return { ...emptySession(sessionID), messages };
|
|
3409
3511
|
}
|
|
3410
|
-
return
|
|
3411
|
-
sessionID: row.session_id,
|
|
3412
|
-
title: row.title ?? undefined,
|
|
3413
|
-
directory: row.session_directory ?? undefined,
|
|
3414
|
-
parentSessionID: row.parent_session_id ?? undefined,
|
|
3415
|
-
rootSessionID: row.root_session_id ?? undefined,
|
|
3416
|
-
lineageDepth: row.lineage_depth ?? undefined,
|
|
3417
|
-
pinned: Boolean(row.pinned),
|
|
3418
|
-
pinReason: row.pin_reason ?? undefined,
|
|
3419
|
-
updatedAt: row.updated_at,
|
|
3420
|
-
compactedAt: row.compacted_at ?? undefined,
|
|
3421
|
-
deleted: Boolean(row.deleted),
|
|
3422
|
-
eventCount: row.event_count,
|
|
3423
|
-
messages,
|
|
3424
|
-
};
|
|
3512
|
+
return this.materializeSessionRow(row, messages);
|
|
3425
3513
|
}
|
|
3426
3514
|
prepareSessionForPersistence(session) {
|
|
3427
3515
|
const parentSessionID = this.sanitizeParentSessionIDSync(session.sessionID, session.parentSessionID);
|