meshcode 2.11.109__tar.gz → 2.11.110__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.109 → meshcode-2.11.110}/PKG-INFO +1 -1
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/__init__.py +1 -1
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/_stop_hook_template.py +8 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/comms_v4.py +42 -6
- meshcode-2.11.110/meshcode/hooks/__init__.py +7 -0
- meshcode-2.11.110/meshcode/hooks/repo_path_lock.py +156 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/hostd.py +92 -12
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/meshcode_mcp/server.py +43 -23
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/protocol_handler.py +36 -6
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/run_agent.py +115 -5
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/setup_clients.py +26 -1
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode.egg-info/SOURCES.txt +2 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/pyproject.toml +2 -1
- {meshcode-2.11.109 → meshcode-2.11.110}/README.md +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/__main__.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/_session_handoff_template 2.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/_session_handoff_template 3.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/claude_update 2.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/claude_update 3.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/cli.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/compat.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/daemon.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/doctor.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/hostd 2.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/invites.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/launcher.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/preferences.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/secrets.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/self_update.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/up 2.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/up.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode/upload.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/setup.cfg +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_autonomous_prompt_inject 2.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_autonomous_prompt_inject 3.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_core.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_doctor.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.109 → meshcode-2.11.110}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -48,6 +48,7 @@ reported commander trapped in this exact path. Two new release paths:
|
|
|
48
48
|
connected") → human-confirmed unreachable.
|
|
49
49
|
"""
|
|
50
50
|
import json
|
|
51
|
+
import os
|
|
51
52
|
import sys
|
|
52
53
|
from pathlib import Path
|
|
53
54
|
|
|
@@ -469,6 +470,13 @@ def _user_explicitly_reports_mcp_failure(transcript_path, lookback_records=_UNRE
|
|
|
469
470
|
|
|
470
471
|
|
|
471
472
|
def main():
|
|
473
|
+
# MESHCODE_AGENT_SESSION gate (task 24e3dd44): this hook is safe to install at USER
|
|
474
|
+
# scope (~/.claude/settings.json) because it NO-OPS for any Claude session that is not
|
|
475
|
+
# a meshcode agent launch. run_agent exports MESHCODE_AGENT_SESSION=1 for every agent;
|
|
476
|
+
# a human's personal `claude` session has it unset -> the loop hook never traps them.
|
|
477
|
+
# (Workspace-scope installs also inherit the env, so agent loop-survival is unchanged.)
|
|
478
|
+
if not os.environ.get("MESHCODE_AGENT_SESSION"):
|
|
479
|
+
sys.exit(0)
|
|
472
480
|
raw = sys.stdin.read()
|
|
473
481
|
try:
|
|
474
482
|
payload = json.loads(raw) if raw else {}
|
|
@@ -1797,8 +1797,15 @@ def _heartbeat_loop_pidfile(project, name):
|
|
|
1797
1797
|
return Path.home() / ".meshcode" / f"heartbeat_{project}_{name}.pid"
|
|
1798
1798
|
|
|
1799
1799
|
|
|
1800
|
-
def _start_heartbeat_daemon(project, name):
|
|
1801
|
-
"""Fork a tiny background process that POSTs mc_heartbeat every 30s.
|
|
1800
|
+
def _start_heartbeat_daemon(project, name, agent_pid=None):
|
|
1801
|
+
"""Fork a tiny background process that POSTs mc_heartbeat every 30s.
|
|
1802
|
+
|
|
1803
|
+
Fix A (task 24e3dd44, mesh-dev): when agent_pid is given (the long-lived claude
|
|
1804
|
+
PID = connect_terminal ppid), the fork verifies that process is ALIVE before each
|
|
1805
|
+
POST and self-exits when gone — so a dead agent stops showing 'online'. Windows-
|
|
1806
|
+
SAFE: never os.kill(pid,0) on win32 (that TERMINATES); uses OpenProcess +
|
|
1807
|
+
GetExitCodeProcess (259 == STILL_ACTIVE).
|
|
1808
|
+
"""
|
|
1802
1809
|
pid_file = _heartbeat_loop_pidfile(project, name)
|
|
1803
1810
|
pid_file.parent.mkdir(parents=True, exist_ok=True)
|
|
1804
1811
|
# Stop any existing heartbeat
|
|
@@ -1818,7 +1825,24 @@ def _start_heartbeat_daemon(project, name):
|
|
|
1818
1825
|
f"project={project!r}; name={name!r}\n"
|
|
1819
1826
|
f"url={SUPABASE_URL!r}; key={SUPABASE_KEY!r}\n"
|
|
1820
1827
|
f"api_key={_ak!r}\n"
|
|
1828
|
+
f"agent_pid={agent_pid!r}\n"
|
|
1821
1829
|
"import urllib.parse\n"
|
|
1830
|
+
"def agent_alive():\n"
|
|
1831
|
+
" if not agent_pid: return True\n"
|
|
1832
|
+
" if sys.platform=='win32':\n"
|
|
1833
|
+
" try:\n"
|
|
1834
|
+
" import ctypes\n"
|
|
1835
|
+
" h=ctypes.windll.kernel32.OpenProcess(0x1000, False, int(agent_pid))\n"
|
|
1836
|
+
" if not h: return False\n"
|
|
1837
|
+
" code=ctypes.c_ulong()\n"
|
|
1838
|
+
" ok=ctypes.windll.kernel32.GetExitCodeProcess(h, ctypes.byref(code))\n"
|
|
1839
|
+
" ctypes.windll.kernel32.CloseHandle(h)\n"
|
|
1840
|
+
" return bool(ok) and code.value==259\n"
|
|
1841
|
+
" except Exception: return True\n"
|
|
1842
|
+
" try:\n"
|
|
1843
|
+
" os.kill(int(agent_pid),0); return True\n"
|
|
1844
|
+
" except ProcessLookupError: return False\n"
|
|
1845
|
+
" except Exception: return True\n"
|
|
1822
1846
|
"def post(path, body):\n"
|
|
1823
1847
|
" req=urllib.request.Request(url+path, data=json.dumps(body).encode(),\n"
|
|
1824
1848
|
" headers={'apikey':key,'Authorization':'Bearer '+key,'Content-Type':'application/json','Accept-Profile':'meshcode','Content-Profile':'meshcode'}, method='POST')\n"
|
|
@@ -1856,6 +1880,8 @@ def _start_heartbeat_daemon(project, name):
|
|
|
1856
1880
|
" except Exception: return True\n"
|
|
1857
1881
|
"pid=get_pid_for_project()\n"
|
|
1858
1882
|
"while True:\n"
|
|
1883
|
+
" if not agent_alive():\n"
|
|
1884
|
+
" sys.exit(0)\n"
|
|
1859
1885
|
" if pid:\n"
|
|
1860
1886
|
" if not check_still_leased(pid):\n"
|
|
1861
1887
|
" sys.exit(0)\n"
|
|
@@ -1961,7 +1987,9 @@ def connect_terminal(project, name, role=""):
|
|
|
1961
1987
|
except Exception:
|
|
1962
1988
|
pass # Non-critical — don't block agent boot
|
|
1963
1989
|
|
|
1964
|
-
|
|
1990
|
+
# Fix A: hand the heartbeat fork the owning claude/agent PID so it self-exits when
|
|
1991
|
+
# the agent dies (instead of keeping a dead agent 'online'). ppid is that long-lived process.
|
|
1992
|
+
hb_pid = _start_heartbeat_daemon(project, name, agent_pid=ppid)
|
|
1965
1993
|
|
|
1966
1994
|
pending = (rpc_result or {}).get("pending_messages", 0)
|
|
1967
1995
|
print(f"[meshcode] Connected: {name} -> {project}")
|
|
@@ -2440,7 +2468,7 @@ TEAM:
|
|
|
2440
2468
|
SETUP (advanced):
|
|
2441
2469
|
setup <proj> <name> [role] Create workspace (auto by 'go')
|
|
2442
2470
|
add-agent <proj> <name> [role] Alias for `setup` above
|
|
2443
|
-
run <agent> [--project <name>] [--autonomous] Launch agent (auto by 'go')
|
|
2471
|
+
run <agent> [--project <name>] [--autonomous] [--repo <path>] Launch agent (auto by 'go')
|
|
2444
2472
|
register <proj> <name> [role] Register agent manually
|
|
2445
2473
|
setup <client> <proj> <name> [role] Legacy: global MCP config
|
|
2446
2474
|
|
|
@@ -2663,14 +2691,21 @@ by `meshcode go`.
|
|
|
2663
2691
|
EXAMPLES:
|
|
2664
2692
|
meshcode setup my-app backend "Backend Engineer"
|
|
2665
2693
|
""",
|
|
2666
|
-
"run": """meshcode run <agent> [--project <name>] [--editor claude|cursor|code]
|
|
2694
|
+
"run": """meshcode run <agent> [--project <name>] [--editor claude|cursor|code] [--repo <path>] [--dry-run]
|
|
2667
2695
|
|
|
2668
2696
|
Launch an agent in your preferred editor. Detects Claude Code, Cursor,
|
|
2669
2697
|
VS Code, Windsurf, or Codex. Use <project>/<agent> to disambiguate.
|
|
2670
2698
|
|
|
2699
|
+
--repo <path> Launch repo-scoped: cwd=<path> (the agent works inside that
|
|
2700
|
+
repo + reads its CLAUDE.md) with a hard repo-path lock that
|
|
2701
|
+
denies file ops outside it. Omit for a normal workspace launch.
|
|
2702
|
+
--dry-run Exercise the full bootstrap (no editor launch); exit 0 on a
|
|
2703
|
+
clean path.
|
|
2704
|
+
|
|
2671
2705
|
EXAMPLES:
|
|
2672
2706
|
meshcode run backend
|
|
2673
2707
|
meshcode run my-app/backend --editor cursor
|
|
2708
|
+
meshcode run backend --repo ~/code/my-repo
|
|
2674
2709
|
""",
|
|
2675
2710
|
"go": """meshcode go <agent> [--project <name>] [--dry-run]
|
|
2676
2711
|
|
|
@@ -3382,6 +3417,7 @@ if __name__ == "__main__":
|
|
|
3382
3417
|
proj_override = flags.get("project")
|
|
3383
3418
|
editor_override = flags.get("editor")
|
|
3384
3419
|
perm_override = flags.get("permission") # bypass | safe | ask
|
|
3420
|
+
repo_override = flags.get("repo") # repo-scoped launch (cwd=repo + repo-lock hooks); task 24e3dd44
|
|
3385
3421
|
if "--bypass" in sys.argv:
|
|
3386
3422
|
perm_override = "bypass"
|
|
3387
3423
|
elif "--safe" in sys.argv:
|
|
@@ -3413,7 +3449,7 @@ if __name__ == "__main__":
|
|
|
3413
3449
|
autonomous = bool(flags.get("autonomous")) or autonomous_env
|
|
3414
3450
|
import importlib
|
|
3415
3451
|
_run = importlib.import_module("meshcode.run_agent").run
|
|
3416
|
-
sys.exit(_run(agent, project=proj_override, editor_override=editor_override, permission_override=perm_override, dry_run=dry_run, autonomous=autonomous))
|
|
3452
|
+
sys.exit(_run(agent, project=proj_override, editor_override=editor_override, permission_override=perm_override, dry_run=dry_run, autonomous=autonomous, repo_path=repo_override))
|
|
3417
3453
|
|
|
3418
3454
|
elif cmd == "scan":
|
|
3419
3455
|
# meshcode scan — detect identicon from stdin/clipboard and launch agent
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""MeshCode packaged Claude Code hook scripts (shipped as package data).
|
|
2
|
+
|
|
3
|
+
Currently: repo_path_lock.py — the canonical PreToolUse repo-path lock that
|
|
4
|
+
hard-locks a repo-scoped agent session to its registered repo even under
|
|
5
|
+
--dangerously-skip-permissions. Installed to ~/.claude/hooks by
|
|
6
|
+
run_agent.ensure_repo_lock_hook_installed() (fallback user-scope path).
|
|
7
|
+
"""
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""MeshCode canonical PreToolUse repo-path lock hook.
|
|
3
|
+
|
|
4
|
+
Hard-locks an agent's filesystem tool calls to its registered repo path —
|
|
5
|
+
EVEN under `--dangerously-skip-permissions` (bypassPermissions). This works
|
|
6
|
+
because Claude Code fires PreToolUse hooks BEFORE any permission-mode check,
|
|
7
|
+
and a hook returning `permissionDecision: "deny"` blocks the tool in every
|
|
8
|
+
mode. settings.json `permissions.deny` does NOT survive bypass — only this hook does.
|
|
9
|
+
|
|
10
|
+
Env contract (exported by the launcher):
|
|
11
|
+
MESHCODE_REPO_LOCK absolute repo root the session is locked to.
|
|
12
|
+
UNSET/empty -> hook is a NO-OP (fail-open).
|
|
13
|
+
MESHCODE_REPO_LOCK_ALLOW optional os.pathsep-separated extra allowed roots.
|
|
14
|
+
MESHCODE_REPO_LOCK_BASH "1" to also scan Bash commands (heuristic).
|
|
15
|
+
Always-allowed: locked root + ALLOW entries + ~/.claude/projects + OS temp.
|
|
16
|
+
"""
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
|
|
23
|
+
_CYGDRIVE_RE = re.compile(r"^/cygdrive/([a-zA-Z])(?=/|$)")
|
|
24
|
+
_MSYS_DRIVE_RE = re.compile(r"^/([a-zA-Z])(?=/|$)")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _demsys(path: str) -> str:
|
|
28
|
+
path = _CYGDRIVE_RE.sub(lambda m: m.group(1) + ":", path)
|
|
29
|
+
return _MSYS_DRIVE_RE.sub(lambda m: m.group(1) + ":", path)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _norm(p: str) -> str:
|
|
33
|
+
return os.path.normcase(os.path.realpath(p))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _resolve(path: str, cwd: str) -> str:
|
|
37
|
+
path = os.path.expanduser(_demsys(path))
|
|
38
|
+
cwd = os.path.expanduser(_demsys(cwd))
|
|
39
|
+
if not os.path.isabs(path):
|
|
40
|
+
path = os.path.join(cwd, path)
|
|
41
|
+
return _norm(path)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _within(target: str, root: str) -> bool:
|
|
45
|
+
if target == root:
|
|
46
|
+
return True
|
|
47
|
+
return target.startswith(root + os.sep)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _allowed_roots(cwd: str):
|
|
51
|
+
roots = []
|
|
52
|
+
lock = os.environ.get("MESHCODE_REPO_LOCK", "").strip()
|
|
53
|
+
if lock:
|
|
54
|
+
roots.append(_norm(lock))
|
|
55
|
+
extra = os.environ.get("MESHCODE_REPO_LOCK_ALLOW", "").strip()
|
|
56
|
+
if extra:
|
|
57
|
+
for r in extra.split(os.pathsep):
|
|
58
|
+
r = r.strip()
|
|
59
|
+
if r:
|
|
60
|
+
roots.append(_norm(r))
|
|
61
|
+
home = os.path.expanduser("~")
|
|
62
|
+
roots.append(_norm(os.path.join(home, ".claude", "projects")))
|
|
63
|
+
roots.append(_norm(tempfile.gettempdir()))
|
|
64
|
+
return roots
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_PATH_KEYS = {
|
|
68
|
+
"Read": ["file_path"],
|
|
69
|
+
"Edit": ["file_path"],
|
|
70
|
+
"Write": ["file_path"],
|
|
71
|
+
"MultiEdit": ["file_path"],
|
|
72
|
+
"NotebookEdit": ["notebook_path"],
|
|
73
|
+
"Glob": ["path"],
|
|
74
|
+
"Grep": ["path"],
|
|
75
|
+
"LS": ["path"],
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _deny(reason: str) -> None:
|
|
80
|
+
print(json.dumps({
|
|
81
|
+
"hookSpecificOutput": {
|
|
82
|
+
"hookEventName": "PreToolUse",
|
|
83
|
+
"permissionDecision": "deny",
|
|
84
|
+
"permissionDecisionReason": reason,
|
|
85
|
+
}
|
|
86
|
+
}))
|
|
87
|
+
sys.exit(0)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _candidate_bash_paths(command: str):
|
|
91
|
+
toks = re.findall(r'"[^"]*"|\'[^\']*\'|\S+', command)
|
|
92
|
+
for t in toks:
|
|
93
|
+
t = t.strip('"\'')
|
|
94
|
+
if not t:
|
|
95
|
+
continue
|
|
96
|
+
looks_pathy = (
|
|
97
|
+
bool(_MSYS_DRIVE_RE.match(t))
|
|
98
|
+
or bool(_CYGDRIVE_RE.match(t))
|
|
99
|
+
or t.startswith("~/")
|
|
100
|
+
or t.startswith("..")
|
|
101
|
+
or "/.." in t
|
|
102
|
+
or "\\.." in t
|
|
103
|
+
or (len(t) >= 3 and t[1] == ":" and t[2] in "\\/")
|
|
104
|
+
)
|
|
105
|
+
if looks_pathy:
|
|
106
|
+
yield t
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main() -> int:
|
|
110
|
+
try:
|
|
111
|
+
raw = sys.stdin.read()
|
|
112
|
+
data = json.loads(raw) if raw.strip() else {}
|
|
113
|
+
except Exception:
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
if not os.environ.get("MESHCODE_REPO_LOCK", "").strip():
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
tool = data.get("tool_name") or ""
|
|
120
|
+
tool_input = data.get("tool_input") or {}
|
|
121
|
+
cwd = data.get("cwd") or os.getcwd()
|
|
122
|
+
roots = _allowed_roots(cwd)
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
if tool in _PATH_KEYS:
|
|
126
|
+
for key in _PATH_KEYS[tool]:
|
|
127
|
+
raw_path = tool_input.get(key)
|
|
128
|
+
if not raw_path:
|
|
129
|
+
continue
|
|
130
|
+
target = _resolve(str(raw_path), cwd)
|
|
131
|
+
if not any(_within(target, r) for r in roots):
|
|
132
|
+
_deny(
|
|
133
|
+
f"repo-path lock: {tool} target '{raw_path}' is outside "
|
|
134
|
+
f"the locked repo. Allowed root: "
|
|
135
|
+
f"{os.environ.get('MESHCODE_REPO_LOCK')}"
|
|
136
|
+
)
|
|
137
|
+
elif tool == "Bash" and os.environ.get("MESHCODE_REPO_LOCK_BASH", "").strip() == "1":
|
|
138
|
+
command = str(tool_input.get("command") or "")
|
|
139
|
+
for cand in _candidate_bash_paths(command):
|
|
140
|
+
target = _resolve(cand, cwd)
|
|
141
|
+
if not any(_within(target, r) for r in roots):
|
|
142
|
+
_deny(
|
|
143
|
+
f"repo-path lock: Bash command references '{cand}' outside "
|
|
144
|
+
f"the locked repo ({os.environ.get('MESHCODE_REPO_LOCK')}). "
|
|
145
|
+
f"Set MESHCODE_REPO_LOCK_BASH=0 to disable bash scanning."
|
|
146
|
+
)
|
|
147
|
+
except SystemExit:
|
|
148
|
+
raise
|
|
149
|
+
except Exception:
|
|
150
|
+
return 0
|
|
151
|
+
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
sys.exit(main())
|
|
@@ -34,6 +34,7 @@ from __future__ import annotations
|
|
|
34
34
|
|
|
35
35
|
import json
|
|
36
36
|
import os
|
|
37
|
+
import re
|
|
37
38
|
import shlex
|
|
38
39
|
import shutil
|
|
39
40
|
import signal as _signal
|
|
@@ -327,7 +328,26 @@ def _meshcode_bin() -> str:
|
|
|
327
328
|
return "meshcode"
|
|
328
329
|
|
|
329
330
|
|
|
330
|
-
def
|
|
331
|
+
def _validate_repo_path(rp) -> Optional[str]:
|
|
332
|
+
"""Validate a per-agent repo override (task 24e3dd44 #4, mc_agents.repo_path).
|
|
333
|
+
Returns an absolute realpath dir, or None. REJECTS (None + logs): NULL/empty, a value with a
|
|
334
|
+
double-quote or control char (injection hardening), or a path that is not an existing dir."""
|
|
335
|
+
if not rp or not isinstance(rp, str):
|
|
336
|
+
return None
|
|
337
|
+
if '"' in rp or any(ord(ch) < 0x20 for ch in rp):
|
|
338
|
+
_log(f"WARN: repo_path rejected (quote/control char): {rp!r}")
|
|
339
|
+
return None
|
|
340
|
+
try:
|
|
341
|
+
p = os.path.realpath(os.path.expanduser(rp))
|
|
342
|
+
except Exception:
|
|
343
|
+
return None
|
|
344
|
+
if not os.path.isdir(p):
|
|
345
|
+
_log(f"WARN: repo_path rejected (not a directory): {rp!r} -> {p}")
|
|
346
|
+
return None
|
|
347
|
+
return p
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _spawn_agent(project: str, agent: str, headless: bool = False, repo_path=None) -> bool:
|
|
331
351
|
"""Relaunch `meshcode run <project>/<agent>`.
|
|
332
352
|
|
|
333
353
|
headless=False (default): VISIBLE terminal window. Samuel must SEE it — we
|
|
@@ -338,9 +358,14 @@ def _spawn_agent(project: str, agent: str, headless: bool = False) -> bool:
|
|
|
338
358
|
|
|
339
359
|
headless=True (Fleet Control mig404 per-agent flag): background process, NO
|
|
340
360
|
window — for fleet agents that don't need a terminal (like the qa launch).
|
|
361
|
+
|
|
362
|
+
repo_path (task 24e3dd44 #4): optional per-agent repo. When set + valid, `--repo <path>` is
|
|
363
|
+
appended so `meshcode run` opens there. NULL/invalid = unchanged. Inert until mc_agents.repo_path
|
|
364
|
+
+ the respawn RPC carry it.
|
|
341
365
|
"""
|
|
342
366
|
target = f"{project}/{agent}"
|
|
343
367
|
bin_ = _meshcode_bin()
|
|
368
|
+
repo = _validate_repo_path(repo_path)
|
|
344
369
|
if headless:
|
|
345
370
|
# background, NO terminal — UNIVERSAL macOS/Linux/Windows (task c1a6c6a8, mesh-dev specs).
|
|
346
371
|
# Clean env (a stale CLAUDECODE aborts `meshcode run`); keep crash logs in a per-agent logfile
|
|
@@ -403,7 +428,8 @@ def _spawn_agent(project: str, agent: str, headless: bool = False) -> bool:
|
|
|
403
428
|
try:
|
|
404
429
|
# `python -m meshcode` (NOT the meshcode.exe shim) so the .exe isn't held open by the
|
|
405
430
|
# agent -> a background `pip install -U` can replace it on Windows (task 14782bb4 #4).
|
|
406
|
-
|
|
431
|
+
argv = [sys.executable, "-m", "meshcode", "run", target] + (["--repo", repo] if repo else [])
|
|
432
|
+
proc = subprocess.Popen(argv, **kwargs)
|
|
407
433
|
# LAUNCH-NO-WINDOW (task 35bee961): record the headless PID so the Stop sweep
|
|
408
434
|
# (_do_stops) can hard-kill it — a headless agent has NO window for Samuel to
|
|
409
435
|
# close, so desired_state='stopped' must be enforced by killing the process.
|
|
@@ -435,14 +461,17 @@ def _spawn_agent(project: str, agent: str, headless: bool = False) -> bool:
|
|
|
435
461
|
except Exception:
|
|
436
462
|
_bindir = ""
|
|
437
463
|
if sys.platform == "win32":
|
|
464
|
+
# repo already validated (no '"'/control chars, real dir) -> safe to double-quote.
|
|
465
|
+
_repo_win = f' --repo "{repo}"' if repo else ''
|
|
438
466
|
cmd = (f'set "CLAUDECODE=" & set "CLAUDE_CODE_SESSION=" & '
|
|
439
467
|
f'set "MESHCODE_NO_UPDATE=" & set "MESHCODE_NO_AUTO_UPDATE=" & '
|
|
440
468
|
+ (f'set "PATH={_bindir};%PATH%" & ' if _bindir else '')
|
|
441
|
-
+ f'"{sys.executable}" -m meshcode run "{target}"')
|
|
469
|
+
+ f'"{sys.executable}" -m meshcode run "{target}"{_repo_win}')
|
|
442
470
|
else:
|
|
471
|
+
_repo_posix = f" --repo {shlex.quote(repo)}" if repo else ''
|
|
443
472
|
cmd = (f"unset CLAUDECODE CLAUDE_CODE_SESSION MESHCODE_NO_UPDATE MESHCODE_NO_AUTO_UPDATE; "
|
|
444
473
|
+ (f"export PATH={shlex.quote(_bindir)}:$PATH; " if _bindir else "")
|
|
445
|
-
+ f"exec {shlex.quote(sys.executable)} -m meshcode run {shlex.quote(target)}")
|
|
474
|
+
+ f"exec {shlex.quote(sys.executable)} -m meshcode run {shlex.quote(target)}{_repo_posix}")
|
|
446
475
|
try:
|
|
447
476
|
from meshcode import protocol_handler as _ph
|
|
448
477
|
ok, info = _ph._spawn_terminal(cmd)
|
|
@@ -567,7 +596,7 @@ def _do_respawns(api_key: str, host_id: str) -> int:
|
|
|
567
596
|
_old_vis_pids = _discover_agent_pids(_target) if (_is_recycle and _visible) else []
|
|
568
597
|
_log(f"{'RECYCLE-RESPAWN' if _is_recycle else 'RESPAWN'} {proj}/{agent} "
|
|
569
598
|
f"({'VISIBLE ' if _visible else ''}stale {c.get('heartbeat_age_s')}s, count={c.get('respawn_count')})")
|
|
570
|
-
if _spawn_agent(proj, agent, headless=_hl):
|
|
599
|
+
if _spawn_agent(proj, agent, headless=_hl, repo_path=c.get("repo_path")):
|
|
571
600
|
_record_spawn(_target) # count the terminal we just opened, against the breaker
|
|
572
601
|
if _is_recycle:
|
|
573
602
|
if _visible and _old_vis_pids:
|
|
@@ -839,8 +868,14 @@ def _pid_cmdline(pid: int) -> str:
|
|
|
839
868
|
def _discover_agent_pids(target: str) -> list:
|
|
840
869
|
"""Fallback PID discovery by command line, for agents spawned before this hostd
|
|
841
870
|
(no recorded PID) or after a state-file loss. Matches `meshcode run <target>`.
|
|
842
|
-
Best-effort; returns [] on any failure.
|
|
871
|
+
Best-effort; returns [] on any failure.
|
|
872
|
+
|
|
873
|
+
SAFETY (prefix-match fix, task 24e3dd44): target must match as a WHOLE token — `run <target>`
|
|
874
|
+
followed by whitespace/end/quote — NEVER a bare substring, else 'back' matches 'back-2' and a
|
|
875
|
+
downstream reaper/force-kill could KILL a healthy sibling. re.escape keeps target literal. On
|
|
876
|
+
POSIX pgrep -f is substring, so re-verify each candidate's full cmdline with the exact regex."""
|
|
843
877
|
pids = []
|
|
878
|
+
tok = re.compile(r"run\s+" + re.escape(target) + r"(?=\s|$|[\"'])")
|
|
844
879
|
try:
|
|
845
880
|
if sys.platform == "win32":
|
|
846
881
|
out = subprocess.run(
|
|
@@ -850,7 +885,7 @@ def _discover_agent_pids(target: str) -> list:
|
|
|
850
885
|
for line in out.splitlines():
|
|
851
886
|
line = line.strip()
|
|
852
887
|
if line.startswith("CommandLine="):
|
|
853
|
-
block_pid = ("meshcode" in line and
|
|
888
|
+
block_pid = ("meshcode" in line and bool(tok.search(line)))
|
|
854
889
|
elif line.startswith("ProcessId=") and block_pid:
|
|
855
890
|
try:
|
|
856
891
|
pids.append(int(line.split("=", 1)[1]))
|
|
@@ -863,14 +898,52 @@ def _discover_agent_pids(target: str) -> list:
|
|
|
863
898
|
capture_output=True, text=True, timeout=8).stdout
|
|
864
899
|
for ln in out.split():
|
|
865
900
|
try:
|
|
866
|
-
|
|
901
|
+
p = int(ln)
|
|
867
902
|
except Exception:
|
|
868
|
-
|
|
903
|
+
continue
|
|
904
|
+
# pgrep matched a substring; keep ONLY if the full cmdline has the exact token.
|
|
905
|
+
if tok.search(_pid_cmdline(p)):
|
|
906
|
+
pids.append(p)
|
|
869
907
|
except Exception:
|
|
870
908
|
pass
|
|
871
909
|
return pids
|
|
872
910
|
|
|
873
911
|
|
|
912
|
+
def _kill_heartbeat_fork(target: str) -> None:
|
|
913
|
+
"""Kill the agent's heartbeat daemon fork (Fix B, task 24e3dd44). The fork is detached into its
|
|
914
|
+
OWN session, so os.killpg(getpgid(agent_pid)) in _kill_headless_pid does NOT take it down — it
|
|
915
|
+
would keep POSTing and show a stopped agent 'online'. Stop it by its pidfile
|
|
916
|
+
heartbeat_<proj>_<name>.pid. Best-effort."""
|
|
917
|
+
try:
|
|
918
|
+
proj, _, agent = target.partition("/")
|
|
919
|
+
if not agent:
|
|
920
|
+
return
|
|
921
|
+
pidf = STATE_DIR / f"heartbeat_{proj}_{agent}.pid"
|
|
922
|
+
if not pidf.exists():
|
|
923
|
+
return
|
|
924
|
+
try:
|
|
925
|
+
hb = int(pidf.read_text(encoding="utf-8").strip())
|
|
926
|
+
except Exception:
|
|
927
|
+
hb = 0
|
|
928
|
+
if hb:
|
|
929
|
+
try:
|
|
930
|
+
if sys.platform == "win32":
|
|
931
|
+
subprocess.run(["taskkill", "/PID", str(hb), "/F"], capture_output=True, timeout=8)
|
|
932
|
+
else:
|
|
933
|
+
os.kill(hb, _signal.SIGTERM)
|
|
934
|
+
except ProcessLookupError:
|
|
935
|
+
pass
|
|
936
|
+
except Exception:
|
|
937
|
+
pass
|
|
938
|
+
try:
|
|
939
|
+
pidf.unlink()
|
|
940
|
+
except Exception:
|
|
941
|
+
pass
|
|
942
|
+
_log(f"STOP {target}: stopped heartbeat fork (pid {hb or '?'})")
|
|
943
|
+
except Exception:
|
|
944
|
+
pass
|
|
945
|
+
|
|
946
|
+
|
|
874
947
|
def _kill_headless_pid(target: str, pid: int) -> bool:
|
|
875
948
|
"""Hard-kill a recorded headless PID + its child tree. Guards against PID reuse by
|
|
876
949
|
confirming the cmdline still looks like this meshcode agent. Returns True if killed."""
|
|
@@ -905,6 +978,7 @@ def _kill_headless_pid(target: str, pid: int) -> bool:
|
|
|
905
978
|
except Exception:
|
|
906
979
|
pass
|
|
907
980
|
_log(f"STOP {target}: hard-killed headless pid {pid}")
|
|
981
|
+
_kill_heartbeat_fork(target) # Fix B: its detached heartbeat fork won't die with the killpg
|
|
908
982
|
return True
|
|
909
983
|
except Exception as e:
|
|
910
984
|
_log(f"WARN: STOP {target} kill pid {pid} failed: {e}")
|
|
@@ -980,8 +1054,11 @@ def _do_force_kills(api_key: str, host_id: str) -> int:
|
|
|
980
1054
|
if not pid:
|
|
981
1055
|
continue
|
|
982
1056
|
if _FORCE_KILL_DRYRUN:
|
|
1057
|
+
# SAFE-ARM logging: log the cmdline so the would-kill can be validated as a real
|
|
1058
|
+
# stuck visible agent before _FORCE_KILL_DRYRUN is flipped.
|
|
983
1059
|
_log(f"FORCE-KILL-DRYRUN {target}: WOULD kill visible pid {pid} (explicit human stop, "
|
|
984
|
-
f"hb {a.get('heartbeat_age_s')}s)
|
|
1060
|
+
f"hb {a.get('heartbeat_age_s')}s) cmdline={_pid_cmdline(pid).strip()[:100]!r} — "
|
|
1061
|
+
f"log-only. Flip _FORCE_KILL_DRYRUN=False to enforce.")
|
|
985
1062
|
elif _kill_headless_pid(target, pid):
|
|
986
1063
|
pids.pop(target, None)
|
|
987
1064
|
n += 1
|
|
@@ -1037,8 +1114,11 @@ def _do_reap(api_key: str, host_id: str) -> int:
|
|
|
1037
1114
|
"""LOG-ONLY while _REAP_DRYRUN; else hard-kill (reuse-guard inside). Returns 1 if killed."""
|
|
1038
1115
|
nonlocal pids
|
|
1039
1116
|
if _REAP_DRYRUN:
|
|
1040
|
-
|
|
1041
|
-
|
|
1117
|
+
# SAFE-ARM logging: include the cmdline of what we WOULD kill so it can be validated as a
|
|
1118
|
+
# true ghost/orphan — NOT a healthy agent — before flip.
|
|
1119
|
+
_log(f"REAP-DRYRUN {target}: WOULD kill pid {pid} ({reason}) "
|
|
1120
|
+
f"cmdline={_pid_cmdline(pid).strip()[:100]!r} — log-only, no kill. "
|
|
1121
|
+
f"Flip _REAP_DRYRUN=False ONLY after confirming zero false positives.")
|
|
1042
1122
|
return 0
|
|
1043
1123
|
if _kill_headless_pid(target, pid):
|
|
1044
1124
|
if pids.get(target) == pid:
|
|
@@ -4324,31 +4324,51 @@ def _mark_realtime_msgs_read_in_db(messages: List[Dict[str, Any]]) -> None:
|
|
|
4324
4324
|
if not api_key:
|
|
4325
4325
|
return
|
|
4326
4326
|
|
|
4327
|
-
#
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4327
|
+
# RE-DELIVERY FIX (2.11.110, task 01465452 / Samuel+commander 2026-06-04):
|
|
4328
|
+
# mark_read IS the read-watermark. If it silently fails under DB load, the
|
|
4329
|
+
# messages stay read=false and meshcode_wait RE-DELIVERS them next cycle
|
|
4330
|
+
# (commander hit this 3-4x this session). Retry transient failures with a
|
|
4331
|
+
# short backoff; SURFACE (WARNING) any IDs still unmarked instead of
|
|
4332
|
+
# swallowing at debug — "exactly-once" delivery depends on this completing.
|
|
4333
|
+
def _retry_rpc(fn, attempts=3, base=0.2):
|
|
4334
|
+
last = None
|
|
4335
|
+
for i in range(attempts):
|
|
4336
|
+
try:
|
|
4337
|
+
r = fn()
|
|
4338
|
+
if not (isinstance(r, dict) and be.is_error(r)):
|
|
4339
|
+
return r, None
|
|
4340
|
+
last = be.get_error_message(r)
|
|
4341
|
+
except Exception as e:
|
|
4342
|
+
last = str(e)
|
|
4343
|
+
if i < attempts - 1:
|
|
4344
|
+
_time.sleep(base * (i + 1)) # 0.2s, 0.4s — transient timeout/contention
|
|
4345
|
+
return None, last
|
|
4346
|
+
|
|
4347
|
+
# Try batch first (1 RPC for N messages), retried on transient failure.
|
|
4348
|
+
result, err = _retry_rpc(lambda: be.sb_rpc("mc_mark_messages_read_batch", {
|
|
4349
|
+
"p_api_key": api_key,
|
|
4350
|
+
"p_project_id": _PROJECT_ID,
|
|
4351
|
+
"p_message_ids": msg_ids,
|
|
4352
|
+
}))
|
|
4353
|
+
if result is not None:
|
|
4354
|
+
log.debug(f"batch mark_read: {result.get('marked', 0)}/{len(msg_ids)} marked")
|
|
4355
|
+
return
|
|
4356
|
+
log.debug(f"batch mark_read failed after retries ({err}); falling back to individual")
|
|
4341
4357
|
|
|
4342
|
-
# Fallback: individual RPCs
|
|
4358
|
+
# Fallback: individual RPCs, each retried. Track stragglers so a persistent
|
|
4359
|
+
# failure is VISIBLE (re-delivery is the lesser evil vs silent message loss).
|
|
4360
|
+
unmarked = []
|
|
4343
4361
|
for mid in msg_ids:
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4362
|
+
_r, _e = _retry_rpc(lambda mid=mid: be.sb_rpc("mc_mark_message_read", {
|
|
4363
|
+
"p_api_key": api_key,
|
|
4364
|
+
"p_project_id": _PROJECT_ID,
|
|
4365
|
+
"p_message_id": mid,
|
|
4366
|
+
}), attempts=2)
|
|
4367
|
+
if _r is None:
|
|
4368
|
+
unmarked.append(mid)
|
|
4369
|
+
if unmarked:
|
|
4370
|
+
log.warning(f"mark_read: {len(unmarked)}/{len(msg_ids)} msgs UNMARKED after retries "
|
|
4371
|
+
f"(will re-deliver) — likely DB-load timeout: {unmarked[:5]}")
|
|
4352
4372
|
|
|
4353
4373
|
# Confirm delivery (best-effort, background) — completes the
|
|
4354
4374
|
# delivery pipeline: sent → read → confirmed.
|