fix(subagent): bound forced child termination

Observe signal errors and bound the final forced-exit edge with disposeGraceMs so a refused or ineffective SIGKILL cannot leave disposal pending forever.

Apply the confirmation bound on POSIX and Windows, remove listeners and timers on every outcome, and update the ACP consumer contract plus the generated config catalog.

Cover emitted signal errors, synchronous kill exceptions, refused termination, and accepted termination that never reports exit.
This commit is contained in:
Tianyi Cui
2026-07-19 12:19:33 +08:00
parent 1b496854c2
commit 4625195968
7 changed files with 118 additions and 23 deletions

View File

@@ -10,7 +10,7 @@ The returned run id is minted in the parent namespace. The child server's sessio
After publication, the provider sends the prompt and collects streamed `agent_message_chunk` text into `SubagentResult.output`. A prompt/transport failure resolves with `stopReason: 'error'`, or `aborted` when the required request signal or disposal requested cancellation.
`dispose()` is idempotent. It removes the signal listener, requests ACP cancellation when possible, closes stdin, and waits `disposeEofGraceMs`. POSIX then escalates through SIGTERM and `disposeGraceMs` before SIGKILL; Windows force-terminates directly because Node maps both signals to `TerminateProcess`. Disposal resolves only after child exit. Every run uses a fresh process; process pooling is not implemented.
`dispose()` is idempotent. It removes the signal listener, requests ACP cancellation when possible, closes stdin, and waits `disposeEofGraceMs`. POSIX then escalates through SIGTERM and `disposeGraceMs` before SIGKILL; Windows force-terminates directly because Node maps both signals to `TerminateProcess`. After forced termination, every platform waits at most `disposeGraceMs` for exit and rejects on a signal error or missing exit. Every run uses a fresh process; process pooling is not implemented.
## Capabilities and context
@@ -27,7 +27,7 @@ ACP advertises no start-time capabilities because this process cannot enforce th
| `permission` | `reject` | Auto-answer permission requests by rejecting or choosing the first allow-shaped option. |
| `env` | `{}` | Explicit child environment layered over a credential-scrubbed parent environment. |
| `disposeEofGraceMs` | `6000` | Grace after stdin EOF before platform termination. |
| `disposeGraceMs` | `3000` | POSIX grace after SIGTERM before SIGKILL; unused on Windows. |
| `disposeGraceMs` | `3000` | Exit-confirmation grace after termination; POSIX also waits this long after SIGTERM before SIGKILL. |
```yaml
- id: subagent-acp

View File

@@ -46,7 +46,7 @@ export interface Config {
* before the parent escalates to a signal.
*/
disposeEofGraceMs?: number
/** POSIX grace period (ms) between `SIGTERM` and `SIGKILL`; unused on Windows. */
/** Termination confirmation window (ms), including forced exit on every platform. */
disposeGraceMs?: number
}

View File

@@ -56,9 +56,9 @@ export interface AcpRunSpec {
*/
disposeEofGraceMs: number
/**
* POSIX grace period (ms) between `SIGTERM` and `SIGKILL` in
* {@link SubagentRun.dispose}; unused on Windows. The plugin fills this from
* its `disposeGraceMs` config.
* Termination confirmation window (ms) in {@link SubagentRun.dispose}; POSIX applies it after
* `SIGTERM` and `SIGKILL`, while Windows applies it after direct forced termination. The plugin
* fills this from its `disposeGraceMs` config.
*/
disposeGraceMs: number
/**