feat: Implement user prompt syncing to Chroma and enhance timeline querying

- Added `getObservationById` method to retrieve observations by ID in SessionStore.
- Introduced `getSessionSummariesByIds` and `getUserPromptsByIds` methods for fetching session summaries and user prompts by IDs.
- Developed `getTimelineAroundTimestamp` and `getTimelineAroundObservation` methods to provide a unified timeline of observations, sessions, and prompts around a specified anchor point.
- Enhanced ChromaSync to format and sync user prompts, including a new `syncUserPrompt` method.
- Updated WorkerService to sync the latest user prompt to Chroma after updating the worker port.
- Created tests for timeline querying and MCP handler logic to ensure functionality.
- Documented the implementation plan for user prompts and timeline context tool in the Chroma search completion plan.
This commit is contained in:
Alex Newman
2025-11-03 16:55:33 -05:00
parent c6bf72ca72
commit 633f89a5fb
18 changed files with 2152 additions and 229 deletions
+65
View File
@@ -280,6 +280,71 @@ async function main() {
const sessionTotalTime = ((Date.now() - sessionStartTime) / 1000).toFixed(1);
console.log(`✅ Synced ${sessionDocs.length} session documents in ${sessionTotalTime}s\n`);
// Fetch user prompts
console.log('📖 Reading user prompts from SQLite...');
const prompts = store.db.prepare(`
SELECT
up.*,
s.project,
s.sdk_session_id
FROM user_prompts up
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
WHERE s.project = ?
ORDER BY up.created_at_epoch DESC
LIMIT 1000
`).all(project) as any[];
console.log(`Found ${prompts.length} user prompts`);
// Prepare prompt documents - one document per prompt
const promptDocs: ChromaDocument[] = [];
for (const prompt of prompts) {
promptDocs.push({
id: `prompt_${prompt.id}`,
document: prompt.prompt_text,
metadata: {
sqlite_id: prompt.id,
doc_type: 'user_prompt',
sdk_session_id: prompt.sdk_session_id,
project: prompt.project,
created_at_epoch: prompt.created_at_epoch,
prompt_number: prompt.prompt_number || 0
}
});
}
console.log(`Created ${promptDocs.length} user prompt documents\n`);
// Sync prompts in batches
console.log('⬆️ Syncing user prompts to ChromaDB...');
const promptBatches = Math.ceil(promptDocs.length / batchSize);
const promptStartTime = Date.now();
for (let i = 0; i < promptDocs.length; i += batchSize) {
const batch = promptDocs.slice(i, i + batchSize);
const batchNumber = Math.floor(i / batchSize) + 1;
const progress = Math.round((batchNumber / promptBatches) * 100);
const docsProcessed = Math.min(i + batchSize, promptDocs.length);
const elapsed = ((Date.now() - promptStartTime) / 1000).toFixed(1);
process.stdout.write(` [${batchNumber}/${promptBatches}] ${progress}% - Syncing docs ${i + 1}-${docsProcessed}/${promptDocs.length} (${elapsed}s elapsed)...`);
await client.callTool({
name: 'chroma_add_documents',
arguments: {
collection_name: collectionName,
documents: batch.map(d => d.document),
ids: batch.map(d => d.id),
metadatas: batch.map(d => d.metadata)
}
});
console.log(' ✓');
}
const promptTotalTime = ((Date.now() - promptStartTime) / 1000).toFixed(1);
console.log(`✅ Synced ${promptDocs.length} user prompt documents in ${promptTotalTime}s\n`);
// Get collection info
const infoResult = await client.callTool({
name: 'chroma_get_collection_info',
+54
View File
@@ -0,0 +1,54 @@
import { SessionStore } from '../src/services/sqlite/SessionStore.js';
const store = new SessionStore();
// Simulate what the MCP handler does
const args = {
anchor: 3300,
depth_before: 10,
depth_after: 10
};
console.log('Testing MCP handler logic with anchor:', args.anchor);
try {
let timeline;
const anchor = args.anchor;
const depth_before = args.depth_before;
const depth_after = args.depth_after;
if (typeof anchor === 'number') {
console.log('Anchor is number, getting observation...');
const obs = store.getObservationById(anchor);
if (!obs) {
console.error('Observation not found!');
process.exit(1);
}
console.log('Found observation:', obs.id, 'at epoch:', obs.created_at_epoch);
console.log('Calling getTimelineAroundObservation...');
timeline = store.getTimelineAroundObservation(anchor, obs.created_at_epoch, depth_before, depth_after);
console.log('Timeline result:', {
observations: timeline.observations?.length,
sessions: timeline.sessions?.length,
prompts: timeline.prompts?.length
});
console.log('Timeline observations type:', typeof timeline.observations);
console.log('Timeline sessions type:', typeof timeline.sessions);
console.log('Timeline prompts type:', typeof timeline.prompts);
if (timeline.observations) {
console.log('First observation:', timeline.observations[0]);
}
}
console.log('\n✓ No errors!');
} catch (err) {
console.error('ERROR:', err.message);
console.error(err.stack);
process.exit(1);
}
store.close();
+67
View File
@@ -0,0 +1,67 @@
import { SessionStore } from '../src/services/sqlite/SessionStore.js';
const store = new SessionStore();
console.log('=== Test 1: Without project filter ===');
try {
const result = store.getTimelineAroundTimestamp(
1730667961000, // timestamp for observation 3300
5,
5
);
console.log('Result:', {
observations: result?.observations?.length,
sessions: result?.sessions?.length,
prompts: result?.prompts?.length
});
} catch (err) {
console.error('ERROR:', err);
}
console.log('\n=== Test 2: With project filter ===');
try {
const result = store.getTimelineAroundTimestamp(
1730667961000,
5,
5,
'claude-mem'
);
console.log('Result:', {
observations: result?.observations?.length,
sessions: result?.sessions?.length,
prompts: result?.prompts?.length
});
} catch (err) {
console.error('ERROR:', err);
}
console.log('\n=== Test 3: With actual observation ID ===');
// First get the actual timestamp for observation 3300
const obs = store.getObservationById(3300);
console.log('Observation 3300:', obs ? `Found at epoch ${obs.created_at_epoch}` : 'Not found');
if (obs) {
try {
const result = store.getTimelineAroundTimestamp(
obs.created_at_epoch,
5,
5
);
console.log('Result:', {
observations: result?.observations?.length,
sessions: result?.sessions?.length,
prompts: result?.prompts?.length
});
console.log('Observations:', result.observations?.map(o => `#${o.id}`));
console.log('Sessions:', result.sessions?.map(s => `#S${s.id}`));
console.log('Prompts:', result.prompts?.map(p => `#P${p.id}`));
} catch (err) {
console.error('ERROR:', err);
}
}
store.close();