Skip to content

fix: pass over a crashed T3 home whose runtime file names the running server's port - #97

Merged
HemSoft merged 2 commits into
mainfrom
fix/issue-96-stale-t3-home-identity
Nov 7, 2026
Merged

HemSoft merged 2 commits into
mainfrom
fix/issue-96-stale-t3-home-identity

Conversation

@HemSoft

@HemSoft HemSoft commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Risk level: 2/10 (project-dashboard)
Worked on by: claude-opus-5-5 on laptop, for Chief dispatch b9abf22d (claim chief-dispatch-b9abf22d-dashboard-launch-maintenance)

Summary

The T3 button's lookup took the first T3 home whose recorded origin accepted a TCP connection. Every T3 home defaults to port 3773, and a crash leaves server-runtime.json behind. A crashed ~/.t3 or configured t3_home therefore shadowed the running ~/.t3-server and linked its stale thread under its stale environment.

  • Server::probe replaces Server::is_listening. It asks the server at the recorded origin for T3's environment descriptor (GET /.well-known/t3/environment, the unauthenticated endpoint T3's own t3 pair reads). A home counts as running only when the descriptor's environmentId is that home's environment-id. The result is a Liveness: Running, Stopped (connection refused or timed out, reported as "T3 Code isn't running" as before), or Other(reason). Other covers another environment, a non-2xx answer, a body that isn't a descriptor, or no answer within 2 s, and surfaces as the existing Unreadable reason.
  • find_in_homes and find keep their signatures and behavior for real servers. find_in_homes_with and find_with take the probe so tests can inject one. The fallback reason reuses the probe of the home resolve_userdata picks instead of probing it twice.
  • The client uses the reqwest blocking client already in the workspace, with no proxy and no redirects, a 300 ms connect timeout, a 2 s overall timeout and a 64 KiB body cap. No production manifest change.
  • The recorded pid is not used. A live PID can belong to an unrelated process after reuse, and checking it would need new per-OS process APIs. The descriptor's environment ID is what identifies the server. Both the descriptor endpoint and the pid field are present in T3 v0.1.0, v0.1.1 and current main (83a3674).

Commits:

  • 4baa070 fix:: the production change above (t3.rs, tests/t3.rs, README).
  • 9850a25 test:: test and capture evidence only. No production source or README change. It touches dashboard-app/src/ui_tests.rs (t3_homes fixture plus one UI test), dashboard-app/src/captures.rs (one capture) and dashboard-core/tests/t3.rs (stalled-server test). It also adds rusqlite.workspace = true as a dashboard-app dev-dependency to write the synthetic state databases. That puts one line in Cargo.lock ("rusqlite" in dashboard-app's list); no new crate or version.

Acceptance Criteria

  • Stale home earlier in the order, same origin as a running later home: links the running home's thread and environment. tests/t3.rs a_stale_home_on_the_running_servers_port_is_passed_over. Also through the board in ui_tests::the_t3_button_links_only_the_home_whose_own_server_answers, and in the screenshots below.
  • Same with the stale home as the configured t3_home: a_stale_configured_t3_home_on_the_running_servers_port_is_passed_over (built through candidate_homes).
  • A stale home whose port is closed is still passed over: a_stale_home_is_passed_over_for_the_running_server.
  • No running server anywhere: every checkout reports T3 Code isn't running. Same test after the fake server stops, and find_reports_why_for_every_checkout_when_t3_is_missing_or_stopped.
  • A runtime file without the fields the check needs never panics. A runtime file without origin, a non-JSON one, and an empty or missing environment-id each give a reason (a_runtime_file_without_the_servers_identity_is_a_reason). A descriptor missing environmentId, with a numeric one, non-JSON, empty, oversized, or HTTP 404/500 each give a reason (something_that_isnt_a_t3_server_on_the_port_is_a_reason). A server that accepts but never answers gives a reason within the probe's timeout (a_server_that_accepts_but_never_answers_is_a_reason). Another environment on a lone stale home's port gives a reason naming the origin (a_stale_home_alone_with_another_environment_on_its_port_links_nothing).
  • No child process and no T3 file writes: tests/no_child_process.rs passes; reading_never_writes_to_t3s_database passes. The probe is an HTTP GET.

Definition of Done

  • Fix in dashboard-core/src/t3.rs; find_in_homes' doc comment matches the behavior.

  • tests/t3.rs covers these cases, each backed by FakeT3, an in-process loopback HTTP thread that stops when dropped (60 s cap):

    • the shared-origin stale home;
    • the stale configured t3_home;
    • missing or malformed identity;
    • another environment;
    • non-T3 answers;
    • a stalled server;
    • an injected probe that asks each home once.

    Runtime fixtures carry T3's current fields (version, pid, port, origin, startedAt, serverVersion).

  • The board's T3 button runs the production lookup over synthetic homes (ui_tests::t3_homes::Homes calls dashboard_core::t3::find_in_homes), in a Windows-run UI test and in Linux captures.

  • README's T3 button section describes the identity check and the new tooltip reason.

  • CI on Ubuntu and Windows: all five required checks pass on 9850a25 (run 42366244188).

  • Review gate: SFL R1 PASS on production head 4baa070 is carried to 9850a25 under the Chief review rule. The parent independently verified the five changed files are tests/capture fixtures, a dev-only dependency and its lock entry. Production source and dependencies are unchanged; all five required checks pass. No new review was requested.

Validation

T3 button screenshots (current head 9850a25)

These come from the existing CI Screenshots step: CAPTURE_DIR="$GITHUB_WORKSPACE/captures" cargo test -p dashboard-app --locked captures -- --ignored. That ran captures_t3_home_identity on ubuntu-latest: CI run 42366244188, job rust (ubuntu-latest) 128384829157, artifact captures 13571888536 (sha256:ffe38f4cd6cdd6ca1159b74d797f999e8493baabf79bf9a2cda1667d15436cde).

The scenario is rendered offscreen by GPUI's wgpu renderer in a 1100×420 window at 2× scale; no desktop window was opened. Two synthetic homes have project rows for /demo/codexbar:

  • .t3: a crashed home, environment env-stale, thread "Stale session from a crashed home".
  • .t3-server: the running home, environment env-live, thread "Live session".

Both runtime files record one loopback descriptor server that answers as env-live. The board's ThreadFinder is the production t3::find_in_homes. Tooltips come from a synthetic window.hover(("t3", 0)) in the test window; there is no OS input. No real T3 install, profile or credentials are involved.

Wrong environment, button disabled. Homes: the crashed .t3 only. The T3 session cell is greyed out and links nothing. Before this fix it would have linked "Stale session from a crashed home".

T3 button disabled for a crashed home whose port another T3 environment answers (ubuntu-latest, 1100x420 @2x)

Wrong environment, hover tooltip. The new reason: "Can't read T3 Code's threads: the server at http://127.0.0.1:42081 is another T3 Code environment, not the one whose files were found".

Hover tooltip with the new reason naming the loopback origin (ubuntu-latest, 1100x420 @2x)

Shared port, button enabled. Homes: the crashed .t3 first, then the running .t3-server, on the same port. The button names "Live session".

T3 button enabled with the running home's Live session (ubuntu-latest, 1100x420 @2x)

Shared port, hover tooltip. "Open the T3 Code thread: Live session" and http://127.0.0.1:42081/env-live/t-live, the running home's environment and thread.

Hover tooltip with the running home's thread link (ubuntu-latest, 1100x420 @2x)

No recording. Each state is a static outcome of what is on disk and what answers at the recorded port. The only interaction is hovering one button, shown above as before and after stills. GPUI's headless renderer in this harness writes PNG frames and has no video encoder, so a recording would show nothing the four stills don't.

Commands and results

On Windows (laptop):

Command 4baa070 9850a25
cargo fmt --all --check pass pass
cargo clippy --workspace --all-targets --locked -- -D warnings pass, 0 warnings pass, 0 warnings
cargo test --workspace --locked pass: app 209, t3 22 pass: app 210, core lib 48, t3 23, no_child_process 1, 22 result lines ok, 0 failed
cargo deny --locked check advisories (0.20.2) advisories ok advisories ok
Standalone cargo check / cargo test -p dashboard-core --locked, separate target dirs pass pass, 21 result lines ok

Windows clippy doesn't compile captures.rs (Linux and macOS only); CI's rust (ubuntu-latest) does, and passed.

Focused tests at 9850a25: cargo test -p dashboard-core --locked --test t3 gives 23 passed. cargo test -p dashboard-app --locked the_t3_button gives 2 passed, including the_t3_button_links_only_the_home_whose_own_server_answers, which checks both board states on Windows. It asserts the disabled button, the exact reason, that no URL opens on click, and that the enabled click opens …/env-live/t-live.

Old-source control: the new tests/t3.rs at 4baa070, minus the two tests that call the new find_in_homes_with, run against a6ce1c8 exported with git archive. 16 pass and 4 fail:

  • a_stale_home_on_the_running_servers_port_is_passed_over
  • a_stale_configured_t3_home_on_the_running_servers_port_is_passed_over
  • a_stale_home_alone_with_another_environment_on_its_port_links_nothing
  • something_that_isnt_a_t3_server_on_the_port_is_a_reason

CI on 9850a25 (run 42366244188): advisories, core (ubuntu-latest), core (windows-latest), rust (ubuntu-latest) and rust (windows-latest) all pass.

Review

SFL R1 on 4baa070: PASS (review), with one Low, non-blocking note. The note: the probe's accepted-connection-then-timeout branch had no stalled-server test. 9850a25 adds a_server_that_accepts_but_never_answers_is_a_reason (test-only); it asserts the exact reason and that the lookup returns within the probe's bound. There are 0 unresolved review threads.

Parent verification: the test-only carry decision above applies to 9850a2533f922916a8834b014ae620c6b0e8fd7d, not a newly posted SFL review of that SHA. The parent downloaded and visually inspected all four current-head attachment images, verified HTTP 200 and each PNG hash against the reported artifact, and confirmed the disabled wrong-environment state and correct enabled live-thread state. The one Low note is covered by the new stalled-server test.

Risks and deferred work

  • A T3 server that is running but slow to answer the descriptor (over 2 s) shows a reason instead of a link until the next lookup. Before, any accepted connection counted.
  • Two homes sharing one environment-id (a copied folder) are still indistinguishable. Out of scope.

Closes #96

🤖 Generated with Claude Code

… server's port (#96)

The T3 button's lookup took the first T3 home whose recorded origin accepted a
TCP connection. Every T3 home defaults to port 3773 and a crash leaves
server-runtime.json behind, so a crashed ~/.t3 or configured t3_home shadowed
the running ~/.t3-server and linked its stale threads and environment.

Server::probe now asks the server at the origin for T3's environment
descriptor (GET /.well-known/t3/environment, which t3 pair reads too) and
counts the home as running only when its environmentId is the home's
environment-id. A closed port is still "not running"; anything else that
answers becomes a reason. find_in_homes_with and find_with take the probe so
tests can stand in for it, and each home is probed once per lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@HemSoft HemSoft added bug Something isn't working model:claude-opus-5-5 PR authored by claude-opus-5-5 labels Nov 7, 2026
@HemSoft

HemSoft commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

@sfl review

@hemsoft-sfl-reviewer hemsoft-sfl-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Verdict: PASS • Round 1

Confidence: 8/10

Review done by claude-opus-5-5 (same family: same-family: codex gated: codex Weekly 167% SUS 60 🔴 pause, defer)

The PR replaces the bare TCP is_listening check with Server::probe. The probe sends GET /.well-known/t3/environment and treats a home as running only when the reported environmentId matches that home's environment-id, so a crashed home sharing port 3773 no longer hides the live server. The PR also adds injectable find_in_homes_with and find_with and probes each home once per lookup. I found no Critical, High or Medium defect: the logic, fallback reason, size and redirect limits, and README claims match exact-SHA code and upstream T3 source. The only finding is a Low test gap.

🔨 Full-Spectrum Review Summary

Effort: high
Review time: 2m 31s
Token count: Unavailable

Pass 🔴 Critical 🟠 High 🟡 Medium ⚪ Low
🔒 Security 0 0 0 0
✅ Accuracy 0 0 0 0
🧰 Quality & Maintainability 0 0 0 1
Total 0 0 0 1

Key Findings

  1. ⚪ LOW — Quality & Maintainability: Timeout and non-connect error path of `probe` is untested. crates/dashboard-core/src/t3.rs:173: Trigger: a server accepts the connection but sends no response within IDENTITY_TIMEOUT (2s), or the request fails after connecting. The new Err(_) branch then returns Liveness::Other("... didn't say which T3 Code server it is"), so the button is disabled with an Unreadable reason rather than NotRunning. That differs from the old TCP check, which reported a slow server as running. The new tests cover connection refused, non-2xx statuses, bad bodies and oversized bodies, but no test covers a stalled server. AGENTS.md asks for failure-path coverage, and a regression in how is_connect versus timeout errors are classified would go unnoticed. Impact is limited to test coverage. The behavior looks intended. (non-blocking)

Coverage

  • Files reviewed:
    • README.md
    • crates/dashboard-core/src/t3.rs
    • crates/dashboard-core/tests/t3.rs
  • Context read:
    • AGENTS.md
    • Cargo.toml
    • crates/dashboard-core/Cargo.toml
    • rustfmt.toml
    • crates/dashboard-app/src/main.rs
    • crates/dashboard-app/src/board_view.rs
    • crates/dashboard-app/src/demo.rs
    • crates/dashboard-core/src/pmd.rs
    • crates/dashboard-core/tests/no_child_process.rs
    • crates/dashboard-core/tests/support/http.rs
    • pingdotgg/t3code packages/contracts/src/environmentHttp.ts
    • pingdotgg/t3code apps/server/src/environment/ServerEnvironment.ts
    • pingdotgg/t3code apps/server/src/config.ts
    • pingdotgg/t3code apps/server/src/cli/pair.ts
  • Verified:
    • Compare base_commit.sha and merge_base_commit.sha equal a6ce1c8e0bdbfcd3c04bf7da0428d6dddac791d3; the only commit, commits[-1].sha, is 4baa07093b57cc5cca2c28eaff177ad57edf0375; PR 97 head and base match
    • The workspace reqwest 0.12 dependency enables blocking and rustls-tls, and dashboard-core already depends on it
    • ThreadFinder::find runs on GPUI background_executor, not inside a tokio runtime, so a blocking reqwest client is safe there; pmd.rs already uses the same blocking client with redirect::Policy::none() and no_proxy()
    • Upstream T3 defines /.well-known/t3/environment in the EnvironmentMetadataHttpApi group with no auth headers or middleware, and its descriptor environmentId is read from environment-id in the same state dir as server-runtime.json
    • Body read is capped by take(DESCRIPTOR_LIMIT + 1) with a length check; redirects are off, no proxy is used, and no credentials are sent
    • When no home is running, find_in_homes_with reuses the stored Liveness for the resolve_userdata pick; homes where locate fails are never probed; covered by each_home_is_asked_once_and_its_answer_decides
    • find_with maps Running, Stopped and Other to lookups, NotRunning and Unreadable respectively, matching the README's new tooltip reasons
    • Tests cover the shared-port stale home for a default and a configured t3_home, a stale home alone, non-T3 answers (404, 500, HTML, empty, missing, numeric and oversized ID) and malformed runtime files
    • No new child process or Command use, so no_child_process.rs still holds
  • Not verified:
    • Runtime behavior against a real T3 server; upstream T3 source was read at its default branch, not the deployed version
    • Whether cargo fmt, clippy and the tests pass: the rust and core CI checks were still in progress at review time; only advisories had succeeded
    • Whether reqwest 0.12 classifies every connect-phase timeout and DNS failure as is_connect, which is inferred from library behavior, not checked in source
    • Lookup latency with a slow server, up to about 2.3s per probed home, was not measured
  • Tests run: None. Reviewer execution is disabled.

HemSoft full-spectrum review • 3 evidence-based passes • Reviewed head 4baa07093b57cc5cca2c28eaff177ad57edf0375 • SFL v0

…ver (#96)

The identity check adds a tooltip reason, so the T3 button needs current-head
UI evidence. ui_tests.rs gains t3_homes: synthetic T3 homes (state database,
runtime file, environment ID) and a loopback thread answering T3's environment
descriptor, behind a ThreadFinder that runs the production find_in_homes.
the_t3_button_links_only_the_home_whose_own_server_answers checks both states
in a test window; captures_t3_home_identity renders them, with the tooltip,
through CI's existing Screenshots step. rusqlite becomes a dev-dependency of
dashboard-app to write those databases; it is already locked.

tests/t3.rs covers a server that accepts the connection but never answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@HemSoft
HemSoft merged commit 04afd90 into main Nov 7, 2026
7 checks passed
@HemSoft
HemSoft deleted the fix/issue-96-stale-t3-home-identity branch November 7, 2026 11:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working model:claude-opus-5-5 PR authored by claude-opus-5-5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pass over a crashed T3 home whose runtime file names the running server's port

1 participant