Download src/acp/persistent-bindings.lifecycle.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 5.15 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/acp/persistent-bindings.lifecycle.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/acp/persistent-bindings.lifecycle.ts
-
curl -L -o persistent-bindings.lifecycle.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/acp/persistent-bindings.lifecycle.ts
5.15 kB
| /** Ensures configured channel-to-ACP bindings have live sessions and matching runtime options. */ | |
| import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; | |
| import type { SessionAcpMeta } from "../config/sessions/types.js"; | |
| import type { OpenClawConfig } from "../config/types.openclaw.js"; | |
| import { logVerbose } from "../globals.js"; | |
| import { formatErrorMessage } from "../infra/errors.js"; | |
| import { getAcpSessionManager } from "./control-plane/manager.js"; | |
| import { | |
| buildConfiguredAcpSessionKey, | |
| normalizeText, | |
| type ConfiguredAcpBindingSpec, | |
| type ResolvedConfiguredAcpBinding, | |
| } from "./persistent-bindings.types.js"; | |
| // Binding lifecycle keeps configured channel conversations attached to matching ACP sessions. | |
| function sessionStructurallyMatchesConfiguredBinding(params: { | |
| cfg: OpenClawConfig; | |
| spec: ConfiguredAcpBindingSpec; | |
| meta: SessionAcpMeta; | |
| }): boolean { | |
| if (params.meta.state === "error") { | |
| return false; | |
| } | |
| const desiredAgent = normalizeLowercaseStringOrEmpty( | |
| params.spec.acpAgentId ?? params.spec.agentId, | |
| ); | |
| const currentAgent = normalizeLowercaseStringOrEmpty(params.meta.agent); | |
| if (!currentAgent || currentAgent !== desiredAgent) { | |
| return false; | |
| } | |
| if (params.meta.mode !== params.spec.mode) { | |
| return false; | |
| } | |
| const desiredBackend = | |
| normalizeText(params.spec.backend) ?? normalizeText(params.cfg.acp?.backend) ?? ""; | |
| if (desiredBackend) { | |
| const currentBackend = (params.meta.backend ?? "").trim(); | |
| if (!currentBackend || currentBackend !== desiredBackend) { | |
| return false; | |
| } | |
| } | |
| const desiredCwd = normalizeText(params.spec.cwd); | |
| if (desiredCwd !== undefined) { | |
| const currentCwd = (params.meta.runtimeOptions?.cwd ?? params.meta.cwd ?? "").trim(); | |
| if (desiredCwd !== currentCwd) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| /** Creates or replaces the ACP session required by one configured binding. */ | |
| export async function ensureConfiguredAcpBindingSession(params: { | |
| cfg: OpenClawConfig; | |
| spec: ConfiguredAcpBindingSpec; | |
| }): Promise<{ ok: true; sessionKey: string } | { ok: false; sessionKey: string; error: string }> { | |
| const sessionKey = buildConfiguredAcpSessionKey(params.spec); | |
| const acpManager = getAcpSessionManager(); | |
| const runtimeOptions = { | |
| ...(params.spec.model ? { model: params.spec.model } : {}), | |
| ...(params.spec.thinking ? { thinking: params.spec.thinking } : {}), | |
| }; | |
| try { | |
| const resolution = acpManager.resolveSession({ | |
| cfg: params.cfg, | |
| agentId: params.spec.agentId, | |
| sessionKey, | |
| }); | |
| if ( | |
| resolution.kind === "ready" && | |
| sessionStructurallyMatchesConfiguredBinding({ | |
| cfg: params.cfg, | |
| spec: params.spec, | |
| meta: resolution.meta, | |
| }) | |
| ) { | |
| // Apply before persisting: rejected controls must not overwrite accepted options. | |
| // Model precedes effort; omission retains the selection because ACP has no unset. | |
| let currentOptions = resolution.meta.runtimeOptions; | |
| for (const key of ["model", "thinking"] as const) { | |
| const value = runtimeOptions[key]; | |
| if (value !== undefined && normalizeText(currentOptions?.[key]) !== value) { | |
| currentOptions = await acpManager.setSessionConfigOption({ | |
| cfg: params.cfg, | |
| agentId: params.spec.agentId, | |
| sessionKey, | |
| key, | |
| value, | |
| }); | |
| } | |
| } | |
| return { | |
| ok: true, | |
| sessionKey, | |
| }; | |
| } | |
| if (resolution.kind !== "none") { | |
| await acpManager.closeSession({ | |
| cfg: params.cfg, | |
| agentId: params.spec.agentId, | |
| sessionKey, | |
| reason: "config-binding-reconfigure", | |
| clearMeta: false, | |
| allowBackendUnavailable: true, | |
| requireAcpSession: false, | |
| }); | |
| } | |
| await acpManager.initializeSession({ | |
| cfg: params.cfg, | |
| agentId: params.spec.agentId, | |
| sessionKey, | |
| agent: params.spec.acpAgentId ?? params.spec.agentId, | |
| mode: params.spec.mode, | |
| runtimeOptions, | |
| cwd: params.spec.cwd, | |
| backendId: params.spec.backend, | |
| }); | |
| return { | |
| ok: true, | |
| sessionKey, | |
| }; | |
| } catch (error) { | |
| const message = formatErrorMessage(error); | |
| logVerbose( | |
| `acp-configured-binding: failed ensuring ${params.spec.channel}:${params.spec.accountId}:${params.spec.conversationId} -> ${sessionKey}: ${message}`, | |
| ); | |
| return { | |
| ok: false, | |
| sessionKey, | |
| error: message, | |
| }; | |
| } | |
| } | |
| /** Resolves a configured binding for a conversation and ensures its ACP session exists. */ | |
| export async function ensureConfiguredAcpBindingReadyCore(params: { | |
| cfg: OpenClawConfig; | |
| configuredBinding: ResolvedConfiguredAcpBinding | null; | |
| }): Promise<{ ok: true } | { ok: false; error: string }> { | |
| if (!params.configuredBinding) { | |
| return { ok: true }; | |
| } | |
| const ensured = await ensureConfiguredAcpBindingSession({ | |
| cfg: params.cfg, | |
| spec: params.configuredBinding.spec, | |
| }); | |
| if (ensured.ok) { | |
| return { ok: true }; | |
| } | |
| return { | |
| ok: false, | |
| error: ensured.error ?? "unknown error", | |
| }; | |
| } | |