Files
openclaw/src/acp/runtime/session-identity.ts
Bob 732e075e92 ACP: reproduce binding restart session reset (#49435)
* 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>
2026-03-18 07:24:38 +01:00

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 } : {}),
};
}