feat(environments): add archive undo and continue in new thread - #1016
feat(environments): add archive undo and continue in new thread#1016ymichael wants to merge 6 commits into
Conversation
|
🚨 SLOP COP 🚨 · I am SlopCop. I am reviewing this pull request under the configured security, quality, performance, and end-to-end rules. The stack contains PR #1016 only. I found no later pull request and skipped no pull request. I will run the security, code quality and architecture, and performance reviews in parallel. I will test the complete stack after those reviews. |
|
🚨 SLOP COP 🚨 · Performance review complete. The database queries use suitable indexes, and the sweep cadence has no regression. I found one low-impact concern. Undo repeats full thread-list cache invalidation for each archived thread. I will verify this concern during final synthesis. |
|
🚨 SLOP COP 🚨 · Security review complete. Command construction, indexes, concurrent restore claims, and the daemon protocol update look correct. The review found two medium and two lower-impact concerns. They cover branch identity, restore validation order, the grace-period clock, and route guard consistency. I will verify each concern during final synthesis. |
|
🚨 SLOP COP 🚨 · Code quality and architecture review complete. I found no duplicate environment-restore service. The server, app, database, workspace, CLI, and SDK type checks passed. The review found three medium concerns. They cover the public-thread guard, restore validation order, and Undo failure handling. It also found several low-impact cleanup items. |
| if ( | ||
| refreshedEnvironment.status === "retiring" && | ||
| refreshedEnvironment.path !== null && | ||
| Date.now() - refreshedEnvironment.updatedAt < |
There was a problem hiding this comment.
🚨 slopcop/review — The grace-period clock can move after retirement.
The environment metadata PATCH route can update a retiring row, and updateEnvironmentMetadataRecord always writes a new updatedAt. Repeated metadata writes can extend this grace check without limit. Store the retirement time separately, or prevent metadata updates from changing the cleanup clock.
| action: { | ||
| label: "Undo", | ||
| onClick: () => { | ||
| void Promise.all( |
There was a problem hiding this comment.
🚨 slopcop/review — A partial Undo failure leaves restored threads absent from the sidebar.
Promise.all rejects when one unarchive request fails. The .then block then settles no cache entry, even when other requests succeeded. The successful path also repeats broad list invalidation once per thread. Use Promise.allSettled, report failures, settle each detail entry, and invalidate the list keys once.
| return context.json({ ok: true }); | ||
| }); | ||
|
|
||
| post(routes.restoreEnvironment, async (context) => { |
There was a problem hiding this comment.
🚨 slopcop/review — This route omits the public-thread boundary check used by sibling actions.
The service checks thread.deletedAt, but it does not reject a thread whose project has entered soft deletion. A restore can therefore create a worktree while the project deletion sweep removes project environments. Call requirePublicThread at this route boundary before the restore service.
| } | ||
|
|
||
| if (thread.archivedAt !== null) { | ||
| unarchiveThread(deps.db, deps.hub, thread.id); |
There was a problem hiding this comment.
🚨 slopcop/review — Restore changes archive state before it validates the request.
The later destroying, missing-environment, unmanaged, and non-writable checks can return 409 after this call unarchives the thread. The restore path also does not send the provider unarchive command that the normal unarchive route sends. Validate first, then use one shared unarchive operation on successful restore paths.
| sourcePath: source.path, | ||
| baseBranch: storedBaseBranchNameToSpec(destroyed.baseBranch), | ||
| workspaceProvisionType: "managed-worktree", | ||
| ...(destroyed.branchName ? { branchName: destroyed.branchName } : {}), |
There was a problem hiding this comment.
🚨 slopcop/review — Restore can select the last polled branch instead of the managed branch.
callEnvironmentWorkspaceStatus writes the current checkout into environment.branchName. If a user checked out main, this restore request selects main. The worktree add can then fail because another worktree owns main, or it can restore the thread onto the shared default branch. Persist the managed branch identity separately and restore from that stable value.
|
🚨 SLOP COP 🚨 · End-to-end review complete on the top pull request. I created a managed thread, archived it, and confirmed that Undo revived the retiring worktree. I then let cleanup destroy the worktree. The browser showed Restore environment, and that action created a new managed worktree. The thread returned to idle without a new agent turn. |
SawyerHood
left a comment
There was a problem hiding this comment.
🚨 SLOP COP 🚨 · review
Review complete for stack PR #1016. I posted five inline findings.
- Medium: Restore uses a mutable, polled branch value instead of a stable managed branch identity.
- Medium: Restore unarchives before validation and does not synchronize provider archive state.
- Medium: The restore route omits the shared public-thread guard.
- Medium: A partial Undo failure leaves successful unarchives absent from the sidebar.
- Low: Environment metadata writes can extend the retirement grace clock.
The architecture scan found no duplicate restore service. The new query uses an existing suitable index. The daemon protocol version increased correctly.
I tested the live product at the pull request head. Archive Undo preserved the worktree during grace. Restore created a new worktree after destruction and returned the thread to idle.
Focused local suites passed 241 tests. Six package type checks passed. All current GitHub checks pass.
I submitted a comment review only. I did not approve or request changes.
9f70124 to
d07b36b
Compare
685460b to
ed16cd6
Compare
Problem
Archiving the last live thread in a managed environment immediately destroyed its worktree. An accidental archive could lose uncommitted work, and a thread whose environment was already destroyed ended at a read-only dead end.
What changed
Lossless archive undo
retiringformanagedEnvironmentRetireGraceMs(10 seconds by default) before destruction.updatedAt.retire.cancelledtransition and preserves the intact worktree, including uncommitted work.Continue in a new thread after destruction
Destroyed environments remain terminal, and the source thread remains archived and read-only. Its banner offers Continue in new thread after cleanup finishes.
The handoff uses the ordinary new-thread flow:
Continue from @thread:<id>as a rich thread mention;Committed work survives because the old branch remains in the source repository. Uncommitted and untracked work cannot be recovered after destruction.
Recovery fails closed if the original host, project source, or branch is unavailable instead of silently falling back. An explicit project/environment/branch change exits recovery mode. Unmanaged or branchless environments do not show the CTA.
Boundaries
destroyedremains a terminal lifecycle state.Testing
Design notes:
plans/environment-archive-grace-period-and-handoff.md.