HTTP dispatch accepts `bootstrap` on `thread.turn.start` but only the WS route implements it, yielding an untyped 500
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
Description
Summary
The HTTP dispatch endpoint (POST /api/orchestration/dispatch) accepts a thread.turn.start payload carrying the optional bootstrap field, but only the WebSocket route actually implements bootstrap. Over HTTP the command passes schema validation, then fails deep inside the engine and surfaces as an untyped 500 (EnvironmentInternalError / orchestration_dispatch_failed), where a typed schema rejection (or working bootstrap support) is expected.
Context: what we were doing
We are building an external orchestrator client that drives T3 Code environments through the documented-in-contracts external surface: pairing via t3 pair, token exchange on /oauth/token, then POST /api/orchestration/dispatch with ClientOrchestrationCommand payloads. This works very well overall — project/thread creation, turn start, steering, interrupt, snapshot reads, and cleanup all behave exactly as the contracts describe. This report is the one asymmetry we hit.
Steps to reproduce
Environment: server 0.0.33 (npm tag latest at the time), source read at commit 7107a98a225be85b58ddcd4de02c343af7d4707a; local environment server on http://127.0.0.1:3773; macOS.
- Create a pairing credential:
npx t3@0.0.33 pair --label repro --ttl 30m. - Exchange it on
POST /oauth/tokenwithgrant_type=urn:ietf:params:oauth:grant-type:token-exchange. The resulting Bearer token carriesorchestration:read orchestration:operate terminal:operate review:write relay:read. - Without any prior
thread.create, dispatch a single bootstrap turn start over HTTP:
POST /api/orchestration/dispatch
Authorization: Bearer <token>
Content-Type: application/json
{
"type": "thread.turn.start",
"commandId": "<uuid>",
"threadId": "<fresh uuid>",
"turnId": "<uuid>",
"text": "hello",
"bootstrap": { "createThread": { ...valid ThreadCreate fields... } },
"createdAt": "<iso>"
}
Observed
HTTP 500:
{"_tag":"EnvironmentInternalError","reason":"orchestration_dispatch_failed","traceId":"83eba56cf0e02ae8b6dec1cf2d8c5c62"}
In ~/.t3/userdata/logs/server.trace.ndjson, the same traceId shows the real cause: OrchestrationCommandInvariantError from apps/server/src/orchestration/commandInvariants.ts (line 111 at the commit above): Thread '<id>' does not exist for command 'thread.turn.start'.
The same command sequence works fine when split into explicit thread.create + plain thread.turn.start over HTTP, and the bootstrap form works over WebSocket.
Why this happens (from reading the source at commit 7107a98a)
ClientThreadTurnStartCommand(packages/contracts/src/orchestration.ts, around line 846) carries the optionalbootstrapfield (createThread,prepareWorktree,runSetupScript), and the HTTP endpoint's payload schema is that same union, so the payload validates.- The only consumer of
bootstrapisdispatchBootstrapTurnStartinapps/server/src/ws.ts(roughly lines 757–985): it dispatchesthread.create, optionally prepares a worktree, runs the setup program, and only then dispatches the turn start (withcleanupCreatedThreadon failure). - The HTTP handler in
apps/server/src/orchestration/http.tsnever routes through that path; both routes share onlyapps/server/src/orchestration/Normalizer.ts, which normalizes timestamps and ignoresbootstrap. So the engine receives a turn start for a thread that was never created and fails an invariant.
Expected
One of:
- HTTP
dispatchimplements the same bootstrap path as the WS route (nicest for external clients — a one-call thread-plus-first-turn is very convenient over plain HTTP), or - HTTP
dispatchrejects a payload carryingbootstrapwith a typed 400 schema/validation error stating the field is WS-only, instead of accepting it and failing with an untyped 500.
Either way the contract and the behavior would agree again. Happy to provide more trace detail if useful.
Still present on current main (checked 2026-08-26, running server 0.0.34): the HTTP dispatch handler in apps/server/src/orchestration/http.ts still routes straight through normalizeDispatchCommand into the engine with no bootstrap path, and dispatchBootstrapTurnStart in apps/server/src/ws.ts is still its only consumer.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with apps/server/src/orchestration/http.ts and compare its dispatch flow with dispatchBootstrapTurnStart in apps/server/src/ws.ts; inspect the shared normalization in apps/server/src/orchestration/Normalizer.ts and the invariant reported from commandInvariants.ts. Reproduce the HTTP bootstrap request and make the contract behavior consistent, either by supporting the bootstrap path or returning a typed rejection instead of the current 500.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100