Skip to content

recycle bin: a by-id delete whose driver removes nothing leaves a bin entry snapshotting a still-live row #31630

Closed

Description

@objectstack-fleet

domain:server#3 · os-project-manager · session_01FEkQ5pXkgNXVpCn2GZDjyy

Filing gate: ② an out-of-scope finding the #30237 PR 5 card thread noted and did not file (class b), escalated by the at-tier contract review of PR #31595, record 6724784594 ③. ⛔ Not a claim.
Reach: every by-id delete on a bin-enabled business object where the driver answers false / 0, i.e. the row was not removed (already gone, filtered out, or a race). The snapshot was written first, so the bin keeps an entry for a row that is still live (or was never there).
class: b · type: Bug
Grade: p3 (self-graded; Ruling-ref: the maintainer, 「定级你可以自己决定」). No duplicate can result: a restore of that entry is refused RESTORE_UNIQUE_VALUE_TAKEN by the id probe. But the entry is noise an admin sees and may try to restore, until the TTL reap removes it. This has been the case since PR 1. PR #31595 already withholds recycleBinEntryId in this case (pinned).
Dedupe (open AND closed): recycle bin entry driver false ghost, bin entry row not removed found nothing beyond #30237.

维护者速读

回收站在物理删除之前先写快照。如果驱动答"没删掉"(false/0),快照条目仍然留在回收站里,指向一条其实还在的记录。恢复会被拒绝,不会造成重复,但管理员会看到一条无效的条目,直到 TTL 清理。本卡让这种情况不留条目。

Task

  • In the engine's by-id delete arm, when the driver answers false / 0, withdraw the bin entry written for this delete. Either delete it inside the same unit before commit, or write the snapshot only after the driver confirms. Say which you chose and why, and read the cascade-set path (engine-delete-cascade.ts) for the same shape.
  • Clause-②: no. No spec surface moves.

Acceptance

  • A pin, red then green: a driver answering false for a by-id delete leaves no sys_recycle_bin entry, while a driver answering true still writes exactly one.

Refs #30237, PR #31595, #3146

Activity

  1. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 8, 2026
  2. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: confirmed · domain:server · p3 · 2026-11-08T17:54Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 8, 2026
  4. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-steve · session_01ADHkmP8JJo4zF2sjTBRwnX

    Claim: PM loop round 1 · #31630 · 2026-11-08T20:56Z
    Session: session_01ADHkmP8JJo4zF2sjTBRwnX (seat); dev = card thread session_01ADHkmR6Bca8k3KKZsr3Ej4
    Account: os-steve
    Branch: claude/project-thread-0gxwgx
    Worktree: the card thread's checkout of objectstack-ai/objectstack
    Domain: domain:server
    Seat: domain:server#4
    File surface: packages/objectql/src/** (the engine's by-id delete arm and its recycle-bin snapshot write; engine-delete-cascade.ts only if the cascade set has the same shape), their tests, .changeset/31630-*.md (stop on breach; no spec edit)
    Container & model: S, mode:thread, model: default tier (dispatch-gates --tier on objectql: no path-derived mandate), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the engine's delete path in objectstack (packages/objectql) writes the bin snapshot before the driver confirms the delete · no platform path covers it today (PR #31595 only withholds recycleBinEntryId) · reached by every by-id delete on a bin-enabled business object whose driver answers false / 0; an admin sees the ghost entry until the TTL reap
    Thread-read: 6724985858
    Premise: holds (filed from contract review 6724784594 ③ on PR #31595, merged; the dev re-measures on main in step 0)
    Serial constraints cleared: #30237 closed (PR #31595 merged); #31629 (same recycle-bin series, core annotation and spec prose) is not claimed and touches no objectql delete path.

  5. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-steve · session_01ADHkmR6Bca8k3KKZsr3Ej4
    os-dev-report

    {
      "issue": 31630,
      "status": "done",
      "branch": "claude/project-thread-0gxwgx",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/31702",
      "session": "session_01ADHkmR6Bca8k3KKZsr3Ej4",
      "premise_still_valid": true,
      "summary": "Step 0 on main 8c9260f79: the by-id arm writes the bin entry (raw driver.create) after the cascade walk and before driver.delete(root), in one transaction only when planCascadeAtomicity is 'atomic'; a false/0 answer kept 1 entry and withheld recycleBinEntryId. memory/sql/mongo all answer false for a row not removed; none answers 0. Chose (a): RecycleBinWrittenEntry gains withdraw(), called on false/0, removing the entry with the write's own driver options (inside the covering transaction when there is one); compensate() and withdraw() share one remove(). (b) was rejected because on the 'none'/'split' plans a bin write after the removal could fail with the row already gone and nothing to compensate. The cascade-set path is not the same shape: cascade children open no capture.",
      "tests": "capture.test.ts new describe, 6 tests (positive exactly-one; false/0 no-transaction; false/0 inside the cascade transaction with the same handle; withdraw failure logs error, answer kept). Ablation via scripts/ablation-replace.mjs on engine.ts (else await entry?.withdraw() -> else void 0): 5 failed / 22 passed, e.g. 'expected [ { object_name: loner, ... } ] to have a length of +0 but got 1'; restored, blob == HEAD a501f350c801, git diff HEAD empty; source import, no dist leg. On head c7032b19: objectql test 572 files / 11622 passed; driver-memory 75 / 1772 passed; driver-sql 245 passed 13 skipped files / 4132 passed 222 skipped; dogfood full 440 passed 3 skipped files / 3652 passed 41 skipped; objectql typecheck exit 0; eslint packages/objectql --no-inline-config exit 0. All VERDICT command-exit 0.",
      "gates": "dispatch-gates --commands derived 69 on c7032b19; 69 run, all exit 0; --ran reconciliation exit 0: '69 derived famil(ies) accounted for, 69 run, 0 NOT-MEASURED'. First-pass exit 3 re-run green: check-plugin-teardown-shape --self-test (fixture commit fetched), check:dual-build-cjs-loads (after full pnpm build), check:type-check-debt (lock queue-timeout behind own dogfood). CI check-runs: in_progress at report time (not waited).",
      "dogfood_battery": "c7032b19 · VERDICT command-exit 0 (held 31m23s) · Test Files 440 passed, 3 skipped (443) · Tests 3652 passed, 41 skipped (3693) · no failures. Run because the card delta names dogfood; no '!' and no (narrowing) arm.",
      "mcp_calls": "4 (all reads): issue_read get #31630, issue_read get_comments #31630 x2, pull_request_read get #31702",
      "api_writes": "2: relay pr_create (POST /repos/objectstack-ai/objectstack/dispatches, run 42527671593, PR #31702, 10959 bytes sent == stored); this report comment via post-stamped.mjs. git push is not counted.",
      "files_changed": [
        ".changeset/31630-recycle-bin-no-ghost-entry.md",
        "packages/objectql/src/engine.ts",
        "packages/objectql/src/recycle-bin/capture.ts",
        "packages/objectql/src/recycle-bin/capture.test.ts"
      ],
      "line_budget": "+128 / -23 over 4 files (151 changed lines; human-merge threshold 3000: under)",
      "deviations": [
        "PR has no assignee: the session context asks for MCP issue_write/update_pull_request to set assignee/reviewer, the standard order forbids GitHub MCP writes; not done, relay read-back shows assignees none.",
        "Not subscribed to PR activity: os-dev's dispatch contract overrides the platform's auto-subscribe posture.",
        "test:repo not run locally (diff not in that project); CI Test Core carries it."
      ],
      "open_questions": [
        {
          "question": "When the root's removal answers false AFTER the cascade walk already removed its children (atomic plan, transaction commits), should the delete keep answering false with the entry withdrawn (this PR), or roll the whole unit back?",
          "options": [
            "A: keep this PR: answer false, entry withdrawn. While the root is live the old entry was unrestorable anyway (RESTORE_UNIQUE_VALUE_TAKEN on the root id, entry is the unit); only a root removed by another actor in the race loses the children's undo.",
            "B: a separate card: a root false after a non-empty cascade becomes a refusal that rolls the unit back, so no child is removed for a root that was not. Changes the delete's answer; its own Clause-2 reading."
          ],
          "recommendation": "A for this card (it is the card's acceptance and changes no answer); B only if the seat judges the race worth a card."
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: none (read-only inference, no reproduction) · a cascade child whose driver answers false keeps its row in the root's snapshot; restore of that entry would be refused if the child is live · noted in PR Acceptance notes, not filed · carrier: none · dedupe: recycle bin cascade child false snapshot"
      ]
    }

    Stamped 2026-11-08T21:57Z.

  6. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-steve · session_01ADHkmP8JJo4zF2sjTBRwnX

    Landed · #31630 · 2026-11-09T01:46Z

    Probe Reading
    PR #31702 merged through the queue 2026-11-09T01:46Z as ae31539c44. The merged head is c769a547bc (ACCEPT 6729787595, which carries ACCEPT 6728563820's TIER_DEFAULT seat review).
    #31630 closed by Fixes #31630

    State change: pm:dispatched is removed in this stroke. No follow-up card is owed. The queue timeout on delegated-admin-own-name-set.posture.test.ts:878 is tracked as #31726 / #31663, not here.

  7. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-steve · session_01ADHkmP8JJo4zF2sjTBRwnX

    Landing follow-through · #31630 · 2026-11-09T01:47Z: unassigning os-steve now that the card is landed (6730291012), which leaves no owner on a closed card.

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 workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions