Refuse queued messages for threads whose environment is gone - #2043
Conversation
POST /threads/:id/queued-messages only checked archived/stopping/deleted and never applied the gone-environment rule the direct send path uses. A thread whose managed worktree was destroyed still answered 201, the message sat in the queue forever, and the auto-send sweep failed every 10 s. Apply goneThreadEnvironmentDetails in createQueuedMessageForThread, refuse threads that ran before but lost their environment row, and label destroyed worktrees "Destroyed" instead of "Provisioning" in formatEnvironmentDisplay. Fixes #1789 Co-Authored-By: Claude <noreply@anthropic.com>
|
🚨 SLOP COP 🚨 · I am SlopCop. I am reviewing this pull request for security, code quality, performance, architecture, and end-to-end behavior. |
| // A destroyed managed worktree also has no path. It is gone, not being set | ||
| // up, so it must not read as "Provisioning" (#1789). | ||
| const isGoneDisplay = | ||
| environment.status === "destroying" || environment.status === "destroyed"; |
There was a problem hiding this comment.
🚨 slopcop/review — The formatter does not show the lifecycle state consistently.
A custom name replaces Destroyed later in this function. Also, destroying maps to Destroyed before destruction completes.
Use a separate lifecycle label. Another option is to define and test the intended name priority and state text.
| ): Promise<ThreadQueuedMessage> { | ||
| const { payload, thread } = args; | ||
| ensureThreadIsWritable(thread); | ||
| ensureQueuedMessageEnvironmentIsNotGone(deps, thread); |
There was a problem hiding this comment.
🚨 slopcop/review — Existing queue rows can still enter the permanent retry loop.
This check protects only new rows. Queue rows survive archive. Unarchive makes them sweep candidates after destruction.
Each sweep then fails and logs again. Handle existing rows during cleanup, or skip them without repeated retries.
Add an archive, destroy, unarchive, and sweep regression test.
This check also occurs before two waits. An archive can change both records before the later insert uses their old state.
Recheck both records inside the same immediate transaction as the insert.
There was a problem hiding this comment.
🚨 SLOP COP 🚨 · review
Plain-English summary: This change stops new queue messages when a thread's work area is being removed or has disappeared. It also marks that work area as Destroyed.
I found two correctness blockers.
- Existing queue rows still survive archive and destruction. Unarchive makes them sweep candidates again, so the permanent failure loop remains.
- The new lifecycle check occurs before two waits. An archive can change the records before the queue insert uses their old state.
I also found two display gaps. A custom environment name hides Destroyed, and destroying displays as Destroyed before completion.
The null-environment path reads provider history twice. The second read can repeat a long synchronous event scan.
Security review found no separate security issue. The 409 response contains only fixed lifecycle values.
The route helper repeats policy from thread-command-environment.ts. A transaction-based queue admission service would keep one policy and close the race.
Validation passed:
- The server regression test passed all four cases.
- The Core UI test passed all 11 cases.
- Server and Core UI type checks passed.
- The source app browser test showed
Environment archivedandDestroyed. - The browser queue request returned 409 with
thread_environment_unavailable. git diff --checkpassed.
The independent GPT-5.6 gate confirmed the blockers. I used a comment review, not an approval or request-changes review.
…ironments in the sweep SlopCop review on #2043: - Pre-existing queued rows survived archive -> destroy -> unarchive and kept the 10 s sweep failing. listIdleThreadsWithQueuedMessages now joins environments and skips destroying/destroyed ones. - The lifecycle check ran before two awaits. Admission (writable + environment) now runs on fresh rows inside the same immediate transaction as the insert, and reads the provider thread id once. - formatEnvironmentDisplay says Destroying for destroying and Destroyed for destroyed. Co-Authored-By: Claude <noreply@anthropic.com>
|
Addressed the SlopCop findings in c4c6df9:
|
What was wrong
POST /threads/:id/queued-messages(createQueuedMessageForThreadinapps/server/src/routes/threads/actions.ts) only checked archived/stopping/deleted. It never applied the gone-environment rule (goneThreadEnvironmentDetails) that the direct send path applies throughrequireThreadCommandEnvironment. A thread whose managed worktree was destroyed (archive → grace window → destroy → unarchive) keptstatus: idle, answered201with a queued-message id, and the message could never drain: the auto-send hit the same409 thread_environment_unavailableinternally and the 10 s sweep retried forever. The CLI also labelled the destroyed worktree "Provisioning".Issue: #1789. Report: https://get-bb.github.io/reports/issues/1789.html
What changed
apps/server/src/routes/threads/actions.ts:createQueuedMessageForThreadadmits the message inside the sameBEGIN IMMEDIATEtransaction as the insert, on the freshly loaded thread row (admitQueuedMessage): writable check, environment check, and one provider-thread-id read. Adestroying/destroyedenvironment returns the same409 thread_environment_unavailableas the send path. A thread withenvironmentId === nullthat already has a provider thread id (the row was pruned after destroy) returns409with reasonnever_attached, again the same as send. A thread that has not run yet and has no environment still accepts queued messages, because that is how messages wait for provisioning.packages/db/src/data/queued-thread-messages.ts:createQueuedThreadMessageInTransactionfor caller-owned transactions;listIdleThreadsWithQueuedMessagesjoinsenvironmentsand skipsdestroying/destroyed, so queued rows that survived archive → destroy → unarchive no longer fail the sweep every 10 s.packages/core-ui/src/environment-display.ts:formatEnvironmentDisplaylabels adestroyingenvironment "Destroying" and adestroyedone "Destroyed" instead of "Provisioning". This changesbb thread showand app metadata labels.HOST_DAEMON_PROTOCOL_VERSIONis unchanged. No new routes or CLI flags.Not done here: the report also suggests a
runtime.environmentStatus/canRunfield on the thread response. That is an API contract addition and is left as a follow-up.How you verified
apps/server/test/public/public-thread-queue-gone-environment.test.ts(from the report, extended): 3 of 5 tests fail before the route fix (expected 201 to be 409); the sweep test (realarchiveThread→retire.requested→destroy.started→destroy.completed→POST /unarchive) fails before the query change; all 5 pass after. One test is a control that a never-run, environment-less thread still gets201.packages/core-ui/test/environment-display.test.ts: new case fordestroying/destroyedlabels.pnpm exec turbo run test typecheck --filter=@bb/db --filter=@bb/server --filter=@bb/core-ui --filter=@bb/cli: typecheck clean; db 406/406; core-ui 17/17; cli 452/452; server 1798/1799. The one failure istest/internal/internal-skill-trees.test.ts(file mode420vs436), a umask difference on this machine, unrelated to this change.Fixes #1789