From 7dea2837565edb70b0212cadc5f5b1b09c9b32df Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 31 May 2026 14:48:57 +0100 Subject: [PATCH] refactor: expand acp core package (#88618) * refactor: expand acp core package * chore: drop acp core package symlink * fix: keep acp core dependency graph stable * fix: add acp core tsconfig subpaths * fix: sync acp core boundary path artifacts * fix: use kysely for cron run-log queries * fix: resolve acp core subpaths in loaders --- .../tsconfig.package-boundary.paths.json | 3 + extensions/xai/tsconfig.json | 3 + packages/acp-core/dist/error-format.d.mts | 10 ++ packages/acp-core/dist/error-format.mjs | 64 +++++++ packages/acp-core/dist/index.d.mts | 15 ++ packages/acp-core/dist/index.mjs | 15 ++ packages/acp-core/dist/meta.d.mts | 7 + packages/acp-core/dist/meta.mjs | 23 +++ packages/acp-core/dist/normalize-text.d.mts | 2 + packages/acp-core/dist/normalize-text.mjs | 2 + packages/acp-core/dist/numeric-options.d.mts | 6 + packages/acp-core/dist/numeric-options.mjs | 7 + packages/acp-core/dist/record-shared.d.mts | 2 + packages/acp-core/dist/record-shared.mjs | 2 + .../acp-core/dist/runtime/error-text.d.mts | 11 ++ packages/acp-core/dist/runtime/error-text.mjs | 24 +++ packages/acp-core/dist/runtime/errors.d.mts | 33 ++++ packages/acp-core/dist/runtime/errors.mjs | 97 +++++++++++ .../dist/runtime/session-identifiers.d.mts | 21 +++ .../dist/runtime/session-identifiers.mjs | 72 ++++++++ .../dist/runtime/session-identity.d.mts | 32 ++++ .../dist/runtime/session-identity.mjs | 139 +++++++++++++++ packages/acp-core/dist/runtime/types.d.mts | 162 ++++++++++++++++++ packages/acp-core/dist/runtime/types.mjs | 1 + .../dist/session-interaction-mode.d.mts | 21 +++ .../dist/session-interaction-mode.mjs | 31 ++++ .../acp-core/dist/session-lineage-meta.d.mts | 32 ++++ .../acp-core/dist/session-lineage-meta.mjs | 38 ++++ packages/acp-core/dist/session.d.mts | 28 +++ packages/acp-core/dist/session.mjs | 128 ++++++++++++++ packages/acp-core/dist/types.d.mts | 67 ++++++++ packages/acp-core/dist/types.mjs | 14 ++ packages/acp-core/package.json | 52 +++++- packages/acp-core/src/error-format.ts | 78 +++++++++ packages/acp-core/src/index.ts | 11 ++ .../acp-core/src}/meta.test.ts | 0 {src/acp => packages/acp-core/src}/meta.ts | 0 .../acp-core/src}/numeric-options.ts | 0 .../acp-core/src}/runtime/error-text.test.ts | 0 .../acp-core/src}/runtime/error-text.ts | 0 .../acp-core/src}/runtime/errors.test.ts | 35 +++- packages/acp-core/src/runtime/errors.ts | 152 ++++++++++++++++ .../src}/runtime/session-identifiers.test.ts | 0 .../src}/runtime/session-identifiers.ts | 4 +- .../acp-core/src}/runtime/session-identity.ts | 10 +- .../src}/session-interaction-mode.test.ts | 0 .../acp-core/src}/session-interaction-mode.ts | 7 +- .../src}/session-lineage-meta.test.ts | 0 .../acp-core/src}/session-lineage-meta.ts | 26 ++- .../acp-core/src}/session.test.ts | 0 {src/acp => packages/acp-core/src}/session.ts | 0 packages/acp-core/src/types.ts | 92 ++++++++++ scripts/lib/extension-package-boundary.ts | 3 + ...e-extension-package-boundary-artifacts.mjs | 12 +- src/acp/control-plane/manager.core.ts | 20 +-- .../manager.identity-reconcile.ts | 16 +- src/acp/event-ledger.ts | 2 +- src/acp/runtime/errors.ts | 153 +---------------- src/acp/session-mapper.ts | 4 +- src/acp/translator.cancel-scoping.test.ts | 2 +- src/acp/translator.event-ledger.test.ts | 2 +- src/acp/translator.lifecycle.test.ts | 2 +- src/acp/translator.permission-relay.test.ts | 2 +- .../translator.prompt-harness.test-support.ts | 2 +- src/acp/translator.prompt-prefix.test.ts | 2 +- .../translator.session-lineage-meta.test.ts | 2 +- src/acp/translator.session-rate-limit.test.ts | 2 +- src/acp/translator.set-session-mode.test.ts | 2 +- src/acp/translator.stop-reason.test.ts | 2 +- src/acp/translator.ts | 9 +- src/acp/types.ts | 48 +----- src/agents/acp-spawn.ts | 8 +- .../agent-command.live-model-switch.test.ts | 2 +- src/agents/agent-command.ts | 4 +- src/agents/tools/sessions-send-tool.ts | 2 +- src/auto-reply/reply/commands-acp.test.ts | 5 +- .../reply/commands-acp/diagnostics.ts | 2 +- .../reply/commands-acp/lifecycle.ts | 8 +- .../reply/commands-acp/runtime-options.ts | 2 +- src/auto-reply/reply/commands-acp/shared.ts | 2 +- .../reply/commands-subagents-focus.test.ts | 2 +- .../reply/commands-subagents/action-focus.ts | 4 +- .../reply/dispatch-acp-transcript.runtime.ts | 2 +- src/auto-reply/reply/dispatch-acp.ts | 12 +- src/auto-reply/reply/dispatch-from-config.ts | 9 +- src/config/sessions/types.ts | 56 ++---- src/cron/run-log/sqlite-store.ts | 125 ++++++++------ src/gateway/server-startup-post-attach.ts | 2 +- src/plugins/plugin-sdk-native-resolver.ts | 10 ++ src/plugins/sdk-alias.ts | 70 ++++++++ test/vitest/vitest.shared.config.ts | 10 ++ tsconfig.json | 20 +++ tsdown.config.ts | 11 ++ 93 files changed, 1848 insertions(+), 386 deletions(-) create mode 100644 packages/acp-core/dist/error-format.d.mts create mode 100644 packages/acp-core/dist/error-format.mjs create mode 100644 packages/acp-core/dist/index.d.mts create mode 100644 packages/acp-core/dist/index.mjs create mode 100644 packages/acp-core/dist/meta.d.mts create mode 100644 packages/acp-core/dist/meta.mjs create mode 100644 packages/acp-core/dist/normalize-text.d.mts create mode 100644 packages/acp-core/dist/normalize-text.mjs create mode 100644 packages/acp-core/dist/numeric-options.d.mts create mode 100644 packages/acp-core/dist/numeric-options.mjs create mode 100644 packages/acp-core/dist/record-shared.d.mts create mode 100644 packages/acp-core/dist/record-shared.mjs create mode 100644 packages/acp-core/dist/runtime/error-text.d.mts create mode 100644 packages/acp-core/dist/runtime/error-text.mjs create mode 100644 packages/acp-core/dist/runtime/errors.d.mts create mode 100644 packages/acp-core/dist/runtime/errors.mjs create mode 100644 packages/acp-core/dist/runtime/session-identifiers.d.mts create mode 100644 packages/acp-core/dist/runtime/session-identifiers.mjs create mode 100644 packages/acp-core/dist/runtime/session-identity.d.mts create mode 100644 packages/acp-core/dist/runtime/session-identity.mjs create mode 100644 packages/acp-core/dist/runtime/types.d.mts create mode 100644 packages/acp-core/dist/runtime/types.mjs create mode 100644 packages/acp-core/dist/session-interaction-mode.d.mts create mode 100644 packages/acp-core/dist/session-interaction-mode.mjs create mode 100644 packages/acp-core/dist/session-lineage-meta.d.mts create mode 100644 packages/acp-core/dist/session-lineage-meta.mjs create mode 100644 packages/acp-core/dist/session.d.mts create mode 100644 packages/acp-core/dist/session.mjs create mode 100644 packages/acp-core/dist/types.d.mts create mode 100644 packages/acp-core/dist/types.mjs create mode 100644 packages/acp-core/src/error-format.ts rename {src/acp => packages/acp-core/src}/meta.test.ts (100%) rename {src/acp => packages/acp-core/src}/meta.ts (100%) rename {src/acp => packages/acp-core/src}/numeric-options.ts (100%) rename {src/acp => packages/acp-core/src}/runtime/error-text.test.ts (100%) rename {src/acp => packages/acp-core/src}/runtime/error-text.ts (100%) rename {src/acp => packages/acp-core/src}/runtime/errors.test.ts (78%) create mode 100644 packages/acp-core/src/runtime/errors.ts rename {src/acp => packages/acp-core/src}/runtime/session-identifiers.test.ts (100%) rename {src/acp => packages/acp-core/src}/runtime/session-identifiers.ts (96%) rename {src/acp => packages/acp-core/src}/runtime/session-identity.ts (96%) rename {src/acp => packages/acp-core/src}/session-interaction-mode.test.ts (100%) rename {src/acp => packages/acp-core/src}/session-interaction-mode.ts (92%) rename {src/acp => packages/acp-core/src}/session-lineage-meta.test.ts (100%) rename {src/acp => packages/acp-core/src}/session-lineage-meta.ts (87%) rename {src/acp => packages/acp-core/src}/session.test.ts (100%) rename {src/acp => packages/acp-core/src}/session.ts (100%) create mode 100644 packages/acp-core/src/types.ts diff --git a/extensions/tsconfig.package-boundary.paths.json b/extensions/tsconfig.package-boundary.paths.json index 9b45504b68bf..b36bc1c3f84e 100644 --- a/extensions/tsconfig.package-boundary.paths.json +++ b/extensions/tsconfig.package-boundary.paths.json @@ -257,6 +257,9 @@ "@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" ], diff --git a/extensions/xai/tsconfig.json b/extensions/xai/tsconfig.json index eb2c1c4ce4d7..5504e4f19f5d 100644 --- a/extensions/xai/tsconfig.json +++ b/extensions/xai/tsconfig.json @@ -243,6 +243,9 @@ "@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" ], diff --git a/packages/acp-core/dist/error-format.d.mts b/packages/acp-core/dist/error-format.d.mts new file mode 100644 index 000000000000..f0da0753ddf8 --- /dev/null +++ b/packages/acp-core/dist/error-format.d.mts @@ -0,0 +1,10 @@ +//#region src/error-format.d.ts +declare function configureAcpErrorRedactor(redactor: ((value: string) => string) | undefined): void; +declare function redactSensitiveText(value: string): string; +/** + * Render a non-Error `cause` value without leaking `[object Object]` or throwing + * while formatting nested ACP runtime failures. + */ +declare function stringifyNonErrorCause(value: unknown): string; +//#endregion +export { configureAcpErrorRedactor, redactSensitiveText, stringifyNonErrorCause }; \ No newline at end of file diff --git a/packages/acp-core/dist/error-format.mjs b/packages/acp-core/dist/error-format.mjs new file mode 100644 index 000000000000..5ba2458e2760 --- /dev/null +++ b/packages/acp-core/dist/error-format.mjs @@ -0,0 +1,64 @@ +//#region src/error-format.ts +const SECRET_PATTERNS = [ + /\b[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CARD[_-]?NUMBER|CARD[_-]?CVC|CARD[_-]?CVV|CVC|CVV|SECURITY[_-]?CODE|PAYMENT[_-]?CREDENTIAL|SHARED[_-]?PAYMENT[_-]?TOKEN)\b\s*[=:]\s*(["']?)([^\s"'\\]+)\1/g, + /\b[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CARD[_-]?NUMBER|CARD[_-]?CVC|CARD[_-]?CVV|CVC|CVV|SECURITY[_-]?CODE|PAYMENT[_-]?CREDENTIAL|SHARED[_-]?PAYMENT[_-]?TOKEN)\b\s*[=:]\s*\\+(["'])([^\s"'\\]+)\\+\1/g, + /[?&](?:access[-_]?token|auth[-_]?token|hook[-_]?token|refresh[-_]?token|api[-_]?key|client[-_]?secret|token|key|secret|password|pass|passwd|auth|signature|card[-_]?number|card[-_]?cvc|card[-_]?cvv|cvc|cvv|security[-_]?code|payment[-_]?credential|shared[-_]?payment[-_]?token)=([^&\s"'<>]+)/gi, + /"(?:apiKey|token|secret|password|passwd|accessToken|refreshToken|cardNumber|card_number|cardCvc|card_cvc|cardCvv|card_cvv|cvc|cvv|securityCode|security_code|paymentCredential|payment_credential|sharedPaymentToken|shared_payment_token)"\s*:\s*"([^"]+)"/g, + /(^|[\s,{])["']?(?:api[-_]key|access[-_]token|refresh[-_]token|authToken|auth[-_]token|clientSecret|client[-_]secret|appSecret|app[-_]secret)["']?\s*[:=]\s*(["'])([^"'\r\n]+)\2/gi, + /(^|[\s,{])["']?(?:authorization|proxy-authorization|cookie|set-cookie|x-api-key|x-auth-token)["']?\s*[:=]\s*(["'])([^"'\r\n]+)\2/gi, + /--(?:api[-_]?key|hook[-_]?token|token|secret|password|passwd|card[-_]?number|card[-_]?cvc|card[-_]?cvv|cvc|cvv|security[-_]?code|payment[-_]?credential|shared[-_]?payment[-_]?token)\s+(["']?)([^\s"']+)\1/gi, + /Authorization\s*[:=]\s*Bearer\s+([A-Za-z0-9._\-+=]+)/gi, + /Authorization\s*[:=]\s*Basic\s+([A-Za-z0-9+/=]+)/gi, + /(?:X-OpenClaw-Token|x-pomerium-jwt-assertion|X-Api-Key|X-Auth-Token)\s*[:=]\s*([^\s"',;]+)/gi, + /\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b/g, + /(^|[\s,;])(?:access_token|refresh_token|auth[-_]?token|api[-_]?key|client[-_]?secret|app[-_]?secret|token|secret|password|passwd|card[-_]?number|card[-_]?cvc|card[-_]?cvv|cvc|cvv|security[-_]?code|payment[-_]?credential|shared[-_]?payment[-_]?token)=([^\s&#]+)/gi, + /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g, + /\b(sk-[A-Za-z0-9_-]{8,})\b/g, + /(ghp_[A-Za-z0-9]{20,})/g, + /(github_pat_[A-Za-z0-9_]{20,})/g, + /(xox[baprs]-[A-Za-z0-9-]{10,})/g, + /(xapp-[A-Za-z0-9-]{10,})/g, + /(gsk_[A-Za-z0-9_-]{10,})/g, + /(AIza[0-9A-Za-z\-_]{20,})/g, + /(ya29\.[0-9A-Za-z_\-./+=]{10,})/g, + /(1\/\/0[0-9A-Za-z_\-./+=]{10,})/g, + /(eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})/g, + /(pplx-[A-Za-z0-9_-]{10,})/g, + /(npm_[A-Za-z0-9]{10,})/g, + /(AKID[A-Za-z0-9]{10,})/g, + /(LTAI[A-Za-z0-9]{10,})/g, + /(hf_[A-Za-z0-9]{10,})/g, + /(r8_[A-Za-z0-9]{10,})/g, + /\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, + /\b(\d{6,}:[A-Za-z0-9_-]{20,})\b/g +]; +let configuredRedactor; +function configureAcpErrorRedactor(redactor) { + configuredRedactor = redactor; +} +function redactSensitiveText(value) { + if (configuredRedactor) return configuredRedactor(value); + let redacted = value; + for (const pattern of SECRET_PATTERNS) redacted = redacted.replace(pattern, (match, ...args) => { + if (match.includes("PRIVATE KEY-----")) return "[REDACTED_PRIVATE_KEY]"; + const token = args.slice(0, -2).findLast((group) => typeof group === "string" && group.length > 0); + return token ? match.replace(token, "[REDACTED]") : "[REDACTED]"; + }); + return redacted; +} +/** +* Render a non-Error `cause` value without leaking `[object Object]` or throwing +* while formatting nested ACP runtime failures. +*/ +function stringifyNonErrorCause(value) { + if (value === null) return "null"; + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value); + try { + return JSON.stringify(value); + } catch { + return Object.prototype.toString.call(value); + } +} +//#endregion +export { configureAcpErrorRedactor, redactSensitiveText, stringifyNonErrorCause }; diff --git a/packages/acp-core/dist/index.d.mts b/packages/acp-core/dist/index.d.mts new file mode 100644 index 000000000000..a96641bab838 --- /dev/null +++ b/packages/acp-core/dist/index.d.mts @@ -0,0 +1,15 @@ +import { configureAcpErrorRedactor, redactSensitiveText, stringifyNonErrorCause } from "./error-format.mjs"; +import { readBool, readNonNegativeInteger, readNumber, readString } from "./meta.mjs"; +import { normalizeText } from "./normalize-text.mjs"; +import { resolveIntegerOption } from "./numeric-options.mjs"; +import { asRecord } from "./record-shared.mjs"; +import { isParentOwnedBackgroundAcpSession, isRequesterParentOfBackgroundAcpSession } from "./session-interaction-mode.mjs"; +import { AcpSessionLineageMeta, AcpSessionLineageRow, toAcpSessionLineageMeta } from "./session-lineage-meta.mjs"; +import { AcpProvenanceMode, AcpServerOptions, AcpSession, AcpSessionRuntimeOptions, SessionAcpIdentity, SessionAcpIdentitySource, SessionAcpIdentityState, SessionAcpMeta, SessionId, normalizeAcpProvenanceMode } from "./types.mjs"; +import { AcpSessionStore, createInMemorySessionStore, defaultAcpSessionStore } from "./session.mjs"; +import { ACP_ERROR_CODES, AcpRuntimeError, AcpRuntimeErrorCode, formatAcpErrorChain, isAcpRuntimeError, toAcpRuntimeError, withAcpRuntimeErrorBoundary } from "./runtime/errors.mjs"; +import { formatAcpRuntimeErrorText, toAcpRuntimeErrorText } from "./runtime/error-text.mjs"; +import { ACP_SESSION_IDENTITY_RENDERER_VERSION, AcpSessionIdentifierRenderMode, resolveAcpSessionCwd, resolveAcpSessionIdentifierLines, resolveAcpSessionIdentifierLinesFromIdentity, resolveAcpThreadSessionDetailLines } from "./runtime/session-identifiers.mjs"; +import { AcpRuntime, AcpRuntimeCapabilities, AcpRuntimeControl, AcpRuntimeDoctorReport, AcpRuntimeEnsureInput, AcpRuntimeEvent, AcpRuntimeHandle, AcpRuntimePromptMode, AcpRuntimeSessionMode, AcpRuntimeStatus, AcpRuntimeTurn, AcpRuntimeTurnAttachment, AcpRuntimeTurnInput, AcpRuntimeTurnResult, AcpRuntimeTurnResultError, AcpSessionUpdateTag } from "./runtime/types.mjs"; +import { createIdentityFromEnsure, createIdentityFromHandleEvent, createIdentityFromStatus, identityEquals, identityHasStableSessionId, isSessionIdentityPending, mergeSessionIdentity, resolveRuntimeHandleIdentifiersFromIdentity, resolveRuntimeResumeSessionId, resolveSessionIdentityFromMeta } from "./runtime/session-identity.mjs"; +export { ACP_ERROR_CODES, ACP_SESSION_IDENTITY_RENDERER_VERSION, AcpProvenanceMode, AcpRuntime, AcpRuntimeCapabilities, AcpRuntimeControl, AcpRuntimeDoctorReport, AcpRuntimeEnsureInput, AcpRuntimeError, AcpRuntimeErrorCode, AcpRuntimeEvent, AcpRuntimeHandle, AcpRuntimePromptMode, AcpRuntimeSessionMode, AcpRuntimeStatus, AcpRuntimeTurn, AcpRuntimeTurnAttachment, AcpRuntimeTurnInput, AcpRuntimeTurnResult, AcpRuntimeTurnResultError, AcpServerOptions, AcpSession, AcpSessionIdentifierRenderMode, AcpSessionLineageMeta, AcpSessionLineageRow, AcpSessionRuntimeOptions, AcpSessionStore, AcpSessionUpdateTag, SessionAcpIdentity, SessionAcpIdentitySource, SessionAcpIdentityState, SessionAcpMeta, SessionId, asRecord, configureAcpErrorRedactor, createIdentityFromEnsure, createIdentityFromHandleEvent, createIdentityFromStatus, createInMemorySessionStore, defaultAcpSessionStore, formatAcpErrorChain, formatAcpRuntimeErrorText, identityEquals, identityHasStableSessionId, isAcpRuntimeError, isParentOwnedBackgroundAcpSession, isRequesterParentOfBackgroundAcpSession, isSessionIdentityPending, mergeSessionIdentity, normalizeAcpProvenanceMode, normalizeText, readBool, readNonNegativeInteger, readNumber, readString, redactSensitiveText, resolveAcpSessionCwd, resolveAcpSessionIdentifierLines, resolveAcpSessionIdentifierLinesFromIdentity, resolveAcpThreadSessionDetailLines, resolveIntegerOption, resolveRuntimeHandleIdentifiersFromIdentity, resolveRuntimeResumeSessionId, resolveSessionIdentityFromMeta, stringifyNonErrorCause, toAcpRuntimeError, toAcpRuntimeErrorText, toAcpSessionLineageMeta, withAcpRuntimeErrorBoundary }; \ No newline at end of file diff --git a/packages/acp-core/dist/index.mjs b/packages/acp-core/dist/index.mjs new file mode 100644 index 000000000000..8461ca5afd48 --- /dev/null +++ b/packages/acp-core/dist/index.mjs @@ -0,0 +1,15 @@ +import { configureAcpErrorRedactor, redactSensitiveText, stringifyNonErrorCause } from "./error-format.mjs"; +import { readBool, readNonNegativeInteger, readNumber, readString } from "./meta.mjs"; +import { normalizeText } from "./normalize-text.mjs"; +import { resolveIntegerOption } from "./numeric-options.mjs"; +import { asRecord } from "./record-shared.mjs"; +import { isParentOwnedBackgroundAcpSession, isRequesterParentOfBackgroundAcpSession } from "./session-interaction-mode.mjs"; +import { toAcpSessionLineageMeta } from "./session-lineage-meta.mjs"; +import { createInMemorySessionStore, defaultAcpSessionStore } from "./session.mjs"; +import { normalizeAcpProvenanceMode } from "./types.mjs"; +import { ACP_ERROR_CODES, AcpRuntimeError, formatAcpErrorChain, isAcpRuntimeError, toAcpRuntimeError, withAcpRuntimeErrorBoundary } from "./runtime/errors.mjs"; +import { formatAcpRuntimeErrorText, toAcpRuntimeErrorText } from "./runtime/error-text.mjs"; +import { createIdentityFromEnsure, createIdentityFromHandleEvent, createIdentityFromStatus, identityEquals, identityHasStableSessionId, isSessionIdentityPending, mergeSessionIdentity, resolveRuntimeHandleIdentifiersFromIdentity, resolveRuntimeResumeSessionId, resolveSessionIdentityFromMeta } from "./runtime/session-identity.mjs"; +import { ACP_SESSION_IDENTITY_RENDERER_VERSION, resolveAcpSessionCwd, resolveAcpSessionIdentifierLines, resolveAcpSessionIdentifierLinesFromIdentity, resolveAcpThreadSessionDetailLines } from "./runtime/session-identifiers.mjs"; +import "./runtime/types.mjs"; +export { ACP_ERROR_CODES, ACP_SESSION_IDENTITY_RENDERER_VERSION, AcpRuntimeError, asRecord, configureAcpErrorRedactor, createIdentityFromEnsure, createIdentityFromHandleEvent, createIdentityFromStatus, createInMemorySessionStore, defaultAcpSessionStore, formatAcpErrorChain, formatAcpRuntimeErrorText, identityEquals, identityHasStableSessionId, isAcpRuntimeError, isParentOwnedBackgroundAcpSession, isRequesterParentOfBackgroundAcpSession, isSessionIdentityPending, mergeSessionIdentity, normalizeAcpProvenanceMode, normalizeText, readBool, readNonNegativeInteger, readNumber, readString, redactSensitiveText, resolveAcpSessionCwd, resolveAcpSessionIdentifierLines, resolveAcpSessionIdentifierLinesFromIdentity, resolveAcpThreadSessionDetailLines, resolveIntegerOption, resolveRuntimeHandleIdentifiersFromIdentity, resolveRuntimeResumeSessionId, resolveSessionIdentityFromMeta, stringifyNonErrorCause, toAcpRuntimeError, toAcpRuntimeErrorText, toAcpSessionLineageMeta, withAcpRuntimeErrorBoundary }; diff --git a/packages/acp-core/dist/meta.d.mts b/packages/acp-core/dist/meta.d.mts new file mode 100644 index 000000000000..0e3577663f55 --- /dev/null +++ b/packages/acp-core/dist/meta.d.mts @@ -0,0 +1,7 @@ +//#region src/meta.d.ts +declare function readString(meta: Record | null | undefined, keys: string[]): string | undefined; +declare function readBool(meta: Record | null | undefined, keys: string[]): boolean | undefined; +declare function readNumber(meta: Record | null | undefined, keys: string[]): number | undefined; +declare function readNonNegativeInteger(meta: Record | null | undefined, keys: string[]): number | undefined; +//#endregion +export { readBool, readNonNegativeInteger, readNumber, readString }; \ No newline at end of file diff --git a/packages/acp-core/dist/meta.mjs b/packages/acp-core/dist/meta.mjs new file mode 100644 index 000000000000..5271ec9d65c3 --- /dev/null +++ b/packages/acp-core/dist/meta.mjs @@ -0,0 +1,23 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; +//#region src/meta.ts +function readMetaValue(meta, keys, normalize) { + if (!meta) return; + for (const key of keys) { + const normalized = normalize(meta[key]); + if (normalized !== void 0) return normalized; + } +} +function readString(meta, keys) { + return readMetaValue(meta, keys, normalizeOptionalString); +} +function readBool(meta, keys) { + return readMetaValue(meta, keys, (value) => typeof value === "boolean" ? value : void 0); +} +function readNumber(meta, keys) { + return readMetaValue(meta, keys, (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0); +} +function readNonNegativeInteger(meta, keys) { + return readMetaValue(meta, keys, (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0); +} +//#endregion +export { readBool, readNonNegativeInteger, readNumber, readString }; diff --git a/packages/acp-core/dist/normalize-text.d.mts b/packages/acp-core/dist/normalize-text.d.mts new file mode 100644 index 000000000000..ac0170821993 --- /dev/null +++ b/packages/acp-core/dist/normalize-text.d.mts @@ -0,0 +1,2 @@ +import { normalizeOptionalString as normalizeText } from "@openclaw/normalization-core/string-coerce"; +export { normalizeText }; \ No newline at end of file diff --git a/packages/acp-core/dist/normalize-text.mjs b/packages/acp-core/dist/normalize-text.mjs new file mode 100644 index 000000000000..5cde19ae6d58 --- /dev/null +++ b/packages/acp-core/dist/normalize-text.mjs @@ -0,0 +1,2 @@ +import { normalizeOptionalString as normalizeText } from "@openclaw/normalization-core/string-coerce"; +export { normalizeText }; diff --git a/packages/acp-core/dist/numeric-options.d.mts b/packages/acp-core/dist/numeric-options.d.mts new file mode 100644 index 000000000000..709175b29d7a --- /dev/null +++ b/packages/acp-core/dist/numeric-options.d.mts @@ -0,0 +1,6 @@ +//#region src/numeric-options.d.ts +declare function resolveIntegerOption(value: number | undefined, fallback: number, params: { + min: number; +}): number; +//#endregion +export { resolveIntegerOption }; \ No newline at end of file diff --git a/packages/acp-core/dist/numeric-options.mjs b/packages/acp-core/dist/numeric-options.mjs new file mode 100644 index 000000000000..4549dbe38ea3 --- /dev/null +++ b/packages/acp-core/dist/numeric-options.mjs @@ -0,0 +1,7 @@ +import { resolveIntegerOption as resolveIntegerOption$1 } from "@openclaw/normalization-core/number-coercion"; +//#region src/numeric-options.ts +function resolveIntegerOption(value, fallback, params) { + return resolveIntegerOption$1(value, fallback, params); +} +//#endregion +export { resolveIntegerOption }; diff --git a/packages/acp-core/dist/record-shared.d.mts b/packages/acp-core/dist/record-shared.d.mts new file mode 100644 index 000000000000..57db6317fc69 --- /dev/null +++ b/packages/acp-core/dist/record-shared.d.mts @@ -0,0 +1,2 @@ +import { asOptionalRecord as asRecord } from "@openclaw/normalization-core/record-coerce"; +export { asRecord }; \ No newline at end of file diff --git a/packages/acp-core/dist/record-shared.mjs b/packages/acp-core/dist/record-shared.mjs new file mode 100644 index 000000000000..7dc876aad932 --- /dev/null +++ b/packages/acp-core/dist/record-shared.mjs @@ -0,0 +1,2 @@ +import { asOptionalRecord as asRecord } from "@openclaw/normalization-core/record-coerce"; +export { asRecord }; diff --git a/packages/acp-core/dist/runtime/error-text.d.mts b/packages/acp-core/dist/runtime/error-text.d.mts new file mode 100644 index 000000000000..eecd491d013f --- /dev/null +++ b/packages/acp-core/dist/runtime/error-text.d.mts @@ -0,0 +1,11 @@ +import { AcpRuntimeError, AcpRuntimeErrorCode } from "./errors.mjs"; + +//#region src/runtime/error-text.d.ts +declare function formatAcpRuntimeErrorText(error: AcpRuntimeError): string; +declare function toAcpRuntimeErrorText(params: { + error: unknown; + fallbackCode: AcpRuntimeErrorCode; + fallbackMessage: string; +}): string; +//#endregion +export { formatAcpRuntimeErrorText, toAcpRuntimeErrorText }; \ No newline at end of file diff --git a/packages/acp-core/dist/runtime/error-text.mjs b/packages/acp-core/dist/runtime/error-text.mjs new file mode 100644 index 000000000000..891c9d926886 --- /dev/null +++ b/packages/acp-core/dist/runtime/error-text.mjs @@ -0,0 +1,24 @@ +import { toAcpRuntimeError } from "./errors.mjs"; +//#region src/runtime/error-text.ts +function resolveAcpRuntimeErrorNextStep(error) { + if (error.code === "ACP_BACKEND_MISSING" || error.code === "ACP_BACKEND_UNAVAILABLE") return "Run `/acp doctor`, install/enable the backend plugin, then retry."; + if (error.code === "ACP_DISPATCH_DISABLED") return "Enable `acp.dispatch.enabled=true` to allow thread-message ACP turns."; + if (error.code === "ACP_SESSION_INIT_FAILED") return "If this session is stale, recreate it with `/acp spawn` and rebind the thread."; + if (error.code === "ACP_INVALID_RUNTIME_OPTION") return "Use `/acp status` to inspect options and pass valid values."; + if (error.code === "ACP_BACKEND_UNSUPPORTED_CONTROL") return "This backend does not support that control; use a supported command."; + if (error.code === "ACP_TURN_FAILED") return "Retry, or use `/acp cancel` and send the message again."; +} +function formatAcpRuntimeErrorText(error) { + const next = resolveAcpRuntimeErrorNextStep(error); + if (!next) return `ACP error (${error.code}): ${error.message}`; + return `ACP error (${error.code}): ${error.message}\nnext: ${next}`; +} +function toAcpRuntimeErrorText(params) { + return formatAcpRuntimeErrorText(toAcpRuntimeError({ + error: params.error, + fallbackCode: params.fallbackCode, + fallbackMessage: params.fallbackMessage + })); +} +//#endregion +export { formatAcpRuntimeErrorText, toAcpRuntimeErrorText }; diff --git a/packages/acp-core/dist/runtime/errors.d.mts b/packages/acp-core/dist/runtime/errors.d.mts new file mode 100644 index 000000000000..2ac287b3d4fa --- /dev/null +++ b/packages/acp-core/dist/runtime/errors.d.mts @@ -0,0 +1,33 @@ +//#region src/runtime/errors.d.ts +declare const ACP_ERROR_CODES: readonly ["ACP_BACKEND_MISSING", "ACP_BACKEND_UNAVAILABLE", "ACP_BACKEND_UNSUPPORTED_CONTROL", "ACP_DISPATCH_DISABLED", "ACP_INVALID_RUNTIME_OPTION", "ACP_SESSION_INIT_FAILED", "ACP_TURN_FAILED"]; +type AcpRuntimeErrorCode = (typeof ACP_ERROR_CODES)[number]; +declare class AcpRuntimeError extends Error { + readonly code: AcpRuntimeErrorCode; + readonly cause?: unknown; + constructor(code: AcpRuntimeErrorCode, message: string, options?: { + cause?: unknown; + }); +} +declare function isAcpRuntimeError(value: unknown): value is AcpRuntimeError; +declare function toAcpRuntimeError(params: { + error: unknown; + fallbackCode: AcpRuntimeErrorCode; + fallbackMessage: string; +}): AcpRuntimeError; +/** + * Render an error and its `.cause` chain as a single human-readable line for + * logs, lifecycle events, and tool results. Format is + * `Name [code]: message <- Name [code]: message <- ...`. Number codes also + * appear, so JSON-RPC error codes like `-32603` survive into surfaces that + * downstream consumers see (gateway logs, telegram replies, tool_result text). + * + * Depth is capped to defend against self-referential `.cause` cycles. + */ +declare function formatAcpErrorChain(error: unknown): string; +declare function withAcpRuntimeErrorBoundary(params: { + run: () => Promise; + fallbackCode: AcpRuntimeErrorCode; + fallbackMessage: string; +}): Promise; +//#endregion +export { ACP_ERROR_CODES, AcpRuntimeError, AcpRuntimeErrorCode, formatAcpErrorChain, isAcpRuntimeError, toAcpRuntimeError, withAcpRuntimeErrorBoundary }; \ No newline at end of file diff --git a/packages/acp-core/dist/runtime/errors.mjs b/packages/acp-core/dist/runtime/errors.mjs new file mode 100644 index 000000000000..614076de2834 --- /dev/null +++ b/packages/acp-core/dist/runtime/errors.mjs @@ -0,0 +1,97 @@ +import { redactSensitiveText, stringifyNonErrorCause } from "../error-format.mjs"; +//#region src/runtime/errors.ts +const ACP_ERROR_CODES = [ + "ACP_BACKEND_MISSING", + "ACP_BACKEND_UNAVAILABLE", + "ACP_BACKEND_UNSUPPORTED_CONTROL", + "ACP_DISPATCH_DISABLED", + "ACP_INVALID_RUNTIME_OPTION", + "ACP_SESSION_INIT_FAILED", + "ACP_TURN_FAILED" +]; +const ACP_ERROR_CODE_SET = new Set(ACP_ERROR_CODES); +var AcpRuntimeError = class extends Error { + constructor(code, message, options) { + super(message); + this.name = "AcpRuntimeError"; + this.code = code; + this.cause = options?.cause; + } +}; +function getForeignAcpRuntimeError(value) { + if (!(value instanceof Error)) return null; + const code = value.code; + if (typeof code !== "string" || !ACP_ERROR_CODE_SET.has(code)) return null; + return { + code, + message: value.message + }; +} +function readAcpRequestErrorDetails(value) { + if (typeof value.code !== "number") return; + const data = value.data; + if (!data || typeof data !== "object") return; + const details = data.details; + if (details === void 0 || details === null) return; + const rendered = redactSensitiveText(stringifyNonErrorCause(details)).trim(); + return rendered.length > 0 ? rendered : void 0; +} +function messageWithAcpRequestErrorDetails(error) { + const details = readAcpRequestErrorDetails(error); + if (!details || error.message.includes(details)) return error.message; + return `${error.message}: ${details}`; +} +function isAcpRuntimeError(value) { + return value instanceof AcpRuntimeError || getForeignAcpRuntimeError(value) !== null; +} +function toAcpRuntimeError(params) { + if (params.error instanceof AcpRuntimeError) return params.error; + const foreignAcpRuntimeError = getForeignAcpRuntimeError(params.error); + if (foreignAcpRuntimeError) return new AcpRuntimeError(foreignAcpRuntimeError.code, foreignAcpRuntimeError.message, { cause: params.error }); + if (params.error instanceof Error) return new AcpRuntimeError(params.fallbackCode, messageWithAcpRequestErrorDetails(params.error), { cause: params.error }); + return new AcpRuntimeError(params.fallbackCode, params.fallbackMessage, { cause: params.error }); +} +/** +* Render an error and its `.cause` chain as a single human-readable line for +* logs, lifecycle events, and tool results. Format is +* `Name [code]: message <- Name [code]: message <- ...`. Number codes also +* appear, so JSON-RPC error codes like `-32603` survive into surfaces that +* downstream consumers see (gateway logs, telegram replies, tool_result text). +* +* Depth is capped to defend against self-referential `.cause` cycles. +*/ +function formatAcpErrorChain(error) { + if (!(error instanceof Error)) return redactSensitiveText(String(error)); + const segments = [renderSingleError(error)]; + let current = error.cause; + let depth = 0; + while (current !== void 0 && current !== null && depth < 8) { + if (current instanceof Error) { + segments.push(renderSingleError(current)); + current = current.cause; + } else { + segments.push(stringifyNonErrorCause(current)); + current = void 0; + } + depth += 1; + } + return redactSensitiveText(segments.join(" <- ")); +} +function renderSingleError(error) { + const codeValue = error.code; + const codeSuffix = typeof codeValue === "string" || typeof codeValue === "number" ? ` [${codeValue}]` : ""; + return `${error.name}${codeSuffix}: ${error.message}`; +} +async function withAcpRuntimeErrorBoundary(params) { + try { + return await params.run(); + } catch (error) { + throw toAcpRuntimeError({ + error, + fallbackCode: params.fallbackCode, + fallbackMessage: params.fallbackMessage + }); + } +} +//#endregion +export { ACP_ERROR_CODES, AcpRuntimeError, formatAcpErrorChain, isAcpRuntimeError, toAcpRuntimeError, withAcpRuntimeErrorBoundary }; diff --git a/packages/acp-core/dist/runtime/session-identifiers.d.mts b/packages/acp-core/dist/runtime/session-identifiers.d.mts new file mode 100644 index 000000000000..8e85ba3a2d7f --- /dev/null +++ b/packages/acp-core/dist/runtime/session-identifiers.d.mts @@ -0,0 +1,21 @@ +import { SessionAcpIdentity, SessionAcpMeta } from "../types.mjs"; + +//#region src/runtime/session-identifiers.d.ts +declare const ACP_SESSION_IDENTITY_RENDERER_VERSION = "v1"; +type AcpSessionIdentifierRenderMode = "status" | "thread"; +declare function resolveAcpSessionIdentifierLines(params: { + sessionKey: string; + meta?: SessionAcpMeta; +}): string[]; +declare function resolveAcpSessionIdentifierLinesFromIdentity(params: { + backend: string; + identity?: SessionAcpIdentity; + mode?: AcpSessionIdentifierRenderMode; +}): string[]; +declare function resolveAcpSessionCwd(meta?: SessionAcpMeta): string | undefined; +declare function resolveAcpThreadSessionDetailLines(params: { + sessionKey: string; + meta?: SessionAcpMeta; +}): string[]; +//#endregion +export { ACP_SESSION_IDENTITY_RENDERER_VERSION, AcpSessionIdentifierRenderMode, resolveAcpSessionCwd, resolveAcpSessionIdentifierLines, resolveAcpSessionIdentifierLinesFromIdentity, resolveAcpThreadSessionDetailLines }; \ No newline at end of file diff --git a/packages/acp-core/dist/runtime/session-identifiers.mjs b/packages/acp-core/dist/runtime/session-identifiers.mjs new file mode 100644 index 000000000000..b5cdc07e8bb8 --- /dev/null +++ b/packages/acp-core/dist/runtime/session-identifiers.mjs @@ -0,0 +1,72 @@ +import { normalizeText } from "../normalize-text.mjs"; +import { isSessionIdentityPending, resolveSessionIdentityFromMeta } from "./session-identity.mjs"; +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; +//#region src/runtime/session-identifiers.ts +const ACP_SESSION_IDENTITY_RENDERER_VERSION = "v1"; +const ACP_AGENT_RESUME_HINT_BY_KEY = new Map([ + ["codex", ({ agentSessionId }) => `resume in Codex CLI: \`codex resume ${agentSessionId}\` (continues this conversation).`], + ["openai", ({ agentSessionId }) => `resume in Codex CLI: \`codex resume ${agentSessionId}\` (continues this conversation).`], + ["codex-cli", ({ agentSessionId }) => `resume in Codex CLI: \`codex resume ${agentSessionId}\` (continues this conversation).`], + ["kimi", ({ agentSessionId }) => `resume in Kimi CLI: \`kimi resume ${agentSessionId}\` (continues this conversation).`], + ["moonshot-kimi", ({ agentSessionId }) => `resume in Kimi CLI: \`kimi resume ${agentSessionId}\` (continues this conversation).`] +]); +function normalizeAgentHintKey(value) { + const normalized = normalizeText(value); + if (!normalized) return; + return normalizeLowercaseStringOrEmpty(normalized).replace(/[\s_]+/g, "-"); +} +function resolveAcpAgentResumeHintLine(params) { + const agentSessionId = normalizeText(params.agentSessionId); + const agentKey = normalizeAgentHintKey(params.agentId); + if (!agentSessionId || !agentKey) return; + const resolver = ACP_AGENT_RESUME_HINT_BY_KEY.get(agentKey); + return resolver ? resolver({ agentSessionId }) : void 0; +} +function resolveAcpSessionIdentifierLines(params) { + return resolveAcpSessionIdentifierLinesFromIdentity({ + backend: normalizeText(params.meta?.backend) ?? "backend", + identity: resolveSessionIdentityFromMeta(params.meta), + mode: "status" + }); +} +function resolveAcpSessionIdentifierLinesFromIdentity(params) { + const backend = normalizeText(params.backend) ?? "backend"; + const mode = params.mode ?? "status"; + const identity = params.identity; + const agentSessionId = normalizeText(identity?.agentSessionId); + const acpxSessionId = normalizeText(identity?.acpxSessionId); + const acpxRecordId = normalizeText(identity?.acpxRecordId); + const hasIdentifier = Boolean(agentSessionId || acpxSessionId || acpxRecordId); + if (isSessionIdentityPending(identity) && hasIdentifier) { + if (mode === "status") return ["session ids: pending (available after the first reply)"]; + return []; + } + const lines = []; + if (agentSessionId) lines.push(`agent session id: ${agentSessionId}`); + if (acpxSessionId) lines.push(`${backend} session id: ${acpxSessionId}`); + if (acpxRecordId) lines.push(`${backend} record id: ${acpxRecordId}`); + return lines; +} +function resolveAcpSessionCwd(meta) { + const runtimeCwd = normalizeText(meta?.runtimeOptions?.cwd); + if (runtimeCwd) return runtimeCwd; + return normalizeText(meta?.cwd); +} +function resolveAcpThreadSessionDetailLines(params) { + const meta = params.meta; + const identity = resolveSessionIdentityFromMeta(meta); + const lines = resolveAcpSessionIdentifierLinesFromIdentity({ + backend: normalizeText(meta?.backend) ?? "backend", + identity, + mode: "thread" + }); + if (lines.length === 0) return lines; + const hint = resolveAcpAgentResumeHintLine({ + agentId: meta?.agent, + agentSessionId: identity?.agentSessionId + }); + if (hint) lines.push(hint); + return lines; +} +//#endregion +export { ACP_SESSION_IDENTITY_RENDERER_VERSION, resolveAcpSessionCwd, resolveAcpSessionIdentifierLines, resolveAcpSessionIdentifierLinesFromIdentity, resolveAcpThreadSessionDetailLines }; diff --git a/packages/acp-core/dist/runtime/session-identity.d.mts b/packages/acp-core/dist/runtime/session-identity.d.mts new file mode 100644 index 000000000000..c1d01deca407 --- /dev/null +++ b/packages/acp-core/dist/runtime/session-identity.d.mts @@ -0,0 +1,32 @@ +import { SessionAcpIdentity, SessionAcpMeta } from "../types.mjs"; +import { AcpRuntimeHandle, AcpRuntimeStatus } from "./types.mjs"; + +//#region src/runtime/session-identity.d.ts +declare function resolveSessionIdentityFromMeta(meta: SessionAcpMeta | undefined): SessionAcpIdentity | undefined; +declare function identityHasStableSessionId(identity: SessionAcpIdentity | undefined): boolean; +declare function resolveRuntimeResumeSessionId(identity: SessionAcpIdentity | undefined): string | undefined; +declare function isSessionIdentityPending(identity: SessionAcpIdentity | undefined): boolean; +declare function identityEquals(left: SessionAcpIdentity | undefined, right: SessionAcpIdentity | undefined): boolean; +declare function mergeSessionIdentity(params: { + current: SessionAcpIdentity | undefined; + incoming: SessionAcpIdentity | undefined; + now: number; +}): SessionAcpIdentity | undefined; +declare function createIdentityFromEnsure(params: { + handle: AcpRuntimeHandle; + now: number; +}): SessionAcpIdentity | undefined; +declare function createIdentityFromHandleEvent(params: { + handle: AcpRuntimeHandle; + now: number; +}): SessionAcpIdentity | undefined; +declare function createIdentityFromStatus(params: { + status: AcpRuntimeStatus | undefined; + now: number; +}): SessionAcpIdentity | undefined; +declare function resolveRuntimeHandleIdentifiersFromIdentity(identity: SessionAcpIdentity | undefined): { + backendSessionId?: string; + agentSessionId?: string; +}; +//#endregion +export { createIdentityFromEnsure, createIdentityFromHandleEvent, createIdentityFromStatus, identityEquals, identityHasStableSessionId, isSessionIdentityPending, mergeSessionIdentity, resolveRuntimeHandleIdentifiersFromIdentity, resolveRuntimeResumeSessionId, resolveSessionIdentityFromMeta }; \ No newline at end of file diff --git a/packages/acp-core/dist/runtime/session-identity.mjs b/packages/acp-core/dist/runtime/session-identity.mjs new file mode 100644 index 000000000000..4c28b99136d3 --- /dev/null +++ b/packages/acp-core/dist/runtime/session-identity.mjs @@ -0,0 +1,139 @@ +import { normalizeText } from "../normalize-text.mjs"; +//#region src/runtime/session-identity.ts +function normalizeIdentityState(value) { + if (value !== "pending" && value !== "resolved") return; + return value; +} +function normalizeIdentitySource(value) { + if (value !== "ensure" && value !== "status" && value !== "event") return; + return value; +} +function normalizeIdentity(identity) { + if (!identity) return; + const state = normalizeIdentityState(identity.state); + const source = normalizeIdentitySource(identity.source); + const acpxRecordId = normalizeText(identity.acpxRecordId); + const acpxSessionId = normalizeText(identity.acpxSessionId); + const agentSessionId = normalizeText(identity.agentSessionId); + const lastUpdatedAt = typeof identity.lastUpdatedAt === "number" && Number.isFinite(identity.lastUpdatedAt) ? identity.lastUpdatedAt : void 0; + if (!state && !source && !Boolean(acpxRecordId || acpxSessionId || agentSessionId) && lastUpdatedAt === void 0) return; + return { + state: state ?? (Boolean(acpxSessionId || agentSessionId) ? "resolved" : "pending"), + ...acpxRecordId ? { acpxRecordId } : {}, + ...acpxSessionId ? { acpxSessionId } : {}, + ...agentSessionId ? { agentSessionId } : {}, + source: source ?? "status", + lastUpdatedAt: lastUpdatedAt ?? Date.now() + }; +} +function readIdentityIdsFromHandle(handle) { + return { + acpxRecordId: normalizeText(handle.acpxRecordId), + acpxSessionId: normalizeText(handle.backendSessionId), + agentSessionId: normalizeText(handle.agentSessionId) + }; +} +function buildSessionIdentity(params) { + const { acpxRecordId, acpxSessionId, agentSessionId } = params.ids; + if (!acpxRecordId && !acpxSessionId && !agentSessionId) return; + return { + state: params.state, + ...acpxRecordId ? { acpxRecordId } : {}, + ...acpxSessionId ? { acpxSessionId } : {}, + ...agentSessionId ? { agentSessionId } : {}, + source: params.source, + lastUpdatedAt: params.now + }; +} +function resolveSessionIdentityFromMeta(meta) { + if (!meta) return; + return normalizeIdentity(meta.identity); +} +function identityHasStableSessionId(identity) { + return Boolean(identity?.acpxSessionId || identity?.agentSessionId); +} +function resolveRuntimeResumeSessionId(identity) { + if (!identity) return; + return normalizeText(identity.agentSessionId) ?? normalizeText(identity.acpxSessionId); +} +function isSessionIdentityPending(identity) { + if (!identity) return true; + return identity.state === "pending"; +} +function identityEquals(left, right) { + const a = normalizeIdentity(left); + const b = normalizeIdentity(right); + if (!a && !b) return true; + if (!a || !b) return false; + return a.state === b.state && a.acpxRecordId === b.acpxRecordId && a.acpxSessionId === b.acpxSessionId && a.agentSessionId === b.agentSessionId && a.source === b.source; +} +function mergeSessionIdentity(params) { + const current = normalizeIdentity(params.current); + const incoming = normalizeIdentity(params.incoming); + if (!current) { + if (!incoming) return; + return { + ...incoming, + lastUpdatedAt: params.now + }; + } + if (!incoming) return current; + const currentResolved = current.state === "resolved"; + const incomingResolved = incoming.state === "resolved"; + const allowIncomingValue = !currentResolved || incomingResolved; + const nextRecordId = allowIncomingValue && incoming.acpxRecordId ? incoming.acpxRecordId : current.acpxRecordId; + const nextAcpxSessionId = allowIncomingValue && incoming.acpxSessionId ? incoming.acpxSessionId : current.acpxSessionId; + const nextAgentSessionId = allowIncomingValue && incoming.agentSessionId ? incoming.agentSessionId : current.agentSessionId; + const nextState = Boolean(nextAcpxSessionId || nextAgentSessionId) ? "resolved" : currentResolved ? "resolved" : incoming.state; + const nextSource = allowIncomingValue ? incoming.source : current.source; + return { + state: nextState, + ...nextRecordId ? { acpxRecordId: nextRecordId } : {}, + ...nextAcpxSessionId ? { acpxSessionId: nextAcpxSessionId } : {}, + ...nextAgentSessionId ? { agentSessionId: nextAgentSessionId } : {}, + source: nextSource, + lastUpdatedAt: params.now + }; +} +function createIdentityFromEnsure(params) { + return buildSessionIdentity({ + ids: readIdentityIdsFromHandle(params.handle), + state: "pending", + source: "ensure", + now: params.now + }); +} +function createIdentityFromHandleEvent(params) { + const ids = readIdentityIdsFromHandle(params.handle); + return buildSessionIdentity({ + ids, + state: ids.agentSessionId ? "resolved" : "pending", + source: "event", + now: params.now + }); +} +function createIdentityFromStatus(params) { + if (!params.status) return; + const details = params.status.details; + const acpxRecordId = normalizeText(params.status.acpxRecordId) ?? normalizeText(details?.acpxRecordId); + const acpxSessionId = normalizeText(params.status.backendSessionId) ?? normalizeText(details?.backendSessionId) ?? normalizeText(details?.acpxSessionId); + const agentSessionId = normalizeText(params.status.agentSessionId) ?? normalizeText(details?.agentSessionId); + if (!acpxRecordId && !acpxSessionId && !agentSessionId) return; + return { + state: Boolean(acpxSessionId || agentSessionId) ? "resolved" : "pending", + ...acpxRecordId ? { acpxRecordId } : {}, + ...acpxSessionId ? { acpxSessionId } : {}, + ...agentSessionId ? { agentSessionId } : {}, + source: "status", + lastUpdatedAt: params.now + }; +} +function resolveRuntimeHandleIdentifiersFromIdentity(identity) { + if (!identity) return {}; + return { + ...identity.acpxSessionId ? { backendSessionId: identity.acpxSessionId } : {}, + ...identity.agentSessionId ? { agentSessionId: identity.agentSessionId } : {} + }; +} +//#endregion +export { createIdentityFromEnsure, createIdentityFromHandleEvent, createIdentityFromStatus, identityEquals, identityHasStableSessionId, isSessionIdentityPending, mergeSessionIdentity, resolveRuntimeHandleIdentifiersFromIdentity, resolveRuntimeResumeSessionId, resolveSessionIdentityFromMeta }; diff --git a/packages/acp-core/dist/runtime/types.d.mts b/packages/acp-core/dist/runtime/types.d.mts new file mode 100644 index 000000000000..8dc9e42e6481 --- /dev/null +++ b/packages/acp-core/dist/runtime/types.d.mts @@ -0,0 +1,162 @@ +//#region src/runtime/types.d.ts +type AcpRuntimePromptMode = "prompt" | "steer"; +type AcpRuntimeSessionMode = "persistent" | "oneshot"; +type AcpSessionUpdateTag = "agent_message_chunk" | "agent_thought_chunk" | "tool_call" | "tool_call_update" | "usage_update" | "available_commands_update" | "current_mode_update" | "config_option_update" | "session_info_update" | "plan" | (string & {}); +type AcpRuntimeControl = "session/set_mode" | "session/set_config_option" | "session/status"; +type AcpRuntimeHandle = { + sessionKey: string; + backend: string; + runtimeSessionName: string; /** Effective runtime working directory for this ACP session, if exposed by adapter/runtime. */ + cwd?: string; /** Backend-local record identifier, if exposed by adapter/runtime (for example acpx record id). */ + acpxRecordId?: string; /** Backend-level ACP session identifier, if exposed by adapter/runtime. */ + backendSessionId?: string; /** Upstream harness session identifier, if exposed by adapter/runtime. */ + agentSessionId?: string; +}; +type AcpRuntimeEnsureInput = { + sessionKey: string; + agent: string; + mode: AcpRuntimeSessionMode; + resumeSessionId?: string; /** Optional runtime model override that must be available during session creation. */ + model?: string; /** Optional runtime thinking/reasoning override that must be available during session creation. */ + thinking?: string; + cwd?: string; + env?: Record; +}; +type AcpRuntimeTurnAttachment = { + mediaType: string; + data: string; +}; +type AcpRuntimeTurnInput = { + handle: AcpRuntimeHandle; + text: string; + attachments?: AcpRuntimeTurnAttachment[]; + mode: AcpRuntimePromptMode; + requestId: string; + signal?: AbortSignal; +}; +type AcpRuntimeCapabilities = { + controls: AcpRuntimeControl[]; + /** + * Optional backend-advertised option keys for session/set_config_option. + * Empty/undefined means "backend accepts keys, but did not advertise a strict list". + */ + configOptionKeys?: string[]; +}; +type AcpRuntimeStatus = { + summary?: string; /** Backend-local record identifier, if exposed by adapter/runtime. */ + acpxRecordId?: string; /** Backend-level ACP session identifier, if known at status time. */ + backendSessionId?: string; /** Upstream harness session identifier, if known at status time. */ + agentSessionId?: string; + details?: Record; +}; +type AcpRuntimeDoctorReport = { + ok: boolean; + code?: string; + message: string; + installCommand?: string; + details?: string[]; +}; +type AcpRuntimeEvent = { + type: "text_delta"; + text: string; + stream?: "output" | "thought"; + tag?: AcpSessionUpdateTag; +} | { + type: "status"; + text: string; + tag?: AcpSessionUpdateTag; + used?: number; + size?: number; +} | { + type: "tool_call"; + text: string; + tag?: AcpSessionUpdateTag; + toolCallId?: string; + status?: string; + title?: string; +} | { + type: "done"; + stopReason?: string; +} | { + type: "error"; + message: string; + code?: string; + detailCode?: string; + retryable?: boolean; +}; +type AcpRuntimeTurnResultError = { + message: string; + code?: string; + detailCode?: string; + retryable?: boolean; +}; +type AcpRuntimeTurnResult = { + status: "completed"; + stopReason?: string; +} | { + status: "cancelled"; + stopReason?: string; +} | { + status: "failed"; + error: AcpRuntimeTurnResultError; +}; +interface AcpRuntimeTurn { + readonly requestId: string; + readonly events: AsyncIterable; + readonly result: Promise; + cancel(input?: { + reason?: string; + }): Promise; + closeStream(input?: { + reason?: string; + }): Promise; +} +interface AcpRuntime { + ensureSession(input: AcpRuntimeEnsureInput): Promise; + /** + * Preferred turn API. Live events are streamed separately from the terminal + * result so adapters can report failures without relying on legacy done/error + * events in the stream. + */ + startTurn?(input: AcpRuntimeTurnInput): AcpRuntimeTurn; + runTurn(input: AcpRuntimeTurnInput): AsyncIterable; + getCapabilities?(input: { + handle?: AcpRuntimeHandle; + }): Promise | AcpRuntimeCapabilities; + getStatus?(input: { + handle: AcpRuntimeHandle; + signal?: AbortSignal; + }): Promise; + setMode?(input: { + handle: AcpRuntimeHandle; + mode: string; + }): Promise; + setConfigOption?(input: { + handle: AcpRuntimeHandle; + key: string; + value: string; + }): Promise; + doctor?(): Promise; + /** + * Prepare the next ensureSession for this session key to start fresh instead + * of reopening backend-owned persistent state. + */ + prepareFreshSession?(input: { + sessionKey: string; + }): Promise; + cancel(input: { + handle: AcpRuntimeHandle; + reason?: string; + }): Promise; + close(input: { + handle: AcpRuntimeHandle; + reason: string; + /** + * Discard backend-owned persistent session state so the next ensureSession + * starts fresh instead of reopening the same conversation. + */ + discardPersistentState?: boolean; + }): Promise; +} +//#endregion +export { AcpRuntime, AcpRuntimeCapabilities, AcpRuntimeControl, AcpRuntimeDoctorReport, AcpRuntimeEnsureInput, AcpRuntimeEvent, AcpRuntimeHandle, AcpRuntimePromptMode, AcpRuntimeSessionMode, AcpRuntimeStatus, AcpRuntimeTurn, AcpRuntimeTurnAttachment, AcpRuntimeTurnInput, AcpRuntimeTurnResult, AcpRuntimeTurnResultError, AcpSessionUpdateTag }; \ No newline at end of file diff --git a/packages/acp-core/dist/runtime/types.mjs b/packages/acp-core/dist/runtime/types.mjs new file mode 100644 index 000000000000..cb0ff5c3b541 --- /dev/null +++ b/packages/acp-core/dist/runtime/types.mjs @@ -0,0 +1 @@ +export {}; diff --git a/packages/acp-core/dist/session-interaction-mode.d.mts b/packages/acp-core/dist/session-interaction-mode.d.mts new file mode 100644 index 000000000000..56a810a49412 --- /dev/null +++ b/packages/acp-core/dist/session-interaction-mode.d.mts @@ -0,0 +1,21 @@ +//#region src/session-interaction-mode.d.ts +type SessionInteractionEntry = { + spawnedBy?: string; + parentSessionKey?: string; + acp?: unknown; +}; +declare function isParentOwnedBackgroundAcpSession(entry?: SessionInteractionEntry | null): boolean; +/** + * Returns true when `entry` is a parent-owned background ACP session AND the + * given `requesterSessionKey` is the session that spawned/owns it. This is a + * strictly narrower check than {@link isParentOwnedBackgroundAcpSession}: the + * target must match *and* the caller must be the parent. + * + * Used to gate behaviors that only make sense for the parent↔own-child pair + * (e.g. skipping the A2A ping-pong flow in `sessions_send`), so that an + * unrelated session with broad visibility (e.g. `tools.sessions.visibility=all`) + * sending to the same target is still routed through the normal A2A path. + */ +declare function isRequesterParentOfBackgroundAcpSession(entry: SessionInteractionEntry | null | undefined, requesterSessionKey: string | null | undefined): boolean; +//#endregion +export { isParentOwnedBackgroundAcpSession, isRequesterParentOfBackgroundAcpSession }; \ No newline at end of file diff --git a/packages/acp-core/dist/session-interaction-mode.mjs b/packages/acp-core/dist/session-interaction-mode.mjs new file mode 100644 index 000000000000..ab49db6ddf25 --- /dev/null +++ b/packages/acp-core/dist/session-interaction-mode.mjs @@ -0,0 +1,31 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; +//#region src/session-interaction-mode.ts +function resolveAcpSessionInteractionMode(entry) { + if (!entry?.acp) return "interactive"; + if (normalizeOptionalString(entry.spawnedBy) || normalizeOptionalString(entry.parentSessionKey)) return "parent-owned-background"; + return "interactive"; +} +function isParentOwnedBackgroundAcpSession(entry) { + return resolveAcpSessionInteractionMode(entry) === "parent-owned-background"; +} +/** +* Returns true when `entry` is a parent-owned background ACP session AND the +* given `requesterSessionKey` is the session that spawned/owns it. This is a +* strictly narrower check than {@link isParentOwnedBackgroundAcpSession}: the +* target must match *and* the caller must be the parent. +* +* Used to gate behaviors that only make sense for the parent↔own-child pair +* (e.g. skipping the A2A ping-pong flow in `sessions_send`), so that an +* unrelated session with broad visibility (e.g. `tools.sessions.visibility=all`) +* sending to the same target is still routed through the normal A2A path. +*/ +function isRequesterParentOfBackgroundAcpSession(entry, requesterSessionKey) { + if (!isParentOwnedBackgroundAcpSession(entry)) return false; + const requester = normalizeOptionalString(requesterSessionKey); + if (!requester) return false; + const spawnedBy = normalizeOptionalString(entry?.spawnedBy); + const parentSessionKey = normalizeOptionalString(entry?.parentSessionKey); + return requester === spawnedBy || requester === parentSessionKey; +} +//#endregion +export { isParentOwnedBackgroundAcpSession, isRequesterParentOfBackgroundAcpSession }; diff --git a/packages/acp-core/dist/session-lineage-meta.d.mts b/packages/acp-core/dist/session-lineage-meta.d.mts new file mode 100644 index 000000000000..815285d0e5c7 --- /dev/null +++ b/packages/acp-core/dist/session-lineage-meta.d.mts @@ -0,0 +1,32 @@ +//#region src/session-lineage-meta.d.ts +declare const SUBAGENT_ROLES: readonly ["orchestrator", "leaf"]; +declare const SUBAGENT_CONTROL_SCOPES: readonly ["children", "none"]; +type SubagentRole = (typeof SUBAGENT_ROLES)[number]; +type SubagentControlScope = (typeof SUBAGENT_CONTROL_SCOPES)[number]; +type AcpSessionLineageMeta = { + sessionKey: string; + kind?: string; + channel?: string; + parentSessionId?: string; + spawnedBy?: string; + spawnDepth?: number; + subagentRole?: SubagentRole; + subagentControlScope?: SubagentControlScope; + spawnedWorkspaceDir?: string; + spawnedCwd?: string; +}; +type AcpSessionLineageRow = { + key: string; + kind?: string; + channel?: string; + parentSessionKey?: string; + spawnedBy?: string; + spawnDepth?: number; + subagentRole?: string; + subagentControlScope?: string; + spawnedWorkspaceDir?: string; + spawnedCwd?: string; +}; +declare function toAcpSessionLineageMeta(row: AcpSessionLineageRow): AcpSessionLineageMeta; +//#endregion +export { AcpSessionLineageMeta, AcpSessionLineageRow, toAcpSessionLineageMeta }; \ No newline at end of file diff --git a/packages/acp-core/dist/session-lineage-meta.mjs b/packages/acp-core/dist/session-lineage-meta.mjs new file mode 100644 index 000000000000..be73eb835618 --- /dev/null +++ b/packages/acp-core/dist/session-lineage-meta.mjs @@ -0,0 +1,38 @@ +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; +//#region src/session-lineage-meta.ts +const SUBAGENT_ROLES = ["orchestrator", "leaf"]; +const SUBAGENT_CONTROL_SCOPES = ["children", "none"]; +function readInteger(value) { + if (typeof value !== "number" || !Number.isInteger(value) || value < 0) return; + return value; +} +function readEnum(value, allowed) { + const normalized = normalizeOptionalString(value); + return allowed.find((candidate) => candidate === normalized); +} +function toAcpSessionLineageMeta(row) { + const sessionKey = normalizeOptionalString(row.key) ?? row.key; + const kind = normalizeOptionalString(row.kind); + const channel = normalizeOptionalString(row.channel); + const parentSessionId = normalizeOptionalString(row.parentSessionKey) ?? normalizeOptionalString(row.spawnedBy); + const spawnedBy = normalizeOptionalString(row.spawnedBy); + const spawnDepth = readInteger(row.spawnDepth); + const subagentRole = readEnum(row.subagentRole, SUBAGENT_ROLES); + const subagentControlScope = readEnum(row.subagentControlScope, SUBAGENT_CONTROL_SCOPES); + const spawnedWorkspaceDir = normalizeOptionalString(row.spawnedWorkspaceDir); + const spawnedCwd = normalizeOptionalString(row.spawnedCwd); + return { + sessionKey, + ...kind ? { kind } : {}, + ...channel ? { channel } : {}, + ...parentSessionId ? { parentSessionId } : {}, + ...spawnedBy ? { spawnedBy } : {}, + ...spawnDepth !== void 0 ? { spawnDepth } : {}, + ...subagentRole ? { subagentRole } : {}, + ...subagentControlScope ? { subagentControlScope } : {}, + ...spawnedWorkspaceDir ? { spawnedWorkspaceDir } : {}, + ...spawnedCwd ? { spawnedCwd } : {} + }; +} +//#endregion +export { toAcpSessionLineageMeta }; diff --git a/packages/acp-core/dist/session.d.mts b/packages/acp-core/dist/session.d.mts new file mode 100644 index 000000000000..48cfa4c5488b --- /dev/null +++ b/packages/acp-core/dist/session.d.mts @@ -0,0 +1,28 @@ +import { AcpSession } from "./types.mjs"; + +//#region src/session.d.ts +type AcpSessionStore = { + createSession: (params: { + sessionKey: string; + cwd: string; + sessionId?: string; + ledgerSessionId?: string; + }) => AcpSession; + hasSession: (sessionId: string) => boolean; + getSession: (sessionId: string) => AcpSession | undefined; + getSessionByRunId: (runId: string) => AcpSession | undefined; + setActiveRun: (sessionId: string, runId: string, abortController: AbortController) => void; + clearActiveRun: (sessionId: string) => void; + cancelActiveRun: (sessionId: string) => boolean; + deleteSession: (sessionId: string) => boolean; + clearAllSessionsForTest: () => void; +}; +type AcpSessionStoreOptions = { + maxSessions?: number; + idleTtlMs?: number; + now?: () => number; +}; +declare function createInMemorySessionStore(options?: AcpSessionStoreOptions): AcpSessionStore; +declare const defaultAcpSessionStore: AcpSessionStore; +//#endregion +export { AcpSessionStore, createInMemorySessionStore, defaultAcpSessionStore }; \ No newline at end of file diff --git a/packages/acp-core/dist/session.mjs b/packages/acp-core/dist/session.mjs new file mode 100644 index 000000000000..edb137c7c5d4 --- /dev/null +++ b/packages/acp-core/dist/session.mjs @@ -0,0 +1,128 @@ +import { resolveIntegerOption } from "./numeric-options.mjs"; +import { randomUUID } from "node:crypto"; +//#region src/session.ts +const DEFAULT_MAX_SESSIONS = 5e3; +const DEFAULT_IDLE_TTL_MS = 1440 * 60 * 1e3; +function createInMemorySessionStore(options = {}) { + const maxSessions = resolveIntegerOption(options.maxSessions, DEFAULT_MAX_SESSIONS, { min: 1 }); + const idleTtlMs = resolveIntegerOption(options.idleTtlMs, DEFAULT_IDLE_TTL_MS, { min: 1e3 }); + const now = options.now ?? Date.now; + const sessions = /* @__PURE__ */ new Map(); + const runIdToSessionId = /* @__PURE__ */ new Map(); + const touchSession = (session, nowMs) => { + session.lastTouchedAt = nowMs; + }; + const removeSession = (sessionId) => { + const session = sessions.get(sessionId); + if (!session) return false; + if (session.activeRunId) runIdToSessionId.delete(session.activeRunId); + session.abortController?.abort(); + sessions.delete(sessionId); + return true; + }; + const reapIdleSessions = (nowMs) => { + const idleBefore = nowMs - idleTtlMs; + for (const [sessionId, session] of sessions.entries()) { + if (session.activeRunId || session.abortController) continue; + if (session.lastTouchedAt > idleBefore) continue; + removeSession(sessionId); + } + }; + const evictOldestIdleSession = () => { + let oldestSessionId = null; + let oldestLastTouchedAt = Number.POSITIVE_INFINITY; + for (const [sessionId, session] of sessions.entries()) { + if (session.activeRunId || session.abortController) continue; + if (session.lastTouchedAt >= oldestLastTouchedAt) continue; + oldestLastTouchedAt = session.lastTouchedAt; + oldestSessionId = sessionId; + } + if (!oldestSessionId) return false; + return removeSession(oldestSessionId); + }; + const createSession = (params) => { + const nowMs = now(); + const sessionId = params.sessionId ?? randomUUID(); + const existingSession = sessions.get(sessionId); + if (existingSession) { + existingSession.sessionKey = params.sessionKey; + if ("ledgerSessionId" in params) existingSession.ledgerSessionId = params.ledgerSessionId; + existingSession.cwd = params.cwd; + touchSession(existingSession, nowMs); + return existingSession; + } + reapIdleSessions(nowMs); + if (sessions.size >= maxSessions && !evictOldestIdleSession()) throw new Error(`ACP session limit reached (max ${maxSessions}). Close idle ACP clients and retry.`); + const session = { + sessionId, + sessionKey: params.sessionKey, + ...params.ledgerSessionId ? { ledgerSessionId: params.ledgerSessionId } : {}, + cwd: params.cwd, + createdAt: nowMs, + lastTouchedAt: nowMs, + abortController: null, + activeRunId: null + }; + sessions.set(sessionId, session); + return session; + }; + const hasSession = (sessionId) => sessions.has(sessionId); + const getSession = (sessionId) => { + const session = sessions.get(sessionId); + if (session) touchSession(session, now()); + return session; + }; + const getSessionByRunId = (runId) => { + const sessionId = runIdToSessionId.get(runId); + if (!sessionId) return; + const session = sessions.get(sessionId); + if (session) touchSession(session, now()); + return session; + }; + const setActiveRun = (sessionId, runId, abortController) => { + const session = sessions.get(sessionId); + if (!session) return; + session.activeRunId = runId; + session.abortController = abortController; + runIdToSessionId.set(runId, sessionId); + touchSession(session, now()); + }; + const clearActiveRun = (sessionId) => { + const session = sessions.get(sessionId); + if (!session) return; + if (session.activeRunId) runIdToSessionId.delete(session.activeRunId); + session.activeRunId = null; + session.abortController = null; + touchSession(session, now()); + }; + const cancelActiveRun = (sessionId) => { + const session = sessions.get(sessionId); + if (!session?.abortController) return false; + session.abortController.abort(); + if (session.activeRunId) runIdToSessionId.delete(session.activeRunId); + session.abortController = null; + session.activeRunId = null; + touchSession(session, now()); + return true; + }; + const deleteSession = (sessionId) => removeSession(sessionId); + const clearAllSessionsForTest = () => { + for (const session of sessions.values()) session.abortController?.abort(); + sessions.clear(); + runIdToSessionId.clear(); + }; + return { + createSession, + hasSession, + getSession, + getSessionByRunId, + setActiveRun, + clearActiveRun, + cancelActiveRun, + deleteSession, + clearAllSessionsForTest + }; +} +const defaultAcpSessionStore = createInMemorySessionStore(); +//#endregion +export { createInMemorySessionStore, defaultAcpSessionStore }; diff --git a/packages/acp-core/dist/types.d.mts b/packages/acp-core/dist/types.d.mts new file mode 100644 index 000000000000..2d44c8faf04b --- /dev/null +++ b/packages/acp-core/dist/types.d.mts @@ -0,0 +1,67 @@ +//#region src/types.d.ts +declare const ACP_PROVENANCE_MODE_VALUES: readonly ["off", "meta", "meta+receipt"]; +type SessionId = string; +type AcpProvenanceMode = (typeof ACP_PROVENANCE_MODE_VALUES)[number]; +declare function normalizeAcpProvenanceMode(value: string | undefined): AcpProvenanceMode | undefined; +type AcpSession = { + sessionId: SessionId; + sessionKey: string; + ledgerSessionId?: string; + cwd: string; + createdAt: number; + lastTouchedAt: number; + abortController: AbortController | null; + activeRunId: string | null; +}; +type AcpServerOptions = { + gatewayUrl?: string; + gatewayToken?: string; + gatewayPassword?: string; + defaultSessionKey?: string; + defaultSessionLabel?: string; + requireExistingSession?: boolean; + resetSession?: boolean; + prefixCwd?: boolean; + provenanceMode?: AcpProvenanceMode; + sessionCreateRateLimit?: { + maxRequests?: number; + windowMs?: number; + }; + verbose?: boolean; +}; +type SessionAcpIdentitySource = "ensure" | "status" | "event"; +type SessionAcpIdentityState = "pending" | "resolved"; +type SessionAcpIdentity = { + state: SessionAcpIdentityState; + acpxRecordId?: string; + acpxSessionId?: string; + agentSessionId?: string; + source: SessionAcpIdentitySource; + lastUpdatedAt: number; +}; +type AcpSessionRuntimeOptions = { + /** + * ACP runtime mode set via session/set_mode (for example: "plan", "normal", "auto"). + */ + runtimeMode?: string; /** ACP runtime config option: model id. */ + model?: string; /** ACP runtime config option: thinking/reasoning effort. */ + thinking?: string; /** Working directory override for ACP session turns. */ + cwd?: string; /** ACP runtime config option: permission profile id. */ + permissionProfile?: string; /** ACP runtime config option: per-turn timeout in seconds. */ + timeoutSeconds?: number; /** Backend-specific option bag mapped through session/set_config_option. */ + backendExtras?: Record; +}; +type SessionAcpMeta = { + backend: string; + agent: string; + runtimeSessionName: string; + identity?: SessionAcpIdentity; + mode: "persistent" | "oneshot"; + runtimeOptions?: AcpSessionRuntimeOptions; + cwd?: string; + state: "idle" | "running" | "error"; + lastActivityAt: number; + lastError?: string; +}; +//#endregion +export { AcpProvenanceMode, AcpServerOptions, AcpSession, AcpSessionRuntimeOptions, SessionAcpIdentity, SessionAcpIdentitySource, SessionAcpIdentityState, SessionAcpMeta, SessionId, normalizeAcpProvenanceMode }; \ No newline at end of file diff --git a/packages/acp-core/dist/types.mjs b/packages/acp-core/dist/types.mjs new file mode 100644 index 000000000000..8b190ba5a7aa --- /dev/null +++ b/packages/acp-core/dist/types.mjs @@ -0,0 +1,14 @@ +import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; +//#region src/types.ts +const ACP_PROVENANCE_MODE_VALUES = [ + "off", + "meta", + "meta+receipt" +]; +function normalizeAcpProvenanceMode(value) { + const normalized = normalizeOptionalLowercaseString(value); + if (!normalized) return; + return ACP_PROVENANCE_MODE_VALUES.includes(normalized) ? normalized : void 0; +} +//#endregion +export { normalizeAcpProvenanceMode }; diff --git a/packages/acp-core/package.json b/packages/acp-core/package.json index 7fb97fae6f41..648722954327 100644 --- a/packages/acp-core/package.json +++ b/packages/acp-core/package.json @@ -19,11 +19,61 @@ "import": "./dist/normalize-text.mjs", "default": "./dist/normalize-text.mjs" }, + "./meta": { + "types": "./dist/meta.d.mts", + "import": "./dist/meta.mjs", + "default": "./dist/meta.mjs" + }, + "./numeric-options": { + "types": "./dist/numeric-options.d.mts", + "import": "./dist/numeric-options.mjs", + "default": "./dist/numeric-options.mjs" + }, "./record-shared": { "types": "./dist/record-shared.d.mts", "import": "./dist/record-shared.mjs", "default": "./dist/record-shared.mjs" }, + "./session": { + "types": "./dist/session.d.mts", + "import": "./dist/session.mjs", + "default": "./dist/session.mjs" + }, + "./session-interaction-mode": { + "types": "./dist/session-interaction-mode.d.mts", + "import": "./dist/session-interaction-mode.mjs", + "default": "./dist/session-interaction-mode.mjs" + }, + "./session-lineage-meta": { + "types": "./dist/session-lineage-meta.d.mts", + "import": "./dist/session-lineage-meta.mjs", + "default": "./dist/session-lineage-meta.mjs" + }, + "./types": { + "types": "./dist/types.d.mts", + "import": "./dist/types.mjs", + "default": "./dist/types.mjs" + }, + "./runtime/error-text": { + "types": "./dist/runtime/error-text.d.mts", + "import": "./dist/runtime/error-text.mjs", + "default": "./dist/runtime/error-text.mjs" + }, + "./runtime/errors": { + "types": "./dist/runtime/errors.d.mts", + "import": "./dist/runtime/errors.mjs", + "default": "./dist/runtime/errors.mjs" + }, + "./runtime/session-identifiers": { + "types": "./dist/runtime/session-identifiers.d.mts", + "import": "./dist/runtime/session-identifiers.mjs", + "default": "./dist/runtime/session-identifiers.mjs" + }, + "./runtime/session-identity": { + "types": "./dist/runtime/session-identity.d.mts", + "import": "./dist/runtime/session-identity.mjs", + "default": "./dist/runtime/session-identity.mjs" + }, "./runtime/types": { "types": "./dist/runtime/types.d.mts", "import": "./dist/runtime/types.mjs", @@ -34,6 +84,6 @@ "@openclaw/normalization-core": "workspace:*" }, "scripts": { - "build": "tsdown src/index.ts src/normalize-text.ts src/record-shared.ts src/runtime/types.ts --no-config --platform node --format esm --dts --out-dir dist --clean" + "build": "tsdown src/index.ts src/error-format.ts src/meta.ts src/normalize-text.ts src/numeric-options.ts src/record-shared.ts src/session.ts src/session-interaction-mode.ts src/session-lineage-meta.ts src/types.ts src/runtime/error-text.ts src/runtime/errors.ts src/runtime/session-identifiers.ts src/runtime/session-identity.ts src/runtime/types.ts --no-config --platform node --format esm --dts --out-dir dist --clean" } } diff --git a/packages/acp-core/src/error-format.ts b/packages/acp-core/src/error-format.ts new file mode 100644 index 000000000000..67a2a56bd48d --- /dev/null +++ b/packages/acp-core/src/error-format.ts @@ -0,0 +1,78 @@ +const SECRET_PATTERNS: RegExp[] = [ + /\b[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CARD[_-]?NUMBER|CARD[_-]?CVC|CARD[_-]?CVV|CVC|CVV|SECURITY[_-]?CODE|PAYMENT[_-]?CREDENTIAL|SHARED[_-]?PAYMENT[_-]?TOKEN)\b\s*[=:]\s*(["']?)([^\s"'\\]+)\1/g, + /\b[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CARD[_-]?NUMBER|CARD[_-]?CVC|CARD[_-]?CVV|CVC|CVV|SECURITY[_-]?CODE|PAYMENT[_-]?CREDENTIAL|SHARED[_-]?PAYMENT[_-]?TOKEN)\b\s*[=:]\s*\\+(["'])([^\s"'\\]+)\\+\1/g, + /[?&](?:access[-_]?token|auth[-_]?token|hook[-_]?token|refresh[-_]?token|api[-_]?key|client[-_]?secret|token|key|secret|password|pass|passwd|auth|signature|card[-_]?number|card[-_]?cvc|card[-_]?cvv|cvc|cvv|security[-_]?code|payment[-_]?credential|shared[-_]?payment[-_]?token)=([^&\s"'<>]+)/gi, + /"(?:apiKey|token|secret|password|passwd|accessToken|refreshToken|cardNumber|card_number|cardCvc|card_cvc|cardCvv|card_cvv|cvc|cvv|securityCode|security_code|paymentCredential|payment_credential|sharedPaymentToken|shared_payment_token)"\s*:\s*"([^"]+)"/g, + /(^|[\s,{])["']?(?:api[-_]key|access[-_]token|refresh[-_]token|authToken|auth[-_]token|clientSecret|client[-_]secret|appSecret|app[-_]secret)["']?\s*[:=]\s*(["'])([^"'\r\n]+)\2/gi, + /(^|[\s,{])["']?(?:authorization|proxy-authorization|cookie|set-cookie|x-api-key|x-auth-token)["']?\s*[:=]\s*(["'])([^"'\r\n]+)\2/gi, + /--(?:api[-_]?key|hook[-_]?token|token|secret|password|passwd|card[-_]?number|card[-_]?cvc|card[-_]?cvv|cvc|cvv|security[-_]?code|payment[-_]?credential|shared[-_]?payment[-_]?token)\s+(["']?)([^\s"']+)\1/gi, + /Authorization\s*[:=]\s*Bearer\s+([A-Za-z0-9._\-+=]+)/gi, + /Authorization\s*[:=]\s*Basic\s+([A-Za-z0-9+/=]+)/gi, + /(?:X-OpenClaw-Token|x-pomerium-jwt-assertion|X-Api-Key|X-Auth-Token)\s*[:=]\s*([^\s"',;]+)/gi, + /\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b/g, + /(^|[\s,;])(?:access_token|refresh_token|auth[-_]?token|api[-_]?key|client[-_]?secret|app[-_]?secret|token|secret|password|passwd|card[-_]?number|card[-_]?cvc|card[-_]?cvv|cvc|cvv|security[-_]?code|payment[-_]?credential|shared[-_]?payment[-_]?token)=([^\s&#]+)/gi, + /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g, + /\b(sk-[A-Za-z0-9_-]{8,})\b/g, + /(ghp_[A-Za-z0-9]{20,})/g, + /(github_pat_[A-Za-z0-9_]{20,})/g, + /(xox[baprs]-[A-Za-z0-9-]{10,})/g, + /(xapp-[A-Za-z0-9-]{10,})/g, + /(gsk_[A-Za-z0-9_-]{10,})/g, + /(AIza[0-9A-Za-z\-_]{20,})/g, + /(ya29\.[0-9A-Za-z_\-./+=]{10,})/g, + /(1\/\/0[0-9A-Za-z_\-./+=]{10,})/g, + /(eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})/g, + /(pplx-[A-Za-z0-9_-]{10,})/g, + /(npm_[A-Za-z0-9]{10,})/g, + /(AKID[A-Za-z0-9]{10,})/g, + /(LTAI[A-Za-z0-9]{10,})/g, + /(hf_[A-Za-z0-9]{10,})/g, + /(r8_[A-Za-z0-9]{10,})/g, + /\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, + /\b(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, +]; + +let configuredRedactor: ((value: string) => string) | undefined; + +export function configureAcpErrorRedactor(redactor: ((value: string) => string) | undefined): void { + configuredRedactor = redactor; +} + +export function redactSensitiveText(value: string): string { + if (configuredRedactor) { + return configuredRedactor(value); + } + let redacted = value; + for (const pattern of SECRET_PATTERNS) { + redacted = redacted.replace(pattern, (match, ...args: string[]) => { + if (match.includes("PRIVATE KEY-----")) { + return "[REDACTED_PRIVATE_KEY]"; + } + const groups = args.slice(0, -2); + const token = groups.findLast((group) => typeof group === "string" && group.length > 0); + return token ? match.replace(token, "[REDACTED]") : "[REDACTED]"; + }); + } + return redacted; +} + +/** + * Render a non-Error `cause` value without leaking `[object Object]` or throwing + * while formatting nested ACP runtime failures. + */ +export function stringifyNonErrorCause(value: unknown): string { + if (value === null) { + return "null"; + } + if (typeof value === "string") { + return value; + } + if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") { + return String(value); + } + try { + return JSON.stringify(value); + } catch { + return Object.prototype.toString.call(value); + } +} diff --git a/packages/acp-core/src/index.ts b/packages/acp-core/src/index.ts index b5e9a774fde5..98a73b8b6cd0 100644 --- a/packages/acp-core/src/index.ts +++ b/packages/acp-core/src/index.ts @@ -1,3 +1,14 @@ +export * from "./error-format.js"; +export * from "./meta.js"; export * from "./normalize-text.js"; +export * from "./numeric-options.js"; export * from "./record-shared.js"; +export * from "./session-interaction-mode.js"; +export * from "./session-lineage-meta.js"; +export * from "./session.js"; +export * from "./types.js"; +export * from "./runtime/error-text.js"; +export * from "./runtime/errors.js"; +export * from "./runtime/session-identifiers.js"; +export * from "./runtime/session-identity.js"; export * from "./runtime/types.js"; diff --git a/src/acp/meta.test.ts b/packages/acp-core/src/meta.test.ts similarity index 100% rename from src/acp/meta.test.ts rename to packages/acp-core/src/meta.test.ts diff --git a/src/acp/meta.ts b/packages/acp-core/src/meta.ts similarity index 100% rename from src/acp/meta.ts rename to packages/acp-core/src/meta.ts diff --git a/src/acp/numeric-options.ts b/packages/acp-core/src/numeric-options.ts similarity index 100% rename from src/acp/numeric-options.ts rename to packages/acp-core/src/numeric-options.ts diff --git a/src/acp/runtime/error-text.test.ts b/packages/acp-core/src/runtime/error-text.test.ts similarity index 100% rename from src/acp/runtime/error-text.test.ts rename to packages/acp-core/src/runtime/error-text.test.ts diff --git a/src/acp/runtime/error-text.ts b/packages/acp-core/src/runtime/error-text.ts similarity index 100% rename from src/acp/runtime/error-text.ts rename to packages/acp-core/src/runtime/error-text.ts diff --git a/src/acp/runtime/errors.test.ts b/packages/acp-core/src/runtime/errors.test.ts similarity index 78% rename from src/acp/runtime/errors.test.ts rename to packages/acp-core/src/runtime/errors.test.ts index e91a0d2bb3b9..70cceb44fcaa 100644 --- a/src/acp/runtime/errors.test.ts +++ b/packages/acp-core/src/runtime/errors.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; +import { configureAcpErrorRedactor } from "../error-format.js"; import { AcpRuntimeError, formatAcpErrorChain, @@ -17,6 +18,10 @@ async function expectRejectedAcpRuntimeError(promise: Promise): Promise throw new Error("expected ACP runtime error rejection"); } +afterEach(() => { + configureAcpErrorRedactor(undefined); +}); + describe("withAcpRuntimeErrorBoundary", () => { it("wraps generic errors with fallback code and source message", async () => { const sourceError = new Error("boom"); @@ -155,4 +160,32 @@ describe("formatAcpErrorChain redaction", () => { expect(out).toMatch(/upstream rejected/); expect(out).not.toContain(token); }); + + it("redacts common HTTP, provider, and private-key credentials in ACP error text", () => { + const secrets = [ + "Authorization: Basic dXNlcjpwYXNzd29yZGFiY2RlZg==", + "Bearer eyJabcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz", + "github_pat_abcdefghijklmnopqrstuvwxyz123456", + ["xoxb", "1234567890", "abcdefghijklmnop"].join("-"), + "bot123456789:abcdefghijklmnopqrstuvwxyz123456", + "-----BEGIN PRIVATE KEY-----\nabcdefghijklmnopqrstuvwxyz\n-----END PRIVATE KEY-----", + ]; + const out = formatAcpErrorChain( + new AcpRuntimeError("ACP_TURN_FAILED", `backend failed: ${secrets.join(" ")}`), + ); + + for (const secret of secrets) { + expect(out).not.toContain(secret); + } + expect(out).toContain("backend failed"); + }); + + it("uses a configured host redactor before rendering ACP error text", () => { + configureAcpErrorRedactor((value) => value.replaceAll("custom-secret", "[CUSTOM]")); + + const out = formatAcpErrorChain(new AcpRuntimeError("ACP_TURN_FAILED", "custom-secret")); + + expect(out).toContain("[CUSTOM]"); + expect(out).not.toContain("custom-secret"); + }); }); diff --git a/packages/acp-core/src/runtime/errors.ts b/packages/acp-core/src/runtime/errors.ts new file mode 100644 index 000000000000..abfb5f11fa8c --- /dev/null +++ b/packages/acp-core/src/runtime/errors.ts @@ -0,0 +1,152 @@ +import { redactSensitiveText, stringifyNonErrorCause } from "../error-format.js"; + +export const ACP_ERROR_CODES = [ + "ACP_BACKEND_MISSING", + "ACP_BACKEND_UNAVAILABLE", + "ACP_BACKEND_UNSUPPORTED_CONTROL", + "ACP_DISPATCH_DISABLED", + "ACP_INVALID_RUNTIME_OPTION", + "ACP_SESSION_INIT_FAILED", + "ACP_TURN_FAILED", +] as const; + +export type AcpRuntimeErrorCode = (typeof ACP_ERROR_CODES)[number]; +const ACP_ERROR_CODE_SET = new Set(ACP_ERROR_CODES); + +export class AcpRuntimeError extends Error { + readonly code: AcpRuntimeErrorCode; + override readonly cause?: unknown; + + constructor(code: AcpRuntimeErrorCode, message: string, options?: { cause?: unknown }) { + super(message); + this.name = "AcpRuntimeError"; + this.code = code; + this.cause = options?.cause; + } +} + +function getForeignAcpRuntimeError(value: unknown): { + code: AcpRuntimeErrorCode; + message: string; +} | null { + if (!(value instanceof Error)) { + return null; + } + const code = (value as { code?: unknown }).code; + if (typeof code !== "string" || !ACP_ERROR_CODE_SET.has(code as AcpRuntimeErrorCode)) { + return null; + } + return { + code: code as AcpRuntimeErrorCode, + message: value.message, + }; +} + +function readAcpRequestErrorDetails(value: Error): string | undefined { + const code = (value as { code?: unknown }).code; + if (typeof code !== "number") { + return undefined; + } + const data = (value as { data?: unknown }).data; + if (!data || typeof data !== "object") { + return undefined; + } + const details = (data as { details?: unknown }).details; + if (details === undefined || details === null) { + return undefined; + } + const rendered = redactSensitiveText(stringifyNonErrorCause(details)).trim(); + return rendered.length > 0 ? rendered : undefined; +} + +function messageWithAcpRequestErrorDetails(error: Error): string { + const details = readAcpRequestErrorDetails(error); + if (!details || error.message.includes(details)) { + return error.message; + } + return `${error.message}: ${details}`; +} + +export function isAcpRuntimeError(value: unknown): value is AcpRuntimeError { + return value instanceof AcpRuntimeError || getForeignAcpRuntimeError(value) !== null; +} + +export function toAcpRuntimeError(params: { + error: unknown; + fallbackCode: AcpRuntimeErrorCode; + fallbackMessage: string; +}): AcpRuntimeError { + if (params.error instanceof AcpRuntimeError) { + return params.error; + } + const foreignAcpRuntimeError = getForeignAcpRuntimeError(params.error); + if (foreignAcpRuntimeError) { + return new AcpRuntimeError(foreignAcpRuntimeError.code, foreignAcpRuntimeError.message, { + cause: params.error, + }); + } + if (params.error instanceof Error) { + return new AcpRuntimeError( + params.fallbackCode, + messageWithAcpRequestErrorDetails(params.error), + { + cause: params.error, + }, + ); + } + return new AcpRuntimeError(params.fallbackCode, params.fallbackMessage, { + cause: params.error, + }); +} + +/** + * Render an error and its `.cause` chain as a single human-readable line for + * logs, lifecycle events, and tool results. Format is + * `Name [code]: message <- Name [code]: message <- ...`. Number codes also + * appear, so JSON-RPC error codes like `-32603` survive into surfaces that + * downstream consumers see (gateway logs, telegram replies, tool_result text). + * + * Depth is capped to defend against self-referential `.cause` cycles. + */ +export function formatAcpErrorChain(error: unknown): string { + if (!(error instanceof Error)) { + return redactSensitiveText(String(error)); + } + const segments: string[] = [renderSingleError(error)]; + let current: unknown = (error as unknown as { cause?: unknown }).cause; + let depth = 0; + while (current !== undefined && current !== null && depth < 8) { + if (current instanceof Error) { + segments.push(renderSingleError(current)); + current = (current as unknown as { cause?: unknown }).cause; + } else { + segments.push(stringifyNonErrorCause(current)); + current = undefined; + } + depth += 1; + } + return redactSensitiveText(segments.join(" <- ")); +} + +function renderSingleError(error: Error): string { + const codeValue = (error as unknown as { code?: unknown }).code; + const codeSuffix = + typeof codeValue === "string" || typeof codeValue === "number" ? ` [${codeValue}]` : ""; + return `${error.name}${codeSuffix}: ${error.message}`; +} + +export async function withAcpRuntimeErrorBoundary(params: { + run: () => Promise; + fallbackCode: AcpRuntimeErrorCode; + fallbackMessage: string; +}): Promise { + try { + return await params.run(); + } catch (error) { + throw toAcpRuntimeError({ + error, + fallbackCode: params.fallbackCode, + fallbackMessage: params.fallbackMessage, + }); + } +} diff --git a/src/acp/runtime/session-identifiers.test.ts b/packages/acp-core/src/runtime/session-identifiers.test.ts similarity index 100% rename from src/acp/runtime/session-identifiers.test.ts rename to packages/acp-core/src/runtime/session-identifiers.test.ts diff --git a/src/acp/runtime/session-identifiers.ts b/packages/acp-core/src/runtime/session-identifiers.ts similarity index 96% rename from src/acp/runtime/session-identifiers.ts rename to packages/acp-core/src/runtime/session-identifiers.ts index 444c21500316..215f2a8d7817 100644 --- a/src/acp/runtime/session-identifiers.ts +++ b/packages/acp-core/src/runtime/session-identifiers.ts @@ -1,6 +1,6 @@ -import { normalizeText } from "@openclaw/acp-core/normalize-text"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; -import type { SessionAcpIdentity, SessionAcpMeta } from "../../config/sessions/types.js"; +import { normalizeText } from "../normalize-text.js"; +import type { SessionAcpIdentity, SessionAcpMeta } from "../types.js"; import { isSessionIdentityPending, resolveSessionIdentityFromMeta } from "./session-identity.js"; export const ACP_SESSION_IDENTITY_RENDERER_VERSION = "v1"; diff --git a/src/acp/runtime/session-identity.ts b/packages/acp-core/src/runtime/session-identity.ts similarity index 96% rename from src/acp/runtime/session-identity.ts rename to packages/acp-core/src/runtime/session-identity.ts index 046b67be8a46..1f8bb4f67f02 100644 --- a/src/acp/runtime/session-identity.ts +++ b/packages/acp-core/src/runtime/session-identity.ts @@ -1,10 +1,6 @@ -import { normalizeText } from "@openclaw/acp-core/normalize-text"; -import type { AcpRuntimeHandle, AcpRuntimeStatus } from "@openclaw/acp-core/runtime/types"; -import type { - SessionAcpIdentity, - SessionAcpIdentitySource, - SessionAcpMeta, -} from "../../config/sessions/types.js"; +import { normalizeText } from "../normalize-text.js"; +import type { SessionAcpIdentity, SessionAcpIdentitySource, SessionAcpMeta } from "../types.js"; +import type { AcpRuntimeHandle, AcpRuntimeStatus } from "./types.js"; function normalizeIdentityState(value: unknown): SessionAcpIdentity["state"] | undefined { if (value !== "pending" && value !== "resolved") { diff --git a/src/acp/session-interaction-mode.test.ts b/packages/acp-core/src/session-interaction-mode.test.ts similarity index 100% rename from src/acp/session-interaction-mode.test.ts rename to packages/acp-core/src/session-interaction-mode.test.ts diff --git a/src/acp/session-interaction-mode.ts b/packages/acp-core/src/session-interaction-mode.ts similarity index 92% rename from src/acp/session-interaction-mode.ts rename to packages/acp-core/src/session-interaction-mode.ts index 21d6f3d17a95..cebe3ae7284a 100644 --- a/src/acp/session-interaction-mode.ts +++ b/packages/acp-core/src/session-interaction-mode.ts @@ -1,9 +1,12 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; -import type { SessionEntry } from "../config/sessions/types.js"; type AcpSessionInteractionMode = "interactive" | "parent-owned-background"; -type SessionInteractionEntry = Pick; +type SessionInteractionEntry = { + spawnedBy?: string; + parentSessionKey?: string; + acp?: unknown; +}; function resolveAcpSessionInteractionMode( entry?: SessionInteractionEntry | null, diff --git a/src/acp/session-lineage-meta.test.ts b/packages/acp-core/src/session-lineage-meta.test.ts similarity index 100% rename from src/acp/session-lineage-meta.test.ts rename to packages/acp-core/src/session-lineage-meta.test.ts diff --git a/src/acp/session-lineage-meta.ts b/packages/acp-core/src/session-lineage-meta.ts similarity index 87% rename from src/acp/session-lineage-meta.ts rename to packages/acp-core/src/session-lineage-meta.ts index bcde134719f7..8826143dcf45 100644 --- a/src/acp/session-lineage-meta.ts +++ b/packages/acp-core/src/session-lineage-meta.ts @@ -1,5 +1,4 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; -import type { GatewaySessionRow } from "../gateway/session-utils.js"; const SUBAGENT_ROLES = ["orchestrator", "leaf"] as const; const SUBAGENT_CONTROL_SCOPES = ["children", "none"] as const; @@ -20,19 +19,18 @@ export type AcpSessionLineageMeta = { spawnedCwd?: string; }; -export type AcpSessionLineageRow = Pick< - GatewaySessionRow, - | "key" - | "kind" - | "channel" - | "parentSessionKey" - | "spawnedBy" - | "spawnDepth" - | "subagentRole" - | "subagentControlScope" - | "spawnedWorkspaceDir" - | "spawnedCwd" ->; +export type AcpSessionLineageRow = { + key: string; + kind?: string; + channel?: string; + parentSessionKey?: string; + spawnedBy?: string; + spawnDepth?: number; + subagentRole?: string; + subagentControlScope?: string; + spawnedWorkspaceDir?: string; + spawnedCwd?: string; +}; function readInteger(value: unknown): number | undefined { if (typeof value !== "number" || !Number.isInteger(value) || value < 0) { diff --git a/src/acp/session.test.ts b/packages/acp-core/src/session.test.ts similarity index 100% rename from src/acp/session.test.ts rename to packages/acp-core/src/session.test.ts diff --git a/src/acp/session.ts b/packages/acp-core/src/session.ts similarity index 100% rename from src/acp/session.ts rename to packages/acp-core/src/session.ts diff --git a/packages/acp-core/src/types.ts b/packages/acp-core/src/types.ts new file mode 100644 index 000000000000..a974d6e93c55 --- /dev/null +++ b/packages/acp-core/src/types.ts @@ -0,0 +1,92 @@ +import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; + +const ACP_PROVENANCE_MODE_VALUES = ["off", "meta", "meta+receipt"] as const; + +export type SessionId = string; + +export type AcpProvenanceMode = (typeof ACP_PROVENANCE_MODE_VALUES)[number]; + +export function normalizeAcpProvenanceMode( + value: string | undefined, +): AcpProvenanceMode | undefined { + const normalized = normalizeOptionalLowercaseString(value); + if (!normalized) { + return undefined; + } + return (ACP_PROVENANCE_MODE_VALUES as readonly string[]).includes(normalized) + ? (normalized as AcpProvenanceMode) + : undefined; +} + +export type AcpSession = { + sessionId: SessionId; + sessionKey: string; + ledgerSessionId?: string; + cwd: string; + createdAt: number; + lastTouchedAt: number; + abortController: AbortController | null; + activeRunId: string | null; +}; + +export type AcpServerOptions = { + gatewayUrl?: string; + gatewayToken?: string; + gatewayPassword?: string; + defaultSessionKey?: string; + defaultSessionLabel?: string; + requireExistingSession?: boolean; + resetSession?: boolean; + prefixCwd?: boolean; + provenanceMode?: AcpProvenanceMode; + sessionCreateRateLimit?: { + maxRequests?: number; + windowMs?: number; + }; + verbose?: boolean; +}; + +export type SessionAcpIdentitySource = "ensure" | "status" | "event"; + +export type SessionAcpIdentityState = "pending" | "resolved"; + +export type SessionAcpIdentity = { + state: SessionAcpIdentityState; + acpxRecordId?: string; + acpxSessionId?: string; + agentSessionId?: string; + source: SessionAcpIdentitySource; + lastUpdatedAt: number; +}; + +export type AcpSessionRuntimeOptions = { + /** + * ACP runtime mode set via session/set_mode (for example: "plan", "normal", "auto"). + */ + runtimeMode?: string; + /** ACP runtime config option: model id. */ + model?: string; + /** ACP runtime config option: thinking/reasoning effort. */ + thinking?: string; + /** Working directory override for ACP session turns. */ + cwd?: string; + /** ACP runtime config option: permission profile id. */ + permissionProfile?: string; + /** ACP runtime config option: per-turn timeout in seconds. */ + timeoutSeconds?: number; + /** Backend-specific option bag mapped through session/set_config_option. */ + backendExtras?: Record; +}; + +export type SessionAcpMeta = { + backend: string; + agent: string; + runtimeSessionName: string; + identity?: SessionAcpIdentity; + mode: "persistent" | "oneshot"; + runtimeOptions?: AcpSessionRuntimeOptions; + cwd?: string; + state: "idle" | "running" | "error"; + lastActivityAt: number; + lastError?: string; +}; diff --git a/scripts/lib/extension-package-boundary.ts b/scripts/lib/extension-package-boundary.ts index 555eb9d24de4..a6bcb28432b8 100644 --- a/scripts/lib/extension-package-boundary.ts +++ b/scripts/lib/extension-package-boundary.ts @@ -154,6 +154,9 @@ export const EXTENSION_PACKAGE_BOUNDARY_BASE_PATHS = { "@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", ], diff --git a/scripts/prepare-extension-package-boundary-artifacts.mjs b/scripts/prepare-extension-package-boundary-artifacts.mjs index 1b4610e09980..9f70b5ec4235 100644 --- a/scripts/prepare-extension-package-boundary-artifacts.mjs +++ b/scripts/prepare-extension-package-boundary-artifacts.mjs @@ -8,8 +8,10 @@ const runTsgoScript = path.join(repoRoot, "scripts/run-tsgo.mjs"); const TYPE_INPUT_EXTENSIONS = new Set([".ts", ".tsx", ".d.ts", ".js", ".mjs", ".json"]); const VALID_MODES = new Set(["all", "package-boundary"]); const ROOT_SHIMS_TIMEOUT_MS = resolveBoundaryRootShimsTimeoutMs(process.env); +const ROOT_SHIMS_MAX_OLD_SPACE_SIZE = + process.env.OPENCLAW_ROOT_SHIMS_MAX_OLD_SPACE_SIZE?.trim() || "8192"; const ROOT_SHIMS_NODE_OPTIONS = - `${process.env.NODE_OPTIONS ?? ""} --max-old-space-size=4096`.trim(); + `${process.env.NODE_OPTIONS ?? ""} --max-old-space-size=${ROOT_SHIMS_MAX_OLD_SPACE_SIZE}`.trim(); const PLUGIN_SDK_TYPE_INPUTS = [ "tsconfig.json", @@ -64,6 +66,10 @@ 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", "dist/plugin-sdk/packages/terminal-core/src/ansi.d.ts", "dist/plugin-sdk/packages/terminal-core/src/decorative-emoji.d.ts", @@ -122,6 +128,10 @@ 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", "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", diff --git a/src/acp/control-plane/manager.core.ts b/src/acp/control-plane/manager.core.ts index f5a0f6a96bf2..40c73f14d6b2 100644 --- a/src/acp/control-plane/manager.core.ts +++ b/src/acp/control-plane/manager.core.ts @@ -1,3 +1,13 @@ +import { + createIdentityFromEnsure, + identityHasStableSessionId, + identityEquals, + isSessionIdentityPending, + mergeSessionIdentity, + resolveRuntimeResumeSessionId, + resolveRuntimeHandleIdentifiersFromIdentity, + resolveSessionIdentityFromMeta, +} from "@openclaw/acp-core/runtime/session-identity"; import type { AcpRuntime, AcpRuntimeCapabilities, @@ -29,16 +39,6 @@ import { withAcpRuntimeErrorBoundary, } from "../runtime/errors.js"; import type { AcpRuntimeErrorCode } from "../runtime/errors.js"; -import { - createIdentityFromEnsure, - identityHasStableSessionId, - identityEquals, - isSessionIdentityPending, - mergeSessionIdentity, - resolveRuntimeResumeSessionId, - resolveRuntimeHandleIdentifiersFromIdentity, - resolveSessionIdentityFromMeta, -} from "../runtime/session-identity.js"; import { clearAcpTurnActive, markAcpTurnActive } from "./active-turns.js"; import { reconcileManagerRuntimeSessionIdentifiers } from "./manager.identity-reconcile.js"; import { diff --git a/src/acp/control-plane/manager.identity-reconcile.ts b/src/acp/control-plane/manager.identity-reconcile.ts index c7a7ebbb512e..c28defa8bb28 100644 --- a/src/acp/control-plane/manager.identity-reconcile.ts +++ b/src/acp/control-plane/manager.identity-reconcile.ts @@ -1,3 +1,11 @@ +import { + createIdentityFromHandleEvent, + createIdentityFromStatus, + identityEquals, + mergeSessionIdentity, + resolveRuntimeHandleIdentifiersFromIdentity, + resolveSessionIdentityFromMeta, +} from "@openclaw/acp-core/runtime/session-identity"; import type { AcpRuntime, AcpRuntimeHandle, @@ -6,14 +14,6 @@ import type { import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { logVerbose } from "../../globals.js"; import { withAcpRuntimeErrorBoundary } from "../runtime/errors.js"; -import { - createIdentityFromHandleEvent, - createIdentityFromStatus, - identityEquals, - mergeSessionIdentity, - resolveRuntimeHandleIdentifiersFromIdentity, - resolveSessionIdentityFromMeta, -} from "../runtime/session-identity.js"; import type { SessionAcpMeta, SessionEntry } from "./manager.types.js"; import { hasLegacyAcpIdentityProjection } from "./manager.utils.js"; diff --git a/src/acp/event-ledger.ts b/src/acp/event-ledger.ts index 6669f676edc5..ef6e66c6f5e2 100644 --- a/src/acp/event-ledger.ts +++ b/src/acp/event-ledger.ts @@ -1,11 +1,11 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { ContentBlock, SessionUpdate } from "@agentclientprotocol/sdk"; +import { resolveIntegerOption } from "@openclaw/acp-core/numeric-options"; import { resolveStateDir } from "../config/paths.js"; import { withFileLock } from "../infra/file-lock.js"; import { readJsonFile, writeTextAtomic } from "../infra/json-files.js"; import { isRecord } from "../utils.js"; -import { resolveIntegerOption } from "./numeric-options.js"; const LEDGER_VERSION = 1; const DEFAULT_MAX_SESSIONS = 200; diff --git a/src/acp/runtime/errors.ts b/src/acp/runtime/errors.ts index d37e0991c756..6f08d4056dad 100644 --- a/src/acp/runtime/errors.ts +++ b/src/acp/runtime/errors.ts @@ -1,153 +1,6 @@ -import { stringifyNonErrorCause } from "../../infra/errors.js"; +import { configureAcpErrorRedactor } from "@openclaw/acp-core"; import { redactSensitiveText } from "../../logging/redact.js"; -export const ACP_ERROR_CODES = [ - "ACP_BACKEND_MISSING", - "ACP_BACKEND_UNAVAILABLE", - "ACP_BACKEND_UNSUPPORTED_CONTROL", - "ACP_DISPATCH_DISABLED", - "ACP_INVALID_RUNTIME_OPTION", - "ACP_SESSION_INIT_FAILED", - "ACP_TURN_FAILED", -] as const; +configureAcpErrorRedactor(redactSensitiveText); -export type AcpRuntimeErrorCode = (typeof ACP_ERROR_CODES)[number]; -const ACP_ERROR_CODE_SET = new Set(ACP_ERROR_CODES); - -export class AcpRuntimeError extends Error { - readonly code: AcpRuntimeErrorCode; - override readonly cause?: unknown; - - constructor(code: AcpRuntimeErrorCode, message: string, options?: { cause?: unknown }) { - super(message); - this.name = "AcpRuntimeError"; - this.code = code; - this.cause = options?.cause; - } -} - -function getForeignAcpRuntimeError(value: unknown): { - code: AcpRuntimeErrorCode; - message: string; -} | null { - if (!(value instanceof Error)) { - return null; - } - const code = (value as { code?: unknown }).code; - if (typeof code !== "string" || !ACP_ERROR_CODE_SET.has(code as AcpRuntimeErrorCode)) { - return null; - } - return { - code: code as AcpRuntimeErrorCode, - message: value.message, - }; -} - -function readAcpRequestErrorDetails(value: Error): string | undefined { - const code = (value as { code?: unknown }).code; - if (typeof code !== "number") { - return undefined; - } - const data = (value as { data?: unknown }).data; - if (!data || typeof data !== "object") { - return undefined; - } - const details = (data as { details?: unknown }).details; - if (details === undefined || details === null) { - return undefined; - } - const rendered = redactSensitiveText(stringifyNonErrorCause(details)).trim(); - return rendered.length > 0 ? rendered : undefined; -} - -function messageWithAcpRequestErrorDetails(error: Error): string { - const details = readAcpRequestErrorDetails(error); - if (!details || error.message.includes(details)) { - return error.message; - } - return `${error.message}: ${details}`; -} - -export function isAcpRuntimeError(value: unknown): value is AcpRuntimeError { - return value instanceof AcpRuntimeError || getForeignAcpRuntimeError(value) !== null; -} - -export function toAcpRuntimeError(params: { - error: unknown; - fallbackCode: AcpRuntimeErrorCode; - fallbackMessage: string; -}): AcpRuntimeError { - if (params.error instanceof AcpRuntimeError) { - return params.error; - } - const foreignAcpRuntimeError = getForeignAcpRuntimeError(params.error); - if (foreignAcpRuntimeError) { - return new AcpRuntimeError(foreignAcpRuntimeError.code, foreignAcpRuntimeError.message, { - cause: params.error, - }); - } - if (params.error instanceof Error) { - return new AcpRuntimeError( - params.fallbackCode, - messageWithAcpRequestErrorDetails(params.error), - { - cause: params.error, - }, - ); - } - return new AcpRuntimeError(params.fallbackCode, params.fallbackMessage, { - cause: params.error, - }); -} - -/** - * Render an error and its `.cause` chain as a single human-readable line for - * logs, lifecycle events, and tool results. Format is - * `Name [code]: message <- Name [code]: message <- ...`. Number codes also - * appear, so JSON-RPC error codes like `-32603` survive into surfaces that - * downstream consumers see (gateway logs, telegram replies, tool_result text). - * - * Depth is capped to defend against self-referential `.cause` cycles. - */ -export function formatAcpErrorChain(error: unknown): string { - if (!(error instanceof Error)) { - return redactSensitiveText(String(error)); - } - const segments: string[] = [renderSingleError(error)]; - let current: unknown = (error as unknown as { cause?: unknown }).cause; - let depth = 0; - while (current !== undefined && current !== null && depth < 8) { - if (current instanceof Error) { - segments.push(renderSingleError(current)); - current = (current as unknown as { cause?: unknown }).cause; - } else { - segments.push(stringifyNonErrorCause(current)); - current = undefined; - } - depth += 1; - } - return redactSensitiveText(segments.join(" <- ")); -} - -function renderSingleError(error: Error): string { - const codeValue = (error as unknown as { code?: unknown }).code; - const codeSuffix = - typeof codeValue === "string" || typeof codeValue === "number" ? ` [${codeValue}]` : ""; - return `${error.name}${codeSuffix}: ${error.message}`; -} - -export async function withAcpRuntimeErrorBoundary(params: { - run: () => Promise; - fallbackCode: AcpRuntimeErrorCode; - fallbackMessage: string; -}): Promise { - try { - return await params.run(); - } catch (error) { - throw toAcpRuntimeError({ - error, - fallbackCode: params.fallbackCode, - fallbackMessage: params.fallbackMessage, - }); - } -} +export * from "@openclaw/acp-core/runtime/errors"; diff --git a/src/acp/session-mapper.ts b/src/acp/session-mapper.ts index 356be60d2087..aa5d72cf6543 100644 --- a/src/acp/session-mapper.ts +++ b/src/acp/session-mapper.ts @@ -1,6 +1,6 @@ +import { readBool, readString } from "@openclaw/acp-core/meta"; +import type { AcpServerOptions } from "@openclaw/acp-core/types"; import type { GatewayClient } from "../gateway/client.js"; -import { readBool, readString } from "./meta.js"; -import type { AcpServerOptions } from "./types.js"; type AcpSessionMeta = { sessionKey?: string; diff --git a/src/acp/translator.cancel-scoping.test.ts b/src/acp/translator.cancel-scoping.test.ts index 1b892a0c6690..2b10577bd5ae 100644 --- a/src/acp/translator.cancel-scoping.test.ts +++ b/src/acp/translator.cancel-scoping.test.ts @@ -1,8 +1,8 @@ import type { CancelNotification, PromptRequest, PromptResponse } from "@agentclientprotocol/sdk"; +import { createInMemorySessionStore } from "@openclaw/acp-core/session"; import { describe, expect, it, vi } from "vitest"; import type { EventFrame } from "../../packages/gateway-protocol/src/index.js"; import type { GatewayClient } from "../gateway/client.js"; -import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; import { createAcpConnection, createAcpGateway } from "./translator.test-helpers.js"; diff --git a/src/acp/translator.event-ledger.test.ts b/src/acp/translator.event-ledger.test.ts index b060745fff81..c4d1298ef6ab 100644 --- a/src/acp/translator.event-ledger.test.ts +++ b/src/acp/translator.event-ledger.test.ts @@ -3,11 +3,11 @@ import type { NewSessionRequest, PromptRequest, } from "@agentclientprotocol/sdk"; +import { createInMemorySessionStore } from "@openclaw/acp-core/session"; import { describe, expect, it, vi } from "vitest"; import type { EventFrame } from "../../packages/gateway-protocol/src/index.js"; import type { GatewayClient } from "../gateway/client.js"; import { createInMemoryAcpEventLedger, type AcpEventLedger } from "./event-ledger.js"; -import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; import { createAcpConnection, createAcpGateway } from "./translator.test-helpers.js"; diff --git a/src/acp/translator.lifecycle.test.ts b/src/acp/translator.lifecycle.test.ts index 5736f9163303..25c46ec29fdf 100644 --- a/src/acp/translator.lifecycle.test.ts +++ b/src/acp/translator.lifecycle.test.ts @@ -7,10 +7,10 @@ import type { ResumeSessionRequest, } from "@agentclientprotocol/sdk"; import { PROTOCOL_VERSION } from "@agentclientprotocol/sdk"; +import { createInMemorySessionStore } from "@openclaw/acp-core/session"; import { describe, expect, it, vi } from "vitest"; import type { GatewayClient } from "../gateway/client.js"; import type { GatewaySessionRow } from "../gateway/session-utils.js"; -import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; import { createAcpConnection, createAcpGateway } from "./translator.test-helpers.js"; diff --git a/src/acp/translator.permission-relay.test.ts b/src/acp/translator.permission-relay.test.ts index 75e5d431fa76..35eebd0fcdca 100644 --- a/src/acp/translator.permission-relay.test.ts +++ b/src/acp/translator.permission-relay.test.ts @@ -1,8 +1,8 @@ import type { CancelNotification } from "@agentclientprotocol/sdk"; +import { createInMemorySessionStore } from "@openclaw/acp-core/session"; import { describe, expect, it, vi } from "vitest"; import type { EventFrame } from "../../packages/gateway-protocol/src/index.js"; import type { GatewayClient } from "../gateway/client.js"; -import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; import { promptAgent } from "./translator.prompt-harness.test-support.js"; import { createAcpConnection, createAcpGateway } from "./translator.test-helpers.js"; diff --git a/src/acp/translator.prompt-harness.test-support.ts b/src/acp/translator.prompt-harness.test-support.ts index d809559de9ca..05188843123e 100644 --- a/src/acp/translator.prompt-harness.test-support.ts +++ b/src/acp/translator.prompt-harness.test-support.ts @@ -1,8 +1,8 @@ import type { PromptRequest } from "@agentclientprotocol/sdk"; +import { createInMemorySessionStore } from "@openclaw/acp-core/session"; import { expect, vi } from "vitest"; import type { EventFrame } from "../../packages/gateway-protocol/src/index.js"; import type { GatewayClient } from "../gateway/client.js"; -import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; import { createAcpConnection, createAcpGateway } from "./translator.test-helpers.js"; diff --git a/src/acp/translator.prompt-prefix.test.ts b/src/acp/translator.prompt-prefix.test.ts index ecf57bd22066..416e071cbc25 100644 --- a/src/acp/translator.prompt-prefix.test.ts +++ b/src/acp/translator.prompt-prefix.test.ts @@ -1,9 +1,9 @@ import os from "node:os"; import path from "node:path"; import type { PromptRequest } from "@agentclientprotocol/sdk"; +import { createInMemorySessionStore } from "@openclaw/acp-core/session"; import { describe, expect, it, vi } from "vitest"; import type { GatewayClient } from "../gateway/client.js"; -import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; import { createAcpConnection, createAcpGateway } from "./translator.test-helpers.js"; diff --git a/src/acp/translator.session-lineage-meta.test.ts b/src/acp/translator.session-lineage-meta.test.ts index 136370a09078..87c8e46264de 100644 --- a/src/acp/translator.session-lineage-meta.test.ts +++ b/src/acp/translator.session-lineage-meta.test.ts @@ -1,7 +1,7 @@ import type { ListSessionsRequest, LoadSessionRequest } from "@agentclientprotocol/sdk"; +import { createInMemorySessionStore } from "@openclaw/acp-core/session"; import { describe, expect, it, vi } from "vitest"; import type { GatewayClient } from "../gateway/client.js"; -import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; import { createAcpConnection, createAcpGateway } from "./translator.test-helpers.js"; diff --git a/src/acp/translator.session-rate-limit.test.ts b/src/acp/translator.session-rate-limit.test.ts index ba2d1e5f2ef9..4faa980c4d91 100644 --- a/src/acp/translator.session-rate-limit.test.ts +++ b/src/acp/translator.session-rate-limit.test.ts @@ -5,10 +5,10 @@ import type { SetSessionConfigOptionRequest, SetSessionModeRequest, } from "@agentclientprotocol/sdk"; +import { createInMemorySessionStore } from "@openclaw/acp-core/session"; import { describe, expect, it, vi } from "vitest"; import type { EventFrame } from "../../packages/gateway-protocol/src/index.js"; import type { GatewayClient } from "../gateway/client.js"; -import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; import { createAcpConnection, createAcpGateway } from "./translator.test-helpers.js"; diff --git a/src/acp/translator.set-session-mode.test.ts b/src/acp/translator.set-session-mode.test.ts index 42456929e935..944c43fd936b 100644 --- a/src/acp/translator.set-session-mode.test.ts +++ b/src/acp/translator.set-session-mode.test.ts @@ -1,7 +1,7 @@ import type { SetSessionModeRequest } from "@agentclientprotocol/sdk"; +import { createInMemorySessionStore } from "@openclaw/acp-core/session"; import { describe, expect, it } from "vitest"; import type { GatewayClient } from "../gateway/client.js"; -import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; import { createAcpConnection, createAcpGateway } from "./translator.test-helpers.js"; diff --git a/src/acp/translator.stop-reason.test.ts b/src/acp/translator.stop-reason.test.ts index b474284e554f..38d19a52985a 100644 --- a/src/acp/translator.stop-reason.test.ts +++ b/src/acp/translator.stop-reason.test.ts @@ -1,7 +1,7 @@ import type { PromptRequest } from "@agentclientprotocol/sdk"; +import { createInMemorySessionStore } from "@openclaw/acp-core/session"; import { describe, expect, it, vi } from "vitest"; import type { GatewayClient } from "../gateway/client.js"; -import { createInMemorySessionStore } from "./session.js"; import { AcpGatewayAgent } from "./translator.js"; import { createChatEvent, diff --git a/src/acp/translator.ts b/src/acp/translator.ts index bd5c3526c8fc..3e9249961f03 100644 --- a/src/acp/translator.ts +++ b/src/acp/translator.ts @@ -33,6 +33,12 @@ import type { ToolCallLocation, ToolKind, } 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 { 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"; @@ -58,7 +64,6 @@ import { formatToolTitle, inferToolKind, } from "./event-mapper.js"; -import { readBool, readNonNegativeInteger, readNumber, readString } from "./meta.js"; import { buildAcpPermissionRequest, parseGatewayExecApprovalEventData, @@ -68,9 +73,7 @@ import { type GatewayExecApprovalDetails, type GatewayExecApprovalEvent, } from "./permission-relay.js"; -import { toAcpSessionLineageMeta, type AcpSessionLineageMeta } from "./session-lineage-meta.js"; import { parseSessionMeta, resetSessionIfNeeded, resolveSessionKey } from "./session-mapper.js"; -import { defaultAcpSessionStore, type AcpSessionStore } from "./session.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) diff --git a/src/acp/types.ts b/src/acp/types.ts index a48fc9a0a900..7cd228682044 100644 --- a/src/acp/types.ts +++ b/src/acp/types.ts @@ -1,51 +1,7 @@ -import type { SessionId } from "@agentclientprotocol/sdk"; -import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce"; +export type { AcpProvenanceMode, AcpServerOptions, AcpSession } from "@openclaw/acp-core/types"; +export { normalizeAcpProvenanceMode } from "@openclaw/acp-core/types"; import { VERSION } from "../version.js"; -const ACP_PROVENANCE_MODE_VALUES = ["off", "meta", "meta+receipt"] as const; - -type AcpProvenanceMode = (typeof ACP_PROVENANCE_MODE_VALUES)[number]; - -export function normalizeAcpProvenanceMode( - value: string | undefined, -): AcpProvenanceMode | undefined { - const normalized = normalizeOptionalLowercaseString(value); - if (!normalized) { - return undefined; - } - return (ACP_PROVENANCE_MODE_VALUES as readonly string[]).includes(normalized) - ? (normalized as AcpProvenanceMode) - : undefined; -} - -export type AcpSession = { - sessionId: SessionId; - sessionKey: string; - ledgerSessionId?: string; - cwd: string; - createdAt: number; - lastTouchedAt: number; - abortController: AbortController | null; - activeRunId: string | null; -}; - -export type AcpServerOptions = { - gatewayUrl?: string; - gatewayToken?: string; - gatewayPassword?: string; - defaultSessionKey?: string; - defaultSessionLabel?: string; - requireExistingSession?: boolean; - resetSession?: boolean; - prefixCwd?: boolean; - provenanceMode?: AcpProvenanceMode; - sessionCreateRateLimit?: { - maxRequests?: number; - windowMs?: number; - }; - verbose?: boolean; -}; - export const ACP_AGENT_INFO = { name: "openclaw-acp", title: "OpenClaw ACP Gateway", diff --git a/src/agents/acp-spawn.ts b/src/agents/acp-spawn.ts index f14e252c0a7a..0c0ac9259e60 100644 --- a/src/agents/acp-spawn.ts +++ b/src/agents/acp-spawn.ts @@ -1,5 +1,9 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; +import { + resolveAcpSessionCwd, + resolveAcpThreadSessionDetailLines, +} from "@openclaw/acp-core/runtime/session-identifiers"; import type { AcpRuntimeSessionMode } from "@openclaw/acp-core/runtime/types"; import { normalizeOptionalLowercaseString, @@ -12,10 +16,6 @@ import { type AcpSpawnRuntimeCloseHandle, } from "../acp/control-plane/spawn.js"; import { isAcpEnabledByPolicy, resolveAcpAgentPolicyError } from "../acp/policy.js"; -import { - resolveAcpSessionCwd, - resolveAcpThreadSessionDetailLines, -} from "../acp/runtime/session-identifiers.js"; import { DEFAULT_HEARTBEAT_EVERY } from "../auto-reply/heartbeat.js"; import { resolveChannelDefaultBindingPlacement, diff --git a/src/agents/agent-command.live-model-switch.test.ts b/src/agents/agent-command.live-model-switch.test.ts index 08c70c6e2b9a..abc66ca18743 100644 --- a/src/agents/agent-command.live-model-switch.test.ts +++ b/src/agents/agent-command.live-model-switch.test.ts @@ -157,7 +157,7 @@ vi.mock("../acp/runtime/errors.js", () => ({ error instanceof Error ? error : new Error(String(error)), })); -vi.mock("../acp/runtime/session-identifiers.js", () => ({ +vi.mock("@openclaw/acp-core/runtime/session-identifiers", () => ({ resolveAcpSessionCwd: () => "/tmp", })); diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index d9a2fc5b3064..a7d453f4b9f9 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -125,7 +125,7 @@ type AgentAttemptResult = Awaited import("../acp/runtime/errors.js"), ); const acpSessionIdentifiersRuntimeLoader = createLazyImportLoader( - () => import("../acp/runtime/session-identifiers.js"), + () => import("@openclaw/acp-core/runtime/session-identifiers"), ); const deliveryRuntimeLoader = createLazyImportLoader( () => import("./command/delivery.runtime.js"), diff --git a/src/agents/tools/sessions-send-tool.ts b/src/agents/tools/sessions-send-tool.ts index e4705efdcd4a..6fde982365f3 100644 --- a/src/agents/tools/sessions-send-tool.ts +++ b/src/agents/tools/sessions-send-tool.ts @@ -1,8 +1,8 @@ import crypto from "node:crypto"; +import { isRequesterParentOfBackgroundAcpSession } from "@openclaw/acp-core/session-interaction-mode"; import { finiteSecondsToTimerSafeMilliseconds } from "@openclaw/normalization-core/number-coercion"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { Type } from "typebox"; -import { isRequesterParentOfBackgroundAcpSession } from "../../acp/session-interaction-mode.js"; import { parseSessionThreadInfoFast } from "../../config/sessions/thread-info.js"; import type { SessionEntry } from "../../config/sessions/types.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; diff --git a/src/auto-reply/reply/commands-acp.test.ts b/src/auto-reply/reply/commands-acp.test.ts index b375adac30e1..a003558fb7c1 100644 --- a/src/auto-reply/reply/commands-acp.test.ts +++ b/src/auto-reply/reply/commands-acp.test.ts @@ -98,10 +98,7 @@ vi.mock("../../agents/acp-spawn.js", () => ({ params.cfg?.agents?.defaults?.sandbox?.mode === "all" ? 'Sandboxed sessions cannot spawn ACP sessions because runtime="acp" runs on the host. Use runtime="subagent" from sandboxed sessions.' : undefined, - resolveRuntimeCwdForAcpSpawn: async (params: { - explicitCwd?: string; - resolvedCwd?: string; - }) => { + resolveRuntimeCwdForAcpSpawn: async (params: { explicitCwd?: string; resolvedCwd?: string }) => { if (params.explicitCwd) { return params.resolvedCwd; } diff --git a/src/auto-reply/reply/commands-acp/diagnostics.ts b/src/auto-reply/reply/commands-acp/diagnostics.ts index cc3062b9deae..01fed41c53e0 100644 --- a/src/auto-reply/reply/commands-acp/diagnostics.ts +++ b/src/auto-reply/reply/commands-acp/diagnostics.ts @@ -1,9 +1,9 @@ +import { formatAcpRuntimeErrorText } from "@openclaw/acp-core/runtime/error-text"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, } from "@openclaw/normalization-core/string-coerce"; import { getAcpSessionManager } from "../../../acp/control-plane/manager.js"; -import { formatAcpRuntimeErrorText } from "../../../acp/runtime/error-text.js"; import { toAcpRuntimeError } from "../../../acp/runtime/errors.js"; import { getAcpRuntimeBackend, requireAcpRuntimeBackend } from "../../../acp/runtime/registry.js"; import { resolveSessionStorePathForAcp } from "../../../acp/runtime/session-meta.js"; diff --git a/src/auto-reply/reply/commands-acp/lifecycle.ts b/src/auto-reply/reply/commands-acp/lifecycle.ts index 565c9e2764d8..b350dafb1430 100644 --- a/src/auto-reply/reply/commands-acp/lifecycle.ts +++ b/src/auto-reply/reply/commands-acp/lifecycle.ts @@ -1,4 +1,8 @@ import { randomUUID } from "node:crypto"; +import { + resolveAcpSessionCwd, + resolveAcpThreadSessionDetailLines, +} from "@openclaw/acp-core/runtime/session-identifiers"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { getAcpSessionManager } from "../../../acp/control-plane/manager.js"; import { resolveAcpSessionResolutionError } from "../../../acp/control-plane/manager.utils.js"; @@ -12,10 +16,6 @@ import { resolveAcpDispatchPolicyError, resolveAcpDispatchPolicyMessage, } from "../../../acp/policy.js"; -import { - resolveAcpSessionCwd, - resolveAcpThreadSessionDetailLines, -} from "../../../acp/runtime/session-identifiers.js"; import { resolveAcpSpawnRuntimePolicyError, resolveRuntimeCwdForAcpSpawn, diff --git a/src/auto-reply/reply/commands-acp/runtime-options.ts b/src/auto-reply/reply/commands-acp/runtime-options.ts index 4be41083065f..3e1366577869 100644 --- a/src/auto-reply/reply/commands-acp/runtime-options.ts +++ b/src/auto-reply/reply/commands-acp/runtime-options.ts @@ -1,3 +1,4 @@ +import { resolveAcpSessionIdentifierLinesFromIdentity } from "@openclaw/acp-core/runtime/session-identifiers"; import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { getAcpSessionManager } from "../../../acp/control-plane/manager.js"; @@ -9,7 +10,6 @@ import { validateRuntimeModelInput, validateRuntimePermissionProfileInput, } from "../../../acp/control-plane/runtime-options.js"; -import { resolveAcpSessionIdentifierLinesFromIdentity } from "../../../acp/runtime/session-identifiers.js"; import { findLatestTaskForRelatedSessionKeyForOwner } from "../../../tasks/task-owner-access.js"; import { sanitizeTaskStatusText } from "../../../tasks/task-status.js"; import type { CommandHandlerResult, HandleCommandsParams } from "../commands-types.js"; diff --git a/src/auto-reply/reply/commands-acp/shared.ts b/src/auto-reply/reply/commands-acp/shared.ts index 06ec17077890..ffc56cf91a2f 100644 --- a/src/auto-reply/reply/commands-acp/shared.ts +++ b/src/auto-reply/reply/commands-acp/shared.ts @@ -1,10 +1,10 @@ import { randomUUID } from "node:crypto"; +import { toAcpRuntimeErrorText } from "@openclaw/acp-core/runtime/error-text"; import type { AcpRuntimeSessionMode } from "@openclaw/acp-core/runtime/types"; import { normalizeOptionalLowercaseString, normalizeOptionalString, } from "@openclaw/normalization-core/string-coerce"; -import { toAcpRuntimeErrorText } from "../../../acp/runtime/error-text.js"; import type { AcpRuntimeError } from "../../../acp/runtime/errors.js"; import { supportsAutomaticThreadBindingSpawn } from "../../../channels/thread-bindings-policy.js"; import type { AcpSessionRuntimeOptions } from "../../../config/sessions/types.js"; diff --git a/src/auto-reply/reply/commands-subagents-focus.test.ts b/src/auto-reply/reply/commands-subagents-focus.test.ts index fe085a1b4f59..7143e725f819 100644 --- a/src/auto-reply/reply/commands-subagents-focus.test.ts +++ b/src/auto-reply/reply/commands-subagents-focus.test.ts @@ -41,7 +41,7 @@ function buildFocusSessionBindingService() { }; } -vi.mock("../../acp/runtime/session-identifiers.js", () => ({ +vi.mock("@openclaw/acp-core/runtime/session-identifiers", () => ({ resolveAcpSessionCwd: () => undefined, resolveAcpThreadSessionDetailLines: (params: { meta?: { identity?: Record }; diff --git a/src/auto-reply/reply/commands-subagents/action-focus.ts b/src/auto-reply/reply/commands-subagents/action-focus.ts index 8f9658fcb8af..630812554c8e 100644 --- a/src/auto-reply/reply/commands-subagents/action-focus.ts +++ b/src/auto-reply/reply/commands-subagents/action-focus.ts @@ -1,8 +1,8 @@ -import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { resolveAcpSessionCwd, resolveAcpThreadSessionDetailLines, -} from "../../../acp/runtime/session-identifiers.js"; +} from "@openclaw/acp-core/runtime/session-identifiers"; +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { readAcpSessionEntry } from "../../../acp/runtime/session-meta.js"; import { normalizeChatType } from "../../../channels/chat-type.js"; import { diff --git a/src/auto-reply/reply/dispatch-acp-transcript.runtime.ts b/src/auto-reply/reply/dispatch-acp-transcript.runtime.ts index 9d80dc0c0354..746980048328 100644 --- a/src/auto-reply/reply/dispatch-acp-transcript.runtime.ts +++ b/src/auto-reply/reply/dispatch-acp-transcript.runtime.ts @@ -1,4 +1,4 @@ -import { resolveAcpSessionCwd } from "../../acp/runtime/session-identifiers.js"; +import { resolveAcpSessionCwd } from "@openclaw/acp-core/runtime/session-identifiers"; import { resolveSessionAgentId } from "../../agents/agent-scope.js"; import { persistAcpTurnTranscript } from "../../agents/command/attempt-execution.js"; import { diff --git a/src/auto-reply/reply/dispatch-acp.ts b/src/auto-reply/reply/dispatch-acp.ts index 6f7a094b26df..d4c4441f161e 100644 --- a/src/auto-reply/reply/dispatch-acp.ts +++ b/src/auto-reply/reply/dispatch-acp.ts @@ -1,16 +1,16 @@ +import { formatAcpRuntimeErrorText } from "@openclaw/acp-core/runtime/error-text"; +import { resolveAcpThreadSessionDetailLines } from "@openclaw/acp-core/runtime/session-identifiers"; +import { + isSessionIdentityPending, + resolveSessionIdentityFromMeta, +} from "@openclaw/acp-core/runtime/session-identity"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, } from "@openclaw/normalization-core/string-coerce"; import { resolveAcpAgentPolicyError, resolveAcpDispatchPolicyError } from "../../acp/policy.js"; -import { formatAcpRuntimeErrorText } from "../../acp/runtime/error-text.js"; import { type AcpRuntimeError, toAcpRuntimeError } from "../../acp/runtime/errors.js"; -import { resolveAcpThreadSessionDetailLines } from "../../acp/runtime/session-identifiers.js"; -import { - isSessionIdentityPending, - resolveSessionIdentityFromMeta, -} from "../../acp/runtime/session-identity.js"; import { resolveAgentDir, resolveAgentWorkspaceDir } from "../../agents/agent-scope.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { TtsAutoMode } from "../../config/types.tts.js"; diff --git a/src/auto-reply/reply/dispatch-from-config.ts b/src/auto-reply/reply/dispatch-from-config.ts index b3c1dcc6b524..feb5d7df922e 100644 --- a/src/auto-reply/reply/dispatch-from-config.ts +++ b/src/auto-reply/reply/dispatch-from-config.ts @@ -1,4 +1,5 @@ import crypto from "node:crypto"; +import { isParentOwnedBackgroundAcpSession } from "@openclaw/acp-core/session-interaction-mode"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, @@ -8,7 +9,6 @@ import { hasOutboundReplyContent, resolveSendableOutboundReplyParts, } from "openclaw/plugin-sdk/reply-payload"; -import { isParentOwnedBackgroundAcpSession } from "../../acp/session-interaction-mode.js"; import { resolveAgentConfig, resolveAgentWorkspaceDir, @@ -206,7 +206,12 @@ function routeThreadIdsDiffer( function isSlackDirectRoutedThreadTurn( ctx: Pick< FinalizedMsgContext, - "ChatType" | "MessageThreadId" | "OriginatingChannel" | "Provider" | "Surface" | "TransportThreadId" + | "ChatType" + | "MessageThreadId" + | "OriginatingChannel" + | "Provider" + | "Surface" + | "TransportThreadId" >, ): boolean { if (normalizeChatType(ctx.ChatType) !== "direct") { diff --git a/src/config/sessions/types.ts b/src/config/sessions/types.ts index e56c44625339..7522145a956a 100644 --- a/src/config/sessions/types.ts +++ b/src/config/sessions/types.ts @@ -1,4 +1,11 @@ import crypto from "node:crypto"; +import type { + AcpSessionRuntimeOptions, + SessionAcpIdentity, + SessionAcpIdentitySource, + SessionAcpIdentityState, + SessionAcpMeta, +} from "@openclaw/acp-core/types"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { ChatType } from "../../channels/chat-type.js"; import type { ChannelId } from "../../channels/plugins/channel-id.types.js"; @@ -26,49 +33,12 @@ export type SessionOrigin = { threadId?: string | number; }; -export type SessionAcpIdentitySource = "ensure" | "status" | "event"; - -export type SessionAcpIdentityState = "pending" | "resolved"; - -export type SessionAcpIdentity = { - state: SessionAcpIdentityState; - acpxRecordId?: string; - acpxSessionId?: string; - agentSessionId?: string; - source: SessionAcpIdentitySource; - lastUpdatedAt: number; -}; - -export type SessionAcpMeta = { - backend: string; - agent: string; - runtimeSessionName: string; - identity?: SessionAcpIdentity; - mode: "persistent" | "oneshot"; - runtimeOptions?: AcpSessionRuntimeOptions; - cwd?: string; - state: "idle" | "running" | "error"; - lastActivityAt: number; - lastError?: string; -}; - -export type AcpSessionRuntimeOptions = { - /** - * ACP runtime mode set via session/set_mode (for example: "plan", "normal", "auto"). - */ - runtimeMode?: string; - /** ACP runtime config option: model id. */ - model?: string; - /** ACP runtime config option: thinking/reasoning effort. */ - thinking?: string; - /** Working directory override for ACP session turns. */ - cwd?: string; - /** ACP runtime config option: permission profile id. */ - permissionProfile?: string; - /** ACP runtime config option: per-turn timeout in seconds. */ - timeoutSeconds?: number; - /** Backend-specific option bag mapped through session/set_config_option. */ - backendExtras?: Record; +export type { + AcpSessionRuntimeOptions, + SessionAcpIdentity, + SessionAcpIdentitySource, + SessionAcpIdentityState, + SessionAcpMeta, }; export type CliSessionBinding = { diff --git a/src/cron/run-log/sqlite-store.ts b/src/cron/run-log/sqlite-store.ts index 70ce1acb275d..19491ecc2ded 100644 --- a/src/cron/run-log/sqlite-store.ts +++ b/src/cron/run-log/sqlite-store.ts @@ -1,6 +1,10 @@ import type { DatabaseSync } from "node:sqlite"; -import type { Insertable, Selectable } from "kysely"; -import { executeSqliteQuerySync, getNodeSqliteKysely } from "../../infra/kysely-sync.js"; +import type { Insertable, Selectable, SelectQueryBuilder } from "kysely"; +import { + executeSqliteQuerySync, + executeSqliteQueryTakeFirstSync, + getNodeSqliteKysely, +} from "../../infra/kysely-sync.js"; import type { DB as OpenClawStateKyselyDatabase } from "../../state/openclaw-state-db.generated.js"; import type { CronRunLogEntry } from "../run-log-types.js"; import type { CronDeliveryStatus, CronRunStatus } from "../types.js"; @@ -10,6 +14,13 @@ type CronRunLogsTable = OpenClawStateKyselyDatabase["cron_run_logs"]; type CronRunLogDatabase = Pick; type CronRunLogRow = Selectable; type CronRunLogInsert = Insertable; +type CronRunLogFilterParams = { + storeKey: string; + jobId?: string; + statuses: CronRunStatus[] | null; + deliveryStatuses: CronDeliveryStatus[] | null; + runId?: string; +}; function getCronRunLogKysely(db: DatabaseSync) { return getNodeSqliteKysely(db); @@ -110,37 +121,33 @@ export function readCronRunLogRows( return executeSqliteQuerySync(db, query.orderBy("ts", "asc").orderBy("seq", "asc")).rows; } -function buildRunLogWhereClause(params: { - storeKey: string; - jobId?: string; - statuses: CronRunStatus[] | null; - deliveryStatuses: CronDeliveryStatus[] | null; - runId?: string; -}): { whereSql: string; values: Array } { - const clauses = ["store_key = ?"]; - const values: Array = [params.storeKey]; +function applyRunLogFilters( + query: SelectQueryBuilder, + params: CronRunLogFilterParams, +): SelectQueryBuilder { + let next = query.where("store_key", "=", params.storeKey); if (params.jobId) { - clauses.push("job_id = ?"); - values.push(params.jobId); + next = next.where("job_id", "=", params.jobId); } if (params.statuses?.length) { - clauses.push(`status IN (${params.statuses.map(() => "?").join(", ")})`); - values.push(...params.statuses); + next = next.where("status", "in", params.statuses); } if (params.deliveryStatuses?.length) { - clauses.push( - `COALESCE(delivery_status, 'not-requested') IN (${params.deliveryStatuses - .map(() => "?") - .join(", ")})`, + next = next.where((eb) => + eb.or( + params.deliveryStatuses!.map((status) => + status === "not-requested" + ? eb.or([eb("delivery_status", "is", null), eb("delivery_status", "=", status)]) + : eb("delivery_status", "=", status), + ), + ), ); - values.push(...params.deliveryStatuses); } const runId = params.runId?.trim(); if (runId) { - clauses.push("run_id = ?"); - values.push(runId); + next = next.where("run_id", "=", runId); } - return { whereSql: clauses.join(" AND "), values }; + return next; } export function countCronRunLogRows(params: { @@ -151,10 +158,15 @@ export function countCronRunLogRows(params: { deliveryStatuses: CronDeliveryStatus[] | null; runId?: string; }): number { - const { whereSql, values } = buildRunLogWhereClause(params); - const row = params.db - .prepare(`SELECT COUNT(*) AS count FROM cron_run_logs WHERE ${whereSql}`) - .get(...values) as { count?: number | bigint } | undefined; + const row = executeSqliteQueryTakeFirstSync( + params.db, + applyRunLogFilters( + getCronRunLogKysely(params.db) + .selectFrom("cron_run_logs") + .select((eb) => eb.fn.countAll().as("count")), + params, + ), + ); return normalizeNumber(row?.count ?? null) ?? 0; } @@ -169,25 +181,27 @@ export function readCronRunLogRowsPage(params: { offset?: number; limit?: number; }): CronRunLogRow[] { - const { whereSql, values } = buildRunLogWhereClause(params); - const order = params.sortDir === "asc" ? "ASC" : "DESC"; - const limitSql = - params.limit === undefined || params.offset === undefined ? "" : " LIMIT ? OFFSET ?"; - const limitValues = - params.limit === undefined || params.offset === undefined ? [] : [params.limit, params.offset]; - return params.db - .prepare( - `SELECT * FROM cron_run_logs WHERE ${whereSql} ORDER BY ts ${order}, seq ${order}${limitSql}`, - ) - .all(...values, ...limitValues) as CronRunLogRow[]; + let query = applyRunLogFilters( + getCronRunLogKysely(params.db).selectFrom("cron_run_logs").selectAll(), + params, + ) + .orderBy("ts", params.sortDir) + .orderBy("seq", params.sortDir); + if (params.limit !== undefined && params.offset !== undefined) { + query = query.limit(params.limit).offset(params.offset); + } + return executeSqliteQuerySync(params.db, query).rows; } function nextCronRunLogSeq(db: DatabaseSync, storeKey: string, jobId: string): number { - const row = db - .prepare( - "SELECT COALESCE(MAX(seq), 0) AS seq FROM cron_run_logs WHERE store_key = ? AND job_id = ?", - ) - .get(storeKey, jobId) as { seq?: number | bigint } | undefined; + const row = executeSqliteQueryTakeFirstSync( + db, + getCronRunLogKysely(db) + .selectFrom("cron_run_logs") + .select((eb) => eb.fn.max("seq").as("seq")) + .where("store_key", "=", storeKey) + .where("job_id", "=", jobId), + ); return (normalizeNumber(row?.seq ?? null) ?? 0) + 1; } @@ -212,14 +226,19 @@ export function pruneCronRunLogRows( keepLines: number, ): void { const keep = Math.max(1, Math.floor(keepLines)); - db.prepare( - `DELETE FROM cron_run_logs - WHERE store_key = ? AND job_id = ? - AND seq NOT IN ( - SELECT seq FROM cron_run_logs - WHERE store_key = ? AND job_id = ? - ORDER BY seq DESC - LIMIT ? - )`, - ).run(storeKey, jobId, storeKey, jobId, keep); + const keepSeqs = getCronRunLogKysely(db) + .selectFrom("cron_run_logs") + .select("seq") + .where("store_key", "=", storeKey) + .where("job_id", "=", jobId) + .orderBy("seq", "desc") + .limit(keep); + executeSqliteQuerySync( + db, + getCronRunLogKysely(db) + .deleteFrom("cron_run_logs") + .where("store_key", "=", storeKey) + .where("job_id", "=", jobId) + .where("seq", "not in", keepSeqs), + ); } diff --git a/src/gateway/server-startup-post-attach.ts b/src/gateway/server-startup-post-attach.ts index 95954df77ea1..7d21fb82f058 100644 --- a/src/gateway/server-startup-post-attach.ts +++ b/src/gateway/server-startup-post-attach.ts @@ -858,7 +858,7 @@ export async function startGatewaySidecars(params: { const [{ getAcpSessionManager }, { ACP_SESSION_IDENTITY_RENDERER_VERSION }] = await Promise.all([ import("../acp/control-plane/manager.js"), - import("../acp/runtime/session-identifiers.js"), + import("@openclaw/acp-core/runtime/session-identifiers"), ]); const result = await getAcpSessionManager().reconcilePendingSessionIdentities({ cfg: params.cfg, diff --git a/src/plugins/plugin-sdk-native-resolver.ts b/src/plugins/plugin-sdk-native-resolver.ts index 4240ff6c524d..9aa9391b1505 100644 --- a/src/plugins/plugin-sdk-native-resolver.ts +++ b/src/plugins/plugin-sdk-native-resolver.ts @@ -79,8 +79,18 @@ const INTERNAL_CORE_PACKAGE_ALIASES = [ 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")], ], }, diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts index 59432bfd3286..5806e6cf518a 100644 --- a/src/plugins/sdk-alias.ts +++ b/src/plugins/sdk-alias.ts @@ -754,6 +754,13 @@ const WORKSPACE_PACKAGE_ALIAS_ENTRIES = [ 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", @@ -761,6 +768,13 @@ const WORKSPACE_PACKAGE_ALIAS_ENTRIES = [ 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", @@ -768,6 +782,62 @@ const WORKSPACE_PACKAGE_ALIAS_ENTRIES = [ 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", diff --git a/test/vitest/vitest.shared.config.ts b/test/vitest/vitest.shared.config.ts index f5553ce54f6b..22d500ae5f85 100644 --- a/test/vitest/vitest.shared.config.ts +++ b/test/vitest/vitest.shared.config.ts @@ -401,8 +401,18 @@ 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"), ...sourcePluginSdkSubpaths.map((subpath) => ({ diff --git a/tsconfig.json b/tsconfig.json index 90d289e7dd03..ecbb7fa879fb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -143,8 +143,28 @@ ], "@openclaw/normalization-core/*": ["./packages/normalization-core/src/*"], "@openclaw/acp-core": ["./packages/acp-core/src/index.ts"], + "@openclaw/acp-core/meta": ["./packages/acp-core/src/meta.ts"], + "@openclaw/acp-core/numeric-options": ["./packages/acp-core/src/numeric-options.ts"], "@openclaw/acp-core/normalize-text": ["./packages/acp-core/src/normalize-text.ts"], "@openclaw/acp-core/record-shared": ["./packages/acp-core/src/record-shared.ts"], + "@openclaw/acp-core/session": ["./packages/acp-core/src/session.ts"], + "@openclaw/acp-core/session-interaction-mode": [ + "./packages/acp-core/src/session-interaction-mode.ts" + ], + "@openclaw/acp-core/session-lineage-meta": [ + "./packages/acp-core/src/session-lineage-meta.ts" + ], + "@openclaw/acp-core/types": ["./packages/acp-core/src/types.ts"], + "@openclaw/acp-core/runtime/error-text": [ + "./packages/acp-core/src/runtime/error-text.ts" + ], + "@openclaw/acp-core/runtime/errors": ["./packages/acp-core/src/runtime/errors.ts"], + "@openclaw/acp-core/runtime/session-identifiers": [ + "./packages/acp-core/src/runtime/session-identifiers.ts" + ], + "@openclaw/acp-core/runtime/session-identity": [ + "./packages/acp-core/src/runtime/session-identity.ts" + ], "@openclaw/acp-core/runtime/types": ["./packages/acp-core/src/runtime/types.ts"], "@openclaw/acp-core/*": ["./packages/acp-core/src/*"], "@openclaw/terminal-core": ["./packages/terminal-core/src/index.ts"], diff --git a/tsdown.config.ts b/tsdown.config.ts index 781756a9b143..07e490e7f689 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -459,9 +459,20 @@ function buildMediaCoreDistEntries(): Record { function buildAcpCoreDistEntries(): Record { 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", }; }