mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
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:
committed by
GitHub
parent
a5d8f09fd4
commit
7b78941ea5
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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",
|
||||
|
||||
206
src/acp/control-plane/manager.background-task.ts
Normal file
206
src/acp/control-plane/manager.background-task.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
@@ -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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
203
src/acp/control-plane/manager.turn-timeout.ts
Normal file
203
src/acp/control-plane/manager.turn-timeout.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
266
src/acp/translator.presentation.ts
Normal file
266
src/acp/translator.presentation.ts
Normal 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 };
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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`),
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user