mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-15 20:10:42 +00:00
fix(acp): canonicalize main alias session rehydrate
This commit is contained in:
@@ -120,6 +120,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Dependencies: refresh workspace dependencies except the pinned Carbon package, and harden ACP session-config writes against non-string SDK values so newer ACP clients fail fast instead of tripping type/runtime mismatches.
|
||||
- Gateway/config errors: surface up to three validation issues in top-level `config.set`, `config.patch`, and `config.apply` error messages while preserving structured issue details. (#42664) Thanks @huntharo.
|
||||
- Hooks/plugin context parity followup: pass `trigger` and `channelId` through embedded `llm_input`, `agent_end`, and `llm_output` hook contexts so plugins receive the same agent metadata across hook phases. (#42362) Thanks @zhoulf1006.
|
||||
- ACP/main session aliases: canonicalize `main` before ACP session lookup so restarted ACP main sessions rehydrate instead of failing closed with `Session is not ACP-enabled: main`. Fixes #25692.
|
||||
|
||||
## 2026.3.8
|
||||
|
||||
|
||||
@@ -44,11 +44,11 @@ import {
|
||||
type TurnLatencyStats,
|
||||
} from "./manager.types.js";
|
||||
import {
|
||||
canonicalizeAcpSessionKey,
|
||||
createUnsupportedControlError,
|
||||
hasLegacyAcpIdentityProjection,
|
||||
normalizeAcpErrorCode,
|
||||
normalizeActorKey,
|
||||
normalizeSessionKey,
|
||||
requireReadySessionMeta,
|
||||
resolveAcpAgentFromSessionKey,
|
||||
resolveAcpSessionResolutionError,
|
||||
@@ -87,7 +87,7 @@ export class AcpSessionManager {
|
||||
constructor(private readonly deps: AcpSessionManagerDeps = DEFAULT_DEPS) {}
|
||||
|
||||
resolveSession(params: { cfg: OpenClawConfig; sessionKey: string }): AcpSessionResolution {
|
||||
const sessionKey = normalizeSessionKey(params.sessionKey);
|
||||
const sessionKey = canonicalizeAcpSessionKey(params);
|
||||
if (!sessionKey) {
|
||||
return {
|
||||
kind: "none",
|
||||
@@ -213,7 +213,10 @@ export class AcpSessionManager {
|
||||
handle: AcpRuntimeHandle;
|
||||
meta: SessionAcpMeta;
|
||||
}> {
|
||||
const sessionKey = normalizeSessionKey(input.sessionKey);
|
||||
const sessionKey = canonicalizeAcpSessionKey({
|
||||
cfg: input.cfg,
|
||||
sessionKey: input.sessionKey,
|
||||
});
|
||||
if (!sessionKey) {
|
||||
throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required.");
|
||||
}
|
||||
@@ -321,7 +324,7 @@ export class AcpSessionManager {
|
||||
sessionKey: string;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<AcpSessionStatus> {
|
||||
const sessionKey = normalizeSessionKey(params.sessionKey);
|
||||
const sessionKey = canonicalizeAcpSessionKey(params);
|
||||
if (!sessionKey) {
|
||||
throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required.");
|
||||
}
|
||||
@@ -397,7 +400,7 @@ export class AcpSessionManager {
|
||||
sessionKey: string;
|
||||
runtimeMode: string;
|
||||
}): Promise<AcpSessionRuntimeOptions> {
|
||||
const sessionKey = normalizeSessionKey(params.sessionKey);
|
||||
const sessionKey = canonicalizeAcpSessionKey(params);
|
||||
if (!sessionKey) {
|
||||
throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required.");
|
||||
}
|
||||
@@ -452,7 +455,7 @@ export class AcpSessionManager {
|
||||
key: string;
|
||||
value: string;
|
||||
}): Promise<AcpSessionRuntimeOptions> {
|
||||
const sessionKey = normalizeSessionKey(params.sessionKey);
|
||||
const sessionKey = canonicalizeAcpSessionKey(params);
|
||||
if (!sessionKey) {
|
||||
throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required.");
|
||||
}
|
||||
@@ -525,7 +528,7 @@ export class AcpSessionManager {
|
||||
sessionKey: string;
|
||||
patch: Partial<AcpSessionRuntimeOptions>;
|
||||
}): Promise<AcpSessionRuntimeOptions> {
|
||||
const sessionKey = normalizeSessionKey(params.sessionKey);
|
||||
const sessionKey = canonicalizeAcpSessionKey(params);
|
||||
const validatedPatch = validateRuntimeOptionPatch(params.patch);
|
||||
if (!sessionKey) {
|
||||
throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required.");
|
||||
@@ -555,7 +558,7 @@ export class AcpSessionManager {
|
||||
cfg: OpenClawConfig;
|
||||
sessionKey: string;
|
||||
}): Promise<AcpSessionRuntimeOptions> {
|
||||
const sessionKey = normalizeSessionKey(params.sessionKey);
|
||||
const sessionKey = canonicalizeAcpSessionKey(params);
|
||||
if (!sessionKey) {
|
||||
throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required.");
|
||||
}
|
||||
@@ -591,7 +594,10 @@ export class AcpSessionManager {
|
||||
}
|
||||
|
||||
async runTurn(input: AcpRunTurnInput): Promise<void> {
|
||||
const sessionKey = normalizeSessionKey(input.sessionKey);
|
||||
const sessionKey = canonicalizeAcpSessionKey({
|
||||
cfg: input.cfg,
|
||||
sessionKey: input.sessionKey,
|
||||
});
|
||||
if (!sessionKey) {
|
||||
throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required.");
|
||||
}
|
||||
@@ -738,7 +744,7 @@ export class AcpSessionManager {
|
||||
sessionKey: string;
|
||||
reason?: string;
|
||||
}): Promise<void> {
|
||||
const sessionKey = normalizeSessionKey(params.sessionKey);
|
||||
const sessionKey = canonicalizeAcpSessionKey(params);
|
||||
if (!sessionKey) {
|
||||
throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required.");
|
||||
}
|
||||
@@ -806,7 +812,10 @@ export class AcpSessionManager {
|
||||
}
|
||||
|
||||
async closeSession(input: AcpCloseSessionInput): Promise<AcpCloseSessionResult> {
|
||||
const sessionKey = normalizeSessionKey(input.sessionKey);
|
||||
const sessionKey = canonicalizeAcpSessionKey({
|
||||
cfg: input.cfg,
|
||||
sessionKey: input.sessionKey,
|
||||
});
|
||||
if (!sessionKey) {
|
||||
throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required.");
|
||||
}
|
||||
|
||||
@@ -170,6 +170,52 @@ describe("AcpSessionManager", () => {
|
||||
expect(resolved.error.message).toContain("ACP metadata is missing");
|
||||
});
|
||||
|
||||
it("canonicalizes the main alias before ACP rehydrate after restart", async () => {
|
||||
const runtimeState = createRuntime();
|
||||
hoisted.requireAcpRuntimeBackendMock.mockReturnValue({
|
||||
id: "acpx",
|
||||
runtime: runtimeState.runtime,
|
||||
});
|
||||
hoisted.readAcpSessionEntryMock.mockImplementation((paramsUnknown: unknown) => {
|
||||
const sessionKey = (paramsUnknown as { sessionKey?: string }).sessionKey;
|
||||
if (sessionKey !== "agent:main:main") {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
sessionKey,
|
||||
storeSessionKey: sessionKey,
|
||||
acp: readySessionMeta(),
|
||||
};
|
||||
});
|
||||
|
||||
const manager = new AcpSessionManager();
|
||||
const cfg = {
|
||||
...baseCfg,
|
||||
session: { mainKey: "main" },
|
||||
agents: { list: [{ id: "main", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
|
||||
await manager.runTurn({
|
||||
cfg,
|
||||
sessionKey: "main",
|
||||
text: "after restart",
|
||||
mode: "prompt",
|
||||
requestId: "r-main",
|
||||
});
|
||||
|
||||
expect(hoisted.readAcpSessionEntryMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cfg,
|
||||
sessionKey: "agent:main:main",
|
||||
}),
|
||||
);
|
||||
expect(runtimeState.ensureSession).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:main",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("serializes concurrent turns for the same ACP session", async () => {
|
||||
const runtimeState = createRuntime();
|
||||
hoisted.requireAcpRuntimeBackendMock.mockReturnValue({
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import {
|
||||
canonicalizeMainSessionAlias,
|
||||
resolveMainSessionKey,
|
||||
} from "../../config/sessions/main-session.js";
|
||||
import type { SessionAcpMeta } from "../../config/sessions/types.js";
|
||||
import { normalizeAgentId, parseAgentSessionKey } from "../../routing/session-key.js";
|
||||
import {
|
||||
normalizeAgentId,
|
||||
normalizeMainKey,
|
||||
parseAgentSessionKey,
|
||||
} from "../../routing/session-key.js";
|
||||
import { ACP_ERROR_CODES, AcpRuntimeError } from "../runtime/errors.js";
|
||||
import type { AcpSessionResolution } from "./manager.types.js";
|
||||
|
||||
@@ -42,6 +50,33 @@ export function normalizeSessionKey(sessionKey: string): string {
|
||||
return sessionKey.trim();
|
||||
}
|
||||
|
||||
export function canonicalizeAcpSessionKey(params: {
|
||||
cfg: OpenClawConfig;
|
||||
sessionKey: string;
|
||||
}): string {
|
||||
const normalized = normalizeSessionKey(params.sessionKey);
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
const lowered = normalized.toLowerCase();
|
||||
if (lowered === "global" || lowered === "unknown") {
|
||||
return lowered;
|
||||
}
|
||||
const parsed = parseAgentSessionKey(lowered);
|
||||
if (parsed) {
|
||||
return canonicalizeMainSessionAlias({
|
||||
cfg: params.cfg,
|
||||
agentId: parsed.agentId,
|
||||
sessionKey: lowered,
|
||||
});
|
||||
}
|
||||
const mainKey = normalizeMainKey(params.cfg.session?.mainKey);
|
||||
if (lowered === "main" || lowered === mainKey) {
|
||||
return resolveMainSessionKey(params.cfg);
|
||||
}
|
||||
return lowered;
|
||||
}
|
||||
|
||||
export function normalizeActorKey(sessionKey: string): string {
|
||||
return sessionKey.trim().toLowerCase();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user