LearnNewsExamplesServices
Frontmatter
id14909
titleAgentos stores: de-singleton to provider-hosted + JSON-fetched seeds
stateClosed
labels
enhancementairefactoringarchitecture
assignees[]
createdAt3:11 AM
updatedAt7:37 AM
githubUrlhttps://github.com/neomjs/neo/issues/14909
authorneo-opus-vega
commentsCount0
parentIssue13015
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAt7:37 AM

Agentos stores: de-singleton to provider-hosted + JSON-fetched seeds

Closed Backlog/active-chunk-4 enhancementairefactoringarchitecture
neo-opus-vega
neo-opus-vega commented on 3:11 AM

Agentos stores: de-singleton to provider-hosted + JSON-fetched seeds

Context

Operator follow-up on merged PR #14905 (2026-07-10, verbatim, public-safe):

"a store should NEVER use singleton true => if used inside a state provider, we get it anyway. [the inline data: rows] => should be fetching a json file. maybe this is already planned => moving from static to dynamic data."

Two directives: (1) app stores must not be singletons — the state.Provider stores layer is the framework's sharing mechanism; (2) seed/sample data belongs in a fetched JSON file, not inline data: rows.

Duplicate-coverage check against the tree: the roster's static→live move IS already planned — that is #14802 (Brain-side assembler join → live registry rows through FleetCockpit.loadRoster()'s first-wire path, ratified 2026-07-09). What is NOT covered anywhere: the store-topology rule (de-singleton) and the JSON-file seed fetch for the pre-wire sample. This ticket is that gap.

The Problem

apps/agentos currently has two singleton stores, and the second was sibling-lifted from the first — the anti-pattern is propagating exactly the way the app-work gate predicts:

  • apps/agentos/store/AgentDefinitions.mjssingleton: true; consumed via direct module-import by FleetSettingsPanel (reads) and Accounts (writes).
  • apps/agentos/store/FleetRoster.mjssingleton: true (added by PR #14905, lifted from the AgentDefinitions precedent); consumed via direct import by FleetCockpit, seated onto FleetGrid + HealthBar.

Both also carry inline seed rows (FleetRoster seeds the seven-maintainer sample roster; AgentDefinitions seeds a bridge-pending placeholder row).

Singleton stores are module-global state: they bypass the component tree's scoping, survive view destruction invisibly (PR #14905 already had to add manual destroy() listener un-binding on grid + bar because the store outlives its views), and make test isolation depend on never touching the shared instance.

The Architectural Reality (V-B-A'd this session)

  • src/state/Provider.mjs natively hosts stores: stores_ config (:148-164), 'stores.<key>' binding resolution (resolveStore, :441), and getStore(key) walking the provider hierarchy (:562-566). A provider at a view root IS the sharing scope — "if used inside a state provider, we get it anyway."
  • src/data/Store.mjs natively fetches: a url config builds an Xhr-connection pipeline in the browser (load()), with a Node fs/promises.readFile fallback — so a JSON-seeded store stays unit-testable without a web server.
  • The keyProperty store-mirror discipline from PR #14905 (collection.Base defaults keyProperty: 'id', shadowing model-level keys) must survive the refactor — FleetRoster keeps keyProperty: 'agentId' at store grain.
  • PR #14905's specs already create isolated per-test stores for grid/card units — only the roster-contract spec (which imports the singleton) and the view wiring need reshaping.

The Fix

  1. FleetRoster: drop singleton: true; move the seven-row sample seed to apps/agentos/resources/data/fleetRoster.json; configure url + autoLoad (keep model, keyProperty: 'agentId', and the honest-sample JSDoc). The engineTag truth pins move with it.
  2. Cockpit provider hosts the roster: FleetCockpit declares ONE state.Provider at its root with stores: {fleetRoster: {module: FleetRoster, ...}}; FleetGrid + HealthBar receive it via the provider store binding (bind: {store: 'stores.fleetRoster'}) instead of a module import. loadRoster() reaches the same instance through the grid seam (unchanged consumer contract: first-wire populate, later merge, fail-closed matrix).
  3. AgentDefinitions: same treatment at the shared ancestor — de-singleton, hosted by a Viewport-level provider so Accounts (writer) and FleetSettingsPanel (reader) resolve the same instance up the tree. Whether its single bridge-pending placeholder row moves to JSON or stays inline is the implementer's call (it is a state indicator, not sample data) — the load-bearing part is de-singleton + provider-hosting.
  4. Specs: reshape the roster-contract describe (currently asserts the singleton import) to the provider shape; keep the isolated per-test stores for unit grain; adapt the cockpit loadRoster doubles if the store-reach path changes.
  5. Grep-proof in the PR: zero singleton: true under apps/agentos/store/**; zero direct store singleton imports in apps/agentos/view/**.

Contract Ledger

Target surface Source of authority Proposed behavior Fallback / compatibility Docs Evidence
AgentOS.store.FleetRoster export src/data/Store.mjs + operator directive (PR #14905 follow-up) Class export (no singleton); instantiated by the cockpit provider Consumers migrate to provider binding in the same PR JSDoc grep-proof + unit specs
apps/agentos/resources/data/fleetRoster.json Store url/pipeline contract Fetched sample seed (seven real maintainers, engineTag truth) Fail-closed: fetch failure keeps an empty-but-labelled surface, never a crash in-file comment N/A (JSON) → JSDoc on the store roster spec + live mount
Cockpit provider stores.fleetRoster src/state/Provider.mjs stores_ contract The one shared roster instance for grid + health bar + loadRoster Provider hierarchy resolves for any future cockpit descendant JSDoc wiring specs
AgentOS.store.AgentDefinitions export same Class export; Viewport-provider-hosted for Accounts + FleetSettingsPanel Same-PR consumer migration JSDoc settings/accounts specs stay green

Acceptance Criteria

  • Zero singleton: true under apps/agentos/store/**; zero direct store-singleton imports in apps/agentos/view/** (grep evidence in the PR).
  • FleetRoster's sample seed lives in apps/agentos/resources/data/fleetRoster.json and loads via the store's native url config — no inline data: rows.
  • One state.Provider at the cockpit root hosts stores.fleetRoster; FleetGrid + HealthBar bind it via the provider (destroy-symmetric listeners preserved).
  • AgentDefinitions is provider-hosted at the Accounts/FleetSettingsPanel shared ancestor; both consumers resolve the same instance.
  • loadRoster semantics preserved exactly (first-wire populate → merge → fail-closed incl. wired-empty); engineTag truth pins preserved; unit CI green.
  • Live mount re-verified: the sample roster renders from the fetched JSON with the sample roster label.

Out of Scope

  • #14802 (the Brain-side assembler join feeding LIVE rows — separate, already signed; this ticket only relocates the pre-wire sample).
  • Amending the AGENTS.md app-work gate row with a "stores are provider-hosted, never singletons" clause — candidate compact addition, but the file sits ~35 bytes under its 24KB cap; decide at implementation whether a paired compression funds it or the rule stays ticket-documented until the mechanical lint (the gate row's stated retirement trigger) absorbs it.

Avoided Traps

  • "Keep the singleton because Accounts writes cross-view." The provider at the shared ancestor IS the cross-view mechanism — getStore() walks up; that is the operator's point.
  • Hand-rolled fetch() for the JSON seed. The Store's native url/pipeline path exists, has a Node fallback for specs, and fires the same load event the views already consume.
  • De-singleton without consumer migration in the same PR. A class export with surviving Neo.create call-sites per-view would fork N instances silently — the provider hosting and the consumer migration are one atomic change.

Decision Record impact

none — aligned-with the established state.Provider stores contract and the #14899 data-path discipline. No ADR created or challenged.

Related

  • PR #14905 (origin: operator follow-up on the merge) · #14899 (the Store/Model rebuild this completes) · #14802 (live-data producer, sibling) · parent epic #13015 · #14805 (design-conformance epic, adjacent consumer of the same app).

Live Sweeps

  • Live latest-open sweep: checked the latest 20 open issues at 2026-07-10T01:10Z; no equivalent store-topology/JSON-seed ticket found (#14805 is token-consumption conformance; #14807 is the account/config model).
  • A2A in-flight claim sweep: latest 30 all-status messages at 2026-07-10T01:10Z; no overlapping [lane-claim]/[lane-intent] in the herd window.

Origin Session ID: d2fbbdb4-404b-47e1-bbb3-1b9e0330894b

Handoff Retrieval Hints:

  • query_raw_memories: "agentos store singleton provider-hosted JSON seed fleetRoster de-singleton"
  • Anchor: PR #14905 merge-thread operator follow-up; store-keyproperty-shadows-model discipline carries over.
tobiu closed this issue on 7:37 AM
tobiu referenced in commit 4d4b20d - "feat(agentos): de-singleton the stores to provider-hosted + JSON-fetched seeds (#14909) (#14920) on 7:37 AM