#!/bin/sh # self-inject.sh — type text into the tmux pane running this agent session. # # The building block for AUTO-FLUSH: an agent checkpoints its session-context, # then has tmux type "/clear" and a resume prompt at its own idle prompt, so a # session flushes with no human at the keyboard. # # Usage: # self-inject.sh -t %PANE [ ...] # self-inject.sh [...] # derive pane from ancestry # self-inject.sh [-t %PANE] # no pairs: report the pane # # Each pair: sleep seconds, then type literally and press Enter. # # TWO HARD-WON GOTCHAS (2026-07-02, archsetup session): # 1. A detached child (setsid/nohup/&) of an agent tool call DIES when the # tool call ends — the harness cleans up the process group. The arm step # must run under the tmux SERVER instead: # tmux run-shell -b "self-inject.sh -t %1 25 '/clear' 15 'go — resume...'" # 2. Under tmux run-shell the process is a child of the tmux server, so # ancestry-based pane detection CANNOT work there. Derive the pane FIRST, # synchronously from the agent's own shell (no -t), then pass it # explicitly with -t when arming. # # Collision hazard: if the user happens to be typing when the send fires, the # injected text merges into their input line (a real /clear became "/clearto" # mid-word). Auto-flush is for sessions running unattended; warn the user to # keep hands off for the armed window if they're present. PANE="" if [ "$1" = "-t" ]; then PANE=$2; shift 2 fi ppid_of() { # /proc//stat: pid (comm) state ppid ... — comm may contain spaces, # so take the 2nd field after the LAST ')'. stat=$(cat "/proc/$1/stat" 2>/dev/null) || return 1 # shellcheck disable=SC2086 # word-splitting the stat tail is the point set -- ${stat##*) } echo "$2" } find_pane() { anc=" " pid=$$ while [ -n "$pid" ] && [ "$pid" -gt 1 ] 2>/dev/null; do anc="$anc$pid " pid=$(ppid_of "$pid") || break done tmux list-panes -a -F "#{pane_pid} #{pane_id}" 2>/dev/null | \ while read -r ppid pane; do case "$anc" in *" $ppid "*) echo "$pane"; break;; esac done } [ -n "$PANE" ] || PANE=$(find_pane) [ -n "$PANE" ] || { echo "self-inject: no owning pane found (pass -t %PANE)" >&2; exit 1; } # With no delay/text pairs, just report the pane (the derive-first step). [ $# -ge 2 ] || { echo "$PANE"; exit 0; } while [ $# -ge 2 ]; do sleep "$1" tmux send-keys -t "$PANE" -l "$2" tmux send-keys -t "$PANE" Enter shift 2 done