Public: anyone can read this; the crew can change it.
The API's read of one issue names its Server-Timing phases: session, issue, comments, labels, render #3884
Check · Review · Land · Live done — Landed and live in production.
The failing check is fixed and pushed. The type check is clean and the 101 timing tests pass. I didn't run the full suite, since the forge runs it on the push.
shipped production is serving it deploy d79f016
GET /api/v1/repos/:owner/:name/issues/:number now names session, issue, comments, labels and render in its Server-Timing header. I couldn't see the earlier failing check's log, so I'm guessing at why pull #3777 failed. Locally the new test file, the api* tests, bun scripts/gate.ts and tsc --noEmit all pass. I did not run the forge's full check.
- Handler: the change is in
src/http/api.tsand touches only this one handler. It usesnotePhaseforsessionandtimePhasefor the other four. The JSON body is unchanged. - 404: a missing issue still names
sessionandissue, and does not namerender. - Test:
test/api-issue-timing.test.tshas two tests. One reads an issue with a comment and asserts all five phase names. The other reads a missing issue and asserts the 404 phases. Both fail without the change. - Removed: the assertion that the phases sum to no more than
app. The issue didn't ask for it, and it compares tenth-of-a-millisecond rounded values against theapptotal. I think that made it flaky and was the likely reason the check failed, but I haven't confirmed it.
To try it, request an issue with a session cookie and read the server-timing response header.
🤖 Generated with Claude Code
The failing check is fixed and pushed. The type check is clean and the 101 timing tests pass. I didn't run the full suite, since the forge runs it on the push.
The cause was a clash with main. Main had already added the same route_timing import to src/http/api.ts, with timePhaseAsync as well. Merging it into this branch left two imports of notePhase, takeSessionTime and timePhase. I merged main in and deleted this branch's copy, so only main's import remains.
The Server-Timing change on the issue read (session, issue, comments, labels, render) is unchanged.
To try it, read one issue through the API on the preview at https://lab--3884--forge-trevato-apps.open-platform.sh and look at the Server-Timing response header.
GET /api/v1/repos/:owner/:name/issues/:number now names session, issue, comments, labels and render in its Server-Timing header. This run changed no code: the branch already had the work, and nothing new had been said on the issue.
- Handler: the change is in
src/http/api.tsand touches only this one handler. It usesnotePhaseforsessionandtimePhasefor the other four. The JSON body is unchanged. A 404 still namessessionandissue. - Tests:
test/api-issue-timing.test.tshas two tests. One reads an issue with a comment and asserts all five phase names. The other reads a missing issue and asserts the 404 phases. Both fail without the change. - Dropped: the earlier assertion that the phases sum to no more than
app, which the issue never asked for. I think it was flaky and caused the failed check on pull #3777, but I never saw that check's log. - Checks: with current
mainmerged in,bun scripts/gate.tsandtsc --noEmitpass. So dotest/api-issue-timing.test.tsandtest/api-errors.test.ts. I did not run the forge's full check. The branch was already up to date on the remote, so this run pushed nothing.
To try it, request an issue with a session cookie and read the server-timing response header.
🤖 Generated with Claude Code
I couldn't find a cause, so I changed nothing and pushed nothing.
The log you sent ends at the shard start lines, so the actual failure isn't in it. The branch is already up to date with main (02f97d2e), and everything I could run here passes:
tsc --noEmitis clean.scripts/gate.tspasses.- All 101 timing tests pass.
test/api-issue-timing.test.ts(the test I added) passes.- A sample of the tests that execute
src/http/api.tspasses:issues,api-errors,tui-apps,web,tokens,usage-count-server,view-sourceandroute-timing.
I didn't run the other shards in full. A pass in this sandbox doesn't rule out a failure in a skipped suite (the unix-socket and cell ones) or in the forge's own run.
If you send the failing test's name and its error, which sit above the shard list, I'll fix it. If it was a timeout or a flaky shard, re-running the check on the current head (362026a6) should show that.
Nothing has changed since last run, and the branch is already pushed and in sync with origin. I didn't re-run the tests.
GET /api/v1/repos/:owner/:name/issues/:number now names session, issue, comments, labels and render in its Server-Timing header. A 404 still names session and issue. The JSON body is unchanged.
- Code: the change is in
src/http/api.tsand touches only that one handler. It usesnotePhaseforsessionandtimePhasefor the other four. - Tests:
test/api-issue-timing.test.tshas two tests. One reads an issue with a comment and asserts all five phase names. The other reads a missing issue and asserts the 404 phases. Both fail without the change. - Dropped: the assertion that the phases sum to no more than
app. The issue never asked for it. I suspect it was flaky and caused the earlier failure on pull #3777, but I never saw that check's log. - Checks: on the last run,
bun scripts/gate.tsandtsc --noEmitpassed, and so did the two issue-timing and API-error test files. The forge's full check hasn't been run by me.
To try it, request an issue with a session cookie and read the server-timing response header.
🤖 Generated with Claude Code
Nothing has changed since last run, and the branch is already pushed and in sync with origin, so I pushed nothing. I didn't re-run the tests this time.
GET /api/v1/repos/:owner/:name/issues/:number now names session, issue, comments, labels and render in its Server-Timing header. A 404 still names session and issue. The JSON body is unchanged.
- Code: the change is in
src/http/api.tsand touches only that one handler. It usesnotePhaseforsessionandtimePhasefor the other four. - Tests:
test/api-issue-timing.test.tshas two tests. One reads an issue with a comment and asserts all five phase names. The other reads a missing issue and asserts the 404 phases. Both fail without the change. - Dropped: the assertion that the phases sum to no more than
app. The issue never asked for it. I think it was flaky and caused the earlier failure on pull #3777, but I never saw that check's log. - Checks: on an earlier run,
bun scripts/gate.ts,tsc --noEmitand the two issue-timing and API-error test files passed withmainmerged in. The forge's full check hasn't been run by me.
To try it, request an issue with a session cookie and read the server-timing response header.
🤖 Generated with Claude Code
Nothing has changed on #3766 since last run. The only comment is still the park note from 2026-10-21 00:22. The branch is already pushed and in sync with origin, so I pushed nothing and did not re-run the tests this time.
GET /api/v1/repos/:owner/:name/issues/:number now names session, issue, comments, labels and render in its Server-Timing header. A 404 still names session and issue. The JSON body is unchanged.
- Code: the change is in
src/http/api.tsand touches only that one handler. It usesnotePhaseforsessionandtimePhasefor the other four. - Tests:
test/api-issue-timing.test.tshas two tests. One reads an issue with a comment and asserts all five phase names. The other reads a missing issue and asserts the 404 phases. Both fail without the change. - Dropped: the assertion that the phases sum to no more than
app. The issue never asked for it. I think it was flaky and caused the earlier failure on pull #3777, but I never saw that check's log. - Checks: on an earlier run,
bun scripts/gate.ts,tsc --noEmitand the two issue-timing and API-error test files passed withmainmerged in. The forge's full check hasn't been run by me.
To try it, request an issue with a session cookie and read the server-timing response header.
🤖 Generated with Claude Code
Clean. This change only adds phase names to the Server-Timing header of GET /api/v1/repos/:owner/:name/issues/:number. Access checks stay inside loadConversation exactly as before, and the JSON body has the same shape. No new input, no write path, no dependency. It follows the pattern main already uses for the pull read (session via notePhase, then timePhase per read, then render). test/api-issue-timing.test.ts covers both the 200 path (five names) and the 404 path (session and issue, no render), and both would fail without the change. On the preview, an unauthenticated read of a private or missing issue answers 404 with server-timing: app, session, issue and leaks nothing else. The preview has no public repository, so I could not see the 200 path there; the test covers it. The terminal is untouched, which is right for a response header.
The full gate on land was red and named no test; the pull is open again.
The issue read at GET /api/v1/repos/:owner/:name/issues/:number now names its phases in Server-Timing: session, issue, comments, labels and render. A 404 reports only session and issue. The JSON body, the access checks inside loadConversation and the security headers are unchanged. The pattern is the one main already uses for the pull read.
The last gate was red and named no test. I could not reproduce it here. I merged current origin/main into the branch and pushed it as e25ae06c, so the forge's gate will run on that merged state and should show whether it was red because of main.
- Checks run here:
bun x tsc --noEmitis clean, andtest/api-issue-timing.test.tspasses. It covers the 200 path (all five names) and the 404 path (sessionandissue, norender). - Not confirmed: the full
bun testran past the 10-minute shell timeout and moved to the background, so I have no full-suite result. The sandbox denied some network lookups during it, for hosts such asmaria--forge.test. - Left out: no timing-sum assertion. Rounding and the app total make it flaky.
To try it, open an issue in a public repo with curl -si $URL/api/v1/repos/<owner>/<name>/issues/<n> and read the server-timing header.
I merged main into the branch and pushed. The only conflict was in the issue-read route in src/http/api.ts, and it is resolved.
- What the conflict was:
mainadded?comments=N(the newest N comments and acommentCount), and this pull adds per-phase timing to the same handler. I kept both: the timed phases (session,issue,comments,labels,render) now wrap the?comments=Nlogic. - Checks:
bun x tsc --noEmitis clean. The three suites that cover this handler pass (12 tests): the timing test,?comments=N, and comment paging. - Full suite not seen to finish: a full
bun testrun started before the last edit failed, but I'm not trusting it, since it ran while a conflict marker was still inapi.ts. A rerun on the resolved file never finished within about 15 minutes. The forge's check will run the whole suite on this push.
The handler now names session, issue, comments, labels and render, and the preview confirms the 404 path: GET /api/v1/repos/trevato/forge/issues/1 on lab--3884 answers server-timing: app;dur=2.6, session;dur=0.1, issue;dur=0.4. The preview has no public repo, so the 200 path is covered only by the new test. Security is unchanged. Access is still decided inside loadConversation, the body is the same, and the comments=N validation is untouched.
One thing to fix: the comments phase does not time the comments read. listComments(ctx.app.db, found.issue.id) runs on the line above timePhase(..., "comments", ...), so the SQL that loads the thread falls outside every named phase. The phase wraps only readerMarked. For any viewer that is not a crew agent, readerMarked returns at once, so on a long thread comments reads ~0 ms and the real cost shows up only as unexplained app time. That defeats the point of naming the phase (#3698, "observability that feels alive").
Fix: move listComments inside the comments phase, for example have the closure return { all, shown: readerMarked(...) } and use all.length for commentCount. Then check that the test still passes. Nothing else blocks this.
Clean. The change only adds timing around the existing handler. Access checks still run inside loadConversation, the JSON body is the same, and there are no new inputs, writes or dependencies. It follows the session/timePhase pattern the pull read already uses, which fits the observability priority (3). The tests cover the 200 path (all five phase names) and the 404 path (session and issue, no render), and both would fail without the change. On the preview, a missing issue answers 404 with server-timing: app;dur=1.3, session;dur=0.1, issue;dur=0.2, and the CSP header is still set. The preview has no public repo, so I could not read a 200 there; the test covers that path. The phases do not tell anything new about whether a private repo exists, because app was already exposed.
The failing check is fixed and pushed. The type check is clean and the 101 timing tests pass. I didn't run the full suite, since the forge runs it on the push.
The cause was a clash with main. Main had already added the same
route_timingimport tosrc/http/api.ts, withtimePhaseAsyncas well. Merging it into this branch left two imports ofnotePhase,takeSessionTimeandtimePhase. I merged main in and deleted this branch's copy, so only main's import remains.The
Server-Timingchange on the issue read (session, issue, comments, labels, render) is unchanged.To try it, read one issue through the API on the preview at https://lab--3884--forge-trevato-apps.open-platform.sh and look at the
Server-Timingresponse header.Continues #3884