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
- func DeleteDurable(nc *nats.Conn, session string)
- func FormatBadge(st PlayerStatus) string
- func ParseSpeed(spec string) (float64, error)
- func StreamName(session string) string
- func WriteCast(w io.Writer, h Header, events []Event) error
- type Capture
- func (c *Capture) Count(paneID string) int
- func (c *Capture) DurableInfo() (bool, uint64)
- func (c *Capture) EnableDurable(opts DurableOptions) error
- func (c *Capture) ExportToFile(paneID, path, title string) (int, error)
- func (c *Capture) Panes() []string
- func (c *Capture) ParseFromOffset(paneID, fromSpec string) (time.Duration, error)
- func (c *Capture) PlaybackPlan(paneID, fromSpec, speedSpec string) ([]PlayStep, error)
- func (c *Capture) Start() error
- func (c *Capture) Stop()
- func (c *Capture) Timeline(paneID string) ([]TimedEvent, error)
- type DurableOptions
- type Event
- type Header
- type PlayStep
- type Player
- type PlayerHooks
- type PlayerStatus
- type TimedEvent
Constants ¶
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.
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 ¶
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 ¶
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 ¶
StreamName returns the per-session replay stream name (design 006 §3.1), with the session mapped into JetStream's allowed name characters.
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 ¶
NewCapture builds a capture bound to nc + codecs for the given session.
func (*Capture) DurableInfo ¶
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 ¶
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) ParseFromOffset ¶
ParseFromOffset parses a --from spec against a timeline's first recorded timestamp — the controlled-player counterpart of PlaybackPlan's parsing.
func (*Capture) PlaybackPlan ¶
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 ¶
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) 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 ¶
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 ¶
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 ¶
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) Done ¶
func (p *Player) Done() <-chan struct{}
Done is closed when the playback goroutine has fully exited.
func (*Player) SeekBy ¶
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) 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 ¶
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 ¶
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.