# Ticket 10.4 — In-App Auto-Update Execution

**Phase**: 10 — Feature Parity Enhancements
**Priority**: Medium
**Depends on**: None

## Description

Extend the existing version check system (ticket 3.4) to support one-click in-app updates. Currently, conduit checks npm for new versions and shows an update banner, but the user must manually run `npm install` or `npx`. claude-relay implements a full "Update now" flow: clicking the button triggers server-side `npx --yes --package=conduit@latest`, spawns a new daemon with updated code, and the old daemon exits gracefully with a special exit code (120) for update handoff.

The relay should:

- Add an `update_now` WebSocket message type
- Server-side: run `npx --yes --package=conduit@latest` (or equivalent install command)
- Spawn a new daemon process with the updated code
- Old daemon exits with code 120 (update handoff)
- CLI daemon watcher detects code 120 and does NOT count it as a crash
- Port retry logic: new daemon retries binding for up to 15 seconds while old daemon releases port
- UI: update banner shows progress ("Updating...", "Restarting...", "Updated to vX.Y.Z")
- Client auto-reconnects to the new daemon

## Acceptance Criteria

### AC1: Update button sends WebSocket message
```gherkin
Given the update banner is visible showing a newer version is available
When the user clicks the "Update now" button
Then an `update_now` WebSocket message is sent to the server
  And the button changes to a disabled "Updating..." state
```

### AC2: Server executes npm install for latest version
```gherkin
Given the server receives an `update_now` WebSocket message
When the update handler runs
Then it executes `npx --yes --package=conduit@latest` (or equivalent)
  And a progress event is broadcast to all connected clients: { status: "installing" }
```

### AC3: New daemon spawns and old daemon exits cleanly
```gherkin
Given the npm install has completed successfully
When the server initiates the update handoff
Then a new daemon process is spawned with the updated code
  And the old daemon broadcasts { status: "restarting" } to all clients
  And the old daemon exits with code 120
```

### AC4: Exit code 120 is treated as intentional handoff
```gherkin
Given the CLI daemon watcher is monitoring the daemon process
When the daemon exits with code 120
Then the watcher does NOT increment the crash counter
  And the watcher does NOT log a crash warning
  And the watcher allows the new daemon to take over
```

### AC5: New daemon retries port binding on EADDRINUSE
```gherkin
Given the old daemon has not yet released the port
When the new daemon attempts to bind to the same port
Then it retries up to 15 times with 1-second intervals
  And if the port becomes available within 15 seconds, the daemon starts successfully
  And if the port is still in use after 15 attempts, the daemon exits with an error
```

### AC6: Browser clients auto-reconnect to new daemon
```gherkin
Given the old daemon has exited and the new daemon is running
When the browser's WebSocket connection drops
Then the client's reconnection logic detects the disconnection
  And it reconnects to the new daemon within the standard retry interval
  And the UI resumes normal operation without manual page reload
```

### AC7: Update banner shows progress throughout the flow
```gherkin
Given the user has clicked "Update now"
When the update progresses through stages
Then the banner shows "Updating..." during npm install
  And the banner shows "Restarting..." during daemon handoff
  And after reconnection, the banner shows "Updated to vX.Y.Z" with the new version
  And the success banner auto-dismisses after 10 seconds
```

## Testable Components

| Component | Test Type | What to Assert |
|-----------|-----------|----------------|
| `update_now` WS message handler | Unit | Receiving `{ type: "update_now" }` triggers the update execution function; invalid message types are ignored |
| Update execution logic | Unit | Calls `child_process.exec` with the correct npx command; broadcasts progress events ("installing", "restarting"); handles exec errors gracefully with an error broadcast |
| Exit code 120 handling in daemon watcher | Unit | Daemon watcher's `onExit` handler recognises code 120 as a non-crash; crash counter remains at 0; no restart delay is applied |
| Port retry on EADDRINUSE | Unit | When `server.listen` throws EADDRINUSE, the retry loop waits 1 second and retries; succeeds on the Nth retry when the port becomes free; gives up after 15 failures with a clear error message |
| Banner progress states | E2E | After clicking "Update now", the banner text transitions through "Updating..." → "Restarting..." → "Updated to vX.Y.Z"; the button is disabled during the update |
| Concurrent update prevention | Unit | If an update is already in progress, subsequent `update_now` messages are rejected with a "update already in progress" response |

## Reference

- claude-relay updater: `../claude-relay/lib/updater.js` (npm version check, update execution, hot restart)
- Existing version check: `conduit/src/lib/version-check.ts`
- Existing daemon watcher: `conduit/src/lib/cli-watcher.ts`
- Existing update banner: `conduit/src/lib/public/components/overlays/Banners.svelte`
