diff --git a/src/store-artifacts.ts b/src/store-artifacts.ts index 2a4faea..b57abbb 100644 --- a/src/store-artifacts.ts +++ b/src/store-artifacts.ts @@ -479,7 +479,7 @@ export async function externalizeMessage( const { storedPart, artifacts: nextArtifacts } = await externalizePart( bindings, part, - message.info.time.created, + message.info?.time?.created ?? 0, ); artifacts.push(...nextArtifacts); storedParts.push(storedPart); @@ -509,7 +509,7 @@ export async function externalizeSession( const { storedPart, artifacts: nextArtifacts } = await externalizePart( bindings, part, - message.info.time.created, + message.info?.time?.created ?? 0, ); artifacts.push(...nextArtifacts); storedParts.push(storedPart); @@ -607,7 +607,7 @@ export function persistStoredSessionSync( insertMessage.run( message.info.id, storedSession.sessionID, - message.info.time.created, + message.info?.time?.created ?? 0, JSON.stringify(message.info), ); diff --git a/src/store-search.ts b/src/store-search.ts index f48b2dd..ffd8cf0 100644 --- a/src/store-search.ts +++ b/src/store-search.ts @@ -296,7 +296,7 @@ export function searchByScan( id: message.info.id, type: message.info.role, sessionID: session.sessionID, - timestamp: message.info.time.created, + timestamp: message.info?.time?.created ?? 0, snippet: buildSnippet(blob, query), content: blob, sourceKind: 'message', @@ -361,7 +361,7 @@ function insertMessageSearchRowsSync(deps: FtsDeps, session: NormalizedSession): session.sessionID, message.info.id, message.info.role, - String(message.info.time.created), + String(message.info?.time?.created ?? 0), content, ); } @@ -380,7 +380,13 @@ export function replaceMessageSearchRowSync( 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); + ).run( + sessionID, + message.info.id, + message.info.role, + String(message.info?.time?.created ?? 0), + content, + ); } export function replaceSummarySearchRowsSync(deps: FtsDeps, sessionIDs?: string[]): void { diff --git a/src/store.ts b/src/store.ts index 1dde676..05720db 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1695,7 +1695,7 @@ export class SqliteLcmStore { return issues.length > 0 ? { sessionID: session.sessionID, issues } : undefined; } - const latestMessageCreated = archived.at(-1)?.info.time.created ?? 0; + const latestMessageCreated = archived.at(-1)?.info?.time?.created ?? 0; const archivedSignature = this.buildArchivedSignature(archived); const rootIDs = state ? parseJson(state.root_node_ids_json) : []; const roots = rootIDs @@ -1848,7 +1848,7 @@ export class SqliteLcmStore { return this.isMessageArchivedSync( session.sessionID, existing.info.id, - existing.info.time.created, + existing.info?.time?.created ?? 0, ); } @@ -1873,7 +1873,7 @@ export class SqliteLcmStore { return this.isMessageArchivedSync( session.sessionID, message.info.id, - message.info.time.created, + message.info?.time?.created ?? 0, ); } case 'message.part.removed': { @@ -1884,7 +1884,7 @@ export class SqliteLcmStore { return this.isMessageArchivedSync( session.sessionID, message.info.id, - message.info.time.created, + message.info?.time?.created ?? 0, ); } default: @@ -3619,7 +3619,7 @@ export class SqliteLcmStore { for (const message of messages) { hash.update(message.info.id); hash.update(message.info.role); - hash.update(String(message.info.time.created)); + hash.update(String(message.info?.time?.created ?? 0)); hash.update(guessMessageText(message, this.options.interop.ignoreToolPrefixes)); hash.update(JSON.stringify(listFiles(message))); hash.update(JSON.stringify(this.listTools([message]))); @@ -3650,7 +3650,7 @@ export class SqliteLcmStore { return []; } - const latestMessageCreated = archivedMessages.at(-1)?.info.time.created ?? 0; + const latestMessageCreated = archivedMessages.at(-1)?.info?.time?.created ?? 0; const archivedSignature = this.buildArchivedSignature(archivedMessages); const state = safeQueryOne( this.getDb().prepare('SELECT * FROM summary_state WHERE session_id = ?'), @@ -3890,7 +3890,7 @@ export class SqliteLcmStore { ).run( sessionID, archivedMessages.length, - archivedMessages.at(-1)?.info.time.created ?? 0, + archivedMessages.at(-1)?.info?.time?.created ?? 0, archivedSignature, JSON.stringify(roots.map((node) => node.nodeID)), now, diff --git a/tests/store-transform.test.mjs b/tests/store-transform.test.mjs index 2c579b6..849688d 100644 --- a/tests/store-transform.test.mjs +++ b/tests/store-transform.test.mjs @@ -1719,6 +1719,54 @@ test('resume and describe skip stored messages with malformed metadata', async ( } }); +test('grep scan fallback skips stored messages with malformed metadata', async () => { + const workspace = makeWorkspace('lcm-grep-malformed-info'); + const dbPath = path.join(workspace, '.lcm', 'lcm.db'); + let store; + let db; + + try { + store = new SqliteLcmStore(workspace, makeOptions()); + await store.init(); + + await createSession(store, workspace, 's1', 1); + for (const [messageID, created, text] of [ + ['m1', 2, 'fallback => keep this result'], + ['m2', 3, 'fallback => skip this result'], + ]) { + await captureMessage(store, { + sessionID: 's1', + messageID, + created, + parts: [textPart('s1', messageID, `${messageID}-p`, text)], + }); + } + + db = new DatabaseSync(dbPath, { + enableForeignKeyConstraints: true, + timeout: 5000, + }); + db.prepare('UPDATE messages SET info_json = ? WHERE session_id = ? AND message_id = ?').run( + JSON.stringify({ id: 'm2' }), + 's1', + 'm2', + ); + db.close(); + db = undefined; + + const results = await store.grep({ query: '=>', sessionID: 's1', limit: 5 }); + + assert.deepEqual( + results.map((result) => result.id), + ['m1'], + ); + } finally { + db?.close(); + store?.close(); + await cleanupWorkspace(workspace); + } +}); + test('capture ignores malformed message and part update events', async () => { const workspace = makeWorkspace('lcm-capture-malformed-events'); let store;