Download src/acp/control-plane/manager.runtime-handle-cache.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 6.55 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/acp/control-plane/manager.runtime-handle-cache.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/acp/control-plane/manager.runtime-handle-cache.ts
-
curl -L -o manager.runtime-handle-cache.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/acp/control-plane/manager.runtime-handle-cache.ts
6.55 kB
| /** Process-local ACP runtime handle cache with lifecycle cleanup and reuse checks. */ | |
| import { | |
| resolveRuntimeHandleIdentifiersFromIdentity, | |
| resolveSessionIdentityFromMeta, | |
| } from "@openclaw/acp-core/runtime/session-identity"; | |
| import type { | |
| AcpRuntime, | |
| AcpRuntimeHandle, | |
| AcpRuntimeSessionMode, | |
| AcpRuntimeStatus, | |
| } from "@openclaw/acp-core/runtime/types"; | |
| import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; | |
| import { logVerbose } from "../../globals.js"; | |
| import { isAcpOwnerRepairRequired } from "./manager.runtime-owner.js"; | |
| import type { AcpSessionTarget, SessionAcpMeta } from "./manager.types.js"; | |
| import { acpSessionActorKey } from "./manager.utils.js"; | |
| import { normalizeText } from "./runtime-options.js"; | |
| import type { SessionActorQueue } from "./session-actor-queue.js"; | |
| /** Cached runtime handle plus the configuration signature that made it reusable. */ | |
| export type CachedRuntimeState = { | |
| runtime: AcpRuntime; | |
| handle: AcpRuntimeHandle; | |
| backend: string; | |
| agent: string; | |
| mode: AcpRuntimeSessionMode; | |
| cwd?: string; | |
| configSignature: string; | |
| appliedControlSignature?: string; | |
| }; | |
| /** Process-local cache of live ACP runtime handles keyed by canonical session actor. */ | |
| export class ManagerRuntimeHandleCache { | |
| private readonly runtimeCache = new Map<string, CachedRuntimeState>(); | |
| get(target: AcpSessionTarget): CachedRuntimeState | null { | |
| return this.runtimeCache.get(acpSessionActorKey(target)) ?? null; | |
| } | |
| set(target: AcpSessionTarget, state: CachedRuntimeState): void { | |
| this.runtimeCache.set(acpSessionActorKey(target), state); | |
| } | |
| clear(target: AcpSessionTarget): void { | |
| this.runtimeCache.delete(acpSessionActorKey(target)); | |
| } | |
| /** Returns cache counters used by ACP manager observability snapshots. */ | |
| getObservabilitySnapshot() { | |
| return { | |
| activeSessions: this.runtimeCache.size, | |
| idleTtlMs: 0, | |
| evictedTotal: 0, | |
| }; | |
| } | |
| /** Closes and removes one cached runtime handle when present. */ | |
| async close( | |
| params: AcpSessionTarget & { reason: string; expectedHandle?: AcpRuntimeHandle }, | |
| ): Promise<void> { | |
| const cached = this.get(params); | |
| if (!cached || (params.expectedHandle && cached.handle !== params.expectedHandle)) { | |
| return; | |
| } | |
| try { | |
| await cached.runtime.close({ | |
| handle: cached.handle, | |
| reason: params.reason, | |
| }); | |
| } catch (error) { | |
| if (params.expectedHandle && isAcpOwnerRepairRequired(error)) { | |
| throw error; | |
| } | |
| logVerbose( | |
| `acp-manager: cached runtime close failed for ${params.sessionKey}: ${String(error)}`, | |
| ); | |
| } finally { | |
| this.clear(params); | |
| } | |
| } | |
| /** Drains every cached handle behind its session actor before process shutdown. */ | |
| async closeAll(params: { actorQueue: SessionActorQueue; reason: string }): Promise<void> { | |
| await Promise.all( | |
| [...this.runtimeCache.keys()].map((actorKey) => | |
| params.actorQueue.run(actorKey, async () => { | |
| const cached = this.runtimeCache.get(actorKey); | |
| if (!cached) { | |
| return; | |
| } | |
| try { | |
| await cached.runtime.close({ handle: cached.handle, reason: params.reason }); | |
| } catch (error) { | |
| logVerbose( | |
| `acp-manager: cached runtime close failed for ${cached.handle.sessionKey}: ${String(error)}`, | |
| ); | |
| } finally { | |
| this.runtimeCache.delete(actorKey); | |
| } | |
| }), | |
| ), | |
| ); | |
| } | |
| /** Clears a cached handle only when the caller still owns the same runtime identifiers. */ | |
| clearIfHandleMatches(params: AcpSessionTarget & { handle: AcpRuntimeHandle }): void { | |
| const cached = this.get(params); | |
| if (!cached || !this.runtimeHandlesMatch(cached.handle, params.handle)) { | |
| return; | |
| } | |
| this.clear(params); | |
| } | |
| /** Checks whether a cached runtime handle is still healthy enough to reuse. */ | |
| async isReusable(params: { | |
| sessionKey: string; | |
| runtime: AcpRuntime; | |
| handle: AcpRuntimeHandle; | |
| }): Promise<boolean> { | |
| if (!params.runtime.getStatus) { | |
| return true; | |
| } | |
| try { | |
| const status = await params.runtime.getStatus({ | |
| handle: params.handle, | |
| }); | |
| if (isRuntimeStatusUnavailable(status)) { | |
| logVerbose( | |
| `acp-manager: evicting cached runtime handle for ${params.sessionKey} after unhealthy status probe: ${status.summary ?? "status unavailable"}`, | |
| ); | |
| return false; | |
| } | |
| return true; | |
| } catch (error) { | |
| if (isAcpOwnerRepairRequired(error)) { | |
| throw error; | |
| } | |
| logVerbose( | |
| `acp-manager: evicting cached runtime handle for ${params.sessionKey} after status probe failed: ${String(error)}`, | |
| ); | |
| return false; | |
| } | |
| } | |
| handleMatchesMeta(params: { handle: AcpRuntimeHandle; meta: SessionAcpMeta }): boolean { | |
| const identity = resolveSessionIdentityFromMeta(params.meta); | |
| const expectedHandleIds = resolveRuntimeHandleIdentifiersFromIdentity(identity); | |
| if ((params.handle.backendSessionId ?? "") !== (expectedHandleIds.backendSessionId ?? "")) { | |
| return false; | |
| } | |
| if ((params.handle.agentSessionId ?? "") !== (expectedHandleIds.agentSessionId ?? "")) { | |
| return false; | |
| } | |
| const expectedAcpxRecordId = identity?.acpxRecordId ?? ""; | |
| const actualAcpxRecordId = | |
| normalizeText((params.handle as { acpxRecordId?: unknown }).acpxRecordId) ?? ""; | |
| return actualAcpxRecordId === expectedAcpxRecordId; | |
| } | |
| private runtimeHandlesMatch(a: AcpRuntimeHandle, b: AcpRuntimeHandle): boolean { | |
| return ( | |
| a.sessionKey === b.sessionKey && | |
| a.agentId === b.agentId && | |
| a.backend === b.backend && | |
| a.runtimeSessionName === b.runtimeSessionName && | |
| (a.cwd ?? "") === (b.cwd ?? "") && | |
| (a.acpxRecordId ?? "") === (b.acpxRecordId ?? "") && | |
| (a.backendSessionId ?? "") === (b.backendSessionId ?? "") && | |
| (a.agentSessionId ?? "") === (b.agentSessionId ?? "") | |
| ); | |
| } | |
| } | |
| function isRuntimeStatusUnavailable(status: AcpRuntimeStatus | undefined): boolean { | |
| if (!status) { | |
| return false; | |
| } | |
| const detailsStatus = normalizeLowercaseStringOrEmpty(status.details?.status); | |
| if (detailsStatus === "dead" || detailsStatus === "no-session") { | |
| return true; | |
| } | |
| const summaryMatch = status.summary?.match(/\bstatus=([^\s]+)/i); | |
| const summaryStatus = normalizeLowercaseStringOrEmpty(summaryMatch?.[1]); | |
| return summaryStatus === "dead" || summaryStatus === "no-session"; | |
| } | |