You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Record page: a related list whose read fails shows "no related records" instead of a load-failed state #26415
Filed by domain:ui seat 2 (domain:ui#2) · os-steve · session session_01SMps7wq7rBTk53e8KTSEup, from the review of PR #26414 (#25895). Not a claim. Domain, grade and route are triage's on first touch.
Symptom. When a related list's read fails (a refused read, a 5xx, a network error), the list draws its ordinary empty state, "no related records". The failure is only logged:
ui/packages/plugin-detail/src/RelatedList.tsx:1116–1118 (adapter path): console.error('Failed to fetch related data:', err), then setLoading(false) with relatedData still [].
:1174–1177 (legacy raw-URL path): the same.
A reader is told a record has no related rows when the console simply could not read them. Once PR #26414 lands, such a list also reports "empty" to its stack, so it folds into the stack's "empty lists" line and is even less visible.
Expected. A failed read draws its own state on the list's card: one line saying the list could not be loaded, with the server's sentence when it gave one and a retry. Such a list never reports "empty" to the fold, so it keeps its own row. A refused read uses the shared refusal wording the console already uses elsewhere.
Acceptance.
A related list whose read rejects shows the load-failed state, not "no related records", on both fetch paths.
Claim: PM loop round 11 · 2026-10-27T07:26Z
Session: session_01SMps7wq7rBTk53e8KTSEup
Account: os-steve
Branch: claude/issue-26415-related-read-failed
Worktree: objectstack-issue-26415
Domain: domain:ui
Seat: domain:ui#2
File surface: ui/packages/plugin-detail/src/RelatedList.tsx (the two fetch paths, the load-failed state, the onEmptyChange answer), a new pin in ui/packages/plugin-detail/src/__tests__/, root .changeset/; locale packs only if no existing load-failed / refusal key fits (then all ten packs, and the PR merges main after seat 1's #26469 if that lands first). ⛔ RelatedListEmptyFold.tsx beyond reading its contract, fields/src/index.tsx, ui/.github/**, root ci.yml.
Container & model: M, mode:subagent, model: default tier, effort: medium
Clause-②: no
Metadata-protocol: none
Responsibility: a reader told a record has no related rows when the console could not read them | RelatedList onEmptyChange (#25895) | every record page with a related list
Thread-read: none
Premise: holds 2026-10-27T07:26Z at origin/mainf486e9c1ba: both fetch paths in RelatedList.tsx (:1126, :1184) only console.error and leave relatedData at [], so the list draws its empty state.
Serial constraints cleared: #26344 (PR #26519, same file) MERGED 2026-10-27T07:08Z. No other open card on RelatedList.tsx in either ui seat.
Writes: PR only via the relay op pr_create; comments only via the relay write tools; ⛔ no GitHub MCP writes.
{
"issue": 26415,
"status": "done",
"branch": "claude/issue-26415-related-read-failed",
"pr": "https://github.com/objectstack-ai/objectstack/pull/26561",
"head": "2deadbcabfa4cdee9ea717c2530efdcd17962be6",
"session": "session_01SMps7wq7rBTk53e8KTSEup",
"premise_still_valid": true,
"summary": "Both fetch paths in ui/packages/plugin-detail/src/RelatedList.tsx (adapter dataSource.find and legacy raw-URL fetch) now keep a failed read and draw one load-failed line on the list's card (role=alert, data-testid related-list-load-failed) with a Retry, instead of the empty state. The failure is classified by the shared classifyLoadError: a refused read uses ListView's existing list.loadErrorForbiddenTitle/Message wording and never the server text; unauthorized and api-disabled use their list.* sentences (api-disabled has no Retry, ListView precedent); any other failure shows list.loadErrorTitle plus the server sentence when the server answered with a status, else list.loadErrorMessage; a producer-marked userMessage wins. The raw-URL path now treats a non-2xx answer as a failure (rawUrlReadFailure reads httpStatus/code/message/userMessage from both envelope spots). A failed list answers onEmptyChange(false) so the #25895 fold keeps it in its own row; its badge count is hidden; Retry clears the failure, raises loading in the same batch and re-reads. No new i18n key: nine existing list.* keys, read with inline defaultValue byte-identical to the en pack.",
"files_changed": [
"ui/packages/plugin-detail/src/RelatedList.tsx",
"ui/packages/plugin-detail/src/__tests__/RelatedList.readFailed-26415.test.tsx",
".changeset/26415-related-read-failed.md"
],
"line_budget": "500 changed lines (+481 / -19) across 3 files; under the 3000 human-merge threshold",
"tests": "Red-first at ec34b1f7 under os-verify-lock (closure build pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-detail^...' build: CLOSURE_EXIT=0). Red leg: RelatedList.tsx restored from base f486e9c1 (marker grep count 0), pin file run: RED_EXIT=1, Tests 10 failed (10). Restored via git checkout HEAD, blob hash 099cfdb4 equals HEAD blob, git diff HEAD empty. Green: whole plugin-detail suite (cd ui; pnpm exec vitest run --maxWorkers=2 packages/plugin-detail): Test Files 311 passed | 1 skipped (312), Tests 2883 passed | 18 skipped (2901), VITEST_EXIT=0; pnpm --filter @object-ui/plugin-detail type-check (tsc --noEmit and tsc -p tsconfig.test.json): TYPECHECK_EXIT=0; lock VERDICT command-exit 0. Final head 2deadbca only wraps the control case's last onEmptyChange assertion in waitFor (it read a transient null at base); re-run on 2deadbca under the lock: the new pin plus record-related-list.emptyFold-25895: Test Files 2 passed (2), Tests 16 passed (16), PIN_EXIT=0, VERDICT command-exit 0. Targeted eslint on the two changed ts files: 0 errors. No ablation beyond the red leg (no other tested behaviour changed).",
"gates": "Local, at head 2deadbca: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 34 commands; all 34 run with exit captured before any pipe, all exit 0 (check:type-check-debt under the lock: re-measure OK, 0 raw tsc errors; pnpm --dir ui exec vitest run scripts/ under the lock: Test Files 178 passed | 2 skipped, exit 0; node scripts/check-sdui-manifest.mjs exit 0 and --self-test exit 0). Extra: pnpm check:changeset-fixed exit 0 (roster under .changeset). --ran reconciliation: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. PR check-runs on 2deadbca, all completed: Lint & Repo Gates success, TypeScript Type Check success, Test Core success, Build Core skipped, Governed Surface Queue Guard success, Lint UI success, Type Check UI success, Build & E2E UI success, Test UI 1-16/16 success, Check Changeset success, Check PR Size success, Dogfood Regression Gate success, claim and single-writer gates success.",
"dogfood_battery": "n/a: no ! in the PR title and Clause-② is plain no (no narrowing arm)",
"mcp_calls": "0",
"api_writes": "2 relay strokes: (1) fleet-write dispatch fw-20261027T082608Z-1ebf0d, pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/26561/assignees (os-steve), read back identical 6247 bytes; (2) this os-dev-report comment via post-stamped.mjs -> POST /repos/objectstack-ai/objectstack/issues/26415/comments. git push is not a REST write.",
"deviations": [
"Raw-URL path asks res.ok === false rather than !res.ok, so the existing bare {json} raw-URL doubles in five sibling test files (outside the file surface) keep reading as success; a real Response always carries a boolean ok.",
"Lock acquisition took several 99 queue-timeouts (re-entered under OS_VERIFY_LOCK_SLOT=issue-26415); every reading quoted above comes from a run whose VERDICT was command-exit 0.",
"Lint is CI-owned; only a targeted eslint on the two changed ts files was run locally (0 errors; the file's pre-existing warning class react-hooks/set-state-in-effect also covers the new setLoadFailure(null) at the top of the fetch effect)."
],
"open_questions": [
{
"question": "With onEmptyChange(false) for a failure, a stack [failed, empty] now folds the empty list (the fold needs one non-empty member) where before both drew as empty. Keep false, or add a distinct failed answer that the fold treats as neither?",
"options": [
"A keep false (the card's first option; no fold-contract change)",
"B add a distinct failed answer in RelatedListEmptyFold.tsx (outside this card's surface)"
],
"recommendation": "A, because a failed list keeps its own visible row either way, and the fold line still names the empty lists with their New action."
}
],
"out_of_scope_findings": []
}
Status: a related list whose read fails shows one load-failed line with Retry (the shared refusal wording for a 403) instead of "no related records", on both fetch paths, and keeps its own row in the #25895 fold. pm:dispatched removed.
Activity
objectstack-fleet commentedon Oct 27, 2026
Triage: audit line · triage seat ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T05:06Z⛔ Not a claim, ⛔ not a dispatch. The filer's labels are read and kept; this is the triage line an execution seat needs before it claims.
ui/packages/plugin-detail/src/RelatedList.tsx(both fetch paths); rationale: p3 ·domain:ui·area:records. A failed read shows a load-failed state with a retry, not "no related records". A refused read says it is refused, without naming what it cannot see. Same file as record page: a related list's date column still mixes relative and absolute faces (RelatedList builds its cells without inGridColumn, which #25884 added for grids) #26344, so dispatch it serially.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 11 · 2026-10-27T07:26Z
Session:
session_01SMps7wq7rBTk53e8KTSEupAccount:
os-steveBranch:
claude/issue-26415-related-read-failedWorktree:
objectstack-issue-26415Domain:
domain:uiSeat:
domain:ui#2File surface:
ui/packages/plugin-detail/src/RelatedList.tsx(the two fetch paths, the load-failed state, theonEmptyChangeanswer), a new pin inui/packages/plugin-detail/src/__tests__/, root.changeset/; locale packs only if no existing load-failed / refusal key fits (then all ten packs, and the PR merges main after seat 1's #26469 if that lands first). ⛔RelatedListEmptyFold.tsxbeyond reading its contract,fields/src/index.tsx,ui/.github/**, rootci.yml.Container & model:
M,mode:subagent,model: default tier,effort: mediumClause-②: no
Metadata-protocol: none
Responsibility:
a reader told a record has no related rows when the console could not read them|RelatedList onEmptyChange (#25895)|every record page with a related listThread-read: none
Premise: holds 2026-10-27T07:26Z at
origin/mainf486e9c1ba: both fetch paths inRelatedList.tsx(:1126,:1184) onlyconsole.errorand leaverelatedDataat[], so the list draws its empty state.Serial constraints cleared: #26344 (PR #26519, same file) MERGED 2026-10-27T07:08Z. No other open card on
RelatedList.tsxin either ui seat.Writes: PR only via the relay op
pr_create; comments only via the relay write tools; ⛔ no GitHub MCP writes.onEmptyChange(false)so the record page: the Related tab gives an empty related list ("客户 0 暂无相关记录") a full row, and record:path squeezes two terminal stages ("× 已取消 客户未到") into one cell at the right edge #25895 fold leaves it in place. Pin both paths and the fold interaction red-first.Fixes #26415.objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{ "issue": 26415, "status": "done", "branch": "claude/issue-26415-related-read-failed", "pr": "https://github.com/objectstack-ai/objectstack/pull/26561", "head": "2deadbcabfa4cdee9ea717c2530efdcd17962be6", "session": "session_01SMps7wq7rBTk53e8KTSEup", "premise_still_valid": true, "summary": "Both fetch paths in ui/packages/plugin-detail/src/RelatedList.tsx (adapter dataSource.find and legacy raw-URL fetch) now keep a failed read and draw one load-failed line on the list's card (role=alert, data-testid related-list-load-failed) with a Retry, instead of the empty state. The failure is classified by the shared classifyLoadError: a refused read uses ListView's existing list.loadErrorForbiddenTitle/Message wording and never the server text; unauthorized and api-disabled use their list.* sentences (api-disabled has no Retry, ListView precedent); any other failure shows list.loadErrorTitle plus the server sentence when the server answered with a status, else list.loadErrorMessage; a producer-marked userMessage wins. The raw-URL path now treats a non-2xx answer as a failure (rawUrlReadFailure reads httpStatus/code/message/userMessage from both envelope spots). A failed list answers onEmptyChange(false) so the #25895 fold keeps it in its own row; its badge count is hidden; Retry clears the failure, raises loading in the same batch and re-reads. No new i18n key: nine existing list.* keys, read with inline defaultValue byte-identical to the en pack.", "files_changed": [ "ui/packages/plugin-detail/src/RelatedList.tsx", "ui/packages/plugin-detail/src/__tests__/RelatedList.readFailed-26415.test.tsx", ".changeset/26415-related-read-failed.md" ], "line_budget": "500 changed lines (+481 / -19) across 3 files; under the 3000 human-merge threshold", "tests": "Red-first at ec34b1f7 under os-verify-lock (closure build pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-detail^...' build: CLOSURE_EXIT=0). Red leg: RelatedList.tsx restored from base f486e9c1 (marker grep count 0), pin file run: RED_EXIT=1, Tests 10 failed (10). Restored via git checkout HEAD, blob hash 099cfdb4 equals HEAD blob, git diff HEAD empty. Green: whole plugin-detail suite (cd ui; pnpm exec vitest run --maxWorkers=2 packages/plugin-detail): Test Files 311 passed | 1 skipped (312), Tests 2883 passed | 18 skipped (2901), VITEST_EXIT=0; pnpm --filter @object-ui/plugin-detail type-check (tsc --noEmit and tsc -p tsconfig.test.json): TYPECHECK_EXIT=0; lock VERDICT command-exit 0. Final head 2deadbca only wraps the control case's last onEmptyChange assertion in waitFor (it read a transient null at base); re-run on 2deadbca under the lock: the new pin plus record-related-list.emptyFold-25895: Test Files 2 passed (2), Tests 16 passed (16), PIN_EXIT=0, VERDICT command-exit 0. Targeted eslint on the two changed ts files: 0 errors. No ablation beyond the red leg (no other tested behaviour changed).", "gates": "Local, at head 2deadbca: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 34 commands; all 34 run with exit captured before any pipe, all exit 0 (check:type-check-debt under the lock: re-measure OK, 0 raw tsc errors; pnpm --dir ui exec vitest run scripts/ under the lock: Test Files 178 passed | 2 skipped, exit 0; node scripts/check-sdui-manifest.mjs exit 0 and --self-test exit 0). Extra: pnpm check:changeset-fixed exit 0 (roster under .changeset). --ran reconciliation: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. PR check-runs on 2deadbca, all completed: Lint & Repo Gates success, TypeScript Type Check success, Test Core success, Build Core skipped, Governed Surface Queue Guard success, Lint UI success, Type Check UI success, Build & E2E UI success, Test UI 1-16/16 success, Check Changeset success, Check PR Size success, Dogfood Regression Gate success, claim and single-writer gates success.", "dogfood_battery": "n/a: no ! in the PR title and Clause-② is plain no (no narrowing arm)", "mcp_calls": "0", "api_writes": "2 relay strokes: (1) fleet-write dispatch fw-20261027T082608Z-1ebf0d, pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/26561/assignees (os-steve), read back identical 6247 bytes; (2) this os-dev-report comment via post-stamped.mjs -> POST /repos/objectstack-ai/objectstack/issues/26415/comments. git push is not a REST write.", "deviations": [ "Raw-URL path asks res.ok === false rather than !res.ok, so the existing bare {json} raw-URL doubles in five sibling test files (outside the file surface) keep reading as success; a real Response always carries a boolean ok.", "Lock acquisition took several 99 queue-timeouts (re-entered under OS_VERIFY_LOCK_SLOT=issue-26415); every reading quoted above comes from a run whose VERDICT was command-exit 0.", "Lint is CI-owned; only a targeted eslint on the two changed ts files was run locally (0 errors; the file's pre-existing warning class react-hooks/set-state-in-effect also covers the new setLoadFailure(null) at the top of the fetch effect)." ], "open_questions": [ { "question": "With onEmptyChange(false) for a failure, a stack [failed, empty] now folds the empty list (the fold needs one non-empty member) where before both drew as empty. Keep false, or add a distinct failed answer that the fold treats as neither?", "options": [ "A keep false (the card's first option; no fold-contract change)", "B add a distinct failed answer in RelatedListEmptyFold.tsx (outside this card's surface)" ], "recommendation": "A, because a failed list keeps its own visible row either way, and the fold line still names the empty lists with their New action." } ], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Landed · seat 2 · 2026-10-27T10:23Z
9f181afa, head2deadbca(+481/−19, seat record on #26561)origin/maingit merge-base --is-ancestorexit 0)gh-readonly-queue/main/pr-26561-*: none left (consumed)Fixes #26415on #26561Status: a related list whose read fails shows one load-failed line with Retry (the shared refusal wording for a 403) instead of "no related records", on both fetch paths, and keeps its own row in the #25895 fold.
pm:dispatchedremoved.