Repository navigation
feat(api)!: define lifecycle semantics for bidirectional streams #3056
Description
User Story
As a client implementing exec, TCP forwarding, or relay protocols, I want explicit stream lifecycle semantics, so that I can half-close input, drain output, cancel cleanly, and distinguish normal completion from transport failure.
Problem Statement
OpenShell bidirectional frame protocols define initialization and data payloads but do not consistently define explicit half-close or close frames. Cancellation, first-frame validation, cross-direction ordering, backpressure, deadlines, and retry behavior are incomplete or distributed across implementation details.
Impact / Why This Matters
SDKs must infer when closing their send direction should preserve the receive direction, and gateways/supervisors can interpret EOF differently. This creates hangs, truncated output, premature relay teardown, and language-specific behavior. Protocol ambiguity becomes harder to correct after the beta compatibility boundary.
Proposed Design
Specify lifecycle state machines for each public bidirectional RPC. Where transport EOF is insufficient or ambiguous, add explicit close/half-close frames carrying the channel and optional reason. Define:
- required first frame and rejection behavior;
- legal frame ordering;
- input half-close while output continues;
- normal completion versus cancellation and error;
- backpressure and maximum frame behavior;
- idle and operation deadlines; and
- whether and how a failed stream can be retried.
Expose equivalent semantics through every SDK.
Acceptance Criteria
- Each bidirectional RPC has a documented lifecycle/state machine.
- TCP forward and relay protocols support clean half-close where the underlying TCP flow requires it.
- Invalid first frames and illegal frame ordering return consistent status codes.
- Client cancellation, server cancellation, idle timeout, and normal EOF are distinguishable.
- Implementations do not truncate remaining output when the input direction closes normally.
- Rust, Python, TypeScript, and Go SDK helpers expose consistent lifecycle behavior.
- Tests cover both half-close directions, simultaneous close, cancellation, timeout, backpressure, and peer failure.
- Gateway/supervisor version-skew expectations for new frames are documented.
Alternatives Considered
Use gRPC stream EOF as the only close signal. This is simple but cannot always express per-channel closure or preserve the opposite direction consistently. Add close semantics only in SDKs. That leaves raw clients and gateway/supervisor peers with an underspecified wire contract.
Agent Investigation
TcpForwardFrame and RelayFrame currently carry initialization and data without a common explicit close payload, while related control streams define separate relay-close messages. Capability/version-skew handling is related to #2949.
Related: #2565, #2949. Source audit: https://gist.github.com/mrunalp/e80942c1544a0225ee588796a41ab30b.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
0.1.0#2565varshaprasad96 commentedon Sep 10, 2026
varshaprasad96 commentedon Sep 10, 2026
Implementation Plan
Issue type:
featComplexity: High
Confidence: Medium — the protocol path is clear; breadth across runtimes and four SDKs is substantial.
Summary
Define explicit lifecycle contracts for all four bidirectional RPCs. Add an in-band, direction-implicit half-close frame only to
ForwardTcpandRelayStream; retain transport EOF forExecSandboxInteractiveinput andConnectSupervisor, where it closes the only request direction without needing the stream to remain writable.Deliver this as two independently mergeable PRs: first the wire/runtime contract, CLI, documentation, and end-to-end behavior; second curated SDK parity across Rust, Python, TypeScript, and Go.
Minimal Contract
StreamHalfClosepayload toTcpForwardFrameandRelayFrame.InvalidArgument.InvalidArgument: first-frame or ordering violation.ResourceExhausted: data frame exceeds the documented 64 KiB maximum.Cancelled: intentional remote cancellation.DeadlineExceeded: server-side idle timeout.Unavailable: peer or transport failure.ExecSandboxExitwith an additive termination reason:PROCESS_EXITEDOPERATION_TIMEOUTPROCESS_EXITEDfor older gateways.TcpForwardInit,SupervisorHello, andSessionAccepted.stream-half-close-v1.ConnectSupervisorwith existing backoff after transport failure or idle timeout.Transport EOF Assessment
StreamHalfClose.Scope
PR 1: Protocol, runtime, CLI, and documentation
proto/openshell.proto: addStreamHalfClose, frame variants, capability fields, documented lifecycle/status tables, 64 KiB data limit, and exec termination reason.sdk/go/proto/openshellv1/*.pb.go: regenerate committed Go bindings.crates/openshell-server/src/grpc/sandbox.rs: coordinate ForwardTcp directions; drain output after input close; propagate stable statuses; reject illegal interactive-exec frames; populate timeout reason.crates/openshell-server/src/supervisor_session.rs: negotiate capabilities; implement relay half-close; enforce ConnectSupervisor ordering and heartbeat timeout.crates/openshell-supervisor-process/src/supervisor_session.rs: negotiate capabilities; relay FIN independently; classify clean close, timeout, cancellation, and failure.architecture/gateway.mdand published reference docs: document lifecycle, liveness, limits, retry, and version skew.PR 2: SDK parity
closeInput(),cancel(), terminal reason, and local-versus-remote cancellation; preserve compatibility aliases where possible.CloseWrite/graceful input close separately fromCancel/full close; stop cancelling immediately afterCloseSend.Implementation Steps
AwaitInit → Open → LocalHalfClosed/RemoteHalfClosed → Closed, with fatal protocol/error states.mise run pre-commit,mise run test,mise run go:ci,mise run sdk:ts:ci, and the relevant Docker E2E lane.Test Plan
mise run e2e:docker.Risks & Open Questions
Statusmust terminate all bridge tasks deterministically; detached tasks must not outlive the RPC.Close()meanings differ by language. Preserve compatibility aliases, but make graceful input close and forceful cancellation explicit.Documentation Impact
architecture/gateway.mdand the support matrix.docs/reference/gateway-config.mdxchange: no gateway TOML field, driver option, default, or Helm rendering changes.LSM Compatibility
No SELinux/AppArmor-specific impact is expected. This changes protobuf framing, socket shutdown, async coordination, and SDK APIs; it does not add
/procinspection, process identity lookup, privilege changes, filesystem labeling, or new binary execution.Revision 1 — initial plan against
ea8eda6d5varshaprasad96 commentedon Sep 10, 2026
Will work on the open questions, and get back on the issue.
varshaprasad96 commentedon Sep 14, 2026
Answers to some open questions, please let me know if there are objections:
A few tradeoffs behind these decisions:
varshaprasad96 commentedon Sep 15, 2026
/assign @varshaprasad96
varshaprasad96 commentedon Sep 22, 2026
PR #3359 is merged. It addresses the interactive-exec slice: request EOF closes input while output continues draining, process exit is distinguished from terminal gRPC completion, and Go/TypeScript expose explicit lifecycle controls while preserving existing interfaces.
I propose tackling the remaining work in three scoped follow-ups:
RelayClosehandling, consistent frame validation/status mapping, and mixed-version tests.For the first follow-up, we still need to pin down status/reason mappings, precedence when terminal events race, and exact legacy-peer fallback behavior.
The September 14 decisions should supersede conflicting parts of the original plan: use request EOF plus capability-gated response half-close, and allow healthy silent connections rather than introducing a default application-idle timeout.
Keeping #3056 open until the remaining cross-RPC, cross-SDK, and version-skew acceptance criteria are covered.
varshaprasad96 commentedon Sep 22, 2026
While preparing the next ForwardTcp/RelayStream slice against current main, we found that HA routing also uses a gateway-to-gateway
PeerRelaystream:client → receiving gateway → owning gateway → supervisor → targetRecommendation: include PeerRelay's directional-close and error-propagation behavior in this slice. Otherwise, the new half-close guarantee would depend on routing: a direct connection could drain correctly while the same operation routed through another gateway retains the existing truncation risk. This is a proposed scope addition, not yet implemented.
The main implications are:
Capability advertisement should use existing initialization messages, avoiding a separate negotiation round trip before application data.
The alternative is a smaller, explicitly single-gateway slice with a documented HA limitation. Including PeerRelay is preferable for a consistent end-to-end forwarding contract. Heartbeat/reconnect policy, exec process-group cancellation, and remaining SDK parity stay in their separate follow-ups.
feat(streams)!: enforce forwarding and relay lifecycle
feat(streams)!: enforce forwarding and relay lifecycle
github-actions commentedon Oct 7, 2026
This issue has had no activity for 14 days and is now marked stale. It may be closed in 7 days if there is no further activity. Comment or remove the state:stale label to keep it open.
github-actions commentedon Oct 22, 2026
Closing this issue after 14 days with no activity since it was marked stale. Reopen it if the work is still relevant.