You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -127,24 +130,26 @@ both reappear; and that a path matched by `.gitignore` does NOT reappear.
127
130
gate on `preserved_ref` being non-empty: a clean worktree at shutdown
128
131
writes a row with an empty `preserved_ref` and must still be restored.
129
132
No new column is needed (consistent with Task 6 leaving `state` alone).
130
-
**Check:** Go test with fakes asserting (a) save calls capture-then-force in
131
-
order and writes preserved_ref before ForceDestroy, (b) RestoreAll restores BOTH
132
-
a worker and an orchestrator, (c) a session the user killed before shutdown is
133
-
not resurrected.
133
+
**Check:** Go test with fakes asserting (a) save calls capture-then-force in
134
+
order and writes preserved_ref before ForceDestroy, (b) RestoreAll restores BOTH
135
+
a worker and an orchestrator, (c) a session the user killed before shutdown is
136
+
not resurrected.
134
137
135
138
### Task 4 — Wire into daemon boot/shutdown (`daemon.go`)
139
+
136
140
- After `startSession` returns and before `srv.Run(ctx)`: call `RestoreAll`
137
141
(best-effort; log failures; never block boot).
138
142
- After `srv.Run(ctx)` returns and before the store closes: call
139
143
`SaveAndTeardownAll` with a fresh bounded context (not the cancelled `ctx`).
140
144
- Expose the manager (or a minimal `LifecycleSaver`/`LifecycleRestorer` seam)
141
145
from the wiring up to `Run`.
142
-
**Check:** Manual run documented in report — spawn a session, edit a tracked
143
-
file + add a new file, `POST /shutdown`; assert worktree removed and
144
-
`refs/ao/preserved/<id>` exists; restart daemon; assert worktree re-created and
145
-
both edits reapplied. Plus `go build ./backend/...` green.
146
+
**Check:** Manual run documented in report — spawn a session, edit a tracked
147
+
file + add a new file, `POST /shutdown`; assert worktree removed and
148
+
`refs/ao/preserved/<id>` exists; restart daemon; assert worktree re-created and
149
+
both edits reapplied. Plus `go build ./backend/...` green.
146
150
147
151
### Task 5 — Frontend: call `/shutdown` before kill (`main.ts`)
152
+
148
153
In `before-quit`: `event.preventDefault()` once, `await fetch(
149
154
http://127.0.0.1:<port>/shutdown, {method:'POST'})` with an ~8s bounded timeout
150
155
(port from the running.json the app already reads), then `killDaemon` +
@@ -153,6 +158,7 @@ http://127.0.0.1:<port>/shutdown, {method:'POST'})` with an ~8s bounded timeout
153
158
log shows the save ran and exited cleanly (not just SIGTERM-killed).
154
159
155
160
### Task 6 — Trim the over-built `session_worktrees.state` enum usage
161
+
156
162
No schema change. Ensure the save/restore code reads/writes only `preserved_ref`
157
163
and leaves `state` at its default; add `ponytail:` comments noting the enum is
158
164
unused multi-repo scaffolding.
@@ -208,6 +214,5 @@ endpoint. No new file, migration, format, or endpoint.
208
214
## Execution order
209
215
210
216
Tasks are sequential where coupled: Task 2 shares the gitworktree adapter with
211
-
Task 1 (do 1 then 2, same package); Task 3 depends on 1 + 2; Task 4 depends on
212
-
3. Task 5 (frontend) and Task 6 (storage cleanup) are independent and can run
217
+
Task 1 (do 1 then 2, same package); Task 3 depends on 1 + 2; Task 4 depends on 3. Task 5 (frontend) and Task 6 (storage cleanup) are independent and can run
213
218
anytime. Suggested order: 1 → 2 → 3 → 4, then 5 and 6.
> Note: the live pass re-reads `rec.IsTerminated` from the pre-pass snapshot, so a session terminated *by* the live pass is not also reaped in the same run. That is fine: its tmux is already gone (that is why it was terminated), so reaping would be a no-op anyway.
341
+
> Note: the live pass re-reads `rec.IsTerminated` from the pre-pass snapshot, so a session terminated _by_ the live pass is not also reaped in the same run. That is fine: its tmux is already gone (that is why it was terminated), so reaping would be a no-op anyway.
- Consumes: the real `Manager.Reconcile`, a real sqlite store, and the test's runtime fake (find how this file already fakes the runtime; reuse it, scripting `IsAlive` per handle).
436
444
437
445
-[ ]**Step 1: Read the existing integration harness**
- Produces: a pure decision helper, e.g. `function planDaemonTakeover(probe: DaemonProbe | null): "reuse" | "replace"`, unit-testable without spawning.
Then, in `startDaemonInner`, after the existing `inspectExistingDaemon` + `resolveDaemonFromPort` attach attempts fail (i.e. just before `spawn`), add: probe the expected port; if something answers but is unhealthy, SIGTERM the holder via the run-file PID and wait for the port to free before spawning. Concretely, before the `spawn(...)` at line 505:
554
564
555
565
```ts
556
-
// A process may hold the port without being a healthy daemon we can attach to
557
-
// (wedged orphan from a crash, or a PID-dead-but-port-held run-file). Spawning
558
-
// then would make the Go child collide and exit 1. Detect it and clear it.
> Use the file's existing run-file read/parse helpers (`parseRunFile`, `defaultRunFilePath`). If `readRunFileSafe`/`rmRunFileSafe`/`waitForPortFree` do not exist, add small local helpers: `readRunFileSafe` wraps `fs.readFile` returning `""` on ENOENT; `rmRunFileSafe` wraps `fs.rm` ignoring ENOENT; `waitForPortFree` polls `readDaemonProbe` until it returns null or the timeout elapses. Keep each to a few lines, matching the file's existing async style.
Copy file name to clipboardExpand all lines: docs/superpowers/plans/2026-06-24-restore-recreate-orchestrator.md
+35-31Lines changed: 35 additions & 31 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -22,11 +22,13 @@
22
22
### Task 1: Typed error for un-resumable restore (fixes the 500)
23
23
24
24
**Files:**
25
+
25
26
- Modify: `backend/internal/session_manager/manager.go` (sentinel near line 25; the "nothing to resume from" return at line 480)
26
27
- Modify: `backend/internal/service/session/service.go` (`toAPIError`, near line 450)
27
28
- Test: `backend/internal/service/session/service_test.go` (new test for the mapping)
28
29
29
30
**Interfaces:**
31
+
30
32
- Produces: `sessionmanager.ErrNotResumable` (a sentinel `error`), and the wire contract `409` with code `SESSION_NOT_RESUMABLE` from `POST /api/v1/sessions/{id}/restore` when a terminated session has neither `agent_session_id` nor `prompt`. Task 2 (frontend) consumes the `SESSION_NOT_RESUMABLE` code.
31
33
32
34
-[ ]**Step 1: Write the failing test**
@@ -117,12 +119,14 @@ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
?"This orchestrator has no saved agent session to resume. You can create a new orchestrator on the same branch; its committed work is preserved and the old worktree is cleaned."
@@ -287,45 +289,47 @@ In `frontend/src/renderer/components/TerminalPane.tsx`, add state and a dialog m
287
289
Add state near the other `useState` hooks in `AttachedTerminal`:
Replace the `catch`/error handling inside `restoreSession` so a `SESSION_NOT_RESUMABLE` code opens the dialog instead of setting the inline error. The `restoreError` returned by `apiClient.POST` is the parsed error envelope, so read its `code`:
setRestoreError(errinstanceofError?err.message:"Unable to restore session");
313
+
} finally {
314
+
setIsRestoring(false);
315
+
}
314
316
```
315
317
316
318
Mount the dialog inside the component's returned JSX (e.g. just before the closing tag of the root `div` in `AttachedTerminal`, alongside the other absolutely-positioned children):
0 commit comments