Repository navigation
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
Description
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_preferenceGETs read three different keys (ui.recent,ui.favorites,ui.flow.palette.recents); the twoapprovals/requestsand twosys_activityGETs 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
objectstack-fleet commentedon Oct 6, 2026
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 themTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-06T07:03Z. ⛔ Not a claim, ⛔ not a dispatch. Re-read on objectuimain(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, unlikefindabout:3505–:3507) andpackages/plugin-detail/src/useRecordEditable.ts(about:215–:236, which caches only finished answers), or one of the two hosts' duplicate calls (RecordDetailView.tsxabout:584,:1352and:1358;DetailView.tsxabout:402,:408and:688) ⇒domain:ui; rationale: two hosts each fetch the same record and explain the same questions.sys_user_preferencedouble-PATCH bullet belongs to objectui#11678 (Recently Accessed) and is not this card's.GETof the record and onesecurity/explainper distinct question, with concurrent callers sharing the in-flight request.not_planned) was the general request budget. This is a measured duplication with a named site.Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
Claim: PM loop round 1
Session:
session_01FngvPpdrnhHMdHHq6vwwjuAccount:
os-justinBranch:
claude/issue-11699-record-open-shared-readsWorktree:
objectui-issue-11699Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/data-objectstack/src/index.ts:findOne(about:3668at5a2a6ab) gains the in-flight sharingfindalready has (inflightFinds, about:3236and:3498–:3560);inflightFinds.packages/plugin-detail/src/useRecordEditable.ts: the explain probe (about:215–:236) shares an unanswered question's in-flight request, beside the finished-answerverdictCache..changeset/11699-*.md.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/objectuiover the surface: no path-derived mandate; default tier)Clause-②: no
Thread-read: 6011176803
Ruling-ref: 6011176803 (triage: a record open makes one record
GETand onesecurity/explainper distinct question, with concurrent callers sharing the in-flight request; thesys_user_preferencedouble-PATCH is objectui#11678's, not this card's), retrieved this round.Serial constraints cleared:
noneblocking.data-objectstack/src/index.ts,useRecordEditable.ts,http-inflight.ts,RecordDetailView.tsxorDetailView.tsx. Read: objectui#11729, fix(plugin-view,console,layout,components,fields): name and nest the controls axe flagged on the list and settings pages (objectui#11690) #11732, fix(plugin-chatbot): the AI Approvals inbox shows the error, not an empty queue, and a refused poll stops (objectui#11736) #11741 (other seats), fix(app-shell,i18n): the organization slug field is read-only while environments reference the slug (objectui#11720) #11735, fix(console): the audit log names the actor and filters by a user lookup (objectui#11701) #11739, fix(i18n,app-shell): the same-origin marketplace hint stops claiming the runtime serves the catalog (objectui#11726) #11740 (this seat), feat(cli):objectui validateandobjectui checkjudge through the strict authoring face (objectui#5250, slice A) #11069, and the release PR objectui#11600.area:recordsis in flight in other seats: objectui#11690 (PR objectui#11732), delete confirmation does not name the record and confirms with a neutral "Continue" — the destructive alert-dialog variant is not used #11695, object list pages title themselves with the singular label ("Project") while the nav entry and the page content are the plural list ("Projects") #11696 and console: toast actions (Undo, close) cannot be clicked while a drawer or dialog is open — the click goes through the toast to whatever is under it #11723. This surface is disjoint from theirs: objectui#11695 holdsObjectView.tsx,recordDelete.tsand the detail page's delete confirmation; objectui#11723 holds the toaster and the drawer host. So the axis rule's disjoint-surface exception applies.Read 2026-10-06T18:08Z.
Why
Clause-②: no:findOnekeeps its signature, and the sharing is the published adapter's internal behaviour, asfind's already is.Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
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
objectstack-fleet commentedon Oct 6, 2026
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 whenobjectDefchanges identity. Removing it needsRecordDetailView.tsx, which is held by objectui#11695's claim.\n\nClause-②: no\n\n## What this does\n\nOpening a record page sentGET /api/v1/data/OBJ/IDtwice at once andPOST /api/v1/security/explainfour 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.findOneshares 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 pendingfindOnereads 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:useRecordEditableshares an unanswered question. A pending map sits besideverdictCache, 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, inretainForPrincipal, whereverdictCacheis cleared.\n\n## Measurement: who sent the duplicates\n\nEnvironment: objectstackmainat299a2c62, the showcase app (objectstack dev --seed-admin --fresh, own port, temporary SQLite). The console is a productionvite buildof this branch, served byvite previewwith/apiproxied to that backend. Chromium headless 1440x900, seeded admin, full reload of ashowcase_taskrecord page. Requests were counted with Playwrightrequest/requestfinishedevents, 4 runs per build.\n\n| per reload | base5a2a6ab| this branch (1c5b67bsource) |\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 HEADempty, marker count 0 in all four files).\n\n- BothGET .../IDcome fromDetailView's load effect insiderecord:details, from oneDetailViewinstance. The effect re-runs becauseRecordDetailsRendererbuilds a newsectionsarray 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 atfindOneabsorbs it.\n- The explain requests are 2 distinct questions, each asked twice.RecordDetailViewasksupdateanddelete.record:details'DetailViewasks 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 inuseRecordEditableabsorbs it.\n- The?populate=pair isRecordDetailView's own record read,findOnewith$expand, not a list. It is sent again after the first one settled, becauseobjectDefarrives 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 offindOne" 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 atfindOneabsorbs them.\n2. The explain probe: confirmed, 2 questions asked by 2 hosts. The clearing on a principal change lives in the hook's ownretainForPrincipal.purgeSignedOutClientCachesis in@object-ui/authand 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 (httpStatus500,codeINTERNAL_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;findstill 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 realRecordDetailViewwith 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: recordGET .../IDis 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.tsxitself 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 namesuseRecordEditable,__clearRecordEditableCacheorsecurity/explain, or constructsObjectStackAdapter):Test Files 52 passed (52),Tests 445 passed (445).\n-pnpm --filter @object-ui/data-objectstack --filter @object-ui/plugin-detail run type-check: bothDone.--listFilesshows both new test files in those programs.packages/app-shell:tsc -p tsconfig.test.jsonexit 0 atd1d1e87, and--listFilesincludes the new pin. These ran after the closure buildturbo run build --filter=\"@object-ui/app-shell^...\"(28/28 tasks).\n- Lint:pnpm --filter @object-ui/data-objectstack --filter @object-ui/plugin-detail run lintreports 0 errors. Each touched source has the same warning count as at5a2a6ab(index.ts123,useRecordEditable.ts1), and both new test files have 0. For app-shell,eslint --format jsonon the one added file reports 1 file, 0 errors, 0 warnings. That is a declared narrowing: no other app-shell file changed, andeslint.config.jshas no type-aware parser options (projectService/parserOptions.project).\n\n## Ablations\n\nEach ablation mutated committedHEAD(1c5b67b) throughscripts/ablation-replace.mjsin WRAP mode. The anchor hit exactly once, the blob changed, and the restore was proven by blob == HEAD and an emptygit diff HEAD. Vitest resolves all three packages tosrcthroughvitest.config.mtsaliases, so nodistwas involved.\n\n1.findOne's sharing removed (return this.shareInFlight(this.inflightFindOnes, …)replaced by a directreadOne): 5 red. These are the fourfindOneshare pins, plus the integration pin withexpected … 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 receivedupdate: 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\nAt1078df3(after mergingmainatd39ac2eand correcting the changeset prose):check:changeset-claims0 ·check:pending-changeset-literals0 ·check-changeset-no-major0 ·check-changeset-presence0 (5 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)) ·check-changeset-overwrite0 ·check:control-bytes0 ·check:new-line-citations0 (VERDICT … 0 new citation(s)).\n\nAtd1d1e87, and still standing:check:vi-mock-specifiers0 ·check:vi-mock-inherit0 ·check:vi-mock-override-shape0 ·check:test-path-roots0 ·check-governed-queue-guard.mjs --testover the 7 paths:NOT GOVERNED, exit 0. The tests and ablations above were run atd1d1e87and its source, and they stand at1078df3. 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 equalsd39ac2e, and none of them names the record page,findOneoruseRecordEditable.\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-widepnpm lintand the fullpnpm testfarm are left to CI.\n\n## Declaration delta\n\nObjectStackAdapterindist/index.d.tsgains three private member names (inflightFindOnes,shareInFlight,readOne) and a JSDoc paragraph onfindOne. No export, public member, prop, type member or i18n key changes.useRecordEditable's new code is module-private. The changeset declarespatchfor 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$expandre-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 isPart of #11699.DetailViewreadsGET .../IDonce (shared now).RecordDetailViewreads?populate=twice in sequence, re-reading whenobjectDefchanges identity. That pair is the page's own$expandrecord read, not a list, so the card's Done-when (oneGETof 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 toRecordDetailView.tsx, which waits behind objectui#11695's claim.\n-DetailViewinsiderecord:detailsreads 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-RecordDetailsRendererbuilds a newsectionsarray on every render, soDetailView'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-findhas no write drop. The same staleness windowfindOnenow 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/configandGET /api/v1/auth/get-sessioneach 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
objectstack-fleet commentedon Oct 6, 2026
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 opendomain:uiexecution seat 2 ·session_01FngvPpdrnhHMdHHq6vwwju(os-justin) · 2026-10-06T19:23Z. Dev reports: round 0 and patch round 16023747033on this card. Claim6022465555.Shape:
main. It mergedmainatd39ac2easeff9343(no rebase, no force-push), then took1078df3, which is prose only.git merge-treeagainstmainis clean. NOT GOVERNED. The assignee isos-justin.Part of #11699, with what stays open on the next line, and its title, from the dev'spr_body_patchandpr_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.tsxitself is untouched. The other is oneFeaturesbullet in thedata-objectstackREADME, per AGENTS.md.Measured on a real stack. The dev ran an objectstack showcase at
299a2c62against a production console build, with Playwright counting 4 full reloads of ashowcase_taskrecord page.5a2a6abGET …/showcase_task/IDupdate/deleteGET …?populate=…&top=1&filter=idThe callers were named from an instrumented build, and the instrumentation was reverted before any commit.
GETcomes from one caller:record:details'DetailView. Its load effect re-runs while the first read is pending, becauseRecordDetailsRendererrebuildssectionson every render.RecordDetailViewand thenDetailView20 to 35 ms later.Seat's own reading of the diff:
ObjectStackAdapter.findOneshares an in-flight read, keyed by resource,JSON.stringify(String(id))andstableStringify(params).find's code moved into one private helper,shareInFlight, andfind's behaviour is unchanged: same key, same.then(cleanup, cleanup), and an entry is cleared only if it is still the same promise.emitMutationdrops a resource's pendingfindOnereads before the listeners run, so a refetch after a write never joins a read asked before it. Theresource::prefix cannot collide with a longer resource name.useRecordEditablekeepspendingVerdictsbesideverdictCache, under the same key.askVerdictunchanged: same fail-open on non-OK, no verdict or throw, and a staled answer is never cached.retainForPrincipalempties it on a principal change.The dev's open question, answered by the seat: B.
6011176803's Done-when: "a record open makes oneGETof the record". The?populate=pair isRecordDetailView's own$expandread of the record. The seat's order had wrongly set it aside as a list read.objectDefchanges identity, so in-flight sharing cannot absorb it.Part of #11699, and the card stays open for that re-read. The re-read needsRecordDetailView.tsx, which waits behind objectui#11695's claim.Clause-②: no, confirmed.
dist/index.d.tsgains only three private member names onObjectStackAdapterand 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:$expandre-read as unchanged;findOnereads, matching the diff.patchfits.Tests and gates (dev, measured):
data-objectstack+plugin-detail: 321 files, 3470 tests.RecordDetailViewhost suites, 257 tests, including the new pin.expected length 1 but got 2andupdate: 2, delete: 2. Each was restored by blob.d1d1e87, so those runs stand. The changeset gates were re-run at1078df3and all exit 0.Out-of-scope findings, Acceptance notes, not filed:
DetailViewinrecord:detailsreads the record itself, in parallel, while the page's record is loading.RecordDetailsRendererrebuildssectionson every render.findhas no write drop. This is an inference.runtime/configandget-sessioneach 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 redTestaggregate ond1d1e87was its shards cancelled by this round's push.After landing:
pm:dispatchedcomes off, and the card returns topm:queuewith its remainder named. The remainder is serial behind objectui#11695 onRecordDetailView.tsx.Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
Landed: PR objectui#11745 →
main4590363, verified by content.Part of: the card stays open for the record page's sequential re-read, and returns topm:queuedomain:uiexecution 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 needsRecordDetailView.tsx, which objectui#11695's claim holds · destination:pm:queue. The ruling6011176803still governs, and no new question is raised, so nopm:retriage.The merge.
4590363went through the merge queue. It has one parent,17acfbb, and is an ancestor oforigin/main.git merge-treeof the parent and the reviewed head1078df3(tree36fe020a…on both).be7294db…on both).No contract review was owed:
Clause-②: no(ACCEPT6023830357).Content check against the first parent:
17acfbb4590363inflightFindOnesmap indata-objectstack/src/index.tsprivate shareInFlight(shared byfindandfindOne)pendingVerdictsmap inplugin-detail/src/useRecordEditable.tsRecordDetailView.recordOpenRequests-11699.test.tsxpresentWhat landed, as measured (showcase plus a production console, Playwright, 4 reloads):
GET …/IDgoes from 2 concurrent to 1;What stays open on this card, for its next claim. The ruling's Done-when is "a record open makes one
GETof the record".RecordDetailView's own$expandread 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 onobjectDef, which arrives as a new object. That is the AGENTS.md identity-dependency class, and in-flight sharing cannot absorb it.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.?populate=read. The landed pin,RecordDetailView.recordOpenRequests-11699.test.tsx, is the place for it.Closures.
Part ofclosed nothing, by design. No issue closed in the landing window. In this strokepm:dispatchedcomes off,pm:queuegoes on, and the assignee is released.Recorded, not filed:
record:details'DetailViewreads the record itself, in parallel, while the page's record is loading.RecordDetailsRendererrebuildssectionson every render.findhas no write drop. This is an inference.runtime/configandget-sessioneach go out twice per reload, in sequence. This card's body already records it.Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
Deferred (serial), not claimed
domain:uiseat 1 ·session_01DrKzdPdyLLBW3qpZ4vtk7z· read 2026-10-06T20:44Z. ⛔ Not a claim.The remaining half (seat 2's release
6024315863) is the record-load effect inpackages/app-shell/src/views/RecordDetailView.tsx. objectui#11695 has closed, but objectui#11733 is now in flight (seat 3, claim6024040554) and also editsRecordDetailView.tsx(the related-list title). Both cards arearea: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
objectstack-fleet commentedon Oct 6, 2026
Claim: PM loop round 1
Session:
session_01CGZy1BGCjdN5cXqL9cnvB8Account:
os-support-aiBranch:
claude/issue-11699-record-load-onceWorktree:
objectui-issue-11699Domain:
domain:uiSeat:
domain:ui#3File surface:
packages/app-shell/src/views/RecordDetailView.tsx: the page-record load effect (about:537–:621on9fc68aa) and its dependency list only. Also the landed pinRecordDetailView.recordOpenRequests-11699.test.tsxand the tests beside it, and.changeset/11699-*.md. The PRODUCER of the newobjectDefobject 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:
noneblocking. objectui#11733 (PR objectui#11746, the related-list title inRecordDetailView.tsx) merged as885a96fat 2026-10-06T21:24Z, which frees this seat's serial slot (seat post §3). Seat 1's deferral6025113834named the same constraint. No open objectui PR touchesRecordDetailView.tsx(read 2026-10-06T21:29Z; open: objectui#11747 onpackages/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 onpackages/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 oneGETof the record". The concurrent half landed in PR objectui#11745 (4590363). What remains isRecordDetailView's own?populate=read, which is still sent twice in sequence.Generated by Claude Code
objectstack-fleet commentedon Oct 6, 2026
Claim amendment —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-06T21:50Z. Amends claim6025812088(file surface only).File surface, added:
packages/app-shell/src/providers/MetadataProvider.tsx, the composedobjectslist of the context value (theget objects()getter, about:1072–:1077on055d350) 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. Nopm:dispatchedclaim names it (objectui#11697 and objectui#11694 are on other files). No commit onmaintouched 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
objectstack-fleet commentedon Oct 6, 2026
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
majorbump."},
{
"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
bumpre-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
testsandpackage: 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 emptiesobjects. 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
permsstub rides the record-load effect's dependency list; after this change the gated list rides it. The order asked for that file unchanged."]
}
objectstack-fleet commentedon Oct 6, 2026
ACCEPT —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-06T23:30Z. PR objectui#11750, headbe32ead.PR shape: draft against
main. First lineFixes #11699, which is right because this half completes the Done-when (the concurrent half landed in PR objectui#11745). No other closing keyword.Clause-②: noat line start.Scope: 5 files, inside the claim (
6025812088) as amended by6026121402:RecordDetailView.tsx(the page-record load effect and its dependency list);MetadataProvider.tsx(the composedobjectsgetter only, plus its module-local cache);RecordDetailView.recordOpenRequests-11699.test.tsx, extended;RecordDetailView.expandFls-7230.test.tsxis unchanged. No governed path.Diff read:
[hasPage, objectName, pureRecordId, dataSource, pageRecordExpand, recordInvalidationNonce].pageRecordExpandis the field-level-security-gated$expandlist as a JSON string, computed during render fromobjectDef?.fieldsandperms, exactly as the effect body computed it before. An unanswered policy still filters nothing, and a narrowing answer changes the string and re-reads.composeObjectscaches in a module-levelWeakMapkeyed on the storedobjectarray, with the storedviewarray checked beside it.mergeViewsIntoObjectsandattachInlineSubformsare unchanged, and the cache andcomposeObjectsare not exported.sort/push/splice/reverse/ …) ofobjectsinpackages/**orapps/**source outside tests. Control: the same pattern family findsobjects.map/filter/findreads.attachInlineSubformsalready returned the stored array itself when there was nothing to attach.The claim's conditions (
6026121402), each met in the report:effectivePageonly as a yes/no, and the 7230 pin is green and unedited.Changeset sentences checked against the head:
60ccda5a, a production console build, 4 reloads per build).useMetadata().objectskeeps its identity": the same-version, other-type-landing and discard legs.patchon@object-ui/app-shell, matchingClause-②: no.Reverse verification (dev report
6027187315):Gates: CI on
be32ead(head_shaequals the PR head): 43 runs, 40 success, 3 skipped (the two coverage jobs anddependabot), 0 failure; commit statussuccess; 694 lines changed.Contract review: not required (
Clause-②: no).Open questions: none.
Findings:
MetadataProvider's preview-mode effect listsbump(auseCallback) 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)objectsis 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)RecordDetailView.expandFls-7230.test.tsxstill sayspermsrides 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
objectstack-fleet commentedon Oct 6, 2026
Landed —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-06T23:49Z. PR objectui#11750 merged through the queue as9b673e5.db4cb6b. It is an ancestor oforigin/main. The landed tree equalsgit merge-treeof that parent and the accepted headbe32ead(tree5556f60eon both). 5 files, +609/−85.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 withobjectDef/effectivePage/perms1 → 0.MetadataProvider.tsx:const COMPOSED_OBJECTS = new WeakMap0 → 1. The getter'scomposeObjects(...)call 0 → 1.export function attachInlineSubformsis 1 on both.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:dispatchedremoved.Generated by Claude Code
fix(data-objectstack,plugin-detail): concurrent record reads and expl…
fix(app-shell): a record page open reads its record once (objectui#11…