Docs: public README, setup guide and architecture, generic deploy files, and the T3 Code notice
This commit is contained in:
1 parent
b08fd9bdb1
commit
f8241e42bb
13 files changed
+345
-301
No files matched your search
@@ -0,0 +1,100 @@
|
||||
# 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](t3-protocol.md), and the watch's API is in
|
||||
[api.md](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](../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.
|
||||
Reference in new issue
Block a user