meshcode 2.11.111rc1__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.
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/PKG-INFO +1 -1
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/__init__.py +1 -1
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/comms_v4.py +12 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/hostd.py +59 -130
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/protocol_handler.py +68 -35
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/run_agent.py +4 -15
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/self_update.py +53 -1
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode.egg-info/SOURCES.txt +0 -8
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/pyproject.toml +1 -1
- meshcode-2.11.111rc1/meshcode/_session_handoff_template 2.py +0 -296
- meshcode-2.11.111rc1/meshcode/_session_handoff_template 3.py +0 -296
- meshcode-2.11.111rc1/meshcode/claude_update 2.py +0 -258
- meshcode-2.11.111rc1/meshcode/claude_update 3.py +0 -258
- meshcode-2.11.111rc1/meshcode/hostd 2.py +0 -1269
- meshcode-2.11.111rc1/meshcode/up 2.py +0 -257
- meshcode-2.11.111rc1/tests/test_autonomous_prompt_inject 2.py +0 -126
- meshcode-2.11.111rc1/tests/test_autonomous_prompt_inject 3.py +0 -126
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/README.md +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/__main__.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/cli.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/compat.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/daemon.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/doctor.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/hooks/__init__.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/hooks/repo_path_lock.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/invites.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/launcher.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/preferences.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/secrets.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/up.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode/upload.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/setup.cfg +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_core.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_doctor.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.111rc1 → meshcode-2.11.112}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -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,
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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
|
|
531
|
-
#
|
|
532
|
-
# from a prior session
|
|
533
|
-
# rebooting / hostd-restart does NOT pop terminals.
|
|
534
|
-
#
|
|
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
|
-
|
|
468
|
+
_spawn_age = c.get("spawned_age_s")
|
|
537
469
|
_hostd_uptime = time.time() - _HOSTD_STARTED_AT
|
|
538
|
-
if
|
|
539
|
-
_log(f"BOOT-AUTOSTART OFF: skip auto-respawn {proj}/{agent} (
|
|
540
|
-
f"start — boot-stale
|
|
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
|
-
#
|
|
548
|
-
#
|
|
549
|
-
#
|
|
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):
|
|
618
|
-
#
|
|
619
|
-
#
|
|
620
|
-
|
|
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"(
|
|
623
|
-
if _spawn_agent(proj, agent,
|
|
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
|
|
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
|
|
834
|
-
#
|
|
835
|
-
#
|
|
836
|
-
#
|
|
837
|
-
#
|
|
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.
|
|
1048
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
183
|
-
#
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
#
|
|
1242
|
-
#
|
|
1243
|
-
#
|
|
1244
|
-
#
|
|
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)
|
|
@@ -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
|