Repository navigation
Package disable and uninstall never reach the metadata/data layer: a disabled package's objects still serve rows, and uninstall leaves 7 orphaned sys_metadata rows #7557
Description
Blocked-by: #7705
> Status 2026-08-11 (engine-core seat): half (A) and the envelope part of half (B) LANDED via PR #7700 (8f1851ea7). What remains open on this card is the persistence half of (B) only, transferred to the metadata lane as #7705. This card closes when #7705 lands. See the ACCEPT comment below for the full verification record.
Two halves of one gap: a package lifecycle operation moves the registry/UI surfaces but never propagates to the metadata store or the data plane. Filed as one card because both are the same missing propagation, on the two operations that need it.
Symptom
(A) Disable is not an enforcement for objects. ✅ FIXED by PR #7700. With the owning package at status:'disabled':
GET /api/v1/data/<object>still answers 200 with all rows. → now404 OBJECT_PACKAGE_DISABLED/meta/objectsstill lists the object. → now dropped from the listing- Nav and views are correctly dropped — so disable is honoured on some surfaces and not others.
- The console renders the live list too, so this is not an API-only artefact.
Reproduced over two disable/enable cycles.
Expected: a disabled package's objects are dropped from the metadata registry and refuse on the data plane, consistently with the nav/view surfaces that already honour the status.
(B) Uninstall leaves orphaned metadata. DELETE /packages/:id answers 200, with:
persisted: { success: false, deletedCount: 0 }
The package disappears from /packages, but 7 sys_metadata rows survive and /meta/* keeps serving them. Reproduced with a second, single-view package.
Expected: either the persisted delete succeeds and the metadata rows go with the package, or the failed persistence is reported as a failure instead of being wrapped in a 200.
- ✅ The dishonest envelope is fixed (PR fix(engine-core): propagate package disable to metadata listings and the data plane (#7557) #7700): the producer was the dispatcher twin
packages/runtime/src/domains/packages.ts, notpackages/rest/src/package-routes.tsas this card guessed — that REST door was already correct. The dispatcher now answers400 PACKAGE_DELETE_PARTIALon per-item failures, matching the other door including its zero-rows carve-out. - ⛔ The rows still orphan —
protocol.deletePackagefinds 0 rows the data plane finds 3 of. Located with evidence, transferred todomain:metadataasprotocol.deletePackagefinds zerosys_metadatarows the data plane finds 3 of — uninstall leaves orphaned rows (persistence half of #7557) #7705, deliberately not patched from the consumer side. Note this half is invisible to the envelope fix by construction (failedCount: 0).
Honest negative, recorded so nobody re-derives it: disable destroys no data — 3 rows before, during and after the disabled window. Now pinned by packages/objectql/src/package-disable-enforcement.test.ts.
Root cause
Not located by the report beyond the symptoms above. Two facts it does pin down:
- The
persisted:{success:false,deletedCount:0}envelope means the delete's own persistence step reported failure and the route answered 200 anyway — the status code and the payload disagree. - Disable is enforced at the nav/view layer but not at the registry or data layer, so the status is consulted in some readers and not others rather than being missing entirely.
Filed domain:engine-core because the surviving surfaces are registry-backed reads; re-route if the fix lands in the /packages route layer (packages/rest) or in the metadata repository instead.
> Measured answer to that last sentence: the disable asymmetry was listItems' early return for object/objects (registry.ts) plus the same exemption in getMetaItems (protocol.ts) — engine-core, as filed. The envelope was packages/runtime (cli lane, declared on #6024). The persistence is packages/metadata-protocol (#7705).
Related
MetadataFacade.unregisterPackageremoves only object contributors — every non-object item the package shipped stays registered #7221 (open):MetadataFacade.unregisterPackageremoves only object contributors — every non-object item the package shipped stays registered. Same family as (B) at the in-memory registry layer; (B) here is the persisted layer (sys_metadatarows surviving a 200 delete). PR fix(engine-core): propagate package disable to metadata listings and the data plane (#7557) #7700's measured fix path did not run through it, so it stays untouched and distinct.- A deleted runtime
objectis still served bySchemaRegistry.getObject— the registry heal reaches themetadatamap but neverobjectContributors#6808 (closed): a deleted runtime object still served bySchemaRegistry.getObject— the registry heal reached themetadatamap but neverobjectContributors. Closed before this build was cut, and the symptom here arrives through a package status rather than a delete, so this is a distinct path rather than a regression of it — worth reading for the shape of the heal. (Its lesson was applied in fix(engine-core): propagate package disable to metadata listings and the data plane (#7557) #7700: the new gate resolves names through the sameresolveObjectKeythe read path uses, so gate and read cannot disagree.)
Reproduction
(A)
- Install a package that ships an object with rows.
PATCHthe package tostatus:'disabled'.GET /api/v1/data/<object>→ 200, all rows.GET /meta/objects→ object still listed. Nav and views → correctly gone.- Re-enable and repeat — reproduced over two cycles. Row count is 3 before, during and after.
(B)
- Install a package (repeated with a second, single-view package).
DELETE /packages/:id→ 200 withpersisted:{success:false,deletedCount:0}.GET /packages→ gone. Querysys_metadata→ 7 rows survive.GET /meta/*→ still serving them.
Source
Extracted from the QA run #7514 (framework a86db17, console 09987b68).
Activity
os-help commentedon Aug 11, 2026
Round note (engine-core seat,
session_01PemxJmMZT9JphrsNx886Ce) — held this round, deliberately, with the reasons recorded so the next selection inherits them:packages/objectql/src/registry.ts(isPackageDisabled:2157; the disable-hiding read at :2139 that nav/views honour), and finding:GET /api/v1/meta/diagnosticsreports 94/94 registry entries INVALID — the platform's own__searchcolumn carries a keyFieldSchemarejects, plus a missingconfig#7561 — dispatched this round — may also touchregistry.ts's diagnostics/validation seam. Same-file cards ride in different rounds; this card goes out after finding:GET /api/v1/meta/diagnosticsreports 94/94 registry entries INVALID — the platform's own__searchcolumn carries a keyFieldSchemarejects, plus a missingconfig#7561 lands.persisted:{success:false}envelope is produced inpackages/rest/src/package-routes.ts(domain:cliterritory), and the orphanedsys_metadatapersistence likely lives in the metadata repository (domain:metadataterritory), while the registry-read surfaces are engine-core. Open findingMetadataFacade.unregisterPackageremoves only object contributors — every non-object item the package shipped stays registered #7221 (MetadataFacade.unregisterPackageremoves only object contributors) is the same family at the in-memory layer, as the card itself notes. This may deserve a contract-first split (registry enforcement / persistence delete / route envelope) rather than one dispatch; if triage prefers the split, the engine-core share is half (A) plus the registry-read part of (B).a86db175, and the disable/uninstall seams are active surfaces this week.Generated by Claude Code
os-help commentedon Aug 11, 2026
Re-pricing note (per the unlock discipline): #7561's landing neither absorbed nor complicated this card — final surfaces were disjoint. The card's cost model is unchanged from filing; the triage-split flag from the round-1 note stands as an option the dev's measurements will settle (see dispatch brief).
Generated by Claude Code
os-help commentedon Aug 11, 2026
Dispatched
mode:cloud: sessionsession_01JiavLQ6DRurPzJ8FuxV1NH(created 10:20Z, model opus, outcome branch = the claim branch). Report expected as an issue comment here at draft-PR time.Generated by Claude Code
os-help commentedon Aug 11, 2026
os-dev report — session
session_01JiavLQ6DRurPzJ8FuxV1NH, branchclaude/issue-7557-package-disable-uninstall-propagation, draft PR #7700.premise_still_valid: truefor both halves — both reproduced live onorigin/main, not read off the source. One dispatch assumption falsified; see (B).Half (A) — shipped in full
Reproduced on a real server (
pnpm dev --fresh, runtime-installed package, 2 package-bound objects, 3 rows): disabled package still answered200with all rows on/data/<object>and still listed the objects on/meta/objects, while nav/views correctly vanished.The asymmetry the card asked me to measure is one line above the predicate:
listItemsreturns early forobject/objectsatregistry.ts:2128-2131before reaching its own filter at:2145, andprotocol.ts:4330-4334exempts the same two types again. Repo-wide there are exactly two consumers ofisPackageDisabled—registry.ts:2145andprotocol.ts:4336.Fix separates two reader classes that the old comment conflated:
getObject,listItems('object')) — load-bearing, not caution:protocol.ts:3119resolves the runtime authoring gate's object universe throughlistItems('object').getMetaItemsloses the object exemption; the data plane refuses inassertObjectRegistered, the one seam all 7findData/getDataentry points funnel through.packagestays unfiltered or disable becomes irreversible.Refuse-vs-hide, settled by measurement (no
needs_decision):404+ newOBJECT_PACKAGE_DISABLED, matching the house precedentOBJECT_API_DISABLED(same "exists but switched off" condition, same status, distinct code). Loudness is in the code, not the status — a bareOBJECT_NOT_FOUNDmakes an AI agent re-create an object that is merely switched off. Registered in the ADR-0112 ledger; asserted oncode+status, never "it threw".Honest negative preserved and pinned: rows and
sys_metadataidentical before / during / after; re-enable restores every surface.Half (B) — envelope shipped, persistence transferred
⛔ Dispatch assumption falsified. The card named
packages/rest/src/package-routes.ts:376as the 200-with-failure producer. That route is already correct — it answers400 PACKAGE_DELETE_PARTIAL/PACKAGE_DELETE_FAILEDand never emits apersistedkey. The real producer is the dispatcher twin,packages/runtime/src/domains/packages.ts(success: truehardcoded, protocol result forwarded underneath), reproduced byte-for-byte:packages/runtimeis engine-core — in lane, so I fixed it rather than transferring: the two doors ofDELETE /packages/:idnow state failure the same way, carve-out included (zero rows is still a successful uninstall; predicate isfailedCount > 0).🔀 TRANSFER —
domain:metadata, the persistence half.protocol.deletePackage(packages/metadata-protocol/src/protocol.ts:11486-11488) found zero rows while 3 rows with that exactpackage_idexisted; the same predicate returns 3 through the data plane (filters=[["package_id","=","com.repro.b"]]→ 3 records). So thewhereis fine and the scoping applied to it is not. Strongest candidate: strictorganization_idequality vs rows stored env-wide (organization_id IS NULL) — the same defect class this package already fixed once, atsys-metadata-repository.ts:887-901, where identical strict equality "silently dropped" env-wide drafts and became an$or(the orphaned-draft bug). Not patched from the consumer side per the stop condition.This half is invisible to the envelope fix by construction (
failedCount: 0), so it needs its own card.#7221 read for shape; the measured fix path does not run through it — untouched, as instructed.
Gates
All green:
check:durability-log-level,check:engine-double-contract,check-engine-split-ratio(100.0%),check:nul-bytes,check:error-code-casing,pnpm build71/71, typecheck, and suites — objectql 3230, metadata-protocol 1062, runtime 2022, spec 9867, rest 1403. Changeset included. Both fix halves reverse-verified independently, directions predicted before running.CI on #7700 is
in_progressat report time — not polled; convergence is the PM's.Note on worktree discipline: the
mode:cloudcontainer's own clone had the branch checked out at the primary; I detached primary HEAD and created a dedicated worktree (/home/user/objectstack-issue-7557) before the first edit rather than using theOS_ALLOW_MAIN_EDITSescape.Generated by Claude Code
chore(spec): regenerate reference docs for the new error code (#7557)
os-help commentedon Aug 11, 2026
ACCEPT — PR #7700 reviewed (engine-core seat,
session_01PemxJmMZT9JphrsNx886Ce); landing is in progress pending one flake re-run. Verified against GitHub, not the report's claims:registry.ts+24,metadata-protocol/src/protocol.ts+60/−9), dispatcher envelope (runtime/src/domains/packages.ts+40), two new substance-asserting test files, error-code ledger entry + regenerated references pages, changeset. Nodocs/adr/**, nocontent/docs/releases/.Part of #7557is correct — the persistence half is transferred, not shipped, so merging must not close this card.protocol.ts:3119; API readers stop) is the right cut, and the refuse-vs-hide choice follows the house precedentOBJECT_API_DISABLED(404 + distinct codeOBJECT_PACKAGE_DISABLED; loudness in the code, not the status). Asserted oncode+statuswith anOBJECT_NOT_FOUNDcontrol.runtime/domains/packages.ts), notpackage-routes.ts:376(that route was already correct); andpackages/runtimeis the cli lane's package family, not engine-core as the report asserted — the producer-side fix is still right, and the lane bookkeeping is settled by a cross-seat declaration on [PM seat] domain:cli — ⏳ vacant #6024 (posted).failedCount > 0carve-out (zero-rows uninstall stays a success) is correctly copied from the REST door so the two doors agree.success); the single red isDogfood Regression Gate (3/3)dead in 13s atpnpm --version— the known Corepack/undici transient (fix(objectql): a lax-admitted value withdraws the irreversible half of an ADR-0104 certificate (#4797) #7285's exact signature,assert(!this.paused)in undici's Parser during the pnpm download; the test step never started). Re-run triggered; ready + auto-merge follow on green.Follow-ups being executed by the seat: transfer card for the persistence half (metadata lane queue), cross-seat declarations on #6017 (error-code ledger touch), #6024 (runtime production touch), #6367 (metadata-protocol touch). On merge this card goes
pm:blockedon the transfer card rather than closing.Generated by Claude Code
os-help commentedon Aug 11, 2026
State note (label + comment pair):
pm:dispatched→pm:blocked, assignee removed, body carriesBlocked-by: #7705.PR #7700 MERGED as
8f1851ea7. Verified by content on a freshorigin/mainref, not from the report:isObjectPackageDisabledpresent in bothregistry.tsandprotocol.ts(2 sites),OBJECT_PACKAGE_DISABLEDregistered in the spec error-code ledger,PACKAGE_DELETE_PARTIALpresent in the dispatcher, and both pin files in the tree. The card body is updated in place to mark which halves shipped, so a future reader does not re-derive fixed symptoms.Why this card did not auto-close: the PR opens
Part of #7557, deliberately — the persistence half of (B) (protocol.deletePackagefinding 0 rows the data plane finds 3 of) is transferred todomain:metadataas #7705 and is untouched by this PR. This card is the coordination anchor for that last piece and closes when #7705 lands. ⛔ Not dispatchable in the engine-core lane while blocked.Cloud session
session_01JiavLQ6DRurPzJ8FuxV1NHarchived. Cross-seat declarations filed: #6017 (error-code ledger entry), #6024 (packages/runtimeproducer fix), #6367 (metadata-protocolgate + the #7705 transfer notice).Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
State note (label + comment pair):
pm:blocked→ closed as completed (engine-core seat,session_01VGAePF7iGGUYUT8oX1cVgx).The blocker landed. This card's own stated closing condition — "this card closes when #7705 lands" — is met, and all three pieces are on
origin/main:getMetaItemsenforcement, data-plane refusal8f1851e(PR #7700)8f1851e(PR #7700)protocol.deletePackagefinds the rows it was missingecd83fd(PR #7771, closing #7705)Verified by content on a fresh
origin/mainref rather than from the merge notice:deletePackageinpackages/metadata-protocol/src/protocol.tsnow widens an org-scoped uninstall to env-wide rows (where.$or = [{ organization_id: <org> }, { organization_id: null }]) — the$orshape this package already used one file over for the orphaned-draft bug — and deliberately leaves the no-org door package-wide, so fixing the dispatcher door does not orphan the REST registrar's rows in turn. That last detail matters here: it is the same bug re-created on the other door, and the fix explicitly refuses it.⛔ Seat error, recorded so it is not repeated
This card sat
pm:blockedfor roughly 17 hours after its blocker had already merged (#7705 landed 2026-08-11T14:36:46Z; this close is 2026-08-12T08:0xZ). The standby patrol probed for the blocker with a bounded window —— and as the day's merges accumulated,
ecd83fdfell off the end of that window. Every patrol since then reported "#7705 not landed" from a false negative, not from a reading. A bounded window is not a blocker probe; it is a recency filter that silently becomes a liar.The probe is now unbounded (
git log origin/main --grep='(#7705)'), and because the defect was in the instrument rather than in this card, every other blocked card in the lane is being re-checked with the corrected probe in this same patrol — the fix is not applied to the one card that happened to expose it.Nothing in this card is left open. #7221 (
MetadataFacade.unregisterPackageremoving only object contributors — the in-memory sibling of the persistence half) stays its own card in the lane queue, as it has throughout, and is not folded into this close.Generated by Claude Code
fix(engine-core): propagate package disable to metadata listings and …
test(runtime): pin the delete door against a refused sys_packages del…
test(rest,runtime,objectql): pin the layered doors' answer for a disa…