meshcode 2.11.136__tar.gz → 2.11.137__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.136 → meshcode-2.11.137}/PKG-INFO +2 -11
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/__init__.py +1 -1
- meshcode-2.11.137/meshcode/helper_visuals.py +142 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/hostd.py +201 -35
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/meshcode_mcp/server.py +41 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/meshcode_mcp/swarm.py +60 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/meshcode_mcp/test_swarm.py +43 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/protocol_handler.py +36 -1
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/run_agent.py +72 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode.egg-info/PKG-INFO +2 -11
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode.egg-info/SOURCES.txt +3 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/pyproject.toml +1 -1
- meshcode-2.11.137/tests/test_helper_visuals.py +199 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_hostd_zombie_sessions.py +203 -0
- meshcode-2.11.137/tests/test_pretrust_claude.py +86 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/README.md +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/__main__.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/cli.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/compat.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/daemon.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/doctor.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/hooks/__init__.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/hooks/repo_path_lock.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/invites.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/launcher.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/preferences.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/secrets.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/self_update.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/up.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode/upload.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/setup.cfg +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_core.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_doctor.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_live_mesh_guard.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_session_replay_gate.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_stop_ghost_terminal.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_swarm_events.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_task_progress.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_terminal_lifecycle.py +0 -0
- {meshcode-2.11.136 → meshcode-2.11.137}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: meshcode
|
|
3
|
-
Version: 2.11.
|
|
3
|
+
Version: 2.11.137
|
|
4
4
|
Summary: Real-time communication between AI agents — Supabase-backed CLI
|
|
5
5
|
Author-email: MeshCode <hello@meshcode.io>
|
|
6
6
|
License: MIT
|
|
@@ -18,17 +18,8 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: Operating System :: OS Independent
|
|
19
19
|
Requires-Python: >=3.9
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
-
Requires-Dist: mcp[cli]>=1.0.0
|
|
22
|
-
Requires-Dist: websockets>=12.0
|
|
23
|
-
Requires-Dist: realtime>=2.0.0
|
|
24
|
-
Requires-Dist: keyring>=24.0
|
|
25
|
-
Requires-Dist: cryptography>=41.0
|
|
26
21
|
Provides-Extra: test
|
|
27
|
-
Requires-Dist: pytest>=8; extra == "test"
|
|
28
22
|
Provides-Extra: dev
|
|
29
|
-
Requires-Dist: build>=1.0; extra == "dev"
|
|
30
|
-
Requires-Dist: twine>=4; extra == "dev"
|
|
31
|
-
Requires-Dist: pytest>=8; extra == "dev"
|
|
32
23
|
|
|
33
24
|
# MeshCode
|
|
34
25
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Helper terminal visuals (task d8f8e325 — Samuel msgs edf0ed8d + 68c7fee0).
|
|
2
|
+
|
|
3
|
+
ENJAMBRE helpers must open VISUALLY DISTINCT from top-level agents on every
|
|
4
|
+
platform, so a wall of fleet tabs reads instantly: amber = ephemeral helper,
|
|
5
|
+
normal = real agent. Two layers (both silent-degrade — a visuals failure must
|
|
6
|
+
NEVER break a spawn):
|
|
7
|
+
|
|
8
|
+
[1] UNIVERSAL — `meshcode run` emits OSC 0/2 (title "helper:<name>") +
|
|
9
|
+
OSC 11 (dark-amber background) at session start. Works in Windows
|
|
10
|
+
Terminal, iTerm2 and most xterm-compatible Linux terminals regardless
|
|
11
|
+
of which launcher opened the window; terminals that ignore OSC 11
|
|
12
|
+
(e.g. macOS Terminal.app) still get the title.
|
|
13
|
+
[2] PER-PLATFORM at spawn — Windows Terminal gets `--tabColor <amber>` +
|
|
14
|
+
`--title helper:<name>` on the fleet tab; the Linux/macOS launcher
|
|
15
|
+
scripts prepend the same OSC prelude so the tint applies even before
|
|
16
|
+
`meshcode run` boots.
|
|
17
|
+
|
|
18
|
+
HOW A SPAWNER KNOWS IT'S A HELPER (no agent_kind in the spawn payloads —
|
|
19
|
+
probed live 2026-06-12: mc_agents_needing_respawn candidates and
|
|
20
|
+
mc_host_config_get agents carry no kind field):
|
|
21
|
+
|
|
22
|
+
- MARKER FILE: swarm.spawn_helper() runs on the PARENT's host, which is the
|
|
23
|
+
same host the helper spawns on (W5: helper row inherits the parent's
|
|
24
|
+
host/repo). It stamps ~/.meshcode/helpers/<name>.json with the helper's
|
|
25
|
+
TTL; spawners treat a FRESH marker as "helper". TTL expiry is the
|
|
26
|
+
cleanup (helpers are ephemeral by contract, TTL <= 86400).
|
|
27
|
+
- NAME PREFIX fallback: a name starting with 'helper' counts even without
|
|
28
|
+
a marker (covers cross-host edge cases + the observed naming convention).
|
|
29
|
+
|
|
30
|
+
Tradeoff (documented): a top-level agent that shares a helper's bare name on
|
|
31
|
+
the same host within the marker TTL would render amber — cosmetic only, and
|
|
32
|
+
name collisions are already rejected server-side within a meshwork.
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import json
|
|
37
|
+
import re
|
|
38
|
+
import sys
|
|
39
|
+
import time
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
|
|
42
|
+
# Spec colors (task d8f8e325): amber tab; dark-amber background tint that
|
|
43
|
+
# keeps default light-on-dark text readable.
|
|
44
|
+
HELPER_TAB_COLOR = "#E8A33D"
|
|
45
|
+
HELPER_BG_COLOR = "#3A2310"
|
|
46
|
+
HELPER_TITLE_PREFIX = "helper:"
|
|
47
|
+
|
|
48
|
+
_MARKER_TTL_CAP_S = 86400 # mirror of the server-side helper TTL ceiling
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _helpers_dir() -> Path:
|
|
52
|
+
"""Marker directory, behind a function so tests can patch it."""
|
|
53
|
+
return Path.home() / ".meshcode" / "helpers"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _safe_name(name: str) -> str:
|
|
57
|
+
return re.sub(r"[^A-Za-z0-9_.-]", "_", (name or "").strip()).strip("_")[:80]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def mark_helper(name: str, ttl_seconds: int = 3600) -> None:
|
|
61
|
+
"""Stamp <name> as a helper on THIS host (called by swarm.spawn_helper).
|
|
62
|
+
Best-effort: never raises. Opportunistically prunes expired markers."""
|
|
63
|
+
try:
|
|
64
|
+
safe = _safe_name(name)
|
|
65
|
+
if not safe:
|
|
66
|
+
return
|
|
67
|
+
d = _helpers_dir()
|
|
68
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
ttl = max(60, min(int(ttl_seconds or 3600), _MARKER_TTL_CAP_S))
|
|
70
|
+
(d / f"{safe}.json").write_text(
|
|
71
|
+
json.dumps({"name": name, "expires_at": time.time() + ttl}),
|
|
72
|
+
encoding="utf-8")
|
|
73
|
+
for p in d.glob("*.json"):
|
|
74
|
+
try:
|
|
75
|
+
if float(json.loads(p.read_text(encoding="utf-8"))
|
|
76
|
+
.get("expires_at", 0)) < time.time():
|
|
77
|
+
p.unlink()
|
|
78
|
+
except Exception:
|
|
79
|
+
continue
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def unmark_helper(name: str) -> None:
|
|
85
|
+
"""Drop the marker (helper retired). Best-effort."""
|
|
86
|
+
try:
|
|
87
|
+
safe = _safe_name(name)
|
|
88
|
+
if safe:
|
|
89
|
+
(_helpers_dir() / f"{safe}.json").unlink(missing_ok=True)
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def is_helper(target: str) -> bool:
|
|
95
|
+
"""True when `target` (bare 'name' or 'project/name') is a swarm helper:
|
|
96
|
+
fresh marker on this host OR the 'helper' name prefix. Never raises."""
|
|
97
|
+
try:
|
|
98
|
+
agent = (target or "").rsplit("/", 1)[-1].strip()
|
|
99
|
+
if not agent:
|
|
100
|
+
return False
|
|
101
|
+
if agent.lower().startswith("helper"):
|
|
102
|
+
return True
|
|
103
|
+
p = _helpers_dir() / f"{_safe_name(agent)}.json"
|
|
104
|
+
if not p.exists():
|
|
105
|
+
return False
|
|
106
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
107
|
+
return float(data.get("expires_at", 0)) >= time.time()
|
|
108
|
+
except Exception:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def helper_title(target: str) -> str:
|
|
113
|
+
"""Tab/window title for a helper: 'helper:<bare name>' (sanitized)."""
|
|
114
|
+
agent = _safe_name((target or "").rsplit("/", 1)[-1])
|
|
115
|
+
return f"{HELPER_TITLE_PREFIX}{agent or 'agent'}"[:48]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def osc_prelude(target: str) -> str:
|
|
119
|
+
"""Escape string: OSC 0 title + OSC 11 background. Terminals that don't
|
|
120
|
+
support either sequence ignore it (that IS the degradation path)."""
|
|
121
|
+
return (f"\x1b]0;{helper_title(target)}\x07"
|
|
122
|
+
f"\x1b]11;{HELPER_BG_COLOR}\x07")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def osc_prelude_shell(target: str) -> str:
|
|
126
|
+
"""The same prelude as a POSIX-shell `printf` command (for launcher
|
|
127
|
+
scripts / bash -lc command strings). Title is sanitized [A-Za-z0-9_.-:]
|
|
128
|
+
so it can never break out of the single quotes."""
|
|
129
|
+
return ("printf '\\033]0;%s\\007\\033]11;%s\\007' 2>/dev/null || true"
|
|
130
|
+
% (helper_title(target), HELPER_BG_COLOR))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def emit_osc(target: str) -> None:
|
|
134
|
+
"""Universal layer: write the prelude to the live terminal from
|
|
135
|
+
`meshcode run`. Best-effort, TTY-gated (a piped/CI stdout never sees
|
|
136
|
+
escape garbage)."""
|
|
137
|
+
try:
|
|
138
|
+
if sys.stdout.isatty():
|
|
139
|
+
sys.stdout.write(osc_prelude(target))
|
|
140
|
+
sys.stdout.flush()
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
@@ -41,6 +41,7 @@ import signal as _signal
|
|
|
41
41
|
import subprocess
|
|
42
42
|
import sys
|
|
43
43
|
import time
|
|
44
|
+
import urllib.error
|
|
44
45
|
import urllib.request
|
|
45
46
|
import uuid
|
|
46
47
|
from pathlib import Path
|
|
@@ -170,6 +171,14 @@ LOG_PATH = STATE_DIR / "hostd.log"
|
|
|
170
171
|
_HOSTD_STARTED_AT = time.time()
|
|
171
172
|
_BOOT_AUTOSTART = os.environ.get("MESHCODE_BOOT_AUTOSTART", "").strip().lower() in ("1", "true", "yes", "on")
|
|
172
173
|
|
|
174
|
+
# task 89d50a14 [B] FAIL-CLOSED anti-self-spawn: when PID discovery ERRORS (psutil
|
|
175
|
+
# broken AND the native fallback raised — e.g. wmic removed on Win11 24H2 before
|
|
176
|
+
# the Get-CimInstance fallback existed), an empty result does NOT mean "no live
|
|
177
|
+
# session"; spawning on it is exactly the double-session zombie. Default: skip
|
|
178
|
+
# the spawn that sweep. This env opts back into the old spawn-blind behavior —
|
|
179
|
+
# escape hatch for a host where discovery can never work but respawn must.
|
|
180
|
+
_DISCOVERY_FAIL_OPEN = os.environ.get("MESHCODE_DISCOVERY_FAIL_OPEN", "").strip().lower() in ("1", "true", "yes", "on")
|
|
181
|
+
|
|
173
182
|
def _env_poll_interval() -> int:
|
|
174
183
|
# task 399d7b51 SPEED: poll faster so click->desired_state->spawn latency drops (was 45s).
|
|
175
184
|
# Env-configurable (survives upgrade); floor 3s so we never hammer the DB.
|
|
@@ -468,6 +477,16 @@ def _rpc(fn: str, payload: dict) -> Optional[dict]:
|
|
|
468
477
|
)
|
|
469
478
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
470
479
|
return json.loads(resp.read().decode("utf-8"))
|
|
480
|
+
except urllib.error.HTTPError as e:
|
|
481
|
+
# task 89d50a14 [E]: log the PostgREST error BODY, not just the status line.
|
|
482
|
+
# mc_log_respawn_event failed 400 for DAYS and the cause (42804 host_id
|
|
483
|
+
# uuid-vs-text) was undiagnosable from "HTTP Error 400: Bad Request" alone.
|
|
484
|
+
try:
|
|
485
|
+
body = e.read().decode("utf-8", "replace").strip()[:300]
|
|
486
|
+
except Exception:
|
|
487
|
+
body = ""
|
|
488
|
+
_log(f"WARN: rpc {fn} failed: {e}" + (f" — {body}" if body else ""))
|
|
489
|
+
return None
|
|
471
490
|
except Exception as e:
|
|
472
491
|
_log(f"WARN: rpc {fn} failed: {e}")
|
|
473
492
|
return None
|
|
@@ -628,6 +647,18 @@ _MAX_SPAWNS_PER_SWEEP = 3
|
|
|
628
647
|
# session, so the SKIP line prints once per agent instead of every ~10s sweep.
|
|
629
648
|
_TOMBSTONE_LOGGED: set = set()
|
|
630
649
|
|
|
650
|
+
# task 89d50a14 [E]: boot-stale leftovers (desired_state='running' from a PRIOR
|
|
651
|
+
# session, e.g. concreta-app/studio fleets never explicitly stopped) re-qualify
|
|
652
|
+
# as candidates EVERY ~10s sweep forever — the skip line printed ~15-19 times
|
|
653
|
+
# per sweep and grew hostd.log to ~40MB. Same once-per-target-per-session
|
|
654
|
+
# discipline as _TOMBSTONE_LOGGED. The skip itself still happens every sweep.
|
|
655
|
+
_BOOTSTALE_LOGGED: set = set()
|
|
656
|
+
|
|
657
|
+
# task 89d50a14 [B]: discovery fail-closed already logged this session (per
|
|
658
|
+
# target) — the FAIL-CLOSED skip repeats every sweep while the candidate
|
|
659
|
+
# persists; log + telemetry only on the first one, clear on recovery.
|
|
660
|
+
_DISCOVERY_ERR_LOGGED: set = set()
|
|
661
|
+
|
|
631
662
|
|
|
632
663
|
def _do_respawns(api_key: str, host_id: str) -> int:
|
|
633
664
|
"""One respawn sweep. Returns number relaunched."""
|
|
@@ -695,10 +726,20 @@ def _do_respawns(api_key: str, host_id: str) -> int:
|
|
|
695
726
|
_hb_age = c.get("heartbeat_age_s")
|
|
696
727
|
_alive_on_our_watch = _hb_age is not None and _hb_age < _hostd_uptime
|
|
697
728
|
if (_spawn_age is None or _spawn_age >= _hostd_uptime) and not _alive_on_our_watch:
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
729
|
+
# log-once per target per daemon session (task 89d50a14 [E]) — this
|
|
730
|
+
# skip repeats every ~10s sweep for every stale fleet and was the
|
|
731
|
+
# main feeder of the ~40MB hostd.log.
|
|
732
|
+
_bs_target = f"{proj}/{agent}"
|
|
733
|
+
if _bs_target not in _BOOTSTALE_LOGGED:
|
|
734
|
+
_BOOTSTALE_LOGGED.add(_bs_target)
|
|
735
|
+
_log(f"BOOT-AUTOSTART OFF: skip auto-respawn {_bs_target} (desired_state=running set "
|
|
736
|
+
f"before this hostd start — boot-stale leftover; explicit launch required). "
|
|
737
|
+
f"Set MESHCODE_BOOT_AUTOSTART=1 to auto-launch at boot. "
|
|
738
|
+
f"(logged once per agent per session; skip repeats silently)")
|
|
701
739
|
continue
|
|
740
|
+
# an explicit launch / live heartbeat got this target past the gate —
|
|
741
|
+
# if it ever goes boot-stale again, the skip deserves a fresh line.
|
|
742
|
+
_BOOTSTALE_LOGGED.discard(f"{proj}/{agent}")
|
|
702
743
|
# RECYCLE FAST-PATH (task c0fc5597): a recycle-exited agent (recycle_fast) is relaunched
|
|
703
744
|
# PROMPTLY (the RPC returned it at a 15s stale gate, not STALE_SECONDS) and recorded as a
|
|
704
745
|
# RECYCLE (mc_record_recycle), NEVER against the crash respawn cap.
|
|
@@ -786,7 +827,20 @@ def _do_respawns(api_key: str, host_id: str) -> int:
|
|
|
786
827
|
_cv.pop("blocked_ts", None)
|
|
787
828
|
_cv["count"] = 0 # TTL elapsed -> one clean retry; re-blocks if it storms again
|
|
788
829
|
# LIVE-SESSION GUARD: never stack a second terminal on a still-alive session.
|
|
789
|
-
|
|
830
|
+
# task 89d50a14 [B]: discovery that ERRORED returns an untrustworthy empty
|
|
831
|
+
# list — fail closed (skip this sweep) instead of concluding "no live session".
|
|
832
|
+
_live_d = _discover_agent_pids_ex(_target)
|
|
833
|
+
if _live_d["errored"] and not _DISCOVERY_FAIL_OPEN:
|
|
834
|
+
if _target not in _DISCOVERY_ERR_LOGGED:
|
|
835
|
+
_DISCOVERY_ERR_LOGGED.add(_target)
|
|
836
|
+
_log(f"SPAWN-AUDIT {_target}: DISCOVERY ERRORED (path={_live_d['path']}; "
|
|
837
|
+
f"{_live_d['error']}) — FAIL-CLOSED: cannot prove no live session, NOT "
|
|
838
|
+
f"spawning; retrying every sweep (set MESHCODE_DISCOVERY_FAIL_OPEN=1 to "
|
|
839
|
+
f"spawn blind). [respawn_blocked_reason=discovery_error] (logged once)")
|
|
840
|
+
_log_respawn_event(api_key, host_id, c, "respawn", "failed", "discovery_error",
|
|
841
|
+
detail=f"live-guard path={_live_d['path']}: {_live_d['error'][:300]}")
|
|
842
|
+
continue
|
|
843
|
+
_live = [p for p in _live_d["pids"] if _pid_alive(p)]
|
|
790
844
|
if _live:
|
|
791
845
|
_hold_all = dict(_st2.get("respawn_hold") or {})
|
|
792
846
|
_hold = dict(_hold_all.get(_target) or {})
|
|
@@ -884,8 +938,27 @@ def _do_respawns(api_key: str, host_id: str) -> int:
|
|
|
884
938
|
# MESHCODE_NO_SPAWN_RECONCILE), spawning anyway IS the double-session bug —
|
|
885
939
|
# skip this sweep and retry on the next one (the survivor either dies then
|
|
886
940
|
# or reconnects and stops qualifying as a respawn candidate).
|
|
887
|
-
|
|
888
|
-
|
|
941
|
+
_ag_d = _discover_agent_pids_ex(_target)
|
|
942
|
+
_sv_d = _discover_serve_pids_ex(_target)
|
|
943
|
+
# task 89d50a14 [B] FAIL-CLOSED: an errored discovery proves NOTHING about
|
|
944
|
+
# live sessions — spawning on its empty result is the double-session bug
|
|
945
|
+
# with extra steps. Skip the spawn; the candidate re-qualifies next sweep.
|
|
946
|
+
if (_ag_d["errored"] or _sv_d["errored"]) and not _DISCOVERY_FAIL_OPEN:
|
|
947
|
+
if _target not in _DISCOVERY_ERR_LOGGED:
|
|
948
|
+
_DISCOVERY_ERR_LOGGED.add(_target)
|
|
949
|
+
_log(f"SPAWN-AUDIT {_target}: DISCOVERY ERRORED (agent path={_ag_d['path']}; "
|
|
950
|
+
f"{_ag_d['error'] or '-'} | serve: {_sv_d['error'] or '-'}) — FAIL-CLOSED: "
|
|
951
|
+
f"cannot prove no live session, NOT spawning; retrying every sweep "
|
|
952
|
+
f"(set MESHCODE_DISCOVERY_FAIL_OPEN=1 to spawn blind). "
|
|
953
|
+
f"[respawn_blocked_reason=discovery_error] (logged once)")
|
|
954
|
+
_log_respawn_event(api_key, host_id, c,
|
|
955
|
+
"recycle_respawn" if _is_recycle else "respawn",
|
|
956
|
+
"failed", "discovery_error",
|
|
957
|
+
detail=(f"pre-spawn path={_ag_d['path']}: "
|
|
958
|
+
f"{(_ag_d['error'] or _sv_d['error'])[:300]}"))
|
|
959
|
+
continue
|
|
960
|
+
_DISCOVERY_ERR_LOGGED.discard(_target) # discovery healthy again → re-arm the log-once
|
|
961
|
+
_still_alive = [p for p in (_ag_d["pids"] + _sv_d["pids"]) if _pid_alive(p)]
|
|
889
962
|
if _still_alive:
|
|
890
963
|
_log(f"SPAWN-AUDIT {_target}: SKIP spawn — live session still present after "
|
|
891
964
|
f"reconcile (pids {_still_alive}); retry next sweep")
|
|
@@ -911,7 +984,8 @@ def _do_respawns(api_key: str, host_id: str) -> int:
|
|
|
911
984
|
_log(f"SPAWN-AUDIT {_target}: spawning — reason={'recycle_fast' if _is_recycle else 'crash_respawn'} "
|
|
912
985
|
f"desired_state=running hb_age={c.get('heartbeat_age_s')}s "
|
|
913
986
|
f"spawned_age={c.get('spawned_age_s')}s respawn_count={c.get('respawn_count')} "
|
|
914
|
-
f"reconciled={_reconciled}
|
|
987
|
+
f"reconciled={_reconciled} discovery={_ag_d['path']}/{_sv_d['path']} "
|
|
988
|
+
f"(agent_pids={len(_ag_d['pids'])} serve_pids={len(_sv_d['pids'])}) host={host_id}")
|
|
915
989
|
_log(f"{'RECYCLE-RESPAWN' if _is_recycle else 'RESPAWN'} {proj}/{agent} "
|
|
916
990
|
f"(stale {c.get('heartbeat_age_s')}s, count={c.get('respawn_count')})")
|
|
917
991
|
if _spawn_agent(proj, agent, repo_path=c.get("repo_path")):
|
|
@@ -1273,13 +1347,54 @@ def _gc_headless_pids() -> None:
|
|
|
1273
1347
|
pass
|
|
1274
1348
|
|
|
1275
1349
|
|
|
1350
|
+
def _cim_pid_cmdline(pid: int) -> str:
|
|
1351
|
+
"""Windows cmdline via Get-CimInstance (task 89d50a14 [B]): wmic is deprecated
|
|
1352
|
+
and REMOVED by default on Win11 24H2+ — every wmic call there raises
|
|
1353
|
+
FileNotFoundError and discovery silently went blind. powershell.exe (Windows
|
|
1354
|
+
PowerShell 5.1) ships with every supported Windows. '' on failure."""
|
|
1355
|
+
try:
|
|
1356
|
+
out = subprocess.run(
|
|
1357
|
+
["powershell", "-NoProfile", "-NonInteractive", "-Command",
|
|
1358
|
+
f"(Get-CimInstance Win32_Process -Filter \"ProcessId={int(pid)}\").CommandLine"],
|
|
1359
|
+
capture_output=True, text=True, timeout=15).stdout
|
|
1360
|
+
return out or ""
|
|
1361
|
+
except Exception:
|
|
1362
|
+
return ""
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
def _cim_python_pid_cmdlines() -> list:
|
|
1366
|
+
"""(pid, cmdline) of every python.exe via Get-CimInstance — the Windows native
|
|
1367
|
+
discovery fallback when psutil is unavailable and wmic is gone (task 89d50a14
|
|
1368
|
+
[B]). One 'pid|cmdline' line per process; partition('|') keeps any '|' inside
|
|
1369
|
+
the cmdline intact. Raises on subprocess failure (caller decides fail-closed)."""
|
|
1370
|
+
script = ("Get-CimInstance Win32_Process -Filter \"Name='python.exe'\" | "
|
|
1371
|
+
"ForEach-Object { '{0}|{1}' -f $_.ProcessId, $_.CommandLine }")
|
|
1372
|
+
out = subprocess.run(
|
|
1373
|
+
["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
|
|
1374
|
+
capture_output=True, text=True, timeout=15).stdout
|
|
1375
|
+
rows = []
|
|
1376
|
+
for line in (out or "").splitlines():
|
|
1377
|
+
pid_s, sep, cl = line.partition("|")
|
|
1378
|
+
if not sep:
|
|
1379
|
+
continue
|
|
1380
|
+
try:
|
|
1381
|
+
rows.append((int(pid_s.strip()), cl))
|
|
1382
|
+
except ValueError:
|
|
1383
|
+
continue
|
|
1384
|
+
return rows
|
|
1385
|
+
|
|
1386
|
+
|
|
1276
1387
|
def _pid_cmdline(pid: int) -> str:
|
|
1277
1388
|
"""Best-effort command line for a pid (to avoid killing a reused PID). '' on failure."""
|
|
1278
1389
|
try:
|
|
1279
1390
|
if sys.platform == "win32":
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1391
|
+
try:
|
|
1392
|
+
out = subprocess.run(
|
|
1393
|
+
["wmic", "process", "where", f"ProcessId={pid}", "get", "CommandLine"],
|
|
1394
|
+
capture_output=True, text=True, timeout=5).stdout
|
|
1395
|
+
except (FileNotFoundError, OSError):
|
|
1396
|
+
# wmic removed (Win11 24H2+) — CIM fallback (task 89d50a14 [B])
|
|
1397
|
+
out = _cim_pid_cmdline(pid)
|
|
1283
1398
|
else:
|
|
1284
1399
|
out = subprocess.run(
|
|
1285
1400
|
["ps", "-o", "args=", "-p", str(pid)],
|
|
@@ -1401,7 +1516,26 @@ def _discover_agent_pids(target: str) -> list:
|
|
|
1401
1516
|
mesh (psutil path only; the native fallbacks accept the regex as-is, documented
|
|
1402
1517
|
tradeoff: a zombie that survives every stop is the live bug, the cross-mesh
|
|
1403
1518
|
same-name collision is the rare one)."""
|
|
1519
|
+
return _discover_agent_pids_ex(target)["pids"]
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
def _discover_agent_pids_ex(target: str) -> dict:
|
|
1523
|
+
"""Instrumented discovery (task 89d50a14 [B]) — same matching as
|
|
1524
|
+
_discover_agent_pids (see its docstring for every safety rule), but the
|
|
1525
|
+
result distinguishes 'found nothing' from 'COULD NOT LOOK':
|
|
1526
|
+
|
|
1527
|
+
{"pids": [..], "errored": bool, "path": str, "error": str}
|
|
1528
|
+
|
|
1529
|
+
errored=True means the path that produced the final result RAISED — the
|
|
1530
|
+
empty pid list is untrustworthy and spawn decisions must FAIL CLOSED
|
|
1531
|
+
(an empty-but-clean result stays spawn-safe). path is the mechanism that
|
|
1532
|
+
produced the result: 'psutil' | 'native-wmic' | 'native-cim' |
|
|
1533
|
+
'native-pgrep'. Windows native goes wmic → Get-CimInstance (wmic is
|
|
1534
|
+
removed by default on Win11 24H2+; it raising FileNotFoundError and the
|
|
1535
|
+
sweep silently treating that as 'no live session' was a double-session
|
|
1536
|
+
feeder)."""
|
|
1404
1537
|
pids = []
|
|
1538
|
+
err_notes = []
|
|
1405
1539
|
tok = _run_token_rx(target)
|
|
1406
1540
|
ps = _psutil()
|
|
1407
1541
|
if ps is not None:
|
|
@@ -1428,30 +1562,51 @@ def _discover_agent_pids(target: str) -> list:
|
|
|
1428
1562
|
# `return pids` skipped the POSIX launcher-children fallback whenever
|
|
1429
1563
|
# psutil was importable — re-defeating the '0 stopped forever' hole on
|
|
1430
1564
|
# every box with psutil. Fall through to the shared tail instead.
|
|
1431
|
-
return pids + ([p for p in _discover_launcher_child_pids(target)
|
|
1432
|
-
|
|
1433
|
-
|
|
1565
|
+
return {"pids": pids + ([p for p in _discover_launcher_child_pids(target)
|
|
1566
|
+
if p not in pids] if sys.platform != "win32" else []),
|
|
1567
|
+
"errored": False, "path": "psutil", "error": ""}
|
|
1568
|
+
except Exception as e:
|
|
1569
|
+
err_notes.append(f"psutil: {e!r}")
|
|
1434
1570
|
pids = [] # psutil enumeration itself broke — fall through to native
|
|
1571
|
+
path = "native"
|
|
1572
|
+
errored = False
|
|
1435
1573
|
try:
|
|
1436
1574
|
if sys.platform == "win32":
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1575
|
+
wmic_out = None
|
|
1576
|
+
try:
|
|
1577
|
+
cp = subprocess.run(
|
|
1578
|
+
["wmic", "process", "where", "name='python.exe'", "get", "ProcessId,CommandLine", "/FORMAT:LIST"],
|
|
1579
|
+
capture_output=True, text=True, timeout=8)
|
|
1580
|
+
if cp.returncode == 0 or (cp.stdout or "").strip():
|
|
1581
|
+
wmic_out = cp.stdout or ""
|
|
1582
|
+
else:
|
|
1583
|
+
err_notes.append(f"wmic: rc={cp.returncode}")
|
|
1584
|
+
except (FileNotFoundError, OSError) as e:
|
|
1585
|
+
# wmic deprecated/removed (Win11 24H2+) — Get-CimInstance fallback
|
|
1586
|
+
err_notes.append(f"wmic: {e!r}")
|
|
1587
|
+
if wmic_out is not None:
|
|
1588
|
+
path = "native-wmic"
|
|
1589
|
+
block_pid = None
|
|
1590
|
+
for line in wmic_out.splitlines():
|
|
1591
|
+
line = line.strip()
|
|
1592
|
+
if line.startswith("CommandLine="):
|
|
1593
|
+
block_pid = ("meshcode" in line and bool(tok.search(line)))
|
|
1594
|
+
elif line.startswith("ProcessId=") and block_pid:
|
|
1595
|
+
try:
|
|
1596
|
+
pids.append(int(line.split("=", 1)[1]))
|
|
1597
|
+
except Exception:
|
|
1598
|
+
pass
|
|
1599
|
+
block_pid = None
|
|
1600
|
+
else:
|
|
1601
|
+
path = "native-cim"
|
|
1602
|
+
for cim_pid, cl in _cim_python_pid_cmdlines():
|
|
1603
|
+
if cl and "meshcode" in cl and tok.search(cl):
|
|
1604
|
+
pids.append(cim_pid)
|
|
1451
1605
|
else:
|
|
1452
1606
|
# task 3ed8781d: pattern WITHOUT the target so bare-name spawns
|
|
1453
1607
|
# (`run "agent"`) are candidates too; the exact tok filter below
|
|
1454
1608
|
# keeps the kill set tight.
|
|
1609
|
+
path = "native-pgrep"
|
|
1455
1610
|
out = subprocess.run(
|
|
1456
1611
|
["pgrep", "-f", "meshcode.* run "],
|
|
1457
1612
|
capture_output=True, text=True, timeout=8).stdout
|
|
@@ -1463,8 +1618,10 @@ def _discover_agent_pids(target: str) -> list:
|
|
|
1463
1618
|
# pgrep matched a substring; keep ONLY if the full cmdline has the exact token.
|
|
1464
1619
|
if tok.search(_pid_cmdline(p)):
|
|
1465
1620
|
pids.append(p)
|
|
1466
|
-
except Exception:
|
|
1467
|
-
|
|
1621
|
+
except Exception as e:
|
|
1622
|
+
# the LAST mechanism available raised — the empty result is a lie, not a fact
|
|
1623
|
+
err_notes.append(f"{path}: {e!r}")
|
|
1624
|
+
errored = True
|
|
1468
1625
|
# GHOST-TERMINAL fix (task 91201315): on POSIX `meshcode run` EXECVP's claude, so the
|
|
1469
1626
|
# live agent's cmdline is `claude …` — no 'meshcode run <target>' survives and the
|
|
1470
1627
|
# pgrep above sees NOTHING for a visible macOS agent (the '0 stopped forever' hole).
|
|
@@ -1477,7 +1634,7 @@ def _discover_agent_pids(target: str) -> list:
|
|
|
1477
1634
|
pids += [p for p in _discover_launcher_child_pids(target) if p not in pids]
|
|
1478
1635
|
except Exception:
|
|
1479
1636
|
pass
|
|
1480
|
-
return pids
|
|
1637
|
+
return {"pids": pids, "errored": errored, "path": path, "error": "; ".join(err_notes)}
|
|
1481
1638
|
|
|
1482
1639
|
|
|
1483
1640
|
def _discover_launcher_child_pids(target: str) -> list:
|
|
@@ -1543,12 +1700,21 @@ def _discover_serve_pids(target: str) -> list:
|
|
|
1543
1700
|
relaunch reconcile) matches on MESHCODE_AGENT alone — the relaunch click means
|
|
1544
1701
|
'fresh session of THIS agent on this box', so any project's orphan of that name
|
|
1545
1702
|
is a survivor to clear. Qualified targets keep the exact two-field match."""
|
|
1703
|
+
return _discover_serve_pids_ex(target)["pids"]
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
def _discover_serve_pids_ex(target: str) -> dict:
|
|
1707
|
+
"""Instrumented serve discovery (task 89d50a14 [B]) — same result contract as
|
|
1708
|
+
_discover_agent_pids_ex. psutil-missing is errored=False (degraded BY DESIGN,
|
|
1709
|
+
documented since 14e0760c: the session-root tree kill still covers parented
|
|
1710
|
+
serves); the enumeration itself raising is errored=True (empty result is
|
|
1711
|
+
untrustworthy → spawn decisions fail closed)."""
|
|
1546
1712
|
ps = _psutil()
|
|
1547
1713
|
if ps is None:
|
|
1548
|
-
return []
|
|
1714
|
+
return {"pids": [], "errored": False, "path": "none(psutil-missing)", "error": ""}
|
|
1549
1715
|
proj, _, agent = target.rpartition("/")
|
|
1550
1716
|
if not agent:
|
|
1551
|
-
return []
|
|
1717
|
+
return {"pids": [], "errored": False, "path": "psutil", "error": ""}
|
|
1552
1718
|
own = _own_pid()
|
|
1553
1719
|
out = []
|
|
1554
1720
|
try:
|
|
@@ -1565,9 +1731,9 @@ def _discover_serve_pids(target: str) -> list:
|
|
|
1565
1731
|
out.append(p.pid)
|
|
1566
1732
|
except Exception:
|
|
1567
1733
|
continue # process vanished / access denied — never block the sweep
|
|
1568
|
-
except Exception:
|
|
1569
|
-
return []
|
|
1570
|
-
return out
|
|
1734
|
+
except Exception as e:
|
|
1735
|
+
return {"pids": [], "errored": True, "path": "psutil", "error": f"psutil: {e!r}"}
|
|
1736
|
+
return {"pids": out, "errored": False, "path": "psutil", "error": ""}
|
|
1571
1737
|
|
|
1572
1738
|
|
|
1573
1739
|
def _kill_heartbeat_fork(target: str) -> None:
|
|
@@ -6723,6 +6723,47 @@ def meshcode_tray_claim(lease_seconds: int = 900,
|
|
|
6723
6723
|
lease_seconds=lease_seconds)
|
|
6724
6724
|
|
|
6725
6725
|
|
|
6726
|
+
@mcp.tool()
|
|
6727
|
+
@with_working_status
|
|
6728
|
+
def meshcode_tray_seed(title: str,
|
|
6729
|
+
description: str = "",
|
|
6730
|
+
priority: str = "normal",
|
|
6731
|
+
swarm_id: Optional[str] = None,
|
|
6732
|
+
parent_task_id: Optional[str] = None,
|
|
6733
|
+
depends_on: Optional[list] = None,
|
|
6734
|
+
queue_position: Optional[int] = None) -> Dict[str, Any]:
|
|
6735
|
+
"""PARENT loop step: seed ONE task into your swarm tray so helpers drain
|
|
6736
|
+
it via meshcode_tray_claim (work-stealing, DAG-gated, priority-ordered).
|
|
6737
|
+
|
|
6738
|
+
ENJAMBRE (mig 524, task d4af9eb7 — before this, nothing could create
|
|
6739
|
+
swarm_id-stamped tasks: lanes went by direct assignment and lost
|
|
6740
|
+
work-stealing + the mig 471 barrier-on-drain). Canonical flow:
|
|
6741
|
+
meshcode_helper_spawn (mints the tray + stamps YOUR swarm_id) →
|
|
6742
|
+
meshcode_tray_seed xN → helpers tray_claim/complete → barrier fires when
|
|
6743
|
+
the tray drains. Seed BEFORE or AFTER spawning — claims only see
|
|
6744
|
+
status='open' rows, there is no race.
|
|
6745
|
+
|
|
6746
|
+
Identity-bound (agent-scoped api key) and member-only: you can seed only
|
|
6747
|
+
the swarm you belong to. The task lands assignee=you (concrete-owner
|
|
6748
|
+
rule); stealing is via claimed_by.
|
|
6749
|
+
|
|
6750
|
+
Args:
|
|
6751
|
+
title: task title (required, <=200 chars).
|
|
6752
|
+
description: full lane instructions (helpers see it at claim time).
|
|
6753
|
+
priority: low|normal|medium|high|urgent (claim order).
|
|
6754
|
+
swarm_id: override tray; omit and the SERVER resolves your own row's swarm.
|
|
6755
|
+
parent_task_id: umbrella task — seeded tasks participate in its
|
|
6756
|
+
barrier-on-drain (mig 471) automatically.
|
|
6757
|
+
depends_on: task UUIDs that DAG-gate this one (validated same-project).
|
|
6758
|
+
queue_position: tiebreaker within same priority (lower first).
|
|
6759
|
+
"""
|
|
6760
|
+
from . import swarm as _swarm
|
|
6761
|
+
return _swarm.tray_seed(_get_api_key(), _PROJECT_ID, title,
|
|
6762
|
+
description=description, priority=priority,
|
|
6763
|
+
swarm_id=swarm_id, parent_task_id=parent_task_id,
|
|
6764
|
+
depends_on=depends_on, queue_position=queue_position)
|
|
6765
|
+
|
|
6766
|
+
|
|
6726
6767
|
@mcp.tool()
|
|
6727
6768
|
@with_working_status
|
|
6728
6769
|
def meshcode_helper_retire(reason: str = "tray_drained") -> Dict[str, Any]:
|
|
@@ -184,6 +184,16 @@ def spawn_helper(api_key: str, project_id: str, name: str, *,
|
|
|
184
184
|
"error": _explain_spawn_refusal(code, err),
|
|
185
185
|
**({"error_code": code} if code else {})}
|
|
186
186
|
|
|
187
|
+
# HELPER VISUALS (task d8f8e325): stamp the local marker SO THE SPAWNER
|
|
188
|
+
# (hostd / protocol_handler, same host by W5 inheritance) can render the
|
|
189
|
+
# helper's terminal distinct (amber tab + helper: title). Best-effort —
|
|
190
|
+
# never blocks the spawn.
|
|
191
|
+
try:
|
|
192
|
+
from meshcode import helper_visuals as _hv
|
|
193
|
+
_hv.mark_helper(name, ttl_seconds=int(ttl_seconds))
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
|
|
187
197
|
out: Dict[str, Any] = {"ok": True, "helper": name, "launched": bool(launch)}
|
|
188
198
|
if isinstance(reg, dict):
|
|
189
199
|
out.update({k: reg[k] for k in
|
|
@@ -217,6 +227,45 @@ def spawn_helper(api_key: str, project_id: str, name: str, *,
|
|
|
217
227
|
return out
|
|
218
228
|
|
|
219
229
|
|
|
230
|
+
RPC_TRAY_SEED = "mc_tray_seed_as_agent"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def tray_seed(api_key: str, project_id: str, title: str, *,
|
|
234
|
+
description: str = "",
|
|
235
|
+
priority: str = "normal",
|
|
236
|
+
swarm_id: Optional[str] = None,
|
|
237
|
+
parent_task_id: Optional[str] = None,
|
|
238
|
+
depends_on: Optional[list] = None,
|
|
239
|
+
queue_position: Optional[int] = None) -> Dict[str, Any]:
|
|
240
|
+
"""Seed ONE task into the swarm tray (task d4af9eb7 — closes the gap where
|
|
241
|
+
mc_task_claim_from_tray could only CLAIM and nothing could create
|
|
242
|
+
swarm_id-stamped tasks, so lanes fell back to direct assignment and lost
|
|
243
|
+
work-stealing + DAG gate + barrier-on-drain).
|
|
244
|
+
|
|
245
|
+
swarm_id=None lets the SERVER resolve the caller's own mc_agents.swarm_id
|
|
246
|
+
(same contract as tray_claim — the SDK never reads mc_agents directly).
|
|
247
|
+
Identity-bound: requires an agent-scoped api key; the caller must belong
|
|
248
|
+
to the swarm. The seeded task lands status='open' (exactly what
|
|
249
|
+
tray_claim picks up), assignee=caller (mig 213 concrete-owner rule;
|
|
250
|
+
stealing is via claimed_by). depends_on ids are validated server-side
|
|
251
|
+
against the same project so a typo can never DAG-block the tray forever."""
|
|
252
|
+
result = be.sb_rpc(RPC_TRAY_SEED, {
|
|
253
|
+
"p_api_key": api_key,
|
|
254
|
+
"p_project_id": project_id,
|
|
255
|
+
"p_title": title,
|
|
256
|
+
"p_description": description,
|
|
257
|
+
"p_priority": priority,
|
|
258
|
+
"p_swarm_id": swarm_id,
|
|
259
|
+
"p_parent_task_id": parent_task_id,
|
|
260
|
+
"p_depends_on": depends_on,
|
|
261
|
+
"p_queue_position": queue_position,
|
|
262
|
+
})
|
|
263
|
+
err = _err(result)
|
|
264
|
+
if err:
|
|
265
|
+
return {"ok": False, "error": err}
|
|
266
|
+
return result if isinstance(result, dict) else {"ok": False, "error": "bad response shape"}
|
|
267
|
+
|
|
268
|
+
|
|
220
269
|
def tray_claim(api_key: str, project_id: str, *,
|
|
221
270
|
swarm_id: Optional[str] = None,
|
|
222
271
|
lease_seconds: int = DEFAULT_LEASE_SECONDS) -> Dict[str, Any]:
|
|
@@ -255,6 +304,17 @@ def retire_self(api_key: str, project_id: str, *,
|
|
|
255
304
|
err = _err(result)
|
|
256
305
|
if err:
|
|
257
306
|
return {"ok": False, "error": err}
|
|
307
|
+
# HELPER VISUALS (task d8f8e325): retired — drop this host's marker so a
|
|
308
|
+
# later same-named top-level agent doesn't render amber. Self-identify via
|
|
309
|
+
# the MCP env (.mcp.json always sets MESHCODE_AGENT). Best-effort.
|
|
310
|
+
try:
|
|
311
|
+
import os as _os
|
|
312
|
+
from meshcode import helper_visuals as _hv
|
|
313
|
+
_own = _os.environ.get("MESHCODE_AGENT", "")
|
|
314
|
+
if _own:
|
|
315
|
+
_hv.unmark_helper(_own)
|
|
316
|
+
except Exception:
|
|
317
|
+
pass
|
|
258
318
|
return result if isinstance(result, dict) else {"ok": False, "error": "bad response shape"}
|
|
259
319
|
|
|
260
320
|
|