@@ -7613,7 +7613,8 @@ export class ObjectStackProtocolImplementation implements
76137613
76147614 /**
76157615 * Heal the in-memory registry after a metadata reset (overlay-row
7616- * delete) on control-plane kernels. Two layers:
7616+ * delete). Walks the layers UNDER the deleted overlay, in order, and
7617+ * stops at the first one that can serve the name:
76177618 *
76187619 * 1. Drop the plain-key runtime shadow so the packaged artifact
76197620 * (registered under `<packageId>:<name>`) becomes the visible
@@ -7626,42 +7627,77 @@ export class ObjectStackProtocolImplementation implements
76267627 * MetadataService baseline (FilesystemLoader-sourced types) and
76277628 * re-register it, preserving the historical refresh behaviour
76287629 * for items the SchemaRegistry never held as artifacts.
7630+ * 3. [#5079] When NEITHER layer has anything, the deleted row was the
7631+ * whole item — so the plain-key entry is retired too
7632+ * ({@link SchemaRegistry.removeOverlayEntry}).
7633+ *
7634+ * ## Why step 3 exists (#5079, the #4432 residual)
7635+ *
7636+ * Step 1 declines for a runtime-CREATED item: `removeRuntimeShadow` only
7637+ * un-shadows a packaged artifact, and there is none. Step 2 then found
7638+ * nothing either — and the method returned, leaving the plain-key entry
7639+ * that #4521's write-through had put there. Nothing else ever removed it,
7640+ * so for the life of the process `GET /meta/<type>` kept enumerating a
7641+ * deleted item, `GET /meta/<type>/<name>` kept serving its body, and the
7642+ * ADR-0110 D3 declaration gate kept resolving it — while the row was gone
7643+ * from `sys_metadata` and the handler registry had already dropped it.
7644+ * The measured symptom: after `DELETE /meta/action/x`, `POST
7645+ * /actions/<obj>/x` 404s with the *handler-miss* wording ("not found")
7646+ * instead of ADR-0110's "has no declaration", because the declaration was
7647+ * still resolvable from this stale entry. The delete's own receipt already
7648+ * tells the truth here — #5927 splits it into "reset to artifact default"
7649+ * (artifact-backed) vs "it no longer exists" (runtime-only); step 3 is the
7650+ * registry making the same distinction the receipt makes.
7651+ *
7652+ * ## Why the layer-2 read is now diagnosed, and runs on every kernel
7653+ *
7654+ * [#5840] left this read on plain `get` because it "decides nothing" —
7655+ * true then, false now: its `undefined` is what licenses step 3 to retire
7656+ * an entry. So it goes through {@link readItemFromMetadataService}, which
7657+ * carries the ADR-0110 D3 verdict, and a DEGRADED read stops the walk
7658+ * without retiring anything. Retiring on an outage would answer "this
7659+ * item exists in no layer" from a read that never reached one — the exact
7660+ * miss-vs-outage confusion #5532/#5840 closed on the sibling paths. The
7661+ * same helper also folds in the singular/plural retry, so a baseline
7662+ * stored under the twin spelling is found rather than retired.
7663+ *
7664+ * RE-REGISTRATION stays control-plane-only (`environmentId === undefined`)
7665+ * — the historical refresh semantics of the original call sites, unchanged.
7666+ * Only the READ is now unconditional, because a project kernel needs the
7667+ * same evidence before retiring an entry.
76297668 *
76307669 * Best-effort: a failure must never block the delete that already
76317670 * succeeded; the next full reload fixes the registry anyway.
76327671 */
76337672 private async restoreArtifactRegistryView(type: string, name: string): Promise<void> {
76347673 try {
76357674 const registry: any = this.engine.registry;
7675+ const singular = PLURAL_TO_SINGULAR[type] ?? type;
76367676 let healed = false;
76377677 if (typeof registry.removeRuntimeShadow === 'function') {
7638- const singular = PLURAL_TO_SINGULAR[type] ?? type;
76397678 healed = registry.removeRuntimeShadow(singular, name);
76407679 if (type !== singular) {
76417680 healed = registry.removeRuntimeShadow(type, name) || healed;
76427681 }
76437682 }
76447683 if (healed) return;
7645- // MetadataService re-registration is control-plane-only — it
7646- // preserves the historical refresh semantics gated on
7647- // `environmentId === undefined` at the original call sites.
7648- if (this.environmentId !== undefined) return;
7649- const services = this.getServicesRegistry?.();
7650- const metadataService = services?.get('metadata');
7651- if (metadataService && typeof metadataService.get === 'function') {
7652- // [#5840] Measured and deliberately left on plain `get`. This
7653- // read decides nothing and asserts nothing: it returns void,
7654- // its `undefined` produces no answer to any caller, and the
7655- // method's own contract above is "best-effort, the next full
7656- // reload fixes the registry anyway". Routing it through
7657- // `getDiagnosed` could only add a log line to a path that is
7658- // already documented as silent — over-applying the rule, which
7659- // is how `error`/`warn` become unreadable (AGENTS.md
7660- // "Degradation log levels", the do-not-over-apply half).
7661- const artifactItem = await metadataService.get(type, name);
7662- if (artifactItem !== undefined) {
7663- this.engine.registry.registerItem(type, artifactItem, 'name');
7684+
7685+ const baseline = await this.readItemFromMetadataService(type, name);
7686+ if (baseline.data !== undefined && baseline.data !== null) {
7687+ if (this.environmentId === undefined) {
7688+ this.engine.registry.registerItem(type, baseline.data, 'name');
76647689 }
7690+ return;
7691+ }
7692+ // ADR-0110 D3 — an outage is not an absence. Leave the entry: it
7693+ // is stale, which is exactly where this method already was, and a
7694+ // later delete or reload heals it.
7695+ if (baseline.degraded) return;
7696+
7697+ // [#5079] No artifact, no baseline: the row WAS the item.
7698+ if (typeof registry.removeOverlayEntry === 'function') {
7699+ registry.removeOverlayEntry(singular, name);
7700+ if (type !== singular) registry.removeOverlayEntry(type, name);
76657701 }
76667702 } catch {
76677703 // Best-effort registry refresh; next read fixes it anyway
0 commit comments