Summary
After a local unmanaged thread failed provisioning while the Host was disconnected, retrying the same project/source path and then reconnecting the Host left one environment ready while the other thread failed with a raw SQLite error:
UNIQUE constraint failed: environments.project_id, environments.host_id, environments.path
Observed behavior and source inspection suggest that two initially pathless provisioning environments can race when their successful results later record the same concrete workspace path. The later completion should reuse/attach to the existing environment, or fail with a stable domain error, rather than surface a database constraint message.
Versions and environment
- bb 0.42.1, macOS desktop app
- macOS 26.6 (25G72), Apple Silicon
- Local Host, unmanaged workspace, standard project with a local-path source
- Source inspected at current
main: 06aeaa994942ae7527dc49d2268c1f801e8542a0
- Provider choice is not relevant; the failure occurs during environment provisioning, before agent work starts
Steps to reproduce
This was reproduced once with the following timing-dependent sequence on 0.42.1:
- Create a standard project whose source is an existing local directory.
- Start a thread in that unmanaged directory while the local Host connection is unavailable or drops during provisioning.
- Observe the first thread fail with
Host is not connected.
- Before all provisioning work from that attempt has fully settled, retry by creating another thread for the same project, Host, and source directory.
- Reconnect/restart the Host so queued/in-flight provisioning results settle.
- Observe one environment become
ready; the other thread fails with the raw unique-constraint error.
I did not deliberately recreate the race against main, because doing so creates additional failed environment records. I inspected the current main completion path instead; it still writes the discovered path without handling this unique conflict.
Expected vs actual
Actual first failure event:
{"code":"thread_provisioning_failed","message":"Provisioning thread failed","detail":"Host is not connected"}
Actual retry failure event:
{"code":"thread_provisioning_failed","message":"Provisioning thread failed","detail":"UNIQUE constraint failed: environments.project_id, environments.host_id, environments.path"}
Expected:
- Provisioning completion is idempotent for a project/Host/path tuple, or
- the second completion attaches/reuses the already-ready environment when safe, or
- the thread receives a stable domain error such as environment_already_attached.
The raw SQLite constraint message should not be exposed as the user-facing provisioning failure.
Evidence
The affected project contained these environment states after the race (workspace path redacted):
environment path status provision type
env_e72m2rg9fh NULL error unmanaged
env_zw7f9huykk NULL error unmanaged
env_8qtmiqs3sn <workspace-path> ready unmanaged
env_yazb6h4p4a NULL destroyed managed-worktree
Affected local thread IDs:
thr_ktsk6jq7vv: Host is not connected
thr_76e29ymjc6: unique constraint failure
Current main source observations:
- Thread creation reuses an environment only when its concrete path is already known:
|
function existingUnmanagedEnvironmentIntentByHostPath( |
|
deps: ThreadCreateDeps, |
|
args: ExistingUnmanagedEnvironmentIntentByHostPathArgs, |
|
): ExistingUnmanagedEnvironmentIntentResult | null { |
|
const existing = findProjectEnvironmentByHostPath( |
|
deps.db, |
|
args.request.projectId, |
|
args.hostId, |
|
args.path, |
|
); |
|
if (!existing) { |
|
return null; |
|
} |
|
|
|
if (!args.branch) { |
|
if (existing.status === "ready" || existing.status === "provisioning") { |
|
return { |
|
environmentId: existing.id, |
|
intent: { |
|
type: "reuse", |
|
environmentId: existing.id, |
|
}, |
|
}; |
- The database intentionally enforces uniqueness for
(project_id, host_id, path):
|
(table) => [ |
|
uniqueIndex("environments_project_host_path_idx").on( |
|
table.projectId, |
|
table.hostId, |
|
table.path, |
|
), |
- A successful Host result records the discovered path directly, without translating or reconciling a unique conflict:
|
if (args.report.ok) { |
|
recordProvisionedEnvironmentWorkspace( |
|
args.deps.db, |
|
args.deps.hub, |
|
args.command.environmentId, |
|
{ |
|
path: args.report.result.path, |
|
isGitRepo: args.report.result.isGitRepo, |
|
isWorktree: args.report.result.isWorktree, |
|
branchName: args.report.result.branchName, |
|
defaultBranch: args.report.result.defaultBranch, |
|
...resolveProvisionedEnvironmentBranchMetadata(args.command), |
|
}, |
|
); |
|
const provisionedOutcome = |
|
applyLoggedEnvironmentLifecycleEventInTransaction(args.deps, { |
|
environmentId: args.command.environmentId, |
|
event: { type: "provision.succeeded" }, |
- The underlying metadata update performs a direct update:
|
export function recordProvisionedEnvironmentWorkspace( |
|
db: EnvironmentWriteConnection, |
|
notifier: DbNotifier, |
|
id: string, |
|
input: RecordProvisionedEnvironmentWorkspaceInput, |
|
) { |
|
return updateEnvironmentMetadataRecord(db, notifier, id, { |
|
path: input.path, |
|
isGitRepo: input.isGitRepo, |
|
isWorktree: input.isWorktree, |
|
branchName: input.branchName, |
|
defaultBranch: input.defaultBranch, |
|
...(input.baseBranch !== undefined ? { baseBranch: input.baseBranch } : {}), |
|
...(input.mergeBaseBranch !== undefined |
|
? { mergeBaseBranch: input.mergeBaseBranch } |
|
: {}), |
|
}); |
What I ruled out
- Not database corruption: exactly one environment owns the concrete path and is
ready; the failed records remain pathless.
- Not an agent/provider failure: both errors occurred during thread/environment provisioning.
- Not a failure of the later workflow run: a separate thread completed successfully after Host recovery.
- No matching open or closed issue was found by searching for the exact constraint text and for environment provisioning/retry terms.
- Not confirmed by a second runtime reproduction on
main; only the still-unhandled source path was verified at 06aeaa9.
Suggested priority and effort
Medium — timing-dependent and recoverable by using the surviving ready environment, but retries after a transient Host disconnect can create failed threads and expose an internal database error. Likely medium effort because safe reuse must account for thread/environment lifecycle ownership, not merely catch and suppress the exception.
AGENT GENERATED
Summary
After a local unmanaged thread failed provisioning while the Host was disconnected, retrying the same project/source path and then reconnecting the Host left one environment ready while the other thread failed with a raw SQLite error:
UNIQUE constraint failed: environments.project_id, environments.host_id, environments.pathObserved behavior and source inspection suggest that two initially pathless provisioning environments can race when their successful results later record the same concrete workspace path. The later completion should reuse/attach to the existing environment, or fail with a stable domain error, rather than surface a database constraint message.
Versions and environment
main:06aeaa994942ae7527dc49d2268c1f801e8542a0Steps to reproduce
This was reproduced once with the following timing-dependent sequence on 0.42.1:
Host is not connected.ready; the other thread fails with the raw unique-constraint error.I did not deliberately recreate the race against
main, because doing so creates additional failed environment records. I inspected the currentmaincompletion path instead; it still writes the discovered path without handling this unique conflict.Expected vs actual
Evidence
The affected project contained these environment states after the race (workspace path redacted):
Affected local thread IDs:
thr_ktsk6jq7vv:Host is not connectedthr_76e29ymjc6: unique constraint failureCurrent
mainsource observations:bb/apps/server/src/services/threads/thread-create.ts
Lines 368 to 390 in 06aeaa9
(project_id, host_id, path):bb/packages/db/src/schema.ts
Lines 469 to 474 in 06aeaa9
bb/apps/server/src/services/environments/environment-provisioning-internal.ts
Lines 588 to 605 in 06aeaa9
bb/packages/db/src/data/environments.ts
Lines 288 to 304 in 06aeaa9
What I ruled out
ready; the failed records remain pathless.main; only the still-unhandled source path was verified at06aeaa9.Suggested priority and effort
Medium — timing-dependent and recoverable by using the surviving ready environment, but retries after a transient Host disconnect can create failed threads and expose an internal database error. Likely medium effort because safe reuse must account for thread/environment lifecycle ownership, not merely catch and suppress the exception.