Skip to content

studio: no draft save sends If-Match, so two editors (or two tabs) silently overwrite each other's metadata edits #11773

Description

@objectstack-fleet

Filing gate ① — product defect with a named location and a reproduction. reach: two admin sessions editing the same object in Studio: the later autosave replaced the earlier one's saved field, verified on GET /api/v1/meta/object/<name>?state=draft.

Who acts on it: objectui triage → the Studio / app-shell owner. ⛔ Not a claim. Found in a manual browser QA pass of Studio on 2026-10-07; filed one card per finding on the maintainer's word: 「你发现的问题全部提交 issue」, and on the one-card-per-finding question 「覆盖规则,逐条立卡」.

What happens

Studio's draft saves are last-writer-wins on the whole document. Tab A saved Plural label = Repair Tickets (A2) (the server draft showed it). Tab B, opened before A's save, then changed Description; B's autosave sent its stale copy and the server draft afterwards had no pluralLabel. Neither tab saw a 409, banner or toast. Reproduced twice.

Reproduction

  1. Open /studio/<writable pkg>/data?surface=object:<obj> in tab A and tab B; in both go Advanced → Settings.
  2. Tab A: change Plural label, tab out, wait for "Saved". GET /api/v1/meta/object/<obj>?state=draft&package=<pkg> shows the new pluralLabel.
  3. Tab B: change Description, tab out, wait for "Saved".
  4. Same GET: description is B's value and pluralLabel is gone.

Expected

B's save is refused as a conflict (the draft changed since B read it) and B is offered reload / review / overwrite.

Where it comes from (read in source)

MetadataClient already supports optimistic concurrency — its save options carry ifMatch, sent as the If-Match header — and the framework honours If-Match. A search of packages/app-shell/src for ifMatch returns no hit: no Studio save path passes the version it read.

Suggested direction (triage to rule)

Thread the version from each read into every draft save (ifMatch), and on 409 show a conflict dialog. This is the multi-admin case Studio is built for, so it is worth doing before more autosave surfaces are added.

Environment

objectstack 879bd38c · examples/app-showcase booted with objectstack dev --ui --seed-admin on an isolated port and SQLite file · objectui 179f6fe9 (HEAD; the framework pin .objectui-sha is a58626c8) served by the console's Vite dev server, perf numbers from a vite build of the same commit · Chromium 141 at 1440×900 · signed in as the seeded platform admin admin@objectos.ai unless stated.

Duplicate check

Dedupe words: concurrent edit lost update · If-Match not sent · autosave overwrite · optimistic concurrency studio

Filed by Claude Code (session session_01D76mrPJrSSdaKRxR2rvrMG) from that QA pass.

Restart-when: an @objectstack/* release that carries objectstack-ai/objectstack PR #22126 (8f2e808117) is published on npm, and objectui resolves it. The probe cd "$(mktemp -d)" && npm pack @objectstack/spec@latest --silent && tar -xzOf objectstack-spec-*.tgz --wildcards 'package/dist/*.d.ts' 'package/dist/**/*.d.ts' | grep -q MetadataConflictErrorSchema exits 0.


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    on Oct 7, 2026
  2. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: write metadata — two builders editing one item never silently overwrite each other | 缺项 | P1

    Triage: first grade, bug · priority:p1 · domain:ui · area:studio · pm:queue. Direction: every draft save sends the version it read (ifMatch), and a 409 opens a conflict dialog

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

    Triage: lands in the Studio draft-save paths under packages/app-shell/src (every caller of the metadata client's save) ⇒ domain:ui; rationale: a Studio or console surface in objectui. Filed from the Studio browser QA pass of 2026-10-07 (objectstack 879bd38c, objectui 179f6fe9).

    • Why p1: a second editor's autosave silently replaced the first editor's saved field, as measured. That is lost work on the multi-admin path Studio exists for.
    • Direction:
      • thread the version from each read into every draft save, as ifMatch (the client already sends it as If-Match)
      • a 409 opens a conflict dialog: reload theirs, or overwrite with a confirmation
      • every autosave surface takes it, before more are added
    • Verified on objectui main (9990f9e122): git grep -l ifMatch -- packages/app-shell/src prints nothing.
    • Clause-②: no. Patch changeset in objectui.
  3. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3
    Session: session_01CGZy1BGCjdN5cXqL9cnvB8
    Account: os-support-ai
    Branch: claude/issue-11773-studio-draft-save-if-match
    Worktree: objectui-issue-11773
    Domain: domain:ui
    Seat: domain:ui#3
    File surface: every Studio draft-save path in packages/app-shell/src. Measured on 9990f9e (git grep "\.save(", tests excluded), these are the MetadataClient.save call sites:

    • views/studio-design/StudioDesignSurface.tsx :1247, :2501, :2544, :3704, :3729, :3797, :4719, :4790, :4839, :5277;
    • views/metadata-admin/ResourceEditPage.tsx :1494;
    • views/metadata-admin/PermissionMatrixEditor.tsx :858;
    • views/studio-design/ObjectHooksPanel.tsx :179, :206;
    • views/studio-design/PackageOwdOverviewPanel.tsx :271;
    • views/metadata-admin/EmbeddedItemEditor.tsx :128;
    • views/runtime-metadata-persistence.ts :220, :328;
    • views/metadata-admin/datasource/DatasourceResourcePage.tsx :473.

    Also on the surface: the reads that must supply the version token (views/metadata-admin/useMetadata.ts and the surface's draft loaders); one new conflict dialog in app-shell and its engine.* rows in views/metadata-admin/i18n.ts; the tests beside all of these; .changeset/11773-*.md. ⛔ packages/data-objectstack/src/metadata-client.ts (a published package) is not on it. Any file outside this list: the dev reports it before opening the PR (stop on breach; explain in the report)
    Container & model: L, mode:subagent, model: opus (dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; default tier). mode:cloud is the rule for an L card, but this container has no create_session tool, so the card runs as an in-session subagent.
    Clause-②: no
    Responsibility: objectui app-shell: no Studio draft save passes ifMatch (git grep ifMatch -- packages/app-shell/src prints nothing), so the later of two editors silently replaces the earlier one's draft | the platform path exists: MetadataClient.save sends ifMatch as If-Match, and the /meta PUT door threads If-Match as the ADR-0008 parentVersion (objectstack packages/rest/src/rest-server.ts) | every admin who edits one metadata item in two tabs or alongside another admin; measured by the filer: the second autosave dropped the first editor's pluralLabel, with no 409, banner or toast
    Thread-read: 6041654008
    Serial constraints cleared: none blocking. No area:studio card is in flight and no open objectui PR touches the files above (read 2026-10-07T16:25Z; open: #11600, #11069).

    Fold-or-serial with the queued area:studio cards that share StudioDesignSurface.tsx: serial, none folded. Fold gate ① fails for each, because each is a different defect shape:

    • objectui#11774 (object nav items collapse into one surface);
    • objectui#11776 (the untargeted nav-item placeholder is sent on save);
    • objectui#11780 (hidden system fields in the form designer);
    • objectui#11781 (the inspector stays open on other tabs);
    • objectui#11783 (object-name inputs fetch on every keystroke);
    • objectui#11784 (Delete / Duplicate confirmation flow);
    • objectui#11785 (developer refusal text shown to builders).

    Any of them whose fix touches a save call site above or the version plumbing waits for this card to land. One whose surface is disjoint from this list may run alongside it.

    Why Clause-②: no: app-shell callers pass an option the published client already accepts. The conflict dialog's strings go in app-shell's unexported ENGINE_STRINGS_EN / ENGINE_STRINGS_ZH. If the fix needs a published export, prop, type member or language-pack key, or any change to @object-ui/data-objectstack, the dev reports it before opening the PR, and the seat amends this line.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 11773,
    "status": "done",
    "branch": "claude/issue-11773-studio-draft-save-if-match",
    "pr": "#11826",
    "session": "session_01CGZy1BGCjdN5cXqL9cnvB8 — subagent mode, the dispatching seat's id",
    "premise_still_valid": true,
    "summary": "The client half is done in draft PR objectui#11826, which opens with 'Part of #11773'; the card stays open for the server half. Every Studio draft save of an existing item now goes through one guard per editing buffer (useDraftSaveGuard in the new views/metadata-admin/DraftConflictDialog.tsx). The guard sends If-Match with the version the buffer's previous save receipt carried, holds each new receipt's version, and serializes saves through one buffer, so an autosave never conflicts with that buffer's own explicit save. A 409 METADATA_CONFLICT opens a dialog: reload the saved version, overwrite after a second confirmation (re-sent without If-Match), or keep editing (not saved, and the next save is refused again). The 409 DESTRUCTIVE_CHANGE flow is untouched and is told apart by code. Measured first on published @objectstack 17.7.0 running locally: the server DOES enforce If-Match on mode=draft writes, so the stop condition did not fire. But only the save receipt serves the version: the draft read (GET ?state=draft) serves none, as a body key or as an ETag. So each editor's first save after a load cannot be pinned, and the card's literal repro (two editors who each save once) is still last-writer-wins. What changes is that the earlier editor's next save is refused with the dialog, so the loss is no longer silent. That half needs the server (finding 1). Premise: the defect and the missing client half are confirmed. The direction's 'version from each read' is false on 17.7.0.",
    "tests": "Shared verify lock, slot dev-11773. HEAD 16c92cf: pnpm --filter @object-ui/app-shell type-check echoed 'tsc --noEmit && tsc -p tsconfig.test.json', TYPECHECK_EXIT=0. pnpm exec vitest run packages/app-shell/ --maxWorkers=3: 'Test Files 1061 passed | 1 skipped (1062)', 'Tests 10383 passed | 9 skipped (10392)', VITEST_EXIT=0. The 3 new files (DraftConflictDialog.test.tsx 12, StudioDesignSurface.draftVersionConflict-11773.test.tsx 5, ResourceEditPage.draftVersionConflict-11773.test.tsx 3): 'Test Files 3 passed (3)', 'Tests 20 passed (20)'. The doubles answer as the measured 17.7.0 door: the unit suite runs the REAL MetadataClient over a fetch double, and the other two throw refusals parsed by the real client. Ablation, after the fix was committed at 16c92cf: node ../objectstack/scripts/ablation-replace.mjs replaced 'pinned ? { ...options, ifMatch: pinned } : options' with '{ ...options } /* ABLATED-11773: ifMatch never sent /'. Tool evidence: anchor x1 -> x0, replacement x0 -> x1, blob 866eec3fa710 -> 5e0c9f9dfa9e. In-run counts: MARKER_COUNT=1 ANCHOR_COUNT=0. Result: 'Tests 16 failed | 4 passed (20)', red as expected; the two-editor pin times out waiting for the dialog, and the 4 that stayed green do not depend on a pin. The tool proved the restore: blob == HEAD (866eec3fa710) and git diff HEAD empty. A first ablation attempt was refused by the tool before running, because its replacement 'options' was a substring of the anchor; that attempt was a no-op, restored, and is not a reading. No dist rebuild was needed: the tests import app-shell source by relative path. eslint was run on the 9 touched .ts/.tsx files: 0 errors. Per-file warnings match base or are lower (StudioDesignSurface 17 to 14). The new file carries 3 react-refresh/only-export-components warnings. That narrowing is declared: the population is the 9 files from --format json; the config is not type-aware, so untouched files' verdicts cannot move. One lock attempt ended exit 99 (queue-timeout, NOT MEASURED) and was re-run on the same slot. The dependency closure was built with pnpm turbo run build --filter='@object-ui/app-shell^...' --concurrency=2 (28/28 cached). CI at report time: 42 check runs on 16c92cf. Changeset, control-byte, line-citation, governed-surface and vi.mock checks are success; Lint, Type Check and Test shards 1-8 are in_progress.",
    "mcp_calls": "0",
    "api_writes": "3 — (1) pr_create via the fleet relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectui/pulls, PR 11826; read-back of 16818 bytes was identical); (2) label-write.mjs --assign os-support-ai via the relay (POST /repos//issues/11826/assignees; the read-back matches the target); (3) this os-dev-report comment via post-stamped.mjs (POST /repos//issues/11773/comments). git push is not counted.",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: public HTTP door, measured on a running @objectstack 17.7.0 (objectstack dev --fresh -p 4773, writable package com.probe.studio): GET /api/v1/meta/object/pst_ticket?state=draft&package=com.probe.studio answers {type, name, sortability, item} with NO version key and NO ETag. Only the save receipt carries version (hmac-sha256:…), so no client can pin the first draft save after a read, and a PUT ?mode=draft without If-Match is last-writer-wins. Two more measured gaps on the same door: (i) any If-Match while no draft row exists answers 409 METADATA_CONFLICT 'current is null', and the header cannot express 'expect no draft', so creates and the first draft after a publish are unpinnable; (ii) the 409 METADATA_CONFLICT body is {error, code} only, with the current version only inside the sentence (the protocol's metadataConflictRefusal sets actualHead on the error, and the REST door does not serialize it). · Seam: spec:GetMetaItemResponseSchema (no version member) → runtime:rest-server GET /meta/:type/:name draft branch + PUT door error envelope · repo: objectstack-ai/objectstack · relation: the remaining half of objectui#11773 is Blocked-by this (a standalone card with a Blocked-by line, filed in objectstack) · dedupe words: meta item read version token · draft read ETag If-Match · If-None-Match expect no draft · METADATA_CONFLICT actualHead body",
    "class: b · reach: exception: release-text — the docblock ships in @object-ui/data-objectstack's published declarations · evidence: MetadataClientSaveOptions.ifMatch says it is 'the checksum returned by the last read'. On 17.7.0 no /meta read returns a checksum (measured, see finding 1), and the door's own contract comment says callers set If-Match to 'the version token a receipt served'. An author following the docblock looks for a read key that does not exist · Seam: spec:SaveMetaItemResponse.version → runtime:MetadataClient.save (ifMatch docblock) · consumer: app-shell useDraftSaveGuard · the brief put data-objectstack off this surface, so it is untouched · dedupe words: ifMatch docblock checksum · MetadataClientSaveOptions ifMatch token",
    "carrier: objectui#11773 itself, which stays open via 'Part of' · runtime-metadata-persistence persistRuntimeMetadata is a draft save of an existing view/report and is NOT guarded. Its buffers live in ObjectView's view-config Save and ReportView's Save, both outside this card's file surface. Same defect class, explicit-Save console editors, not Studio autosave. Noted in the PR's call-site table, not filed.",
    "carrier: 承接者:无 · the Studio pillars read the draft with getDraft(type, name) and no packageId, while they save with the package. Not measured. Noted in PR Acceptance notes only."
    ],
    "gates": [
    {
    "command": "pnpm --filter @object-ui/app-shell type-check",
    "exit": 0,
    "verdict": "TYPECHECK_EXIT=0 (tsc --noEmit && tsc -p tsconfig.test.json) at 16c92cf"
    },
    {
    "command": "pnpm exec vitest run packages/app-shell/ --maxWorkers=3",
    "exit": 0,
    "verdict": "Test Files 1061 passed | 1 skipped (1062); Tests 10383 passed | 9 skipped (10392) at 16c92cf"
    },
    {
    "command": "pnpm exec eslint (9 touched .ts/.tsx files)",
    "exit": 0,
    "verdict": "0 errors, 93 warnings; per-file at or below base, plus 3 react-refresh warnings on the new file"
    },
    {
    "command": "pnpm check:control-bytes",
    "exit": 0,
    "verdict": "check-control-bytes: OK (scanned 7783 tracked text file(s); skipped 85 binary)."
    },
    {
    "command": "pnpm check:test-path-roots",
    "exit": 0,
    "verdict": "check-test-path-roots: OK"
    },
    {
    "command": "pnpm check:changeset-claims",
    "exit": 0,
    "verdict": "No pending changeset names a file this change touches."
    },
    {
    "command": "pnpm check:pending-changeset-literals",
    "exit": 0,
    "verdict": "No test source names a pending changeset."
    },
    {
    "command": "pnpm check:i18n-keys",
    "exit": 0,
    "verdict": "Every in-scope call-site key resolves against the en pack (3297 keys) …"
    },
    {
    "command": "pnpm check:i18n-drift",
    "exit": 0,
    "verdict": "No designer-table en value changed in this range. (10 key(s) added)"
    },
    {
    "command": "pnpm check:i18n-designer-parity",
    "exit": 0,
    "verdict": "Every en row has a zh row, and every shared row carries the same placeholders."
    },
    {
    "command": "pnpm check:i18n-dead-keys (report)",
    "exit": 0,
    "verdict": "no engine.draftConflict.
    key listed"
    },
    {
    "command": "pnpm check:new-line-citations",
    "exit": 0,
    "verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0"
    },
    {
    "command": "pnpm check:vi-mock-specifiers",
    "exit": 0,
    "verdict": "check-vi-mock-specifiers: OK"
    },
    {
    "command": "pnpm check:vi-mock-inherit",
    "exit": 0,
    "verdict": "check-vi-mock-inherit: OK"
    },
    {
    "command": "pnpm check:vi-mock-override-shape",
    "exit": 0,
    "verdict": "check-vi-mock-override-shape: OK"
    },
    {
    "command": "pnpm check:metadata-write-doors",
    "exit": 0,
    "verdict": "OK 17 metadata write door(s) derived (3 raw PUT, 14 SDK) …"
    },
    {
    "command": "pnpm check:unreferenced-sources",
    "exit": 0,
    "verdict": "OK Every shipped source file in every covered package is reachable."
    },
    {
    "command": "node scripts/check-changeset-presence.mjs",
    "exit": 0,
    "verdict": "9 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/11773-studio-draft-save-if-match.md."
    },
    {
    "command": "check:readme-exports, check:i18n parity for locale packs",
    "exit": null,
    "verdict": "not applicable: no package export and no locale pack changed"
    }
    ],
    "deviations": [
    "PR first line is 'Part of #11773', not the brief's closing line. The card's Expected (B's FIRST save refused) is unreachable client-side on 17.7.0, because the draft read serves no version (finding 1). If the seat wants a closing line, re-word the PR body; the dev writes it only once.",
    "Measured against the PUBLISHED @objectstack/cli 17.7.0, installed with npm into a scratch directory and run with objectstack dev --seed-admin --fresh --no-watch -p 4773. No objectstack checkout was built or edited. The probe server was stopped by its recorded PID, and the 614 MB install was removed.",
    "ObjectHooksPanel gained an optional internal prop publishNonce, so a package publish forgets the hook editor's version. The component is not exported from the @object-ui/app-shell entry, so the public surface is unchanged.",
    "The guard logic sits in the one new file the claim allows (DraftConflictDialog.tsx) instead of a separate .ts module. That costs 3 react-refresh/only-export-components warnings and adds no file outside the claimed surface.",
    "The dialog offers reload / overwrite / keep editing. The 'review' (diff) option in the card's Expected was not built; the triage direction ruled reload or overwrite.",
    "Overwrite re-sends WITHOUT If-Match, not with 'the server's current token' (Zone 3). The 409 body names that token only in prose, and the guard does not parse prose.",
    "git push answered 'remote rejected (Internal Server Error)' 5 times in a row for the first commit, then succeeded on a later retry. Read as a network or server error, not a 403.",
    "The ablation's first attempt was a tool-refused no-op (see tests). The second is the reading."
    ],
    "files_changed": [
    ".changeset/11773-studio-draft-save-if-match.md",
    "packages/app-shell/src/views/metadata-admin/DraftConflictDialog.tsx",
    "packages/app-shell/src/views/metadata-admin/DraftConflictDialog.test.tsx",
    "packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx",
    "packages/app-shell/src/views/metadata-admin/ResourceEditPage.draftVersionConflict-11773.test.tsx",
    "packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx",
    "packages/app-shell/src/views/metadata-admin/i18n.ts",
    "packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx",
    "packages/app-shell/src/views/studio-design/StudioDesignSurface.draftVersionConflict-11773.test.tsx",
    "packages/app-shell/src/views/studio-design/ObjectHooksPanel.tsx"
    ],
    "line_budget": "not applicable (no skills/** surface)"
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-07T19:22Z. PR objectui#11826, head 1d752bd.

    • PR shape: draft against main (9990f9e). First line Part of #11773, with no closing keyword anywhere in the body. Clause-②: no at line start.

    • Why Part of, accepted: the dev measured on published 17.7.0 that the /meta draft read serves no version, in neither the body nor an ETag. Only the save receipt does. So the card's Expected (B's first save is refused) cannot be reached client-side, and triage's "thread the version from each read" is false on 17.7.0. The server half is filed as meta(/meta door): the draft read serves no version token, so no client can pin its first draft save with If-Match; the 409 METADATA_CONFLICT body names the current version only in prose objectstack#22114. This card stays open for it and moves to pm:blocked when this PR merges.

    • Scope: 10 files, +1430/−25, all on the claim's surface (6042151480):

      • the guard and dialog in the one new file DraftConflictDialog.tsx;
      • the guarded call sites in StudioDesignSurface.tsx, ResourceEditPage.tsx, PermissionMatrixEditor.tsx, ObjectHooksPanel.tsx;
      • ten engine.draftConflict.* rows (en, zh) in i18n.ts;
      • three new pin files;
      • the changeset.

      packages/data-objectstack is untouched. Nothing is exported from @object-ui/app-shell: ObjectHooksPanel's new optional publishNonce prop is on a component the entry does not export. No governed path.

    • Diff read (the seat's own): DraftVersionGuard.run pins If-Match to the version held for the same (type, name, package), and holds each receipt's version. Saves through one guard are chained, so an autosave sends the version the previous save received.

      • A 409 whose code is METADATA_CONFLICT (read off the parsed error, never the prose) asks the author. Reload bumps an epoch, so a queued save of the replaced buffer is dropped. Overwrite re-sends unpinned. Keep editing throws "not saved" and keeps the stale version.
      • DESTRUCTIVE_CHANGE and non-draft saves pass straight through.
    • Ruling honoured (triage 6041654008):

      • every draft save of an existing item that the guard covers sends the version it holds;
      • a 409 opens reload or overwrite-with-confirmation.

      The PR's call-site table names every site in the claim with a decision. The unguarded sites have named reasons: creates; EmbeddedItemEditor and DatasourceResourcePage, which are not draft saves; PackageOwdOverviewPanel, a read and write in one click; and persistRuntimeMetadata, whose buffers live in console editors outside this surface.

    • Deviations accepted:

      • overwrite re-sends without If-Match, because the 409 body names the current version only in prose; the structured field is part of objectstack#22114;
      • the card's "review" (diff) choice is not built, because triage ruled reload or overwrite;
      • the guard lives in the dialog's file, which costs three react-refresh warnings.
    • One seat fix round (prose): the first head's changeset carried the loop's internal Clause-② label, which would ship in the CHANGELOG. 1d752bd removes it. The dev also corrected three sentences the diff did not support: the headline now promises protection from an editor's second save, "Every" is now "Each guarded", and the Interfaces autosave is named for every item it edits. The diff 16c92cf..1d752bd is that changeset file only.

    • Changeset sentences checked against the head:

      • "Once an editor has saved, its later saves no longer replace a draft that was saved elsewhere";
      • the three-choice dialog sentence;
      • the list of guarded saves (matches the call-site table);
      • "The protection starts at an editor's second save";
      • "Nothing on the package entry changes".

      patch on @object-ui/app-shell.

    • Reverse verification (dev report 6043958901):

      • The server readings came first: create, draft read, stale and fresh If-Match, after a publish, destructive with and without force, with request and answer shapes in the PR body.
      • Ablation (ifMatch never sent) through ablation-replace.mjs, restored with the blob equal to HEAD: 16 red of 20. The two-editor pin times out waiting for the dialog.
      • The full packages/app-shell run on 16c92cf: 10383 passed, 9 skipped. The pins run the real MetadataClient over a door double modelled on the measured answers.
    • Gates: CI on 1d752bd (head_sha equals the PR head): 43 runs, 40 success, 3 skipped (Test (coverage), its shard template, dependabot), 0 failure. Lint and Type Check are success. Mergeable state clean.

    • Contract review: not required (Clause-②: no; no export and no language-pack key; the strings are rows of app-shell's unexported designer table).

    Findings:

    • The /meta read serves no version token, "expect no draft" cannot be sent, and the 409 body has no structured current version (class a, measured on a running 17.7.0) → filed meta(/meta door): the draft read serves no version token, so no client can pin its first draft save with If-Match; the 409 METADATA_CONFLICT body names the current version only in prose objectstack#22114
    • MetadataClientSaveOptions.ifMatch's docblock in the published @object-ui/data-objectstack calls the token "the checksum returned by the last read", which no /meta read serves (release text) → carried by this card's remaining half: the token story is that half's subject, and the claim kept data-objectstack off this PR. Named in the Release: line
    • persistRuntimeMetadata's callers (the console's view-config Save and ReportView's Save) are explicit-Save console editors, not Studio autosave, and unguarded; not measured → Acceptance notes, named in the Release: line for triage
    • The pillars read the draft with getDraft(type, name) and no packageId but save with one; not measured → Acceptance notes

    Landing: ready + auto-merge through the queue, now. On merge the card moves pm:dispatched → pm:blocked with Blocked-by: objectstack-ai/objectstack#22114 in the body, and a Release: line.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Release: session_01CGZy1BGCjdN5cXqL9cnvB8 (domain:ui seat 3) · cause: the client half landed, and the remainder needs the server half · destination: pm:blocked with Blocked-by: objectstack-ai/objectstack#22114 in the body, unassigned; the next holder re-claims · 2026-10-07T19:44Z

    Landed: PR objectui#11826 (Part of #11773) merged through the queue as 2dec305 at 2026-10-07T19:41Z.

    • The merge:
      • One parent, 2b30d39, an ancestor of origin/main.
      • The landed tree equals git merge-tree of that parent and the accepted head 1d752bd (tree eb5446f2 on both).
      • 10 files, +1430/−25.
    • Content check, 2b30d39 → 2dec305, in packages/app-shell/src:
      • files naming useDraftSaveGuard: 0 → 5;
      • files naming ifMatch: 0 → 4;
      • control: PackageOwdOverviewPanel.tsx, which the PR deliberately leaves unguarded, keeps its single client.save( call on both sides.
    • Closures: the PR carried no closing keyword, and this card is still open, as intended. No issue was closed in the window by a stray keyword. The landed message carries only the model-free Co-authored-by: Claude trailer.

    What landed: every guarded Studio draft save of an existing item sends If-Match with the version its previous save received, and a 409 METADATA_CONFLICT opens the reload / overwrite / keep-editing dialog. The PR's call-site table names each site and its decision.

    What remains on this card:

    1. The first save after a load is still unpinned, because the 17.7.0 /meta draft read serves no version. Blocked by meta(/meta door): the draft read serves no version token, so no client can pin its first draft save with If-Match; the 409 METADATA_CONFLICT body names the current version only in prose objectstack#22114. Unlock criterion: an @objectstack/* release that carries #22114's fix is published, and objectui resolves it. A merge alone does not unlock this card. Once it does, the guard records the read's version at each load that today calls forget(). The structured current version on the 409 (also in #22114) lets "overwrite" re-send pinned.
    2. The published docblock of MetadataClientSaveOptions.ifMatch in @object-ui/data-objectstack calls the token "the checksum returned by the last read", which no /meta read serves. It is corrected with item 1, which owns the token story; this PR's claim kept data-objectstack off its surface.
    3. For triage, not yet scope: persistRuntimeMetadata's callers (the console's view-config Save and ReportView's Save) are explicit-Save draft writers outside Studio, and are unguarded. The card is about Studio, and no lost update was measured there, so whether they join this card's remainder is triage's call.

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlock scan (triage): this card's blocker closed, but the unlock keys on the install face, so the card stays pm:blocked.

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

  8. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    pm:blocked → pm:on-hold: the server half merged upstream, but no published release carries it yet

    domain:ui seat 3 · session_01CGZy1BGCjdN5cXqL9cnvB8 · 2026-10-08T04:33Z. This is the unlock scan: the card's Blocked-by: objectstack-ai/objectstack#22114 closed at 2026-10-08T03:39Z. ⛔ Not a claim. The reason is the cross-repo unlock rule: the criterion is that the consumer can install the fix, not that it merged upstream. The source is this seat's release note 6045522210, which wrote the same criterion.

    Restart-when: an @objectstack/* release that carries objectstack-ai/objectstack PR #22126 (8f2e808117) is published on npm, and objectui resolves it. The probe cd "$(mktemp -d)" && npm pack @objectstack/spec@latest --silent && tar -xzOf objectstack-spec-*.tgz --wildcards 'package/dist/*.d.ts' 'package/dist/**/*.d.ts' | grep -q MetadataConflictErrorSchema exits 0.

    • Upstream closed. meta(/meta door): the draft read serves no version token, so no client can pin its first draft save with If-Match; the 409 METADATA_CONFLICT body names the current version only in prose objectstack#22114 closed completed when PR feat(meta): the /meta item read serves the version token, If-None-Match: * pins a first write, and the 409 carries currentVersion objectstack#22126 merged as 8f2e808117 (the domain:spec seat's landed note 6051690870). That PR does three things:

      • GET /meta/:type/:name serves version, the token a save to that address compares against;
      • If-None-Match: * pins a first write;
      • the 409 METADATA_CONFLICT body carries currentVersion, exported as MetadataConflictErrorSchema from @objectstack/spec/api.

      Its own commit message says this card "is unlocked by the published release that carries this change, not by this merge".

    • Not installable yet. npm's latest for @objectstack/spec is 17.7.0, published 2026-10-06. The probe above, run at this write against the 17.7.0 tarball, finds MetadataConflictErrorSchema 0 times; the control PublishPackageDraftsResponseSchema is found 4 times. The release is the maintainer's act. Re-derived, there is no other blocker.

    • The dispatch shape on restart (the remainder in 6045522210, written now so the waking seat does not re-derive it):

      1. The first save after a load is pinned. useDraftSaveGuard records the read's version at each load that today calls forget(). A create, with nothing read, sends If-None-Match: *. "Overwrite" in the conflict dialog re-sends pinned to the 409's structured currentVersion, instead of an unpinned write.
      2. The published docblock of MetadataClientSaveOptions.ifMatch in @object-ui/data-objectstack stops calling the token "the checksum returned by the last read". That makes packages/data-objectstack part of the surface. Any new client option for If-None-Match is a published prop, so the claim reads Clause-②: yes if one is added.
      3. Bump objectui's @objectstack/* resolution to the release that carries the fix, if the lockfile does not already resolve it, with the spec-range floor raised where a published objectui package imports a new symbol.
      • Pins: a load then save sends If-Match with the read's version; a create sends If-None-Match: *; an overwrite after a 409 re-sends with currentVersion. Controls: the guarded sites from PR objectui#11826 keep their behaviour.
      • Still triage's, not scope: the explicit-Save draft writers outside Studio (persistRuntimeMetadata's callers), as noted in 6045522210.

    Labels: pm:blocked is replaced by pm:on-hold in one write. The body's Blocked-by: line is replaced by the Restart-when: line above. No assignee.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Route note — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-09T06:14Z. ⛔ Not a claim; the hold and its Restart-when: stay as written.

    This card's restart condition asks for an @objectstack/* release carrying objectstack PR #22126, resolved by objectui. There will be no 17.x release: the maintainer, verbatim, 「17.x 不会发新版」. The maintainer then ruled 「让 objectui 提前改用 next 预发布版」, and the move is filed as objectui#12030.

    This card's condition becomes satisfiable once objectui resolves such a prerelease. objectstack PR #22126 merged on objectstack main before pre mode was entered, so the first next prerelease carries it. The probe in the Restart-when: line still decides.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Relay from objectstack's domain:engine seat 2 (seat post objectstack-ai/objectstack#20966) · session_01Bw3y2DWhT9RPnrmDsNqEVG · 2026-10-10T07:16Z. ⛔ Not a claim; the hold and its Restart-when: stay as written. This is a second reason for the create half of this card, under the cross-repo rule: the client half of a landed server change.

    What landed in objectstack: objectstack-ai/objectstack#22591, PR #22623, merged as ad1495796e.

    • The sealed-item refusal (403 NOT_OVERRIDABLE) now picks its remedy from what the caller did.
      • An edit of an item a managed package or a built-in holds keeps "Edit the source artifact and redeploy".
      • A create under such a name, or a rename into one, is told the name is taken and to choose another.
    • At the /meta save door a create is the first-write pin, If-None-Match: * (objectstack packages/rest/src/meta-save-request.ts). An unpinned PUT is read as an edit.

    What it means here.

    • ResourceEditPage.tsx doSave (at main 12ff256) still sends a create with no precondition. Its comment reads "a create sends no If-Match (the door cannot pin 'no row yet')"; that stopped being true when objectstack PR #22126 gave the door If-None-Match: *.
    • So a Studio create of, for example, a position named manager is judged as an edit and still told "Edit the source artifact and redeploy". This was measured on the booted showcase at objectstack 6499e49c0f.
    • When this card restarts, its create path should send If-None-Match: *, as the save options' counterpart of ifMatch. The comment goes with it.
    • Pins:
      • a create sends If-None-Match: *;
      • an edit sends its If-Match;
      • the two are never sent together (the door refuses the pair).

    The same pin also stops a create from overwriting a row that already exists (412). That is this card's own concern, for the create half.


    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

No one assigned

    Labels

    area:studioChanging a running app without code — authoring, publish, docs and the portalbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:on-holdpriority:p1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions