You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(migrate,metadata-protocol): os migrate meta --stored rewrites sys_metadata rows in place (#4327) (#4464)
* feat(migrate,metadata-protocol): os migrate meta --stored rewrites sys_metadata rows in place (#4327)
#4317 closed the correctness gap from the read side: every stored-row
rehydration seam replays the full ADR-0087 conversion chain, retired entries
included, so a row written under any past protocol is served canonical
forever. The rows themselves stayed legacy — the chain re-lowers them on every
load and each logs a conversion notice per process. Until now the only things
that rewrote such a row were a Studio re-save and duplicatePackage.
`os migrate meta --stored` walks sys_metadata (active + draft, all orgs),
replays the same applyConversionsToStoredItem pass, and re-saves each changed
body through saveMetaItem — so a rewritten row gets a sys_metadata_history
entry, a fresh checksum and the mutation projectors, exactly like an author's
save. The history row's source is `migrate-stored`, distinguishing an upgrade
from an edit. parentVersion is the row's own checksum, so a concurrent writer
produces a 409 the report names rather than a clobber.
Preview is the default and --apply the only writing mode, matching its two
siblings and #3617's "a dry run changes nothing"; an apply run refuses to
start while another process holds the SQLite database. Nothing gates on this
having run (#3855) and no sys_migration flag is recorded — a flag would
advertise enforcement that does not exist. What a run buys is hygiene plus an
assertable verdict: nothing left to do exits 0, work remaining exits 1.
Three carve-outs are reported rather than counted as done: flow rows (their
seam is AutomationEngine.registerFlow, which holds the executor registry the
node-type conflict guard needs), types with no repository write path (agent),
and rows that still fail the current schema after conversion. An empty scan
says it attests nothing rather than reading as a pass.
Also: protocol.migrateStoredMetadata() returns the same structured report an
admin route would render, and saveMetaItem takes an optional `source` for its
history/audit rows — server-stated, never request-derived.
Closes#4327
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoZPKPDqJ7WB7z84xk9y3f
* docs(adr-0087): name the follow-up that gives flow rows the same finish line (#4454)
The addendum said flows were "tracked separately" without saying where.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoZPKPDqJ7WB7z84xk9y3f
* docs(releases): name the stored-metadata pass on the v17 page (#4327)
The v17 entry for #3903 described the read-path guarantee and stopped there,
and the upgrade checklist listed the two per-deployment migrations without
this one. Both now point at `os migrate meta --stored`, marked optional —
it opens no gate, unlike its two neighbours in that list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoZPKPDqJ7WB7z84xk9y3f
---------
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: content/docs/deployment/cli.mdx
+60Lines changed: 60 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -589,6 +589,7 @@ written.
589
589
|`os migrate plan`| Warns and continues — a plan writes nothing either way |
590
590
|`os migrate apply`|**Refuses** (exit 1, `error: database_busy` under `--json`). Stop the other process, or pass `--force`|
591
591
|`os migrate files-to-references --apply`|**Refuses** likewise — it rewrites rows, so a concurrent writer is at least as dangerous |
592
+
|`os migrate meta --stored --apply`|**Refuses** likewise — it rewrites `sys_metadata` rows, and a live process saving metadata is exactly the collision |
592
593
593
594
The check applies to SQLite only: Postgres and MySQL take their own server-side
594
595
locks. Only same-user processes are visible without elevated privileges, and a
@@ -642,6 +643,7 @@ where the data lives.
642
643
|---------|-------------|
643
644
|`os migrate files-to-references`| Convert legacy file-field values to `sys_file` references, verify the ownership ledger, and record the deployment's migration flag |
644
645
|`os migrate value-shapes`| Scan stored reference and structured-JSON field values against the platform's value contract, and record the deployment's migration flag when clean |
646
+
|`os migrate meta --stored`| Replay the metadata conversion chain over this deployment's `sys_metadata` rows and rewrite the ones still carrying a pre-protocol shape. Hygiene, not a gate — nothing depends on it having run |
645
647
646
648
```bash
647
649
os migrate files-to-references # Dry run: full report, writes nothing
@@ -774,6 +776,64 @@ closed gate logs that it is enforcing, and an app that declares neither class
774
776
of field says nothing at all. So a running deployment always tells you the
775
777
state of its own data — which is the question `os migrate meta` cannot answer.
776
778
779
+
#### `os migrate meta --stored`
780
+
781
+
The two commands above are about **application data**. This one is about the
782
+
**metadata itself**, at rest: the `sys_metadata` rows Studio and the runtime
783
+
authoring APIs write.
784
+
785
+
Those rows already *read* correctly whatever protocol they were written under —
786
+
every rehydration seam replays the full conversion chain, so a body from an
787
+
older major is served in today's canonical shape and always will be. What the
788
+
rows do not do is *change*: they keep their original bytes, the chain re-lowers
789
+
them on every load, and each one logs a conversion notice once per boot. This
790
+
command ends that for the deployment that runs it.
791
+
792
+
```bash
793
+
os migrate meta --stored # Preview: per-row report, writes nothing
794
+
os migrate meta --stored --apply # Rewrite the rows (prompts)
795
+
os migrate meta --stored --apply --yes --json # CI / scripts
796
+
os migrate meta --stored --type view --type object # Restrict to a type (repeatable)
797
+
```
798
+
799
+
It walks `active` and `draft` rows across every organization (archived rows are
800
+
a record of what *was* and are never read), replays the same chain the read path
801
+
does, and re-saves each changed body through the normal write path — so a
802
+
rewritten row gets a `sys_metadata_history` entry, a fresh checksum, and the
803
+
mutation projectors, exactly like an author's save. The history entry's source
804
+
is `migrate-stored`, so a later diff shows which changes were an upgrade and
805
+
which were somebody's edit.
806
+
807
+
Three things it deliberately declines, and names in the report rather than
808
+
counting as done:
809
+
810
+
| Not rewritten | Why |
811
+
| :--- | :--- |
812
+
|`flow` rows | Flow-node conversions carry a conflict guard that needs the automation engine's live executor registry; flows canonicalize at their own seam when the engine loads them |
813
+
| Types with no repository write path (`agent`) | Their write path records no history and would force a draft live — a half-write is worse than leaving the row to the read path |
814
+
| Rows that still fail the current schema after conversion | That is a genuine contract violation, not chain-owned history. The write path's rejection is correct; fix the row in Studio |
815
+
816
+
<Callouttype="warn">
817
+
`--apply` is the only writing mode, and it rewrites **metadata** — each affected
818
+
row's checksum moves and each gets a history entry. Preview first. Like the
819
+
other row-rewriting migration, an apply run refuses to start while another
820
+
process holds the SQLite database (`--force` overrides).
821
+
</Callout>
822
+
823
+
**Nothing gates on this having run.** The read path is the guarantee, for every
824
+
deployment, whether or not anyone runs this — an operator-run migration is not
825
+
something the platform can depend on. What running it buys is hygiene (cleaner
826
+
diffs, exports and history from here on, and the recurring boot notices go
827
+
quiet) plus one thing that was previously unobtainable: **you can assert it.**
828
+
A run with nothing left to do exits `0`; a deployment with rows still carrying
829
+
an old dialect exits `1`. So "my metadata is on protocol N" becomes a check
830
+
rather than a belief.
831
+
832
+
Note the division of labour with the default mode: `os migrate meta --from N`
833
+
rewrites an **author's source** and reads no database; `--stored` rewrites **one
834
+
deployment's rows** and reads no config. Same chain, opposite ends of the
835
+
contract — which is why the two modes are mutually exclusive.
0 commit comments