Files
multi-agent-mux/.agents/reports/canary-projects-multi-agent-mux-creator-claude/report-skill-optimization-analysis.md
T
Godopu 7eeb4b709a perf(skills): optimize sleeps and modularize duplication with multi-agent consensus PASS
- 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
2026-07-11 10:08:41 +09:00

14 KiB
Raw Blame History

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.sh 1212, reconcile.sh 644, delegate-job wrapper 440, create_session.sh 408, stop_session.sh 370, update_yaml_resumed.sh 164, status.sh 140, resolve_session_id.sh 44)
  • Audit baseline: working tree on da895fc + the uncommitted prompt-lock F5 fix in create_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.


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 17 are low-risk and independently landable; 910 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.