- OP-1: Implement reactive _wait_session_gone in lib.sh and stop_session.sh with set -e || true guard - OP-2: Event-driven MQTT subscribe handshake with sub_pid liveness in delegate-job - OP-3: Replace CPU time.sleep(0.5) spin with threading.Event wait in reconcile.sh - OP-4: Define mam_tmux dispatcher targeting resolved _REAL_TMUX_PATH to prevent recursion - OP-6 & OP-7: Add token variables and bash version source check in lib.sh - Integrate approved optimization plan and PASS review reports from all agents
14 KiB
MAM Skill Optimization Analysis (Creator Claude)
- Analyst: Creator Claude (
canary-projects-multi-agent-mux-creator-claude) - Date: 2026-07-11
- Brief:
.mam/reports/brief-skill-optimization-analysis.md - Scope: all 8 shell entry points under
.agents/skills/— 3,422 lines total (lib.sh1212,reconcile.sh644, delegate-job wrapper 440,create_session.sh408,stop_session.sh370,update_yaml_resumed.sh164,status.sh140,resolve_session_id.sh44) - Audit baseline: working tree on
da895fc+ the uncommitted prompt-lock F5 fix increate_session.sh(reviewed PASS, awaiting commit) - Method: full-tree greps (sleeps, tmux-resolution variants,
|| true/2>/dev/null, BASH_SOURCE, heredocs, eval), shellcheck run, targeted reads of every flagged site.
Baseline strengths (for calibration)
Uniform #!/usr/bin/env bash + set -euo pipefail across all 7 executables (lib.sh correctly bare as a sourced library); zero eval in any executable script; zero warning-level shellcheck findings beyond 6 pre-existing ones; the worst sleep offenders (resume's blind sleep 5/3, inject's sleep 0.5+blind C-m) were already eliminated by the FW-W2 send_keys_safe work. The findings below are the next tier.
Focus 1 — Inefficient Polling / Sleeps
S1 (HIGH VALUE) — stop_session.sh:193,200: fixed post-kill waits
tmux send-keys … # graceful exitkey
sleep 3 # ← always pays 3 s
…
tmux kill-session …
sleep 5 # ← always pays 5 s
Every graceful stop pays the full 3 s even when the agent exits in 200 ms, and the kill path always pays 5 s. Worse than slow: on a loaded host an agent needing >3 s to flush falsely escalates to SIGTERM. Proposal: add to lib.sh —
# _wait_session_gone <sess> <max_sec> — returns 0 as soon as the session dies
_wait_session_gone() {
local sess="$1" max="${2:-5}" i
for ((i = 0; i < max * 4; i++)); do
_sks_tmux has-session -t "$sess" 2>/dev/null || return 0
sleep 0.25
done
return 1
}
Replace sleep 3 with _wait_session_gone "$SESSION_NAME" 5 and sleep 5 with _wait_session_gone "$SESSION_NAME" 8. Reactive (typical stop drops from ~8 s to <1 s), and more tolerant of slow exits.
S2 (HIGH VALUE) — delegate-job :119 and :205: sleep 1 as MQTT handshake
The comment admits the ordering dependency ("MQTT does not queue non-retained messages for absent subscribers"), then guesses: if CONNACK+SUBACK takes >1 s (public broker broker.hivemq.com over WAN — entirely realistic), the agent's started event is lost silently and the job idles to timeout; on a local broker the 1 s ×2 per review-loop iteration is pure waste. Proposal (event-driven handshake, also fixes E4): job_subscriber.py already logs to $logf — have it print a sentinel line (e.g. SUBSCRIBED <topic>) from its on_subscribe callback (flush immediately), then in the wrapper replace both sleeps with:
for _ in {1..25}; do grep -q '^SUBSCRIBED ' "$logf" 2>/dev/null && break; sleep 0.2; done
grep -q '^SUBSCRIBED ' "$logf" || { echo "ERROR: subscriber never reached SUBACK (see $logf)" >&2; exit 1; }
Removes the race instead of betting on it, and converts a dead-on-arrival subscriber (see E4) into a loud failure.
S3 (minor) — lib.sh:1042-1085 wait_for_tui_ready
Two capture-pane invocations per iteration (_pane_dialog_open + its own content=$(…)) with a hand-rolled local_tmux. Capture once per iteration into a variable and test both predicates on it; use _pane_capture (see D4). The 15×1 s poll budget itself is fine.
S4 (accepted as-is) — reconcile.sh:273 sleep "$POLL_INTERVAL" broker-down fallback loop is by design; see E1 for its real problem (silence, not pacing).
Focus 2 — Helper Duplication & Modularization
D1 (HIGH VALUE) — four divergent server-aware tmux resolutions
| Site | Form |
|---|---|
lib.sh:1104-1110 _sks_tmux() |
function — canonical, word-split-safe |
lib.sh:1043-1046 (wait_for_tui_ready) |
local_tmux="tmux -L $NAME" string |
create_session.sh:212-215 |
same string pattern (file also has a _tmux helper used by its trap — two mechanisms in one script) |
delegate-job :347-350 |
_tmux="tmux -L $NAME" string |
The string variants rely on unquoted word-splitting ($local_tmux send-keys …) — the exact idiom class shellcheck SC2086 exists for, and each future call site must re-remember the != default rule. Proposal: rename/promote _sks_tmux to mam_tmux() (keep _sks_tmux as an alias for compatibility) and replace all three string variants. Mechanical, ~10 lines net deletion.
D2 — delegate-job wrapper: duplicated subscriber-spawn + instruction template
The register→spawn-subscriber→sleep→build-instructions block appears twice (direct path :113-131, review-loop path :199-215) and the copies have already drifted (log filename schema differs; the review-loop copy carries iteration metadata the direct copy lacks). Extract _spawn_job_subscriber <job_id> <logf> and _job_instruction_block <job_id> <pub_cmd> <task_text>; combine with S2 so the handshake logic exists exactly once.
D3 — ready/dialog token lists duplicated inside lib.sh
Claude ready-tokens 'Anthropic|Assistant|Chat|Welcome|projects' at both lib.sh:1058 (wait_for_tui_ready) and lib.sh:1205 (handle_startup_dialogs); trust-dialog tokens split between _pane_dialog_open (:1137-1139) and handle_startup_dialogs' specific greps (:1199,1201). Token-list drift between two grep sites is precisely the class of defect just fixed in the prompt-lock round — next TUI release, someone updates one list and not the other. Proposal: single-source constants near the top of lib.sh —
_MAM_DIALOG_TOKENS='Do you trust the files|Yes, proceed|No, exit|Allow this|Press Enter to continue|browser to authenticate|Use arrow keys|Esc to cancel'
_MAM_READY_TOKENS_CLAUDE='Anthropic|Assistant|Chat|Welcome|projects'
plus a _ready_regex_for <agent> case-helper so wait_for_tui_ready's per-agent regexes live in one lookup. All grep sites reference the variables.
D4 — wait_for_tui_ready predates its own library's capture helpers
It hand-builds local_tmux and calls raw capture-pane instead of _pane_capture/_pane_tail. Folding it onto the helpers (with S3's single-capture-per-iteration) deletes ~8 lines and closes D1's second row for free.
D5 (micro) — stop_session.sh:128-129: two python3 -c processes to read two JSON fields from the same $MAPPED_DATA. One process printing both ('…; d=json.load(sys.stdin); print(d.get("cwd",""), d.get("job_id",""), sep="\t")') halves the fork cost; or add a tiny json_get helper to lib.sh if more call sites appear.
Focus 3 — Portability & POSIX Compliance
Shebang discipline means raw-sh execution is not a real exposure; the genuine gaps are three, and two are already roadmapped — listed here with confirmations, not double-counted as new:
P1 (= FW-P1 / FW-D3, confirmed at lib.sh:254-255)
mountpoint="$(df --output=target "$f" 2>/dev/null | tail -1)" || return 1
if mount | grep -q "$mountpoint.*nfs|…"
GNU-only df --output + Linux mount output format. On macOS/BSD, df errors → suppressed by 2>/dev/null → _check_is_nfs silently returns "not NFS" → the NFS/WAL safety switch is dead exactly where flock is least reliable. Portable replacement: mountpoint="$(df -P "$f" 2>/dev/null | awk 'NR==2{print $6}')" (df -P is POSIX) and probe filesystem type via stat -f -c %T on Linux / stat -f %T on BSD behind a case "$(uname)" — or at minimum log a warning when detection is unavailable instead of silently passing.
P2 (= FW-P5, confirmed at lib.sh:17)
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" — under zsh sourcing (documented agent workflow: source .agents/skills/lib.sh), BASH_SOURCE is empty → dirname "" → . → SKILL_DIR silently becomes the caller's cwd, and every relative resolution downstream (:950, :966) misroutes. Proposal (fail-loud, 3 lines at the top of lib.sh):
if [ -z "${BASH_SOURCE:-}" ]; then
echo "lib.sh must be sourced from bash (zsh/sh detected)" >&2; return 1 2>/dev/null || exit 1
fi
Silent misresolution becomes an immediate, explained failure. (Full zsh support via ${(%):-%N} is possible but not worth the dual-dialect maintenance.)
P3 (= FW-P6, confirmed) — depth-hardcoded root resolution
status.sh:29 (…/../../../../), reconcile.sh:48, and the ../.. sourcing prologue in all 6 skill scripts. One directory-layout refactor breaks all of them at once. FW-P6's marker-walk (find_workspace_root() ascending to .git/.mam/.env, exported once as WORKSPACE_ROOT) remains the right fix; the sourcing prologues can stay relative (they express a true structural invariant within the skills tree) — it's the workspace-root hops that should go through the marker walk.
P4 (non-issues, verified): fractional sleep 0.5/0.25 (GNU+BSD+busybox all accept), head -c/tail -c/awk NF (POSIX), no grep -P, no sed -i, no readlink -f, no eval — clean.
Focus 4 — Error Handling & Robustness
E1 (HIGHEST SEVERITY in this audit) — reconcile.sh:269: degraded mode is fully silent
bash "$_self" --once --emit-diff >/dev/null 2>&1 || true
This runs only in the broker-down fallback — the mode whose entire purpose is "keep reconciling when eventing is gone" — and it discards stdout, stderr, and the exit code. If reconciliation itself is failing every cycle (locked DB, missing python module, corrupted YAML), the operator sees a healthy-looking monitor while drift accumulates unboundedly. Proposal:
if ! out=$(bash "$_self" --once --emit-diff 2>&1); then
fails=$((fails + 1))
echo "[$(date -u +%FT%TZ)] poll-reconcile failed ($fails consecutive): ${out##*$'\n'}" >&2
[ "$fails" -ge 5 ] && { echo "FATAL: 5 consecutive reconcile failures — exiting for supervisor restart" >&2; exit 1; }
else
fails=0
fi
E2 — deliberate vs. accidental suppression (survey result)
The 18 || true / 31 2>/dev/null sites were individually reviewed. The large majority are legitimate idempotency guards (stop's kill-chain probing possibly-absent sessions; _pane_capture's probe semantics) — no action. The accidental class is E1 (above) and E4 (below).
E3 — stop_session.sh:128-129: unguarded JSON parse under set -e, no EXIT trap
Malformed registry JSON kills the stop mid-flight with a bare Python traceback; unlike create_session.sh, stop_session.sh has no cleanup/context trap, so the operator gets no indication of what state the stop reached (exitkey sent? captured? row updated?). Cheap fix: wrap the parse (… || { echo "ERROR: corrupt registry row for '$SESSION_NAME' — run monitor reconcile" >&2; exit 1; }) and add a minimal trap 'echo "stop aborted at stage $STAGE" >&2' ERR with a STAGE variable advanced at each phase.
E4 — delegate-job subscriber spawned fire-and-forget
"$PY" job_subscriber.py … >"$logf" 2>&1 & followed only by sleep 1: if the subscriber dies instantly (bad --registry-dir, missing paho-mqtt in the venv), the wrapper proceeds, the agent publishes into the void, and the job "runs" with zero audit trail until timeout. The S2 SUBACK-sentinel handshake converts this to a loud early failure — one fix, two findings (S2+E4).
E5 — shellcheck backlog (6 warnings, pre-existing)
SC2034 ×2 (ONCE, EMIT_DIFF "unused" in reconcile.sh — likely consumed inside the python heredoc via env; verify and either export or rename with _ prefix to document intent), SC2155 ×2, SC2164 ×2 (cd without || exit — real hazard under odd cwd removal). All are ≤2-line fixes; clearing them makes future "0 new findings" review gates strict.
Prioritized Optimization Plan
| # | Item | Files | Effort | Impact |
|---|---|---|---|---|
| 1 | E1 silent degraded loop → logged + bounded failures | reconcile.sh | S | Correctness/observability of the safety net |
| 2 | S2+E4 SUBACK sentinel handshake replaces both sleep 1 |
delegate-job wrapper, job_subscriber.py | M | Eliminates event-loss race on slow brokers; loud subscriber failures |
| 3 | S1 _wait_session_gone reactive stop |
lib.sh, stop_session.sh | S | ~7 s faster stops; no false SIGTERM escalation |
| 4 | D1+D4 mam_tmux() unification (retire 3 string variants) |
lib.sh, create_session.sh, delegate-job | S | Single source of truth; kills word-split hazard |
| 5 | D3 token-list constants + _ready_regex_for |
lib.sh | S | Prevents recurrence of the F1-class drift bug |
| 6 | P2 fail-loud non-bash source guard | lib.sh | S | Converts silent misresolution to instant diagnosis (FW-P5) |
| 7 | E3 stop parse guard + stage trap | stop_session.sh | S | Debuggable partial-stop states |
| 8 | D2 delegate-job block extraction | delegate-job wrapper | M | Stops copy drift (already observable) |
| 9 | P1 POSIX NFS detection (FW-P1/FW-D3) | lib.sh | M | Restores the WAL safety switch on macOS/BSD |
| 10 | P3 marker-walk root resolution (FW-P6) | lib.sh, status.sh, reconcile.sh | M | Layout-refactor resilience |
| 11 | E5 shellcheck backlog + D5 micro | reconcile.sh, lib.sh, stop_session.sh | S | Strict lint gate for future reviews |
Items 1–7 are low-risk and independently landable; 9–10 discharge existing roadmap entries (FW-P1/FW-D3/FW-P5/FW-P6 — update FUTURE_WORKS on landing). Every DoD should include the scratch-server functional suite from the prompt-lock review (T-A…T-E) plus bash -n + shellcheck zero-new.
Summary
The tree is in good structural shape — consistent strict-mode headers, no eval, and the recent FW-W2 work already modernized the highest-risk delivery path. The remaining debt clusters into: two timing bets that should be handshakes (stop waits, MQTT subscribe), one silent failure mode in exactly the code path that exists for resilience (reconcile fallback), and four copies of the tmux-server rule plus two copies of the TUI token lists — the same drift pattern that caused the last production bug. Eleven changes, mostly small, none speculative.