@meridiona/meridian-darwin-arm64 1.66.0 → 1.66.2

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.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 1.66.0
1
+ 1.66.2
package/bin/meridian CHANGED
Binary file
package/bin/meridian-tray CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meridiona/meridian-darwin-arm64",
3
- "version": "1.66.0",
3
+ "version": "1.66.2",
4
4
  "description": "Prebuilt Meridian app for macOS arm64 (daemon binary + dashboard + Python services). Installed via @meridiona/meridian.",
5
5
  "homepage": "https://github.com/Meridiona/meridian",
6
6
  "repository": {
@@ -13,7 +13,7 @@ app_state: dict[str, Any] = {}
13
13
  # `received`/`total` are AGGREGATE byte counts summed across every pipeline model
14
14
  # (llm + reranker + embedder), so the wizard's progress bar has a single honest
15
15
  # denominator. The wire contract the Rust tray decodes is exactly
16
- # state/received/total/error (tray/src-tauri/src/mlx_server.rs::PrefetchStatus);
16
+ # state/received/total/error/speed (tray/src-tauri/src/mlx_server.rs::PrefetchStatus);
17
17
  # `models` is an additive per-model breakdown the tray ignores.
18
18
  prefetch_state: dict[str, Any] = {
19
19
  "state": "idle",
@@ -16,6 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  import asyncio
18
18
  import logging
19
+ import os
19
20
  import threading
20
21
  import time
21
22
  from pathlib import Path
@@ -31,12 +32,12 @@ log = logging.getLogger("agents.server")
31
32
  router = APIRouter()
32
33
 
33
34
  # Speed is derived from the growth of on-disk `received` between /prefetch_status
34
- # polls — NOT from a tqdm hook. hf_xet (the Xet accelerator these models use) does
35
- # its download in Rust and bypasses the classic tqdm bar, so a tqdm_class never
36
- # fires; the byte-delta approach works for any backend. These hold the last
37
- # (received, monotonic-time) sample to diff against.
38
- _last_recv = 0
39
- _last_recv_ts = 0.0
35
+ # polls — NOT from a tqdm hook (a tqdm_class override never fires reliably across
36
+ # backends). The byte-delta approach is backend-agnostic; with Xet disabled for the
37
+ # prefetch (see `_download_spec`) the classic path grows the blob linearly, so this
38
+ # now reads as a smooth rate rather than the 0-then-burst pattern hf_xet produced.
39
+ # Stored as a dict so both functions can mutate it in-place without `global`s.
40
+ _speed_state: dict[str, float | int] = {"recv": 0, "ts": 0.0}
40
41
 
41
42
 
42
43
  def _hf_cache_dir_for(model_id: str) -> Path:
@@ -82,17 +83,84 @@ def _spec_total_bytes(spec: model_registry.ModelSpec) -> int:
82
83
  return total
83
84
 
84
85
 
86
+ def _env_positive_int(name: str, default: int) -> int:
87
+ """Read a positive-int env override, falling back to `default` on bad input.
88
+
89
+ Parsed at import, so a non-integer value (operator typo) must degrade to the
90
+ default rather than raise ValueError and brick MLX-server startup.
91
+ """
92
+ raw = os.environ.get(name)
93
+ if raw is None:
94
+ return default
95
+ try:
96
+ return max(1, int(raw))
97
+ except ValueError:
98
+ log.warning(
99
+ "server: ignoring non-integer env override; using default",
100
+ extra={"env_var": name, "raw": raw, "default": default},
101
+ )
102
+ return default
103
+
104
+
105
+ # How many times to (re)enter snapshot_download for one model before giving up.
106
+ # Each retry resumes from the on-disk ``.incomplete`` partial (HF Range request),
107
+ # so bytes already pulled are never re-downloaded. Env-overridable for ops.
108
+ _PREFETCH_MAX_ATTEMPTS = _env_positive_int("MERIDIAN_PREFETCH_MAX_ATTEMPTS", 5)
109
+
110
+
85
111
  def _download_spec(spec: model_registry.ModelSpec) -> None:
86
112
  """Download one model's weights to the HF cache (no load into memory).
87
113
 
88
- All specs go through ``snapshot_download`` (filtered to the spec's
89
- allow_patterns); ``hf-xet`` transparently accelerates the Xet-backed transfers.
114
+ Forces the classic (non-Xet) transfer path and retries with resume so a
115
+ stalled connection can never wedge the download indefinitely:
116
+
117
+ * **Xet is disabled for the pull.** ``hf_xet`` does its transfer in Rust with
118
+ no per-read timeout and can't be interrupted from Python, so a half-open
119
+ connection to the Xet CAS endpoint hangs forever (observed: a prefetch stuck
120
+ at 57% for 4h on a healthy 16 Mbps link — bytes never resumed). The classic
121
+ path caps every read at ``HF_HUB_DOWNLOAD_TIMEOUT`` (10s) and resumes from the
122
+ ``.incomplete`` partial via a Range request, so a stall raises and self-heals
123
+ instead of hanging. On a cold first-run pull of distinct model weights Xet's
124
+ chunk-dedup buys little, so this trades an unused fast-path for a bounded,
125
+ resumable one. (Bonus: the classic path grows the on-disk blob linearly, so
126
+ ``/prefetch_status``'s byte-delta speed reads smoothly instead of sitting at 0
127
+ then jumping in bursts as hf_xet flushes reconstructed chunks.)
128
+ * **Bounded retries** re-enter ``snapshot_download`` on any error; each attempt
129
+ resumes from the partial, so transient drops that exhaust the library's own
130
+ per-read retries still converge without re-pulling what's on disk.
131
+
90
132
  ``mlx_lm.load`` / ``mlx_embeddings.load`` resolve the files straight from the
91
133
  cache afterwards. Only disk is touched — the single-slot runtime loads each
92
- model lazily on first use.
134
+ model lazily on first use. Disabling Xet here is process-global (it flips a
135
+ huggingface_hub module constant), so later in-process model loads inherit the
136
+ same bounded, resumable path — which is what we want on a network where Xet
137
+ stalls.
93
138
  """
139
+ from huggingface_hub import constants as hf_constants
94
140
  from huggingface_hub import snapshot_download
95
- snapshot_download(spec.model_id, allow_patterns=spec.allow_patterns)
141
+
142
+ # Flip the *module attribute* (re-read per download at file_download.py call
143
+ # sites), NOT the env var: the env is parsed into this constant once at import,
144
+ # so ``os.environ`` here would be a no-op for an already-imported library.
145
+ hf_constants.HF_HUB_DISABLE_XET = True
146
+
147
+ for attempt in range(1, _PREFETCH_MAX_ATTEMPTS + 1):
148
+ try:
149
+ snapshot_download(spec.model_id, allow_patterns=spec.allow_patterns)
150
+ return
151
+ except Exception as exc: # noqa: BLE001 — retry-with-resume, re-raise if exhausted
152
+ log.warning(
153
+ "server: prefetch download attempt failed; will resume from partial",
154
+ extra={
155
+ "model_id": spec.model_id,
156
+ "attempt": attempt,
157
+ "max_attempts": _PREFETCH_MAX_ATTEMPTS,
158
+ "error": str(exc),
159
+ },
160
+ )
161
+ if attempt >= _PREFETCH_MAX_ATTEMPTS:
162
+ raise # all attempts exhausted — surface the active error to _run_prefetch
163
+ time.sleep(min(2**attempt, 30)) # 2s, 4s, 8s, 16s … capped at 30s
96
164
 
97
165
 
98
166
  def _run_prefetch(specs: list[model_registry.ModelSpec]) -> None:
@@ -140,7 +208,7 @@ def _run_prefetch(specs: list[model_registry.ModelSpec]) -> None:
140
208
  prefetch_state["error"] = str(exc)
141
209
  prefetch_state["speed"] = 0.0
142
210
  root.set_status(trace.Status(trace.StatusCode.ERROR, str(exc)))
143
- log.error("server: model prefetch failed", extra={"error": str(exc)})
211
+ log.exception("server: model prefetch failed", extra={"error": str(exc)})
144
212
 
145
213
 
146
214
  @router.post("/prefetch_model")
@@ -160,13 +228,13 @@ async def prefetch_model() -> dict:
160
228
 
161
229
  from fastapi.concurrency import run_in_threadpool
162
230
 
163
- global _last_recv, _last_recv_ts
164
231
  specs = list(model_registry.ALL_SPECS)
165
232
 
166
233
  with prefetch_lock:
167
234
  if prefetch_state["state"] in ("downloading", "done"):
168
235
  return dict(prefetch_state) # idempotent — no duplicate downloads
169
- _last_recv, _last_recv_ts = 0, 0.0
236
+ _speed_state["recv"] = 0
237
+ _speed_state["ts"] = 0.0
170
238
  prefetch_state.update(
171
239
  state="downloading",
172
240
  received=0,
@@ -189,7 +257,7 @@ async def prefetch_model() -> dict:
189
257
  return_exceptions=True,
190
258
  )
191
259
  totals: list[int] = []
192
- for spec, r in zip(specs, probe_results):
260
+ for spec, r in zip(specs, probe_results, strict=True):
193
261
  if isinstance(r, int):
194
262
  totals.append(r)
195
263
  else:
@@ -223,7 +291,6 @@ async def prefetch_status() -> dict:
223
291
  """
224
292
  from fastapi.concurrency import run_in_threadpool
225
293
 
226
- global _last_recv, _last_recv_ts
227
294
  with prefetch_lock:
228
295
  st = dict(prefetch_state)
229
296
  models = list(st.get("models", []))
@@ -252,12 +319,13 @@ async def prefetch_status() -> dict:
252
319
  # EMA-smoothed; decays to 0 when bytes stop flowing (delta 0) or the run ends.
253
320
  now = time.monotonic()
254
321
  with prefetch_lock:
255
- if st["state"] == "downloading" and _last_recv_ts and now > _last_recv_ts:
256
- inst = max(0, st["received"] - _last_recv) / (now - _last_recv_ts)
322
+ if st["state"] == "downloading" and _speed_state["ts"] and now > _speed_state["ts"]:
323
+ inst = max(0, st["received"] - _speed_state["recv"]) / (now - _speed_state["ts"])
257
324
  prev = prefetch_state.get("speed", 0.0) or 0.0
258
325
  prefetch_state["speed"] = inst if prev == 0.0 else prev * 0.5 + inst * 0.5
259
326
  elif st["state"] != "downloading":
260
327
  prefetch_state["speed"] = 0.0
261
- _last_recv, _last_recv_ts = st["received"], now
328
+ _speed_state["recv"] = st["received"]
329
+ _speed_state["ts"] = now
262
330
  st["speed"] = prefetch_state["speed"]
263
331
  return st
@@ -96,7 +96,7 @@ _ENTITY_RESCUE_CAP = 4
96
96
  _embedder = None # cached (model, tokenizer) tuple
97
97
 
98
98
 
99
- def _get_embedder():
99
+ def _get_embedder() -> tuple:
100
100
  global _embedder
101
101
  if _embedder is None:
102
102
  import mlx_embeddings
@@ -135,7 +135,8 @@ def evict_embedder() -> None:
135
135
  _embedder = None
136
136
  gc.collect()
137
137
  try:
138
- import mlx.core as mx; mx.clear_cache()
138
+ import mlx.core as mx
139
+ mx.clear_cache()
139
140
  except Exception: # noqa: BLE001 — cache flush is best-effort; non-fatal if mlx absent
140
141
  pass
141
142
  log.info("session_distiller: embedding model evicted from memory")
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meridian-agents"
7
- version = "1.66.0"
7
+ version = "1.66.2"
8
8
  description = "Meridian agents — MLX classifier server and Jira worklog synthesis for meridian.db"
9
9
  requires-python = ">=3.11"
10
10
  authors = [{ name = "Meridiona" }]