Skip to content

fix(metadata-protocol): a flow save that skipped canonicalization says so (#4580) - #4582

Merged
os-zhuang merged 1 commit into
mainfrom
claude/flow-metadata-persistence-issue-cn4aiy
Aug 2, 2026
Merged

fix(metadata-protocol): a flow save that skipped canonicalization says so (#4580)#4582
os-zhuang merged 1 commit into
mainfrom
claude/flow-metadata-persistence-issue-cn4aiy

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #4580. Follow-up to #4542 / #4548.

What

saveMetaItem canonicalizes flow bodies before the schema gate (#4548). Four postures live at that seam — three announce themselves, one said nothing:

posture signal before signal now
clean canonicalization row healed, --stored reports canonical unchanged
refused rename 409 FLOW_CONVERSION_CONFLICT naming token + path unchanged
canonicalizer throws → raw save none console.warn, deduped per flow
no automation service os migrate meta --stored reports it unchanged

The fallback itself is correct and is unchanged by this PR: canonicalizeStoredFlow is stricter than the gate (strict parse, cycle detection, control-flow regions), so a WIP draft with a temporary cycle must stay saveable — in publish mode as well as draft. What was wrong is that it was invisible.

Why it mattered

A save that skipped canonicalization was indistinguishable from one that healed the row. A body that is both a legacy dialect and unparseable by the strict canonicalizer re-persisted verbatim — the exact #4542 symptom, arriving silently, while the boot warning for legacy stored rows tells the author that re-saving is the remedy. The author found out only by running os migrate meta --stored and seeing the row failed.

It was also the one quiet link in a chain that is otherwise loud by design (ADR-0087 D2): conversions emit notices, convertStoredItem warns once per row on read, migrateStoredMetadata reports failed with the message.

What changed

One catch block in packages/metadata-protocol/src/protocol.ts now warns with the flow name and the canonicalizer's own error, plus a flowCanonicalizeFallbackWarned dedupe set alongside the existing storedConversionWarned — Studio autosaves the same draft repeatedly and a WIP cycle throws on every write, so an undeduped warning would be a spam loop.

No behavior change: the body still saves, the schema gate stays the arbiter, registerFlow still refuses to arm a malformed flow.

Decision carried forward from #4548

Refusing the save in publish mode was reconsidered here and rejected again, for a reason worth recording:

  • publish is the default mode, so refusing would silently tighten validation for every existing caller — a breaking change for bodies that save fine today.
  • It can only be enforced where an automation service exists. On a control-plane / metadata-only host resolveFlowCanonicalizer() returns undefined and there is no strict parse to run — so the same body would save on one host and 422 on another. That is unlike the 409, which reflects genuine environment state (a token owned by a live executor); a 422 should depend on the body alone.

Observability was the actual gap, and this closes it.

Tests

5 new cases in protocol.save-flow-canonicalization.test.ts (16 total in the file):

  • throw-fallback warns, naming the flow, the canonicalizer's reason, and the --stored remedy
  • deduped across three repeat saves of the same flow
  • the clean path stays silent
  • the conflict path stays silent (the 409 is the signal)
  • a host with no automation service stays silent (nothing was skipped)

Assertions filter warnings by message rather than counting all console.warn calls, so unrelated one-shot protocol warnings (e.g. #3770's) can't make them order-dependent.

Full metadata-protocol suite green (206 tests, was 201). Typecheck and eslint clean on both changed files.

Riders

Changeset (.changeset/save-flow-canonicalization-fallback-warning.md). No content/docs/releases/ changes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NDmJ5ASMSzcw1q17vh32MG


Generated by Claude Code

…s so (#4580)

`saveMetaItem` canonicalizes flow bodies before the schema gate (#4542). When
the canonicalizer throws — it is stricter than the gate: strict parse, cycle
detection, control-flow region validation — the save falls back to the raw body
so a WIP draft with a temporary cycle stays saveable. That fallback is correct
and unchanged here. It was also completely silent.

Three of the four postures at this seam announce themselves: a clean pass heals
the row, a refused rename fails with 409 FLOW_CONVERSION_CONFLICT naming the
token, and a host with no automation service is reported by `os migrate meta
--stored`. The throw-fallback said nothing — so a save that skipped
canonicalization was indistinguishable from one that healed the row, and a body
that is BOTH a legacy dialect and unparseable re-persisted verbatim. That is
the #4542 symptom arriving silently, against a boot warning that tells the
author re-saving is the remedy.

The fallback now warns, naming the flow and the canonicalizer's own error,
deduped once per flow per process (the `convertStoredItem` pattern — Studio
autosaves the same draft repeatedly and a WIP cycle throws on every write).
This aligns the write seam with ADR-0087 D2's "loud" posture.

No behavior change: the body still saves, the gate stays the arbiter, and
`registerFlow` still refuses to arm a malformed flow. Refusing the save in
publish mode was considered and rejected — publish is the default mode, so it
would silently tighten validation for every existing caller, and it can only be
enforced where an automation service exists, making the same body saveable on a
control-plane host and a 422 on an automation host.

Tests: 5 new cases in protocol.save-flow-canonicalization.test.ts — warns with
the flow name and reason; deduped across repeat saves; silent on the clean,
conflict, and no-service paths. Full metadata-protocol suite green (206).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDmJ5ASMSzcw1q17vh32MG
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 2, 2026 8:50am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review August 2, 2026 09:00
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 2, 2026
Merged via the queue into main with commit 705e5c8 Aug 2, 2026
20 checks passed
@os-zhuang
os-zhuang deleted the claude/flow-metadata-persistence-issue-cn4aiy branch August 2, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

saveMetaItem's flow canonicalizer throw-fallback is silent — a publish-mode save that skipped canonicalization looks identical to a clean one

2 participants