support_desk 0.2.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +197 -0
- data/README.md +1131 -14
- data/app/assets/stylesheets/support_desk.css +10 -0
- data/app/controllers/support_desk/tickets_controller.rb +23 -1
- data/app/helpers/support_desk/engine_helper.rb +16 -0
- data/app/views/support_desk/console/tickets/_actions.html.erb +15 -0
- data/app/views/support_desk/console/tickets/_assignment.html.erb +16 -2
- data/app/views/support_desk/console/tickets/_composer.html.erb +27 -0
- data/app/views/support_desk/console/tickets/_context_card.html.erb +28 -0
- data/app/views/support_desk/console/tickets/_draft.html.erb +114 -0
- data/app/views/support_desk/console/tickets/_message.html.erb +34 -1
- data/app/views/support_desk/console/tickets/_ticket_row.html.erb +20 -0
- data/app/views/support_desk/console/tickets/_timeline.html.erb +68 -14
- data/app/views/support_desk/console/tickets/show.html.erb +5 -0
- data/app/views/support_desk/tickets/_human_door.html.erb +24 -0
- data/app/views/support_desk/tickets/_ticket_row.html.erb +13 -0
- data/config/locales/support_desk.console.en.yml +63 -0
- data/config/locales/support_desk.console.es.yml +65 -0
- data/config/locales/support_desk.en.yml +15 -0
- data/config/locales/support_desk.es.yml +23 -0
- data/config/routes.rb +7 -1
- data/lib/generators/support_desk/assistant_generator.rb +193 -0
- data/lib/generators/support_desk/install_generator.rb +21 -3
- data/lib/generators/support_desk/templates/add_assistants_to_support_desk.rb.erb +236 -0
- data/lib/generators/support_desk/templates/assistant/service.rb.erb +60 -0
- data/lib/generators/support_desk/templates/assistant/turn_job.rb.erb +72 -0
- data/lib/generators/support_desk/templates/assistant/turn_job_test.rb.erb +69 -0
- data/lib/generators/support_desk/templates/create_support_desk_message_registrations.rb.erb +42 -0
- data/lib/generators/support_desk/templates/initializer.rb +33 -0
- data/lib/generators/support_desk/upgrade_generator.rb +25 -7
- data/lib/support_desk/assistant_policy.rb +213 -0
- data/lib/support_desk/brief.rb +283 -0
- data/lib/support_desk/configuration.rb +552 -2
- data/lib/support_desk/console.rb +290 -8
- data/lib/support_desk/context_card.rb +10 -1
- data/lib/support_desk/doctor.rb +171 -1
- data/lib/support_desk/engine.rb +16 -1
- data/lib/support_desk/errors.rb +37 -0
- data/lib/support_desk/events.rb +12 -5
- data/lib/support_desk/macros.rb +14 -1
- data/lib/support_desk/message_signatures.rb +20 -0
- data/lib/support_desk/models/assistant.rb +188 -0
- data/lib/support_desk/models/concerns/requester.rb +18 -0
- data/lib/support_desk/models/desk.rb +26 -3
- data/lib/support_desk/models/draft.rb +275 -0
- data/lib/support_desk/models/event.rb +15 -1
- data/lib/support_desk/models/message_registration.rb +12 -0
- data/lib/support_desk/models/ticket/assistance.rb +899 -0
- data/lib/support_desk/models/ticket.rb +406 -58
- data/lib/support_desk/outcome.rb +38 -0
- data/lib/support_desk/queue.rb +41 -5
- data/lib/support_desk/test_helpers.rb +152 -0
- data/lib/support_desk/timeline.rb +40 -11
- data/lib/support_desk/topic.rb +22 -0
- data/lib/support_desk/topic_tree.rb +7 -1
- data/lib/support_desk/transcript.rb +234 -0
- data/lib/support_desk/version.rb +1 -1
- data/lib/support_desk.rb +151 -0
- data/lib/tasks/support_desk.rake +57 -0
- metadata +33 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 66ba1a32f86acca1b4068bbca4091fcb16c4e6840a9d57ae5dc44bea25572ea2
|
|
4
|
+
data.tar.gz: 2433e492636e42eeffde1652a2c7d21915267c6f0a9180bea8bb010d902cdfad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac1246120f4c2aa2763f81866006f054e0dfd14901d42120ddc25d754b08dad350b1627552faaa9e4fd62d37a50d785eeae9abe6dab1c5dc9adbb9f9bb44acd1
|
|
7
|
+
data.tar.gz: 66fca7c4ddad8c9b41358448ec24beda2a3dcd9bade8bd373fe7433a565700d500c212f863c1755048faa7dd803c79c1fa92d3fb0b029a072322d1eff6672994
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,203 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.3.2] - Unreleased
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Persist message-registration receipts atomically with clocks and turn revisions.
|
|
12
|
+
A new message with an earlier timestamp or smaller UUID is no longer dropped
|
|
13
|
+
as a replay. It invalidates the turn and triggers requester notifications and
|
|
14
|
+
handoff detection once, without rewinding clocks. This also fixes human-only
|
|
15
|
+
desks; no assistant configuration is needed to encounter the old defect.
|
|
16
|
+
- Recovery discovers unregistered replies on closed cases and honors the desk's
|
|
17
|
+
reopen/locked policy. Repair also runs when the assistant is disabled; only
|
|
18
|
+
actual assistant dispatch requires one to be configured.
|
|
19
|
+
- Public assistant assignment, release and reply refuse `turn: :current`.
|
|
20
|
+
Internal outreach and seat-taking use private paths and actual tokens.
|
|
21
|
+
- Default Chats signatures render assistant messages using per-message
|
|
22
|
+
disclosure snapshots, including old 0.3.0 messages with provenance but no
|
|
23
|
+
assistant-row snapshot. Rename, mode change and flag-off do not erase old
|
|
24
|
+
disclosure. Human replies and approved drafts keep human signatures, and host
|
|
25
|
+
signature overrides remain authoritative. Transcript names are snapshotted
|
|
26
|
+
for new messages; older ones use their stored display name.
|
|
27
|
+
- Correct the README signature override to preserve the human fallback.
|
|
28
|
+
|
|
29
|
+
### Upgrade
|
|
30
|
+
|
|
31
|
+
Run `rails generate support_desk:upgrade` and migrate **with support writes paused
|
|
32
|
+
and all old web/jobs drained**. The new receipt table follows the existing ticket
|
|
33
|
+
and message ID types and seeds historical messages up to the old role clocks.
|
|
34
|
+
Old clocks cannot reveal lost callbacks behind that baseline: review suspect
|
|
35
|
+
historical cases before resuming. Only 0.3.2 processes may write after cutover.
|
|
36
|
+
See README for recovery, rollback limitations and the unchanged SQLite caveat.
|
|
37
|
+
No gem publication or deployment is performed by this change.
|
|
38
|
+
|
|
39
|
+
## [0.3.1] - 2026-09-19
|
|
40
|
+
|
|
41
|
+
**Hardening the assistant, after an adversarial review of 0.3.0.** Nine
|
|
42
|
+
defects, every one of them in the concurrency, recovery and kill-switch
|
|
43
|
+
guarantees the release advertised rather than in what a green suite
|
|
44
|
+
covered. Nothing here changes what an assistant may do; it changes what
|
|
45
|
+
can happen to a case while she is doing it. `assign!` and `release!` take
|
|
46
|
+
a `turn:` when `by:` is an assistant — the one API change — and a host
|
|
47
|
+
with no assistant is untouched.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **A customer's next question can no longer be answered around (R1).**
|
|
52
|
+
Reconciliation is a `SELECT`, and a requester never takes the case's row
|
|
53
|
+
lock, so a question could commit between the answer reading the case and
|
|
54
|
+
the answer writing to it: she replied to the previous one, and a human
|
|
55
|
+
approved a proposal from a page that predated the new one — `seen_turn`
|
|
56
|
+
was not evidence that the reviewer had seen the last committed message.
|
|
57
|
+
Every speaking path now takes the ticket's row lock and then the
|
|
58
|
+
conversation row chats updates inside every message insert, before
|
|
59
|
+
reconciling. The lock order is ticket → conversation everywhere, and it
|
|
60
|
+
is written down. **The guarantee is a row lock, so it holds on PostgreSQL
|
|
61
|
+
and MySQL and not on SQLite**, which has no row locks and whose WAL
|
|
62
|
+
snapshot reads straight through an open write transaction: a desk with an
|
|
63
|
+
assistant belongs on PostgreSQL or MySQL, and `doctor` now warns when it
|
|
64
|
+
is not.
|
|
65
|
+
- **A seat is decided under the lock (R2).** `assign!` asked whether
|
|
66
|
+
anybody held the case, and whether she could hold it, *before* the
|
|
67
|
+
transition acquired the row. No threads were needed to break it: load an
|
|
68
|
+
unassigned case into one instance, give it to a person through another,
|
|
69
|
+
and the first instance took the seat from them. A topic cap or a pause
|
|
70
|
+
committed while a take waited for the lock was read too late in the same
|
|
71
|
+
way.
|
|
72
|
+
- **A lost registration is repairable (R3).** A requester message that
|
|
73
|
+
committed while the process that should have registered it died left a
|
|
74
|
+
turn nothing could ever consume: reconciliation happened inside the
|
|
75
|
+
candidate answer's savepoint, the bumped revision made that very answer
|
|
76
|
+
stale, and the `StaleTurn` rolled the repair back with it — for ever,
|
|
77
|
+
while the clocks said `awaiting_requester` so no idle query could see the
|
|
78
|
+
case. The fold is now its own committed step, and
|
|
79
|
+
`redispatch_assistant_turns!` looks for unregistered messages before it
|
|
80
|
+
looks at clocks.
|
|
81
|
+
- **Messages that share an instant no longer deadlock the turn (R4).** The
|
|
82
|
+
unseen-message test was "its id is not the one the clock was set from",
|
|
83
|
+
so every other message on that timestamp read as new: reconciliation
|
|
84
|
+
folded an already-registered one in again, bumped the revision, and made
|
|
85
|
+
the answer that discovered it stale. The retry did the same thing. "Seen"
|
|
86
|
+
is now the pair `(created_at, id)` compared in chats' own transcript
|
|
87
|
+
order, by the same rule in Ruby and in SQL.
|
|
88
|
+
- **The silent sweep rechecks before it escalates (R5).** Its predicates
|
|
89
|
+
were true when it SELECTED its candidates. A person who answered, took
|
|
90
|
+
the case or reset the clock in between lost anyway: the case was marked
|
|
91
|
+
human-required, its priority went up, and the customer was told a person
|
|
92
|
+
was coming — on a case that already had one.
|
|
93
|
+
- **Stranded seats come back (R6).** `deactivate!` promised the sweep would
|
|
94
|
+
release her cases; the sweep only ever visited configured assistants with
|
|
95
|
+
a `responds_within`, and only cases already overdue. An assistant with no
|
|
96
|
+
promise kept her seat for ever, and one removed by the documented kill
|
|
97
|
+
switch — which takes her configuration away — was not visited at all,
|
|
98
|
+
while the doctor's assistant checks returned early on exactly that state.
|
|
99
|
+
`SupportDesk.reclaim_assistant_seats!` (and `rake
|
|
100
|
+
support_desk:reclaim_assistant_seats`) reads the seats that exist.
|
|
101
|
+
- **Running out of turns tells the host (R7).** The budget branch wrote the
|
|
102
|
+
`escalated` event, released her seat and posted the public hand-off line,
|
|
103
|
+
but only the outer `escalate!` published `:ticket_escalated` — so a
|
|
104
|
+
host's "a person is needed here" notifier never heard about a
|
|
105
|
+
conversation that ran out of turns. One private method now publishes it
|
|
106
|
+
for every hand-off.
|
|
107
|
+
- **Disclosure on old messages stops moving (R8).** The requester-facing
|
|
108
|
+
signature resolved live configuration, so taking her out of the
|
|
109
|
+
initializer rewrote "— Rose · asistente virtual" into "— Rose" on
|
|
110
|
+
messages nobody had touched. Her name and mode are snapshotted onto her
|
|
111
|
+
own row and read back from it when nothing declares her any more. A
|
|
112
|
+
rename still renames history, deliberately; the README shows the
|
|
113
|
+
`Chats.config.message_signature` lambda for hosts that want each bubble
|
|
114
|
+
frozen.
|
|
115
|
+
- **The kill switch is read after the wait (R9).** Eligibility was checked
|
|
116
|
+
before the case's row lock, and the policy then read `active?` off that
|
|
117
|
+
same stale copy, so an already-seated assistant whose job queued behind a
|
|
118
|
+
busy case could still speak after `deactivate!` had committed. Every verb
|
|
119
|
+
resolves her under the lock now, from the row. The ordinary in-flight
|
|
120
|
+
window — a switch that commits after that read — is documented rather
|
|
121
|
+
than claimed away.
|
|
122
|
+
|
|
123
|
+
### Changed
|
|
124
|
+
|
|
125
|
+
- `Ticket#assign!` and `Ticket#release!` take `turn:`. It is REQUIRED when
|
|
126
|
+
`by:` is an assistant (`ArgumentError` when omitted, `StaleTurn` when
|
|
127
|
+
stale) and ignored for a person, whose calls are unchanged.
|
|
128
|
+
- `SupportDesk.release_silent_assistants!` runs
|
|
129
|
+
`reclaim_assistant_seats!` first, and its count includes what that
|
|
130
|
+
reclaimed.
|
|
131
|
+
- The doctor's assistant invariants run whenever an assistant ROW exists,
|
|
132
|
+
not only while one is configured.
|
|
133
|
+
- `Ticket.with_unregistered_requester_messages` — the scope behind the
|
|
134
|
+
repair half of redispatch.
|
|
135
|
+
|
|
136
|
+
## [0.3.0] - 2026-09-19
|
|
137
|
+
|
|
138
|
+
**A machine can answer, and a person still owns every word it sends.** An
|
|
139
|
+
assistant is an agent with a policy: she has a seat, a name, a turn budget
|
|
140
|
+
and a level that says what she may produce here — and at the default level
|
|
141
|
+
that is a proposal a human reads and sends. Nothing in this release turns
|
|
142
|
+
itself on. Without `config.assistant` the gem behaves exactly as 0.2 did.
|
|
143
|
+
|
|
144
|
+
### Added
|
|
145
|
+
|
|
146
|
+
- `config.assistant :rose do |rose| … end` — one block per assistant, with `name`, `avatar`, `autonomy`, `disclosure`, `max_turns`, `responds_within`, `may_open_conversations`, the three system lines (`hand_off_line`, `human_requested_line`, `disclosure_line`) and the two hooks (`hand_off_when`, `cap`). `config.assistants`, `config.assistant?(key)`, `config.default_assistant`, and `desk.assistant = :rose` (an explicit `nil` disables one desk rather than inheriting).
|
|
147
|
+
- **`disclosure` is required and has no default.** Four modes — `:signature_and_notice`, `:signature`, `:notice`, `:none` — and boot fails until one is chosen. Whether a customer is told they are talking to a machine is a legal decision in a jurisdiction the gem knows nothing about.
|
|
148
|
+
- `SupportDesk::AssistantPolicy` — what she may do on ONE case, right now, and why. `LEVELS` (`off observe draft reply resolve`), `VERBS_BY_LEVEL`, `level` / `because` / `ceilings` / `floors`, `may?` / `may_observe?` / `may_draft?` / `may_reply?` / `may_hold?` / `may_close?`, `allowed_verbs` / `forbidden_verbs` / `to_h`. Ceilings lower what she may ever produce (her autonomy, the topic's cap, the host's `cap` block, the case's cap, a pause); floors lower it because of the case's state. `because` names the one rule that decided it.
|
|
149
|
+
- `topic :payments, assistant: :draft` — a topic cap, and it only ever tightens: `Topic#assistant_cap` is the minimum over the node and every ancestor.
|
|
150
|
+
- `SupportDesk::Assistant` — the record a message can be authored by, an assignment can point at and an audit log can name. `SupportDesk.assistant(key = nil)`, `SupportDesk.reset_assistants!`, `SupportDesk.ai_actor?(record)`; `deactivate!(by:)` / `activate!(by:)` is the cross-process kill switch, read fresh from the row before every write.
|
|
151
|
+
- `Ticket#respond!(body, by:, turn:, …)` — the one verb a harness needs. Policy decides whether it is sent, proposed as a draft, or withheld with the reason on the record; the answer is a `SupportDesk::Outcome` (`sent?` / `drafted?` / `withheld?` / `escalated?` / `turn`).
|
|
152
|
+
- `Ticket#draft!`, `Ticket#escalate!(reason:, summary:)`, `Ticket#request_human!(by: requester)`, `Ticket#pause_assistant!` / `#resume_assistant!`.
|
|
153
|
+
- **The turn.** `assistant_revision` is an integer bumped by every registered message and every transition; `ticket.assistant_turn` spells it, and EVERY assistant action requires it and consumes it. A late, retried or redelivered action raises `SupportDesk::StaleTurn` and writes nothing — which is why this gem has no idempotency keys, claim rows or leases.
|
|
154
|
+
- `SupportDesk::Draft` — a reply she proposes and a person sends. `send!(by:, seen_turn:, body:)` posts it as the HUMAN's message (they read it, they own it, the requester sees their signature) and keeps the original; `reject!(by:, reason:)` records why, which is the number that tells you whether she is ready for a higher level. `stale?`, `edited?`, `final_body`, `confidence_percent`, and the `reviewed` / `verbatim` / `edited` scopes.
|
|
155
|
+
- `SupportDesk::Transcript` — the conversation as ordered turns (`role`, `name`, `body`, `at`, `attachments`, `assisted`), with four roles rather than the clocks' three. `to_a` / `to_h` / `to_text` / `last(n)` / `since(message)` / `size`, and `ticket.transcript(limit:)`.
|
|
156
|
+
- `SupportDesk::Brief` — everything a machine needs to answer one case, as versioned data. `ticket.brief(include_internal: false, transcript_limit: 50)`, `to_h` / `to_text` / `policy`. Facts only: `may` / `may_not` are the authorization, and there is not one imperative sentence in it.
|
|
157
|
+
- `Requester#support_context` — key/value pairs about the PERSON, the way `Supportable#support_context` is pairs about the thing they are asking about. Empty by default; rendered by `ContextCard#requester_pairs` and included in the brief.
|
|
158
|
+
- Readers and scopes for the new state: `Ticket#assistant`, `#assistant_policy`, `#assistant_turn`, `#assistant_in_play?`, `#held_by_assistant?`, `#human_required?`, `#assistant_paused?`, `#assistant_turns_left`, `#assistant_message?(message)`, `#drafts` / `#pending_draft`; and `held_by_assistants`, `held_by_humans`, `needs_human`, `assistant_paused`, `assistant_capped`, `resolved_by_assistant`, `with_pending_draft`, `assistant_idle_since(time)`. `Desk#assistant` / `#assistant?` name the one that works a desk.
|
|
159
|
+
- Console: `send_draft`, `reject_draft`, `pause_assistant`, `resume_assistant`, the proposal card (Enviar / Editar / Descartar) with the confidence pill, the sources and a staleness warning, the helpers `support_pending_draft` and `support_assistant`, `"no_pending_draft"` as an `unavailable_reason`, and the `needs_human` queue tab (`Queue#needs_human`, `#visible_tabs`).
|
|
160
|
+
- Requester engine: `POST /tickets/:id/request_human`, the `support_desk/tickets/_human_door` partial and the `support_human_door(ticket)` helper. The door is offered whenever an assistant is or has been in play on the case — history counts, so it can never vanish because somebody edited an initializer after she answered.
|
|
161
|
+
- Events: `assistant_turn` (the only one a harness subscribes to; it carries the turn), `draft_proposed`, `draft_sent`, `draft_rejected`, `assistant_withheld`, `ticket_escalated`, `human_requested`, `assistant_paused`, `assistant_resumed`. The matching `Event::KINDS` are new too; the five internal ones join `note` and `drop_in` in `Event::INTERNAL_KINDS`, which is what `Event.requester_visible` excludes, and `Event#summary` reads the paragraph an escalation left.
|
|
162
|
+
- Spanish and English copy for all of it — the system lines, the console's proposal card and flashes, the queue's new tab and the requester's door — with the suite asserting both files carry the same keys.
|
|
163
|
+
- Errors: `SupportDesk::NotAnAssistant`, `SupportDesk::AssistantNotAllowed` (carrying the `policy` and the `verb`), `SupportDesk::StaleTurn`.
|
|
164
|
+
- `rake support_desk:release_silent_assistants` (every minute) and `rake support_desk:redispatch_assistant_turns` (every five) — the net under a dead harness and the at-least-once retry of a dropped turn. `rake support_desk:assistant_status` reads and writes nothing.
|
|
165
|
+
- Doctor: `assistants (config)`, `assistant turn subscriber`, `assistant authorship`, `assistant silence`, `assistant seats`, `assistant idle turns`, `drafts`, `ai agents without policy`.
|
|
166
|
+
- `SupportDesk::TestHelpers`: `support_assistant`, `respond_as`, `draft_as`, `assert_pending_draft` / `refute_pending_draft`, `assert_needs_human` / `refute_needs_human`, `assert_held_by_assistant`, `refute_assistant_spoke`, `assert_assistant_policy`, `with_assistant_config`, `with_topic_assistant_cap`.
|
|
167
|
+
- Generators: `rails g support_desk:assistant Rose --disclosure MODE [--desk KEY] [--autonomy LEVEL]` writes the turn job, the service and a job test, and PRINTS the initializer stanza, the subscription and the two scheduler lines. It never edits an initializer, never overwrites a file and copies no locales. `install` and `upgrade` both copy the additive assistants migration.
|
|
168
|
+
|
|
169
|
+
### Changed — regardless of configuration
|
|
170
|
+
|
|
171
|
+
These land whether or not you declare an assistant:
|
|
172
|
+
|
|
173
|
+
- **A host model declared `acts_as_support_agent kind: :ai` is now refused for every support write, by it or to it** (`NotAnAssistant`). It used to be treated as a human. `kind:` is validated at declaration (`:human` or `:ai`), and `SupportDesk.doctor` warns about such classes. Only the desk's own `SupportDesk::Assistant` has machine authority.
|
|
174
|
+
- **Console picker values are `SupportDesk.actor_key(agent)`**, not bare ids — an assistant and a user can share an integer id. A bare id is still accepted for one release, and only when exactly one pool member matches it.
|
|
175
|
+
- `Queue::TABS` gains `:needs_human`. It is hidden unless the desk has an assistant or the count is non-zero, so no existing queue grows a tab it has no use for.
|
|
176
|
+
- Every registration and every transition writes `assistant_revision`. Additive; a 0.2 process reading those rows is unaffected.
|
|
177
|
+
- The schema adds `support_desk_assistants` and `support_desk_drafts`, and nine columns on `support_desk_tickets` — two defaulted integers (`assistant_revision`, `assistant_turns_count`), the rest nullable.
|
|
178
|
+
|
|
179
|
+
### Changed — only when an assistant is configured
|
|
180
|
+
|
|
181
|
+
- `Desk#agents` is now an **Array** (humans plus her), and `Desk#humans` is the human-only pool. Page people with `ticket.agents_to_notify` or `desk.humans`: notifying a machine is notifying nobody.
|
|
182
|
+
- `announce_assignments` never announces her — "Rose is taking care of your request" is not a thing to say.
|
|
183
|
+
- **A human reply on a case she holds takes it over under every `reply_policy`**, and supersedes the pending proposal. `:assignee_only` exists so two people don't answer at once, and she is not one.
|
|
184
|
+
- `close!` expires pending proposals; reopening a case she closed leaves it unassigned and caps her at `:draft` for the rest of its life.
|
|
185
|
+
- `Ticket#reply!` and `post_agent_message!` accept `metadata:` (provenance) and `turn:`.
|
|
186
|
+
|
|
187
|
+
### Upgrading from 0.2
|
|
188
|
+
|
|
189
|
+
Additive and rolling-safe — unlike the 0.2 cutover, no drain is needed.
|
|
190
|
+
|
|
191
|
+
1. `bundle update support_desk` (0.3.0).
|
|
192
|
+
2. `rails generate support_desk:upgrade` then `rails db:migrate`. The migration only adds tables and nullable columns.
|
|
193
|
+
3. **Deploy every process to 0.3.0 before adding `config.assistant`.** A 0.2 worker cannot honour a turn it does not know about.
|
|
194
|
+
4. `rails g support_desk:assistant Rose --disclosure …`, paste the printed stanza, wire the job to `:assistant_turn`, schedule the two rake tasks, and subscribe your notifiers to `draft_proposed`, `ticket_escalated` and `human_requested`.
|
|
195
|
+
5. Render `support_desk/tickets/_human_door` in your thread.
|
|
196
|
+
6. `SupportDesk.doctor` green.
|
|
197
|
+
7. **Start at `:draft`.** Every word goes through a person until the acceptance rate on reviewed proposals says otherwise, and keep topic caps on money and identity when you raise it.
|
|
198
|
+
|
|
199
|
+
Rolling back: `rose.deactivate!` is the kill switch and needs no deploy. A
|
|
200
|
+
code rollback to 0.2 keeps the schema — the columns are nullable and the two
|
|
201
|
+
tables are ignored — but loses the provenance rendering of her messages, so
|
|
202
|
+
do it only if 0.3 itself is broken.
|
|
203
|
+
|
|
7
204
|
## [0.2.0] - 2026-09-17
|
|
8
205
|
|
|
9
206
|
**The desk can write first.** Until now a case could only start with somebody
|