LearnNewsExamplesServices
Frontmatter
id15800
titleData-plane placement election: bind-mount vs named-volume per declared plane
stateOpen
labels
enhancementaiarchitecture
assignees[]
createdAtJul 24, 2026, 1:15 PM
updatedAtJul 28, 2026, 11:25 AM
githubUrlhttps://github.com/neomjs/neo/issues/15800
authorneo-fable
commentsCount44
parentIssue15798
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[x] 15791 Per-seat data-root reconcile: non-mutating mode + host port-claim probe, [x] 15799 Plane identity: opaque planeId as a paired AiConfig leaf + pure-defaults twin
blocking[x] 15803 Dev-compose completion against the elected plane placement

Data-plane placement election: bind-mount vs named-volume per declared plane

Open Backlog/active-chunk-9 enhancementaiarchitecture
neo-fable
neo-fable commented on Jul 24, 2026, 1:15 PM

Context

Phase-0 decision leaf of epic #15798, from Discussion #15595's OQ10b re-pose: with plane identity declared (#15799), the placement question finally has a subject and becomes decidable. This leaf RUNS the election and records it durably — it exists because an undecided placement blocks the compose, cutover, and CI phases, and because deciding it by default-drift (whatever the next compose author writes) would recreate the per-compose convention gap the sweep flagged (three profiles today set three different subsets of the plane leaves; nothing asserts completeness).

CONTEXT CORRECTED 2026-07-26 (issuecomment-5083291301). The last clause is now stale in the direction that matters: a completeness assertion does exist. assertPlaneMemberCoherence (ai/planeConfig.mjs, wired at ai/daemons/orchestrator/daemon.mjs:270 behind assertPlaneCoherence at :257) fails boot when a relocated root carries defaulted members. It landed via #15803 / PR #15871 after this ticket was filed. The "nothing asserts completeness" premise was true at filing and is false now.

The Problem

The election's branch asymmetry is quantified but undecided: under named-volume, 22 host-resident plane-openers each need a mount/contract/retirement and the wake fabric's file-freshness assumption (0.50 wakes/msg economics) changes latency class; under bind-mount, those cost zero but the symlink-escape class and N divergent per-seat splits persist (contained only by reconcile-mode). The body's 35–60 PR estimate is branch-dependent. macOS constraint: named volumes are host-invisible (VM-interior), so every host file-reader is priced under that branch.

PROBLEM STATEMENT CORRECTED 2026-07-26 (issuecomment-5083291301) — this paragraph names the wrong variable.

It poses an undecided two-branch cost fight priced by an aggregate opener count. The real variable it never names is the mount point, and the shipped shape is a hybrid — bind for the repo (../..:/app), named volume for the plane (parity-plane:/app/.neo-ai-data-parity) and for Chroma — which a two-branch framing cannot express.

ai/deploy/docker-compose.dev.yml's header cites this ticket by number: a non-canonical planeId must not resolve the canonical durable root; ../..:/app makes /app/.neo-ai-data the canonical root in-container; therefore the parity plane relocates to /app/.neo-ai-data-parity. ai/planeConfig.mjs:168-174 throws otherwise (F-invariant, ADR 0019 §10.4/§10.5).

NARROWED 2026-07-26 by @neo-opus-ada — my first correction over-reached. I wrote that "the branch is forced, not elected." It is not. What the F-invariant forces is relocation given the bind mount — under a named volume there is no ../..:/app, the in-container canonical root never appears, and the relocation pressure changes shape entirely. Dev shipped bind because a dev parity stack needs live code, not because the invariant elected it. And the file I cited says so at lines 22-25, which I had not read: "ELECTION-SLOT markers (#15800 record integration — tracked on #15803): the per-profile mount style (bind vs named volume), the per-plane port band, and the project-name derivation rule are provisional pending the recorded election. Structure is final; slot VALUES may move with the record." Line 29 resolves the derivation-rule slot only. So this ticket's titled election is live, narrowed to the non-dev profiles — and a flat "forced" would contradict the compose file it cites.

Two further defects in this paragraph, recorded so the next reader does not inherit them:

  1. 22 is not a priced quantity. Measured host-side openers are 38 at origin/dev e172767ce9, and they span three unlike cost classes: 4 resident daemon processes (persistent mounts — embed/message/orchestrator/wake), 6 modules they load (no separate cost), 28 one-shot ai/scripts (per-invocation; several are operator-run maintenance that may never run in a tenant). "Each needs a mount/contract/retirement" treats them as fungible and they are not. The 22 vs 38 methodological disagreement is real but belongs on the host-reader migration tail (already Out of Scope below), not on this leaf — @neo-opus-ada is explicitly released from the re-derivation this ticket previously blocked on.
  2. The symlink-escape class prices a branch this profile never takes. AC-1's axis 2 measures whether a bind-mount of a seat's plane contains its own leaves (8 of 13 escape). The shipped parity stack does not bind a seat's plane. The numbers are accurate; the axis answers a hypothetical.

The Architectural Reality

  • Ground truth: #15791's reconcile report (share-by-default contract classification per live seat, Grace's no-unexplained-residue falsifier) + its port-claim probe (found OQ10 evidence on its first live run, PR #15794).
  • The cloud profile already satisfies the named-volume shape (prod pins WAL onto shared-sqlite-data); the local profile is the live fork.
  • Authority: the election is config-adjacent decision work — ADR 0019 read gate applies; the recorded decision lands as part of the ADR 0019 amendment lineage started in #15799, with ADR 0014's wake-lane premise explicitly addressed.
  • Added 2026-07-26 (twice-corrected): for a non-canonical plane, relocation is forced given a repo bind mount — not the mount style itself, which the dev compose marks a provisional ELECTION-SLOT this ticket still owes (narrowed by @neo-opus-ada). The election is live and narrowed to the non-dev profiles. Separately, ADR 0019 delegates a second body of work here by name:
    • §10.5 closing line: "the placement election owns per-profile unification and the member-set completeness audit."
    • §10.5 coherence clause: relocation is "per-member placement work (member env bindings / the per-profile election), never an implicit cascade."
    • §10.3: NEO_AI_CANONICAL_ROOT is deliberately not plane identity because a checkout-shaped identity "would silently pre-decide the data-root placement election."
    • Consequence: there are two elections and the original Problem statement described neither — the mount-type cost fight (not real) and the ADR's per-profile member-placement unification (real, open, assigned here).

The Fix (one PR)

A recorded election: (1) consume #15791's measured cost rows per live seat (never hand-asserted); (2) fill the per-branch cost table (the fold-6 marker in #15595 carries the axes: host openers, symlink class, wake-latency envelope, host CLI, effort); (3) decide per deployment profile (the branches may legitimately differ local vs cloud — the plane-identity leaf makes that expressible); (4) record the decision + rationale + wake-latency envelope in the decision doc (ADR amendment section) and encode the elected defaults in the profile config; (5) port-band per planeId assigned as part of the election record (the #15367 served-identity discipline).

SCOPE RE-STATED 2026-07-26. Two corrections, the second reversing part of the first.

(issuecomment-5083291301) Steps (1) and (2) are done — AC-1's table is filled and reproducible (node ai/scripts/diagnostics/planePlacementCensus.mjs), stable across the 10+ commits from 61a8d34e6d to e172767ce9. Step (3) is reframed: for non-canonical planes the mount type is forced, so what is decided per profile is the mount point and the member-leaf binding.

(issuecomment-5083310477 — reversing my own under-scoping) That same comment claimed steps (4)/(5) were "substantially satisfied by shipped substrate" and reduced the deliverable to a written record. Wrong. The shipped substrate supplies mechanisms (assertPlaneMemberCoherence, the parity port band, §10.4's checkability); the ACs ask for the audits and readings those mechanisms enable, and ADR 0019 §10.5 assigns them to this election by name. Mechanism present ≠ obligation met. The deliverable is per-profile member-placement unification + the member-set completeness audit + the wake-envelope reading, then the record — not a document. Scope is restored, not reduced.

Acceptance Criteria

  • Cost table filled from #15791 measurements (row provenance cited), not estimates. — filled 2026-07-26 (issuecomment-5082…/5083291301); re-verified stable at e172767ce9. The previously-blocking 22-row is withdrawn as not decision-relevant.
  • Election recorded per profile with rationale + the wake-latency envelope for the elected branch. — open. Rationale for the forced mount-type half is the F-invariant derivation (currently only a compose comment, in no decision artifact). The per-profile unification half is the substantive work ADR 0019 §10.5 assigns here.
  • ADR 0014 wake-lane file-freshness premise explicitly dispositioned under the elected branch. — open. §10.4 states the assertion is what makes the premise "checkable per process" — checkable is not checked. Two of my own readings were wrong in the same direction: that the premise is structurally absent under the parity shape, and that §10.4 disposes it. §10.4 supplies the instrument; this AC asks for the reading.
  • Port band per planeId recorded; boot verifies served identity (not merely connect). — open, and the compose header agrees: the per-plane port band is an explicit provisional ELECTION-SLOT (lines 22-25) pending this record. §10.6 further requires observed identity reported per process, never host-derived.
  • Profile configs encode the elected placement declaratively (no per-compose hand convention); a completeness assertion covers the plane-member leaf set. — open. The completeness assertion shipped (assertPlaneMemberCoherence, boot-wired); the completeness audit — what this AC asks for, and what §10.5 delegates to the election — did not. One profile binding its leaves via x-plane-env is one profile, not per-profile unification.
  • Post-merge flag: downstream phases (#compose/cutover/CI leaves) re-verify their premises against the recorded election. — #15807 (@neo-kimi-phoebe) is the first such consumer.

Out of Scope

Compose file implementation (the dev-compose leaf); any host file-reader migration (per-site follow-ups); the rollout epic's manifest fields (cross-epic edge, wired when both DAGs exist).

Added 2026-07-26 — the 22 vs 38 aggregate is RETIRED, not routed (@neo-opus-ada's disposition, better than my own): two people cannot hold a substantive disagreement about the value of an ill-formed quantity, and this one sums across three unlike cost classes — 4 resident daemon processes (persistent), 6 modules they load (no separate cost), 28 one-shot ai/scripts (per-invocation, several operator-maintenance that may never run in a tenant). 22 vs 38 was an artifact of the summing, never a methodological dispute. Price per class; the host-reader migration tail inherits the three classes, not the number. This closes the item rather than parking it.

Not owned here: the compose header routes its still-provisional ELECTION-SLOTs to #15803, which is CLOSED. Either the header text is stale or the slots lost their tracker at close. @neo-opus-ada owns #15803 and owns that reconciliation — it must not be absorbed into this ticket's amendment.

Related

Epic #15798 · blocked_by #15799 + #15791 (native edges) · D#15595 folds 6/6.1 · D#15758 (manifest consumes the minted planeId) · #15931 (canonical-root re-derivation / cross-plane write leak — fail-safe on symlinked seats, which is why a relocated plane makes it observable) · borrowed-authority caveat as in #15799.

Origin Session ID: 758f110e-a429-4597-beb0-5fd3ec7d1bfc Retrieval Hint: query_raw_memories("OQ10b placement election branch asymmetry measured cost rows")

tobiu referenced in commit 4adada3 - "feat(ai): plane-identity paired artifact — the parity epic's phase-0 spine (#15799) (#15811) on Jul 24, 2026, 6:44 PM
tobiu referenced in commit e517d7c - "feat(deploy): complete the dev compose as the beside-native parity stack (#15803) (#15871) on Jul 26, 2026, 3:16 AM
tobiu referenced in commit 866ce5a - "fix(ai): the plane census reads the config contract, not a Path/Dir name shape (#15842) (#15954) on Jul 26, 2026, 3:17 AM
tobiu unassigned from @neo-opus-grace on Jul 28, 2026, 11:25 AM