From 8018ca9d74df8b036ea2079a7b4274958e69a40b Mon Sep 17 00:00:00 2001 From: itswl Date: Mon, 2 Nov 2026 13:48:58 +0800 Subject: [PATCH 1/3] watch: a cursor older than a day and a half asks for twice as much, up to the server's ceiling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scanner asked every chat for at most WATCH_MAX_PER_CHAT (60) messages and, when a chat filled it, could only note that older ones may be missing: the results come newest first and the cursor moves to the newest. A second deployment running copies of these files hit that cap on two chats in each of two Monday first rounds — a weekend's backlog is not a round's. A chat whose cursor is older than 36 hours (after a weekend, a holiday, or a chat that just appeared) now asks for twice the limit, never more than 200, which is search_chat_records' own ceiling; weekday rounds are unchanged. The truncation note names the limit actually asked. Tested with a weekend group and a weekday group, at the default and at 150; the test fails with the fixed limit back. --- deploy/watch/tests/test_scan.py | 35 +++++++++++++++++++++++++++++++++ deploy/watch/watch_scan.py | 13 +++++++++--- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/deploy/watch/tests/test_scan.py b/deploy/watch/tests/test_scan.py index e188319..2218c1b 100644 --- a/deploy/watch/tests/test_scan.py +++ b/deploy/watch/tests/test_scan.py @@ -232,3 +232,38 @@ def test_a_bot_that_says_so_in_its_name_never_reaches_the_digest(monkeypatch): ) found = watch_scan.scan_chat(chat, {}, watch_scan.when("2026-10-27T02:10:00Z"), []) assert [(name, [who for _, who, _ in msgs]) for name, msgs in found] == [("ops room", ["Talbot"])] + + +class _Feeds: + """A chat service with the given feeds; records every search it is asked.""" + + def __init__(self, feeds): + self.feeds, self.asked = feeds, [] + + def call(self, tool, **args): + if tool == "list_folder_feeds": + return {"feeds": self.feeds} + assert tool == "search_chat_records", tool + self.asked.append(args) + return {"messages": []} + + +def test_a_stale_cursor_asks_for_twice_as_much_up_to_the_servers_ceiling(monkeypatch): + """A Monday's first round: the weekend group's cursor is Friday's evening.""" + now = watch_scan.when("2026-11-02T01:40:00Z") + feeds = [ + {"name": "weekend group", "chat_type_label": "group chat", "last_message_send_at": "2026-11-01T07:00:00Z"}, + {"name": "weekday group", "chat_type_label": "group chat", "last_message_send_at": "2026-11-02T01:30:00Z"}, + ] + cursors = { + "weekend group": watch_scan.when("2026-10-30T11:20:00Z"), + "weekday group": watch_scan.when("2026-11-02T01:00:00Z"), + } + monkeypatch.setattr(watch_scan, "PER_CHAT", 60) + chat = _Feeds(feeds) + watch_scan.scan_chat(chat, dict(cursors), now, []) + assert {a["contact_name"]: a["limit"] for a in chat.asked} == {"weekend group": 120, "weekday group": 60} + monkeypatch.setattr(watch_scan, "PER_CHAT", 150) + chat = _Feeds(feeds) + watch_scan.scan_chat(chat, dict(cursors), now, []) + assert {a["contact_name"]: a["limit"] for a in chat.asked} == {"weekend group": 200, "weekday group": 150} diff --git a/deploy/watch/watch_scan.py b/deploy/watch/watch_scan.py index 781fbb1..9ac931f 100644 --- a/deploy/watch/watch_scan.py +++ b/deploy/watch/watch_scan.py @@ -93,6 +93,12 @@ # 60 而不是 12:截断要罕见到算异常。上限是 200,一个 20 分钟的窗口里 60 条已经 # 是刷屏了。真截断了下面会明说,因为结果是新的在前,被丢掉的是更早的那些。 PER_CHAT = int(os.environ.get("WATCH_MAX_PER_CHAT") or 60) +# 游标比这还旧的会话(周末之后、长假之后、刚出现的会话)积压的不是一轮的量: +# 按常量取,取满了就把更早的丢掉。另一套跑着这些文件副本的部署,在两个周一的首轮 +# 各有两个会话「一次取满 60 条」。这种会话单次多取一倍,仍不超过服务端对 limit 的 +# 上限;工作日的常规轮次不变。 +STALE_CURSOR_SECONDS = 36 * 3600 +CHAT_LIMIT_MAX = 200 # search_chat_records 的 limit 上限,超过会被整个调用拒掉 # 服务端对这个参数的上限就是 100,传 200 会被整个调用拒掉——而一次被拒的 # 列表调用意味着这一轮完全没看聊天。 FEED_LIMIT = int(os.environ.get("WATCH_FEED_LIMIT") or 100) @@ -396,13 +402,14 @@ def scan_chat( continue # 到期放行。取完(或取失败走 unreachable)都在下面清 pending。 + limit = PER_CHAT if now - base < STALE_CURSOR_SECONDS else min(PER_CHAT * 2, CHAT_LIMIT_MAX) args = { "contact_name": name, # from_time 是闭区间。原样传游标会把「设定这个游标的那条消息」再取 # 一遍——2026-09-04 就是这么把一条五小时前的 @All 当成新消息发出去的。 "from_time": str(int(base) + 1), "to_time": str(int(now)), - "limit": PER_CHAT, + "limit": limit, } if feed.get("chat_type_label") == "group chat": args["search_group"] = True @@ -467,9 +474,9 @@ def scan_chat( unreachable.pop(name, None) # 又够得着了;下次再断会重新说一次 if pending is not None: pending.pop(name, None) # 攒的一批已经端出去了;下一批重新起算 - if len(got) >= PER_CHAT: + if len(got) >= limit: notes.append( - f"{name}: 一次取满 {PER_CHAT} 条,更早的可能没取到(结果是新的在前,游标仍推进到最新,否则会一直重报)" + f"{name}: 一次取满 {limit} 条,更早的可能没取到(结果是新的在前,游标仍推进到最新,否则会一直重报)" ) if picked: findings.append((name, picked)) From 98b61e1d952c904a8621b02e8eaf11f97ec01286 Mon Sep 17 00:00:00 2001 From: itswl Date: Mon, 2 Nov 2026 13:48:58 +0800 Subject: [PATCH 2/3] work: the probes do not ask for an instance metadata service that cannot exist on their network An AWS CLI call whose profile names no region asks 169.254.169.254 for one; on the probes that ask goes to the egress proxy, which refuses it, and every refusal lands in the egress ledger as an attempt to leave that nobody made. A second deployment running copies of these files saw six in one session for exactly this reason. The probes sit on an internal network and their AWS credentials are files, so AWS_EC2_METADATA_DISABLED=true joins the shared probe environment: the CLI says 'specify a region' instead, and the posture check skips the metadata question. The operator's current profile names its region, so nothing changes today; this keeps the next profile from writing false refusals. --- deploy/docker-compose.work.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/deploy/docker-compose.work.yml b/deploy/docker-compose.work.yml index 13fe7ed..e2bd3d8 100644 --- a/deploy/docker-compose.work.yml +++ b/deploy/docker-compose.work.yml @@ -43,6 +43,14 @@ x-probe-proxy: &probe-proxy HTTP_PROXY: ${HOOKPROBE_EGRESS_PROXY:-http://egress-proxy:8888} HTTPS_PROXY: ${HOOKPROBE_EGRESS_PROXY:-http://egress-proxy:8888} NO_PROXY: ${HOOKPROBE_NO_PROXY:-127.0.0.1,localhost,host.docker.internal,hookrelay,sink,egress-proxy,mcp-gate,watch-signer} + # No instance metadata service exists on the probes' network, and their AWS + # credentials are files. Without this, an AWS CLI call whose profile names no + # region asks 169.254.169.254 for one through the proxy above, and each ask + # lands in the egress ledger as a refusal that is nobody's attempt to leave — + # a second deployment running copies of these files saw six in one session. + # The CLI now says "specify a region" instead; the posture check skips the + # metadata question for the same reason. + AWS_EC2_METADATA_DISABLED: "true" services: hookrelay: From d369f46c2f648392ea4b533b3c796115de905c5f Mon Sep 17 00:00:00 2001 From: itswl Date: Mon, 2 Nov 2026 13:48:58 +0800 Subject: [PATCH 3/3] =?UTF-8?q?deps:=20msgpack=201.2.4=20=E2=80=94=20an=20?= =?UTF-8?q?advisory=20published=20since=20the=20last=20lock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CVE-2026-114613 (msgpack's C extension leaks a decoded value when a map key is rejected; fixed in 1.2.3) turned the gate red at pip-audit; every service's lock carried 1.2.1, main included. msgpack reaches the venvs only through pip-audit's own cachecontrol (msgpack>=0.5.2,<2.0.0), and the images install runtime packages only, so no published image carries it. 1.2.4 is the newest release, tagged upstream; installed into each venv, the locks regenerated by scripts/relock.sh, and msgpack is the only pin that moved. STACK GATE GREEN. --- hookjudge/requirements.lock | 2 +- hookprobe/requirements.lock | 2 +- hookrelay/requirements.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hookjudge/requirements.lock b/hookjudge/requirements.lock index 1899743..131c3a7 100644 --- a/hookjudge/requirements.lock +++ b/hookjudge/requirements.lock @@ -24,7 +24,7 @@ librt==0.16.0 license-expression==30.4.4 markdown-it-py==4.2.0 mdurl==0.1.2 -msgpack==1.2.1 +msgpack==1.2.4 mypy==2.4.0 mypy_extensions==1.1.0 opentelemetry-api==1.45.1 diff --git a/hookprobe/requirements.lock b/hookprobe/requirements.lock index 9c98c40..f3c4c13 100644 --- a/hookprobe/requirements.lock +++ b/hookprobe/requirements.lock @@ -33,7 +33,7 @@ markdown-it-py==4.2.0 mcp==2.3.0 mcp-types==2.3.0 mdurl==0.1.2 -msgpack==1.2.1 +msgpack==1.2.4 mypy==2.4.0 mypy_extensions==1.1.0 opentelemetry-api==1.45.1 diff --git a/hookrelay/requirements.lock b/hookrelay/requirements.lock index 177a244..33ee005 100644 --- a/hookrelay/requirements.lock +++ b/hookrelay/requirements.lock @@ -24,7 +24,7 @@ librt==0.16.0 license-expression==30.4.4 markdown-it-py==4.2.0 mdurl==0.1.2 -msgpack==1.2.1 +msgpack==1.2.4 mypy==2.4.0 mypy_extensions==1.1.0 opentelemetry-api==1.45.1