Skip to content

[factory] Slack thread replies never reach the babysitter — the dispatch thread is one-way #273

Description

@khaliqgant

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

  1. A reply in a Factory dispatch thread reaches the babysitter for that issue. The thread→issue mapping already exists in state; use it.
  2. 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.
  3. 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.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    factoryFactory automation trigger

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions