openclaw/src/gateway/server-startup.ts

192 lines
6.6 KiB
TypeScript
Raw Normal View History

feat: ACP thread-bound agents (#23580) * 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)
2026-02-26 11:00:09 +01:00
import { getAcpSessionManager } from "../acp/control-plane/manager.js";
import { ACP_SESSION_IDENTITY_RENDERER_VERSION } from "../acp/runtime/session-identifiers.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import { loadModelCatalog } from "../agents/model-catalog.js";
import {
getModelRefStatus,
resolveConfiguredModelRef,
resolveHooksGmailModel,
} from "../agents/model-selection.js";
import { resolveAgentSessionDirs } from "../agents/session-dirs.js";
import { cleanStaleLockFiles } from "../agents/session-write-lock.js";
import type { CliDeps } from "../cli/deps.js";
import type { loadConfig } from "../config/config.js";
import { resolveStateDir } from "../config/paths.js";
import { startGmailWatcherWithLogs } from "../hooks/gmail-watcher-lifecycle.js";
import {
clearInternalHooks,
createInternalHookEvent,
triggerInternalHook,
} from "../hooks/internal-hooks.js";
2026-01-17 01:31:39 +00:00
import { loadInternalHooks } from "../hooks/loader.js";
import { isTruthyEnvValue } from "../infra/env.js";
import type { loadOpenClawPlugins } from "../plugins/loader.js";
import { type PluginServicesHandle, startPluginServices } from "../plugins/services.js";
import { startBrowserControlServerIfEnabled } from "./server-browser.js";
import {
scheduleRestartSentinelWake,
shouldWakeFromRestartSentinel,
} from "./server-restart-sentinel.js";
import { startGatewayMemoryBackend } from "./server-startup-memory.js";
const SESSION_LOCK_STALE_MS = 30 * 60 * 1000;
export async function startGatewaySidecars(params: {
cfg: ReturnType<typeof loadConfig>;
2026-01-30 03:15:10 +01:00
pluginRegistry: ReturnType<typeof loadOpenClawPlugins>;
defaultWorkspaceDir: string;
deps: CliDeps;
startChannels: () => Promise<void>;
log: { warn: (msg: string) => void };
logHooks: {
info: (msg: string) => void;
warn: (msg: string) => void;
error: (msg: string) => void;
};
logChannels: { info: (msg: string) => void; error: (msg: string) => void };
logBrowser: { error: (msg: string) => void };
}) {
try {
const stateDir = resolveStateDir(process.env);
const sessionDirs = await resolveAgentSessionDirs(stateDir);
for (const sessionsDir of sessionDirs) {
await cleanStaleLockFiles({
sessionsDir,
staleMs: SESSION_LOCK_STALE_MS,
removeStale: true,
log: { warn: (message) => params.log.warn(message) },
});
}
} catch (err) {
params.log.warn(`session lock cleanup failed on startup: ${String(err)}`);
}
2026-01-30 03:15:10 +01:00
// Start OpenClaw browser control server (unless disabled via config).
let browserControl: Awaited<ReturnType<typeof startBrowserControlServerIfEnabled>> = null;
try {
browserControl = await startBrowserControlServerIfEnabled();
} catch (err) {
params.logBrowser.error(`server failed to start: ${String(err)}`);
}
// Start Gmail watcher if configured (hooks.gmail.account).
await startGmailWatcherWithLogs({
cfg: params.cfg,
log: params.logHooks,
});
// Validate hooks.gmail.model if configured.
if (params.cfg.hooks?.gmail?.model) {
const hooksModelRef = resolveHooksGmailModel({
cfg: params.cfg,
defaultProvider: DEFAULT_PROVIDER,
});
if (hooksModelRef) {
const { provider: defaultProvider, model: defaultModel } = resolveConfiguredModelRef({
cfg: params.cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const catalog = await loadModelCatalog({ config: params.cfg });
const status = getModelRefStatus({
cfg: params.cfg,
catalog,
ref: hooksModelRef,
defaultProvider,
defaultModel,
});
if (!status.allowed) {
params.logHooks.warn(
`hooks.gmail.model "${status.key}" not in agents.defaults.models allowlist (will use primary instead)`,
);
}
if (!status.inCatalog) {
params.logHooks.warn(
`hooks.gmail.model "${status.key}" not in the model catalog (may fail at runtime)`,
);
}
}
}
2026-01-17 01:31:39 +00:00
// Load internal hook handlers from configuration and directory discovery.
try {
// Clear any previously registered hooks to ensure fresh loading
clearInternalHooks();
const loadedCount = await loadInternalHooks(params.cfg, params.defaultWorkspaceDir);
if (loadedCount > 0) {
2026-01-17 01:55:42 +00:00
params.logHooks.info(
`loaded ${loadedCount} internal hook handler${loadedCount > 1 ? "s" : ""}`,
);
2026-01-17 01:31:39 +00:00
}
} catch (err) {
params.logHooks.error(`failed to load hooks: ${String(err)}`);
2026-01-17 01:31:39 +00:00
}
// Launch configured channels so gateway replies via the surface the message came from.
2026-01-30 03:15:10 +01:00
// Tests can opt out via OPENCLAW_SKIP_CHANNELS (or legacy OPENCLAW_SKIP_PROVIDERS).
const skipChannels =
2026-01-30 03:15:10 +01:00
isTruthyEnvValue(process.env.OPENCLAW_SKIP_CHANNELS) ||
isTruthyEnvValue(process.env.OPENCLAW_SKIP_PROVIDERS);
if (!skipChannels) {
try {
await params.startChannels();
} catch (err) {
params.logChannels.error(`channel startup failed: ${String(err)}`);
}
} else {
params.logChannels.info(
2026-01-30 03:15:10 +01:00
"skipping channel start (OPENCLAW_SKIP_CHANNELS=1 or OPENCLAW_SKIP_PROVIDERS=1)",
);
}
if (params.cfg.hooks?.internal?.enabled) {
setTimeout(() => {
const hookEvent = createInternalHookEvent("gateway", "startup", "gateway:startup", {
cfg: params.cfg,
deps: params.deps,
workspaceDir: params.defaultWorkspaceDir,
});
void triggerInternalHook(hookEvent);
}, 250);
}
let pluginServices: PluginServicesHandle | null = null;
try {
pluginServices = await startPluginServices({
registry: params.pluginRegistry,
config: params.cfg,
workspaceDir: params.defaultWorkspaceDir,
});
} catch (err) {
params.log.warn(`plugin services failed to start: ${String(err)}`);
}
feat: ACP thread-bound agents (#23580) * 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)
2026-02-26 11:00:09 +01:00
if (params.cfg.acp?.enabled) {
void getAcpSessionManager()
.reconcilePendingSessionIdentities({ cfg: params.cfg })
.then((result) => {
if (result.checked === 0) {
return;
}
params.log.warn(
`acp startup identity reconcile (renderer=${ACP_SESSION_IDENTITY_RENDERER_VERSION}): checked=${result.checked} resolved=${result.resolved} failed=${result.failed}`,
);
})
.catch((err) => {
params.log.warn(`acp startup identity reconcile failed: ${String(err)}`);
});
}
void startGatewayMemoryBackend({ cfg: params.cfg, log: params.log }).catch((err) => {
params.log.warn(`qmd memory startup initialization failed: ${String(err)}`);
});
if (shouldWakeFromRestartSentinel()) {
setTimeout(() => {
void scheduleRestartSentinelWake({ deps: params.deps });
}, 750);
}
return { browserControl, pluginServices };
}