diff --git a/CHANGELOG.md b/CHANGELOG.md index 6814cff66..70185653f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Gateway/Restart: fix restart-loop edge cases by keeping `openclaw.mjs -> dist/entry.js` bootstrap detection explicit, reacquiring the gateway lock for in-process restart fallback paths, and tightening restart-loop regression coverage. (#23416) Thanks @jeffwnli. +- Signal/Monitor: treat user-initiated abort shutdowns as clean exits when auto-started `signal-cli` is terminated, while still surfacing unexpected daemon exits as startup/runtime failures. (#23379) Thanks @frankekn. - Channels/Dedupe: centralize plugin dedupe primitives in plugin SDK (memory + persistent), move Feishu inbound dedupe to a namespace-scoped persistent store, and reuse shared dedupe cache logic for Zalo webhook replay + Tlon processed-message tracking to reduce duplicate handling during reconnect/replay paths. - ACP/Gateway: wait for gateway hello before opening ACP requests, and fail fast on pre-hello connect failures to avoid startup hangs and early `gateway not connected` request races. (#23390) Thanks @janckerchen. - Security/Audit: add `openclaw security audit` detection for open group policies that expose runtime/filesystem tools without sandbox/workspace guards (`security.exposure.open_groups_with_runtime_or_fs`). diff --git a/src/signal/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts b/src/signal/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts index 6cbaf9662..47f5fb173 100644 --- a/src/signal/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts +++ b/src/signal/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts @@ -246,6 +246,43 @@ describe("monitorSignalProvider tool results", () => { ).rejects.toThrow(/signal daemon exited/i); }); + it("treats daemon exit after user abort as clean shutdown", async () => { + const runtime = createMonitorRuntime(); + setSignalAutoStartConfig(); + const abortController = new AbortController(); + let exited = false; + let resolveExit!: (value: { code: number | null; signal: NodeJS.Signals | null }) => void; + const exitedPromise = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>( + (resolve) => { + resolveExit = resolve; + }, + ); + const stop = vi.fn(() => { + if (exited) { + return; + } + exited = true; + resolveExit({ code: null, signal: "SIGTERM" }); + }); + spawnSignalDaemonMock.mockReturnValueOnce({ + stop, + exited: exitedPromise, + isExited: () => exited, + }); + streamMock.mockImplementationOnce(async () => { + abortController.abort(new Error("stop")); + }); + + await expect( + runMonitorWithMocks({ + autoStart: true, + baseUrl: SIGNAL_BASE_URL, + runtime, + abortSignal: abortController.signal, + }), + ).resolves.toBeUndefined(); + }); + it("skips tool summaries with responsePrefix", async () => { replyMock.mockResolvedValue({ text: "final reply" }); diff --git a/src/signal/monitor.ts b/src/signal/monitor.ts index 0bcff74b7..5dce5f407 100644 --- a/src/signal/monitor.ts +++ b/src/signal/monitor.ts @@ -330,6 +330,11 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi const daemonAbortController = new AbortController(); const mergedAbort = mergeAbortSignals(opts.abortSignal, daemonAbortController.signal); let daemonHandle: ReturnType | null = null; + let daemonStopRequested = false; + const stopDaemon = () => { + daemonStopRequested = true; + daemonHandle?.stop(); + }; if (autoStart) { const cliPath = opts.cliPath ?? accountInfo.config.cliPath ?? "signal-cli"; @@ -347,6 +352,9 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi runtime, }); void daemonHandle.exited.then((exit) => { + if (daemonStopRequested || opts.abortSignal?.aborted) { + return; + } daemonExitError = new Error( `signal daemon exited (code=${String(exit.code ?? "null")} signal=${String(exit.signal ?? "null")})`, ); @@ -357,7 +365,7 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi } const onAbort = () => { - daemonHandle?.stop(); + stopDaemon(); }; opts.abortSignal?.addEventListener("abort", onAbort, { once: true }); @@ -426,6 +434,6 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi } finally { mergedAbort.dispose(); opts.abortSignal?.removeEventListener("abort", onAbort); - daemonHandle?.stop(); + stopDaemon(); } }