Context
Found while implementing #16513 (turn-presence writes bypassing the deployment). That ticket's fix moves the harness hooks' write onto the Memory Core's MCP surface. Tracing what else touches AGENT_TURN_PRESENCE surfaced a second, larger participant that could not ride the same PR.
Live latest-open sweep at filing: #16495–#16519 window checked; #16488/#16495 own the SDK-boundary/module-scope-import class, #16514/#16515 own the ai/ duplication and census lanes. None covers the wake daemon's store binding.
The Problem
ai/daemons/wake/daemon.mjs opens the graph from a host config leaf and serves every read from it:
:2866 DB_PATH = memoryCoreConfig.storagePaths.graph;
:2896 db = initializeDatabase(DB_PATH);
Consumers of that one handle, measured:
| line |
read |
:554 |
getGraphLogEntries |
:579 |
getActiveShapeCSubscriptions |
:584-585 |
getNodesData / getEdgesData |
:638 |
getDbNode |
:742 |
DELIVERED_TO edge probe |
:842-852 |
isMessageReadFor (message read-state) |
:1994, :2006 |
findTurnPresenceAfter — the Codex delivery proof |
:2802 |
getActiveHarnessPresence |
In a containerized deployment that path is not the served store. healthcheck reports plane.dataRoot: /app/.neo-ai-data, and ai/deploy/docker-compose.yml:193 mounts shared-sqlite-data:/app/.neo-ai-data/sqlite where shared-sqlite-data is declared bare at :483 — a Docker-managed named volume, inside the Docker Desktop VM on macOS. The contrast in the same file is the control: :327 mounts backups as ${NEO_HOST_BACKUP_ROOT:-${HOME}/.neo-ai/backups}, a real host bind, which is why backups are host-reachable and the graph is not.
So there is nothing host-visible for storagePaths.graph to point at, and every read above answers from a diverged file — successfully, because a stale SQLite file serves reads correctly. That is the same failure shape PR #16401 ruled on from the subscription side: the contract was right and the in-repo precedent was wrong.
The Architectural Reality
- This is not a stray bypass. Unlike the hook writer in
#16513 — one call site that could be lifted cleanly — the daemon is a wholesale host-store reader. Migrating a single read would leave the process reading most things from one store and one thing from another, which is a worse state than the coherent-but-wrong one it is in now. The unit of repair is the process, not the call site.
- Currently dormant, and that is a sequencing fact rather than a reason to skip it.
daemon.mjs is not running on this deployment: launchctl shows com.neomjs.agent-os-wake bound to ai/daemons/wake/receiver.mjs (the manifest-driven route dispatcher), and healthcheck reports features.wake.daemonRunning: false. Wakes are delivered today by the receiver, which reads a routes manifest and never touches the graph.
#16513 leaves the correlation key ready. record_turn_presence now accepts and persists wakeSubmitNonce, so once this daemon reads the served store, findTurnPresenceAfter's nonce match works against beacons the hooks are already writing there.
The Latent Regression, stated plainly
With #16513 merged and this ticket open, starting daemon.mjs would degrade scheduleCodexTurnStartProof to wake-submit-unknown for every delivery: hooks write the served store, the daemon polls the host file, and the nonce never matches. Today those two agree — both are wrong together, which is why the proof "works".
This is latent, not live, because the process does not run. It is recorded here so that starting it is a decision made with the constraint visible, rather than a surprise.
The Fix
- Inventory the eight read sites and decide per-read whether it belongs on the MCP surface, on a served-store binding, or is genuinely host-local (the daemon's own state files are not in question).
- Move the process to the deployment's store as one change, so it never runs half-migrated.
ai/daemons/wake/readSubscriptionsOverMcp.mjs is the in-tree precedent for the subscription read; several other reads have no tool surface yet and may need one, which is the real sizing risk.
- Prove the delivery proof end-to-end: a wake submit followed by a nonce-correlated
wake-submit-started, with hook and daemon on different filesystems — the case a colocated setup cannot exercise and therefore the one that must be constructed deliberately.
Acceptance Criteria
Out of Scope
#16513 itself (the hook write path; merged or in flight).
- The deployment topology decision — two orchestrators across two data roots is an operator call, and this ticket makes the daemon correct under either outcome.
- The wake receiver, which is manifest-driven and does not read the graph.
Decision Record impact
none — applies an already-adopted mitigation to a process that predates it.
Related
#16513 (the writer half, same root cause), PR #16401 (the ruling this re-confirms), #16510.
Context
Found while implementing
#16513(turn-presence writes bypassing the deployment). That ticket's fix moves the harness hooks' write onto the Memory Core's MCP surface. Tracing what else touchesAGENT_TURN_PRESENCEsurfaced a second, larger participant that could not ride the same PR.Live latest-open sweep at filing:
#16495–#16519window checked;#16488/#16495own the SDK-boundary/module-scope-import class,#16514/#16515own theai/duplication and census lanes. None covers the wake daemon's store binding.The Problem
ai/daemons/wake/daemon.mjsopens the graph from a host config leaf and serves every read from it:Consumers of that one handle, measured:
:554getGraphLogEntries:579getActiveShapeCSubscriptions:584-585getNodesData/getEdgesData:638getDbNode:742DELIVERED_TOedge probe:842-852isMessageReadFor(message read-state):1994,:2006findTurnPresenceAfter— the Codex delivery proof:2802getActiveHarnessPresenceIn a containerized deployment that path is not the served store.
healthcheckreportsplane.dataRoot: /app/.neo-ai-data, andai/deploy/docker-compose.yml:193mountsshared-sqlite-data:/app/.neo-ai-data/sqlitewhereshared-sqlite-datais declared bare at:483— a Docker-managed named volume, inside the Docker Desktop VM on macOS. The contrast in the same file is the control::327mounts backups as${NEO_HOST_BACKUP_ROOT:-${HOME}/.neo-ai/backups}, a real host bind, which is why backups are host-reachable and the graph is not.So there is nothing host-visible for
storagePaths.graphto point at, and every read above answers from a diverged file — successfully, because a stale SQLite file serves reads correctly. That is the same failure shapePR #16401ruled on from the subscription side: the contract was right and the in-repo precedent was wrong.The Architectural Reality
#16513— one call site that could be lifted cleanly — the daemon is a wholesale host-store reader. Migrating a single read would leave the process reading most things from one store and one thing from another, which is a worse state than the coherent-but-wrong one it is in now. The unit of repair is the process, not the call site.daemon.mjsis not running on this deployment:launchctlshowscom.neomjs.agent-os-wakebound toai/daemons/wake/receiver.mjs(the manifest-driven route dispatcher), andhealthcheckreportsfeatures.wake.daemonRunning: false. Wakes are delivered today by the receiver, which reads a routes manifest and never touches the graph.#16513leaves the correlation key ready.record_turn_presencenow accepts and persistswakeSubmitNonce, so once this daemon reads the served store,findTurnPresenceAfter's nonce match works against beacons the hooks are already writing there.The Latent Regression, stated plainly
With
#16513merged and this ticket open, startingdaemon.mjswould degradescheduleCodexTurnStartProoftowake-submit-unknownfor every delivery: hooks write the served store, the daemon polls the host file, and the nonce never matches. Today those two agree — both are wrong together, which is why the proof "works".This is latent, not live, because the process does not run. It is recorded here so that starting it is a decision made with the constraint visible, rather than a surprise.
The Fix
ai/daemons/wake/readSubscriptionsOverMcp.mjsis the in-tree precedent for the subscription read; several other reads have no tool surface yet and may need one, which is the real sizing risk.wake-submit-started, with hook and daemon on different filesystems — the case a colocated setup cannot exercise and therefore the one that must be constructed deliberately.Acceptance Criteria
AGENT_TURN_PRESENCE, message, subscription, and presence read indaemon.mjsresolves against the store the deployment serves, or is documented as deliberately host-local with the reason.wake-submit-startedon a nonce-correlated interval when hook and daemon do not share a filesystem.Out of Scope
#16513itself (the hook write path; merged or in flight).Decision Record impact
none— applies an already-adopted mitigation to a process that predates it.Related
#16513(the writer half, same root cause),PR #16401(the ruling this re-confirms),#16510.