Skip to content

gstack-brain-cache: cmdGet/cmdInvalidate crash on a valid _meta.json that lacks last_refresh #1879

Closed

Description

@jbetala7

Observed problem

bin/gstack-brain-cache crashes with a TypeError when its _meta.json is valid JSON but is missing the last_refresh map. cmdGet and cmdInvalidate both throw instead of degrading gracefully.

loadMeta already defends against a missing file and against corrupt (unparseable) JSON — both return a fresh, fully-formed meta. But when the file parses successfully yet lacks last_refresh, loadMeta returns it verbatim:

function loadMeta(...): CacheMeta {
  const path = metaPath(scope, projectSlug);
  if (!existsSync(path)) { return { ...fresh with last_refresh: {} }; }
  try {
    return JSON.parse(readFileSync(path, 'utf-8')) as CacheMeta;   // <- returned verbatim, not normalized
  } catch {
    return { ...fresh with last_refresh: {} };
  }
}

Downstream, refreshEntity guards its sibling map (meta.last_attempt = meta.last_attempt || {}) but three consumers dereference meta.last_refresh unguarded:

  • isStale (reached via cmdGet's warm-path check): const last = meta.last_refresh[entityName];
  • cmdInvalidate: delete meta.last_refresh[entityName];
  • refreshEntity: meta.last_refresh[entityName] = Date.now();

The asymmetry is visible in the type itself: last_attempt? is optional (hence the || {} guard) while last_refresh is typed as required — so the guard was only applied to one of the two maps.

Current behavior on upstream main

With a _meta.json whose schema_version matches the current pack and whose endpoint_hash matches (so the schema/endpoint rebuild path is not taken), but with no last_refresh key:

  • cmdGet('product', 'helsinki') -> TypeError: undefined is not an object (evaluating 'meta.last_refresh[entityName]')
  • cmdInvalidate('product', 'helsinki') -> TypeError: undefined is not an object (evaluating 'delete meta.last_refresh[entityName]')

Reproduced against main with a 2-test bun test. Such a meta can arise from external tooling, a hand-edit, or any valid-but-partial persisted state — exactly the class of input the corrupt-JSON branch was added to survive.

Expected behavior

A valid-but-partial _meta.json should be treated like the corrupt-JSON case: normalized to a usable meta so cmdGet falls through to a normal cold-refresh/stale path and cmdInvalidate is a safe no-op. Neither should throw.

Duplicate searches performed

Candidate fix shape

Normalize once in loadMeta so all consumers receive a complete meta:

const parsed = JSON.parse(readFileSync(path, 'utf-8')) as CacheMeta;
parsed.last_refresh = parsed.last_refresh || {};
parsed.last_attempt = parsed.last_attempt || {};
return parsed;

Plus a regression test that seeds a _meta.json without last_refresh and asserts cmdGet/cmdInvalidate do not throw.

Activity

  1. added a commit that references this issue on Jun 5, 2026
    d3a132a
  2. jbetala7 commented on Jun 5, 2026

    @jbetala7
    ContributorAuthor

    PR up: #1880 — normalizes a valid-but-partial _meta.json in loadMeta (guards both last_refresh and last_attempt) so cmdGet/cmdInvalidate degrade gracefully instead of throwing. Includes regression tests that fail on main and pass with the fix.

  3. garrytan commented on Jun 8, 2026

    @garrytan
    Owner

    Fixed in #1911 (v1.57.6.0). loadMeta now shape-guards _meta.json (null/array/primitive → cold cache) and normalizes the dereferenced maps; missing schema/endpoint identity forces a safe rebuild rather than crashing. Thanks @jbetala7. Auto-closes on merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions