# The two boot paths have drifted

Last verified: 06-08-2026 @ 69d0f5e

V-639. Reads with `docs/operations.md`.

## What landed

`cmd/mavend/boot.go`. `newDaemonAPI(deps)` builds the CoreAPI with every field
set, and `startBackground(ctx, &wg, deps)` starts the voice server and every
worker through `goWorker`. `backgroundWorkers(deps)` is the pure list behind it,
so a test can compare the set without standing a daemon up. Both paths in
`run()` now read `coreAPI = newDaemonAPI(depsNow())` and one
`startBackground(...)`, where `depsNow` reads whatever the current path wired.

The shadowed `wg` is gone. Four tests in `cmd/mavend/boot_test.go`. Every
`daemonAPI` field is set on a fully wired deployment. The handler gets the API
it was built with. The worker set is asserted by name, at the full set and at
the floor.

Still by hand: unlock a locked box by passkey, ask something that needs Nexus,
and check `/tools` lists the MCP servers.

## What is wrong

`run()` in `cmd/mavend/main.go` brings the daemon up two ways. A box with a key in the
environment starts unlocked and wires everything at lines 280 to 621. A box without one
starts locked. It wires the same things again inside the unlock closure, at lines 500 to
579, after a passkey assertion.

The two lists have drifted apart. Three ways.

**Seven workers start untracked.** The unlocked path puts every one through
`goWorker(&wg, ...)`, so `waitWorkers` at line 637 can wait for them. The unlock path
starts `tl.run`, `factWorker`, `evalWorker`, `feedWkr`, `crawlWkr`, `mcp.run` and
`home.run` as bare `go func()`. Nothing waits for any of them.

That is the shutdown bug the code already documents at lines 631 to 636, reintroduced on
the other path. The comment there records what it cost the first time. `run()` never
returned, so `defer st.Close()` never sealed the database. The deployed ciphertext was
eleven days stale before anyone noticed.

**A shadowed WaitGroup hides it.** Line 529 declares `var wg sync.WaitGroup` inside the
`if voiceW != nil` block, shadowing the one from line 359. It is `Add`ed and `Done`d and
never waited. Reading the block, the voice server looks tracked. It is not.

**Two `daemonAPI` fields are never set.** The unlocked path fills `nexus` at line 295 and
`getMCPServers` at line 305. The unlock path fills neither. So after a passkey unlock,
`ResolveEntity` answers `ErrNotImplemented` with a `nexus` block configured, and
`MCPServers` answers empty with an `mcp` block configured.

The second is the worse one. Empty is not a degraded answer, it is a wrong answer, and
`/tools` renders it as "not configured".

## Why it drifted

`wireTelegramIntake` was added to both paths on 06-08-2026 (V-637) and it does use the
outer `wg`, at line 519. So the newest line on that path is correct and the older ones
around it are not. The path gets touched one line at a time and is never read whole.

The shape of `cmd/mavend` is what allows that. It is 155 files and 9,551 lines of code.
Six things live in it with no seam between them:

- the handler
- the action dispatch
- the 19 query sources
- the wiring functions
- the six background workers
- these two boot paths

Nothing in the package makes the divergence visible.

## The fix

Make the two paths call one function instead of listing the same wiring twice.

One `startBackground(ctx, &wg, deps)` that takes what it needs and starts every worker
through `goWorker`. One `newDaemonAPI(deps)` that fills every field, including `nexus` and
`getMCPServers`, so a field added later cannot reach one path and miss the other. Both
call sites then read as one call each, and a future addition has one place to go.

Delete the shadowed `wg` at line 529 as part of it.

## How it is judged

`make test` stays green.

The regression that matters is a test asserting the two paths wire the same set. Compare
the constructed `daemonAPI` field by field, and assert the worker count started under the
outer `wg` matches. Without that, this drifts again the next time a wiring line is added.

Then confirm on a locked box: unlock by passkey, ask something that needs Nexus, and check
`/tools` lists the MCP servers. Both answer wrongly today.

## Priority

Latent, not live. `deploy/mavend.json` sets `db_key_env`, so homesrv boots unlocked and
takes the correct path. This bites the locked deployment that `docs/operations.md`
describes, and it bites silently.
