Repository navigation
fix(inbox): repeat pending Mail notices through the incremental projection - #165
Conversation
…ot acted on A Mail notice is one-shot: once issued, it stands until every Mail it named is read, answered or itself delivered, and until then no other notice goes to that Actor, also not for Mail that arrives later. Delivery of the notice proves nothing: a notice typed into a busy terminal is accepted and can still be lost. In one of our groups an Actor's notice was accepted, more Mail arrived for it over the following hours, and the Actor heard nothing until it happened to read its Inbox. The new group delivery setting `mail_notice_repeat_seconds` bounds how long a notice stands. Above zero, Mail still waiting that long after its last notice is announced again, and the new notice names everything then waiting, including Mail that arrived after the first one. A read, a reply or the Mail's own delivery still ends a notice early, exactly as before. The default, 0, keeps today's single notice.
|
The missed-notice scenario is worth addressing, and keeping repeats opt-in is sensible. This version needs to be rebased and implemented in the current production path before it can land. After #151, Please exercise the actual daemon notice/delivery path: accepted-but-unread notice, later Mail, no repeat before the interval, repeat containing still-pending Mail, and stop after read/reply/delivery of the Mail itself or Actor-generation change. Document that repeating a notice does not repeat the underlying task execution. Keep the current zero/default behavior unchanged. |
Main now finds due Mail notices with an incremental projection (inbox/notices.rs) and keeps the ordered reducer only as a test-time cross-check. mail_notice_repeat_seconds moves into the projection: each Mail notice claim keeps its time, and a claim older than the setting no longer stands, so Mail still waiting is announced again.
|
Hi Chester, some context on today's burst of PRs, and a thank-you. For a while we've been running our own workflow plugin on top of CCCC, called cwf: boards, task routing, notices, health checks. Over time it grew into one huge binary (around 100 MB) that I was maintaining alongside CCCC itself. I'd had enough of that, so I decided to split it. Whatever is really a CCCC bug or a CCCC primitive goes upstream to you, and our policy stays in a much smaller plugin on our side. Today's batch was the first big slice of that split, which is why so much landed at once. Thank you for how you handled it. You merged thirteen of our PRs in a day, including #158, and re-validated #158 yourself. Your reviews on the rest found real problems: output activity isn't proof of work, an absent endpoint isn't an exit, recording a decision isn't finishing the work, and several of ours were written against the reducer that #151 had already made test-only. Those are fair calls. That kind of review makes CCCC better than any fork of it could be. What we'll do with them:
We'll also slow down: smaller pieces, one at a time. We love CCCC. It runs our agents every day, and it is the reason any of this was possible. Thank you for building it, and for the care you put into every review. |
Make synchronous Snapshot decode accounting local to the test thread so unrelated parallel tests cannot contaminate the warm-query assertion. This removes its ignore requirement.
A never-read batch otherwise appends notices and retains claims without bound. Add delivery.mail_notice_repeat_max (default three), reconstruct its current-batch count from notice facts, and prune expired or withdrawn claims independently. Configuration changes rebuild the projection so pruned claims can be re-evaluated. Captured-prefix and real-daemon tests cover exact exhaustion, new Mail, rebuilds and 64 intervals.
A runtime restart or new_session is not actor removal and re-addition. Exercise both through real daemon lifecycle and PTY delivery, and document that pending Mail and the repeat budget survive.
Delayed notice delivery does not postpone the repeat clock; state the consequence for actor-visible spacing explicitly.
Withdrawal already releases the batching claim without an interval wait. Document that rule alongside the new finite attempt budget.
Pruning compares notice generation against the cached Group document. Replacing an Actor under the same id and role must rebuild that document; otherwise each new-generation claim and budget is immediately discarded. Include created_at in the routing fingerprint and extend the real-daemon cap test across warm-cache removal and re-addition.
The previous selection stops before a later-boundary read and cannot prove unread Mail at repeat. Preserve it through the read as a stop case, and use an unresolved history audited through the assertion horizon for repeats. Short-interval cap replay stays before later Mail arrives; restore the captured read boundary through the real cursor transaction.
A zero decoder counter must not make the warm-query check pass vacuously; first require the cold captured-history build to decode records.
Warm the configured one-repeat projection before lifecycle changes. Require the remaining repeat after restart/new_session and silence after the next interval, which a reset budget cannot satisfy.
Partial resolution changes the pending set and therefore starts a new batch budget; this is not a lifetime cap per individual Mail.
A persistently busy peer can exhaust its finite attempt budget without receiving a notice. State that tradeoff rather than implying eventual delivery.
Full-workspace clippy reports items_after_test_module for this cfg(test)-only helper. Relocate it without changing any production item or test behavior.
The whole-daemon lifecycle case also passes before the generation fingerprint fix because intervening response reads can rebuild the empty-Actor cache. Keep it as end-to-end coverage and add a direct production-projection case with actual core Actor operations and replayed captured Mail payload. Its exhausted new-generation budget must survive pruning.
The initial projection scenario also passed on the pre-fix source: subsequent ledger changes may independently rebuild it. Add a cfg(test)-only read of the actual Group consumed by pruning and assert its generation immediately after the core Actor replacement, before any append. Keep the cap assertion and all runtime items unchanged.
|
CI note: on current |
A notice can be accepted by a terminal while its Mail remains unread. With
delivery.mail_notice_repeat_seconds > 0, a later unread pass can issue afresh notice for all still-pending Mail, including later arrivals. Omitted
or zero preserves single-notice behavior. Read, reply, delivery of the Mail
itself, or an Actor-generation change ends the corresponding eligibility.
Repeating a notice does not repeat the underlying task execution, promote
Mail, advance its Inbox cursor, or create another Mail obligation.
How we built it. The policy lives in
inbox::notices::NoticeState::due,called by the existing incremental
inbox::projection::duepath.automation::unread_noticesremains a test-only ordered differential oracle.No full-ledger replay is added to production ticks. Claims expire from the
notice's issue time, not its acceptance time; delayed delivery can therefore
make actor-visible notices arrive closer together than the configured interval.
delivery.mail_notice_repeat_maxdefaults to three repeats after a batch'sinitial notice. The cap is per pending batch: changing its ordered Mail IDs,
including partial resolution, starts a fresh budget. Expired and withdrawn
claims are pruned separately from the durable batch count. Withdrawal releases
the interval claim but still consumes an attempt: a peer that stays busy can
remain un-notified after all max+1 attempts are withdrawn. This finite policy
does not guarantee eventual notice delivery. The count survives projection
rebuilds and runtime restarts. A runtime restart or
actor_new_sessionkeepsthe Actor generation and pending Mail; removal and re-addition changes it.
The cache fingerprint includes that generation before pruning uses its Group.
Maintainer question. Is three the desired bounded default for
delivery.mail_notice_repeat_maxwhen repetition is enabled? This makes thecontext and ledger limit explicit; you can change or drop that default.
How we tested it. Two redacted captured protocol histories distinguish
an accepted-but-unread notice with later Mail from a read whose later boundary
closes both earlier Mail before the repeat. The private completeness audit
runs through the complete assertion horizon, plus the next interval where
needed, rather than ending at the last selected event. The repeat source also
has a 66-hour closing-fact audit. Public READMEs state what each history shows;
IDs, times, order and protocol relationships remain, while bodies use nonempty
redaction markers. No source bodies or private provenance are published.
Core cases compare the production projection with the ordered oracle, check
zero/default and pre-interval behavior, exact configured/default caps, changed
batches, bounded claims over 64 short intervals, and rebuilds. The normal-CI
warm check first requires positive cold decoding, then no decoder growth.
A generation case observes the cached Group before an append can independently
refresh it. Additional notices and lifecycle operations are explicitly
controlled replay actions, not claimed as captured historical records.
Daemon cases use the production unread path and actual PTY acceptance. Each
read/reply/manual-Mail-delivery/generation stop case first proves the repeat
works. Restart and new-session cases require both their remaining repeat and
silence after budget exhaustion. A foreground daemon probe also exercises
Unix-socket RPC, the unchanged periodic scheduler, both pending Mail, a later
read, and normal shutdown. Its reports bind source, binary, fixture and probe
hashes.
The merge-only head has the same tree as the reviewed v4 head. Fresh CI at
this merge head passes every job, including
Rust formatting, workspace clippy with
-D warnings, all 2,737 workspacetests selected by the CI profile, serial lifecycle checks, offline native
sessions and Windows smoke. The profile excludes 53 tests by design.
The full stock reference passes 2,724 workspace tests and fails exactly the
13 new missing-feature cases; no other workspace test fails. The dedicated
pre-fix reference fails only the cached-generation assertion. Independent
source, capture-completeness and merge-delta reviews passed.
The probe run requires all eight core and nine daemon cases on the candidate,
exactly six core and seven daemon missing-feature failures on the stock
reference, and exactly the generation-observation case failing on the pre-fix
3eab6a1creference while all other cases pass. Compilation or setup failuresare rejected as negative evidence.
Tested on: GitHub-hosted
ubuntu-26.04(x86_64 Linux), Rust 1.88.0.The full CI also passes on its GitHub-hosted Windows smoke runner.