toga-ai 1.0.868 → 1.0.869

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.
@@ -34,7 +34,7 @@ api2's front controller ("**Prong A**") can **detect a retriable concurrency fai
34
34
 
35
35
  The replay is **route-scoped by an allowlist** (`DEADLOCK_RETRIABLE_ROUTES` — currently `['/v2/advance-shipping-notices', '/v2/service-requests']`) — because whole-request in-process replay is **only safe for routes with no non-rollbackable external side effect**. The retry gate is now **`wasDeadlock() || wasRetriableUniqueRace()`**.
36
36
 
37
- > **Status (2026-09-22).** Working on `_sandbox-dev` and **proven under real concurrency** (10/10 concurrent SR POSTs, zero duplicates). **Production still has NO race handling at all** for this — prod needs this fix plus the original `TRUE-82095` work. Prod evidence: 12 duplicate-number failures since 2026-08-18, latest `RITM6811124` / `REQ4510546`, which only succeeded on a manual resubmit as `SR131026`.
37
+ > **Status (2026-09-22).** Working on `_sandbox-dev` and **proven under real concurrency** (10/10 concurrent SR POSTs, zero duplicates). **Production still has NO race handling at all** for this — prod needs this fix plus the original `TRUE-82095` work. Prod evidence: 12 duplicate-number failures since 2026-08-18, latest `RITM6811124` / `REQ4510546`, which only succeeded on a manual resubmit as `SR131026`. The race is now **confirmed from production traffic**, not just the load test — see *Confirmed in production* below.
38
38
 
39
39
  **Constants (verified 2026-09-22):** `MAX_DEADLOCK_ATTEMPTS = 5` (not 3), backoff 20ms-120ms widening per attempt, `DEADLOCK_RETRIABLE_ROUTES = ['/v2/advance-shipping-notices', '/v2/service-requests']`.
40
40
 
@@ -92,6 +92,28 @@ Two consequences of the typed exception worth knowing:
92
92
 
93
93
  **Side benefit:** the same fix repairs **ASN** (`_Model_Compass_AdvanceShippingNotice`), which sets the same flag and had the identical exposure.
94
94
 
95
+ ### Confirmed in production — three same-second ServiceNow POSTs, one 1062 (2026-09-22)
96
+
97
+ The race is **not** synthetic. Real Prudential/ServiceNow bulk traffic reproduced it in prod, and prod is **still exposed** (the fix is on `_sandbox-dev` only).
98
+
99
+ Three POSTs to `/v2/service-requests` arrived from ServiceNow (`sourceIp 199.91.136.12`) inside the same second, **2026-09-21 23:27:47–48 CST**:
100
+
101
+ | `Logs_Prudential.Api.id` | Result | Number |
102
+ |---|---|---|
103
+ | 8736482 | **201** | `SR131023` |
104
+ | 8736484 | **201** | `SR131024` |
105
+ | 8736483 | **400 `EV-10`** | — |
106
+
107
+ The 400's `responsePayload` carried the exact failure: **`Duplicate entry 'SR131023' for key 'ServiceRequests.number'`**, MySQL errno **1062**, on `INSERT INTO ServiceRequests`. So all three reads of `MAX(number)` returned the same value; two won (`131023`, `131024`) and the third re-tried `131023` and hit the UNIQUE key. Exactly the unlocked `FIELD_AUTOINCREMENT` generator described above.
108
+
109
+ **⚠ Note the number in the error is NOT the failed request's number.** `SR131023` belongs to the *winner*. A client report of "duplicate SR131023" is about a request that never got a number at all — do not go looking for two `SR131023` rows.
110
+
111
+ ### ⚠ Open risk before the prod deploy — ServiceNow ALSO resends, so a fixed TOGA could create TWO SRs
112
+
113
+ The failed `REQ4510546` above was **re-sent from the ServiceNow side ~6 hours later** (2026-09-22 05:23:32 CST, `transactionId 179007261259565713857`) and succeeded as **`SR131026`**. So today the outside resend is the only recovery — and it works.
114
+
115
+ Once the `TRUE-82095` retry ships to prod, **TOGA retries internally within the same request**. If ServiceNow still resends the earlier failure on its own schedule, the same REQ can end up with **two ServiceRequests rows**. Confirm ServiceNow's resend behaviour with the client (contact: **Ferdaus**) before or alongside the prod deploy. Nothing in this retry protocol de-duplicates by `c_reqNumber`.
116
+
95
117
  ### Verifying a concurrency fix — the gate-file pattern
96
118
 
97
119
  A plain background loop spreads requests out over time and can **miss the collision window entirely**, so it produces a false pass. What works: N `curl` workers each **block on a shared gate file**, then a single `touch` releases all of them at the same instant.
@@ -124,6 +146,8 @@ Checked and **wrong**; recorded so it is not raised again. `Logs.Api.transaction
124
146
  - `ERRNO_DUPLICATE_ENTRY` is absent from `_beta`/`_sandbox-dev` — naming it directly fatals there; define a local `1062` constant and read `$lastErrorNumber` via `property_exists()`.
125
147
  - `DEADLOCK_RETRIABLE_ROUTES` is now `/v2/advance-shipping-notices` **and** `/v2/service-requests`; do not add a route without proving it is replay-safe (no irreversible side effect, no mutated superglobal).
126
148
  - **Every other `FIELD_AUTOINCREMENT` model has the same unlocked generator** — SalesOrder, PurchaseOrder, Ticket, ItemReceipt, InventoryAdjustment, TransferOrder — and **none is on `DEADLOCK_RETRIABLE_ROUTES`**, so a duplicate-number clash on those routes still returns 400. The durable answer is fixing the number generator so the read and the insert cannot split; that is a bigger, separate change.
149
+ - **The `SR…` number inside a `Duplicate entry` error belongs to the WINNER, not the failed request.** The loser never got a number. A "duplicate SR131023" report means one request was lost, not that two rows share a number.
150
+ - **ServiceNow resends a failed SR on its own** (proven: `REQ4510546` failed, came back ~6h later as `SR131026`). After the prod deploy adds an internal retry, an outside resend could produce a **second SR for the same REQ** — confirm with the client first.
127
151
  - Do not reach for this protocol for an HTTP 429 — use `_ApiRequest::setAutoRetry()`.
128
152
 
129
153
  ## Related
@@ -136,6 +160,7 @@ Checked and **wrong**; recorded so it is not raised again. `Logs.Api.transaction
136
160
  - [ENVIRONMENT decides the `_underscore` branch](./environment-variable-drives-underscore-branch.md)
137
161
 
138
162
  ## Change history
163
+ - 2026-09-22 — Added production confirmation of the race (3 same-second ServiceNow POSTs, `Logs_Prudential.Api` 8736482/8736483/8736484, 1062 on `SR131023`) and the open double-SR risk from ServiceNow's own resend. Investigation only, no code change. (rgirish)
139
164
  - 2026-09-22 — Found and fixed the reason this retry never fired on `/v2/service-requests`: the API logger's own 1062s overwrote `_Database::$lastErrorNumber`; now saved/restored in `try/finally` inside `saveApiLogUnderItsOwnTransactionId()`. Proven 10/10 under real concurrency on sandbox-dev. Also fixes ASN. Prod still unpatched. (rgirish)
140
165
  - 2026-09-22 — Corrected constants: `MAX_DEADLOCK_ATTEMPTS = 5` (was documented as 3); `DEADLOCK_RETRIABLE_ROUTES` now includes `/v2/service-requests`. (rgirish)
141
166
  - 2026-09-16 — Scoped the protocol against `_ApiRequest::setAutoRetry()` (MySQL vs HTTP retry). (bala)
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: prudential
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-16
10
- owners: ["bala"]
9
+ updated: 2026-09-22
10
+ owners: ["bala", "rgirish"]
11
11
  files:
12
12
  - worker/crons/notifications/reports/prudential/prudential_auth_rejection_alert.php
13
13
  - worker/schedules/cron.worker.notification.json
@@ -19,6 +19,7 @@ related:
19
19
  - ../profile.md
20
20
  - ../../../2.0/apps/api2/features/request-logging.md
21
21
  - ../../../2.0/apps/api2/features/v2-api-error-codes.md
22
+ - ../../../2.0/apps/api2/features/v2-deadlock-retry.md
22
23
  ---
23
24
  The 15-min cron that alerts on refused (401/403) inbound Prudential REQs, where each code is logged, and how to tell "we refused it" from "they never sent it"; open when a Prudential REQ is reported missing.
24
25
 
@@ -68,7 +69,25 @@ Two different failure shapes get reported the same way ("REQ is missing"):
68
69
 
69
70
  Always check **both** schemas before answering — a 401 will not be in `Logs_Prudential`.
70
71
 
72
+ ### Triaging a "duplicate SR number" report — the recipe (2026-09-22)
73
+
74
+ A third shape, and the report is misleading on its face: ServiceNow says *"duplicate `SR131023`"*, but **that number belongs to a different request that succeeded**. The reported REQ got no number at all — it lost the `FIELD_AUTOINCREMENT` race and died on `1062` (see [V2 request retry](../../../2.0/apps/api2/features/v2-deadlock-retry.md)). Do not go hunting for two rows with the same number.
75
+
76
+ Where the Prudential data actually lives:
77
+ - ServiceNow REQ/RITM land in `Client_Prudential.ServiceRequests` as **`c_reqNumber`** and **`c_ritmNumber`** — *not* `c_snowRequestNumber` / `c_snowRitmNumber`. The other ServiceNow columns on that table are `c_sysId` and `c_sysCreatedOn`.
78
+ - `ServiceRequests` has **no `dtModified` column** — the timestamps are `dtCreated` / `dtUpdated`.
79
+ - `c_sysCreatedOn` is ServiceNow time (**EST**); `dtCreated` is TOGA DB time (**CST**). **A one-hour gap between them is expected**, not a bug.
80
+
81
+ How to find the failure:
82
+ 1. Look the call up in `Logs_Prudential.Api` by **`transactionId`** — it is UNIQUE and indexed, so this is instant.
83
+ 2. Read the full MySQL error out of **`responsePayload.messages[].identifiers.error`** — that is where `Duplicate entry 'SR131023' for key 'ServiceRequests.number'` and errno `1062` live.
84
+ 3. Check whether the REQ came back later on its own: ServiceNow does resend. `REQ4510546` failed on 2026-09-21 and succeeded ~6h later as `SR131026`.
85
+
86
+ **⚠ Never run an unbounded `requestPayload LIKE '%REQ…%'` against `Logs_Prudential.Api`** — `requestPayload` is not indexed, so it full-scans the table and times out. Always bound it with a **`dtStamp` window plus `route` / `method`** first (`dtStamp` is indexed; `route` and `responseCode` are not).
87
+
71
88
  ## Gotchas
89
+ - **Prudential SR columns are `c_reqNumber` / `c_ritmNumber`**, there is no `dtModified` (it is `dtCreated`/`dtUpdated`), and `c_sysCreatedOn` (EST) sitting ~1h off `dtCreated` (CST) is correct.
90
+ - **Never `LIKE` on `requestPayload` without a `dtStamp` window** — it scans `Logs_Prudential.Api` and times out.
72
91
  - **Filter `method = 'POST'` or you get browser noise.** Unauthenticated **GET**s on this route from human sourceIps (`99.179.145.25`, `107.139.5.190`) also log 401, with an **empty `requestPayload`** — they show up as REQ "Unknown". Only a POST is a real ServiceNow submission.
73
92
  - **`prudential_api_500_retry.php` is broken and has almost certainly never worked.** At lines 57 and 70 it references a bare constant **`PRUDENTIAL_CLIENT_UUID` that is defined nowhere** in any repo (grep returns only its own two uses); it should be `App_Api_Toga2::CLIENT_UUID_PRUDENTIAL`. On PHP 8 an undefined constant is a **fatal Error**, so the cron dies every run; on PHP 7 it evaluates to the literal string and the client check never matches, so it silently retries nothing. It also appears in **no** `schedules/*.json` file, so it may never have been registered. **Not fixed as of 2026-09-10 — reported only.**
74
93
  - **`Logs.Api.dtStamp` IS indexed** (`Api_dtStamp_IDX`) in both base `Logs` and `Logs_Prudential`, which is what makes the 15-minute window read cheap. But `route` and `responseCode` are **not** indexed, so a wide `dtStamp` range plus a route filter still times out (a 2.5-month grouped query aborted at 300 s). Keep the window tight. See [request-logging.md](../../../2.0/apps/api2/features/request-logging.md).
@@ -78,3 +97,6 @@ Always check **both** schemas before answering — a 401 will not be in `Logs_Pr
78
97
  - [Service request address validation](service-request-address-validation.md)
79
98
  - [Prudential profile](../profile.md)
80
99
  - [V2 request logging](../../../2.0/apps/api2/features/request-logging.md), [V2 API error codes](../../../2.0/apps/api2/features/v2-api-error-codes.md)
100
+
101
+ ## Change history
102
+ - 2026-09-22 — Added the "duplicate SR number" triage recipe: real column names, where the MySQL error text sits in `responsePayload`, the `requestPayload LIKE` timeout trap, and the EST/CST one-hour offset. Prod investigation only, no code change. (rgirish)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.868",
3
+ "version": "1.0.869",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",