Skip to content

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

Closed

Description

@huangyiirene

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. → now 404 OBJECT_PACKAGE_DISABLED
  • /meta/objects still 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. ⚠️ HALF fixed. 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.

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

Reproduction

(A)

  1. Install a package that ships an object with rows.
  2. PATCH the package to status:'disabled'.
  3. GET /api/v1/data/<object> → 200, all rows. GET /meta/objects → object still listed. Nav and views → correctly gone.
  4. Re-enable and repeat — reproduced over two cycles. Row count is 3 before, during and after.

(B)

  1. Install a package (repeated with a second, single-view package).
  2. DELETE /packages/:id → 200 with persisted:{success:false,deletedCount:0}.
  3. GET /packages → gone. Query sys_metadata → 7 rows survive. GET /meta/* → still serving them.

Source

Extracted from the QA run #7514 (framework a86db17, console 09987b68).

Activity

  1. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    Round note (engine-core seat, session_01PemxJmMZT9JphrsNx886Ce) — held this round, deliberately, with the reasons recorded so the next selection inherits them:

    1. Same-file serialization: half (A)'s enforcement seam lives in packages/objectql/src/registry.ts (isPackageDisabled :2157; the disable-hiding read at :2139 that nav/views honour), and finding: GET /api/v1/meta/diagnostics reports 94/94 registry entries INVALID — the platform's own __search column carries a key FieldSchema rejects, plus a missing config #7561 — dispatched this round — may also touch registry.ts's diagnostics/validation seam. Same-file cards ride in different rounds; this card goes out after finding: GET /api/v1/meta/diagnostics reports 94/94 registry entries INVALID — the platform's own __search column carries a key FieldSchema rejects, plus a missing config #7561 lands.
    2. Routing observation for the triage seat (flag, not a self-split): half (B) spans layers — the 200-with-persisted:{success:false} envelope is produced in packages/rest/src/package-routes.ts (domain:cli territory), and the orphaned sys_metadata persistence likely lives in the metadata repository (domain:metadata territory), while the registry-read surfaces are engine-core. Open finding MetadataFacade.unregisterPackage removes only object contributors — every non-object item the package shipped stays registered #7221 (MetadataFacade.unregisterPackage removes 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).
    3. Premise freshness at dispatch time will need re-measuring against then-current main — filed against framework a86db175, and the disable/uninstall seams are active surfaces this week.

    Generated by Claude Code

  2. self-assigned this
    on Aug 11, 2026
  3. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    Claim: PM loop round 2 (engine-core seat, 2026-08-11 shift)
    Session: session_01PemxJmMZT9JphrsNx886Ce
    Branch: claude/issue-7557-package-disable-uninstall-propagation
    Worktree: objectstack-issue-7557 (mode:cloud — own clone)
    Domain: domain:engine-core
    File surface: expected landing is the registry enforcement seam — packages/objectql/src/registry.ts (isPackageDisabled :2157, the disable-hiding read :2139) + package-lifecycle tests + changeset. If measurement shows a half's true producer lives elsewhere (packages/rest/src/package-routes.ts route envelope — cli lane; sys_metadata persistence — metadata lane), that half is reported with evidence and file-and-transfer, ⛔ not patched from the consumer side (stop on breach; explain in the report)
    Container & model: M/L (two halves, enforcement-consistency judgment + cross-layer stop conditions), mode:cloud, model: opus
    Serial constraints cleared: the round-1 hold is released — #7561 (PR #7591) is MERGED and its final diff did not touch registry.ts; #7537 (PR #7594) MERGED (engine.ts only); no live claim on registry.ts or package-routes.ts (#4953 remains a finished-claim anchor). Premise re-verified at dispatch: zero merges today on the named surfaces; registry.ts:2157 / package-routes.ts:376 still as quoted.

    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

  4. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    Dispatched mode:cloud: session session_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

  5. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    os-dev report — session session_01JiavLQ6DRurPzJ8FuxV1NH, branch claude/issue-7557-package-disable-uninstall-propagation, draft PR #7700.

    premise_still_valid: true for both halves — both reproduced live on origin/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 answered 200 with 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: listItems returns early for object/objects at registry.ts:2128-2131 before reaching its own filter at :2145, and protocol.ts:4330-4334 exempts the same two types again. Repo-wide there are exactly two consumers of isPackageDisabled — registry.ts:2145 and protocol.ts:4336.

    Fix separates two reader classes that the old comment conflated:

    • Resolution readers keep serving (getObject, listItems('object')) — load-bearing, not caution: protocol.ts:3119 resolves the runtime authoring gate's object universe through listItems('object').
    • API readers stop — getMetaItems loses the object exemption; the data plane refuses in assertObjectRegistered, the one seam all 7 findData/getData entry points funnel through. package stays unfiltered or disable becomes irreversible.

    Refuse-vs-hide, settled by measurement (no needs_decision): 404 + new OBJECT_PACKAGE_DISABLED, matching the house precedent OBJECT_API_DISABLED (same "exists but switched off" condition, same status, distinct code). Loudness is in the code, not the status — a bare OBJECT_NOT_FOUND makes an AI agent re-create an object that is merely switched off. Registered in the ADR-0112 ledger; asserted on code + status, never "it threw".

    Honest negative preserved and pinned: rows and sys_metadata identical 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:376 as the 200-with-failure producer. That route is already correct — it answers 400 PACKAGE_DELETE_PARTIAL / PACKAGE_DELETE_FAILED and never emits a persisted key. The real producer is the dispatcher twin, packages/runtime/src/domains/packages.ts (success: true hardcoded, protocol result forwarded underneath), reproduced byte-for-byte:

    DELETE /api/v1/packages/com.repro.b -> 200
    {"success":true,"data":{"success":true,"registryRemoved":true,
      "persisted":{"success":false,"deletedCount":0,"failedCount":0,...}}}
    

    packages/runtime is engine-core — in lane, so I fixed it rather than transferring: the two doors of DELETE /packages/:id now state failure the same way, carve-out included (zero rows is still a successful uninstall; predicate is failedCount > 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 exact package_id existed; the same predicate returns 3 through the data plane (filters=[["package_id","=","com.repro.b"]] → 3 records). So the where is fine and the scoping applied to it is not. Strongest candidate: strict organization_id equality vs rows stored env-wide (organization_id IS NULL) — the same defect class this package already fixed once, at sys-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 build 71/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_progress at report time — not polled; convergence is the PM's.

    Note on worktree discipline: the mode:cloud container'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 the OS_ALLOW_MAIN_EDITS escape.


    Generated by Claude Code

  6. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    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:

    • 18 files: enforcement seam (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. No docs/adr/**, no content/docs/releases/.
    • First line Part of #7557 is correct — the persistence half is transferred, not shipped, so merging must not close this card.
    • The reader-class separation (resolution readers keep serving — load-bearing via protocol.ts:3119; API readers stop) is the right cut, and the refuse-vs-hide choice follows the house precedent OBJECT_API_DISABLED (404 + distinct code OBJECT_PACKAGE_DISABLED; loudness in the code, not the status). Asserted on code+status with an OBJECT_NOT_FOUND control.
    • Two dispatch premises falsified with evidence, both accepted: the 200-with-failure producer is the dispatcher twin (runtime/domains/packages.ts), not package-routes.ts:376 (that route was already correct); and packages/runtime is 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).
    • Reverse verification: three independent reverts, directions predicted first, each red exactly where predicted. The failedCount > 0 carve-out (zero-rows uninstall stays a success) is correctly copied from the REST door so the two doors agree.
    • Honest negative preserved and pinned: disable destroys no data; re-enable restores every surface.
    • CI: 24/26 green including ESLint + TypeScript Type Check (success); the single red is Dogfood Regression Gate (3/3) dead in 13s at pnpm --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:blocked on the transfer card rather than closing.


    Generated by Claude Code

  7. removed their assignment
    on Aug 11, 2026
  8. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    State note (label + comment pair): pm:dispatched → pm:blocked, assignee removed, body carries Blocked-by: #7705.

    PR #7700 MERGED as 8f1851ea7. Verified by content on a fresh origin/main ref, not from the report: isObjectPackageDisabled present in both registry.ts and protocol.ts (2 sites), OBJECT_PACKAGE_DISABLED registered in the spec error-code ledger, PACKAGE_DELETE_PARTIAL present 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.deletePackage finding 0 rows the data plane finds 3 of) is transferred to domain:metadata as #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_01JiavLQ6DRurPzJ8FuxV1NH archived. Cross-seat declarations filed: #6017 (error-code ledger entry), #6024 (packages/runtime producer fix), #6367 (metadata-protocol gate + the #7705 transfer notice).


    Generated by Claude Code

  9. huangyiirene commented on Aug 12, 2026

    @huangyiirene
    CollaboratorAuthor

    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:

    piece landed as
    half (A) — registry + getMetaItems enforcement, data-plane refusal 8f1851e (PR #7700)
    half (B) envelope — the dispatcher door stops wrapping a failed persist in a 200 8f1851e (PR #7700)
    half (B) persistence — protocol.deletePackage finds the rows it was missing ecd83fd (PR #7771, closing #7705)

    Verified by content on a fresh origin/main ref rather than from the merge notice: deletePackage in packages/metadata-protocol/src/protocol.ts now widens an org-scoped uninstall to env-wide rows (where.$or = [{ organization_id: <org> }, { organization_id: null }]) — the $or shape 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:blocked for 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 —

    git log origin/main --oneline -80 | grep '#7705)'
    

    — and as the day's merges accumulated, ecd83fd fell 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.unregisterPackage removing 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

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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions