meshcode 2.11.135__tar.gz → 2.11.136__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 (102) hide show
  1. {meshcode-2.11.135 → meshcode-2.11.136}/PKG-INFO +1 -1
  2. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/hostd.py +106 -20
  4. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/protocol_handler.py +19 -0
  5. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode.egg-info/PKG-INFO +1 -1
  6. {meshcode-2.11.135 → meshcode-2.11.136}/pyproject.toml +1 -1
  7. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_hostd_zombie_sessions.py +141 -3
  8. {meshcode-2.11.135 → meshcode-2.11.136}/README.md +0 -0
  9. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/__main__.py +0 -0
  10. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/_session_handoff_template.py +0 -0
  11. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/_stop_hook_template.py +0 -0
  12. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/ascii_art.py +0 -0
  13. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/atomic_push.py +0 -0
  14. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/claude_update.py +0 -0
  15. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/cli.py +0 -0
  16. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/comms_v4.py +0 -0
  17. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/compat.py +0 -0
  18. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/daemon.py +0 -0
  19. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/date_parse.py +0 -0
  20. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/doctor.py +0 -0
  21. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/error_hints.py +0 -0
  22. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/exceptions.py +0 -0
  23. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/hooks/__init__.py +0 -0
  24. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/hooks/repo_path_lock.py +0 -0
  25. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/invites.py +0 -0
  26. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/launcher.py +0 -0
  27. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/launcher_install.py +0 -0
  28. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/meshcode_mcp/__init__.py +0 -0
  29. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/meshcode_mcp/__main__.py +0 -0
  30. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/meshcode_mcp/backend.py +0 -0
  31. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/meshcode_mcp/realtime.py +0 -0
  32. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/meshcode_mcp/server.py +0 -0
  33. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  34. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/meshcode_mcp/swarm.py +0 -0
  35. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/meshcode_mcp/test_backend.py +0 -0
  36. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  37. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  38. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  39. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  40. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  41. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/meshcode_mcp/test_swarm.py +0 -0
  42. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/preferences.py +0 -0
  43. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/protocol_v2.py +0 -0
  44. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/quickstart.py +0 -0
  45. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/rpc_allowlist.py +0 -0
  46. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/run_agent.py +0 -0
  47. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/scripts/check_secrets.py +0 -0
  48. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/scripts/race_rate_harness.py +0 -0
  49. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/secrets.py +0 -0
  50. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/self_update.py +0 -0
  51. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/setup_clients.py +0 -0
  52. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/supervisor.py +0 -0
  53. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/up.py +0 -0
  54. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode/upload.py +0 -0
  55. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode.egg-info/SOURCES.txt +0 -0
  56. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode.egg-info/dependency_links.txt +0 -0
  57. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode.egg-info/entry_points.txt +0 -0
  58. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode.egg-info/requires.txt +0 -0
  59. {meshcode-2.11.135 → meshcode-2.11.136}/meshcode.egg-info/top_level.txt +0 -0
  60. {meshcode-2.11.135 → meshcode-2.11.136}/setup.cfg +0 -0
  61. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_auto_update_hardening.py +0 -0
  62. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_autonomous_closegap_1.py +0 -0
  63. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_autonomous_closegap_2.py +0 -0
  64. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_autonomous_closegap_3.py +0 -0
  65. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_autonomous_prompt_inject.py +0 -0
  66. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_boot_bug_regression.py +0 -0
  67. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_color_truecolor.py +0 -0
  68. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_core.py +0 -0
  69. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_cross_agent_messaging.py +0 -0
  70. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_date_parse.py +0 -0
  71. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_doctor.py +0 -0
  72. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_epistemic_v1_python_sdk.py +0 -0
  73. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  74. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_esc_deaf_state.py +0 -0
  75. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_exceptions.py +0 -0
  76. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_file_upload.py +0 -0
  77. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_init_device_code.py +0 -0
  78. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_install_guard.py +0 -0
  79. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_lease_sigterm_release.py +0 -0
  80. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_live_mesh_guard.py +0 -0
  81. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_mark_read_batch.py +0 -0
  82. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_marketplace_ratings.py +0 -0
  83. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_migration_integrity.py +0 -0
  84. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_realtime_event_freshness.py +0 -0
  85. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_rls_cross_tenant.py +0 -0
  86. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_rpc_grants.py +0 -0
  87. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_rpc_migrations.py +0 -0
  88. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_run_agent_dry_run.py +0 -0
  89. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_run_agent_no_server_import.py +0 -0
  90. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_security_regressions.py +0 -0
  91. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_self_update_user_site.py +0 -0
  92. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_sentinel.py +0 -0
  93. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_session_replay_gate.py +0 -0
  94. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_setup_path.py +0 -0
  95. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_sleep_signals.py +0 -0
  96. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_status_enum_coverage.py +0 -0
  97. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_stay_on_loop_hook.py +0 -0
  98. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_stop_ghost_terminal.py +0 -0
  99. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_swarm_events.py +0 -0
  100. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_task_progress.py +0 -0
  101. {meshcode-2.11.135 → meshcode-2.11.136}/tests/test_terminal_lifecycle.py +0 -0
  102. {meshcode-2.11.135 → meshcode-2.11.136}/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.135
3
+ Version: 2.11.136
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.135"
2
+ __version__ = "2.11.136"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -1330,6 +1330,46 @@ def _own_ancestry_pids() -> set:
1330
1330
  return anc
1331
1331
 
1332
1332
 
1333
+ def _run_token_rx(target: str):
1334
+ """`meshcode run <target>` token regex accepting BOTH spawn forms (task 3ed8781d).
1335
+
1336
+ BARE-NAME GAP (the Windows stop-por-RPC zombie, Samuel 2026-06-11): hostd spawns
1337
+ `run "project/agent"` but the FE Start click goes through protocol_handler's
1338
+ cmd_launch_batch, which spawns `run "agent"` (bare). The old per-call-site token
1339
+ (`run <project/agent>`) could NEVER match a bare-spawned session, so stop/force-kill/
1340
+ reap/reconcile discovery silently found nothing for protocol-launched agents — the
1341
+ session survived every Stop, the relaunch opened a NEW wt tab on top, and Samuel's
1342
+ box accumulated up to 3 sessions/tabs per agent.
1343
+
1344
+ - target 'proj/agent': matches `run [\"']?(proj/)?agent` — both qualified and bare.
1345
+ - target 'agent' (bare, the protocol reconcile caller): matches `run [\"']?(<any>/)?agent`
1346
+ — both forms again, since a survivor may have been hostd-spawned.
1347
+ All forms keep the optional opening quote (QUOTED-TARGET FIX 14e0760c) and the
1348
+ trailing lookahead, so 'qa' can never prefix-match 'qa-2'."""
1349
+ proj, _, agent = target.rpartition("/")
1350
+ if proj:
1351
+ pfx = r"(?:" + re.escape(proj) + r"/)?"
1352
+ else:
1353
+ pfx = r"(?:[^\s\"']*/)?"
1354
+ return re.compile(r"run\s+[\"']?" + pfx + re.escape(agent) + r"(?=\s|$|[\"'])")
1355
+
1356
+
1357
+ def _bare_match_is_foreign(cl: str, cwd: str, target: str) -> bool:
1358
+ """Cross-project guard for BARE-token matches (task 3ed8781d). When `target` is
1359
+ project-qualified but the candidate cmdline only carries the bare agent name,
1360
+ a same-named agent of ANOTHER mesh on this box could collide. The workspace
1361
+ cwd (`~/meshcode/<project>-<agent>`) disambiguates: if the cwd basename looks
1362
+ like a workspace of this agent but a DIFFERENT project, skip it. Unreadable
1363
+ cwd or qualified-token matches are never rejected (best-effort guard only)."""
1364
+ proj, _, agent = target.rpartition("/")
1365
+ if not proj or f"{proj}/{agent}" in (cl or ""):
1366
+ return False # qualified match — unambiguous
1367
+ # separator-agnostic last path component (a Windows cwd seen from tests/tools
1368
+ # running under POSIX must split the same way)
1369
+ base = re.split(r"[\\/]", (cwd or "").rstrip("/\\"))[-1]
1370
+ return base.endswith(f"-{agent}") and base != f"{proj}-{agent}"
1371
+
1372
+
1333
1373
  def _discover_agent_pids(target: str) -> list:
1334
1374
  """Fallback PID discovery by command line, for agents spawned before this hostd
1335
1375
  (no recorded PID) or after a state-file loss. Matches `meshcode run <target>`.
@@ -1353,9 +1393,16 @@ def _discover_agent_pids(target: str) -> list:
1353
1393
  (`run\\s+<target>`) NEVER matched on Windows — stop/force-kill discovery silently
1354
1394
  found nothing, which is how stopped sessions survived as zombies. The token now
1355
1395
  accepts one optional opening quote; the trailing lookahead still forbids any
1356
- 'qa' vs 'qa-2' prefix collision."""
1396
+ 'qa' vs 'qa-2' prefix collision.
1397
+
1398
+ BARE-NAME FIX (task 3ed8781d): the token now comes from _run_token_rx, which ALSO
1399
+ matches protocol-handler bare-name spawns (`run "agent"`) — the form every FE Start
1400
+ click produces. Bare matches are cwd-guarded against same-named agents of another
1401
+ mesh (psutil path only; the native fallbacks accept the regex as-is, documented
1402
+ tradeoff: a zombie that survives every stop is the live bug, the cross-mesh
1403
+ same-name collision is the rare one)."""
1357
1404
  pids = []
1358
- tok = re.compile(r"run\s+[\"']?" + re.escape(target) + r"(?=\s|$|[\"'])")
1405
+ tok = _run_token_rx(target)
1359
1406
  ps = _psutil()
1360
1407
  if ps is not None:
1361
1408
  try:
@@ -1366,10 +1413,23 @@ def _discover_agent_pids(target: str) -> list:
1366
1413
  continue
1367
1414
  cl = " ".join(p.cmdline() or [])
1368
1415
  if "meshcode" in cl and tok.search(cl) and "python" in (p.name() or "").lower():
1416
+ try:
1417
+ cwd = p.cwd() or ""
1418
+ except Exception:
1419
+ cwd = ""
1420
+ if _bare_match_is_foreign(cl, cwd, target):
1421
+ _log(f"DISCOVER {target}: pid {p.pid} bare-name match belongs to "
1422
+ f"another project (cwd {cwd!r}) — skip")
1423
+ continue
1369
1424
  pids.append(p.pid)
1370
1425
  except Exception:
1371
1426
  continue
1372
- return pids
1427
+ # GHOST-TERMINAL fix (91201315) APPLIES HERE TOO (task 3ed8781d): the old
1428
+ # `return pids` skipped the POSIX launcher-children fallback whenever
1429
+ # psutil was importable — re-defeating the '0 stopped forever' hole on
1430
+ # every box with psutil. Fall through to the shared tail instead.
1431
+ return pids + ([p for p in _discover_launcher_child_pids(target)
1432
+ if p not in pids] if sys.platform != "win32" else [])
1373
1433
  except Exception:
1374
1434
  pids = [] # psutil enumeration itself broke — fall through to native
1375
1435
  try:
@@ -1389,8 +1449,11 @@ def _discover_agent_pids(target: str) -> list:
1389
1449
  pass
1390
1450
  block_pid = None
1391
1451
  else:
1452
+ # task 3ed8781d: pattern WITHOUT the target so bare-name spawns
1453
+ # (`run "agent"`) are candidates too; the exact tok filter below
1454
+ # keeps the kill set tight.
1392
1455
  out = subprocess.run(
1393
- ["pgrep", "-f", f"meshcode run {target}"],
1456
+ ["pgrep", "-f", "meshcode.* run "],
1394
1457
  capture_output=True, text=True, timeout=8).stdout
1395
1458
  for ln in out.split():
1396
1459
  try:
@@ -1474,12 +1537,17 @@ def _discover_serve_pids(target: str) -> list:
1474
1537
  sibling agent's serve (same project, different agent) can never match. psutil-only
1475
1538
  (environ() needs a real process handle); without psutil returns [] — degraded but
1476
1539
  safe, and the session-root tree kill still takes any serve that is STILL parented
1477
- under the session."""
1540
+ under the session.
1541
+
1542
+ BARE-TARGET support (task 3ed8781d): a bare `agent` target (the protocol-handler
1543
+ relaunch reconcile) matches on MESHCODE_AGENT alone — the relaunch click means
1544
+ 'fresh session of THIS agent on this box', so any project's orphan of that name
1545
+ is a survivor to clear. Qualified targets keep the exact two-field match."""
1478
1546
  ps = _psutil()
1479
1547
  if ps is None:
1480
1548
  return []
1481
- proj, _, agent = target.partition("/")
1482
- if not proj or not agent:
1549
+ proj, _, agent = target.rpartition("/")
1550
+ if not agent:
1483
1551
  return []
1484
1552
  own = _own_pid()
1485
1553
  out = []
@@ -1492,7 +1560,8 @@ def _discover_serve_pids(target: str) -> list:
1492
1560
  if "meshcode" not in cl or "serve" not in cl or "meshcode_mcp" not in cl:
1493
1561
  continue
1494
1562
  env = p.environ() or {}
1495
- if env.get("MESHCODE_PROJECT") == proj and env.get("MESHCODE_AGENT") == agent:
1563
+ if env.get("MESHCODE_AGENT") == agent and \
1564
+ (not proj or env.get("MESHCODE_PROJECT") == proj):
1496
1565
  out.append(p.pid)
1497
1566
  except Exception:
1498
1567
  continue # process vanished / access denied — never block the sweep
@@ -1507,10 +1576,16 @@ def _kill_heartbeat_fork(target: str) -> None:
1507
1576
  would keep POSTing and show a stopped agent 'online'. Stop it by its pidfile
1508
1577
  heartbeat_<proj>_<name>.pid. Best-effort."""
1509
1578
  try:
1510
- proj, _, agent = target.partition("/")
1579
+ proj, _, agent = target.rpartition("/")
1511
1580
  if not agent:
1512
1581
  return
1513
- pidf = STATE_DIR / f"heartbeat_{proj}_{agent}.pid"
1582
+ if proj:
1583
+ pidf = STATE_DIR / f"heartbeat_{proj}_{agent}.pid"
1584
+ else:
1585
+ # bare target (task 3ed8781d, protocol-handler reconcile): project
1586
+ # unknown — glob the agent's pidfile across projects (first hit).
1587
+ cands = sorted(STATE_DIR.glob(f"heartbeat_*_{agent}.pid"))
1588
+ pidf = cands[0] if cands else STATE_DIR / f"heartbeat__{agent}.pid"
1514
1589
  if not pidf.exists():
1515
1590
  return
1516
1591
  try:
@@ -1542,15 +1617,25 @@ def _write_stop_marker(target: str) -> None:
1542
1617
  (6/6 needed SIGKILL), so the wrapper sees rc=137 — indistinguishable from
1543
1618
  a crash by exit code alone. The wrapper treats a non-zero rc as a CLEAN
1544
1619
  stop (closes its own tab) only when this marker exists and is fresh;
1545
- real crashes have no marker and keep their scrollback open. Best-effort."""
1620
+ real crashes have no marker and keep their scrollback open. Best-effort.
1621
+
1622
+ DUAL-LABEL FIX (task 3ed8781d, Windows tab accumulation): the launcher script
1623
+ computes its marker name from ITS OWN spawn cmd — hostd spawns carry
1624
+ 'project/agent' (label 'project_agent') but protocol-handler launch-batch spawns
1625
+ carry the BARE agent name (label 'agent'). A stop that only stamped the
1626
+ target-form marker left a bare-spawned wrapper seeing rc!=0 WITHOUT its marker
1627
+ -> `pause` -> dead wt tab held open forever; relaunch then added a fresh tab
1628
+ (Samuel: 'demasiadas tabs'). Stamp BOTH labels — both are consumed/GC'd."""
1546
1629
  try:
1547
- # EXACT mirror of protocol_handler._launcher_label's sanitization —
1548
- # the wrapper computes the same name at generation time.
1549
- safe = re.sub(r"[^A-Za-z0-9_.-]", "_", target.strip()).strip("_")[:80]
1550
- if not safe:
1551
- return
1552
1630
  STATE_DIR.mkdir(parents=True, exist_ok=True)
1553
- (STATE_DIR / f"stopmark_{safe}").write_text(str(time.time()), encoding="utf-8")
1631
+ labels = {target.strip(), target.rpartition("/")[2].strip()}
1632
+ for raw in labels:
1633
+ # EXACT mirror of protocol_handler._launcher_label's sanitization —
1634
+ # the wrapper computes the same name at generation time.
1635
+ safe = re.sub(r"[^A-Za-z0-9_.-]", "_", raw).strip("_")[:80]
1636
+ if not safe:
1637
+ continue
1638
+ (STATE_DIR / f"stopmark_{safe}").write_text(str(time.time()), encoding="utf-8")
1554
1639
  except Exception:
1555
1640
  pass
1556
1641
 
@@ -1569,9 +1654,10 @@ def _session_root_pid(target: str, pid: int) -> int:
1569
1654
  ps = _psutil()
1570
1655
  if ps is None:
1571
1656
  return pid
1572
- # optional opening quote: the Windows spawn cmdline quotes the target (see
1573
- # _discover_agent_pids QUOTED-TARGET FIX) same token everywhere.
1574
- tok = re.compile(r"run\s+[\"']?" + re.escape(target) + r"(?=\s|$|[\"'])")
1657
+ # Same token everywhere (task 3ed8781d): _run_token_rx accepts the quoted AND the
1658
+ # bare-name spawn forms, so the climb also reaches the wrapper of a protocol-
1659
+ # handler-launched session (`run "agent"`).
1660
+ tok = _run_token_rx(target)
1575
1661
  anc = _own_ancestry_pids()
1576
1662
  root = pid
1577
1663
  try:
@@ -1047,6 +1047,7 @@ def cmd_launch_batch(agent_names: Iterable[str], repo_path: Optional[str] = None
1047
1047
 
1048
1048
  launched: list[str] = []
1049
1049
  skipped: list[dict] = []
1050
+ reconciled: dict = {} # agent -> surviving session trees killed pre-spawn (3ed8781d)
1050
1051
 
1051
1052
  # Resolve `meshcode` binary path (CLI wrapper installed by pip).
1052
1053
  mc_bin = shutil.which("meshcode") or "meshcode"
@@ -1094,6 +1095,22 @@ def cmd_launch_batch(agent_names: Iterable[str], repo_path: Optional[str] = None
1094
1095
  skipped.append({"agent": name,
1095
1096
  "reason": f"cooldown {_LAUNCH_COOLDOWN_S}s (recently launched)"})
1096
1097
  continue
1098
+ # RELAUNCH RECONCILIATION (task 3ed8781d — the hostd respawn path has had this
1099
+ # since 14e0760c, but THIS path — every FE Start click — never did): a
1100
+ # stopped-but-surviving session (stale heartbeat, so DEDUP 1 doesn't see it)
1101
+ # would get a fresh session spawned ON TOP, splitting the inbox and stacking
1102
+ # wt tabs (Samuel's Windows box: up to 3 sessions/tabs per agent). Kill any
1103
+ # surviving session tree of this agent BEFORE opening the new one — strictly
1104
+ # pre-spawn, so the fresh session can never be in the kill set. hostd's
1105
+ # machinery (token discovery + tree kill + stop-marker so the old tab
1106
+ # self-closes) accepts bare names since task 3ed8781d. Best-effort.
1107
+ try:
1108
+ from meshcode import hostd as _hostd
1109
+ _rec = _hostd._reconcile_target(name)
1110
+ if _rec:
1111
+ reconciled[name] = _rec
1112
+ except Exception:
1113
+ pass
1097
1114
  # PER-PLATFORM quoting (mesh-core FIX2): cmd.exe wants double-quotes, not POSIX shlex
1098
1115
  # single-quotes (cmd.exe passes single-quotes through literally -> file-not-found).
1099
1116
  if sys.platform == "win32":
@@ -1113,6 +1130,8 @@ def cmd_launch_batch(agent_names: Iterable[str], repo_path: Optional[str] = None
1113
1130
  "agents": launched,
1114
1131
  "skipped": skipped,
1115
1132
  }
1133
+ if reconciled:
1134
+ out["reconciled"] = reconciled
1116
1135
  print(json.dumps(out))
1117
1136
  return 0 if not skipped else 1
1118
1137
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.135
3
+ Version: 2.11.136
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.11.135"
7
+ version = "2.11.136"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -34,13 +34,17 @@ class FakeNoSuchProcess(Exception):
34
34
 
35
35
 
36
36
  class FakeProc:
37
- def __init__(self, table, pid, ppid, cmdline, name="", env=None):
37
+ def __init__(self, table, pid, ppid, cmdline, name="", env=None, cwd=""):
38
38
  self._table = table
39
39
  self.pid = pid
40
40
  self._ppid = ppid
41
41
  self._cmdline = cmdline
42
42
  self._name = name or (cmdline.split()[0] if cmdline else "")
43
43
  self._env = env or {}
44
+ self._cwd = cwd
45
+
46
+ def cwd(self):
47
+ return self._cwd
44
48
 
45
49
  # --- psutil.Process API surface used by hostd ---
46
50
  def cmdline(self):
@@ -81,8 +85,8 @@ class FakePsutil:
81
85
  self.live = set()
82
86
  self.killed = [] # kill order, for pre-spawn ordering assertions
83
87
 
84
- def add(self, pid, ppid, cmdline, name="", env=None):
85
- self.procs[pid] = FakeProc(self, pid, ppid, cmdline, name=name, env=env)
88
+ def add(self, pid, ppid, cmdline, name="", env=None, cwd=""):
89
+ self.procs[pid] = FakeProc(self, pid, ppid, cmdline, name=name, env=env, cwd=cwd)
86
90
  self.live.add(pid)
87
91
  return self.procs[pid]
88
92
 
@@ -427,5 +431,139 @@ class DiscoveryTests(ZombieFixBase):
427
431
  self.assertEqual(hostd._discover_agent_pids(TARGET), [qa["run"]])
428
432
 
429
433
 
434
+ class BareNameSpawnTests(ZombieFixBase):
435
+ """task 3ed8781d — the Windows stop-por-RPC + relaunch zombie. protocol_handler's
436
+ cmd_launch_batch (every FE Start click) spawns `meshcode run "agent"` (BARE name);
437
+ the old per-site token (`run <project/agent>`) never matched it, so stop/reap/
438
+ reconcile found nothing -> sessions + wt tabs stacked up to 3 per agent."""
439
+
440
+ def add_bare_session(self, base, agent="qa", cwd=""):
441
+ """Protocol-handler-shaped tree: cmd wrapper runs the launcher SCRIPT (no
442
+ token in its cmdline post-a4001d59) -> python `run "<agent>"` (bare)."""
443
+ self.ps.add(50, 1, "WindowsTerminal.exe", name="WindowsTerminal.exe")
444
+ self.ps.add(base, 50,
445
+ f'cmd /c C:\\u\\.meshcode\\launchers\\{agent}.cmd',
446
+ name="cmd.exe")
447
+ self.ps.add(base + 1, base, f'C:\\py\\python.exe -m meshcode run "{agent}"',
448
+ name="python.exe", cwd=cwd)
449
+ self.ps.add(base + 2, base + 1, "C:\\claude\\claude.exe --session x",
450
+ name="claude.exe")
451
+ return {"cmd": base, "run": base + 1, "claude": base + 2}
452
+
453
+ def test_qualified_target_discovers_bare_spawn(self):
454
+ """LOAD-BEARING: hostd stop sweeps (target 'proj/agent') must find a
455
+ protocol-launched session whose cmdline is `run "agent"`."""
456
+ self.add_hostd()
457
+ s = self.add_bare_session(100)
458
+ self.assertEqual(hostd._discover_agent_pids(TARGET), [s["run"]])
459
+
460
+ def test_bare_target_discovers_qualified_spawn(self):
461
+ """Reverse direction: the protocol-handler relaunch reconcile (bare 'qa')
462
+ must find a hostd-spawned session (`run "mesh-core/qa"`)."""
463
+ self.add_hostd()
464
+ qa = self.add_session(100)
465
+ self.assertEqual(hostd._discover_agent_pids("qa"), [qa["run"]])
466
+
467
+ def test_bare_forms_keep_prefix_safety(self):
468
+ """'qa' must never match 'qa-2' in either form."""
469
+ self.add_hostd()
470
+ self.ps.add(601, 1, 'python.exe -m meshcode run "qa-2"', name="python.exe")
471
+ self.ps.add(602, 1, 'python.exe -m meshcode run "mesh-core/qa-2"', name="python.exe")
472
+ self.assertEqual(hostd._discover_agent_pids(TARGET), [])
473
+ self.assertEqual(hostd._discover_agent_pids("qa"), [])
474
+
475
+ def test_bare_match_foreign_project_cwd_is_skipped(self):
476
+ """Cross-mesh guard: a bare-spawned 'qa' whose cwd is ANOTHER project's
477
+ workspace (~/meshcode/<other>-qa) is not killable via 'mesh-core/qa'."""
478
+ self.add_hostd()
479
+ self.add_bare_session(100, cwd="C:\\u\\meshcode\\other-mesh-qa")
480
+ self.assertEqual(hostd._discover_agent_pids(TARGET), [])
481
+
482
+ def test_bare_match_own_project_cwd_is_accepted(self):
483
+ self.add_hostd()
484
+ s = self.add_bare_session(100, cwd="C:\\u\\meshcode\\mesh-core-qa")
485
+ self.assertEqual(hostd._discover_agent_pids(TARGET), [s["run"]])
486
+
487
+ def test_stop_kills_bare_spawned_session_tree(self):
488
+ """End-to-end: desired_state='stopped' takes down the protocol-launched
489
+ tree (python run + claude); the launcher cmd wrapper SURVIVES by design
490
+ (no token in its cmdline) to run the stopmark epilogue -> tab self-closes."""
491
+ self.add_hostd()
492
+ s = self.add_bare_session(100)
493
+ self.rpc.script("mc_agents_to_stop",
494
+ {"ok": True, "agents": [{"project_name": "mesh-core", "agent": "qa"}]})
495
+ n = hostd._do_stops("k", "h")
496
+ self.assertEqual(n, 1)
497
+ self.assertNotIn(s["run"], self.ps.live, "bare-spawned run survived the stop")
498
+ self.assertNotIn(s["claude"], self.ps.live, "bare-spawned claude survived the stop")
499
+ self.assertIn(50, self.ps.live, "terminal host was killed")
500
+
501
+ def test_serve_discovery_bare_target_matches_agent_env(self):
502
+ self.ps.add(200, 1, "python.exe -m meshcode.meshcode_mcp serve", name="python.exe",
503
+ env={"MESHCODE_PROJECT": "mesh-core", "MESHCODE_AGENT": "qa"})
504
+ self.assertEqual(hostd._discover_serve_pids("qa"), [200])
505
+ self.assertEqual(hostd._discover_serve_pids("backend"), [])
506
+
507
+
508
+ class StopMarkerDualLabelTests(unittest.TestCase):
509
+ """task 3ed8781d — _write_stop_marker must stamp BOTH the target-form label
510
+ (hostd spawns) and the bare-agent label (protocol-handler spawns): the launcher
511
+ script checks the label derived from ITS OWN spawn cmd, and a missing marker
512
+ flips an intentional stop into a `pause`-held dead wt tab."""
513
+
514
+ def test_writes_both_labels(self):
515
+ import tempfile
516
+ from pathlib import Path
517
+ with tempfile.TemporaryDirectory() as td:
518
+ with mock.patch.object(hostd, "STATE_DIR", Path(td)):
519
+ hostd._write_stop_marker("mesh-core/qa")
520
+ names = sorted(p.name for p in Path(td).glob("stopmark_*"))
521
+ self.assertEqual(names, ["stopmark_mesh-core_qa", "stopmark_qa"])
522
+
523
+ def test_bare_target_writes_single_label(self):
524
+ import tempfile
525
+ from pathlib import Path
526
+ with tempfile.TemporaryDirectory() as td:
527
+ with mock.patch.object(hostd, "STATE_DIR", Path(td)):
528
+ hostd._write_stop_marker("qa")
529
+ names = sorted(p.name for p in Path(td).glob("stopmark_*"))
530
+ self.assertEqual(names, ["stopmark_qa"])
531
+
532
+
533
+ class LaunchBatchReconcileTests(unittest.TestCase):
534
+ """task 3ed8781d — cmd_launch_batch (every FE Start click / launch-url) must
535
+ reconcile surviving sessions pre-spawn, like the hostd respawn path has since
536
+ 14e0760c."""
537
+
538
+ def _run(self, reconcile_side_effect):
539
+ from meshcode import protocol_handler as ph
540
+ import json as _json
541
+ printed = []
542
+ with mock.patch.object(ph, "live_agent_names", lambda names, project=None: set()), \
543
+ mock.patch.object(ph, "_read_cooldowns", lambda: {}), \
544
+ mock.patch.object(ph, "_record_spawn", lambda n, now=None: None), \
545
+ mock.patch.object(ph, "_spawn_terminal", lambda cmd: (True, "wt(fleet-tab)")), \
546
+ mock.patch.object(hostd, "_reconcile_target", reconcile_side_effect), \
547
+ mock.patch("builtins.print", lambda *a, **k: printed.append(a[0] if a else "")):
548
+ rc = ph.cmd_launch_batch(["qa"])
549
+ return rc, _json.loads(printed[-1])
550
+
551
+ def test_reconcile_called_before_spawn_and_reported(self):
552
+ calls = []
553
+ rc, out = self._run(lambda name: calls.append(name) or 2)
554
+ self.assertEqual(rc, 0)
555
+ self.assertEqual(calls, ["qa"])
556
+ self.assertEqual(out.get("reconciled"), {"qa": 2})
557
+ self.assertEqual(out["agents"], ["qa"])
558
+
559
+ def test_reconcile_failure_never_blocks_launch(self):
560
+ def boom(name):
561
+ raise RuntimeError("psutil exploded")
562
+ rc, out = self._run(boom)
563
+ self.assertEqual(rc, 0)
564
+ self.assertEqual(out["agents"], ["qa"])
565
+ self.assertNotIn("reconciled", out)
566
+
567
+
430
568
  if __name__ == "__main__":
431
569
  unittest.main()
File without changes
File without changes
File without changes
File without changes