Skip to content

Commit aef631b

Browse files
fix(cloud-connection): an install-local uninstall withdraws the package's flows from the automation engine and unbinds their triggers (#27395) (#27530)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuTx8CYB9VacnACVpw5eNqMx)_ 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` 的失败结果,说明里写明流程可能留到下次重启。 - **为什么改**:以前卸载后,元数据里流程已经 404,引擎里却还是 `active`:按名触发照样跑、写运行记录,记录触发和定时触发也还挂着,直到别的什么事碰巧触发一次重载(QA #27392 复现两次)。卸载接口自己的说明还写着「已从运行中的内核撤回」。按 triage 裁决,卸载走重载用的同一条撤回路径,不另起一份引擎侧清单。 - **风险与回滚**:只改卸载接口这一处,安装路径不动。多出的一次重新同步和安装时那次完全一样(流程重同步、作者钩子与动作重同步、幂等的建表同步)。注册表拒绝撤回时不宣告,因为包还在。回滚 = revert 本 PR。 - **先看**:`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/:manifestId` now announces `metadata:reloaded` after 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 stayed `active` and 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 from `origin/main` at `eef83734d9`, with `origin/main` `ded67bfb59` merged in before this PR was opened. ## What changes ### The mechanism, measured - The uninstall door withdrew the package with `SchemaRegistry.uninstallPackage`, withdrew the object copies from the `metadata` service, ran the protocol's uninstall cleanups, and returned. It announced nothing. - service-automation withdraws a flow in exactly one place after boot: `resyncFlowsFromProtocol`, on `metadata:reloaded`. It re-reads the protocol's flow view and calls `withdrawFlow` for every flow it had armed that the view no longer lists. `withdrawFlow` calls `deactivateFlowTrigger` first, so the trigger is unbound in the same step. - The install door announces that signal (`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. - `announceUninstall` announces `metadata:reloaded` with 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 named `metadata:reloaded` (the new `KERNEL_RESYNC` constant, named for the event, as `registry.uninstallPackage` and `metadata.unregister` are named for their verbs). That outcome rides on `cleanups` the way a refused registry withdrawal does. The cause and the remedy (restart) go to the operator log at `warn`, the level the install's announce failure uses, and never onto the wire. - The note and the info log are composed per outcome. When the re-sync did not run, the note says the engine can still hold the package's flows until the next restart, naming the `metadata:reloaded` entry. The copy-withdrawal sentence is unchanged, byte for byte. When everything ran, the note's claim ("withdrawn from the running kernel") is now true. - ⛔ Nothing in the door withdraws a flow or unbinds a trigger itself. No engine-side list of the package's flows is kept. - The module note's `DELETE` paragraph says so. `packages/services/service-automation/src/plugin.ts` was not needed and is not touched. No `packages/spec` change. ### Changeset `.changeset/27395-install-local-uninstall-flow-resync.md`: `@objectstack/cloud-connection` patch, with `Clause-②: no` and `Metadata-protocol: none` copied from the claim. ## Tests - **New, end to end:** `packages/qa/dogfood/test/install-local-uninstall-flows-withdrawn.dogfood.test.ts`. One showcase boot with `MarketplaceInstallLocalPlugin` mounted. It installs an inline package that ships two flows: one run by name, and one bound to `record-after-insert` on `showcase_category`, an object that outlives the uninstall. Five cases: 1. PRECONDITION: the install hot-loads both flows as the package's (`_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 one `sys_automation_run` row. 2. The uninstall answers 200 with no failed outcome, and `GET /meta/flow/NAME` answers 404 (the half that already worked). 3. `GET /automation/NAME` answers 404 for both flows. 4. `POST /automation/NAME/trigger` answers 404, and no run is listed or written. 5. An insert into the watched object runs nothing: the record trigger was unbound in the same step. - **New, the door's half:** `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's `trigger` pins: exactly one `metadata:reloaded`, naming `app/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 no `trigger` each give one failed `metadata:reloaded` outcome, last; the cause stays in the log. - **Moved pins (rig only):** the three existing uninstall rigs (`…-uninstall-withdrawal`, `…-uninstall-cleanups`, `…-uninstall-meta-reads`) gain `trigger: async () => undefined` on their kernel context. Every real `PluginContext` declares `trigger`. Without it, these rigs now get the failed re-sync outcome on `cleanups`, which their `toEqual` assertions on `cleanups` and their empty-warning assertions refuse. No assertion changed. ## Verification All local readings below are on `7cb1ca3adb` (the merge of `ded67bfb59`) unless a line says otherwise. <details><summary>Red first, on the base source (eef8373)</summary> The red tests were committed first (`243399d5`). Each leg wrote the base blob of the plugin file over the fix with `git show`, ran the pins, and restored from `HEAD`. Every restore was proved by `git hash-object` equal to the HEAD blob and a clean `git status --porcelain`. - Unit: `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. - Dogfood, after rebuilding `@objectstack/cloud-connection` from 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` → 200 `status: active`, `_provenance: package`, `_packageId: com.example.qauninst`; - the trigger → 200, the run completed; - the record flow: listed 2 / rows 2, expected 1 / 1. </details> <details><summary>Green, and the ablation (ablation-replace, fix committed first)</summary> - Green, after the fix was rebuilt (preflight: `announceUninstall` present in 4 built files, tree clean): dogfood 5/5; unit 7/7. - Mutate leg (`scripts/ablation-replace.mjs`). The anchor `const resync = withdrawal ? undefined : await this.announceUninstall(ctx, manifestId);` hit once and was replaced by `void this.announceUninstall; const resync: UninstallCleanupOutcome | undefined = ((globalThis as any).ablated27395 = undefined);`. Blob `e25af962d8ba` → `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 empty `git diff HEAD`. - A first mutate leg without the `void` read 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. - Restore leg: rebuild exit 0; preflight `ablated27395 --absent` found the marker absent from all 6 built files, tree clean; dogfood 5/5. </details> <details><summary>Package suite, typecheck and gates</summary> On `7cb1ca3adb`, after `pnpm install --frozen-lockfile` and 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). - Gates: `node scripts/pm/dispatch-gates.mjs --commands` derived 64 commands on this tree. All 64 ran, each exit captured before any pipe; 64 exit 0. `pnpm check:dual-build-cjs-loads` first answered exit 3 (seven packages outside the closure had no `dist/`); 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:repo` was not run locally: the diff edits no repo-project test and the derivation does not name it. CI's `Test Core` runs it. </details> ## Acceptance notes - **Serial order.** No code was edited before the claim `6493772753` named this branch. - **Overlap with #27344** (os-bill, open, also edits this plugin file). Its hunks are at base lines 214, 734, 1954, 1969 and 3464. This diff's hunks are at 111, 303 and 2291 to 2308, so they are disjoint. A bare-clone `git merge-tree --write-tree` of this head against #27344's head `355edc0624` exits 0 (tree `674cf2d5f8`). Whichever lands second merges `main`. - **The install door's own re-sync failure** is still only logged (`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`). - **A sibling door, not measured.** The runtime's `DELETE /api/v1/packages/:id` (`packages/runtime/src/domains/packages.ts`) also withdraws through `deletePackage` / `uninstallPackage` and announces no `metadata:reloaded`. Whether its flows stay live depends on what `deletePackage`'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 - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (Not touched.) - [x] 2. Every name I added uses the casing for its kind (`KERNEL_RESYNC`, `announceUninstall`). - [x] 3. No formatter ran; lint is CI's run. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A patch changeset for `@objectstack/cloud-connection`; both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English, apart from the maintainer quick-read block the project asks for. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L6bhgTsswEK7wz8UFCtF5z --- _Generated by [Claude Code](https://claude.ai/code/session_01L6bhgTsswEK7wz8UFCtF5z)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6892888 commit aef631b

7 files changed

Lines changed: 623 additions & 6 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/cloud-connection': patch
3+
---
4+
5+
fix(cloud-connection): an install-local uninstall withdraws the package's flows from the automation engine at once, and unbinds their triggers
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`DELETE /api/v1/marketplace/install-local/:manifestId` withdrew the package from the registry and ran its uninstall cleanups, but never told the running kernel. The automation engine withdraws a flow only when it re-syncs on `metadata:reloaded`, so the uninstalled package's flows stayed registered: `GET /api/v1/meta/flow/NAME` answered 404 while `GET /api/v1/automation/NAME` still answered `active`, `POST /api/v1/automation/NAME/trigger` still ran the flow and wrote a run row, and its record and schedule triggers stayed armed. That lasted until some unrelated reload, such as a view publish or another package's install. The door's note said the package was withdrawn from the running kernel.
11+
12+
- **Now.** After the registry withdrawal and the cleanups, the door announces `metadata:reloaded` with `{ changed: ['app/MANIFEST_ID'] }`, the same signal the install door's hot load announces. The engine's re-sync withdraws every flow it had armed that the package no longer provides and unbinds each one's trigger. Right after the `DELETE` returns, `GET /api/v1/automation/NAME` and its trigger door answer 404, and a write to a watched object runs nothing.
13+
- **When the re-sync does not run.** If a subscriber throws, or the kernel context cannot announce, the uninstall still answers 200, and `cleanups` carries one failed outcome named `metadata:reloaded`. The note then says the engine can still hold the package's flows until the next restart. The cause goes to the operator log at `warn`, with the restart as the remedy.
14+
- **Unchanged.** Nothing is announced when the uninstall did not happen (a refused caller, an id this door never installed, a ledger write that failed), or when the registry refused the withdrawal, because the package is still registered.

‎packages/cloud-connection/src/marketplace-install-local-plugin.ts‎

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@
111111
* serving them at once too (#24070), then runs the uninstall
112112
* cleanups domain plugins registered with the protocol (#21490) —
113113
* the package's permission sets and their grants go with it — and
114-
* reports each outcome as `cleanups`, a refused withdrawal included.
114+
* last announces the withdrawal as `metadata:reloaded`, so the
115+
* automation engine withdraws the package's flows and unbinds their
116+
* triggers at once (#27395); it reports each outcome as `cleanups`,
117+
* a refused withdrawal or a re-sync that did not run included.
115118
*
116119
* Persistence layout:
117120
* <cwd>/.objectstack/installed-packages/<safe-manifest-id>.json
@@ -303,6 +306,13 @@ const REGISTRY_WITHDRAWAL = 'registry.uninstallPackage';
303306
*/
304307
const OBJECT_COPY_WITHDRAWAL = 'metadata.unregister';
305308

309+
/**
310+
* [#27395] The outcome name this door reports when the running kernel was not
311+
* told of the withdrawal — named for the event it announces, the re-sync the
312+
* automation engine withdraws an uninstalled package's flows on.
313+
*/
314+
const KERNEL_RESYNC = 'metadata:reloaded';
315+
306316
/**
307317
* [#8976] The capability every MUTATING install-local route demands.
308318
*
@@ -2291,13 +2301,28 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
22912301
...(copies ? [copies] : []),
22922302
...await this.runUninstallCleanups(ctx, manifestId, admission.userId, declared),
22932303
];
2294-
const withdrawals = (withdrawal ? 1 : 0) + (copies ? 1 : 0);
2304+
// [#27395] LAST — after the registry withdrawal and the cleanups, never
2305+
// before, for the window described above — announce the withdrawal as
2306+
// `metadata:reloaded`, the re-sync the install's hot load announces, so
2307+
// the consumers that hold what they read from a package re-read it now
2308+
// that it is gone. ⛔ Nothing here withdraws a flow or unbinds a trigger
2309+
// itself: the uninstall drives the one teardown a reload drives. Not
2310+
// when the registry refused: the package is still registered, so a
2311+
// re-sync would only read it back.
2312+
const resync = withdrawal ? undefined : await this.announceUninstall(ctx, manifestId);
2313+
if (resync) cleanups.push(resync);
2314+
const withdrawals = (withdrawal ? 1 : 0) + (copies ? 1 : 0) + (resync ? 1 : 0);
22952315
ctx.logger?.info?.(
22962316
`[MarketplaceInstallLocal] uninstalled ${manifestId} (cached manifest removed; `
22972317
+ (withdrawal
22982318
? 'the running kernel did NOT withdraw it, so it stays loaded until the next restart; '
2299-
: copies
2300-
? 'withdrawn from the running kernel\'s registry, but the metadata service kept copies of its objects until the next restart; '
2319+
: copies || resync
2320+
? 'withdrawn from the running kernel\'s registry, but '
2321+
+ [
2322+
...(copies ? ['the metadata service kept copies of its objects'] : []),
2323+
...(resync ? ['the automation engine can still hold its flows'] : []),
2324+
].join(' and ')
2325+
+ ' until the next restart; '
23012326
: 'withdrawn from the running kernel; ')
23022327
+ `${cleanups.length - withdrawals} uninstall cleanup(s) ran)`,
23032328
);
@@ -2308,13 +2333,81 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
23082333
cleanups,
23092334
note: withdrawal
23102335
? 'Cached manifest removed, and the uninstall cleanups this runtime\'s plugins registered ran — each one\'s outcome is in `cleanups`. The running kernel did not withdraw the package, so it stays loaded until the next restart; that is the `registry.uninstallPackage` entry in `cleanups`.'
2311-
: copies
2312-
? 'Cached manifest removed, the package withdrawn from the running kernel\'s registry, and the uninstall cleanups this runtime\'s plugins registered ran — each one\'s outcome is in `cleanups`. The metadata service kept copies of the package\'s objects, so the object listing and reads keep serving them until the next restart; that is the `metadata.unregister` entry in `cleanups`.'
2336+
: copies || resync
2337+
? 'Cached manifest removed, the package withdrawn from the running kernel\'s registry, and the uninstall cleanups this runtime\'s plugins registered ran — each one\'s outcome is in `cleanups`.'
2338+
+ (copies
2339+
? ' The metadata service kept copies of the package\'s objects, so the object listing and reads keep serving them until the next restart; that is the `metadata.unregister` entry in `cleanups`.'
2340+
: '')
2341+
+ (resync
2342+
? ' The running kernel\'s re-sync did not run, so the automation engine can still hold the package\'s flows — triggerable by name, their triggers armed — until the next restart; that is the `metadata:reloaded` entry in `cleanups`.'
2343+
: '')
23132344
: 'Cached manifest removed, the package withdrawn from the running kernel, and the uninstall cleanups this runtime\'s plugins registered ran — each one\'s outcome is in `cleanups`.',
23142345
},
23152346
}, 200);
23162347
};
23172348

2349+
/**
2350+
* [#27395] Announce an uninstall this door just completed to the running
2351+
* kernel as `metadata:reloaded` — the re-sync signal
2352+
* {@link announceHotInstall} announces for an install, with the same
2353+
* payload — so every consumer that holds what it read from a package
2354+
* re-reads a kernel that no longer has it:
2355+
*
2356+
* - `service-automation` re-syncs its flows from the protocol
2357+
* (`resyncFlowsFromProtocol`) and withdraws each flow it armed that the
2358+
* protocol's view no longer lists — the uninstalled package's, which
2359+
* the registry withdrawal took out of that view — and `withdrawFlow`
2360+
* unbinds each one's record or schedule trigger in the same step.
2361+
*
2362+
* Before this, the door withdrew the package from the registry and the
2363+
* engine never heard of it: `GET /meta/flow/NAME` answered 404 while
2364+
* `GET /automation/NAME` still answered `active`, and the trigger door, a
2365+
* record write or a schedule still ran the uninstalled package's flow
2366+
* until an unrelated reload — a view publish, another package's install —
2367+
* cleared it. ⛔ No engine-side list of the package's flows is kept or
2368+
* withdrawn here: a second teardown is how the two would drift.
2369+
*
2370+
* Never throws: the uninstall has already happened. Two answers:
2371+
* - `undefined` — announced, and every subscriber returned;
2372+
* - one failed outcome named {@link KERNEL_RESYNC}, when the context
2373+
* cannot announce or a subscriber threw. It rides on `cleanups`, the
2374+
* way a refused registry withdrawal does, and the operator log says
2375+
* what it costs and the remedy; the cause is logged, never put on the
2376+
* wire. A FUNCTIONAL degradation, so `warn` — the level the install's
2377+
* announce failure uses.
2378+
*/
2379+
private announceUninstall = async (
2380+
ctx: PluginContext,
2381+
manifestId: string,
2382+
): Promise<UninstallCleanupOutcome | undefined> => {
2383+
const failed: UninstallCleanupOutcome = {
2384+
name: KERNEL_RESYNC,
2385+
success: false,
2386+
removed: 0,
2387+
error: 'the running kernel did not re-sync — the automation engine can still hold the package\'s flows until the next restart',
2388+
};
2389+
const cost = 'its flows can stay registered in the automation engine — triggerable by name, their record and '
2390+
+ 'schedule triggers still armed — until the runtime restarts. The outcome is on the response (`cleanups`). '
2391+
+ 'Remedy: restart the runtime — the ledger entry is already gone, so the restart does not bring the package back.';
2392+
if (typeof ctx.trigger !== 'function') {
2393+
ctx.logger?.warn?.(
2394+
`[MarketplaceInstallLocal] uninstalled ${manifestId}, but this kernel context cannot announce `
2395+
+ `metadata:reloaded — ${cost}`,
2396+
);
2397+
return failed;
2398+
}
2399+
try {
2400+
await ctx.trigger('metadata:reloaded', { changed: [`app/${manifestId}`] });
2401+
return undefined;
2402+
} catch (err: any) {
2403+
ctx.logger?.warn?.(
2404+
`[MarketplaceInstallLocal] uninstalled ${manifestId}, but the metadata:reloaded re-sync FAILED `
2405+
+ `(${err?.message ?? err}) — ${cost}`,
2406+
);
2407+
return failed;
2408+
}
2409+
};
2410+
23182411
/**
23192412
* [#21576] Withdraw a package this door just removed from its ledger from
23202413
* the running kernel — through `SchemaRegistry.uninstallPackage`, the ONE

‎packages/cloud-connection/src/marketplace-install-local-uninstall-cleanups.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ async function bootPlugin(protocolMode: ProtocolMode = { kind: 'runner' }, opts:
136136
return svc;
137137
},
138138
logger,
139+
// Every kernel context can announce; the uninstall's `metadata:reloaded`
140+
// (#27395) is pinned in the resync file, so here it reaches no subscriber.
141+
trigger: async () => undefined,
139142
};
140143
const plugin = new MarketplaceInstallLocalPlugin({ controlPlaneUrl: 'off', storageDir: dir });
141144
await plugin.start(ctx);

‎packages/cloud-connection/src/marketplace-install-local-uninstall-meta-reads.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ async function bootPlugin(opts: { refuseUnregister?: string; before?: (metadata:
187187
return services[name];
188188
},
189189
logger,
190+
// Every kernel context can announce; the uninstall's `metadata:reloaded`
191+
// (#27395) is pinned in the resync file, so here it reaches no subscriber.
192+
trigger: async () => undefined,
190193
};
191194
const plugin = new MarketplaceInstallLocalPlugin({ controlPlaneUrl: 'off', storageDir: dir });
192195
await plugin.start(ctx as never);

0 commit comments

Comments
 (0)