17 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/tasks/requirements/task-launch-failure-recovery.md | 2026-08-19 | done |
Implementation Plan: Task Launch Failure Recovery
Overview
The implementation first defines durable error and repository-target contracts. It then hardens Git resolution, carries exact row identity, and adds launch policy. The final waves add task-scoped recovery, bounded delivery, responsive UI, translations, and E2E coverage.
Confirmed root cause
gitref.DefaultBranchfalls back to the current localHEADbranch when no origin ref resolves. Repository import can therefore persist a feature branch as the default.resolveBaseRefWithFallbackstops when the selected base and stored default are absent. It does not refreshorigin/HEADbefore it returnsErrInvalidBaseBranch.autoStartTaskForLoadedStepcallsStartTaskbefore it checks the relevant PR state. No session exists at this point, so session metadata cannot own the gate result.- The lifecycle request carries
RepositoryID, but it dropsTaskRepository.ID. The recovery and self-heal paths cannot target one same-repository branch row safely. handleSessionLaunchFailedcreates separate warning messages, archive/delete actions, and toast suppression. That path conflicts with a typed task error and its recovery actions.
Backend
Error contracts and persistence owners
apps/backend/internal/task/models/models.go- Add typed launch categories and recovery-action constants.
- Extend
LastAgentErrorwithRecoveryActions,TaskRepositoryID, and an explicit bounded stamp. - Add
TaskLaunchErrorandMetaKeyLastLaunchErrorfor pre-session outcomes. - Add load, store, clear, and stable-stamp helpers for the task metadata record.
- Keep session-created launch errors in
task_sessions.metadata["last_agent_error"]. - Store the PR auto-start gate in
tasks.metadata["last_launch_error"]. - Add atomic, key-scoped task-error set and compare-and-clear operations. They do not rewrite the full metadata object or clear a newer stamp.
- Make repeated writes of the same gate result a semantic no-op.
Local default detection
apps/backend/internal/common/gitref/gitref.go- Return the resolution source with the branch name inside the package.
- Make
DefaultBranchOrEmptyreturn empty for a current-HEAD-only result. - Keep
DefaultBranchbehavior unchanged for callers that need a display fallback.
- Keep
internal/common/gitreffree of subprocess and network work.
Live remote-default refresh
apps/backend/internal/worktree/manager_lifecycle.go- Add exported
Manager.ResolveRemoteDefaultBranch(ctx, repoPath). - Read
refs/remotes/origin/HEADbefore the helper runs a command. - Run
git remote set-head origin --autothrough the existing Git admission and classification path. - Use
Manager.inspectTimeoutand the caller context. - Set the existing noninteractive Git environment, including
GIT_TERMINAL_PROMPT=0. - Preserve auth, network, cancellation, and timeout errors as typed causes.
- Add exported
resolveBaseRefWithFallbackuses the live default after the stored default fails. It returnsErrInvalidBaseBranchonly when every branch source is absent.
Exact task-repository identity
Carry TaskRepositoryID through every per-repository launch boundary:
executor.repoInfo
-> executor.RepoSpec
-> lifecycle.RepoLaunchSpec
-> lifecycle.LaunchRequest.RepoSpecs()
-> lifecycle.RepoPrepareSpec
-> lifecycle.EnvPrepareRequest.RepoSpecs()
-> lifecycle.RepoWorktreeResult
Also add the legacy top-level single-repository field where the request synthesizes one spec. Update fresh launch, resume, environment reuse, and multi-repository tests.
The launch-failure result and LastAgentError record carry the exact row ID.
If identity is missing or ambiguous, omit branch recovery actions.
Worktree fallback self-heal
apps/backend/internal/agent/runtime/lifecycle/env_preparer_worktree.go- Preserve the requested base before worktree creation.
- Return the resolved
Worktree.BaseBranchand fallback warning withTaskRepositoryID.
- At the orchestrator boundary, inspect each prepared worktree result.
When fallback occurred, call
Service.UpdateRepositoryBaseBranchfor the exact row. - Keep the worktree manager and lifecycle packages free of a task-service dependency.
- Treat the update as best effort. A write error logs a warning and does not fail the launch.
PR auto-start gate
- Add a narrow PR reader to the orchestrator.
- Load the task-repository rows and active PR links before
StartTask. - Match a row by exact
(repository_id, pr_number)when metadata has a positive PR number. - Otherwise match exact normalized repository and checkout/head branch values.
- Normalize branches by trimming whitespace and one documented Git ref prefix. Keep the final comparison case-sensitive.
- Skip launch only when at least one relevant PR exists and all relevant states are terminal.
- Store
TaskLaunchError{code: pr_already_closed}with a stable stamp from relevant PR identity and state. - Add
mark_review_doneonly when the workflow has a valid terminal final step. - Fail open for lookup errors, no match, open state, empty state, or unknown state.
- Clear the task-owned gate result after a later launch succeeds.
Session launch classification
apps/backend/internal/orchestrator/executor/executor_execute.go- Classify
ErrInvalidBaseBranchasbase_branch_missing. - Attach the exact
TaskRepositoryIDand only valid recovery actions. - Persist the extended
LastAgentErrorwith the existing failed state changes.
- Classify
- Inject a narrow review-completion eligibility capability into the executor. It requires a valid terminal final step and terminal relevant PR state.
Legacy launch-guidance removal
- Remove the handled missing-branch message path from
handleSessionLaunchFailed. - Remove its archive/delete action metadata and
missing_pr_branch_recovery_claimeduse. - Do not set
suppressToastfor a handled typed launch error. - Retain unrelated recovery and toast-suppression behavior.
Task launch recovery action
- Register
task.launch.recoverin the orchestrator handler set and shared WS action constants. - Authorize the task before any lookup.
- Require
error_stampand compare it with the current projected source record. - Validate the optional session-task pair and repository-row ownership.
- For
retry_default, use the worktree manager remote-default resolver. Update the repository default and exact task base before relaunch. - For
pick_base_branch, validate the selected remote branch. Update the exact task base before relaunch. - For
mark_review_done, list workflow steps and select the final valid terminal step. Calltask.Service.MoveTaskWithOptionsso WIP, state, history, and events stay consistent. - Re-evaluate relevant PR state before the move. Reject the action when a relevant PR is open, unknown, or no longer matches.
- Add a recovery-only move option that permits
FAILEDto becomeCOMPLETEDat that step. Do not change failed-state preservation for normal moves. - Clear the source error only after the recovery action succeeds.
- Use a compare-and-clear write so success cannot erase a newer error.
- Replace it with
default_branch_unresolvedwhen a default cannot resolve.
Bounded status projection
- Update
apps/backend/internal/task/statussummary/model.gowith optionalSessionID, optionalTaskRepositoryID,Category, andRecoveryActions. - Bound category and row IDs to 64 and 256 UTF-8 bytes.
- Accept only the three known action values. Deduplicate them, preserve order, and keep at most three.
- Update live projection and authoritative rebuild to read both error sources.
- Select the newest active record by
occurred_at, then stable stamp. - Ignore malformed task metadata instead of rejecting the complete summary.
- Update
docs/specs/platform/requirements/bounded-task-status-delivery.mdwith this contract.
Frontend
Types and live summary selection
- Extend
apps/web/lib/types/task-status-summary.tswith the new bounded fields. - Extend
OfficeTaskand local task-detailTaskwithstatusSummary. - Map
status_summaryinapps/web/app/office/tasks/[id]/page.tsx. - Add a selector that returns the newest summary for a task across detail data and live kanban caches.
- Update
statusSummaryActiveErrorPreviewso task-owned errors do not require a session ID. - Extend
LastAgentError,RunError, and their parsers with recovery fields and row identity.
Shared launch-error card
- Add
apps/web/components/task/simple/components/task-launch-error-entry.tsx. - Render the card from
TaskStatusSummary.active_errorbefore the Chat empty state. - Reuse the card from
RunErrorEntryfor typed session launch errors. Keep existing resume, fresh-start, and managed-runtime behavior unchanged. - Suppress the summary card when a matching session run-error card already renders.
- Send the three new actions through
task.launch.recover. - Keep action logic and error state shared across desktop and mobile.
- Replace the raw launch toast with
task:launchFailedSeeDetails.
Branch picker and mobile contract
- Extract reusable branch-list and repository-resolution logic from
apps/web/components/task/base-branch-picker.tsx. - Desktop opens the existing popover pattern from the Pick a base branch action.
- Phone opens
apps/web/components/task/mobile/mobile-picker-sheet.tsxfrom the same action. - The task Chat surface is the desktop and phone entry point.
- The error card stays inline because it is short and task-specific.
- Actions wrap and use at least 44px touch targets.
- The task Chat surface owns page scrolling. The picker owns list scrolling and safe-area padding.
- No document-level horizontal overflow is permitted.
Internationalization
- Add category headlines, action labels, picker text, and pointer-toast text to all five task catalogs.
- Write English,
pt-pt, andzh-cnvalues directly. - Generate
zh-hkandzh-twwithpnpm run i18n:zh-hant.
Tests
- Local default detection
- File:
apps/backend/internal/common/gitref/gitref_test.go - Prove that current-
HEAD-only persistence returns empty. - Prove that origin and local main/master refs keep current behavior.
- File:
- Live remote default
- File:
apps/backend/internal/worktree/manager_lifecycle_test.go - Prove success from existing
origin/HEADand refreshedorigin/HEAD. - Prove cancellation, bounded timeout, auth/network classification, and unresolved behavior.
- File:
- Repository identity and self-heal
- Files: executor multi-repository tests and lifecycle worktree-preparer tests.
- Prove exact identity for same-repository multi-branch rows, fallback writes, no-op, and write-error continuation.
- PR gate
- File:
apps/backend/internal/orchestrator/event_handlers_workflow_test.go - Cover merged, closed, open, mixed states, unknown state, no row, lookup error, and replay idempotency.
- Cover exact PR-number matching and same-repository sibling branches.
- File:
- Launch classification
- File:
apps/backend/internal/orchestrator/executor/executor_execute_test.go - Start with a regression that lacks a typed error and exact row target.
- Prove missing-base and generic categories plus ambiguous-target action removal.
- File:
- Legacy consolidation
- File:
apps/backend/internal/orchestrator/task_launch_failure_test.go - Prove that handled errors create no warning message, archive/delete actions, claim, or toast suppression.
- File:
- Recovery action
- File: the owning orchestrator WS handler test.
- Cover task/session/repository ownership, all three actions, forged payloads, and retry idempotency.
- Cover a workflow with no valid terminal final step and a PR that reopens before the action.
- Status projection
- Files:
apps/backend/internal/task/statussummary/*_test.goand rebuild service tests. - Cover newest-source selection, restart rebuild, bounds, known-action filtering, and malformed metadata.
- Files:
- Frontend
- Files: focused tests beside the parser, selector, Chat component, launch-error card, and
RunErrorEntry. - Prove the no-session card, exact WS payloads, existing recovery preservation, and branch picker composition.
- Files: focused tests beside the parser, selector, Chat component, launch-error card, and
E2E Tests
- Desktop merged-PR gate
- File:
apps/web/e2e/tests/task/launch-failure-recovery.spec.ts - Prove no session starts, the durable card survives reload, and Mark review done moves the task.
- File:
- Desktop missing-base recovery
- Same file.
- Prove exact-row retry and branch selection, relaunch, self-heal, reload, and pointer toast.
- Legacy missing-branch regression
- File:
apps/web/e2e/tests/pr/pr-watcher-missing-branch.spec.ts - Replace warning-message and archive/delete expectations with the typed task-error outcome.
- File:
- Mobile recovery parity
- File:
apps/web/e2e/tests/task/mobile-launch-failure-recovery.spec.ts - Prove the same branch-selection outcome through
MobilePickerSheet. - Prove 44px action targets, viewport containment, internal picker scrolling, and no page overflow.
- File:
Use the managed runner so each E2E command builds production assets and performs cleanup.
Verification Results
Implemented and verified on 2026-08-20.
cd apps/backend && go test ./internal/orchestrator/... ./internal/agent/runtime/lifecycle/... ./internal/worktree/...: 5,139 tests passed in 13 packages.cd apps/backend && make build: passed.cd apps/backend && make e2e-plugin-package: passed.cd apps/web && pnpm run lint: passed.cd apps/web && pnpm run typecheck: passed.cd apps/web && pnpm run i18n:check: passed. The catalogs contain 7,159 referenced keys and complete translations.- Focused frontend recovery and status tests: 4 files and 50 tests passed.
- Responsive recovery tests: 2 files and 24 tests passed.
- Desktop task recovery E2E: 2 tests passed.
- Mobile task recovery E2E: 1 test passed.
- Desktop and mobile PR watcher E2E: 1 test passed in each project.
- Desktop and mobile recovery screenshots were captured and inspected.
No public documentation update was required. The change affects internal task recovery behavior.
PR Fixup Remediation
PR #2832 review and CI findings were addressed before delivery.
- Recovery now reloads all task and session error sources and rejects an action when its source is not the newest active error.
- Workflow terminal-step traversal uses one shared helper.
- Session metadata compare-and-clear propagates
RowsAffectederrors. - Windows treats the wildcard-bind
WSAEACCESconflict as a port collision. - Typed launch errors remain visible when they have no recovery action.
- Summary polling keeps the highest revision and duplicate recovery surfaces show the same pending action.
cd apps/backend && make lint: passed with zero issues.cd apps/backend && go test ./internal/orchestrator/... ./internal/common/netutil/... ./internal/task/repository/sqlite/...: 3,468 tests passed in 11 packages.cd apps/backend && go test -c -o /tmp/kandev-netutil-windows.test.exe ./internal/common/netutil: passed.cd apps/web && pnpm run lint: passed.cd apps/web && pnpm run typecheck: passed.- Focused frontend fixup tests: 3 files and 64 tests passed.
Second PR fixup
- Removed the no-op launch-failure callback and retargeted its test to the real state-transition fallback.
- Added bounded message persistence, exact explicit-stamp matching, sanitized unresolved-default details, and an environment-gated Postgres CAS test.
- Replaced stale full-row default-branch writes with an expected-value guarded column update.
- Rejected empty relaunch responses, preserved successful relaunches when compare-and-clear logging is needed, and added session-owned recovery tests.
- Required matching session error stamps in the chat timeline and rendered only typed task errors in the recovery layout branch.
- Added focused positive typed-error assertions and mutex-safe background launch checks.
Implementation Waves And Parallel Candidates
Wave 1 (parallel candidates, user authorization required):
- [x] [task-01-failure-taxonomy-contracts](task-01-failure-taxonomy-contracts.md)
- [x] [task-02-gitref-default-hardening](task-02-gitref-default-hardening.md)
Wave 2:
- [x] [task-03-worktree-live-default-fallback](task-03-worktree-live-default-fallback.md) (depends 02)
- [x] [task-05-pr-review-autostart-gating](task-05-pr-review-autostart-gating.md) (depends 01)
- [x] [task-10-task-base-self-heal](task-10-task-base-self-heal.md) (depends 01)
Wave 3:
- [x] [task-04-launch-failure-classification](task-04-launch-failure-classification.md) (depends 01,10)
Wave 4:
- [x] [task-07-status-summary-projection](task-07-status-summary-projection.md) (depends 01,04,05)
- [x] [task-12-remove-legacy-launch-guidance](task-12-remove-legacy-launch-guidance.md) (depends 04)
Wave 5:
- [x] [task-06-recovery-actions-ws](task-06-recovery-actions-ws.md) (depends 01,02,03,04,05,10,12)
- [x] [task-08-frontend-failure-surface-and-recovery](task-08-frontend-failure-surface-and-recovery.md) (depends 07)
Wave 6:
- [x] [task-11-responsive-launch-error-surface](task-11-responsive-launch-error-surface.md) (depends 06,08)
Wave 7:
- [x] [task-09-e2e-and-i18n](task-09-e2e-and-i18n.md) (depends 11)
The default execution order is sequential in the primary conversation. The waves do not authorize subagents.