1- """Auto-handoff — hook-written HANDOFF.md for agent-independent continuation .
1+ """Auto-handoff — hook-written HANDOFF.md and session restart .
22
3- Called by session_monitor when hard stop triggers. The hook writes
4- the handoff directly so the agent cannot prevent, delay, or fake it.
5- When continue_mode is on, also schedules a background restart.
3+ Called by session_monitor when context pressure is high and continue_mode
4+ is on. Writes HANDOFF.md and launches a new claude session in the background.
65"""
76
8- import os
97import re
108import shutil
119import subprocess
@@ -47,20 +45,10 @@ def write_auto_handoff(
4745 previous_handoff : str ,
4846 git_log : str ,
4947) -> None :
50- """Write HANDOFF.md from hook — agent-independent handoff.
51-
52- Args:
53- handoff_path: Path to write HANDOFF.md
54- state: SessionState (duck-typed — needs exchanges, tool_calls,
55- cumulative_output_bytes, compactions, last_tool_sequence)
56- stop_reason: Human-readable reason for the stop
57- previous_handoff: Content of existing HANDOFF.md (empty string if none)
58- git_log: Output of git log --oneline
59- """
48+ """Write HANDOFF.md from hook — agent-independent handoff."""
6049 goal , session_number = parse_handoff_header (previous_handoff )
6150 next_session = session_number + 1
6251
63- # Build last tool sequence summary
6452 tool_summary = ""
6553 if state .last_tool_sequence :
6654 tool_lines = []
@@ -127,110 +115,41 @@ def get_git_log(max_entries: int = 10) -> str:
127115 return ""
128116
129117
130- def find_claude_pid () -> int :
131- """Walk up the process tree to find the claude process PID.
132-
133- Hooks run as: claude → bash → python3 (this process).
134- os.getppid() gives bash, which dies when the hook exits.
135- We need the actual claude PID so the restarter waits for it.
136- Returns 0 if not found.
137- """
138- try :
139- pid = os .getppid () # start from our parent
140- for _ in range (10 ): # walk up max 10 levels
141- result = subprocess .run (
142- ["ps" , "-p" , str (pid ), "-o" , "ppid=,comm=" ],
143- capture_output = True , text = True , timeout = 5 ,
144- )
145- if result .returncode != 0 :
146- break
147- parts = result .stdout .strip ().split (None , 1 )
148- if len (parts ) < 2 :
149- break
150- ppid , comm = int (parts [0 ]), parts [1 ]
151- if "claude" in comm .lower ():
152- return pid # this PID is the claude process
153- if ppid <= 1 :
154- break
155- pid = ppid
156- except (OSError , ValueError , subprocess .TimeoutExpired ):
157- pass
158- return 0
159-
160-
161- def build_restarter_code (
162- wait_pid : int ,
163- session_dir : str ,
164- project_dir : str ,
165- claude_bin : str ,
166- ) -> str :
167- """Build the Python source for the background restarter process.
168-
169- Separated from schedule_restart for testability.
170- """
171- return f"""
172- import os, shutil, subprocess, sys, time
173- # Wait for the claude process to exit
174- wait_pid = { wait_pid }
175- for _ in range(600):
176- try:
177- os.kill(wait_pid, 0)
178- time.sleep(1)
179- except OSError:
180- break
181- # Brief pause to let Claude fully shut down
182- time.sleep(2)
183- # Clean session state
184- session_dir = { session_dir !r}
185- if os.path.isdir(session_dir):
186- shutil.rmtree(session_dir, ignore_errors=True)
187- # Relaunch
188- prompt = "Read HANDOFF.md and continue from where the previous session left off."
189- cmd = [{ claude_bin !r} , "-p", prompt, "--output-format", "json", "--dangerously-skip-permissions"]
190- env = {{**os.environ, "AGENT_TOOLKIT_CONTINUE": "true"}}
191- subprocess.run(cmd, cwd={ project_dir !r} , env=env)
192- """
193-
194-
195- def schedule_restart (project_dir : Path ) -> None :
196- """Spawn a detached background process that restarts claude after this session exits.
118+ def launch_new_session (project_dir : Path ) -> bool :
119+ """Launch a new claude session in the background.
197120
198- Finds the actual claude process (not the bash intermediary), then spawns
199- a restarter that waits for it to exit, cleans .session/, and relaunches.
121+ Spawns `claude -p` as a detached process. The new session reads
122+ HANDOFF.md and continues. No PID-watching — just fire and forget.
123+ Returns True if launched, False if claude not found.
200124 """
201125 claude_bin = shutil .which ("claude" )
202126 if not claude_bin :
203- sys .stderr .write ("auto_handoff: claude not on PATH, cannot schedule restart\n " )
204- return
205-
206- claude_pid = find_claude_pid ()
207- if claude_pid == 0 :
208- sys .stderr .write ("auto_handoff: could not find claude process in tree\n " )
209- return
210-
211- restarter_code = build_restarter_code (
212- wait_pid = claude_pid ,
213- session_dir = str (project_dir / ".session" ),
214- project_dir = str (project_dir ),
215- claude_bin = claude_bin ,
216- )
127+ sys .stderr .write ("auto_handoff: claude not on PATH, cannot launch new session\n " )
128+ return False
129+
130+ prompt = "Read HANDOFF.md and continue from where the previous session left off."
131+ cmd = [
132+ claude_bin , "-p" , prompt ,
133+ "--output-format" , "json" ,
134+ "--dangerously-skip-permissions" ,
135+ ]
136+
217137 try :
218138 subprocess .Popen (
219- [sys .executable , "-c" , restarter_code ],
139+ cmd ,
140+ cwd = str (project_dir ),
220141 start_new_session = True ,
221142 stdout = subprocess .DEVNULL ,
222143 stderr = subprocess .DEVNULL ,
223144 )
145+ return True
224146 except OSError as exc :
225- sys .stderr .write (f"auto_handoff: failed to schedule restart: {{exc}}\n " )
147+ sys .stderr .write (f"auto_handoff: failed to launch session: { exc } \n " )
148+ return False
226149
227150
228151def trigger_auto_handoff (state , stop_reason : str ) -> None :
229- """Trigger hook-written handoff. Called when hard stop fires.
230-
231- Reads existing HANDOFF.md, gets git log, writes the new handoff,
232- and schedules a background restart if continue_mode is on.
233- """
152+ """Write HANDOFF.md and launch a new session if continue_mode is on."""
234153 handoff_path = Path ("HANDOFF.md" )
235154 previous_handoff = ""
236155 if handoff_path .exists ():
@@ -250,4 +169,4 @@ def trigger_auto_handoff(state, stop_reason: str) -> None:
250169 )
251170
252171 if getattr (state , "continue_mode" , False ):
253- schedule_restart (Path .cwd ())
172+ launch_new_session (Path .cwd ())
0 commit comments