fix(service-automation): reconcile declarative connectors against a set the metadata reload actually refreshes (#7742) - #7847
Conversation
…et the metadata reload actually refreshes (#7742) A connector edit followed by a metadata reload changed nothing: no reconcile, no teardown, no re-materialize — the pre-edit connector kept serving until the process restarted. `os dev` masks it (the serve child restarts on recompile), so the trigger that walks into it is a Studio package publish into a running server. Confirmed on origin/main before the fix: an edited, an added and a deleted connector are all no-ops on the reload path. The reconcile was fine; its INPUT was a boot snapshot. `reconcileDeclaredConnectors` read `ql.registry.listItems('connector')`, and no reload path re-ingests connector items into that registry — ObjectQL's own `metadata:reloaded` handler re-ingests the payload's OBJECT definitions and stops there. So the reconcile compared the boot world against itself and found nothing to do. Every existing test drove the reload through a hand-mutated fake registry, which is why the path looked covered. `readDeclaredConnectorItems` now folds the sources a reload does refresh over that registry read, one per trigger: * the artifact carried on the `metadata:reloaded` payload — the dev/HMR trigger, and the only place an edited or deleted definition exists. Held as plugin state, so a degraded-instance retry firing minutes later does not fall back to the boot snapshot and rebuild the pre-edit instance. The fold is a replacement scoped to the packages the artifact speaks for (its manifest id + the `_packageId` stamped on its items), not a union: a union can never observe a deletion, while an unscoped replacement would tear down a connector another package contributed. * `protocol.getMetaItems({ type: 'connector' })` — the flattened `/meta` view the flow re-sync already reads, which layers the `sys_metadata` rows a publish promotes to active over the registry (overlay wins). Post-boot reconciles only: at boot the registry was just built and is current by construction, and that read costs a `sys_metadata` query and can fail — neither belongs on the fail-loudly boot path. Both reads fail safe. An absent, failing, or empty-while-the-registry-is-not answer is "no answer" and never tears down a live connector, and an announcement with no connector collection at all (a publish's bare `{ changed }`, or an artifact with no `connectors:` key) leaves every instance alone — only an artifact carrying an EMPTY array is the honest "none left". An unchanged entry still hashes to the same signature, so reloads do not churn live connections. The descriptor audit beside the reconcile reads the same declaration, so its warning describes the stack as it is now. `readFlowDefsFromProtocol`'s body is now the shared `readMetaItemsFromProtocol` — same normalization, same `null`-means-no-answer contract, byte-identical log message for the flow type. New tests use a REAL `SchemaRegistry` and deliberately never mutate it across the reload, which is the fact the old harness hid. Reverse-verified: with the fix reverted, the edit / add / delete / publish cases fail exactly the way the QA run observed, while the three never-tear-down guards pass on both sides.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also reference the affected code. These are read-only:
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31535703749 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
Queue-ejection triage receipt (services seat): known flaky signature — re-queueing once, unchanged. Readings: queue build 31535703749 failed in Action: one re-queue as-is. A second ejection on this signature hands the PR to the queue steward rather than another self re-queue. Generated by Claude Code |
Fixes #7742
What was broken
A connector edit followed by a metadata reload changed nothing: no reconcile, no teardown, no re-materialize — the pre-edit connector kept serving until the process restarted.
os devmasks it (the serve child restarts on recompile), so the trigger that walks straight into it is a Studio package publish into a running server.Premise verified on
origin/mainbefore implementing. A test driving the real reload shape against a realSchemaRegistryshows the edit, the addition and the deletion are all no-ops:Root cause
The reconcile was fine; its input was a boot snapshot.
reconcileDeclaredConnectorsreadql.registry.listItems('connector'), and no reload path re-ingests connector items into that registry — ObjectQL's ownmetadata:reloadedhandler re-ingests the payload's OBJECT definitions (ingestReloadedObjects) and stops there. So the reconcile compared the boot world against itself and found nothing to do.Every existing connector reload test drove the reload through a hand-mutated fake registry (
state.declared = nextand only then fire the hook), which is why the path looked covered.The fix
readDeclaredConnectorItemsfolds the sources a reload does refresh over that registry read — one per trigger:metadata:reloadedpayload — the dev/HMR trigger, and the only place an edited or deleted definition exists at all. Held as plugin state, so a degraded-instance retry firing minutes later does not fall back to the boot snapshot and rebuild the pre-edit instance. The fold is a replacement scoped to the packages the artifact speaks for (its manifest id + the_packageIdstamped on its items), not a union: a union can never observe a deletion, while an unscoped replacement would tear down a connector another package contributed.protocol.getMetaItems({ type: 'connector' })— the flattened/metaview the flow re-sync already reads, which layers thesys_metadatarows a publish promotes to active over the registry (mergePackageAwareOverlay: the overlay wins). Post-boot reconciles only — at boot the registry was just built and is current by construction, and this read costs asys_metadataquery and can fail, so neither belongs on the fail-loudly boot path.Both reads fail safe: an absent, failing, or empty-while-the-registry-is-not answer is treated as no answer and never tears down a live connector; an announcement carrying no connector collection at all (a publish's bare
{ changed }, or an artifact with noconnectors:key) leaves every instance alone. Only an artifact carrying an empty array is the honest "none left". An unchanged entry still hashes to the same signature, so reloads do not churn live connections.The descriptor audit beside the reconcile reads the same declaration, so its warning describes the stack as it is now rather than as it booted.
readFlowDefsFromProtocol's body became the sharedreadMetaItemsFromProtocol— same normalization, samenull-means-no-answer contract, byte-identical log message for the flow type.Scope note
Deliberately not changed: the artifact reload path in
packages/metadata*. Making the reload re-ingest connector items into the registry for all consumers isdomain:metadataterritory; this lands the services-side shape the dispatch named — the automation plugin's reconcile reading a fresh input.Tests
connector-reload-reingest.test.ts(8 tests) uses a realSchemaRegistryand deliberately never mutates it across the reload — the registry going stale is the fact under test. Assertions are on the observable effect (the old instance'sclose(), a re-materialization carrying the newproviderConfig), not on call counts: edit / add / delete / another package's connector surviving / Studio-publish view / empty + failing view / no-collection payload.Reverse-verified: with the fix reverted and the tests kept, the four behavioural cases fail exactly the way the QA run observed (no-op reconcile), while the never-tear-down guards pass on both sides.
Gates
@objectstack/service-automationsuite: 928 passed / 76 files--filter @objectstack/service-automation...): 20/20tsc --noEmit: no new errors (3 pre-existing in the untouchednested-region-parity.test.ts)pnpm check:docs-audit-scope: passconnector-mcp23,connector-rest16): pass.changeset/connector-reload-reingest.md(patch)One flake observed once and not reproducible in isolation or on re-run:
engine.test.ts > should execute unconditional branches in parallel(a wall-clock parallelism assertion, untouched by this diff).Generated by Claude Code