thundercode commited on
Commit
5ce82be
Β·
verified Β·
1 Parent(s): c9654a2

release: add docs/DEPLOYMENT.md

Browse files
Files changed (1) hide show
  1. docs/DEPLOYMENT.md +285 -86
docs/DEPLOYMENT.md CHANGED
@@ -1,126 +1,325 @@
1
  # Deployment
2
 
3
- **Status tags:** `IMPLEMENTED` Β· `VERIFIED` Β· `MEASURED` Β· `OPEN` Β· `DEFERRED` Β· `BLOCKED`.
4
 
5
- The live system is deployed across three **private** repositories plus one **public** umbrella repo.
6
- The monorepo working copy is **not** the deployed source. This document records the live topology,
7
- the environment variables, the deploy mechanics, and the traps that cost real debugging time.
 
 
 
 
8
 
9
  ---
10
 
11
  ## 1. Live revisions (VERIFIED)
12
 
 
 
13
  | Component | Repository | Visibility | Branch | Revision | Host |
14
  |---|---|---|---|---|---|
15
- | Frontend | `Anish-lab-blip/SatQuery-Frontend` | private | `main` | **`2d7ae53b482d`** | Cloudflare Pages β†’ `satquery.pages.dev` |
16
- | Backend / orchestrator | `Anish-lab-blip/SatQuery-Backend` | private | `main` | **`89d80eaddec5`** | Render β†’ `satquery-backend-m4yv.onrender.com` |
17
- | Inference | `Anish-lab-blip/SatQuery-Inference` | private | `main` | **`5a0936ace491`** | Codespace `potential-space-trout-r4ppw969w45j2pvvw`, port 8000 |
18
- | Public umbrella | `Anish-lab-blip/SatQuery-AI` | **public** | `main` | `3dcabd32da41` | this release home |
19
- | Monorepo (working copy) | `C:/Users/anish/satquery-ai` | local only | `master` | `9d57aed` | **no remote**; 334 dirty entries |
20
 
21
- > **Trap.** `deploy/` inside the monorepo is **stale and untracked**. It is **not** the deployed
22
- > source. Edits must go to the three real repositories.
23
 
24
- ## 2. Live health (VERIFIED, probed)
25
 
26
- ```json
27
- {"status":"ok","service":"satquery-orchestrator",
28
- "tunnel":{"agent_connected":true,"agent_id":"codespaces-fd1038","pending":0,"completed":97},
29
- "config":{"codespace_name":"potential-space-trout-r4ppw969w45j2pvvw\n","codespace_port":8000,
30
- "transport_mode":"auto","tunnel_timeout_s":150.0,"wake_timeout_s":120.0,
31
- "upstream_timeout_s":90.0,"device":"cpu","has_github_token":true}}
32
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- Note `codespace_name` still carries a trailing `\n` β€” the cosmetic B-02 patch is **not** deployed.
 
 
35
 
36
- ## 3. Environment variables
37
 
38
- ### 3.1 Render (gateway) β€” measured live
 
39
 
40
- | Variable | Value (live) |
41
  |---|---|
42
- | `CODESPACE_NAME` | `potential-space-trout-r4ppw969w45j2pvvw` |
43
- | `CODESPACE_PORT` | `8000` |
44
- | `SATQUERY_ALLOWED_ORIGINS` | `https://satquery.pages.dev` |
45
- | `SATQUERY_DEVICE` | `cpu` |
46
- | `SATQUERY_TRANSPORT` | `auto` |
47
- | `SATQUERY_TUNNEL_TIMEOUT_S` | `150` |
48
- | `SATQUERY_WAKE_TIMEOUT_S` | `120` |
49
- | `SATQUERY_UPSTREAM_TIMEOUT_S` | `90` |
50
- | `GITHUB_TOKEN` | present |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
- > There is **no** `SATQUERY_UPSTREAM_URL` and **no** `HF_TOKEN` in the live config β€” the transport is
53
- > the outbound tunnel, not a forwarded port.
 
54
 
55
- ### 3.2 Codespace (inference)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
  | Variable | Purpose |
58
  |---|---|
59
- | `PORT` | platform-assigned; **must be read** |
60
  | `SATQUERY_DEVICE` | `cpu` \| `cuda` \| `mps` \| `null`; read **without importing torch** |
61
- | `SATQUERY_MAX_FILE_BYTES` | per-file cap (shared with Render) |
62
- | `SATQUERY_ASSET_ENABLED` / `SATQUERY_ASSET_DIR` | both required for `/v1/assets`; fails closed (503) otherwise |
63
  | `SATQUERY_ASSET_MAX_FILES` / `SATQUERY_ASSET_TTL_S` | optional handle capacity / lifetime |
64
 
65
- ## 4. Deploy mechanics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
- - **Frontend β†’ Cloudflare Pages.** Staged by `scripts/stage_pages.mjs`, deployed with
68
- `npx wrangler pages deploy`.
69
- - **Backend β†’ Render.** `render.yaml` blueprint; `main.py` exposes `app`.
70
- - **Inference β†’ Codespace.** `deploy/codespace/serve.py` serves `build_space_app()` on `$PORT`;
71
- `.devcontainer/` forwards port 8000 and runs the tunnel agent on start via `postStartCommand`.
72
- - **Repository writes** are performed through the **GitHub Git Data API** (blob β†’ tree β†’ commit β†’
73
- `PATCH` ref) with **sha256 byte-verification** of every uploaded blob. Deletions are expressed as
74
- `sha: null` tree entries. This is used instead of `git push` so each deployed file is verified by
75
- content hash.
76
 
77
- ### 4.1 Deployed-artifact integrity
 
 
78
 
79
- Deployed files were re-read from the GitHub API and compared byte-for-byte against the local copies:
80
- **9 files sha256 byte-identical**, and the deployed HEAD re-read from the API. See
81
- `verify_deployed_head.py`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- ## 5. Cold start (documented, not hidden)
 
 
 
 
84
 
85
- Render's free tier sleeps when idle and the Codespace may be stopped. The frontend shows
86
- *"Waking inference engine…"* while Render starts the Codespace and polls health. Cold start is
87
- therefore **tens of seconds**, and this is documented rather than papered over.
88
 
89
- ## 6. Known deployment issues
90
 
91
- | ID | Issue | State |
 
 
92
  |---|---|---|
93
- | **B-07** | Transient tunnel-agent gaps β†’ a request can hang or return 504. Patch prepared, **NOT deployed**. | **OPEN** |
94
- | β€” | B-07 root shape: in `auto` mode a tunnel timeout **falls through** to the forward path (`main.py:546`), burning `wake_timeout_s = 120` on a `302` (~249 s β‰ˆ 150 + 120). | measured |
95
- | **B-02** | `/api/health` `codespace_name` trailing `\n`. Cosmetic; the wake path strips it. | **OPEN (cosmetic)** |
96
- | β€” | Frontend/Backend/Inference repos are **private**, so their links 404 for an outside audience. | by design |
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- ## 7. Platform traps (recorded so they are not rediscovered)
99
 
100
- - **Cloudflare `_headers` rules CONCATENATE, they do not override.** Two matching rules are merged;
101
- Chromium takes the **first** `max-age` it encounters. A later rule cannot "fix" an earlier one.
102
- - **Cloudflare 308-redirects `X.html` β†’ `/X`.** Reference the extensionless path.
103
- - **A forwarded Codespace port returns `302` for a private repo** β€” which is *why* the tunnel exists.
104
- - **The tunnel agent must be started by the devcontainer `postStartCommand`**, or a restarted
105
- Codespace comes up with `agent_connected:false`.
106
- - **Never retry `POST /api/infer` at the gateway** β€” a retry consumes inference twice.
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
- ## 8. Preconditions for a fresh deploy (open items)
 
 
 
 
109
 
110
  1. Cloudflare Pages project name / domain (needed for the deploy command and `robots.txt` sitemap).
111
- 2. Artifacts present, or capabilities honestly ship `available: false` (the service **degrades rather
112
- than crashes** on absent artifacts).
113
- 3. `HF_TOKEN` set on Render **if** the HF proxy path is used (not used in the live config).
114
- 4. Codespace `.devcontainer/` forwarding `:8000` and starting the tunnel agent.
115
- 5. The five historical backend blockers closed and verified on the first live run (ASGI stack in
116
- requirements; `$PORT` read; `OPTIONS` preflight; fail-fast app construction; adapter integrity
117
- compared against an expected digest).
118
-
119
- ## 9. Historical context
120
-
121
- The superseded design ran inference on an **HF Space with ZeroGPU** behind a **Railway** gateway. The
122
- active design moves to **Render + Codespace**, CPU-first, with an **outbound tunnel**. The four-endpoint
123
- contract, the gateway responsibility table, the env-var vocabulary and the config freeze are
124
- unchanged β€” only host names moved. `configs/deploy.yaml` still describes the old HF-Space/ZeroGPU
125
- target and is left **undisturbed as frozen paperwork** (editing it would move the config hash); no
126
- Gradio runtime exists in code.
 
 
 
 
 
 
 
 
 
 
 
1
  # Deployment
2
 
3
+ **Status tags:** `IMPLEMENTED` Β· `VERIFIED` Β· `MEASURED` Β· `OPEN` Β· `DEFERRED` Β· `BLOCKED` Β· `BY DESIGN`.
4
 
5
+ The live SatQuery AI system runs across **three private repositories** plus one **public umbrella
6
+ repository**. The monorepo working copy is **not** the deployed source. This document records the live
7
+ topology, every environment variable, the deploy mechanics, the cold-start behaviour, the known
8
+ issues, and the platform traps that cost real debugging time.
9
+
10
+ > **The single most important trap in this document.** `deploy/` inside the monorepo is **stale and
11
+ > untracked**. It is **not** the deployed source. Edits must go to the three real repositories.
12
 
13
  ---
14
 
15
  ## 1. Live revisions (VERIFIED)
16
 
17
+ Read from the GitHub API during the release reconnaissance.
18
+
19
  | Component | Repository | Visibility | Branch | Revision | Host |
20
  |---|---|---|---|---|---|
21
+ | Frontend | `Anish-lab-blip/SatQuery-Frontend` | **private** | `main` | **`2d7ae53b482d`** | Cloudflare Pages β†’ `satquery.pages.dev` |
22
+ | Backend / orchestrator | `Anish-lab-blip/SatQuery-Backend` | **private** | `main` | **`89d80eaddec5`** | Render β†’ `satquery-backend-m4yv.onrender.com` |
23
+ | Inference | `Anish-lab-blip/SatQuery-Inference` | **private** | `main` | **`5a0936ace491`** | Codespace `potential-space-trout-r4ppw969w45j2pvvw`, port 8000 |
24
+ | Public umbrella | `Anish-lab-blip/SatQuery-AI` | **public** | `main` | `3dcabd32da41` ("Initial commit") | this release home |
25
+ | Monorepo (working copy) | `C:/Users/anish/satquery-ai` | local only | `master` | `9d57aed` | **no git remote**; 334 dirty entries |
26
 
27
+ The public umbrella repository previously contained **only a 13-byte README** (`# SatQuery-AI`). It is
28
+ the intended home for this release.
29
 
30
+ ## 2. The four tiers
31
 
 
 
 
 
 
 
32
  ```
33
+ Browser
34
+ β”‚ HTTPS
35
+ β–Ό
36
+ Cloudflare Pages β€” satquery.pages.dev (static frontend, 11 pages)
37
+ β”‚ HTTPS / JSON β†’ /api/*
38
+ β–Ό
39
+ Render β€” satquery-backend-m4yv.onrender.com (orchestrator / gateway)
40
+ β”‚ outbound long-poll POST /tunnel/agent
41
+ β–Ό
42
+ GitHub Codespace β€” FastAPI inference, CPU, port 8000
43
+ β”‚ build_space_app()
44
+ β–Ό
45
+ specialists: SmolVLM Β· RemoteCLIP Β· MiniLM Β· CROMA Β· STANet
46
+ β”‚
47
+ β–Ό
48
+ ResultEnvelope β†’ tunnel β†’ Render β†’ browser
49
+ ```
50
+
51
+ ```mermaid
52
+ flowchart LR
53
+ U[Browser] -->|HTTPS| CF["Cloudflare Pages<br/>static frontend"]
54
+ CF -->|"HTTPS JSON<br/>/api/health Β· /api/capabilities Β· /api/infer Β· /api/assets"| R["Render<br/>orchestrator / gateway"]
55
+ R -->|"outbound long-poll<br/>POST /tunnel/agent"| C["GitHub Codespace<br/>FastAPI inference :8000"]
56
+ C --> S[(SmolVLM Β· RemoteCLIP<br/>MiniLM Β· CROMA Β· STANet)]
57
+ C -->|ResultEnvelope| R
58
+ R -->|"envelope + error translation"| CF
59
+ ```
60
+
61
+ ## 3. Why each tier exists
62
+
63
+ ### 3.1 Cloudflare Pages β€” the static tier
64
 
65
+ Serves the frontend. **No backend, no secrets, and no API calls of its own** on the static pages. The
66
+ one exception is the Analyze console, which calls the gateway. Staged by `scripts/stage_pages.mjs` and
67
+ deployed with `npx wrangler pages deploy`.
68
 
69
+ ### 3.2 Render β€” the gateway
70
 
71
+ A deliberately **thin, stateless** orchestrator. Its responsibilities, from
72
+ `docs/DEPLOYMENT_ARCHITECTURE.md` Β§2:
73
 
74
+ | Responsibility | Detail |
75
  |---|---|
76
+ | Schema validation | reject malformed requests before they cost inference |
77
+ | Size limits | whole-request body cap, shared with the Codespace |
78
+ | Rate limiting | per-IP count + window β€” **fairness, NOT a security control** |
79
+ | CORS allowlist | the Pages origin; **never `*`** |
80
+ | Request ids | correlate a request across tiers |
81
+ | Timeouts | sit inside the task budget |
82
+ | Secret custody | holds credentials that must never reach the browser |
83
+ | Error translation | upstream failures β†’ the documented error envelope |
84
+
85
+ It is **not** a model host. It has **no database, no auth, and no queue**.
86
+
87
+ > **Hard rule.** The gateway must **not** retry `POST /api/infer` on its own β€” a retry would consume
88
+ > inference a second time. The client decides on retry.
89
+
90
+ ### 3.3 GitHub Codespace β€” the inference tier
91
+
92
+ Runs the real inference service: `build_space_app()` from `app/space_app.py`, served by
93
+ `deploy/codespace/serve.py` on `$PORT`, in **CPU mode**. It honours the four-endpoint contract
94
+ (`/v1/health`, `/v1/capabilities`, `/v1/analyze`, `/v1/assets`), imports cheaply without torch, reuses
95
+ `app/serving.py` as the composition root, and **degrades rather than crashes** on absent artifacts.
96
+
97
+ ### 3.4 Hugging Face β€” the model tier
98
+
99
+ Holds the six trained artifacts and the model card. It is **not** the runtime inference host; the
100
+ Codespace resolves the pinned backbones from the Hub at run time.
101
+
102
+ ## 4. Why the transport is an outbound tunnel
103
+
104
+ The inference host is a Codespace in a **private** repository. A forwarded port for a private repo
105
+ returns **`302`**, so an inbound-forwarding design cannot work. Instead:
106
 
107
+ - the Codespace runs `deploy/codespace/tunnel_agent.py`;
108
+ - the agent **dials out** to `POST /tunnel/agent` and long-polls;
109
+ - work is executed against `http://127.0.0.1:8000` **locally**.
110
 
111
+ This inverts the usual direction: the inference host needs **no inbound firewall hole**. It also means
112
+ the transport is only alive while the agent is polling.
113
+
114
+ **Measured:** `GET /api/health` reported `tunnel.agent_connected: true` with a non-zero `completed`
115
+ counter, and `POST /api/infer {}` returned `422 invalid_request` with the response header
116
+ `x-satquery-transport: tunnel`.
117
+
118
+ ## 5. The full live health payload (VERIFIED, probed)
119
+
120
+ ```json
121
+ {
122
+ "status": "ok",
123
+ "service": "satquery-orchestrator",
124
+ "tunnel": {
125
+ "agent_connected": true,
126
+ "agent_id": "codespaces-fd1038",
127
+ "pending": 0,
128
+ "completed": 97
129
+ },
130
+ "config": {
131
+ "codespace_name": "potential-space-trout-r4ppw969w45j2pvvw\n",
132
+ "codespace_port": 8000,
133
+ "transport_mode": "auto",
134
+ "tunnel_timeout_s": 150.0,
135
+ "wake_timeout_s": 120.0,
136
+ "upstream_timeout_s": 90.0,
137
+ "device": "cpu",
138
+ "has_github_token": true
139
+ }
140
+ }
141
+ ```
142
+
143
+ Note `codespace_name` still carries a **trailing `\n`** β€” this is **B-02**, cosmetic and `OPEN`; the
144
+ wake path strips it (`_codespace_name()`), so only the health payload reports the raw value.
145
+
146
+ ## 6. Environment variables
147
+
148
+ ### 6.1 Render (gateway) β€” measured live values
149
+
150
+ | Variable | Value (live) | Purpose |
151
+ |---|---|---|
152
+ | `CODESPACE_NAME` | `potential-space-trout-r4ppw969w45j2pvvw` | which Codespace to wake |
153
+ | `CODESPACE_PORT` | `8000` | the inference port |
154
+ | `SATQUERY_ALLOWED_ORIGINS` | `https://satquery.pages.dev` | CORS allowlist (never `*`) |
155
+ | `SATQUERY_DEVICE` | `cpu` | device preference |
156
+ | `SATQUERY_TRANSPORT` | `auto` | tunnel first, then forward |
157
+ | `SATQUERY_TUNNEL_TIMEOUT_S` | `150` | how long to wait on the tunnel |
158
+ | `SATQUERY_WAKE_TIMEOUT_S` | `120` | how long to wait for a cold start |
159
+ | `SATQUERY_UPSTREAM_TIMEOUT_S` | `90` | gateway β†’ upstream budget |
160
+ | `GITHUB_TOKEN` | present | Codespace control |
161
+ | `PORT` | platform-supplied | Render's own listen port |
162
+
163
+ > **Measured absence.** There is **no** `SATQUERY_UPSTREAM_URL` and **no** `HF_TOKEN` in the live
164
+ > config. The transport is the outbound tunnel, not a forwarded port. This contradicts the older
165
+ > `docs/DEPLOYMENT_TOPOLOGY.md` Β§3.2 table, which predates the tunnel design.
166
+
167
+ ### 6.2 Codespace (inference)
168
 
169
  | Variable | Purpose |
170
  |---|---|
171
+ | `PORT` | platform-assigned; **must be read** (a historical blocker was that no code read it) |
172
  | `SATQUERY_DEVICE` | `cpu` \| `cuda` \| `mps` \| `null`; read **without importing torch** |
173
+ | `SATQUERY_MAX_FILE_BYTES` | per-file cap, shared with Render |
174
+ | `SATQUERY_ASSET_ENABLED` / `SATQUERY_ASSET_DIR` | both required for `/v1/assets`; **fails closed (503)** otherwise |
175
  | `SATQUERY_ASSET_MAX_FILES` / `SATQUERY_ASSET_TTL_S` | optional handle capacity / lifetime |
176
 
177
+ ### 6.3 Config-loader environment overrides
178
+
179
+ Two registry values can be overridden from the environment without editing the YAML
180
+ (`core/config.py`):
181
+
182
+ | Variable | Effect |
183
+ |---|---|
184
+ | `SATQUERY_PRECISION` | overrides `training.precision` |
185
+ | `SATQUERY_TORCH_COMPILE` | overrides `deployment.torch_compile` (`"true"` β†’ `True`) |
186
+
187
+ Both are still validated by the loader β€” setting `SATQUERY_TORCH_COMPILE=true` **fails startup**
188
+ because finding C-8 forbids `torch.compile`.
189
+
190
+ ## 7. Deploy mechanics
191
+
192
+ | Tier | Mechanism |
193
+ |---|---|
194
+ | Frontend β†’ Cloudflare Pages | `scripts/stage_pages.mjs` builds a Pages bundle; `npx wrangler pages deploy` |
195
+ | Backend β†’ Render | `render.yaml` blueprint; `main.py` exposes the ASGI object `app` |
196
+ | Inference β†’ Codespace | `deploy/codespace/serve.py` binds `build_space_app()` to `$PORT`; `.devcontainer/` forwards 8000 and starts the tunnel agent via `postStartCommand` |
197
+ | Repository writes | the **GitHub Git Data API** β€” blob β†’ tree β†’ commit β†’ `PATCH` ref |
198
+
199
+ ### 7.1 Why the Git Data API instead of `git push`
200
+
201
+ Every deployed file is uploaded as a **blob** whose sha256 is **computed locally and verified against
202
+ the uploaded blob**, then assembled into a tree, committed, and the branch ref patched. This means:
203
+
204
+ - each file is **content-verified** rather than trusted;
205
+ - deletions are expressed explicitly as `sha: null` tree entries;
206
+ - the deploy is **idempotent** β€” re-running it with identical content produces no change.
207
+
208
+ **Measured:** 9 deployed files were re-read from the API and found **sha256 byte-identical** to the
209
+ local copies, with the deployed HEAD re-read independently (`verify_deployed_head.py`).
210
 
211
+ ## 8. Cold start (documented, not hidden)
 
 
 
 
 
 
 
 
212
 
213
+ Render's free tier sleeps when idle, and the Codespace may be stopped. Before a request can be served,
214
+ Render must start the Codespace (if stopped) and wait for the tunnel agent to reconnect. The frontend
215
+ shows *"Waking inference engine…"* during this.
216
 
217
+ | Property | Value |
218
+ |---|---|
219
+ | Cold start | **tens of seconds** |
220
+ | Tunnel wait before falling through | `SATQUERY_TUNNEL_TIMEOUT_S` = 150 s |
221
+ | Wake wait | `SATQUERY_WAKE_TIMEOUT_S` = 120 s |
222
+ | Upstream budget | `SATQUERY_UPSTREAM_TIMEOUT_S` = 90 s |
223
+
224
+ Cold start is **documented rather than papered over**: an honest "this will take a while the first
225
+ time" is better than a silent hang.
226
+
227
+ ### 8.1 The `transport_mode: auto` fallthrough β€” B-07 (`OPEN`)
228
+
229
+ `SATQUERY_TRANSPORT=auto` means: **try the tunnel; on timeout, fall through to the forward path.**
230
+
231
+ The forward path to a **private** repo returns `302` quickly β€” but the wake step still consumes
232
+ `SATQUERY_WAKE_TIMEOUT_S` (120 s) **first**. So a worst-case failed request takes roughly
233
+
234
+ ```
235
+ 150 s (tunnel timeout) + 120 s (wake timeout on a 302) β‰ˆ 249 s
236
+ ```
237
 
238
+ This is the **root shape** of the observed transient tunnel gap, and it is why a request can appear to
239
+ hang and then fail. A patch (`fix-b07-forward-unavailable.patch`) was authored and verified
240
+ (`py_compile` clean, applies cleanly to the deployed `main.py`), adding distinct
241
+ `forward_unavailable` (503, terminal 302/401/403 on the forward path) and `upstream_timeout`
242
+ (504, tunnel healthy but slow) codes, plus the `codespace_name` `.strip()` fix.
243
 
244
+ > **Status: B-07 is `OPEN`.** The patch is **prepared but NOT deployed.** The deployed health payload
245
+ > still shows the trailing `\n`.
 
246
 
247
+ ## 9. The five historical backend blockers
248
 
249
+ Before any backend could boot, five verified blockers had to be closed. The current design closes them:
250
+
251
+ | # | Blocker (verified) | How it is closed |
252
  |---|---|---|
253
+ | 1 | `requirements.txt` declared no `fastapi` / `uvicorn` / `httpx` / `starlette` | the runtime installs the ASGI stack so `build_space_app()` and the gateway can import |
254
+ | 2 | no code read `$PORT` β€” a platform port would be ignored | `deploy/codespace/serve.py` binds to `$PORT`; Render reads its own |
255
+ | 3 | hand-rolled CORS raised `405` on `OPTIONS`, so browser preflight failed | the gateway registers `OPTIONS` explicitly / uses Starlette's CORS middleware |
256
+ | 4 | module-level `app = create_app()` swallowed config errors into `app = None` | construction errors now **propagate** (fail-fast) instead of leaving a dead `app` |
257
+ | 5 | adapter integrity unverified on load (`_adapter_sha256` computed but never compared) | the load path compares the digest against an expected value, or fails startup |
258
+
259
+ ## 10. Platform traps (recorded so they are not rediscovered)
260
+
261
+ | Trap | Detail |
262
+ |---|---|
263
+ | **Cloudflare `_headers` CONCATENATE** | Two matching rules are **merged, not overridden**. A specific rule nested under a broad `/assets/img/*` rule yields `max-age=604800, …, max-age=0, must-revalidate` β€” and Chromium takes the **FIRST** `max-age`. The file's own "later rules override" comment is **false**. |
264
+ | **Cloudflare 308 redirect** | `X.html` β†’ `/X`. Reference the extensionless path. |
265
+ | **Forwarded port returns 302** | for a private repo β€” this is *why* the tunnel exists. |
266
+ | **Tunnel agent must start on boot** | via the devcontainer `postStartCommand`, or a restarted Codespace comes up with `agent_connected: false`. |
267
+ | **Never retry `/api/infer` at the gateway** | a retry consumes inference twice. |
268
+ | **`deploy/` is stale and untracked** | not the deployed source. |
269
 
270
+ ## 11. The superseded design, and what did NOT change
271
 
272
+ The earlier design ran inference on an **HF Space with ZeroGPU** (5 GPU-min/day, `@spaces.GPU`
273
+ decoration) behind a **Railway** gateway. The active design changes three things:
274
+
275
+ 1. **CPU-first instead of ZeroGPU.** No code change was required β€” `device_preference` honours
276
+ `SATQUERY_DEVICE` and defaults to CPU, every specialist defaults to `device="cpu"`, and all
277
+ placement is `.to(device)` (never `.cuda()`).
278
+ 2. **A real, always-buildable inference environment.** A Codespace gives a reproducible container
279
+ without a GPU quota or a Space's cold-start constraint. The wake flow replaces ZeroGPU lazy
280
+ loading as the cold-start story.
281
+ 3. **No GPU quota to protect at the gateway.** Rate/size limits remain, but as **fairness** controls
282
+ rather than quota protection.
283
+
284
+ **What did NOT change:**
285
+
286
+ | Unchanged | Detail |
287
+ |---|---|
288
+ | the four-endpoint contract | `/v1/health`, `/v1/capabilities`, `/v1/analyze`, `/v1/assets` |
289
+ | the gateway responsibility table | Β§3.2 above |
290
+ | the env-var vocabulary | only host names moved (`SATQUERY_SPACE_URL` β†’ `SATQUERY_UPSTREAM_URL`) |
291
+ | the config freeze | `78f1e3700da15aa1` |
292
 
293
+ `configs/deploy.yaml` still describes the old HF-Space/ZeroGPU target (`platform: huggingface-spaces`,
294
+ `sdk: gradio`, `zerogpu: true`, the `gpu_duration_*` values). It is **frozen paperwork**: no Gradio
295
+ runtime exists in code, and editing it would move `Config.hash`. It is left undisturbed.
296
+
297
+ ## 12. Deployment preconditions (open items)
298
 
299
  1. Cloudflare Pages project name / domain (needed for the deploy command and `robots.txt` sitemap).
300
+ 2. Artifacts present, **or** capabilities honestly ship `available: false` β€” the service degrades
301
+ rather than crashes on absent artifacts.
302
+ 3. `HF_TOKEN` on Render **if** the HF proxy path is used (not used in the live config).
303
+ 4. Codespace `.devcontainer/` forwarding `:8000` **and** starting the tunnel agent.
304
+ 5. The five blockers in Β§9 closed and verified on the first live run.
305
+
306
+ ## 13. What is `NOT RUN` / `OPEN` / `BLOCKED` for this topic
307
+
308
+ | Item | State |
309
+ |---|---|
310
+ | B-07 tunnel fallthrough | **OPEN** β€” patch prepared, NOT deployed |
311
+ | B-02 `codespace_name` trailing `\n` | **OPEN** (cosmetic) |
312
+ | A deployed system-level load test | **NOT RUN** |
313
+ | Multi-region / HA deployment | **NOT RUN** |
314
+ | A production database / persistence | **BY DESIGN** (absent) |
315
+ | The ZeroGPU/Gradio deployment target | **REJECTED** (superseded; frozen paperwork only) |
316
+
317
+ ## 14. Where the evidence lives
318
+
319
+ | Evidence | Location |
320
+ |---|---|
321
+ | Live health + capabilities probe | `release/CURRENT_RELEASE_STATE.md` Β§1 |
322
+ | Topology contract | `docs/DEPLOYMENT_TOPOLOGY.md`, `docs/DEPLOYMENT_ARCHITECTURE.md` |
323
+ | The undeployed B-07 patch | session scratch: `fix-b07-forward-unavailable.patch` |
324
+ | Deployed-artifact integrity check | `verify_deployed_head.py` (session scratch) |
325
+ | Live validation (3 passes) | `.workbuddy-ai/scratch/live_validation/` |