opencode-lcm 0.13.4 → 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 +7 -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-snapshot.d.ts +2 -0
- package/dist/store-snapshot.js +4 -3
- package/dist/store.d.ts +3 -0
- package/dist/store.js +110 -33
- 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-snapshot.ts +9 -2
- package/src/store.ts +136 -30
- package/src/types.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.13.5] - 2026-04-11
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Restored schema-v2 compatibility for persisted stores after the PR #6 merge
|
|
14
|
+
- Removed the unfinished `llm-cli` summary configuration surface before shipping it
|
|
15
|
+
- Restored Biome-clean formatting so the full cross-platform CI matrix passes again
|
|
16
|
+
|
|
10
17
|
## [0.13.4] - 2026-04-09
|
|
11
18
|
|
|
12
19
|
### Fixed
|
package/dist/constants.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
export declare const SUMMARY_LEAF_MESSAGES = 6;
|
|
6
6
|
export declare const SUMMARY_BRANCH_FACTOR = 3;
|
|
7
7
|
export declare const SUMMARY_NODE_CHAR_LIMIT = 260;
|
|
8
|
-
export declare const STORE_SCHEMA_VERSION =
|
|
8
|
+
export declare const STORE_SCHEMA_VERSION = 2;
|
|
9
9
|
export declare const EXPAND_MESSAGE_LIMIT = 6;
|
|
10
10
|
export declare const AUTOMATIC_RETRIEVAL_QUERY_TOKENS = 8;
|
|
11
11
|
export declare const AUTOMATIC_RETRIEVAL_RECENT_MESSAGES = 3;
|
package/dist/constants.js
CHANGED
|
@@ -7,7 +7,7 @@ export const SUMMARY_LEAF_MESSAGES = 6;
|
|
|
7
7
|
export const SUMMARY_BRANCH_FACTOR = 3;
|
|
8
8
|
export const SUMMARY_NODE_CHAR_LIMIT = 260;
|
|
9
9
|
// Store schema
|
|
10
|
-
export const STORE_SCHEMA_VERSION =
|
|
10
|
+
export const STORE_SCHEMA_VERSION = 2;
|
|
11
11
|
// Message retrieval limits
|
|
12
12
|
export const EXPAND_MESSAGE_LIMIT = 6;
|
|
13
13
|
// Automatic retrieval configuration
|
package/dist/options.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { OpencodeLcmOptions } from './types.js';
|
|
1
|
+
import type { OpencodeLcmOptions, SummaryV2Options } from './types.js';
|
|
2
|
+
export declare const DEFAULT_SUMMARY_V2: SummaryV2Options;
|
|
2
3
|
export declare const DEFAULT_OPTIONS: OpencodeLcmOptions;
|
|
3
4
|
export declare function resolveOptions(raw: unknown): OpencodeLcmOptions;
|
package/dist/options.js
CHANGED
|
@@ -36,6 +36,10 @@ const DEFAULT_AUTOMATIC_RETRIEVAL = {
|
|
|
36
36
|
stopOnFirstScopeWithHits: false,
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
|
+
export const DEFAULT_SUMMARY_V2 = {
|
|
40
|
+
strategy: 'deterministic-v2',
|
|
41
|
+
perMessageBudget: 110,
|
|
42
|
+
};
|
|
39
43
|
export const DEFAULT_OPTIONS = {
|
|
40
44
|
interop: DEFAULT_INTEROP,
|
|
41
45
|
scopeDefaults: DEFAULT_SCOPE_DEFAULTS,
|
|
@@ -60,6 +64,7 @@ export const DEFAULT_OPTIONS = {
|
|
|
60
64
|
'zip-metadata',
|
|
61
65
|
],
|
|
62
66
|
previewBytePeek: 16,
|
|
67
|
+
summaryV2: DEFAULT_SUMMARY_V2,
|
|
63
68
|
};
|
|
64
69
|
function asRecord(value) {
|
|
65
70
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -184,6 +189,16 @@ function asAutomaticRetrievalStopOptions(value, fallback) {
|
|
|
184
189
|
stopOnFirstScopeWithHits: asBoolean(record?.stopOnFirstScopeWithHits, fallback.stopOnFirstScopeWithHits),
|
|
185
190
|
};
|
|
186
191
|
}
|
|
192
|
+
function asSummaryStrategy(value, fallback) {
|
|
193
|
+
return value === 'deterministic-v1' || value === 'deterministic-v2' ? value : fallback;
|
|
194
|
+
}
|
|
195
|
+
function asSummaryV2Options(value, fallback) {
|
|
196
|
+
const record = asRecord(value);
|
|
197
|
+
return {
|
|
198
|
+
strategy: asSummaryStrategy(record?.strategy, fallback.strategy),
|
|
199
|
+
perMessageBudget: asNumber(record?.perMessageBudget, fallback.perMessageBudget),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
187
202
|
export function resolveOptions(raw) {
|
|
188
203
|
const options = asRecord(raw);
|
|
189
204
|
const interop = asRecord(options?.interop);
|
|
@@ -213,5 +228,6 @@ export function resolveOptions(raw) {
|
|
|
213
228
|
artifactViewChars: asNumber(options?.artifactViewChars, DEFAULT_OPTIONS.artifactViewChars),
|
|
214
229
|
binaryPreviewProviders: asStringArray(options?.binaryPreviewProviders, DEFAULT_OPTIONS.binaryPreviewProviders),
|
|
215
230
|
previewBytePeek: asNumber(options?.previewBytePeek, DEFAULT_OPTIONS.previewBytePeek),
|
|
231
|
+
summaryV2: asSummaryV2Options(options?.summaryV2, DEFAULT_SUMMARY_V2),
|
|
216
232
|
};
|
|
217
233
|
}
|
package/dist/store-snapshot.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SqlDatabaseLike } from './store-types.js';
|
|
2
|
+
import type { SummaryStrategyName } from './types.js';
|
|
2
3
|
export type SnapshotScope = 'session' | 'root' | 'worktree' | 'all';
|
|
3
4
|
export type SnapshotWorktreeMode = 'auto' | 'preserve' | 'current';
|
|
4
5
|
export type SessionRow = {
|
|
@@ -38,6 +39,7 @@ export type SummaryNodeRow = {
|
|
|
38
39
|
end_index: number;
|
|
39
40
|
message_ids_json: string;
|
|
40
41
|
summary_text: string;
|
|
42
|
+
strategy: SummaryStrategyName;
|
|
41
43
|
created_at: number;
|
|
42
44
|
};
|
|
43
45
|
export type SummaryEdgeRow = {
|
package/dist/store-snapshot.js
CHANGED
|
@@ -81,8 +81,8 @@ export async function importStoreSnapshot(bindings, input) {
|
|
|
81
81
|
const insertBlob = db.prepare(`INSERT OR REPLACE INTO artifact_blobs (content_hash, content_text, char_count, created_at) VALUES (?, ?, ?, ?)`);
|
|
82
82
|
const insertArtifact = db.prepare(`INSERT OR REPLACE INTO artifacts (artifact_id, session_id, message_id, part_id, artifact_kind, field_name, preview_text, content_text, content_hash, metadata_json, char_count, created_at)
|
|
83
83
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
84
|
-
const insertNode = db.prepare(`INSERT OR REPLACE INTO summary_nodes (node_id, session_id, level, node_kind, start_index, end_index, message_ids_json, summary_text, created_at)
|
|
85
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
84
|
+
const insertNode = db.prepare(`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)
|
|
85
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
86
86
|
const insertEdge = db.prepare(`INSERT OR REPLACE INTO summary_edges (session_id, parent_id, child_id, child_position) VALUES (?, ?, ?, ?)`);
|
|
87
87
|
const insertState = db.prepare(`INSERT OR REPLACE INTO summary_state (session_id, archived_count, latest_message_created, archived_signature, root_node_ids_json, updated_at)
|
|
88
88
|
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
@@ -101,7 +101,7 @@ export async function importStoreSnapshot(bindings, input) {
|
|
|
101
101
|
insertArtifact.run(row.artifact_id, row.session_id, row.message_id, row.part_id, row.artifact_kind, row.field_name, row.preview_text, row.content_text, row.content_hash, row.metadata_json, row.char_count, row.created_at);
|
|
102
102
|
}
|
|
103
103
|
for (const row of snapshot.summary_nodes) {
|
|
104
|
-
insertNode.run(row.node_id, row.session_id, row.level, row.node_kind, row.start_index, row.end_index, row.message_ids_json, row.summary_text, row.created_at);
|
|
104
|
+
insertNode.run(row.node_id, row.session_id, row.level, row.node_kind, row.start_index, row.end_index, row.message_ids_json, row.summary_text, row.strategy, row.created_at);
|
|
105
105
|
}
|
|
106
106
|
for (const row of snapshot.summary_edges)
|
|
107
107
|
insertEdge.run(row.session_id, row.parent_id, row.child_id, row.child_position);
|
|
@@ -232,6 +232,7 @@ function parseSummaryNodeRow(value) {
|
|
|
232
232
|
end_index: expectNumber(row.end_index, 'summary_nodes[].end_index'),
|
|
233
233
|
message_ids_json: expectString(row.message_ids_json, 'summary_nodes[].message_ids_json'),
|
|
234
234
|
summary_text: expectString(row.summary_text, 'summary_nodes[].summary_text'),
|
|
235
|
+
strategy: expectString(row.strategy ?? 'deterministic-v1', 'summary_nodes[].strategy'),
|
|
235
236
|
created_at: expectNumber(row.created_at, 'summary_nodes[].created_at'),
|
|
236
237
|
};
|
|
237
238
|
}
|
package/dist/store.d.ts
CHANGED
|
@@ -264,6 +264,8 @@ export declare class SqliteLcmStore {
|
|
|
264
264
|
private compactMessageInPlace;
|
|
265
265
|
private shouldIgnoreTool;
|
|
266
266
|
private summarizeMessages;
|
|
267
|
+
private summarizeMessagesDeterministicV1;
|
|
268
|
+
private summarizeMessagesDeterministicV2;
|
|
267
269
|
private listTools;
|
|
268
270
|
private buildArchivedSignature;
|
|
269
271
|
private getArchivedMessages;
|
|
@@ -294,6 +296,7 @@ export declare class SqliteLcmStore {
|
|
|
294
296
|
private refreshSearchIndexesSync;
|
|
295
297
|
private ensureSessionColumnsSync;
|
|
296
298
|
private ensureSummaryStateColumnsSync;
|
|
299
|
+
private ensureSummaryNodeColumnsSync;
|
|
297
300
|
private ensureArtifactColumnsSync;
|
|
298
301
|
private backfillArtifactBlobsSync;
|
|
299
302
|
private refreshAllLineageSync;
|
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
|
}
|
|
@@ -775,6 +782,7 @@ export class SqliteLcmStore {
|
|
|
775
782
|
end_index INTEGER NOT NULL,
|
|
776
783
|
message_ids_json TEXT NOT NULL,
|
|
777
784
|
summary_text TEXT NOT NULL,
|
|
785
|
+
strategy TEXT NOT NULL DEFAULT 'deterministic-v1',
|
|
778
786
|
created_at INTEGER NOT NULL,
|
|
779
787
|
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
780
788
|
);
|
|
@@ -830,6 +838,7 @@ export class SqliteLcmStore {
|
|
|
830
838
|
`);
|
|
831
839
|
this.ensureSessionColumnsSync();
|
|
832
840
|
this.ensureSummaryStateColumnsSync();
|
|
841
|
+
this.ensureSummaryNodeColumnsSync();
|
|
833
842
|
this.ensureArtifactColumnsSync();
|
|
834
843
|
logStartupPhase('open-db:create-indexes');
|
|
835
844
|
db.exec('CREATE INDEX IF NOT EXISTS idx_artifacts_content_hash ON artifacts(content_hash)');
|
|
@@ -1262,7 +1271,7 @@ export class SqliteLcmStore {
|
|
|
1262
1271
|
issues.push('unexpected-summary-edges');
|
|
1263
1272
|
return issues.length > 0 ? { sessionID: session.sessionID, issues } : undefined;
|
|
1264
1273
|
}
|
|
1265
|
-
const latestMessageCreated = archived.at(-1)
|
|
1274
|
+
const latestMessageCreated = messageCreatedAt(archived.at(-1));
|
|
1266
1275
|
const archivedSignature = this.buildArchivedSignature(archived);
|
|
1267
1276
|
const rootIDs = state ? parseJson(state.root_node_ids_json) : [];
|
|
1268
1277
|
const roots = rootIDs
|
|
@@ -1373,7 +1382,7 @@ export class SqliteLcmStore {
|
|
|
1373
1382
|
case 'message.updated': {
|
|
1374
1383
|
const existing = session.messages.find((message) => message.info.id === payload.properties.info.id);
|
|
1375
1384
|
if (existing) {
|
|
1376
|
-
return this.isMessageArchivedSync(session.sessionID, existing.info.id, existing
|
|
1385
|
+
return this.isMessageArchivedSync(session.sessionID, existing.info.id, messageCreatedAt(existing));
|
|
1377
1386
|
}
|
|
1378
1387
|
return this.readMessageCountSync(session.sessionID) >= this.options.freshTailMessages;
|
|
1379
1388
|
}
|
|
@@ -1387,13 +1396,13 @@ export class SqliteLcmStore {
|
|
|
1387
1396
|
const message = session.messages.find((entry) => entry.info.id === payload.properties.part.messageID);
|
|
1388
1397
|
if (!message)
|
|
1389
1398
|
return false;
|
|
1390
|
-
return this.isMessageArchivedSync(session.sessionID, message.info.id, message
|
|
1399
|
+
return this.isMessageArchivedSync(session.sessionID, message.info.id, messageCreatedAt(message));
|
|
1391
1400
|
}
|
|
1392
1401
|
case 'message.part.removed': {
|
|
1393
1402
|
const message = session.messages.find((entry) => entry.info.id === payload.properties.messageID);
|
|
1394
1403
|
if (!message)
|
|
1395
1404
|
return false;
|
|
1396
|
-
return this.isMessageArchivedSync(session.sessionID, message.info.id, message
|
|
1405
|
+
return this.isMessageArchivedSync(session.sessionID, message.info.id, messageCreatedAt(message));
|
|
1397
1406
|
}
|
|
1398
1407
|
default:
|
|
1399
1408
|
return false;
|
|
@@ -2711,6 +2720,12 @@ export class SqliteLcmStore {
|
|
|
2711
2720
|
return this.options.interop.ignoreToolPrefixes.some((prefix) => toolName.startsWith(prefix));
|
|
2712
2721
|
}
|
|
2713
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) {
|
|
2714
2729
|
const goals = messages
|
|
2715
2730
|
.filter((message) => message.info.role === 'user')
|
|
2716
2731
|
.map((message) => guessMessageText(message, this.options.interop.ignoreToolPrefixes))
|
|
@@ -2735,6 +2750,55 @@ export class SqliteLcmStore {
|
|
|
2735
2750
|
return truncate(`Archived messages ${messages.length}`, limit);
|
|
2736
2751
|
return truncate(segments.join(' || '), limit);
|
|
2737
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
|
+
}
|
|
2738
2802
|
listTools(messages) {
|
|
2739
2803
|
const tools = [];
|
|
2740
2804
|
for (const message of messages) {
|
|
@@ -2751,13 +2815,13 @@ export class SqliteLcmStore {
|
|
|
2751
2815
|
buildArchivedSignature(messages) {
|
|
2752
2816
|
const hash = createHash('sha256');
|
|
2753
2817
|
for (const message of messages) {
|
|
2754
|
-
hash.update(message.info
|
|
2755
|
-
hash.update(message.info
|
|
2756
|
-
hash.update(String(message
|
|
2757
|
-
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) ?? ''));
|
|
2758
2822
|
hash.update(JSON.stringify(listFiles(message)));
|
|
2759
2823
|
hash.update(JSON.stringify(this.listTools([message])));
|
|
2760
|
-
hash.update(String(message.
|
|
2824
|
+
hash.update(String(messageParts(message).length));
|
|
2761
2825
|
}
|
|
2762
2826
|
return hash.digest('hex');
|
|
2763
2827
|
}
|
|
@@ -2777,7 +2841,7 @@ export class SqliteLcmStore {
|
|
|
2777
2841
|
this.clearSummaryGraphSync(sessionID);
|
|
2778
2842
|
return [];
|
|
2779
2843
|
}
|
|
2780
|
-
const latestMessageCreated = archivedMessages.at(-1)
|
|
2844
|
+
const latestMessageCreated = messageCreatedAt(archivedMessages.at(-1));
|
|
2781
2845
|
const archivedSignature = this.buildArchivedSignature(archivedMessages);
|
|
2782
2846
|
const state = safeQueryOne(this.getDb().prepare('SELECT * FROM summary_state WHERE session_id = ?'), [sessionID], 'ensureSummaryGraphSync');
|
|
2783
2847
|
if (state &&
|
|
@@ -2824,6 +2888,8 @@ export class SqliteLcmStore {
|
|
|
2824
2888
|
const expectedSummaryText = this.summarizeMessages(archivedMessages.slice(node.startIndex, node.endIndex + 1));
|
|
2825
2889
|
if (node.summaryText !== expectedSummaryText)
|
|
2826
2890
|
return false;
|
|
2891
|
+
if (node.strategy !== (this.options.summaryV2?.strategy ?? 'deterministic-v1'))
|
|
2892
|
+
return false;
|
|
2827
2893
|
const children = this.readSummaryChildrenSync(node.nodeID);
|
|
2828
2894
|
if (node.nodeKind === 'leaf') {
|
|
2829
2895
|
return (children.length === 0 && node.endIndex - node.startIndex + 1 <= SUMMARY_LEAF_MESSAGES);
|
|
@@ -2858,6 +2924,7 @@ export class SqliteLcmStore {
|
|
|
2858
2924
|
}
|
|
2859
2925
|
rebuildSummaryGraphSync(sessionID, archivedMessages, archivedSignature) {
|
|
2860
2926
|
const now = Date.now();
|
|
2927
|
+
const summaryStrategy = this.options.summaryV2?.strategy ?? 'deterministic-v1';
|
|
2861
2928
|
let level = 0;
|
|
2862
2929
|
const nodes = [];
|
|
2863
2930
|
const edges = [];
|
|
@@ -2870,6 +2937,7 @@ export class SqliteLcmStore {
|
|
|
2870
2937
|
endIndex: input.endIndex,
|
|
2871
2938
|
messageIDs: input.messageIDs,
|
|
2872
2939
|
summaryText: input.summaryText,
|
|
2940
|
+
strategy: summaryStrategy,
|
|
2873
2941
|
createdAt: now,
|
|
2874
2942
|
});
|
|
2875
2943
|
let currentLevel = [];
|
|
@@ -2922,13 +2990,13 @@ export class SqliteLcmStore {
|
|
|
2922
2990
|
withTransaction(db, 'rebuildSummaryGraph', () => {
|
|
2923
2991
|
this.clearSummaryGraphSync(sessionID);
|
|
2924
2992
|
const insertNode = db.prepare(`INSERT INTO summary_nodes
|
|
2925
|
-
(node_id, session_id, level, node_kind, start_index, end_index, message_ids_json, summary_text, created_at)
|
|
2926
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
2993
|
+
(node_id, session_id, level, node_kind, start_index, end_index, message_ids_json, summary_text, strategy, created_at)
|
|
2994
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
2927
2995
|
const insertEdge = db.prepare(`INSERT INTO summary_edges (session_id, parent_id, child_id, child_position)
|
|
2928
2996
|
VALUES (?, ?, ?, ?)`);
|
|
2929
2997
|
const insertSummaryFts = db.prepare('INSERT INTO summary_fts (session_id, node_id, level, created_at, content) VALUES (?, ?, ?, ?, ?)');
|
|
2930
2998
|
for (const node of nodes) {
|
|
2931
|
-
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);
|
|
2932
3000
|
insertSummaryFts.run(node.sessionID, node.nodeID, String(node.level), String(node.createdAt), node.summaryText);
|
|
2933
3001
|
}
|
|
2934
3002
|
for (const edge of edges) {
|
|
@@ -2941,7 +3009,7 @@ export class SqliteLcmStore {
|
|
|
2941
3009
|
latest_message_created = excluded.latest_message_created,
|
|
2942
3010
|
archived_signature = excluded.archived_signature,
|
|
2943
3011
|
root_node_ids_json = excluded.root_node_ids_json,
|
|
2944
|
-
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);
|
|
2945
3013
|
});
|
|
2946
3014
|
return roots;
|
|
2947
3015
|
}
|
|
@@ -2958,6 +3026,7 @@ export class SqliteLcmStore {
|
|
|
2958
3026
|
endIndex: row.end_index,
|
|
2959
3027
|
messageIDs: parseJson(row.message_ids_json),
|
|
2960
3028
|
summaryText: row.summary_text,
|
|
3029
|
+
strategy: row.strategy,
|
|
2961
3030
|
createdAt: row.created_at,
|
|
2962
3031
|
};
|
|
2963
3032
|
}
|
|
@@ -3238,6 +3307,14 @@ export class SqliteLcmStore {
|
|
|
3238
3307
|
return;
|
|
3239
3308
|
db.exec("ALTER TABLE summary_state ADD COLUMN archived_signature TEXT NOT NULL DEFAULT ''");
|
|
3240
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
|
+
}
|
|
3241
3318
|
ensureArtifactColumnsSync() {
|
|
3242
3319
|
const db = this.getDb();
|
|
3243
3320
|
const columns = db.prepare('PRAGMA table_info(artifacts)').all();
|
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-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
|
}
|
package/src/store.ts
CHANGED
|
@@ -72,6 +72,7 @@ import type {
|
|
|
72
72
|
ScopeName,
|
|
73
73
|
SearchResult,
|
|
74
74
|
StoreStats,
|
|
75
|
+
SummaryStrategyName,
|
|
75
76
|
} from './types.js';
|
|
76
77
|
import {
|
|
77
78
|
asRecord,
|
|
@@ -101,6 +102,7 @@ type SummaryNodeData = {
|
|
|
101
102
|
endIndex: number;
|
|
102
103
|
messageIDs: string[];
|
|
103
104
|
summaryText: string;
|
|
105
|
+
strategy: SummaryStrategyName;
|
|
104
106
|
createdAt: number;
|
|
105
107
|
};
|
|
106
108
|
|
|
@@ -379,6 +381,7 @@ function filterValidConversationMessages(
|
|
|
379
381
|
messages: ConversationMessage[],
|
|
380
382
|
context?: MessageValidationContext,
|
|
381
383
|
): ConversationMessage[] {
|
|
384
|
+
if (context?.operation === 'transformMessages') return messages;
|
|
382
385
|
const valid = messages.filter((message) => Boolean(getValidMessageInfo(message?.info)));
|
|
383
386
|
const dropped = messages.length - valid.length;
|
|
384
387
|
if (dropped > 0 && context) {
|
|
@@ -416,14 +419,21 @@ function getDeferredPartUpdateKey(event: Event): string | undefined {
|
|
|
416
419
|
return `${event.properties.part.sessionID}:${event.properties.part.messageID}:${event.properties.part.id}`;
|
|
417
420
|
}
|
|
418
421
|
|
|
419
|
-
function
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
422
|
+
function messageCreatedAt(message: ConversationMessage | undefined): number {
|
|
423
|
+
const created = message?.info?.time?.created;
|
|
424
|
+
return typeof created === 'number' && Number.isFinite(created) ? created : 0;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function messageParts(message: ConversationMessage | undefined): Part[] {
|
|
428
|
+
return Array.isArray(message?.parts) ? message.parts : [];
|
|
429
|
+
}
|
|
425
430
|
|
|
426
|
-
|
|
431
|
+
function signatureString(value: unknown, fallback = ''): string {
|
|
432
|
+
return typeof value === 'string' ? value : fallback;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function compareMessages(a: ConversationMessage, b: ConversationMessage): number {
|
|
436
|
+
return messageCreatedAt(a) - messageCreatedAt(b);
|
|
427
437
|
}
|
|
428
438
|
|
|
429
439
|
function emptySession(sessionID: string): NormalizedSession {
|
|
@@ -501,19 +511,21 @@ function isSyntheticLcmTextPart(part: Part, markers?: string[]): boolean {
|
|
|
501
511
|
function guessMessageText(message: ConversationMessage, ignoreToolPrefixes: string[]): string {
|
|
502
512
|
const segments: string[] = [];
|
|
503
513
|
|
|
504
|
-
for (const part of message
|
|
514
|
+
for (const part of messageParts(message)) {
|
|
505
515
|
switch (part.type) {
|
|
506
516
|
case 'text': {
|
|
507
517
|
if (isSyntheticLcmTextPart(part, ['archive-summary', 'retrieved-context', 'archived-part']))
|
|
508
518
|
break;
|
|
509
|
-
|
|
510
|
-
|
|
519
|
+
const text = typeof part.text === 'string' ? part.text : '';
|
|
520
|
+
if (text.startsWith('[Archived by opencode-lcm:')) break;
|
|
521
|
+
const sanitized = sanitizeAutomaticRetrievalSourceText(text);
|
|
511
522
|
if (sanitized) segments.push(sanitized);
|
|
512
523
|
break;
|
|
513
524
|
}
|
|
514
525
|
case 'reasoning': {
|
|
515
|
-
|
|
516
|
-
|
|
526
|
+
const text = typeof part.text === 'string' ? part.text : '';
|
|
527
|
+
if (text.startsWith('[Archived by opencode-lcm:')) break;
|
|
528
|
+
const sanitized = sanitizeAutomaticRetrievalSourceText(text);
|
|
517
529
|
if (sanitized) segments.push(sanitized);
|
|
518
530
|
break;
|
|
519
531
|
}
|
|
@@ -525,12 +537,13 @@ function guessMessageText(message: ConversationMessage, ignoreToolPrefixes: stri
|
|
|
525
537
|
break;
|
|
526
538
|
}
|
|
527
539
|
case 'tool': {
|
|
528
|
-
|
|
540
|
+
const toolName = typeof part.tool === 'string' ? part.tool : '';
|
|
541
|
+
if (ignoreToolPrefixes.some((prefix) => toolName.startsWith(prefix))) break;
|
|
529
542
|
const state = part.state;
|
|
530
|
-
if (state.status === 'completed') segments.push(`${
|
|
531
|
-
if (state.status === 'error') segments.push(`${
|
|
543
|
+
if (state.status === 'completed') segments.push(`${toolName}: ${state.output}`);
|
|
544
|
+
if (state.status === 'error') segments.push(`${toolName}: ${state.error}`);
|
|
532
545
|
if (state.status === 'pending' || state.status === 'running') {
|
|
533
|
-
segments.push(`${
|
|
546
|
+
segments.push(`${toolName}: ${JSON.stringify(state.input)}`);
|
|
534
547
|
}
|
|
535
548
|
if (state.status === 'completed' && state.attachments && state.attachments.length > 0) {
|
|
536
549
|
const attachmentNames = state.attachments
|
|
@@ -538,7 +551,7 @@ function guessMessageText(message: ConversationMessage, ignoreToolPrefixes: stri
|
|
|
538
551
|
.filter(Boolean)
|
|
539
552
|
.slice(0, 4);
|
|
540
553
|
if (attachmentNames.length > 0)
|
|
541
|
-
segments.push(`${
|
|
554
|
+
segments.push(`${toolName} attachments: ${attachmentNames.join(', ')}`);
|
|
542
555
|
}
|
|
543
556
|
break;
|
|
544
557
|
}
|
|
@@ -1065,6 +1078,7 @@ export class SqliteLcmStore {
|
|
|
1065
1078
|
end_index INTEGER NOT NULL,
|
|
1066
1079
|
message_ids_json TEXT NOT NULL,
|
|
1067
1080
|
summary_text TEXT NOT NULL,
|
|
1081
|
+
strategy TEXT NOT NULL DEFAULT 'deterministic-v1',
|
|
1068
1082
|
created_at INTEGER NOT NULL,
|
|
1069
1083
|
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
1070
1084
|
);
|
|
@@ -1121,6 +1135,7 @@ export class SqliteLcmStore {
|
|
|
1121
1135
|
|
|
1122
1136
|
this.ensureSessionColumnsSync();
|
|
1123
1137
|
this.ensureSummaryStateColumnsSync();
|
|
1138
|
+
this.ensureSummaryNodeColumnsSync();
|
|
1124
1139
|
this.ensureArtifactColumnsSync();
|
|
1125
1140
|
logStartupPhase('open-db:create-indexes');
|
|
1126
1141
|
db.exec('CREATE INDEX IF NOT EXISTS idx_artifacts_content_hash ON artifacts(content_hash)');
|
|
@@ -1710,7 +1725,7 @@ export class SqliteLcmStore {
|
|
|
1710
1725
|
return issues.length > 0 ? { sessionID: session.sessionID, issues } : undefined;
|
|
1711
1726
|
}
|
|
1712
1727
|
|
|
1713
|
-
const latestMessageCreated = archived.at(-1)
|
|
1728
|
+
const latestMessageCreated = messageCreatedAt(archived.at(-1));
|
|
1714
1729
|
const archivedSignature = this.buildArchivedSignature(archived);
|
|
1715
1730
|
const rootIDs = state ? parseJson<string[]>(state.root_node_ids_json) : [];
|
|
1716
1731
|
const roots = rootIDs
|
|
@@ -1863,7 +1878,7 @@ export class SqliteLcmStore {
|
|
|
1863
1878
|
return this.isMessageArchivedSync(
|
|
1864
1879
|
session.sessionID,
|
|
1865
1880
|
existing.info.id,
|
|
1866
|
-
existing
|
|
1881
|
+
messageCreatedAt(existing),
|
|
1867
1882
|
);
|
|
1868
1883
|
}
|
|
1869
1884
|
|
|
@@ -1888,7 +1903,7 @@ export class SqliteLcmStore {
|
|
|
1888
1903
|
return this.isMessageArchivedSync(
|
|
1889
1904
|
session.sessionID,
|
|
1890
1905
|
message.info.id,
|
|
1891
|
-
message
|
|
1906
|
+
messageCreatedAt(message),
|
|
1892
1907
|
);
|
|
1893
1908
|
}
|
|
1894
1909
|
case 'message.part.removed': {
|
|
@@ -1899,7 +1914,7 @@ export class SqliteLcmStore {
|
|
|
1899
1914
|
return this.isMessageArchivedSync(
|
|
1900
1915
|
session.sessionID,
|
|
1901
1916
|
message.info.id,
|
|
1902
|
-
message
|
|
1917
|
+
messageCreatedAt(message),
|
|
1903
1918
|
);
|
|
1904
1919
|
}
|
|
1905
1920
|
default:
|
|
@@ -3588,6 +3603,16 @@ export class SqliteLcmStore {
|
|
|
3588
3603
|
private summarizeMessages(
|
|
3589
3604
|
messages: ConversationMessage[],
|
|
3590
3605
|
limit = SUMMARY_NODE_CHAR_LIMIT,
|
|
3606
|
+
): string {
|
|
3607
|
+
const strategy = this.options.summaryV2?.strategy ?? 'deterministic-v1';
|
|
3608
|
+
return strategy === 'deterministic-v2'
|
|
3609
|
+
? this.summarizeMessagesDeterministicV2(messages, limit)
|
|
3610
|
+
: this.summarizeMessagesDeterministicV1(messages, limit);
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
private summarizeMessagesDeterministicV1(
|
|
3614
|
+
messages: ConversationMessage[],
|
|
3615
|
+
limit = SUMMARY_NODE_CHAR_LIMIT,
|
|
3591
3616
|
): string {
|
|
3592
3617
|
const goals = messages
|
|
3593
3618
|
.filter((message) => message.info.role === 'user')
|
|
@@ -3617,6 +3642,71 @@ export class SqliteLcmStore {
|
|
|
3617
3642
|
return truncate(segments.join(' || '), limit);
|
|
3618
3643
|
}
|
|
3619
3644
|
|
|
3645
|
+
private summarizeMessagesDeterministicV2(
|
|
3646
|
+
messages: ConversationMessage[],
|
|
3647
|
+
limit = SUMMARY_NODE_CHAR_LIMIT,
|
|
3648
|
+
): string {
|
|
3649
|
+
const perMsgBudget = this.options.summaryV2?.perMessageBudget ?? 110;
|
|
3650
|
+
const ignoreToolPrefixes = this.options.interop.ignoreToolPrefixes;
|
|
3651
|
+
const userTexts = messages
|
|
3652
|
+
.filter((message) => message.info.role === 'user')
|
|
3653
|
+
.map((message) => guessMessageText(message, ignoreToolPrefixes))
|
|
3654
|
+
.filter(Boolean);
|
|
3655
|
+
const assistantTexts = messages
|
|
3656
|
+
.filter((message) => message.info.role === 'assistant')
|
|
3657
|
+
.map((message) => guessMessageText(message, ignoreToolPrefixes))
|
|
3658
|
+
.filter(Boolean);
|
|
3659
|
+
const allFiles = [...new Set(messages.flatMap(listFiles))];
|
|
3660
|
+
const allTools = [...new Set(this.listTools(messages))];
|
|
3661
|
+
const hasErrors = messages.some((message) =>
|
|
3662
|
+
message.parts.some(
|
|
3663
|
+
(part) =>
|
|
3664
|
+
(part.type === 'tool' && 'state' in part && part.state?.status === 'error') ||
|
|
3665
|
+
(part.type === 'text' &&
|
|
3666
|
+
/\b(?:error|exception|fail(?:ed|ure)?)\b/i.test(part.text ?? '')),
|
|
3667
|
+
),
|
|
3668
|
+
);
|
|
3669
|
+
|
|
3670
|
+
const segments: string[] = [];
|
|
3671
|
+
if (userTexts.length > 0) {
|
|
3672
|
+
const first = truncate(userTexts[0], perMsgBudget);
|
|
3673
|
+
if (userTexts.length > 1) {
|
|
3674
|
+
const last = truncate(userTexts[userTexts.length - 1], perMsgBudget);
|
|
3675
|
+
segments.push(`Goals: ${first} → ${last}`);
|
|
3676
|
+
} else {
|
|
3677
|
+
segments.push(`Goals: ${first}`);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
if (assistantTexts.length > 0) {
|
|
3682
|
+
const recent = assistantTexts
|
|
3683
|
+
.slice(-2)
|
|
3684
|
+
.map((text) => truncate(text, perMsgBudget))
|
|
3685
|
+
.join(' | ');
|
|
3686
|
+
segments.push(`Work: ${recent}`);
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
if (allFiles.length > 0) {
|
|
3690
|
+
const shown = allFiles.slice(0, 6).join(', ');
|
|
3691
|
+
segments.push(
|
|
3692
|
+
allFiles.length > 6 ? `Files[${allFiles.length}]: ${shown}` : `Files: ${shown}`,
|
|
3693
|
+
);
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
if (allTools.length > 0) {
|
|
3697
|
+
const shown = allTools.slice(0, 6).join(', ');
|
|
3698
|
+
segments.push(
|
|
3699
|
+
allTools.length > 6 ? `Tools[${allTools.length}]: ${shown}` : `Tools: ${shown}`,
|
|
3700
|
+
);
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
if (hasErrors) segments.push('⚠err');
|
|
3704
|
+
segments.push(`${messages.length}msg(u:${userTexts.length}/a:${assistantTexts.length})`);
|
|
3705
|
+
|
|
3706
|
+
if (segments.length === 0) return truncate(`Archived ${messages.length} messages`, limit);
|
|
3707
|
+
return truncate(segments.join(' || '), limit);
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3620
3710
|
private listTools(messages: ConversationMessage[]): string[] {
|
|
3621
3711
|
const tools: string[] = [];
|
|
3622
3712
|
for (const message of messages) {
|
|
@@ -3632,13 +3722,13 @@ export class SqliteLcmStore {
|
|
|
3632
3722
|
private buildArchivedSignature(messages: ConversationMessage[]): string {
|
|
3633
3723
|
const hash = createHash('sha256');
|
|
3634
3724
|
for (const message of messages) {
|
|
3635
|
-
hash.update(message.info
|
|
3636
|
-
hash.update(message.info
|
|
3637
|
-
hash.update(String(message
|
|
3638
|
-
hash.update(guessMessageText(message, this.options.interop.ignoreToolPrefixes));
|
|
3725
|
+
hash.update(signatureString(message.info?.id, 'unknown-message'));
|
|
3726
|
+
hash.update(signatureString(message.info?.role, 'unknown-role'));
|
|
3727
|
+
hash.update(String(messageCreatedAt(message)));
|
|
3728
|
+
hash.update(String(guessMessageText(message, this.options.interop.ignoreToolPrefixes) ?? ''));
|
|
3639
3729
|
hash.update(JSON.stringify(listFiles(message)));
|
|
3640
3730
|
hash.update(JSON.stringify(this.listTools([message])));
|
|
3641
|
-
hash.update(String(message.
|
|
3731
|
+
hash.update(String(messageParts(message).length));
|
|
3642
3732
|
}
|
|
3643
3733
|
return hash.digest('hex');
|
|
3644
3734
|
}
|
|
@@ -3665,7 +3755,7 @@ export class SqliteLcmStore {
|
|
|
3665
3755
|
return [];
|
|
3666
3756
|
}
|
|
3667
3757
|
|
|
3668
|
-
const latestMessageCreated = archivedMessages.at(-1)
|
|
3758
|
+
const latestMessageCreated = messageCreatedAt(archivedMessages.at(-1));
|
|
3669
3759
|
const archivedSignature = this.buildArchivedSignature(archivedMessages);
|
|
3670
3760
|
const state = safeQueryOne<SummaryStateRow>(
|
|
3671
3761
|
this.getDb().prepare('SELECT * FROM summary_state WHERE session_id = ?'),
|
|
@@ -3729,6 +3819,7 @@ export class SqliteLcmStore {
|
|
|
3729
3819
|
archivedMessages.slice(node.startIndex, node.endIndex + 1),
|
|
3730
3820
|
);
|
|
3731
3821
|
if (node.summaryText !== expectedSummaryText) return false;
|
|
3822
|
+
if (node.strategy !== (this.options.summaryV2?.strategy ?? 'deterministic-v1')) return false;
|
|
3732
3823
|
|
|
3733
3824
|
const children = this.readSummaryChildrenSync(node.nodeID);
|
|
3734
3825
|
if (node.nodeKind === 'leaf') {
|
|
@@ -3768,6 +3859,7 @@ export class SqliteLcmStore {
|
|
|
3768
3859
|
archivedSignature: string,
|
|
3769
3860
|
): SummaryNodeData[] {
|
|
3770
3861
|
const now = Date.now();
|
|
3862
|
+
const summaryStrategy = this.options.summaryV2?.strategy ?? 'deterministic-v1';
|
|
3771
3863
|
let level = 0;
|
|
3772
3864
|
const nodes: SummaryNodeData[] = [];
|
|
3773
3865
|
const edges: Array<{
|
|
@@ -3794,6 +3886,7 @@ export class SqliteLcmStore {
|
|
|
3794
3886
|
endIndex: input.endIndex,
|
|
3795
3887
|
messageIDs: input.messageIDs,
|
|
3796
3888
|
summaryText: input.summaryText,
|
|
3889
|
+
strategy: summaryStrategy,
|
|
3797
3890
|
createdAt: now,
|
|
3798
3891
|
});
|
|
3799
3892
|
|
|
@@ -3857,8 +3950,8 @@ export class SqliteLcmStore {
|
|
|
3857
3950
|
|
|
3858
3951
|
const insertNode = db.prepare(
|
|
3859
3952
|
`INSERT INTO summary_nodes
|
|
3860
|
-
(node_id, session_id, level, node_kind, start_index, end_index, message_ids_json, summary_text, created_at)
|
|
3861
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3953
|
+
(node_id, session_id, level, node_kind, start_index, end_index, message_ids_json, summary_text, strategy, created_at)
|
|
3954
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3862
3955
|
);
|
|
3863
3956
|
const insertEdge = db.prepare(
|
|
3864
3957
|
`INSERT INTO summary_edges (session_id, parent_id, child_id, child_position)
|
|
@@ -3878,6 +3971,7 @@ export class SqliteLcmStore {
|
|
|
3878
3971
|
node.endIndex,
|
|
3879
3972
|
JSON.stringify(node.messageIDs),
|
|
3880
3973
|
node.summaryText,
|
|
3974
|
+
node.strategy,
|
|
3881
3975
|
node.createdAt,
|
|
3882
3976
|
);
|
|
3883
3977
|
insertSummaryFts.run(
|
|
@@ -3905,7 +3999,7 @@ export class SqliteLcmStore {
|
|
|
3905
3999
|
).run(
|
|
3906
4000
|
sessionID,
|
|
3907
4001
|
archivedMessages.length,
|
|
3908
|
-
archivedMessages.at(-1)
|
|
4002
|
+
messageCreatedAt(archivedMessages.at(-1)),
|
|
3909
4003
|
archivedSignature,
|
|
3910
4004
|
JSON.stringify(roots.map((node) => node.nodeID)),
|
|
3911
4005
|
now,
|
|
@@ -3932,6 +4026,7 @@ export class SqliteLcmStore {
|
|
|
3932
4026
|
endIndex: row.end_index,
|
|
3933
4027
|
messageIDs: parseJson<string[]>(row.message_ids_json),
|
|
3934
4028
|
summaryText: row.summary_text,
|
|
4029
|
+
strategy: row.strategy,
|
|
3935
4030
|
createdAt: row.created_at,
|
|
3936
4031
|
};
|
|
3937
4032
|
}
|
|
@@ -4370,6 +4465,17 @@ export class SqliteLcmStore {
|
|
|
4370
4465
|
db.exec("ALTER TABLE summary_state ADD COLUMN archived_signature TEXT NOT NULL DEFAULT ''");
|
|
4371
4466
|
}
|
|
4372
4467
|
|
|
4468
|
+
private ensureSummaryNodeColumnsSync(): void {
|
|
4469
|
+
const db = this.getDb();
|
|
4470
|
+
const columns = db.prepare('PRAGMA table_info(summary_nodes)').all() as Array<{ name: string }>;
|
|
4471
|
+
const names = new Set(columns.map((column) => column.name));
|
|
4472
|
+
if (names.has('strategy')) return;
|
|
4473
|
+
|
|
4474
|
+
db.exec(
|
|
4475
|
+
"ALTER TABLE summary_nodes ADD COLUMN strategy TEXT NOT NULL DEFAULT 'deterministic-v1'",
|
|
4476
|
+
);
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4373
4479
|
private ensureArtifactColumnsSync(): void {
|
|
4374
4480
|
const db = this.getDb();
|
|
4375
4481
|
const columns = db.prepare('PRAGMA table_info(artifacts)').all() as Array<{ name: string }>;
|
package/src/types.ts
CHANGED
|
@@ -55,6 +55,13 @@ export type AutomaticRetrievalOptions = {
|
|
|
55
55
|
stop: AutomaticRetrievalStopOptions;
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
export type SummaryStrategyName = 'deterministic-v1' | 'deterministic-v2';
|
|
59
|
+
|
|
60
|
+
export type SummaryV2Options = {
|
|
61
|
+
strategy: SummaryStrategyName;
|
|
62
|
+
perMessageBudget: number;
|
|
63
|
+
};
|
|
64
|
+
|
|
58
65
|
export type OpencodeLcmOptions = {
|
|
59
66
|
interop: InteropOptions;
|
|
60
67
|
scopeDefaults: ScopeDefaults;
|
|
@@ -74,6 +81,7 @@ export type OpencodeLcmOptions = {
|
|
|
74
81
|
artifactViewChars: number;
|
|
75
82
|
binaryPreviewProviders: string[];
|
|
76
83
|
previewBytePeek: number;
|
|
84
|
+
summaryV2: SummaryV2Options;
|
|
77
85
|
};
|
|
78
86
|
|
|
79
87
|
export type CapturedEvent = {
|