Enhance memory search functionality with timeline context retrieval (#151)
- Introduced optional timeline context retrieval step in memory search flow to provide users with better understanding of previous sessions. - Updated SKILL.md to reflect new flow, including timeline context commands and usage scenarios. - Refactored timeline retrieval commands in timeline-by-query.md and timeline.md to utilize new MCP tools for streamlined access. - Implemented filtering logic in search-server.ts to respect depth_before and depth_after parameters when displaying timeline items. - Improved response formatting to include filtered item counts and enhanced user guidance for timeline queries.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -19,8 +19,9 @@ Use when users ask about PREVIOUS sessions (not current conversation):
|
|||||||
**ALWAYS follow this exact flow:**
|
**ALWAYS follow this exact flow:**
|
||||||
|
|
||||||
1. **Search** - Get an index of results with IDs
|
1. **Search** - Get an index of results with IDs
|
||||||
2. **Review** - Look at titles/dates, pick relevant IDs
|
2. **Timeline** (optional) - Get context around top results to understand what was happening
|
||||||
3. **Fetch** - Get full details ONLY for those IDs
|
3. **Review** - Look at titles/dates/context, pick relevant IDs
|
||||||
|
4. **Fetch** - Get full details ONLY for those IDs
|
||||||
|
|
||||||
### Step 1: Search Everything
|
### Step 1: Search Everything
|
||||||
|
|
||||||
@@ -44,11 +45,30 @@ curl "http://localhost:37777/api/search?query=authentication&format=index&limit=
|
|||||||
ID: 10942
|
ID: 10942
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2: Pick IDs
|
### Step 2: Get Timeline Context (Optional)
|
||||||
|
|
||||||
Review the index results. Identify which IDs are actually relevant. Discard the rest.
|
When you need to understand "what was happening" around a result:
|
||||||
|
|
||||||
### Step 3: Fetch by ID
|
```bash
|
||||||
|
# Get timeline around an observation ID
|
||||||
|
curl "http://localhost:37777/api/timeline?anchor=11131&depth_before=3&depth_after=3"
|
||||||
|
|
||||||
|
# Or use query to find + get timeline in one step
|
||||||
|
curl "http://localhost:37777/api/timeline?query=authentication&depth_before=3&depth_after=3"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns exactly `depth_before + 1 + depth_after` items** - observations, sessions, and prompts interleaved chronologically around the anchor.
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
- User asks "what was happening when..."
|
||||||
|
- Need to understand sequence of events
|
||||||
|
- Want broader context around a specific observation
|
||||||
|
|
||||||
|
### Step 3: Pick IDs
|
||||||
|
|
||||||
|
Review the index results (and timeline if used). Identify which IDs are actually relevant. Discard the rest.
|
||||||
|
|
||||||
|
### Step 4: Fetch by ID
|
||||||
|
|
||||||
For each relevant ID, fetch full details:
|
For each relevant ID, fetch full details:
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,16 @@ Search for observations and get timeline context in a single request. Combines s
|
|||||||
- User asks: "Timeline of database work"
|
- User asks: "Timeline of database work"
|
||||||
- Need to find something then see temporal context
|
- Need to find something then see temporal context
|
||||||
|
|
||||||
## Command
|
## MCP Tool
|
||||||
|
|
||||||
```bash
|
Use the `get_timeline_by_query` MCP tool:
|
||||||
|
|
||||||
|
```
|
||||||
# Auto mode: Uses top search result as timeline anchor
|
# Auto mode: Uses top search result as timeline anchor
|
||||||
curl -s "http://localhost:37777/api/timeline/by-query?query=authentication&mode=auto&depth_before=10&depth_after=10"
|
get_timeline_by_query(query="authentication", mode="auto", depth_before=10, depth_after=10)
|
||||||
|
|
||||||
# Interactive mode: Shows top N search results for manual selection
|
# Interactive mode: Shows top N search results for manual selection
|
||||||
curl -s "http://localhost:37777/api/timeline/by-query?query=authentication&mode=interactive&limit=5"
|
get_timeline_by_query(query="authentication", mode="interactive", limit=5)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Parameters
|
## Parameters
|
||||||
@@ -34,8 +36,8 @@ curl -s "http://localhost:37777/api/timeline/by-query?query=authentication&mode=
|
|||||||
|
|
||||||
Automatically gets timeline around best match:
|
Automatically gets timeline around best match:
|
||||||
|
|
||||||
```bash
|
```
|
||||||
curl -s "http://localhost:37777/api/timeline/by-query?query=JWT+authentication&mode=auto&depth_before=10&depth_after=10"
|
get_timeline_by_query(query="JWT authentication", mode="auto", depth_before=10, depth_after=10)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
@@ -64,8 +66,8 @@ curl -s "http://localhost:37777/api/timeline/by-query?query=JWT+authentication&m
|
|||||||
|
|
||||||
Shows top search results for manual review:
|
Shows top search results for manual review:
|
||||||
|
|
||||||
```bash
|
```
|
||||||
curl -s "http://localhost:37777/api/timeline/by-query?query=authentication&mode=interactive&limit=5"
|
get_timeline_by_query(query="authentication", mode="interactive", limit=5)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
@@ -89,7 +91,7 @@ curl -s "http://localhost:37777/api/timeline/by-query?query=authentication&mode=
|
|||||||
"score": 0.87
|
"score": 0.87
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"next_step": "Use /api/timeline/context?anchor=<id>&depth_before=10&depth_after=10"
|
"next_step": "Use get_context_timeline(anchor=<id>, depth_before=10, depth_after=10)"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -9,17 +9,14 @@ Get a chronological timeline of observations, sessions, and prompts around a spe
|
|||||||
- User asks: "What happened before and after that change?"
|
- User asks: "What happened before and after that change?"
|
||||||
- Need temporal context around an event
|
- Need temporal context around an event
|
||||||
|
|
||||||
## Command
|
## MCP Tool
|
||||||
|
|
||||||
```bash
|
Use the `get_context_timeline` MCP tool:
|
||||||
# Using observation ID as anchor
|
|
||||||
curl -s "http://localhost:37777/api/timeline/context?anchor=1234&depth_before=10&depth_after=10"
|
|
||||||
|
|
||||||
# Using session ID as anchor
|
```
|
||||||
curl -s "http://localhost:37777/api/timeline/context?anchor=S545&depth_before=10&depth_after=10"
|
get_context_timeline(anchor=1234, depth_before=10, depth_after=10)
|
||||||
|
get_context_timeline(anchor="S545", depth_before=10, depth_after=10)
|
||||||
# Using ISO timestamp as anchor
|
get_context_timeline(anchor="2024-11-09T12:00:00Z", depth_before=10, depth_after=10)
|
||||||
curl -s "http://localhost:37777/api/timeline/context?anchor=2024-11-09T12:00:00Z&depth_before=10&depth_after=10"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Parameters
|
## Parameters
|
||||||
|
|||||||
@@ -135,6 +135,46 @@ Other tips:
|
|||||||
• To sort by date: Use orderBy: "date_desc" or "date_asc"`;
|
• To sort by date: Use orderBy: "date_desc" or "date_asc"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeline item for unified chronological display
|
||||||
|
*/
|
||||||
|
interface TimelineItem {
|
||||||
|
type: 'observation' | 'session' | 'prompt';
|
||||||
|
data: any;
|
||||||
|
epoch: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter timeline items to respect depth_before/depth_after window around anchor
|
||||||
|
*/
|
||||||
|
function filterTimelineByDepth(
|
||||||
|
items: TimelineItem[],
|
||||||
|
anchorId: number | string,
|
||||||
|
anchorEpoch: number,
|
||||||
|
depth_before: number,
|
||||||
|
depth_after: number
|
||||||
|
): TimelineItem[] {
|
||||||
|
if (items.length === 0) return items;
|
||||||
|
|
||||||
|
let anchorIndex = -1;
|
||||||
|
if (typeof anchorId === 'number') {
|
||||||
|
anchorIndex = items.findIndex(item => item.type === 'observation' && item.data.id === anchorId);
|
||||||
|
} else if (typeof anchorId === 'string' && anchorId.startsWith('S')) {
|
||||||
|
const sessionNum = parseInt(anchorId.slice(1), 10);
|
||||||
|
anchorIndex = items.findIndex(item => item.type === 'session' && item.data.id === sessionNum);
|
||||||
|
} else {
|
||||||
|
// Timestamp anchor - find closest item
|
||||||
|
anchorIndex = items.findIndex(item => item.epoch >= anchorEpoch);
|
||||||
|
if (anchorIndex === -1) anchorIndex = items.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anchorIndex === -1) return items;
|
||||||
|
|
||||||
|
const startIndex = Math.max(0, anchorIndex - depth_before);
|
||||||
|
const endIndex = Math.min(items.length, anchorIndex + depth_after + 1);
|
||||||
|
return items.slice(startIndex, endIndex);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format observation as index entry (title, date, ID only)
|
* Format observation as index entry (title, date, ID only)
|
||||||
*/
|
*/
|
||||||
@@ -723,22 +763,16 @@ const tools = [
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine and sort all items chronologically
|
// Combine, sort, and filter timeline items
|
||||||
interface TimelineItem {
|
|
||||||
type: 'observation' | 'session' | 'prompt';
|
|
||||||
data: any;
|
|
||||||
epoch: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items: TimelineItem[] = [
|
const items: TimelineItem[] = [
|
||||||
...timeline.observations.map((obs: any) => ({ type: 'observation' as const, data: obs, epoch: obs.created_at_epoch })),
|
...timeline.observations.map((obs: any) => ({ type: 'observation' as const, data: obs, epoch: obs.created_at_epoch })),
|
||||||
...timeline.sessions.map((sess: any) => ({ type: 'session' as const, data: sess, epoch: sess.created_at_epoch })),
|
...timeline.sessions.map((sess: any) => ({ type: 'session' as const, data: sess, epoch: sess.created_at_epoch })),
|
||||||
...timeline.prompts.map((prompt: any) => ({ type: 'prompt' as const, data: prompt, epoch: prompt.created_at_epoch }))
|
...timeline.prompts.map((prompt: any) => ({ type: 'prompt' as const, data: prompt, epoch: prompt.created_at_epoch }))
|
||||||
];
|
];
|
||||||
|
|
||||||
items.sort((a, b) => a.epoch - b.epoch);
|
items.sort((a, b) => a.epoch - b.epoch);
|
||||||
|
const filteredItems = filterTimelineByDepth(items, anchorId, anchorEpoch, depth_before, depth_after);
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (filteredItems.length === 0) {
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
type: 'text' as const,
|
type: 'text' as const,
|
||||||
@@ -789,7 +823,7 @@ const tools = [
|
|||||||
|
|
||||||
// Header
|
// Header
|
||||||
if (query) {
|
if (query) {
|
||||||
const anchorObs = items.find(item => item.type === 'observation' && item.data.id === anchorId);
|
const anchorObs = filteredItems.find(item => item.type === 'observation' && item.data.id === anchorId);
|
||||||
const anchorTitle = anchorObs ? (anchorObs.data.title || 'Untitled') : 'Unknown';
|
const anchorTitle = anchorObs ? (anchorObs.data.title || 'Untitled') : 'Unknown';
|
||||||
lines.push(`# Timeline for query: "${query}"`);
|
lines.push(`# Timeline for query: "${query}"`);
|
||||||
lines.push(`**Anchor:** Observation #${anchorId} - ${anchorTitle}`);
|
lines.push(`**Anchor:** Observation #${anchorId} - ${anchorTitle}`);
|
||||||
@@ -797,7 +831,7 @@ const tools = [
|
|||||||
lines.push(`# Timeline around anchor: ${anchorId}`);
|
lines.push(`# Timeline around anchor: ${anchorId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push(`**Window:** ${depth_before} records before → ${depth_after} records after | **Items:** ${items.length} (${timeline.observations.length} obs, ${timeline.sessions.length} sessions, ${timeline.prompts.length} prompts)`);
|
lines.push(`**Window:** ${depth_before} records before → ${depth_after} records after | **Items:** ${filteredItems.length}`);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
// Legend
|
// Legend
|
||||||
@@ -806,7 +840,7 @@ const tools = [
|
|||||||
|
|
||||||
// Group by day
|
// Group by day
|
||||||
const dayMap = new Map<string, TimelineItem[]>();
|
const dayMap = new Map<string, TimelineItem[]>();
|
||||||
for (const item of items) {
|
for (const item of filteredItems) {
|
||||||
const day = formatDate(item.epoch);
|
const day = formatDate(item.epoch);
|
||||||
if (!dayMap.has(day)) {
|
if (!dayMap.has(day)) {
|
||||||
dayMap.set(day, []);
|
dayMap.set(day, []);
|
||||||
@@ -2029,22 +2063,16 @@ const tools = [
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine and sort all items chronologically
|
// Combine, sort, and filter timeline items
|
||||||
interface TimelineItem {
|
|
||||||
type: 'observation' | 'session' | 'prompt';
|
|
||||||
data: any;
|
|
||||||
epoch: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items: TimelineItem[] = [
|
const items: TimelineItem[] = [
|
||||||
...timeline.observations.map(obs => ({ type: 'observation' as const, data: obs, epoch: obs.created_at_epoch })),
|
...timeline.observations.map(obs => ({ type: 'observation' as const, data: obs, epoch: obs.created_at_epoch })),
|
||||||
...timeline.sessions.map(sess => ({ type: 'session' as const, data: sess, epoch: sess.created_at_epoch })),
|
...timeline.sessions.map(sess => ({ type: 'session' as const, data: sess, epoch: sess.created_at_epoch })),
|
||||||
...timeline.prompts.map(prompt => ({ type: 'prompt' as const, data: prompt, epoch: prompt.created_at_epoch }))
|
...timeline.prompts.map(prompt => ({ type: 'prompt' as const, data: prompt, epoch: prompt.created_at_epoch }))
|
||||||
];
|
];
|
||||||
|
|
||||||
items.sort((a, b) => a.epoch - b.epoch);
|
items.sort((a, b) => a.epoch - b.epoch);
|
||||||
|
const filteredItems = filterTimelineByDepth(items, anchorId, anchorEpoch, depth_before, depth_after);
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (filteredItems.length === 0) {
|
||||||
const anchorDate = new Date(anchorEpoch).toLocaleString();
|
const anchorDate = new Date(anchorEpoch).toLocaleString();
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
@@ -2094,7 +2122,7 @@ const tools = [
|
|||||||
|
|
||||||
// Header
|
// Header
|
||||||
lines.push(`# Timeline around anchor: ${anchorId}`);
|
lines.push(`# Timeline around anchor: ${anchorId}`);
|
||||||
lines.push(`**Window:** ${depth_before} records before → ${depth_after} records after | **Items:** ${items.length} (${timeline.observations.length} obs, ${timeline.sessions.length} sessions, ${timeline.prompts.length} prompts)`);
|
lines.push(`**Window:** ${depth_before} records before → ${depth_after} records after | **Items:** ${filteredItems.length}`);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
// Legend
|
// Legend
|
||||||
@@ -2103,7 +2131,7 @@ const tools = [
|
|||||||
|
|
||||||
// Group by day
|
// Group by day
|
||||||
const dayMap = new Map<string, TimelineItem[]>();
|
const dayMap = new Map<string, TimelineItem[]>();
|
||||||
for (const item of items) {
|
for (const item of filteredItems) {
|
||||||
const day = formatDate(item.epoch);
|
const day = formatDate(item.epoch);
|
||||||
if (!dayMap.has(day)) {
|
if (!dayMap.has(day)) {
|
||||||
dayMap.set(day, []);
|
dayMap.set(day, []);
|
||||||
@@ -2338,22 +2366,16 @@ const tools = [
|
|||||||
project
|
project
|
||||||
);
|
);
|
||||||
|
|
||||||
// Combine and sort all items chronologically (same logic as get_context_timeline)
|
// Combine, sort, and filter timeline items
|
||||||
interface TimelineItem {
|
|
||||||
type: 'observation' | 'session' | 'prompt';
|
|
||||||
data: any;
|
|
||||||
epoch: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items: TimelineItem[] = [
|
const items: TimelineItem[] = [
|
||||||
...timeline.observations.map(obs => ({ type: 'observation' as const, data: obs, epoch: obs.created_at_epoch })),
|
...timeline.observations.map(obs => ({ type: 'observation' as const, data: obs, epoch: obs.created_at_epoch })),
|
||||||
...timeline.sessions.map(sess => ({ type: 'session' as const, data: sess, epoch: sess.created_at_epoch })),
|
...timeline.sessions.map(sess => ({ type: 'session' as const, data: sess, epoch: sess.created_at_epoch })),
|
||||||
...timeline.prompts.map(prompt => ({ type: 'prompt' as const, data: prompt, epoch: prompt.created_at_epoch }))
|
...timeline.prompts.map(prompt => ({ type: 'prompt' as const, data: prompt, epoch: prompt.created_at_epoch }))
|
||||||
];
|
];
|
||||||
|
|
||||||
items.sort((a, b) => a.epoch - b.epoch);
|
items.sort((a, b) => a.epoch - b.epoch);
|
||||||
|
const filteredItems = filterTimelineByDepth(items, topResult.id, 0, depth_before, depth_after);
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (filteredItems.length === 0) {
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
type: 'text' as const,
|
type: 'text' as const,
|
||||||
@@ -2403,7 +2425,7 @@ const tools = [
|
|||||||
// Header
|
// Header
|
||||||
lines.push(`# Timeline for query: "${query}"`);
|
lines.push(`# Timeline for query: "${query}"`);
|
||||||
lines.push(`**Anchor:** Observation #${topResult.id} - ${topResult.title || 'Untitled'}`);
|
lines.push(`**Anchor:** Observation #${topResult.id} - ${topResult.title || 'Untitled'}`);
|
||||||
lines.push(`**Window:** ${depth_before} records before → ${depth_after} records after | **Items:** ${items.length} (${timeline.observations.length} obs, ${timeline.sessions.length} sessions, ${timeline.prompts.length} prompts)`);
|
lines.push(`**Window:** ${depth_before} records before → ${depth_after} records after | **Items:** ${filteredItems.length}`);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
// Legend
|
// Legend
|
||||||
@@ -2412,7 +2434,7 @@ const tools = [
|
|||||||
|
|
||||||
// Group by day
|
// Group by day
|
||||||
const dayMap = new Map<string, TimelineItem[]>();
|
const dayMap = new Map<string, TimelineItem[]>();
|
||||||
for (const item of items) {
|
for (const item of filteredItems) {
|
||||||
const day = formatDate(item.epoch);
|
const day = formatDate(item.epoch);
|
||||||
if (!dayMap.has(day)) {
|
if (!dayMap.has(day)) {
|
||||||
dayMap.set(day, []);
|
dayMap.set(day, []);
|
||||||
|
|||||||
Reference in New Issue
Block a user