feat(issues): wire session resume through CLI and frontend handlers

Add --resume-sessions CLI arg to investigate subparser. Frontend reads
persisted session IDs from investigation_state.json and passes them to
the subprocess when resuming an interrupted investigation.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Sondre Engebråten
2026-02-14 13:32:44 +01:00
co-authored by Claude Opus 4.6
parent a89bd75904
commit 4d3db8f407
4 changed files with 62 additions and 8 deletions
@@ -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
+19 -1
View File
@@ -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(
@@ -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,
@@ -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();