fix(coding-agent): fire session shutdown on TERM and HUP closes #3212
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `session_shutdown` to fire on `SIGHUP` and `SIGTERM` in interactive, print, and RPC modes so extensions can run shutdown cleanup on those signal-driven exits ([#3212](https://github.com/badlogic/pi-mono/issues/3212))
|
||||
- Fixed screenshot path parsing to handle lower case am/pm in macOS screenshot filenames ([#3194](https://github.com/badlogic/pi-mono/pull/3194) by [@jay-aye-see-kay](https://github.com/jay-aye-see-kay))
|
||||
- Fixed interactive auto-retry status updates to show a live countdown during backoff instead of a static retry delay message ([#3187](https://github.com/badlogic/pi-mono/issues/3187))
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ user sends another prompt ◄─────────────────
|
||||
/model or Ctrl+P (model selection/cycling)
|
||||
└─► model_select
|
||||
|
||||
exit (Ctrl+C, Ctrl+D)
|
||||
exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)
|
||||
└─► session_shutdown
|
||||
```
|
||||
|
||||
@@ -403,7 +403,7 @@ pi.on("session_tree", async (event, ctx) => {
|
||||
|
||||
#### session_shutdown
|
||||
|
||||
Fired on exit (Ctrl+C, Ctrl+D, SIGTERM).
|
||||
Fired on exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM).
|
||||
|
||||
```typescript
|
||||
pi.on("session_shutdown", async (_event, ctx) => {
|
||||
|
||||
@@ -479,7 +479,7 @@ export interface SessionCompactEvent {
|
||||
fromExtension: boolean;
|
||||
}
|
||||
|
||||
/** Fired on process exit */
|
||||
/** Fired on graceful process shutdown paths such as Ctrl+C, Ctrl+D, SIGHUP, and SIGTERM. */
|
||||
export interface SessionShutdownEvent {
|
||||
type: "session_shutdown";
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ export class InteractiveMode {
|
||||
|
||||
// Agent subscription unsubscribe function
|
||||
private unsubscribe?: () => void;
|
||||
private signalCleanupHandlers: Array<() => void> = [];
|
||||
|
||||
// Track if editor is in bash mode (text starts with !)
|
||||
private isBashMode = false;
|
||||
@@ -477,6 +478,8 @@ export class InteractiveMode {
|
||||
async init(): Promise<void> {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
this.registerSignalHandlers();
|
||||
|
||||
// Load changelog (only show new entries, skip for resumed sessions)
|
||||
this.changelogMarkdown = this.getChangelogForDisplay();
|
||||
|
||||
@@ -2905,6 +2908,7 @@ export class InteractiveMode {
|
||||
private async shutdown(): Promise<void> {
|
||||
if (this.isShuttingDown) return;
|
||||
this.isShuttingDown = true;
|
||||
this.unregisterSignalHandlers();
|
||||
await this.runtimeHost.dispose();
|
||||
|
||||
// Wait for any pending renders to complete
|
||||
@@ -2927,6 +2931,30 @@ export class InteractiveMode {
|
||||
await this.shutdown();
|
||||
}
|
||||
|
||||
private registerSignalHandlers(): void {
|
||||
this.unregisterSignalHandlers();
|
||||
|
||||
const signals: NodeJS.Signals[] = ["SIGTERM"];
|
||||
if (process.platform !== "win32") {
|
||||
signals.push("SIGHUP");
|
||||
}
|
||||
|
||||
for (const signal of signals) {
|
||||
const handler = () => {
|
||||
void this.shutdown();
|
||||
};
|
||||
process.on(signal, handler);
|
||||
this.signalCleanupHandlers.push(() => process.off(signal, handler));
|
||||
}
|
||||
}
|
||||
|
||||
private unregisterSignalHandlers(): void {
|
||||
for (const cleanup of this.signalCleanupHandlers) {
|
||||
cleanup();
|
||||
}
|
||||
this.signalCleanupHandlers = [];
|
||||
}
|
||||
|
||||
private handleCtrlZ(): void {
|
||||
// Keep the event loop alive while suspended. Without this, stopping the TUI
|
||||
// can leave Node with no ref'ed handles, causing the process to exit on fg
|
||||
@@ -4785,6 +4813,7 @@ export class InteractiveMode {
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.unregisterSignalHandlers();
|
||||
if (this.loadingAnimation) {
|
||||
this.loadingAnimation.stop();
|
||||
this.loadingAnimation = undefined;
|
||||
|
||||
@@ -33,6 +33,34 @@ export async function runPrintMode(runtimeHost: AgentSessionRuntime, options: Pr
|
||||
let exitCode = 0;
|
||||
let session = runtimeHost.session;
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
let disposed = false;
|
||||
const signalCleanupHandlers: Array<() => void> = [];
|
||||
|
||||
const disposeRuntime = async (): Promise<void> => {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
unsubscribe?.();
|
||||
await runtimeHost.dispose();
|
||||
};
|
||||
|
||||
const registerSignalHandlers = (): void => {
|
||||
const signals: NodeJS.Signals[] = ["SIGTERM"];
|
||||
if (process.platform !== "win32") {
|
||||
signals.push("SIGHUP");
|
||||
}
|
||||
|
||||
for (const signal of signals) {
|
||||
const handler = () => {
|
||||
void disposeRuntime().finally(() => {
|
||||
process.exit(signal === "SIGHUP" ? 129 : 143);
|
||||
});
|
||||
};
|
||||
process.on(signal, handler);
|
||||
signalCleanupHandlers.push(() => process.off(signal, handler));
|
||||
}
|
||||
};
|
||||
|
||||
registerSignalHandlers();
|
||||
|
||||
const rebindSession = async (): Promise<void> => {
|
||||
session = runtimeHost.session;
|
||||
@@ -128,8 +156,10 @@ export async function runPrintMode(runtimeHost: AgentSessionRuntime, options: Pr
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
return 1;
|
||||
} finally {
|
||||
unsubscribe?.();
|
||||
await runtimeHost.dispose();
|
||||
for (const cleanup of signalCleanupHandlers) {
|
||||
cleanup();
|
||||
}
|
||||
await disposeRuntime();
|
||||
await flushRawStdout();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,8 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
|
||||
|
||||
// Shutdown request flag
|
||||
let shutdownRequested = false;
|
||||
let shuttingDown = false;
|
||||
const signalCleanupHandlers: Array<() => void> = [];
|
||||
|
||||
/** Helper for dialog methods with signal/timeout support */
|
||||
function createDialogPromise<T>(
|
||||
@@ -336,7 +338,23 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
|
||||
});
|
||||
};
|
||||
|
||||
const registerSignalHandlers = (): void => {
|
||||
const signals: NodeJS.Signals[] = ["SIGTERM"];
|
||||
if (process.platform !== "win32") {
|
||||
signals.push("SIGHUP");
|
||||
}
|
||||
|
||||
for (const signal of signals) {
|
||||
const handler = () => {
|
||||
void shutdown(signal === "SIGHUP" ? 129 : 143);
|
||||
};
|
||||
process.on(signal, handler);
|
||||
signalCleanupHandlers.push(() => process.off(signal, handler));
|
||||
}
|
||||
};
|
||||
|
||||
await rebindSession();
|
||||
registerSignalHandlers();
|
||||
|
||||
// Handle a single command
|
||||
const handleCommand = async (command: RpcCommand): Promise<RpcResponse> => {
|
||||
@@ -614,12 +632,19 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
|
||||
*/
|
||||
let detachInput = () => {};
|
||||
|
||||
async function shutdown(): Promise<never> {
|
||||
async function shutdown(exitCode = 0): Promise<never> {
|
||||
if (shuttingDown) {
|
||||
process.exit(exitCode);
|
||||
}
|
||||
shuttingDown = true;
|
||||
for (const cleanup of signalCleanupHandlers) {
|
||||
cleanup();
|
||||
}
|
||||
unsubscribe?.();
|
||||
await runtimeHost.dispose();
|
||||
detachInput();
|
||||
process.stdin.pause();
|
||||
process.exit(0);
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
async function checkShutdownRequested(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user