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/src/store.ts
CHANGED
|
@@ -528,6 +528,12 @@ function logStartupPhase(phase: string, context?: Record<string, unknown>): void
|
|
|
528
528
|
getLogger().info(`startup phase: ${phase}`, context);
|
|
529
529
|
}
|
|
530
530
|
|
|
531
|
+
function unrefTimer(timer: ReturnType<typeof setTimeout> | undefined): void {
|
|
532
|
+
if (typeof timer === 'object' && timer && 'unref' in timer && typeof timer.unref === 'function') {
|
|
533
|
+
timer.unref();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
531
537
|
type SqliteRuntime = 'bun' | 'node';
|
|
532
538
|
type SqliteRuntimeOptions = {
|
|
533
539
|
envOverride?: string | undefined;
|
|
@@ -707,6 +713,10 @@ export class SqliteLcmStore {
|
|
|
707
713
|
private readonly workspaceDirectory: string;
|
|
708
714
|
private db?: SqlDatabaseLike;
|
|
709
715
|
private dbReadyPromise?: Promise<void>;
|
|
716
|
+
private deferredInitTimer?: ReturnType<typeof setTimeout>;
|
|
717
|
+
private deferredInitPromise?: Promise<void>;
|
|
718
|
+
private deferredInitRequested = false;
|
|
719
|
+
private activeOperationCount = 0;
|
|
710
720
|
private readonly pendingPartUpdates = new Map<string, Event>();
|
|
711
721
|
private pendingPartUpdateTimer?: ReturnType<typeof setTimeout>;
|
|
712
722
|
private pendingPartUpdateFlushPromise?: Promise<void>;
|
|
@@ -725,8 +735,27 @@ export class SqliteLcmStore {
|
|
|
725
735
|
await mkdir(this.baseDir, { recursive: true });
|
|
726
736
|
}
|
|
727
737
|
|
|
738
|
+
// Keep deferred SQLite maintenance off the active connection while a store operation is running.
|
|
739
|
+
private async withStoreActivity<T>(operation: () => Promise<T>): Promise<T> {
|
|
740
|
+
this.activeOperationCount += 1;
|
|
741
|
+
try {
|
|
742
|
+
return await operation();
|
|
743
|
+
} finally {
|
|
744
|
+
this.activeOperationCount -= 1;
|
|
745
|
+
if (this.activeOperationCount === 0 && this.deferredInitRequested) {
|
|
746
|
+
this.scheduleDeferredInit();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
private async waitForDeferredInitIfRunning(): Promise<void> {
|
|
752
|
+
if (!this.deferredInitPromise) return;
|
|
753
|
+
await this.deferredInitPromise;
|
|
754
|
+
}
|
|
755
|
+
|
|
728
756
|
private async prepareForRead(): Promise<void> {
|
|
729
757
|
await this.ensureDbReady();
|
|
758
|
+
await this.waitForDeferredInitIfRunning();
|
|
730
759
|
await this.flushDeferredPartUpdates();
|
|
731
760
|
}
|
|
732
761
|
|
|
@@ -737,15 +766,7 @@ export class SqliteLcmStore {
|
|
|
737
766
|
this.pendingPartUpdateTimer = undefined;
|
|
738
767
|
void this.flushDeferredPartUpdates();
|
|
739
768
|
}, SqliteLcmStore.deferredPartUpdateDelayMs);
|
|
740
|
-
|
|
741
|
-
if (
|
|
742
|
-
typeof this.pendingPartUpdateTimer === 'object' &&
|
|
743
|
-
this.pendingPartUpdateTimer &&
|
|
744
|
-
'unref' in this.pendingPartUpdateTimer &&
|
|
745
|
-
typeof this.pendingPartUpdateTimer.unref === 'function'
|
|
746
|
-
) {
|
|
747
|
-
this.pendingPartUpdateTimer.unref();
|
|
748
|
-
}
|
|
769
|
+
unrefTimer(this.pendingPartUpdateTimer);
|
|
749
770
|
}
|
|
750
771
|
|
|
751
772
|
private clearDeferredPartUpdateTimer(): void {
|
|
@@ -790,63 +811,73 @@ export class SqliteLcmStore {
|
|
|
790
811
|
}
|
|
791
812
|
|
|
792
813
|
async captureDeferred(event: Event): Promise<void> {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
814
|
+
return this.withStoreActivity(async () => {
|
|
815
|
+
switch (event.type) {
|
|
816
|
+
case 'message.part.updated': {
|
|
817
|
+
const key = getDeferredPartUpdateKey(event);
|
|
818
|
+
if (!key) return await this.capture(event);
|
|
819
|
+
this.pendingPartUpdates.set(key, event);
|
|
820
|
+
this.scheduleDeferredPartUpdateFlush();
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
case 'message.part.removed':
|
|
824
|
+
this.clearDeferredPartUpdateForPart(
|
|
825
|
+
event.properties.sessionID,
|
|
826
|
+
event.properties.messageID,
|
|
827
|
+
event.properties.partID,
|
|
828
|
+
);
|
|
829
|
+
break;
|
|
830
|
+
case 'message.removed':
|
|
831
|
+
this.clearDeferredPartUpdatesForMessage(
|
|
832
|
+
event.properties.sessionID,
|
|
833
|
+
event.properties.messageID,
|
|
834
|
+
);
|
|
835
|
+
break;
|
|
836
|
+
case 'session.deleted':
|
|
837
|
+
this.clearDeferredPartUpdatesForSession(extractSessionID(event));
|
|
838
|
+
break;
|
|
839
|
+
default:
|
|
840
|
+
break;
|
|
800
841
|
}
|
|
801
|
-
case 'message.part.removed':
|
|
802
|
-
this.clearDeferredPartUpdateForPart(
|
|
803
|
-
event.properties.sessionID,
|
|
804
|
-
event.properties.messageID,
|
|
805
|
-
event.properties.partID,
|
|
806
|
-
);
|
|
807
|
-
break;
|
|
808
|
-
case 'message.removed':
|
|
809
|
-
this.clearDeferredPartUpdatesForMessage(
|
|
810
|
-
event.properties.sessionID,
|
|
811
|
-
event.properties.messageID,
|
|
812
|
-
);
|
|
813
|
-
break;
|
|
814
|
-
case 'session.deleted':
|
|
815
|
-
this.clearDeferredPartUpdatesForSession(extractSessionID(event));
|
|
816
|
-
break;
|
|
817
|
-
default:
|
|
818
|
-
break;
|
|
819
|
-
}
|
|
820
842
|
|
|
821
|
-
|
|
843
|
+
await this.capture(event);
|
|
844
|
+
});
|
|
822
845
|
}
|
|
823
846
|
|
|
824
847
|
async flushDeferredPartUpdates(): Promise<void> {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
848
|
+
return this.withStoreActivity(async () => {
|
|
849
|
+
if (this.pendingPartUpdateFlushPromise) return this.pendingPartUpdateFlushPromise;
|
|
850
|
+
if (this.pendingPartUpdates.size === 0) return;
|
|
851
|
+
|
|
852
|
+
this.clearDeferredPartUpdateTimer();
|
|
853
|
+
this.pendingPartUpdateFlushPromise = (async () => {
|
|
854
|
+
while (this.pendingPartUpdates.size > 0) {
|
|
855
|
+
const batch = [...this.pendingPartUpdates.values()];
|
|
856
|
+
this.pendingPartUpdates.clear();
|
|
857
|
+
for (const event of batch) {
|
|
858
|
+
await this.capture(event);
|
|
859
|
+
}
|
|
835
860
|
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
});
|
|
861
|
+
})().finally(() => {
|
|
862
|
+
this.pendingPartUpdateFlushPromise = undefined;
|
|
863
|
+
if (this.pendingPartUpdates.size > 0) this.scheduleDeferredPartUpdateFlush();
|
|
864
|
+
});
|
|
841
865
|
|
|
842
|
-
|
|
866
|
+
return this.pendingPartUpdateFlushPromise;
|
|
867
|
+
});
|
|
843
868
|
}
|
|
844
869
|
|
|
845
870
|
private async ensureDbReady(): Promise<void> {
|
|
846
|
-
if (this.
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
871
|
+
if (!this.dbReadyPromise) {
|
|
872
|
+
if (this.db) {
|
|
873
|
+
this.scheduleDeferredInit();
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
this.dbReadyPromise = this.openAndInitializeDb();
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
await this.dbReadyPromise;
|
|
880
|
+
this.scheduleDeferredInit();
|
|
850
881
|
}
|
|
851
882
|
|
|
852
883
|
private async openAndInitializeDb(): Promise<void> {
|
|
@@ -1024,8 +1055,6 @@ export class SqliteLcmStore {
|
|
|
1024
1055
|
await this.migrateLegacyArtifacts();
|
|
1025
1056
|
logStartupPhase('open-db:write-schema-version', { schemaVersion: STORE_SCHEMA_VERSION });
|
|
1026
1057
|
this.writeSchemaVersionSync(STORE_SCHEMA_VERSION);
|
|
1027
|
-
logStartupPhase('open-db:deferred-init:start');
|
|
1028
|
-
this.completeDeferredInit();
|
|
1029
1058
|
logStartupPhase('open-db:ready');
|
|
1030
1059
|
} catch (error) {
|
|
1031
1060
|
logStartupPhase('open-db:error', {
|
|
@@ -1040,6 +1069,59 @@ export class SqliteLcmStore {
|
|
|
1040
1069
|
|
|
1041
1070
|
private deferredInitCompleted = false;
|
|
1042
1071
|
|
|
1072
|
+
private runDeferredInit(): Promise<void> {
|
|
1073
|
+
if (this.deferredInitCompleted) return Promise.resolve();
|
|
1074
|
+
if (this.deferredInitPromise) return this.deferredInitPromise;
|
|
1075
|
+
|
|
1076
|
+
this.deferredInitPromise = this.withStoreActivity(async () => {
|
|
1077
|
+
this.deferredInitRequested = false;
|
|
1078
|
+
logStartupPhase('deferred-init:start');
|
|
1079
|
+
this.completeDeferredInit();
|
|
1080
|
+
})
|
|
1081
|
+
.catch((error) => {
|
|
1082
|
+
getLogger().warn('Deferred LCM maintenance failed', {
|
|
1083
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1084
|
+
});
|
|
1085
|
+
})
|
|
1086
|
+
.finally(() => {
|
|
1087
|
+
this.deferredInitPromise = undefined;
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
return this.deferredInitPromise;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
private scheduleDeferredInit(): void {
|
|
1094
|
+
if (!this.db || this.deferredInitCompleted || this.deferredInitPromise) {
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
this.deferredInitRequested = true;
|
|
1099
|
+
if (this.activeOperationCount > 0 || this.deferredInitTimer) return;
|
|
1100
|
+
|
|
1101
|
+
logStartupPhase('deferred-init:scheduled');
|
|
1102
|
+
this.deferredInitTimer = setTimeout(() => {
|
|
1103
|
+
this.deferredInitTimer = undefined;
|
|
1104
|
+
if (this.activeOperationCount > 0) {
|
|
1105
|
+
this.scheduleDeferredInit();
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
void this.runDeferredInit();
|
|
1109
|
+
}, 0);
|
|
1110
|
+
unrefTimer(this.deferredInitTimer);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
private async ensureDeferredInitComplete(): Promise<void> {
|
|
1114
|
+
await this.ensureDbReady();
|
|
1115
|
+
if (this.deferredInitCompleted) return;
|
|
1116
|
+
|
|
1117
|
+
if (this.deferredInitTimer) {
|
|
1118
|
+
clearTimeout(this.deferredInitTimer);
|
|
1119
|
+
this.deferredInitTimer = undefined;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
await this.runDeferredInit();
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1043
1125
|
private readSchemaVersionSync(): number {
|
|
1044
1126
|
return firstFiniteNumber(this.getDb().prepare('PRAGMA user_version').get()) ?? 0;
|
|
1045
1127
|
}
|
|
@@ -1101,6 +1183,10 @@ export class SqliteLcmStore {
|
|
|
1101
1183
|
close(): void {
|
|
1102
1184
|
this.clearDeferredPartUpdateTimer();
|
|
1103
1185
|
this.pendingPartUpdates.clear();
|
|
1186
|
+
if (this.deferredInitTimer) {
|
|
1187
|
+
clearTimeout(this.deferredInitTimer);
|
|
1188
|
+
this.deferredInitTimer = undefined;
|
|
1189
|
+
}
|
|
1104
1190
|
if (!this.db) return;
|
|
1105
1191
|
this.db.close();
|
|
1106
1192
|
this.db = undefined;
|
|
@@ -1108,65 +1194,75 @@ export class SqliteLcmStore {
|
|
|
1108
1194
|
}
|
|
1109
1195
|
|
|
1110
1196
|
async capture(event: Event): Promise<void> {
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
if (!normalized) return;
|
|
1197
|
+
return this.withStoreActivity(async () => {
|
|
1198
|
+
const normalized = normalizeEvent(event);
|
|
1199
|
+
if (!normalized) return;
|
|
1115
1200
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1201
|
+
const shouldRecord = this.shouldRecordEvent(normalized.type);
|
|
1202
|
+
const shouldPersistSession =
|
|
1203
|
+
Boolean(normalized.sessionID) && this.shouldPersistSessionForEvent(normalized.type);
|
|
1204
|
+
if (!shouldRecord && !shouldPersistSession) return;
|
|
1119
1205
|
|
|
1120
|
-
|
|
1121
|
-
if (!this.shouldPersistSessionForEvent(normalized.type)) return;
|
|
1206
|
+
await this.ensureDeferredInitComplete();
|
|
1122
1207
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
: this.readSessionSync(normalized.sessionID);
|
|
1127
|
-
const previousParentSessionID = session.parentSessionID;
|
|
1128
|
-
const shouldSyncDerivedState = this.shouldSyncDerivedSessionStateForEvent(session, normalized);
|
|
1129
|
-
let next = this.applyEvent(session, normalized);
|
|
1130
|
-
next.updatedAt = Math.max(next.updatedAt, normalized.timestamp);
|
|
1131
|
-
next.eventCount += 1;
|
|
1132
|
-
next = this.prepareSessionForPersistence(next);
|
|
1208
|
+
if (shouldRecord) {
|
|
1209
|
+
this.writeEvent(normalized);
|
|
1210
|
+
}
|
|
1133
1211
|
|
|
1134
|
-
|
|
1212
|
+
if (!normalized.sessionID || !shouldPersistSession) return;
|
|
1135
1213
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1214
|
+
const session =
|
|
1215
|
+
resolveCaptureHydrationMode() === 'targeted'
|
|
1216
|
+
? this.readSessionForCaptureSync(normalized)
|
|
1217
|
+
: this.readSessionSync(normalized.sessionID);
|
|
1218
|
+
const previousParentSessionID = session.parentSessionID;
|
|
1219
|
+
const shouldSyncDerivedState = this.shouldSyncDerivedSessionStateForEvent(
|
|
1220
|
+
session,
|
|
1221
|
+
normalized,
|
|
1222
|
+
);
|
|
1223
|
+
let next = this.applyEvent(session, normalized);
|
|
1224
|
+
next.updatedAt = Math.max(next.updatedAt, normalized.timestamp);
|
|
1225
|
+
next.eventCount += 1;
|
|
1226
|
+
next = this.prepareSessionForPersistence(next);
|
|
1227
|
+
|
|
1228
|
+
await this.persistCapturedSession(next, normalized);
|
|
1229
|
+
|
|
1230
|
+
if (this.shouldRefreshLineageForEvent(normalized.type)) {
|
|
1231
|
+
this.refreshAllLineageSync();
|
|
1232
|
+
const refreshed = this.readSessionHeaderSync(normalized.sessionID);
|
|
1233
|
+
if (refreshed) {
|
|
1234
|
+
next = {
|
|
1235
|
+
...next,
|
|
1236
|
+
parentSessionID: refreshed.parentSessionID,
|
|
1237
|
+
rootSessionID: refreshed.rootSessionID,
|
|
1238
|
+
lineageDepth: refreshed.lineageDepth,
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1146
1241
|
}
|
|
1147
|
-
}
|
|
1148
1242
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1243
|
+
if (shouldSyncDerivedState) {
|
|
1244
|
+
this.syncDerivedSessionStateSync(this.readSessionSync(normalized.sessionID));
|
|
1245
|
+
}
|
|
1152
1246
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1247
|
+
if (
|
|
1248
|
+
this.shouldSyncDerivedLineageSubtree(
|
|
1249
|
+
normalized.type,
|
|
1250
|
+
previousParentSessionID,
|
|
1251
|
+
next.parentSessionID,
|
|
1252
|
+
)
|
|
1253
|
+
) {
|
|
1254
|
+
this.syncDerivedLineageSubtreeSync(normalized.sessionID, true);
|
|
1255
|
+
}
|
|
1162
1256
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1257
|
+
if (this.shouldCleanupOrphanBlobsForEvent(normalized.type)) {
|
|
1258
|
+
this.deleteOrphanArtifactBlobsSync();
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1166
1261
|
}
|
|
1167
1262
|
|
|
1168
1263
|
async stats(): Promise<StoreStats> {
|
|
1169
1264
|
await this.prepareForRead();
|
|
1265
|
+
await this.ensureDeferredInitComplete();
|
|
1170
1266
|
const db = this.getDb();
|
|
1171
1267
|
const totalRow = validateRow<{ count: number; latest: number | null }>(
|
|
1172
1268
|
db.prepare('SELECT COUNT(*) AS count, MAX(ts) AS latest FROM events').get(),
|
|
@@ -1283,13 +1379,14 @@ export class SqliteLcmStore {
|
|
|
1283
1379
|
scope?: string;
|
|
1284
1380
|
limit?: number;
|
|
1285
1381
|
}): Promise<SearchResult[]> {
|
|
1286
|
-
await this.prepareForRead();
|
|
1287
1382
|
const resolvedScope = this.resolveConfiguredScope('grep', input.scope, input.sessionID);
|
|
1288
|
-
const sessionIDs = this.resolveScopeSessionIDs(resolvedScope, input.sessionID);
|
|
1289
1383
|
const limit = input.limit ?? 5;
|
|
1290
1384
|
const needle = input.query.trim();
|
|
1291
1385
|
if (!needle) return [];
|
|
1292
1386
|
|
|
1387
|
+
await this.prepareForRead();
|
|
1388
|
+
const sessionIDs = this.resolveScopeSessionIDs(resolvedScope, input.sessionID);
|
|
1389
|
+
|
|
1293
1390
|
const ftsResults = this.searchWithFts(needle, sessionIDs, limit);
|
|
1294
1391
|
if (ftsResults.length > 0) return ftsResults;
|
|
1295
1392
|
return this.searchByScan(needle.toLowerCase(), sessionIDs, limit);
|
|
@@ -2516,71 +2613,73 @@ export class SqliteLcmStore {
|
|
|
2516
2613
|
vacuum?: boolean;
|
|
2517
2614
|
limit?: number;
|
|
2518
2615
|
}): Promise<string> {
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2616
|
+
return this.withStoreActivity(async () => {
|
|
2617
|
+
await this.prepareForRead();
|
|
2618
|
+
const apply = input?.apply ?? false;
|
|
2619
|
+
const vacuum = input?.vacuum ?? true;
|
|
2620
|
+
const limit = clamp(input?.limit ?? 10, 1, 50);
|
|
2621
|
+
const candidates = this.readPrunableEventTypeCountsSync();
|
|
2622
|
+
const candidateEvents = candidates.reduce((sum, row) => sum + row.count, 0);
|
|
2623
|
+
const beforeSizes = await this.readStoreFileSizes();
|
|
2624
|
+
|
|
2625
|
+
if (!apply || candidateEvents === 0) {
|
|
2626
|
+
return [
|
|
2627
|
+
`candidate_events=${candidateEvents}`,
|
|
2628
|
+
`apply=false`,
|
|
2629
|
+
`vacuum_requested=${vacuum}`,
|
|
2630
|
+
`db_bytes=${beforeSizes.dbBytes}`,
|
|
2631
|
+
`wal_bytes=${beforeSizes.walBytes}`,
|
|
2632
|
+
`shm_bytes=${beforeSizes.shmBytes}`,
|
|
2633
|
+
`total_bytes=${beforeSizes.totalBytes}`,
|
|
2634
|
+
...(candidates.length > 0
|
|
2635
|
+
? [
|
|
2636
|
+
'candidate_event_types:',
|
|
2637
|
+
...candidates.slice(0, limit).map((row) => `- ${row.eventType} count=${row.count}`),
|
|
2638
|
+
]
|
|
2639
|
+
: ['candidate_event_types:', '- none']),
|
|
2640
|
+
].join('\n');
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
const eventTypes = candidates.map((row) => row.eventType);
|
|
2644
|
+
if (eventTypes.length > 0) {
|
|
2645
|
+
const placeholders = eventTypes.map(() => '?').join(', ');
|
|
2646
|
+
this.getDb()
|
|
2647
|
+
.prepare(`DELETE FROM events WHERE event_type IN (${placeholders})`)
|
|
2648
|
+
.run(...eventTypes);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
let vacuumApplied = false;
|
|
2652
|
+
this.getDb().exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
2653
|
+
if (vacuum) {
|
|
2654
|
+
this.getDb().exec('VACUUM');
|
|
2655
|
+
this.getDb().exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
2656
|
+
vacuumApplied = true;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
const afterSizes = await this.readStoreFileSizes();
|
|
2526
2660
|
|
|
2527
|
-
if (!apply || candidateEvents === 0) {
|
|
2528
2661
|
return [
|
|
2529
2662
|
`candidate_events=${candidateEvents}`,
|
|
2530
|
-
`
|
|
2663
|
+
`deleted_events=${candidateEvents}`,
|
|
2664
|
+
`apply=true`,
|
|
2531
2665
|
`vacuum_requested=${vacuum}`,
|
|
2532
|
-
`
|
|
2533
|
-
`
|
|
2534
|
-
`
|
|
2535
|
-
`
|
|
2666
|
+
`vacuum_applied=${vacuumApplied}`,
|
|
2667
|
+
`db_bytes_before=${beforeSizes.dbBytes}`,
|
|
2668
|
+
`wal_bytes_before=${beforeSizes.walBytes}`,
|
|
2669
|
+
`shm_bytes_before=${beforeSizes.shmBytes}`,
|
|
2670
|
+
`total_bytes_before=${beforeSizes.totalBytes}`,
|
|
2671
|
+
`db_bytes_after=${afterSizes.dbBytes}`,
|
|
2672
|
+
`wal_bytes_after=${afterSizes.walBytes}`,
|
|
2673
|
+
`shm_bytes_after=${afterSizes.shmBytes}`,
|
|
2674
|
+
`total_bytes_after=${afterSizes.totalBytes}`,
|
|
2536
2675
|
...(candidates.length > 0
|
|
2537
2676
|
? [
|
|
2538
|
-
'
|
|
2677
|
+
'deleted_event_types:',
|
|
2539
2678
|
...candidates.slice(0, limit).map((row) => `- ${row.eventType} count=${row.count}`),
|
|
2540
2679
|
]
|
|
2541
|
-
: ['
|
|
2680
|
+
: ['deleted_event_types:', '- none']),
|
|
2542
2681
|
].join('\n');
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
const eventTypes = candidates.map((row) => row.eventType);
|
|
2546
|
-
if (eventTypes.length > 0) {
|
|
2547
|
-
const placeholders = eventTypes.map(() => '?').join(', ');
|
|
2548
|
-
this.getDb()
|
|
2549
|
-
.prepare(`DELETE FROM events WHERE event_type IN (${placeholders})`)
|
|
2550
|
-
.run(...eventTypes);
|
|
2551
|
-
}
|
|
2552
|
-
|
|
2553
|
-
let vacuumApplied = false;
|
|
2554
|
-
this.getDb().exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
2555
|
-
if (vacuum) {
|
|
2556
|
-
this.getDb().exec('VACUUM');
|
|
2557
|
-
this.getDb().exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
2558
|
-
vacuumApplied = true;
|
|
2559
|
-
}
|
|
2560
|
-
|
|
2561
|
-
const afterSizes = await this.readStoreFileSizes();
|
|
2562
|
-
|
|
2563
|
-
return [
|
|
2564
|
-
`candidate_events=${candidateEvents}`,
|
|
2565
|
-
`deleted_events=${candidateEvents}`,
|
|
2566
|
-
`apply=true`,
|
|
2567
|
-
`vacuum_requested=${vacuum}`,
|
|
2568
|
-
`vacuum_applied=${vacuumApplied}`,
|
|
2569
|
-
`db_bytes_before=${beforeSizes.dbBytes}`,
|
|
2570
|
-
`wal_bytes_before=${beforeSizes.walBytes}`,
|
|
2571
|
-
`shm_bytes_before=${beforeSizes.shmBytes}`,
|
|
2572
|
-
`total_bytes_before=${beforeSizes.totalBytes}`,
|
|
2573
|
-
`db_bytes_after=${afterSizes.dbBytes}`,
|
|
2574
|
-
`wal_bytes_after=${afterSizes.walBytes}`,
|
|
2575
|
-
`shm_bytes_after=${afterSizes.shmBytes}`,
|
|
2576
|
-
`total_bytes_after=${afterSizes.totalBytes}`,
|
|
2577
|
-
...(candidates.length > 0
|
|
2578
|
-
? [
|
|
2579
|
-
'deleted_event_types:',
|
|
2580
|
-
...candidates.slice(0, limit).map((row) => `- ${row.eventType} count=${row.count}`),
|
|
2581
|
-
]
|
|
2582
|
-
: ['deleted_event_types:', '- none']),
|
|
2583
|
-
].join('\n');
|
|
2682
|
+
});
|
|
2584
2683
|
}
|
|
2585
2684
|
|
|
2586
2685
|
async retentionReport(input?: {
|
|
@@ -2750,24 +2849,26 @@ export class SqliteLcmStore {
|
|
|
2750
2849
|
sessionID?: string;
|
|
2751
2850
|
scope?: string;
|
|
2752
2851
|
}): Promise<string> {
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2852
|
+
return this.withStoreActivity(async () => {
|
|
2853
|
+
await this.prepareForRead();
|
|
2854
|
+
return exportStoreSnapshot(
|
|
2855
|
+
{
|
|
2856
|
+
workspaceDirectory: this.workspaceDirectory,
|
|
2857
|
+
normalizeScope: this.normalizeScope.bind(this),
|
|
2858
|
+
resolveScopeSessionIDs: this.resolveScopeSessionIDs.bind(this),
|
|
2859
|
+
readScopedSessionRowsSync: this.readScopedSessionRowsSync.bind(this),
|
|
2860
|
+
readScopedMessageRowsSync: this.readScopedMessageRowsSync.bind(this),
|
|
2861
|
+
readScopedPartRowsSync: this.readScopedPartRowsSync.bind(this),
|
|
2862
|
+
readScopedResumeRowsSync: this.readScopedResumeRowsSync.bind(this),
|
|
2863
|
+
readScopedArtifactRowsSync: this.readScopedArtifactRowsSync.bind(this),
|
|
2864
|
+
readScopedArtifactBlobRowsSync: this.readScopedArtifactBlobRowsSync.bind(this),
|
|
2865
|
+
readScopedSummaryRowsSync: this.readScopedSummaryRowsSync.bind(this),
|
|
2866
|
+
readScopedSummaryEdgeRowsSync: this.readScopedSummaryEdgeRowsSync.bind(this),
|
|
2867
|
+
readScopedSummaryStateRowsSync: this.readScopedSummaryStateRowsSync.bind(this),
|
|
2868
|
+
},
|
|
2869
|
+
input,
|
|
2870
|
+
);
|
|
2871
|
+
});
|
|
2771
2872
|
}
|
|
2772
2873
|
|
|
2773
2874
|
async importSnapshot(input: {
|
|
@@ -2775,31 +2876,35 @@ export class SqliteLcmStore {
|
|
|
2775
2876
|
mode?: 'replace' | 'merge';
|
|
2776
2877
|
worktreeMode?: SnapshotWorktreeMode;
|
|
2777
2878
|
}): Promise<string> {
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2879
|
+
return this.withStoreActivity(async () => {
|
|
2880
|
+
await this.prepareForRead();
|
|
2881
|
+
return importStoreSnapshot(
|
|
2882
|
+
{
|
|
2883
|
+
workspaceDirectory: this.workspaceDirectory,
|
|
2884
|
+
getDb: () => this.getDb(),
|
|
2885
|
+
clearSessionDataSync: this.clearSessionDataSync.bind(this),
|
|
2886
|
+
backfillArtifactBlobsSync: this.backfillArtifactBlobsSync.bind(this),
|
|
2887
|
+
refreshAllLineageSync: this.refreshAllLineageSync.bind(this),
|
|
2888
|
+
syncAllDerivedSessionStateSync: this.syncAllDerivedSessionStateSync.bind(this),
|
|
2889
|
+
refreshSearchIndexesSync: this.refreshSearchIndexesSync.bind(this),
|
|
2890
|
+
},
|
|
2891
|
+
input,
|
|
2892
|
+
);
|
|
2893
|
+
});
|
|
2791
2894
|
}
|
|
2792
2895
|
|
|
2793
2896
|
async resume(sessionID?: string): Promise<string> {
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2897
|
+
return this.withStoreActivity(async () => {
|
|
2898
|
+
await this.prepareForRead();
|
|
2899
|
+
const resolvedSessionID = sessionID ?? this.latestSessionIDSync();
|
|
2900
|
+
if (!resolvedSessionID) return 'No stored resume snapshots yet.';
|
|
2797
2901
|
|
|
2798
|
-
|
|
2799
|
-
|
|
2902
|
+
const existing = this.getResumeSync(resolvedSessionID);
|
|
2903
|
+
if (existing && !this.isManagedResumeNote(existing)) return existing;
|
|
2800
2904
|
|
|
2801
|
-
|
|
2802
|
-
|
|
2905
|
+
const generated = await this.buildCompactionContext(resolvedSessionID);
|
|
2906
|
+
return generated ?? existing ?? 'No stored resume snapshot for that session.';
|
|
2907
|
+
});
|
|
2803
2908
|
}
|
|
2804
2909
|
|
|
2805
2910
|
async expand(input: {
|
|
@@ -2883,53 +2988,60 @@ export class SqliteLcmStore {
|
|
|
2883
2988
|
}
|
|
2884
2989
|
|
|
2885
2990
|
async transformMessages(messages: ConversationMessage[]): Promise<boolean> {
|
|
2886
|
-
|
|
2887
|
-
|
|
2991
|
+
return this.withStoreActivity(async () => {
|
|
2992
|
+
if (messages.length < this.options.minMessagesForTransform) return false;
|
|
2888
2993
|
|
|
2889
|
-
|
|
2890
|
-
|
|
2994
|
+
const window = resolveArchiveTransformWindow(messages, this.options.freshTailMessages);
|
|
2995
|
+
if (!window) return false;
|
|
2891
2996
|
|
|
2892
|
-
|
|
2997
|
+
await this.prepareForRead();
|
|
2893
2998
|
|
|
2894
|
-
|
|
2895
|
-
if (roots.length === 0) return false;
|
|
2999
|
+
const { anchor, archived, recent } = window;
|
|
2896
3000
|
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
anchor.info.sessionID,
|
|
2900
|
-
recent,
|
|
2901
|
-
anchor,
|
|
2902
|
-
);
|
|
2903
|
-
for (const message of archived) {
|
|
2904
|
-
this.compactMessageInPlace(message);
|
|
2905
|
-
}
|
|
3001
|
+
const roots = this.ensureSummaryGraphSync(anchor.info.sessionID, archived);
|
|
3002
|
+
if (roots.length === 0) return false;
|
|
2906
3003
|
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
3004
|
+
const summary = buildActiveSummaryText(
|
|
3005
|
+
roots,
|
|
3006
|
+
archived.length,
|
|
3007
|
+
this.options.summaryCharBudget,
|
|
3008
|
+
);
|
|
3009
|
+
const retrieval = await this.buildAutomaticRetrievalContext(
|
|
3010
|
+
anchor.info.sessionID,
|
|
3011
|
+
recent,
|
|
3012
|
+
anchor,
|
|
3013
|
+
);
|
|
3014
|
+
for (const message of archived) {
|
|
3015
|
+
this.compactMessageInPlace(message);
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
anchor.parts = anchor.parts.filter(
|
|
3019
|
+
(part) => !isSyntheticLcmTextPart(part, ['archive-summary', 'retrieved-context']),
|
|
3020
|
+
);
|
|
3021
|
+
const syntheticParts: Part[] = [];
|
|
3022
|
+
if (retrieval) {
|
|
3023
|
+
syntheticParts.push({
|
|
3024
|
+
id: `lcm-memory-${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
|
3025
|
+
sessionID: anchor.info.sessionID,
|
|
3026
|
+
messageID: anchor.info.id,
|
|
3027
|
+
type: 'text',
|
|
3028
|
+
text: retrieval,
|
|
3029
|
+
synthetic: true,
|
|
3030
|
+
metadata: { opencodeLcm: 'retrieved-context' },
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
2912
3033
|
syntheticParts.push({
|
|
2913
|
-
id: `lcm-
|
|
3034
|
+
id: `lcm-summary-${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
|
2914
3035
|
sessionID: anchor.info.sessionID,
|
|
2915
3036
|
messageID: anchor.info.id,
|
|
2916
3037
|
type: 'text',
|
|
2917
|
-
text:
|
|
3038
|
+
text: summary,
|
|
2918
3039
|
synthetic: true,
|
|
2919
|
-
metadata: { opencodeLcm: '
|
|
3040
|
+
metadata: { opencodeLcm: 'archive-summary' },
|
|
2920
3041
|
});
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
id: `lcm-summary-${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
|
2924
|
-
sessionID: anchor.info.sessionID,
|
|
2925
|
-
messageID: anchor.info.id,
|
|
2926
|
-
type: 'text',
|
|
2927
|
-
text: summary,
|
|
2928
|
-
synthetic: true,
|
|
2929
|
-
metadata: { opencodeLcm: 'archive-summary' },
|
|
3042
|
+
anchor.parts.push(...syntheticParts);
|
|
3043
|
+
return true;
|
|
2930
3044
|
});
|
|
2931
|
-
anchor.parts.push(...syntheticParts);
|
|
2932
|
-
return true;
|
|
2933
3045
|
}
|
|
2934
3046
|
|
|
2935
3047
|
systemHint(): string | undefined {
|
|
@@ -4240,17 +4352,40 @@ export class SqliteLcmStore {
|
|
|
4240
4352
|
): { rootSessionID: string; lineageDepth: number } {
|
|
4241
4353
|
if (!parentSessionID) return { rootSessionID: sessionID, lineageDepth: 0 };
|
|
4242
4354
|
|
|
4243
|
-
const
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
| { root_session_id: string | null; lineage_depth: number | null }
|
|
4247
|
-
| undefined;
|
|
4355
|
+
const seen = new Set<string>([sessionID]);
|
|
4356
|
+
let currentSessionID: string | undefined = parentSessionID;
|
|
4357
|
+
let lineageDepth = 1;
|
|
4248
4358
|
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4359
|
+
while (currentSessionID && !seen.has(currentSessionID)) {
|
|
4360
|
+
seen.add(currentSessionID);
|
|
4361
|
+
const parent = this.getDb()
|
|
4362
|
+
.prepare(
|
|
4363
|
+
'SELECT parent_session_id, root_session_id, lineage_depth FROM sessions WHERE session_id = ?',
|
|
4364
|
+
)
|
|
4365
|
+
.get(currentSessionID) as
|
|
4366
|
+
| {
|
|
4367
|
+
parent_session_id: string | null;
|
|
4368
|
+
root_session_id: string | null;
|
|
4369
|
+
lineage_depth: number | null;
|
|
4370
|
+
}
|
|
4371
|
+
| undefined;
|
|
4372
|
+
|
|
4373
|
+
if (!parent) return { rootSessionID: currentSessionID, lineageDepth };
|
|
4374
|
+
if (parent.root_session_id && parent.lineage_depth !== null) {
|
|
4375
|
+
return {
|
|
4376
|
+
rootSessionID: parent.root_session_id,
|
|
4377
|
+
lineageDepth: parent.lineage_depth + lineageDepth,
|
|
4378
|
+
};
|
|
4379
|
+
}
|
|
4380
|
+
if (!parent.parent_session_id) {
|
|
4381
|
+
return { rootSessionID: currentSessionID, lineageDepth };
|
|
4382
|
+
}
|
|
4383
|
+
|
|
4384
|
+
currentSessionID = parent.parent_session_id;
|
|
4385
|
+
lineageDepth += 1;
|
|
4386
|
+
}
|
|
4387
|
+
|
|
4388
|
+
return { rootSessionID: parentSessionID, lineageDepth };
|
|
4254
4389
|
}
|
|
4255
4390
|
|
|
4256
4391
|
private applyEvent(session: NormalizedSession, event: CapturedEvent): NormalizedSession {
|
|
@@ -4322,31 +4457,44 @@ export class SqliteLcmStore {
|
|
|
4322
4457
|
return row?.note;
|
|
4323
4458
|
}
|
|
4324
4459
|
|
|
4325
|
-
private
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4460
|
+
private materializeSessionRow(
|
|
4461
|
+
row: SessionRow,
|
|
4462
|
+
messages: ConversationMessage[] = [],
|
|
4463
|
+
): NormalizedSession {
|
|
4464
|
+
const parentSessionID = row.parent_session_id ?? undefined;
|
|
4465
|
+
const derivedLineage =
|
|
4466
|
+
row.root_session_id === null || row.lineage_depth === null
|
|
4467
|
+
? this.resolveLineageSync(row.session_id, parentSessionID)
|
|
4468
|
+
: undefined;
|
|
4332
4469
|
|
|
4333
4470
|
return {
|
|
4334
4471
|
sessionID: row.session_id,
|
|
4335
4472
|
title: row.title ?? undefined,
|
|
4336
4473
|
directory: row.session_directory ?? undefined,
|
|
4337
|
-
parentSessionID
|
|
4338
|
-
rootSessionID: row.root_session_id ??
|
|
4339
|
-
lineageDepth: row.lineage_depth ??
|
|
4474
|
+
parentSessionID,
|
|
4475
|
+
rootSessionID: row.root_session_id ?? derivedLineage?.rootSessionID,
|
|
4476
|
+
lineageDepth: row.lineage_depth ?? derivedLineage?.lineageDepth,
|
|
4340
4477
|
pinned: Boolean(row.pinned),
|
|
4341
4478
|
pinReason: row.pin_reason ?? undefined,
|
|
4342
4479
|
updatedAt: row.updated_at,
|
|
4343
4480
|
compactedAt: row.compacted_at ?? undefined,
|
|
4344
4481
|
deleted: Boolean(row.deleted),
|
|
4345
4482
|
eventCount: row.event_count,
|
|
4346
|
-
messages
|
|
4483
|
+
messages,
|
|
4347
4484
|
};
|
|
4348
4485
|
}
|
|
4349
4486
|
|
|
4487
|
+
private readSessionHeaderSync(sessionID: string): NormalizedSession | undefined {
|
|
4488
|
+
const row = safeQueryOne<SessionRow>(
|
|
4489
|
+
this.getDb().prepare('SELECT * FROM sessions WHERE session_id = ?'),
|
|
4490
|
+
[sessionID],
|
|
4491
|
+
'readSessionHeaderSync',
|
|
4492
|
+
);
|
|
4493
|
+
if (!row) return undefined;
|
|
4494
|
+
|
|
4495
|
+
return this.materializeSessionRow(row);
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4350
4498
|
private clearSessionDataSync(sessionID: string): void {
|
|
4351
4499
|
const db = this.getDb();
|
|
4352
4500
|
db.prepare('DELETE FROM message_fts WHERE session_id = ?').run(sessionID);
|
|
@@ -4507,21 +4655,7 @@ export class SqliteLcmStore {
|
|
|
4507
4655
|
if (!row) {
|
|
4508
4656
|
return { ...emptySession(sessionID), messages };
|
|
4509
4657
|
}
|
|
4510
|
-
return
|
|
4511
|
-
sessionID: row.session_id,
|
|
4512
|
-
title: row.title ?? undefined,
|
|
4513
|
-
directory: row.session_directory ?? undefined,
|
|
4514
|
-
parentSessionID: row.parent_session_id ?? undefined,
|
|
4515
|
-
rootSessionID: row.root_session_id ?? undefined,
|
|
4516
|
-
lineageDepth: row.lineage_depth ?? undefined,
|
|
4517
|
-
pinned: Boolean(row.pinned),
|
|
4518
|
-
pinReason: row.pin_reason ?? undefined,
|
|
4519
|
-
updatedAt: row.updated_at,
|
|
4520
|
-
compactedAt: row.compacted_at ?? undefined,
|
|
4521
|
-
deleted: Boolean(row.deleted),
|
|
4522
|
-
eventCount: row.event_count,
|
|
4523
|
-
messages,
|
|
4524
|
-
};
|
|
4658
|
+
return this.materializeSessionRow(row, messages);
|
|
4525
4659
|
});
|
|
4526
4660
|
}
|
|
4527
4661
|
|
|
@@ -4568,21 +4702,7 @@ export class SqliteLcmStore {
|
|
|
4568
4702
|
return { ...emptySession(sessionID), messages };
|
|
4569
4703
|
}
|
|
4570
4704
|
|
|
4571
|
-
return
|
|
4572
|
-
sessionID: row.session_id,
|
|
4573
|
-
title: row.title ?? undefined,
|
|
4574
|
-
directory: row.session_directory ?? undefined,
|
|
4575
|
-
parentSessionID: row.parent_session_id ?? undefined,
|
|
4576
|
-
rootSessionID: row.root_session_id ?? undefined,
|
|
4577
|
-
lineageDepth: row.lineage_depth ?? undefined,
|
|
4578
|
-
pinned: Boolean(row.pinned),
|
|
4579
|
-
pinReason: row.pin_reason ?? undefined,
|
|
4580
|
-
updatedAt: row.updated_at,
|
|
4581
|
-
compactedAt: row.compacted_at ?? undefined,
|
|
4582
|
-
deleted: Boolean(row.deleted),
|
|
4583
|
-
eventCount: row.event_count,
|
|
4584
|
-
messages,
|
|
4585
|
-
};
|
|
4705
|
+
return this.materializeSessionRow(row, messages);
|
|
4586
4706
|
}
|
|
4587
4707
|
|
|
4588
4708
|
private prepareSessionForPersistence(session: NormalizedSession): NormalizedSession {
|