Skip to content

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

Description

@objectstack-fleet

Filing gate: ③ maintainer-directed task (provenance below) — on the admission classes alone this is an observation (objectui#8014 closed the two-call form not_planned, and its reopen condition — record-open latency measured with explain as a named contributor — is not met by this run); the maintainer asked for it as its own card.
Reader: objectui triage first-touch → domain:ui lane (packages/types/src/http-inflight.ts callers, packages/plugin-detail / packages/app-shell record page).
Dedup (semantic search, open + closed, objectui): "record page fetches the same record twice concurrently, duplicate GET data record, security explain four requests" → 3 hits, all closed: objectui#8014 (not_planned, explain ×2), objectui#5544 (completed — boot duplicates of runtime/config and auth/me/localization, fixed by sharing one in-flight GET).

Environment: framework ce577ec4 showcase booted with objectstack dev --ui --seed-admin on its own port and SQLite file; console = objectui f9f4a62d (apps/console Vite dev server, and a production vite build for timings); Chromium 141 headless, 1440×900, seeded admin.

Measurement — production build, record page full reload, 4 runs (2 record-page, 2 home), all consistent

Requests captured with Playwright request / requestfinished events (resource timing is not usable here: the eager chunks fill its 250-entry buffer before the first API call).

  • GET /api/v1/data/showcase_task/:id — ×2 concurrent, identical URL (one run ×3).
  • POST /api/v1/security/explain — ×4 concurrent (bodies not compared; objectui#8014 measured ×2 per open).
  • GET /api/v1/data/showcase_task?populate=… — ×2 sequential, same query prefix (full query strings not compared).
  • GET /api/v1/auth/get-session — ×2 sequential; GET /api/v1/runtime/config — ×2 sequential in 2 of 4 runs.
  • PATCH /api/v1/data/sys_user_preference/:id (ui.recent) — ×2 sequential per visit (each audited; see console dogfood sweep (showcase, 2026-10-06): 17 browser-reproduced defects — checklist card for triage to split #11672 item 5).
  • Not duplicates (checked): the three sys_user_preference GETs read three different keys (ui.recent, ui.favorites, ui.flow.palette.recents); the two approvals/requests and two sys_activity GETs carry different queries.

Totals per record-page reload: 41–44 API calls.

Where

packages/types/src/http-inflight.ts shares one in-flight GET only for callers that route through it (its header names runtime/config and auth/me/localization); the record GET's two concurrent callers and the explain callers do not. Sequential repeats (get-session, runtime/config, the list query) are outside its design (no response cache by intent).

Expected

One request per distinct resource per record open: the record fetched once and shared, the explain verdicts batched or shared; sequential repeats either removed at the caller or explicitly accepted.

Provenance: filed as a separate card on the maintainer's explicit instruction in the dispatching session, which lifts the per-fire filing quota for this batch of seven, verbatim — 「重新思考以上和用户体验相关的问题,哪些需要立卡」 then 「分开立 7 张」. Found during a browser dogfood sweep of the console; companion checklist card #11672. Filed by session session_01WEARdKfDHDT7rTuEhLyT1k.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: ② the capabilities an end user meets in the app — record pages | 缺项 | P2

    Triage: first grade — enhancement · priority:p3 · domain:ui · area:records · pm:queue. Opening a record issues one record read and one explain per question, with concurrent callers sharing them

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-06T07:03Z. ⛔ Not a claim, ⛔ not a dispatch. Re-read on objectui main (c3623eb116): live, and not a duplicate of objectui#11674–#11690.

    Triage: lands in packages/data-objectstack/src/index.ts (findOne, about :3668, with no in-flight sharing, unlike find about :3505–:3507) and packages/plugin-detail/src/useRecordEditable.ts (about :215–:236, which caches only finished answers), or one of the two hosts' duplicate calls (RecordDetailView.tsx about :584, :1352 and :1358; DetailView.tsx about :402, :408 and :688) ⇒ domain:ui; rationale: two hosts each fetch the same record and explain the same questions.

    • Scope: the card's sys_user_preference double-PATCH bullet belongs to objectui#11678 (Recently Accessed) and is not this card's.
    • Done when: a record open makes one GET of the record and one security/explain per distinct question, with concurrent callers sharing the in-flight request.
    • Pin: a request count on record open, measured.
    • objectui#8014 (closed not_planned) was the general request budget. This is a measured duplication with a named site.

    Generated by Claude Code

  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    enhancementNew feature or request
    and removed on Oct 6, 2026
  3. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01FngvPpdrnhHMdHHq6vwwju
    Account: os-justin
    Branch: claude/issue-11699-record-open-shared-reads
    Worktree: objectui-issue-11699
    Domain: domain:ui
    Seat: domain:ui#2
    File surface:

    • packages/data-objectstack/src/index.ts:
      • findOne (about :3668 at 5a2a6ab) gains the in-flight sharing find already has (inflightFinds, about :3236 and :3498–:3560);
      • plus a sibling map beside inflightFinds.
    • packages/plugin-detail/src/useRecordEditable.ts: the explain probe (about :215–:236) shares an unanswered question's in-flight request, beside the finished-answer verdictCache.
    • The tests beside both files, and .changeset/11699-*.md.
    • ⛔ Not the two hosts (RecordDetailView.tsx, DetailView.tsx). RecordDetailView's delete confirmation is held by objectui#11695's claim. The fix lands at the two shared sources, so every caller shares the read. If the dev measures that a host's own duplicate call is the cause and sharing at the source cannot remove it, it stops and reports.

    Stop on breach; explain in the report.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier --repo objectstack-ai/objectui over the surface: no path-derived mandate; default tier)
    Clause-②: no
    Thread-read: 6011176803
    Ruling-ref: 6011176803 (triage: a record open makes one record GET and one security/explain per distinct question, with concurrent callers sharing the in-flight request; the sys_user_preference double-PATCH is objectui#11678's, not this card's), retrieved this round.
    Serial constraints cleared: none blocking.

    Read 2026-10-06T18:08Z.

    Why Clause-②: no:

    • Sharing an in-flight read changes no export, prop, type member or i18n key. findOne keeps its signature, and the sharing is the published adapter's internal behaviour, as find's already is.
    • If the dev measures that a new export is needed, it reports the built-declaration delta. The seat then amends this line and runs the at-tier contract review.

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 11699,
    "status": "done",
    "branch": "claude/issue-11699-record-open-shared-reads",
    "pr": "#11745",
    "session": "session_01FngvPpdrnhHMdHHq6vwwju — subagent, so the parent's id; it is the Claude-Session trailer on both commits",
    "premise_still_valid": true,
    "summary": "Measured first on a real stack: an objectstack showcase at 299a2c62 plus a production console build of 5a2a6ab, with Playwright counting requests over 4 full reloads of a showcase_task record page. Base sent GET /data/showcase_task/ID twice concurrently, and explain update and delete twice each concurrently, 43-44 API calls per reload. Instrumented caller stacks found 1 caller for the double GET: record:details' DetailView, whose load effect re-runs while its first read is pending because RecordDetailsRenderer rebuilds sections on every render. They found 2 hosts for each explain question: RecordDetailView, and DetailView asking 20-35 ms later. The fix shares the in-flight request at both sources. ObjectStackAdapter.findOne shares by resource+id+params through one private helper, shareInFlight, that find now uses too, with find's behaviour unchanged; a write through emitMutation drops that resource's pending findOne reads. useRecordEditable keeps a pending map beside verdictCache under the same key; a staled question leaves the map, and retainForPrincipal empties it. After the fix, measured the same way: GET ID once, each explain question once, 40 calls per reload on 4 of 4 runs. Neither host file was edited. One reading to confirm: the card's ruling scopes the ?populate= pair out. It is actually RecordDetailView's own $expand record read, sent again in sequence when objectDef changes identity, so a record open still reads the record 3 times as 2 different requests (see open_questions).",
    "tests": "All runs are through os-verify-lock, at the final source (HEAD d1d1e87; source unchanged since 1c5b67b). (1) pnpm exec vitest run --maxWorkers=2 packages/data-objectstack/ packages/plugin-detail/: Test Files 321 passed, 1 skipped (322); Tests 3470 passed, 8 skipped (3478). (2) pnpm exec vitest run --maxWorkers=3 packages/app-shell/src/views/RecordDetailView: Test Files 42 passed (42), Tests 257 passed (257). These are the host suites objectui#11713 broke, plus the new pin. (3) The 52 consumer suites outside the two packages (every test file naming useRecordEditable, __clearRecordEditableCache or security/explain, or constructing ObjectStackAdapter): Test Files 52 passed (52), Tests 445 passed (445). (4) type-check: pnpm --filter data-objectstack --filter plugin-detail run type-check, both Done; --listFiles counts the new test files 1 and 2. app-shell tsc -p tsconfig.test.json exit 0 at d1d1e87, and --listFiles includes the new pin. These ran after the closure build turbo run build --filter=@object-ui/app-shell^... (28/28). (5) lint: both packages report 0 errors, with touched-source warning counts equal to 5a2a6ab (index.ts 123, useRecordEditable.ts 1); app-shell is a declared narrowing to the one added file: eslint json 1 file, 0 errors, 0 warnings; config not type-aware. Ablation: 5 legs through objectstack scripts/ablation-replace.mjs in WRAP mode on committed HEAD 1c5b67b. Each anchor hit x1 and the blob changed; each restore was proven by blob == HEAD and an empty git diff HEAD. No build is needed: vitest.config.mts aliases data-objectstack, plugin-detail and react to src. Leg 1, findOne sharing removed: 5 red, including the integration pin 'expected length 1 but got 2'. Leg 2, the explain pending map removed: 3 red, with the integration pin receiving update:2, delete:2. Legs 3-5 (write drop, principal clear, staled removal): exactly the matching pin red, 1 each. Browser re-measure: before.json totals 44, 43, 43, 43; after.json totals 40, 40, 40, 40.",
    "gates": [
    {
    "cmd": "pnpm check:new-line-citations",
    "exit": 0,
    "head": "d1d1e87",
    "verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0"
    },
    {
    "cmd": "pnpm check:control-bytes",
    "exit": 0,
    "head": "d1d1e87"
    },
    {
    "cmd": "pnpm check:changeset-claims",
    "exit": 0,
    "head": "d1d1e87"
    },
    {
    "cmd": "pnpm check:pending-changeset-literals",
    "exit": 0,
    "head": "d1d1e87"
    },
    {
    "cmd": "node scripts/check-changeset-no-major.mjs",
    "exit": 0,
    "head": "d1d1e87"
    },
    {
    "cmd": "node scripts/check-changeset-presence.mjs",
    "exit": 0,
    "head": "d1d1e87",
    "verdict": "5 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)"
    },
    {
    "cmd": "node scripts/check-changeset-overwrite.mjs",
    "exit": 0,
    "head": "d1d1e87"
    },
    {
    "cmd": "pnpm check:vi-mock-specifiers",
    "exit": 0,
    "head": "d1d1e87"
    },
    {
    "cmd": "pnpm check:vi-mock-inherit",
    "exit": 0,
    "head": "d1d1e87"
    },
    {
    "cmd": "pnpm check:vi-mock-override-shape",
    "exit": 0,
    "head": "d1d1e87"
    },
    {
    "cmd": "pnpm check:test-path-roots",
    "exit": 0,
    "head": "d1d1e87"
    },
    {
    "cmd": "node scripts/check-governed-queue-guard.mjs --test (the 7 diff paths)",
    "exit": 0,
    "head": "d1d1e87",
    "verdict": "NOT GOVERNED"
    },
    {
    "cmd": "pnpm check:readme-exports",
    "exit": null,
    "head": "d1d1e87",
    "verdict": "NOT MEASURED, reason: needs every package built; it judges README import bindings only, and the README edit adds a prose bullet, no import"
    },
    {
    "cmd": "pnpm check:doc-snippets",
    "exit": null,
    "head": "d1d1e87",
    "verdict": "NOT MEASURED, reason: no code fence added"
    },
    {
    "cmd": "CI on PR #11745 head d1d1e87",
    "exit": null,
    "head": "d1d1e87",
    "verdict": "in_progress at report time: 42 check runs, 20 success, 3 skipped, 19 in progress, 0 failed"
    }
    ],
    "files_changed": [
    ".changeset/11699-record-open-shared-reads.md",
    "packages/data-objectstack/src/index.ts",
    "packages/data-objectstack/src/findOne.inflightShare-11699.test.ts",
    "packages/data-objectstack/README.md",
    "packages/plugin-detail/src/useRecordEditable.ts",
    "packages/plugin-detail/src/useRecordEditable.inflightShare-11699.test.tsx",
    "packages/app-shell/src/views/RecordDetailView.recordOpenRequests-11699.test.tsx"
    ],
    "line_budget": "n/a: no skills/** or governed ledger touched (governed-queue-guard: NOT GOVERNED). Diff 7 files, +841/-35, mostly the three new test files.",
    "deviations": [
    "File surface, declared in the PR body: (a) a new test-only file in app-shell beside the host, packages/app-shell/src/views/RecordDetailView.recordOpenRequests-11699.test.tsx, which is the order's integration pin; RecordDetailView.tsx itself is untouched. (b) One Features bullet in packages/data-objectstack/README.md, per AGENTS.md #2.",
    "Measurement used a temporary objectstack worktree, /home/user/objectstack-boot-11699 at origin/main 299a2c62. pnpm install there, and the showcase closure (60 tasks) and console builds ran under the lock. The worktree has been removed.",
    "The caller stacks came from temporary uncommitted instrumentation in this worktree's RecordDetailView.tsx, DetailView.tsx, useRecordEditable.ts and index.ts. It was reverted with git checkout HEAD before any commit; git diff HEAD was empty and the marker count was 0 in all four files.",
    "Gate clue path: the order names packages/app-shell/src/views/tests/RecordDetailView*. Those suites live at packages/app-shell/src/views/RecordDetailView.*.test.tsx, with no tests directory, and those are what ran.",
    "Zone 2 mechanism assumption 1 was falsified in detail: the double GET has ONE caller (DetailView re-running its effect), not two findOne callers. The remedy still holds. In assumption 2, the principal-change clear lives in the hook's retainForPrincipal; purgeSignedOutClientCaches is in @object-ui/auth and never reaches this module map.",
    "Commit trailers use objectui AGENTS.md's model-free pair (Co-authored-by: Claude, Claude-Session) rather than the harness reminder's model-named Co-Authored-By line, per AGENTS.md and os-dev.md.",
    "Labels: zero written, because the order names only --assign. The PR's 5 labels are from labeler.yml."
    ],
    "mcp_calls": "0",
    "api_writes": "3, each one fleet-relay dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed as objectstack-fleet[bot]: (1) pr_create POST /repos/objectstack-ai/objectui/pulls, draft, read back 11668 of 11668 bytes identical, PR #11745; (2) label-write --assign os-justin, POST /repos//issues/11745/assignees, read back MATCHES; (3) this os-dev-report comment, POST /repos//issues/11699/comments via post-stamped. git push x3 is not REST.",
    "open_questions": [
    {
    "question": "Does the Done-when's 'one GET of the record' cover RecordDetailView's sequential ?populate= re-read? The ruling calls it a list, but it is the page's own $expand record read, sent again after the first settles when objectDef changes identity. Sharing in flight cannot absorb it, so with this PR a record open still reads the record 3 times as 2 different requests.",
    "options": [
    "A: keep 'Fixes #11699' as the ruling scopes it, and file the host re-read separately (out_of_scope_findings item 1)",
    "B: the seat amends the PR's first line to 'Part of #11699' and the card stays open for the host re-read, which needs RecordDetailView.tsx, held by objectui#11695's claim"
    ],
    "recommendation": "A, because the ruling explicitly scopes the ?populate= pair out of the Done-when. The remaining re-read is a different mechanism (a sequential effect re-run on an identity dependency, in a host file) that no in-flight sharing at the source can reach. It sits in a file another claim holds."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: public door, the console record page. A full reload of a showcase_task record (Playwright request count, 4 of 4 runs on both base 5a2a6ab and this branch) sends GET /api/v1/data/showcase_task?populate=...&top=1&filter=id twice in sequence; one read is expected. Evidence: instrumented build marks for RecordDetailView's record-load effect show every dependency equal between the two runs except objectDef (a new identity), so this is the AGENTS.md #10 identity-dependency class. The re-read also re-requests the same $expand list. Dedupe words: record page populate read twice, RecordDetailView objectDef identity re-read, duplicate expand record GET",
    "class: a · reach: same door, every console reload measured (4 of 4 runs, both builds): GET /api/v1/runtime/config and GET /api/v1/auth/get-session each go out twice in sequence (runtime/config at about 20 ms and again at about 650 ms). Callers were not measured. runtime/config was one of objectui#5544's boot duplicates (closed completed), so this may be a regression of that fix. Dedupe words: runtime config fetched twice on boot, get-session requested twice, objectui#5544 boot duplicate, console boot duplicate requests",
    "carrier: 承接者:无 · noted in the PR's Acceptance notes, not filed. RecordDetailsRenderer rebuilds sections on every render, so DetailView's load effect re-runs on unrelated re-renders (the root of the double GET this PR absorbs). DetailView in record:details reads the record itself, in parallel, while the page's ctx.data is still loading. find has no write drop (inference, not reproduced)."
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 11699,
    "status": "done",
    "branch": "claude/issue-11699-record-open-shared-reads",
    "pr": "#11745",
    "session": "session_01FngvPpdrnhHMdHHq6vwwju — subagent, so the parent's id; it is the Claude-Session trailer on all four commits",
    "round": "patch round 1 (order patch-11699.md): ruling B, the PR lands as Part of #11699",
    "premise_still_valid": true,
    "summary": "Patch round 1, with no code change. I re-created the worktree on the branch at d1d1e87, which equals the remote tip. I merged main at d39ac2e as a merge commit, eff9343, with no rebase and no force; main came in through a ref I named, refs/os-11699/main, never FETCH_HEAD. Then I corrected the changeset's prose and left the frontmatter unchanged (commit 1078df3, pushed). The first sentence now says a record open no longer sends the same record read twice at once, and asks each explain question once, with concurrent callers sharing the request. One sentence now names the record page's own $expand read, sent again when the object definition changes identity, as not changed here. Two sentences were tightened to hold against the diff: the write drop applies to pending findOne reads only, and no pending question is shared across a principal change. pr_body_patch and pr_title below carry the replacement PR text, which I did not write myself. Its first line is Part of #11699 and its next line says what stays open. Acceptance note 1, the two measurement sentences that cited the old scope reading, and the gates section are rewritten to match.",
    "tests": "No source changed this round, so the earlier test runs and the 5 ablation legs (all run at d1d1e87 and its source) stand. Blob equality, d1d1e87 vs 1078df3: index.ts 1111bb549be0, findOne.inflightShare-11699.test.ts 0213b6729ab9, data-objectstack README.md 77db26497224, useRecordEditable.ts ee7937186695, useRecordEditable.inflightShare-11699.test.tsx edb377e7b5c7, RecordDetailView.recordOpenRequests-11699.test.tsx ac620190d1e5. All six are EQUAL, with no empty hash. The only file that changed d1d1e87..HEAD and that main did not change (5a2a6ab..d39ac2e) is .changeset/11699-record-open-shared-reads.md. Every file main touched is identical between HEAD and d39ac2e. The merge brought in app-shell (marketplace, organizations, runtime-config.ts), i18n, plugin-chatbot and apps/console. A grep of that app-shell diff for useRecordEditable, findOne, RecordDetailView or DetailView finds 0 hits. CI re-runs the full farm on the new head.",
    "gates": [
    {
    "cmd": "pnpm check:changeset-claims",
    "exit": 0,
    "head": "1078df3"
    },
    {
    "cmd": "pnpm check:pending-changeset-literals",
    "exit": 0,
    "head": "1078df3"
    },
    {
    "cmd": "node scripts/check-changeset-no-major.mjs",
    "exit": 0,
    "head": "1078df3"
    },
    {
    "cmd": "node scripts/check-changeset-presence.mjs",
    "exit": 0,
    "head": "1078df3",
    "verdict": "Compared with d39ac2e (merge-base): 5 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)"
    },
    {
    "cmd": "node scripts/check-changeset-overwrite.mjs",
    "exit": 0,
    "head": "1078df3",
    "verdict": "1 changeset(s) added, 0 modified, 0 deleted"
    },
    {
    "cmd": "pnpm check:control-bytes",
    "exit": 0,
    "head": "1078df3"
    },
    {
    "cmd": "pnpm check:new-line-citations",
    "exit": 0,
    "head": "1078df3",
    "verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0"
    }
    ],
    "files_changed": [
    ".changeset/11699-record-open-shared-reads.md"
    ],
    "files_changed_pr_total": [
    ".changeset/11699-record-open-shared-reads.md",
    "packages/data-objectstack/src/index.ts",
    "packages/data-objectstack/src/findOne.inflightShare-11699.test.ts",
    "packages/data-objectstack/README.md",
    "packages/plugin-detail/src/useRecordEditable.ts",
    "packages/plugin-detail/src/useRecordEditable.inflightShare-11699.test.tsx",
    "packages/app-shell/src/views/RecordDetailView.recordOpenRequests-11699.test.tsx"
    ],
    "line_budget": "n/a: no skills/** or governed ledger touched. This round changed only the changeset, 4 lines rewritten, frontmatter byte-identical.",
    "deviations": [
    "Beyond the two sentence changes the order named, two existing changeset sentences were tightened because they did not hold against the diff. 'drops the pending reads' became 'drops the pending findOne reads' (find has no write drop). 'drops every pending question' became 'No pending question is shared across a change of the signed-in principal' (the pending answer still reaches askers that already joined; it just cannot be joined across the change).",
    "pr_body_patch also rewrites two measurement sentences and the gates section, besides line 1, line 2 and acceptance note 1, because they cited the old scope reading or the old head. Every other byte of the stored body is unchanged.",
    "The merge commit eff9343 carries the model-free trailer pair, per objectui AGENTS.md."
    ],
    "mcp_calls": "0",
    "api_writes": "1 this round: this os-dev-report comment, POST /repos//issues/11699/comments via post-stamped over the fleet relay (POST /repos/objectstack-ai/objectstack/dispatches). git push x1 (d1d1e87..1078df3) is not REST. The PR was not PATCHed.",
    "pr_title": "fix(data-objectstack,plugin-detail): concurrent record reads and explain questions share one request (objectui#11699)",
    "pr_body_patch": "Part of #11699\n\nStays open on the card: RecordDetailView's sequential ?populate= re-read of the record when objectDef changes identity. Removing it needs RecordDetailView.tsx, which is held by objectui#11695's claim.\n\nClause-②: no\n\n## What this does\n\nOpening a record page sent GET /api/v1/data/OBJ/ID twice at once and POST /api/v1/security/explain four times. Measured, both duplications come from concurrent callers of one shared source. So the in-flight request is shared at those two sources, and neither host page is edited.\n\n- @object-ui/data-objectstack: ObjectStackAdapter.findOne shares an in-flight read keyed by resource + id + params. find's sharing code moved into one private helper, shareInFlight, that both methods now call; find's behaviour is unchanged (its own pin is in the new test file). The entry is dropped when the read settles, so this is not a response cache. A rejection reaches every caller that shared it and is not remembered. A write through the adapter (emitMutation, which every create / update / delete / bulk / batch write goes through) drops the pending findOne reads of the resource it wrote, so a read asked after a save is never answered by one sent before it.\n- @object-ui/plugin-detail: useRecordEditable shares an unanswered question. A pending map sits beside verdictCache, keyed the same way (principal, object, record, operation). A question whose record changes while it is pending is removed from the map, so the re-ask is a fresh request. A principal change empties the map, in retainForPrincipal, where verdictCache is cleared.\n\n## Measurement: who sent the duplicates\n\nEnvironment: objectstack main at 299a2c62, the showcase app (objectstack dev --seed-admin --fresh, own port, temporary SQLite). The console is a production vite build of this branch, served by vite preview with /api proxied to that backend. Chromium headless 1440x900, seeded admin, full reload of a showcase_task record page. Requests were counted with Playwright request / requestfinished events, 4 runs per build.\n\n| per reload | base 5a2a6ab | this branch (1c5b67b source) |\n|---|---|---|\n| GET /api/v1/data/showcase_task/ID | 2, concurrent | 1 |\n| POST /api/v1/security/explain, update | 2, concurrent | 1 |\n| POST /api/v1/security/explain, delete | 2, concurrent | 1 |\n| GET /api/v1/data/showcase_task?populate=…&top=1&filter=… | 2, sequential | 2, sequential (see below) |\n| all API requests | 44, 43, 43, 43 | 40, 40, 40, 40 |\n\nThe callers were named from one instrumented, unminified build. The instrumentation was reverted before anything was committed (git diff HEAD empty, marker count 0 in all four files).\n\n- Both GET .../ID come from DetailView's load effect inside record:details, from one DetailView instance. The effect re-runs because RecordDetailsRenderer builds a new sections array on every render, and the re-run happens while the first read is still on the wire. That read has no $expand, because the highlights strip's fields are de-duplicated out of the body. Sharing at findOne absorbs it.\n- The explain requests are 2 distinct questions, each asked twice. RecordDetailView asks update and delete. record:details' DetailView asks the same two about 20 to 35 ms later, while the first answers are still pending. The principal was the same for all four, because permissions had loaded before the page mounted. Sharing in useRecordEditable absorbs it.\n- The ?populate= pair is RecordDetailView's own record read, findOne with $expand, not a list. It is sent again after the first one settled, because objectDef arrives as a new object. Requests that are sequential cannot be shared in flight, so this pair is what keeps the card open. See acceptance note 1.\n\n### The PM's mechanism assumptions, as measured\n\n1. The record read: "two callers of findOne" is falsified in detail. It is ONE caller, DetailView, whose effect runs again while its first read is pending. The predicted remedy holds: the calls are concurrent on one adapter with the same key, and sharing at findOne absorbs them.\n2. The explain probe: confirmed, 2 questions asked by 2 hosts. The clearing on a principal change lives in the hook's own retainForPrincipal. purgeSignedOutClientCaches is in @object-ui/auth and does not reach this module-scope map.\n3. Hosts stay out: every in-scope duplicate is concurrent and goes through the same adapter or the same module, so neither host is touched. The only duplicate that sharing cannot absorb is the sequential ?populate= re-read. It is a host's own sequential call, and it stays open on the card (acceptance note 1).\n4. No new import enters the record page's module graph: neither source file gains an import.\n\n## Tests\n\n- packages/data-objectstack/src/findOne.inflightShare-11699.test.ts (new, 7 tests). This test runs the real adapter and the real client over a transport that holds each request until the test answers it. It covers: one GET for two concurrent calls; a new GET after settle; a different id, resource or params is not merged; a rejection is the same error for both callers (httpStatus 500, code INTERNAL_ERROR) and the next call asks again; a write to the resource makes the next read ask again; a write to another resource does not; find still shares.\n- packages/plugin-detail/src/useRecordEditable.inflightShare-11699.test.tsx (new, 5 tests). The explain endpoint holds each request. It covers: two mounts send one POST and both show the verdict; distinct questions are not merged; a fail-open answer is shared but not remembered; a data change while pending makes the re-ask a fresh request; a principal change empties the map, so an unknown-principal ask after it does not join one from before.\n- packages/app-shell/src/views/RecordDetailView.recordOpenRequests-11699.test.tsx (new, the integration pin). It mounts the real RecordDetailView with a showcase-shaped record page over the real adapter and the real hook. It releases held requests in the order the measurement saw, then counts: record GET .../ID is 1, and each of the 2 distinct explain questions is asked once. This is a test-only addition to app-shell, beside the host and outside the claim's file surface. RecordDetailView.tsx itself is untouched.\n\nRuns (from the worktree root, through the shared verify lock):\n\n- pnpm exec vitest run --maxWorkers=2 packages/data-objectstack/ packages/plugin-detail/: Test Files 321 passed | 1 skipped (322), Tests 3470 passed | 8 skipped (3478).\n- pnpm exec vitest run --maxWorkers=3 packages/app-shell/src/views/RecordDetailView: Test Files 42 passed (42), Tests 257 passed (257). These are the host suites objectui#11713 once broke, plus the new pin.\n- The consumer suites of both sources (every test file outside the two packages that names useRecordEditable, __clearRecordEditableCache or security/explain, or constructs ObjectStackAdapter): Test Files 52 passed (52), Tests 445 passed (445).\n- pnpm --filter @object-ui/data-objectstack --filter @object-ui/plugin-detail run type-check: both Done. --listFiles shows both new test files in those programs. packages/app-shell: tsc -p tsconfig.test.json exit 0 at d1d1e87, and --listFiles includes the new pin. These ran after the closure build turbo run build --filter=\"@object-ui/app-shell^...\" (28/28 tasks).\n- Lint: pnpm --filter @object-ui/data-objectstack --filter @object-ui/plugin-detail run lint reports 0 errors. Each touched source has the same warning count as at 5a2a6ab (index.ts 123, useRecordEditable.ts 1), and both new test files have 0. For app-shell, eslint --format json on the one added file reports 1 file, 0 errors, 0 warnings. That is a declared narrowing: no other app-shell file changed, and eslint.config.js has no type-aware parser options (projectService / parserOptions.project).\n\n## Ablations\n\nEach ablation mutated committed HEAD (1c5b67b) through scripts/ablation-replace.mjs in WRAP mode. The anchor hit exactly once, the blob changed, and the restore was proven by blob == HEAD and an empty git diff HEAD. Vitest resolves all three packages to src through vitest.config.mts aliases, so no dist was involved.\n\n1. findOne's sharing removed (return this.shareInFlight(this.inflightFindOnes, …) replaced by a direct readOne): 5 red. These are the four findOne share pins, plus the integration pin with expected … to have a length of 1 but got 2, which is the measured double GET.\n2. The explain in-flight map removed (if (pending) return pending.answer; replaced): 3 red. The integration pin received update: 2, delete: 2, which is the measured four explains.\n3. The write drop removed: only "a write to the resource … makes the next read ask again" is red.\n4. The map clear on a principal change removed: only the principal-change pin is red.\n5. The removal of a staled question from the map undone: only the data-change pin is red.\n\n## Gates (exit codes captured before any pipe)\n\nAt 1078df3 (after merging main at d39ac2e and correcting the changeset prose): check:changeset-claims 0 · check:pending-changeset-literals 0 · check-changeset-no-major 0 · check-changeset-presence 0 (5 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)) · check-changeset-overwrite 0 · check:control-bytes 0 · check:new-line-citations 0 (VERDICT … 0 new citation(s)).\n\nAt d1d1e87, and still standing: check:vi-mock-specifiers 0 · check:vi-mock-inherit 0 · check:vi-mock-override-shape 0 · check:test-path-roots 0 · check-governed-queue-guard.mjs --test over the 7 paths: NOT GOVERNED, exit 0. The tests and ablations above were run at d1d1e87 and its source, and they stand at 1078df3. All six non-changeset files of this change have the same blob at both heads. The only file that differs outside the merge is the changeset. Every file the merge brought in equals d39ac2e, and none of them names the record page, findOne or useRecordEditable.\n\nNOT MEASURED: check:readme-exports, reason: it needs every package built, and it judges only the import bindings a README writes, while the README edit here adds a prose bullet and no import. check:doc-snippets, reason: no code fence was added. The repo-wide pnpm lint and the full pnpm test farm are left to CI.\n\n## Declaration delta\n\nObjectStackAdapter in dist/index.d.ts gains three private member names (inflightFindOnes, shareInFlight, readOne) and a JSDoc paragraph on findOne. No export, public member, prop, type member or i18n key changes. useRecordEditable's new code is module-private. The changeset declares patch for both packages (.changeset/11699-record-open-shared-reads.md). Its prose claims only what was measured: the same record read is no longer sent twice at once, and the page's own $expand re-read is named as unchanged.\n\n## Acceptance notes\n\n- The record is still read 3 times per open, as 2 different requests, so this PR is Part of #11699. DetailView reads GET .../ID once (shared now). RecordDetailView reads ?populate= twice in sequence, re-reading when objectDef changes identity. That pair is the page's own $expand record read, not a list, so the card's Done-when (one GET of the record per open) still covers it. The second read is sent after the first settles, so no in-flight sharing can absorb it. Removing it means an edit to RecordDetailView.tsx, which waits behind objectui#11695's claim.\n- DetailView inside record:details reads the record itself while the page's record (ctx.data) is still loading, in parallel to the page's own read. This is a host-coordination question, not a sharing one, because the two requests carry different params.\n- RecordDetailsRenderer builds a new sections array on every render, so DetailView's load effect re-runs on unrelated re-renders. That is the root of the double GET, now absorbed by sharing. The schema read it repeats is served by the metadata cache.\n- find has no write drop. The same staleness window findOne now closes is still open for lists. This is an inference, not reproduced.\n- Measured and left alone, because they are not record reads: GET /api/v1/runtime/config and GET /api/v1/auth/get-session each go out twice, in sequence, on every reload (4 of 4 runs, both builds).\n\n---\n\n_Generated by Claude Code_\n",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted in the PR's Acceptance notes, not filed. RecordDetailsRenderer rebuilds sections on every render, so DetailView's load effect re-runs on unrelated re-renders (the root of the double GET this PR absorbs). DetailView in record:details reads the record itself, in parallel, while the page's ctx.data is still loading. find has no write drop (inference, not reproduced). Per the seat: the runtime/config and get-session finding names no producer and is not filed, and the populate re-read is now the card's open half rather than a separate finding."
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT (patch round 1): PR objectui#11745 at 1078df3 (Part of #11699). No contract review is owed; it lands when CI is green, and this card stays open

    domain:ui execution seat 2 · session_01FngvPpdrnhHMdHHq6vwwju (os-justin) · 2026-10-06T19:23Z. Dev reports: round 0 and patch round 1 6023747033 on this card. Claim 6022465555.

    • Shape:

      • The PR is a draft on base main. It merged main at d39ac2e as eff9343 (no rebase, no force-push), then took 1078df3, which is prose only.
      • It has 7 files, +841/−35. git merge-tree against main is clean. NOT GOVERNED. The assignee is os-justin.
      • In this stroke the seat rewrote the PR's first line as Part of #11699, with what stays open on the next line, and its title, from the dev's pr_body_patch and pr_title.
    • Surface: inside the claim, plus two additions the seat accepts. One is a test-only integration pin beside the host, app-shell/src/views/RecordDetailView.recordOpenRequests-11699.test.tsx; RecordDetailView.tsx itself is untouched. The other is one Features bullet in the data-objectstack README, per AGENTS.md.

    • Measured on a real stack. The dev ran an objectstack showcase at 299a2c62 against a production console build, with Playwright counting 4 full reloads of a showcase_task record page.

      per reload base 5a2a6ab this PR
      GET …/showcase_task/ID 2, concurrent 1
      explain update / delete 2 + 2, concurrent 1 + 1
      GET …?populate=…&top=1&filter=id 2, sequential 2, sequential
      all API requests 44, 43, 43, 43 40, 40, 40, 40

      The callers were named from an instrumented build, and the instrumentation was reverted before any commit.

      • The double GET comes from one caller: record:details' DetailView. Its load effect re-runs while the first read is pending, because RecordDetailsRenderer rebuilds sections on every render.
      • Each explain question is asked by two hosts, RecordDetailView and then DetailView 20 to 35 ms later.
    • Seat's own reading of the diff:

      • ObjectStackAdapter.findOne shares an in-flight read, keyed by resource, JSON.stringify(String(id)) and stableStringify(params).
        • find's code moved into one private helper, shareInFlight, and find's behaviour is unchanged: same key, same .then(cleanup, cleanup), and an entry is cleared only if it is still the same promise.
        • emitMutation drops a resource's pending findOne reads before the listeners run, so a refetch after a write never joins a read asked before it. The resource:: prefix cannot collide with a longer resource name.
      • useRecordEditable keeps pendingVerdicts beside verdictCache, under the same key.
        • The probe body moved into askVerdict unchanged: same fail-open on non-OK, no verdict or throw, and a staled answer is never cached.
        • A staled probe leaves the map, and retainForPrincipal empties it on a principal change.
        • The test seam clears it.
      • Neither source file gains an import, so the record page's module graph is unchanged. That is objectui#11713's lesson.
    • The dev's open question, answered by the seat: B.

      • The governing text is triage 6011176803's Done-when: "a record open makes one GET of the record". The ?populate= pair is RecordDetailView's own $expand read of the record. The seat's order had wrongly set it aside as a list read.
      • That pair is sequential, re-sent when objectDef changes identity, so in-flight sharing cannot absorb it.
      • So this PR is Part of #11699, and the card stays open for that re-read. The re-read needs RecordDetailView.tsx, which waits behind objectui#11695's claim.
    • Clause-②: no, confirmed. dist/index.d.ts gains only three private member names on ObjectStackAdapter and a JSDoc paragraph. No export, public member, prop, type member or i18n key changes.

    • Changeset (@object-ui/data-objectstack + @object-ui/plugin-detail: patch), corrected this round and checked sentence by sentence:

      • the first sentence now claims only what was measured;
      • one sentence names the page's own $expand re-read as unchanged;
      • the write drop is scoped to findOne reads, matching the diff.

      patch fits.

    • Tests and gates (dev, measured):

      • data-objectstack + plugin-detail: 321 files, 3470 tests.
      • The 42 RecordDetailView host suites, 257 tests, including the new pin.
      • The 52 consumer suites of both sources, 445 tests.
      • Type-check and lint: 0 errors, with warning counts equal to base.
      • Five ablation legs, each red exactly where predicted. The integration pin read expected length 1 but got 2 and update: 2, delete: 2. Each was restored by blob.
      • Round 1 changed only the changeset. All six other files have blobs identical to d1d1e87, so those runs stand. The changeset gates were re-run at 1078df3 and all exit 0.
    • Out-of-scope findings, Acceptance notes, not filed:

      • DetailView in record:details reads the record itself, in parallel, while the page's record is loading.
      • RecordDetailsRenderer rebuilds sections on every render.
      • find has no write drop. This is an inference.
      • runtime/config and get-session each go out twice per reload, in sequence. No producer is named, and the card body already records it.
    • CI on 1078df3: in progress at this read (26 success, 3 skipped, 13 running, 0 failed). The red Test aggregate on d1d1e87 was its shards cancelled by this round's push.

    • After landing: pm:dispatched comes off, and the card returns to pm:queue with its remainder named. The remainder is serial behind objectui#11695 on RecordDetailView.tsx.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR objectui#11745 → main 4590363, verified by content. Part of: the card stays open for the record page's sequential re-read, and returns to pm:queue

    domain:ui execution seat 2 · session_01FngvPpdrnhHMdHHq6vwwju (os-justin) · 2026-10-06T19:55Z.

    Release: claim 6022465555 (session_01FngvPpdrnhHMdHHq6vwwju, os-justin) · cause: the concurrent half landed (PR objectui#11745), and the remaining half needs RecordDetailView.tsx, which objectui#11695's claim holds · destination: pm:queue. The ruling 6011176803 still governs, and no new question is raised, so no pm:retriage.

    • The merge.

      • Squash 4590363 went through the merge queue. It has one parent, 17acfbb, and is an ancestor of origin/main.
      • 7 files, +841/−35.
      • The landed tree equals git merge-tree of the parent and the reviewed head 1078df3 (tree 36fe020a… on both).
      • The changed lines are identical (md5 be7294db… on both).
    • No contract review was owed: Clause-②: no (ACCEPT 6023830357).

    • Content check against the first parent:

      reading 17acfbb 4590363
      inflightFindOnes map in data-objectstack/src/index.ts 0 1
      private shareInFlight (shared by find and findOne) 0 1
      pendingVerdicts map in plugin-detail/src/useRecordEditable.ts 0 1
      RecordDetailView.recordOpenRequests-11699.test.tsx present 0 1
    • What landed, as measured (showcase plus a production console, Playwright, 4 reloads):

      • the record GET …/ID goes from 2 concurrent to 1;
      • each explain question goes from 2 concurrent to 1;
      • API requests per reload go from 43–44 to 40.
    • What stays open on this card, for its next claim. The ruling's Done-when is "a record open makes one GET of the record". RecordDetailView's own $expand read of the record (GET …?populate=…&top=1&filter=id) is still sent twice in sequence. The second read goes out after the first settles, because the record-load effect depends on objectDef, which arrives as a new object. That is the AGENTS.md identity-dependency class, and in-flight sharing cannot absorb it.

      • The fix is a host edit in packages/app-shell/src/views/RecordDetailView.tsx, which objectui#11695's claim holds. So the next claim waits for that claim's PR to land or to show a file list that leaves the record-load effect alone.
      • The pin to add is: a record open counts one ?populate= read. The landed pin, RecordDetailView.recordOpenRequests-11699.test.tsx, is the place for it.
    • Closures. Part of closed nothing, by design. No issue closed in the landing window. In this stroke pm:dispatched comes off, pm:queue goes on, and the assignee is released.

    • Recorded, not filed:

      • record:details' DetailView reads the record itself, in parallel, while the page's record is loading.
      • RecordDetailsRenderer rebuilds sections on every render.
      • find has no write drop. This is an inference.
      • runtime/config and get-session each go out twice per reload, in sequence. This card's body already records it.

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Deferred (serial), not claimed

    domain:ui seat 1 · session_01DrKzdPdyLLBW3qpZ4vtk7z · read 2026-10-06T20:44Z. ⛔ Not a claim.

    The remaining half (seat 2's release 6024315863) is the record-load effect in packages/app-shell/src/views/RecordDetailView.tsx. objectui#11695 has closed, but objectui#11733 is now in flight (seat 3, claim 6024040554) and also edits RecordDetailView.tsx (the related-list title). Both cards are area:records, so this card waits for objectui#11733's PR to land or to show a file list that leaves the record-load effect alone.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01CGZy1BGCjdN5cXqL9cnvB8
    Account: os-support-ai
    Branch: claude/issue-11699-record-load-once
    Worktree: objectui-issue-11699
    Domain: domain:ui
    Seat: domain:ui#3
    File surface: packages/app-shell/src/views/RecordDetailView.tsx: the page-record load effect (about :537–:621 on 9fc68aa) and its dependency list only. Also the landed pin RecordDetailView.recordOpenRequests-11699.test.tsx and the tests beside it, and .changeset/11699-*.md. The PRODUCER of the new objectDef object is in scope only if measurement names it (AGENTS.md rule #10: "Both sides of the seam, or neither holds"); the dev reports its file before opening the PR, and the seat amends this line (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; default tier)
    Clause-②: no
    Responsibility: n/a — not a defect card
    Thread-read: 6025113834
    Serial constraints cleared: none blocking. objectui#11733 (PR objectui#11746, the related-list title in RecordDetailView.tsx) merged as 885a96f at 2026-10-06T21:24Z, which frees this seat's serial slot (seat post §3). Seat 1's deferral 6025113834 named the same constraint. No open objectui PR touches RecordDetailView.tsx (read 2026-10-06T21:29Z; open: objectui#11747 on packages/fields/src/index.tsx, #11742 a dependency bump, #11729, #11600, #11069). Same axis in flight: objectui#11683 (area:records, PR objectui#11747 in the merge queue) is on packages/fields/src/index.tsx; the file surfaces are disjoint.

    Why Clause-②: no: an effect stops re-reading a record it already holds. No accepted input, export or prop changes.

    This claim takes the half seat 2 released in 6024315863. The ruling's Done-when (6011176803) is "a record open makes one GET of the record". The concurrent half landed in PR objectui#11745 (4590363). What remains is RecordDetailView's own ?populate= read, which is still sent twice in sequence.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim amendment — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-06T21:50Z. Amends claim 6025812088 (file surface only).

    File surface, added: packages/app-shell/src/providers/MetadataProvider.tsx, the composed objects list of the context value (the get objects() getter, about :1072–:1077 on 055d350) only.

    Why: the dev measured the producer before editing it, as the claim required. On 4 of 4 showcase reloads, the record-load effect's second run changed only objectDef. The new object was JSON-equal to the old one and came out of the getter at the same context version. The getter rebuilds the list (mergeViewsIntoObjects + attachInlineSubforms) on every read. objectui AGENTS.md rule #10 says "Both sides of the seam, or neither holds", so the producer side is in scope.

    Serial check for the added file, read 2026-10-06T21:50Z: no open objectui PR touches MetadataProvider.tsx. No pm:dispatched claim names it (objectui#11697 and objectui#11694 are on other files). No commit on main touched it today.

    Unchanged: Clause-②: no. The two exported helpers keep their signatures, and no export, prop or accepted input changes. If that stops being true, the dev reports it before opening the PR.


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 11699,
    "status": "done",
    "branch": "claude/issue-11699-record-load-once",
    "pr": "#11750",
    "session": "session_01CGZy1BGCjdN5cXqL9cnvB8 — subagent, so the parent's id; it is the Claude-Session trailer on all four commits",
    "premise_still_valid": true,
    "summary": "The record page's own $expand read (GET /data/OBJ?populate=...) went out twice in sequence on open; it now goes out once, so together with PR objectui#11745 a record open reads the record once per request kind, and the PR's first line closes the card. Measured first on a real stack: objectstack showcase at 60ccda5a, a production console build of objectui 055d350 with temporary instrumentation, 4 full reloads. The effect's second run changed ONLY objectDef: perms, effectivePage, the record id, dataSource and the nonce were the same objects, and objectDef.fields was the same object. The new definition was JSON-equal and came out of MetadataProvider's objects getter at the same context version, which re-wrapped stored definitions on every read. Both sides fixed, as the coordinator accepted (claim amendment 6026121402). Consumer: the effect keys on hasPage and the FLS-gated expand list as a JSON string. Producer: the composed objects list is cached in a module-local WeakMap keyed on the two stored arrays it is built from. Before keying on identity, every write path was shown to replace those arrays, never mutate them. After, the same measurement: populate read 2 to 1 on 4 of 4 reloads, GET .../ID 1 to 1, API requests per reload 40 to 39; both builds render the record. Dormant here: a rule #10 hazard in the same provider (the preview-mode effect depends on the bump callback and clears the cache on re-run). Not changed here (outside the amended surface); recorded in the PR's Acceptance notes.",
    "tests": "All at be32ead (final HEAD) unless named. (1) pnpm exec vitest run --maxWorkers=2 packages/app-shell/: Test Files 1052 passed | 1 skipped (1053), Tests 10301 passed | 9 skipped (10310), exit 0. (2) The apps/console test files reading the metadata provider or useMetadata (consumers of the producer change): Test Files 17 passed (17), Tests 184 passed (184), exit 0. (3) The two pins + the unchanged 7230 pin: Test Files 3 passed (3), Tests 18 passed (18) at 6892ddb; the two later commits are type-only and the changeset, and run (1) covers all three files at be32ead. (4) pnpm --filter @object-ui/app-shell type-check: exit 0, 0 error TS, after turbo run build --filter=@object-ui/app-shell^... (28 successful, 28 total); tsc -p tsconfig.test.json --listFiles includes both new test files (2 of 2). (5) eslint --format json on the 4 touched files: 4 files, 0 errors; source warning counts equal 055d350 (RecordDetailView.tsx 110, MetadataProvider.tsx 52), test files 0. That is a declared narrowing: eslint.config.js has no projectService / parserOptions.project, so the diff cannot move an untouched file's verdict. Ablations: 3 legs, each through objectstack scripts/ablation-replace.mjs in WRAP mode on committed be32ead. Each anchor hit x1, the blob changed, and the restore was proven by blob == HEAD and an empty git diff HEAD. The pins import the mutated files by relative path, so no dist is involved. Directions were predicted before the runs, and all three matched. Leg C (old dependency list): 4 red, exactly the four identity cases ('to have a length of 1 but got 2' / 'but got 3'); 9 green, including both live controls and all 6 tests of the 7230 pin. Leg P (old getter, no cache): 3 red (same-version, other type landing, discard), 2 green. Leg M (cache held on the context value object, i.e. a React-memo cache): 2 red (other type landing, discard), 3 green; so the discard leg can fail for its own reason. Real stack: base.json 40/40/40/40 total, populate 2 per reload; after.json 39/39/39/39, populate 1; populate URL byte-identical; h1 'Content backlog' on both builds.",
    "gates": [
    {
    "cmd": "pnpm check:changeset-claims",
    "exit": 0,
    "head": "be32ead",
    "verdict": "No pending changeset names a file this change touches."
    },
    {
    "cmd": "pnpm check:pending-changeset-literals",
    "exit": 0,
    "head": "be32ead"
    },
    {
    "cmd": "node scripts/check-changeset-no-major.mjs",
    "exit": 0,
    "head": "be32ead",
    "verdict": "No changeset declares a major bump."
    },
    {
    "cmd": "node scripts/check-changeset-presence.mjs",
    "exit": 0,
    "head": "be32ead",
    "verdict": "4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/11699-record-load-once.md."
    },
    {
    "cmd": "node scripts/check-changeset-overwrite.mjs",
    "exit": 0,
    "head": "be32ead",
    "verdict": "1 changeset(s) added, 0 modified, 0 deleted."
    },
    {
    "cmd": "pnpm check:control-bytes",
    "exit": 0,
    "head": "be32ead",
    "verdict": "check-control-bytes: OK"
    },
    {
    "cmd": "pnpm check:test-path-roots",
    "exit": 0,
    "head": "be32ead",
    "verdict": "check-test-path-roots: OK"
    },
    {
    "cmd": "pnpm check:new-line-citations",
    "exit": 0,
    "head": "be32ead",
    "verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only → exit 0"
    },
    {
    "cmd": "pnpm check:vi-mock-specifiers",
    "exit": 0,
    "head": "be32ead",
    "verdict": "check-vi-mock-specifiers: OK"
    },
    {
    "cmd": "pnpm check:vi-mock-inherit",
    "exit": 0,
    "head": "be32ead",
    "verdict": "check-vi-mock-inherit: OK"
    },
    {
    "cmd": "pnpm check:vi-mock-override-shape",
    "exit": 0,
    "head": "be32ead",
    "verdict": "check-vi-mock-override-shape: OK"
    },
    {
    "cmd": "node scripts/check-governed-queue-guard.mjs --test (the 5 diff paths)",
    "exit": 0,
    "head": "be32ead",
    "verdict": "NOT GOVERNED — 5 path(s) checked against 5 governed surface(s); none matched."
    },
    {
    "cmd": "pnpm --filter @object-ui/app-shell type-check",
    "exit": 0,
    "head": "be32ead"
    },
    {
    "cmd": "pnpm exec eslint --format json (4 touched files)",
    "exit": 0,
    "head": "c9324d2",
    "verdict": "4 files, 0 errors; warnings equal base on both sources"
    },
    {
    "cmd": "pnpm exec vitest run --maxWorkers=2 packages/app-shell/",
    "exit": 0,
    "head": "be32ead",
    "verdict": "Test Files 1052 passed | 1 skipped (1053)"
    },
    {
    "cmd": "pnpm check:readme-exports",
    "exit": null,
    "head": "be32ead",
    "verdict": "NOT MEASURED, reason: no export changes and no README changes"
    },
    {
    "cmd": "i18n gates",
    "exit": null,
    "head": "be32ead",
    "verdict": "NOT MEASURED, reason: no locale pack changes"
    },
    {
    "cmd": "CI on PR #11750 head be32ead",
    "exit": null,
    "head": "be32ead",
    "verdict": "in_progress at 2026-10-06T23:13:40Z: 42 check runs, 21 success, 3 skipped, 18 in progress, 0 failed"
    }
    ],
    "files_changed": [
    ".changeset/11699-record-load-once.md",
    "packages/app-shell/src/providers/MetadataProvider.tsx",
    "packages/app-shell/src/providers/tests/MetadataProvider.composedObjectsIdentity-11699.test.tsx",
    "packages/app-shell/src/views/RecordDetailView.recordOpenRequests-11699.test.tsx",
    "packages/app-shell/src/views/RecordDetailView.tsx"
    ],
    "line_budget": "n/a: no skills/** surface; NOT GOVERNED.",
    "deviations": [
    "Producer file: the measurement named packages/app-shell/src/providers/MetadataProvider.tsx. I reported it to the coordinator through SendMessage(main) BEFORE editing it and before opening the PR, as the claim requires. The seat amended the claim (6026121402, limited to the composed objects getter). All four of the coordinator's conditions are met: (1) the write-path census; (2) both directions pinned plus a forced discard of the context value memo; (3) helper signatures unchanged and the cache module-local, so Clause-② no holds; (4) the effect reads effectivePage only as a yes/no, by reading the base code, and the 7230 pin is unchanged and green.",
    "The provider pin's discard proxy discards every armed useMemo but NOT useCallback, unlike permissions' providerCtxIdentity.discarded.test.tsx, which the coordinator pointed to. This was measured: the first draft discarded callbacks too and went red because objects emptied. Discarding bump re-runs the preview-mode effect, which clears the whole cache after mount; that hazard is outside the amended surface. The condition asked for a discard of the context value memo, and the leg does that. It first proves the discard landed: the context value is a new object.",
    "Real-stack measurement used a temporary objectstack worktree /home/user/objectstack-boot-11699 at origin/main 60ccda5a, fetched into a named ref (refs/os-11699-boot/main) rather than FETCH_HEAD. pnpm install, then the 60-task showcase closure build under the lock. Worktree removed, ref deleted, backend and both previews stopped by recorded PID and own port (the --fresh temp DB removed itself).",
    "Temporary uncommitted instrumentation in RecordDetailView.tsx and MetadataProvider.tsx on base 055d350 for the base build. Reverted with git checkout HEAD -- (absolute paths) before any fix was written: git diff HEAD empty, 0 markers, blob == HEAD for both. The after build carried 0 markers (grep of the bundle: 0 chunks).",
    "origin/main moved 885a96f → 055d350 (objectui#11683, fields only) between dispatch and worktree creation; the worktree is based on 055d350. RecordDetailView.tsx did not change in that commit.",
    "One lock queue-timeout (exit 99, NOT MEASURED) on the after console build behind another seat's 10-minute run; re-queued under the same slot name and completed.",
    "Commit trailers use objectui AGENTS.md's model-free pair (Co-authored-by: Claude, Claude-Session) rather than the harness reminder's model-named line. The PR footer uses os-dev's session-URL form.",
    "Labels: zero written. labeler.yml applied tests and package: app-shell. The PR assignee was set to os-support-ai (the card's) and read back MATCHES."
    ],
    "mcp_calls": "0 MCP GitHub calls. One non-GitHub agent message: SendMessage to main naming the producer file before the PR, as the claim requires.",
    "api_writes": "3, each one fleet-relay dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed as objectstack-fleet[bot]: (1) pr_create POST /repos/objectstack-ai/objectui/pulls, draft; read back 13381 of 13381 bytes identical; PR #11750. (2) label-write --assign os-support-ai, POST /repos//issues/11750/assignees; read back MATCHES. (3) This os-dev-report comment, POST /repos//issues/11699/comments via post-stamped. git push x4 (055d350 empty probe, 948ddbd, 6892ddb, be32ead) is not REST.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted in PR objectui#11750's Acceptance notes, not filed (dormant: React does not discard on its own in this tree). MetadataProvider's preview-mode effect lists bump (a useCallback) as a dependency. After mount, any re-run clears the whole metadata cache, so a forced callback discard empties objects. This is an AGENTS.md #10 instance outside the amended surface, observed in the provider pin's first draft.",
    "carrier: 承接者:无 · noted in the Acceptance notes, not filed (not measured). A refetch that answers byte-identical metadata (an explicit refresh() after a publish or install) still installs new stored arrays, so the composed objects list is new then. That is rule #10's refetch half. The record page no longer re-reads on it; other identity-keyed consumers may recompute.",
    "carrier: 承接者:无 · noted, not filed. RecordDetailView.expandFls-7230.test.tsx keeps its header line saying the perms stub rides the record-load effect's dependency list; after this change the gated list rides it. The order asked for that file unchanged."
    ]
    }

  12. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-06T23:30Z. PR objectui#11750, head be32ead.

    • PR shape: draft against main. First line Fixes #11699, which is right because this half completes the Done-when (the concurrent half landed in PR objectui#11745). No other closing keyword. Clause-②: no at line start.

    • Scope: 5 files, inside the claim (6025812088) as amended by 6026121402:

      • RecordDetailView.tsx (the page-record load effect and its dependency list);
      • MetadataProvider.tsx (the composed objects getter only, plus its module-local cache);
      • the landed pin RecordDetailView.recordOpenRequests-11699.test.tsx, extended;
      • a new provider pin;
      • the changeset.

      RecordDetailView.expandFls-7230.test.tsx is unchanged. No governed path.

    • Diff read:

      • The effect now depends on [hasPage, objectName, pureRecordId, dataSource, pageRecordExpand, recordInvalidationNonce]. pageRecordExpand is the field-level-security-gated $expand list as a JSON string, computed during render from objectDef?.fields and perms, exactly as the effect body computed it before. An unanswered policy still filters nothing, and a narrowing answer changes the string and re-reads.
      • composeObjects caches in a module-level WeakMap keyed on the stored object array, with the stored view array checked beside it. mergeViewsIntoObjects and attachInlineSubforms are unchanged, and the cache and composeObjects are not exported.
      • Consumer check: no in-place mutation (sort / push / splice / reverse / …) of objects in packages/** or apps/** source outside tests. Control: the same pattern family finds objects.map / filter / find reads. attachInlineSubforms already returned the stored array itself when there was nothing to attach.
    • The claim's conditions (6026121402), each met in the report:

      • (1) the write-path census for the stored arrays, recorded in the cache's docblock;
      • (2) both directions pinned, with live controls for replacing objects and views, plus a forced discard of the context-value memo;
      • (3) the helpers' signatures are unchanged;
      • (4) the effect reads effectivePage only as a yes/no, and the 7230 pin is green and unedited.
    • Changeset sentences checked against the head:

      • "2 per open … to 1", "40 to 39" (the dev's real-stack measurement: showcase at objectstack 60ccda5a, a production console build, 4 reloads per build).
      • The three non-re-read cases each have a pin: an equal definition as a new object, an assigned page replacing the synthesized one, and an unchanged permission answer.
      • The two re-read cases each have a live control: a denied relation and changed relations.
      • "useMetadata().objects keeps its identity": the same-version, other-type-landing and discard legs.
      • "a landed … fetch, or an invalidation, still produces a new list": the live controls.
      • "Nothing is added to the package entry".

      patch on @object-ui/app-shell, matching Clause-②: no.

    • Reverse verification (dev report 6027187315):

      • Leg C (the old dependency list) gave 4 red, exactly the identity cases, and 9 green, including the 7230 pin's 6 tests.
      • Leg P (the old getter) gave 3 red and 2 green.
      • Leg M (a React-memo cache in place of the module cache) gave 2 red, which shows the discard leg fails for its own reason.
      • Each restore was proven by blob match.
    • Gates: CI on be32ead (head_sha equals the PR head): 43 runs, 40 success, 3 skipped (the two coverage jobs and dependabot), 0 failure; commit status success; 694 lines changed.

    • Contract review: not required (Clause-②: no).

    • Open questions: none.

    Findings:

    • MetadataProvider's preview-mode effect lists bump (a useCallback) as a dependency, so a forced callback discard re-runs it and clears the whole metadata cache. This is an AGENTS.md [WIP] Enhance every detail of the designer #10 instance outside the amended surface → Acceptance notes (dormant: React does not discard on its own in this tree; no reach)
    • a refetch that answers byte-identical metadata still installs new stored arrays, so objects is new then (rule [WIP] Enhance every detail of the designer #10's refetch half). The record page no longer re-reads on it; other identity-keyed consumers may recompute → Acceptance notes (not measured)
    • the header comment of RecordDetailView.expandFls-7230.test.tsx still says perms rides the effect's dependency list; the gated list rides it now → Acceptance notes (a test-file comment; the dispatch asked for that file unchanged)

    Landing: ready + auto-merge through the queue, now. Nothing is serial behind this PR (seat post §3).


    Generated by Claude Code

  13. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-06T23:49Z. PR objectui#11750 merged through the queue as 9b673e5.

    • The merge. One parent, db4cb6b. It is an ancestor of origin/main. The landed tree equals git merge-tree of that parent and the accepted head be32ead (tree 5556f60e on both). 5 files, +609/−85.
    • Content check, db4cb6b → 9b673e5:
      • RecordDetailView.tsx: const pageRecordExpand = JSON.stringify( 0 → 1. The new dependency list [hasPage, objectName, pureRecordId, dataSource, pageRecordExpand, recordInvalidationNonce] 0 → 1. The old list with objectDef / effectivePage / perms 1 → 0.
      • MetadataProvider.tsx: const COMPOSED_OBJECTS = new WeakMap 0 → 1. The getter's composeObjects(...) call 0 → 1.
      • Control: export function attachInlineSubforms is 1 on both.
    • Closures. The PR's only closing keyword was Fixes #11699. objectui#11699 is the only issue closed since 23:20Z. Both halves of the card's Done-when have now landed: the concurrent half in PR objectui#11745 (4590363), the sequential half here. pm:dispatched removed.
    • Serial queue. Nothing was queued behind this PR (seat post §3).

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatenhancementNew feature or requestpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions