@@ -835,7 +835,7 @@ func (e *Executor) cleanupInactiveDoneTasks() {
835835
836836 // Also kill the tmux window to fully clean up
837837 windowName := TmuxWindowName (task .ID )
838- killAllWindowsByNameAllSessions (windowName )
838+ KillAllWindowsByNameAllSessions (windowName )
839839
840840 e .logLine (task .ID , "system" , "Claude process cleaned up (inactive done task)" )
841841 }
@@ -1620,30 +1620,57 @@ func TmuxSessionName(taskID int64) string {
16201620 return fmt .Sprintf ("%s:%s" , getDaemonSessionName (), TmuxWindowName (taskID ))
16211621}
16221622
1623- // CapturePaneContent captures the last N lines from a task's tmux pane.
1624- // Returns the trimmed content, or empty string if the pane doesn't exist or capture fails.
1625- func CapturePaneContent (taskID int64 , lines int ) string {
1626- sessionName := TmuxSessionName (taskID )
1627-
1628- // Check if session exists first
1629- if err := exec .Command ("tmux" , "has-session" , "-t" , sessionName ).Run (); err != nil {
1623+ // CapturePaneContent captures the last N lines from a tmux pane.
1624+ // target can be a pane ID (e.g., "%1234") or a window target (e.g., "task-daemon-XXX:2")
1625+ // in which case ".0" is appended to select the first pane.
1626+ // Returns the trimmed content, or empty string if capture fails.
1627+ func CapturePaneContent (windowTarget string , lines int ) string {
1628+ if windowTarget == "" {
16301629 return ""
16311630 }
16321631
1633- ctx , cancel := context .WithTimeout (context .Background (), 2 * time .Second )
1634- defer cancel ()
1632+ // If it's already a pane ID (starts with %), use directly; otherwise append .0
1633+ target := windowTarget
1634+ if ! strings .HasPrefix (windowTarget , "%" ) {
1635+ target = windowTarget + ".0"
1636+ }
16351637
1636- // Capture the last N lines from pane 0 (the executor pane)
1637- target := sessionName + ".0"
1638- startLine := fmt .Sprintf ("-%d" , lines )
1639- out , err := exec .CommandContext (ctx , "tmux" , "capture-pane" , "-t" , target , "-p" , "-S" , startLine ).Output ()
1640- if err != nil {
1638+ // Try capture with a 3-second timeout and one retry
1639+ for attempt := 0 ; attempt < 2 ; attempt ++ {
1640+ ctx , cancel := context .WithTimeout (context .Background (), 3 * time .Second )
1641+ out , err := exec .CommandContext (ctx , "tmux" , "capture-pane" , "-t" , target , "-p" , "-S" , fmt .Sprintf ("-%d" , lines )).Output ()
1642+ cancel ()
1643+
1644+ if err == nil {
1645+ content := strings .TrimRight (string (out ), " \t \n \r " )
1646+ if content != "" {
1647+ return content
1648+ }
1649+ }
1650+
1651+ // Only retry once after a short delay
1652+ if attempt == 0 {
1653+ time .Sleep (200 * time .Millisecond )
1654+ }
1655+ }
1656+
1657+ return ""
1658+ }
1659+
1660+ // FormatSessionHandoff formats captured pane content into a handoff prompt for the new executor.
1661+ func FormatSessionHandoff (prevExecutor , capturedContent string ) string {
1662+ if capturedContent == "" {
16411663 return ""
16421664 }
16431665
1644- // Trim trailing whitespace/empty lines
1645- content := strings .TrimRight (string (out ), " \t \n \r " )
1646- return content
1666+ var sb strings.Builder
1667+ sb .WriteString ("## Previous Session Context\n \n " )
1668+ sb .WriteString (fmt .Sprintf ("This task was previously worked on using **%s**. Below is the terminal output from that session.\n " , prevExecutor ))
1669+ sb .WriteString ("Use this context to continue the work seamlessly.\n \n " )
1670+ sb .WriteString ("```\n " )
1671+ sb .WriteString (capturedContent )
1672+ sb .WriteString ("\n ```\n \n ---\n \n " )
1673+ return sb .String ()
16471674}
16481675
16491676// SendKeyToPane sends a key sequence to a task's executor tmux pane.
@@ -1661,9 +1688,9 @@ func SendKeyToPane(taskID int64, keys ...string) error {
16611688 return exec .Command ("tmux" , args ... ).Run ()
16621689}
16631690
1664- // killAllWindowsByNameAllSessions kills ALL windows with a given name across all daemon sessions.
1691+ // KillAllWindowsByNameAllSessions kills ALL windows with a given name across all daemon sessions.
16651692// Also kills any -shell variant windows.
1666- func killAllWindowsByNameAllSessions (windowName string ) {
1693+ func KillAllWindowsByNameAllSessions (windowName string ) {
16671694 ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
16681695 defer cancel ()
16691696
@@ -2162,7 +2189,7 @@ func (e *Executor) runClaude(ctx context.Context, task *db.Task, workDir, prompt
21622189 windowTarget := fmt .Sprintf ("%s:%s" , daemonSession , windowName )
21632190
21642191 // Kill ALL existing windows with this name (handles duplicates)
2165- killAllWindowsByNameAllSessions (windowName )
2192+ KillAllWindowsByNameAllSessions (windowName )
21662193
21672194 // Setup Claude hooks for status updates
21682195 cleanupHooks , err := e .setupClaudeHooks (workDir , task .ID )
@@ -2339,7 +2366,7 @@ func (e *Executor) runClaudeResume(ctx context.Context, task *db.Task, workDir,
23392366 windowTarget := fmt .Sprintf ("%s:%s" , daemonSession , windowName )
23402367
23412368 // Kill ALL existing windows with this name (handles duplicates)
2342- killAllWindowsByNameAllSessions (windowName )
2369+ KillAllWindowsByNameAllSessions (windowName )
23432370
23442371 // Setup Claude hooks for status updates
23452372 cleanupHooks , err := e .setupClaudeHooks (workDir , task .ID )
@@ -2527,7 +2554,7 @@ func (e *Executor) resumeClaudeDangerous(task *db.Task, workDir string) bool {
25272554 windowName := TmuxWindowName (taskID )
25282555
25292556 // Kill ALL existing windows with this name (handles duplicates)
2530- killAllWindowsByNameAllSessions (windowName )
2557+ KillAllWindowsByNameAllSessions (windowName )
25312558
25322559 // Ensure task-daemon session exists for creating new window
25332560 daemonSession , err := ensureTmuxDaemon ()
@@ -2692,7 +2719,7 @@ func (e *Executor) resumeClaudeSafe(task *db.Task, workDir string) bool {
26922719 windowName := TmuxWindowName (taskID )
26932720
26942721 // Kill ALL existing windows with this name (handles duplicates)
2695- killAllWindowsByNameAllSessions (windowName )
2722+ KillAllWindowsByNameAllSessions (windowName )
26962723
26972724 // Ensure task-daemon session exists for creating new window
26982725 daemonSession , err := ensureTmuxDaemon ()
@@ -2817,7 +2844,7 @@ func (e *Executor) resumeCodexWithMode(task *db.Task, workDir string, dangerousM
28172844
28182845 windowName := TmuxWindowName (taskID )
28192846 // Kill ALL existing windows with this name (handles duplicates)
2820- killAllWindowsByNameAllSessions (windowName )
2847+ KillAllWindowsByNameAllSessions (windowName )
28212848
28222849 daemonSession , err := ensureTmuxDaemon ()
28232850 if err != nil {
@@ -2921,7 +2948,7 @@ func (e *Executor) resumeGeminiWithMode(task *db.Task, workDir string, dangerous
29212948
29222949 windowName := TmuxWindowName (taskID )
29232950 // Kill ALL existing windows with this name (handles duplicates)
2924- killAllWindowsByNameAllSessions (windowName )
2951+ KillAllWindowsByNameAllSessions (windowName )
29252952
29262953 daemonSession , err := ensureTmuxDaemon ()
29272954 if err != nil {
@@ -4860,7 +4887,7 @@ func (e *Executor) runPi(ctx context.Context, task *db.Task, workDir, prompt str
48604887 windowTarget := fmt .Sprintf ("%s:%s" , daemonSession , windowName )
48614888
48624889 // Kill ALL existing windows with this name (handles duplicates)
4863- killAllWindowsByNameAllSessions (windowName )
4890+ KillAllWindowsByNameAllSessions (windowName )
48644891
48654892 // Create a temp file for the prompt (avoids quoting issues)
48664893 promptFile , err := os .CreateTemp ("" , "task-prompt-*.txt" )
@@ -5006,7 +5033,7 @@ func (e *Executor) runPiResume(ctx context.Context, task *db.Task, workDir, prom
50065033 windowTarget := fmt .Sprintf ("%s:%s" , daemonSession , windowName )
50075034
50085035 // Kill ALL existing windows with this name (handles duplicates)
5009- killAllWindowsByNameAllSessions (windowName )
5036+ KillAllWindowsByNameAllSessions (windowName )
50105037
50115038 // Create a temp file for the feedback (avoids quoting issues)
50125039 feedbackFile , err := os .CreateTemp ("" , "task-feedback-*.txt" )
0 commit comments