Add session-aware task context and auto-linking (CF-166 Phase 2)

Enhancements:

1. Session Context Table
   - Migration 014: session_context table
   - Tracks current working task per session
   - Auto-updated on task status changes

2. Auto-Linking to Current Working Task
   - When task status → in_progress: sets as current_task_id
   - When task status → completed: clears current_task_id
   - New tasks auto-link to current working task with 'relates_to'
   - Shows auto-link message during task creation

3. Session ID Resolution
   - Fixed path: ~/.cache/session-memory/current_session
   - Falls back to environment variable CLAUDE_SESSION_ID
   - Generates timestamp-based ID if unavailable

Example Flow:
```
1. task update CF-123 --status in_progress
   → Sets CF-123 as current working task

2. task add --title "Related work"
   → Created: CF-124
   🔗 Auto-linked to: CF-123 (current working task)

3. task update CF-123 --status completed
   → Clears current working task
```

Files Changed:
- migrations/014_session_context.sql (new, 33 lines)
- src/tools/crud.ts (auto-linking logic, session context management)

Benefits:
- Zero manual linking for related work
- Session context preserved automatically
- Investigation workflows auto-organized
- Retroactive work tracking prevented

Note: Requires MCP server restart to take effect.

Remaining CF-166 Features (Phase 3):
- task investigate command
- Investigation workflow helper
- Task creation reminders

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-01-17 08:44:19 +02:00
parent f7f9cfe3d8
commit 90bb7e94f0
2 changed files with 79 additions and 3 deletions

View File

@@ -0,0 +1,28 @@
-- Migration 014: Session Context for Auto-linking
-- Tracks current working task per session for automatic task relationship creation
CREATE TABLE IF NOT EXISTS session_context (
session_id TEXT PRIMARY KEY,
current_task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_session_context_task ON session_context(current_task_id);
-- Auto-update timestamp on changes
CREATE OR REPLACE FUNCTION update_session_context_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER session_context_updated
BEFORE UPDATE ON session_context
FOR EACH ROW
EXECUTE FUNCTION update_session_context_timestamp();
-- Record migration
INSERT INTO schema_migrations (version) VALUES ('014_session_context')
ON CONFLICT (version) DO NOTHING;

View File

@@ -5,6 +5,7 @@ import { getEmbedding, formatEmbedding } from '../embeddings.js';
import type { Task, ChecklistItem, TaskLink } from '../types.js'; import type { Task, ChecklistItem, TaskLink } from '../types.js';
import { getRecentDelegations } from './delegations.js'; import { getRecentDelegations } from './delegations.js';
import { getTaskCommits } from './commits.js'; import { getTaskCommits } from './commits.js';
import { taskLink } from './relations.js';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as os from 'os'; import * as os from 'os';
@@ -18,8 +19,8 @@ function getSessionId(): string {
return process.env.CLAUDE_SESSION_ID; return process.env.CLAUDE_SESSION_ID;
} }
// Try to read from cache file (usage-stats format) // Try to read from cache file (session-memory format)
const cacheFile = path.join(os.homedir(), '.claude', 'cache', '.current_session_id'); const cacheFile = path.join(os.homedir(), '.cache', 'session-memory', 'current_session');
try { try {
const sessionId = fs.readFileSync(cacheFile, 'utf-8').trim(); const sessionId = fs.readFileSync(cacheFile, 'utf-8').trim();
if (sessionId) return sessionId; if (sessionId) return sessionId;
@@ -138,7 +139,29 @@ export async function taskAdd(args: TaskAddArgs): Promise<string> {
// Record activity for session tracking // Record activity for session tracking
await recordActivity(taskId, 'created', undefined, 'open'); await recordActivity(taskId, 'created', undefined, 'open');
return `Created: ${taskId}\n Title: ${title}\n Type: ${type}\n Priority: ${priority}\n Project: ${projectKey}${embedding ? '\n (embedded for semantic search)' : ''}${duplicateWarning}`; // Check for session context and auto-link to current working task
let autoLinkMessage = '';
const session_id = getSessionId();
try {
const sessionContext = await queryOne<{ current_task_id: string }>(
`SELECT current_task_id FROM session_context WHERE session_id = $1`,
[session_id]
);
if (sessionContext?.current_task_id) {
// Auto-link to current working task
await taskLink({
from_id: taskId,
to_id: sessionContext.current_task_id,
link_type: 'relates_to'
});
autoLinkMessage = `\n\n🔗 Auto-linked to: ${sessionContext.current_task_id} (current working task)`;
}
} catch {
// Silently fail if session context unavailable
}
return `Created: ${taskId}\n Title: ${title}\n Type: ${type}\n Priority: ${priority}\n Project: ${projectKey}${embedding ? '\n (embedded for semantic search)' : ''}${duplicateWarning}${autoLinkMessage}`;
} }
/** /**
@@ -404,5 +427,30 @@ export async function taskUpdate(args: TaskUpdateArgs): Promise<string> {
await recordActivity(id, 'updated'); await recordActivity(id, 'updated');
} }
// Manage session context based on status changes
if (status) {
const session_id = getSessionId();
try {
if (status === 'in_progress') {
// Set as current working task
await execute(
`INSERT INTO session_context (session_id, current_task_id)
VALUES ($1, $2)
ON CONFLICT (session_id) DO UPDATE SET current_task_id = $2, updated_at = NOW()`,
[session_id, id]
);
} else if (status === 'completed') {
// Clear if this is the current working task
await execute(
`DELETE FROM session_context
WHERE session_id = $1 AND current_task_id = $2`,
[session_id, id]
);
}
} catch {
// Silently fail if session context unavailable
}
}
return `Updated: ${id}`; return `Updated: ${id}`;
} }