Reply from Zendesk only, and drop the Discord reply flow - #51
Merged
Conversation
An em dash is the clearest tell that a reply was machine-written, and no reply this team has sent uses one. Both prompts that write customer-facing text now forbid them: the composer in note_reply.py and the translator in reply.py. A prompt rule is advisory and this text emails a real person, so triage.undash is the guarantee. The spaced form is punctuation and becomes a comma; anything left is joining two things, like a range, and becomes the hyphen a person would have typed. It lives in triage.py because note_reply imports reply, so neither could hold it without a copy or a cycle. Never applied to reply_en. On an English ticket reply.py sends the agent's own text verbatim, and rewriting someone's punctuation would be wrong — that field is not even in TRANSLATION_PROPERTIES, so it cannot reach the validator. The three internal prompts are left alone: placement returns a key, and the digest and transcript are written for agents, where a dash is just punctuation.
Replies are written as `claude:` private notes on the ticket. The Comment button on each digest card was a second way in with opposite semantics for the same words, and it had already put "claude draft thanks for sending the logs" in front of a customer on #27673. Removed: reply.py and its tests, the /discord/interactions route, the Ed25519 signature check, the compose modal, the preview embeds and the Send button, the allowlists, and the DISCORD_PUBLIC_KEY / ALLOWED_USER_IDS / ALLOWED_ROLE_IDS / DISCORD_GUILD_ID settings. relay.py goes from 709 lines to 197 and serves one route. The digest is unchanged in content and now read-only: one Text Display per ticket instead of a Section wrapping a button. Two things follow. The 40-component limit no longer binds, since a ticket costs one component rather than three, so MAX_MESSAGE_TEXT_CHARS is the only real constraint and MAX_ENTRIES_PER_MESSAGE stays at 10 for readability. And the special case that kept the collapsed abuse line from getting a button pointing at an arbitrary member of the set is gone with the buttons. fetch_ticket and fetch_comments move to triage.py so note_reply no longer depends on the deleted module. Long dashes are now impossible in a note this tool writes, not merely discouraged: para() and bold_para() run triage.undash, which covers the templates, the model's prose and the house-answer file alike. verbatim() is exempt on purpose, since it carries the agent's own brief and the exact reply text, and neither is ours to rewrite. Coverage is kept rather than deleted: test_relay.py is now the webhook gate plus direct tests for ticket_number and dry_run_requested, which were only exercised indirectly before, including the systemd inline-# trap that a plain equality check reads backwards.
…eader - run_reply writes the note carrying the done marker BEFORE publishing the reply. Sent the other way round, a failure between the two left the customer emailed and the command unclaimed, and the next run emailed them again, which is the one thing the marker exists to prevent. The cost of this order is the opposite and much smaller: a failed public comment leaves a note saying a reply went out when none did, and the run exits non-zero saying so. - load_house refuses a file with no `groups`. place_ticket and run_explain read book["groups"] unguarded, so a half-written file raised there instead of degrading here. Cells missing answer/n/consistency are dropped rather than failing the file, since a truncated cell should cost its own group's grounding and not everyone else's. - zendesk_signature_ok reads an offset-less timestamp as UTC. Left naive, .timestamp() read it as the host's local time, so the age was wrong by the UTC offset: a stale request accepted, or every fresh one refused, on any box not set to UTC. - The note webhook answers 400 rather than 500 for a body that is valid JSON but not an object. A bare number, string or list has no .get, and the resulting AttributeError was caught by neither ValueError nor TypeError. - "no public comments to translate" no longer tags claude-error. It is the command working and finding nothing to do, like the two branches either side of it, and claude-error is meant to be the queue of broken tickets. - The digest header reports skipped_quiet. It was recorded and never rendered, and it is usually the largest of the three skip counts: 56 of 79 on a real 72h window. Unreported, the digest looked like it analyzed a fraction of its fetch for no stated reason. - write_to_ticket's docstring described the additional_tags mechanism it stopped using when tags moved to the sub-resource. Docs: the tag table gains claude-solved and says which tags are never cleared and why; the removed Discord reply path is described in the past tense; the deploy guide documents ZENDESK_HOUSE_ANSWERS, which was configurable and undocumented; and the smoke test says the throwaway ticket needs a public comment, since `claude: english` has nothing to translate without one.
mpretty-cyro
requested changes
Sep 8, 2026
mpretty-cyro
left a comment
Collaborator
There was a problem hiding this comment.
-
pynaclmay no longer be needed now that the Ed25519 andnaclimports have been removed
Bilb
added this pull request to stack #53
September 9, 2026 01:40
mpretty-cyro
approved these changes
Sep 9, 2026
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.
Replies are written as
claude:private notes on the ticket. The Comment button oneach digest card was a second way in that meant the opposite thing by the same words,
and it had already put
claude draft thanks for sending the logsin front of a realcustomer on #27673.
The digest itself is unchanged in content, and is now read-only.
Removed
reply.pyand its tests, the/discord/interactionsroute, the Ed25519 signaturecheck, the compose modal, the preview embeds and the Send button, the allowlists, and
the
DISCORD_PUBLIC_KEY/ALLOWED_USER_IDS/ALLOWED_ROLE_IDS/DISCORD_GUILD_IDsettings.
relay.pygoes from 709 lines to 197 and serves one route.The digest
One Text Display per ticket instead of a Section wrapping a button. Same header, same
lines, same severity markers, same collapsed abuse line, same chunking and coverage
tracking.
Two things follow. The 40-component limit no longer binds, since a ticket costs one
component rather than three, so
MAX_MESSAGE_TEXT_CHARSis the only real constraintand
MAX_ENTRIES_PER_MESSAGEstays at 10 because that is a readable message. And thespecial case that stopped the collapsed abuse line getting a button pointing at an
arbitrary member of the set is gone with the buttons.
Long dashes
Now impossible in a note this tool writes rather than merely discouraged.
para()andbold_para()runtriage.undash, which covers the templates, the model's prose andthe house-answer file alike.
verbatim()is exempt on purpose: it carries the agent'sown brief and the exact reply text, and neither is ours to rewrite.
Also
fetch_ticketandfetch_commentsmove totriage.py, sonote_replyno longerdepends on the deleted module.
Coverage is kept rather than deleted:
test_relay.pyis now the webhook gate plusdirect tests for
ticket_numberanddry_run_requested, which were only exercisedindirectly before — including the systemd inline-
#trap that a plain equality checkreads backwards.
441 tests pass.
Deploying
systemctl restart zendesk-relay, and drop the/discord/interactionslocation fromthe live nginx config — leaving it only means a 404 on a path nothing calls. The four
dead settings can come out of
/etc/zendesk/envwhenever.