refactor: extract ACP close session flow

Refactor ACP close-session ownership by extracting the runtime close/recovery lifecycle into `manager.close-session.ts`.

Verification:
- `pnpm test src/acp/control-plane/manager.test.ts src/acp/control-plane/manager.runtime-config.test.ts src/acp/control-plane/manager.runtime-handles.test.ts`
- `pnpm tsgo:prod`
- `pnpm check:test-types`
- `node scripts/run-oxlint.mjs src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.close-session.ts`
- `pnpm format:check src/acp/control-plane/manager.core.ts src/acp/control-plane/manager.close-session.ts`
- `git diff --check`
- `.agents/skills/autoreview/scripts/autoreview --mode local`
- GitHub PR checks for #88744 passed

Real behavior proof:
Behavior addressed: ACP close-session ownership moved out of `AcpSessionManager` without changing close/recovery behavior.
Real environment tested: Local OpenClaw checkout, Node/pnpm repo toolchain, GitHub Actions PR CI.
Exact steps or command run after this patch: Focused ACP manager tests covering close-session behavior, runtime config, and runtime handles, plus prod/test type checks, lint, format, diff check, autoreview, and PR CI.
Evidence after fix: All listed local commands passed, autoreview reported no accepted/actionable findings, and GitHub PR checks passed.
Observed result after fix: `manager.core.ts` dropped from 1149 LOC to 1038 LOC while close-session runtime lifecycle handling lives in `manager.close-session.ts`.
What was not tested: Live ACP backend close/recovery against a real external ACP provider.
This commit is contained in:
Peter Steinberger
2026-05-31 19:42:46 +01:00
committed by GitHub
parent 465a5456fe
commit 2f449285b9
2 changed files with 167 additions and 124 deletions
@@ -0,0 +1,154 @@
import {
identityHasStableSessionId,
resolveSessionIdentityFromMeta,
} from "@openclaw/acp-core/runtime/session-identity";
import type { AcpRuntime, AcpRuntimeHandle } from "@openclaw/acp-core/runtime/types";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { toAcpRuntimeError, withAcpRuntimeErrorBoundary } from "../runtime/errors.js";
import type { ManagerRuntimeHandleCache } from "./manager.runtime-handle-cache.js";
import {
discardPersistedManagerRuntimeState,
isRecoverableManagerAcpxExitError,
tryPrepareFreshManagerRuntimeSession,
} from "./manager.runtime-resume-state.js";
import type {
AcpCloseSessionInput,
AcpCloseSessionResult,
AcpSessionManagerDeps,
AcpSessionResolution,
SessionAcpMeta,
WriteManagerSessionMeta,
} from "./manager.types.js";
import { requireReadySessionMeta, resolveAcpSessionResolutionError } from "./manager.utils.js";
type EnsureRuntimeHandle = (params: {
cfg: OpenClawConfig;
sessionKey: string;
meta: SessionAcpMeta;
}) => Promise<{ runtime: AcpRuntime; handle: AcpRuntimeHandle; meta: SessionAcpMeta }>;
export async function runManagerCloseSession(params: {
input: AcpCloseSessionInput;
sessionKey: string;
deps: Pick<AcpSessionManagerDeps, "getRuntimeBackend">;
runtimeHandles: ManagerRuntimeHandleCache;
resolveSession: (params: { cfg: OpenClawConfig; sessionKey: string }) => AcpSessionResolution;
ensureRuntimeHandle: EnsureRuntimeHandle;
writeSessionMeta: WriteManagerSessionMeta;
}): Promise<AcpCloseSessionResult> {
const { input, sessionKey } = params;
const resolution = params.resolveSession({
cfg: input.cfg,
sessionKey,
});
const resolutionError = resolveAcpSessionResolutionError(resolution);
if (resolutionError) {
if (input.requireAcpSession ?? true) {
throw resolutionError;
}
return {
runtimeClosed: false,
metaCleared: false,
};
}
const meta = requireReadySessionMeta(resolution);
const currentIdentity = resolveSessionIdentityFromMeta(meta);
const shouldSkipRuntimeClose =
input.discardPersistentState &&
currentIdentity != null &&
!identityHasStableSessionId(currentIdentity);
let runtimeClosed = false;
let runtimeNotice: string | undefined;
if (shouldSkipRuntimeClose) {
await tryPrepareFreshManagerRuntimeSession({
deps: params.deps,
cfg: input.cfg,
meta,
sessionKey,
logPrefix: "acp close fast-reset",
});
params.runtimeHandles.clear(sessionKey);
} else {
try {
const { runtime: ensuredRuntime, handle } = await params.ensureRuntimeHandle({
cfg: input.cfg,
sessionKey,
meta,
});
await withAcpRuntimeErrorBoundary({
run: async () =>
await ensuredRuntime.close({
handle,
reason: input.reason,
discardPersistentState: input.discardPersistentState,
}),
fallbackCode: "ACP_TURN_FAILED",
fallbackMessage: "ACP close failed before completion.",
});
runtimeClosed = true;
params.runtimeHandles.clear(sessionKey);
} catch (error) {
const acpError = toAcpRuntimeError({
error,
fallbackCode: "ACP_TURN_FAILED",
fallbackMessage: "ACP close failed before completion.",
});
if (
input.allowBackendUnavailable &&
(acpError.code === "ACP_BACKEND_MISSING" ||
acpError.code === "ACP_BACKEND_UNAVAILABLE" ||
(input.discardPersistentState && acpError.code === "ACP_SESSION_INIT_FAILED") ||
(input.discardPersistentState && acpError.code === "ACP_BACKEND_UNSUPPORTED_CONTROL") ||
isRecoverableManagerAcpxExitError(acpError.message))
) {
if (input.discardPersistentState) {
await tryPrepareFreshManagerRuntimeSession({
deps: params.deps,
cfg: input.cfg,
meta,
sessionKey,
logPrefix: "acp close recovery",
missingBackendError: acpError,
});
}
// Treat unavailable backends as terminal for this cached handle so it
// cannot continue counting against maxConcurrentSessions.
params.runtimeHandles.clear(sessionKey);
runtimeNotice = acpError.message;
} else {
throw acpError;
}
}
}
let metaCleared = false;
if (input.discardPersistentState && !input.clearMeta) {
await discardPersistedManagerRuntimeState({
cfg: input.cfg,
sessionKey,
writeSessionMeta: params.writeSessionMeta,
});
}
if (input.clearMeta) {
await params.writeSessionMeta({
cfg: input.cfg,
sessionKey,
mutate: (_current, entry) => {
if (!entry) {
return null;
}
return null;
},
failOnError: true,
});
metaCleared = true;
}
return {
runtimeClosed,
runtimeNotice,
metaCleared,
};
}
+13 -124
View File
@@ -22,6 +22,7 @@ import {
toAcpRuntimeError,
withAcpRuntimeErrorBoundary,
} from "../runtime/errors.js";
import { runManagerCloseSession } from "./manager.close-session.js";
import { reconcileManagerRuntimeSessionIdentifiers } from "./manager.identity-reconcile.js";
import {
applyManagerRuntimeControls,
@@ -29,11 +30,6 @@ import {
} from "./manager.runtime-controls.js";
import { ManagerRuntimeHandleCache } from "./manager.runtime-handle-cache.js";
import { ensureManagerRuntimeHandle } from "./manager.runtime-handle-ensure.js";
import {
discardPersistedManagerRuntimeState,
isRecoverableManagerAcpxExitError,
tryPrepareFreshManagerRuntimeSession,
} from "./manager.runtime-resume-state.js";
import { runManagerTurn } from "./manager.turn-runner.js";
import {
type AcpCloseSessionInput,
@@ -58,7 +54,6 @@ import {
normalizeAcpErrorCode,
normalizeActorKey,
requireReadySessionMeta,
resolveAcpSessionResolutionError,
resolveMissingMetaError,
} from "./manager.utils.js";
import {
@@ -738,125 +733,19 @@ export class AcpSessionManager {
throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required.");
}
await this.evictIdleRuntimeHandles(input.cfg);
return await this.withSessionActor(sessionKey, async () => {
const resolution = this.resolveSession({
cfg: input.cfg,
sessionKey,
});
const resolutionError = resolveAcpSessionResolutionError(resolution);
if (resolutionError) {
if (input.requireAcpSession ?? true) {
throw resolutionError;
}
return {
runtimeClosed: false,
metaCleared: false,
};
}
const meta = requireReadySessionMeta(resolution);
const currentIdentity = resolveSessionIdentityFromMeta(meta);
const shouldSkipRuntimeClose =
input.discardPersistentState &&
currentIdentity != null &&
!identityHasStableSessionId(currentIdentity);
let runtimeClosed = false;
let runtimeNotice: string | undefined;
if (shouldSkipRuntimeClose) {
if (input.discardPersistentState) {
await tryPrepareFreshManagerRuntimeSession({
deps: this.deps,
cfg: input.cfg,
meta,
sessionKey,
logPrefix: "acp close fast-reset",
});
}
this.runtimeHandles.clear(sessionKey);
} else {
try {
const { runtime: ensuredRuntime, handle } = await this.ensureRuntimeHandle({
cfg: input.cfg,
sessionKey,
meta,
});
await withAcpRuntimeErrorBoundary({
run: async () =>
await ensuredRuntime.close({
handle,
reason: input.reason,
discardPersistentState: input.discardPersistentState,
}),
fallbackCode: "ACP_TURN_FAILED",
fallbackMessage: "ACP close failed before completion.",
});
runtimeClosed = true;
this.runtimeHandles.clear(sessionKey);
} catch (error) {
const acpError = toAcpRuntimeError({
error,
fallbackCode: "ACP_TURN_FAILED",
fallbackMessage: "ACP close failed before completion.",
});
if (
input.allowBackendUnavailable &&
(acpError.code === "ACP_BACKEND_MISSING" ||
acpError.code === "ACP_BACKEND_UNAVAILABLE" ||
(input.discardPersistentState && acpError.code === "ACP_SESSION_INIT_FAILED") ||
(input.discardPersistentState &&
acpError.code === "ACP_BACKEND_UNSUPPORTED_CONTROL") ||
isRecoverableManagerAcpxExitError(acpError.message))
) {
if (input.discardPersistentState) {
await tryPrepareFreshManagerRuntimeSession({
deps: this.deps,
cfg: input.cfg,
meta,
sessionKey,
logPrefix: "acp close recovery",
missingBackendError: acpError,
});
}
// Treat unavailable backends as terminal for this cached handle so it
// cannot continue counting against maxConcurrentSessions.
this.runtimeHandles.clear(sessionKey);
runtimeNotice = acpError.message;
} else {
throw acpError;
}
}
}
let metaCleared = false;
if (input.discardPersistentState && !input.clearMeta) {
await discardPersistedManagerRuntimeState({
cfg: input.cfg,
return await this.withSessionActor(
sessionKey,
async () =>
await runManagerCloseSession({
input,
sessionKey,
writeSessionMeta: async (writeParams) => await this.writeSessionMeta(writeParams),
});
}
if (input.clearMeta) {
await this.writeSessionMeta({
cfg: input.cfg,
sessionKey,
mutate: (_current, entry) => {
if (!entry) {
return null;
}
return null;
},
failOnError: true,
});
metaCleared = true;
}
return {
runtimeClosed,
runtimeNotice,
metaCleared,
};
});
deps: this.deps,
runtimeHandles: this.runtimeHandles,
resolveSession: this.resolveSession.bind(this),
ensureRuntimeHandle: this.ensureRuntimeHandle.bind(this),
writeSessionMeta: this.writeSessionMeta.bind(this),
}),
);
}
private async ensureRuntimeHandle(params: {