fix(activity): #216 · source approval inbox from the uncapped PendingList#217
Merged
Conversation
…List The Activity "NEEDS YOU" inbox (badge count, triage band, selection, approve/deny, inline clear-signing review, and the meta-rail) drew its rows from `activity_feed`, which the daemon caps at the newest 200 records. In a long agent session (>200 records) an old still-`Pending` approval was shed from that feed, so it became not just uncounted but *unreachable* — unselectable and un-approvable — on the very surface built to triage it, and could lapse at its approval TTL unseen. Clean inbox/log split: the INBOX now reads the daemon's uncapped `pending_list()` (via new `Shell::inbox` + `inbox_pending()`), while the settled LOG keeps reading the capped `activity_feed()` (capping a log is correct). A waiting approval can no longer be hidden regardless of how large the log grows. - contract: `PendingRecord` gains `reason` + `timestamp_ms` (`#[serde(default)]`, additive — no spec_version bump) so the inbox card renders the same over-cap chip the feed did. - signerd: `pending_list()` stamps them from `req.breached`/`req.created_ms`; new in-process test pins that the inbox is uncapped past 200 records. - app: `refresh_activity` dual-fetches inbox + log under one epoch guard; band/selection/approve/deny/review/rail/count all re-sourced from the uncapped inbox; `activity_feed`'s 200-cap left unchanged. The no-blind-approve and wrong-deny-race invariants are preserved. Closes #216
This was referenced Jul 8, 2026
…een_pending After #216 the on-Activity count comes from the uncapped inbox, not the feed, so the old param name was misleading (the doc had to apologize for it). Pure rename in a private helper; callers pass positionally.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The Activity "NEEDS YOU" approval inbox — the sidebar badge count, the triage band,
row selection / arrow-nav, inline approve/deny, the clear-signing review card, and the
meta-rail — drew its rows from
activity_feed(), which the daemon caps at the newest 200records (
ACTIVITY_FEED_CAP). A record's sort key (seq) is fixed at creation and never changes,expire_stale()only flips status in place, andself.requestsis only ever cleared wholesale onUnlock — so in a long agent session (>200 records) an old still-
Pendingapproval is shed from thefeed. It then becomes not merely uncounted but unreachable — unselectable and un-approvable —
on the very surface built to triage it, and can lapse at its approval TTL unseen.
pending_list()was already uncapped; the bug was that the app sourced its inbox from thecapped log instead of the uncapped inbox.
Fix — a clean inbox/log split
Two conceptually different things, two functions:
pending_list()via a newShell::inbox: Vec<PendingRecord>+inbox_pending()filter.activity_feed()— capping a log iscorrect, so the cap and
activity_feed()are unchanged.A waiting approval can no longer be hidden, uncounted, or made unreachable regardless of how large
the settled log grows.
Changes
rpc.rs,lib.rs):PendingRecordgainsreason: BreachedLimit+timestamp_ms: u64(
#[serde(default)], additive struct growth — nospec_versionbump, per wire-evolution rulefeat(contract): deckard-contract crate + virtual Cargo workspace (freeze-first wire) #3) so the inbox card renders the same over-cap chip the feed showed.
daemon.rs):pending_list()stamps the two fields from the samereq.breached/req.created_msthe feed already uses; new in-process test pins the inbox stays uncapped past200 records (fails if anyone re-caps it).
shell.rs,activity_view.rs):refresh_activitydual-fetches inbox + log under oneepoch guard; every approval consumer re-sourced from the uncapped inbox; new
activity_pending_row(&PendingRecord)renders the proposed row; the no-blind-approve andwrong-deny-race invariants are preserved verbatim across the
&ActivityRecord→&PendingRecordswap; lock/reset also clears
inbox(no phantom "N waiting" after lock).Verification
cargo fmt --all --checkcleanjust check— clippy-D warningson both default and--features traycargo test --workspace— 39 binaries, 0 failures; 7 new/renamed ActivityFeed 200-cap can drop a still-Proposed row → on-Activity "NEEDS YOU" band + badge undercount waiting approvals #216 tests incl. the daemonuncap guard and the two no-blind-approve invariant tests
Cargo.toml/Cargo.lockuntouched)no surviving approval path through the capped feed, invariants intact, dual-fetch fails safe
(partial error keeps both prior snapshots + shows the loud error line), daemon test non-vacuous,
wire change spec-compliant, render parity pixel-for-pixel.
Visual QA
This is a GUI change, but the new render path is a source swap that reproduces the prior
proposed-row visuals pixel-for-pixel (verified by the codex review: same amber human signal,
cite_phrase(reason)chip, "→ You waiting",⌘⏎/xhint). A live before/after could not be drivenfrom the agent shell — on macOS 14+ an agent-launched GPUI app never becomes the key app, so
synthetic unlock can't land (see
docs/dev/driving-the-app.md→ Honest caveats). To capture locally:just qa→scripts/deckard-drive.sh unlock deckard-qa→ navigate to Activity (⌘⇧A) →shot.Follow-ups (separate tickets, out of scope)
self.requestsgrowth + live-Pendingadmission — bound via retention/admission,never a feed-level cap (any cap on the pending subset re-opens this bug).
the same cap).
Closes #216