SessionForge 1.0.0 Techlosoft - Remote Ops Workspace ================================================================== Record a command's output together with the exact timing of every chunk, then replay it later at true speed. The point of the tool is the pauses: "here is exactly what happened on that box, including the ninety seconds where nothing moved." INSTALL ------------------------------------------------------------------ go build -o sessionforge . Go standard library only. No third-party modules, no network access needed to build. Prebuilt binaries are in dist/ for linux-amd64, darwin-amd64, darwin-arm64 and windows-amd64. QUICK START ------------------------------------------------------------------ # record a deploy, watching it live as it runs sessionforge record deploy.jsonl --title "prod deploy" -- ./deploy.sh # watch it back at real speed sessionforge replay deploy.jsonl # watch it back at 4x, and never wait more than 2s on a pause sessionforge replay deploy.jsonl --speed 4 --max-idle 2 # what is in this recording? sessionforge info deploy.jsonl # just the text, no timing sessionforge cat deploy.jsonl COMMANDS ------------------------------------------------------------------ sessionforge record [--title TEXT] -- [args] Runs the command, streams its output to your terminal live, and writes every chunk to the cast file stamped with the elapsed time it actually arrived. stdout and stderr are captured on separate pipes, tagged "o" and "e", and stored interleaved in the order they were read. record is deliberately transparent. It prints no banner and no summary: its own stdout and stderr carry only the child's bytes, so you can drop it in front of an existing command in a script and nothing downstream notices. It exits with the child's exit status, or 128+N if the child was killed by signal N, or 127 if the command could not be started at all. The command must come after "--". This is required, not optional, so that the recorded command's own flags are never mistaken for SessionForge's flags. sessionforge replay [--speed N] [--max-idle S] [--no-timing] Replays the cast to your terminal with the original inter-event delays. stdout events go to stdout, stderr events go to stderr. --speed N Divide every delay by N. 2 is twice as fast, 0.5 is half speed. Default 1.0. --max-idle S Cap any single gap at S seconds. A five minute pause replays in S seconds; short gaps are untouched. Default 0, meaning no cap. --no-timing Dump everything instantly, no sleeping. Delays are scheduled against an absolute start time rather than by sleeping for each gap in turn, so a long replay does not accumulate drift. sessionforge info [--json] Header fields, event count, duration, total bytes split by stream, the longest idle gap and the recorded exit code. --json emits the same data as a single JSON object. Duration is the timestamp of the final event. sessionforge cat [--stdout-only | --stderr-only] The plain transcript with no timing at all. With no filter this is byte-for-byte what the command printed. sessionforge help | version -h, --help and help all work, on the tool and on each subcommand, and exit 0. A bad invocation prints usage to stderr and exits 1. CAST FORMAT ------------------------------------------------------------------ JSON Lines. Line 1 is a header object: {"version":1,"command":["sh","-c","..."], "started_at":"2026-08-10T04:10:29Z","shell":"/bin/bash", "width":80,"height":24,"title":"paused demo"} Every following line is one event array: [elapsed_seconds, "o"|"e", "chunk of text"] [0.001227, "o", "first\n"] [1.002926, "o", "second\n"] [3.004161, "o", "third\n"] The last line is a footer object holding the outcome: {"exit_code":0,"duration":3.004391188} {"exit_code":-1,"duration":2.494,"signal":"terminated", "interrupted":true} Events are written to disk one complete line at a time, as they happen. Nothing is held in memory waiting for the command to finish, so a recording killed part way through is still a valid file. Binary payloads. A chunk that is not valid UTF-8 cannot be stored in a JSON string without corruption, so it is stored base64 with a fourth field marking it: [0.008778, "o", "AAECAwQF...", "b64"] Valid UTF-8 chunks are never base64-encoded, so ordinary recordings stay human-readable. Every byte value 0x00 to 0xFF survives a record -> cat round trip unchanged, including NULs, invalid UTF-8 sequences and ANSI escapes. RELATIONSHIP TO ASCIINEMA ------------------------------------------------------------------ The format is deliberately close to the asciinema v2 cast format, which is also JSON Lines with a header object followed by [time, code, data] event arrays. If you know that format you will find this one familiar, and a small script can convert between them. It is NOT claimed to be asciinema-compatible, and interoperability with the asciinema player has not been tested. Known differences: - asciinema v2 records a PTY. SessionForge records two pipes, so it has a separate "e" stream tag for stderr where asciinema has only "o" (plus "i" for input, which SessionForge does not have). - SessionForge adds a trailing footer object carrying the exit code. asciinema has no such line, and a strict reader would reject it. - SessionForge adds the optional fourth "b64" field on events with non-UTF-8 payloads. asciinema has no fourth field. - The header key set differs: asciinema uses "env" and "idle_time_limit", SessionForge uses "shell" and "started_at" as an RFC3339 string rather than a Unix timestamp. Verified asciinema interoperability is on the roadmap below. Until it is actually tested, treat the two formats as similar but separate. WHAT IS NOT IMPLEMENTED ------------------------------------------------------------------ Read this part. It is the honest boundary of the tool. This records a command's OUTPUT STREAMS. It is not a full interactive PTY session recorder. A real PTY needs platform-specific syscalls (openpty, ioctl TIOCSWINSZ and friends) that are not in the portable Go standard library, so this release stays on pipes. The consequences are real: - Full-screen TUI programs are not usefully recorded. top, htop, vim, less and anything else that draws with cursor addressing will detect that it is not attached to a terminal and behave differently, or emit escape sequences that make no sense to replay into a plain terminal. The bytes are captured faithfully; they just are not what you wanted. - Programs change their behaviour when not on a TTY. Most tools disable colour, disable progress bars and switch from line buffering to block buffering when stdout is a pipe. That last one affects timing directly: a block-buffered program hands you its output in 4KB lumps, so the recording shows lumps arriving, not lines. If the timing matters, force the program's own unbuffered or line-buffered mode. - Terminal size is not detected and resizing is not captured. The width and height in the header come from the COLUMNS and LINES environment variables, defaulting to 80x24. They are metadata only; nothing reads them back. - Interactive input is not recorded. The child inherits your stdin so interactive commands still work, but what you type is not stored in the cast and is not replayed. - Cross-stream ordering has a floor. stdout and stderr are separate kernel pipes with no ordering guarantee between them, and a chunk is timestamped when SessionForge reads it, not when the child wrote it. Measured on Linux: writes to the two streams separated by 1ms or more are recorded in the correct order every time (20/20 trials at 1ms, 5ms, 10ms and 50ms). Writes issued in the same microsecond, as in "echo out; echo err >&2; echo out2", may be recorded in either relative order, and consecutive writes to the same stream will often coalesce into one chunk. Stream TAGS are always correct and no bytes are ever lost or reordered within a stream; only the o-versus-e interleaving of near-simultaneous writes is uncertain. A PTY would fix this by merging both streams onto one file descriptor, at the cost of being unable to tell them apart. - replay writes bytes to a terminal. It does not emulate one. There is no screen model, no cursor tracking and no way to seek, pause or scrub. - No compression, no encryption, no upload, no secret redaction. A cast file is plaintext; if the command printed a credential, the cast contains that credential. - Windows binaries build and run, but signal handling there is limited: Go on Windows can only kill a child process, not send it a graceful SIGINT or SIGTERM. Recording, replay, info and cat are unaffected. ROADMAP ------------------------------------------------------------------ - PTY capture, so full-screen TUI programs, real colour output and true stream ordering are recorded properly. - Interactive input recording, with an "i" event stream. - HTML and SVG player export, for pasting a replay into a ticket or a postmortem without asking anyone to install a binary. - Verified asciinema-format interoperability, both directions, tested against the real player rather than assumed. EXIT STATUS ------------------------------------------------------------------ record the child's exit status; 128+N if the child was killed by signal N; 127 if the command could not be started others 0 on success, 1 on any error Every error message goes to stderr and starts with "sessionforge: ". Malformed cast files report the offending line number.