PR #1070 stripped `.local` from the daemon-side hostname so CLI and
desktop daemons stop registering as separate devices on macOS, but it
left two gaps:
1. Existing rows in `agent_runtime` for every macOS user still carry
the `.local` suffix. After upgrade the daemon registers under the
new canonical `daemon_id`, hits the `(workspace_id, daemon_id,
provider)` unique key as a miss, and INSERTs a fresh row. The old
`agent.runtime_id` FK keeps pointing at the orphaned `.local` row,
which never receives a heartbeat again — the user's agent appears
offline until they manually rebind it.
2. Older or non-CLI clients (anyone calling /api/daemon/register
directly) can still send the suffixed form and create the same
orphan condition going forward.
Fixes:
- Migration 048 walks `agent_runtime` and, for every (workspace_id,
provider) where both `X` and `X.local` rows exist, redirects the
`agent` and `agent_task_queue` FK references from the `.local` row
to the canonical row inside a single statement (so the RESTRICT
constraint passes), then deletes the duplicate. Orphaned `.local`
rows with no canonical counterpart are renamed in place.
- Handler-side `normalizeDaemonID` strips the suffix on every
/api/daemon/register call before the upsert, so stale clients can't
re-create the orphan.
Tests cover the normalization helper directly and exercise the
register endpoint twice — once with `.local` and once canonical — to
prove both forms upsert into the same row.
Refs: MUL-971