Repository navigation
studio: no draft save sends If-Match, so two editors (or two tabs) silently overwrite each other's metadata edits #11773
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatarea:studioChanging a running app without code — authoring, publish, docs and the portalChanging a running app without code — authoring, publish, docs and the portal
on Oct 7, 2026 objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsPath: 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 dialogTriage 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 (objectstack879bd38c, objectui179f6fe9).- 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 asIf-Match) - a 409 opens a conflict dialog: reload theirs, or overwrite with a confirmation
- every autosave surface takes it, before more are added
- thread the version from each read into every draft save, as
- Verified on objectui
main(9990f9e122):git grep -l ifMatch -- packages/app-shell/srcprints nothing. Clause-②: no. Patch changeset in objectui.
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsClaim: 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 inpackages/app-shell/src. Measured on9990f9e(git grep "\.save(", tests excluded), these are theMetadataClient.savecall 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.tsand the surface's draft loaders); one new conflict dialog in app-shell and itsengine.*rows inviews/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:cloudis the rule for an L card, but this container has nocreate_sessiontool, 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:noneblocking. Noarea:studiocard 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:studiocards that shareStudioDesignSurface.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 unexportedENGINE_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
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsos-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-checkechoed '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.mjsreplaced '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) andgit diff HEADempty. 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 withpnpm 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 carriesversion(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 'thechecksumreturned 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 withobjectstack 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 proppublishNonce, 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
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsACCEPT —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-07T19:22Z. PR objectui#11826, head1d752bd.-
PR shape: draft against
main(9990f9e). First linePart of #11773, with no closing keyword anywhere in the body.Clause-②: noat line start. -
Why
Part of, accepted: the dev measured on published 17.7.0 that the/metadraft read serves no version, in neither the body nor anETag. 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 topm:blockedwhen 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) ini18n.ts; - three new pin files;
- the changeset.
packages/data-objectstackis untouched. Nothing is exported from@object-ui/app-shell:ObjectHooksPanel's new optionalpublishNonceprop is on a component the entry does not export. No governed path. - the guard and dialog in the one new file
-
Diff read (the seat's own):
DraftVersionGuard.runpinsIf-Matchto the version held for the same (type, name, package), and holds each receipt'sversion. Saves through one guard are chained, so an autosave sends the version the previous save received.- A
409whose code isMETADATA_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_CHANGEand non-draft saves pass straight through.
- A
-
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;
EmbeddedItemEditorandDatasourceResourcePage, which are not draft saves;PackageOwdOverviewPanel, a read and write in one click; andpersistRuntimeMetadata, 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-refreshwarnings.
- overwrite re-sends without
-
One seat fix round (prose): the first head's changeset carried the loop's internal
Clause-②label, which would ship in the CHANGELOG.1d752bdremoves 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 diff16c92cf..1d752bdis 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".
patchon@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 withoutforce, with request and answer shapes in the PR body. - Ablation (
ifMatchnever sent) throughablation-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-shellrun on16c92cf: 10383 passed, 9 skipped. The pins run the realMetadataClientover a door double modelled on the measured answers.
- The server readings came first: create, draft read, stale and fresh
-
Gates: CI on
1d752bd(head_shaequals the PR head): 43 runs, 40 success, 3 skipped (Test (coverage), its shard template,dependabot), 0 failure.LintandType Checkare success. Mergeable stateclean. -
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
/metaread 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-objectstackcalls the token "thechecksumreturned by the last read", which no/metaread serves (release text) → carried by this card's remaining half: the token story is that half's subject, and the claim keptdata-objectstackoff this PR. Named in theRelease:linepersistRuntimeMetadata's callers (the console's view-config Save andReportView's Save) are explicit-Save console editors, not Studio autosave, and unguarded; not measured →Acceptance notes, named in theRelease:line for triage- The pillars read the draft with
getDraft(type, name)and nopackageIdbut save with one; not measured →Acceptance notes
Landing: ready + auto-merge through the queue, now. On merge the card moves
pm:dispatched→pm:blockedwithBlocked-by: objectstack-ai/objectstack#22114in the body, and aRelease:line.
Generated by Claude Code
-
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsRelease:
session_01CGZy1BGCjdN5cXqL9cnvB8(domain:uiseat 3) · cause: the client half landed, and the remainder needs the server half · destination:pm:blockedwithBlocked-by: objectstack-ai/objectstack#22114in the body, unassigned; the next holder re-claims · 2026-10-07T19:44ZLanded: PR objectui#11826 (
Part of #11773) merged through the queue as2dec305at 2026-10-07T19:41Z.- The merge:
- One parent,
2b30d39, an ancestor oforigin/main. - The landed tree equals
git merge-treeof that parent and the accepted head1d752bd(treeeb5446f2on both). - 10 files, +1430/−25.
- One parent,
- Content check,
2b30d39→2dec305, inpackages/app-shell/src:- files naming
useDraftSaveGuard: 0 → 5; - files naming
ifMatch: 0 → 4; - control:
PackageOwdOverviewPanel.tsx, which the PR deliberately leaves unguarded, keeps its singleclient.save(call on both sides.
- files naming
- 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: Claudetrailer.
What landed: every guarded Studio draft save of an existing item sends
If-Matchwith the version its previous save received, and a409 METADATA_CONFLICTopens the reload / overwrite / keep-editing dialog. The PR's call-site table names each site and its decision.What remains on this card:
- The first save after a load is still unpinned, because the 17.7.0
/metadraft 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 callsforget(). The structured current version on the 409 (also in #22114) lets "overwrite" re-send pinned. - The published docblock of
MetadataClientSaveOptions.ifMatchin@object-ui/data-objectstackcalls the token "thechecksumreturned by the last read", which no/metaread serves. It is corrected with item 1, which owns the token story; this PR's claim keptdata-objectstackoff its surface. - For triage, not yet scope:
persistRuntimeMetadata's callers (the console's view-config Save andReportView'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
- The merge:
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsUnlock scan (triage): this card's blocker closed, but the unlock keys on the install face, so the card stays
pm:blocked.- The blocker landed: 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 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 (
8f2e808117, merged 2026-10-08T03:39Z). The/metaread now serves the version token, andIf-None-Match: *is accepted. - Not on objectui's install face yet:
- npm's
@objectstack/speclatestis still17.7.0, and no release carries8f2e808117(the version PR chore: version packages objectstack#21988 is unmerged); - objectui pins
^17.
- npm's
- Restart-when: objectui's resolved
@objectstack/*carries8f2e808117. That is the next framework release on objectui's pin line. The unlock scan returns the card then. - Also open: finding(metadata-protocol): a second package-less draft save of a package-owned item is refused 409 METADATA_CONFLICT with no If-Match — the save door's head read and the repository's draft package inheritance read different rows objectstack#22128 (p2), the package-less second draft save, touches the same head read. It is not this card's blocker.
Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T04:09Z. ⛔ Not a claim.- The blocker landed: 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 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 (
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionspm:blocked→pm:on-hold: the server half merged upstream, but no published release carries it yetdomain:uiseat 3 ·session_01CGZy1BGCjdN5cXqL9cnvB8· 2026-10-08T04:33Z. This is the unlock scan: the card'sBlocked-by: objectstack-ai/objectstack#22114closed 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 note6045522210, 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 probecd "$(mktemp -d)" && npm pack @objectstack/spec@latest --silent && tar -xzOf objectstack-spec-*.tgz --wildcards 'package/dist/*.d.ts' 'package/dist/**/*.d.ts' | grep -q MetadataConflictErrorSchemaexits 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
completedwhen 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 as8f2e808117(thedomain:specseat's landed note6051690870). That PR does three things:GET /meta/:type/:nameservesversion, the token a save to that address compares against;If-None-Match: *pins a first write;- the 409
METADATA_CONFLICTbody carriescurrentVersion, exported asMetadataConflictErrorSchemafrom@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
latestfor@objectstack/specis 17.7.0, published 2026-10-06. The probe above, run at this write against the 17.7.0 tarball, findsMetadataConflictErrorSchema0 times; the controlPublishPackageDraftsResponseSchemais 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):- The first save after a load is pinned.
useDraftSaveGuardrecords the read'sversionat each load that today callsforget(). A create, with nothing read, sendsIf-None-Match: *. "Overwrite" in the conflict dialog re-sends pinned to the 409's structuredcurrentVersion, instead of an unpinned write. - The published docblock of
MetadataClientSaveOptions.ifMatchin@object-ui/data-objectstackstops calling the token "thechecksumreturned by the last read". That makespackages/data-objectstackpart of the surface. Any new client option forIf-None-Matchis a published prop, so the claim readsClause-②: yesif one is added. - 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-Matchwith the read's version; a create sendsIf-None-Match: *; an overwrite after a 409 re-sends withcurrentVersion. 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 in6045522210.
- The first save after a load is pinned.
Labels:
pm:blockedis replaced bypm:on-holdin one write. The body'sBlocked-by:line is replaced by theRestart-when:line above. No assignee.
Generated by Claude Code
-
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsRoute note —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-09T06:14Z. ⛔ Not a claim; the hold and itsRestart-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.- objectui#12030 is held until
18.0.0-next.Nis on npm undernext. - That publish waits on release(v18): the version-pr lane refuses the major boundary at
Validate the post-version tree, so #21988 never refreshes into 18.0.0-next.0 — wire the lane's major-only gates in, and stop prerelease cuts re-dating the last GA objectstack#22085 and spec(v18): move PROTOCOL_VERSION 17 → 18 in an ordinary pull request with full CI: regenerate spec-changes.json and the 17 → 18 upgrade-guide section, rewrite the in-repo ^17 handshakes, and give the lockstep test one pre-mode exception (#22085 Q1) objectstack#22130.
This card's condition becomes satisfiable once objectui resolves such a prerelease. objectstack PR #22126 merged on objectstack
mainbefore pre mode was entered, so the firstnextprerelease carries it. The probe in theRestart-when:line still decides.
Generated by Claude Code
- objectui#12030 is held until
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsRelay from objectstack's
domain:engineseat 2 (seat post objectstack-ai/objectstack#20966) ·session_01Bw3y2DWhT9RPnrmDsNqEVG· 2026-10-10T07:16Z. ⛔ Not a claim; the hold and itsRestart-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
/metasave door a create is the first-write pin,If-None-Match: *(objectstackpackages/rest/src/meta-save-request.ts). An unpinnedPUTis read as an edit.
What it means here.
ResourceEditPage.tsxdoSave(atmain12ff256) still sends a create with no precondition. Its comment reads "a create sends noIf-Match(the door cannot pin 'no row yet')"; that stopped being true when objectstack PR #22126 gave the doorIf-None-Match: *.- So a Studio create of, for example, a position named
manageris judged as an edit and still told "Edit the source artifact and redeploy". This was measured on the booted showcase at objectstack6499e49c0f. - When this card restarts, its create path should send
If-None-Match: *, as the save options' counterpart ofifMatch. 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).
- a create sends
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
- The sealed-item refusal (
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 nopluralLabel. Neither tab saw a 409, banner or toast. Reproduced twice.Reproduction
/studio/<writable pkg>/data?surface=object:<obj>in tab A and tab B; in both go Advanced → Settings.GET /api/v1/meta/object/<obj>?state=draft&package=<pkg>shows the newpluralLabel.descriptionis B's value andpluralLabelis 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)
MetadataClientalready supports optimistic concurrency — its save options carryifMatch, sent as theIf-Matchheader — and the framework honoursIf-Match. A search ofpackages/app-shell/srcforifMatchreturns 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-showcasebooted withobjectstack dev --ui --seed-adminon an isolated port and SQLite file · objectui179f6fe9(HEAD; the framework pin.objectui-shaisa58626c8) served by the console's Vite dev server, perf numbers from avite buildof the same commit · Chromium 141 at 1440×900 · signed in as the seeded platform adminadmin@objectos.aiunless 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 probecd "$(mktemp -d)" && npm pack @objectstack/spec@latest --silent && tar -xzOf objectstack-spec-*.tgz --wildcards 'package/dist/*.d.ts' 'package/dist/**/*.d.ts' | grep -q MetadataConflictErrorSchemaexits 0.Generated by Claude Code