feat(slack): add writeback resolver — postMessage, thread replies, reactions#33
Conversation
…actions Slack was the only widely-used adapter without a writeback resolver, despite the path table in the setting-up-relayfile skill listing `/slack/channels/<id>/messages/new.json` as a write surface. Round-tripping agent → channel was effectively undocumented and unimplemented; this commit ships the missing piece. ## Routes POST /slack/channels/<channel>/messages/new.json → chat.postMessage POST /slack/channels/<channel>/messages/<msg>/replies/new.json → chat.postMessage with thread_ts POST /slack/channels/<channel>/messages/<msg>/reactions/new.json → reactions.add ## Channel resolution The channel path segment is whatever `path-mapper.namedSegment(name, id)` emitted — either a Slack id (`C01ABC123` / `D…` / `G…`) or a slugified channel name (`customer-success`). Slack's `channel` parameter accepts both forms, so we forward the id verbatim and prefix the name with `#`. Avoids needing a separate metadata lookup. ## Timestamp resolution The message path segment is `path-mapper.messageSegment`'s output, either `<tsToken>` or `<subjectSlug>--<tsToken>`. The token is `messageTs.replace(/\./g, '_')`. We reverse by replacing the *last* underscore with a dot to recover `1234567890.001234`. ## Payload shapes `messages/new.json` and `replies/new.json` accept: - a plain string → posted as `text` - a JSON object → forwards `text`, `blocks`, `attachments`, plus `thread_ts` override, bot-identity overrides (`username`, `icon_emoji`, `icon_url`), unfurl flags, mrkdwn flag, and `reply_broadcast` (thread-only). `reactions/new.json` accepts: - a plain string → emoji name (with or without surrounding colons) - a JSON object with `name` (or `reaction`) ## Test coverage 12 new tests across post_message, reply_in_thread, add_reaction, and unmatched paths. Slack package: 21 tests pass total. Closes the documentation/implementation gap that motivated the slack question on relayfile-adapters#32. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds Slack writeback support: a new resolver maps writeback URL paths to Slack Web API requests (post_message, reactions.add), with payload parsing/validation and overrides. Exposes resolver and types via index; adds tests for writeback behavior and introduces/exports a round-trip-safe channel path mapper. ChangesSlack Writeback Feature
Path Mapping / Package Exports
Sequence DiagramsequenceDiagram
actor Caller
participant Index as "packages/slack/src/index.ts"
participant Writeback as "packages/slack/src/writeback.ts"
participant PathMapper as "packages/slack/src/path-mapper.ts"
participant Types as "packages/slack/src/types.ts"
Caller->>Index: import resolveWritebackRequest
Index->>Writeback: re-exported function
Caller->>Writeback: resolveWritebackRequest(path, content)
Writeback->>PathMapper: parse/extract channel segment (slug--id)
Writeback->>Writeback: decode tsToken/subject, parse payload (string or JSON), validate fields
Writeback->>Types: construct SlackWritebackRequest
Writeback-->>Caller: return SlackWritebackRequest (action, method, endpoint, body)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 654bce8947
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| function extractSlackChannel(segment: string): string { | ||
| const decoded = decodeURIComponent(segment); | ||
| // Slack ids: C (public), G (private/legacy), D (DM) prefix + uppercase alphanumerics. | ||
| if (/^[CDG][A-Z0-9]{7,}$/.test(decoded)) return decoded; | ||
| return decoded.startsWith('#') ? decoded : `#${decoded}`; |
There was a problem hiding this comment.
Preserve channel identity instead of posting to slugified names
When a synced message path was generated with a Slack channel name that contains an underscore, namedSegment() slugifies it by replacing the underscore with - (packages/slack/src/path-mapper.ts:50-60), so an actual channel like customer_success becomes /slack/channels/customer-success/.... This resolver then treats that lossy slug as the Slack channel name and sends channel: '#customer-success'; because Slack channel names may contain underscores, writeback will either fail with channel_not_found or post to the wrong hyphenated channel if one exists. Use the preserved channel ID or a reversible channel segment rather than resolving the display slug as the API target.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 26d960b. Path-mapper now emits <slug>--<channelId> for channel segments via a new channelSegment(channelName, channelId) helper (mirrors the Notion / Linear round-trip pattern from #32). extractSlackChannel extracts the canonical id from the --<id>$ suffix first, then bare canonical id, then falls back to #<slug> (best-effort, limitation explicitly documented in JSDoc). Both buildPostMessage and buildAddReaction also honor an explicit channel field in the JSON payload as override — the escape hatch for paths that haven't been re-synced. New tests cover the <slug>--<id> extraction and the payload-override path.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/slack/src/writeback.ts`:
- Around line 167-169: The code only sets body.reply_broadcast when the
URL-derived threadTs is present, so reply_broadcast is ignored if the request
includes thread_ts only in the JSON payload; update the condition around
replyBroadcast (and the assignment to body.reply_broadcast) to check for a
thread timestamp coming from either source (the existing threadTs variable or
the parsed payload's thread_ts field) — e.g., derive a single truthy
threadTimestamp = threadTs || parsed['thread_ts'] (or use the existing parsed
getter for thread_ts) and use that in the if check so reply_broadcast is applied
whenever a thread timestamp exists in either place.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 87bc2d30-dd23-40b8-9630-9de4cbf9e6c7
📒 Files selected for processing (5)
packages/slack/package.jsonpackages/slack/src/__tests__/writeback.test.tspackages/slack/src/index.tspackages/slack/src/types.tspackages/slack/src/writeback.ts
…d thread_ts Addresses PR #33 feedback. ## P2 — Codex: lossy channel slug breaks writeback for `_`-named channels `namedSegment(name, id)` slugified the channel name (replacing `_` with `-`) and dropped the canonical id, so a synced path for an actual `customer_success` Slack channel emitted `/slack/channels/customer-success/...`. The writeback resolver then forwarded `channel: '#customer-success'` to Slack — either 404'd with `channel_not_found` or silently posted to a different channel that happened to use the hyphenated form. Fix mirrors the Notion / Linear `<slug>--<id>` round-trip pattern: - `path-mapper.ts`: new `channelSegment(channelName, channelId)` emits `<slug>--<channelId>` when a name is available, else falls back to the normalized id alone. Used by `channelMetadataPath` and `channelMessagesDirectory` (which `messagePath` inherits). - `writeback.ts`: `extractSlackChannel` now extracts the canonical id from `--<id>$` segments first, then bare canonical id, then falls back to `#<slug>` (best-effort, with the limitation explicitly documented). - Both `buildPostMessage` and `buildAddReaction` honor an explicit `channel` field in the JSON payload as override — the documented escape hatch for paths that haven't been re-synced into the new form. ## Minor — CodeRabbit: `reply_broadcast` ignored when thread_ts is payload-only The check was on the URL-derived `threadTs`, so a top-level `messages/new.json` with `thread_ts` in the payload had its `reply_broadcast` flag silently dropped. Now we check `body.thread_ts`, which reflects URL + payload override. The `action` field also follows effective thread state — a top-level path with payload `thread_ts` returns `reply_in_thread`, not `post_message`. ## Tests - channel id extraction from `<slug>--<id>` form (post_message + add_reaction) - `channel` payload override on both routes - `reply_broadcast` honored when `thread_ts` comes from payload - `reply_broadcast` ignored on top-level message with no thread context Slack package: **27 tests pass** (12 existing + 15 new). Typecheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Slack was the only widely-used adapter without a writeback resolver, despite the path table in the setting-up-relayfile skill listing
/slack/channels/<id>/messages/new.jsonas a write surface. Round-trippingagent → Slack channelwas effectively undocumented and unimplemented. This PR ships the missing piece.Routes
POST /slack/channels/<channel>/messages/new.jsonchat.postMessagePOST /slack/channels/<channel>/messages/<msg>/replies/new.jsonchat.postMessagewiththread_tsPOST /slack/channels/<channel>/messages/<msg>/reactions/new.jsonreactions.addChannel resolution
The
<channel>segment is whateverpath-mapper.namedSegment(name, id)emitted — either a Slack id (C01ABC123/D…/G…) or a slugified channel name (customer-success). Slack'schannelparameter accepts both forms, so we forward the id verbatim and prefix the name with#. No metadata lookup needed.Timestamp resolution
The
<msg>segment ispath-mapper.messageSegment's output — either<tsToken>or<subjectSlug>--<tsToken>. The token ismessageTs.replace(/\./g, '_'). The resolver reverses by replacing the last underscore with a dot to recover1234567890.001234.Payload shapes
messages/new.jsonandreplies/new.jsonaccept:texttext,blocks,attachments, plusthread_tsoverride, bot-identity overrides (username,icon_emoji,icon_url), unfurl flags,mrkdwn, andreply_broadcast(thread-only).reactions/new.jsonaccepts:name(orreaction)Test coverage
12 new tests covering
post_message,reply_in_thread,add_reaction, and unmatched paths. Slack package: 21 tests pass total, typecheck clean.Test plan
chat.postMessagewriteback against a sandbox workspace<msg>/replies/new.jsoncorrectly reconstructsthread_tsreactions.addwith bare emoji name works🤖 Generated with Claude Code