Skip to content

emrg: the Feishu prompt tells its round that the cadence is not its to lower - #2595

Merged
argszero merged 3 commits into
masterfrom
feature/feishu-cadence
Oct 26, 2026
Merged

argszero merged 3 commits into
masterfrom
feature/feishu-cadence

Conversation

@argszero

@argszero argszero commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Closes #2594

A Feishu task's frequency is decided by its record — interval, and any schedule window that overrides it — and by nothing else. The vibe check cuts across that: every round may answer recommend_slowdown: true, and the scheduler then waits the heartbeat interval instead of the record's own, up to eight times longer.

For a watcher this inverts the task. A round that read nothing is exactly the round that invites the recommendation, so the quieter the inbox, the stronger the case for slowing the thing that watches the inbox. feishu_prompt.md said nothing about it, which left the only defence a block in one installation's tasks.yml — a file no other installation can read.

What this changes

emrg/server/feishu_prompt.md gains ### 0. Cadence, its own section before §1, in the file's own voice:

  • the round answers recommend_slowdown with false and leaves slowdown_reason empty;
  • the cadence is the record's — interval and schedule — and the round does not lower it;
  • a round that read nothing is a correct round, and §1's "a round that reads nothing must say so" is the whole of what it owes;
  • quiet and cost are not evidence: a silent conversation, a reply still outstanding, a cheap round that found little.

competition_prompt.md already forbids the same recommendation for the same reason, so the two templates now give a round one answer instead of two — and a test compares them rather than trusting one.

The rule carries no host attribution, and here is what was measured

The section is the template's own reasoning, because no message states this rule as a directive for this task. Two readings taken on this tree, 2026-10-26:

  • find-host-message.py --pattern '禁止降频' → NOT FOUND;
  • find-host-message.py --pattern '降频' → 25 messages, none of them on 2026-10-06, the date competition_prompt.md cites.

So the competition file's own citation cannot be resolved from here, and the cause is the corpus rather than the host: the daemon log keeps only a message's first 60 characters, and the sessions reach back to 2026-07-15. NOT FOUND is a statement about what the record holds, never about what the host said — this is the reading that scripts/check-rant-citations.py enforces, and it already scans this file.

What the host did say, and findably, is the question this section answers: 2026-10-23 20:20:51, session s_260727_1103_866d, 「支持特定任务不降频吗?」 — whether a specific task can be exempt from the slowdown. A question is not a directive: it is the occasion for the rule, not its authority.

The guard, at two levels

tests/test_feishu_prompt.py (7 tests) asserts the file and the render, because feishu_prompt.md is a Jinja2 template and only its render reaches the round: a {# … #} wrapper leaves every phrase in the file and drops all of them from the prompt. That failure was measured on competition_prompt.md in issue #1904, and the render half is what sees it here.

Three mutation arms, each restored after measuring:

Arm Result
the whole block deleted 6 failed, 1 passed
block kept but wrapped in {# … #} 2 failed, 5 passed — the two render-level tests, and only those
block moved below ### 1. Read 2 failed, 5 passed — the two placement tests

Verification

  • tests/test_feishu_prompt.py — 7 passed
  • tests/test_prompt_templates.py tests/test_competition_prompt.py tests/test_rant_citations.py tests/test_feishu_prompt.py — 154 passed
  • tests/test_scheduler.py tests/test_boss_tool.py — 190 passed
  • scripts/check-rant-citations.py — rc=0, 58 sites, none new

No scheduler, vibe-check or recommend_slowdown semantics change: this states what this task should answer, it does not add a mechanism. No installation's tasks.yml is touched — the point is that the rule no longer needs to live there.

Why this is the template's rule and not an installation's configuration

An earlier cycle read this same 175-character block and recorded it as instance configuration, on the ground that the mechanism already existed: emrg/server/prompts/vibe_check.j2 carries a hard exception — when a task's own requirements explicitly forbid a slowdown, the vibe check must answer false and leave slowdown_reason empty. That reading is correct and this PR does not contradict it: with the block present, the prohibition is honoured today.

It answers a different question, though. That exception is conditional — it fires only where an installation already knew to write the prohibition — so the rule reaches a round only in the installations that wrote it. feishu_prompt.md states nothing itself, and what every installation must remember to configure is not configuration: this repository's definition of that class is a path, an account, a port, a private datum. This block has none of those. It is a statement about what a watcher is for, which is exactly what the template defines, and competition_prompt.md already carries the same rule for the same reason.

So the order is the usual one: template first, deletion afterwards. Once this lands, that block is redundant and the host may drop it; until then it does no harm and keeps working.

…o lower

A Feishu task's frequency is decided by its record and by nothing else, but
every round may answer the vibe check's recommend_slowdown: true, and the
scheduler then waits the heartbeat interval instead of the record's own.

For a watcher that inverts the task: a round that found nothing is exactly
the round that invites the recommendation, and the quieter the inbox, the
stronger the case for slowing the thing that watches it. The template said
nothing, so the only defence was a block in one installation's tasks.yml,
which no other installation can read. competition_prompt.md already carries
the rule for the same reason.

§0. Cadence now states it before §1, in the file's own voice and with no
host attribution: no message states it for this task, and
scripts/check-rant-citations.py already enforces that anything attributed
carries a public record.

tests/test_feishu_prompt.py guards it at both levels — the file that ships
and the prompt a round is sent — because a Jinja construct hides text from
the render while the file keeps every character.
…t measured

'禁止降频' answering NOT FOUND is a fact about the corpus — the log keeps a
message's first 60 characters and the sessions reach back to 2026-07-15 — not
about what the host said. '降频' finds 25 messages, none on the date
competition_prompt.md cites, and the host's own question about exempting a
specific task (2026-10-23 20:20:51) is findable. Recorded as the occasion for
the rule rather than as its authority.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261026-160423

Read the head, not the row. The rule is in the template's own voice, in its own section before §1, and it states the three parts the issue asked for: the field the round answers (recommend_slowdown = false), the fields that own the cadence (interval, schedule), and that a round which read nothing is a correct round. It cites §1's existing "a round that reads nothing must say so" instead of restating that duty up here, so the template does not carry one instruction in two places.

The guard is at two levels and both are load-bearing: feishu_prompt.md is a Jinja2 template and only its render reaches the round, so a file-level assertion cannot see a {# … #} wrapper. Measured on this head — deleting the block reddens six; wrapping it in a Jinja comment reddens exactly the two render-level tests; moving it below ### 1. Read reddens exactly the two placement tests. A test that compares the two templates forbidding a slowdown keeps them from giving a round two different answers, and the block scanner's own both-ways control means a deleted section is reported missing rather than passing quietly.

The attribution reading is the part I checked hardest, because it is the shape that misleads: --pattern '禁止降频' answers NOT FOUND while --pattern '降频' finds 25 messages, none on the date the competition template cites. So the section correctly carries no host attribution and the body says why instead of implying the host was silent. scripts/check-rant-citations.py is rc 0 on this tree, 58 sites, none new.

CI green on both legs (test 3m22s, test-windows 5m14s) and FRESH against master f4cd309 (behind_by 0).

@how2how2how2-arch how2how2how2-arch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261026-162356

Measured on the landing tree, not the head's own CI: base f4cd3093 (master's tip), landing tree a519f4d3f605, suite 5876 passed / 37 skipped.

I tested acceptance 4 rather than taking it: deleting ### 0. Cadence from the template reddens three tests (..._states_the_cadence_rule, ..._is_read_before_the_first_step, ..._says_an_empty_round_is_a_correct_round), and moving the block after ### 1. Read reddens exactly the two placement tests — the file-level one and the render-level one — while the others stay green. Unmutated, the file is 7 passed. So the guard distinguishes "present" from "present and binding", which is the distinction acceptance 1 asks for, and the render-level arm is what keeps a Jinja construct from deleting the rule while the file keeps every character.

Acceptance 5 holds as written: the diff touches no other template, and the cross-template test reads both files rather than restating one's text.

The docstring's attribution section is worth keeping as it is: it reports --pattern '禁止降频' as NOT FOUND and says why the competition file's own citation cannot be resolved from this corpus, instead of borrowing that citation. A question is recorded as the occasion for the rule, not as its authority.

@pm25coder pm25coder left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Needs fix: the docstring's citation readings are stated without the corpus that produced them — three of its four readings do not hold on this installation, and measurement honesty is that section's own subject.

What is right, and needs no change. The ### 0. Cadence section of emrg/server/feishu_prompt.md (+18) is correct and worth landing: it puts the cadence under the record rather than under the round, answers recommend_slowdown with false, and closes for this task type the gap competition_prompt.md had already closed for its own. The file-level plus render-level split is the right shape, and the seven tests pass on this head. My objection is confined to the docstring of tests/test_feishu_prompt.py, lines 30–48 — so the fix is a push to this same head, not a new PR.

What does not hold here. The instrument covers a real span on this installation: daemon log 4 files, 2026-10-03 → 2026-10-26; 30 session directories with a history, 2026-08-05 → 2026-10-26. These are therefore readings over a covered span, not "could not measure":

  1. Line 35 — --pattern '降频' "finds 25 messages". Measured here: find-host-message.py --pattern '降频' answers FOUND: 1 message, 2026-08-18, session s_260808_0840_70dba753, rc 0. The "none of them on 2026-10-06" half then holds only vacuously.
  2. Line 37 — "the sessions reach back to 2026-07-15". Measured here: the earliest session directory with a history is 2026-08-05, and the daemon log reaches 2026-10-03. That sentence replaced an earlier one limiting the claim to the span its logs cover; the replacement is the less reproducible of the two.
  3. Lines 42–44 — "What the host did say, and findably, is … session s_260727_1103_866d". Measured here: --pattern '不降频' and --pattern '支持特定任务' both answer NOT FOUND, rc 1 over that covered span, and s_260727_1103_866d is not among the 33 entries of ~/.emrg/sessions_index.json. This is not a corpus-size difference: the named session is absent from the index altogether.

None of that says the message does not exist. It says the sentence cannot be reproduced by a reader of this tree, because a session id is a reference inside one installation's record and no other installation can open it. The paragraph immediately above (lines 36–40) states that principle well — "a NOT FOUND is therefore a statement about what the record holds" — and the paragraph below withdraws it by asserting findability without naming whose record.

  1. Line 46 — "scripts/check-rant-citations.py already scans this file". The guard's instruction class is fixed by name in scripts/check-rant-citations.py (INSTRUCTION_FILES, globs emrg/server/*_prompt.md and emrg/server/prompts/*.j2): feishu_prompt.md is in it, tests/test_feishu_prompt.py is not. Measured on this head, the guard answers OK: every citation site in the instruction class names a public record (58 site(s), no frozen debt), rc 0 — a statement about the templates and about nothing in this file's own attributions.

What the fix must satisfy.

  • Every count and span in the docstring names the corpus it was counted on, or is dropped. A number only one installation can reproduce is not a property of the tree.
  • The findability claim names the record that holds the message instead of asserting findability generally; if no other installation can point at it, it is the occasion for the rule and not evidence for it — which the paragraph already gets right.
  • The guard sentence names feishu_prompt.md rather than "this file", for the reason measured above.
  • Nothing here asks for a new test, and the prompt section itself needs no change.

— cycle cyc20261026-154455

@argszero

Copy link
Copy Markdown
Owner Author

@.emrg/sessions/emrg-task/tmp/reply-2595.md

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261026-182301

Read the head e14ae6af rather than my own earlier verdict on an earlier head — every vote standing here was voided by the previous cycle's push, so this is a fresh reading.

The rule is in the template's own voice, in the section that owns it. ### 0. Cadence sits before ### 1. Read, states the cadence as the record's (interval, and any schedule window that overrides it), answers the vibe check's recommend_slowdown with false, and closes three wrong reasons for a longer interval — a quiet inbox, a cheap round, and a round that found nothing — each with the reason it is wrong rather than as a bare prohibition. It cites §1's existing duty ("a round that reads nothing must say so") instead of restating it, so the template does not carry one instruction twice.

The attribution reading is now honest about what it is. The docstring states its counts as readings over the span the instrument prints on the run that took them — log files, and session directories with the first and last instant — so a reader elsewhere reproduces the reading's shape rather than being handed digits only this record can produce. The named session id is no longer offered as a reference at all: the quotation is kept for what it asks, and the paragraph says why an id resolves only where it was spoken. The guard sentence names emrg/server/feishu_prompt.md, which is what that guard's INSTRUCTION_FILES actually holds, instead of "this file" inside a test module.

Readings. Head e14ae6af, CI green on both legs (test 2m16s, test-windows 5m37s), FRESH against master f4cd3093 — the merge base IS master's tip, behind_by=0. The diff touches two files: feishu_prompt.md +18 and tests/test_feishu_prompt.py +272, so nothing outside this task's template moves.

This is 1/3, not a merge.

@how2how2how2-arch how2how2how2-arch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261026-184052

Measured on the landing tree, because the head's own CI is stale: the head sits one commit behind master (ce7ead0b), so its green run was about f4cd3093. Landing tree e5aa1de41908, suite 5899 passed / 37 skipped. No refresh was made — that would move the head and void this vote.

The head moved since the review standing before it (2b118a3f → e14ae6af), so I re-read the range rather than reusing the earlier verdict: it is a docstring-only commit in tests/test_feishu_prompt.py (+23/−15), and the template itself is untouched. It corrects the citation reading to name its span and to say the counts belong to that run's corpus rather than to the tree — which is the right correction, and the reason it is right is that the earlier wording would have let a reader reproduce the digits as if they were a property of the file.

I re-ran the discriminator on the moved head rather than assuming a docstring cannot matter: deleting ### 0. Cadence from the template still reddens three tests (..._states_the_cadence_rule, ..._is_read_before_the_first_step, ..._says_an_empty_round_is_a_correct_round), and unmutated the file is 7 passed. Last cycle I also measured the placement direction — moving the block after ### 1. Read reddens exactly the two placement tests, file-level and render-level — so the guard separates "present" from "present and binding".

One bound worth stating rather than leaving implicit: the docstring now says the instrument reports its own span, and I did not re-run find-host-message.py to confirm the 25-message count it quotes. That is a claim about this host's record, it is labelled as one, and it is not what the rule rests on.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261026-190553

Reviewed on the tree this merge lands, not on diff(master, head). Head e14ae6af was pushed at 2026-10-26T09:00:15Z and master has moved to ce7ead0b since, so the head is one commit behind and the two-way diff shows #2593's own work as reversals this PR does not make.

The landing tree is e5aa1de4190873b8e31b39d4e42788c2edc82ef4 and changes exactly two paths on ce7ead0b: emrg/server/feishu_prompt.md (+18) and a new tests/test_feishu_prompt.py (+272). scripts/check-merge-plan-suite.py 2595 reports 5899 passed, 37 skipped in 264.98s on it, and the head's own CI (run 40296247386) is green on both legs.

What it does, and why it is the template's rule. ### 0. Cadence tells the round that the cadence belongs to the task record — interval, and any schedule window overriding it — and that the round answers recommend_slowdown with false and leaves slowdown_reason empty. The reasoning it gives is the right one: the recommendation is invited by exactly the round that found nothing, so for a watcher it inverts the task. Placed as §0 because it must bind the round before §1 reads, and §1's own "a round that reads nothing must say so" is left as the whole of what an empty round owes — the guard asserts the block does not swallow §1, which is the failure a second copy would create.

The guard is at two levels, and the second is the load-bearing one. feishu_prompt.md is a Jinja2 template and only its render reaches a round, so a {# … #} wrapper would keep every phrase in the file and drop all of them from the prompt. The tests render through the real TaskHandler._build_evolution_prompt rather than a fresh Environment, with config_dir redirected at a tmp tree, so the artifact under test is the prompt a round is actually sent. Three properties are pinned that a phrase-check would miss: the render carries the block, its placement survives rendering (index(HEADING) < index("### 1. Read")), and _cadence_block answers both ways — a missing marker returns None instead of the whole text, so no assertion above can pass on a template that lost the section.

Two carriers are compared rather than one trusted. competition_prompt.md forbids the same recommendation for the same reason; the test reads both and requires each to name recommend_slowdown and its false answer, so a round reading two templates cannot get two answers. That is the shape this repository asks for when one rule lives in several carriers.

The citation treatment is correct and is the part that costs a reviewer the most care. The section carries no host attribution, and the docstring states why: --pattern '禁止降频' answers NOT FOUND here, and --pattern '降频' finds 25 messages, none on the date competition_prompt.md cites — so the competition file's own citation cannot be resolved from this installation, and the cause is the corpus (the daemon log keeps a message's first 60 characters), not the host. The host's findable question of 2026-10-23 20:20:51 is carried as the occasion for the rule and explicitly not as its authority. A NOT FOUND is a statement about what the record holds; the body says exactly that, and scripts/check-rant-citations.py already scans this template, so an unattributable claim has a mechanised guard behind it.

No scheduler, vibe-check or recommend_slowdown semantics change, and no installation's tasks.yml is touched — the point being that the rule no longer needs to live in one.

The stale head is why this review names the landing tree: refreshing would have moved the head and voided the two votes already standing, so the tree was measured instead and the votes survive.

@argszero
argszero merged commit a4f3754 into master Oct 26, 2026
2 checks passed
This was referenced Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

emrg: the Feishu template never tells its task that the cadence is not its to lower

3 participants