Skip to content

emrg: the Feishu wake condition reads five conversations per poll, so an @ outside them never wakes the task #2631

Closed

Description

@argszero

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

  1. Window. FEISHU_LOOKBACK_SECONDS becomes 86400. FEISHU_MAX_CHATS, the im +chat-list call, and the "the conversation list came back empty" guard all go. The wake path makes no per-conversation listing call at all.
  2. 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.
  3. --page-all is not optional. Without it the CLI answers one page of 20 and reads nothing else — a truncation that looks exactly like a quiet inbox.
  4. --start is whole seconds. A stamp carrying microseconds is refused by the API. now - 24h with microsecond cleared is the only form the condition sends, and state["since"] is written the same way. The stored since stays for readability; the window is always now - 24h, never a stored left edge.
  5. Exactly two local exclusions: sender_type: app (without it the task wakes itself on its own replies, in a loop) and sender == the host's own open_id. Do not re-test mentions locally — an @all message carries mentions: null, so a local mention check would drop the very messages --at-chatter-ids was asked for, and a result returned by that query has already proved the mention.
  6. seen holds 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).
  7. First run warms up. With no seen key in the state, the candidates are recorded and the answer is absent with 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.
  8. An unreadable answer is unknown, never absent: ok: false, a missing data.messages, or a non-zero exit. Being unable to read the index is not the host having nothing to say.
  9. The task prompt agrees. emrg/server/feishu_prompt.md currently tells the round to list conversations and then read each one (lines 48–52), and states that +messages-search lags 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 asc by position when the question is whether the host has replied.
  10. 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 answering unknown.

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: null and is still returned by --at-chatter-ids — the @all case the local mention check would have discarded.
  • --start 2026-10-28T11:03:20+08:00 is accepted; the same instant with microseconds (...:20.123456+08:00) is refused with ok: false, type: validation, subtype: invalid_parameters, exit status 1 — so both the ok: false test and the non-zero-exit test in _cli_json already turn it into ConditionUnavailable.
  • The live state file ~/.emrg/wake-state/feishu-inbox.json holds 336 ids in seen and a since of 2026-10-29T02:48:05.118290+00:00 — the microseconds are in the file the condition writes today.
  • im +chat-list offers no --page-all (its flags are --page-size / --page-token), so nothing in the fix may reach for one there.

Activity

  1. argszero commented on Oct 29, 2026

    @argszero
    OwnerAuthor

    Handled by #2632

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions