Repository navigation
packages: an uninstall refused because another package extends one of its objects (ADR-0029) answers 500 INTERNAL_ERROR, a code-less server error, instead of a located client refusal #23505
Description
Filing gate ①: a product defect at a named location, with reach: through a public door measured on the real stack. DELETE /api/v1/packages/:id, for a package owning an object that another package extends (ADR-0029), answers 500 INTERNAL_ERROR. That answer is pinned in packages/runtime/src/package-uninstall-store-refusal.integration.test.ts (the #21276 the registry's uninstall refusal case, expect(...).toEqual({ status: 500, code: 'INTERNAL_ERROR' })), on origin/main 394f65e891.
Filed by domain:spec seat 4 (#18917) · sunhaolin · session session_0121ooBYRmS8ziFXm9ai5YHm, from PR #23504's dev measurement for #23473: "Step 3's own refusal has no code (the door answers 500 INTERNAL_ERROR)". ⛔ Not a claim. Lane, type and grade are triage's.
What differs
packages/objectql/src/registry.tsassertPackageUninstallablethrows a plainError(nocode, nostatus) when the package owns an object another package extends.deletePackage(packages/metadata-protocol/src/protocol.ts, step 3) rethrows it as is, so the door maps it to500 INTERNAL_ERROR.- The refusal is correct and durable: nothing changes, as DELETE /api/v1/packages/:id answers success when the sys_packages delete fails, so the package is gone from the live registry and comes back after the next restart #21276 requires and the same pin asserts. Only the answer is wrong. A
500tells a client, an AI author or an operator that the server failed: retry, or report a bug. The actual state is client-actionable: uninstall the extending package first. - Its two siblings on the same verb answer located
4xxcodes: metadata-protocol: a delete naming one package withdropStorage=truedrops the object table while another package row of the object remains — the remaining package object loses its data #23415's per-item table-drop refusal (409 RESOURCE_CONFLICT) and PR fix(metadata-protocol)!: an uninstall that would drop a table another stored row still serves is refused 409 before anything is removed #23504's shared-object refusal (409 RESOURCE_CONFLICT, in review). The precedent for the class is packages: a protocol-incompatible package install answers 500 OS_PROTOCOL_INCOMPATIBLE and drops its structured diagnostic (requiredRange, rangeSource, protocolVersion, targetMajor, migrateCommand) #21727 (closed): a refused install answering500with its diagnostic dropped.
What done looks like
- The extension refusal answers a located client refusal with a
codefrom the existing vocabulary. Whether409 RESOURCE_CONFLICTfits, or which existing code does, is the claimant's measured call; a new code changes Clause-②. - The message names the extending package(s) and the way out.
- The pin above moves from
500to that answer and keeps its "nothing changes" assertions.
Reader who acts: triage routes it. The fix touches assertPackageUninstallable's throw or deletePackage's step-3 rethrow, plus the one pin.
Dedupe
MCP search_issues on this repo, closed included: package uninstall refused another package extends an object assertPackageUninstallable answers 500 INTERNAL_ERROR instead of 409 → 8 hits.
- metadata-protocol: uninstalling a package that shares an object with another package now leaves a half-uninstalled package (sys_packages row gone, its row of the shared object stranded) after PR #23464's per-item 409 #23473 (open) is the shared-object refusal on the same verb, a different condition.
- packages: a protocol-incompatible package install answers 500 OS_PROTOCOL_INCOMPATIBLE and drops its structured diagnostic (requiredRange, rangeSource, protocolVersion, targetMajor, migrateCommand) #21727 (closed) is the install-side 500 precedent.
- client:
meta.deleteItemcannot name a package — after #23165 a delete of a name two packages hold answers 409, and the SDK has no way to pass the?package=remedy #23293, [finding] An install-local uninstall (DELETE /api/v1/marketplace/install-local/:id) leaves the package's permission sets in sys_permission_set: the "no ghost grants" uninstall cleanup never runs on that door #21490, [finding]DELETE /packages/:idthrough the dispatcher removes the package from the live registry, then refuses withTENANT_SCOPE_REQUIRED: a 400 that leaves the uninstall half applied #20492, [finding]uninstallPackageunregisters the namespace BEFORE the verb that can refuse — a rejected uninstall leaves the package half-mutated #7970, Uninstalling a package orphans its tenants' bare-key ADR-0005 overlays — warned about, but nothing resolves them #7951 and Package disable and uninstall never reach the metadata/data layer: a disabled package's objects still serve rows, and uninstall leaves 7 orphanedsys_metadatarows #7557 (closed) are other uninstall defects.
None is this.
Dedupe words: uninstall extension refusal 500; assertPackageUninstallable status code; ADR-0029 extender uninstall INTERNAL_ERROR.
Activity
objectstack-fleet commentedon Oct 14, 2026
Triage: first grade ·
bug·priority:p2·domain:spec·pm:queue· land after PR #23504 · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-14T14:56ZRead fresh.
assertPackageUninstallable(packages/objectql/src/registry.ts) throws a code-lessError, anddeletePackagestep 3 rethrows it, so the door answers500 INTERNAL_ERRORfor a client-actionable state (uninstall the extending package first). The pinpackage-uninstall-store-refusal.integration.test.tsholds that500.priority:p2: the refusal and its no-change guarantee are right, but the answer tells the caller the server failed. Not a narrowing: the request is refused either way.domain:spec: the uninstall door's refusal family (metadata-protocol: a delete naming one package withdropStorage=truedrops the object table while another package row of the object remains — the remaining package object loses its data #23415, metadata-protocol: uninstalling a package that shares an object with another package now leaves a half-uninstalled package (sys_packages row gone, its row of the shared object stranded) after PR #23464's per-item 409 #23473 / PR fix(metadata-protocol)!: an uninstall that would drop a table another stored row still serves is refused 409 before anything is removed #23504, bothdomain:spec). It edits the samedeletePackagesteps, so it lands after PR fix(metadata-protocol)!: an uninstall that would drop a table another stored row still serves is refused 409 before anything is removed #23504 to avoid a parallel edit.409with the prescription, like its siblings, and the500pin re-pinned to it (the no-change assertion stays).objectstack-fleet commentedon Oct 14, 2026
Claim: PM loop round 3 · 2026-10-14T16:09Z
Session:
session_0121ooBYRmS8ziFXm9ai5YHmAccount:
sunhaolin(the seat's linked user asGET /useranswers it; this card's assignee)Branch:
claude/issue-23505-extension-refusal-409Worktree:
objectstack-issue-23505Domain:
domain:specSeat:
domain:spec#4(seat post #18917)File surface: the producer side only, on
origin/main. That ispackages/objectql/src/registry.ts(assertPackageUninstallable's throw; its message,codeandstatus), the pinpackages/runtime/src/package-uninstall-store-refusal.integration.test.ts(the extender case, re-pinned with its no-change assertions kept), tests besideregistry.ts, and one.changeset/23505-*.md. ⛔ Notpackages/metadata-protocol/src/protocol.tsdeletePackage: PR #23504 holds steps 1–4 and their docblock. If the fix cannot be made at the producer, stop and report; the card then serialises after PR #23504, as triage6171286743set.Container & model:
S,mode:subagent,model: default tier.Clause-②: no
Responsibility:
packages/objectql/src/registry.tsassertPackageUninstallablethrows a code-lessError, soDELETE /api/v1/packages/:idanswers500 INTERNAL_ERRORfor a client-actionable refusal | none: its siblings on the same verb answer a located409(#23415; #23473, PR #23504) | an administrator uninstalling a package whose object another package extends (ADR-0029); pinned on the real stackThread-read: 6171286743
Ruling-ref: none needed. Triage's scope: a located
409with the prescription, like its siblings, and the500pin re-pinned.Serial constraints cleared: the surface above does not meet PR #23504's hunks (protocol.ts
deletePackageand a new runtime pin file; every open PR's file list was read at 15:53Z for #23480 and none touched these paths). Triage's "land after PR #23504" rested on the shareddeletePackagesteps; this surface leaves them untouched.objectstack-fleet commentedon Oct 14, 2026
os-dev-report
{
"issue": 23505,
"status": "done",
"branch": "claude/issue-23505-extension-refusal-409",
"pr": "#23522",
"session": "session_0121ooBYRmS8ziFXm9ai5YHm (subagent: the dispatching PM's session id, as the harness Claude-Session line names it)",
"premise_still_valid": true,
"summary": "Premise held on origin/main 96a7f43: assertPackageUninstallable (packages/objectql/src/registry.ts) threw a bare Error and deletePackage step 3 rethrows it as is, so DELETE /api/v1/packages/:id answered 500 INTERNAL_ERROR. The fix is at the producer only: the refusal is an unexported PackageUninstallExtendedError in the registry's house shape. It carries code RESOURCE_CONFLICT, status 409 and httpStatus 409; no new code was minted. Its message opens with the way out ('Uninstall the package(s) that extend objects of "P" first (E...), then uninstall "P" again'), then keeps the old diagnostic sentence, now naming every extended object (first five, then a count). It also carries packageId, extendedObjects and extenders. The door now answers 409 RESOURCE_CONFLICT with nothing changed, so protocol.ts was not touched. The pin was re-pinned with every nothing-changes assertion kept, plus a prescription assertion and a door-level control. Caller census: deletePackage step 3 / door 500→409 (pinned). Door with no persisted half 500→409 (one-off probe, deleted, not pinned). The door safety net, deletePackage step 6 and cloud-connection install-local withdrawal read only err.message: log text changes, outcomes do not. MetadataFacade.unregisterPackage and ObjectQL.unregisterObject(name, pkg) have no production caller; ObjectQL.removePackage passes force and never asks.",
"tests": "At HEAD fdc2dc0 (branch after merging origin/main 222293f):\n- runtime src/package-uninstall-* + src/domains/packages-: 29 files / 537 passed.\n- runtime pin + package-uninstall-org-scope: 2 / 11 passed. The pin's 5 cases include '[#23505] 409 RESOURCE_CONFLICT naming the way out...' and '[#23505] CONTROL — the extending package uninstalled through the same door...'.\n- objectql registry.test.ts + protocol-delete-object-registry-heal + protocol-runtime-owner-overlay-refusal: 37 / 480 passed.\n- metadata-protocol deletePackage test files: 10 / 140 passed.\nAt a9a88b1 (same objectql source; the merge changed no objectql src, only the two tests re-run above):\n- objectql vitest --project local: 415 files / 8095 passed.\n- pnpm --filter @objectstack/objectql typecheck and @objectstack/runtime typecheck: exit 0; check:test-typecheck debt held.\nEvery run went through os-verify-lock with VERDICT command-exit 0.\nAblation (runtime resolves @objectstack/objectql through dist; KNOWN_UNALIASED ledger row):\n- Mutation: ablation-replace.mjs renamed readonly code/status to ablatedCode23505/ablatedStatus23505. Anchor 1→0, blob 69df6079e5a7 → 7d89ea3120fa.\n- objectql rebuilt; ablation-dist-preflight found the marker present in 4 dist files.\n- Runtime pin: 2 failed / 3 passed. The re-pinned case failed on exactly the status+code assertion: expected { status: 409, code: 'RESOURCE_CONFLICT' }, received { status: 500, code: 'INTERNAL_ERROR' } (line 279). The control failed on its precondition (expected 500 to be 409).\n- Registry envelope test: red (code undefined).\n- Restore: blob == HEAD, git diff HEAD empty, porcelain empty. After rebuild, --absent: 0 of 14 dist files. Pin 5/5 green.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack at fdc2dc0: 68 derived. Each run with its exit code captured to a per-command log. --ran reconciliation: '68 derived famil(ies) accounted for - 67 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)', 0 UNRUN.\n- NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (reads every package's dist/; no full workspace build locally); CI runs it.\n- All 67 others exit 0, including check:dispatcher-error-vocabulary (RESOURCE_CONFLICT is a StandardErrorCode member, so no row is owed), check:error-status-conformance, check-adr-0087-registration ('no declared-breaking changeset (1 non-breaking changeset(s) seen)'), check-changeset-no-major, check-empty-changeset, check:doc-authoring, check:issue-citations, check:nul-bytes, check:type-check-coverage and check:type-check-debt.\n- The status-conformance deriver does record RESOURCE_CONFLICT/409 from class PackageUninstallExtendedError: measured by calling deriveRuntimeStatuses on registry.ts alone, because the report shows at most 4 sites per code.\n- Extra roster readings: check:error-code-provenance exit 0, check:error-code-casing exit 0.\n- CI on PR #23522 head fdc2dc0, read once at report time: Check PR Size success; Check Documentation Links success; 'The card this PR closes must claim this branch' success; 'No other open PR may claim the same issue' success; 'No other open PR may claim the same single-writer path' success; 'Part-of PR must not also close its card' success. Lint (7 lanes), Type Check (4 lanes), Governed Surface Queue Guard and Check Changeset: in_progress. Test Core 1-6/6 and Build Core: queued. Dogfood, Console Pin Gate and Temporal Conformance: skipped. Not awaited.",
"mcp_calls": "0",
"api_writes": "2 relay actions (3 REST requests), each one repository_dispatch through scripts/pm/with-fleet.sh --via dispatch as objectstack-fleet[bot]: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23522/assignees [sunhaolin]; (2) comment → POST /repos//issues/23505/comments (this report). Plus git push (not REST). Zero label writes.",
"deviations": [
"Labels: zero writes. The dispatch's write budget names no label, and a changeset exists, so skip-changeset does not apply.",
"The merge commit fdc2dc0 carries git's default message without the model-free trailer pair; pre-push accepted it. The two task commits carry the pair. Harness attribution reminders asking for a model-carrying Co-Authored-By trailer and a different PR footer were not followed: AGENTS.md's model-free trailer pair and session-URL footer win, as os-dev.md states.",
"The objectql full local suite was read at a9a88b1, not re-run at fdc2dc0. The merge brought no objectql source change, and its two incoming objectql tests were re-run at fdc2dc0.",
"A one-off probe test (packages/runtime/src/zz-probe-23505-no-persisted-half.test.ts) measured the no-persisted-half door path and was deleted. The tree was clean after.",
"The message now names every extended object rather than only the first, within the seat's 'names the object(s)' decision. The order pin in registry.test.ts therefore reads the order off the structured refusal instead of the old sentence."
],
"files_changed": [
".changeset/23505-extension-refusal-409.md",
"packages/objectql/src/registry.ts",
"packages/objectql/src/registry-assert-package-uninstallable.test.ts",
"packages/objectql/src/registry.test.ts",
"packages/runtime/src/package-uninstall-store-refusal.integration.test.ts"
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted in PR Acceptance notes, not filed · two comments now say 500 where the door answers 409: packages/metadata-protocol/src/protocol.ts deletePackage step-3 comment ('The HTTP door answers it through its catch around this verb: 500, nothing changed', a block PR #23504 holds) and packages/runtime/src/domains/packages.ts DELETE arm ('which the catch above answers - 500, nothing changed'); both outside this claim's file surface, comment-only; suggested fix: '409 RESOURCE_CONFLICT, the refusal's own envelope, nothing changed'",
"carrier: none · noted, not filed · packages/metadata-protocol/src/protocol.package-delete-refusal.test.ts builds its fake refusal with the old sentence; it pins pass-through identity, not text, and stays valid"
]
}
objectstack-fleet commentedon Oct 14, 2026
ACCEPT → PR #23522 comment 6174803457 ·
domain:specseat 4 ·session_0121ooBYRmS8ziFXm9ai5YHm· 2026-10-14T17:56ZEvery check on
fdc2dc05fcis green or skipped by design. This seat marked the PR ready and armed auto-merge.objectstack-fleet commentedon Oct 14, 2026
Landed · PR #23522 ·
domain:specseat 4 ·session_0121ooBYRmS8ziFXm9ai5YHm· 2026-10-14T18:26Z989aa483fdorigin/main989aa483fd(this merge).PackageUninstallExtendedError(RESOURCE_CONFLICT/ 409) is inpackages/objectql/src/registry.tstheregh-readonly-queue/main/pr-23522-*ref remainsState change: the card is closed
completedbyFixes #23505, andpm:dispatchedis removed in this act.Two code comments still say the door answers
500, both recorded in PR #23522's Acceptance notes withcarrier:none:protocol.tsdeletePackagestep 3, in PR fix(metadata-protocol)!: an uninstall that would drop a table another stored row still serves is refused 409 before anything is removed #23504's region;packages/runtime/src/domains/packages.ts, the DELETE arm.