Repository navigation
fix(service-automation): an installed package's connectors materialize again after a restart (#28591) - #28639
Conversation
…kage's connector Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U Co-authored-by: Claude <noreply@anthropic.com>
…ing kernel:ready, at kernel:bootstrapped Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U Co-authored-by: Claude <noreply@anthropic.com>
…nnectors registered during kernel:ready Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U Co-authored-by: Claude <noreply@anthropic.com>
…ped at install and at a restart; changeset Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U Co-authored-by: Claude <noreply@anthropic.com>
…ped at install and at a restart Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U Co-authored-by: Claude <noreply@anthropic.com>
…hydrate-connectors
…hydrate-connectors
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ab42fa612464d2a1a09e78c8ab2c62d73c23255c && git checkout ab42fa612464d2a1a09e78c8ab2c62d73c23255c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 28ef35e0e3799e1305e82c2d12f0515ca6bf519c 84e3f0ab42caded742dda94aa315d7148dc815da && git checkout -B drift-repro 28ef35e0e3799e1305e82c2d12f0515ca6bf519c && git merge --no-ff 84e3f0ab42caded742dda94aa315d7148dc815da
node scripts/docs-audit/affected-docs.mjs --json 28ef35e0e3799e1305e82c2d12f0515ca6bf519c
|
|
epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U 席位核对ACCEPT at head
Noted, not filed (agreed): the flows of an installed package survive a restart only because install-local's Landing: not governed. The seat lands it now ( |
Fixes #28591
Clause-②: no
Metadata-protocol: none
维护者速读
kernel:bootstrapped再读一次连接器声明;只有当它与start()里那次启动 reconcile 读到的不同,才以非致命方式再 reconcile 一次。重启后,已安装包的连接器重新上线。kernel:ready里才重新注册已安装的包,晚于 automation 在start()里唯一一次 reconcile,且不广播 reload。包还在、流程还能绑定,但连接器从GET /api/v1/automation/connectors消失,调用它的流程在派发时失败。start()之后又有插件注册了连接器时,启动才多一次 reconcile;普通启动不多跑。这次 reconcile 用重载语义:配置错的条目记 ERROR 并跳过,不让启动失败。回滚就是 revert 本 PR,行为退回到「重启后已安装包的连接器缺席」。packages/services/service-automation/src/plugin.ts(reconcileConnectorsRegisteredDuringBoot和它的kernel:bootstrapped钩子,以及declaredInstanceFingerprint);packages/services/service-automation/src/connector-boot-registered-reconcile.test.ts;packages/qa/dogfood/test/install-local-restart-connectors.dogfood.test.ts。pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/install-local-restart-connectors.dogfood.test.ts,预期Tests 5 passed (5);同一命令在main上是 1 failed(重启后连接器不在列表里)。What changes
Measured first, at BASE
d94dd3a6. TwobootShowcaseboots over one install-ledger directory. The first boot ranPOST /api/v1/marketplace/install-localwith an inline manifest that declares a provider-boundopenapiconnector with an INLINE document, and one run-by-name flow. Then the second boot read the listings.GET /automation/connectorsbefore the restart listed the connectorready.GET /marketplace/install-locallisted the package, andGET /automation/rstconn_manual_probeanswered 200, so the package's flows survive a restart.["showcase_status_api","showcase_status_openapi","showcase_mcp_tools","rest","slack"].How flows learn about rehydrated packages, and why connectors did not. The boot log of the second boot shows the order:
[Automation] materialized 3 provider-bound connector instance(s): the boot reconcile instart().[MarketplaceInstallLocal] rehydrated com.dogfood.restartconn@0.1.0: the install-localkernel:readyhandler.[Automation] Bound 33 flow(s) from the protocol at kernel:ready: 32 on the first boot. This is automation's ownkernel:readyflow sync, and it reads the protocol's view after the rehydrate.Flows therefore learn by reading in a consumer boot hook that runs after the producer's. Connectors had no such read. They are reconciled only in
start()and onmetadata:reloaded, and the rehydrate announces no reload.The fix takes the same road for connectors: a consumer-side boot read, region-local to
service-automation.AutomationServicePlugin.start()registers akernel:bootstrappedhook. The kernel fires that hook after everykernel:readyhandler has settled, and its docs name it for "reconcile work that consumes data produced by a later-starting plugin'skernel:readyhandler". So the hook does not depend on plugin order. The flow sync'skernel:readyread does depend on it. In the unit test the producer'skernel:readyhandler runs after automation's, and the new hook still materializes the connector.reconcileConnectorsRegisteredDuringBootreads the registry declaration (post: false, nosys_metadataread). It compares that declaration with thedeclaredInstanceFingerprintof the declaration the last reconcile read: the sorted name andconnectorInstanceSignatureof each enabled, provider-bound entry. It runs afatal: falsereconcile only when the two differ. A boot with no installs runs no second reconcile. The unit control shows exactly onereconcileDeclaredConnectorscall,{ fatal: true }. In the dogfood log, the first boot prints onematerialized … instance(s)line.kernel:readyis logged aterrorand that entry is skipped, as on install. The boot completes. A reconcile run that throws anyway is reported once atwarn: a connector that is not live is a functional degradation.Why not candidate (a), one
metadata:reloadedannounced by the rehydrate. It would fire insidekernel:ready, ahead of handlers that have not run yet. It would also re-run every reload consumer on every boot with installs: ObjectQL's hook and action re-sync and schema sync, the flow re-sync, i18n, and security invalidation. Flows do not need that. TheannounceHotInstalldocblock records the rehydrate's choice not to announce. Option (b) changes one consumer and leavesmarketplace-install-local-plugin.tsuntouched.content/docs/automation/connectors.mdx, "Failure modes at a glance": one row. A bad entry of an installed package is logged and skipped at the install and at a restart. Before, the table read "hard boot error" for every configuration fault outside a runtime reload..changeset/28591-rehydrated-package-connectors.md:@objectstack/service-automationpatch.No
packages/specchange.Tests
packages/qa/dogfood/test/install-local-restart-connectors.dogfood.test.ts(new, permanent). It runs two boots over one ledger and asserts five things: the pre-restart precondition; the package listed after the restart and not markednotLoaded; the connectorreadywith itsGetThingsaction after the restart; the flow served by the engine after the restart; and the host'sshowcase_status_openapistill listed (control).packages/services/service-automation/src/connector-boot-registered-reconcile.test.ts(new, 4 cases):kernel:readyhandler is listed afterbootstrap()and dispatched end-to-end through a flowconnector_action, and the host entry's factory runs once;start()runs ONE reconcile;[{fatal:true},{fatal:false}];errorand skipped, the boot completes, and its sibling materializes.Runs at head
84e3f0ab, after the second merge oforigin/main:Restart dogfood case: BASE red, fix green
d94dd3a6(the fix not in the tree, service-automation built from BASE):pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/install-local-restart-connectors.dogfood.test.ts. Result:Tests 1 failed | 4 passed (5). The failure wasafter the restart, the package's connector is listed ready …withAssertionError: ["showcase_status_api","showcase_status_openapi","showcase_mcp_tools","rest","slack"]: expected undefined to be defined. The flow and host-connector cases were green, so flows survive a restart.63d2ffde, then84e3f0abafter rebuilding: same command,Tests 5 passed (5). On the second boot the log printsrehydrated com.dogfood.restartconn@0.1.0, thenBound 33 flow(s) … at kernel:ready, thenmaterialized connector instance 'rstconn_fixture' via provider 'openapi' (1 action(s)), all beforeBootstrap complete. The first boot (empty ledger) prints a singlematerialized 3 provider-bound connector instance(s)beforeBootstrap complete: no second reconcile.Unit suite, typecheck, and the one ablation
pnpm --filter @objectstack/service-automation test(the whole suite, not file-selected):Test Files 251 passed (251),Tests 3177 passed (3177)at84e3f0ab. It was 251 and 3173 atc5d3ce33.pnpm --filter @objectstack/service-automation typecheck(tsc pluscheck:test-typecheck): exit 0.pnpm --filter @objectstack/dogfood typecheck: exit 0. Withtsc --listFiles, each program holds its new test file (1 hit each).bcbb7ad8(plugin.ts blobe344a65d).node scripts/ablation-replace.mjsswapped the anchorawait this.reconcileConnectorsRegisteredDuringBoot(ctx);forvoid ctx; /* ABLATED_28591 */. The anchor count went 1 to 0 and the blob wente344a65dto05a767c8. The unit file imports./plugin.jsfrom source, so no build sits in the path. Result:Tests 3 failed | 1 passed (4). All three cases that need the second reconcile went red, and the plain-boot control stayed green. Restored: blob equals HEADe344a65dandgit diff HEADis empty.Acceptance notes
connector_actionruns against the rehydrated-shape entry. The dogfood case asserts the listedreadystate and the action key. Its document's server is127.0.0.1:9, so a real dispatch there would only measure an unreachable upstream.kernel:readyhandler ran before automation's. That order is not declared anywhere. The connector half now useskernel:bootstrappedand does not depend on it. Whetheros serve's composition orders the two the same way was not measured.Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths: 5-path change set against merge basea9c6eac6f, 91 commands. Each was run at84e3f0abwith its exit code recorded before any pipe, and all 91 exited 0.--ran:91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN.check:skill-examplesandcheck:dual-build-cjs-loadsread the dists of 7 packages outside this diff. They were built (turbo, 75/75 tasks) before the run.check-platform-checklistis not owed: no export was renamed or deleted.test:repois declared to CI: no repo-project test was touched.grep -naPexit 1 (no match).Generated by Claude Code