Compare commits
1 Commits
mos-comms-
...
enhance/tm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9c07cdeba |
@@ -87,6 +87,10 @@ message crosses the wire as base64 (`-b`) to avoid all shell-quoting hazards.
|
||||
|
||||
- `agent-send.sh` — inter-agent wrapper (preamble + local/remote dispatch).
|
||||
- `send-message.sh` — low-level reliable single-pane submitter (`-b` base64 input).
|
||||
- `auto-submit-drafts.sh` — watchdog that flushes stable unsubmitted prompt
|
||||
drafts on a coordinator pane (default target `mos-claude`); run it as a
|
||||
long-lived process alongside the coordinator session.
|
||||
- `agent-send.test.sh` — regression + grammar lock for `agent-send.sh`.
|
||||
- `test-send-message-socket.sh` — smoke test for named-socket isolation.
|
||||
|
||||
## Distribution
|
||||
|
||||
80
packages/mosaic/framework/tools/tmux/auto-submit-drafts.sh
Executable file
80
packages/mosaic/framework/tools/tmux/auto-submit-drafts.sh
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# auto-submit-drafts.sh — watchdog for Claude Code panes that receive channel
|
||||
# messages but leave them as unsubmitted prompt drafts. Intended for Mos only.
|
||||
set -uo pipefail
|
||||
|
||||
TARGET="${1:-mos-claude}"
|
||||
INTERVAL="${INTERVAL:-2}"
|
||||
STABLE_SECONDS="${STABLE_SECONDS:-4}"
|
||||
LOG_PREFIX="[auto-submit-drafts:$TARGET]"
|
||||
|
||||
last_prompt=""
|
||||
first_seen=0
|
||||
|
||||
prompt_text() {
|
||||
tmux capture-pane -t "$TARGET" -p 2>/dev/null | python3 -c '
|
||||
import sys, re
|
||||
lines = sys.stdin.read().splitlines()
|
||||
idx = None
|
||||
for i in range(len(lines)-1, -1, -1):
|
||||
if "❯" in lines[i]:
|
||||
idx = i
|
||||
break
|
||||
if idx is None:
|
||||
raise SystemExit
|
||||
parts = []
|
||||
after = lines[idx].split("❯", 1)[1]
|
||||
parts.append(after)
|
||||
for line in lines[idx+1:]:
|
||||
# Stop at Claude Code separator/border lines.
|
||||
if "─" in line or "╰" in line or "╭" in line:
|
||||
break
|
||||
s = line.replace("\u00a0", " ")
|
||||
s = re.sub(r"[\x00-\x1f\x7f]", "", s).strip()
|
||||
if s:
|
||||
parts.append(s)
|
||||
text = " ".join(parts).replace("\u00a0", " ")
|
||||
text = re.sub(r"[\x00-\x1f\x7f]", "", text).strip()
|
||||
print(text)
|
||||
'
|
||||
}
|
||||
|
||||
while true; do
|
||||
if ! tmux has-session -t "$TARGET" 2>/dev/null; then
|
||||
echo "$LOG_PREFIX target missing; waiting" >&2
|
||||
sleep "$INTERVAL"
|
||||
last_prompt=""
|
||||
first_seen=0
|
||||
continue
|
||||
fi
|
||||
|
||||
current="$(prompt_text || true)"
|
||||
now="$(date +%s)"
|
||||
|
||||
if [[ -z "$current" ]]; then
|
||||
last_prompt=""
|
||||
first_seen=0
|
||||
sleep "$INTERVAL"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$current" != "$last_prompt" ]]; then
|
||||
last_prompt="$current"
|
||||
first_seen="$now"
|
||||
sleep "$INTERVAL"
|
||||
continue
|
||||
fi
|
||||
|
||||
age=$(( now - first_seen ))
|
||||
if (( age >= STABLE_SECONDS )); then
|
||||
echo "$LOG_PREFIX submitting stable draft after ${age}s: ${current:0:120}" >&2
|
||||
tmux send-keys -t "$TARGET" C-j
|
||||
sleep 0.8
|
||||
tmux send-keys -t "$TARGET" C-m
|
||||
sleep 2
|
||||
last_prompt=""
|
||||
first_seen=0
|
||||
else
|
||||
sleep "$INTERVAL"
|
||||
fi
|
||||
done
|
||||
@@ -77,10 +77,20 @@ snippet=$(printf '%s' "$MSG" | tr '\n' ' ' | tr -s ' ' | sed 's/[^[:print:]]//g'
|
||||
|
||||
# 1) Paste the body as a bracketed paste so multi-line content does not submit
|
||||
# line-by-line. load-buffer/paste-buffer is far safer than `send-keys -l`.
|
||||
printf '%s' "$MSG" | "${tmux_cmd[@]}" load-buffer -b __mosaic_send -
|
||||
# Buffer name MUST be unique per invocation: concurrent senders on the shared
|
||||
# tmux server race a fixed name (load overwrites load, -d deletes underneath),
|
||||
# cross-delivering or dropping messages — bit the fleet on the 2026-07-09
|
||||
# simultaneous restart (briefs swapped between sessions).
|
||||
BUF="__mosaic_send_$$_$(date +%s%N)"
|
||||
printf '%s' "$MSG" | "${tmux_cmd[@]}" load-buffer -b "$BUF" -
|
||||
# -p = bracketed paste when the client supports it; fall back if not.
|
||||
"${tmux_cmd[@]}" paste-buffer -d -p -b __mosaic_send -t "$EFFECTIVE_TARGET" 2>/dev/null \
|
||||
|| "${tmux_cmd[@]}" paste-buffer -d -b __mosaic_send -t "$EFFECTIVE_TARGET"
|
||||
"${tmux_cmd[@]}" paste-buffer -d -p -b "$BUF" -t "$EFFECTIVE_TARGET" 2>/dev/null \
|
||||
|| "${tmux_cmd[@]}" paste-buffer -d -b "$BUF" -t "$EFFECTIVE_TARGET" \
|
||||
|| "${tmux_cmd[@]}" delete-buffer -b "$BUF" 2>/dev/null
|
||||
# ^ -d deletes the buffer only on a SUCCESSFUL paste; if both attempts fail
|
||||
# (e.g. the target vanished since the liveness check), delete explicitly —
|
||||
# named buffers are exempt from tmux's buffer-limit eviction, so orphans
|
||||
# would otherwise accumulate forever.
|
||||
sleep 0.5
|
||||
|
||||
# 2) Submit, then verify; flush with another Enter if it is still a draft.
|
||||
|
||||
@@ -47,4 +47,32 @@ if capture_default | grep -qF "agent socket hello"; then
|
||||
fail "agent-send.sh leaked named-socket message to default tmux server"
|
||||
fi
|
||||
|
||||
# Concurrency: parallel senders on one server must not cross-deliver or drop.
|
||||
# Locks the unique-per-invocation paste buffer (a fixed buffer name raced:
|
||||
# load overwrote load, -d deleted underneath — messages swapped between panes).
|
||||
CONC_N=5
|
||||
for i in $(seq 1 "$CONC_N"); do
|
||||
tmux -L "$SOCKET" new-session -d -s "conc-$i" -c "$TMPDIR" 'bash --noprofile --norc -i'
|
||||
done
|
||||
pids=()
|
||||
for i in $(seq 1 "$CONC_N"); do
|
||||
"$SEND_MESSAGE" -L "$SOCKET" -t "=conc-$i" -m "CONCPAYLOAD-${i}-END" >/dev/null &
|
||||
pids+=($!)
|
||||
done
|
||||
for pid in "${pids[@]}"; do
|
||||
wait "$pid" || fail "concurrent send-message.sh invocation exited non-zero"
|
||||
done
|
||||
sleep 0.2
|
||||
for i in $(seq 1 "$CONC_N"); do
|
||||
pane=$(tmux -L "$SOCKET" capture-pane -t "=conc-$i:0.0" -p)
|
||||
printf '%s' "$pane" | grep -qF "CONCPAYLOAD-${i}-END" \
|
||||
|| fail "concurrent send dropped payload for pane conc-$i"
|
||||
for j in $(seq 1 "$CONC_N"); do
|
||||
[ "$j" = "$i" ] && continue
|
||||
if printf '%s' "$pane" | grep -qF "CONCPAYLOAD-${j}-END"; then
|
||||
fail "concurrent send cross-delivered payload $j to pane conc-$i"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo "ok - named tmux socket send tools"
|
||||
|
||||
Reference in New Issue
Block a user