meshcode 2.11.120__tar.gz → 2.11.122__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 (94) hide show
  1. {meshcode-2.11.120 → meshcode-2.11.122}/PKG-INFO +1 -1
  2. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/hostd.py +141 -0
  4. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/meshcode_mcp/realtime.py +13 -1
  5. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/meshcode_mcp/server.py +30 -19
  6. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/meshcode_mcp/sleep_signals.py +44 -1
  7. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode.egg-info/PKG-INFO +1 -1
  8. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode.egg-info/SOURCES.txt +1 -0
  9. {meshcode-2.11.120 → meshcode-2.11.122}/pyproject.toml +1 -1
  10. meshcode-2.11.122/tests/test_swarm_events.py +115 -0
  11. {meshcode-2.11.120 → meshcode-2.11.122}/README.md +0 -0
  12. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/__main__.py +0 -0
  13. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/_session_handoff_template.py +0 -0
  14. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/_stop_hook_template.py +0 -0
  15. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/ascii_art.py +0 -0
  16. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/atomic_push.py +0 -0
  17. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/claude_update.py +0 -0
  18. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/cli.py +0 -0
  19. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/comms_v4.py +0 -0
  20. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/compat.py +0 -0
  21. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/daemon.py +0 -0
  22. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/date_parse.py +0 -0
  23. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/doctor.py +0 -0
  24. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/error_hints.py +0 -0
  25. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/exceptions.py +0 -0
  26. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/hooks/__init__.py +0 -0
  27. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/hooks/repo_path_lock.py +0 -0
  28. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/invites.py +0 -0
  29. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/launcher.py +0 -0
  30. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/launcher_install.py +0 -0
  31. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/meshcode_mcp/__init__.py +0 -0
  32. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/meshcode_mcp/__main__.py +0 -0
  33. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/meshcode_mcp/backend.py +0 -0
  34. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/meshcode_mcp/test_backend.py +0 -0
  35. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  36. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  37. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  38. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  39. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  40. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/preferences.py +0 -0
  41. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/protocol_handler.py +0 -0
  42. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/protocol_v2.py +0 -0
  43. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/quickstart.py +0 -0
  44. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/rpc_allowlist.py +0 -0
  45. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/run_agent.py +0 -0
  46. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/scripts/check_secrets.py +0 -0
  47. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/scripts/race_rate_harness.py +0 -0
  48. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/secrets.py +0 -0
  49. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/self_update.py +0 -0
  50. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/setup_clients.py +0 -0
  51. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/supervisor.py +0 -0
  52. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/up.py +0 -0
  53. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode/upload.py +0 -0
  54. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode.egg-info/dependency_links.txt +0 -0
  55. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode.egg-info/entry_points.txt +0 -0
  56. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode.egg-info/requires.txt +0 -0
  57. {meshcode-2.11.120 → meshcode-2.11.122}/meshcode.egg-info/top_level.txt +0 -0
  58. {meshcode-2.11.120 → meshcode-2.11.122}/setup.cfg +0 -0
  59. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_auto_update_hardening.py +0 -0
  60. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_autonomous_closegap_1.py +0 -0
  61. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_autonomous_closegap_2.py +0 -0
  62. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_autonomous_closegap_3.py +0 -0
  63. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_autonomous_prompt_inject.py +0 -0
  64. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_boot_bug_regression.py +0 -0
  65. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_color_truecolor.py +0 -0
  66. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_core.py +0 -0
  67. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_cross_agent_messaging.py +0 -0
  68. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_date_parse.py +0 -0
  69. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_doctor.py +0 -0
  70. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_epistemic_v1_python_sdk.py +0 -0
  71. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  72. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_esc_deaf_state.py +0 -0
  73. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_exceptions.py +0 -0
  74. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_file_upload.py +0 -0
  75. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_init_device_code.py +0 -0
  76. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_install_guard.py +0 -0
  77. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_lease_sigterm_release.py +0 -0
  78. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_mark_read_batch.py +0 -0
  79. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_marketplace_ratings.py +0 -0
  80. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_migration_integrity.py +0 -0
  81. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_realtime_event_freshness.py +0 -0
  82. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_rls_cross_tenant.py +0 -0
  83. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_rpc_grants.py +0 -0
  84. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_rpc_migrations.py +0 -0
  85. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_run_agent_dry_run.py +0 -0
  86. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_run_agent_no_server_import.py +0 -0
  87. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_security_regressions.py +0 -0
  88. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_self_update_user_site.py +0 -0
  89. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_sentinel.py +0 -0
  90. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_setup_path.py +0 -0
  91. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_sleep_signals.py +0 -0
  92. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_status_enum_coverage.py +0 -0
  93. {meshcode-2.11.120 → meshcode-2.11.122}/tests/test_stay_on_loop_hook.py +0 -0
  94. {meshcode-2.11.120 → meshcode-2.11.122}/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.120
3
+ Version: 2.11.122
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.120"
2
+ __version__ = "2.11.122"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -548,10 +548,21 @@ def _do_respawns(api_key: str, host_id: str) -> int:
548
548
  # q1: never touch the fresh terminal). Every recycle is visible now (headless
549
549
  # removed, task cba01de5), so this always runs for a recycle.
550
550
  _old_vis_pids = _discover_agent_pids(_target) if _is_recycle else []
551
+ # DEAD-WINDOW REAP (task 70ac6d25, Samuel hit it twice 2026-06-09): an
552
+ # exit-for-respawn rotation takes THIS plain-respawn path, and the old
553
+ # window — whose launch script predates the .119 in-script self-closer,
554
+ # or whose agent died without rc=0 — lingers as "[Process completed]".
555
+ # Snapshot the target's ALREADY-DEAD launcher windows BEFORE spawning;
556
+ # close them only after the fresh spawn succeeds. The new window can
557
+ # never be in the set: it's busy AND post-snapshot. Live windows
558
+ # (busy=true) are never matched, so a healthy agent is untouchable.
559
+ _dead_wins = _list_dead_terminal_windows(_target)
551
560
  _log(f"{'RECYCLE-RESPAWN' if _is_recycle else 'RESPAWN'} {proj}/{agent} "
552
561
  f"(stale {c.get('heartbeat_age_s')}s, count={c.get('respawn_count')})")
553
562
  if _spawn_agent(proj, agent, repo_path=c.get("repo_path")):
554
563
  _record_spawn(_target) # count the terminal we just opened, against the breaker
564
+ if _dead_wins:
565
+ _close_dead_terminal_windows(_target, _dead_wins)
555
566
  if _is_recycle:
556
567
  if _old_vis_pids:
557
568
  _close_old_visible_recycle(_target, _old_vis_pids) # close old window (DRY-RUN first)
@@ -719,6 +730,71 @@ def _close_old_visible_recycle(target: str, old_pids) -> int:
719
730
  return n
720
731
 
721
732
 
733
+ def _terminal_window_marker(target: str) -> str:
734
+ """Title marker of `target`'s launcher windows. protocol_handler spawns via
735
+ ~/.meshcode/launchers/<label>.command (label = _launcher_label's sanitized
736
+ 'proj/agent'), and macOS Terminal keeps that filename in the window title
737
+ for the window's whole life — verified live 2026-06-10 (running:
738
+ '… ◂ meshcode-self-improve_backend.command — 80×24'; dead windows keep it
739
+ too). Mirrors _launcher_label's sanitization so the marker can't drift."""
740
+ safe = re.sub(r"[^A-Za-z0-9_.-]", "_", target.strip()).strip("_")
741
+ return (safe[:80] + ".command") if safe else ""
742
+
743
+
744
+ def _list_dead_terminal_windows(target: str) -> list:
745
+ """macOS: ids of Terminal windows that belonged to `target`'s launcher and
746
+ whose tab is NO LONGER BUSY — the '[Process completed]' orphans a rotation
747
+ leaves behind (task 70ac6d25). busy=false means no process runs in the tab,
748
+ so a live agent can never match; the marker filter keeps every non-meshcode
749
+ window (user shells, other apps' tabs) out of reach. The marker is built
750
+ from a sanitized charset ([A-Za-z0-9_.-]) so it can't inject AppleScript.
751
+ Returns [] on non-macOS, no Automation permission, or any error."""
752
+ if sys.platform != "darwin":
753
+ return []
754
+ marker = _terminal_window_marker(target)
755
+ if not marker:
756
+ return []
757
+ script = (
758
+ 'set out to ""\n'
759
+ 'tell application "Terminal"\n'
760
+ ' repeat with w in (every window)\n'
761
+ ' try\n'
762
+ f' if (busy of selected tab of w) is false and (name of w contains "{marker}") then\n'
763
+ ' set out to out & (id of w) & linefeed\n'
764
+ ' end if\n'
765
+ ' end try\n'
766
+ ' end repeat\n'
767
+ 'end tell\n'
768
+ 'return out')
769
+ try:
770
+ out = subprocess.run(["/usr/bin/osascript", "-e", script],
771
+ capture_output=True, text=True, timeout=10).stdout or ""
772
+ return [int(x) for x in out.split() if x.strip().isdigit()]
773
+ except Exception:
774
+ return []
775
+
776
+
777
+ def _close_dead_terminal_windows(target: str, window_ids) -> int:
778
+ """Close (saving no) a pre-collected set of dead launcher windows. The ids
779
+ come from _list_dead_terminal_windows BEFORE the fresh spawn, so the new
780
+ window is excluded by construction (same snapshot discipline as
781
+ _close_old_visible_recycle). 'every window whose id is N' no-ops harmlessly
782
+ if the user already closed it. Best-effort; returns count closed."""
783
+ n = 0
784
+ for wid in window_ids:
785
+ try:
786
+ r = subprocess.run(
787
+ ["/usr/bin/osascript", "-e",
788
+ f'tell application "Terminal" to close (every window whose id is {int(wid)}) saving no'],
789
+ capture_output=True, text=True, timeout=10)
790
+ if r.returncode == 0:
791
+ _log(f"DEAD-WINDOW-REAP {target}: closed dead terminal window id {wid} (rotation orphan)")
792
+ n += 1
793
+ except Exception:
794
+ pass
795
+ return n
796
+
797
+
722
798
  def _spawn_rate_ok(target: str):
723
799
  """Anti-spam circuit breaker. Returns (ok, tripped_burst, reason).
724
800
 
@@ -1016,6 +1092,16 @@ def _do_force_kills(api_key: str, host_id: str) -> int:
1016
1092
  pids.pop(target, None)
1017
1093
  n += 1
1018
1094
  _log(f"FORCE-KILL {target}: killed visible pid {pid} (explicit human stop)")
1095
+ # DEAD-WINDOW REAP (task 70ac6d25): the kill leaves the window as
1096
+ # "[Process completed]" — the human pressed Stop, so finish the job.
1097
+ # Brief settle so the process tree exits and busy flips false.
1098
+ try:
1099
+ time.sleep(1.5)
1100
+ _wins = _list_dead_terminal_windows(target)
1101
+ if _wins:
1102
+ _close_dead_terminal_windows(target, _wins)
1103
+ except Exception:
1104
+ pass
1019
1105
  st["headless_pids"] = pids
1020
1106
  _save_state(st)
1021
1107
  return n
@@ -1676,6 +1762,52 @@ def _do_version_recycles(api_key: str, host_id: str) -> int:
1676
1762
  return n
1677
1763
 
1678
1764
 
1765
+ # c301be69: hold the singleton lock handle for the daemon's lifetime — module
1766
+ # global so it's never GC'd (GC would close the fd and release the lock).
1767
+ _HOSTD_LOCK_FH = None
1768
+
1769
+
1770
+ def _acquire_hostd_singleton():
1771
+ """One hostd per machine (task c301be69 — duplicate-daemon race observed
1772
+ live 2026-06-09: a manual `nohup meshcode hostd run` raced auto-bootstrap,
1773
+ TWO daemons ran ~30s = double respawn/recycle sweeps = storm class).
1774
+
1775
+ Exclusive flock on ~/.meshcode/hostd.lock, held for the process lifetime.
1776
+ The OS releases it on ANY death (crash, kill -9), so there is no
1777
+ stale-pidfile problem; the pid written inside is informational only.
1778
+ Python opens fds CLOEXEC (PEP 446), so the self-reexec-on-version-drift
1779
+ path releases the lock at exec time and the fresh image re-acquires it.
1780
+
1781
+ Returns (fh, status): status in 'acquired' | 'held' | 'error'.
1782
+ 'error' means the lock could not be evaluated — callers FAIL OPEN
1783
+ (one possibly-duplicate daemon beats no daemon at all)."""
1784
+ try:
1785
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
1786
+ fh = open(STATE_DIR / "hostd.lock", "a+")
1787
+ try:
1788
+ if sys.platform == "win32":
1789
+ import msvcrt
1790
+ fh.seek(0)
1791
+ msvcrt.locking(fh.fileno(), msvcrt.LK_NBLCK, 1)
1792
+ else:
1793
+ import fcntl
1794
+ fcntl.flock(fh.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
1795
+ except OSError:
1796
+ fh.close()
1797
+ return (None, "held")
1798
+ try:
1799
+ fh.seek(0)
1800
+ fh.truncate()
1801
+ fh.write(str(os.getpid()))
1802
+ fh.flush()
1803
+ except Exception:
1804
+ pass
1805
+ return (fh, "acquired")
1806
+ except Exception as e:
1807
+ _log(f"WARN singleton lock unavailable ({e}) — proceeding unlocked (fail-open)")
1808
+ return (None, "error")
1809
+
1810
+
1679
1811
  def cmd_hostd(args: list) -> int:
1680
1812
  """Entry point for `meshcode hostd ...`."""
1681
1813
  if not args or args[0] in ("-h", "--help"):
@@ -1704,6 +1836,15 @@ def cmd_hostd(args: list) -> int:
1704
1836
  if not api_key:
1705
1837
  _log("FATAL: no api key — run `meshcode login` (key is read from the keychain)")
1706
1838
  return 1
1839
+ # SINGLETON (task c301be69): second instance exits 0 with one log line.
1840
+ # Covers the manual-run vs auto-bootstrap race; _hostd_bootstrap_if_needed
1841
+ # honors it implicitly — its spawned `hostd run` hits this gate and exits.
1842
+ global _HOSTD_LOCK_FH
1843
+ _lk, _lk_status = _acquire_hostd_singleton()
1844
+ if _lk_status == "held":
1845
+ _log("hostd already running on this machine (hostd.lock held) — exiting (singleton c301be69)")
1846
+ return 0
1847
+ _HOSTD_LOCK_FH = _lk # may be None on 'error' (fail-open)
1707
1848
  # P1 hostd_CRASH_LOOP: the detached Windows daemon has NO console, so crashes (incl hard
1708
1849
  # faults / uncaught exceptions) went to INVISIBLE stderr — hostd.log showed nothing. Capture
1709
1850
  # stderr + faulthandler tracebacks to a file + install an excepthook so the NEXT crash is
@@ -347,7 +347,19 @@ class RealtimeListener:
347
347
  self.message_event.set()
348
348
  except Exception:
349
349
  pass
350
- log.info(f"new message from {enriched['from']}")
350
+ # ENJAMBRE G3 (task 3b0e4129): the swarm barrier trigger
351
+ # (mig 471) delivers via this same mc_messages INSERT path
352
+ # (the runtime has no PG conn for LISTEN mc_swarm_events).
353
+ # Log it explicitly so "did the parent wake instantly on
354
+ # drain?" is answerable from the agent log alone.
355
+ _pl = enriched.get("payload")
356
+ if isinstance(_pl, dict) and _pl.get("type") == "swarm_barrier_complete":
357
+ log.info(
358
+ f"swarm_barrier_complete (swarm={_pl.get('swarm_id')}, "
359
+ f"parent_task={_pl.get('parent_task_id')}) — instant wake for {self.agent_name}"
360
+ )
361
+ else:
362
+ log.info(f"new message from {enriched['from']}")
351
363
  if self.notify_callback:
352
364
  try:
353
365
  await self.notify_callback(enriched)
@@ -590,9 +590,11 @@ def _filter_and_mark(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
590
590
  from .sleep_signals import ( # noqa: F401
591
591
  _SLEEP_PAYLOAD_TYPES,
592
592
  _SLEEP_TEXT_MARKERS,
593
+ _SWARM_EVENT_TYPES,
593
594
  _KNOWN_HUMAN_HANDLES,
594
595
  _is_human_authored,
595
596
  _looks_like_sleep_signal,
597
+ _looks_like_swarm_event,
596
598
  _split_messages,
597
599
  )
598
600
 
@@ -4107,11 +4109,14 @@ def _try_auto_complete_on_ship(payload: Dict[str, Any]) -> Optional[Dict[str, An
4107
4109
  return None
4108
4110
 
4109
4111
 
4110
- def _try_auto_complete_on_done_status(task_hint: str) -> Optional[Dict[str, Any]]:
4111
- """CLOSEGAP-3 sibling: when set_status='done', complete the single
4112
- in_progress task if there's exactly one. No-op for 0 or >1 active.
4113
- Summary built from the task_hint + a fallback note.
4114
- """
4112
+ def _try_auto_park_on_done_status(task_hint: str, status: str = "done") -> Optional[Dict[str, Any]]:
4113
+ """CLOSEGAP-3 v2 (task 53767993): when set_status='done'/'sleeping' with
4114
+ exactly ONE in_progress claim, PARK it roll back to 'claimed' with a
4115
+ checkpoint note (mc_task_checkpoint_park, mig 476) NEVER complete it.
4116
+ Live incident 2026-06-09T23:30Z: the old auto-COMPLETE closed 250b7c1e as
4117
+ done with zero work at an ordered exit-for-respawn (board lied to Samuel;
4118
+ re-opened as 7a467894). status='done' may now ONLY come from an explicit
4119
+ meshcode_task_complete. No-op for 0 or >1 active claims."""
4115
4120
  try:
4116
4121
  api_key = _get_api_key()
4117
4122
  if not api_key:
@@ -4126,24 +4131,30 @@ def _try_auto_complete_on_done_status(task_hint: str) -> Optional[Dict[str, Any]
4126
4131
  if len(active) != 1:
4127
4132
  return None
4128
4133
  target = active[0]
4129
- summary = (
4130
- f"AUTO-CHECKPOINT (CLOSEGAP-3): agent flipped set_status='done'. "
4131
- f"Task hint: {(task_hint or '(none)')[:200]}. "
4132
- f"No ship-report content capturedset_status payload-only."
4134
+ note = (
4135
+ f"CHECKPOINT (CLOSEGAP-3 park): agent flipped set_status='{status}' "
4136
+ f"mid-claim (rotation/sleep). Task hint: {(task_hint or '(none)')[:200]}. "
4137
+ f"Work NOT verified completeresuming agent must task_complete explicitly."
4133
4138
  )
4134
- result = be.task_complete(api_key, _PROJECT_ID, target["id"], AGENT_NAME, summary=summary)
4139
+ result = be.sb_rpc("mc_task_checkpoint_park", {
4140
+ "p_api_key": api_key,
4141
+ "p_project_id": _PROJECT_ID,
4142
+ "p_task_id": target["id"],
4143
+ "p_agent": AGENT_NAME,
4144
+ "p_note": note,
4145
+ })
4135
4146
  if not (isinstance(result, dict) and result.get("ok")):
4136
4147
  return None
4137
4148
  try:
4138
4149
  _log_activity_bg(
4139
4150
  "auto_checkpoint",
4140
- f"{AGENT_NAME} auto-completed task {target['id'][:8]} on set_status=done",
4151
+ f"{AGENT_NAME} PARKED task {target['id'][:8]} on set_status={status} (claimed again, NOT done)",
4141
4152
  )
4142
4153
  except Exception:
4143
4154
  pass
4144
- return {"task_id": target["id"][:8], "title": str(target.get("title") or "")[:80]}
4155
+ return {"task_id": target["id"][:8], "title": str(target.get("title") or "")[:80], "parked": True}
4145
4156
  except Exception as e:
4146
- log.debug(f"[meshcode] auto-checkpoint on done-status failed: {e}")
4157
+ log.debug(f"[meshcode] auto-park on done-status failed: {e}")
4147
4158
  return None
4148
4159
 
4149
4160
 
@@ -5077,15 +5088,15 @@ def meshcode_set_status(status: str, task: str = "") -> Dict[str, Any]:
5077
5088
  }
5078
5089
  except Exception:
5079
5090
  pass # best-effort; never block set_status
5080
- # CLOSEGAP-3 (task 69dc5a62, Samuel reframe ce506e0d): when the
5081
- # agent flips set_status='done' with exactly one active in_progress
5082
- # claim, auto-complete that claim with a status-driven summary.
5091
+ # CLOSEGAP-3 v2 (task 53767993, was 69dc5a62): when the agent flips
5092
+ # set_status='done'/'sleeping' with exactly one active in_progress claim,
5093
+ # PARK that claim (back to 'claimed' + checkpoint note) — never complete.
5083
5094
  # Baseline (NOT autonomous-gated). Best-effort.
5084
- if status == "done" and isinstance(resp, dict) and resp.get("ok"):
5095
+ if status in ("done", "sleeping") and isinstance(resp, dict) and resp.get("ok"):
5085
5096
  try:
5086
- _cp = _try_auto_complete_on_done_status(task)
5097
+ _cp = _try_auto_park_on_done_status(task, status)
5087
5098
  if _cp:
5088
- resp["auto_checkpointed_task"] = _cp
5099
+ resp["auto_parked_task"] = _cp
5089
5100
  except Exception:
5090
5101
  pass
5091
5102
  return resp
@@ -56,6 +56,38 @@ _SLEEP_TEXT_MARKERS = (
56
56
  # server + hook share authority on who counts as a human user.
57
57
  _KNOWN_HUMAN_HANDLES = frozenset({"sammybenu", "samuel", "sam"})
58
58
 
59
+ # ── ENJAMBRE G3 (task 3b0e4129) — swarm barrier events ──────────────────────
60
+ # The mc_swarm_barrier_check trigger (mig 471) fires when a swarm's tray
61
+ # drains. The runtime has NO raw Postgres connection (PostgREST + Realtime
62
+ # websocket only), so a literal LISTEN on the 'mc_swarm_events' pg_notify
63
+ # channel is impossible — which is why the emitter ALSO inserts an
64
+ # mc_messages row to the parent (mig 471 R4). That row already rides the
65
+ # existing wait transport: Realtime postgres_changes INSERT → message_event
66
+ # wake → instant delivery (DB-poll fallback covers the no-websocket case).
67
+ #
68
+ # This layer adds the SEMANTIC half: classify those payloads into a
69
+ # first-class `swarm_events` field (plus an actionable hint) so the parent
70
+ # agent unambiguously sees "barrier complete → aggregate + close parent"
71
+ # instead of a generic system message. Events stay in `messages` too —
72
+ # every existing wake/refuse/count consumer keeps identical behavior.
73
+ _SWARM_EVENT_TYPES = frozenset({"swarm_barrier_complete"})
74
+
75
+ _SWARM_HINT = (
76
+ "swarm barrier complete — the tray is drained. Read each child task's "
77
+ "completion_summary/deliverables (+ scratchpad rollup if used), aggregate, "
78
+ "then meshcode_task_complete the parent task."
79
+ )
80
+
81
+
82
+ def _looks_like_swarm_event(m: Dict[str, Any]) -> bool:
83
+ """True when a message is a structured swarm lifecycle event (server-emitted
84
+ by the barrier trigger). Matches payload.type only — deliberately shaped,
85
+ sender-independent (the emitter writes from_agent='system')."""
86
+ pl = m.get("payload") or {}
87
+ if isinstance(pl, dict):
88
+ return str(pl.get("type", "")).lower() in _SWARM_EVENT_TYPES
89
+ return False
90
+
59
91
 
60
92
  def _is_human_authored(m: Dict[str, Any]) -> bool:
61
93
  """True when the message carries explicit human-user authorship.
@@ -128,6 +160,7 @@ def _split_messages(messages: List[Dict[str, Any]]) -> Dict[str, Any]:
128
160
  real: List[Dict[str, Any]] = []
129
161
  acks: List[Dict[str, Any]] = []
130
162
  dones: List[Dict[str, Any]] = []
163
+ swarm: List[Dict[str, Any]] = []
131
164
  for m in messages:
132
165
  t = m.get("type", "msg")
133
166
  if t == "ack":
@@ -136,9 +169,19 @@ def _split_messages(messages: List[Dict[str, Any]]) -> Dict[str, Any]:
136
169
  dones.append(m)
137
170
  else:
138
171
  real.append(m)
139
- return {
172
+ # ENJAMBRE G3: ALSO surface barrier events first-class. Additive —
173
+ # the event stays in `messages` so wake/refuse/count semantics are
174
+ # byte-identical for every existing consumer; `swarm_events` rides
175
+ # the **split spreads in server.py into every wait/check envelope.
176
+ if _looks_like_swarm_event(m):
177
+ swarm.append(m)
178
+ out = {
140
179
  "messages": real,
141
180
  "acks": acks,
142
181
  "done_signals": dones,
143
182
  "count": len(real),
144
183
  }
184
+ if swarm:
185
+ out["swarm_events"] = swarm
186
+ out["swarm_hint"] = _SWARM_HINT
187
+ return out
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.120
3
+ Version: 2.11.122
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -88,4 +88,5 @@ tests/test_setup_path.py
88
88
  tests/test_sleep_signals.py
89
89
  tests/test_status_enum_coverage.py
90
90
  tests/test_stay_on_loop_hook.py
91
+ tests/test_swarm_events.py
91
92
  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.120"
7
+ version = "2.11.122"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -0,0 +1,115 @@
1
+ """ENJAMBRE G3 runtime (task 3b0e4129) — swarm_barrier_complete delivery.
2
+
3
+ The mc_swarm_barrier_check trigger (mig 471) emits pg_notify('mc_swarm_events')
4
+ AND inserts an mc_messages row to the parent agent (R4 transport — the runtime
5
+ has no raw PG connection, so the inbox row IS the LISTEN-equivalent; Realtime
6
+ postgres_changes on mc_messages wakes meshcode_wait instantly).
7
+
8
+ This suite locks in the semantic layer: _split_messages classifies those
9
+ payloads into a first-class `swarm_events` field + `swarm_hint`, WITHOUT
10
+ changing any existing wake/refuse/count semantics (events stay in `messages`).
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import unittest
15
+
16
+ from meshcode.meshcode_mcp.sleep_signals import (
17
+ _looks_like_swarm_event,
18
+ _split_messages,
19
+ )
20
+
21
+
22
+ def _barrier_msg(swarm_id="11111111-2222-3333-4444-555555555555",
23
+ parent_task_id="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
24
+ sender="system", mtype="system"):
25
+ return {
26
+ "from": sender,
27
+ "type": mtype,
28
+ "payload": {
29
+ "type": "swarm_barrier_complete",
30
+ "swarm_id": swarm_id,
31
+ "parent_task_id": parent_task_id,
32
+ },
33
+ }
34
+
35
+
36
+ def _msg(sender="ai-agent", payload=None, mtype="msg"):
37
+ return {"from": sender, "type": mtype, "payload": payload or {}}
38
+
39
+
40
+ class TestLooksLikeSwarmEvent(unittest.TestCase):
41
+
42
+ def test_barrier_payload_detected(self):
43
+ self.assertTrue(_looks_like_swarm_event(_barrier_msg()))
44
+
45
+ def test_sender_independent(self):
46
+ # The emitter writes from_agent='system', but classification is
47
+ # payload-shaped, not sender-shaped.
48
+ self.assertTrue(_looks_like_swarm_event(_barrier_msg(sender="anything")))
49
+
50
+ def test_plain_system_message_is_not_swarm(self):
51
+ m = _msg(payload={"type": "task_assigned", "task_id": "x"}, mtype="system")
52
+ self.assertFalse(_looks_like_swarm_event(m))
53
+
54
+ def test_text_mention_is_not_swarm(self):
55
+ # Prose ABOUT the barrier is not the event.
56
+ m = _msg(payload={"text": "the swarm_barrier_complete fired earlier"})
57
+ self.assertFalse(_looks_like_swarm_event(m))
58
+
59
+ def test_payload_not_dict(self):
60
+ self.assertFalse(_looks_like_swarm_event({"from": "x", "payload": "swarm_barrier_complete"}))
61
+
62
+ def test_empty_payload(self):
63
+ self.assertFalse(_looks_like_swarm_event(_msg(payload={})))
64
+
65
+
66
+ class TestSplitMessagesSwarmField(unittest.TestCase):
67
+
68
+ def test_barrier_lands_in_swarm_events_and_messages(self):
69
+ out = _split_messages([_barrier_msg()])
70
+ self.assertEqual(len(out["swarm_events"]), 1)
71
+ # Back-compat invariant: the event ALSO stays a real message so every
72
+ # existing wake/refuse path (which checks split["messages"]) still fires.
73
+ self.assertEqual(len(out["messages"]), 1)
74
+ self.assertEqual(out["count"], 1)
75
+ self.assertEqual(len(out["done_signals"]), 0)
76
+
77
+ def test_hint_present_only_with_events(self):
78
+ out = _split_messages([_barrier_msg()])
79
+ self.assertIn("swarm_hint", out)
80
+ self.assertIn("task_complete the parent task", out["swarm_hint"])
81
+
82
+ def test_no_swarm_keys_on_plain_batch(self):
83
+ # Additive contract: envelopes without barrier events are byte-identical
84
+ # to the pre-G3 shape (no empty keys polluting every wait response).
85
+ out = _split_messages([_msg(payload={"text": "normal chatter"})])
86
+ self.assertNotIn("swarm_events", out)
87
+ self.assertNotIn("swarm_hint", out)
88
+
89
+ def test_mixed_batch_classification(self):
90
+ msgs = [
91
+ _msg(sender="other", payload={"text": "normal chatter"}),
92
+ _barrier_msg(),
93
+ _msg(mtype="ack", payload={}),
94
+ _msg(sender="commander", mtype="broadcast", payload={"type": "got_done"}),
95
+ ]
96
+ out = _split_messages(msgs)
97
+ self.assertEqual(len(out["swarm_events"]), 1)
98
+ self.assertEqual(len(out["messages"]), 2) # chatter + barrier
99
+ self.assertEqual(len(out["acks"]), 1)
100
+ self.assertEqual(len(out["done_signals"]), 1)
101
+
102
+ def test_barrier_does_not_trip_must_exit_path(self):
103
+ # A barrier completing must NEVER be confused with a sleep signal.
104
+ out = _split_messages([_barrier_msg()])
105
+ self.assertEqual(len(out["done_signals"]), 0)
106
+
107
+ def test_payload_fields_preserved(self):
108
+ out = _split_messages([_barrier_msg(swarm_id="s-1", parent_task_id="p-1")])
109
+ ev = out["swarm_events"][0]["payload"]
110
+ self.assertEqual(ev["swarm_id"], "s-1")
111
+ self.assertEqual(ev["parent_task_id"], "p-1")
112
+
113
+
114
+ if __name__ == "__main__":
115
+ unittest.main()
File without changes
File without changes
File without changes
File without changes