meshcode 2.11.155__tar.gz → 2.11.158__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 (122) hide show
  1. {meshcode-2.11.155 → meshcode-2.11.158}/PKG-INFO +1 -1
  2. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/hostd.py +38 -48
  4. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/server.py +52 -0
  5. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/protocol_handler.py +83 -4
  6. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/self_update.py +10 -0
  7. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/setup_clients.py +8 -0
  8. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode.egg-info/PKG-INFO +1 -1
  9. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode.egg-info/SOURCES.txt +4 -0
  10. {meshcode-2.11.155 → meshcode-2.11.158}/pyproject.toml +1 -1
  11. meshcode-2.11.158/tests/test_ensure_boot_env_urgent_wake.py +95 -0
  12. meshcode-2.11.158/tests/test_fleet_reaper.py +176 -0
  13. meshcode-2.11.158/tests/test_no_appleevents_on_sweep.py +81 -0
  14. meshcode-2.11.158/tests/test_urgent_wake_tmux.py +137 -0
  15. {meshcode-2.11.155 → meshcode-2.11.158}/README.md +0 -0
  16. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/__main__.py +0 -0
  17. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/_launch_smoke.py +0 -0
  18. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/_session_handoff_template.py +0 -0
  19. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/_stop_hook_template.py +0 -0
  20. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/_update_guard.py +0 -0
  21. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/ascii_art.py +0 -0
  22. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/atomic_push.py +0 -0
  23. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/claude_update.py +0 -0
  24. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/cli.py +0 -0
  25. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/comms_v4.py +0 -0
  26. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/compat.py +0 -0
  27. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/daemon.py +0 -0
  28. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/date_parse.py +0 -0
  29. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/doctor.py +0 -0
  30. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/error_hints.py +0 -0
  31. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/exceptions.py +0 -0
  32. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/helper_visuals.py +0 -0
  33. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/hooks/__init__.py +0 -0
  34. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/hooks/push_guard.py +0 -0
  35. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/hooks/repo_path_lock.py +0 -0
  36. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/invites.py +0 -0
  37. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/launcher.py +0 -0
  38. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/launcher_install.py +0 -0
  39. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/__init__.py +0 -0
  40. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/__main__.py +0 -0
  41. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/backend.py +0 -0
  42. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/realtime.py +0 -0
  43. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  44. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/swarm.py +0 -0
  45. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/test_backend.py +0 -0
  46. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  47. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  48. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  49. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  50. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  51. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/meshcode_mcp/test_swarm.py +0 -0
  52. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/preferences.py +0 -0
  53. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/protocol_v2.py +0 -0
  54. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/quickstart.py +0 -0
  55. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/rpc_allowlist.py +0 -0
  56. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/run_agent.py +0 -0
  57. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/scripts/check_secrets.py +0 -0
  58. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/scripts/race_rate_harness.py +0 -0
  59. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/secrets.py +0 -0
  60. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/supervisor.py +0 -0
  61. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/up.py +0 -0
  62. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode/upload.py +0 -0
  63. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode.egg-info/dependency_links.txt +0 -0
  64. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode.egg-info/entry_points.txt +0 -0
  65. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode.egg-info/requires.txt +0 -0
  66. {meshcode-2.11.155 → meshcode-2.11.158}/meshcode.egg-info/top_level.txt +0 -0
  67. {meshcode-2.11.155 → meshcode-2.11.158}/setup.cfg +0 -0
  68. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_auto_update_hardening.py +0 -0
  69. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_autonomous_closegap_1.py +0 -0
  70. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_autonomous_closegap_2.py +0 -0
  71. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_autonomous_closegap_3.py +0 -0
  72. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_autonomous_prompt_inject.py +0 -0
  73. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_boot_bug_regression.py +0 -0
  74. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_color_truecolor.py +0 -0
  75. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_core.py +0 -0
  76. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_cross_agent_messaging.py +0 -0
  77. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_date_parse.py +0 -0
  78. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_doctor.py +0 -0
  79. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_epistemic_v1_python_sdk.py +0 -0
  80. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  81. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_esc_deaf_state.py +0 -0
  82. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_exceptions.py +0 -0
  83. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_file_upload.py +0 -0
  84. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_helper_visuals.py +0 -0
  85. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_hostd_launch_pinned_env.py +0 -0
  86. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_hostd_serve_discovery_split.py +0 -0
  87. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_hostd_zombie_sessions.py +0 -0
  88. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_init_device_code.py +0 -0
  89. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_install_guard.py +0 -0
  90. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_launch_smoke.py +0 -0
  91. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_lease_sigterm_release.py +0 -0
  92. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_live_mesh_guard.py +0 -0
  93. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_mark_read_batch.py +0 -0
  94. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_marketplace_ratings.py +0 -0
  95. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_migration_integrity.py +0 -0
  96. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_preflight_hb_gate.py +0 -0
  97. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_pretrust_claude.py +0 -0
  98. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_push_guard.py +0 -0
  99. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_realtime_event_freshness.py +0 -0
  100. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_replica_base_workspace_fallback.py +0 -0
  101. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_replica_boot_protocol_unconditional.py +0 -0
  102. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_rls_cross_tenant.py +0 -0
  103. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_rm_guard.py +0 -0
  104. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_rpc_grants.py +0 -0
  105. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_rpc_migrations.py +0 -0
  106. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_run_agent_dry_run.py +0 -0
  107. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_run_agent_no_server_import.py +0 -0
  108. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_security_regressions.py +0 -0
  109. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_self_update_user_site.py +0 -0
  110. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_sentinel.py +0 -0
  111. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_session_replay_gate.py +0 -0
  112. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_setup_path.py +0 -0
  113. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_sleep_signals.py +0 -0
  114. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_status_enum_coverage.py +0 -0
  115. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_stay_on_loop_hook.py +0 -0
  116. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_stop_ghost_terminal.py +0 -0
  117. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_swarm_events.py +0 -0
  118. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_task_progress.py +0 -0
  119. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_terminal_lifecycle.py +0 -0
  120. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_up_launch_cmd.py +0 -0
  121. {meshcode-2.11.155 → meshcode-2.11.158}/tests/test_update_guard.py +0 -0
  122. {meshcode-2.11.155 → meshcode-2.11.158}/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.158
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.158"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -1497,57 +1497,47 @@ def _terminal_window_marker(target: str) -> str:
1497
1497
 
1498
1498
 
1499
1499
  def _list_dead_terminal_windows(target: str) -> list:
1500
- """macOS: ids of Terminal windows that belonged to `target`'s launcher and
1501
- whose tab is NO LONGER BUSY — the '[Process completed]' orphans a rotation
1502
- leaves behind (task 70ac6d25). busy=false means no process runs in the tab,
1503
- so a live agent can never match; the marker filter keeps every non-meshcode
1504
- window (user shells, other apps' tabs) out of reach. The marker is built
1505
- from a sanitized charset ([A-Za-z0-9_.-]) so it can't inject AppleScript.
1506
- Returns [] on non-macOS, no Automation permission, or any error."""
1507
- if sys.platform != "darwin":
1508
- return []
1509
- marker = _terminal_window_marker(target)
1510
- if not marker:
1511
- return []
1512
- script = (
1513
- 'set out to ""\n'
1514
- 'tell application "Terminal"\n'
1515
- ' repeat with w in (every window)\n'
1516
- ' try\n'
1517
- f' if (busy of selected tab of w) is false and (name of w contains "{marker}") then\n'
1518
- ' set out to out & (id of w) & linefeed\n'
1519
- ' end if\n'
1520
- ' end try\n'
1521
- ' end repeat\n'
1522
- 'end tell\n'
1523
- 'return out')
1524
- try:
1525
- out = subprocess.run(["/usr/bin/osascript", "-e", script],
1526
- capture_output=True, text=True, timeout=10).stdout or ""
1527
- return [int(x) for x in out.split() if x.strip().isdigit()]
1528
- except Exception:
1529
- return []
1500
+ """DISABLED no-op (macOS TCC fix, task e0465408 / Prong 1 #2).
1501
+
1502
+ This used to enumerate Terminal windows via `osascript -e 'tell application
1503
+ "Terminal" ...'` to find '[Process completed]' rotation orphans (task
1504
+ 70ac6d25 / 2.11.157). That Apple Event fired from python3.11 on the hostd
1505
+ lifecycle sweep and was RC-1 of the recurring macOS consent prompt
1506
+ "python3.11 would like to access data from other apps"
1507
+ (kTCCServiceAppleEvents). Because the interpreter is ad-hoc signed (bare
1508
+ cdhash, no Designated Requirement) the grant never stuck across versions, so
1509
+ it re-prompted on every sweep. The `.command` launcher already self-closes
1510
+ its OWN tab on any exit (clean / recycle / crash) via a tty-targeted close
1511
+ run from the agent's shell (see protocol_handler._write_fleet_native_agent),
1512
+ so this hostd-side reaper was a redundant belt. Removing it takes Apple
1513
+ Events off the python sweep path with no dead-tab regression — only
1514
+ un-trappable SIGKILL orphans may linger, which are rare and cannot be closed
1515
+ without Apple Events anyway. Kept as a no-op so the call sites stay harmless.
1516
+ NEVER reintroduce osascript here — guarded by
1517
+ tests/test_no_appleevents_on_sweep.py."""
1518
+ return []
1530
1519
 
1531
1520
 
1532
1521
  def _close_dead_terminal_windows(target: str, window_ids) -> int:
1533
- """Close (saving no) a pre-collected set of dead launcher windows. The ids
1534
- come from _list_dead_terminal_windows BEFORE the fresh spawn, so the new
1535
- window is excluded by construction (same snapshot discipline as
1536
- _close_old_visible_recycle). 'every window whose id is N' no-ops harmlessly
1537
- if the user already closed it. Best-effort; returns count closed."""
1538
- n = 0
1539
- for wid in window_ids:
1540
- try:
1541
- r = subprocess.run(
1542
- ["/usr/bin/osascript", "-e",
1543
- f'tell application "Terminal" to close (every window whose id is {int(wid)}) saving no'],
1544
- capture_output=True, text=True, timeout=10)
1545
- if r.returncode == 0:
1546
- _log(f"DEAD-WINDOW-REAP {target}: closed dead terminal window id {wid} (rotation orphan)")
1547
- n += 1
1548
- except Exception:
1549
- pass
1550
- return n
1522
+ """DISABLED no-op (macOS TCC fix, task e0465408 / Prong 1 #2).
1523
+
1524
+ Previously closed dead launcher windows by firing
1525
+ `osascript -e 'tell application "Terminal" to close ...'` from python3.11.
1526
+ That Apple Event was RC-1 of the recurring TCC prompt
1527
+ ("python3.11 would like to access data from other apps") — the adhoc python
1528
+ interpreter has no stable Designated Requirement, so the kTCCServiceAppleEvents
1529
+ grant never persists across per-version cdhash changes and re-prompts forever.
1530
+
1531
+ The `.command` launcher already self-closes its OWN tab on any exit
1532
+ (clean/recycle/crash) via a tty-targeted close run from the agent's SHELL
1533
+ (not python — different TCC attribution), so this hostd-side reaper was a
1534
+ redundant belt. Removing it takes Apple Events off the python sweep path with
1535
+ no dead-tab regression only un-trappable SIGKILL orphans may linger, which
1536
+ are rare and cannot be closed without Apple Events anyway. Kept as a no-op so
1537
+ the call sites stay harmless.
1538
+ NEVER reintroduce osascript here — guarded by
1539
+ tests/test_no_appleevents_on_sweep.py."""
1540
+ return 0
1551
1541
 
1552
1542
 
1553
1543
  def _spawn_rate_ok(target: str):
@@ -491,9 +491,61 @@ 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":
531
+ # macOS TCC fix (task e0465408 / Prong 1 #3): the AppleScript
532
+ # keystroke fallback fires osascript from python3.11, which triggers
533
+ # the recurring "python3.11 would like to access data from other apps"
534
+ # consent prompt (kTCCServiceAppleEvents; adhoc python has no stable
535
+ # Designated Requirement so the grant never persists). tmux send-keys
536
+ # (the fast-path above) is the durable, prompt-free wake mechanism.
537
+ # The AppleScript fallback is OFF by default and only re-enabled by
538
+ # operators who explicitly opt into Apple Events:
539
+ # MESHCODE_AUTOWAKE_APPLESCRIPT=1
540
+ # When off and tmux was unavailable/failed, we skip keystroke
541
+ # injection entirely (no prompt, no wake) rather than re-prompt Samuel.
542
+ if os.environ.get("MESHCODE_AUTOWAKE_APPLESCRIPT", "").strip().lower() \
543
+ not in ("1", "true", "yes", "on"):
544
+ log.debug(
545
+ "auto-wake: tmux unavailable and AppleScript fallback "
546
+ "disabled (MESHCODE_AUTOWAKE_APPLESCRIPT off) — skipping "
547
+ "keystroke injection to avoid macOS TCC prompt")
548
+ return
497
549
  # Sanitize app_name — TERM_PROGRAM is an env var, not DB-sourced,
498
550
  # but defense-in-depth: only allow known terminal app names.
499
551
  parent_app = os.environ.get("TERM_PROGRAM", "Terminal")
@@ -356,8 +356,12 @@ def _fleet_attach_macos(tmux: str) -> tuple[bool, str]:
356
356
  ]
357
357
  try:
358
358
  launch_dir.mkdir(parents=True, exist_ok=True)
359
+ # macOS TCC fix (task e0465408) security guardrail: launcher dir +
360
+ # .command owner-only (0o700). These scripts are LaunchServices-executed
361
+ # via `open -a` and must never be world-readable/-writable.
362
+ _harden_launcher_dir(launch_dir)
359
363
  script_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
360
- os.chmod(script_path, 0o755)
364
+ os.chmod(script_path, 0o700)
361
365
  except Exception as e:
362
366
  return False, f"could not write fleet launcher: {e}"
363
367
  r = subprocess.run(["open", "-a", "Terminal", str(script_path)],
@@ -373,6 +377,60 @@ def _fleet_attach_linux(tmux: str) -> tuple[bool, str]:
373
377
  return _spawn_terminal_linux(attach)
374
378
 
375
379
 
380
+ def _reap_dead_fleet_windows(tmux: str, cap: Optional[int] = None) -> int:
381
+ """Reap accumulated DEAD windows from the fleet tmux session (task 22fcdb21,
382
+ Samuel P1: dead panes stacked to 51).
383
+
384
+ The RAII trap in _fleet_wrap closes a window on a CLEAN / SIGTERM exit, but
385
+ a SIGKILL (rc=137: force-kill / OOM / recycle) cannot be trapped, so the
386
+ per-window `remain-on-exit=failed` keeps that dead pane forever. Over days
387
+ these pile up and `#{session_windows}` (the 'N agents' counter) counts
388
+ cadavers, not live agents.
389
+
390
+ Sweeps every fleet window whose panes are ALL dead, KEEPING only the `cap`
391
+ most-recently-dead (bounded crash-retention for debugging) and killing the
392
+ rest. It NEVER touches a window with a live pane, so it can never close a
393
+ running agent. Returns the count reaped. Best-effort; never raises. Tunable
394
+ via MESHCODE_FLEET_DEAD_CAP (default 3; 0 = reap every dead pane)."""
395
+ if cap is None:
396
+ try:
397
+ cap = int(os.environ.get("MESHCODE_FLEET_DEAD_CAP", "3") or 3)
398
+ except ValueError:
399
+ cap = 3
400
+ cap = max(0, cap)
401
+ try:
402
+ r = _tmux(tmux, "list-panes", "-s", "-t", f"={_FLEET_SESSION}",
403
+ "-F", "#{window_id}\t#{pane_dead}\t#{pane_dead_time}")
404
+ if r.returncode != 0:
405
+ return 0
406
+ # Group panes by window: a window is dead only if EVERY pane is dead
407
+ # (fleet windows are single-pane, but stay correct if that changes).
408
+ wins: dict = {}
409
+ for line in (r.stdout or "").splitlines():
410
+ parts = line.split("\t")
411
+ if len(parts) < 2:
412
+ continue
413
+ wid, pdead = parts[0], parts[1]
414
+ try:
415
+ pdt = int(parts[2]) if len(parts) > 2 and parts[2] else 0
416
+ except ValueError:
417
+ pdt = 0
418
+ w = wins.setdefault(wid, {"all_dead": True, "dt": 0})
419
+ if pdead != "1":
420
+ w["all_dead"] = False
421
+ else:
422
+ w["dt"] = max(w["dt"], pdt)
423
+ dead = sorted(((v["dt"], wid) for wid, v in wins.items() if v["all_dead"]),
424
+ reverse=True) # newest dead first; keep[:cap], reap the tail
425
+ reaped = 0
426
+ for _dt, wid in dead[cap:]:
427
+ if _tmux(tmux, "kill-window", "-t", wid).returncode == 0:
428
+ reaped += 1
429
+ return reaped
430
+ except Exception:
431
+ return 0
432
+
433
+
376
434
  def _spawn_fleet_tab(cmd: str) -> tuple[bool, str]:
377
435
  """Run `cmd` as a TAB (tmux window) of the shared fleet window.
378
436
 
@@ -399,6 +457,11 @@ def _spawn_fleet_tab(cmd: str) -> tuple[bool, str]:
399
457
  if r.returncode != 0: # lost a create race -> join as a tab
400
458
  fresh_session = False
401
459
  if not fresh_session:
460
+ # Sweep-on-launch: reap accumulated dead panes BEFORE adding this
461
+ # agent's tab, so the fleet bar reflects live agents (not the 51
462
+ # cadavers Samuel saw) and a respawn reuses the slot (task 22fcdb21).
463
+ # Only ever touches already-dead windows — never a live agent.
464
+ _reap_dead_fleet_windows(tmux)
402
465
  r = _tmux(tmux, "new-window", "-d", "-t", f"={_FLEET_SESSION}:",
403
466
  "-n", label, "-P", "-F", "#{window_id}", wrapped)
404
467
  if r.returncode != 0:
@@ -501,6 +564,19 @@ def _spawn_fleet_tab(cmd: str) -> tuple[bool, str]:
501
564
  _FLEET_NATIVE_DISABLED_TTL_S = 1800 # back off after a watcher "fallback"
502
565
 
503
566
 
567
+ def _harden_launcher_dir(d) -> None:
568
+ """macOS TCC fix (task e0465408) security guardrail: keep the launcher dir
569
+ owner-only (0o700). The `.command`/watcher scripts here are executed by
570
+ LaunchServices via `open -a` and may carry non-secret launch flags; they must
571
+ never be world-readable/-writable (was 0o755). Secrets are NEVER written into
572
+ these scripts (they come via .mcp.json / keychain / env-file). Best-effort:
573
+ a chmod failure must not wedge a launch."""
574
+ try:
575
+ os.chmod(str(d), 0o700)
576
+ except Exception:
577
+ pass
578
+
579
+
504
580
  def _fleet_native_paths() -> dict:
505
581
  d = Path.home() / ".meshcode" / "launchers"
506
582
  return {
@@ -654,10 +730,11 @@ while :; do
654
730
  done
655
731
  '''
656
732
  p["dir"].mkdir(parents=True, exist_ok=True)
733
+ _harden_launcher_dir(p["dir"]) # owner-only launcher dir (TCC fix e0465408)
657
734
  driver = p["dir"] / "fleet-native-driver.applescript"
658
735
  driver.write_text(_FLEET_NATIVE_DRIVER_AS + "\n", encoding="utf-8")
659
736
  p["watcher"].write_text(body, encoding="utf-8")
660
- os.chmod(p["watcher"], 0o755)
737
+ os.chmod(p["watcher"], 0o700)
661
738
  return p["watcher"]
662
739
 
663
740
 
@@ -737,8 +814,9 @@ def _write_fleet_native_agent(cmd: str) -> Path:
737
814
  'esac',
738
815
  ]
739
816
  p["dir"].mkdir(parents=True, exist_ok=True)
817
+ _harden_launcher_dir(p["dir"]) # owner-only launcher dir (TCC fix e0465408)
740
818
  script_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
741
- os.chmod(script_path, 0o755)
819
+ os.chmod(script_path, 0o700)
742
820
  return script_path
743
821
 
744
822
 
@@ -963,8 +1041,9 @@ def _spawn_terminal_macos(cmd: str) -> tuple[bool, str]:
963
1041
  lines.append('fi')
964
1042
  try:
965
1043
  launch_dir.mkdir(parents=True, exist_ok=True)
1044
+ _harden_launcher_dir(launch_dir) # owner-only launcher dir (TCC fix e0465408)
966
1045
  script_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
967
- os.chmod(script_path, 0o755)
1046
+ os.chmod(script_path, 0o700)
968
1047
  except Exception as e:
969
1048
  return False, f"could not write launcher {script_path}: {e}"
970
1049
  # Clear any stale start-marker so the poll below can only see a FRESH one
@@ -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.158
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
@@ -87,6 +89,7 @@ tests/test_live_mesh_guard.py
87
89
  tests/test_mark_read_batch.py
88
90
  tests/test_marketplace_ratings.py
89
91
  tests/test_migration_integrity.py
92
+ tests/test_no_appleevents_on_sweep.py
90
93
  tests/test_preflight_hb_gate.py
91
94
  tests/test_pretrust_claude.py
92
95
  tests/test_push_guard.py
@@ -113,4 +116,5 @@ tests/test_task_progress.py
113
116
  tests/test_terminal_lifecycle.py
114
117
  tests/test_up_launch_cmd.py
115
118
  tests/test_update_guard.py
119
+ tests/test_urgent_wake_tmux.py
116
120
  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.158"
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"]))