name: slack-relayfile-writeback-debugging description: Use when an outbound Slack writeback (channel post, thread reply, or DM) silently fails to deliver — no error, no dead-letter. Covers the writeback pipeline, the relayfile ops-API getOp recipe to read the persisted lastError, where the Slack error actually surfaces (NOT the consumer logs), the missing_scope/im:write 3-place fix, cf-vs-bridge dispatch, and duplicate/silent-send pitfalls.
Debugging Slack writeback non-delivery (relayfile ↔ cloud)
You are diagnosing why an outbound Slack writeback did not reach Slack. A writeback is a JSON file dropped under a mounted command root (…/channels/<id>__<slug>/messages[/<ts>/replies]/<file>.json or …/users/<U>/messages/<file>.json). Confirm with evidence, never a hypothesis.
Pipeline (cloud, AgentWorkforce/cloud)
local file → mount pushes up → ingest creates a writeback OP → WRITEBACK_QUEUE
→ batch executor → cf OR bridge provider → Slack API
- Ingest → op:
packages/relayfile/src/durable-objects/handlers/ops.tsrecordMutations (~1043-1101) makes a writeback op for ANYagent_write;provider_syncorigin is suppressed (returns "succeeded" with NO op). - Queue consumer (batch):
packages/relayfile/src/queue-consumer.ts:71→executeProviderWritebackBatch. It logs ONLYwriteback.start(provider-executor.ts:258) — never the Slack error. Sowrangler tail relayfile-writeback-consumerconfirms dispatch but never shows why it failed. - Path allowlist:
provider-executor.tsgetUnsupportedReason (951-1010);SLACK_UPSERT_WRITEBACK_PATH(1203-1205) — includesusers/<U>/messages(DM path allowed). - cf-vs-bridge fork:
:132(single) /:288(batch). Defaultwriteback_dispatch_via = bridge(workspace.ts:1062); cf has the idempotency guard the bridge lacks (cloud#1985).
THE diagnostic: read the op's lastError (ground truth)
curl -H "Authorization: Bearer <token from <mount>/.relay/creds.json>" \
-H "X-Correlation-Id: anything" \
https://api.relayfile.dev/v1/workspaces/<rw_id>/ops/<opId>
- ⚠️
X-Correlation-Idis required — without it: HTTP 400missing X-Correlation-Id header. - base =
DEFAULT_RELAYFILE_BASE_URL=https://api.relayfile.dev. <rw_id>: grep the mount's.relay/state.jsonfor"workspaceId".- token:
.relay/creds.jsontoken(JWT, hasops:read; re-minted ~hourly — re-read it). <opId>: from thewriteback.startlog, or list…/ops?limit=Nand matchpath.- Result:
{status, lastError, attemptCount}.status:failed+attemptCount:0= a permanent (non-retryable) failure that is not dead-lettered and not surfaced — the "silent, no error" signature. - The error ALSO surfaces as the cf audit event
console.info("[relayfile] writeback audit event", {event})in cloud-web-worker (event.metadata.slackError) — but getOp is faster + persisted.
Known failure modes → fix
lastError: missing_scopeon a DM → the Slack app lacksim:write. DM send callsconversations.open(needsim:write) beforechat.postMessage; channels only needchat:write(so channels work, DMs don't).missing_scopeisn't retryable (providers/slack.ts:476-485) → permanent_failure → silent. Fix = 3 places, in order: (1) Slack app → Bot Token Scopes → addim:write(+mpim:write) → reinstall; (2) Nango dashboard → add the same to theslack-relayintegration's requested scopes (the OAuth request set lives here, NOT in the repo — easy to miss); (3) re-OAuth/reconnect (existing tokens get no retroactive grant). Verify: re-fire → getOpsucceeded.- Duplicate posts → the legacy bridge path lacks the cf idempotency guard. Delete the command file after confirmed delivery; migrate the workspace to cf (cloud#1977).
- Silently never sent (looks clean) → on a poll mount the push is ~30s-cadence. The workspace
revisioncounter is GLOBAL (not a dispatch signal); deleting the command file before a push cycle uploads it = silent non-delivery. Confirm dispatch by: operator receipt, OR the file surviving ≥1–2 cycles withpendingWriteback1→0, OR (top-level) the bot echo in the flatmessages/<ts>/tree. Thread REPLIES never echo. - mirror does NOT auto-delete dispatched command files — manual delete-after-send is still required, or a future
clearStatere-dispatches them.
Don't
- Don't conclude from
wrangler tailof the consumer (it only logswriteback.start). - Don't blame adapter version:
@relayfile/adapter-slack0.3.9 already resolves DM→post_dm; cloud#1997 was a READ-side fix. The send path is present + deployed — the gap is almost always scope/config, confirmed via getOp.