Help us shape the future of Go! Take the 2026 Go Developer Survey

replay

package
v0.2.7 Latest Latest
Warning

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

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

Documentation

Overview

Package replay implements session replay / time-travel (design 006): capture a session's pane output and export it as an asciicast (.cast) file so demos and incidents become reproducible artifacts.

This package provides the asciicast v2 writer (pure), an in-session output Capture, and JetStream-backed durable capture (durable.go): a per-session stream on the pane output subject so export survives a daemon restart. Full scrubbing playback into a read-only replay pane is a later increment.

Index

Constants

View Source
const (
	DefaultRetention = 7 * 24 * time.Hour
	DefaultMaxBytes  = 1 << 30 // 1 GiB
)

Durable stream retention defaults (design 006 §3.1: 7d / 1GB, whichever first). Overridable via `[replay] retention` / `max_bytes` in rysh.config.yaml.

View Source
const RoleReplay = "replay"

RoleReplay marks a ConversationMessage as replayed output so the capture (RAM ring and stream walks) can exclude it from the recording.

Variables

This section is empty.

Functions

func DeleteDurable

func DeleteDurable(nc *nats.Conn, session string)

DeleteDurable removes the session's replay stream if present. Called when capture is configured OFF: the stream captures publishes at the broker, so a leftover from a previously-enabled run would keep storing output while ##replay reports capture off — exactly the storage surprise design 006's opt-in exists to prevent. Recorded output is local-only and deletable with the session (design 006 §5).

func FormatBadge

func FormatBadge(st PlayerStatus) string

FormatBadge renders the REPLAY badge text for a status, e.g. "REPLAY 01:23/04:56 ×2" ("×max" at max speed, "⏸" when paused).

func ParseSpeed

func ParseSpeed(spec string) (float64, error)

ParseSpeed parses a --speed value: a positive number (2, 0.5, …) or "max" (no delays at all, design 006 §3.2). Empty means 1x.

func StreamName

func StreamName(session string) string

StreamName returns the per-session replay stream name (design 006 §3.1), with the session mapped into JetStream's allowed name characters.

func WriteCast

func WriteCast(w io.Writer, h Header, events []Event) error

WriteCast writes an asciicast v2 document: a JSON header line followed by one [time, kind, data] JSON array per event ("o" output, "r" resize). Playable by `asciinema play`.

Types

type Capture

type Capture struct {
	// contains filtered or unexported fields
}

Capture subscribes to the merged pane output subject and records timestamped output events per pane, for export as an asciicast. It runs on the NATS subscription goroutine; the mutex guards the event store (a documented exception to the no-mutex rule, at the NATS/actor boundary — same as the proxy's audit buffer).

func NewCapture

func NewCapture(nc *nats.Conn, codecs *msg.CodecRegistry, session string) *Capture

NewCapture builds a capture bound to nc + codecs for the given session.

func (*Capture) Count

func (c *Capture) Count(paneID string) int

Count returns the number of captured events for a pane ("" = all panes).

func (*Capture) DurableInfo

func (c *Capture) DurableInfo() (bool, uint64)

DurableInfo reports whether durable capture is active and the stream's stored message count. False covers both "never enabled" (JetStream absent, capture configured off) and "stream lost since enable".

func (*Capture) EnableDurable

func (c *Capture) EnableDurable(opts DurableOptions) error

EnableDurable ensures the session's replay stream exists on the merged pane output and pane resize subjects — the same subjects Start subscribes to, so the durable and in-memory captures always record the same traffic (an existing stream's subject list is refreshed by the UpdateStream below, so streams created before resize capture existed pick it up). Idempotent: on restart the existing stream is reused and its limits refreshed to the current options. js/streamName are written once here and only read from the owning actor's goroutine, so the mutex stays scoped to the event store.

func (*Capture) ExportToFile

func (c *Capture) ExportToFile(paneID, path, title string) (int, error)

ExportToFile writes the captured output for paneID ("" = all) as an asciicast at path. Returns the event count written. A single-pane export carries the pane's resize timeline: the header is the size in effect at the first output, and later size changes become asciicast "r" events (design 006). The merged export keeps its latest-size header and no "r" events.

func (*Capture) Panes

func (c *Capture) Panes() []string

Panes returns the pane IDs with captured output.

func (*Capture) ParseFromOffset

func (c *Capture) ParseFromOffset(paneID, fromSpec string) (time.Duration, error)

ParseFromOffset parses a --from spec against a timeline's first recorded timestamp — the controlled-player counterpart of PlaybackPlan's parsing.

func (*Capture) PlaybackPlan

func (c *Capture) PlaybackPlan(paneID, fromSpec, speedSpec string) ([]PlayStep, error)

PlaybackPlan builds the ##replay play schedule for a pane ("" = all panes merged), falling back to the durable stream when the RAM ring is empty (post-restart), exactly like export.

func (*Capture) Start

func (c *Capture) Start() error

Start subscribes to the merged pane output subject (pane.*.output) and the pane resize subject (pane.*.resized) so the export knows the real pane size.

func (*Capture) Stop

func (c *Capture) Stop()

Stop unsubscribes.

func (*Capture) Timeline

func (c *Capture) Timeline(paneID string) ([]TimedEvent, error)

Timeline returns the recording for a pane ("" = all panes merged) as offsets from the first event, plus nothing else — the controlled player's input. Falls back to the durable stream when the RAM ring is empty (post-restart), exactly like export and PlaybackPlan.

type DurableOptions

type DurableOptions struct {
	Retention time.Duration // stream MaxAge; 0 ⇒ DefaultRetention
	MaxBytes  int64         // stream MaxBytes; 0 ⇒ DefaultMaxBytes
}

DurableOptions bounds the durable stream. Zero values mean the defaults.

type Event

type Event struct {
	Time float64
	Data string
	Kind string
}

Event is one cast event: seconds since capture start, the event kind, and the data. Kind "" or "o" is terminal output; "r" is a mid-recording resize with Data "COLSxROWS" (asciicast v2 resize event).

type Header struct {
	Version   int    `json:"version"`
	Width     int    `json:"width"`
	Height    int    `json:"height"`
	Timestamp int64  `json:"timestamp,omitempty"` // unix seconds of the first event
	Title     string `json:"title,omitempty"`
}

Header is the asciicast v2 header line.

type PlayStep

type PlayStep struct {
	Delay time.Duration
	Text  string
}

PlayStep is one scheduled playback emission: wait Delay (already speed-scaled), then emit Text.

type Player

type Player struct {
	// contains filtered or unexported fields
}

Player runs one playback on its own goroutine until the schedule finishes or Stop is called. It never touches actor state: the emit/onDone callbacks carry everything it needs, and all publishing they do is thread-safe NATS.

Two modes share the type: StartPlayer (v1, pre-planned steps, stop-only) and StartControlledPlayer (v2, a recording timeline plus pause/seek/speed controls and status reporting). Control methods are no-ops on a v1 player.

func StartControlledPlayer

func StartControlledPlayer(tl []TimedEvent, from time.Duration, speed float64, lead time.Duration, after func(time.Duration) <-chan time.Time, h PlayerHooks) *Player

StartControlledPlayer schedules a recording timeline on a new goroutine with interactive controls (v2, the replay pane's engine). `tl` must be sorted by At (Capture.Timeline guarantees it). Events with At < from are skipped, the same seek-forward semantics as --from in v1. `after` is the clock seam.

`lead` is an initial grace wait before the first emission (still stop/control-interruptible): the replay pane is created asynchronously (workspace → tab → lane → group → pane spawn + NATS subscribe), so emitting immediately could race the pane's output subscription and drop the first events. 0 = start immediately.

func StartPlayer

func StartPlayer(steps []PlayStep, after func(time.Duration) <-chan time.Time, emit func(string), onDone func(stopped bool)) *Player

StartPlayer schedules steps on a new goroutine. `after` is the clock seam (time.After in production; tests inject a fake). `emit` publishes one step's text. `onDone(stopped)` runs exactly once when playback ends — stopped=true when Stop cut it short. Both callbacks run on the player goroutine.

func (*Player) Active

func (p *Player) Active() bool

Active reports whether the playback is still running.

func (*Player) Done

func (p *Player) Done() <-chan struct{}

Done is closed when the playback goroutine has fully exited.

func (*Player) SeekBy

func (p *Player) SeekBy(d time.Duration)

SeekBy moves the playback position by d (negative = backward). A backward seek clears the destination and re-renders from the start of the buffer at max speed; a forward seek instantly emits the skipped events.

func (*Player) SpeedDown

func (p *Player) SpeedDown()

func (*Player) SpeedUp

func (p *Player) SpeedUp()

SpeedUp doubles the playback speed (capped); SpeedDown halves it (floored).

func (*Player) Status

func (p *Player) Status() (PlayerStatus, bool)

Status returns the current playback status. ok=false on a v1 player, which tracks no position.

func (*Player) Stop

func (p *Player) Stop()

Stop cancels the playback. Idempotent and safe from any goroutine.

func (*Player) TogglePause

func (p *Player) TogglePause()

TogglePause pauses a running playback or resumes a paused one.

type PlayerHooks

type PlayerHooks struct {
	// Emit publishes one event's text.
	Emit func(string)
	// Clear wipes the destination before a backward-seek re-render. A backward
	// seek re-renders from the start of the buffer: clear, then re-emit every
	// event up to the target instantly (documented v2 behavior — there is no
	// terminal-state snapshotting to rewind to).
	Clear func()
	// OnDone runs exactly once when playback ends; stopped=true when Stop cut
	// it short.
	OnDone func(stopped bool)
	// OnStatus runs after every status change (emission, pause, seek, speed) —
	// the badge feed.
	OnStatus func(PlayerStatus)
}

PlayerHooks are the callbacks a controlled playback drives. All run on the player goroutine and must not touch actor state directly (publish via thread-safe NATS, or hand off to the owning actor's mailbox).

type PlayerStatus

type PlayerStatus struct {
	Pos    time.Duration
	Total  time.Duration
	Speed  float64
	Paused bool
}

PlayerStatus is a snapshot of a controlled playback, for the REPLAY badge: position/total in recording time, current speed (+Inf = max), paused flag. Pos is the recording-time offset of the last emitted event (or the seek target) — it advances per emission, not per wall-clock tick.

type TimedEvent

type TimedEvent struct {
	At   time.Duration
	Text string
}

TimedEvent is one recorded emission on the playback timeline, at offset At from the start of the recording. Unlike PlayStep (whose delays are already speed-scaled and relative), TimedEvents keep absolute recording time so a controlled player can seek and change speed mid-flight.

Jump to

Keyboard shortcuts

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