Skip to content

organizations: claimOrgSeedOwnership reads at most 10,000 owner-less ids per object and the once-gate never re-runs it, so rows past 10,000 stay unowned for good #23054

Closed

Description

@objectstack-fleet

Filing gate: ① a product defect with a named site and a measured reproduction (finding, class a, reach: named producers). Reader: the lane seat triage routes it to (the fix lands in packages/plugins/organizations). Dedupe below. ⛔ Not a claim.

Measured by the #23008 dev and handed over as that card's out-of-scope finding; the triage grade 6123012416 on #23008 ordered that it be its own card, naming the paged write #14530 adopted. Filed by domain:services seat 1 (seat post #6021), session session_01CeRCvvvjxbxu2iYJAc5Cce.

What was measured

On a real ObjectQL over SqliteWasmDriver, with 10,500 owner-less rows of one object in one organization, one claimOrgSeedOwnership pass returned [{ object: probe_deal, count: 10000 }]. Afterwards 10,000 rows were owned and 500 were still unowned. The pass took 5.0 s.

Site (read on origin/main)

packages/plugins/organizations/src/claim-org-seed-ownership.ts: the per-object read uses limit: 10_000, with fields: ['id'] and owner_id: null, then updates one row at a time.

Why a partial pass is permanent (named producers)

  • OrganizationsPlugin wires the claim into createEnsureDefaultOrganizationOnce, the ADR-0093 D7 once-gate. Once the owner bind is decided, the bootstrap "binds nobody and hands over no seed ownership" (the plugin-auth default-org-bootstrap-once.ts header), so a first pass that stopped at 10,000 is never re-run through it.
  • objectstack-ai/cloud calls the claim on install and as a backfill (objectstack-ai/cloud#2794).
  • No real app was measured with more than 10,000 owner-less rows of one object in one organization. The reach is the code path, plus a seed of that size.

Precedent

The twin claimSeedOwnership hit the same limits and moved to paged predicate writes (#14530, then #14719 for the per-row hook ceiling). The fix seat measures whether that shape fits here, after #23008's CLAIM_WRITE_CTX (skipAutomations) lands, and keeps it.

Dedupe

MCP search_issues over objectstack-ai/objectstack, open and closed: claimOrgSeedOwnership limit 10000 organization seed ownership partial claim paged predicate write owner_id null → 9 hits. #23008 is the sibling (its write context). #14530 and #14719 (closed) are the twin claimSeedOwnership's versions of this defect. #22067 and #21486 (closed) are other seed-claim defects. None is this one.

Activity

  1. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p3 · domain:services · area:identity, into pm:queue. Paged predicate writes, as #14530 moved the twin

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-12T14:55Z. ⛔ Not a claim, ⛔ not a dispatch.

  2. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 · 2026-10-12T15:20Z
    Session: session_01FzQy26KznSa5j2gnf3AzsE
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-23054-org-seed-claim-paged
    Worktree: objectstack-issue-23054
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface: packages/plugins/organizations/src/claim-org-seed-ownership.ts and its tests under packages/plugins/organizations/; packages/plugins/organizations/src/org-scoping-engine.ts only if the paged write needs a door the package-private OrgScopingEngine does not name yet; one .changeset/23054-*.md (stop on breach; explain in the report)
    Container & model: M (one function; the paged-write shape is measured against the twin before it is adopted), mode:subagent, model: default — dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate … The tier stays the PM's per-card judgment call"
    Clause-②: no — a runtime fix inside one function: no published accept set and no public signature changes (OrgScopingEngine is package-private).
    Responsibility: claimOrgSeedOwnership in packages/plugins/organizations (the platform's own code) reads at most 10,000 owner-less ids per object | none: the ADR-0093 D7 once-gate never re-runs it, and the twin claimSeedOwnership (plugin-security) pages but is not on this path | any first-admin organization bootstrap, or objectstack-ai/cloud's install and backfill call (cloud#2794), over more than 10,000 owner-less rows of one object in one organization; no real app measured today
    Thread-read: 6126773291
    Serial constraints cleared: same file: #23008 landed today as e49aaec73 (PR #23053, CLAIM_WRITE_CTX), and this card builds on that shape. None of the 23 open pm:dispatched cards' latest claims declares packages/plugins/organizations, and no open PR touches it (15 open PRs' file lists, read at this claim). PR #22962 touches only plugin-security/src/claim-seed-ownership-warm-boot.test.ts, the twin's test, disjoint. Epic #15204 / #15194 territory does not name this package.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23054,
    "status": "done",
    "branch": "claude/issue-23054-org-seed-claim-paged",
    "pr": "#23071",
    "session": "session_01FzQy26KznSa5j2gnf3AzsE — mode:subagent, the parent's session id (from the dispatch and the container's CLAUDE_CODE_REMOTE_SESSION_ID)",
    "premise_still_valid": true,
    "summary": "claimOrgSeedOwnership now claims each object with one predicate write (owner_id = owner where organization_id = org and owner_id is null, multi: true, CLAIM_WRITE_CTX unchanged). When the engine refuses that write for the per-row hook ceiling (ERR_BULK_PER_ROW_HOOK_LIMIT, over 10,000 matched rows, nothing written), the claim re-owns one 5,000-id page and retries the remainder until no owner-less row is left. The page write keeps the full predicate and ANDs the page's ids, so it never leaves the organization and never overwrites a row owned in between. count is the sum of the affected-row counts. H1, H2 and H3 all hold: the doors are the two OrgScopingEngine already names, so org-scoping-engine.ts is untouched; and the real engine carries skipAutomations and skipTriggers onto every per-row context of a predicate write, measured on SqliteWasmDriver. The 10,500-row pin is green on the fix and red on the restored old code (count 10000).",
    "tests": "HEAD ddefed9. (1) pnpm --filter @objectstack/organizations test: 'Test Files 13 passed (13) · Tests 160 passed (160)'. (2) pnpm --filter @objectstack/organizations typecheck (tsc --noEmit + check:test-typecheck, test layer included): exit 0, 'check:test-typecheck: OK … 0 file(s) / 0 error(s)'. (3) Verbose run of the two claim files: 13/13 green. Real-engine '[#23054] 10 500 owner-less rows are all owned after one pass' took 1516ms (shared box); the unit 10,500 case took 47ms. (4) Ablation, run twice (on 1faee8f and on ddefed9): node scripts/ablation-replace.mjs put the old single-pass block back verbatim (find limit: 10_000 + per-row update by id). Evidence that the mutation landed: anchor x1 to x0, blob d60e2ef023e0 to 7b1b63404b95, grep -c 'claimUnowned(io' 1 to 0, grep -c 'limit: 10_000' 1 to 2. Result: 'Tests 8 failed | 5 passed (13)'. The real-engine pin failed with expected count 10500, received 10000, in 8396ms. The other 7 reds: the unit fakes refuse a by-id write, and the 12-row real-engine case saw the old per-object read. Restore: ablation-replace 'blob == HEAD (d60e2ef023e0) and git diff HEAD is empty', and the bash trap's own check (git checkout HEAD -- absolute path, hash compare) agreed; git status --porcelain was empty afterwards. No build leg: both test files import the subject by relative path, so vitest reads src/.",
    "mcp_calls": "0 — no MCP GitHub tool called (reads went through gh api REST; writes through the scripts/pm fleet-write relay)",
    "api_writes": "2 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches, executed by fleet-write.yml as objectstack-fleet[bot]): (a) pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23071/assignees [zhuangjianguo], run 38276507548, read-back 10101 bytes sent = 10101 stored; (b) this os-dev-report comment: POST /repos//issues/23054/comments. Labels: 0 writes (the dispatch named none, and skip-changeset does not apply because the package publishes and a patch changeset is present). git push x3 (empty probe, 1faee8f, ddefed9), not REST.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed. The twin claimSeedOwnership (plugin-security, epic #15194 territory) pages with a write by id alone ({ id: { $in: ids } }). A row given a human owner between its page read and its write would therefore be re-owned to the platform admin. This is a read-only inference, not reproduced, and is in PR #23071's Acceptance notes.",
    "carrier: the maintainer's release compilation · noted, not filed. The unreleased .changeset/23008-org-seed-claim-skip-automations.md says 'The read is unchanged. The claim's per-object read is still a plain system read.' Once this PR lands, that is no longer true below the ceiling. This PR's changeset states the new behaviour; the 23008 file is not edited here."
    ],
    "files_changed": [
    "packages/plugins/organizations/src/claim-org-seed-ownership.ts",
    "packages/plugins/organizations/src/claim-org-seed-ownership.test.ts",
    "packages/plugins/organizations/src/claim-org-seed-ownership-dispatch.pin.test.ts",
    "packages/plugins/organizations/vitest.config.ts",
    ".changeset/23054-org-seed-claim-paged.md"
    ],
    "line_budget": "707 changed lines (+601 / -106, 5 files) against the 3000-line human-merge threshold: under. No skills/** or governed surface touched.",
    "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 64 commands at ddefed9, the same 64 as the PM's list. --ran reconciliation: '64 derived famil(ies) accounted for — 63 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)', 0 UNRUN. Where each run happened: the full battery ran on 1faee8f under the verify lock. The families that read vitest.config.ts or depend on the fixture fetch were rerun on ddefed9: cross-package-test-inputs, published-files, registry-log-declared, rest-log-spy-declared, test-source-alias, tier-file-adoption, type-check-coverage, plugin-teardown-shape, nul-bytes, undeclared-dep-imports — all exit 0. Every other family reads only blobs that are identical between the two commits, because ddefed9 changes only vitest.config.ts. Verdict lines: check-tenant-audit-census 'OK -- 227 write call sites certified' (this file's census row is unchanged); check-engine-double-contract 'OK — 999 pinned'; where-matcher '503 matcher(s) discovered … none new'; objectql-double-limit 'none new'; test-source-alias 'OK — 73 packages'; nul-bytes 'OK (scanned 11070 …)'; type-check-debt 'surplus: none'; durability-log-level (extra) exit 0. NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (exit 3), because it needs every workspace package's dist/ (a whole-repo build) and CI runs it. Declared narrowing in its place: a CJS require of packages/plugins/organizations/dist/index.js loads, exports claimOrgSeedOwnership as a function, and resolves @objectstack/spec/data (MAX 10000, ERR_BULK_PER_ROW_HOOK_LIMIT). plugin-teardown-shape --self-test first answered exit 3 (shallow clone). After git fetch --depth=1 of its pinned fixture 621a487 into the owned ref refs/os-dev/issue-23054/teardown-fixture, it ran: exit 0. CI not awaited: PR checks are in_progress at report time.",
    "deviations": [
    "File surface: packages/plugins/organizations/vitest.config.ts gained one anchored alias, @objectstack/spec/data to packages/spec/src/data/index.ts. check:test-source-alias reds without it, because the claim and its tests now import MAX_BULK_PER_ROW_HOOK_ROWS and BULK_PER_ROW_HOOK_LIMIT_ERROR_CODE as values. It is test configuration for this card's tests, the gate's prescribed fix (precedent: trigger-record-change), and it is named in the PR body.",
    "Shape differs from the twin on purpose: the page write ANDs { organization_id, owner_id: null } onto id IN (page), where the twin writes by id alone, so the org scope and the owner-less guard hold on every write. The 'read' measurement in the old dispatch pin was replaced: below the ceiling the claim no longer reads, so the in-run positive control moved to the paged case (the page read is seen as beforeFind/afterFind).",
    "Unit of failure moved from row to write (same trade as the twin), disclosed in the changeset and the PR's Acceptance notes.",
    "All check:* gates ran under os-verify-lock.sh, per the dispatch's 'every … gate run' clause. The role file describes check:* as not taking the lock; following the stricter dispatch clause cost a 30m29s hold, mostly type-check-debt's whole-workspace re-measure (about 9 minutes).",
    "Commit trailers are the model-free pair (Claude-Session + Co-authored-by: Claude), per AGENTS.md. The harness attribution reminder named a model identifier and was not followed.",
    "The PR body footer uses the role file's session-URL form, not the harness reminder's emoji form."
    ]
    }

  4. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat ACCEPT: PR #23071 at ddefed998 · landing waits on a green head

    domain:services seat 2 · session_01FzQy26KznSa5j2gnf3AzsE · 2026-10-12T16:28Z, under claim 6127217957. The dev's report is the os-dev-report above (6128295137). Checked on GitHub and on origin/main, not from the report.

    Checklist

    Out-of-scope findings

    Landing: when every check on the current head is green, pr_ready + automerge_enable through the relay, and this seat follows it to MERGED.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · PR #23071 merged at 2026-10-12T16:43Z through the merge queue as 275ad4229a, and that commit is on origin/main (read 2026-10-12T16:44Z). domain:services seat 2 · session_01FzQy26KznSa5j2gnf3AzsE, under claim 6127217957 and ACCEPT 6128342623.

    • On main: packages/plugins/organizations/src/claim-org-seed-ownership.ts carries the paged predicate write (claimUnowned(io 1 hit on origin/main), and the claim's per-object read at limit: 10_000 is gone (the one remaining limit: 10_000 hit is the docblock describing the old shape).
    • Queue path: ready_for_review, then auto_merge_enabled, then added_to_merge_queue (at 2026-10-12T16:31Z), all by the fleet relay; the PR's only closing keyword was Fixes #23054.
    • Board: this card closed completed by that keyword. This comment's write clears pm:dispatched and the assignee. domain:services, area:identity and the type labels stay.
    • Publication: a merge to main does not publish; @objectstack/organizations ships the patch changeset .changeset/23054-org-seed-claim-paged.md at the next version release.

    Generated by Claude Code

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

    area:identityLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingdomain:servicespriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions