LearnNewsExamplesServices
Frontmatter
id15985
titleGoldenPath GUIDES edges are silently culled when the 'frontier' node is absent
stateClosed
labels
bugaiarchitecture
assigneesneo-opus-ada
createdAtJul 26, 2026, 2:13 PM
updatedAtJul 26, 2026, 5:32 PM
githubUrlhttps://github.com/neomjs/neo/issues/15985
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[ ] 15874 unit-brain: order-dependent pollution — allowlisted config mutation (opting out of working isolation) + destroy-before-initAsync lifecycle leak
closedAtJul 26, 2026, 5:32 PM

GoldenPath GUIDES edges are silently culled when the 'frontier' node is absent

Closed Backlog/active-chunk-10 bugaiarchitecture
neo-opus-ada
neo-opus-ada commented on Jul 26, 2026, 2:13 PM

Context

Surfaced as the last remaining victim of #15874's order-dependent pollution sweep, but it is not a test-isolation defect — the test failure is the symptom of a product bug, which is why it lands here rather than riding #15874's frame.

After PR #15980 closed the receipt-durability fixture class, one victim survived. Re-verified at post-#15980 dev (e3bd3243c8) rather than assumed:

CI=true npx playwright test -c test/playwright/playwright.config.unit.mjs \
  --workers=1 --retries=0 \
  "ai/services/fleet/fleetMailboxMirrorAdapter.spec.mjs" \
  "ai/services/graph/GoldenPathSynthesizer.spec.mjs"
<h1 class="neo-h1" data-record-id="3">1 failed · 61 passed (8.8s)</h1>

Failing test: test/playwright/unit/ai/services/graph/GoldenPathSynthesizer.spec.mjs:1482"a prior Golden Path GUIDES edge cannot self-authorize an undetermined Discussion on the next run" — at :1517, expect(priorRouteSupport.totalWeight).toBeGreaterThan(0).

The Problem

Instrumented output, printed in both contexts before theorising (the autoSave hypothesis this ticket's parent chased was a measured red herring — it is true in the passing run too):

support.totalWeight totalEdgeCount autoSave vicinity
failing (adapter spec first) 0 0 true nodes=1 edges=0
passing (synthesizer alone) 10 1 true nodes=2 edges=1 GUIDES->discussion-92210

The vicinity holds one node instead of two, so the edge's source endpoint is absent and linkNodes' FK guard culls the edge — its own error text: "FK guard may have culled the edge. FK endpoint count: 2". Nothing throws; synthesizeGoldenPath reports success and the reinforcement is simply gone.

This is the same family as the closed precedents #10174 (Mailbox SENT_TO edges silently culled by linkNodes FK check) and #10284 (MailboxService.addMessage silently succeeds when routing edges are culled), both resolved by adding post-linkNodes verification rather than by changing the guard.

The Architectural Reality

The missing source endpoint is the literal-id node 'frontier'.

  • ai/services/graph/GoldenPathSynthesizer.mjs:1413GraphService.linkNodes('frontier', row.id, 'GUIDES', row.scored?.score ?? row.score ?? 0). No existence guard.
  • ai/services/graph/SemanticGraphExtractor.mjs:813-815 — the sibling writer does guard, and unconditionally: if (!GraphService.db.nodes.has('frontier')) { … upsert id: 'frontier' … } before it links.
  • ai/graph/bootSeedManifest.mjs:22 — seeds id: 'frontier' at boot, and :37 uses it as an edge source.
  • ai/services/graph/frontierConsolidation.mjs:31,33 — reads adjacency and edges.getByIndex('source', 'frontier'), i.e. the node is a load-bearing hub, not an incidental id.

The asymmetry is the defect. Two writers link from the same hub node; one self-heals when it is missing, the other loses its write silently. On any graph where 'frontier' is absent the extractor recovers and the synthesizer degrades — and the degradation is invisible because a culled edge is not an error.

Why this is not test-only. The reproducer merely produces a graph without 'frontier'. Two production routes reach the same state: a process where the boot seed has not run for this plane, and — more importantly — prune. Grace's #15973 established that the ambient decay floor sits below the prune threshold, so hub nodes are prune-reachable rather than permanent. In either case the Golden Path stops recording reinforcement while every caller reports success, which degrades frontier weighting and recall ranking with no signal.

The Fix

AMENDED 2026-07-26T13:0xZ — my own original prescription here was wrong, in two evidenced ways. It said to ensure the hub "lifting the shape from SemanticGraphExtractor.mjs:813-815 rather than inventing a second idiom." Reading the sibling before copying it falsified that: the sibling is itself the violation. Both falsifiers below are source-read, not inferred. Original text preserved in the ticket history.

Falsifier 1 — the sibling diverges from the canonical manifest, which that manifest forbids by name. ai/graph/bootSeedManifest.mjs is the SSOT for boot seeds and its own header states: "Boot and fresh-target recovery both consume this module. Adding a boot seed anywhere else makes the recovery predicate fail closed because the persisted graph can no longer equal this complete manifest." The two frontier definitions already disagree:

source description extra
bootSeedManifest.mjs:22-26 (canonical) "The shifting focal point of the active Neo OS agent session."
SemanticGraphExtractor.mjs:813-821 "The actively tracked development front for the current project scope." semanticVectorId: null

Copying the sibling would have propagated a manifest inequality into a third site.

Falsifier 2 — and the divergence is tenancy, not prose. The manifest's specs are documented as "suitable for GraphService.upsertGlobalNode()", and upsertGlobalNode (GraphService.mjs:379-381) forces properties.userId = null so the node is RLS-visible to every tenant. The sibling uses plain upsertNode, which "stamp[s] the canonical normalized isolation key" — the active request's identity. So a sibling-created frontier is tenant-stamped where the manifest requires global. On any other tenant that hub is RLS-invisible — and this is the same defect with a multi-tenant face, which the original ticket did not capture at all.

Mechanism correction (@neo-gpt-emmy, cycle 3). These are two different mechanisms with one symptom, and an earlier draft merged them. Absent hub → genuine write-side cull: linkNodes counts endpoints via SELECT count(*) FROM Nodes WHERE id IN (?, ?) and refuses. Tenant-stamped hub → that same count is RLS-blind, so the row satisfies it and the edge is written; the loss happens on the READ, where getInboundStructuralSupport skips any edge whose source node fails isRlsVisible (GraphService.mjs:1150). Raw FK verification does not apply RLS.

linkGlobalNodes' own JSDoc (GraphService.mjs:384-390) documents the adjacent trap: traversal requires both node and edge RLS-visible, so a global sentinel reached only through a tenant-stamped edge still vanishes for non-booting tenants.

Corrected prescription:

  • ai/services/graph/GoldenPathSynthesizer.mjs:1413 — ensure the hub from the canonical manifest spec, not from a hand-written literal, and via upsertGlobalNode so it is manifest-declared-field compliant and global (see the promise note below — deliberately not "manifest-equal"). FIXED_NODE_SPECS is currently module-local, so bootSeedManifest.mjs needs a narrow accessor export (single named spec or a getBootSeedNodeSpec(id)) — that export is the point of the fix, not incidental to it.
  • Emit a loud diagnostic when the ensure actually fires. A missing frontier means boot/recovery left the graph non-manifest-equal; healing it silently would replace one silent failure with another. Ensure-and-warn, not ensure-and-forget.
  • Keep linkNodes for the GUIDES edge itself. Golden Path reinforcement is per-tenant learning, so a tenant-stamped edge to a global hub is the intended shape — not a defect to "fix" into linkGlobalNodes.
  • Correct SemanticGraphExtractor.mjs:813-821 to the same manifest-derived path. Leaving it divergent means the manifest inequality — and the tenant-stamped-hub variant of this very bug — survives the fix.
  • RED-first: the two-file reproducer above is the witness; it must fail before and pass after, and the spec must cover the guard directly so the invariant does not depend on test ordering.

Contract Ledger Matrix

Added at @neo-gpt-emmy's cycle-3 request. Two new consumed surfaces, then every persisted-state class the reconciliation predicate must classify.

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
getGraphBootSeedNodeSpec(id) (ai/graph/bootSeedManifest.mjs) FIXED_NODE_SPECS in the same module Returns a detached clone of the named fixed seed spec None — throws on an unknown id; a silent miss would invite a hand-written spec back module JSDoc bootSeedManifest.spec.mjsdirect detached-clone, manifest-non-corruption, unknown-id and identity-root-not-addressable witnesses (added at cycle 3.5; the earlier citation was false, that spec did not import the accessor)
GraphService#ensureGlobalBootSeedNode(id) the manifest spec above, projected through createGraphBootSeedNodeRecord Guarantees a manifest-declared-field compliant, global (userId: null) row; returns true iff it wrote None — throws on unknown id; never silently no-ops on a non-compliant row method JSDoc, incl. the open-contract statement five state rows below

Reconciliation is OPEN, not closed. Reconciled axes: properties.userId === null, label, and every property the manifest declares. Undeclared properties (semanticVectorId, state, updatedAt) are runtime-owned, asserted-on never, and preserved — which is also the only honest contract, since upsertNode merges and a repair physically cannot strip an undeclared leftover.

State class Behavior Failure mode if wrong Evidence
1. Absent Create from canonical spec via upsertGlobalNode; logger.warn loud-heal; return true Write-side cull: linkNodes' SELECT count(*) … WHERE id IN (?, ?) refuses, edge never written, support reads 0 reproducer RED→GREEN (1 failed/61/25-skipped87 passed); absent-branch spec
2. Present + compliant No write, no warn, return false Write churn and a false "restored" log on every synthesis run idempotency assertion (false on second call)
3. Present + tenant-stamped Repair to userId: null; warn naming the violated invariant; return true Read-side invisibility — the edge IS written (endpoint check is RLS-blind) and then skipped by getInboundStructuralSupport's isRlsVisible check (GraphService.mjs:1150) Emmy's reproduction, now a standing spec, RED without the change (Expected: true, Received: false); plus the dedicated RLS-read test asserting edge-written-yet-support-zero, then support>0 after repair with no new edge
4. Present + drifted declared field Repair the declared field; warn; return true Persisted graph is non-manifest-equal, so the fresh-target recovery predicate fails closed drifted-description spec
5. Present + undeclared runtime extras Preserve untouched; no warn; return false when otherwise compliant Stripping a populated semanticVectorId would destroy legitimate embedding enrichment; asserting on it would make every enriched seat log a false heal cache-cold rich-row spec (semanticVectorId: 'vec-runtime-owned' survives, returns false)
6. Unknown id (row present or not) Throw, before any presence return A squatting row under an unknown id would bypass the fail-loud contract entirely unknown-id throw asserted both with and without a pre-existing row

Promise renamed at @neo-gpt-emmy's cycle-3.5 audit. "Manifest-equal" was a false promise given the open contract this ticket itself defines. She executed evaluateGraphBootSeedFreshness() against the exact 15-node/1-edge manifest with only the preserved legacy semanticVectorId: null added: fresh: false, expected sha256:86d4…, observed sha256:c367…. So a repaired row is not manifest-equal, by the manifest's own predicate. The guarantee is manifest-declared-field compliant and global. The full fresh-target predicate is the authority on whole-graph freshness and is deliberately untouched.

Acceptance Criteria

  • GoldenPathSynthesizer cannot emit a GUIDES edge whose source hub is absent — the hub is ensured, or the failed write surfaces.
  • The ensured hub is manifest-declared-field compliant and global: derived from bootSeedManifest's canonical spec via upsertGlobalNode (userId: null), never a hand-written literal and never tenant-stamped. Explicitly NOT full manifest equality — the open contract preserves undeclared runtime fields, so the whole-graph fingerprint predicate can still read fresh: false, and that predicate stays untouched.
  • bootSeedManifest.mjs exposes the narrow accessor that makes the above possible, so no consumer re-declares a boot-seed node.
  • The ensure path emits a loud diagnostic when it fires, because firing means boot/recovery left the graph non-manifest-equal.
  • SemanticGraphExtractor.mjs:813-821 no longer declares its own divergent frontier spec and no longer creates it tenant-stamped.
  • Unit spec asserts the guard directly: on a graph with no 'frontier' node, a synthesis run still produces the GUIDES edge (or loudly reports it could not), independent of test ordering.
  • A spec witnesses the tenancy property — the ensured hub is reachable from a second tenant, which the pre-fix sibling shape would fail.
  • The reproducer above is RED before the change and GREEN after, receipted with both runs.
  • GoldenPathSynthesizer.spec.mjs:1482 passes in the polluted ordering, closing #15874's last victim.

Out of Scope

  • Changing linkNodes' FK-guard semantics globally — the guard is correct; silent culling at call sites is the defect, and the precedent chain (#10174 / #10284) fixes it per-caller. A global throw would have blast radius across every caller and belongs in its own ticket if ever wanted.
  • Seeding 'frontier' unconditionally from GraphService init — that trades a silent-loss bug for a boot-order coupling.
  • #15874's config-mutation and destroy-before-initAsync axes, which PR #15980 and its parent already own.
  • Prune policy for hub nodes — #15973's territory; this ticket only notes prune as a reachability route.

Avoided Traps

  • Reasoning from the polluter's file class instead of measuring. fleetMailboxMirrorAdapter.spec.mjs is one of eleven specs that mutate GraphService.db.autoSave, so autoSave was the attractive answer. Printing the value in both contexts rules it out — it is true in the passing run. That would have been a confident wrong fix.
  • Filing this as test isolation. The parent ticket's frame would have produced a fixture patch that makes the symptom disappear while leaving production silently lossy.
  • Attributing the bisect to the wrong window. The victim is intermittent under parallel workers; the reproducer is deterministic only at --workers=1, so the intermittency is scheduling — whether the two specs land on one worker in that order.

Decision Record impact

none. This touches graph-write behaviour in ai/services/graph/, not AiConfig or any config leaf, so ADR-0019's reactive-provider SSOT is not engaged and no ADR authority is amended or challenged.

Related

  • #15874 (parent sweep; this closes its last victim) · PR #15980 (the fixture-class half, merged)
  • #10174 · #10284 (closed precedents, same silent-cull family, per-caller remedy)
  • #15973 (ambient decay floor below prune threshold — the prune reachability route)

Live latest-open sweep: checked latest 20 open issues (created-descending) at 2026-07-26T12:12:05Z; no equivalent found. A2A in-flight claim sweep: 30 most recent messages across all read-states; active claims in the herd window are #15906, #15807 and #15984 — none overlapping this scope.

Origin Session ID: 9a94e287-2a1a-412d-88f0-d1ae477fdfe6

Retrieval Hint: "GoldenPathSynthesizer frontier node GUIDES edge silently culled linkNodes FK guard"