fix(coding-agent): fire session shutdown on TERM and HUP closes #3212
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
### Fixed
|
### 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 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))
|
- 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 or Ctrl+P (model selection/cycling)
|
||||||
└─► model_select
|
└─► model_select
|
||||||
|
|
||||||
exit (Ctrl+C, Ctrl+D)
|
exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)
|
||||||
└─► session_shutdown
|
└─► session_shutdown
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -403,7 +403,7 @@ pi.on("session_tree", async (event, ctx) => {
|
|||||||
|
|
||||||
#### session_shutdown
|
#### session_shutdown
|
||||||
|
|
||||||
Fired on exit (Ctrl+C, Ctrl+D, SIGTERM).
|
Fired on exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM).
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
pi.on("session_shutdown", async (_event, ctx) => {
|
pi.on("session_shutdown", async (_event, ctx) => {
|
||||||
|
|||||||
@@ -479,7 +479,7 @@ export interface SessionCompactEvent {
|
|||||||
fromExtension: boolean;
|
fromExtension: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fired on process exit */
|
/** Fired on graceful process shutdown paths such as Ctrl+C, Ctrl+D, SIGHUP, and SIGTERM. */
|
||||||
export interface SessionShutdownEvent {
|
export interface SessionShutdownEvent {
|
||||||
type: "session_shutdown";
|
type: "session_shutdown";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ export class InteractiveMode {
|
|||||||
|
|
||||||
// Agent subscription unsubscribe function
|
// Agent subscription unsubscribe function
|
||||||
private unsubscribe?: () => void;
|
private unsubscribe?: () => void;
|
||||||
|
private signalCleanupHandlers: Array<() => void> = [];
|
||||||
|
|
||||||
// Track if editor is in bash mode (text starts with !)
|
// Track if editor is in bash mode (text starts with !)
|
||||||
private isBashMode = false;
|
private isBashMode = false;
|
||||||
@@ -477,6 +478,8 @@ export class InteractiveMode {
|
|||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
if (this.isInitialized) return;
|
if (this.isInitialized) return;
|
||||||
|
|
||||||
|
this.registerSignalHandlers();
|
||||||
|
|
||||||
// Load changelog (only show new entries, skip for resumed sessions)
|
// Load changelog (only show new entries, skip for resumed sessions)
|
||||||
this.changelogMarkdown = this.getChangelogForDisplay();
|
this.changelogMarkdown = this.getChangelogForDisplay();
|
||||||
|
|
||||||
@@ -2905,6 +2908,7 @@ export class InteractiveMode {
|
|||||||
private async shutdown(): Promise<void> {
|
private async shutdown(): Promise<void> {
|
||||||
if (this.isShuttingDown) return;
|
if (this.isShuttingDown) return;
|
||||||
this.isShuttingDown = true;
|
this.isShuttingDown = true;
|
||||||
|
this.unregisterSignalHandlers();
|
||||||
await this.runtimeHost.dispose();
|
await this.runtimeHost.dispose();
|
||||||
|
|
||||||
// Wait for any pending renders to complete
|
// Wait for any pending renders to complete
|
||||||
@@ -2927,6 +2931,30 @@ export class InteractiveMode {
|
|||||||
await this.shutdown();
|
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 {
|
private handleCtrlZ(): void {
|
||||||
// Keep the event loop alive while suspended. Without this, stopping the TUI
|
// 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
|
// can leave Node with no ref'ed handles, causing the process to exit on fg
|
||||||
@@ -4785,6 +4813,7 @@ export class InteractiveMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
|
this.unregisterSignalHandlers();
|
||||||
if (this.loadingAnimation) {
|
if (this.loadingAnimation) {
|
||||||
this.loadingAnimation.stop();
|
this.loadingAnimation.stop();
|
||||||
this.loadingAnimation = undefined;
|
this.loadingAnimation = undefined;
|
||||||
|
|||||||
@@ -33,6 +33,34 @@ export async function runPrintMode(runtimeHost: AgentSessionRuntime, options: Pr
|
|||||||
let exitCode = 0;
|
let exitCode = 0;
|
||||||
let session = runtimeHost.session;
|
let session = runtimeHost.session;
|
||||||
let unsubscribe: (() => void) | undefined;
|
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> => {
|
const rebindSession = async (): Promise<void> => {
|
||||||
session = runtimeHost.session;
|
session = runtimeHost.session;
|
||||||
@@ -128,8 +156,10 @@ export async function runPrintMode(runtimeHost: AgentSessionRuntime, options: Pr
|
|||||||
console.error(error instanceof Error ? error.message : String(error));
|
console.error(error instanceof Error ? error.message : String(error));
|
||||||
return 1;
|
return 1;
|
||||||
} finally {
|
} finally {
|
||||||
unsubscribe?.();
|
for (const cleanup of signalCleanupHandlers) {
|
||||||
await runtimeHost.dispose();
|
cleanup();
|
||||||
|
}
|
||||||
|
await disposeRuntime();
|
||||||
await flushRawStdout();
|
await flushRawStdout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
|
|||||||
|
|
||||||
// Shutdown request flag
|
// Shutdown request flag
|
||||||
let shutdownRequested = false;
|
let shutdownRequested = false;
|
||||||
|
let shuttingDown = false;
|
||||||
|
const signalCleanupHandlers: Array<() => void> = [];
|
||||||
|
|
||||||
/** Helper for dialog methods with signal/timeout support */
|
/** Helper for dialog methods with signal/timeout support */
|
||||||
function createDialogPromise<T>(
|
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();
|
await rebindSession();
|
||||||
|
registerSignalHandlers();
|
||||||
|
|
||||||
// Handle a single command
|
// Handle a single command
|
||||||
const handleCommand = async (command: RpcCommand): Promise<RpcResponse> => {
|
const handleCommand = async (command: RpcCommand): Promise<RpcResponse> => {
|
||||||
@@ -614,12 +632,19 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
|
|||||||
*/
|
*/
|
||||||
let detachInput = () => {};
|
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?.();
|
unsubscribe?.();
|
||||||
await runtimeHost.dispose();
|
await runtimeHost.dispose();
|
||||||
detachInput();
|
detachInput();
|
||||||
process.stdin.pause();
|
process.stdin.pause();
|
||||||
process.exit(0);
|
process.exit(exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkShutdownRequested(): Promise<void> {
|
async function checkShutdownRequested(): Promise<void> {
|
||||||
|
|||||||
Reference in New Issue
Block a user