Repository navigation
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
Description
Filing gate ①, class (a): a reproducible defect with a named producer.
reach:the uninstall door (deletePackage, served atDELETE /api/v1/packages/:id).- Measured in process on the real stack: real ObjectQL and protocol, better-sqlite3 tables.
- Read through to the HTTP answer from source (
400 PACKAGE_DELETE_PARTIAL), not driven over HTTP.
This card carries the uninstall half of #23415's family. #23415 (PR #23464) keeps the metadata delete door's 409 refusal, which stops the data loss on both doors.
Filed by domain:spec seat 4 (#18917) · sunhaolin · session session_0121ooBYRmS8ziFXm9ai5YHm. Sources: #23415's dev report (os-dev-report comment on #23415, open_questions[0]) and PR #23464's contract review (6167575307, ③, which escalated it to this seat). ⛔ Not a claim. Grade and lane are triage's.
What happens
deletePackage deletes each of the package's sys_metadata rows with packageId set and, unless keepData: true, with dropStorage: true. Take a package P whose object another package Q also holds.
- Before PR fix(metadata-protocol)!: a delete with dropStorage drops an object's table only with its last stored row, refused 409 while another row remains #23464: that per-item delete dropped the object's table, so Q's records were lost.
- After PR fix(metadata-protocol)!: a delete with dropStorage drops an object's table only with its last stored row, refused 409 while another row remains #23464: the per-item delete is refused
409 RESOURCE_CONFLICT, and the uninstall collects it infailed[](success: false; the HTTP door answers400 PACKAGE_DELETE_PARTIAL). By then thesys_packagesrow is already gone (step 4, the first durable step), and the package's other rows are deleted. P's row of the shared object, its table and its records stay. - So the result is a half-uninstalled package: P is no longer installed, but a row bound to P still serves the object.
- The refusal text names
dropStorage, which an uninstall caller never sent.
What done looks like (the shape is the owner's call, possibly a decision)
Two shapes are in sight. They differ on a destructive default, so the claimant may need a ruling:
- B, refuse the whole uninstall. A pre-flight before step 4 answers
409when any of P's active object rows shares the object with another row, so a refused uninstall leaves everything as it was. The prescription would bekeepData: true, or removing the other package's row first. This is the dev's and the contract reviewer's recommendation, and [finding]uninstallPackageunregisters the namespace BEFORE the verb that can refuse — a rejected uninstall leaves the package half-mutated #7970 (closed) fixed the same "refused after the first durable step" shape foruninstallPackage. - C, uninstall P and keep the shared table. Delete P's row, drop only the tables P alone held, keep the shared one because Q still serves it, and name the kept table in the response.
- This seat's concern with B: dropping storage is the uninstall's DEFAULT, not an opt-in the caller sent.
keepData: truekeeps EVERY table P alone held, so B makes an administrator choose between orphaning all of P's private tables and deleting Q's definition. C is not the skip route 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 rejected, because that rejection was about ignoring an explicitdropStorage=trueat the metadata door.
Either way: a real-stack pin of the two-package uninstall, and the unchanged single-package uninstall as the control.
Dedupe
MCP search_issues on this repo, closed included: deletePackage uninstall shared object another package row stranded sys_packages deleted partial uninstall dropStorage keepData → 11 hits.
- metadata-protocol: a delete naming one package with
dropStorage=truedrops the object table while another package row of the object remains — the remaining package object loses its data #23415 (open) is the metadata delete door: this card's parent family, not this card. - [finding]
uninstallPackageunregisters the namespace BEFORE the verb that can refuse — a rejected uninstall leaves the package half-mutated #7970 (closed) isuninstallPackage's refusal after the first durable step: the same shape on another verb, already fixed. - Uninstalling a package after a kernel reboot leaves its permission set's sys_permission_set row behind (managed_by admin, package_id null) #23299, [finding] After an install-local uninstall, a later hot install of ANOTHER package re-projects the uninstalled package's permission set, which survives the restart as an orphan package-managed row #21576, [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,
protocol.deletePackagefinds zerosys_metadatarows the data plane finds 3 of — uninstall leaves orphaned rows (persistence half of #7557) #7705, 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, Product question: an uninstall with no organizationId deletes EVERY organization's rows for that package (measured 5 of 5, including a foreign org's) #7780, Uninstalling a package orphans its tenants' bare-key ADR-0005 overlays — warned about, but nothing resolves them #7951 and runtime /packages domain 持久化不对称:enable/disable/uninstall 直写 in-memory registry,install/update 却走 protocol service 落 sys_packages #5047 are other uninstall leftovers: permission sets, overlays, organizations, registry persistence.
None is this.
Dedupe words: deletePackage shared object partial uninstall; uninstall stranded row sys_packages gone; keepData shared table uninstall refusal.
Activity
objectstack-fleet commentedon Oct 15, 2026
Release:
session_0121ooBYRmS8ziFXm9ai5YHm·domain:specseat 4 (#18917) ·sunhaolin· 2026-10-15T02:56Zneeds-user-decision, with the assignee cleared in the same act.Resume-from:none. The work is done, 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 at head7d36e8d205is the deliverable. ⛔ Do not re-dispatch a dev for this card.Hand-off note for the seat that lands it. Guard: the next
domain:specseat.CONTRACT_REVIEW_TIERpassed (6172644842), and the seat record is ACCEPT (6172664218). Both are on head7d36e8d205..changeset/23415-drop-storage-other-rows.md. The request is6171760091, made under ruling D on [finding] theskip-changesetlabel suppresses the DELIBERATE-CORRECTION refusal whose own text says 「no label and no diff shape makes that safe」 — declared contract against enforced behaviour #18375 (5729188290).Check Changesetstays red by design: it is not a required context and does not run onmerge_group. ⛔skip-changesetis not applied.main, a dev round mergesmaininto it with a merge commit.7d36e8d205insidepackages/metadata-protocol/src/protocol.ts, re-read the pre-flight before step 4 ofdeletePackage.mergeable_stateis clean:pr_readythenautomerge_enable.Fixes #23473closes the card.Decision: confirm the correction to a pending release note
The problem in one line. The next release note would tell upgraders that an uninstall blocked by a shared object "leaves the object's row and table in place" and reports it in
failed[]. After PR #23504 that is false, because the whole uninstall is refused before anything is removed. Only the maintainer's written word on the PR can correct someone else's pending note.Governing text: ruling D on #18375 (
5729188290, 「同意」 2026-09-18), and route 0 of theRequire a changesetstep in.github/workflows/pr-automation.yml. A PR that edits another change's pending changeset takes noskip-changeset: "Write the confirmation on the PR — name the note and what changed under it, and get it confirmed there".Protocol change: none. The question is one sentence of release-note text. The refusal itself is PR #23504's reviewed change (
6172644842).Premise, with its re-check: the 23415 note is on
mainand not yet released. Re-check:git cat-file -e origin/main:.changeset/23415-drop-storage-other-rows.md && echo pending.409, and nothing is removedfailed[]partial uninstall that the release no longer performsmain's current behaviour: the package record is removed while its row of the shared object stays, listed infailed[]. That is the half-uninstall this card recordsRuled:here, claims the card and lands PR fix(metadata-protocol)!: an uninstall that would drop a table another stored row still serves is refused 409 before anything is removed #23504 by the hand-off steps above. On B, that seat sends a dev for a text round that removes the 23415 edit, then lands. On C, the card moves topm:on-holdwith aRestart-when:naming the maintainer's condition.维护者速读
409),什么都不删。顺带把一条尚未发布的发版说明(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 那条)改成与新行为一致的一句。skip-changesetlabel suppresses the DELIBERATE-CORRECTION refusal whose own text says 「no label and no diff shape makes that safe」 — declared contract against enforced behaviour #18375 裁决 D,改别人待发的说明必须由你在 PR 上亲口确认。6172644842,席位核对 ACCEPT6172664218。os-decision-facets
DELETE /api/v1/packages/:id可达;未点名现用者。409一致;B 会让 AI 按failed[]写处理逻辑,而实际收到的是整单拒绝。skip-changesetlabel suppresses the DELIBERATE-CORRECTION refusal whose own text says 「no label and no diff shape makes that safe」 — declared contract against enforced behaviour #18375 ruling D (5729188290) sets the route. None of the 8 ADR sections the probe names answers this note's content. thread: noobjectstack-fleet commentedon Oct 15, 2026
Decision-box entry: passes · triage seat ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T03:01ZEntry check on the release
6183361784. These are all present:skip-changesetlabel suppresses the DELIBERATE-CORRECTION refusal whose own text says 「no label and no diff shape makes that safe」 — declared contract against enforced behaviour #18375,5729188290, and route 0 ofpr-automation.yml's changeset step);7d36e8d205, with the contract review PASS6172644842and the seat's ACCEPT6172664218;It passes.
The ask is a single written word on PR #23504: confirm the correction to the pending note
.changeset/23415-drop-storage-other-rows.md. Under the fix, an uninstall blocked by a shared object is refused whole (409) before anything is removed. The pending note says it "leaves the object's row and table in place".The grade stays p1 +
target:v18. The hand-off note names the nextdomain:specseat for the landing steps.objectstack-fleet commentedon Oct 15, 2026
Ruling: maintainer 「确认」 · letter A · 2026-10-15T04:15Z · said to
domain:specseat 4's sessionsession_0121ooBYRmS8ziFXm9ai5YHm(sunhaolin) in its chat, in answer to the seat's explanation of PR #23504. Carried by that session with provenance under ruling D on #18375; not a director batch.6184266311records the confirmation of the.changeset/23415-drop-storage-other-rows.mdcorrection, as ruling D requires.needs-user-decision→pm:queue, with no assignee.6172644842and seat record ACCEPT6172664218, both on head7d36e8d205.domain:specseat claims the card and lands PR fix(metadata-protocol)!: an uninstall that would drop a table another stored row still serves is refused 409 before anything is removed #23504 by the hand-off steps in6183361784.maininto the head.main.git merge-treeis clean.packages/metadata-protocol/src/protocol.tschanged 9 times onmainsince the merge base, among them fix(metadata-protocol): the read naming no package serves the row the package-less save writes, beside a package-unbound row (S6) #23489 (the package-less read) and fix(metadata-protocol)!: a save into a second package of an object a runtime-authored row owns is refused (ADR-0029 D9.9) #23510.mainin, then re-reads the pre-flight againstpackagelessAddresson the merged head before arming the PR.target:v18.objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop, temporary re-seat · 2026-10-15T04:30Z · landing only: PR #23504, already reviewed and confirmed
Session:
session_0121ooBYRmS8ziFXm9ai5YHmAccount:
sunhaolin(the seat's linked user asGET /useranswers it; this card's assignee)Branch:
claude/issue-23473-uninstall-shared-object-refusal(head7d36e8d205, unchanged since the seat record)Worktree: none. No dev is dispatched: the work is done.
Domain:
domain:specSeat:
domain:spec#4(seat post #18917; re-seat marker6184406254, on the maintainer's word 「临时回座把 #23504 落地」)File surface: none. This claim lands the reviewed head as it is. The merge queue builds the PR on current
mainand runs the full PR battery there. The seat readsdeletePackage's pre-flight on that merged tree before arming. If that reading finds a break, the seat stops and the card goes back with the reading.Container & model: n/a. No dev and no reviewer is needed: the narrowing's
CONTRACT_REVIEW_TIERreview PASSed on this exact head (6172644842), and a base merge would move the head off that record.Clause-②: no
Responsibility: unchanged from the first claim
6168221878.Thread-read: 6184273013
Ruling-ref:
6184273013(the maintainer's 「确认」, carried; on PR #235046184266311) under ruling D on #18375; lane decision6168058499(shape B).Serial constraints cleared: I read every open PR's file list in this act. Only PR #23469 (another lane, draft,
marchtian; already named in the first claim6168221878) touchespackages/metadata-protocol/src/protocol.ts. InsidedeletePackageit changes the step-2 row read (engine.find→findEnvironmentRows) and the per-item loop after step 4. Neither is the pre-flight that PR #23504 adds between steps 3 and 4, andgit merge-treeofmain+ PR #23504 + PR #23469 is clean. PR #23504 lands first; PR #23469 mergesmainafter it. The landing reading also covers the 9maincommits to that file since this PR's merge base.Resume-from: none (the newest
Release:,6183361784, carries none).objectstack-fleet commentedon Oct 15, 2026
Claim amended ·
domain:specseat 4 ·session_0121ooBYRmS8ziFXm9ai5YHm· claim6184449232· 2026-10-15T05:05ZWhat happened. PR #23504 left the merge queue on a CI failure.
38682506920on the queue commit5e97584201, which ismain8d05456cbeplus this PR.Test Core (6/6)failed inpackages/runtime(pnpm run testexited 1).8d05456cbealone the same shard is green, so the failure comes from this PR meetingmain.ci-failure.mjs: annotations only), so the failing test is not named yet.The landing claim becomes a dev round. Changes to
6184449232:objectstack-issue-23473.deletePackagesteps 1–4 inpackages/metadata-protocol/src/protocol.ts, this PR's pin (packages/runtime/src/package-uninstall-shared-object.integration.test.ts), the two changesets, and a merge ofmaininto the branch (merge commit, no rebase, no force). A test thatmainadded is changed only if it asserts behaviour this PR deliberately replaces, and the report must name it.M,mode:subagent, default tier.Prime suspect. It is a reading, not yet a measurement.
package-uninstall-store-refusal.integration.test.tschanged onmainwith PR #23522 (#23505). Its CONTROL case uninstalls the extending package through the same door. The new pre-flight may read the extending package's row of the extended object as a second holder, and refuse.Reviews owed. The merge moves the head, so the
CONTRACT_REVIEW_TIERreview and the seat record are re-done on the new head before the queue.objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{
"issue": 23473,
"round": "merge round after merge-queue ejection (run 38682506920); claim 6184449232 amended by 6184860304",
"status": "done",
"branch": "claude/issue-23473-uninstall-shared-object-refusal",
"pr": "#23504",
"head": "addcca0cf5fc63ba01412d211c8c93dc8dbf8941",
"merge_commit": "192d2051daaf74b40f9f5727449000f60c99646c (git merge origin/main at 8d05456; no conflict, protocol.ts auto-merged; no rebase, no amend, no force)",
"session": "session_0121ooBYRmS8ziFXm9ai5YHm (subagent: the dispatching PM session)",
"premise_still_valid": true,
"summary": "The queue failure was the PR's own pin meeting main's #23510 (222293f, ADR-0029 D9.9), not the pre-flight: on the merge commit 192d205 the three P-and-Q cases of package-uninstall-shared-object.integration.test.ts ('without keepData: refused 409 ...', 'the way out, keepData: ...', 'the way out, P's own row first: ...') failed in setup at h.publish(Q, SHARED) with "Error: Cannot layer object 'probe_ticket': the overlay is bound to package 'com.probe.q', but the object is owned by package 'com.probe.p'. An object has exactly one registry entry ..." (OBJECT_OVERLAY_PACKAGE_MISMATCH / 422 at the save door). The prime suspect is falsified: package-uninstall-store-refusal.integration.test.ts, CONTROL included, passes on the merged tree. Premise measured: the shared state still arises, through today's doors (a draft saved into Q before P's row exists and promoted after it, by publishMetaItem or POST /packages/Q/publish-drafts: rows [P, Q], registry P; a package-less save first, then P and Q: rows [P, Q, package-less]) and as rows stored before D9.9, which boot leaves at rest; on each door-reached state the uninstall of P without keepData answered 409 RESOURCE_CONFLICT and removed nothing. Fix, inside the claim's surface: the pin's twoPackageObject saves P's row through the door and seeds Q's row straight into the store as a row stored before ADR-0029 D9.9 (the save's shape, checksum by hashSpec), as #23510 re-seeded its two sibling fixtures; every case assertion, the package-less case and the CONTROL are unchanged. protocol.ts and both changesets are byte-identical to the reviewed head's delta (the 23415 TO sentence untouched). I seeded rather than use the draft-promotion door because that door is a D9.9 gap and a fixture built on it would break again when the gap closes; the seat may prefer the door path.",
"fix": "packages/runtime/src/package-uninstall-shared-object.integration.test.ts only (commit addcca0): new seedStoredRow(name, packageId) helper; seedUnboundRow now calls it (same id, label and body as before); twoPackageObject seeds Q's row instead of publish(Q). No protocol.ts change, no new error code, no route change, no Clause-② change.",
"producers_measured": [
"save door, P then Q: Q refused 422 OBJECT_OVERLAY_PACKAGE_MISMATCH; rows [P]",
"save door, P then a save naming no package: addressed to P's row; rows [P]",
"save door, a save naming no package first, then P, then Q: all succeed, registry owner the sys_metadata sentinel; rows [P, Q, package-less]; uninstall P without keepData 409, nothing removed",
"Q draft saved before P's row, P publishes, Q promotes via publishMetaItem (package Q): success; rows [P, Q], registry P; uninstall P 409, nothing removed",
"same, promoted via POST /api/v1/packages/com.probe.q/publish-drafts at HttpDispatcher.handlePackages: 200, outcome published, failed []; rows [P, Q], registry serves P",
"same, via publishPackageDrafts in process: outcome published; rows [P, Q]",
"duplicatePackage P to Q with targetNamespace qq: copy renamed qq_probe_ticket; rows [P] (the kept-name case is #23510's reading: failed with the D9.9 text; not re-measured here)",
"installPackage writes no object sys_metadata row (code reading); importUnmanagedPackage refuses a name an environment row holds before writing (409, code reading); loadMetaFromDb counts a mismatched stored row in errors and leaves it stored (code reading)"
],
"tests": "All at addcca0 unless named, --maxWorkers=2, each under scripts/pm/os-verify-lock.sh, verdict from its VERDICT command-exit line. RED BEFORE at 192d205: pin + two sibling uninstall integration files: 'Tests 3 failed | 13 passed (16)', VERDICT command-exit 1 (the three P-and-Q cases, setup error quoted in summary). AFTER: pin 'Tests 5 passed (5)' exit 0. Runtime uninstall sweep (every src/package-uninstall-.integration.test.ts + every src/domains/packages-.test.ts): 'Test Files 30 passed (30) / Tests 542 passed (542)' exit 0. Runtime local project whole: one-shot run hit the ~10 min foreground cap (exit 124, no verdict, not a measurement), then 4 vitest shards: 96/96/96/96 files passed, tests 1517 + 1234 (4 skipped) + 1694 (15 skipped) + 1332, all exit 0. Runtime typecheck (tsc --noEmit + check:test-typecheck) exit 0, 'OK — ... 27 file(s) / 190 error(s) / 68 pinned signature(s) held'. Metadata-protocol whole suite (3 shards): 80 + 78 (2 skipped) + 78 (1 skipped) files passed; tests 1483 + 25547 (12 skipped) + 1391 (7 skipped), all exit 0; typecheck exit 0. CLI (for the Test Core 2/6 reading): unit tier 2 shards 145 + 145 files, 2416 + 1908 tests, exit 0; integration tier 131 files in 9 slices, all exit 0 (one slice first killed by my own 290 s timeout, exit 124, re-run green); CI's exact slice OS_TEST_SHARD=1/3 both tiers: 'Test Files 141 passed (141) / Tests 1956 passed | 1 skipped', exit 0. ABLATION at addcca0 via scripts/ablation-replace.mjs WRAP mode: anchor 'if (others.length === 0 && !at.unbound) continue;' 1 -> 0, blob 04eee5b790 -> e70c5d7193, replacement adds a marker condition so no row refuses; metadata-protocol rebuilt; ablation-dist-preflight: marker in dist/index.js and dist/index.cjs; predicted 2 red / 3 green, observed 'Tests 2 failed | 3 passed (5)': cases 1 and 2 answered 400 PACKAGE_DELETE_PARTIAL, registryRemoved true, the object in failed[] with the per-item RESOURCE_CONFLICT text (so the pre-flight refuses the same rows the per-item delete refuses). Restore: blob == HEAD 04eee5b7906ebe40733101d3f8815a5b5ececa30, git diff HEAD empty, whole-tree git status --porcelain empty, rebuilt, --absent: marker in none of 24 built files, pin 5/5 green. A first mutation spelling was refused by the tool (replacement contained the anchor, count could not drop) and restored at once: a no-op, nothing measured. The red-before leg of 'revert the fix' is the merge-commit reading above, since the fix is fixture-only.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at addcca0: 65 commands (stderr: tree of objectstack-ai/objectstack at addcca0; 4 paths vs merge base 8d05456). Each run with its exit captured before any pipe: 63 exit 0; 'node scripts/check-empty-changeset.mjs --base origin/main' exit 1 on .changeset/23415-drop-storage-other-rows.md, the maintainer-confirmed deliberate correction (6184266311), red by design; 'pnpm check:dual-build-cjs-loads' first exit 3 (PREREQUISITE NOT MET, packages without dist/), re-run exit 0 ('110 published require entry point(s) across 68 package(s) load'). --ran: 'Run reconciliation — 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN', exit 0. CI check-runs on addcca0: 31 success, 6 skipped, 3 failure. Green: Build Core, TypeScript Type Check (+ four Type Check lanes), Lint & Repo Gates (+ its lint lanes), Governed Surface Queue Guard, Dogfood Regression Gate, Test Core (1/6), (3/6), (4/6), (5/6), (6/6) (6/6 is runtime + metadata-protocol, the queue's failing shard). Failure: Check Changeset (same 23415 note, red on 7d36e8d too, expected); Test Core (2/6) (CLI slice 1/3, run 38686154595) and the Test Core rollup. Test Core (2/6) NOT REPRODUCED: the job log is unreadable from this container (gh run view --log-failed: run in progress then; the log download host productionresultssa10.blob.core.windows.net is refused by the egress proxy; ci-failure.mjs: annotations only, 'command (.../packages/cli) pnpm run test exited (1)'), so the failing test is not named. Evidence it is not this PR: CI's exact slice passes locally (above); the whole CLI suite passes locally; the same shard was green on 7d36e8d; the queue run on 5e97584 had Test Core (2/6) success, and 5e97584 differs from addcca0 only in the runtime pin file, which no CLI test reads (git grep: no hit); the locally computed PR partition puts '@objectstack/cli 1/3' on shard 2/6. Nothing was changed for it. Rerunning that failed job is a write outside my budget: left to the seat.",
"mergeable_state": "blocked (read after the push: draft=false, mergeable=true, head addcca0)",
"pr_body": "Not PATCHed. The current body is stale (its first acceptance note says the 23415 note is not edited; it is, with the maintainer's confirmation) and predates this round. Replacement body: /tmp/claude-0/-home-user/a2360ed0-e03c-558a-9d23-40c33afc0152/scratchpad/issue-23473/pr-body-replacement.md",
"mcp_calls": "0",
"api_writes": "1 — POST /repos//issues/23473/comments (this report, via scripts/pm/post-stamped.mjs). Plus two git pushes (not REST): 192d205, addcca0. No label write: the PR's labels were not in this dispatch's scope.",
"deviations": [
"Ablation target: the dispatch asked to revert the fix and see red. The fix is fixture-only, so that red is the merge-commit reproduction; the one ablation instead disabled the pre-flight to prove the re-seeded fixture still bites (os-dev: ablate only tested behaviour).",
"origin/main moved to be48c5e after the merge; not merged again (AGENTS.md §10: merge only on conflict, main-caused red or queue removal).",
"The re-review owed on the new head (claim amendment 6184860304) is the seat's; nothing was readied, queued or labelled."
],
"open_questions": [
{
"question": "Fixture shape for the P-and-Q cases: seeded legacy row (as landed) or the door path that still reaches the state today (Q's draft saved before P's row, promoted after)?",
"options": [
"A keep the seeded row: robust to the D9.9 gap being closed, same as #23510's two sibling fixtures",
"B build Q's row through draft-then-promote: a real door today, but the fixture breaks again when D9.9 closes that gap"
],
"recommendation": "A, because the pin's subject is the uninstall, not how the row got in, and the door-reach is recorded in the PR body as a measurement"
}
],
"out_of_scope_findings": [
"class: b · reach: public door — POST /api/v1/packages/com.probe.q/publish-drafts at HttpDispatcher.handlePackages answered 200 (outcome published, failed []) and stored an active row bound to com.probe.q of an object a runtime-authored row of com.probe.p owns; registry still serves P · evidence: draft saved into Q (saveMetaItem mode draft) before P's row existed, then P saved, then Q promoted; publishMetaItem with package Q does the same; the save door refuses that same row 422 OBJECT_OVERLAY_PACKAGE_MISMATCH · Seam: spec:ADR-0029 D9.9 overlayPackageMismatchError → runtime:promoteDraftForPublish / publishPackageDrafts (packages/metadata-protocol/src/protocol.ts), registry write-through then refused in applyObjectRegistryMutation · same family as the D9.9 close-out named in #23510's acceptance notes: fold into it, not a single-point card · dedupe words: D9.9 draft promote second package; publish-drafts OBJECT_OVERLAY_PACKAGE_MISMATCH; runtime-authored owner draft; promoteDraftForPublish classifyObjectContribution",
"carrier: #23510's acceptance notes (D9.9 family close-out, the seat) · the package-less-owner case re-measured on 192d205: a save naming no package, then saves into P and Q, all succeed, rows [P, Q, package-less], registry owner sys_metadata · noted, not filed"
]
}
objectstack-fleet commentedon Oct 15, 2026
ACCEPT → PR #23504 comment 6187493234 ·
domain:specseat 4 (temporary re-seat) ·session_0121ooBYRmS8ziFXm9ai5YHm· 2026-10-15T08:06ZThis round was the queue failure. The PR's own fixture met
main's ADR-0029 D9.9 refusal. The pre-flight did not cause it.mainwas merged in, and Q's row is now seeded as a row stored before D9.9. The new head isaddcca0cf5. TheCONTRACT_REVIEW_TIERreview PASSed on it (6187465541).Test Core (2/6)CLI failure passed on its single re-run.The seat arms the PR now.
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{
"issue": 23473,
"round": "second merge round after merge-queue ejection (queue run 38703520759, queue commit 75e7900); claim 6184449232 amended by 6184860304",
"addendum_to": "6187145964",
"status": "done",
"branch": "claude/issue-23473-uninstall-shared-object-refusal",
"pr": "#23504",
"head": "4f639e56b06075f522098437d04b08dc06075c11",
"merge_commit": "ce7a65df2c (git merge origin/main at 23e689d; no conflict; protocol.ts auto-merged; this PR's delta to protocol.ts byte-identical before and after the merge; no rebase, no amend, no force)",
"session": "session_0121ooBYRmS8ziFXm9ai5YHm (subagent: the dispatching PM session)",
"premise_still_valid": true,
"summary": "Reproduced on merge commit ce7a65d: 4 of the pin's 5 cases failed in setup. These are every case that seeds a stored row: 'without keepData: refused 409 ...', 'a package-less row of the object counts as another holder', 'the way out, keepData: ...' and 'the way out, P's own row first: ...'. The error was "Error: Unknown field 'organization_id' on object 'sys_metadata'" at seedStoredRow (package-uninstall-shared-object.integration.test.ts:156). The CONTROL passed. Cause: #23469 (dd29bbe, ADR-0131 D6/D7) dropped the organization column from the sys_metadata family, and with it organization_id: null from the sibling pin's seed. Fix in commit 4f639e5: the pin's seed follows main's row shape (meta-delete-drop-storage-other-rows.test.ts on main) and no longer writes the column. Nothing else changed: the pre-flight, protocol.ts and both changesets are as reviewed, and the 23415 TO sentence is untouched. Address-read comparison on the merged tree: the pre-flight and deleteMetaItem both still call the one packagelessAddress. The pre-flight passes (type, name, undefined, 'active'); deleteMetaItem passes (singularTypeForRepo, name, member, targetState). #23469 changed only that function's body: its where clause was { type, name, state, organization_id: null, ...member } via engine.find, and is now { type, name, state, ...member } via findEnvironmentRows, the seam that drops a legacy organization's rows. Both callers therefore moved together. deletePackage's step-2 read now goes through the same seam, and its per-item loop lost the legacy-organization branch, so the pre-flight's input rows and the per-item delete's rows are the same set.",
"fix": "packages/runtime/src/package-uninstall-shared-object.integration.test.ts only (commit 4f639e5): organization_id: null removed from seedStoredRow's insert. No protocol.ts change, no new error code, no route change, no Clause-② change.",
"tests": "All at 4f639e5 unless named, --maxWorkers=2, each under scripts/pm/os-verify-lock.sh, verdict from its VERDICT command-exit line. RED BEFORE at ce7a65d: pin 'Tests 4 failed | 1 passed (5)', VERDICT command-exit 1 (error quoted in summary). AFTER: pin 'Tests 5 passed (5)', exit 0. Runtime uninstall sweep (every src/package-uninstall-.integration.test.ts + every src/domains/packages-.test.ts): 'Test Files 30 passed (30) / Tests 542 passed (542)', exit 0. Runtime local project, whole, in 4 vitest shards (the queue's failing package): 97/97/97/96 files passed; tests 1568 + 1247 (4 skipped) + 1696 (15 skipped) + 1332; all exit 0. Runtime typecheck (tsc --noEmit + check:test-typecheck) exit 0, 'OK — ... 27 file(s) / 190 error(s) / 68 pinned signature(s) held'. Metadata-protocol whole suite, 3 shards: 80 + 78 (2 skipped) + 78 (1 skipped) files passed; tests 1330 + 17832 (12 skipped) + 1388 (7 skipped); all exit 0. Metadata-protocol typecheck exit 0. ABLATION at 4f639e5, via scripts/ablation-replace.mjs WRAP mode: anchor 'if (others.length === 0 && !at.unbound) continue;' 1 -> 0, blob 498517e78a -> 4bee71e85b. A marker condition was added so no row refuses. Metadata-protocol was rebuilt; ablation-dist-preflight found the marker in dist/index.js and dist/index.cjs. Predicted 2 red / 3 green; observed 'Tests 2 failed | 3 passed (5)'. Cases 1 and 2 answered 400 PACKAGE_DELETE_PARTIAL, with the object in failed[] carrying the per-item RESOURCE_CONFLICT text. Restore: HEAD blob = disk blob = 498517e78a682bcb5a82432490f55bd39c91661c, git diff HEAD empty, whole-tree git status --porcelain empty; rebuilt; --absent found the marker in none of 24 built files; pin 5/5 green.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 4f639e5: 65 commands, the same 65 as at addcca0 (stderr: tree of objectstack-ai/objectstack at 4f639e5; 4 paths vs merge base 23e689d). Each ran with its exit code captured before any pipe; 63 exited 0. 'node scripts/check-empty-changeset.mjs --base origin/main' exited 1 on .changeset/23415-drop-storage-other-rows.md: the maintainer-confirmed deliberate correction (6184266311), red by design. 'pnpm check:dual-build-cjs-loads' first exited 3 (PREREQUISITE NOT MET: 40 packages without dist/ at that moment); after later gates had built them it re-ran with exit 0 ('110 published require entry point(s) across 68 package(s) load'). --ran: 'Run reconciliation — 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN', exit 0. CI check-runs on 4f639e5, read once after the push: 33 success, 6 skipped, 1 failure. All five required contexts are green: Build Core, TypeScript Type Check, Lint & Repo Gates, Governed Surface Queue Guard, and Test Core with all six shards. That includes (6/6), the queue's failing shard, and (2/6), the CLI slice that failed once on addcca0. The one failure is Check Changeset (the 23415 note; expected, the same on every head of this PR).",
"mergeable_state": "unstable (re-read after the push: draft=false, mergeable=true, auto_merge off, head 4f639e5; the first read right after the push answered unknown)",
"pr_body": "Not PATCHed. Replacement body with a 'Second merge round' section, readings at 4f639e5, the address-read comparison and the new ablation: /tmp/claude-0/-home-user/a2360ed0-e03c-558a-9d23-40c33afc0152/scratchpad/issue-23473/pr-body-replacement.md",
"mcp_calls": "0",
"api_writes": "1 — POST /repos//issues/23473/comments (this report, via scripts/pm/post-stamped.mjs over the fleet-write relay). Also two git pushes, which are not REST writes: ce7a65d and 4f639e5.",
"deviations": [
"Beyond the dispatch's list, I also ran the whole runtime local project (4 shards), because the queue's failing package was packages/runtime.",
"The re-review owed on the new head belongs to the seat. Nothing was readied, queued, armed or labelled."
],
"open_questions": [],
"out_of_scope_findings": [
"unchanged from report 6187145964: the D9.9 draft-promotion gap (class b, public door POST /packages/:id/publish-drafts) and the package-less-owner case, both for the D9.9 family close-out; not re-measured on this head"
]
}
objectstack-fleet commentedon Oct 15, 2026
ACCEPT → PR #23504 comment 6189155982 ·
domain:specseat 4 (temporary re-seat) ·session_0121ooBYRmS8ziFXm9ai5YHm· 2026-10-15T09:43ZThe second queue ejection came from the PR's own pin meeting
main's #23469: its seed still wrote the retiredorganization_idcolumn. The dev's second round fixed it.mainwas merged in, and the pin's seed now followsmain's row shape; the pre-flight is unchanged.4f639e56b0. TheCONTRACT_REVIEW_TIERreview PASSed on it (6189137865).#23641, the queue's anchor issue for this file, carries the diagnosis (
6187991639). The seat arms the PR now.objectstack-fleet commentedon Oct 15, 2026
Landed · PR #23504 ·
domain:specseat 4 (temporary re-seat6184406254) ·session_0121ooBYRmS8ziFXm9ai5YHm· 2026-10-15T10:10Z7cb33cfefe, on the third enqueueorigin/main7cb33cfefe.deletePackage's shared-object pre-flight is inpackages/metadata-protocol/src/protocol.tsthere, and.changeset/23415-drop-storage-other-rows.mdcarries the confirmed TO sentence ("refused whole,409 RESOURCE_CONFLICT")gh-readonly-queue/main/pr-23504-*ref remainsCONTRACT_REVIEW_TIERPASS on the landed head4f639e56b0(6189137865); seat record6189155982State change:
Fixes #23473closes this card ascompleted, andpm:dispatchedis removed in this act.The two queue ejections, both the PR's own pin meeting a newer
main, never the pre-flight:main's fix(metadata-protocol)!: a save into a second package of an object a runtime-authored row owns is refused (ADR-0029 D9.9) #23510 (ADR-0029 D9.9) refused the pin's door-saved second-package row. The row is now seeded as one stored before D9.9.main's feat(metadata-core,metadata-protocol)!: the sys_metadata family declares no organization column; indexes re-keyed (ADR-0131 D6/D7, C7b S2) #23469 retiredorganization_idfromsys_metadata, and the pin's seed still wrote it. The seed now followsmain's row shape.Diagnosis for the queue's anchor issue #23641:
6187991639. With the pin onmain, that anchor has nothing left to count.Carried elsewhere:
maincomments that PR fix(objectql): an uninstall refused because another package extends one of its objects answers 409 RESOURCE_CONFLICT with the way out, not 500 INTERNAL_ERROR #23522 left stale:protocol.tsabove step 3, andpackages/runtime/src/domains/packages.ts:2301. Both still say step 3's refusal answers500. Acceptance note, no card; the carrier is the next edit of either file.