Files
gib f8241e42bb
Gateway / check (push) Successful in 9s
Gateway / image (push) Successful in 20s
Docs: public README, setup guide and architecture, generic deploy files, and the T3 Code notice
2026-11-02 13:51:19 -05:00

5.0 KiB

Architecture

WristCode puts every T3 Code environment on an Apple Watch. From the watch you can see which agents need you, answer their requests, reply, start and browse threads, and get notifications. It runs over the watch's own cellular connection, so no phone is needed.

How a client talks to a T3 server is in t3-protocol.md, and the watch's API is in api.md.

Shape

watchOS app  --HTTPS + device token-->  gateway (Bun)  --WebSocket + bearer-->  T3 server x N
                                           |
                                           +--HTTP/2-->  APNs  -->  watch notifications
  • The watch never talks to T3 directly. watchOS only allows WebSockets for audio apps, and every T3 write goes over a WebSocket.
  • The gateway keeps one orchestration.subscribeShell stream open per T3 server and holds every thread shell in memory. It serves lists from memory, so the watch never waits on a T3 round trip to draw the feed. Thread text comes from each thread's bounded snapshot, read on demand and cached until the thread changes.
  • The gateway is the only place that holds T3 credentials. The watch holds only its own device token.

Gateway

Bun and TypeScript, with no framework: Bun.serve for HTTP, bun:sqlite for state, and node:http2 for APNs. The T3 RPC client is hand-written and needs no Effect runtime or vendored contracts.

File Job
src/t3/rpc.ts Effect RPC JSON framing: request ids, pending calls, Ack on every Chunk, pings, and a silence timeout
src/t3/host.ts One T3 server: route choice, ticket mint, connect, reconnect with backoff, shell state, and the HTTP reads
src/t3/types.ts Narrow, lenient types for the fields the gateway reads. Unknown fields and values pass through
src/hub.ts Every host in the database, kept connected and rereading the table for changes
src/awareness.ts A port of T3's own awareness logic, which maps a thread shell to a phase
src/views.ts The feed, thread detail, browse, search and summary responses
src/notify.ts Watches phase changes and decides what to push
src/push.ts Sends pushes to each paired watch that wants that kind
src/apns.ts ES256 provider JWT and an HTTP/2 client for APNs
src/api.ts The watch's REST API
src/db.ts SQLite schema and queries
src/cli.ts The operator CLI: host add, host list, host remove, pair, device list, device revoke

T3 credentials

host add takes a T3 pairing credential, not a long-lived token. The gateway exchanges it for a 30 day session limited to reading and driving threads, and stores that in SQLite. A pairing credential works once and expires in minutes, so the session token never appears in a terminal. T3 sessions cannot be refreshed, so the gateway checks each one daily and pushes a warning to the watch a week before it expires. scripts/renew-hosts.sh renews them all in one go.

A host can have several routes, such as a LAN address, a VPN address and a public URL. The gateway tries them in order on every connect and stays on the first that answers.

Notifications

The gateway pushes when a thread starts waiting for you, finishes, or fails. On a cold start it loads state without pushing, so a restart never replays old news. Each watch picks which kinds it wants. A subagent's request is pushed under the thread that started it.

Storage

hosts(id, label, url, alt_urls, environment_id, token, token_expires_at, created_at)
devices(id, name, token_hash, apns_token, apns_env, notifications, created_at, last_seen_at, revoked_at)
pairing_codes(code_hash, expires_at, used_at)

Device tokens and pairing codes are stored as SHA-256 hashes.

Testing

Unit tests cover the parts that carry logic: RPC framing against an in-process fake server, route choice and reconnects, the awareness port, feed ordering and subagent roll-up, notification diffing, push filtering, and the API shapes. Run them with bun test in gateway/.

Watch app

SwiftUI, watch only (WKRunsIndependentlyOfCompanionApp). XcodeGen generates the project from watch/project.yml. A widget extension provides the complications and the Smart Stack widget. The watch makes plain URLSession calls, and the device token lives in the Keychain, shared with the widget.

The feed refreshes when it appears, when the app returns to the foreground, every 10 seconds while it is on screen, and when a notification arrives.

watch/README.md covers building, the demo mode and the screenshot tests.

Decisions

  • Threads started from the watch use the project's default model and the host's default runtime mode, unless the watch picks another model or turns on Ask before acting.
  • An App Store app has to be an iOS app, so a watch-only app ships inside an empty iOS wrapper. The watch app's own bundle ID is also its APNs topic.
  • delegate_task children are not marked as subagents by T3, so the gateway recognizes them by delegatedFrom and treats them as subagents of the thread that started them.