feat(tmux,envelope): add hybrid node/container execution to muse-tmux and naturalize prompt envelope to operator directive
This commit is contained in:
+73
-35
@@ -15,15 +15,28 @@ TMUX_BIN = "/home/super/.local/bin/tmux"
|
||||
if not os.path.exists(TMUX_BIN):
|
||||
TMUX_BIN = "tmux"
|
||||
|
||||
def _ensure_logging(session):
|
||||
def _target_label(node=None, container=None):
|
||||
if node:
|
||||
return f"node netns '{node}' (/tmp/tmux-{node}.sock)"
|
||||
if container:
|
||||
return f"docker container '{container}'"
|
||||
return f"shared socket {SOCKET_PATH}"
|
||||
|
||||
def _ensure_logging(session, node=None, container=None):
|
||||
try:
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
log_file = os.path.join(LOG_DIR, f"{session}.log")
|
||||
_run_tmux("pipe-pane", "-o", "-t", session, f"cat >> {log_file}")
|
||||
tag = f"-{node}" if node else (f"-{container}" if container else "")
|
||||
log_file = os.path.join(LOG_DIR, f"{session}{tag}.log")
|
||||
_run_tmux("pipe-pane", "-o", "-t", session, f"cat >> {log_file}", node=node, container=container)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _run_tmux(*args, capture=True):
|
||||
def _run_tmux(*args, capture=True, node=None, container=None):
|
||||
if node:
|
||||
cmd = ["/home/super/Projects/NetVM/bin/netvm-exec.sh", node, "--", TMUX_BIN, "-S", f"/tmp/tmux-{node}.sock"] + list(args)
|
||||
elif container:
|
||||
cmd = ["docker", "exec", "-i", container, "tmux"] + list(args)
|
||||
else:
|
||||
cmd = [TMUX_BIN, "-S", SOCKET_PATH] + list(args)
|
||||
res = subprocess.run(cmd, capture_output=capture, text=True)
|
||||
return res.returncode, res.stdout, res.stderr
|
||||
@@ -32,11 +45,13 @@ def cmd_new(args):
|
||||
session = args.session
|
||||
window = getattr(args, "window", None)
|
||||
command = getattr(args, "command", None)
|
||||
node = getattr(args, "node", None)
|
||||
container = getattr(args, "container", None)
|
||||
|
||||
# Check if session already exists
|
||||
rc, out, _ = _run_tmux("has-session", "-t", session)
|
||||
rc, out, _ = _run_tmux("has-session", "-t", session, node=node, container=container)
|
||||
if rc == 0:
|
||||
print(f"Session '{session}' already exists on shared socket {SOCKET_PATH}.")
|
||||
print(f"Session '{session}' already exists on {_target_label(node, container)}.")
|
||||
return 0
|
||||
|
||||
t_args = ["new-session", "-d", "-s", session]
|
||||
@@ -45,10 +60,10 @@ def cmd_new(args):
|
||||
if command:
|
||||
t_args.append(command)
|
||||
|
||||
rc, out, err = _run_tmux(*t_args)
|
||||
rc, out, err = _run_tmux(*t_args, node=node, container=container)
|
||||
if rc == 0:
|
||||
_ensure_logging(session)
|
||||
print(f"✔ Created session '{session}' on {SOCKET_PATH} (logging to logs/tmux/{session}.log)")
|
||||
_ensure_logging(session, node=node, container=container)
|
||||
print(f"✔ Created session '{session}' on {_target_label(node, container)} (logging enabled)")
|
||||
return 0
|
||||
else:
|
||||
print(f"Failed to create session: {err.strip() or out.strip()}", file=sys.stderr)
|
||||
@@ -58,24 +73,26 @@ def cmd_send(args):
|
||||
session = args.session
|
||||
keys = args.keys
|
||||
enter = not getattr(args, "no_enter", False)
|
||||
node = getattr(args, "node", None)
|
||||
container = getattr(args, "container", None)
|
||||
|
||||
# Check session
|
||||
rc, _, _ = _run_tmux("has-session", "-t", session)
|
||||
rc, _, _ = _run_tmux("has-session", "-t", session, node=node, container=container)
|
||||
if rc != 0:
|
||||
# Auto-create if not exists
|
||||
print(f"Notice: Session '{session}' does not exist; creating now...", file=sys.stderr)
|
||||
_run_tmux("new-session", "-d", "-s", session)
|
||||
_ensure_logging(session)
|
||||
print(f"Notice: Session '{session}' does not exist on {_target_label(node, container)}; creating now...", file=sys.stderr)
|
||||
_run_tmux("new-session", "-d", "-s", session, node=node, container=container)
|
||||
_ensure_logging(session, node=node, container=container)
|
||||
else:
|
||||
_ensure_logging(session)
|
||||
_ensure_logging(session, node=node, container=container)
|
||||
|
||||
t_args = ["send-keys", "-t", session, keys]
|
||||
if enter:
|
||||
t_args.append("Enter")
|
||||
|
||||
rc, out, err = _run_tmux(*t_args)
|
||||
rc, out, err = _run_tmux(*t_args, node=node, container=container)
|
||||
if rc == 0:
|
||||
print(f"✔ Sent keys to '{session}'")
|
||||
print(f"✔ Sent keys to '{session}' on {_target_label(node, container)}")
|
||||
return 0
|
||||
else:
|
||||
print(f"Failed to send keys: {err.strip() or out.strip()}", file=sys.stderr)
|
||||
@@ -84,9 +101,11 @@ def cmd_send(args):
|
||||
def cmd_capture(args):
|
||||
session = args.session
|
||||
lines = getattr(args, "lines", 30) or 30
|
||||
node = getattr(args, "node", None)
|
||||
container = getattr(args, "container", None)
|
||||
|
||||
t_args = ["capture-pane", "-p", "-t", session, "-S", f"-{lines}"]
|
||||
rc, out, err = _run_tmux(*t_args)
|
||||
rc, out, err = _run_tmux(*t_args, node=node, container=container)
|
||||
if rc == 0:
|
||||
# Strip trailing blank lines
|
||||
clean = out.rstrip()
|
||||
@@ -97,12 +116,14 @@ def cmd_capture(args):
|
||||
return rc
|
||||
|
||||
def cmd_list(args):
|
||||
rc, out, err = _run_tmux("list-sessions")
|
||||
node = getattr(args, "node", None)
|
||||
container = getattr(args, "container", None)
|
||||
rc, out, err = _run_tmux("list-sessions", node=node, container=container)
|
||||
if rc == 0:
|
||||
print(out.strip() or "No active sessions.")
|
||||
return 0
|
||||
elif "no server running" in (err or "").lower() or rc == 1:
|
||||
print(f"No active tmux server on {SOCKET_PATH}.")
|
||||
print(f"No active tmux server on {_target_label(node, container)}.")
|
||||
return 0
|
||||
else:
|
||||
print(f"Error: {err.strip() or out.strip()}", file=sys.stderr)
|
||||
@@ -110,9 +131,11 @@ def cmd_list(args):
|
||||
|
||||
def cmd_kill(args):
|
||||
session = args.session
|
||||
rc, out, err = _run_tmux("kill-session", "-t", session)
|
||||
node = getattr(args, "node", None)
|
||||
container = getattr(args, "container", None)
|
||||
rc, out, err = _run_tmux("kill-session", "-t", session, node=node, container=container)
|
||||
if rc == 0:
|
||||
print(f"✔ Killed session '{session}'")
|
||||
print(f"✔ Killed session '{session}' on {_target_label(node, container)}")
|
||||
return 0
|
||||
else:
|
||||
print(f"Error: {err.strip() or out.strip()}", file=sys.stderr)
|
||||
@@ -120,10 +143,12 @@ def cmd_kill(args):
|
||||
|
||||
def cmd_prune(args):
|
||||
ttl_seconds = getattr(args, "ttl", 7200) or 7200
|
||||
rc, out, err = _run_tmux("list-sessions", "-F", "#{session_name} #{session_activity} #{session_attached}")
|
||||
node = getattr(args, "node", None)
|
||||
container = getattr(args, "container", None)
|
||||
rc, out, err = _run_tmux("list-sessions", "-F", "#{session_name} #{session_activity} #{session_attached}", node=node, container=container)
|
||||
if rc != 0:
|
||||
if "no server running" in (err or "").lower() or rc == 1:
|
||||
print(f"No active tmux server on {SOCKET_PATH}.")
|
||||
print(f"No active tmux server on {_target_label(node, container)}.")
|
||||
return 0
|
||||
print(f"Error listing sessions: {err.strip() or out.strip()}", file=sys.stderr)
|
||||
return rc
|
||||
@@ -139,65 +164,78 @@ def cmd_prune(args):
|
||||
idle_time = now - s_activity
|
||||
# Only prune if unattached and idle > TTL
|
||||
if s_attached == 0 and idle_time > ttl_seconds:
|
||||
_run_tmux("kill-session", "-t", s_name)
|
||||
_run_tmux("kill-session", "-t", s_name, node=node, container=container)
|
||||
reaped.append((s_name, idle_time))
|
||||
else:
|
||||
kept.append((s_name, idle_time, s_attached))
|
||||
|
||||
if reaped:
|
||||
print(f"✔ Reaped {len(reaped)} stale unattached session(s) (idle > {ttl_seconds//3600}h):")
|
||||
print(f"✔ Reaped {len(reaped)} stale unattached session(s) on {_target_label(node, container)} (idle > {ttl_seconds//3600}h):")
|
||||
for s_name, idle in reaped:
|
||||
print(f" • {s_name} (idle: {idle//60}m)")
|
||||
else:
|
||||
print(f"No stale unattached sessions to reap (all active within {ttl_seconds//3600}h).")
|
||||
print(f"No stale unattached sessions to reap on {_target_label(node, container)} (all active within {ttl_seconds//3600}h).")
|
||||
return 0
|
||||
|
||||
def cmd_attach(args):
|
||||
session = args.session
|
||||
node = getattr(args, "node", None)
|
||||
container = getattr(args, "container", None)
|
||||
if node:
|
||||
cmd = ["/home/super/Projects/NetVM/bin/netvm-exec.sh", node, "--", TMUX_BIN, "-S", f"/tmp/tmux-{node}.sock", "attach", "-t", session]
|
||||
os.execv(cmd[0], cmd)
|
||||
elif container:
|
||||
cmd = ["docker", "exec", "-it", container, "tmux", "attach", "-t", session]
|
||||
os.execv("/usr/bin/docker", cmd)
|
||||
else:
|
||||
cmd = [TMUX_BIN, "-S", SOCKET_PATH, "attach", "-t", session]
|
||||
# Replace current process for interactive attach
|
||||
os.execv(TMUX_BIN, cmd)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Shared Muse tmux socket manager")
|
||||
# Common parent parser for hybrid routing
|
||||
parent = argparse.ArgumentParser(add_help=False)
|
||||
parent.add_argument("--node", default=argparse.SUPPRESS, help="Target NetVM node namespace (e.g. pip, dev, 646, opm, muse, def)")
|
||||
parent.add_argument("--container", default=argparse.SUPPRESS, help="Target Docker container name")
|
||||
|
||||
parser = argparse.ArgumentParser(description="Shared Muse tmux socket manager (host, netns nodes, containers)", parents=[parent])
|
||||
subparsers = parser.add_subparsers(dest="action")
|
||||
|
||||
# list
|
||||
p_ls = subparsers.add_parser("list", aliases=["ls"], help="List sessions on shared socket")
|
||||
p_ls = subparsers.add_parser("list", aliases=["ls"], parents=[parent], help="List sessions on shared socket or node/container")
|
||||
p_ls.set_defaults(func=cmd_list)
|
||||
|
||||
# new
|
||||
p_new = subparsers.add_parser("new", help="Create new background session")
|
||||
p_new = subparsers.add_parser("new", parents=[parent], help="Create new background session")
|
||||
p_new.add_argument("session", help="Session name")
|
||||
p_new.add_argument("--window", "-w", help="Initial window name")
|
||||
p_new.add_argument("--command", "-c", help="Command to run in session")
|
||||
p_new.set_defaults(func=cmd_new)
|
||||
|
||||
# send / send-keys
|
||||
p_send = subparsers.add_parser("send", aliases=["send-keys"], help="Send keys to a session")
|
||||
p_send = subparsers.add_parser("send", aliases=["send-keys"], parents=[parent], help="Send keys to a session")
|
||||
p_send.add_argument("session", help="Target session name")
|
||||
p_send.add_argument("keys", help="Keys / command string to send")
|
||||
p_send.add_argument("--no-enter", action="store_true", help="Do not send Enter key after keys")
|
||||
p_send.set_defaults(func=cmd_send)
|
||||
|
||||
# capture
|
||||
p_cap = subparsers.add_parser("capture", aliases=["cap", "tail"], help="Capture pane output")
|
||||
p_cap = subparsers.add_parser("capture", aliases=["cap", "tail"], parents=[parent], help="Capture pane output")
|
||||
p_cap.add_argument("session", help="Target session name")
|
||||
p_cap.add_argument("--lines", "-n", type=int, default=30, help="Number of scrollback lines to capture (default: 30)")
|
||||
p_cap.set_defaults(func=cmd_capture)
|
||||
|
||||
# kill
|
||||
p_kill = subparsers.add_parser("kill", help="Kill a session")
|
||||
p_kill = subparsers.add_parser("kill", parents=[parent], help="Kill a session")
|
||||
p_kill.add_argument("session", help="Session name")
|
||||
p_kill.set_defaults(func=cmd_kill)
|
||||
|
||||
# prune
|
||||
p_prune = subparsers.add_parser("prune", help="Reap stale unattached sessions inactive for >TTL (default: 7200s / 2h)")
|
||||
p_prune = subparsers.add_parser("prune", parents=[parent], help="Reap stale unattached sessions inactive for >TTL (default: 7200s / 2h)")
|
||||
p_prune.add_argument("--ttl", type=int, default=7200, help="Inactivity threshold in seconds (default: 7200)")
|
||||
p_prune.set_defaults(func=cmd_prune)
|
||||
|
||||
# attach
|
||||
p_att = subparsers.add_parser("attach", help="Attach to a session interactively")
|
||||
p_att = subparsers.add_parser("attach", parents=[parent], help="Attach to a session interactively")
|
||||
p_att.add_argument("session", help="Session name")
|
||||
p_att.set_defaults(func=cmd_attach)
|
||||
|
||||
|
||||
+14
-24
@@ -89,7 +89,7 @@ def thread_url(target):
|
||||
|
||||
|
||||
def wrap(job_name, job_id, agent, target, rendered):
|
||||
"""Return the full DM body (without the [JOB id] prefix)."""
|
||||
"""Return the full DM body as a direct, authentic operator directive."""
|
||||
profile = pick_profile(job_name)
|
||||
spawn = spawn_call(job_id, job_name, profile)
|
||||
follow = native_followup_call(job_id, job_name, profile, agent)
|
||||
@@ -99,33 +99,23 @@ def wrap(job_name, job_id, agent, target, rendered):
|
||||
tmux_new = _tool("tmux.new", {"session": session_name, "command": "bash"})
|
||||
tmux_send = _tool("tmux.send", {"session": session_name, "keys": "echo 'Starting task execution...'"})
|
||||
top = (
|
||||
f"[WO:{wo_id}] WORK ORDER - ACTION REQUIRED, NOT INFORMATIONAL.\n"
|
||||
"Your FIRST output must be tool calls, not prose. Run background commands via shared tmux:\n"
|
||||
" 1) %s\n"
|
||||
" 2) %s\n"
|
||||
" 3) %s\n"
|
||||
" 4) %s\n"
|
||||
"Each tool call returns a result message; keep going until the task is done.\n"
|
||||
"---- TASK ----\n" % (tmux_new, tmux_send, spawn, follow)
|
||||
f"Operator Directive [ref:{wo_id}]:\n"
|
||||
"Execute the task below using tool calls. Background tmux session is ready for command execution:\n"
|
||||
f" • {tmux_new}\n"
|
||||
f" • {tmux_send}\n"
|
||||
f" • Subagent assistance: {spawn}\n"
|
||||
f" • Verification schedule: {follow}\n\n"
|
||||
"--- Task ---\n"
|
||||
)
|
||||
has_result = "[RESULT" in rendered
|
||||
bottom = (
|
||||
"\n---- END TASK ----\n"
|
||||
"REMINDER (importance: critical): work is only real if a tool call ran. "
|
||||
"Execute and inspect commands in your background tmux session or subagents:\n"
|
||||
" %s\n"
|
||||
" %s\n"
|
||||
" Capture output: [TOOL tmux.capture {\"session\": \"%s\", \"lines\": 30}]\n"
|
||||
"Native tools (preferred): cron.create (runonce/interval), cron.status, cron.view, cron.runs "
|
||||
"(truth about what ran), cron.run, subagent spawn; hooks.dry_run before hooks.enable. "
|
||||
"Native cron form: %s\n"
|
||||
"Box fallback tools: [TOOL health.check {}] [TOOL swarm.list {}] [TOOL swarm.status {\"swarm_id\": \"<id>\"}] "
|
||||
"[TOOL swarm.results {\"swarm_id\": \"<id>\"}] [TOOL quality.check {}]\n"
|
||||
% (spawn, follow, session_name, native_followup_call(job_id, job_name, profile, agent))
|
||||
"\n--- End Task ---\n\n"
|
||||
f"Inspect tmux output: [TOOL tmux.capture {{\"session\": \"{session_name}\", \"lines\": 30}}]\n"
|
||||
"Tools available: cron.create, cron.runs, health.check, swarm.spawn, swarm.list.\n"
|
||||
)
|
||||
bottom += _box_surface_block(agent, job_name, job_id, target)
|
||||
if not has_result:
|
||||
bottom += "Close with: [RESULT %s] <OK|FAILED>: <summary of tool outputs>\n" % job_id
|
||||
bottom += f"When complete, report your verdict: [RESULT {job_id}] OK: <summary of actions>\n"
|
||||
else:
|
||||
bottom += "Close using the [RESULT ...] line specified in the task, summarising tool outputs.\n"
|
||||
bottom += f"Conclude with your [RESULT {job_id}] line reporting outcomes.\n"
|
||||
return top + rendered.strip() + "\n" + bottom
|
||||
|
||||
|
||||
Reference in New Issue
Block a user