From 6fd2172b3fa3ee09c9709ff9f4f6ff09abf0b2a5 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:15:11 -0700 Subject: [PATCH] feat(runtime): remove an agent's downloaded runtime only once nothing on the host can still run it (#29139) * feat(runtime): remove an agent's downloaded runtime only once nothing on the host can still run it managedRuntime.remove {agent} (host-admin, capability managed-runtime.remove.v1) deletes the agent's releases inside the launch gate. Before anything is deleted the gate aborts starts under way (chat starts and conversation copies), stops the agent's loaded chats on this host, and proves every retained child gone: failed chat starts and sign-in children whose close was never proven. Every close settles before the gate releases; if any owner stays unproven, Remove fails and keeps the files. Stopping chats by agent is now one generic host helper that sign-in shares. Install refuses while a removal runs, Remove refuses while an install runs, and a removal cannot be cancelled. * fix(runtime): status says whether a copy is there to remove, damaged copies included canRemove is set whenever Orca's copy is present, whole or damaged, so Settings offers Remove for a damaged copy too. The failed-start retain is dropped: the launch gate already holds every child it admitted until it exits. --- src/main/acp/acp-launch-specs.ts | 5 +- .../antigravity/antigravity-chat-account.ts | 4 + .../managed-runtime-installation.test.ts | 7 +- .../managed-runtime-installation.ts | 50 ++++++++++++- .../rpc/methods/antigravity-accounts.ts | 21 +----- .../methods/managed-runtime-install.test.ts | 75 ++++++++++++++++++- .../rpc/methods/managed-runtime-install.ts | 28 ++++++- .../runtime/structured-agent-chat-stop.ts | 18 +++++ src/shared/managed-runtime-install.ts | 4 + src/shared/protocol-version.ts | 6 +- .../rpc-params-catalog.generated.ts | 1 + 11 files changed, 188 insertions(+), 31 deletions(-) create mode 100644 src/main/runtime/structured-agent-chat-stop.ts diff --git a/src/main/acp/acp-launch-specs.ts b/src/main/acp/acp-launch-specs.ts index f1760340f44..60c27cb9011 100644 --- a/src/main/acp/acp-launch-specs.ts +++ b/src/main/acp/acp-launch-specs.ts @@ -49,12 +49,13 @@ import { scrubAntigravityAcpEnvironment } from '../antigravity/antigravity-acp-environment' import { + ANTIGRAVITY_LAUNCH_GATE, answerAntigravityChatAuth, antigravityChatAccount } from '../antigravity/antigravity-chat-account' import { readAntigravitySignInLine } from '../antigravity/antigravity-sign-in-url' import type { AcpSignInWatch } from './acp-agent-connection' -import { AcpLaunchGate } from './acp-launch-gate' +import type { AcpLaunchGate } from './acp-launch-gate' /** How an agent lists its models without a session: from its `initialize` answer (plus extension * requests), from a listing command, or not at all. Never `authenticate` or `session/new`. */ @@ -288,7 +289,7 @@ export const ANTIGRAVITY_LAUNCH_SPEC = { privateTempDirectory: true, // `session/load` replays the whole conversation, which the chat already holds. resumePreference: 'resume', - launchGate: new AcpLaunchGate('Antigravity') + launchGate: ANTIGRAVITY_LAUNCH_GATE } satisfies AcpLaunchSpec export const ACP_LAUNCH_SPECS: readonly AcpLaunchSpec[] = [ diff --git a/src/main/antigravity/antigravity-chat-account.ts b/src/main/antigravity/antigravity-chat-account.ts index 8181131ff77..85d655e0070 100644 --- a/src/main/antigravity/antigravity-chat-account.ts +++ b/src/main/antigravity/antigravity-chat-account.ts @@ -7,11 +7,14 @@ import type { AcpAgentConnection } from '../acp/acp-agent-connection' import { ProviderAcquisitionStarts } from '../provider-process/provider-acquisition-starts' import { RevisionedState } from '../runtime/revisioned-state' import { antigravityBrowserHelperWorks } from './antigravity-browser-helper' +import { AcpLaunchGate } from '../acp/acp-launch-gate' type SignInOwner = Pick const retained = new ProviderAcquisitionStarts() const folder = hostFolderAccountBinding('GEMINI_HOME', 'antigravity-chat', ['antigravity-acp']) +export const ANTIGRAVITY_LAUNCH_GATE = new AcpLaunchGate('Antigravity') + /** A failure whose message is already the sentence a person reads. */ export class AntigravitySignInSentence extends Error {} @@ -48,6 +51,7 @@ export async function closeAntigravitySignInOwner( ): Promise { if (!(await connection.close().catch(() => false)) && !connection.exited) { retained.retainFailed(home, connection) + ANTIGRAVITY_LAUNCH_GATE.retain(connection) throw new AntigravitySignInCloseUnprovenError() } } diff --git a/src/main/node-runtime/managed-runtime-installation.test.ts b/src/main/node-runtime/managed-runtime-installation.test.ts index 5afea1e8d5e..33f2bb8351a 100644 --- a/src/main/node-runtime/managed-runtime-installation.test.ts +++ b/src/main/node-runtime/managed-runtime-installation.test.ts @@ -48,6 +48,7 @@ async function rig(bodies: ('hang' | 'whole')[]) { new ManagedRuntimeInstallation('agent', runtime, { dataDirectory, findExternal: async () => false, + fence: (work) => work(), fetcher }) const phase = async (installation: ManagedRuntimeInstallation) => @@ -124,7 +125,11 @@ it('repairs a damaged copy when Install is clicked again', async () => { installation.install() await vi.waitFor(async () => expect(await phase(installation)).toBe('succeeded')) await writeFile(join(root, '1.0', 'server'), 'cut') - expect(await installation.status()).toMatchObject({ installedVersion: null, damaged: true }) + expect(await installation.status()).toMatchObject({ + installedVersion: null, + damaged: true, + canRemove: true + }) installation.install() await vi.waitFor(async () => expect(await phase(installation)).toBe('succeeded')) diff --git a/src/main/node-runtime/managed-runtime-installation.ts b/src/main/node-runtime/managed-runtime-installation.ts index 7d0fbf32fd8..44f666e862e 100644 --- a/src/main/node-runtime/managed-runtime-installation.ts +++ b/src/main/node-runtime/managed-runtime-installation.ts @@ -1,8 +1,10 @@ // Installs a managed runtime when the person asks, on the host that runs its chats. One instance per // runtime owns every change to its releases: it reclaims old ones once, first, then installs one at a // time. Publishing the release by rename is the commit point; a cancel after it changes nothing. +// Remove deletes the releases only inside the host's ownership fence, never beside an install. import { randomUUID } from 'node:crypto' +import { rm } from 'node:fs/promises' import type { ManagedRuntimeInstallState, ManagedRuntimeOperation @@ -23,6 +25,8 @@ export type ManagedRuntimeInstallationDeps = { dataDirectory: string /** Whether a start would run a copy found outside Orca's releases (PATH, install folders). */ findExternal: () => Promise + /** Runs `work` once nothing on this host runs the runtime; rejects without running it otherwise. */ + fence: (work: () => Promise) => Promise fetcher?: MainHttpClient['fetch'] } @@ -83,6 +87,8 @@ export class ManagedRuntimeInstallation { installedVersion: release?.version ?? null, installedCurrent: release?.current ?? false, ...(pin && !release?.current && broken.includes(pin.version) && { damaged: true }), + // A damaged copy is removable too, though no launch runs it. + canRemove: release !== null || broken.length > 0, source } } @@ -96,6 +102,9 @@ export class ManagedRuntimeInstallation { } const current = this.state.get().value const { running } = this + if (running && current?.kind === 'remove') { + throw new Error(`${label} is being removed. Try again after it finishes.`) + } if (current && current.id === running?.id && !running.controller.signal.aborted) { return current } @@ -113,12 +122,39 @@ export class ManagedRuntimeInstallation { return operation } + /** Joins a running removal; refused while an install, or its cleanup, still runs. */ + remove(): ManagedRuntimeOperation { + const current = this.state.get().value + if (this.running && current?.kind === 'remove') { + return current + } + if (this.running) { + throw new Error(`${this.runtime.label} is installing. Try again after it finishes.`) + } + const run: Run = { id: randomUUID(), controller: new AbortController(), committed: false } + const operation: ManagedRuntimeOperation = { + id: run.id, + kind: 'remove', + phase: 'removing', + downloadedBytes: 0, + totalBytes: null + } + this.running = run + this.state.set(operation) + this.mutation = this.mutation.then(() => this.runRemove(run)) + return operation + } + /** Resolves with the outcome once the work has unwound: `cancelled`, or `succeeded` when it had * already committed. No install queues behind a running one, so `mutation` ends with it. */ async cancel(operationId: string): Promise { - if (this.state.get().value?.id !== operationId) { + const current = this.state.get().value + if (current?.id !== operationId) { throw new Error(`That ${this.runtime.label} installation is no longer running.`) } + if (current.kind === 'remove') { + throw new Error(`Removing ${this.runtime.label} cannot be cancelled.`) + } const run = this.running if (run?.id === operationId && !run.committed) { run.controller.abort() @@ -170,6 +206,18 @@ export class ManagedRuntimeInstallation { } } + private async runRemove(run: Run): Promise { + try { + await this.deps.fence(() => rm(this.root, { recursive: true, force: true })) + this.publish(run, { phase: 'succeeded' }) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + this.publish(run, { phase: 'failed', error: message }) + } finally { + this.running = this.running === run ? null : this.running + } + } + private async reclaim(): Promise { const { release, unreadable } = resolveInstalledRuntimeRelease(this.root, this.runtime.pin()) if (unreadable) { diff --git a/src/main/runtime/rpc/methods/antigravity-accounts.ts b/src/main/runtime/rpc/methods/antigravity-accounts.ts index 92ad05247bf..b8a3f3a9bc7 100644 --- a/src/main/runtime/rpc/methods/antigravity-accounts.ts +++ b/src/main/runtime/rpc/methods/antigravity-accounts.ts @@ -15,30 +15,13 @@ import { startAntigravityChatSignIn, subscribeAntigravityChatSignIn } from '../../../antigravity/antigravity-chat-sign-in' -import { getStructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-registry' import { bindStructuredAgentSessionStream } from './structured-agent-session-status-stream' import { structuredAgentHostLaunchSources } from '../../structured-agent-launch-support' - -/** Stops each running Antigravity chat on this host as a worktree teardown stops its chats; the - * chat stays, and its next send starts the agent again. */ -async function stopLiveAntigravityChats(): Promise { - const host = getStructuredAgentSessionHost() - if (!host) { - return - } - const ids = host.loadedSessionIds().filter((id) => host.sessionAgent(id) === 'antigravity') - // Every close settles before the caller's fence releases, even when one rejects. - const closes = await Promise.allSettled(ids.map((id) => host.close(id, 'evict'))) - // A close that could not prove its child gone keeps it loaded. - const stillLoaded = host.loadedSessionIds().some((id) => ids.includes(id)) - if (stillLoaded || closes.some((close) => close.status === 'rejected')) { - throw new Error('An Antigravity chat on this host could not be stopped. Try again.') - } -} +import { stopStructuredAgentChats } from '../../structured-agent-chat-stop' const chatSignInHost = (runtime: OrcaRuntimeService) => ({ launchSources: () => structuredAgentHostLaunchSources('antigravity', runtime), - stopLiveChats: stopLiveAntigravityChats + stopLiveChats: () => stopStructuredAgentChats('antigravity', 'Antigravity') }) /** The caller a sign-in belongs to, as its transport proved it; one it can't name gets none. */ diff --git a/src/main/runtime/rpc/methods/managed-runtime-install.test.ts b/src/main/runtime/rpc/methods/managed-runtime-install.test.ts index 3e732922525..8b59404b944 100644 --- a/src/main/runtime/rpc/methods/managed-runtime-install.test.ts +++ b/src/main/runtime/rpc/methods/managed-runtime-install.test.ts @@ -1,8 +1,11 @@ -import { mkdtemp, rm } from 'node:fs/promises' +import { existsSync } from 'node:fs' +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { expect, it, vi } from 'vitest' +import { acpLaunchSpecFor } from '../../../acp/acp-launch-specs' import type * as LaunchSpecs from '../../../acp/acp-launch-specs' +import { writeRuntimeReleaseReceipt } from '../../../node-runtime/managed-runtime-release' import type { RpcContext } from '../core' import { MANAGED_RUNTIME_INSTALL_METHODS } from './managed-runtime-install' @@ -23,8 +26,26 @@ vi.mock('../../../network/http-client', () => ({ }) }) })) +// A host with one loaded chat, which a requested close stops. +const chats = vi.hoisted((): { loaded: string[]; closed: string[] } => ({ loaded: [], closed: [] })) +vi.mock( + '../../../native-chat/agent-session-wire/structured-agent-session-registry', + async (actual) => ({ + ...(await actual()), + getStructuredAgentSessionHost: () => ({ + loadedSessionIds: () => [...chats.loaded], + sessionAgent: () => 'opencode', + close: async (id: string) => { + chats.closed.push(id) + chats.loaded = chats.loaded.filter((loaded) => loaded !== id) + } + }) + }) +) vi.mock('../../../acp/acp-launch-specs', async (importOriginal) => { const actual = await importOriginal() + const { AcpLaunchGate } = await import('../../../acp/acp-launch-gate') + const launchGate = new AcpLaunchGate('Grok') const pin = { version: '1.0', url: 'https://example.test/grok-1.0.tar.gz', @@ -37,12 +58,12 @@ vi.mock('../../../acp/acp-launch-specs', async (importOriginal) => { ...actual, acpLaunchSpecFor: (agent: string) => { const spec = actual.acpLaunchSpecFor(agent) - return spec && { ...spec, command: 'orca-rpc-managed-test', managedRuntime } + return spec && { ...spec, command: 'orca-rpc-managed-test', managedRuntime, launchGate } } } }) -const [subscribeMethod, installMethod, cancelMethod] = MANAGED_RUNTIME_INSTALL_METHODS +const [subscribeMethod, installMethod, cancelMethod, removeMethod] = MANAGED_RUNTIME_INSTALL_METHODS // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the handlers read only the settings store and the stream registry. const ctx = { runtime: { @@ -79,3 +100,51 @@ it('pushes a snapshot, then the install another client starts, until the transpo expect(frames).toHaveLength(seen) await rm(host.dataDirectory, { recursive: true, force: true }) }) + +it('removes the files only once every chat and unclosed child that may run them is stopped', async () => { + host.dataDirectory = await mkdtemp(join(tmpdir(), 'orca-runtime-remove-rpc-')) + const agent = 'opencode' + const spec = acpLaunchSpecFor(agent) + const pin = spec?.managedRuntime?.pin() + if (!spec?.launchGate || !pin) { + throw new Error('managed test spec missing') + } + const release = join(host.dataDirectory, 'rt', pin.version) + await mkdir(release, { recursive: true }) + await writeFile(join(release, 'server'), 'server', { mode: 0o755 }) + await writeRuntimeReleaseReceipt(release, pin) + const transport = new AbortController() + const frames: unknown[] = [] + await subscribeMethod.handler({ agent }, { ...ctx, signal: transport.signal }, (frame) => + frames.push(frame) + ) + const removed = async () => { + const { operation } = await removeMethod.handler({ agent }, ctx) + await vi.waitFor(() => + expect(frames.at(-1)).toMatchObject({ + operation: { id: operation?.id, phase: expect.stringMatching(/ed$/) } + }) + ) + return frames.at(-1) + } + // A sign-in child whose close was never proven still holds the files. + const child = { close: vi.fn(async () => false), onExit: () => {} } + spec.launchGate.retain(child) + chats.loaded = ['chat-1'] + expect(await removed()).toMatchObject({ + installedVersion: pin.version, + operation: { kind: 'remove', phase: 'failed', error: expect.stringContaining('stopped') } + }) + + child.close.mockResolvedValue(true) + chats.loaded = ['chat-2'] + expect(await removed()).toMatchObject({ + installedVersion: null, + source: null, + operation: { kind: 'remove', phase: 'succeeded' } + }) + expect(chats.closed).toEqual(['chat-1', 'chat-2']) + expect(existsSync(join(host.dataDirectory, 'rt'))).toBe(false) + transport.abort() + await rm(host.dataDirectory, { recursive: true, force: true }) +}) diff --git a/src/main/runtime/rpc/methods/managed-runtime-install.ts b/src/main/runtime/rpc/methods/managed-runtime-install.ts index dde5d195757..86c78bb574f 100644 --- a/src/main/runtime/rpc/methods/managed-runtime-install.ts +++ b/src/main/runtime/rpc/methods/managed-runtime-install.ts @@ -1,6 +1,7 @@ // The runtime Orca installs for an agent, on the host that receives the call: the one that runs -// that agent's chats. Needs no workspace. Negotiated by `managed-runtime.install.v1`. Status is -// pushed: a snapshot on subscribe, then one per state revision. +// that agent's chats. Needs no workspace. Negotiated by `managed-runtime.install.v1`; Remove by +// `managed-runtime.remove.v1`. Status is pushed: a snapshot on subscribe, then one per state +// revision. import type { OrcaRuntimeService } from '../../orca-runtime' import { @@ -12,6 +13,7 @@ import { resolveAcpLaunchCommand } from '../../../acp/acp-structured-launch-reso import { ManagedRuntimeInstallation } from '../../../node-runtime/managed-runtime-installation' import { isAgentSessionPreSpawnError } from '../../../native-chat/agent-session-wire/structured-agent-session-adapter' import { getProfileUserDataPath } from '../../../orca-profiles/profile-storage-paths' +import { stopStructuredAgentChats } from '../../structured-agent-chat-stop' import { resolveHostStructuredAgentLaunchEnvironment } from '../../structured-agent-launch-support' import { isPathInsideOrEqual } from '../../../../shared/cross-platform-path' import { defineMethod, defineStreamingMethod } from '../core' @@ -47,12 +49,20 @@ function installationFor(agent: string, runtime: OrcaRuntimeService): ManagedRun throw error } } + const { launchGate } = spec + const managedRuntime = spec.managedRuntime + // Chats, starts, copies, sign-ins and children not proven gone all hold the gate's fence. + const fence = (work: () => Promise): Promise => + launchGate + ? launchGate.run(() => stopStructuredAgentChats(agent, managedRuntime.label), work) + : Promise.reject(new Error(`${managedRuntime.label} cannot be removed on this host.`)) const installation = installations.get(agent) ?? // The chat host's own state root, where its launches look. - new ManagedRuntimeInstallation(agent, spec.managedRuntime, { + new ManagedRuntimeInstallation(agent, managedRuntime, { dataDirectory, - findExternal + findExternal, + fence }) installations.set(agent, installation) return installation @@ -122,5 +132,15 @@ export const MANAGED_RUNTIME_INSTALL_METHODS = [ // The outcome once the run has unwound: `cancelled`, or `succeeded` if it had committed. return await installationFor(agent, runtime).cancel(operationId) } + }), + defineMethod({ + name: 'managedRuntime.remove', + permission: 'host-admin', + params: ManagedRuntimeAgentParams, + handler: async ({ agent }, { runtime }) => { + const installation = installationFor(agent, runtime) + installation.remove() + return installation.status() + } }) ] as const diff --git a/src/main/runtime/structured-agent-chat-stop.ts b/src/main/runtime/structured-agent-chat-stop.ts new file mode 100644 index 00000000000..f6929dfe09a --- /dev/null +++ b/src/main/runtime/structured-agent-chat-stop.ts @@ -0,0 +1,18 @@ +import { getStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry' + +/** Stops each running chat of `agent` on this host as a worktree teardown stops its chats; the chat + * stays, and its next send starts the agent again. Rejects unless every child is proven gone. */ +export async function stopStructuredAgentChats(agent: string, label: string): Promise { + const host = getStructuredAgentSessionHost() + if (!host) { + return + } + const ids = host.loadedSessionIds().filter((id) => host.sessionAgent(id) === agent) + // Every close settles before the caller's fence releases, even when one rejects. + const closes = await Promise.allSettled(ids.map((id) => host.close(id, 'evict'))) + // A close that could not prove its child gone keeps it loaded. + const stillLoaded = host.loadedSessionIds().some((id) => ids.includes(id)) + if (stillLoaded || closes.some((close) => close.status === 'rejected')) { + throw new Error(`Not every ${label} chat on this host could be stopped. Try again.`) + } +} diff --git a/src/shared/managed-runtime-install.ts b/src/shared/managed-runtime-install.ts index 25f7b5d48c1..0c0d9af27c3 100644 --- a/src/shared/managed-runtime-install.ts +++ b/src/shared/managed-runtime-install.ts @@ -2,6 +2,8 @@ // The host answers managedRuntime.subscribe, managedRuntime.install and managedRuntime.cancel. export const MANAGED_RUNTIME_INSTALL_RUNTIME_CAPABILITY = 'managed-runtime.install.v1' as const +// Why separate: a host that installs may predate managedRuntime.remove. +export const MANAGED_RUNTIME_REMOVE_RUNTIME_CAPABILITY = 'managed-runtime.remove.v1' as const export type ManagedRuntimeOperation = { id: string @@ -34,6 +36,8 @@ export type ManagedRuntimeInstallState = { installedCurrent: boolean /** The pinned release's folder is there but not whole; Install repairs it. */ damaged?: boolean + /** Orca's copy is there to remove, whole or damaged; absent from a host without Remove. */ + canRemove?: boolean /** What a chat start runs now: Orca's release, a copy found outside it, or nothing. */ source: 'managed' | 'external' | null /** The latest operation; an ended one stays readable until the next starts. */ diff --git a/src/shared/protocol-version.ts b/src/shared/protocol-version.ts index 0688fd0147a..ac2679f7d79 100644 --- a/src/shared/protocol-version.ts +++ b/src/shared/protocol-version.ts @@ -63,7 +63,10 @@ export * from './agent-session-resume-runtime-capabilities' import { AGENT_SESSION_BACKGROUND_TASK_CHILD_VIEWS_CAPABILITY } from './agent-session-background-task-child-views-capability' import { AGENT_SESSION_STOP_RUNTIME_CAPABILITIES } from './agent-session-stop-capabilities' import { AGENT_SESSION_CREATE_RUNTIME_CAPABILITIES } from './agent-session-create-capabilities' -import { MANAGED_RUNTIME_INSTALL_RUNTIME_CAPABILITY } from './managed-runtime-install' +import { + MANAGED_RUNTIME_INSTALL_RUNTIME_CAPABILITY, + MANAGED_RUNTIME_REMOVE_RUNTIME_CAPABILITY +} from './managed-runtime-install' import { REMOTE_SERVER_UPDATE_CAPABILITY } from './remote-server-update' import { MOBILE_RELAY_RUNTIME_CAPABILITIES } from './mobile-desktop-relay-contract' import { SKILL_RUNTIME_CAPABILITIES } from './skill-install-capability' @@ -232,6 +235,7 @@ export const RUNTIME_CAPABILITIES = [ AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY, REMOTE_SERVER_UPDATE_CAPABILITY, MANAGED_RUNTIME_INSTALL_RUNTIME_CAPABILITY, + MANAGED_RUNTIME_REMOVE_RUNTIME_CAPABILITY, AGENT_SESSION_HOST_AUTHORITY_RUNTIME_CAPABILITY, AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY, AGENT_SESSION_KEYBOARD_RUNTIME_CAPABILITY, diff --git a/src/shared/rpc-contract/rpc-params-catalog.generated.ts b/src/shared/rpc-contract/rpc-params-catalog.generated.ts index 5a1c7378c56..fc72eb49418 100644 --- a/src/shared/rpc-contract/rpc-params-catalog.generated.ts +++ b/src/shared/rpc-contract/rpc-params-catalog.generated.ts @@ -1104,6 +1104,7 @@ export const RPC_PARAMS_BY_METHOD = { 'linear.updateIssue': IssueUpdateOfLinearParams, 'managedRuntime.cancel': ManagedRuntimeCancelParams, 'managedRuntime.install': ManagedRuntimeAgentParams, + 'managedRuntime.remove': ManagedRuntimeAgentParams, 'managedRuntime.subscribe': ManagedRuntimeAgentParams, 'managedServer.cancelStop': ManagedServerSelector, 'managedServer.forget': ManagedServerSelector,