---
id: 12170
title: 'api Store autoLoad never loads: default Pipeline shadows api branch'
state: CLOSED
labels:
  - bug
  - ai
  - regression
  - core
assignees:
  - neo-opus-ada
createdAt: '2026-05-29T11:52:59Z'
updatedAt: '2026-05-29T12:22:34Z'
githubUrl: 'https://github.com/neomjs/neo/issues/12170'
author: neo-opus-ada
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-29T12:22:34Z'
---
# api Store autoLoad never loads: default Pipeline shadows api branch

## Context

PR #12134 fixed an autoLoad boot race for api-backed stores by making `Store.load()` await `Neo.remotes.Api.ready()` before giving up on a missing service stub. While dogfooding that fix in a real remotes-api app (an api-backed `Store` with `autoLoad: true` over a websocket), the store still never loaded: service stub registered, ws connected, **no `Api is not defined` error, and zero RPC frames on the wire**. The root cause is *upstream* of #12134's fix — and it makes that fix unreachable for the exact case it targets.

## The Problem

`Store.load()` branches on data source in this order:

```js
if (me.pipeline) { /* connection→parser→normalizer */ }
else if (me.api) { /* remotes-api RPC — the #12134 fix lives here */ }
else { /* url */ }
```

The `else if (me.api)` branch is **dead code for every store**, because every store always has a `pipeline` instance. `beforeSetPipeline()` only skips the default-instance path for the url case (`!value && me.url && !me.api`); for an api store (no `url`) it falls through to:

```js
return ClassSystemUtil.beforeSetInstance(value, Pipeline, {store: me});
```

and `ClassSystemUtil.beforeSetInstance()` mints a default instance for a falsy value:

```js
if (!config && DefaultClass) { config = Neo.create(DefaultClass, defaultValues) }
```

So `pipeline_: null` → a real `Pipeline` → `if (me.pipeline)` always wins → the api RPC path never executes → `autoLoad` produces no request. Manual `service.read()` calls work because they bypass `Store.load()` entirely, which is why this stayed latent until an api-backed store relied on `autoLoad`.

## The Architectural Reality

- `src/data/Store.mjs` — `beforeSetPipeline()` (~L440), `load()` branch order (`if (me.pipeline)` ~L911 vs `else if (me.api)` ~L993), `pipeline_: null` config (~L158).
- `src/util/ClassSystem.mjs` — `beforeSetInstance()` default-instance creation (`if (!config && DefaultClass)`).
- Pipeline (connection→parser→normalizer) and remotes-api (RPC stubs via `Neo.currentWorker.promiseMessage`) are **two parallel, not-yet-combined data-loading universes**. The only bridge today is `Neo.remotes.Api.register()` minting a per-method `Pipeline`+`Rpc` for methods that declare `parser/normalizer/pipeline`. Store-level api loading does not flow through a store `pipeline`.

## The Fix

Stop minting a default `Pipeline` for api-configured stores in `beforeSetPipeline()`, so `me.pipeline` stays `null` and `load()` reaches the `else if (me.api)` branch (which already carries the #12134 fix):

```js
beforeSetPipeline(value, oldValue) {
    let me = this;

    if (me.api) return null;   // api stores load via remotes-api, never a default pipeline

    oldValue?.destroy();

    if (!value && me.url) { /* … url pipeline … */ }

    return ClassSystemUtil.beforeSetInstance(value, Pipeline, {store: me});
}
```

Order-independent by design: Neo's reactive config getter resolves `me.api` on demand from `configSymbol` (`Neo.createConfig` get-path; `initConfig` populates the full merged config into `configSymbol` before `processConfigs`), so the guard reads the correct `api` value regardless of which reactive config is processed first.

### Variant considered (operator-flagged trade-off)

`if (me.api) return null` enforces a strict **api XOR pipeline** invariant: a store that already has an `api` cannot be switched to a `pipeline` at runtime without nulling `api` first. The alternative `if (!value && me.api) return null` would block only the auto-default (preserving runtime api→pipeline switching via an explicit pipeline value), at the cost of the clean XOR invariant. The runtime-switch case was weighed as ~0.001% likely; defaulting to the strict form.

## Contract Ledger

| Target Surface | Source of Authority | Proposed Behavior | Fallback | Docs | Evidence |
|---|---|---|---|---|---|
| `Neo.data.Store#pipeline` (config) | `Store.beforeSetPipeline()` `src/data/Store.mjs` | api-configured store no longer auto-mints a default `Pipeline`; `pipeline` stays `null` | strict: api store never gets a pipeline (runtime switch requires nulling `api` first) | JSDoc on `pipeline_` + `api_` | regression repro: api store `autoLoad:true` → zero RPC pre-fix |
| `Neo.data.Store#load()` (api branch ~L993) | `src/data/Store.mjs` | becomes reachable for api stores; fires the remotes-api RPC | n/a | — | #12134 fix goes live |

## Decision Record impact

`none` — no ADR governs the `data.Store` Pipeline architecture (the `learn/agentos/decisions/*` pipeline mentions are unrelated infra/graph ADRs).

## Acceptance Criteria

- [ ] `beforeSetPipeline()` returns `null` (no default `Pipeline`) when `me.api` is set.
- [ ] An api-configured `Store` with `autoLoad: true` has `pipeline === null` after construction.
- [ ] Such a store's `load()` reaches the `else if (me.api)` branch and fires the remotes-api RPC.
- [ ] url-based and explicit-pipeline stores are unaffected.
- [ ] Unit test covering an api + `autoLoad` store reaching the api branch (regression lock).

## Out of Scope

- Unifying the Pipeline and remotes-api universes (routing remotes through connection→parser→normalizer). Separate, larger refactor — see Related.
- Changing `load()` branch order.
- The `!value && me.api` permissive variant (documented above; not the chosen form).

## Avoided Traps

- **Flipping `load()` branch order** (`if (me.api)` before `if (me.pipeline)`): treats the symptom, leaves a spurious `Pipeline` + `onPipelinePush` listener on every api store, and entrenches "api wins" instead of "api stores have no pipeline." Rejected in favor of fixing the cause in `beforeSetPipeline()`.
- **Assuming a config-ordering hazard** when `beforeSetPipeline` reads `me.api`: Neo resolves reactive configs on demand from `configSymbol`, so cross-config reads inside hooks are order-independent. No ordering guard needed.

## Related

- #12132 / #12134 — autoLoad awaits remotes-api registration (the fix this unblocks).
- Follow-up (to file): unify Pipeline + remotes-api data-loading paths (epic-scale).

## Handoff Retrieval Hints

- Commit anchor: `be50f4f6d` (#12134) — the api branch this makes reachable.
- Semantic query: `"api store autoLoad pipeline shadows api branch beforeSetPipeline default instance"`

## Timeline

- 2026-05-29T11:53:01Z @neo-opus-ada added the `bug` label
- 2026-05-29T11:53:01Z @neo-opus-ada added the `ai` label
- 2026-05-29T11:53:01Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-05-29T11:53:01Z @neo-opus-ada added the `regression` label
- 2026-05-29T11:53:01Z @neo-opus-ada added the `core` label
- 2026-05-29T12:04:55Z @neo-opus-ada cross-referenced by PR #12171
- 2026-05-29T12:15:49Z @neo-opus-ada referenced in commit `da88d7c` - "fix(data): api stores no longer auto-create a default Pipeline (#12170)

beforeSetPipeline() returned a default Pipeline for api-configured stores (via ClassSystemUtil.beforeSetInstance), so Store.load()'s 'if (me.pipeline)' branch always shadowed the 'else if (me.api)' RPC branch — an autoLoad:true api store never fired its remotes-api request. Guard added: api stores keep pipeline=null and reach the api branch (which carries the #12132/#12134 fix). Adds StoreApiPipeline.spec.mjs regression coverage."
- 2026-05-29T12:22:34Z @tobiu referenced in commit `f7862b2` - "fix(data): api stores no longer auto-create a default Pipeline (#12170) (#12171)

beforeSetPipeline() returned a default Pipeline for api-configured stores (via ClassSystemUtil.beforeSetInstance), so Store.load()'s 'if (me.pipeline)' branch always shadowed the 'else if (me.api)' RPC branch — an autoLoad:true api store never fired its remotes-api request. Guard added: api stores keep pipeline=null and reach the api branch (which carries the #12132/#12134 fix). Adds StoreApiPipeline.spec.mjs regression coverage.

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-29T12:22:34Z @tobiu closed this issue
- 2026-05-29T15:50:27Z @neo-opus-ada cross-referenced by #12173
- 2026-05-29T16:36:47Z @neo-opus-ada cross-referenced by #12175

