meshcode 2.11.111__tar.gz → 2.11.112__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 (101) hide show
  1. {meshcode-2.11.111 → meshcode-2.11.112}/PKG-INFO +1 -1
  2. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/comms_v4.py +12 -0
  4. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/hostd.py +59 -130
  5. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/protocol_handler.py +68 -35
  6. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/run_agent.py +4 -15
  7. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/self_update.py +53 -1
  8. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode.egg-info/PKG-INFO +1 -1
  9. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode.egg-info/SOURCES.txt +0 -8
  10. {meshcode-2.11.111 → meshcode-2.11.112}/pyproject.toml +1 -1
  11. meshcode-2.11.111/meshcode/_session_handoff_template 2.py +0 -296
  12. meshcode-2.11.111/meshcode/_session_handoff_template 3.py +0 -296
  13. meshcode-2.11.111/meshcode/claude_update 2.py +0 -258
  14. meshcode-2.11.111/meshcode/claude_update 3.py +0 -258
  15. meshcode-2.11.111/meshcode/hostd 2.py +0 -1269
  16. meshcode-2.11.111/meshcode/up 2.py +0 -257
  17. meshcode-2.11.111/tests/test_autonomous_prompt_inject 2.py +0 -126
  18. meshcode-2.11.111/tests/test_autonomous_prompt_inject 3.py +0 -126
  19. {meshcode-2.11.111 → meshcode-2.11.112}/README.md +0 -0
  20. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/__main__.py +0 -0
  21. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/_session_handoff_template.py +0 -0
  22. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/_stop_hook_template.py +0 -0
  23. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/ascii_art.py +0 -0
  24. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/atomic_push.py +0 -0
  25. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/claude_update.py +0 -0
  26. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/cli.py +0 -0
  27. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/compat.py +0 -0
  28. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/daemon.py +0 -0
  29. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/date_parse.py +0 -0
  30. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/doctor.py +0 -0
  31. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/error_hints.py +0 -0
  32. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/exceptions.py +0 -0
  33. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/hooks/__init__.py +0 -0
  34. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/hooks/repo_path_lock.py +0 -0
  35. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/invites.py +0 -0
  36. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/launcher.py +0 -0
  37. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/launcher_install.py +0 -0
  38. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/meshcode_mcp/__init__.py +0 -0
  39. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/meshcode_mcp/__main__.py +0 -0
  40. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/meshcode_mcp/backend.py +0 -0
  41. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/meshcode_mcp/realtime.py +0 -0
  42. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/meshcode_mcp/server.py +0 -0
  43. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  44. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/meshcode_mcp/test_backend.py +0 -0
  45. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  46. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  47. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  48. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  49. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  50. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/preferences.py +0 -0
  51. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/protocol_v2.py +0 -0
  52. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/quickstart.py +0 -0
  53. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/rpc_allowlist.py +0 -0
  54. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/scripts/check_secrets.py +0 -0
  55. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/scripts/race_rate_harness.py +0 -0
  56. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/secrets.py +0 -0
  57. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/setup_clients.py +0 -0
  58. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/supervisor.py +0 -0
  59. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/up.py +0 -0
  60. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode/upload.py +0 -0
  61. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode.egg-info/dependency_links.txt +0 -0
  62. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode.egg-info/entry_points.txt +0 -0
  63. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode.egg-info/requires.txt +0 -0
  64. {meshcode-2.11.111 → meshcode-2.11.112}/meshcode.egg-info/top_level.txt +0 -0
  65. {meshcode-2.11.111 → meshcode-2.11.112}/setup.cfg +0 -0
  66. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_auto_update_hardening.py +0 -0
  67. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_autonomous_closegap_1.py +0 -0
  68. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_autonomous_closegap_2.py +0 -0
  69. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_autonomous_closegap_3.py +0 -0
  70. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_autonomous_prompt_inject.py +0 -0
  71. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_boot_bug_regression.py +0 -0
  72. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_color_truecolor.py +0 -0
  73. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_core.py +0 -0
  74. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_cross_agent_messaging.py +0 -0
  75. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_date_parse.py +0 -0
  76. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_doctor.py +0 -0
  77. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_epistemic_v1_python_sdk.py +0 -0
  78. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  79. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_esc_deaf_state.py +0 -0
  80. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_exceptions.py +0 -0
  81. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_file_upload.py +0 -0
  82. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_init_device_code.py +0 -0
  83. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_install_guard.py +0 -0
  84. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_lease_sigterm_release.py +0 -0
  85. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_mark_read_batch.py +0 -0
  86. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_marketplace_ratings.py +0 -0
  87. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_migration_integrity.py +0 -0
  88. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_realtime_event_freshness.py +0 -0
  89. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_rls_cross_tenant.py +0 -0
  90. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_rpc_grants.py +0 -0
  91. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_rpc_migrations.py +0 -0
  92. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_run_agent_dry_run.py +0 -0
  93. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_run_agent_no_server_import.py +0 -0
  94. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_security_regressions.py +0 -0
  95. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_self_update_user_site.py +0 -0
  96. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_sentinel.py +0 -0
  97. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_setup_path.py +0 -0
  98. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_sleep_signals.py +0 -0
  99. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_status_enum_coverage.py +0 -0
  100. {meshcode-2.11.111 → meshcode-2.11.112}/tests/test_stay_on_loop_hook.py +0 -0
  101. {meshcode-2.11.111 → meshcode-2.11.112}/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.111
3
+ Version: 2.11.112
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.111"
2
+ __version__ = "2.11.112"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -3816,6 +3816,18 @@ if __name__ == "__main__":
3816
3816
  print(f" Already up to date ({cur}).")
3817
3817
  sys.exit(0)
3818
3818
  print(f" Latest version: {latest}")
3819
+ # 8e4f93f9 (stability): `meshcode upgrade` is the FORCE escape hatch — it
3820
+ # overwrites the env inline. Warn if live agents share it (the overwrite can
3821
+ # drop their MCP sessions). Auto-update already DEFERS while agents are live.
3822
+ try:
3823
+ _live = _su._live_mcp_serves_excluding_self()
3824
+ except Exception:
3825
+ _live = 0
3826
+ if _live:
3827
+ print()
3828
+ print(f" WARNING: {_live} live agent serve(s) share this env. Upgrading OVERWRITES")
3829
+ print(f" it while they run and may drop their MCP sessions (8e4f93f9). Close those")
3830
+ print(f" agents first for a clean upgrade, or confirm below to force anyway.")
3819
3831
  print()
3820
3832
  try:
3821
3833
  confirm = input(f" Upgrade to {latest}? [Y/n] ").strip().lower()
@@ -358,105 +358,26 @@ def _validate_repo_path(rp) -> Optional[str]:
358
358
  return p
359
359
 
360
360
 
361
- def _spawn_agent(project: str, agent: str, headless: bool = False, repo_path=None) -> bool:
362
- """Relaunch `meshcode run <project>/<agent>`.
361
+ def _spawn_agent(project: str, agent: str, repo_path=None) -> bool:
362
+ """Relaunch `meshcode run <project>/<agent>` in a VISIBLE terminal window.
363
363
 
364
- headless=False (default): VISIBLE terminal window. Samuel must SEE it — we
365
- delegate to protocol_handler._spawn_terminal, the canonical visible spawner
366
- (macOS: osascript-if-Automation-granted else `open` a .command wrapper, no TCC
367
- needed; Linux/Windows: native terminals). On failure we WARN loudly and return
368
- False we NEVER silently fall back to headless when visible was asked.
364
+ Samuel must SEE it — we delegate to protocol_handler._spawn_terminal, the
365
+ canonical visible spawner (macOS: osascript-if-Automation-granted else `open`
366
+ a .command wrapper, no TCC needed; Linux/Windows: native terminals). On failure
367
+ we WARN loudly and return False we NEVER silently fall back to a background
368
+ process when a window was asked for.
369
369
 
370
- headless=True (Fleet Control mig404 per-agent flag): background process, NO
371
- window for fleet agents that don't need a terminal (like the qa launch).
370
+ HEADLESS REMOVED (task cba01de5, Ian 2026-06-05): the background/no-window spawn
371
+ mode caused too many lifecycle problems and is gone agents now ALWAYS spawn
372
+ visible. Stop/recycle still enforce desired_state via the cmdline-discovery kill
373
+ sweeps below (a visible agent has a window the sweeps locate by command line).
372
374
 
373
375
  repo_path (task 24e3dd44 #4): optional per-agent repo. When set + valid, `--repo <path>` is
374
376
  appended so `meshcode run` opens there. NULL/invalid = unchanged. Inert until mc_agents.repo_path
375
377
  + the respawn RPC carry it.
376
378
  """
377
379
  target = f"{project}/{agent}"
378
- bin_ = _meshcode_bin()
379
380
  repo = _validate_repo_path(repo_path)
380
- if headless:
381
- # background, NO terminal — UNIVERSAL macOS/Linux/Windows (task c1a6c6a8, mesh-dev specs).
382
- # Clean env (a stale CLAUDECODE aborts `meshcode run`); keep crash logs in a per-agent logfile
383
- # (NOT DEVNULL — we want to debug headless agents that die on boot).
384
- # task 14782bb4: spawned agents DO auto-update (non-blocking) — strip any inherited
385
- # NO_*UPDATE so the agent's launch runs the bg updater (the daemon launcher stays disabled).
386
- # No hang: run_agent now uses the NON-blocking updater + we spawn via `python -m meshcode`
387
- # (not the .exe shim) so a bg `pip install -U` can replace meshcode.exe on Windows.
388
- env = {k: v for k, v in os.environ.items()
389
- if k not in ("CLAUDECODE", "CLAUDE_CODE_SESSION",
390
- "MESHCODE_NO_UPDATE", "MESHCODE_NO_AUTO_UPDATE")}
391
- # LAUNCH-NO-WINDOW (task 35bee961): tell run_agent it's headless so it spawns
392
- # the Claude Code child with CREATE_NO_WINDOW. Without this, our DETACHED_PROCESS
393
- # python parent (no console) spawns `cmd.exe /c claude.cmd` and Windows ALLOCATES
394
- # A FRESH CONSOLE WINDOW for the child -> a terminal pops up despite "headless".
395
- env["MESHCODE_HEADLESS"] = "1"
396
- log_dir = STATE_DIR / "logs"
397
- try:
398
- log_dir.mkdir(parents=True, exist_ok=True)
399
- except Exception:
400
- pass
401
- safe = f"{project}__{agent}".replace("/", "_").replace("\\", "_")
402
- log_path = log_dir / f"{safe}.headless.log"
403
- logf = None
404
- try:
405
- logf = open(log_path, "ab")
406
- except Exception:
407
- logf = None
408
- kwargs = {
409
- "stdin": subprocess.DEVNULL,
410
- "stdout": (logf if logf is not None else subprocess.DEVNULL),
411
- "stderr": subprocess.STDOUT,
412
- "env": env,
413
- }
414
- if sys.platform == "win32":
415
- # CREATE_NO_WINDOW (0x08000000) | DETACHED_PROCESS (0x08): no console window, fully
416
- # detached from this daemon. NEVER pythonw (swallows stderr). Ensure venv Scripts +
417
- # System32 on PATH (Windows PATH cap ~32k).
418
- kwargs["creationflags"] = 0x08000000 | 0x00000008
419
- try:
420
- scripts = str(Path(sys.executable).resolve().parent) # venv Scripts dir
421
- except Exception:
422
- scripts = ""
423
- sysroot = os.environ.get("SystemRoot", r"C:\Windows")
424
- base_path = os.pathsep.join(p for p in (scripts, sysroot + r"\System32", sysroot) if p)
425
- env["PATH"] = (base_path + os.pathsep + env.get("PATH", ""))[:30000]
426
- else:
427
- # POSIX: detach into its own session so it survives the daemon + has no controlling tty.
428
- kwargs["start_new_session"] = True
429
- # RELIABILITY (commander field finding): a headless respawn died with "meshcode not on
430
- # PATH" — the spawned `meshcode run` (and its `meshcode mcp` MCP child) needs the
431
- # `meshcode` console script findable. Prepend sys.executable's bin dir (where the
432
- # console script lives) to PATH, mirroring the win32 venv-Scripts injection above.
433
- try:
434
- bindir = str(Path(sys.executable).resolve().parent)
435
- if bindir and bindir not in env.get("PATH", "").split(os.pathsep):
436
- env["PATH"] = bindir + os.pathsep + env.get("PATH", "")
437
- except Exception:
438
- pass
439
- try:
440
- # `python -m meshcode` (NOT the meshcode.exe shim) so the .exe isn't held open by the
441
- # agent -> a background `pip install -U` can replace it on Windows (task 14782bb4 #4).
442
- argv = [sys.executable, "-m", "meshcode", "run", target] + (["--repo", repo] if repo else [])
443
- proc = subprocess.Popen(argv, **kwargs)
444
- # LAUNCH-NO-WINDOW (task 35bee961): record the headless PID so the Stop sweep
445
- # (_do_stops) can hard-kill it — a headless agent has NO window for Samuel to
446
- # close, so desired_state='stopped' must be enforced by killing the process.
447
- _record_headless_pid(target, proc.pid)
448
- _log(f"spawned {target} HEADLESS (no window, {sys.platform}; pid={proc.pid}; log={log_path})")
449
- return True
450
- except Exception as e:
451
- _log(f"WARN: headless spawn {target} failed: {e}")
452
- return False
453
- finally:
454
- # child inherited its own fd; safe to drop the daemon's handle.
455
- if logf is not None:
456
- try:
457
- logf.close()
458
- except Exception:
459
- pass
460
381
  # env hygiene (item1 RC): a stale CLAUDECODE aborts `meshcode run` (exit2). Build the terminal
461
382
  # command PER-PLATFORM (mesh-core FIX1, 0x80070002): cmd.exe uses & (NOT ';'), set "V=" to clear
462
383
  # (NOT bash unset/export), double-quotes (NOT shlex POSIX), no exec — else Windows Terminal splits
@@ -527,29 +448,37 @@ def _do_respawns(api_key: str, host_id: str) -> int:
527
448
  # do NOT re-record here (that would inflate the count on a mere rate-limit skip).
528
449
  _log(f"SKIP respawn {proj}/{agent}: not allowed (count={c.get('respawn_count')}, rate-limited/at-cap)")
529
450
  continue
530
- # BOOT-AUTOSTART GATE (task b6da0d54): don't auto-launch an agent whose last heartbeat
531
- # PREDATES this hostd start (or never heartbeated) — that's a boot-stale 'running' leftover
532
- # from a prior session, NOT a live-then-crashed agent. Skip it (explicit launch required) so
533
- # rebooting / hostd-restart does NOT pop terminals. An agent alive AFTER startup that died =
534
- # crash -> falls through to respawn (crash-recovery preserved). MESHCODE_BOOT_AUTOSTART=1 opts out.
451
+ # BOOT-AUTOSTART GATE (task b6da0d54; signal fixed in task 8067e04c): don't auto-launch
452
+ # an agent whose desired_state='running' was set BEFORE this hostd started — that's a
453
+ # boot-stale 'running' leftover from a prior session (the "terminals pop at boot" bug),
454
+ # NOT an explicit launch. Skip it so rebooting / hostd-restart does NOT pop terminals.
455
+ #
456
+ # We key on spawned_at — stamped on EVERY desired_state->running transition by the trigger
457
+ # _mc_stamp_spawned_at (mig424) — surfaced here as an AGE (spawned_age_s, mig438). That is
458
+ # the EXPLICIT-LAUNCH signal: a Launch All / Start click stamps a fresh spawned_at. The
459
+ # previous heartbeat check was WRONG: a just-launched agent hasn't heartbeated yet
460
+ # (heartbeat_age_s=None), so it was skipped as a "leftover" and Launch All never spawned
461
+ # it (Windows regression, fis — task d7bbfecb). Comparing AGES (spawned_age_s vs hostd
462
+ # uptime), not timestamps, avoids DB/host clock skew.
463
+ # spawned_age_s is None or >= uptime -> running set before this hostd start -> SKIP (leftover).
464
+ # spawned_age_s < uptime -> explicit launch AFTER start -> SPAWN.
465
+ # Crash-respawn preserved: an agent launched THIS session then crashed has spawned_age_s <
466
+ # uptime -> respawned. MESHCODE_BOOT_AUTOSTART=1 opts out (auto-launch everything, old behavior).
535
467
  if not _BOOT_AUTOSTART:
536
- _hb_age = c.get("heartbeat_age_s")
468
+ _spawn_age = c.get("spawned_age_s")
537
469
  _hostd_uptime = time.time() - _HOSTD_STARTED_AT
538
- if _hb_age is None or _hb_age >= _hostd_uptime:
539
- _log(f"BOOT-AUTOSTART OFF: skip auto-respawn {proj}/{agent} (no heartbeat since hostd "
540
- f"start — boot-stale 'running' leftover; explicit launch required). "
470
+ if _spawn_age is None or _spawn_age >= _hostd_uptime:
471
+ _log(f"BOOT-AUTOSTART OFF: skip auto-respawn {proj}/{agent} (desired_state=running set "
472
+ f"before this hostd start — boot-stale leftover; explicit launch required). "
541
473
  f"Set MESHCODE_BOOT_AUTOSTART=1 to auto-launch at boot.")
542
474
  continue
543
475
  # RECYCLE FAST-PATH (task c0fc5597): a recycle-exited agent (recycle_fast) is relaunched
544
476
  # PROMPTLY (the RPC returned it at a 15s stale gate, not STALE_SECONDS) and recorded as a
545
477
  # RECYCLE (mc_record_recycle), NEVER against the crash respawn cap.
546
478
  _is_recycle = bool(c.get("recycle_fast"))
547
- # 798dd1bd: on-demand recycle with recycle_visible -> reopen a FRESH VISIBLE window
548
- # (Samuel's "terminals reopen fresh"); else PRESERVE the agent's headless state
549
- # (don't silently un-headless a headless fleet backend2 A2). One-shot: mc_record_recycle
550
- # clears recycle_visible so subsequent respawns use the normal headless flag.
551
- _visible = _is_recycle and bool(c.get("recycle_visible"))
552
- _hl = False if _visible else bool(c.get("headless"))
479
+ # HEADLESS REMOVED (task cba01de5): every spawn is now a VISIBLE window, so every
480
+ # recycle reopens a fresh terminal and must close the old one (handled below). The
481
+ # server-side recycle_visible / headless flags are ignored (visible is the only mode).
553
482
  # Circuit-breaker: bound spawns per target so a crash/recycle loop can
554
483
  # never storm the user's terminals (Samuel 2026-06-04). Applies to BOTH
555
484
  # paths — the recycle fast-path has no server-side cap, so this is its
@@ -614,16 +543,17 @@ def _do_respawns(api_key: str, host_id: str) -> int:
614
543
  except Exception:
615
544
  pass
616
545
  continue
617
- # Part 2 (Samuel req #2): for a VISIBLE recycle, snapshot the OLD window
618
- # pid(s) BEFORE spawning the fresh one — so the new pid is never in the
619
- # close set (commander q1: never touch the fresh terminal).
620
- _old_vis_pids = _discover_agent_pids(_target) if (_is_recycle and _visible) else []
546
+ # Part 2 (Samuel req #2): on a recycle, snapshot the OLD window pid(s) BEFORE
547
+ # spawning the fresh one — so the new pid is never in the close set (commander
548
+ # q1: never touch the fresh terminal). Every recycle is visible now (headless
549
+ # removed, task cba01de5), so this always runs for a recycle.
550
+ _old_vis_pids = _discover_agent_pids(_target) if _is_recycle else []
621
551
  _log(f"{'RECYCLE-RESPAWN' if _is_recycle else 'RESPAWN'} {proj}/{agent} "
622
- f"({'VISIBLE ' if _visible else ''}stale {c.get('heartbeat_age_s')}s, count={c.get('respawn_count')})")
623
- if _spawn_agent(proj, agent, headless=_hl, repo_path=c.get("repo_path")):
552
+ f"(stale {c.get('heartbeat_age_s')}s, count={c.get('respawn_count')})")
553
+ if _spawn_agent(proj, agent, repo_path=c.get("repo_path")):
624
554
  _record_spawn(_target) # count the terminal we just opened, against the breaker
625
555
  if _is_recycle:
626
- if _visible and _old_vis_pids:
556
+ if _old_vis_pids:
627
557
  _close_old_visible_recycle(_target, _old_vis_pids) # close old window (DRY-RUN first)
628
558
  _rpc("mc_record_recycle",
629
559
  {"p_api_key": api_key, "p_project_id": c.get("project_id"), "p_agent_name": agent})
@@ -830,24 +760,21 @@ def _record_spawn(target: str) -> None:
830
760
 
831
761
 
832
762
  # ------------------------------------------------------------------
833
- # Stop — hard-kill a HEADLESS agent (task 35bee961). A headless agent has no
834
- # window Samuel can close, so a Stop (desired_state='stopped') that the agent
835
- # doesn't honor cooperatively (must_exit) would otherwise run forever. We record
836
- # each headless spawn's PID (persisted across hostd restarts) and kill it when the
837
- # cloud says it should be stopped but it's still heartbeating.
763
+ # Stop — hard-kill an agent the cloud marked stopped that won't exit cooperatively
764
+ # (task 35bee961). A Stop (desired_state='stopped') the agent doesn't honor via
765
+ # must_exit would otherwise run forever, so the sweeps below DISCOVER the agent's
766
+ # process by command line (`meshcode run <target>`) and kill it.
767
+ #
768
+ # NOTE: headless (background, no-window) spawn was removed (task cba01de5, Ian
769
+ # 2026-06-05) — nothing records into the `headless_pids` map any more, so the kill
770
+ # sweeps now rely entirely on the cmdline-discovery fallback (which already handled
771
+ # visible agents). The `headless_pids` map + _kill_headless_pid() are retained as
772
+ # the generic recorded-PID kill path (also used by visible-recycle window close);
773
+ # they stay EMPTY until a future feature repopulates them. Renaming/removing the
774
+ # now-vestigial map belongs to the visible-lifecycle redesign (back-2), NOT here —
775
+ # ripping it out would break the Stop/force-kill sweeps for visible agents.
838
776
  # ------------------------------------------------------------------
839
777
 
840
- def _record_headless_pid(target: str, pid: int) -> None:
841
- try:
842
- st = _load_state()
843
- pids = st.get("headless_pids") or {}
844
- pids[target] = pid # overwrite any stale entry for this target (refresh on respawn)
845
- st["headless_pids"] = pids
846
- _save_state(st)
847
- except Exception:
848
- pass
849
-
850
-
851
778
  def _gc_headless_pids() -> None:
852
779
  """GC dead PIDs from headless_pids (cb90b058) so a stale entry can't mask a live agent +
853
780
  the recorded PID stays accurate for enforce. Cheap; runs once per sweep. Best-effort."""
@@ -1044,8 +971,10 @@ def _do_stops(api_key: str, host_id: str) -> int:
1044
971
  return n
1045
972
 
1046
973
 
1047
- # 38523a98 Gap 1: explicit human force-kill of VISIBLE agents. DRY-RUN first (same gate as the reaper).
1048
- _FORCE_KILL_DRYRUN = True
974
+ # 38523a98 Gap 1: explicit human force-kill of VISIBLE agents. ENABLED in 2.11.112 (task fa11ff48):
975
+ # Samuel-blessed + 0 false-positives verified (backend2 pre-check + empty owner-scoped queue on our box).
976
+ # _REAP_DRYRUN (autonomous reaper, below) stays True — SEPARATE gate, needs its own 0-FP + Samuel OK.
977
+ _FORCE_KILL_DRYRUN = False
1049
978
 
1050
979
 
1051
980
  def _do_force_kills(api_key: str, host_id: str) -> int:
@@ -153,49 +153,82 @@ def _detect_macos_terminal() -> str:
153
153
  return "terminal"
154
154
 
155
155
 
156
+ def _launcher_label(cmd: str) -> str:
157
+ """Derive a stable, filesystem-safe launcher name from the spawn `cmd`.
158
+
159
+ Pulls the `meshcode run <target>` argument (project/agent) when present so
160
+ the launcher is human-recognisable (~/.meshcode/launchers/<agent>.command);
161
+ falls back to a short hash of the cmd otherwise. Slashes/odd chars are
162
+ collapsed to `_` so the result is always a single safe path component.
163
+ """
164
+ m = re.search(r"meshcode\s+run\s+(?:'([^']*)'|\"([^\"]*)\"|(\S+))", cmd)
165
+ raw = ((m.group(1) or m.group(2) or m.group(3)) if m else "") or ""
166
+ safe = re.sub(r"[^A-Za-z0-9_.-]", "_", raw.strip()).strip("_")
167
+ if not safe:
168
+ import hashlib
169
+ safe = "agent-" + hashlib.sha1(cmd.encode("utf-8")).hexdigest()[:10]
170
+ return safe[:80]
171
+
172
+
156
173
  def _spawn_terminal_macos(cmd: str) -> tuple[bool, str]:
157
174
  """Spawn `cmd` in a new VISIBLE Terminal/iTerm window (detached).
158
175
 
159
- Primary: osascript ('do script') opens a window in the running Terminal/iTerm
160
- but REQUIRES the macOS Automation permission. If the user hasn't granted it,
161
- osascript fails. Fallback: a temp .command file launched via `open`, which opens
162
- a visible Terminal window WITHOUT Automation (it just opens the app with a
163
- document). So the spawn is ALWAYS a visible terminal, never a silent headless
164
- background process (LAUNCH-1 spawn-visible gap).
176
+ NATIVE `.command` + `open -a <App>` ONLY deliberately NO osascript/AppleScript.
177
+
178
+ Why no AppleScript: driving the singleton Terminal.app via Apple Events from a
179
+ process that lacks Full Disk Access (e.g. the launchd-managed hostd) makes
180
+ macOS re-attribute Terminal.app's TCC "responsible process" to that no-FDA
181
+ caller, which silently strips ~/Desktop / ~/Documents access from ALL Terminal
182
+ windows — including ones the user opened by hand — mid-session. `open -a` goes
183
+ through LaunchServices (no Apple Events), so Terminal.app keeps its own TCC
184
+ identity and nothing gets poisoned. (back-2 Mac TCC fix, 2026-06-05.)
185
+
186
+ The launcher is a stable per-target `.command` under ~/.meshcode/launchers that
187
+ `cd "$HOME"` first (a NON-protected cwd) so the launching shell never dies on
188
+ getcwd() even without FDA, then runs the agent under the venv interpreter
189
+ (derived from sys.executable — no hardcoded venv name) so the user never has to
190
+ `source .../activate` by hand.
165
191
  """
166
192
  term = _detect_macos_terminal()
167
- # AppleScript escaping: backslash + double-quote
168
- safe = cmd.replace("\\", "\\\\").replace('"', '\\"')
169
- if term == "iterm":
170
- script = (
171
- 'tell application "iTerm"\n'
172
- ' create window with default profile\n'
173
- f' tell current session of current window to write text "{safe}"\n'
174
- 'end tell\n'
175
- )
176
- else:
177
- script = f'tell application "Terminal" to do script "{safe}"\n'
178
- r = subprocess.run(["osascript", "-e", script],
193
+ app = "iTerm" if term == "iterm" else "Terminal"
194
+ # venv bin dir (…/<venv>/bin) derived from the live interpreter, NOT hardcoded,
195
+ # so `meshcode`/`python` resolve without a manual `source activate`. NB: do NOT
196
+ # .resolve() — a venv's bin/python symlinks back to the base interpreter (e.g.
197
+ # Homebrew's framework bin), so resolving would escape the venv. sys.executable
198
+ # is already the absolute venv python, so .parent is the venv bin.
199
+ try:
200
+ venv_bin = str(Path(sys.executable).parent)
201
+ except Exception:
202
+ venv_bin = ""
203
+ # Stable launcher path (debuggable, reused across spawns; not /tmp).
204
+ launch_dir = Path.home() / ".meshcode" / "launchers"
205
+ script_path = launch_dir / f"{_launcher_label(cmd)}.command"
206
+ lines = ["#!/bin/bash",
207
+ 'cd "$HOME" 2>/dev/null || cd /'] # neutral, non-TCC-protected cwd
208
+ if venv_bin:
209
+ lines.append(f'export PATH={shlex.quote(venv_bin)}:"$PATH"')
210
+ lines.append(cmd) # ends in `exec … meshcode run …`
211
+ try:
212
+ launch_dir.mkdir(parents=True, exist_ok=True)
213
+ script_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
214
+ os.chmod(script_path, 0o755)
215
+ except Exception as e:
216
+ return False, f"could not write launcher {script_path}: {e}"
217
+ # `open -a <App> <file>` activates the app + brings it to the FRONT (focused).
218
+ # NEVER bare `open <file>` / `-g`: those can open in the background → looks like
219
+ # "nothing happened" (same class of bug as the Windows `-w new` focus fix).
220
+ r = subprocess.run(["open", "-a", app, str(script_path)],
179
221
  capture_output=True, text=True)
180
222
  if r.returncode == 0:
181
223
  return True, term
182
- osa_err = (r.stderr or "osascript failed").strip()
183
- # Fallback: a .command launcher opened via `open` no Automation permission needed.
184
- try:
185
- import tempfile
186
- fd, path = tempfile.mkstemp(suffix=".command", prefix="meshcode-launch-")
187
- with os.fdopen(fd, "w") as f:
188
- f.write("#!/bin/bash\n")
189
- f.write(cmd + "\n")
190
- f.write('rm -f "$0"\n') # self-clean the temp launcher on exit
191
- os.chmod(path, 0o755)
192
- r2 = subprocess.run(["open", path], capture_output=True, text=True)
193
- if r2.returncode == 0:
194
- return True, "terminal(open-fallback)"
195
- return False, (f"osascript failed ({osa_err}); "
196
- f"open fallback failed ({(r2.stderr or '').strip()})")
197
- except Exception as e:
198
- return False, f"osascript failed ({osa_err}); open fallback exception: {e}"
224
+ err = (r.stderr or "open failed").strip()
225
+ # Last-ditch: plain `open` lets LaunchServices pick the .command handler
226
+ # (still no AppleScript). A background window beats no window.
227
+ r2 = subprocess.run(["open", str(script_path)], capture_output=True, text=True)
228
+ if r2.returncode == 0:
229
+ return True, f"{term}(open-default)"
230
+ return False, (f"open -a {app} failed ({err}); "
231
+ f"open fallback failed ({(r2.stderr or '').strip()})")
199
232
 
200
233
 
201
234
  def _spawn_terminal_linux(cmd: str) -> tuple[bool, str]:
@@ -1238,22 +1238,11 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
1238
1238
  if use_shell:
1239
1239
  print(f"[meshcode] (Windows: launching via shell for .cmd wrapper)")
1240
1240
  launch_cwd = str(ws) if os.path.isdir(ws) else None
1241
- # LAUNCH-NO-WINDOW (task 35bee961): when hostd spawned us HEADLESS, our parent
1242
- # python has NO console (DETACHED_PROCESS|CREATE_NO_WINDOW). Spawning the Claude
1243
- # child here WITHOUT CREATE_NO_WINDOW makes Windows allocate a FRESH console
1244
- # window for it -> a terminal pops up despite "headless". Pass CREATE_NO_WINDOW so
1245
- # the child stays windowless too. Detect headless via the env flag hostd sets, OR
1246
- # by having no console of our own (GetConsoleWindow()==0) as a belt-and-suspenders.
1241
+ # HEADLESS REMOVED (task cba01de5, Ian 2026-06-05): hostd no longer spawns us in a
1242
+ # background/no-window mode, so the Claude child always runs inside our visible
1243
+ # console no CREATE_NO_WINDOW. (Previously MESHCODE_HEADLESS / GetConsoleWindow()==0
1244
+ # forced a windowless child; that belongs to the headless feature, now gone.)
1247
1245
  _kwargs = {"shell": use_shell, "cwd": launch_cwd}
1248
- _headless = os.environ.get("MESHCODE_HEADLESS", "").lower() in ("1", "true", "yes")
1249
- if not _headless:
1250
- try:
1251
- import ctypes as _ct
1252
- _headless = _ct.windll.kernel32.GetConsoleWindow() == 0
1253
- except Exception:
1254
- _headless = False
1255
- if _headless:
1256
- _kwargs["creationflags"] = 0x08000000 # CREATE_NO_WINDOW
1257
1246
  result = _sp.run(cmd, **_kwargs)
1258
1247
  sys.exit(result.returncode)
1259
1248
  else:
@@ -325,6 +325,10 @@ def _spawn_background_updater(target_version: str, target_python: Optional[str]
325
325
  exits. Next `meshcode run` consumes the result. `target_python` (when set)
326
326
  is the agent's MCP-server env — pip installs into IT, not the launcher.
327
327
  """
328
+ # 8e4f93f9 (stability): never overwrite a shared site-packages while live
329
+ # agents import it (would clobber their MCP). Defer to a quiescent launch.
330
+ if _live_mcp_serves_excluding_self() != 0:
331
+ return False
328
332
  if not _acquire_lock():
329
333
  return False
330
334
 
@@ -371,6 +375,36 @@ def _spawn_background_updater(target_version: str, target_python: Optional[str]
371
375
  return False
372
376
 
373
377
 
378
+ # ============================================================
379
+ # 8e4f93f9 (Samuel #1 STABILITY): co-resident guard
380
+ # ============================================================
381
+
382
+ def _live_mcp_serves_excluding_self() -> int:
383
+ """Count live `meshcode_mcp serve` processes other than self/parent.
384
+
385
+ 8e4f93f9 RC: an inline/background `pip install -U meshcode` OVERWRITES the
386
+ shared site-packages that co-resident LIVE agents are importing → clobbers
387
+ their MCP (the ~1h drop). Windows is immune (file-lock makes the overwrite
388
+ fail). We mirror that on POSIX by DEFERRING the overwrite while ANY serve is
389
+ live; the update applies on a future quiescent launch (`meshcode update`
390
+ forces it). FAIL-SAFE: on any error return 1 ("assume busy → defer") so we
391
+ NEVER overwrite a live env under uncertainty.
392
+ """
393
+ if sys.platform == "win32":
394
+ return 0 # Windows file-locks already prevent a live overwrite
395
+ try:
396
+ out = subprocess.run(
397
+ ["pgrep", "-f", "meshcode_mcp serve"],
398
+ capture_output=True, text=True, timeout=5,
399
+ ).stdout
400
+ pids = {int(x) for x in out.split() if x.strip().isdigit()}
401
+ pids.discard(os.getpid())
402
+ pids.discard(os.getppid())
403
+ return len(pids)
404
+ except Exception:
405
+ return 1 # fail-safe: assume busy, defer (never clobber under uncertainty)
406
+
407
+
374
408
  # ============================================================
375
409
  # Public entrypoint — called from meshcode run
376
410
  # ============================================================
@@ -466,7 +500,7 @@ def sync_agent_env(mcp_python: str, verbose: bool = False) -> None:
466
500
  # subprocess inherits the latest meshcode_mcp/server.py on disk.
467
501
  # ============================================================
468
502
 
469
- def check_and_maybe_update_blocking(verbose: bool = True, timeout_sec: int = 60) -> Optional[str]:
503
+ def check_and_maybe_update_blocking(verbose: bool = True, timeout_sec: int = 60, force: bool = False) -> Optional[str]:
470
504
  """Blocking pip install -U meshcode. Returns the new version, or None.
471
505
 
472
506
  Same skip-gates as check_and_maybe_update() EXCEPT the `auto_update`
@@ -508,6 +542,24 @@ def check_and_maybe_update_blocking(verbose: bool = True, timeout_sec: int = 60)
508
542
  pass
509
543
  return None
510
544
 
545
+ # 8e4f93f9 (Samuel #1 STABILITY): on POSIX an inline `pip install -U` would
546
+ # OVERWRITE the shared site-packages that co-resident LIVE agents are importing
547
+ # → clobbers their MCP (the ~1h drop RC). Mirror Windows' file-lock immunity:
548
+ # if ANY other meshcode_mcp serve is live, DEFER — the update lands on a future
549
+ # QUIESCENT launch. `meshcode update` (force=True) overrides for an intentional,
550
+ # operator-initiated upgrade.
551
+ if not force:
552
+ _live = _live_mcp_serves_excluding_self()
553
+ if _live != 0:
554
+ if verbose:
555
+ print(
556
+ f"[meshcode] {_live} live agent serve(s) share this env — deferring "
557
+ f"update to a clean launch (run `meshcode update` to force). "
558
+ f"Staying on meshcode {_current_version()}.",
559
+ file=sys.stderr,
560
+ )
561
+ return None
562
+
511
563
  cur = _current_version()
512
564
  latest = fetch_latest_version()
513
565
  _mark_checked(latest)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.111
3
+ Version: 2.11.112
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -2,14 +2,10 @@ README.md
2
2
  pyproject.toml
3
3
  meshcode/__init__.py
4
4
  meshcode/__main__.py
5
- meshcode/_session_handoff_template 2.py
6
- meshcode/_session_handoff_template 3.py
7
5
  meshcode/_session_handoff_template.py
8
6
  meshcode/_stop_hook_template.py
9
7
  meshcode/ascii_art.py
10
8
  meshcode/atomic_push.py
11
- meshcode/claude_update 2.py
12
- meshcode/claude_update 3.py
13
9
  meshcode/claude_update.py
14
10
  meshcode/cli.py
15
11
  meshcode/comms_v4.py
@@ -19,7 +15,6 @@ meshcode/date_parse.py
19
15
  meshcode/doctor.py
20
16
  meshcode/error_hints.py
21
17
  meshcode/exceptions.py
22
- meshcode/hostd 2.py
23
18
  meshcode/hostd.py
24
19
  meshcode/invites.py
25
20
  meshcode/launcher.py
@@ -34,7 +29,6 @@ meshcode/secrets.py
34
29
  meshcode/self_update.py
35
30
  meshcode/setup_clients.py
36
31
  meshcode/supervisor.py
37
- meshcode/up 2.py
38
32
  meshcode/up.py
39
33
  meshcode/upload.py
40
34
  meshcode.egg-info/PKG-INFO
@@ -63,8 +57,6 @@ tests/test_auto_update_hardening.py
63
57
  tests/test_autonomous_closegap_1.py
64
58
  tests/test_autonomous_closegap_2.py
65
59
  tests/test_autonomous_closegap_3.py
66
- tests/test_autonomous_prompt_inject 2.py
67
- tests/test_autonomous_prompt_inject 3.py
68
60
  tests/test_autonomous_prompt_inject.py
69
61
  tests/test_boot_bug_regression.py
70
62
  tests/test_color_truecolor.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.11.111"
7
+ version = "2.11.112"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}