Skip to content

fix(activity): #216 · source approval inbox from the uncapped PendingList#217

Merged
hellno merged 2 commits into
mainfrom
hellno/explore-issue-216-bug
Jul 8, 2026
Merged

fix(activity): #216 · source approval inbox from the uncapped PendingList#217
hellno merged 2 commits into
mainfrom
hellno/explore-issue-216-bug

Conversation

@hellno

@hellno hellno commented Jul 8, 2026

Copy link
Copy Markdown
Owner

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 200
records (ACTIVITY_FEED_CAP). A record's sort key (seq) is fixed at creation and never changes,
expire_stale() only flips status in place, and self.requests is only ever cleared wholesale on
Unlock — so in a long agent session (>200 records) an old still-Pending approval is shed from the
feed. 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 the
capped log instead of the uncapped inbox.

Fix — a clean inbox/log split

Two conceptually different things, two functions:

  • INBOX (badge + band + selection + approve/deny + review + rail) now reads the uncapped
    pending_list() via a new Shell::inbox: Vec<PendingRecord> + inbox_pending() filter.
  • LOG (day-grouped history) keeps reading the capped activity_feed() — capping a log is
    correct, 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

  • contract (rpc.rs, lib.rs): PendingRecord gains reason: BreachedLimit + timestamp_ms: u64
    (#[serde(default)], additive struct growth — no spec_version bump, per wire-evolution rule
    feat(contract): deckard-contract crate + virtual Cargo workspace (freeze-first wire) #3) so the inbox card renders the same over-cap chip the feed showed.
  • signerd (daemon.rs): pending_list() stamps the two fields from the same req.breached /
    req.created_ms the feed already uses; new in-process test pins the inbox stays uncapped past
    200 records
    (fails if anyone re-caps it).
  • app (shell.rs, activity_view.rs): refresh_activity dual-fetches inbox + log under one
    epoch guard; every approval consumer re-sourced from the uncapped inbox; new
    activity_pending_row(&PendingRecord) renders the proposed row; the no-blind-approve and
    wrong-deny-race invariants are preserved verbatim across the &ActivityRecord&PendingRecord
    swap; lock/reset also clears inbox (no phantom "N waiting" after lock).

Verification

  • cargo fmt --all --check clean
  • just check — clippy -D warnings on both default and --features tray
  • cargo test --workspace39 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 daemon
    uncap guard and the two no-blind-approve invariant tests
  • No dependency drift (Cargo.toml/Cargo.lock untouched)
  • Cross-model review (codex / GPT-5): GOAL MET across all dimensions — inbox uncapped end-to-end,
    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", ⌘⏎/x hint). A live before/after could not be driven
from 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 qascripts/deckard-drive.sh unlock deckard-qa → navigate to Activity (⌘⇧A) → shot.

Follow-ups (separate tickets, out of scope)

  1. Unbounded self.requests growth + live-Pending admission — bound via retention/admission,
    never a feed-level cap (any cap on the pending subset re-opens this bug).
  2. The same 200-cap also hides settled rows from the on-Activity LOG band (the remaining edge of
    the same cap).

Closes #216

…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
…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.
@hellno
hellno merged commit d81f08c into main Jul 8, 2026
5 checks passed
@hellno
hellno deleted the hellno/explore-issue-216-bug branch July 8, 2026 16:21
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.

ActivityFeed 200-cap can drop a still-Proposed row → on-Activity "NEEDS YOU" band + badge undercount waiting approvals

1 participant