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
-
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."
-
The orchestrator knows which babysitter owns which PR. #babysitterPr is keyed by issue key (factory.ts:3792).
-
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.
- 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.
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
The orchestrator already receives the events.
LIVE_GITHUB_ISSUE_GLOB = ${GITHUB_ISSUE_ROOT}/**whereGITHUB_ISSUE_ROOT = '/github/repos'(factory.ts:138,150), subscribed atfactory.ts:577. That terminal**covers reviews, review comments, issue comments and checks. The inline comment at:571-573says as much: "already covers the PR change events the babysitter consumes."The orchestrator knows which babysitter owns which PR.
#babysitterPris keyed by issue key (factory.ts:3792).And then it returns.
#ensureBabysitter(factory.ts:3791-3795):Once a babysitter exists, every subsequent PR event for that issue refreshes internal state and is dropped. The babysitter is never told.
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:
#sweepPrStateCompletionsis 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
#waitForInjectedAndSubmit→#submitInjectedTask(factory.ts:3536-3600) delivers via relay broker DM then writes\rto the PTY to submit. Retry/confirmation policy atfactory.ts:169-172(INJECTION_MAX_ATTEMPTS = 6,INJECTION_CONFIRMATION_TIMEOUT_MS = 90_000). Used today at:2920,:3495,:3531.relayflow-registry.ts:19,203already normalizes to agithub.pull_request.review_submittedshape.#dispatchRelayflowEvent(factory.ts:938) exists but is opt-in via#relayflows, and nopull_requestpolicy is registered anywhere — it's only exercised in tests (factory.test.ts:4233,4297).Proposal
Where
#ensureBabysittercurrently early-returns on an already-spawned babysitter, inject the event into that babysitter's session instead of dropping it, via the existing#submitInjectedTaskpath.Scope it to the events the babysitter actually acts on:
pull_request_review.submitted(especiallychanges_requested)pull_request_review_comment.created/issue_comment.createdon its PRDesign notes:
coalesceMs: 750(src/subscriptions/specs.ts:245-247) while the live subscription usescoalesce: 'none'(factory.ts:577) — worth reconciling deliberately rather than by default.git rebaseshouldn't be derailed. Consider deferring while a critical section is in flight.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.Two viable shapes: register a
pull_requestrelayflow policy and use the existing#dispatchRelayflowEventpath, or inject directly from#ensureBabysitterand leave relayflow for later. The direct path is smaller and doesn't require committing to the relayflow abstraction.Acceptance
Related