Skip to content

fix(session): exec claude on resume so the agent leads the pane - #1786

Open
paskal wants to merge 1 commit into
asheshgoplani:mainfrom
paskal:fix/exec-claude-on-resume
Open

fix(session): exec claude on resume so the agent leads the pane#1786
paskal wants to merge 1 commit into
asheshgoplani:mainfrom
paskal:fix/exec-claude-on-resume

Conversation

@paskal

@paskal paskal commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

What problem does this solve?

A session behaves differently depending on whether it was started or restarted. On a fresh start agent-deck execs the agent, so claude is the pane's process-group leader. buildClaudeResumeCommand and the resume / continue branches of buildClaudeCommand do not, so after any restart the pane is led by bash with claude as its child.

Anything inspecting the pane then reads it as a shell rather than an agent: #{pane_current_command} reports bash, and any check comparing a pid against the tty's foreground pgid concludes no agent is running.

Why this change

exec is what the fresh-start path already does, and its existing comment gives the reason ("exec replaces the current shell with claude for proper job control"). Adding it to the remaining four paths makes the two spawn shapes agree rather than introducing a new convention.

It is also the same class of problem as #1567 / #1580, where the fix was to make tmux execvp() bash directly so the tool is the pane leader instead of being wrapped by the server default-shell. That fixed the outer layer; these command strings reintroduce it at the inner one, because bash's implicit tail-exec optimisation does not apply to an && chain.

Nothing follows the agent invocation in these strings, so exec costs nothing here.

Considered and rejected: leaving it and having consumers special-case the wrapper. That pushes an agent-deck implementation detail onto every tool that reads a pane, and does not fix the start/restart asymmetry.

User impact

Restarted claude sessions now present the same way as freshly started ones: #{pane_current_command} reports the agent, and no wrapper shell lingers in the process tree. Anything that reads panes to detect a running agent (status tooling, tmux-driven integrations, external bridges) starts working across restarts.

No user-visible agent-deck behaviour changes otherwise: same flags, same session ids, same resume semantics.

Evidence

Same session, restarted through the built binary.

Before, on the pane's tty:

$ ps -o pid=,pgid=,stat=,comm= -t ttys002
14053 14053 Ss+  /bin/bash
14058 14053 S+   claude
$ tmux display-message -p -t @124 '#{pane_current_command}'
bash

After:

$ ps -o pid=,pgid=,stat=,comm= -t ttys014
31718 31718 Ss+  claude
$ tmux display-message -p -t @124 '#{pane_current_command}'
2.1.220

The wrapper shell is gone and claude is the group leader (pid == pgid).

Downstream effect that led me here: with ccgram driving the same panes, a restarted session was silent because its hooks were discarded as coming from a "nested" claude (pid != foreground pgid) and the pane was typed as a shell. After the change the same session bound and relayed normally, confirmed end to end (agent reply delivered to its Telegram topic).

Sandboxed suite, exactly as CONTRIBUTING specifies:

HOME=$(mktemp -d) XDG_CONFIG_HOME= XDG_DATA_HOME= XDG_CACHE_HOME= go test ./...

All packages pass except TestIssue1421_CleanStaleSSHSockets, which fails identically on a pristine origin/main worktree on this machine, so it is not from this change:

issue1421_stale_ssh_socket_test.go:27: recreate orphan socket:
  listen unix /var/folders/.../stale@host:22: bind: invalid argument

That looks environmental (macOS $TMPDIR path plus @/: in the socket name) rather than a real regression; happy to open a separate issue if it is not already known.

self-check.sh reports 9 PASS, including revert-check ("tests fail without the change, as they should") and diff-size +81/-6.

AI disclosure

  • Human-written
  • AI-assisted (I directed and reviewed it)
  • AI-authored (a model wrote most of it)

Model(s), if AI helped: claude-opus-5

What actually bothered you

My human set ccgram up so he could answer his agent-deck sessions from his phone, and outbound never worked. In his words: "I see in telegram message I sent to you in this chat but not the reply." Typing from Telegram reached the agent, but nothing the agent said ever came back.

It took a long time to find because it is invisible from the outside: a freshly started session worked and the same session after a restart did not, with no error anywhere. The one session that behaved had simply never been restarted.

Checklist

  • Targeted diff: one problem, no unrelated changes
  • Tests added or updated for new behavior
  • Test suite passes sandboxed: HOME=$(mktemp -d) XDG_CONFIG_HOME= XDG_DATA_HOME= XDG_CACHE_HOME= go test ./... — everything passes except TestIssue1421_CleanStaleSSHSockets, which fails the same way on a pristine origin/main worktree here (see Evidence). Not ticking this since your pipeline runs internal/session and will see the same failure; leaving it visible rather than claiming a clean run.
  • If this touches a hot path (list, status, session output, startup, tmux layer): before/after timing evidence included — no timing evidence, and I think none is meaningful here: the change adds a constant exec to a command string built once per spawn, with no extra work at runtime and nothing added to list, status or session output. Say the word if you want numbers anyway.
  • CHANGELOG.md untouched (entries are added at landing)
  • AI-assisted? Disclosed above, with validation evidence, and I can answer questions about the code
  • "Allow edits from maintainers" is enabled

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

👋 Thanks for the contribution — intake looks complete.

Your PR body carries everything the maintainer's validation pipeline reads first: the problem, the reasoning, the human intent behind it, and an AI-disclosure. It will be applied, built, and tested against main, and you'll get a structured result within about a day. Merges are always human.

gate marker read: ai=authored model=claude-opus-5 intent=yes

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Claude session command builders now prefix supported invocations with exec, and a new test validates restart, fresh-start, resume, and continue command paths.

Changes

Claude execution handoff

Layer / File(s) Summary
Add exec to Claude command builders
internal/session/instance.go
Session-mode and resume command construction now prefixes Claude invocations with exec across continue, resume, session-id, restart, and picker paths.
Validate pane leader commands
internal/session/claude_exec_pane_leader_test.go
Adds parameterized coverage verifying that generated commands end with exec handing control to Claude.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested labels: needs-info

Suggested reviewers: asheshgoplani

🚥 Pre-merge checks | ✅ 7
✅ Passed checks (7 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 70.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Remote_parity ✅ Passed The PR only changes internal/session files, with no edits under internal/ui/ or cmd/agent-deck/.
Test_coverage_per_surface ✅ Passed Shared session spawn builder now has table-driven coverage for restart, fresh, resume, and continue; all CLI/TUI/web/remote entrypoints funnel through it.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title uses Conventional Commits format and clearly matches the resume/continue exec change.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/session/claude_exec_pane_leader_test.go (1)

33-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise the conversation-history --resume branch.

This fresh temporary instance has no conversation data, so this case takes the --session-id fallback. Add a fixture recognized by sessionHasConversationData and assert both the start and restart --resume paths retain the final exec.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/session/claude_exec_pane_leader_test.go` around lines 33 - 39,
Update the “resume by id” test setup to add conversation-history fixture data
recognized by sessionHasConversationData, ensuring buildClaudeCommand selects
the --resume branch instead of the --session-id fallback. Assert that both the
initial start and restart command paths preserve the final exec invocation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/session/claude_exec_pane_leader_test.go`:
- Around line 33-39: Update the “resume by id” test setup to add
conversation-history fixture data recognized by sessionHasConversationData,
ensuring buildClaudeCommand selects the --resume branch instead of the
--session-id fallback. Assert that both the initial start and restart command
paths preserve the final exec invocation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 688c9031-9800-4bcb-8c28-1ef52e55a062

📥 Commits

Reviewing files that changed from the base of the PR and between 580e772 and d1de439.

📒 Files selected for processing (2)
  • internal/session/claude_exec_pane_leader_test.go
  • internal/session/instance.go

The fresh-start path already exec's claude, so the agent replaces the wrapper
shell and becomes the pane's process-group leader. buildClaudeResumeCommand and
the resume/continue branches of buildClaudeCommand did not, so every restarted
session left bash leading the group with claude as its child:

    fg pgid = 14053   /bin/bash
    claude  = 14058   (pgid 14053)

That makes a session look different depending on whether it was started or
restarted. tmux reports #{pane_current_command} as `bash` rather than the agent,
and anything comparing a pid against the tty's foreground pgid concludes the
agent is not running. ccgram hits both: it drops the session's hooks as
"nested" and types the pane as a shell, so a restarted session goes silent
while a freshly started one works.

Nothing follows the agent invocation in these command strings, so exec is free
here, and it makes the two spawn paths agree.
@paskal
paskal force-pushed the fix/exec-claude-on-resume branch from d1de439 to 991759b Compare July 28, 2026 17:24
@github-actions github-actions Bot added intake:clean PR/issue passed the intake contract ai-authored Primarily authored by an AI agent and removed needs-info labels Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-authored Primarily authored by an AI agent intake:clean PR/issue passed the intake contract

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant