feat: inject file observation timeline on PreToolUse Read hook
When Claude reads a file, the PreToolUse hook queries for existing observations about that file and injects the timeline into context via additionalContext + permissionDecision: allow. This prevents duplicate observations and saves tokens through active rediscovery. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { Database } from 'bun:sqlite';
|
||||
import path from 'path';
|
||||
import { logger } from '../../../utils/logger.js';
|
||||
import type { ObservationRecord } from '../../../types/database.js';
|
||||
import type { GetObservationsByIdsOptions, ObservationSessionRow } from './types.js';
|
||||
@@ -111,3 +112,42 @@ export function getObservationsForSession(
|
||||
|
||||
return stmt.all(memorySessionId) as ObservationSessionRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get observations associated with a given file path.
|
||||
* Searches both files_read and files_modified using basename matching
|
||||
* to handle differing absolute paths across sessions.
|
||||
*/
|
||||
export function getObservationsByFilePath(
|
||||
db: Database,
|
||||
filePath: string,
|
||||
options?: { project?: string; limit?: number }
|
||||
): ObservationRecord[] {
|
||||
const basename = path.basename(filePath);
|
||||
const likePattern = `%${basename}%`;
|
||||
const limit = options?.limit ?? 30;
|
||||
|
||||
const additionalConditions: string[] = [];
|
||||
const params: any[] = [likePattern, likePattern];
|
||||
|
||||
if (options?.project) {
|
||||
additionalConditions.push('AND project = ?');
|
||||
params.push(options.project);
|
||||
}
|
||||
|
||||
const additionalWhere = additionalConditions.join(' ');
|
||||
|
||||
const stmt = db.prepare(`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE (
|
||||
EXISTS (SELECT 1 FROM json_each(files_read) WHERE value LIKE ?)
|
||||
OR EXISTS (SELECT 1 FROM json_each(files_modified) WHERE value LIKE ?)
|
||||
)
|
||||
${additionalWhere}
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ${limit}
|
||||
`);
|
||||
|
||||
return stmt.all(...params) as ObservationRecord[];
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { SessionManager } from '../../SessionManager.js';
|
||||
import { SSEBroadcaster } from '../../SSEBroadcaster.js';
|
||||
import type { WorkerService } from '../../../worker-service.js';
|
||||
import { BaseRouteHandler } from '../BaseRouteHandler.js';
|
||||
import { getObservationsByFilePath } from '../../../sqlite/observations/get.js';
|
||||
|
||||
export class DataRoutes extends BaseRouteHandler {
|
||||
constructor(
|
||||
@@ -39,6 +40,7 @@ export class DataRoutes extends BaseRouteHandler {
|
||||
|
||||
// Fetch by ID endpoints
|
||||
app.get('/api/observation/:id', this.handleGetObservationById.bind(this));
|
||||
app.get('/api/observations/by-file', this.handleGetObservationsByFile.bind(this));
|
||||
app.post('/api/observations/batch', this.handleGetObservationsByIds.bind(this));
|
||||
app.get('/api/session/:id', this.handleGetSessionById.bind(this));
|
||||
app.post('/api/sdk-sessions/batch', this.handleGetSdkSessionsByIds.bind(this));
|
||||
@@ -108,6 +110,26 @@ export class DataRoutes extends BaseRouteHandler {
|
||||
res.json(observation);
|
||||
});
|
||||
|
||||
/**
|
||||
* Get observations associated with a file path
|
||||
* GET /api/observations/by-file?path=<file_path>&project=<project>&limit=30
|
||||
*/
|
||||
private handleGetObservationsByFile = this.wrapHandler((req: Request, res: Response): void => {
|
||||
const filePath = req.query.path as string | undefined;
|
||||
if (!filePath) {
|
||||
this.badRequest(res, 'path query parameter is required');
|
||||
return;
|
||||
}
|
||||
|
||||
const project = req.query.project as string | undefined;
|
||||
const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : undefined;
|
||||
|
||||
const db = this.dbManager.getSessionStore().db;
|
||||
const observations = getObservationsByFilePath(db, filePath, { project, limit });
|
||||
|
||||
res.json({ observations, count: observations.length });
|
||||
});
|
||||
|
||||
/**
|
||||
* Get observations by array of IDs
|
||||
* POST /api/observations/batch
|
||||
|
||||
Reference in New Issue
Block a user