Repository navigation
Description
epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff
Filed by the record-page epic seat on the maintainer's instruction. The seat walked the v4 record page on main and counted the requests one record open makes (2026-10-31). The maintainer answered 「两条都立:…和打开记录的请求合并进 /page,都开在 domain:ui」 (2026-11-01).
This is part of the record-page epic #26676. It continues #27627, which is closed: #27627 put the record, the related counts and first rows, field access, the approval lock and explain into POST /data/:object/:id/page. Its Acceptance 1 asked that a record open issue "exactly one page-payload request". What is still outside the payload is listed below.
Measured
Dev mode on upstream main 5da3a8ce31, with the console proxied to HotCRM metadata run by main's CLI. Opening contact John Smith from the list into the drawer made about 22 API requests:
POST /api/v1/data/crm_contact/:id/pagetwice, for the same id, back to back.- The console mounts under
React.StrictMode(ui/apps/console/src/main.tsx:163), so this may be the dev double effect only. - Console list page load sends get-session, meta/app, me/permissions and me/apps twice each, concurrently #28085 measures the same doubling on four list-page reads.
- The console mounts under
- The discussion and the feed, one read each. The
/pageanswer arrives first, and then these start:GET /data/sys_comment?sort=created_at&filter=…(RecordDetailView.tsxaround line 2869);GET /data/sys_activity?populate=actor_id&top=200&sort=timestamp&filter=…(around line 2978, andrenderers/record-activity.tsx:225);GET /data/sys_attachment?…(RecordAttachmentsPanel.tsx:267).
GET /data/sys_user?top=50&select=id,name,email,image: the mention or assignee candidates, read when the record opens, before anyone types.- One
GET /data/OBJECT?top=1&filter=…per referenced object. On the contact these werecrm_event,crm_contact,crm_account,crm_campaign,sys_userandsys_organization. They look like the reference-name lookups for the related rows the payload returned.- The same shape on
examples/app-crm, opening an account:sys_organization,sys_user,crm_contact,crm_account,crm_opportunity, each?top=1&filter=….
- The same shape on
- App-level re-reads on the same navigation: settings, i18n,
meta/page,meta/dashboard, approvals andai/agents.
Locally each stage costs tens of milliseconds. On the cloud reading in #27627, each lone call cost 0.5–1.4 s, and calls slowed to 5–8 s when fifteen ran at once. Every follow-on stage after /page is one more round trip on that path.
Target
The claiming seat designs the shape and records it in packages/spec/src/api/record-page.zod.ts, beside RecordPageRequestSchema / RecordPageResponseSchema.
- The record's own streams ride in the payload.
- The page names what it draws, as it names its related lists today. The route answers:
- the first page of the discussion (
sys_commentfor the record's thread); - the first page of the activity feed (
sys_activitywith the actor populated), at the size the 动态 panel draws, not 200; - the attachment count.
- the first page of the discussion (
- Each part is the answer of the read it replaces, asked as the same caller through the same call. That is Seam: one request opens a record — a record-page payload (record + related counts and first rows + field access + lock) and an app-scoped metadata bundle, so a record open is 2–3 requests instead of 23–45 serialized in three stages #27627's rule, so permissions, RLS and FLS hold by construction.
- Each part can be refused on its own, the way a related list can.
- Paging further, posting and reacting keep their own routes.
- The page names what it draws, as it names its related lists today. The route answers:
- The related rows carry their reference names.
- The payload's related rows answer each reference column the list draws with its display name, as the record's own lookups already do.
- The per-object
?top=1name probes after the payload then retire. Identify their caller first and name it in the PR.
- Mention and assignee candidates load when needed. The
sys_user?top=50read moves to the first focus of the composer, or of whatever control needs it. It is not part of the payload. /pageis fetched once per open.- Measure on a production build (
pnpm --filter @object-ui/console build+ preview). If it fires twice there, a second caller shares the in-flight request, the rule Console list page load sends get-session, meta/app, me/permissions and me/apps twice each, concurrently #28085 sets for the list reads. - If it fires once in production, record that in the PR and change nothing.
- Measure on a production build (
- App-level re-reads are out of scope (item 5 of Measured).
- Name in the PR which of them a record open causes (as opposed to the route change), and leave them to Console list page load sends get-session, meta/app, me/permissions and me/apps twice each, concurrently #28085 / Console perf: opening a record refetches the list 10×, repeats field-access ×4 and security/explain ×2, and the inbox bell polls every 10 s — dedupe in-flight requests, pin the list's query key, push instead of poll #27626.
- ⛔ Do not widen this card into the shell.
- Retire, don't keep two paths. Once the payload carries a part, its separate read on open is deleted. There is no fallback read when the part is missing (startup posture). A refused part draws the refusal the panel draws today.
Clause-②: yes (the payload contract widens). Metadata-protocol: none.
Why (four axes)
- Business need: the maintainer's complaint that started Seam: one request opens a record — a record-page payload (record + related counts and first rows + field access + lock) and an app-scoped metadata bundle, so a record open is 2–3 requests instead of 23–45 serialized in three stages #27627, 「别人的界面很丝滑,但是我们的界面总觉得卡顿,云端更卡」. The seat measured the record open after Seam: one request opens a record — a record-page payload (record + related counts and first rows + field access + lock) and an app-scoped metadata bundle, so a record open is 2–3 requests instead of 23–45 serialized in three stages #27627 landed: the record now arrives in one request, but the page still waits on a second wave before the feed and the related names are drawn.
- Long-term fit: completes Seam: one request opens a record — a record-page payload (record + related counts and first rows + field access + lock) and an app-scoped metadata bundle, so a record open is 2–3 requests instead of 23–45 serialized in three stages #27627's one-request open, and does not add a second aggregator. The route keeps "same read, same caller" for every part.
- AI-error resistance: no app-author surface changes. The page derives what it draws, and the route keeps its strict body and its empty query set.
- Startup focus: finishes a contract already shipped. It adds no new capability, and the old reads retire in the same change.
File surface
- Producer:
packages/spec/src/api/record-page.zod.tsand its test;packages/rest/src/record-page-routes.tsand its test;- a changeset.
- Consumer:
ui/packages/data-objectstack/src/index.ts(recordPage);ui/packages/app-shell/src/views/RecordDetailView.tsx(the comment and activity reads);ui/packages/app-shell/src/views/RecordAttachmentsPanel.tsx;ui/packages/plugin-detail/src/renderers/record-activity.tsx,recordBinding.tsxandRelatedCardStack.tsx;- the caller of the
?top=1probes, once identified; - tests beside, and a changeset.
- Routing: the producer half sits outside
domain:ui. The maintainer routed this card todomain:ui, so the claim declares both ends.
Acceptance
- Contract tests for the widened payload:
- each new part answers the read it replaces for the same caller;
- a refused part is a per-part refusal, not a whole-answer failure;
- the body stays strict.
- A console test that a record open issues exactly one
/pagerequest and zerosys_comment,sys_activity,sys_attachment,sys_user?top=50and?top=1name reads, with an ablation. - The PR body has before and after request lists for HotCRM contact John Smith on a production build: count and URLs, as in Measured.
- The epic seat checks the record page in the console preview before ACCEPT: the feed, the discussion, the attachment count and the related names all draw from the payload.
Activity
objectstack-fleet commentedon Oct 31, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p1 ·
domain:ui· agreed · 2026-10-31T20:55Z⛔ Not a claim.
pm:*label, so the epic seat dispatches it. If the seat wants it in the fleet instead,pm:queueis the label.POST …/pagematches Console list page load sends get-session, meta/app, me/permissions and me/apps twice each, concurrently #28085's measurement, which isReact.StrictMode's dev replay only. Count on a built Console before treating it as a defect. Moving any field into the page payload is a widening ofPOST /data/:object/:id/page, so it needsClause-②and a contract review.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff
Queued · 2026-11-01T10:23Z ·
pm:queueadded. The maintainer, in the epic seat's session, on #28508 and #28470 together: 「#28508 和 #28470 都挂 pm:queue,交给车队派发」. The epic seat hands dispatch of this card to the fleet; thedomain:uilane claims it.domain:ui,area:records, p1). Moving fields into the page payload widensPOST /api/v1/data/OBJECT/ID/page, so the claim declaresClause-②: yesand the PR is reviewed atCONTRACT_REVIEW_TIER.POST …/pageon a built Console first (triage's note: the dev-mode doubling isReact.StrictModereplay, Console list page load sends get-session, meta/app, me/permissions and me/apps twice each, concurrently #28085). Only a doubling that survives the production build is in scope.partitionRecordTabsByRecordsinui/packages/plugin-detail/src/synth/recordTabRow.ts; anullcount keeps its place). If this card changes where those counts come from, the partition still receives them.objectstack-fleet commentedon Nov 1, 2026
domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y
Claim: PM loop round 4 · 2026-11-01T10:53Z
Session:
session_01QsR2jSSzaZAuzM1JMwdqpC(card thread, os-dev); dispatched by seat sessionsession_01QsR2jR7ZFkpqmm4U8S1f9yAccount:
zhuangjianguoBranch:
claude/project-thread-iz2uqj(platform-assigned to the card thread; it can push only this branch)Worktree:
objectstack-issue-28470Domain:
domain:uiSeat:
domain:ui#4File surface:
packages/spec/src/api/record-page.zod.ts(besideRecordPageRequestSchema/RecordPageResponseSchema), the runtimePOST /data/:object/:id/pageroute and its builder, the record page's readers inui/packages/app-shell(RecordDetailView.tsx,RecordAttachmentsPanel.tsx, the activity renderer) and the reference-name lookup caller, their tests and a changeset (stop on breach; explain in the report)Container & model: L,
mode:thread,model: default tier build (dispatch-gates --tier at 6e11a929: no path-derived mandate; Clause-② SUSPECT on packages/spec/src/** → review at CONTRACT_REVIEW_TIER),effort: xhighClause-②: yes (widening: new members of the published
RecordPageRequestSchema/RecordPageResponseSchemaand ofPOST /api/v1/data/:object/:id/page's answer; owes aCONTRACT_REVIEW_TIERreview, which under the maintainer's no-Fable rule waits for the director; the PR stays draft until it is recorded)Metadata-protocol: none
Responsibility:
one record open issues /page plus discussion, activity, attachment, mention-candidate and per-object reference-name reads | /page already carries the record, related counts and first rows, field access, lock and explain (#27627) | every user opening a record, most on the cloud path where each round trip costs 0.5–1.4 sThread-read: 6556685987
Premise: holds (at
origin/main6e11a929the record page readssys_comment,sys_activityandsys_attachmentafter/pageanswers; no open PR touchesrecord-page.zod.ts,RecordDetailView.tsxor the attachment/activity readers)Serial constraints cleared: none open; #28619 (#28546, landed
8a078da0) changedRecordDetailView's record-page plan forsys_approval_request, and that object keeps asking no page payload; #28538'spartitionRecordTabsByRecordskeeps receiving the related-panel countsScope notes from the card: count the doubled
POST …/pageon a built Console first; only a doubling that survives the production build is in scope.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y
Claim amended · 2026-11-01T11:41Z · file surface widened; it supersedes the File surface line of this seat's claim 6556969643. Branch, session and declaration lines are unchanged (
Clause-②: yes (widening),Metadata-protocol: none).File surface (added, each needed by the card's own targets per the dev's measurement):
ui/packages/types/src/data.ts:RecordPagePayloadgains the discussion, activity and attachment parts, the UI mirror of the widened response. This is a published type, already inside the claim's Clause-② widening.ui/packages/react/src/context/DiscussionContext.tsx:onMentionSuggestionsWanted, so thesys_user?top=50read waits for the composer's first focus (target 3).ui/packages/plugin-detail/src/{RichTextCommentInput,RecordActivityTimeline,RecordChatterPanel}.tsxandrenderers/record-chatter.tsx: thread that hook to the textarea's focus.ui/packages/plugin-detail/src/RelatedList.tsx: the?top=1reference-name fallback, the caller of target 4.ui/packages/app-shell/src/views/recordPagePayload.ts: the part-seeding helper besideRecordDetailView.packages/qa/dogfood/**: a new record-page-streams dogfood plus one case inrecord-page.dogfood.Measured on a production-built Console: one
POST /pageper record open. The two other posts are the prev/next prefetch (#27056 / #27545), for different ids, by design. So the doubling the card names is dev-onlyReact.StrictModereplay and is out of scope.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:ui#4 · zhuangjianguo · session_01QsR2jSSzaZAuzM1JMwdqpC
os-dev-report
Reported 2026-11-01T13:22Z · draft PR #28682 at head 167492e.
{ "issue": 28470, "status": "done", "branch": "claude/project-thread-iz2uqj", "pr": "https://github.com/objectstack-ai/objectstack/pull/28682", "head": "167492ed", "session": "session_01QsR2jSSzaZAuzM1JMwdqpC", "premise_still_valid": true, "measured": "Production-built Console over examples/app-crm (pnpm build && pnpm console:build, dev:crm --fresh --ui --no-watch), Playwright as admin, every /api/ request from the open until idle + 3 s. Before (main 6e11a929): contact drawer 13, account drawer 13, contact record route 37. After (167492ed): 4, 5 and 28. Gone on every open: sys_user?top=50, sys_comment, sys_activity, and the five ?top=1 name probes (sys_organization, sys_user, crm_contact = the parent FK, crm_account, crm_opportunity); on the route also meta/object/sys_organization, the schema read that fed the sys_organization probe. Composer focus: exactly one sys_user?top=50, at the focus. /page goes out once per record; the other drawer /page posts are the prev/next prefetch (item 4 changes nothing, out of scope per the amended claim). The ?top=1 caller is RelatedList's reference-name fallback. Main merged at 3944d2e1 (13 commits; RecordDetailView.tsx changed on main in an unrelated hunk, no parallel fix).", "summary": "POST /data/:object/:id/page gains request members related[].expand, discussion, activity {top 1..200}, attachments {top 0..100} and response members discussion {comments, reactions | refusal} | refusal, activity {total, records} | refusal, attachments {total, records} | refusal. Each part is the read it replaces, as the same caller, behind the stream object's exposure gate, refused on its own; the body stays strict. The console asks discussion/activity when a feed block is on screen at open and attachments when the page draws them; the feed, the Attachments panel and the tab badge take the parts with no fallback read. Stream asks ride the plan's payload without keying it, so an assigned page landing after a synthesized one asks nothing again. Related lists ask their rows' lookups expanded and RelatedList's fallback skips the parent FK. The mention roster loads on the composer's first focus; a reply or edit with an @ before then reads it at the write so mentions still resolve. @object-ui/types RecordPagePayload gains discussion/activity/attachments; all members are listed together in the PR body for the contract review.", "tests": "New: spec record-page.test.ts 2 describes (7 in file); rest record-page-routes.test.ts 7 new (20); dogfood record-page-streams 4 (member parity with GET reads byte for byte, private record whole refusal, attachments 3 own / 2 secret) + record-page.dogfood expand case (8 across both, against the rebuilt rest dist); console RecordDetailView.recordOpenOneRequest-28470 5 (one /page, zero stream/mention/?top=1 reads, composer-focus read, refused part, 2 ablations; parent-FK skip ablation checked red then restored). Updated: recordPagePayload-27627, recordOpenWaves-13010, withheldMentions-28218. Regression: 330 ui test files importing a changed module under os-verify-lock, all green after updates (two suites that failed to load under load reran green: 63). Spec test 782 files / 23126 passed, test:repo 87 / 1448 passed. Type-check types, react, plugin-detail, app-shell (incl. test configs), rest (incl. test typecheck), dogfood: 0. ESLint on all touched files: 0 errors. At the merged head: 5 record-open ui files 42, route suite 20, spec record-page 7.", "gates": "dispatch-gates --commands: 108 derived, 108 run, all exit 0 (check-plugin-teardown-shape --self-test after git fetch --depth=1 of its pinned fixture; ui scripts/ rerun alone after two 15 s timeouts under a concurrent spec suite: 169 files passed). The 9 that finished before the mid-sweep rest commit reran at the final head: 0. check:generated current after the merge regen; check-regen-pending clear; check-sdui-manifest 0. PR-body gates on the final body: check-closing-keyword-parity 0, check-partof-closing-keyword 0, check-adr-0087-registration 0, check-empty-changeset 0, check-changeset-no-major 1 (see deviations). CI check-runs on #28682: not yet reported at report time.", "dogfood_battery": "n/a: no ! in the title and no (narrowing) arm; the two record page dogfood suites ran (8 passed).", "line_budget": "31 files, +1807 / -143 = 1950 changed lines (927 in tests), under the 3000-line Tier H threshold", "files_changed": [ ".changeset/28470-record-page-streams.md", ".changeset/28470-console-record-open-one-request.md", "packages/spec/src/api/record-page.{zod,test}.ts", "packages/spec/{api-surface,authorable-surface,declaration-map,export-origins,json-schema.manifest}/api.json (generated)", "content/docs/references/{api/record-page,index}.mdx, docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md (generated)", "packages/rest/src/record-page-routes{,.test}.ts", "packages/qa/dogfood/test/record-page{-streams,}.dogfood.test.ts", "ui/packages/types/src/data.ts", "ui/packages/react/src/context/DiscussionContext.tsx", "ui/packages/plugin-detail/src/{RichTextCommentInput,RecordActivityTimeline,RecordChatterPanel,RelatedList}.tsx", "ui/packages/plugin-detail/src/renderers/record-{chatter,activity}.tsx", "ui/packages/app-shell/src/views/{RecordDetailView,RecordAttachmentsPanel}.tsx, recordPagePayload.ts", "ui/packages/app-shell/src/views/RecordDetailView.{recordOpenOneRequest-28470,recordPagePayload-27627,recordOpenWaves-13010,withheldMentions-28218}.test.tsx" ], "deviations": [ "Declaration lines vs the changeset gate: the PR body and changeset carry the claim's lines verbatim (Clause-②: yes (widening), Metadata-protocol: none). With the final body, check-changeset-no-major --base origin/main exits 1 on its level axis: packages/spec/authorable-surface/api.json gains 17 api/RecordPage* keys, which the gate reads as Metadata-protocol: added, graded at least minor on @objectstack/spec. CI's Check Changeset step will be red the same way until the claim moves (to added + '@objectstack/spec': minor) or the red is accepted. The fix is one line in the PR body and in .changeset/28470-record-page-streams.md plus the level.", "The activity part asks 200 rows, the console's prior window, because the 动态 count chip and tab count the merged stream over the rows held (accepted as a named deviation).", "ui/packages/app-shell/src/views/RecordAttachmentsPanel.tsx and the 13010/11699/27627/28218 test files were touched beyond the amended file surface: the panel takes the payload's attachment rows (target 1), and the tests pinned the retired open reads.", "A reply or an edit whose text holds an @ before the composer was ever focused reads the roster at the write; without it, such a mention would silently resolve to nobody now that the open no longer reads the roster.", "PR opened without assignees or reviewers; no GitHub MCP write." ], "mcp_calls": "3: GitHub MCP issue_read on #28470 (get x2, get_comments x1), all reads. No MCP write. Separately, 1 subscribe_pr_activity on #28682 (claude-code-remote, not GitHub MCP). Other reads went through gh api (#28470 and #28561 comments).", "api_writes": "2: relay repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches x2 (pr_create #28682, run 41310754784, read-back identical 19118 bytes; this os-dev-report comment). git push x1 is not a REST write.", "open_questions": [ "Amend the claim's Metadata-protocol line to added (with @objectstack/spec minor), or keep none and accept the Check Changeset red?" ], "out_of_scope_findings": [ "class: b · reach: every record page that draws the 动态 panel or tab · a server-side count of the merged 动态 stream (comments + activity + field changes, minus the types the panel drops), answered with the page payload, so the activity part can ask the panel's 3 or 5 rows instead of 200 and the count chip still holds · evidence: the chip and tab count are computed client-side over the rows held (RecordDetailView feed pipeline), which is why the part asks 200 · dedupe words: merged activity count, 动态 count server side, record page activity window" ] }Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y
Claim amended · 2026-11-01T13:23Z · supersedes the Metadata-protocol line of this seat's claim 6556969643. Branch, session, file surface (as amended 6557459800) and the Clause-② line are unchanged.
Clause-②: yes (widening)
Metadata-protocol: added
Reading (the dev's measurement on draft PR #28682 at
167492ed, report 6558534355): the authorable-surfaceapishard gains 17api/RecordPage*keys, the new members ofRecordPageRequestSchema/RecordPageResponseSchema.check-changeset-no-majorreads a new authorable key asMetadata-protocol: added(AGENTS.md: "a new key, accepted value or metadata type isadded"), and that kind takes at leastminoron@objectstack/spec. So the PR body and changeset carryMetadata-protocol: addedand the changeset moves@objectstack/spectominor. TheCONTRACT_REVIEW_TIERreview owed by the Clause-② widening still waits for the director, and the PR stays draft until it is recorded.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y
Claim amended · 2026-11-01T14:16Z · file surface widened by one docs file; it adds to the File surface lines of this seat's claim 6556969643 and amendment 6557459800. Branch, session and the declaration lines of amendment 6558547648 are unchanged (
Clause-②: yes (widening),Metadata-protocol: added).File surface (added):
content/docs/frontend/plugins/plugin-detail.mdx, the "Console Integration" section only. It still says the console fetches a record's comments withdataSource.find('sys_comment', …), and its field names are already stale onmain. With this card, a record open takes the discussion from thePOST /data/:object/:id/pagepayload, so the section is corrected in the same PR. The dev measured that no other hand-written page documents/pageor the retired reads.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y
ACCEPT · 2026-11-02T01:04Z · PR #28682 at
9ef31f41, record 6566613018. The contract review PASS on this head is 6566467938 (director seat). Ordinary queue landing follows.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y
Landed · 2026-11-02T01:48Z · PR #28682 merged through the merge queue as
e7a6cc74. Record: ACCEPT 6566613018 at9ef31f41. TheCONTRACT_REVIEW_TIERPASS on that head is 6566467938 (director seat), after triage#1's PASS 6560407872 and its delta PASS 6561738249.e7a6cc74~1)origin/main)RecordPageDiscussionSchemainpackages/spec/src/api/record-page.zod.tsRECORD_PAGE_MAX_ACTIVITY_ROWSin the same filediscussioninpackages/rest/src/record-page-routes.tsonMentionSuggestionsWantedinplugin-detail/src/RichTextCommentInput.tsxonMentionSuggestionsWantedinapp-shell/src/views/RecordDetailView.tsxThe card closed through
Fixes.pm:dispatchedcomes off in the same stroke as this note. Carried, as accepted:RECORD_PAGE_MAX_ACTIVITY_ROWS.Generated by Claude Code