docs: correct 005-ai-support design to reuse the existing ticket state machine

003-ticketing's ticket-state-machine.ts already defines AI_ANALYZING/
AI_TROUBLESHOOTING/AI_VERIFYING/AI_RESOLVED/HUMAN_ESCALATION ticket
statuses, clearly authored anticipating this feature. Corrects the plan
before implementation: AISupportSession.status now drives Ticket.status
through the existing ticketsService.updateStatus (reusing its optimistic
concurrency), instead of an isolated status field the rest of the system
never sees. Also clarifies that knowledge retrieval is an in-process
service call through knowledge's index.ts, not an HTTP loopback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
saqib mir
2026-09-02 16:55:01 +05:30
co-authored by Claude Sonnet 5
parent 72dddcdf74
commit eceb00632d
4 changed files with 69 additions and 7 deletions
@@ -47,3 +47,15 @@
§A2, not yet built anywhere in this codebase), model routing/fallback (doc 11 §B2), full cost
dashboards (doc 09), localization (doc 11 §B7), and idle-session timeout/expiry.
- All items pass; no revision iterations were needed.
## Planning notes (added during /speckit-plan research)
- **Found a real integration point, not a gap**: 003-ticketing's `ticket-state-machine.ts`
already defines `AI_ANALYZING`/`AI_TROUBLESHOOTING`/`AI_VERIFYING`/`AI_RESOLVED`/
`HUMAN_ESCALATION` ticket statuses — a near-exact match for doc 06's
`AISupportSession.status` enum, clearly authored anticipating this feature. research.md's
design was corrected during planning (before any code was written) to drive `Ticket.status`
through this existing state machine via the existing `ticketsService.updateStatus(...)`, rather
than leaving `AISupportSession.status` as an isolated field the rest of the system can't see —
see research.md "AISupportSession.status drives Ticket.status through the existing state
machine".
+1 -1
View File
@@ -11,7 +11,7 @@ by FR-005).
|---|---|---|
| id | String @id @default(cuid()) | |
| ticketId | String | FK → `Ticket`. One **active** session per ticket at a time (FR-001) — enforced in the repository (partial-condition check, not a DB constraint, since a ticket accumulates multiple ended sessions over time if re-escalated and re-opened) |
| status | String | `analyzing` \| `troubleshooting` \| `verifying` \| `resolved` \| `escalated` \| `ended_by_agent` (doc 06's enum, plus `ended_by_agent` for FR-023) |
| status | String | `analyzing` \| `troubleshooting` \| `verifying` \| `resolved` \| `escalated` \| `ended_by_agent` (doc 06's enum, plus `ended_by_agent` for FR-023). Every transition except `ended_by_agent` is mirrored onto `Ticket.status` via the *existing* 003 state machine (`AI_ANALYZING`/`AI_TROUBLESHOOTING`/`AI_VERIFYING`/`AI_RESOLVED`/`HUMAN_ESCALATION`) through `ticketsService.updateStatus(..., 'ai')` — research.md "AISupportSession.status drives Ticket.status" |
| activeRunbookKey | String? | Set when a diagnosis matches a runbook (research.md "Runbook engine") |
| currentStepIndex | Int? | App-owned index into the active runbook's `steps`; null when no runbook is active |
| clarifyingQuestionsAsked | Int @default(0) | Counted against `AIConfidencePolicy.maxClarifyingQuestions` (FR-009) |
+8 -2
View File
@@ -42,8 +42,14 @@ human — both were previously unimplementable (no AI session existed) and are a
`src/modules/ai-support/{sessions,tools,troubleshooting,escalation}/` (standard module shape,
research.md "Module placement"). New infra: `src/infrastructure/ai/` (Anthropic client
singleton). New job: `src/jobs/ai-session/` (registered in `src/bootstrap/queue.bootstrap.ts`).
Modifies `src/modules/ticketing/tickets/service/tickets.service.ts` (enqueue on ticket creation —
research.md "Session triggering") and `prisma/schema.prisma`.
Modifies `src/modules/ticketing/tickets/service/tickets.service.ts` — two hooks: enqueue on
ticket creation (research.md "Session triggering"), and end any active `AISupportSession` inside
`updateStatus` when a human actor moves the ticket (research.md "AISupportSession.status drives
Ticket.status", FR-023). Also modifies `prisma/schema.prisma`. **Discovered during this planning
pass**: 003-ticketing's `ticket-state-machine.ts` already defines the exact `AI_ANALYZING →
AI_TROUBLESHOOTING → AI_VERIFYING → AI_RESOLVED` / `HUMAN_ESCALATION` states this feature drives
— this feature reuses that state machine and `ticketsService.updateStatus` directly rather than
introducing a parallel one.
**Project Type**: Backend service — single project.
+48 -4
View File
@@ -12,7 +12,9 @@
pure structured-output call means it can never emit a stray tool proposal, and keeps the
confidence score honest (it's the model's stated belief about the classification, not
entangled with whatever it also did with tools that turn).
2. Deterministically (no model call): call `GET /knowledge/retrieve` (004) scoped to the
2. Deterministically (no model call): call `knowledgeService.retrieve(...)` (004, imported
through `ai-support/knowledge`'s public `index.ts` — Principle III; an in-process call, not
an HTTP loopback to this same service's own `GET /knowledge/retrieve` route) scoped to the
diagnosis's product/feature and the ticket's category, and apply the confidence-band policy
(FR-004) to the diagnosis. This decides the branch: ask / proceed / escalate — this decision
is application code, never delegated to the model.
@@ -84,8 +86,9 @@
`src/modules/ai-support/tools/constants/tool-registry.ts`. Four tools ship in this feature:
- `getTicketSnapshot` (low risk) — reads the ticket, its problem, and recent messages; real,
read-only, always available.
- `searchProductKnowledge` (low risk) — re-queries `GET /knowledge/retrieve` with a
model-supplied feature/category refinement mid-conversation; real, read-only.
- `searchProductKnowledge` (low risk) — re-runs `knowledgeService.retrieve(...)` (same
in-process call as above) with a model-supplied feature/category refinement mid-conversation;
real, read-only.
- `verifyProductResolution` (low risk) — **a documented, fail-closed placeholder**, exactly the
same pattern already established by `ticketing/attachments`'s
`UnimplementedPlaceholderScanner`: it always returns `{ confirmed: false, status: "unknown"
@@ -167,6 +170,43 @@
codebase already has BullMQ wired up for exactly this "background work after a DB write"
purpose.
## Decision: `AISupportSession.status` drives `Ticket.status` through the existing state machine
- **Decision**: `src/modules/ticketing/tickets/mapper/ticket-state-machine.ts` (built in
003-ticketing, before this feature existed) already defines `NEW → AI_ANALYZING →
AI_TROUBLESHOOTING → AI_VERIFYING → AI_RESOLVED`, with `HUMAN_ESCALATION` reachable from every
AI_* state and `HUMAN_ESCALATION → IN_PROGRESS` as the human hand-off edge — a near-exact match
for doc 06's `AISupportSession.status` enum (`analyzing | troubleshooting | verifying |
resolved | escalated`). This feature does not invent a parallel status concept: every time a
session's own status changes, it calls the **existing**
`ticketsService.updateStatus(ticketId, newTicketStatus, expectedVersion, 'ai')` (003) to drive
the ticket through the matching status (`analyzing→AI_ANALYZING`,
`troubleshooting→AI_TROUBLESHOOTING`, `verifying→AI_VERIFYING`, `resolved→AI_RESOLVED`,
`escalated→HUMAN_ESCALATION`), reusing 003's own optimistic-concurrency handling
(`expectedVersion`/`409`) rather than adding a second one. The session's own `status` field
still exists separately (data-model.md) because it carries session-scoped values the ticket
state machine doesn't need to know about (`ended_by_agent` — see FR-023 below — never appears
on `Ticket`), but for every value the two share, the ticket is the caller-visible source of
truth and the session record is the AI-internal detail behind it.
- **Rationale**: `Ticket.status` is what every other part of this codebase (agents, SLA,
orchestration once built, the ticket-status contract in 003) already reads to know where a
ticket stands — a session-only status field that never touched `Ticket.status` would make the
ticket lie about its own state while an AI session was quietly doing something else internally.
Reusing 003's state machine and its `updateStatus` method also means this feature inherits
003's already-tested transition validation and concurrency guarantee for free, rather than
re-deriving both.
- **Alternatives considered**: A session-only status with no `Ticket.status` linkage — rejected
per the rationale above. Building a second, AI-specific transition table — rejected; 003's
table already defines exactly these states and edges, and doc 06's `AISupportSession.status`
values were clearly authored to match it in the first place.
- **FR-023 implementation note**: `ticketsService.updateStatus` gains one additional check — when
it's called with an actor other than `'ai'` (a human agent action) while an `AISupportSession`
for that ticket is still active, the session is ended (`status: ended_by_agent`) as part of the
same call, before the ticket's own status update commits. This is the concrete mechanism behind
"a human agent takes ownership ends the AI session the same way an escalation does" (FR-023) —
there's no separate "agent claims ticket" endpoint yet (orchestration/assignment is a later
phase), so any human-actor status transition is the signal this feature has available today.
## Decision: Runbook engine — the app selects the step, the model only phrases and interprets it
- **Decision**: `AISupportSession` gains `activeRunbookKey String?` and `currentStepIndex Int?`
@@ -191,12 +231,16 @@
## Decision: Verification and resolution — a session can only close on tool evidence
- **Decision**: `AISupportSession.status` transitions to `resolved` only when a
- **Decision**: `AISupportSession.status` (and, via the decision above, `Ticket.status`)
transitions `verifying → resolved` (`AI_VERIFYING → AI_RESOLVED`) only when a
`verifyProductResolution` tool result exists on the session with `confirmed: true` — which,
given that tool's current fail-closed placeholder implementation (above), means resolution
through this exact tool never actually auto-fires yet in this deployment. Customer confirmation
(a message recorded during the session) is stored and surfaced in the escalation/resolution
summary, but the status transition's guard checks tool evidence only, never message content.
Once `Ticket.status` reaches `AI_RESOLVED`, this feature's own responsibility ends — whether the
ticket then moves to `RESOLUTION_PENDING_CUSTOMER` or straight to `RESOLVED` is 003-ticketing's
existing generic status-update surface, not something this feature further automates.
- **Rationale**: FR-018/FR-019 verbatim — resolution requires verification evidence, customer
confirmation is secondary-only. Guarding the state transition on a structured tool-result field
(not on parsing what the AI "said" about the outcome) keeps this enforceable in code, not just