fix(overseer): de-flood upstream PR notifications in the inbox (title + scoring) - #99
Conversation
… + scoring)
The operator inbox was dominated by upstream GitHub PR-notification items:
~25 of 174 items had a bare PR URL as their entire title, and channel-sourced
PR babysit items shared the worker/system priority scale so they occupied the
highest-priority tier, drowning genuine operator-attention items.
Ingest + scoring fixes (channel producer = contrib-state / meta-daily):
- Title: pickPrimaryArtifactTitle now synthesizes a human ref
("tiann#987", or "repo#num: <title>" when a title is present, or parsed
from the PR/issue URL) and never falls through to a bare https URL.
- Scoring: computeCoarseBasePriority is sourceKind-aware. Channel items
(external GitHub PR notifications) are demoted below every worker/system item
via CHANNEL_PRIORITY_OFFSET, and `progress` gets a defined rank instead of
the unknown default. Genuine blocked/needs_decision/failed workers now
always rank above routine PR notifications; order within the channel band
is preserved. promoteAttentionEvent threads event.sourceKind.
- Backfill: ensureOverseerInboxSchema re-derives title + base_priority for
existing rows from their latest source event (deterministic; title/priority
only, never status), so the live wall is repaired on next hub start.
- Producer: pec_build_channel_event_body gains --title (emitted into the
artifactRef only when set); wired at the notification site where the GitHub
subject title is in hand.
Tests: overseerInbox (channel demotion, progress rank, repo#number + no-bare-URL
regression), store (channel promotion + backfill repair), pr-emoji-core (--title).
Co-authored-by: Cursor <cursoragent@cursor.com>
Evidence: real production inbox (174 items,
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3933a10800
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const now = Date.now() | ||
| const category = mapEventTypeToInboxCategory(event.eventType) | ||
| const basePriority = computeCoarseBasePriority(event.eventType) | ||
| const basePriority = computeCoarseBasePriority(event.eventType, event.sourceKind) |
There was a problem hiding this comment.
Preserve worker priority when merging channel events
When a channel notification is bound to a session that already has an active worker item, findActiveInboxItemForSession returns that same item and the update replaces its worker priority with the new 100-offset channel priority. A routine PR notification can therefore push an unresolved worker blocker below every other worker item—the opposite of the stated ordering guarantee. Keep the most urgent priority among the item's source events or prevent channel events from merging into worker items.
Useful? React with 👍 / 👎.
| nextTitle = buildInboxTitleFromEvent( | ||
| row.artifact_refs ?? latest?.artifactRefs ?? null, | ||
| latest?.payloadJson ?? null, | ||
| row.summary |
There was a problem hiding this comment.
Derive backfilled titles from the latest event
When an earlier event supplied artifact refs but the latest event for the same active item does not, promotion preserves the old artifact_refs via COALESCE while correctly deriving the current title from the latest event's session payload. On the next hub start, this precedence selects those stale row-level refs ahead of the latest event's null refs and rewrites the title back to the old PR artifact. Use the latest event's artifact refs whenever that event exists, falling back to the row only when the source event is unavailable.
Useful? React with 👍 / 👎.
| for (const row of rows) { | ||
| const eventIds = parseIdArray(row.source_event_ids) | ||
| const latestId = eventIds.length > 0 ? Math.max(...eventIds) : null | ||
| const latest = latestId !== null ? getSystemEventById(db, latestId) : null |
There was a problem hiding this comment.
Avoid rescanning the entire inbox on every startup
On long-lived installations, every historical inbox row is retained, including resolved items, but ensureOverseerInboxSchema invokes this backfill on every hub start and this loop performs a separate event lookup for every row. Startup work therefore grows linearly with the unbounded audit history and incurs an N+1 query pattern even after all rows have already been repaired. Gate this as a one-time migration or target only legacy rows with a set-based query.
Useful? React with 👍 / 👎.
| if (ref.repo?.trim() && typeof ref.number === 'number') { | ||
| return `${ref.repo.trim()}#${ref.number}` |
There was a problem hiding this comment.
Validate artifact refs before calling string methods
When stored or worker-produced artifact JSON contains a non-string repo value, parseArtifactRefs accepts the entry because it only checks that it is an object, and this new call then throws on repo.trim(). Because the new startup backfill parses every historical inbox row, one legacy or malformed artifact can now abort hub initialization; validate the parsed shape with Zod or guard typeof ref.repo === 'string' before trimming it.
AGENTS.md reference: AGENTS.md:L61-L61
Useful? React with 👍 / 👎.
…ating A completed item is "nothing more to do - the only relevance is that it happened" (context, not attention). Let FINALE (completed) items decay off the active attention surface after a window, and immediately obsolete any lingering STALE rows (idle-silence detection was retired as noise; those are orphaned legacy). Rows are retained as history - status is moved out of the active set, never deleted. - sweepDecayedTerminalItems: resolve FINALE past FINALE_DECAY_WINDOW_MS (14d), obsolete STALE regardless of age. Idempotent. - Run on Store init (immediate effect on existing DB) and on the 5s sync tick (live decay) alongside checkStaleSessions. Co-authored-by: Cursor <cursoragent@cursor.com>
Follow-up: auto-dispose terminal inbox items (operator-greenlit)Same de-flood theme, second lever. Operator (2026-07-31): a completed item is
Tests: 2 new cases (decayed vs fresh vs non-terminal FINALE; STALE obsolete). |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e6150ad59a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| `UPDATE inbox_items | ||
| SET status = 'obsoleted', resolved_at = ?, updated_at = ? | ||
| WHERE ${active} AND category = 'STALE'` |
There was a problem hiding this comment.
Preserve worker-reported stalled items
Restrict this cleanup to legacy hub-inferred stale rows rather than every STALE item. A worker AGENT_NOTIFY_SUMMARY with status stalled is still mapped to a stale attention candidate (deriveAttentionCandidate returns 1), so it is promoted into the inbox and then this 5-second sweep immediately marks it obsolete, hiding a current worker request from the operator.
Useful? React with 👍 / 👎.
| `UPDATE inbox_items | ||
| SET status = 'resolved', resolved_at = ?, updated_at = ? | ||
| WHERE ${active} AND category = 'FINALE' AND updated_at < ?` |
There was a problem hiding this comment.
Keep completed items that still require operator action
Do not resolve every aged FINALE item solely from its category. Completed events enter the inbox specifically when a done notification includes a nonempty action—such as "Merge PR"—because deriveAttentionCandidate('done', action) returns 1; if that action remains outstanding for 14 days, this sweep silently marks it resolved even though the operator never completed or dismissed it.
Useful? React with 👍 / 👎.
…prompt (H1/H2/H4) Ingest-peer handoff (PR #99 -> converse/entity layer): - H2: two-level `detail: 'lean'|'full'` (default lean) on every context tool, threaded into projectToolResultForBrain(tool, result, detail). Closes the coverage gap — get_session_state, get_session_recent_output (raw terminal text capped at 280 chars in lean; was a token bomb), and get_worker_health (signal trail dropped in lean) now have lean projections. `full` returns raw rows, still bounded by limit/n + the outer char clamp. No token-budget engine. - H1: system-prompt rule — for a specific inbox item, pull explain_priority then query_events{sessionId} for that session's backlog as salience before answering. - H4: confirmed query_open_loops spans all non-deleted sessions (active AND archived); never filters on session.active. Documented in the method + doc. - H3: query_session_actions reader deferred (0 dispositions today). Tests: session-state/recent-output/worker-health lean projections, detail:full passthrough; pass-through test moved to explain_priority. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
… share it Worker self-reported status:"stalled" (via AGENT_NOTIFY_SUMMARY) lands as event_type 'stale' -> category STALE, same as the retired hub-inferred silence detection. P0.5 analysis of the live DB found these are alive (4 in the last 7d) while hub-inferred stale stopped 2026-07-17. Blanket-obsoleting category=STALE would eat live "I'm stalled" signals, so the sweep now resolves FINALE (completed) only. Historical hub-inferred STALE cruft is a separate operator-approved one-shot, not this live sweep. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 72aaab1bac
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| this.messageService.releaseMatureScheduledMessages(Date.now()) | ||
| // Terminal inbox items are context, not attention — auto-dispose the | ||
| // decayed ones so a backlog of finished work stops crowding triage. | ||
| this.store.inbox.sweepDecayedTerminal() |
There was a problem hiding this comment.
Move the terminal sweep off the five-second tick
On every running hub, the inactivity timer invokes this branch every 5 seconds (syncEngine.ts:186), so the 14-day decay policy executes 17,280 UPDATE statements per day. The query filters by category and updated_at, but the inbox index only covers (status, base_priority, created_at), forcing repeated scans of all active rows and unnecessary SQLite writer transactions even when nothing can expire. Run this sweep on a much coarser cadence or index/schedule it by the next decay deadline.
Useful? React with 👍 / 👎.
Problem (live evidence,
:3006, 2026-07-30)The operator's Overseer attention inbox was dominated by upstream GitHub PR-notification items. Measured live via
query_inbox(174 active items):https://github.com/tiann/hapi/pull/987).sourceKind: channel) shared the worker/system priority scale, soblocked/needs_decisionPR notifications sat at priority 20-30 - the highest-priority tier, above genuine operator items. The 27B Overseer, asked "what needs my attention?", reported the top-of-queue items were "mostly GitHub PRs fortiann/hapi."Root causes
pickPrimaryArtifactTitlefell through tomatch.urlwhen agithub_prartifactRef had notitle(the contrib-state producer never emitted one).computeCoarseBasePriority(eventType)ignoredsourceKind, so a channelblocked== workerblocked== 20.progress(pre-PR / CI-in-flight) wasn't handled → fell to the unknowndefault(70) + misleadingQUESTIONcategory.Fixes (ingest + scoring only - NOT the converse layer)
shared/overseerInbox.ts): renderstiann/hapi#987, orrepo#num: <title>when a title is present, or parsed from the PR/issue URL. Never a bare URL.shared/overseerInbox.ts): channel items demoted below every worker/system item viaCHANNEL_PRIORITY_OFFSET;progressgets a defined rank. Genuine blocked/needs_decision/failed workers now always rank above routine PR notifications; order within the channel band preserved.promoteAttentionEventthreadsevent.sourceKind.hub/store/inboxItems.ts):ensureOverseerInboxSchemare-derives title + base_priority for existing rows from their latest source event (deterministic; title/priority only, never status), so the live wall is repaired on next hub start.pr-emoji-core.sh+hapi-meta-daily.sh):pec_build_channel_event_bodygains--title, emitted into the artifactRef only when set; wired at the notification site where the GitHub subject title is in hand.Tests
overseerInbox.test.ts: channel demotion,progressrank,repo#numbersynthesis + no-bare-URL regression, URL parsing.inboxItems.test.ts: channel promotion (title + demoted priority + ordering) and backfill repair of legacy rows.pr-emoji-core.test.sh:--titlepresent/absent in artifactRef.bun run typecheck:hubclean. (Pre-existing unrelatedweb/HappyThread.tsxtypecheck error lives on the base layer, not this diff.)Delivery
Fork-only (overseer stack; NOT upstream). Stacked on
feat/contrib-state-channel-ingest. Soup: repoint that manifest layer to this branch; meta bot rematerializes (this session does not stack-switch/activate).Plan:
docs/plans/2026-07-30-overseer-inbox-pr-notif-title-and-scoring.md(mirror).Made with Cursor