mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-08 07:41:08 +00:00
* ACP: reproduce restart binding regression * ACP: resume configured bindings after restart * ACP: scope restart resume to persistent sessions --------- Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
220 lines
6.9 KiB
TypeScript
220 lines
6.9 KiB
TypeScript
import type {
|
|
SessionAcpIdentity,
|
|
SessionAcpIdentitySource,
|
|
SessionAcpMeta,
|
|
} from "../../config/sessions/types.js";
|
|
import type { AcpRuntimeHandle, AcpRuntimeStatus } from "./types.js";
|
|
|
|
function normalizeText(value: unknown): string | undefined {
|
|
if (typeof value !== "string") {
|
|
return undefined;
|
|
}
|
|
const trimmed = value.trim();
|
|
return trimmed || undefined;
|
|
}
|
|
|
|
function normalizeIdentityState(value: unknown): SessionAcpIdentity["state"] | undefined {
|
|
if (value !== "pending" && value !== "resolved") {
|
|
return undefined;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function normalizeIdentitySource(value: unknown): SessionAcpIdentitySource | undefined {
|
|
if (value !== "ensure" && value !== "status" && value !== "event") {
|
|
return undefined;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function normalizeIdentity(
|
|
identity: SessionAcpIdentity | undefined,
|
|
): SessionAcpIdentity | undefined {
|
|
if (!identity) {
|
|
return undefined;
|
|
}
|
|
const state = normalizeIdentityState(identity.state);
|
|
const source = normalizeIdentitySource(identity.source);
|
|
const acpxRecordId = normalizeText(identity.acpxRecordId);
|
|
const acpxSessionId = normalizeText(identity.acpxSessionId);
|
|
const agentSessionId = normalizeText(identity.agentSessionId);
|
|
const lastUpdatedAt =
|
|
typeof identity.lastUpdatedAt === "number" && Number.isFinite(identity.lastUpdatedAt)
|
|
? identity.lastUpdatedAt
|
|
: undefined;
|
|
const hasAnyId = Boolean(acpxRecordId || acpxSessionId || agentSessionId);
|
|
if (!state && !source && !hasAnyId && lastUpdatedAt === undefined) {
|
|
return undefined;
|
|
}
|
|
const resolved = Boolean(acpxSessionId || agentSessionId);
|
|
const normalizedState = state ?? (resolved ? "resolved" : "pending");
|
|
return {
|
|
state: normalizedState,
|
|
...(acpxRecordId ? { acpxRecordId } : {}),
|
|
...(acpxSessionId ? { acpxSessionId } : {}),
|
|
...(agentSessionId ? { agentSessionId } : {}),
|
|
source: source ?? "status",
|
|
lastUpdatedAt: lastUpdatedAt ?? Date.now(),
|
|
};
|
|
}
|
|
|
|
export function resolveSessionIdentityFromMeta(
|
|
meta: SessionAcpMeta | undefined,
|
|
): SessionAcpIdentity | undefined {
|
|
if (!meta) {
|
|
return undefined;
|
|
}
|
|
return normalizeIdentity(meta.identity);
|
|
}
|
|
|
|
export function identityHasStableSessionId(identity: SessionAcpIdentity | undefined): boolean {
|
|
return Boolean(identity?.acpxSessionId || identity?.agentSessionId);
|
|
}
|
|
|
|
export function resolveRuntimeResumeSessionId(
|
|
identity: SessionAcpIdentity | undefined,
|
|
): string | undefined {
|
|
if (!identity) {
|
|
return undefined;
|
|
}
|
|
return normalizeText(identity.acpxSessionId) ?? normalizeText(identity.agentSessionId);
|
|
}
|
|
|
|
export function isSessionIdentityPending(identity: SessionAcpIdentity | undefined): boolean {
|
|
if (!identity) {
|
|
return true;
|
|
}
|
|
return identity.state === "pending";
|
|
}
|
|
|
|
export function identityEquals(
|
|
left: SessionAcpIdentity | undefined,
|
|
right: SessionAcpIdentity | undefined,
|
|
): boolean {
|
|
const a = normalizeIdentity(left);
|
|
const b = normalizeIdentity(right);
|
|
if (!a && !b) {
|
|
return true;
|
|
}
|
|
if (!a || !b) {
|
|
return false;
|
|
}
|
|
return (
|
|
a.state === b.state &&
|
|
a.acpxRecordId === b.acpxRecordId &&
|
|
a.acpxSessionId === b.acpxSessionId &&
|
|
a.agentSessionId === b.agentSessionId &&
|
|
a.source === b.source
|
|
);
|
|
}
|
|
|
|
export function mergeSessionIdentity(params: {
|
|
current: SessionAcpIdentity | undefined;
|
|
incoming: SessionAcpIdentity | undefined;
|
|
now: number;
|
|
}): SessionAcpIdentity | undefined {
|
|
const current = normalizeIdentity(params.current);
|
|
const incoming = normalizeIdentity(params.incoming);
|
|
if (!current) {
|
|
if (!incoming) {
|
|
return undefined;
|
|
}
|
|
return { ...incoming, lastUpdatedAt: params.now };
|
|
}
|
|
if (!incoming) {
|
|
return current;
|
|
}
|
|
|
|
const currentResolved = current.state === "resolved";
|
|
const incomingResolved = incoming.state === "resolved";
|
|
const allowIncomingValue = !currentResolved || incomingResolved;
|
|
const nextRecordId =
|
|
allowIncomingValue && incoming.acpxRecordId ? incoming.acpxRecordId : current.acpxRecordId;
|
|
const nextAcpxSessionId =
|
|
allowIncomingValue && incoming.acpxSessionId ? incoming.acpxSessionId : current.acpxSessionId;
|
|
const nextAgentSessionId =
|
|
allowIncomingValue && incoming.agentSessionId
|
|
? incoming.agentSessionId
|
|
: current.agentSessionId;
|
|
|
|
const nextResolved = Boolean(nextAcpxSessionId || nextAgentSessionId);
|
|
const nextState: SessionAcpIdentity["state"] = nextResolved
|
|
? "resolved"
|
|
: currentResolved
|
|
? "resolved"
|
|
: incoming.state;
|
|
const nextSource = allowIncomingValue ? incoming.source : current.source;
|
|
const next: SessionAcpIdentity = {
|
|
state: nextState,
|
|
...(nextRecordId ? { acpxRecordId: nextRecordId } : {}),
|
|
...(nextAcpxSessionId ? { acpxSessionId: nextAcpxSessionId } : {}),
|
|
...(nextAgentSessionId ? { agentSessionId: nextAgentSessionId } : {}),
|
|
source: nextSource,
|
|
lastUpdatedAt: params.now,
|
|
};
|
|
return next;
|
|
}
|
|
|
|
export function createIdentityFromEnsure(params: {
|
|
handle: AcpRuntimeHandle;
|
|
now: number;
|
|
}): SessionAcpIdentity | undefined {
|
|
const acpxRecordId = normalizeText((params.handle as { acpxRecordId?: unknown }).acpxRecordId);
|
|
const acpxSessionId = normalizeText(params.handle.backendSessionId);
|
|
const agentSessionId = normalizeText(params.handle.agentSessionId);
|
|
if (!acpxRecordId && !acpxSessionId && !agentSessionId) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
state: "pending",
|
|
...(acpxRecordId ? { acpxRecordId } : {}),
|
|
...(acpxSessionId ? { acpxSessionId } : {}),
|
|
...(agentSessionId ? { agentSessionId } : {}),
|
|
source: "ensure",
|
|
lastUpdatedAt: params.now,
|
|
};
|
|
}
|
|
|
|
export function createIdentityFromStatus(params: {
|
|
status: AcpRuntimeStatus | undefined;
|
|
now: number;
|
|
}): SessionAcpIdentity | undefined {
|
|
if (!params.status) {
|
|
return undefined;
|
|
}
|
|
const details = params.status.details;
|
|
const acpxRecordId =
|
|
normalizeText((params.status as { acpxRecordId?: unknown }).acpxRecordId) ??
|
|
normalizeText(details?.acpxRecordId);
|
|
const acpxSessionId =
|
|
normalizeText(params.status.backendSessionId) ??
|
|
normalizeText(details?.backendSessionId) ??
|
|
normalizeText(details?.acpxSessionId);
|
|
const agentSessionId =
|
|
normalizeText(params.status.agentSessionId) ?? normalizeText(details?.agentSessionId);
|
|
if (!acpxRecordId && !acpxSessionId && !agentSessionId) {
|
|
return undefined;
|
|
}
|
|
const resolved = Boolean(acpxSessionId || agentSessionId);
|
|
return {
|
|
state: resolved ? "resolved" : "pending",
|
|
...(acpxRecordId ? { acpxRecordId } : {}),
|
|
...(acpxSessionId ? { acpxSessionId } : {}),
|
|
...(agentSessionId ? { agentSessionId } : {}),
|
|
source: "status",
|
|
lastUpdatedAt: params.now,
|
|
};
|
|
}
|
|
|
|
export function resolveRuntimeHandleIdentifiersFromIdentity(
|
|
identity: SessionAcpIdentity | undefined,
|
|
): { backendSessionId?: string; agentSessionId?: string } {
|
|
if (!identity) {
|
|
return {};
|
|
}
|
|
return {
|
|
...(identity.acpxSessionId ? { backendSessionId: identity.acpxSessionId } : {}),
|
|
...(identity.agentSessionId ? { agentSessionId: identity.agentSessionId } : {}),
|
|
};
|
|
}
|