Repository navigation
fix(cloud-connection): an install-local uninstall withdraws the package's flows from the automation engine and unbinds their triggers (#27395) - #27530
Conversation
…'s flows live in the engine (red) Pins the door's half (one metadata:reloaded, last, after the withdrawal and the cleanups; a failed re-sync is a failed outcome on the response) and the composed half on the real showcase boot (GET /automation/NAME 404, the trigger door 404 with no run row, the record trigger unbound). Both fail on the base: the door announces nothing. Claude-Session: https://claude.ai/code/session_01L6bhgTsswEK7wz8UFCtF5z Co-authored-by: Claude <noreply@anthropic.com>
…reloaded, so the engine withdraws the package's flows and unbinds their triggers The DELETE door withdrew the package from the registry and ran the cleanups but told the running kernel nothing; the automation engine withdraws a flow only in its re-sync on metadata:reloaded, so the uninstalled package's flows stayed active and triggerable until an unrelated reload. The door now announces the same re-sync the install's hot load does, last, after the withdrawal and the cleanups; a re-sync that does not run is one failed outcome on the response. The existing uninstall rigs gain the trigger every kernel context has. Claude-Session: https://claude.ai/code/session_01L6bhgTsswEK7wz8UFCtF5z Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 5 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 5ed5a730f021089920df2ebfcb45b9f3219e0016 && git checkout 5ed5a730f021089920df2ebfcb45b9f3219e0016
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 57c51aab4d2f63293676e9412daed8a5fc54c1bc 7cb1ca3adb07d6f0fc545dd3cc3d6109797410f3 && git checkout -B drift-repro 57c51aab4d2f63293676e9412daed8a5fc54c1bc && git merge --no-ff 7cb1ca3adb07d6f0fc545dd3cc3d6109797410f3
node scripts/docs-audit/affected-docs.mjs --json 57c51aab4d2f63293676e9412daed8a5fc54c1bc
|
|
domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw 维护者速读(席位终稿)
席位核对 — ACCEPT ·
|
|
domain:server#1 · os-sales · session_01L6bhgXcN9HaZYSEUdJWEjw Landed ·
|
| Reading | Value |
|---|---|
| PR #27530 | merged at 2026-10-29T11:36Z, merge commit aef631b80b, head 7cb1ca3a (the ACCEPTed head) |
| Card #27395 | closed / completed by this PR's Fixes |
State change: pm:dispatched removed from #27395. No other card was closed by this PR's body. The next QA wave retests it (anchor #27377 §5).
Generated by Claude Code
Requested by Sales · project thread
Fixes #27395
Clause-②: no
Metadata-protocol: none
Skills-docs: none — nothing a metadata author writes changes, and no skill or doc teaches the uninstall leaving flows live; the fix makes the door's existing note true
Ruling-ref: triage 6489212277
维护者速读
DELETE /api/v1/marketplace/install-local/ID)在撤回注册表、跑完卸载清理之后,最后再宣告一次metadata:reloaded(和安装时热加载宣告的是同一个信号、同一个载荷)。自动化引擎收到后重新同步,把这个包的流程撤掉,并在同一步解绑它们的记录触发和定时触发。宣告失败时,卸载照样成功,cleanups里多一条名为metadata:reloaded的失败结果,说明里写明流程可能留到下次重启。active:按名触发照样跑、写运行记录,记录触发和定时触发也还挂着,直到别的什么事碰巧触发一次重载(QA QA run · release-18.3.0-w3-manual-api (25/25) · c4c515ad · 2026-10-29 · 23 PASS / 2 PARTIAL / 0 FAIL / 0 BLOCKED / 0 NOT-RUN #27392 复现两次)。卸载接口自己的说明还写着「已从运行中的内核撤回」。按 triage 裁决,卸载走重载用的同一条撤回路径,不另起一份引擎侧清单。packages/cloud-connection/src/marketplace-install-local-plugin.ts里handleUninstall末尾新加的宣告和说明文字的拼装,以及新方法announceUninstall;然后是 dogfood 新测试packages/qa/dogfood/test/install-local-uninstall-flows-withdrawn.dogfood.test.ts。pnpm --filter @objectstack/cloud-connection exec vitest run --maxWorkers=2 src/marketplace-install-local-uninstall-resync.test.ts,应见 7 个用例全部通过(宣告一次、在撤回和清理之后、失败作为一条结果上报,以及没卸载成功时不宣告)。Under the claim
6493772753.DELETE /api/v1/marketplace/install-local/:manifestIdnow announcesmetadata:reloadedafter it withdraws the package from the registry and runs the cleanups, so the automation engine withdraws the package's flows and unbinds their triggers in the same request. Before, nothing told the engine: the flows stayedactiveand triggerable until some unrelated reload.Branch
claude/project-thread-30b1mx(the card thread's harness-pinned branch; it names no issue, so the claim is the identity record), cut fromorigin/mainateef83734d9, withorigin/mainded67bfb59merged in before this PR was opened.What changes
The mechanism, measured
SchemaRegistry.uninstallPackage, withdrew the object copies from themetadataservice, ran the protocol's uninstall cleanups, and returned. It announced nothing.resyncFlowsFromProtocol, onmetadata:reloaded. It re-reads the protocol's flow view and callswithdrawFlowfor every flow it had armed that the view no longer lists.withdrawFlowcallsdeactivateFlowTriggerfirst, so the trigger is unbound in the same step.announceHotInstall,{ changed: ['app/ID'] }). That is why a view publish, or any other package's install, cleared the stale flows in the QA run.So the dispatch hypothesis held as written. The ruling (triage 6489212277, verbatim): "The uninstall door drives the same unregister path a reload uses. It is one teardown, not a second engine-side list."
The change (
packages/cloud-connection/src/marketplace-install-local-plugin.ts)handleUninstall: after the cleanups,announceUninstall(ctx, manifestId)runs last. It is never run before the withdrawal and the cleanups, for the window the door's existing comment describes. It is skipped when the registry refused the withdrawal: the package is still registered, so a re-sync would only read it back.announceUninstallannouncesmetadata:reloadedwith the install's payload,{ changed: ['app/MANIFEST_ID'] }. It never throws. If a subscriber throws, or the context cannot announce, it returns one failed outcome namedmetadata:reloaded(the newKERNEL_RESYNCconstant, named for the event, asregistry.uninstallPackageandmetadata.unregisterare named for their verbs). That outcome rides oncleanupsthe way a refused registry withdrawal does. The cause and the remedy (restart) go to the operator log atwarn, the level the install's announce failure uses, and never onto the wire.metadata:reloadedentry. The copy-withdrawal sentence is unchanged, byte for byte. When everything ran, the note's claim ("withdrawn from the running kernel") is now true.DELETEparagraph says so.packages/services/service-automation/src/plugin.tswas not needed and is not touched. Nopackages/specchange.Changeset
.changeset/27395-install-local-uninstall-flow-resync.md:@objectstack/cloud-connectionpatch, withClause-②: noandMetadata-protocol: nonecopied from the claim.Tests
packages/qa/dogfood/test/install-local-uninstall-flows-withdrawn.dogfood.test.ts. One showcase boot withMarketplaceInstallLocalPluginmounted. It installs an inline package that ships two flows: one run by name, and one bound torecord-after-insertonshowcase_category, an object that outlives the uninstall. Five cases:_packageId), and each runs once: the first through the trigger door, the second through an insert. Each leaves one run in the engine's listing and onesys_automation_runrow.GET /meta/flow/NAMEanswers 404 (the half that already worked).GET /automation/NAMEanswers 404 for both flows.POST /automation/NAME/triggeranswers 404, and no run is listed or written.packages/cloud-connection/src/marketplace-install-local-uninstall-resync.test.ts, 7 cases. One ordered event log over the registry, the cleanup runner and the kernel'striggerpins: exactly onemetadata:reloaded, namingapp/ID, after the withdrawal and after the cleanups, with the ledger entry gone. Nothing is announced for a refused caller, an unknown id, a failed ledger write, or a refused registry withdrawal. A throwing subscriber and a context with notriggereach give one failedmetadata:reloadedoutcome, last; the cause stays in the log.…-uninstall-withdrawal,…-uninstall-cleanups,…-uninstall-meta-reads) gaintrigger: async () => undefinedon their kernel context. Every realPluginContextdeclarestrigger. Without it, these rigs now get the failed re-sync outcome oncleanups, which theirtoEqualassertions oncleanupsand their empty-warning assertions refuse. No assertion changed.Verification
All local readings below are on
7cb1ca3adb(the merge ofded67bfb59) unless a line says otherwise.Red first, on the base source (eef8373)
The red tests were committed first (
243399d5). Each leg wrote the base blob of the plugin file over the fix withgit show, ran the pins, and restored fromHEAD. Every restore was proved bygit hash-objectequal to the HEAD blob and a cleangit status --porcelain.src/marketplace-install-local-uninstall-resync.test.ts→ 3 failed / 4 passed. The three positive cases fail. The four "announces nothing" controls pass on the base, as they should.@objectstack/cloud-connectionfrom the base blob (ablation-dist-preflight … 'announceUninstall' --absent→ the marker is absent from all 6 built files) → 3 failed / 2 passed. The precondition and the 200 passed; the three engine cases failed with the QA run's exact readings:GET /automation/qauninst_manual_probe→ 200status: active,_provenance: package,_packageId: com.example.qauninst;Green, and the ablation (ablation-replace, fix committed first)
announceUninstallpresent in 4 built files, tree clean): dogfood 5/5; unit 7/7.scripts/ablation-replace.mjs). The anchorconst resync = withdrawal ? undefined : await this.announceUninstall(ctx, manifestId);hit once and was replaced byvoid this.announceUninstall; const resync: UninstallCleanupOutcome | undefined = ((globalThis as any).ablated27395 = undefined);. Blobe25af962d8ba→3bdeeea26dea. Build exit 0, and preflight found the marker in 2 built files. Result: dogfood 3 failed / 2 passed (the same three cases), unit 3 failed / 4 passed. The restore checked blob == HEAD and an emptygit diff HEAD.voidread is not counted: its JS bundles carried the marker, but the DTS build exited 1 on TS6133 (the now-unused private). It gave the same 3 red / 3 red.ablated27395 --absentfound the marker absent from all 6 built files, tree clean; dogfood 5/5.Package suite, typecheck and gates
On
7cb1ca3adb, afterpnpm install --frozen-lockfileand a rebuild of the dogfood closure plus@objectstack/driver-turso(lock VERDICT command-exit 0, 67/67 tasks):pnpm --filter @objectstack/cloud-connection test→ VERDICT command-exit 0, Test Files 60 passed (60), Tests 754 passed (754).pnpm --filter @objectstack/cloud-connection typecheck→ VERDICT command-exit 0.pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/install-local-uninstall-flows-withdrawn.dogfood.test.ts→ VERDICT command-exit 0, 5 passed (5).node scripts/pm/dispatch-gates.mjs --commandsderived 64 commands on this tree. All 64 ran, each exit captured before any pipe; 64 exit 0.pnpm check:dual-build-cjs-loadsfirst answered exit 3 (seven packages outside the closure had nodist/); those seven were built and it then exited 0, so that is the recorded reading.dispatch-gates --ran→ "64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero (every line carries its exit code, none is 3).test:repowas not run locally: the diff edits no repo-project test and the derivation does not name it. CI'sTest Coreruns it.Acceptance notes
6493772753named this branch.git merge-tree --write-treeof this head against feat(service-automation, runtime, spec): connector credential store and open-tier credentialRef chain (#27237) #27344's head355edc0624exits 0 (tree674cf2d5f8). Whichever lands second mergesmain.announceHotInstall, unchanged): the install answers 200 and says nothing on the wire. That is outside this card; this PR reports the uninstall's failure on the response because the uninstall already reports partial completion there (cleanups).DELETE /api/v1/packages/:id(packages/runtime/src/domains/packages.ts) also withdraws throughdeletePackage/uninstallPackageand announces nometadata:reloaded. Whether its flows stay live depends on whatdeletePackage's stored-row deletes signal to the engine, and that was not measured here. It is a read-only inference, so per the dev contract it is noted, not filed.Human Minimum
packages/specholds no runtime logic. (Not touched.)KERNEL_RESYNC,announceUninstall).main, one pull request.@objectstack/cloud-connection; both declaration lines are filled in here and in the changeset.packages/specnot touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01L6bhgTsswEK7wz8UFCtF5z
Generated by Claude Code