meshcode 2.11.90__tar.gz → 2.11.93__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.90 → meshcode-2.11.93}/PKG-INFO +9 -1
- {meshcode-2.11.90 → meshcode-2.11.93}/README.md +8 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/__init__.py +1 -1
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/ascii_art.py +193 -5
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/hostd.py +125 -7
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode.egg-info/PKG-INFO +9 -1
- {meshcode-2.11.90 → meshcode-2.11.93}/pyproject.toml +1 -1
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/__main__.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/cli.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/compat.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/daemon.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/doctor.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/invites.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/launcher.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/server.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/preferences.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/run_agent.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/secrets.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/self_update.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/up.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/upload.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/setup.cfg +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_core.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_doctor.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.90 → meshcode-2.11.93}/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.
|
|
3
|
+
Version: 2.11.93
|
|
4
4
|
Summary: Real-time communication between AI agents — Supabase-backed CLI
|
|
5
5
|
Author-email: MeshCode <hello@meshcode.io>
|
|
6
6
|
License: MIT
|
|
@@ -442,6 +442,14 @@ CDN propagation. Wait ~60s and force-fetch directly from the origin:
|
|
|
442
442
|
pip install --no-cache-dir -i https://pypi.org/simple/ meshcode
|
|
443
443
|
```
|
|
444
444
|
|
|
445
|
+
**12. Upgrading to v2.11.88+: relaunch already-running agents and host ONCE**
|
|
446
|
+
From v2.11.88 the host daemon (hostd) auto-restarts itself when a newer SDK lands on disk, so the Stop/kill sweep and launch fixes pick up automatically — and managed agents version-recycle cooperatively to follow. But an agent (or hostd) **already running an older SDK** carries that old code in memory and can't be auto-recycled (old code doesn't honor the recycle signal). Cross the gap once:
|
|
447
|
+
```bash
|
|
448
|
+
pip install --upgrade meshcode # writes the new wheel to disk
|
|
449
|
+
meshcode login # restarts a stale hostd (v2.11.90+ does this only if provably stale)
|
|
450
|
+
```
|
|
451
|
+
Then relaunch each still-old agent once (`meshcode run <project>/<agent>`, or close + reopen its Claude Code session). After this one-time relaunch, every agent obeys cooperative recycle and stays current automatically — no manual restarts thereafter.
|
|
452
|
+
|
|
445
453
|
---
|
|
446
454
|
|
|
447
455
|
## Links
|
|
@@ -410,6 +410,14 @@ CDN propagation. Wait ~60s and force-fetch directly from the origin:
|
|
|
410
410
|
pip install --no-cache-dir -i https://pypi.org/simple/ meshcode
|
|
411
411
|
```
|
|
412
412
|
|
|
413
|
+
**12. Upgrading to v2.11.88+: relaunch already-running agents and host ONCE**
|
|
414
|
+
From v2.11.88 the host daemon (hostd) auto-restarts itself when a newer SDK lands on disk, so the Stop/kill sweep and launch fixes pick up automatically — and managed agents version-recycle cooperatively to follow. But an agent (or hostd) **already running an older SDK** carries that old code in memory and can't be auto-recycled (old code doesn't honor the recycle signal). Cross the gap once:
|
|
415
|
+
```bash
|
|
416
|
+
pip install --upgrade meshcode # writes the new wheel to disk
|
|
417
|
+
meshcode login # restarts a stale hostd (v2.11.90+ does this only if provably stale)
|
|
418
|
+
```
|
|
419
|
+
Then relaunch each still-old agent once (`meshcode run <project>/<agent>`, or close + reopen its Claude Code session). After this one-time relaunch, every agent obeys cooperative recycle and stays current automatically — no manual restarts thereafter.
|
|
420
|
+
|
|
413
421
|
---
|
|
414
422
|
|
|
415
423
|
## Links
|
|
@@ -12,6 +12,7 @@ Identicons are GUARANTEED unique — SHA-256 produces 2^256 patterns.
|
|
|
12
12
|
The embedded tag ⟨ meshwork/agent ⟩ enables paste-to-run.
|
|
13
13
|
"""
|
|
14
14
|
import hashlib
|
|
15
|
+
import os
|
|
15
16
|
|
|
16
17
|
# ── Block characters ────────────────────────────────────────────────
|
|
17
18
|
BLOCKS = {
|
|
@@ -316,6 +317,187 @@ def hex_to_ansi(hex_color: str) -> str:
|
|
|
316
317
|
best_dist = dist
|
|
317
318
|
best_idx = i
|
|
318
319
|
return COLORS[best_idx]
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# ── TERMINAL-BOOT-REAL-SPRITE (task 8e2e14d1) ───────────────────────
|
|
323
|
+
# Render each agent's REAL dashboard sprite in 24-bit TRUECOLOR so the
|
|
324
|
+
# terminal banner matches the dashboard PixelMascot "igualito". The body/
|
|
325
|
+
# eyes/mouth are a faithful port of src/components/PixelMascot.tsx
|
|
326
|
+
# generateConfig(name) — derived purely from the name hash (the dashboard
|
|
327
|
+
# does NOT use the stored mascot_config for the body), and the color is the
|
|
328
|
+
# agent's exact profile hex (not a nearest-16 approximation).
|
|
329
|
+
|
|
330
|
+
# 8 body templates — LEFT HALF (4 cols), mirrored to 7 wide. 7 rows.
|
|
331
|
+
# 0=empty 1=body 2=highlight 3=shadow. Exact copy of PixelMascot.tsx BODIES.
|
|
332
|
+
_DASH_BODIES = [
|
|
333
|
+
[[0,0,1,1],[0,1,1,1],[1,1,1,1],[1,1,2,1],[1,1,1,1],[0,1,1,1],[0,0,1,0]], # round blob
|
|
334
|
+
[[0,1,1,1],[1,1,1,1],[1,1,1,1],[1,1,2,1],[1,1,1,1],[1,1,1,1],[0,1,0,1]], # square bot
|
|
335
|
+
[[0,0,1,1],[0,1,1,1],[0,1,1,1],[1,1,2,1],[1,1,1,1],[0,1,1,0],[0,1,0,1]], # tall
|
|
336
|
+
[[0,0,0,0],[0,1,1,1],[1,1,1,1],[1,1,2,1],[1,1,1,1],[1,1,1,1],[1,0,0,1]], # wide toad
|
|
337
|
+
[[0,0,1,1],[0,1,1,1],[1,1,1,1],[1,1,2,1],[1,1,1,1],[1,1,1,1],[1,0,1,0]], # ghost
|
|
338
|
+
[[1,0,0,0],[1,1,1,1],[0,1,1,1],[1,1,2,1],[1,1,1,1],[0,1,1,0],[0,1,0,1]], # cat-ish
|
|
339
|
+
[[0,1,1,1],[1,1,1,1],[1,1,1,1],[0,0,1,1],[0,1,2,1],[0,1,1,1],[0,1,0,1]], # mushroom
|
|
340
|
+
[[0,0,1,0],[0,1,1,1],[1,1,1,1],[1,1,2,1],[1,1,1,1],[0,1,1,1],[0,0,1,0]], # star
|
|
341
|
+
]
|
|
342
|
+
# {row, col} of the eyes (mirrored across the 7-wide grid). Exact copy.
|
|
343
|
+
_DASH_EYES = [
|
|
344
|
+
(3,1),(3,1),(3,1),(3,1),(2,1),(2,1),(3,1),(3,1),
|
|
345
|
+
]
|
|
346
|
+
# Mouth present? (none/smile/dot/open → bool). Exact copy of MOUTHS.
|
|
347
|
+
_DASH_MOUTH = [False, True, True, True, False, True]
|
|
348
|
+
# Hash-derived accessory the dashboard draws BY DEFAULT (renderMascot,
|
|
349
|
+
# pick(ACCESSORIES,h,8)). Exact copy of PixelMascot.tsx ACCESSORIES types.
|
|
350
|
+
_DASH_ACCESSORIES = ["none", "none", "hat", "antenna", "horns", "crown", "bowtie", "none"]
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _js_hash(s: str) -> int:
|
|
354
|
+
"""Port of PixelMascot.tsx hashStr: h=((h<<5)-h+c)|0 == 31*h+c (mod 2^32),
|
|
355
|
+
returned as Math.abs of the signed int32. Must match the dashboard so the
|
|
356
|
+
same name picks the same body/eyes/mouth."""
|
|
357
|
+
h = 0
|
|
358
|
+
for ch in s:
|
|
359
|
+
h = (h * 31 + ord(ch)) & 0xFFFFFFFF
|
|
360
|
+
if h >= 0x80000000:
|
|
361
|
+
h -= 0x100000000
|
|
362
|
+
return abs(h)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _js_pick(arr, h: int, offset: int):
|
|
366
|
+
"""Port of pick(arr, hash, offset) = arr[(hash >>> offset) % len]."""
|
|
367
|
+
return arr[(h >> offset) % len(arr)]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _mirror_body(half):
|
|
371
|
+
"""Port of mirrorBody: row -> reverse(row[0:3]) + row (4 cols -> 7 cols)."""
|
|
372
|
+
return [list(reversed(row[0:3])) + list(row) for row in half]
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _hsl_to_rgb(h: float, s: float, l: float):
|
|
376
|
+
c = (1 - abs(2 * l - 1)) * s
|
|
377
|
+
x = c * (1 - abs((h / 60.0) % 2 - 1))
|
|
378
|
+
m = l - c / 2
|
|
379
|
+
if h < 60: r, g, b = c, x, 0
|
|
380
|
+
elif h < 120: r, g, b = x, c, 0
|
|
381
|
+
elif h < 180: r, g, b = 0, c, x
|
|
382
|
+
elif h < 240: r, g, b = 0, x, c
|
|
383
|
+
elif h < 300: r, g, b = x, 0, c
|
|
384
|
+
else: r, g, b = c, 0, x
|
|
385
|
+
return (round((r + m) * 255), round((g + m) * 255), round((b + m) * 255))
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _agent_color_rgb(name: str, profile_color: str = None):
|
|
389
|
+
"""Exact profile hex when present, else the dashboard hashAgentColor(name)
|
|
390
|
+
fallback: hsl(abs(hash)%360, 70%, 65%) — keeps terminal == dashboard."""
|
|
391
|
+
if profile_color:
|
|
392
|
+
hx = profile_color.strip().lstrip("#")
|
|
393
|
+
if len(hx) == 6:
|
|
394
|
+
try:
|
|
395
|
+
return (int(hx[0:2], 16), int(hx[2:4], 16), int(hx[4:6], 16))
|
|
396
|
+
except ValueError:
|
|
397
|
+
pass
|
|
398
|
+
hue = _js_hash(name) % 360
|
|
399
|
+
return _hsl_to_rgb(hue, 0.70, 0.65)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _supports_truecolor() -> bool:
|
|
403
|
+
"""24-bit detection. Force on/off via env for testing + dumb-terminal safety."""
|
|
404
|
+
if os.environ.get("MESHCODE_FORCE_TRUECOLOR") == "1":
|
|
405
|
+
return True
|
|
406
|
+
if os.environ.get("MESHCODE_NO_TRUECOLOR") or os.environ.get("NO_COLOR"):
|
|
407
|
+
return False
|
|
408
|
+
ct = os.environ.get("COLORTERM", "").lower()
|
|
409
|
+
if "truecolor" in ct or "24bit" in ct:
|
|
410
|
+
return True
|
|
411
|
+
term = os.environ.get("TERM", "").lower()
|
|
412
|
+
if "truecolor" in term or "24bit" in term or "direct" in term:
|
|
413
|
+
return True
|
|
414
|
+
if os.environ.get("TERM_PROGRAM") in ("iTerm.app", "Apple_Terminal", "vscode", "WezTerm", "ghostty"):
|
|
415
|
+
return True
|
|
416
|
+
return False
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def render_pixel_mascot_truecolor(agent_name: str, profile_color: str = None,
|
|
420
|
+
mascot_config: dict = None) -> str:
|
|
421
|
+
"""The agent's REAL dashboard sprite in 24-bit truecolor. Each dashboard
|
|
422
|
+
pixel = two block chars (≈ square in a terminal cell). Falls back to the
|
|
423
|
+
nearest-16 single-color path via the caller when truecolor is unavailable."""
|
|
424
|
+
h = _js_hash(agent_name)
|
|
425
|
+
half = _js_pick(_DASH_BODIES, h, 0)
|
|
426
|
+
eye_row, eye_col = _js_pick(_DASH_EYES, h, 4)
|
|
427
|
+
has_mouth = _js_pick(_DASH_MOUTH, h, 12)
|
|
428
|
+
acc = _js_pick(_DASH_ACCESSORIES, h, 8)
|
|
429
|
+
|
|
430
|
+
grid = [list(r) for r in _mirror_body(half)]
|
|
431
|
+
rows = len(grid)
|
|
432
|
+
cols = len(grid[0])
|
|
433
|
+
# Overlay eyes (mirrored) + mouth as dark pixels, exactly like the canvas.
|
|
434
|
+
if 0 <= eye_row < rows:
|
|
435
|
+
if 0 <= eye_col < cols:
|
|
436
|
+
grid[eye_row][eye_col] = 9
|
|
437
|
+
if 0 <= cols - 1 - eye_col < cols:
|
|
438
|
+
grid[eye_row][cols - 1 - eye_col] = 9
|
|
439
|
+
mouth_row = eye_row + 2
|
|
440
|
+
if has_mouth and 0 <= mouth_row < rows:
|
|
441
|
+
grid[mouth_row][cols // 2] = 9
|
|
442
|
+
|
|
443
|
+
# Accessories — the dashboard draws one hash-derived accessory by default
|
|
444
|
+
# (codes: 4=hat 5=horns 6=crown 7=bowtie 8=antenna-tip). bowtie sits below
|
|
445
|
+
# the face inside the body; hat/antenna/horns/crown need headroom, so we
|
|
446
|
+
# prepend 2 rows and draw above the body's first filled row, matching the
|
|
447
|
+
# canvas geometry (relative to topRow).
|
|
448
|
+
if acc == "bowtie":
|
|
449
|
+
bow = eye_row + 3 # canvas bowY = (eyes.row + 3)
|
|
450
|
+
if 0 <= bow < len(grid):
|
|
451
|
+
for c in (cols // 2 - 1, cols // 2, cols // 2 + 1):
|
|
452
|
+
if 0 <= c < cols:
|
|
453
|
+
grid[bow][c] = 7
|
|
454
|
+
elif acc in ("hat", "antenna", "horns", "crown"):
|
|
455
|
+
grid = [[0] * cols, [0] * cols] + grid
|
|
456
|
+
top = next((i for i, rw in enumerate(grid) if any(rw)), 2)
|
|
457
|
+
cc = cols // 2
|
|
458
|
+
if acc == "hat":
|
|
459
|
+
for x in range(1, cols - 1):
|
|
460
|
+
grid[top - 1][x] = 4 # brim
|
|
461
|
+
for x in range(2, cols - 2):
|
|
462
|
+
grid[top - 2][x] = 4 # crown top
|
|
463
|
+
elif acc == "horns":
|
|
464
|
+
grid[top - 1][1] = 5
|
|
465
|
+
grid[top - 1][cols - 2] = 5
|
|
466
|
+
elif acc == "crown":
|
|
467
|
+
for x in range(1, cols - 1):
|
|
468
|
+
grid[top - 1][x] = 6 # band
|
|
469
|
+
for x in (1, cc, cols - 2):
|
|
470
|
+
grid[top - 2][x] = 6 # spikes
|
|
471
|
+
elif acc == "antenna":
|
|
472
|
+
grid[top - 1][cc] = 1 # stalk = body color
|
|
473
|
+
grid[top - 2][cc] = 8 # white tip
|
|
474
|
+
|
|
475
|
+
r, g, b = _agent_color_rgb(agent_name, profile_color)
|
|
476
|
+
palette = {
|
|
477
|
+
1: (r, g, b),
|
|
478
|
+
2: (min(255, r + 60), min(255, g + 60), min(255, b + 60)), # highlight
|
|
479
|
+
3: (max(0, r - 40), max(0, g - 40), max(0, b - 40)), # shadow
|
|
480
|
+
4: (max(0, r - 30), max(0, g - 30), max(0, b - 30)), # hat (canvas r-30)
|
|
481
|
+
5: (min(255, r + 40), min(255, g + 40), min(255, b + 40)), # horns (canvas r+40)
|
|
482
|
+
6: (251, 191, 36), # crown #fbbf24
|
|
483
|
+
7: (244, 63, 94), # bowtie #f43f5e
|
|
484
|
+
8: (255, 255, 255), # antenna tip
|
|
485
|
+
9: (10, 10, 20), # eyes/mouth #0a0a14
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
out = []
|
|
489
|
+
for grow in grid:
|
|
490
|
+
cells = []
|
|
491
|
+
for v in grow:
|
|
492
|
+
if v == 0:
|
|
493
|
+
cells.append(" ")
|
|
494
|
+
else:
|
|
495
|
+
cr, cg, cb = palette[v]
|
|
496
|
+
cells.append(f"\x1b[38;2;{cr};{cg};{cb}m██\x1b[0m")
|
|
497
|
+
out.append(" " + "".join(cells))
|
|
498
|
+
return "\n".join(out)
|
|
499
|
+
|
|
500
|
+
|
|
319
501
|
YELLOW = "\033[33m"
|
|
320
502
|
STAR = "★"
|
|
321
503
|
|
|
@@ -595,11 +777,17 @@ def render_welcome(agent_name: str, meshwork_name: str, ascii_art: str,
|
|
|
595
777
|
lines.append(f"{color}{BOLD} ╚══════════════════════════════════════════╝{RESET}")
|
|
596
778
|
lines.append("")
|
|
597
779
|
|
|
598
|
-
#
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
780
|
+
# TERMINAL-BOOT-REAL-SPRITE (8e2e14d1): render the agent's REAL dashboard
|
|
781
|
+
# sprite in 24-bit truecolor so the boot banner matches the dashboard
|
|
782
|
+
# PixelMascot "igualito" (exact profile-color, real body/eyes/mouth). Fall
|
|
783
|
+
# back to the nearest-16 single-color Unicode mascot on dumb terminals.
|
|
784
|
+
if _supports_truecolor():
|
|
785
|
+
for line in render_pixel_mascot_truecolor(agent_name, profile_color, mascot_config).split("\n"):
|
|
786
|
+
lines.append(line)
|
|
787
|
+
else:
|
|
788
|
+
art_to_render = render_pixel_mascot(agent_name, mascot_config)
|
|
789
|
+
for line in art_to_render.split("\n"):
|
|
790
|
+
lines.append(f" {color}{line}{RESET}")
|
|
603
791
|
|
|
604
792
|
lines.append("")
|
|
605
793
|
lines.append(f" {BOLD}{color}●{RESET} {BOLD}{agent_name}{RESET}{commander_badge} {DIM}{greeting}{RESET}")
|
|
@@ -55,6 +55,27 @@ try:
|
|
|
55
55
|
except Exception:
|
|
56
56
|
_RUNNING_VERSION = "0.0.0"
|
|
57
57
|
_LAST_UPDATE_KICK_MONO = 0.0
|
|
58
|
+
_REEXEC_GUARD_LOGGED = False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _has_supervisor() -> bool:
|
|
62
|
+
"""True if an OS supervisor (launchd/systemd/Task Scheduler) manages this hostd and will
|
|
63
|
+
relaunch it on clean exit. Field data (Samuel Mac): os.execv is BLOCKED in the sandboxed
|
|
64
|
+
runtime, so where a supervisor exists we PREFER exit-and-relaunch over execv."""
|
|
65
|
+
import platform
|
|
66
|
+
try:
|
|
67
|
+
s = platform.system()
|
|
68
|
+
if s == "Darwin":
|
|
69
|
+
return _hostd_plist_path().exists()
|
|
70
|
+
if s == "Windows":
|
|
71
|
+
return subprocess.run(["schtasks", "/Query", "/TN", _HOSTD_TASK_NAME],
|
|
72
|
+
capture_output=True, text=True).returncode == 0
|
|
73
|
+
if shutil.which("systemctl"):
|
|
74
|
+
return subprocess.run(["systemctl", "--user", "is-enabled", _HOSTD_SYSTEMD_UNIT],
|
|
75
|
+
capture_output=True, text=True).returncode == 0
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
return False
|
|
58
79
|
|
|
59
80
|
|
|
60
81
|
def _maybe_self_restart_on_version_drift() -> None:
|
|
@@ -96,13 +117,42 @@ def _maybe_self_restart_on_version_drift() -> None:
|
|
|
96
117
|
newer = False
|
|
97
118
|
if not newer:
|
|
98
119
|
return
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
# Loop-guard (backend2 finding): in a source/dev run, importlib.metadata can report a
|
|
121
|
+
# pip-installed wheel NEWER than the __init__.py actually executing, so the drift would
|
|
122
|
+
# PERSIST across restart -> storm. Persist the attempt; if we already tried to reach this
|
|
123
|
+
# exact on-disk target recently and didn't advance, skip until the guard window passes.
|
|
124
|
+
global _REEXEC_GUARD_LOGGED
|
|
125
|
+
try:
|
|
126
|
+
_st = _load_state()
|
|
127
|
+
except Exception:
|
|
128
|
+
_st = {}
|
|
129
|
+
_att = _st.get("reexec_attempt") or {}
|
|
130
|
+
if _att.get("target") == ondisk and (time.time() - float(_att.get("at", 0) or 0)) < 600:
|
|
131
|
+
if not _REEXEC_GUARD_LOGGED:
|
|
132
|
+
_REEXEC_GUARD_LOGGED = True
|
|
133
|
+
_log(f"version drift {_RUNNING_VERSION}->{ondisk} but a recent restart didn't advance the "
|
|
134
|
+
f"running version (source/dev run?). Skipping to avoid a restart storm; retry after 600s.")
|
|
135
|
+
return
|
|
136
|
+
_st["reexec_attempt"] = {"target": ondisk, "at": time.time()}
|
|
137
|
+
try:
|
|
138
|
+
_save_state(_st)
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
_log(f"VERSION DRIFT: running {_RUNNING_VERSION}, on-disk {ondisk} -> restart to load new code "
|
|
142
|
+
f"(Stop kill sweep + daemon fixes). headless_pids persisted.")
|
|
143
|
+
# Prefer supervisor-restart where one manages hostd: field data (Samuel Mac) showed os.execv
|
|
144
|
+
# is BLOCKED in the sandboxed runtime and the os._exit->KeepAlive fallback is what actually
|
|
145
|
+
# restarts. So when a supervisor exists, exit cleanly and let it relaunch on the new wheel.
|
|
146
|
+
if _has_supervisor():
|
|
147
|
+
_log("supervisor present -> clean exit; launchd/systemd/schtasks relaunches on new code")
|
|
148
|
+
os._exit(0)
|
|
149
|
+
# No supervisor (e.g. dev foreground run): execv is the only in-place restart. If it's blocked,
|
|
150
|
+
# do NOT self-destruct (nothing would relaunch us) — stay on old code, retry after the guard window.
|
|
101
151
|
try:
|
|
102
152
|
os.execv(sys.executable, [sys.executable, "-m", "meshcode"] + sys.argv[1:])
|
|
103
153
|
except Exception as e:
|
|
104
|
-
_log(f"WARN:
|
|
105
|
-
|
|
154
|
+
_log(f"WARN: no supervisor + execv failed ({e}); staying on {_RUNNING_VERSION}, retry after guard window")
|
|
155
|
+
return
|
|
106
156
|
|
|
107
157
|
STATE_DIR = Path.home() / ".meshcode"
|
|
108
158
|
HOST_ID_PATH = STATE_DIR / "host_id"
|
|
@@ -317,6 +367,16 @@ def _spawn_agent(project: str, agent: str, headless: bool = False) -> bool:
|
|
|
317
367
|
else:
|
|
318
368
|
# POSIX: detach into its own session so it survives the daemon + has no controlling tty.
|
|
319
369
|
kwargs["start_new_session"] = True
|
|
370
|
+
# RELIABILITY (commander field finding): a headless respawn died with "meshcode not on
|
|
371
|
+
# PATH" — the spawned `meshcode run` (and its `meshcode mcp` MCP child) needs the
|
|
372
|
+
# `meshcode` console script findable. Prepend sys.executable's bin dir (where the
|
|
373
|
+
# console script lives) to PATH, mirroring the win32 venv-Scripts injection above.
|
|
374
|
+
try:
|
|
375
|
+
bindir = str(Path(sys.executable).resolve().parent)
|
|
376
|
+
if bindir and bindir not in env.get("PATH", "").split(os.pathsep):
|
|
377
|
+
env["PATH"] = bindir + os.pathsep + env.get("PATH", "")
|
|
378
|
+
except Exception:
|
|
379
|
+
pass
|
|
320
380
|
try:
|
|
321
381
|
# `python -m meshcode` (NOT the meshcode.exe shim) so the .exe isn't held open by the
|
|
322
382
|
# agent -> a background `pip install -U` can replace it on Windows (task 14782bb4 #4).
|
|
@@ -382,8 +442,18 @@ def _do_respawns(api_key: str, host_id: str) -> int:
|
|
|
382
442
|
# do NOT re-record here (that would inflate the count on a mere rate-limit skip).
|
|
383
443
|
_log(f"SKIP respawn {proj}/{agent}: not allowed (count={c.get('respawn_count')}, rate-limited/at-cap)")
|
|
384
444
|
continue
|
|
385
|
-
|
|
445
|
+
# RECYCLE FAST-PATH (task c0fc5597): a recycle-exited agent (recycle_fast) is relaunched
|
|
446
|
+
# PROMPTLY (the RPC returned it at a 15s stale gate, not STALE_SECONDS) and recorded as a
|
|
447
|
+
# RECYCLE (mc_record_recycle), NEVER against the crash respawn cap.
|
|
448
|
+
_is_recycle = bool(c.get("recycle_fast"))
|
|
449
|
+
_log(f"{'RECYCLE-RESPAWN' if _is_recycle else 'RESPAWN'} {proj}/{agent} "
|
|
450
|
+
f"(stale {c.get('heartbeat_age_s')}s, count={c.get('respawn_count')})")
|
|
386
451
|
if _spawn_agent(proj, agent, headless=bool(c.get("headless"))):
|
|
452
|
+
if _is_recycle:
|
|
453
|
+
_rpc("mc_record_recycle",
|
|
454
|
+
{"p_api_key": api_key, "p_project_id": c.get("project_id"), "p_agent_name": agent})
|
|
455
|
+
n += 1
|
|
456
|
+
continue
|
|
387
457
|
rec = _rpc("mc_record_respawn",
|
|
388
458
|
{"p_api_key": api_key, "p_project_id": c.get("project_id"), "p_agent_name": agent})
|
|
389
459
|
# mig404: give-up is ATOMIC inside mc_record_respawn (sets desired_state='crashed').
|
|
@@ -578,6 +648,50 @@ def _do_stops(api_key: str, host_id: str) -> int:
|
|
|
578
648
|
return n
|
|
579
649
|
|
|
580
650
|
|
|
651
|
+
def _do_recycle_enforce(api_key: str, host_id: str) -> int:
|
|
652
|
+
"""RECYCLE-MUST-GUARANTEE-RESPAWN (task c0fc5597). An interactive claude (no --print) can't
|
|
653
|
+
self-exit on must_exit=recycle: the model leaves its loop but the process idles + the MCP
|
|
654
|
+
heartbeat keeps it non-stale, so hostd never respawns it -> recycle degrades to a silent
|
|
655
|
+
sleep. ENFORCE it: an agent that CONSUMED a recycle (mc_recycle_enforce_candidates: marker set
|
|
656
|
+
>= grace ago so its PreCompact handoff already wrote, but STILL ALIVE = didn't exit) gets its
|
|
657
|
+
process FORCE-KILLED. Per commander conditions: kill ONLY the recorded headless PID for THAT
|
|
658
|
+
agent (headless_pids, with _kill_headless_pid's reuse-guard) — NEVER blind cmdline. After the
|
|
659
|
+
kill it goes stale and the recycle FAST-PATH in _do_respawns relaunches it within seconds ->
|
|
660
|
+
SessionStart restores the handoff. Returns number force-killed."""
|
|
661
|
+
res = _rpc("mc_recycle_enforce_candidates", {"p_api_key": api_key, "p_host_id": host_id})
|
|
662
|
+
if not res or not res.get("ok"):
|
|
663
|
+
return 0
|
|
664
|
+
cands = res.get("candidates") or []
|
|
665
|
+
if not cands:
|
|
666
|
+
return 0
|
|
667
|
+
st = _load_state()
|
|
668
|
+
pids = st.get("headless_pids") or {}
|
|
669
|
+
n = 0
|
|
670
|
+
for c in cands:
|
|
671
|
+
proj, agent = c.get("project_name"), c.get("agent")
|
|
672
|
+
if not proj or not agent:
|
|
673
|
+
continue
|
|
674
|
+
target = f"{proj}/{agent}"
|
|
675
|
+
pid = pids.get(target)
|
|
676
|
+
if not pid:
|
|
677
|
+
# commander condition #1: recorded-PID ONLY, never blind cmdline (agents are identical
|
|
678
|
+
# `claude -- boot` on POSIX -> a cmdline guess would kill a HEALTHY agent). An agent
|
|
679
|
+
# spawned by an older hostd has no recorded PID -> can't safely enforce; it needs the
|
|
680
|
+
# one-time relaunch (README #12), after which hostd records its PID + enforce works.
|
|
681
|
+
_log(f"RECYCLE-ENFORCE SKIP {target}: consumed recycle {c.get('recycled_age_s')}s ago "
|
|
682
|
+
f"but still alive and NO recorded PID — needs a one-time relaunch (README #12).")
|
|
683
|
+
continue
|
|
684
|
+
if _kill_headless_pid(target, pid):
|
|
685
|
+
pids.pop(target, None)
|
|
686
|
+
_log(f"RECYCLE-ENFORCE {target}: force-killed stuck-alive recycling agent (pid {pid}, "
|
|
687
|
+
f"recycled {c.get('recycled_age_s')}s ago, hb {c.get('heartbeat_age_s')}s) "
|
|
688
|
+
f"-> fast-path respawn will relaunch it.")
|
|
689
|
+
n += 1
|
|
690
|
+
st["headless_pids"] = pids
|
|
691
|
+
_save_state(st)
|
|
692
|
+
return n
|
|
693
|
+
|
|
694
|
+
|
|
581
695
|
def _do_recycles(api_key: str, host_id: str) -> int:
|
|
582
696
|
"""Uptime-based recycle at task boundary. Returns number recycled."""
|
|
583
697
|
cfg = _rpc("mc_host_config_get", {"p_api_key": api_key, "p_host_id": host_id})
|
|
@@ -1084,13 +1198,17 @@ def cmd_hostd(args: list) -> int:
|
|
|
1084
1198
|
# re-exec NOW (before doing work) so Stop's kill sweep + launch fixes
|
|
1085
1199
|
# always run on a stale-running host without manual intervention.
|
|
1086
1200
|
_maybe_self_restart_on_version_drift()
|
|
1201
|
+
# c0fc5597: enforce recycle BEFORE respawn — force-kill a stuck-alive
|
|
1202
|
+
# recycling agent so it goes stale, then _do_respawns' recycle fast-path
|
|
1203
|
+
# relaunches it within seconds (guaranteed reappearance, no silent sleep).
|
|
1204
|
+
enforced = _do_recycle_enforce(api_key, host_id)
|
|
1087
1205
|
relaunched = _do_respawns(api_key, host_id)
|
|
1088
1206
|
recycled = _do_recycles(api_key, host_id)
|
|
1089
1207
|
ver_recycled = _do_version_recycles(api_key, host_id)
|
|
1090
1208
|
stopped = _do_stops(api_key, host_id)
|
|
1091
1209
|
_up = int(time.monotonic() - _spawn_mono)
|
|
1092
|
-
if relaunched or recycled or ver_recycled or stopped:
|
|
1093
|
-
_log(f"sweep done (uptime={_up}s) — {relaunched} respawned, {recycled} recycled, {ver_recycled} version-recycled, {stopped} stopped")
|
|
1210
|
+
if relaunched or recycled or ver_recycled or stopped or enforced:
|
|
1211
|
+
_log(f"sweep done (uptime={_up}s) — {relaunched} respawned, {recycled} recycled, {ver_recycled} version-recycled, {stopped} stopped, {enforced} recycle-enforced")
|
|
1094
1212
|
elif time.monotonic() - _last_alive_log >= 60:
|
|
1095
1213
|
_log(f"alive — uptime={_up}s")
|
|
1096
1214
|
_last_alive_log = time.monotonic()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshcode
|
|
3
|
-
Version: 2.11.
|
|
3
|
+
Version: 2.11.93
|
|
4
4
|
Summary: Real-time communication between AI agents — Supabase-backed CLI
|
|
5
5
|
Author-email: MeshCode <hello@meshcode.io>
|
|
6
6
|
License: MIT
|
|
@@ -442,6 +442,14 @@ CDN propagation. Wait ~60s and force-fetch directly from the origin:
|
|
|
442
442
|
pip install --no-cache-dir -i https://pypi.org/simple/ meshcode
|
|
443
443
|
```
|
|
444
444
|
|
|
445
|
+
**12. Upgrading to v2.11.88+: relaunch already-running agents and host ONCE**
|
|
446
|
+
From v2.11.88 the host daemon (hostd) auto-restarts itself when a newer SDK lands on disk, so the Stop/kill sweep and launch fixes pick up automatically — and managed agents version-recycle cooperatively to follow. But an agent (or hostd) **already running an older SDK** carries that old code in memory and can't be auto-recycled (old code doesn't honor the recycle signal). Cross the gap once:
|
|
447
|
+
```bash
|
|
448
|
+
pip install --upgrade meshcode # writes the new wheel to disk
|
|
449
|
+
meshcode login # restarts a stale hostd (v2.11.90+ does this only if provably stale)
|
|
450
|
+
```
|
|
451
|
+
Then relaunch each still-old agent once (`meshcode run <project>/<agent>`, or close + reopen its Claude Code session). After this one-time relaunch, every agent obeys cooperative recycle and stays current automatically — no manual restarts thereafter.
|
|
452
|
+
|
|
445
453
|
---
|
|
446
454
|
|
|
447
455
|
## Links
|
|
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
|