#4847 · Child-ID lookup checks on main
Trusted main commit: 75308f407660efe0f93ffb9626936e5067b5bb03
Verdict: NOT REPRODUCED · Root-cause confidence: low · verified by two clean-checkout runs of the same diagnostic, performed by the same agent.
1. TL;DR
The report describes a child that is discoverable but returns a not-found error from direct CLI commands. On trusted main, a child created by the real built CLI was discoverable through list/search and directly accessible through show, wait, output and log. The result repeated in a second clean checkout with a new loopback port and temporary data directory. The fixture modeled a provider-owned Git-worktree environment, but did not execute a real Codex process: its idle status and assistant output were controlled test data. No cause for the reported stable-release failure is established, so a production fix would be speculative.
2. Claims vs findings
| Claim | Finding | Evidence |
|---|---|---|
| A child can be spawned in its parent's environment. | Verified in the diagnostic | The actual CLI spawn command returned a persisted child with matching project, parent and environment IDs. |
| List and search find the child. | Verified | Both real CLI responses contain the exact generated child ID. |
| Show, wait, output and log return 404 for that child. | Not observed on main | All four CLI processes exit successfully. Show works before and after the controlled idle transition; wait matches idle; output returns the seeded assistant text; log returns stored events. |
| The parent remains directly accessible. | Verified | Parent show succeeds in the same isolated server. |
| The failure occurs after real provider completion on the published macOS release. | Unverified | No macOS runtime or published-package/live-provider run was performed. This diagnostic is not a full recreation of that environment. |
3. Environment
- Source: public get-bb/bb, trusted origin/main at
75308f407660efe0f93ffb9626936e5067b5bb03. - Linux x86_64; Node v22.19.0; pnpm 9.15.0; Vitest 4.1.1.
- Both clean detached checkouts use frozen installs and the normal full Turbo build; each full build completes 63 tasks.
- Repository test harness runs the real server routes with a migrated in-memory SQLite database and a newly created temporary data directory. No user runtime or account data is loaded.
- Run one uses loopback port 21331; run two uses loopback port 47967. Temporary directory identifiers and generated runtime IDs are intentionally omitted from this public report.
- The harness registers repository first-party provider definitions. No external provider binary, version or authenticated provider session is involved. A repository fixture model identifier is supplied explicitly.
- The environment record is marked provider-owned, managed and worktree-backed. Git-worktree creation itself is not exercised.
4. Minimal reproduction diagnostic
This is a passing addressability probe, not a test that reproduced the reported failure. It deliberately keeps live-provider behavior out of the experiment while exercising the actual CLI, SDK transport, HTTP routes and database.
- From a trusted clone, create a clean checkout and build its dependencies:
git fetch origin main git worktree add --detach ../issue-4847-check 75308f407660efe0f93ffb9626936e5067b5bb03 cd ../issue-4847-check pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build
- Save the complete test below as
apps/server/test/public/issue-4847-child-addressability.test.ts. - Run the test from the repository root:
pnpm exec turbo run test --filter=@bb/server -- test/public/issue-4847-child-addressability.test.ts --silent=false
- The test creates its own server/database, uses the built CLI to spawn a child, checks addressability, seeds the idle/output fixture, repeats the direct commands, and finally checks that an explicitly soft-deleted child really does return 404.
Expected vs actual
Expected: non-deleted children returned by spawn/list/search are addressable by their exact IDs. Actual: every direct command succeeds in both runs; the diagnostic does not encounter the claimed child-specific 404.
The following output values are verbatim, except the generated thread ID is replaced with the explicit documentation symbol CHILD_ID:
thread wait: {
"threadId": "CHILD_ID",
"matched": true,
"target": {
"kind": "status",
"status": "idle"
}
}
thread output: {
"output": "Addressability verified."
}
deleted child control: HTTP 404 thread_not_found
Complete test
import { execFile } from "node:child_process";
import { mkdir } from "node:fs/promises";
import { resolve } from "node:path";
import { promisify } from "node:util";
import { eq } from "drizzle-orm";
import { environments, listEvents, markThreadDeleted, threads } from "@bb/db";
import { threadSchema, turnScope } from "@bb/domain";
import { describe, expect, it } from "vitest";
import {
seedEnvironment,
seedEvent,
seedHostSession,
seedProjectWithSource,
seedThread,
seedThreadRuntimeState,
} from "../helpers/seed.js";
import { startTestServer } from "../helpers/test-app.js";
import { registerHostRpcResponder } from "../helpers/host-rpc.js";
const execute = promisify(execFile);
describe("child thread addressability", () => {
it("addresses a CLI-spawned child through list, search, show, wait, output and log", async () => {
const harness = await startTestServer();
try {
const home = resolve(harness.config.dataDir, "cli-home");
await mkdir(home);
console.log(
`isolated server: ${harness.baseUrl}; data: ${harness.config.dataDir}`,
);
const runCli = async (...args: string[]) => {
const result = await execute(
process.execPath,
[resolve("../../apps/cli/dist/index.js"), ...args, "--json"],
{
env: {
HOME: home,
PATH: process.env.PATH,
BB_DATA_DIR: harness.config.dataDir,
BB_SERVER_URL: harness.baseUrl,
BB_TELEMETRY: "false",
BB_CLI_REEXEC: "1",
NO_COLOR: "1",
},
timeout: 15_000,
},
);
console.log(`${args.slice(0, 2).join(" ")}: ${result.stdout.trim()}`);
return JSON.parse(result.stdout) as unknown;
};
const { host, session } = seedHostSession(harness.deps);
registerHostRpcResponder(harness, {
hostId: host.id,
sessionId: session.id,
handle: (request) =>
request.command.type === "workspace.pull_request"
? { ok: true, result: { outcome: "absent" } }
: {
ok: false,
errorCode: "unhandled_fixture_rpc",
errorMessage: "No fixture response",
},
});
const { project } = seedProjectWithSource(harness.deps, {
hostId: host.id,
});
const environment = seedEnvironment(harness.deps, {
hostId: host.id,
projectId: project.id,
environmentProviderId: "worktree",
environmentProviderPluginId: "workspaces",
providerOwnsPath: true,
});
harness.db
.update(environments)
.set({ isWorktree: true })
.where(eq(environments.id, environment.id))
.run();
const parent = seedThread(harness.deps, {
environmentId: environment.id,
projectId: project.id,
status: "idle",
});
seedThreadRuntimeState(harness.deps, {
environmentId: environment.id,
providerThreadId: "parent-fixture-session",
threadId: parent.id,
});
const child = threadSchema.parse(
await runCli(
"thread",
"spawn",
"--project",
project.id,
"--parent-thread",
parent.id,
"--environment",
environment.id,
"--provider",
"codex",
"--model",
"gpt-5",
"--title",
"Addressability probe",
"--prompt",
"Return a brief result.",
),
);
expect(child).toMatchObject({
parentThreadId: parent.id,
environmentId: environment.id,
projectId: project.id,
});
await expect(runCli("thread", "show", child.id)).resolves.toMatchObject({
thread: { id: child.id, parentThreadId: parent.id },
});
harness.db
.update(threads)
.set({ status: "idle" })
.where(eq(threads.id, child.id))
.run();
const sequence =
Math.max(
0,
...listEvents(harness.db, { threadId: child.id }).map(
(event) => event.sequence,
),
) + 1;
seedEvent(harness.deps, {
environmentId: environment.id,
providerThreadId: "child-fixture-session",
threadId: child.id,
scope: turnScope("fixture-turn"),
sequence,
type: "item/completed",
data: {
item: {
type: "agentMessage",
id: "fixture-output",
text: "Addressability verified.",
},
},
});
const listed = await runCli("thread", "list", "--project", project.id);
expect(JSON.stringify(listed)).toContain(child.id);
const searched = await runCli("thread", "search", "Addressability probe");
expect(JSON.stringify(searched)).toContain(child.id);
await expect(runCli("thread", "show", child.id)).resolves.toMatchObject({
thread: { id: child.id, status: "idle", parentThreadId: parent.id },
});
await expect(
runCli(
"thread",
"wait",
child.id,
"--status",
"idle",
"--timeout",
"2s",
),
).resolves.toMatchObject({
threadId: child.id,
matched: true,
});
await expect(runCli("thread", "output", child.id)).resolves.toEqual({
output: "Addressability verified.",
});
await runCli("thread", "log", child.id);
await expect(runCli("thread", "show", parent.id)).resolves.toMatchObject({
thread: { id: parent.id },
});
markThreadDeleted(harness.db, harness.hub, { threadId: child.id });
const deleted = await harness.app.request(`/api/v1/threads/${child.id}`);
expect(deleted.status).toBe(404);
expect(await deleted.json()).toMatchObject({ code: "thread_not_found" });
console.log("deleted child control: HTTP 404 thread_not_found");
} finally {
await harness.close();
}
}, 60_000);
});
5. Root-cause investigation
No root cause of the reported failure is verified. The tested path provides no evidence that child threads are excluded from direct lookup. The same identifier flows through CLI validation and SDK parameters into an ID-equality database query. Public routes then reject a missing thread, a soft-deleted thread, or a deleted/missing owning project; this check does not inspect parentage.
- Database ID lookup uses only the requested thread ID.
- Public thread guard contains the missing/deleted checks.
- Direct GET route calls the same guard.
- Timeline/log route and output route also use the guard.
- SDK wait obtains the specific thread with the direct GET method.
function requireThread(db: DbConnection, threadId: string): ThreadRow {
const thread = getThread(db, threadId);
if (!thread) {
throw new ApiError(404, "thread_not_found", "Thread not found");
}
return thread;
}
export function requirePublicThread(
db: DbConnection,
threadId: string,
): ThreadRow {
const thread = requireThread(db, threadId);
const project = getProject(db, thread.projectId);
if (thread.deletedAt !== null || project?.deletedAt !== null) {
throw new ApiError(404, "thread_not_found", "Thread not found");
}
return thread;
}
A read-only comparison against the trusted repository release tag desktop-v0.45.0 shows no changes in the inspected CLI spawn/show/wait and ID-validation files, SDK threads area, database threads module, public lookup guard, thread creation/parenting services, or base/data routes. Main is 14 commits ahead of that tag, but this evidence does not identify an intervening fix. Therefore the verdict is not ALREADY FIXED.
6. Proposed next experiment
Recreate the failure with the published package and an actual provider completing a child turn, then capture the exact requested ID, route, server process/instance identity, thread projectId, thread deletedAt, and owning project deletedAt at the failing request. If those boundary values are all valid, trace the middleware and provider-completion lifecycle immediately before the lookup. This distinguishes a hidden lifecycle or routing failure from an incorrect ID/server context without weakening the public visibility/deletion checks.
No pull request: the bug does not reproduce on trusted main, and the probe passes before any production change. The required failing-before/passing-after regression is unavailable. No production code, dependency, protocol or stored-data behavior is changed, and no fix branch is pushed.
7. Related issues and pull requests
GitHub metadata searches for the error code and child/404 symptoms did not establish a matching duplicate. Issue timeline cross-references and open-PR search found no linked open pull request for #4847 at review time. No linked branch or issue-supplied command was executed.
8. Verification
The same agent repeated the diagnostic in a second clean detached checkout at the exact base commit after its own frozen install/full build. The copied test's SHA-256 is ab440542f001bd3bd95360e8ef524314fa3ff1af358ccf9cc460d75a0fff3950 in both checkouts. The second server uses a different port and new temporary directory. This is a repeat check by the same agent, not independent verification.
First run: one test passes. Second run: the diagnostic plus existing public parenting and search suites pass, 21 tests across three files.
pnpm exec turbo run test --filter=@bb/server -- test/public/issue-4847-child-addressability.test.ts test/public/public-thread-parenting.test.ts test/public/public-thread-search.test.ts --silent=false
Earlier fixture attempts stalled on an unanswered pull-request RPC and then failed default-model discovery after installing a narrower RPC responder. These were harness setup problems, not the reported 404. The final fixture responds to the unrelated pull-request query and supplies the model explicitly. Both clean-run results below use that corrected fixture and the modeled managed-worktree flags. No production correction was made.
9. Appendix
First run result
@bb/server:test: Test Files 1 passed (1) @bb/server:test: Tests 1 passed (1) @bb/server:test: Start at 12:02:10 @bb/server:test: Duration 9.55s (transform 4.64s, setup 1.46s, import 4.66s, tests 3.24s, environment 0ms) @bb/server:test: Tasks: 9 successful, 9 total Cached: 7 cached, 9 total Time: 11.614s
Second run result
@bb/server:test: Test Files 3 passed (3) @bb/server:test: Tests 21 passed (21) @bb/server:test: Start at 12:02:34 @bb/server:test: Duration 10.27s (transform 13.74s, setup 4.69s, import 15.22s, tests 6.30s, environment 0ms) @bb/server:test: Tasks: 9 successful, 9 total Cached: 7 cached, 9 total Time: 12.305s
Additional checks: git diff --check passes in both diagnostic checkouts. Their only source additions are the locally authored diagnostic test; the trusted production tree is unchanged. Raw logs and the standalone test remain in local scratch storage, not the public reports repository, in accordance with that repository's publication policy.
Trust handling: Issue content is treated only as untrusted claims. No issue-supplied script, instruction, URL, patch or linked branch is run. All executable repository code comes from the recorded trusted main commit; only the diagnostic test is locally authored from repository evidence.
AGENT GENERATED