meshcode 2.11.155__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.
Files changed (121) hide show
  1. {meshcode-2.11.155 → meshcode-2.11.157}/PKG-INFO +1 -1
  2. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/meshcode_mcp/server.py +34 -0
  4. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/protocol_handler.py +59 -0
  5. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/self_update.py +10 -0
  6. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/setup_clients.py +8 -0
  7. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode.egg-info/PKG-INFO +1 -1
  8. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode.egg-info/SOURCES.txt +3 -0
  9. {meshcode-2.11.155 → meshcode-2.11.157}/pyproject.toml +1 -1
  10. meshcode-2.11.157/tests/test_ensure_boot_env_urgent_wake.py +95 -0
  11. meshcode-2.11.157/tests/test_fleet_reaper.py +176 -0
  12. meshcode-2.11.157/tests/test_urgent_wake_tmux.py +137 -0
  13. {meshcode-2.11.155 → meshcode-2.11.157}/README.md +0 -0
  14. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/__main__.py +0 -0
  15. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/_launch_smoke.py +0 -0
  16. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/_session_handoff_template.py +0 -0
  17. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/_stop_hook_template.py +0 -0
  18. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/_update_guard.py +0 -0
  19. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/ascii_art.py +0 -0
  20. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/atomic_push.py +0 -0
  21. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/claude_update.py +0 -0
  22. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/cli.py +0 -0
  23. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/comms_v4.py +0 -0
  24. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/compat.py +0 -0
  25. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/daemon.py +0 -0
  26. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/date_parse.py +0 -0
  27. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/doctor.py +0 -0
  28. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/error_hints.py +0 -0
  29. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/exceptions.py +0 -0
  30. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/helper_visuals.py +0 -0
  31. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/hooks/__init__.py +0 -0
  32. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/hooks/push_guard.py +0 -0
  33. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/hooks/repo_path_lock.py +0 -0
  34. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/hostd.py +0 -0
  35. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/invites.py +0 -0
  36. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/launcher.py +0 -0
  37. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/launcher_install.py +0 -0
  38. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/meshcode_mcp/__init__.py +0 -0
  39. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/meshcode_mcp/__main__.py +0 -0
  40. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/meshcode_mcp/backend.py +0 -0
  41. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/meshcode_mcp/realtime.py +0 -0
  42. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  43. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/meshcode_mcp/swarm.py +0 -0
  44. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/meshcode_mcp/test_backend.py +0 -0
  45. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  46. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  47. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  48. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  49. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  50. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/meshcode_mcp/test_swarm.py +0 -0
  51. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/preferences.py +0 -0
  52. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/protocol_v2.py +0 -0
  53. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/quickstart.py +0 -0
  54. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/rpc_allowlist.py +0 -0
  55. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/run_agent.py +0 -0
  56. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/scripts/check_secrets.py +0 -0
  57. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/scripts/race_rate_harness.py +0 -0
  58. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/secrets.py +0 -0
  59. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/supervisor.py +0 -0
  60. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/up.py +0 -0
  61. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode/upload.py +0 -0
  62. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode.egg-info/dependency_links.txt +0 -0
  63. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode.egg-info/entry_points.txt +0 -0
  64. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode.egg-info/requires.txt +0 -0
  65. {meshcode-2.11.155 → meshcode-2.11.157}/meshcode.egg-info/top_level.txt +0 -0
  66. {meshcode-2.11.155 → meshcode-2.11.157}/setup.cfg +0 -0
  67. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_auto_update_hardening.py +0 -0
  68. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_autonomous_closegap_1.py +0 -0
  69. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_autonomous_closegap_2.py +0 -0
  70. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_autonomous_closegap_3.py +0 -0
  71. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_autonomous_prompt_inject.py +0 -0
  72. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_boot_bug_regression.py +0 -0
  73. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_color_truecolor.py +0 -0
  74. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_core.py +0 -0
  75. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_cross_agent_messaging.py +0 -0
  76. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_date_parse.py +0 -0
  77. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_doctor.py +0 -0
  78. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_epistemic_v1_python_sdk.py +0 -0
  79. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  80. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_esc_deaf_state.py +0 -0
  81. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_exceptions.py +0 -0
  82. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_file_upload.py +0 -0
  83. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_helper_visuals.py +0 -0
  84. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_hostd_launch_pinned_env.py +0 -0
  85. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_hostd_serve_discovery_split.py +0 -0
  86. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_hostd_zombie_sessions.py +0 -0
  87. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_init_device_code.py +0 -0
  88. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_install_guard.py +0 -0
  89. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_launch_smoke.py +0 -0
  90. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_lease_sigterm_release.py +0 -0
  91. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_live_mesh_guard.py +0 -0
  92. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_mark_read_batch.py +0 -0
  93. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_marketplace_ratings.py +0 -0
  94. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_migration_integrity.py +0 -0
  95. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_preflight_hb_gate.py +0 -0
  96. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_pretrust_claude.py +0 -0
  97. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_push_guard.py +0 -0
  98. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_realtime_event_freshness.py +0 -0
  99. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_replica_base_workspace_fallback.py +0 -0
  100. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_replica_boot_protocol_unconditional.py +0 -0
  101. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_rls_cross_tenant.py +0 -0
  102. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_rm_guard.py +0 -0
  103. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_rpc_grants.py +0 -0
  104. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_rpc_migrations.py +0 -0
  105. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_run_agent_dry_run.py +0 -0
  106. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_run_agent_no_server_import.py +0 -0
  107. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_security_regressions.py +0 -0
  108. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_self_update_user_site.py +0 -0
  109. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_sentinel.py +0 -0
  110. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_session_replay_gate.py +0 -0
  111. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_setup_path.py +0 -0
  112. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_sleep_signals.py +0 -0
  113. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_status_enum_coverage.py +0 -0
  114. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_stay_on_loop_hook.py +0 -0
  115. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_stop_ghost_terminal.py +0 -0
  116. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_swarm_events.py +0 -0
  117. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_task_progress.py +0 -0
  118. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_terminal_lifecycle.py +0 -0
  119. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_up_launch_cmd.py +0 -0
  120. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_update_guard.py +0 -0
  121. {meshcode-2.11.155 → meshcode-2.11.157}/tests/test_wait_open_tasks_contradiction.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.155
3
+ Version: 2.11.157
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.11.155"
2
+ __version__ = "2.11.157"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -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:
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.155
3
+ Version: 2.11.157
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -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
@@ -113,4 +115,5 @@ tests/test_task_progress.py
113
115
  tests/test_terminal_lifecycle.py
114
116
  tests/test_up_launch_cmd.py
115
117
  tests/test_update_guard.py
118
+ tests/test_urgent_wake_tmux.py
116
119
  tests/test_wait_open_tasks_contradiction.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.11.155"
7
+ version = "2.11.157"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -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"]))
@@ -0,0 +1,137 @@
1
+ """
2
+ Urgent-wake tmux interrupt path (task 780d7397)
3
+ ================================================
4
+ Samuel (2026-06-22): a priority='urgent' message must INTERRUPT a busy agent
5
+ and be read at once, not merely queue until its next meshcode_wait.
6
+
7
+ Mechanism (server._try_auto_wake):
8
+ - When the MCP serve process inherits TMUX_PANE (the fleet runs inside tmux),
9
+ prefer `tmux send-keys`: Escape (interrupt the current Claude turn) → inject
10
+ a short marker literally (-l) → Enter (submit). This actually interrupts
11
+ generation, unlike the AppleScript/PowerShell/xdotool window-activation
12
+ fallbacks which only help a truly idle agent.
13
+ - Gated on MESHCODE_URGENT_WAKE (urgent-only; we deliberately do NOT enable
14
+ full MESHCODE_AUTO_WAKE). setup_clients._build_server_block bakes the flag.
15
+
16
+ These tests don't need a running MCP server — they drive _try_auto_wake directly
17
+ with the module globals arranged + subprocess stubbed.
18
+ """
19
+ import inspect
20
+ import os
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ # Minimal env so importing the server module doesn't hard-exit.
25
+ for _k, _v in {
26
+ "SUPABASE_URL": "https://gjinagyyjttyxnaoavnz.supabase.co",
27
+ "SUPABASE_KEY": "test",
28
+ "MESHCODE_MCP_SERVE": "1",
29
+ "MESHCODE_PROJECT": "meshcode-self-improve",
30
+ "MESHCODE_PROJECT_ID": "test",
31
+ "MESHCODE_AGENT": "backend",
32
+ "MESHCODE_API_KEY": "test-key",
33
+ }.items():
34
+ os.environ.setdefault(_k, _v)
35
+
36
+ sys.path.insert(0, str(Path(__file__).parent.parent))
37
+
38
+ import subprocess as _real_subprocess # server.py does `import subprocess` inside the fn
39
+
40
+
41
+ def _arrange(srv, *, urgent_wake=True, auto_wake=False, in_wait=False,
42
+ state="working", agent="backend", pane="%7"):
43
+ """Put the module into a known state and capture tmux/osascript calls."""
44
+ srv._URGENT_WAKE = urgent_wake
45
+ srv._AUTO_WAKE = auto_wake
46
+ srv._IN_WAIT = in_wait
47
+ srv._current_state = state
48
+ srv.AGENT_NAME = agent
49
+ srv._last_tool_at = 0.0
50
+ srv._time.sleep = lambda *a, **k: None
51
+ if pane is None:
52
+ os.environ.pop("TMUX_PANE", None)
53
+ else:
54
+ os.environ["TMUX_PANE"] = pane
55
+ calls = []
56
+
57
+ class _R:
58
+ returncode = 0
59
+
60
+ _real_subprocess.run = lambda args, **kw: (calls.append(args), _R())[1]
61
+ return calls
62
+
63
+
64
+ def _get_srv():
65
+ import meshcode.meshcode_mcp.server as srv
66
+ return srv
67
+
68
+
69
+ def test_urgent_fires_escape_marker_enter_in_order():
70
+ srv = _get_srv()
71
+ calls = _arrange(srv, pane="%7")
72
+ srv._try_auto_wake(from_agent="mesh-commander", preview="urgent test", urgent=True)
73
+
74
+ assert any(c[:4] == ["tmux", "send-keys", "-t", "%7"] and c[-1] == "Escape"
75
+ for c in calls), f"no Escape: {calls}"
76
+ assert any("-l" in c and any("URGENT" in str(x) for x in c)
77
+ for c in calls), f"no literal URGENT marker: {calls}"
78
+ assert any(c[-1] == "Enter" for c in calls), f"no Enter submit: {calls}"
79
+ # ordering: Escape < marker < Enter
80
+ order = [c[-1] if c[-1] in ("Escape", "Enter") else ("MARKER" if "-l" in c else c[-1])
81
+ for c in calls]
82
+ assert order.index("Escape") < order.index("MARKER") < order.index("Enter"), order
83
+ # tmux path must return before any AppleScript fallback runs
84
+ assert not any("osascript" in str(c) for c in calls), f"fallback fired: {calls}"
85
+
86
+
87
+ def test_urgent_overrides_busy_state():
88
+ """A 'working' (busy) agent must still be interrupted by urgent."""
89
+ srv = _get_srv()
90
+ calls = _arrange(srv, state="working", pane="%7")
91
+ srv._try_auto_wake(from_agent="mesh-commander", preview="x", urgent=True)
92
+ assert calls, "urgent should fire even when the agent is working"
93
+
94
+
95
+ def test_non_urgent_busy_agent_does_not_fire():
96
+ """Non-urgent + not sleeping → state guard blocks (no keystroke injection)."""
97
+ srv = _get_srv()
98
+ calls = _arrange(srv, state="working", pane="%7")
99
+ srv._try_auto_wake(from_agent="mesh-commander", preview="x", urgent=False)
100
+ assert calls == [], f"non-urgent busy agent should not fire: {calls}"
101
+
102
+
103
+ def test_self_echo_guard():
104
+ """Urgent from yourself must never inject into your own terminal."""
105
+ srv = _get_srv()
106
+ calls = _arrange(srv, agent="backend", pane="%7")
107
+ srv._try_auto_wake(from_agent="backend", preview="self", urgent=True)
108
+ assert calls == [], f"self-echo should not fire: {calls}"
109
+
110
+
111
+ def test_gate_blocks_when_urgent_wake_disabled():
112
+ """No MESHCODE_URGENT_WAKE → urgent must not fire (opt-in respected)."""
113
+ srv = _get_srv()
114
+ calls = _arrange(srv, urgent_wake=False, auto_wake=False, pane="%7")
115
+ srv._try_auto_wake(from_agent="mesh-commander", preview="x", urgent=True)
116
+ assert calls == [], f"urgent fired with URGENT_WAKE off: {calls}"
117
+
118
+
119
+ def test_skips_while_in_wait():
120
+ """_IN_WAIT means the wait loop will deliver — never inject keystrokes."""
121
+ srv = _get_srv()
122
+ calls = _arrange(srv, in_wait=True, pane="%7")
123
+ srv._try_auto_wake(from_agent="mesh-commander", preview="x", urgent=True)
124
+ assert calls == [], f"fired during meshcode_wait: {calls}"
125
+
126
+
127
+ def test_setup_clients_bakes_urgent_wake_only():
128
+ """setup_clients env dict opts into urgent-only, never full auto-wake."""
129
+ import meshcode.setup_clients as sc
130
+ src = inspect.getsource(sc._build_server_block)
131
+ assert '"MESHCODE_URGENT_WAKE": "1"' in src, "URGENT_WAKE flag missing"
132
+ assert '"MESHCODE_AUTO_WAKE"' not in src, "AUTO_WAKE must stay unset (urgent-only)"
133
+
134
+
135
+ if __name__ == "__main__":
136
+ import pytest
137
+ sys.exit(pytest.main([__file__, "-q"]))
File without changes
File without changes
File without changes
File without changes