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:
Alex Newman
2025-10-17 16:23:11 -04:00
parent 372854948c
commit d4a71c994d
13 changed files with 480 additions and 265 deletions
+23 -15
View File
@@ -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)}});