Skip to content

Commit aefc1eb

Browse files
fix(claude-code): retain on SessionEnd even when retainEveryNTurns > 1 (#1152)
With retainEveryNTurns > 1, short Claude Code sessions (fewer turns than the interval) never hit a retain boundary and their transcript is silently dropped on session close. SessionEnd previously only stopped the daemon and did not flush. Refactor retain.py by splitting main() into: - main(): reads stdin, delegates to run_retain(hook_input, force=False) - run_retain(hook_input, force=False): the retain body; force=True bypasses the retainEveryNTurns turn-counter skip so a caller can request a final flush. session_end.py now imports run_retain and calls it with force=True before stopping the daemon, guaranteeing that every session lands on disk regardless of length or retain cadence. Net effect: `retainEveryNTurns: 10` (the default) stops silently losing sessions under 10 turns. Co-authored-by: biostartechnology <info@biostartechnology.com>
1 parent 9c9d791 commit aefc1eb

2 files changed

Lines changed: 22 additions & 11 deletions

File tree

hindsight-integrations/claude-code/scripts/retain.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,21 +69,14 @@ def read_transcript(transcript_path: str) -> list:
6969
return messages
7070

7171

72-
def main():
72+
def run_retain(hook_input: dict, force: bool = False) -> None:
7373
config = load_config()
7474

7575
if not config.get("autoRetain"):
7676
debug_log(config, "Auto-retain disabled, exiting")
7777
return
7878

79-
# Read hook input from stdin
80-
try:
81-
hook_input = json.load(sys.stdin)
82-
except (json.JSONDecodeError, EOFError):
83-
print("[Hindsight] Failed to read hook input", file=sys.stderr)
84-
return
85-
86-
debug_log(config, f"Stop hook input keys: {list(hook_input.keys())}")
79+
debug_log(config, f"Retain hook_input keys: {list(hook_input.keys())} force={force}")
8780

8881
session_id = hook_input.get("session_id", "unknown")
8982
transcript_path = hook_input.get("transcript_path", "")
@@ -102,8 +95,8 @@ def main():
10295
retain_full_window = False
10396
messages_to_retain = all_messages
10497

105-
# Respect retainEveryNTurns in both modes
106-
if retain_every_n > 1:
98+
# Respect retainEveryNTurns in both modes, unless force=True (SessionEnd final retain)
99+
if retain_every_n > 1 and not force:
107100
turn_count = increment_turn_count(session_id)
108101
if turn_count % retain_every_n != 0:
109102
next_at = ((turn_count // retain_every_n) + 1) * retain_every_n
@@ -226,6 +219,15 @@ def _resolve_template(value: str) -> str:
226219
print(f"[Hindsight] Retain failed: {e}", file=sys.stderr)
227220

228221

222+
def main():
223+
try:
224+
hook_input = json.load(sys.stdin)
225+
except (json.JSONDecodeError, EOFError):
226+
print("[Hindsight] Failed to read hook input", file=sys.stderr)
227+
return
228+
run_retain(hook_input, force=False)
229+
230+
229231
if __name__ == "__main__":
230232
try:
231233
main()

hindsight-integrations/claude-code/scripts/session_end.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ def main():
2828

2929
debug_log(config, f"SessionEnd hook, reason: {hook_input.get('reason', 'unknown')}")
3030

31+
# Force a final retain before stopping the daemon — guarantees short sessions
32+
# (fewer turns than retainEveryNTurns) still land on disk.
33+
if config.get("autoRetain") and hook_input.get("transcript_path"):
34+
try:
35+
from retain import run_retain
36+
run_retain(hook_input, force=True)
37+
except Exception as e:
38+
print(f"[Hindsight] SessionEnd final retain error: {e}", file=sys.stderr)
39+
3140
# Stop daemon if we started it
3241
def _dbg(*a):
3342
debug_log(config, *a)

0 commit comments

Comments
 (0)