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/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
|
}
|
|
@@ -613,6 +626,9 @@ type CaptureHydrationOptions = {
|
|
|
613
626
|
isBunRuntime?: boolean;
|
|
614
627
|
platform?: string | undefined;
|
|
615
628
|
};
|
|
629
|
+
type ReadMessageOptions = {
|
|
630
|
+
hydrateArtifacts?: boolean;
|
|
631
|
+
};
|
|
616
632
|
|
|
617
633
|
function normalizeSqliteRuntimeOverride(value: string | undefined): SqliteRuntime | 'auto' {
|
|
618
634
|
const normalized = value?.trim().toLowerCase();
|
|
@@ -652,6 +668,17 @@ export function resolveCaptureHydrationMode(
|
|
|
652
668
|
return isBunRuntime && platform === 'win32' ? 'full' : 'targeted';
|
|
653
669
|
}
|
|
654
670
|
|
|
671
|
+
function shouldUseLightweightPartCapture(
|
|
672
|
+
event: CapturedEvent,
|
|
673
|
+
options?: CaptureHydrationOptions,
|
|
674
|
+
): boolean {
|
|
675
|
+
const isBunRuntime =
|
|
676
|
+
options?.isBunRuntime ?? (typeof globalThis === 'object' && 'Bun' in globalThis);
|
|
677
|
+
const platform = options?.platform ?? process.platform;
|
|
678
|
+
if (!isBunRuntime || platform !== 'win32') return false;
|
|
679
|
+
return event.type === 'message.part.updated' || event.type === 'message.part.removed';
|
|
680
|
+
}
|
|
681
|
+
|
|
655
682
|
function isSqliteRuntimeImportError(runtime: SqliteRuntime, error: unknown): boolean {
|
|
656
683
|
const code =
|
|
657
684
|
typeof error === 'object' && error && 'code' in error && typeof error.code === 'string'
|
|
@@ -1051,6 +1078,7 @@ export class SqliteLcmStore {
|
|
|
1051
1078
|
end_index INTEGER NOT NULL,
|
|
1052
1079
|
message_ids_json TEXT NOT NULL,
|
|
1053
1080
|
summary_text TEXT NOT NULL,
|
|
1081
|
+
strategy TEXT NOT NULL DEFAULT 'deterministic-v1',
|
|
1054
1082
|
created_at INTEGER NOT NULL,
|
|
1055
1083
|
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
1056
1084
|
);
|
|
@@ -1107,6 +1135,7 @@ export class SqliteLcmStore {
|
|
|
1107
1135
|
|
|
1108
1136
|
this.ensureSessionColumnsSync();
|
|
1109
1137
|
this.ensureSummaryStateColumnsSync();
|
|
1138
|
+
this.ensureSummaryNodeColumnsSync();
|
|
1110
1139
|
this.ensureArtifactColumnsSync();
|
|
1111
1140
|
logStartupPhase('open-db:create-indexes');
|
|
1112
1141
|
db.exec('CREATE INDEX IF NOT EXISTS idx_artifacts_content_hash ON artifacts(content_hash)');
|
|
@@ -1281,8 +1310,9 @@ export class SqliteLcmStore {
|
|
|
1281
1310
|
|
|
1282
1311
|
if (!normalized.sessionID || !shouldPersistSession) return;
|
|
1283
1312
|
|
|
1284
|
-
const session =
|
|
1285
|
-
|
|
1313
|
+
const session = shouldUseLightweightPartCapture(normalized)
|
|
1314
|
+
? this.readSessionForCaptureSync(normalized, { hydrateArtifacts: false })
|
|
1315
|
+
: resolveCaptureHydrationMode() === 'targeted'
|
|
1286
1316
|
? this.readSessionForCaptureSync(normalized)
|
|
1287
1317
|
: this.readSessionSync(normalized.sessionID);
|
|
1288
1318
|
const previousParentSessionID = session.parentSessionID;
|
|
@@ -1695,7 +1725,7 @@ export class SqliteLcmStore {
|
|
|
1695
1725
|
return issues.length > 0 ? { sessionID: session.sessionID, issues } : undefined;
|
|
1696
1726
|
}
|
|
1697
1727
|
|
|
1698
|
-
const latestMessageCreated = archived.at(-1)
|
|
1728
|
+
const latestMessageCreated = messageCreatedAt(archived.at(-1));
|
|
1699
1729
|
const archivedSignature = this.buildArchivedSignature(archived);
|
|
1700
1730
|
const rootIDs = state ? parseJson<string[]>(state.root_node_ids_json) : [];
|
|
1701
1731
|
const roots = rootIDs
|
|
@@ -1848,7 +1878,7 @@ export class SqliteLcmStore {
|
|
|
1848
1878
|
return this.isMessageArchivedSync(
|
|
1849
1879
|
session.sessionID,
|
|
1850
1880
|
existing.info.id,
|
|
1851
|
-
existing
|
|
1881
|
+
messageCreatedAt(existing),
|
|
1852
1882
|
);
|
|
1853
1883
|
}
|
|
1854
1884
|
|
|
@@ -1873,7 +1903,7 @@ export class SqliteLcmStore {
|
|
|
1873
1903
|
return this.isMessageArchivedSync(
|
|
1874
1904
|
session.sessionID,
|
|
1875
1905
|
message.info.id,
|
|
1876
|
-
message
|
|
1906
|
+
messageCreatedAt(message),
|
|
1877
1907
|
);
|
|
1878
1908
|
}
|
|
1879
1909
|
case 'message.part.removed': {
|
|
@@ -1884,7 +1914,7 @@ export class SqliteLcmStore {
|
|
|
1884
1914
|
return this.isMessageArchivedSync(
|
|
1885
1915
|
session.sessionID,
|
|
1886
1916
|
message.info.id,
|
|
1887
|
-
message
|
|
1917
|
+
messageCreatedAt(message),
|
|
1888
1918
|
);
|
|
1889
1919
|
}
|
|
1890
1920
|
default:
|
|
@@ -3573,6 +3603,16 @@ export class SqliteLcmStore {
|
|
|
3573
3603
|
private summarizeMessages(
|
|
3574
3604
|
messages: ConversationMessage[],
|
|
3575
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,
|
|
3576
3616
|
): string {
|
|
3577
3617
|
const goals = messages
|
|
3578
3618
|
.filter((message) => message.info.role === 'user')
|
|
@@ -3602,6 +3642,71 @@ export class SqliteLcmStore {
|
|
|
3602
3642
|
return truncate(segments.join(' || '), limit);
|
|
3603
3643
|
}
|
|
3604
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
|
+
|
|
3605
3710
|
private listTools(messages: ConversationMessage[]): string[] {
|
|
3606
3711
|
const tools: string[] = [];
|
|
3607
3712
|
for (const message of messages) {
|
|
@@ -3617,13 +3722,13 @@ export class SqliteLcmStore {
|
|
|
3617
3722
|
private buildArchivedSignature(messages: ConversationMessage[]): string {
|
|
3618
3723
|
const hash = createHash('sha256');
|
|
3619
3724
|
for (const message of messages) {
|
|
3620
|
-
hash.update(message.info
|
|
3621
|
-
hash.update(message.info
|
|
3622
|
-
hash.update(String(message
|
|
3623
|
-
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) ?? ''));
|
|
3624
3729
|
hash.update(JSON.stringify(listFiles(message)));
|
|
3625
3730
|
hash.update(JSON.stringify(this.listTools([message])));
|
|
3626
|
-
hash.update(String(message.
|
|
3731
|
+
hash.update(String(messageParts(message).length));
|
|
3627
3732
|
}
|
|
3628
3733
|
return hash.digest('hex');
|
|
3629
3734
|
}
|
|
@@ -3650,7 +3755,7 @@ export class SqliteLcmStore {
|
|
|
3650
3755
|
return [];
|
|
3651
3756
|
}
|
|
3652
3757
|
|
|
3653
|
-
const latestMessageCreated = archivedMessages.at(-1)
|
|
3758
|
+
const latestMessageCreated = messageCreatedAt(archivedMessages.at(-1));
|
|
3654
3759
|
const archivedSignature = this.buildArchivedSignature(archivedMessages);
|
|
3655
3760
|
const state = safeQueryOne<SummaryStateRow>(
|
|
3656
3761
|
this.getDb().prepare('SELECT * FROM summary_state WHERE session_id = ?'),
|
|
@@ -3714,6 +3819,7 @@ export class SqliteLcmStore {
|
|
|
3714
3819
|
archivedMessages.slice(node.startIndex, node.endIndex + 1),
|
|
3715
3820
|
);
|
|
3716
3821
|
if (node.summaryText !== expectedSummaryText) return false;
|
|
3822
|
+
if (node.strategy !== (this.options.summaryV2?.strategy ?? 'deterministic-v1')) return false;
|
|
3717
3823
|
|
|
3718
3824
|
const children = this.readSummaryChildrenSync(node.nodeID);
|
|
3719
3825
|
if (node.nodeKind === 'leaf') {
|
|
@@ -3753,6 +3859,7 @@ export class SqliteLcmStore {
|
|
|
3753
3859
|
archivedSignature: string,
|
|
3754
3860
|
): SummaryNodeData[] {
|
|
3755
3861
|
const now = Date.now();
|
|
3862
|
+
const summaryStrategy = this.options.summaryV2?.strategy ?? 'deterministic-v1';
|
|
3756
3863
|
let level = 0;
|
|
3757
3864
|
const nodes: SummaryNodeData[] = [];
|
|
3758
3865
|
const edges: Array<{
|
|
@@ -3779,6 +3886,7 @@ export class SqliteLcmStore {
|
|
|
3779
3886
|
endIndex: input.endIndex,
|
|
3780
3887
|
messageIDs: input.messageIDs,
|
|
3781
3888
|
summaryText: input.summaryText,
|
|
3889
|
+
strategy: summaryStrategy,
|
|
3782
3890
|
createdAt: now,
|
|
3783
3891
|
});
|
|
3784
3892
|
|
|
@@ -3842,8 +3950,8 @@ export class SqliteLcmStore {
|
|
|
3842
3950
|
|
|
3843
3951
|
const insertNode = db.prepare(
|
|
3844
3952
|
`INSERT INTO summary_nodes
|
|
3845
|
-
(node_id, session_id, level, node_kind, start_index, end_index, message_ids_json, summary_text, created_at)
|
|
3846
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3953
|
+
(node_id, session_id, level, node_kind, start_index, end_index, message_ids_json, summary_text, strategy, created_at)
|
|
3954
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3847
3955
|
);
|
|
3848
3956
|
const insertEdge = db.prepare(
|
|
3849
3957
|
`INSERT INTO summary_edges (session_id, parent_id, child_id, child_position)
|
|
@@ -3863,6 +3971,7 @@ export class SqliteLcmStore {
|
|
|
3863
3971
|
node.endIndex,
|
|
3864
3972
|
JSON.stringify(node.messageIDs),
|
|
3865
3973
|
node.summaryText,
|
|
3974
|
+
node.strategy,
|
|
3866
3975
|
node.createdAt,
|
|
3867
3976
|
);
|
|
3868
3977
|
insertSummaryFts.run(
|
|
@@ -3890,7 +3999,7 @@ export class SqliteLcmStore {
|
|
|
3890
3999
|
).run(
|
|
3891
4000
|
sessionID,
|
|
3892
4001
|
archivedMessages.length,
|
|
3893
|
-
archivedMessages.at(-1)
|
|
4002
|
+
messageCreatedAt(archivedMessages.at(-1)),
|
|
3894
4003
|
archivedSignature,
|
|
3895
4004
|
JSON.stringify(roots.map((node) => node.nodeID)),
|
|
3896
4005
|
now,
|
|
@@ -3917,6 +4026,7 @@ export class SqliteLcmStore {
|
|
|
3917
4026
|
endIndex: row.end_index,
|
|
3918
4027
|
messageIDs: parseJson<string[]>(row.message_ids_json),
|
|
3919
4028
|
summaryText: row.summary_text,
|
|
4029
|
+
strategy: row.strategy,
|
|
3920
4030
|
createdAt: row.created_at,
|
|
3921
4031
|
};
|
|
3922
4032
|
}
|
|
@@ -4355,6 +4465,17 @@ export class SqliteLcmStore {
|
|
|
4355
4465
|
db.exec("ALTER TABLE summary_state ADD COLUMN archived_signature TEXT NOT NULL DEFAULT ''");
|
|
4356
4466
|
}
|
|
4357
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
|
+
|
|
4358
4479
|
private ensureArtifactColumnsSync(): void {
|
|
4359
4480
|
const db = this.getDb();
|
|
4360
4481
|
const columns = db.prepare('PRAGMA table_info(artifacts)').all() as Array<{ name: string }>;
|
|
@@ -4872,7 +4993,10 @@ export class SqliteLcmStore {
|
|
|
4872
4993
|
return parentSessionID;
|
|
4873
4994
|
}
|
|
4874
4995
|
|
|
4875
|
-
private readSessionForCaptureSync(
|
|
4996
|
+
private readSessionForCaptureSync(
|
|
4997
|
+
event: CapturedEvent,
|
|
4998
|
+
options?: ReadMessageOptions,
|
|
4999
|
+
): NormalizedSession {
|
|
4876
5000
|
const sessionID = event.sessionID;
|
|
4877
5001
|
if (!sessionID) return emptySession('');
|
|
4878
5002
|
|
|
@@ -4881,17 +5005,17 @@ export class SqliteLcmStore {
|
|
|
4881
5005
|
|
|
4882
5006
|
switch (payload.type) {
|
|
4883
5007
|
case 'message.updated': {
|
|
4884
|
-
const message = this.readMessageSync(sessionID, payload.properties.info.id);
|
|
5008
|
+
const message = this.readMessageSync(sessionID, payload.properties.info.id, options);
|
|
4885
5009
|
if (message) session.messages = [message];
|
|
4886
5010
|
return session;
|
|
4887
5011
|
}
|
|
4888
5012
|
case 'message.part.updated': {
|
|
4889
|
-
const message = this.readMessageSync(sessionID, payload.properties.part.messageID);
|
|
5013
|
+
const message = this.readMessageSync(sessionID, payload.properties.part.messageID, options);
|
|
4890
5014
|
if (message) session.messages = [message];
|
|
4891
5015
|
return session;
|
|
4892
5016
|
}
|
|
4893
5017
|
case 'message.part.removed': {
|
|
4894
|
-
const message = this.readMessageSync(sessionID, payload.properties.messageID);
|
|
5018
|
+
const message = this.readMessageSync(sessionID, payload.properties.messageID, options);
|
|
4895
5019
|
if (message) session.messages = [message];
|
|
4896
5020
|
return session;
|
|
4897
5021
|
}
|
|
@@ -4900,7 +5024,11 @@ export class SqliteLcmStore {
|
|
|
4900
5024
|
}
|
|
4901
5025
|
}
|
|
4902
5026
|
|
|
4903
|
-
private readMessageSync(
|
|
5027
|
+
private readMessageSync(
|
|
5028
|
+
sessionID: string,
|
|
5029
|
+
messageID: string,
|
|
5030
|
+
options?: ReadMessageOptions,
|
|
5031
|
+
): ConversationMessage | undefined {
|
|
4904
5032
|
const db = this.getDb();
|
|
4905
5033
|
const row = safeQueryOne<MessageRow>(
|
|
4906
5034
|
db.prepare('SELECT * FROM messages WHERE session_id = ? AND message_id = ?'),
|
|
@@ -4909,11 +5037,14 @@ export class SqliteLcmStore {
|
|
|
4909
5037
|
);
|
|
4910
5038
|
if (!row) return undefined;
|
|
4911
5039
|
|
|
5040
|
+
const hydrateArtifacts = options?.hydrateArtifacts ?? true;
|
|
4912
5041
|
const artifactsByPart = new Map<string, ArtifactData[]>();
|
|
4913
|
-
|
|
4914
|
-
const
|
|
4915
|
-
|
|
4916
|
-
|
|
5042
|
+
if (hydrateArtifacts) {
|
|
5043
|
+
for (const artifact of this.readArtifactsForMessageSync(messageID)) {
|
|
5044
|
+
const list = artifactsByPart.get(artifact.partID) ?? [];
|
|
5045
|
+
list.push(artifact);
|
|
5046
|
+
artifactsByPart.set(artifact.partID, list);
|
|
5047
|
+
}
|
|
4917
5048
|
}
|
|
4918
5049
|
|
|
4919
5050
|
const parts = db
|
|
@@ -4939,7 +5070,7 @@ export class SqliteLcmStore {
|
|
|
4939
5070
|
info,
|
|
4940
5071
|
parts: parts.map((partRow) => {
|
|
4941
5072
|
const part = parseJson<Part>(partRow.part_json);
|
|
4942
|
-
hydratePartFromArtifacts(part, artifactsByPart.get(part.id) ?? []);
|
|
5073
|
+
if (hydrateArtifacts) hydratePartFromArtifacts(part, artifactsByPart.get(part.id) ?? []);
|
|
4943
5074
|
return part;
|
|
4944
5075
|
}),
|
|
4945
5076
|
};
|
|
@@ -5002,15 +5133,19 @@ export class SqliteLcmStore {
|
|
|
5002
5133
|
const message = session.messages.find(
|
|
5003
5134
|
(entry) => entry.info.id === payload.properties.part.messageID,
|
|
5004
5135
|
);
|
|
5136
|
+
const preservedArtifacts = message
|
|
5137
|
+
? this.readArtifactsForMessageSync(message.info.id).filter(
|
|
5138
|
+
(artifact) => artifact.partID !== payload.properties.part.id,
|
|
5139
|
+
)
|
|
5140
|
+
: [];
|
|
5005
5141
|
const externalized = message ? await this.externalizeMessage(message) : undefined;
|
|
5006
5142
|
withTransaction(this.getDb(), 'capture', () => {
|
|
5007
5143
|
this.upsertSessionRowSync(session);
|
|
5008
5144
|
if (externalized) {
|
|
5009
|
-
this.replaceStoredMessageSync(
|
|
5010
|
-
|
|
5011
|
-
externalized.
|
|
5012
|
-
|
|
5013
|
-
);
|
|
5145
|
+
this.replaceStoredMessageSync(session.sessionID, externalized.storedMessage, [
|
|
5146
|
+
...preservedArtifacts,
|
|
5147
|
+
...externalized.artifacts,
|
|
5148
|
+
]);
|
|
5014
5149
|
}
|
|
5015
5150
|
});
|
|
5016
5151
|
return;
|
|
@@ -5019,15 +5154,19 @@ export class SqliteLcmStore {
|
|
|
5019
5154
|
const message = session.messages.find(
|
|
5020
5155
|
(entry) => entry.info.id === payload.properties.messageID,
|
|
5021
5156
|
);
|
|
5157
|
+
const preservedArtifacts = message
|
|
5158
|
+
? this.readArtifactsForMessageSync(message.info.id).filter(
|
|
5159
|
+
(artifact) => artifact.partID !== payload.properties.partID,
|
|
5160
|
+
)
|
|
5161
|
+
: [];
|
|
5022
5162
|
const externalized = message ? await this.externalizeMessage(message) : undefined;
|
|
5023
5163
|
withTransaction(this.getDb(), 'capture', () => {
|
|
5024
5164
|
this.upsertSessionRowSync(session);
|
|
5025
5165
|
if (externalized) {
|
|
5026
|
-
this.replaceStoredMessageSync(
|
|
5027
|
-
|
|
5028
|
-
externalized.
|
|
5029
|
-
|
|
5030
|
-
);
|
|
5166
|
+
this.replaceStoredMessageSync(session.sessionID, externalized.storedMessage, [
|
|
5167
|
+
...preservedArtifacts,
|
|
5168
|
+
...externalized.artifacts,
|
|
5169
|
+
]);
|
|
5031
5170
|
}
|
|
5032
5171
|
});
|
|
5033
5172
|
return;
|
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 = {
|