Summary
A human replying in the Slack dispatch thread should reach the issue's babysitter. It does not. The reply lands in Slack, nothing routes it, and the agents that own the work never see it.
What happened
Factory posted its dispatch notification for AgentWorkforce/cloud#3045:
3045: factory agents dispatched.
State: dispatching · Agents: ar-3045-impl-cloud, ar-3045-review-cloud
The principal replied in that thread:
@agent Relay there is unaddressed pr feedback here and failing ci
That is exactly the intended interaction — a human noticing something wrong and saying so where the notification lives, rather than hunting for the right agent. It produced no action. ar-3045-babysit-cloud and ar-3045-review-cloud were both alive at the time and neither reacted.
The observation was correct, incidentally: cloud#3049 had three unaddressed review findings (one 🔴 bug, one 🟨 security, one 🟡 correctness) and two failing CI checks. So the human was right and the channel was the problem.
Why this should work through the relayfile mount
Slack is already a relayfile-projected provider. Thread replies land in the mount as files under the channel/thread tree, and the writeback contract runs the other way. So the routing primitive exists — a reply is a file appearing at a path that is already associated with the dispatch thread.
Factory also already tracks the association. src/orchestrator/factory.ts:
#postAndWatchSlackDispatchThread posts the dispatch message and stores the thread id
this.#state.setSlackThread(this.#workspaceId, issueKey(record.issue), root.threadId)
#ensureSlackConversationSession(record, root.threadId)
#watchSlackThread(record, root.threadId)
So the thread id is persisted against the issue key and a watcher is registered. The gap is between that watcher firing and the babysitter being woken with the reply.
Asked-for behaviour
- A reply in a Factory dispatch thread reaches the babysitter for that issue. The thread→issue mapping already exists in state; use it.
- Route through the relayfile mount rather than a separate Slack API path. The projection is the coordination substrate, and a second ingestion path would drift from it.
- Acknowledge in-thread. A human who replies and gets silence cannot tell whether the message was received, whether an agent is working, or whether the feature exists. Even a reaction closes that loop.
- Fail loudly if routing cannot happen — no live babysitter, no thread mapping, agent unreachable. Silence is the current behaviour and it is indistinguishable from "working on it."
Definition of done
- A reply in a dispatch thread demonstrably reaches the babysitter for that issue, verified by the agent acting on it, not by a log line saying it was sent.
- The human sees an acknowledgement in-thread.
- A reply that cannot be routed produces a visible error rather than silence.
Verification — against a real thread
Do not close this on a unit test asserting a message was dispatched.
Dispatch an issue, reply in its Slack thread, and show the babysitter receiving and acting on it. Attach the thread and the agent's response.
Notes
- Related:
factory#269 covers the content of these notifications (bare issue numbers, no repo, no link). This issue covers the return path. Both are about the dispatch thread being a real interface rather than a one-way log.
- The specific findings the principal was pointing at are in
cloud#3049 and are being addressed separately.
mergePolicy: never — open the PR and stop at review.
- Branch from
origin/main.
Summary
A human replying in the Slack dispatch thread should reach the issue's babysitter. It does not. The reply lands in Slack, nothing routes it, and the agents that own the work never see it.
What happened
Factory posted its dispatch notification for
AgentWorkforce/cloud#3045:The principal replied in that thread:
That is exactly the intended interaction — a human noticing something wrong and saying so where the notification lives, rather than hunting for the right agent. It produced no action.
ar-3045-babysit-cloudandar-3045-review-cloudwere both alive at the time and neither reacted.The observation was correct, incidentally:
cloud#3049had three unaddressed review findings (one 🔴 bug, one 🟨 security, one 🟡 correctness) and two failing CI checks. So the human was right and the channel was the problem.Why this should work through the relayfile mount
Slack is already a relayfile-projected provider. Thread replies land in the mount as files under the channel/thread tree, and the writeback contract runs the other way. So the routing primitive exists — a reply is a file appearing at a path that is already associated with the dispatch thread.
Factory also already tracks the association.
src/orchestrator/factory.ts:#postAndWatchSlackDispatchThreadposts the dispatch message and stores the thread idthis.#state.setSlackThread(this.#workspaceId, issueKey(record.issue), root.threadId)#ensureSlackConversationSession(record, root.threadId)#watchSlackThread(record, root.threadId)So the thread id is persisted against the issue key and a watcher is registered. The gap is between that watcher firing and the babysitter being woken with the reply.
Asked-for behaviour
Definition of done
Verification — against a real thread
Do not close this on a unit test asserting a message was dispatched.
Dispatch an issue, reply in its Slack thread, and show the babysitter receiving and acting on it. Attach the thread and the agent's response.
Notes
factory#269covers the content of these notifications (bare issue numbers, no repo, no link). This issue covers the return path. Both are about the dispatch thread being a real interface rather than a one-way log.cloud#3049and are being addressed separately.mergePolicy: never— open the PR and stop at review.origin/main.