fix(automation): bind a flow published while the server runs, without a restart - #2576
Merged
Merged
Conversation
… 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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Jul 4, 2026
Closed
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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
The publish path fired no rebind signal.
POST /packages/:id/publish-drafts→protocol.publishPackageDraftspromotes drafts to active but emitted no event the automation service listens to. The runtime dispatcher now announcesmetadata:reloadedafter 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 asrebindError. Fires only when something was published/unhidden.The runtime re-sync read the wrong source. The automation service's
metadata:reloadedre-sync pulledmetadata.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 readsprotocol.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 andGET /meta/flowuse — via a sharedreadFlowDefsFromProtocolhelper, while keeping the teardown of flows removed from the artifact. A failed or unavailable protocol read returnsnulland is a no-op — it never tears down live flows. Thekernel:readycold-boot bind stays additive (no teardown), reusing the same helper.Only one
metadata:reloadedsubscriber 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— extractreadFlowDefsFromProtocol;metadata:reloadedre-sync (resyncFlowsFromMetadata→resyncFlowsFromProtocol) reads the protocol and keeps teardown;syncFlowsFromProtocol(kernel:ready) reuses the helper, stays additive.packages/runtime/src/http-dispatcher.ts— announcemetadata:reloadedafter a successfulpublish-drafts.Tests
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, notmetadata.list(the exact bug); does not tear down live flows on a failed protocol read. The first/third fail on the pre-fix code.http-dispatcher.test.tscases: publish-drafts announcesmetadata:reloaded/ does not announce when nothing was published.flow-hot-reload.test.tsfake service (metadata → protocol source).@objectstack/service-automation231/231,@objectstack/runtime447/447; eslint clean.Verification (end-to-end, clean instance, no restart)
On a fresh showcase (
:3210):showcase_pubtestas a draft in a writable base package.showcase_task→ flow did not fire (negative control — still a draft).POST /packages/:id/publish-draftswithout restarting → flow became active; response carried norebindError.process.stderr.writeprobe inAutomationEngine.execute, sincelogger.infois suppressed andsys_automation_runonly records suspended runs). Probe removed before commit.🤖 Generated with Claude Code