pingdotgg / pingdotgg/t3code

HTTP dispatch accepts `bootstrap` on `thread.turn.start` but only the WS route implements it, yielding an untyped 500

Open
#8,319 0 comments 0 reactions 0 assignees View on GitHub

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.

  1. Create a pairing credential: npx t3@0.0.33 pair --label repro --ttl 30m.
  2. Exchange it on POST /oauth/token with grant_type=urn:ietf:params:oauth:grant-type:token-exchange. The resulting Bearer token carries orchestration:read orchestration:operate terminal:operate review:write relay:read.
  3. 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 optional bootstrap field (createThread, prepareWorktree, runSetupScript), and the HTTP endpoint's payload schema is that same union, so the payload validates.
  • The only consumer of bootstrap is dispatchBootstrapTurnStart in apps/server/src/ws.ts (roughly lines 757–985): it dispatches thread.create, optionally prepares a worktree, runs the setup program, and only then dispatches the turn start (with cleanupCreatedThread on failure).
  • The HTTP handler in apps/server/src/orchestration/http.ts never routes through that path; both routes share only apps/server/src/orchestration/Normalizer.ts, which normalizes timestamps and ignores bootstrap. So the engine receives a turn start for a thread that was never created and fails an invariant.

Expected

One of:

  1. HTTP dispatch implements 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
  2. HTTP dispatch rejects a payload carrying bootstrap with 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.