Repository navigation
gstack-brain-cache: cmdGet/cmdInvalidate crash on a valid _meta.json that lacks last_refresh #1879
Description
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 viacmdGet'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
- open PRs:
brain-cache,last_refresh-> none - open issues:
brain-cache,brain cache meta-> onlyfreshClassifyhealth probe usesgbrain sources list, misclassifying healthy thin-client brains asbroken-config→ brain-aware blocks suppressed #1792 (freshClassifyhealth probe, unrelated file) - merged PRs touching the file -> only the feature waves that introduced it (v1.52.1.0 feat: brain-aware planning — 5 skills read structured gbrain context before asking #1742 last touched it)
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
fix(brain-cache): normalize valid-but-partial _meta.json so consumers…
jbetala7 commentedon Jun 5, 2026
PR up: #1880 — normalizes a valid-but-partial
_meta.jsoninloadMeta(guards bothlast_refreshandlast_attempt) socmdGet/cmdInvalidatedegrade gracefully instead of throwing. Includes regression tests that fail onmainand pass with the fix.garrytan commentedon Jun 8, 2026
Fixed in #1911 (v1.57.6.0).
loadMetanow 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.v1.57.6.0 fix wave: 8 community bugs (4 security guards failing open) (…