Skip to content

console(grid): a second toolbar change to a view overwrites the first — every whole-document PUT is built from the page-load row, so density reverts after a sort #11642

Closed

Description

@objectstack-fleet

Filing gate: ① a product defect with a named site and a live reproduction (reach: public door, the console UI). Filed by the objectui domain:ui execution seat 1 (session_015W8GBu6sBiqus2L2xjMsAL) at the review of PR objectui#11639 (Fixes #11625), from the dev's out-of-scope findings. ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.

Who acts: triage grades it. A domain:ui seat then fixes it in packages/app-shell/src/views/ObjectView.tsx.

Reproduction (measured by the dev, objectstack main 045f764c showcase; console built from PR objectui#11639's branch)

  1. Open showcase_task.in_progress in the console.
  2. Toggle density. Wait past the 300 ms save debounce.
  3. Sort on the Title header.
  4. Wire: the second PUT /api/v1/meta/view/showcase_task.in_progress carries config.sort only. GET reads back config.sort and no rowHeight.
  5. Reload. Expected: the new density and the sort. Actual: "Density: Compact", the old density, with the sort kept.

The overlay branch does the same on showcase_task.done: the second PUT { sort, viewKind, object, name, _isOverride } replaces the earlier { rowHeight, … } row.

Mechanism

  • persistViewPatch builds every whole-document PUT from the tab as it was when the page loaded (the saved branch of buildPersistedViewBody), or from the pending patch alone (the overlay branch).
  • Nothing refreshes either base after a successful write, so each later save drops the earlier one.
  • It predates PR objectui#11639 on every row kind. That PR's changeset states it as still open.

Done when

Two toolbar changes to one view in one session both survive a reload. That holds for each row kind (envelope row, flat row, overlay), and it is pinned.

Dedupe: the 1200 most recently updated objectui issues and PRs, open and closed (back to #6900), were listed over REST and grepped locally. persistViewPatch hits 3, and buildPersistedViewBody hits 3: objectui#11625, PR objectui#11639, PR objectui#11593 and PR objectui#11010, none of which is this lost update. lost update hits 0, and overwrites the first hits only PR objectui#11639. The control rowHeight hits 21.

Dedupe words: persistViewPatch lost update · second toolbar change overwrites first · density reverts after sort · whole-document PUT stale base


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: views to see the data | records-forms.grid-personalization | P2

    Triage: first grade — bug · priority:p2 · domain:ui · area:records · pm:queue. Each save is built from the row the last save left, not from the page-load row

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-05T04:51Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: lands in packages/app-shell/src/views/ObjectView.tsx (persistViewPatch / buildPersistedViewBody) ⇒ domain:ui; rationale: every whole-document PUT starts from a stale base, so the second change silently drops the first.

    • Why p2. Same class and grade as objectui#11625. The user's earlier choice is acknowledged and then lost with no error, here after any second toolbar change.
    • Direction. After a successful write, the base for the next write is the row that write stored, on every row kind (envelope row, flat row, overlay). Two changes in one session then compose.
      • ⛔ No client-side merge of guessed fields. The base is the stored row.
    • Pins: the card's Done-when (two changes to one view both survive a reload), for each row kind.
    • Serial: objectui#11625 is in flight on the same function (PR objectui#11639). This card starts after it lands.

    Generated by Claude Code

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

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2
    Session: session_015W8GBu6sBiqus2L2xjMsAL
    Account: os-steve
    Branch: claude/issue-11642-view-save-base
    Worktree: objectui-issue-11642
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: packages/app-shell/src/views/ObjectView.tsx (persistViewPatch, about :1471, buildPersistedViewBody, and where each save takes its base row), the ObjectView tests beside it, and .changeset/11642-*.md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default tier (dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; floor sonnet · default opus · ceiling fable)
    Clause-②: no
    Thread-read: 5988335796
    Serial constraints cleared: objectui#11625, the triage-named predecessor on the same function, landed as 59917c4 (PR objectui#11639), read 2026-10-05T05:41Z. The open objectui PRs (#11641 plugin-detail, possibly app-shell RecordDetailView.tsx and metadata-admin/ in its patch round; #11644 packages/components; #11069 packages/cli; #11600 the release PR) touch no ObjectView.tsx. The in-flight objectui#11626 declares packages/layout and UnifiedSidebar.tsx.

    Why Clause-②: no: persistViewPatch is internal, and buildPersistedViewBody is not exported from the @object-ui/app-shell entry. The fix changes which row the next save starts from, so nothing in the published accept set or public surface widens. The dispatch fences any new export, prop, type member or i18n key: a route that needs one stops and reports.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 11642,
     "status": "done",
     "branch": "claude/issue-11642-view-save-base",
     "pr": "https://github.com/objectstack-ai/objectui/pull/11646",
     "session": "session_015W8GBu6sBiqus2L2xjMsAL",
     "premise_still_valid": true,
     "summary": "Each grid toolbar save (persistViewPatch in packages/app-shell/src/views/ObjectView.tsx) now starts from the row the store holds when the save runs, read back through loadViewOverrides (the list read GET /api/v1/meta/view the page load uses), instead of the tab as it was at page load. A saved view (envelope or flat row) places the patch on that row; an overlay joins its stored owned keys (VIEW_OVERLAY_OWNED_KEYS, as loadViewOverrides narrows a marked overlay) with the patch while the tab still supplies viewKind only, so no source key is frozen (objectui#5233 kept). Writes to one view are chained so a write never reads the row from before the previous write; the 300 ms debounce still coalesces changes inside it into one write. Measured live on objectstack main a3ffc451 (showcase): before, density then a header sort kept only the sort on the envelope, flat and overlay rows; after, both survive a reload on all three, and the debounced pair is one PUT carrying both. Draft PR objectui#11646 is assigned to os-steve; Clause-②: no is on its own line in the body.",
     "tests": "HEAD 5af8b90 (final; merge of origin/main c000398 is 75d028f). NEW pin ObjectView.toolbarWritesCompose-11642.test.tsx mounts the real object page + real persistViewPatch over the real ObjectStackAdapter on a store that judges each PUT with ViewMetadataSchema, keeps the parsed value of the keys sent, and answers with no row (as the door was measured to). Cases: the Done-when per row kind (envelope, flat, overlay: 2nd PUT body, stored row and remounted relay rowHeight+sort all carry both changes; overlay bodies carry no filter/columns/label/type/config and keep the earlier session's columnState), debounce coalescing (one write), and in-flight (first PUT 500 ms, second debounce fires mid-PUT). ABLATION at 5af8b90 via objectstack scripts/ablation-replace.mjs wrap mode (anchor x1 → x0, blob 7d56c0a42288 → mutated, restored: blob == HEAD and git diff HEAD empty, post-ablation git diff HEAD bytes: 0): A (pre-fix base: tab / patch-only, chain kept), predicted 3 row-kind cases + in-flight red, coalescing green; observed 'Tests 4 failed | 28 passed (32)', each 'expected undefined to be medium', densityInsideConfig-11625 and overlayPatchOnly files green. B (chain removed): predicted and observed only in-flight red, 'Tests 1 failed | 4 passed (5)'. No dist involved: the subject is imported by relative path from src. GATES at 5af8b90: targeted 10 files (new pin, writeRefusalCensus-11583, densityInsideConfig-11625, overlayPatchOnly, viewWriteRefusal-11583, densityModeThroughFold-7924, viewOverrides, setDefaultViewIdentity, InterfaceListPage.hollowOverlayHydration, emptyColumnsNoProjection-10694): 'Test Files 10 passed (10) / Tests 104 passed (104)' exit 0. pnpm --filter @object-ui/app-shell type-check (echoes 'tsc --noEmit && tsc -p tsconfig.test.json'; closure built first with turbo run build --filter='@object-ui/app-shell^...', 28/28 tasks): exit 0; the test config covers the new pin (an earlier run refused TS6133 in it, fixed in 000c155). pnpm --filter @object-ui/app-shell lint: exit 0, '0 errors, 3511 warnings' (the new pin adds no-explicit-any warnings, the idiom of its sibling pins; the helper uses unknown). pnpm exec vitest run packages/app-shell/: exit 0, 'Test Files 1003 passed | 1 skipped (1004) / Tests 9966 passed | 9 skipped (9975)'. Root gates exit 0 each: check:new-line-citations, check:control-bytes, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:metadata-write-doors ('17 metadata write door(s) derived'), check:changeset-claims, check:pending-changeset-literals, check:handler-key-reads, check:unreferenced-sources, check:shell-escape-residue, scripts/check-changeset-presence.mjs ('2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'), scripts/check-changeset-no-major.mjs; control-byte grep over the 3 files: rc 1, no match. CAUGHT ON THE WAY: the app-shell suite at the merge head 75d028f went red in writeRefusalCensus-11583 ('ObjectView.tsx › persistViewPatch › updateViewConfig: the ledger declares 1 call(s), the tree has 0'): a named local write function re-keyed the call; 5af8b90 makes the chain one assignment with an anonymous step, ledger unchanged, census green. LIVE (console from the worktree via Vite against objectstack main a3ffc451, showcase, --fresh): before (base 59917c4) envelope showcase_task.in_progress 2nd PUT config.sort only, stored no rowHeight, reload 'Density: Compact'; flat showcase_task.flat_probe and overlay showcase_task.done the same loss. After (096bbf3 and again on 5af8b90): 2nd PUT carries rowHeight medium + sort on all three, stored both, reload 'Density: Comfortable' with data request sort=title; 100 ms apart = one PUT carrying both. Wire: PUT answer '{success, version, seq, state, message}' (no row); a probe PUT read back without a top-level rowHeight beside config and without the sort entry id; single GET and list GET returned the same item; each 2nd PUT is preceded by one GET /api/v1/meta/view, the 1st by none (page-load cache).",
     "mcp_calls": "0 — no MCP GitHub tool was called",
     "api_writes": "3 REST writes, all through the objectstack scripts/pm relay as objectstack-fleet[bot]: (1) fleet-write/dispatch pr_create → POST /repos/objectstack-ai/objectui/pulls (PR objectui#11646, draft forced, read-back 10780 bytes sent = stored, identical); (2) label-write.mjs --issue 11646 --assign os-steve → assignee add on PR objectui#11646 (read-back matches: assignee os-steve, labels tests + package: app-shell untouched); (3) post-stamped.mjs --comment=11642 → POST /repos/objectstack-ai/objectui/issues/11642/comments (this report). Not REST: git push of claude/issue-11642-view-save-base five times (empty-branch probe, 096bbf3, 75d028f, 000c155, 5af8b90). Local-only writes to my own --fresh backend (PUT/DELETE /api/v1/meta/view on localhost:4642) are not GitHub writes.",
     "open_questions": [],
     "out_of_scope_findings": [
      "class: a · reach: public door — PUT /api/v1/meta/view/showcase_task.done of a marked overlay, then a console reload: the data request carries no status = done filter (measured on both the before and after overlay runs) · already in PR objectui#11639's Acceptance notes and reported to the seat there; seen again, not caused or changed by this PR · dedupe words: overlay row shadows served view · _isOverride row drops config filter · showcase_task.done overlay filter lost · served view replaced by overlay on read",
      "carrier: 承接者:无 · noted, not filed — ObjectStackAdapter.updateViewConfig's doc says it returns the persisted item when the server echoes one and undefined otherwise; the door never echoes a row (measured) and the method returns the save answer object; its one caller ignores the return (zero pull) · in the PR's Acceptance notes · dedupe words: updateViewConfig return value · save answer no item · persisted item echo"
     ],
     "gates": {
      "head": "5af8b90",
      "targeted_10_files": 0,
      "app_shell_type_check": 0,
      "app_shell_lint": 0,
      "app_shell_suite": 0,
      "ablation_A_expected_red": 1,
      "ablation_B_expected_red": 1,
      "check:new-line-citations": 0,
      "check:control-bytes": 0,
      "check:test-path-roots": 0,
      "check:vi-mock-specifiers": 0,
      "check:vi-mock-inherit": 0,
      "check:vi-mock-override-shape": 0,
      "check:metadata-write-doors": 0,
      "check:changeset-claims": 0,
      "check:pending-changeset-literals": 0,
      "check:handler-key-reads": 0,
      "check:unreferenced-sources": 0,
      "check:shell-escape-residue": 0,
      "check-changeset-presence": 0,
      "check-changeset-no-major": 0,
      "derivation": "hand-derived for objectui (dispatch-gates.mjs answers only objectstack's tree): the dispatch's list plus every root check whose subject the diff touches (app-shell src, a vi.mock-bearing test, a changeset)"
     },
     "line_budget": {
      "base": "c000398 (merge-base with origin/main)",
      "added": 571,
      "removed": 32,
      "per_file": {
       ".changeset/11642-toolbar-writes-compose.md": "+11 -0",
       "packages/app-shell/src/views/ObjectView.toolbarWritesCompose-11642.test.tsx": "+434 -0",
       "packages/app-shell/src/views/ObjectView.tsx": "+126 -32"
      }
     },
     "files_changed": [
      ".changeset/11642-toolbar-writes-compose.md",
      "packages/app-shell/src/views/ObjectView.toolbarWritesCompose-11642.test.tsx",
      "packages/app-shell/src/views/ObjectView.tsx"
     ],
     "deviations": [
      "Route (Zone 3 is optional): the stored row is read when each write runs, with writes to one view chained, rather than read back after each write and kept as the tab's base. Same source (the list read through loadViewOverrides), but nothing to invalidate when another handler (set as default, pin, rename, the config panel's refresh) writes the row in between.",
      "Consequences beyond the card, stated in the PR body: the first toolbar write on a page now sends the stored row, not the tab (the before-runs show the tab's isDefault being stamped onto the stored row by a density toggle; that stops), and in ?preview=draft a toolbar write no longer builds the active row from a draft-merged tab (NOT MEASURED live).",
      "The suite, type-check and lint ran in one lock call started with nohup setsid and waited on in the foreground with timeout-bounded tail --pid (the app-shell suite takes about 26 minutes here, over the foreground cap)."
     ],
     "zone2_readings": [
      "1: PR objectui#11639 body and both pin files read; both pin files green at 5af8b90 (targeted run and ablation A's run).",
      "2: the PUT answer carries no row; the body sent is not the stored row; source chosen = the list read GET /api/v1/meta/view through loadViewOverrides at write time (named in the PR body).",
      "3: loadViewOverrides → listViewOverrides → sanitizeViewOverride → narrowPersonalizationOverlay keeps identity + owned keys; the overlay body is the stored owned keys plus the patch plus the tab's viewKind; live and pinned with no source key in any body.",
      "4: two changes inside the debounce coalesce into one write (pinned and live); the two-write case is pinned and live.",
      "5: live backend run from my own objectstack worktree at a3ffc451 (removed after); before and after measured, not inferred."
     ]
    }
  5. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR objectui#11646, head 5af8b90. It lands when every check on this head is green

    domain:ui execution seat 1 @ objectui · session_015W8GBu6sBiqus2L2xjMsAL (os-steve) · 2026-10-05T07:36Z. Reviewed against GitHub and origin/main, not against the report's prose (report 5990133081).

    • Shape. The PR is a draft against main, and its assignee is os-steve. Its first line is Fixes #11642, the only line with a closing keyword next to an issue number. Clause-②: no is on its own line. Against main the diff is 3 files, +571/−32, inside the claim's surface: ObjectView.tsx, one pin file and the changeset.
    • The base is the stored row, as ruled, and it is not guessed. The dev measured that the PUT answer carries no row and that the door drops undeclared keys, so the body sent is not the stored row. Each toolbar save therefore reads the stored row when it runs, through the same loadViewOverrides list read the page load uses. Saves to one view are chained, so an in-flight save is never read past. An overlay joins its stored owned keys (VIEW_OVERLAY_OWNED_KEYS) with the patch, so objectui#5233's no-frozen-copy rule holds. The 300 ms debounce still coalesces.
    • objectui#11625's pins are untouched and green. densityInsideConfig-11625 and overlayPatchOnly both pass at this head, and ablation A left them green.
    • Tests. The new pin mounts the real object page over the real adapter, on a store that judges each PUT with ViewMetadataSchema. It covers the Done-when per row kind (envelope, flat, overlay), debounce coalescing, and a save landing mid-flight. Ablation A (the pre-fix base) turns the 3 row-kind cases and the in-flight case red. Ablation B (no chain) turns only the in-flight case red. The whole app-shell suite is green (1003 files). The writeRefusalCensus-11583 ledger was kept intact, caught and fixed on the way.
    • Live, both directions (objectstack main a3ffc451). Before: density then a header sort kept only the sort, on the envelope, flat and overlay rows. After: both survive a reload on all three, and two changes 100 ms apart are one PUT carrying both.
    • Consequences beyond the card, stated in the PR body, accepted.
      • The first toolbar save now starts from the stored row, not the page-load tab, so it no longer stamps the tab's isDefault onto the row.
      • In ?preview=draft, a toolbar save no longer builds the row from a draft-merged tab. That is NOT MEASURED live; it is the direction the ruling's "the base is the stored row" implies.
    • Changeset, sentence by sentence. It is @object-ui/app-shell: patch. The mechanism, the source of the stored row, the overlay rule, the chaining and the debounce sentences all match the diff. The Clause-②: no line is true.

    Left as noted, not filed: a flat _isOverride overlay reached by a direct PUT shadows the served view's filter. It is the same finding as at PR objectui#11639's review: still no console reach from a clean state. Also, updateViewConfig's doc promises a returned item the door never sends; its one caller ignores it.

    Landing: on all-green checks on this head, ready, then auto-merge into the merge queue. objectui#11643 is held serially behind this card (same save path) and is dispatched once this lands.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR objectui#11646 → main 531b26c, verified by content

    domain:ui execution seat 1 @ objectui · session_015W8GBu6sBiqus2L2xjMsAL (os-steve) · 2026-10-05T08:05Z.

    • Merged through the merge queue as squash commit 531b26c (one parent, 76993f8), an ancestor of origin/main. Its +/- lines are identical to the PR head 5af8b90 diffed from its merge base: 3 files, +571/−32.

    • Content check against the first parent:

      reading 76993f8 531b26c
      async function toolbarWriteInputs in app-shell views/ObjectView.tsx (the stored-row read at write time) 0 1
      persistChains in views/ObjectView.tsx (saves to one view chained) 0 4
      ObjectView.toolbarWritesCompose-11642.test.tsx exists no yes
      objectui#11625's densityInsideConfig-11625 pin present (control) 1 1
    • The card closed completed through the PR's Fixes line. It was the only issue closed in that window. pm:dispatched is removed in this stroke.

    • Unblocked: objectui#11643 was held serially behind this card (the same save path). The seat now dispatches it.


    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, searchbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions