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-draftsprotocol.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 (resyncFlowsFromMetadataresyncFlowsFromProtocol) 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>
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