Skip to content

fix(automation,spec): the cold-boot flow bind must survive the read path's own annotations - #4424

Merged
os-zhuang merged 3 commits into
mainfrom
claude/strip-read-decorations-flow-bind
Aug 1, 2026
Merged

fix(automation,spec): the cold-boot flow bind must survive the read path's own annotations#4424
os-zhuang merged 3 commits into
mainfrom
claude/strip-read-decorations-flow-bind

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes the framework half of objectstack-ai/cloud#971.

The bug

getMetaItems({ type: 'flow' }) decorates every served item with _diagnostics (and _draft on a preview read). The cold-boot bind fed that served document straight into engine.registerFlowFlowSchema.parse — and since #4001 closed the metadata schemas, an unrecognized key throws instead of being dropped. So every flow failed to register on every boot:

WARN [Automation] cold-boot flow bind: failed to register task_hours_pause_project: [
  { "code": "unrecognized_keys", "keys": ["_diagnostics"], "path": [], … }
]

Not AI-specific — cloud's boot-smoke saw the same on the sample package's overdue_escalation / task_completion / quick_add_task. The stored metadata was always clean (_diagnostics is never persisted — #4326 sees to that on the write path). We broke our own parse with our own annotation.

Not fatal today only by luck: RecordChangeTriggerPlugin binds record flows by a second path, so automations kept firing behind the WARN. A flow whose only binding path is this one would simply have gone silently dead — and every boot log carried one WARN per flow, which is exactly the noise that hides a real failure.

resyncFlowsFromProtocol reads the same seam, so publish-time rebinds were failing identically (flow re-sync: failed to register …).

The fix

Strip the read decorations at the read seam (readFlowDefsFromProtocol) — not by loosening FlowSchema. The payload is malformed because we decorated it, so the producer's annotation is the producer's to remove; widening the schema would make our own read shape a second, permanent contract.

The canonical list moves from metadata-protocol (where it was module-private) into @objectstack/spec/kernel, because the producer (metadata-protocol) and the consumers (service-automation's bind, and whoever is next) sit in different layers and must not drift. metadata-protocol now imports it and re-exports stripReadDecorations unchanged — no public-surface change.

The strip removes only the read decorations, never the ADR-0010 envelope (_lock, _packageId, …): FLOW_KEYS allowlists those precisely so a served flow keeps its provenance on re-parse, and a blanket _-purge would erase a packaged flow's binding on every rebind.

Regression coverage

All three verified to fail without the fix (and the two pre-existing cold-boot tests pass either way — they served naked bodies, which is why they never caught this):

  • flow-cold-boot-bind.test.ts — a decorated payload binds, in both the bare and { item: … } envelope shapes, with _packageId preserved and _diagnostics/_draft gone.
  • protocol.read-decorations.test.ts — driven through the real getMetaItems:
    1. the raw served flow must still be rejected (proves the strip is load-bearing, not decoration);
    2. the stripped one parses;
    3. the drift guard — every key the read adds must be either a known decoration or allowlisted by the closed schema. Stamping a hypothetical _servedAt on the read path fails it by name:

      the read path stamped a key that is neither stripped (add it to METADATA_READ_DECORATIONS in @objectstack/spec) nor accepted by the closed schema … expected [ '_servedAt' ] to deeply equal []

Sweep for other re-feeds

This is the only live one. Everything else that touches a served document is already safe: the write-back paths funnel through saveMetaItem (strips first, #4326); permission-set-projection, assemblePackageManifest and cloud's service-ai-studio each run their own strip; service-analytics / import-prepare / admin-import-users read named fields only; and AgentSchema/SkillSchema are deliberately not .strict(). _packageId is not the same disease — every closed schema allowlists it via MetadataProtectionFields.

Test

@objectstack/spec 7225 ✓ · @objectstack/metadata-protocol 139 ✓ · @objectstack/service-automation 566 ✓

🤖 Generated with Claude Code

…ath's own annotations

`getMetaItems({ type: 'flow' })` decorates every served item with
`_diagnostics` (and `_draft` on a preview read). The cold-boot bind fed that
served document straight into `engine.registerFlow` → `FlowSchema.parse`, and
since #4001 closed the metadata schemas an unrecognized key THROWS instead of
being dropped. So every flow failed to register on every boot:

    WARN [Automation] cold-boot flow bind: failed to register task_hours_pause_project:
      { "code": "unrecognized_keys", "keys": ["_diagnostics"], … }

Not AI-specific — cloud's boot-smoke saw it on the sample package's
`overdue_escalation` / `task_completion` / `quick_add_task` too. The stored
metadata was always clean; we broke our own parse with our own annotation.

Not fatal today only by luck: the record-change plugin binds record flows by a
second path, so automations kept firing behind the WARN. A flow whose only
binding path is this one would have gone silently dead.

Fixed at the read seam (`readFlowDefsFromProtocol`), not by loosening
`FlowSchema`: the payload is malformed because WE decorated it, so the producer's
annotation is the producer's to remove — widening the schema would make our own
read shape a second, permanent contract.

The canonical decoration list moves from `metadata-protocol` (module-private)
into `@objectstack/spec/kernel`, because the producer and the consumers sit in
different layers and must not drift; `metadata-protocol` imports it and
re-exports `stripReadDecorations` unchanged. The strip removes ONLY the read
decorations — never the ADR-0010 envelope (`_lock`, `_packageId`, …), which
`FLOW_KEYS` allowlists and a rebind must preserve.

Regression coverage, all three verified to fail without the fix:
  • flow-cold-boot-bind.test.ts — a decorated payload binds, in both the bare
    and `{ item: … }` envelope shapes, with `_packageId` preserved.
  • protocol.read-decorations.test.ts — against the REAL `getMetaItems`: the
    raw served flow must still be REJECTED (the strip is load-bearing), the
    stripped one must parse, and every key the read ADDS must be either a known
    decoration or allowlisted by the closed schema. That last one is the drift
    guard: stamping a new `_foo` on the read path fails it by name with the fix
    to apply.

Refs cloud#971, #4001, #4326.
@vercel

vercel Bot commented Aug 1, 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 1, 2026 3:59am

Request Review

@github-actions github-actions Bot added the tests label Aug 1, 2026
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/service-automation, @objectstack/spec.

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

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/service-automation, @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol, @objectstack/service-automation, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/service-automation, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/service-automation, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol, @objectstack/service-automation, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

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.

@github-actions github-actions Bot added the size/m label Aug 1, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Aug 1, 2026
… snapshot

METADATA_READ_DECORATIONS / MetadataReadDecoration / stripReadDecorations —
additive only (0 breaking), moved in from metadata-protocol so the read-path
producer and its cross-layer consumers share one definition.
@os-zhuang
os-zhuang merged commit 5b843fb into main Aug 1, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/strip-read-decorations-flow-bind branch August 1, 2026 04:12
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.

1 participant