Skip to content

Reply from Zendesk only, and drop the Discord reply flow - #51

Merged
Bilb merged 4 commits into
feat/reply-from-zendeskfrom
refactor/reply-from-zendesk-only
Sep 9, 2026
Merged

Reply from Zendesk only, and drop the Discord reply flow#51
Bilb merged 4 commits into
feat/reply-from-zendeskfrom
refactor/reply-from-zendesk-only

Conversation

@Bilb

@Bilb Bilb commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Replies are written as claude: private notes on the ticket. The Comment button on
each 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 logs in front of a real
customer on #27673.

The digest itself is unchanged in content, and is now read-only.

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

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_CHARS is the only real constraint
and MAX_ENTRIES_PER_MESSAGE stays at 10 because that is a readable message. And the
special 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() 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: it carries the agent's
own brief and the exact reply text, and neither is ours to rewrite.

Also

fetch_ticket and fetch_comments move to triage.py, so note_reply no longer
depends on the deleted module.

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.

441 tests pass.

Deploying

systemctl restart zendesk-relay, and drop the /discord/interactions location from
the live nginx config — leaving it only means a 404 on a path nothing calls. The four
dead settings can come out of /etc/zendesk/env whenever.

Bilb added 4 commits September 4, 2026 14:56
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 mpretty-cyro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • pynacl may no longer be needed now that the Ed25519 and nacl imports have been removed

Comment thread zendesk_triage/triage.py
Comment thread zendesk_triage/note_reply.py
@Bilb
Bilb added this pull request to stack #53 September 9, 2026 01:40
@Bilb
Bilb merged commit f887ff5 into main Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants