Skip to content

Babysitter goes blind after spawn: route its own PR's review/CI events into its session instead of dropping them #87

Description

@khaliqgant

Summary

The babysitter is spawned event-driven and then goes blind. It receives no events for the PR it owns, so it has to decide on its own when to go re-read the mount. Its reaction latency to a review comment or a red check is bounded by whenever it next chooses to look — not by when the event arrives.

Everything needed to fix this already exists in the orchestrator. It's a routing gap, not a missing capability.

What happens today

  1. The orchestrator already receives the events. LIVE_GITHUB_ISSUE_GLOB = ${GITHUB_ISSUE_ROOT}/** where GITHUB_ISSUE_ROOT = '/github/repos' (factory.ts:138,150), subscribed at factory.ts:577. That terminal ** covers reviews, review comments, issue comments and checks. The inline comment at :571-573 says as much: "already covers the PR change events the babysitter consumes."

  2. The orchestrator knows which babysitter owns which PR. #babysitterPr is keyed by issue key (factory.ts:3792).

  3. And then it returns. #ensureBabysitter (factory.ts:3791-3795):

async #ensureBabysitter(record: InFlightIssue, prRef: {...}): Promise<void> {
  this.#babysitterPr.set(record.issue.key, { repo: prRef.repo, prNumber: prRef.prNumber, path: prRef.path })
  if (this.#babysitterSpawned.has(record.issue.key)) {
    return                      // <-- every post-spawn PR event dies here
  }
  ...
}

Once a babysitter exists, every subsequent PR event for that issue refreshes internal state and is dropped. The babysitter is never told.

  1. So the prompt tells it to poll. templates.ts:123: "Read the PR diff, CI checks, and review threads via ${mountRoot}/github/repos." That's a pull, on the agent's own initiative, with no signal that anything changed.

Worth being precise about a nearby line, because it reads like this is already solved: templates.ts:92's "delivered to you as an <integration-event> system message injected into your session — wait for it, do not poll" applies only to the Slack dispatch-thread question flow (asking a human a question mid-task). It does not apply to PR events.

Why this matters

The babysitter is the right design — a fresh long-lived agent holding the issue spec as the definition of done, able to DM the implementers and reviewer. It's identity-scoped to exactly one PR, which is the ideal subscriber. It just has no way to be woken.

Practical consequences:

  • Latency is unbounded and non-deterministic. A reviewer leaves a comment; the babysitter finds out whenever it next loops.
  • It burns tokens re-reading the mount to discover that nothing changed.
  • #sweepPrStateCompletions is disabled when the babysitter is enabled (factory.ts:999-1005, "Disabling it here is what makes the babysitter path webhook-driven rather than polled") — which is true of the orchestrator's completion sweep, but the babysitter's own awareness is still a pull. The polling didn't go away; it moved into the agent.

The pieces already exist

  • Injection: #waitForInjectedAndSubmit#submitInjectedTask (factory.ts:3536-3600) delivers via relay broker DM then writes \r to the PTY to submit. Retry/confirmation policy at factory.ts:169-172 (INJECTION_MAX_ATTEMPTS = 6, INJECTION_CONFIRMATION_TIMEOUT_MS = 90_000). Used today at :2920, :3495, :3531.
  • Normalization: relayflow-registry.ts:19,203 already normalizes to a github.pull_request.review_submitted shape.
  • Dispatch: #dispatchRelayflowEvent (factory.ts:938) exists but is opt-in via #relayflows, and no pull_request policy is registered anywhere — it's only exercised in tests (factory.test.ts:4233,4297).

Proposal

Where #ensureBabysitter currently early-returns on an already-spawned babysitter, inject the event into that babysitter's session instead of dropping it, via the existing #submitInjectedTask path.

Scope it to the events the babysitter actually acts on:

  • pull_request_review.submitted (especially changes_requested)
  • pull_request_review_comment.created / issue_comment.created on its PR
  • check failures (red → actionable; green → probably not worth a wake)
  • merge conflict / base-branch divergence

Design notes:

  • Only its own PR. The orchestrator already maps PR → issue → babysitter, so no broadcast and no in-agent filtering.
  • Coalesce. A review with eight inline comments shouldn't be eight injections. The per-integration spec builder already uses coalesceMs: 750 (src/subscriptions/specs.ts:245-247) while the live subscription uses coalesce: 'none' (factory.ts:577) — worth reconciling deliberately rather than by default.
  • Don't interrupt destructively. Injection submits to the PTY immediately; a babysitter mid-git rebase shouldn't be derailed. Consider deferring while a critical section is in flight.
  • Keep the pull as a backstop. Events can be missed (the subscription is from: 'now', so a reconnect window drops events); the babysitter should still be able to re-read the mount. Event-driven for latency, pull for correctness.
  • Untrusted content. Review bodies are attacker-authorable and the babysitter holds push credentials. Whatever gets injected should be fenced rather than concatenated raw into the session (see AgentWorkforce/cloud#2671 for the same concern in personas).

Two viable shapes: register a pull_request relayflow policy and use the existing #dispatchRelayflowEvent path, or inject directly from #ensureBabysitter and leave relayflow for later. The direct path is smaller and doesn't require committing to the relayflow abstraction.

Acceptance

  • A review comment on the babysitter's PR reaches its session within seconds, with no polling.
  • Events for other PRs never reach it.
  • A multi-comment review produces one wake, not N.
  • Injection while the babysitter is mid-critical-section doesn't corrupt its work.
  • The babysitter can still self-heal by re-reading the mount if an event is missed.

Related

  • AgentWorkforce/cloud#2670 — the same gap as a general primitive. This issue is the concrete, highest-value instance of it.
  • AgentWorkforce/cloud#2671 — fencing untrusted provider text before it enters an agent's context.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions