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/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 compareMessages(a: ConversationMessage, b: ConversationMessage): number {
420
- const aInfo = getValidMessageInfo(a.info);
421
- const bInfo = getValidMessageInfo(b.info);
422
- if (!aInfo && !bInfo) return 0;
423
- if (!aInfo) return 1;
424
- if (!bInfo) return -1;
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
- return aInfo.time.created - bInfo.time.created || aInfo.id.localeCompare(bInfo.id);
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.parts) {
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
- if (part.text.startsWith('[Archived by opencode-lcm:')) break;
510
- const sanitized = sanitizeAutomaticRetrievalSourceText(part.text);
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
- if (part.text.startsWith('[Archived by opencode-lcm:')) break;
516
- const sanitized = sanitizeAutomaticRetrievalSourceText(part.text);
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
- if (ignoreToolPrefixes.some((prefix) => part.tool.startsWith(prefix))) break;
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(`${part.tool}: ${state.output}`);
531
- if (state.status === 'error') segments.push(`${part.tool}: ${state.error}`);
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(`${part.tool}: ${JSON.stringify(state.input)}`);
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(`${part.tool} attachments: ${attachmentNames.join(', ')}`);
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
- resolveCaptureHydrationMode() === 'targeted'
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)?.info.time.created ?? 0;
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.info.time.created,
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.info.time.created,
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.info.time.created,
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.id);
3621
- hash.update(message.info.role);
3622
- hash.update(String(message.info.time.created));
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.parts.length));
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)?.info.time.created ?? 0;
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)?.info.time.created ?? 0,
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(event: CapturedEvent): NormalizedSession {
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(sessionID: string, messageID: string): ConversationMessage | undefined {
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
- for (const artifact of this.readArtifactsForMessageSync(messageID)) {
4914
- const list = artifactsByPart.get(artifact.partID) ?? [];
4915
- list.push(artifact);
4916
- artifactsByPart.set(artifact.partID, list);
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
- session.sessionID,
5011
- externalized.storedMessage,
5012
- externalized.artifacts,
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
- session.sessionID,
5028
- externalized.storedMessage,
5029
- externalized.artifacts,
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 = {