feat: Enhance session management with prompt tracking
- Added prompt_number to observations and session summaries for better tracking. - Implemented prompt counter in SDK sessions to manage user prompts effectively. - Updated database schema to include prompt tracking columns and removed unique constraints on session summaries. - Modified hooks to utilize prompt_number in observations and summaries. - Changed worker service to handle summarize requests instead of finalize, keeping the SDK agent active. - Improved logging for better debugging and tracking of prompt numbers across sessions.
This commit is contained in:
Vendored
+58
-85
File diff suppressed because one or more lines are too long
+2
-2
@@ -39,8 +39,8 @@
|
||||
"publish:npm": "node scripts/publish.js",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "node --test tests/",
|
||||
"test:context": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | node claude-mem/scripts/context-hook.js 2>/dev/null",
|
||||
"test:context:verbose": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | node claude-mem/scripts/context-hook.js",
|
||||
"test:context": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | node plugin/scripts/context-hook.js 2>/dev/null",
|
||||
"test:context:verbose": "echo '{\"session_id\":\"test-'$(date +%s)'\",\"cwd\":\"'$(pwd)'\",\"source\":\"startup\"}' | node plugin/scripts/context-hook.js",
|
||||
"worker:start": "pm2 start ecosystem.config.cjs",
|
||||
"worker:stop": "pm2 stop claude-mem-worker",
|
||||
"worker:restart": "pm2 restart claude-mem-worker",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
import h from"better-sqlite3";import{join as i,dirname as T,basename as w}from"path";import{homedir as u}from"os";import{existsSync as x,mkdirSync as S}from"fs";var a=process.env.CLAUDE_MEM_DATA_DIR||i(u(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||i(u(),".claude"),I=i(a,"archives"),O=i(a,"logs"),L=i(a,"trash"),y=i(a,"backups"),C=i(a,"settings.json"),m=i(a,"claude-mem.db"),P=i(l,"settings.json"),H=i(l,"commands"),N=i(l,"CLAUDE.md");function _(o){S(o,{recursive:!0})}var c=class{db;constructor(){_(a),this.db=new h(m),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(t=>t.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
import f from"better-sqlite3";import{join as i,dirname as I,basename as R}from"path";import{homedir as _}from"os";import{existsSync as L,mkdirSync as D}from"fs";var a=process.env.CLAUDE_MEM_DATA_DIR||i(_(),".claude-mem"),d=process.env.CLAUDE_CONFIG_DIR||i(_(),".claude"),C=i(a,"archives"),P=i(a,"logs"),y=i(a,"trash"),O=i(a,"backups"),H=i(a,"settings.json"),g=i(a,"claude-mem.db"),N=i(d,"settings.json"),U=i(d,"commands"),M=i(d,"CLAUDE.md");function E(o){D(o,{recursive:!0})}var m=class{db;constructor(){E(a),this.db=new f(g),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(t=>t.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{this.db.pragma("table_info(sdk_sessions)").some(c=>c.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[HooksDatabase] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(c=>c.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to session_summaries table")),this.db.pragma("index_list(session_summaries)").some(c=>c.unique===1)&&(console.error("[HooksDatabase] WARNING: session_summaries.sdk_session_id has UNIQUE constraint. Cannot be removed in SQLite without recreating table."),console.error("[HooksDatabase] Multiple summaries per session will fail until table is recreated."))}catch(e){console.error("[HooksDatabase] Prompt tracking migration error:",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,s)}getRecentObservations(e,s=20){return this.db.prepare(`
|
||||
SELECT type, text, created_at
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
@@ -27,15 +27,23 @@ import h from"better-sqlite3";import{join as i,dirname as T,basename as w}from"p
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(s,e)}createSDKSession(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(`
|
||||
`).run(s,e)}incrementPromptCounter(e){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
|
||||
WHERE id = ?
|
||||
`).run(e),this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,s,t){let r=new Date,n=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,s,t,r.toISOString(),n).lastInsertRowid}updateSDKSessionId(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`).run(s,e)}setWorkerPort(e,s){this.db.prepare(`
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(s,e).changes===0&&console.error(`[HooksDatabase] Skipped updating sdk_session_id for session ${e} - already set (prevents FOREIGN KEY constraint violation)`)}setWorkerPort(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
@@ -44,16 +52,16 @@ import h from"better-sqlite3";import{join as i,dirname as T,basename as w}from"p
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}storeObservation(e,s,t,r){let n=new Date,d=n.getTime();this.db.prepare(`
|
||||
`).get(e)?.worker_port||null}storeObservation(e,s,t,r,n){let u=new Date,p=u.getTime();this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,r,t,n.toISOString(),d)}storeSummary(e,s,t){let r=new Date,n=r.getTime();this.db.prepare(`
|
||||
(sdk_session_id, project, text, type, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,r,t,n||null,u.toISOString(),p)}storeSummary(e,s,t,r){let n=new Date,u=n.getTime();this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request||null,t.investigated||null,t.learned||null,t.completed||null,t.next_steps||null,t.files_read||null,t.files_edited||null,t.notes||null,r.toISOString(),n)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
next_steps, files_read, files_edited, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,s,t.request||null,t.investigated||null,t.learned||null,t.completed||null,t.next_steps||null,t.files_read||null,t.files_edited||null,t.notes||null,r||null,n.toISOString(),u)}markSessionCompleted(e){let s=new Date,t=s.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
@@ -65,5 +73,5 @@ import h from"better-sqlite3";import{join as i,dirname as T,basename as w}from"p
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};async function g(o){try{console.error("[claude-mem cleanup] Hook fired",{input:o?{session_id:o.session_id,cwd:o.cwd,reason:o.reason}:null}),o||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s});let t=new c,r=t.findActiveSDKSession(e);if(r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),t.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),r.worker_port)try{let n=await fetch(`http://127.0.0.1:${r.worker_port}/sessions/${r.id}`,{method:"DELETE",signal:AbortSignal.timeout(5e3)});n.ok?console.error("[claude-mem cleanup] Session deleted successfully via HTTP"):console.error("[claude-mem cleanup] Failed to delete session:",await n.text())}catch(n){console.error("[claude-mem cleanup] HTTP DELETE error:",n.message)}else console.error("[claude-mem cleanup] No worker port, cannot send DELETE request");try{t.markSessionFailed(r.id),console.error("[claude-mem cleanup] Session marked as failed in database")}catch(n){console.error("[claude-mem cleanup] Failed to mark session as failed:",n)}t.close(),console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}catch(e){console.error("[claude-mem cleanup] Unexpected error in hook",{error:e.message,stack:e.stack,name:e.name}),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}import{stdin as E}from"process";var p="";E.on("data",o=>p+=o);E.on("end",async()=>{try{let o=p.trim()?JSON.parse(p):void 0;await g(o)}catch(o){console.error(`[claude-mem cleanup-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
`).run(e.toISOString(),s).changes}close(){this.db.close()}};async function b(o){try{console.error("[claude-mem cleanup] Hook fired",{input:o?{session_id:o.session_id,cwd:o.cwd,reason:o.reason}:null}),o||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=o;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s});let t=new m,r=t.findActiveSDKSession(e);if(r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),t.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),r.worker_port)try{let n=await fetch(`http://127.0.0.1:${r.worker_port}/sessions/${r.id}`,{method:"DELETE",signal:AbortSignal.timeout(5e3)});n.ok?console.error("[claude-mem cleanup] Session deleted successfully via HTTP"):console.error("[claude-mem cleanup] Failed to delete session:",await n.text())}catch(n){console.error("[claude-mem cleanup] HTTP DELETE error:",n.message)}else console.error("[claude-mem cleanup] No worker port, cannot send DELETE request");try{t.markSessionFailed(r.id),console.error("[claude-mem cleanup] Session marked as failed in database")}catch(n){console.error("[claude-mem cleanup] Failed to mark session as failed:",n)}t.close(),console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}catch(e){console.error("[claude-mem cleanup] Unexpected error in hook",{error:e.message,stack:e.stack,name:e.name}),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}}import{stdin as k}from"process";var l="";k.on("data",o=>l+=o);k.on("end",async()=>{try{let o=l.trim()?JSON.parse(l):void 0;await b(o)}catch(o){console.error(`[claude-mem cleanup-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
import R from"path";import D from"better-sqlite3";import{join as c,dirname as w,basename as v}from"path";import{homedir as f}from"os";import{existsSync as C,mkdirSync as k}from"fs";var l=process.env.CLAUDE_MEM_DATA_DIR||c(f(),".claude-mem"),g=process.env.CLAUDE_CONFIG_DIR||c(f(),".claude"),L=c(l,"archives"),P=c(l,"logs"),N=c(l,"trash"),j=c(l,"backups"),H=c(l,"settings.json"),E=c(l,"claude-mem.db"),U=c(g,"settings.json"),W=c(g,"commands"),$=c(g,"CLAUDE.md");function S(p){k(p,{recursive:!0})}var u=class{db;constructor(){S(l),this.db=new D(E),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
|
||||
import R from"path";import T from"better-sqlite3";import{join as c,dirname as x,basename as y}from"path";import{homedir as E}from"os";import{existsSync as O,mkdirSync as D}from"fs";var u=process.env.CLAUDE_MEM_DATA_DIR||c(E(),".claude-mem"),g=process.env.CLAUDE_CONFIG_DIR||c(E(),".claude"),P=c(u,"archives"),N=c(u,"logs"),H=c(u,"trash"),U=c(u,"backups"),M=c(u,"settings.json"),f=c(u,"claude-mem.db"),W=c(g,"settings.json"),$=c(g,"commands"),j=c(g,"CLAUDE.md");function k(m){D(m,{recursive:!0})}var l=class{db;constructor(){k(u),this.db=new T(f),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(r=>r.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{this.db.pragma("table_info(sdk_sessions)").some(p=>p.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[HooksDatabase] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to session_summaries table")),this.db.pragma("index_list(session_summaries)").some(p=>p.unique===1)&&(console.error("[HooksDatabase] WARNING: session_summaries.sdk_session_id has UNIQUE constraint. Cannot be removed in SQLite without recreating table."),console.error("[HooksDatabase] Multiple summaries per session will fail until table is recreated."))}catch(e){console.error("[HooksDatabase] Prompt tracking migration error:",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}getRecentObservations(e,t=20){return this.db.prepare(`
|
||||
SELECT type, text, created_at
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
@@ -27,15 +27,23 @@ import R from"path";import D from"better-sqlite3";import{join as c,dirname as w,
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(t,e)}createSDKSession(e,t,s){let o=new Date,i=o.getTime();return this.db.prepare(`
|
||||
`).run(t,e)}incrementPromptCounter(e){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
|
||||
WHERE id = ?
|
||||
`).run(e),this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,t,r){let i=new Date,a=i.getTime();return this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,t,s,o.toISOString(),i).lastInsertRowid}updateSDKSessionId(e,t){this.db.prepare(`
|
||||
`).run(e,t,r,i.toISOString(),a).lastInsertRowid}updateSDKSessionId(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`).run(t,e)}setWorkerPort(e,t){this.db.prepare(`
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(t,e).changes===0&&console.error(`[HooksDatabase] Skipped updating sdk_session_id for session ${e} - already set (prevents FOREIGN KEY constraint violation)`)}setWorkerPort(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
@@ -44,29 +52,29 @@ import R from"path";import D from"better-sqlite3";import{join as c,dirname as w,
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}storeObservation(e,t,s,o){let i=new Date,r=i.getTime();this.db.prepare(`
|
||||
`).get(e)?.worker_port||null}storeObservation(e,t,r,i,a){let s=new Date,d=s.getTime();this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,o,s,i.toISOString(),r)}storeSummary(e,t,s){let o=new Date,i=o.getTime();this.db.prepare(`
|
||||
(sdk_session_id, project, text, type, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,i,r,a||null,s.toISOString(),d)}storeSummary(e,t,r,i){let a=new Date,s=a.getTime();this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,s.request||null,s.investigated||null,s.learned||null,s.completed||null,s.next_steps||null,s.files_read||null,s.files_edited||null,s.notes||null,o.toISOString(),i)}markSessionCompleted(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
next_steps, files_read, files_edited, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,r.request||null,r.investigated||null,r.learned||null,r.completed||null,r.next_steps||null,r.files_read||null,r.files_edited||null,r.notes||null,i||null,a.toISOString(),s)}markSessionCompleted(e){let t=new Date,r=t.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(t.toISOString(),s,e)}markSessionFailed(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
`).run(t.toISOString(),r,e)}markSessionFailed(e){let t=new Date,r=t.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`).run(t.toISOString(),s,e)}cleanupOrphanedSessions(){let e=new Date,t=e.getTime();return this.db.prepare(`
|
||||
`).run(t.toISOString(),r,e)}cleanupOrphanedSessions(){let e=new Date,t=e.getTime();return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function _(p){let e=p?.cwd??process.cwd(),t=e?R.basename(e):"unknown-project",s=new u;try{let o=s.getRecentSummaries(t,5),i=s.getRecentObservations(t,20);if(o.length===0&&i.length===0){console.log(`# Recent Session Context
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function b(m){let e=m?.cwd??process.cwd(),t=e?R.basename(e):"unknown-project",r=new l;try{let i=r.getRecentSummaries(t,5),a=r.getRecentObservations(t,20);if(i.length===0&&a.length===0){console.log(`# Recent Session Context
|
||||
|
||||
No previous sessions found for this project yet.`);return}let r=[];if(r.push("# Recent Session Context"),r.push(""),i.length>0){r.push(`## Recent Observations (${i.length})`),r.push("");let n={};for(let a of i)n[a.type]||(n[a.type]=[]),n[a.type].push({text:a.text,created_at:a.created_at});let d=["feature","bugfix","refactor","discovery","decision"];for(let a of d)if(n[a]&&n[a].length>0){r.push(`### ${a.charAt(0).toUpperCase()+a.slice(1)}s`);for(let b of n[a])r.push(`- ${b.text}`);r.push("")}}if(o.length===0){console.log(r.join(`
|
||||
`));return}r.push("## Recent Sessions"),r.push("");let m=o.length===1?"session":"sessions";r.push(`Showing last ${o.length} ${m} for **${t}**:`),r.push("");for(let n of o){if(r.push("---"),r.push(""),n.request&&r.push(`**Request:** ${n.request}`),n.completed&&r.push(`**Completed:** ${n.completed}`),n.learned&&r.push(`**Learned:** ${n.learned}`),n.next_steps&&r.push(`**Next Steps:** ${n.next_steps}`),n.files_read)try{let d=JSON.parse(n.files_read);Array.isArray(d)&&d.length>0&&r.push(`**Files Read:** ${d.join(", ")}`)}catch{n.files_read.trim()&&r.push(`**Files Read:** ${n.files_read}`)}if(n.files_edited)try{let d=JSON.parse(n.files_edited);Array.isArray(d)&&d.length>0&&r.push(`**Files Edited:** ${d.join(", ")}`)}catch{n.files_edited.trim()&&r.push(`**Files Edited:** ${n.files_edited}`)}r.push(`**Date:** ${n.created_at.split("T")[0]}`),r.push("")}console.log(r.join(`
|
||||
`))}finally{s.close()}}import{stdin as h}from"process";try{if(h.isTTY)_();else{let p="";h.on("data",e=>p+=e),h.on("end",()=>{let e=p.trim()?JSON.parse(p):void 0;_(e)})}}catch(p){console.error(`[claude-mem context-hook error: ${p.message}]`),process.exit(0)}
|
||||
No previous sessions found for this project yet.`);return}let s=[];if(s.push("# Recent Session Context"),s.push(""),a.length>0){s.push(`## Recent Observations (${a.length})`),s.push("");let n={};for(let o of a)n[o.type]||(n[o.type]=[]),n[o.type].push({text:o.text,prompt_number:o.prompt_number,created_at:o.created_at});let p=["feature","bugfix","refactor","discovery","decision"];for(let o of p)if(n[o]&&n[o].length>0){s.push(`### ${o.charAt(0).toUpperCase()+o.slice(1)}s`);for(let _ of n[o]){let S=_.prompt_number?` (prompt #${_.prompt_number})`:"";s.push(`- ${_.text}${S}`)}s.push("")}}if(i.length===0){console.log(s.join(`
|
||||
`));return}s.push("## Recent Sessions"),s.push("");let d=i.length===1?"session":"sessions";s.push(`Showing last ${i.length} ${d} for **${t}**:`),s.push("");for(let n of i){s.push("---"),s.push("");let p=n.prompt_number?` (Prompt #${n.prompt_number})`:"";if(s.push(`**Summary${p}**`),s.push(""),n.request&&s.push(`**Request:** ${n.request}`),n.completed&&s.push(`**Completed:** ${n.completed}`),n.learned&&s.push(`**Learned:** ${n.learned}`),n.next_steps&&s.push(`**Next Steps:** ${n.next_steps}`),n.files_read)try{let o=JSON.parse(n.files_read);Array.isArray(o)&&o.length>0&&s.push(`**Files Read:** ${o.join(", ")}`)}catch{n.files_read.trim()&&s.push(`**Files Read:** ${n.files_read}`)}if(n.files_edited)try{let o=JSON.parse(n.files_edited);Array.isArray(o)&&o.length>0&&s.push(`**Files Edited:** ${o.join(", ")}`)}catch{n.files_edited.trim()&&s.push(`**Files Edited:** ${n.files_edited}`)}s.push(`**Date:** ${n.created_at.split("T")[0]}`),s.push("")}console.log(s.join(`
|
||||
`))}finally{r.close()}}import{stdin as h}from"process";try{if(h.isTTY)b();else{let m="";h.on("data",e=>m+=e),h.on("end",()=>{let e=m.trim()?JSON.parse(m):void 0;b(e)})}}catch(m){console.error(`[claude-mem context-hook error: ${m.message}]`),process.exit(0)}
|
||||
|
||||
+23
-15
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
import T from"path";import D from"better-sqlite3";import{join as a,dirname as v,basename as P}from"path";import{homedir as k}from"os";import{existsSync as U,mkdirSync as w}from"fs";var p=process.env.CLAUDE_MEM_DATA_DIR||a(k(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||a(k(),".claude"),L=a(p,"archives"),N=a(p,"logs"),W=a(p,"trash"),j=a(p,"backups"),M=a(p,"settings.json"),f=a(p,"claude-mem.db"),F=a(l,"settings.json"),$=a(l,"commands"),q=a(l,"CLAUDE.md");function E(r){w(r,{recursive:!0})}var d=class{db;constructor(){E(p),this.db=new D(f),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
|
||||
import A from"path";import w from"better-sqlite3";import{join as p,dirname as x,basename as C}from"path";import{homedir as k}from"os";import{existsSync as N,mkdirSync as R}from"fs";var c=process.env.CLAUDE_MEM_DATA_DIR||p(k(),".claude-mem"),b=process.env.CLAUDE_CONFIG_DIR||p(k(),".claude"),U=p(c,"archives"),W=p(c,"logs"),M=p(c,"trash"),j=p(c,"backups"),F=p(c,"settings.json"),S=p(c,"claude-mem.db"),$=p(b,"settings.json"),q=p(b,"commands"),G=p(b,"CLAUDE.md");function f(r){R(r,{recursive:!0})}var l=class{db;constructor(){f(c),this.db=new w(S),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{this.db.pragma("table_info(sdk_sessions)").some(u=>u.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[HooksDatabase] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(u=>u.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(u=>u.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to session_summaries table")),this.db.pragma("index_list(session_summaries)").some(u=>u.unique===1)&&(console.error("[HooksDatabase] WARNING: session_summaries.sdk_session_id has UNIQUE constraint. Cannot be removed in SQLite without recreating table."),console.error("[HooksDatabase] Multiple summaries per session will fail until table is recreated."))}catch(e){console.error("[HooksDatabase] Prompt tracking migration error:",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}getRecentObservations(e,t=20){return this.db.prepare(`
|
||||
SELECT type, text, created_at
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
@@ -27,15 +27,23 @@ import T from"path";import D from"better-sqlite3";import{join as a,dirname as v,
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(t,e)}createSDKSession(e,t,s){let o=new Date,n=o.getTime();return this.db.prepare(`
|
||||
`).run(t,e)}incrementPromptCounter(e){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
|
||||
WHERE id = ?
|
||||
`).run(e),this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,t,s){let n=new Date,o=n.getTime();return this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,t,s,o.toISOString(),n).lastInsertRowid}updateSDKSessionId(e,t){this.db.prepare(`
|
||||
`).run(e,t,s,n.toISOString(),o).lastInsertRowid}updateSDKSessionId(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`).run(t,e)}setWorkerPort(e,t){this.db.prepare(`
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(t,e).changes===0&&console.error(`[HooksDatabase] Skipped updating sdk_session_id for session ${e} - already set (prevents FOREIGN KEY constraint violation)`)}setWorkerPort(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
@@ -44,16 +52,16 @@ import T from"path";import D from"better-sqlite3";import{join as a,dirname as v,
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}storeObservation(e,t,s,o){let n=new Date,i=n.getTime();this.db.prepare(`
|
||||
`).get(e)?.worker_port||null}storeObservation(e,t,s,n,o){let a=new Date,i=a.getTime();this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,o,s,n.toISOString(),i)}storeSummary(e,t,s){let o=new Date,n=o.getTime();this.db.prepare(`
|
||||
(sdk_session_id, project, text, type, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,n,s,o||null,a.toISOString(),i)}storeSummary(e,t,s,n){let o=new Date,a=o.getTime();this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,s.request||null,s.investigated||null,s.learned||null,s.completed||null,s.next_steps||null,s.files_read||null,s.files_edited||null,s.notes||null,o.toISOString(),n)}markSessionCompleted(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
next_steps, files_read, files_edited, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,s.request||null,s.investigated||null,s.learned||null,s.completed||null,s.next_steps||null,s.files_read||null,s.files_edited||null,s.notes||null,n||null,o.toISOString(),a)}markSessionCompleted(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
@@ -65,4 +73,4 @@ import T from"path";import D from"better-sqlite3";import{join as a,dirname as v,
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function R(r,e,t){return r==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:t.reason||"Pre-compact operation failed",suppressOutput:!0}:r==="SessionStart"?e&&t.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t.context}}:{continue:!0,suppressOutput:!0}:r==="UserPromptSubmit"||r==="PostToolUse"?{continue:!0,suppressOutput:!0}:r==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...t.reason&&!e?{stopReason:t.reason}:{}}}function u(r,e,t={}){let s=R(r,e,t);return JSON.stringify(s)}async function A(){let{readFileSync:r,existsSync:e}=await import("fs"),{join:t}=await import("path"),{homedir:s}=await import("os"),o=t(s(),".claude-mem","worker.port");if(!e(o))return null;try{let n=r(o,"utf8").trim();return parseInt(n,10)}catch{return null}}async function b(r){if(!r)throw new Error("newHook requires input");let{session_id:e,cwd:t,prompt:s}=r,o=T.basename(t),n=new d;try{let i=n.findActiveSDKSession(e),c;if(i){c=i.id,console.log(u("UserPromptSubmit",!0));return}let g=n.findAnySDKSession(e);g?(c=g.id,n.reactivateSession(c,s),console.error(`[new-hook] Reactivated session ${c} for Claude session ${e}`)):(c=n.createSDKSession(e,o,s),console.error(`[new-hook] Created new session ${c} for Claude session ${e}`));let _=await A();if(!_){console.error("[new-hook] Worker service not running. Start with: npm run worker:start"),console.log(u("UserPromptSubmit",!0));return}let S=await fetch(`http://127.0.0.1:${_}/sessions/${c}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:o,userPrompt:s}),signal:AbortSignal.timeout(5e3)});S.ok||console.error("[new-hook] Failed to init session:",await S.text()),console.log(u("UserPromptSubmit",!0))}catch(i){console.error("[new-hook] FATAL ERROR:",i.message),console.error("[new-hook] Stack:",i.stack),console.error("[new-hook] Full error:",JSON.stringify(i,Object.getOwnPropertyNames(i))),console.log(u("UserPromptSubmit",!0))}finally{n.close()}}import{stdin as h}from"process";var m="";h.on("data",r=>m+=r);h.on("end",async()=>{try{let r=m.trim()?JSON.parse(m):void 0;await b(r)}catch(r){console.error(`[claude-mem new-hook error: ${r.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function T(r,e,t){return r==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:t.reason||"Pre-compact operation failed",suppressOutput:!0}:r==="SessionStart"?e&&t.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t.context}}:{continue:!0,suppressOutput:!0}:r==="UserPromptSubmit"||r==="PostToolUse"?{continue:!0,suppressOutput:!0}:r==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...t.reason&&!e?{stopReason:t.reason}:{}}}function _(r,e,t={}){let s=T(r,e,t);return JSON.stringify(s)}async function I(){let{readFileSync:r,existsSync:e}=await import("fs"),{join:t}=await import("path"),{homedir:s}=await import("os"),n=t(s(),".claude-mem","worker.port");if(!e(n))return null;try{let o=r(n,"utf8").trim();return parseInt(o,10)}catch{return null}}async function h(r){if(!r)throw new Error("newHook requires input");let{session_id:e,cwd:t,prompt:s}=r,n=A.basename(t),o=new l;try{let a=o.findActiveSDKSession(e),i,d=!1;if(a){i=a.id;let m=o.incrementPromptCounter(i);console.error(`[new-hook] Continuing session ${i}, prompt #${m}`)}else{let m=o.findAnySDKSession(e);if(m){i=m.id,o.reactivateSession(i,s);let g=o.incrementPromptCounter(i);d=!0,console.error(`[new-hook] Reactivated session ${i}, prompt #${g}`)}else{i=o.createSDKSession(e,n,s);let g=o.incrementPromptCounter(i);d=!0,console.error(`[new-hook] Created new session ${i}, prompt #${g}`)}}let u=await I();if(!u){console.error("[new-hook] Worker service not running. Start with: npm run worker:start"),console.log(_("UserPromptSubmit",!0));return}if(d){let m=await fetch(`http://127.0.0.1:${u}/sessions/${i}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:n,userPrompt:s}),signal:AbortSignal.timeout(5e3)});m.ok||console.error("[new-hook] Failed to init session:",await m.text())}console.log(_("UserPromptSubmit",!0))}catch(a){console.error("[new-hook] FATAL ERROR:",a.message),console.error("[new-hook] Stack:",a.stack),console.error("[new-hook] Full error:",JSON.stringify(a,Object.getOwnPropertyNames(a))),console.log(_("UserPromptSubmit",!0))}finally{o.close()}}import{stdin as D}from"process";var E="";D.on("data",r=>E+=r);D.on("end",async()=>{try{let r=E.trim()?JSON.parse(E):void 0;await h(r)}catch(r){console.error(`[claude-mem new-hook error: ${r.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
|
||||
+23
-15
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
import b from"better-sqlite3";import{join as n,dirname as w,basename as O}from"path";import{homedir as g}from"os";import{existsSync as P,mkdirSync as f}from"fs";var c=process.env.CLAUDE_MEM_DATA_DIR||n(g(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||n(g(),".claude"),y=n(c,"archives"),H=n(c,"logs"),C=n(c,"trash"),L=n(c,"backups"),U=n(c,"settings.json"),_=n(c,"claude-mem.db"),N=n(l,"settings.json"),W=n(l,"commands"),M=n(l,"CLAUDE.md");function S(o){f(o,{recursive:!0})}var d=class{db;constructor(){S(c),this.db=new b(_),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
|
||||
import T from"better-sqlite3";import{join as a,dirname as O,basename as P}from"path";import{homedir as b}from"os";import{existsSync as H,mkdirSync as h}from"fs";var p=process.env.CLAUDE_MEM_DATA_DIR||a(b(),".claude-mem"),_=process.env.CLAUDE_CONFIG_DIR||a(b(),".claude"),L=a(p,"archives"),y=a(p,"logs"),N=a(p,"trash"),U=a(p,"backups"),M=a(p,"settings.json"),E=a(p,"claude-mem.db"),W=a(_,"settings.json"),j=a(_,"commands"),F=a(_,"CLAUDE.md");function k(o){h(o,{recursive:!0})}var l=class{db;constructor(){k(p),this.db=new T(E),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{this.db.pragma("table_info(sdk_sessions)").some(u=>u.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[HooksDatabase] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(u=>u.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(u=>u.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to session_summaries table")),this.db.pragma("index_list(session_summaries)").some(u=>u.unique===1)&&(console.error("[HooksDatabase] WARNING: session_summaries.sdk_session_id has UNIQUE constraint. Cannot be removed in SQLite without recreating table."),console.error("[HooksDatabase] Multiple summaries per session will fail until table is recreated."))}catch(e){console.error("[HooksDatabase] Prompt tracking migration error:",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}getRecentObservations(e,t=20){return this.db.prepare(`
|
||||
SELECT type, text, created_at
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
@@ -27,15 +27,23 @@ import b from"better-sqlite3";import{join as n,dirname as w,basename as O}from"p
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(t,e)}createSDKSession(e,t,s){let r=new Date,i=r.getTime();return this.db.prepare(`
|
||||
`).run(t,e)}incrementPromptCounter(e){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
|
||||
WHERE id = ?
|
||||
`).run(e),this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,t,s){let r=new Date,n=r.getTime();return this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,t,s,r.toISOString(),i).lastInsertRowid}updateSDKSessionId(e,t){this.db.prepare(`
|
||||
`).run(e,t,s,r.toISOString(),n).lastInsertRowid}updateSDKSessionId(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`).run(t,e)}setWorkerPort(e,t){this.db.prepare(`
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(t,e).changes===0&&console.error(`[HooksDatabase] Skipped updating sdk_session_id for session ${e} - already set (prevents FOREIGN KEY constraint violation)`)}setWorkerPort(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
@@ -44,16 +52,16 @@ import b from"better-sqlite3";import{join as n,dirname as w,basename as O}from"p
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}storeObservation(e,t,s,r){let i=new Date,a=i.getTime();this.db.prepare(`
|
||||
`).get(e)?.worker_port||null}storeObservation(e,t,s,r,n){let i=new Date,c=i.getTime();this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,r,s,i.toISOString(),a)}storeSummary(e,t,s){let r=new Date,i=r.getTime();this.db.prepare(`
|
||||
(sdk_session_id, project, text, type, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,r,s,n||null,i.toISOString(),c)}storeSummary(e,t,s,r){let n=new Date,i=n.getTime();this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,s.request||null,s.investigated||null,s.learned||null,s.completed||null,s.next_steps||null,s.files_read||null,s.files_edited||null,s.notes||null,r.toISOString(),i)}markSessionCompleted(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
next_steps, files_read, files_edited, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,s.request||null,s.investigated||null,s.learned||null,s.completed||null,s.next_steps||null,s.files_read||null,s.files_edited||null,s.notes||null,r||null,n.toISOString(),i)}markSessionCompleted(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
@@ -65,4 +73,4 @@ import b from"better-sqlite3";import{join as n,dirname as w,basename as O}from"p
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function h(o,e,t){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:t.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&t.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...t.reason&&!e?{stopReason:t.reason}:{}}}function u(o,e,t={}){let s=h(o,e,t);return JSON.stringify(s)}var T=new Set(["TodoWrite","ListMcpResourcesTool"]);async function E(o){if(!o)throw new Error("saveHook requires input");let{session_id:e,tool_name:t,tool_input:s,tool_output:r}=o;if(T.has(t)){console.log(u("PostToolUse",!0));return}let i=new d,a=i.findActiveSDKSession(e);if(i.close(),!a){console.log(u("PostToolUse",!0));return}if(!a.worker_port){console.error("[save-hook] No worker port for session",a.id),console.log(u("PostToolUse",!0));return}try{let p=await fetch(`http://127.0.0.1:${a.worker_port}/sessions/${a.id}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:t,tool_input:JSON.stringify(s),tool_output:JSON.stringify(r)}),signal:AbortSignal.timeout(2e3)});p.ok||console.error("[save-hook] Failed to send observation:",await p.text())}catch(p){console.error("[save-hook] Error:",p.message)}finally{console.log(u("PostToolUse",!0))}}import{stdin as k}from"process";var m="";k.on("data",o=>m+=o);k.on("end",async()=>{try{let o=m.trim()?JSON.parse(m):void 0;await E(o)}catch(o){console.error(`[claude-mem save-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function D(o,e,t){return o==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:t.reason||"Pre-compact operation failed",suppressOutput:!0}:o==="SessionStart"?e&&t.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t.context}}:{continue:!0,suppressOutput:!0}:o==="UserPromptSubmit"||o==="PostToolUse"?{continue:!0,suppressOutput:!0}:o==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...t.reason&&!e?{stopReason:t.reason}:{}}}function d(o,e,t={}){let s=D(o,e,t);return JSON.stringify(s)}var R=new Set(["TodoWrite","ListMcpResourcesTool"]);async function f(o){if(!o)throw new Error("saveHook requires input");let{session_id:e,tool_name:t,tool_input:s,tool_output:r}=o;if(R.has(t)){console.log(d("PostToolUse",!0));return}let n=new l,i=n.findActiveSDKSession(e);if(!i){n.close(),console.log(d("PostToolUse",!0));return}if(!i.worker_port){n.close(),console.error("[save-hook] No worker port for session",i.id),console.log(d("PostToolUse",!0));return}let c=n.getPromptCounter(i.id);n.close();try{let m=await fetch(`http://127.0.0.1:${i.worker_port}/sessions/${i.id}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:t,tool_input:JSON.stringify(s),tool_output:JSON.stringify(r),prompt_number:c}),signal:AbortSignal.timeout(2e3)});m.ok||console.error("[save-hook] Failed to send observation:",await m.text())}catch(m){console.error("[save-hook] Error:",m.message)}finally{console.log(d("PostToolUse",!0))}}import{stdin as S}from"process";var g="";S.on("data",o=>g+=o);S.on("end",async()=>{try{let o=g.trim()?JSON.parse(g):void 0;await f(o)}catch(o){console.error(`[claude-mem save-hook error: ${o.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
import b from"better-sqlite3";import{join as n,dirname as w,basename as A}from"path";import{homedir as m}from"os";import{existsSync as v,mkdirSync as f}from"fs";var i=process.env.CLAUDE_MEM_DATA_DIR||n(m(),".claude-mem"),d=process.env.CLAUDE_CONFIG_DIR||n(m(),".claude"),P=n(i,"archives"),y=n(i,"logs"),H=n(i,"trash"),C=n(i,"backups"),L=n(i,"settings.json"),g=n(i,"claude-mem.db"),U=n(d,"settings.json"),N=n(d,"commands"),W=n(d,"CLAUDE.md");function _(r){f(r,{recursive:!0})}var c=class{db;constructor(){_(i),this.db=new b(g),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
|
||||
import D from"better-sqlite3";import{join as i,dirname as v,basename as w}from"path";import{homedir as g}from"os";import{existsSync as C,mkdirSync as h}from"fs";var a=process.env.CLAUDE_MEM_DATA_DIR||i(g(),".claude-mem"),l=process.env.CLAUDE_CONFIG_DIR||i(g(),".claude"),P=i(a,"archives"),H=i(a,"logs"),L=i(a,"trash"),N=i(a,"backups"),U=i(a,"settings.json"),b=i(a,"claude-mem.db"),M=i(l,"settings.json"),W=i(l,"commands"),j=i(l,"CLAUDE.md");function E(r){h(r,{recursive:!0})}var m=class{db;constructor(){E(a),this.db=new D(b),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns()}ensureWorkerPortColumn(){try{this.db.pragma("table_info(sdk_sessions)").some(s=>s.name==="worker_port")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER"),console.error("[HooksDatabase] Added worker_port column to sdk_sessions table"))}catch(e){console.error("[HooksDatabase] Migration error:",e.message)}}ensurePromptTrackingColumns(){try{this.db.pragma("table_info(sdk_sessions)").some(p=>p.name==="prompt_counter")||(this.db.exec("ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0"),console.error("[HooksDatabase] Added prompt_counter column to sdk_sessions table")),this.db.pragma("table_info(observations)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE observations ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to observations table")),this.db.pragma("table_info(session_summaries)").some(p=>p.name==="prompt_number")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER"),console.error("[HooksDatabase] Added prompt_number column to session_summaries table")),this.db.pragma("index_list(session_summaries)").some(p=>p.unique===1)&&(console.error("[HooksDatabase] WARNING: session_summaries.sdk_session_id has UNIQUE constraint. Cannot be removed in SQLite without recreating table."),console.error("[HooksDatabase] Multiple summaries per session will fail until table is recreated."))}catch(e){console.error("[HooksDatabase] Prompt tracking migration error:",e.message)}}getRecentSummaries(e,t=10){return this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`).all(e,t)}getRecentObservations(e,t=20){return this.db.prepare(`
|
||||
SELECT type, text, created_at
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
@@ -27,15 +27,23 @@ import b from"better-sqlite3";import{join as n,dirname as w,basename as A}from"p
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'active', user_prompt = ?, worker_port = NULL
|
||||
WHERE id = ?
|
||||
`).run(t,e)}createSDKSession(e,t,s){let o=new Date,a=o.getTime();return this.db.prepare(`
|
||||
`).run(t,e)}incrementPromptCounter(e){return this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
|
||||
WHERE id = ?
|
||||
`).run(e),this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||1}getPromptCounter(e){return this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(e)?.prompt_counter||0}createSDKSession(e,t,s){let o=new Date,n=o.getTime();return this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(e,t,s,o.toISOString(),a).lastInsertRowid}updateSDKSessionId(e,t){this.db.prepare(`
|
||||
`).run(e,t,s,o.toISOString(),n).lastInsertRowid}updateSDKSessionId(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`).run(t,e)}setWorkerPort(e,t){this.db.prepare(`
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(t,e).changes===0&&console.error(`[HooksDatabase] Skipped updating sdk_session_id for session ${e} - already set (prevents FOREIGN KEY constraint violation)`)}setWorkerPort(e,t){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
@@ -44,16 +52,16 @@ import b from"better-sqlite3";import{join as n,dirname as w,basename as A}from"p
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`).get(e)?.worker_port||null}storeObservation(e,t,s,o){let a=new Date,u=a.getTime();this.db.prepare(`
|
||||
`).get(e)?.worker_port||null}storeObservation(e,t,s,o,n){let u=new Date,c=u.getTime();this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,o,s,a.toISOString(),u)}storeSummary(e,t,s){let o=new Date,a=o.getTime();this.db.prepare(`
|
||||
(sdk_session_id, project, text, type, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,o,s,n||null,u.toISOString(),c)}storeSummary(e,t,s,o){let n=new Date,u=n.getTime();this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,s.request||null,s.investigated||null,s.learned||null,s.completed||null,s.next_steps||null,s.files_read||null,s.files_edited||null,s.notes||null,o.toISOString(),a)}markSessionCompleted(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
next_steps, files_read, files_edited, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(e,t,s.request||null,s.investigated||null,s.learned||null,s.completed||null,s.next_steps||null,s.files_read||null,s.files_edited||null,s.notes||null,o||null,n.toISOString(),u)}markSessionCompleted(e){let t=new Date,s=t.getTime();this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
@@ -65,4 +73,4 @@ import b from"better-sqlite3";import{join as n,dirname as w,basename as A}from"p
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE status = 'active'
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function h(r,e,t){return r==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:t.reason||"Pre-compact operation failed",suppressOutput:!0}:r==="SessionStart"?e&&t.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t.context}}:{continue:!0,suppressOutput:!0}:r==="UserPromptSubmit"||r==="PostToolUse"?{continue:!0,suppressOutput:!0}:r==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...t.reason&&!e?{stopReason:t.reason}:{}}}function p(r,e,t={}){let s=h(r,e,t);return JSON.stringify(s)}async function S(r){if(!r)throw new Error("summaryHook requires input");let{session_id:e}=r,t=new c,s=t.findActiveSDKSession(e);if(t.close(),!s){console.log(p("Stop",!0));return}if(!s.worker_port){console.error("[summary-hook] No worker port for session",s.id),console.log(p("Stop",!0));return}try{let o=await fetch(`http://127.0.0.1:${s.worker_port}/sessions/${s.id}/finalize`,{method:"POST",headers:{"Content-Type":"application/json"},signal:AbortSignal.timeout(2e3)});o.ok||console.error("[summary-hook] Failed to finalize:",await o.text())}catch(o){console.error("[summary-hook] Error:",o.message)}finally{console.log(p("Stop",!0))}}import{stdin as E}from"process";var l="";E.on("data",r=>l+=r);E.on("end",async()=>{try{let r=l.trim()?JSON.parse(l):void 0;await S(r)}catch(r){console.error(`[claude-mem summary-hook error: ${r.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
`).run(e.toISOString(),t).changes}close(){this.db.close()}};function T(r,e,t){return r==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:t.reason||"Pre-compact operation failed",suppressOutput:!0}:r==="SessionStart"?e&&t.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t.context}}:{continue:!0,suppressOutput:!0}:r==="UserPromptSubmit"||r==="PostToolUse"?{continue:!0,suppressOutput:!0}:r==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...t.reason&&!e?{stopReason:t.reason}:{}}}function d(r,e,t={}){let s=T(r,e,t);return JSON.stringify(s)}async function k(r){if(!r)throw new Error("summaryHook requires input");let{session_id:e}=r,t=new m,s=t.findActiveSDKSession(e);if(!s){t.close(),console.log(d("Stop",!0));return}if(!s.worker_port){t.close(),console.error("[summary-hook] No worker port for session",s.id),console.log(d("Stop",!0));return}let o=t.getPromptCounter(s.id);t.close();try{let n=await fetch(`http://127.0.0.1:${s.worker_port}/sessions/${s.id}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:o}),signal:AbortSignal.timeout(2e3)});n.ok||console.error("[summary-hook] Failed to generate summary:",await n.text())}catch(n){console.error("[summary-hook] Error:",n.message)}finally{console.log(d("Stop",!0))}}import{stdin as S}from"process";var _="";S.on("data",r=>_+=r);S.on("end",async()=>{try{let r=_.trim()?JSON.parse(_):void 0;await k(r)}catch(r){console.error(`[claude-mem summary-hook error: ${r.message}]`),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}});
|
||||
|
||||
@@ -42,10 +42,10 @@ export function contextHook(input?: SessionStartInput): void {
|
||||
output.push('');
|
||||
|
||||
// Group observations by type
|
||||
const byType: Record<string, Array<{text: string; created_at: string}>> = {};
|
||||
const byType: Record<string, Array<{text: string; prompt_number: number | null; created_at: string}>> = {};
|
||||
for (const obs of observations) {
|
||||
if (!byType[obs.type]) byType[obs.type] = [];
|
||||
byType[obs.type].push({ text: obs.text, created_at: obs.created_at });
|
||||
byType[obs.type].push({ text: obs.text, prompt_number: obs.prompt_number, created_at: obs.created_at });
|
||||
}
|
||||
|
||||
// Display each type
|
||||
@@ -54,7 +54,8 @@ export function contextHook(input?: SessionStartInput): void {
|
||||
if (byType[type] && byType[type].length > 0) {
|
||||
output.push(`### ${type.charAt(0).toUpperCase() + type.slice(1)}s`);
|
||||
for (const obs of byType[type]) {
|
||||
output.push(`- ${obs.text}`);
|
||||
const promptLabel = obs.prompt_number ? ` (prompt #${obs.prompt_number})` : '';
|
||||
output.push(`- ${obs.text}${promptLabel}`);
|
||||
}
|
||||
output.push('');
|
||||
}
|
||||
@@ -76,6 +77,10 @@ export function contextHook(input?: SessionStartInput): void {
|
||||
output.push('---');
|
||||
output.push('');
|
||||
|
||||
const promptLabel = summary.prompt_number ? ` (Prompt #${summary.prompt_number})` : '';
|
||||
output.push(`**Summary${promptLabel}**`);
|
||||
output.push('');
|
||||
|
||||
if (summary.request) {
|
||||
output.push(`**Request:** ${summary.request}`);
|
||||
}
|
||||
|
||||
+33
-25
@@ -48,26 +48,31 @@ export async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
// Check for any existing session (active, failed, or completed)
|
||||
let existing = db.findActiveSDKSession(session_id);
|
||||
let sessionDbId: number;
|
||||
let isNewSession = false;
|
||||
|
||||
if (existing) {
|
||||
// Session already active, just continue
|
||||
// Session already active, increment prompt counter
|
||||
sessionDbId = existing.id;
|
||||
console.log(createHookResponse('UserPromptSubmit', true));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for inactive sessions we can reuse
|
||||
const inactive = db.findAnySDKSession(session_id);
|
||||
|
||||
if (inactive) {
|
||||
// Reactivate the existing session
|
||||
sessionDbId = inactive.id;
|
||||
db.reactivateSession(sessionDbId, prompt);
|
||||
console.error(`[new-hook] Reactivated session ${sessionDbId} for Claude session ${session_id}`);
|
||||
const promptNumber = db.incrementPromptCounter(sessionDbId);
|
||||
console.error(`[new-hook] Continuing session ${sessionDbId}, prompt #${promptNumber}`);
|
||||
} else {
|
||||
// Create new session
|
||||
sessionDbId = db.createSDKSession(session_id, project, prompt);
|
||||
console.error(`[new-hook] Created new session ${sessionDbId} for Claude session ${session_id}`);
|
||||
// Check for inactive sessions we can reuse
|
||||
const inactive = db.findAnySDKSession(session_id);
|
||||
|
||||
if (inactive) {
|
||||
// Reactivate the existing session
|
||||
sessionDbId = inactive.id;
|
||||
db.reactivateSession(sessionDbId, prompt);
|
||||
const promptNumber = db.incrementPromptCounter(sessionDbId);
|
||||
isNewSession = true;
|
||||
console.error(`[new-hook] Reactivated session ${sessionDbId}, prompt #${promptNumber}`);
|
||||
} else {
|
||||
// Create new session
|
||||
sessionDbId = db.createSDKSession(session_id, project, prompt);
|
||||
const promptNumber = db.incrementPromptCounter(sessionDbId);
|
||||
isNewSession = true;
|
||||
console.error(`[new-hook] Created new session ${sessionDbId}, prompt #${promptNumber}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Find worker service port
|
||||
@@ -78,16 +83,19 @@ export async function newHook(input?: UserPromptSubmitInput): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize session via HTTP
|
||||
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 }),
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
// Only initialize worker on new sessions
|
||||
if (isNewSession) {
|
||||
// Initialize session via HTTP
|
||||
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 }),
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[new-hook] Failed to init session:', await response.text());
|
||||
if (!response.ok) {
|
||||
console.error('[new-hook] Failed to init session:', await response.text());
|
||||
}
|
||||
}
|
||||
|
||||
console.log(createHookResponse('UserPromptSubmit', true));
|
||||
|
||||
+8
-2
@@ -34,19 +34,24 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
|
||||
const db = new HooksDatabase();
|
||||
const session = db.findActiveSDKSession(session_id);
|
||||
db.close();
|
||||
|
||||
if (!session) {
|
||||
db.close();
|
||||
console.log(createHookResponse('PostToolUse', true));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.worker_port) {
|
||||
db.close();
|
||||
console.error('[save-hook] No worker port for session', session.id);
|
||||
console.log(createHookResponse('PostToolUse', true));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current prompt number for this session
|
||||
const promptNumber = db.getPromptCounter(session.id);
|
||||
db.close();
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}/observations`, {
|
||||
method: 'POST',
|
||||
@@ -54,7 +59,8 @@ export async function saveHook(input?: PostToolUseInput): Promise<void> {
|
||||
body: JSON.stringify({
|
||||
tool_name,
|
||||
tool_input: JSON.stringify(tool_input),
|
||||
tool_output: JSON.stringify(tool_output)
|
||||
tool_output: JSON.stringify(tool_output),
|
||||
prompt_number: promptNumber
|
||||
}),
|
||||
signal: AbortSignal.timeout(2000)
|
||||
});
|
||||
|
||||
+10
-4
@@ -9,7 +9,7 @@ export interface StopInput {
|
||||
|
||||
/**
|
||||
* Summary Hook - Stop
|
||||
* Sends FINALIZE message to worker via HTTP POST
|
||||
* Sends SUMMARIZE message to worker via HTTP POST (not finalize - keeps SDK agent running)
|
||||
*/
|
||||
export async function summaryHook(input?: StopInput): Promise<void> {
|
||||
if (!input) {
|
||||
@@ -19,28 +19,34 @@ export async function summaryHook(input?: StopInput): Promise<void> {
|
||||
const { session_id } = input;
|
||||
const db = new HooksDatabase();
|
||||
const session = db.findActiveSDKSession(session_id);
|
||||
db.close();
|
||||
|
||||
if (!session) {
|
||||
db.close();
|
||||
console.log(createHookResponse('Stop', true));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.worker_port) {
|
||||
db.close();
|
||||
console.error('[summary-hook] No worker port for session', session.id);
|
||||
console.log(createHookResponse('Stop', true));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current prompt number
|
||||
const promptNumber = db.getPromptCounter(session.id);
|
||||
db.close();
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}/finalize`, {
|
||||
const response = await fetch(`http://127.0.0.1:${session.worker_port}/sessions/${session.id}/summarize`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prompt_number: promptNumber }),
|
||||
signal: AbortSignal.timeout(2000)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[summary-hook] Failed to finalize:', await response.text());
|
||||
console.error('[summary-hook] Failed to generate summary:', await response.text());
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[summary-hook] Error:', error.message);
|
||||
|
||||
@@ -18,8 +18,10 @@ export class HooksDatabase {
|
||||
this.db.pragma('synchronous = NORMAL');
|
||||
this.db.pragma('foreign_keys = ON');
|
||||
|
||||
// Run migration to add worker_port column if it doesn't exist
|
||||
// Run migrations
|
||||
this.ensureWorkerPortColumn();
|
||||
this.ensurePromptTrackingColumns();
|
||||
this.removeSessionSummariesUniqueConstraint();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,6 +42,125 @@ export class HooksDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure prompt tracking columns exist (migration 006)
|
||||
*/
|
||||
private ensurePromptTrackingColumns(): void {
|
||||
try {
|
||||
// Check sdk_sessions for prompt_counter
|
||||
const sessionsInfo = this.db.pragma('table_info(sdk_sessions)');
|
||||
const hasPromptCounter = (sessionsInfo as any[]).some((col: any) => col.name === 'prompt_counter');
|
||||
|
||||
if (!hasPromptCounter) {
|
||||
this.db.exec('ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0');
|
||||
console.error('[HooksDatabase] Added prompt_counter column to sdk_sessions table');
|
||||
}
|
||||
|
||||
// Check observations for prompt_number
|
||||
const observationsInfo = this.db.pragma('table_info(observations)');
|
||||
const obsHasPromptNumber = (observationsInfo as any[]).some((col: any) => col.name === 'prompt_number');
|
||||
|
||||
if (!obsHasPromptNumber) {
|
||||
this.db.exec('ALTER TABLE observations ADD COLUMN prompt_number INTEGER');
|
||||
console.error('[HooksDatabase] Added prompt_number column to observations table');
|
||||
}
|
||||
|
||||
// Check session_summaries for prompt_number
|
||||
const summariesInfo = this.db.pragma('table_info(session_summaries)');
|
||||
const sumHasPromptNumber = (summariesInfo as any[]).some((col: any) => col.name === 'prompt_number');
|
||||
|
||||
if (!sumHasPromptNumber) {
|
||||
this.db.exec('ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER');
|
||||
console.error('[HooksDatabase] Added prompt_number column to session_summaries table');
|
||||
}
|
||||
|
||||
// Remove UNIQUE constraint on session_summaries.sdk_session_id
|
||||
// SQLite doesn't support dropping constraints, so we need to check if it exists first
|
||||
const summariesIndexes = this.db.pragma('index_list(session_summaries)');
|
||||
const hasUniqueConstraint = (summariesIndexes as any[]).some((idx: any) => idx.unique === 1);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[HooksDatabase] Prompt tracking migration error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove UNIQUE constraint from session_summaries.sdk_session_id (migration 007)
|
||||
*/
|
||||
private removeSessionSummariesUniqueConstraint(): void {
|
||||
try {
|
||||
// Check if UNIQUE constraint exists
|
||||
const summariesIndexes = this.db.pragma('index_list(session_summaries)');
|
||||
const hasUniqueConstraint = (summariesIndexes as any[]).some((idx: any) => idx.unique === 1);
|
||||
|
||||
if (!hasUniqueConstraint) {
|
||||
// Already migrated
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('[HooksDatabase] Removing UNIQUE constraint from session_summaries.sdk_session_id...');
|
||||
|
||||
// Begin transaction
|
||||
this.db.exec('BEGIN TRANSACTION');
|
||||
|
||||
try {
|
||||
// Create new table without UNIQUE constraint
|
||||
this.db.exec(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
learned TEXT,
|
||||
completed TEXT,
|
||||
next_steps TEXT,
|
||||
files_read TEXT,
|
||||
files_edited TEXT,
|
||||
notes TEXT,
|
||||
prompt_number INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Copy data from old table
|
||||
this.db.exec(`
|
||||
INSERT INTO session_summaries_new
|
||||
SELECT id, sdk_session_id, project, request, investigated, learned,
|
||||
completed, next_steps, files_read, files_edited, notes,
|
||||
prompt_number, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
`);
|
||||
|
||||
// Drop old table
|
||||
this.db.exec('DROP TABLE session_summaries');
|
||||
|
||||
// Rename new table
|
||||
this.db.exec('ALTER TABLE session_summaries_new RENAME TO session_summaries');
|
||||
|
||||
// Recreate indexes
|
||||
this.db.exec(`
|
||||
CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`);
|
||||
|
||||
// Commit transaction
|
||||
this.db.exec('COMMIT');
|
||||
|
||||
console.error('[HooksDatabase] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id');
|
||||
} catch (error: any) {
|
||||
// Rollback on error
|
||||
this.db.exec('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[HooksDatabase] Migration error (remove UNIQUE constraint):', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent session summaries for a project
|
||||
*/
|
||||
@@ -52,12 +173,13 @@ export class HooksDatabase {
|
||||
files_read: string | null;
|
||||
files_edited: string | null;
|
||||
notes: string | null;
|
||||
prompt_number: number | null;
|
||||
created_at: string;
|
||||
}> {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
files_read, files_edited, notes, prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
@@ -73,10 +195,11 @@ export class HooksDatabase {
|
||||
getRecentObservations(project: string, limit: number = 20): Array<{
|
||||
type: string;
|
||||
text: string;
|
||||
prompt_number: number | null;
|
||||
created_at: string;
|
||||
}> {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT type, text, created_at
|
||||
SELECT type, text, prompt_number, created_at
|
||||
FROM observations
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
@@ -132,6 +255,36 @@ export class HooksDatabase {
|
||||
stmt.run(userPrompt, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment prompt counter and return new value
|
||||
*/
|
||||
incrementPromptCounter(id: number): number {
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run(id);
|
||||
|
||||
const result = this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(id) as { prompt_counter: number } | undefined;
|
||||
|
||||
return result?.prompt_counter || 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current prompt counter for a session
|
||||
*/
|
||||
getPromptCounter(id: number): number {
|
||||
const result = this.db.prepare(`
|
||||
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
|
||||
`).get(id) as { prompt_counter: number | null } | undefined;
|
||||
|
||||
return result?.prompt_counter || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new SDK session
|
||||
*/
|
||||
@@ -151,15 +304,20 @@ export class HooksDatabase {
|
||||
|
||||
/**
|
||||
* Update SDK session ID (captured from init message)
|
||||
* Only updates if current sdk_session_id is NULL to avoid breaking foreign keys
|
||||
*/
|
||||
updateSDKSessionId(id: number, sdkSessionId: string): void {
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`);
|
||||
|
||||
stmt.run(sdkSessionId, id);
|
||||
const result = stmt.run(sdkSessionId, id);
|
||||
|
||||
if (result.changes === 0) {
|
||||
console.error(`[HooksDatabase] Skipped updating sdk_session_id for session ${id} - already set (prevents FOREIGN KEY constraint violation)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,18 +355,19 @@ export class HooksDatabase {
|
||||
sdkSessionId: string,
|
||||
project: string,
|
||||
type: string,
|
||||
text: string
|
||||
text: string,
|
||||
promptNumber?: number
|
||||
): void {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
(sdk_session_id, project, text, type, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(sdkSessionId, project, text, type, now.toISOString(), nowEpoch);
|
||||
stmt.run(sdkSessionId, project, text, type, promptNumber || null, now.toISOString(), nowEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,7 +385,8 @@ export class HooksDatabase {
|
||||
files_read?: string;
|
||||
files_edited?: string;
|
||||
notes?: string;
|
||||
}
|
||||
},
|
||||
promptNumber?: number
|
||||
): void {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
@@ -234,8 +394,8 @@ export class HooksDatabase {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
next_steps, files_read, files_edited, notes, prompt_number, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
@@ -249,6 +409,7 @@ export class HooksDatabase {
|
||||
summary.files_read || null,
|
||||
summary.files_edited || null,
|
||||
summary.notes || null,
|
||||
promptNumber || null,
|
||||
now.toISOString(),
|
||||
nowEpoch
|
||||
);
|
||||
|
||||
@@ -20,13 +20,15 @@ interface ObservationMessage {
|
||||
tool_name: string;
|
||||
tool_input: string;
|
||||
tool_output: string;
|
||||
prompt_number: number;
|
||||
}
|
||||
|
||||
interface FinalizeMessage {
|
||||
type: 'finalize';
|
||||
interface SummarizeMessage {
|
||||
type: 'summarize';
|
||||
prompt_number: number;
|
||||
}
|
||||
|
||||
type WorkerMessage = ObservationMessage | FinalizeMessage;
|
||||
type WorkerMessage = ObservationMessage | SummarizeMessage;
|
||||
|
||||
/**
|
||||
* Active session state
|
||||
@@ -36,10 +38,10 @@ interface ActiveSession {
|
||||
sdkSessionId: string | null;
|
||||
project: string;
|
||||
userPrompt: string;
|
||||
isFinalized: boolean;
|
||||
pendingMessages: WorkerMessage[];
|
||||
abortController: AbortController;
|
||||
generatorPromise: Promise<void> | null;
|
||||
lastPromptNumber: number; // Track which prompt_number we last sent to SDK
|
||||
}
|
||||
|
||||
class WorkerService {
|
||||
@@ -57,7 +59,7 @@ class WorkerService {
|
||||
// Session endpoints
|
||||
this.app.post('/sessions/:sessionDbId/init', this.handleInit.bind(this));
|
||||
this.app.post('/sessions/:sessionDbId/observations', this.handleObservation.bind(this));
|
||||
this.app.post('/sessions/:sessionDbId/finalize', this.handleFinalize.bind(this));
|
||||
this.app.post('/sessions/:sessionDbId/summarize', this.handleSummarize.bind(this));
|
||||
this.app.get('/sessions/:sessionDbId/status', this.handleStatus.bind(this));
|
||||
this.app.delete('/sessions/:sessionDbId', this.handleDelete.bind(this));
|
||||
}
|
||||
@@ -133,10 +135,10 @@ class WorkerService {
|
||||
sdkSessionId: null,
|
||||
project,
|
||||
userPrompt,
|
||||
isFinalized: false,
|
||||
pendingMessages: [],
|
||||
abortController: new AbortController(),
|
||||
generatorPromise: null
|
||||
generatorPromise: null,
|
||||
lastPromptNumber: 0
|
||||
};
|
||||
|
||||
this.sessions.set(sessionDbId, session);
|
||||
@@ -164,11 +166,11 @@ class WorkerService {
|
||||
|
||||
/**
|
||||
* POST /sessions/:sessionDbId/observations
|
||||
* Body: { tool_name, tool_input, tool_output }
|
||||
* Body: { tool_name, tool_input, tool_output, prompt_number }
|
||||
*/
|
||||
private handleObservation(req: Request, res: Response): void {
|
||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
||||
const { tool_name, tool_input, tool_output } = req.body;
|
||||
const { tool_name, tool_input, tool_output, prompt_number } = req.body;
|
||||
|
||||
const session = this.sessions.get(sessionDbId);
|
||||
if (!session) {
|
||||
@@ -176,28 +178,26 @@ class WorkerService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.isFinalized) {
|
||||
res.status(400).json({ error: 'Session already finalized' });
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`[WorkerService] Queueing observation for session ${sessionDbId}:`, tool_name);
|
||||
|
||||
session.pendingMessages.push({
|
||||
type: 'observation',
|
||||
tool_name,
|
||||
tool_input,
|
||||
tool_output
|
||||
tool_output,
|
||||
prompt_number
|
||||
});
|
||||
|
||||
res.json({ status: 'queued', queueLength: session.pendingMessages.length });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /sessions/:sessionDbId/finalize
|
||||
* POST /sessions/:sessionDbId/summarize
|
||||
* Body: { prompt_number }
|
||||
*/
|
||||
private handleFinalize(req: Request, res: Response): void {
|
||||
private handleSummarize(req: Request, res: Response): void {
|
||||
const sessionDbId = parseInt(req.params.sessionDbId, 10);
|
||||
const { prompt_number } = req.body;
|
||||
|
||||
const session = this.sessions.get(sessionDbId);
|
||||
if (!session) {
|
||||
@@ -205,16 +205,14 @@ class WorkerService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.isFinalized) {
|
||||
res.status(400).json({ error: 'Session already finalized' });
|
||||
return;
|
||||
}
|
||||
console.error(`[WorkerService] Requesting summary for session ${sessionDbId}, prompt #${prompt_number}`);
|
||||
|
||||
console.error(`[WorkerService] Finalizing session ${sessionDbId}`);
|
||||
session.pendingMessages.push({
|
||||
type: 'summarize',
|
||||
prompt_number
|
||||
});
|
||||
|
||||
session.pendingMessages.push({ type: 'finalize' });
|
||||
|
||||
res.json({ status: 'finalizing' });
|
||||
res.json({ status: 'queued', queueLength: session.pendingMessages.length });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,7 +231,6 @@ class WorkerService {
|
||||
sessionDbId,
|
||||
sdkSessionId: session.sdkSessionId,
|
||||
project: session.project,
|
||||
isFinalized: session.isFinalized,
|
||||
pendingMessages: session.pendingMessages.length
|
||||
});
|
||||
}
|
||||
@@ -263,12 +260,10 @@ class WorkerService {
|
||||
]);
|
||||
}
|
||||
|
||||
// Mark as failed if not completed
|
||||
if (!session.isFinalized) {
|
||||
const db = new HooksDatabase();
|
||||
db.markSessionFailed(sessionDbId);
|
||||
db.close();
|
||||
}
|
||||
// Mark as failed since we're aborting
|
||||
const db = new HooksDatabase();
|
||||
db.markSessionFailed(sessionDbId);
|
||||
db.close();
|
||||
|
||||
this.sessions.delete(sessionDbId);
|
||||
|
||||
@@ -315,10 +310,10 @@ class WorkerService {
|
||||
? content.filter((c: any) => c.type === 'text').map((c: any) => c.text).join('\n')
|
||||
: typeof content === 'string' ? content : '';
|
||||
|
||||
console.error(`[WorkerService] SDK response (${textContent.length} chars)`);
|
||||
console.error(`[WorkerService] SDK response (${textContent.length} chars) for prompt #${session.lastPromptNumber}`);
|
||||
|
||||
// Parse and store
|
||||
this.handleAgentMessage(session, textContent);
|
||||
// Parse and store with prompt number
|
||||
this.handleAgentMessage(session, textContent, session.lastPromptNumber);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +337,7 @@ class WorkerService {
|
||||
|
||||
/**
|
||||
* Create async message generator for SDK streaming
|
||||
* Keeps running continuously - no finalize, agent stays alive for entire Claude Code session
|
||||
*/
|
||||
private async* createMessageGenerator(session: ActiveSession): AsyncIterable<SDKUserMessage> {
|
||||
const claudeSessionId = `session-${session.sessionDbId}`;
|
||||
@@ -359,8 +355,12 @@ class WorkerService {
|
||||
}
|
||||
};
|
||||
|
||||
// Process messages as they arrive
|
||||
while (!session.isFinalized) {
|
||||
// Process messages continuously until session is deleted
|
||||
while (true) {
|
||||
if (session.abortController.signal.aborted) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (session.pendingMessages.length === 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
continue;
|
||||
@@ -369,9 +369,9 @@ class WorkerService {
|
||||
while (session.pendingMessages.length > 0) {
|
||||
const message = session.pendingMessages.shift()!;
|
||||
|
||||
if (message.type === 'finalize') {
|
||||
console.error(`[WorkerService] Processing FINALIZE for session ${session.sessionDbId}`);
|
||||
session.isFinalized = true;
|
||||
if (message.type === 'summarize') {
|
||||
console.error(`[WorkerService] Processing SUMMARIZE for session ${session.sessionDbId}, prompt #${message.prompt_number}`);
|
||||
session.lastPromptNumber = message.prompt_number;
|
||||
|
||||
const db = new HooksDatabase();
|
||||
const dbSession = db.db.prepare(`
|
||||
@@ -382,8 +382,24 @@ class WorkerService {
|
||||
db.close();
|
||||
|
||||
if (dbSession) {
|
||||
const finalizePrompt = buildFinalizePrompt(dbSession);
|
||||
console.error(`[WorkerService] Yielding finalize prompt (${finalizePrompt.length} chars)`);
|
||||
const summarizePrompt = `You have been processing tool observations for this session. Please generate a summary of what you've learned from prompt #${message.prompt_number}.
|
||||
|
||||
Use this XML format:
|
||||
|
||||
<session_summary>
|
||||
<request>What was the user trying to accomplish in this prompt?</request>
|
||||
<investigated>What code/systems did you explore?</investigated>
|
||||
<learned>What did you learn about the codebase?</learned>
|
||||
<completed>What was done or determined?</completed>
|
||||
<next_steps>What should happen next?</next_steps>
|
||||
<files_read>["file1.ts", "file2.ts"]</files_read>
|
||||
<files_edited>["file3.ts"]</files_edited>
|
||||
<notes>Any additional context or insights</notes>
|
||||
</session_summary>
|
||||
|
||||
Respond ONLY with the XML block. Be concise and specific.`;
|
||||
|
||||
console.error(`[WorkerService] Yielding summarize prompt`);
|
||||
|
||||
yield {
|
||||
type: 'user',
|
||||
@@ -391,14 +407,13 @@ class WorkerService {
|
||||
parent_tool_use_id: null,
|
||||
message: {
|
||||
role: 'user',
|
||||
content: finalizePrompt
|
||||
content: summarizePrompt
|
||||
}
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if (message.type === 'observation') {
|
||||
session.lastPromptNumber = message.prompt_number;
|
||||
|
||||
if (message.type === 'observation') {
|
||||
const observationPrompt = buildObservationPrompt({
|
||||
id: 0,
|
||||
tool_name: message.tool_name,
|
||||
@@ -407,7 +422,7 @@ class WorkerService {
|
||||
created_at_epoch: Date.now()
|
||||
});
|
||||
|
||||
console.error(`[WorkerService] Yielding observation: ${message.tool_name}`);
|
||||
console.error(`[WorkerService] Yielding observation: ${message.tool_name} (prompt #${message.prompt_number})`);
|
||||
|
||||
yield {
|
||||
type: 'user',
|
||||
@@ -425,23 +440,24 @@ class WorkerService {
|
||||
|
||||
/**
|
||||
* Handle agent message - parse and store observations/summaries
|
||||
* Gets prompt_number from the message that triggered this response
|
||||
*/
|
||||
private handleAgentMessage(session: ActiveSession, content: string): void {
|
||||
private handleAgentMessage(session: ActiveSession, content: string, promptNumber: number): void {
|
||||
// Parse observations
|
||||
const observations = parseObservations(content);
|
||||
console.error(`[WorkerService] Parsed ${observations.length} observations`);
|
||||
console.error(`[WorkerService] Parsed ${observations.length} observations for prompt #${promptNumber}`);
|
||||
|
||||
const db = new HooksDatabase();
|
||||
for (const obs of observations) {
|
||||
if (session.sdkSessionId) {
|
||||
db.storeObservation(session.sdkSessionId, session.project, obs.type, obs.text);
|
||||
db.storeObservation(session.sdkSessionId, session.project, obs.type, obs.text, promptNumber);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse summary
|
||||
const summary = parseSummary(content);
|
||||
if (summary && session.sdkSessionId) {
|
||||
console.error(`[WorkerService] Parsed summary for session ${session.sessionDbId}`);
|
||||
console.error(`[WorkerService] Parsed summary for session ${session.sessionDbId}, prompt #${promptNumber}`);
|
||||
|
||||
const summaryWithArrays = {
|
||||
request: summary.request,
|
||||
@@ -454,7 +470,7 @@ class WorkerService {
|
||||
notes: summary.notes
|
||||
};
|
||||
|
||||
db.storeSummary(session.sdkSessionId, session.project, summaryWithArrays);
|
||||
db.storeSummary(session.sdkSessionId, session.project, summaryWithArrays, promptNumber);
|
||||
}
|
||||
|
||||
db.close();
|
||||
|
||||
Reference in New Issue
Block a user