openclaw / src /acp /persistent-bindings.lifecycle.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
76289e7 verified
Raw History Blame Contribute Delete
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",
};
}