Skip to content

tmux: keep the fresh-context intent until a turn completes (#352) - #948

Open
ziomik wants to merge 1 commit into
bradbrok:mainfrom
ziomik:fix/352-sticky-fresh-context
Open

tmux: keep the fresh-context intent until a turn completes (#352)#948
ziomik wants to merge 1 commit into
bradbrok:mainfrom
ziomik:fix/352-sticky-fresh-context

Conversation

@ziomik

@ziomik ziomik commented Jul 31, 2026

Copy link
Copy Markdown

The bug

context_restart launches a fresh REPL, but the fresh-context intent is spent on that single launch. If the fresh REPL wedges before completing its first turn, the inflight watchdog force-restarts it at 600s — well past the 180s FRESH_CONTEXT_RESPAWN_GRACE_SEC window — and the relaunch resolves --continue, which resumes the newest transcript on disk: the pre-restart one. The context window is never actually cleared, so the agent restarts into the same full window and the loop repeats every ~10 minutes.

Observed on engineer (daemon journald, tmux[engineer]):

claude_cmd_built mode=fresh force_fresh=True prior_transcript=True      # restart, binds 5543e594
inflight head aged 613.0s > 600.0s, transcript quiet + REPL not idle
  — REPL stuck; scheduling force_restart
claude_cmd_built mode=continue force_fresh=False prior_transcript=True  # intent already spent
transcript path updated to .../59201989.jsonl                          # the PRE-restart transcript

The decisive detail: the fresh transcript (5543e594.jsonl) never exists on disk. The fresh REPL never completes a turn, so Claude Code never writes the file — which is also why the watchdog sees "transcript quiet" and force-restarts in the first place. So resuming by path alone would not have been enough either: it would point at a file that isn't there.

The fix

Two halves, each insufficient alone.

1. The fresh intent is sticky. Armed on a force-fresh launch (same place the respawn grace is armed), disarmed only by a correlated post-fresh turn completion — the one event that proves the fresh REPL is alive and owns a real transcript. Until then every relaunch, force_restart included, builds mode=fresh. Without this, half 2 is still defeated: the intent is burned by the first force_restart.

2. No blind --continue. --continue means "newest transcript in this project dir", a guess that goes wrong exactly when it matters. When the session has a bound transcript path (SessionStart hook or discovery), resume it explicitly with --resume <path>. When that bound transcript does not exist on disk, launch fresh rather than resume somebody else's conversation. When no bind was ever recorded (e.g. first launch after a daemon restart), the legacy --continue behavior is unchanged.

claude_cmd_built now also logs fresh_intent_pending and resume_path.

Tests

New TestStickyFreshContextIntent — written first, and the three behavioral ones failed with exactly the production symptom ('--continue' in ['claude', '--continue', ...]) before the fix:

  • fresh intent survives grace expiry until a turn completes (the reproduction: past the grace, at watchdog time)
  • a completed post-fresh turn releases the intent (no permanent-fresh regression)
  • a warm relaunch resumes the bound transcript by path
  • a relaunch is fresh when the bound transcript does not exist — the decisive case
  • an unknown active transcript keeps legacy --continue

Green: 589 passed / 1 skipped across test_tmux_session, test_tmux_transcript, test_tmux_context_autorestart, test_tmux_context_realtime, test_codex_tmux_session, test_codex_tmux_transcript, test_tmux_container_isolation; plus 82 passed across test_daemon and test_agent_status. ruff check clean.

Not in this PR

Raising the watchdog quiet tolerance (600s → ~900s) for wake_reason=context_restart was discussed as a follow-up. Deliberately left out: it changes watchdog timing, and this PR is meant to be reviewable on the launch-mode logic alone.

Deploy note

This touches the session-manager force_restart path. Timing of the production rollout to be agreed before shipping.

🤖 Opened by Engineer

context_restart launched fresh, but if the fresh REPL wedged before
completing its first turn the inflight watchdog force_restarted it at
600s — past the 180s respawn grace — and the relaunch resolved
--continue, which resumes the newest transcript on disk: the very one
the restart was meant to leave behind. The window was never cleared.

Two halves, each insufficient alone:

1. Sticky fresh intent: armed on a force-fresh launch, disarmed only by
   a correlated post-fresh turn completion. Without it the intent is
   burned by the first force_restart.
2. Explicit --resume <path> instead of --continue when the session has a
   bound transcript; if that transcript does not exist on disk (the
   fresh REPL never wrote one) launch fresh rather than resume another
   conversation. No recorded bind keeps legacy --continue.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant