meshcode 2.11.114rc1__tar.gz → 2.11.115__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 (99) hide show
  1. {meshcode-2.11.114rc1 → meshcode-2.11.115}/PKG-INFO +1 -1
  2. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/hostd.py +0 -236
  4. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/meshcode_mcp/server.py +111 -7
  5. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/protocol_handler.py +34 -1
  6. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/run_agent.py +0 -17
  7. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode.egg-info/PKG-INFO +1 -1
  8. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode.egg-info/SOURCES.txt +37 -7
  9. {meshcode-2.11.114rc1 → meshcode-2.11.115}/pyproject.toml +1 -1
  10. meshcode-2.11.115/tests/test_auto_update_hardening.py +295 -0
  11. meshcode-2.11.115/tests/test_autonomous_closegap_1.py +164 -0
  12. meshcode-2.11.115/tests/test_autonomous_closegap_2.py +210 -0
  13. meshcode-2.11.115/tests/test_autonomous_closegap_3.py +163 -0
  14. meshcode-2.11.115/tests/test_autonomous_prompt_inject.py +126 -0
  15. meshcode-2.11.115/tests/test_boot_bug_regression.py +205 -0
  16. meshcode-2.11.115/tests/test_color_truecolor.py +83 -0
  17. meshcode-2.11.115/tests/test_core.py +216 -0
  18. meshcode-2.11.115/tests/test_cross_agent_messaging.py +366 -0
  19. meshcode-2.11.115/tests/test_date_parse.py +112 -0
  20. meshcode-2.11.115/tests/test_doctor.py +123 -0
  21. meshcode-2.11.115/tests/test_epistemic_v1_python_sdk.py +177 -0
  22. meshcode-2.11.115/tests/test_epistemic_v1_stop_conditions.py +158 -0
  23. meshcode-2.11.115/tests/test_esc_deaf_state.py +361 -0
  24. meshcode-2.11.115/tests/test_exceptions.py +107 -0
  25. meshcode-2.11.115/tests/test_file_upload.py +171 -0
  26. meshcode-2.11.115/tests/test_init_device_code.py +68 -0
  27. meshcode-2.11.115/tests/test_install_guard.py +170 -0
  28. meshcode-2.11.115/tests/test_lease_sigterm_release.py +299 -0
  29. meshcode-2.11.115/tests/test_mark_read_batch.py +200 -0
  30. meshcode-2.11.115/tests/test_marketplace_ratings.py +174 -0
  31. meshcode-2.11.115/tests/test_migration_integrity.py +176 -0
  32. meshcode-2.11.115/tests/test_realtime_event_freshness.py +236 -0
  33. meshcode-2.11.115/tests/test_rls_cross_tenant.py +255 -0
  34. meshcode-2.11.115/tests/test_rpc_grants.py +76 -0
  35. meshcode-2.11.115/tests/test_rpc_migrations.py +452 -0
  36. meshcode-2.11.115/tests/test_run_agent_dry_run.py +128 -0
  37. meshcode-2.11.115/tests/test_run_agent_no_server_import.py +85 -0
  38. meshcode-2.11.115/tests/test_security_regressions.py +228 -0
  39. meshcode-2.11.115/tests/test_self_update_user_site.py +139 -0
  40. meshcode-2.11.115/tests/test_sentinel.py +148 -0
  41. meshcode-2.11.115/tests/test_setup_path.py +66 -0
  42. meshcode-2.11.115/tests/test_sleep_signals.py +160 -0
  43. meshcode-2.11.115/tests/test_status_enum_coverage.py +231 -0
  44. meshcode-2.11.115/tests/test_stay_on_loop_hook.py +302 -0
  45. meshcode-2.11.115/tests/test_wait_open_tasks_contradiction.py +87 -0
  46. meshcode-2.11.114rc1/meshcode/_session_handoff_template 2.py +0 -296
  47. meshcode-2.11.114rc1/meshcode/_session_handoff_template 3.py +0 -296
  48. meshcode-2.11.114rc1/meshcode/claude_update 2.py +0 -258
  49. meshcode-2.11.114rc1/meshcode/claude_update 3.py +0 -258
  50. meshcode-2.11.114rc1/meshcode/hostd 2.py +0 -1269
  51. meshcode-2.11.114rc1/meshcode/up 2.py +0 -257
  52. {meshcode-2.11.114rc1 → meshcode-2.11.115}/README.md +0 -0
  53. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/__main__.py +0 -0
  54. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/_session_handoff_template.py +0 -0
  55. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/_stop_hook_template.py +0 -0
  56. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/ascii_art.py +0 -0
  57. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/atomic_push.py +0 -0
  58. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/claude_update.py +0 -0
  59. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/cli.py +0 -0
  60. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/comms_v4.py +0 -0
  61. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/compat.py +0 -0
  62. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/daemon.py +0 -0
  63. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/date_parse.py +0 -0
  64. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/doctor.py +0 -0
  65. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/error_hints.py +0 -0
  66. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/exceptions.py +0 -0
  67. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/hooks/__init__.py +0 -0
  68. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/hooks/repo_path_lock.py +0 -0
  69. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/invites.py +0 -0
  70. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/launcher.py +0 -0
  71. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/launcher_install.py +0 -0
  72. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/meshcode_mcp/__init__.py +0 -0
  73. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/meshcode_mcp/__main__.py +0 -0
  74. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/meshcode_mcp/backend.py +0 -0
  75. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/meshcode_mcp/realtime.py +0 -0
  76. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  77. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/meshcode_mcp/test_backend.py +0 -0
  78. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  79. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  80. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  81. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  82. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  83. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/preferences.py +0 -0
  84. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/protocol_v2.py +0 -0
  85. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/quickstart.py +0 -0
  86. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/rpc_allowlist.py +0 -0
  87. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/scripts/check_secrets.py +0 -0
  88. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/scripts/race_rate_harness.py +0 -0
  89. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/secrets.py +0 -0
  90. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/self_update.py +0 -0
  91. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/setup_clients.py +0 -0
  92. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/supervisor.py +0 -0
  93. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/up.py +0 -0
  94. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode/upload.py +0 -0
  95. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode.egg-info/dependency_links.txt +0 -0
  96. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode.egg-info/entry_points.txt +0 -0
  97. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode.egg-info/requires.txt +0 -0
  98. {meshcode-2.11.114rc1 → meshcode-2.11.115}/meshcode.egg-info/top_level.txt +0 -0
  99. {meshcode-2.11.114rc1 → meshcode-2.11.115}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.114rc1
3
+ Version: 2.11.115
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.114rc1"
2
+ __version__ = "2.11.115"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -816,32 +816,6 @@ def _pid_cmdline(pid: int) -> str:
816
816
  return ""
817
817
 
818
818
 
819
- def _pid_alive(pid: int) -> bool:
820
- """True iff `pid` is a live process. Signal-0 probe on POSIX; tasklist on Windows.
821
- Best-effort — on any unexpected error returns False (treat unknown as not-alive so
822
- the ghost detector never mis-reports). PermissionError means the process exists but
823
- isn't ours = alive; ProcessLookupError means dead."""
824
- try:
825
- pid = int(pid)
826
- if pid <= 0:
827
- return False
828
- if sys.platform == "win32":
829
- # /FO CSV so the pid is a quoted field — anchor on `"<pid>"` instead of a bare
830
- # substring (a bare pid can match a memory/session column → false-positive).
831
- out = subprocess.run(["tasklist", "/FI", f"PID eq {pid}", "/NH", "/FO", "CSV"],
832
- capture_output=True, text=True, timeout=5).stdout or ""
833
- return f'"{pid}"' in out
834
- try:
835
- os.kill(pid, 0)
836
- except ProcessLookupError:
837
- return False
838
- except PermissionError:
839
- return True
840
- return True
841
- except Exception:
842
- return False
843
-
844
-
845
819
  def _discover_agent_pids(target: str) -> list:
846
820
  """Fallback PID discovery by command line, for agents spawned before this hostd
847
821
  (no recorded PID) or after a state-file loss. Matches `meshcode run <target>`.
@@ -1228,215 +1202,6 @@ def _do_recycle_enforce(api_key: str, host_id: str) -> int:
1228
1202
  return n
1229
1203
 
1230
1204
 
1231
- # ------------------------------------------------------------------
1232
- # FOCUS-1 GHOST SESSIONS (back-2, task 09fca8fe) — dead-MCP detector.
1233
- #
1234
- # OS-ghost = a `claude` agent process still ALIVE but its `meshcode_mcp serve`
1235
- # child DEAD → no heartbeat → the dashboard shows it offline/invisible while a
1236
- # terminal window is still open ("connected-but-invisible"). Two local pidfiles
1237
- # correlate it, no roster RPC needed for detection:
1238
- # S3 claude alive — ~/.meshcode/pids/<project>__<agent>.pid, stamped by
1239
- # run_agent right before os.execvp (the pid survives exec => == claude pid).
1240
- # S2 mcp dead — {tempdir}/meshcode_mcp_<project>_<agent>.pid, written by the
1241
- # MCP server (meshcode_mcp/server.py). If that pid is dead/missing, mcp died.
1242
- # A ghost = S3 alive AND S2 dead. The eventual reap (kill the orphaned claude so
1243
- # the respawn sweep relaunches it clean) ALSO needs S1 = the server's is_ghost /
1244
- # effective_status='offline' (prod mc_agent_liveness) as a third independent
1245
- # guard, plus the cmdline reuse-guard — wired when arming.
1246
- #
1247
- # SHIPS LOG-ONLY (_GHOST_REAP_DRYRUN=True): logs GHOST-DRYRUN candidates so we can
1248
- # confirm ZERO false positives across the live fleet BEFORE flipping to a real
1249
- # kill (commander-mandated staged arming). Kills NOTHING while dry-run.
1250
- #
1251
- # GUARDS folded from back's cross-review (HIGH_1/HIGH_2) — all enforced even in the
1252
- # DRY-RUN classification so the logs only ever show TRUE ghosts:
1253
- # - boot-grace: skip until spawned_age (pidfile mtime) > GHOST_BOOT_GRACE_SEC, so a
1254
- # freshly-booting agent (claude up, MCP still connecting) is NOT mis-flagged.
1255
- # - cwd-guard: the live pid's working dir MUST equal the agent's recorded launch cwd
1256
- # (pidfile {cwd}, or the workspace as legacy fallback). Anti PID-reuse mis-kill —
1257
- # post-execvp claude's cmdline loses <target>, so cwd is the stable correlator. If
1258
- # cwd is unreadable on this platform → fail-SAFE (skip, never reap on uncertainty).
1259
- # ------------------------------------------------------------------
1260
- _GHOST_REAP_DRYRUN = True
1261
- GHOST_BOOT_GRACE_SEC = _env_int("MESHCODE_GHOST_BOOT_GRACE_SEC", 120, 30) # mac boots slow; >> _REAP grace + MCP reconnect
1262
- GHOST_PERSIST_SEC = _env_int("MESHCODE_GHOST_PERSIST_SEC", 90, 30) # ghost must hold this long → a brief mcp restart doesn't count (MED_restart)
1263
-
1264
-
1265
- def _mcp_serve_pid(project: str, agent: str) -> Optional[int]:
1266
- """Read the MCP server's recorded pid for this agent (meshcode_mcp/server.py
1267
- lockfile). Returns the pid, or None if the lockfile is missing/unparseable.
1268
- Path + JSON|bare-int format mirror _pid_lockfile_path()/_read_pid_lockfile()."""
1269
- try:
1270
- import tempfile as _tf
1271
- safe = f"meshcode_mcp_{project}_{agent}.pid".replace("/", "_").replace(" ", "_")
1272
- path = os.path.join(_tf.gettempdir(), safe)
1273
- if not os.path.exists(path):
1274
- return None
1275
- raw = open(path, "r").read().strip()
1276
- if not raw:
1277
- return None
1278
- try:
1279
- data = json.loads(raw)
1280
- if isinstance(data, dict) and "pid" in data:
1281
- return int(data["pid"])
1282
- except (ValueError, TypeError):
1283
- pass
1284
- return int(raw)
1285
- except Exception:
1286
- return None
1287
-
1288
-
1289
- def _agent_launcher_record(project: str, agent: str):
1290
- """Read run_agent's pre-execvp pidfile (~/.meshcode/pids/<project>__<agent>.pid).
1291
- Returns (pid:int|None, cwd:str|None). New format is JSON {"pid","cwd"}; tolerates
1292
- the legacy bare-int (no recorded cwd)."""
1293
- try:
1294
- safe = f"{project}__{agent}".replace("/", "_").replace("\\", "_").replace(" ", "_")
1295
- path = STATE_DIR / "pids" / f"{safe}.pid"
1296
- if not path.exists():
1297
- return None, None
1298
- raw = path.read_text(encoding="utf-8").strip()
1299
- if not raw:
1300
- return None, None
1301
- try:
1302
- data = json.loads(raw)
1303
- if isinstance(data, dict) and "pid" in data:
1304
- return int(data["pid"]), (data.get("cwd") or None)
1305
- except (ValueError, TypeError):
1306
- pass
1307
- return int(raw), None
1308
- except Exception:
1309
- return None, None
1310
-
1311
-
1312
- def _read_pid_cwd(pid: int) -> Optional[str]:
1313
- """Best-effort working directory of a live pid (time-boxed). None if unreadable.
1314
- linux : /proc/<pid>/cwd symlink
1315
- macOS : lsof -a -p <pid> -d cwd -Fn → the 'n' line (no Full Disk Access needed)
1316
- win : None (run_agent does NOT execvp there; cmdline keeps `run <target>`, so
1317
- the existing cmdline token-guard already correlates — cwd not needed)."""
1318
- try:
1319
- if sys.platform == "win32":
1320
- return None
1321
- if sys.platform == "darwin":
1322
- out = subprocess.run(["lsof", "-a", "-p", str(int(pid)), "-d", "cwd", "-Fn"],
1323
- capture_output=True, text=True, timeout=5).stdout or ""
1324
- for line in out.splitlines():
1325
- if line.startswith("n"):
1326
- return line[1:].strip() or None
1327
- return None
1328
- return os.readlink(f"/proc/{int(pid)}/cwd") # linux
1329
- except Exception:
1330
- return None
1331
-
1332
-
1333
- def _pid_cwd_matches(pid: int, expected_cwd: Optional[str], project: str, agent: str):
1334
- """Tri-state: True if the pid's live cwd == the agent's launch cwd; False if it
1335
- differs; None if unreadable. `expected_cwd` is the pidfile-recorded cwd; when absent
1336
- (legacy pidfile) fall back to the canonical workspace dir. realpath both sides
1337
- (mac /Users vs /private symlinks, --repo dirs). Autonomous reap requires True."""
1338
- live = _read_pid_cwd(pid)
1339
- if not live:
1340
- return None # unreadable → unknown → fail-safe skip
1341
- exp = set()
1342
- if expected_cwd:
1343
- try: exp.add(os.path.realpath(os.path.expanduser(expected_cwd)))
1344
- except Exception: pass
1345
- try:
1346
- exp.add(os.path.realpath(os.path.join(os.path.expanduser("~"), "meshcode", f"{project}-{agent}")))
1347
- except Exception:
1348
- pass
1349
- try:
1350
- return os.path.realpath(live) in exp
1351
- except Exception:
1352
- return None
1353
-
1354
-
1355
- def _do_ghost_reap(api_key: str, host_id: str) -> int:
1356
- """Detect dead-MCP ghosts (claude ALIVE + mcp_serve DEAD) from local pidfiles.
1357
- LOG-ONLY while _GHOST_REAP_DRYRUN — confirm zero false positives in the fleet logs
1358
- before arming. Returns the count that WOULD be reaped.
1359
-
1360
- Self-contained: enumerates this host's launcher pidfiles (agents WE launched), so
1361
- no roster RPC is needed for detection. All HIGH guards (boot-grace + cwd-match) are
1362
- enforced here so DRYRUN logs only ever show TRUE ghosts. Fail-open at the top level
1363
- so a bug here can never wedge the sweep."""
1364
- n = 0
1365
- try:
1366
- pid_dir = STATE_DIR / "pids"
1367
- if not pid_dir.is_dir():
1368
- return 0
1369
- now = time.time()
1370
- st = _load_state()
1371
- seen = dict(st.get("ghost_seen") or {}) # {target: first_candidate_ts} — MED_restart persistence
1372
- still = set() # targets that are candidates THIS sweep (others get cleared)
1373
- for pf in sorted(pid_dir.glob("*.pid")):
1374
- try:
1375
- stem = pf.stem # "<project>__<agent>"
1376
- if "__" not in stem:
1377
- continue
1378
- project, agent = stem.split("__", 1)
1379
- target = f"{project}/{agent}"
1380
- claude_pid, rec_cwd = _agent_launcher_record(project, agent)
1381
- if not claude_pid or not _pid_alive(claude_pid):
1382
- # MED_pidfile_stale: the agent exited (run_agent execvp'd, can't self-clean) →
1383
- # the launcher pid is dead. Prune the stale pidfile so a future reused pid can't FP.
1384
- try:
1385
- pf.unlink()
1386
- except Exception:
1387
- pass
1388
- continue # S3 fail: no live claude → not a ghost
1389
- # HIGH_2 boot-grace: pidfile mtime ≈ spawn time (written just before execvp).
1390
- try:
1391
- spawned_age = now - pf.stat().st_mtime
1392
- except Exception:
1393
- spawned_age = 1e9
1394
- if spawned_age < GHOST_BOOT_GRACE_SEC:
1395
- continue # still booting / MCP may be connecting → never reap
1396
- mcp_pid = _mcp_serve_pid(project, agent)
1397
- if mcp_pid and _pid_alive(mcp_pid):
1398
- continue # S2 fail: mcp_serve alive → healthy
1399
- mcp_state = (f"pid {mcp_pid} DEAD" if mcp_pid else "lockfile MISSING")
1400
- # HIGH_1 cwd-guard: the live pid MUST be running in this agent's launch cwd.
1401
- live_cwd = _read_pid_cwd(claude_pid)
1402
- cwd_match = _pid_cwd_matches(claude_pid, rec_cwd, project, agent)
1403
- if cwd_match is not True:
1404
- _log(f"GHOST-SKIP {target}: claude pid {claude_pid} + mcp {mcp_state} but cwd-guard "
1405
- f"{'UNREADABLE' if cwd_match is None else 'MISMATCH'} "
1406
- f"(live_cwd={live_cwd!r} expected~{rec_cwd!r}) — NOT a confirmed ghost, no reap.")
1407
- continue # fail-SAFE: never reap on cwd uncertainty/mismatch
1408
- # Candidate (S3 alive ∧ boot-grace ∧ S2 dead ∧ cwd-match). MED_restart persistence:
1409
- # only COUNT after the ghost has held GHOST_PERSIST_SEC, so a brief mcp restart
1410
- # (dead for one sweep) never registers.
1411
- still.add(target)
1412
- first_ts = seen.get(target) or now
1413
- seen[target] = first_ts
1414
- held = now - first_ts
1415
- if held < GHOST_PERSIST_SEC:
1416
- _log(f"GHOST-PENDING {target}: dead-MCP candidate held {int(held)}s (< {GHOST_PERSIST_SEC}s) "
1417
- f"— waiting for persistence before counting (guards against a brief mcp restart).")
1418
- continue
1419
- # CONFIRMED, PERSISTENT dead-MCP ghost.
1420
- n += 1
1421
- cmdline = _pid_cmdline(claude_pid).strip()[:120]
1422
- _log(f"GHOST-DRYRUN {target}: claude pid {claude_pid} ALIVE (cwd={live_cwd!r}, "
1423
- f"spawned_age={int(spawned_age)}s, ghost_held={int(held)}s) but meshcode_mcp serve "
1424
- f"{mcp_state} — connected-but-invisible. WOULD reap claude → respawn relaunches clean "
1425
- f"[log-only; arming also needs S1 is_ghost + human-defer + spawn-breaker]. cmdline={cmdline!r}")
1426
- # ARMED PATH (not yet enabled): AND S1 (roster is_ghost) AND not _human_recently_active(target)
1427
- # AND _spawn_rate_ok(target), then _kill_headless_pid(target, claude_pid) → respawn relaunches.
1428
- except Exception:
1429
- continue
1430
- # Clear persistence for targets that recovered (mcp back, agent gone) so a future
1431
- # transient doesn't inherit a stale clock.
1432
- seen = {t: ts for t, ts in seen.items() if t in still}
1433
- st["ghost_seen"] = seen
1434
- _save_state(st)
1435
- except Exception:
1436
- pass
1437
- return n
1438
-
1439
-
1440
1205
  def _do_recycles(api_key: str, host_id: str) -> int:
1441
1206
  """Uptime-based recycle at task boundary. Returns number recycled."""
1442
1207
  # DEAD-FEATURE (task 222b1b02, Samuel 2026-06-04): the RECYCLE feature is disabled in
@@ -1994,7 +1759,6 @@ def cmd_hostd(args: list) -> int:
1994
1759
  stopped = _do_stops(api_key, host_id)
1995
1760
  force_killed = _do_force_kills(api_key, host_id) # 38523a98 Gap1: visible explicit human stop
1996
1761
  reaped = _do_reap(api_key, host_id) # 38523a98: kill ghosts/dup-PIDs/crashed-orphans
1997
- ghosts = _do_ghost_reap(api_key, host_id) # FOCUS-1 09fca8fe: dead-MCP ghost detector (LOG-ONLY)
1998
1762
  _gc_headless_pids() # cb90b058: drop dead PIDs (stale entry can't mask a live agent)
1999
1763
  _up = int(time.monotonic() - _spawn_mono)
2000
1764
  if relaunched or recycled or ver_recycled or stopped or enforced or reaped or force_killed:
@@ -2898,6 +2898,13 @@ def meshcode_send(to: Union[str, List[str]], message: Any, in_reply_to: Optional
2898
2898
  msg_ids[], count}. Cross-mesh '@' is single-recipient only (mixed lists rejected v1).
2899
2899
  sensitive=True hides from exports. encrypted=True for secrets (AES-256-GCM, cross-mesh only).
2900
2900
  type= optional typed-catalog tag; soft-validates, never refuses."""
2901
+ # mode-2 soft-fail (R2-3): a malformed in_reply_to (not a valid uuid) would hit
2902
+ # the uuid cast at the RPC boundary and reject the WHOLE message. Drop it so the
2903
+ # message still sends (pairs with mig463's nonexistent-but-valid-uuid soft-fail).
2904
+ if in_reply_to is not None and not re.fullmatch(
2905
+ r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
2906
+ str(in_reply_to)):
2907
+ in_reply_to = None
2901
2908
  # ── Normalize `to` into a recipients list ──────────────────────────────
2902
2909
  if isinstance(to, list):
2903
2910
  recipients: List[str] = [str(t).strip() for t in to if str(t).strip()]
@@ -3514,6 +3521,58 @@ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
3514
3521
  return None
3515
3522
 
3516
3523
 
3524
+ def _pending_filter(tasks: List[Dict[str, Any]]) -> Optional[List[Dict[str, str]]]:
3525
+ """The _get_pending_tasks_summary filter, applied to a task list we already
3526
+ have (e.g. from mc_wait_poll) so we don't re-fetch. Identical output shape."""
3527
+ pending = [
3528
+ {"id": t["id"][:8], "title": t["title"][:80], "priority": t.get("priority", "normal"), "status": t["status"]}
3529
+ for t in tasks
3530
+ if t.get("status") in ("open", "in_progress")
3531
+ and (t.get("claimed_by") == AGENT_NAME or t.get("assignee") == AGENT_NAME)
3532
+ ]
3533
+ if _is_leader_agent():
3534
+ pending.extend([
3535
+ {"id": t["id"][:8], "title": t["title"][:80], "priority": t.get("priority", "normal"), "status": t["status"]}
3536
+ for t in tasks
3537
+ if t.get("status") == "open" and t.get("assignee") == "*" and not t.get("claimed_by")
3538
+ ])
3539
+ _PRIORITY_ORDER = {"urgent": 0, "high": 1, "normal": 2, "low": 3}
3540
+ pending.sort(key=lambda t: _PRIORITY_ORDER.get(t.get("priority", "normal"), 2))
3541
+ return pending if pending else None
3542
+
3543
+
3544
+ def _wait_poll_or_legacy() -> Dict[str, Any]:
3545
+ """R2-1: ONE mc_wait_poll replaces _check_recycle_request + _check_stop_request
3546
+ + _get_pending_tasks_summary (3 RPCs + 3 api_key resolves -> 1). Semantics are
3547
+ identical (recycle = mig446 guard, stop = desired_state, tasks via the same
3548
+ filter). Falls back to the 3 legacy RPCs on ANY error so a mixed old-SDK/new-RPC
3549
+ fleet never breaks. recycle-first order is preserved by the caller."""
3550
+ try:
3551
+ api_key = _get_api_key()
3552
+ if api_key:
3553
+ resp = be.sb_rpc("mc_wait_poll", {
3554
+ "p_api_key": api_key,
3555
+ "p_project_id": _PROJECT_ID,
3556
+ "p_agent_name": AGENT_NAME,
3557
+ })
3558
+ if isinstance(resp, dict) and resp.get("ok"):
3559
+ return {
3560
+ "recycle": bool(resp.get("recycle")),
3561
+ "stop": bool(resp.get("stop")),
3562
+ "tasks": _pending_filter(resp.get("tasks") or []),
3563
+ "_via": "mc_wait_poll",
3564
+ }
3565
+ except Exception as e:
3566
+ log.debug(f"[meshcode] mc_wait_poll failed; legacy fallback: {e}")
3567
+ # Legacy fallback — the 3 separate RPCs (old behavior, never breaks).
3568
+ return {
3569
+ "recycle": _check_recycle_request(),
3570
+ "stop": _check_stop_request(),
3571
+ "tasks": _get_pending_tasks_summary(),
3572
+ "_via": "legacy",
3573
+ }
3574
+
3575
+
3517
3576
  def _check_recycle_request() -> bool:
3518
3577
  """Server-authorized recycle (task 548c863e, mig 364).
3519
3578
 
@@ -3598,7 +3657,9 @@ def _try_auto_claim_self_assigned_tasks(max_claims: int = 20) -> List[Dict[str,
3598
3657
  for target in candidates[:max_claims]:
3599
3658
  claim_result = be.sb_rpc("mc_task_claim", {
3600
3659
  "p_api_key": api_key,
3660
+ "p_project_id": _PROJECT_ID,
3601
3661
  "p_task_id": target["id"],
3662
+ "p_claiming_agent": AGENT_NAME,
3602
3663
  })
3603
3664
  if isinstance(claim_result, dict) and claim_result.get("ok"):
3604
3665
  log.info(f"[meshcode] auto-claimed self-assigned task: {target['title'][:60]}")
@@ -3643,7 +3704,9 @@ def _try_auto_claim_task() -> Optional[Dict[str, str]]:
3643
3704
  # Claim it
3644
3705
  claim_result = be.sb_rpc("mc_task_claim", {
3645
3706
  "p_api_key": api_key,
3707
+ "p_project_id": _PROJECT_ID,
3646
3708
  "p_task_id": target["id"],
3709
+ "p_claiming_agent": AGENT_NAME,
3647
3710
  })
3648
3711
  if isinstance(claim_result, dict) and claim_result.get("ok"):
3649
3712
  log.info(f"[meshcode] auto-claimed task: {target['title'][:60]}")
@@ -3758,7 +3821,9 @@ def _try_auto_claim_open_by_heuristic(max_load: int = 2) -> Optional[Dict[str, A
3758
3821
  score, target = scored[0]
3759
3822
  claim_result = be.sb_rpc("mc_task_claim", {
3760
3823
  "p_api_key": api_key,
3824
+ "p_project_id": _PROJECT_ID,
3761
3825
  "p_task_id": target["id"],
3826
+ "p_claiming_agent": AGENT_NAME,
3762
3827
  })
3763
3828
  if not (isinstance(claim_result, dict) and claim_result.get("ok")):
3764
3829
  log.debug(f"[meshcode] heuristic claim lost race on {target['id'][:8]}: {claim_result}")
@@ -4196,11 +4261,11 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
4196
4261
  "count": len(pending_tasks),
4197
4262
  "you_are_autonomous": _get_autonomous_mode(),
4198
4263
  "task_pull_active": True,
4199
- "hint": "Work the highest-priority task with meshcode_task_start, then meshcode_task_complete. Wait will surface the next one automatically.",
4264
+ "hint": "Work highest-priority task; wait surfaces the next one.",
4200
4265
  }
4201
4266
  if _auto_started:
4202
4267
  out["auto_started_task"] = _auto_started
4203
- out["hint"] = f"Task '{_auto_started['title'][:60]}' auto-started (in_progress). Work it NOW, then meshcode_task_complete when done."
4268
+ out["hint"] = f"Task '{_auto_started['title'][:60]}' auto-started work it, then task_complete."
4204
4269
  if _wait_entry_auto_claimed:
4205
4270
  out["auto_claimed_at_wait_entry"] = _wait_entry_auto_claimed
4206
4271
  return out
@@ -4223,6 +4288,11 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
4223
4288
  pass # All messages already seen — fall through to wait loop
4224
4289
  else:
4225
4290
  split = _split_messages(deduped)
4291
+ # L5-P3 (task 96d5f2e9): honor include_acks on the refused path too —
4292
+ # this was the only delivery path that leaked acks into the payload,
4293
+ # billing a turn's worth of "X read your message" noise to every agent.
4294
+ if not include_acks:
4295
+ split["acks"] = []
4226
4296
  # Only refuse for real messages — ack-only batches should not block wait
4227
4297
  if split["messages"] or split["done_signals"]:
4228
4298
  resp = {
@@ -4299,7 +4369,8 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
4299
4369
  # us fresh. Checked BEFORE pending_tasks: the fresh session
4300
4370
  # re-attacks any open tasks on boot, so an outstanding backlog
4301
4371
  # must not pin a stale, context-heavy session alive.
4302
- if _check_recycle_request():
4372
+ _wp = _wait_poll_or_legacy() # R2-1: 3 RPCs -> 1 (legacy fallback inside)
4373
+ if _wp["recycle"]:
4303
4374
  _set_state("sleeping", "recycle")
4304
4375
  result["must_exit"] = True
4305
4376
  result["exit_reason"] = (
@@ -4312,7 +4383,7 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
4312
4383
  # toggled this agent OFF (desired_state='stopped'). Exit CLEAN — sticky
4313
4384
  # 'stopped' means hostd will NOT respawn us (unlike recycle). Checked
4314
4385
  # right after recycle so a power-off wins over auto-claim/pending-tasks.
4315
- if _check_stop_request():
4386
+ if _wp["stop"]:
4316
4387
  _set_state("sleeping", "stopped")
4317
4388
  result["must_exit"] = True
4318
4389
  result["exit_reason"] = (
@@ -4322,8 +4393,8 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
4322
4393
  result["stopped"] = True
4323
4394
  break
4324
4395
 
4325
- # Check if new tasks appeared while we waited
4326
- pending_tasks = _get_pending_tasks_summary()
4396
+ # New tasks that appeared while we waited (from the same poll)
4397
+ pending_tasks = _wp["tasks"]
4327
4398
  if pending_tasks:
4328
4399
  result["pending_tasks"] = pending_tasks
4329
4400
  break # Return so agent can work tasks
@@ -4405,7 +4476,8 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
4405
4476
  # `you_are_autonomous` is the canonical field per commander memo
4406
4477
  # decisions msg 29cae7f9; `autonomous_mode` retained for back-compat.
4407
4478
  if isinstance(result, dict):
4408
- result["autonomous_mode"] = _autonomous
4479
+ # L5-P4: `you_are_autonomous` is canonical (commander memo 29cae7f9);
4480
+ # the duplicate `autonomous_mode` alias was dropped from the envelope.
4409
4481
  result["you_are_autonomous"] = _autonomous
4410
4482
  return result
4411
4483
  finally:
@@ -6056,6 +6128,19 @@ def meshcode_boot() -> Dict[str, Any]:
6056
6128
  # Non-fatal: surface that the hints are unavailable but boot succeeds.
6057
6129
  resp.setdefault("time_context_available", False)
6058
6130
 
6131
+ # L5-P4 (task 96d5f2e9): offline agents' mc_agents.task often holds their
6132
+ # FULL registration role text (150+ tokens each), re-billed on every boot.
6133
+ # The peer roster only needs a glanceable line.
6134
+ try:
6135
+ _mesh = resp.get("mesh_status")
6136
+ if isinstance(_mesh, list):
6137
+ for _row in _mesh:
6138
+ _ct = _row.get("current_task") if isinstance(_row, dict) else None
6139
+ if isinstance(_ct, str) and len(_ct) > 120:
6140
+ _row["current_task"] = _ct[:117] + "..."
6141
+ except Exception:
6142
+ pass
6143
+
6059
6144
  # c0ab14c1 Phase 2: open-promise time-awareness (what I owe / what I'm
6060
6145
  # waiting on, with overdue/elapsed minutes). Soft-fail; only added when
6061
6146
  # there's something open.
@@ -7247,6 +7332,25 @@ def meshcode_recall_search(query: str) -> Dict[str, Any]:
7247
7332
  query: Plain English search query.
7248
7333
  """
7249
7334
  api_key = _get_api_key()
7335
+ # L6 M4 (task 84c426d4): hybrid semantic+FTS recall via the mc-embed edge
7336
+ # function (embeds the query with gte-small, then calls mc_memory_search v2
7337
+ # with p_query_embedding — RRF fusion server-side). Soft-fail to the direct
7338
+ # RPC (FTS+substring arms of the same v2 fn) if the edge fn is unreachable.
7339
+ try:
7340
+ import urllib.request as _url
7341
+ _req = _url.Request(
7342
+ f"{be.SUPABASE_URL}/functions/v1/mc-embed",
7343
+ data=json.dumps({"search": {
7344
+ "api_key": api_key, "agent_name": AGENT_NAME,
7345
+ "query": query, "project_name": PROJECT_NAME, "limit": 20,
7346
+ }}).encode(),
7347
+ headers={"Content-Type": "application/json"}, method="POST")
7348
+ with _url.urlopen(_req, timeout=8) as _resp:
7349
+ _out = json.loads(_resp.read().decode())
7350
+ if isinstance(_out, dict) and _out.get("ok"):
7351
+ return _out
7352
+ except Exception as _e:
7353
+ log.debug(f"mc-embed search fallback to direct RPC: {_e}")
7250
7354
  return be.sb_rpc("mc_memory_search", {
7251
7355
  "p_api_key": api_key,
7252
7356
  "p_agent_name": AGENT_NAME,
@@ -207,7 +207,40 @@ def _spawn_terminal_macos(cmd: str) -> tuple[bool, str]:
207
207
  'cd "$HOME" 2>/dev/null || cd /'] # neutral, non-TCC-protected cwd
208
208
  if venv_bin:
209
209
  lines.append(f'export PATH={shlex.quote(venv_bin)}:"$PATH"')
210
- lines.append(cmd) # ends in `exec meshcode run …`
210
+ # RECYCLE-KILL-OLD-TERMINAL (task 2094ff3d, Samuel "que se cierre la terminal
211
+ # pasada"). A recycle/stop ends the agent cleanly but macOS Terminal leaves the
212
+ # window open as "[Process completed]" — an orphan. Run the agent WITHOUT `exec`
213
+ # so bash survives its exit and can close THIS window. `cmd` ends in
214
+ # `exec … meshcode run …`; strip the leading `exec ` so the agent runs as a child
215
+ # (it still execvp's claude internally — that becomes the window's foreground proc).
216
+ lines.append('MC_TTY="$(tty 2>/dev/null)"')
217
+ # `cmd` is a compound line (`unset …; export PATH=…; exec <py> -m meshcode run …`),
218
+ # so the `exec` is NOT leading. Drop the LAST `exec ` token (the one before the
219
+ # interpreter) so the agent runs as a CHILD and bash survives to close the window.
220
+ if "exec " in cmd:
221
+ _head, _, _tail = cmd.rpartition("exec ")
222
+ _run = _head + _tail
223
+ else:
224
+ _run = cmd
225
+ lines.append(_run)
226
+ lines.append('MC_RC=$?')
227
+ # Close THIS Terminal window ONLY on a clean exit (recycle/stop => 0). On a CRASH
228
+ # (non-zero) leave it OPEN so the scrollback is available for debugging (commander
229
+ # condition: debugging > clean window). The own-$MC_TTY filter closes ONLY this
230
+ # window — never another agent's. saving no => no "close?" prompt. macOS-only path.
231
+ lines.append('if [ "$MC_RC" = "0" ] && [ -n "$MC_TTY" ]; then')
232
+ lines.append(
233
+ " /usr/bin/osascript"
234
+ " -e 'tell application \"Terminal\"'"
235
+ " -e 'repeat with w in windows'"
236
+ " -e 'try'"
237
+ " -e \"if (tty of selected tab of w) is \\\"$MC_TTY\\\" then\""
238
+ " -e 'close w saving no'"
239
+ " -e 'end if'"
240
+ " -e 'end try'"
241
+ " -e 'end repeat'"
242
+ " -e 'end tell' >/dev/null 2>&1")
243
+ lines.append('fi')
211
244
  try:
212
245
  launch_dir.mkdir(parents=True, exist_ok=True)
213
246
  script_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
@@ -1246,23 +1246,6 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
1246
1246
  result = _sp.run(cmd, **_kwargs)
1247
1247
  sys.exit(result.returncode)
1248
1248
  else:
1249
- # FOCUS-1 ghost reap (back-2, 09fca8fe): stamp THIS pid so hostd can correlate
1250
- # the agent's visible session for dead-MCP / ghost detection + reap-by-PID. execvp
1251
- # PRESERVES the pid, so this file's pid == the claude session pid exactly. Without a
1252
- # recorded visible pid, hostd's reap/enforce/close-old can't anchor a visible agent
1253
- # (the root gap behind orphaned recycle windows + connected-but-invisible ghosts).
1254
- # Record {pid, cwd}: cwd is the exact launch dir (os.chdir(ws) above, or the
1255
- # --repo dir) so hostd's kill-time cwd-guard can compare the LIVE pid's cwd to
1256
- # THIS recorded cwd — anti PID-reuse mis-kill (a reused pid running a different
1257
- # agent's claude has a different cwd → no match → never killed).
1258
- try:
1259
- _pdir = Path.home() / ".meshcode" / "pids"
1260
- _pdir.mkdir(parents=True, exist_ok=True)
1261
- _safe = f"{resolved_project}__{agent}".replace("/", "_").replace("\\", "_").replace(" ", "_")
1262
- (_pdir / f"{_safe}.pid").write_text(
1263
- json.dumps({"pid": os.getpid(), "cwd": os.getcwd()}), encoding="utf-8")
1264
- except Exception:
1265
- pass
1266
1249
  # Unix: replace this process with the editor
1267
1250
  os.execvp(cmd[0], cmd)
1268
1251
  except FileNotFoundError:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.114rc1
3
+ Version: 2.11.115
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
@@ -58,4 +52,40 @@ meshcode/meshcode_mcp/test_prefs_claude_version.py
58
52
  meshcode/meshcode_mcp/test_realtime.py
59
53
  meshcode/meshcode_mcp/test_server_wrapper.py
60
54
  meshcode/scripts/check_secrets.py
61
- meshcode/scripts/race_rate_harness.py
55
+ meshcode/scripts/race_rate_harness.py
56
+ tests/test_auto_update_hardening.py
57
+ tests/test_autonomous_closegap_1.py
58
+ tests/test_autonomous_closegap_2.py
59
+ tests/test_autonomous_closegap_3.py
60
+ tests/test_autonomous_prompt_inject.py
61
+ tests/test_boot_bug_regression.py
62
+ tests/test_color_truecolor.py
63
+ tests/test_core.py
64
+ tests/test_cross_agent_messaging.py
65
+ tests/test_date_parse.py
66
+ tests/test_doctor.py
67
+ tests/test_epistemic_v1_python_sdk.py
68
+ tests/test_epistemic_v1_stop_conditions.py
69
+ tests/test_esc_deaf_state.py
70
+ tests/test_exceptions.py
71
+ tests/test_file_upload.py
72
+ tests/test_init_device_code.py
73
+ tests/test_install_guard.py
74
+ tests/test_lease_sigterm_release.py
75
+ tests/test_mark_read_batch.py
76
+ tests/test_marketplace_ratings.py
77
+ tests/test_migration_integrity.py
78
+ tests/test_realtime_event_freshness.py
79
+ tests/test_rls_cross_tenant.py
80
+ tests/test_rpc_grants.py
81
+ tests/test_rpc_migrations.py
82
+ tests/test_run_agent_dry_run.py
83
+ tests/test_run_agent_no_server_import.py
84
+ tests/test_security_regressions.py
85
+ tests/test_self_update_user_site.py
86
+ tests/test_sentinel.py
87
+ tests/test_setup_path.py
88
+ tests/test_sleep_signals.py
89
+ tests/test_status_enum_coverage.py
90
+ tests/test_stay_on_loop_hook.py
91
+ 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.114rc1"
7
+ version = "2.11.115"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}