Summary
Factory's human-loop today is one-shot: post a dispatch thread (src/writeback/slack.ts, MountSlackWriteback.postThread/reply), and separately handle a single clarification round-trip per thin/blocked triage (human-durable-clarification-wake, human-slack-reply-watch, human-clarified-redispatch in .agentworkforce/features/manifest.yaml). There's no way for a human to just talk to the agent back and forth in the thread the way Amika's Slack integration does (@amika mention spins up a sandbox mapped 1:1 to the thread; the session continues across replies). This issue is that capability, generalized to Slack now with Telegram as a documented follow-on.
Important constraint this must respect
FleetClient (src/ports/fleet.ts) still exposes waitForInjected/sendInput for live delivery into a running session, but issue #105 is an explicit, in-progress effort to make that structurally impossible — the stated invariant is "an agent receives input only at spawn or via resume; live injection into a running session is prohibited," with 3 remaining call sites (the babysitter wake path) being migrated to coalesced resume. A turn-by-turn chat feature must be built on the post-#105 model from day one — resume-with-fresh-task, not injection — or it becomes the reason that invariant never lands. Treat this as a hard design constraint, not a nice-to-have.
Proposed approach
Model this exactly like the babysitter's durable, coalesced wake (pr-durable-babysitter-sessions, pr-babysitter-wake-coalescing in the manifest) rather than inventing a new mechanism.
A. Thread-to-session mapping
One Slack thread maps to one agent session, keyed the same way BabysitterSessionState keys PR ownership (src/ports/state.ts, src/state/file-state-store.ts) — persist thread→session ownership so a restart doesn't lose the mapping, mirroring pr-durable-babysitter-sessions.
B. Message delivery via resume, not injection
A new reply in the thread should not call sendMessage/sendInput into a live process. It should: coalesce rapid-fire replies within a short window (reuse the coalescing logic behind pr-babysitter-wake-coalescing rather than reinventing it), then deliver via FleetClient.resume({ sessionRef, task: <folded-in message + prior context> }) — exactly the pattern human-clarified-redispatch already uses for the single-clarification case. This is the same resume-with-fresh-task shape #105 is converging the babysitter onto; this feature should land after or alongside #105's migration, reusing its coalescing primitive rather than building a second one.
C. Slack-side subscription
Extend the Slack subscription surface (src/subscriptions/specs.ts and the Slack-filtering equivalent of linear-filter.ts) to watch arbitrary thread replies, not just the single correlated clarification reply human-slack-reply-watch currently expects — this is a broadening of an existing mechanism, not new plumbing.
D. Telegram (follow-up, not this issue's critical path)
Telegram has none of Slack's existing surface — no mount/writeback module (src/writeback/slack.ts has no Telegram counterpart), no subscription globs, no channel-allow-list concept (assertSlackChannelAllowed in slack.ts is Slack-specific). Land Slack first; scope Telegram as a second, smaller issue once the resume-based session-continuity core (A/B) is proven — only the provider-specific mount/writeback/subscription layer needs to be rebuilt per channel. The session-continuity mechanism itself should not need touching twice, which is the whole point of building it on the generic FleetClient.resume() contract rather than Slack-specific code.
Acceptance criteria
Notes for the implementer
Blocked by: #105 (the no-live-injection invariant this must be built on top of, not around)
Summary
Factory's human-loop today is one-shot: post a dispatch thread (
src/writeback/slack.ts,MountSlackWriteback.postThread/reply), and separately handle a single clarification round-trip per thin/blocked triage (human-durable-clarification-wake,human-slack-reply-watch,human-clarified-redispatchin.agentworkforce/features/manifest.yaml). There's no way for a human to just talk to the agent back and forth in the thread the way Amika's Slack integration does (@amikamention spins up a sandbox mapped 1:1 to the thread; the session continues across replies). This issue is that capability, generalized to Slack now with Telegram as a documented follow-on.Important constraint this must respect
FleetClient(src/ports/fleet.ts) still exposeswaitForInjected/sendInputfor live delivery into a running session, but issue #105 is an explicit, in-progress effort to make that structurally impossible — the stated invariant is "an agent receives input only at spawn or via resume; live injection into a running session is prohibited," with 3 remaining call sites (the babysitter wake path) being migrated to coalesced resume. A turn-by-turn chat feature must be built on the post-#105 model from day one — resume-with-fresh-task, not injection — or it becomes the reason that invariant never lands. Treat this as a hard design constraint, not a nice-to-have.Proposed approach
Model this exactly like the babysitter's durable, coalesced wake (
pr-durable-babysitter-sessions,pr-babysitter-wake-coalescingin the manifest) rather than inventing a new mechanism.A. Thread-to-session mapping
One Slack thread maps to one agent session, keyed the same way
BabysitterSessionStatekeys PR ownership (src/ports/state.ts,src/state/file-state-store.ts) — persist thread→session ownership so a restart doesn't lose the mapping, mirroringpr-durable-babysitter-sessions.B. Message delivery via resume, not injection
A new reply in the thread should not call
sendMessage/sendInputinto a live process. It should: coalesce rapid-fire replies within a short window (reuse the coalescing logic behindpr-babysitter-wake-coalescingrather than reinventing it), then deliver viaFleetClient.resume({ sessionRef, task: <folded-in message + prior context> })— exactly the patternhuman-clarified-redispatchalready uses for the single-clarification case. This is the same resume-with-fresh-task shape #105 is converging the babysitter onto; this feature should land after or alongside #105's migration, reusing its coalescing primitive rather than building a second one.C. Slack-side subscription
Extend the Slack subscription surface (
src/subscriptions/specs.tsand the Slack-filtering equivalent oflinear-filter.ts) to watch arbitrary thread replies, not just the single correlated clarification replyhuman-slack-reply-watchcurrently expects — this is a broadening of an existing mechanism, not new plumbing.D. Telegram (follow-up, not this issue's critical path)
Telegram has none of Slack's existing surface — no mount/writeback module (
src/writeback/slack.tshas no Telegram counterpart), no subscription globs, no channel-allow-list concept (assertSlackChannelAllowedinslack.tsis Slack-specific). Land Slack first; scope Telegram as a second, smaller issue once the resume-based session-continuity core (A/B) is proven — only the provider-specific mount/writeback/subscription layer needs to be rebuilt per channel. The session-continuity mechanism itself should not need touching twice, which is the whole point of building it on the genericFleetClient.resume()contract rather than Slack-specific code.Acceptance criteria
sendInput/waitForInjectedon a live session.pr-durable-babysitter-sessionsrestart-recovery tests insrc/orchestrator/factory.test.ts).expect(fleet.messages).toEqual([])-style invariant test).Notes for the implementer
npm test(vitest); extendsrc/orchestrator/factory.test.tsand add coverage undersrc/writeback/slack.test.ts(or equivalent).Blocked by: #105 (the no-live-injection invariant this must be built on top of, not around)