What minimum ExecutionBackend and ProcessHandle contract should replace the ChildProcessWithoutNullStreams assumption while preserving the current local Pi path and establishing an OCI-oriented seam? Settle stream, readiness, exit, wait, identity, inspect, graceful/forced termination, and cleanup semantics, including where compatibility adaptation ends.
Part of #1
Blocked by: #2, #3
## Question
What minimum ExecutionBackend and ProcessHandle contract should replace the ChildProcessWithoutNullStreams assumption while preserving the current local Pi path and establishing an OCI-oriented seam? Settle stream, readiness, exit, wait, identity, inspect, graceful/forced termination, and cleanup semantics, including where compatibility adaptation ends.
Claimed for this Wayfinder session. I will trace the existing Pi process assumptions, compare deep-module interface alternatives, and resolve the minimum backend-neutral contract without implementing downstream supervisor or persistence tickets.
Claimed for this Wayfinder session. I will trace the existing Pi process assumptions, compare deep-module interface alternatives, and resolve the minimum backend-neutral contract without implementing downstream supervisor or persistence tickets.
Choose a lifecycle-oriented, backend-neutral process seam. LocalExecutionBackend and OciSandboxBackend are the first two adapters. The seam sits below provider protocol handling and above process/resource ownership.
ProcessRef contains a backend identifier, an opaque backend resource ID, and lifetime: "handle" | "backend".
handle means the resource is owned by the current daemon attachment and is not adoptable after daemon loss. Local execution uses this lifetime.
backend means the resource may outlive the daemon attachment and can be passed to attach, inspect, and cleanup. Supervisor-owned sandbox execution uses this lifetime.
The opaque resource ID is not a PID, container ID, bundle path, bridge token, or supervisor protocol object at this seam.
ProcessInspection reports only starting, running, exited, or missing; an unreachable backend is a typed error rather than an unknown state. An exited inspection contains the same immutable ProcessExit returned by wait().
Streams and readiness
The interface uses Node Writable/Readable byte streams so the local adapter does not copy or reinterpret provider traffic.
stdin, stdout, and stderr are attachment-scoped. Each output channel preserves byte order. No cross-channel ordering is promised.
Writes honor normal Writable backpressure and fail explicitly; adapters may not silently drop input or output.
start and attach resolve once the backend has accepted the operation and the attachment can accept input and deliver output. This is transport readiness, not provider-protocol readiness.
Pi retains its current behavior of sending RPC immediately. Providers that have a ready frame or negotiation keep that logic above this seam.
A reconnecting adapter must deliver replay before live output. Cursor, acknowledgment, duplicate suppression, and writer fencing belong to the reconnectable Pi transport/bridge protocol, not the generic process interface.
Exit, wait, termination, release, and cleanup
wait() is idempotent. Every call resolves to the same known exit and never treats a non-zero exit as a rejected promise.
Spawn/start failure rejects start. Attachment or control loss before a known exit rejects wait with a typed availability error; callers must use inspect(ref) before claiming the resource exited.
terminate("graceful") and terminate("force") are idempotent and return the known exit. Graceful termination has a bounded backend policy and throws a typed timeout if the resource remains alive; it never silently escalates. The caller may then request force explicitly.
For local Pi, JsonlRpcProcess.close() preserves the current sequence: end stdin, request graceful tree termination, log on timeout, request forced tree termination, and log if a terminal exit still cannot be observed.
release() drops the daemon attachment and never claims that the resource exited. It is allowed for a running backend-lifetime resource. Releasing a running handle-lifetime resource fails as not detachable; local callers terminate first.
cleanup(ref) removes exited/missing backend resources and bookkeeping. It is idempotent, does not imply termination, and fails while the resource is known running.
Compatibility boundary
LocalExecutionBackend is the only adapter that imports Node ChildProcess, spawnProcess, and tree-kill behavior.
JsonlRpcProcess accepts a ProcessHandle; it continues to own JSONL framing, chunk reassembly, request IDs, timeouts, response correlation, stderr diagnostics, and pending-request failure.
PiRuntimeSession remains unchanged. Provider orchestration does not learn backend kinds, PIDs, containers, runsc, Unix sockets, replay cursors, mounts, networks, or credentials.
OciSandboxBackend translates ProcessSpec into the restricted supervisor's approved launch profile. The supervisor protocol never receives arbitrary daemon-supplied OCI JSON, runtime flags, capabilities, mounts, or raw host paths.
The backend registry remains an internal core seam. Current Paseo plugins cannot register execution backends; future plugin registration can target this interface without changing provider callers.
Rejected shapes
A child-process-only adapter was too shallow: it would require a second interface for durable identity, attach, inspection, and cleanup.
A capability-rich transport exposed replay cursors, writer epochs, readiness promises, and policy hints to every caller. Those facts belong to the bridge, supervisor, mount, network, and lifecycle modules and remain open in their owning tickets.
Human decision: accept the lifecycle-oriented process seam.
## Resolution
Choose a lifecycle-oriented, backend-neutral process seam. `LocalExecutionBackend` and `OciSandboxBackend` are the first two adapters. The seam sits below provider protocol handling and above process/resource ownership.
```ts
interface ExecutionBackend {
start(spec: ProcessSpec): Promise<ProcessHandle>;
attach(ref: ProcessRef): Promise<ProcessHandle>;
inspect(ref: ProcessRef): Promise<ProcessInspection>;
cleanup(ref: ProcessRef): Promise<void>;
}
interface ProcessHandle {
readonly ref: ProcessRef;
readonly stdin: Writable;
readonly stdout: Readable;
readonly stderr: Readable;
wait(): Promise<ProcessExit>;
terminate(mode: "graceful" | "force"): Promise<ProcessExit>;
release(): Promise<void>;
}
```
### Identity and state
`ProcessRef` contains a backend identifier, an opaque backend resource ID, and `lifetime: "handle" | "backend"`.
- `handle` means the resource is owned by the current daemon attachment and is not adoptable after daemon loss. Local execution uses this lifetime.
- `backend` means the resource may outlive the daemon attachment and can be passed to `attach`, `inspect`, and `cleanup`. Supervisor-owned sandbox execution uses this lifetime.
- The opaque resource ID is not a PID, container ID, bundle path, bridge token, or supervisor protocol object at this seam.
`ProcessInspection` reports only `starting`, `running`, `exited`, or `missing`; an unreachable backend is a typed error rather than an `unknown` state. An exited inspection contains the same immutable `ProcessExit` returned by `wait()`.
### Streams and readiness
- The interface uses Node `Writable`/`Readable` byte streams so the local adapter does not copy or reinterpret provider traffic.
- stdin, stdout, and stderr are attachment-scoped. Each output channel preserves byte order. No cross-channel ordering is promised.
- Writes honor normal `Writable` backpressure and fail explicitly; adapters may not silently drop input or output.
- `start` and `attach` resolve once the backend has accepted the operation and the attachment can accept input and deliver output. This is transport readiness, not provider-protocol readiness.
- Pi retains its current behavior of sending RPC immediately. Providers that have a ready frame or negotiation keep that logic above this seam.
- A reconnecting adapter must deliver replay before live output. Cursor, acknowledgment, duplicate suppression, and writer fencing belong to the reconnectable Pi transport/bridge protocol, not the generic process interface.
### Exit, wait, termination, release, and cleanup
- `wait()` is idempotent. Every call resolves to the same known exit and never treats a non-zero exit as a rejected promise.
- Spawn/start failure rejects `start`. Attachment or control loss before a known exit rejects `wait` with a typed availability error; callers must use `inspect(ref)` before claiming the resource exited.
- `terminate("graceful")` and `terminate("force")` are idempotent and return the known exit. Graceful termination has a bounded backend policy and throws a typed timeout if the resource remains alive; it never silently escalates. The caller may then request force explicitly.
- For local Pi, `JsonlRpcProcess.close()` preserves the current sequence: end stdin, request graceful tree termination, log on timeout, request forced tree termination, and log if a terminal exit still cannot be observed.
- `release()` drops the daemon attachment and never claims that the resource exited. It is allowed for a running backend-lifetime resource. Releasing a running handle-lifetime resource fails as not detachable; local callers terminate first.
- `cleanup(ref)` removes exited/missing backend resources and bookkeeping. It is idempotent, does not imply termination, and fails while the resource is known running.
### Compatibility boundary
- `LocalExecutionBackend` is the only adapter that imports Node `ChildProcess`, `spawnProcess`, and tree-kill behavior.
- `JsonlRpcProcess` accepts a `ProcessHandle`; it continues to own JSONL framing, chunk reassembly, request IDs, timeouts, response correlation, stderr diagnostics, and pending-request failure.
- `PiRuntimeSession` remains unchanged. Provider orchestration does not learn backend kinds, PIDs, containers, runsc, Unix sockets, replay cursors, mounts, networks, or credentials.
- `OciSandboxBackend` translates `ProcessSpec` into the restricted supervisor's approved launch profile. The supervisor protocol never receives arbitrary daemon-supplied OCI JSON, runtime flags, capabilities, mounts, or raw host paths.
- The backend registry remains an internal core seam. Current Paseo plugins cannot register execution backends; future plugin registration can target this interface without changing provider callers.
### Rejected shapes
- A child-process-only adapter was too shallow: it would require a second interface for durable identity, attach, inspection, and cleanup.
- A capability-rich transport exposed replay cursors, writer epochs, readiness promises, and policy hints to every caller. Those facts belong to the bridge, supervisor, mount, network, and lifecycle modules and remain open in their owning tickets.
Human decision: accept the lifecycle-oriented process seam.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of #1
Blocked by: #2, #3
Question
What minimum ExecutionBackend and ProcessHandle contract should replace the ChildProcessWithoutNullStreams assumption while preserving the current local Pi path and establishing an OCI-oriented seam? Settle stream, readiness, exit, wait, identity, inspect, graceful/forced termination, and cleanup semantics, including where compatibility adaptation ends.
jb referenced this issue2026-08-22 22:21:43 -04:00
Claimed for this Wayfinder session. I will trace the existing Pi process assumptions, compare deep-module interface alternatives, and resolve the minimum backend-neutral contract without implementing downstream supervisor or persistence tickets.
Resolution
Choose a lifecycle-oriented, backend-neutral process seam.
LocalExecutionBackendandOciSandboxBackendare the first two adapters. The seam sits below provider protocol handling and above process/resource ownership.Identity and state
ProcessRefcontains a backend identifier, an opaque backend resource ID, andlifetime: "handle" | "backend".handlemeans the resource is owned by the current daemon attachment and is not adoptable after daemon loss. Local execution uses this lifetime.backendmeans the resource may outlive the daemon attachment and can be passed toattach,inspect, andcleanup. Supervisor-owned sandbox execution uses this lifetime.ProcessInspectionreports onlystarting,running,exited, ormissing; an unreachable backend is a typed error rather than anunknownstate. An exited inspection contains the same immutableProcessExitreturned bywait().Streams and readiness
Writable/Readablebyte streams so the local adapter does not copy or reinterpret provider traffic.Writablebackpressure and fail explicitly; adapters may not silently drop input or output.startandattachresolve once the backend has accepted the operation and the attachment can accept input and deliver output. This is transport readiness, not provider-protocol readiness.Exit, wait, termination, release, and cleanup
wait()is idempotent. Every call resolves to the same known exit and never treats a non-zero exit as a rejected promise.start. Attachment or control loss before a known exit rejectswaitwith a typed availability error; callers must useinspect(ref)before claiming the resource exited.terminate("graceful")andterminate("force")are idempotent and return the known exit. Graceful termination has a bounded backend policy and throws a typed timeout if the resource remains alive; it never silently escalates. The caller may then request force explicitly.JsonlRpcProcess.close()preserves the current sequence: end stdin, request graceful tree termination, log on timeout, request forced tree termination, and log if a terminal exit still cannot be observed.release()drops the daemon attachment and never claims that the resource exited. It is allowed for a running backend-lifetime resource. Releasing a running handle-lifetime resource fails as not detachable; local callers terminate first.cleanup(ref)removes exited/missing backend resources and bookkeeping. It is idempotent, does not imply termination, and fails while the resource is known running.Compatibility boundary
LocalExecutionBackendis the only adapter that imports NodeChildProcess,spawnProcess, and tree-kill behavior.JsonlRpcProcessaccepts aProcessHandle; it continues to own JSONL framing, chunk reassembly, request IDs, timeouts, response correlation, stderr diagnostics, and pending-request failure.PiRuntimeSessionremains unchanged. Provider orchestration does not learn backend kinds, PIDs, containers, runsc, Unix sockets, replay cursors, mounts, networks, or credentials.OciSandboxBackendtranslatesProcessSpecinto the restricted supervisor's approved launch profile. The supervisor protocol never receives arbitrary daemon-supplied OCI JSON, runtime flags, capabilities, mounts, or raw host paths.Rejected shapes
Human decision: accept the lifecycle-oriented process seam.