Skip to content

Releases: rameerez/support_desk

0.3.2

Choose a tag to compare

@rameerez rameerez released this 24 Sep 02:09
f82904b

Fixed

  • Persist message-registration receipts atomically with clocks and turn revisions.
    A new message with an earlier timestamp or smaller UUID is no longer dropped
    as a replay. It invalidates the turn and triggers requester notifications and
    handoff detection once, without rewinding clocks. This also fixes human-only
    desks; no assistant configuration is needed to encounter the old defect.
  • Recovery discovers unregistered replies on closed cases and honors the desk's
    reopen/locked policy. Repair also runs when the assistant is disabled; only
    actual assistant dispatch requires one to be configured.
  • Public assistant assignment, release and reply refuse turn: :current.
    Internal outreach and seat-taking use private paths and actual tokens.
  • Default Chats signatures render assistant messages using per-message
    disclosure snapshots, including old 0.3.0 messages with provenance but no
    assistant-row snapshot. Rename, mode change and flag-off do not erase old
    disclosure. Human replies and approved drafts keep human signatures, and host
    signature overrides remain authoritative. Transcript names are snapshotted
    for new messages; older ones use their stored display name.
  • Correct the README signature override to preserve the human fallback.

Upgrade

Run rails generate support_desk:upgrade and migrate with support writes paused
and all old web/jobs drained
. The new receipt table follows the existing ticket
and message ID types and seeds historical messages up to the old role clocks.
Old clocks cannot reveal lost callbacks behind that baseline: review suspect
historical cases before resuming. Only 0.3.2 processes may write after cutover.
See README for recovery, rollback limitations and the unchanged SQLite caveat.
No gem publication or deployment is performed by this change.

0.3.1 — Assistants hardening

Choose a tag to compare

@rameerez rameerez released this 19 Sep 03:05
ebcf953

Hardening the assistant, after an adversarial review of 0.3.0. Nine defects in the concurrency, recovery and kill-switch guarantees the release advertised — every one now a regression test that failed on 0.3.0. Nothing changes what an assistant may do; it changes what can happen to a case while she is doing it.

  • A customer's next question can no longer be answered around (R1). Every speaking path locks the ticket, then the conversation row chats updates inside every message insert, before reconciling — so an in-flight message blocks the answer until it commits and makes the turn stale. Holds on PostgreSQL and MySQL; on SQLite it is documented and doctor warns.
  • Seats are decided under the lock (R2); assign! / release! take a turn: when by: is an assistant — the one API change.
  • A lost registration is repairable (R3): the fold commits on its own, and redispatch_assistant_turns! looks for unregistered messages before it looks at clocks.
  • Equal-timestamp messages no longer deadlock the turn (R4): the watermark is (created_at, id) in chats' transcript order, one rule in Ruby and SQL.
  • The silent sweep rechecks before it escalates (R5).
  • Stranded seats come back (R6): SupportDesk.reclaim_assistant_seats! / rake support_desk:reclaim_assistant_seats read the seats that exist, with no timeout and no clock; doctor seat checks run whenever assistant rows exist.
  • Running out of turns publishes :ticket_escalated (R7).
  • Disclosure on old messages stops moving (R8): name and mode are snapshotted onto the assistant row.
  • The kill switch is read after waiting for the lock (R9).

Hosts with no assistant are untouched. Gem SHA-256: 0b7bd2310212553a911aa0643ef872aff63aae901a13308b689f709e4dd5b886

0.3.0 — Assistants

Choose a tag to compare

@rameerez rameerez released this 19 Sep 00:14
108eefd

A machine can answer, and a person still owns every word it sends.

An assistant is an agent with a policy: a seat, a name, a turn budget and a level (off · observe · draft · reply · resolve) that says what she may produce on a case. At the default level that is a proposal a human reads and sends. Nothing turns itself on — without config.assistant the gem behaves exactly as 0.2 did.

Highlights

  • The gem owns the guardrails, the host owns the model. No LLM dependency, no prompt, no retrieval, no job loop. A ≤30-line job subscribes to :assistant_turn, reads ticket.brief, calls any model, and hands the answer to ticket.respond!(text, by: rose, turn:). Policy decides what that becomes.
  • The turn. assistant_revision moves on every message and every transition; every assistant action requires and consumes assistant_turn, so a late, retried or redelivered job raises StaleTurn and writes nothing. No idempotency keys, claims or leases needed.
  • Human in the loop. SupportDesk::Draft — one pending proposal per case; Enviar / Editar / Descartar in the console; a sent draft is the human's message with the machine's provenance in metadata. A stale approval is refused, never sent.
  • Two exits, always. escalate! (assistant → humans) and request_human! (the customer's door, POST /tickets/:id/request_human). Humans outrank assistants under every reply_policy.
  • Disclosure is your explicit choice. disclosure is required with no default: :signature_and_notice, :signature, :notice or :none. Exports say from: "assistant" in every mode.
  • A silent assistant never hides a case. rake support_desk:release_silent_assistants releases her seat and asks for a person; redispatch_assistant_turns retries dropped turns; eight new doctor checks.
  • Context as data. ticket.transcript (four roles) and ticket.brief (versioned facts, may/may_not, internal notes opt-in); Requester#support_context.
  • DX. rails g support_desk:assistant Rose --disclosure MODE, SupportDesk::TestHelpers additions, a full README section and reference.

Behaviour changes regardless of configuration

A host acts_as_support_agent kind: :ai model is now refused for every support write; console picker values are actor keys; Queue::TABS gains :needs_human (hidden unless relevant); every registration/transition writes assistant_revision. See the CHANGELOG for the upgrade steps — additive and rolling-safe.

Gem SHA-256: 885d2d97899879c94df47a63ffec810d5c820a229e814d201667dd7e64f75c19

What's Changed

  • System test: wait for the 422 navigation before touching the form by @rameerez in #4
  • README: the whole public surface, as a reference by @rameerez in #5
  • Assistants: AI agents as a first-class use case (0.3.0) by @rameerez in #6

Full Changelog: v0.2.0...v0.3.0

v0.2.0

Choose a tag to compare

@rameerez rameerez released this 17 Sep 21:53
4701814

support_desk 0.2.0 — the desk can write first

lucia.open_support_conversation_with!(alice, "We saw your refund bounced", about: order)
  • New verb, same seam. agent.open_support_conversation_with!(requester, message, about:, topic:) over Ticket.open!(…, by: agent); opened_by is a record like closed_by, with opened_by_requester? / opened_by_support? and scopes.
  • Atomic. The case, the opener's seat (reason: opened, no announcement), the opening line and the first message are one savepoint — a refused message leaves nothing behind. reply! gets the same guarantee.
  • The limits are on asking, not on being asked. open_rate_limit / max_open_tickets count only requester-opened cases.
  • config.opening_line / config.opening_line_from_support — the system line a thread opens with, posted inside the transaction and ordered before the first message by construction. config.find_requester for the console's "Write to someone". has_support_tickets if: — who may ask and be written to; eligibility is checked at every write.
  • Console: new + open_conversation, GlobalID allow-lists, reuse authorization under the row lock, 303/422 with the draft preserved; the door in the mounted and generated consoles.
  • Schema: one additive migration (rails g support_desk:upgrade), rake support_desk:backfill_opened_by, doctor checks.
  • Read the CHANGELOG's "Upgrading from 0.1": this is a drained cutover, not a rolling deploy — 0.1 processes reject the new opened assignment reason.
  • Verified on Rails 7.2 / 8.0 / 8.1 (SQLite) and PostgreSQL; rubocop and brakeman clean.

What's Changed

  • Writing first: the desk can open the conversation (0.2.0) by @rameerez in #3

Full Changelog: v0.1.3...v0.2.0

v0.1.3 — release integrity and correctness fixes

Choose a tag to compare

@rameerez rameerez released this 17 Sep 00:14

Correctness fixes from an integration review against a real host, plus the dependency bump that makes signing work without turning staff into messagers.

Fixed

  • Opening a case and its first message is atomic. A rejected upload no longer leaves a case with nothing in it; the validation error renders with the draft preserved.
  • Reopening a historical case keeps both conversations when a newer case already exists, instead of collapsing them. New submissions still reuse the open case.
  • Assignment authorizes its actor, and assignment, hand-off, release and the reply policy all check current state under the row lock — so two agents racing on the same case cannot both win.
  • Delayed message registration reads both clocks to decide whose turn it is, and can no longer reopen a case that was closed after the message was sent.
  • Reading a console transcript marks the desk's read horizon through the last message actually displayed.
  • Topics resolve within their own ticket's desk, and wizard routing and response promises honour non-default desks.
  • The requester upload form is multipart, and its send budget is shared with chats rather than being a second, separate limit.

Requires chats >= 0.3.2

Authors there need no messaging capabilities of their own, so a staff member signs a desk's reply without being given acts_as_messager. The dependency is ~> 0.3, >= 0.3.2.

Upgrading

No migration, no configuration change. If you are on 0.1.1, upgrade regardless: that package was built from the wrong tree and raises on every requester screen.

Verified

Tests 661 runs, 3004 assertions, 0 failures
Databases SQLite, PostgreSQL
Rails 7.2, 8.0, 8.1
Lint / security rubocop clean, brakeman 0 warnings

What's Changed

  • Fix ticket integrity on support_desk 0.1.2; prepare 0.1.3 by @rameerez in #2

New Contributors

Full Changelog: v0.1.2...v0.1.3

v0.1.2

Choose a tag to compare

@rameerez rameerez released this 16 Sep 19:21

0.1.1 was packaged from the wrong tree. This is the release it should have been.

The gem published to rubygems as 0.1.1 does not declare verified: true on SupportDesk::Desk, so no desk is badged — while its release notes said that was the entire point of the release.

The repository was correct the whole time. The tag, main, and every test all carried the change; only the package did not. That is the worst shape this mistake can take: nothing in git looks wrong, and the suite passes, because the suite runs against the repository rather than against what was shipped.

What changed

  • SupportDesk::Desk declares verified: true, so SupportDesk::Desk.chat_verified? is true and every desk carries the official-account mark in the inbox row, the grouped row and the thread header.

  • A guard test asks the loaded class, not the source file:

    assert SupportDesk::Desk.chat_verified?,
           "SupportDesk::Desk is not verified — if the repo source says it is, " \
           "the loaded gem was built from a different tree"

    Run inside a packaged gem, that reads the packaged file. A build from a stale tree now fails instead of shipping quietly.

If you installed 0.1.1

Upgrade. Nothing else is different, there is no migration and no API change: you get the badge that 0.1.1 promised.

Verified

Tests 647 runs, 2945 assertions, 0 failures
Databases SQLite, PostgreSQL
Rails 7.2, 8.0, 8.1
Lint / security rubocop clean, brakeman 0 warnings
The package itself unpacked and checked: carries verified: true

Requires chats ~> 0.3.

Full Changelog: v0.1.1...v0.1.2

v0.1.1

Choose a tag to compare

@rameerez rameerez released this 16 Sep 19:08

The support desk is an official account.

Every desk now carries the mark that tells a customer the account is really you — in their inbox row, in the grouped row that folds their cases together, and at the top of the thread.

# nothing to write: SupportDesk::Desk declares it
acts_as_messager verified: true, notifications: false, blockable: false, inbox: :grouped

It is real behaviour from chats 0.3.1, not a picture: a scalloped rosette with a tick knocked out, announced to screen readers as "Official account", its colour the --chats-verified custom property, its glyph replaceable with config.verified_badge.

Why it matters for a support desk specifically: the one conversation in someone's inbox that will ask them to confirm a refund, a card, or an address is the one an impostor most wants to sit next to. A mark that says this is us is worth more here than on any other kind of account.

Also in this release

The README leads with the product. Nine screens of a support desk running on a made-up delivery app — the category a DoorDash, an Uber Eats or a Grab occupies, because everyone has needed support from one. Grouped inbox, topic picker, order picker, compose with context, signed reply, agent queue, the case, internal notes, hand-off.

Upgrading

Two things to know:

  1. Requires chats ~> 0.3. 0.2.x has no verified: keyword, so a host resolving an older chats would hit ArgumentError at class definition, before SupportDesk.doctor could explain anything. The dependency is tightened so bundler says it first.
  2. Nothing else changed. No migration, no configuration, no API. Upgrade and your desk is badged.

Verified

Tests 646 runs, 2944 assertions, 0 failures
Databases SQLite, PostgreSQL
Rails 7.2, 8.0, 8.1
Lint / security rubocop clean, brakeman 0 warnings

Run against the published chats 0.3.1, not a local checkout.

Full detail in CHANGELOG.md.

Full Changelog: v0.1.0...v0.1.1

v0.1.0

Choose a tag to compare

@rameerez rameerez released this 16 Sep 04:57

Customer support for any Rails app: tickets that are real conversations.

Somebody asks for help about something in your app. A desk answers. Your staff sign their replies. Your team works a queue. All of it on threads your users already understand, because it is the same messaging they use for everything else.

support_desk is a product gem on the chats kernel: chats owns the transcript, realtime, attachments, read state and moderation; this gem owns everything a case needs on top. The same shape as usage_credits on wallets.

class User < ApplicationRecord
  acts_as_messager                      # chats
  has_support_tickets                   # the person asking
  acts_as_support_agent if: :admin?     # the person answering
end

class Order < ApplicationRecord
  supportable topic: :order             # "I need help with this order"
end

ticket = alice.ask_support!("It never arrived", about: order)
ticket.assign!(to: lucia)
ticket.reply!("Looking into it now", by: lucia)
ticket.close!(by: lucia)

What ships in 0.1.0, and what each is for

Cases that are conversations

  • A ticket is one chats conversation, with the ticket as its subject. One transcript, one read horizon, one realtime story, one moderation contract.
  • Why: assignment, response clocks and audit are all per case. A single eternal thread per customer cannot answer "who owns this, and how long have they been waiting".

A desk that is not a person

  • The desk sends every reply; humans and bots author them. Your users see one counterpart, "Support", with a signature under each answer.
  • Why: staff come and go and hand cases to each other. Handoffs change who writes, never who the thread is with, and nobody's personal profile is exposed to customers.

An inbox that stays usable

  • Every case with the desk collapses into one row in the user's messages, with an aggregate unread count. Tapping it opens a support-only list.
  • Why: a customer with a dozen past cases should not have their real conversations pushed off the screen. This is a chats primitive, so it also works for shops, organizations or bots.

Topics, as a tree you define in code

  • topic :order, about: Order with pickers, prefill, visibility rules, per-topic routing and priority, and a required free-form exit.
  • Why: a topic decides which of the user's records they can attach, what the wizard asks, and where the case lands. That is behaviour, so it belongs in code, reviewed and versioned.

Assignment as a history

  • Take, assign, hand off with a note, release, plus the full record of who held a case, when, and why they stopped.
  • Why: "time per agent", shift handovers and drop-in cover are unanswerable from a single assignee column.

A console you bring your own UI to

  • Three layers, stop at whichever you like: query objects and presenters that work in any UI; a controller concern plus a routing concern for any admin framework; or rails g support_desk:console madmin, which writes a full console into your app that you own and can edit.
  • A mountable turnkey console ships too, for apps with no admin framework.

Requester screens, included

  • A mounted engine with the list, a three-step wizard, entry points you drop anywhere with link_to_support about: @order, and ejectable views.
  • Hotwire Native path rules included, so the flow behaves on mobile.

Events, not opinions

  • SupportDesk.on(:ticket_opened), :requester_replied, :ticket_transitioned and nine more. Multi-subscriber, error-isolated, emitted after commit.
  • Why: the gem never sends your email, push or Slack. It tells you what happened and stays out of the way. A subscriber that raises is reported and never rolls back a ticket.

Things it refuses to get wrong

  • Authorization both ways. A requester cannot close, refile, reassign or reopen someone else's case, and cannot reach a topic hidden from them by typed path, deep link or a record's own topic. An agent cannot read a desk they are not allowed to see, and the refusal does not leak whether a case exists.
  • Concurrency. "One open case per thing" is enforced by a partial unique index on PostgreSQL and SQLite, so two simultaneous requests produce one case, not two. MySQL has no partial indexes, so there the model plus SupportDesk.doctor hold the line, and the migration says so.
  • Privacy. Internal notes are events, never messages: they never reach the customer's thread and never appear in a data export. Notification titles carry no customer name by default.
  • Idempotency. A redelivered message event never rewinds a clock or reopens a case twice.
  • Replayable state. SupportDesk.doctor checks the invariants the database cannot, and exits non-zero, so a mis-wired desk fails your build rather than a customer's support screen.

Verified

Tests 638 runs, 2904 assertions, 0 failures, 0 skips
Databases SQLite, PostgreSQL
Rails 7.2, 8.0, 8.1
Lint / security rubocop clean, brakeman 0 warnings

Built against the released chats 0.2.0, not a local checkout.

Full detail in CHANGELOG.md and the README.

Full Changelog: https://github.com/rameerez/support_desk/commits/v0.1.0