Documentation
¶
Overview ¶
Package nativeturn is the serve-level survival layer for native ACP chain turns: it runs an in-flight turn on a serve-rooted Registry, off any single connection, so a dropped connection detaches a viewer without cancelling the turn. Anti-zombie guarantees are enforced by four belts: a last-viewer grace timer, a hard per-turn deadline, a bounded replay journal, and a periodic reaper backstop. Every exported type is safe for concurrent use.
Index ¶
- Constants
- Variables
- type Config
- type Event
- type Registry
- func (r *Registry) AttachIfRunning(ctx context.Context, sid libacp.SessionID, viewer Viewer) (*Turn, bool, error)
- func (r *Registry) Cancel(sid libacp.SessionID) bool
- func (r *Registry) Close() error
- func (r *Registry) Get(sid libacp.SessionID) (TurnStatus, bool)
- func (r *Registry) List() []TurnStatus
- func (r *Registry) ReapIdle(_ context.Context) error
- func (r *Registry) Start(sid libacp.SessionID, fn TurnFunc, viewer Viewer) (*Turn, bool, error)
- func (r *Registry) Stop(sid libacp.SessionID) bool
- type Result
- type Turn
- type TurnFunc
- type TurnStatus
- type Viewer
Constants ¶
const ( // DefaultTurnDeadline is the fallback hard turn ceiling (belt 2). DefaultTurnDeadline = 15 * time.Minute // DefaultGraceWindow is the fallback last-viewer-detach survival window (belt 1). DefaultGraceWindow = 60 * time.Second // DefaultJournalSize bounds the per-session replay journal (belt 3). DefaultJournalSize = 512 )
const ( // StateRunning: the turn's chain is executing and at least one viewer is attached. StateRunning = "running" // StateGrace: the chain is still executing but no viewer is attached; a // grace timer is counting down (belt 1). A reattach returns it to // running; expiry cancels it. StateGrace = "grace" // StateFinished: the chain has ended and the session awaits viewer // detach / reaper cleanup. StateFinished = "finished" // StateSuspended: the chain suspended on a pending human approval and // checkpointed durably; its goroutine has ended. Lifecycle-wise it is a // finished turn (same teardown/reap rules), but the distinct state tells // the operator board to wait for the approval rather than "done". A // later resume attaches as a fresh turn with a fresh journal. StateSuspended = "suspended" )
Turn states, the vocabulary of TurnStatus.State.
Variables ¶
var ErrClosed = errors.New("nativeturn: registry is closed")
ErrClosed is returned by Start once the Registry has been Closed. It is a sentinel so callers can branch on errors.Is(err, ErrClosed) if they ever need to.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// TurnDeadline is the hard wall-clock ceiling on a single turn (belt 2).
// A turn still running when it elapses is terminated with a context
// deadline. Must be > 0.
TurnDeadline time.Duration
// GraceWindow is how long an in-flight turn survives with no viewer
// attached before it is cancelled and torn down (belt 1); a reattach
// inside the window keeps it alive. Must be > 0.
GraceWindow time.Duration
// JournalSize bounds the per-session replay journal (belt 3): the most
// recent JournalSize events are retained, dropped oldest-first. Zero or
// negative resolves to DefaultJournalSize in New.
JournalSize int
}
Config holds the survival-layer tunables. TurnDeadline and GraceWindow are operator-facing (env-backed, see ParseEnv); JournalSize is overridable only in-process (tests).
type Event ¶
type Event struct {
// Seq is the 1-based monotonic id of this event within its session's
// turn. Increases by one per emitted update, never repeats or reorders.
Seq uint64
// Update is the session/update notification as emitted by the turn,
// before any per-viewer normalization.
Update libacp.SessionNotification
}
Event is one captured session/update tagged with a per-session monotonic sequence number, for a future SSE transport's Last-Event-ID replay. The in-process WebSocket viewer ignores Seq today (it replays the whole retained journal on attach).
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry owns every native session's in-flight turn, off any single connection. Created once at serve boot, shared across all per-connection Transports, and keyed by ACP session id: a turn survives a viewer detach, a reconnecting viewer replays the journal, and the anti-zombie belts guarantee no turn runs forever unwatched. Every method is safe for concurrent use.
func New ¶
New returns a Registry rooted on a fresh serve context. Call Close at shutdown to tear every in-flight turn down. A non-positive JournalSize, TurnDeadline, or GraceWindow is floored to its default.
func (*Registry) AttachIfRunning ¶
func (r *Registry) AttachIfRunning(ctx context.Context, sid libacp.SessionID, viewer Viewer) (*Turn, bool, error)
AttachIfRunning attaches viewer to sid's in-flight turn if one exists, replaying the journal and joining the live fan-out. Returns (nil, false, nil) when no in-flight turn exists — a finished turn is deliberately not attachable here, since a reconnecting client's durable transcript already carries it.
func (*Registry) Cancel ¶
Cancel cancels sid's in-flight turn and tears it down (session/cancel). Reports whether a turn was present; a cancel with no turn in flight is a no-op returning false. Any viewer awaiting the turn unblocks with its cancelled result.
func (*Registry) Close ¶
Close tears every in-flight turn down and cancels the Registry's root context. It is the runtime-shutdown hook; after Close, Start returns ErrClosed. Idempotent.
func (*Registry) Get ¶
func (r *Registry) Get(sid libacp.SessionID) (TurnStatus, bool)
Get returns the status of sid's active turn, or ok=false when none is active (never started, or already reaped).
func (*Registry) List ¶
func (r *Registry) List() []TurnStatus
List returns a snapshot of every active turn, sorted by session id. The live side is snapshotted under mu and each status read outside it, so a turn attaching or finishing concurrently lands on either side of the boundary — a point-in-time report, not a transaction.
func (*Registry) ReapIdle ¶
ReapIdle sweeps every session and tears down the ones that must not linger (belt 4, the periodic backstop behind the timer-driven grace path). A session is reaped when: it is finished with no viewers; it is in-flight, unwatched, and past its grace deadline; or wall-clock has passed its hard deadline plus one grace window. A running, watched turn inside its deadline is never reaped. Always returns nil.
func (*Registry) Start ¶
Start ensures a turn is running for sid and attaches viewer to it. If a turn is already in-flight for sid, viewer joins it (journal replay + live fan-out) and started is false. Otherwise a fresh turn is started on a serve-rooted, hard-deadline-bounded context (belt 2), with viewer as its first attached viewer; started is true. Returns ErrClosed once the Registry is Closed.
type Result ¶
type Result struct {
// StopReason is the ACP stop reason the connected client's prompt resolves with.
StopReason libacp.StopReason
// Err is a genuine execution failure the client must see as a JSON-RPC
// error. Nil for a clean end and for a cancellation, which resolves with
// StopReasonCancelled and no error.
Err error
// Suspended marks a turn whose chain parked on a pending human approval:
// not a failure (Err is nil), and status reads StateSuspended instead of
// StateFinished until reaped.
Suspended bool
}
Result is the outcome of one turn, returned by a TurnFunc and read back by the connected viewer to resolve its libacp session/prompt RPC.
type Turn ¶
type Turn struct {
// contains filtered or unexported fields
}
Turn is a caller's handle to one session's turn. It is how a connected viewer awaits completion, detaches on connection drop, and (for session/cancel) cancels.
func (*Turn) Await ¶
Await blocks until the turn completes (returning its Result and true) or ctx is done first (returning the zero Result and false, without disturbing the turn). It is a convenience over selecting on Done yourself.
func (*Turn) Cancel ¶
func (t *Turn) Cancel()
Cancel ends this turn now and tears it down — the explicit user cancel (session/cancel). Any viewer awaiting completion unblocks with the turn's (cancelled) result. Idempotent.
func (*Turn) Detach ¶
Detach removes viewerID from this turn's fan-out. It NEVER cancels the turn while another viewer remains; detaching the last viewer of an in-flight turn starts the grace window (Belt 1). Idempotent.
func (*Turn) Done ¶
func (t *Turn) Done() <-chan struct{}
Done is closed when the turn's chain ends; Result is then readable.
type TurnFunc ¶
type TurnFunc func(ctx context.Context, emit func(ctx context.Context, n libacp.SessionNotification)) Result
TurnFunc runs one turn's work. ctx is the serve-rooted, hard-deadline-bounded turn context (belt 2), not any connection's context, so the work outlives the client that started it. emit journals a session/update and fans it out to every attached viewer, exactly once and in order.
type TurnStatus ¶
type TurnStatus struct {
SessionID libacp.SessionID `json:"sessionId"`
StartedAt time.Time `json:"startedAt"`
// Deadline is the hard wall-clock time this turn is terminated at (belt 2).
Deadline time.Time `json:"deadline"`
// Viewers is how many viewers are attached right now (0 in StateGrace).
Viewers int `json:"viewers"`
// State is one of StateRunning / StateGrace / StateFinished.
State string `json:"state"`
}
TurnStatus is a point-in-time snapshot of one active turn (Registry.List/Get). It is a value copy: mutating it never affects the live turn.
type Viewer ¶
type Viewer interface {
// ID uniquely identifies this viewer within a session. Two viewers on
// one session must not share an ID.
ID() string
// Deliver receives one turn event, in order: the replayed journal
// backlog on attach, then every live event.
//
// It must not block — it runs on the turn's fan-out path under the
// session lock, so a blocking call stalls the turn and every other
// viewer. Enqueue and return. The returned error is advisory only.
Deliver(ctx context.Context, ev Event) error
}
Viewer is a consumer attached to one session's live (and replayed) turn stream.