Skip to content

Slack writeback gate silently strips human-steering: a stale sync signal removes the agent's ability to ask, and the bypass reads the same broken source #94

Description

@khaliqgant

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

  1. #slackFreshness() (factory.js ~3785) reads mount.getSyncStatus('slack'); lastEventAt is 6wk old vs staleAfterMs (default 10min) → degraded.
  2. The designed bypass — slackGateBypassedByEventWatermarkcannot 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.
  3. #shouldSkipSlackWriteback('dispatch-thread') → true → #postAndWatchSlackDispatchThread never runs → setSlackThread never called.
  4. #slackDispatchThreadFor returns undefined (it returns a thread only if one is already in state).
  5. 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

  1. 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."
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions