meshcode 2.10.49__tar.gz → 2.10.50__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.10.49 → meshcode-2.10.50}/PKG-INFO +1 -1
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/__init__.py +1 -1
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/meshcode_mcp/realtime.py +34 -4
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/meshcode_mcp/server.py +127 -63
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.49 → meshcode-2.10.50}/pyproject.toml +1 -1
- {meshcode-2.10.49 → meshcode-2.10.50}/README.md +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/cli.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/comms_v4.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/invites.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/launcher.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/preferences.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/run_agent.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/secrets.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/self_update.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/setup.cfg +0 -0
- {meshcode-2.10.49 → meshcode-2.10.50}/tests/test_status_enum_coverage.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "2.10.
|
|
2
|
+
__version__ = "2.10.50"
|
|
@@ -173,6 +173,18 @@ class RealtimeListener:
|
|
|
173
173
|
"schema": "meshcode",
|
|
174
174
|
"table": "mc_messages",
|
|
175
175
|
"filter": "to_agent=eq.*",
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"event": "INSERT",
|
|
179
|
+
"schema": "meshcode",
|
|
180
|
+
"table": "mc_tasks",
|
|
181
|
+
"filter": f"assignee=eq.{url_quote(self.agent_name, safe='')}",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"event": "UPDATE",
|
|
185
|
+
"schema": "meshcode",
|
|
186
|
+
"table": "mc_tasks",
|
|
187
|
+
"filter": f"assignee=eq.{url_quote(self.agent_name, safe='')}",
|
|
176
188
|
}
|
|
177
189
|
]
|
|
178
190
|
}
|
|
@@ -260,7 +272,22 @@ class RealtimeListener:
|
|
|
260
272
|
# {"event": "postgres_changes", "payload": {"data": {"record": {...}, "type": "INSERT", ...}}}
|
|
261
273
|
if event == "postgres_changes":
|
|
262
274
|
data = payload.get("data") or {}
|
|
263
|
-
|
|
275
|
+
table = data.get("table", (data.get("record") or {}).get("_table", ""))
|
|
276
|
+
change_type = data.get("type")
|
|
277
|
+
|
|
278
|
+
# ── mc_tasks events: wake the agent when a task is assigned/updated ──
|
|
279
|
+
if table == "mc_tasks" and change_type in ("INSERT", "UPDATE"):
|
|
280
|
+
record = data.get("record") or {}
|
|
281
|
+
assignee = record.get("assignee", "")
|
|
282
|
+
if assignee == self.agent_name and record.get("status") in ("open", "in_progress"):
|
|
283
|
+
log.info(f"task event: {change_type} task '{record.get('title', '?')[:60]}' for {self.agent_name}")
|
|
284
|
+
try:
|
|
285
|
+
self.message_event.set()
|
|
286
|
+
except Exception:
|
|
287
|
+
pass
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
if change_type == "INSERT":
|
|
264
291
|
record = data.get("record") or {}
|
|
265
292
|
to = record.get("to_agent")
|
|
266
293
|
from_agent = record.get("from_agent")
|
|
@@ -302,13 +329,16 @@ class RealtimeListener:
|
|
|
302
329
|
|
|
303
330
|
def drain(self) -> list:
|
|
304
331
|
"""Pop and return all queued messages."""
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
#
|
|
332
|
+
# Clear event FIRST, then drain queue. If a message arrives between
|
|
333
|
+
# clear() and the drain, it re-sets the event and lands in queue —
|
|
334
|
+
# the next wait_for_message returns immediately. This avoids the race
|
|
335
|
+
# where clear() after drain() could eat a wake signal.
|
|
308
336
|
try:
|
|
309
337
|
self.message_event.clear()
|
|
310
338
|
except Exception:
|
|
311
339
|
pass
|
|
340
|
+
out = list(self.queue)
|
|
341
|
+
self.queue.clear()
|
|
312
342
|
return out
|
|
313
343
|
|
|
314
344
|
async def wait_for_message(self, timeout: Optional[float] = None) -> bool:
|
|
@@ -13,6 +13,7 @@ import logging
|
|
|
13
13
|
import os
|
|
14
14
|
import sys
|
|
15
15
|
import hashlib as _hashlib
|
|
16
|
+
import threading as _threading
|
|
16
17
|
import traceback as _traceback
|
|
17
18
|
from collections import deque
|
|
18
19
|
from contextlib import asynccontextmanager
|
|
@@ -104,12 +105,14 @@ _SEEN_MSG_IDS: dict = {} # key -> timestamp (monotonic)
|
|
|
104
105
|
_SEEN_MSG_ORDER: deque = deque()
|
|
105
106
|
_SEEN_MSG_CAP = 2000
|
|
106
107
|
_SEEN_TTL = 300.0 # 5 minutes
|
|
108
|
+
_SEEN_LOCK = _threading.Lock() # Guards _SEEN_MSG_IDS + _SEEN_MSG_ORDER
|
|
107
109
|
|
|
108
110
|
# ============================================================
|
|
109
111
|
# Auto-wake: when agent is NOT in meshcode_wait and a message
|
|
110
112
|
# arrives, inject text into the terminal to wake the agent.
|
|
111
113
|
# ============================================================
|
|
112
114
|
_IN_WAIT = False # True while meshcode_wait is blocking
|
|
115
|
+
_STATE_LOCK = _threading.Lock() # Guards _IN_WAIT, _CURRENT_STATE, _last_tool_at
|
|
113
116
|
# Default OFF — keystroke injection can corrupt stdin on some terminals.
|
|
114
117
|
# The primary nudge path is now in comms_v4.nudge_agent() which uses
|
|
115
118
|
# platform-specific window activation (SetForegroundWindow on Windows,
|
|
@@ -292,8 +295,8 @@ def _seen_key(msg: Dict[str, Any]) -> str:
|
|
|
292
295
|
return f"{msg.get('from') or msg.get('from_agent')}|{msg.get('ts') or msg.get('created_at')}|{payload_str}"
|
|
293
296
|
|
|
294
297
|
|
|
295
|
-
def
|
|
296
|
-
"""Remove entries older than _SEEN_TTL
|
|
298
|
+
def _evict_expired_unlocked() -> None:
|
|
299
|
+
"""Remove entries older than _SEEN_TTL. Caller MUST hold _SEEN_LOCK."""
|
|
297
300
|
now = _time.monotonic()
|
|
298
301
|
while _SEEN_MSG_ORDER:
|
|
299
302
|
oldest_key = _SEEN_MSG_ORDER[0]
|
|
@@ -305,31 +308,43 @@ def _evict_expired() -> None:
|
|
|
305
308
|
break
|
|
306
309
|
|
|
307
310
|
|
|
311
|
+
def _evict_expired() -> None:
|
|
312
|
+
"""Thread-safe wrapper for eviction."""
|
|
313
|
+
with _SEEN_LOCK:
|
|
314
|
+
_evict_expired_unlocked()
|
|
315
|
+
|
|
316
|
+
|
|
308
317
|
def _mark_seen(key: str) -> None:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
318
|
+
with _SEEN_LOCK:
|
|
319
|
+
now = _time.monotonic()
|
|
320
|
+
if key in _SEEN_MSG_IDS:
|
|
321
|
+
_SEEN_MSG_IDS[key] = now
|
|
322
|
+
return
|
|
312
323
|
_SEEN_MSG_IDS[key] = now
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
while len(_SEEN_MSG_ORDER) > _SEEN_MSG_CAP:
|
|
319
|
-
old = _SEEN_MSG_ORDER.popleft()
|
|
320
|
-
_SEEN_MSG_IDS.pop(old, None)
|
|
324
|
+
_SEEN_MSG_ORDER.append(key)
|
|
325
|
+
_evict_expired_unlocked()
|
|
326
|
+
while len(_SEEN_MSG_ORDER) > _SEEN_MSG_CAP:
|
|
327
|
+
old = _SEEN_MSG_ORDER.popleft()
|
|
328
|
+
_SEEN_MSG_IDS.pop(old, None)
|
|
321
329
|
|
|
322
330
|
|
|
323
331
|
def _filter_and_mark(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
324
|
-
"""Drop already-seen messages; mark the rest as seen."""
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
332
|
+
"""Drop already-seen messages; mark the rest as seen. Thread-safe."""
|
|
333
|
+
with _SEEN_LOCK:
|
|
334
|
+
_evict_expired_unlocked()
|
|
335
|
+
out = []
|
|
336
|
+
for m in messages:
|
|
337
|
+
k = _seen_key(m)
|
|
338
|
+
if k in _SEEN_MSG_IDS:
|
|
339
|
+
continue
|
|
340
|
+
now = _time.monotonic()
|
|
341
|
+
_SEEN_MSG_IDS[k] = now
|
|
342
|
+
_SEEN_MSG_ORDER.append(k)
|
|
343
|
+
out.append(m)
|
|
344
|
+
# Cap enforcement
|
|
345
|
+
while len(_SEEN_MSG_ORDER) > _SEEN_MSG_CAP:
|
|
346
|
+
old = _SEEN_MSG_ORDER.popleft()
|
|
347
|
+
_SEEN_MSG_IDS.pop(old, None)
|
|
333
348
|
return out
|
|
334
349
|
|
|
335
350
|
|
|
@@ -1330,11 +1345,12 @@ def _heartbeat_loop_inner():
|
|
|
1330
1345
|
try:
|
|
1331
1346
|
be.sb_rpc("mc_heartbeat", {"p_project_id": _PROJECT_ID, "p_agent_name": AGENT_NAME, "p_version": _SDK_VERSION})
|
|
1332
1347
|
|
|
1333
|
-
# CPU-based status detection
|
|
1348
|
+
# CPU-based status detection — read shared state under lock
|
|
1334
1349
|
parent_cpu = _get_parent_cpu()
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1350
|
+
with _STATE_LOCK:
|
|
1351
|
+
cur_state = _current_state
|
|
1352
|
+
in_wait = _IN_WAIT
|
|
1353
|
+
idle_secs = _time.time() - _last_tool_at
|
|
1338
1354
|
|
|
1339
1355
|
if _is_windows and lease_counter % 12 == 0:
|
|
1340
1356
|
# Periodic Windows debug dump (every ~60s on pro, ~180s on free)
|
|
@@ -1501,6 +1517,10 @@ async def lifespan(_app):
|
|
|
1501
1517
|
_heartbeat_stop.set()
|
|
1502
1518
|
except Exception:
|
|
1503
1519
|
pass
|
|
1520
|
+
try:
|
|
1521
|
+
be.set_status(_PROJECT_ID, AGENT_NAME, "offline", "terminal closed", api_key=_get_api_key())
|
|
1522
|
+
except Exception:
|
|
1523
|
+
pass
|
|
1504
1524
|
try:
|
|
1505
1525
|
_remove_pid_lockfile()
|
|
1506
1526
|
except Exception:
|
|
@@ -1949,19 +1969,17 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
|
|
|
1949
1969
|
db_pending = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=_get_api_key())
|
|
1950
1970
|
if db_pending and db_pending > 0:
|
|
1951
1971
|
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=True, api_key=_get_api_key())
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
else:
|
|
1964
|
-
split = _split_messages(deduped)
|
|
1972
|
+
if raw:
|
|
1973
|
+
msgs = [
|
|
1974
|
+
{"from": m["from_agent"], "type": m.get("type", "msg"),
|
|
1975
|
+
"ts": m.get("created_at"), "payload": m.get("payload", {}),
|
|
1976
|
+
"id": m.get("id"), "parent_id": m.get("parent_msg_id")}
|
|
1977
|
+
for m in raw
|
|
1978
|
+
]
|
|
1979
|
+
# DB is source of truth: if unread in DB, deliver to agent.
|
|
1980
|
+
# Don't let in-memory dedup cache block delivery.
|
|
1981
|
+
_filter_and_mark(msgs) # mark as seen but don't filter
|
|
1982
|
+
split = _split_messages(msgs)
|
|
1965
1983
|
# Only refuse for real messages — ack-only batches should not block wait
|
|
1966
1984
|
if split["messages"] or split["done_signals"]:
|
|
1967
1985
|
return {
|
|
@@ -1974,7 +1992,8 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
|
|
|
1974
1992
|
except Exception:
|
|
1975
1993
|
pass
|
|
1976
1994
|
|
|
1977
|
-
|
|
1995
|
+
with _STATE_LOCK:
|
|
1996
|
+
_IN_WAIT = True
|
|
1978
1997
|
_set_state("waiting", "listening for messages")
|
|
1979
1998
|
capped_timeout = min(max(1, int(timeout_seconds)), 20)
|
|
1980
1999
|
try:
|
|
@@ -1996,6 +2015,13 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
|
|
|
1996
2015
|
_CONSECUTIVE_IDLE_SECONDS = 0
|
|
1997
2016
|
break
|
|
1998
2017
|
|
|
2018
|
+
# Tasks detected mid-wait (via Realtime mc_tasks subscription
|
|
2019
|
+
# or periodic sub-iteration polling) — return immediately.
|
|
2020
|
+
if result.get("pending_tasks"):
|
|
2021
|
+
_set_state("online", "")
|
|
2022
|
+
_CONSECUTIVE_IDLE_SECONDS = 0
|
|
2023
|
+
break
|
|
2024
|
+
|
|
1999
2025
|
if result.get("timed_out"):
|
|
2000
2026
|
_CONSECUTIVE_IDLE_SECONDS += capped_timeout
|
|
2001
2027
|
|
|
@@ -2047,7 +2073,8 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
|
|
|
2047
2073
|
log.debug(f"last_seen memory persist failed: {e}")
|
|
2048
2074
|
return result
|
|
2049
2075
|
finally:
|
|
2050
|
-
|
|
2076
|
+
with _STATE_LOCK:
|
|
2077
|
+
_IN_WAIT = False
|
|
2051
2078
|
|
|
2052
2079
|
|
|
2053
2080
|
def _mark_realtime_msgs_read_in_db(messages: List[Dict[str, Any]]) -> None:
|
|
@@ -2111,6 +2138,9 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
2111
2138
|
"got_message": True,
|
|
2112
2139
|
**split,
|
|
2113
2140
|
}
|
|
2141
|
+
# Expose queue overflow metric so agents know messages were lost
|
|
2142
|
+
if _REALTIME and _REALTIME.dropped_count > 0:
|
|
2143
|
+
out["dropped_messages"] = _REALTIME.dropped_count
|
|
2114
2144
|
done = _detect_global_done(deduped)
|
|
2115
2145
|
if done:
|
|
2116
2146
|
out["got_done"] = True
|
|
@@ -2130,10 +2160,10 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
2130
2160
|
_rt_live = _REALTIME and _REALTIME.is_subscribed
|
|
2131
2161
|
|
|
2132
2162
|
if _rt_live:
|
|
2133
|
-
# 2a)
|
|
2134
|
-
# Split into 5s sub-waits
|
|
2135
|
-
#
|
|
2136
|
-
#
|
|
2163
|
+
# 2a) Realtime wait with DB safety net.
|
|
2164
|
+
# Split into 5s sub-waits. Between each sub-wait, also check DB
|
|
2165
|
+
# as a safety net — Realtime WS can die silently and is_subscribed
|
|
2166
|
+
# stays True, so we must not rely on it exclusively.
|
|
2137
2167
|
_rt_sub_timeout = 5.0
|
|
2138
2168
|
_rt_elapsed = 0.0
|
|
2139
2169
|
woke = False
|
|
@@ -2153,6 +2183,34 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
2153
2183
|
if woke:
|
|
2154
2184
|
break
|
|
2155
2185
|
_rt_elapsed += _this_wait
|
|
2186
|
+
# Check for new tasks between sub-iterations
|
|
2187
|
+
_sub_tasks = _get_pending_tasks_summary()
|
|
2188
|
+
if _sub_tasks:
|
|
2189
|
+
return {"timed_out": False, "got_message": False, "pending_tasks": _sub_tasks, "reason": "task_detected_mid_wait"}
|
|
2190
|
+
# DB safety net: check for unread messages even when Realtime
|
|
2191
|
+
# claims to be alive. Realtime WS can die silently.
|
|
2192
|
+
try:
|
|
2193
|
+
_safety_key = _get_api_key()
|
|
2194
|
+
if _safety_key:
|
|
2195
|
+
_safety_cnt = be.count_pending(_PROJECT_ID, AGENT_NAME, api_key=_safety_key)
|
|
2196
|
+
if _safety_cnt and _safety_cnt > 0:
|
|
2197
|
+
log.info(f"[meshcode] DB safety net: {_safety_cnt} unread msgs despite Realtime — reading from DB")
|
|
2198
|
+
_safety_raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=True, api_key=_safety_key)
|
|
2199
|
+
if _safety_raw:
|
|
2200
|
+
_safety_msgs = [
|
|
2201
|
+
{"from": m["from_agent"], "type": m.get("type", "msg"),
|
|
2202
|
+
"ts": m.get("created_at"), "payload": m.get("payload", {}),
|
|
2203
|
+
"id": m.get("id"), "parent_id": m.get("parent_msg_id")}
|
|
2204
|
+
for m in _safety_raw
|
|
2205
|
+
]
|
|
2206
|
+
_filter_and_mark(_safety_msgs)
|
|
2207
|
+
_safety_split = _split_messages(_safety_msgs)
|
|
2208
|
+
if not include_acks:
|
|
2209
|
+
_safety_split["acks"] = []
|
|
2210
|
+
if _safety_split["messages"] or _safety_split["done_signals"]:
|
|
2211
|
+
return {"got_message": True, **_safety_split}
|
|
2212
|
+
except Exception as _db_err:
|
|
2213
|
+
log.warning(f"[meshcode] DB safety net error: {_db_err}")
|
|
2156
2214
|
# Health check: if subscription dropped, switch to DB poll
|
|
2157
2215
|
if not (_REALTIME and _REALTIME.is_subscribed):
|
|
2158
2216
|
log.info("[meshcode] Realtime subscription lost mid-wait — switching to DB poll")
|
|
@@ -2188,15 +2246,17 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
2188
2246
|
"id": m.get("id"), "parent_id": m.get("parent_msg_id")}
|
|
2189
2247
|
for m in raw
|
|
2190
2248
|
]
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2249
|
+
# DB is source of truth: if read=false in DB, deliver
|
|
2250
|
+
# regardless of in-memory dedup cache (fixes stuck loop
|
|
2251
|
+
# where dedup blocks delivery but mark_read already fired).
|
|
2252
|
+
_filter_and_mark(msgs) # mark as seen but don't filter
|
|
2253
|
+
split = _split_messages(msgs)
|
|
2254
|
+
if not include_acks:
|
|
2255
|
+
split["acks"] = []
|
|
2256
|
+
if split["messages"] or split["done_signals"]:
|
|
2257
|
+
return {"got_message": True, **split}
|
|
2198
2258
|
except Exception as e:
|
|
2199
|
-
log.
|
|
2259
|
+
log.warning(f"DB poll fallback error: {e}")
|
|
2200
2260
|
|
|
2201
2261
|
# Final fallback: one last DB check (covers realtime path missing msgs)
|
|
2202
2262
|
try:
|
|
@@ -2212,15 +2272,15 @@ async def _meshcode_wait_inner(actual_timeout: int, include_acks: bool) -> Dict[
|
|
|
2212
2272
|
"id": m.get("id"), "parent_id": m.get("parent_msg_id")}
|
|
2213
2273
|
for m in raw
|
|
2214
2274
|
]
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2275
|
+
# DB is source of truth: deliver all unread messages.
|
|
2276
|
+
_filter_and_mark(msgs) # mark as seen but don't filter
|
|
2277
|
+
split = _split_messages(msgs)
|
|
2278
|
+
if not include_acks:
|
|
2279
|
+
split["acks"] = []
|
|
2280
|
+
if split["messages"] or split["done_signals"]:
|
|
2281
|
+
return {"got_message": True, **split}
|
|
2222
2282
|
except Exception as e:
|
|
2223
|
-
log.
|
|
2283
|
+
log.warning(f"final DB fallback error: {e}")
|
|
2224
2284
|
|
|
2225
2285
|
# Check if there's any pending work before returning timeout
|
|
2226
2286
|
pending_tasks = _get_pending_tasks_summary()
|
|
@@ -2302,8 +2362,12 @@ def meshcode_check(include_acks: bool = False, since: Optional[str] = None, mark
|
|
|
2302
2362
|
|
|
2303
2363
|
# When mark_read=True, update tracking state so messages aren't re-processed
|
|
2304
2364
|
if mark_read and deduped:
|
|
2305
|
-
|
|
2306
|
-
|
|
2365
|
+
with _SEEN_LOCK:
|
|
2366
|
+
_now = _time.monotonic()
|
|
2367
|
+
for m in deduped:
|
|
2368
|
+
k = _seen_key(m)
|
|
2369
|
+
_SEEN_MSG_IDS[k] = _now
|
|
2370
|
+
_SEEN_MSG_ORDER.append(k)
|
|
2307
2371
|
latest_ts = max((str(m.get("ts", "")) for m in deduped), default=None)
|
|
2308
2372
|
if latest_ts and (not _LAST_SEEN_TS or latest_ts > _LAST_SEEN_TS):
|
|
2309
2373
|
_LAST_SEEN_TS = latest_ts
|
|
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
|