Explicit pane termination and recursive container removal
| Field | Value |
|---|---|
| Status | Accepted |
| Accepted | 2026-11-07, explicitly approved by the maintainer |
| Authors | commixon (commixon), with Claude |
| Created | 2026-11-07 |
| Linear | COM-245; implementation COM-246 |
| Supersedes | None |
| Superseded by | None |
History
- 2026-11-10 (venom-org/venom-spec#144, venom-org/venomx#147; normative,
narrow compatible change; accepted by venom-run (unattended, delegated by
the maintainer) on 2026-11-10): §7’s “MUST NOT include argv” rule excepts
RFC-0007 §5’s derived
argv0(the basename ofargv[0], at most 255 bytes) in thepaneentity andpane.created. Every other part of argv stays excluded.
Summary
Add one destructive verb, pane.kill, and an opt-in recursive mode to
window.remove and session.remove in venomx. pane.kill terminates a
live pane’s process group with a requested signal, escalates to SIGKILL
on a monotonic deadline, reaps it and optionally removes the pane.
Recursive removal authorizes the whole subtree, seals it against new
children, terminates every live pane in it with one shared deadline and
deletes the subtree in one commit once every pane has exited.
This is a compatible extension of the RFC-0007
foundation. Calls that do not opt in keep RFC-0007 §5 exactly: removing a
running pane fails conflict.pane_running, removing a nonempty container
fails conflict.not_empty, and nothing is signalled. COM-246 implements
this RFC after COM-238. Grants follow RFC-0005,
events RFC-0003, stream closure
RFC-0006, transport and cancellation
RFC-0002, schema RFC-0004.
Motivation
RFC-0007 deferred live-pane kill and recursive removal so the foundation could keep removal atomic, with no unbounded wait and no implicit cascade. Without them, a client that wants to close a window must find every pane, get each program to exit by sending input, poll for exits, remove every pane, then remove the window. An agent cannot stop a hung program at all short of stopping the daemon. A presentation client’s “close window” binding (COM-223) has no single mutation to call.
Doing this carelessly is dangerous: an authorized root does not imply
authorized descendants (window:* does not cover panes), a PID or PGID can
be recycled to an unrelated process, a signal-ignoring child can stall a
synchronous handler, and a spawn racing a removal can leave a ghost pane
or a leaked principal. This RFC fixes each of those boundaries before
COM-246 writes code.
Design
1. Examples
Assume a fresh daemon as in RFC-0007 §1: session s0, window w0 with
panes p0 and p1, and window w1 with pane p2.
$ VENOMX_TOKEN=@owner venomx pane kill --pane p1 --json
{"pane":"p1","outcome":"terminated","removed":false,"escalated":false,"signal":"SIGHUP"}
$ VENOMX_TOKEN=@owner venomx pane kill --pane p1 --json
{"pane":"p1","outcome":"already_exited","removed":false,"escalated":false,"signal":"SIGHUP"}
$ VENOMX_TOKEN=@owner venomx pane kill --pane p1 --remove=true --json
{"pane":"p1","outcome":"already_exited","removed":true,"escalated":false,"signal":"SIGHUP"}
$ VENOMX_TOKEN=@owner venomx window remove --window w0 --json
{"error":{"code":"conflict.not_empty","message":"window has panes","data":{}}}
$ echo $?
5
$ VENOMX_TOKEN=@owner venomx window remove --window w0 --recursive=true --json
{}
$ VENOMX_TOKEN=@owner venomx session remove --session s0 --recursive=true --signal SIGTERM --grace-ms 500 --json
{}
A program that ignores the first signal is killed at the deadline:
$ VENOMX_TOKEN=@owner venomx session create --name hung --json
{"session":"s1","name":"hung"}
$ VENOMX_TOKEN=@owner venomx window create --session s1 --argv '["/bin/sh","-c","trap \"\" HUP TERM INT; while :; do sleep 1; done"]' --json
{"window":"w2","session":"s1","pane":"p3","principal":"g4","pid":41022}
$ VENOMX_TOKEN=@owner venomx pane kill --pane p3 --grace-ms 300 --json
{"pane":"p3","outcome":"terminated","removed":false,"escalated":true,"signal":"SIGKILL"}
The same call on the wire (RFC-0002 NDJSON, after hello):
{"id":5,"verb":"pane.kill","params":{"pane":"p3","grace_ms":300}}
{"id":5,"ok":{"pane":"p3","outcome":"terminated","removed":false,"escalated":true,"signal":"SIGKILL"}}
A response arrives only after the pane’s leader has been reaped (§4). The
first response above came back as soon as the shell exited on SIGHUP;
the second came back about 300 ms after the request.
2. Schema
All additions go in the venomx RFC-0004 format-1 manifest. The CLI flags
follow RFC-0004 §4 (grace_ms → --grace-ms, booleans as --remove=true).
All verbs are nonstreaming with standard exit policy.
| Verb | New parameters | Result | Dispatch grant |
|---|---|---|---|
pane.kill (new) |
Required pane; optional signal, grace_ms, remove |
{pane,outcome,removed,escalated} plus optional exit_code or signal |
write |
window.remove |
Optional recursive, signal, grace_ms |
{} (unchanged) |
write (unchanged) |
session.remove |
Optional recursive, signal, grace_ms |
{} (unchanged) |
write (unchanged) |
pane.remove is unchanged; pane.kill --remove=true is the live-pane
form. The shared parameter records are:
.{
.{ .name = "signal", .summary = "First signal sent to each live pane's process group", .type = .{ .kind = .string, .choices = .{ "SIGHUP", "SIGTERM", "SIGINT" } }, .default = "SIGHUP" },
.{ .name = "grace_ms", .summary = "Milliseconds to wait after the first signal before sending SIGKILL", .type = .{ .kind = .integer, .min = 0, .max = 30000 }, .default = 2000 },
}
pane.kill adds:
.{
.{ .name = "pane", .summary = "Pane to terminate", .type = .{ .kind = .string }, .required = true },
.{ .name = "remove", .summary = "Also remove the pane once it has exited", .type = .{ .kind = .boolean }, .default = false },
}
window.remove and session.remove add:
.{
.{ .name = "recursive", .summary = "Terminate every live pane and remove all children (destructive); signal and grace_ms apply only when true", .type = .{ .kind = .boolean }, .default = false },
}
The pane.kill result is a closed object: required string pane;
required string outcome with choices terminated and already_exited;
required booleans removed and escalated; optional integer exit_code
(0–255); optional string signal. exit_code and signal describe the
leader’s wait status and follow RFC-0007 §3: normal exit gives exit_code,
death by signal gives the symbolic signal, never both. escalated is
true only when this termination sent SIGKILL because the grace deadline
passed (§4). It is false for already_exited.
The default SIGHUP and 2000 ms match RFC-0007’s shutdown. Only the three
listed signals are accepted; arbitrary signal delivery is not part of
this RFC. With recursive=false (the default), signal and grace_ms are
ignored and the verbs behave exactly as in RFC-0007. These are additive
optional inputs, so RFC-0004 §7 treats them as compatible; the schema
digest still changes.
Summaries for all three verbs MUST say that the operation is destructive and cannot be undone once the first signal is sent. RFC-0004 format 1 has no MCP annotation member, so generated MCP tools carry this only in their descriptions (see Open questions).
3. Authorization and sealing
Every check happens in the request’s first reactor turn, before any signal, seal or event. Its order:
- RFC-0005 dispatch gate:
writeon some scope. writeon the named target (pane:<id>,window:<id>orsession:<id>), before lookup, so an unauthorized caller getsdenied.writewhether or not the target exists. Then the usualnot_found.<kind>.unavailable.shutdownif the daemon is stopping (RFC-0007 §2, RFC-0009 §4). Authorization still comes first.- For a recursive removal,
writeon every descendant: each window in ascending ID, then each of its panes in ascending ID. Concrete container scopes normally cover descendants (RFC-0005 §5), but kind wildcards andselfdo not always:write window:*holds no right on panes, and aselfgrant does not cover a pane another principal spawned into a window the caller does not own. The first failing descendant ends the request withdenied.write. Following RFC-0005 §7, the error data hasrightandprincipalbut noresource, because the caller did not name the descendant. There is no partial cascade. - Conflict checks: the target or an ancestor already sealed by another
in-flight recursive removal gives
conflict.removing; withrecursive=falsethe RFC-0007conflict.not_emptyandconflict.pane_runningrules apply unchanged.
When every check passes, a recursive removal seals the target and its whole subtree in the same reactor turn. While sealed:
window.createinto a sealed session,pane.splitof a sealed pane,pane.relaunch(RFC-0010) of a sealed archive,pane.removeof a sealed exited pane, ordinarywindow.removeof a sealed empty window, andpane.kill --remove=trueof a sealed pane failconflict.removingwithout effects.pane.killwithremove=falseof a sealed pane joins its termination (§4) and succeeds when that pane has exited.- Reads,
pane.capture, events and RFC-0006 attach (watcher or writer) work as before.
A creation staged before the seal (RFC-0007 §5: principal, PTY and child
prepared, not yet committed) re-checks the seal at commit. A sealed target
makes it fail conflict.removing with RFC-0007’s rollback: kill and reap
the staged child, close its PTY, revoke the staged principal, discard
staged events. A creation that committed before the seal is part of the
subtree, was authorized in step 4 and is terminated with it. No
interleaving can add a pane the authorization did not cover.
Authorization is not repeated at later phases. A grant revoked after the first turn does not stop an operation that already passed (as for in-flight requests in RFC-0005 §6.4), but a retry re-authorizes from scratch.
pane.kill needs write on the pane, as RFC-0005 §4’s table already lists.
A pane principal holding write self may kill its own pane; its response
may then be lost with its connection, which the caller must treat as an
unknown outcome. Killing a pane ends its bound root process, so RFC-0005
§6.4’s automatic revocation of that principal and its descendants applies
unchanged. Panes spawned from it keep running with revoked principals,
exactly as when the root exits on its own.
4. Termination
A pane is running, terminating or exited. terminating is new, can
only be entered through this RFC’s verbs, and is never left except to
exited. A terminating pane still counts as running for RFC-0007’s
conflict.pane_running.
Target. The only signal target is the pane’s process group, created
with the PTY: its ID is the leader’s PID, recorded by the daemon when it
forked the leader. It is never derived from a PID read later, /proc,
OSC data or request params. The daemon MUST NOT signal a process group
ID ≤ 1, its own process group, or any group it did not create in this
daemon instance (RFC-0010 §7 already forbids old ones).
Pinning. A group ID cannot be reused while its leader is an unreaped
zombie. The daemon MUST observe every pane leader’s exit without reaping
it (waitid with WNOWAIT, or a pidfd), finish any group signalling that
exit requires, and only then reap, all in one reactor turn. After a
leader has been reaped, the daemon MUST NOT signal its group again. An
already exited pane is therefore never signalled. Leaders SHOULD be
signalled through a pidfd where the platform has one.
Escalation. In the first turn, the daemon marks each live target
terminating, emits pane.terminating (§6), sends the requested signal
to the group and then SIGCONT, so a stopped job can act on it. It reads
the monotonic clock once (CLOCK_MONOTONIC on Linux) and sets the
deadline to that time plus grace_ms. Wall-clock changes MUST NOT move
it. On Linux, time spent suspended does not count, which can only make
the grace longer.
- If the leader exits before the deadline, the daemon sends one
SIGKILLto the still-pinned group to stop the members left behind (for example a background job of a non-interactive shell that ignored the signal), then reaps.escalatedstays false. - At the deadline, it sends
SIGKILLto the group of each target whose leader has not exited, setsescalated, and starts a fixed 1000 ms kill-wait deadline on the same clock. On the leader’s exit it reaps as above. grace_ms=0sends the requested signal,SIGCONTandSIGKILLin the first turn.
All waiting is reactor timers and child-exit notifications. No step blocks
the reactor, other requests, event delivery or stream output. A request’s
total time is at most grace_ms + 1000 ms plus scheduling delay.
Joining. A pane has at most one termination. A pane.kill or
recursive removal that finds a target already terminating joins it: no
second requested signal is sent and the deadline is not extended.
A joiner with a deadline earlier than the current one shortens it to its
own. A joiner arriving after escalation sends SIGKILL once more (safe
while pinned) and starts its own 1000 ms kill-wait. All joined requests
complete from the same exit and report the same escalated and status.
Input. On entering terminating the pane’s input queue is frozen:
queued bytes not yet written to the PTY are discarded, later RFC-0006
INPUT frames are discarded without closing the stream, and pane.send
fails conflict.pane_terminating. RFC-0007 §5 already says a successful
pane.send means “queued”, not “delivered”. pane.split of a
terminating pane fails conflict.pane_terminating. The authoritative Vt
keeps answering terminal queries until exit, so a program waiting on a
reply is not stalled by the freeze.
Exit. A killed pane’s exit is an ordinary exit: the RFC-0007
observation and pane.exited sequence, its RFC-0006 EXIT frame with the
final offset, and RFC-0005 automatic revocation. The pane, its Vt and its
history are retained until removed.
Not covered. Processes that left the group (setsid, setpgid, the
jobs of an interactive shell with job control, double forks) are not
signalled. They are reparented to the daemon as subreaper and reaped when
they exit, and doctor counts them as escaped descendants (RFC-0005 §12).
Closing the PTY master at removal hangs up the terminal, which most
programs still attached to it obey, but that is best effort. No response
claims they were stopped. OS-backed containment is deferred to COM-262.
5. Completion, removal and failure
A request responds with success only when its completion condition holds:
| Request | Completion condition |
|---|---|
pane.kill, remove=false |
The leader has exited and been reaped after the group sweep; the pane is exited; its exit events are committed. |
pane.kill, remove=true |
As above, then the pane is removed (below). |
Recursive window.remove / session.remove |
Every pane in the subtree is exited and reaped, and the subtree is removed (below). |
Removal commit. Removal happens in one reactor turn, so event
snapshots, RFC-0010 checkpoints and other requests see the tree either
whole or gone. For each removed pane the daemon closes its PTY master,
revokes its principal if still live, queues RFC-0006 CLOSE with
{"reason":"pane_removed"} on every stream after anything already
queued for it, and releases its Vt, history and ring. It then deletes the
topology and appends events in this order:
pane.kill --remove=true: RFC-0007’s pane removal (observation cleanup,pane.removed, thenwindow.layout_changedwith the parent collapsed and the revision incremented).- Recursive
window.remove: for each pane in ascending ID, observation cleanup andpane.removed; thenwindow.removed. No intermediatewindow.layout_changedis emitted for a window that is being removed. - Recursive
session.remove: step 2 for each window in ascending ID, thensession.removed.
IDs are never reused (RFC-0007 §3). In on-demand mode, removing the last
session latches stopping in this same turn (RFC-0009 §4). A
recursive removal of a subtree with no live panes performs its first turn
and its removal commit together, so it is as atomic as RFC-0007 removal.
Removal never waits for a client. A slow or stalled stream gets CLOSE
after the frames already in its bounded backlog (RFC-0006 §6.2); bytes
beyond that are dropped, and RESYNC is no longer possible because the
pane is gone. A slow event subscriber recovers removal events by cursor
replay (§6).
Timeout. If a kill-wait deadline passes with a leader still not exited
(for example a process in uninterruptible sleep), the request fails
timeout.terminate (exit 7). Its data lists the panes still terminating,
all of which the caller was authorized for in §3:
{"code":"timeout.terminate","message":"pane did not exit after SIGKILL","data":{"panes":["p3"]}}
Nothing is removed. Panes that did exit stay as exited panes, the seal is
lifted, and stuck panes stay terminating. The daemon keeps supervising
them: when a stuck leader eventually exits, it is swept, reaped and
becomes an ordinary exited pane, but it is not removed, because no
request is waiting. Doctor reports stuck panes until then.
Cancellation. RFC-0002 cancel, closing the requesting connection,
and daemon shutdown end the request with cancelled (exit 8; on shutdown
the response is best effort). Signals already sent cannot be taken back,
so cancellation does not stop termination: escalation continues to its
deadline and exits are reaped as usual. Cancellation does stop removal:
nothing is removed and the seal is lifted. Cancellation before the first
turn commits has no effect at all. A shutdown that begins during
termination takes over the remaining processes with RFC-0007 §2’s own
SIGHUP/SIGKILL procedure and deadline.
Retries. Every request is safe to repeat:
pane.killon a terminating pane joins; on an exited pane it returnsalready_exitedwithout sending a signal; withremove=trueit removes the exited pane.- A retry after a lost successful
remove=trueor recursive response getsnot_found.<kind>, because IDs are not reused. Clients that lose a response inspect current state, as with RFC-0010 restore. - A recursive retry after
timeout.terminateorcancelledstarts again at §3: re-authorize, re-seal, join any pane still terminating, skip exited ones, then remove.
No automatic retry happens on reconnect, restore or startup.
6. Events and retained authorization
The product event catalog (RFC-0003 §6, RFC-0007 §7) gains one entry and one data field; existing field types do not change:
| Type | Resource | Data | Snapshot entity |
|---|---|---|---|
pane.terminating |
pane:<pane> |
pane string, requested_signal string |
pane, with optional boolean terminating |
pane.terminating is emitted once per termination, in the first turn,
in ascending pane ID order, after the turn’s seal and before any exit.
Joining does not emit it again. requested_signal is the signal from the
request (SIGHUP, SIGTERM or SIGINT); it is named so it is never
confused with the death signal of a wait status. The venomx pane
snapshot entity gains an optional terminating field, present and true
only while the pane is terminating. Escalation and stuck panes are not
events; they are visible in results, errors and doctor.
An event is authorized against its own resource. After a removal that
resource no longer exists, but subscribers can still replay its events
from a cursor within RFC-0003 retention. The daemon MUST therefore keep,
for each removed pane, window and session, a frozen authorization record:
its resource ID, its owner principal ID and that principal’s ancestor
chain, and its own containment ancestors, all as of the removal commit.
read on retained events about it (pane.terminating, pane.exited,
pane.removed, window.removed, session.removed and any older retained
event for it) is evaluated with live grants against this record: a
<kind>:<id> scope matches the resource or a frozen ancestor, self
matches the frozen owner chain. A record is freed when the last retained
event that refers to it leaves the log, so its memory is bounded by
retention. Records hold IDs only, never tokens.
7. Errors
New product errors:
| Code | Exit | When / data |
|---|---|---|
conflict.pane_terminating |
5 | pane.send or pane.split on a terminating pane; empty data |
conflict.removing |
5 | Mutation inside a subtree sealed by an in-flight recursive removal; empty data |
timeout.terminate |
7 | A leader did not exit by its kill-wait deadline; {"panes":[string]} |
Unchanged: denied.write (RFC-0005 data), not_found.pane, .window,
.session, conflict.not_empty and conflict.pane_running for
non-recursive calls, unavailable.shutdown, cancelled, and
invalid_params.enum / .range / .type for bad signal, grace_ms,
remove or recursive. Errors, events and doctor MUST NOT include argv,
except RFC-0007 §5’s derived argv0 in the pane entity and
pane.created, terminal content or credentials.
8. Conformance matrix
COM-246 MUST pass each case against a real headless daemon, with real processes and PTYs, through both the CLI and MCP where a verb is called. “No side effects” means: no signal, no seal, no event, no topology change, no principal change. Cases that need an unkillable or reused process use a daemon test seam that withholds exit notification or injects a recycled ID; the seam is compiled only into test builds.
| # | Setup | Action | Expected |
|---|---|---|---|
| 1 | Pane running /bin/sh |
pane kill |
terminated, escalated=false, signal=SIGHUP; response after reap; pane.terminating then pane.exited; EXIT frame on streams |
| 2 | Pane ignoring HUP/TERM/INT (§1) | pane kill --grace-ms 300 |
escalated=true, signal=SIGKILL; response in [300, 1300] ms plus scheduling |
| 3 | sh -c '(trap "" HUP; sleep 1000) & wait' |
pane kill |
Leader exits on HUP; group sweep kills sleep; no process left in the group |
| 4 | Group stopped with SIGSTOP |
pane kill --signal SIGTERM |
SIGCONT delivered; exits without escalation |
| 5 | Case 2 in progress | Other clients ping, send to and attach other panes, read events | Each answers within normal latency; nothing waits on the grace |
| 6 | Exited pane | pane kill, then with --remove=true |
already_exited, no signal (seam observes none); then removed with RFC-0007 events |
| 7 | Missing ID; caller with and without write |
pane kill, recursive removals |
not_found.* for authorized caller; denied.write identical for existing and missing targets |
| 8 | Session with two windows, nested splits, running, exited and archive panes | session remove --recursive=true |
Event order of §5 exactly; tree, PTYs, principals, Vts gone; IDs not reused |
| 9 | Nonempty window, running pane | window remove, pane remove (no opt-in) |
conflict.not_empty, conflict.pane_running; no side effects |
| 10 | Caller with write session:s0 |
Recursive session remove |
Succeeds |
| 11 | Caller with only write window:*; caller with write self and a pane owned by another principal |
Recursive window remove |
denied.write without resource; no side effects on any pane |
| 12 | Split or window.create staged in a target |
Recursive removal commits first, then the other way round | Either the new pane is part of the subtree and removed, or creation fails conflict.removing with no leaked child, PTY or principal |
| 13 | Pane terminating | pane send, INPUT frames, pane split, pane remove |
conflict.pane_terminating, discarded without stream close, conflict.pane_terminating, conflict.pane_running; queued bytes never reach the PTY |
| 14 | Sealed subtree | Each mutation in §3’s list | conflict.removing; pane kill without remove joins and succeeds |
| 15 | A stream that never reads | Kill and remove its pane | Response independent of the stream; stream gets CLOSE pane_removed after its backlog or is closed by overflow |
| 16 | Subscribers with read pane:p1, read session:s0, unrelated read pane:p9 |
Remove, then replay from an older cursor | Scoped subscribers receive the removal events; the unrelated one never does |
| 17 | Case 2 in progress | cancel, and separately close the connection |
cancelled; process still killed by the deadline; nothing removed; seal lifted; retry succeeds |
| 18 | Seam withholds one leader’s exit | Recursive removal | timeout.terminate with that pane; other panes exited, nothing removed, seal lifted; doctor reports it stuck; later exit leaves an exited, unremoved pane; retry then succeeds |
| 19 | Two concurrent pane kill, the second with a shorter grace |
Both | One requested signal; deadline shortened; both get the same result |
| 20 | Repeat after success | pane kill, --remove=true, recursive removal |
already_exited; not_found.pane; not_found.window / .session |
| 21 | Sentinel processes outside the daemon; seam recycles a reaped leader’s PID | All kill cases | Sentinels, the daemon’s own group and the recycled ID never receive a signal |
| 22 | Pane kills itself with write self |
pane kill --pane <own> |
Pane terminates; principal and descendants revoked; daemon unaffected |
| 23 | Shutdown during case 2 | SIGTERM to serve |
In-flight request cancelled (best effort); RFC-0007 shutdown bound holds |
| 24 | On-demand daemon with one session | Recursive session remove |
stopping latched in the removal turn; later mutation unavailable.shutdown |
| 25 | Recovery enabled; checkpoint during case 2 | Restore that checkpoint | Terminating panes restored as interrupted archives; never a half-removed tree |
| 26 | Any case above | window get, event snapshots during it |
Every published tree valid; revisions only increase; no ghost or duplicate pane |
| 27 | After all cases | Daemon inspection | No zombies, no extra PTY fds, no live principals of removed panes, frozen records freed after retention |
Agent-contract impact
- Verbs/schema: one new verb,
pane.kill, and optional parameters onwindow.removeandsession.remove(§2), generated into CLI, MCP, docs and completions from the one manifest. No new grant names, no local dispatch, no format change. - JSON/exits: standard. New errors in §7;
timeout.terminateuses the existingtimeoutclass (exit 7). - Events:
pane.terminatingand therequested_signalfield, theterminatingsnapshot field, and frozen authorization records for replay of removal events (§6). - Skill: the generated skill MUST say that
pane killand--recursive=trueare destructive and irreversible once started, needwriteon every affected resource, are never implied by plainremove, and that clients should inspect withwindow get/session getfirst and treat a lost response as unknown. - Doctor: add
panes.terminating(info: count),panes.stuck(warn: IDs of panes past their kill-wait deadline), counters for kills, escalations,timeout.terminate, discarded input bytes and escaped descendants reaped, andevents.removed_records(count). Standard redaction applies. - Config: none. Signal and grace are per request and bounded by the schema. No setting changes RFC-0007’s shutdown deadline.
- Grants:
writeon the target and, for recursive removal, on every descendant, checked before effects (§3). RFC-0005 revocation and audit are unchanged; eachdenied.writeis audited as usual. - Headless/OSC: fully headless. No OSC change; a pane’s OSC events stop at exit as today.
Alternatives
- Make plain
removekill: breaks RFC-0007 callers that rely onconflict.pane_runningandconflict.not_emptyas a safety check. - Fire-and-forget kill (respond after sending the signal): cannot tell the caller whether cleanup happened, and leaves “kill then remove” to a polling loop. A bounded wait is cheap because it is asynchronous.
- Best-effort recursive removal (remove what you may): an authorized root with an unauthorized descendant would leave a partial cascade. All-or-nothing authorization is simpler to reason about.
- Remove each pane as it exits: clients would see a series of shrinking trees and could act on a half-removed window. One commit at the end keeps topology atomic.
- Roll back on timeout or cancel: a delivered signal cannot be undone; pretending otherwise would misreport state.
- Signal the whole terminal session or walk
/proc: needs process enumeration, races with PID reuse and is not a containment boundary. OS containment is the right fix and is deferred (COM-262). - Configurable defaults: a per-daemon grace would make the same call behave differently on different machines. Per-request values suffice.
- Do nothing: clients keep scripting send/poll/remove loops, and a hung program can only be stopped by stopping the daemon.
Open questions
- Arbitrary signal delivery (
SIGUSR1,SIGWINCH, …): resolved, out of scope. This RFC only terminates; a signal-sending verb needs its own proposal. - Escaped descendants and OS-backed containment: deferred to COM-262. Until then only the pinned process group is signalled and nothing claims more (§4).
- MCP destructive annotations: deferred to COM-263. Until then the verb summaries carry the warning (§2).
- Configurable grace or kill-wait: resolved, no config (Agent-contract impact). The fixed 1000 ms kill-wait can be revisited by measurement.
The maintainer accepted this RFC and its resolved/deferred questions on 2026-11-07. RFC-0007’s History records that this RFC now covers the live-pane kill and recursive removal that RFC-0007 §5 deferred.