SysDeck/klanker-gate/TODO.md

17 KiB
Executable File

TODO

Deliberate, known follow-ups. Each item names what is missing, why it was left, and what has to move for it to be done.

This file used to be a pointer to two owning records - a numbered decision log and an open-risks register. Both were retired on 2026-07-30 (item 1), and this file is now the register itself: it is where a new deliberate divergence gets recorded, and where a code comment points when its rationale is too long to sit at the point of use. That raises the bar on what goes in it: an entry needs the mechanism, the evidence in code, what "done" means, and the trigger that reopens it, because there is no second document to carry the rationale.

Two kinds of entry live here, and they are kept apart on purpose:

  • Open follow-ups (the numbered items) - work that is not done. Their numbers are reading order and will change as items are added and closed, so never cite an open item by number from code.
  • Decisions the code cites - closed decisions whose rationale a source comment depends on. Each carries a stable key such as D-REBUILD-HEADERS. Code cites the key, never a position, which is the one property the retired numbered scheme had and the reason it was citable at all.

Bare numbers such as "decision-log 57" that survive in comments or in CLAUDE.md are provenance only: they record that a decision was taken and where it was once written down. They resolve to nothing on disk, and four of them resolve to nothing anywhere (item 1).


1. The decision log and open-risks register are retired, and 38 files still cite them

Status: decided 2026-07-30 (owner: retirement accepted). Links and code citations closed; four items' rationale is unrecoverable. docs/contracts/decision-log.md (50 448 B at HEAD) and docs/guides/open-risks.md (23 279 B) were deleted from the working tree, along with nine other docs/contracts/* files, docs/guides/admin-api-cookbook.md, both docs/runbooks/* files and docs/assets/architecture-diagram.md. The owner accepted the retirement rather than restoring from HEAD.

Owner: unassigned · Severity: Major · Records: this item

What makes it work rather than a clean deletion. The numbered scheme was load-bearing. 38 files in the tree cite it, and not only docs:

Citer Was Now
CLAUDE.md 10 0 links, inline numbers kept as provenance
apps/gateway/context.ts 4 0
packages/core/src/translate.ts 3 0
packages/core/src/middleware.ts 2 0
AGENTS.md, permissions.md, tests, other docs 19 prose provenance only

15 of those citations were markdown links, and were dead links, in CLAUDE.md, AGENTS.md, CONTRIBUTING.md, docs/benchmark-report.md and this file; all 15 are gone. The nine in the three source files above were prose references of the form "(decision-log 71)" and are gone too - see below for what that cost.

Nine source citations, and only two carried content the comment did not. Reviewed one by one on 2026-07-30. Eight of the nine comments already stated the constraint they cited, so the number was decoration and dropping it lost nothing. The exception was middleware.ts, which said "see decision-log 87 for the three rebuild sites, the 15 affected routes and the accepted serveDir consequence" - a forward reference to information held nowhere else. That content is now D-REBUILD-HEADERS, re-verified against the code rather than copied from memory, and the shared-rate-limit rationale is D-SHARED-RATE-LIMIT.

Four items are gone for good: 14, 15, 69 and 79. They were cited by translate.ts (14, 15, 79) and context.ts (69). Each cited comment states its own constraint, so no behavior is undocumented:

Lost item The constraint that survives, in the comment
14 a zero token count means "the provider never told us", never "known and discarded"
15 edge translators run AFTER the plugin stream tap, so hooks see the canonical shape
69 config reload applies REMOVALS, because an upsert-only reload cannot revoke
79 one canonical execution path is what lets every ingress dialect share it

What is lost is the reasoning behind each, and any alternative that was rejected. Reconstructing them would mean inventing rationale, so they are recorded as lost rather than guessed at. If one of these four decisions is ever reopened, treat it as undecided and re-derive it.

The retired revision is also not recoverable. HEAD holds an older lineage: its log is the original Wave-1..Wave-5 prose with no numbered entries at all (items 57, 68, 81 and 87 return zero hits) and its risk register stops at R19. The numbered 1..87 log and R20..R25 existed only in the uncommitted tree. There is no stash, no checkout/reset in the reflog, and no editor local-history copy.

Consequence absorbed. This file is the new home, and Decisions the code cites is where a divergence whose rationale a comment depends on gets recorded, keyed rather than numbered. Work that planned to append entries 88-91 or risks R26-R27 records them there instead, with a fresh key each, at the point the work lands rather than in advance.

Still open:

  • docs/contracts/fixtures/ holds the two golden fixtures that tests/contract/golden_chat_test.ts:28 reads at module load, and is now the only inhabitant of a retired directory. Moving it under tests/contract/ would finish the retirement; it was left alone because the deletion of those two files is what made the suite red in the first place and re-touching them was not worth bundling into that repair.
  • The bare numbers left inline in CLAUDE.md and in permissions.md, tests and other docs. They are provenance, not links, and are labelled as such in the preamble here.

Reopen trigger: a reader following a citation that resolves to nothing, or a new divergence recorded as a bare number instead of a key.

2. Telemetry does not cost the media surfaces

Status: done for the chat surfaces (2026-07-29); the media surfaces are mid-build. INFERENCE_PATHS is now an isInferencePath() predicate covering the four canonical paths plus /v1/messages, /cohere/v2/chat, GenAI generate actions, the Azure deployment-scoped ops and OpenRouter chat and embeddings, with metrics, usage, spans and log enrichment widened together as required below.

Owner: unassigned · Severity: Minor · Records: this item; provenance decision-log 56 (the gap) and 81 (the chat-surface closure), open-risks R19

What is left is /v1/images/generations, /v1/audio/speech and /v1/audio/transcriptions. They still need normalizeUsage and the pricing catalog to model per-image, per-character and per-second billing before they can be observed without producing counted-but-uncosted records.

Route Billing unit
/v1/images/generations per image
/v1/audio/speech per character
/v1/audio/transcriptions per second of audio

/v1/batches, /v1/files, /v1/count_tokens and /v1/models are correctly outside the set: none is a per-request billable completion.

Why it was left. The log trail was wired to the telemetry layer that already existed; that did not change what the layer observes. Widening the predicate moves billing-adjacent accounting - usage records feed governance budgets and the analytics rollups - which is a materially larger blast radius than a log column.

Done means: metrics, usage records, spans and log enrichment all widen together, not logs alone.

Reopen trigger: any request to observe a non-/v1-canonical inference surface, or a report that spend on one of the routes above is missing from /api/analytics or the Grafana dashboards.

3. LOG_LEVEL is documented and plumbed but read by nothing

Status: OPEN. LOG_LEVEL has a row in the quick-reference table of docs/reference/environment-variables.md and a mention under "Core gateway and PostgreSQL", and docker-compose.yml forwards it into the container. No code reads it, on any file type. An operator setting LOG_LEVEL=debug gets silence.

Owner: unassigned · Severity: Minor · Records: this item

It was removed from .env.example in the 2026-07-30 env cleanup, because an example file that lists a dead knob is the defect. The docs row and the Compose passthrough still promise it.

Done means: either implement a bounded log-level parse and restore the .env.example line, or drop the docs row and the Compose passthrough too.

Reopen trigger: a report that log verbosity cannot be changed.

4. totalTokens is an unclamped vendor sum

Status: OPEN, narrowed. In apps/gateway/routes/telemetry.ts:135 totalTokens is prompt + completion + (cacheCreation ?? 0) with no ceiling. It reaches the usage record (:159, :190) and the gen_ai.usage.total_tokens span attribute (:216). It never passes through costMicroUsd, so the clamp that protects the cost counter does not cover it.

Owner: unassigned · Severity: Minor · Records: this item

A provider returning three fields at MAX_SAFE_INTEGER yields a usage row and a span attribute of 3 x MAX_SAFE_INTEGER. Money is unaffected.

Done means: the same bounded parse the cost path uses, applied after the vendor sum rather than per field - clamping each field independently leaves 2 x MAX_SAFE_INTEGER, which is the measured failure of the per-field approach.

Reopen trigger: an implausible token total in /api/analytics or a Tempo span.

5. Deliberate gaps that are still live

Each is a decision, not an oversight; the row is here so it stays visible. The evidence column is the code that implements the refusal.

Gap Shape Evidence
OpenRouter native GET /generation and GET /key Explicit 501 openrouter_ingress.ts:171
Aggregator-path Bedrock native ingress Explicit 501 compat_families.ts:795
Serving a cache hit as a stream Not implemented by design cache reads are non-streaming; completed streams are stored via the passive tee
Code Mode executor Two gates, and the run primitive stubs packages/mcp/src/codemode/, FROSTY_CODE_MODE
Kubernetes and Helm packaging Not present deployment assets are Docker and Compose only
Some provider-panel config fields Persisted and surfaced, not enforced field comments in packages/contracts/src/config.ts

The authoritative version of this table is the "Gaps and partial implementations" section of docs/concepts/functionality-and-capabilities.md. Do not let the two drift; that file wins.

6. Multi-process serving has a residual per-process gap

Status: shipped with a named residual. FROSTY_WORKERS is live, and budgets and rate-limit windows are fleet-wide through PostgreSQL. The residual gap is per-process state that no shared authority covers.

Owner: unassigned · Severity: Info · Records: this item; provenance decision-log 62, 70, 71 (the shipped behavior) and 73 (the residual)

Reopen trigger: an operator report of limit overshoot that shared-authority rate limiting does not explain.


Decisions the code cites

Closed decisions whose rationale a source comment depends on. Each has a stable key; a comment cites the key and nothing else, so entries can be added, split or reordered without invalidating a citation. A key is never reused or renamed.

Only decisions a comment genuinely cannot carry inline belong here. If the constraint fits in a note under four lines at the point of use, that is where it goes and no entry is needed - which is why this section is short and is expected to stay short.

D-REBUILD-HEADERS

Rebuilding a Response around a new body invalidates the headers that describe the old one, so the serving boundary drops all three.

REBUILT_BODY_HEADERS in packages/core/src/middleware.ts is content-encoding, content-length, transfer-encoding, matched lowercased.

Why each one:

  • Content-Encoding - Deno's fetch decompresses transparently but keeps the header. A rebuild loses the internal already-decoded flag, so the header stops describing the bytes and becomes an instruction the client acts on and fails. This is the one that corrupted responses rather than merely mis-describing them.
  • Content-Length - a provider's value describes the encoded bytes.
  • Transfer-Encoding - hop-by-hop; the serving runtime owns framing.

Three sites rebuild a Response around a replacement body. Two strip, one deliberately does not:

Site Behavior
middleware.ts:90 (makeRequestLogger) strips - this is the serving boundary
routes/helpers.ts:147 (rebuild) strips
providers/src/client.ts:171 (withBody) does not strip, and is safe because of the placement below

The fix is at the serving boundary because that placement is terminal in both directions. withBody is INWARD of the request logger, so a stale header it attaches is cleaned on the way out. reshapeError (compat_families.ts:188) rebuilds around a different body too, but lives inside compatPrefixMiddleware, which main.ts composes OUTWARD of the logger - it is safe for the opposite reason, because it copies headers the logger has already cleaned. A third header-copy at governance.ts:584 reuses the same body, so its framing headers are still true.

A denylist, not an allowlist. An allowlist would silently drop openai-organization, x-request-id and the x-ratelimit-* family, which reach the client today and must continue to. The trade is that a future provider header this rebuild also invalidates is inherited rather than caught.

Accepted consequence. The rule applies to every response passing the logger, including static assets from serveDir (main.ts:110). Those lose a Content-Length the runtime then re-derives, so the cost is a recomputation, not a behavior change.

Provenance: decided 2026-07-29 as decision-log 87 and measured then as repairing 15 live-broken routes. That route count is recorded as it was measured on that date and has not been re-derived since; the mechanism and the three sites above were re-verified 2026-07-30.

D-SHARED-RATE-LIMIT

Fixed-window rate limiting moves to a shared PostgreSQL authority only when more than one process shares the port, because a single process already is the whole fleet.

Measured: a shared reservation costs ~1.8 ms at 50 concurrent against local PostgreSQL, versus ~1 us for the in-process Map. In single-process mode the Map is already fleet-accurate, so paying that buys nothing.

FROSTY_SHARED_RATE_LIMIT is auto|on|off, resolved by sharedRateLimitEnabled - auto keys off FROSTY_WORKERS, on forces it for operators running separate replicas that auto cannot detect, off accepts N-times-the-limit across N processes. When a shared limiter is present, VirtualKeyManager stands its own in-process windows down so exactly one authority counts.

Provenance: decided as decision-log 71; the numbers live in docs/benchmark-report.md, which is their maintained home.