Bug Report Intake (part 2)
Part 2 of the Bug Report Intake page: the three channels Omniscio watches for you — a connected Gmail inbox, a Sentry organization and GitHub repositories — how to add, test and edit each source, what happens on every poll, and how a source that keeps failing raises a durable alert.
What it is
This is part 2 of the Bug Report Intake page. It covers the three channels Omniscio watches for you — a connected Gmail inbox, a Sentry organization, and GitHub repositories — how a source is added, tested and edited, what happens on each poll, and what the panel does when a source keeps failing.
Where to find it
All of it is configured in one place: the Sources tab of the Bug Intake panel in your projects sidebar. Add source opens a form where you choose the project the reports belong to and the source type — Sentry or GitHub — and then fill in the details that source needs.
Gmail has no source row at all: once your Gmail account is connected, tagged bug mail is picked up on its own. Each source row shows its Enabled and Auto-spawn switches, when it last polled, when it last succeeded, the last error it hit, and a Test connection button. The Pause all polling switch in the panel header suspends every source at once without losing each one's own setting.
How it behaves
Gmail intake
The [BUG: <slug>] / [FR: <slug>] convention also works on the user's connected Gmail account, not just the AgentMail inbox. A dedicated poller (src/main/services/email/gmail-bug-intake-poller.ts) checks the connected Gmail inbox every 60 seconds and feeds matching mail into the exact same transport-agnostic router the AgentMail path uses.
How it runs:
- Runs whenever Gmail is connected. The poller gates only on Gmail being authenticated: it is independent of the Automations setting (contrast with the automations-gated
gmail-automation-poller.ts). It stays inert until a project actually has a bug-intake slug configured, which is the real opt-in, exactly as on the AgentMail path: a[BUG:]mail with no matching slug lands unrouted, and ordinary mail is never touched. - Cheap subject pre-filter, then full fetch. Each tick searches the newest 20 inbox threads from the last seven days — so a report that arrived while Omniscio was closed is still picked up, but turning intake on never digs up months-old mail — and parses subjects from search metadata first; only a
[BUG: <slug>]/[FR: <slug>]match pays the full thread fetch, whose first message's real subject is then re-parsed authoritatively (rejectingRe:/Fwd:). - Same pipeline as AgentMail. A matched message goes through the same LLM prescreen, the same router with the 30-second per-slug cooldown and the per-project daily cap, and the same
BEGIN-USER-REPORT/END-USER-REPORTuntrusted-data fencing. Attachments ride the same 4-route pipeline, with the bytes fetched from the Gmail API. - Persistent dedup. Before any prescreen cost, the message id is checked against
bug_intake_processed, so an app restart (or the overlapping search window) can never reprocess an already-handled message.
When the spawned investigation finishes, a dedicated completion handler (src/main/services/email/gmail-bug-intake-completion.ts) emails the agent's findings back to the tester on the original Gmail thread, with the same protections as the AgentMail reply path: aside/sidechain messages are never leaked into the outbound mail, and an agent that embeds the EMAIL_REFUSE_v1 sentinel silently refuses (no reply is sent, so a probing sender gets no feedback).
Channel bookkeeping: bug_intake_processed records Gmail arrivals under the same email channel class (source = 'email') as AgentMail, so exact-id dedup and the Layer 1 similarity hint deliberately span both transports. The AgentMail/Gmail distinction lives on email_inbound_tracking.source ('agentmail' by default, 'gmail' for this path; the column was added by a dated ledger migration), which is what routes the finished session's reply back out the transport it came in on: each completion handler scopes to its own value, so the two can never cross-fire on one session.
v1 limitation: a tester's Re: reply on a Gmail bug thread is not threaded into the running session yet (unlike AgentMail replies, which append as operator messages). It never spawns a duplicate either; the strict ^\[ subject anchor rejects it. To file a follow-up as a fresh report, the tester sends a new email with a fresh [BUG: <slug>] subject.
Sentry intake — what it does
Sentry intake is the new vertical. Once you wire up a Sentry organization, Omniscio polls its unresolved-issue feed every 5 minutes (with 0–10s random jitter to avoid thundering-herd), and for each newly-seen issue it can spawn a brand-new Claude Code session in the linked project — same shape as an email-routed session, but the source label is sentry_intake and the operator chat row is a flattened render of the Sentry event (title, level, culprit, first/last seen, count, permalink).
Adding a Sentry source
- Sidebar → Bug Intake virtual project → Sources tab → Add source
- Pick a Project from your real (non-virtual, non-deleted) projects
- Set the Source type to Sentry (a GitHub source follows the GitHub steps below instead)
- Enter Org slug and Project slug — both must match
[a-z0-9_-]{1,64}(the same regex Sentry uses) - Pick a Sentry credential from the dropdown — managed at Settings → Workflow → Automation Credentials, kind
sentry, holds the Sentry auth token (OS-keyring encrypted; never crosses IPC) - Toggle Auto-spawn on if you want polled matches to spawn sessions immediately; leave off to land everything in Review for manual approval
- Test connection — one-shot GET against the Sentry org/project endpoint; reports
ok / errorverbatim with the HTTP body excerpt on failure. Stateless: no DB write, no scheduler refresh - Save — the row lands in
project_intake_sources, the scheduler refreshes, and the first poll fires after the jitter window
Editing a source from the same form is identical except the Project field is immutable (the composite (project_id, source) UNIQUE constraint would block the change anyway, and silently rebinding history to a different project is worse than asking the user to delete-and-recreate). Any field edit invalidates the previous Test connection result so you don't ship with a stale green checkmark.
Configuring Sentry sources via CLI (external automation)
External AIs and scripts (via the omniscio-control skill) can do everything the Sources tab does without opening Omniscio, through CLI control-server routes (127.0.0.1:19519, bearer-token auth): POST /intake/sources, PATCH /intake/sources/:id, DELETE /intake/sources/:id, plus POST/PATCH/DELETE /automation/credentials for the Sentry auth-token row a source references.
These follow a split-gate: because a disabled source polls nothing, creating or editing one applies immediately — but the moment a source is enabled it arms a recurring outbound poll that can spawn Claude sessions (which costs money), so that one step routes through the Omniscio inbox for approval, exactly like a human ticking the Enabled toggle.
- Creating a source with
enabled: false(andDELETE, and any edit that does not turn a disabled source on) applies immediately — the row shows up in the Sources tab right away. - Creating a source with
enabled: true(or omittingenabled, since the default is on), or flipping a disabled source to enabled viaPATCH, lands as a pending approval in the inbox; the source isn't written and doesn't start polling until the operator approves it. - Credential rows always apply immediately — a stored token is inert until an enabled source uses it. High-risk credential kinds (SMTP, Google service-account) stay blocked unless the project explicitly opts in.
(Agents with repo access: full per-route status codes, idempotency, and the deferred-write dispatcher arms live in the CLI gating reference.)
What happens on a poll
The Sentry client (fetchUnresolvedSentryIssues) GETs /api/0/projects/{org}/{project}/issues/?query=is:unresolved. The result list is filtered:
- First connect (
last_success_at IS NULL) — every unresolved issue is a candidate, sorted bylastSeenDESC; the top 5 spawn (ifautoSpawnis on) and the rest are recorded inbug_intake_processedwithdecision='unrouted'andreason='pending review'so they show up in the Review tab for manual review - Subsequent polls — every issue the fetch returns is a candidate; the exact-id claim below decides what is new. There is deliberately no
firstSeen ≥ last_success_atwindow: Sentry'sfirstSeenis the event's own timestamp, not when the issue becomes visible in the issue list (crashpad uploads a native crash on the next app launch, and Sentry's alert rule and search index each add lag), so that window permanently discarded any issue that surfaced more than one poll interval after its event — measured 2026-10-02 at 25 of 209 per-error alert emails with no row at all. A poll claims at mostMAX_NEW_CLAIMS_PER_POLL(5) genuinely-new issues so a catch-up after an outage is paced rather than a burst; the rest are re-fetched and claimed on the following polls
The response is paged: the issue list is capped at 100 rows sorted by firstSeen DESC, so page 1 alone hides everything older, and an issue below the fold can never climb back up — its position is set by a firstSeen that never changes. The fetch therefore follows the response's Link: rel="next" cursor, and every page is re-read on every poll, up to SENTRY_MAX_ISSUE_PAGES (5). It deliberately does not stop at a page it has already recorded: the claim table has gaps by construction, so a "this page is all known, so stop" rule turns the first such page into a permanent barrier and strands everything below it — the same silent drop the paging exists to remove. A short page, a missing cursor or the ceiling are the only ends to the walk. A next-page link that is not on the Sentry API base is refused, never followed. In practice this costs almost nothing: this project's entire unresolved list is two pages.
Each candidate flows through tryClaimIntakeDedupe(), which inserts a bug_intake_processed row keyed by (source='sentry', external_id=<issue.id>) via INSERT OR IGNORE — so a repeat poll cycle (or a parallel manual spawn) can never spawn the same Sentry issue twice.
If autoSpawn is on and the dedupe row was newly claimed, spawnIntakeSession() runs the same atomic transaction the email path uses: session row + operator chat bubble + display_order bump, then processManager.launch(sessionId, projectId, workDir, prompt). The session row is stamped source = 'sentry_intake'.
After the poll, the scheduler updates last_polled_at, last_success_at, and clears last_error (or sets last_error to the exception message on failure). The scheduler keeps a per-source mutex so an overlapping tick (e.g. user clicks Poll now mid-cycle) short-circuits instead of double-fetching.
24h continuous-failure escalation
If a source has had no successful poll for 24 hours, the scheduler emits a single intake:source-error-escalation push containing { sourceId, projectId, source, lastError, lastSuccessAt }. The Bug Intake view subscribes and surfaces it as a red row on the Sources tab so the user notices a stuck token before a week passes. The escalation is one-shot per stuck source — a successful poll clears the escalated flag and re-arms.
Durable "source broken" inbox card (2026-06-13)
The 24h push above is transient (a toast/red row, easy to miss) and its "already fired" flag lives only in memory, so an app restart re-armed it — a dead Sentry credential (after a hardware/keyring change) went unnoticed for ~5 days. A durable, self-clearing inbox card now closes that gap. Once a source has been failing continuously past ~1 hour — measured from its last successful poll, or its creation time if it has never succeeded — the scheduler raises ONE persistent inbox alert per source via the inbox-alert primitive (createAlert), so it shows on desktop, mobile, and the badge count and survives restart (it's a DB row). The card names the provider, echoes the already-humanized last_error (never a raw dump), and points at Bug Intake → Sources. It is raised once (guarded by findActiveByDedupKey, not coalesced, so the 5-min poll never inflates the dedup count) and auto-clears on the next successful poll; a reconcile pass on scheduler start()/refresh() also clears cards for any source that was disabled or deleted. Bulletproof (any error swallowed — polling never breaks) and gated by the AMC_DISABLE_INTAKE_SOURCE_ALERT env kill switch. The legacy 24h escalation push is untouched (this card is additive — earlier and durable). Producer: src/main/services/intake/intake-source-alert.ts; 8 test-locked invariants in intake-source-alert-contract.md.
A source that keeps failing to START sessions (2026-09-25)
A source can poll perfectly and still fail every investigation it tries to start (no active account, a bad model or project path). Every intake source — Sentry, GitHub, emailed reports (the [BUG:]/[FR:] router, the Gmail leg, the Help Desk investigation), in-app and teammate-assigned reports — starts its sessions through the one shared start (spawnIntakeSession), and that start tells the alarm how each attempt went. Three failed starts in a row for the same source and project raise one card, filed under that project and naming the source in plain words ("GitHub reports", "Email reports", …); the next successful start clears it (even one raised before a restart). A safety-screen hold is not a failure, and a report for a deleted project is not counted. The card asks for nothing it cannot offer in one click, so it carries no button. Producer: src/main/services/intake/intake-spawn-failure-monitor.ts. Until 2026-09-25 only Sentry fed this alarm, counted per poll.
Manual spawn / dismiss from Review
Each Review row carries two buttons:
- Spawn now → routes straight to
INTAKE_MANUAL_SPAWN(spawning is billable; it fires immediately with no confirmation — the button disables while in flight, and the backend is idempotent by issue id) which re-usesspawnIntakeSessionagainst the recordedexternal_id, then stamps the row'ssession_idso it transitions out of Review into Audit. A GitHub row works the same way: the poll stored only the issue's title and link when it parked it (its source has auto-spawn off), so Spawn re-reads that one issue withgh(getRepoIssue) and starts it through the same screened start (manuallySpawnGithubIntake); a failed launch puts it back in Review. An issue closed on GitHub since it was parked is not started (the poll only ever starts open issues): the card stays, with a message to dismiss it here or reopen it on GitHub first. The poll itself never re-reads a parked issue, so Spawn is the only way it starts. A Sentry or GitHub report whose automatic start failed waits here too, with the reason A session could not be started for this report. Use Spawn to try again. — its poll never fetches it again, so it is no longer markedfailedand lost (the raw error goes to the log, never the card). On success the receipt toast carries a Go to session action (samenavigateToSessionhelper) so you can open the freshly-spawned session in one click; it is offer-to-open, not auto-navigate, so spawning several in a row is not disrupted - Dismiss → marks the row
decision = 'manually_dismissed'so it disappears from Review but stays in Audit with the reason intact
Global pause
The Pause all polling toggle at the top of the Bug Intake view binds to the intakeGloballyPaused AppSettings field. The scheduler reads it on every tick — when true, tick() returns immediately with error: 'globally paused' and no Sentry HTTP call goes out. The same intakeHold() answer (src/main/services/intake/intake-switches.ts) holds the email and in-app sources too (see the main Bug Report Intake page). The per-source enabled flag is unaffected, so flipping the global pause off resumes everything at its previous state.
GitHub intake — what it does
GitHub intake is the third inflow. Point a source at one repository and Omniscio polls its open issues every 5 minutes (the same scheduler + 0–10s jitter as Sentry), spawning a Claude Code session per new issue in the linked project. The session's source label is github_intake and the operator chat row is a flattened, fenced render of the issue (number, title, reporter, labels, URL, body).
Key differences from the Sentry vertical:
- Auth is the shipped
ghCLI — no stored credential. A GitHub source carriescredentials_id = NULL; Omniscio reads issues with the sameghlogin the rest of Omniscio already uses (runGh/gh api), so there is no token to create or manage. An Omniscio user who already uses any GitHub feature already hasghsigned in. - No triage gate. GitHub issues are human-filed and label-filterable, so they go straight to investigation (subject to dedup + caps) — none of Sentry's severity / consolidation / AI-second-look machinery applies.
- Scope: all new open issues, optional single label. Leave the label blank to watch every new open issue, or set one label (e.g.
bug) to narrow it. Pull requests are excluded. Watermarked oncreated_at: first connect spawns the 5 most-recent, then only issues created after the last successful poll. - The dedup id is
owner/repo#number(e.g.jlstradingco/agent-orchestrator#42) — human-readable in the Audit tab. Exact-id dedup (tryClaimIntakeDedupe) guarantees an issue never spawns twice; the Layer 1 hint uses arepo|titlesignature so a re-filed duplicate is flagged for the investigator within the 24h window. - A per-source daily spawn cap (
GITHUB_DAILY_SPAWN_CAP) bounds a busy or public repo on top of the watermark — Sentry has no daily cap because its triage gate already throttles volume.
Adding a GitHub source
- Sidebar → Bug Intake → Sources tab → Add source
- Set the Source type to GitHub
- Pick a Project from your real (non-virtual, non-deleted) projects
- Enter the Owner and Repository (e.g.
jlstradingco/agent-orchestrator) — validated client-side by the same[A-Za-z0-9_.-]regexes the backend Zod schema uses - Optionally set a Label filter to watch only issues carrying one label
- Test connection — confirms the repo is reachable under your current
ghlogin (no credential needed); stateless, no DB write - Toggle Auto-spawn and Save — the row lands in
project_intake_sources, the scheduler refreshes, and the first poll fires after the jitter window
Editing is identical except Source type and Project are immutable (delete and recreate to change either). A GitHub issue title/body on a public repo is attacker-submittable, so it is fenced as reporter-supplied data in the spawn prompt (the same posture as an inbound email bug body) and never treated as instructions.
Related
This is one part of four. Part 1 is the overview, the sidebar panel and the email channel; part 3 covers what a spawned investigation receives and how duplicate reports are collapsed; part 4 covers the routing decision table, analytics, out-of-scope notes and the code map. For the alert cards a broken source raises, see inbox alerts; for the mail plumbing the Gmail channel rides on, see Agent email.
Last verified 2026-10-03