Skip to content

feat(stream): read the connection ack instead of assuming it - #63

Merged
htcom-code merged 3 commits into
mainfrom
feat/sse-ready-ack
Sep 21, 2026
Merged

htcom-code merged 3 commits into
mainfrom
feat/sse-ready-ack

Conversation

@htcom-code

Copy link
Copy Markdown
Owner

The platform opens every stream connection with a ready frame now
(protean #80, 237bcd0), stating what it is, what it is
recording, how often it pushes and how much it retains. The console had
been asserting four of those on its own. It reads them instead — and
where the platform says nothing, it says it does not know rather than
falling back to the guess.

Two further fixes came out of staring at an idle platform while
verifying this, and are in their own commits.

What the console stopped asserting

before after
LIVE badge LIVE · 200 buffer on every platform — a ring size this console had never been told and could not see the stated capacity, or no claim at all
silence watchdog 6s, because the platform was assumed to push at 1Hz 6 × the stated tickMs, clamped to 2–60s
module table, empty "enable protean.trace.metrics.enabled=true and restart" — told operators to switch on a setting that was already on, and the key is live, so "restart" was wrong too reads metricsEnabled: off / on-and-idle / not stated
trace table, empty "requests appear here as the platform serves them" — a promise nothing keeps while trace.enabled=false reads tracesEnabled and says recording is off
header origin only origin · protean 0.1.0-SNAPSHOT (display only)

tracesEnabled is read before metricsEnabled in the module panel,
and that ordering is required rather than defensive. Per-module metrics
are aggregated from recorded requests, and the contract states
metricsEnabled as the effective value (trace.enabled AND
trace.metrics.enabled) — so a platform with recording off reports
metrics off as well, and that value read on its own produces "turn it on
and rows appear": an instruction that changes nothing beside a promise
nothing can keep.

The ack is display and diagnostics only — nothing branches on which
implementation answered. Its values are connection-scoped and dropped
when the stream is rebuilt, because they are live settings on the
platform and the last connection's answers may no longer be true.

A platform that sends no ack is unchanged. Go still sends the older
hello, Rust sends nothing, and both captures stay in the fixtures as
that axis: the console stays live, claims nothing, and keeps the 6s
window it has always used.

Verified against a running platform, not only against tests

examples/quickstart on :8080, protean 237bcd0. The protean fixture in
stream-fixtures.ts is the real connect bytes from that session
(correlation ids synthetic — public repo), not a constructed example.

  • ready first, once per connection; metrics/modules/summary
    then repeat at 1Hz. buffered (5) equalled the rows of the trace
    frame behind it (5).
  • The three empty-state screens were each produced by restarting the
    platform into that state, because they cannot be told apart from the
    data: the metrics frame is [] in all of them.
    • metricsEnabled: trueNo module traffic yet
    • metricsEnabled: falseModule metrics are off
    • tracesEnabled: falseTrace recording is off, in both panels
  • Reconnect re-learning was exercised for real: a platform restart
    dropped the stream, the hook rebuilt it, and the panels changed on the
    new ack without the page being touched.

The contract dependency above was found during that verification and
fixed on the platform side as well: the ack was sending the raw metrics
switch while the MCP tool answered with the effective value, so the same
name meant two things. Reported, and protean has pinned the contract to
the effective value.

Also in this PR

fix(ui): keep an idle screen readable and truthful — the latency
chart removed itself when there were no samples, taking half the row it
shares with the status mix and leaving a layout that read as broken
rather than quiet. It draws its frame now, on a scale labelled as a
default, with nothing over the grid: a line, an area or a point would
each be data the platform never sent. And the status mix printed
1 req beside four zeroes — the header count and the bar widths were one
number, and the || 1 that keeps a width from dividing by zero was also
what the header showed.

fix(dev): resolve the proxy target once, for both uses — the dev
proxy read process.env, which does not carry .env files (Vite loads
those itself), so setting VITE_PROTEAN_TARGET in .env as the README
instructs moved the name in the header while every request still went to
the default: the header named a host nothing was sent to. Unset, the two
then fell back differently (proxy to localhost:8080, header to the dev
server's own origin). The config resolves it once and hands it to both.
Dev only — a production build carries no proxy and must keep naming its
own origin, which is where its requests actually go.

Tests

199 passing, 29 of them new: the ack's watchdog sizing and clamps,
partial and unreadable acks, per-connection scoping, the three empty
states in both panels, the empty chart, and the status count. The
ack-contradicting pair (tracesEnabled: false with metricsEnabled: true) is kept as a guard — the console talks to three implementations
and cannot verify an ack, so it declines to promise rows on the strength
of one.

The platform now opens every stream connection with a `ready` frame
(protean 237bcd0, #80) stating what it is, what it is recording, how
often it pushes and how much it retains. Four things the console had
been asserting on its own are now read from it, and where the platform
says nothing the console says it does not know rather than falling back
to the guess.

- the LIVE badge printed "200 buffer" on every platform — a ring size
  this console had never been told and could not see. It prints the
  stated `capacity`, or nothing at all.
- the silence watchdog was 6s because the platform was assumed to push
  at 1Hz. It is six times the stated `tickMs`, clamped to 2-60s: a few
  milliseconds would trip on scheduler jitter and an hour would never
  trip, and this is the only signal that a platform is gone.
- the module table read an empty `metrics` array as "switch this on",
  which is equally true of a platform that is on and idle — and it told
  operators to enable a setting that was already enabled. It reads
  `metricsEnabled`. Its "and restart" was wrong too: the key is live.
- the empty trace table promised "requests appear here as the platform
  serves them", which nothing keeps while `trace.enabled` is false. It
  reads `tracesEnabled`.

`tracesEnabled` is also read before `metricsEnabled` in the module
panel. Per-module metrics are aggregated from recorded requests, and the
contract states `metricsEnabled` as the effective value — so a platform
with recording off reports metrics off as well, and that value read on
its own would produce "turn it on and rows appear": an instruction that
changes nothing beside a promise nothing can keep.

The ack is display and diagnostics only — no behaviour branches on which
implementation answered. Its values are connection-scoped and dropped on
reconnect, because they are live settings on the platform and the last
connection's answers may no longer be true.

A platform that sends no ack is unchanged: Go still sends the older
`hello`, Rust sends nothing, and both captures stay in the fixtures as
that axis. The protean fixture is the real connect bytes, captured
2026-09-21 against examples/quickstart.

Tags: #stream #honesty
Co-Authored-By: htjulia <htjulia1@gmail.com>
An idle platform is what a console shows the moment someone opens it,
and two panels handled that state badly.

The latency chart removed itself when there were no samples, which also
took half the row it shares with the status mix and left a layout that
read as broken rather than quiet. It draws its frame instead — on a
default scale, labelled as one, with nothing over the grid: a line, an
area or a point would each be data the platform never sent.

The status mix printed "1 req" beside four zeroes. The header count and
the bar widths were one number, and the `|| 1` that keeps a width from
dividing by zero was also what the header showed. The guard belongs to
the geometry, not to the count.

Tags: #honesty
Co-Authored-By: htjulia <htjulia1@gmail.com>
The dev server and the header answered "which platform is this console
talking to" separately, and disagreed twice.

The proxy read `process.env`, which does not carry `.env` files — Vite
loads those itself — so setting VITE_PROTEAN_TARGET in `.env` as the
README instructs moved the name in the header while every request still
went to the default. The header named a host nothing was sent to. With
the variable unset they then fell back differently: the proxy to
localhost:8080, the header to the dev server's own origin.

The config resolves it once, hands it to the proxy, and injects that
exact string for the header to read. Injection is dev-only on purpose: a
production build carries no proxy and must keep naming its own origin,
which is where its requests actually go.

Tags: #dev
Co-Authored-By: htjulia <htjulia1@gmail.com>
@htcom-code
htcom-code merged commit 834cbf7 into main Sep 21, 2026
4 checks passed
@htcom-code
htcom-code deleted the feat/sse-ready-ack branch September 21, 2026 07:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant