Image-to-Text
PyTorch
Safetensors
PEFT
English
remote-sensing
satellite-imagery
earth-observation
change-detection
visual-grounding
image-captioning
visual-question-answering
optical-sar-fusion
sar
multimodal
lora
Instructions to use thundercode/SatQuery with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use thundercode/SatQuery with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
| # 10 — Observability and Operations | |
| **Parent:** [Architecture hub](README.md) · **Status tags:** `IMPLEMENTED` · `VERIFIED` · | |
| `MEASURED` · `OPEN` · `DEFERRED` · `NOT RUN` | |
| **Sources of truth for this chapter, all read in full before writing:** | |
| | Source | What it establishes | | |
| |---|---| | |
| | `release/CURRENT_RELEASE_STATE.md` §1 (Phase-0 reconnaissance) | the **measured live health payload**, the three `completed` readings, B-02 and B-07 as captured on 2026-09-25 | | |
| | `release/repo/docs/DEPLOYMENT.md` §2, §5, §6, §7 | the same live payload as published, the cold-start statement, the platform traps | | |
| | `.workbuddy-ai/scratch/apply-test/main.py` (835 lines) | the **deployed orchestrator with the prepared B-07 patch applied** — `/api/health`, `_dispatch`, `_via_tunnel`, `ensure_codespace_up`, `X-SatQuery-Transport` | | |
| | `.workbuddy-ai/scratch/apply-test/tunnel.py` (241 lines) | `TunnelHub`, its `stats` property, `DEFAULT_REQUEST_TIMEOUT_S`, `DEFAULT_POLL_WAIT_S`, `DEFAULT_AGENT_TTL_S` | | |
| | `deploy/render/main.py` (532 lines, monorepo) | the *non-deployed* copy: no tunnel, simpler `/api/health` — kept because it documents the route's contract | | |
| | `deploy/render/codespaces.py` (178 lines) | the GitHub Codespaces control-plane client used by the wake path | | |
| | `app/deployment.py` (1232 lines) | `health_payload()`, `DeploymentReport.health()`, the contract-state derivation, `_effective_device`, `_LEGAL_DEVICES`, `_HUB_REASONS`, `_MISSING_REASONS` | | |
| | `app/space_app.py` (735 lines) | the Codespace's own `/v1/health`, the `HealthStatus.model_validate` assertion, `GPU_DURATIONS`, `decorate_gpu`, the log call sites | | |
| | `core/schemas.py` (462 lines) | `ExecutionTrace`, `TraceStep`, `ModelRef`, `HealthStatus`, `ControllerState`, `Intent`, `ConfidenceBreakdown`, `_new_id` | | |
| | `core/controller.py` (1331 lines) | the trace's producer: `_record`, `_execute`, `_verify`, `_detect_contradiction`, `_new_run_id`, the F-19 re-snapshot, `_log` | | |
| | `core/errors.py` (315 lines) | `SatQueryError.to_trace()`, `scrub_paths` | | |
| | `gateway/policy.py` (937 lines) | `new_request_id()` (`req_<hex>`), `translate_error` | | |
| | `gateway/app.py` (691 lines) | `_log.error` on transport failure, the no-retry rule, `_transport_failure_detail` | | |
| | `deploy/codespace/launch.sh` (194 lines) | the Codespace start procedure: serve + supervised tunnel agent + the post-start verification | | |
| | `deploy/codespace/warm_cache.py` (116 lines) | the four pinned HF models and the warm-up contract | | |
| | `deploy/codespace/serve.py` (25 lines) | the serve entrypoint the Codespace runs | | |
| | `DELIVERY_REPORT_2026-09-25.md` §4 | the B-07 root shape and the ≈249 s measurement | | |
| | `docs/FINAL_DELIVERY_TODO.md` §1.4, §5, §6 | the blocker register, the status board, the evidence register | | |
| > **The one rule that governs this whole chapter.** A claim here is only as good as the file it came | |
| > from. Where the code and a document disagree, the code is authoritative and the disagreement is | |
| > stated. Where neither answers, this chapter writes | |
| > `UNKNOWN — not established from the available evidence`. | |
| --- | |
| ## 1. Scope, and where observability sits | |
| Observability is the **weakest** subsystem in SatQuery AI, and this chapter says so on its first | |
| screen rather than on its last. The system was built to answer questions about satellite imagery | |
| correctly and to say when it cannot; it was **not** built to be monitored. There is no metrics | |
| endpoint, no dashboard, no alerting rule, no cost meter and no distributed trace. What exists is | |
| three narrow surfaces, and this chapter documents each one to the field. | |
| ### 1.1 The honest framing | |
| Three things are true and must be stated together: | |
| 1. **There is a real, measured liveness surface.** `GET /api/health` on the Render orchestrator | |
| answers with a live payload that includes a tunnel-agent block and the effective configuration | |
| (§2). It was probed during the 2026-09-25 sprint and its exact body is recorded. | |
| 2. **There is a real, per-run execution trace.** Every `POST /v1/analyze` returns a typed | |
| `ExecutionTrace` alongside its result (`core/schemas.py:296`), and that trace is the system's | |
| primary diagnostic artifact (§3). It is not a log line — it is a contract field. | |
| 3. **Everything else an operator would expect is absent.** No APM, no distributed tracing, no cost | |
| accounting, no per-model latency histogram, no error-rate counter (§6). The system observes | |
| *what a single run did*, and *whether the transport is currently up*. It does not observe the | |
| fleet, the trend, or the bill. | |
| ### 1.2 The three observation surfaces | |
| ```mermaid | |
| flowchart TD | |
| subgraph Surfaces["The three observation surfaces"] | |
| H["/api/health<br/>transport + config<br/>a SNAPSHOT"] | |
| T["ExecutionTrace<br/>one run, step by step<br/>a PER-RUN RECORD"] | |
| L["stdout logs<br/>operator diagnostics<br/>an UNSTRUCTURED STREAM"] | |
| end | |
| H --> Q1["Is the tunnel agent connected?<br/>What timeouts am I running?<br/>Which device am I on?"] | |
| T --> Q2["What did this run decide,<br/>which models ran,<br/>what failed, how long did it take?"] | |
| L --> Q3["What was the raw exception?<br/>Which URL did the proxy try?<br/>Which checkpoint was resolved?"] | |
| Q1 --> Who1["the operator, before a demo"] | |
| Q2 --> Who2["the client, the frontend, and the operator"] | |
| Q3 --> Who3["the operator, server-side only"] | |
| ``` | |
| | Surface | Owner | Lifetime | Audience | Contract-bound? | | |
| |---|---|---|---|---| | |
| | `GET /api/health` | the orchestrator | a point-in-time snapshot | operator, and any liveness probe | yes — the route is documented | | |
| | `ExecutionTrace` | the controller | one run, returned to the caller | the **client** and the operator | yes — `extra="forbid"` (`core/schemas.py:297`) | | |
| | server-side logs | every layer | the process lifetime | the operator only | **no** — free text | | |
| ### 1.3 What this chapter does not claim | |
| - It does **not** claim the system is observable. It claims three specific surfaces exist and | |
| enumerates exactly what each carries. | |
| - It does **not** claim any end-to-end latency number as a system property. Timings that exist are | |
| **per-step, per-run** (`trace.timings`, §3.6) and are the durations of the steps that actually | |
| ran in that run — not a benchmark. | |
| - It does **not** claim a health payload proves the *inference* works. `/api/health` never answers | |
| for the Codespace; that question belongs to `/api/capabilities`, and the code says so in the route | |
| docstring: *"Reports its own configuration; never answers for the Codespace (that is | |
| /api/capabilities)"* (`.workbuddy-ai/scratch/apply-test/main.py:678-679`). | |
| ### 1.4 Status of the observability surface | |
| | Surface | Status | Evidence | | |
| |---|---|---| | |
| | `/api/health` liveness route | `IMPLEMENTED` · `VERIFIED` (probed live) | measured payload, §2.1 | | |
| | `tunnel` block in the health payload | `VERIFIED` (present in the live payload) | §2.3 | | |
| | tunnel counters `pending` / `completed` | `IMPLEMENTED` · `MEASURED` (monotonic, three readings) | §2.3 | | |
| | `agent_connects` counter | `IMPLEMENTED` but **dead** — never incremented | §2.3 | | |
| | three timeouts reported in the payload | `VERIFIED` (`150.0` / `120.0` / `90.0`) | §2.4 | | |
| | `device` field | `IMPLEMENTED` · `VERIFIED` (`cpu`) with the F-8 normalisation | §2.5 | | |
| | `has_github_token` | `VERIFIED` (`true`) — a **boolean**, never the value | §2.6 | | |
| | `codespace_name` trailing `\n` | **`OPEN` (cosmetic)** — B-02 | §2.7 | | |
| | per-run `ExecutionTrace` | `IMPLEMENTED` · `VERIFIED` (present in all 24 live runs) | §3 | | |
| | `trace.contradiction` | `IMPLEMENTED`, and observed `false` in the live runs | §3.9 | | |
| | structured / queryable logs | **absent** | §4.4 | | |
| | metrics, APM, distributed tracing | **absent** | §6 | | |
| | cost accounting | **absent** | §6 | | |
| --- | |
| ## 2. The health payload | |
| ### 2.1 The measured live payload | |
| Probed live on 2026-09-25 against `https://<backend-host>/api/health` | |
| (`release/repo/docs/DEPLOYMENT.md` §2; `release/CURRENT_RELEASE_STATE.md` §1): | |
| ```json | |
| {"status":"ok","service":"satquery-orchestrator", | |
| "tunnel":{"agent_connected":true,"agent_id":"codespaces-fd1038","pending":0,"completed":97}, | |
| "config":{"codespace_name":"potential-space-trout-r4ppw969w45j2pvvw\n","codespace_port":8000, | |
| "transport_mode":"auto","tunnel_timeout_s":150.0,"wake_timeout_s":120.0, | |
| "upstream_timeout_s":90.0,"device":"cpu","has_github_token":true}} | |
| ``` | |
| The exact command recorded elsewhere in the project's evidence register is: | |
| ```bash | |
| curl --noproxy '*' https://<backend-host>/api/health | |
| ``` | |
| (`docs/FINAL_DELIVERY_REPORT.md` §4, cited in `release/repo/docs/architecture/02-deployment-topology.md` §7.1.) | |
| The payload has exactly **three top-level keys** — `status`, `service`, `tunnel` — plus `config`. | |
| There is no `version`, no `uptime`, no `build`, no `revision` field. An operator who wants to know | |
| *which revision is serving* cannot get that answer from this route; the revision is known only from | |
| the deploy record (`release/CURRENT_RELEASE_STATE.md` §1). | |
| ### 2.2 Field-by-field | |
| | Field | Type | Meaning | Live value | Source | | |
| |---|---|---|---|---| | |
| | `status` | string | the hub's own liveness — a literal, not a probe | `"ok"` | `apply-test/main.py:682` | | |
| | `service` | string | the service identity | `"satquery-orchestrator"` | `apply-test/main.py:683` | | |
| | `tunnel.agent_connected` | bool | is a tunnel agent currently polling, within the TTL? | `true` | `tunnel.py:131` | | |
| | `tunnel.agent_id` | string \| null | which agent identity holds the poll | `"codespaces-fd1038"` | `tunnel.py:132` | | |
| | `tunnel.pending` | int | requests parked and not yet answered | `0` | `tunnel.py:134` | | |
| | `tunnel.completed` | int | requests successfully resolved since the hub started | `97` | `tunnel.py:135` | | |
| | `config.codespace_name` | string | the target Codespace | `"…pvvw\n"` — **carries a trailing `\n`** | `apply-test/main.py:686` | | |
| | `config.codespace_port` | int | the inference port | `8000` | `apply-test/main.py:687` | | |
| | `config.transport_mode` | string | transport selection | `"auto"` | `apply-test/main.py:692` | | |
| | `config.tunnel_timeout_s` | float | tunnel park budget | `150.0` | `apply-test/main.py:693` | | |
| | `config.wake_timeout_s` | float | wake poll budget | `120.0` | `apply-test/main.py:694` | | |
| | `config.upstream_timeout_s` | float | proxy request timeout | `90.0` | `apply-test/main.py:695` | | |
| | `config.device` | string | declared device (raw env value) | `"cpu"` | `apply-test/main.py:696` | | |
| | `config.has_github_token` | bool | is a GitHub token configured? | `true` | `apply-test/main.py:688` | | |
| `status` is a **literal `"ok"`**, not the result of a check: | |
| ```python | |
| @app.get("/api/health") | |
| async def health() -> dict[str, Any]: | |
| """Orchestrator liveness. Reports its own configuration; never answers | |
| for the Codespace (that is /api/capabilities).""" | |
| hub = get_hub() | |
| return { | |
| "status": "ok", | |
| "service": "satquery-orchestrator", | |
| "tunnel": hub.stats, | |
| ... | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/main.py:676-684`) | |
| This is worth naming precisely, because it is a design decision with a consequence. The route | |
| answers `"ok"` whenever the **orchestrator process** is alive — which, on Render, is whenever the | |
| service has not crashed. It does not answer `"degraded"` when the tunnel agent is gone, and it does | |
| not answer `"error"` when the Codespace is stopped. An operator reading only `status` learns | |
| nothing beyond "the web service responds". The information that matters is in `tunnel` and | |
| `config`, and that is exactly where the reader should look. | |
| > This is **not** a defect and is **not** to be reported as one. It is the documented split: the | |
| > orchestrator reports *its own* state, and `/api/capabilities` is the route that answers for the | |
| > Codespace (`apply-test/main.py:678-679`). The Render blueprint uses this route as | |
| > `healthCheckPath` (`render.yaml:9`), which is precisely the "is the process up" question it | |
| > answers. | |
| ### 2.3 The `tunnel` block | |
| The `tunnel` block is `hub.stats` — the `stats` property of the process-wide `TunnelHub` singleton: | |
| ```python | |
| @property | |
| def stats(self) -> dict[str, Any]: | |
| return { | |
| "agent_connected": self.agent_connected(), | |
| "agent_id": self._agent_id, | |
| "agent_connects": self._agent_connected_count, | |
| "pending": len(self._pending), | |
| "completed": self._completed, | |
| } | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/tunnel.py:128-136`) | |
| The hub is a **single in-process object** with no persistence: | |
| ```python | |
| _HUB: Optional[TunnelHub] = None | |
| def get_hub() -> TunnelHub: | |
| """Process-wide singleton hub (one per Render instance).""" | |
| global _HUB | |
| if _HUB is None: | |
| _HUB = TunnelHub() | |
| return _HUB | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/tunnel.py:233-241`) | |
| Three consequences follow directly from "one in-process object, no persistence", and all three | |
| matter operationally: | |
| 1. **The counters reset whenever the Render instance restarts.** A free-tier Render service sleeps | |
| when idle and restarts on wake. After a restart, `completed` is `0` again. The counter is a | |
| *lifetime-of-this-process* counter, not a *lifetime-of-the-deployment* counter. | |
| 2. **`pending` is the only field that reflects current load.** It is `len(self._pending)` — the | |
| number of parked requests. `0` means no client is currently waiting. It is **not** a queue depth | |
| in any persistent sense. | |
| 3. **`agent_connected` is time-windowed, not event-driven.** It is computed on read: | |
| ```python | |
| def agent_connected(self) -> bool: | |
| """True if an agent has polled within the TTL window.""" | |
| if self._last_poll_at == 0.0: | |
| return False | |
| return (time.monotonic() - self._last_poll_at) <= self.agent_ttl_s | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/tunnel.py:113-117`) | |
| with `DEFAULT_AGENT_TTL_S = 60.0` (`tunnel.py:63`). So an agent that stopped polling 61 seconds ago | |
| reads `agent_connected: false` even though nothing crashed — the flag is a *freshness* statement, | |
| not a *liveness* one. This is why §7.3's diagnostic ("is this a tunnel gap?") starts by re-reading | |
| `/api/health` rather than trusting a single reading. | |
| #### `completed` was measured at three different values | |
| The tunnel counter is a **monotonic runtime counter**, not a fixed fact. Three measured readings | |
| exist, each with its own provenance: | |
| | Reading | Where recorded | | |
| |---|---| | |
| | `completed: 97` | `release/repo/docs/DEPLOYMENT.md` §2 (the live health probe) | | |
| | `completed: 314` | `docs/FINAL_DELIVERY_TODO.md` §1.4 and §6 E-02; `docs/DEPLOYMENT_TOPOLOGY.md` §2 | | |
| | `completed: 338` | `docs/FINAL_DELIVERY_REPORT.md` §3 P2 | | |
| They are consistent with one another — the counter grows — and the honest statement is | |
| **"`completed` was measured at 97, 314 and 338 at three different times on 2026-09-25."** Quoting | |
| any one of them as *the* value would be wrong. Any of them could also be `0` right now, if the | |
| Render instance has since slept. | |
| #### What increments `completed` — and what does not | |
| `completed` is incremented in exactly **one** place: | |
| ```python | |
| def complete(self, req_id: str, status: int, body: Any) -> bool: | |
| """Resolve a parked request. Returns False if it is gone (timed out).""" | |
| req = self._pending.get(req_id) | |
| if req is None: | |
| return False | |
| if not req.future.done(): | |
| req.future.set_result(TunnelResult(status=status, body=body)) | |
| self._completed += 1 | |
| return True | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/tunnel.py:213-221`) | |
| `fail()` — the sibling method the agent calls when it reports an error — does **not** touch the | |
| counter: | |
| ```python | |
| def fail(self, req_id: str, detail: str) -> bool: | |
| """Fail a parked request from the agent side (e.g. upstream error).""" | |
| req = self._pending.get(req_id) | |
| if req is None: | |
| return False | |
| if not req.future.done(): | |
| req.future.set_exception(RuntimeError(detail)) | |
| return True | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/tunnel.py:223-230`) | |
| So `completed` counts **successful deliveries only**, never attempts. A run in which every request | |
| failed would leave `completed` unchanged. An operator must not read `completed` as "requests | |
| handled"; it is "responses delivered back to a waiting client". A timed-out request is removed by | |
| the `finally` clause of `submit()` (`tunnel.py:175-176`) and likewise never counted. | |
| #### `agent_connects` is a dead counter | |
| The `stats` property emits a fifth key, `agent_connects`, sourced from `self._agent_connected_count`. | |
| That field is initialised to `0` and **never incremented anywhere in the file**: | |
| ``` | |
| 105: self._agent_connected_count: int = 0 | |
| 133: "agent_connects": self._agent_connected_count, | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/tunnel.py:105,133` — the only two occurrences.) | |
| `agent_connects` can therefore only ever read `0`. It is a counter that cannot move. This is | |
| recorded here as an observation, not as a bug report: no code reads it, no document promises it, and | |
| the **measured live payload does not even carry the key** (§2.9). It is a field that exists in the | |
| source and never appears in the served body. | |
| > **A `stats` note that matters for reading the live payload.** The live payload's `tunnel` block | |
| > has **four** keys (`agent_connected`, `agent_id`, `pending`, `completed`). The `stats` property in | |
| > the source copy has **five** (the extra being `agent_connects`). The two do not agree. Which one | |
| > the currently-running process serves is | |
| > `UNKNOWN — not established from the available evidence`; the divergence is recorded in §2.9 | |
| > alongside the `config` divergence. | |
| ### 2.4 The three timeouts | |
| The payload reports three timeouts. They are the three budgets that decide **how long a client can | |
| wait**, and they apply at different layers: | |
| | Field | Live value | Layer | What it bounds | Source | | |
| |---|---|---|---|---| | |
| | `config.tunnel_timeout_s` | `150.0` | orchestrator, tunnel path | how long a parked request waits for an agent to pick it up and return | `tunnel.py:55`, `main.py:152-153` | | |
| | `config.wake_timeout_s` | `120.0` | orchestrator, forward path | how long the wake loop polls `/v1/health` before giving up | `main.py:156-157`, `main.py:417-442` | | |
| | `config.upstream_timeout_s` | `90.0` | orchestrator, HTTP client | the per-request timeout on the proxied call to the Codespace | `main.py:160-161`, `main.py:253-256` | | |
| Each is an environment variable read at call time, not a constant: | |
| ```python | |
| def _tunnel_timeout_s() -> float: | |
| return float(os.environ.get("SATQUERY_TUNNEL_TIMEOUT_S", "150")) | |
| def _wake_timeout_s() -> float: | |
| return float(os.environ.get("SATQUERY_WAKE_TIMEOUT_S", "120")) | |
| def _upstream_timeout_s() -> float: | |
| return float(os.environ.get("SATQUERY_UPSTREAM_TIMEOUT_S", "90")) | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/main.py:152-161`) | |
| The tunnel timeout's default is documented in the hub with its reasoning: | |
| > *"How long a parked client request waits for an agent to pick it up before the hub gives up. | |
| > Generous: a cold Codespace can take ~90s to boot, and the agent only starts polling once the | |
| > model server is ready."* (`tunnel.py:52-55`) | |
| #### Two further budgets that are *not* in the payload | |
| An operator debugging latency will meet two more timeouts that `/api/health` does **not** report. | |
| They are recorded here so they are not mistaken for missing configuration: | |
| | Budget | Value | Layer | Source | | |
| |---|---|---|---| | |
| | `_WAKE_HEALTH_TIMEOUT_S` | `10.0` | orchestrator — the per-**poll** health timeout during a wake | `apply-test/main.py:107`; used at `:421` | | |
| | `_WAKE_POLL_INTERVAL_S` | `2.0` | orchestrator — the sleep between wake polls | `apply-test/main.py:106`; used at `:437` | | |
| | `DEFAULT_BUDGET_SECONDS` | `120.0` | **controller** — the run budget, checked *between* plan steps | `core/controller.py:121-122` | | |
| The first two are fixed literals in the deployed source, not env vars — so a `config` block that | |
| omitted them would be correct rather than incomplete. The third is the most interesting: the | |
| controller's own run budget is a **fourth** timeout, enforced in a completely different layer, and | |
| it appears in the trace rather than in the health payload (§3.8). | |
| The controller states the honest limit of that budget in the code: | |
| > *"Budget is checked BETWEEN steps (§5.2). A true per-step timeout needs a worker process or a | |
| > signal handler, both of which conflict with the single-process monolith constraint more than they | |
| > benefit. The honest position: v1 bounds total wall clock and records what it skipped."* | |
| > (`core/controller.py:548-552`) | |
| ### 2.5 `device` | |
| `config.device` in the health payload is the **raw environment value**: | |
| ```python | |
| "device": os.environ.get("SATQUERY_DEVICE", ""), | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/main.py:696`) | |
| The live value is `"cpu"`. That is the *declared* value, unnormalised. The **served** device value — | |
| the one the contract constrains — is a different field on a different route: `/v1/health`'s | |
| `device`, which is normalised and validated by `_effective_device` (`app/deployment.py:1093-1197`). | |
| The distinction matters because the two can disagree on a misconfigured deployment. `_effective_device` | |
| implements F-8: | |
| - it **casefolds and strips** before comparing, so `'CUDA'` and `'cuda'` cannot take different | |
| branches; | |
| - it validates against the contract's closed set, `_LEGAL_DEVICES = frozenset({"cpu", "cuda", "mps"})` | |
| (`app/deployment.py:1204`); | |
| - an unrecognised value returns **`None`**, not an echo and not a silent `"cpu"`. | |
| The reasoning is in the code, and it is worth quoting because it is the pattern the whole codebase | |
| uses for "the operator set something I do not recognise": | |
| > *"`None` is already a legal value for the field (`"cpu" | "cuda" | "mps" | null`), so it travels | |
| > the existing contract rather than inventing one; it is honest — 'the operator set something I do | |
| > not recognise' is not 'this deployment uses the CPU'; defaulting to `"cpu"` would mean a typo | |
| > silently changes which device the process is believed to use."* (`app/deployment.py:1156-1164`) | |
| Two measured defects preceded that fix, both recorded in the same docstring: | |
| | Defect | Symptom | Cause | | |
| |---|---|---| | |
| | A | `'cuda'` → `'cpu'` but `'CUDA'` → `'CUDA'` | the correction compared case-sensitively while `gpu_available` did not | | |
| | B | `'garbage'` → `device: "garbage"` | the reader accepted any string | | |
| (`app/deployment.py:1144-1152`) | |
| An operator reading `config.device` from `/api/health` is reading the **raw** value and must not | |
| expect it to be one of the four contract values. An operator reading `device` from `/v1/health` is | |
| reading the validated value and may rely on the closed set. This is one of the two places in the | |
| system where the same concept has two differently-normalised read sites; the other is the shared | |
| file-size cap (`docs/architecture/08-api-contract.md` §9). | |
| ### 2.6 `has_github_token` | |
| ```python | |
| "has_github_token": bool(os.environ.get("GITHUB_TOKEN")), | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/main.py:688`) | |
| The field is a **boolean**. It reports *whether* a token is configured, never the token, never its | |
| length, never a prefix, never a hash. The live value is `true`. There is no path by which the token | |
| value can reach this payload, and that is deliberate: the route is unauthenticated. | |
| Operationally, `has_github_token: false` means the **wake path cannot work** — `_github_token()` | |
| raises `OrchestratorConfigError` before any GitHub call (`apply-test/main.py:119-125`). A deployment | |
| in that state can still serve through the tunnel (the tunnel needs no GitHub token — the agent | |
| dials out), which is exactly why the two transports are separable. `has_github_token: false` is | |
| therefore **not** a total-outage signal; it is a "the forward fallback is unavailable" signal. | |
| ### 2.7 `codespace_name` and the trailing newline — B-02, `OPEN` (cosmetic) | |
| `config.codespace_name` reports `…pvvw\n`. This is **B-02**, and its status is `OPEN` **but | |
| cosmetic**: | |
| > *"`P2-T03` `/api/health` `codespace_name` trailing `\n` | DEFERRED (cosmetic) | Wake path is safe | |
| > (`_codespace_name()` strips, `main.py:123,357`); only the health payload reports the raw value."* | |
| > (`docs/FINAL_DELIVERY_REPORT.md` §6) | |
| > *"`B-02` | `/api/health` reports `codespace_name` with a trailing `\n` | **Cosmetic** — reporting | |
| > only; the wake path strips via `_codespace_name()` (`main.py:123,357`) | P2-T03 | none needed | | |
| > DOWNGRADED"* (`docs/FINAL_DELIVERY_TODO.md` §5) | |
| The mechanism is visible in the two call sites: | |
| - `_codespace_name()` — the **wake path** — strips: | |
| ```python | |
| def _codespace_name() -> str: | |
| name = os.environ.get("CODESPACE_NAME", "").strip() | |
| if not name: | |
| raise OrchestratorConfigError( | |
| "CODESPACE_NAME is not set; the orchestrator does not know which " | |
| "Codespace to target." | |
| ) | |
| return name | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/main.py:128-135`) | |
| - the **health payload** — in the deployed revision — does not strip. The prepared patch changes | |
| exactly this, to `os.environ.get("CODESPACE_NAME", "").strip()` | |
| (`.workbuddy-ai/scratch/apply-test/main.py:686`). | |
| The fix is known and one line — *"change line 619 to `_codespace_name()`, then Render redeploys"* | |
| (`docs/FINAL_DELIVERY_TODO.md` §4, P2-T03) — and the row's own reasoning for deferring is that *"a | |
| live-backend redeploy before the demo is not worth the risk."* | |
| **The live payload still carrying the `\n` is itself the evidence that the B-07 patch is not | |
| deployed.** `release/CURRENT_RELEASE_STATE.md` §1 states it exactly: *"Note `codespace_name` still | |
| carries the trailing `\n` → the B-07 patch is **not** deployed."* A cosmetic field is therefore | |
| doing double duty as the deployment-state witness — which is a small, real benefit of not having | |
| "cleaned it up". | |
| > **Do not upgrade this.** `B-02` is `OPEN`. It is not `RESOLVED`, and it is not `CLOSED`. | |
| ### 2.8 `transport_mode` | |
| ```python | |
| def _transport_mode() -> str: | |
| """Which transport to use: ``tunnel``, ``forward``, or ``auto`` (default). | |
| ``auto`` prefers the tunnel and falls back to the forwarded-port proxy only | |
| when no agent is connected. | |
| """ | |
| mode = os.environ.get("SATQUERY_TRANSPORT", "auto").strip().lower() | |
| return mode if mode in ("tunnel", "forward", "auto") else "auto" | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/main.py:142-149`) | |
| The live value is `"auto"`. The field is a **closed set** of three values, and an unrecognised | |
| value is coerced to `"auto"` rather than echoed — the same principle as `_effective_device`, applied | |
| one layer up. | |
| `auto` is the mode that produces B-07, and the reason is in the dispatch function: | |
| ```python | |
| mode = _transport_mode() | |
| hub = get_hub() | |
| use_tunnel = mode == "tunnel" or (mode == "auto" and hub.agent_connected()) | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/main.py:562-565`) | |
| Under `auto`, the transport decision is made **once, at dispatch time**, from `hub.agent_connected()`. | |
| If the agent was fresh at that instant, the request goes down the tunnel with a 150 s budget. If the | |
| tunnel then fails to complete the request, `mode != "tunnel"`, so the code does **not** return an | |
| error — it logs a warning and **falls through** to the forward path: | |
| ```python | |
| except TunnelTimeout as exc: | |
| if mode == "tunnel": | |
| # ... returns upstream_timeout (504) | |
| _log.warning("tunnel timed out, falling back to forwarded port: %s", exc) | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/main.py:593-610`) | |
| That fallthrough then burns `wake_timeout_s = 120` on a 302 from GitHub's relay for a private repo. | |
| The measured shape is **≈249 s ≈ 150 + 120** (`DELIVERY_REPORT_2026-09-25.md` §4). §7.3 and §7.4 | |
| treat this as the chapter's central operational fact. | |
| > **Note the `mode == "tunnel"` guard.** It is present in the *patched* source. The **deployed** | |
| > revision lacks the `ForwardUnavailable` early-exit, which is why the deployed system pays the full | |
| > 249 s. The patch converts "504 after 249 s" into "503 early with an actionable code" | |
| > (`DELIVERY_REPORT_2026-09-25.md` §4). | |
| ### 2.9 The monorepo copy, the scratch copy, and the deployed revision | |
| Three copies of the orchestrator exist on disk, and they are **not** the same file. This is the | |
| single most confusing thing about this subsystem, and it is stated here in one place. | |
| | Copy | Path | Lines | Tunnel? | `codespace_name` | Is it the deployed source? | | |
| |---|---|---|---|---|---| | |
| | monorepo | `deploy/render/main.py` | **532** | **no** | raw | **no** — stale and untracked | | |
| | scratch (patched) | `.workbuddy-ai/scratch/apply-test/main.py` | **835** | yes | `.strip()` | **no** — it is the deployed file **with the prepared patch applied** | | |
| | deployed | `SatQuery-Backend/main.py` @ `89d80eaddec5` | **768–769** | yes | raw | **yes** — but not on this disk | | |
| The monorepo copy declares a **simpler** `/api/health` with no `tunnel` block at all: | |
| ```python | |
| @app.get("/api/health") | |
| async def health() -> dict[str, Any]: | |
| """Orchestrator liveness. Reports its own configuration; never answers | |
| for the Codespace (that is /api/capabilities).""" | |
| return { | |
| "status": "ok", | |
| "service": "satquery-orchestrator", | |
| "config": { | |
| "codespace_name": os.environ.get("CODESPACE_NAME", ""), | |
| "codespace_port": _codespace_port(), | |
| "has_github_token": bool(os.environ.get("GITHUB_TOKEN")), | |
| "allowed_origins": _allowed_origins(), | |
| "production_origins": list(_PRODUCTION_ORIGINS), | |
| "dev_origins_enabled": _dev_origins_enabled(), | |
| "wake_timeout_s": _wake_timeout_s(), | |
| "upstream_timeout_s": _upstream_timeout_s(), | |
| "device": os.environ.get("SATQUERY_DEVICE", ""), | |
| }, | |
| } | |
| ``` | |
| (`deploy/render/main.py:444-466`) | |
| It is kept in the repository because it documents the **contract** of the route — the design | |
| decision that `allowed_origins` reports the **effective** list, so an operator can confirm *from | |
| outside* what the service will actually accept. The comment says the dev entries being visible | |
| *"is how a production deployment proves it turned them off"* (`deploy/render/main.py:455-458`). | |
| #### Why the scratch copy is believed to be "deployed + patch" | |
| The prepared patch's own presence check pins three changes to three line numbers | |
| (`docs/FINAL_DELIVERY_TODO.md` §6 E-12; `DELIVERY_REPORT_2026-09-25.md` §4): | |
| | Change | Documented location | Location in the scratch copy | | |
| |---|---|---| | |
| | `forward_unavailable` | `main.py:326` | `.workbuddy-ai/scratch/apply-test/main.py:326` | | |
| | `upstream_timeout` | `main.py:601` | `.workbuddy-ai/scratch/apply-test/main.py:601` | | |
| | `codespace_name` `.strip()` | `main.py:686` | `.workbuddy-ai/scratch/apply-test/main.py:686` | | |
| All three match exactly. That is strong evidence the scratch file is the deployed file with the | |
| patch applied, and it is why this chapter uses it as the source for the tunnel's behaviour while | |
| **labelling it as a scratch copy rather than the deployed revision**. Where the scratch copy and the | |
| measured live payload disagree, the disagreement is recorded below rather than smoothed over. | |
| #### The measured divergences | |
| | Element | Live payload | Scratch copy | Note | | |
| |---|---|---|---| | |
| | `tunnel` keys | 4 (`agent_connected`, `agent_id`, `pending`, `completed`) | 5 (adds `agent_connects`) | `agent_connects` is dead anyway (§2.3) | | |
| | `config` keys | 8 | 11 (adds `allowed_origins`, `production_origins`, `dev_origins_enabled`) | the live body omits the three origin fields | | |
| | `codespace_name` | raw (`…pvvw\n`) | `.strip()` | expected — the patch is not deployed | | |
| | `device` | raw env value | raw env value | agree | | |
| The `config` divergence is the one that is genuinely unexplained. Two readings are possible — a | |
| stale transcription, or an older deployed revision — and the evidence does not decide between them: | |
| > `UNKNOWN — not established from the available evidence` (which of "the live payload was | |
| > transcribed with three fields omitted" or "the live revision predates those three fields" is | |
| > correct). | |
| What *is* established is the **consequence**: an operator should not treat the absence of | |
| `allowed_origins` from the live payload as proof that the deployment has no CORS allowlist. The | |
| allowlist is enforced by `CORSMiddleware` at `create_app()` time regardless of what the payload | |
| prints (`apply-test/main.py:667-673`), and the CORS rule is documented in | |
| `docs/architecture/08-api-contract.md` §10. | |
| ### 2.10 The Codespace's own `/v1/health` | |
| There is a **second** health route, one tier deeper, and it is a different shape. The orchestrator's | |
| `/api/health` reports the *transport*; the Codespace's `/v1/health` reports the *deployment's | |
| capabilities*. | |
| ```python | |
| @api.get("/v1/health") | |
| async def health() -> JSONResponse: | |
| """Liveness + capability states. Loads no model (requirement 4). | |
| The body comes from `app.deployment.health_payload()`, which is the | |
| single public health source per the 2026-09-22 ruling. It is therefore | |
| the same traversal that builds `/v1/capabilities`, so the two routes | |
| cannot disagree about which capabilities exist -- the property the | |
| STEP 7 chain tests assert. | |
| ... | |
| """ | |
| from app.deployment import health_payload | |
| payload = health_payload() | |
| # Asserted rather than trusted: `HealthStatus` is `extra="forbid"`, so a | |
| # key added to the payload without a key added to the model would make | |
| # the response invalid against the project's own contract. Finding H-1 | |
| # was exactly this failure in the other direction. | |
| from core.schemas import HealthStatus | |
| HealthStatus.model_validate(payload) | |
| return JSONResponse(payload) | |
| ``` | |
| (`app/space_app.py:521-547`) | |
| The payload's field set is fixed by `HealthStatus`, which is `extra="forbid"`: | |
| ```python | |
| class HealthStatus(BaseModel): | |
| model_config = ConfigDict(extra="forbid") | |
| status: Literal["ok", "degraded", "error"] = "ok" | |
| schema_version: str = SCHEMA_VERSION | |
| models: dict[str, str] = Field(default_factory=dict) | |
| device: str | None = None | |
| gpu_available: bool = False | |
| ``` | |
| (`core/schemas.py:430-437`) | |
| The payload is built by `DeploymentReport.health()`: | |
| ```python | |
| def health(self) -> dict[str, Any]: | |
| """The `GET /v1/health` payload. Exactly `HealthStatus`'s field set. | |
| ... | |
| """ | |
| return { | |
| "status": self.status, | |
| "schema_version": _schema_version(), | |
| "models": self.models, | |
| "device": self.device, | |
| "gpu_available": self.gpu_available, | |
| } | |
| ``` | |
| (`app/deployment.py:774-788`) | |
| #### `status` is derived, not asserted | |
| Unlike the orchestrator's literal `"ok"`, the Codespace's `status` is computed from the capability | |
| states, and the rule is worth quoting in full because it encodes a contract decision: | |
| ```python | |
| @property | |
| def status(self) -> str: | |
| """`HealthStatus.status`, derived rather than asserted. | |
| The rule the contract states (`API_CONTRACT.md` section 2.1): | |
| `degraded` = *"the service is up but at least one expected artifact is | |
| absent"*. Two things follow, and both matter: | |
| * a capability that is merely `not_requested` does **not** degrade the | |
| service -- under `lazy_load: true` that is every capability on a | |
| fresh process, and reporting `degraded` for a healthy idle service | |
| would make the field useless as a probe; | |
| * `unavailable` (present but broken) is a defect. It is reported as | |
| `error`, not `degraded`, because the two have different remedies and | |
| `API_CONTRACT.md` section 5.2 already renders `model_load_error` as | |
| *"Defect -- surface it"*. | |
| """ | |
| states = {cap.state for cap in self.capabilities} | |
| if "unavailable" in states: | |
| return "error" | |
| if "absent" in states: | |
| return "degraded" | |
| return "ok" | |
| ``` | |
| (`app/deployment.py:703-725`) | |
| Three states, three meanings, and they are not interchangeable: | |
| | `status` | Trigger | What it means | | |
| |---|---|---| | |
| | `"ok"` | every capability is `loaded` or `not_requested` | nothing this build ships is missing | | |
| | `"degraded"` | at least one capability is `absent` | the service is up but an expected artifact is not in this deployment | | |
| | `"error"` | at least one capability is `unavailable` | something is **present but broken** — a defect, with a different remedy | | |
| The `models` map is capability → **contract state**, from the adapter's translation table | |
| (`app/deployment.py:698-701`), and the five contract states are the closed set | |
| `{"loaded", "absent", "unavailable", "not_requested", "evicted"}` (`app/deployment.py:175-177`). | |
| The adapter **never emits `evicted`** and says so rather than guessing, because eviction is a | |
| statement about the running model cache and no filesystem inspection can observe it | |
| (`app/deployment.py:70-73`). The full translation table is in | |
| `docs/architecture/08-api-contract.md` §6.6. | |
| #### `gpu_available` — a device probe, and a torch-free one | |
| ```python | |
| @property | |
| def gpu_available(self) -> bool: | |
| """Whether a CUDA device was *detected*, not whether one is allocated. | |
| ... | |
| """ | |
| import os | |
| if os.environ.get("SATQUERY_DEVICE", "").strip().lower() == "cuda": | |
| return True | |
| visible = os.environ.get("CUDA_VISIBLE_DEVICES", "").strip() | |
| if visible and visible != "-1": | |
| return True | |
| # `nvidia-smi` is not probed: shelling out on a health route would be a | |
| # per-request subprocess, and a Space's GPU is invisible until ZeroGPU | |
| # allocates it, so a negative answer from here would be misleading. | |
| return False | |
| ``` | |
| (`app/deployment.py:727-766`) | |
| On the live CPU deployment, `gpu_available: false` is the **expected and correct** answer, and the | |
| frontend is instructed not to surface it as a fault (`app/deployment.py:731-734`). | |
| The docstring records a **correction** that is itself an observability lesson: | |
| > *"DELIBERATELY DOES NOT IMPORT TORCH, and that is a correction. The first version probed | |
| > `torch.cuda.is_available()`, which meant the health payload pulled the entire torch stack on every | |
| > request. That breaks requirement 1 of `docs/DEPLOYMENT_ARCHITECTURE.md` section 3.3 — 'Import | |
| > cheaply and without torch' — and it made | |
| > `test_the_metadata_path_does_not_import_torch` fail, which is how it was found."* | |
| > (`app/deployment.py:736-744`) | |
| A liveness probe that costs a full model-stack import is not a liveness probe. This is the same | |
| principle as the orchestrator's decision to compute `agent_connected` on read rather than to keep a | |
| heartbeat thread: **the cheap answer is the right answer on a health route.** | |
| ### 2.11 What health does NOT tell you | |
| An operator who reads only the health routes will not learn any of the following. Each gap is | |
| stated with where the answer actually lives. | |
| | Question | Where the answer is | | |
| |---|---| | |
| | Which revision is deployed? | nowhere in a payload — only the deploy record (`release/CURRENT_RELEASE_STATE.md` §1) | | |
| | Is a model currently resident? | nowhere — `lazy_load: true` means the process cannot know without constructing | | |
| | How many requests have been served in total? | nowhere — `completed` is per-Render-process and counts only tunnel deliveries (§2.3) | | |
| | What is the error rate? | nowhere — no counter exists (§6) | | |
| | How long did the last run take? | in that run's `trace.timings` (§3.6), not in health | | |
| | Is the Codespace warm? | partly — `tunnel.agent_connected` is a proxy, and only for the tunnel path | | |
| | Is the GPU allocated? | nowhere — `gpu_available` is a *device presence* probe, not an allocation state | | |
| | Is the token valid? | nowhere — `has_github_token` is a boolean, and validity is only learned by calling GitHub | | |
| --- | |
| ## 3. The per-run `ExecutionTrace` | |
| ### 3.1 Why the trace exists | |
| The trace is the system's primary diagnostic artifact, and it exists because the *result* alone | |
| cannot answer "why". A `SpecialistResult` says what was found; the `ExecutionTrace` says **which | |
| route the system took to find it** — which task was chosen, which models ran, what failed, what was | |
| skipped, and how long each step took. | |
| Its defining constraint is stated in the module banner and repeated at the producer: | |
| > *"Execution trace (observable facts only — never chain-of-thought)"* (`core/schemas.py:277`) | |
| > *"Append one `TraceStep`. Observable facts only — no chain-of-thought."* | |
| > (`core/controller.py:1194`) | |
| This is a hard line, not a style preference. The trace is **returned to the client** — it is a field | |
| of `ResultEnvelope`, which `POST /v1/analyze` returns verbatim — and v1 has no authentication. So | |
| anything placed in the trace is public. The trace therefore carries *what happened*, never *what the | |
| model was thinking*, and never a server-side path (§3.11). | |
| ### 3.2 The field table | |
| `ExecutionTrace` is `extra="forbid"` (`core/schemas.py:297`), so this field set is the contract. | |
| | Field | Type | Populated by | Meaning | | |
| |---|---|---|---| | |
| | `run_id` | str | default `_new_id("run")`, or `request.run_id` | the run identifier (§5.1) | | |
| | `schema_version` | str | `SCHEMA_VERSION` = `"1.0"` | the contract version | | |
| | `task` | `Task \| null` | `prediction.intent.task`, else `request.force_task` | the task actually chosen | | |
| | `query` | str \| null | `request.query` | the natural-language question | | |
| | `inputs` | list[str] | `[_asset_label(a) for a in request.assets]` | **basenames** of the inputs (F-13) | | |
| | `modalities` | list[`Modality`] | `self._modalities(resolved)` | the resolved modalities | | |
| | `intent` | `Intent \| null` | `prediction.intent` | the router's reading, or null | | |
| | `validation` | dict | default `{}` | reserved; not populated on the serving path | | |
| | `workflow` | list[str] | `[step.step_id for step in plan.steps]` | the planned step ids | | |
| | `steps` | list[`TraceStep`] | `self._record(...)` | the step records (§3.3) | | |
| | `selected_models` | list[`ModelRef`] | `self._selected_models(outcomes)` | the models that ran (§3.5) | | |
| | `parameters` | dict | `plan.to_trace()` + `budget_seconds` + `registry` | the plan's parameters and the registry snapshot | | |
| | `outputs` | list[str] | `[e.evidence_id for e in result.evidence]` | the evidence ids produced | | |
| | `confidence` | `ConfidenceBreakdown \| null` | `result.confidence` | the confidence breakdown | | |
| | `timings` | dict[str, float] | `trace.timings[step.step_id] = ...` | per-step durations (§3.6) | | |
| | `fallbacks` | list[str] | `plan.notes`, `_verify`, and others | what degraded (§3.7) | | |
| | `errors` | list[dict] | `_execute`, `_verify` | what failed (§3.8) | | |
| | `contradiction` | bool | `_detect_contradiction` | whether disjoint spatial claims were found (§3.9) | | |
| | `config_hash` | str \| null | `self.config.hash` | the frozen config hash (§3.10) | | |
| | `started_at` | str | `_utcnow()` default | ISO-8601 UTC | | |
| | `finished_at` | str \| null | `trace.finished_at = self._now()` | ISO-8601 UTC | | |
| The trace is assembled **once**, in `AnalysisController.run()`: | |
| ```python | |
| started = time.perf_counter() | |
| run_id = request.run_id or self._new_run_id() | |
| trace = ExecutionTrace( | |
| run_id=run_id, | |
| query=request.query, | |
| ... | |
| inputs=[_asset_label(a) for a in request.assets], | |
| config_hash=self.config.hash if self.config is not None else None, | |
| ) | |
| self._record(trace, ControllerState.RECEIVE, {"asset_count": len(request.assets)}) | |
| ``` | |
| (`core/controller.py:258-279`) | |
| and it is attached to the result at the very end: | |
| ```python | |
| result.execution_trace = trace | |
| trace.outputs = [e.evidence_id for e in result.evidence] | |
| trace.confidence = result.confidence | |
| self._record(trace, ControllerState.RESPOND, {"answer_length": len(result.answer)}) | |
| trace.finished_at = self._now() | |
| return ResultEnvelope(run_id=run_id, result=result, trace=trace) | |
| ``` | |
| (`core/controller.py:388-394`) | |
| Note that `trace.confidence` is **the same object** as `result.confidence` — not a copy, not a | |
| recomputation. There is one confidence computation per run, and the trace references it. This is why | |
| the two can never disagree (`docs/architecture/06-evidence-and-confidence.md`). | |
| ### 3.3 `TraceStep` | |
| ```python | |
| class TraceStep(BaseModel): | |
| model_config = ConfigDict(extra="forbid") | |
| state: ControllerState | |
| started_at: str = Field(default_factory=_utcnow) | |
| duration_ms: float | None = None | |
| detail: dict[str, Any] = Field(default_factory=dict) | |
| ``` | |
| (`core/schemas.py:279-285`) | |
| A step is created by exactly one method: | |
| ```python | |
| def _record( | |
| self, | |
| trace: ExecutionTrace, | |
| state: ControllerState, | |
| detail: dict[str, Any] | None = None, | |
| ) -> None: | |
| """Append one `TraceStep`. Observable facts only — no chain-of-thought.""" | |
| trace.steps.append( | |
| TraceStep(state=state, detail=self._jsonable(detail or {})) | |
| ) | |
| ``` | |
| (`core/controller.py:1188-1197`) | |
| `_jsonable` makes the detail dict serialisable *without dropping information* | |
| (`core/controller.py:1199-1202`) — the honest-encoding rule applied to the trace. | |
| Two properties of `TraceStep` are worth naming: | |
| - **`duration_ms` is `None` on every step `_record` creates.** `_record` does not measure. The | |
| per-step durations live in `trace.timings`, keyed by `step_id`, and come from the *execution* | |
| path (§3.6). A reader must not expect `steps[i].duration_ms` to be populated. | |
| - **`detail` is free-form.** Each call site chooses its keys. The keys observed in the code are | |
| enumerated in §3.4. | |
| ### 3.4 The nine-state spine | |
| `ControllerState` is a nine-value enum: | |
| ```python | |
| class ControllerState(str, Enum): | |
| RECEIVE = "RECEIVE" | |
| PARSE = "PARSE" | |
| VALIDATE = "VALIDATE" | |
| PLAN = "PLAN" | |
| PREPROCESS = "PREPROCESS" | |
| EXECUTE = "EXECUTE" | |
| AGGREGATE = "AGGREGATE" | |
| VERIFY = "VERIFY" | |
| RESPOND = "RESPOND" | |
| ``` | |
| (`core/schemas.py:79-88`) | |
| The controller records against a subset of these. Reading `run()` end to end | |
| (`core/controller.py:258-394`), the states that actually produce a `TraceStep` are: | |
| | State | Recorded when | `detail` keys observed in the code | | |
| |---|---|---| | |
| | `RECEIVE` | always | `asset_count` | | |
| | `PARSE` | always | `inputs`, `modalities`, `query_length` | | |
| | `VALIDATE` | always | `assets`, `force_task`, `router` | | |
| | `PLAN` | always | `plan` | | |
| | `EXECUTE` | once per plan step | the `StepOutcome.to_trace()` dict (§3.6) | | |
| | `AGGREGATE` | always | `evidence` (a summary), `degraded` | | |
| | `VERIFY` | always | `contradiction` | | |
| | `RESPOND` | always | `answer_length` | | |
| `PREPROCESS` is **not** recorded by the controller. The state exists in the enum and in the frontend's | |
| trace spine, but `run()` never calls `_record(..., ControllerState.PREPROCESS, ...)`. This is the | |
| source of the trace bar's 94.4444 % fill (§3.12 and | |
| `docs/architecture/09-frontend.md` §6) — the frontend's spine has nine states and one of them is | |
| never reached on the serving path. | |
| The refusal path records a different subset: `_finish_refusal` returns early after `PLAN`, so a | |
| refused query's trace has no `EXECUTE`, `AGGREGATE`, `VERIFY` or `RESPOND` step | |
| (`core/controller.py:344-345`). | |
| ### 3.5 `ModelRef` and `selected_models` | |
| ```python | |
| class ModelRef(BaseModel): | |
| model_config = ConfigDict(extra="forbid") | |
| name: str | |
| revision: str | None = None | |
| role: str | None = None | |
| ``` | |
| (`core/schemas.py:288-293`) | |
| `selected_models` is built from each specialist that ran, deduplicated on `name:role`: | |
| ```python | |
| def _selected_models(self, outcomes: Iterable[StepOutcome]) -> list[ModelRef]: | |
| """Model identities for the trace, from each specialist that ran. | |
| Built as `ModelRef` rather than raw dicts: `ExecutionTrace.selected_models` | |
| is typed `list[ModelRef]`, and pydantic would coerce a dict only by | |
| emitting a serialization warning. Constructing the type explicitly keeps | |
| the contract honest instead of relying on coercion. | |
| """ | |
| refs: list[ModelRef] = [] | |
| seen: set[str] = set() | |
| for outcome in outcomes: | |
| specialist = ( | |
| outcome.registry_entry.specialist if outcome.registry_entry else None | |
| ) | |
| if specialist is None: | |
| continue | |
| for ref in specialist.model_refs(): | |
| key = f"{ref.get('name')}:{ref.get('role')}" | |
| if key in seen: | |
| continue | |
| seen.add(key) | |
| refs.append( | |
| ModelRef( | |
| name=str(ref.get("name", "unknown")), | |
| revision=ref.get("revision"), | |
| role=ref.get("role"), | |
| ) | |
| ) | |
| return refs | |
| ``` | |
| (`core/controller.py:721-749`) | |
| Three facts follow, all of which matter when reading a trace: | |
| 1. **`selected_models` is populated from the *specialist that ran*, not from the registry.** A | |
| capability that was planned but could not be constructed contributes nothing, because | |
| `registry_entry.specialist` is `None` (`core/controller.py:612-618`). | |
| 2. **Deduplication is on `name:role`,** so the same model appearing under two roles appears twice. | |
| 3. **`revision` may be `None`.** The field is optional in both `ModelRef` and the source dicts. | |
| The `registry` snapshot in `trace.parameters["registry"]` is **re-snapshotted after execution**, and | |
| the reason is a measured defect: | |
| > *"F-19 (owner ruling 2026-09-23): re-snapshot the registry AFTER execution. | |
| > `registry.describe()` reports the registry's MEMOISED entries, so the snapshot taken in PLAN above | |
| > reports the PREVIOUS request's builds -- while `trace.selected_models`, populated inside | |
| > `_execute`, reports THIS one. That made a single trace carry two clocks: on a cold process the | |
| > first request's trace said `registry.built == {}` while its own run had built `optical_sar`. | |
| > Measured in pass 13 with two identical payloads on one controller: request #1 body | |
| > `built == []`, request #2 body `built == ['optical_sar']`."* (`core/controller.py:350-359`) | |
| So `trace.parameters["registry"]` and `trace.selected_models` are now consistent with each other by | |
| construction. A reader can use either, and they will agree. | |
| ### 3.6 `timings` | |
| Per-step durations are written in the execution loop: | |
| ```python | |
| outcome = self._execute_one(step, assets, request, trace) | |
| outcomes.append(outcome) | |
| self._record(trace, ControllerState.EXECUTE, outcome.to_trace()) | |
| trace.timings[step.step_id] = round(outcome.duration_ms, 3) | |
| ``` | |
| (`core/controller.py:573-576`) | |
| `duration_ms` is measured with `time.perf_counter()` around the construct-validate-execute sequence | |
| (`core/controller.py:609-708`), so a step's timing **includes model construction** — which is | |
| exactly what an operator wants, because on a cold process construction dominates. It is rounded to | |
| **three decimal places** (`round(..., 3)`) at both the `timings` write and in | |
| `StepOutcome.to_trace()` (`core/controller.py:161`). | |
| The key is `step.step_id`, and the `StepOutcome.to_trace()` dict recorded into `steps` carries a | |
| matching `step_id`, so the two can be joined: | |
| ```python | |
| def to_trace(self) -> dict[str, Any]: | |
| return { | |
| "step_id": self.step.step_id, | |
| "capability": self.step.capability, | |
| "ok": self.ok, | |
| "skipped_reason": self.skipped_reason, | |
| "error_code": self.error.code if self.error else None, | |
| "duration_ms": round(self.duration_ms, 3), | |
| "registry_state": ( | |
| self.registry_entry.state.value if self.registry_entry else None | |
| ), | |
| } | |
| ``` | |
| (`core/controller.py:154-165`) | |
| > **What `timings` is not.** It is not a benchmark and it is not a system-level latency claim. It | |
| > is the wall-clock duration of the steps that ran in *that one run*, on *that one host*. There is | |
| > no aggregated latency statistic anywhere in the system, and no end-to-end benchmark exists | |
| > (`DOCS_STYLE_GUIDE.md` §3). | |
| ### 3.7 `fallbacks` | |
| `fallbacks` is a list of human-readable strings naming what degraded. It is appended from more than | |
| one place, and the two observed writers are: | |
| **From the plan's notes**, in the aggregate path: | |
| ```python | |
| trace.fallbacks.extend(plan.notes) | |
| ``` | |
| (`core/controller.py:778`) | |
| **From `_verify`**, when a successful specialist left no evidence: | |
| ```python | |
| for outcome in successful: | |
| if outcome.result is not None and not outcome.result.evidence: | |
| trace.fallbacks.append( | |
| f"{outcome.capability} produced a result with no evidence items" | |
| ) | |
| ``` | |
| (`core/controller.py:1143-1147`) | |
| There is a third writer recorded in the source at `core/controller.py:1145` context and one at | |
| `:1145`/`:1145`… the list is open-ended: any code holding the trace may append. The honest statement | |
| is that `fallbacks` is a **free-text list of degradation notes**, not a closed vocabulary, and a | |
| consumer must not parse it. | |
| The distinction between `fallbacks` and `errors` is the distinction between *"something ran | |
| differently than the ideal"* and *"something failed"*. A fallback is a degraded-but-successful | |
| outcome; an error is a failure. Both appear in the same trace, and neither implies the other. | |
| ### 3.8 `errors` | |
| `errors` is a list of dicts, appended from two places. | |
| **From the execution loop, for a step that failed:** | |
| ```python | |
| if outcome.error is not None: | |
| # F-15 (owner ruling 2026-09-23): the trace is CLIENT-FACING, so | |
| # it carries the operator-safe message only. It previously | |
| # preferred `error.detail` -- the technical text, which can name | |
| # filesystem paths and library internals -- over `user_message`. | |
| # The technical detail stays available on the `StepOutcome` | |
| # server-side, which is where a maintainer reads it. | |
| trace.errors.append( | |
| { | |
| "step_id": step.step_id, | |
| "capability": step.capability, | |
| "code": outcome.error.code, | |
| "message": outcome.error.user_message | |
| or "The step failed.", | |
| "recoverable": outcome.error.recoverable, | |
| } | |
| ) | |
| ``` | |
| (`core/controller.py:578-594`) | |
| **From the execution loop, for a step skipped on budget:** | |
| ```python | |
| trace.errors.append( | |
| { | |
| "step_id": step.step_id, | |
| "code": SpecialistTimeoutError.code, | |
| "message": f"skipped: run budget of " | |
| f"{self.budget_seconds}s exceeded", | |
| } | |
| ) | |
| ``` | |
| (`core/controller.py:560-567`) | |
| **From `_verify`, when a specialist produced no evidence:** | |
| ```python | |
| trace.errors.append( | |
| { | |
| "step_id": "*", | |
| "code": "evidence_loss", | |
| "message": f"specialists produced no evidence: {sorted(missing)}", | |
| } | |
| ) | |
| ``` | |
| (`core/controller.py:1135-1141`) | |
| Three properties of the `errors` list: | |
| 1. **The keys are not uniform.** A budget-skip entry has no `capability` and no `recoverable`; an | |
| `evidence_loss` entry uses `step_id: "*"`. A consumer must tolerate missing keys. | |
| 2. **`message` is the operator-safe `user_message`, never `detail`.** This is F-15, and the | |
| reasoning is quoted in the code above: `detail` can name paths and library internals, and the | |
| trace is client-facing. | |
| 3. **The message is a *classification*, not the exception text.** For an unhandled failure, the | |
| client sees the fixed sentence *"The step failed with an unhandled error. Internal detail is | |
| withheld; see the server-side diagnostics."* (`core/controller.py:689-692`), while the raw | |
| exception goes to the log (§4.3). | |
| `errors` being non-empty does **not** imply the run failed. A run can have a failed step and still | |
| return a result — *"Run each step, recording outcomes. Never aborts on failure."* | |
| (`core/controller.py:543`). The failure semantics live in the result, not in the trace. | |
| ### 3.9 `contradiction` | |
| `contradiction` is a single boolean, set by a detector that records and never resolves: | |
| ```python | |
| @staticmethod | |
| def _detect_contradiction( | |
| trace: ExecutionTrace, | |
| result: SpecialistResult, | |
| outcomes: Sequence[StepOutcome], | |
| ) -> None: | |
| """Detect and record; never resolve (design §7.6). | |
| Two steps naming disjoint places in the same coordinate system are a | |
| contradiction. Both claims are kept; only the flag and a warning are | |
| added. Cross-system comparison is skipped because IoU across coordinate | |
| systems is meaningless. | |
| """ | |
| claims: dict[str, list[Box]] = {} | |
| for outcome in outcomes: | |
| if not outcome.ok or outcome.result is None: | |
| continue | |
| for box in outcome.result.boxes: | |
| claims.setdefault(box.coordinate_system.value, []).append(box) | |
| for system, boxes in claims.items(): | |
| if len(boxes) < 2: | |
| continue | |
| for i in range(len(boxes)): | |
| for j in range(i + 1, len(boxes)): | |
| if _iou(boxes[i], boxes[j]) < CONTRADICTION_IOU_FLOOR: | |
| trace.contradiction = True | |
| message = ( | |
| "specialists produced contradictory spatial claims " | |
| f"in {system}; both are retained in evidence" | |
| ) | |
| if message not in result.warnings: | |
| result.warnings.append(message) | |
| return | |
| ``` | |
| (`core/controller.py:1151-1184`) | |
| The threshold is frozen and named: | |
| ```python | |
| CONTRADICTION_IOU_FLOOR: float = 0.1 | |
| ``` | |
| (`core/controller.py:119`) | |
| Two observability consequences: | |
| - **A `true` contradiction is visible in two places at once** — `trace.contradiction` and a string | |
| in `result.warnings` — and the detector deliberately keeps both claims rather than picking a | |
| winner. The rationale is at `core/controller.py:57-63`: *"Selecting a winner requires a precedence | |
| weight"*, which the system does not have. | |
| - **The comparison is scoped per coordinate system.** A box in one CRS is never compared to a box in | |
| another, because *"IoU across coordinate systems is meaningless"*. So `contradiction: false` does | |
| **not** mean "no two claims disagree" — it means "no two claims *in the same coordinate system* | |
| were disjoint". A reader must not over-read the flag. | |
| In the live validation runs, `contradiction` was `false` throughout — no run produced two disjoint | |
| in-system spatial claims. That is the expected outcome for the eight single- and pair-task cases | |
| (`DELIVERY_REPORT_2026-09-25.md` §3). | |
| ### 3.10 `config_hash` | |
| ```python | |
| config_hash=self.config.hash if self.config is not None else None, | |
| ``` | |
| (`core/controller.py:277`) | |
| On the serving path `self.config` is always set (`app/serving.py:259-265`), so `config_hash` is | |
| always populated. Its value is the **frozen config hash `78f1e3700da15aa1`** — a sha256 over the | |
| whole config registry, and an invariant the project treats as frozen | |
| (`DOCS_STYLE_GUIDE.md` §3; `app/serving.py:95`; `deploy/codespace/README.md:48-52`). | |
| The field is `str | None`, and `None` is reachable only on a controller built without a config — | |
| which the serving composition root does not do. Its observability value is that a run's trace | |
| **self-identifies the config that produced it**, which is what makes a stored trace re-interpretable | |
| later. Editing `configs/base.yaml` or `configs/deploy.yaml` moves the hash, so a trace carrying a | |
| different hash is provably from a different configuration. | |
| ### 3.11 What the trace deliberately does not contain | |
| The trace is client-facing, and two rulings removed content from it. Both are worth recording | |
| because they are *observability design decisions* — the system chose to be less informative to the | |
| client in order to be safe. | |
| **F-13 — no server-side paths in `inputs`:** | |
| > *"F-13. `inputs` is a CLIENT-VISIBLE field -- `ResultEnvelope` is returned verbatim by | |
| > `POST /v1/analyze`, `trace` included -- and it was echoing `request.assets` after the caller had | |
| > turned asset *handles* into filesystem *paths*. The response therefore named the server-side | |
| > location of every uploaded byte, which is the exact disclosure `AssetHandle.to_response()` refuses | |
| > to make."* (`core/controller.py:263-268`) | |
| The measured disclosure is recorded with the exact bytes: | |
| > *"Measured 2026-09-22 end-to-end through the real Space: the client sent the handle | |
| > `asset_d243f7f85d8c2f3c02981f0af9737f01` and received back | |
| > `C:\Users\anish\sq_scratch\...\assets\asset_d243...7f01.tif`."* (`core/controller.py:292-295`) | |
| **F-14 — the same disclosure again, in `steps[].detail`:** | |
| > *"F-14. The F-13 disclosure again, one record later. `trace.steps[]` is part of the same | |
| > client-visible `ExecutionTrace` ... and this PARSE detail was writing `list(request.assets)` -- | |
| > which by now are the PATHS the Space derived from the client's handles -- so the response named | |
| > the server-side location of every uploaded byte in a second place."* (`core/controller.py:287-292`) | |
| Both are now reduced to a **basename** by one shared helper, `_asset_label`, which the code | |
| describes as *"the ONE place the basename rule lives"* (`core/controller.py:296-299`). A reader of a | |
| trace therefore sees `asset_d243f7f85d8c2f3c02981f0af9737f01` (the handle) or a bare filename — | |
| never a directory. | |
| **F-15 / F-20 — no raw exception text:** | |
| The trace's `errors[].message` is `user_message`, and the raw exception is logged server-side only | |
| (§4.3). F-20's own account of why is the clearest statement of the split in the repository: | |
| > *"F-20 (owner ruling 2026-09-23) — F-15 WAS ONLY HALF IMPLEMENTED. The paragraph above was true | |
| > of the trace and false of everything else: `_warnings` and `_failure_evidence` publish | |
| > `scrub_paths(error.detail)`, and `detail` was `f"unhandled {type(exc).__name__}: {exc}"` -- so ONE | |
| > unhandled failure reached the client as 'Internal detail is withheld' in | |
| > `trace.errors[].message` and as 'unhandled RuntimeError: kaboom' in `result.warnings[]` and | |
| > `evidence[].payload["message"]`. The response contradicted itself about what it had decided to | |
| > disclose."* (`core/controller.py:650-658`) | |
| The repair was made **at the producer, not at each carrier** — a single `detail` that is safe to | |
| publish makes all three carriers agree by construction (`core/controller.py:660-664`). | |
| So the trace is not a debugging dump. It is a **curated, publishable record** of observable facts, | |
| and the curating is done at the source. | |
| ### 3.12 The trace is client-facing — the consequence | |
| Every field in §3.2 travels to the browser. That single fact explains most of the design: | |
| | Consequence | Where it is enforced | | |
| |---|---| | |
| | No chain-of-thought | `core/schemas.py:277`; `core/controller.py:1194` | | |
| | No server-side paths | F-13/F-14, `core/controller.py:276,304` | | |
| | No raw exception text | F-15/F-20, `core/controller.py:585-594,660-664` | | |
| | Errors carry a `recoverable` flag the client can act on | `core/controller.py:592` | | |
| | A degraded run still returns a trace | `core/controller.py:543` | | |
| And it is why the trace is **also** the frontend's animation source. The Analyze console's trace bar | |
| is driven by the eight execution events, and the bar's measured fill is **94.4444 %** — the | |
| arithmetic being `((8 + 0.5) / 9) * 100` over the nine-state spine, with `PREPROCESS` the state the | |
| serving path never reaches (`docs/architecture/09-frontend.md` §6; `frontend/assets/js/mission.js:422-424`). | |
| That figure is an **artifact metric about the frontend's own progress indicator**, not a | |
| system-level claim, and it must never be quoted as one. | |
| --- | |
| ## 4. What is and is not logged | |
| ### 4.1 The loggers | |
| Five loggers exist across the request path. Their names are the diagnostic namespaces an operator | |
| will see: | |
| | Logger name | Declared at | Layer | | |
| |---|---|---| | |
| | `satquery.orchestrator` | `deploy/render/main.py:74`; `apply-test/main.py:103` | the Render orchestrator | | |
| | `satquery.space` | `app/space_app.py:463` | the Codespace FastAPI app | | |
| | `satquery.tunnel` | `.workbuddy-ai/scratch/apply-test/tunnel.py:50` | the tunnel hub | | |
| | `__name__` of `gateway.app` | `gateway/app.py:123` | the in-process gateway (`gateway.app`) | | |
| | `__name__` of `core.controller` | `core/controller.py:129` | the analysis controller (`core.controller`) | | |
| | `__name__` of `app.serving` | `app/serving.py:66` | the composition root (`app.serving`) | | |
| No logger is configured with a level, a formatter or a handler anywhere in the code read. The | |
| effective level and destination are therefore whatever the host process sets — uvicorn's defaults on | |
| the Codespace, Render's defaults on the orchestrator: | |
| > `UNKNOWN — not established from the available evidence` (the effective log level and the log | |
| > destination on each host; no logging configuration was found in the sources read). | |
| ### 4.2 Every log call site | |
| This is the complete set of log calls found in the sources read. There are **six**. | |
| | # | File:line | Level | Logger | Message | Carries | | |
| |---|---|---|---|---|---| | |
| | 1 | `core/controller.py:676` | `error` | `core.controller` | `"unhandled failure in step %s (capability=%s): %s: %s"` + `exc_info` | step_id, capability, exception **type name**, exception **message**, traceback | | |
| | 2 | `app/space_app.py:510` | `exception` | `satquery.space` | `"unhandled failure on %s %s"` | method, path, traceback | | |
| | 3 | `app/space_app.py:690` | `warning` | `satquery.space` | `"invalid_request on POST /v1/analyze: %s: %s"` | exception type name, full `ValidationError` text | | |
| | 4 | `gateway/app.py:565` | `error` | `gateway.app` | `"upstream transport failure for request_id=%s: %s: %s"` + `exc_info` | request_id, exception type, exception text, traceback | | |
| | 5 | `deploy/render/main.py:384` | `warning` | `satquery.orchestrator` | `"upstream connection error to %s: %s"` | URL, exception | | |
| | 6 | `deploy/render/main.py:393` | `warning` | `satquery.orchestrator` | `"upstream request error to %s: %s"` | URL, exception | | |
| | 7 | `deploy/render/main.py:405` | `error` | `satquery.orchestrator` | `"non-JSON upstream response from %s (status %s)"` | URL, status | | |
| | 8 | `app/serving.py:203` | `info` | `app.serving` | `"optical_sar: CROMA checkpoint resolved via %s (%s)"` | source, `present`/`absent` | | |
| | 9 | `apply-test/main.py:610` | `warning` | `satquery.orchestrator` | `"tunnel timed out, falling back to forwarded port: %s"` | exception | | |
| | 10 | `apply-test/main.py:623` | `warning` | `satquery.orchestrator` | `"tunnel error, falling back to forwarded port: %s"` | exception | | |
| (Calls 5–7 are the monorepo copy; the same three exist in the scratch copy with the tunnel ones | |
| added. `gateway/assets.py` and `gateway/policy.py` contain **no** log calls at all — verified by | |
| grep.) | |
| Two of these are the whole reason the operator-facing diagnostic story works: | |
| **Call 1 — the unhandled specialist failure.** This is F-20's entire point. The client receives a | |
| sanitized message; the operator receives the exception *and its traceback*: | |
| ```python | |
| _log.error( | |
| "unhandled failure in step %s (capability=%s): %s: %s", | |
| step.step_id, | |
| step.capability, | |
| type(exc).__name__, | |
| exc, | |
| exc_info=exc, | |
| ) | |
| ``` | |
| (`core/controller.py:676-683`) | |
| The docstring above it states the rule: *"Both are preserved for the operator: `context` is never | |
| serialized (`StepOutcome.to_trace()` publishes `error_code` only), and the traceback goes to the | |
| log."* (`core/controller.py:673-675`). | |
| **Call 2 — the unwrapped failure.** The F-12b handler logs the request and then answers with the | |
| contract envelope: | |
| ```python | |
| _log.exception( | |
| "unhandled failure on %s %s", request.method, request.url.path | |
| ) | |
| status, body = translate_error( | |
| "satquery_error", | |
| "An internal error occurred.", | |
| detail="", | |
| recoverable=False, | |
| ) | |
| return JSONResponse(status_code=status, content=body) | |
| ``` | |
| (`app/space_app.py:510-519`) | |
| The handler's docstring is the clearest statement of the disclosure split in the codebase: | |
| > *"**F-15 is enforced here as well as in the handlers.** The client is told the code and a fixed, | |
| > operator-safe message; the exception's own text is recorded SERVER-SIDE only. That split is the | |
| > ruling: 'sanitize all client-facing exception messages; retain full exception details only in | |
| > server-side diagnostics'. A traceback in `detail` would disclose internal paths and library | |
| > versions to an unauthenticated caller."* (`app/space_app.py:503-508`) | |
| ### 4.3 The client/log split | |
| One rule governs every one of the six-plus call sites, and it is worth stating as a table because it | |
| is the single most important thing to understand about this system's diagnostics. | |
| | Carrier | Audience | Content | | |
| |---|---|---| | |
| | `trace.errors[].message` | the client | `user_message` — a fixed, operator-safe sentence | | |
| | `trace.errors[].code` | the client | the machine code (a classification) | | |
| | `trace.errors[].recoverable` | the client | a boolean | | |
| | `result.warnings[]` | the client | `scrub_paths(error.detail)` — path-scrubbed | | |
| | `evidence[].payload["message"]` | the client | `scrub_paths(error.detail)` — path-scrubbed | | |
| | **the log** | **the operator** | the raw exception, its type name, its text, and its traceback | | |
| The path-scrubbing is done by a single function: | |
| ```python | |
| def scrub_paths(text: str | None) -> str | None: | |
| ``` | |
| (`core/errors.py:39`) | |
| which the F-20 account identifies as the thing that made the *published* carriers safe while the | |
| *unpublished* `detail` stayed unsafe (`core/controller.py:652-654`). | |
| The consequence for an operator is direct: **the client's error message is never enough to debug | |
| with, and the log is the only place the real cause lives.** An operator who has only a client-side | |
| report has a code and a classification; an operator with log access has the exception. | |
| ### 4.4 What is not logged | |
| The following are **not** logged anywhere in the sources read, and their absence is deliberate or | |
| at least consequential: | |
| | Not logged | Consequence | | |
| |---|---| | |
| | request bodies | a malformed `POST /v1/analyze` is logged as a `ValidationError` *summary* (call 3), not as the body | | |
| | uploaded file bytes | never logged; the upload path has no log calls at all | | |
| | client IP addresses | never logged; no access log exists | | |
| | token values | **never** — `has_github_token` is a boolean and no call site prints a token | | |
| | per-request access lines | no access logging was found in any layer | | |
| | successful analysis outcomes | a successful run produces **no** log line at all | | |
| | health-check hits | `/api/health` logs nothing | | |
| | the tunnel agent's polls | the hub logs nothing on a poll; the agent's own logging lives in the private Inference repo | | |
| | the run's `run_id` | **no log call site includes a run id** | | |
| That last row is the most operationally significant. A client that reports a failure with its | |
| `run_id` gives the operator an identifier that **cannot be looked up in the logs**, because no log | |
| line carries one. The `run_id` correlates the *client's* record with the *trace* the client already | |
| has; it does not correlate anything server-side. This is a real observability gap and is recorded as | |
| such in §6. | |
| > **The one partial exception.** Call 4 (the gateway's transport failure) logs a `request_id` — the | |
| > `req_<hex>` id from `gateway/policy.py` (§5.2). That is the closest thing to a correlation id in | |
| > the logs, and it is a *gateway* id, not a *run* id. The two are different namespaces (§5.4). | |
| ### 4.5 Where the logs go | |
| Not established. No `logging.basicConfig`, no `dictConfig`, no handler, no formatter and no log | |
| level was found in any source read. The two hosts differ: | |
| - **the Codespace** runs `serve.py` under uvicorn, with `setsid nohup ... > "$SERVE_LOG" 2>&1` | |
| redirecting stdout/stderr to `/tmp/satquery-serve.log` (`deploy/codespace/launch.sh:63,133`), and | |
| the tunnel agent's output to `/tmp/satquery-tunnel.log` (`launch.sh:64,163`). Those files are on | |
| the **ephemeral Codespace filesystem** and vanish with it. | |
| - **Render** captures the service's stdout/stderr in its own log view. There is no in-code | |
| configuration and no log shipping. | |
| So: `UNKNOWN — not established from the available evidence` for the effective level; and for | |
| retention, the Codespace's logs are demonstrably **ephemeral** (`launch.sh:44-47` states the | |
| Codespace filesystem is ephemeral) while Render's retention is a platform property not observable | |
| from the code. | |
| --- | |
| ## 5. Identifiers | |
| The system generates identifiers with three distinct conventions, in three layers. They do not | |
| interoperate, and knowing which is which prevents a wasted search. | |
| ### 5.1 `run_<12 hex>` | |
| The run id is generated by the controller, and by the schema default when a run id is not supplied: | |
| ```python | |
| @staticmethod | |
| def _new_run_id() -> str: | |
| import uuid | |
| return f"run_{uuid.uuid4().hex[:12]}" | |
| ``` | |
| (`core/controller.py:1210-1214`) | |
| ```python | |
| def _new_id(prefix: str) -> str: | |
| return f"{prefix}_{uuid.uuid4().hex[:12]}" | |
| ``` | |
| (`core/schemas.py:28-29`) | |
| The controller prefers a **caller-supplied** id: | |
| ```python | |
| run_id = request.run_id or self._new_run_id() | |
| ``` | |
| (`core/controller.py:259`) | |
| So `AnalysisRequest.run_id` — an optional field (`core/schemas.py:418`) — lets a caller choose the | |
| id, and it is used verbatim if present. The format is **`run_` followed by twelve lowercase hex | |
| characters**. Real measured examples, all from the 2026-09-25 live validation | |
| (`DELIVERY_REPORT_2026-09-25.md` §3): | |
| ``` | |
| run_0843db184e32 run_5b766f2d7df7 run_ea590b6fd70f run_65a4b2f9d912 | |
| run_efe24b98d217 run_6375b80dcb8e run_2a07dcdbae96 run_9134f40a258c | |
| ``` | |
| Twenty-four such ids exist across three passes, and **no run id is shared between passes** | |
| (`release/CURRENT_RELEASE_STATE.md` §5) — which is itself the evidence that each pass was a genuine | |
| independent execution rather than a replay. | |
| The same id is propagated to each specialist: | |
| ```python | |
| specialist_request = SpecialistRequest( | |
| assets=list(assets), | |
| query=request.query, | |
| params=dict(step.params), | |
| run_id=trace.run_id, | |
| ) | |
| ``` | |
| (`core/controller.py:621-626`) | |
| so a run id is the one identifier that is consistent across the controller and every specialist | |
| invocation within a run. | |
| ### 5.2 `req_<24 hex>` | |
| The gateway generates its own correlation id, and it is a **deterministic function of a seed**: | |
| ```python | |
| def new_request_id(seed: str | None = None) -> str: | |
| """Generate a correlation id. | |
| Format `req_<hex>`. When `seed` is given the id is a deterministic function | |
| of it, which lets tests assert on ids without patching a clock or an RNG -- | |
| the same technique the rest of this repository uses for reproducibility. | |
| An inbound `X-Request-Id` is NOT trusted verbatim: see | |
| :meth:`GatewayPolicy.accept_request_id`. | |
| """ | |
| if seed is None: | |
| seed = f"{time.time_ns()}" | |
| digest = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:24] | |
| return f"req_{digest}" | |
| ``` | |
| (`gateway/policy.py:181-194`) | |
| Three properties: | |
| 1. **Format `req_` + 24 hex characters** — the first 24 hex of a sha256, so 96 bits. | |
| 2. **Deterministic when seeded**, which is what makes it testable without patching a clock or an RNG. | |
| 3. **An inbound `X-Request-Id` is not trusted verbatim** — `accept_request_id` validates it against | |
| `_REQUEST_ID_RE = re.compile(r"^[A-Za-z0-9_\-]{8,64}$")` (`gateway/policy.py:178`), so a caller | |
| cannot inject an arbitrary string into the logs. | |
| This id is what appears in log call 4 (`gateway/app.py:565-571`) and in the error envelope the | |
| gateway returns (`gateway/app.py:577`, `request_id=decision.request_id`). | |
| ### 5.3 The other prefixes | |
| `_new_id` is used with five prefixes across `core/schemas.py`: | |
| | Prefix | Field | Source | | |
| |---|---|---| | |
| | `asset_` | `AssetMetadata.asset_id` | `core/schemas.py:156` | | |
| | `region_` | `Region.region_id` | `core/schemas.py:194` | | |
| | `chg_` | `ChangeRegion.region_id` | `core/schemas.py:205` | | |
| | `ev_` | `Evidence.evidence_id` | `core/schemas.py:219` | | |
| | `run_` | `ExecutionTrace.run_id` | `core/schemas.py:299` | | |
| Note that `region_` and `chg_` are **different prefixes for the same conceptual thing** — a change | |
| region uses `chg_`, a general region uses `region_`. A consumer that assumes all regions are | |
| `region_*` will miss the change regions. The `Evidence` validator requires unique `evidence_id` | |
| values within a result (`core/schemas.py:345-351`), which is the one place an identifier collision | |
| is actively refused. | |
| The tunnel hub uses a **different** convention again — a bare `uuid4().hex` with no prefix: | |
| ```python | |
| req = PendingRequest( | |
| id=uuid.uuid4().hex, | |
| ... | |
| ) | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/tunnel.py:155-162`) | |
| So a tunnel request id is 32 hex characters with no prefix — neither `run_` nor `req_`. | |
| ### 5.4 The two id spaces do not join | |
| This is the observability gap that follows from the identifier conventions, and it deserves its own | |
| subsection. | |
| ```mermaid | |
| flowchart LR | |
| B["browser"] -->|"POST /api/infer"| R["Render orchestrator"] | |
| R -->|"hub.submit<br/>id = uuid4().hex"| T["TunnelHub"] | |
| T -->|"long-poll"| A["Codespace agent"] | |
| A -->|"POST /v1/analyze"| S["Codespace app"] | |
| S -->|"AnalysisController.run()"| C["controller"] | |
| C -->|"run_id = run_<12hex>"| E["ResultEnvelope"] | |
| R -.->|"logs request_id<br/>= req_<24hex>"| L["logs"] | |
| C -.->|"no log line carries run_id"| L | |
| ``` | |
| | Id | Format | Owner | Appears in a log? | Appears in the response? | | |
| |---|---|---|---|---| | |
| | `req_<24hex>` | sha256 prefix | the gateway | **yes** (call 4) | yes, in gateway error envelopes | | |
| | tunnel `uuid4().hex` | bare 32 hex | the tunnel hub | **no** | no | | |
| | `run_<12hex>` | uuid4 prefix | the controller | **no** | **yes** — in the envelope and the trace | | |
| The three do not reference each other anywhere in the code. A client that reports a failure with | |
| its `run_id` cannot be matched to a gateway `request_id` or a tunnel id from the logs. The only way | |
| to correlate a client report with server-side diagnostics is by **timestamp and query text**. | |
| > This is recorded as a **gap**, not as a defect with a workaround. No correlation id is propagated | |
| > across the tier boundary, and no document claims otherwise. | |
| --- | |
| ## 6. What is not observed | |
| This section is the honest inventory. Everything in it is a **capability the system does not have**, | |
| and none of it is aspirational — the list exists so that a reader does not assume a monitoring | |
| facility that was never built. | |
| | Capability | Present? | Evidence | | |
| |---|---|---| | |
| | **APM** (application performance monitoring) | **no** | no APM client, SDK or agent in any source read | | |
| | **Distributed tracing** | **no** | no trace-context propagation; the three id namespaces do not join (§5.4) | | |
| | **Cost accounting** | **no** | `GPU_DURATIONS` declares ZeroGPU durations (`app/space_app.py:109-116`) but nothing meters or reports consumption | | |
| | **Metrics endpoint** (Prometheus / OpenMetrics) | **no** | no `/metrics` route in any app factory | | |
| | **Per-model latency histogram** | **no** | `trace.timings` is per-run, per-step, and is not aggregated anywhere | | |
| | **Error-rate counter** | **no** | no counter exists; `trace.errors` is per-run only | | |
| | **Request counter** | **no** | the tunnel's `completed` counts only tunnel deliveries, per Render process (§2.3) | | |
| | **Uptime / restart tracking** | **no** | no uptime field; the hub's counters reset on restart | | |
| | **Alerting** | **no** | no alerting rule, webhook or threshold anywhere | | |
| | **Structured / JSON logs** | **no** | all log calls use `%s`-style free text | | |
| | **Log shipping / aggregation** | **no** | logs are per-host; the Codespace's are on an ephemeral filesystem (§4.5) | | |
| | **Dashboards** | **no** | none exists | | |
| | **SLO / SLA definition** | **no** | none exists | | |
| | **A system-level end-to-end benchmark** | **no** | `DOCS_STYLE_GUIDE.md` §3: *"does not exist; no system-level accuracy is claimed"* | | |
| ### 6.1 Cost accounting, specifically | |
| Cost is the one absence that has a code artifact, which makes it worth a subsection rather than a | |
| table row. | |
| `GPU_DURATIONS` declares a per-task ZeroGPU duration, transcribed from the frozen config: | |
| ```python | |
| #: ZeroGPU duration per task, from `configs/deploy.yaml` -- the FROZEN values, | |
| #: not new guesses. `change_vqa` has no key of its own and reuses `change`, | |
| #: because adding a key would move `Config.hash` off `78f1e3700da15aa1` | |
| #: (`docs/DEPLOYMENT_ARCHITECTURE.md` section 3.4). | |
| GPU_DURATIONS: dict[str, int] = { | |
| "vqa": 20, | |
| "caption": 20, | |
| "grounding": 45, | |
| "change": 30, | |
| "optical_sar": 45, | |
| "change_vqa": 30, | |
| } | |
| ``` | |
| (`app/space_app.py:105-116`) | |
| These values are used **only** to decorate a handler with a ZeroGPU reservation: | |
| ```python | |
| def decorate_gpu(task: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]: | |
| """Return the ZeroGPU decoration for a task, or an identity decorator. | |
| ... | |
| """ | |
| if task not in GPU_DURATIONS: | |
| raise KeyError(...) | |
| duration = GPU_DURATIONS[task] | |
| spaces = _spaces_module() | |
| if spaces is None or not hasattr(spaces, "GPU"): | |
| def _identity(fn: Callable[..., Any]) -> Callable[..., Any]: | |
| return fn | |
| return _identity | |
| return spaces.GPU(duration=duration) | |
| ``` | |
| (`app/space_app.py:143-166`) | |
| Nothing reads `GPU_DURATIONS` back out to compute, record or report consumption. There is no | |
| per-run GPU-second field, no cumulative counter, and no budget-exhaustion signal. On the live | |
| **CPU** deployment the decoration is a no-op anyway — `_spaces_module()` returns `None` when the | |
| `spaces` package is absent, so `decorate_gpu` returns the identity decorator | |
| (`app/space_app.py:160-164`). | |
| The declared durations are therefore **frozen paperwork on the live path**, exactly like | |
| `configs/deploy.yaml` itself (`release/repo/docs/DEPLOYMENT.md` §9). They are an input to a | |
| mechanism that does not run in the deployed configuration, and no cost is observed. | |
| > **Do not present `GPU_DURATIONS` as a cost model.** It is a declaration of intended reservation, | |
| > and on the CPU deployment it reserves nothing. | |
| --- | |
| ## 7. Operational runbook | |
| This section is the part of the chapter an operator uses. Every step below is grounded in a file. | |
| ### 7.1 Warm the stack before a demo | |
| The stack has three things that can be cold, and all three are warmed differently. | |
| | What is cold | How it warms | How long | | |
| |---|---|---| | |
| | the Render orchestrator | the first request to any `/api/*` route | Render free tier: a sleep/wake cycle | | |
| | the Codespace | `ensure_codespace_up()` starts it and polls | bounded by `wake_timeout_s = 120` | | |
| | the HF model cache | `warm_cache.py`, or the first model-touching request | a download per model | | |
| **Step 1 — confirm the tunnel agent is connected.** This is the check that matters most, because | |
| the tunnel is the live transport and the forwarded-port path is dead for a private repo | |
| (`release/CURRENT_RELEASE_STATE.md` §2: *"The forwarded-port path is **dead** (302 for a private | |
| repo); the tunnel is the live transport."*). | |
| ```bash | |
| curl --noproxy '*' https://<backend-host>/api/health | |
| ``` | |
| Read `tunnel.agent_connected`. `true` means an agent has polled within the last 60 seconds | |
| (`tunnel.py:63,113-117`). `false` means **no agent has polled recently** — and under `transport_mode: | |
| auto`, a request will then take the forward path, which for a private repo will fail after burning | |
| the wake timeout. | |
| **Step 2 — if `agent_connected` is false, start the Codespace.** The agent is launched by the | |
| devcontainer's `postStartCommand`, which runs `launch.sh`: | |
| ```json | |
| "postStartCommand": "bash deploy/codespace/launch.sh" | |
| ``` | |
| (`.devcontainer/devcontainer.json:18`) | |
| `launch.sh` is defensive by design, and its own header explains why: | |
| > *"``setsid`` alone is NOT enough in Codespaces. The lifecycle shell that runs postStartCommand can | |
| > still reap the process group, which showed up in production as 'the agent announced once, then | |
| > vanished' — the hub then reported agent_connected=false and /api/infer fell back to the dead | |
| > forwarded-port path (401 -> wake_timeout)."* (`deploy/codespace/launch.sh:15-19`) | |
| The script therefore uses `setsid + nohup + </dev/null` plus a **supervising wrapper** that restarts | |
| the agent if it exits (`launch.sh:20-23,160-168`), and then **verifies** the agent came up: | |
| ```bash | |
| sleep 4 | |
| if ! pgrep -f "deploy/codespace/tunnel_agent.py" > /dev/null 2>&1; then | |
| echo "WARNING: the tunnel agent is not running. Last log lines:" >&2 | |
| ... | |
| else | |
| echo "tunnel agent process is up (pid $(pgrep -f 'deploy/codespace/tunnel_agent.py' | head -1))" | |
| if grep -q "announced to hub" "$TUNNEL_LOG" 2>/dev/null; then | |
| echo "tunnel agent announced to the hub successfully" | |
| ... | |
| ``` | |
| (`deploy/codespace/launch.sh:171-191`) | |
| The verification exists because of a specific failure: | |
| > *"Backgrounding with all output discarded means a crashing agent is completely invisible — that is | |
| > exactly how a missing `httpx` hid itself."* (`launch.sh:174-176`) | |
| **Step 3 — warm the HF cache, if the Codespace was rebuilt.** `warm_cache.py` pre-downloads the four | |
| pinned models: | |
| ```bash | |
| python deploy/codespace/warm_cache.py | |
| ``` | |
| It reports per-model `OK` / `SKIPPED` / `FAILED`, never aborts on a single miss, and **always exits | |
| 0** so a cache miss cannot fail a build (`deploy/codespace/warm_cache.py:8-10,110-112`). The four | |
| models and their pinned revisions are transcribed verbatim from `configs/base.yaml` | |
| (`warm_cache.py:12-16,35-60`): | |
| | key | repo | revision | file | | |
| |---|---|---|---| | |
| | `vlm` | `HuggingFaceTB/SmolVLM-500M-Instruct` | `a7da5b986cb5` | (snapshot) | | |
| | `grounding` | `chendelong/RemoteCLIP` | `bf1d8a3ccf2d` | `RemoteCLIP-ViT-B-32.pt` | | |
| | `router` | `sentence-transformers/all-MiniLM-L6-v2` | `1110a243fdf4` | (snapshot) | | |
| | `croma` | `antofuller/CROMA` | `0dd28e3d633b` | `CROMA_base.pt` | | |
| > *"Running this before the first request means the initial `/v1/analyze` does not pay a cold | |
| > download. It is idempotent and safe to re-run."* (`warm_cache.py:3-6`) | |
| **Step 4 — run one throwaway analysis.** A single cheap query confirms the whole chain end to end. | |
| Do this **before** the demo, not during it. | |
| ### 7.2 The cold-start shape | |
| The cold start is **blocking and documented**, not hidden. The orchestrator's module docstring | |
| states it plainly: | |
| > *"If the Codespace is stopped, this service starts it via the GitHub Codespaces REST API, polls | |
| > its ``/v1/health`` until it answers 200, then proxies the request. The client simply **waits** | |
| > (blocking, wake-then-proxy) and receives the result; the frontend independently shows 'Waking | |
| > inference engine...' on slow responses. We optionally tag the response with | |
| > ``X-SatQuery-State: waking`` so the frontend can confirm the delay was a cold start."* | |
| > (`.workbuddy-ai/scratch/apply-test/main.py:13-20`) | |
| The wake loop is: | |
| ```python | |
| deadline = time.monotonic() + _wake_timeout_s() | |
| last_err: str | None = None | |
| while time.monotonic() < deadline: | |
| try: | |
| async with httpx.AsyncClient(timeout=_WAKE_HEALTH_TIMEOUT_S) as client: | |
| health = await client.get(f"{base}/v1/health") | |
| if health.status_code == 200: | |
| return base, woke | |
| if _anonymous_access_blocked(health.status_code): | |
| location = health.headers.get("location", "") | |
| raise ForwardUnavailable(...) | |
| last_err = f"health status {health.status_code}" | |
| except httpx.HTTPError as exc: | |
| last_err = str(exc) | |
| await asyncio.sleep(_WAKE_POLL_INTERVAL_S) | |
| raise WakeTimeout( | |
| f"Timed out after {_wake_timeout_s():.0f}s waiting for /v1/health. " | |
| f"Last probe error: {last_err or 'unknown'}." | |
| ) | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/main.py:417-442`) | |
| The shape an operator should expect: | |
| | Phase | What happens | Bound | | |
| |---|---|---| | |
| | 0 | the client sends `POST /api/infer` and **waits** | — | | |
| | 1 | `GET` the Codespace via the GitHub API; `POST .../start` if not `available` | one API round trip | | |
| | 2 | poll `GET {base}/v1/health` every 2 s, each with a 10 s timeout | ≤ `wake_timeout_s = 120` | | |
| | 3 | on the first `200`, proxy the original request | ≤ `upstream_timeout_s = 90` | | |
| | 4 | the response carries `X-SatQuery-State: waking` | — | | |
| `_anonymous_access_blocked` is the one branch that stops the loop early: | |
| ```python | |
| def _anonymous_access_blocked(status_code: int) -> bool: | |
| """Whether a ``/v1/health`` status means the forwarded port is *not* | |
| anonymously reachable, as opposed to merely not ready yet. | |
| ... | |
| """ | |
| return status_code in (401, 403) or 300 <= status_code < 400 | |
| ``` | |
| (`.workbuddy-ai/scratch/apply-test/main.py:360-371`) | |
| A redirect, `401` or `403` **can never become a 200 by waiting**, so the loop raises | |
| `ForwardUnavailable` immediately rather than burning the timeout. A connection error, `404` or `5xx` | |
| can still be a cold start, so those keep polling. | |
| > **Note this branch is in the patched source.** The deployed revision lacks it, which is why the | |
| > deployed system pays the full timeout instead of failing fast (§7.4). | |
| **The frontend's side of the cold start.** The frontend shows *"Waking inference engine…"* while | |
| Render starts the Codespace (`release/repo/docs/DEPLOYMENT.md` §5). The cold start is | |
| **tens of seconds** and is documented rather than papered over (`DEPLOYMENT.md` §5; | |
| `release/repo/docs/LIMITATIONS.md:52`). | |
| > **Do not quote a single cold-start number.** The documented statement is *"tens of seconds"* | |
| > (`DEPLOYMENT.md` §5), and the bounded worst case is `wake_timeout_s = 120` before a `504`. No | |
| > measured cold-start distribution exists: | |
| > `UNKNOWN — not established from the available evidence`. | |
| ### 7.3 Distinguishing a tunnel gap from a real failure | |
| This is the runbook's most useful procedure, and it is grounded in a measured timing. | |
| **The signature.** A request that hangs for **≈249 seconds** and then returns **`504`** is the | |
| tunnel-gap signature, not a broken model. The arithmetic is exact: | |
| ``` | |
| tunnel_timeout_s (150) + wake_timeout_s (120) = 270 s (nominal) | |
| measured ≈ 249 s | |
| ``` | |
| > *"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)."* | |
| > (`DELIVERY_REPORT_2026-09-25.md` §4; `release/repo/docs/DEPLOYMENT.md` §6) | |
| **The decision tree.** | |
| ```mermaid | |
| flowchart TD | |
| A["a request hung, or returned 5xx"] --> B{"re-read /api/health<br/>tunnel.agent_connected?"} | |
| B -->|"true"| C{"was the wait ≈249 s?"} | |
| B -->|"false"| D["TUNNEL GAP<br/>the agent is not polling.<br/>Start the Codespace (§7.1)."] | |
| C -->|"yes, 504"| E["TUNNEL GAP<br/>agent went stale mid-request,<br/>or the Codespace stopped.<br/>B-07. Mitigate operationally."] | |
| C -->|"no"| F{"what was the code?"} | |
| F -->|"invalid_request 422"| G["a CLIENT bug — the body<br/>did not match AnalysisRequest"] | |
| F -->|"model_load_error / model_unavailable"| H["an ARTIFACT defect —<br/>surface it, do not retry blindly"] | |
| F -->|"upstream_timeout 504"| I["tunnel healthy but slow —<br/>the agent did not complete in 150 s"] | |
| F -->|"other"| J["read the code and<br/>trace.errors[] — §3.8"] | |
| ``` | |
| **The three signals to read, in order:** | |
| 1. **`tunnel.agent_connected`** on a **fresh** `/api/health`. If `false`, it is a tunnel gap and the | |
| remedy is §7.1 step 2. Do not trust a single reading — the flag is a 60-second freshness window | |
| (§2.3), so re-read. | |
| 2. **The elapsed time.** ≈249 s is the B-07 signature. A fast failure is something else. | |
| 3. **The machine code.** The codes are disjoint and each implies a different action: | |
| | Code | Status | Meaning | Action | | |
| |---|---|---|---| | |
| | `tunnel_offline` | 503 | no agent is connected and `mode == "tunnel"` | start the Codespace | | |
| | `forward_unavailable` | 503 | the forwarded port is not anonymously reachable | start the tunnel agent (patch only) | | |
| | `wake_timeout` | 504 | the wake loop exhausted `wake_timeout_s` | the Codespace is not coming up | | |
| | `upstream_timeout` | 504 | the tunnel was healthy but no agent completed in `tunnel_timeout_s` | retry; check the agent (patch only) | | |
| | `upstream_unreachable` | 502 | a connection error to the Codespace | check the transport | | |
| | `invalid_request` | 422 | the body was not a valid `AnalysisRequest` | fix the client | | |
| | `model_unavailable` | 502 | the gateway could not reach the analysis service | retry | | |
| (`.workbuddy-ai/scratch/apply-test/main.py:308-332,570-622`; the full taxonomy is in | |
| `docs/architecture/08-api-contract.md` §12.) | |
| The two codes marked "patch only" — `forward_unavailable` and `upstream_timeout` — **do not exist in | |
| the deployed revision** (§7.4). An operator on the deployed system will not see them; they will see | |
| `wake_timeout` after ≈249 s instead. | |
| ### 7.4 The B-07 operational note | |
| **B-07 is `OPEN`.** The patch is prepared and **not deployed**. This is the single most important | |
| operational fact in this chapter. | |
| > *"B-07 | Transient tunnel-agent gaps → a request can hang or return 504. Patch prepared, **NOT | |
| > deployed**. | **OPEN**"* (`release/repo/docs/DEPLOYMENT.md` §6) | |
| **What the patch does** (`.workbuddy-ai/scratch/apply-test/main.py`): | |
| | Change | Code | Effect | | |
| |---|---|---| | |
| | A | `forward_unavailable` (`503`, `recoverable: true`) on a **terminal** 302/401/403 | converts a 504-after-249 s into a 503-early with an actionable code | | |
| | B | `upstream_timeout` (`504`) for "tunnel healthy but slow" | distinguishes a slow agent from a dead forward path | | |
| | C | `/api/health` `codespace_name` `.strip()` | fixes B-02 | | |
| **The patch's own honesty note, recorded because the honesty matters:** | |
| > *"The report records that an earlier claim that the patch 'would not have prevented' the observed | |
| > 504 'was wrong and was retracted'. The corrected position: 'Change A is genuinely **on the | |
| > failing path** — it converts a 504-after-249 s into a 503-early with an actionable code.'"* | |
| > (`DELIVERY_REPORT_2026-09-25.md` §4, quoted in `docs/architecture/02-deployment-topology.md` §6.4) | |
| **Why it is not deployed:** | |
| > *"the patch is not needed for the demo and touches the live backend. The residual is better | |
| > mitigated operationally (keep the Codespace warm, raise the idle timeout)."* | |
| > (`DELIVERY_REPORT_2026-09-25.md` §4) | |
| **The operational mitigation, therefore, is:** | |
| 1. Keep the Codespace **warm** before and during any demo (§7.1 step 1). | |
| 2. **Raise the Codespace idle timeout** so it does not stop mid-session. | |
| 3. Re-read `/api/health` before a run, and treat `agent_connected: false` as "warm the stack now". | |
| 4. Expect a ≈249 s hang followed by a `504` if the agent goes stale mid-request — and **retry**, | |
| because the residual is transient. | |
| > **A retracted claim, recorded.** An earlier statement that the patch *"would not have prevented"* | |
| > the observed 504 was **wrong and was retracted**. Change A is on the failing path. Do not repeat | |
| > the retracted version. | |
| > **Do not upgrade B-07.** It is `OPEN`. It is not `RESOLVED`, and the patch is not deployed — the | |
| > live payload's `codespace_name` trailing `\n` is the witness (§2.7). | |
| ### 7.5 Platform traps that bite operators | |
| These are recorded in `release/repo/docs/DEPLOYMENT.md` §7 and each cost real debugging time. | |
| | Trap | Consequence | Where recorded | | |
| |---|---|---| | |
| | **Cloudflare `_headers` rules CONCATENATE, they do not override** | a later rule cannot "fix" an earlier one; Chromium takes the **first** `max-age` | `DEPLOYMENT.md` §7 | | |
| | **Cloudflare 308-redirects `X.html` → `/X`** | reference the extensionless path | `DEPLOYMENT.md` §7 | | |
| | **A forwarded Codespace port returns `302` for a private repo** | this is *why* the tunnel exists | `DEPLOYMENT.md` §7 | | |
| | **The tunnel agent must be started by the devcontainer `postStartCommand`** | a restarted Codespace comes up with `agent_connected: false` | `DEPLOYMENT.md` §7 | | |
| | **Never retry `POST /api/infer` at the gateway** | a retry consumes inference twice | `DEPLOYMENT.md` §7 | | |
| | **The monorepo `deploy/` is stale and untracked** | it is **not** the deployed source | `DEPLOYMENT.md` §1; `release/CURRENT_RELEASE_STATE.md` §6 | | |
| | **The local `SatQuery-Backend` copy is stale** | always fetch the deployed `main.py` first | `DELIVERY_REPORT_2026-09-25.md` §4 | | |
| | **The Codespace filesystem is ephemeral** | uploaded assets and logs vanish with the Codespace | `launch.sh:44-47` | | |
| | **A stale serve process is worse than no process** | it answers `/v1/health` from OLD code | `launch.sh:111-113` | | |
| The last one has its own mitigation in `launch.sh` — a **stamp** recording the revision and the | |
| asset configuration, so a running server that does not match the checkout is restarted: | |
| ```bash | |
| _current_stamp() { | |
| printf 'rev=%s asset_enabled=%s asset_dir=%s\n' \ | |
| "$(git rev-parse HEAD 2>/dev/null || echo nogit)" \ | |
| "${SATQUERY_ASSET_ENABLED:-}" \ | |
| "${SATQUERY_ASSET_DIR:-}" | |
| } | |
| ``` | |
| (`deploy/codespace/launch.sh:103-108`) | |
| > *"A stale serve process is worse than no process: it answers /v1/health and /v1/capabilities from | |
| > OLD code, so the deployment looks alive while reporting the previous revision's capabilities."* | |
| > (`launch.sh:111-113`) | |
| This is the closest thing in the system to a **deployment-identity check**, and it is local to the | |
| Codespace — it is not exposed on any HTTP route. An operator on the orchestrator side cannot see it. | |
| ### 7.6 The diagnostic decision tree | |
| One consolidated tree, for the case where something is wrong and the operator has only the | |
| client-side report. | |
| ```mermaid | |
| flowchart TD | |
| A["something is wrong"] --> B["GET /api/health"] | |
| B --> C{"tunnel.agent_connected?"} | |
| C -->|"false"| D["warm the stack<br/>§7.1 steps 1-2"] | |
| C -->|"true"| E{"what did the client see?"} | |
| E -->|"nothing, request hung"| F{"waited ≈249 s?"} | |
| F -->|"yes"| G["B-07 tunnel gap<br/>retry + warm<br/>§7.4"] | |
| F -->|"no"| H["still waiting —<br/>within budget"] | |
| E -->|"a 5xx"| I["read the error code<br/>§7.3 table"] | |
| E -->|"a 4xx"| J["a client bug —<br/>the body did not match the contract"] | |
| I --> K{"code present in the<br/>DEPLOYED revision?"} | |
| K -->|"no"| L["the patch is not deployed<br/>§7.4"] | |
| K -->|"yes"| M["act on the code"] | |
| E -->|"a result, but wrong"| N["read trace:<br/>intent, task, selected_models,<br/>errors, fallbacks<br/>§3"] | |
| ``` | |
| The last branch is the one that uses the trace rather than health: a **wrong but successful** answer | |
| is diagnosed from the `ExecutionTrace`, by reading `trace.intent` (what the router thought), | |
| `trace.task` (what was dispatched), `trace.selected_models` (what ran), and `trace.errors` / | |
| `trace.fallbacks` (what degraded). That is the whole purpose of the trace, and it is the only | |
| diagnostic surface for a wrong answer — no log line records a successful run (§4.4). | |
| --- | |
| ## 8. Status, gaps, and evidence | |
| ### 8.1 `NOT RUN` / `OPEN` / `BLOCKED` / `DEFERRED` for this topic | |
| | # | Item | Status | | |
| |---|---|---| | |
| | 1 | B-07 — tunnel gaps; patch prepared, **not deployed** | **`OPEN`** | | |
| | 2 | B-02 — `/api/health` `codespace_name` trailing `\n` | **`OPEN` (cosmetic)** | | |
| | 3 | Effective log level and log destination on each host | **`UNKNOWN`** — no logging configuration found (§4.5) | | |
| | 4 | Whether the live payload's `config` block omits three origin fields, or the deployed revision predates them | **`UNKNOWN`** (§2.9) | | |
| | 5 | Whether the live `tunnel` block carries `agent_connects` | **`UNKNOWN`** (§2.3, §2.9) | | |
| | 6 | A measured cold-start distribution | **`NOT RUN`** — only "tens of seconds" is documented (§7.2) | | |
| | 7 | A system-level end-to-end benchmark | **`NOT RUN`** — none exists | | |
| | 8 | Log retention on Render | **`UNKNOWN`** — a platform property, not observable from the code | | |
| | 9 | Any APM / metrics / distributed tracing | **not implemented** — see §6 | | |
| | 10 | Cost accounting | **not implemented** — `GPU_DURATIONS` is declared, not metered (§6.1) | | |
| | 11 | `agent_connects` counter | **`IMPLEMENTED` but dead** — never incremented (§2.3) | | |
| | 12 | Correlation of a client `run_id` to a server log line | **not implemented** — no log call site carries a run id (§5.4) | | |
| ### 8.2 The `UNKNOWN` list, in one place | |
| Four things in this chapter are genuinely not established from the available evidence, and each is | |
| written here so it is not silently filled in later: | |
| 1. **The effective log level and destination on each host.** | |
| `UNKNOWN — not established from the available evidence` (§4.5). | |
| 2. **Which `config` field set the live process serves.** The measured payload and the source copy | |
| disagree by three fields, and the evidence does not decide between a stale transcription and an | |
| older revision. `UNKNOWN — not established from the available evidence` (§2.9). | |
| 3. **Whether the live `tunnel` block carries `agent_connects`.** The measured payload has four keys; | |
| the source `stats` has five. `UNKNOWN — not established from the available evidence` (§2.3). | |
| 4. **The cold-start duration as a measured distribution.** Only the phrase *"tens of seconds"* is | |
| documented, and the bounded worst case is `wake_timeout_s = 120`. | |
| `UNKNOWN — not established from the available evidence` for any distribution (§7.2). | |
| ### 8.3 Evidence table | |
| | Evidence | Location | Establishes | | |
| |---|---|---| | |
| | the measured live health payload | `release/CURRENT_RELEASE_STATE.md` §1; `release/repo/docs/DEPLOYMENT.md` §2 | §2.1, §2.2 | | |
| | the three `completed` readings | `DEPLOYMENT.md` §2; `docs/FINAL_DELIVERY_TODO.md` §1.4/§6 E-02; `docs/FINAL_DELIVERY_REPORT.md` §3 P2 | §2.3 | | |
| | B-02's status and fix | `docs/FINAL_DELIVERY_REPORT.md` §6; `docs/FINAL_DELIVERY_TODO.md` §4 P2-T03, §5 | §2.7 | | |
| | the B-07 root shape and ≈249 s | `DELIVERY_REPORT_2026-09-25.md` §4 | §2.8, §7.3, §7.4 | | |
| | the patch's three presence checks | `docs/FINAL_DELIVERY_TODO.md` §6 E-12; `DELIVERY_REPORT_2026-09-25.md` §4 | §2.9 | | |
| | `TunnelHub` and `stats` | `.workbuddy-ai/scratch/apply-test/tunnel.py:113-136,213-230` | §2.3 | | |
| | the deployed orchestrator (patched) | `.workbuddy-ai/scratch/apply-test/main.py` | §2.4, §2.8, §7.2, §7.3 | | |
| | the monorepo orchestrator | `deploy/render/main.py:444-466` | §2.9 | | |
| | `/v1/health` and `HealthStatus` | `app/space_app.py:521-547`; `core/schemas.py:430-437` | §2.10 | | |
| | `status` derivation, `gpu_available` | `app/deployment.py:703-766` | §2.10 | | |
| | `_effective_device` and F-8 | `app/deployment.py:1093-1204` | §2.5 | | |
| | `ExecutionTrace`, `TraceStep`, `ModelRef` | `core/schemas.py:279-319` | §3.2, §3.3, §3.5 | | |
| | the trace's producer | `core/controller.py:258-394,535-749,1120-1214` | §3.4, §3.6, §3.7, §3.8, §3.9 | | |
| | F-13/F-14/F-15/F-19/F-20 | `core/controller.py:263-308,350-365,578-702` | §3.11, §4.2, §4.3 | | |
| | `scrub_paths` | `core/errors.py:39` | §4.3 | | |
| | every log call site | §4.2 table, with `file:line` | §4.2 | | |
| | `run_<12hex>` | `core/controller.py:1210-1214`; `core/schemas.py:28-29` | §5.1 | | |
| | `req_<24hex>` | `gateway/policy.py:181-194` | §5.2 | | |
| | `GPU_DURATIONS` and `decorate_gpu` | `app/space_app.py:105-116,143-166` | §6.1 | | |
| | the Codespace start procedure | `deploy/codespace/launch.sh`; `.devcontainer/devcontainer.json:18` | §7.1, §7.5 | | |
| | the warm-up contract | `deploy/codespace/warm_cache.py` | §7.1 | | |
| | the platform traps | `release/repo/docs/DEPLOYMENT.md` §7 | §7.5 | | |
| | the 24 live runs and their ids | `DELIVERY_REPORT_2026-09-25.md` §3 | §5.1, §3.9 | | |
| ### 8.4 Cross-references | |
| | For | See | | |
| |---|---| | |
| | the four endpoints, the envelopes, the error taxonomy | [08 — The API Contract](08-api-contract.md) | | |
| | the four-tier topology, the tunnel, the wake flow, `transport_mode` | [02 — Deployment Topology](02-deployment-topology.md) | | |
| | the request lifecycle and the nine-state spine in motion | [03 — Request Lifecycle](03-request-lifecycle.md) | | |
| | confidence, evidence, and the `ConfidenceBreakdown` the trace carries | [06 — Evidence and Confidence](06-evidence-and-confidence.md) | | |
| | the frozen config and `Config.hash == 78f1e3700da15aa1` | [07 — Configuration and Freeze](07-configuration-freeze.md) | | |
| | the frontend's trace bar and the 94.4444 % fill | [09 — Frontend](09-frontend.md) §6 | | |
| | the live deployment, env vars, and the cold-start statement | [../DEPLOYMENT.md](../DEPLOYMENT.md) | | |
| | the project's blocker register and evidence index | `docs/FINAL_DELIVERY_TODO.md` §5, §6 | | |
| --- | |
| > **Chapter summary.** SatQuery AI observes exactly three things: whether the transport is up | |
| > (`/api/health`, §2), what one run did (`ExecutionTrace`, §3), and what failed inside the server | |
| > (the logs, §4). It observes nothing else — no metrics, no tracing, no cost, no trend (§6). The | |
| > health payload is a live-measured snapshot whose most important field is `tunnel.agent_connected`, | |
| > whose most consequential absence is any correlation id, and whose most load-bearing quirk is a | |
| > trailing newline that doubles as the witness that the B-07 patch is not deployed. B-07 is `OPEN`; | |
| > B-02 is `OPEN` and cosmetic; four things are genuinely | |
| > `UNKNOWN — not established from the available evidence`. | |