Repository navigation
emrg: the Feishu wake condition reads five conversations per poll, so an @ outside them never wakes the task #2631
Description
What happens now
emrg/server/wake.py polls Feishu for any task whose record carries when: {feishu: N}. _condition_feishu (line 279) first asks the CLI for the host's most recently active conversations:
im +chat-list --types=p2p,group --sort=active_time --as user --page-size 5 --json
then reads each of them with one call of its own:
im +chat-messages-list --chat-id <id> --start <ISO> --order asc --as user --no-reactions --json
over a 900-second window (FEISHU_LOOKBACK_SECONDS = 900, line 68; FEISHU_MAX_CHATS = 5, line 63). _needs_the_host (line 624) counts a message as needing the host when it mentions the host, mentions @all, or arrives in a direct conversation — but only among the messages of those five conversations.
Why that is wrong
FEISHU_MAX_CHATS is a wall-clock budget, not a correctness choice: one conversation is one CLI call in the foreground, so the number a poll can read is bounded by how long a poll may take. The consequence is a dead zone, and the dead zone is silent. A conversation outside the five most recently active is never read on any poll, so an @ or a direct message in it never wakes the task. What the condition answers in that state is absent — the same answer a quiet inbox gives — so nothing in the log distinguishes "nobody asked" from "somebody asked somewhere this poll does not look".
FEISHU_SEEN_CAP's comment on line 73 rests on the same premise: the lookback window is what makes forgetting old ids safe, and a 900-second window is short enough that it does not hold for a day-long one.
What it should do
Read every conversation, and take less time doing it, by asking the message index instead of walking the conversation list: two im +messages-search calls per poll over a 24-hour window. Coverage and cost trade places — the bounded path covers five conversations in tens of seconds, the search covers all of them in a few.
What the fix must satisfy
- Window.
FEISHU_LOOKBACK_SECONDSbecomes86400.FEISHU_MAX_CHATS, theim +chat-listcall, and the "the conversation list came back empty" guard all go. The wake path makes no per-conversation listing call at all. - Two queries, each carrying
--as user --no-reactions --page-all --json --start <ISO>:--at-chatter-ids <host>— the CLI's help states this "also matches messages that @ALL", which is what makes one query cover both mention forms;--chat-type p2p— direct messages, in which no mention is needed.
--page-allis not optional. Without it the CLI answers one page of 20 and reads nothing else — a truncation that looks exactly like a quiet inbox.--startis whole seconds. A stamp carrying microseconds is refused by the API.now - 24hwithmicrosecondcleared is the only form the condition sends, andstate["since"]is written the same way. The storedsincestays for readability; the window is alwaysnow - 24h, never a stored left edge.- Exactly two local exclusions:
sender_type: app(without it the task wakes itself on its own replies, in a loop) andsender == the host's own open_id. Do not re-test mentions locally — an@allmessage carriesmentions: null, so a local mention check would drop the very messages--at-chatter-idswas asked for, and a result returned by that query has already proved the mention. seenholds only ids that survived those exclusions, and the watermark is bounded by count, trimming in first-seen order rather than by string order (an id's lexical order says nothing about its age, so a lexical trim can drop the newest id and keep a stale one).- First run warms up. With no
seenkey in the state, the candidates are recorded and the answer isabsentwith a reason naming the warm-up. Otherwise the first poll after deployment wakes the task for 24 hours of history it has already handled — one such wake costs a full round. - An unreadable answer is
unknown, neverabsent:ok: false, a missingdata.messages, or a non-zero exit. Being unable to read the index is not the host having nothing to say. - The task prompt agrees.
emrg/server/feishu_prompt.mdcurrently tells the round to list conversations and then read each one (lines 48–52), and states that+messages-searchlags a send by seconds and so can never establish that nothing arrived (lines 57–60). Both need to match what the wake condition now does: search for the inbox sweep, and+chat-messages-list --order ascby position when the question is whether the host has replied. - Tests run through the injected runner only, never the real CLI: argv pinned, the
@all-with-null-mentions case, p2p stranger / p2p app / p2p host, de-duplication across two polls, the warm-up, the cap, and each failure shape answeringunknown.
Measured on this host (lark-cli 1.0.77)
| query | messages | wall clock |
|---|---|---|
A: --at-chatter-ids <host>, last 24 h |
7 | 1.87 s |
B: --chat-type p2p, last 24 h |
26 | 3.44 s |
| A ∪ B (0 ids overlap) | 33 | 5.31 s |
| after both exclusions (real candidates) | 19 | — |
- One of A's seven carries
mentions: nulland is still returned by--at-chatter-ids— the@allcase the local mention check would have discarded. --start 2026-10-28T11:03:20+08:00is accepted; the same instant with microseconds (...:20.123456+08:00) is refused withok: false,type: validation,subtype: invalid_parameters, exit status 1 — so both theok: falsetest and the non-zero-exit test in_cli_jsonalready turn it intoConditionUnavailable.- The live state file
~/.emrg/wake-state/feishu-inbox.jsonholds 336 ids inseenand asinceof2026-10-29T02:48:05.118290+00:00— the microseconds are in the file the condition writes today. im +chat-listoffers no--page-all(its flags are--page-size/--page-token), so nothing in the fix may reach for one there.
Activity
argszero commentedon Oct 29, 2026
Handled by #2632