-
Notifications
You must be signed in to change notification settings - Fork 0
fix(overseer): de-flood upstream PR notifications in the inbox (title + scoring) #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/contrib-state-channel-ingest
Are you sure you want to change the base?
Changes from all commits
3933a10
e6150ad
72aaab1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -218,7 +218,7 @@ export function promoteAttentionEvent( | |
|
|
||
| const now = Date.now() | ||
| const category = mapEventTypeToInboxCategory(event.eventType) | ||
| const basePriority = computeCoarseBasePriority(event.eventType) | ||
| const basePriority = computeCoarseBasePriority(event.eventType, event.sourceKind) | ||
| const title = buildInboxTitleFromEvent(event.artifactRefs, event.payloadJson, event.summary) | ||
| const suggestedAction = extractSuggestedAction(event.payloadJson) | ||
| const existing = findActiveInboxItemForSession(db, event.relatedSessionId) | ||
|
|
@@ -347,6 +347,100 @@ function extractSuggestedAction(payloadJson: string | null): string | null { | |
| return null | ||
| } | ||
|
|
||
| /** | ||
| * Idempotent backfill — re-derive `title` + `base_priority`/`priority` for | ||
| * existing inbox rows from their latest source event. Deterministic (pure | ||
| * functions of the event + artifact refs) and touches only title/priority | ||
| * (never status/resolution/feedback), so it is safe to run on every boot. | ||
| * | ||
| * Repairs rows promoted before the title-synthesis + channel-priority-band | ||
| * changes (bare-PR-URL titles at worker-band priority) without waiting for the | ||
| * next PR transition to re-promote them. A no-op once every row already matches. | ||
| */ | ||
| export function backfillInboxDerivedFields(db: Database): void { | ||
| const rows = db.prepare( | ||
| 'SELECT id, title, base_priority, artifact_refs, summary, source_event_ids FROM inbox_items' | ||
| ).all() as Array<{ | ||
| id: number | ||
| title: string | ||
| base_priority: number | ||
| artifact_refs: string | null | ||
| summary: string | ||
| source_event_ids: string | null | ||
| }> | ||
| if (rows.length === 0) return | ||
|
|
||
| const update = db.prepare( | ||
| 'UPDATE inbox_items SET title = ?, base_priority = ?, priority = ? WHERE id = ?' | ||
| ) | ||
| 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 | ||
|
Comment on lines
+376
to
+379
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On long-lived installations, every historical inbox row is retained, including resolved items, but Useful? React with 👍 / 👎. |
||
|
|
||
| // Only recompute the title when we have material to derive it from, | ||
| // so a row with a good session-name title and a since-deleted event | ||
| // is never regressed to its summary. | ||
| let nextTitle = row.title | ||
| if (latest || row.artifact_refs) { | ||
| nextTitle = buildInboxTitleFromEvent( | ||
| row.artifact_refs ?? latest?.artifactRefs ?? null, | ||
| latest?.payloadJson ?? null, | ||
| row.summary | ||
|
Comment on lines
+386
to
+389
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an earlier event supplied artifact refs but the latest event for the same active item does not, promotion preserves the old Useful? React with 👍 / 👎. |
||
| ) | ||
| } | ||
| const nextPriority = latest | ||
| ? computeCoarseBasePriority(latest.eventType, latest.sourceKind) | ||
| : row.base_priority | ||
|
|
||
| if (nextTitle === row.title && nextPriority === row.base_priority) continue | ||
| update.run(nextTitle, nextPriority, nextPriority, row.id) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * How long a terminal (FINALE / completed) item stays on the active attention | ||
| * surface before it auto-resolves. A completed item is "nothing more to do — | ||
| * the only relevance is that it happened" (operator, 2026-07-31), so it is | ||
| * context, not attention. Keep it visible briefly, then get it out of the way. | ||
| */ | ||
| export const FINALE_DECAY_WINDOW_MS = 14 * 24 * 60 * 60 * 1000 | ||
|
|
||
| /** | ||
| * Auto-resolve decayed terminal (completed) inbox items so a backlog of finished | ||
| * work stops crowding the operator's attention surface. Rows are RETAINED as | ||
| * history — status leaves the active set, never deleted. | ||
| * | ||
| * Only FINALE (completed) is swept, and only once it has sat past the decay | ||
| * window. | ||
| * | ||
| * STALE is deliberately NOT swept here. The hub-inferred "No agent output for N | ||
| * minutes" silence detection was retired (see | ||
| * `OverseerEventRecorder.checkStaleSessions`, now returns []; last such event on | ||
| * the live DB was 2026-07-17) — but a worker that self-reports status:"stalled" | ||
| * via AGENT_NOTIFY_SUMMARY ALSO lands as `event_type='stale'` → category STALE, | ||
| * and that is a live, operator-relevant signal (observed as recently as 3 days | ||
| * before this was written). Blanket-obsoleting STALE would eat those self- | ||
| * reports. Historical hub-inferred STALE cruft is a separate operator-approved | ||
| * one-shot, not this live sweep. | ||
| * | ||
| * Idempotent: matches only active rows, so re-running changes nothing once swept. | ||
| * Returns the number of rows resolved. | ||
| */ | ||
| export function sweepDecayedTerminalItems( | ||
| db: Database, | ||
| now: number = Date.now(), | ||
| windowMs: number = FINALE_DECAY_WINDOW_MS | ||
| ): number { | ||
| const result = db.prepare( | ||
| `UPDATE inbox_items | ||
| SET status = 'resolved', resolved_at = ?, updated_at = ? | ||
| WHERE status IN ('new', 'surfaced', 'deferred', 'snoozed') | ||
| AND category = 'FINALE' AND updated_at < ?` | ||
| ).run(now, now, now - windowMs) | ||
| return result.changes | ||
| } | ||
|
|
||
| /** | ||
| * Idempotent Overseer inbox DDL — runs on every Store init, NOT gated on SCHEMA_VERSION. | ||
| */ | ||
|
|
@@ -399,6 +493,9 @@ export function ensureOverseerInboxSchema(db: Database): void { | |
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_inbox_operator_actions_item ON inbox_operator_actions(inbox_item_id, created_at DESC); | ||
| `) | ||
|
|
||
| backfillInboxDerivedFields(db) | ||
| sweepDecayedTerminalItems(db) | ||
| } | ||
|
|
||
| export function dropOverseerInboxSchema(db: Database): void { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -564,6 +564,9 @@ export class SyncEngine { | |
| // Piggybacked on the inactivity tick; not a logical part of expireInactive | ||
| // but shares its 5s cadence (avoids a second timer). | ||
| 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On every running hub, the inactivity timer invokes this branch every 5 seconds ( Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| private reloadAll(): void { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a channel notification is bound to a session that already has an active worker item,
findActiveInboxItemForSessionreturns 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 👍 / 👎.