opencode-lcm 0.12.0 → 0.13.1
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/CHANGELOG.md +6 -0
- package/dist/store-doctor.d.ts +73 -0
- package/dist/store-doctor.js +106 -0
- package/dist/store-schema.d.ts +8 -0
- package/dist/store-schema.js +23 -0
- package/dist/store-session-read.d.ts +97 -0
- package/dist/store-session-read.js +80 -0
- package/dist/store.d.ts +12 -0
- package/dist/store.js +477 -271
- package/package.json +1 -1
- package/src/store.ts +577 -299
package/dist/store.js
CHANGED
|
@@ -147,6 +147,46 @@ function extractTimestamp(event) {
|
|
|
147
147
|
return properties.time;
|
|
148
148
|
return Date.now();
|
|
149
149
|
}
|
|
150
|
+
function logMalformedMessage(message, context, extra) {
|
|
151
|
+
getLogger().warn(message, {
|
|
152
|
+
operation: context.operation,
|
|
153
|
+
sessionID: context.sessionID,
|
|
154
|
+
eventType: context.eventType,
|
|
155
|
+
...extra,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function getValidMessageInfo(info) {
|
|
159
|
+
const record = asRecord(info);
|
|
160
|
+
if (!record)
|
|
161
|
+
return undefined;
|
|
162
|
+
const time = asRecord(record.time);
|
|
163
|
+
if (typeof record.id !== 'string' ||
|
|
164
|
+
typeof record.sessionID !== 'string' ||
|
|
165
|
+
typeof record.role !== 'string' ||
|
|
166
|
+
typeof time?.created !== 'number' ||
|
|
167
|
+
!Number.isFinite(time.created)) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
return info;
|
|
171
|
+
}
|
|
172
|
+
function filterValidConversationMessages(messages, context) {
|
|
173
|
+
const valid = messages.filter((message) => Boolean(getValidMessageInfo(message?.info)));
|
|
174
|
+
const dropped = messages.length - valid.length;
|
|
175
|
+
if (dropped > 0 && context) {
|
|
176
|
+
logMalformedMessage('Skipping malformed conversation messages', context, { dropped });
|
|
177
|
+
}
|
|
178
|
+
return valid;
|
|
179
|
+
}
|
|
180
|
+
function isValidMessagePartUpdate(event) {
|
|
181
|
+
if (event.type !== 'message.part.updated')
|
|
182
|
+
return false;
|
|
183
|
+
const part = asRecord(event.properties.part);
|
|
184
|
+
if (!part)
|
|
185
|
+
return false;
|
|
186
|
+
return (typeof part.id === 'string' &&
|
|
187
|
+
typeof part.messageID === 'string' &&
|
|
188
|
+
typeof part.sessionID === 'string');
|
|
189
|
+
}
|
|
150
190
|
function normalizeEvent(event) {
|
|
151
191
|
const record = asRecord(event);
|
|
152
192
|
if (!record || typeof record.type !== 'string')
|
|
@@ -165,7 +205,15 @@ function getDeferredPartUpdateKey(event) {
|
|
|
165
205
|
return `${event.properties.part.sessionID}:${event.properties.part.messageID}:${event.properties.part.id}`;
|
|
166
206
|
}
|
|
167
207
|
function compareMessages(a, b) {
|
|
168
|
-
|
|
208
|
+
const aInfo = getValidMessageInfo(a.info);
|
|
209
|
+
const bInfo = getValidMessageInfo(b.info);
|
|
210
|
+
if (!aInfo && !bInfo)
|
|
211
|
+
return 0;
|
|
212
|
+
if (!aInfo)
|
|
213
|
+
return 1;
|
|
214
|
+
if (!bInfo)
|
|
215
|
+
return -1;
|
|
216
|
+
return aInfo.time.created - bInfo.time.created || aInfo.id.localeCompare(bInfo.id);
|
|
169
217
|
}
|
|
170
218
|
function emptySession(sessionID) {
|
|
171
219
|
return {
|
|
@@ -323,7 +371,7 @@ function listFiles(message) {
|
|
|
323
371
|
function makeSessionTitle(session) {
|
|
324
372
|
if (session.title)
|
|
325
373
|
return session.title;
|
|
326
|
-
const firstUser = session.messages.find((message) => message.info
|
|
374
|
+
const firstUser = session.messages.find((message) => getValidMessageInfo(message.info)?.role === 'user');
|
|
327
375
|
if (!firstUser)
|
|
328
376
|
return undefined;
|
|
329
377
|
return truncate(guessMessageText(firstUser, []), 80);
|
|
@@ -336,6 +384,11 @@ function logStartupPhase(phase, context) {
|
|
|
336
384
|
return;
|
|
337
385
|
getLogger().info(`startup phase: ${phase}`, context);
|
|
338
386
|
}
|
|
387
|
+
function unrefTimer(timer) {
|
|
388
|
+
if (typeof timer === 'object' && timer && 'unref' in timer && typeof timer.unref === 'function') {
|
|
389
|
+
timer.unref();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
339
392
|
function normalizeSqliteRuntimeOverride(value) {
|
|
340
393
|
const normalized = value?.trim().toLowerCase();
|
|
341
394
|
if (normalized === 'bun' || normalized === 'node')
|
|
@@ -463,6 +516,10 @@ export class SqliteLcmStore {
|
|
|
463
516
|
workspaceDirectory;
|
|
464
517
|
db;
|
|
465
518
|
dbReadyPromise;
|
|
519
|
+
deferredInitTimer;
|
|
520
|
+
deferredInitPromise;
|
|
521
|
+
deferredInitRequested = false;
|
|
522
|
+
activeOperationCount = 0;
|
|
466
523
|
pendingPartUpdates = new Map();
|
|
467
524
|
pendingPartUpdateTimer;
|
|
468
525
|
pendingPartUpdateFlushPromise;
|
|
@@ -476,8 +533,27 @@ export class SqliteLcmStore {
|
|
|
476
533
|
async init() {
|
|
477
534
|
await mkdir(this.baseDir, { recursive: true });
|
|
478
535
|
}
|
|
536
|
+
// Keep deferred SQLite maintenance off the active connection while a store operation is running.
|
|
537
|
+
async withStoreActivity(operation) {
|
|
538
|
+
this.activeOperationCount += 1;
|
|
539
|
+
try {
|
|
540
|
+
return await operation();
|
|
541
|
+
}
|
|
542
|
+
finally {
|
|
543
|
+
this.activeOperationCount -= 1;
|
|
544
|
+
if (this.activeOperationCount === 0 && this.deferredInitRequested) {
|
|
545
|
+
this.scheduleDeferredInit();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async waitForDeferredInitIfRunning() {
|
|
550
|
+
if (!this.deferredInitPromise)
|
|
551
|
+
return;
|
|
552
|
+
await this.deferredInitPromise;
|
|
553
|
+
}
|
|
479
554
|
async prepareForRead() {
|
|
480
555
|
await this.ensureDbReady();
|
|
556
|
+
await this.waitForDeferredInitIfRunning();
|
|
481
557
|
await this.flushDeferredPartUpdates();
|
|
482
558
|
}
|
|
483
559
|
scheduleDeferredPartUpdateFlush() {
|
|
@@ -487,12 +563,7 @@ export class SqliteLcmStore {
|
|
|
487
563
|
this.pendingPartUpdateTimer = undefined;
|
|
488
564
|
void this.flushDeferredPartUpdates();
|
|
489
565
|
}, SqliteLcmStore.deferredPartUpdateDelayMs);
|
|
490
|
-
|
|
491
|
-
this.pendingPartUpdateTimer &&
|
|
492
|
-
'unref' in this.pendingPartUpdateTimer &&
|
|
493
|
-
typeof this.pendingPartUpdateTimer.unref === 'function') {
|
|
494
|
-
this.pendingPartUpdateTimer.unref();
|
|
495
|
-
}
|
|
566
|
+
unrefTimer(this.pendingPartUpdateTimer);
|
|
496
567
|
}
|
|
497
568
|
clearDeferredPartUpdateTimer() {
|
|
498
569
|
if (!this.pendingPartUpdateTimer)
|
|
@@ -536,57 +607,64 @@ export class SqliteLcmStore {
|
|
|
536
607
|
this.clearDeferredPartUpdateTimer();
|
|
537
608
|
}
|
|
538
609
|
async captureDeferred(event) {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
610
|
+
return this.withStoreActivity(async () => {
|
|
611
|
+
switch (event.type) {
|
|
612
|
+
case 'message.part.updated': {
|
|
613
|
+
const key = getDeferredPartUpdateKey(event);
|
|
614
|
+
if (!key)
|
|
615
|
+
return await this.capture(event);
|
|
616
|
+
this.pendingPartUpdates.set(key, event);
|
|
617
|
+
this.scheduleDeferredPartUpdateFlush();
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
case 'message.part.removed':
|
|
621
|
+
this.clearDeferredPartUpdateForPart(event.properties.sessionID, event.properties.messageID, event.properties.partID);
|
|
622
|
+
break;
|
|
623
|
+
case 'message.removed':
|
|
624
|
+
this.clearDeferredPartUpdatesForMessage(event.properties.sessionID, event.properties.messageID);
|
|
625
|
+
break;
|
|
626
|
+
case 'session.deleted':
|
|
627
|
+
this.clearDeferredPartUpdatesForSession(extractSessionID(event));
|
|
628
|
+
break;
|
|
629
|
+
default:
|
|
630
|
+
break;
|
|
547
631
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
break;
|
|
551
|
-
case 'message.removed':
|
|
552
|
-
this.clearDeferredPartUpdatesForMessage(event.properties.sessionID, event.properties.messageID);
|
|
553
|
-
break;
|
|
554
|
-
case 'session.deleted':
|
|
555
|
-
this.clearDeferredPartUpdatesForSession(extractSessionID(event));
|
|
556
|
-
break;
|
|
557
|
-
default:
|
|
558
|
-
break;
|
|
559
|
-
}
|
|
560
|
-
await this.capture(event);
|
|
632
|
+
await this.capture(event);
|
|
633
|
+
});
|
|
561
634
|
}
|
|
562
635
|
async flushDeferredPartUpdates() {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
636
|
+
return this.withStoreActivity(async () => {
|
|
637
|
+
if (this.pendingPartUpdateFlushPromise)
|
|
638
|
+
return this.pendingPartUpdateFlushPromise;
|
|
639
|
+
if (this.pendingPartUpdates.size === 0)
|
|
640
|
+
return;
|
|
641
|
+
this.clearDeferredPartUpdateTimer();
|
|
642
|
+
this.pendingPartUpdateFlushPromise = (async () => {
|
|
643
|
+
while (this.pendingPartUpdates.size > 0) {
|
|
644
|
+
const batch = [...this.pendingPartUpdates.values()];
|
|
645
|
+
this.pendingPartUpdates.clear();
|
|
646
|
+
for (const event of batch) {
|
|
647
|
+
await this.capture(event);
|
|
648
|
+
}
|
|
574
649
|
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
650
|
+
})().finally(() => {
|
|
651
|
+
this.pendingPartUpdateFlushPromise = undefined;
|
|
652
|
+
if (this.pendingPartUpdates.size > 0)
|
|
653
|
+
this.scheduleDeferredPartUpdateFlush();
|
|
654
|
+
});
|
|
655
|
+
return this.pendingPartUpdateFlushPromise;
|
|
580
656
|
});
|
|
581
|
-
return this.pendingPartUpdateFlushPromise;
|
|
582
657
|
}
|
|
583
658
|
async ensureDbReady() {
|
|
584
|
-
if (this.
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
659
|
+
if (!this.dbReadyPromise) {
|
|
660
|
+
if (this.db) {
|
|
661
|
+
this.scheduleDeferredInit();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
this.dbReadyPromise = this.openAndInitializeDb();
|
|
665
|
+
}
|
|
666
|
+
await this.dbReadyPromise;
|
|
667
|
+
this.scheduleDeferredInit();
|
|
590
668
|
}
|
|
591
669
|
async openAndInitializeDb() {
|
|
592
670
|
logStartupPhase('open-db:start', { dbPath: this.dbPath });
|
|
@@ -755,8 +833,6 @@ export class SqliteLcmStore {
|
|
|
755
833
|
await this.migrateLegacyArtifacts();
|
|
756
834
|
logStartupPhase('open-db:write-schema-version', { schemaVersion: STORE_SCHEMA_VERSION });
|
|
757
835
|
this.writeSchemaVersionSync(STORE_SCHEMA_VERSION);
|
|
758
|
-
logStartupPhase('open-db:deferred-init:start');
|
|
759
|
-
this.completeDeferredInit();
|
|
760
836
|
logStartupPhase('open-db:ready');
|
|
761
837
|
}
|
|
762
838
|
catch (error) {
|
|
@@ -770,6 +846,54 @@ export class SqliteLcmStore {
|
|
|
770
846
|
}
|
|
771
847
|
}
|
|
772
848
|
deferredInitCompleted = false;
|
|
849
|
+
runDeferredInit() {
|
|
850
|
+
if (this.deferredInitCompleted)
|
|
851
|
+
return Promise.resolve();
|
|
852
|
+
if (this.deferredInitPromise)
|
|
853
|
+
return this.deferredInitPromise;
|
|
854
|
+
this.deferredInitPromise = this.withStoreActivity(async () => {
|
|
855
|
+
this.deferredInitRequested = false;
|
|
856
|
+
logStartupPhase('deferred-init:start');
|
|
857
|
+
this.completeDeferredInit();
|
|
858
|
+
})
|
|
859
|
+
.catch((error) => {
|
|
860
|
+
getLogger().warn('Deferred LCM maintenance failed', {
|
|
861
|
+
message: error instanceof Error ? error.message : String(error),
|
|
862
|
+
});
|
|
863
|
+
})
|
|
864
|
+
.finally(() => {
|
|
865
|
+
this.deferredInitPromise = undefined;
|
|
866
|
+
});
|
|
867
|
+
return this.deferredInitPromise;
|
|
868
|
+
}
|
|
869
|
+
scheduleDeferredInit() {
|
|
870
|
+
if (!this.db || this.deferredInitCompleted || this.deferredInitPromise) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
this.deferredInitRequested = true;
|
|
874
|
+
if (this.activeOperationCount > 0 || this.deferredInitTimer)
|
|
875
|
+
return;
|
|
876
|
+
logStartupPhase('deferred-init:scheduled');
|
|
877
|
+
this.deferredInitTimer = setTimeout(() => {
|
|
878
|
+
this.deferredInitTimer = undefined;
|
|
879
|
+
if (this.activeOperationCount > 0) {
|
|
880
|
+
this.scheduleDeferredInit();
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
void this.runDeferredInit();
|
|
884
|
+
}, 0);
|
|
885
|
+
unrefTimer(this.deferredInitTimer);
|
|
886
|
+
}
|
|
887
|
+
async ensureDeferredInitComplete() {
|
|
888
|
+
await this.ensureDbReady();
|
|
889
|
+
if (this.deferredInitCompleted)
|
|
890
|
+
return;
|
|
891
|
+
if (this.deferredInitTimer) {
|
|
892
|
+
clearTimeout(this.deferredInitTimer);
|
|
893
|
+
this.deferredInitTimer = undefined;
|
|
894
|
+
}
|
|
895
|
+
await this.runDeferredInit();
|
|
896
|
+
}
|
|
773
897
|
readSchemaVersionSync() {
|
|
774
898
|
return firstFiniteNumber(this.getDb().prepare('PRAGMA user_version').get()) ?? 0;
|
|
775
899
|
}
|
|
@@ -819,6 +943,10 @@ export class SqliteLcmStore {
|
|
|
819
943
|
close() {
|
|
820
944
|
this.clearDeferredPartUpdateTimer();
|
|
821
945
|
this.pendingPartUpdates.clear();
|
|
946
|
+
if (this.deferredInitTimer) {
|
|
947
|
+
clearTimeout(this.deferredInitTimer);
|
|
948
|
+
this.deferredInitTimer = undefined;
|
|
949
|
+
}
|
|
822
950
|
if (!this.db)
|
|
823
951
|
return;
|
|
824
952
|
this.db.close();
|
|
@@ -826,51 +954,58 @@ export class SqliteLcmStore {
|
|
|
826
954
|
this.dbReadyPromise = undefined;
|
|
827
955
|
}
|
|
828
956
|
async capture(event) {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
-
};
|
|
957
|
+
return this.withStoreActivity(async () => {
|
|
958
|
+
const normalized = normalizeEvent(event);
|
|
959
|
+
if (!normalized)
|
|
960
|
+
return;
|
|
961
|
+
if (this.shouldSkipMalformedCapturedEvent(normalized))
|
|
962
|
+
return;
|
|
963
|
+
const shouldRecord = this.shouldRecordEvent(normalized.type);
|
|
964
|
+
const shouldPersistSession = Boolean(normalized.sessionID) && this.shouldPersistSessionForEvent(normalized.type);
|
|
965
|
+
if (!shouldRecord && !shouldPersistSession)
|
|
966
|
+
return;
|
|
967
|
+
await this.ensureDeferredInitComplete();
|
|
968
|
+
if (shouldRecord) {
|
|
969
|
+
this.writeEvent(normalized);
|
|
860
970
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
971
|
+
if (!normalized.sessionID || !shouldPersistSession)
|
|
972
|
+
return;
|
|
973
|
+
const session = resolveCaptureHydrationMode() === 'targeted'
|
|
974
|
+
? this.readSessionForCaptureSync(normalized)
|
|
975
|
+
: this.readSessionSync(normalized.sessionID);
|
|
976
|
+
const previousParentSessionID = session.parentSessionID;
|
|
977
|
+
const shouldSyncDerivedState = this.shouldSyncDerivedSessionStateForEvent(session, normalized);
|
|
978
|
+
let next = this.applyEvent(session, normalized);
|
|
979
|
+
next.updatedAt = Math.max(next.updatedAt, normalized.timestamp);
|
|
980
|
+
next.eventCount += 1;
|
|
981
|
+
next = this.prepareSessionForPersistence(next);
|
|
982
|
+
await this.persistCapturedSession(next, normalized);
|
|
983
|
+
if (this.shouldRefreshLineageForEvent(normalized.type)) {
|
|
984
|
+
this.refreshAllLineageSync();
|
|
985
|
+
const refreshed = this.readSessionHeaderSync(normalized.sessionID);
|
|
986
|
+
if (refreshed) {
|
|
987
|
+
next = {
|
|
988
|
+
...next,
|
|
989
|
+
parentSessionID: refreshed.parentSessionID,
|
|
990
|
+
rootSessionID: refreshed.rootSessionID,
|
|
991
|
+
lineageDepth: refreshed.lineageDepth,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
if (shouldSyncDerivedState) {
|
|
996
|
+
this.syncDerivedSessionStateSync(this.readSessionSync(normalized.sessionID));
|
|
997
|
+
}
|
|
998
|
+
if (this.shouldSyncDerivedLineageSubtree(normalized.type, previousParentSessionID, next.parentSessionID)) {
|
|
999
|
+
this.syncDerivedLineageSubtreeSync(normalized.sessionID, true);
|
|
1000
|
+
}
|
|
1001
|
+
if (this.shouldCleanupOrphanBlobsForEvent(normalized.type)) {
|
|
1002
|
+
this.deleteOrphanArtifactBlobsSync();
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
871
1005
|
}
|
|
872
1006
|
async stats() {
|
|
873
1007
|
await this.prepareForRead();
|
|
1008
|
+
await this.ensureDeferredInitComplete();
|
|
874
1009
|
const db = this.getDb();
|
|
875
1010
|
const totalRow = validateRow(db.prepare('SELECT COUNT(*) AS count, MAX(ts) AS latest FROM events').get(), { count: 'number', latest: 'nullable' }, 'stats.totalEvents');
|
|
876
1011
|
const sessionRow = validateRow(db.prepare('SELECT COUNT(*) AS count FROM sessions').get(), { count: 'number' }, 'stats.sessionCount');
|
|
@@ -920,13 +1055,13 @@ export class SqliteLcmStore {
|
|
|
920
1055
|
};
|
|
921
1056
|
}
|
|
922
1057
|
async grep(input) {
|
|
923
|
-
await this.prepareForRead();
|
|
924
1058
|
const resolvedScope = this.resolveConfiguredScope('grep', input.scope, input.sessionID);
|
|
925
|
-
const sessionIDs = this.resolveScopeSessionIDs(resolvedScope, input.sessionID);
|
|
926
1059
|
const limit = input.limit ?? 5;
|
|
927
1060
|
const needle = input.query.trim();
|
|
928
1061
|
if (!needle)
|
|
929
1062
|
return [];
|
|
1063
|
+
await this.prepareForRead();
|
|
1064
|
+
const sessionIDs = this.resolveScopeSessionIDs(resolvedScope, input.sessionID);
|
|
930
1065
|
const ftsResults = this.searchWithFts(needle, sessionIDs, limit);
|
|
931
1066
|
if (ftsResults.length > 0)
|
|
932
1067
|
return ftsResults;
|
|
@@ -1261,8 +1396,9 @@ export class SqliteLcmStore {
|
|
|
1261
1396
|
}
|
|
1262
1397
|
}
|
|
1263
1398
|
syncDerivedSessionStateSync(session, preserveExistingResume = false) {
|
|
1264
|
-
const
|
|
1265
|
-
this.
|
|
1399
|
+
const sanitizedSession = this.sanitizeSessionMessages(session, 'syncDerivedSessionStateSync');
|
|
1400
|
+
const roots = this.ensureSummaryGraphSync(sanitizedSession.sessionID, this.getArchivedMessages(sanitizedSession.messages));
|
|
1401
|
+
this.writeResumeSync(sanitizedSession, roots, preserveExistingResume);
|
|
1266
1402
|
return roots;
|
|
1267
1403
|
}
|
|
1268
1404
|
syncDerivedLineageSubtreeSync(sessionID, preserveExistingResume = false) {
|
|
@@ -1864,66 +2000,68 @@ export class SqliteLcmStore {
|
|
|
1864
2000
|
};
|
|
1865
2001
|
}
|
|
1866
2002
|
async compactEventLog(input) {
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
2003
|
+
return this.withStoreActivity(async () => {
|
|
2004
|
+
await this.prepareForRead();
|
|
2005
|
+
const apply = input?.apply ?? false;
|
|
2006
|
+
const vacuum = input?.vacuum ?? true;
|
|
2007
|
+
const limit = clamp(input?.limit ?? 10, 1, 50);
|
|
2008
|
+
const candidates = this.readPrunableEventTypeCountsSync();
|
|
2009
|
+
const candidateEvents = candidates.reduce((sum, row) => sum + row.count, 0);
|
|
2010
|
+
const beforeSizes = await this.readStoreFileSizes();
|
|
2011
|
+
if (!apply || candidateEvents === 0) {
|
|
2012
|
+
return [
|
|
2013
|
+
`candidate_events=${candidateEvents}`,
|
|
2014
|
+
`apply=false`,
|
|
2015
|
+
`vacuum_requested=${vacuum}`,
|
|
2016
|
+
`db_bytes=${beforeSizes.dbBytes}`,
|
|
2017
|
+
`wal_bytes=${beforeSizes.walBytes}`,
|
|
2018
|
+
`shm_bytes=${beforeSizes.shmBytes}`,
|
|
2019
|
+
`total_bytes=${beforeSizes.totalBytes}`,
|
|
2020
|
+
...(candidates.length > 0
|
|
2021
|
+
? [
|
|
2022
|
+
'candidate_event_types:',
|
|
2023
|
+
...candidates.slice(0, limit).map((row) => `- ${row.eventType} count=${row.count}`),
|
|
2024
|
+
]
|
|
2025
|
+
: ['candidate_event_types:', '- none']),
|
|
2026
|
+
].join('\n');
|
|
2027
|
+
}
|
|
2028
|
+
const eventTypes = candidates.map((row) => row.eventType);
|
|
2029
|
+
if (eventTypes.length > 0) {
|
|
2030
|
+
const placeholders = eventTypes.map(() => '?').join(', ');
|
|
2031
|
+
this.getDb()
|
|
2032
|
+
.prepare(`DELETE FROM events WHERE event_type IN (${placeholders})`)
|
|
2033
|
+
.run(...eventTypes);
|
|
2034
|
+
}
|
|
2035
|
+
let vacuumApplied = false;
|
|
2036
|
+
this.getDb().exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
2037
|
+
if (vacuum) {
|
|
2038
|
+
this.getDb().exec('VACUUM');
|
|
2039
|
+
this.getDb().exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
2040
|
+
vacuumApplied = true;
|
|
2041
|
+
}
|
|
2042
|
+
const afterSizes = await this.readStoreFileSizes();
|
|
1875
2043
|
return [
|
|
1876
2044
|
`candidate_events=${candidateEvents}`,
|
|
1877
|
-
`
|
|
2045
|
+
`deleted_events=${candidateEvents}`,
|
|
2046
|
+
`apply=true`,
|
|
1878
2047
|
`vacuum_requested=${vacuum}`,
|
|
1879
|
-
`
|
|
1880
|
-
`
|
|
1881
|
-
`
|
|
1882
|
-
`
|
|
2048
|
+
`vacuum_applied=${vacuumApplied}`,
|
|
2049
|
+
`db_bytes_before=${beforeSizes.dbBytes}`,
|
|
2050
|
+
`wal_bytes_before=${beforeSizes.walBytes}`,
|
|
2051
|
+
`shm_bytes_before=${beforeSizes.shmBytes}`,
|
|
2052
|
+
`total_bytes_before=${beforeSizes.totalBytes}`,
|
|
2053
|
+
`db_bytes_after=${afterSizes.dbBytes}`,
|
|
2054
|
+
`wal_bytes_after=${afterSizes.walBytes}`,
|
|
2055
|
+
`shm_bytes_after=${afterSizes.shmBytes}`,
|
|
2056
|
+
`total_bytes_after=${afterSizes.totalBytes}`,
|
|
1883
2057
|
...(candidates.length > 0
|
|
1884
2058
|
? [
|
|
1885
|
-
'
|
|
2059
|
+
'deleted_event_types:',
|
|
1886
2060
|
...candidates.slice(0, limit).map((row) => `- ${row.eventType} count=${row.count}`),
|
|
1887
2061
|
]
|
|
1888
|
-
: ['
|
|
2062
|
+
: ['deleted_event_types:', '- none']),
|
|
1889
2063
|
].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');
|
|
2064
|
+
});
|
|
1927
2065
|
}
|
|
1928
2066
|
async retentionReport(input) {
|
|
1929
2067
|
await this.prepareForRead();
|
|
@@ -2051,44 +2189,50 @@ export class SqliteLcmStore {
|
|
|
2051
2189
|
].join('\n');
|
|
2052
2190
|
}
|
|
2053
2191
|
async exportSnapshot(input) {
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2192
|
+
return this.withStoreActivity(async () => {
|
|
2193
|
+
await this.prepareForRead();
|
|
2194
|
+
return exportStoreSnapshot({
|
|
2195
|
+
workspaceDirectory: this.workspaceDirectory,
|
|
2196
|
+
normalizeScope: this.normalizeScope.bind(this),
|
|
2197
|
+
resolveScopeSessionIDs: this.resolveScopeSessionIDs.bind(this),
|
|
2198
|
+
readScopedSessionRowsSync: this.readScopedSessionRowsSync.bind(this),
|
|
2199
|
+
readScopedMessageRowsSync: this.readScopedMessageRowsSync.bind(this),
|
|
2200
|
+
readScopedPartRowsSync: this.readScopedPartRowsSync.bind(this),
|
|
2201
|
+
readScopedResumeRowsSync: this.readScopedResumeRowsSync.bind(this),
|
|
2202
|
+
readScopedArtifactRowsSync: this.readScopedArtifactRowsSync.bind(this),
|
|
2203
|
+
readScopedArtifactBlobRowsSync: this.readScopedArtifactBlobRowsSync.bind(this),
|
|
2204
|
+
readScopedSummaryRowsSync: this.readScopedSummaryRowsSync.bind(this),
|
|
2205
|
+
readScopedSummaryEdgeRowsSync: this.readScopedSummaryEdgeRowsSync.bind(this),
|
|
2206
|
+
readScopedSummaryStateRowsSync: this.readScopedSummaryStateRowsSync.bind(this),
|
|
2207
|
+
}, input);
|
|
2208
|
+
});
|
|
2069
2209
|
}
|
|
2070
2210
|
async importSnapshot(input) {
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2211
|
+
return this.withStoreActivity(async () => {
|
|
2212
|
+
await this.prepareForRead();
|
|
2213
|
+
return importStoreSnapshot({
|
|
2214
|
+
workspaceDirectory: this.workspaceDirectory,
|
|
2215
|
+
getDb: () => this.getDb(),
|
|
2216
|
+
clearSessionDataSync: this.clearSessionDataSync.bind(this),
|
|
2217
|
+
backfillArtifactBlobsSync: this.backfillArtifactBlobsSync.bind(this),
|
|
2218
|
+
refreshAllLineageSync: this.refreshAllLineageSync.bind(this),
|
|
2219
|
+
syncAllDerivedSessionStateSync: this.syncAllDerivedSessionStateSync.bind(this),
|
|
2220
|
+
refreshSearchIndexesSync: this.refreshSearchIndexesSync.bind(this),
|
|
2221
|
+
}, input);
|
|
2222
|
+
});
|
|
2081
2223
|
}
|
|
2082
2224
|
async resume(sessionID) {
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2225
|
+
return this.withStoreActivity(async () => {
|
|
2226
|
+
await this.prepareForRead();
|
|
2227
|
+
const resolvedSessionID = sessionID ?? this.latestSessionIDSync();
|
|
2228
|
+
if (!resolvedSessionID)
|
|
2229
|
+
return 'No stored resume snapshots yet.';
|
|
2230
|
+
const existing = this.getResumeSync(resolvedSessionID);
|
|
2231
|
+
if (existing && !this.isManagedResumeNote(existing))
|
|
2232
|
+
return existing;
|
|
2233
|
+
const generated = await this.buildCompactionContext(resolvedSessionID);
|
|
2234
|
+
return generated ?? existing ?? 'No stored resume snapshot for that session.';
|
|
2235
|
+
});
|
|
2092
2236
|
}
|
|
2093
2237
|
async expand(input) {
|
|
2094
2238
|
await this.prepareForRead();
|
|
@@ -2145,45 +2289,53 @@ export class SqliteLcmStore {
|
|
|
2145
2289
|
return note;
|
|
2146
2290
|
}
|
|
2147
2291
|
async transformMessages(messages) {
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2292
|
+
return this.withStoreActivity(async () => {
|
|
2293
|
+
const validMessages = filterValidConversationMessages(messages, {
|
|
2294
|
+
operation: 'transformMessages',
|
|
2295
|
+
});
|
|
2296
|
+
if (validMessages.length !== messages.length) {
|
|
2297
|
+
messages.splice(0, messages.length, ...validMessages);
|
|
2298
|
+
}
|
|
2299
|
+
if (messages.length < this.options.minMessagesForTransform)
|
|
2300
|
+
return false;
|
|
2301
|
+
const window = resolveArchiveTransformWindow(messages, this.options.freshTailMessages);
|
|
2302
|
+
if (!window)
|
|
2303
|
+
return false;
|
|
2304
|
+
await this.prepareForRead();
|
|
2305
|
+
const { anchor, archived, recent } = window;
|
|
2306
|
+
const roots = this.ensureSummaryGraphSync(anchor.info.sessionID, archived);
|
|
2307
|
+
if (roots.length === 0)
|
|
2308
|
+
return false;
|
|
2309
|
+
const summary = buildActiveSummaryText(roots, archived.length, this.options.summaryCharBudget);
|
|
2310
|
+
const retrieval = await this.buildAutomaticRetrievalContext(anchor.info.sessionID, recent, anchor);
|
|
2311
|
+
for (const message of archived) {
|
|
2312
|
+
this.compactMessageInPlace(message);
|
|
2313
|
+
}
|
|
2314
|
+
anchor.parts = anchor.parts.filter((part) => !isSyntheticLcmTextPart(part, ['archive-summary', 'retrieved-context']));
|
|
2315
|
+
const syntheticParts = [];
|
|
2316
|
+
if (retrieval) {
|
|
2317
|
+
syntheticParts.push({
|
|
2318
|
+
id: `lcm-memory-${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
|
2319
|
+
sessionID: anchor.info.sessionID,
|
|
2320
|
+
messageID: anchor.info.id,
|
|
2321
|
+
type: 'text',
|
|
2322
|
+
text: retrieval,
|
|
2323
|
+
synthetic: true,
|
|
2324
|
+
metadata: { opencodeLcm: 'retrieved-context' },
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2166
2327
|
syntheticParts.push({
|
|
2167
|
-
id: `lcm-
|
|
2328
|
+
id: `lcm-summary-${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
|
2168
2329
|
sessionID: anchor.info.sessionID,
|
|
2169
2330
|
messageID: anchor.info.id,
|
|
2170
2331
|
type: 'text',
|
|
2171
|
-
text:
|
|
2332
|
+
text: summary,
|
|
2172
2333
|
synthetic: true,
|
|
2173
|
-
metadata: { opencodeLcm: '
|
|
2334
|
+
metadata: { opencodeLcm: 'archive-summary' },
|
|
2174
2335
|
});
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
id: `lcm-summary-${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
|
2178
|
-
sessionID: anchor.info.sessionID,
|
|
2179
|
-
messageID: anchor.info.id,
|
|
2180
|
-
type: 'text',
|
|
2181
|
-
text: summary,
|
|
2182
|
-
synthetic: true,
|
|
2183
|
-
metadata: { opencodeLcm: 'archive-summary' },
|
|
2336
|
+
anchor.parts.push(...syntheticParts);
|
|
2337
|
+
return true;
|
|
2184
2338
|
});
|
|
2185
|
-
anchor.parts.push(...syntheticParts);
|
|
2186
|
-
return true;
|
|
2187
2339
|
}
|
|
2188
2340
|
systemHint() {
|
|
2189
2341
|
if (!this.options.systemHint)
|
|
@@ -2195,6 +2347,40 @@ export class SqliteLcmStore {
|
|
|
2195
2347
|
'Keep ctx_* usage selective and treat those calls as infrastructure, not task intent.',
|
|
2196
2348
|
].join(' ');
|
|
2197
2349
|
}
|
|
2350
|
+
sanitizeSessionMessages(session, operation) {
|
|
2351
|
+
const messages = filterValidConversationMessages(session.messages, {
|
|
2352
|
+
operation,
|
|
2353
|
+
sessionID: session.sessionID,
|
|
2354
|
+
});
|
|
2355
|
+
return messages.length === session.messages.length ? session : { ...session, messages };
|
|
2356
|
+
}
|
|
2357
|
+
shouldSkipMalformedCapturedEvent(event) {
|
|
2358
|
+
const payload = event.payload;
|
|
2359
|
+
switch (payload.type) {
|
|
2360
|
+
case 'message.updated': {
|
|
2361
|
+
if (getValidMessageInfo(payload.properties.info))
|
|
2362
|
+
return false;
|
|
2363
|
+
logMalformedMessage('Skipping malformed message.updated event', {
|
|
2364
|
+
operation: 'capture',
|
|
2365
|
+
sessionID: event.sessionID,
|
|
2366
|
+
eventType: payload.type,
|
|
2367
|
+
});
|
|
2368
|
+
return true;
|
|
2369
|
+
}
|
|
2370
|
+
case 'message.part.updated': {
|
|
2371
|
+
if (isValidMessagePartUpdate(payload))
|
|
2372
|
+
return false;
|
|
2373
|
+
logMalformedMessage('Skipping malformed message.part.updated event', {
|
|
2374
|
+
operation: 'capture',
|
|
2375
|
+
sessionID: event.sessionID,
|
|
2376
|
+
eventType: payload.type,
|
|
2377
|
+
});
|
|
2378
|
+
return true;
|
|
2379
|
+
}
|
|
2380
|
+
default:
|
|
2381
|
+
return false;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2198
2384
|
async buildAutomaticRetrievalContext(sessionID, recent, anchor) {
|
|
2199
2385
|
if (!this.options.automaticRetrieval.enabled)
|
|
2200
2386
|
return undefined;
|
|
@@ -3001,9 +3187,17 @@ export class SqliteLcmStore {
|
|
|
3001
3187
|
return searchByScanModule(this.searchDeps(), query, sessionIDs, limit);
|
|
3002
3188
|
}
|
|
3003
3189
|
replaceMessageSearchRowsSync(session) {
|
|
3004
|
-
|
|
3190
|
+
const sanitizedSession = this.sanitizeSessionMessages(session, 'replaceMessageSearchRowsSync');
|
|
3191
|
+
replaceMessageSearchRowsModule(this.searchDeps(), redactStructuredValue(sanitizedSession, this.privacy));
|
|
3005
3192
|
}
|
|
3006
3193
|
replaceMessageSearchRowSync(sessionID, message) {
|
|
3194
|
+
if (!getValidMessageInfo(message.info)) {
|
|
3195
|
+
logMalformedMessage('Skipping malformed message search row', {
|
|
3196
|
+
operation: 'replaceMessageSearchRowSync',
|
|
3197
|
+
sessionID,
|
|
3198
|
+
});
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3007
3201
|
replaceMessageSearchRowModule(this.searchDeps(), sessionID, redactStructuredValue(message, this.privacy));
|
|
3008
3202
|
}
|
|
3009
3203
|
refreshSearchIndexesSync(sessionIDs) {
|
|
@@ -3132,15 +3326,29 @@ export class SqliteLcmStore {
|
|
|
3132
3326
|
resolveLineageSync(sessionID, parentSessionID) {
|
|
3133
3327
|
if (!parentSessionID)
|
|
3134
3328
|
return { rootSessionID: sessionID, lineageDepth: 0 };
|
|
3135
|
-
const
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3329
|
+
const seen = new Set([sessionID]);
|
|
3330
|
+
let currentSessionID = parentSessionID;
|
|
3331
|
+
let lineageDepth = 1;
|
|
3332
|
+
while (currentSessionID && !seen.has(currentSessionID)) {
|
|
3333
|
+
seen.add(currentSessionID);
|
|
3334
|
+
const parent = this.getDb()
|
|
3335
|
+
.prepare('SELECT parent_session_id, root_session_id, lineage_depth FROM sessions WHERE session_id = ?')
|
|
3336
|
+
.get(currentSessionID);
|
|
3337
|
+
if (!parent)
|
|
3338
|
+
return { rootSessionID: currentSessionID, lineageDepth };
|
|
3339
|
+
if (parent.root_session_id && parent.lineage_depth !== null) {
|
|
3340
|
+
return {
|
|
3341
|
+
rootSessionID: parent.root_session_id,
|
|
3342
|
+
lineageDepth: parent.lineage_depth + lineageDepth,
|
|
3343
|
+
};
|
|
3344
|
+
}
|
|
3345
|
+
if (!parent.parent_session_id) {
|
|
3346
|
+
return { rootSessionID: currentSessionID, lineageDepth };
|
|
3347
|
+
}
|
|
3348
|
+
currentSessionID = parent.parent_session_id;
|
|
3349
|
+
lineageDepth += 1;
|
|
3350
|
+
}
|
|
3351
|
+
return { rootSessionID: parentSessionID, lineageDepth };
|
|
3144
3352
|
}
|
|
3145
3353
|
applyEvent(session, event) {
|
|
3146
3354
|
const payload = event.payload;
|
|
@@ -3200,26 +3408,33 @@ export class SqliteLcmStore {
|
|
|
3200
3408
|
const row = safeQueryOne(this.getDb().prepare('SELECT note FROM resumes WHERE session_id = ?'), [sessionID], 'getResumeSync');
|
|
3201
3409
|
return row?.note;
|
|
3202
3410
|
}
|
|
3203
|
-
|
|
3204
|
-
const
|
|
3205
|
-
|
|
3206
|
-
|
|
3411
|
+
materializeSessionRow(row, messages = []) {
|
|
3412
|
+
const parentSessionID = row.parent_session_id ?? undefined;
|
|
3413
|
+
const derivedLineage = row.root_session_id === null || row.lineage_depth === null
|
|
3414
|
+
? this.resolveLineageSync(row.session_id, parentSessionID)
|
|
3415
|
+
: undefined;
|
|
3207
3416
|
return {
|
|
3208
3417
|
sessionID: row.session_id,
|
|
3209
3418
|
title: row.title ?? undefined,
|
|
3210
3419
|
directory: row.session_directory ?? undefined,
|
|
3211
|
-
parentSessionID
|
|
3212
|
-
rootSessionID: row.root_session_id ??
|
|
3213
|
-
lineageDepth: row.lineage_depth ??
|
|
3420
|
+
parentSessionID,
|
|
3421
|
+
rootSessionID: row.root_session_id ?? derivedLineage?.rootSessionID,
|
|
3422
|
+
lineageDepth: row.lineage_depth ?? derivedLineage?.lineageDepth,
|
|
3214
3423
|
pinned: Boolean(row.pinned),
|
|
3215
3424
|
pinReason: row.pin_reason ?? undefined,
|
|
3216
3425
|
updatedAt: row.updated_at,
|
|
3217
3426
|
compactedAt: row.compacted_at ?? undefined,
|
|
3218
3427
|
deleted: Boolean(row.deleted),
|
|
3219
3428
|
eventCount: row.event_count,
|
|
3220
|
-
messages
|
|
3429
|
+
messages,
|
|
3221
3430
|
};
|
|
3222
3431
|
}
|
|
3432
|
+
readSessionHeaderSync(sessionID) {
|
|
3433
|
+
const row = safeQueryOne(this.getDb().prepare('SELECT * FROM sessions WHERE session_id = ?'), [sessionID], 'readSessionHeaderSync');
|
|
3434
|
+
if (!row)
|
|
3435
|
+
return undefined;
|
|
3436
|
+
return this.materializeSessionRow(row);
|
|
3437
|
+
}
|
|
3223
3438
|
clearSessionDataSync(sessionID) {
|
|
3224
3439
|
const db = this.getDb();
|
|
3225
3440
|
db.prepare('DELETE FROM message_fts WHERE session_id = ?').run(sessionID);
|
|
@@ -3355,25 +3570,14 @@ export class SqliteLcmStore {
|
|
|
3355
3570
|
// Build NormalizedSession results
|
|
3356
3571
|
return sessionIDs.map((sessionID) => {
|
|
3357
3572
|
const row = sessionMap.get(sessionID);
|
|
3358
|
-
const messages = messagesBySession.get(sessionID) ?? []
|
|
3573
|
+
const messages = filterValidConversationMessages(messagesBySession.get(sessionID) ?? [], {
|
|
3574
|
+
operation: 'readSessionsBatchSync',
|
|
3575
|
+
sessionID,
|
|
3576
|
+
});
|
|
3359
3577
|
if (!row) {
|
|
3360
3578
|
return { ...emptySession(sessionID), messages };
|
|
3361
3579
|
}
|
|
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
|
-
};
|
|
3580
|
+
return this.materializeSessionRow(row, messages);
|
|
3377
3581
|
});
|
|
3378
3582
|
}
|
|
3379
3583
|
readSessionSync(sessionID) {
|
|
@@ -3400,28 +3604,14 @@ export class SqliteLcmStore {
|
|
|
3400
3604
|
parts.push(part);
|
|
3401
3605
|
partsByMessage.set(partRow.message_id, parts);
|
|
3402
3606
|
}
|
|
3403
|
-
const messages = messageRows.map((messageRow) => ({
|
|
3607
|
+
const messages = filterValidConversationMessages(messageRows.map((messageRow) => ({
|
|
3404
3608
|
info: parseJson(messageRow.info_json),
|
|
3405
3609
|
parts: partsByMessage.get(messageRow.message_id) ?? [],
|
|
3406
|
-
}));
|
|
3610
|
+
})), { operation: 'readSessionSync', sessionID });
|
|
3407
3611
|
if (!row) {
|
|
3408
3612
|
return { ...emptySession(sessionID), messages };
|
|
3409
3613
|
}
|
|
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
|
-
};
|
|
3614
|
+
return this.materializeSessionRow(row, messages);
|
|
3425
3615
|
}
|
|
3426
3616
|
prepareSessionForPersistence(session) {
|
|
3427
3617
|
const parentSessionID = this.sanitizeParentSessionIDSync(session.sessionID, session.parentSessionID);
|
|
@@ -3492,8 +3682,16 @@ export class SqliteLcmStore {
|
|
|
3492
3682
|
const parts = db
|
|
3493
3683
|
.prepare('SELECT * FROM parts WHERE session_id = ? AND message_id = ? ORDER BY sort_key ASC, part_id ASC')
|
|
3494
3684
|
.all(sessionID, messageID);
|
|
3685
|
+
const info = parseJson(row.info_json);
|
|
3686
|
+
if (!getValidMessageInfo(info)) {
|
|
3687
|
+
logMalformedMessage('Skipping malformed stored message', {
|
|
3688
|
+
operation: 'readMessageSync',
|
|
3689
|
+
sessionID,
|
|
3690
|
+
}, { messageID });
|
|
3691
|
+
return undefined;
|
|
3692
|
+
}
|
|
3495
3693
|
return {
|
|
3496
|
-
info
|
|
3694
|
+
info,
|
|
3497
3695
|
parts: parts.map((partRow) => {
|
|
3498
3696
|
const part = parseJson(partRow.part_json);
|
|
3499
3697
|
hydratePartFromArtifacts(part, artifactsByPart.get(part.id) ?? []);
|
|
@@ -3607,7 +3805,15 @@ export class SqliteLcmStore {
|
|
|
3607
3805
|
event_count = excluded.event_count`).run(session.sessionID, title ?? null, directory ?? null, worktreeKey ?? null, session.parentSessionID ?? null, session.rootSessionID ?? session.sessionID, session.lineageDepth ?? 0, session.pinned ? 1 : 0, pinReason ?? null, session.updatedAt, session.compactedAt ?? null, session.deleted ? 1 : 0, session.eventCount);
|
|
3608
3806
|
}
|
|
3609
3807
|
upsertMessageInfoSync(sessionID, message) {
|
|
3610
|
-
const
|
|
3808
|
+
const validated = getValidMessageInfo(message.info);
|
|
3809
|
+
if (!validated) {
|
|
3810
|
+
logMalformedMessage('Skipping malformed message metadata', {
|
|
3811
|
+
operation: 'upsertMessageInfoSync',
|
|
3812
|
+
sessionID,
|
|
3813
|
+
});
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
const info = redactStructuredValue(validated, this.privacy);
|
|
3611
3817
|
this.getDb()
|
|
3612
3818
|
.prepare(`INSERT INTO messages (message_id, session_id, created_at, info_json)
|
|
3613
3819
|
VALUES (?, ?, ?, ?)
|
|
@@ -3638,7 +3844,7 @@ export class SqliteLcmStore {
|
|
|
3638
3844
|
return buildArtifactSearchContentModule(artifact);
|
|
3639
3845
|
}
|
|
3640
3846
|
async externalizeSession(session) {
|
|
3641
|
-
return externalizeSessionModule(this.artifactDeps(), session);
|
|
3847
|
+
return externalizeSessionModule(this.artifactDeps(), this.sanitizeSessionMessages(session, 'externalizeSession'));
|
|
3642
3848
|
}
|
|
3643
3849
|
writeEvent(event) {
|
|
3644
3850
|
const payloadStub = event.type.startsWith('message.') || event.type.startsWith('session.')
|