Long-running commands & timeouts
The golden rule: never use sleep outside the managed terminal to guess how long a command
takes. pairmux run returns when the command finishes, reaches a recognized quiet prompt, or hits
its timeout. When no completion is observed by that deadline, run does not kill the command; it
returns status: running and a structured way to continue.
The pattern: run → wait → peek
# 1. Create the terminal once, then run commands in it.
pairmux --json new --name build
pairmux --json run build "echo hello world"
If it finished in time, you get the result directly:
{"schema":"pairmux.v1","ok":true,"status":"done","terminal":"build","mode":"hooks","exit_code":0,"duration_ms":101,"output":"hello world"}
Now start a deterministic slow demo. The sleep calls are part of the managed program; the agent is
not sleeping to poll it. run blocks for only one second:
pairmux --json run build 'for step in 1 2 3; do echo "step $step"; sleep 1; done' --timeout 1s
If its deadline arrives without completion or a recognized prompt, run returns status: running
(an outcome, not an error) with the current tail and next steps. Timing can change which step is in
the tail; this is a representative hooks-mode result:
{"schema":"pairmux.v1","ok":true,"status":"running","terminal":"build","mode":"hooks","output":"step 1","next":["pairmux peek build","pairmux log build --cmd 2"]}
# 2. Wait for the next actionable terminal state.
pairmux --json wait build --idle 800
{"schema":"pairmux.v1","ok":true,"status":"idle","terminal":"build","mode":"hooks","next":["pairmux peek build","pairmux run build \"...\""]}
# 3. Look whenever you want — peek is read-only and never blocks.
pairmux --json peek build
The example reaches idle, but wait --idle can also return awaiting-input, dead, or timeout.
Read the returned status and follow its next field.
Choosing a timeout
--timeout is a Go duration (30s, 2m, 1h). Pick a value that bounds how long you're willing to
block, not how long the command may run. A run timeout returns status: running; a wait timeout
returns status: timeout. Neither one kills the managed command. A reasonable loop is a short first
run, followed by wait calls with deliberate deadlines.
Waiting for a specific signal
Output silence alone does not count as idle: a sleeping or I/O-blocked command remains running, and
the wait continues to its timeout. For a command whose completion is not the interesting event — a
dev server that never exits, say — a future pattern can be the useful outcome.
wait --pattern deliberately starts at the journal offset where that wait begins; it does not
search earlier output. This demo delays its readiness line long enough to arm the waiter:
pairmux --json new --name web
pairmux --json run web 'sleep 5; printf "listening on %s\n" "127.0.0.1:3000"; while :; do sleep 60; done' --timeout 1s
pairmux --json wait web --pattern "listening on 127" --timeout 10s
{"schema":"pairmux.v1","ok":true,"status":"pattern-found","terminal":"web","mode":"hooks","output":"listening on 127.0.0.1:3000","next":["pairmux peek web"]}
For a real service, first inspect the run response and search existing history:
pairmux --json log web --grep "listening on 127"
The command text intentionally keeps the full readiness phrase out of the shell's echoed command line. Otherwise a journal search can match the command itself before the service emits its real readiness output.
If history has no match, arm wait --pattern for a future occurrence. A line can arrive between a
history read and a later wait; strict gap-free monitoring requires another agent or process to start
the waiter before the action that emits the line. After a wait timeout, search the complete log again.
--pattern is an RE2 regex matched against shaped output (ANSI stripped), so
error|panic|listening all work. Clean up the persistent demo when finished:
pairmux --json kill web
Finding one line in a huge log
When output is truncated, the reply tells you how to get the rest — you never have to re-run the command:
"truncated":{"omitted_lines":1187,"get_full":"pairmux log build --cmd 17"}
Then query the complete selected history instead of re-running:
pairmux --json log build --cmd 17 # the whole command's output
pairmux --json log build --grep "error|FAIL" # every match, with journal line numbers
pairmux --json log build --range 400:460 # a specific line range
pairmux --json log build --range 1:end # every shaped journal line
Routine observation remains bounded so a huge journal cannot flood an agent: peek reads at most the
final 64 KiB, and default log reads at most the final 4 MiB before keeping 500 lines. Those responses
report a skipped raw prefix as truncated.omitted_bytes and point to the executable
pairmux log NAME --range 1:end recovery. Explicit --cmd, --grep, and --range selections read
the complete requested history and may therefore return large replies.
Tips
- Prefer reading the log over re-running. The journal already has the command's captured output; re-running wastes time and can change state.
- Tune
--head/--tailonrunif you want more or less inline output (defaults 50 / 200). - One command per terminal. Starting a second command while one is running returns
E_BUSY. Open another terminal withpairmux newfor parallel work.