fix: kill stuck ACP child processes on startup and harden sessions in discord threads (#33699)

* Gateway: resolve agent.wait for chat.send runs

* Discord: harden ACP thread binding + listener timeout

* ACPX: handle already-exited child wait

* Gateway/Discord: address PR review findings

* Discord: keep ACP error-state thread bindings on startup

* gateway: make agent.wait dedupe bridge event-driven

* discord: harden ACP probe classification and cap startup fan-out

* discord: add cooperative timeout cancellation

* discord: fix startup probe concurrency helper typing

* plugin-sdk: avoid Windows root-alias shard timeout

* plugin-sdk: keep root alias reflection path non-blocking

* discord+gateway: resolve remaining PR review findings

* gateway+discord: fix codex review regressions

* Discord/Gateway: address Codex review findings

* Gateway: keep agent.wait lifecycle active with shared run IDs

* Discord: clean up status reactions on aborted runs

* fix: add changelog note for ACP/Discord startup hardening (#33699) (thanks @dutifulbob)

---------

Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
This commit is contained in:
Bob
2026-03-04 10:52:28 +01:00
committed by GitHub
parent bd25182d5a
commit 61f7cea48b
30 changed files with 2568 additions and 180 deletions

View File

@@ -316,70 +316,85 @@ export class AcpSessionManager {
async getSessionStatus(params: {
cfg: OpenClawConfig;
sessionKey: string;
signal?: AbortSignal;
}): Promise<AcpSessionStatus> {
const sessionKey = normalizeSessionKey(params.sessionKey);
if (!sessionKey) {
throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required.");
}
this.throwIfAborted(params.signal);
await this.evictIdleRuntimeHandles({ cfg: params.cfg });
return await this.withSessionActor(sessionKey, async () => {
const resolution = this.resolveSession({
cfg: params.cfg,
sessionKey,
});
if (resolution.kind === "none") {
throw new AcpRuntimeError(
"ACP_SESSION_INIT_FAILED",
`Session is not ACP-enabled: ${sessionKey}`,
);
}
if (resolution.kind === "stale") {
throw resolution.error;
}
const {
runtime,
handle: ensuredHandle,
meta: ensuredMeta,
} = await this.ensureRuntimeHandle({
cfg: params.cfg,
sessionKey,
meta: resolution.meta,
});
let handle = ensuredHandle;
let meta = ensuredMeta;
const capabilities = await this.resolveRuntimeCapabilities({ runtime, handle });
let runtimeStatus: AcpRuntimeStatus | undefined;
if (runtime.getStatus) {
runtimeStatus = await withAcpRuntimeErrorBoundary({
run: async () => await runtime.getStatus!({ handle }),
fallbackCode: "ACP_TURN_FAILED",
fallbackMessage: "Could not read ACP runtime status.",
return await this.withSessionActor(
sessionKey,
async () => {
this.throwIfAborted(params.signal);
const resolution = this.resolveSession({
cfg: params.cfg,
sessionKey,
});
}
({ handle, meta, runtimeStatus } = await this.reconcileRuntimeSessionIdentifiers({
cfg: params.cfg,
sessionKey,
runtime,
handle,
meta,
runtimeStatus,
failOnStatusError: true,
}));
const identity = resolveSessionIdentityFromMeta(meta);
return {
sessionKey,
backend: handle.backend || meta.backend,
agent: meta.agent,
...(identity ? { identity } : {}),
state: meta.state,
mode: meta.mode,
runtimeOptions: resolveRuntimeOptionsFromMeta(meta),
capabilities,
runtimeStatus,
lastActivityAt: meta.lastActivityAt,
lastError: meta.lastError,
};
});
if (resolution.kind === "none") {
throw new AcpRuntimeError(
"ACP_SESSION_INIT_FAILED",
`Session is not ACP-enabled: ${sessionKey}`,
);
}
if (resolution.kind === "stale") {
throw resolution.error;
}
const {
runtime,
handle: ensuredHandle,
meta: ensuredMeta,
} = await this.ensureRuntimeHandle({
cfg: params.cfg,
sessionKey,
meta: resolution.meta,
});
let handle = ensuredHandle;
let meta = ensuredMeta;
const capabilities = await this.resolveRuntimeCapabilities({ runtime, handle });
let runtimeStatus: AcpRuntimeStatus | undefined;
if (runtime.getStatus) {
runtimeStatus = await withAcpRuntimeErrorBoundary({
run: async () => {
this.throwIfAborted(params.signal);
const status = await runtime.getStatus!({
handle,
...(params.signal ? { signal: params.signal } : {}),
});
this.throwIfAborted(params.signal);
return status;
},
fallbackCode: "ACP_TURN_FAILED",
fallbackMessage: "Could not read ACP runtime status.",
});
}
({ handle, meta, runtimeStatus } = await this.reconcileRuntimeSessionIdentifiers({
cfg: params.cfg,
sessionKey,
runtime,
handle,
meta,
runtimeStatus,
failOnStatusError: true,
}));
const identity = resolveSessionIdentityFromMeta(meta);
return {
sessionKey,
backend: handle.backend || meta.backend,
agent: meta.agent,
...(identity ? { identity } : {}),
state: meta.state,
mode: meta.mode,
runtimeOptions: resolveRuntimeOptionsFromMeta(meta),
capabilities,
runtimeStatus,
lastActivityAt: meta.lastActivityAt,
lastError: meta.lastError,
};
},
params.signal,
);
}
async setSessionRuntimeMode(params: {
@@ -1295,9 +1310,23 @@ export class AcpSessionManager {
}
}
private async withSessionActor<T>(sessionKey: string, op: () => Promise<T>): Promise<T> {
private async withSessionActor<T>(
sessionKey: string,
op: () => Promise<T>,
signal?: AbortSignal,
): Promise<T> {
const actorKey = normalizeActorKey(sessionKey);
return await this.actorQueue.run(actorKey, op);
return await this.actorQueue.run(actorKey, async () => {
this.throwIfAborted(signal);
return await op();
});
}
private throwIfAborted(signal?: AbortSignal): void {
if (!signal?.aborted) {
return;
}
throw new AcpRuntimeError("ACP_TURN_FAILED", "ACP operation aborted.");
}
private getCachedRuntimeState(sessionKey: string): CachedRuntimeState | null {

View File

@@ -117,7 +117,7 @@ export interface AcpRuntime {
handle?: AcpRuntimeHandle;
}): Promise<AcpRuntimeCapabilities> | AcpRuntimeCapabilities;
getStatus?(input: { handle: AcpRuntimeHandle }): Promise<AcpRuntimeStatus>;
getStatus?(input: { handle: AcpRuntimeHandle; signal?: AbortSignal }): Promise<AcpRuntimeStatus>;
setMode?(input: { handle: AcpRuntimeHandle; mode: string }): Promise<void>;