Documentation
¶
Overview ¶
Package controller manages the lifecycle of a single cliwrap-agent subprocess and its managed child. Each Controller owns one agent connection, tracks state transitions, and translates inbound IPC messages into observable state changes.
Index ¶
- func ReadHandshakeHello(conn net.Conn) (ipc.HelloPayload, error)
- func ReadHandshakePTYRingDump(conn net.Conn) (ipc.PTYRingDumpPayload, error)
- type Controller
- func (c *Controller) AgentSupportsFrameAck() bool
- func (c *Controller) AgentSupportsPTY() bool
- func (c *Controller) AgentSupportsPersistence() bool
- func (c *Controller) ChildPID() int
- func (c *Controller) Close(ctx context.Context) error
- func (c *Controller) CloseConnOnly(ctx context.Context) error
- func (c *Controller) CrashSource() CrashSource
- func (c *Controller) Options() ControllerOptions
- func (c *Controller) SendPTY(_ context.Context, msgType ipc.MsgType, payload []byte) error
- func (c *Controller) SetInitialPTYBuffer(b []byte)
- func (c *Controller) SetOnStateChange(cb func(cwtypes.State))
- func (c *Controller) Start(ctx context.Context) error
- func (c *Controller) State() cwtypes.State
- func (c *Controller) Stop(ctx context.Context) error
- func (c *Controller) SubscribePTYData() (ch <-chan PTYData, unsubscribe func())
- type ControllerOptions
- type CrashInfo
- type CrashSource
- type Heartbeat
- type HeartbeatOptions
- type PTYData
- type PingSender
- type ReattachOptions
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ReadHandshakeHello ¶ added in v0.3.0
func ReadHandshakeHello(conn net.Conn) (ipc.HelloPayload, error)
ReadHandshakeHello reads exactly one MsgHello frame from conn and decodes the HelloPayload. Used by Manager.Reattach (CW-G4) to verify StartedAt before constructing a Controller.
func ReadHandshakePTYRingDump ¶ added in v0.3.0
func ReadHandshakePTYRingDump(conn net.Conn) (ipc.PTYRingDumpPayload, error)
ReadHandshakePTYRingDump reads exactly one MsgTypePTYRingDump frame and decodes the PTYRingDumpPayload. Used by Manager.Reattach (CW-G4) after ReadHandshakeHello.
Types ¶
type Controller ¶
type Controller struct {
// contains filtered or unexported fields
}
Controller manages exactly one agent subprocess and its child.
func NewController ¶
func NewController(opts ControllerOptions) (*Controller, error)
NewController constructs a Controller.
func NewControllerFromReattach ¶ added in v0.3.0
func NewControllerFromReattach(opts ReattachOptions) (*Controller, error)
NewControllerFromReattach constructs a Controller around a connection to an already-running persistent agent. Returns the Controller in StateRunning with the initial PTY buffer queued for the next SubscribePTYData subscriber.
CW-G4 Task 14.
func (*Controller) AgentSupportsFrameAck ¶ added in v0.4.4
func (c *Controller) AgentSupportsFrameAck() bool
AgentSupportsFrameAck reports whether the connected agent advertised the "frame_ack" feature: it emits MsgAckData in response to any inbound frame whose header has FlagAckRequired set. When set, Controller.Stop uses Conn.SendAndAwaitAck to turn MsgStopChild into a synchronous request/response. When unset, Stop falls back to the historical fire-and-forget path. v0.4.4+.
func (*Controller) AgentSupportsPTY ¶ added in v0.3.0
func (c *Controller) AgentSupportsPTY() bool
AgentSupportsPTY reports whether the connected agent advertised the "pty" feature during capability negotiation.
func (*Controller) AgentSupportsPersistence ¶ added in v0.3.0
func (c *Controller) AgentSupportsPersistence() bool
AgentSupportsPersistence reports whether the connected agent advertised the "persistence" feature during capability negotiation. Used by processHandle.Start to refuse Persistent=true against pre-CW-G4 agents.
func (*Controller) ChildPID ¶
func (c *Controller) ChildPID() int
ChildPID returns the PID of the running child (0 if none).
func (*Controller) Close ¶
func (c *Controller) Close(ctx context.Context) error
Close tears everything down. Idempotent.
func (*Controller) CloseConnOnly ¶ added in v0.3.0
func (c *Controller) CloseConnOnly(ctx context.Context) error
CloseConnOnly closes the IPC connection without sending SIGTERM to the agent. Used by ProcessHandle.Close on persistent sessions (CW-G4) — the agent should remain alive in detached mode, ready for next reattach.
For reattach-derived controllers, c.handle is nil already, so Close and CloseConnOnly are equivalent. The asymmetry only matters for the original spawning host's controller, where c.handle holds the AgentHandle whose Close would SIGTERM the agent.
func (*Controller) CrashSource ¶ added in v0.3.0
func (c *Controller) CrashSource() CrashSource
CrashSource returns the source that triggered the last crash transition. The value is only meaningful when State() == StateCrashed.
func (*Controller) Options ¶ added in v0.2.0
func (c *Controller) Options() ControllerOptions
Options returns the options the controller was constructed with. Exposed for test inspection; production code should not rely on this.
func (*Controller) SendPTY ¶ added in v0.3.0
SendPTY sends a raw PTY control frame to the agent. payload must already be encoded (e.g. via ipc.EncodePTYWrite). The context is present for interface symmetry and future flow-control; the send is currently non-blocking relative to ctx.
func (*Controller) SetInitialPTYBuffer ¶ added in v0.3.0
func (c *Controller) SetInitialPTYBuffer(b []byte)
SetInitialPTYBuffer stores the ring buffer snapshot received via MsgPTYRingDump on reattach. The buffer is delivered as the first chunk(s) to the next SubscribePTYData subscriber; subsequent subscribers see only live data.
CW-G4. Idempotent (last-write-wins) but typically called once per reattach.
func (*Controller) SetOnStateChange ¶ added in v0.4.0
func (c *Controller) SetOnStateChange(cb func(cwtypes.State))
SetOnStateChange registers cb to be called every time the controller's state transitions to a new value. The callback is also invoked when setState is called with the same value as the current state? No — duplicate transitions are filtered: cb fires only when old != new. Pass nil to disable. Replacing an existing callback is supported and atomic with respect to setState.
The callback runs on the goroutine that triggered the transition — usually the IPC dispatch loop or a host-initiated Start/Stop call — so it MUST NOT block. Hosts that need to do heavy work should hand off to a separate goroutine.
func (*Controller) Start ¶
func (c *Controller) Start(ctx context.Context) error
Start spawns the agent and issues START_CHILD.
func (*Controller) State ¶
func (c *Controller) State() cwtypes.State
State returns the current state.
func (*Controller) Stop ¶
func (c *Controller) Stop(ctx context.Context) error
Stop sends MsgStopChild.
When the connected agent advertises the `frame_ack` capability, Stop blocks (until ctx) on the agent's MsgAckData reply, which the agent emits AFTER `dispatcher.stopChild` has cancelled the runner and awaited its wg. The caller is therefore guaranteed that by the time Stop returns nil, the child is dead and the runner has fully cleaned up. Older agents that lack the capability fall back to fire-and- forget (current historical behaviour) — Stop returns immediately once the frame is enqueued.
func (*Controller) SubscribePTYData ¶ added in v0.3.0
func (c *Controller) SubscribePTYData() (ch <-chan PTYData, unsubscribe func())
SubscribePTYData registers a new PTY data subscriber. The returned channel receives PTYData values for every inbound MsgTypePTYData frame. The caller MUST invoke the returned unsubscribe function exactly once when done, typically via defer. Failing to call it leaks the channel.
If the controller is not configured for PTY mode the caller should guard with Spec.PTY != nil before calling this method. SubscribePTYData itself does not validate the spec; it simply registers a receiver.
type ControllerOptions ¶
type ControllerOptions struct {
Spec cwtypes.Spec
Spawner *supervise.Spawner
RuntimeDir string
// PersistentDir is the parent directory for per-session metadata
// (CW-G4). Used only when Spec.Persistent=true. Caller (Manager)
// passes Manager.persistentDir.
PersistentDir string
// OutboxCapacity overrides the in-memory IPC outbox slot count. Zero
// uses the legacy default (1024). Larger values reduce the chance of
// overflow under bursty traffic at the cost of memory.
OutboxCapacity int
// DisableWAL skips the disk-backed write-ahead log on the outgoing
// IPC direction. When true, outbox overflow drops messages rather
// than fsync-spilling to disk; the caller accepts the rare-drop
// trade-off in exchange for predictable latency under bursty input.
// Default (false) preserves the legacy "messages are never lost"
// guarantee.
DisableWAL bool
// OnLogChunk is invoked for every inbound ipc.MsgLogChunk frame.
// The callback runs on the ipc.Conn dispatch goroutine, so it must
// not block for long. Callers that need to do heavy work should
// hand the data off to a separate goroutine. May be nil, in which
// case log chunks are dropped.
OnLogChunk func(stream uint8, data []byte)
}
ControllerOptions configures a Controller.
type CrashInfo ¶
type CrashInfo struct {
Source CrashSource
AgentExit int
AgentSig int
ChildExit int
ChildSig int
Reason string
DetectedAt time.Time
}
CrashInfo unifies information from all four crash-detection layers. The earliest-arriving source wins for the "primary" fields; Layer 4 (OS wait) augments but does not overwrite.
func (*CrashInfo) Record ¶
func (c *CrashInfo) Record(src CrashSource, data CrashInfo)
Record merges data from a detection source. The first call wins for Source/Reason/ChildExit/ChildSig; OS wait data is always merged in.
type CrashSource ¶
type CrashSource int
CrashSource identifies which of the 4 crash-detection layers fired first.
const ( CrashSourceNone CrashSource = iota CrashSourceExplicit CrashSourceConnectionLost CrashSourceHeartbeat CrashSourceOSWait )
func (CrashSource) String ¶
func (s CrashSource) String() string
String returns the lowercase name of the crash source.
type Heartbeat ¶
type Heartbeat struct {
// contains filtered or unexported fields
}
Heartbeat sends periodic PINGs and fires OnMiss after the configured number of consecutive missing PONGs.
func NewHeartbeat ¶
func NewHeartbeat(opts HeartbeatOptions) *Heartbeat
NewHeartbeat returns a Heartbeat.
func (*Heartbeat) RecordPong ¶
func (h *Heartbeat) RecordPong()
RecordPong resets the miss counter to zero.
type HeartbeatOptions ¶
type HeartbeatOptions struct {
Sender PingSender
Interval time.Duration
Timeout time.Duration
MissThreshold int
OnMiss func(consecutiveMisses int)
}
HeartbeatOptions configures a Heartbeat.
type PingSender ¶
type PingSender interface {
SendPing() error
}
PingSender is any object that can emit a PING message.
type ReattachOptions ¶ added in v0.3.0
type ReattachOptions struct {
Spec cwtypes.Spec
Conn net.Conn // post-handshake; controller wraps in ipc.Conn
InitialBuffer []byte // ring buffer dump from agent
RuntimeDir string
PersistentDir string
OnLogChunk func(stream uint8, data []byte)
// OutboxCapacity / DisableWAL mirror ControllerOptions; passed through
// to the freshly-constructed ipc.Conn for the reattach connection.
OutboxCapacity int
DisableWAL bool
}
ReattachOptions carries the inputs needed to construct a Controller around an already-handshaked persistent session conn (CW-G4 Task 14).
The caller (Manager.Reattach) has already: 1. Verified the session metadata and pid alive. 2. Dialed the per-session UNIX socket. 3. Read the Hello frame and verified StartedAt against meta.StartedAt. 4. Read the PTYRingDump frame.
NewControllerFromReattach takes ownership of Conn from this point; further reads belong to the IPC layer.