Skip to content

Commit da22c00

Browse files
authored
feat(parser): add background_tasks and session_crons fields for v2.1.145+ compat (#108)
Stop and SubagentStop hook input payloads in Claude Code v2.1.145+ include two new fields: background_tasks (array of running task descriptors) and session_crons (array of session-scoped cron jobs). Add both as Option<Value> to Entry so they are captured rather than silently skipped on future hook entries. Fixes #106
1 parent 3fc65ba commit da22c00

2 files changed

Lines changed: 112 additions & 13 deletions

File tree

specs/01-parser-pipeline.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -67,19 +67,21 @@ Each JSONL line is decoded into an `Entry` struct that mirrors the raw Claude Co
6767

6868
### Key Fields
6969

70-
| Field | Description |
71-
| ------------------ | --------------------------------------------------------------- |
72-
| `uuid` | Unique message identifier |
73-
| `entry_type` | Discriminant: `user`, `assistant`, `system`, `hook_event`, etc. |
74-
| `role` | Same as `entry_type` for most messages |
75-
| `content` | Message body (string or content-block array) |
76-
| `model` | Model string (assistant messages only) |
77-
| `subtype` | Hook subtype: `PreToolUse`, `PostToolUse`, `Stop`, … |
78-
| `hookEvent` | Hook event name |
79-
| `isCompactSummary` | Compaction boundary marker |
80-
| `away_summary` | Session-recap text |
81-
| `forkedFrom` | Pre-v2.1.118 fork reference |
82-
| `tool_use_result` | JSON object for tool results |
70+
| Field | Description |
71+
| ------------------ | ------------------------------------------------------------------------ |
72+
| `uuid` | Unique message identifier |
73+
| `entry_type` | Discriminant: `user`, `assistant`, `system`, `hook_event`, etc. |
74+
| `role` | Same as `entry_type` for most messages |
75+
| `content` | Message body (string or content-block array) |
76+
| `model` | Model string (assistant messages only) |
77+
| `subtype` | Hook subtype: `PreToolUse`, `PostToolUse`, `Stop`, … |
78+
| `hookEvent` | Hook event name |
79+
| `isCompactSummary` | Compaction boundary marker |
80+
| `away_summary` | Session-recap text |
81+
| `forkedFrom` | Pre-v2.1.118 fork reference |
82+
| `tool_use_result` | JSON object for tool results |
83+
| `background_tasks` | v2.1.145+: running background task descriptors (Stop/SubagentStop hooks) |
84+
| `session_crons` | v2.1.145+: registered session cron jobs (Stop/SubagentStop hooks) |
8385

8486
```mermaid
8587
classDiagram

src-tauri/src/parser/entry.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ pub struct Entry {
111111
// notifications, window titles, or bells without a controlling terminal.
112112
#[serde(default, rename = "terminalSequence")]
113113
pub terminal_sequence: Option<String>,
114+
// Present in Stop and SubagentStop hook input payloads (v2.1.145+). Claude Code includes
115+
// currently-running background task descriptors and session-scoped cron jobs so hooks can
116+
// inspect or block on them before the session exits.
117+
#[serde(default, rename = "background_tasks")]
118+
pub background_tasks: Option<Value>,
119+
#[serde(default, rename = "session_crons")]
120+
pub session_crons: Option<Value>,
114121
}
115122

116123
#[derive(Debug, Deserialize, Default)]
@@ -621,6 +628,96 @@ mod tests {
621628
);
622629
}
623630

631+
// --- Issue #106: v2.1.145+ Stop/SubagentStop gain background_tasks and session_crons ---
632+
633+
#[test]
634+
fn parse_entry_captures_background_tasks_and_session_crons_v2_1_145() {
635+
// v2.1.145+: Stop and SubagentStop hook input payloads include background_tasks
636+
// (array of running task descriptors) and session_crons (array of registered cron jobs).
637+
// Both must be captured as Value so callers can inspect them.
638+
let line = json!({
639+
"type": "system",
640+
"subtype": "hook_progress",
641+
"uuid": "stop-hook-uuid-145",
642+
"timestamp": "2026-05-19T10:00:00Z",
643+
"hookEvent": "Stop",
644+
"hookName": "on-stop",
645+
"background_tasks": [{"id": "task-1", "description": "running bg job"}],
646+
"session_crons": [{"id": "cron-1", "schedule": "*/5 * * * *"}]
647+
});
648+
let bytes = serde_json::to_vec(&line).unwrap();
649+
let entry =
650+
parse_entry(&bytes).expect("must parse Stop hook entry with new v2.1.145 fields");
651+
652+
let tasks = entry
653+
.background_tasks
654+
.expect("background_tasks must be captured");
655+
assert!(tasks.is_array(), "background_tasks must be an array");
656+
assert_eq!(tasks.as_array().unwrap().len(), 1);
657+
assert_eq!(tasks[0].get("id").and_then(|v| v.as_str()), Some("task-1"));
658+
659+
let crons = entry.session_crons.expect("session_crons must be captured");
660+
assert!(crons.is_array(), "session_crons must be an array");
661+
assert_eq!(crons.as_array().unwrap().len(), 1);
662+
assert_eq!(
663+
crons[0].get("schedule").and_then(|v| v.as_str()),
664+
Some("*/5 * * * *")
665+
);
666+
}
667+
668+
#[test]
669+
fn parse_entry_background_tasks_and_session_crons_default_to_none_when_absent() {
670+
// Stop/SubagentStop hook entries from before v2.1.145 have no background_tasks or
671+
// session_crons fields — both must default to None.
672+
let line = json!({
673+
"type": "system",
674+
"subtype": "hook_progress",
675+
"uuid": "stop-hook-uuid-old",
676+
"timestamp": "2026-05-01T10:00:00Z",
677+
"hookEvent": "Stop",
678+
"hookName": "on-stop"
679+
});
680+
let bytes = serde_json::to_vec(&line).unwrap();
681+
let entry = parse_entry(&bytes).expect("must parse old Stop hook entry");
682+
assert!(
683+
entry.background_tasks.is_none(),
684+
"background_tasks must be None when absent"
685+
);
686+
assert!(
687+
entry.session_crons.is_none(),
688+
"session_crons must be None when absent"
689+
);
690+
}
691+
692+
#[test]
693+
fn parse_entry_subagent_stop_with_background_tasks_and_session_crons() {
694+
// SubagentStop hook input also gains these fields in v2.1.145+.
695+
let line = json!({
696+
"type": "system",
697+
"subtype": "hook_progress",
698+
"uuid": "subagent-stop-uuid-145",
699+
"timestamp": "2026-05-19T11:00:00Z",
700+
"hookEvent": "SubagentStop",
701+
"hookName": "on-subagent-stop",
702+
"background_tasks": [],
703+
"session_crons": [{"id": "c1"}, {"id": "c2"}]
704+
});
705+
let bytes = serde_json::to_vec(&line).unwrap();
706+
let entry =
707+
parse_entry(&bytes).expect("must parse SubagentStop hook entry with new fields");
708+
709+
let tasks = entry
710+
.background_tasks
711+
.expect("background_tasks must be captured");
712+
assert!(
713+
tasks.as_array().unwrap().is_empty(),
714+
"empty array must be preserved"
715+
);
716+
717+
let crons = entry.session_crons.expect("session_crons must be captured");
718+
assert_eq!(crons.as_array().unwrap().len(), 2);
719+
}
720+
624721
// --- Issue #85: lone UTF-16 surrogate sanitization ---
625722

626723
#[test]

0 commit comments

Comments
 (0)