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.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
- if (typeof this.pendingPartUpdateTimer === 'object' &&
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
- switch (event.type) {
540
- case 'message.part.updated': {
541
- const key = getDeferredPartUpdateKey(event);
542
- if (!key)
543
- return await this.capture(event);
544
- this.pendingPartUpdates.set(key, event);
545
- this.scheduleDeferredPartUpdateFlush();
546
- return;
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
- case 'message.part.removed':
549
- this.clearDeferredPartUpdateForPart(event.properties.sessionID, event.properties.messageID, event.properties.partID);
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
- if (this.pendingPartUpdateFlushPromise)
564
- return this.pendingPartUpdateFlushPromise;
565
- if (this.pendingPartUpdates.size === 0)
566
- return;
567
- this.clearDeferredPartUpdateTimer();
568
- this.pendingPartUpdateFlushPromise = (async () => {
569
- while (this.pendingPartUpdates.size > 0) {
570
- const batch = [...this.pendingPartUpdates.values()];
571
- this.pendingPartUpdates.clear();
572
- for (const event of batch) {
573
- await this.capture(event);
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
- })().finally(() => {
577
- this.pendingPartUpdateFlushPromise = undefined;
578
- if (this.pendingPartUpdates.size > 0)
579
- this.scheduleDeferredPartUpdateFlush();
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.db)
585
- return;
586
- if (this.dbReadyPromise)
587
- return this.dbReadyPromise;
588
- this.dbReadyPromise = this.openAndInitializeDb();
589
- return this.dbReadyPromise;
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
- await this.ensureDbReady();
830
- const normalized = normalizeEvent(event);
831
- if (!normalized)
832
- return;
833
- if (this.shouldRecordEvent(normalized.type)) {
834
- this.writeEvent(normalized);
835
- }
836
- if (!normalized.sessionID)
837
- return;
838
- if (!this.shouldPersistSessionForEvent(normalized.type))
839
- return;
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
- if (shouldSyncDerivedState) {
863
- this.syncDerivedSessionStateSync(this.readSessionSync(normalized.sessionID));
864
- }
865
- if (this.shouldSyncDerivedLineageSubtree(normalized.type, previousParentSessionID, next.parentSessionID)) {
866
- this.syncDerivedLineageSubtreeSync(normalized.sessionID, true);
867
- }
868
- if (this.shouldCleanupOrphanBlobsForEvent(normalized.type)) {
869
- this.deleteOrphanArtifactBlobsSync();
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
- await this.prepareForRead();
1868
- const apply = input?.apply ?? false;
1869
- const vacuum = input?.vacuum ?? true;
1870
- const limit = clamp(input?.limit ?? 10, 1, 50);
1871
- const candidates = this.readPrunableEventTypeCountsSync();
1872
- const candidateEvents = candidates.reduce((sum, row) => sum + row.count, 0);
1873
- const beforeSizes = await this.readStoreFileSizes();
1874
- if (!apply || candidateEvents === 0) {
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
- `apply=false`,
1994
+ `deleted_events=${candidateEvents}`,
1995
+ `apply=true`,
1878
1996
  `vacuum_requested=${vacuum}`,
1879
- `db_bytes=${beforeSizes.dbBytes}`,
1880
- `wal_bytes=${beforeSizes.walBytes}`,
1881
- `shm_bytes=${beforeSizes.shmBytes}`,
1882
- `total_bytes=${beforeSizes.totalBytes}`,
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
- 'candidate_event_types:',
2008
+ 'deleted_event_types:',
1886
2009
  ...candidates.slice(0, limit).map((row) => `- ${row.eventType} count=${row.count}`),
1887
2010
  ]
1888
- : ['candidate_event_types:', '- none']),
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
- await this.prepareForRead();
2055
- return exportStoreSnapshot({
2056
- workspaceDirectory: this.workspaceDirectory,
2057
- normalizeScope: this.normalizeScope.bind(this),
2058
- resolveScopeSessionIDs: this.resolveScopeSessionIDs.bind(this),
2059
- readScopedSessionRowsSync: this.readScopedSessionRowsSync.bind(this),
2060
- readScopedMessageRowsSync: this.readScopedMessageRowsSync.bind(this),
2061
- readScopedPartRowsSync: this.readScopedPartRowsSync.bind(this),
2062
- readScopedResumeRowsSync: this.readScopedResumeRowsSync.bind(this),
2063
- readScopedArtifactRowsSync: this.readScopedArtifactRowsSync.bind(this),
2064
- readScopedArtifactBlobRowsSync: this.readScopedArtifactBlobRowsSync.bind(this),
2065
- readScopedSummaryRowsSync: this.readScopedSummaryRowsSync.bind(this),
2066
- readScopedSummaryEdgeRowsSync: this.readScopedSummaryEdgeRowsSync.bind(this),
2067
- readScopedSummaryStateRowsSync: this.readScopedSummaryStateRowsSync.bind(this),
2068
- }, input);
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
- await this.prepareForRead();
2072
- return importStoreSnapshot({
2073
- workspaceDirectory: this.workspaceDirectory,
2074
- getDb: () => this.getDb(),
2075
- clearSessionDataSync: this.clearSessionDataSync.bind(this),
2076
- backfillArtifactBlobsSync: this.backfillArtifactBlobsSync.bind(this),
2077
- refreshAllLineageSync: this.refreshAllLineageSync.bind(this),
2078
- syncAllDerivedSessionStateSync: this.syncAllDerivedSessionStateSync.bind(this),
2079
- refreshSearchIndexesSync: this.refreshSearchIndexesSync.bind(this),
2080
- }, input);
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
- await this.prepareForRead();
2084
- const resolvedSessionID = sessionID ?? this.latestSessionIDSync();
2085
- if (!resolvedSessionID)
2086
- return 'No stored resume snapshots yet.';
2087
- const existing = this.getResumeSync(resolvedSessionID);
2088
- if (existing && !this.isManagedResumeNote(existing))
2089
- return existing;
2090
- const generated = await this.buildCompactionContext(resolvedSessionID);
2091
- return generated ?? existing ?? 'No stored resume snapshot for that session.';
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
- await this.prepareForRead();
2149
- if (messages.length < this.options.minMessagesForTransform)
2150
- return false;
2151
- const window = resolveArchiveTransformWindow(messages, this.options.freshTailMessages);
2152
- if (!window)
2153
- return false;
2154
- const { anchor, archived, recent } = window;
2155
- const roots = this.ensureSummaryGraphSync(anchor.info.sessionID, archived);
2156
- if (roots.length === 0)
2157
- return false;
2158
- const summary = buildActiveSummaryText(roots, archived.length, this.options.summaryCharBudget);
2159
- const retrieval = await this.buildAutomaticRetrievalContext(anchor.info.sessionID, recent, anchor);
2160
- for (const message of archived) {
2161
- this.compactMessageInPlace(message);
2162
- }
2163
- anchor.parts = anchor.parts.filter((part) => !isSyntheticLcmTextPart(part, ['archive-summary', 'retrieved-context']));
2164
- const syntheticParts = [];
2165
- if (retrieval) {
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-memory-${randomUUID().replace(/-/g, '').slice(0, 12)}`,
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: retrieval,
2275
+ text: summary,
2172
2276
  synthetic: true,
2173
- metadata: { opencodeLcm: 'retrieved-context' },
2277
+ metadata: { opencodeLcm: 'archive-summary' },
2174
2278
  });
2175
- }
2176
- syntheticParts.push({
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 parent = this.getDb()
3136
- .prepare('SELECT root_session_id, lineage_depth FROM sessions WHERE session_id = ?')
3137
- .get(parentSessionID);
3138
- if (!parent)
3139
- return { rootSessionID: parentSessionID, lineageDepth: 1 };
3140
- return {
3141
- rootSessionID: parent.root_session_id ?? parentSessionID,
3142
- lineageDepth: (parent.lineage_depth ?? 0) + 1,
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
- readSessionHeaderSync(sessionID) {
3204
- const row = safeQueryOne(this.getDb().prepare('SELECT * FROM sessions WHERE session_id = ?'), [sessionID], 'readSessionHeaderSync');
3205
- if (!row)
3206
- return undefined;
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: row.parent_session_id ?? undefined,
3212
- rootSessionID: row.root_session_id ?? undefined,
3213
- lineageDepth: row.lineage_depth ?? undefined,
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);