nativeturn

package
v0.37.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 2, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

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

View Source
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
)
View Source
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

View Source
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).

func ParseEnv

func ParseEnv(turnMax, turnGrace string) (Config, error)

ParseEnv builds a Config from the raw CONTENOX_TURN_MAX (hard deadline) and CONTENOX_TURN_GRACE (last-viewer grace) env strings. An empty field takes the corresponding default; a value must be a positive Go duration (e.g. "15m", "90s"). JournalSize is not env-configurable.

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

func New(cfg Config) *Registry

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

func (r *Registry) Cancel(sid libacp.SessionID) bool

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

func (r *Registry) Close() error

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

func (r *Registry) ReapIdle(_ context.Context) error

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

func (r *Registry) Start(sid libacp.SessionID, fn TurnFunc, viewer Viewer) (*Turn, bool, error)

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.

func (*Registry) Stop

func (r *Registry) Stop(sid libacp.SessionID) bool

Stop is the operator-surface twin of Cancel: it ends sid's turn and tears it down, reporting whether one was present. Distinct verb from the protocol-level session/cancel, though both reduce to the same teardown. Idempotent.

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

func (t *Turn) Await(ctx context.Context) (Result, bool)

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

func (t *Turn) Detach(viewerID string)

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.

func (*Turn) Result

func (t *Turn) Result() Result

Result returns the turn's outcome. Only meaningful after Done is closed.

func (*Turn) SessionID

func (t *Turn) SessionID() libacp.SessionID

SessionID is the ACP session this turn serves.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL