Replace search skill with mem-search (#91)

* feat: add mem-search skill with progressive disclosure architecture

Add comprehensive mem-search skill for accessing claude-mem's persistent
cross-session memory database. Implements progressive disclosure workflow
and token-efficient search patterns.

Features:
- 12 search operations (observations, sessions, prompts, by-type, by-concept, by-file, timelines, etc.)
- Progressive disclosure principles to minimize token usage
- Anti-patterns documentation to guide LLM behavior
- HTTP API integration for all search functionality
- Common workflows with composition examples

Structure:
- SKILL.md: Entry point with temporal trigger patterns
- principles/: Progressive disclosure + anti-patterns
- operations/: 12 search operation files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: add CHANGELOG entry for mem-search skill

Document mem-search skill addition in Unreleased section with:
- 100% effectiveness compliance metrics
- Comparison to previous search skill implementation
- Progressive disclosure architecture details
- Reference to audit report documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: add mem-search skill audit report

Add comprehensive audit report validating mem-search skill against
Anthropic's official skill-creator documentation.

Report includes:
- Effectiveness metrics comparison (search vs mem-search)
- Critical issues analysis for production readiness
- Compliance validation across 6 key dimensions
- Reference implementation guidance

Result: mem-search achieves 100% compliance vs search's 67%

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: Add comprehensive search architecture analysis document

- Document current state of dual search architectures (HTTP API and MCP)
- Analyze HTTP endpoints and MCP search server architectures
- Identify DRY violations across search implementations
- Evaluate the use of curl as the optimal approach for search
- Provide architectural recommendations for immediate and long-term improvements
- Outline action plan for cleanup, feature parity, DRY refactoring

* refactor: Remove deprecated search skill documentation and operations

* refactor: Reorganize documentation into public and context directories

Changes:
- Created docs/public/ for Mintlify documentation (.mdx files)
- Created docs/context/ for internal planning and implementation docs
- Moved all .mdx files and assets to docs/public/
- Moved all internal .md files to docs/context/
- Added CLAUDE.md to both directories explaining their purpose
- Updated docs.json paths to work with new structure

Benefits:
- Clear separation between user-facing and internal documentation
- Easier to maintain Mintlify docs in dedicated directory
- Internal context files organized separately

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Enhance session management and continuity in hooks

- Updated new-hook.ts to clarify session_id threading and idempotent session creation.
- Modified prompts.ts to require claudeSessionId for continuation prompts, ensuring session context is maintained.
- Improved SessionStore.ts documentation on createSDKSession to emphasize idempotent behavior and session connection.
- Refined SDKAgent.ts to detail continuation prompt logic and its reliance on session.claudeSessionId for unified session handling.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alex Newman <thedotmack@gmail.com>
This commit is contained in:
basher83
2025-11-11 16:15:07 -05:00
committed by GitHub
parent eafdd6a7be
commit 97d565e3cd
92 changed files with 5038 additions and 1812 deletions
+35 -3
View File
@@ -1,6 +1,36 @@
/**
* New Hook - UserPromptSubmit
* Consolidated entry point + logic
*
* DUAL PURPOSE HOOK: Handles BOTH session initialization AND continuation
* ==========================================================================
*
* CRITICAL ARCHITECTURE FACTS (NEVER FORGET):
*
* 1. SESSION ID THREADING - The Single Source of Truth
* - Claude Code assigns ONE session_id per conversation
* - ALL hooks in that conversation receive the SAME session_id
* - We ALWAYS use this session_id - NEVER generate our own
* - This is how NEW hook, SAVE hook, and SUMMARY hook stay connected
*
* 2. NO EXISTENCE CHECKS NEEDED
* - createSDKSession is idempotent (INSERT OR IGNORE)
* - Prompt #1: Creates new database row, returns new ID
* - Prompt #2+: Row exists, returns existing ID
* - We NEVER need to check "does session exist?" - just use the session_id
*
* 3. CONTINUATION LOGIC LOCATION
* - This hook does NOT contain continuation prompt logic
* - That lives in SDKAgent.ts (lines 125-127)
* - SDKAgent checks promptNumber to choose init vs continuation prompt
* - BOTH prompts receive the SAME session_id from this hook
*
* 4. UNIFIED WITH SAVE HOOK
* - SAVE hook uses: db.createSDKSession(session_id, '', '')
* - NEW hook uses: db.createSDKSession(session_id, project, prompt)
* - Both use session_id from hook context - this keeps everything connected
*
* This is KISS in action: Use the session_id we're given, trust idempotent
* database operations, and let SDKAgent handle init vs continuation logic.
*/
import path from 'path';
@@ -32,7 +62,9 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
const db = new SessionStore();
// Save session_id for indexing
// CRITICAL: Use session_id from hook as THE source of truth
// createSDKSession is idempotent - creates new or returns existing
// This is how ALL hooks stay connected to the same session
const sessionDbId = db.createSDKSession(session_id, project, prompt);
const promptNumber = db.incrementPromptCounter(sessionDbId);
@@ -50,7 +82,7 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/init`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ project, userPrompt: prompt }),
body: JSON.stringify({ project, userPrompt: prompt, promptNumber }),
signal: AbortSignal.timeout(5000)
});
+20 -2
View File
@@ -184,9 +184,27 @@ IMPORTANT! DO NOT do any work other than generate the PROGRESS SUMMARY - and re
/**
* Build prompt for continuation of existing session
*
* CRITICAL: Why claudeSessionId Parameter is Required
* ====================================================
* This function receives claudeSessionId from SDKAgent.ts, which comes from:
* - SessionManager.initializeSession (fetched from database)
* - SessionStore.createSDKSession (stored by new-hook.ts)
* - new-hook.ts receives it from Claude Code's hook context
*
* The claudeSessionId is the SAME session_id used by:
* - NEW hook (to create/fetch session)
* - SAVE hook (to store observations)
* - This continuation prompt (to maintain session context)
*
* This is how everything stays connected - ONE session_id threading through
* all hooks and prompts in the same conversation.
*
* Called when: promptNumber > 1 (see SDKAgent.ts line 150)
* First prompt: Uses buildInitPrompt instead (promptNumber === 1)
*/
export function buildContinuationPrompt(userPrompt: string, promptNumber: number): string {
return `This is the next prompt from the user for the session you're observing.
export function buildContinuationPrompt(userPrompt: string, promptNumber: number, claudeSessionId: string): string {
return `This is continuation prompt #${promptNumber} for session ${claudeSessionId} that you're observing.
CRITICAL: Record what was LEARNED/BUILT/FIXED/DEPLOYED/CONFIGURED, not what you (the observer) are doing.
+26 -3
View File
@@ -934,14 +934,37 @@ export class SessionStore {
/**
* Create a new SDK session (idempotent - returns existing session ID if already exists)
* Sets both claude_session_id and sdk_session_id to the same value
*
* CRITICAL ARCHITECTURE: Session ID Threading
* ============================================
* This function is the KEY to how claude-mem stays unified across hooks:
*
* - NEW hook calls: createSDKSession(session_id, project, prompt)
* - SAVE hook calls: createSDKSession(session_id, '', '')
* - Both use the SAME session_id from Claude Code's hook context
*
* IDEMPOTENT BEHAVIOR (INSERT OR IGNORE):
* - Prompt #1: session_id not in database → INSERT creates new row
* - Prompt #2+: session_id exists → INSERT ignored, fetch existing ID
* - Result: Same database ID returned for all prompts in conversation
*
* WHY THIS MATTERS:
* - NO "does session exist?" checks needed anywhere
* - NO risk of creating duplicate sessions
* - ALL hooks automatically connected via session_id
* - SAVE hook observations go to correct session (same session_id)
* - SDKAgent continuation prompt has correct context (same session_id)
*
* This is KISS in action: Trust the database UNIQUE constraint and
* INSERT OR IGNORE to handle both creation and lookup elegantly.
*/
createSDKSession(claudeSessionId: string, project: string, userPrompt: string): number {
const now = new Date();
const nowEpoch = now.getTime();
// Try to insert - will be ignored if session already exists
// claude_session_id and sdk_session_id are the same value
// CRITICAL: INSERT OR IGNORE makes this idempotent
// First call (prompt #1): Creates new row
// Subsequent calls (prompt #2+): Ignored, returns existing ID
const stmt = this.db.prepare(`
INSERT OR IGNORE INTO sdk_sessions
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
+73 -327
View File
@@ -19,6 +19,8 @@ import { homedir } from 'os';
import { getPackageRoot } from '../shared/paths.js';
import { getWorkerPort } from '../shared/worker-utils.js';
import { logger } from '../utils/logger.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
// Import composed services
import { DatabaseManager } from './worker/DatabaseManager.js';
@@ -32,6 +34,7 @@ export class WorkerService {
private app: express.Application;
private server: http.Server | null = null;
private startTime: number = Date.now();
private mcpClient: Client;
// Composed services
private dbManager: DatabaseManager;
@@ -55,6 +58,11 @@ export class WorkerService {
this.paginationHelper = new PaginationHelper(this.dbManager);
this.settingsManager = new SettingsManager(this.dbManager);
this.mcpClient = new Client({
name: 'worker-search-proxy',
version: '1.0.0'
}, { capabilities: {} });
this.setupMiddleware();
this.setupRoutes();
}
@@ -177,6 +185,17 @@ export class WorkerService {
// Initialize database (once, stays open)
await this.dbManager.initialize();
// Connect to MCP search server
const searchServerPath = path.join(__dirname, '..', '..', 'plugin', 'scripts', 'search-server.mjs');
const transport = new StdioClientTransport({
command: 'node',
args: [searchServerPath],
env: process.env
});
await this.mcpClient.connect(transport);
logger.success('WORKER', 'Connected to MCP search server');
// Start HTTP server
const port = getWorkerPort();
this.server = await new Promise<http.Server>((resolve, reject) => {
@@ -828,37 +847,15 @@ export class WorkerService {
* Search observations
* GET /api/search/observations?query=...&format=index&limit=20&project=...
*/
private handleSearchObservations(req: Request, res: Response): void {
private async handleSearchObservations(req: Request, res: Response): Promise<void> {
try {
const query = req.query.query as string;
const format = (req.query.format as string) || 'full';
const limit = parseInt(req.query.limit as string, 10) || 20;
const project = req.query.project as string | undefined;
if (!query) {
res.status(400).json({ error: 'Missing required parameter: query' });
return;
}
const sessionSearch = this.dbManager.getSessionSearch();
const results = sessionSearch.searchObservations(query, { limit, project });
res.json({
query,
count: results.length,
format,
results: format === 'index' ? results.map(r => ({
id: r.id,
type: r.type,
title: r.title,
subtitle: r.subtitle,
created_at_epoch: r.created_at_epoch,
project: r.project,
score: r.score
})) : results
const result = await this.mcpClient.callTool({
name: 'search_observations',
arguments: req.query
});
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Search observations failed', {}, error as Error);
logger.failure('WORKER', 'Search failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
}
}
@@ -867,35 +864,15 @@ export class WorkerService {
* Search session summaries
* GET /api/search/sessions?query=...&format=index&limit=20
*/
private handleSearchSessions(req: Request, res: Response): void {
private async handleSearchSessions(req: Request, res: Response): Promise<void> {
try {
const query = req.query.query as string;
const format = (req.query.format as string) || 'full';
const limit = parseInt(req.query.limit as string, 10) || 20;
if (!query) {
res.status(400).json({ error: 'Missing required parameter: query' });
return;
}
const sessionSearch = this.dbManager.getSessionSearch();
const results = sessionSearch.searchSessions(query, { limit });
res.json({
query,
count: results.length,
format,
results: format === 'index' ? results.map(r => ({
id: r.id,
request: r.request,
completed: r.completed,
created_at_epoch: r.created_at_epoch,
project: r.project,
score: r.score
})) : results
const result = await this.mcpClient.callTool({
name: 'search_sessions',
arguments: req.query
});
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Search sessions failed', {}, error as Error);
logger.failure('WORKER', 'Search failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
}
}
@@ -904,36 +881,15 @@ export class WorkerService {
* Search user prompts
* GET /api/search/prompts?query=...&format=index&limit=20
*/
private handleSearchPrompts(req: Request, res: Response): void {
private async handleSearchPrompts(req: Request, res: Response): Promise<void> {
try {
const query = req.query.query as string;
const format = (req.query.format as string) || 'full';
const limit = parseInt(req.query.limit as string, 10) || 20;
const project = req.query.project as string | undefined;
if (!query) {
res.status(400).json({ error: 'Missing required parameter: query' });
return;
}
const sessionSearch = this.dbManager.getSessionSearch();
const results = sessionSearch.searchUserPrompts(query, { limit, project });
res.json({
query,
count: results.length,
format,
results: format === 'index' ? results.map(r => ({
id: r.id,
claude_session_id: r.claude_session_id,
prompt_number: r.prompt_number,
prompt_text: r.prompt_text,
created_at_epoch: r.created_at_epoch,
score: r.score
})) : results
const result = await this.mcpClient.callTool({
name: 'search_user_prompts',
arguments: req.query
});
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Search prompts failed', {}, error as Error);
logger.failure('WORKER', 'Search failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
}
}
@@ -942,37 +898,15 @@ export class WorkerService {
* Search observations by concept
* GET /api/search/by-concept?concept=discovery&format=index&limit=5
*/
private handleSearchByConcept(req: Request, res: Response): void {
private async handleSearchByConcept(req: Request, res: Response): Promise<void> {
try {
const concept = req.query.concept as string;
const format = (req.query.format as string) || 'full';
const limit = parseInt(req.query.limit as string, 10) || 10;
const project = req.query.project as string | undefined;
if (!concept) {
res.status(400).json({ error: 'Missing required parameter: concept' });
return;
}
const sessionSearch = this.dbManager.getSessionSearch();
const results = sessionSearch.findByConcept(concept, { limit, project });
res.json({
concept,
count: results.length,
format,
results: format === 'index' ? results.map(r => ({
id: r.id,
type: r.type,
title: r.title,
subtitle: r.subtitle,
created_at_epoch: r.created_at_epoch,
project: r.project,
concepts: r.concepts
})) : results
const result = await this.mcpClient.callTool({
name: 'find_by_concept',
arguments: req.query
});
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Search by concept failed', {}, error as Error);
logger.failure('WORKER', 'Search failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
}
}
@@ -981,45 +915,15 @@ export class WorkerService {
* Search by file path
* GET /api/search/by-file?filePath=...&format=index&limit=10
*/
private handleSearchByFile(req: Request, res: Response): void {
private async handleSearchByFile(req: Request, res: Response): Promise<void> {
try {
const filePath = req.query.filePath as string;
const format = (req.query.format as string) || 'full';
const limit = parseInt(req.query.limit as string, 10) || 10;
const project = req.query.project as string | undefined;
if (!filePath) {
res.status(400).json({ error: 'Missing required parameter: filePath' });
return;
}
const sessionSearch = this.dbManager.getSessionSearch();
const results = sessionSearch.findByFile(filePath, { limit, project });
res.json({
filePath,
count: results.observations.length + results.sessions.length,
format,
results: {
observations: format === 'index' ? results.observations.map(r => ({
id: r.id,
type: r.type,
title: r.title,
subtitle: r.subtitle,
created_at_epoch: r.created_at_epoch,
project: r.project
})) : results.observations,
sessions: format === 'index' ? results.sessions.map(r => ({
id: r.id,
request: r.request,
completed: r.completed,
created_at_epoch: r.created_at_epoch,
project: r.project
})) : results.sessions
}
const result = await this.mcpClient.callTool({
name: 'find_by_file',
arguments: req.query
});
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Search by file failed', {}, error as Error);
logger.failure('WORKER', 'Search failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
}
}
@@ -1028,36 +932,15 @@ export class WorkerService {
* Search observations by type
* GET /api/search/by-type?type=bugfix&format=index&limit=10
*/
private handleSearchByType(req: Request, res: Response): void {
private async handleSearchByType(req: Request, res: Response): Promise<void> {
try {
const type = req.query.type as string;
const format = (req.query.format as string) || 'full';
const limit = parseInt(req.query.limit as string, 10) || 10;
const project = req.query.project as string | undefined;
if (!type) {
res.status(400).json({ error: 'Missing required parameter: type' });
return;
}
const sessionSearch = this.dbManager.getSessionSearch();
const results = sessionSearch.findByType(type as any, { limit, project });
res.json({
type,
count: results.length,
format,
results: format === 'index' ? results.map(r => ({
id: r.id,
type: r.type,
title: r.title,
subtitle: r.subtitle,
created_at_epoch: r.created_at_epoch,
project: r.project
})) : results
const result = await this.mcpClient.callTool({
name: 'find_by_type',
arguments: req.query
});
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Search by type failed', {}, error as Error);
logger.failure('WORKER', 'Search failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
}
}
@@ -1066,49 +949,15 @@ export class WorkerService {
* Get recent context (summaries and observations for a project)
* GET /api/context/recent?project=...&limit=3
*/
private handleGetRecentContext(req: Request, res: Response): void {
private async handleGetRecentContext(req: Request, res: Response): Promise<void> {
try {
const project = (req.query.project as string) || path.basename(process.cwd());
const limit = parseInt(req.query.limit as string, 10) || 3;
const sessionStore = this.dbManager.getSessionStore();
const sessions = sessionStore.getRecentSessionsWithStatus(project, limit);
const contextData = sessions.map(session => {
const summary = session.has_summary && session.sdk_session_id
? sessionStore.getSummaryForSession(session.sdk_session_id)
: null;
const observations = session.sdk_session_id
? sessionStore.getObservationsForSession(session.sdk_session_id)
: [];
return {
session_id: session.id,
sdk_session_id: session.sdk_session_id,
project: session.project,
status: session.status,
has_summary: session.has_summary,
summary,
observations: observations.map(o => ({
id: o.id,
type: o.type,
title: o.title,
subtitle: o.subtitle,
created_at_epoch: o.created_at_epoch
})),
created_at_epoch: session.started_at_epoch
};
});
res.json({
project,
limit,
count: contextData.length,
sessions: contextData
const result = await this.mcpClient.callTool({
name: 'get_recent_context',
arguments: req.query
});
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Get recent context failed', {}, error as Error);
logger.failure('WORKER', 'Search failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
}
}
@@ -1117,59 +966,15 @@ export class WorkerService {
* Get context timeline around an anchor point
* GET /api/context/timeline?anchor=123&depth_before=10&depth_after=10&project=...
*/
private handleGetContextTimeline(req: Request, res: Response): void {
private async handleGetContextTimeline(req: Request, res: Response): Promise<void> {
try {
const anchor = req.query.anchor as string;
const depthBefore = parseInt(req.query.depth_before as string, 10) || 10;
const depthAfter = parseInt(req.query.depth_after as string, 10) || 10;
const project = req.query.project as string | undefined;
if (!anchor) {
res.status(400).json({ error: 'Missing required parameter: anchor' });
return;
}
const sessionStore = this.dbManager.getSessionStore();
let timeline;
// Check if anchor is a number (observation ID)
if (/^\d+$/.test(anchor)) {
const obsId = parseInt(anchor, 10);
const obs = sessionStore.getObservationById(obsId);
if (!obs) {
res.status(404).json({ error: `Observation #${obsId} not found` });
return;
}
timeline = sessionStore.getTimelineAroundObservation(obsId, obs.created_at_epoch, depthBefore, depthAfter, project);
} else if (anchor.startsWith('S') || anchor.startsWith('#S')) {
// Session ID
const sessionId = anchor.replace(/^#?S/, '');
const sessionNum = parseInt(sessionId, 10);
const sessions = sessionStore.getSessionSummariesByIds([sessionNum]);
if (sessions.length === 0) {
res.status(404).json({ error: `Session #${sessionNum} not found` });
return;
}
timeline = sessionStore.getTimelineAroundTimestamp(sessions[0].created_at_epoch, depthBefore, depthAfter, project);
} else {
// ISO timestamp
const date = new Date(anchor);
if (isNaN(date.getTime())) {
res.status(400).json({ error: `Invalid timestamp: ${anchor}` });
return;
}
timeline = sessionStore.getTimelineAroundTimestamp(date.getTime(), depthBefore, depthAfter, project);
}
res.json({
anchor,
depth_before: depthBefore,
depth_after: depthAfter,
project,
timeline
const result = await this.mcpClient.callTool({
name: 'get_context_timeline',
arguments: req.query
});
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Get context timeline failed', {}, error as Error);
logger.failure('WORKER', 'Search failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
}
}
@@ -1178,74 +983,15 @@ export class WorkerService {
* Get timeline by query (search first, then get timeline around best match)
* GET /api/timeline/by-query?query=...&mode=auto&depth_before=10&depth_after=10
*/
private handleGetTimelineByQuery(req: Request, res: Response): void {
private async handleGetTimelineByQuery(req: Request, res: Response): Promise<void> {
try {
const query = req.query.query as string;
const mode = (req.query.mode as string) || 'auto';
const depthBefore = parseInt(req.query.depth_before as string, 10) || 10;
const depthAfter = parseInt(req.query.depth_after as string, 10) || 10;
const project = req.query.project as string | undefined;
if (!query) {
res.status(400).json({ error: 'Missing required parameter: query' });
return;
}
const sessionSearch = this.dbManager.getSessionSearch();
const sessionStore = this.dbManager.getSessionStore();
// Search based on mode
let bestMatch: any = null;
let searchResults: any = null;
if (mode === 'observations' || mode === 'auto') {
const obsResults = sessionSearch.searchObservations(query, { limit: 1, project });
if (obsResults.length > 0) {
bestMatch = obsResults[0];
searchResults = { type: 'observation', results: obsResults };
}
}
if (!bestMatch && (mode === 'sessions' || mode === 'auto')) {
const sessionResults = sessionSearch.searchSessions(query, { limit: 1 });
if (sessionResults.length > 0) {
bestMatch = sessionResults[0];
searchResults = { type: 'session', results: sessionResults };
}
}
if (!bestMatch) {
res.json({
query,
mode,
match: null,
timeline: null,
message: 'No matches found for query'
});
return;
}
// Get timeline around best match
const timeline = searchResults.type === 'observation'
? sessionStore.getTimelineAroundObservation(bestMatch.id, bestMatch.created_at_epoch, depthBefore, depthAfter, project)
: sessionStore.getTimelineAroundTimestamp(bestMatch.created_at_epoch, depthBefore, depthAfter, project);
res.json({
query,
mode,
match: {
type: searchResults.type,
id: bestMatch.id,
title: bestMatch.title || bestMatch.request,
score: bestMatch.score,
created_at_epoch: bestMatch.created_at_epoch
},
depth_before: depthBefore,
depth_after: depthAfter,
timeline
const result = await this.mcpClient.callTool({
name: 'get_timeline_by_query',
arguments: req.query
});
res.json(result.content);
} catch (error) {
logger.failure('WORKER', 'Get timeline by query failed', {}, error as Error);
logger.failure('WORKER', 'Search failed', {}, error as Error);
res.status(500).json({ error: (error as Error).message });
}
}
+24 -1
View File
@@ -115,16 +115,39 @@ export class SDKAgent {
/**
* Create event-driven message generator (yields messages from SessionManager)
*
* CRITICAL: CONTINUATION PROMPT LOGIC
* ====================================
* This is where NEW hook's dual-purpose nature comes together:
*
* - Prompt #1 (lastPromptNumber === 1): buildInitPrompt
* - Full initialization prompt with instructions
* - Sets up the SDK agent's context
*
* - Prompt #2+ (lastPromptNumber > 1): buildContinuationPrompt
* - Continuation prompt for same session
* - Includes session context and prompt number
*
* BOTH prompts receive session.claudeSessionId:
* - This comes from the hook's session_id (see new-hook.ts)
* - Same session_id used by SAVE hook to store observations
* - This is how everything stays connected in one unified session
*
* NO SESSION EXISTENCE CHECKS NEEDED:
* - SessionManager.initializeSession already fetched this from database
* - Database row was created by new-hook's createSDKSession call
* - We just use the session_id we're given - simple and reliable
*/
private async *createMessageGenerator(session: ActiveSession): AsyncIterableIterator<SDKUserMessage> {
// Yield initial user prompt with context (or continuation if prompt #2+)
// CRITICAL: Both paths use session.claudeSessionId from the hook
yield {
type: 'user',
message: {
role: 'user',
content: session.lastPromptNumber === 1
? buildInitPrompt(session.project, session.claudeSessionId, session.userPrompt)
: buildContinuationPrompt(session.userPrompt, session.lastPromptNumber)
: buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.claudeSessionId)
},
session_id: session.claudeSessionId,
parent_tool_use_id: null,