Skip to content

fix(tui,mcp): startup freeze 3-layer + usage probe trigger/matching/render chain - #2542

Merged
topcheer merged 5 commits into
mainfrom
fix/usage-probe-url-sidebar
Sep 18, 2026
Merged

topcheer merged 5 commits into
mainfrom
fix/usage-probe-url-sidebar

Conversation

@topcheer

Copy link
Copy Markdown
Owner

What

Five-part fix chain, all verified by unit tests + live evidence:

  1. Startup freeze (3-layer) — ack timeout 10s→2s, permanent downgrade on deadline, subs upgrade moved off the plugin write lock. Drain 20s→250ms (live-captured).
  2. Usage auto-trigger — probes previously fired ONLY on manual panel open/r. Now first probe at input-drain end, 60s re-arm chain (cache-deduped upstream).
  3. Full-URL probe matching — hosts are shared between metered and coding-plan APIs (open.bigmodel.cn); only /coding* and /api/anthropic paths claim the zai probe. Metered bases resolve to no probe (sidebar explains why).
  4. Runtime-source key resolution — usage chain resolves through the SAME resolver as chat (vendor-key fallback + ${VAR} expansion). zai/cn-coding-openai (key at vendor level only) previously read as keyless on the usage chain while chat worked.
  5. fetch Cmd returned tea.Batch as Msg — a Cmd VALUE returned where a Msg is required; Update dispatch silently dropped it. Probes ran, results vanished, panel stuck on 获取中 / sidebar stuck on probing forever.

Regression anchors

  • TestVendorLevelKeyReachesProbe (endpoint keyless + vendor key → probe gets vendor key)
  • TestFetchCmdReturnsRealMsg (cmd() returns usageInfoUpdatedMsg, flips panel out of fetching)
  • TestExplainUsageSidebarBranches (every no-probe branch renders its reason)

Test plan

  • internal/tui ok
  • internal/usage ok (metered-unclaimed is asserted)
  • internal/config ok
  • full repo go test zero FAIL

Junjun Zhang and others added 5 commits September 18, 2026 23:00
Freeze (live-captured 2026-09-18, drain 20s -> 4.4s -> 250ms):
- subscriptions/listen ack wait 10s -> 2s (SubscriptionsAckTimeout):
  silent servers (Pen.app stdio pair) never answer -32601, each stalled
  the connect path the full 10s and gated 'connected' state late
- deadline-exceeded now downgrades permanently (subStateUnsupported),
  reconnects do not re-pay the ack wait
- subs upgrade moved OFF the plugin write lock (postConnect): Connect
  held m.mu across the ack wait while refreshCommands -> SnapshotMCP ->
  Info() takes the same lock's read side - every UI command refresh
  stalled behind ack deadlines; two serialized 10s stalls = the 20s
  freeze; 8s-timeout HTTP servers queued behind them died (railway/
  zread/cf now connect)

Sidebar usage attribution (user report: sidebar never updates):
- attribution now compares in the RESOLVED probe-id domain
  (svc.Resolve of the session's current endpoint URL), not the config
  vendor-name domain; session-runtime vendor/endpoint (persisted per
  session, config file only feeds new sessions) was renamed configs'
  silent-empty-sidebar root cause. Unit tests pin URL-domain
  attribution, error-clears-stale, and name-domain fallback.

Instrumentation:
- mcpServersMsg handler segment timing (toMCPInfos/refreshCommands/
  pendingOAuth) and slow-view-render section attribution logs

Bench: internal/chat render/scroll/stream micro-benchmarks.

Co-Authored-By: ggcode <noreply@ggcode.dev>

Co-Authored-By: ggcode <noreply@ggcode.dev>
Root cause of "usage never shows" (user: trigger logic rewritten a dozen
times, never right): fetchAllUsageCmd had exactly two callers - manual
openUsagePanel and the panel's r key. Unless the user manually opened the
usage panel, no probe ever fired, no usageInfoUpdatedMsg ever arrived, and
the sidebar stayed empty regardless of attribution/parse correctness. All
previous fixes patched the result end; the trigger end never existed.

- First probe fires at input-drain end (UI interactive, ~250ms after
  start); subsequent rounds re-arm from handleUsageInfoUpdated every 60s
  (Service success/negative/429 caches de-duplicate upstream HTTP; zai's
  5h rolling window recovers minute-by-minute, so 60s is the first
  cadence that tracks the meter)
- explainUsageSidebar: every no-probe branch renders a one-line reason in
  the sidebar (vendor X not in config / endpoint not found / no api key /
  no usage probe for host / probing...) - silent-empty sidebars are now
  diagnosable from the UI alone; probe failure shows the error, success
  clears the line
- branch unit tests (vendor-missing / unclaimed-host / keyless / probeable)

Co-Authored-By: ggcode <noreply@ggcode.dev>

Co-Authored-By: ggcode <noreply@ggcode.dev>
Design corrections per user review (2026-09-18):
- Probes now match the FULL endpoint URL, not the bare host:
  open.bigmodel.cn is SHARED between metered /api/paas/v4 and coding
  /api/coding/paas/v4 - only the plan entrance carries window quotas,
  so ZaiProbe claims /coding* and /api/anthropic paths; metered bases
  resolve to no probe (sidebar explains: no usage probe for host).
- currentEndpointForUsage now resolves through the SAME runtime
  resolver as chat (ResolveEndpointSelection): vendor-level key
  fallback + ${VAR} expansion applied. Previously it read the raw
  config field, so zai/cn-coding-openai (key at vendor level only)
  read as keyless and no probe ever fired - while chat worked fine.
- No attribution layer: probes fire only for the session's current
  endpoint, so any result that arrives renders directly. The probe-id
  comparison, vendor-name fallback, and msg.baseURL round-trip are
  deleted.
- Sidebar shows the exact blocking reason (resolution error verbatim /
  no api key / no usage probe for host) when nothing can render.

Tests updated to the URL semantics: metered bases unclaimed, coding
paths claimed, test fixtures carry models and coding paths.

Co-Authored-By: ggcode <noreply@ggcode.dev>

Co-Authored-By: ggcode <noreply@ggcode.dev>
Regression anchor for the 2026-09-18 break: zai/cn-coding-openai carries
its api key ONLY at vendor level. Constructs that exact config shape and
asserts (1) the probe is authorized, (2) fetchAllUsageCmd dispatches one
probe call, (3) Fetch receives the VENDOR-level key and the coding-plan
URL. Without this, the usage chain reading raw config fields (keyless)
while chat resolved fine can silently return.

Co-Authored-By: ggcode <noreply@ggcode.dev>

Co-Authored-By: ggcode <noreply@ggcode.dev>
The fetch closure did 'return tea.Batch(cmds...)' - a Cmd VALUE (function)
returned where a Msg is required. bubbletea dispatched the function object
to Update, no handler matched, and every probe result vanished: the usage
panel stuck on 获取中 and the sidebar on probing forever, while the probes
themselves ran fine. Deliver the first result as the Cmd's real Msg; extra
results go through program.Send.

Guard test: TestFetchCmdReturnsRealMsg asserts cmd() returns
usageInfoUpdatedMsg and that it flips the panel out of fetching.

Co-Authored-By: ggcode <noreply@ggcode.dev>
@topcheer
topcheer merged commit 88188fe into main Sep 18, 2026
9 checks passed
topcheer pushed a commit that referenced this pull request Sep 19, 2026
3 PRs this cycle: #2533 memory recall-time conflict arbitration,
#2535 runeval offline scorecard (/runreport), #2542 startup freeze
3-layer + usage probe trigger/URL-matching/key-resolution/render
chain (fetch Cmd-as-Msg drop fix). verify-ci passed (GOFLAGS=-p=1).

Co-Authored-By: ggcode <noreply@ggcode.dev>
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.

1 participant