244 lines
9.8 KiB
Markdown
244 lines
9.8 KiB
Markdown
# Session Resumption LLD
|
||
|
||
## Goals
|
||
- Support multiple active sessions per task.
|
||
- Resume sessions after backend restart without relying on user navigation.
|
||
- Distinguish resumable vs non-resumable agents (no `resume_token`).
|
||
- Keep session state consistent across backend, agentctl, and UI.
|
||
- Support local and remote executors.
|
||
|
||
## Key Assumptions
|
||
- A session is the unit of execution; each session has at most one active agent execution.
|
||
- Task can have multiple sessions concurrently (active or historical).
|
||
- Agents that support session resumption provide a `resume_token` in session metadata.
|
||
- Agents that do not support resumption cannot resume after restart.
|
||
- Remote executors keep running across backend restarts; the backend must reconnect.
|
||
|
||
## Data Model (existing + additions)
|
||
- `task_sessions`
|
||
- `id`, `task_id`, `state`, `agent_profile_id`, `agent_execution_id`, `metadata` (includes `resume_token`)
|
||
- `worktree_id`, `worktree_path`, `worktree_branch`
|
||
- `executor_id` (local/remote)
|
||
- `executors_running` (persistent table)
|
||
- `id` (executor instance id), `session_id`, `task_id`, `runtime`
|
||
- `agentctl_url`, `agentctl_port`, `container_id` (docker), `pid` (local)
|
||
- `status`: `starting | ready | error | stopped`
|
||
- `supports_session_resume`, `supports_workspace_only`
|
||
- `resume_token`
|
||
- `workspace_path`, `worktree_id`, `worktree_path`, `worktree_branch`
|
||
- `last_seen_at`, `error_message`
|
||
- `executors_cache` (in-memory only)
|
||
- `execution_id`, `session_id`, `runtime`, `status`, `agentctl_url/port`, `workspace_path`
|
||
|
||
Note: agentctl state is derived from `executors_running.status` and health checks.
|
||
|
||
## Agent Capability
|
||
- Add `supports_session_resume` boolean to agent registry or agent profile.
|
||
- When `false`, mark session as non-resumable on backend restart.
|
||
- UI should show: “This agent cannot resume previous sessions. Create a new session.”
|
||
|
||
## Executor Types
|
||
- **Local Docker**: containers survive backend restart; recover via Docker APIs.
|
||
- **Local Standalone**: agentctl processes are local and typically *do not* survive restart.
|
||
- **Remote Executor**: agentctl runs remotely; survives backend restart; backend must reconnect.
|
||
|
||
## Startup Flow (Backend)
|
||
Goal: recover agentctl instances from `executors_running` on backend start, not when a user opens a session.
|
||
|
||
1) **Load executors**
|
||
- Query `executors_running` where `status` in (`starting`, `ready`).
|
||
|
||
2) **Reject non-resumable executors**
|
||
- If `supports_session_resume=false`, mark related sessions `ERROR` and set `error_message=not_resumable`.
|
||
|
||
3) **Resume executors by runtime**
|
||
- **Local Docker**:
|
||
- Verify Docker daemon running.
|
||
- Locate containers by `executors_running.container_id` or session label.
|
||
- If container missing: mark executor `error`, mark sessions `ERROR`.
|
||
- If container running: healthcheck agentctl.
|
||
- **Local Standalone**:
|
||
- Verify worktree path exists.
|
||
- If missing: mark executor `error`, mark sessions `ERROR`.
|
||
- Start agentctl process and healthcheck.
|
||
- **Remote Executor**:
|
||
- Call `Health()` on `agentctl_url`.
|
||
- If unreachable: mark executor `error`, mark sessions `ERROR`.
|
||
|
||
4) **Sync executor cache**
|
||
- Register running executions in `executors_cache` keyed by `session_id`.
|
||
- Update `executors_running.last_seen_at`.
|
||
|
||
5) **Enable workspace access**
|
||
- If agentctl is healthy, set `executors_running.status=ready`.
|
||
- Shell/fs/git are enabled regardless of agent resume.
|
||
|
||
6) **Resume agent session**
|
||
- If `supports_session_resume=true` and `resume_token` present, resume agent.
|
||
- On success: set `session.state = WAITING_FOR_INPUT`.
|
||
- On failure: set `session.state = ERROR`, update `error_message`.
|
||
|
||
7) **Publish updates**
|
||
- Emit session agentctl events to UI.
|
||
|
||
## Executor Resumption Capability
|
||
Executors should report whether agent session resume is supported by the runtime + agent, so the backend can decouple executor lifecycle from agent lifecycle.
|
||
|
||
### Agentctl Capability Probe
|
||
After agentctl is reachable, call a capability endpoint (new or existing) to determine:
|
||
- `supports_session_resume`: whether the agent can resume with the session resumption token.
|
||
- `supports_workspace_only`: whether workspace-only mode is available (for git/fs/shell without agent).
|
||
|
||
Note: `requires_new_session` is redundant with `supports_session_resume=false`. If we keep it, it must be a derived/alias field, not an independent signal.
|
||
|
||
This allows:\n
|
||
- Executor resume (agentctl + workspace streams) even if agent resume is not possible.
|
||
- The UI to enable shell/git/files regardless of agent resumption outcome.
|
||
|
||
## Session Resumption Flow (Backend)
|
||
Triggered when user explicitly requests resume.
|
||
|
||
1) Validate session:
|
||
- Session exists and belongs to task.
|
||
- Agent profile supports resume and session has `resume_token`.
|
||
- Worktree exists if required.
|
||
|
||
2) Create agentctl instance (local only):
|
||
- Call `LaunchAgent` with `session_id`, `worktree_path`, `worktree_branch`.
|
||
- Persist `executors_running.status=starting`.
|
||
|
||
3) Start agent process and resume:
|
||
- Resume agent session with `resume_token`.
|
||
- On success:
|
||
- `session.state = WAITING_FOR_INPUT`
|
||
- `executors_running.status = ready`
|
||
- `task.state = REVIEW`
|
||
- On failure:
|
||
- `session.state = ERROR`
|
||
- `executors_running.status = error`
|
||
- Persist `error_message`.
|
||
|
||
## Session Status API
|
||
### `task.session.status`
|
||
Return session-specific status (no task-level fallback):
|
||
- `session_id`, `task_id`, `state`, `agent_profile_id`
|
||
- `executor_status`, `agent_execution_id`, `runtime`
|
||
- `is_agent_running` (based on execution for that session)
|
||
- `is_resumable` (resume supported + `resume_token` present)
|
||
- `needs_resume` (no running execution + resumable)
|
||
- `resume_reason` (e.g., `agent_not_running`, `not_resumable`, `missing_resume_token`)
|
||
- `worktree_path`, `worktree_branch`
|
||
|
||
## WebSocket Events
|
||
Session-scoped events:
|
||
- `session.agentctl_starting`
|
||
- `session.agentctl_ready`
|
||
- `session.agentctl_error`
|
||
- `session.state_changed`
|
||
- `session.message.added`, `session.message.updated`
|
||
|
||
Payloads must include `session_id` and `task_id`.
|
||
|
||
## Frontend Flow
|
||
1) **SSR page** loads session + task + messages and hydrates store.
|
||
2) **useSessionResumption**
|
||
- Calls `task.session.status`.
|
||
- If `needs_resume && is_resumable`: show “Resume session” CTA or auto-resume (configurable).
|
||
- If `!is_resumable`: show “Create new session” CTA.
|
||
3) **UI states**
|
||
- `executor_status=starting`: show spinner and disable shell/file panels.
|
||
- `executor_status=ready`: allow shell/file access.
|
||
- `executor_status=error`: show error badge and retry/resume action.
|
||
|
||
## Multi-Session per Task Considerations
|
||
- Executor and lifecycle manager must key executions by `session_id` (done).
|
||
- `task.session.status` must only check `execution.SessionID == sessionID`.
|
||
- WebSocket subscriptions should be per-session to avoid cross-session updates.
|
||
|
||
## Non-Resumable Agents
|
||
- Add `supports_session_resume=false` in agent registry/profile.
|
||
- Backend returns `is_resumable=false` in status.
|
||
- UI displays a banner: “This agent doesn’t support resuming. Start a new session.”
|
||
|
||
## Open Decisions
|
||
- Whether to keep `executors_running` lean or store full runtime metadata.
|
||
- Auto-resume on backend startup vs on-demand (current request: startup).
|
||
- Whether to auto-resume all resumable sessions or only those in RUNNING/WAITING_FOR_INPUT.
|
||
- Remote executor reconnect timeout/backoff policy.
|
||
|
||
## Resumption Flow Diagrams
|
||
|
||
### Local Docker Executor (agentctl container survives)
|
||
```
|
||
Backend Restart
|
||
|
|
||
|-- load executors_running (starting/ready)
|
||
|-- docker: RecoverInstances()
|
||
| -> map container to executors_running.id or session_id label
|
||
|-- agentctl Health()
|
||
|-- agentctl Capabilities? (supports_session_resume)
|
||
|-- register AgentExecution(session_id, runtime=local_docker)
|
||
|-- executors_running.status = ready (workspace-only enabled)
|
||
|
|
||
|-- if supports_session_resume & resume_token present:
|
||
| ResumeSession
|
||
| session.state = WAITING_FOR_INPUT
|
||
| else:
|
||
| session.state = ERROR (not_resumable)
|
||
|
|
||
'-- WS events to UI:
|
||
session.agentctl_ready / session.state_changed
|
||
```
|
||
|
||
### Local Standalone Executor (agentctl does NOT survive)
|
||
```
|
||
Backend Restart
|
||
|
|
||
|-- load executors_running (starting/ready)
|
||
|-- no local instances to recover
|
||
|-- LaunchAgent from executors_running (workspace-only)
|
||
|-- executors_running.status = starting
|
||
|-- agentctl Health()
|
||
|-- agentctl Capabilities? (supports_session_resume)
|
||
|-- executors_running.status = ready (workspace-only enabled)
|
||
|
|
||
|-- if supports_session_resume & resume_token present:
|
||
| ResumeSession
|
||
| session.state = WAITING_FOR_INPUT
|
||
| else:
|
||
| session.state = ERROR (not_resumable)
|
||
|
|
||
'-- WS events to UI:
|
||
session.agentctl_starting / ready / error
|
||
```
|
||
|
||
### Remote Executor (agentctl survives remotely)
|
||
```
|
||
Backend Restart
|
||
|
|
||
|-- load executors_running: agentctl_url, id
|
||
|-- connect to agentctl Health()
|
||
|-- agentctl Capabilities? (supports_session_resume)
|
||
|-- register AgentExecution(session_id, runtime=remote)
|
||
|-- executors_running.status = ready (workspace-only enabled)
|
||
|
|
||
|-- if supports_session_resume & resume_token present:
|
||
| ResumeSession
|
||
| session.state = WAITING_FOR_INPUT
|
||
| else:
|
||
| session.state = ERROR (not_resumable)
|
||
|
|
||
'-- WS events to UI:
|
||
session.agentctl_ready / session.state_changed
|
||
```
|
||
|
||
## Executor vs Agent Resumption
|
||
Separating executor resume from agent resume allows:
|
||
- Shell/git/files to function as soon as agentctl is reachable.
|
||
- Agent resume to fail gracefully without losing workspace access.
|
||
|
||
Example UI behavior:
|
||
1) `session.agentctl_ready` -> enable shell/git/files panels.
|
||
2) If `supports_session_resume=false` -> show “Create new session to continue messaging.”
|
||
3) If resume fails -> show error but keep workspace panels active.
|