meshcode 2.11.134__tar.gz → 2.11.135__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 (102) hide show
  1. {meshcode-2.11.134 → meshcode-2.11.135}/PKG-INFO +1 -1
  2. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/hostd.py +0 -146
  4. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/meshcode_mcp/backend.py +13 -8
  5. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/meshcode_mcp/server.py +24 -4
  6. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode.egg-info/PKG-INFO +1 -1
  7. {meshcode-2.11.134 → meshcode-2.11.135}/pyproject.toml +1 -1
  8. {meshcode-2.11.134 → meshcode-2.11.135}/README.md +0 -0
  9. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/__main__.py +0 -0
  10. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/_session_handoff_template.py +0 -0
  11. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/_stop_hook_template.py +0 -0
  12. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/ascii_art.py +0 -0
  13. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/atomic_push.py +0 -0
  14. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/claude_update.py +0 -0
  15. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/cli.py +0 -0
  16. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/comms_v4.py +0 -0
  17. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/compat.py +0 -0
  18. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/daemon.py +0 -0
  19. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/date_parse.py +0 -0
  20. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/doctor.py +0 -0
  21. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/error_hints.py +0 -0
  22. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/exceptions.py +0 -0
  23. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/hooks/__init__.py +0 -0
  24. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/hooks/repo_path_lock.py +0 -0
  25. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/invites.py +0 -0
  26. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/launcher.py +0 -0
  27. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/launcher_install.py +0 -0
  28. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/meshcode_mcp/__init__.py +0 -0
  29. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/meshcode_mcp/__main__.py +0 -0
  30. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/meshcode_mcp/realtime.py +0 -0
  31. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  32. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/meshcode_mcp/swarm.py +0 -0
  33. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/meshcode_mcp/test_backend.py +0 -0
  34. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  35. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  36. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  37. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  38. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  39. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/meshcode_mcp/test_swarm.py +0 -0
  40. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/preferences.py +0 -0
  41. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/protocol_handler.py +0 -0
  42. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/protocol_v2.py +0 -0
  43. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/quickstart.py +0 -0
  44. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/rpc_allowlist.py +0 -0
  45. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/run_agent.py +0 -0
  46. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/scripts/check_secrets.py +0 -0
  47. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/scripts/race_rate_harness.py +0 -0
  48. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/secrets.py +0 -0
  49. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/self_update.py +0 -0
  50. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/setup_clients.py +0 -0
  51. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/supervisor.py +0 -0
  52. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/up.py +0 -0
  53. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode/upload.py +0 -0
  54. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode.egg-info/SOURCES.txt +0 -0
  55. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode.egg-info/dependency_links.txt +0 -0
  56. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode.egg-info/entry_points.txt +0 -0
  57. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode.egg-info/requires.txt +0 -0
  58. {meshcode-2.11.134 → meshcode-2.11.135}/meshcode.egg-info/top_level.txt +0 -0
  59. {meshcode-2.11.134 → meshcode-2.11.135}/setup.cfg +0 -0
  60. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_auto_update_hardening.py +0 -0
  61. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_autonomous_closegap_1.py +0 -0
  62. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_autonomous_closegap_2.py +0 -0
  63. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_autonomous_closegap_3.py +0 -0
  64. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_autonomous_prompt_inject.py +0 -0
  65. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_boot_bug_regression.py +0 -0
  66. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_color_truecolor.py +0 -0
  67. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_core.py +0 -0
  68. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_cross_agent_messaging.py +0 -0
  69. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_date_parse.py +0 -0
  70. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_doctor.py +0 -0
  71. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_epistemic_v1_python_sdk.py +0 -0
  72. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  73. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_esc_deaf_state.py +0 -0
  74. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_exceptions.py +0 -0
  75. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_file_upload.py +0 -0
  76. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_hostd_zombie_sessions.py +0 -0
  77. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_init_device_code.py +0 -0
  78. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_install_guard.py +0 -0
  79. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_lease_sigterm_release.py +0 -0
  80. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_live_mesh_guard.py +0 -0
  81. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_mark_read_batch.py +0 -0
  82. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_marketplace_ratings.py +0 -0
  83. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_migration_integrity.py +0 -0
  84. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_realtime_event_freshness.py +0 -0
  85. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_rls_cross_tenant.py +0 -0
  86. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_rpc_grants.py +0 -0
  87. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_rpc_migrations.py +0 -0
  88. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_run_agent_dry_run.py +0 -0
  89. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_run_agent_no_server_import.py +0 -0
  90. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_security_regressions.py +0 -0
  91. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_self_update_user_site.py +0 -0
  92. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_sentinel.py +0 -0
  93. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_session_replay_gate.py +0 -0
  94. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_setup_path.py +0 -0
  95. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_sleep_signals.py +0 -0
  96. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_status_enum_coverage.py +0 -0
  97. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_stay_on_loop_hook.py +0 -0
  98. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_stop_ghost_terminal.py +0 -0
  99. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_swarm_events.py +0 -0
  100. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_task_progress.py +0 -0
  101. {meshcode-2.11.134 → meshcode-2.11.135}/tests/test_terminal_lifecycle.py +0 -0
  102. {meshcode-2.11.134 → meshcode-2.11.135}/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.134
3
+ Version: 2.11.135
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.11.134"
2
+ __version__ = "2.11.135"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -1037,26 +1037,6 @@ def _recycle_blocked(st, target, now=None):
1037
1037
  return None
1038
1038
 
1039
1039
 
1040
- # Samuel rule 2026-06-04: never recycle a CONNECTED agent (live MCP session) except
1041
- # the >3h uptime lifecycle. BUSY_STATUSES (working/online/busy) MISSES a connected-
1042
- # but-idle agent (status idle/standby, window open, heartbeat fresh) — a fresh
1043
- # heartbeat is the stronger 'live session' signal, so an idle-but-connected agent
1044
- # was being version-recycled out from under the user (the storm he kept seeing).
1045
- _CONNECTED_HEARTBEAT_S = _env_int("MESHCODE_CONNECTED_HEARTBEAT_SEC", 60, 10)
1046
-
1047
-
1048
- def _agent_connected(a) -> bool:
1049
- """True if the agent has a live MCP session: an explicitly-busy status OR a
1050
- very-recent heartbeat (window open even when idle/standby)."""
1051
- if (a.get("status") or "") in BUSY_STATUSES:
1052
- return True
1053
- hb = a.get("heartbeat_age_s")
1054
- try:
1055
- return hb is not None and float(hb) < _CONNECTED_HEARTBEAT_S
1056
- except (TypeError, ValueError):
1057
- return False
1058
-
1059
-
1060
1040
  # Part 2 (Samuel req #2 2026-06-04): on a VISIBLE recycle, close the OLD window so
1061
1041
  # old+new don't both stay open (audit gap 6a203baa). DRY-RUN first (commander q2 +
1062
1042
  # reaper safe-arm pattern): log-only until the logs confirm it's ONLY the old pid.
@@ -2122,57 +2102,6 @@ def _do_recycles(api_key: str, host_id: str) -> int:
2122
2102
  # it stays dead even if a schedule row reappears. Crash-RESPAWN (_do_respawns) is
2123
2103
  # UNAFFECTED — only RECYCLE triggers are killed.
2124
2104
  return 0
2125
- cfg = _rpc("mc_host_config_get", {"p_api_key": api_key, "p_host_id": host_id})
2126
- if not cfg or not cfg.get("ok"):
2127
- return 0
2128
- pol = _rpc("mc_host_recycle_policy", {"p_api_key": api_key, "p_host_id": host_id})
2129
- mode = (pol or {}).get("recycle_mode")
2130
- value = (pol or {}).get("recycle_value")
2131
- if mode != "time" or not value:
2132
- return 0 # context-recycle is agent-cooperative; only time is daemon-driven
2133
- st = _load_state()
2134
- now = time.time()
2135
- n = 0
2136
- seen = set()
2137
- for a in cfg.get("agents", []):
2138
- if a.get("desired_state") != "running":
2139
- continue
2140
- proj, agent = a.get("project_name"), a.get("name")
2141
- if not proj or not agent:
2142
- continue
2143
- key = f"{a.get('project_id')}/{agent}"
2144
- seen.add(key)
2145
- first = st.get(key)
2146
- if first is None:
2147
- st[key] = now # start the uptime clock on first observation
2148
- continue
2149
- if (a.get("status") or "") in BUSY_STATUSES:
2150
- continue # task boundary only — never mid-task
2151
- if (now - first) >= float(value) * 3600.0:
2152
- _log(f"RECYCLE {proj}/{agent} (uptime {(now-first)/3600:.1f}h >= {value}h)")
2153
- # Server-authorized clean-exit (task 548c863e, mig 364): SIGNAL the
2154
- # recycle instead of spawning. A direct _spawn_agent here duplicates
2155
- # the still-alive process. mc_request_recycle sets a flag; the
2156
- # agent's wait-loop consumes it, returns must_exit/reason=recycle,
2157
- # exits CLEAN (Stop-hook writes the handoff), then the respawn path
2158
- # (_do_respawns) relaunches it fresh. Recorded via mc_record_recycle
2159
- # so it's NEVER counted against the crash respawn cap.
2160
- req = _rpc("mc_request_recycle",
2161
- {"p_api_key": api_key, "p_project_id": a.get("project_id"), "p_agent_name": agent})
2162
- if req and req.get("ok") and req.get("requested"):
2163
- _rpc("mc_record_recycle",
2164
- {"p_api_key": api_key, "p_project_id": a.get("project_id"), "p_agent_name": agent})
2165
- st[key] = now # reset clock after signaling recycle
2166
- n += 1
2167
- elif req and req.get("ok") and not req.get("requested"):
2168
- # Agent flipped busy between the roster read and the request —
2169
- # skip; retry next sweep at the next task boundary.
2170
- _log(f"SKIP recycle {proj}/{agent}: {req.get('reason','not_requested')}")
2171
- # prune state for agents no longer managed on this host
2172
- for k in [k for k in st if k not in seen]:
2173
- st.pop(k, None)
2174
- _save_state(st)
2175
- return n
2176
2105
 
2177
2106
 
2178
2107
  # ------------------------------------------------------------------
@@ -2502,10 +2431,6 @@ def _hostd_bootstrap(verbose: bool = False) -> bool:
2502
2431
  return False
2503
2432
 
2504
2433
 
2505
- # a69ac010-C: after this many version-recycles at the SAME cv with no advance, STOP (env won't converge).
2506
- _VERREC_MAX_ATTEMPTS = 3
2507
-
2508
-
2509
2434
  def _do_version_recycles(api_key: str, host_id: str) -> int:
2510
2435
  """Recycle managed agents running an OLDER meshcode than what's installed on disk so they pick up
2511
2436
  the auto-updated version (task 14782bb4 D). GRACIOUS (guardrail #5): idle-only (NEVER a BUSY agent /
@@ -2516,77 +2441,6 @@ def _do_version_recycles(api_key: str, host_id: str) -> int:
2516
2441
  # This was the env-mismatch storm source; fixed-at-source via run_agent env-sync, but the
2517
2442
  # whole recycle feature is being removed per owner. Crash-RESPAWN is unaffected.
2518
2443
  return 0
2519
- cfg = _rpc("mc_host_config_get", {"p_api_key": api_key, "p_host_id": host_id})
2520
- if not cfg or not cfg.get("ok"):
2521
- return 0
2522
- try:
2523
- from meshcode import self_update as _su
2524
- disk = _su._current_version()
2525
- except Exception:
2526
- return 0
2527
- if not disk:
2528
- return 0
2529
- st = _load_state()
2530
- now = time.time()
2531
- n = 0
2532
- for a in cfg.get("agents", []):
2533
- if a.get("desired_state") != "running":
2534
- continue
2535
- cv = a.get("cli_version")
2536
- if not cv:
2537
- continue
2538
- try:
2539
- if not _su._is_newer(disk, cv):
2540
- continue # agent already on the on-disk version (or newer) — nothing to do
2541
- except Exception:
2542
- continue
2543
- if _agent_connected(a):
2544
- continue # Samuel rule: never version-recycle a CONNECTED agent (live MCP session,
2545
- # even if idle/standby) — the >3h uptime lifecycle (_do_recycles) is the
2546
- # only recycle that may touch a connected agent.
2547
- proj, agent = a.get("project_name"), a.get("name")
2548
- if not proj or not agent:
2549
- continue
2550
- if _recycle_blocked(st, f"{proj}/{agent}", now):
2551
- continue # 451d33a0 unify: already blocked as a non-converging recycle — don't also version-recycle it
2552
- key = f"verrec/{a.get('project_id')}/{agent}"
2553
- if now - float(st.get(key, 0) or 0) < 1800:
2554
- continue # rate-limit: <=1 version-recycle per agent / 30min (no recycle-storm)
2555
- # a69ac010-C CONVERGENCE GUARD: if we've already version-recycled this agent _VERREC_MAX_ATTEMPTS
2556
- # times at THIS exact cv and it never advanced, the agent's spawn env is OLDER than hostd's disk
2557
- # and recycling will NEVER converge (respawn pulls the same stale env -> same cv -> recycle again).
2558
- # STOP + alert ONCE instead of version-recycling every 30min forever (the env-mismatch storm RC).
2559
- conv_key = f"verrec_conv/{a.get('project_id')}/{agent}"
2560
- conv = dict(st.get(conv_key) or {})
2561
- if conv.get("cv") == cv and int(conv.get("count", 0)) >= _VERREC_MAX_ATTEMPTS:
2562
- if not conv.get("alerted"):
2563
- conv["alerted"] = True
2564
- st[conv_key] = conv
2565
- _save_state(st)
2566
- _log(f"VERSION-RECYCLE STUCK {proj}/{agent}: still {cv} after {conv['count']} recycles "
2567
- f"(disk {disk}); spawn env older than hostd — align it (e.g. "
2568
- f"~/meshcode-env/bin/pip install -U meshcode). BLOCKING further version-recycles "
2569
- f"until cv advances. [recycle_blocked_reason=version_no_converge]")
2570
- continue
2571
- req = _rpc("mc_request_recycle",
2572
- {"p_api_key": api_key, "p_project_id": a.get("project_id"), "p_agent_name": agent})
2573
- if req and req.get("ok") and req.get("requested"):
2574
- _rpc("mc_record_recycle",
2575
- {"p_api_key": api_key, "p_project_id": a.get("project_id"), "p_agent_name": agent})
2576
- st[key] = now
2577
- # advance convergence counter for THIS cv; a cv change (env fixed) resets + releases the guard.
2578
- if conv.get("cv") == cv:
2579
- conv["count"] = int(conv.get("count", 0)) + 1
2580
- else:
2581
- conv = {"cv": cv, "count": 1}
2582
- conv["alerted"] = False
2583
- st[conv_key] = conv
2584
- n += 1
2585
- _log(f"VERSION-RECYCLE {proj}/{agent}: running {cv} < disk {disk} — recycle at idle to update "
2586
- f"(attempt {conv['count']}/{_VERREC_MAX_ATTEMPTS})")
2587
- if n:
2588
- _save_state(st)
2589
- return n
2590
2444
 
2591
2445
 
2592
2446
  # c301be69: hold the singleton lock handle for the daemon's lifetime — module
@@ -907,16 +907,21 @@ def take_memory_hints() -> List[Dict]:
907
907
  return out
908
908
 
909
909
 
910
- def read_inbox(project_id: str, agent: str, mark_read: bool = True, api_key: Optional[str] = None) -> List[Dict]:
911
- # Use SECURITY DEFINER RPC when api_key is available (bypasses RLS safely)
910
+ def read_inbox(project_id: str, agent: str, mark_read: bool = True, api_key: Optional[str] = None,
911
+ rpc_result: Optional[Dict] = None) -> List[Dict]:
912
+ # Use SECURITY DEFINER RPC when api_key is available (bypasses RLS safely).
913
+ # rpc_result (mig 521 wait-drain): a pre-fetched mc_read_inbox payload (the
914
+ # 'inbox' field of mc_wait_poll v5). When given, skip the RPC and run the
915
+ # identical post-process (decrypt, memory_hints, ack) on it.
912
916
  global _LAST_MEMORY_HINTS
913
917
  if api_key:
914
- rpc_result = sb_rpc("mc_read_inbox", {
915
- "p_api_key": api_key,
916
- "p_project_id": project_id,
917
- "p_agent_name": agent,
918
- "p_mark_read": mark_read,
919
- })
918
+ if rpc_result is None:
919
+ rpc_result = sb_rpc("mc_read_inbox", {
920
+ "p_api_key": api_key,
921
+ "p_project_id": project_id,
922
+ "p_agent_name": agent,
923
+ "p_mark_read": mark_read,
924
+ })
920
925
  if isinstance(rpc_result, dict) and rpc_result.get("ok"):
921
926
  # mig 220: stash memory_hints so the calling tool can attach
922
927
  # them to its response. Reset on every call so stale hints
@@ -3665,6 +3665,11 @@ def _wait_poll_or_legacy() -> Dict[str, Any]:
3665
3665
  "p_api_key": api_key,
3666
3666
  "p_project_id": _PROJECT_ID,
3667
3667
  "p_agent_name": AGENT_NAME,
3668
+ # wait-loop fase 2 (proposal 0bd63160, mig 521): the server
3669
+ # folds read_inbox INTO the poll when there is pending mail —
3670
+ # a message-carrying cycle is 1 RPC instead of 2. mc_wait_poll
3671
+ # v5 (5-arg, default false) serves old SDKs unchanged.
3672
+ "p_drain_inbox": True,
3668
3673
  })
3669
3674
  if isinstance(resp, dict) and resp.get("ok"):
3670
3675
  return {
@@ -3677,6 +3682,9 @@ def _wait_poll_or_legacy() -> Dict[str, Any]:
3677
3682
  # Surface it so the caller folds that RPC away. None on
3678
3683
  # the legacy path = caller must count_pending itself.
3679
3684
  "pending_count": resp.get("pending_count"),
3685
+ # mig 521: pre-drained mc_read_inbox result (same shape the
3686
+ # RPC returns) — present only when pending_count>0.
3687
+ "inbox": resp.get("inbox"),
3680
3688
  "_via": "mc_wait_poll",
3681
3689
  }
3682
3690
  except Exception as e:
@@ -3687,21 +3695,30 @@ def _wait_poll_or_legacy() -> Dict[str, Any]:
3687
3695
  "stop": _check_stop_request(),
3688
3696
  "tasks": _get_pending_tasks_summary(),
3689
3697
  "pending_count": None,
3698
+ "inbox": None,
3690
3699
  "_via": "legacy",
3691
3700
  }
3692
3701
 
3693
3702
 
3694
- def _drain_unread_response(include_acks: bool) -> Optional[Dict[str, Any]]:
3703
+ def _drain_unread_response(include_acks: bool,
3704
+ rpc_result: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
3695
3705
  """SDK-EFF (task ab1f9f5a): read+mark unread DB messages and shape them as
3696
3706
  a wait response. Extracted from the inner-loop's old final-fallback tail so
3697
3707
  the outer loop can invoke it only when mc_wait_poll reports pending_count>0
3698
3708
  (was: an unconditional count_pending every idle cycle). Returns None when
3699
- nothing real (acks only / already seen) was found."""
3709
+ nothing real (acks only / already seen) was found.
3710
+
3711
+ rpc_result (mig 521, proposal 0bd63160): pre-drained mc_read_inbox payload
3712
+ embedded in the mc_wait_poll v5 response ('inbox' key). When given, NO
3713
+ read_inbox RPC fires — backend.read_inbox still runs its full post-process
3714
+ (decrypt, memory_hints, ack) on the embedded payload, so semantics are
3715
+ byte-identical to the 2-RPC path."""
3700
3716
  try:
3701
3717
  api_key = _get_api_key()
3702
3718
  if not api_key:
3703
3719
  return None
3704
- raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=True, api_key=api_key)
3720
+ raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=True, api_key=api_key,
3721
+ rpc_result=rpc_result)
3705
3722
  if not raw:
3706
3723
  return None
3707
3724
  msgs = [
@@ -4591,7 +4608,10 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
4591
4608
  except Exception:
4592
4609
  _db_pending = 0
4593
4610
  if _db_pending > 0:
4594
- _drained = _drain_unread_response(include_acks)
4611
+ # mig 521: the poll already drained the inbox server-side
4612
+ # (1 RPC); pass it through. None => classic read_inbox RPC.
4613
+ _drained = _drain_unread_response(include_acks,
4614
+ rpc_result=_wp.get("inbox"))
4595
4615
  if _drained:
4596
4616
  result = _drained
4597
4617
  _set_state("online", "")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.134
3
+ Version: 2.11.135
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.11.134"
7
+ version = "2.11.135"
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
File without changes