Skip to content

fix(automation): bind a flow published while the server runs, without a restart - #2576

Merged
os-zhuang merged 1 commit into
mainfrom
fix/automation-publish-rebind
Jul 4, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
fix/automation-publish-rebind

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #2560 (cold-boot flow binding). A flow published while the server is running — the Studio online-authoring journey (author a record-triggered automation, publish it, immediately update a matching record) — did not fire. Its trigger only bound on the next process restart.

Two gaps, both fixed:

  1. The publish path fired no rebind signal. POST /packages/:id/publish-drafts → protocol.publishPackageDrafts promotes drafts to active but emitted no event the automation service listens to. The runtime dispatcher now announces metadata:reloaded after a successful publish — the same signal a dev artifact reload fires (MetadataPlugin._reloadAndAnnounce) — so boot-cached consumers re-sync without a restart. Best-effort: a subscriber failure never fails the publish (drafts are already live); it rides the response as rebindError. Fires only when something was published/unhidden.

  2. The runtime re-sync read the wrong source. The automation service's metadata:reloaded re-sync pulled metadata.list('flow'), which returns 0 in a real running server (it does not surface inline app flows), so even when the hook fired it bound nothing. It now reads protocol.getMetaItems({ type: 'flow' }) — the same flattened flow view fix(automation): bind flow triggers on cold boot, not just after HMR reload #2560's cold-boot bind and GET /meta/flow use — via a shared readFlowDefsFromProtocol helper, while keeping the teardown of flows removed from the artifact. A failed or unavailable protocol read returns null and is a no-op — it never tears down live flows. The kernel:ready cold-boot bind stays additive (no teardown), reusing the same helper.

Only one metadata:reloaded subscriber exists (the automation service), so firing it on publish has no collateral effect. Production is largely unaffected — a deploy reboots the process, so #2560's cold-boot bind covers it; this closes the gap for dev and single-instance Studio authoring.

Changes

  • packages/services/service-automation/src/plugin.ts — extract readFlowDefsFromProtocol; metadata:reloaded re-sync (resyncFlowsFromMetadata → resyncFlowsFromProtocol) reads the protocol and keeps teardown; syncFlowsFromProtocol (kernel:ready) reuses the helper, stays additive.
  • packages/runtime/src/http-dispatcher.ts — announce metadata:reloaded after a successful publish-drafts.

Tests

  • New flow-publish-rebind.test.ts (4 cases): binds a flow that appears in the protocol after boot; tears down a flow the protocol no longer serves; reads the protocol, not metadata.list (the exact bug); does not tear down live flows on a failed protocol read. The first/third fail on the pre-fix code.
  • New http-dispatcher.test.ts cases: publish-drafts announces metadata:reloaded / does not announce when nothing was published.
  • Updated flow-hot-reload.test.ts fake service (metadata → protocol source).
  • Green locally: @objectstack/service-automation 231/231, @objectstack/runtime 447/447; eslint clean.

Verification (end-to-end, clean instance, no restart)

On a fresh showcase (:3210):

  1. Authored a record-triggered flow showcase_pubtest as a draft in a writable base package.
  2. Pre-publish update of a showcase_task → flow did not fire (negative control — still a draft).
  3. POST /packages/:id/publish-drafts without restarting → flow became active; response carried no rebindError.
  4. Post-publish update → flow fired (temporary process.stderr.write probe in AutomationEngine.execute, since logger.info is suppressed and sys_automation_run only records suspended runs). Probe removed before commit.

🤖 Generated with Claude Code

… a restart

Follow-up to #2560 (cold-boot flow binding). A flow published while the server
runs — author a record-triggered automation in the Studio, publish it,
immediately update a matching record — did not fire; its trigger only bound on
the next restart.

Two gaps, both fixed:

1. The publish path fired no rebind signal. The runtime dispatcher now announces
   'metadata:reloaded' after a successful POST /packages/:id/publish-drafts — the
   same signal a dev artifact reload fires — so boot-cached consumers re-sync.

2. The runtime re-sync read the wrong source. The automation service's
   'metadata:reloaded' re-sync pulled metadata.list('flow'), which returns 0 in a
   real running server; it now reads protocol.getMetaItems({type:'flow'}) — the
   same flattened view #2560's cold-boot bind uses — keeping teardown of removed
   flows. A failed/unavailable protocol read is a no-op and never tears down live
   flows.

Verified end-to-end on a clean instance: authored a record-triggered flow in a
writable package, published it without restarting, then updated a matching record
and observed the flow fire (before the fix it did not). New regression tests:
flow-publish-rebind.test.ts (re-sync binds a post-boot flow, tears down a removed
one, reads the protocol not metadata.list, and never tears down on a failed read)
plus two http-dispatcher.test.ts cases (publish fires / does not fire the hook).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 4, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 4, 2026 12:47pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Jul 4, 2026
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/runtime, packages/services.

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

  • content/docs/concepts/cloud-artifact-api.mdx (via packages/runtime)
  • content/docs/concepts/implementation-status.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/concepts/packages.mdx (via @objectstack/runtime)
  • content/docs/guides/api-reference.mdx (via @objectstack/runtime)
  • content/docs/guides/authentication.mdx (via @objectstack/runtime)
  • content/docs/guides/cloud-deployment.mdx (via @objectstack/runtime)
  • content/docs/guides/deployment-vercel.mdx (via @objectstack/runtime)
  • content/docs/guides/driver-configuration.mdx (via @objectstack/runtime)
  • content/docs/guides/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/guides/packages.mdx (via @objectstack/runtime, packages/services)
  • content/docs/guides/plugin-chatbot-integration.mdx (via @objectstack/runtime)
  • content/docs/guides/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/guides/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/guides/runtime-services/index.mdx (via packages/services)
  • content/docs/guides/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/guides/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/protocol/objectos/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/objectos/i18n-standard.mdx (via packages/services)
  • content/docs/protocol/objectos/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/objectos/lifecycle.mdx (via @objectstack/runtime)

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 merged commit 9693a36 into main Jul 4, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the fix/automation-publish-rebind branch July 4, 2026 12:52
os-zhuang added a commit that referenced this pull request Aug 20, 2026
…ublish

`POST /api/v1/meta/:type/:name/publish` promoted a draft to `active` and told
nobody. The lifecycle event that makes boot-cached consumers re-read had two
announcers — the metadata plugin's dev-artifact watcher, and the runtime
dispatcher after `POST /packages/:id/publish-drafts` (#2576) — so publishing
item by item, which is what AI authoring and the item-level Studio doors do,
fired neither. Measured on a cloud rig: a record-change flow published as
`state='active'` produced no bind log and never executed until the kernel was
rebuilt and `kernel:ready` re-bound it (#2560).

`publishMetaItem` now notifies through a new `onMetaItemPublished` seam, and
`ObjectQLPlugin.subscribeMetadataRebind` — armed for both protocol assembly
modes — turns that into `ctx.trigger('metadata:reloaded', { changed })`. The
producer notifies and the host announces because the protocol holds no kernel
hook bus, which is the same division `HttpDispatcher.announceKernelEvent` makes
for the batch door. `changed` carries the batch door's `{type}/{name}` spelling,
so a subscriber cannot tell the two doors apart.

The notification is deliberately NOT the existing `onMetadataMutation`: that one
is emitted from `runPublishSideEffects`, which the batch door runs once per
promoted draft, so announcing on it would fan a full kernel re-sync (schema DDL,
connector re-materialization, flow re-bind) out once per item of a
"publish whole app". A CONTROL case pins that the batch door emits none.

Awaited, so the publish's own 2xx means the re-bind was attempted rather than
queued; best-effort, so a throwing subscriber is logged at `warn` and never
turns a landed publish into an error. No wire key is added — the batch door's
`rebindError` exists because its response is a batch receipt, and adding a
zero-reader diagnostic key to `PublishMetaItemResponseSchema` buys no capability.

Part of #10219
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 23, 2026
…nd finds env-wide drafts (objectstack-ai#10291)

* fix(metadata-protocol): announce metadata:reloaded after a per-item publish

`POST /api/v1/meta/:type/:name/publish` promoted a draft to `active` and told
nobody. The lifecycle event that makes boot-cached consumers re-read had two
announcers — the metadata plugin's dev-artifact watcher, and the runtime
dispatcher after `POST /packages/:id/publish-drafts` (objectstack-ai#2576) — so publishing
item by item, which is what AI authoring and the item-level Studio doors do,
fired neither. Measured on a cloud rig: a record-change flow published as
`state='active'` produced no bind log and never executed until the kernel was
rebuilt and `kernel:ready` re-bound it (objectstack-ai#2560).

`publishMetaItem` now notifies through a new `onMetaItemPublished` seam, and
`ObjectQLPlugin.subscribeMetadataRebind` — armed for both protocol assembly
modes — turns that into `ctx.trigger('metadata:reloaded', { changed })`. The
producer notifies and the host announces because the protocol holds no kernel
hook bus, which is the same division `HttpDispatcher.announceKernelEvent` makes
for the batch door. `changed` carries the batch door's `{type}/{name}` spelling,
so a subscriber cannot tell the two doors apart.

The notification is deliberately NOT the existing `onMetadataMutation`: that one
is emitted from `runPublishSideEffects`, which the batch door runs once per
promoted draft, so announcing on it would fan a full kernel re-sync (schema DDL,
connector re-materialization, flow re-bind) out once per item of a
"publish whole app". A CONTROL case pins that the batch door emits none.

Awaited, so the publish's own 2xx means the re-bind was attempted rather than
queued; best-effort, so a throwing subscriber is logged at `warn` and never
turns a landed publish into an error. No wire key is added — the batch door's
`rebindError` exists because its response is a batch receipt, and adding a
zero-reader diagnostic key to `PublishMetaItemResponseSchema` buys no capability.

Part of objectstack-ai#10219

* fix(metadata-protocol): resolve the draft's own org scope on a per-item publish

The per-item publish door could not SEE a draft the batch door published fine.
Measured on a cloud rig: four AI-authored `view` drafts sitting at
`state='draft'` in `sys_metadata` — listed by the console's pending-changes
banner, promoted by its one-click "publish 4 changes" button — were each refused
per item with `404 [no_draft] No pending draft exists for view/<name> — nothing
to publish.`

`view` is one of the types the registry declares `allowOrgOverride: true`, so
`organizationIdForMetaWrite` threads the session's active organization into the
publish. The drafts were authored env-wide (`organization_id = NULL`), which is
what package / AI authoring writes, and a strict `organization_id = <org>`
lookup can never match them. `object` and `flow` are not org-overridable, which
is exactly why per-item publish worked for them and failed for views — one
symptom, split by a registry flag.

This is the single-item twin of objectstack-ai#3115. The batch door fixed it by DISCOVERING
each draft's scope (`listDrafts` surfaces a non-null-org caller's own rows and
the env-wide ones through its `$or`, and the promote targets `d.organizationId`);
the per-item door DEDUCED one instead. It now discovers, with the ADR-0005
precedence — an org holding its own draft publishes that one, and only an org
with no draft of its own falls through to the env-wide row it was already
authoring into.

Resolved before every gate below it, so the ADR-0010 lock check, the objectstack-ai#6190
org-scoped-write refusal and the promote all judge the one scope the row is
actually in. When neither scope holds a draft the caller's own scope is returned
unchanged, so a genuinely absent draft still raises the same `NO_DRAFT` refusal
from the scope it asked about. No `catch`: a driver failure must fail the
publish rather than resolve to a scope nobody verified.

Fixes objectstack-ai#10219

* chore: record the two new fake engines in the engine-double ledger

`check:engine-double-contract` retains a per-file pin for every test double that
routes its `delete()` / `update()` through `assertEngineDeleteDispatch` /
`assertEngineUpdateDispatch`. The two stub engines added with this card's
regression tests are pinned coverage the ledger did not know about, so it was
not protecting those files. Regenerated with `--write`: 4 rows added or grown,
0 lost.

Part of objectstack-ai#10219

---------

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview — a0e4cb5c Deployed Jul 4, 2026 by vercel[bot]
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant