Repository navigation
record page open: four serial waves — the related cards' top=1 count reads repeat what their top=3 reads already answered, security/explain is sent twice again, and the timeline, comments, attachments and user list load in the record's own wave (1.9 s in-app, 6.5 s on a full load at ~350 ms RTT) #13010
Copy link
Copy link
Closed
Labels
area:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2
Description
What was measured
Setup:
- Staging environment
os-6hf1jq, HotCRM 5.1.0. Cloud94704206, so objectuic4dd7078ed4aand frameworkbe076dad28a0. Warm kernel. - Built-in Chromium pane, HTTP/2.
- The user's round trip to the origin is about 350 ms:
/api/v1/healthmedian 353 ms over 5 calls, and 8 concurrent health calls finish in 345–368 ms. - Record: opportunity
3c6SWnT0jFcWZOm9, which has 3 line items, 3 quote slots and 3 task slots.
What the timings mean: at that round trip, every serial wave the record page adds costs about 0.35 s before any server time.
In-app open (list → record): title at 1.86 s, line items at 1.97 s, 15 requests in 4 waves
| Wave | Starts | Requests | Ends |
|---|---|---|---|
| 1 | 40–59 ms | approvals/requests?object&recordId; security/explain ×2 (identical, same millisecond); data/crm_opportunity?populate=organization_id,created_by,updated_by,owning_business_unit_id…; data/sys_user?top=50&select=id,name,email,image; data/sys_activity?populate=actor_id&top=200; data/sys_comment?thread_id=…; data/sys_attachment?top=100 |
0.68–1.81 s; the record itself lands last, at 1.81 s |
| 2 | 995 ms | crm_quote / crm_opportunity_line_item / crm_task related cards, top=3 |
1.81–2.34 s |
| 3 | 1858 ms | the same three related reads again, top=1 |
2.35 s |
| 4 | 2343 ms | PATCH sys_user_preference/… (recent items) |
2.84 s |
- Wave 3 asks for nothing wave 2 did not already answer. The
top=3response carries the real count:{ records: 3, total: 6, hasMore: true }for an opportunity with 6 lines. So thetop=1read per card is a full extra wave plus 3 requests. - Wave 2 starts at ~1 s. The related-card reads need only the record id, which is in the route, yet they start after something in wave 1. The gate was not traced.
security/explainis sent twice for one record open. Every record-detail open costs two separate/api/v1/security/explainround trips, where the grid's sibling hook batches #8014 and record page open: the same record GET is issued twice concurrently and security/explain four times — the in-flight GET sharing from objectui#5544 covers only the callers that opted in #11699 closed this. At this pin it is back: two identical POSTs at the same millisecond. The only record-scope fetcher ispackages/plugin-grid/src/hooks/useRecordCrudVerdicts.ts:371.- Wave 1 carries four reads the first paint does not show. The timeline (
sys_activitytop=200withpopulate=actor_id), comments, attachments and thesys_usertop=50list load on every open, before the 活动 tab is opened. They share the wave with the record read. Under the server's concurrent-request queueing (objectstack-ai/cloud#3053), the record read's TTFB goes from 0.5 s alone to 1.76 s in this wave.
Full load of the same kind of record URL: record body at 6.5 s, 49 requests
data/crm_opportunitystarts at 3.82 s, behind seven boot waves:runtime/config;get-session;- discovery / i18n / organization;
meta/appandsys_user_preference;- the whole
meta/object(110 KB),meta/view,meta/page,settings; settings/localization,meta/dashboard,meta/report,ai/agents,approvals,packages(300 KB);- then the record wave.
- After the whole registry was read,
meta/object/crm_opportunity,crm_quote,crm_opportunity_line_item,crm_task, thencrm_account,sys_userandcrm_contactare each fetched again on their own. - Already filed: the boot waves, the whole-registry read,
meta/dashboard/meta/report/ai/agents/ bell feeds on open are console open: page routes read the whole object registry (751 KB), the chrome's dashboard and report getters add a serial wave, and nine calls the page does not read run on open #12980, for page routes. This card does not repeat them. It adds that a record route pays the same, plus the per-object re-reads, plus the four waves above.
Wanted
- A related card counts from the
totalits own list read returns. No secondtop=1read per card. - The related-card reads start in the first wave, from the route's record id.
- One
security/explainper record open: share the in-flight request, as Every record-detail open costs two separate/api/v1/security/explainround trips, where the grid's sibling hook batches #8014 did. - The timeline, comments, attachments and the user list load when their tab or panel is shown, or after first paint, not in the record's wave.
- A record route does not re-read an object's metadata that the registry read already returned.
Acceptance
- The same in-app open, measured the same way: at most 2 waves before the record title and the related cards are drawn; one
security/explain; notop=1follow-up read; nosys_activity/sys_comment/sys_attachment/sys_userread until their tab is opened. - A test per item.
How to reproduce the measurement
Open any record from a list in the console. In the page:
performance.clearResourceTimings()before the click;- then read
performance.getEntriesByType('resource')filtered to/api/for start, end and TTFB.
When firing the same URL concurrently, add a unique query parameter and cache: 'no-store'. Otherwise Chromium's HTTP cache lock serializes identical GETs and fakes a server-side staircase.
Dedupe
Searched objectui, objectstack and cloud, open and closed, for "record page slow", "record detail load", "waterfall", "security/explain twice", "related list count", "record page performance".
- Neighbours:
- objectui#11699 and Every record-detail open costs two separate
/api/v1/security/explainround trips, where the grid's sibling hook batches #8014 (closed: the duplicate GET / explain, now back for explain); - objectui#12701 (closed: approvals and record fetch counts);
- objectui#12809 (closed: splash waves);
- objectui#12980 (open: page-route boot reads);
- objectstack#10757 (open: per-request DB legs);
- cloud#2914 (open: control-plane console open).
- objectui#11699 and Every record-detail open costs two separate
- None covers the record route's own waves, the redundant count reads or the eager feed reads.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2
Activity
objectstack-fleet commentedon Oct 19, 2026
Triage:
pm:queue· the maintainer's p2 anddomain:uistand · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-19T01:56ZTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch.
pm:*state. It now enterspm:queue.origin/main78b07f468a:packages/plugin-grid/src/hooks/useRecordCrudVerdicts.tsis the record-grainedPOST /api/v1/security/explainfetcher the card names.top=1count re-read;security/explain;meta/objectre-reads after the whole registry.The boot waves stay with objectui#12980. The
domain:uiseat sequences the two, because they touch the same providers.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 13 (
priority:p2, part 1: Wanted items 1–2; item 3 dropped, premise disproved)Session:
session_01XVp19E1MYUTFM3dnkwz8gBAccount:
baozhoutao(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-13010-record-open-wavesWorktree:
objectui-issue-13010Domain:
domain:uiSeat:
domain:ui#2File surface (amended in place after the dev's measured stop, report
6258677488):packages/components/src/renderers/layout/containers.tsx(thepage:tabsbadge probe effect and itsctx.data.idgate),packages/components/src/hooks/related-count-store.ts,packages/plugin-detail/src/renderers/record-reference-rail.tsx(fetchEntry, to send its read through the store),packages/app-shell/src/views/RecordDetailView.recordOpenWaves-13010.test.tsx(the dev's harness) and tests beside these, and one.changeset/13010-*.md(each touched packagepatch). Was:RecordDetailView.tsx,useRecordCrudVerdicts.ts. Neither holds the mechanism, so both are dropped. ⛔ Items 4 and 5 are not in this part. ⛔ No new export. Stop on breach; explain in the report.Container & model:
M,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuireads no path-derived mandate)Clause-②: no
Responsibility:
@object-ui/app-shell's record page reads each related card's count a second time (top=1) after itstop=3list read already returnedtotal, starts the related reads a wave late, and@object-ui/plugin-grid'suseRecordCrudVerdictssendssecurity/explaintwice | objectui#8014 / objectui#11699 already shared that in-flight request once | every record open (measured 15 requests in 4 waves, title at 1.86 s at ~350 ms RTT).Thread-read: 6258012994
Serial constraints cleared: no open objectui PR touches
containers.tsx,related-count-store.tsorrecord-reference-rail.tsx(re-checked at the amendment).containers.tsx's last change9e7e1a5(objectui#12992) is on main. objectui#12980 part 7 (PR #13013) touchesuseNavMemberGates.tsandappObjectScope.tsonly.Claimed at: 2026-10-19T02:24Z
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 13010, "status": "blocked", "branch": "claude/issue-13010-record-open-waves", "pr": null, "session": "session_01XVp19E1MYUTFM3dnkwz8gB", "premise_still_valid": false, "summary": "Part 1 (Wanted 1-3) stopped at the file-surface breach the order names. Section 1 measured on objectui main d0934b919d, in a rendered test: the real RecordDetailView over the real ObjectStackAdapter, StrictMode, a held transport answered one wave at a time, and an authored page shaped like HotCRM's opportunity page (tab strip whose Related tab holds 3 record:related_list, plus a record:reference_rail aside with 3 entries, limit 3). Wave 1: approvals, explain update, explain delete, the record read, sys_user top=50, sys_comment, sys_activity top=200, and the rail's 3 card reads (top=3, $count true). Wave 2: the tab strip's 3 badge probes (top=1, $count true). (i) Reproduced. The top=3 read is the rail's fetchEntry in packages/plugin-detail/src/renderers/record-reference-rail.tsx. The top=1 probe is NOT in RecordDetailView: it is the page:tabs renderer's probe effect in packages/components/src/renderers/layout/containers.tsx, which calls RelatedCountStore.fetch, which sends the $top 1 read in packages/components/src/hooks/related-count-store.ts. The badge is the Related TAB's badge (a sum over the tab's related lists). Each card's own badge already shows its read's total. (ii) Reproduced. The gate is `const parentId = ctx?.data?.id;` in the tab strip (containers.tsx): the RECORD BODY's id. RecordContextValue.recordId is a required member that every RecordContextProvider supplies. The rail is not gated on the record: it reads ctx.recordId and waits only for IntersectionObserver visibility and getObjectSchema. (iii) NOT a duplicate. The two POSTs carry different bodies, operation update and operation delete, from useRecordEditable (the page header's recordWriteAllowed / recordDeleteAllowed; DetailView's twin calls join them through askVerdict, objectui#11699). The card's pin c4dd7078ed4a contains that sharing fix: merge-base --is-ancestor 45903630aa c4dd7078ed4a exits 0, and that answer holds in any checkout. objectui#8014 never shared anything: it was closed not planned, and its subject was this same update/delete pair. useRecordCrudVerdicts (plugin-grid) sends nothing on a record open. Item 3's premise is false. ExplainRequest takes one operation, so one POST per open would need a spec contract change, not sharing. Both remaining fixes live in packages/components (containers.tsx, related-count-store.ts), which is outside the claimed surface, so I stopped rather than edit there. Pushed for resume: Resume-from: claude/issue-13010-record-open-waves@0a2a365924. It adds one commit, the measurement harness packages/app-shell/src/views/RecordDetailView.recordOpenWaves-13010.test.tsx, pinning only facts that hold before and after the fix: the record read and the rail reads go out in wave 1, and the two explain bodies are update and delete. Production code is unchanged.", "tests": "Measured at 0a2a365924. The first runs printed the wave table through a deliberate failing assertion, since the console is silenced; it was removed before the commit. `bash /home/user/objectstack/scripts/pm/os-verify-lock.sh -c 'pnpm exec vitest run --maxWorkers=1 packages/app-shell/src/views/RecordDetailView.recordOpenWaves-13010.test.tsx'` gave Tests 1 passed (1), VERDICT command-exit 0. Raw wave table before any change: w1 GET approvals/requests; w1 POST security/explain update; w1 POST security/explain delete; w1 GET data/os_13010_deal/rec-13010; w1 GET sys_user top=50; w1 GET sys_comment; w1 GET sys_activity top=200; w1 GET os_13010_quote top=3; w1 GET os_13010_line top=3; w1 GET os_13010_task top=3; w2 GET os_13010_quote top=1; w2 GET os_13010_line top=1; w2 GET os_13010_task top=1. That is 13 requests in 2 waves; the card's wave 3 is this harness's wave 2, because here the rail and the record share wave 1. A temporary find-stack probe named the callers: the probe comes from containers.tsx finder, then related-count-store.ts fetchCount; the card reads come from record-reference-rail.tsx fetchEntry, then runWorker. A temporary probe in the rail source was restored with `git checkout HEAD --`. Proof by blob hash: HEAD 2e3a58f02ea7b201723770996801bae1f47b4871 equals disk 2e3a58f02ea7b201723770996801bae1f47b4871, and `git diff HEAD` is empty. eslint on the one changed file (`--format json`): 1 file, 0 errors, 0 warnings. check:control-bytes exit 0. check-hand-rolled-comment-mask exit 0. check:test-path-roots exit 0. Control-byte self-grep on the new file: no hits. No ablations: no fix landed. NOT MEASURED: app-shell type-check, the pre-tsc battery beyond the three above, the package directory run, and the related-family runs. Reason: status blocked with no production change; declared to CI.", "mcp_calls": "0", "api_writes": "1 — POST /repos/objectstack-ai/objectui/issues/13010/comments (this os-dev-report, via post-stamped.mjs). Not REST writes: two git pushes of claude/issue-13010-record-open-waves (the empty probe, then 0a2a365924). No PR, no label.", "open_questions": [ { "question": "Widen part 1's file surface so items 1 and 2 can land? Both mechanisms live in packages/components: the tab strip probe and its record-body gate (containers.tsx), and the count store (related-count-store.ts).", "options": [ "A: widen to packages/components/src/renderers/layout/containers.tsx and packages/components/src/hooks/related-count-store.ts, plus tests beside them; components joins the patch changeset. Item 2 becomes a one-read change: parentId comes from ctx.recordId, the route id every provider supplies, in place of ctx.data.id.", "B: re-scope part 1 to the rail alone (plugin-detail): after its read, the rail writes its total into RelatedCountStore.set. That removes the probe only when the rail finishes before the probe starts, and fixes neither the wave nor the race." ], "recommendation": "A. Business need: measured on the shipped HotCRM page shape. Long-term soundness: the fix belongs where the gate and the probe live, contract-first, with no consumer-side workaround. Guarding against AI mistakes: no new authoring surface. Startup focus: no new export, two files." }, { "question": "Item 1's mechanism once item 2 lands. The tab strip's probe effect then runs in wave 1 and is reached before the rail's visibility-gated read, so adopting a FINISHED card total cannot stop the probe. Only sharing the card's IN-FLIGHT read does.", "options": [ "A: the rail sends its read through the existing RelatedCountStore.fetch, with a finder that keeps the card's own $top and $count and keeps the rows. The store's existing in-flight dedupe then hands that total to the tab badge. The rail registers before its schema await, so the badge probe joins it. Only for entries whose declared filter equals the list's (no filter on either side). The crm_task tab badge (declared status filter, none on the rail card) keeps its probe. No new export.", "B: land item 2 only. The probes join wave 1, so the extra wave goes, but the 3 count requests stay." ], "recommendation": "A, with B as the fallback if the registration order cannot be made deterministic without a timing hack. Business need: it removes requests the card measured. Long-term soundness: one store, one in-flight entry per question. Guarding against AI mistakes: neutral. Startup focus: no new surface." }, { "question": "Item 3: the record open's two explain POSTs are two different questions (update, delete), not one question sent twice. Keep item 3?", "options": [ "A: drop item 3. No duplicate exists; the objectui#11699 sharing is present at the card's pin.", "B: one POST answering both operations. That needs an operations array on @objectstack/spec ExplainRequest and server support (objectstack side), then a client change.", "C: ask the delete verdict only when the header's overflow menu opens. That changes when Delete is gated." ], "recommendation": "A. Business need: no defect was measured; it is one round trip in the same wave, not an extra wave. Long-term soundness: B invents a contract for a cost the waves table does not show. Guarding against AI mistakes: neutral. Startup focus: no new surface, so per the framework, no expansion." } ], "out_of_scope_findings": [ "carrier: the card itself · noted, not filed. The card attributes the record-scope explain fetcher to useRecordCrudVerdicts; on main the record page's fetcher is plugin-detail useRecordEditable (askVerdict). A correction for the seat to note on the card." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Seat rulings · objectui#13010 part 1 · after the dev's measured stop ·
domain:ui#2·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-19T03:00ZOn the dev report
6258677488(status: blocked,Resume-from: claude/issue-13010-record-open-waves@0a2a365924). Every ruling below changes request timing only, with no product-visible behaviour, so all three are the seat's to make.maind0934b9, in a rendered test on the HotCRM page shape:top=1probe is thepage:tabsbadge's (containers.tsx→RelatedCountStore.fetchinrelated-count-store.ts). It is the Related TAB's sum badge, not a card's badge.top=3reads arerecord:reference_rail'sfetchEntry.ctx.data.id, the record body's id, thoughRecordContextValue.recordIdis always supplied from the route.containers.tsxandrelated-count-store.ts(plus the rail), amended in place on the claim6258321076. Item 2 becomes readingctx.recordIdin place ofctx.data.id.RelatedCountStore.fetch(keeping its own$top,$countand rows), so the store's existing in-flight sharing hands that total to the tab badge. This applies only where the badge's declared filter equals the list's.crm_task's declared status filter) keeps its own probe. ⛔ Never show a count from a different filter.security/explainPOSTs on a record open are two questions,operation: updateandoperation: delete, fromplugin-detail'suseRecordEditable.45903630aais an ancestor ofc4dd7078ed4a).useRecordCrudVerdictssends nothing on a record open.@objectstack/spec'sExplainRequest. No wave cost was measured, so the framework says no expansion.explainfetcher isplugin-detail'suseRecordEditable, notplugin-grid'suseRecordCrudVerdicts.meta/objectre-reads, sequenced with objectui#12980).Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 13010, "status": "done", "branch": "claude/issue-13010-record-open-waves", "pr": "https://github.com/objectstack-ai/objectui/pull/13019", "session": "session_01XVp19E1MYUTFM3dnkwz8gB", "premise_still_valid": true, "summary": "Resumed from 0a2a365924 under the seat rulings (6258704497). Item 2 landed: page:tabs (packages/components/src/renderers/layout/containers.tsx) keys its badge probes on ctx.recordId, the required RecordContextValue member every provider fills, in place of ctx.data.id. So on a record open the probes go out beside the record read, not a wave after it. Lists whose visibility the record decides keep the wait: behind a node-level visibleWhen, or in a tab whose item-level visibleWhen decides it, they are probed once the row lands. Item 1 fell back to B, as the ruling allows. After item 2 the tab strip's probe effect reaches RelatedCountStore.fetch after a single microtask hop. The rail's read sits behind its IntersectionObserver latch, which flips after a frame. So the probe always goes first; the after-table shows top=1 issued before top=3. Making the rail win would mean dropping its visibility gate and relying on effect order plus microtask order: a timing hack. The rail and the store are untouched. Item 3 dropped per the ruling; explain untouched. Before: 13 requests in 2 waves (the 3 top=1 probes in wave 2). After: 13 requests in 1 wave. Draft PR #13019, assignee baozhoutao (read back), body first lines `Part of #13010` and `Clause-②: no`, body stored byte-identical (7037 bytes).", "tests": "At head 355c0146f3, under os-verify-lock, `pnpm exec vitest run --maxWorkers=1` over 32 files, 252 tests passed, VERDICT command-exit 0. The run covers the page-tabs-* family (the objectui#12987 visibleWhen count pins and objectui#12992 included), related-count-store*, record-reference-rail*, the RecordDetailView.related* pins (8882 / 4664 / 5795 / 11733), childWriteRefreshesParent-12244, recordOpenRequests-*, accessibleNames-12625, authoredActivityThreading-12249, PagePreview, SchemaRenderer.visibleWhenRecordBinding, the new page-tabs-probe-route-id-13010, and the harness. Wave tables come from the harness. Before (d0934b919d): wave 1 holds approvals, explain update, explain delete, the record, sys_user top=50, sys_comment, sys_activity top=200, and quote/line/task top=3; wave 2 holds quote/line/task top=1. After (355c0146f3): all 13 in wave 1, the top=1 probes issued before the top=3 reads. Ablations, each run once through objectstack scripts/ablation-replace.mjs, each anchor 1 to 0, each restore proven by blob 09c621807d6a equal to HEAD with git diff HEAD empty: (1) parentId re-keyed on ctx?.data?.id gave 3 red (the harness wave-1 pin, the ungated pin, the node-gate pin); (2) node-gate filter line deleted gave 1 red; (3) item-gate return deleted stayed green on its first run, because the pin's predicate read false with no row. I rewrote the predicate to record.plan != 'free' and the rerun went red. Closure build --filter '@object-ui/app-shell^...' BUILD_EXIT=0. components type-check (tsc --noEmit plus tsconfig.test.json) TC_EXIT=0, re-run after the final edit; app-shell type-check TC_EXIT=0. eslint --format json on the 3 changed files: 0 errors. containers.tsx has 94 warnings, base 94; the new test has 4 no-explicit-any warnings in the 12987 test's pattern. Exit codes are recorded in the scratchpad battery file.", "gates": "exit 0: check:phantom-deps, check:self-import, check:unreferenced-sources, check:doc-example-readers, check:handler-key-reads, check:side-effects-array, check:element-data-source-declaration, check:spec-symbols, check:action-forward-parity, check:designer-field-key-parity, check:icon-record-names, check:i18n-keys, check:i18n-drift, check:i18n-designer-parity, check:control-bytes, check:test-path-roots, check:new-line-citations, check:vi-mock-specifiers, check:vi-mock-inherit, check-hand-rolled-comment-mask, check-changeset-fixed, check-changeset-presence, check-changeset-no-major. check:spec-floors exits 1 with no-artifact: it needs a whole-workspace build, so NOT MEASURED, recorded and not fixed per the order. Declared to CI: the full package directories of @object-ui/components and @object-ui/app-shell, and repo-wide pnpm lint. CI status not awaited (in_progress).", "files_changed": "packages/components/src/renderers/layout/containers.tsx; packages/components/src/__tests__/page-tabs-probe-route-id-13010.test.tsx (new); packages/app-shell/src/views/RecordDetailView.recordOpenWaves-13010.test.tsx (new, the harness); .changeset/13010-record-open-waves.md (@object-ui/components patch, @object-ui/app-shell patch). Inside the amended surface; record-reference-rail.tsx and related-count-store.ts were not changed (fallback B).", "deviations": "Pin (a) did not land: fallback B, reason in summary. The PR body footer uses the role file's session-URL form, not the harness reminder's line. No labels written: objectui has no gate reading a label the order named.", "mcp_calls": "0", "api_writes": "3 relay writes: POST /repos/objectstack-ai/objectui/issues/13010/comments (the first, blocked os-dev-report 6258677488); pr_create POST /repos/objectstack-ai/objectui/pulls (draft, #13019) plus its assignee POST /repos/objectstack-ai/objectui/issues/13019/assignees; and this os-dev-report comment. Not REST writes: five git pushes of the branch.", "open_questions": [], "out_of_scope_findings": [ "carrier: the seat (PR #13019 Acceptance notes) · noted, not filed. A page that renders the rail and the tab badge for the same unfiltered relation still pays two reads per relation in one wave. Removing them needs the rail to register before the probe: a store-level or rail-visibility-gate design call.", "carrier: PR #13019 Acceptance notes · noted, not filed. Ungated badge probes now go out once on a record that turns out to be missing or refused, before the page swaps to its not-found or denied state." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
ACCEPT · objectui#13010 part 1 · PR #13019 ·
domain:ui#2·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-19T03:35ZPR #13019 at head
355c014(Part of #13010), reviewed against the claim6258321076(amended in place), the seat rulings6258704497and the dev reports6258677488/6258974871.page:tabs(packages/components/src/renderers/layout/containers.tsx) keys its badge probes onctx.recordId, the requiredRecordContextValuemember every provider fills, in place ofctx.data.id. So on a record open the probes go out beside the record read. A list whose visibility the record decides (behind a node-levelvisibleWhen, or in a tab whose item-levelvisibleWhendecides it) still waits for the row.RelatedCountStore.fetchafter one microtask, while the rail's read sits behind itsIntersectionObserverlatch, which flips after a frame. Making the rail win would mean dropping its visibility gate and leaning on effect and microtask order, which is a timing hack. The rail and the store are untouched. The extra count reads remain, but in the same wave.6258704497).RecordDetailView.recordOpenWaves-13010, the realRecordDetailViewover the real adapter, HotCRM page shape): before, 13 requests in 2 waves (the 3top=1probes in wave 2); after, 13 requests in 1 wave.@object-ui/components: patch,@object-ui/app-shell: patch.page-tabs-probe-route-id-13010: probes keyed on the route id; the node-gated and item-gated lists wait for the row.visibleWhencount pins, the objectui#12992 scope pins and the The related-list tab BADGE compiles its parent scope as bare equality too, so a multi-value related list shows rows with no count — and the store's own badge/row parity claim stops holding #8882 / Auto-derived related lists consume the new field-levelrelatedListFilterdeclaration — list query AND-composed, tab badge honors the same filter #4664 / Auto-derived related lists have no default-sort surface — derivation emits no sort, component skips $orderby, rows fall to PK order #5795 / console: object-list links and titles outside the list page still name the list with the singular label — related-list titles, the record form breadcrumb, favorites and recents, the record back-link (family of objectui#11696) #11733 related pins all stay green.ctx.data.id; drop the node-gate filter; drop the item-gate return), each red, each restore proven by blob hash. The item-gate pin's predicate was strengthened after its first ablation stayed green.componentsandapp-shelltype-check 0; eslint 0 errors (warnings equal to base); the pre-tsc battery and the changeset checks all 0. Both full package dirs are declared to CI.pm:dispatchedand its assignee. Still carried: item 1's remainder (above), item 4 (timeline, comments, attachments and user list on first view) and item 5 (per-objectmeta/objectre-reads).355c014(filter=latest),mergeable_stateclean.Verdict: ACCEPT. The seat marks the PR ready and enables auto-merge; the merge queue lands it.
Generated by Claude Code
5 remaining items
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 13 (
priority:p2, part 4: Wanted item 1's remainder, the rail and the tab badge both read)Session:
session_01XVp19E1MYUTFM3dnkwz8gBAccount:
baozhoutao(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-13010-rail-badge-one-readWorktree:
objectui-issue-13010-railDomain:
domain:uiSeat:
domain:ui#2File surface:
packages/components/src/hooks/related-count-store.ts,packages/components/src/renderers/layout/containers.tsx(thepage:tabsprobe effect only),packages/plugin-detail/src/renderers/record-reference-rail.tsx(fetchEntry), tests beside them (incl. the part-1 harnessRecordDetailView.recordOpenWaves-13010.test.tsx), and one.changeset/13010-rail-badge-one-read.md(each touched packagepatch). ⛔ No timing hack (no reliance on effect / microtask / frame order); ⛔ never a count from a different filter; ⛔ no new module or barrel export (amended: three additive members on the already-exportedRelatedCountStoreare accepted as a declared widening). Stop on breach; explain in the report.Container & model:
M,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuireads no path-derived mandate)Clause-②: yes (amended in place at review: the three new members
fetchRows/claimRows/releaseRowssit onRelatedCountStore, which@object-ui/componentsexports from its index (index.ts:141), so its published surface widens; changeset@object-ui/components: minor; contract review owed)Responsibility: on a record page that renders both
record:reference_railand apage:tabsbadge over the same unfiltered relation, the badge'stop=1probe and the rail'stop=3read ask the same question twice (part 1, PR #13019, put them in one wave; the count stays doubled) |RelatedCountStorealready shares an in-flight question | every such record open (3 extra requests on the HotCRM opportunity page).Thread-read: 6259912246
Serial constraints cleared: objectui#13010 part 1 (PR #13019,
containers.tsx) landed as46dbf32. No open objectui PR touches these files. objectui#13010 parts 2 (RecordDetailView.tsx) and 3 (adapter / provider) are disjoint.Claimed at: 2026-10-19T05:04Z
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
ACCEPT · objectui#13010 part 2 · PR #13021 ·
domain:ui#2·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-19T05:09ZPR #13021 at head
41855ac(Part of #13010), reviewed against the part-2 claim6259275403, the dev report6259798183and the review6259912246(PASS, atCONTRACT_REVIEW_TIER).P1, measured: the
sys_activitytop=200,sys_commentandsys_usertop=50 reads fill only theDiscussionContext, which onlyrecord:discussion/record:chatter/record:activitydraw; the header draws none of them.sys_attachmenttop=100 isRecordAttachmentsPanel's read. Inside a tab it already ran only when that tab was active. The host's foot-of-page fallback panel (anenable.filesobject whose page places norecord:attachments) mounted on every open; that is HotCRM's case.Diff read by the seat (
RecordDetailView.tsx):pageShowsNodeOnOpenover the rendered tree and?tab=), or once it has been (a sticky flag for the view's life).IntersectionObserver(rootMargin 200px, the rail's gate) reports it. Until then it shows its own loading state, never "No attachments yet".RecordAttachmentsPanel,record-activityandrecord-historyare unchanged. No new export, prop or cache.The seat's risk, answered by the review:
pageShowsNodeOnOpenmirrorsPageTabsRenderer's active-item rule. Line by line, every shape matches the renderer or errs toward reading, except an authoredproperties.defaultTabwith?tab=: theSchemaRendererhoist lets the authored value win. That shape needs a key the publishedPageTabsPropsdoes not declare. The seat took the review's hardening anyway, on this branch (41855ac): a strip withdefaultTabin either spelling is unreadable whatever?tab=says, so its feed is read eagerly.Clause-②: no. Changeset
@object-ui/app-shell: patch.Measured (harness, HotCRM-shaped page over an
enable.filesobject): 14 requests in 1 wave → 10 requests in 1 wave. None of the four reads go out at open. Part 1's page went from 13 to 10.Pins:
?tab=activity/?tab=attachmentsstill read at open; the badge probes are unchanged; the edited objectui#12701 pin (0 activity reads on a page with no feed block) holds;properties.defaultTab: 'activity'opened at?tab=detailsreads the feed;?tab=: zero feed reads AND no feed block in the DOM, so a renderer that mounted a non-first tab unprompted turns it red.Four ablations (feed gate, mention gate, foot panel, hardening), each red; each restore proven by blob hash.
Local gates (dev reports): app-shell families 70 files / 462 tests,
plugin-detail135,collaboration56;app-shelltype-check 0; eslint 0 errors; the pre-tsc battery and the changeset checks all 0. The fullpackages/app-shell/dir is declared to CI.Commits: trailers are model-free.
Acceptance notes:
sys_audit_logtop=50 plus itssys_username lookup is still read at open for atrackHistoryobject before the History tab opens. It is the same mechanism, carried on this card as item 4's remainder (class c: cost, not a wrong answer).The card stays open with
pm:dispatchedand its assignee. In flight: part 3 (item 5, per-objectmeta/objectre-reads, claim6260076943) and part 4 (item 1's remainder, the rail and the badge, claim6260082983). Carried: item 4'ssys_audit_logremainder.CI at head: 36/36 check-runs success or skipped on
41855ac(filter=latest),mergeable_stateclean.Verdict: ACCEPT. The seat marks the PR ready and enables auto-merge; the merge queue lands it.
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 13010, "status": "needs_decision", "branch": "claude/issue-13010-object-schema-reuse", "pr": null, "session": "session_01XVp19E1MYUTFM3dnkwz8gB (subagent: parent's id)", "premise_still_valid": false, "summary": "Part 3 (Wanted item 5) stopped at Section 1 P1, the order's fork. The two answers are not the same document: the per-name read GET /api/v1/meta/object/NAME serves the per-column `sortability` projection beside `item` (objectstack 26501ae24f, meta-item-read-gate.ts `translateMetaEnvelope`, reached only from `createMetaItemAnswer`). The whole-registry read GET /api/v1/meta/object (`createMetaListAnswer`, steps 1-8) serves none. The adapter's `fetchObjectSchemaFresh` re-attaches that projection to the schema under OBJECT_SORTABILITY_KEY. `readObjectSortability` reads it in RelatedList (plugin-detail, via `useSettledSchema` then `getObjectSchema`), ObjectGrid and ListView. The adapter caches one answer per object NAME, shared by every caller, so a registry-sourced answer for any name would take the projection away from every later sortability reader of that name. They would then fall back to 'no signal served' and offer sorts the server refuses. Callers cannot be told apart without changing callers, which the order rules out. So no half of item 5 can be built under the rulings, and no code, PR or changeset was written. Measured callers (probe below): `page:tabs` badge probe (components containers.tsx, reads `.fields`) and `record:reference_rail` (plugin-detail record-reference-rail.tsx, reads `.fields` plus the name field). Each fires once per name and goes out in WAVE 1, beside the record read; it is not a wave of its own. On the Related tab the same cached entry is what RelatedList reads. Source-named readers of the other staging names: DetailView `getObjectSchema(schema.objectName)` for the record's object, and RelatedList's lookup-label batch `getObjectSchema(target)` for lookup targets. The branch was pushed empty as the claim marker and has no commits beyond main 4016370af3.", "tests": "Measurement only; no committed test, since there is no diff. Throwaway probe built from RecordDetailView.recordOpenWaves-13010.test.tsx (real ObjectStackAdapter.getObjectSchema, held transport, `meta/object/NAME` answered with item plus a `sortability` envelope, every getObjectSchema call logged with its caller frame). Run as `bash os-verify-lock.sh -c 'pnpm exec vitest run --maxWorkers=1 packages/app-shell/src/views/__probe13010c.test.tsx'`; the lock printed VERDICT command-exit 0 (Test Files 1 passed). Output: META READS = w1 GET /api/v1/meta/object/os_13010_quote, w1 .../os_13010_line, w1 .../os_13010_task (3 reads, all wave 1). CALLS = per name, 2x containers.tsx (page:tabs badge probe) and 1x record-reference-rail.tsx, coalesced by the adapter's per-name cache into one read each. A later getObjectSchema(os_13010_quote) issued no new request and returned the cached document with readObjectSortability = {fields:{name:{sortable:true},status:{sortable:false,reason:formula}}}, which shows one per-name entry serves every caller. The probe file was deleted and is not committed. Server half read at objectstack origin/main 26501ae24f (packages/rest/src/meta-item-read-gate.ts: sortability appears only in translateMetaEnvelope, and zero `sortab` hits inside createMetaListAnswer; packages/runtime/src/domains/meta.ts item path names sortability, list path does not). Gates: NOT MEASURED for every family (type-check, targeted vitest, lint, pre-tsc battery, check-changeset-fixed), reason: no diff exists to judge. Ablation and pins (a)(b)(c): not applicable, nothing implemented.", "mcp_calls": "0", "api_writes": "2 so far: 1 git push (empty branch claude/issue-13010-object-schema-reuse, the claim marker) and 1 os-dev-report comment POST /repos/objectstack-ai/objectui/issues/13010/comments via scripts/pm/post-stamped.mjs. No PR, no labels.", "open_questions": [ { "question": "The per-name object read carries `sortability` (consumed by RelatedList / ObjectGrid / ListView through getObjectSchema), and the whole-registry read does not. Should a record route stop re-reading an object's metadata the registry already returned, and if so how?", "options": [ "A. Upstream contract first: objectstack's GET /meta/object list answer serves each object's sortability projection too (on both transports, the REST server and the runtime dispatcher, computed from each caller's runtime view as the item read does). objectui then attaches it per registry item, and getObjectSchema answers from the MetadataProvider's registry entry under the existing publish / metadata-change invalidation. Cost: a spec plus rest plus runtime change in objectstack (needs:contract-review; per-object mask and runtime-view work on a list already about 110 KB), then the objectui half as this order describes it. Effort: medium-large, two repos, serial.", "B. Keep the per-name read and close item 5 as measured-not-redundant. Each name is read once per session (adapter per-name cache, coalesced across callers), and the reads go out in the first wave beside the record read, not in a wave of their own. Cost: the 3-7 extra parallel requests per full record load stay. Effort: none.", "C. Re-point fields-only callers (page:tabs badge probe, reference rail, lookup-label resolvers, DetailView) at the MetadataProvider registry and leave getObjectSchema for sortability readers. Cost: overturns the 'callers unchanged' ruling, gives one object two schema sources with different lifetimes (provider ttlMs versus the adapter's MetadataCache TTL), and reintroduces the per-caller divergence the getObjectSchema choke point (reference-key normalization, sortability attach) exists to prevent. Effort: medium, several packages.", "D. Answer from the registry anyway and drop sortability. Rejected on measurement: the related lists and grids would offer server-refused sorts again, undoing the sortability projection's purpose." ], "recommendation": "B now, with A filed for the maintainer only if the remaining requests matter. Business need (measured): the reads are parallel and in wave 1, and each name is fetched once per session; the card's acceptance criteria (waves before title, one explain, no top=1, lazy feeds) do not depend on item 5. Long-term soundness: A is the only contract-first route (the registry becomes a complete document); C is a consumer-side split that fossilizes two schema sources. Preventing AI mistakes: D and C both create states where a reader silently gets a weaker document. B and A keep one strict answer per name. Startup focus: A widens a server contract and adds per-object list cost for a saving of a few parallel requests, with no measured user pull beyond this card. So B by default, and A is the maintainer's call." } ], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Seat ruling · objectui#13010 part 3 (item 5) · measured not redundant ·
domain:ui#2·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-19T05:15ZOn the dev report
6260198880(needs_decision,premise_still_valid: false, no PR). The claim is6260076943.GET /api/v1/meta/object/NAMEserves the per-columnsortabilityprojection besideitem(objectstack26501ae24f,packages/rest/src/meta-item-read-gate.tstranslateMetaEnvelope, reached only fromcreateMetaItemAnswer). The whole-registry read (createMetaListAnswer) serves none.OBJECT_SORTABILITY_KEY.readObjectSortabilityreads it inRelatedList,ObjectGridandListView, so they do not offer sorts the server refuses.page:tabsbadge probe, the reference rail andRelatedList), and the reads go out in wave 1, beside the record read.claude/issue-13010-object-schema-reusestays; ref deletes are refused.pm:dispatchedand its assignee:6260082983) is in flight;sys_audit_logremainder, claimed after PR fix(app-shell): a record page reads its timeline, comments, user list and foot attachments when shown (objectui#13010 part 2) #13021 lands (same file).Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 13 (
priority:p2, part 5: item 4's remainder, the History tab'ssys_audit_logread at open)Session:
session_01XVp19E1MYUTFM3dnkwz8gBAccount:
baozhoutao(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-13010-history-on-viewWorktree:
objectui-issue-13010-historyDomain:
domain:uiSeat:
domain:ui#2File surface:
packages/app-shell/src/views/RecordDetailView.tsx(only if it owns the read),packages/plugin-detail/src/renderers/record-history.tsxand the synth History tab (packages/plugin-detail/src/synth/buildDefaultPageSchema.ts) only if they own it, tests beside them (incl. the part-1 harness), and one.changeset/13010-history-on-view.md(each touched packagepatch). ⛔ Thepage:tabsbadge probes unchanged; ⛔ no new export. Stop on breach; explain in the report.Container & model:
S,mode:subagent,model: opus(default tier;dispatch-gates --tier --repo objectstack-ai/objectuireads no path-derived mandate)Clause-②: no
Responsibility:
@object-ui/app-shell/plugin-detailreadsys_audit_logtop=50 plus asys_username lookup at every open of atrackHistoryobject, before the History tab is opened (named by the part-2 review6259912246) | part 2's on-view rule (PR #13021) | every record open of atrackHistoryobject.Thread-read: 6260216763
Serial constraints cleared: PR #13021 (part 2) edits
RecordDetailView.tsxand is in the merge queue. This part branches from #13021's head41855acand mergesmainafter #13021 lands, before its PR opens. objectui#13010 part 4 and objectui#13020 are disjoint.Claimed at: 2026-10-19T05:23Z
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Landed · objectui#13010 part 2 · PR #13021 ·
domain:ui#2·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-19T05:25Z29c19cconmain. Verified by content: all 4 files in29c19cc^1..29c19ccare blob-identical to the accepted head41855ac. The squash message is model-free.defaultTabstill reads eagerly.pm:dispatchedand its assignee. In flight: part 4 (claim6260082983) and part 5 (the History tab'ssys_audit_logread, claim6260317153, now frommain29c19cc). Item 5 is retired, measured not redundant (ruling on the card).Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 13010, "status": "done", "branch": "claude/issue-13010-rail-badge-one-read", "pr": "https://github.com/objectstack-ai/objectui/pull/13023", "session": "session_01XVp19E1MYUTFM3dnkwz8gB", "premise_still_valid": true, "summary": "Part 4 landed as candidate X. The order's measure-and-choose step held it to be the smallest correct design: no timing hack, and both orders are pinned. record:reference_rail claims its entries' rows in RelatedCountStore while it renders (claimRows). Every render in a commit finishes before any effect in it, so the claim precedes the page:tabs probe effect. A badge probe (RelatedCountStore.fetch, containers.tsx unchanged) that finds a claim on its unfiltered key sends $top equal to the rail's row cap with $count, and the store holds the rows until the rail takes them (fetchRows: take held rows, join a big-enough in-flight read, or read itself and register in flight so a later badge joins it). A badge with a declared filter has a different key and keeps its own top=1 probe. A rail alone still waits for visibility, and a refresh still reads directly. Held rows are a hand-off: taken once, and dropped on invalidation or on the rail's unmount or a new scope. invalidate now also drops pre-change in-flight reads. Where the probe goes out before the rail has rendered at all (the rail in a later commit), the rail cannot use the one-row read and reads itself: two reads, as before, correct. That case is said in the PR. Harness wave table, before at 4016370af3: 13 requests in wave 1, with quote and line each read twice (top=1 and top=3). After at b94d1a2272: 11 requests in wave 1, with quote and line each read once at top=3; the task badge (declared filter) and the task card stay two reads. Draft PR #13023: assignee baozhoutao (read back), body lines 1 and 2 `Part of #13010` and `Clause-②: no`, stored byte-identical (9306 bytes).", "tests": "Head b94d1a2272, under os-verify-lock with --maxWorkers=1: 28 files and 177 tests passed, VERDICT command-exit 0. The files are the related-count-store* family (incl. the new sharedRead-13010), record-reference-rail* (incl. the new sharedRead-13010: badge-first settled then rail visible; badge-first with the rail visible in flight; rail-first; rail alone hidden makes no read; rail alone visible on the old wire; filtered badge keeps top=1; refresh re-reads and draws new rows; unmount releases the claim), the page-tabs* family (incl. page-tabs-probe-route-id-13010), RecordDetailView.related* (8882/4664/5795/11733), RecordDetailView.childWriteRefreshesParent-12244, and the harness RecordDetailView.recordOpenWaves-13010. The harness has the new pin (a), 'each unfiltered relation the rail and the tab badge both show is ONE read, and both are drawn'. It asserts quote and line reads equal [3], the Related tab badge 17, card rows 0-2 drawn with no row 3, and the card totals. The part-1 pins and the filtered-badge CONTROL are still green. Ablation, once, through objectstack scripts/ablation-replace.mjs. It replaced the anchor `const claimed = filter === undefined ? claims.get(k) : undefined;` with `const claimed = undefined;`: anchor 1 to 0, blob 872f791187f8 to dacf43eb883a. The harness went 1 failed and 3 passed: pin (a) red with 'expected [ 1, 3 ] to deeply equal [ 3 ]' (os_13010_quote). The restore was proven by blob 872f791187f8 equal to HEAD with git diff HEAD empty. No dist build was needed: the root vitest config aliases @object-ui/components and @object-ui/plugin-detail to src. Wave tables come from the harness pin, run on the branch sources and on the base's two source files (restored, git diff HEAD empty). Closure build --filter '@object-ui/app-shell^...' BUILD_EXIT=0. type-check (tsc --noEmit plus tsconfig.test.json): components 0 and app-shell 0, run on the final content of those packages; plugin-detail 0, re-run at b94d1a2272. eslint --no-inline-config --format json on the 5 changed ts/tsx files: 5 files, 0 errors. Warnings: store 3 (base 3) and rail 11 (base 11); the new test files have 8 no-explicit-any.", "gates": "exit 0: check:phantom-deps, check:self-import, check:unreferenced-sources, check:doc-example-readers, check:handler-key-reads, check:side-effects-array, check:element-data-source-declaration, check:spec-symbols, check:action-forward-parity, check:designer-field-key-parity, check:icon-record-names, check:i18n-keys, check:i18n-drift, check:i18n-designer-parity, check:control-bytes, check:test-path-roots, check:new-line-citations, check:vi-mock-specifiers, check:vi-mock-inherit, check-hand-rolled-comment-mask, check-changeset-fixed, check-changeset-presence, check-changeset-no-major. check:spec-floors exits 1 with no-artifact: it needs a whole-workspace build, so NOT MEASURED; recorded, not fixed, per the order. Declared to CI, not run locally per the order: the full package directories of @object-ui/components, @object-ui/plugin-detail and @object-ui/app-shell, and repo-wide pnpm lint. CI not awaited: in_progress.", "line_budget": "n/a: no skills/** or governed path touched", "files_changed": "packages/components/src/hooks/related-count-store.ts; packages/plugin-detail/src/renderers/record-reference-rail.tsx; packages/app-shell/src/views/RecordDetailView.recordOpenWaves-13010.test.tsx (pin a); packages/components/src/__tests__/related-count-store.sharedRead-13010.test.ts (new); packages/plugin-detail/src/renderers/__tests__/record-reference-rail.sharedRead-13010.test.tsx (new); .changeset/13010-rail-badge-one-read.md (components, plugin-detail, app-shell, each patch). containers.tsx is not changed: the store decides $top, so the probe effect needed no edit. 6 files, +665/-43.", "deviations": "1) The three new functions are members of the existing exported RelatedCountStore object: fetchRows, claimRows, releaseRows. No new module or barrel export; this is the rail's only path to the store, so the seat may read it against 'no new export'. 2) Commit trailers are the order's model-free pair, not the harness reminder's Co-Authored-By line with a model name. The PR body footer is the role file's session-URL form, not the reminder's line. 3) No labels written: the order named none, and objectui has no gate that reads one. 4) In dev StrictMode only, the simulated unmount releases the claim and the effect re-claims in the same commit. A badge probe in that window, with no awaited schema read, falls back to two reads. Noted in the PR, not engineered around.", "mcp_calls": "0", "api_writes": "2 relay writes via objectstack scripts/pm/fleet-write/dispatch.mjs (POST /repos/objectstack-ai/objectstack/dispatches). (1) pr_create, executed as POST /repos/objectstack-ai/objectui/pulls (draft, #13023) plus POST /repos/objectstack-ai/objectui/issues/13023/assignees. (2) This os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/13010/comments. Not REST writes: 2 git pushes (the empty branch, then both commits).", "open_questions": [], "out_of_scope_findings": [ "carrier: PR #13023 Acceptance notes · noted, not filed. A child write still costs two reads for a list both the rail and the badge show: the badge's post-invalidation re-probe (the claimed top=N read) and the rail's direct refresh read. The count is unchanged from before; this card is about the record open.", "carrier: PR #13023 Acceptance notes · noted, not filed. RelatedCountStore still installs a count from a read that went out before an invalidation (pre-existing). Part 4 stops such a read's rows being held and stops later asks from joining it, but leaves the count path as it was." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Contract review
Served-tier:
CONTRACT_REVIEW_TIERHead-sha:
8710095be37beb4240bf12154a4fe3328894b56fLocal-runs: none
Inputs read: card #13010 body, the part-4 claim (
6260082983) and the os-dev-report (6260555066); PR #13023 body, file list (6 files, +693/-43) and net diff; the base sources ofrelated-count-store.ts,containers.tsx,record-reference-rail.tsxand the three otherRelatedCountStorecallers, read-only, to judge ①.Check-runs on the head: NONE.
get_check_runsreturnstotal_count: 0and the combined status ispendingwith zero statuses. No gate family has rendered a verdict on8710095b; the dev's os-dev-report says "CI not awaited: in_progress" for an earlier head (b94d1a2272), and the third commit (theminorchangeset and JSDoc) has no CI at all. The PR also reportsmergeable_state: dirty(a conflict withmain). This record judges the contract; the derived gate families (vitest, type-check, lint, changeset guards) remain unverified by CI at this head and must be green before the adopting seat merges.① Derived judgments
Public surface of
@object-ui/components. Right: the only widening is the three new members on the already-exportedRelatedCountStoreobject (fetchRows,claimRows,releaseRows). The barrel line is unchanged (export { useRelatedCount, useRelatedCountVersion, RelatedCountStore }); no new named export, no newexport type.ReadResultandInflightReadstay module-local interfaces;ReadResult({ total: number; rows: unknown[] }) becomes reachable only structurally, as the resolved return type offetchRows, the same way the pre-existing localProbeFnis reachable throughfetch._resetgains two moreclear()s (test-only member, unchanged shape).@object-ui/plugin-detailpublishes nothing new:claimRailRows/releaseRailRowsare module-private helpers;RecordReferenceRailRenderer's props are unchanged.@object-ui/app-shellchanges a test file only.fetchfor its existing callers (containers.tsxprobe effect, the four invalidation callers). Right: with no claim on the key the wire is byte-identical ({ $filter, $top: 1, $count: true }, samecomposeParentScopeFilter/mergeFilterNodescomposition), the return is the sametotal, a failed read still resolves0and leaves no count, the!parentIdarm still resolves0, and concurrent probes still join one in-flight read. The declared-filter path cannot see a claim (claimedis read only whenfilter === undefined, and a filtered probe keys differently anyway). The attachments probe keys on a syntheticrelationshipFieldno rail entry can claim.invalidatenow also drops in-flight reads (andinvalidateAllclears them). A behaviour change, and safe. Before: a probe issued after an invalidation joined the pre-change in-flight read and cached its (stale) total. After: it sends its own read. The pre-change read still runs to completion and still callssetCount(the dev's second out-of-scope note); with two reads out, the stale one can land second and overwrite the fresh count, which is no worse than before (before, only the stale number ever existed on that key) and usually better. Thefinallyguardinflight.get(k) === entrymeans a dropped entry never deletes its successor.useBulkExecutor,data-invalidationand the store's own bus subscription callinvalidate(objectName)/invalidateAll()with the same signatures;core'sparent-scope.tsonly mentions the store in a comment. None of the four is affected beyond that.Timing independence. Right for the case the PR claims. The rail's claim is made in the render body (
if (claimable) claimRailRows(...)), and React runs every render of a commit before any passive effect of that commit, so a tab strip in the same commit sees the claim when its probe effect runs. Both orders then converge on one read: badge first (claim found,$top= the largest claimed cap, rows held), rail first (fetchRowsregisters its read withfromRows: true, the badge'sfetchjoins it fortotal). Where the probe left before the rail rendered, the rail refuses atopsmaller than its cap and reads itself (two reads, correct), which the dev states. The render-phase side effect is a deliberate impurity; it is idempotent (max-monotonic per key), so repeated or discarded renders claim nothing new.A count from a different filter / object / relation. Not possible: the claim key is
key(object, relField, parentId)with no filter segment, and a filtered badge's key carries#JSON(filter)inside the relField segment. A badge sums only keys it probed itself. The one way a claim changes a badge's read is$top, never$filter: both sides compile the parent condition through the samecomposeParentScopeFilterwith the samegetObjectSchema(name)?.fieldssource, so the rows the badge fetches for the rail are the rows the rail would have fetched.Stale rows for the rail. Child write (objectui#12244): the bus calls
invalidate(child), which drops held rows and the in-flight entry, and the rail's own subscription bumpsinvalidationNonce, whose re-run isrefreshingand readsdataSource.finddirectly, never the store. Record swap: keys carryparentId, the effect cleanup releases the old scope's claims, andisCurrent()drops late answers. Both right.One hole, narrow, worth filing (see ③): a PARENT-record change (
notifyDataChanged({ objectName: parent, recordId }), what a record action withrefreshAfter: trueposts) is treated as stale by the rail (parentStale) but NOT by the store (invalidate(parentObjectName)matches no child key). If the rail has not yet read when that change arrives (it is belowxl,display:none, or below the fold on a tall page) the nonce bumps, but the later first run of its fetch effect is notrefreshing(fetchedScopeRefis empty), so it goes throughfetchRowsand takes the rows the badge's probe held BEFORE the action ran. Before this PR that first read was always fresh. The rows are then exactly as stale as the badge count already drawn beside them (the store never invalidated that key either, pre-existing), and the next child-object change clears both. A one-line repair exists in the rail (releaseRailRowsonparentStale, or bypass held rows wheninvalidationNonceis non-zero on a first read). The dev's refresh CONTROL covers only the rail-already-read case.Memory / leak of the hand-off. Bounded. A rail mounted but never visible (sub-
xl) makes the badge probe fetchrowCaprows instead of one and hold them until the rail's unmount cleanup (releaseRows) or a child invalidation: a few rows per entry per record open, and a slightly larger probe payload than before on every sub-xlopen, which the PR body does not mention. A rail whose render is discarded without ever committing leaves its claim (one number per key) with no cleanup; a badge that then probes holdsrowCaprows for that key until a child invalidation. Keys includeparentId, so this cannot be hit twice for one record and grows only with discarded first renders of distinct records; negligible, but it is the cost of claiming in render, and should sit in the file header.JSDoc accuracy. Accurate on all three.
claimRows: idempotent, largest cap wins, survives invalidation, dropped byreleaseRows/_reset: matches the code.fetchRows: taken once, joins a big-enough in-flight read, else reads itself and registers in flight; the one omission is that an own read is NOT registered when a too-small read is already out (the code comment says so, the JSDoc does not).releaseRows: drops claim, held rows and only afromRowsin-flight entry: matches. Two further edge notes, not defects: two rails claiming the same key with different caps share one claim, and either's release drops it for both (unrealistic page); in dev StrictMode the simulated unmount of a VISIBLE rail drops itsfromRowsentry and the re-mounted effect starts a second own read (the dev frames the dev-only window as badge-side; it is rail-side too; production has no simulated unmount).② Semver level
@object-ui/components: minoris right: three additive members on a published object are a widening, and this repo marks every non-major change (widenings and even its own breaking changes)minorper AGENTS.md (nomajoroutside an@objectstackmajor sync).@object-ui/plugin-detail: patchis right: a behaviour fix with no surface change.@object-ui/app-shell: patchis redundant: only a test file changed, and the policy's explicit form for that is an empty declaration; within thefixedgroup the bump is moot in practice, so this is a nit, not a defect. The changeset body names each member and cites by content, nopath:line.Clause-②: yeson the PR body and the claim is the correct reading: the surface widens, a contract review is owed, and this is it.③ Boundary flags
RelatedCountStore, no new module/barrel export): accepted by the amended claim; confirmed in the diff. Acceptance note.Co-authored-by: ClaudeandClaude-Session:. Acceptance note.open_questions: none in the report. Nothing to answer.setCount. The sameinflight.get(k) === entryguard the dev added for held rows would close it in one line. Class c: file as a small follow-up.refreshAfterparent change + rail becomes visible later draws pre-action held rows). Class b: file a follow-up against part 4 with the one-line repair named above; it narrows the PR body's "No stale rows after a refresh" ruling to the rail-already-read case, which is the case objectui#12244 was about.xlcost (badge probe fetchesrowCaprows a hidden rail never draws). Acceptance note.dirtyagainstmain. Not a contract matter, but the adopting seat must not merge on this record alone: the gate verdicts are absent, not green.Implemented-by:
claude/issue-13010-rail-badge-one-readReviewed-by:
session_01XVp19E1MYUTFM3dnkwz8gBVERDICT: PASS
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 13010, "status": "done", "branch": "claude/issue-13010-history-on-view", "pr": "https://github.com/objectstack-ai/objectui/pull/13025", "session": "session_01XVp19E1MYUTFM3dnkwz8gB (subagent: parent's id)", "premise_still_valid": true, "summary": "Part 5 (item 4's remainder). P1 measured on part 2's harness: RecordDetailView's audit history effect owns the read, not record-history.tsx or the synth. On every open of a trackHistory object whose registry holds sys_audit_log, it sent sys_audit_log top=50 in wave 1 and a sys_user $in author lookup in wave 2, with the History tab closed. Its only consumer is the record:history node in the synthesized page's History tab (host entries); an assigned page renders as authored and its record:history self-fetches sys_activity. Fix: the effect now runs only when that node is on screen, judged by part 2's pageShowsNodeOnOpen with a module-private HISTORY_CONSUMER_TYPES = {record:history}. The judgment runs over one extra buildDefaultPageSchema call carrying the History tab with no rows (the rendered tree adds it only in the late synth rebuild). Strips the rule cannot read keep the eager read. The flag is latched for the view's life, like the feed's. Assigned pages no longer read rows nothing showed. The mounting commit shows the tab's loading state (historyPending) until the read starts. Harness, synth page of a trackHistory object: before 12 requests in 2 waves, after 10 requests in 1 wave. No new export; record-history.tsx and buildDefaultPageSchema.ts unchanged; badge probes unchanged.", "tests": "HEAD d1ec111451. Before-run (pins on base code, harness `-t 'part 5'`): 3 failed / 1 passed, i.e. (a),(b),(d) red and the ?tab=history CONTROL green; wave table w1 sys_audit_log top=50 + w2 sys_user top=1. After: harness 15/15 (os-verify-lock VERDICT command-exit 0). Families (path names record-history|RecordDetailView|recordOpenWaves-13010|buildDefaultPageSchema + files naming trackHistory), vitest --maxWorkers=1 under lock on c3d5f644bc: 'Test Files 80 passed (80) / Tests 659 passed (659)', VERDICT 0. Rerun on d1ec111451 (after a cast-only commit), together with type-check: harness + auditDisplayLocale-10442 + feedGrant-12082 + recordOpenRequests-12701: 'Test Files 4 passed (4) / Tests 40 passed (40)', VERDICT 0. Closure build `pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build`: VERDICT 0. `pnpm --filter @object-ui/app-shell type-check` (tsc --noEmit && tsc -p tsconfig.test.json; --listFilesOnly lists the harness once): exit 0 on d1ec111451. After-measure via ablation-replace (a forced-fail assertion to print the table, restored): 10 requests, all w1. Ablation: ablation-replace.mjs --delete on anchor `if (!historyWanted) return;` in RecordDetailView.tsx. Anchor 1->0, blob 8abcb53b48f1 -> e96672f60021. Harness 3 failed (a,b,d) / 12 passed (incl. c). Restore: blob after restore 8abcb53b48f1 == HEAD blob, `git diff HEAD` empty. No dist in path (relative source import). Declared to CI: full packages/app-shell/ dir (whole-package runs ruled out by the order).", "mcp_calls": "0", "api_writes": "2 — fleet-write relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches): pr_create (POST /repos/objectstack-ai/objectui/pulls draft + POST /repos/objectstack-ai/objectui/issues/13025/assignees, read-back identical, assignee baozhoutao), and this os-dev-report comment (POST /repos/objectstack-ai/objectui/issues/13010/comments); plus 3 git pushes of the branch (not REST); 0 label writes (objectui path labels are labeler.yml's, none named)", "open_questions": [], "out_of_scope_findings": [], "gates": "closure build 0; app-shell type-check 0; vitest families 80/659 pass (c3d5f644bc), final 4/40 pass (d1ec111451); eslint narrowed to 2 files exit 0, 0 errors, RecordDetailView.tsx 102 warnings = base 102, harness 0 (population: root eslint.config.js; count 2 from --format json; no type-aware linting so no untouched file moves); pre-tsc battery all 0: check:phantom-deps, check:self-import, check:unreferenced-sources, check:doc-example-readers, check:handler-key-reads, check:side-effects-array, check:element-data-source-declaration, check:spec-symbols, check:action-forward-parity, check:designer-field-key-parity, check:icon-record-names, check:i18n-keys, check:i18n-drift, check:i18n-designer-parity, check:control-bytes, check-hand-rolled-comment-mask.mjs; check-changeset-fixed.mjs 0; check-changeset-presence.mjs 0; check:new-line-citations 0; check:spec-floors 1 (recorded, not fixed, as ordered; diff touches no package.json). CI on the PR: not waited for (in_progress at report time).", "line_budget": "n/a — no skills/** or governed ledger touched", "deviations": "(1) Ablation tool and lock wrapper run by absolute path from the objectstack checkout; objectui has no scripts/pm or ablation-replace.mjs. (2) The assigned-page path no longer reads sys_audit_log. That follows from the ruling (read when its consumer is shown; an assigned page has no consumer of these rows), but it goes beyond the card's named 'before the History tab is opened' case, so it is pinned as (d) and named in the PR. (3) The PR body went out without the session-URL attribution footer, and the platform did not append one: the read-back is identical, 7151 bytes. Per the write-once rule it was not PATCHed. If the seat wants it, append the two lines `---` and `_Generated by [Claude Code](https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB)_` after a blank line at the end of PR #13025's body. (4) Not browser-verified against a live stack; the harness drives the real page over the real adapter.", "files_changed": [ "packages/app-shell/src/views/RecordDetailView.tsx", "packages/app-shell/src/views/RecordDetailView.recordOpenWaves-13010.test.tsx", ".changeset/13010-history-on-view.md" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
ACCEPT · objectui#13010 part 5 · PR #13025 ·
domain:ui#2·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-19T06:18ZPR #13025 at head
d1ec111(Part of #13010), reviewed against the part-5 claim6260317153and the dev report6260844463.RecordDetailView's audit-history effect owns the read (notrecord-history.tsx, not the synth). On every open of atrackHistoryobject it sentsys_audit_logtop=50 in wave 1 and asys_user $inauthor lookup in wave 2, with the History tab closed. Its only consumer is the synthesized page's History tabrecord:history(host entries). An assigned page renders as authored, and itsrecord:historyself-fetchessys_activity.RecordDetailView.tsx):pageShowsNodeOnOpen(no second copy of the tab rule) with a module-privateHISTORY_CONSUMER_TYPES = {record:history}.buildDefaultPageSchemacall carrying the History tab with no rows, since the rendered tree adds it only in the late synth rebuild.defaultTab, keep the eager read. The flag is latched for the view's life.historyPending), never "no history".record-history.tsx,buildDefaultPageSchema.tsand the badge probes are unchanged; no new export.sys_audit_log. That follows from the ruling: nothing on an assigned page draws the host's audit rows.@object-ui/app-shell: patch.trackHistoryobject): 12 requests in 2 waves → 10 requests in 1 wave.sys_audit_log/ author lookup at open on a non-History default tab; (b) opening History reads once and draws the rows; (c) CONTROL:?tab=historyreads at open; (d) an assigned page makes no host audit read. Part 2's pins stay green. Before-run on base code: (a), (b) and (d) red, (c) green. Ablation (thehistoryWantedguard deleted) turned (a), (b) and (d) red; the restore was proven by blob hash.app-shelltype-check 0; eslint 0 errors (warnings equal to base); the pre-tsc battery and the changeset checks all 0. The fullpackages/app-shell/dir is declared to CI.mainfirst.d1ec111(filter=latest),mergeable_stateclean.Verdict: ACCEPT. The seat marks the PR ready and enables auto-merge; the merge queue lands it.
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Landed · objectui#13010 part 5 · PR #13025 ·
domain:ui#2·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-19T06:33Zacefe6fonmain. Verified by content: all 3 files inacefe6f^1..acefe6fare blob-identical to the accepted headd1ec111. The squash message is model-free.trackHistoryrecord no longer readssys_audit_logand its author lookup at open. The read waits until the synthesized History tab is on screen; an authoreddefaultTabstrip still reads eagerly. An assigned page, where nothing draws the host's audit rows, no longer reads them at all. Measured: 12 requests in 2 waves → 10 in 1.pm:dispatchedand its assignee. In flight: part 4 (PR fix(components): a rail card and a tab badge over the same list share one read (objectui#13010 part 4) #13023, the rail and the badge share one read). It is in a fix round: a merge ofmainand the parent-change staleness repair found by its contract review.mainnow also carries this part's edit to the shared harness file.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
ACCEPT · objectui#13010 part 4 · PR #13023 ·
domain:ui#2·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-19T06:54ZPR #13023 at head
bc3667b(Part of #13010), reviewed against the part-4 claim6260082983(amended in place:Clause-②: yes), the dev report6260555066and the contract review6260789156(PASS, atCONTRACT_REVIEW_TIER).record:reference_railclaims its entries' rows inRelatedCountStorewhile it renders (claimRows). Every render in a commit finishes before any effect, so the claim precedes thepage:tabsprobe effect. A badge probe that finds a claim on its unfiltered key asks the rail's$topwith$countand holds the rows for a one-time hand-off (fetchRows). A rail asking first registers its read in flight and the badge joins it. No timing dependence; both orders are pinned.top=1probe.containers.tsxis unchanged.top=3. The filtered task badge and its card stay two reads.RelatedCountStore, exported from@object-ui/components' index, gainsclaimRows,fetchRowsandreleaseRows, each with a JSDoc stating the hand-off contract. Changeset@object-ui/components: minor,plugin-detail/app-shell: patch.0d2db8e, mergesc390b4d,bc3667b):refreshAfter: true, rail hidden or below the fold) let the rail's first read take rows held before the change. The rail now reads directly whenrefreshing || invalidationNonce > 0. Pinned (the off-screen rail, parent change, then on screen: a fresh second read draws the new row). Cost: a mounted rail that has heard an invalidation stops sharing for later records.inflight.get(k) === entry, so a read an invalidation overtook no longer overwrites the fresh count. Pinned.(object, relField, parentId)).mainmerged twice (parts 2 and 5 landed meanwhile, both on the shared harness). All three parts' harness pins are kept.bc3667b;components/plugin-detail/app-shelltype-check 0; eslint 0 errors; the pre-tsc battery and the changeset checks all 0. Full package dirs are declared to CI.xl, the badge probe fetches the rail'srowCaprows that a hidden rail never draws: a slightly larger probe.pm:dispatchedand its assignee; this is its last carried item. Items 3 and 5 were retired by measurement (rulings on the card).bc3667b(filter=latest),mergeable_stateclean.Verdict: ACCEPT. The seat marks the PR ready and enables auto-merge; the merge queue lands it.
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Closing · objectui#13010 · all items landed or retired by measurement ·
domain:ui#2·session_01XVp19E1MYUTFM3dnkwz8gB· 2026-10-19T07:09ZPart 4 landed: PR #13023 merged as
d0b9648. Verified by content: all 6 files ind0b9648^1..d0b9648are blob-identical to the accepted headbc3667b. The squash message is model-free.record:reference_railand thepage:tabsbadge now share one read of an unfiltered relation (RelatedCountStore.claimRows/fetchRows/releaseRows, a declaredminorwidening of@object-ui/components). The review's parent-change staleness finding and the overtaken-count guard were fixed before landing.The card's Wanted, item by item:
top=1count re-read per related card46dbf32(PR #13019) put the probes in wave 1. Part 4d0b9648(PR #13023) made the rail and the badge one read. A badge with a different filter keeps its own probe, and never shows a count from another filter.46dbf32: probes keyed on the route'srecordId.security/explainupdate,delete), and objectui#11699's sharing is present (ruling6258704497).29c19cc(PR #13021) and part 5acefe6f(PR #13025, the History tab'ssys_audit_log).meta/objectre-readsortability, which the list read lacks, and it costs no wave (ruling B on the card).Measured on the harness (the real
RecordDetailViewover the real adapter, HotCRM page shape): the card's 15 requests in 4 waves went to one wave, with every first-wave read the first paint needs (10–11 requests by page shape). Notop=1follow-up for a relation the rail also shows. None ofsys_activity/sys_comment/sys_attachment/sys_usertop=50 /sys_audit_loguntil their block is on screen.Empty remote refs left by refused ref deletes:
claude/issue-13010-object-schema-reuse.Closed
completed;pm:dispatchedis removed in the same act.Generated by Claude Code