@@ -1809,7 +1809,7 @@ fn main() -> Result<()> {
18091809 { "type" : "command" , "command" : cmd } ,
18101810 ] } ] ) ,
18111811 ) ;
1812- for ev in [ "PostToolUse" , "Stop" , "PreCompact" ] {
1812+ for ev in [ "PostToolUse" , "Stop" , "PreCompact" , "SessionEnd" ] {
18131813 obj. insert (
18141814 ev. to_string ( ) ,
18151815 serde_json:: json!( [ { "matcher" : "" , "hooks" : [ { "type" : "command" , "command" : cmd } ] } ] ) ,
@@ -2493,6 +2493,21 @@ runs in the background and won't block you; it only fills gaps and never closes
24932493 return Ok ( ( ) ) ;
24942494 }
24952495
2496+ // SessionEnd with reason "clear": /clear discards the conversation
2497+ // and the transcript orphans, so this is the LAST chance to capture
2498+ // the final segment. Extracted to its own function so its locals do
2499+ // NOT bloat `main`'s already-huge stack frame — inlining it here
2500+ // overflowed the 1 MiB Windows main-thread stack on every command.
2501+ if kind == "SessionEnd" {
2502+ return run_session_end_catchup (
2503+ & payload,
2504+ & events_path,
2505+ & project_hash,
2506+ & backend,
2507+ live_session_id. as_deref ( ) ,
2508+ ) ;
2509+ }
2510+
24962511 // Drain any pending entries first (Task 10 fills the real-classifier branch).
24972512 drain_pending (
24982513 & events_path,
@@ -3924,6 +3939,58 @@ fn run_nudge() -> anyhow::Result<()> {
39243939}
39253940
39263941/// Proactive recall injector (opt-in hook). Reads the UserPromptSubmit payload
3942+ /// SessionEnd(reason=clear) catch-up: enqueue transcript chunks newer than the
3943+ /// active task's last event, then spawn the classify-worker. Kept OUT of `main`
3944+ /// so its locals don't grow `main`'s already-huge stack frame — inlining it
3945+ /// overflowed the 1 MiB Windows main-thread stack on every command.
3946+ fn run_session_end_catchup (
3947+ payload : & serde_json:: Value ,
3948+ events_path : & std:: path:: Path ,
3949+ project_hash : & str ,
3950+ backend : & str ,
3951+ live_session_id : Option < & str > ,
3952+ ) -> anyhow:: Result < ( ) > {
3953+ let reason = payload. get ( "reason" ) . and_then ( |x| x. as_str ( ) ) . unwrap_or ( "" ) ;
3954+ if reason != "clear" || !events_path. exists ( ) {
3955+ return Ok ( ( ) ) ;
3956+ }
3957+ let state_path = tj_core:: paths:: state_dir ( ) ?. join ( format ! ( "{project_hash}.sqlite" ) ) ;
3958+ let conn = tj_core:: db:: open ( & state_path) ?;
3959+ tj_core:: db:: ingest_new_events ( & conn, events_path, project_hash) ?;
3960+ let Some ( tc) = recent_task_contexts ( & conn, 1 ) ?. into_iter ( ) . next ( ) else {
3961+ return Ok ( ( ) ) ;
3962+ } ;
3963+ let last_event_ts: Option < String > = conn
3964+ . query_row (
3965+ "SELECT timestamp FROM events_index WHERE task_id=?1 ORDER BY timestamp DESC LIMIT 1" ,
3966+ rusqlite:: params![ & tc. task_id] ,
3967+ |r| r. get :: < _ , String > ( 0 ) ,
3968+ )
3969+ . ok ( ) ;
3970+ let transcript_path = payload
3971+ . get ( "transcript_path" )
3972+ . and_then ( |x| x. as_str ( ) )
3973+ . map ( std:: path:: PathBuf :: from) ;
3974+ if let Some ( tp) = transcript_path. as_ref ( ) {
3975+ if tp. exists ( ) {
3976+ let enq = enqueue_transcript_chunks_since_last_event (
3977+ tp,
3978+ events_path,
3979+ project_hash,
3980+ backend,
3981+ last_event_ts. as_deref ( ) ,
3982+ "SessionEndChunk" ,
3983+ live_session_id,
3984+ )
3985+ . unwrap_or ( 0 ) ;
3986+ if enq > 0 && std:: env:: var ( "TJ_DISABLE_CLASSIFY_SPAWN" ) . is_err ( ) {
3987+ let _ = spawn_classify_worker ( backend) ;
3988+ }
3989+ }
3990+ }
3991+ Ok ( ( ) )
3992+ }
3993+
39273994/// from stdin, keyword-searches the global index for relevant prior
39283995/// decisions/rejections/constraints across all projects, and emits a budgeted
39293996/// `additionalContext` block. Never blocks the prompt: any miss, empty result,
0 commit comments