Changelog
v0.9 tbot serve waits for the mod by default¶
- [feature] Launch-order-agnostic startup.
tbot serveno longer fails fast when the mod is unreachable at startup. It now retries the/api/pingprobe withexp_backoff(1s→30s)— the same cadencetbot watchandtbot listenalready use — until the game is reachable. Runtbot servefirst, then start Timberborn; both orderings work. - [feature]
--no-waitopt-out. Scripts and CI that prefer the legacy fail-fast behaviour (clean exit if the mod is down) can pass--no-wait. The friendlyModUnreachableErrorhandler incli.commands.serveis unchanged. - [behaviour] The first retry logs
serve: waiting for mod at http://… (launch Timberborn + load a save). Retrying every 1s…at INFO so the operator can see what's happening; subsequent retries are at DEBUG to avoid log spam. Once the mod responds,serve: mod reachable at …is logged at INFO.
v0.9 CLI: python-fire dispatcher¶
The tbot CLI is now dispatched via python-fire instead of the hand-rolled argparse + key:value dispatcher. The custom cli/args.py and cli/dispatcher.py modules are gone.
Breaking change for shell scripts and AI prompts
The legacy tbot <command> key:value key:value syntax no longer works. Pass arguments either positionally (in the order shown in tbot <command> --help) or with --key=value / --key value flags. Hyphens and underscores in flag names are interchangeable. Existing user-edited prompts under ~/.config/timberbot/agent_prompts/ need re-materializing (tbot init --force) or hand-updating.
- [feature] python-fire dispatcher.
tbot <method>introspects every publicTimberbotClientmethod and exposes it as a typed subcommand. Built-in subcommands (top,manager,launch,init,listen,watch,serve,agent) live as methods on theTbotclass;agentis a sub-group withrun/list_backends/prompts. - [feature] Per-command
--help.tbot <command> --helprenders the full Fire screen with positional args, flags, types, and defaults.tbot --helpkeeps a concise top-level index of builtins + client methods. - [feature] Hyphen↔underscore flag aliasing.
--source_id=42and--source-id=42both work for any flag. - [removed]
timberbot.cli.args/timberbot.cli.dispatcher. Scripts that importedparse_flags,_build_registry,parse_kv_args,format_usage, or_inject_listen_globalsneed updating; the same global flags (--host=,--port=,--auth-token=,--json,-v/--debug) are now parsed bytimberbot.cli.main.parse_global_flags.
Migration cheatsheet¶
| You were doing | Do this instead |
|---|---|
tbot set_speed speed:3 |
tbot set_speed 3 or tbot set_speed --speed=3 |
tbot buildings name:Pump |
tbot buildings --name=Pump |
tbot place_building prefab:Path x:120 y:130 z:2 orientation:south |
tbot place_building --prefab=Path --x=120 --y=130 --z=2 --orientation=south |
tbot link source_id:42 target_id:44 input:a |
tbot link --source-id=42 --target-id=44 --input=a |
tbot brain goal:"reach 50 beavers" |
tbot brain --goal="reach 50 beavers" |
tbot launch settlement:MyCastle save:day5 |
tbot launch --settlement=MyCastle --save=day5 |
v0.9 WebSocket cutover¶
Hard cutover — no fallback path. The heartbeat-polling channel and the outbound-HTTP-webhook channel are both replaced by a single long-lived WebSocket.
Breaking change for existing tbot watch, tbot listen, and webhook subscribers
Old tbot versions stop working against a v0.9 WebSocket mod — they speak the deleted HTTP heartbeat. Reinstall the matching tbot version (pipx install --upgrade timberbot).
Anyone hosting an HTTP server to receive POST /api/webhooks deliveries must migrate to either tbot listen or a custom WebSocket subscriber. There is no shim — the outbound HTTP delivery loop is gone.
- [feature] WebSocket transport. Parallel listener on
wsPort(default 8086,ws://host:wsPort/api/ws). Frame envelope{"type": "...", "payload": {...}}. Server→client:state,event,error,pong. Client→server:heartbeat,ping. Auth viaAuthorization: Bearer <token>on the upgrade request (or?token=query param). Heartbeat cadence drops from 2 s polling to 30 s — WS ping/pong and TCP keepalive handle liveness. - [feature]
tbot listenis now a pure WS client. No more--port/--hostinbound flags;tbot listenconnects out to the mod and printseventframes as they arrive.--prettyand--forward-to FILE|URLstill work. - [feature]
tbot watchis now a pure WS client. Polling loop and local-listener trigger queue deleted; the connector subscribes to the WS and reacts tostateframes.--listen-portis gone — no inbound HTTP server. - [feature] WS protocol contract. New
docs/websocket-protocol.mdis the authoritative wire spec. OpenAPI stays HTTP-only. - [removed] HTTP endpoints:
POST /api/tbot/heartbeat,POST /api/tbot/register,POST /api/webhooks,GET /api/webhooks,POST /api/webhooks/delete. The HTTP delivery loop, batching, circuit breaker, and subscriber registry inTimberbotEvents.csare gutted (the[OnEvent]handlers stay; they now publish to the WS broadcaster). - [removed] Settings keys:
webhooksEnabled,webhookBatchMs,webhookCircuitBreaker,webhookMaxPendingEvents,webhookValidateUrls. Logged as ignored on load. - [removed]
tbotWebhookUrlephemeral field andExpireWebhookIfStale()fromTimberbotAgentState/TimberbotService— no connector-trigger URL is ever stored. - [removed] Python client methods:
TimberbotClient.tbot_heartbeat,tbot_register,set_webhook,delete_webhook,list_webhooks. Generated pydantic models for those endpoints are deleted. - [removed] CLI commands:
tbot register_webhook,tbot unregister_webhook,tbot list_webhooks. Usetbot listeninstead. - [docs] architecture.md, getting-started.md, and the renamed events.md (previously
webhooks.md) rewritten around the WebSocket transport.events.mdnow documents WS event consumption rather than HTTP webhook registration.AGENTS.md(repo root) addsdocs/websocket-protocol.mdto the dev-facing read-first list.
Migration cheatsheet¶
| You were doing | Do this instead |
|---|---|
tbot register_webhook url:... events:... then hosting your own HTTP server |
tbot listen (pure WS client; no port needed) |
tbot watch --listen-port 9000 |
tbot watch (no --listen-port flag) |
| Custom HTTP receiver subscribed to events | Open a WS to ws://host:8086/api/ws, filter on frame.payload.event |
Bumping webhookBatchMs to reduce delivery rate |
Buffer client-side — the mod pushes one frame per event |
| Relying on circuit-breaker auto-disable | Reconnect on your own; the mod drops slow consumers from the bounded send queue |
v0.9 architecture cutover (connector model)¶
Hard cutover — no fallback path. The widget no longer spawns the agent; the connector does.
- [feature]
tbot watchconnector. Long-running Python process: reconnects with exponential backoff, heartbeats every 30 s over the WebSocket, dispatchestbot agent run(or attaches toopencode serve) per cycle. - [feature] Launch / Stop ready gate. The widget button replaces Start/Stop. Until the player presses Launch, the mod returns
409 game_not_readyon every/api/*read and write except/api/agent/*,/api/ready,/api/ping. Game events keep flowing over the WebSocket. - [feature] Mode dropdown. Request (default) vs Autonomous. Request mode has a per-launch prompt textarea; autonomous mode persists
goaltostate.jsonand lets the connector pick cadence. - [feature] Four new endpoints.
GET /api/agent/state,POST /api/agent/config,POST /api/agent/request,POST /api/ready. (POST /api/tbot/registerandPOST /api/tbot/heartbeatwere added here and later removed in the WS cutover above.) - [feature] Bearer-token auth.
authTokeninsettings.jsonrequiresAuthorization: Bearer <token>on every/api/*route (constant-time compare). The mod refuses to start with a non-localhostlistenAddressand emptyauthToken. Client side adds[client].auth_token,TBOT_AUTH_TOKEN, andTimberbotClient(auth_token=…). - [feature]
tbot listenWS client. Pure WebSocket subscriber that connects to the mod and printseventframes;--prettyfor human rendering,--forward-to FILE|URLfor piping. - [feature] opencode
--attach.[backends.opencode].attach_url(or--attach-url) makestbot agent runtarget a long-runningopencode serveinstead of spawning a fresh process. - [feature]
state.json. New file alongsidesettings.jsonfor agent-shaped state. Persistsmode,goal,lastError. Ephemeral state (ready,pendingRequest,tbotWebhookUrl,lastAckedRequestId) resets on save load. (tbotWebhookUrlis later removed in the WS cutover entry above; no replacement field — there's no connector-trigger URL to store.) - [removed]
TimberbotAgentsubprocess spawn. The mod is a pure HTTP server. - [removed] Settings keys:
agentBinary,agentGoal,agentModel,agentEffort,agentCommandTemplate,agentAllowlistEnabled,agentAllowedBinaries,tbotCommand. Backend choice lives in~/.config/timberbot/config.toml; the connector runstbot, not the mod. - [docs] getting-started.md, timberbot.md,
webhooks.md(later renamed to events.md in the WS cutover), and architecture.md rewritten around the new flow.
Error messages¶
Every API error response is now rich and actionable. The AI gets enough context to correct the next call without guessing.
Write endpoints (TimberbotWrite.cs)
- [fix] not_found errors explain that ids are ephemeral and tell the caller to re-query buildings
- [fix] invalid_type errors include the building name and explain which building types support the operation
- [fix] invalid_param errors echo the bad value and list all valid options
- [fix] no_population includes requested vs available count
- [fix] insufficient_science includes cost and current points with a human-readable message
- [fix] district errors (not_found, SetDistribution) list all available district names
- [fix] recipe and plantable errors list all available options for that building
Placement (TimberbotPlacement.cs)
- [fix] placement validation errors name the blocking object (e.g. "occupied by Lumberjack") instead of generic "occupied"
- [fix] every validation error includes a suggestion (e.g. "demolish it or try a different location")
- [fix] not_unlocked tells the caller to use science/unlock first
- [internal] extracted FindBlockerAt() helper for DRY blocker lookup across buildings, natural resources, and tracked blockers
HTTP server (TimberbotHttpServer.cs)
- [fix] invalid_body explains the expected format
- [fix] unknown_endpoint now lists all GET and POST endpoints (was only 13, now 55+)
Python client (timberbot.py) - [fix] unknown CLI parameters now show the bad param, valid params, and full usage line - [fix] toon error output shows the full response dict instead of just the error string
Storage¶
- [feature]
POST /api/building/storageendpoint for piles, warehouses, and tanks - [feature] set allowed good (
good:Water) and storage mode (mode:obtain) in one call - [feature] four storage modes matching the player UI: accept, obtain, supply, empty
- [feature]
storageModeandallowedGoodfields in buildings detail:full output - [fix]
SingleGoodAllowernow usesAllow()/Disallow()instead of direct property set, so the UI updates correctly - [breaking]
/api/stockpile/goodand/api/stockpile/capacityremoved, replaced by/api/building/storage
Write endpoint fixes¶
- [fix]
Workplace.SetDesiredWorkers()used instead of direct property set, firesDesiredWorkersChangedevent and handles overstaffing - [fix]
WorkingHoursManager.WorkedPartOfDayused instead of directEndHoursset, firesWorkingHoursChangedEvent - [fix]
set_workersminimum clamped to 1 (was 0), matching player UI behavior - [breaking]
set_capacityremoved, player cannot change stockpile capacity
Agent¶
- [removed] shipped Claude Code hooks (
pretool-bash.py,session-start.py) deleted. they blocked parallel tool calls and are no longer needed
Panel defaults¶
- [fix] model and effort defaults extracted to named constants (
DefaultClaudeModel,DefaultCodexModel, etc.) instead of scattered string literals - [fix] switching binary (claude/codex) auto-selects the correct default model and effort
Testing¶
- [new] 141 xUnit unit tests for
TimberbotJw(serialization, commas, nesting, reuse) andTimberbotPure(orientation, name cleanup, assertions, normalization, quoting) - [new]
timberbot/test/project (net8.0, xUnit) shares source files with the main project, no Unity deps required - [internal] extracted pure static helpers from 5 Unity-dependent files into
TimberbotPure.cs
Docs¶
- [docs] "Timberbot AI" pages renamed to "Timberbot Guide"
- [docs] getting-started page updated with setup instructions