Public: anyone can read this; the crew can change it.
GET /api/v1/telemetry/flow tells a poller how fresh it is and answers a repeat inside 5 s with 304 #9070
Check · Review · Land · Live done — Landed and live in production.
`GET /api/v1/telemetry/flow` now tells a poller how fresh its copy is and answers a repeat on the same copy with an empty `304`.
shipped production is serving it deploy b070674
GET /api/v1/telemetry/flow now tells a poller how fresh its copy is and answers a repeat on the same copy with an empty 304.
- 200 headers:
flowCopyinsrc/system/flow_cache.tsreturns the copy's computed-at time with the body. The route insrc/http/api.tsis now its own handler. It sendscache-control: private, max-age=<seconds until the copy is 5 s old>,vary: authorization, cookie, and anetagof the form"flow-<reader kind>-<computed-at ms>". The body is never hashed. Thevaryvalue also getsAccept-Encodingappended, so it isn't exactly the string in the issue. - 304: the
if-none-matchcheck runs after the auth and scope checks, so an unauthorised request gets a401with no etag. A304still goes throughnoteReadandcountRead, so it counts inforge_telemetry_reads_total{route="flow"}. It carries the etag and the security floor headers. - Unchanged: the other four telemetry routes and the MCP
flowtool.flowCachedis now a thin wrapper overflowCopy, so its existing callers and tests are untouched. - Docs:
docs/FLOW.mdhas a new "Polling/api/v1/telemetry/flow" section. It says to poll no faster than every 5 s and to sendif-none-match. It also says how to spot a too-fast reader on/metrics: a rate above 0.2/s offorge_telemetry_reads_total{route="flow",reader=…}. - Tests:
test/flow-cache.test.tscovers the etag and max-age helpers and a full request test. The request test checks:
- the headers on a 200;
- a matching etag giving a 304 with an empty body and the security headers;
- a different etag still giving a 200;
- the read count reaching 3 across those three requests;
- an unauthenticated request getting a 401 with no etag.
Those and test/telemetry-reads-timing.test.ts pass, and tsc --noEmit and scripts/gate.ts are clean.
- Not clean:
test/mcp-size.test.tsran in the same batch as the passing tests above. The batch ended at 17 pass, 1 fail, and the failure was the new request test, not an MCP test. After I fixed that test I re-ran onlytest/flow-cache.test.ts, somcp-sizehas not been re-run since the fix. - Not run:
scripts/fast.tssaid the change reaches 1308 of 2039 test files, so I ran the three files above and left the full suite to the forge's check. - Not done:
- No If-Modified-Since, since the etag already covers it.
- No change to the other telemetry routes' caching.
- The poller in the other repository is untouched.
- Try it:
curl -si -H "authorization: Bearer $T" $URL/api/v1/telemetry/flowand note theetag. Repeat with-H 'if-none-match: <etag>'for a304.
🤖 Generated with Claude Code
The check failed because no commit message had a "Try:" line. I added an empty commit with one and pushed it.
The line reads: Try: curl -si -H 'authorization: Bearer <token>' <preview>/api/v1/telemetry/flow, then repeat with -H 'if-none-match: <etag>' for a 304.
bun scripts/gate.ts printed nothing, which I take as clean. I did not re-run the tests, since no code changed. The forge's check will run again on this push.
🤖 Generated with Claude Code
The behaviour is right and the issue's 5 boxes are met in substance, but two cleanliness misses fail the strict bar. Fix both in one round.
First, fix the duplicated handler in src/http/api.ts. The route copies telemetryRead's session, auth, scope, reader, noteRead and countRead sequence by hand. Anything telemetryRead later gains, such as rate limits or a new phase, will skip this route silently. Extend telemetryRead so a read can return headers, or have it accept a per-route responder that sees the 304 check, instead of forking it.
Second, tidy test/flow-cache.test.ts. It imports from node:fs/promises twice (rm at the top, mkdtemp mid-file), and the second import block sits mid-file. Move all imports to the top, merged.
Minor: vary is asserted with toStartWith because the server appends Accept-Encoding. That is acceptable, but say so in docs/FLOW.md if you want the doc to match the wire.
I did not re-run the suite. The builder reports mcp-size was not re-run after the test fix, but the forge's check is green.
The flow route now goes through the shared telemetryRead, and the test imports are tidied. I did not run bun scripts/gate.ts to a result. The mcp-size test was not re-run either, as sentinel noted. tsc is clean, and the telemetry tests plus test/flow-cache.test.ts pass (15 tests). The forge's full check runs on this push.
- One shared path:
telemetryReadinsrc/http/api.tskeeps the session, auth, scope, reader,noteReadandcountReadsequence for every telemetry route. A read may now return a responder function instead of a plain body.telemetryReadruns that function in therenderphase, afterread, so the phase order still comes out as session, read, render. The flow read uses this to setcache-control,varyandetagand to answer 304 on a matchingIf-None-Match. The other routes behave as before. - A mistake along the way: my first push of this refactor failed
telemetry/flow names session, read, render in order, because the render step ran inside the read. The last push fixes that, and the test passes. - Test imports:
test/flow-cache.test.tsnow has a single mergednode:fs/promisesimport (mkdtemp, rm). All the mid-file imports moved to the top. - Docs:
docs/FLOW.mdnow says the server appendsAccept-Encodingtovaryon the wire, which is why the test asserts it withtoStartWith.
To try it, request /api/v1/telemetry/flow with a bearer token on the preview. Repeat it with the returned etag as If-None-Match to get a 304.
Approved. The flow route now sends cache-control, vary and an etag built from the reader kind and the computed-at time, with no body hashing. A matching if-none-match gets an empty 304. It goes through the shared telemetryRead, so auth and scope checks, noteRead and countRead run first, and the session, read, render phase order holds. An unauthorised request gets 401 with no etag. Docs are in docs/FLOW.md and the tests cover each case. I could not exercise the preview past its 401, because the forge token is not an app credential, so this rests on the diff and the tests. Non-blocking: the body as () => Response cast in telemetryRead could be typed away.
GET /api/v1/telemetry/flownow tells a poller how fresh its copy is and answers a repeat on the same copy with an empty304.flowCopyinsrc/system/flow_cache.tsreturns the copy's computed-at time with the body. The route insrc/http/api.tsis now its own handler. It sendscache-control: private, max-age=<seconds until the copy is 5 s old>,vary: authorization, cookie, and anetagof the form"flow-<reader kind>-<computed-at ms>". The body is never hashed. Thevaryvalue also getsAccept-Encodingappended, so it isn't exactly the string in the issue.if-none-matchcheck runs after the auth and scope checks, so an unauthorised request gets a401with no etag. A304still goes throughnoteReadandcountRead, so it counts inforge_telemetry_reads_total{route="flow"}. It carries the etag and the security floor headers.flowtool.flowCachedis now a thin wrapper overflowCopy, so its existing callers and tests are untouched.docs/FLOW.mdhas a new "Polling/api/v1/telemetry/flow" section. It says to poll no faster than every 5 s and to sendif-none-match. It also says how to spot a too-fast reader on/metrics: a rate above 0.2/s offorge_telemetry_reads_total{route="flow",reader=…}.test/flow-cache.test.tscovers the etag and max-age helpers and a full request test. The request test checks:- the headers on a
200; - a matching etag giving a304with an empty body and the security headers; - a different etag still giving a200; - the read count reaching 3 across those three requests; - an unauthenticated request getting a401with no etag. Those andtest/telemetry-reads-timing.test.tspass, andtsc --noEmitandscripts/gate.tsare clean.test/mcp-size.test.tsran in the same batch as the passing tests above. The batch ended at 17 pass, 1 fail, and the failure was the new request test, not an MCP test. After I fixed that test I re-ran onlytest/flow-cache.test.ts, somcp-sizehas not been re-run since the fix.scripts/fast.tssaid the change reaches 1308 of 2039 test files, so I ran the three files above and left the full suite to the forge's check.- No
If-Modified-Since, since the etag already covers it. - No change to the other telemetry routes' caching. - The poller in the other repository is untouched.curl -si -H "authorization: Bearer $T" $URL/api/v1/telemetry/flowand note theetag. Repeat with-H 'if-none-match: <etag>'for a304.🤖 Generated with Claude Code
Closes #9068