You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
recycle bin: a by-id delete whose driver removes nothing leaves a bin entry snapshotting a still-live row #31630
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.
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.
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.
{
"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"
]
}
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).
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.
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.
Activity
objectstack-fleet commentedon Nov 8, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: confirmed ·
domain:server· p3 · 2026-11-08T17:54Z⛔ Not a claim.
domain:server, for the recycle-bin write path (engine: snapshot deletes into the recycle bin, restore under original ids, purge, and the TTL reap (#3146 slice 3) #30237 series, ADR-0148).RESTORE_UNIQUE_VALUE_TAKEN), and the TTL reap removes it.Generated by Claude Code
objectstack-fleet commentedon Nov 8, 2026
domain:server#4 · os-steve · session_01ADHkmP8JJo4zF2sjTBRwnX
Claim: PM loop round 1 · #31630 · 2026-11-08T20:56Z
Session:
session_01ADHkmP8JJo4zF2sjTBRwnX(seat); dev = card threadsession_01ADHkmR6Bca8k3KKZsr3Ej4Account:
os-steveBranch:
claude/project-thread-0gxwgxWorktree: the card thread's checkout of
objectstack-ai/objectstackDomain:
domain:serverSeat:
domain:server#4File surface:
packages/objectql/src/**(the engine's by-id delete arm and its recycle-bin snapshot write;engine-delete-cascade.tsonly 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: mediumClause-②: 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 withholdsrecycleBinEntryId) · reached by every by-id delete on a bin-enabled business object whose driver answersfalse/0; an admin sees the ghost entry until the TTL reapThread-read: 6724985858
Premise: holds (filed from contract review 6724784594 ③ on PR #31595, merged; the dev re-measures on
mainin 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
objectqldelete path.objectstack-fleet commentedon Nov 8, 2026
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.
objectstack-fleet commentedon Nov 9, 2026
domain:server#4 · os-steve · session_01ADHkmP8JJo4zF2sjTBRwnX
Landed · #31630 · 2026-11-09T01:46Z
ae31539c44. The merged head isc769a547bc(ACCEPT 6729787595, which carries ACCEPT 6728563820's TIER_DEFAULT seat review).Fixes #31630State change:
pm:dispatchedis removed in this stroke. No follow-up card is owed. The queue timeout ondelegated-admin-own-name-set.posture.test.ts:878is tracked as #31726 / #31663, not here.objectstack-fleet commentedon Nov 9, 2026
domain:server#4 · os-steve · session_01ADHkmP8JJo4zF2sjTBRwnX
Landing follow-through · #31630 · 2026-11-09T01:47Z: unassigning
os-stevenow that the card is landed (6730291012), which leaves no owner on a closed card.