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.
Observed problem
bin/gstack-brain-cachecrashes with aTypeErrorwhen its_meta.jsonis valid JSON but is missing thelast_refreshmap.cmdGetandcmdInvalidateboth throw instead of degrading gracefully.loadMetaalready defends against a missing file and against corrupt (unparseable) JSON — both return a fresh, fully-formed meta. But when the file parses successfully yet lackslast_refresh,loadMetareturns it verbatim:Downstream,
refreshEntityguards its sibling map (meta.last_attempt = meta.last_attempt || {}) but three consumers dereferencemeta.last_refreshunguarded: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) whilelast_refreshis typed as required — so the guard was only applied to one of the two maps.Current behavior on upstream
mainWith a
_meta.jsonwhoseschema_versionmatches the current pack and whoseendpoint_hashmatches (so the schema/endpoint rebuild path is not taken), but with nolast_refreshkey: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
mainwith a 2-testbun 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.jsonshould be treated like the corrupt-JSON case: normalized to a usable meta socmdGetfalls through to a normal cold-refresh/stale path andcmdInvalidateis a safe no-op. Neither should throw.Duplicate searches performed
brain-cache,last_refresh-> nonebrain-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)Candidate fix shape
Normalize once in
loadMetaso all consumers receive a complete meta:Plus a regression test that seeds a
_meta.jsonwithoutlast_refreshand assertscmdGet/cmdInvalidatedo not throw.