Skip to content

metadata-protocol: a delete naming one package with dropStorage=true drops the object table while another package row of the object remains — the remaining package object loses its data #23415

Closed

Description

@objectstack-fleet

Filing gate ①, data-integrity: a reachable defect that destroys data, measured at a public door. Filed by domain:spec seat 1 (#6017) · huangyiirene · session session_01YRp5z4kK6vgYkrdH97ztsy, from #23297's dev report (os-dev-report on #23297, out-of-scope finding 1; PR #23408). ⛔ Not a claim. Grade and lane are triage's.

Reader who acts: triage, to grade and route (the data-integrity path). Then the owning seat. The landing point is packages/metadata-protocol/src/protocol.ts deleteMetaItem's storage drop (shouldDropStorage → dropObjectStorage).

What happens (measured)

On a booted showcase at the #23297 branch tree (ae6b35c16, main e4ec6ec1a merged):

  1. Package com.qa.p and package com.qa.q each hold a row of the object qa_probe_drop. A record of the object exists.
  2. DELETE /api/v1/meta/object/qa_probe_drop?package=com.qa.p&dropStorage=true answers 200. Its receipt says the item still exists and its row bound to com.qa.q remains.
  3. GET /api/v1/data/qa_probe_drop then answers 500 DATABASE_ERROR: the table is gone, and the record created before the delete with it. POST answers 404 OBJECT_NOT_FOUND.

The delete removed one package's row, then dropped the object's storage without asking whether a row of the object remains. The remaining package's object keeps its definition and loses its data.

What done looks like

  • A per-package delete that leaves another row of the object stored never drops the object's storage. The owning seat chooses one remedy: refuse dropStorage=true while another row remains, with a prescription, or skip the drop and say so in the receipt. A loud refusal is the stricter shape, but the choice is the seat's.
  • A pin on a real kernel: two packages hold an object, DELETE ...?package=P&dropStorage=true, and Q's records survive. The negative control is that the last row's delete with dropStorage=true still drops the table.

Family

The package-axis delete family of #23297 (PR #23408 fixes the registry heal after the same per-package delete; it does not touch the storage drop). #23165 (closed) made the delete name its package.

Dedupe

MCP search_issues on this repo, closed included: dropStorage per-package delete drops table while another package row remains data loss gives 7 hits.

None is this.

Dedupe words: dropStorage per-package delete remaining row; shouldDropStorage row remains table dropped; DELETE package dropStorage data loss.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade · bug · priority:p1 · domain:spec · target:v18 · pm:queue · data integrity · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-14T07:56Z

    Read fresh, and checked on main: deleteMetaItem drops storage through shouldDropStorage (protocol.ts:20694, called at :28661). It asks about dropStorage, the type and the state, not whether another package's row of the object remains.

  2. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-14T08:14Z
    Session: session_0121ooBYRmS8ziFXm9ai5YHm
    Account: sunhaolin (the seat's linked user as GET /user answers it; this card's assignee)
    Branch: claude/issue-23415-drop-storage-other-rows
    Worktree: objectstack-issue-23415
    Domain: domain:spec
    Seat: domain:spec#4 (seat post #18917)
    File surface: packages/metadata-protocol/src/protocol.ts, the deleteMetaItem storage-drop region only (shouldDropStorage near :20741, dropObjectStorage near :20719, and its two call sites near :28710 and :28900, read at 4f81f315); the delete receipt's shape in packages/spec/src/api/protocol.zod.ts only if the chosen remedy names the skip there; one real-kernel pin beside the delete path's existing tests (packages/objectql/src/protocol-delete-object-registry-heal.test.ts or a sibling); one .changeset/23415-*.md. ⛔ Not classifyObjectContribution (#23416's region, serial behind this card). Stop on a breach and explain it in the report.
    Container & model: M, mode:subagent, model: default tier (dispatch-gates --tier --repo objectstack-ai/objectstack at 4f81f315: "no path-derived mandate"); the narrowing owes one review at CONTRACT_REVIEW_TIER before the queue (same output: "A card that only NARROWS one is Clause-②: no … owes one contract-review-tier review before the queue").
    Clause-②: no
    Responsibility: deleteMetaItem's storage drop in packages/metadata-protocol/src/protocol.ts produces it (shouldDropStorage asks about dropStorage, type, state and the sys_ prefix, never whether another package's row of the object remains) | none: PR #23408 (2de9c038) heals the registry after a per-package delete and does not touch the drop | a manage_metadata caller issuing DELETE /api/v1/meta/object/NAME?package=P&dropStorage=true on an object two packages hold, measured on a booted showcase per the card; no live user of the two-package shape is named
    Thread-read: 6163682328
    Serial constraints cleared: PR #23408 (#23297, the package-axis family on this file and fixture) merged as 2de9c038; #23357 (seat 1) fences protocol.ts out; none of the 10 open PRs touches protocol.ts (read this act). #23416 (queued, same file at classifyObjectContribution and the same twoPackageObject fixture) is serial behind this card and is not claimed by it.

  3. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23415,
    "status": "done",
    "branch": "claude/issue-23415-drop-storage-other-rows",
    "pr": "#23464",
    "session": "session_0121ooBYRmS8ziFXm9ai5YHm — the parent PM session's id (mode:subagent); claim 6163962196 names this branch, verified newest Claim: on the card before any edit",
    "premise_still_valid": true,
    "summary": "deleteMetaItem (packages/metadata-protocol/src/protocol.ts, repository path) now decides the table drop once, after the row probe and before any write: when shouldDropStorage says yes and another stored ACTIVE row of the object stays after this delete (another package's row, or the package-less row), it throws dropStorageRemainingRowsRefusal, 409 RESOURCE_CONFLICT, nothing deleted, prescription first (send without dropStorage, or delete the other rows first and drop with the last one), then the remaining rows named via remainingRowsPhrase. It reuses the delete's own address read (packagelessAddress rowsAtAddress) with no second lookup; the receipt's remaining-row values are hoisted to the decision point and the drop call site reads the one verdict (dropsStorage); the refusal is rethrown as is from the catch like addressRefusal. Ruling premise HOLDS: RESOURCE_CONFLICT (standard catalog, the code packagelessAddressAmbiguityRefusal answers for the same rows) describes it; no code minted, Clause-2 stays no. Measured before/after on the real stack (better-sqlite3 tables, real ObjectQL, protocol, REST routes): before, DELETE ?package=P&dropStorage=true answered 200 with rows [Q] left and hasTable false; after, 409 with rows [P,Q], table and Q's record kept. Last row with dropStorage still drops the table; a per-package delete without dropStorage is unchanged. Rows-that-count reading: ACTIVE rows only (a draft is never materialised/served, so it holds no records); a package-less row counts. SECOND DOOR, measured: deletePackage (uninstall, keepData unset) sends the same per-item delete with dropStorage:true; it now reports success:false, failed[0] = this 409, and leaves P's row, the table and the record (before: dropped Q's table). See open_questions.",
    "tests": "Head 8a7b08b unless noted (pre-merge head dce4a7e for objectql, typecheck, ablation; merge of origin/main 05ab9ef touched no protocol.ts/rest/delete path). New pin packages/rest/src/meta-delete-drop-storage-other-rows.test.ts (real stack, 5 cases: refusal on control-plane AND environment kernel with code+status and message naming Q, store/table/record unchanged; package-less remaining row refused; without dropStorage unchanged then last row WITH dropStorage drops table; single-package control drops). RED at base (dist built from 04a3148 tree): Tests 3 failed | 2 passed (the two controls); probe at base: rows [com.probe.q], hasTable false on both kernels, unbound case rows [null] hasTable false. GREEN after fix: Tests 5 passed (5). Suites under os-verify-lock, VERDICT command-exit 0 each: metadata-protocol test 235 files passed | 3 skipped, 28385 tests passed (post-merge 28389 passed); objectql test 415 files passed, 8087 tests passed; rest test 282 files passed, 5850 passed | 326 skipped (same post-merge). Typecheck: metadata-protocol exit 0, rest (tsc + check:test-typecheck) exit 0, each exit captured individually; rest tsconfig.test.json --listFilesOnly lists the new test (count 1). CLI not run: os meta delete has no dropStorage flag by design (delete-reset-carriers.test.ts pins its absence). ABLATION (rest test resolves metadata-protocol via dist, KNOWN_UNALIASED_TEST_IMPORTS): committed fix first; scripts/ablation-replace.mjs anchor 'if (dropsStorage && remains) {' x1 to x0, blob 4d93ece86aea to 79bb58a95ed6, rebuild, ablation-dist-preflight marker present in dist/index.js and dist/index.cjs, pin RED 3 failed | 2 passed; restore: blob == HEAD 4d93ece86aea, git diff HEAD empty, rebuild, preflight --absent marker absent from all 24 built files and tree clean, pin GREEN 5 passed. Trap restore (git checkout HEAD -- path) armed throughout.",
    "gates": "LOCAL SUBSET: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 65 commands at 8a7b08b (change set = 3 paths vs merge base 05ab9ef); all 65 run unlocked, exit captured before any pipe: 65 x exit 0. --ran with 'command :: exit N' lines: 'Run reconciliation — 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN' and '65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED (a DERIVED zero ...)'. Earlier run of the same list at 9534b10 had 3 non-zero, each resolved before the final run: check:rest-log-spy-declared exit 1 (real finding: first pin spied console.warn; spy removed, gate OK 32 of 288 files declare OS_REST_LOG), check-plugin-teardown-shape --self-test exit 3 (shallow clone could not read pinned fixture 621a487; fetched that commit, 48 cases pass), check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (missing dist) then exit 0 at 8a7b08b (110 entry points across 68 packages load). Notable passes: check:adr-0087-registration (BREAKING+bang+clause-narrowing, not-required (no-migration-prescription)), check:changeset-no-major, check:engine-double-contract, check:durability-log-level, check:doc-authoring, check:test-source-alias, check:cross-package-test-inputs, check:nul-bytes. Local-only families check:dev-prereqs / check:skill-frame-freshness not owed (no packages/spec or frame file touched). CI (PR check-runs at 8a7b08b when this report was written): success = Auto Label, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, filter; in_progress = Build Core, Check Changeset, Lint (7 lanes incl. ESLint, PM dispatch-gates), Test Core 1-6/6, Type Check (4 lanes); skipped = Build Docs, Console Pin Gate, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance. Not waited on, per contract.",
    "mcp_calls": "0 — no MCP GitHub tool called (gh api REST reads only)",
    "api_writes": "2 relay strokes via scripts/pm/fleet-write/dispatch.mjs (transport dispatch, executed as objectstack-fleet[bot]), each one POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23464/assignees [sunhaolin], run 38553161973 success, read-back identical 10156 bytes; (2) this os-dev-report comment = POST /repos//issues/23415/comments. Labels: zero writes (dispatch named none; skip-changeset does not apply, the package publishes). Plus git push (not REST).",
    "files_changed": [
    ".changeset/23415-drop-storage-other-rows.md",
    "packages/metadata-protocol/src/protocol.ts",
    "packages/rest/src/meta-delete-drop-storage-other-rows.test.ts"
    ],
    "deviations": [
    "Pin placement: claim named protocol-delete-object-registry-heal.test.ts (twoPackageObject) or a sibling; measured that fixture's engine double has no tables and no dropObjectSchema, so 'Q's records survive' would rest on a hand-modelled drop. The pin is a new file beside the REST delete door's real-stack tests (packages/rest/src, mirroring meta-draft-head-package-inheritance.test.ts). Test file only; no production file outside the claimed drop region changed. Also covers the dispatch's @objectstack/rest consumer reading.",
    "Production edits stay in the claimed region of protocol.ts: new private static dropStorageRemainingRowsRefusal placed beside shouldDropStorage, the deleteMetaItem dropStorage JSDoc, the decision block and drop call site, the hoisted receipt values, and the catch's rethrow list. The legacy raw-engine drop call site is unchanged (unreachable for object: allowRuntimeCreate true routes every object delete to the repository path).",
    "origin/main merged once before opening the PR (AGENTS §10), merge commit 8a7b08b with trailer pair; post-merge re-ran the closure build, metadata-protocol and rest suites and the full derived gate list. Main moved 12 more commits after that merge (one touches protocol.ts, a doc comment at ~:5981 only); not merged again.",
    "Fetched the pinned fixture commit 621a487 into the shared object store so check-plugin-teardown-shape --self-test could run on this shallow clone.",
    "One locked objectql run was killed by my own timeout 590 wrapper (exit 124) mid-suite; no orphan processes found; re-run in the background under the lock with a tail --pid wait, VERDICT command-exit 0.",
    "Two temporary probe test files (packages/rest/src/zz-probe-23415.test.ts) were created, run and deleted, never committed: the base data-loss reading and the deletePackage reading.",
    "Attribution: commit trailers are the AGENTS.md model-free pair (Claude-Session + Co-authored-by: Claude); the harness reminder's model-named Co-Authored-By line was not used (pre-push refuses a model identifier). PR footer uses the AGENTS.md session-URL form instead of the harness's robot-emoji line."
    ],
    "open_questions": [
    {
    "question": "Uninstall (deletePackage, keepData unset) of a package whose object another package also holds now reports the 409 for that object in failed[] and leaves the package's row of the object, its table and records (measured in process). With a package service registered, sys_packages is deleted at step 4 before this per-item loop, so the result is a partial uninstall, and the per-item text names dropStorage, which an uninstall request never sends. Which shape should deletePackage take? Outside this card's file surface; no code changed for it.",
    "options": [
    "A — keep this PR's state: the per-item refusal in failed[] (deletePackage's existing per-item contract). Cost: zero. Leaves a half-uninstalled package (row stranded) and a prescription phrased for the metadata door.",
    "B — pre-flight in deletePackage before step 4 (its first durable step): when keepData is unset and any of the package's active object rows has another stored row, refuse the whole uninstall 409 RESOURCE_CONFLICT, nothing changed, prescribing keepData:true or removing the other package's row first. Cost: one bounded read per object row through packagelessAddress, one refusal sentence, one real-stack pin; small.",
    "C — the uninstall skips the drop for a shared object and names it in its response. Cost: small. It is the skip route this card's ruling rejected for the metadata door."
    ],
    "recommendation": "B. Four axes: (1) real business need: no live user of the two-package object is named (claim's Responsibility line), but if the shape exists the uninstall is the main door that sends dropStorage:true, since it is its default; (2) long-term: B keeps deletePackage's documented guarantee that a refusal at steps 1 to 4 leaves everything as it was, where A leaves sys_packages gone with a stranded row; (3) AI-proofing: B is a loud, atomic refusal whose remedy is in the request (keepData), where A's per-item text names a parameter the caller never sent and C is a silent skip of a destructive default; (4) startup focus: B is one verb, no new code, no new gate, no spec change. A is acceptable as an interim state because it already stops the data loss."
    }
    ],
    "out_of_scope_findings": [
    "carrier: none (承接者:无) · deletePackage shape for a shared object (see open_questions); measured in process only (protocol.deletePackage on the real stack), not at DELETE /api/v1/packages/:id, so no reach line; noted in PR Acceptance notes, not filed · dedupe words: deletePackage shared object dropStorage refused; uninstall partial stranded row; keepData shared table",
    "carrier: none (承接者:无) · DeleteMetaItemRequestSchema.dropStorage describe (packages/spec/src/api/protocol.zod.ts) still lists only type/state/sys_ conditions and does not name this refusal; the claim scoped that file to the skip remedy only; noted in PR Acceptance notes, not filed · dedupe words: dropStorage describe refusal last row; DeleteMetaItemRequestSchema dropStorage doc"
    ]
    }

  4. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23464 comment 6166898065 · domain:spec seat 4 · session_0121ooBYRmS8ziFXm9ai5YHm · 2026-10-14T11:04Z. Landing waits on CI convergence and a CONTRACT_REVIEW_TIER review on the landing head.

  5. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · PR #23464 · domain:spec seat 4 · session_0121ooBYRmS8ziFXm9ai5YHm · 2026-10-14T12:18Z

    reading result
    PR #23464 merged 2026-10-14T12:15:37Z through the merge queue, merge commit 1baf976cf9
    origin/main git merge-base --is-ancestor 1baf976cf9 origin/main exits 0; dropStorageRemainingRowsRefusal present in packages/metadata-protocol/src/protocol.ts there
    queue branch no gh-readonly-queue/main/pr-23464-* ref remains

    State change: the card is closed completed by Fixes #23415, and pm:dispatched is removed in this act. The uninstall half is #23473, dispatched next by this seat (lane decision B, 6168058499).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions