Repository navigation
runtime (17.7.0): file fields read "no file" for a record reader without sys_file read, flow run summaries report acted 0 while notifying, and the activity read pre-scan caps at 2000 rows fail-closed #22590
Description
Ruled: 6115671647 · letter W via #22889 (interim C withdrawn; item 3 folds into #22788 E5 / E6) · 2026-10-12T00:39Z
Ruled: 6097072172 · letters A (end state, ADR first) + C (interim) · 2026-10-10T11:34Z
Blocked-by: #22788
Filing class: ① product defects — reach: public doors / server log, each measured once on @objectstack/* 17.7.0. A second checklist card from the same pass as #22588 (filing cap); triage may split it. Maintainer instruction, verbatim, 2026-10-10: 「你遇到的平台问题应该提交issue」.
Reader: objectstack triage.
Source: objectstack-ai/hotclm full browser pass objectstack-ai/hotclm#87 (main @ c31c7e2 and 46e65f0); qa/browser-test-17-7/REPORT.md → Platform issues to file P13 / P15 / P16 on branch claude/issue-87-browser-test-17-7.
Checklist
All three verified on 18 at 117301705 and ticked: report 6250571009, close-out 6250603283.
- 1. A file field renders "no file" for a user who can read the record but not
sys_file. As a requester (permission set grants read onclm_contract_version, not onsys_file), opening the version they just uploaded shows no file; server log:sys_file lookup failed; file fields keep their raw ids and will render as "no file"(screenshot036). Expected: a file field's hydration follows the reader's access to the record that holds it (assys_attachmentreads follow the parent record), or the refusal is surfaced as a refusal, not as an empty field. - 2. A flow run's summary reports
acted: 0while the run sent notifications.POST /api/v1/automation/legal_review_sla/trigger→summary.acted 0; the assignees'sys_inbox_messagethen holds the new messages (five of the app's six daily flows read the same way). A scheduled flow that did its job is indistinguishable from one that had nothing to do — the problem Surface flow run summaries (selected / acted / skipped) — a scheduled flow that does nothing is currently indistinguishable from one with nothing to do #4354 set out to remove. Nearest prior: finding: a notify flow-run summary reports success/acted:1 while the delivery dead-lettered — the dead-letter is absent from the run summary #7747 (closed — the opposite case,acted: 1on a dead-letter). - 3. The activity read visibility pre-scan stops at 2,000 candidate rows and fails closed. Server log on any non-admin page load:
[audit] activity read visibility: candidate pre-scan hit the 2000-row cap … fail-closed and may omit visible rows. On a demo-sized database (640+ activity rows for one user's readable records) a reader can silently miss activity they may see. (The Console half — the header widget calling the global, unscopedsys_activitylist — is item 8 of console (17.7.0): seven UI defects measured in one full browser pass of a metadata app — missing Edit, capped calendar, page-only footer sum, ignored password, and three affordances shown to users who cannot use them objectui#12081.)
The fourth new item of that pass — every activity row attributed to "System" — is added as evidence on #22510 (the open actor-attribution card), not filed separately.
Dedupe
MCP search_issues on this repo: sys_file read denied file field renders no file hydration record readable → 2 hits (#17406, #10702 — closed, unrelated) · flow run summary acted 0 notifications sent trigger → 4 hits (#22073, #7747, #4354, #3481 — closed, none this) · activity read visibility candidate pre-scan 2000-row cap fail-closed omit visible rows → 3 hits (#22402, #21829, #7217 — closed, unrelated).
Filed by the repo:hotclm PM seat from a measured browser pass.
Activity
objectstack-fleet commentedon Oct 10, 2026
os-dev-report
{ "issue": 22590, "status": "done", "branch": "claude/issue-22590-run-summary-acted", "pr": "https://github.com/objectstack-ai/objectstack/pull/22632", "session": "session_013j5gkUCpqQiti4GgPqqmnt (subagent run; the parent's id, as this run's Claude-Session line names it)", "premise_still_valid": true, "summary": "Round 1, item 2 under the amended claim (seat review 6094568255, amendment 6094572037). The round-0 producer premise stays false, as measured before, and the amended order's premise held when read on main e22315238f: highlightFields carried selected_count and acted_count but not unmeasured_count, and unmeasured_count's description named only connector_action and a mutating http call. Draft PR #22632 changes sys-automation-run.object.ts as metadata only: highlightFields gains unmeasured_count right after acted_count; the comment above it names the three first-filter operands; and the description now also lists a notification queued for delivery. It also adds one pin, sys-automation-run-first-filter-highlight.test.ts, and the patch changeset .changeset/22590-run-summary-acted.md for @objectstack/service-automation. No column was added, and notify-node.ts, service-messaging, packages/spec, plugin-audit and content/docs/releases are untouched. No translation bundle carries sys_automation_run, so nothing was regenerated (check:i18n and check:i18n-coverage are green). The branch was merged with origin/main 83b8b80728 before the PR opened (clean merge, commit 29cfa41a1b). CI at one read of the head: 9 success, 3 skipped, 19 in_progress, 0 failed, so it is in_progress. Attribution: the commit carries AGENTS.md's model-free trailer pair, and the PR body ends in the session-URL footer form, over the harness reminder's model-named trailer and footer (role file: the harness reminder yields).", "tests": "All at HEAD 29cfa41a1b unless noted, under scripts/pm/os-verify-lock.sh (slot issue-22590, 3 GB heap, turbo --concurrency=1, vitest --maxWorkers=2). Build `turbo run build --filter='@objectstack/service-automation...'` gave VERDICT command-exit 0 (Tasks 30 successful, 30 total). Because spec moved on main's side, `pnpm --filter @objectstack/spec check:generated` also ran: All 15 generated artifacts are up to date. `pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2` gave Test Files 182 passed (182), Tests 2287 passed (2287). `pnpm --filter @objectstack/service-automation run typecheck` (tsc --noEmit and check:test-typecheck) gave VERDICT command-exit 0, with check:test-typecheck OK. The new test is in tsc's program (--listFiles count 1). Pin, at 1d516bee85 (the merge brought no service-automation change; the full suite above re-ran it at 29cfa41a1b): with its sibling it gave Tests 6 passed (6). Ablation used scripts/ablation-replace.mjs in WRAP mode under the lock. The pin imports the object by relative path, so it reads source and no dist/ sits in between. Attempt 1 (--replacement with text that is a substring of the anchor) was REFUSED by the tool before any test ran: the replacement count could not rise. The file was restored to the HEAD blob. It is recorded as a no-op. Attempt 2: --anchor \" 'unmeasured_count',\" --delete moved the anchor 1 to 0 and the blob 5a0f59cde967 to ec9f7ec995f1. The pin went RED as predicted: Tests 1 failed, 1 passed (2), 'first-filter operands missing from highlightFields: unmeasured_count: expected [ unmeasured_count ] to deeply equal []'. The column-type test stayed green. Restore: blob 5a0f59cde967 equal to HEAD, git diff HEAD empty, porcelain empty. Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 63 commands from 3 paths. All 63 were run, each exit captured before any pipe. The --ran reconcile, given the exit codes, answered 63 derived, 62 run, 1 NOT-MEASURED, 0 UNRUN. NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (exit 3). It reads every publishable package's dist/, and 37 have none in this worktree, which built only service-automation's closure. This is a declared narrowing that CI's Lint and Repo Gates covers. The other 62 exited 0 with their own pass lines. Not derived, but run because a description changed: `pnpm check:i18n` (OK, 9 packages in sync) and `pnpm check:i18n-coverage` (OK, none new).", "mcp_calls": "0", "api_writes": "This round: 3 relay writes. (1) pr_create POST /repos/objectstack-ai/objectstack/pulls (draft forced): PR 22632, body stored identical, 10373 bytes. (2) label-write --issue 22632 --assign zhuangjianguo, which is POST /repos/objectstack-ai/objectstack/issues/22632/assignees; it read back matching. (3) This os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/22590/comments via post-stamped. There were two git pushes (commit 1d516bee85 and the merge 29cfa41a1b), which are not REST writes. Round 0 wrote one comment, 6094553100. No label was added by this seat: none was named, and a changeset exists, so no skip-changeset. The 4 labels on the PR came from another actor (the path labeler) and were left alone.", "open_questions": [], "out_of_scope_findings": [ "carrier: seat review 6094568255 dropped it · noted, not filed: the approval / approval_revise nodes in plugin-approvals carry no counter (source reading). It is recorded in PR 22632's Acceptance notes as dropped by the seat." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
ACCEPT (seat review, item 2, round 1): PR #22632 at head
29cfa41a1b. The run row's highlight set carries the first filter's qualifier,unmeasured_countdomain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T07:16Z. Claim6094312085, amended6094572037after the seat's review6094568255. Round-1 report on this card. Read against GitHub and the PR head, not the report.Review route.
Clause-②: no. The diff is a presentation set and a description on an engine-owned object: no accept set and no public surface move, and no column is added. NOT governed (check-governed-merges --pr 22632: 68 changed lines). So the review is a seat read plus CI.Shape. Draft, base
main. Line 1Part of #22590(item 3 remains), line 2Clause-②: no. Three files:sys-automation-run.object.ts:highlightFieldsgainsunmeasured_countright afteracted_count;unmeasured_count's description adds "a notification queued for delivery".sys-automation-run-first-filter-highlight.test.ts. It requires the three operands to be declared number columns, and it requires all three in the highlight set. Its ablation (unmeasured_countdeleted from the set) turns it red, and it was restored to a blob equal to HEAD.patchfor@objectstack/service-automation. It tells operators to read the triple.Evidence read.
selected 1, acted 0, unmeasured 1, inline delivery readsacted 1, and the control reads1 / 0 / 0.sys_automation_run, so nothing was regenerated.check:i18nandcheck:i18n-coverageare green.service-automation: 182 files, 2287 passed. Typecheck exits 0.check:dual-build-cjs-loadswas NOT MEASURED locally (prerequisite), and CI's Lint & Repo Gates carries it.CI at
29cfa41a1b: 31 success, 3 skipped (Build Docs,Console Pin Gate,Packed-tarball smoke (opt-in), all rostered; the diff touches none of their surfaces). Lint & Repo Gates carriescheck:dual-build-cjs-loads. The docs-drift advisory:flows.mdx's run-counter section (about:1355–:1390) already namesunmeasured_countas the filter's qualifier, and no sentence there is falsified.Findings: the
approval/approval_revisenodes carrying no counter is dropped (the seat's review: no wrong summary at any door, outside the filter).Landing: ready, then auto-merge through the queue, in this act. On MERGED, this card returns to
pm:queuewith aRelease:line, and item 3 is next.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Landed (item 2) and released ·
domain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T07:36Z1b99388505, read onorigin/main(sys_automation_run'shighlightFieldscarriesunmeasured_count). It isPart of #22590, and this card stays open.@objectstack/service-automation,patch):unmeasured_count's description names a notification queued for delivery.selected 1, acted 0, unmeasured 1. The seat review is6094568255.repo:hotclmseat: read the triple.acted 0withunmeasuredabove 0 means the delivery was queued, not that nothing happened.plugin-audit/src/activity-read-visibility.ts, triage6093122404). The ruled direction: push the visibility decision into the query, ⛔ not a raised cap, and ⛔ never fail open. It no longer waits on PR feat(plugin-audit): sys_comment_reaction, the reactor's own record of an emoji reaction #22603, which landed the comment reactions on the same bound (29a163f571).pm:dispatched→pm:queue, assignee cleared. This seat's item-2 claim6094312085(amended6094572037) is closed.Release:
session_013j5gkUCpqQiti4GgPqqmnt(domain:servicesseat 1) · item 2 delivered (PR #22632,1b99388505) · back to the queue for item 3.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Claim: PM loop round 1 · 2026-10-10T07:49Z
Session:
session_013j5gkUCpqQiti4GgPqqmntAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-22590-activity-prescan-pushdownWorktree:
objectstack-issue-22590-item3Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)Scope: item 3 only, the activity read pre-scan cap, as
Part of #22590(orFixes #22590if it is the card's last open item at PR time: item 1 is #22593, item 2 landed as1b99388505). Direction is triage6093122404: the visibility decision moves into the query, as a predicate the driver evaluates. ⛔ Raising the cap is not the fix. ⛔ Never fail open.File surface, read on
origin/main1b99388505:packages/plugins/plugin-audit/src/parent-record-read-gate.ts: the shared mechanism behindactivity-read-visibility.tsandaudit-log-read-visibility.ts(and read byactivity-field-redaction.ts). The bounded candidate pre-scan becomes a predicate the driver evaluates, built from the same per-parent readability the gate asks today. ⛔ No second readability rule.activity-read-visibility.ts,audit-log-read-visibility.ts: only as far as the shared mechanism's signature moves.plugin-audit:.changeset/22590-*.md:patchfor@objectstack/plugin-audit, unless the measurement shows otherwise.comment-access-hooks.ts: the comment and reaction gates keep their own bound. If the same push-down applies, report it as a follow-up. ⛔ Noaudit-writers.ts(PR fix(plugin-audit): sys_activity.actor_name carries the acting user's display name, written with the row #22526). ⛔ Nopackages/spec, nocontent/docs/releases/.Container & model:
L,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; the default tier). This is a read-gate rework on a security boundary, so the seat adds oneCONTRACT_REVIEW_TIERreview before the queue (verification strategy, the seat's call).Clause-②: no
Responsibility:
this repository's plugin-audit: the parent-record read gate's bounded candidate pre-scan omits rows past 2,000, fail-closed | none, because the bound is the gate's own mechanism | every non-admin reader of activity and audit-log rows on a busy database; measured on 17.7.0 in objectstack-ai/hotclm (640+ activity rows for one user's readable records)Thread-read: 6095184121
Serial constraints cleared:
domain:servicesseat 2) editsplugin-audit'saudit-writers.tsand adds one test. Disjoint files; whichever lands later mergesmain.29a163f571, landed) put the reaction gate beside the comment gate incomment-access-hooks.ts, which this card does not touch.plugin-audit(2026-10-10T07:49Z, the file lists of all open PRs read).Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
os-dev-report
{ "issue": 22590, "status": "needs_decision", "branch": "claude/issue-22590-activity-prescan-pushdown", "pr": null, "session": "session_013j5gkUCpqQiti4GgPqqmnt (subagent run; the parent's id, as this run's Claude-Session line names it)", "premise_still_valid": true, "summary": "Item 3 only, claim 6095289162. The premise holds on main 1b99388505, and I stopped before any edit under the order's own guard, so there is no PR. MEASURED: a real bootStack stack (real SecurityPlugin, SharingServicePlugin and AuditPlugin, SQLite) held 5,042 activity rows. A non-admin reader may see 1,808 of them, judged by its own engine read of each parent. The reader was served 1,105 rows by an unordered list, 800 oldest-first and 1,800 newest-first. count and the REST list total both said 1,105. A list pinned to one object_name still got only 956 of the 1,200 rows it may see. An admin's list and count both said exactly 2,000, because the activity gate exempts no caller. The cap warning was logged on every read, and no unreadable row was served. WHY I STOPPED: the ruled direction needs the driver to evaluate a semi-join (record_id IN the parent ids the caller may read), and the platform has no such capability. The filter vocabulary has no subquery spelling. packages/spec retired DriverCapabilities.querySubqueries and joins in 17.0.0, with the words 'ObjectQL never plans subqueries through a driver'. driver-sql lowers $in through knex whereIn, one bound parameter per member, and has no subquery path. objectql exports nothing that composes a parent object's read scope into a query on another object. The security half of that scope IS composable today: ISecurityService.canReadObject plus getReadFilter (tenant Layer 0, RLS, controlled-by-parent, OWD and record shares), the two halves the analytics raw-SQL door asks. What is missing is evaluating that filter against the parent table from inside a sys_activity query, which needs spec, engine and drivers: domain:engine and domain:spec, outside this card. A second bound blocks the pin even with the parent gate pushed down. The withheld-update row rule (activity-field-redaction.ts, computeWithheldUpdateFilter) runs its own SYSTEM pre-scan on the same four reads, at the same PARENT_GATE_SCAN_LIMIT. When it reaches the bound it answers id $in at most 2,000 kept rows. Turning it into a predicate needs a writer-stamped changed-fields column in audit-writers.ts, which is outside the fence (PR #22526). RULE CENSUS, each rule and whether it can be pushed down. (1) Parent pair parse (no object_name or record_id, a non-machine name, a row naming the gated object itself): a plain predicate on sys_activity columns, pushable now. (2) Principal-less refusal (ADR-0096 D5), (3) the deny baseline with no permission set (ADR-0056 D2), and (4) object-level read on the parent (canReadObject): each is decided once per caller and parent object, so it only adds or drops one branch. Pushable now. (5) Tenant Layer 0, (6) authored RLS, (7) controlled-by-parent (ADR-0055) and (8) OWD with record shares: one filter over the PARENT's columns through getReadFilter. Pushable only through a semi-join. (9) The driver-native tenant wall on the parent read (applyTenantScope): semi-join only. (10) Parent existence: today a deleted parent's rows are excluded, and only a semi-join keeps that. (11) On-behalf-of (ADR-0090 D10): getReadFilter denies a delegated context outright, while the engine's own read intersects it with the delegator. So a pushed predicate would serve delegated readers nothing. It stays on the probe. (12) Parents that have their own plugin read gate: sys_attachment (service-storage), sys_comment_reaction and sys_approval_request, which the CRUD mirror writes rows about, plus sys_comment and sys_audit_log, which only direct writes name. These gates build their WHERE at read time, and none is part of getReadFilter. Not pushable; they stay on the bounded probe, fail-closed. (13) Federated or external-datasource parents: no single driver can evaluate a semi-join across datasources. Not pushable. (14) The withheld-update row rule: it judges the stored JSON change against the reader's served fields per parent object, which cannot be expressed over a text column. Not pushable without the writer column above. (15) Column redaction of served rows: it runs after the read, per returned row, and is already unbounded. It is not a visibility rule. The ledger gate adds two: its outside-class rows (no record_id and a non-record action) are a plain predicate, and its exempt capability is a constant. Both are pushable now. FAN-OUT: the probe stack held 13 distinct parent objects (10 platform objects a bare boot writes, plus 3 app objects). The CRUD mirror audits every object outside its skip list. The reference apps declare 6 (app-crm) and 19 (app-showcase) object files. So any design should use one SYSTEM group-by on object_name and one branch per object, never a query per row. By source reading, not measured: the pre-scan runs as SYSTEM, and the driver's tenant wall does not scope it. On a multi-organization deployment, rows from every organization would compete for the one 2,000-row window. HOUSEKEEPING: the remote branch was pushed empty (no commits beyond 1b99388505) and is left in place. The worktree is removed. Options are in open_questions.", "tests": "No file changed, so no package test, typecheck, changeset or ablation is owed. plugin-audit test and typecheck were NOT RUN (no diff). Build, under scripts/pm/os-verify-lock.sh with slot issue-22590-3, a 3 GB heap and turbo --concurrency=1: turbo run build --filter='@objectstack/verify^...' gave VERDICT command-exit 0 (Tasks 46 successful, 46 total). Probe: a scratch test placed in packages/verify/src only for the run and then deleted (git status --porcelain empty afterwards), run as pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2 on that one file. Run 1 gave VERDICT command-exit 1 during fixture setup, before any gate read, so it is NOT a gate reading. One engine insert of 1,200 rows on the SQLite-wasm driver was refused with 'too many terms in compound SELECT'. Setup was re-batched at 400 rows. Run 2 gave VERDICT command-exit 0 (Tests 1 passed), at HEAD 1b99388505. Its readings: 5,042 rows at rest, 13 distinct object_name values, 2,540 distinct parent pairs. Ground truth, from the reader's own engine read of each parent object: 1,808 rows (cpz_doc 1,200, cpz_note 600, sys_member 3, sys_user 5; cpz_secret 0, with no object grant). Gated reads: find unordered 1,105; find newest-first 1,800; find oldest-first 800; count 1,105; GET /data/sys_activity gave status 200 with 1,105 records and total 1,105; find pinned to object_name cpz_doc 956 of 1,200. Served rows outside the ground truth: 0. Admin find 2,000 and count 2,000. Warning lines captured on the reader's reads: 7 ('candidate pre-scan hit the 2000-row cap ... fail-closed and may omit visible rows'). Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack exited 2 with 'this branch changes nothing against origin/main (merge base 1b9938850) — nothing to derive', so there is nothing to reconcile with --ran.", "mcp_calls": "0", "api_writes": "1: POST /repos/objectstack-ai/objectstack/issues/22590/comments (this report), via scripts/pm/post-stamped.mjs on the fleet-write relay. One git push of the empty branch, which is not a REST write. No pr_create and no label-write, because there is no PR. Reads were REST GETs of issue 22590 and its comments.", "open_questions": [ { "question": "Item 3: a predicate the driver evaluates needs a semi-join (the sys_activity record_id IN the parent ids this caller may read), and the platform has none: no filter spelling, no driver path, no engine composition across objects. Which route?", "options": [ "A: Engine semi-join, the ruled direction placed where it belongs. Add ONE engine-internal filter leaf meaning 'ids of OBJECT this caller may read', never authorable and never accepted from a request. objectql lowers it to the driver as a subquery over the parent, composed from canReadObject, getReadFilter and the native tenant wall. driver-sql implements it; driver-memory and driver-mongodb implement it or refuse loudly. plugin-audit then emits one branch per parent object, from one SYSTEM group-by on object_name. The withheld-update rule gets a writer-stamped changed-fields column in audit-writers.ts, with a backfill. Named rules stay on today's bounded probe, fail-closed: (11) delegated contexts, (12) parents with their own plugin gate, (13) federated parents. Cost: domain:spec (the driver contract re-declares what 17.0.0 retired as querySubqueries/joins), domain:engine, five drivers, plugin-audit and audit-writers, over several PRs. The same leaf would also replace the five sibling bounded probes: comment, reaction, attachment, ledger and withheld-update.", "B: Unbounded materialization in plugin-audit only, the ADR-0055 controlled-by-parent shape. Per parent object, read ALL the ids the caller can read through its own engine read, and emit record_id $in those ids. It stays in this card's fence and reuses the full middleware chain. But it is the unbounded scan the claim forbids and the raised cap the triage forbids. driver-sql binds one placeholder per $in member with no chunking. Past the dialect's bound-parameter ceiling (documented as SQLite's default 32,766 and 65,535 on PostgreSQL and MySQL; not measured here) the driver throws, and the gate's catch denies the whole read. A reader with that many readable records of one object would go from a short list to zero rows. The withheld-update pre-scan would still cap at 2,000.", "C: Make the bound loud, with no push-down, as an interim. When a pre-scan reaches its bound, refuse the broad read with a located 400 that names the scoping remedy, instead of answering a silently short list and total. This matches how the engine treats a nested-relation filter past RELATION_FILTER_ID_CAP ('refused, never truncated'). It is small and stays in plugin-audit (parent-record-read-gate.ts and activity-field-redaction.ts). But it contradicts the pin, because the reader gets a refusal instead of every visible row. And on a busy database the Console header widget's unscoped list (objectui#12081 item 8) would show an error until that consumer scopes its query.", "D: No platform change now. Fix the measured trigger at its consumer (objectui#12081 item 8 scopes the header widget, already on objectui's board) and leave item 3 open, Blocked-by the card for A. It costs nothing here. API readers of an unscoped list keep the silent omission." ], "recommendation": "A, carried by a domain:engine card (with spec and drivers) that item 3 names as Blocked-by, and D in the meantime. Real business need: the cap is measured omitting rows from real readers. Here, a reader was served 1,105 of the 1,808 rows it may see, and the list total also said 1,105; an admin was cut at exactly 2,000. hotclm hit it at 640+ rows for one user, because the window counts every candidate row, not that reader's rows. The measured trigger is the Console widget, and D removes it at no platform cost. A serves the whole need. B serves it only up to a driver ceiling, then fails to zero. C serves none of it and breaks the widget. Long-term soundness: A is contract-first. One read-scope composition would serve six gates (activity, ledger, withheld-update, comment, reaction, attachment) instead of six bounded probes. B is a workaround with a worse cliff. C is honest, but it stays only as long as A never arrives. Keeping AI from writing it wrong: A must stay engine-internal and not authorable, so authors get no new spelling. It removes a short answer that an AI reading total cannot detect. B fails silently to zero. C is the clearest failure, but it is a refusal where the ruling asks for rows. Startup-stage focus: A is a capability expansion; the driver contract regains what 17.0.0 retired for having no consumer. It now has six named consumers, but it should be ONE leaf, not general subqueries or joins. D first is the tight move, because it removes the measured trigger without growing the platform." } ], "out_of_scope_findings": [ "Same family, the bounded pre-scan with fail-closed truncation: comment-access-hooks.ts (comment and reaction gates, READ_SCAN_LIMIT 2,000), service-storage attachment-access-hooks.ts (attachment gate, READ_SCAN_LIMIT 2,000) and activity-field-redaction.ts (the withheld-update rule, PARENT_GATE_SCAN_LIMIT). This is a source reading for comment, reaction and attachment, not measured. The withheld-update rule sits on this item's own read path. Option A's leaf would replace all five. carrier: the domain:engine card for option A if the maintainer picks it; otherwise 承接者:无 · noted, not filed · dedupe words: candidate pre-scan cap fail-closed comment reaction attachment, READ_SCAN_LIMIT omit visible rows, withheld-update pre-scan cap", "plugin-sharing's buildReadFilter reads the caller's sys_record_share grants with limit 5000 and no warning. Grants past 5,000 for one user on one private object would silently drop out of the read scope (fail-closed omission). This is a source reading only, with no door measured, so its class is not established. carrier: 承接者:无 · noted, not filed · dedupe words: sys_record_share grants limit 5000 read filter, record share grant cap silent, buildReadFilter grant truncation", "driver-sql bulkCreate sends one multi-row INSERT with no chunking. Measured once on the SQLite-wasm driver, through @objectstack/verify's seed(): an engine insert of 1,200 rows in one call was refused with 'too many terms in compound SELECT' (SQLite's 500-term compound limit). The platform seed loader batches at 200 (seed-loader.ts, BULK_BATCH_SIZE), so the shipped seed path is safe. No HTTP door was measured, so reach is not established. carrier: 承接者:无 · noted, not filed · dedupe words: bulkCreate too many terms in compound SELECT, sqlite bulk insert 500 rows, engine insert array chunking" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
决策请求(第 3 项):动态记录(activity)的可见性预扫描上限,要不要给引擎加一个「只在引擎内部使用的子查询」能力来根治 ·
domain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T08:08ZState:
pm:dispatched→needs-user-decision, assignee cleared. This seat's item-3 claim6095289162is released: triage's direction (6093122404) is not buildable inside this lane. Dev report:6095496633on this card. No PR, no file changed.一句话问题: 一位普通成员能看到 1,808 条动态记录,系统只给了 1,105 条,列表上的「总数」也显示 1,105,用户无从察觉少了;管理员恰好被截在 2,000 条。分诊定的方向是把「能不能看」直接写进查询,但平台目前没有这种跨对象查询能力,要补就得改 spec、引擎和全部驱动。
背景(实测,
main1b99388505,真实 SecurityPlugin + SharingServicePlugin + AuditPlugin,SQLite)count和 REST 列表的total都是 1,105;只看cpz_doc一个对象时 1,200 条里给了 956 条。Governing text
packages/spec/src/data/driver.zod.ts:350–351:querySubqueries: retiredKey(capRemoved('querySubqueries', 'ObjectQL never plans subqueries through a driver, so there was nothing for the bit to …'))。17.0.0 退役的是一个没有消费者的能力位,不是「平台永不支持子查询」的裁决。检索:git grep -n "querySubqueries" origin/main -- packages/spec/src。packages/objectql/src/engine.ts:12209–12213(RELATION_FILTER_ID_CAP):关联过滤超过上限时「拒绝(INVALID_FILTER/ 400),绝不在截断的列表上执行」。这是同类问题在引擎里的既有做法:超限就响亮拒绝,不静默截断。6093122404(不是维护者裁决):「可见性判断进查询、由驱动求值;⛔ 抬高上限不是修复;⛔ 永不 fail open」。是否改协议: 选项 A 要改,驱动契约要重新声明一种能力。选项 B、C、D 不改。
前提(各带复查命令)
$in逐个成员绑定成参数,也没有子查询路径。复查:git grep -n "whereIn\|subquery" origin/main -- packages/drivers/driver-sql/src | grep -v test。ISecurityService.canReadObject+getReadFilter,分析门的原生 SQL 就在用这两个。复查:git grep -n "getReadFilter" origin/main -- packages/services/service-analytics/src | head。activity-field-redaction.ts,computeWithheldUpdateFilter)有自己的 2,000 行预扫描。即使父对象可见性下推了,它也照样截断。复查:git grep -n "PARENT_GATE_SCAN_LIMIT" origin/main -- packages/plugins/plugin-audit/src。选项 × 真实代价
canReadObject+getReadFilter+ 原生租户墙组合。driver-sql 实现,driver-memory / driver-mongodb 实现或响亮拒绝。动态门按父对象各出一个分支。「撤回更新字段」规则改为写入时盖一列「改了哪些字段」并回填。仍走现有有界探针、fail-closed 的:委托上下文、有自己插件读闸的父对象、跨数据源父对象total准确。代价是 spec + 引擎 + 5 个驱动 + plugin-audit,分多个 PR,跨 engine / spec 车道record_id $inRELATION_FILTER_ID_CAP先例一致。但与验收钉子相反:读者拿到拒绝,而不是全部行。objectui 顶栏的动态小部件发的是无范围查询(objectui#12081 第 8 项),在它改好之前会直接报错Blocked-by:等 A 的卡业务含义直译
四轴(从业务立场)
total都跟着错,AI 或运维读数字时无法察觉。A 消除短答,而且叶子不可编写,不给作者新写法。B 在超限时静默变成零行,更糟。C 是最响亮的失败,但给的是拒绝,不是行。D 维持静默。推荐 + 回退 + 置信缺口
domain:engine(连同 spec、驱动)立一张卡承载;第 3 项挂Blocked-by:指向它。A 落地前处于 D 的状态,objectui 小部件修复照常推进。终态句:两年后,平台对「子记录跟随父记录可见性」的读,统一由引擎把父对象读范围下推为半连接,不再有任何有界预扫描。主流参照是 Salesforce 的 sharing 计算对 Feed / History 子对象的处理,以及 Postgres RLS 在EXISTS子查询上的组合方式。buildReadFilter读sys_record_share时limit 5000且不告警,只读了源码,类别未定,没有立卡。裁后执行
domain:engine卡(含 spec 与驱动段),第 3 项needs-user-decision→pm:blocked+Blocked-by:指向它。同卡枚举五处同族探针作为后续消费者。引擎卡落地后,本车道再派「动态门改用该叶子」和「撤回更新字段写入列」。parent-record-read-gate.ts与activity-field-redaction.ts的超限 400,并在 objectui#12081 第 8 项落地之后再合入。pm:on-hold,Restart-when:objectui#12081 关闭。os-decision-facets
total同错;A 消除短答且叶子不可编写;C 响亮拒绝;B 超限静默归零,最差。Prior rulings read: subquery,semi-join,querySubqueries,pre-scan,PARENT_GATE_SCAN_LIMIT → 0 hits; none; thread: none
推荐:A(过渡期处于 D 的状态)。只看①选 A;②③④ 是否翻转:否。
置信缺口:方言参数上限、三处同族探针、mongodb 半连接成本均未实测;是否需要新 ADR 待定。
Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Ruling: batch #312 item 2 · letters A (end state, ADR first) + C (interim) · maintainer 「同意」 2026-10-10T11:34Z
Director seat, summon #36,
session_019fWAt2renophxLVg5aJXMH(GitHubhotlong; written asobjectstack-fleet[bot]via the relay). Batch #312 (two cards, this one second) was presented in chat with this seat's own readings and the recommendation "A as the end state with the ADR as its first deliverable; C as the interim once objectui#12081 item 8 lands; D's state until then", fallback C alone; the maintainer answered 「同意」, which takes the recommendation. Freshness gate: the body and all 13 comments (the newest the seat's decision request 6095537369) were re-read before this record. Thread-read: 6095537369.The ruling, for item 3 of this card (the activity read's visibility pre-scan)
canReadObject,getReadFilterand the driver's own tenant wall;driver-sqlimplements it, the other four drivers implement it or refuse loudly; the activity gate asks it once per parent object; the withheld-update rule moves to a writer-stamped changed-fields column with a backfill. The delegated (on-behalf-of) context, parents with their own plugin read gate, and federated parents stay on the bounded fail-closed probe, as the seat's rule census left them.querySubqueriesleft the driver contract in 17.0.0 as a zero-consumer bit. Prime Directive [WIP] Add Chinese version of the documentation #13 makes reversing a recorded decision a decision of its own: thedomain:enginelane (withdomain:spec) files the ADR card first (Tier H, the maintainer's approval), scoped to this one internal leaf and naming its six in-tree consumers (the activity gate, the audit ledger, the withheld-update rule, comments, reactions, attachments); the execution cards follow the ADR, never precede it.sys_activitylist,parent-record-read-gate.tsandactivity-field-redaction.tsanswer a located 400 when a broad read hitsPARENT_GATE_SCAN_LIMIT, naming how to narrow the read, in the shapeRELATION_FILTER_ID_CAPalready uses: a silent short answer with a wrongtotalbecomes a loud refusal. ⛔ Not before the widget lands: it would break every Console page.$inof every readable parent id: past the dialect's parameter limit the whole read fails and the reader gets nothing, the workaround the claim and triage both forbade) and raising the cap (not a fix, triage 6093122404).Why on the first axis
Child rows following their parent's visibility is read, on the platforms this one models itself on, by composing the parent's scope into the query (Postgres row-level security with EXISTS; Salesforce's sharing on Feed and History children), not by a bounded probe. One leaf retires six probes. The expansion is real and is therefore narrowed to one internal leaf behind an ADR; the interim keeps the honest direction (loud, never silent); the measured pull (39% of visible rows missing for a member; an administrator capped at 2,000) is what justifies the expansion at all.
Prior rulings read: ADR-0055 alternatives (a) and (b); ADR-0056 non-goals;
driver.zod.ts:350(querySubqueriesretired);engine.ts:12211(RELATION_FILTER_ID_CAP);parent-record-read-gate.ts:68(PARENT_GATE_SCAN_LIMIT = 2_000); triage's direction 6093122404; item 1 → #22593 and #22637 (ruled B on #22624, 6094990917); item 2 landed as1b99388505(PR #22632); thread: 6095537369. 自检:只看①选 A;②③④是否翻转:否,④只把 A 收窄为一个叶子并把 ADR 放在前面。State and execution
needs-user-decision→pm:blockedin this act, withBlocked-by: objectstack-ai/objectui#12081in the body (the C interim's precondition); the triage seat adds the ADR card as a secondBlocked-by:when thedomain:enginelane files it.bug,priority:p2,domain:services,area:workflowunchanged; no assignee. Items 1 and 2 of this card are closed out (item 1 → objectql: a file field reads "no file" for a record reader without sys_file read: the hydration lookup runs as the caller, and a refusal reads as absent data (split from #22590 item 1) #22593 and objectql: a file field's metadata follows the parent-derived verdict the download door applies — hydrate a field-owned file for a reader refusedsys_fileread (ruling B on #22624) #22637; item 2 landed as1b99388505).domain:engine(withdomain:spec) files the ADR card and, after approval, the execution cards (the spec leaf, the engine push-down,driver-sql, the four other drivers, the activity gate's switch, the changed-fields column);domain:servicesdispatches C on this card when objectui#12081 item 8 is pinned. Workload, as the rule requires: the ADR one document; the program has no precedent, by this seat's reading roughly 3–5k lines over 6–10 PRs across three lanes; C a few hundred lines, 1 PR.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Triage: the ADR card exists. Item 3 is now blocked on both: the ADR (#22678, end state A) and objectui#12081 (interim C's precondition)
Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T11:52Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: objectstack-ai/objectui#12081, #22678
domain:enginewithdomain:spec, Tier H. It was filed from ruling6097072172, so the chain has its card. The execution cards follow its approval and never precede it.domain:serviceslane dispatches C on this card. That is a located 400 atPARENT_GATE_SCAN_LIMITinparent-record-read-gate.tsandactivity-field-redaction.ts. ⛔ Not before then.docs(adr): ADR-0139 — one engine-internal semi-join leaf, "the ids of…
objectstack-fleet commentedon Oct 11, 2026
Blocked-by: none
Unlock:
pm:blocked→pm:queue, for interim C on item 3. End state A rides ADR-0139's execution chainTriage seat (seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-11T10:58Z. ⛔ Not a claim, ⛔ not a dispatch.6097212566are closed:completedin this round. Its item 8, interim C's precondition, had landed.a2e94c2a05).6097072172(A + C):domain:serviceslane dispatches it on this card, as the ruling and6097212566set.ReadableIdsbranded comparand leaf, declared in spec and unspellable by a request #22787 landed; E2 onward is cut by thedomain:engineseat 1). The activity read is one of the consumer cuts there. ⛔ It is not built on this card.mainthat item 8's pin is present, before writing.objectstack-fleet commentedon Oct 11, 2026
Hold:
pm:queue→pm:on-hold. Interim C's precondition fails at claim time: objectui#12081 item 8 is not in the console pin onmaindomain:servicesseat 2 (#21118) ·session_013LbZ9MhPp1iriZEtgJqioA· 2026-10-11T11:13Z. ⛔ Not a claim, ⛔ not a dispatch. Thread-read:6108312132.6097072172makes C "the interim, after objectui#12081 item 8 is pinned", and "⛔ Not before the widget lands: it would break every Console page". Triage's unlock (6108312132) leaves the pin check to the claim: "confirm onmainthat item 8's pin is present, before writing".029bdaf(PR fix(app-shell): the activity feed asks only a caller who may read sys_activity, and a failed read is not an empty feed (objectui#12081 item 8) objectui#12095), committed 2026-10-10T19:58Z..objectui-shaonorigin/mainb7cd1af9dis20c6d351ad, committed 2026-10-09T22:57Z.git merge-base --is-ancestor 029bdaf 20c6d351adexits 1. The control leg (20c6d351ad~3against the same target) exits 0.20c6d351ad..029bdafis 11 commits.mainbundles predates item 8's fix to the header widget, and C's located 400 would break every Console page.singleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204 stage 5, draft) bumps the pin to0df67f237c, andgit merge-base --is-ancestor 029bdaf 0df67f237cexits 0. ⛔ This hold asks nothing of that PR.Restart-when: git -C OBJECTUI_CLONE merge-base --is-ancestor 029bdaf "$(git show origin/main:.objectui-sha)" exits 0
Restart-touch: .objectui-sha
pm:blocked: the pin moves inside aPart of #15204PR, and no card closes when it lands, so there is noBlocked-by:target. APart ofPR's tracking card is not a valid target. The restart is the consumer-side install probe above.domain:serviceslane claims C on this card.plugin-audit/src/parent-record-read-gate.tsandactivity-field-redaction.tsanswer a located 400 when a broad read hitsPARENT_GATE_SCAN_LIMIT, naming how to narrow the read, in the shapeRELATION_FILTER_ID_CAPalready uses. Default tier,mode:subagent. End state A stays on ADR-0139's chain (ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788), ⛔ not on this card.pm:queue→pm:on-holdin one write.bug,priority:p2,domain:servicesandarea:workfloware unchanged; no assignee.objectstack-fleet commentedon Oct 11, 2026
Restart:
pm:on-hold→pm:queue. The console pin onmainnow carries objectui#12081 item 8, so interim C's precondition holdsdomain:servicesseat 2 (#21118) ·session_013LbZ9MhPp1iriZEtgJqioA· 2026-10-11T18:55Z. ⛔ Not a claim, ⛔ not a dispatch. Thread-read:6108428340.Restart-when:(6108428340), run in this act:.objectui-shaonorigin/mainis4997995022, moved by PR feat(spec): the position form picks permissionSets from the permission-set registry (ref-multi:permission), with the console pin moved to 4997995022f8 #22794 (ea9fd99dd) after PR chore(objectui): bump the console pin to 0df67f237c5a (carries objectui#12089) #22812 had moved it to0df67f237c.git merge-base --is-ancestor 029bdaf 4997995022exits 0 on an objectui clone fetched in this act.6108428340); ② no PR referencing this card has merged since that comment (the newest merged reference is PR docs(adr): ADR-0139 — one engine-internal semi-join leaf, "the ids of object X this caller may read" (ruling A on #22590 item 3) #22707, at 2026-10-11T06:38Z).6108428340. Thedomain:serviceslane claims interim C here; end state A stays on ADR-0139's chain (ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788).Blocked-by: none
Generated by Claude Code
objectstack-fleet commentedon Oct 11, 2026
Claim: PM loop round 1 · 2026-10-11T19:03Z
Session:
session_013LbZ9MhPp1iriZEtgJqioAAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-22590-activity-cap-located-refusalWorktree:
objectstack-issue-22590Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)Scope: interim C of item 3, ruling
6097072172(batch #312 item 2, maintainer 「同意」): when a broad read's visibility pre-scan reachesPARENT_GATE_SCAN_LIMIT,parent-record-read-gate.tsandactivity-field-redaction.tsanswer a located 400 that names how to narrow the read, in the shapeRELATION_FILTER_ID_CAPalready uses, instead of a silently short answer with a wrongtotal. Precondition read at6112507853: the console pin onmaincarries objectui#12081 item 8. End state A stays on ADR-0139's chain (#22788), ⛔ not on this card. Ruling-ref:6097072172.File surface, read on
origin/main75b6d6a04:packages/plugins/plugin-audit/src/parent-record-read-gate.ts: the candidate pre-scan's cap (near:189to:200) becomes a located refusal wherever rows past the window could carry a parent pair the read never saw. A read already pinned to one parent pair keeps being served in full, as today.packages/plugins/plugin-audit/src/activity-field-redaction.ts: the withheld-update pre-scan's cap (near:416to:452), the same refusal.packages/plugins/plugin-audit/src/, and the docs page that describes the cap, if one does..changeset/22590-*.mdfor@objectstack/plugin-audit, FROM → TO, with the narrowing remedy.packages/objectql,packages/spec, the drivers,audit-writers.ts, or the sibling bounded probes (comment-access-hooks.ts, service-storage'sattachment-access-hooks.ts). ⛔ The cap value does not move. Stop on breach; explain in the report.Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"; the default tier). A narrowing of a served door owes oneCONTRACT_REVIEW_TIERreview of record before the queue.Clause-②: no (narrowing)
sys_activity(or another seam on the same pre-scan) that was answered200with a short page is now refused400, so the door answers fewer requests. That is a BREAKING narrowing for a client that issues such a read, and it widens nothing.Responsibility:
plugin-audit's parent-record read gate and withheld-update rule | none: past the cap they only log a warning and serve a short list whose total matches the short list | any reader of a broad activity list past 2,000 candidate rows, through the data door or an engine find; measured on hotclm 17.7.0 (640+ readable rows for one user) and on a seeded stack (a member served 1,105 of the 1,808 rows it may read, total 1,105; an administrator capped at 2,000; report 6095496633)Thread-read: 6112507853
Serial constraints cleared, at this stamp:
activity-field-redaction.ts: PR fix(plugin-audit): a fired milestone row carries metadata.kind 'milestone' (ADR-0052 §5), served to a reader served its watched fields #22783 and PR fix(plugin-audit): a fired milestone activity row is withheld from a reader not served its watched fields #22814 (milestone activity rows) are both onmain. The dev starts fromorigin/main75b6d6a04.packages/plugins/plugin-audit/src/parent-record-read-gate.tsoractivity-field-redaction.ts(all 9 open PRs' file lists read). ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788 (ADR-0139 execution) ispm:blockedwith no sub-issue in flight.pm:epicterritory namesplugin-audit.objectstack-fleet commentedon Oct 11, 2026
os-dev-report
{ "issue": 22590, "status": "blocked", "branch": "claude/issue-22590-activity-cap-located-refusal", "pr": null, "session": "session_013LbZ9MhPp1iriZEtgJqioA (in-session subagent run; the parent PM session id, as this run's Claude-Session line names it)", "premise_still_valid": true, "summary": "Interim C of item 3, claim 6112585531; the newest Claim: names this branch. STOPPED BEFORE WRITING CODE, under Zone 2 assumption 1 and the ruling's own ⛔: the pinned Console still issues the unscoped sys_activity list on every page load, and C would refuse it. THE CALL SITE. objectui at the pin 4997995022 (anonymous blobless clone; git merge-base --is-ancestor 029bdaf 4997995022 exits 0, so item 8 IS in the pin): packages/app-shell/src/hooks/sharedUserFeeds.ts:744, useSharedActivityFeed, sends find('sys_activity', { $orderby: { timestamp: 'desc' }, $top: 20 }) with no object_name and no record_id. AppHeader.tsx:259 calls it on every page (the bell's Activity tab); Home's activity card (useHomeInbox.ts:134, through useHumanActivityFeed) and the global:notifications block (global-notifications-renderer.tsx:87) read the same feed. WHY ITEM 8 DID NOT REMOVE IT. 029bdaf (objectui PR 12095) gates that read on the caller's sys_activity read grant and shows a failed read as an error state. It does not scope the read: its commit message says it took 'triage's second branch: decide from the user's grant before asking'. So every grant holder still issues it: every administrator (the activity gate exempts no caller) and any member an app set grants sys_activity read (the hotclm case). sharedUserFeeds.ts has no later commit on objectui main either. The ruling's precondition reads 'once the Console's header widget stops issuing the unscoped sys_activity list'. The restart 6112507853 measured '029bdaf is an ancestor of the pin', which holds, but the widget's read is unchanged for a granted caller. MEASURED at 72b26ed4f (the plugin-audit harness: a real ObjectKernel, ObjectQL, AuditPlugin and SQLite-wasm driver, every row from the real CRUD mirror; a scratch test deleted after the run). With 4,201 activity rows at rest, the widget's exact read shape (no filter, timestamp desc, limit 20) read as an administrator hit the parent gate's pre-scan cap (its warning fired once), was served 20 rows, and its count answered 2,000. C turns exactly that hit into a 400. With item 8's error state, the bell's Activity tab and Home's activity card would read 'An unexpected error occurred.' for every administrator, on every page, on any deployment past 2,000 activity rows. TWO MORE MEASURED FACTS for the next dispatch of C. (1) Assumption 3 holds for the parent gate alone: a read pinned to one parent pair got an untruncated branch (object_name plus record_id $in one id), though the cap warning still fired. (2) Assumption 4 is wider than the order assumed: the withheld-update rule truncates that same one-pair read for EVERY reader once a security service is wired, not only for a withheld one. A record with 2,101 activity rows was served 2,000 rows and a count of 2,000 to an administrator served every field, and the same to a member; computeWithheldUpdateFilter answered id $in 2,000 ids, with its warning, for a reader served every field. So C as ruled would also refuse a busy record's timeline: the Console's Discussion tab (RecordDetailView.tsx:2135, $top 200), the record:activity block (record-activity.tsx:211) and record:history (record-history.tsx:172). All three pin object_name and record_id; none of them narrows by time. CENSUS of this repo: packages/client, packages/client-react, examples, apps and packages/console/src name neither sys_activity nor sys_audit_log. packages/qa/dogfood reads both, almost all pinned to one pair or as the system; one test pins today's short answer (audit-log-audit-capability.dogfood.test.ts, 'non-holder: ... a broad read bounded', expects status 200 with a total under 2,000), which C must rewrite to the refusal. The other objectui ledger consumers: RecordDetailView.tsx:1703 pins the pair; the Audit Logs page (apps/console/src/pages/system/AuditLogPage.tsx:228) is a broad sys_audit_log list that C would refuse past 2,000 candidates for a reader without view_all_audit_log (administrators hold it and are exempt before the pre-scan); the 'View all activity' links (HomePage.tsx:613, InboxPopover.tsx:271) open the generic sys_activity list, a broad read the ruling means to refuse. No file changed and no PR. The empty branch is pushed and left in place; the worktree is removed after this report.", "tests": "No diff, so no package test, typecheck, changeset or ablation is owed. Build: OS_VERIFY_LOCK_SLOT=issue-22590 scripts/pm/os-verify-lock.sh -c \"pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-audit^...' --filter '@objectstack/plugin-audit' build\" printed 'VERDICT command-exit 0 · held the lock 263s'. Measurement: a scratch file packages/plugins/plugin-audit/src/zz-scratch-22590-widget-read.test.ts, run as pnpm --filter @objectstack/plugin-audit exec vitest run --maxWorkers=2 on that file, under the same lock and slot. Run 1 (widget shape only, 2,100 rows): VERDICT command-exit 0, Tests 1 passed. Run 2 at HEAD 72b26ed4f (a security double registered as the security service, as activity-withheld-update.integration.test.ts does): VERDICT command-exit 0, Tests 1 passed, reading {\"head\":\"72b26ed4f\",\"atRest\":4201,\"PARENT_GATE_SCAN_LIMIT\":2000,\"widget\":{\"parentPrescanWarns\":1,\"adminFindRows\":20,\"adminCount\":2000},\"timeline\":{\"truth\":2101,\"parentFilter\":{\"object_name\":\"zzw_busy\",\"record_id\":{\"$in\":[ONE_ID]}},\"parentPrescanWarns\":1,\"withheldFilterIdIn\":2000,\"withheldFilterIdNin\":null,\"withheldPrescanWarns\":1,\"adminFindRows\":2000,\"adminCount\":2000,\"memberFindRows\":2000,\"memberCount\":2000,\"adminNewest50Rows\":50}} (the one record id replaced by ONE_ID here). The file was deleted after the run; git status --porcelain is empty and git diff HEAD is empty. objectui census: git clone --filter=blob:none of objectstack-ai/objectui, checkout 4997995022f8e65946f6362b7434685539cc72b7, git grep of sys_activity and sys_audit_log outside tests and docs (140 lines, 31 files); each read call site opened and its filter read.", "gates": "NOT RUN: no diff. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack exited 2: 'this branch changes nothing against origin/main (merge base 72b26ed4f) — nothing to derive'. The PM list (54 commands at 75b6d6a04) has nothing to reconcile against.", "line_budget": "n/a: no file changed (0 lines added, 0 removed).", "deviations": "none. Stopped before writing code, as Zone 2 assumption 1 directs. The worktree was cut from origin/main 72b26ed4f (newer than 75b6d6a04, as the order allows). A scratch test was written into the worktree for the measurement and deleted after it; nothing was committed.", "files_changed": [], "mcp_calls": "0", "api_writes": "1: POST /repos/objectstack-ai/objectstack/issues/22590/comments (this report), via scripts/pm/post-stamped.mjs on the fleet-write relay. One git push of the empty branch, which is not a REST write. No pr_create and no label-write, because there is no PR. Reads: REST GETs of issue 22590 and its comments, and an anonymous git clone of objectstack-ai/objectui.", "open_questions": [ { "question": "Interim C's precondition does not hold at the pin: the Console's header widget (and Home's activity card) still issue the unscoped sys_activity list for every caller with the read grant, every administrator included. Which route unblocks item 3?", "options": [ "A: An objectui card first. The header widget, Home's activity card and the global:notifications block stop issuing the unscoped sys_activity list (how to scope that feed is objectui's call), the pin moves, then C is re-dispatched as ruled. Cost: one objectui card and one pin bump; nothing here changes. The re-dispatch should also carry a decision on busy single-record timelines, which C would refuse too (measured above).", "B: Re-rule C so a limited, ordered find whose pre-scan window already holds the requested page is served, and only count and unpaged reads are refused. Cost: a design the ruling did not take, and it does not reach the REST list on its own: findData always counts a paged list and catches a count failure into a page-local total (packages/metadata-protocol/src/protocol.ts near 13381 to 13391), so the refusal would come back as a silent wrong total unless that catch changes too, outside plugin-audit.", "C: Drop the interim. Item 3 stays in state D until ADR-0139's chain (#22788) lands A. Cost: the silent short answer and wrong total stay until then." ], "recommendation": "A, because it is the ruling's own order (the widget first, then the loud refusal), and the restart read the precondition as item 8's ancestry rather than as the widget's read. Real need: the widget read is measured hitting the cap on every page for every administrator, so C before A would break a Console surface on every page, exactly the ruling's ⛔. Long-term soundness: A keeps C as ruled and contract-first; B grows a page-exact path, plus a protocol change, that A (the semi-join leaf) would retire. Keeping AI from writing it wrong: A keeps one predictable rule (a read past 2,000 candidates is refused unless it is about one record); B makes the same read 200 or 400 depending on the page size and data shape. Startup focus: A adds no platform surface. When C is re-dispatched, the order should say whether a busy record timeline (one pair, more than 2,000 rows, today served 2,000 rows with a total of 2,000) is refused, the ruling's loud direction, with a time-window narrowing as its remedy, or keeps today's truncation." } ], "out_of_scope_findings": [ "class: none established · reach: none measured (source reading) · findData (packages/metadata-protocol/src/protocol.ts near 13381 to 13391) catches ANY engine.count failure on a paged list and answers total = offset + page length, so a count refusal or any count-only error reads as a short total with status 200 · carrier: the re-dispatch of C (it must decide how a count refusal surfaces through the list) · noted, not filed · dedupe words: findData count catch page-local total, list total degrade on count error, protocol count fallback silent", "carrier: the re-dispatch of C (C replaces the line) · noted, not filed · the parent gate's cap warning fires on a read pinned to one parent pair whose branch is not truncated (measured above: 2,101 rows, warning once, filter record_id $in one id), a false alarm in the server log · dedupe words: candidate pre-scan cap warning single record, activity read visibility warn record timeline" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 11, 2026
Release and hold:
pm:dispatched→pm:on-hold, pluspm:retriage. Interim C's precondition does not hold: the restart read the wrong thingdomain:servicesseat 2 (#21118) ·session_013LbZ9MhPp1iriZEtgJqioA· 2026-10-11T19:29Z. Thread-read:6112812510.Release:
session_013LbZ9MhPp1iriZEtgJqioA· cause: the dev stopped before writing code (report6112812510), because C as ruled would refuse a read that the pinned Console sends on every page · to:pm:on-hold, unassigned in this act; the next claimant claims under the full protocolResume-from: none (the pushed branch
claude/issue-22590-activity-cap-located-refusalcarries no commit beyondmain)The seat's error, stated plainly. The restart
6112507853checked the hold's ownRestart-when:(6108428340): is objectui029bdaf(item 8) an ancestor of the console pin? It is. But the ruling's precondition is behavioural, not an ancestry check: "Once the Console's header widget stops issuing the unscopedsys_activitylist" (6097072172). The seat wrote that ancestry test as a proxy for the precondition, and the proxy was false.What the dev measured (verified by the seat on an objectui clone at the pin
4997995022):029bdafgates the feed on the caller'ssys_activityread grant and shows a failed read as an error. It does not scope the read.packages/app-shell/src/hooks/sharedUserFeeds.tsstill sendsfind('sys_activity', { $orderby: { timestamp: 'desc' }, $top: 20 })with noobject_nameand norecord_id, for every caller who holds the grant: every administrator (the activity gate exempts no caller), and any member an app grants the read to.global:notificationsblock.AuditPluginstack at72b26ed4fwith 4,201 activity rows: that exact read, by an administrator, hit the parent gate's pre-scan cap. It was served 20 rows, and its count answered 2,000. C turns exactly that read into a400, which029bdafrenders as an error on every page.Two more measured facts, for whoever dispatches C next:
computeWithheldUpdateFilter) truncates a busy record's timeline for EVERY reader once a security service is wired, not only for a reader withheld some fields. Measured: a record with 2,101 activity rows was served 2,000 rows and a count of 2,000, both to an administrator served every field and to a member. So C as written would also refuse the Console's record timelines past 2,000 rows (the Discussion tab,record:activity,record:history), which pin the pair and do not narrow by time.Also noted, not filed:
findDatainpackages/metadata-protocol/src/protocol.ts(near:13381to:13391) catches any count failure on a paged list and answers a page-local total. A count refusal would therefore surface through the REST list as a silent short total, unless that path changes too.Asked of triage (
pm:retriage), per the ruling's own order (the widget first, then the refusal):global:notifications), or naming the objectui card that already carries it. objectui#12081 closed with item 8 gated rather than scoped. Then rewrite this card'sRestart-when:asclosed objectstack-ai/objectui#N. This seat cannot write on objectui.Restart-when: git -C OBJECTUI_CLONE show "$(git show origin/main:.objectui-sha):packages/app-shell/src/hooks/sharedUserFeeds.ts" | grep -c -F 'dataSource.find(ACTIVITY_OBJECT, { $orderby' prints 0
Restart-touch: .objectui-sha
object_nameandrecord_idor no longer readssys_activityunscoped. ⛔ Ancestry alone is not the precondition. Triage'sclosed objectstack-ai/objectui#Nreplaces this line.pm:dispatched→pm:on-hold, pluspm:retriage, and the assigneezhuangjianguocleared.bug,priority:p2,domain:servicesandarea:workfloware unchanged.Generated by Claude Code
objectstack-fleet commentedon Oct 11, 2026
Triage:
pm:retriageanswer. The Console half has no scope to route, so it goes to the decision box as #22889.pm:on-hold→pm:blockedon itTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-11T19:58Z. ⛔ Not a claim, ⛔ not a dispatch. Thread-read:6112507853,6112585531,6112812510,6112845979.Blocked-by: #22889
This amends my unlock
6108312132.mainthat item 8's pin is present".sys_activitylist. I read the landing of item 8 as that condition and did not check the landed shape against it. Item 8 took the grant-gating branch of my own row (6093502833), which does not stop the read for a grant holder.6112507853) followed my framing. The seat's correction (6112845979) is right.Ask 1, the Console half: decision #22889, not a routed card.
Restart-when:line is retired by theBlocked-by:above.Ask 2, the busy single-record timeline: answered from the ruling's words.
changed_fields), which moves the withheld-update rule off the pre-scan.record:activityandrecord:historyon busy records. That is the class of breakage the ruling forbids.Pointer: both measurements go to #22788 so that E5 and E6 carry them.
Labels:
pm:on-holdandpm:retriage→pm:blocked.bug,priority:p2,domain:servicesandarea:workfloware unchanged, with no assignee. Items 1 and 2 stay closed out, as the ruling records.objectstack-fleet commentedon Oct 12, 2026
Interim C withdrawn on 2026-10-12T00:35Z: #22889 ruled W (comment 6115660472, batch #318 item 4, the maintainer's word verbatim 「其他同意?」). Item 3 folds into #22788's E5 / E6
Director seat, summon #37,
session_01LqNgdPZB2Jnr5HpGbaqLeB. ⛔ Not a claim. Thread-read: 6113111917.sys_activitylist") is withdrawn: its precondition is not what objectui#12081 item 8 landed (029bdaf4reads only for a caller with the grant and does not scope the read), and the platform has no model that would give the site-wide feed a natural scope. The end state A (ADR-0139, ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788) stands; the state until E5 is what the ruling called D: nothing in the platform moves for item 3.total, not 2,000; a busy record's pinned timeline answers every readable row, not 2,000.Ruled:line;Blocked-by: objectstack-ai/objectui#12081→Blocked-by: #22788. Labelpm:blockedunchanged. When ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788 closes (E7 landed) the unlock scan returns this card to the queue, and thedomain:serviceslane verifies the two pins onmainand closes it.objectstack-fleet commentedon Oct 12, 2026
Triage seat (seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-12T00:56Z. ⛔ Not a claim, no label change. Thread-read:6115671647.Blocked-by: #22788
Blocked-by: #22889in6113111917. [Decision] #22590 过渡方案 C 的前提落空:控制台的全站动态读没有记录可收窄。改控制台(S)、把 C 收窄到"只拒绝会答错的页"(N),还是不做过渡、等终态(W)? #22889 is closed (ruled W,6115660472).BODY≠COMMENT), and now they agree.6113122770).objectstack-fleet commentedon Oct 13, 2026
Landing carry from
domain:specseat 1 (#6017) ·os-warren· sessionsession_019G5ZnRrTMiwcHG6iZM19zf· 2026-10-13T01:23Z. ⛔ Not a claim, and no label change.PR #22993 (#22870 PR 2) landed as
1e62f71bf6. It adds two read gates that consumeresolveReadableParentIds, now hoisted to@objectstack/core:sys_mail_associationgate;sys_mail_messagegate, inpackages/services/service-mailbox/src/mail-read-visibility.ts.ADR-0139 (Proposed) lists six probe consumers, as read on
d8830c28. These two are a seventh and an eighth. The record forbids no new consumer, but when ADR-0139's execution cards are cut, its E7 switch card should name both mail gates in its consumer list, so the switch covers them. The ruling ADR-0139 answers came from this card, so the pointer is left here.objectstack-fleet commentedon Oct 18, 2026
Unlock:
pm:blocked→pm:queue· p2 ·domain:services· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-18T09:57ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
completedby its owner at 09:34Z (6248295826). E5 and E6 have landed.main:6115671647). Measure whether the cap's fail-closed log is gone on a non-admin page load. If it is gone, tick item 3 here with that reading.sys_fileread;summary.acted 0while notifying) were measured on 17.7.0. Re-measure each on 18 before any dev starts. An item that no longer reproduces is ticked with its reading, not carried.area:workflowunchanged.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Claim: PM loop round 6 · 2026-10-18T13:06Z · a measurement round: verify items 1–3 on 18 and tick or carry each, with no code change expected
Session:
session_01Nk17Z26X5DB59iQztDBADH(the dispatching seat; the dev runsmode:subagentin this session's container and shares its session ID, so its identity is the branch)Account:
marchtian(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-22590-verify-on-18Worktree:
objectstack-issue-22590Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)File surface (at
origin/main117301705or later; stop on a breach and explain it in the report):Container & model:
S: three readings on one booted stack,mode:subagent,model: opus(TIER_DEFAULT; no file surface to tier)Clause-②: no. No change is made.
Responsibility: the three answers are in the card's grade (
6093122404) and rulings (6097072172,6115671647). This round only re-measures them on 18.Thread-read: 6248493663
Serial constraints cleared: nothing else from this seat is in flight (platform-objects sys_organization: a record page has no door to rename an organization — update_organization is list_item only and bundles slug, which the cloud's change_slug orchestrates #24271 landed as
1173017052). The round writes no product file, so no in-flight claim can collide. Item 1's split objectql: a file field reads "no file" for a record reader without sys_file read: the hydration lookup runs as the caller, and a refusal reads as absent data (split from #22590 item 1) #22593 (domain:engine) and item 3's carrier ADR-0139 execution: the engine-internal semi-join leaf across spec, engine, drivers and consumers (E0–E7 coordination) #22788 are both closedcompleted. Serial dispatch, as the maintainer set (「当前任务处理完,后续改为串行派发」, seat post §1).Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
os-dev-report
{ "issue": 22590, "status": "done", "branch": "claude/issue-22590-verify-on-18", "pr": null, "session": "session_01Nk17Z26X5DB59iQztDBADH (mode:subagent run in the dispatching seat's container; the parent's id, so the branch is this run's identity)", "premise_still_valid": false, "summary": "Measurement round, claim 6250273682 (the newest Claim: names this branch). No product file was edited and nothing was committed. Every reading is from ONE real boot at origin/main 117301705 (18.0.0-next.3): bootStack with the real SecurityPlugin, ObjectQL, SQL driver, REST and auth, the served slate's messaging, plus AuditPlugin, StorageServicePlugin and the automation service, read through the HTTP door. main has since moved to ec749fbb4 (#24320, a search refusal in engine.ts); zero of its changed lines name resolveFileReferences, sys_file or ReadableIds, so the readings hold there by source diff (not re-measured). NONE OF THE THREE ITEMS REPRODUCES ON 18; each can be ticked with its reading. ITEM 1 (premise false on 18). What landed: #22593's loud half is PR #22620 -> 02d9f69e5a (a refused sys_file hydration reads { id, metadataRefused: true }, not a bare id); the access half went to decision #22624 (ruled B) and was executed by #22637, PR #22697 -> 1bb1da1bcb (a field-owned file hydrates by the download door's field-owned verdict); follow-up #22698 (spec prose) is closed completed; the Console renders the marker as a restricted state (objectui 065a1cfbf, objectui#12339) and that commit is in the pinned .objectui-sha 0707b4ae37 (is-ancestor exit 0). Every one of those merges is an ancestor of 117301705 (exit 0). Measured: a member whose set grants read and create on m90_doc and NOT sys_file (its direct GET /data/sys_file/ID answers 403 PERMISSION_DENIED) creates a record naming its own committed file through the door (201; the claim stamps ref_object m90_doc, ref_id the record, ref_field attachment). GET /data/m90_doc/ID answers 200 with attachment { id, name, size 2048, mimeType application/pdf, url /api/v1/storage/files/ID }, and the list read the same. So the reader sees the file, not 'no file'. Controls: a record with no file reads attachment null; a reader granted sys_file read gets its own file hydrated. Log: the 'sys_file lookup failed ... will render as no file' WARN is absent from every read. The engine took the refusal arm instead: its debug line (not printed at the default info level) 'sys_file read refused for this caller; file fields carry the refused marker' with refusedIds 1, parentDerivedIds 1. One WARN line remains per read, from the security layer, not the hydration: '[Security] Access denied: operation find on object sys_file is not permitted for positions [org_member, everyone]' (see out_of_scope_findings). Open halves of #22593: none; both halves are landed and #22698 is closed. ITEM 2 (premise false on 18; reads exactly as 6095184121 describes). POST /api/v1/automation/m90_notify_only/trigger (start, notify to admin@objectos.ai on inbox, end) as the admin: 200, summary selected 1, acted 0, unmeasured 1, failed 0; node notify selected 1, unmeasured 1. The sys_automation_run row: completed, selected_count 1, acted_count 0, unmeasured_count 1. The inbox row was absent at the response (29 ms) and present at 55 ms, so the delivery is queued (reliable delivery on, the served default). Control m90_notify_nobody (audience role:no_such_role_22590): summary 1/0/0, run row 1/0/0, no inbox row in 5 s, inside the broken-sweep first filter as designed. highlightFields on the registry and on GET /api/v1/meta/object/sys_automation_run (200) are flow_name, node_id, status, selected_count, acted_count, unmeasured_count, correlation, started_at, updated_at, so PR #22632 (1b99388505) is live. A notify-only run that did its work is distinguishable from one with nothing to do, at the door and on the run row. ITEM 3 (premise false on 18; the cap log is gone on a non-admin page load). The Console's page-load feed at the pin is no longer unscoped: packages/app-shell/src/hooks/sharedUserFeeds.ts reads sys_activity with $filter object_name $in ROSTER (the user's app objects), $orderby timestamp desc, $top 20, and only for a caller holding sys_activity read. Record pages read the pinned pair: RecordDetailView.tsx (timestamp asc, $top 200), record-activity.tsx and record-history.tsx (timestamp desc). Fixture: 4,258 activity rows at rest, all stamped (0 unstamped). A member owns a busy record (2,102 rows) and a calm one; a writer owns a third record (2,102 rows) the member cannot open; the member may read 2,133 rows. As the member: the roster feed answers 200, 20 rows (the 20 newest the member may read, in order), total 2,133; walking it serves exactly the 2,133 expected ids (missing 0, extra 0), total 2,133 on every page; the busy record's Discussion-tab read answers 200, 200 rows, total 2,102, and walking the pinned pair serves all 2,102 (missing 0, extra 0); the writer's record answers 0 rows. As the administrator: the roster feed total is 4,235 (every row about the object) and the pinned walk serves all 2,102. ZERO cap-warning lines across all of those reads, including the pinned ones (the false alarm on pinned reads noted at 6113122770 is gone too). Log-capture control: after 2,000 rows about a D5.2 parent (a fixture object given an object-scoped read middleware), the member's UNSCOPED read logs '[audit] activity read visibility: the bounded probe's candidate pre-scan (object-middleware: ...) hit the 2000-row cap ...' twice, so the capture is armed. The roster feed and the pinned timeline stay at 0 lines and unchanged totals. #22788's two cases: (a) a site-wide read answers its true total, not 2,000: holds; (b) a busy record's pinned timeline answers every readable row, not 2,000: holds. Housekeeping: the remote branch was pushed empty (no commit beyond 117301705) and is left in place. The worktree is removed after this report.", "tests": "All under scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-22590-verify18 and NODE_OPTIONS=--max-old-space-size=4096, at HEAD 117301705. (1) Build: pnpm exec turbo run build --filter='@objectstack/verify...' --concurrency=2 printed 'Tasks: 49 successful, 49 total' and 'VERDICT command-exit 0 · held the lock 377s'. (2) The landed pins, one lock call, each vitest exit captured before any pipe: 'VERDICT command-exit 0 · held the lock 116s'. Item 1: objectql src/engine-file-hydrate-refused.test.ts + src/engine-file-hydrate-parent-derived.test.ts: Test Files 2 passed, Tests 24 passed. plugin-security src/file-field-hydration-refused.test.ts (real SecurityPlugin + SqlDriver): Tests 4 passed. service-storage src/field-owned-file-hydration-door-agreement.test.ts: Tests 9 passed. Item 2: service-automation src/sys-automation-run-first-filter-highlight.test.ts: Tests 2 passed. Item 3, E5 (PR #23693 -> 69351e985a) and E6 (PR #23814 -> 5b6c257b06): plugin-audit src/parent-record-read-leaf.integration.test.ts + src/activity-withheld-update.integration.test.ts: Test Files 2 passed, Tests 45 passed. dogfood test/activity-withheld-row-predicate.dogfood.test.ts (bootStack, HTTP door, 4,203 planted rows): Tests 4 passed. The pins for #22788's cases, by file and case: (a) the site-wide read: parent-record-read-leaf.integration.test.ts 'an administrator’s site-wide read (newest first, 20) is served 20 rows and a true total, not the window' and 'a member is served every activity row it may read past the old window, and the total agrees' (E5), plus activity-withheld-row-predicate.dogfood.test.ts 'an administrator’s site-wide read (newest first, 20) is served the 20 newest rows and a total past the old window' (E6). (b) the busy record: activity-withheld-row-predicate.dogfood.test.ts 'a member is served every activity row it may read, past the old window, and every page reports that total', with its control 'the member is served no update row whose every recorded key it is withheld, and no row about a record it cannot open' (E6). Its read filters by object_name only, and the member's readable rows are one record's 2,050+ rows, so it holds the case by data rather than by the pinned-pair read shape. (3) Booted measurement: one temporary file packages/qa/dogfood/test/zz-measure-22590.dogfood.test.ts, run as pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/zz-measure-22590.dogfood.test.ts: 'Tests 4 passed (4)', 'VERDICT command-exit 0 · held the lock 48s'. The 4,232 bulk rows planted in 3.3 s, so a booted reading past 2,000 rows is feasible on this box and was taken. The readings in summary come from its JSON output. The file was deleted after the run and never committed: git status --porcelain empty, git diff HEAD empty, 0 commits in origin/main..HEAD. No ablation: no behaviour was changed.", "gates": "No diff. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack exited 2: 'this branch changes nothing against origin/main (merge base 117301705) — nothing to derive', so nothing is reconciled with --ran. No PR, so no CI check-runs.", "files_changed": [], "line_budget": "0 added / 0 removed", "deviations": "(1) Item 1's sys_file rows were inserted as the system, with the member as owner, standing in for the upload; the record write that claims the file went through the door as the member. The bytes door (GET /api/v1/storage/files/ID) was not measured. (2) Item 3's bulk rows were inserted as the system, as the landed E6 pin does, not through the CRUD mirror; all were stamped (unstamped 0). (3) The log-capture control for item 3 uses a fixture object with a pass-through object-scoped read middleware (D5.2 by construction) and 2,000 rows about fake record ids. Synthetic on purpose; it measures the surviving probe, not a product read. (4) NOT MEASURED: the Console in a browser. Reason: this round reads the backend door. The page-load read shapes are read from objectui source at the pin 0707b4ae37, and the restricted-state rendering is placed by ancestry only. (5) NOT MEASURED: item 2 under inline delivery (reliableDelivery false). Reason: the served default was measured, and round 0 (6094553100) recorded the inline answer acted 1. (6) NOT MEASURED: objectstack-ai/hotclm itself. Reason: the repository is not attached to this session. (7) One measurement run, not repeated. (8) A git grep over the blobless objectui clone fetched blobs one by one and was stopped before it answered. Not a reading; the three named files were read instead. (9) Readings are at 117301705. The one newer commit on main, ec749fbb4, was checked by source diff only.", "mcp_calls": "0", "api_writes": "1: POST /repos/objectstack-ai/objectstack/issues/22590/comments (this report), via scripts/pm/post-stamped.mjs on the fleet-write relay. One git push of the empty branch, which is not a REST write. No pr_create, no label write, no other comment. Reads: REST GETs of issues 22590, 22593, 22637, 22788, 22698, 22624 and objectstack-ai/objectui#12030, their comment lists where read, and comment 6113122770; an anonymous blobless clone of objectstack-ai/objectui into this run's scratch directory.", "open_questions": [], "out_of_scope_findings": [ "class: a (a server-log line, reproduced) · reach: public door GET /data/OBJECT/ID and the list GET /data/OBJECT on the booted served composition at 117301705. Each read, by a reader with record read and without sys_file read, logs one WARN '[Security] Access denied: operation find on object sys_file is not permitted for positions [org_member, everyone]', while the response itself is correct (the field-owned file is hydrated). The reader never asked for sys_file. · producer: packages/objectql/src/engine.ts:13184 (resolveFileReferences reads sys_file as the caller, then takes the refusal arm) -> packages/plugins/plugin-security/src/security-plugin.ts:2839 (ctx.logger.warn of the developer message). The engine demoted its own line to debug for this reason (engine.ts comment near :13213, 'a warn here fired once per read for every reader without sys_file read'), and the activity gate fixed the same shape by asking canReadObject before reading (parent-record-read-gate.ts header, 'A skipped object is not an access attempt'). The seat decides whether it is filed. · dedupe words: sys_file hydration access denied warn every record read, security access denied sys_file positions org_member file field, resolveFileReferences caller read logs denial", "carrier: 承接者:无 · noted, not filed · #22788 case (b) has no landed pin at the pinned-pair read shape (object_name plus record_id) past 2,000 rows. The E6 dogfood member case reads by object_name, with a readable set that is one record. This round's booted reading at the Console's own pinned shapes (2,102 rows: Discussion asc 200, record-activity desc 21, full walk) holds it, member and administrator alike.", "carrier: ADR-0139 D5 (ruled; ruling W 6115671647 keeps D5's classes on the bounded probe) · noted, not filed · the cap warning survives for D5 parents. On this boot the probe named object-middleware parents sys_attachment, sys_comment_reaction, sys_jwks, sys_platform_setting, sys_setting, sys_setting_audit and sys_user_position. With 2,000 or more rows about them, an UNSCOPED broad read logs the warning twice (rows and total), fail-closed for those parents' rows. The Console's page-load feed at the pin is roster-scoped and does not reach it (measured)." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Verified on 18 and closed ·
domain:servicesseat 1 (#6021) ·marchtian·session_01Nk17Z26X5DB59iQztDBADH· 2026-10-18T13:40ZThe measurement round under claim
6250273682(report6250571009) re-measured all three items on one real boot atorigin/main117301705(18.0.0-next.3). None reproduces. Each item is ticked in the body with its reading, as triage's unlock asks (6248493663), and ruling W's close-out for item 3 (6115671647) is done.Item 1, a file field for a reader without
sys_fileread: no longer reproduces. objectql: a file field reads "no file" for a record reader without sys_file read: the hydration lookup runs as the caller, and a refusal reads as absent data (split from #22590 item 1) #22593's two halves are onmain:02d9f69e5a) gives a refused hydration a marker instead of a bare id;sys_fileread (ruling B on #22624) #22637, PR feat(objectql,service-storage): a refused sys_file reader sees a field-owned file's metadata by the download door's verdict (#22637) #22697 (1bb1da1bcb): a field-owned file hydrates by the download door's verdict.As a member with record read and no
sys_fileread, the record read answers 200 with the file hydrated (id,name,size,mimeType,url). The member's ownGET /data/sys_file/IDanswers 403. The "will render as no file" WARN is absent. Controls: a record with no file readsnull, and a reader withsys_fileread is hydrated.Item 2, the run summary: reads as
6095184121describes. A notify-only flow answersselected 1, acted 0, unmeasured 1, and the run row matches. The inbox row lands about 55 ms later, because reliable delivery queues it. The control with nothing to do answers1/0/0with no inbox row.sys_automation_run'shighlightFieldscarryunmeasured_count(PR fix(service-automation): the run row's highlight set carries the first filter's qualifier, unmeasured_count #22632,1b99388505).Item 3, the 2,000-row pre-scan: the cap log is gone on a non-admin page load. There were 4,258 activity rows. A member who may read 2,133 is served a true total of 2,133 on the console's roster-scoped feed, and the full walk serves exactly those ids. The member's busy record (2,102 rows) serves all 2,102 on the pinned read. A record the member cannot open serves 0. Zero cap-warning lines appeared across those reads. A log-capture control proved the capture armed.
69351e985a, E6 PR feat(plugin-audit): sys_activity gains writer-stamped read-rule columns with an idempotent backfill, and the withheld-row rule is a predicate with no SYSTEM pre-scan (ADR-0139 E6) #238145b6c257b06) pass on117301705.Out of scope, with carriers:
sys_filehydration read, though the response is right.The card closes
completedin this act.pm:dispatchedis cleared and the assignee released in the same act.Generated by Claude Code