meshcode 2.11.124__tar.gz → 2.11.126__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.124 → meshcode-2.11.126}/PKG-INFO +1 -1
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/__init__.py +1 -1
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/hostd.py +110 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/meshcode_mcp/sleep_signals.py +22 -6
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.124 → meshcode-2.11.126}/pyproject.toml +1 -1
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_sleep_signals.py +52 -6
- {meshcode-2.11.124 → meshcode-2.11.126}/README.md +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/__main__.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/cli.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/compat.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/daemon.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/doctor.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/hooks/__init__.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/hooks/repo_path_lock.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/invites.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/launcher.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/meshcode_mcp/swarm.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/meshcode_mcp/test_swarm.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/preferences.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/run_agent.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/secrets.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/self_update.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/up.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode/upload.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/setup.cfg +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_core.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_doctor.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_swarm_events.py +0 -0
- {meshcode-2.11.124 → meshcode-2.11.126}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -209,6 +209,34 @@ SPAWN_MIN_INTERVAL_SEC = _env_int("MESHCODE_SPAWN_MIN_INTERVAL_SEC", 45, 5) #
|
|
|
209
209
|
SPAWN_BURST_CAP = _env_int("MESHCODE_SPAWN_BURST_CAP", 5, 2) # max spawns ...
|
|
210
210
|
SPAWN_BURST_WINDOW_SEC = _env_int("MESHCODE_SPAWN_BURST_WINDOW_SEC", 600, 60) # ... within this rolling window before tripping
|
|
211
211
|
|
|
212
|
+
# ------------------------------------------------------------------
|
|
213
|
+
# Plain-respawn guards (Samuel P0 2026-06-10, terminal storm): the burst
|
|
214
|
+
# breaker above NEVER saw the overnight storm — hostd respawned zylo-bemate
|
|
215
|
+
# every ~16 min for 13 h (125 terminals): slow boots under load kept resetting
|
|
216
|
+
# the server-side respawn cap (the agent heartbeats briefly, count -> 0), the
|
|
217
|
+
# ~16 min cadence stays under 5 spawns/10 min, and every respawn STACKED a new
|
|
218
|
+
# window on top of the still-alive previous session, adding load that made the
|
|
219
|
+
# next boot even slower. Two guards on the plain-respawn path in _do_respawns:
|
|
220
|
+
#
|
|
221
|
+
# LIVE-SESSION GUARD — never open a second terminal while the previous
|
|
222
|
+
# session's process is still alive: HOLD (spawn nothing), hard-kill the stuck
|
|
223
|
+
# session after _RESPAWN_STUCK_KILL_S, and relaunch only once it is fully
|
|
224
|
+
# gone. An explicit Start (fresh spawned_at) kills the old session at once —
|
|
225
|
+
# a restart the human asked for must first fully close the old terminal.
|
|
226
|
+
#
|
|
227
|
+
# CONVERGENCE GUARD — mirror of the recycle guard (451d33a0) for plain
|
|
228
|
+
# respawns, keyed on time BETWEEN consecutive respawns so it catches ANY
|
|
229
|
+
# cadence: _RESPAWN_CONVERGE_MAX consecutive respawns each started less than
|
|
230
|
+
# _RESPAWN_CONVERGE_LIFETIME_S after the previous one -> BLOCK further
|
|
231
|
+
# respawns for _RESPAWN_CONVERGE_BLOCK_TTL_S + resolve the pending launch as
|
|
232
|
+
# failed (FE toast). An explicit Start clears the block.
|
|
233
|
+
# ------------------------------------------------------------------
|
|
234
|
+
_RESPAWN_STUCK_KILL_S = _env_int("MESHCODE_RESPAWN_STUCK_KILL_SEC", 600, 60) # kill a stale-but-alive session after this hold
|
|
235
|
+
_RESPAWN_CONVERGE_MAX = _env_int("MESHCODE_RESPAWN_CONVERGE_MAX", 3, 2) # consecutive short-lived respawns before blocking
|
|
236
|
+
_RESPAWN_CONVERGE_LIFETIME_S = _env_int("MESHCODE_RESPAWN_CONVERGE_LIFETIME_SEC", 1800, 120) # "short-lived" = next respawn needed within this
|
|
237
|
+
_RESPAWN_CONVERGE_BLOCK_TTL_S = _env_int("MESHCODE_RESPAWN_CONVERGE_BLOCK_TTL_SEC", 21600, 600) # block duration before one clean retry
|
|
238
|
+
_RESPAWN_FRESH_CLICK_S = _env_int("MESHCODE_RESPAWN_FRESH_CLICK_SEC", 90, 30) # spawned_age_s under this = explicit Start click
|
|
239
|
+
|
|
212
240
|
|
|
213
241
|
def _log(msg: str) -> None:
|
|
214
242
|
line = f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}"
|
|
@@ -535,6 +563,88 @@ def _do_respawns(api_key: str, host_id: str) -> int:
|
|
|
535
563
|
_rrall[_target] = _rr
|
|
536
564
|
_rst["recyrespawn"] = _rrall
|
|
537
565
|
_save_state(_rst)
|
|
566
|
+
# ---- plain-respawn guards (Samuel P0 2026-06-10, terminal storm; see
|
|
567
|
+
# ---- constants block above for the full incident rationale) ----
|
|
568
|
+
if not _is_recycle:
|
|
569
|
+
_now2 = time.time()
|
|
570
|
+
_st2 = _load_state()
|
|
571
|
+
try:
|
|
572
|
+
_fresh_click = float(c.get("spawned_age_s")) < _RESPAWN_FRESH_CLICK_S
|
|
573
|
+
except (TypeError, ValueError):
|
|
574
|
+
_fresh_click = False
|
|
575
|
+
_cv_all = dict(_st2.get("respawn_conv") or {})
|
|
576
|
+
_cv = dict(_cv_all.get(_target) or {})
|
|
577
|
+
if _fresh_click and (_cv or _target in (_st2.get("respawn_hold") or {})):
|
|
578
|
+
# explicit Start (fresh spawned_at) = human permission: clear guard state
|
|
579
|
+
_cv = {}
|
|
580
|
+
_cv_all.pop(_target, None)
|
|
581
|
+
_hh = dict(_st2.get("respawn_hold") or {})
|
|
582
|
+
_hh.pop(_target, None)
|
|
583
|
+
_st2["respawn_conv"] = _cv_all
|
|
584
|
+
_st2["respawn_hold"] = _hh
|
|
585
|
+
_save_state(_st2)
|
|
586
|
+
_log(f"RESPAWN-GUARD {_target}: explicit Start — cleared hold/convergence state")
|
|
587
|
+
elif _cv.get("blocked_ts"):
|
|
588
|
+
if (_now2 - float(_cv["blocked_ts"])) < _RESPAWN_CONVERGE_BLOCK_TTL_S:
|
|
589
|
+
_log(f"SKIP respawn {_target}: BLOCKED (respawn_no_converge, "
|
|
590
|
+
f"{_cv.get('count')} short-lived respawns) — holding until TTL or a manual Start")
|
|
591
|
+
continue
|
|
592
|
+
_cv.pop("blocked_ts", None)
|
|
593
|
+
_cv["count"] = 0 # TTL elapsed -> one clean retry; re-blocks if it storms again
|
|
594
|
+
# LIVE-SESSION GUARD: never stack a second terminal on a still-alive session.
|
|
595
|
+
_live = [p for p in _discover_agent_pids(_target) if _pid_alive(p)]
|
|
596
|
+
if _live:
|
|
597
|
+
_hold_all = dict(_st2.get("respawn_hold") or {})
|
|
598
|
+
_hold = dict(_hold_all.get(_target) or {})
|
|
599
|
+
_first = float(_hold.get("first_ts") or _now2)
|
|
600
|
+
if _fresh_click or (_now2 - _first) >= _RESPAWN_STUCK_KILL_S:
|
|
601
|
+
_killed = sum(1 for _p in _live if _kill_headless_pid(_target, _p))
|
|
602
|
+
_hold_all.pop(_target, None)
|
|
603
|
+
_log(f"RESPAWN-STUCK-KILL {_target}: closed {_killed}/{len(_live)} stale-but-alive "
|
|
604
|
+
f"session pid(s) {_live} "
|
|
605
|
+
f"({'explicit Start' if _fresh_click else 'stuck > ' + str(_RESPAWN_STUCK_KILL_S) + 's'}) "
|
|
606
|
+
f"— relaunch on next sweep once fully gone")
|
|
607
|
+
else:
|
|
608
|
+
_hold["first_ts"] = _first
|
|
609
|
+
_hold_all[_target] = _hold
|
|
610
|
+
_log(f"RESPAWN-HOLD {_target}: previous session still ALIVE (pids {_live}) — "
|
|
611
|
+
f"NOT opening another terminal; stuck-kill in "
|
|
612
|
+
f"{int(_RESPAWN_STUCK_KILL_S - (_now2 - _first))}s unless it heartbeats")
|
|
613
|
+
_st2["respawn_hold"] = _hold_all
|
|
614
|
+
_save_state(_st2)
|
|
615
|
+
continue
|
|
616
|
+
# previous session fully gone — count this relaunch against convergence
|
|
617
|
+
_hold_all = dict(_st2.get("respawn_hold") or {})
|
|
618
|
+
if _target in _hold_all:
|
|
619
|
+
_hold_all.pop(_target, None)
|
|
620
|
+
_st2["respawn_hold"] = _hold_all
|
|
621
|
+
if _cv.get("last_ts") and (_now2 - float(_cv["last_ts"])) <= _RESPAWN_CONVERGE_LIFETIME_S:
|
|
622
|
+
_cv["count"] = int(_cv.get("count", 0)) + 1
|
|
623
|
+
else:
|
|
624
|
+
_cv = {"count": 1} # converged/idle long enough -> fresh count
|
|
625
|
+
_cv["last_ts"] = _now2
|
|
626
|
+
if _cv["count"] >= _RESPAWN_CONVERGE_MAX:
|
|
627
|
+
_cv["blocked_ts"] = _now2
|
|
628
|
+
_cv_all[_target] = _cv
|
|
629
|
+
_st2["respawn_conv"] = _cv_all
|
|
630
|
+
_save_state(_st2)
|
|
631
|
+
_log(f"ALERT {_target}: respawn NOT CONVERGING — {_cv['count']} consecutive respawns "
|
|
632
|
+
f"each <{_RESPAWN_CONVERGE_LIFETIME_S}s apart; BLOCKING respawns for "
|
|
633
|
+
f"{_RESPAWN_CONVERGE_BLOCK_TTL_S}s (a manual Start clears the block). "
|
|
634
|
+
f"[respawn_blocked_reason=respawn_no_converge]")
|
|
635
|
+
try: # stop the dashboard's eternal 'launching…' spinner + actionable toast
|
|
636
|
+
_rpc("mc_resolve_launch", {
|
|
637
|
+
"p_api_key": api_key, "p_project_id": c.get("project_id"), "p_agent": agent,
|
|
638
|
+
"p_status": "failed", "p_reason": "respawn_no_converge",
|
|
639
|
+
"p_detail": f"agent kept dying or booting too slowly {_cv['count']}x in a row — "
|
|
640
|
+
f"respawns paused to protect your machine and tokens; press Start "
|
|
641
|
+
f"to retry when ready"})
|
|
642
|
+
except Exception:
|
|
643
|
+
pass
|
|
644
|
+
continue
|
|
645
|
+
_cv_all[_target] = _cv
|
|
646
|
+
_st2["respawn_conv"] = _cv_all
|
|
647
|
+
_save_state(_st2)
|
|
538
648
|
_ok, _burst, _why = _spawn_rate_ok(_target)
|
|
539
649
|
if not _ok:
|
|
540
650
|
_log(f"SKIP {'recycle-' if _is_recycle else ''}respawn {_target}: rate-limited ({_why})")
|
|
@@ -136,18 +136,32 @@ def _is_human_authored(m: Dict[str, Any]) -> bool:
|
|
|
136
136
|
return False
|
|
137
137
|
|
|
138
138
|
|
|
139
|
+
def _sender_may_order_sleep(m: Dict[str, Any]) -> bool:
|
|
140
|
+
"""Samuel directive 2026-06-10 (msg 54eec209): a RUNNING agent is only
|
|
141
|
+
slept by an order from the USER or the COMMANDER. True when the message
|
|
142
|
+
is human-authored OR carries the SERVER-VERIFIED commander stamp
|
|
143
|
+
(payload.sender_is_commander, mig 483 — stamped by mc_send_message, never
|
|
144
|
+
client-claimed). A plain ai peer can no longer must_exit a sibling."""
|
|
145
|
+
if _is_human_authored(m):
|
|
146
|
+
return True
|
|
147
|
+
pl = m.get("payload") or {}
|
|
148
|
+
return isinstance(pl, dict) and pl.get("sender_is_commander") is True
|
|
149
|
+
|
|
150
|
+
|
|
139
151
|
def _looks_like_sleep_signal(m: Dict[str, Any]) -> bool:
|
|
140
152
|
"""Detect mesh messages that authorize the wait-loop exit.
|
|
141
153
|
|
|
142
154
|
See module docstring for the two valid encodings and the rationale
|
|
143
|
-
for ignoring idiom matches from AI-role senders.
|
|
155
|
+
for ignoring idiom matches from AI-role senders. Since mig 483 + .125,
|
|
156
|
+
structured directives additionally require an authorized sender
|
|
157
|
+
(human or server-stamped commander) — see _sender_may_order_sleep.
|
|
144
158
|
"""
|
|
145
159
|
pl = m.get("payload") or {}
|
|
146
160
|
if isinstance(pl, dict):
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if
|
|
150
|
-
return
|
|
161
|
+
structured = (str(pl.get("type", "")).lower() in _SLEEP_PAYLOAD_TYPES
|
|
162
|
+
or str(pl.get("directive", "")).lower() in _SLEEP_PAYLOAD_TYPES)
|
|
163
|
+
if structured:
|
|
164
|
+
return _sender_may_order_sleep(m)
|
|
151
165
|
text = str(pl.get("text", "")).lower()
|
|
152
166
|
if text and any(marker in text for marker in _SLEEP_TEXT_MARKERS):
|
|
153
167
|
if _is_human_authored(m) and _human_text_is_directive(text):
|
|
@@ -194,7 +208,9 @@ def _split_messages(messages: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
|
194
208
|
t = m.get("type", "msg")
|
|
195
209
|
if t == "ack":
|
|
196
210
|
acks.append(m)
|
|
197
|
-
elif (t == "done"
|
|
211
|
+
elif ((t == "done" and _sender_may_order_sleep(m)) or _looks_like_sleep_signal(m)) \
|
|
212
|
+
and _recent_enough(m):
|
|
213
|
+
# type='done' rows are sleep-class too — same sender gate applies
|
|
198
214
|
dones.append(m)
|
|
199
215
|
else:
|
|
200
216
|
real.append(m)
|
|
@@ -77,17 +77,23 @@ class TestStructuredDirective(unittest.TestCase):
|
|
|
77
77
|
of sender — these are deliberately-shaped directives."""
|
|
78
78
|
|
|
79
79
|
def test_got_done_broadcast_from_commander(self):
|
|
80
|
+
# post-mig-483 reality: the server stamps sender_is_commander
|
|
80
81
|
m = _msg(sender="mesh-commander", mtype="broadcast",
|
|
81
|
-
payload={"type": "got_done"})
|
|
82
|
+
payload={"type": "got_done", "sender_is_commander": True})
|
|
82
83
|
self.assertTrue(_looks_like_sleep_signal(m))
|
|
83
84
|
|
|
84
85
|
def test_payload_type_sleep_from_ai(self):
|
|
86
|
+
# POLICY CHANGE (54eec209): bare ai sleep directive is ignored
|
|
85
87
|
m = _msg(sender="any-agent", payload={"type": "sleep"})
|
|
86
|
-
self.
|
|
88
|
+
self.assertFalse(_looks_like_sleep_signal(m))
|
|
87
89
|
|
|
88
90
|
def test_payload_directive_shutdown(self):
|
|
91
|
+
# POLICY CHANGE (54eec209): a plain ai peer cannot order shutdown
|
|
89
92
|
m = _msg(sender="any-agent", payload={"directive": "shutdown"})
|
|
90
|
-
self.
|
|
93
|
+
self.assertFalse(_looks_like_sleep_signal(m))
|
|
94
|
+
m2 = _msg(sender="any-agent",
|
|
95
|
+
payload={"directive": "shutdown", "sender_is_commander": True})
|
|
96
|
+
self.assertTrue(_looks_like_sleep_signal(m2))
|
|
91
97
|
|
|
92
98
|
def test_payload_type_unknown_is_not_sleep(self):
|
|
93
99
|
m = _msg(sender="any-agent", payload={"type": "msg"})
|
|
@@ -132,7 +138,7 @@ class TestSplitMessages(unittest.TestCase):
|
|
|
132
138
|
|
|
133
139
|
def test_got_done_broadcast_lands_in_dones(self):
|
|
134
140
|
msgs = [_msg(sender="mesh-commander", mtype="broadcast",
|
|
135
|
-
payload={"type": "got_done"})]
|
|
141
|
+
payload={"type": "got_done", "sender_is_commander": True})]
|
|
136
142
|
out = _split_messages(msgs)
|
|
137
143
|
self.assertEqual(len(out["done_signals"]), 1)
|
|
138
144
|
|
|
@@ -146,7 +152,8 @@ class TestSplitMessages(unittest.TestCase):
|
|
|
146
152
|
msgs = [
|
|
147
153
|
_msg(sender="ai", payload={"text": "a dormir is what Samuel said"}),
|
|
148
154
|
_msg(sender="sammybenu", payload={"text": "a dormir"}),
|
|
149
|
-
_msg(sender="commander", mtype="broadcast",
|
|
155
|
+
_msg(sender="commander", mtype="broadcast",
|
|
156
|
+
payload={"type": "got_done", "sender_is_commander": True}),
|
|
150
157
|
_msg(sender="other", payload={"text": "normal chatter"}),
|
|
151
158
|
_msg(mtype="ack", payload={}),
|
|
152
159
|
]
|
|
@@ -204,5 +211,44 @@ class TestCasualSpanishComplaintGuards(unittest.TestCase):
|
|
|
204
211
|
def test_structured_directive_untouched_by_guards(self):
|
|
205
212
|
# dashboard-button path: payload.type fires regardless of text shape
|
|
206
213
|
m = _msg(sender="mesh-commander",
|
|
207
|
-
payload={"type": "got_done", "text": self.REPRO
|
|
214
|
+
payload={"type": "got_done", "text": self.REPRO,
|
|
215
|
+
"sender_is_commander": True})
|
|
216
|
+
self.assertTrue(_looks_like_sleep_signal(m))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class TestOnlyCommanderOrHumanMayOrderSleep(unittest.TestCase):
|
|
220
|
+
"""Samuel directive 2026-06-10 (msg 54eec209) + mig 483: structured sleep
|
|
221
|
+
directives and type='done' messages from a plain AI peer must NOT
|
|
222
|
+
must_exit; only humans or the server-stamped commander may order sleep."""
|
|
223
|
+
|
|
224
|
+
def test_ai_peer_structured_sleep_ignored(self):
|
|
225
|
+
m = _msg(sender="backend2", payload={"type": "got_done", "sender_role": "ai"})
|
|
226
|
+
self.assertFalse(_looks_like_sleep_signal(m))
|
|
227
|
+
|
|
228
|
+
def test_commander_stamp_authorizes(self):
|
|
229
|
+
m = _msg(sender="mesh-commander",
|
|
230
|
+
payload={"type": "got_done", "sender_role": "ai",
|
|
231
|
+
"sender_is_commander": True})
|
|
208
232
|
self.assertTrue(_looks_like_sleep_signal(m))
|
|
233
|
+
|
|
234
|
+
def test_client_claimed_stamp_string_rejected(self):
|
|
235
|
+
# only boolean True (server-stamped) counts
|
|
236
|
+
m = _msg(sender="rogue", payload={"type": "stop", "sender_is_commander": "true"})
|
|
237
|
+
self.assertFalse(_looks_like_sleep_signal(m))
|
|
238
|
+
|
|
239
|
+
def test_human_structured_still_works(self):
|
|
240
|
+
m = _msg(sender="sammybenu", payload={"type": "sleep"})
|
|
241
|
+
self.assertTrue(_looks_like_sleep_signal(m))
|
|
242
|
+
|
|
243
|
+
def test_done_type_from_ai_peer_not_sleep(self):
|
|
244
|
+
m = _msg(sender="backend2", mtype="done",
|
|
245
|
+
payload={"text": "task xyz done", "sender_role": "ai"})
|
|
246
|
+
split = _split_messages([m])
|
|
247
|
+
self.assertEqual(len(split["done_signals"]), 0)
|
|
248
|
+
self.assertEqual(len(split["messages"]), 1)
|
|
249
|
+
|
|
250
|
+
def test_done_type_from_commander_is_sleep(self):
|
|
251
|
+
m = _msg(sender="mesh-commander", mtype="done",
|
|
252
|
+
payload={"text": "got_done", "sender_is_commander": True})
|
|
253
|
+
split = _split_messages([m])
|
|
254
|
+
self.assertEqual(len(split["done_signals"]), 1)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|