Skip to content

feat: resume / re-run for aborted and failed sessions - #170

Closed
lucastononro wants to merge 4 commits into
fix/97-split-page-tsxfrom
fix/106-resume-sessions
Closed

feat: resume / re-run for aborted and failed sessions#170
lucastononro wants to merge 4 commits into
fix/97-split-page-tsxfrom
fix/106-resume-sessions

Conversation

@lucastononro

@lucastononro lucastononro commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

abort_agent cancels the asyncio task and all in-flight work is lost — with 10-minute sandbox executions and 50-trial Optuna runs there was no way to resume or retry a failed/cancelled/timed_out session without re-driving the whole conversation. This adds a backend resume/retry endpoint that relaunches the agent with the prior tool history + completed-task state loaded, skipping steps whose artifacts already exist on the volume, plus a Resume/Retry action in the studio.

Changes

Backend

  • POST /sessions/{id}/resume (routers/sessions.py): relaunches the chat agent for an interrupted session. Guards: 404 unknown session, 409 when an agent is already running (no silent abort — that's what this feature protects against), 400 for a fresh created session with nothing to resume. Publishes a session_resumed SSE event and mirrors send_message's launch wrapper (state eagerly set to chat_running, done/cancelled/failed transitions on completion).
  • services/agent/resume.py (new): build_resume_context() assembles a recovered-progress prompt block from the three durable traces of the interrupted run — persisted agent_thought tool_use/tool_result history (capped at 40 blocks × 500 chars), the session task list, and the workspace file listing from the volume — with explicit resume rules ("treat listed files as completed work; verify cheaply instead of regenerating").
  • run_agent grows an opt-in resume_context: str | None = None kwarg: appended to the system prompt, and the full prior conversation is injected (a resume has no freshly-persisted user message to exclude). When the kwarg is absent the code path is unchanged.
  • SessionResume schema: optional mode (resume|retry — only shades the relaunch prompt), model, agent_models, agent_thinking.

Frontend

  • ChatPane: amber Resume/Retry banner above the input when the session is failed/cancelled/timed_out and idle ("Retry" for failed, "Resume" otherwise).
  • useSessionStream: reducer case for the new session_resumed SSE event → status bubble + eager isRunning.
  • api.resumeSession(sessionId, mode); SSEEvent union extended with session_resumed.

Test plan

Existing (proving no regression): full backend suite green — 306 passed, 8 skipped (was 296/8 before); tsc --noEmit, next lint, next build all green.

New (backend/tests/test_resume.py, 10 tests):

  • endpoint guards: 404 unknown session, 400 created state, 409 while running
  • relaunch: run_agent awaited with stage="chat", the built resume_context, and a mode-shaded user_prompt; session transitions to done when the run completes
  • session_resumed SSE published exactly once with {mode, prior_state}
  • build_resume_context: tool history (with ERROR marker) / task list / workspace listing sections, plain chat messages not duplicated, empty-session placeholders
  • is_resumable_state matrix
  • legacy-path byte-identity: a normal send_message(run_agent=true) launch carries no resume_context kwarg and an unchanged user_prompt — default behavior is untouched when the feature isn't triggered

Closes #106

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4

Greptile Summary

This PR adds resume/retry capability for failed, cancelled, and timed_out sessions, addressing the pain of losing in-flight work on long-running sandbox and Optuna runs. A new POST /sessions/{id}/resume endpoint relaunches the agent with recovered context (tool history, task list, workspace file listing) injected into the system prompt so completed steps are skipped.

  • Backend: new services/agent/resume.py assembles the recovery prompt block from three durable traces; run_agent gains an opt-in resume_context kwarg that appends the block and injects the full conversation history (rather than excluding the last message as normal follow-ups do); the router endpoint mirrors send_message's fire-and-forget task pattern with 404/409/400 guards.
  • Frontend: amber Resume/Retry banner appears above the input when the session is interrupted and idle; useSessionStream handles the new session_resumed SSE event to eagerly flip isRunning; api.resumeSession and the SSEEvent union are extended to match.

Confidence Score: 4/5

Safe to merge after addressing the missing error logging in the resume background task.

The _run_resume background task's except Exception block silently swallows errors without calling logger.exception or capture_exception, unlike the parallel _run_followup in send_message. A failing resume run would update the DB state to failed but leave no trace in logs or Sentry, making these failures invisible in production. All other aspects of the change — the new endpoint guards, the resume-context assembly, the runner changes, and the frontend wiring — are well-implemented and well-tested.

Files Needing Attention: backend/routers/sessions.py — the _run_resume exception handler needs the same logger.exception + capture_exception calls present in _run_followup

Important Files Changed

Filename Overview
backend/routers/sessions.py Adds POST /sessions/{id}/resume endpoint mirroring send_message's launch wrapper; the _run_resume exception handler is missing logger.exception and capture_exception unlike _run_followup
backend/services/agent/resume.py New module assembling resume-context prompt block from tool history, task list, and workspace files; defensive error handling, correct _prior_run_outcome branching for done vs. interrupted states
backend/services/agent/runner.py Adds opt-in resume_context kwarg; appended to system prompt when present, and full history is injected for resume runs (no exclusion of last message); existing path is byte-identical when kwarg is absent
backend/tests/test_resume.py 10 new tests covering endpoint guards, relaunch path, SSE publishing, resume-context sections, is_resumable_state matrix, and legacy-path byte-identity; comprehensive coverage
frontend/src/components/chat/ChatPane.tsx Adds amber Resume/Retry banner for failed/cancelled/timed_out sessions; animate-fade-in is defined in tailwind.config.ts; banner is correctly gated on !isRunning
frontend/src/lib/useSessionStream.ts Adds session_resumed SSE case; eagerly sets isRunning=true and adds status bubble; correctly clears streamingItemIdRef
frontend/src/lib/api.ts Adds resumeSession helper with typed return; clean
frontend/src/lib/types.ts Adds SessionResumedData interface and extends SSEEvent discriminated union; clean
frontend/src/app/page.tsx Adds handleResume callback and passes sessionState + onResume props to ChatPane; clean wiring

Sequence Diagram

sequenceDiagram
    participant UI as Frontend (ChatPane)
    participant API as POST /sessions/{id}/resume
    participant Resume as services/agent/resume
    participant Runner as run_agent
    participant SSE as SSE Broadcaster

    UI->>API: "POST /resume {mode}"
    API->>API: 404 if session not found
    API->>API: 409 if agent already running
    API->>API: 400 if not resumable state
    API->>Resume: build_resume_context(session_id, prior_state)
    Resume-->>API: system prompt block (tool history + tasks + files)
    API->>API: "session.state = chat_running"
    API->>SSE: "publish session_resumed {mode, prior_state}"
    SSE-->>UI: session_resumed → setIsRunning(true)
    API->>Runner: asyncio.create_task(run_agent(..., resume_context))
    API-->>UI: "{status: resumed, mode, prior_state}"
    Runner->>Runner: "system_prompt += resume_context"
    Runner->>Runner: inject full history (no last-msg exclusion)
    Runner-->>API: done / CancelledError / Exception
    API->>API: "session.state = done / cancelled / failed"
Loading

Reviews (2): Last reviewed commit: "Merge branch 'staging-v0.0.5' into fix/1..." | Re-trigger Greptile

Backend:
- New POST /sessions/{id}/resume endpoint: relaunches the agent for a
  failed / cancelled / timed-out (or stale *_running) session. Guards:
  404 unknown session, 409 while an agent is running, 400 for a fresh
  session with no prior run.
- services/agent/resume.py builds the recovered-progress block from the
  three durable traces of the interrupted run: persisted agent_thought
  tool history, the session task list, and the workspace file listing —
  so the relaunched agent skips steps whose artifacts already exist on
  the volume instead of redoing them.
- run_agent grows an opt-in resume_context kwarg (appended to the system
  prompt; full prior conversation injected since a resume has no fresh
  user message). Default path is unchanged.
- "resume" vs "retry" mode only shades the relaunch prompt.

Frontend:
- Resume/Retry banner above the ChatPane input when the session is
  failed / cancelled / timed_out and idle; label + mode follow the state.
- New `session_resumed` SSE event -> status bubble + spinner in
  useSessionStream; api.resumeSession() client call.

Tests: 10 new (endpoint guards, relaunch kwargs, SSE publish, context
assembly, and a legacy-path test proving send_message launches carry no
resume kwargs).

Closes #106

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
Comment thread backend/services/agent/resume.py
Comment thread backend/services/agent/resume.py Outdated
lucastononro and others added 3 commits July 29, 2026 16:06
- resume.py: prior-run wording no longer claims "stopped early" for
  done/*_done sessions (resumed as re-runs of finished work) — new
  _prior_run_outcome helper used by both build_resume_context and
  build_resume_prompt.
- resume.py: widen _clip annotation to str | None to match its runtime
  None guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	backend/services/agent/runner.py
#	frontend/src/components/chat/ChatPane.tsx
#	frontend/src/lib/useSessionStream.ts
@lucastononro

Copy link
Copy Markdown
Owner Author

Merged into integration branch staging-v0.0.5 (see the STAGING-v0.0.5.md ledger on that branch for merge order, Greptile follow-ups and test results). These changes will land on main via the staging merge — closing to clear the queue.

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