|
1 | | -"""Session limit enforcement. |
| 1 | +"""Session limit enforcement — two-layer system. |
2 | 2 |
|
3 | | -continue_mode=True: write HANDOFF.md and launch a new session. |
4 | | -continue_mode=False: warn only, session continues through compaction. |
| 3 | +Layer 1 (compact_at_minutes, default 70): Write HANDOFF.md breadcrumb, |
| 4 | +session continues through compaction. No restart needed. |
| 5 | +
|
| 6 | +Layer 2 (max_session_minutes, default 200): Hard stop. Write final |
| 7 | +HANDOFF.md with restart prompt for the user to paste into new session. |
5 | 8 | """ |
6 | 9 |
|
7 | | -from auto_handoff import trigger_auto_handoff |
| 10 | +from auto_handoff import trigger_auto_handoff, write_auto_handoff, get_git_log |
| 11 | +from pathlib import Path |
8 | 12 | from session_state import ( |
9 | 13 | FALLBACK_MAX_EXCHANGES, |
10 | 14 | HARD_THRESHOLD_BYTES, |
11 | 15 | SessionState, |
| 16 | + check_compact_threshold, |
12 | 17 | check_thresholds, |
13 | 18 | should_warn, |
14 | 19 | ) |
15 | 20 |
|
16 | 21 |
|
| 22 | +RESTART_PROMPT_TEMPLATE = """ |
| 23 | +
|
| 24 | +## Restart Prompt |
| 25 | +
|
| 26 | +Copy-paste the following into your next Claude session to continue seamlessly: |
| 27 | +
|
| 28 | +--- |
| 29 | +
|
| 30 | +Read HANDOFF.md first. You are continuing a multi-session task. The previous session hit its time/context limit. Pick up exactly where it left off: |
| 31 | +1. Read HANDOFF.md for goal, progress, and next steps |
| 32 | +2. Read project-state.md for overall project context |
| 33 | +3. Continue the work — do NOT re-do completed items listed above |
| 34 | +
|
| 35 | +--- |
| 36 | +""" |
| 37 | + |
| 38 | + |
| 39 | +def _write_breadcrumb_handoff(state: SessionState, reason: str) -> None: |
| 40 | + """Layer 1: Write HANDOFF.md as a breadcrumb. Session continues.""" |
| 41 | + handoff_path = Path("HANDOFF.md") |
| 42 | + previous = "" |
| 43 | + if handoff_path.exists(): |
| 44 | + try: |
| 45 | + previous = handoff_path.read_text(encoding="utf-8") |
| 46 | + except OSError: |
| 47 | + pass |
| 48 | + |
| 49 | + git_log = get_git_log() |
| 50 | + write_auto_handoff( |
| 51 | + handoff_path=handoff_path, |
| 52 | + state=state, |
| 53 | + stop_reason=f"[BREADCRUMB] {reason}", |
| 54 | + previous_handoff=previous, |
| 55 | + git_log=git_log, |
| 56 | + ) |
| 57 | + |
| 58 | + |
| 59 | +def _write_final_handoff(state: SessionState, reason: str) -> None: |
| 60 | + """Layer 2: Write final HANDOFF.md with restart prompt.""" |
| 61 | + handoff_path = Path("HANDOFF.md") |
| 62 | + previous = "" |
| 63 | + if handoff_path.exists(): |
| 64 | + try: |
| 65 | + previous = handoff_path.read_text(encoding="utf-8") |
| 66 | + except OSError: |
| 67 | + pass |
| 68 | + |
| 69 | + git_log = get_git_log() |
| 70 | + write_auto_handoff( |
| 71 | + handoff_path=handoff_path, |
| 72 | + state=state, |
| 73 | + stop_reason=f"[HARD STOP] {reason}", |
| 74 | + previous_handoff=previous, |
| 75 | + git_log=git_log, |
| 76 | + ) |
| 77 | + |
| 78 | + # Append restart prompt to HANDOFF.md |
| 79 | + content = handoff_path.read_text(encoding="utf-8") |
| 80 | + handoff_path.write_text(content + RESTART_PROMPT_TEMPLATE, encoding="utf-8") |
| 81 | + |
| 82 | + |
17 | 83 | def apply_session_limits(state: SessionState) -> tuple: |
18 | | - """Apply byte/time limits. Returns (state, response_message). |
| 84 | + """Apply two-layer time/byte limits. Returns (state, response_message). |
19 | 85 |
|
20 | | - continue_mode=True: writes HANDOFF.md and launches new claude session. |
21 | | - continue_mode=False: warns only, session continues through compaction. |
| 86 | + Layer 1 (compact_at): Write breadcrumb HANDOFF.md, session continues. |
| 87 | + Layer 2 (max_session_minutes): Hard stop with restart prompt. |
22 | 88 | """ |
| 89 | + # --- Layer 2: Hard stop --- |
23 | 90 | triggered, stop_reason = check_thresholds(state) |
24 | 91 |
|
25 | | - if triggered and state.continue_mode and not state.warned: |
26 | | - state.warned = True |
27 | | - trigger_auto_handoff(state, stop_reason) |
| 92 | + if triggered and state.stopped == 0: |
| 93 | + state.stopped = 2 |
| 94 | + _write_final_handoff(state, stop_reason) |
28 | 95 | return state, ( |
29 | | - f"SESSION LIMIT: {stop_reason}. " |
30 | | - f"HANDOFF.md written. A new session is being launched. " |
31 | | - f"Wrap up current work." |
| 96 | + f"SESSION HARD STOP: {stop_reason}.\n\n" |
| 97 | + f"HANDOFF.md has been written with a restart prompt.\n" |
| 98 | + f"This session can no longer make meaningful progress.\n\n" |
| 99 | + f"To continue, start a new session and paste:\n" |
| 100 | + f" \"Read HANDOFF.md first. You are continuing a multi-session task.\"\n\n" |
| 101 | + f"Or run: python3 scripts/auto_continue.py" |
32 | 102 | ) |
33 | 103 |
|
34 | | - if triggered and not state.warned: |
| 104 | + # --- Layer 1: Breadcrumb (write HANDOFF.md, keep going) --- |
| 105 | + compact_triggered, compact_reason = check_compact_threshold(state) |
| 106 | + |
| 107 | + if compact_triggered and not state.warned: |
35 | 108 | state.warned = True |
| 109 | + _write_breadcrumb_handoff(state, compact_reason) |
36 | 110 | return state, ( |
37 | | - f"SESSION LIMIT: {stop_reason}. " |
38 | | - f"The session continues (compaction handles context pressure). " |
39 | | - f"Quality may degrade. Wrap up current work." |
| 111 | + f"SESSION CHECKPOINT ({compact_reason}): " |
| 112 | + f"HANDOFF.md updated as a breadcrumb in case of crash/compaction. " |
| 113 | + f"Session continues — no restart needed. " |
| 114 | + f"After compaction, re-read HANDOFF.md to re-orient." |
40 | 115 | ) |
41 | 116 |
|
| 117 | + # --- Warning (approaching limits) --- |
42 | 118 | if should_warn(state) and not state.warned: |
43 | 119 | state.warned = True |
44 | 120 | return state, ( |
|
0 commit comments