How the Activity feed is built — and where it falls short.
Six stages transform raw files and agent output into the ordered Activity feed you see in NowDashboard.
Three independent pools are merged into one list. Order matters: MEMORY → manual → discovered.
Scans every .md file line-by-line for date + status keyword pairs. Stalled project files (untouched 14+ days) auto-generate a stalled entry.
Threads added via "+ new thread." Stored in JSON, classified by due date if present, else open.
Agent output from "Scan now" — searches Linear, Notion, PostHog, Outlook/Teams, claude-output. Persisted in cache; never auto-deleted.
Four filters run in sequence. Any item caught by any filter is dropped before it reaches ranking.
resolutions.md. Permanently hidden — no expiry.postpones.json. Hidden until the snooze date passes.Item identity key: file:line|first-80-chars-of-text
The same work item can appear from multiple sources. mergeThreads runs two passes to collapse them:
file:line|text-prefix → duplicate dropped.AWS-456), Notion page IDs, Linear URLs. Any two items sharing a reference → only the first survives. MEMORY items come first, so hand-written annotations win over agent paraphrases.Each item gets a weight. Items are sorted by weight descending, then capped per kind.
days_overdue= max(0, −daysOff) — only positive for overdue items; 0 for everything elsesource_coeff= 1.0 for all sources (uniform; awaiting calibration data)pavel_mention+1 if text contains "pavel" (case-insensitive)status_keywordscount of distinct status words (due, blocked, awaiting, decision, …)escalate+1 if latest auto-check verdict is ESCALATEEach item is enriched with context before being sent to the frontend.
Items are displayed in kind order: overdue → today → upcoming → stalled → open. Within each kind, weight-sorted.
Each row shows: status dot · timing marker · source · verdict · hover actions (resolve, check, postpone) · w<N> debug badge
Grounded in research on Gmail Priority Inbox, PagerDuty AIOps, GitHub Notifications, and WSJF / Cost of Delay prioritization literature.
days_overdue = max(0, -daysOff) — urgency is zero for everything that isn't already overdue. An item due today, in 3 days, or in 14 days all score 0 urgency. Within the today, upcoming, and open kinds, ranking degrades to "how many status buzzwords does the text contain." Meanwhile a stalled project untouched for 45 days scores 45 — dominating over a Linear ticket that's 2 days overdue.
e^(-daysOff/τ)) — smooth urgency gradients that increase as a deadline approaches, without a hard cliff that only activates after it passes.
computeWeight uses none of it. The mtime field exists on stalled items but is never fed into the weight. Discovered items have lastSeenAt but it's used only for the binary stale/not-stale flag.recencyScore = 1 / (1 + daysSinceTouch) (or equivalent decay) as a signal in computeWeight. Feed mtime and lastSeenAt into it.
file:line|first-80-chars-of-text causes two classes of problems:line: 1 hardcoded, so any two items from the same source will collide if their text prefix matches.dedup_key independent of content. Gmail threads by conversation ID. Introduce a content-addressed hash or explicit stable key for discovered items — decouple identity from display text.
mergeThreads finds a duplicate (two items sharing AWS-456), it keeps whichever appears first. MEMORY-first is the right default. But within discovered items, order is non-deterministic (Object.values() of JSON). If the same Linear ticket appears from two discovery runs with different text, whichever was cached first wins — even if the newer entry has a better description or a useful URL in the location field.
lastSeenAt.
| Aspect | Today | Better |
|---|---|---|
| Macro sort | Kind hierarchy (overdue → today → upcoming → stalled → open) | ✓ Already good — matches production patterns |
| Suppression | 4-layer gating (resolved, postponed, CLOSE TTL, snooze fatigue) | ✓ Already strong — aligned with PagerDuty's approach |
| Intra-kind ranking | Weight formula dominated by days_overdue; today / upcoming score 0 |
Continuous urgency function (e.g., 14 − daysOff or exponential decay) |
| Recency | Not used in ranking at all | Time-since-last-touch as a signal in computeWeight |
| Identity | Text-prefix key — fragile to edits and paraphrasing | Content-addressed hash or explicit dedup_key for discovered items |
| Dedup | First-wins across sources | Most-recent-wins or richest-metadata-wins |
| Feedback loop | Actions recorded but don't influence future surfacing | Outcome capture → coefficient calibration (planned but not started) |
| User control | Single flat list, no filters | Filter chips for source, verdict, recency (GitHub model) |
| Scheduler throughput | 4 items / 4h — 45h to cycle through 45 items | Higher throughput or shorter interval |
| Discovery budget | Single prompt, single budget, source-blind | Per-source allocation or parallel sub-tasks |