---
title: Bug Report Intake (part 2)
---

# Bug Report Intake (part 2)

## What it is

This is part 2 of the [Bug Report Intake](bug-report-intake.md) 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 (rejecting `Re:` / `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-REPORT` untrusted-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

1. Sidebar → **Bug Intake** virtual project → **Sources** tab → **Add source**
2. Pick a **Project** from your real (non-virtual, non-deleted) projects
3. Set the **Source type** to **Sentry** (a GitHub source follows the GitHub steps below instead)
4. Enter **Org slug** and **Project slug** — both must match `[a-z0-9_-]{1,64}` (the same regex Sentry uses)
5. 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)
6. Toggle **Auto-spawn** on if you want polled matches to spawn sessions immediately; leave off to land everything in Review for manual approval
7. **Test connection** — one-shot GET against the Sentry org/project endpoint; reports `ok / error` verbatim with the HTTP body excerpt on failure. Stateless: no DB write, no scheduler refresh
8. **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` (and `DELETE`, 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 omitting `enabled`, since the default is on), or flipping a disabled source to enabled via `PATCH`, 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 by `lastSeen` DESC; the top **5** spawn (if `autoSpawn` is on) and the rest are recorded in `bug_intake_processed` with `decision='unrouted'` and `reason='pending review'` so they show up in the Review tab for manual review
- **Subsequent polls** — only issues whose `firstSeen ≥ last_success_at` are considered candidates (Sentry has no `since=` filter on the issue-list endpoint, so we filter client-side)

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](inbox-alerts.md) (`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-uses `spawnIntakeSession` against the recorded `external_id`, then stamps the row's `session_id` so 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 with `gh` (`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 marked `failed` and lost (the raw error goes to the log, never the card). On success the receipt toast carries a **Go to session** action (same `navigateToSession` helper) 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 `gh` CLI — no stored credential.** A GitHub source carries `credentials_id = NULL`; Omniscio reads issues with the same `gh` login 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 has `gh` signed 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 on `created_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 a `repo|title` signature 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

1. Sidebar → **Bug Intake** → **Sources** tab → **Add source**
2. Set the **Source type** to **GitHub**
3. Pick a **Project** from your real (non-virtual, non-deleted) projects
4. 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
5. Optionally set a **Label filter** to watch only issues carrying one label
6. **Test connection** — confirms the repo is reachable under your current `gh` login (no credential needed); stateless, no DB write
7. 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](bug-report-intake.md) is the overview, the sidebar panel and the email channel; [part 3](bug-report-intake-part-3.md) covers what a spawned investigation receives and how duplicate reports are collapsed; [part 4](bug-report-intake-part-4.md) 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](inbox-alerts.md); for the mail plumbing the Gmail channel rides on, see [Agent email](agent-email.md).
