Skip to content

fix(overseer): de-flood upstream PR notifications in the inbox (title + scoring) - #99

Open
heavygee wants to merge 3 commits into
feat/contrib-state-channel-ingestfrom
feat/contrib-state-inbox-title-priority
Open

fix(overseer): de-flood upstream PR notifications in the inbox (title + scoring)#99
heavygee wants to merge 3 commits into
feat/contrib-state-channel-ingestfrom
feat/contrib-state-inbox-title-priority

Conversation

@heavygee

Copy link
Copy Markdown
Owner

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):

  • 25 items had a bare PR URL as their whole title (e.g. https://github.com/tiann/hapi/pull/987).
  • Channel-sourced PR-babysit items (sourceKind: channel) shared the worker/system priority scale, so blocked/needs_decision PR 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 for tiann/hapi."

Root causes

  • Title: pickPrimaryArtifactTitle fell through to match.url when a github_pr artifactRef had no title (the contrib-state producer never emitted one).
  • Scoring: computeCoarseBasePriority(eventType) ignored sourceKind, so a channel blocked == worker blocked == 20. progress (pre-PR / CI-in-flight) wasn't handled → fell to the unknown default (70) + misleading QUESTION category.

Fixes (ingest + scoring only - NOT the converse layer)

  • Title synthesis (shared/overseerInbox.ts): renders tiann/hapi#987, or repo#num: <title> when a title is present, or parsed from the PR/issue URL. Never a bare URL.
  • sourceKind-aware priority (shared/overseerInbox.ts): channel items demoted below every worker/system item via CHANNEL_PRIORITY_OFFSET; progress gets a defined rank. Genuine blocked/needs_decision/failed workers now always rank above routine PR notifications; order within the channel band preserved. promoteAttentionEvent threads event.sourceKind.
  • Backfill (hub/store/inboxItems.ts): 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 (pr-emoji-core.sh + hapi-meta-daily.sh): 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.test.ts: channel demotion, progress rank, repo#number synthesis + 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: --title present/absent in artifactRef.
  • bun run typecheck:hub clean. (Pre-existing unrelated web/HappyThread.tsx typecheck 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

… + 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>
@heavygee

Copy link
Copy Markdown
Owner Author

Evidence: real production inbox (174 items, :3006) through this PR's code

The live "after" via the Overseer converse requires the soup rebuild (this session does not stack-switch/activate). As a faithful proxy, I ran the actual buildInboxTitle + computeCoarseBasePriority from this branch over the real 174-item inbox dumped from :3006.

Title fix

bare-URL titles BEFORE: 25  AFTER: 0
  https://github.com/tiann/hapi/pull/987   ->   tiann/hapi#987
  https://github.com/tiann/hapi/pull/958   ->   tiann/hapi#958
  https://github.com/tiann/hapi/pull/897   ->   tiann/hapi#897
  https://github.com/tiann/hapi/pull/847   ->   tiann/hapi#847
  ...

Triage order — top 12 of "what needs my attention"

--- BEFORE (current live) ---
   1. p20 [PR] https://github.com/tiann/hapi/pull/987
   2. p20 [PR] https://github.com/tiann/hapi/pull/958
   3. p20 [PR] https://github.com/tiann/hapi/pull/897
   4. p20 [PR] https://github.com/tiann/hapi/pull/847
   5. p20 [PR] https://github.com/tiann/hapi/pull/986
   6. p20      Peer: W1.8 acceptance (#24)
   7. p20 [PR] session external_refs + PR chip
   8. p20 [PR] https://github.com/tiann/hapi/pull/1108
   9. p20 [PR] https://github.com/tiann/hapi/pull/945
  10. p30      Syncthing (server + phones setup)
  11. p30      expenses
  12. p30      Peer: in-scope janus cattle until GO

--- AFTER (this PR + backfill) ---
   1. p20      PR #36 open with all W1.8 acceptance work; live share
   2. p30      Family Link ... (operator decision)
   3. p30      Note pad ready for Cursor Pro June ...
   4. p30      pool-in live via interim LV; bridges blocked ...
   ...
PR-notification items in top-12: BEFORE 8/12  ->  AFTER 0/12

Channel-sourced PR notifications are demoted below every worker/system item; the bare-URL wall is gone. Genuine operator items (peer acceptance, real decisions, worker states) surface first.

The live converse before/after will be captured once the meta bot rematerializes the soup with this layer.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +386 to +389
nextTitle = buildInboxTitleFromEvent(
row.artifact_refs ?? latest?.artifactRefs ?? null,
latest?.payloadJson ?? null,
row.summary

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +376 to +379
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +142 to +143
if (ref.repo?.trim() && typeof ref.number === 'number') {
return `${ref.repo.trim()}#${ref.number}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
@heavygee

Copy link
Copy Markdown
Owner Author

Follow-up: auto-dispose terminal inbox items (operator-greenlit)

Same de-flood theme, second lever. Operator (2026-07-31): a completed item is
"nothing more to do - the only relevance is that it happened" (context, not
attention). Live inbox was 73% FINALE (128/174 items) that never get disposed.

sweepDecayedTerminalItems(db, now, windowMs) (hub/src/store/inboxItems.ts):

  • FINALE (completed): auto-resolve once it has sat past FINALE_DECAY_WINDOW_MS (14d).
  • STALE: obsolete immediately regardless of age. Idle-silence detection was
    retired as noise (checkStaleSessions returns []; 0 stale events live), so
    any STALE rows are orphaned legacy. Confirmed on :3006.
  • Rows are retained as history - status leaves the active set, never deleted.
  • Idempotent (matches only active rows). Runs on Store init (immediate effect on
    the existing DB) + the 5s sync tick (live decay), alongside checkStaleSessions.

Tests: 2 new cases (decayed vs fresh vs non-terminal FINALE; STALE obsolete).
bun test hub/src/store/inboxItems.test.ts -> 12 pass. typecheck:hub clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread hub/src/store/inboxItems.ts Outdated
Comment on lines +435 to +437
`UPDATE inbox_items
SET status = 'obsoleted', resolved_at = ?, updated_at = ?
WHERE ${active} AND category = 'STALE'`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread hub/src/store/inboxItems.ts Outdated
Comment on lines +430 to +432
`UPDATE inbox_items
SET status = 'resolved', resolved_at = ?, updated_at = ?
WHERE ${active} AND category = 'FINALE' AND updated_at < ?`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

heavygee added a commit that referenced this pull request Jul 31, 2026
…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>
heavygee added a commit that referenced this pull request Jul 31, 2026
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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

heavygee added a commit that referenced this pull request Jul 31, 2026
…llision note (ensureOverseerInboxSchema union)

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

1 participant