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.
Files changed (91) hide show
  1. {meshcode-2.11.90 → meshcode-2.11.93}/PKG-INFO +9 -1
  2. {meshcode-2.11.90 → meshcode-2.11.93}/README.md +8 -0
  3. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/__init__.py +1 -1
  4. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/ascii_art.py +193 -5
  5. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/hostd.py +125 -7
  6. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode.egg-info/PKG-INFO +9 -1
  7. {meshcode-2.11.90 → meshcode-2.11.93}/pyproject.toml +1 -1
  8. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/__main__.py +0 -0
  9. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/_session_handoff_template.py +0 -0
  10. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/_stop_hook_template.py +0 -0
  11. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/atomic_push.py +0 -0
  12. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/claude_update.py +0 -0
  13. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/cli.py +0 -0
  14. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/comms_v4.py +0 -0
  15. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/compat.py +0 -0
  16. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/daemon.py +0 -0
  17. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/date_parse.py +0 -0
  18. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/doctor.py +0 -0
  19. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/error_hints.py +0 -0
  20. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/exceptions.py +0 -0
  21. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/invites.py +0 -0
  22. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/launcher.py +0 -0
  23. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/launcher_install.py +0 -0
  24. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/__init__.py +0 -0
  25. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/__main__.py +0 -0
  26. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/backend.py +0 -0
  27. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/realtime.py +0 -0
  28. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/server.py +0 -0
  29. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  30. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/test_backend.py +0 -0
  31. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  32. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  33. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  34. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  35. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  36. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/preferences.py +0 -0
  37. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/protocol_handler.py +0 -0
  38. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/protocol_v2.py +0 -0
  39. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/quickstart.py +0 -0
  40. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/rpc_allowlist.py +0 -0
  41. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/run_agent.py +0 -0
  42. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/scripts/check_secrets.py +0 -0
  43. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/scripts/race_rate_harness.py +0 -0
  44. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/secrets.py +0 -0
  45. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/self_update.py +0 -0
  46. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/setup_clients.py +0 -0
  47. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/supervisor.py +0 -0
  48. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/up.py +0 -0
  49. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode/upload.py +0 -0
  50. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode.egg-info/SOURCES.txt +0 -0
  51. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode.egg-info/dependency_links.txt +0 -0
  52. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode.egg-info/entry_points.txt +0 -0
  53. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode.egg-info/requires.txt +0 -0
  54. {meshcode-2.11.90 → meshcode-2.11.93}/meshcode.egg-info/top_level.txt +0 -0
  55. {meshcode-2.11.90 → meshcode-2.11.93}/setup.cfg +0 -0
  56. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_auto_update_hardening.py +0 -0
  57. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_autonomous_closegap_1.py +0 -0
  58. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_autonomous_closegap_2.py +0 -0
  59. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_autonomous_closegap_3.py +0 -0
  60. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_autonomous_prompt_inject.py +0 -0
  61. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_boot_bug_regression.py +0 -0
  62. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_color_truecolor.py +0 -0
  63. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_core.py +0 -0
  64. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_cross_agent_messaging.py +0 -0
  65. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_date_parse.py +0 -0
  66. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_doctor.py +0 -0
  67. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_epistemic_v1_python_sdk.py +0 -0
  68. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  69. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_esc_deaf_state.py +0 -0
  70. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_exceptions.py +0 -0
  71. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_file_upload.py +0 -0
  72. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_init_device_code.py +0 -0
  73. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_install_guard.py +0 -0
  74. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_lease_sigterm_release.py +0 -0
  75. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_mark_read_batch.py +0 -0
  76. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_marketplace_ratings.py +0 -0
  77. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_migration_integrity.py +0 -0
  78. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_realtime_event_freshness.py +0 -0
  79. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_rls_cross_tenant.py +0 -0
  80. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_rpc_grants.py +0 -0
  81. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_rpc_migrations.py +0 -0
  82. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_run_agent_dry_run.py +0 -0
  83. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_run_agent_no_server_import.py +0 -0
  84. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_security_regressions.py +0 -0
  85. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_self_update_user_site.py +0 -0
  86. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_sentinel.py +0 -0
  87. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_setup_path.py +0 -0
  88. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_sleep_signals.py +0 -0
  89. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_status_enum_coverage.py +0 -0
  90. {meshcode-2.11.90 → meshcode-2.11.93}/tests/test_stay_on_loop_hook.py +0 -0
  91. {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.90
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
@@ -1,5 +1,5 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.11.90"
2
+ __version__ = "2.11.93"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -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
- # Always use pixel mascot — generates deterministic defaults from agent name if no config
599
- art_to_render = render_pixel_mascot(agent_name, mascot_config)
600
-
601
- for line in art_to_render.split("\n"):
602
- lines.append(f" {color}{line}{RESET}")
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
- _log(f"VERSION DRIFT: running {_RUNNING_VERSION}, on-disk {ondisk} -> self-reexec to load "
100
- f"new code (Stop kill sweep + daemon fixes). headless_pids persisted; KeepAlive re-attaches.")
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: self-reexec failed ({e}); exiting so the supervisor restarts us")
105
- os._exit(0)
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
- _log(f"RESPAWN {proj}/{agent} (stale {c.get('heartbeat_age_s')}s, count={c.get('respawn_count')})")
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.90
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.11.90"
7
+ version = "2.11.93"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes