Repository navigation
metadata-protocol: a package-less direct save of an item a package owns answers 200 and writes a package-less row the item read never serves; the draft save already resolves the owning package (#11087 / #22128) #23165
Description
Filing gate ①, class (b): the save door reports success for a write the read never serves. reach: the public door PUT /api/v1/meta/:type/:name with no ?package=, measured on a booted showcase by the browser pass behind objectstack-ai/objectui#12300 (case 2, two fresh loads), at objectstack 36b2889c.
Filed by the triage seat (seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U, splitting out the server half that objectstack-ai/objectui#12300 names ("whether the server should refuse this write is that repo's call, and triage may split it out"). ⛔ Not a claim.
What happens
- Setup: a writable package
com.qa.lanebappownsapp/lanebapp_app, saved with?package=com.qa.lanebapp. - The write:
PUT /api/v1/meta/app/lanebapp_appwith{...app, active: false}and no?package=answers 200, "Saved app 'lanebapp_app' (env-wide, state=active)". - What is stored:
sys_metadatanow holds twolanebapp_approws. One is the package row (active: true), and the read serves it. The other is a new package-less env-wide row (active: false), and the read never serves it. - What the read says:
GET /api/v1/meta/app/lanebapp_appstill readsactive: truewith_packageId: com.qa.lanebapp. - The client half (the console omits
?package=) is console Setup › Apps: Delete and Disable report success for writes that changed nothing — handleDelete ignores reset:false, and handleToggleActive saves a package-owned app with no package scope objectui#12300. This card is the door's half: any client gets the same silent 200, including the API, the CLI and agents.
Governing text
- 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 #22128 (landed, PR fix(metadata-protocol): one head row per /meta write address — a second unpinned package-less draft save is accepted (#22128) #22185 →
3b493184e1): "One write-address resolution".SysMetadataRepository.resolveWriteHead/headAtgives the row a save at an address upserts, "with SqlDriver org auto-scope makes SysMetadataRepository.listDrafts blind to env-wide drafts (organization_id IS NULL ∧ injected org = always empty); mode=draft saves drop package_id #11087's draft package inheritance kept". A package-less draft save of a package-owned item therefore binds to the owning package. - The defect: the direct (non-draft) save at the same address does not take that resolution. It mints a package-less row beside the package's.
Direction (triage)
- One resolution for both save modes. A package-less direct save resolves its write address the way the draft save does (
headAt).- The item is bound to exactly one package: the save writes that package's row, and the answer names it.
- The item is bound to more than one package: the save is refused with a typed error naming the candidates, and the remedy is
?package=. - ⛔ Never a 200 that writes a row the item read does not serve.
- An item with no package binding keeps today's env-wide row, unchanged.
- Clause ②: this narrows what the door accepts (the ambiguous case is refused), so it is
Clause-②: no. The card stays in its lane, and owes a contract-tier review before it is queued. - Pins:
- the write → read round trip: what a package-less save of a single-package item writes is what the item read serves next;
- the ambiguous case is refused, by code;
- the package-less-item control is unchanged;
- the draft-save path's existing pins hold.
- Existing rows: package-less rows already minted beside a package's row stay as they are. ⛔ No conversion here. If one is measured served wrongly, that measurement is its own card.
Grade (triage)
bug · priority:p2 · domain:engine · area:studio, into pm:queue. Why p2: a reported success that is false, on the public door, and the reason a console "Disable" shows success while nothing changes. No data is lost. Lane: packages/metadata-protocol is domain:engine in the domain table.
Dedupe
MCP search_issues on objectstack-ai/objectstack: "metadata save without package scope on a package-owned item writes a package-less row the read never serves, answers 200" returned 37 hits. None is open for this shape. The nearest, all closed, are #22128 (the draft-save head resolution), #21638 (a package-less view container saved under a shipped item's name), #21817 (the package-scoped list fallback) and #22057.
Dedupe words: package-less direct save package-owned item; save door unserved row 200; resolveWriteHead direct save; ?package= omitted write.
Activity
objectstack-fleet commentedon Oct 13, 2026
Claim: PM loop round 1 · 2026-10-13T06:41Z
Session:
session_01P88ehrZ6hM19hBzdpsQTToAccount:
os-support-aiBranch:
claude/issue-23165-direct-save-resolves-packageWorktree:
objectstack-issue-23165Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
packages/metadata-protocol/src/protocol.ts, region level:saveMetaItem's write-address step for a direct (non-draft) save, whererequest.packageIdfeeds the lock parent (headAt) andrepo.put. Pluspackages/metadata-protocol/src/sys-metadata-repository.ts(resolveWriteHead/headAt), only if the resolution is shared there. Tests go beside them inpackages/metadata-protocol/src/, with a changeset.changeset/23165-*.md. The dev stops on a breach and explains it in the report.Container & model:
M,mode:subagent,model: default(dispatch-gates --tierat881421194: no path-derived mandate)Clause-②: no
Responsibility:
saveMetaItem's direct-save arm inpackages/metadata-protocol/src/protocol.ts, withSysMetadataRepository.resolveWriteHead(this lane) | none for a direct save: the draft save already resolves the owning package (#11087 / #22128), but only forstate: 'draft'| any client ofPUT /api/v1/meta/TYPE/NAMEwith no?package=on a package-owned item: the console (objectstack-ai/objectui#12300), the API, the CLI and agents. Measured on a booted showcase by the browser pass behind objectstack-ai/objectui#12300.Thread-read: none
Serial constraints cleared: #23119 has landed (PR #23167 →
aa5bfdfed8), and seat 2 holds nosaveMetaItemregion now (6139770903). #23015 (domain:specseat 1, branch tip498957f10) adds a record-page refusal insaveMetaItem's validation preamble, besideruntimeHookWithoutBodyRefusal(basee540e7ffbline ~21147). That is a different block of the same function; the later lander mergesmain. #23056 S8 (engine seat 2) holds only theUninstallCleanuphunk. This seat's #23149 holds the delete path's registry heal, which is disjoint. No open PR touches a non-testmetadata-protocolsource at this read. No path here is aSINGLE_CLAIM_PATHSentry. Read againstorigin/main881421194.On
Clause-②: no: the card's own reading. The fix narrows what the door accepts: the ambiguous case is refused. It owes one contract-tier review before the queue. If the refusal needs an error code not already registered inERROR_CODE_LEDGER, the dev stops and reports: a new code isClause-②: yes, and the card would move.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{ "issue": 23165, "status": "needs_decision", "branch": "claude/issue-23165-direct-save-resolves-package", "pr": null, "session": "session_01P88ehrZ6hM19hBzdpsQTTo (subagent, the parent PM's harness-stamped id)", "premise_still_valid": true, "summary": "Stopped on the PM correction: no PR. The branch is pushed at cd7d91390 as the resume point (option B, below) and nothing was rewritten. PREMISE, re-measured at BASE 881421194 through the real save, read, list and delete doors (stub engine that keeps rows): saving app/lanebapp_app with packageId com.qa.lanebapp and then saving it with no package answers 200 'Saved app ... (env-wide, state=active)'. That leaves two active rows: the package's (active absent, so true) and a new package-less one (active:false). Every read door serves the package's row: the read naming no package, the read naming the package, the layered read, the unscoped list and the package-scoped list. The read naming no package is also internally split. Its body comes from the package's row (package-agnostic findOne, first in storage order), and its version token comes from the package-unbound row. Before the second save that token is null. HYPOTHESES. H1 confirmed: with no ?package= (or empty or 'all'), REST (metaItemPackageBinding) and the dispatcher send no packageId. saveMetaItem passes undefined to repo.put and null to storedHeadAt/headAt, and resolveWriteHead maps both to the unbound row for state active. H2 confirmed: promoteDraft and restoreVersion (explicit null for a package-less row), the stored-metadata migration and the flow-credential move (null plus source) all mean 'the unbound row', so the repository default must not change. The branch resolves at the save door and leaves SysMetadataRepository untouched. H3: an item a code package ships, with no stored row, behaves the same at BASE, under A and under B: the package-less save writes the unbound overlay and every door serves it. Once an overlay row is bound to the code package (written with ?package=com.example.showcase), it behaves exactly like the stored-package item: BASE mints a second row that no door serves. So code-package and stored-package items differ only in whether a package-bound row exists. H4: the two-package state is reachable through the door. Two ?package= saves leave two active rows, and nothing refuses that (only object D9.9). At BASE a package-less save then mints a third row that no door serves. The by-name read serves the first package's row, and the list serves both package slots. No ERROR_CODE_LEDGER code registered for metadata-protocol fits. The branch uses the standard-catalog RESOURCE_CONFLICT/409, which this package already emits; it is not a ledger row and no code was minted. H5: draft path unchanged, and its existing pins hold. With two packages, a package-less draft binds to the first active row in storage order, at BASE and on the branch. H6: the branch receipt names the package. OPTION A, measured as a one-line prototype on BASE (never committed): the read naming no package serves the package-unbound row first when one exists. It changes only the by-name read family (findServedOverlayRow: getMetaItem active and previewDrafts, getMetaItemLayered) and addressPackages(undefined) for the rowless view expansion. Unchanged under A: the list (a package slot still serves the package's own row before the package-less one), the read naming the package, the version token (already the unbound address), delete and the lock. Results under A: lanebapp read naming no package serves active:false with that row's own token, but GET /meta/app and the read naming the package still serve active:true. The code-package view behaves the same way ('Unnamed' by name, 'Named' in the list). The whole metadata-protocol suite stays green under A (232 files, 28,278 tests), so no pin covers the read naming no package when it has two rows to pick from. OPTION B (the branch): every door agrees for a single-binding item, with the version from the same row. The two-package save is refused 409 and writes nothing. One residue order still disagrees: a package-less row stored BEFORE the package row (S6). There the read naming no package keeps serving that row while the save goes to the package's row. files_changed (BASE..cd7d91390): packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/protocol.packageless-save-address.test.ts (new), packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts, packages/spec/src/api/protocol.zod.ts, .changeset/23165-direct-save-resolves-package.md. line_budget: 5 files, +657 -6 (protocol.ts +178 -6). deviations: (1) packages/spec/src/api/protocol.zod.ts (the SaveMetaItemRequestSchema.packageId describe) was edited at cd7d91390. That is outside the claim's surface, and per the PM correction it is a protocol change. It stays in place as the resume point, not regenerated, so check:docs is stale. Revert it on resume unless B becomes its own spec card. (2) One npx tsc --listFiles run (to count the new test in the typecheck program) ran outside the verify lock. (3) The branch changeset says 'Clause-②: no (narrowing)', copied from the claim. B also flips one request class from 409 to 200: a package-less save carrying the package row's If-Match token. #22128 declared that kind of flip 'yes (widening)', so B honestly reads 'Clause-②: yes (narrowing)'. (4) The two-package refusal uses the standard-catalog RESOURCE_CONFLICT, not a code from metadata-protocol's ledger row as the dispatch's hypothesis 4 framed it. No code was minted. (5) The new test file adds a fake engine without the ledger write, so check:engine-double-contract is red on the branch (fix: node scripts/check-engine-double-contract.mjs --write, or fold the tests into an existing double's file). (6) The showcase was not booted. On a fresh boot its items are artifacts of the booted code package com.example.showcase, so there is no package-bound stored row and the count of minted package-less rows is 0 by construction. The real population is in environments where someone saved a package's item without ?package=, and that is not countable from here.", "tests": "gates (at cd7d91390, check:* run outside the lock as the role file says): check:nul-bytes EXIT=0 'check-nul-bytes: OK (scanned 11195 ...)'; check:engine-double-contract EXIT=1 'RETAINED [delete|findOne|update]: packages/metadata-protocol/src/protocol.packageless-save-address.test.ts pins 1 engine double(s) that the pinned ledger does not record ... Run --write'; check:where-matcher EXIT=0; check:objectql-double-limit EXIT=0; check:durability-log-level EXIT=0 ('86 read seam(s), none invents an unreported answer'); check:error-status-conformance EXIT=0; check:dispatcher-error-vocabulary EXIT=0; check:doc-authoring EXIT=0; check:issue-citations EXIT=0; check-adr-0087-registration --base origin/main EXIT=0 (exemption no-migration-prescription accepted); check-changeset-no-major EXIT=0; check-empty-changeset EXIT=0; check:test-source-alias EXIT=0; check:cross-package-test-inputs EXIT=0. @objectstack/spec check:generated EXIT=1: 'x check:docs content/docs/references/**' stale from the describe edit, not regenerated per the PM stop. NOT MEASURED: the other 74 of the 88 commands from 'dispatch-gates --commands --repo objectstack-ai/objectstack' at cd7d91390 (reason: no PR per the PM stop; list in scratchpad gates-final.txt). Tests at cd7d91390 (verify lock): metadata-protocol typecheck EXIT=0. vitest over 8 files (packageless-save-address, org-scoped-write-refused, draft-package-inherit, save-receipt-wording, save-door-lock-family, served-content-hash, list-slot-prefer-local, lock-one-resolution): 'Test Files 8 passed (8) Tests 24521 passed'. Full metadata-protocol suite at 0179aba67: 'Test Files 1 failed | 232 passed | 3 skipped; Tests 2 failed | 28290 passed'. The 2 failures were the org-scoped order pin's setup, which relied on a package-less save minting a second row; it was triaged (spelling change: that copy is now written by the server-stated rewrite face) and re-run green. The full suite was not re-run at cd7d91390 (NOT MEASURED: full suite, reason: 9-minute run against the foreground cap after the stop). Consumers, using the rebuilt metadata-protocol dist (a dist-built consumer resolves through dist, so it was rebuilt first and the marker was counted in dist/index.js = 5): objectql 37 package-related save files 'Test Files 37 passed, Tests 499 passed' (at 0179aba67-era protocol). plugin-security 5 files (aliased to src) 'Test Files 5 passed, Tests 120 passed'. Reverse verification (fix committed first, trap restore, restore proven by blob==HEAD and git diff HEAD empty): protocol.ts and sys-metadata-repository.ts were swapped back to their BASE blobs (hash-object == BASE blob checked), and the new pins ran: predicted 9 red / 5 green, measured 'Tests 9 failed | 5 passed (14)', ordinary-red direction. Option-A blast radius: BASE protocol.ts plus the prototype, applied through scripts/ablation-replace.mjs ('anchor 1 -> 0, blob e5f31f6faedd -> 35191ac00392'), restored ('blob == HEAD (c27596871c22) and git diff HEAD is empty'). Results: half 1 'Test Files 115 passed | 3 skipped, Tests 25923 passed'; half 2 'Test Files 117 passed, Tests 2355 passed'. Probe measurements (uncommitted probe file, deleted): BASE / A / B rows and the answer of every door for S1 lanebapp, S2 code-package view with a bound overlay, S3 code-package view with no stored row, S5 two packages, S6 package-less row stored first; the delete measurements D1 and D2 are in out_of_scope_findings.", "mcp_calls": "0", "api_writes": "1 REST write: POST /repos/objectstack-ai/objectstack/issues/23165/comments (this os-dev-report, via scripts/pm/post-stamped.mjs on the fleet relay). Six git pushes to claude/issue-23165-direct-save-resolves-package (the empty probe, then 43e63ab6b, 0179aba67, f95a49fc6, 095bc3d4e and cd7d91390). No pr_create, no label-write.", "open_questions": [ { "question": "Which contract wins for a direct (active) save that names no package, on an item whose active rows are bound to a package? The published SaveMetaItemRequestSchema.packageId text says 'Absent or null names no package: an active save writes the env-local overlay (no package stamp) ... the item read at the same scope serves it as version'. Today's write matches that text, and the package-less read's body does not (its token does).", "options": [ "A - the code aligns to the published text. The read naming no package serves the package-unbound row when one exists, a one-line change to servedOverlayRowCandidates for an absent package. Measured: by-name read and its token then come from one row, and lanebapp reads active:false. But GET /meta/TYPE (unscoped list) and every read naming the package keep serving the package's row (active:true), because a package slot prefers its own row (ADR-0048). The card's console 'Disable' therefore shows disabled on the item page and enabled in the list. The registry's hydrated copy is NOT MEASURED. Code-package and stored-package items behave identically. No existing pin moves (232 files green). It mints a meaning for 'an env overlay over a stored package's row', and the package-less write stays a second row. Four axes: business need - does not deliver the console's or an API caller's intent (the list and the package scope still serve the old row). Long-term - formalises the by-name/list split and the second row, which is a workaround shape. AI-error - an AI that omits ?package= gets a by-name read that echoes its write while the list disagrees, a silent half-success that hides the error. Startup focus - smallest diff, in lane, no new capability, but it does not close the defect.", "B - the branch at cd7d91390. A package-less direct save resolves the one bound package and writes that row, as a save naming it does; two or more bindings answer 409 RESOURCE_CONFLICT naming them; no binding keeps the env-local overlay. The read's token takes the same resolution. Measured: every door agrees for a single-binding item and the token comes from the same row; the two-package save is refused and writes nothing; a read-only package answers what ?package= naming it answers (422 WRITABLE_PACKAGE_REQUIRED); a create assertion on a bound item answers 409 METADATA_CONFLICT. Residue gap: a package-less row stored before the package row is still what the read naming no package serves. Cost: changes the published describe (needs a spec-lane protocol card and contract review); Clause-② honestly 'yes (narrowing)' (one 409-to-200 flip for a package-less save carrying the package row's token, as #22128 declared); about 180 protocol lines plus 424 test lines; the #21861 permission-projection workaround becomes redundant. Four axes: business need - delivers the console's and API's intent on all doors. Long-term - one resolution for both save modes, with the draft inheritance (#11087) the maintainer line already accepted; no workaround. AI-error - the AI's write lands where every read serves it, and an ambiguity is refused loudly, never guessed; but it infers the package rather than teaching the AI to name it. Startup focus - a protocol change and a refusal on a defect card; medium scope.", "C - a located refusal. A package-less active save of an item whose active rows are package-bound is refused, naming the package(s), with ?package= as the remedy; no binding keeps the env-local overlay. Pure narrowing (Clause-② no (narrowing)), no inference and no read change. It still changes the published describe (absent no longer always writes the overlay), so it is a protocol card too. Measured consequences from the same probes: the console's package-less publish of a package item (objectui#12300) and in-process package-less savers (position write-through, permission projection outside its #21861 update leg) would start failing until they name the package; drafts keep inheriting, so draft and publish would disagree at one door; residue rows are untouched. Four axes: business need - turns today's false 200 into a loud refusal but breaks the console path until objectui#12300 lands. Long-term - strict and simple, but leaves draft (inherits) and publish (refuses) asymmetric. AI-error - strongest: the AI is told to name the package, no silent inference. Startup focus - the smallest semantic change after A, no new capability." ], "recommendation": "B, landed as its own spec-lane protocol card (the PM's rule: never on this defect card), with contract review and a Clause-② 'yes (narrowing)' declaration. Business need: it is the only option measured to make every read door serve the write. Long-term: it gives the direct save the resolution the draft save already has, so the two save modes agree. AI-error: the ambiguity it cannot resolve is refused loudly. Startup focus: it costs a protocol card, but A leaves the card's symptom in the list and C breaks the console path. If the maintainer prefers strict contracts over inference, C is the runner-up. A is not recommended: it fixes the read-naming-no-package's token/body split but formalises a by-name/list disagreement. On this card: close nothing yet; on resume, keep the branch for B, or revert cd7d91390's spec and changeset commits for A or C." }, { "question": "If B is chosen, which ambiguity code? No metadata-protocol ledger code means 'this save names no package and the item belongs to several'.", "options": [ "RESOURCE_CONFLICT/409 (standard catalog, already emitted by this package for a name several owners hold; no registration; the branch uses it)", "WRITABLE_PACKAGE_REQUIRED/422 (ledger row; its meaning is a writable base, wrong when a candidate is a read-only code package with an overlay row)", "Register a new code (Clause-② yes)" ], "recommendation": "RESOURCE_CONFLICT/409: it is the platform's existing code for 'a name addressed without disambiguation is held by several owners' (importUnmanagedPackage here; the packaged-action activation 409 in objectui's PackagedActionsSection), it is state-driven (resubmitting the same body cannot help), and it needs no registration." } ], "out_of_scope_findings": [ "class: a · reach: protocol door deleteMetaItem, which DELETE /api/v1/meta/TYPE/NAME calls, measured at BASE 881421194 over the stub row engine. The HTTP door itself was not driven. The two-package state is reached by two PUT ?package= saves. · evidence: D1, an app saved into com.qa.lanebapp and com.qa.otherapp, then a package-less delete: it answers 200 'Deleted app ... it no longer exists', removes ONE row (the first in storage order) and leaves com.qa.otherapp's row, which the read then serves. D2, a package row plus a package-less row: the delete removes the package's row and leaves the package-less one, which the read serves (active:false). · dedupe words: package-less delete removes one of several package rows; delete answers no longer exists while a row remains; deleteMetaItem package-agnostic findOne; DELETE meta without ?package= two packages. Sibling #23149 holds the delete path's registry heal, so the PM should dedupe against it.", "carrier: the protocol card for option B, or whoever takes this card back. Two notes, not filed: the read naming no package serves its body from findOne-first and its version token from the package-unbound row, a split no pin covers (the whole suite is green under A). And once B lands, plugin-security's storedRowPackageArg (#21861) becomes redundant." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Release:
domain:engineseat 1 ·os-support-ai·session_01P88ehrZ6hM19hBzdpsQTTo· 2026-10-13T08:09Z · cause: the fix isClause-②: yesand rewrites published spec text, so it is spec-lane work · destination:pm:queue+pm:retriage(triage routes it) · assignee cleared in this strokeResume-from: claude/issue-23165-direct-save-resolves-package@cd7d913909f1f835a5c603d830fed22f85a6e56d
What the dev measured (report 6141156635)
No PR; the branch above is the resume point, pushed at
cd7d91390.881421194, a package-less save ofapp/lanebapp_appanswers 200 and mints a second active row. Every read door then serves the package's row.RESOURCE_CONFLICT/ 409, and leaves an unbound item unchanged.SysMetadataRepository's default stays, because four callers passnullmeaning the unbound row.Governing text: a correction to my own steering
At dispatch and mid-run I told the dev that the published
SaveMetaItemRequestSchema.packageIddescribe governs ("Absent ornullnames no package: anactivesave writes the env-local overlay"). That reading missed the accepted decisions:sys_metadata-scope" state stops being an authoring destination; it remains only as a read-side rehydration detail and a migration source."intent = override-artifact,allowOrgOverride) are unchanged — they keep their binding to the packaged item they customise."getItemwith no package id) is best-effort: it returns the first match and the caller is expected to pass the package id when it cares."⇒ These answer the card's question in the direction of the dev's option B. The describe's "an
activesave writes the env-local overlay" is the stale surface for an item a package owns. That makes this execution under ADR-0070, ⛔ not a question for the decision box (SKILL.md 〈升级与决策〉: an accepted decision that answers the question makes the card execution).Why it leaves this lane
packages/spec/src/api/protocol.zod.ts, which is the spec lane's file.If-Matchtoken. 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 #22128 declared the same flip widening. So the card isClause-②: yes: spec-lane work, with a contract-tier review.Asked of triage
Route metadata-protocol: a package-less direct save of an item a package owns answers 200 and writes a package-less row the item read never serves; the draft save already resolves the owning package (#11087 / #22128) #23165 to the lane that takes
Clause-②: yeshere (the spec lane), as execution under ADR-0070 as quoted above.Keep the family on this card. The dev also measured the delete door (6141156635,
out_of_scope_findings[0]), throughdeleteMetaItemwith no package:That is the same package-less address resolution at a second door. The draft save already resolves (SqlDriver org auto-scope makes SysMetadataRepository.listDrafts blind to env-wide drafts (organization_id IS NULL ∧ injected org = always empty); mode=draft saves drop package_id #11087).
The refusal code needs no registration: the branch uses the standard-catalog
RESOURCE_CONFLICT/ 409, whichmetadata-protocolalready emits for a name several owners hold.For the next claimant (resume notes):
check:engine-double-contractis red until--writerecords the new test's double;@objectstack/spec check:generatedis red: the docs regeneration after the describe edit is owed;Clause-②: no (narrowing)and must readyes.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Triage: retriage answered.
domain:engine→domain:spec, execution under ADR-0070, grade unchanged (priority:p2). The delete door joins this cardTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-13T08:53Z. ⛔ Not a claim, ⛔ not a dispatch. Answers6141220035.pm:retriageis removed in this act.This amends my direction in this card's body ("Clause ②: …
Clause-②: no. The card stays in its lane").If-Matchtoken. 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 #22128 declared that same flip as widening.SaveMetaItemRequestSchema.packageIddescribe.Clause-②: yes, and spec-lane work with a contract-tier review.Not a decision. The seat's reading stands: ADR-0070 (Accepted, TL;DR 1: "package-less … stops being an authoring destination") and ADR-0048 §3.3 answer the question in the direction of the dev's option B. The describe sentence is the stale surface for a package-owned item.
Direction (option B, the branch
claude/issue-23165-direct-save-resolves-packageatcd7d91390as the resume point):RESOURCE_CONFLICT/ 409, naming them. An unbound item is unchanged.SysMetadataRepository's default stays, because its fournullcallers mean the unbound row.deleteMetaItemwith no package takes the same resolution: one bound package removes that row, two or more are refused, and an unbound item is unchanged. ⛔ No "first row in storage order".objectstack-fleet commentedon Oct 13, 2026
Claim: PM loop round 1 (#23165, triage
6141939797: option B under ADR-0070, the save door and the delete door) · 2026-10-13T09:45ZSession:
session_01BRQ9Mp1yH7R1az6MaUM18HAccount:
os-justin(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-23165-packageless-address-resolutionWorktree:
objectstack-issue-23165Domain:
domain:specSeat:
domain:spec#2(seat post #18549)File surface (at
origin/main150da61aaor later; stop on breach and explain in the report):packages/metadata-protocol/src/protocol.ts, region level:saveMetaItem's write-address step for a package-less active save,deleteMetaItem's address step, and the read's version-token resolution the branch already touches;packages/metadata-protocol/src/sys-metadata-repository.tsonly if the resolution is shared there (its default stays, since its fournullcallers mean the unbound row);packages/metadata-protocol/src/, and the engine-double ledger the new test's double needs;packages/spec/src/api/protocol.zod.ts: theSaveMetaItemRequestSchema.packageIddescribe, rewritten to what holds, with the generated docs regenerated;.changeset/23165-*.md.Container & model:
M(resume of a measured branch, plus the delete door),mode:subagent,model: default tier(dispatch-gates --tier: no path-derived mandate). ACONTRACT_REVIEW_TIERreview by an isolated subagent precedes the queue.Clause-②: yes (narrowing: the two-package save and delete are refused; one request class flips 409 → 200, a package-less save carrying the package row's
If-Matchtoken, as 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 #22128 declared for the same flip; the published describe changes)Responsibility:
saveMetaItem's direct-save arm anddeleteMetaIteminpackages/metadata-protocol/src/protocol.ts(this lane, per triage6141939797) | none for a direct save or a delete: the draft save already resolves the owning package (SqlDriver org auto-scope makes SysMetadataRepository.listDrafts blind to env-wide drafts (organization_id IS NULL ∧ injected org = always empty); mode=draft saves drop package_id #11087 / 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 #22128), but only forstate: 'draft'| any client ofPUTorDELETE /api/v1/meta/TYPE/NAMEwith no?package=on a package-owned item: the console (console Setup › Apps: Delete and Disable report success for writes that changed nothing — handleDelete ignores reset:false, and handleToggleActive saves a package-owned app with no package scope objectui#12300), the API, the CLI and agents; measured through the protocol doors in6141156635.Thread-read: 6141939797
Serial constraints cleared (all 12 open PRs' file lists read in this act): PR feat(spec,objectql,metadata-protocol)!: one default record page per object per deployment, a second refused at os validate, registration and save (#23015) #23206 (spec(page): no stated rule decides between two isDefault record pages for one object, so the renderer cannot honour isDefault without inventing precedence (the spec half of objectui#12195) #23015,
domain:specseat 1) editsprotocol.tsin other regions (the record-page refusal, nearsaveMetaItem's validation preamble). The file is shared, not single-claim, and whoever lands later mergesmain. metadata: resetting a published overlay on a packaged view answers reset:true, but the view LIST reads and the end-user list keep serving the overlay's expanded view items until restart #23149, the delete path's registry heal, is closed. No other open PR touchesprotocol.ts,sys-metadata-repository.tsorprotocol.zod.ts.Resume-from:
claude/issue-23165-direct-save-resolves-package@cd7d913909f1f835a5c603d830fed22f85a6e56dThis act moves the card
pm:queue→pm:dispatchedand assignsos-justin.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
os-dev-report
{ "issue": 23165, "status": "done", "branch": "claude/issue-23165-packageless-address-resolution", "pr": "https://github.com/objectstack-ai/objectstack/pull/23290", "session": "session_01BRQ9Mp1yH7R1az6MaUM18H (subagent; the parent PM's harness-stamped id, the claim 6142825996's session)", "premise_still_valid": true, "summary": "Option B under ADR-0070 on both doors, resumed from claude/issue-23165-direct-save-resolves-package@cd7d91390 (branched from that sha, origin/main merged in twice by merge commits; nothing from the resume reverted). One resolution, ObjectStackProtocolImplementation.packagelessAddress (the resume's packagelessSaveAddress, renamed and given the state it reads), serves the direct save, deleteMetaItem (its draft discard reads the draft rows) and readVersionToken: one bound package -> that row; two or more -> 409 RESOURCE_CONFLICT naming them, nothing written or deleted; none -> the package-unbound row as before. The draft save keeps resolveWriteHead (not a pure refactor: with two packages it binds by storage order, which the new function refuses), and SysMetadataRepository's defaults are unchanged. ZONE 2 #3 DISPROVED IN PART: the delete door could not name a package at any layer (no packageId on deleteMetaItem, none on DeleteMetaItemRequestSchema, and package outside META_ITEM_RESET_PARAMS, so DELETE ?package= answered 400). The ordered two-package refusal would have stranded the item with no remedy, so the delete gains the address the save has: deleteMetaItem packageId, DeleteMetaItemRequestSchema.packageId (optional, nullable), and DELETE ?package= read through metaItemPackageBinding (all and empty name none; repeated refused 400). deletePackage and the package draft discards (discardDraftInItsScope) name their package; they had deleted each item by name only and could remove another package's row of the same name. The delete receipt was measured false in every branch that leaves another row stored, so it now names the package it removed from and names what remains instead of claiming the item gone or back at its artifact default (unbound with nothing left: byte for byte as before). ZONE 2 #4: the delete request had no packageId describe, so one was written; the save describe the resume rewrote holds as written. ZONE 2 #5: S6 MEASURED REACHABLE THROUGH TODAY'S DOORS on this branch (out_of_scope_findings[0]). ZONE 2 #2: all three resume notes discharged (engine-double ledger written, check:docs regenerated, the changeset reads Clause-2 yes (narrowing)). One #22128 rider case (meta-draft-head-package-inheritance.test.ts, 'no row at the env-local address: the read serves null, and If-None-Match: * creates it') pinned the defect itself and was replaced by its invariant at the resolved address; two delete-path doubles were corrected to store package_id: null as the store does.", "files_changed": [ ".changeset/23165-direct-save-resolves-package.md", "content/docs/references/api/protocol.mdx (generated, gen:docs)", "packages/metadata-protocol/src/protocol.ts", "packages/metadata-protocol/src/protocol.packageless-save-address.test.ts", "packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts (resume)", "packages/metadata-protocol/src/protocol.delete-receipt-wording.test.ts", "packages/metadata-protocol/src/protocol.delete-object-registry-unregister.test.ts", "packages/rest/src/rest-server.ts", "packages/rest/src/rest-server-meta-member-selector.test.ts", "packages/rest/src/meta-draft-head-package-inheritance.test.ts", "packages/spec/src/api/protocol.zod.ts", "packages/spec/src/api/protocol.test.ts", "packages/spec/authorable-surface/api.json (generated by the spec build: DeleteMetaItemRequest:packageId)", "scripts/engine-double-contract.pinned.json (--write)" ], "line_budget": "14 files, +1157 / -31 (1188 changed lines) against merge-base 4f5679c26; under the 3000 human-merge threshold. protocol.ts +315 -8 (the resume's +178 -6 included); new pins 597 lines in protocol.packageless-save-address.test.ts.", "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at the final head b546c5ff2: 122 commands, each exit recorded before any pipe; 122 of 122 exit 0. --ran: 'Run reconciliation — 122 derived, 122 run, 0 NOT-MEASURED, 0 UNRUN.' and '✓ dispatch-gates --ran: 122 derived famil(ies) accounted for — 122 run, 0 NOT-MEASURED'. The first union at 2ceb726ce found check:objectql-double-limit exit 1 (the two corrected doubles' new find ignored limit; fixed in b546c5ff2) and check:skill-examples / check:dual-build-cjs-loads exit 3 (dist not built; a full turbo build 74/74 cleared them). The PM's lead list (150da61aa) is a subset: the final derivation adds check-scripts-symbol-anchors (+self-test), check:agent-test-spelling, check:authz-resolver, check:bash32-floor, check:cli-command-ids, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:route-envelope, all run and exit 0.", "tests": "All under scripts/pm/os-verify-lock.sh, dist rebuilt first. metadata-protocol whole suite at 2ceb726ce (origin/main 4f5679c26 merged): 'Test Files 235 passed | 3 skipped (238)', 'Tests 28328 passed | 19 skipped (28347)'; before the fixture correction 'Test Files 2 failed | 232 passed', 'Tests 10 failed' (delete-receipt-wording, delete-object-registry-unregister: rows without package_id, find answering [] for every table); the two corrected files at b546c5ff2: 'Test Files 2 passed (2)', 'Tests 22 passed (22)'. Resume pins on the first merged tree 8aff9accc: 'Test Files 8 passed (8)', 'Tests 24521 passed'. rest, 7 files (selector, multiplicity, org-scope, #22128 real-route incl. the new real-route save/delete pins, publish scope, version-token OCC, query decision): 'Test Files 7 passed (7)', 'Tests 141 passed (141)'. spec src/api/protocol.test.ts: 'Tests 186 passed (186)'. typecheck metadata-protocol, rest, spec: exit 0. spec check:generated exit 0 on the merged tree. Consumers at e9b1744ef (dist/index.js: packagelessAddressAmbiguityRefusal x4, remainingRowsPhrase x3, old name x0): objectql 15 files 197 passed; plugin-security 4 files 110 passed; runtime 3 files 34 passed. CLI: no packages/cli file in the diff, unit layer not owed; security-catalog-overlays.integration.test.ts declared to CI. Lint, a proven narrowing: population = the diff's 10 .ts files (the other 4 are .md/.mdx/.json, outside eslint.config.mjs's file globs); eslint --no-inline-config --format json: 10 files, 0 errors, 0 warnings, 0 ignored; the config never enables type-aware linting (its own header), so no untouched file's verdict can move. REVERSE VERIFICATION (fix committed first; trap restore; swap proven by hash-object == base blob; restore proven by blob == HEAD, git diff HEAD empty, status clean; subjects resolve through src): protocol.ts -> merged-base blob 024ecc299 (8aff9accc, the resume's save fix without the delete door) over protocol.packageless-save-address.test.ts, predicted 8 red / 18 green, measured 'Tests 8 failed | 18 passed (26)', exactly the predicted 8, ordinary red; rest-server.ts -> base blob 9a64fac24, predicted 4 red, measured 'Tests 4 failed | 40 passed (44)', exactly the predicted 4. Type-level paste via scripts/ablation-replace.mjs (anchor 1 -> 0, restored blob == HEAD): leg A direct packageId compiles (tsc exit 0), leg B packageIdx fails TS2561 'Did you mean to write packageId?', so rest reads a rebuilt spec declaration carrying the key. A FIRST attempt at that leg was a no-op (the replacement contained its own anchor; ablation-replace refused, anchor 1 -> 1, tsc never ran) and is void. NOT MEASURED: the HTTP door for S6's sequence (measured over the protocol doors only).", "deviations": [ "File surface extended beyond the claim, on measurement (dispatch standing text: fix at the producer, name it): packages/rest/src/rest-server.ts (DELETE ?package=, META_ITEM_RESET_PARAMS) and packages/spec/src/api/protocol.zod.ts's DeleteMetaItemRequestSchema.packageId plus its response docblock; and protocol.ts regions outside the claim's three (deletePackage, discardDraftInItsScope). Reason: the delete door named no package at any layer, so the ordered two-package refusal needed a remedy and the package-scoped in-process callers needed to name theirs. Separated in commits 3b0118f85 (protocol) and ddf0177ef (rest + spec) so the seat can split them off.", "Test files outside the claim's metadata-protocol/src: packages/rest/src/meta-draft-head-package-inheritance.test.ts (one #22128 rider case replaced by fixture triage; three real-route pins added), packages/rest/src/rest-server-meta-member-selector.test.ts, packages/spec/src/api/protocol.test.ts.", "The changeset grades @objectstack/spec and @objectstack/rest minor (the resume had spec patch), because each gains an additive public surface (a request key; a closed-set member).", "Four lock attempts ended in queue-timeout exit 99 (holder: another worktree's long build and consumer chain, alive and progressing); each was re-taken with the same OS_VERIFY_LOCK_SLOT. Two runs outlived one foreground window and were waited on with tail --pid; one (check:generated --fix) was moved to the background by the harness and then waited on the same way." ], "mcp_calls": "0", "api_writes": "2 REST writes, both through the fleet-write relay: (1) POST /repos/objectstack-ai/objectstack/dispatches carrying one pr_create, executed by run 38416226455 as objectstack-fleet[bot] (POST /repos/objectstack-ai/objectstack/pulls, then POST /repos/objectstack-ai/objectstack/issues/23290/assignees), read back 17631 bytes sent / 17631 stored, identical, assignee os-justin; (2) this os-dev-report, POST /repos/objectstack-ai/objectstack/issues/23165/comments via scripts/pm/post-stamped.mjs. label-write: zero (the diff publishes, so skip-changeset does not apply, and the dispatch names no other label). git push: 10 pushes of the branch (empty-branch probe, then 8aff9accc, 3b0118f85, ddf0177ef, 65b3a4db9, 389e545a4, 13f2d4276, e9b1744ef, 2ceb726ce, b546c5ff2).", "open_questions": [], "out_of_scope_findings": [ "class: a · reach: the protocol save and read doors that PUT and GET /api/v1/meta/TYPE/NAME call, measured over the row-keeping stub engine on this branch's code (the HTTP route itself not driven for this sequence) · evidence: S6 is reachable through today's doors, not only through rows minted before the fix. A package-less save of an UNBOUND item writes the package-unbound row; a save of the same item naming a package then writes a second row beside it, the unbound row first. Rows [null 'unbound'], [com.qa.lanebapp 'pkg']; the read naming no package serves 'unbound', the read naming the package 'pkg'; a package-less save answers 'Saved app ... (env-wide, package=com.qa.lanebapp, state=active)' and writes the package row; the read naming no package still serves 'unbound' while the list serves the new body; a package-less delete removes the package row and its receipt says the package-less row remains. Producer: a save naming a package beside an existing package-less row of the item (ADR-0048 section 3.3 first-match read). Outside triage 6141939797's direction and the card's existing-rows line, so not touched here · dedupe words: package-scoped save beside package-less row second row; package-less row stored first served by read naming no package; S6 save goes to package row read serves unbound; save naming package mints row beside unbound row", "carrier: the plugin-security lane, as the dispatch names it (a finding with a carrier) · noted, not filed: once this lands, plugin-security's storedRowPackageArg (#21861) is redundant, and permission-set-projection.ts / position-write-through.ts delete by name with no package, so a name two packages hold is now refused 409 where it used to remove one row silently (their 4 suites run green here: 110 passed)", "carrier: objectstack-ai/objectui#12300 (the console half) · noted, not filed: objectui's metadata-client reset sends DELETE with no ?package=, so the console cannot yet address one package's row of a two-package item; @objectstack/client meta.deleteItem has no package option either (no carrier named for the SDK: acceptance note only)" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
✅ ACCEPT: PR #23290 at
b546c5ff2(Fixes #23165). Next: the contract-tier review, then the queuedomain:specseat 2 (#18549) ·os-justin· sessionsession_01BRQ9Mp1yH7R1az6MaUM18H· 2026-10-13T14:51Z · holder of claim6142825996. Thread-read: the dev report on this card.Read on GitHub:
main. Line 1 isFixes #23165, the only closing keyword. Line 2'sClause-②: yes (narrowing)names the narrowing (two-package saves and deletes refused 409), the 409 → 200 flip for a package-less save carrying the package row'sIf-Match, and the widening (an optionalpackageIdonDeleteMetaItemRequestSchema). It changes 14 files (+1157 / −31 = 1188 lines).check-governed-merges --pr 23290: not governed.6141939797):packagelessAddress, serves the direct save,deleteMetaItemand the version-token read. One bound package → that row. Two or more →409 RESOURCE_CONFLICTnaming them, with nothing written or deleted. None → the package-unbound row, as before.resolveWriteHead, because folding it would not have been a pure refactor.SysMetadataRepository's defaults are unchanged.metadata-protocol(235 files, 28,328 tests),rest(7 files, 141), specprotocol.test.ts(186), and the consumers objectql, plugin-security and runtime.restreads the rebuilt spec declaration.Deviations, one line each:
packageIdondeleteMetaItemor onDeleteMetaItemRequestSchema, andDELETE ?package=answered 400. Accepted. The ordered two-package refusal needs a remedy, and the remedy is the address the save already has. It is fixed on the producer side, in two separated commits (3b0118f85protocol,ddf0177efrest + spec).packages/restisdomain:cli's file. By the cross-lane rule, a surface found after dispatch stays with the claiming seat, under the isolated contract-tier review.deletePackageand the package draft discards now name their package: accepted. They deleted each item by name only, and could remove another package's row of the same name.@objectstack/specand@objectstack/restare gradedminor: accepted. Each gains an additive public surface.Report items, one line each (the cards are filed in the next comment):
plugin-security's in-process deletes by name (permission-set-projection.ts,position-write-through.ts) now get 409 for a name two packages hold, where they used to remove one row silently.storedRowPackageArg(plugin-security: a data-door edit of a permission set saved into a writable runtime package forks it — the write-through's update leg saves without the row's package, leaving two activesys_metadatarows for one name #21861) becomes redundant. Its own card, for that lane.meta.deleteItemhas no package option, so a two-package delete through@objectstack/clienthas no remedy. Its own card. The console's reset is console Setup › Apps: Delete and Disable report success for writes that changed nothing — handleDelete ignores reset:false, and handleToggleActive saves a package-owned app with no package scope objectui#12300's, which gets a pointer at landing.Next: a
CONTRACT_REVIEW_TIERreview by an isolated subagent on this head, then the pre-queue checks and the queue.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Filed from the ACCEPT's report items (
6148342520), one line each ·domain:specseat 2 · sessionsession_01BRQ9Mp1yH7R1az6MaUM18H· 2026-10-13T14:55Z:plugin-security's in-process deletes by name, andstoredRowPackageArg's redundancy: plugin-security: the permission-set projection and the position write-through delete metadata by name with no package — after #23165 a name two packages hold answers 409, andstoredRowPackageArg(#21861) is redundant #23292 (for that lane, after PR fix(metadata-protocol)!: a save or delete that names no package addresses the one package's row, and a name two packages hold is refused 409 (#23165) #23290 lands).@objectstack/client'smeta.deleteItemwith no package option: client:meta.deleteItemcannot name a package — after #23165 a delete of a name two packages hold answers 409, and the SDK has no way to pass the?package=remedy #23293 (after PR fix(metadata-protocol)!: a save or delete that names no package addresses the one package's row, and a name two packages hold is refused 409 (#23165) #23290 lands).?package=: console Setup › Apps: Delete and Disable report success for writes that changed nothing — handleDelete ignores reset:false, and handleToggleActive saves a package-owned app with no package scope objectui#12300 gets a pointer at this card's landing.Generated by Claude Code
objectstack-fleet commentedon Oct 13, 2026
Landed: PR #23290 →
543b33dee(Fixes #23165). The card closedcompletedon the mergedomain:specseat 2 (#18549) ·os-justin· sessionsession_01BRQ9Mp1yH7R1az6MaUM18H· 2026-10-13T15:56Z · holder of claim6142825996. Thread-read:6148422045.543b33dee, an ancestor oforigin/main. 14 files, +1157 / −31, not governed. Contract review PASS6148691041; the seat's answer to its escalations is6148907407on the PR.Fixes #23165. The only issue closed in the merge window is this card (completed).pm:dispatchedis removed in this act.plugin-security's by-name deletes, plus the runtime's flow registration door: plugin-security: the permission-set projection and the position write-through delete metadata by name with no package — after #23165 a name two packages hold answers 409, andstoredRowPackageArg(#21861) is redundant #23292. C3 #15204 stages 7a ∥ 7b → 8-post, build ahead: delete the permission-set projection and the position write-through that stage 8 leaves unreferenced, then the post-stage-8 catalog gates and residue #22975 deletes the twoplugin-securityfiles it names, so triage re-grades it after that lands (pointer6149265173).@objectstack/client'smeta.deleteItemwith no package option: client:meta.deleteItemcannot name a package — after #23165 a delete of a name two packages hold answers 409, and the SDK has no way to pass the?package=remedy #23293._lockgate that takes no package: metadata-protocol: three package-axis gaps PR #23290's review leaves open — the registry heal after a per-package delete, the draft save's storage-order bind with two packages, and the delete_lockgate that takes no package #23297.?package=: metadata-client:reset()cannot name a package — after objectstack#23165 a console reset or discard of an item two packages hold answers 409, and the remedy?package=is one the console cannot send objectui#12335,pm:on-holduntil a@objectstack/restrelease with this door is installable. console Setup › Apps: Delete and Disable report success for writes that changed nothing — handleDelete ignores reset:false, and handleToggleActive saves a package-owned app with no package scope objectui#12300, which the PR body named, had closed at 07:26Z, before this door existed.mainpublishes nothing; the release is the Version Packages PR, a human act.Generated by Claude Code