Skip to content

§1+§2: polish, harden, invariant re-proofs + ROADMAP update - #38

Merged
michaelroy-amd merged 11 commits into
mainfrom
supergoal/phase-10-polish-harden
Jun 26, 2026
Merged

§1+§2: polish, harden, invariant re-proofs + ROADMAP update#38
michaelroy-amd merged 11 commits into
mainfrom
supergoal/phase-10-polish-harden

Conversation

@michaelroy-amd

Copy link
Copy Markdown
Member

Stack 10/10 · base: supergoal/phase-9-reroute-and-gate

  • Edge/error hardening: seam errors recoverable (no panic), approval Esc/Cancel no focus-trap, daemon no-double-spawn, no-key chat actionable.
  • Invariants re-proved by command (rocm-dash-core pure; rocm-dash-tui 0 rocm-core edges; rig sole-namer; rocm→rocmd acyclic).
  • Splits the oversized app.rs into app/{slash,chat,summary}.rs; dedups the agent complete() tail. Updates repo ROADMAP.md (§2 done, §1 parity-reached + gated).

Reviewed: final cumulative rust + security + maintainability over phases 1-10, zero unresolved CRITICAL/HIGH. Full suite green (1708 tests, --test-threads=1).

@michaelroy-amd

Copy link
Copy Markdown
Member Author

Launch-panic fix: provider key resolution moved off the runtime thread

Symptom: bare rocm / rocm chat panicked at launch with
Cannot start a runtime from within a runtime (tokio multi-thread block_on)
whenever ANTHROPIC_API_KEY was unset in the shell.

Root cause (from the real backtrace):

run_chat → Runtime::block_on → run_async → resolved_args        (dash multi-thread runtime worker)
  → provider_keys::resolve_provider_api_key   (env-first, keyring-fallback)
  → NativeProviderKeyStore::get_secret → zbus_secret_service_keyring_store::Store::new
  → secret_service::blocking::SecretService::connect
  → zbus::blocking::Connection::session → Runtime::block_on    ← PANIC (runtime within a runtime)

resolved_args was being called inside run_async. With no env key, the Anthropic
lookup falls through to the OS secret-service store, whose zbus::blocking shim
builds its own tokio runtime + block_on — illegal while the dash runtime is
already entered on that thread. (Introduced on this branch in 614aac6, which began
sourcing the Anthropic key into ResolvedArgs.)

Fix: hoist resolved_args() to the synchronous run/run_chat entry points,
before rt.block_on, and pass the prebuilt ResolvedArgs into run_async. The
keyring lookup now runs on the plain main thread where zbus::blocking is legal.
run_async drops initial_tab and takes args instead, making it type-level
impossible to re-resolve keys on a runtime thread.

Verification:

  • Root cause proven deterministically (throwaway tests, since removed): the keyring
    call panics inside block_on and is panic-free outside it.
  • PTY launch with ANTHROPIC_API_KEY unset no longer panics.
  • Existing dash::tests pass; clean build.

Audited all other keyring call sites (providers.rs, main.rs config subcommands,
legacy tui.rs) — all run on synchronous threads. Runtime provider-switching reuses
the already-resolved key from ResolvedArgs, so it never re-hits the keyring.

@michaelroy-amd

Copy link
Copy Markdown
Member Author

Chat endpoint detection: use the real launched port (managed-services registry)

Problem: the dash chat only auto-detected the well-known local ports
(vLLM 8000, Lemonade 13305). An engine launched via the tool on a
non-default port — e.g. vLLM on :11435 — was invisible to chat detection, so
bare rocm fell back to the no-key default instead of talking to the local
server we just started.

Fix (115f322): detection is now registry-first. It queries the
read-only services tool and uses the actual endpoint_url of a ready,
OpenAI-compatible managed service (vllm/lemonade/llama.cpp/sglang/
pytorch), preferring the most recently created. ready mirrors the bin's own
HTTP readiness check (what rocm services shows), so a selected endpoint is one
already verified to serve; a best-effort /v1/models fetch confirms liveness
and supplies the served model id.

  • Wired into both the startup backend resolution and the live "press d
    to detect" path.
  • Falls back to the well-known TCP probe (8000/13305) when no managed
    service exists.
  • Never overrides an explicitly configured chat_url/env URL — config
    precedence preserved (the registry only fills the tier where the default port
    would otherwise be probed).

Behavior

  • vLLM already running at launch → chat auto-binds to its real endpoint, any port.
  • Launched mid-session → press d in chat to re-detect live.
  • Nothing managed → unchanged (probe 8000/13305).

Tests/lint: new pure picker pick_managed_chat_endpoint with 7 unit tests
(headline non-default-port case, ready-only gating, newest-wins, engine
filtering, model_ref fallback, malformed/empty). Full dash-tui suite (457)
passes; cargo fmt --check and cargo clippy --workspace --all-targets -D warnings clean.

Verify on an MI3xx node

rocm services            # status: ready, endpoint: http://127.0.0.1:11435/v1
rocm                     # chat tab auto-binds to :11435

@rominf rominf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM. Verified the 767 deletions are all verbatim moves of app/mod.rs into app/{slash,chat,summary}.rs with visibility narrowed to pub(super) — no behavior, error handling, or validators removed; approval-gating intact. The launch-panic fix is type-level enforced (hoists the keyring probe out of the async runtime), and the "invariant re-proofs" are real assertions (no-double-spawn guard, recoverable seam errors, honest provider revert) rather than comment-claims.

Two minor notes: the architectural-purity invariants are compiler-enforced but have no guard test (a cargo tree CI check would catch a future accidental dep); and resolved_args() now probes the keyring unconditionally including on --demo/--replay paths (harmless, wasted round-trip).

@michaelroy-amd
michaelroy-amd force-pushed the supergoal/phase-9-reroute-and-gate branch from 488ca6a to 11f5683 Compare June 23, 2026 03:41
@michaelroy-amd
michaelroy-amd force-pushed the supergoal/phase-10-polish-harden branch from 115f322 to ed7442b Compare June 23, 2026 03:41

@rominf rominf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes on signing grounds — this is separate from the code review above, which still stands.

Every commit on this PR is unsigned: GitHub reports verification.verified = false (reason unsigned) for all of them. Signed commits are required before this can merge.

To fix, configure commit signing and re-sign the branch history, e.g. with SSH signing:

git config gpg.format ssh
git config user.signingkey <your-signing-key.pub>
git config commit.gpgsign true
git rebase --exec 'git commit --amend --no-edit -S' <base-branch>
git push --force-with-lease

Since this is a stacked PR, re-signing is easiest from the bottom of the stack upward — re-signing a base branch rewrites its SHAs and the dependent branches will need rebasing/re-pushing on top.

Once the commits show as Verified I'll clear this.

@michaelroy-amd
michaelroy-amd force-pushed the supergoal/phase-9-reroute-and-gate branch from 11f5683 to 05085b1 Compare June 23, 2026 17:39
@michaelroy-amd
michaelroy-amd force-pushed the supergoal/phase-10-polish-harden branch from ed7442b to 42f262b Compare June 23, 2026 17:39

@rominf rominf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Signing gate cleared — all commits now show Verified. Re-reviewed the current diff against supergoal/phase-9-reroute-and-gate after the re-sign.

Approving. The headline honest provider revert is present and correct — ProviderSwitch { previous, target } snapshots the pre-switch provider and reverts to previous (not hardcoded Local) on build failure, fixing the #36 desync; test-covered. The app.rsapp/{mod,slash,chat,summary}.rs split is a faithful file move (bodies byte-identical), and the pre-runtime keyring resolution correctly avoids the nested-runtime panic.

Non-blocking follow-up (unchanged): a CI check asserting the acyclic rocm → rocmd cargo-tree invariant would be nice but isn't in this diff and doesn't gate merge.

Copilot AI 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.

Pull request overview

Polishes and hardens the unified dashboard chat/daemon flows, re-proving key invariants while refactoring the rocm-dash TUI app module into cohesive submodules and updating the project roadmap to reflect §1/§2 completion status.

Changes:

  • Refactors rocm-dash-tui app logic by splitting chat backend construction, slash-command routing, and summary rendering into dedicated modules.
  • Improves chat/provider robustness (managed-service endpoint detection, actionable “no backend” empty state, provider-switch revert correctness) and deduplicates agent completion tail logic.
  • Hardens daemon/background-helper behavior (no double-spawn guard) and avoids tokio runtime nesting issues by resolving keyring-dependent args before starting the dashboard runtime.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
ROADMAP.md Updates roadmap narrative/status for §1 parity and §2 daemon wiring regression.
crates/rocm-dash-tui/src/app/summary.rs Adds pure, concise rendering/parsing helpers for slash-tool outcomes and /plan results.
crates/rocm-dash-tui/src/app/slash.rs Moves slash-command parsing/routing out of app/mod.rs into an I/O-free reducer helper.
crates/rocm-dash-tui/src/app/mod.rs Wires new submodules, improves provider-switch edge data, adds actionable no-backend message, and updates chat detection to consider managed endpoints.
crates/rocm-dash-tui/src/app/chat.rs Centralizes provider agent construction, adds managed-service endpoint picking/detection, and keeps config persistence logic cohesive.
crates/rocm-dash-tui/src/agent.rs Deduplicates agent complete() tail (timeout/error/annotation), centralizes token budget, and adds seam-error recoverability tests.
apps/rocm/src/main.rs Adds background-helper liveness guard helper + tests; prints a note when interactive rocm chat ignores --provider (rerouted to dash chat).
apps/rocm/src/dash.rs Extracts dashboard runtime builder and resolves args (incl. keyring lookup) before entering tokio runtime to avoid nested-runtime panics.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

pub(super) fn scalar_or_shape(v: &serde_json::Value) -> String {
use serde_json::Value;
match v {
Value::String(s) => s.clone(),
Comment thread apps/rocm/src/main.rs
Comment on lines +1216 to +1218
eprintln!(
"note: launching the dash chat; switch providers with /provider <name>"
);

@rominf rominf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-approving the current head after the cascade rebase onto the updated base — refreshing my prior approval onto this exact commit.

Re-reviewed: §1/§2 polish: app module split (chat/slash/summary), provider-switch revert correctness, daemon no-double-spawn guard, nested-runtime fix.

Copilot re-reviewed this same commit (COMMENTED, no blocking findings). No clear issues; LGTM.

@michaelroy-amd
michaelroy-amd force-pushed the supergoal/phase-9-reroute-and-gate branch from 4c515e0 to 2c8d3c8 Compare June 26, 2026 18:10
Base automatically changed from supergoal/phase-9-reroute-and-gate to main June 26, 2026 21:34
…ing tools

A read-only and a mutating ROCm tool whose injected executor returns
RocmToolOutcome::Error("boom") return a Value carrying an `error` key
(recoverable) and never panic; the mutating error path surfaces no
approval intent. Complements the existing None-executor graceful tests.

Signed-off-by: Michael Roy <michael.roy@amd.com>
…t edge states

- /provider revert: capture the provider active BEFORE the optimistic
  switch (new ProviderSwitch{previous,target} edge) so a failed build
  (missing key) reverts active_provider to the prior provider, not
  unconditionally Local (Phase-8 review deferral).
- No-backend submit: emit an actionable empty-state (NO_CHAT_BACKEND_MSG)
  naming the two recovery paths (detect / /provider) instead of a terse
  "chat backend unavailable".
- Tests: provider-switch captures previous; no-provider/no-key chat
  surfaces an actionable error turn (no panic); approval modal Esc and 'n'
  both yield a closing verdict that clears the modal with the covered tab
  preserved (no focus trap).

Signed-off-by: Michael Roy <michael.roy@amd.com>
…interactive chat

- background_helper_already_running(&paths): factor the "should spawn?"
  decision out of ensure_background_helper_running so it is testable
  hermetically with no spawn side effect. Hermetic offline tests prove it
  is true for a live recorded pid + running=true (no double spawn) and
  false for a missing state file, a dead/zero pid, or running=false.
- Interactive `rocm chat` with --provider now emits a one-line note
  pointing at /provider for live switching instead of dropping the flag
  silently; still reroutes to dash::run_chat. Test reads the handler body.

Signed-off-by: Michael Roy <michael.roy@amd.com>
…n_chat

Dedup the identical multi-thread tokio runtime construction in dash::run
and dash::run_chat into a tiny private build_dashboard_runtime() -> Result<Runtime>.
Behavior identical.

Signed-off-by: Michael Roy <michael.roy@amd.com>
…on) done

S1: parity REACHED, bare rocm/chat REROUTED to the dash, tui.rs deletion
GATED (retained behind _RETAINED_TUI_ENTRY); add a what-shipped note (read-only
tools, approval-gated mutating, /plan, /provider+anthropic, 30-command parity
map) linking the parity map + checklists. S2: DONE — rocm daemon runs the real
loop in-process via the rocmd lib with on-demand double-spawn-guarded autostart;
render_daemon_text retained as --status. Add a brief Supergoal S1+S2 summary.

Signed-off-by: Michael Roy <michael.roy@amd.com>
…plete() tail

HIGH-1: convert the oversized app.rs (~2813 production lines) into a module
directory. Extract handle_slash_command into app/slash.rs, the chat-backend
construction/detection/persistence group (build_chat_agent, detect_local_chat,
persist_chat_endpoint, config_with_chat, fetch_first_model, set_chat_config)
into app/chat.rs, and the pure display utilities (summarize_slash_tool,
summarize_json_value, scalar_or_shape, parse_plan_result, SUMMARY_MAX_FIELDS)
into app/summary.rs. AppState struct, core constructor, and the event loop stay
in app/mod.rs (now ~2122 production lines). Pure code movement, no behavior
change; a file->dir module move keeps crate::app::* paths valid.

HIGH-2: dedup the 6-line complete() tail copy-pasted across RigAgentClient,
ChatGptAgentClient, and AnthropicAgentClient into one generic helper
finish_agent_request<F, E>, generic over IntoFuture<Output = Result<String, E>>
to bridge rig's per-backend typestate. Also add const MAX_AGENT_TOKENS = 1024
(u64, to match rig's AgentBuilder::max_tokens) used by all three .max_tokens()
calls (rust-reviewer LOW).

Behavior identical; 1708 tests pass single-threaded; clippy -D warnings + fmt
--check clean; agent.rs remains the sole rig crate namer.

Signed-off-by: Michael Roy <michael.roy@amd.com>
Bare `rocm` and `rocm chat` panicked at launch with "Cannot start a
runtime from within a runtime" whenever ANTHROPIC_API_KEY was unset.

run_async built ResolvedArgs on a dash multi-thread runtime worker via
resolved_args(), whose Anthropic-key lookup falls through to the OS
secret-service store when the env var is absent. That store uses
zbus::blocking, which builds its own runtime and block_on's internally —
illegal while a runtime is already entered on the thread.

Hoist resolved_args() to the synchronous run/run_chat entry points,
before rt.block_on, and pass the prebuilt ResolvedArgs into run_async.
The keyring lookup now runs on the plain main thread where zbus::blocking
is legal. run_async drops its initial_tab param and takes args instead,
so it is type-level impossible to re-resolve keys on a runtime thread.

Root cause confirmed deterministically: the keyring call panics inside
block_on and is panic-free outside it.

Signed-off-by: Michael Roy <michael.roy@amd.com>
… launched port)

The dash chat only auto-detected the well-known local ports (vLLM 8000,
Lemonade 13305), so an engine launched via the tool on a non-default port
(e.g. vLLM on 11435) was invisible to chat detection.

Make detection registry-first: query the read-only `services` tool and use
the actual `endpoint_url` of a ready, OpenAI-compatible managed service
(vllm/lemonade/llama.cpp/sglang/pytorch), preferring the most recently
created. "ready" mirrors the bin's own HTTP readiness check, so a picked
endpoint is one already verified to serve; a best-effort /v1/models fetch
confirms liveness and supplies the served model id. Falls back to the
well-known TCP probe when no managed service exists, and never overrides an
explicitly configured chat_url/env URL (config precedence preserved).

Wired into both the startup backend resolution and the live "press d to
detect" path. New pure picker `pick_managed_chat_endpoint` with unit tests
(incl. the headline non-default-port case, ready-only gating, newest-wins,
engine filtering, and model_ref fallback).

Signed-off-by: Michael Roy <michael.roy@amd.com>
Signed-off-by: Michael Roy <michael.roy@amd.com>
Signed-off-by: Michael Roy <michael.roy@amd.com>
…headers)

The phase-10 app.rs split predates #36/#37 and the repo's MIT relicense, so the
extracted modules carried stale logic and license headers that the rebase's
"keep the split structure" resolution preserved. Restore main's behavior:

- build_chat_agent's OpenAI arm requires a non-empty key and returns None
  otherwise (#36); the older split passed the raw Option through, building a
  dead sk-no-key backend that 401s at request time.
- /update --apply is position-independent (scan all tokens), matching
  /uninstall, not only the second token.
- /uninstall rejects conflicting --apply + --dry-run with a guided error.
- app/{chat,slash,summary}.rs SPDX headers switched Apache-2.0 → MIT to match
  the relicensed tree (licenserc.toml / hawkeye gate).

All dash-tui tests pass; hawkeye clean.

Signed-off-by: Michael Roy <michael.roy@amd.com>
@michaelroy-amd
michaelroy-amd enabled auto-merge June 26, 2026 22:40
@michaelroy-amd
michaelroy-amd force-pushed the supergoal/phase-10-polish-harden branch from a8d4367 to c9c8648 Compare June 26, 2026 22:40
@michaelroy-amd
michaelroy-amd added this pull request to the merge queue Jun 26, 2026
Merged via the queue into main with commit 2a5a7ec Jun 26, 2026
8 checks passed
@michaelroy-amd
michaelroy-amd deleted the supergoal/phase-10-polish-harden branch June 26, 2026 23:16
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.

3 participants