opencode-lcm 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -3
- package/dist/store.d.ts +20 -4
- package/dist/store.js +456 -314
- package/package.json +1 -1
- package/src/store.ts +556 -360
package/src/store.ts
CHANGED
|
@@ -303,10 +303,6 @@ function readSessionStats(db: SqlDatabaseLike): {
|
|
|
303
303
|
};
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
type ReadSessionOptions = {
|
|
307
|
-
artifactMessageIDs?: string[];
|
|
308
|
-
};
|
|
309
|
-
|
|
310
306
|
function extractSessionID(event: unknown): string | undefined {
|
|
311
307
|
const record = asRecord(event);
|
|
312
308
|
if (!record) return undefined;
|
|
@@ -364,6 +360,15 @@ function compareMessages(a: ConversationMessage, b: ConversationMessage): number
|
|
|
364
360
|
return a.info.time.created - b.info.time.created;
|
|
365
361
|
}
|
|
366
362
|
|
|
363
|
+
function emptySession(sessionID: string): NormalizedSession {
|
|
364
|
+
return {
|
|
365
|
+
sessionID,
|
|
366
|
+
updatedAt: 0,
|
|
367
|
+
eventCount: 0,
|
|
368
|
+
messages: [],
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
367
372
|
function buildSummaryNodeID(sessionID: string, level: number, slot: number): string {
|
|
368
373
|
return `sum_${hashContent(`summary:${sessionID}`).slice(0, 12)}_l${level}_p${slot}`;
|
|
369
374
|
}
|
|
@@ -523,12 +528,23 @@ function logStartupPhase(phase: string, context?: Record<string, unknown>): void
|
|
|
523
528
|
getLogger().info(`startup phase: ${phase}`, context);
|
|
524
529
|
}
|
|
525
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
|
+
|
|
526
537
|
type SqliteRuntime = 'bun' | 'node';
|
|
527
538
|
type SqliteRuntimeOptions = {
|
|
528
539
|
envOverride?: string | undefined;
|
|
529
540
|
isBunRuntime?: boolean;
|
|
530
541
|
platform?: string | undefined;
|
|
531
542
|
};
|
|
543
|
+
type CaptureHydrationMode = 'full' | 'targeted';
|
|
544
|
+
type CaptureHydrationOptions = {
|
|
545
|
+
isBunRuntime?: boolean;
|
|
546
|
+
platform?: string | undefined;
|
|
547
|
+
};
|
|
532
548
|
|
|
533
549
|
function normalizeSqliteRuntimeOverride(value: string | undefined): SqliteRuntime | 'auto' {
|
|
534
550
|
const normalized = value?.trim().toLowerCase();
|
|
@@ -554,6 +570,20 @@ export function resolveSqliteRuntime(options?: SqliteRuntimeOptions): SqliteRunt
|
|
|
554
570
|
return resolveSqliteRuntimeCandidates(options)[0];
|
|
555
571
|
}
|
|
556
572
|
|
|
573
|
+
export function resolveCaptureHydrationMode(
|
|
574
|
+
options?: CaptureHydrationOptions,
|
|
575
|
+
): CaptureHydrationMode {
|
|
576
|
+
const isBunRuntime =
|
|
577
|
+
options?.isBunRuntime ?? (typeof globalThis === 'object' && 'Bun' in globalThis);
|
|
578
|
+
const platform = options?.platform ?? process.platform;
|
|
579
|
+
|
|
580
|
+
// The targeted fresh-tail capture path is safe under Node, but the bundled
|
|
581
|
+
// Bun runtime on Windows has been the only environment where users have
|
|
582
|
+
// reported native crashes in this hot path. Keep the older full-session
|
|
583
|
+
// hydration there until Bun/Windows is proven stable again.
|
|
584
|
+
return isBunRuntime && platform === 'win32' ? 'full' : 'targeted';
|
|
585
|
+
}
|
|
586
|
+
|
|
557
587
|
function isSqliteRuntimeImportError(runtime: SqliteRuntime, error: unknown): boolean {
|
|
558
588
|
const code =
|
|
559
589
|
typeof error === 'object' && error && 'code' in error && typeof error.code === 'string'
|
|
@@ -683,6 +713,10 @@ export class SqliteLcmStore {
|
|
|
683
713
|
private readonly workspaceDirectory: string;
|
|
684
714
|
private db?: SqlDatabaseLike;
|
|
685
715
|
private dbReadyPromise?: Promise<void>;
|
|
716
|
+
private deferredInitTimer?: ReturnType<typeof setTimeout>;
|
|
717
|
+
private deferredInitPromise?: Promise<void>;
|
|
718
|
+
private deferredInitRequested = false;
|
|
719
|
+
private activeOperationCount = 0;
|
|
686
720
|
private readonly pendingPartUpdates = new Map<string, Event>();
|
|
687
721
|
private pendingPartUpdateTimer?: ReturnType<typeof setTimeout>;
|
|
688
722
|
private pendingPartUpdateFlushPromise?: Promise<void>;
|
|
@@ -701,8 +735,27 @@ export class SqliteLcmStore {
|
|
|
701
735
|
await mkdir(this.baseDir, { recursive: true });
|
|
702
736
|
}
|
|
703
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
|
+
|
|
704
756
|
private async prepareForRead(): Promise<void> {
|
|
705
757
|
await this.ensureDbReady();
|
|
758
|
+
await this.waitForDeferredInitIfRunning();
|
|
706
759
|
await this.flushDeferredPartUpdates();
|
|
707
760
|
}
|
|
708
761
|
|
|
@@ -713,15 +766,7 @@ export class SqliteLcmStore {
|
|
|
713
766
|
this.pendingPartUpdateTimer = undefined;
|
|
714
767
|
void this.flushDeferredPartUpdates();
|
|
715
768
|
}, SqliteLcmStore.deferredPartUpdateDelayMs);
|
|
716
|
-
|
|
717
|
-
if (
|
|
718
|
-
typeof this.pendingPartUpdateTimer === 'object' &&
|
|
719
|
-
this.pendingPartUpdateTimer &&
|
|
720
|
-
'unref' in this.pendingPartUpdateTimer &&
|
|
721
|
-
typeof this.pendingPartUpdateTimer.unref === 'function'
|
|
722
|
-
) {
|
|
723
|
-
this.pendingPartUpdateTimer.unref();
|
|
724
|
-
}
|
|
769
|
+
unrefTimer(this.pendingPartUpdateTimer);
|
|
725
770
|
}
|
|
726
771
|
|
|
727
772
|
private clearDeferredPartUpdateTimer(): void {
|
|
@@ -766,63 +811,73 @@ export class SqliteLcmStore {
|
|
|
766
811
|
}
|
|
767
812
|
|
|
768
813
|
async captureDeferred(event: Event): Promise<void> {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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;
|
|
776
841
|
}
|
|
777
|
-
case 'message.part.removed':
|
|
778
|
-
this.clearDeferredPartUpdateForPart(
|
|
779
|
-
event.properties.sessionID,
|
|
780
|
-
event.properties.messageID,
|
|
781
|
-
event.properties.partID,
|
|
782
|
-
);
|
|
783
|
-
break;
|
|
784
|
-
case 'message.removed':
|
|
785
|
-
this.clearDeferredPartUpdatesForMessage(
|
|
786
|
-
event.properties.sessionID,
|
|
787
|
-
event.properties.messageID,
|
|
788
|
-
);
|
|
789
|
-
break;
|
|
790
|
-
case 'session.deleted':
|
|
791
|
-
this.clearDeferredPartUpdatesForSession(extractSessionID(event));
|
|
792
|
-
break;
|
|
793
|
-
default:
|
|
794
|
-
break;
|
|
795
|
-
}
|
|
796
842
|
|
|
797
|
-
|
|
843
|
+
await this.capture(event);
|
|
844
|
+
});
|
|
798
845
|
}
|
|
799
846
|
|
|
800
847
|
async flushDeferredPartUpdates(): Promise<void> {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
+
}
|
|
811
860
|
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
});
|
|
861
|
+
})().finally(() => {
|
|
862
|
+
this.pendingPartUpdateFlushPromise = undefined;
|
|
863
|
+
if (this.pendingPartUpdates.size > 0) this.scheduleDeferredPartUpdateFlush();
|
|
864
|
+
});
|
|
817
865
|
|
|
818
|
-
|
|
866
|
+
return this.pendingPartUpdateFlushPromise;
|
|
867
|
+
});
|
|
819
868
|
}
|
|
820
869
|
|
|
821
870
|
private async ensureDbReady(): Promise<void> {
|
|
822
|
-
if (this.
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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();
|
|
826
881
|
}
|
|
827
882
|
|
|
828
883
|
private async openAndInitializeDb(): Promise<void> {
|
|
@@ -1000,8 +1055,6 @@ export class SqliteLcmStore {
|
|
|
1000
1055
|
await this.migrateLegacyArtifacts();
|
|
1001
1056
|
logStartupPhase('open-db:write-schema-version', { schemaVersion: STORE_SCHEMA_VERSION });
|
|
1002
1057
|
this.writeSchemaVersionSync(STORE_SCHEMA_VERSION);
|
|
1003
|
-
logStartupPhase('open-db:deferred-init:start');
|
|
1004
|
-
this.completeDeferredInit();
|
|
1005
1058
|
logStartupPhase('open-db:ready');
|
|
1006
1059
|
} catch (error) {
|
|
1007
1060
|
logStartupPhase('open-db:error', {
|
|
@@ -1016,6 +1069,59 @@ export class SqliteLcmStore {
|
|
|
1016
1069
|
|
|
1017
1070
|
private deferredInitCompleted = false;
|
|
1018
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
|
+
|
|
1019
1125
|
private readSchemaVersionSync(): number {
|
|
1020
1126
|
return firstFiniteNumber(this.getDb().prepare('PRAGMA user_version').get()) ?? 0;
|
|
1021
1127
|
}
|
|
@@ -1077,6 +1183,10 @@ export class SqliteLcmStore {
|
|
|
1077
1183
|
close(): void {
|
|
1078
1184
|
this.clearDeferredPartUpdateTimer();
|
|
1079
1185
|
this.pendingPartUpdates.clear();
|
|
1186
|
+
if (this.deferredInitTimer) {
|
|
1187
|
+
clearTimeout(this.deferredInitTimer);
|
|
1188
|
+
this.deferredInitTimer = undefined;
|
|
1189
|
+
}
|
|
1080
1190
|
if (!this.db) return;
|
|
1081
1191
|
this.db.close();
|
|
1082
1192
|
this.db = undefined;
|
|
@@ -1084,68 +1194,75 @@ export class SqliteLcmStore {
|
|
|
1084
1194
|
}
|
|
1085
1195
|
|
|
1086
1196
|
async capture(event: Event): Promise<void> {
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
if (!normalized) return;
|
|
1197
|
+
return this.withStoreActivity(async () => {
|
|
1198
|
+
const normalized = normalizeEvent(event);
|
|
1199
|
+
if (!normalized) return;
|
|
1091
1200
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1201
|
+
const shouldRecord = this.shouldRecordEvent(normalized.type);
|
|
1202
|
+
const shouldPersistSession =
|
|
1203
|
+
Boolean(normalized.sessionID) && this.shouldPersistSessionForEvent(normalized.type);
|
|
1204
|
+
if (!shouldRecord && !shouldPersistSession) return;
|
|
1095
1205
|
|
|
1096
|
-
|
|
1097
|
-
if (!this.shouldPersistSessionForEvent(normalized.type)) return;
|
|
1206
|
+
await this.ensureDeferredInitComplete();
|
|
1098
1207
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
const previousParentSessionID = session.parentSessionID;
|
|
1103
|
-
let next = this.applyEvent(session, normalized);
|
|
1104
|
-
next.updatedAt = Math.max(next.updatedAt, normalized.timestamp);
|
|
1105
|
-
next.eventCount += 1;
|
|
1106
|
-
next = this.prepareSessionForPersistence(next);
|
|
1107
|
-
const shouldSyncDerivedState = this.shouldSyncDerivedSessionStateForEvent(
|
|
1108
|
-
session,
|
|
1109
|
-
next,
|
|
1110
|
-
normalized,
|
|
1111
|
-
);
|
|
1208
|
+
if (shouldRecord) {
|
|
1209
|
+
this.writeEvent(normalized);
|
|
1210
|
+
}
|
|
1112
1211
|
|
|
1113
|
-
|
|
1212
|
+
if (!normalized.sessionID || !shouldPersistSession) return;
|
|
1114
1213
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
+
}
|
|
1125
1241
|
}
|
|
1126
|
-
}
|
|
1127
1242
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1243
|
+
if (shouldSyncDerivedState) {
|
|
1244
|
+
this.syncDerivedSessionStateSync(this.readSessionSync(normalized.sessionID));
|
|
1245
|
+
}
|
|
1131
1246
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1247
|
+
if (
|
|
1248
|
+
this.shouldSyncDerivedLineageSubtree(
|
|
1249
|
+
normalized.type,
|
|
1250
|
+
previousParentSessionID,
|
|
1251
|
+
next.parentSessionID,
|
|
1252
|
+
)
|
|
1253
|
+
) {
|
|
1254
|
+
this.syncDerivedLineageSubtreeSync(normalized.sessionID, true);
|
|
1255
|
+
}
|
|
1141
1256
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1257
|
+
if (this.shouldCleanupOrphanBlobsForEvent(normalized.type)) {
|
|
1258
|
+
this.deleteOrphanArtifactBlobsSync();
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1145
1261
|
}
|
|
1146
1262
|
|
|
1147
1263
|
async stats(): Promise<StoreStats> {
|
|
1148
1264
|
await this.prepareForRead();
|
|
1265
|
+
await this.ensureDeferredInitComplete();
|
|
1149
1266
|
const db = this.getDb();
|
|
1150
1267
|
const totalRow = validateRow<{ count: number; latest: number | null }>(
|
|
1151
1268
|
db.prepare('SELECT COUNT(*) AS count, MAX(ts) AS latest FROM events').get(),
|
|
@@ -1262,13 +1379,14 @@ export class SqliteLcmStore {
|
|
|
1262
1379
|
scope?: string;
|
|
1263
1380
|
limit?: number;
|
|
1264
1381
|
}): Promise<SearchResult[]> {
|
|
1265
|
-
await this.prepareForRead();
|
|
1266
1382
|
const resolvedScope = this.resolveConfiguredScope('grep', input.scope, input.sessionID);
|
|
1267
|
-
const sessionIDs = this.resolveScopeSessionIDs(resolvedScope, input.sessionID);
|
|
1268
1383
|
const limit = input.limit ?? 5;
|
|
1269
1384
|
const needle = input.query.trim();
|
|
1270
1385
|
if (!needle) return [];
|
|
1271
1386
|
|
|
1387
|
+
await this.prepareForRead();
|
|
1388
|
+
const sessionIDs = this.resolveScopeSessionIDs(resolvedScope, input.sessionID);
|
|
1389
|
+
|
|
1272
1390
|
const ftsResults = this.searchWithFts(needle, sessionIDs, limit);
|
|
1273
1391
|
if (ftsResults.length > 0) return ftsResults;
|
|
1274
1392
|
return this.searchByScan(needle.toLowerCase(), sessionIDs, limit);
|
|
@@ -1645,70 +1763,58 @@ export class SqliteLcmStore {
|
|
|
1645
1763
|
);
|
|
1646
1764
|
}
|
|
1647
1765
|
|
|
1648
|
-
private captureArtifactHydrationMessageIDs(event: CapturedEvent): string[] {
|
|
1649
|
-
const payload = event.payload as Event;
|
|
1650
|
-
|
|
1651
|
-
switch (payload.type) {
|
|
1652
|
-
case 'message.updated':
|
|
1653
|
-
return [payload.properties.info.id];
|
|
1654
|
-
case 'message.part.updated':
|
|
1655
|
-
return [payload.properties.part.messageID];
|
|
1656
|
-
case 'message.part.removed':
|
|
1657
|
-
return [payload.properties.messageID];
|
|
1658
|
-
default:
|
|
1659
|
-
return [];
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
private archivedMessageIDs(messages: ConversationMessage[]): string[] {
|
|
1664
|
-
return this.getArchivedMessages(messages).map((message) => message.info.id);
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
private didArchivedMessagesChange(
|
|
1668
|
-
before: ConversationMessage[],
|
|
1669
|
-
after: ConversationMessage[],
|
|
1670
|
-
): boolean {
|
|
1671
|
-
const beforeIDs = this.archivedMessageIDs(before);
|
|
1672
|
-
const afterIDs = this.archivedMessageIDs(after);
|
|
1673
|
-
if (beforeIDs.length !== afterIDs.length) return true;
|
|
1674
|
-
return beforeIDs.some((messageID, index) => messageID !== afterIDs[index]);
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
private isArchivedMessage(messages: ConversationMessage[], messageID?: string): boolean {
|
|
1678
|
-
if (!messageID) return false;
|
|
1679
|
-
return this.archivedMessageIDs(messages).includes(messageID);
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
1766
|
private shouldSyncDerivedSessionStateForEvent(
|
|
1683
|
-
|
|
1684
|
-
next: NormalizedSession,
|
|
1767
|
+
session: NormalizedSession,
|
|
1685
1768
|
event: CapturedEvent,
|
|
1686
1769
|
): boolean {
|
|
1687
1770
|
const payload = event.payload as Event;
|
|
1688
1771
|
|
|
1689
1772
|
switch (payload.type) {
|
|
1690
1773
|
case 'message.updated': {
|
|
1691
|
-
const
|
|
1692
|
-
|
|
1693
|
-
this.didArchivedMessagesChange(previous.messages, next.messages) ||
|
|
1694
|
-
this.isArchivedMessage(previous.messages, messageID) ||
|
|
1695
|
-
this.isArchivedMessage(next.messages, messageID)
|
|
1774
|
+
const existing = session.messages.find(
|
|
1775
|
+
(message) => message.info.id === payload.properties.info.id,
|
|
1696
1776
|
);
|
|
1777
|
+
if (existing) {
|
|
1778
|
+
return this.isMessageArchivedSync(
|
|
1779
|
+
session.sessionID,
|
|
1780
|
+
existing.info.id,
|
|
1781
|
+
existing.info.time.created,
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
return this.readMessageCountSync(session.sessionID) >= this.options.freshTailMessages;
|
|
1786
|
+
}
|
|
1787
|
+
case 'message.removed': {
|
|
1788
|
+
const existing = safeQueryOne<{ created_at: number }>(
|
|
1789
|
+
this.getDb().prepare(
|
|
1790
|
+
'SELECT created_at FROM messages WHERE session_id = ? AND message_id = ?',
|
|
1791
|
+
),
|
|
1792
|
+
[session.sessionID, payload.properties.messageID],
|
|
1793
|
+
'shouldSyncDerivedSessionStateForEvent.messageRemoved',
|
|
1794
|
+
);
|
|
1795
|
+
if (!existing) return false;
|
|
1796
|
+
return this.readMessageCountSync(session.sessionID) > this.options.freshTailMessages;
|
|
1697
1797
|
}
|
|
1698
|
-
case 'message.removed':
|
|
1699
|
-
return this.didArchivedMessagesChange(previous.messages, next.messages);
|
|
1700
1798
|
case 'message.part.updated': {
|
|
1701
|
-
const
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1799
|
+
const message = session.messages.find(
|
|
1800
|
+
(entry) => entry.info.id === payload.properties.part.messageID,
|
|
1801
|
+
);
|
|
1802
|
+
if (!message) return false;
|
|
1803
|
+
return this.isMessageArchivedSync(
|
|
1804
|
+
session.sessionID,
|
|
1805
|
+
message.info.id,
|
|
1806
|
+
message.info.time.created,
|
|
1705
1807
|
);
|
|
1706
1808
|
}
|
|
1707
1809
|
case 'message.part.removed': {
|
|
1708
|
-
const
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1810
|
+
const message = session.messages.find(
|
|
1811
|
+
(entry) => entry.info.id === payload.properties.messageID,
|
|
1812
|
+
);
|
|
1813
|
+
if (!message) return false;
|
|
1814
|
+
return this.isMessageArchivedSync(
|
|
1815
|
+
session.sessionID,
|
|
1816
|
+
message.info.id,
|
|
1817
|
+
message.info.time.created,
|
|
1712
1818
|
);
|
|
1713
1819
|
}
|
|
1714
1820
|
default:
|
|
@@ -2507,71 +2613,73 @@ export class SqliteLcmStore {
|
|
|
2507
2613
|
vacuum?: boolean;
|
|
2508
2614
|
limit?: number;
|
|
2509
2615
|
}): Promise<string> {
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
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();
|
|
2517
2660
|
|
|
2518
|
-
if (!apply || candidateEvents === 0) {
|
|
2519
2661
|
return [
|
|
2520
2662
|
`candidate_events=${candidateEvents}`,
|
|
2521
|
-
`
|
|
2663
|
+
`deleted_events=${candidateEvents}`,
|
|
2664
|
+
`apply=true`,
|
|
2522
2665
|
`vacuum_requested=${vacuum}`,
|
|
2523
|
-
`
|
|
2524
|
-
`
|
|
2525
|
-
`
|
|
2526
|
-
`
|
|
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}`,
|
|
2527
2675
|
...(candidates.length > 0
|
|
2528
2676
|
? [
|
|
2529
|
-
'
|
|
2677
|
+
'deleted_event_types:',
|
|
2530
2678
|
...candidates.slice(0, limit).map((row) => `- ${row.eventType} count=${row.count}`),
|
|
2531
2679
|
]
|
|
2532
|
-
: ['
|
|
2680
|
+
: ['deleted_event_types:', '- none']),
|
|
2533
2681
|
].join('\n');
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2536
|
-
const eventTypes = candidates.map((row) => row.eventType);
|
|
2537
|
-
if (eventTypes.length > 0) {
|
|
2538
|
-
const placeholders = eventTypes.map(() => '?').join(', ');
|
|
2539
|
-
this.getDb()
|
|
2540
|
-
.prepare(`DELETE FROM events WHERE event_type IN (${placeholders})`)
|
|
2541
|
-
.run(...eventTypes);
|
|
2542
|
-
}
|
|
2543
|
-
|
|
2544
|
-
let vacuumApplied = false;
|
|
2545
|
-
this.getDb().exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
2546
|
-
if (vacuum) {
|
|
2547
|
-
this.getDb().exec('VACUUM');
|
|
2548
|
-
this.getDb().exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
2549
|
-
vacuumApplied = true;
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
const afterSizes = await this.readStoreFileSizes();
|
|
2553
|
-
|
|
2554
|
-
return [
|
|
2555
|
-
`candidate_events=${candidateEvents}`,
|
|
2556
|
-
`deleted_events=${candidateEvents}`,
|
|
2557
|
-
`apply=true`,
|
|
2558
|
-
`vacuum_requested=${vacuum}`,
|
|
2559
|
-
`vacuum_applied=${vacuumApplied}`,
|
|
2560
|
-
`db_bytes_before=${beforeSizes.dbBytes}`,
|
|
2561
|
-
`wal_bytes_before=${beforeSizes.walBytes}`,
|
|
2562
|
-
`shm_bytes_before=${beforeSizes.shmBytes}`,
|
|
2563
|
-
`total_bytes_before=${beforeSizes.totalBytes}`,
|
|
2564
|
-
`db_bytes_after=${afterSizes.dbBytes}`,
|
|
2565
|
-
`wal_bytes_after=${afterSizes.walBytes}`,
|
|
2566
|
-
`shm_bytes_after=${afterSizes.shmBytes}`,
|
|
2567
|
-
`total_bytes_after=${afterSizes.totalBytes}`,
|
|
2568
|
-
...(candidates.length > 0
|
|
2569
|
-
? [
|
|
2570
|
-
'deleted_event_types:',
|
|
2571
|
-
...candidates.slice(0, limit).map((row) => `- ${row.eventType} count=${row.count}`),
|
|
2572
|
-
]
|
|
2573
|
-
: ['deleted_event_types:', '- none']),
|
|
2574
|
-
].join('\n');
|
|
2682
|
+
});
|
|
2575
2683
|
}
|
|
2576
2684
|
|
|
2577
2685
|
async retentionReport(input?: {
|
|
@@ -2741,24 +2849,26 @@ export class SqliteLcmStore {
|
|
|
2741
2849
|
sessionID?: string;
|
|
2742
2850
|
scope?: string;
|
|
2743
2851
|
}): Promise<string> {
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
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
|
+
});
|
|
2762
2872
|
}
|
|
2763
2873
|
|
|
2764
2874
|
async importSnapshot(input: {
|
|
@@ -2766,31 +2876,35 @@ export class SqliteLcmStore {
|
|
|
2766
2876
|
mode?: 'replace' | 'merge';
|
|
2767
2877
|
worktreeMode?: SnapshotWorktreeMode;
|
|
2768
2878
|
}): Promise<string> {
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
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
|
+
});
|
|
2782
2894
|
}
|
|
2783
2895
|
|
|
2784
2896
|
async resume(sessionID?: string): Promise<string> {
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2897
|
+
return this.withStoreActivity(async () => {
|
|
2898
|
+
await this.prepareForRead();
|
|
2899
|
+
const resolvedSessionID = sessionID ?? this.latestSessionIDSync();
|
|
2900
|
+
if (!resolvedSessionID) return 'No stored resume snapshots yet.';
|
|
2788
2901
|
|
|
2789
|
-
|
|
2790
|
-
|
|
2902
|
+
const existing = this.getResumeSync(resolvedSessionID);
|
|
2903
|
+
if (existing && !this.isManagedResumeNote(existing)) return existing;
|
|
2791
2904
|
|
|
2792
|
-
|
|
2793
|
-
|
|
2905
|
+
const generated = await this.buildCompactionContext(resolvedSessionID);
|
|
2906
|
+
return generated ?? existing ?? 'No stored resume snapshot for that session.';
|
|
2907
|
+
});
|
|
2794
2908
|
}
|
|
2795
2909
|
|
|
2796
2910
|
async expand(input: {
|
|
@@ -2874,53 +2988,60 @@ export class SqliteLcmStore {
|
|
|
2874
2988
|
}
|
|
2875
2989
|
|
|
2876
2990
|
async transformMessages(messages: ConversationMessage[]): Promise<boolean> {
|
|
2877
|
-
|
|
2878
|
-
|
|
2991
|
+
return this.withStoreActivity(async () => {
|
|
2992
|
+
if (messages.length < this.options.minMessagesForTransform) return false;
|
|
2879
2993
|
|
|
2880
|
-
|
|
2881
|
-
|
|
2994
|
+
const window = resolveArchiveTransformWindow(messages, this.options.freshTailMessages);
|
|
2995
|
+
if (!window) return false;
|
|
2882
2996
|
|
|
2883
|
-
|
|
2997
|
+
await this.prepareForRead();
|
|
2884
2998
|
|
|
2885
|
-
|
|
2886
|
-
if (roots.length === 0) return false;
|
|
2999
|
+
const { anchor, archived, recent } = window;
|
|
2887
3000
|
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
anchor.info.sessionID,
|
|
2891
|
-
recent,
|
|
2892
|
-
anchor,
|
|
2893
|
-
);
|
|
2894
|
-
for (const message of archived) {
|
|
2895
|
-
this.compactMessageInPlace(message);
|
|
2896
|
-
}
|
|
3001
|
+
const roots = this.ensureSummaryGraphSync(anchor.info.sessionID, archived);
|
|
3002
|
+
if (roots.length === 0) return false;
|
|
2897
3003
|
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
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
|
+
}
|
|
2903
3033
|
syntheticParts.push({
|
|
2904
|
-
id: `lcm-
|
|
3034
|
+
id: `lcm-summary-${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
|
2905
3035
|
sessionID: anchor.info.sessionID,
|
|
2906
3036
|
messageID: anchor.info.id,
|
|
2907
3037
|
type: 'text',
|
|
2908
|
-
text:
|
|
3038
|
+
text: summary,
|
|
2909
3039
|
synthetic: true,
|
|
2910
|
-
metadata: { opencodeLcm: '
|
|
3040
|
+
metadata: { opencodeLcm: 'archive-summary' },
|
|
2911
3041
|
});
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
id: `lcm-summary-${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
|
2915
|
-
sessionID: anchor.info.sessionID,
|
|
2916
|
-
messageID: anchor.info.id,
|
|
2917
|
-
type: 'text',
|
|
2918
|
-
text: summary,
|
|
2919
|
-
synthetic: true,
|
|
2920
|
-
metadata: { opencodeLcm: 'archive-summary' },
|
|
3042
|
+
anchor.parts.push(...syntheticParts);
|
|
3043
|
+
return true;
|
|
2921
3044
|
});
|
|
2922
|
-
anchor.parts.push(...syntheticParts);
|
|
2923
|
-
return true;
|
|
2924
3045
|
}
|
|
2925
3046
|
|
|
2926
3047
|
systemHint(): string | undefined {
|
|
@@ -4231,17 +4352,40 @@ export class SqliteLcmStore {
|
|
|
4231
4352
|
): { rootSessionID: string; lineageDepth: number } {
|
|
4232
4353
|
if (!parentSessionID) return { rootSessionID: sessionID, lineageDepth: 0 };
|
|
4233
4354
|
|
|
4234
|
-
const
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
| { root_session_id: string | null; lineage_depth: number | null }
|
|
4238
|
-
| undefined;
|
|
4355
|
+
const seen = new Set<string>([sessionID]);
|
|
4356
|
+
let currentSessionID: string | undefined = parentSessionID;
|
|
4357
|
+
let lineageDepth = 1;
|
|
4239
4358
|
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
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 };
|
|
4245
4389
|
}
|
|
4246
4390
|
|
|
4247
4391
|
private applyEvent(session: NormalizedSession, event: CapturedEvent): NormalizedSession {
|
|
@@ -4313,31 +4457,44 @@ export class SqliteLcmStore {
|
|
|
4313
4457
|
return row?.note;
|
|
4314
4458
|
}
|
|
4315
4459
|
|
|
4316
|
-
private
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
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;
|
|
4323
4469
|
|
|
4324
4470
|
return {
|
|
4325
4471
|
sessionID: row.session_id,
|
|
4326
4472
|
title: row.title ?? undefined,
|
|
4327
4473
|
directory: row.session_directory ?? undefined,
|
|
4328
|
-
parentSessionID
|
|
4329
|
-
rootSessionID: row.root_session_id ??
|
|
4330
|
-
lineageDepth: row.lineage_depth ??
|
|
4474
|
+
parentSessionID,
|
|
4475
|
+
rootSessionID: row.root_session_id ?? derivedLineage?.rootSessionID,
|
|
4476
|
+
lineageDepth: row.lineage_depth ?? derivedLineage?.lineageDepth,
|
|
4331
4477
|
pinned: Boolean(row.pinned),
|
|
4332
4478
|
pinReason: row.pin_reason ?? undefined,
|
|
4333
4479
|
updatedAt: row.updated_at,
|
|
4334
4480
|
compactedAt: row.compacted_at ?? undefined,
|
|
4335
4481
|
deleted: Boolean(row.deleted),
|
|
4336
4482
|
eventCount: row.event_count,
|
|
4337
|
-
messages
|
|
4483
|
+
messages,
|
|
4338
4484
|
};
|
|
4339
4485
|
}
|
|
4340
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
|
+
|
|
4341
4498
|
private clearSessionDataSync(sessionID: string): void {
|
|
4342
4499
|
const db = this.getDb();
|
|
4343
4500
|
db.prepare('DELETE FROM message_fts WHERE session_id = ?').run(sessionID);
|
|
@@ -4496,27 +4653,13 @@ export class SqliteLcmStore {
|
|
|
4496
4653
|
const row = sessionMap.get(sessionID);
|
|
4497
4654
|
const messages = messagesBySession.get(sessionID) ?? [];
|
|
4498
4655
|
if (!row) {
|
|
4499
|
-
return { sessionID,
|
|
4656
|
+
return { ...emptySession(sessionID), messages };
|
|
4500
4657
|
}
|
|
4501
|
-
return
|
|
4502
|
-
sessionID: row.session_id,
|
|
4503
|
-
title: row.title ?? undefined,
|
|
4504
|
-
directory: row.session_directory ?? undefined,
|
|
4505
|
-
parentSessionID: row.parent_session_id ?? undefined,
|
|
4506
|
-
rootSessionID: row.root_session_id ?? undefined,
|
|
4507
|
-
lineageDepth: row.lineage_depth ?? undefined,
|
|
4508
|
-
pinned: Boolean(row.pinned),
|
|
4509
|
-
pinReason: row.pin_reason ?? undefined,
|
|
4510
|
-
updatedAt: row.updated_at,
|
|
4511
|
-
compactedAt: row.compacted_at ?? undefined,
|
|
4512
|
-
deleted: Boolean(row.deleted),
|
|
4513
|
-
eventCount: row.event_count,
|
|
4514
|
-
messages,
|
|
4515
|
-
};
|
|
4658
|
+
return this.materializeSessionRow(row, messages);
|
|
4516
4659
|
});
|
|
4517
4660
|
}
|
|
4518
4661
|
|
|
4519
|
-
private readSessionSync(sessionID: string
|
|
4662
|
+
private readSessionSync(sessionID: string): NormalizedSession {
|
|
4520
4663
|
const db = this.getDb();
|
|
4521
4664
|
const row = safeQueryOne<SessionRow>(
|
|
4522
4665
|
db.prepare('SELECT * FROM sessions WHERE session_id = ?'),
|
|
@@ -4534,14 +4677,7 @@ export class SqliteLcmStore {
|
|
|
4534
4677
|
)
|
|
4535
4678
|
.all(sessionID) as PartRow[];
|
|
4536
4679
|
const artifactsByPart = new Map<string, ArtifactData[]>();
|
|
4537
|
-
const
|
|
4538
|
-
const artifacts =
|
|
4539
|
-
artifactMessageIDs === undefined
|
|
4540
|
-
? this.readArtifactsForSessionSync(sessionID)
|
|
4541
|
-
: [...new Set(artifactMessageIDs)].flatMap((messageID) =>
|
|
4542
|
-
this.readArtifactsForMessageSync(messageID),
|
|
4543
|
-
);
|
|
4544
|
-
for (const artifact of artifacts) {
|
|
4680
|
+
for (const artifact of this.readArtifactsForSessionSync(sessionID)) {
|
|
4545
4681
|
const list = artifactsByPart.get(artifact.partID) ?? [];
|
|
4546
4682
|
list.push(artifact);
|
|
4547
4683
|
artifactsByPart.set(artifact.partID, list);
|
|
@@ -4563,29 +4699,10 @@ export class SqliteLcmStore {
|
|
|
4563
4699
|
}));
|
|
4564
4700
|
|
|
4565
4701
|
if (!row) {
|
|
4566
|
-
return {
|
|
4567
|
-
sessionID,
|
|
4568
|
-
updatedAt: 0,
|
|
4569
|
-
eventCount: 0,
|
|
4570
|
-
messages,
|
|
4571
|
-
};
|
|
4702
|
+
return { ...emptySession(sessionID), messages };
|
|
4572
4703
|
}
|
|
4573
4704
|
|
|
4574
|
-
return
|
|
4575
|
-
sessionID: row.session_id,
|
|
4576
|
-
title: row.title ?? undefined,
|
|
4577
|
-
directory: row.session_directory ?? undefined,
|
|
4578
|
-
parentSessionID: row.parent_session_id ?? undefined,
|
|
4579
|
-
rootSessionID: row.root_session_id ?? undefined,
|
|
4580
|
-
lineageDepth: row.lineage_depth ?? undefined,
|
|
4581
|
-
pinned: Boolean(row.pinned),
|
|
4582
|
-
pinReason: row.pin_reason ?? undefined,
|
|
4583
|
-
updatedAt: row.updated_at,
|
|
4584
|
-
compactedAt: row.compacted_at ?? undefined,
|
|
4585
|
-
deleted: Boolean(row.deleted),
|
|
4586
|
-
eventCount: row.event_count,
|
|
4587
|
-
messages,
|
|
4588
|
-
};
|
|
4705
|
+
return this.materializeSessionRow(row, messages);
|
|
4589
4706
|
}
|
|
4590
4707
|
|
|
4591
4708
|
private prepareSessionForPersistence(session: NormalizedSession): NormalizedSession {
|
|
@@ -4622,6 +4739,85 @@ export class SqliteLcmStore {
|
|
|
4622
4739
|
return parentSessionID;
|
|
4623
4740
|
}
|
|
4624
4741
|
|
|
4742
|
+
private readSessionForCaptureSync(event: CapturedEvent): NormalizedSession {
|
|
4743
|
+
const sessionID = event.sessionID;
|
|
4744
|
+
if (!sessionID) return emptySession('');
|
|
4745
|
+
|
|
4746
|
+
const session = this.readSessionHeaderSync(sessionID) ?? emptySession(sessionID);
|
|
4747
|
+
const payload = event.payload as Event;
|
|
4748
|
+
|
|
4749
|
+
switch (payload.type) {
|
|
4750
|
+
case 'message.updated': {
|
|
4751
|
+
const message = this.readMessageSync(sessionID, payload.properties.info.id);
|
|
4752
|
+
if (message) session.messages = [message];
|
|
4753
|
+
return session;
|
|
4754
|
+
}
|
|
4755
|
+
case 'message.part.updated': {
|
|
4756
|
+
const message = this.readMessageSync(sessionID, payload.properties.part.messageID);
|
|
4757
|
+
if (message) session.messages = [message];
|
|
4758
|
+
return session;
|
|
4759
|
+
}
|
|
4760
|
+
case 'message.part.removed': {
|
|
4761
|
+
const message = this.readMessageSync(sessionID, payload.properties.messageID);
|
|
4762
|
+
if (message) session.messages = [message];
|
|
4763
|
+
return session;
|
|
4764
|
+
}
|
|
4765
|
+
default:
|
|
4766
|
+
return session;
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
|
|
4770
|
+
private readMessageSync(sessionID: string, messageID: string): ConversationMessage | undefined {
|
|
4771
|
+
const db = this.getDb();
|
|
4772
|
+
const row = safeQueryOne<MessageRow>(
|
|
4773
|
+
db.prepare('SELECT * FROM messages WHERE session_id = ? AND message_id = ?'),
|
|
4774
|
+
[sessionID, messageID],
|
|
4775
|
+
'readMessageSync',
|
|
4776
|
+
);
|
|
4777
|
+
if (!row) return undefined;
|
|
4778
|
+
|
|
4779
|
+
const artifactsByPart = new Map<string, ArtifactData[]>();
|
|
4780
|
+
for (const artifact of this.readArtifactsForMessageSync(messageID)) {
|
|
4781
|
+
const list = artifactsByPart.get(artifact.partID) ?? [];
|
|
4782
|
+
list.push(artifact);
|
|
4783
|
+
artifactsByPart.set(artifact.partID, list);
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4786
|
+
const parts = db
|
|
4787
|
+
.prepare(
|
|
4788
|
+
'SELECT * FROM parts WHERE session_id = ? AND message_id = ? ORDER BY sort_key ASC, part_id ASC',
|
|
4789
|
+
)
|
|
4790
|
+
.all(sessionID, messageID) as PartRow[];
|
|
4791
|
+
|
|
4792
|
+
return {
|
|
4793
|
+
info: parseJson<Message>(row.info_json),
|
|
4794
|
+
parts: parts.map((partRow) => {
|
|
4795
|
+
const part = parseJson<Part>(partRow.part_json);
|
|
4796
|
+
hydratePartFromArtifacts(part, artifactsByPart.get(part.id) ?? []);
|
|
4797
|
+
return part;
|
|
4798
|
+
}),
|
|
4799
|
+
};
|
|
4800
|
+
}
|
|
4801
|
+
|
|
4802
|
+
private readMessageCountSync(sessionID: string): number {
|
|
4803
|
+
const row = this.getDb()
|
|
4804
|
+
.prepare('SELECT COUNT(*) AS count FROM messages WHERE session_id = ?')
|
|
4805
|
+
.get(sessionID) as { count: number };
|
|
4806
|
+
return row.count;
|
|
4807
|
+
}
|
|
4808
|
+
|
|
4809
|
+
private isMessageArchivedSync(sessionID: string, messageID: string, createdAt: number): boolean {
|
|
4810
|
+
const row = this.getDb()
|
|
4811
|
+
.prepare(
|
|
4812
|
+
`SELECT COUNT(*) AS count
|
|
4813
|
+
FROM messages
|
|
4814
|
+
WHERE session_id = ?
|
|
4815
|
+
AND (created_at > ? OR (created_at = ? AND message_id > ?))`,
|
|
4816
|
+
)
|
|
4817
|
+
.get(sessionID, createdAt, createdAt, messageID) as { count: number };
|
|
4818
|
+
return row.count >= this.options.freshTailMessages;
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4625
4821
|
private async persistCapturedSession(
|
|
4626
4822
|
session: NormalizedSession,
|
|
4627
4823
|
event: CapturedEvent,
|