LearnNewsExamplesServices
Frontmatter
id17368
titleA corrupt instance roster reads as an empty one, and nothing says otherwise
stateClosed
labels
bugaiagent-os
assigneesneo-opus-ada
createdAtAug 18, 2026, 8:56 PM
updatedAtAug 21, 2026, 7:35 PM
githubUrlhttps://github.com/neomjs/neo/issues/17368
authorneo-opus-grace
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 21, 2026, 7:35 PM

A corrupt instance roster reads as an empty one, and nothing says otherwise

Closed Backlog/active-chunk-17 bugaiagent-os
neo-opus-grace
neo-opus-grace commented on Aug 18, 2026, 8:56 PM

Context

Found reviewing PR #17365 (#17328's instance switcher). Filed rather than raised as a review nit: the PR is a 2440-line first landing that is otherwise sound, @neo-fable-clio is wrapping it up, and I did not judge this worth blocking on. Filing is the honest alternative to a foldable note — a reviewer's grade should be block it or do not write it, and this is the third form: worth fixing, not worth blocking.

Live latest-open sweep: checked the latest 12 open issues at 2026-08-18T18:51Z; no equivalent. Adjacent but distinct from #17367 (hand-rolled menu, keyboard + dismissal).

The Problem

instanceRosterStorage.mjs distinguishes two failure modes and reports only one:

export function reviveInstanceRoster(json) {
    try   { parsed = JSON.parse(json) }
    catch { return {records: [], dropped: []} }        // ← envelope failure, SILENT

    if (!Array.isArray(parsed)) return {records: [], dropped: []}  // ← same

    parsed.forEach(row => {
        try   { records.push(...) }
        catch (error) { dropped.push({record: row, reason: ...}) }  // ← row failure, REPORTED
    });
}

Its own docblock states the intent: "Fail-open to EMPTY on a malformed envelope (a broken storage value must not brick the switcher), fail-loud per ROW on contract refusals." Fail-open is right. Fail-open and fail-silent are different things, and only the first was chosen deliberately.

The caller surfaces exactly what it is given:

const {records, dropped} = reviveInstanceRoster(value);
dropped.forEach(({reason}) => console.warn(`fleet instance roster: dropped a stored row — ${reason}`));

A malformed envelope yields an empty dropped, so no warning fires at all.

What the operator sees. Their configured roster fails to parse; the switcher seeds the boot profile as the first row; the UI shows exactly one instance and looks completely normal — indistinguishable from a fresh install. Every instance they configured is gone from view, with no message, no marker, and no reason. The localStorage value is still on disk and recoverable, but nothing tells them there is anything to recover.

This is the honest-absence contract that governs the rest of this feature, inverted: a missing roster entry renders as absent everywhere else in the design; a missing roster renders as a normal empty one.

The Architectural Reality

  • apps/agentos/fleet/instanceRosterStorage.mjsreviveInstanceRoster, both envelope-failure returns.
  • apps/agentos/view/ViewportController.mjs — the caller, which warns per dropped row and has nothing to warn about here.

The Fix

Give the envelope failure a channel the row failures already have. The shape matters less than that it exists — e.g. a third return field naming the envelope outcome (ok / unparseable / not-an-array), leaving records/dropped semantics untouched so no existing caller changes meaning.

The caller then reports it the same way it reports dropped rows. Whether that is a console warning, a degraded-state marker on the switcher, or both, is the implementer's call — but the operator must be able to tell "you have one instance" from "your roster did not parse".

Decision Record impact

none.

Acceptance Criteria

  • A malformed envelope (unparseable JSON, or valid JSON that is not an array) is distinguishable by the caller from a genuinely absent or empty roster.
  • The distinction reaches a surface an operator can see — not only a return value the caller may ignore.
  • Fail-open behaviour is preserved: a broken storage value still yields a working switcher and never throws.
  • Row-level dropped reporting is unchanged; this adds a channel rather than reshaping the existing one.
  • A test covers both envelope shapes, asserting the signal is present — and a control asserting a valid empty roster produces no such signal, so the new channel cannot fire unconditionally.

Out of Scope

  • Recovering or migrating the unparseable value. Telling the operator it happened is this ticket; salvage is a different question and may not be worth answering.
  • #17367's keyboard and dismissal work on the same switcher.

Avoided Traps

  • "Throw instead." That breaks the deliberate fail-open property — a corrupt storage value must not brick the switcher. The defect is silence, not leniency.
  • "The records: [] return is enough for the caller to infer it." An empty roster and a corrupt roster produce byte-identical returns today; there is nothing to infer from.

Related

  • #17328 · PR #17365 — the landing this was found in
  • #17367 — the other filed gap on the same switcher

Origin Session ID: ad99f59b-9d2c-4f82-b6ce-8c8357ef1879

Retrieval Hint: query_raw_memories("instance roster malformed envelope silent empty fail-open fail-silent").

tobiu referenced in commit 6d60a11 - "fix(agentos): a corrupt or unreadable instance roster says so (#17368) (#17473) on Aug 21, 2026, 7:35 PM
tobiu closed this issue on Aug 21, 2026, 7:35 PM