fix(desktop): resolve agent hook endpoint at call time so host-service restarts don't strand running agents
Update 2026-08-15: rebased onto #6479, which moved agent-setup from apps/desktop/src/main/lib/agent-setup/ to packages/agent-setup/. Same change, new paths; templates were byte-identical across the move. Package suite: 121 pass.
Links
- Root-cause analysis + repro transcripts:
plans/agent-hook-url-frozen-port.md - Customer report: Maxime Auburtin (pro), 2026-08-13, four support emails; report 4 is the reproduced root cause
Summary
- Agent lifecycle hooks (notify, cursor, copilot, gemini) now resolve the host-service endpoint at call time instead of trusting the
SUPERSET_HOST_AGENT_HOOK_URLfrozen into the terminal's env at creation. - Fixes the fleet-splitting bug where every agent started before a host-service restart silently POSTs status to a dead port forever, while agents started after work fine.
Why / Context
getHostAgentHookUrl() bakes the current HOST_SERVICE_PORT into a URL that becomes part of the PTY's immutable process env (packages/host-service/src/terminal/terminal.ts:165-169, terminal.ts:2422). PTYs survive restarts via the pty-daemon (env untouched on adoption), and superset start --daemon binds a fresh findFreePort() on every start, so one CLI restart permanently kills status for every running agent. The desktop coordinator's per-org stable-port pinning masks this for app-only users, which is why it looks intermittent. The failure is silent (notify.sh swallows curl status) and no restart of the app or service can fix it, because the stale value lives in the agent process env.
The per-org manifest (~/.superset/host/<orgId>/manifest.json) already carries the live endpoint and is rewritten by both starters on every boot; the hooks simply never consulted it.
How It Works
- Each hook script builds a candidate list: the env URL first (fast path, one curl when healthy), then
<endpoint>/trpc/notifications.hookfor every${SUPERSET_HOME_DIR:-$HOME/.superset}/host/*/manifest.json, deduped. - It POSTs to candidates in order and stops when a response body contains
"ignored":false, which only the host that owns the terminal returns (notifications.ts); wrong-org hosts answer"ignored":trueand are probed past. Dead candidates fail in milliseconds on localhost. - Any 2xx without an owner still short-circuits the v1 Electron fallback, preserving the previous "any 2xx wins" semantics.
- Because hooks are re-executed from disk on every event, shipping the updated scripts heals agents that are already running: on the next hook fire they fall through the dead env URL and deliver via the manifest. No terminal restart needed.
- Markers bumped so installers rewrite the scripts: notify v8->v9, cursor v6->v7, copilot v4->v5, gemini v5->v6.
Manual QA Checklist
Validated via a controlled repro (real host-service createApp on real TCP ports, same host.db across a restart, real rendered templates; transcripts in the plan doc):
- Baseline: hook delivers 200 while the creating service instance is alive
- Before fix: after a restart on a new port, frozen-env hook gets
000on the old port and drops silently; fresh-env hook works (exact customer symptom) - After fix: frozen-env hook falls through the dead URL and delivers 200 via the manifest endpoint
- Wrong-org host (
ignored:true) does not terminate dispatch; owning org still receives the event - Env-URL fast path: manifests are not probed when the env URL answers with
ignored:false -
bash -npasses on all four rendered templates
Not manually tested: a full desktop-app end-to-end (app restart with a live agent TUI). The dispatch path exercised by the repro is byte-identical to production (same templates, same tRPC route); noting for reviewer judgment.
Testing
bun test src/main/lib/agent-setup/(apps/desktop): 113 pass- New behavioral tests spawn the real script against a fake host-service on a real port: manifest failover when the env URL is dead, probe-past on
ignored:true, env-URL fast path. Tests defaultSUPERSET_HOME_DIRto an empty temp dir so they never touch a developer's real manifests. - Mutation check: breaking the manifest glob makes 3 tests fail; restored, all pass.
bun run typecheck(exit 0)bun run lint(exit 0)
Design Decisions
- Manifest read in the scripts instead of a Unix socket: a stable local socket is the cleaner long-term shape but needs a new listener, curl
--unix-socketassumptions, and coordination across starters; it also heals existing agents no faster. The manifest is already rewritten every start by both the CLI and the desktop child. - Instead of CLI port pinning: pinning (which the desktop already does) narrows the window but keeps the flaw; the port can be taken, and lineages still split when switching between app-started and CLI-started services.
ignored:falseas the owner discriminator: avoids putting org identity into the terminal env (which would freeze too) and makes multi-org probing safe with zero auth changes; the endpoint is unauthenticated by design.
Known Limitations
- Pure headless installs where the desktop app never runs don't get hook scripts rewritten today Shipped independently in #6479: host-service now provisions hooks on headless CLI hosts, so this fix reaches them too.
Follow-ups
- CLI stable-port parity with the desktop coordinator.
- One-shot UI surfacing when hook POSTs fail with connection errors.
- Remove or populate
SUPERSET_AGENT_HOOK_PORTin the CLI spawn (v1-only value, unset on CLI-started services, nothing in v2 consumes it).
Risks / Rollout
- Risk: hook scripts now read manifest files per event. Scope is a couple of
greps over tiny local JSON files and at most one extra localhost curl per stale endpoint; hooks already shell out to curl per event. - Rollout: ships with the desktop release; scripts rewrite on app start (content-diff based) and running agents heal on their next hook fire.
- Rollback: revert the templates and marker bumps; installers rewrite the previous scripts on next start.
Summary by cubic
Prevents agents from losing status delivery after a host-service restart. Previously hooks POSTed to the SUPERSET_HOST_AGENT_HOOK_URL frozen at terminal creation; after a restart on a new port, running agents sent to a dead socket. Now hooks resolve the endpoint at call time: they try the env URL first, then manifest endpoints, and stop when the owning host returns ignored:false; “any 2xx” still skips the v1 fallback.
- Updates
notify,cursor,copilot,geminitemplates to gather candidates from the env and${SUPERSET_HOME_DIR:-$HOME/.superset}/host/*/manifest.json, dedupe, and dispatch with low timeouts. Changes live inpackages/agent-setup/templates/*-hook.template.sh. - Bumps hook markers (notify v9, cursor v7, copilot v5, gemini v6) and updates wrappers/tests in
packages/agent-setup. Adds behavioral tests for manifest failover, wrong‑org probe‑past (ignored:true), and env fast path; tests isolateSUPERSET_HOME_DIRto a temp dir. - Docs: tags repro transcripts in
plans/agent-hook-url-frozen-port.mdas text.
Rollout
- No migration. Desktop rewrites hooks on app start; running agents heal on their next hook event.
- Low risk: may read tiny local manifests and add at most one extra localhost curl when the env URL is stale.
Summary by CodeRabbit
-
Bug Fixes
- Improved agent hook delivery after host-service restarts by resolving current endpoints at call time.
- Hooks now try configured and discovered endpoints, avoid duplicates, and recognize explicit delivery acceptance.
- Preserved fallback behavior when no host accepts an event.
- Added handling for unavailable, ignored, or outdated endpoints.
-
Documentation
- Added design, validation, reproduction, and follow-up documentation for frozen hook URLs.
-
Tests
- Expanded coverage for endpoint discovery, fallback behavior, dead URLs, ignored hosts, and successful fast paths.