refactor: clean up ACP package metadata and helpers (#88659)

* refactor: derive acp core package subpath maps

* refactor: split acp manager task and timeout helpers

* refactor: split acp translator presentation helpers

* fix: keep packaged acp core plugin aliases

* ci: split gateway control plane runtime shard
This commit is contained in:
Peter Steinberger
2026-05-31 15:53:14 +01:00
committed by GitHub
parent a5d8f09fd4
commit 7b78941ea5
18 changed files with 1073 additions and 840 deletions

View File

@@ -254,12 +254,39 @@
"@openclaw/acp-core/normalize-text": [
"../dist/plugin-sdk/packages/acp-core/src/normalize-text.d.ts"
],
"@openclaw/acp-core/meta": [
"../dist/plugin-sdk/packages/acp-core/src/meta.d.ts"
],
"@openclaw/acp-core/numeric-options": [
"../dist/plugin-sdk/packages/acp-core/src/numeric-options.d.ts"
],
"@openclaw/acp-core/record-shared": [
"../dist/plugin-sdk/packages/acp-core/src/record-shared.d.ts"
],
"@openclaw/acp-core/session": [
"../dist/plugin-sdk/packages/acp-core/src/session.d.ts"
],
"@openclaw/acp-core/session-interaction-mode": [
"../dist/plugin-sdk/packages/acp-core/src/session-interaction-mode.d.ts"
],
"@openclaw/acp-core/session-lineage-meta": [
"../dist/plugin-sdk/packages/acp-core/src/session-lineage-meta.d.ts"
],
"@openclaw/acp-core/types": [
"../dist/plugin-sdk/packages/acp-core/src/types.d.ts"
],
"@openclaw/acp-core/runtime/error-text": [
"../dist/plugin-sdk/packages/acp-core/src/runtime/error-text.d.ts"
],
"@openclaw/acp-core/runtime/errors": [
"../dist/plugin-sdk/packages/acp-core/src/runtime/errors.d.ts"
],
"@openclaw/acp-core/runtime/session-identifiers": [
"../dist/plugin-sdk/packages/acp-core/src/runtime/session-identifiers.d.ts"
],
"@openclaw/acp-core/runtime/session-identity": [
"../dist/plugin-sdk/packages/acp-core/src/runtime/session-identity.d.ts"
],
"@openclaw/acp-core/runtime/types": [
"../dist/plugin-sdk/packages/acp-core/src/runtime/types.d.ts"
],

View File

@@ -240,12 +240,39 @@
"@openclaw/acp-core/normalize-text": [
"../../dist/plugin-sdk/packages/acp-core/src/normalize-text.d.ts"
],
"@openclaw/acp-core/meta": [
"../../dist/plugin-sdk/packages/acp-core/src/meta.d.ts"
],
"@openclaw/acp-core/numeric-options": [
"../../dist/plugin-sdk/packages/acp-core/src/numeric-options.d.ts"
],
"@openclaw/acp-core/record-shared": [
"../../dist/plugin-sdk/packages/acp-core/src/record-shared.d.ts"
],
"@openclaw/acp-core/session": [
"../../dist/plugin-sdk/packages/acp-core/src/session.d.ts"
],
"@openclaw/acp-core/session-interaction-mode": [
"../../dist/plugin-sdk/packages/acp-core/src/session-interaction-mode.d.ts"
],
"@openclaw/acp-core/session-lineage-meta": [
"../../dist/plugin-sdk/packages/acp-core/src/session-lineage-meta.d.ts"
],
"@openclaw/acp-core/types": [
"../../dist/plugin-sdk/packages/acp-core/src/types.d.ts"
],
"@openclaw/acp-core/runtime/error-text": [
"../../dist/plugin-sdk/packages/acp-core/src/runtime/error-text.d.ts"
],
"@openclaw/acp-core/runtime/errors": [
"../../dist/plugin-sdk/packages/acp-core/src/runtime/errors.d.ts"
],
"@openclaw/acp-core/runtime/session-identifiers": [
"../../dist/plugin-sdk/packages/acp-core/src/runtime/session-identifiers.d.ts"
],
"@openclaw/acp-core/runtime/session-identity": [
"../../dist/plugin-sdk/packages/acp-core/src/runtime/session-identity.d.ts"
],
"@openclaw/acp-core/runtime/types": [
"../../dist/plugin-sdk/packages/acp-core/src/runtime/types.d.ts"
],

View File

@@ -234,6 +234,12 @@ function resolveGatewayServerShardName(file) {
) {
return "agentic-control-plane-startup-runtime";
}
if (name.includes("cron")) {
return "agentic-control-plane-runtime-cron";
}
if (name.includes("network")) {
return "agentic-control-plane-runtime-network";
}
if (
name.includes("plugin") ||
name.includes("hooks") ||
@@ -242,6 +248,12 @@ function resolveGatewayServerShardName(file) {
) {
return "agentic-control-plane-http-plugin-ws";
}
if (name.startsWith("server-")) {
return "agentic-control-plane-runtime-server";
}
if (name.startsWith("server.") || name.startsWith("server/")) {
return "agentic-control-plane-runtime-state";
}
return "agentic-control-plane-runtime";
}
@@ -257,6 +269,10 @@ function createGatewayServerSplitShards() {
"agentic-control-plane-http-models",
"agentic-control-plane-http-plugin-ws",
"agentic-control-plane-runtime",
"agentic-control-plane-runtime-cron",
"agentic-control-plane-runtime-network",
"agentic-control-plane-runtime-server",
"agentic-control-plane-runtime-state",
"agentic-control-plane-startup-runtime",
]
.map((shardName) => ({

View File

@@ -20,6 +20,38 @@ const privateLocalOnlyPluginSdkPackageDtsPaths = Object.fromEntries(
]),
) as Record<string, readonly string[]>;
function buildPackageBoundaryDtsPaths(params: {
packageName: string;
packageDir: string;
}): Record<string, readonly string[]> {
const packageJson = JSON.parse(
readFileSync(join("packages", params.packageDir, "package.json"), "utf8"),
) as { exports?: Record<string, unknown> };
return Object.fromEntries(
Object.entries(packageJson.exports ?? {}).flatMap(([exportKey, value]) => {
const subpath =
exportKey === "." ? "" : exportKey.startsWith("./") ? exportKey.slice(2) : null;
const importPath =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>).import
: value;
if (subpath === null || subpath.includes("..") || typeof importPath !== "string") {
return [];
}
if (!importPath.startsWith("./dist/") || !importPath.endsWith(".mjs")) {
return [];
}
const specifier = subpath ? `${params.packageName}/${subpath}` : params.packageName;
return [
[
specifier,
[`../dist/plugin-sdk/packages/${params.packageDir}/src/${subpath || "index"}.d.ts`],
],
];
}),
);
}
export const EXTENSION_PACKAGE_BOUNDARY_BASE_PATHS = {
"openclaw/extension-api": ["../src/extensionAPI.ts"],
"openclaw/plugin-sdk": ["../dist/plugin-sdk/index.d.ts"],
@@ -147,19 +179,10 @@ export const EXTENSION_PACKAGE_BOUNDARY_BASE_PATHS = {
"../dist/plugin-sdk/packages/normalization-core/src/string-coerce.d.ts",
],
"@openclaw/normalization-core/*": ["../dist/plugin-sdk/packages/normalization-core/src/*.d.ts"],
"@openclaw/acp-core": ["../dist/plugin-sdk/packages/acp-core/src/index.d.ts"],
"@openclaw/acp-core/normalize-text": [
"../dist/plugin-sdk/packages/acp-core/src/normalize-text.d.ts",
],
"@openclaw/acp-core/record-shared": [
"../dist/plugin-sdk/packages/acp-core/src/record-shared.d.ts",
],
"@openclaw/acp-core/runtime/errors": [
"../dist/plugin-sdk/packages/acp-core/src/runtime/errors.d.ts",
],
"@openclaw/acp-core/runtime/types": [
"../dist/plugin-sdk/packages/acp-core/src/runtime/types.d.ts",
],
...buildPackageBoundaryDtsPaths({
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
}),
"@openclaw/acp-core/*": ["../dist/plugin-sdk/packages/acp-core/src/*.d.ts"],
"@openclaw/terminal-core": ["../dist/plugin-sdk/packages/terminal-core/src/index.d.ts"],
"@openclaw/terminal-core/ansi": ["../dist/plugin-sdk/packages/terminal-core/src/ansi.d.ts"],

View File

@@ -13,6 +13,27 @@ const ROOT_SHIMS_MAX_OLD_SPACE_SIZE =
const ROOT_SHIMS_NODE_OPTIONS =
`${process.env.NODE_OPTIONS ?? ""} --max-old-space-size=${ROOT_SHIMS_MAX_OLD_SPACE_SIZE}`.trim();
function listPackageDtsOutputsFromExports({ packageDir, outputPrefix }) {
const packageJson = JSON.parse(
fs.readFileSync(path.join(repoRoot, "packages", packageDir, "package.json"), "utf8"),
);
return Object.entries(packageJson.exports ?? {})
.flatMap(([exportKey, value]) => {
const entry =
exportKey === "." ? "index" : exportKey.startsWith("./") ? exportKey.slice(2) : "";
const importPath =
value && typeof value === "object" && !Array.isArray(value) ? value.import : value;
if (!entry || entry.includes("..") || typeof importPath !== "string") {
return [];
}
if (!importPath.startsWith("./dist/") || !importPath.endsWith(".mjs")) {
return [];
}
return [`${outputPrefix}/${entry}.d.ts`];
})
.toSorted((a, b) => a.localeCompare(b));
}
const PLUGIN_SDK_TYPE_INPUTS = [
"tsconfig.json",
"src/plugin-sdk",
@@ -33,6 +54,10 @@ const PLUGIN_SDK_TYPE_INPUTS = [
];
const ROOT_DTS_INPUTS = ["tsconfig.plugin-sdk.dts.json", ...PLUGIN_SDK_TYPE_INPUTS];
const ROOT_DTS_STAMP = "dist/plugin-sdk/.boundary-dts.stamp";
const ACP_CORE_REQUIRED_DTS_OUTPUTS = listPackageDtsOutputsFromExports({
packageDir: "acp-core",
outputPrefix: "dist/plugin-sdk/packages/acp-core/src",
});
const ROOT_DTS_REQUIRED_OUTPUTS = [
"dist/plugin-sdk/packages/memory-host-sdk/src/engine-embeddings.d.ts",
"dist/plugin-sdk/packages/memory-host-sdk/src/secret.d.ts",
@@ -66,11 +91,7 @@ const ROOT_DTS_REQUIRED_OUTPUTS = [
"dist/plugin-sdk/packages/media-core/src/mime.d.ts",
"dist/plugin-sdk/packages/media-core/src/read-byte-stream-with-limit.d.ts",
"dist/plugin-sdk/packages/media-core/src/read-response-with-limit.d.ts",
"dist/plugin-sdk/packages/acp-core/src/index.d.ts",
"dist/plugin-sdk/packages/acp-core/src/normalize-text.d.ts",
"dist/plugin-sdk/packages/acp-core/src/record-shared.d.ts",
"dist/plugin-sdk/packages/acp-core/src/runtime/errors.d.ts",
"dist/plugin-sdk/packages/acp-core/src/runtime/types.d.ts",
...ACP_CORE_REQUIRED_DTS_OUTPUTS,
"dist/plugin-sdk/packages/terminal-core/src/ansi.d.ts",
"dist/plugin-sdk/packages/terminal-core/src/decorative-emoji.d.ts",
"dist/plugin-sdk/packages/terminal-core/src/health-style.d.ts",
@@ -103,6 +124,10 @@ const ROOT_DTS_REQUIRED_OUTPUTS = [
];
const PACKAGE_DTS_INPUTS = ["packages/plugin-sdk/tsconfig.json", ...PLUGIN_SDK_TYPE_INPUTS];
const PACKAGE_DTS_STAMP = "packages/plugin-sdk/dist/.boundary-dts.stamp";
const ACP_CORE_REQUIRED_PACKAGE_DTS_OUTPUTS = listPackageDtsOutputsFromExports({
packageDir: "acp-core",
outputPrefix: "packages/plugin-sdk/dist/packages/acp-core/src",
});
const PACKAGE_DTS_REQUIRED_OUTPUTS = [
"packages/plugin-sdk/dist/packages/markdown-core/src/code-spans.d.ts",
"packages/plugin-sdk/dist/packages/markdown-core/src/fences.d.ts",
@@ -128,11 +153,7 @@ const PACKAGE_DTS_REQUIRED_OUTPUTS = [
"packages/plugin-sdk/dist/packages/media-core/src/mime.d.ts",
"packages/plugin-sdk/dist/packages/media-core/src/read-byte-stream-with-limit.d.ts",
"packages/plugin-sdk/dist/packages/media-core/src/read-response-with-limit.d.ts",
"packages/plugin-sdk/dist/packages/acp-core/src/index.d.ts",
"packages/plugin-sdk/dist/packages/acp-core/src/normalize-text.d.ts",
"packages/plugin-sdk/dist/packages/acp-core/src/record-shared.d.ts",
"packages/plugin-sdk/dist/packages/acp-core/src/runtime/errors.d.ts",
"packages/plugin-sdk/dist/packages/acp-core/src/runtime/types.d.ts",
...ACP_CORE_REQUIRED_PACKAGE_DTS_OUTPUTS,
"packages/plugin-sdk/dist/packages/model-catalog-core/src/configured-model-refs.d.ts",
"packages/plugin-sdk/dist/packages/model-catalog-core/src/model-catalog-normalize.d.ts",
"packages/plugin-sdk/dist/packages/model-catalog-core/src/model-catalog-refs.d.ts",

View File

@@ -0,0 +1,206 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { logVerbose } from "../../globals.js";
import {
createRunningTaskRun,
completeTaskRunByRunId,
failTaskRunByRunId,
startTaskRunByRunId,
} from "../../tasks/detached-task-runtime.js";
import { resolveRequiredCompletionTerminalResult } from "../../tasks/task-completion-contract.js";
import type { DeliveryContext } from "../../utils/delivery-context.js";
import { AcpRuntimeError } from "../runtime/errors.js";
import type { AcpSessionManagerDeps } from "./manager.types.js";
import { normalizeText } from "./runtime-options.js";
const ACP_BACKGROUND_TASK_TEXT_MAX_LENGTH = 160;
const ACP_BACKGROUND_TASK_PROGRESS_MAX_LENGTH = 240;
export type BackgroundTaskContext = {
requesterSessionKey: string;
requesterOrigin?: DeliveryContext;
childSessionKey: string;
runId: string;
label?: string;
task: string;
};
export function summarizeBackgroundTaskText(text: string): string {
const normalized = normalizeText(text) ?? "ACP background task";
if (normalized.length <= ACP_BACKGROUND_TASK_TEXT_MAX_LENGTH) {
return normalized;
}
return `${normalized.slice(0, ACP_BACKGROUND_TASK_TEXT_MAX_LENGTH - 1)}`;
}
export function appendBackgroundTaskProgressSummary(current: string, chunk: string): string {
const normalizedChunk = chunk.replace(/\s+/g, " ");
if (!normalizedChunk) {
return current;
}
const chunkToAppend = current ? normalizedChunk : normalizedChunk.trimStart();
if (!chunkToAppend) {
return current;
}
const combined = `${current}${chunkToAppend}`.replace(/\s+/g, " ");
if (combined.length <= ACP_BACKGROUND_TASK_PROGRESS_MAX_LENGTH) {
return combined;
}
return `${combined.slice(0, ACP_BACKGROUND_TASK_PROGRESS_MAX_LENGTH - 1)}`;
}
export function resolveBackgroundTaskFailureStatus(error: AcpRuntimeError): "failed" | "timed_out" {
return /\btimed out\b/i.test(error.message) ? "timed_out" : "failed";
}
export function resolveBackgroundTaskTerminalResult(progressSummary: string): {
terminalOutcome?: "blocked";
terminalSummary?: string;
} {
const requiredCompletionResult = resolveRequiredCompletionTerminalResult(progressSummary);
if (requiredCompletionResult.terminalOutcome) {
return requiredCompletionResult;
}
const normalized = normalizeText(progressSummary)?.replace(/\s+/g, " ").trim();
if (!normalized) {
return {};
}
const permissionDeniedMatch = normalized.match(
/\b(?:write failed:\s*)?permission denied(?: for (?<path>\S+))?\.?/i,
);
if (permissionDeniedMatch) {
const path = normalizeText(permissionDeniedMatch.groups?.path)?.replace(/[.,;:!?]+$/, "");
return {
terminalOutcome: "blocked",
terminalSummary: path ? `Permission denied for ${path}.` : "Permission denied.",
};
}
if (
/\bneed a writable session\b/i.test(normalized) ||
/\bfilesystem authorization\b/i.test(normalized) ||
/`?apply_patch`?/i.test(normalized)
) {
return {
terminalOutcome: "blocked",
terminalSummary: "Writable session or apply_patch authorization required.",
};
}
return {};
}
export function resolveBackgroundTaskContext(params: {
deps: AcpSessionManagerDeps;
cfg: OpenClawConfig;
sessionKey: string;
requestId: string;
text: string;
}): BackgroundTaskContext | null {
const childEntry = params.deps.readSessionEntry({
cfg: params.cfg,
sessionKey: params.sessionKey,
})?.entry;
const requesterSessionKey =
normalizeText(childEntry?.spawnedBy) ?? normalizeText(childEntry?.parentSessionKey);
if (!requesterSessionKey) {
return null;
}
const parentEntry = params.deps.readSessionEntry({
cfg: params.cfg,
sessionKey: requesterSessionKey,
})?.entry;
return {
requesterSessionKey,
requesterOrigin: parentEntry?.deliveryContext ?? childEntry?.deliveryContext,
childSessionKey: params.sessionKey,
runId: params.requestId,
label: normalizeText(childEntry?.label),
task: summarizeBackgroundTaskText(params.text),
};
}
export function createBackgroundTaskRecord(
context: BackgroundTaskContext,
startedAt: number,
): void {
try {
createRunningTaskRun({
runtime: "acp",
sourceId: context.runId,
ownerKey: context.requesterSessionKey,
scopeKind: "session",
requesterOrigin: context.requesterOrigin,
childSessionKey: context.childSessionKey,
runId: context.runId,
label: context.label,
task: context.task,
startedAt,
});
} catch (error) {
logVerbose(
`acp-manager: failed creating background task for ${context.runId}: ${String(error)}`,
);
}
}
export function markBackgroundTaskRunning(
runId: string,
params: {
sessionKey?: string;
lastEventAt?: number;
progressSummary?: string | null;
},
): void {
try {
startTaskRunByRunId({
runId,
runtime: "acp",
sessionKey: params.sessionKey,
lastEventAt: params.lastEventAt,
progressSummary: params.progressSummary,
});
} catch (error) {
logVerbose(`acp-manager: failed updating background task for ${runId}: ${String(error)}`);
}
}
export function markBackgroundTaskTerminal(
runId: string,
params: {
sessionKey?: string;
status: "succeeded" | "failed" | "timed_out";
endedAt: number;
lastEventAt?: number;
error?: string;
progressSummary?: string | null;
terminalSummary?: string | null;
terminalOutcome?: "succeeded" | "blocked" | null;
},
): void {
try {
if (params.status === "succeeded") {
completeTaskRunByRunId({
runId,
runtime: "acp",
sessionKey: params.sessionKey,
endedAt: params.endedAt,
lastEventAt: params.lastEventAt,
progressSummary: params.progressSummary,
terminalSummary: params.terminalSummary,
terminalOutcome: params.terminalOutcome,
});
return;
}
failTaskRunByRunId({
runId,
runtime: "acp",
sessionKey: params.sessionKey,
status: params.status,
endedAt: params.endedAt,
lastEventAt: params.lastEventAt,
error: params.error,
progressSummary: params.progressSummary,
terminalSummary: params.terminalSummary,
});
} catch (error) {
logVerbose(`acp-manager: failed updating background task for ${runId}: ${String(error)}`);
}
}

View File

@@ -12,26 +12,15 @@ import type {
AcpRuntime,
AcpRuntimeCapabilities,
AcpRuntimeHandle,
AcpRuntimeSessionMode,
AcpRuntimeStatus,
} from "@openclaw/acp-core/runtime/types";
import { clampTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
import { resolveRuntimeConfigCacheKey } from "../../config/runtime-snapshot.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { logVerbose } from "../../globals.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { normalizeAgentId } from "../../routing/session-key.js";
import { isAcpSessionKey } from "../../sessions/session-key-utils.js";
import {
createRunningTaskRun,
completeTaskRunByRunId,
failTaskRunByRunId,
startTaskRunByRunId,
} from "../../tasks/detached-task-runtime.js";
import { resolveRequiredCompletionTerminalResult } from "../../tasks/task-completion-contract.js";
import type { DeliveryContext } from "../../utils/delivery-context.js";
import {
AcpRuntimeError,
formatAcpErrorChain,
@@ -40,12 +29,26 @@ import {
} from "../runtime/errors.js";
import type { AcpRuntimeErrorCode } from "../runtime/errors.js";
import { clearAcpTurnActive, markAcpTurnActive } from "./active-turns.js";
import {
appendBackgroundTaskProgressSummary,
createBackgroundTaskRecord,
markBackgroundTaskRunning,
markBackgroundTaskTerminal,
resolveBackgroundTaskContext,
resolveBackgroundTaskFailureStatus,
resolveBackgroundTaskTerminalResult,
} from "./manager.background-task.js";
import { reconcileManagerRuntimeSessionIdentifiers } from "./manager.identity-reconcile.js";
import {
applyManagerRuntimeControls,
resolveManagerRuntimeCapabilities,
} from "./manager.runtime-controls.js";
import { consumeAcpTurnStream } from "./manager.turn-stream.js";
import {
awaitTurnWithTimeout,
cleanupTimedOutTurn,
resolveTurnTimeoutMs,
} from "./manager.turn-timeout.js";
import {
type AcpCloseSessionInput,
type AcpCloseSessionResult,
@@ -92,82 +95,6 @@ import {
import { SessionActorQueue } from "./session-actor-queue.js";
const ACP_TURN_TIMEOUT_GRACE_MS = 1_000;
const ACP_TURN_TIMEOUT_CLEANUP_GRACE_MS = 2_000;
const ACP_TURN_TIMEOUT_REASON = "turn-timeout";
const ACP_BACKGROUND_TASK_TEXT_MAX_LENGTH = 160;
const ACP_BACKGROUND_TASK_PROGRESS_MAX_LENGTH = 240;
function summarizeBackgroundTaskText(text: string): string {
const normalized = normalizeText(text) ?? "ACP background task";
if (normalized.length <= ACP_BACKGROUND_TASK_TEXT_MAX_LENGTH) {
return normalized;
}
return `${normalized.slice(0, ACP_BACKGROUND_TASK_TEXT_MAX_LENGTH - 1)}`;
}
function appendBackgroundTaskProgressSummary(current: string, chunk: string): string {
const normalizedChunk = chunk.replace(/\s+/g, " ");
if (!normalizedChunk) {
return current;
}
const chunkToAppend = current ? normalizedChunk : normalizedChunk.trimStart();
if (!chunkToAppend) {
return current;
}
const combined = `${current}${chunkToAppend}`.replace(/\s+/g, " ");
if (combined.length <= ACP_BACKGROUND_TASK_PROGRESS_MAX_LENGTH) {
return combined;
}
return `${combined.slice(0, ACP_BACKGROUND_TASK_PROGRESS_MAX_LENGTH - 1)}`;
}
function resolveBackgroundTaskFailureStatus(error: AcpRuntimeError): "failed" | "timed_out" {
return /\btimed out\b/i.test(error.message) ? "timed_out" : "failed";
}
function resolveBackgroundTaskTerminalResult(progressSummary: string): {
terminalOutcome?: "blocked";
terminalSummary?: string;
} {
const requiredCompletionResult = resolveRequiredCompletionTerminalResult(progressSummary);
if (requiredCompletionResult.terminalOutcome) {
return requiredCompletionResult;
}
const normalized = normalizeText(progressSummary)?.replace(/\s+/g, " ").trim();
if (!normalized) {
return {};
}
const permissionDeniedMatch = normalized.match(
/\b(?:write failed:\s*)?permission denied(?: for (?<path>\S+))?\.?/i,
);
if (permissionDeniedMatch) {
const path = normalizeText(permissionDeniedMatch.groups?.path)?.replace(/[.,;:!?]+$/, "");
return {
terminalOutcome: "blocked",
terminalSummary: path ? `Permission denied for ${path}.` : "Permission denied.",
};
}
if (
/\bneed a writable session\b/i.test(normalized) ||
/\bfilesystem authorization\b/i.test(normalized) ||
/`?apply_patch`?/i.test(normalized)
) {
return {
terminalOutcome: "blocked",
terminalSummary: "Writable session or apply_patch authorization required.",
};
}
return {};
}
type BackgroundTaskContext = {
requesterSessionKey: string;
requesterOrigin?: DeliveryContext;
childSessionKey: string;
runId: string;
label?: string;
task: string;
};
export class AcpSessionManager {
private readonly actorQueue = new SessionActorQueue();
@@ -745,7 +672,8 @@ export class AcpSessionManager {
const actorKey = normalizeActorKey(sessionKey);
const taskContext =
input.mode === "prompt"
? this.resolveBackgroundTaskContext({
? resolveBackgroundTaskContext({
deps: this.deps,
cfg: input.cfg,
sessionKey,
requestId: input.requestId,
@@ -753,7 +681,7 @@ export class AcpSessionManager {
})
: null;
if (taskContext) {
this.createBackgroundTaskRecord(taskContext, turnStartedAt);
createBackgroundTaskRecord(taskContext, turnStartedAt);
}
let taskProgressSummary = "";
const initialResolution = this.resolveSession({
@@ -805,7 +733,7 @@ export class AcpSessionManager {
errorCode: errorToRecord.code,
});
if (taskContext) {
this.markBackgroundTaskTerminal(taskContext.runId, {
markBackgroundTaskTerminal(taskContext.runId, {
sessionKey,
status: resolveBackgroundTaskFailureStatus(errorToRecord),
endedAt: Date.now(),
@@ -942,7 +870,7 @@ export class AcpSessionManager {
);
}
if (taskContext) {
this.markBackgroundTaskRunning(taskContext.runId, {
markBackgroundTaskRunning(taskContext.runId, {
sessionKey,
lastEventAt: Date.now(),
progressSummary: taskProgressSummary || null,
@@ -951,12 +879,12 @@ export class AcpSessionManager {
},
onEvent: input.onEvent,
});
const turnTimeoutMs = this.resolveTurnTimeoutMs({
const turnTimeoutMs = resolveTurnTimeoutMs({
cfg: input.cfg,
meta,
});
const sessionMode = meta.mode;
const turnOutcome = await this.awaitTurnWithTimeout({
const turnOutcome = await awaitTurnWithTimeout({
sessionKey,
turnPromise,
timeoutMs: turnTimeoutMs + ACP_TURN_TIMEOUT_GRACE_MS,
@@ -967,10 +895,16 @@ export class AcpSessionManager {
if (!activeTurn) {
return;
}
await this.cleanupTimedOutTurn({
await cleanupTimedOutTurn({
sessionKey,
activeTurn,
mode: sessionMode,
clearCachedRuntimeStateIfHandleMatches: (turn) => {
this.clearCachedRuntimeStateIfHandleMatches({
sessionKey,
handle: turn.handle,
});
},
});
},
});
@@ -985,7 +919,7 @@ export class AcpSessionManager {
});
if (taskContext) {
const terminalResult = resolveBackgroundTaskTerminalResult(taskProgressSummary);
this.markBackgroundTaskTerminal(taskContext.runId, {
markBackgroundTaskTerminal(taskContext.runId, {
sessionKey,
status: "succeeded",
endedAt: Date.now(),
@@ -1092,200 +1026,6 @@ export class AcpSessionManager {
);
}
private resolveTurnTimeoutMs(params: { cfg: OpenClawConfig; meta: SessionAcpMeta }): number {
const runtimeTimeoutSeconds = resolveRuntimeOptionsFromMeta(params.meta).timeoutSeconds;
if (
typeof runtimeTimeoutSeconds === "number" &&
Number.isFinite(runtimeTimeoutSeconds) &&
runtimeTimeoutSeconds > 0
) {
return clampTimerTimeoutMs(Math.round(runtimeTimeoutSeconds * 1_000), 1_000) ?? 1_000;
}
return resolveAgentTimeoutMs({
cfg: params.cfg,
minMs: 1_000,
});
}
private async awaitTurnWithTimeout<T>(params: {
sessionKey: string;
turnPromise: Promise<T>;
timeoutMs: number;
timeoutLabelMs: number;
onTimeout: () => Promise<void>;
}): Promise<T> {
const observedTurnPromise: Promise<
| {
kind: "value";
value: T;
}
| {
kind: "error";
error: unknown;
}
> = params.turnPromise.then(
(value) => ({
kind: "value" as const,
value,
}),
(error) => ({
kind: "error" as const,
error,
}),
);
if (params.timeoutMs <= 0) {
const outcome = await observedTurnPromise;
if (outcome.kind === "error") {
throw outcome.error;
}
return outcome.value;
}
const timeoutMs = clampTimerTimeoutMs(params.timeoutMs, 1);
if (timeoutMs === undefined) {
const outcome = await observedTurnPromise;
if (outcome.kind === "error") {
throw outcome.error;
}
return outcome.value;
}
const timeoutToken = Symbol("acp-turn-timeout");
let timer: NodeJS.Timeout | undefined;
const timeoutPromise = new Promise<typeof timeoutToken>((resolve) => {
timer = setTimeout(() => resolve(timeoutToken), timeoutMs);
timer.unref?.();
});
try {
const outcome = await Promise.race([observedTurnPromise, timeoutPromise]);
if (outcome === timeoutToken) {
void observedTurnPromise.then((lateOutcome) => {
if (lateOutcome.kind === "error") {
logVerbose(
`acp-manager: detached late turn error after timeout for ${params.sessionKey}: ${String(lateOutcome.error)}`,
);
}
});
await params.onTimeout();
throw new AcpRuntimeError(
"ACP_TURN_FAILED",
`ACP turn timed out after ${Math.max(1, Math.round(params.timeoutLabelMs / 1_000))}s.`,
);
}
if (outcome.kind === "error") {
throw outcome.error;
}
return outcome.value;
} finally {
if (timer) {
clearTimeout(timer);
}
}
}
private async cleanupTimedOutTurn(params: {
sessionKey: string;
activeTurn: ActiveTurnState;
mode: AcpRuntimeSessionMode;
}): Promise<void> {
params.activeTurn.abortController.abort();
if (!params.activeTurn.cancelPromise) {
params.activeTurn.cancelPromise = params.activeTurn.runtime.cancel({
handle: params.activeTurn.handle,
reason: ACP_TURN_TIMEOUT_REASON,
});
}
const cancelFinished = await this.awaitCleanupWithGrace({
sessionKey: params.sessionKey,
label: "cancel",
promise: params.activeTurn.cancelPromise,
});
if (params.mode !== "oneshot") {
return;
}
const closePromise = params.activeTurn.runtime.close({
handle: params.activeTurn.handle,
reason: ACP_TURN_TIMEOUT_REASON,
});
const closeFinished = await this.awaitCleanupWithGrace({
sessionKey: params.sessionKey,
label: "close",
promise: closePromise,
});
if (cancelFinished && closeFinished) {
this.clearCachedRuntimeStateIfHandleMatches({
sessionKey: params.sessionKey,
handle: params.activeTurn.handle,
});
return;
}
void Promise.allSettled([params.activeTurn.cancelPromise, closePromise]).then(() => {
this.clearCachedRuntimeStateIfHandleMatches({
sessionKey: params.sessionKey,
handle: params.activeTurn.handle,
});
});
}
private async awaitCleanupWithGrace(params: {
sessionKey: string;
label: "cancel" | "close";
promise: Promise<unknown>;
}): Promise<boolean> {
const observedCleanupPromise: Promise<
| {
kind: "done";
}
| {
kind: "error";
error: unknown;
}
> = params.promise.then(
() => ({
kind: "done" as const,
}),
(error) => ({
kind: "error" as const,
error,
}),
);
const timeoutToken = Symbol(`acp-timeout-${params.label}`);
let timer: NodeJS.Timeout | undefined;
const timeoutPromise = new Promise<typeof timeoutToken>((resolve) => {
timer = setTimeout(() => resolve(timeoutToken), ACP_TURN_TIMEOUT_CLEANUP_GRACE_MS);
timer.unref?.();
});
try {
const outcome = await Promise.race([observedCleanupPromise, timeoutPromise]);
if (outcome === timeoutToken) {
void observedCleanupPromise.then((lateOutcome) => {
if (lateOutcome.kind === "error") {
logVerbose(
`acp-manager: detached timed-out turn ${params.label} cleanup failed for ${params.sessionKey}: ${String(lateOutcome.error)}`,
);
}
});
logVerbose(
`acp-manager: timed-out turn ${params.label} cleanup exceeded ${ACP_TURN_TIMEOUT_CLEANUP_GRACE_MS}ms for ${params.sessionKey}`,
);
return false;
}
if (outcome.kind === "error") {
logVerbose(
`acp-manager: timed-out turn ${params.label} cleanup failed for ${params.sessionKey}: ${String(outcome.error)}`,
);
}
return true;
} finally {
if (timer) {
clearTimeout(timer);
}
}
}
async cancelSession(params: {
cfg: OpenClawConfig;
sessionKey: string;
@@ -2277,118 +2017,4 @@ export class AcpSessionManager {
normalizeText((params.handle as { acpxRecordId?: unknown }).acpxRecordId) ?? "";
return actualAcpxRecordId === expectedAcpxRecordId;
}
private resolveBackgroundTaskContext(params: {
cfg: OpenClawConfig;
sessionKey: string;
requestId: string;
text: string;
}): BackgroundTaskContext | null {
const childEntry = this.deps.readSessionEntry({
cfg: params.cfg,
sessionKey: params.sessionKey,
})?.entry;
const requesterSessionKey =
normalizeText(childEntry?.spawnedBy) ?? normalizeText(childEntry?.parentSessionKey);
if (!requesterSessionKey) {
return null;
}
const parentEntry = this.deps.readSessionEntry({
cfg: params.cfg,
sessionKey: requesterSessionKey,
})?.entry;
return {
requesterSessionKey,
requesterOrigin: parentEntry?.deliveryContext ?? childEntry?.deliveryContext,
childSessionKey: params.sessionKey,
runId: params.requestId,
label: normalizeText(childEntry?.label),
task: summarizeBackgroundTaskText(params.text),
};
}
private createBackgroundTaskRecord(context: BackgroundTaskContext, startedAt: number): void {
try {
createRunningTaskRun({
runtime: "acp",
sourceId: context.runId,
ownerKey: context.requesterSessionKey,
scopeKind: "session",
requesterOrigin: context.requesterOrigin,
childSessionKey: context.childSessionKey,
runId: context.runId,
label: context.label,
task: context.task,
startedAt,
});
} catch (error) {
logVerbose(
`acp-manager: failed creating background task for ${context.runId}: ${String(error)}`,
);
}
}
private markBackgroundTaskRunning(
runId: string,
params: {
sessionKey?: string;
lastEventAt?: number;
progressSummary?: string | null;
},
): void {
try {
startTaskRunByRunId({
runId,
runtime: "acp",
sessionKey: params.sessionKey,
lastEventAt: params.lastEventAt,
progressSummary: params.progressSummary,
});
} catch (error) {
logVerbose(`acp-manager: failed updating background task for ${runId}: ${String(error)}`);
}
}
private markBackgroundTaskTerminal(
runId: string,
params: {
sessionKey?: string;
status: "succeeded" | "failed" | "timed_out";
endedAt: number;
lastEventAt?: number;
error?: string;
progressSummary?: string | null;
terminalSummary?: string | null;
terminalOutcome?: "succeeded" | "blocked" | null;
},
): void {
try {
if (params.status === "succeeded") {
completeTaskRunByRunId({
runId,
runtime: "acp",
sessionKey: params.sessionKey,
endedAt: params.endedAt,
lastEventAt: params.lastEventAt,
progressSummary: params.progressSummary,
terminalSummary: params.terminalSummary,
terminalOutcome: params.terminalOutcome,
});
return;
}
failTaskRunByRunId({
runId,
runtime: "acp",
sessionKey: params.sessionKey,
status: params.status,
endedAt: params.endedAt,
lastEventAt: params.lastEventAt,
error: params.error,
progressSummary: params.progressSummary,
terminalSummary: params.terminalSummary,
});
} catch (error) {
logVerbose(`acp-manager: failed updating background task for ${runId}: ${String(error)}`);
}
}
}

View File

@@ -0,0 +1,203 @@
import type { AcpRuntimeSessionMode } from "@openclaw/acp-core/runtime/types";
import { clampTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion";
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { logVerbose } from "../../globals.js";
import { AcpRuntimeError } from "../runtime/errors.js";
import type { ActiveTurnState, SessionAcpMeta } from "./manager.types.js";
import { resolveRuntimeOptionsFromMeta } from "./runtime-options.js";
const ACP_TURN_TIMEOUT_CLEANUP_GRACE_MS = 2_000;
const ACP_TURN_TIMEOUT_REASON = "turn-timeout";
export function resolveTurnTimeoutMs(params: {
cfg: OpenClawConfig;
meta: SessionAcpMeta;
}): number {
const runtimeTimeoutSeconds = resolveRuntimeOptionsFromMeta(params.meta).timeoutSeconds;
if (
typeof runtimeTimeoutSeconds === "number" &&
Number.isFinite(runtimeTimeoutSeconds) &&
runtimeTimeoutSeconds > 0
) {
return clampTimerTimeoutMs(Math.round(runtimeTimeoutSeconds * 1_000), 1_000) ?? 1_000;
}
return resolveAgentTimeoutMs({
cfg: params.cfg,
minMs: 1_000,
});
}
export async function awaitTurnWithTimeout<T>(params: {
sessionKey: string;
turnPromise: Promise<T>;
timeoutMs: number;
timeoutLabelMs: number;
onTimeout: () => Promise<void>;
}): Promise<T> {
const observedTurnPromise: Promise<
| {
kind: "value";
value: T;
}
| {
kind: "error";
error: unknown;
}
> = params.turnPromise.then(
(value) => ({
kind: "value" as const,
value,
}),
(error) => ({
kind: "error" as const,
error,
}),
);
if (params.timeoutMs <= 0) {
const outcome = await observedTurnPromise;
if (outcome.kind === "error") {
throw outcome.error;
}
return outcome.value;
}
const timeoutMs = clampTimerTimeoutMs(params.timeoutMs, 1);
if (timeoutMs === undefined) {
const outcome = await observedTurnPromise;
if (outcome.kind === "error") {
throw outcome.error;
}
return outcome.value;
}
const timeoutToken = Symbol("acp-turn-timeout");
let timer: NodeJS.Timeout | undefined;
const timeoutPromise = new Promise<typeof timeoutToken>((resolve) => {
timer = setTimeout(() => resolve(timeoutToken), timeoutMs);
timer.unref?.();
});
try {
const outcome = await Promise.race([observedTurnPromise, timeoutPromise]);
if (outcome === timeoutToken) {
void observedTurnPromise.then((lateOutcome) => {
if (lateOutcome.kind === "error") {
logVerbose(
`acp-manager: detached late turn error after timeout for ${params.sessionKey}: ${String(lateOutcome.error)}`,
);
}
});
await params.onTimeout();
throw new AcpRuntimeError(
"ACP_TURN_FAILED",
`ACP turn timed out after ${Math.max(1, Math.round(params.timeoutLabelMs / 1_000))}s.`,
);
}
if (outcome.kind === "error") {
throw outcome.error;
}
return outcome.value;
} finally {
if (timer) {
clearTimeout(timer);
}
}
}
export async function cleanupTimedOutTurn(params: {
sessionKey: string;
activeTurn: ActiveTurnState;
mode: AcpRuntimeSessionMode;
clearCachedRuntimeStateIfHandleMatches: (activeTurn: ActiveTurnState) => void;
}): Promise<void> {
params.activeTurn.abortController.abort();
if (!params.activeTurn.cancelPromise) {
params.activeTurn.cancelPromise = params.activeTurn.runtime.cancel({
handle: params.activeTurn.handle,
reason: ACP_TURN_TIMEOUT_REASON,
});
}
const cancelFinished = await awaitCleanupWithGrace({
sessionKey: params.sessionKey,
label: "cancel",
promise: params.activeTurn.cancelPromise,
});
if (params.mode !== "oneshot") {
return;
}
const closePromise = params.activeTurn.runtime.close({
handle: params.activeTurn.handle,
reason: ACP_TURN_TIMEOUT_REASON,
});
const closeFinished = await awaitCleanupWithGrace({
sessionKey: params.sessionKey,
label: "close",
promise: closePromise,
});
if (cancelFinished && closeFinished) {
params.clearCachedRuntimeStateIfHandleMatches(params.activeTurn);
return;
}
void Promise.allSettled([params.activeTurn.cancelPromise, closePromise]).then(() => {
params.clearCachedRuntimeStateIfHandleMatches(params.activeTurn);
});
}
async function awaitCleanupWithGrace(params: {
sessionKey: string;
label: "cancel" | "close";
promise: Promise<unknown>;
}): Promise<boolean> {
const observedCleanupPromise: Promise<
| {
kind: "done";
}
| {
kind: "error";
error: unknown;
}
> = params.promise.then(
() => ({
kind: "done" as const,
}),
(error) => ({
kind: "error" as const,
error,
}),
);
const timeoutToken = Symbol(`acp-timeout-${params.label}`);
let timer: NodeJS.Timeout | undefined;
const timeoutPromise = new Promise<typeof timeoutToken>((resolve) => {
timer = setTimeout(() => resolve(timeoutToken), ACP_TURN_TIMEOUT_CLEANUP_GRACE_MS);
timer.unref?.();
});
try {
const outcome = await Promise.race([observedCleanupPromise, timeoutPromise]);
if (outcome === timeoutToken) {
void observedCleanupPromise.then((lateOutcome) => {
if (lateOutcome.kind === "error") {
logVerbose(
`acp-manager: detached timed-out turn ${params.label} cleanup failed for ${params.sessionKey}: ${String(lateOutcome.error)}`,
);
}
});
logVerbose(
`acp-manager: timed-out turn ${params.label} cleanup exceeded ${ACP_TURN_TIMEOUT_CLEANUP_GRACE_MS}ms for ${params.sessionKey}`,
);
return false;
}
if (outcome.kind === "error") {
logVerbose(
`acp-manager: timed-out turn ${params.label} cleanup failed for ${params.sessionKey}: ${String(outcome.error)}`,
);
}
return true;
} finally {
if (timer) {
clearTimeout(timer);
}
}
}

View File

@@ -1,3 +1,4 @@
import { ACP_ERROR_CODES, AcpRuntimeError } from "@openclaw/acp-core/runtime/errors";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import {
canonicalizeMainSessionAlias,
@@ -10,7 +11,6 @@ import {
normalizeMainKey,
parseAgentSessionKey,
} from "../../routing/session-key.js";
import { ACP_ERROR_CODES, AcpRuntimeError } from "../runtime/errors.js";
import type { AcpSessionResolution } from "./manager.types.js";
export function resolveAcpAgentFromSessionKey(sessionKey: string, fallback = "main"): string {

View File

@@ -1,6 +1,6 @@
import { AcpRuntimeError } from "@openclaw/acp-core/runtime/errors";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeAgentId } from "../routing/session-key.js";
import { AcpRuntimeError } from "./runtime/errors.js";
const ACP_DISABLED_MESSAGE = "ACP is disabled by policy (`acp.enabled=false`).";
const ACP_DISPATCH_DISABLED_MESSAGE =

View File

@@ -0,0 +1,266 @@
import type {
InitializeRequest,
SessionConfigOption,
SessionModeState,
} from "@agentclientprotocol/sdk";
import {
toAcpSessionLineageMeta,
type AcpSessionLineageMeta,
} from "@openclaw/acp-core/session-lineage-meta";
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { BASE_THINKING_LEVELS } from "../auto-reply/thinking.shared.js";
import type { GatewaySessionRow } from "../gateway/session-utils.js";
export const ACP_THOUGHT_LEVEL_CONFIG_ID = "thought_level";
export const ACP_FAST_MODE_CONFIG_ID = "fast_mode";
export const ACP_VERBOSE_LEVEL_CONFIG_ID = "verbose_level";
export const ACP_TRACE_LEVEL_CONFIG_ID = "trace_level";
export const ACP_REASONING_LEVEL_CONFIG_ID = "reasoning_level";
export const ACP_RESPONSE_USAGE_CONFIG_ID = "response_usage";
export const ACP_ELEVATED_LEVEL_CONFIG_ID = "elevated_level";
export const ACP_TIMEOUT_CONFIG_ID = "timeout";
export const ACP_TIMEOUT_SECONDS_CONFIG_ID = "timeout_seconds";
export type ClientCapabilityState = {
readTextFile: boolean;
writeTextFile: boolean;
terminal: boolean;
};
export type GatewaySessionPresentationRow = Pick<
GatewaySessionRow,
| "key"
| "kind"
| "channel"
| "parentSessionKey"
| "spawnedBy"
| "spawnDepth"
| "subagentRole"
| "subagentControlScope"
| "spawnedWorkspaceDir"
| "spawnedCwd"
| "displayName"
| "label"
| "derivedTitle"
| "updatedAt"
| "thinkingLevel"
| "fastMode"
| "modelProvider"
| "model"
| "thinkingLevels"
| "verboseLevel"
| "traceLevel"
| "reasoningLevel"
| "responseUsage"
| "elevatedLevel"
| "totalTokens"
| "totalTokensFresh"
| "contextTokens"
>;
export type SessionPresentation = {
configOptions: SessionConfigOption[];
modes: SessionModeState;
};
export type SessionMetadata = {
title?: string | null;
updatedAt?: string | null;
_meta?: AcpSessionLineageMeta;
};
export type SessionUsageSnapshot = {
size: number;
used: number;
};
export type SessionSnapshot = SessionPresentation & {
metadata?: SessionMetadata;
usage?: SessionUsageSnapshot;
};
export function normalizeClientCapabilities(
capabilities: InitializeRequest["clientCapabilities"] | undefined,
): ClientCapabilityState {
return {
readTextFile: capabilities?.fs?.readTextFile === true,
writeTextFile: capabilities?.fs?.writeTextFile === true,
terminal: capabilities?.terminal === true,
};
}
function formatThinkingLevelName(level: string): string {
switch (level) {
case "xhigh":
return "Extra High";
case "adaptive":
return "Adaptive";
default:
return level.length > 0 ? `${level[0].toUpperCase()}${level.slice(1)}` : "Unknown";
}
}
function buildThinkingModeDescription(level: string): string | undefined {
if (level === "adaptive") {
return "Use the Gateway session default thought level.";
}
return undefined;
}
function formatConfigValueName(value: string): string {
switch (value) {
case "xhigh":
return "Extra High";
default:
return value.length > 0 ? `${value[0].toUpperCase()}${value.slice(1)}` : "Unknown";
}
}
function buildSelectConfigOption(params: {
id: string;
name: string;
description: string;
currentValue: string;
values: readonly string[];
category?: string;
}): SessionConfigOption {
return {
type: "select",
id: params.id,
name: params.name,
category: params.category,
description: params.description,
currentValue: params.currentValue,
options: params.values.map((value) => ({
value,
name: formatConfigValueName(value),
})),
};
}
export function buildSessionPresentation(params: {
row?: GatewaySessionPresentationRow;
overrides?: Partial<GatewaySessionPresentationRow>;
}): SessionPresentation {
const row = {
...params.row,
...params.overrides,
};
const availableLevelIds: string[] = row.thinkingLevels?.map((level) => level.id) ?? [
...BASE_THINKING_LEVELS,
];
const currentModeId = normalizeOptionalString(row.thinkingLevel) || "adaptive";
if (!availableLevelIds.includes(currentModeId)) {
availableLevelIds.push(currentModeId);
}
const modes: SessionModeState = {
currentModeId,
availableModes: availableLevelIds.map((level) => ({
id: level,
name: formatThinkingLevelName(level),
description: buildThinkingModeDescription(level),
})),
};
const configOptions: SessionConfigOption[] = [
buildSelectConfigOption({
id: ACP_THOUGHT_LEVEL_CONFIG_ID,
name: "Thought level",
category: "thought_level",
description:
"Controls how much deliberate reasoning OpenClaw requests from the Gateway model.",
currentValue: currentModeId,
values: availableLevelIds,
}),
buildSelectConfigOption({
id: ACP_FAST_MODE_CONFIG_ID,
name: "Fast mode",
description: "Controls whether OpenAI sessions use the Gateway fast-mode profile.",
currentValue: row.fastMode ? "on" : "off",
values: ["off", "on"],
}),
buildSelectConfigOption({
id: ACP_VERBOSE_LEVEL_CONFIG_ID,
name: "Tool verbosity",
description:
"Controls how much tool progress and output detail OpenClaw keeps enabled for the session.",
currentValue: normalizeOptionalString(row.verboseLevel) || "off",
values: ["off", "on", "full"],
}),
buildSelectConfigOption({
id: ACP_TRACE_LEVEL_CONFIG_ID,
name: "Plugin trace",
description: "Controls whether plugin-owned trace lines are shown for the session.",
currentValue: normalizeOptionalString(row.traceLevel) || "off",
values: ["off", "on"],
}),
buildSelectConfigOption({
id: ACP_REASONING_LEVEL_CONFIG_ID,
name: "Reasoning stream",
description: "Controls whether reasoning-capable models emit reasoning text for the session.",
currentValue: normalizeOptionalString(row.reasoningLevel) || "off",
values: ["off", "on", "stream"],
}),
buildSelectConfigOption({
id: ACP_RESPONSE_USAGE_CONFIG_ID,
name: "Usage detail",
description:
"Controls how much usage information OpenClaw attaches to responses for the session.",
currentValue: normalizeOptionalString(row.responseUsage) || "off",
values: ["off", "tokens", "full"],
}),
buildSelectConfigOption({
id: ACP_ELEVATED_LEVEL_CONFIG_ID,
name: "Elevated actions",
description: "Controls how aggressively the session allows elevated execution behavior.",
currentValue: normalizeOptionalString(row.elevatedLevel) || "off",
values: ["off", "on", "ask", "full"],
}),
];
return { configOptions, modes };
}
export function buildSessionMetadata(params: {
row?: GatewaySessionPresentationRow;
sessionKey: string;
}): SessionMetadata {
const title =
normalizeOptionalString(params.row?.derivedTitle) ||
normalizeOptionalString(params.row?.displayName) ||
normalizeOptionalString(params.row?.label) ||
params.sessionKey;
const updatedAt = timestampMsToIsoString(params.row?.updatedAt) ?? null;
return {
title,
updatedAt,
_meta: toAcpSessionLineageMeta(
params.row ?? {
key: params.sessionKey,
kind: "unknown",
},
),
};
}
export function buildSessionUsageSnapshot(
row?: GatewaySessionPresentationRow,
): SessionUsageSnapshot | undefined {
const totalTokens = row?.totalTokens;
const contextTokens = row?.contextTokens;
if (
row?.totalTokensFresh !== true ||
typeof totalTokens !== "number" ||
!Number.isFinite(totalTokens) ||
typeof contextTokens !== "number" ||
!Number.isFinite(contextTokens) ||
contextTokens <= 0
) {
return undefined;
}
const size = Math.max(0, Math.floor(contextTokens));
const used = Math.max(0, Math.min(Math.floor(totalTokens), size));
return { size, used };
}

View File

@@ -21,9 +21,7 @@ import type {
PromptResponse,
ResumeSessionRequest,
ResumeSessionResponse,
SessionConfigOption,
SessionInfo,
SessionModeState,
SessionUpdate,
SetSessionConfigOptionRequest,
SetSessionConfigOptionResponse,
@@ -35,14 +33,10 @@ import type {
} from "@agentclientprotocol/sdk";
import { readBool, readNonNegativeInteger, readNumber, readString } from "@openclaw/acp-core/meta";
import { defaultAcpSessionStore, type AcpSessionStore } from "@openclaw/acp-core/session";
import {
toAcpSessionLineageMeta,
type AcpSessionLineageMeta,
} from "@openclaw/acp-core/session-lineage-meta";
import { toAcpSessionLineageMeta } from "@openclaw/acp-core/session-lineage-meta";
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import type { EventFrame } from "../../packages/gateway-protocol/src/index.js";
import { BASE_THINKING_LEVELS } from "../auto-reply/thinking.shared.js";
import type { GatewayClient } from "../gateway/client.js";
import type { GatewaySessionRow, SessionsListResult } from "../gateway/session-utils.js";
import {
@@ -74,19 +68,28 @@ import {
type GatewayExecApprovalEvent,
} from "./permission-relay.js";
import { parseSessionMeta, resetSessionIfNeeded, resolveSessionKey } from "./session-mapper.js";
import {
ACP_ELEVATED_LEVEL_CONFIG_ID,
ACP_FAST_MODE_CONFIG_ID,
ACP_REASONING_LEVEL_CONFIG_ID,
ACP_RESPONSE_USAGE_CONFIG_ID,
ACP_THOUGHT_LEVEL_CONFIG_ID,
ACP_TIMEOUT_CONFIG_ID,
ACP_TIMEOUT_SECONDS_CONFIG_ID,
ACP_TRACE_LEVEL_CONFIG_ID,
ACP_VERBOSE_LEVEL_CONFIG_ID,
buildSessionMetadata,
buildSessionPresentation,
buildSessionUsageSnapshot,
normalizeClientCapabilities,
type ClientCapabilityState,
type GatewaySessionPresentationRow,
type SessionSnapshot,
} from "./translator.presentation.js";
import { ACP_AGENT_INFO, type AcpServerOptions } from "./types.js";
// Maximum allowed prompt size (2MB) to prevent DoS via memory exhaustion (CWE-400, GHSA-cxpw-2g23-2vgw)
const MAX_PROMPT_BYTES = 2 * 1024 * 1024;
const ACP_THOUGHT_LEVEL_CONFIG_ID = "thought_level";
const ACP_FAST_MODE_CONFIG_ID = "fast_mode";
const ACP_VERBOSE_LEVEL_CONFIG_ID = "verbose_level";
const ACP_TRACE_LEVEL_CONFIG_ID = "trace_level";
const ACP_REASONING_LEVEL_CONFIG_ID = "reasoning_level";
const ACP_RESPONSE_USAGE_CONFIG_ID = "response_usage";
const ACP_ELEVATED_LEVEL_CONFIG_ID = "elevated_level";
const ACP_TIMEOUT_CONFIG_ID = "timeout";
const ACP_TIMEOUT_SECONDS_CONFIG_ID = "timeout_seconds";
const ACP_LOAD_SESSION_REPLAY_LIMIT = 1_000_000;
const ACP_GATEWAY_DISCONNECT_GRACE_MS = 5_000;
const ACP_LIST_SESSIONS_DEFAULT_PAGE_SIZE = 100;
@@ -131,12 +134,6 @@ type PendingPrompt = {
toolCalls?: Map<string, PendingToolCall>;
};
type ClientCapabilityState = {
readTextFile: boolean;
writeTextFile: boolean;
terminal: boolean;
};
type PendingApprovalRelay = {
approvalId: string;
runId: string;
@@ -157,63 +154,6 @@ type AcpGatewayAgentOptions = AcpServerOptions & {
sessionStore?: AcpSessionStore;
};
type GatewaySessionPresentationRow = Pick<
GatewaySessionRow,
| "key"
| "kind"
| "channel"
| "parentSessionKey"
| "spawnedBy"
| "spawnDepth"
| "subagentRole"
| "subagentControlScope"
| "spawnedWorkspaceDir"
| "spawnedCwd"
| "displayName"
| "label"
| "derivedTitle"
| "updatedAt"
| "thinkingLevel"
| "fastMode"
| "modelProvider"
| "model"
| "thinkingLevels"
| "verboseLevel"
| "traceLevel"
| "reasoningLevel"
| "responseUsage"
| "elevatedLevel"
| "totalTokens"
| "totalTokensFresh"
| "contextTokens"
>;
type SessionPresentation = {
configOptions: SessionConfigOption[];
modes: SessionModeState;
};
type SessionMetadata = {
title?: string | null;
updatedAt?: string | null;
_meta?: AcpSessionLineageMeta;
};
type SessionUsageSnapshot = {
size: number;
used: number;
};
function normalizeClientCapabilities(
capabilities: InitializeRequest["clientCapabilities"] | undefined,
): ClientCapabilityState {
return {
readTextFile: capabilities?.fs?.readTextFile === true,
writeTextFile: capabilities?.fs?.writeTextFile === true,
terminal: capabilities?.terminal === true,
};
}
function isAdminScopeProvenanceRejection(err: unknown): boolean {
if (!(err instanceof Error)) {
return false;
@@ -233,11 +173,6 @@ function isGatewayCloseError(err: unknown): boolean {
return err instanceof Error && err.message.startsWith("gateway closed (");
}
type SessionSnapshot = SessionPresentation & {
metadata?: SessionMetadata;
usage?: SessionUsageSnapshot;
};
type AgentWaitResult = {
status?: "ok" | "error" | "timeout";
error?: string;
@@ -317,139 +252,6 @@ function resolveListSessionsPageSize(meta: Record<string, unknown> | null | unde
const SESSION_CREATE_RATE_LIMIT_DEFAULT_MAX_REQUESTS = 120;
const SESSION_CREATE_RATE_LIMIT_DEFAULT_WINDOW_MS = 10_000;
function formatThinkingLevelName(level: string): string {
switch (level) {
case "xhigh":
return "Extra High";
case "adaptive":
return "Adaptive";
default:
return level.length > 0 ? `${level[0].toUpperCase()}${level.slice(1)}` : "Unknown";
}
}
function buildThinkingModeDescription(level: string): string | undefined {
if (level === "adaptive") {
return "Use the Gateway session default thought level.";
}
return undefined;
}
function formatConfigValueName(value: string): string {
switch (value) {
case "xhigh":
return "Extra High";
default:
return value.length > 0 ? `${value[0].toUpperCase()}${value.slice(1)}` : "Unknown";
}
}
function buildSelectConfigOption(params: {
id: string;
name: string;
description: string;
currentValue: string;
values: readonly string[];
category?: string;
}): SessionConfigOption {
return {
type: "select",
id: params.id,
name: params.name,
category: params.category,
description: params.description,
currentValue: params.currentValue,
options: params.values.map((value) => ({
value,
name: formatConfigValueName(value),
})),
};
}
function buildSessionPresentation(params: {
row?: GatewaySessionPresentationRow;
overrides?: Partial<GatewaySessionPresentationRow>;
}): SessionPresentation {
const row = {
...params.row,
...params.overrides,
};
const availableLevelIds: string[] = row.thinkingLevels?.map((level) => level.id) ?? [
...BASE_THINKING_LEVELS,
];
const currentModeId = normalizeOptionalString(row.thinkingLevel) || "adaptive";
if (!availableLevelIds.includes(currentModeId)) {
availableLevelIds.push(currentModeId);
}
const modes: SessionModeState = {
currentModeId,
availableModes: availableLevelIds.map((level) => ({
id: level,
name: formatThinkingLevelName(level),
description: buildThinkingModeDescription(level),
})),
};
const configOptions: SessionConfigOption[] = [
buildSelectConfigOption({
id: ACP_THOUGHT_LEVEL_CONFIG_ID,
name: "Thought level",
category: "thought_level",
description:
"Controls how much deliberate reasoning OpenClaw requests from the Gateway model.",
currentValue: currentModeId,
values: availableLevelIds,
}),
buildSelectConfigOption({
id: ACP_FAST_MODE_CONFIG_ID,
name: "Fast mode",
description: "Controls whether OpenAI sessions use the Gateway fast-mode profile.",
currentValue: row.fastMode ? "on" : "off",
values: ["off", "on"],
}),
buildSelectConfigOption({
id: ACP_VERBOSE_LEVEL_CONFIG_ID,
name: "Tool verbosity",
description:
"Controls how much tool progress and output detail OpenClaw keeps enabled for the session.",
currentValue: normalizeOptionalString(row.verboseLevel) || "off",
values: ["off", "on", "full"],
}),
buildSelectConfigOption({
id: ACP_TRACE_LEVEL_CONFIG_ID,
name: "Plugin trace",
description: "Controls whether plugin-owned trace lines are shown for the session.",
currentValue: normalizeOptionalString(row.traceLevel) || "off",
values: ["off", "on"],
}),
buildSelectConfigOption({
id: ACP_REASONING_LEVEL_CONFIG_ID,
name: "Reasoning stream",
description: "Controls whether reasoning-capable models emit reasoning text for the session.",
currentValue: normalizeOptionalString(row.reasoningLevel) || "off",
values: ["off", "on", "stream"],
}),
buildSelectConfigOption({
id: ACP_RESPONSE_USAGE_CONFIG_ID,
name: "Usage detail",
description:
"Controls how much usage information OpenClaw attaches to responses for the session.",
currentValue: normalizeOptionalString(row.responseUsage) || "off",
values: ["off", "tokens", "full"],
}),
buildSelectConfigOption({
id: ACP_ELEVATED_LEVEL_CONFIG_ID,
name: "Elevated actions",
description: "Controls how aggressively the session allows elevated execution behavior.",
currentValue: normalizeOptionalString(row.elevatedLevel) || "off",
values: ["off", "on", "ask", "full"],
}),
];
return { configOptions, modes };
}
function extractReplayChunks(message: GatewayTranscriptMessage): ReplayChunk[] {
const role = typeof message.role === "string" ? message.role : "";
if (role !== "user" && role !== "assistant") {
@@ -497,48 +299,6 @@ function extractReplayChunks(message: GatewayTranscriptMessage): ReplayChunk[] {
return replayChunks;
}
function buildSessionMetadata(params: {
row?: GatewaySessionPresentationRow;
sessionKey: string;
}): SessionMetadata {
const title =
normalizeOptionalString(params.row?.derivedTitle) ||
normalizeOptionalString(params.row?.displayName) ||
normalizeOptionalString(params.row?.label) ||
params.sessionKey;
const updatedAt = timestampMsToIsoString(params.row?.updatedAt) ?? null;
return {
title,
updatedAt,
_meta: toAcpSessionLineageMeta(
params.row ?? {
key: params.sessionKey,
kind: "unknown",
},
),
};
}
function buildSessionUsageSnapshot(
row?: GatewaySessionPresentationRow,
): SessionUsageSnapshot | undefined {
const totalTokens = row?.totalTokens;
const contextTokens = row?.contextTokens;
if (
row?.totalTokensFresh !== true ||
typeof totalTokens !== "number" ||
!Number.isFinite(totalTokens) ||
typeof contextTokens !== "number" ||
!Number.isFinite(contextTokens) ||
contextTokens <= 0
) {
return undefined;
}
const size = Math.max(0, Math.floor(contextTokens));
const used = Math.max(0, Math.min(Math.floor(totalTokens), size));
return { size, used };
}
function buildSystemInputProvenance(originSessionId: string) {
return {
kind: "external_user" as const,

View File

@@ -2,7 +2,11 @@ import fs from "node:fs";
import Module from "node:module";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { buildPluginLoaderAliasMap, type PluginSdkResolutionPreference } from "./sdk-alias.js";
import {
buildPluginLoaderAliasMap,
listWorkspacePackageExportAliasEntries,
type PluginSdkResolutionPreference,
} from "./sdk-alias.js";
type ResolveFilename = (
request: string,
@@ -74,26 +78,6 @@ const INTERNAL_CORE_PACKAGE_ALIASES = [
["read-response-with-limit", "read-response-with-limit.ts"],
],
},
{
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
subpaths: [
["", "index.ts"],
["meta", "meta.ts"],
["normalize-text", "normalize-text.ts"],
["numeric-options", "numeric-options.ts"],
["record-shared", "record-shared.ts"],
["session", "session.ts"],
["session-interaction-mode", "session-interaction-mode.ts"],
["session-lineage-meta", "session-lineage-meta.ts"],
["types", "types.ts"],
["runtime/error-text", path.join("runtime", "error-text.ts")],
["runtime/errors", path.join("runtime", "errors.ts")],
["runtime/session-identifiers", path.join("runtime", "session-identifiers.ts")],
["runtime/session-identity", path.join("runtime", "session-identity.ts")],
["runtime/types", path.join("runtime", "types.ts")],
],
},
{
packageName: "@openclaw/llm-core",
packageDir: "llm-core",
@@ -306,7 +290,19 @@ function listInternalCorePackageNativeAliases(
target: string;
parentRoots: string[];
}> = [];
for (const entry of INTERNAL_CORE_PACKAGE_ALIASES) {
const internalCorePackageAliases = [
...INTERNAL_CORE_PACKAGE_ALIASES,
{
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
subpaths: listWorkspacePackageExportAliasEntries({
packageRoot,
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
}).map((entry) => [entry.subpath, entry.srcFile] as const),
},
];
for (const entry of internalCorePackageAliases) {
for (const [subpath, srcFile] of entry.subpaths) {
const request = subpath ? `${entry.packageName}/${subpath}` : entry.packageName;
const target = path.join(packageRoot, "packages", entry.packageDir, "src", srcFile);

View File

@@ -1752,6 +1752,28 @@ describe("plugin sdk alias helpers", () => {
).toBe(fs.realpathSync(modelCatalogCore.distFile));
});
it("derives acp-core aliases from packaged root dist when package metadata is absent", () => {
const fixture = createPluginSdkAliasFixture();
const sourcePluginEntry = writePluginEntry(
fixture.root,
bundledPluginFile("demo", "src/index.ts"),
);
const acpRuntimeErrors = path.join(fixture.root, "dist", "acp-core", "runtime", "errors.js");
mkdirSafeDir(path.dirname(acpRuntimeErrors));
fs.writeFileSync(acpRuntimeErrors, "export {};\n", "utf-8");
const cwdWithoutOpenClawPackage = makeTempDir();
const aliases = withCwd(cwdWithoutOpenClawPackage, () =>
withEnv({ NODE_ENV: undefined }, () =>
buildPluginLoaderAliasMap(sourcePluginEntry, undefined, undefined, "dist"),
),
);
expect(fs.realpathSync(aliases["@openclaw/acp-core/runtime/errors"] ?? "")).toBe(
fs.realpathSync(acpRuntimeErrors),
);
});
it("aliases bundled plugin package public surfaces for source plugin transforms", () => {
const { fixture, sourceApiPath, sourceRuntimeApiPath } =
createBundledPluginPackagePublicSurfaceAliasFixture();

View File

@@ -34,6 +34,14 @@ type PluginSdkPackageJson = {
version?: string;
};
type WorkspacePackageAliasEntry = {
packageName: string;
packageDir: string;
subpath: string;
srcFile: string;
distFile: string;
};
const STARTUP_ARGV1 = process.argv[1];
const pluginSdkPackageJsonByRoot = new Map<string, PluginSdkPackageJson | null>();
@@ -508,7 +516,7 @@ const JS_STATIC_RELATIVE_DEPENDENCY_PATTERN =
// Jiti-loaded plugin code runs outside the Vitest/tsgo resolver, so every
// workspace package import reachable from plugin SDK barrels needs an explicit
// source/dist alias here to keep source checkouts and packaged builds aligned.
const WORKSPACE_PACKAGE_ALIAS_ENTRIES = [
const WORKSPACE_PACKAGE_ALIAS_ENTRIES: WorkspacePackageAliasEntry[] = [
{
packageName: "@openclaw/gateway-client",
packageDir: "gateway-client",
@@ -747,104 +755,6 @@ const WORKSPACE_PACKAGE_ALIAS_ENTRIES = [
srcFile: "read-response-with-limit.ts",
distFile: "read-response-with-limit.mjs",
},
{
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
subpath: "",
srcFile: "index.ts",
distFile: "index.mjs",
},
{
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
subpath: "meta",
srcFile: "meta.ts",
distFile: "meta.mjs",
},
{
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
subpath: "normalize-text",
srcFile: "normalize-text.ts",
distFile: "normalize-text.mjs",
},
{
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
subpath: "numeric-options",
srcFile: "numeric-options.ts",
distFile: "numeric-options.mjs",
},
{
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
subpath: "record-shared",
srcFile: "record-shared.ts",
distFile: "record-shared.mjs",
},
{
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
subpath: "session",
srcFile: "session.ts",
distFile: "session.mjs",
},
{
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
subpath: "session-interaction-mode",
srcFile: "session-interaction-mode.ts",
distFile: "session-interaction-mode.mjs",
},
{
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
subpath: "session-lineage-meta",
srcFile: "session-lineage-meta.ts",
distFile: "session-lineage-meta.mjs",
},
{
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
subpath: "types",
srcFile: "types.ts",
distFile: "types.mjs",
},
{
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
subpath: "runtime/error-text",
srcFile: path.join("runtime", "error-text.ts"),
distFile: path.join("runtime", "error-text.mjs"),
},
{
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
subpath: "runtime/errors",
srcFile: path.join("runtime", "errors.ts"),
distFile: path.join("runtime", "errors.mjs"),
},
{
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
subpath: "runtime/session-identifiers",
srcFile: path.join("runtime", "session-identifiers.ts"),
distFile: path.join("runtime", "session-identifiers.mjs"),
},
{
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
subpath: "runtime/session-identity",
srcFile: path.join("runtime", "session-identity.ts"),
distFile: path.join("runtime", "session-identity.mjs"),
},
{
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
subpath: "runtime/types",
srcFile: path.join("runtime", "types.ts"),
distFile: path.join("runtime", "types.mjs"),
},
{
packageName: "@openclaw/normalization-core",
packageDir: "normalization-core",
@@ -1105,6 +1015,117 @@ const ROOT_PACKAGED_WORKSPACE_PACKAGE_DIRS = new Set([
"terminal-core",
]);
function normalizePackageExportSubpath(exportKey: string): string | null {
if (exportKey === ".") {
return "";
}
if (!exportKey.startsWith("./")) {
return null;
}
const subpath = exportKey.slice(2);
return subpath && !subpath.includes("..") ? subpath : null;
}
function resolvePackageExportImportPath(value: unknown): string | null {
if (typeof value === "string") {
return value;
}
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
const record = value as Record<string, unknown>;
return typeof record.import === "string"
? record.import
: typeof record.default === "string"
? record.default
: null;
}
function listRootPackagedWorkspacePackageAliasEntries(params: {
packageRoot: string;
packageName: string;
packageDir: string;
}): WorkspacePackageAliasEntry[] {
const distRoot = path.join(params.packageRoot, "dist", params.packageDir);
if (!fs.existsSync(distRoot)) {
return [];
}
const entries: WorkspacePackageAliasEntry[] = [];
const visit = (dir: string, prefix = "") => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const relativePath = prefix ? path.join(prefix, entry.name) : entry.name;
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
visit(fullPath, relativePath);
continue;
}
if (!entry.isFile() || !relativePath.endsWith(".js")) {
continue;
}
const normalizedRelativePath = relativePath.split(path.sep).join("/");
const subpath =
normalizedRelativePath === "index.js" ? "" : normalizedRelativePath.slice(0, -".js".length);
if (subpath.includes("..")) {
continue;
}
entries.push({
packageName: params.packageName,
packageDir: params.packageDir,
subpath,
srcFile: `${subpath || "index"}.ts`,
distFile: relativePath,
});
}
};
visit(distRoot);
return entries.toSorted((a, b) => a.subpath.localeCompare(b.subpath));
}
export function listWorkspacePackageExportAliasEntries(params: {
packageRoot: string;
packageName: string;
packageDir: string;
}): WorkspacePackageAliasEntry[] {
const packageJsonPath = path.join(
params.packageRoot,
"packages",
params.packageDir,
"package.json",
);
const fallbackPackageRoot = resolveOpenClawPackageRootSync({ cwd: process.cwd() });
const packageJson =
tryReadJsonSync<PluginSdkPackageJson>(packageJsonPath) ??
(fallbackPackageRoot
? tryReadJsonSync<PluginSdkPackageJson>(
path.join(fallbackPackageRoot, "packages", params.packageDir, "package.json"),
)
: null);
const exports = packageJson?.exports;
if (!exports || typeof exports !== "object" || Array.isArray(exports)) {
return listRootPackagedWorkspacePackageAliasEntries(params);
}
const entries: WorkspacePackageAliasEntry[] = [];
for (const [exportKey, value] of Object.entries(exports)) {
const subpath = normalizePackageExportSubpath(exportKey);
const importPath = resolvePackageExportImportPath(value);
if (subpath === null || !importPath?.startsWith("./dist/") || !importPath.endsWith(".mjs")) {
continue;
}
const distFile = importPath.slice("./dist/".length);
const srcFile = distFile.replace(/\.mjs$/u, ".ts");
entries.push({
packageName: params.packageName,
packageDir: params.packageDir,
subpath,
srcFile,
distFile,
});
}
return entries.length > 0
? entries.toSorted((a, b) => a.subpath.localeCompare(b.subpath))
: listRootPackagedWorkspacePackageAliasEntries(params);
}
function isUsableDistPluginSdkArtifact(candidate: string): boolean {
if (!fs.existsSync(candidate)) {
return false;
@@ -1308,7 +1329,15 @@ function resolveWorkspacePackageAliasMap(params: {
pluginSdkResolution: params.pluginSdkResolution,
});
const aliasMap: Record<string, string> = {};
for (const entry of WORKSPACE_PACKAGE_ALIAS_ENTRIES) {
const workspacePackageAliasEntries = [
...WORKSPACE_PACKAGE_ALIAS_ENTRIES,
...listWorkspacePackageExportAliasEntries({
packageRoot,
packageName: "@openclaw/acp-core",
packageDir: "acp-core",
}),
];
for (const entry of workspacePackageAliasEntries) {
const alias = entry.subpath ? `${entry.packageName}/${entry.subpath}` : entry.packageName;
for (const kind of orderedKinds) {
const candidates =

View File

@@ -505,6 +505,10 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => {
"agentic-control-plane-http-models",
"agentic-control-plane-http-plugin-ws",
"agentic-control-plane-runtime",
"agentic-control-plane-runtime-cron",
"agentic-control-plane-runtime-network",
"agentic-control-plane-runtime-server",
"agentic-control-plane-runtime-state",
"agentic-control-plane-startup-runtime",
]);
expect(controlPlaneShards).toEqual(

View File

@@ -1,5 +1,6 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import acpCorePackageJson from "../../packages/acp-core/package.json" with { type: "json" };
import { pluginSdkSubpaths } from "../../scripts/lib/plugin-sdk-entries.mjs";
import privateLocalOnlyPluginSdkSubpaths from "../../scripts/lib/plugin-sdk-private-local-only-subpaths.json" with { type: "json" };
import {
@@ -97,6 +98,14 @@ function sourcePackageAlias(packageId: string, subpath?: string) {
};
}
function sourcePackageAliasesFromExports(packageId: string, exports: Record<string, unknown>) {
return Object.keys(exports)
.map((exportKey) => (exportKey === "." ? undefined : exportKey.slice(2)))
.filter((subpath) => subpath === undefined || (subpath && !subpath.includes("..")))
.toSorted((a, b) => (a ?? "").localeCompare(b ?? ""))
.map((subpath) => sourcePackageAlias(packageId, subpath));
}
export function resolveSharedVitestWorkerConfig(params: {
env?: Record<string, string | undefined>;
isCI?: boolean;
@@ -401,20 +410,7 @@ export const sharedVitestConfig = {
sourcePackageAlias("media-core", "read-byte-stream-with-limit"),
sourcePackageAlias("media-core", "read-response-with-limit"),
sourcePackageAlias("media-core"),
sourcePackageAlias("acp-core", "meta"),
sourcePackageAlias("acp-core", "normalize-text"),
sourcePackageAlias("acp-core", "numeric-options"),
sourcePackageAlias("acp-core", "record-shared"),
sourcePackageAlias("acp-core", "session"),
sourcePackageAlias("acp-core", "session-interaction-mode"),
sourcePackageAlias("acp-core", "session-lineage-meta"),
sourcePackageAlias("acp-core", "types"),
sourcePackageAlias("acp-core", "runtime/error-text"),
sourcePackageAlias("acp-core", "runtime/errors"),
sourcePackageAlias("acp-core", "runtime/session-identifiers"),
sourcePackageAlias("acp-core", "runtime/session-identity"),
sourcePackageAlias("acp-core", "runtime/types"),
sourcePackageAlias("acp-core"),
...sourcePackageAliasesFromExports("acp-core", acpCorePackageJson.exports),
...sourcePluginSdkSubpaths.map((subpath) => ({
find: `openclaw/plugin-sdk/${subpath}`,
replacement: path.join(repoRoot, "src", "plugin-sdk", `${subpath}.ts`),

View File

@@ -457,24 +457,35 @@ function buildMediaCoreDistEntries(): Record<string, string> {
};
}
function buildAcpCoreDistEntries(): Record<string, string> {
return {
"error-format": "packages/acp-core/src/error-format.ts",
index: "packages/acp-core/src/index.ts",
meta: "packages/acp-core/src/meta.ts",
"normalize-text": "packages/acp-core/src/normalize-text.ts",
"numeric-options": "packages/acp-core/src/numeric-options.ts",
"record-shared": "packages/acp-core/src/record-shared.ts",
session: "packages/acp-core/src/session.ts",
"session-interaction-mode": "packages/acp-core/src/session-interaction-mode.ts",
"session-lineage-meta": "packages/acp-core/src/session-lineage-meta.ts",
types: "packages/acp-core/src/types.ts",
"runtime/error-text": "packages/acp-core/src/runtime/error-text.ts",
"runtime/errors": "packages/acp-core/src/runtime/errors.ts",
"runtime/session-identifiers": "packages/acp-core/src/runtime/session-identifiers.ts",
"runtime/session-identity": "packages/acp-core/src/runtime/session-identity.ts",
"runtime/types": "packages/acp-core/src/runtime/types.ts",
function buildPackageDistEntriesFromExports(packageDir: string): Record<string, string> {
const packageJsonPath = path.join("packages", packageDir, "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
exports?: Record<string, unknown>;
};
const entries: Record<string, string> = {};
for (const [exportKey, value] of Object.entries(packageJson.exports ?? {})) {
const entry =
exportKey === "." ? "index" : exportKey.startsWith("./") ? exportKey.slice(2) : "";
if (!entry || entry.includes("..")) {
continue;
}
const importPath =
typeof value === "object" && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>).import
: value;
if (typeof importPath !== "string" || !importPath.startsWith("./dist/")) {
continue;
}
const sourcePath = importPath
.replace(/^\.\/dist\//u, `packages/${packageDir}/src/`)
.replace(/\.mjs$/u, ".ts");
entries[entry] = sourcePath;
}
return Object.fromEntries(Object.entries(entries).toSorted(([a], [b]) => a.localeCompare(b)));
}
function buildAcpCoreDistEntries(): Record<string, string> {
return buildPackageDistEntriesFromExports("acp-core");
}
function buildTerminalCoreDistEntries(): Record<string, string> {