opencode-lcm 0.13.2 → 0.13.5
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 +20 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/options.d.ts +2 -1
- package/dist/options.js +16 -0
- package/dist/store-artifacts.js +3 -3
- package/dist/store-search.js +3 -3
- package/dist/store-snapshot.d.ts +2 -0
- package/dist/store-snapshot.js +4 -3
- package/dist/store.d.ts +3 -0
- package/dist/store.js +150 -48
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
- package/src/constants.ts +1 -1
- package/src/options.ts +21 -0
- package/src/store-artifacts.ts +3 -3
- package/src/store-search.ts +9 -3
- package/src/store-snapshot.ts +9 -2
- package/src/store.ts +191 -52
- package/src/types.ts +8 -0
package/dist/store.js
CHANGED
|
@@ -170,6 +170,8 @@ function getValidMessageInfo(info) {
|
|
|
170
170
|
return info;
|
|
171
171
|
}
|
|
172
172
|
function filterValidConversationMessages(messages, context) {
|
|
173
|
+
if (context?.operation === 'transformMessages')
|
|
174
|
+
return messages;
|
|
173
175
|
const valid = messages.filter((message) => Boolean(getValidMessageInfo(message?.info)));
|
|
174
176
|
const dropped = messages.length - valid.length;
|
|
175
177
|
if (dropped > 0 && context) {
|
|
@@ -204,16 +206,18 @@ function getDeferredPartUpdateKey(event) {
|
|
|
204
206
|
return undefined;
|
|
205
207
|
return `${event.properties.part.sessionID}:${event.properties.part.messageID}:${event.properties.part.id}`;
|
|
206
208
|
}
|
|
209
|
+
function messageCreatedAt(message) {
|
|
210
|
+
const created = message?.info?.time?.created;
|
|
211
|
+
return typeof created === 'number' && Number.isFinite(created) ? created : 0;
|
|
212
|
+
}
|
|
213
|
+
function messageParts(message) {
|
|
214
|
+
return Array.isArray(message?.parts) ? message.parts : [];
|
|
215
|
+
}
|
|
216
|
+
function signatureString(value, fallback = '') {
|
|
217
|
+
return typeof value === 'string' ? value : fallback;
|
|
218
|
+
}
|
|
207
219
|
function compareMessages(a, b) {
|
|
208
|
-
|
|
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);
|
|
220
|
+
return messageCreatedAt(a) - messageCreatedAt(b);
|
|
217
221
|
}
|
|
218
222
|
function emptySession(sessionID) {
|
|
219
223
|
return {
|
|
@@ -289,22 +293,24 @@ function isSyntheticLcmTextPart(part, markers) {
|
|
|
289
293
|
}
|
|
290
294
|
function guessMessageText(message, ignoreToolPrefixes) {
|
|
291
295
|
const segments = [];
|
|
292
|
-
for (const part of message
|
|
296
|
+
for (const part of messageParts(message)) {
|
|
293
297
|
switch (part.type) {
|
|
294
298
|
case 'text': {
|
|
295
299
|
if (isSyntheticLcmTextPart(part, ['archive-summary', 'retrieved-context', 'archived-part']))
|
|
296
300
|
break;
|
|
297
|
-
|
|
301
|
+
const text = typeof part.text === 'string' ? part.text : '';
|
|
302
|
+
if (text.startsWith('[Archived by opencode-lcm:'))
|
|
298
303
|
break;
|
|
299
|
-
const sanitized = sanitizeAutomaticRetrievalSourceText(
|
|
304
|
+
const sanitized = sanitizeAutomaticRetrievalSourceText(text);
|
|
300
305
|
if (sanitized)
|
|
301
306
|
segments.push(sanitized);
|
|
302
307
|
break;
|
|
303
308
|
}
|
|
304
309
|
case 'reasoning': {
|
|
305
|
-
|
|
310
|
+
const text = typeof part.text === 'string' ? part.text : '';
|
|
311
|
+
if (text.startsWith('[Archived by opencode-lcm:'))
|
|
306
312
|
break;
|
|
307
|
-
const sanitized = sanitizeAutomaticRetrievalSourceText(
|
|
313
|
+
const sanitized = sanitizeAutomaticRetrievalSourceText(text);
|
|
308
314
|
if (sanitized)
|
|
309
315
|
segments.push(sanitized);
|
|
310
316
|
break;
|
|
@@ -317,15 +323,16 @@ function guessMessageText(message, ignoreToolPrefixes) {
|
|
|
317
323
|
break;
|
|
318
324
|
}
|
|
319
325
|
case 'tool': {
|
|
320
|
-
|
|
326
|
+
const toolName = typeof part.tool === 'string' ? part.tool : '';
|
|
327
|
+
if (ignoreToolPrefixes.some((prefix) => toolName.startsWith(prefix)))
|
|
321
328
|
break;
|
|
322
329
|
const state = part.state;
|
|
323
330
|
if (state.status === 'completed')
|
|
324
|
-
segments.push(`${
|
|
331
|
+
segments.push(`${toolName}: ${state.output}`);
|
|
325
332
|
if (state.status === 'error')
|
|
326
|
-
segments.push(`${
|
|
333
|
+
segments.push(`${toolName}: ${state.error}`);
|
|
327
334
|
if (state.status === 'pending' || state.status === 'running') {
|
|
328
|
-
segments.push(`${
|
|
335
|
+
segments.push(`${toolName}: ${JSON.stringify(state.input)}`);
|
|
329
336
|
}
|
|
330
337
|
if (state.status === 'completed' && state.attachments && state.attachments.length > 0) {
|
|
331
338
|
const attachmentNames = state.attachments
|
|
@@ -333,7 +340,7 @@ function guessMessageText(message, ignoreToolPrefixes) {
|
|
|
333
340
|
.filter(Boolean)
|
|
334
341
|
.slice(0, 4);
|
|
335
342
|
if (attachmentNames.length > 0)
|
|
336
|
-
segments.push(`${
|
|
343
|
+
segments.push(`${toolName} attachments: ${attachmentNames.join(', ')}`);
|
|
337
344
|
}
|
|
338
345
|
break;
|
|
339
346
|
}
|
|
@@ -417,6 +424,13 @@ export function resolveCaptureHydrationMode(options) {
|
|
|
417
424
|
// hydration there until Bun/Windows is proven stable again.
|
|
418
425
|
return isBunRuntime && platform === 'win32' ? 'full' : 'targeted';
|
|
419
426
|
}
|
|
427
|
+
function shouldUseLightweightPartCapture(event, options) {
|
|
428
|
+
const isBunRuntime = options?.isBunRuntime ?? (typeof globalThis === 'object' && 'Bun' in globalThis);
|
|
429
|
+
const platform = options?.platform ?? process.platform;
|
|
430
|
+
if (!isBunRuntime || platform !== 'win32')
|
|
431
|
+
return false;
|
|
432
|
+
return event.type === 'message.part.updated' || event.type === 'message.part.removed';
|
|
433
|
+
}
|
|
420
434
|
function isSqliteRuntimeImportError(runtime, error) {
|
|
421
435
|
const code = typeof error === 'object' && error && 'code' in error && typeof error.code === 'string'
|
|
422
436
|
? error.code
|
|
@@ -768,6 +782,7 @@ export class SqliteLcmStore {
|
|
|
768
782
|
end_index INTEGER NOT NULL,
|
|
769
783
|
message_ids_json TEXT NOT NULL,
|
|
770
784
|
summary_text TEXT NOT NULL,
|
|
785
|
+
strategy TEXT NOT NULL DEFAULT 'deterministic-v1',
|
|
771
786
|
created_at INTEGER NOT NULL,
|
|
772
787
|
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
773
788
|
);
|
|
@@ -823,6 +838,7 @@ export class SqliteLcmStore {
|
|
|
823
838
|
`);
|
|
824
839
|
this.ensureSessionColumnsSync();
|
|
825
840
|
this.ensureSummaryStateColumnsSync();
|
|
841
|
+
this.ensureSummaryNodeColumnsSync();
|
|
826
842
|
this.ensureArtifactColumnsSync();
|
|
827
843
|
logStartupPhase('open-db:create-indexes');
|
|
828
844
|
db.exec('CREATE INDEX IF NOT EXISTS idx_artifacts_content_hash ON artifacts(content_hash)');
|
|
@@ -970,9 +986,11 @@ export class SqliteLcmStore {
|
|
|
970
986
|
}
|
|
971
987
|
if (!normalized.sessionID || !shouldPersistSession)
|
|
972
988
|
return;
|
|
973
|
-
const session =
|
|
974
|
-
? this.readSessionForCaptureSync(normalized)
|
|
975
|
-
:
|
|
989
|
+
const session = shouldUseLightweightPartCapture(normalized)
|
|
990
|
+
? this.readSessionForCaptureSync(normalized, { hydrateArtifacts: false })
|
|
991
|
+
: resolveCaptureHydrationMode() === 'targeted'
|
|
992
|
+
? this.readSessionForCaptureSync(normalized)
|
|
993
|
+
: this.readSessionSync(normalized.sessionID);
|
|
976
994
|
const previousParentSessionID = session.parentSessionID;
|
|
977
995
|
const shouldSyncDerivedState = this.shouldSyncDerivedSessionStateForEvent(session, normalized);
|
|
978
996
|
let next = this.applyEvent(session, normalized);
|
|
@@ -1253,7 +1271,7 @@ export class SqliteLcmStore {
|
|
|
1253
1271
|
issues.push('unexpected-summary-edges');
|
|
1254
1272
|
return issues.length > 0 ? { sessionID: session.sessionID, issues } : undefined;
|
|
1255
1273
|
}
|
|
1256
|
-
const latestMessageCreated = archived.at(-1)
|
|
1274
|
+
const latestMessageCreated = messageCreatedAt(archived.at(-1));
|
|
1257
1275
|
const archivedSignature = this.buildArchivedSignature(archived);
|
|
1258
1276
|
const rootIDs = state ? parseJson(state.root_node_ids_json) : [];
|
|
1259
1277
|
const roots = rootIDs
|
|
@@ -1364,7 +1382,7 @@ export class SqliteLcmStore {
|
|
|
1364
1382
|
case 'message.updated': {
|
|
1365
1383
|
const existing = session.messages.find((message) => message.info.id === payload.properties.info.id);
|
|
1366
1384
|
if (existing) {
|
|
1367
|
-
return this.isMessageArchivedSync(session.sessionID, existing.info.id, existing
|
|
1385
|
+
return this.isMessageArchivedSync(session.sessionID, existing.info.id, messageCreatedAt(existing));
|
|
1368
1386
|
}
|
|
1369
1387
|
return this.readMessageCountSync(session.sessionID) >= this.options.freshTailMessages;
|
|
1370
1388
|
}
|
|
@@ -1378,13 +1396,13 @@ export class SqliteLcmStore {
|
|
|
1378
1396
|
const message = session.messages.find((entry) => entry.info.id === payload.properties.part.messageID);
|
|
1379
1397
|
if (!message)
|
|
1380
1398
|
return false;
|
|
1381
|
-
return this.isMessageArchivedSync(session.sessionID, message.info.id, message
|
|
1399
|
+
return this.isMessageArchivedSync(session.sessionID, message.info.id, messageCreatedAt(message));
|
|
1382
1400
|
}
|
|
1383
1401
|
case 'message.part.removed': {
|
|
1384
1402
|
const message = session.messages.find((entry) => entry.info.id === payload.properties.messageID);
|
|
1385
1403
|
if (!message)
|
|
1386
1404
|
return false;
|
|
1387
|
-
return this.isMessageArchivedSync(session.sessionID, message.info.id, message
|
|
1405
|
+
return this.isMessageArchivedSync(session.sessionID, message.info.id, messageCreatedAt(message));
|
|
1388
1406
|
}
|
|
1389
1407
|
default:
|
|
1390
1408
|
return false;
|
|
@@ -2702,6 +2720,12 @@ export class SqliteLcmStore {
|
|
|
2702
2720
|
return this.options.interop.ignoreToolPrefixes.some((prefix) => toolName.startsWith(prefix));
|
|
2703
2721
|
}
|
|
2704
2722
|
summarizeMessages(messages, limit = SUMMARY_NODE_CHAR_LIMIT) {
|
|
2723
|
+
const strategy = this.options.summaryV2?.strategy ?? 'deterministic-v1';
|
|
2724
|
+
return strategy === 'deterministic-v2'
|
|
2725
|
+
? this.summarizeMessagesDeterministicV2(messages, limit)
|
|
2726
|
+
: this.summarizeMessagesDeterministicV1(messages, limit);
|
|
2727
|
+
}
|
|
2728
|
+
summarizeMessagesDeterministicV1(messages, limit = SUMMARY_NODE_CHAR_LIMIT) {
|
|
2705
2729
|
const goals = messages
|
|
2706
2730
|
.filter((message) => message.info.role === 'user')
|
|
2707
2731
|
.map((message) => guessMessageText(message, this.options.interop.ignoreToolPrefixes))
|
|
@@ -2726,6 +2750,55 @@ export class SqliteLcmStore {
|
|
|
2726
2750
|
return truncate(`Archived messages ${messages.length}`, limit);
|
|
2727
2751
|
return truncate(segments.join(' || '), limit);
|
|
2728
2752
|
}
|
|
2753
|
+
summarizeMessagesDeterministicV2(messages, limit = SUMMARY_NODE_CHAR_LIMIT) {
|
|
2754
|
+
const perMsgBudget = this.options.summaryV2?.perMessageBudget ?? 110;
|
|
2755
|
+
const ignoreToolPrefixes = this.options.interop.ignoreToolPrefixes;
|
|
2756
|
+
const userTexts = messages
|
|
2757
|
+
.filter((message) => message.info.role === 'user')
|
|
2758
|
+
.map((message) => guessMessageText(message, ignoreToolPrefixes))
|
|
2759
|
+
.filter(Boolean);
|
|
2760
|
+
const assistantTexts = messages
|
|
2761
|
+
.filter((message) => message.info.role === 'assistant')
|
|
2762
|
+
.map((message) => guessMessageText(message, ignoreToolPrefixes))
|
|
2763
|
+
.filter(Boolean);
|
|
2764
|
+
const allFiles = [...new Set(messages.flatMap(listFiles))];
|
|
2765
|
+
const allTools = [...new Set(this.listTools(messages))];
|
|
2766
|
+
const hasErrors = messages.some((message) => message.parts.some((part) => (part.type === 'tool' && 'state' in part && part.state?.status === 'error') ||
|
|
2767
|
+
(part.type === 'text' &&
|
|
2768
|
+
/\b(?:error|exception|fail(?:ed|ure)?)\b/i.test(part.text ?? ''))));
|
|
2769
|
+
const segments = [];
|
|
2770
|
+
if (userTexts.length > 0) {
|
|
2771
|
+
const first = truncate(userTexts[0], perMsgBudget);
|
|
2772
|
+
if (userTexts.length > 1) {
|
|
2773
|
+
const last = truncate(userTexts[userTexts.length - 1], perMsgBudget);
|
|
2774
|
+
segments.push(`Goals: ${first} → ${last}`);
|
|
2775
|
+
}
|
|
2776
|
+
else {
|
|
2777
|
+
segments.push(`Goals: ${first}`);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
if (assistantTexts.length > 0) {
|
|
2781
|
+
const recent = assistantTexts
|
|
2782
|
+
.slice(-2)
|
|
2783
|
+
.map((text) => truncate(text, perMsgBudget))
|
|
2784
|
+
.join(' | ');
|
|
2785
|
+
segments.push(`Work: ${recent}`);
|
|
2786
|
+
}
|
|
2787
|
+
if (allFiles.length > 0) {
|
|
2788
|
+
const shown = allFiles.slice(0, 6).join(', ');
|
|
2789
|
+
segments.push(allFiles.length > 6 ? `Files[${allFiles.length}]: ${shown}` : `Files: ${shown}`);
|
|
2790
|
+
}
|
|
2791
|
+
if (allTools.length > 0) {
|
|
2792
|
+
const shown = allTools.slice(0, 6).join(', ');
|
|
2793
|
+
segments.push(allTools.length > 6 ? `Tools[${allTools.length}]: ${shown}` : `Tools: ${shown}`);
|
|
2794
|
+
}
|
|
2795
|
+
if (hasErrors)
|
|
2796
|
+
segments.push('⚠err');
|
|
2797
|
+
segments.push(`${messages.length}msg(u:${userTexts.length}/a:${assistantTexts.length})`);
|
|
2798
|
+
if (segments.length === 0)
|
|
2799
|
+
return truncate(`Archived ${messages.length} messages`, limit);
|
|
2800
|
+
return truncate(segments.join(' || '), limit);
|
|
2801
|
+
}
|
|
2729
2802
|
listTools(messages) {
|
|
2730
2803
|
const tools = [];
|
|
2731
2804
|
for (const message of messages) {
|
|
@@ -2742,13 +2815,13 @@ export class SqliteLcmStore {
|
|
|
2742
2815
|
buildArchivedSignature(messages) {
|
|
2743
2816
|
const hash = createHash('sha256');
|
|
2744
2817
|
for (const message of messages) {
|
|
2745
|
-
hash.update(message.info
|
|
2746
|
-
hash.update(message.info
|
|
2747
|
-
hash.update(String(message
|
|
2748
|
-
hash.update(guessMessageText(message, this.options.interop.ignoreToolPrefixes));
|
|
2818
|
+
hash.update(signatureString(message.info?.id, 'unknown-message'));
|
|
2819
|
+
hash.update(signatureString(message.info?.role, 'unknown-role'));
|
|
2820
|
+
hash.update(String(messageCreatedAt(message)));
|
|
2821
|
+
hash.update(String(guessMessageText(message, this.options.interop.ignoreToolPrefixes) ?? ''));
|
|
2749
2822
|
hash.update(JSON.stringify(listFiles(message)));
|
|
2750
2823
|
hash.update(JSON.stringify(this.listTools([message])));
|
|
2751
|
-
hash.update(String(message.
|
|
2824
|
+
hash.update(String(messageParts(message).length));
|
|
2752
2825
|
}
|
|
2753
2826
|
return hash.digest('hex');
|
|
2754
2827
|
}
|
|
@@ -2768,7 +2841,7 @@ export class SqliteLcmStore {
|
|
|
2768
2841
|
this.clearSummaryGraphSync(sessionID);
|
|
2769
2842
|
return [];
|
|
2770
2843
|
}
|
|
2771
|
-
const latestMessageCreated = archivedMessages.at(-1)
|
|
2844
|
+
const latestMessageCreated = messageCreatedAt(archivedMessages.at(-1));
|
|
2772
2845
|
const archivedSignature = this.buildArchivedSignature(archivedMessages);
|
|
2773
2846
|
const state = safeQueryOne(this.getDb().prepare('SELECT * FROM summary_state WHERE session_id = ?'), [sessionID], 'ensureSummaryGraphSync');
|
|
2774
2847
|
if (state &&
|
|
@@ -2815,6 +2888,8 @@ export class SqliteLcmStore {
|
|
|
2815
2888
|
const expectedSummaryText = this.summarizeMessages(archivedMessages.slice(node.startIndex, node.endIndex + 1));
|
|
2816
2889
|
if (node.summaryText !== expectedSummaryText)
|
|
2817
2890
|
return false;
|
|
2891
|
+
if (node.strategy !== (this.options.summaryV2?.strategy ?? 'deterministic-v1'))
|
|
2892
|
+
return false;
|
|
2818
2893
|
const children = this.readSummaryChildrenSync(node.nodeID);
|
|
2819
2894
|
if (node.nodeKind === 'leaf') {
|
|
2820
2895
|
return (children.length === 0 && node.endIndex - node.startIndex + 1 <= SUMMARY_LEAF_MESSAGES);
|
|
@@ -2849,6 +2924,7 @@ export class SqliteLcmStore {
|
|
|
2849
2924
|
}
|
|
2850
2925
|
rebuildSummaryGraphSync(sessionID, archivedMessages, archivedSignature) {
|
|
2851
2926
|
const now = Date.now();
|
|
2927
|
+
const summaryStrategy = this.options.summaryV2?.strategy ?? 'deterministic-v1';
|
|
2852
2928
|
let level = 0;
|
|
2853
2929
|
const nodes = [];
|
|
2854
2930
|
const edges = [];
|
|
@@ -2861,6 +2937,7 @@ export class SqliteLcmStore {
|
|
|
2861
2937
|
endIndex: input.endIndex,
|
|
2862
2938
|
messageIDs: input.messageIDs,
|
|
2863
2939
|
summaryText: input.summaryText,
|
|
2940
|
+
strategy: summaryStrategy,
|
|
2864
2941
|
createdAt: now,
|
|
2865
2942
|
});
|
|
2866
2943
|
let currentLevel = [];
|
|
@@ -2913,13 +2990,13 @@ export class SqliteLcmStore {
|
|
|
2913
2990
|
withTransaction(db, 'rebuildSummaryGraph', () => {
|
|
2914
2991
|
this.clearSummaryGraphSync(sessionID);
|
|
2915
2992
|
const insertNode = db.prepare(`INSERT INTO summary_nodes
|
|
2916
|
-
(node_id, session_id, level, node_kind, start_index, end_index, message_ids_json, summary_text, created_at)
|
|
2917
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
2993
|
+
(node_id, session_id, level, node_kind, start_index, end_index, message_ids_json, summary_text, strategy, created_at)
|
|
2994
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
2918
2995
|
const insertEdge = db.prepare(`INSERT INTO summary_edges (session_id, parent_id, child_id, child_position)
|
|
2919
2996
|
VALUES (?, ?, ?, ?)`);
|
|
2920
2997
|
const insertSummaryFts = db.prepare('INSERT INTO summary_fts (session_id, node_id, level, created_at, content) VALUES (?, ?, ?, ?, ?)');
|
|
2921
2998
|
for (const node of nodes) {
|
|
2922
|
-
insertNode.run(node.nodeID, node.sessionID, node.level, node.nodeKind, node.startIndex, node.endIndex, JSON.stringify(node.messageIDs), node.summaryText, node.createdAt);
|
|
2999
|
+
insertNode.run(node.nodeID, node.sessionID, node.level, node.nodeKind, node.startIndex, node.endIndex, JSON.stringify(node.messageIDs), node.summaryText, node.strategy, node.createdAt);
|
|
2923
3000
|
insertSummaryFts.run(node.sessionID, node.nodeID, String(node.level), String(node.createdAt), node.summaryText);
|
|
2924
3001
|
}
|
|
2925
3002
|
for (const edge of edges) {
|
|
@@ -2932,7 +3009,7 @@ export class SqliteLcmStore {
|
|
|
2932
3009
|
latest_message_created = excluded.latest_message_created,
|
|
2933
3010
|
archived_signature = excluded.archived_signature,
|
|
2934
3011
|
root_node_ids_json = excluded.root_node_ids_json,
|
|
2935
|
-
updated_at = excluded.updated_at`).run(sessionID, archivedMessages.length, archivedMessages.at(-1)
|
|
3012
|
+
updated_at = excluded.updated_at`).run(sessionID, archivedMessages.length, messageCreatedAt(archivedMessages.at(-1)), archivedSignature, JSON.stringify(roots.map((node) => node.nodeID)), now);
|
|
2936
3013
|
});
|
|
2937
3014
|
return roots;
|
|
2938
3015
|
}
|
|
@@ -2949,6 +3026,7 @@ export class SqliteLcmStore {
|
|
|
2949
3026
|
endIndex: row.end_index,
|
|
2950
3027
|
messageIDs: parseJson(row.message_ids_json),
|
|
2951
3028
|
summaryText: row.summary_text,
|
|
3029
|
+
strategy: row.strategy,
|
|
2952
3030
|
createdAt: row.created_at,
|
|
2953
3031
|
};
|
|
2954
3032
|
}
|
|
@@ -3229,6 +3307,14 @@ export class SqliteLcmStore {
|
|
|
3229
3307
|
return;
|
|
3230
3308
|
db.exec("ALTER TABLE summary_state ADD COLUMN archived_signature TEXT NOT NULL DEFAULT ''");
|
|
3231
3309
|
}
|
|
3310
|
+
ensureSummaryNodeColumnsSync() {
|
|
3311
|
+
const db = this.getDb();
|
|
3312
|
+
const columns = db.prepare('PRAGMA table_info(summary_nodes)').all();
|
|
3313
|
+
const names = new Set(columns.map((column) => column.name));
|
|
3314
|
+
if (names.has('strategy'))
|
|
3315
|
+
return;
|
|
3316
|
+
db.exec("ALTER TABLE summary_nodes ADD COLUMN strategy TEXT NOT NULL DEFAULT 'deterministic-v1'");
|
|
3317
|
+
}
|
|
3232
3318
|
ensureArtifactColumnsSync() {
|
|
3233
3319
|
const db = this.getDb();
|
|
3234
3320
|
const columns = db.prepare('PRAGMA table_info(artifacts)').all();
|
|
@@ -3639,7 +3725,7 @@ export class SqliteLcmStore {
|
|
|
3639
3725
|
}
|
|
3640
3726
|
return parentSessionID;
|
|
3641
3727
|
}
|
|
3642
|
-
readSessionForCaptureSync(event) {
|
|
3728
|
+
readSessionForCaptureSync(event, options) {
|
|
3643
3729
|
const sessionID = event.sessionID;
|
|
3644
3730
|
if (!sessionID)
|
|
3645
3731
|
return emptySession('');
|
|
@@ -3647,19 +3733,19 @@ export class SqliteLcmStore {
|
|
|
3647
3733
|
const payload = event.payload;
|
|
3648
3734
|
switch (payload.type) {
|
|
3649
3735
|
case 'message.updated': {
|
|
3650
|
-
const message = this.readMessageSync(sessionID, payload.properties.info.id);
|
|
3736
|
+
const message = this.readMessageSync(sessionID, payload.properties.info.id, options);
|
|
3651
3737
|
if (message)
|
|
3652
3738
|
session.messages = [message];
|
|
3653
3739
|
return session;
|
|
3654
3740
|
}
|
|
3655
3741
|
case 'message.part.updated': {
|
|
3656
|
-
const message = this.readMessageSync(sessionID, payload.properties.part.messageID);
|
|
3742
|
+
const message = this.readMessageSync(sessionID, payload.properties.part.messageID, options);
|
|
3657
3743
|
if (message)
|
|
3658
3744
|
session.messages = [message];
|
|
3659
3745
|
return session;
|
|
3660
3746
|
}
|
|
3661
3747
|
case 'message.part.removed': {
|
|
3662
|
-
const message = this.readMessageSync(sessionID, payload.properties.messageID);
|
|
3748
|
+
const message = this.readMessageSync(sessionID, payload.properties.messageID, options);
|
|
3663
3749
|
if (message)
|
|
3664
3750
|
session.messages = [message];
|
|
3665
3751
|
return session;
|
|
@@ -3668,16 +3754,19 @@ export class SqliteLcmStore {
|
|
|
3668
3754
|
return session;
|
|
3669
3755
|
}
|
|
3670
3756
|
}
|
|
3671
|
-
readMessageSync(sessionID, messageID) {
|
|
3757
|
+
readMessageSync(sessionID, messageID, options) {
|
|
3672
3758
|
const db = this.getDb();
|
|
3673
3759
|
const row = safeQueryOne(db.prepare('SELECT * FROM messages WHERE session_id = ? AND message_id = ?'), [sessionID, messageID], 'readMessageSync');
|
|
3674
3760
|
if (!row)
|
|
3675
3761
|
return undefined;
|
|
3762
|
+
const hydrateArtifacts = options?.hydrateArtifacts ?? true;
|
|
3676
3763
|
const artifactsByPart = new Map();
|
|
3677
|
-
|
|
3678
|
-
const
|
|
3679
|
-
|
|
3680
|
-
|
|
3764
|
+
if (hydrateArtifacts) {
|
|
3765
|
+
for (const artifact of this.readArtifactsForMessageSync(messageID)) {
|
|
3766
|
+
const list = artifactsByPart.get(artifact.partID) ?? [];
|
|
3767
|
+
list.push(artifact);
|
|
3768
|
+
artifactsByPart.set(artifact.partID, list);
|
|
3769
|
+
}
|
|
3681
3770
|
}
|
|
3682
3771
|
const parts = db
|
|
3683
3772
|
.prepare('SELECT * FROM parts WHERE session_id = ? AND message_id = ? ORDER BY sort_key ASC, part_id ASC')
|
|
@@ -3694,7 +3783,8 @@ export class SqliteLcmStore {
|
|
|
3694
3783
|
info,
|
|
3695
3784
|
parts: parts.map((partRow) => {
|
|
3696
3785
|
const part = parseJson(partRow.part_json);
|
|
3697
|
-
|
|
3786
|
+
if (hydrateArtifacts)
|
|
3787
|
+
hydratePartFromArtifacts(part, artifactsByPart.get(part.id) ?? []);
|
|
3698
3788
|
return part;
|
|
3699
3789
|
}),
|
|
3700
3790
|
};
|
|
@@ -3744,22 +3834,34 @@ export class SqliteLcmStore {
|
|
|
3744
3834
|
return;
|
|
3745
3835
|
case 'message.part.updated': {
|
|
3746
3836
|
const message = session.messages.find((entry) => entry.info.id === payload.properties.part.messageID);
|
|
3837
|
+
const preservedArtifacts = message
|
|
3838
|
+
? this.readArtifactsForMessageSync(message.info.id).filter((artifact) => artifact.partID !== payload.properties.part.id)
|
|
3839
|
+
: [];
|
|
3747
3840
|
const externalized = message ? await this.externalizeMessage(message) : undefined;
|
|
3748
3841
|
withTransaction(this.getDb(), 'capture', () => {
|
|
3749
3842
|
this.upsertSessionRowSync(session);
|
|
3750
3843
|
if (externalized) {
|
|
3751
|
-
this.replaceStoredMessageSync(session.sessionID, externalized.storedMessage,
|
|
3844
|
+
this.replaceStoredMessageSync(session.sessionID, externalized.storedMessage, [
|
|
3845
|
+
...preservedArtifacts,
|
|
3846
|
+
...externalized.artifacts,
|
|
3847
|
+
]);
|
|
3752
3848
|
}
|
|
3753
3849
|
});
|
|
3754
3850
|
return;
|
|
3755
3851
|
}
|
|
3756
3852
|
case 'message.part.removed': {
|
|
3757
3853
|
const message = session.messages.find((entry) => entry.info.id === payload.properties.messageID);
|
|
3854
|
+
const preservedArtifacts = message
|
|
3855
|
+
? this.readArtifactsForMessageSync(message.info.id).filter((artifact) => artifact.partID !== payload.properties.partID)
|
|
3856
|
+
: [];
|
|
3758
3857
|
const externalized = message ? await this.externalizeMessage(message) : undefined;
|
|
3759
3858
|
withTransaction(this.getDb(), 'capture', () => {
|
|
3760
3859
|
this.upsertSessionRowSync(session);
|
|
3761
3860
|
if (externalized) {
|
|
3762
|
-
this.replaceStoredMessageSync(session.sessionID, externalized.storedMessage,
|
|
3861
|
+
this.replaceStoredMessageSync(session.sessionID, externalized.storedMessage, [
|
|
3862
|
+
...preservedArtifacts,
|
|
3863
|
+
...externalized.artifacts,
|
|
3864
|
+
]);
|
|
3763
3865
|
}
|
|
3764
3866
|
});
|
|
3765
3867
|
return;
|
package/dist/types.d.ts
CHANGED
|
@@ -45,6 +45,11 @@ export type AutomaticRetrievalOptions = {
|
|
|
45
45
|
scopeBudgets: AutomaticRetrievalScopeBudgets;
|
|
46
46
|
stop: AutomaticRetrievalStopOptions;
|
|
47
47
|
};
|
|
48
|
+
export type SummaryStrategyName = 'deterministic-v1' | 'deterministic-v2';
|
|
49
|
+
export type SummaryV2Options = {
|
|
50
|
+
strategy: SummaryStrategyName;
|
|
51
|
+
perMessageBudget: number;
|
|
52
|
+
};
|
|
48
53
|
export type OpencodeLcmOptions = {
|
|
49
54
|
interop: InteropOptions;
|
|
50
55
|
scopeDefaults: ScopeDefaults;
|
|
@@ -64,6 +69,7 @@ export type OpencodeLcmOptions = {
|
|
|
64
69
|
artifactViewChars: number;
|
|
65
70
|
binaryPreviewProviders: string[];
|
|
66
71
|
previewBytePeek: number;
|
|
72
|
+
summaryV2: SummaryV2Options;
|
|
67
73
|
};
|
|
68
74
|
export type CapturedEvent = {
|
|
69
75
|
id: string;
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const SUMMARY_BRANCH_FACTOR = 3;
|
|
|
9
9
|
export const SUMMARY_NODE_CHAR_LIMIT = 260;
|
|
10
10
|
|
|
11
11
|
// Store schema
|
|
12
|
-
export const STORE_SCHEMA_VERSION =
|
|
12
|
+
export const STORE_SCHEMA_VERSION = 2;
|
|
13
13
|
|
|
14
14
|
// Message retrieval limits
|
|
15
15
|
export const EXPAND_MESSAGE_LIMIT = 6;
|
package/src/options.ts
CHANGED
|
@@ -9,6 +9,8 @@ import type {
|
|
|
9
9
|
ScopeDefaults,
|
|
10
10
|
ScopeName,
|
|
11
11
|
ScopeProfile,
|
|
12
|
+
SummaryStrategyName,
|
|
13
|
+
SummaryV2Options,
|
|
12
14
|
} from './types.js';
|
|
13
15
|
|
|
14
16
|
const DEFAULT_INTEROP: InteropOptions = {
|
|
@@ -54,6 +56,11 @@ const DEFAULT_AUTOMATIC_RETRIEVAL: AutomaticRetrievalOptions = {
|
|
|
54
56
|
},
|
|
55
57
|
};
|
|
56
58
|
|
|
59
|
+
export const DEFAULT_SUMMARY_V2: SummaryV2Options = {
|
|
60
|
+
strategy: 'deterministic-v2',
|
|
61
|
+
perMessageBudget: 110,
|
|
62
|
+
};
|
|
63
|
+
|
|
57
64
|
export const DEFAULT_OPTIONS: OpencodeLcmOptions = {
|
|
58
65
|
interop: DEFAULT_INTEROP,
|
|
59
66
|
scopeDefaults: DEFAULT_SCOPE_DEFAULTS,
|
|
@@ -78,6 +85,7 @@ export const DEFAULT_OPTIONS: OpencodeLcmOptions = {
|
|
|
78
85
|
'zip-metadata',
|
|
79
86
|
],
|
|
80
87
|
previewBytePeek: 16,
|
|
88
|
+
summaryV2: DEFAULT_SUMMARY_V2,
|
|
81
89
|
};
|
|
82
90
|
|
|
83
91
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
@@ -238,6 +246,18 @@ function asAutomaticRetrievalStopOptions(
|
|
|
238
246
|
};
|
|
239
247
|
}
|
|
240
248
|
|
|
249
|
+
function asSummaryStrategy(value: unknown, fallback: SummaryStrategyName): SummaryStrategyName {
|
|
250
|
+
return value === 'deterministic-v1' || value === 'deterministic-v2' ? value : fallback;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function asSummaryV2Options(value: unknown, fallback: SummaryV2Options): SummaryV2Options {
|
|
254
|
+
const record = asRecord(value);
|
|
255
|
+
return {
|
|
256
|
+
strategy: asSummaryStrategy(record?.strategy, fallback.strategy),
|
|
257
|
+
perMessageBudget: asNumber(record?.perMessageBudget, fallback.perMessageBudget),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
241
261
|
export function resolveOptions(raw: unknown): OpencodeLcmOptions {
|
|
242
262
|
const options = asRecord(raw);
|
|
243
263
|
const interop = asRecord(options?.interop);
|
|
@@ -293,5 +313,6 @@ export function resolveOptions(raw: unknown): OpencodeLcmOptions {
|
|
|
293
313
|
DEFAULT_OPTIONS.binaryPreviewProviders,
|
|
294
314
|
),
|
|
295
315
|
previewBytePeek: asNumber(options?.previewBytePeek, DEFAULT_OPTIONS.previewBytePeek),
|
|
316
|
+
summaryV2: asSummaryV2Options(options?.summaryV2, DEFAULT_SUMMARY_V2),
|
|
296
317
|
};
|
|
297
318
|
}
|
package/src/store-artifacts.ts
CHANGED
|
@@ -479,7 +479,7 @@ export async function externalizeMessage(
|
|
|
479
479
|
const { storedPart, artifacts: nextArtifacts } = await externalizePart(
|
|
480
480
|
bindings,
|
|
481
481
|
part,
|
|
482
|
-
message.info
|
|
482
|
+
message.info?.time?.created ?? 0,
|
|
483
483
|
);
|
|
484
484
|
artifacts.push(...nextArtifacts);
|
|
485
485
|
storedParts.push(storedPart);
|
|
@@ -509,7 +509,7 @@ export async function externalizeSession(
|
|
|
509
509
|
const { storedPart, artifacts: nextArtifacts } = await externalizePart(
|
|
510
510
|
bindings,
|
|
511
511
|
part,
|
|
512
|
-
message.info
|
|
512
|
+
message.info?.time?.created ?? 0,
|
|
513
513
|
);
|
|
514
514
|
artifacts.push(...nextArtifacts);
|
|
515
515
|
storedParts.push(storedPart);
|
|
@@ -607,7 +607,7 @@ export function persistStoredSessionSync(
|
|
|
607
607
|
insertMessage.run(
|
|
608
608
|
message.info.id,
|
|
609
609
|
storedSession.sessionID,
|
|
610
|
-
message.info
|
|
610
|
+
message.info?.time?.created ?? 0,
|
|
611
611
|
JSON.stringify(message.info),
|
|
612
612
|
);
|
|
613
613
|
|
package/src/store-search.ts
CHANGED
|
@@ -296,7 +296,7 @@ export function searchByScan(
|
|
|
296
296
|
id: message.info.id,
|
|
297
297
|
type: message.info.role,
|
|
298
298
|
sessionID: session.sessionID,
|
|
299
|
-
timestamp: message.info
|
|
299
|
+
timestamp: message.info?.time?.created ?? 0,
|
|
300
300
|
snippet: buildSnippet(blob, query),
|
|
301
301
|
content: blob,
|
|
302
302
|
sourceKind: 'message',
|
|
@@ -361,7 +361,7 @@ function insertMessageSearchRowsSync(deps: FtsDeps, session: NormalizedSession):
|
|
|
361
361
|
session.sessionID,
|
|
362
362
|
message.info.id,
|
|
363
363
|
message.info.role,
|
|
364
|
-
String(message.info
|
|
364
|
+
String(message.info?.time?.created ?? 0),
|
|
365
365
|
content,
|
|
366
366
|
);
|
|
367
367
|
}
|
|
@@ -380,7 +380,13 @@ export function replaceMessageSearchRowSync(
|
|
|
380
380
|
|
|
381
381
|
db.prepare(
|
|
382
382
|
'INSERT INTO message_fts (session_id, message_id, role, created_at, content) VALUES (?, ?, ?, ?, ?)',
|
|
383
|
-
).run(
|
|
383
|
+
).run(
|
|
384
|
+
sessionID,
|
|
385
|
+
message.info.id,
|
|
386
|
+
message.info.role,
|
|
387
|
+
String(message.info?.time?.created ?? 0),
|
|
388
|
+
content,
|
|
389
|
+
);
|
|
384
390
|
}
|
|
385
391
|
|
|
386
392
|
export function replaceSummarySearchRowsSync(deps: FtsDeps, sessionIDs?: string[]): void {
|
package/src/store-snapshot.ts
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import { withTransaction } from './sql-utils.js';
|
|
5
5
|
import type { SqlDatabaseLike } from './store-types.js';
|
|
6
|
+
import type { SummaryStrategyName } from './types.js';
|
|
6
7
|
import { resolveWorkspacePath } from './workspace-path.js';
|
|
7
8
|
import { normalizeWorktreeKey } from './worktree-key.js';
|
|
8
9
|
|
|
@@ -50,6 +51,7 @@ export type SummaryNodeRow = {
|
|
|
50
51
|
end_index: number;
|
|
51
52
|
message_ids_json: string;
|
|
52
53
|
summary_text: string;
|
|
54
|
+
strategy: SummaryStrategyName;
|
|
53
55
|
created_at: number;
|
|
54
56
|
};
|
|
55
57
|
|
|
@@ -254,8 +256,8 @@ export async function importStoreSnapshot(
|
|
|
254
256
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
255
257
|
);
|
|
256
258
|
const insertNode = db.prepare(
|
|
257
|
-
`INSERT OR REPLACE INTO summary_nodes (node_id, session_id, level, node_kind, start_index, end_index, message_ids_json, summary_text, created_at)
|
|
258
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
259
|
+
`INSERT OR REPLACE INTO summary_nodes (node_id, session_id, level, node_kind, start_index, end_index, message_ids_json, summary_text, strategy, created_at)
|
|
260
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
259
261
|
);
|
|
260
262
|
const insertEdge = db.prepare(
|
|
261
263
|
`INSERT OR REPLACE INTO summary_edges (session_id, parent_id, child_id, child_position) VALUES (?, ?, ?, ?)`,
|
|
@@ -315,6 +317,7 @@ export async function importStoreSnapshot(
|
|
|
315
317
|
row.end_index,
|
|
316
318
|
row.message_ids_json,
|
|
317
319
|
row.summary_text,
|
|
320
|
+
row.strategy,
|
|
318
321
|
row.created_at,
|
|
319
322
|
);
|
|
320
323
|
}
|
|
@@ -464,6 +467,10 @@ function parseSummaryNodeRow(value: unknown): SummaryNodeRow {
|
|
|
464
467
|
end_index: expectNumber(row.end_index, 'summary_nodes[].end_index'),
|
|
465
468
|
message_ids_json: expectString(row.message_ids_json, 'summary_nodes[].message_ids_json'),
|
|
466
469
|
summary_text: expectString(row.summary_text, 'summary_nodes[].summary_text'),
|
|
470
|
+
strategy: expectString(
|
|
471
|
+
row.strategy ?? 'deterministic-v1',
|
|
472
|
+
'summary_nodes[].strategy',
|
|
473
|
+
) as SummaryStrategyName,
|
|
467
474
|
created_at: expectNumber(row.created_at, 'summary_nodes[].created_at'),
|
|
468
475
|
};
|
|
469
476
|
}
|