SCN_003 CRM Support-Operations Resolution Skill
Executable SOP for resolving offline-ticket batches, mobile contact-center queues,
and enterprise export-complaint response packages against the shared support console
API. Follow the procedures literally — record fields are the source of truth, never
assume values from the queue note or issue summary alone.
0. Environment & API map
Base URL (use this, not any 127.0.0.1 URL printed in a prompt):
<remote-env-url>
Health: GET /health → {"ok": true}
Catalog: GET /api/catalog (endpoint list + aggregate counts)
| Endpoint |
Returns |
Notes |
GET /api/accounts |
array |
all service accounts |
GET /api/accounts/<account_id> |
object or {"error":"not_found"} |
auth + status used for eligibility gate |
GET /api/tickets |
array |
all tickets |
GET /api/tickets/<ticket_id> |
object |
carries subscribed_mbps, service_type, service_area |
GET /api/outages?service_area=<area> |
array |
each has active, service_types[], outage_id, eta_hours |
GET /api/diagnostics/<ticket_id> |
object or {} |
pre-troubleshooting metrics + root_causes[] |
GET /api/troubleshooting/<ticket_id> |
object or {} |
post-troubleshooting metrics (post_*) + steps[] |
GET /api/customers GET /api/lines GET /api/lines/<line_id> |
object/array |
mobile line record |
GET /api/devices/<device_id> |
object |
mobile device settings |
GET /api/plans/<plan_id> |
object |
has data_limit_gb, data_refueling_price_per_gb |
GET /api/bills |
array |
each: amount_due_usd, bill_id, customer_id, due_date, status |
GET /api/cases GET /api/cases/<case_id> |
array/object |
mobile case root |
GET /api/enterprise/accounts |
array |
each: account_owner, finance_owner, name, tier |
GET /api/enterprise/incidents |
array |
each: engineering_owner, account_owner, severity, product, summary, received_at, status |
GET /api/enterprise/export-runs?incident_id=<id> |
array |
each run: run_date, status (FAILED/SUCCEEDED), failure_code, exported_record_count |
GET /api/enterprise/messages?query=<text> |
array |
each: author, body, channel, created_at, message_id |
GET /api/enterprise/sla/<enterprise_account_id> |
object |
credit_trigger, monthly_export_credit_percent, executive_contact |
Empty body {} from diagnostics/troubleshooting = no record for that ticket.
A /api/diagnostics/<id> or /api/troubleshooting/<id> record may exist in the API
even for a ticket that was gate-decided out (FAILED/PENDING_ACTION). Those records
must NOT be used for issue-flag computation; they exist for the
diagnostic_records_skipped_by_gate audit metric only.
1. Diagnostic floors (inferrable thresholds)
These absolute/ratio floors are NOT printed by the API; derive issue flags by
comparing diagnostics metrics to these floors:
| Metric |
Floor (INFERRRED) |
Issue flag set when |
latency_ms |
100 ms |
latency_ms > 100 → latency_issue = true |
jitter_ms |
30 ms |
jitter_ms > 30 → stability_issue = true |
bandwidth_mbps |
0.80 × subscribed_mbps |
bandwidth_mbps < 0.80 × subscribed_mbps → bandwidth_issue = true |
Inferred from gold-flag matching across the 5 diagnostic tickets whose post-troubleshooting
status was known (TCK-5107 RESOLVED, TCK-5184 ESCALATED, TCK-5402 RESOLVED,
TCK-5406 ESCOLATED, TCK-5407 ESCALATED):
- RESOLVED post metrics sit at/below floors: post lat 82 & 79 (≤100), post jit 21 & 18 (≤30),
post bw 272/300=0.91 & 93/100=0.93 (≥0.80).
- ESCALATED post metrics still violate at least one floor; pre-troubleshooting metrics
on every diagnostics-used ticket all violate the corresponding floor set.
- The bandwidth ratio 0.80 sits in the only weakly-constrained window
(TCK-5107 pre 209/300=0.697 fails; TCK-5107 post 272/300=0.907 passes), so 80% is the
best single round-number estimate.
Boundary rule: treat >= for floors below which there is no issue and > for floors
above which there is an issue (strict inequalities; values do not hit the boundary in
observed data).
2. Family A — Offline ticket batch resolution
Two output flavors exist; both follow the same gating + diagnosing + troubleshooting SOP.
Distinguish by the answer template:
- Flavor-1 (full):
ticket_decisions[] with final_resolution_status,
diagnostic_needed, latency_issue, stability_issue, bandwidth_issue,
outage_id, escalation_team, resolution_route; batch_summary has
RESOLVED / PENDING_ACTION / ESCALATED / FAILED + tickets_requiring_customer_wait.
- Flavor-2 (queue-quality):
ticket_decisions[] with final_resolution_status,
route_team, key_blocker, diagnostic_required; queue_summary has per-team counts
(TIER2_SUPPORT, FIELD_OPS, NETWORK_ENGINEERING, ACCOUNTS_PAYABLE) + per-status counts.
2.1 Gating order (apply strictly in this order; first hit wins)
For each ticket_id in the payload CSV (preserve payload order):
GET /api/accounts/<account_id>.
- If the API returns
{"error":"not_found"} or HTTP error →
INVALID_ACCOUNT (key_blocker). final_resolution_status=FAILED,
diagnostic_needed=false, all issue flags=false, escalation_team=NONE,
route_team=NONE, outage_id="".
- If
account.authentication.last_login_status == "FAILURE" OR
account.authentication.account_recovery_status == "FAILURE" →
AUTH_FAILED. final_resolution_status=FAILED, diagnostic_needed=false,
escalation_team=NONE, route_team=NONE, outage_id="".
- If
account.status == "Suspended":
- Look at the line/ticket context (issue_summary or queue_note). If it mentions
"overdue" (or the linked
/api/lines/<line_id> shows
suspension_reason=="OVERDUE_BILL", or there's a linked bill with
status=="Overdue") → OVERDUE_SUSPENSION (FAILED, route_team=
ACCOUNTS_PAYABLE, escalation_team=ACCOUNTS_PAYABLE, diagnostic_needed=false).
- Otherwise (generic hold / non-billing suspension) → INELIGIBLE_ACCOUNT
(FAILED, diagnostic_needed=false, escalation_team=NONE, route_team=NONE).
- Fraud-suspension variant: if issue context says "fraud" → FRAUD_SUSPENSION
(FAILED, route_team=NONE, diagnostic_needed=false). Field exists in enum;
treat same as INELIGIBLE_ACCOUNT for team routing unless evidence says otherwise.
- Account gate passed (Active + auth SUCCESS) → check outage.
GET /api/outages?service_area=<ticket.service_area>.
Consider an outage a match iff outage.active == true AND
ticket.service_type in outage.service_types.
- Match → ACTIVE_OUTAGE (key_blocker). final_resolution_status=PENDING_ACTION,
diagnostic_needed=false, all issue flags=false, resolution_route=OUTAGE_WAIT,
outage_id= (Flavor-1 only), route_team=NONE.
- No match → proceed to diagnostics (diagnostic_needed=true).
- Diagnostics route:
GET /api/diagnostics/<ticket_id> and
GET /api/troubleshooting/<ticket_id>.
2.2 Pre-troubleshooting issue flag evaluation
Flavor-1 tickets that ran diagnostics populate latency_issue,
stability_issue, bandwidth_issue from diagnostics × floors:
latency_issue = diagnostic.latency_ms > 100
stability_issue = diagnostic.jitter_ms > 30
bandwidth_issue = diagnostic.bandwidth_mbps < 0.80 × ticket.subscribed_mbps
(Flavor-2 does not emit per-issue flags but the underlying floors still drive the
post-troubleshooting RESOLVED-vs-ESCALATED determination.)
2.3 Post-troubleshooting resolution determination
For each ticket that got diagnostics:
- Compute post-issue flags the same way against the troubleshooting record's
post_
metrics:
- post_latency_issue =
troubleshooting.post_latency_ms > 100
- post_stability_issue =
troubleshooting.post_jitter_ms > 30
- post_bandwidth_issue =
troubleshooting.post_bandwidth_mbps < 0.80 × subscribed_mbps
- If ALL THREE post flags are false → RESOLVED via AUTO_TROUBLESHOOTING
(final_resolution_status=RESOLVED, escalation_team=NONE, route_team=NONE,
resolution_route=AUTO_TROUBLESHOOTING).
- If ANY post flag is true → ESCALATED (final_resolution_status=ESCALATED,
resolution_route=ESCALATION). Escalation team comes from the root_causes of the
diagnostics record (see §2.4).
Note RESOLVED/ESCALATED still preserve the original pre-troubleshooting issue flags
in the Flavor-1 output (issue flags are PRE-TROUBLESHOOTING violations, not post).
2.4 Root-cause → escalation team map
For each ESCALATED ticket, take diagnostics.root_causes[] and map by keyword:
| Keyword in any root_cause |
Escalation team (route_team) |
fiber OR signal |
FIELD_OPS |
backbone OR capacity |
NETWORK_ENGINEERING |
provisioning |
TIER2_SUPPORT |
billing OR overdue OR payment |
ACCOUNTS_PAYABLE |
| (no match / unrecognized) |
TIER2_SUPPORT (fallback, inferred) |
A ticket's key_blocker (Flavor-2) for ESCALATED tickets uses the root_cause text:
- BACKBONE_CAPACITY → key_blocker=NETWORK_CAPACITY
- PROVISIONING_STALE → key_blocker=PROVISIONING_STALE
- FIBER_DROP_DAMAGE or SIGNAL_LOSS → key_blocker=PHYSICAL_LINE_FAULT
Other variants follow the same root_cause → team keyword pattern.
Evidence from gold:
- TCK-5184 root_causes=[FIBER_DROP_DAMAGE, SIGNAL_LOSS] both → FIELD_OPS ✓
- TCK-5406 root_causes=[BACKBONE_CAPACITY] → NETWORK_ENGINEERING ✓
- TCK-5407 root_causes=[PROVISIONING_STALE] → TIER2_SUPPORT ✓
2.5 Summary fields
Flavor-1 batch_summary:
RESOLVED/PENDING_ACTION/ESCALATED/FAILED = counts of each final_resolution_status.
tickets_requiring_customer_wait = count of PENDING_ACTION (waiting on outage).
Flavor-2 queue_summary:
- Per-status counts (FAILED, PENDING_ACTION, RESOLVED, ESCALATED).
- Per-team counts (TIER2_SUPPORT, FIELD_OPS, NETWORK_ENGINEERING, ACCOUNTS_PAYABLE) =
counts of tickets routed to that team anywhere — including FAILED tickets routed to
ACCOUNTS_PAYABLE via OVERDUE_SUSPENSION.
2.6 Field-emission cheat sheet
| Flavor-1 field |
Where it comes from |
ticket_id |
payload CSV |
account_id |
payload CSV |
final_resolution_status |
§2.1 gate / §2.3 post determination |
diagnostic_needed |
true iff account+outage gates both pass |
latency_issue/stability_issue/bandwidth_issue |
PRE-troubleshooting flags (§2.2) — set to false if gate-decided |
outage_id |
matched outage id or "" |
escalation_team |
NONE for RESOLVED/PENDING/FAILED; team per §2.4 for ESCALATED |
resolution_route |
AUTO_TROUBLESHOOTING / OUTAGE_WAIT / ESCALATION / INELIGIBLE_ACCOUNT / AUTH_FAILED / INVALID_ACCOUNT |
| Flavor-2 field |
Where it comes from |
ticket_id |
payload CSV |
final_resolution_status |
same as Flavor-1 |
route_team |
NONE for non-escalated; team per §2.4 for ESCALATED; ACCOUNTS_PAYABLE for OVERDUE_SUSPENSION |
key_blocker |
NONE / ACTIVE_OUTAGE / INVALID_ACCOUNT / AUTH_FAILED / OVERDUE_SUSPENSION / FRAUD_SUSPENSION / NETWORK_CAPACITY / PROVISIONING_STALE / PHYSICAL_LINE_FAULT |
diagnostic_required |
alias of diagnostic_needed |
3. Family B — Mobile contact-center triage
Two output flavors:
- Flavor-1 (full):
case_decisions[] with
case_id, customer_id, line_id, primary_action, secondary_action,
permission, bill_id, charge_amount_usd, final_route;
queue_summary with self_service_fixes, billing_recoveries,
carrier_updates, human_transfers.
- Flavor-2 (data-recovery analyst):
case_decisions[] with
case_id, primary_action, secondary_action, data_refuel_gb,
charge_amount_usd, carrier_update_required, final_route;
worklist_summary with data_refuel_cases, carrier_updates,
device_setting_fixes, human_transfers, total_estimated_customer_charge_usd.
For every case_id in the payload (preserve ascending case_id order):
GET /api/cases/<case_id> → {customer_id, customer_location, device_id, line_id, issue_type, summary}.
GET /api/lines/<line_id> → {status, suspension_reason, roaming_enabled, data_used_gb, plan_id, device_id}.
GET /api/devices/<device_id> → settings (see decision tree).
GET /api/plans/<line.plan_id> → {data_limit_gb, data_refueling_price_per_gb} (only needed for REFUEL cases).
- (Billing only)
GET /api/bills, find bill where customer_id == case.customer_id and status == "Overdue" → use as the payment bill.
3.1 Mobile decision tree (first match wins, evaluate top-down)
| Condition (read in order) |
primary_action |
secondary_action |
final_route |
Extra fields |
line.status == "Suspended" (any suspension_reason) AND customer is ready to pay (per queue note) |
SEND_PAYMENT_REQUEST |
RESUME_LINE_REBOOT |
BILLING_RECOVERY |
bill_id = overdue bill; charge_amount_usd = bill.amount_due_usd |
line.suspension_reason == "FRAUD" or fraud-indicated (no pay resolution) |
TRANSFER_HUMAN |
NO_ACTION |
HUMAN_TRANSFER |
— |
device.sim_status == "missing" and (issue_type=NO_SERVICE or speed_test=no_connection) |
RESEAT_SIM |
NO_ACTION |
SELF_SERVICE (or DEVICE_SETTING_FIX in Flavor-2 if NO_SERVICE excluded) |
— |
device.sim_status indicates locked ("locked" / repeated PIN) and no other setting fix |
RESET_APN_REBOOT |
NO_ACTION |
SELF_SERVICE |
— |
case.customer_location == "abroad" AND line.roaming_enabled == false (phone has roaming on but line side missing) |
ENABLE_LINE_ROAMING (Flavor-1) / ENABLE_LINE_ROAMING (Flavor-2) |
NO_ACTION |
CARRIER_UPDATE |
carrier_update_required = true |
case.customer_location == "abroad" AND line.roaming_enabled == true AND device.phone_roaming_enabled == false |
TOGGLE_ROAMING |
NO_ACTION |
SELF_SERVICE (Flavor-1) / DEVICE_SETTING_FIX (Flavor-2 if applicable) |
— |
device.airplane_mode == true |
TOGGLE_AIRPLANE_MODE |
NO_ACTION |
SELF_SERVICE |
— |
device.mobile_data_enabled == false |
TOGGLE_MOBILE_DATA |
NO_ACTION |
SELF_SERVICE (Flavor-1) / DEVICE_SETTING_FIX (Flavor-2) |
— |
device.vpn_connected == true (slow_data or poor speed_test) |
DISCONNECT_VPN |
NO_ACTION |
SELF_SERVICE (Flavor-1) / DEVICE_SETTING_FIX (Flavor-2) |
— |
device.data_saver_mode == true (slow_data) |
TOGGLE_DATA_SAVER |
NO_ACTION |
SELF_SERVICE (Flavor-1) / DEVICE_SETTING_FIX (Flavor-2) |
— |
device.network_mode_preference == "3g_only" or similar old mode (slow_data, not already matched) |
SET_NETWORK_MODE |
NO_ACTION |
SELF_SERVICE (Flavor-1) / DEVICE_SETTING_FIX (Flavor-2) |
— |
case.issue_type == "MMS" AND device.can_send_mms == false AND device.mmsc_url_present == true AND any messaging_permissions.{sms,storage} == false |
GRANT_MESSAGING_PERMISSION |
NO_ACTION |
SELF_SERVICE |
permission: sms / storage / sms_and_storage corresponding to the missing flag(s) |
line.data_used_gb >= plan.data_limit_gb (data cap hit) and customer accepts refuel (customer_preferences.accepted_refuel_gb in payload) |
REFUEL_DATA (Flavor-1) / REFUEL_DATA (Flavor-2: takes priority over REFUEL_DATA variant) |
NO_ACTION |
BILLING_RECOVERY (Flavor-1) / DATA_RECOVERY (Flavor-2) |
data_refuel_gb = accepted_refuel_gb; charge_amount_usd = accepted_refuel_gb × plan.data_refueling_price_per_gb |
| None of the above resolves the case |
TRANSFER_HUMAN |
NO_ACTION |
HUMAN_TRANSFER |
— |
Permission field semantics (Flavor-1 MMS):
messaging_permissions.sms == false && storage == false → permission = "sms_and_storage"
messaging_permissions.sms == false only → permission = "sms"
messaging_permissions.storage == false only → permission = "storage"
- nothing missing →
permission = "NONE" (shouldn't happen for a GRANT action)
Evidence mapping:
- CASE-2101: NO_SERVICE, home, line Active roaming true, sim_status=missing → RESEAT_SIM → SELF_SERVICE
- CASE-2102: NO_SERVICE, line.status=Suspended, suspension_reason=OVERDUE_BILL, customer ready →
SEND_PAYMENT_REQUEST + RESUME_LINE_REBOOT, BILL-2102 amount=$86.40 → BILLING_RECOVERY
- CASE-2103: MOBILE_DATA abroad, line.roaming_enabled=true, device.phone_roaming_enabled=false →
TOGGLE_ROAMING → SELF_SERVICE
- CASE-2104: MMS, device.can_send_mms=false, mmsc_url_present=true, storage=false →
GRANT_MESSAGING_PERMISSION, permission="storage" → SELF_SERVICE
- CASE-2105: SLOW_DATA, vpn_connected=true → DISCONNECT_VPN → SELF_SERVICE
- CASE-2501: data_used_gb=16.2 ≥ plan.data_limit_gb=15.0, customer_preferences.accepted_refuel_gb=2.0,
plan=PLAN-PREMIUM data_refueling_price_per_gb=2.0 → REFUEL_DATA, refuel_gb=2.0, charge=$4.00 → DATA_RECOVERY
- CASE-2502: abroad, line.roaming_enabled=false, device.phone_roaming_enabled=true →
ENABLE_LINE_ROAMING, carrier_update_required=true → CARRIER_UPDATE
- CASE-2503: data_saver_mode=true → TOGGLE_DATA_SAVER → DEVICE_SETTING_FIX
- CASE-2504: network_mode_preference="3g_only" → SET_NETWORK_MODE → DEVICE_SETTING_FIX
- CASE-2505: mobile_data_enabled=false → TOGGLE_MOBILE_DATA → DEVICE_SETTING_FIX
3.2 Final-route conventions by flavor
| Action |
Self-service route |
| TOGGLE_ROAMING |
Flavor-1: SELF_SERVICE. Flavor-2: DEVICE_SETTING_FIX. |
| RESEAT_SIM, GRANT_MESSAGING_PERMISSION, DISCONNECT_VPN, SET_NETWORK_MODE, TOGGLE_DATA_SAVER, TOGGLE_MOBILE_DATA |
Flavor-1: SELF_SERVICE. Flavor-2: DEVICE_SETTING_FIX. |
| SEND_PAYMENT_REQUEST + RESUME_LINE_REBOOT |
Flavor-1: BILLING_RECOVERY. (Same action set not used in Flavor-2.) |
| REFUEL_DATA |
Flavor-1: BILLING_RECOVERY (per full enum). Flavor-2: DATA_RECOVERY. |
| ENABLE_LINE_ROAMING |
Both flavors: CARRIER_UPDATE. |
| TRANSFER_HUMAN |
Both flavors: HUMAN_TRANSFER. |
carrier_update_required=true whenever primary_action=ENABLE_LINE_ROAMING (and only then) in Flavor-2.
3.3 Charge calculation
charge_amount_usd for billing / refuel cases:
- Billing-recovery (payment request):
charge_amount_usd = overdue_bill.amount_due_usd (two decimals).
- Data refuel:
charge_amount_usd = accepted_refuel_gb × plan.data_refueling_price_per_gb (two decimals).
- All non-billing actions:
charge_amount_usd = 0.00.
3.4 Summary rollups
| Flavor-1 queue_summary |
Count |
| self_service_fixes |
tickets with final_route=SELF_SERVICE |
| billing_recoveries |
tickets with final_route=BILLING_RECOVERY |
| carrier_updates |
tickets with final_route=CARRIER_UPDATE |
| human_transfers |
tickets with final_route=HUMAN_TRANSFER |
| Flavor-2 worklist_summary |
Value |
| data_refuel_cases |
count primary_action=REFUEL_DATA |
| carrier_updates |
count primary_action=ENABLE_LINE_ROAMING |
| device_setting_fixes |
count final_route=DEVICE_SETTING_FIX |
| human_transfers |
count primary_action=TRANSFER_HUMAN |
| total_estimated_customer_charge_usd |
sum of all charge_amount_usd (two decimals) |
Flavor-1 charge_amount_usd is "number with two decimals"; Flavor-2 too.
Flavor-2 data_refuel_gb is "number with one decimal", 0.0 when N/A.
4. Family C — Enterprise export-complaint response package
For each enterprise complaint payload (client_complaint_email.txt + response_requirements.json):
4.1 Identify the incident
GET /api/enterprise/incidents → find by incident_id (often present in the complaint email,
e.g., "INC-7301") OR by matching summary keyword against the complaint subject/body.
- Pull
enterprise_account_id, engineering_owner, account_owner, severity from the
incident record (these become engineering_owner/account_owner/severity in the response
package — they are authoritative, do not derive from the email).
4.2 Identify failed export window and backfill
GET /api/enterprise/export-runs?incident_id=<incident_id>.
Filter to runs where status == "FAILED". Sort by run_date ascending.
failure_window.start_date = first failed run_date.
failure_window.end_date = last failed run_date.
failure_window.failed_days = count of failed runs.
backfill_days = failed_days (manual backfill equals the number of failed runs).
root_cause_category = human-readable summary derived from the failed runs' common
failure_code plus message-board evidence (see below). E.g.,
STALE_CREDENTIAL ("scheduler still references old secret" message) →
"stale credential after rotation".
STAGING_STORAGE_QUOTA ("staging bucket reached quota") → "staging bucket quota".
4.3 SLA credit
GET /api/enterprise/sla/<enterprise_account_id>:
sla_credit_percent = sla_contract.monthly_export_credit_percent (integer percent).
- The
credit_trigger text is supporting evidence (e.g., "3 consecutive failed export runs"
vs "critical export outage longer than 72 hours") — verify the failure condition is satisfied
before asserting the credit in the package; the credit_percent value itself is taken verbatim.
4.4 Contributing alert issue
GET /api/enterprise/messages?query=<client keyword or incident id> and inspect the channels.
- If the early-alert message has
channel containing "archive" (e.g., export-alerts-archive)
→ contributing_alert_issue = "ARCHIVED_ALERT_ROUTE".
- If no such archive-route evidence →
"NONE" or "UNKNOWN" per template enum.
4.5 Owners and channel/evidence/report naming
GET /api/enterprise/accounts → find by enterprise_account_id:
account_owner = the enterprise account's account_owner (authoritative, matches incident).
engineering_owner = the incident's engineering_owner (authoritative).
finance_owner = the enterprise account's finance_owner (used for share permission view tier).
Naming conventions (per response_requirements.naming_style):
channel_name: client name → lowercase, hyphenated, punctation stripped. E.g.,
"Asteri Retail Inc." → asteri-retail-inc.
evidence_folder: "<Client Name> <Month Year> Investigation". Use month/year of the failure
window start. E.g., failure start 2026-05-12 → "Asteri Retail Inc. May 2026 Investigation".
report_title: "<Client Name> Export Failure - Resolution Report".
response_status:
sla_credit_percent > 0 → "NEEDS_FINANCE_REVIEW".
- (Coverage for other statuses in the enum READY_TO_SEND / NEEDS_ENGINEERING_REVIEW /
UNDER_INVESTIGATION should drive off the SLA-credit result and incident.status.)
Observed: closed SLA-credit case → NEEDS_FINANCE_REVIEW.
4.6 Share permissions
Take response_requirements.permission_users_to_include in order. For each user:
- If user matches
enterprise_account.finance_owner → "view".
- The next listed user (typically engineering/owner role) →
"edit".
- Any further users not on the finance tier →
"upload_only" (enum value; inferred for the
third-tier case).
Order the output array exactly as listed in permission_users_to_include.
Evidence (train_003): permission_users_to_include=["laura.brown","jun.chen"] →
[{user:laura.brown,permission:view}, {user:jun.chen,permission:edit}].
laura.brown is ENT-3001.finance_owner → view tier.
4.7 Response-package field reference
| Field |
Source |
incident_id |
enterprise incident (or complaint email) |
enterprise_account_id |
enterprise incident |
root_cause_category |
failure_code + message body (human-readable short summary) |
contributing_alert_issue |
ARCHIVED_ALERT_ROUTE / NONE / UNKNOWN (per message channel) |
failure_window.start_date / end_date / failed_days |
min/max/count of FAILED export runs |
backfill_days |
= failed_days |
sla_credit_percent |
sla contract monthly_export_credit_percent |
severity |
enterprise incident.severity (Critical/High/Medium/Low) |
engineering_owner |
enterprise incident.engineering_owner |
account_owner |
enterprise incident.account_owner (matches enterprise account) |
channel_name |
client name lowercased-hyphenated |
evidence_folder |
" Investigation" |
report_title |
" Export Failure - Resolution Report" |
share_permissions[] |
ordered list with view (finance_owner)/edit/upload_only per §4.6 |
response_status |
NEEDS_FINANCE_REVIEW when sla_credit_percent > 0 |
5. Audit-math definitions (when test prompts request these)
These are computed over the batch of tickets the test prompt includes. Always compute
over the tickets whose diagnostic_needed flag became true for diagnostic audits,
and over ESCALATED/RESOLVED-with-troubleshooting tickets for post audits.
bandwidth_floor(tkt) = 0.80 × tkt.subscribed_mbps
latency_floor = 100 (ms)
jitter_floor = 30 (ms)
pre_troubleshooting_bandwidth_gap_total_mbps:
Sum over every diagnostics-used ticket (those with diagnostic_needed == true) whose
diagnostics.bandwidth_mbps < bandwidth_floor of (bandwidth_floor - bandwidth_mbps).
Units: Mbps. Gated-out tickets are excluded (even if their API diagnostic record exists).
diagnostic_records_skipped_by_gate:
Count of tickets where diagnostic_needed == false BUT the API actually returns a
non-empty diagnostics record at /api/diagnostics/<ticket_id>. (These records were
available but skipped because the gate decided the ticket.)
post_troubleshooting_remaining_issue_flags:
For each "unresolved diagnostic ticket" (diagnostic_needed=true AND
final_resolution_status==ESCALATED) count how many of
{post_latency_issue, post_stability_issue, post_bandwidth_issue} are still true
against the floors. Sum across all unresolved diagnostic tickets.
post_threshold_excess_totals:
For each unresolved diagnostic ticket, compute:
latency_excess = max(0, troubleshooting.post_latency_ms - 100)
jitter_excess = max(0, troubleshooting.post_jitter_ms - 30)
bandwidth_short = max(0, bandwidth_floor(tkt) - troubleshooting.post_bandwidth_mbps)
Sum (latency_excess + jitter_excess + bandwidth_short) across all unresolved
diagnostic tickets. Report as a single total (ms + ms + Mbps aggregated).
tickets_using_post_troubleshooting_records:
Count of tickets with diagnostic_needed == true AND a non-empty
/api/troubleshooting/<ticket_id> record (i.e., post_ metrics were used to decide
RESOLVED vs ESCALATED).
tickets_with_active_outage_match:
Count of tickets for which an active outage (active=true, service_type in
outage.service_types) sits in the ticket's service_area.
unique_escalation_teams:
Sorted ascending set of distinct escalation_team values across the batch
(excluding NONE).
root_cause_escalation_ticket_ids (by team):
Map: team_name → sorted list of ticket_ids whose final escalation_team == team_name
(i.e., the ESCOLATED tickets, grouped by their resolved escalation team per §2.4).
Only populate teams that have at least one ticket.
post_success and post_failure id lists:
post_success_ids = sorted ascending list of ticket_ids whose
final_resolution_status == RESOLVED (after troubleshooting, all post flags cleared).
post_failure_ids = sorted ascending list of ticket_ids whose
final_resolution_status == ESCALATED (post still has at least one issue).
Both limited to tickets that went through diagnostics (had a troubleshooting recheck).
Per-ticket floor + shortfall list (sorted by ticket_id ascending):
For each diagnostics-used ticket, report:
ticket_id
bandwidth_floor_mbps = 0.80 × subscribed_mbps
bandwidth_shortfall_mbps = max(0, bandwidth_floor_mbps - diagnostics.bandwidth_mbps)
latency_floor_ms = 100
latency_excess_ms = max(0, diagnostics.latency_ms - 100)
jitter_floor_ms = 30
jitter_excess_ms = max(0, diagnostics.jitter_ms - 30)
Sorted ascending by ticket_id.
6. Pitfalls and edge cases
- API URLs in prompts lie: any prompt printing
http://127.0.0.1:8057 (or similar)
must be ignored; the base URL is <remote-env-url>. Apply to every endpoint
substitution.
- Gated-out tickets may still have diagnostics / troubleshooting records: those are
"skipped by gate" for the audit. Do NOT compute pre issue-flags for them in the per-ticket
answer; their flags go to
false in Flavor-1.
- Bandwidth ratio only weakly constrained: train data narrows it to
0.697 < ratio ≤ 0.907 from TCK-5107 alone. Commit to 0.80 unless a test item
contradicts (then revise proportionally). The jitter floor (≤21 to <33.5) and latency
floor (≤82 to <97 from TCK-5402 pre being just below) commit to 30 ms and 100 ms.
- Outage service_type overlap: an outage applies only if the ticket's
service_type
is in outage.service_types. A plain active == true in the right service_area does
not suffice — service_type must match.
- Order preservation: preserve the CSV payload order in
ticket_decisions;
preserve ascending case_id order in case_decisions.
- Permission users order: in Flavor-C, output share_permissions in the exact order of
response_requirements.permission_users_to_include (do NOT sort alphabetically).
route_team for OVERDUE_SUSPENSION is ACCOUNTS_PAYABLE even though the ticket is
FAILED: the per-team summary counts include this ticket. Same for FRAUD_SUSPENSION
if the test sets its team accordingly.
- Two mobile output flavors: same underlying decision tree, but Flavor-2 collapses
billing/billing-recovery routes to DATA_RECOVERY and merges self-service fixes into
DEVICE_SETTING_FIX. Watch the answer template before deciding which fields to emit.
- REFUEL_DATA needs both: line.data_used_gb ≥ plan.data_limit_gb AND explicit
customer_preferences.accepted_refuel_gb in the worklist payload. If the customer
refuses, fall through to other matches (often TRANSFER_HUMAN).
- root_cause → team matching: case-insensitive substring match. A single root_cause
may contain multiple keywords (BACKBONE_CAPACITY matches both
backbone and
capacity → NETWORK_ENGINEERING).
- failure_window count vs duration:
failed_days is the count of FAILED export runs,
NOT the calendar span (e.g., 2026-05-12 to 2026-05-14 = 3 dates = 3 FAILED runs).
- SLA credit value comes verbatim from the SLA contract: never recompute from the
number of failed days. Validate the trigger text is satisfied, but use the contract's
monthly_export_credit_percent literally.
- response_status: when SLA credit > 0, status is NEEDS_FINANCE_REVIEW (finance_owner
gets view permission in the share list); engineering gets edit. Verify against
incident.status and sla.credit_trigger if the prompt insists on a different status enum.
7. Quick threshold summary (commit these numbers)
- Latency floor: 100 ms (inferred — TCK-5107 post=82 cleared, TCK-5407 post=121 still failing).
- Jitter floor: 30 ms (inferred — TCK-5107 post=21 cleared, all ESCALATED posts ≥32).
- Bandwidth ratio: 0.80 of
subscribed_mbps (inferred — TCK-5107 pre=209/300 fails,
post=272/300 passes; ratio window is 0.697–0.907).
1---2name: fewshot-attempt-01-43description: SCN_003 CRM Support-Operations Resolution Skill4---5# SCN_003 CRM Support-Operations Resolution Skill67Executable SOP for resolving offline-ticket batches, mobile contact-center queues,8and enterprise export-complaint response packages against the shared support console9API. Follow the procedures literally — record fields are the source of truth, never10assume values from the queue note or issue summary alone.1112## 0. Environment & API map1314Base URL (use this, not any 127.0.0.1 URL printed in a prompt):15`<remote-env-url>`1617Health: `GET /health` → `{"ok": true}`18Catalog: `GET /api/catalog` (endpoint list + aggregate counts)1920| Endpoint | Returns | Notes |21|---|---|---|22| `GET /api/accounts` | array | all service accounts |23| `GET /api/accounts/<account_id>` | object or `{"error":"not_found"}` | auth + status used for eligibility gate |24| `GET /api/tickets` | array | all tickets |25| `GET /api/tickets/<ticket_id>` | object | carries `subscribed_mbps`, `service_type`, `service_area` |26| `GET /api/outages?service_area=<area>` | array | each has `active`, `service_types[]`, `outage_id`, `eta_hours` |27| `GET /api/diagnostics/<ticket_id>` | object or `{}` | pre-troubleshooting metrics + `root_causes[]` |28| `GET /api/troubleshooting/<ticket_id>` | object or `{}` | post-troubleshooting metrics (`post_*`) + `steps[]` |29| `GET /api/customers` `GET /api/lines` `GET /api/lines/<line_id>` | object/array | mobile line record |30| `GET /api/devices/<device_id>` | object | mobile device settings |31| `GET /api/plans/<plan_id>` | object | has `data_limit_gb`, `data_refueling_price_per_gb` |32| `GET /api/bills` | array | each: `amount_due_usd`, `bill_id`, `customer_id`, `due_date`, `status` |33| `GET /api/cases` `GET /api/cases/<case_id>` | array/object | mobile case root |34| `GET /api/enterprise/accounts` | array | each: `account_owner`, `finance_owner`, `name`, `tier` |35| `GET /api/enterprise/incidents` | array | each: `engineering_owner`, `account_owner`, `severity`, `product`, `summary`, `received_at`, `status` |36| `GET /api/enterprise/export-runs?incident_id=<id>` | array | each run: `run_date`, `status` (FAILED/SUCCEEDED), `failure_code`, `exported_record_count` |37| `GET /api/enterprise/messages?query=<text>` | array | each: `author`, `body`, `channel`, `created_at`, `message_id` |38| `GET /api/enterprise/sla/<enterprise_account_id>` | object | `credit_trigger`, `monthly_export_credit_percent`, `executive_contact` |3940Empty body `{}` from diagnostics/troubleshooting = no record for that ticket.4142A `/api/diagnostics/<id>` or `/api/troubleshooting/<id>` record may exist in the API43even for a ticket that was gate-decided out (FAILED/PENDING_ACTION). Those records44must NOT be used for issue-flag computation; they exist for the45`diagnostic_records_skipped_by_gate` audit metric only.4647## 1. Diagnostic floors (inferrable thresholds)4849These absolute/ratio floors are NOT printed by the API; derive issue flags by50comparing diagnostics metrics to these floors:5152| Metric | Floor (INFERRRED) | Issue flag set when |53|---|---|---|54| `latency_ms` | 100 ms | `latency_ms > 100` → `latency_issue = true` |55| `jitter_ms` | 30 ms | `jitter_ms > 30` → `stability_issue = true` |56| `bandwidth_mbps` | `0.80 × subscribed_mbps` | `bandwidth_mbps < 0.80 × subscribed_mbps` → `bandwidth_issue = true` |5758Inferred from gold-flag matching across the 5 diagnostic tickets whose post-troubleshooting59status was known (TCK-5107 RESOLVED, TCK-5184 ESCALATED, TCK-5402 RESOLVED,60TCK-5406 ESCOLATED, TCK-5407 ESCALATED):61- RESOLVED post metrics sit at/below floors: post lat 82 & 79 (≤100), post jit 21 & 18 (≤30),62 post bw 272/300=0.91 & 93/100=0.93 (≥0.80).63- ESCALATED post metrics still violate at least one floor; pre-troubleshooting metrics64 on every diagnostics-used ticket all violate the corresponding floor set.65- The bandwidth ratio 0.80 sits in the only weakly-constrained window66 (TCK-5107 pre 209/300=0.697 fails; TCK-5107 post 272/300=0.907 passes), so 80% is the67 best single round-number estimate.6869Boundary rule: treat `>=` for floors below which there is no issue and `>` for floors70above which there is an issue (strict inequalities; values do not hit the boundary in71observed data).7273## 2. Family A — Offline ticket batch resolution7475Two output flavors exist; both follow the same gating + diagnosing + troubleshooting SOP.76Distinguish by the answer template:77- **Flavor-1 (full)**: `ticket_decisions[]` with `final_resolution_status`,78 `diagnostic_needed`, `latency_issue`, `stability_issue`, `bandwidth_issue`,79 `outage_id`, `escalation_team`, `resolution_route`; batch_summary has80 RESOLVED / PENDING_ACTION / ESCALATED / FAILED + `tickets_requiring_customer_wait`.81- **Flavor-2 (queue-quality)**: `ticket_decisions[]` with `final_resolution_status`,82 `route_team`, `key_blocker`, `diagnostic_required`; queue_summary has per-team counts83 (TIER2_SUPPORT, FIELD_OPS, NETWORK_ENGINEERING, ACCOUNTS_PAYABLE) + per-status counts.8485### 2.1 Gating order (apply strictly in this order; first hit wins)8687For each `ticket_id` in the payload CSV (preserve payload order):88891. `GET /api/accounts/<account_id>`.90 - If the API returns `{"error":"not_found"}` or HTTP error →91 **INVALID_ACCOUNT** (key_blocker). final_resolution_status=FAILED,92 diagnostic_needed=false, all issue flags=false, escalation_team=NONE,93 route_team=NONE, outage_id="".94 - If `account.authentication.last_login_status == "FAILURE"` OR95 `account.authentication.account_recovery_status == "FAILURE"` →96 **AUTH_FAILED**. final_resolution_status=FAILED, diagnostic_needed=false,97 escalation_team=NONE, route_team=NONE, outage_id="".98 - If `account.status == "Suspended"`:99 - Look at the line/ticket context (issue_summary or queue_note). If it mentions100 "overdue" (or the linked `/api/lines/<line_id>` shows101 `suspension_reason=="OVERDUE_BILL"`, or there's a linked bill with102 `status=="Overdue"`) → **OVERDUE_SUSPENSION** (FAILED, route_team=103 ACCOUNTS_PAYABLE, escalation_team=ACCOUNTS_PAYABLE, diagnostic_needed=false).104 - Otherwise (generic hold / non-billing suspension) → **INELIGIBLE_ACCOUNT**105 (FAILED, diagnostic_needed=false, escalation_team=NONE, route_team=NONE).106 - Fraud-suspension variant: if issue context says "fraud" → **FRAUD_SUSPENSION**107 (FAILED, route_team=NONE, diagnostic_needed=false). Field exists in enum;108 treat same as INELIGIBLE_ACCOUNT for team routing unless evidence says otherwise.1092. Account gate passed (Active + auth SUCCESS) → check outage.110 `GET /api/outages?service_area=<ticket.service_area>`.111 Consider an outage a match iff `outage.active == true` AND112 `ticket.service_type in outage.service_types`.113 - Match → **ACTIVE_OUTAGE** (key_blocker). final_resolution_status=PENDING_ACTION,114 diagnostic_needed=false, all issue flags=false, resolution_route=OUTAGE_WAIT,115 outage_id=<matched outage_id> (Flavor-1 only), route_team=NONE.116 - No match → proceed to diagnostics (diagnostic_needed=true).1173. Diagnostics route: `GET /api/diagnostics/<ticket_id>` and118 `GET /api/troubleshooting/<ticket_id>`.119120### 2.2 Pre-troubleshooting issue flag evaluation121122Flavor-1 tickets that ran diagnostics populate `latency_issue`,123`stability_issue`, `bandwidth_issue` from diagnostics × floors:124125- `latency_issue = diagnostic.latency_ms > 100`126- `stability_issue = diagnostic.jitter_ms > 30`127- `bandwidth_issue = diagnostic.bandwidth_mbps < 0.80 × ticket.subscribed_mbps`128129(Flavor-2 does not emit per-issue flags but the underlying floors still drive the130post-troubleshooting RESOLVED-vs-ESCALATED determination.)131132### 2.3 Post-troubleshooting resolution determination133134For each ticket that got diagnostics:135- Compute post-issue flags the same way against the troubleshooting record's `post_`136 metrics:137 - post_latency_issue = `troubleshooting.post_latency_ms > 100`138 - post_stability_issue = `troubleshooting.post_jitter_ms > 30`139 - post_bandwidth_issue = `troubleshooting.post_bandwidth_mbps < 0.80 × subscribed_mbps`140- If ALL THREE post flags are false → **RESOLVED via AUTO_TROUBLESHOOTING**141 (final_resolution_status=RESOLVED, escalation_team=NONE, route_team=NONE,142 resolution_route=AUTO_TROUBLESHOOTING).143- If ANY post flag is true → **ESCALATED** (final_resolution_status=ESCALATED,144 resolution_route=ESCALATION). Escalation team comes from the root_causes of the145 diagnostics record (see §2.4).146147Note RESOLVED/ESCALATED still preserve the original pre-troubleshooting issue flags148in the Flavor-1 output (issue flags are PRE-TROUBLESHOOTING violations, not post).149150### 2.4 Root-cause → escalation team map151152For each ESCALATED ticket, take `diagnostics.root_causes[]` and map by keyword:153154| Keyword in any root_cause | Escalation team (route_team) |155|---|---|156| `fiber` OR `signal` | FIELD_OPS |157| `backbone` OR `capacity` | NETWORK_ENGINEERING |158| `provisioning` | TIER2_SUPPORT |159| `billing` OR `overdue` OR `payment` | ACCOUNTS_PAYABLE |160| (no match / unrecognized) | TIER2_SUPPORT (fallback, inferred) |161162A ticket's `key_blocker` (Flavor-2) for ESCALATED tickets uses the root_cause text:163- BACKBONE_CAPACITY → key_blocker=NETWORK_CAPACITY164- PROVISIONING_STALE → key_blocker=PROVISIONING_STALE165- FIBER_DROP_DAMAGE or SIGNAL_LOSS → key_blocker=PHYSICAL_LINE_FAULT166Other variants follow the same root_cause → team keyword pattern.167168Evidence from gold:169- TCK-5184 root_causes=[FIBER_DROP_DAMAGE, SIGNAL_LOSS] both → FIELD_OPS ✓170- TCK-5406 root_causes=[BACKBONE_CAPACITY] → NETWORK_ENGINEERING ✓171- TCK-5407 root_causes=[PROVISIONING_STALE] → TIER2_SUPPORT ✓172173### 2.5 Summary fields174175Flavor-1 `batch_summary`:176- `RESOLVED`/`PENDING_ACTION`/`ESCALATED`/`FAILED` = counts of each final_resolution_status.177- `tickets_requiring_customer_wait` = count of PENDING_ACTION (waiting on outage).178179Flavor-2 `queue_summary`:180- Per-status counts (FAILED, PENDING_ACTION, RESOLVED, ESCALATED).181- Per-team counts (TIER2_SUPPORT, FIELD_OPS, NETWORK_ENGINEERING, ACCOUNTS_PAYABLE) =182 counts of tickets routed to that team anywhere — including FAILED tickets routed to183 ACCOUNTS_PAYABLE via OVERDUE_SUSPENSION.184185### 2.6 Field-emission cheat sheet186187| Flavor-1 field | Where it comes from |188|---|---|189| `ticket_id` | payload CSV |190| `account_id` | payload CSV |191| `final_resolution_status` | §2.1 gate / §2.3 post determination |192| `diagnostic_needed` | true iff account+outage gates both pass |193| `latency_issue`/`stability_issue`/`bandwidth_issue` | PRE-troubleshooting flags (§2.2) — set to `false` if gate-decided |194| `outage_id` | matched outage id or `""` |195| `escalation_team` | NONE for RESOLVED/PENDING/FAILED; team per §2.4 for ESCALATED |196| `resolution_route` | AUTO_TROUBLESHOOTING / OUTAGE_WAIT / ESCALATION / INELIGIBLE_ACCOUNT / AUTH_FAILED / INVALID_ACCOUNT |197198| Flavor-2 field | Where it comes from |199|---|---|200| `ticket_id` | payload CSV |201| `final_resolution_status` | same as Flavor-1 |202| `route_team` | NONE for non-escalated; team per §2.4 for ESCALATED; ACCOUNTS_PAYABLE for OVERDUE_SUSPENSION |203| `key_blocker` | NONE / ACTIVE_OUTAGE / INVALID_ACCOUNT / AUTH_FAILED / OVERDUE_SUSPENSION / FRAUD_SUSPENSION / NETWORK_CAPACITY / PROVISIONING_STALE / PHYSICAL_LINE_FAULT |204| `diagnostic_required` | alias of `diagnostic_needed` |205206## 3. Family B — Mobile contact-center triage207208Two output flavors:209- **Flavor-1 (full)**: `case_decisions[]` with210 `case_id`, `customer_id`, `line_id`, `primary_action`, `secondary_action`,211 `permission`, `bill_id`, `charge_amount_usd`, `final_route`;212 queue_summary with `self_service_fixes`, `billing_recoveries`,213 `carrier_updates`, `human_transfers`.214- **Flavor-2 (data-recovery analyst)**: `case_decisions[]` with215 `case_id`, `primary_action`, `secondary_action`, `data_refuel_gb`,216 `charge_amount_usd`, `carrier_update_required`, `final_route`;217 worklist_summary with `data_refuel_cases`, `carrier_updates`,218 `device_setting_fixes`, `human_transfers`, `total_estimated_customer_charge_usd`.219220For every `case_id` in the payload (preserve ascending case_id order):2211. `GET /api/cases/<case_id>` → `{customer_id, customer_location, device_id, line_id, issue_type, summary}`.2222. `GET /api/lines/<line_id>` → `{status, suspension_reason, roaming_enabled, data_used_gb, plan_id, device_id}`.2233. `GET /api/devices/<device_id>` → settings (see decision tree).2244. `GET /api/plans/<line.plan_id>` → `{data_limit_gb, data_refueling_price_per_gb}` (only needed for REFUEL cases).2255. (Billing only) `GET /api/bills`, find bill where `customer_id == case.customer_id` and `status == "Overdue"` → use as the payment bill.226227### 3.1 Mobile decision tree (first match wins, evaluate top-down)228229| Condition (read in order) | primary_action | secondary_action | final_route | Extra fields |230|---|---|---|---|---|231| `line.status == "Suspended"` (any suspension_reason) AND customer is ready to pay (per queue note) | `SEND_PAYMENT_REQUEST` | `RESUME_LINE_REBOOT` | BILLING_RECOVERY | `bill_id` = overdue bill; `charge_amount_usd` = bill.amount_due_usd |232| `line.suspension_reason == "FRAUD"` or fraud-indicated (no pay resolution) | `TRANSFER_HUMAN` | `NO_ACTION` | HUMAN_TRANSFER | — |233| `device.sim_status == "missing"` and (issue_type=NO_SERVICE or speed_test=no_connection) | `RESEAT_SIM` | `NO_ACTION` | SELF_SERVICE (or DEVICE_SETTING_FIX in Flavor-2 if NO_SERVICE excluded) | — |234| `device.sim_status` indicates locked ("locked" / repeated PIN) and no other setting fix | `RESET_APN_REBOOT` | `NO_ACTION` | SELF_SERVICE | — |235| `case.customer_location == "abroad"` AND `line.roaming_enabled == false` (phone has roaming on but line side missing) | `ENABLE_LINE_ROAMING` (Flavor-1) / `ENABLE_LINE_ROAMING` (Flavor-2) | `NO_ACTION` | CARRIER_UPDATE | `carrier_update_required = true` |236| `case.customer_location == "abroad"` AND `line.roaming_enabled == true` AND `device.phone_roaming_enabled == false` | `TOGGLE_ROAMING` | `NO_ACTION` | SELF_SERVICE (Flavor-1) / DEVICE_SETTING_FIX (Flavor-2 if applicable) | — |237| `device.airplane_mode == true` | `TOGGLE_AIRPLANE_MODE` | `NO_ACTION` | SELF_SERVICE | — |238| `device.mobile_data_enabled == false` | `TOGGLE_MOBILE_DATA` | `NO_ACTION` | SELF_SERVICE (Flavor-1) / DEVICE_SETTING_FIX (Flavor-2) | — |239| `device.vpn_connected == true` (slow_data or poor speed_test) | `DISCONNECT_VPN` | `NO_ACTION` | SELF_SERVICE (Flavor-1) / DEVICE_SETTING_FIX (Flavor-2) | — |240| `device.data_saver_mode == true` (slow_data) | `TOGGLE_DATA_SAVER` | `NO_ACTION` | SELF_SERVICE (Flavor-1) / DEVICE_SETTING_FIX (Flavor-2) | — |241| `device.network_mode_preference == "3g_only"` or similar old mode (slow_data, not already matched) | `SET_NETWORK_MODE` | `NO_ACTION` | SELF_SERVICE (Flavor-1) / DEVICE_SETTING_FIX (Flavor-2) | — |242| `case.issue_type == "MMS"` AND `device.can_send_mms == false` AND `device.mmsc_url_present == true` AND any `messaging_permissions.{sms,storage} == false` | `GRANT_MESSAGING_PERMISSION` | `NO_ACTION` | SELF_SERVICE | `permission`: sms / storage / sms_and_storage corresponding to the missing flag(s) |243| `line.data_used_gb >= plan.data_limit_gb` (data cap hit) and customer accepts refuel (`customer_preferences.accepted_refuel_gb` in payload) | `REFUEL_DATA` (Flavor-1) / `REFUEL_DATA` (Flavor-2: takes priority over REFUEL_DATA variant) | `NO_ACTION` | BILLING_RECOVERY (Flavor-1) / DATA_RECOVERY (Flavor-2) | `data_refuel_gb` = accepted_refuel_gb; `charge_amount_usd` = `accepted_refuel_gb × plan.data_refueling_price_per_gb` |244| None of the above resolves the case | `TRANSFER_HUMAN` | `NO_ACTION` | HUMAN_TRANSFER | — |245246Permission field semantics (Flavor-1 MMS):247- `messaging_permissions.sms == false && storage == false` → `permission = "sms_and_storage"`248- `messaging_permissions.sms == false` only → `permission = "sms"`249- `messaging_permissions.storage == false` only → `permission = "storage"`250- nothing missing → `permission = "NONE"` (shouldn't happen for a GRANT action)251252Evidence mapping:253- CASE-2101: NO_SERVICE, home, line Active roaming true, sim_status=missing → RESEAT_SIM → SELF_SERVICE254- CASE-2102: NO_SERVICE, line.status=Suspended, suspension_reason=OVERDUE_BILL, customer ready →255 SEND_PAYMENT_REQUEST + RESUME_LINE_REBOOT, BILL-2102 amount=$86.40 → BILLING_RECOVERY256- CASE-2103: MOBILE_DATA abroad, line.roaming_enabled=true, device.phone_roaming_enabled=false →257 TOGGLE_ROAMING → SELF_SERVICE258- CASE-2104: MMS, device.can_send_mms=false, mmsc_url_present=true, storage=false →259 GRANT_MESSAGING_PERMISSION, permission="storage" → SELF_SERVICE260- CASE-2105: SLOW_DATA, vpn_connected=true → DISCONNECT_VPN → SELF_SERVICE261- CASE-2501: data_used_gb=16.2 ≥ plan.data_limit_gb=15.0, customer_preferences.accepted_refuel_gb=2.0,262 plan=PLAN-PREMIUM data_refueling_price_per_gb=2.0 → REFUEL_DATA, refuel_gb=2.0, charge=$4.00 → DATA_RECOVERY263- CASE-2502: abroad, line.roaming_enabled=false, device.phone_roaming_enabled=true →264 ENABLE_LINE_ROAMING, carrier_update_required=true → CARRIER_UPDATE265- CASE-2503: data_saver_mode=true → TOGGLE_DATA_SAVER → DEVICE_SETTING_FIX266- CASE-2504: network_mode_preference="3g_only" → SET_NETWORK_MODE → DEVICE_SETTING_FIX267- CASE-2505: mobile_data_enabled=false → TOGGLE_MOBILE_DATA → DEVICE_SETTING_FIX268269### 3.2 Final-route conventions by flavor270271| Action | Self-service route |272|---|---|273| TOGGLE_ROAMING | Flavor-1: SELF_SERVICE. Flavor-2: DEVICE_SETTING_FIX. |274| RESEAT_SIM, GRANT_MESSAGING_PERMISSION, DISCONNECT_VPN, SET_NETWORK_MODE, TOGGLE_DATA_SAVER, TOGGLE_MOBILE_DATA | Flavor-1: SELF_SERVICE. Flavor-2: DEVICE_SETTING_FIX. |275| SEND_PAYMENT_REQUEST + RESUME_LINE_REBOOT | Flavor-1: BILLING_RECOVERY. (Same action set not used in Flavor-2.) |276| REFUEL_DATA | Flavor-1: BILLING_RECOVERY (per full enum). Flavor-2: DATA_RECOVERY. |277| ENABLE_LINE_ROAMING | Both flavors: CARRIER_UPDATE. |278| TRANSFER_HUMAN | Both flavors: HUMAN_TRANSFER. |279280`carrier_update_required=true` whenever primary_action=ENABLE_LINE_ROAMING (and only then) in Flavor-2.281282### 3.3 Charge calculation283284`charge_amount_usd` for billing / refuel cases:285- Billing-recovery (payment request): `charge_amount_usd = overdue_bill.amount_due_usd` (two decimals).286- Data refuel: `charge_amount_usd = accepted_refuel_gb × plan.data_refueling_price_per_gb` (two decimals).287- All non-billing actions: `charge_amount_usd = 0.00`.288289### 3.4 Summary rollups290291| Flavor-1 queue_summary | Count |292|---|---|293| self_service_fixes | tickets with final_route=SELF_SERVICE |294| billing_recoveries | tickets with final_route=BILLING_RECOVERY |295| carrier_updates | tickets with final_route=CARRIER_UPDATE |296| human_transfers | tickets with final_route=HUMAN_TRANSFER |297298| Flavor-2 worklist_summary | Value |299|---|---|300| data_refuel_cases | count primary_action=REFUEL_DATA |301| carrier_updates | count primary_action=ENABLE_LINE_ROAMING |302| device_setting_fixes | count final_route=DEVICE_SETTING_FIX |303| human_transfers | count primary_action=TRANSFER_HUMAN |304| total_estimated_customer_charge_usd | sum of all `charge_amount_usd` (two decimals) |305306Flavor-1 `charge_amount_usd` is "number with two decimals"; Flavor-2 too.307Flavor-2 `data_refuel_gb` is "number with one decimal", `0.0` when N/A.308309## 4. Family C — Enterprise export-complaint response package310311For each enterprise complaint payload (client_complaint_email.txt + response_requirements.json):312313### 4.1 Identify the incident3143151. `GET /api/enterprise/incidents` → find by `incident_id` (often present in the complaint email,316 e.g., "INC-7301") OR by matching `summary` keyword against the complaint subject/body.3172. Pull `enterprise_account_id`, `engineering_owner`, `account_owner`, `severity` from the318 incident record (these become `engineering_owner`/`account_owner`/`severity` in the response319 package — they are authoritative, do not derive from the email).320321### 4.2 Identify failed export window and backfill322323`GET /api/enterprise/export-runs?incident_id=<incident_id>`.324Filter to runs where `status == "FAILED"`. Sort by `run_date` ascending.325- `failure_window.start_date` = first failed run_date.326- `failure_window.end_date` = last failed run_date.327- `failure_window.failed_days` = count of failed runs.328- `backfill_days` = `failed_days` (manual backfill equals the number of failed runs).329- `root_cause_category` = human-readable summary derived from the failed runs' common330 `failure_code` plus message-board evidence (see below). E.g.,331 `STALE_CREDENTIAL` ("scheduler still references old secret" message) →332 `"stale credential after rotation"`.333 `STAGING_STORAGE_QUOTA` ("staging bucket reached quota") → `"staging bucket quota"`.334335### 4.3 SLA credit336337`GET /api/enterprise/sla/<enterprise_account_id>`:338- `sla_credit_percent` = `sla_contract.monthly_export_credit_percent` (integer percent).339- The `credit_trigger` text is supporting evidence (e.g., "3 consecutive failed export runs"340 vs "critical export outage longer than 72 hours") — verify the failure condition is satisfied341 before asserting the credit in the package; the credit_percent value itself is taken verbatim.342343### 4.4 Contributing alert issue344345`GET /api/enterprise/messages?query=<client keyword or incident id>` and inspect the channels.346- If the early-alert message has `channel` containing "archive" (e.g., `export-alerts-archive`)347 → `contributing_alert_issue = "ARCHIVED_ALERT_ROUTE"`.348- If no such archive-route evidence → `"NONE"` or `"UNKNOWN"` per template enum.349350### 4.5 Owners and channel/evidence/report naming351352`GET /api/enterprise/accounts` → find by `enterprise_account_id`:353- `account_owner` = the enterprise account's `account_owner` (authoritative, matches incident).354- `engineering_owner` = the incident's `engineering_owner` (authoritative).355- `finance_owner` = the enterprise account's `finance_owner` (used for share permission view tier).356357Naming conventions (per `response_requirements.naming_style`):358- `channel_name`: client name → lowercase, hyphenated, punctation stripped. E.g.,359 "Asteri Retail Inc." → `asteri-retail-inc`.360- `evidence_folder`: `"<Client Name> <Month Year> Investigation"`. Use month/year of the failure361 window start. E.g., failure start 2026-05-12 → `"Asteri Retail Inc. May 2026 Investigation"`.362- `report_title`: `"<Client Name> Export Failure - Resolution Report"`.363364`response_status`:365- `sla_credit_percent > 0` → `"NEEDS_FINANCE_REVIEW"`.366- (Coverage for other statuses in the enum READY_TO_SEND / NEEDS_ENGINEERING_REVIEW /367 UNDER_INVESTIGATION should drive off the SLA-credit result and incident.status.)368 Observed: closed SLA-credit case → NEEDS_FINANCE_REVIEW.369370### 4.6 Share permissions371372Take `response_requirements.permission_users_to_include` in order. For each user:373- If user matches `enterprise_account.finance_owner` → `"view"`.374- The next listed user (typically engineering/owner role) → `"edit"`.375- Any further users not on the finance tier → `"upload_only"` (enum value; inferred for the376 third-tier case).377378Order the output array exactly as listed in permission_users_to_include.379380Evidence (train_003): permission_users_to_include=["laura.brown","jun.chen"] →381[{user:laura.brown,permission:view}, {user:jun.chen,permission:edit}].382laura.brown is ENT-3001.finance_owner → view tier.383384### 4.7 Response-package field reference385386| Field | Source |387|---|---|388| `incident_id` | enterprise incident (or complaint email) |389| `enterprise_account_id` | enterprise incident |390| `root_cause_category` | failure_code + message body (human-readable short summary) |391| `contributing_alert_issue` | ARCHIVED_ALERT_ROUTE / NONE / UNKNOWN (per message channel) |392| `failure_window.start_date / end_date / failed_days` | min/max/count of FAILED export runs |393| `backfill_days` | = failed_days |394| `sla_credit_percent` | sla contract monthly_export_credit_percent |395| `severity` | enterprise incident.severity (Critical/High/Medium/Low) |396| `engineering_owner` | enterprise incident.engineering_owner |397| `account_owner` | enterprise incident.account_owner (matches enterprise account) |398| `channel_name` | client name lowercased-hyphenated |399| `evidence_folder` | "<Client Name> <FailureMonth Year> Investigation" |400| `report_title` | "<Client Name> Export Failure - Resolution Report" |401| `share_permissions[]` | ordered list with view (finance_owner)/edit/upload_only per §4.6 |402| `response_status` | NEEDS_FINANCE_REVIEW when sla_credit_percent > 0 |403404## 5. Audit-math definitions (when test prompts request these)405406These are computed over the batch of tickets the test prompt includes. Always compute407over the tickets whose `diagnostic_needed` flag became `true` for diagnostic audits,408and over ESCALATED/RESOLVED-with-troubleshooting tickets for post audits.409410`bandwidth_floor(tkt) = 0.80 × tkt.subscribed_mbps`411 `latency_floor = 100 (ms)`412 `jitter_floor = 30 (ms)`4134141. `pre_troubleshooting_bandwidth_gap_total_mbps`:415 Sum over every diagnostics-used ticket (those with `diagnostic_needed == true`) whose416 `diagnostics.bandwidth_mbps < bandwidth_floor` of `(bandwidth_floor - bandwidth_mbps)`.417 Units: Mbps. Gated-out tickets are excluded (even if their API diagnostic record exists).4184192. `diagnostic_records_skipped_by_gate`:420 Count of tickets where `diagnostic_needed == false` BUT the API actually returns a421 non-empty diagnostics record at `/api/diagnostics/<ticket_id>`. (These records were422 available but skipped because the gate decided the ticket.)4234243. `post_troubleshooting_remaining_issue_flags`:425 For each "unresolved diagnostic ticket" (diagnostic_needed=true AND426 final_resolution_status==ESCALATED) count how many of427 {post_latency_issue, post_stability_issue, post_bandwidth_issue} are still true428 against the floors. Sum across all unresolved diagnostic tickets.4294304. `post_threshold_excess_totals`:431 For each unresolved diagnostic ticket, compute:432 - `latency_excess = max(0, troubleshooting.post_latency_ms - 100)`433 - `jitter_excess = max(0, troubleshooting.post_jitter_ms - 30)`434 - `bandwidth_short = max(0, bandwidth_floor(tkt) - troubleshooting.post_bandwidth_mbps)`435 Sum (latency_excess + jitter_excess + bandwidth_short) across all unresolved436 diagnostic tickets. Report as a single total (ms + ms + Mbps aggregated).4374385. `tickets_using_post_troubleshooting_records`:439 Count of tickets with `diagnostic_needed == true` AND a non-empty440 `/api/troubleshooting/<ticket_id>` record (i.e., post_ metrics were used to decide441 RESOLVED vs ESCALATED).4424436. `tickets_with_active_outage_match`:444 Count of tickets for which an active outage (active=true, service_type in445 outage.service_types) sits in the ticket's service_area.4464477. `unique_escalation_teams`:448 Sorted ascending set of distinct `escalation_team` values across the batch449 (excluding NONE).4504518. `root_cause_escalation_ticket_ids` (by team):452 Map: team_name → sorted list of ticket_ids whose final escalation_team == team_name453 (i.e., the ESCOLATED tickets, grouped by their resolved escalation team per §2.4).454 Only populate teams that have at least one ticket.4554569. `post_success` and `post_failure` id lists:457 - `post_success_ids` = sorted ascending list of ticket_ids whose458 final_resolution_status == RESOLVED (after troubleshooting, all post flags cleared).459 - `post_failure_ids` = sorted ascending list of ticket_ids whose460 final_resolution_status == ESCALATED (post still has at least one issue).461 Both limited to tickets that went through diagnostics (had a troubleshooting recheck).46246310. Per-ticket floor + shortfall list (sorted by ticket_id ascending):464 For each diagnostics-used ticket, report:465 - `ticket_id`466 - `bandwidth_floor_mbps = 0.80 × subscribed_mbps`467 - `bandwidth_shortfall_mbps = max(0, bandwidth_floor_mbps - diagnostics.bandwidth_mbps)`468 - `latency_floor_ms = 100`469 - `latency_excess_ms = max(0, diagnostics.latency_ms - 100)`470 - `jitter_floor_ms = 30`471 - `jitter_excess_ms = max(0, diagnostics.jitter_ms - 30)`472 Sorted ascending by ticket_id.473474## 6. Pitfalls and edge cases475476- **API URLs in prompts lie**: any prompt printing `http://127.0.0.1:8057` (or similar)477 must be ignored; the base URL is `<remote-env-url>`. Apply to every endpoint478 substitution.479- **Gated-out tickets may still have diagnostics / troubleshooting records**: those are480 "skipped by gate" for the audit. Do NOT compute pre issue-flags for them in the per-ticket481 answer; their flags go to `false` in Flavor-1.482- **Bandwidth ratio only weakly constrained**: train data narrows it to483 0.697 < ratio ≤ 0.907 from TCK-5107 alone. Commit to 0.80 unless a test item484 contradicts (then revise proportionally). The jitter floor (≤21 to <33.5) and latency485 floor (≤82 to <97 from TCK-5402 pre being just below) commit to 30 ms and 100 ms.486- **Outage service_type overlap**: an outage applies only if the ticket's `service_type`487 is in outage.service_types. A plain `active == true` in the right service_area does488 not suffice — service_type must match.489- **Order preservation**: preserve the CSV payload order in `ticket_decisions`;490 preserve ascending case_id order in `case_decisions`.491- **Permission users order**: in Flavor-C, output share_permissions in the exact order of492 `response_requirements.permission_users_to_include` (do NOT sort alphabetically).493- **`route_team` for OVERDUE_SUSPENSION is ACCOUNTS_PAYABLE even though the ticket is494 FAILED**: the per-team summary counts include this ticket. Same for FRAUD_SUSPENSION495 if the test sets its team accordingly.496- **Two mobile output flavors**: same underlying decision tree, but Flavor-2 collapses497 billing/billing-recovery routes to DATA_RECOVERY and merges self-service fixes into498 DEVICE_SETTING_FIX. Watch the answer template before deciding which fields to emit.499- **REFUEL_DATA needs both**: line.data_used_gb ≥ plan.data_limit_gb AND explicit500 customer_preferences.accepted_refuel_gb in the worklist payload. If the customer501 refuses, fall through to other matches (often TRANSFER_HUMAN).502- **root_cause → team matching**: case-insensitive substring match. A single root_cause503 may contain multiple keywords (BACKBONE_CAPACITY matches both `backbone` and504 `capacity` → NETWORK_ENGINEERING).505- **failure_window count vs duration**: `failed_days` is the count of FAILED export runs,506 NOT the calendar span (e.g., 2026-05-12 to 2026-05-14 = 3 dates = 3 FAILED runs).507- **SLA credit value comes verbatim from the SLA contract**: never recompute from the508 number of failed days. Validate the trigger text is satisfied, but use the contract's509 `monthly_export_credit_percent` literally.510- **response_status**: when SLA credit > 0, status is NEEDS_FINANCE_REVIEW (finance_owner511 gets view permission in the share list); engineering gets edit. Verify against512 incident.status and sla.credit_trigger if the prompt insists on a different status enum.513514## 7. Quick threshold summary (commit these numbers)515516- **Latency floor: 100 ms** (inferred — TCK-5107 post=82 cleared, TCK-5407 post=121 still failing).517- **Jitter floor: 30 ms** (inferred — TCK-5107 post=21 cleared, all ESCALATED posts ≥32).518- **Bandwidth ratio: 0.80** of `subscribed_mbps` (inferred — TCK-5107 pre=209/300 fails,519 post=272/300 passes; ratio window is 0.697–0.907).