opencode-lcm 0.13.2 → 0.13.4

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 CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.13.4] - 2026-04-09
11
+
12
+ ### Fixed
13
+ - Publish validation now keeps the Bun-on-Windows lightweight capture regression test portable across non-Windows CI runners
14
+ - Republish the current malformed-message and Bun Windows capture fixes to npm after the failed 0.13.3 release check
15
+
16
+ ## [0.13.3] - 2026-04-09
17
+
18
+ ### Fixed
19
+ - Bun on Windows now tolerates part-update capture when the parent message has not been materialized yet
20
+ - Additional malformed-message hardening now covers `message.info.time.created` reads and grep scan fallback paths
21
+ - Restored CI-clean formatting after the PR #5 merge so release validation and publish can complete
22
+
10
23
  ## [0.13.2] - 2026-04-08
11
24
 
12
25
  ### Fixed
@@ -273,7 +273,7 @@ export async function externalizeMessage(bindings, message) {
273
273
  const storedInfo = parseJson(JSON.stringify(message.info));
274
274
  const storedParts = [];
275
275
  for (const part of message.parts) {
276
- const { storedPart, artifacts: nextArtifacts } = await externalizePart(bindings, part, message.info.time.created);
276
+ const { storedPart, artifacts: nextArtifacts } = await externalizePart(bindings, part, message.info?.time?.created ?? 0);
277
277
  artifacts.push(...nextArtifacts);
278
278
  storedParts.push(storedPart);
279
279
  }
@@ -292,7 +292,7 @@ export async function externalizeSession(bindings, session) {
292
292
  const storedInfo = parseJson(JSON.stringify(message.info));
293
293
  const storedParts = [];
294
294
  for (const part of message.parts) {
295
- const { storedPart, artifacts: nextArtifacts } = await externalizePart(bindings, part, message.info.time.created);
295
+ const { storedPart, artifacts: nextArtifacts } = await externalizePart(bindings, part, message.info?.time?.created ?? 0);
296
296
  artifacts.push(...nextArtifacts);
297
297
  storedParts.push(storedPart);
298
298
  }
@@ -335,7 +335,7 @@ export function persistStoredSessionSync(bindings, storedSession, artifacts) {
335
335
  const insertMessage = db.prepare('INSERT INTO messages (message_id, session_id, created_at, info_json) VALUES (?, ?, ?, ?)');
336
336
  const insertPart = db.prepare('INSERT INTO parts (part_id, session_id, message_id, sort_key, part_json) VALUES (?, ?, ?, ?, ?)');
337
337
  for (const message of storedSession.messages) {
338
- insertMessage.run(message.info.id, storedSession.sessionID, message.info.time.created, JSON.stringify(message.info));
338
+ insertMessage.run(message.info.id, storedSession.sessionID, message.info?.time?.created ?? 0, JSON.stringify(message.info));
339
339
  message.parts.forEach((part, index) => {
340
340
  insertPart.run(part.id, storedSession.sessionID, part.messageID, index, JSON.stringify(part));
341
341
  });
@@ -202,7 +202,7 @@ export function searchByScan(deps, query, sessionIDs, limit = 5) {
202
202
  id: message.info.id,
203
203
  type: message.info.role,
204
204
  sessionID: session.sessionID,
205
- timestamp: message.info.time.created,
205
+ timestamp: message.info?.time?.created ?? 0,
206
206
  snippet: buildSnippet(blob, query),
207
207
  content: blob,
208
208
  sourceKind: 'message',
@@ -255,7 +255,7 @@ function insertMessageSearchRowsSync(deps, session) {
255
255
  const content = deps.guessMessageText(message, deps.ignoreToolPrefixes);
256
256
  if (!content)
257
257
  continue;
258
- insert.run(session.sessionID, message.info.id, message.info.role, String(message.info.time.created), content);
258
+ insert.run(session.sessionID, message.info.id, message.info.role, String(message.info?.time?.created ?? 0), content);
259
259
  }
260
260
  }
261
261
  export function replaceMessageSearchRowSync(deps, sessionID, message) {
@@ -264,7 +264,7 @@ export function replaceMessageSearchRowSync(deps, sessionID, message) {
264
264
  const content = deps.guessMessageText(message, deps.ignoreToolPrefixes);
265
265
  if (!content)
266
266
  return;
267
- db.prepare('INSERT INTO message_fts (session_id, message_id, role, created_at, content) VALUES (?, ?, ?, ?, ?)').run(sessionID, message.info.id, message.info.role, String(message.info.time.created), content);
267
+ db.prepare('INSERT INTO message_fts (session_id, message_id, role, created_at, content) VALUES (?, ?, ?, ?, ?)').run(sessionID, message.info.id, message.info.role, String(message.info?.time?.created ?? 0), content);
268
268
  }
269
269
  export function replaceSummarySearchRowsSync(deps, sessionIDs) {
270
270
  const db = deps.getDb();
package/dist/store.js CHANGED
@@ -417,6 +417,13 @@ export function resolveCaptureHydrationMode(options) {
417
417
  // hydration there until Bun/Windows is proven stable again.
418
418
  return isBunRuntime && platform === 'win32' ? 'full' : 'targeted';
419
419
  }
420
+ function shouldUseLightweightPartCapture(event, options) {
421
+ const isBunRuntime = options?.isBunRuntime ?? (typeof globalThis === 'object' && 'Bun' in globalThis);
422
+ const platform = options?.platform ?? process.platform;
423
+ if (!isBunRuntime || platform !== 'win32')
424
+ return false;
425
+ return event.type === 'message.part.updated' || event.type === 'message.part.removed';
426
+ }
420
427
  function isSqliteRuntimeImportError(runtime, error) {
421
428
  const code = typeof error === 'object' && error && 'code' in error && typeof error.code === 'string'
422
429
  ? error.code
@@ -970,9 +977,11 @@ export class SqliteLcmStore {
970
977
  }
971
978
  if (!normalized.sessionID || !shouldPersistSession)
972
979
  return;
973
- const session = resolveCaptureHydrationMode() === 'targeted'
974
- ? this.readSessionForCaptureSync(normalized)
975
- : this.readSessionSync(normalized.sessionID);
980
+ const session = shouldUseLightweightPartCapture(normalized)
981
+ ? this.readSessionForCaptureSync(normalized, { hydrateArtifacts: false })
982
+ : resolveCaptureHydrationMode() === 'targeted'
983
+ ? this.readSessionForCaptureSync(normalized)
984
+ : this.readSessionSync(normalized.sessionID);
976
985
  const previousParentSessionID = session.parentSessionID;
977
986
  const shouldSyncDerivedState = this.shouldSyncDerivedSessionStateForEvent(session, normalized);
978
987
  let next = this.applyEvent(session, normalized);
@@ -1253,7 +1262,7 @@ export class SqliteLcmStore {
1253
1262
  issues.push('unexpected-summary-edges');
1254
1263
  return issues.length > 0 ? { sessionID: session.sessionID, issues } : undefined;
1255
1264
  }
1256
- const latestMessageCreated = archived.at(-1)?.info.time.created ?? 0;
1265
+ const latestMessageCreated = archived.at(-1)?.info?.time?.created ?? 0;
1257
1266
  const archivedSignature = this.buildArchivedSignature(archived);
1258
1267
  const rootIDs = state ? parseJson(state.root_node_ids_json) : [];
1259
1268
  const roots = rootIDs
@@ -1364,7 +1373,7 @@ export class SqliteLcmStore {
1364
1373
  case 'message.updated': {
1365
1374
  const existing = session.messages.find((message) => message.info.id === payload.properties.info.id);
1366
1375
  if (existing) {
1367
- return this.isMessageArchivedSync(session.sessionID, existing.info.id, existing.info.time.created);
1376
+ return this.isMessageArchivedSync(session.sessionID, existing.info.id, existing.info?.time?.created ?? 0);
1368
1377
  }
1369
1378
  return this.readMessageCountSync(session.sessionID) >= this.options.freshTailMessages;
1370
1379
  }
@@ -1378,13 +1387,13 @@ export class SqliteLcmStore {
1378
1387
  const message = session.messages.find((entry) => entry.info.id === payload.properties.part.messageID);
1379
1388
  if (!message)
1380
1389
  return false;
1381
- return this.isMessageArchivedSync(session.sessionID, message.info.id, message.info.time.created);
1390
+ return this.isMessageArchivedSync(session.sessionID, message.info.id, message.info?.time?.created ?? 0);
1382
1391
  }
1383
1392
  case 'message.part.removed': {
1384
1393
  const message = session.messages.find((entry) => entry.info.id === payload.properties.messageID);
1385
1394
  if (!message)
1386
1395
  return false;
1387
- return this.isMessageArchivedSync(session.sessionID, message.info.id, message.info.time.created);
1396
+ return this.isMessageArchivedSync(session.sessionID, message.info.id, message.info?.time?.created ?? 0);
1388
1397
  }
1389
1398
  default:
1390
1399
  return false;
@@ -2744,7 +2753,7 @@ export class SqliteLcmStore {
2744
2753
  for (const message of messages) {
2745
2754
  hash.update(message.info.id);
2746
2755
  hash.update(message.info.role);
2747
- hash.update(String(message.info.time.created));
2756
+ hash.update(String(message.info?.time?.created ?? 0));
2748
2757
  hash.update(guessMessageText(message, this.options.interop.ignoreToolPrefixes));
2749
2758
  hash.update(JSON.stringify(listFiles(message)));
2750
2759
  hash.update(JSON.stringify(this.listTools([message])));
@@ -2768,7 +2777,7 @@ export class SqliteLcmStore {
2768
2777
  this.clearSummaryGraphSync(sessionID);
2769
2778
  return [];
2770
2779
  }
2771
- const latestMessageCreated = archivedMessages.at(-1)?.info.time.created ?? 0;
2780
+ const latestMessageCreated = archivedMessages.at(-1)?.info?.time?.created ?? 0;
2772
2781
  const archivedSignature = this.buildArchivedSignature(archivedMessages);
2773
2782
  const state = safeQueryOne(this.getDb().prepare('SELECT * FROM summary_state WHERE session_id = ?'), [sessionID], 'ensureSummaryGraphSync');
2774
2783
  if (state &&
@@ -2932,7 +2941,7 @@ export class SqliteLcmStore {
2932
2941
  latest_message_created = excluded.latest_message_created,
2933
2942
  archived_signature = excluded.archived_signature,
2934
2943
  root_node_ids_json = excluded.root_node_ids_json,
2935
- updated_at = excluded.updated_at`).run(sessionID, archivedMessages.length, archivedMessages.at(-1)?.info.time.created ?? 0, archivedSignature, JSON.stringify(roots.map((node) => node.nodeID)), now);
2944
+ updated_at = excluded.updated_at`).run(sessionID, archivedMessages.length, archivedMessages.at(-1)?.info?.time?.created ?? 0, archivedSignature, JSON.stringify(roots.map((node) => node.nodeID)), now);
2936
2945
  });
2937
2946
  return roots;
2938
2947
  }
@@ -3639,7 +3648,7 @@ export class SqliteLcmStore {
3639
3648
  }
3640
3649
  return parentSessionID;
3641
3650
  }
3642
- readSessionForCaptureSync(event) {
3651
+ readSessionForCaptureSync(event, options) {
3643
3652
  const sessionID = event.sessionID;
3644
3653
  if (!sessionID)
3645
3654
  return emptySession('');
@@ -3647,19 +3656,19 @@ export class SqliteLcmStore {
3647
3656
  const payload = event.payload;
3648
3657
  switch (payload.type) {
3649
3658
  case 'message.updated': {
3650
- const message = this.readMessageSync(sessionID, payload.properties.info.id);
3659
+ const message = this.readMessageSync(sessionID, payload.properties.info.id, options);
3651
3660
  if (message)
3652
3661
  session.messages = [message];
3653
3662
  return session;
3654
3663
  }
3655
3664
  case 'message.part.updated': {
3656
- const message = this.readMessageSync(sessionID, payload.properties.part.messageID);
3665
+ const message = this.readMessageSync(sessionID, payload.properties.part.messageID, options);
3657
3666
  if (message)
3658
3667
  session.messages = [message];
3659
3668
  return session;
3660
3669
  }
3661
3670
  case 'message.part.removed': {
3662
- const message = this.readMessageSync(sessionID, payload.properties.messageID);
3671
+ const message = this.readMessageSync(sessionID, payload.properties.messageID, options);
3663
3672
  if (message)
3664
3673
  session.messages = [message];
3665
3674
  return session;
@@ -3668,16 +3677,19 @@ export class SqliteLcmStore {
3668
3677
  return session;
3669
3678
  }
3670
3679
  }
3671
- readMessageSync(sessionID, messageID) {
3680
+ readMessageSync(sessionID, messageID, options) {
3672
3681
  const db = this.getDb();
3673
3682
  const row = safeQueryOne(db.prepare('SELECT * FROM messages WHERE session_id = ? AND message_id = ?'), [sessionID, messageID], 'readMessageSync');
3674
3683
  if (!row)
3675
3684
  return undefined;
3685
+ const hydrateArtifacts = options?.hydrateArtifacts ?? true;
3676
3686
  const artifactsByPart = new Map();
3677
- for (const artifact of this.readArtifactsForMessageSync(messageID)) {
3678
- const list = artifactsByPart.get(artifact.partID) ?? [];
3679
- list.push(artifact);
3680
- artifactsByPart.set(artifact.partID, list);
3687
+ if (hydrateArtifacts) {
3688
+ for (const artifact of this.readArtifactsForMessageSync(messageID)) {
3689
+ const list = artifactsByPart.get(artifact.partID) ?? [];
3690
+ list.push(artifact);
3691
+ artifactsByPart.set(artifact.partID, list);
3692
+ }
3681
3693
  }
3682
3694
  const parts = db
3683
3695
  .prepare('SELECT * FROM parts WHERE session_id = ? AND message_id = ? ORDER BY sort_key ASC, part_id ASC')
@@ -3694,7 +3706,8 @@ export class SqliteLcmStore {
3694
3706
  info,
3695
3707
  parts: parts.map((partRow) => {
3696
3708
  const part = parseJson(partRow.part_json);
3697
- hydratePartFromArtifacts(part, artifactsByPart.get(part.id) ?? []);
3709
+ if (hydrateArtifacts)
3710
+ hydratePartFromArtifacts(part, artifactsByPart.get(part.id) ?? []);
3698
3711
  return part;
3699
3712
  }),
3700
3713
  };
@@ -3744,22 +3757,34 @@ export class SqliteLcmStore {
3744
3757
  return;
3745
3758
  case 'message.part.updated': {
3746
3759
  const message = session.messages.find((entry) => entry.info.id === payload.properties.part.messageID);
3760
+ const preservedArtifacts = message
3761
+ ? this.readArtifactsForMessageSync(message.info.id).filter((artifact) => artifact.partID !== payload.properties.part.id)
3762
+ : [];
3747
3763
  const externalized = message ? await this.externalizeMessage(message) : undefined;
3748
3764
  withTransaction(this.getDb(), 'capture', () => {
3749
3765
  this.upsertSessionRowSync(session);
3750
3766
  if (externalized) {
3751
- this.replaceStoredMessageSync(session.sessionID, externalized.storedMessage, externalized.artifacts);
3767
+ this.replaceStoredMessageSync(session.sessionID, externalized.storedMessage, [
3768
+ ...preservedArtifacts,
3769
+ ...externalized.artifacts,
3770
+ ]);
3752
3771
  }
3753
3772
  });
3754
3773
  return;
3755
3774
  }
3756
3775
  case 'message.part.removed': {
3757
3776
  const message = session.messages.find((entry) => entry.info.id === payload.properties.messageID);
3777
+ const preservedArtifacts = message
3778
+ ? this.readArtifactsForMessageSync(message.info.id).filter((artifact) => artifact.partID !== payload.properties.partID)
3779
+ : [];
3758
3780
  const externalized = message ? await this.externalizeMessage(message) : undefined;
3759
3781
  withTransaction(this.getDb(), 'capture', () => {
3760
3782
  this.upsertSessionRowSync(session);
3761
3783
  if (externalized) {
3762
- this.replaceStoredMessageSync(session.sessionID, externalized.storedMessage, externalized.artifacts);
3784
+ this.replaceStoredMessageSync(session.sessionID, externalized.storedMessage, [
3785
+ ...preservedArtifacts,
3786
+ ...externalized.artifacts,
3787
+ ]);
3763
3788
  }
3764
3789
  });
3765
3790
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-lcm",
3
- "version": "0.13.2",
3
+ "version": "0.13.4",
4
4
  "description": "Long-memory plugin for OpenCode with context-mode interop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -479,7 +479,7 @@ export async function externalizeMessage(
479
479
  const { storedPart, artifacts: nextArtifacts } = await externalizePart(
480
480
  bindings,
481
481
  part,
482
- message.info.time.created,
482
+ message.info?.time?.created ?? 0,
483
483
  );
484
484
  artifacts.push(...nextArtifacts);
485
485
  storedParts.push(storedPart);
@@ -509,7 +509,7 @@ export async function externalizeSession(
509
509
  const { storedPart, artifacts: nextArtifacts } = await externalizePart(
510
510
  bindings,
511
511
  part,
512
- message.info.time.created,
512
+ message.info?.time?.created ?? 0,
513
513
  );
514
514
  artifacts.push(...nextArtifacts);
515
515
  storedParts.push(storedPart);
@@ -607,7 +607,7 @@ export function persistStoredSessionSync(
607
607
  insertMessage.run(
608
608
  message.info.id,
609
609
  storedSession.sessionID,
610
- message.info.time.created,
610
+ message.info?.time?.created ?? 0,
611
611
  JSON.stringify(message.info),
612
612
  );
613
613
 
@@ -296,7 +296,7 @@ export function searchByScan(
296
296
  id: message.info.id,
297
297
  type: message.info.role,
298
298
  sessionID: session.sessionID,
299
- timestamp: message.info.time.created,
299
+ timestamp: message.info?.time?.created ?? 0,
300
300
  snippet: buildSnippet(blob, query),
301
301
  content: blob,
302
302
  sourceKind: 'message',
@@ -361,7 +361,7 @@ function insertMessageSearchRowsSync(deps: FtsDeps, session: NormalizedSession):
361
361
  session.sessionID,
362
362
  message.info.id,
363
363
  message.info.role,
364
- String(message.info.time.created),
364
+ String(message.info?.time?.created ?? 0),
365
365
  content,
366
366
  );
367
367
  }
@@ -380,7 +380,13 @@ export function replaceMessageSearchRowSync(
380
380
 
381
381
  db.prepare(
382
382
  'INSERT INTO message_fts (session_id, message_id, role, created_at, content) VALUES (?, ?, ?, ?, ?)',
383
- ).run(sessionID, message.info.id, message.info.role, String(message.info.time.created), content);
383
+ ).run(
384
+ sessionID,
385
+ message.info.id,
386
+ message.info.role,
387
+ String(message.info?.time?.created ?? 0),
388
+ content,
389
+ );
384
390
  }
385
391
 
386
392
  export function replaceSummarySearchRowsSync(deps: FtsDeps, sessionIDs?: string[]): void {
package/src/store.ts CHANGED
@@ -613,6 +613,9 @@ type CaptureHydrationOptions = {
613
613
  isBunRuntime?: boolean;
614
614
  platform?: string | undefined;
615
615
  };
616
+ type ReadMessageOptions = {
617
+ hydrateArtifacts?: boolean;
618
+ };
616
619
 
617
620
  function normalizeSqliteRuntimeOverride(value: string | undefined): SqliteRuntime | 'auto' {
618
621
  const normalized = value?.trim().toLowerCase();
@@ -652,6 +655,17 @@ export function resolveCaptureHydrationMode(
652
655
  return isBunRuntime && platform === 'win32' ? 'full' : 'targeted';
653
656
  }
654
657
 
658
+ function shouldUseLightweightPartCapture(
659
+ event: CapturedEvent,
660
+ options?: CaptureHydrationOptions,
661
+ ): boolean {
662
+ const isBunRuntime =
663
+ options?.isBunRuntime ?? (typeof globalThis === 'object' && 'Bun' in globalThis);
664
+ const platform = options?.platform ?? process.platform;
665
+ if (!isBunRuntime || platform !== 'win32') return false;
666
+ return event.type === 'message.part.updated' || event.type === 'message.part.removed';
667
+ }
668
+
655
669
  function isSqliteRuntimeImportError(runtime: SqliteRuntime, error: unknown): boolean {
656
670
  const code =
657
671
  typeof error === 'object' && error && 'code' in error && typeof error.code === 'string'
@@ -1281,8 +1295,9 @@ export class SqliteLcmStore {
1281
1295
 
1282
1296
  if (!normalized.sessionID || !shouldPersistSession) return;
1283
1297
 
1284
- const session =
1285
- resolveCaptureHydrationMode() === 'targeted'
1298
+ const session = shouldUseLightweightPartCapture(normalized)
1299
+ ? this.readSessionForCaptureSync(normalized, { hydrateArtifacts: false })
1300
+ : resolveCaptureHydrationMode() === 'targeted'
1286
1301
  ? this.readSessionForCaptureSync(normalized)
1287
1302
  : this.readSessionSync(normalized.sessionID);
1288
1303
  const previousParentSessionID = session.parentSessionID;
@@ -1695,7 +1710,7 @@ export class SqliteLcmStore {
1695
1710
  return issues.length > 0 ? { sessionID: session.sessionID, issues } : undefined;
1696
1711
  }
1697
1712
 
1698
- const latestMessageCreated = archived.at(-1)?.info.time.created ?? 0;
1713
+ const latestMessageCreated = archived.at(-1)?.info?.time?.created ?? 0;
1699
1714
  const archivedSignature = this.buildArchivedSignature(archived);
1700
1715
  const rootIDs = state ? parseJson<string[]>(state.root_node_ids_json) : [];
1701
1716
  const roots = rootIDs
@@ -1848,7 +1863,7 @@ export class SqliteLcmStore {
1848
1863
  return this.isMessageArchivedSync(
1849
1864
  session.sessionID,
1850
1865
  existing.info.id,
1851
- existing.info.time.created,
1866
+ existing.info?.time?.created ?? 0,
1852
1867
  );
1853
1868
  }
1854
1869
 
@@ -1873,7 +1888,7 @@ export class SqliteLcmStore {
1873
1888
  return this.isMessageArchivedSync(
1874
1889
  session.sessionID,
1875
1890
  message.info.id,
1876
- message.info.time.created,
1891
+ message.info?.time?.created ?? 0,
1877
1892
  );
1878
1893
  }
1879
1894
  case 'message.part.removed': {
@@ -1884,7 +1899,7 @@ export class SqliteLcmStore {
1884
1899
  return this.isMessageArchivedSync(
1885
1900
  session.sessionID,
1886
1901
  message.info.id,
1887
- message.info.time.created,
1902
+ message.info?.time?.created ?? 0,
1888
1903
  );
1889
1904
  }
1890
1905
  default:
@@ -3619,7 +3634,7 @@ export class SqliteLcmStore {
3619
3634
  for (const message of messages) {
3620
3635
  hash.update(message.info.id);
3621
3636
  hash.update(message.info.role);
3622
- hash.update(String(message.info.time.created));
3637
+ hash.update(String(message.info?.time?.created ?? 0));
3623
3638
  hash.update(guessMessageText(message, this.options.interop.ignoreToolPrefixes));
3624
3639
  hash.update(JSON.stringify(listFiles(message)));
3625
3640
  hash.update(JSON.stringify(this.listTools([message])));
@@ -3650,7 +3665,7 @@ export class SqliteLcmStore {
3650
3665
  return [];
3651
3666
  }
3652
3667
 
3653
- const latestMessageCreated = archivedMessages.at(-1)?.info.time.created ?? 0;
3668
+ const latestMessageCreated = archivedMessages.at(-1)?.info?.time?.created ?? 0;
3654
3669
  const archivedSignature = this.buildArchivedSignature(archivedMessages);
3655
3670
  const state = safeQueryOne<SummaryStateRow>(
3656
3671
  this.getDb().prepare('SELECT * FROM summary_state WHERE session_id = ?'),
@@ -3890,7 +3905,7 @@ export class SqliteLcmStore {
3890
3905
  ).run(
3891
3906
  sessionID,
3892
3907
  archivedMessages.length,
3893
- archivedMessages.at(-1)?.info.time.created ?? 0,
3908
+ archivedMessages.at(-1)?.info?.time?.created ?? 0,
3894
3909
  archivedSignature,
3895
3910
  JSON.stringify(roots.map((node) => node.nodeID)),
3896
3911
  now,
@@ -4872,7 +4887,10 @@ export class SqliteLcmStore {
4872
4887
  return parentSessionID;
4873
4888
  }
4874
4889
 
4875
- private readSessionForCaptureSync(event: CapturedEvent): NormalizedSession {
4890
+ private readSessionForCaptureSync(
4891
+ event: CapturedEvent,
4892
+ options?: ReadMessageOptions,
4893
+ ): NormalizedSession {
4876
4894
  const sessionID = event.sessionID;
4877
4895
  if (!sessionID) return emptySession('');
4878
4896
 
@@ -4881,17 +4899,17 @@ export class SqliteLcmStore {
4881
4899
 
4882
4900
  switch (payload.type) {
4883
4901
  case 'message.updated': {
4884
- const message = this.readMessageSync(sessionID, payload.properties.info.id);
4902
+ const message = this.readMessageSync(sessionID, payload.properties.info.id, options);
4885
4903
  if (message) session.messages = [message];
4886
4904
  return session;
4887
4905
  }
4888
4906
  case 'message.part.updated': {
4889
- const message = this.readMessageSync(sessionID, payload.properties.part.messageID);
4907
+ const message = this.readMessageSync(sessionID, payload.properties.part.messageID, options);
4890
4908
  if (message) session.messages = [message];
4891
4909
  return session;
4892
4910
  }
4893
4911
  case 'message.part.removed': {
4894
- const message = this.readMessageSync(sessionID, payload.properties.messageID);
4912
+ const message = this.readMessageSync(sessionID, payload.properties.messageID, options);
4895
4913
  if (message) session.messages = [message];
4896
4914
  return session;
4897
4915
  }
@@ -4900,7 +4918,11 @@ export class SqliteLcmStore {
4900
4918
  }
4901
4919
  }
4902
4920
 
4903
- private readMessageSync(sessionID: string, messageID: string): ConversationMessage | undefined {
4921
+ private readMessageSync(
4922
+ sessionID: string,
4923
+ messageID: string,
4924
+ options?: ReadMessageOptions,
4925
+ ): ConversationMessage | undefined {
4904
4926
  const db = this.getDb();
4905
4927
  const row = safeQueryOne<MessageRow>(
4906
4928
  db.prepare('SELECT * FROM messages WHERE session_id = ? AND message_id = ?'),
@@ -4909,11 +4931,14 @@ export class SqliteLcmStore {
4909
4931
  );
4910
4932
  if (!row) return undefined;
4911
4933
 
4934
+ const hydrateArtifacts = options?.hydrateArtifacts ?? true;
4912
4935
  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);
4936
+ if (hydrateArtifacts) {
4937
+ for (const artifact of this.readArtifactsForMessageSync(messageID)) {
4938
+ const list = artifactsByPart.get(artifact.partID) ?? [];
4939
+ list.push(artifact);
4940
+ artifactsByPart.set(artifact.partID, list);
4941
+ }
4917
4942
  }
4918
4943
 
4919
4944
  const parts = db
@@ -4939,7 +4964,7 @@ export class SqliteLcmStore {
4939
4964
  info,
4940
4965
  parts: parts.map((partRow) => {
4941
4966
  const part = parseJson<Part>(partRow.part_json);
4942
- hydratePartFromArtifacts(part, artifactsByPart.get(part.id) ?? []);
4967
+ if (hydrateArtifacts) hydratePartFromArtifacts(part, artifactsByPart.get(part.id) ?? []);
4943
4968
  return part;
4944
4969
  }),
4945
4970
  };
@@ -5002,15 +5027,19 @@ export class SqliteLcmStore {
5002
5027
  const message = session.messages.find(
5003
5028
  (entry) => entry.info.id === payload.properties.part.messageID,
5004
5029
  );
5030
+ const preservedArtifacts = message
5031
+ ? this.readArtifactsForMessageSync(message.info.id).filter(
5032
+ (artifact) => artifact.partID !== payload.properties.part.id,
5033
+ )
5034
+ : [];
5005
5035
  const externalized = message ? await this.externalizeMessage(message) : undefined;
5006
5036
  withTransaction(this.getDb(), 'capture', () => {
5007
5037
  this.upsertSessionRowSync(session);
5008
5038
  if (externalized) {
5009
- this.replaceStoredMessageSync(
5010
- session.sessionID,
5011
- externalized.storedMessage,
5012
- externalized.artifacts,
5013
- );
5039
+ this.replaceStoredMessageSync(session.sessionID, externalized.storedMessage, [
5040
+ ...preservedArtifacts,
5041
+ ...externalized.artifacts,
5042
+ ]);
5014
5043
  }
5015
5044
  });
5016
5045
  return;
@@ -5019,15 +5048,19 @@ export class SqliteLcmStore {
5019
5048
  const message = session.messages.find(
5020
5049
  (entry) => entry.info.id === payload.properties.messageID,
5021
5050
  );
5051
+ const preservedArtifacts = message
5052
+ ? this.readArtifactsForMessageSync(message.info.id).filter(
5053
+ (artifact) => artifact.partID !== payload.properties.partID,
5054
+ )
5055
+ : [];
5022
5056
  const externalized = message ? await this.externalizeMessage(message) : undefined;
5023
5057
  withTransaction(this.getDb(), 'capture', () => {
5024
5058
  this.upsertSessionRowSync(session);
5025
5059
  if (externalized) {
5026
- this.replaceStoredMessageSync(
5027
- session.sessionID,
5028
- externalized.storedMessage,
5029
- externalized.artifacts,
5030
- );
5060
+ this.replaceStoredMessageSync(session.sessionID, externalized.storedMessage, [
5061
+ ...preservedArtifacts,
5062
+ ...externalized.artifacts,
5063
+ ]);
5031
5064
  }
5032
5065
  });
5033
5066
  return;