Repository navigation
feat(datasource): reply to a Pylon issue from Forest on the customer's channel [PRD-1873] - #420
Open
christophebrun-forest wants to merge 7 commits into
Conversation
Client::Writes gains reply_to_issue and create_issue_note, posting to
/issues/{id}/reply and /issues/{id}/note with the id escaped. Both get
their own 30/min window in RateLimits instead of the shared fallback.
fetch_complete_issue_messages reads the thread for a caller acting on
it: a failed or truncated read raises instead of degrading to nil, so
an unreadable thread is never taken for one with no customer message.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… [PRD-1873] Each embedded message now carries email_from, email_to and email_cc, flattened from Pylon's email_info and null outside email. A reply to an email issue must name its recipients, which Pylon does not infer. Blind copies and the RFC 5322 message id header stay out. Also corrects the thread endpoint budget in the MessagesEmbedder comment (120/min). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Answers a Pylon issue from Forest on the channel the customer used, or posts an internal note. The channel is the source of the first customer-visible message; the reply targets the latest message on that channel. Email recipients are pre-filled from that message, support addresses left out, since Pylon does not infer them. The form reads the thread once per render. The executor reads it again, strictly: an unreadable thread is reported as such, never as an issue without customer message. Pylon 4xx reach the operator as the action error. Replies are signed by the API token account for now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With post_as_operator, a public reply passes the user_id of the active Pylon user whose email or emails match the Forest caller email, case insensitive. No match, several matches or a caller without email refuse the reply and send nothing: falling back to the token would sign some replies in the operator name and others not, out of their sight. Internal notes stay signed by the token account. Off by default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Options, the channel rule, how email recipients are pre-filled, who signs a reply, and what to know before granting the action: the issue_id_field column is the authority, and post_as_operator trusts the emails shared between Forest and Pylon. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…873] - Recipients: the sender of the latest email joins To only when the customer wrote it, and the addresses the team wrote from in the thread are left out even when support_emails misses them. - An internal note no longer needs the thread, so an unreadable thread still lets the operator leave one. - A reply or note answered 2xx is taken as delivered whatever its body, and a non-4xx failure asks the operator to check Pylon before sending again instead of surfacing a bare 500. - The per-render thread cache holds its state strongly: WeakMap values could be collected mid-render. - An emptied rich-text editor (<p><br></p>) is refused as empty. - The operator lookup asks Pylon for active users only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Coverage Impact ⬆️ Merging this pull request will increase total coverage on Modified Files with Diff Coverage (8) 🛟 Help
|
christophebrun-forest
marked this pull request as ready for review
October 30, 2026 15:22
…D-1873] - Answering a customer email writes to its sender only: the customer's own To holds the support inbox, which nothing else names before the team's first email when support_emails is not set. - The support and team addresses are also removed from what the operator types in To and CC, and CC is deduplicated against To. - An email_info that is not an object no longer raises out of the thread lookup. - The per-render state is kept on the render context rather than in a thread-local slot that outlived the request. - A literal non-breaking space or zero-width character no longer passes as a message, and a blank Pylon user name falls back to the email. - issue_messages_path is private; the README states what a submit costs in thread reads, the agent rendering the form again before executing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 new issues
|
| support_emails: normalize_emails(options[:support_emails]) | ||
| } | ||
|
|
||
| collection_customizer.add_action((options[:action_name] || NAME).to_s, build_action(datasource, opts)) |
| note(datasource, issue_id, body) | ||
| end | ||
| result_builder.public_send(status, message: message) | ||
| end |
| text.gsub(/[[:space:]\u200B-\u200D\u2060\uFEFF]/, '').empty? | ||
| end | ||
|
|
||
| def reply(datasource, state, body, context, opts) |
| payload['user_id'] = operator.user['id'] if operator | ||
| write(state.issue_id, 'reply', reply_success(state, payload, operator&.user)) do | ||
| datasource.client.reply_to_issue(state.issue_id, payload) | ||
| end |
| opts[:allow_internal_note] ? "#{no_target} Post an internal note instead." : no_target | ||
| end | ||
|
|
||
| def recipients_field(label, state, opts, side) |
|
|
||
| Result.new(error: refusal(email, matches.size)) | ||
| rescue APIError => e | ||
| Result.new(error: "Could not read the Pylon users to sign the reply in your name: #{e.message}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What
Adds a
ReplyToIssueaction plugin toforest_admin_datasource_pylon. An operator can answer a Pylon issue from Forest, and the reply reaches the customer on the channel they used: Slack stays Slack, email stays email. The operator can post an internal note instead.Linear: PRD-1873
How
Pylon keeps the channel:
POST /issues/{id}/replydelivers the reply into the conversation of the message named bymessage_id. The plugin picks that message and, for email, the recipients, which Pylon does not infer.ReplyTarget): the channel is thesourceof the first customer-visible message. The reply targets the latest customer-visible message on that channel. With no customer-visible message, only an internal note is offered.support_emailsand the addresses the team wrote from in the thread are left out, from the pre-fill and from what the operator types.FormBuilder): reply type, channel (read-only), To and CC (email only), rich-text message. The thread is read once per render.fetch_complete_issue_messages, which raises instead of degrading tonil, so an unreadable thread is never taken for an issue without a customer message;post_as_operator(off by default):user_idof the active Pylon user whose email matches the Forest caller's;reply_to_issueandcreate_issue_note, each with its own 30/minRateLimitsrule. A 2xx is taken as delivered whatever its body.email_from,email_toandemail_cc.Reviewer notes
ObjectSpace::WeakMaplets values be collected mid-render, andWeakKeyMapneeds Ruby 3.3 while the gem supports 3.0.messageslist shows an unrelated front error, tracked in PRD-1875.Testing
bundle exec rspecin the package: 902 examples, 0 failures, 100% line coverage.bundle exec rubocop: clean.🤖 Generated with Claude Code
Note
Add ReplyToIssue plugin to reply to Pylon issues from Forest
ReplyToIssueplugin that registers a single-record action on the issue collection. It posts a public reply to the issue's resolved conversation target, or an internal note when chosen or no reply target exists.nilon failure. Embedded messages now includeemail_from,email_to, andemail_cc.Macroscope summarized c4da6bd.