Skip to content

Allow archiving a thread after its environment row is pruned - #2042

Merged
SawyerHood merged 2 commits into
mainfrom
bb/fix-1924-archiving-a-thread-fails-with-409-once-thr_cwifvke6pv
Aug 20, 2026
Merged

Allow archiving a thread after its environment row is pruned#2042
SawyerHood merged 2 commits into
mainfrom
bb/fix-1924-archiving-a-thread-fails-with-409-once-thr_cwifvke6pv

Conversation

@SawyerHood

Copy link
Copy Markdown
Collaborator

What was wrong

A thread can never be archived once its environment row is gone. pruneDestroyedEnvironments hard-deletes destroyed environment rows after 7 days with no live-thread guard. threads.environment_id is ON DELETE SET NULL, so a still-unarchived thread silently loses its pointer. POST /threads/:id/archive and /archive-all then call requireThreadHostCommandEnvironment, which throws 409 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_VERSION bump.

  • thread-command-environment.ts: add resolveThreadHostCommandEnvironment. It returns null for a null pointer and still throws for a dangling non-null id.
  • thread-archive.ts: ArchiveThreadWithLifecycleEffectsArgs.environment is nullable. Skip the runtime stop when null. Hidden forks and archiveThreadAndChildren use the resolver; only non-null environments enter affectedEnvironmentIds.
  • routes/threads/actions.ts: routes.archive uses the resolver. routes.stop uses 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

  • New test apps/server/test/threads/archive-pruned-environment.test.ts seeds a thread, marks its environment destroyed 8 days ago, runs pruneDestroyedEnvironments, and archives via /archive and /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 is internal-skill-trees.test.ts (file mode 420 vs 436, umask) and fails identically without this change.

Fixes #1924

AGENT GENERATED: by Claude Opus 5

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>
@SawyerHood
SawyerHood marked this pull request as ready for review August 20, 2026 17:23
@bb-slop-cop

bb-slop-cop Bot commented Aug 20, 2026

Copy link
Copy Markdown

🚨 SLOP COP 🚨 · review

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;

@bb-slop-cop bb-slop-cop Bot Aug 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 slopcop/reviewBlocking: 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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@bb-slop-cop bb-slop-cop Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 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>
@SawyerHood
SawyerHood merged commit 8e37200 into main Aug 20, 2026
22 of 23 checks passed
@SawyerHood
SawyerHood deleted the bb/fix-1924-archiving-a-thread-fails-with-409-once-thr_cwifvke6pv branch August 20, 2026 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Archiving a thread fails with 409 once its environment row has been pruned

1 participant