feat(console): report the running commit, and say which door failed (re-target of #152) - #154
Conversation
Two of the four HANDOFF-CONNECTIVITY-TRIAGE-1.0 permanent fixes.
/api/version, beside /api/healthz. healthz says the process answers;
version says what code is answering. On 2026-08-01 this console served
a weeks-stale build because two deployments had failed and Railway kept
the last good image live, and nothing on the running service could say
so. force-dynamic is load-bearing: without it Next evaluates the handler
during the build and freezes the env values that were set then.
Degraded states now carry evidence. 'The data API is unreachable.' was
shown for an API answering 200 on /healthz. One sentence covered CORS,
404, 401, DNS and a dead dependency, so it named none of them and a
healthy service read as dark. degradationFor now accepts a
DegradationOrigin and renders a detail line naming the door, the host
and the status. A request that never landed reads differently from any
answered status, and a 401 says credential rather than outage.
The objects proxy already reported the upstream it could not reach
(app/api/objects/_upstream.ts:76); TransportObserver simply dropped it.
It now carries door and host through to the banner.
A bare number still means what it meant. Several callers pass a
synthetic 400/500 to steer the generic template for failures that never
made a request, so only an explicit origin object renders a detail.
Rendering "answered 400" for those would replace one lie with another,
and there is a test pinning that.
vitest run src/lib/degradation.test.ts 11 passed
pnpm --filter @commonplace/console run build:railway exit 0,
/api/version present in the route manifest as dynamic
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Six review comments, all correct. Two were real defects.
The evidence never reached a user. degradationFor produced a `detail`
line and ChatPage rendered only `cause`, in both the sidebar and the
main panel. The data path shipped without the surface, so the feature
was invisible. Both render paths now show the detail beneath the cause.
The evidence described the wrong request. `loadError` comes from the
chat catalog and thread fetches; `transportOrigin` comes from the
object-seam observer. A healthy /api/objects/views probe followed by a
502 from /api/chat/projects rendered a banner claiming the data API
answered 200. Only the `disconnected` branch carries origin now, since
`connection` is derived from onTransport and is genuinely its outcome.
Evidence about the wrong request is worse than the generic sentence
this change set out to replace.
The rest:
- /api/version served RAILWAY_PROJECT_ID, RAILWAY_SERVICE_ID and
RAILWAY_REPLICA_ID from an unauthenticated route. The endpoint exists
to answer "which commit, and where"; a service name and an environment
name answer that, and the IDs answer nothing. Names only now.
- HttpBlockHost.onStatus now carries the door it dialed. Only that layer
knows whether /objects/query or /objects/action ran, so without it a
later failure overwrote the probe's origin with a bare status and the
banner fell back to "The data API". Optional second parameter, so
existing observers keep compiling.
- 403 is no longer reported as a credential problem. connectionFor maps
it to 'identity-refused', and the objects proxy returns it for
active_workspace_claim_required and active_workspace_membership_refused,
where the credential is fine and simply does not reach this workspace.
Telling that reader to fix their credential sends them the wrong way.
Split from 401 and pinned with a test.
- Reattached the orphaned TransportObserver doc comment.
Note for anyone verifying locally: apps/console depends on
packages/block-view through `file:`, and pnpm COPIES file: dependencies
into the store instead of symlinking them. Editing the package has no
effect on the console until `pnpm install` re-copies it. The first build
here failed on the stale copy's one-argument onStatus while the source
already had two. CI installs fresh so it would never have seen it, which
makes this a local-only trap.
vitest run src/lib/degradation.test.ts 12 passed
vitest run (@commonplace/block-view) 16 passed, 4 files
pnpm --filter @commonplace/console run build:railway
exit 0, /api/version present as dynamic
pnpm-lock.yaml unchanged
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
|
Warning Review limit reached
Next review available in: 40 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe PR adds a dynamic ChangesOrigin-aware degradation diagnostics
Console version endpoint
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant HttpBlockHost
participant ConsoleBlockHost
participant ChatPage
HttpBlockHost->>ConsoleBlockHost: status and request door
ConsoleBlockHost->>ChatPage: transport status and origin
ChatPage->>ChatPage: build degradation origin evidence
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR re-lands the previously-merged (but mis-targeted) console changes to improve runtime diagnostics: it adds a /api/version endpoint to report the currently running commit, and enriches “degraded/unreachable” UI states with concrete transport evidence (which door was called, which host answered, and what status came back).
Changes:
- Add
/api/version(dynamic) to report the running git SHA/branch and selected Railway environment names. - Thread “door” (endpoint) information through
HttpBlockHost→ console transport observer so degraded states can describe the failing request. - Extend degradation rendering to optionally include an evidence/detail line, with targeted tests to pin behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/block-view/src/host/HttpBlockHost.ts | Adds an optional door parameter to onStatus and reports which endpoint produced each HTTP outcome. |
| apps/console/src/lib/degradation.ts | Introduces DegradationOrigin + evidence rendering (describeOrigin) and updates degradationFor to accept an origin object. |
| apps/console/src/lib/degradation.test.ts | Adds tests that pin the new evidence/detail behavior and the 401/403/404 distinctions. |
| apps/console/src/lib/console-host.ts | Carries door/host info into the transport observer, including parsing upstream host from proxy error bodies in probe(). |
| apps/console/src/components/chat/ChatPage.tsx | Stores last transport outcome to provide origin evidence to the disconnected-state degradation banner and renders optional detail text. |
| apps/console/src/app/api/version/route.ts | New API route reporting runtime git metadata and selected Railway environment names. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| service_name: runtimeEnv('RAILWAY_SERVICE_NAME'), | ||
| environment_name: runtimeEnv('RAILWAY_ENVIRONMENT_NAME'), | ||
| }, | ||
| }); |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0022491571
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ) | ||
| : connection === 'disconnected' | ||
| ? degradationFor('console_data_api_unreachable') | ||
| ? degradationFor('console_data_api_unreachable', transportOrigin) |
There was a problem hiding this comment.
Capture origin for catalog load failures
When the object API is unavailable during initial chat load, fetchChatCatalog() hits /api/chat/projects, whose server catalog calls the same object seam and leaves loadError set after its 502 response. That branch then permanently wins over this disconnected branch and calls degradationFor without an origin, so the stable sidebar and main panel still discard the door, host, and status added by this change. Capture the catalog request's own origin rather than relying only on probe evidence.
Useful? React with 👍 / 👎.
| <p>{degradation.cause}</p> | ||
| {degradation.detail ? ( | ||
| <p className="text-ij-ink-disabled">{degradation.detail}</p> |
There was a problem hiding this comment.
Show transport evidence after an existing thread disconnects
When connectivity fails after a chat has loaded, thread and catalog remain populated, so the sidebar selects ChatSidebar and the main panel selects RuntimeTree; this newly added detail block is gated out by !thread. Those loaded-state components only show their existing generic unreachable messages, making the captured door and status invisible during the common mid-session outage scenario. Render or pass the degradation detail into the loaded-thread path as well.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/console/src/components/chat/ChatPage.tsx`:
- Around line 408-435: Update the degradation selection in ChatPage to pass
transportOrigin to degradationFor when connection is either disconnected or
identity-refused. Preserve the existing loadError precedence and the current
unreachable degradation behavior for disconnected connections, while allowing
the 403 workspace-refusal detail for identity-refused transport outcomes.
In `@apps/console/src/lib/console-host.ts`:
- Around line 301-305: The query error handlers in queryLiveWire and
queryLiveDomain should not emit console_data_api_unreachable when
HttpBlockHost’s onStatus has already reported the non-OK status and door. Track
whether onStatus reported status/origin for the current request, suppress the
duplicate generic event in that case, and continue emitting
console_data_api_unreachable for failures without prior status/origin, including
post-200 response parse failures.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 35920c2d-0c6c-41be-802b-f088233c76af
📒 Files selected for processing (6)
apps/console/src/app/api/version/route.tsapps/console/src/components/chat/ChatPage.tsxapps/console/src/lib/console-host.tsapps/console/src/lib/degradation.test.tsapps/console/src/lib/degradation.tspackages/block-view/src/host/HttpBlockHost.ts
| // A null status means the request never landed, which describeOrigin reports | ||
| // differently from any answered status. Passing the observed origin is what | ||
| // turns "The data API is unreachable." into a sentence that also says which | ||
| // door, which host, and what came back. | ||
| const transportOrigin = lastTransport | ||
| ? { | ||
| door: lastTransport.origin?.door, | ||
| host: lastTransport.origin?.host, | ||
| status: lastTransport.status ?? undefined, | ||
| } | ||
| : undefined; | ||
|
|
||
| // Only the disconnected branch may carry that evidence. `connection` is | ||
| // derived from onTransport, so the last transport outcome is genuinely its | ||
| // outcome. `loadError` is not: it comes from the chat catalog and thread | ||
| // fetches, which are different requests. A healthy /api/objects/views probe | ||
| // followed by a 502 from /api/chat/projects would otherwise render a banner | ||
| // claiming the data API answered 200. Evidence about the wrong request is | ||
| // worse than no evidence, which is the failure this whole change exists to | ||
| // stop. | ||
| const degradation = loadError | ||
| ? degradationFor( | ||
| loadError === 'workspace_object_scope_unenforced' | ||
| ? 'workspace_object_scope_unenforced' | ||
| : 'console_data_api_unreachable', | ||
| ) | ||
| : connection === 'disconnected' | ||
| ? degradationFor('console_data_api_unreachable') | ||
| ? degradationFor('console_data_api_unreachable', transportOrigin) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm no other component already renders a dedicated message for identity-refused sessions.
rg -n -C3 "identity-refused" -g '*.ts' -g '*.tsx' apps/console/srcRepository: Travis-Gilbert/CommonPlace
Length of output: 8348
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## ChatPage relevant sections"
sed -n '40,70p' apps/console/src/components/chat/ChatPage.tsx
sed -n '235,265p' apps/console/src/components/chat/ChatPage.tsx
sed -n '340,445p' apps/console/src/components/chat/ChatPage.tsx
sed -n '455,515p' apps/console/src/components/chat/ChatPage.tsx
echo "## degradation.ts relevant section"
sed -n '180,235p' apps/console/src/lib/degradation.ts
echo "## ConsoleApp connectionFor"
sed -n '110,140p' apps/console/src/components/ConsoleApp.tsx
echo "## degraded test expectations"
sed -n '70,115p' apps/console/src/lib/degradation.test.ts
echo "## deterministic call-site flow probe"
python3 - <<'PY'
from pathlib import Path
p = Path('apps/console/src/components/chat/ChatPage.tsx')
s = p.read_text()
checks = {
"ChatPage connectionFor has 401 unauthenticated": "status === 401" in s and "return 'unauthenticated'" in s,
"ChatPage connectionFor has 403 identity-refused": "status === 403" in s and "return 'identity-refused'" in s,
"degradation only includes disconnected:", "connection === 'disconnected'" in s,
"needsSignIn is unauthenticated": "connection === 'unauthenticated'" in s,
"unreachable includes identity-refused": "connection !== 'identity-refused'" in s,
}
for k, v in checks.items():
print(f"{k}: {v}")
PYRepository: Travis-Gilbert/CommonPlace
Length of output: 15198
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## ChatPage remaining relevant render section"
sed -n '448,530p' apps/console/src/components/chat/ChatPage.tsx
echo "## console_data_api_unreachable callers"
rg -n -C4 "console_data_api_unreachable|degradationFor\\(" apps/console/src -g '*.ts' -g '*.tsx'
echo "## behavior probe for ChatPage connection/degradation flow"
python3 - <<'PY'
from pathlib import Path
s = Path('apps/console/src/components/chat/ChatPage.tsx').read_text()
print("contains 401 unauthenticated:", "status === 401" in s and "return 'unauthenticated'" in s)
print("contains 403 identity-refused:", "status === 403" in s and "return 'identity-refused'" in s)
print("degradation condition only disconnected:", "connection === 'disconnected'" in s and "transportOrigin" in s)
print("needsSignIn only unauthenticated:", "needsSignIn = connection === 'unauthenticated'" in s)
print("status bar has identity-refused action:", "identity-refused" in Path('apps/console/src/components/shell/StatusBar.tsx').read_text())
print("thread view render for identity-refused:", "connection === 'identity-refused' return 'Authentication refused'" in Path('apps/console/src/views/ThreadView.tsx').read_text())
PYRepository: Travis-Gilbert/CommonPlace
Length of output: 24494
Render 403 workspace-refusal evidence when connection is identity-refused.
degradationFor already returns a 403 workspace-refusal detail, but ChatPage only passes transportOrigin when connection === 'disconnected'. Since status === 403 maps to identity-refused, a 403 response from the transported request does not render the evidence banner. Include connection === 'identity-refused' in the same transport degradation branch.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/console/src/components/chat/ChatPage.tsx` around lines 408 - 435, Update
the degradation selection in ChatPage to pass transportOrigin to degradationFor
when connection is either disconnected or identity-refused. Preserve the
existing loadError precedence and the current unreachable degradation behavior
for disconnected connections, while allowing the 403 workspace-refusal detail
for identity-refused transport outcomes.
| // Carry the door through. Without it a later /objects/query or | ||
| // /objects/action failure would overwrite the probe's origin with a | ||
| // bare status, and the banner would fall back to naming "the data API" | ||
| // when it could have named the request that actually failed. | ||
| onStatus: (status, door) => this.observer?.(status, null, door ? { door } : undefined), |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check whether existing console-host tests exercise the query-failure -> observer path.
fd -a 'console-host.test.ts' | xargs -I{} rg -n -C6 'onTransport|onStatus|console_data_api_unreachable|queryLiveWire|queryLiveDomain|401|403' {}Repository: Travis-Gilbert/CommonPlace
Length of output: 168
🏁 Script executed:
#!/bin/bash
set -eu
echo "Locate target file and tests"
fd -a 'console-host.(ts|test\.ts)$' . || true
echo
echo "Search relevant symbols"
rg -n -C 4 'console_data_api_unreachable|queryLiveWire|queryLiveDomain|onStatus|observer\\?\\(' apps/console/src || trueRepository: Travis-Gilbert/CommonPlace
Length of output: 477
🏁 Script executed:
#!/bin/bash
set -eu
echo "Inspect target file around key sections"
sed -n '110,150p' apps/console/src/lib/console-host.ts
sed -n '285,315p' apps/console/src/lib/console-host.ts
sed -n '345,385p' apps/console/src/lib/console-host.ts
sed -n '968,1000p' apps/console/src/lib/console-host.ts
sed -n '1053,1090p' apps/console/src/lib/console-host.ts
echo
echo "Inspect HttpBlockHost onStatus query path"
rg -n -C 6 'onStatus|fetchRawObjectSet|objects/query|door' apps/console/src/lib -S || trueRepository: Travis-Gilbert/CommonPlace
Length of output: 45538
🏁 Script executed:
#!/bin/bash
set -eu
echo "Inspect ConsoleBlockHost query callers and transport wiring"
rg -n -C 5 'queryLiveWire|queryLiveDomain|setLastTransport|setConnection|degradationFor|lastTransport|connection' apps/console/src/lib -S
echo
echo "Search tests for ConsoleBlockHost transport observer/origin coverage"
rg -n -C 6 'ConsoleBlockHost|QueryLiveWire|queryLiveWire|queryLiveDomain|onTransport|console_data_api_unreachable|console_data_api_unreachable|origin|401|403' apps/console/src/lib/console-host.test.ts apps/console/src/lib/degradation.test.ts -S || trueRepository: Travis-Gilbert/CommonPlace
Length of output: 50382
Keep query-status failures from calling console_data_api_unreachable again.
HttpBlockHost already reports non-ok query responses through the constructor’s onStatus, including status and door. queryLiveWire and queryLiveDomain catch that thrown error, then report the same request as console_data_api_unreachable with no status or origin, so 404/401/403/502 details are overwritten by the generic unreachable banner. Only emit console_data_api_unreachable when no onStatus status/origin was already reported, while still handling post-200 parse failures.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/console/src/lib/console-host.ts` around lines 301 - 305, The query error
handlers in queryLiveWire and queryLiveDomain should not emit
console_data_api_unreachable when HttpBlockHost’s onStatus has already reported
the non-OK status and door. Track whether onStatus reported status/origin for
the current request, suppress the duplicate generic event in that case, and
continue emitting console_data_api_unreachable for failures without prior
status/origin, including post-200 response parse failures.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
ECC bundle files are already tracked in this repository. Skipping generation of another bundle PR. |
#152 was merged, but not into
main. This re-lands the same commits against the right base.What happened
#152 was stacked on
fix/console-route-export-api-key-cache(#151). GitHub does not auto-retarget a stacked PR when its base branch still exists after the parent merges, so:mainfix/console-route-export-api-key-cachebf7bb97fmainmainmoved past the stale base, so #152's work never reached it. Verified rather than assumed:1ae55b1f(feat(console): report the running commit, and say which door failed #152's merge commit) is not an ancestor oforigin/mainapps/console/src/app/api/version/route.tsdoes not exist onorigin/maindegradation.tsonorigin/maincontains zero occurrences ofDegradationOrigin/describeOriginv2.theoremharness.com/api/versionreturns 404 and serves Next.js HTMLNothing was lost. The branch is intact and this diff is exactly the six files, with no #151 duplication since that is already on main.
What this contains
Unchanged from #152, review comments included:
/api/versionreporting the running commit at request time (force-dynamic; Railway injects the sha into the container, not the build)HttpBlockHost.onStatuscarries the door it dialedVerification
Re-run on this branch:
vitest run src/lib/degradation.test.tsvitest run(@commonplace/block-view)pnpm --filter @commonplace/console run build:railwayƒ /api/versionin the manifestThe lesson worth keeping
A stacked PR whose base branch still exists will merge into that branch, not into
main, and GitHub will report it as MERGED either way. The PR status is not evidence the code shipped.git merge-base --is-ancestor <merge-commit> origin/mainis.I hit this same trap on Theorem#425 earlier and caught it there by retargeting manually. I did not re-check this one after #151 merged.
Summary by CodeRabbit
New Features
Bug Fixes