fix: handle intentional signal daemon shutdown on abort (#23379) (thanks @frankekn)
This commit is contained in:
@@ -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`).
|
||||
|
||||
@@ -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" });
|
||||
|
||||
|
||||
@@ -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<typeof spawnSignalDaemon> | 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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user