diff --git a/apps/backend/runners/github/orchestrator.py b/apps/backend/runners/github/orchestrator.py index fb49f3dc..273fd2b5 100644 --- a/apps/backend/runners/github/orchestrator.py +++ b/apps/backend/runners/github/orchestrator.py @@ -1519,6 +1519,7 @@ class GitHubOrchestrator: self, issue_number: int, project_root: Path | None = None, + resume_sessions: dict[str, str] | None = None, ) -> dict: """ Run an AI investigation on a GitHub issue. @@ -1531,6 +1532,8 @@ class GitHubOrchestrator: issue_number: The GitHub issue number to investigate project_root: Working directory for agents (e.g., worktree path). Defaults to self.project_dir. + resume_sessions: Optional dict mapping specialist name to SDK + session ID for resuming interrupted investigations. Returns: Dict with investigation results (InvestigationReport as dict) @@ -1616,6 +1619,7 @@ class GitHubOrchestrator: issue_labels=issue_labels, issue_comments=issue_comments, project_root=working_dir, + resume_sessions=resume_sessions, ) # Update state to findings_ready diff --git a/apps/backend/runners/github/runner.py b/apps/backend/runners/github/runner.py index 4cf57ae6..f1d05567 100644 --- a/apps/backend/runners/github/runner.py +++ b/apps/backend/runners/github/runner.py @@ -348,13 +348,25 @@ async def cmd_investigate(args) -> int: sys.stderr.reconfigure(line_buffering=True) config = get_config(args) + + # Parse resume sessions if provided + resume_sessions = None + if getattr(args, "resume_sessions", None): + try: + resume_sessions = json.loads(args.resume_sessions) + except json.JSONDecodeError: + safe_print("Warning: Invalid --resume-sessions JSON, starting fresh") + orchestrator = GitHubOrchestrator( project_dir=args.project, config=config, progress_callback=print_progress, ) - result = await orchestrator.investigate_issue(args.issue_number) + result = await orchestrator.investigate_issue( + args.issue_number, + resume_sessions=resume_sessions, + ) # Output JSON for the Electron frontend to parse safe_print("\nJSON Output") @@ -830,6 +842,12 @@ def main(): investigate_parser.add_argument( "issue_number", type=int, help="Issue number to investigate" ) + investigate_parser.add_argument( + "--resume-sessions", + type=str, + default=None, + help="JSON dict of specialist_name:session_id for resuming interrupted investigations", + ) # post-investigation command post_investigation_parser = subparsers.add_parser( diff --git a/apps/backend/runners/github/services/issue_investigation_orchestrator.py b/apps/backend/runners/github/services/issue_investigation_orchestrator.py index e0dd3692..e6b4705e 100644 --- a/apps/backend/runners/github/services/issue_investigation_orchestrator.py +++ b/apps/backend/runners/github/services/issue_investigation_orchestrator.py @@ -152,6 +152,7 @@ class IssueInvestigationOrchestrator(ParallelAgentOrchestrator): issue_labels: list[str] | None = None, issue_comments: list[str] | None = None, project_root: Path | None = None, + resume_sessions: dict[str, str] | None = None, ) -> InvestigationReport: """ Run a full investigation on a GitHub issue. @@ -217,6 +218,7 @@ class IssueInvestigationOrchestrator(ParallelAgentOrchestrator): model=model, thinking_budget=thinking_budget, issue_number=issue_number, + resume_sessions=resume_sessions, ) self._report_progress( @@ -331,6 +333,7 @@ Use Read, Grep, and Glob tools to explore the codebase. model: str, thinking_budget: int | None, issue_number: int | None = None, + resume_sessions: dict[str, str] | None = None, ) -> dict[str, dict[str, Any]]: """Run all investigation specialists in parallel. @@ -340,6 +343,8 @@ Use Read, Grep, and Glob tools to explore the codebase. model: Model to use thinking_budget: Max thinking tokens issue_number: GitHub issue number (for session persistence) + resume_sessions: Optional dict mapping specialist name to SDK + session ID for resuming interrupted sessions. Returns: Dict mapping specialist name → stream result dict @@ -363,6 +368,10 @@ Use Read, Grep, and Glob tools to explore the codebase. if thinking_budget else None ) + # Look up resume session ID for this specialist + _resume_id = ( + resume_sessions.get(cfg.name) if resume_sessions else None + ) return self._run_specialist_session( config=cfg, prompt=_prompt, @@ -373,6 +382,7 @@ Use Read, Grep, and Glob tools to explore the codebase. agent_type="investigation_specialist", context_name=f"Investigation:{cfg.name}", max_messages=cfg.max_turns, + resume_session_id=_resume_id, on_thinking=lambda text, _name=cfg.name: emit_json_event( "thinking", _name, diff --git a/apps/frontend/src/main/ipc-handlers/github/investigation-handlers.ts b/apps/frontend/src/main/ipc-handlers/github/investigation-handlers.ts index 93c9aaed..2b922a0e 100644 --- a/apps/frontend/src/main/ipc-handlers/github/investigation-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/github/investigation-handlers.ts @@ -977,13 +977,35 @@ async function runInvestigation( }); } - const args = buildRunnerArgs( - getRunnerPath(backendPath), - project.path, - 'investigate', - [String(issueNumber)], - { model, thinkingLevel }, - ); + // Read session IDs for interrupted investigation resume + let resumeSessionsArg: string[] = []; + try { + const stateFile = path.join( + project.path, '.auto-claude', 'issues', `${issueNumber}`, 'investigation_state.json' + ); + if (fs.existsSync(stateFile)) { + const stateData = JSON.parse(fs.readFileSync(stateFile, 'utf-8')); + if (stateData.sessions && Object.keys(stateData.sessions).length > 0) { + // Only pass sessions if this is a resume (status was 'investigating') + if (stateData.status === 'investigating') { + resumeSessionsArg = ['--resume-sessions', JSON.stringify(stateData.sessions)]; + } + } + } + } catch { + // Non-fatal: will start fresh + } + + const args = [ + ...buildRunnerArgs( + getRunnerPath(backendPath), + project.path, + 'investigate', + [String(issueNumber)], + { model, thinkingLevel }, + ), + ...resumeSessionsArg, + ]; const startedAt = new Date().toISOString();