meshcode 2.11.131__tar.gz → 2.11.132__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.131 → meshcode-2.11.132}/PKG-INFO +1 -1
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/__init__.py +1 -1
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/hostd.py +26 -2
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/meshcode_mcp/backend.py +77 -15
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/meshcode_mcp/server.py +125 -48
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.131 → meshcode-2.11.132}/pyproject.toml +1 -1
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_stop_ghost_terminal.py +4 -1
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_wait_open_tasks_contradiction.py +5 -1
- {meshcode-2.11.131 → meshcode-2.11.132}/README.md +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/__main__.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/cli.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/compat.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/daemon.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/doctor.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/hooks/__init__.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/hooks/repo_path_lock.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/invites.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/launcher.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/meshcode_mcp/swarm.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/meshcode_mcp/test_swarm.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/preferences.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/run_agent.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/secrets.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/self_update.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/up.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode/upload.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/setup.cfg +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_core.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_doctor.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_hostd_zombie_sessions.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_live_mesh_guard.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_session_replay_gate.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_swarm_events.py +0 -0
- {meshcode-2.11.131 → meshcode-2.11.132}/tests/test_terminal_lifecycle.py +0 -0
|
@@ -473,6 +473,30 @@ def _rpc(fn: str, payload: dict) -> Optional[dict]:
|
|
|
473
473
|
return None
|
|
474
474
|
|
|
475
475
|
|
|
476
|
+
# SDK-EFF (task ab1f9f5a): mc_host_config_get was ~75k calls in the prod audit
|
|
477
|
+
# because every sweep helper fetched the full roster independently. One fetch
|
|
478
|
+
# now serves the whole ~10s sweep via a short TTL cache. TTL stays under
|
|
479
|
+
# POLL_INTERVAL_SEC so each sweep still sees a fresh roster — an etag scheme
|
|
480
|
+
# would never hit anyway: the payload embeds live heartbeat_age_s, so the
|
|
481
|
+
# server-side hash changes on every call by construction.
|
|
482
|
+
_HOST_CFG_CACHE: dict = {"key": None, "at": 0.0, "cfg": None}
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _host_cfg(api_key: str, host_id: str) -> Optional[dict]:
|
|
486
|
+
"""mc_host_config_get with a per-sweep TTL cache. Failures are NOT cached."""
|
|
487
|
+
ttl = max(3.0, min(float(POLL_INTERVAL_SEC) - 1.0, 8.0))
|
|
488
|
+
now = time.monotonic()
|
|
489
|
+
c = _HOST_CFG_CACHE
|
|
490
|
+
if c["cfg"] is not None and c["key"] == (api_key, host_id) and now - c["at"] < ttl:
|
|
491
|
+
return c["cfg"]
|
|
492
|
+
cfg = _rpc("mc_host_config_get", {"p_api_key": api_key, "p_host_id": host_id})
|
|
493
|
+
if cfg and cfg.get("ok"):
|
|
494
|
+
c["key"] = (api_key, host_id)
|
|
495
|
+
c["at"] = now
|
|
496
|
+
c["cfg"] = cfg
|
|
497
|
+
return cfg
|
|
498
|
+
|
|
499
|
+
|
|
476
500
|
# ------------------------------------------------------------------
|
|
477
501
|
# Respawn
|
|
478
502
|
# ------------------------------------------------------------------
|
|
@@ -1808,7 +1832,7 @@ def _do_stopped_ghost_sweep(api_key: str, host_id: str) -> int:
|
|
|
1808
1832
|
<target>' cmdlines (token-safe) or direct children of the target's own
|
|
1809
1833
|
hostd launcher .command — a human `claude --resume` in a plain tab can't
|
|
1810
1834
|
match either. Returns number of ghosts killed."""
|
|
1811
|
-
cfg =
|
|
1835
|
+
cfg = _host_cfg(api_key, host_id) # SDK-EFF ab1f9f5a: per-sweep TTL cache
|
|
1812
1836
|
if not cfg or not cfg.get("ok"):
|
|
1813
1837
|
return 0
|
|
1814
1838
|
st = _load_state()
|
|
@@ -1932,7 +1956,7 @@ def _do_reap(api_key: str, host_id: str) -> int:
|
|
|
1932
1956
|
NEVER touches: a VISIBLE agent (user owns the window), or a desired_state='running' agent (workers are
|
|
1933
1957
|
sacred — only the dup-EXTRA of a running agent is reapable, never its live survivor). Returns count
|
|
1934
1958
|
actually killed (0 while DRY-RUN)."""
|
|
1935
|
-
cfg =
|
|
1959
|
+
cfg = _host_cfg(api_key, host_id) # SDK-EFF ab1f9f5a: per-sweep TTL cache
|
|
1936
1960
|
if not cfg or not cfg.get("ok"):
|
|
1937
1961
|
return 0
|
|
1938
1962
|
agents = cfg.get("agents") or []
|
|
@@ -444,7 +444,13 @@ _SKIP_RECORDING = {"mc_heartbeat", "mc_record_event", "mc_agent_set_status_by_ap
|
|
|
444
444
|
# ~18k err/12h log pollution. The two call sites already
|
|
445
445
|
# treat it as soft-fail. Silence telemetry until Samuel
|
|
446
446
|
# approves the public-wrapper DDL.
|
|
447
|
-
"mc_tasks_due_soon"
|
|
447
|
+
"mc_tasks_due_soon",
|
|
448
|
+
# SDK-EFF (task ab1f9f5a): the consolidated wait-loop poll and
|
|
449
|
+
# the two consume RPCs it superseded are pure infrastructure —
|
|
450
|
+
# recording them re-creates the exact null-value noise the
|
|
451
|
+
# task_list/count_pending skip above was added to kill
|
|
452
|
+
# (1 mc_record_event per idle cycle, fleet-wide).
|
|
453
|
+
"mc_wait_poll", "mc_consume_stop", "mc_consume_recycle"}
|
|
448
454
|
|
|
449
455
|
|
|
450
456
|
def _get_api_key() -> Optional[str]:
|
|
@@ -511,22 +517,78 @@ def sb_rpc(fn_name: str, params: Dict, *, _max_retries: int = 3) -> Any:
|
|
|
511
517
|
code="0", source="network")
|
|
512
518
|
|
|
513
519
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
520
|
+
# SDK-EFF batching (task ab1f9f5a, audit c81c3b20): mc_record_event was the #1
|
|
521
|
+
# RPC in prod (~353k calls/wk) because _bg_record fired one RPC AND one thread
|
|
522
|
+
# per recorded event. Events now buffer in-process and a single daemon flusher
|
|
523
|
+
# ships them as ONE mc_record_events_batch call (live in prod, server caps 50
|
|
524
|
+
# events/batch) every _EVENT_FLUSH_SECS or as soon as _EVENT_FLUSH_MAX pile up.
|
|
525
|
+
# Recording stays best-effort: a failed flush drops that slice (the per-event
|
|
526
|
+
# path dropped on failure too) — a dead DB must never grow memory unbounded.
|
|
527
|
+
_EVENT_BUF: List[Dict] = []
|
|
528
|
+
_EVENT_BUF_LOCK = _threading.Lock()
|
|
529
|
+
_EVENT_FLUSH_SECS = 5.0
|
|
530
|
+
_EVENT_FLUSH_MAX = 40 # flush-early threshold, under the 50 server cap
|
|
531
|
+
_EVENT_BUF_HARD_CAP = 400 # runaway guard: beyond this, oldest events drop
|
|
532
|
+
_EVENT_FLUSH_WAKE = _threading.Event()
|
|
533
|
+
_event_flusher_started = False
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _flush_events_now():
|
|
537
|
+
"""Drain the buffer in <=50-event slices (server batch cap). Best-effort."""
|
|
538
|
+
while True:
|
|
539
|
+
with _EVENT_BUF_LOCK:
|
|
540
|
+
if not _EVENT_BUF:
|
|
541
|
+
return
|
|
542
|
+
batch = _EVENT_BUF[:50]
|
|
543
|
+
del _EVENT_BUF[:50]
|
|
544
|
+
resp = sb_rpc_raw("mc_record_events_batch", {
|
|
545
|
+
"p_api_key": _recording_api_key,
|
|
546
|
+
"p_project_id": _recording_project_id,
|
|
547
|
+
"p_agent_name": _recording_agent_name,
|
|
548
|
+
"p_events": batch,
|
|
549
|
+
})
|
|
550
|
+
if resp is None or not (isinstance(resp, dict) and resp.get("ok")):
|
|
551
|
+
log.debug("event batch flush failed; %d events dropped", len(batch))
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _event_flusher_loop():
|
|
555
|
+
while True:
|
|
556
|
+
_EVENT_FLUSH_WAKE.wait(timeout=_EVENT_FLUSH_SECS)
|
|
557
|
+
_EVENT_FLUSH_WAKE.clear()
|
|
518
558
|
try:
|
|
519
|
-
|
|
520
|
-
"p_api_key": _recording_api_key,
|
|
521
|
-
"p_project_id": _recording_project_id,
|
|
522
|
-
"p_agent_name": _recording_agent_name,
|
|
523
|
-
"p_session_id": _recording_session_id,
|
|
524
|
-
"p_event_type": event_type,
|
|
525
|
-
"p_payload": payload,
|
|
526
|
-
})
|
|
559
|
+
_flush_events_now()
|
|
527
560
|
except Exception as e:
|
|
528
|
-
log.debug(f"
|
|
529
|
-
|
|
561
|
+
log.debug(f"event flusher error: {e}")
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _ensure_event_flusher():
|
|
565
|
+
global _event_flusher_started
|
|
566
|
+
if _event_flusher_started:
|
|
567
|
+
return
|
|
568
|
+
with _EVENT_BUF_LOCK:
|
|
569
|
+
if _event_flusher_started:
|
|
570
|
+
return
|
|
571
|
+
_event_flusher_started = True
|
|
572
|
+
_threading.Thread(target=_event_flusher_loop, daemon=True,
|
|
573
|
+
name="mc-event-flusher").start()
|
|
574
|
+
import atexit
|
|
575
|
+
atexit.register(_flush_events_now)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _bg_record(event_type: str, payload: dict):
|
|
579
|
+
"""Buffer a session event for batched recording — never blocks the caller."""
|
|
580
|
+
with _EVENT_BUF_LOCK:
|
|
581
|
+
if len(_EVENT_BUF) >= _EVENT_BUF_HARD_CAP:
|
|
582
|
+
del _EVENT_BUF[:_EVENT_FLUSH_MAX]
|
|
583
|
+
_EVENT_BUF.append({
|
|
584
|
+
"session_id": _recording_session_id,
|
|
585
|
+
"event_type": event_type,
|
|
586
|
+
"payload": payload,
|
|
587
|
+
})
|
|
588
|
+
buffered = len(_EVENT_BUF)
|
|
589
|
+
_ensure_event_flusher()
|
|
590
|
+
if buffered >= _EVENT_FLUSH_MAX:
|
|
591
|
+
_EVENT_FLUSH_WAKE.set()
|
|
530
592
|
|
|
531
593
|
|
|
532
594
|
def sb_rpc_raw(fn_name: str, params: Dict) -> Any:
|
|
@@ -3671,6 +3671,12 @@ def _wait_poll_or_legacy() -> Dict[str, Any]:
|
|
|
3671
3671
|
"recycle": bool(resp.get("recycle")),
|
|
3672
3672
|
"stop": bool(resp.get("stop")),
|
|
3673
3673
|
"tasks": _pending_filter(resp.get("tasks") or []),
|
|
3674
|
+
# SDK-EFF (task ab1f9f5a): mc_wait_poll has returned the
|
|
3675
|
+
# unread count since mig 457, but the SDK ignored it and
|
|
3676
|
+
# kept its own count_pending in the inner-loop tail.
|
|
3677
|
+
# Surface it so the caller folds that RPC away. None on
|
|
3678
|
+
# the legacy path = caller must count_pending itself.
|
|
3679
|
+
"pending_count": resp.get("pending_count"),
|
|
3674
3680
|
"_via": "mc_wait_poll",
|
|
3675
3681
|
}
|
|
3676
3682
|
except Exception as e:
|
|
@@ -3680,10 +3686,48 @@ def _wait_poll_or_legacy() -> Dict[str, Any]:
|
|
|
3680
3686
|
"recycle": _check_recycle_request(),
|
|
3681
3687
|
"stop": _check_stop_request(),
|
|
3682
3688
|
"tasks": _get_pending_tasks_summary(),
|
|
3689
|
+
"pending_count": None,
|
|
3683
3690
|
"_via": "legacy",
|
|
3684
3691
|
}
|
|
3685
3692
|
|
|
3686
3693
|
|
|
3694
|
+
def _drain_unread_response(include_acks: bool) -> Optional[Dict[str, Any]]:
|
|
3695
|
+
"""SDK-EFF (task ab1f9f5a): read+mark unread DB messages and shape them as
|
|
3696
|
+
a wait response. Extracted from the inner-loop's old final-fallback tail so
|
|
3697
|
+
the outer loop can invoke it only when mc_wait_poll reports pending_count>0
|
|
3698
|
+
(was: an unconditional count_pending every idle cycle). Returns None when
|
|
3699
|
+
nothing real (acks only / already seen) was found."""
|
|
3700
|
+
try:
|
|
3701
|
+
api_key = _get_api_key()
|
|
3702
|
+
if not api_key:
|
|
3703
|
+
return None
|
|
3704
|
+
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=True, api_key=api_key)
|
|
3705
|
+
if not raw:
|
|
3706
|
+
return None
|
|
3707
|
+
msgs = [
|
|
3708
|
+
{"from": m["from_agent"], "type": m.get("type", "msg"),
|
|
3709
|
+
"ts": m.get("created_at"), "payload": m.get("payload", {}),
|
|
3710
|
+
"id": m.get("id"), "parent_id": m.get("parent_msg_id")}
|
|
3711
|
+
for m in raw
|
|
3712
|
+
]
|
|
3713
|
+
deduped = _filter_and_mark(msgs)
|
|
3714
|
+
if not deduped:
|
|
3715
|
+
return None
|
|
3716
|
+
split = _split_messages(deduped)
|
|
3717
|
+
if not include_acks:
|
|
3718
|
+
split["acks"] = []
|
|
3719
|
+
if split["messages"] or split["done_signals"]:
|
|
3720
|
+
return {
|
|
3721
|
+
"got_message": True,
|
|
3722
|
+
**split,
|
|
3723
|
+
"auto_marked": len(deduped),
|
|
3724
|
+
"types_seen": sorted({m.get("type", "msg") for m in deduped}),
|
|
3725
|
+
}
|
|
3726
|
+
except Exception as e:
|
|
3727
|
+
log.debug(f"drain_unread error: {e}")
|
|
3728
|
+
return None
|
|
3729
|
+
|
|
3730
|
+
|
|
3687
3731
|
def _check_recycle_request() -> bool:
|
|
3688
3732
|
"""Server-authorized recycle (task 548c863e, mig 364).
|
|
3689
3733
|
|
|
@@ -3735,7 +3779,8 @@ def _check_stop_request() -> bool:
|
|
|
3735
3779
|
return False
|
|
3736
3780
|
|
|
3737
3781
|
|
|
3738
|
-
def _try_auto_claim_self_assigned_tasks(max_claims: int = 20
|
|
3782
|
+
def _try_auto_claim_self_assigned_tasks(max_claims: int = 20,
|
|
3783
|
+
tasks: Optional[List[Dict[str, Any]]] = None) -> List[Dict[str, str]]:
|
|
3739
3784
|
"""URGENT_RUNTIME_FIX_TASK_PULL (Samuel directive 2026-05-26T04:20Z msg
|
|
3740
3785
|
a15d7ed4 + commander msg 0cfca3f3 AUTOCLAIM_ON_RECONNECT): claim every
|
|
3741
3786
|
OPEN task already ASSIGNED to me but not yet claimed by anyone. Closes
|
|
@@ -3745,6 +3790,10 @@ def _try_auto_claim_self_assigned_tasks(max_claims: int = 20) -> List[Dict[str,
|
|
|
3745
3790
|
"antes siempre que habia task assigned el agente sabia que tenia un
|
|
3746
3791
|
task assigned pendiente y no paraba hasta completar todos").
|
|
3747
3792
|
|
|
3793
|
+
SDK-EFF (task ab1f9f5a): pass `tasks` (full task rows already fetched by
|
|
3794
|
+
the caller) to skip this function's own mc_task_list round-trip — the
|
|
3795
|
+
wait-entry path was fetching the identical list twice back-to-back.
|
|
3796
|
+
|
|
3748
3797
|
Returns the list of claimed task summaries (may be empty).
|
|
3749
3798
|
"""
|
|
3750
3799
|
claimed: List[Dict[str, str]] = []
|
|
@@ -3752,10 +3801,11 @@ def _try_auto_claim_self_assigned_tasks(max_claims: int = 20) -> List[Dict[str,
|
|
|
3752
3801
|
api_key = _get_api_key()
|
|
3753
3802
|
if not api_key:
|
|
3754
3803
|
return claimed
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3804
|
+
if tasks is None:
|
|
3805
|
+
result = be.task_list(api_key, _PROJECT_ID, AGENT_NAME, status_filter="open")
|
|
3806
|
+
if not isinstance(result, dict) or not result.get("ok"):
|
|
3807
|
+
return claimed
|
|
3808
|
+
tasks = result.get("tasks", [])
|
|
3759
3809
|
candidates = [
|
|
3760
3810
|
t for t in tasks
|
|
3761
3811
|
if t.get("assignee") == AGENT_NAME
|
|
@@ -4302,11 +4352,23 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
|
|
|
4302
4352
|
# offline at create time" gap. Always-on, not gated on
|
|
4303
4353
|
# autonomous_mode. Best-effort.
|
|
4304
4354
|
try:
|
|
4305
|
-
|
|
4355
|
+
# SDK-EFF (task ab1f9f5a): ONE mc_task_list serves both the auto-claim
|
|
4356
|
+
# sweep and the pending summary below — they were identical back-to-back
|
|
4357
|
+
# fetches. The post-claim snapshot stays valid for _pending_filter:
|
|
4358
|
+
# a just-claimed task still matches on assignee==me with status 'open'.
|
|
4359
|
+
_entry_tasks_raw = None
|
|
4360
|
+
try:
|
|
4361
|
+
_tl = be.task_list(_get_api_key(), _PROJECT_ID, AGENT_NAME, status_filter=None)
|
|
4362
|
+
if isinstance(_tl, dict) and _tl.get("ok"):
|
|
4363
|
+
_entry_tasks_raw = _tl.get("tasks", [])
|
|
4364
|
+
except Exception:
|
|
4365
|
+
_entry_tasks_raw = None
|
|
4366
|
+
_wait_entry_auto_claimed = _try_auto_claim_self_assigned_tasks(tasks=_entry_tasks_raw)
|
|
4306
4367
|
except Exception:
|
|
4307
4368
|
_wait_entry_auto_claimed = []
|
|
4308
4369
|
|
|
4309
|
-
pending_tasks =
|
|
4370
|
+
pending_tasks = (_pending_filter(_entry_tasks_raw) if _entry_tasks_raw is not None
|
|
4371
|
+
else _get_pending_tasks_summary())
|
|
4310
4372
|
if pending_tasks and os.environ.get("MESHCODE_WAIT_BLOCKS_ON_TASKS", "").lower() in ("1", "true", "yes"):
|
|
4311
4373
|
if not _is_leader_agent():
|
|
4312
4374
|
return {
|
|
@@ -4341,11 +4403,14 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
|
|
|
4341
4403
|
# ("te escribo y no sale palomita azul en nadie"). If there are unread messages,
|
|
4342
4404
|
# skip task_pull and fall through to PRODUCT RULE 2 so they get delivered + marked
|
|
4343
4405
|
# read first. Task-pull still fires normally when the inbox is empty.
|
|
4344
|
-
|
|
4406
|
+
# SDK-EFF (task ab1f9f5a): count once, share with PRODUCT RULE 2 below —
|
|
4407
|
+
# the entry path was issuing the identical count_pending twice in a row.
|
|
4408
|
+
_entry_db_pending = 0
|
|
4345
4409
|
try:
|
|
4346
|
-
|
|
4410
|
+
_entry_db_pending = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=_get_api_key()) or 0
|
|
4347
4411
|
except Exception:
|
|
4348
|
-
|
|
4412
|
+
_entry_db_pending = 0
|
|
4413
|
+
_has_unread = bool(_entry_db_pending)
|
|
4349
4414
|
if _tasks_to_start and not _is_leader_agent() and not _has_unread:
|
|
4350
4415
|
# Auto-start the highest priority OPEN task so the agent sees it as
|
|
4351
4416
|
# in_progress and works it immediately. Samuel directive: "agents
|
|
@@ -4392,7 +4457,10 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
|
|
|
4392
4457
|
|
|
4393
4458
|
# PRODUCT RULE 2: If agent has unread messages in DB, refuse to wait.
|
|
4394
4459
|
try:
|
|
4395
|
-
|
|
4460
|
+
# SDK-EFF (task ab1f9f5a): reuse the entry count from above. read_inbox
|
|
4461
|
+
# is the authoritative drain; a message landing in the microseconds
|
|
4462
|
+
# between the two counts was equally invisible to the old double-count.
|
|
4463
|
+
db_pending = _entry_db_pending
|
|
4396
4464
|
if db_pending and db_pending > 0:
|
|
4397
4465
|
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=True, api_key=_get_api_key())
|
|
4398
4466
|
msgs = [
|
|
@@ -4507,6 +4575,32 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
|
|
|
4507
4575
|
result["stopped"] = True
|
|
4508
4576
|
break
|
|
4509
4577
|
|
|
4578
|
+
# SDK-EFF (task ab1f9f5a): realtime-missed messages. This drain
|
|
4579
|
+
# used to live in the inner-loop tail as its own count_pending;
|
|
4580
|
+
# mc_wait_poll already reports the unread count, so only the
|
|
4581
|
+
# actual read_inbox costs an RPC — and only when there IS mail.
|
|
4582
|
+
# Ordering note: recycle/stop now win over a missed message
|
|
4583
|
+
# (mc_wait_poll consumed the recycle flag, so it must be
|
|
4584
|
+
# honored); the message stays unread and redelivers after the
|
|
4585
|
+
# respawn / power-on. Legacy path (pending_count=None) keeps
|
|
4586
|
+
# the explicit count so old-RPC fleets lose no coverage.
|
|
4587
|
+
_db_pending = _wp.get("pending_count")
|
|
4588
|
+
if _db_pending is None:
|
|
4589
|
+
try:
|
|
4590
|
+
_db_pending = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=_get_api_key()) or 0
|
|
4591
|
+
except Exception:
|
|
4592
|
+
_db_pending = 0
|
|
4593
|
+
if _db_pending > 0:
|
|
4594
|
+
_drained = _drain_unread_response(include_acks)
|
|
4595
|
+
if _drained:
|
|
4596
|
+
result = _drained
|
|
4597
|
+
_set_state("online", "")
|
|
4598
|
+
_CONSECUTIVE_IDLE_SECONDS = 0
|
|
4599
|
+
_msg_count = len(result.get("messages", []))
|
|
4600
|
+
if _msg_count:
|
|
4601
|
+
_log_activity_bg("message_delivered", f"{AGENT_NAME} received {_msg_count} message(s)")
|
|
4602
|
+
break
|
|
4603
|
+
|
|
4510
4604
|
# New tasks that appeared while we waited (from the same poll)
|
|
4511
4605
|
pending_tasks = _wp["tasks"]
|
|
4512
4606
|
if pending_tasks:
|
|
@@ -4562,6 +4656,17 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
|
|
|
4562
4656
|
continue
|
|
4563
4657
|
# ── END INTERNAL LOOP ──────────────────────────────────────
|
|
4564
4658
|
|
|
4659
|
+
# CAL-3 calendar context — SDK-EFF (task ab1f9f5a) moved it here from
|
|
4660
|
+
# the inner-loop tail: once per RETURNED wait instead of once per idle
|
|
4661
|
+
# cycle (it was a guaranteed RPC every ~20s that the outer loop usually
|
|
4662
|
+
# threw away on `continue`).
|
|
4663
|
+
if isinstance(result, dict) and result.get("timed_out"):
|
|
4664
|
+
try:
|
|
4665
|
+
_cal_ctx = _calendar_context_for_self()
|
|
4666
|
+
if _cal_ctx is not None:
|
|
4667
|
+
result["calendar_context"] = _cal_ctx
|
|
4668
|
+
except Exception:
|
|
4669
|
+
pass
|
|
4565
4670
|
# _LAST_SEEN_TS persist removed (2026-05-28) — UUID dedup cache is sufficient
|
|
4566
4671
|
# Surface memory_hints from mig 220 if a read_inbox happened during
|
|
4567
4672
|
# this wait cycle (PRODUCT RULE 2 path) or inside _meshcode_wait_inner.
|
|
@@ -4829,43 +4934,15 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
4829
4934
|
except Exception as e:
|
|
4830
4935
|
log.debug(f"DB poll fallback error: {e}")
|
|
4831
4936
|
|
|
4832
|
-
#
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
{"from": m["from_agent"], "type": m.get("type", "msg"),
|
|
4842
|
-
"ts": m.get("created_at"), "payload": m.get("payload", {}),
|
|
4843
|
-
"id": m.get("id"), "parent_id": m.get("parent_msg_id")}
|
|
4844
|
-
for m in raw
|
|
4845
|
-
]
|
|
4846
|
-
deduped = _filter_and_mark(msgs)
|
|
4847
|
-
if deduped:
|
|
4848
|
-
split = _split_messages(deduped)
|
|
4849
|
-
if not include_acks:
|
|
4850
|
-
split["acks"] = []
|
|
4851
|
-
if split["messages"] or split["done_signals"]:
|
|
4852
|
-
return {"got_message": True, **split}
|
|
4853
|
-
except Exception as e:
|
|
4854
|
-
log.debug(f"final DB fallback error: {e}")
|
|
4855
|
-
|
|
4856
|
-
# Check if there's any pending work before returning timeout
|
|
4857
|
-
pending_tasks = _get_pending_tasks_summary()
|
|
4858
|
-
out: Dict[str, Any] = {"timed_out": True}
|
|
4859
|
-
if pending_tasks:
|
|
4860
|
-
out["pending_tasks"] = pending_tasks
|
|
4861
|
-
else:
|
|
4862
|
-
out["no_work"] = True
|
|
4863
|
-
# CAL-3 calendar context — surface due/overdue/next_due on timeouts so the
|
|
4864
|
-
# agent knows whether to nudge work even when no new msg arrived.
|
|
4865
|
-
_ctx = _calendar_context_for_self()
|
|
4866
|
-
if _ctx is not None:
|
|
4867
|
-
out["calendar_context"] = _ctx
|
|
4868
|
-
return out
|
|
4937
|
+
# SDK-EFF (task ab1f9f5a): the tail here used to fire 3 more RPCs per idle
|
|
4938
|
+
# cycle — count_pending (realtime-missed-message fallback), task_list (via
|
|
4939
|
+
# _get_pending_tasks_summary) and mc_tasks_due_soon (calendar) — right
|
|
4940
|
+
# before the OUTER loop issued mc_wait_poll, which already answers the
|
|
4941
|
+
# first two. The missed-message drain now lives in the outer loop (driven
|
|
4942
|
+
# by mc_wait_poll's pending_count), tasks come from the same poll, and the
|
|
4943
|
+
# calendar snapshot is attached once per RETURNED wait instead of once per
|
|
4944
|
+
# cycle. Net: 4 RPCs/idle-cycle -> 1.
|
|
4945
|
+
return {"timed_out": True}
|
|
4869
4946
|
|
|
4870
4947
|
|
|
4871
4948
|
@mcp.tool()
|
|
@@ -178,7 +178,10 @@ class TestGhostSweep:
|
|
|
178
178
|
assert re.search(r"heartbeat_age_s", self.SRC) and "90" in self.SRC
|
|
179
179
|
|
|
180
180
|
def test_uses_roster_not_heartbeat_gated_rpc(self):
|
|
181
|
-
|
|
181
|
+
# SDK-EFF (task ab1f9f5a): the sweep now reads the roster through
|
|
182
|
+
# _host_cfg, the per-sweep TTL cache over mc_host_config_get — same
|
|
183
|
+
# no-heartbeat-gate source, one RPC per sweep instead of one per helper.
|
|
184
|
+
assert "mc_host_config_get" in self.SRC or "_host_cfg(" in self.SRC
|
|
182
185
|
assert "mc_agents_to_stop" not in self.SRC
|
|
183
186
|
|
|
184
187
|
def test_wired_into_poll_loop(self):
|
|
@@ -63,7 +63,11 @@ class TestWaitContradictionFix(unittest.TestCase):
|
|
|
63
63
|
def test_pending_tasks_hint_path_unchanged(self):
|
|
64
64
|
# Existing hint surface in the timeout path must remain — that's
|
|
65
65
|
# how the agent now sees open tasks without being blocked.
|
|
66
|
-
|
|
66
|
+
# SDK-EFF (task ab1f9f5a): the hint moved from the inner-loop tail
|
|
67
|
+
# (out[...]) to the outer loop's mc_wait_poll branch (result[...]) —
|
|
68
|
+
# same surface, fed by one consolidated RPC instead of a task_list
|
|
69
|
+
# per idle cycle.
|
|
70
|
+
idx = self.source.find('result["pending_tasks"] = pending_tasks')
|
|
67
71
|
self.assertGreater(idx, 0,
|
|
68
72
|
"fallback timeout path must still surface "
|
|
69
73
|
"pending_tasks as a hint so the agent sees "
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|