meshcode 2.11.121__tar.gz → 2.11.123__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.121 → meshcode-2.11.123}/PKG-INFO +1 -1
  2. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/hostd.py +64 -1
  4. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/meshcode_mcp/server.py +49 -19
  5. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/meshcode_mcp/sleep_signals.py +30 -1
  6. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode.egg-info/PKG-INFO +1 -1
  7. {meshcode-2.11.121 → meshcode-2.11.123}/pyproject.toml +1 -1
  8. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_sleep_signals.py +48 -0
  9. {meshcode-2.11.121 → meshcode-2.11.123}/README.md +0 -0
  10. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/__main__.py +0 -0
  11. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/_session_handoff_template.py +0 -0
  12. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/_stop_hook_template.py +0 -0
  13. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/ascii_art.py +0 -0
  14. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/atomic_push.py +0 -0
  15. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/claude_update.py +0 -0
  16. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/cli.py +0 -0
  17. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/comms_v4.py +0 -0
  18. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/compat.py +0 -0
  19. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/daemon.py +0 -0
  20. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/date_parse.py +0 -0
  21. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/doctor.py +0 -0
  22. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/error_hints.py +0 -0
  23. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/exceptions.py +0 -0
  24. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/hooks/__init__.py +0 -0
  25. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/hooks/repo_path_lock.py +0 -0
  26. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/invites.py +0 -0
  27. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/launcher.py +0 -0
  28. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/launcher_install.py +0 -0
  29. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/meshcode_mcp/__init__.py +0 -0
  30. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/meshcode_mcp/__main__.py +0 -0
  31. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/meshcode_mcp/backend.py +0 -0
  32. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/meshcode_mcp/realtime.py +0 -0
  33. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/meshcode_mcp/test_backend.py +0 -0
  34. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  35. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  36. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  37. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  38. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  39. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/preferences.py +0 -0
  40. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/protocol_handler.py +0 -0
  41. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/protocol_v2.py +0 -0
  42. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/quickstart.py +0 -0
  43. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/rpc_allowlist.py +0 -0
  44. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/run_agent.py +0 -0
  45. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/scripts/check_secrets.py +0 -0
  46. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/scripts/race_rate_harness.py +0 -0
  47. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/secrets.py +0 -0
  48. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/self_update.py +0 -0
  49. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/setup_clients.py +0 -0
  50. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/supervisor.py +0 -0
  51. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/up.py +0 -0
  52. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode/upload.py +0 -0
  53. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode.egg-info/SOURCES.txt +0 -0
  54. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode.egg-info/dependency_links.txt +0 -0
  55. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode.egg-info/entry_points.txt +0 -0
  56. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode.egg-info/requires.txt +0 -0
  57. {meshcode-2.11.121 → meshcode-2.11.123}/meshcode.egg-info/top_level.txt +0 -0
  58. {meshcode-2.11.121 → meshcode-2.11.123}/setup.cfg +0 -0
  59. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_auto_update_hardening.py +0 -0
  60. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_autonomous_closegap_1.py +0 -0
  61. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_autonomous_closegap_2.py +0 -0
  62. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_autonomous_closegap_3.py +0 -0
  63. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_autonomous_prompt_inject.py +0 -0
  64. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_boot_bug_regression.py +0 -0
  65. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_color_truecolor.py +0 -0
  66. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_core.py +0 -0
  67. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_cross_agent_messaging.py +0 -0
  68. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_date_parse.py +0 -0
  69. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_doctor.py +0 -0
  70. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_epistemic_v1_python_sdk.py +0 -0
  71. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  72. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_esc_deaf_state.py +0 -0
  73. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_exceptions.py +0 -0
  74. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_file_upload.py +0 -0
  75. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_init_device_code.py +0 -0
  76. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_install_guard.py +0 -0
  77. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_lease_sigterm_release.py +0 -0
  78. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_mark_read_batch.py +0 -0
  79. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_marketplace_ratings.py +0 -0
  80. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_migration_integrity.py +0 -0
  81. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_realtime_event_freshness.py +0 -0
  82. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_rls_cross_tenant.py +0 -0
  83. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_rpc_grants.py +0 -0
  84. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_rpc_migrations.py +0 -0
  85. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_run_agent_dry_run.py +0 -0
  86. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_run_agent_no_server_import.py +0 -0
  87. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_security_regressions.py +0 -0
  88. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_self_update_user_site.py +0 -0
  89. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_sentinel.py +0 -0
  90. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_setup_path.py +0 -0
  91. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_status_enum_coverage.py +0 -0
  92. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_stay_on_loop_hook.py +0 -0
  93. {meshcode-2.11.121 → meshcode-2.11.123}/tests/test_swarm_events.py +0 -0
  94. {meshcode-2.11.121 → meshcode-2.11.123}/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.121
3
+ Version: 2.11.123
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.121"
2
+ __version__ = "2.11.123"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -467,7 +467,15 @@ def _do_respawns(api_key: str, host_id: str) -> int:
467
467
  if not _BOOT_AUTOSTART:
468
468
  _spawn_age = c.get("spawned_age_s")
469
469
  _hostd_uptime = time.time() - _HOSTD_STARTED_AT
470
- if _spawn_age is None or _spawn_age >= _hostd_uptime:
470
+ # ALIVE-ON-OUR-WATCH bypass (task baefc8ab part C — live miss 2026-06-10T00:31Z:
471
+ # front-end exited for respawn minutes after a hostd restart; its spawned_at predated
472
+ # the restart, the gate read it as a boot-stale leftover and ATE the respawn until a
473
+ # manual Start). An agent that HEARTBEATED during this daemon's lifetime is not a
474
+ # reboot leftover — it lived and died on our watch, so its respawn is legitimate.
475
+ # A true boot-stale leftover has heartbeat_age_s >= uptime (or None).
476
+ _hb_age = c.get("heartbeat_age_s")
477
+ _alive_on_our_watch = _hb_age is not None and _hb_age < _hostd_uptime
478
+ if (_spawn_age is None or _spawn_age >= _hostd_uptime) and not _alive_on_our_watch:
471
479
  _log(f"BOOT-AUTOSTART OFF: skip auto-respawn {proj}/{agent} (desired_state=running set "
472
480
  f"before this hostd start — boot-stale leftover; explicit launch required). "
473
481
  f"Set MESHCODE_BOOT_AUTOSTART=1 to auto-launch at boot.")
@@ -1762,6 +1770,52 @@ def _do_version_recycles(api_key: str, host_id: str) -> int:
1762
1770
  return n
1763
1771
 
1764
1772
 
1773
+ # c301be69: hold the singleton lock handle for the daemon's lifetime — module
1774
+ # global so it's never GC'd (GC would close the fd and release the lock).
1775
+ _HOSTD_LOCK_FH = None
1776
+
1777
+
1778
+ def _acquire_hostd_singleton():
1779
+ """One hostd per machine (task c301be69 — duplicate-daemon race observed
1780
+ live 2026-06-09: a manual `nohup meshcode hostd run` raced auto-bootstrap,
1781
+ TWO daemons ran ~30s = double respawn/recycle sweeps = storm class).
1782
+
1783
+ Exclusive flock on ~/.meshcode/hostd.lock, held for the process lifetime.
1784
+ The OS releases it on ANY death (crash, kill -9), so there is no
1785
+ stale-pidfile problem; the pid written inside is informational only.
1786
+ Python opens fds CLOEXEC (PEP 446), so the self-reexec-on-version-drift
1787
+ path releases the lock at exec time and the fresh image re-acquires it.
1788
+
1789
+ Returns (fh, status): status in 'acquired' | 'held' | 'error'.
1790
+ 'error' means the lock could not be evaluated — callers FAIL OPEN
1791
+ (one possibly-duplicate daemon beats no daemon at all)."""
1792
+ try:
1793
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
1794
+ fh = open(STATE_DIR / "hostd.lock", "a+")
1795
+ try:
1796
+ if sys.platform == "win32":
1797
+ import msvcrt
1798
+ fh.seek(0)
1799
+ msvcrt.locking(fh.fileno(), msvcrt.LK_NBLCK, 1)
1800
+ else:
1801
+ import fcntl
1802
+ fcntl.flock(fh.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
1803
+ except OSError:
1804
+ fh.close()
1805
+ return (None, "held")
1806
+ try:
1807
+ fh.seek(0)
1808
+ fh.truncate()
1809
+ fh.write(str(os.getpid()))
1810
+ fh.flush()
1811
+ except Exception:
1812
+ pass
1813
+ return (fh, "acquired")
1814
+ except Exception as e:
1815
+ _log(f"WARN singleton lock unavailable ({e}) — proceeding unlocked (fail-open)")
1816
+ return (None, "error")
1817
+
1818
+
1765
1819
  def cmd_hostd(args: list) -> int:
1766
1820
  """Entry point for `meshcode hostd ...`."""
1767
1821
  if not args or args[0] in ("-h", "--help"):
@@ -1790,6 +1844,15 @@ def cmd_hostd(args: list) -> int:
1790
1844
  if not api_key:
1791
1845
  _log("FATAL: no api key — run `meshcode login` (key is read from the keychain)")
1792
1846
  return 1
1847
+ # SINGLETON (task c301be69): second instance exits 0 with one log line.
1848
+ # Covers the manual-run vs auto-bootstrap race; _hostd_bootstrap_if_needed
1849
+ # honors it implicitly — its spawned `hostd run` hits this gate and exits.
1850
+ global _HOSTD_LOCK_FH
1851
+ _lk, _lk_status = _acquire_hostd_singleton()
1852
+ if _lk_status == "held":
1853
+ _log("hostd already running on this machine (hostd.lock held) — exiting (singleton c301be69)")
1854
+ return 0
1855
+ _HOSTD_LOCK_FH = _lk # may be None on 'error' (fail-open)
1793
1856
  # P1 hostd_CRASH_LOOP: the detached Windows daemon has NO console, so crashes (incl hard
1794
1857
  # faults / uncaught exceptions) went to INVISIBLE stderr — hostd.log showed nothing. Capture
1795
1858
  # stderr + faulthandler tracebacks to a file + install an excepthook so the NEXT crash is
@@ -4109,11 +4109,14 @@ def _try_auto_complete_on_ship(payload: Dict[str, Any]) -> Optional[Dict[str, An
4109
4109
  return None
4110
4110
 
4111
4111
 
4112
- def _try_auto_complete_on_done_status(task_hint: str) -> Optional[Dict[str, Any]]:
4113
- """CLOSEGAP-3 sibling: when set_status='done', complete the single
4114
- in_progress task if there's exactly one. No-op for 0 or >1 active.
4115
- Summary built from the task_hint + a fallback note.
4116
- """
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."""
4117
4120
  try:
4118
4121
  api_key = _get_api_key()
4119
4122
  if not api_key:
@@ -4128,24 +4131,30 @@ def _try_auto_complete_on_done_status(task_hint: str) -> Optional[Dict[str, Any]
4128
4131
  if len(active) != 1:
4129
4132
  return None
4130
4133
  target = active[0]
4131
- summary = (
4132
- f"AUTO-CHECKPOINT (CLOSEGAP-3): agent flipped set_status='done'. "
4133
- f"Task hint: {(task_hint or '(none)')[:200]}. "
4134
- 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."
4135
4138
  )
4136
- 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
+ })
4137
4146
  if not (isinstance(result, dict) and result.get("ok")):
4138
4147
  return None
4139
4148
  try:
4140
4149
  _log_activity_bg(
4141
4150
  "auto_checkpoint",
4142
- 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)",
4143
4152
  )
4144
4153
  except Exception:
4145
4154
  pass
4146
- 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}
4147
4156
  except Exception as e:
4148
- log.debug(f"[meshcode] auto-checkpoint on done-status failed: {e}")
4157
+ log.debug(f"[meshcode] auto-park on done-status failed: {e}")
4149
4158
  return None
4150
4159
 
4151
4160
 
@@ -5079,15 +5088,15 @@ def meshcode_set_status(status: str, task: str = "") -> Dict[str, Any]:
5079
5088
  }
5080
5089
  except Exception:
5081
5090
  pass # best-effort; never block set_status
5082
- # CLOSEGAP-3 (task 69dc5a62, Samuel reframe ce506e0d): when the
5083
- # agent flips set_status='done' with exactly one active in_progress
5084
- # 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.
5085
5094
  # Baseline (NOT autonomous-gated). Best-effort.
5086
- 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"):
5087
5096
  try:
5088
- _cp = _try_auto_complete_on_done_status(task)
5097
+ _cp = _try_auto_park_on_done_status(task, status)
5089
5098
  if _cp:
5090
- resp["auto_checkpointed_task"] = _cp
5099
+ resp["auto_parked_task"] = _cp
5091
5100
  except Exception:
5092
5101
  pass
5093
5102
  return resp
@@ -7436,6 +7445,27 @@ def meshcode_auto_wake_toggle(enabled: bool) -> Dict[str, Any]:
7436
7445
  }
7437
7446
 
7438
7447
 
7448
+ @mcp.tool()
7449
+ def meshcode_mesh_keep_alive(enabled: bool) -> Dict[str, Any]:
7450
+ """Toggle "mesh prendida" keep-alive for THIS meshwork (task baefc8ab,
7451
+ Samuel 2026-06-10: "quiero dejar la mesh prendida sin tasks — que se
7452
+ mantengan atentos a cualquier mensaje").
7453
+
7454
+ ON: the server sweep (mig 479) WAKES a stopped agent within ~5 min of an
7455
+ unread direct message arriving — sleep stays cheap (zero idle tokens) but
7456
+ the mesh answers. Explicit human Stops are still respected; agents offline
7457
+ >7 days are never resurrected. OFF (default): current behavior.
7458
+
7459
+ Owner or commander only — enforced server-side (mc_set_mesh_keep_alive).
7460
+ """
7461
+ res = be.sb_rpc("mc_set_mesh_keep_alive", {
7462
+ "p_api_key": _get_api_key(),
7463
+ "p_project_id": _PROJECT_ID,
7464
+ "p_enabled": bool(enabled),
7465
+ })
7466
+ return res if isinstance(res, dict) else {"error": str(res)}
7467
+
7468
+
7439
7469
  def inbox_resource() -> str:
7440
7470
  """Current pending messages for this agent. Read-only — does NOT mark as read."""
7441
7471
  pending = be.sb_select(
@@ -56,6 +56,35 @@ _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
+ # Task 3ec2cd4b (live repro 2026-06-10T00:34Z, msg 62062a6b): Samuel wrote
60
+ # "...que NO se vayan a dormir asi nomas... sabes?" — a COMPLAINT about agents
61
+ # sleeping, the opposite of an authorization — and the bare substring
62
+ # "a dormir" tripped must_exit on the commander mid-crisis. Human TEXT only
63
+ # authorizes sleep when it is directive-SHAPED; in doubt the message degrades
64
+ # to a normal msg (never must_exit). Three guards, human-text path only
65
+ # (structured payload.type/directive is untouched — that's the dashboard
66
+ # button / deliberate-agent path):
67
+ # LENGTH: a directive is short ("todos a dormir"); >80 chars is prose.
68
+ # QUESTION: "?"/"¿" = asking or complaining, not ordering.
69
+ # NEGATION: a negation token anywhere kills it ("que no se vayan a dormir").
70
+ _HUMAN_DIRECTIVE_MAX_CHARS = 80
71
+ _HUMAN_NEGATION_TOKENS = (
72
+ " no ", " not ", " nunca ", " jamas ", " jamás ", "que no", "sin que",
73
+ "don't", "do not", " ni ",
74
+ )
75
+
76
+
77
+ def _human_text_is_directive(text: str) -> bool:
78
+ """True when human text is shaped like an order, not prose about sleep."""
79
+ if len(text) > _HUMAN_DIRECTIVE_MAX_CHARS:
80
+ return False
81
+ if "?" in text or "\u00bf" in text:
82
+ return False
83
+ padded = " " + text.strip().lower() + " "
84
+ if any(tok in padded for tok in _HUMAN_NEGATION_TOKENS):
85
+ return False
86
+ return True
87
+
59
88
  # ── ENJAMBRE G3 (task 3b0e4129) — swarm barrier events ──────────────────────
60
89
  # The mc_swarm_barrier_check trigger (mig 471) fires when a swarm's tray
61
90
  # drains. The runtime has NO raw Postgres connection (PostgREST + Realtime
@@ -121,7 +150,7 @@ def _looks_like_sleep_signal(m: Dict[str, Any]) -> bool:
121
150
  return True
122
151
  text = str(pl.get("text", "")).lower()
123
152
  if text and any(marker in text for marker in _SLEEP_TEXT_MARKERS):
124
- if _is_human_authored(m):
153
+ if _is_human_authored(m) and _human_text_is_directive(text):
125
154
  return True
126
155
  return False
127
156
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.121
3
+ Version: 2.11.123
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.121"
7
+ version = "2.11.123"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -158,3 +158,51 @@ class TestSplitMessages(unittest.TestCase):
158
158
 
159
159
  if __name__ == "__main__":
160
160
  unittest.main()
161
+
162
+
163
+ class TestCasualSpanishComplaintGuards(unittest.TestCase):
164
+ """Task 3ec2cd4b — live repro 2026-06-10T00:34Z (msg 62062a6b): Samuel's
165
+ COMPLAINT about agents sleeping ("que NO se vayan a dormir asi nomas...")
166
+ tripped must_exit on the commander. Human text must be directive-shaped."""
167
+
168
+ REPRO = (
169
+ "oye ya me acague los agents se estan exiting and sleepign cuando no "
170
+ "tienen nada, que no se vayan a dormir asi nomas, porque aveces lo "
171
+ "querio dejar prendido sabes? que recomeindas, por ejemplo ahroita FE "
172
+ "no tenia nada asignadno y se fue a dormir , pero que si el suaurio "
173
+ "queria dejar su mesh rpendida sin taksks? sin que se fuerman que se "
174
+ "mantengan atentos a caulqueir mensaje"
175
+ )
176
+
177
+ def test_literal_live_repro_never_sleeps(self):
178
+ m = _msg(sender="sammybenu", payload={"text": self.REPRO})
179
+ self.assertFalse(_looks_like_sleep_signal(m))
180
+ split = _split_messages([m])
181
+ self.assertEqual(len(split["done_signals"]), 0)
182
+ self.assertEqual(len(split["messages"]), 1)
183
+
184
+ def test_negated_short_directive_blocked(self):
185
+ m = _msg(sender="sammybenu", payload={"text": "que no se vayan a dormir"})
186
+ self.assertFalse(_looks_like_sleep_signal(m))
187
+
188
+ def test_question_about_sleep_blocked(self):
189
+ m = _msg(sender="sammybenu", payload={"text": "se fueron a dormir?"})
190
+ self.assertFalse(_looks_like_sleep_signal(m))
191
+
192
+ def test_long_prose_with_marker_blocked(self):
193
+ m = _msg(sender="sammybenu", payload={
194
+ "text": "bueno pues al final creo que el plan de hoy salio bien y "
195
+ "ya cada quien puede irse a dormir cuando guste, mañana "
196
+ "seguimos con lo del deploy y lo de los archivos"})
197
+ self.assertFalse(_looks_like_sleep_signal(m))
198
+
199
+ def test_short_human_directive_still_authorizes(self):
200
+ for text in ("todos a dormir", "a dormir", "go to sleep", "sleep now"):
201
+ m = _msg(sender="sammybenu", payload={"text": text})
202
+ self.assertTrue(_looks_like_sleep_signal(m), text)
203
+
204
+ def test_structured_directive_untouched_by_guards(self):
205
+ # dashboard-button path: payload.type fires regardless of text shape
206
+ m = _msg(sender="mesh-commander",
207
+ payload={"type": "got_done", "text": self.REPRO})
208
+ self.assertTrue(_looks_like_sleep_signal(m))
File without changes
File without changes
File without changes
File without changes