mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-14 11:30:41 +00:00
* docs: add ACP persistent binding experiment plan * docs: align ACP persistent binding spec to channel-local config * docs: scope Telegram ACP bindings to forum topics only * docs: lock bound /new and /reset behavior to in-place ACP reset * ACP: add persistent discord/telegram conversation bindings * ACP: fix persistent binding reuse and discord thread parent context * docs: document channel-specific persistent ACP bindings * ACP: split persistent bindings and share conversation id helpers * ACP: defer configured binding init until preflight passes * ACP: fix discord thread parent fallback and explicit disable inheritance * ACP: keep bound /new and /reset in-place * ACP: honor configured bindings in native command flows * ACP: avoid configured fallback after runtime bind failure * docs: refine ACP bindings experiment config examples * acp: cut over to typed top-level persistent bindings * ACP bindings: harden reset recovery and native command auth * Docs: add ACP bound command auth proposal * Tests: normalize i18n registry zh-CN assertion encoding * ACP bindings: address review findings for reset and fallback routing * ACP reset: gate hooks on success and preserve /new arguments * ACP bindings: fix auth and binding-priority review findings * Telegram ACP: gate ensure on auth and accepted messages * ACP bindings: fix session-key precedence and unavailable handling * ACP reset/native commands: honor fallback targets and abort on bootstrap failure * Config schema: validate ACP binding channel and Telegram topic IDs * Discord ACP: apply configured DM bindings to native commands * ACP reset tails: dispatch through ACP after command handling * ACP tails/native reset auth: fix target dispatch and restore full auth * ACP reset detection: fallback to active ACP keys for DM contexts * Tests: type runTurn mock input in ACP dispatch test * ACP: dedup binding route bootstrap and reset target resolution * reply: align ACP reset hooks with bound session key * docs: replace personal discord ids with placeholders * fix: add changelog entry for ACP persistent bindings (#34873) (thanks @dutifulbob) --------- Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
199 lines
5.6 KiB
TypeScript
199 lines
5.6 KiB
TypeScript
import type { OpenClawConfig } from "../config/config.js";
|
|
import type { SessionAcpMeta } from "../config/sessions/types.js";
|
|
import { logVerbose } from "../globals.js";
|
|
import { getAcpSessionManager } from "./control-plane/manager.js";
|
|
import { resolveAcpAgentFromSessionKey } from "./control-plane/manager.utils.js";
|
|
import { resolveConfiguredAcpBindingSpecBySessionKey } from "./persistent-bindings.resolve.js";
|
|
import {
|
|
buildConfiguredAcpSessionKey,
|
|
normalizeText,
|
|
type ConfiguredAcpBindingSpec,
|
|
} from "./persistent-bindings.types.js";
|
|
import { readAcpSessionEntry } from "./runtime/session-meta.js";
|
|
|
|
function sessionMatchesConfiguredBinding(params: {
|
|
cfg: OpenClawConfig;
|
|
spec: ConfiguredAcpBindingSpec;
|
|
meta: SessionAcpMeta;
|
|
}): boolean {
|
|
const desiredAgent = (params.spec.acpAgentId ?? params.spec.agentId).trim().toLowerCase();
|
|
const currentAgent = (params.meta.agent ?? "").trim().toLowerCase();
|
|
if (!currentAgent || currentAgent !== desiredAgent) {
|
|
return false;
|
|
}
|
|
|
|
if (params.meta.mode !== params.spec.mode) {
|
|
return false;
|
|
}
|
|
|
|
const desiredBackend = params.spec.backend?.trim() || params.cfg.acp?.backend?.trim() || "";
|
|
if (desiredBackend) {
|
|
const currentBackend = (params.meta.backend ?? "").trim();
|
|
if (!currentBackend || currentBackend !== desiredBackend) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const desiredCwd = params.spec.cwd?.trim();
|
|
if (desiredCwd !== undefined) {
|
|
const currentCwd = (params.meta.runtimeOptions?.cwd ?? params.meta.cwd ?? "").trim();
|
|
if (desiredCwd !== currentCwd) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
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();
|
|
try {
|
|
const resolution = acpManager.resolveSession({
|
|
cfg: params.cfg,
|
|
sessionKey,
|
|
});
|
|
if (
|
|
resolution.kind === "ready" &&
|
|
sessionMatchesConfiguredBinding({
|
|
cfg: params.cfg,
|
|
spec: params.spec,
|
|
meta: resolution.meta,
|
|
})
|
|
) {
|
|
return {
|
|
ok: true,
|
|
sessionKey,
|
|
};
|
|
}
|
|
|
|
if (resolution.kind !== "none") {
|
|
await acpManager.closeSession({
|
|
cfg: params.cfg,
|
|
sessionKey,
|
|
reason: "config-binding-reconfigure",
|
|
clearMeta: false,
|
|
allowBackendUnavailable: true,
|
|
requireAcpSession: false,
|
|
});
|
|
}
|
|
|
|
await acpManager.initializeSession({
|
|
cfg: params.cfg,
|
|
sessionKey,
|
|
agent: params.spec.acpAgentId ?? params.spec.agentId,
|
|
mode: params.spec.mode,
|
|
cwd: params.spec.cwd,
|
|
backendId: params.spec.backend,
|
|
});
|
|
|
|
return {
|
|
ok: true,
|
|
sessionKey,
|
|
};
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
logVerbose(
|
|
`acp-persistent-binding: failed ensuring ${params.spec.channel}:${params.spec.accountId}:${params.spec.conversationId} -> ${sessionKey}: ${message}`,
|
|
);
|
|
return {
|
|
ok: false,
|
|
sessionKey,
|
|
error: message,
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function resetAcpSessionInPlace(params: {
|
|
cfg: OpenClawConfig;
|
|
sessionKey: string;
|
|
reason: "new" | "reset";
|
|
}): Promise<{ ok: true } | { ok: false; skipped?: boolean; error?: string }> {
|
|
const sessionKey = params.sessionKey.trim();
|
|
if (!sessionKey) {
|
|
return {
|
|
ok: false,
|
|
skipped: true,
|
|
};
|
|
}
|
|
|
|
const configuredBinding = resolveConfiguredAcpBindingSpecBySessionKey({
|
|
cfg: params.cfg,
|
|
sessionKey,
|
|
});
|
|
const meta = readAcpSessionEntry({
|
|
cfg: params.cfg,
|
|
sessionKey,
|
|
})?.acp;
|
|
if (!meta) {
|
|
if (configuredBinding) {
|
|
const ensured = await ensureConfiguredAcpBindingSession({
|
|
cfg: params.cfg,
|
|
spec: configuredBinding,
|
|
});
|
|
if (ensured.ok) {
|
|
return { ok: true };
|
|
}
|
|
return {
|
|
ok: false,
|
|
error: ensured.error,
|
|
};
|
|
}
|
|
return {
|
|
ok: false,
|
|
skipped: true,
|
|
};
|
|
}
|
|
|
|
const acpManager = getAcpSessionManager();
|
|
const agent =
|
|
normalizeText(meta.agent) ??
|
|
configuredBinding?.acpAgentId ??
|
|
configuredBinding?.agentId ??
|
|
resolveAcpAgentFromSessionKey(sessionKey, "main");
|
|
const mode = meta.mode === "oneshot" ? "oneshot" : "persistent";
|
|
const runtimeOptions = { ...meta.runtimeOptions };
|
|
const cwd = normalizeText(runtimeOptions.cwd ?? meta.cwd);
|
|
|
|
try {
|
|
await acpManager.closeSession({
|
|
cfg: params.cfg,
|
|
sessionKey,
|
|
reason: `${params.reason}-in-place-reset`,
|
|
clearMeta: false,
|
|
allowBackendUnavailable: true,
|
|
requireAcpSession: false,
|
|
});
|
|
|
|
await acpManager.initializeSession({
|
|
cfg: params.cfg,
|
|
sessionKey,
|
|
agent,
|
|
mode,
|
|
cwd,
|
|
backendId: normalizeText(meta.backend) ?? normalizeText(params.cfg.acp?.backend),
|
|
});
|
|
|
|
const runtimeOptionsPatch = Object.fromEntries(
|
|
Object.entries(runtimeOptions).filter(([, value]) => value !== undefined),
|
|
) as SessionAcpMeta["runtimeOptions"];
|
|
if (runtimeOptionsPatch && Object.keys(runtimeOptionsPatch).length > 0) {
|
|
await acpManager.updateSessionRuntimeOptions({
|
|
cfg: params.cfg,
|
|
sessionKey,
|
|
patch: runtimeOptionsPatch,
|
|
});
|
|
}
|
|
return { ok: true };
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
logVerbose(`acp-persistent-binding: failed reset for ${sessionKey}: ${message}`);
|
|
return {
|
|
ok: false,
|
|
error: message,
|
|
};
|
|
}
|
|
}
|