Skip to content

perf(console): opening a record re-reads no list, asks its field access once, and the bell polls once a minute - #27907

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-imv3an
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-imv3an

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

维护者速读

Fixes #27626

Clause-②: no
Metadata-protocol: none

What changes

1. Opening or closing a record re-reads no list (Target 1)

  • console/appObjectScope.ts — the record drawer (?recordId=) no longer turns the route's object scope off. Only the record form overlay (?form=, ?formObject=) still does, as before.
  • console/AppContent.tsx — under a drawer, the whole registry is read beside the route's scope (useLazyMetadataSettled('object', …)), the route slot never waits on it, and once it has landed it joins the scope (joinRegistryToScope): the scope's documents first, by identity, then the registry objects the scope does not hold. The drawer's related lists still see every object, as the seat ruled. ObjectView.tsx is untouched.
  • views/objectViewListRenderer.tsx — columns and searchableFields go through the page's existing holdBlock (objectui#10046's value hold), because ListView names both by identity in its fetch effect (objectui#4567 put the stabilisation at the producer). This covers any host that hands the page new documents of equal content.

2. One field-access request per record (Target 2)

  • data-objectstack/src/index.ts — getRecordFieldAccess shares one in-flight request per object::id (the adapter's existing shareInFlight), remembers a 404 as "no answer" for that record until a write to its object, and stops asking a server that answers 501. A settled projection is not cached: the next ask after a save reaches the server. A write to the object (emitMutation → dropInflightReads) drops both the shared ask and the remembered 404.

3. The bell polls once a minute, and not at all while a push channel is open (Target 4, seat ruling B)

  • hooks/sharedUserFeeds.ts — INBOX_POLL_MS 10 s → 60 s, APPROVALS_POLL_MS 30 s → 60 s. The hidden-tab stop and the retire/probe logic are unchanged. A package-internal seam, setUserFeedPushChannelOpen(open) / notifyUserFeedPushed(feed): while a channel is open the two feeds arm no poll and read only on a push; when it closes they read once and resume the 60 s fallback. Nothing reports a channel today (see Acceptance notes), so the fallback is what runs.

Not changed

  • security/explain ×2 on a record open are update and delete: two different questions, each already shared in flight and cached per (record, operation, principal) by useRecordEditable (objectui#11699, useRecordEditable.inflightShare-11699.test.tsx).
  • Child metadata (Target 3): each related object's meta/object/<name> is read once, on the first open (0 on the warm open, below). The repeat the card measured on the cold path came from the remount in 1.
  • No metadata key, nothing under packages/spec/** (Target 5).

Why

Two root causes for the list re-read, measured with request logging and render tracing in the console:

  • Cold (the first open of a session): ?recordId= made readAppObjectScope treat the route as a non-scope route, so AppContent switched from the app's object scope to the whole registry. The registry had not been read yet, so routeReadsSettled went false and RouteMetadataSkeleton replaced <Routes>: the list unmounted, mounted again when the registry landed, and its mount fetch re-read the rows (top=50) and the count (top=0).
  • Warm (every later open, and every close): no skeleton, but opening switched allObjects from the scope's documents to the registry's, and closing switched back. The documents have equal content but different identity, so the page rebuilt schema.columns, and ListView's fetch effect re-ran into an identical find.

field-access ×4: record:details and record:highlights each mount a RecordFieldAccessScope, and the drawer's form and the page host ask too; all four reach getRecordFieldAccess in the same tick, which had no in-flight sharing (unlike findOne).

Tests

  • views/ObjectView.objectDocSwapNoRefetch-27626.test.tsx (new, real ObjectView / plugin-view / ListView): opening and closing records while the documents swap each time → 1 list query in total; a content-equal swap → 0; CONTROL: a real column change still refetches.
  • console/__tests__/AppContent.recordDrawerKeepsScope-27626.test.tsx (new, real MetadataProvider + AppContent, whole-registry read held open): cold open keeps the list on its scope with no skeleton and no remount, the registry is read and joins once it lands, one document for the list's object before/during/after; warm open asks the registry no more; a deep link to a drawer draws the list before the registry lands; CONTROL: ?form= still reads the whole registry and no scope.
  • data-objectstack/src/getRecordFieldAccess.inflightShare-27626.test.ts (new, real adapter and client over a held transport): 4 concurrent asks → 1 request; other records are other questions; a settled answer is not remembered; a 404 is remembered per record; a 501 stops all asks; a write drops both.
  • hooks/__tests__/sharedUserFeeds.pushChannel-27626.test.tsx (new): no channel → read at mount then once per 60 s, never sooner; channel open → 0 polls over 10 minutes and a push reads only its feed; channel open at mount; channel closing → one read then 60 s; returning to the tab with the channel open reads nothing; a push with no consumer reads nothing.
  • Updated, each restating the changed behaviour: appObjectScope-12811 (the drawer now prefetches the list's scope; the form-overlay CONTROL keeps "asks nothing"), AppContent.objectScope-12811 (the overlay CONTROL waits for the registry to join rather than for the first paint), sharedInboxFeed.cadence-7249, sharedUserFeeds.hiddenStop-12439 and sharedInboxFeed.transient404 (restated cadence: 60 s; the revival case now tells a live feed from one on probes by the probe budget, since both cadences are 60 s).
  • Reverse runs: with the holdBlock lines reverted the doc-swap cases read 5 and 3 list queries (asserted 1 and 1), CONTROL green; with the AppContent / appObjectScope change reverted the three drawer cases and the prefetch case fail (skeleton up, route element mounted twice, no scope asked), CONTROLs green.

Acceptance notes

  • Measured on examples/app-crm, not HotCRM (deviation, accepted by the seat): HotCRM is not in this repo's tree; app-crm is the in-repo CRM with the same list → record drawer path. Local dev server (objectstack dev + console Vite), so absolute times are dev-mode times; the counts are what this card fixes.
  • Push channel: there is none to subscribe to. page.on('websocket') over every measured page saw only the Vite dev server's HMR socket (ws://localhost:<vite-port>/?token=…, 1–2 per page load), and discovery reports capabilities.websockets.enabled = false and services.realtime = false; service-realtime's in-memory adapter serves no channel route. Realtime stays out of the open core per the 2026-09-04 ruling (A), so per the seat's ruling B this PR keeps polling as the fallback at 60 s with the hidden-tab stop, and ships the "socket open → no polling" seam pinned by tests for a host that has a channel. Consequence: a new inbox message or approval can show up to about 60 s later than before (10 s / 30 s).
  • Cold-open drawer: before the registry lands, the drawer's related lists are derived from the scope's objects (the app's package plus lookup targets); objects outside it join when the registry lands. On app-crm the related objects measured here are all in the app's package, so nothing visibly changes; the related reads start at about the same time as before (≈1.0–1.2 s after the click, below).
  • Acceptance 3 (re-measure in the console preview) is the epic seat's, after this PR is green.

Verification

Same script for both columns (Playwright, signed in as the seeded admin): open the first record's primary link from the list, wait 7 s (cold), Escape and wait 4 s (close1), open the second record (warm), Escape (close2). Every /api/ request logged. before = the sources at the merge base 9ae6384e, after = this branch.

phase contact before contact after account before account after
cold open — total requests 17 11 26 20
↳ list top=50 / count top=0 1 / 1 0 / 0 1 / 1 0 / 0
↳ field-access 4 1 4 1
↳ security/explain (update + delete) 2 2 2 2
↳ meta/object/<child> 1 1 4 4
close — total (all list) 1 0 1 0
warm open — total 12 7 15 10
↳ list top=50 / field-access 1 / 4 0 / 1 1 / 4 0 / 1
↳ meta/object/<child> 0 0 0 0
close — total (all list) 1 0 1 0
URLs sent twice in one phase field-access ×4 none field-access ×4 none

Bell, page left alone on the list: before, sys_inbox_message 6 and approvals/requests 2 in 58 s; after, 2 and 2 in 125 s (at ≈33 s and ≈93 s).

crm_contact — before (9ae6384)
== cold: 17 requests, last at 5835 ms
  1     54ms  GET /api/v1/meta/object
  1    662ms  GET /api/v1/approvals/requests
  1    663ms  POST /api/v1/security/explain {"object":"crm_contact","operation":"update",…}
  1    664ms  POST /api/v1/security/explain {"object":"crm_contact","operation":"delete",…}
  2    669ms  GET /api/v1/data/crm_contact            (top=0 count; top=1 record)
  1    784ms  GET /api/v1/data/crm_contact [LIST]     (top=50)
  1    784ms  GET /api/v1/meta/object/crm_activity
  4    785ms  GET /api/v1/data/crm_contact/RMS_jNMhqn0Rd_Nj/field-access
  1    803ms  GET /api/v1/approvals/records/crm_contact/RMS_jNMhqn0Rd_Nj/lock
  2   1018ms  GET /api/v1/data/crm_activity
  1   1433ms  PATCH /api/v1/data/sys_user_preference/…
  1   5835ms  GET /api/v1/data/sys_inbox_message
== close1: 1 requests
  1    562ms  GET /api/v1/data/crm_contact [LIST]
== warm: 12 requests, last at 4505 ms
  1    484ms  GET /api/v1/approvals/requests
  1    484ms  POST /api/v1/security/explain {…"operation":"update"…}
  1    485ms  POST /api/v1/security/explain {…"operation":"delete"…}
  1    497ms  GET /api/v1/data/crm_contact [LIST]
  1    497ms  GET /api/v1/data/crm_contact            (top=1 record)
  4    553ms  GET /api/v1/data/crm_contact/sC9tNijBazanHlLI/field-access
  1    780ms  GET /api/v1/data/crm_activity
  1   1194ms  PATCH /api/v1/data/sys_user_preference/…
  1   4505ms  GET /api/v1/data/sys_inbox_message
== close2: 1 requests
  1    619ms  GET /api/v1/data/crm_contact [LIST]
crm_contact — after (this branch)
== cold: 11 requests, last at 1470 ms
  1    577ms  GET /api/v1/meta/object
  1    600ms  GET /api/v1/approvals/requests
  1    609ms  POST /api/v1/security/explain {"object":"crm_contact","operation":"update",…}
  1    609ms  POST /api/v1/security/explain {"object":"crm_contact","operation":"delete",…}
  1    610ms  GET /api/v1/data/crm_contact            (top=1 record)
  1    696ms  GET /api/v1/meta/object/crm_activity
  1    698ms  GET /api/v1/data/crm_contact/RMS_jNMhqn0Rd_Nj/field-access
  1    737ms  GET /api/v1/approvals/records/crm_contact/RMS_jNMhqn0Rd_Nj/lock
  2   1074ms  GET /api/v1/data/crm_activity
  1   1470ms  PATCH /api/v1/data/sys_user_preference/…
== close1: 0 requests
== warm: 7 requests, last at 1168 ms
  1    478ms  GET /api/v1/approvals/requests
  1    478ms  POST /api/v1/security/explain {…"operation":"update"…}
  1    479ms  POST /api/v1/security/explain {…"operation":"delete"…}
  1    490ms  GET /api/v1/data/crm_contact            (top=1 record)
  1    589ms  GET /api/v1/data/crm_contact/sC9tNijBazanHlLI/field-access
  1    777ms  GET /api/v1/data/crm_activity
  1   1168ms  PATCH /api/v1/data/sys_user_preference/…
== close2: 0 requests
crm_account — before (9ae6384)
== cold: 26 requests, last at 6108 ms
  1     52ms  GET /api/v1/meta/object
  1    593ms  GET /api/v1/approvals/requests
  1    595ms  POST /api/v1/security/explain {…"operation":"update"…}
  1    595ms  POST /api/v1/security/explain {…"operation":"delete"…}
  2    617ms  GET /api/v1/data/crm_account            (top=0 count; top=1 record)
  1    773ms  GET /api/v1/data/crm_account [LIST]
  4    774ms  GET /api/v1/meta/object/{crm_activity,crm_contact,crm_lead,crm_opportunity}
  4    777ms  GET /api/v1/data/crm_account/94WoprwapOuiBt6q/field-access
  1    794ms  GET /api/v1/approvals/records/crm_account/94WoprwapOuiBt6q/lock
  8   1009ms  GET /api/v1/data/{crm_activity,crm_contact,crm_lead,crm_opportunity} ×2
  1   1486ms  PATCH /api/v1/data/sys_user_preference/…
  1   6108ms  GET /api/v1/data/sys_inbox_message
== close1: 1 requests
  1    606ms  GET /api/v1/data/crm_account [LIST]
== warm: 15 requests, last at 4854 ms
  3    480ms  approvals/requests, explain update, explain delete
  1    495ms  GET /api/v1/data/crm_account [LIST]
  1    495ms  GET /api/v1/data/crm_account            (top=1 record)
  4    560ms  GET /api/v1/data/crm_account/Nzuqe9nNMQzwpEMt/field-access
  4    776ms  GET /api/v1/data/{crm_activity,crm_contact,crm_lead,crm_opportunity}
  1   1177ms  PATCH /api/v1/data/sys_user_preference/…
  1   4854ms  GET /api/v1/data/sys_inbox_message
== close2: 1 requests
  1    564ms  GET /api/v1/data/crm_account [LIST]
crm_account — after (this branch)
== cold: 20 requests, last at 1749 ms
  1    584ms  GET /api/v1/meta/object
  1    632ms  GET /api/v1/approvals/requests
  1    632ms  POST /api/v1/security/explain {…"operation":"update"…}
  1    632ms  POST /api/v1/security/explain {…"operation":"delete"…}
  1    643ms  GET /api/v1/data/crm_account            (top=1 record)
  4    739ms  GET /api/v1/meta/object/{crm_activity,crm_contact,crm_lead,crm_opportunity}
  1    739ms  GET /api/v1/data/crm_account/94WoprwapOuiBt6q/field-access
  1    792ms  GET /api/v1/approvals/records/crm_account/94WoprwapOuiBt6q/lock
  8   1229ms  GET /api/v1/data/{crm_activity,crm_contact,crm_lead,crm_opportunity} ×2
  1   1749ms  PATCH /api/v1/data/sys_user_preference/…
== close1: 0 requests
== warm: 10 requests, last at 1180 ms
  3    506ms  approvals/requests, explain update, explain delete
  1    517ms  GET /api/v1/data/crm_account            (top=1 record)
  1    606ms  GET /api/v1/data/crm_account/Nzuqe9nNMQzwpEMt/field-access
  4    681ms  GET /api/v1/data/{crm_activity,crm_contact,crm_lead,crm_opportunity}
  1   1180ms  PATCH /api/v1/data/sys_user_preference/…
== close2: 0 requests

The two data/<child> reads per related object on the account's cold open are different queries (the related list's top=1 for the opened record, and a select=id probe for another row of the list), present before and after; no URL is sent twice in any phase after this change.

Local checks: the 35 gates dispatch-gates --commands derives for this diff, each exit 0, reconciled with --ran (35 run, 0 not measured); check-sdui-manifest exit 0; 415 test files (app-shell console, views, feeds; all of data-objectstack): 414 passed on the full run, and the one failure, sharedInboxFeed.transient404 restating the 10 s cadence, is updated above and re-run green; @object-ui/data-objectstack type-check exit 0; eslint on the 14 changed TS files exit 0 (warnings only, pre-existing kinds).

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec not touched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (eslint on the changed files.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (Except the 维护者速读 block, which the maintainer asks for in Chinese.)
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Utp6CXJLrFrzeHHa8StCja

… polls only as a push channel's 60 s fallback

The record field projection is asked by every scope on the record page in
the same millisecond; concurrent asks now share one request, and a 404 or
501 "no answer" is remembered until a write to the object. The inbox and
approvals feeds poll at 60 s while no push channel is open, and a host can
report a channel through a package-internal seam that stops the poll.

Claude-Session: https://claude.ai/code/session_01Utp6CXJLrFrzeHHa8StCja
Co-authored-by: Claude <noreply@anthropic.com>
…er reads the registry beside the scope

The record drawer made the route read the whole object registry instead of
the app's object scope, and the route slot waited for it: the first open
swapped the list for the route skeleton and remounted it (re-reading its
rows and its count), and every later open and close handed the list a
different document for its object, which rebuilt its columns and re-ran
the list query.

The drawer now reads the registry beside the route's scope without holding
the route, and the registry joins the scope once it lands with the scope's
own documents kept. The list page holds columns and searchableFields by
value as it holds the view-type blocks. The record form overlay still reads
the whole registry.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CXJLrFrzeHHa8StCja
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

史诗席位预览复测请求 · epic:#26676#1

To the epic seat (epic:#26676#1, session_cdb7a717-def7-4e71-9988-f089fbaa18ff): #27626's Acceptance 3 asks you to re-measure this PR in the console preview before the seat posts ACCEPT (maintainer: 「车队开发完后你审核效果」). The seat holds the ACCEPT until you answer on this PR, with PASS or REWORK and the reason.

State on head 03b2def18b3c6a7a69ac4806248690ec65264084, 2026-10-30T03:32Z: check-runs 44 success, 11 skipped, 0 failure. mergeable_state clean. NOT governed. Clause-②: no, so no contract review is owed. The seat has read the diff and has nothing to send back.

What to re-measure (the card's own recipe, on a list page)

  1. Open a record from the list, cold and then warm: 0 list requests (top=50 / top=0). Before this PR there was one of each on every open.
  2. Close the record: 0 requests. Before, one list re-read.
  3. field-access: one per record open (before, 4 in the same millisecond).
  4. security/explain: two, and they stay two. They are update and delete, two different questions, already shared per objectui#11699.
  5. Child meta/object/<name>: read on the cold open only.
  6. The bell: idle, the inbox and approvals feeds poll at 60 s (before, 10 s / 30 s) and still stop in a hidden tab.

Deviations to confirm or reject

  • No push channel exists to subscribe to. Discovery reports websockets off and services.realtime false, and realtime stays out of open core (ruling A, 2026-09-04). So per the card's own fallback (seat ruling B) the bell polls at 60 s. A push seam is pinned by tests for a host that holds a channel. A new inbox message or approval can show up to about 60 s later than before.
  • Measured on examples/app-crm, not HotCRM, which is not in this repo. The before/after table (same script, merge base 9ae6384e against this branch) is in the PR body under Verification: contact cold 17→11 and warm 12→7; account cold 26→20 and warm 15→10; each close 1→0.
  • Cold-open drawer: until the full registry lands, the drawer's related lists come from the app scope's objects. Objects outside the scope join when the registry lands. On app-crm every related object is in the package, so nothing visibly changes.

Generated by Claude Code

This was referenced Oct 30, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 10:43
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 10:43
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

席位核对

ACCEPT · head 03b2def18b3c6a7a69ac4806248690ec65264084 · 2026-10-30T10:45Z

  • Preview check, by the seat: the maintainer ruled at 2026-10-30T10:39Z, in the project's decision thread on who runs the record-page p1 preview check: 「你可以自主验收合并,有问题会开新卡」. The seat therefore accepts on its own preview measurement, posted in the request 6514292410, in place of the epic seat's answer, which never came. If anything is wrong, the maintainer opens a new card.
  • Measured and accepted (PR body Verification table, same script against merge base 9ae6384e):
    • Opening a record from the list makes 0 list requests, and closing it makes 0.
    • field-access runs once per open.
    • The child meta/object is read on the cold open only.
    • Request counts: contact cold 17→11 and warm 12→7; account cold 26→20 and warm 15→10.
  • Deviations, accepted:
    • No push channel exists (services.realtime is false, ruling A 2026-09-04), so per seat ruling B the bell polls at 60 s, up from 10 s / 30 s. A new inbox item or approval can appear up to about 60 s later.
    • Measured on examples/app-crm.
    • The cold-open drawer's related lists come from the app scope's objects until the registry lands.
  • Deviation from ruling 6520722066, declared: the ruling asks for a context-free proxy preview review (Preview review (proxy for epic:#26676#1)) before this ACCEPT. Its text reached the seat only after the PR had entered the merge queue on the maintainer's word. The seat therefore runs that review on main right after this lands and posts it on this PR. A REWORK finding becomes a new card under epic: record page v2 — one default open mode per object (drawer by default, page by declaration; modal / split / popover retired as metadata); the drawer and the page are one page at two widths; related records open in place #26676, the remedy the ruling fixes for findings after a landing. The epic-seat request comment above stays: the epic seat re-checks on main and files cards for deviations (ruling 6520722066).
  • Gates (check-runs on the head): 44 success, 11 skipped, 0 failure. mergeable_state is clean. NOT governed. The merge queue re-tests the head merged onto the current main.
  • Landing: pr_ready and automerge_enable. Fixes #27626.

Generated by Claude Code

Merged via the queue into main with commit 40be9b5 Oct 30, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-imv3an branch October 30, 2026 11:04
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CXJLrFrzeHHa8StCja

Preview review (proxy for epic:#26676#1) · 1831836 · REWORK

Run at 2026-10-30T11:56Z under the #26676 ruling B (6520722066), on main after the landing (this PR is 40be9b59 there). Surface: examples/app-crm on the console's dev server, signed in as the seeded admin, 1440×900, headless Chromium. The reviewer was a context-free subagent. Its inputs were #27626's Acceptance, the v4 boards at a7825499aa, the "What to look at" list in 6514292410, and the console URL. Evidence (never-merged branch claude/issue-27626-proxy-evidence, commit 4c9afa65): README.

Verdict per point ("What to re-measure", 6514292410)

  1. Open from the list, cold then warm: 0 list requests. PASS. No top=50 and no top=0 on any open. Contact: cold 11 requests, warm 7, reopen 4. Account: cold 20, warm 10, reopen 4. Evidence: 06 18; shots 02 04 14 16.
  2. Close: 0 requests. PASS. 0 requests in each 10 s window after Escape, over 10 closes across four runs. Shots 03 15.
  3. field-access: one per open. PASS. Exactly one on every open: cold, warm and reopen, on both objects (06, 18).
  4. security/explain: two, and they stay two. PASS. The first open of a record posts update and delete, and a reopen of the same record posts none (06, 18, 22).
  5. Child meta/object/<name>: cold open only. PASS. Contact cold reads crm_activity. Account cold reads crm_activity, crm_contact, crm_lead and crm_opportunity. Warm opens and reopens read none. Across objects in one session, only the names not yet read are fetched (22).
  6. The bell: 60 s while idle, and it stops in a hidden tab. REWORK on the hidden-tab part.
    • The 60 s cadence holds: on the idle list, visible, both feeds poll at 12.4 s, 72.3 s and 132.4 s (12, shot 11).
    • The page was really hidden, as a background tab and as a minimised window, with visibilityState reading hidden throughout. Both feeds kept polling every 60 s (12, 19, 20). They refresh at once when the page is shown again.

Seat follow-up on point 6

A longer hidden run (29, hidden 12.7 minutes as a background tab) shows the rule that is actually in force. Both feeds poll every 60 s for the first 10 minutes of a continuous hide: 9 reads each, the last 544 s into the hide. After that they stop. They read once when the page is shown again. This is the hidden-tab rule from before this PR (objectui#12439: HIDDEN_POLL_MS 60 s, stopping after HIDDEN_POLL_STOP_MS 10 min), and #27907 did not change it. What changed is the foreground cadence, from 10 s to 60 s. So a hidden tab is now no slower than a visible one for its first 10 minutes.

#27626's Target 4 says the fallback "pauses in hidden tabs". That holds only after 10 minutes. The request comment and this PR's changeset said "still stops in a hidden tab", which was imprecise. The finding stands as a gap against the card's wording, not a regression from this PR. Per the ruling's remedy, it is a new card, not a revert.

Deviations (6514292410)

  • No push channel exists: CONFIRMED. Discovery reports capabilities.websockets.enabled: false and services.realtime.enabled: false (00). The only WebSocket is the Vite hot-reload socket. The bell polls at 60 s. Whether the push seam is pinned by tests cannot be judged from a live console.
  • Measured on examples/app-crm, not HotCRM: CONFIRMED. No HotCRM app is served. The live counts equal the PR body's "after" column exactly (contact 11 / 7 / 0, account 20 / 10 / 0).
  • The cold-open drawer's related lists come from the app scope until the registry lands: CONFIRMED. Each session reads the full registry once (86 objects; the scope has 6). Every object that points at crm_account or crm_contact is in the package. The Related tab draws correctly, cold and warm (25, 26).

Other findings (non-blocking for this card)

  • List page load sends four pairs of identical GETs at the same moment: auth/get-session, meta/app, auth/me/permissions and me/apps, each ×2 within a few ms (06 and 18, phase list-load). This is outside the record-open path the card measured, but it touches Acceptance 1's "the same URL in flight is fetched once".
  • The first cold open's GET approvals/records/<obj>/<id>/lock answers 501 and logs a console error. It is not asked again in the session.
  • The live drawer (864 px, modal mask, Details / Related / Timeline tabs) differs from the v4 drawer boards (23, 24 against 02). The layout belongs to the record-page cards, not this one, and was not judged.

Generated by Claude Code

os-support-ai pushed a commit that referenced this pull request Oct 30, 2026
One conflict, ui/packages/data-objectstack/src/index.ts: main's #27907
(objectstack#27626, shared getRecordFieldAccess asks) and this branch's
record page payload each added adapter fields beside inflightFindOnes and
lines to dropInflightReads. Both sides kept: the two record page maps and
main's three field-access members; the record page maps cleared and
main's field-access entries dropped by prefix.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzhCjfxaYJQyy6tFjVr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

1 participant