meshcode 2.11.119__tar.gz → 2.11.121__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {meshcode-2.11.119 → meshcode-2.11.121}/PKG-INFO +1 -1
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/__init__.py +1 -1
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/hostd.py +86 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/meshcode_mcp/realtime.py +13 -1
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/meshcode_mcp/server.py +4 -1
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/meshcode_mcp/sleep_signals.py +44 -1
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode.egg-info/SOURCES.txt +1 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/pyproject.toml +1 -1
- meshcode-2.11.121/tests/test_swarm_events.py +115 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/README.md +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/__main__.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/cli.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/compat.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/daemon.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/doctor.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/hooks/__init__.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/hooks/repo_path_lock.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/invites.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/launcher.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/preferences.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/run_agent.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/secrets.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/self_update.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/up.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode/upload.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/setup.cfg +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_core.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_doctor.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.119 → meshcode-2.11.121}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -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
|
|
@@ -347,7 +347,19 @@ class RealtimeListener:
|
|
|
347
347
|
self.message_event.set()
|
|
348
348
|
except Exception:
|
|
349
349
|
pass
|
|
350
|
-
|
|
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
|
|
|
@@ -2906,7 +2908,8 @@ def meshcode_send(to: Union[str, List[str]], message: Any, in_reply_to: Optional
|
|
|
2906
2908
|
# mode-2 soft-fail (R2-3): a malformed in_reply_to (not a valid uuid) would hit
|
|
2907
2909
|
# the uuid cast at the RPC boundary and reject the WHOLE message. Drop it so the
|
|
2908
2910
|
# message still sends (pairs with mig463's nonexistent-but-valid-uuid soft-fail).
|
|
2909
|
-
|
|
2911
|
+
import re as _re # .119 regression: no module-level `re`; local like lines 3809/3942/4008
|
|
2912
|
+
if in_reply_to is not None and not _re.fullmatch(
|
|
2910
2913
|
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}",
|
|
2911
2914
|
str(in_reply_to)):
|
|
2912
2915
|
in_reply_to = None
|
|
@@ -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
|
-
|
|
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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|