Skip to content

feat(web): auto re-pair a local assistant on connect via vellum wake (LUM-2233)#33271

Merged
vex-assistant-bot[bot] merged 3 commits into
mainfrom
devin/1780504985-local-recovery-affordances
Jun 3, 2026
Merged

feat(web): auto re-pair a local assistant on connect via vellum wake (LUM-2233)#33271
vex-assistant-bot[bot] merged 3 commits into
mainfrom
devin/1780504985-local-recovery-affordances

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Prompt / plan

Reach parity with the native macOS client's bootstrap for the web/Electron local-assistant login path, and nothing more — a minimal, unblocking change. The native client (clients/macos/.../App/AppDelegate+Bootstrap.swift) re-pairs a stopped, expired, or mis-seeded local assistant automatically (forceReBootstrap → re-provision creds + restart) before any error reaches the user. The web/Electron connect path had only the token-refresh rung and dead-ended on a "Couldn't connect" + Retry card. This PR adds the one missing rung: an automatic vellum wake + retry on a repairable connect failure.

wake is the CLI's non-destructive repair primitive — it re-seeds the guardian token from a sibling environment and restarts the daemon + gateway, leaving the assistant's data and identity untouched. It is the CLI equivalent of native's re-pair.

Closes LUM-2233.

Broader recovery UI (a terminal recovery card with Create/Remove and per-lockfile-state cloud fallbacks) is intentionally out of scope and handed off under LUM-2232.

Changes

The wake seam is wired end to end, following the existing hatch/retire pattern at every layer:

  • @vellumai/local-moderunWake CLI driver (mirrors runRetire: spawn vellum wake <id>, 60s timeout, never-reject { ok } contract).
  • Electron mainvellum:localMode:wake IPC handler.
  • Electron preloadwindow.vellum.localMode.wake contract.
  • Web dev middleware/assistant/__local/wake endpoint in the Vite plugin (loopback-guarded, mirrors the retire endpoint).
  • runtime/local-mode-hostwakeLocalAssistantHost seam branching Electron IPC vs dev fetch.
  • lib/local-modeprimeLocalGatewayConnectionWithRepair: on a repairable failure, run wake once and re-prime; classify a GuardianTokenError 403 (refused loopback boundary) as non-repairable.
  • stores/auth-store — the interactive connectLocalAssistant opts into the repair-wrapped primitive; the best-effort boot probe stays on the plain primitive.

Test plan

  • Unit (new):
    • packages/local-mode/src/__tests__/wake.test.tsrunWake spawns the right argv, surfaces CLI output on non-zero exit, and resolves (not rejects) on spawn failure.
    • apps/web/src/runtime/local-mode-host.test.tswakeLocalAssistantHost POSTs to the dev endpoint, routes through the Electron bridge without touching fetch, and reports an unsupported failure when an older shell lacks the wake channel.
    • apps/web/src/lib/local-mode-repair.test.ts — clean connect never wakes; a repairable failure wakes once then retries to success; a still-failing retry and a failed wake both surface the original error without looping; a non-repairable 403 surfaces immediately and never wakes.
  • bunx tsc --noEmit green for apps/web, packages/local-mode, apps/macos.
  • bun run lint green for apps/web (pre-existing exhaustive-deps warnings only, none in touched files).
  • CI.

Safety

  • Additive and opt-in. Only the interactive connectLocalAssistant changes behavior; the happy path is unchanged (a clean first prime never spawns anything). The boot probe deliberately stays on the plain primitive so app launch never spawns daemon processes.
  • Non-destructive repair. wake re-pairs in place — it never retires or re-hatches, so the assistant's data and identity survive. This matches native's forceReBootstrap, which is destructive only to credentials, not the assistant.
  • Failure modes preserve the existing UI. A non-repairable 403, a failed wake, or a still-failing retry all propagate the original error so the existing connect-error card surfaces it unchanged.
  • Version skew (mixed macOS / web bundle). The macOS app and web bundle do not release together, so a newer renderer can run against an older Electron preload that predates the wake IPC channel. The wake channel is typed optional in the renderer and guarded at the seam, so an older shell degrades to a no-op repair (falls through to the connect error) instead of throwing.

References

  • Native parity reference: clients/macos/vellum-assistant/App/AppDelegate+Bootstrap.swift (forceReBootstrap, ensureActorCredentials).
  • HeyAPI fetch interceptors and the existing hatch/retire host seam this mirrors.

CLI verb checklist

Not applicable — this PR adds no new IPC route under assistant/src/runtime/routes/. It wires an Electron-main IPC handler and a Vite dev middleware that drive the existing vellum wake CLI verb; no new assistant verb is needed.


Link to Devin session: https://app.devin.ai/sessions/15bca57bd4c64a3085cfb80e1f26355a
Requested by: @ashleeradka

… bootstrap

When connecting to a local assistant from the login page, the connect path
now self-heals before surfacing an error: on a repairable failure it runs
`vellum wake` (re-seeds the guardian token and restarts the daemon + gateway,
leaving the assistant's data and identity untouched), then primes the
connection once more. This matches the native client's bootstrap, which
re-pairs a stopped, expired, or mis-seeded assistant automatically rather
than dead-ending the user on a "Couldn't connect" + Retry card.

Wiring follows the existing hatch/retire host seam end to end:
- `runWake` CLI driver in @vellumai/local-mode (mirrors runRetire).
- `vellum:localMode:wake` IPC handler + preload contract in the Electron app.
- `/assistant/__local/wake` dev middleware in the web Vite plugin.
- `wakeLocalAssistantHost` seam in runtime/local-mode-host (Electron IPC vs
  dev fetch), and `primeLocalGatewayConnectionWithRepair` wrapping the connect
  primitive with one wake + retry.

Safe and contained: only the interactive connect (`connectLocalAssistant`)
opts into repair; the best-effort boot probe stays on the plain primitive so
app launch never spawns daemon processes. A 403 (refused loopback boundary) is
non-repairable and surfaces unchanged; a failed wake or a still-failing retry
propagates the original error so the existing connect-error UI is preserved.
The preload `wake` channel is treated as optional in the renderer so an older
Electron shell (the macOS app and web bundle don't release together) degrades
gracefully instead of throwing.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@linear

linear Bot commented Jun 3, 2026

Copy link
Copy Markdown

LUM-2233

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ca8d30093

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/web/src/lib/local-mode.ts
Comment thread apps/web/src/stores/auth-store.ts
…ests

connectLocalAssistant now primes through the repair wrapper, so the
@/lib/local-mode mock must export it; without it the imported function
is undefined and the connect tests throw before exercising behavior.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@ashleeradka

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6442c1a94e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/local-mode/src/wake.ts Outdated
vellum wake cold-starts a stopped assistant: the CLI waits up to 60s for
the daemon (plus a 60s source-daemon fallback) and up to 30s for the
gateway. The 60s wrapper timeout could kill a slow-but-succeeding wake
before the gateway came up, misreporting auto-repair as a timeout. Raise
the safety-net timeout to 180s so it sits above those readiness windows.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@vex-assistant-bot

Copy link
Copy Markdown
Contributor

@codex review — WAKE_TIMEOUT_MS raised to 180s in commit 67f8955 after your P2. Please re-verify at HEAD.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 67f8955dfb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +313 to +314
await primeLocalGatewayConnection();
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Verify gateway reachability before skipping wake

When a user reconnects to a slept/stopped local assistant while an unexpired gateway token is still cached for the same token URL, primeLocalGatewayConnection() can resolve without touching the gateway: it reads the guardian token from disk, ensureGatewayToken() returns the cached token, and this path returns before wake is attempted. connectLocalAssistant then marks the user logged in even though the gateway is still stopped, so the new auto-repair path misses a common stopped-assistant case until later API calls fail. Consider forcing a real gateway/token probe or bypassing the cached gateway token before deciding repair is unnecessary.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same gap as the earlier thread on this file — agreed it's real, and intentionally not fixed here.

ensureGatewayToken() returns the cached token (30-day TTL) without touching the gateway, so a slept/stopped assistant with a live token primes "successfully" and this path returns before wake; the dead gateway only surfaces on the first data-plane request.

Two reasons it stays out of this PR:

  1. It changes the happy path. A real gateway/token probe (or bypassing the cache) on every connect would spawn the daemon + gateway on every login even when nothing's wrong — out of scope for this minimal native-parity change. Note native is the same shape: forceReBootstrap fires from GatewayConnectionManager.attemptRePair() on a failed gateway request, not a pre-connect probe.
  2. It's the gateway-liveness half of the recovery work being handed to @noanflaherty.

Tracked as remaining scope on LUM-2232 (gateway-liveness probe / recovery driven off the first failed request). Leaving this thread open as the pointer to that follow-up rather than resolving it.

@vex-assistant-bot vex-assistant-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

APPROVE

Value: Closes the last rung the web/Electron local-connect path was missing relative to the native macOS bootstrap (forceReBootstrap → re-provision creds + restart). On a repairable connect failure, the renderer now spawns vellum wake <id> (non-destructive: re-seeds guardian token + restarts daemon+gateway, data/identity intact) and re-primes once. Adds one rung; carves out the gateway-liveness probe + recovery UI as LUM-2232 (handed to @noanflaherty) — clean scope discipline.

Layered seam (mirrors existing retire symmetrically):

  1. packages/local-mode/src/wake.tsrunWake CLI driver: spawn vellum wake <id> with the existing CliInvocation, never-reject {ok, status?, error?} contract. WAKE_TIMEOUT_MS = 180_000 (raised in 67f8955 after Codex's correctly-flagged P2 — see below).
  2. apps/macos/src/main/local-mode.tsvellum:localMode:wake IPC handler.
  3. apps/macos/src/preload/index.ts — typed wake channel on window.vellum.localMode.
  4. apps/web/src/runtime/local-mode-host.tswakeLocalAssistantHost: Electron branch reads window.vellum!.localMode.wake as optional (older shell → {ok: false, error: 'Wake is not supported by this app version'} → degrades to no-op repair); dev branch POST /assistant/__local/wake.
  5. apps/web/vite-plugin-local-mode.ts — loopback-guarded dev endpoint mirroring retire.
  6. apps/web/src/lib/local-mode.tsprimeLocalGatewayConnectionWithRepair: try-prime → if isRepairableConnectError (everything except GuardianTokenError.status === 403, which is a host-refused-loopback security boundary wake can't change) → resolve assistantId → wake → if repair.ok re-prime; otherwise throw the original error.
  7. apps/web/src/stores/auth-store.ts — ONLY the interactive connectLocalAssistant opts in. The boot probe stays on the plain primitive — deliberate so app launch never spawns daemon processes.
Codex P2 at HEAD "Verify gateway reachability before skipping wake" — mooted (out of scope, scope-carved to LUM-2232)

Codex's mechanic is correct: ensureGatewayToken() returns a cached gateway token (30-day TTL) without contacting the gateway, so a slept/stopped assistant with a still-cached token primes "successfully" and this path skips wake — the dead gateway only surfaces on the first data-plane request.

Devin's inline reply at HEAD covers exactly this:

  1. Native parity says: repair on failure, not preemptively. forceReBootstrap in clients/macos/.../AppDelegate+Bootstrap.swift fires from GatewayConnectionManager.attemptRePair() on a failed gateway request, not a pre-connect probe. Forcing a wake/probe on every connect would change the happy path and spawn daemon+gateway on every login even when nothing's wrong.
  2. Gateway-liveness is the other half of recovery work, explicitly carved out in the PR body and tracked as remaining scope on LUM-2232 (gateway-liveness probe + recovery driven off the first failed request, handed to @noanflaherty).

The finding is a follow-up pointer, not a regression. Devin left the thread open as the LUM-2232 hand-off marker rather than resolving it — correct ergonomics.

Earlier Codex findings — both auto-resolved by Devin
  • First commit P2 "Update local-mode mock for new repair call" → Devin self-fixed in 6442c1a9: auth-store.test.ts's @/lib/local-mode mock now exports both primeLocalGatewayConnection (boot probe) and primeLocalGatewayConnectionWithRepair (interactive connect). Cleared inline.
  • Second commit P2 "Allow wake to outlive CLI startup waits" → Devin self-fixed in 67f8955d. Original WAKE_TIMEOUT_MS = 60_000 was below the CLI's documented readiness windows (60s daemon wait + optional 60s source-daemon fallback + 30s gateway wait ≈ 90s prod, ≥150s on dev source-fallback). A cold-start wake could SIGTERM at 60s while still succeeding, misreporting as timeout. Raised to 180s with a docstring citing the CLI readiness windows. retire's 60s correctly unchanged — teardown is a different envelope.
Anti-pattern cross-check
  • No as runtime-boundary casts in the diff. ✓
  • Version skew handled. macOS shell + web bundle don't release together, so a newer renderer can run against an older preload that predates the wake channel. The channel is typed optional on window.vellum.localMode.wake and the host seam returns {ok: false, error: 'Wake is not supported by this app version'} on undefined. Caller treats as no-op repair → falls through to existing connect-error card. Same shape the rest of the local-mode-host seam uses. ✓
  • Non-destructive repair. Wake re-seeds creds + restarts; data + identity survive (matches native). The retire path stays separate for destructive removal. ✓
  • Opt-in. Only interactive connectLocalAssistant. Boot probe (best-effort) stays on plain primeLocalGatewayConnection so app launch never spawns daemon. ✓
  • 403 classification. GuardianTokenError.status === 403 = host-refused-loopback security decision wake can't change → surfaces unchanged. All other failures (missing/expired/malformed token, unreachable/stopped gateway) → repairable. Correct boundary. ✓
  • Symmetric with retire at every layer — same {ok, error?} contract, same runRetire-style never-reject CLI driver, same dev-endpoint shape. New code reads exactly like the existing pattern. ✓
  • Test coverage. 3 new test files: wake.test.ts (spawn argv + non-zero exit + spawn failure), local-mode-host.test.ts (dev POST + Electron bridge + older-shell unsupported), local-mode-repair.test.ts (clean→no-wake, repairable→wake-then-retry-success, still-failing-retry → original error, failed-wake → original error, 403 → immediate surface, no wake). ✓

Territory check (R11e): Self-hosted local-connect arc — Boss-owned (Devin-authored on Boss's behalf, follow-up to #33241 and #33252 that already merged this session). auth-store.ts is touched but the line range is the connectLocalAssistant action only; #33219 (tri-state platform-session liveness, also Devin/Boss-authored, still open) is a separate concern in the same store. Same arc, same author lineage — no external collision. Not Vargas's SSE/seq territory. Not Mahmoud's vembda territory. ✓

Merge gate at HEAD 67f8955d: Vex ✓ · Codex P2 at HEAD mooted (out-of-scope, scope-carved to LUM-2232, Devin inline rebuttal at HEAD with native-parity citation) · Codex's earlier P2s both self-fixed by Devin and cleared ✓ · CI all green ✓. Devin is last pusher → bot-merge may be blocked by branch protection; will attempt and flag if blocked.

Vellum Constitution — Distinct: this is exactly the kind of "add one rung at a time" PR that keeps a multi-cycle recovery arc legible. Carving out the gateway-liveness probe as LUM-2232 instead of bundling it preserves both the diff's reviewability and the next reviewer's leverage on the harder half (when to force a repair vs trust a cached token) — the right surface area for the right decision.

@vex-assistant-bot vex-assistant-bot Bot merged commit 205d011 into main Jun 3, 2026
25 checks passed
@vex-assistant-bot vex-assistant-bot Bot deleted the devin/1780504985-local-recovery-affordances branch June 3, 2026 17:52
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