Skip to content

rollbackMetaItem / revertCommit always 409 on a package-bound overlay row — restoreVersion's put() scopes the parent lookup to package_id IS NULL #6215

Description

@baozhoutao

Blocked-by: #4636

Found while implementing #4636 PR1 (write-path ownership key). Unassigned — nobody is on this. Recording only, per Prime Directive #10. Not fixed there: it is a SysMetadataRepository scoping defect, outside that PR's ruled scope, and it changes the repository write path — it deserves its own pin tests and review.

Evidence

SysMetadataRepository.restoreVersion (packages/metadata-protocol/src/sys-metadata-repository.ts, ~729) reads the current active row without a package predicate, then re-puts the historical body with no packageId option:

const currentActive = await this.get(ref, { state: 'active' });   // "match any package"
return this.put(ref, body, {
  parentVersion: currentActive?.hash ?? null,
  ...
});                                                                // no packageId

put then scopes its own existing-row lookup with:

where: this.whereFor(ref, state, opts.packageId ?? null)

and whereFor treats null as a predicate, not as "any package":

if (packageId !== undefined) where.package_id = packageId; // string -> eq; null -> IS NULL

undefined ?? null is null, so the lookup is always narrowed to package_id IS NULL. For a row bound to a real package (app.myapp, …) it therefore finds nothing:

  • existingHash reads null;
  • opts.parentVersion is the hash the unscoped read just returned;
  • null !== hash -> ConflictError.

Observed, driving the real protocol + a sys_metadata / sys_metadata_history double (objectstack#4636 branch claude/issue-4636-writepath-real-package-id, packages/objectql/src/protocol-writepath-object-ownership.test.ts):

[metadata_conflict] object/myapp_invoice advanced during rollback.
Expected parent sha256:00ca6e72c421ecc68fa6a5153ef2b962ea307d6c9f78646425666a0c885927bc but current is null.

The same row rolls back fine when it has no package binding, which isolates the package dimension as the cause.

Why it matters

Every rollback of an overlay row authored inside a Studio package workspace fails, and fails with a message that blames a concurrent edit ("advanced during rollback") when nothing changed. Two callers, both user-facing:

  • rollbackMetaItem — the per-item version-history revert;
  • revertCommit (protocol.ts, ~9986) — the package-commit revert, same restoreVersion call.

ADR-0070 pushes authoring toward always resolving a writable base package, so the share of rows carrying a real package_id grows over time; the package-less rows that still work are the legacy shape.

Note the second face of the same bug: if the parent check ever did pass (no active row at all), put's existing would still be null and it would insert a duplicate global row rather than update the package-bound one.

Suggested fix

Thread the binding through, rather than making a consumer tolerant: restoreVersion already reads the row it is restoring, so it can pass that row's package_id into put (and into its own get), matching what saveMetaItem does. Alternatively give put an explicit "any package" spelling distinct from null — today undefined means that, and ?? null erases it at the one call site that needed it. Whichever route, the ?? null in put is the line that turns "unspecified" into "unbound".

Needs a pin test at both callers (rollbackMetaItem and revertCommit) with a package-bound row. #4636 PR1 leaves a tripwire test asserting the current 409 ([tripwire] a package-bound rollback still 409s) precisely so this fix trips it: whoever lands the fix should replace that test with the assertion one line above it, which already states what the registry write-through must then do with the package ownership key.

Related: #4636 (the write-through whose package-bound limb this defect makes unreachable), ADR-0048 (the per-package overlay row), ADR-0070.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions