Summary
When Slack sync reports stale, factory's writeback circuit breaker trips and skips the dispatch thread. Because the agent's "you may ask a human" instructions are gated on that thread existing, the whole human-steering feature is silently removed from the agent's task. The agent is not told it can ask; it just proceeds and guesses. Nothing surfaces this except one warn line.
Today this fires on a false signal: the Slack last_event_at field is frozen at 2026-06-06 while webhooks deliver in ~8s (AgentWorkforce/cloud#2666). So human-steering — the differentiator we pitch as "the part that beats a tmux full of agents" — is off in the field right now, and looks identical to "the agent didn't need to ask."
Reproduced end to end (0.1.19, published)
Config: babysitter.enabled: true, slack.channel: C0BAC2KSTGV, repo AgentWorkforce/relayed.
Issue #48 was written to be unanswerable from the repo — "the release cadence and approver are not written down anywhere... do not guess, do not invent a placeholder... ask the maintainer." An agent that can ask, must.
Dispatch log:
[factory] Slack sync degraded; skipping Slack writeback
{"context":"dispatch-thread","reason":"slack sync status is lagging",
"status":{"provider":"slack","status":"lagging",
"lastEventAt":"2026-06-06T12:05:00.000Z",
"watermarkTs":"2026-06-06T12:05:00.000Z","lagSeconds":0}}
The implementer's resulting task (read off its live cmdline):
| string |
present? |
ask the maintainer (from the issue body) |
yes |
RELEASE-POLICY (from the issue body) |
yes |
factory-needs-input (the ask mechanism) |
NO |
slack (the thread) |
NO |
The agent was told to ask and given no mechanism to.
Chain
#slackFreshness() (factory.js ~3785) reads mount.getSyncStatus('slack'); lastEventAt is 6wk old vs staleAfterMs (default 10min) → degraded.
- The designed bypass —
slackGateBypassedByEventWatermark — cannot fire: #slackEventWatermark() returns the same frozen 2026-06-06T12:05:00.000Z. Both inputs to the gate come from the same broken source, so the safety net is not independent of what it guards against.
#shouldSkipSlackWriteback('dispatch-thread') → true → #postAndWatchSlackDispatchThread never runs → setSlackThread never called.
#slackDispatchThreadFor returns undefined (it returns a thread only if one is already in state).
templates.ts: const questionInstructions = input.slackDispatchThread ? [...] : [] → the ask instructions vanish from the rendered task.
Why this is worse than a normal outage
The failure is silent and load-bearing:
- The agent doesn't know the feature existed, so it can't report its absence. It guesses and produces plausible work — exactly the failure human-steering is supposed to prevent.
- The user sees a normal run. Nothing in the PR, the issue, or the terminal says "clarification was disabled."
- It degrades the product to "an agent that guesses" precisely when a human is available and reachable in seconds.
Suggestions
- Don't derive the gate from a field that can lie about the path it guards. The gate and its bypass both read the sync status/watermark. Webhook delivery is healthy and observable — an event-derived watermark (or the mount's actual message arrivals) would be an independent signal. As written,
#slackFreshness cannot distinguish "Slack is stale" from "the field is wrong."
- Fail loudly, don't strip capability. If the thread can't be posted, that's a dispatch-quality decision the human should see — surface it on the issue/PR ("clarification unavailable: Slack writeback degraded"), or refuse to dispatch an issue that needs clarification, rather than silently shipping an agent that can't ask.
- Consider decoupling the ask instructions from thread existence: the agent could emit
[factory-needs-input] regardless, and factory decides how to route (Slack, GitHub comment, or escalate) — there's already a GitHub clarification path (#injectPendingGithubClarification).
Root cause of today's false trip: AgentWorkforce/cloud#2666 (Slack last_event_at stale while webhooks are real-time — ingest is NOT dead; the sync/schedule registration is). Fixing that unblocks human-steering, but the silent-strip behavior is worth fixing independently — the next stale-signal event will do this again.
Refs: AgentWorkforce/cloud#2666, #84, #85, #87.
Summary
When Slack sync reports stale, factory's writeback circuit breaker trips and skips the dispatch thread. Because the agent's "you may ask a human" instructions are gated on that thread existing, the whole human-steering feature is silently removed from the agent's task. The agent is not told it can ask; it just proceeds and guesses. Nothing surfaces this except one
warnline.Today this fires on a false signal: the Slack
last_event_atfield is frozen at2026-06-06while webhooks deliver in ~8s (AgentWorkforce/cloud#2666). So human-steering — the differentiator we pitch as "the part that beats a tmux full of agents" — is off in the field right now, and looks identical to "the agent didn't need to ask."Reproduced end to end (0.1.19, published)
Config:
babysitter.enabled: true,slack.channel: C0BAC2KSTGV, repoAgentWorkforce/relayed.Issue #48 was written to be unanswerable from the repo — "the release cadence and approver are not written down anywhere... do not guess, do not invent a placeholder... ask the maintainer." An agent that can ask, must.
Dispatch log:
The implementer's resulting task (read off its live cmdline):
ask the maintainer(from the issue body)RELEASE-POLICY(from the issue body)factory-needs-input(the ask mechanism)slack(the thread)The agent was told to ask and given no mechanism to.
Chain
#slackFreshness()(factory.js~3785) readsmount.getSyncStatus('slack');lastEventAtis 6wk old vsstaleAfterMs(default 10min) → degraded.slackGateBypassedByEventWatermark— cannot fire:#slackEventWatermark()returns the same frozen2026-06-06T12:05:00.000Z. Both inputs to the gate come from the same broken source, so the safety net is not independent of what it guards against.#shouldSkipSlackWriteback('dispatch-thread')→ true →#postAndWatchSlackDispatchThreadnever runs →setSlackThreadnever called.#slackDispatchThreadForreturnsundefined(it returns a thread only if one is already in state).templates.ts:const questionInstructions = input.slackDispatchThread ? [...] : []→ the ask instructions vanish from the rendered task.Why this is worse than a normal outage
The failure is silent and load-bearing:
Suggestions
#slackFreshnesscannot distinguish "Slack is stale" from "the field is wrong."[factory-needs-input]regardless, and factory decides how to route (Slack, GitHub comment, or escalate) — there's already a GitHub clarification path (#injectPendingGithubClarification).Root cause of today's false trip: AgentWorkforce/cloud#2666 (Slack
last_event_atstale while webhooks are real-time — ingest is NOT dead; the sync/schedule registration is). Fixing that unblocks human-steering, but the silent-strip behavior is worth fixing independently — the next stale-signal event will do this again.Refs: AgentWorkforce/cloud#2666, #84, #85, #87.