mirror of https://github.com/openclaw/openclaw.git
Plugins: add loader lifecycle state machine
This commit is contained in:
parent
99045f85c1
commit
d32f65eb5e
|
|
@ -25,52 +25,52 @@ This is an implementation checklist, not a future-design spec.
|
|||
|
||||
## Current Inventory
|
||||
|
||||
| Surface | Current implementation | Target owner | Status | How it has been handled so far |
|
||||
| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Active runtime registry state | `src/plugins/runtime.ts` plus global plugin runtime state | `src/extension-host/active-registry.ts` | `moved` | Host-owned active registry exists; `src/plugins/runtime.ts` is now a compatibility facade. |
|
||||
| Normalized extension descriptor model | plugin manifests and package metadata interpreted ad hoc across `src/plugins/manifest.ts`, `src/plugins/discovery.ts`, `src/plugins/install.ts` | `src/extension-host/schema.ts` | `partial` | `ResolvedExtension`, `ResolvedContribution`, and `ContributionPolicy` exist; current manifests project into them through compatibility adapters. |
|
||||
| Resolved static registry | flat rows in `src/plugins/manifest-registry.ts` | `src/extension-host/resolved-registry.ts` | `partial` | Manifest records now carry `resolvedExtension`; a host-owned resolved registry view exists for static consumers. |
|
||||
| Manifest/package metadata loading | `src/plugins/manifest.ts`, `src/plugins/discovery.ts`, `src/plugins/install.ts` | `src/extension-host/schema.ts` and `src/extension-host/manifest-registry.ts` | `partial` | Package metadata parsing is routed through host schema helpers; legacy loader flow still supplies the source manifests. |
|
||||
| Loader SDK alias compatibility | `src/plugins/loader.ts` | `src/extension-host/loader-compat.ts` | `partial` | Plugin-SDK alias candidate ordering, alias-file resolution, and scoped alias-map construction now live in host-owned loader compatibility helpers. |
|
||||
| Loader cache key and registry cache control | `src/plugins/loader.ts` | `src/extension-host/loader-cache.ts` | `partial` | Cache-key construction, LRU registry cache reads and writes, and cache clearing now delegate through host-owned loader-cache helpers while preserving the current cache shape and cap. |
|
||||
| Loader provenance and duplicate-order policy | `src/plugins/loader.ts` | `src/extension-host/loader-policy.ts` | `partial` | Plugin-record creation, duplicate precedence, provenance indexing, and allowlist/untracked warnings now live in host-owned loader-policy helpers. |
|
||||
| Loader initial candidate planning and record creation | `src/plugins/loader.ts` | `src/extension-host/loader-records.ts` | `partial` | Duplicate detection, initial record creation, manifest metadata attachment, and first-pass enable-state planning now delegate through host-owned loader-records helpers. |
|
||||
| Loader entry-path opening and module import | `src/plugins/loader.ts` | `src/extension-host/loader-import.ts` | `partial` | Boundary-checked entry opening and module import now delegate through host-owned loader-import helpers while preserving the current trusted in-process loading model. |
|
||||
| Loader module-export, config-validation, and memory-slot decisions | `src/plugins/loader.ts` | `src/extension-host/loader-runtime.ts` | `partial` | Module export resolution, export-metadata application, config validation, and early or final memory-slot decisions now delegate through host-owned loader-runtime helpers. |
|
||||
| Loader post-import planning and register execution | `src/plugins/loader.ts` | `src/extension-host/loader-register.ts` | `partial` | Definition application, post-import validation planning, and `register(...)` execution now delegate through host-owned loader-register helpers while preserving current plugin behavior. |
|
||||
| Loader per-candidate orchestration | `src/plugins/loader.ts` | `src/extension-host/loader-flow.ts` | `partial` | The per-candidate load flow now runs through a host-owned orchestrator that composes planning, import, runtime validation, register execution, and record-state helpers. |
|
||||
| Loader top-level load orchestration | `src/plugins/loader.ts` | `src/extension-host/loader-orchestrator.ts` | `partial` | Cache hits, runtime creation, discovery, manifest loading, candidate ordering, candidate processing, and finalization now route through a host-owned loader orchestrator while `src/plugins/loader.ts` remains the compatibility facade. |
|
||||
| Loader record-state transitions | `src/plugins/loader.ts` | `src/extension-host/loader-state.ts` | `partial` | Disabled, error, validate-only, and registered plugin-record state transitions now delegate through host-owned loader-state helpers, including explicit compatibility `lifecycleState` mapping; a real lifecycle state machine still does not exist. |
|
||||
| Loader final cache, warning, and activation finalization | `src/plugins/loader.ts` | `src/extension-host/loader-finalize.ts` | `partial` | Cache writes, untracked-extension warnings, final memory-slot warnings, and registry activation now delegate through a host-owned loader-finalize helper; the lifecycle state machine is still pending. |
|
||||
| Channel lookup | `src/channels/plugins/index.ts`, `src/channels/plugins/registry-loader.ts`, `src/channels/registry.ts` | extension-host-backed registries plus kernel channel contracts | `partial` | Readers now consume the host-owned active registry, but writes still originate from plugin registration. |
|
||||
| Dock lookup | `src/channels/dock.ts` | host-owned static descriptors | `partial` | Runtime lookup now uses the host boundary; dock ownership itself has not moved yet. |
|
||||
| Message-channel normalization | `src/utils/message-channel.ts` | host-owned channel registry view | `partial` | Lookup path now reads through the host-owned active registry. |
|
||||
| Default plugin HTTP route lookup | `src/plugins/http-registry.ts` | host-owned route registry | `partial` | Default registry resolution now uses the host boundary; route registration compatibility still flows through the legacy plugin API. |
|
||||
| Channel catalog static metadata | `src/channels/plugins/catalog.ts` | host-owned static descriptors | `partial` | Package metadata parsing now flows through host schema helpers; full canonical catalog migration has not started. |
|
||||
| Plugin skill discovery | `src/agents/skills/plugin-skills.ts` | host-owned resolved registry | `moved` | Static consumer now reads only resolved-extension data for skill paths and enablement filtering. |
|
||||
| Plugin auto-enable | `src/config/plugin-auto-enable.ts` | host-owned resolved registry | `partial` | Primary logic runs on resolved-extension data; old manifest-registry injection remains as a compatibility input for older callers and tests. |
|
||||
| Config validation indexing | `src/config/validation.ts`, `src/config/resolved-extension-validation.ts` | host-owned resolved registry | `moved` | Validation indexing now builds from resolved-extension records instead of flat manifest rows. |
|
||||
| Config doc baseline generation | `src/config/doc-baseline.ts` | host-owned resolved registry | `moved` | Bundled plugin and channel metadata now load through the resolved-extension registry. |
|
||||
| Plugin loader activation | `src/plugins/loader.ts` | extension host lifecycle + compatibility loader | `partial` | Activation now routes through `src/extension-host/activation.ts`, but discovery, enablement, provenance, module loading, and policy still live in the legacy plugin loader. |
|
||||
| Channel registration writes | `src/plugins/registry.ts` | host-owned channel registry | `partial` | Validation and normalization now delegate to `src/extension-host/runtime-registrations.ts`, but the legacy plugin API still performs the write. |
|
||||
| Provider registration writes | `src/plugins/registry.ts` | host-owned provider registry | `partial` | Provider normalization still happens in plugin-era validation, but duplicate detection and normalized registration shape now delegate to `src/extension-host/runtime-registrations.ts`. |
|
||||
| HTTP route registration writes | `src/plugins/registry.ts` | host-owned route registry | `partial` | Route validation and normalization now delegate to `src/extension-host/runtime-registrations.ts`, but the legacy plugin API still performs the write. |
|
||||
| Gateway method registration writes | `src/plugins/registry.ts` | host-owned runtime contribution registry | `partial` | Duplicate detection and normalized method registration now delegate to `src/extension-host/runtime-registrations.ts`, but the legacy plugin API still performs the write. |
|
||||
| Tool registration writes | `src/plugins/registry.ts` | host-owned tool registry | `partial` | Tool-name normalization and tool-factory shaping now delegate to `src/extension-host/runtime-registrations.ts`, but duplicate handling still follows the legacy tool path. |
|
||||
| CLI registration writes | `src/plugins/registry.ts` | host-owned CLI registry | `partial` | CLI command-name normalization now delegates to `src/extension-host/runtime-registrations.ts`, but the legacy plugin API still performs the write. |
|
||||
| Service registration writes | `src/plugins/registry.ts` | host-owned service registry | `partial` | Service-id normalization now delegates to `src/extension-host/runtime-registrations.ts`, but lifecycle remains legacy-owned. |
|
||||
| Command registration writes | `src/plugins/registry.ts` | host-owned command registry | `partial` | Command-name normalization now delegates to `src/extension-host/runtime-registrations.ts`, but duplicate enforcement still depends on the legacy plugin command registry. |
|
||||
| Context-engine registration writes | `src/plugins/registry.ts` | host-owned context-engine registry | `partial` | Context-engine id normalization now delegates to `src/extension-host/runtime-registrations.ts`, but the actual context-engine registry remains legacy-owned. |
|
||||
| Legacy hook registration writes | `src/plugins/registry.ts` | host-owned hook registry | `partial` | Hook-entry construction and event normalization now delegate to `src/extension-host/runtime-registrations.ts`, but internal-hook bridging still remains in the legacy plugin registry. |
|
||||
| Typed-hook registration writes | `src/plugins/registry.ts` | host-owned typed-hook registry | `partial` | Typed-hook record construction and hook-name validation now delegate to `src/extension-host/runtime-registrations.ts`, but prompt-injection policy and execution semantics remain legacy-owned. |
|
||||
| Hook execution and global runner | `src/plugins/hook-runner-global.ts`, `src/hooks/internal-hooks.ts`, plugin hook registration in `src/plugins/registry.ts` | canonical kernel event stages + host bridges | `not started` | No canonical event-stage migration has landed yet. |
|
||||
| Service lifecycle | `src/plugins/services.ts` and plugin service registration | extension host lifecycle | `not started` | Service startup and teardown still depend on legacy plugin registry/service ownership. |
|
||||
| CLI registration | plugin CLI registration in `src/plugins/registry.ts` and CLI loaders | extension host registry + static descriptors where possible | `not started` | No host-owned CLI registry exists yet. |
|
||||
| Gateway/server methods | `src/plugins/registry.ts` gateway handler registration | host-owned runtime contribution registry | `not started` | Still registered directly into the legacy plugin registry. |
|
||||
| Slot arbitration | `src/plugins/slots.ts` | host-owned arbitration model | `not started` | Current slot selection remains plugin-era logic. |
|
||||
| ACP backend registry | `src/acp/runtime/registry.ts` | host-owned runtime-backend registry | `not started` | ACP backends still mutate a global ACP runtime registry directly. |
|
||||
| Onboarding/install/setup surfaces | `src/plugins/install.ts`, package manifests, channel catalog, onboarding commands | host-owned static descriptors | `partial` | Static metadata normalization has started; full setup/install descriptor migration is not done. |
|
||||
| Pilot migrations | `extensions/thread-ownership`, `extensions/telegram`, `extensions/acpx` | extension-host path with parity tracking | `not started` | No pilot runs through the host path yet. |
|
||||
| Surface | Current implementation | Target owner | Status | How it has been handled so far |
|
||||
| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Active runtime registry state | `src/plugins/runtime.ts` plus global plugin runtime state | `src/extension-host/active-registry.ts` | `moved` | Host-owned active registry exists; `src/plugins/runtime.ts` is now a compatibility facade. |
|
||||
| Normalized extension descriptor model | plugin manifests and package metadata interpreted ad hoc across `src/plugins/manifest.ts`, `src/plugins/discovery.ts`, `src/plugins/install.ts` | `src/extension-host/schema.ts` | `partial` | `ResolvedExtension`, `ResolvedContribution`, and `ContributionPolicy` exist; current manifests project into them through compatibility adapters. |
|
||||
| Resolved static registry | flat rows in `src/plugins/manifest-registry.ts` | `src/extension-host/resolved-registry.ts` | `partial` | Manifest records now carry `resolvedExtension`; a host-owned resolved registry view exists for static consumers. |
|
||||
| Manifest/package metadata loading | `src/plugins/manifest.ts`, `src/plugins/discovery.ts`, `src/plugins/install.ts` | `src/extension-host/schema.ts` and `src/extension-host/manifest-registry.ts` | `partial` | Package metadata parsing is routed through host schema helpers; legacy loader flow still supplies the source manifests. |
|
||||
| Loader SDK alias compatibility | `src/plugins/loader.ts` | `src/extension-host/loader-compat.ts` | `partial` | Plugin-SDK alias candidate ordering, alias-file resolution, and scoped alias-map construction now live in host-owned loader compatibility helpers. |
|
||||
| Loader cache key and registry cache control | `src/plugins/loader.ts` | `src/extension-host/loader-cache.ts` | `partial` | Cache-key construction, LRU registry cache reads and writes, and cache clearing now delegate through host-owned loader-cache helpers while preserving the current cache shape and cap. |
|
||||
| Loader provenance and duplicate-order policy | `src/plugins/loader.ts` | `src/extension-host/loader-policy.ts` | `partial` | Plugin-record creation, duplicate precedence, provenance indexing, and allowlist/untracked warnings now live in host-owned loader-policy helpers. |
|
||||
| Loader initial candidate planning and record creation | `src/plugins/loader.ts` | `src/extension-host/loader-records.ts` | `partial` | Duplicate detection, initial record creation, manifest metadata attachment, and first-pass enable-state planning now delegate through host-owned loader-records helpers. |
|
||||
| Loader entry-path opening and module import | `src/plugins/loader.ts` | `src/extension-host/loader-import.ts` | `partial` | Boundary-checked entry opening and module import now delegate through host-owned loader-import helpers while preserving the current trusted in-process loading model. |
|
||||
| Loader module-export, config-validation, and memory-slot decisions | `src/plugins/loader.ts` | `src/extension-host/loader-runtime.ts` | `partial` | Module export resolution, export-metadata application, config validation, and early or final memory-slot decisions now delegate through host-owned loader-runtime helpers. |
|
||||
| Loader post-import planning and register execution | `src/plugins/loader.ts` | `src/extension-host/loader-register.ts` | `partial` | Definition application, post-import validation planning, and `register(...)` execution now delegate through host-owned loader-register helpers while preserving current plugin behavior. |
|
||||
| Loader per-candidate orchestration | `src/plugins/loader.ts` | `src/extension-host/loader-flow.ts` | `partial` | The per-candidate load flow now runs through a host-owned orchestrator that composes planning, import, runtime validation, register execution, and record-state helpers. |
|
||||
| Loader top-level load orchestration | `src/plugins/loader.ts` | `src/extension-host/loader-orchestrator.ts` | `partial` | Cache hits, runtime creation, discovery, manifest loading, candidate ordering, candidate processing, and finalization now route through a host-owned loader orchestrator while `src/plugins/loader.ts` remains the compatibility facade. |
|
||||
| Loader record-state transitions | `src/plugins/loader.ts` | `src/extension-host/loader-state.ts` | `partial` | The loader now enforces an explicit lifecycle transition model (`prepared -> imported -> validated -> registered -> ready`, plus terminal `disabled` and `error`) while still mapping back to compatibility `PluginRecord.status` values. |
|
||||
| Loader final cache, warning, and activation finalization | `src/plugins/loader.ts` | `src/extension-host/loader-finalize.ts` | `partial` | Cache writes, untracked-extension warnings, final memory-slot warnings, readiness promotion, and registry activation now delegate through a host-owned loader-finalize helper; broader host lifecycle and policy semantics are still pending. |
|
||||
| Channel lookup | `src/channels/plugins/index.ts`, `src/channels/plugins/registry-loader.ts`, `src/channels/registry.ts` | extension-host-backed registries plus kernel channel contracts | `partial` | Readers now consume the host-owned active registry, but writes still originate from plugin registration. |
|
||||
| Dock lookup | `src/channels/dock.ts` | host-owned static descriptors | `partial` | Runtime lookup now uses the host boundary; dock ownership itself has not moved yet. |
|
||||
| Message-channel normalization | `src/utils/message-channel.ts` | host-owned channel registry view | `partial` | Lookup path now reads through the host-owned active registry. |
|
||||
| Default plugin HTTP route lookup | `src/plugins/http-registry.ts` | host-owned route registry | `partial` | Default registry resolution now uses the host boundary; route registration compatibility still flows through the legacy plugin API. |
|
||||
| Channel catalog static metadata | `src/channels/plugins/catalog.ts` | host-owned static descriptors | `partial` | Package metadata parsing now flows through host schema helpers; full canonical catalog migration has not started. |
|
||||
| Plugin skill discovery | `src/agents/skills/plugin-skills.ts` | host-owned resolved registry | `moved` | Static consumer now reads only resolved-extension data for skill paths and enablement filtering. |
|
||||
| Plugin auto-enable | `src/config/plugin-auto-enable.ts` | host-owned resolved registry | `partial` | Primary logic runs on resolved-extension data; old manifest-registry injection remains as a compatibility input for older callers and tests. |
|
||||
| Config validation indexing | `src/config/validation.ts`, `src/config/resolved-extension-validation.ts` | host-owned resolved registry | `moved` | Validation indexing now builds from resolved-extension records instead of flat manifest rows. |
|
||||
| Config doc baseline generation | `src/config/doc-baseline.ts` | host-owned resolved registry | `moved` | Bundled plugin and channel metadata now load through the resolved-extension registry. |
|
||||
| Plugin loader activation | `src/plugins/loader.ts` | extension host lifecycle + compatibility loader | `partial` | Activation now routes through `src/extension-host/activation.ts`, but discovery, enablement, provenance, module loading, and policy still live in the legacy plugin loader. |
|
||||
| Channel registration writes | `src/plugins/registry.ts` | host-owned channel registry | `partial` | Validation and normalization now delegate to `src/extension-host/runtime-registrations.ts`, but the legacy plugin API still performs the write. |
|
||||
| Provider registration writes | `src/plugins/registry.ts` | host-owned provider registry | `partial` | Provider normalization still happens in plugin-era validation, but duplicate detection and normalized registration shape now delegate to `src/extension-host/runtime-registrations.ts`. |
|
||||
| HTTP route registration writes | `src/plugins/registry.ts` | host-owned route registry | `partial` | Route validation and normalization now delegate to `src/extension-host/runtime-registrations.ts`, but the legacy plugin API still performs the write. |
|
||||
| Gateway method registration writes | `src/plugins/registry.ts` | host-owned runtime contribution registry | `partial` | Duplicate detection and normalized method registration now delegate to `src/extension-host/runtime-registrations.ts`, but the legacy plugin API still performs the write. |
|
||||
| Tool registration writes | `src/plugins/registry.ts` | host-owned tool registry | `partial` | Tool-name normalization and tool-factory shaping now delegate to `src/extension-host/runtime-registrations.ts`, but duplicate handling still follows the legacy tool path. |
|
||||
| CLI registration writes | `src/plugins/registry.ts` | host-owned CLI registry | `partial` | CLI command-name normalization now delegates to `src/extension-host/runtime-registrations.ts`, but the legacy plugin API still performs the write. |
|
||||
| Service registration writes | `src/plugins/registry.ts` | host-owned service registry | `partial` | Service-id normalization now delegates to `src/extension-host/runtime-registrations.ts`, but lifecycle remains legacy-owned. |
|
||||
| Command registration writes | `src/plugins/registry.ts` | host-owned command registry | `partial` | Command-name normalization now delegates to `src/extension-host/runtime-registrations.ts`, but duplicate enforcement still depends on the legacy plugin command registry. |
|
||||
| Context-engine registration writes | `src/plugins/registry.ts` | host-owned context-engine registry | `partial` | Context-engine id normalization now delegates to `src/extension-host/runtime-registrations.ts`, but the actual context-engine registry remains legacy-owned. |
|
||||
| Legacy hook registration writes | `src/plugins/registry.ts` | host-owned hook registry | `partial` | Hook-entry construction and event normalization now delegate to `src/extension-host/runtime-registrations.ts`, but internal-hook bridging still remains in the legacy plugin registry. |
|
||||
| Typed-hook registration writes | `src/plugins/registry.ts` | host-owned typed-hook registry | `partial` | Typed-hook record construction and hook-name validation now delegate to `src/extension-host/runtime-registrations.ts`, but prompt-injection policy and execution semantics remain legacy-owned. |
|
||||
| Hook execution and global runner | `src/plugins/hook-runner-global.ts`, `src/hooks/internal-hooks.ts`, plugin hook registration in `src/plugins/registry.ts` | canonical kernel event stages + host bridges | `not started` | No canonical event-stage migration has landed yet. |
|
||||
| Service lifecycle | `src/plugins/services.ts` and plugin service registration | extension host lifecycle | `not started` | Service startup and teardown still depend on legacy plugin registry/service ownership. |
|
||||
| CLI registration | plugin CLI registration in `src/plugins/registry.ts` and CLI loaders | extension host registry + static descriptors where possible | `not started` | No host-owned CLI registry exists yet. |
|
||||
| Gateway/server methods | `src/plugins/registry.ts` gateway handler registration | host-owned runtime contribution registry | `not started` | Still registered directly into the legacy plugin registry. |
|
||||
| Slot arbitration | `src/plugins/slots.ts` | host-owned arbitration model | `not started` | Current slot selection remains plugin-era logic. |
|
||||
| ACP backend registry | `src/acp/runtime/registry.ts` | host-owned runtime-backend registry | `not started` | ACP backends still mutate a global ACP runtime registry directly. |
|
||||
| Onboarding/install/setup surfaces | `src/plugins/install.ts`, package manifests, channel catalog, onboarding commands | host-owned static descriptors | `partial` | Static metadata normalization has started; full setup/install descriptor migration is not done. |
|
||||
| Pilot migrations | `extensions/thread-ownership`, `extensions/telegram`, `extensions/acpx` | extension-host path with parity tracking | `not started` | No pilot runs through the host path yet. |
|
||||
|
||||
## Completed Pattern So Far
|
||||
|
||||
|
|
@ -87,14 +87,14 @@ That pattern has been used for:
|
|||
- active registry ownership
|
||||
- normalized extension schema and resolved-extension records
|
||||
- static consumers such as skills, validation, auto-enable, and config baseline generation
|
||||
- loader compatibility, cache control, initial candidate planning, entry-path import, policy, runtime decisions, post-import register flow, per-candidate orchestration, top-level load orchestration, record-state transitions with explicit compatibility lifecycle mapping, and final cache plus activation finalization
|
||||
- loader compatibility, cache control, initial candidate planning, entry-path import, policy, runtime decisions, post-import register flow, per-candidate orchestration, top-level load orchestration, explicit loader lifecycle transitions, and final cache plus activation finalization
|
||||
|
||||
## Immediate Next Targets
|
||||
|
||||
These are the next lowest-risk cutover steps:
|
||||
|
||||
1. Replace remaining static-only manifest-registry injections with resolved-extension registry inputs where practical.
|
||||
2. Grow the compatibility `lifecycleState` mapping into an explicit lifecycle state machine and move the remaining activation-state and policy ownership into `src/extension-host/*`.
|
||||
2. Extend the new loader lifecycle state machine into broader activation-state and policy ownership in `src/extension-host/*`.
|
||||
3. Introduce explicit host-owned registration surfaces for runtime writes, starting with the least-coupled registries.
|
||||
4. Move minimal SDK compatibility and loader normalization into `src/extension-host/*` without breaking current `openclaw/plugin-sdk/*` loading.
|
||||
5. Start the first pilot on `extensions/thread-ownership` only after the host-side registry and lifecycle seams are explicit.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import { finalizeExtensionHostRegistryLoad } from "./loader-finalize.js";
|
||||
import { createExtensionHostPluginRecord } from "./loader-policy.js";
|
||||
import { setExtensionHostPluginRecordLifecycleState } from "./loader-state.js";
|
||||
|
||||
function createRegistry(): PluginRegistry {
|
||||
return {
|
||||
|
|
@ -23,6 +25,17 @@ describe("extension host loader finalize", () => {
|
|||
it("adds missing memory-slot warnings and runs cache plus activation", () => {
|
||||
const registry = createRegistry();
|
||||
const calls: string[] = [];
|
||||
const record = createExtensionHostPluginRecord({
|
||||
id: "demo",
|
||||
source: "/plugins/demo.js",
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
configSchema: true,
|
||||
});
|
||||
setExtensionHostPluginRecordLifecycleState(record, "imported");
|
||||
setExtensionHostPluginRecordLifecycleState(record, "validated");
|
||||
setExtensionHostPluginRecordLifecycleState(record, "registered");
|
||||
registry.plugins.push(record);
|
||||
|
||||
const result = finalizeExtensionHostRegistryLoad({
|
||||
registry,
|
||||
|
|
@ -56,6 +69,7 @@ describe("extension host loader finalize", () => {
|
|||
level: "warn",
|
||||
message: "memory slot plugin not found or not marked as memory: memory-a",
|
||||
});
|
||||
expect(registry.plugins[0]?.lifecycleState).toBe("ready");
|
||||
expect(calls).toEqual(["cache:cache-key:true", "activate:cache-key:true"]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { PluginRegistry } from "../plugins/registry.js";
|
|||
import type { PluginLogger } from "../plugins/types.js";
|
||||
import type { ExtensionHostProvenanceIndex } from "./loader-policy.js";
|
||||
import { warnAboutUntrackedLoadedExtensions } from "./loader-policy.js";
|
||||
import { markExtensionHostRegistryPluginsReady } from "./loader-state.js";
|
||||
|
||||
export function finalizeExtensionHostRegistryLoad(params: {
|
||||
registry: PluginRegistry;
|
||||
|
|
@ -32,6 +33,7 @@ export function finalizeExtensionHostRegistryLoad(params: {
|
|||
if (params.cacheEnabled) {
|
||||
params.setCachedRegistry(params.cacheKey, params.registry);
|
||||
}
|
||||
markExtensionHostRegistryPluginsReady(params.registry);
|
||||
params.activateRegistry(params.registry, params.cacheKey);
|
||||
return params.registry;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,4 +205,52 @@ describe("extension host loader flow", () => {
|
|||
expect(registry.plugins[0]?.lifecycleState).toBe("error");
|
||||
expect(registry.diagnostics[0]?.message).toContain("failed to load plugin");
|
||||
});
|
||||
|
||||
it("records fully registered plugins before final readiness promotion", () => {
|
||||
const { rootDir, entryPath } = createTempPluginFixture();
|
||||
const registry = createRegistry();
|
||||
|
||||
processExtensionHostPluginCandidate({
|
||||
candidate: createCandidate(rootDir, entryPath),
|
||||
manifestRecord: createManifestRecord(rootDir, entryPath),
|
||||
normalizedConfig: normalizePluginsConfig({
|
||||
entries: {
|
||||
demo: {
|
||||
enabled: true,
|
||||
config: { enabled: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
rootConfig: {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: {
|
||||
enabled: true,
|
||||
config: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
validateOnly: false,
|
||||
logger: {
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
},
|
||||
registry,
|
||||
seenIds: new Map(),
|
||||
selectedMemoryPluginId: null,
|
||||
createApi: () => ({}) as never,
|
||||
loadModule: () =>
|
||||
({
|
||||
default: {
|
||||
id: "demo",
|
||||
register: () => {},
|
||||
},
|
||||
}) as never,
|
||||
});
|
||||
|
||||
expect(registry.plugins[0]?.lifecycleState).toBe("registered");
|
||||
expect(registry.plugins[0]?.status).toBe("loaded");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ export function processExtensionHostPluginCandidate(params: {
|
|||
};
|
||||
}
|
||||
|
||||
setExtensionHostPluginRecordLifecycleState(record, "imported");
|
||||
const resolved = resolveExtensionHostModuleExport(moduleImport.module);
|
||||
const loadedPlan = planExtensionHostLoadedPlugin({
|
||||
record,
|
||||
|
|
@ -214,8 +215,8 @@ export function processExtensionHostPluginCandidate(params: {
|
|||
};
|
||||
}
|
||||
|
||||
setExtensionHostPluginRecordLifecycleState(record, "validated");
|
||||
if (loadedPlan.kind === "validate-only") {
|
||||
setExtensionHostPluginRecordLifecycleState(record, "validated");
|
||||
appendExtensionHostPluginRecord({
|
||||
registry: params.registry,
|
||||
record,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { PluginRegistry } from "../plugins/registry.js";
|
|||
import { createExtensionHostPluginRecord } from "./loader-policy.js";
|
||||
import {
|
||||
appendExtensionHostPluginRecord,
|
||||
markExtensionHostRegistryPluginsReady,
|
||||
setExtensionHostPluginRecordLifecycleState,
|
||||
setExtensionHostPluginRecordDisabled,
|
||||
setExtensionHostPluginRecordError,
|
||||
|
|
@ -35,6 +36,10 @@ describe("extension host loader state", () => {
|
|||
configSchema: true,
|
||||
});
|
||||
|
||||
expect(setExtensionHostPluginRecordLifecycleState(record, "imported")).toMatchObject({
|
||||
lifecycleState: "imported",
|
||||
status: "loaded",
|
||||
});
|
||||
expect(setExtensionHostPluginRecordLifecycleState(record, "validated")).toMatchObject({
|
||||
lifecycleState: "validated",
|
||||
status: "loaded",
|
||||
|
|
@ -45,6 +50,20 @@ describe("extension host loader state", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("rejects invalid lifecycle jumps", () => {
|
||||
const record = createExtensionHostPluginRecord({
|
||||
id: "demo",
|
||||
source: "/plugins/demo.js",
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
configSchema: true,
|
||||
});
|
||||
|
||||
expect(() => setExtensionHostPluginRecordLifecycleState(record, "registered")).toThrow(
|
||||
"invalid extension host lifecycle transition: prepared -> registered",
|
||||
);
|
||||
});
|
||||
|
||||
it("marks plugin records disabled", () => {
|
||||
const record = createExtensionHostPluginRecord({
|
||||
id: "demo",
|
||||
|
|
@ -100,4 +119,27 @@ describe("extension host loader state", () => {
|
|||
expect(registry.plugins).toEqual([record]);
|
||||
expect(seenIds.get("demo")).toBe("workspace");
|
||||
});
|
||||
|
||||
it("promotes registered plugins to ready during finalization", () => {
|
||||
const registry = createRegistry();
|
||||
const record = createExtensionHostPluginRecord({
|
||||
id: "demo",
|
||||
source: "/plugins/demo.js",
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
configSchema: true,
|
||||
});
|
||||
|
||||
setExtensionHostPluginRecordLifecycleState(record, "imported");
|
||||
setExtensionHostPluginRecordLifecycleState(record, "validated");
|
||||
setExtensionHostPluginRecordLifecycleState(record, "registered");
|
||||
registry.plugins.push(record);
|
||||
|
||||
markExtensionHostRegistryPluginsReady(registry);
|
||||
|
||||
expect(record).toMatchObject({
|
||||
lifecycleState: "ready",
|
||||
status: "loaded",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,17 +9,52 @@ const EXTENSION_HOST_LIFECYCLE_STATUS_MAP: Record<
|
|||
PluginRecord["status"]
|
||||
> = {
|
||||
prepared: "loaded",
|
||||
imported: "loaded",
|
||||
disabled: "disabled",
|
||||
validated: "loaded",
|
||||
registered: "loaded",
|
||||
ready: "loaded",
|
||||
error: "error",
|
||||
};
|
||||
|
||||
const EXTENSION_HOST_PLUGIN_LIFECYCLE_TRANSITIONS: Record<
|
||||
PluginRecordLifecycleState,
|
||||
Set<PluginRecordLifecycleState>
|
||||
> = {
|
||||
prepared: new Set(["imported", "disabled", "error"]),
|
||||
imported: new Set(["validated", "disabled", "error"]),
|
||||
disabled: new Set(),
|
||||
validated: new Set(["registered", "disabled", "error"]),
|
||||
registered: new Set(["ready", "error"]),
|
||||
ready: new Set(["error"]),
|
||||
error: new Set(),
|
||||
};
|
||||
|
||||
function assertExtensionHostPluginLifecycleTransition(
|
||||
currentState: PluginRecordLifecycleState | undefined,
|
||||
nextState: PluginRecordLifecycleState,
|
||||
): void {
|
||||
if (currentState === undefined) {
|
||||
if (nextState === "prepared" || nextState === "disabled" || nextState === "error") {
|
||||
return;
|
||||
}
|
||||
throw new Error(`invalid initial extension host lifecycle transition: <none> -> ${nextState}`);
|
||||
}
|
||||
if (currentState === nextState) {
|
||||
return;
|
||||
}
|
||||
if (EXTENSION_HOST_PLUGIN_LIFECYCLE_TRANSITIONS[currentState].has(nextState)) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`invalid extension host lifecycle transition: ${currentState} -> ${nextState}`);
|
||||
}
|
||||
|
||||
export function setExtensionHostPluginRecordLifecycleState(
|
||||
record: PluginRecord,
|
||||
nextState: PluginRecordLifecycleState,
|
||||
opts?: { error?: string },
|
||||
): PluginRecord {
|
||||
assertExtensionHostPluginLifecycleTransition(record.lifecycleState, nextState);
|
||||
record.lifecycleState = nextState;
|
||||
record.status = EXTENSION_HOST_LIFECYCLE_STATUS_MAP[nextState];
|
||||
|
||||
|
|
@ -52,6 +87,14 @@ export function setExtensionHostPluginRecordError(
|
|||
return setExtensionHostPluginRecordLifecycleState(record, "error", { error: message });
|
||||
}
|
||||
|
||||
export function markExtensionHostRegistryPluginsReady(registry: PluginRegistry): void {
|
||||
for (const record of registry.plugins) {
|
||||
if (record.lifecycleState === "registered") {
|
||||
setExtensionHostPluginRecordLifecycleState(record, "ready");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function appendExtensionHostPluginRecord(params: {
|
||||
registry: PluginRegistry;
|
||||
record: PluginRecord;
|
||||
|
|
|
|||
|
|
@ -109,9 +109,11 @@ export type PluginCommandRegistration = {
|
|||
|
||||
export type PluginRecordLifecycleState =
|
||||
| "prepared"
|
||||
| "imported"
|
||||
| "disabled"
|
||||
| "validated"
|
||||
| "registered"
|
||||
| "ready"
|
||||
| "error";
|
||||
|
||||
export type PluginRecord = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue