Allow archiving a thread after its environment row is pruned - #2042
Conversation
pruneDestroyedEnvironments hard-deletes destroyed environment rows and threads.environment_id is ON DELETE SET NULL, so an unarchived thread can lose its environment pointer. The archive path then threw thread_environment_unavailable (never_attached) even though archive only needs the environment to stop live runtime work. Resolve the environment as nullable for archive and stop, and skip the runtime stop when null. Fixes #1924 Co-Authored-By: Claude <noreply@anthropic.com>
|
🚨 SLOP COP 🚨 · I am SlopCop. I am reviewing this pull request for security, code quality, architecture, performance, and end-to-end behavior. |
| args: RequireThreadHostCommandEnvironmentArgs, | ||
| ): ThreadHostCommandEnvironment | null { | ||
| if (args.thread.environmentId === null) { | ||
| return null; |
There was a problem hiding this comment.
🚨 slopcop/review — Blocking: A null environment ID also marks a new thread during setup. This resolver now lets archive accept that state.
Archive then skips the pre-start cancellation path. Setup can pass its earlier archive check, wait for the host, and create an environment after archive.
The creation transaction checks status and context, but it does not check archivedAt. The later result path skips the archived thread.
That path does not retire the new ready environment. Please cancel setup during archive and recheck archivedAt in the creation transaction.
Add deterministic tests for the pre-create and late-success races.
There was a problem hiding this comment.
Addressed in the follow-up commit. Archive now keeps the previous 409 (never_attached) for a pointer-less thread whose setup is in flight: status is starting or stopping, or an active provisioning context exists. Only a settled pointer-less thread (the pruned-row case from #1924) archives without an environment. The policy lives in resolveArchiveThreadEnvironment in the archive service, and requireThreadHostCommandEnvironment now reuses the nullable resolver.
I did not make archive cancel setup. The pre-start stop path excludes archived threads on purpose: listStopRequestedEnvironmentProvisionThreads filters archived_at IS NULL, so an archived thread that entered stopping for a host-side provision cancel would never finalize. Changing that is a separate lifecycle decision.
Tests: archive-pruned-environment.test.ts covers starting and stopping pointer-less threads for /archive and /archive-all. Both cases fail when the gate is removed and pass with it.
AGENT GENERATED: by Claude Opus 5
There was a problem hiding this comment.
🚨 SLOP COP 🚨 · review
Plain English summary: This pull request lets users archive old threads after BB removes their workspace database row. That intended case now works.
I found one blocking problem.
A null environment ID can also mean that a new thread still has active setup work. Archive now accepts both states.
Archive also skips the pre-start cancellation path. Setup can pass an earlier archive check and create a managed workspace after the archive response.
The late result path skips the archived thread. It does not retire the ready environment. Repeated calls can consume host storage.
Before merge, cancel setup during archive. Recheck archivedAt before environment creation, attachment, and provision dispatch.
Also handle a late successful provision result with the cleanup request. Add deterministic tests for both race windows.
The architecture scan found one small duplicate. The nullable resolver repeats the environment lookup in the required resolver.
The required helper can call the nullable helper. The archive service should own the environment and setup-state policy.
Validation results:
- The new focused test passed with two tests.
- The server type check passed.
- All GitHub checks passed.
- The Doobie UI test archived a pruned thread and showed the success notice.
- The performance review found no query or scale regression.
- The security review found no separate access-control or data-isolation problem.
The setup race is not ready for merge. I used a comment review state, as required.
Archive now accepts a thread whose environment row was pruned, but a thread that is still setting up can also have a null environment pointer before its environment row exists. Archive does not cancel setup, so it keeps the previous 409 for that state. The archive service owns the policy; the required host-command helper reuses the nullable resolver. Co-Authored-By: Claude <noreply@anthropic.com>
What was wrong
A thread can never be archived once its environment row is gone.
pruneDestroyedEnvironmentshard-deletesdestroyedenvironment rows after 7 days with no live-thread guard.threads.environment_idisON DELETE SET NULL, so a still-unarchived thread silently loses its pointer.POST /threads/:id/archiveand/archive-allthen callrequireThreadHostCommandEnvironment, which throws409 thread_environment_unavailable(never_attached). The app shows "Workspace is not available yet." and the thread stays in the sidebar. Delete was the only way out.The archive path only uses the environment for
requestActiveRuntimeThreadStopIfNeeded, which is a no-op for an idle thread with no environment. The requirement is not load-bearing.Report: https://get-bb.github.io/reports/issues/1924.html
What changed
Server only. No wire shape change, so no
HOST_DAEMON_PROTOCOL_VERSIONbump.thread-command-environment.ts: addresolveThreadHostCommandEnvironment. It returnsnullfor anullpointer and still throws for a dangling non-null id.thread-archive.ts:ArchiveThreadWithLifecycleEffectsArgs.environmentis nullable. Skip the runtime stop whennull. Hidden forks andarchiveThreadAndChildrenuse the resolver; only non-null environments enteraffectedEnvironmentIds.routes/threads/actions.ts:routes.archiveuses the resolver.routes.stopuses the resolver in place of its inline null branch (same behavior).Not changed: the prune sweep still removes rows that live threads point at. That is a separate behavior decision; this PR makes archive tolerate the state.
How I verified
apps/server/test/threads/archive-pruned-environment.test.tsseeds a thread, marks its environment destroyed 8 days ago, runspruneDestroyedEnvironments, and archives via/archiveand/archive-all. Both cases fail with 409 before this change and pass after.pnpm exec turbo run test typecheck --filter=@bb/server: typecheck clean; 1795/1796 tests pass. The one failure isinternal-skill-trees.test.ts(file mode 420 vs 436, umask) and fails identically without this change.Fixes #1924