mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 07:17:40 +00:00
TUI: guard SIGTERM shutdown against setRawMode EBADF
This commit is contained in:
@@ -81,6 +81,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Web UI/Assistant text: strip internal `<relevant-memories>...</relevant-memories>` scaffolding from rendered assistant messages (while preserving code-fence literals), preventing memory-context leakage in chat output for models that echo internal blocks. (#29851) Thanks @Valkster70.
|
- Web UI/Assistant text: strip internal `<relevant-memories>...</relevant-memories>` scaffolding from rendered assistant messages (while preserving code-fence literals), preventing memory-context leakage in chat output for models that echo internal blocks. (#29851) Thanks @Valkster70.
|
||||||
- Dashboard/Sessions: allow authenticated Control UI clients to delete and patch sessions while still blocking regular webchat clients from session mutation RPCs, fixing Dashboard session delete failures. (#21264) Thanks @jskoiz.
|
- Dashboard/Sessions: allow authenticated Control UI clients to delete and patch sessions while still blocking regular webchat clients from session mutation RPCs, fixing Dashboard session delete failures. (#21264) Thanks @jskoiz.
|
||||||
- TUI/Session model status: clear stale runtime model identity when model overrides change so `/model` updates are reflected immediately in `sessions.patch` responses and `sessions.list` status surfaces. (#28619) Thanks @lejean2000.
|
- TUI/Session model status: clear stale runtime model identity when model overrides change so `/model` updates are reflected immediately in `sessions.patch` responses and `sessions.list` status surfaces. (#28619) Thanks @lejean2000.
|
||||||
|
- TUI/SIGTERM shutdown: ignore `setRawMode EBADF` teardown errors during `SIGTERM` exit so long-running TUI sessions do not crash on terminal shutdown races, while still rethrowing unrelated stop errors. (#29430) Thanks @Cormazabal.
|
||||||
- Memory/Hybrid recall: when strict hybrid scoring yields no hits, preserve keyword-backed matches using a text-weight floor so freshly indexed lexical canaries no longer disappear behind `minScore` filtering. (#29112) Thanks @ceo-nada.
|
- Memory/Hybrid recall: when strict hybrid scoring yields no hits, preserve keyword-backed matches using a text-weight floor so freshly indexed lexical canaries no longer disappear behind `minScore` filtering. (#29112) Thanks @ceo-nada.
|
||||||
- Cron/Reminder session routing: preserve `job.sessionKey` for `sessionTarget="main"` runs so queued reminders wake and deliver in the originating scoped session/channel instead of being forced to the agent main session.
|
- Cron/Reminder session routing: preserve `job.sessionKey` for `sessionTarget="main"` runs so queued reminders wake and deliver in the originating scoped session/channel instead of being forced to the agent main session.
|
||||||
- Agents/Sessions list transcript paths: resolve `sessions_list` `transcriptPath` via agent-aware session path options and ignore combined-store sentinel paths (`(multiple)`) so listed transcript paths always point to the state directory. (#28379) Thanks @fafuzuoluo.
|
- Agents/Sessions list transcript paths: resolve `sessions_list` `transcriptPath` via agent-aware session path options and ignore combined-store sentinel paths (`(multiple)`) so listed transcript paths always point to the state directory. (#28379) Thanks @fafuzuoluo.
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { getSlashCommands, parseCommand } from "./commands.js";
|
import { getSlashCommands, parseCommand } from "./commands.js";
|
||||||
import {
|
import {
|
||||||
createBackspaceDeduper,
|
createBackspaceDeduper,
|
||||||
|
isIgnorableTuiStopError,
|
||||||
resolveCtrlCAction,
|
resolveCtrlCAction,
|
||||||
resolveFinalAssistantText,
|
resolveFinalAssistantText,
|
||||||
resolveGatewayDisconnectState,
|
resolveGatewayDisconnectState,
|
||||||
resolveTuiSessionKey,
|
resolveTuiSessionKey,
|
||||||
|
stopTuiSafely,
|
||||||
} from "./tui.js";
|
} from "./tui.js";
|
||||||
|
|
||||||
describe("resolveFinalAssistantText", () => {
|
describe("resolveFinalAssistantText", () => {
|
||||||
@@ -150,3 +152,36 @@ describe("resolveCtrlCAction", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("TUI shutdown safety", () => {
|
||||||
|
it("treats setRawMode EBADF errors as ignorable", () => {
|
||||||
|
expect(isIgnorableTuiStopError(new Error("setRawMode EBADF"))).toBe(true);
|
||||||
|
expect(
|
||||||
|
isIgnorableTuiStopError({
|
||||||
|
code: "EBADF",
|
||||||
|
syscall: "setRawMode",
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not ignore unrelated stop errors", () => {
|
||||||
|
expect(isIgnorableTuiStopError(new Error("something else failed"))).toBe(false);
|
||||||
|
expect(isIgnorableTuiStopError({ code: "EIO", syscall: "write" })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("swallows only ignorable stop errors", () => {
|
||||||
|
expect(() => {
|
||||||
|
stopTuiSafely(() => {
|
||||||
|
throw new Error("setRawMode EBADF");
|
||||||
|
});
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rethrows non-ignorable stop errors", () => {
|
||||||
|
expect(() => {
|
||||||
|
stopTuiSafely(() => {
|
||||||
|
throw new Error("boom");
|
||||||
|
});
|
||||||
|
}).toThrow("boom");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -246,6 +246,30 @@ export function createBackspaceDeduper(params?: { dedupeWindowMs?: number; now?:
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isIgnorableTuiStopError(error: unknown): boolean {
|
||||||
|
if (!error || typeof error !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const err = error as { code?: unknown; syscall?: unknown; message?: unknown };
|
||||||
|
const code = typeof err.code === "string" ? err.code : "";
|
||||||
|
const syscall = typeof err.syscall === "string" ? err.syscall : "";
|
||||||
|
const message = typeof err.message === "string" ? err.message : "";
|
||||||
|
if (code === "EBADF" && syscall === "setRawMode") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return /setRawMode/i.test(message) && /EBADF/i.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopTuiSafely(stop: () => void): void {
|
||||||
|
try {
|
||||||
|
stop();
|
||||||
|
} catch (error) {
|
||||||
|
if (!isIgnorableTuiStopError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type CtrlCAction = "clear" | "warn" | "exit";
|
type CtrlCAction = "clear" | "warn" | "exit";
|
||||||
|
|
||||||
export function resolveCtrlCAction(params: {
|
export function resolveCtrlCAction(params: {
|
||||||
@@ -770,7 +794,7 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
}
|
}
|
||||||
exitRequested = true;
|
exitRequested = true;
|
||||||
client.stop();
|
client.stop();
|
||||||
tui.stop();
|
stopTuiSafely(() => tui.stop());
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user