* docs: add ACP thread-bound agents plan doc * docs: expand ACP implementation specification * feat(acp): route ACP sessions through core dispatch and lifecycle cleanup * feat(acp): add /acp commands and Discord spawn gate * ACP: add acpx runtime plugin backend * fix(subagents): defer transient lifecycle errors before announce * Agents: harden ACP sessions_spawn and tighten spawn guidance * Agents: require explicit ACP target for runtime spawns * docs: expand ACP control-plane implementation plan * ACP: harden metadata seeding and spawn guidance * ACP: centralize runtime control-plane manager and fail-closed dispatch * ACP: harden runtime manager and unify spawn helpers * Commands: route ACP sessions through ACP runtime in agent command * ACP: require persisted metadata for runtime spawns * Sessions: preserve ACP metadata when updating entries * Plugins: harden ACP backend registry across loaders * ACPX: make availability probe compatible with adapters * E2E: add manual Discord ACP plain-language smoke script * ACPX: preserve streamed spacing across Discord delivery * Docs: add ACP Discord streaming strategy * ACP: harden Discord stream buffering for thread replies * ACP: reuse shared block reply pipeline for projector * ACP: unify streaming config and adopt coalesceIdleMs * Docs: add temporary ACP production hardening plan * Docs: trim temporary ACP hardening plan goals * Docs: gate ACP thread controls by backend capabilities * ACP: add capability-gated runtime controls and /acp operator commands * Docs: remove temporary ACP hardening plan * ACP: fix spawn target validation and close cache cleanup * ACP: harden runtime dispatch and recovery paths * ACP: split ACP command/runtime internals and centralize policy * ACP: harden runtime lifecycle, validation, and observability * ACP: surface runtime and backend session IDs in thread bindings * docs: add temp plan for binding-service migration * ACP: migrate thread binding flows to SessionBindingService * ACP: address review feedback and preserve prompt wording * ACPX plugin: pin runtime dependency and prefer bundled CLI * Discord: complete binding-service migration cleanup and restore ACP plan * Docs: add standalone ACP agents guide * ACP: route harness intents to thread-bound ACP sessions * ACP: fix spawn thread routing and queue-owner stall * ACP: harden startup reconciliation and command bypass handling * ACP: fix dispatch bypass type narrowing * ACP: align runtime metadata to agentSessionId * ACP: normalize session identifier handling and labels * ACP: mark thread banner session ids provisional until first reply * ACP: stabilize session identity mapping and startup reconciliation * ACP: add resolved session-id notices and cwd in thread intros * Discord: prefix thread meta notices consistently * Discord: unify ACP/thread meta notices with gear prefix * Discord: split thread persona naming from meta formatting * Extensions: bump acpx plugin dependency to 0.1.9 * Agents: gate ACP prompt guidance behind acp.enabled * Docs: remove temp experiment plan docs * Docs: scope streaming plan to holy grail refactor * Docs: refactor ACP agents guide for human-first flow * Docs/Skill: add ACP feature-flag guidance and direct acpx telephone-game flow * Docs/Skill: add OpenCode and Pi to ACP harness lists * Docs/Skill: align ACP harness list with current acpx registry * Dev/Test: move ACP plain-language smoke script and mark as keep * Docs/Skill: reorder ACP harness lists with Pi first * ACP: split control-plane manager into core/types/utils modules * Docs: refresh ACP thread-bound agents plan * ACP: extract dispatch lane and split manager domains * ACP: centralize binding context and remove reverse deps * Infra: unify system message formatting * ACP: centralize error boundaries and session id rendering * ACP: enforce init concurrency cap and strict meta clear * Tests: fix ACP dispatch binding mock typing * Tests: fix Discord thread-binding mock drift and ACP request id * ACP: gate slash bypass and persist cleared overrides * ACPX: await pre-abort cancel before runTurn return * Extension: pin acpx runtime dependency to 0.1.11 * Docs: add pinned acpx install strategy for ACP extension * Extensions/acpx: enforce strict local pinned startup * Extensions/acpx: tighten acp-router install guidance * ACPX: retry runtime test temp-dir cleanup * Extensions/acpx: require proactive ACPX repair for thread spawns * Extensions/acpx: require restart offer after acpx reinstall * extensions/acpx: remove workspace protocol devDependency * extensions/acpx: bump pinned acpx to 0.1.13 * extensions/acpx: sync lockfile after dependency bump * ACPX: make runtime spawn Windows-safe * fix: align doctor-config-flow repair tests with default-account migration (#23580) (thanks @osolmaz)
204 lines
7.2 KiB
TypeScript
204 lines
7.2 KiB
TypeScript
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";
|
|
import { loadSessionStore } from "../../../config/sessions.js";
|
|
import type { SessionEntry } from "../../../config/sessions/types.js";
|
|
import { getSessionBindingService } from "../../../infra/outbound/session-binding-service.js";
|
|
import type { CommandHandlerResult, HandleCommandsParams } from "../commands-types.js";
|
|
import { resolveAcpCommandBindingContext } from "./context.js";
|
|
import {
|
|
ACP_DOCTOR_USAGE,
|
|
ACP_INSTALL_USAGE,
|
|
ACP_SESSIONS_USAGE,
|
|
formatAcpCapabilitiesText,
|
|
resolveAcpInstallCommandHint,
|
|
resolveConfiguredAcpBackendId,
|
|
stopWithText,
|
|
} from "./shared.js";
|
|
import { resolveBoundAcpThreadSessionKey } from "./targets.js";
|
|
|
|
export async function handleAcpDoctorAction(
|
|
params: HandleCommandsParams,
|
|
restTokens: string[],
|
|
): Promise<CommandHandlerResult> {
|
|
if (restTokens.length > 0) {
|
|
return stopWithText(`⚠️ ${ACP_DOCTOR_USAGE}`);
|
|
}
|
|
|
|
const backendId = resolveConfiguredAcpBackendId(params.cfg);
|
|
const installHint = resolveAcpInstallCommandHint(params.cfg);
|
|
const registeredBackend = getAcpRuntimeBackend(backendId);
|
|
const managerSnapshot = getAcpSessionManager().getObservabilitySnapshot(params.cfg);
|
|
const lines = ["ACP doctor:", "-----", `configuredBackend: ${backendId}`];
|
|
lines.push(`activeRuntimeSessions: ${managerSnapshot.runtimeCache.activeSessions}`);
|
|
lines.push(`runtimeIdleTtlMs: ${managerSnapshot.runtimeCache.idleTtlMs}`);
|
|
lines.push(`evictedIdleRuntimes: ${managerSnapshot.runtimeCache.evictedTotal}`);
|
|
lines.push(`activeTurns: ${managerSnapshot.turns.active}`);
|
|
lines.push(`queueDepth: ${managerSnapshot.turns.queueDepth}`);
|
|
lines.push(
|
|
`turnLatencyMs: avg=${managerSnapshot.turns.averageLatencyMs}, max=${managerSnapshot.turns.maxLatencyMs}`,
|
|
);
|
|
lines.push(
|
|
`turnCounts: completed=${managerSnapshot.turns.completed}, failed=${managerSnapshot.turns.failed}`,
|
|
);
|
|
const errorStatsText =
|
|
Object.entries(managerSnapshot.errorsByCode)
|
|
.map(([code, count]) => `${code}=${count}`)
|
|
.join(", ") || "(none)";
|
|
lines.push(`errorCodes: ${errorStatsText}`);
|
|
if (registeredBackend) {
|
|
lines.push(`registeredBackend: ${registeredBackend.id}`);
|
|
} else {
|
|
lines.push("registeredBackend: (none)");
|
|
}
|
|
|
|
if (registeredBackend?.runtime.doctor) {
|
|
try {
|
|
const report = await registeredBackend.runtime.doctor();
|
|
lines.push(`runtimeDoctor: ${report.ok ? "ok" : "error"} (${report.message})`);
|
|
if (report.code) {
|
|
lines.push(`runtimeDoctorCode: ${report.code}`);
|
|
}
|
|
if (report.installCommand) {
|
|
lines.push(`runtimeDoctorInstall: ${report.installCommand}`);
|
|
}
|
|
for (const detail of report.details ?? []) {
|
|
lines.push(`runtimeDoctorDetail: ${detail}`);
|
|
}
|
|
} catch (error) {
|
|
lines.push(
|
|
`runtimeDoctor: error (${
|
|
toAcpRuntimeError({
|
|
error,
|
|
fallbackCode: "ACP_TURN_FAILED",
|
|
fallbackMessage: "Runtime doctor failed.",
|
|
}).message
|
|
})`,
|
|
);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const backend = requireAcpRuntimeBackend(backendId);
|
|
const capabilities = backend.runtime.getCapabilities
|
|
? await backend.runtime.getCapabilities({})
|
|
: { controls: [] as string[], configOptionKeys: [] as string[] };
|
|
lines.push("healthy: yes");
|
|
lines.push(`capabilities: ${formatAcpCapabilitiesText(capabilities.controls ?? [])}`);
|
|
if ((capabilities.configOptionKeys?.length ?? 0) > 0) {
|
|
lines.push(`configKeys: ${capabilities.configOptionKeys?.join(", ")}`);
|
|
}
|
|
return stopWithText(lines.join("\n"));
|
|
} catch (error) {
|
|
const acpError = toAcpRuntimeError({
|
|
error,
|
|
fallbackCode: "ACP_TURN_FAILED",
|
|
fallbackMessage: "ACP backend doctor failed.",
|
|
});
|
|
lines.push("healthy: no");
|
|
lines.push(formatAcpRuntimeErrorText(acpError));
|
|
lines.push(`next: ${installHint}`);
|
|
lines.push(`next: openclaw config set plugins.entries.${backendId}.enabled true`);
|
|
if (backendId.toLowerCase() === "acpx") {
|
|
lines.push("next: verify acpx is installed (`acpx --help`).");
|
|
}
|
|
return stopWithText(lines.join("\n"));
|
|
}
|
|
}
|
|
|
|
export function handleAcpInstallAction(
|
|
params: HandleCommandsParams,
|
|
restTokens: string[],
|
|
): CommandHandlerResult {
|
|
if (restTokens.length > 0) {
|
|
return stopWithText(`⚠️ ${ACP_INSTALL_USAGE}`);
|
|
}
|
|
const backendId = resolveConfiguredAcpBackendId(params.cfg);
|
|
const installHint = resolveAcpInstallCommandHint(params.cfg);
|
|
const lines = [
|
|
"ACP install:",
|
|
"-----",
|
|
`configuredBackend: ${backendId}`,
|
|
`run: ${installHint}`,
|
|
`then: openclaw config set plugins.entries.${backendId}.enabled true`,
|
|
"then: /acp doctor",
|
|
];
|
|
return stopWithText(lines.join("\n"));
|
|
}
|
|
|
|
function formatAcpSessionLine(params: {
|
|
key: string;
|
|
entry: SessionEntry;
|
|
currentSessionKey?: string;
|
|
threadId?: string;
|
|
}): string {
|
|
const acp = params.entry.acp;
|
|
if (!acp) {
|
|
return "";
|
|
}
|
|
const marker = params.currentSessionKey === params.key ? "*" : " ";
|
|
const label = params.entry.label?.trim() || acp.agent;
|
|
const threadText = params.threadId ? `, thread:${params.threadId}` : "";
|
|
return `${marker} ${label} (${acp.mode}, ${acp.state}, backend:${acp.backend}${threadText}) -> ${params.key}`;
|
|
}
|
|
|
|
export function handleAcpSessionsAction(
|
|
params: HandleCommandsParams,
|
|
restTokens: string[],
|
|
): CommandHandlerResult {
|
|
if (restTokens.length > 0) {
|
|
return stopWithText(ACP_SESSIONS_USAGE);
|
|
}
|
|
|
|
const currentSessionKey = resolveBoundAcpThreadSessionKey(params) || params.sessionKey;
|
|
if (!currentSessionKey) {
|
|
return stopWithText("⚠️ Missing session key.");
|
|
}
|
|
|
|
const { storePath } = resolveSessionStorePathForAcp({
|
|
cfg: params.cfg,
|
|
sessionKey: currentSessionKey,
|
|
});
|
|
|
|
let store: Record<string, SessionEntry>;
|
|
try {
|
|
store = loadSessionStore(storePath);
|
|
} catch {
|
|
store = {};
|
|
}
|
|
|
|
const bindingContext = resolveAcpCommandBindingContext(params);
|
|
const normalizedChannel = bindingContext.channel;
|
|
const normalizedAccountId = bindingContext.accountId || undefined;
|
|
const bindingService = getSessionBindingService();
|
|
|
|
const rows = Object.entries(store)
|
|
.filter(([, entry]) => Boolean(entry?.acp))
|
|
.toSorted(([, a], [, b]) => (b?.updatedAt ?? 0) - (a?.updatedAt ?? 0))
|
|
.slice(0, 20)
|
|
.map(([key, entry]) => {
|
|
const bindingThreadId = bindingService
|
|
.listBySession(key)
|
|
.find(
|
|
(binding) =>
|
|
(!normalizedChannel || binding.conversation.channel === normalizedChannel) &&
|
|
(!normalizedAccountId || binding.conversation.accountId === normalizedAccountId),
|
|
)?.conversation.conversationId;
|
|
return formatAcpSessionLine({
|
|
key,
|
|
entry,
|
|
currentSessionKey,
|
|
threadId: bindingThreadId,
|
|
});
|
|
})
|
|
.filter(Boolean);
|
|
|
|
if (rows.length === 0) {
|
|
return stopWithText("ACP sessions:\n-----\n(none)");
|
|
}
|
|
|
|
return stopWithText(["ACP sessions:", "-----", ...rows].join("\n"));
|
|
}
|