Skip to content

emrg: a scheduled task can be woken early by a when condition #2592

Closed

Description

@argszero

What is observed

A scheduled task's cadence is fixed by its own record. emrg/server/scheduler.py documents the rule at the top of the module — name, type, enabled, interval are the common base fields, and schedule optionally overrides the cadence inside time-of-day windows. effective_interval is the single producer of "how often, right now", and it answers from the record alone.

So a task runs on its own clock whether or not there is anything to do, and two kinds of waste follow from the same cause:

  • A task that mostly has nothing to do still runs. A task polling an inbox every 15 minutes starts a full agent round each time to discover that nothing arrived. The events that actually matter arrive as pulses, and a fixed interval is the wrong instrument for a pulse.
  • A task that has something to do waits for its next slot. A task on a four-hour interval can be hours late for an item that appeared one minute after its last run.

Neither end is reachable from inside a task's own prompt: "should I run now" is knowledge the scheduler holds, not the agent.

What is wanted

A new optional field, when, on a task record in tasks.yml:

- name: some-inbox-task
  interval: 14400
  when:
    feishu: 300

- name: some-open-source-task
  interval: 14400
  when:
    rant: 600

when answers exactly one question: under what condition may this task be woken earlier than its own cadence? It is a mapping from a condition name to a positive number of seconds meaning "while this condition holds, run it again at most this often".

It does not interfere with interval or schedule. interval governs how often the task runs on its own initiative, schedule overrides that inside time windows, and when only ever wakes a task earlier. It never suppresses a run the task would have taken anyway. A task with no when behaves exactly as it does today.

Conditions are built in — a user writes no command, script or function name

Condition What holding means
rant The task's agent has a rant that is not yet completed
feishu Something on Feishu needs the host: a real @-mention of the host, an @-all, or a direct message to the host — any of the three counts

Defaults

# Item
1 tasks.yml accepts when: a mapping whose keys are condition names and whose values are positive integer seconds. An unknown condition name, a non-positive value, or a value below the task's own minimum are all refused with a readable error — never silently ignored
2 The built-in condition table has two entries, rant and feishu. Each is a read-only check answering holds / does not hold / cannot tell; cannot tell is treated as does not hold and logged, and never blocks another task
3 The rant condition reads this machine's rant records and asks whether the task's own agent has an entry whose status is not completed. The rant writer changes not at all — no "on rant write, trigger" path is added
4 The feishu condition queries through lark-cli as the user, and all three cases count as a hit. It maintains its own watermark of message ids and times under ~/.emrg/wake-state/<task name>.json, because Feishu message times are minute-granular and cannot serve as a cursor — de-duplication must be by message id
5 Waking uses the existing manual-trigger channel. The task itself is unchanged and does not know it was woken
6 A wake that arrives while the task is already running does not interrupt it and does not mark the condition read; the condition is evaluated again next time, so a message is never swallowed
7 The watch is polling — each condition is checked once per its own configured seconds. There is no second, event-push path
8 The field is documented wherever a task's fields are: both READMEs and the field reference in Agent.md
9 The GUI's task editor preserves when verbatim when saving. Editing another field must not drop it. This issue requires only that it survives round-tripping, not that the GUI edits it
10 schedule and when compose: schedule decides the natural rhythm, when only advances it. Neither overrides the other

Acceptance

# Done means
1 A new test: a task with no when behaves byte-for-byte as the current implementation
2 A new test: a holding condition wakes the task early, a non-holding one leaves it asleep on its original interval, and two conditions each count on their own seconds without affecting each other
3 A new test: all four bad inputs (unknown condition, 0, negative, below the minimum) are refused with a readable error
4 A new test: a condition that holds again while the task is running neither interrupts nor marks it read, and it still holds next round
5 A new test: the feishu condition de-duplicates by message id (one wake per id) and does not wake on cannot tell
6 A new test: after the GUI edits another field, when is still on the record
7 Mutation verification: deleting the trigger, making the condition always true, deleting the minimum check, and deleting the de-duplication each turn the corresponding test red
8 The repository's full suite passes and GitHub CI is green on all three platforms

What this must not do

  • It must not add a second scheduling path: no event push, no watcher process outside the scheduler's own poll loop.
  • It must not change how a rant is written, or add a trigger to the rant writer.
  • It must not let a task be woken so often that it burns more than it saves — the configured seconds are a floor on the gap, and that floor is enforced.

Activity

  1. argszero commented on Oct 26, 2026

    @argszero
    OwnerAuthor

    Handled by #2593

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