@@ -1512,6 +1512,12 @@ fn main() -> Result<()> {
15121512 let events_path = tj_core:: paths:: events_dir ( ) ?. join ( format ! ( "{project_hash}.jsonl" ) ) ;
15131513 std:: fs:: create_dir_all ( events_path. parent ( ) . unwrap ( ) ) ?;
15141514
1515+ // Live Claude Code session id (hook payload → env fallback),
1516+ // stamped additively onto the live events this hook emits so
1517+ // consumers can correlate them with the session. None when
1518+ // neither source is present (standalone behaviour unchanged).
1519+ let live_session_id = tj_core:: session_id:: live_session_id ( Some ( & payload) ) ;
1520+
15151521 // SessionStart: emit a JSON envelope with compact resume-packs of
15161522 // open tasks so Claude Code injects them into its system context
15171523 // automatically. This is the load-bearing UX for "the journal
@@ -1680,6 +1686,7 @@ fn main() -> Result<()> {
16801686 ) ;
16811687 event. confidence = Some ( 0.9 ) ;
16821688 event. status = tj_core:: event:: EventStatus :: Confirmed ;
1689+ tj_core:: session_id:: stamp_session_id ( & mut event. meta , live_session_id. as_deref ( ) ) ;
16831690 let mut writer = tj_core:: storage:: JsonlWriter :: open ( & events_path) ?;
16841691 writer. append ( & event) ?;
16851692 writer. flush_durable ( ) ?;
@@ -1732,6 +1739,7 @@ fn main() -> Result<()> {
17321739 & backend,
17331740 last_event_ts. as_deref ( ) ,
17341741 "PreCompactChunk" ,
1742+ live_session_id. as_deref ( ) ,
17351743 )
17361744 . unwrap_or ( 0 ) ;
17371745 if enq > 0 && std:: env:: var ( "TJ_DISABLE_CLASSIFY_SPAWN" ) . is_err ( ) {
@@ -1793,6 +1801,7 @@ fn main() -> Result<()> {
17931801 ) ;
17941802 event. confidence = Some ( 1.0 ) ;
17951803 event. status = tj_core:: event:: EventStatus :: Confirmed ;
1804+ tj_core:: session_id:: stamp_session_id ( & mut event. meta , live_session_id. as_deref ( ) ) ;
17961805 let mut writer = tj_core:: storage:: JsonlWriter :: open ( & events_path) ?;
17971806 writer. append ( & event) ?;
17981807 writer. flush_durable ( ) ?;
@@ -1865,6 +1874,7 @@ fn main() -> Result<()> {
18651874 & backend,
18661875 last_event_ts. as_deref ( ) ,
18671876 "StopChunk" ,
1877+ live_session_id. as_deref ( ) ,
18681878 )
18691879 . unwrap_or ( 0 ) ;
18701880 if enq > 0 && std:: env:: var ( "TJ_DISABLE_CLASSIFY_SPAWN" ) . is_err ( ) {
@@ -1947,7 +1957,7 @@ fn main() -> Result<()> {
19471957 . unwrap_or ( false ) ;
19481958 let is_mock = mock_event_type. is_some ( ) && mock_task_id. is_some ( ) ;
19491959 if !is_mock && !force_sync {
1950- let _ = persist_pending_v2 ( & events_path, & kind, & text, & project_hash, & backend) ?;
1960+ let _ = persist_pending_v2 ( & events_path, & kind, & text, & project_hash, & backend, live_session_id . as_deref ( ) ) ?;
19511961 // Fire-and-forget worker. Errors here are best-effort —
19521962 // a failure to spawn just means the entry sits in
19531963 // pending/ until the next hook fires another spawn.
@@ -3150,6 +3160,7 @@ fn persist_pending_v2(
31503160 text : & str ,
31513161 project_hash : & str ,
31523162 backend : & str ,
3163+ session_id : Option < & str > ,
31533164) -> anyhow:: Result < std:: path:: PathBuf > {
31543165 let pending_dir = events_path
31553166 . parent ( )
@@ -3159,7 +3170,7 @@ fn persist_pending_v2(
31593170 . join ( "pending" ) ;
31603171 std:: fs:: create_dir_all ( & pending_dir) ?;
31613172 let id = ulid:: Ulid :: new ( ) . to_string ( ) ;
3162- let payload = serde_json:: json!( {
3173+ let mut payload = serde_json:: json!( {
31633174 "schema" : "v2" ,
31643175 "kind" : kind,
31653176 "text" : text,
@@ -3168,6 +3179,9 @@ fn persist_pending_v2(
31683179 "backend" : backend,
31693180 "queued_at" : chrono:: Utc :: now( ) . to_rfc3339( ) ,
31703181 } ) ;
3182+ if let Some ( sid) = session_id {
3183+ payload[ "session_id" ] = serde_json:: Value :: String ( sid. to_string ( ) ) ;
3184+ }
31713185 let path = pending_dir. join ( format ! ( "{id}.json" ) ) ;
31723186 std:: fs:: write ( & path, serde_json:: to_string_pretty ( & payload) ?) ?;
31733187 Ok ( path)
@@ -3194,6 +3208,7 @@ fn enqueue_transcript_chunks_since_last_event(
31943208 backend : & str ,
31953209 last_event_ts : Option < & str > ,
31963210 assistant_chunk_kind : & str ,
3211+ session_id : Option < & str > ,
31973212) -> anyhow:: Result < usize > {
31983213 use tj_core:: session:: parser:: {
31993214 extract_assistant_texts, extract_user_text, parse_session, SessionEntry ,
@@ -3226,7 +3241,7 @@ fn enqueue_transcript_chunks_since_last_event(
32263241 continue ;
32273242 }
32283243 }
3229- persist_pending_v2 ( events_path, kind, & text, project_hash, backend) ?;
3244+ persist_pending_v2 ( events_path, kind, & text, project_hash, backend, session_id ) ?;
32303245 count += 1 ;
32313246 }
32323247 Ok ( count)
@@ -3394,6 +3409,9 @@ fn process_pending_entry(
33943409 . unwrap_or ( "" )
33953410 . to_string ( ) ;
33963411
3412+ // Inherit the session id queued on the v2 chunk (additive; absent → None).
3413+ let chunk_session_id = tj_core:: session_id:: session_id_from_payload ( & v) ;
3414+
33973415 // Mirror the synchronous flow that used to live in IngestHook —
33983416 // see commit history of v0.6.1 for the original. Auto-open, run
33993417 // classifier, apply integrity safeguards, persist event, telemetry.
@@ -3510,6 +3528,7 @@ fn process_pending_entry(
35103528 event. confidence = Some ( confidence) ;
35113529 event. status = tj_core:: classifier:: decide_status ( confidence) ;
35123530 event. evidence_strength = evidence_strength;
3531+ tj_core:: session_id:: stamp_session_id ( & mut event. meta , chunk_session_id. as_deref ( ) ) ;
35133532
35143533 let mut writer = tj_core:: storage:: JsonlWriter :: open ( events_path) ?;
35153534 writer. append ( & event) ?;
@@ -3686,6 +3705,33 @@ mod inline_tests {
36863705 // declared before this module begins.
36873706 use super :: * ;
36883707
3708+ #[ test]
3709+ fn persist_pending_v2_includes_session_id_when_present ( ) {
3710+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
3711+ let events_path = dir. path ( ) . join ( "events" ) . join ( "h.jsonl" ) ;
3712+ std:: fs:: create_dir_all ( events_path. parent ( ) . unwrap ( ) ) . unwrap ( ) ;
3713+ let p = persist_pending_v2 ( & events_path, "PostToolUse" , "txt" , "h" , "hybrid" , Some ( "sess-9" ) )
3714+ . unwrap ( ) ;
3715+ let body = std:: fs:: read_to_string ( & p) . unwrap ( ) ;
3716+ let v: serde_json:: Value = serde_json:: from_str ( & body) . unwrap ( ) ;
3717+ assert_eq ! ( v[ "session_id" ] , serde_json:: json!( "sess-9" ) ) ;
3718+ assert_eq ! (
3719+ tj_core:: session_id:: session_id_from_payload( & v) . as_deref( ) ,
3720+ Some ( "sess-9" )
3721+ ) ;
3722+ }
3723+
3724+ #[ test]
3725+ fn persist_pending_v2_omits_session_id_when_none ( ) {
3726+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
3727+ let events_path = dir. path ( ) . join ( "events" ) . join ( "h.jsonl" ) ;
3728+ std:: fs:: create_dir_all ( events_path. parent ( ) . unwrap ( ) ) . unwrap ( ) ;
3729+ let p = persist_pending_v2 ( & events_path, "PostToolUse" , "txt" , "h" , "hybrid" , None ) . unwrap ( ) ;
3730+ let body = std:: fs:: read_to_string ( & p) . unwrap ( ) ;
3731+ let v: serde_json:: Value = serde_json:: from_str ( & body) . unwrap ( ) ;
3732+ assert ! ( v. get( "session_id" ) . is_none( ) ) ;
3733+ }
3734+
36893735 #[ test]
36903736 fn is_rewind_prompt_simple ( ) {
36913737 assert ! ( is_rewind_prompt( "/rewind" ) ) ;
0 commit comments