meshcode 2.11.154__tar.gz → 2.11.157__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {meshcode-2.11.154 → meshcode-2.11.157}/PKG-INFO +1 -1
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/__init__.py +1 -1
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/meshcode_mcp/server.py +34 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/protocol_handler.py +59 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/run_agent.py +101 -11
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/self_update.py +10 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/setup_clients.py +8 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode.egg-info/SOURCES.txt +5 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/pyproject.toml +1 -1
- meshcode-2.11.157/tests/test_ensure_boot_env_urgent_wake.py +95 -0
- meshcode-2.11.157/tests/test_fleet_reaper.py +176 -0
- meshcode-2.11.157/tests/test_replica_base_workspace_fallback.py +87 -0
- meshcode-2.11.157/tests/test_replica_boot_protocol_unconditional.py +64 -0
- meshcode-2.11.157/tests/test_urgent_wake_tmux.py +137 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/README.md +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/__main__.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/_launch_smoke.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/_update_guard.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/cli.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/compat.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/daemon.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/doctor.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/helper_visuals.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/hooks/__init__.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/hooks/push_guard.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/hooks/repo_path_lock.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/hostd.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/invites.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/launcher.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/meshcode_mcp/swarm.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/meshcode_mcp/test_swarm.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/preferences.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/secrets.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/up.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode/upload.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/setup.cfg +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_core.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_doctor.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_helper_visuals.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_hostd_launch_pinned_env.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_hostd_serve_discovery_split.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_hostd_zombie_sessions.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_launch_smoke.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_live_mesh_guard.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_preflight_hb_gate.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_pretrust_claude.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_push_guard.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_rm_guard.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_session_replay_gate.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_stop_ghost_terminal.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_swarm_events.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_task_progress.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_terminal_lifecycle.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_up_launch_cmd.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_update_guard.py +0 -0
- {meshcode-2.11.154 → meshcode-2.11.157}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -491,6 +491,40 @@ def _try_auto_wake(from_agent: str, preview: str, urgent: bool = False) -> None:
|
|
|
491
491
|
# configurations, pipelines, tmux without termguicolors) render the raw
|
|
492
492
|
# bytes — "[91m[1m[mesh][0m …" — which is worse UX than no color at all.
|
|
493
493
|
nudge = f"[mesh] {safe_agent} > {safe_preview} — meshcode_check()"
|
|
494
|
+
# tmux fast-path (task 780d7397): the fleet runs inside a tmux session, so the
|
|
495
|
+
# MCP serve process (a child of `claude` in the pane) inherits TMUX_PANE. When
|
|
496
|
+
# it's present, prefer `tmux send-keys` over the window-activation fallbacks —
|
|
497
|
+
# only tmux can ACTUALLY interrupt Claude's current turn (Escape) and then
|
|
498
|
+
# inject+submit the nudge, so an urgent message reaches a mid-step agent in
|
|
499
|
+
# ~1-2s instead of waiting for its next tool-call boundary. AppleScript/
|
|
500
|
+
# PowerShell/xdotool below cannot interrupt generation; they only help a truly
|
|
501
|
+
# idle agent and so remain the fallback when we're not inside tmux.
|
|
502
|
+
tmux_pane = os.environ.get("TMUX_PANE", "")
|
|
503
|
+
if tmux_pane and re.match(r'^%[0-9]+$', tmux_pane):
|
|
504
|
+
# Urgent gets a short, unambiguous marker that forces a meshcode_check —
|
|
505
|
+
# never the full payload (the agent reads the real message via the check).
|
|
506
|
+
marker = (f"[URGENT] {safe_agent} - run meshcode_check()"
|
|
507
|
+
if urgent else nudge)
|
|
508
|
+
try:
|
|
509
|
+
# 1) Escape interrupts Claude's in-flight generation/turn.
|
|
510
|
+
subprocess.run(["tmux", "send-keys", "-t", tmux_pane, "Escape"],
|
|
511
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
512
|
+
timeout=5)
|
|
513
|
+
# 2) brief pause so the interrupt registers before we type.
|
|
514
|
+
_time.sleep(0.2)
|
|
515
|
+
# 3) inject the marker literally (-l so it is never parsed as a key
|
|
516
|
+
# name), then submit with a separate Enter so Claude reads it now.
|
|
517
|
+
subprocess.run(["tmux", "send-keys", "-t", tmux_pane, "-l", marker],
|
|
518
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
519
|
+
timeout=5)
|
|
520
|
+
_time.sleep(0.15) # let the TUI register the typed text before submit
|
|
521
|
+
subprocess.run(["tmux", "send-keys", "-t", tmux_pane, "Enter"],
|
|
522
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
523
|
+
timeout=5)
|
|
524
|
+
log.info(f"auto-wake: interrupted+injected via tmux pane {tmux_pane}")
|
|
525
|
+
return
|
|
526
|
+
except Exception as e:
|
|
527
|
+
log.debug(f"auto-wake tmux path failed, falling back: {e}")
|
|
494
528
|
system = platform.system()
|
|
495
529
|
try:
|
|
496
530
|
if system == "Darwin":
|
|
@@ -373,6 +373,60 @@ def _fleet_attach_linux(tmux: str) -> tuple[bool, str]:
|
|
|
373
373
|
return _spawn_terminal_linux(attach)
|
|
374
374
|
|
|
375
375
|
|
|
376
|
+
def _reap_dead_fleet_windows(tmux: str, cap: Optional[int] = None) -> int:
|
|
377
|
+
"""Reap accumulated DEAD windows from the fleet tmux session (task 22fcdb21,
|
|
378
|
+
Samuel P1: dead panes stacked to 51).
|
|
379
|
+
|
|
380
|
+
The RAII trap in _fleet_wrap closes a window on a CLEAN / SIGTERM exit, but
|
|
381
|
+
a SIGKILL (rc=137: force-kill / OOM / recycle) cannot be trapped, so the
|
|
382
|
+
per-window `remain-on-exit=failed` keeps that dead pane forever. Over days
|
|
383
|
+
these pile up and `#{session_windows}` (the 'N agents' counter) counts
|
|
384
|
+
cadavers, not live agents.
|
|
385
|
+
|
|
386
|
+
Sweeps every fleet window whose panes are ALL dead, KEEPING only the `cap`
|
|
387
|
+
most-recently-dead (bounded crash-retention for debugging) and killing the
|
|
388
|
+
rest. It NEVER touches a window with a live pane, so it can never close a
|
|
389
|
+
running agent. Returns the count reaped. Best-effort; never raises. Tunable
|
|
390
|
+
via MESHCODE_FLEET_DEAD_CAP (default 3; 0 = reap every dead pane)."""
|
|
391
|
+
if cap is None:
|
|
392
|
+
try:
|
|
393
|
+
cap = int(os.environ.get("MESHCODE_FLEET_DEAD_CAP", "3") or 3)
|
|
394
|
+
except ValueError:
|
|
395
|
+
cap = 3
|
|
396
|
+
cap = max(0, cap)
|
|
397
|
+
try:
|
|
398
|
+
r = _tmux(tmux, "list-panes", "-s", "-t", f"={_FLEET_SESSION}",
|
|
399
|
+
"-F", "#{window_id}\t#{pane_dead}\t#{pane_dead_time}")
|
|
400
|
+
if r.returncode != 0:
|
|
401
|
+
return 0
|
|
402
|
+
# Group panes by window: a window is dead only if EVERY pane is dead
|
|
403
|
+
# (fleet windows are single-pane, but stay correct if that changes).
|
|
404
|
+
wins: dict = {}
|
|
405
|
+
for line in (r.stdout or "").splitlines():
|
|
406
|
+
parts = line.split("\t")
|
|
407
|
+
if len(parts) < 2:
|
|
408
|
+
continue
|
|
409
|
+
wid, pdead = parts[0], parts[1]
|
|
410
|
+
try:
|
|
411
|
+
pdt = int(parts[2]) if len(parts) > 2 and parts[2] else 0
|
|
412
|
+
except ValueError:
|
|
413
|
+
pdt = 0
|
|
414
|
+
w = wins.setdefault(wid, {"all_dead": True, "dt": 0})
|
|
415
|
+
if pdead != "1":
|
|
416
|
+
w["all_dead"] = False
|
|
417
|
+
else:
|
|
418
|
+
w["dt"] = max(w["dt"], pdt)
|
|
419
|
+
dead = sorted(((v["dt"], wid) for wid, v in wins.items() if v["all_dead"]),
|
|
420
|
+
reverse=True) # newest dead first; keep[:cap], reap the tail
|
|
421
|
+
reaped = 0
|
|
422
|
+
for _dt, wid in dead[cap:]:
|
|
423
|
+
if _tmux(tmux, "kill-window", "-t", wid).returncode == 0:
|
|
424
|
+
reaped += 1
|
|
425
|
+
return reaped
|
|
426
|
+
except Exception:
|
|
427
|
+
return 0
|
|
428
|
+
|
|
429
|
+
|
|
376
430
|
def _spawn_fleet_tab(cmd: str) -> tuple[bool, str]:
|
|
377
431
|
"""Run `cmd` as a TAB (tmux window) of the shared fleet window.
|
|
378
432
|
|
|
@@ -399,6 +453,11 @@ def _spawn_fleet_tab(cmd: str) -> tuple[bool, str]:
|
|
|
399
453
|
if r.returncode != 0: # lost a create race -> join as a tab
|
|
400
454
|
fresh_session = False
|
|
401
455
|
if not fresh_session:
|
|
456
|
+
# Sweep-on-launch: reap accumulated dead panes BEFORE adding this
|
|
457
|
+
# agent's tab, so the fleet bar reflects live agents (not the 51
|
|
458
|
+
# cadavers Samuel saw) and a respawn reuses the slot (task 22fcdb21).
|
|
459
|
+
# Only ever touches already-dead windows — never a live agent.
|
|
460
|
+
_reap_dead_fleet_windows(tmux)
|
|
402
461
|
r = _tmux(tmux, "new-window", "-d", "-t", f"={_FLEET_SESSION}:",
|
|
403
462
|
"-n", label, "-P", "-F", "#{window_id}", wrapped)
|
|
404
463
|
if r.returncode != 0:
|
|
@@ -933,11 +933,18 @@ def _preflight_hb_enabled() -> bool:
|
|
|
933
933
|
return os.environ.get("MESHCODE_HOSTD_SPAWN") != "1"
|
|
934
934
|
|
|
935
935
|
|
|
936
|
-
def _report_launch_failure(agent: str, project: str, reason: str, detail: str
|
|
936
|
+
def _report_launch_failure(agent: str, project: str, reason: str, detail: str,
|
|
937
|
+
status: str = "offline") -> None:
|
|
937
938
|
"""Editor spawn failed AFTER the pre-flight heartbeat already marked the
|
|
938
939
|
agent online — revert the ghost 'online' row to offline and tell the
|
|
939
940
|
dashboard WHY (launch-reliability fix 809d3b37, factors C + H).
|
|
940
941
|
|
|
942
|
+
`status` (must be a valid mc_agents.status per valid_agent_status): default
|
|
943
|
+
'offline' reverts a ghost-online editor-spawn failure. The replica
|
|
944
|
+
workspace-provisioning failure (task 9698d738) passes 'needs_setup' — the
|
|
945
|
+
failure IS an unprovisioned workspace, so the dashboard CTA matches the fix
|
|
946
|
+
(`meshcode setup <proj> <agent>`). 'error' is NOT a valid status.
|
|
947
|
+
|
|
941
948
|
This replaces a broken inline block in run()'s FileNotFoundError handler
|
|
942
949
|
that referenced `api_key`/`project_id` — names that are never bound in
|
|
943
950
|
run()'s scope — so the very RPC meant to surface "claude not installed"
|
|
@@ -987,12 +994,13 @@ def _report_launch_failure(agent: str, project: str, reason: str, detail: str) -
|
|
|
987
994
|
except Exception:
|
|
988
995
|
pass
|
|
989
996
|
|
|
990
|
-
# 2)
|
|
997
|
+
# 2) Set the failure status (default 'offline' reverts the pre-flight ghost;
|
|
998
|
+
# 'needs_setup' for an unprovisioned replica workspace — factor C).
|
|
991
999
|
status_body = json.dumps({
|
|
992
1000
|
"p_api_key": api_key,
|
|
993
1001
|
"p_project_id": project_id,
|
|
994
1002
|
"p_agent_name": agent,
|
|
995
|
-
"p_status":
|
|
1003
|
+
"p_status": status,
|
|
996
1004
|
"p_task": f"launch failed: {reason}",
|
|
997
1005
|
})
|
|
998
1006
|
status_req = Request(
|
|
@@ -1005,6 +1013,49 @@ def _report_launch_failure(agent: str, project: str, reason: str, detail: str) -
|
|
|
1005
1013
|
print(f"[meshcode] Launch-failure report skipped: {e}", file=sys.stderr)
|
|
1006
1014
|
|
|
1007
1015
|
|
|
1016
|
+
def _provision_replica_mcp_from_base(project: str, agent: str, ws: "Path") -> bool:
|
|
1017
|
+
"""BLINDAJE (task 9698d738 / Samuel P0 "réplicas prenden pero no bootean"):
|
|
1018
|
+
provision a replica's .mcp.json by cloning the BASE agent's known-good one when a
|
|
1019
|
+
fresh setup_workspace() fails (e.g. the keychain is unreadable non-interactively at
|
|
1020
|
+
hostd spawn -> setup_workspace returns 2 -> run_agent used to `return 2` and DIE
|
|
1021
|
+
before the MCP server even started: instance_id NULL, heartbeat NEVER, never ran the
|
|
1022
|
+
lease = "prende pero no bootea").
|
|
1023
|
+
|
|
1024
|
+
A replica named '<base>-<N>' is the same user's clone, so the base's MCP server
|
|
1025
|
+
block (api key / project_id / keychain profile baked at its last setup) is valid for
|
|
1026
|
+
it. We copy it, swap ONLY the server id + MESHCODE_AGENT, and write it to the replica
|
|
1027
|
+
workspace. The base workspace exists whenever the base agent booted (the common
|
|
1028
|
+
case — and the empirically healthy replicas all had a pre-existing workspace).
|
|
1029
|
+
Returns True iff a .mcp.json was written. Never raises."""
|
|
1030
|
+
try:
|
|
1031
|
+
import re as _re
|
|
1032
|
+
m = _re.match(r"^(?P<base>.+)-(?P<n>\d+)$", agent)
|
|
1033
|
+
if not m:
|
|
1034
|
+
return False # not a '<base>-<N>' replica name — nothing to clone
|
|
1035
|
+
base = m.group("base")
|
|
1036
|
+
base_mcp = WORKSPACES_ROOT / f"{project}-{base}" / ".mcp.json"
|
|
1037
|
+
if not base_mcp.exists():
|
|
1038
|
+
return False
|
|
1039
|
+
doc = json.loads(base_mcp.read_text(encoding="utf-8"))
|
|
1040
|
+
servers = doc.get("mcpServers") or {}
|
|
1041
|
+
if not servers:
|
|
1042
|
+
return False
|
|
1043
|
+
# base workspaces hold exactly one server block
|
|
1044
|
+
_base_id, block = next(iter(servers.items()))
|
|
1045
|
+
block = json.loads(json.dumps(block)) # deep copy so we don't mutate the source
|
|
1046
|
+
env = block.get("env")
|
|
1047
|
+
if isinstance(env, dict):
|
|
1048
|
+
env["MESHCODE_AGENT"] = agent # the only identity that differs from the base
|
|
1049
|
+
new_id = f"meshcode-{project}-{agent}"
|
|
1050
|
+
ws.mkdir(parents=True, exist_ok=True)
|
|
1051
|
+
(ws / ".mcp.json").write_text(
|
|
1052
|
+
json.dumps({"mcpServers": {new_id: block}}, indent=2) + "\n", encoding="utf-8")
|
|
1053
|
+
return (ws / ".mcp.json").exists()
|
|
1054
|
+
except Exception as e:
|
|
1055
|
+
print(f"[meshcode] replica base-fallback .mcp.json failed: {e}", file=sys.stderr)
|
|
1056
|
+
return False
|
|
1057
|
+
|
|
1058
|
+
|
|
1008
1059
|
# Repo-scoped launch (task 24e3dd44 / core-commander launch-diff). When `meshcode run
|
|
1009
1060
|
# <agent> --repo <path>` is used, the agent boots with cwd=repo (not the meshcode
|
|
1010
1061
|
# workspace), so its repo CLAUDE.md loads — we carry the boot protocol via
|
|
@@ -1293,19 +1344,44 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
1293
1344
|
if not mcp_json_path.exists():
|
|
1294
1345
|
# Regenerate via setup_workspace (has all data it needs from server)
|
|
1295
1346
|
print(f"[meshcode] .mcp.json missing from workspace — regenerating...", file=sys.stderr)
|
|
1347
|
+
_regen_ok = False
|
|
1348
|
+
_regen_err = ""
|
|
1296
1349
|
try:
|
|
1297
1350
|
from .setup_clients import setup_workspace
|
|
1298
1351
|
rc = setup_workspace(resolved_project, agent,
|
|
1299
1352
|
keychain_profile=_preferred_keychain_profile(resolved_project, agent))
|
|
1300
|
-
|
|
1353
|
+
_regen_ok = (rc == 0 and mcp_json_path.exists())
|
|
1354
|
+
if _regen_ok:
|
|
1301
1355
|
print(f"[meshcode] .mcp.json regenerated successfully.", file=sys.stderr)
|
|
1302
1356
|
else:
|
|
1303
|
-
|
|
1304
|
-
print(f"[meshcode]
|
|
1305
|
-
return 2
|
|
1357
|
+
_regen_err = f"setup_workspace rc={rc}"
|
|
1358
|
+
print(f"[meshcode] ERROR: could not regenerate .mcp.json ({_regen_err}).", file=sys.stderr)
|
|
1306
1359
|
except Exception as e:
|
|
1360
|
+
_regen_err = repr(e)
|
|
1307
1361
|
print(f"[meshcode] ERROR: .mcp.json missing and regeneration failed: {e}", file=sys.stderr)
|
|
1308
|
-
|
|
1362
|
+
|
|
1363
|
+
# BLINDAJE 1 (task 9698d738): replica base-workspace fallback. A replica whose
|
|
1364
|
+
# workspace was never provisioned hits this path; if setup_workspace fails (e.g.
|
|
1365
|
+
# keychain unreadable non-interactively at spawn) the process used to `return 2`
|
|
1366
|
+
# and DIE pre-boot ("prende pero no bootea"). Clone the base agent's known-good
|
|
1367
|
+
# .mcp.json so the replica boots anyway.
|
|
1368
|
+
if not _regen_ok and _provision_replica_mcp_from_base(resolved_project, agent, ws) \
|
|
1369
|
+
and mcp_json_path.exists():
|
|
1370
|
+
print(f"[meshcode] .mcp.json provisioned from base agent (replica fallback, "
|
|
1371
|
+
f"task 9698d738).", file=sys.stderr)
|
|
1372
|
+
_regen_ok = True
|
|
1373
|
+
|
|
1374
|
+
# BLINDAJE 2 (task 9698d738): never die SILENT. If we still can't provision,
|
|
1375
|
+
# report the launch as FAILED (mc_resolve_launch status='failed' + status row)
|
|
1376
|
+
# so the dashboard shows WHY instead of a phantom 'spawned but offline' replica.
|
|
1377
|
+
if not _regen_ok:
|
|
1378
|
+
_report_launch_failure(
|
|
1379
|
+
agent, resolved_project,
|
|
1380
|
+
"workspace .mcp.json could not be provisioned",
|
|
1381
|
+
f"setup_workspace failed ({_regen_err or 'unknown'}) and no base "
|
|
1382
|
+
f"workspace to clone — run `meshcode setup {resolved_project} {agent}`",
|
|
1383
|
+
status="needs_setup")
|
|
1384
|
+
print(f"[meshcode] Run `meshcode setup {resolved_project} {agent}` manually.", file=sys.stderr)
|
|
1309
1385
|
return 2
|
|
1310
1386
|
|
|
1311
1387
|
# R2-5 (2.11.117) boot-always-latest: resolve target version (explicit pin
|
|
@@ -1672,10 +1748,24 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
1672
1748
|
cmd += [
|
|
1673
1749
|
"--mcp-config", str(mcp_json_path),
|
|
1674
1750
|
"--settings", str(ws / ".claude" / "settings.json"),
|
|
1675
|
-
"--append-system-prompt", MESHCODE_BOOT_PROTOCOL.format(
|
|
1676
|
-
agent=agent, project=resolved_project,
|
|
1677
|
-
role=(agent_role or "MCP-connected agent"), repo=repo_lock),
|
|
1678
1751
|
]
|
|
1752
|
+
# Boot protocol — ALWAYS injected (task 9698d738 / Samuel P0 "réplicas prenden
|
|
1753
|
+
# pero NO bootean en TODAS las meshes"). It was gated on repo_lock on the
|
|
1754
|
+
# assumption that non-repo launches load the protocol from the workspace
|
|
1755
|
+
# CLAUDE.md at cwd=ws. But hostd-spawned REPLICAS frequently have NO provisioned
|
|
1756
|
+
# ws/CLAUDE.md (mc_replicate_agent creates the DB row, not the disk workspace)
|
|
1757
|
+
# and/or chdir lands elsewhere -> with --append gated OFF they received ZERO boot
|
|
1758
|
+
# instructions -> claude starts (spawn) but never runs SESSION START -> never
|
|
1759
|
+
# boots, in every mesh. Injecting unconditionally GUARANTEES boot regardless of
|
|
1760
|
+
# workspace provisioning / cwd, and is idempotent with a present CLAUDE.md (same
|
|
1761
|
+
# text), so repo-scoped and provisioned launches are unchanged. repo defaults to
|
|
1762
|
+
# the workspace path when there's no repo-lock.
|
|
1763
|
+
cmd += [
|
|
1764
|
+
"--append-system-prompt", MESHCODE_BOOT_PROTOCOL.format(
|
|
1765
|
+
agent=agent, project=resolved_project,
|
|
1766
|
+
role=(agent_role or "MCP-connected agent"),
|
|
1767
|
+
repo=(repo_lock or str(ws))),
|
|
1768
|
+
]
|
|
1679
1769
|
cmd.extend(["--", "boot"])
|
|
1680
1770
|
_launch_cwd = repo_lock or str(ws)
|
|
1681
1771
|
if not os.path.isdir(_launch_cwd):
|
|
@@ -704,6 +704,16 @@ def ensure_boot_env(mcp_json_path, verbose: bool = True) -> Optional[str]:
|
|
|
704
704
|
if env_block.get("MESHCODE_EXPECTED_VERSION") != target:
|
|
705
705
|
env_block["MESHCODE_EXPECTED_VERSION"] = target
|
|
706
706
|
changed = True
|
|
707
|
+
# urgent-wake default-on rollout (task 780d7397): setup_clients bakes
|
|
708
|
+
# MESHCODE_URGENT_WAKE=1 only for FRESH `meshcode setup`. Existing agents'
|
|
709
|
+
# .mcp.json is never regenerated on `meshcode run`, so the env-gated
|
|
710
|
+
# feature would stay OFF forever on installed fleets. Backfill it here —
|
|
711
|
+
# the one place that mutates an existing server env every boot — but only
|
|
712
|
+
# when the key is ABSENT, so an explicit user opt-out (set to "0") is
|
|
713
|
+
# never clobbered.
|
|
714
|
+
if "MESHCODE_URGENT_WAKE" not in env_block:
|
|
715
|
+
env_block["MESHCODE_URGENT_WAKE"] = "1"
|
|
716
|
+
changed = True
|
|
707
717
|
if changed:
|
|
708
718
|
tmp = mcp_json_path.with_name(mcp_json_path.name + ".tmp")
|
|
709
719
|
tmp.write_text(json.dumps(doc, indent=2), encoding="utf-8")
|
|
@@ -215,6 +215,14 @@ def _build_server_block(project: str, project_id: str, agent: str, role: str,
|
|
|
215
215
|
# Mark this Python process as the MCP serve subprocess so other
|
|
216
216
|
# parts of the package (e.g. self_update) skip auto-update inside it.
|
|
217
217
|
"MESHCODE_MCP_SERVE": "1",
|
|
218
|
+
# Urgent-interrupt nudging (task 780d7397, Samuel 2026-06-22): a
|
|
219
|
+
# priority='urgent' message must INTERRUPT a busy agent and be read
|
|
220
|
+
# at once, not merely queue until its next meshcode_wait. This opt-in
|
|
221
|
+
# gates the urgent-only path in server._try_auto_wake (tmux ESC+inject
|
|
222
|
+
# when in a tmux pane, AppleScript/etc fallback otherwise). Deliberately
|
|
223
|
+
# urgent-only — we do NOT set MESHCODE_AUTO_WAKE so non-urgent traffic
|
|
224
|
+
# never injects keystrokes (that stays the conservative default OFF).
|
|
225
|
+
"MESHCODE_URGENT_WAKE": "1",
|
|
218
226
|
}
|
|
219
227
|
if editor_type:
|
|
220
228
|
env["MESHCODE_EDITOR_TYPE"] = editor_type
|
|
@@ -70,11 +70,13 @@ tests/test_core.py
|
|
|
70
70
|
tests/test_cross_agent_messaging.py
|
|
71
71
|
tests/test_date_parse.py
|
|
72
72
|
tests/test_doctor.py
|
|
73
|
+
tests/test_ensure_boot_env_urgent_wake.py
|
|
73
74
|
tests/test_epistemic_v1_python_sdk.py
|
|
74
75
|
tests/test_epistemic_v1_stop_conditions.py
|
|
75
76
|
tests/test_esc_deaf_state.py
|
|
76
77
|
tests/test_exceptions.py
|
|
77
78
|
tests/test_file_upload.py
|
|
79
|
+
tests/test_fleet_reaper.py
|
|
78
80
|
tests/test_helper_visuals.py
|
|
79
81
|
tests/test_hostd_launch_pinned_env.py
|
|
80
82
|
tests/test_hostd_serve_discovery_split.py
|
|
@@ -91,6 +93,8 @@ tests/test_preflight_hb_gate.py
|
|
|
91
93
|
tests/test_pretrust_claude.py
|
|
92
94
|
tests/test_push_guard.py
|
|
93
95
|
tests/test_realtime_event_freshness.py
|
|
96
|
+
tests/test_replica_base_workspace_fallback.py
|
|
97
|
+
tests/test_replica_boot_protocol_unconditional.py
|
|
94
98
|
tests/test_rls_cross_tenant.py
|
|
95
99
|
tests/test_rm_guard.py
|
|
96
100
|
tests/test_rpc_grants.py
|
|
@@ -111,4 +115,5 @@ tests/test_task_progress.py
|
|
|
111
115
|
tests/test_terminal_lifecycle.py
|
|
112
116
|
tests/test_up_launch_cmd.py
|
|
113
117
|
tests/test_update_guard.py
|
|
118
|
+
tests/test_urgent_wake_tmux.py
|
|
114
119
|
tests/test_wait_open_tasks_contradiction.py
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ensure_boot_env backfills MESHCODE_URGENT_WAKE (task 780d7397 / 2.11.157)
|
|
3
|
+
=========================================================================
|
|
4
|
+
setup_clients bakes MESHCODE_URGENT_WAKE=1 only for a FRESH `meshcode setup`.
|
|
5
|
+
An already-installed agent's .mcp.json is never regenerated on `meshcode run`
|
|
6
|
+
(run_agent only writes it when MISSING), so the env-gated urgent-wake feature
|
|
7
|
+
would stay OFF forever on existing fleets — exactly the gap that would have made
|
|
8
|
+
the front-end smoke fail.
|
|
9
|
+
|
|
10
|
+
ensure_boot_env is the one place that mutates an existing server env block every
|
|
11
|
+
boot. It must backfill the flag when ABSENT, but must NEVER clobber an explicit
|
|
12
|
+
user opt-out (value "0").
|
|
13
|
+
|
|
14
|
+
These tests stub the version-resolution surface so no network / env-pin happens;
|
|
15
|
+
they exercise only the .mcp.json mutation.
|
|
16
|
+
"""
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
22
|
+
|
|
23
|
+
import meshcode.self_update as su
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _write_mcp(tmp_path, env_block):
|
|
27
|
+
p = tmp_path / ".mcp.json"
|
|
28
|
+
doc = {
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"meshcode-x": {
|
|
31
|
+
"command": "/fake/envs/9.9.9/bin/python3",
|
|
32
|
+
"args": ["-m", "meshcode.meshcode_mcp", "serve"],
|
|
33
|
+
"env": env_block,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
p.write_text(json.dumps(doc, indent=2), encoding="utf-8")
|
|
38
|
+
return p
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _stub_versions(monkeypatch, target="9.9.9"):
|
|
42
|
+
"""No-network: current env already == target, so no env-pin path runs."""
|
|
43
|
+
monkeypatch.setattr(su, "update_disabled", lambda: False)
|
|
44
|
+
monkeypatch.setattr(su, "is_editable_install", lambda: False)
|
|
45
|
+
monkeypatch.setattr(su, "resolve_target_version", lambda: target)
|
|
46
|
+
monkeypatch.setattr(su, "_env_version", lambda cmd: target)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _read_env(p):
|
|
50
|
+
doc = json.loads(p.read_text(encoding="utf-8"))
|
|
51
|
+
return next(iter(doc["mcpServers"].values()))["env"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_backfills_when_absent(tmp_path, monkeypatch):
|
|
55
|
+
_stub_versions(monkeypatch)
|
|
56
|
+
p = _write_mcp(tmp_path, {"MESHCODE_EXPECTED_VERSION": "9.9.9"})
|
|
57
|
+
out = su.ensure_boot_env(p, verbose=False)
|
|
58
|
+
assert out == "9.9.9"
|
|
59
|
+
assert _read_env(p)["MESHCODE_URGENT_WAKE"] == "1"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_does_not_clobber_explicit_optout(tmp_path, monkeypatch):
|
|
63
|
+
_stub_versions(monkeypatch)
|
|
64
|
+
p = _write_mcp(tmp_path, {
|
|
65
|
+
"MESHCODE_EXPECTED_VERSION": "9.9.9",
|
|
66
|
+
"MESHCODE_URGENT_WAKE": "0",
|
|
67
|
+
})
|
|
68
|
+
su.ensure_boot_env(p, verbose=False)
|
|
69
|
+
assert _read_env(p)["MESHCODE_URGENT_WAKE"] == "0", "user opt-out was clobbered"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_preserves_existing_on_value(tmp_path, monkeypatch):
|
|
73
|
+
_stub_versions(monkeypatch)
|
|
74
|
+
p = _write_mcp(tmp_path, {
|
|
75
|
+
"MESHCODE_EXPECTED_VERSION": "9.9.9",
|
|
76
|
+
"MESHCODE_URGENT_WAKE": "1",
|
|
77
|
+
})
|
|
78
|
+
su.ensure_boot_env(p, verbose=False)
|
|
79
|
+
assert _read_env(p)["MESHCODE_URGENT_WAKE"] == "1"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_idempotent_second_boot_no_rewrite(tmp_path, monkeypatch):
|
|
83
|
+
_stub_versions(monkeypatch)
|
|
84
|
+
p = _write_mcp(tmp_path, {"MESHCODE_EXPECTED_VERSION": "9.9.9"})
|
|
85
|
+
su.ensure_boot_env(p, verbose=False)
|
|
86
|
+
mtime1 = p.stat().st_mtime_ns
|
|
87
|
+
# Second boot: everything already in place -> changed=False -> no rewrite.
|
|
88
|
+
su.ensure_boot_env(p, verbose=False)
|
|
89
|
+
assert p.stat().st_mtime_ns == mtime1, "rewrote .mcp.json with nothing to change"
|
|
90
|
+
assert _read_env(p)["MESHCODE_URGENT_WAKE"] == "1"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
import pytest
|
|
95
|
+
sys.exit(pytest.main([__file__, "-q"]))
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fleet dead-window reaper (task 22fcdb21, Samuel P1)
|
|
3
|
+
===================================================
|
|
4
|
+
Samuel saw "51 agents" in the fleet tab bar — almost all cadavers. A SIGKILL
|
|
5
|
+
(rc=137: force-kill / OOM / recycle) cannot be trapped by the RAII wrapper, so
|
|
6
|
+
`remain-on-exit=failed` keeps that dead pane forever and `#{session_windows}`
|
|
7
|
+
counts corpses, not live agents.
|
|
8
|
+
|
|
9
|
+
`_reap_dead_fleet_windows` sweeps windows whose panes are ALL dead, keeping only
|
|
10
|
+
the `cap` most-recently-dead (bounded crash retention) and killing the rest. The
|
|
11
|
+
load-bearing safety property: it must NEVER kill a window that still has a live
|
|
12
|
+
pane (a running agent).
|
|
13
|
+
|
|
14
|
+
These tests stub `_tmux` so nothing real is spawned.
|
|
15
|
+
"""
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
21
|
+
|
|
22
|
+
import meshcode.protocol_handler as ph
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _R:
|
|
26
|
+
def __init__(self, returncode=0, stdout=""):
|
|
27
|
+
self.returncode = returncode
|
|
28
|
+
self.stdout = stdout
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _install_fake_tmux(monkeypatch_target, panes_output, *, list_rc=0):
|
|
32
|
+
"""Replace ph._tmux with a stub. Records every kill-window target."""
|
|
33
|
+
killed = []
|
|
34
|
+
|
|
35
|
+
def fake(tmux, *args):
|
|
36
|
+
if args and args[0] == "list-panes":
|
|
37
|
+
return _R(returncode=list_rc, stdout=panes_output)
|
|
38
|
+
if args and args[0] == "kill-window":
|
|
39
|
+
# args == ("kill-window", "-t", "<wid>")
|
|
40
|
+
killed.append(args[2])
|
|
41
|
+
return _R(returncode=0)
|
|
42
|
+
return _R(returncode=0)
|
|
43
|
+
|
|
44
|
+
monkeypatch_target._tmux = fake
|
|
45
|
+
return killed
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _panes(*rows):
|
|
49
|
+
"""rows: (window_id, pane_dead, pane_dead_time) tuples -> tmux -F lines."""
|
|
50
|
+
return "\n".join(f"{w}\t{d}\t{t}" for (w, d, t) in rows)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_reaps_dead_beyond_cap_keeps_newest():
|
|
54
|
+
# 5 dead windows, cap=2 -> keep the 2 newest-dead (@5,@4), reap @3,@2,@1.
|
|
55
|
+
out = _panes(("@1", "1", "100"), ("@2", "1", "200"), ("@3", "1", "300"),
|
|
56
|
+
("@4", "1", "400"), ("@5", "1", "500"))
|
|
57
|
+
orig = ph._tmux
|
|
58
|
+
try:
|
|
59
|
+
killed = _install_fake_tmux(ph, out)
|
|
60
|
+
n = ph._reap_dead_fleet_windows("tmux", cap=2)
|
|
61
|
+
assert n == 3, n
|
|
62
|
+
assert set(killed) == {"@1", "@2", "@3"}, killed
|
|
63
|
+
assert "@4" not in killed and "@5" not in killed, killed
|
|
64
|
+
finally:
|
|
65
|
+
ph._tmux = orig
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_never_reaps_live_window():
|
|
69
|
+
# @2 is LIVE (pane_dead=0). Even with cap=0 it must survive.
|
|
70
|
+
out = _panes(("@1", "1", "100"), ("@2", "0", "0"), ("@3", "1", "300"))
|
|
71
|
+
orig = ph._tmux
|
|
72
|
+
try:
|
|
73
|
+
killed = _install_fake_tmux(ph, out)
|
|
74
|
+
n = ph._reap_dead_fleet_windows("tmux", cap=0)
|
|
75
|
+
assert "@2" not in killed, f"killed a LIVE agent window: {killed}"
|
|
76
|
+
assert set(killed) == {"@1", "@3"}, killed
|
|
77
|
+
assert n == 2, n
|
|
78
|
+
finally:
|
|
79
|
+
ph._tmux = orig
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_multipane_window_with_one_live_pane_survives():
|
|
83
|
+
# @1 has two panes: one dead, one live -> window is NOT all-dead -> keep.
|
|
84
|
+
out = _panes(("@1", "1", "100"), ("@1", "0", "0"), ("@2", "1", "200"))
|
|
85
|
+
orig = ph._tmux
|
|
86
|
+
try:
|
|
87
|
+
killed = _install_fake_tmux(ph, out)
|
|
88
|
+
ph._reap_dead_fleet_windows("tmux", cap=0)
|
|
89
|
+
assert "@1" not in killed, f"reaped a window with a live pane: {killed}"
|
|
90
|
+
assert killed == ["@2"], killed
|
|
91
|
+
finally:
|
|
92
|
+
ph._tmux = orig
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_cap_zero_reaps_all_dead():
|
|
96
|
+
out = _panes(("@1", "1", "100"), ("@2", "1", "200"))
|
|
97
|
+
orig = ph._tmux
|
|
98
|
+
try:
|
|
99
|
+
killed = _install_fake_tmux(ph, out)
|
|
100
|
+
n = ph._reap_dead_fleet_windows("tmux", cap=0)
|
|
101
|
+
assert n == 2 and set(killed) == {"@1", "@2"}, killed
|
|
102
|
+
finally:
|
|
103
|
+
ph._tmux = orig
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_under_cap_reaps_nothing():
|
|
107
|
+
out = _panes(("@1", "1", "100"), ("@2", "1", "200"))
|
|
108
|
+
orig = ph._tmux
|
|
109
|
+
try:
|
|
110
|
+
killed = _install_fake_tmux(ph, out)
|
|
111
|
+
n = ph._reap_dead_fleet_windows("tmux", cap=3)
|
|
112
|
+
assert n == 0 and killed == [], killed
|
|
113
|
+
finally:
|
|
114
|
+
ph._tmux = orig
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_list_panes_failure_is_noop():
|
|
118
|
+
orig = ph._tmux
|
|
119
|
+
try:
|
|
120
|
+
killed = _install_fake_tmux(ph, "", list_rc=1)
|
|
121
|
+
n = ph._reap_dead_fleet_windows("tmux", cap=0)
|
|
122
|
+
assert n == 0 and killed == [], killed
|
|
123
|
+
finally:
|
|
124
|
+
ph._tmux = orig
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_env_default_cap_when_none():
|
|
128
|
+
out = _panes(("@1", "1", "100"), ("@2", "1", "200"),
|
|
129
|
+
("@3", "1", "300"), ("@4", "1", "400"))
|
|
130
|
+
orig = ph._tmux
|
|
131
|
+
prev = os.environ.get("MESHCODE_FLEET_DEAD_CAP")
|
|
132
|
+
try:
|
|
133
|
+
os.environ["MESHCODE_FLEET_DEAD_CAP"] = "1"
|
|
134
|
+
killed = _install_fake_tmux(ph, out)
|
|
135
|
+
n = ph._reap_dead_fleet_windows("tmux") # cap=None -> env -> 1
|
|
136
|
+
assert n == 3, n
|
|
137
|
+
assert "@4" not in killed, killed # newest-dead kept
|
|
138
|
+
finally:
|
|
139
|
+
if prev is None:
|
|
140
|
+
os.environ.pop("MESHCODE_FLEET_DEAD_CAP", None)
|
|
141
|
+
else:
|
|
142
|
+
os.environ["MESHCODE_FLEET_DEAD_CAP"] = prev
|
|
143
|
+
ph._tmux = orig
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_bad_env_cap_falls_back_to_default():
|
|
147
|
+
out = _panes(*[(f"@{i}", "1", str(i * 100)) for i in range(1, 6)])
|
|
148
|
+
orig = ph._tmux
|
|
149
|
+
prev = os.environ.get("MESHCODE_FLEET_DEAD_CAP")
|
|
150
|
+
try:
|
|
151
|
+
os.environ["MESHCODE_FLEET_DEAD_CAP"] = "not-an-int"
|
|
152
|
+
killed = _install_fake_tmux(ph, out)
|
|
153
|
+
n = ph._reap_dead_fleet_windows("tmux") # bad env -> default cap 3
|
|
154
|
+
assert n == 2, n # 5 dead, keep 3, reap 2
|
|
155
|
+
finally:
|
|
156
|
+
if prev is None:
|
|
157
|
+
os.environ.pop("MESHCODE_FLEET_DEAD_CAP", None)
|
|
158
|
+
else:
|
|
159
|
+
os.environ["MESHCODE_FLEET_DEAD_CAP"] = prev
|
|
160
|
+
ph._tmux = orig
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_exception_safe_returns_zero():
|
|
164
|
+
orig = ph._tmux
|
|
165
|
+
try:
|
|
166
|
+
def boom(*a, **k):
|
|
167
|
+
raise RuntimeError("tmux exploded")
|
|
168
|
+
ph._tmux = boom
|
|
169
|
+
assert ph._reap_dead_fleet_windows("tmux", cap=0) == 0
|
|
170
|
+
finally:
|
|
171
|
+
ph._tmux = orig
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
import pytest
|
|
176
|
+
sys.exit(pytest.main([__file__, "-q"]))
|