toga-ai 1.0.715 → 1.0.717

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.
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-30
9
+ updated: 2026-08-31
10
10
  owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
@@ -732,6 +732,35 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
732
732
  line). **Fix (`library`):** skip lines whose `adjustQtyBy` is null in **BOTH** passes over
733
733
  `inventoryList->inventory` (the item-build pass and the serial/unit pass). Shared fix; unblocked NYCHH
734
734
  inventory adjustments.
735
+ - **⚠ Item-receipt sync froze on a PARTIAL receipt's NULL-quantity PO lines — the item-receipt TWIN of the
736
+ inventory-adjustment NULL-quantity fix above (fixed 2026-08-31).** For a partial item receipt NetSuite
737
+ returns **all** the PO's linked lines but sets `quantity=null` on the lines **not received on this
738
+ receipt** (SuiteQL `transactionline.quantity` is NULL for an un-received PO line). The REST shim
739
+ `App_Api_Netsuite_Rest::fetchItemReceiptById` (`rest.php` ~L2196) faithfully carries that null, and
740
+ `syncItemReceiptFromNetsuite` wrote those null lines into the `POST /v2/item-receipts` payload →
741
+ `ItemReceiptItems.quantity` is `NOT NULL` → api2 `EV-10` / MySQL **1048** "Column 'quantity' cannot be
742
+ null", the section threw (no per-record try/catch — fail-loud by design), and froze at `1-RUNNING`
743
+ re-trying the same poison receipt every 5 min. Seen on **NYCHH** ITEM_RECEIPTS: stuck on receipt **#22302**
744
+ (NetSuite internalId 3425518, dated 2023-05-01; 5 PO lines, 2 received qty 260, 3 returned null), watermark
745
+ frozen at 2023-05-01, throwing every run (`Logs.Issue 578`, "Invalid API Response" from
746
+ `send()` `toga2.php:3517`). **Fix (`library`):** skip line items whose `quantity` **`is_null`** (NOT
747
+ `empty`, so a legitimate received qty of 0 still writes) in **BOTH** passes over
748
+ `$nsItemReceipt->itemList->item` — the payload-build loop (~L3376) and the unit/serial pass (~L3538).
749
+ Shared fix; no signature change, no SQL. Recovery is automatic: the watermark sits at the failing window's
750
+ start, so on deploy the section self-heals (RUNNING→IDLE, watermark advances past #22302, window regrows).
751
+ Diagnosis chain (reinforces the playbook above): `Client_Nychh.Parameters` to localize the stuck section
752
+ (`1-RUNNING` + frozen watermark) → `Logs.Event` filtered by `clientId=3` for the crash file:line +
753
+ `transactionId` (`Logs.Event` carries `clientId`, `Logs.Issue` does not) → `Logs_Nychh.Api` on that
754
+ `transactionId` for the untruncated `EV-10`/1048 rejection and the payload showing the `quantity:null`
755
+ lines.
756
+ - **⚠ OPEN, not yet root-caused (found 2026-08-31): NYCHH ITEM_FULFILLMENTS watermark frozen at 2021-12-29.**
757
+ `NETSUITE_EXECUTION_MODE_ITEM_FULFILLMENTS='3-IDLE'` with the window collapsed to ~3s (it has been
758
+ throwing recently) but currently **IDLE** — so it crawls a few seconds per run rather than sitting in a
759
+ hard `1-RUNNING` freeze. Distinct from the fixed ITEM_RECEIPTS freeze above; needs its own investigation.
760
+ Context: the 2026-08-31 full 19-client togasupply prod sweep found all other clients healthy
761
+ (`864000-IDLE`) except Elite SALES_ORDERS `864000-RUNNING` (healthy mid-run, large window, not stuck) and
762
+ AIG SALES_ORDERS `8-RUNNING` (the known held account, already documented). Only NYCHH ITEM_RECEIPTS was in
763
+ a true `1-RUNNING` freeze.
735
764
  - **⚠ Item-fulfillment stale-item prune must delete tracking-number children before the unit AND the item
736
765
  (fixed 2026-08-30).** In `syncItemFulfillmentFromNetsuite`'s cleanup loop, deleting a stale
737
766
  item-fulfillment-item-**unit** failed api2 `EV-11` / MySQL **1451** (FK
@@ -792,6 +821,16 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
792
821
 
793
822
  ## Change history
794
823
 
824
+ - 2026-08-31 — **Item-receipt partial-receipt NULL-quantity fix (shared, `library`).**
825
+ `syncItemReceiptFromNetsuite` now **skips line items whose `quantity` is null** (unreceived PO lines on a
826
+ partial receipt) in both passes over `$nsItemReceipt->itemList->item` (payload-build ~L3376 + unit/serial
827
+ ~L3538), guarded on `is_null` so a received qty of 0 still writes. This is the item-receipt twin of the
828
+ 2026-08-30 inventory-adjustment NULL-quantity skip — the receipt path never got that guard. It unfroze
829
+ NYCHH ITEM_RECEIPTS (stuck `1-RUNNING` on receipt #22302 / NetSuite 3425518 / 2023-05-01 → `EV-10`/1048
830
+ "quantity cannot be null", `Logs.Issue 578`); self-heals on deploy. Also flagged an OPEN item: NYCHH
831
+ ITEM_FULFILLMENTS watermark frozen at 2021-12-29 (`3-IDLE`, window collapsed to ~3s), not yet root-caused.
832
+ 19-client sweep otherwise healthy. `library` only, no SQL, `php -l` clean; not deployed at capture time.
833
+ (jcardinal)
795
834
  - 2026-08-31 — **Invoice + serial-unit + duplicate-Item root-cause fixes (shared, `library`).**
796
835
  `syncInvoiceFromNetsuite` now assigns a **sequential 1..N invoice `lineNumber`** (NetSuite reuses `line`
797
836
  across multi-SO invoices → `InvoiceItems UNIQUE(invoiceId,lineNumber)` 1062 → atomic rollback → NYCHH
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-28
9
+ updated: 2026-08-31
10
10
  owners: ["jcardinal", "mhammontree", "bala"]
11
11
  files:
12
12
  - worker2/Worker/Monitor/Compass.php
@@ -65,9 +65,24 @@ Each measures one Compass/Office Depot integration metric and pushes a decided a
65
65
  - **OfficeDepotEdiImportQueue** (S3) — counts files >10 min old in
66
66
  `s3://agilant-as2/OfficeDepot/`, excluding `OUTBOX/`, `SENT/` and directory placeholders.
67
67
  - **CompassSalesOrderTransmission** (DB) — approved SOs (both approval stages) with
68
- `c_dtTransmittedToCompass` NULL (not yet transmitted to MITS).
68
+ `c_dtTransmittedToCompass` NULL (not yet transmitted to MITS). Alarms on **either**
69
+ `pendingCount > 10` **OR** `oldestPendingAgeMinutes > 180` (3h), so a single order stuck too
70
+ long pages even under the count threshold — this catches the 1.0 cron's
71
+ [poison-order starvation](../../../clients/compass-usa/features/mits-sales-order-transmission-alerting.md).
72
+ **Age is measured from the FINAL (stage-2) approval decision** (`ApprovalDecisions` stage 2
73
+ `dtDecision` where `isApproved = 1` — not stage 1, not order date; that is the "eligible to
74
+ transmit" clock). The query **excludes canceled orders** (`salesOrderStageId IS NULL OR
75
+ <> 8`), mirroring the transmit cron's WHERE, so an order canceled after approval (e.g.
76
+ SA136567, now stage 8) neither counts nor false-alarms forever — this also fixed a
77
+ pre-existing count mismatch vs. the cron. Config: `$ALARM_AGE_MINUTES = 180`; body echoes
78
+ `oldestPendingAgeMinutes` + `alarmAgeMinutesThreshold`.
69
79
  - **CompassIncomingPurchaseOrders** (DB) — `SA%` SOs transmitted to MITS >4h ago (on/after
70
- the 2026-04-30 cutoff) with no PO returned.
80
+ the 2026-04-30 cutoff) with no PO returned. (This supersedes the 1.0
81
+ `App_SystemMonitor_Compass` "orders returned from MITS" sub-check, which is a **latent
82
+ no-op**: it selects `so`/`number`/etc but then reads `$row['pendingCount']`, a column the
83
+ query never returns, so `$pendingCount` is always null and that sub-check silently always
84
+ reports OK. Verified 2026-08-31 in `library/app/systemmonitor/compass.php` ~lines 111-140,
85
+ TRUE-78712.)
71
86
  - **CompassPurchaseOrderTransmissionsToVendors** (DB) — unsubmitted POs for monitored
72
87
  vendors (Office Depot, Strategic Systems, Compass, Presidio) due today or earlier.
73
88
  - **EmailedAsnImport** (mailbox) — counts inbox messages in the emailed-ASN mailbox via
@@ -415,6 +430,18 @@ check for another client.
415
430
  monitors must pass the region (see [Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md)).
416
431
 
417
432
  ## Change history
433
+ - 2026-08-31 — Added **age-based detection** to `CompassSalesOrderTransmission()`: it now alarms
434
+ on `pendingCount > 10` **OR** `oldestPendingAgeMinutes > 180` (new `$ALARM_AGE_MINUTES = 180`),
435
+ so one order stuck too long pages even under the count threshold — catching the 1.0 transmit
436
+ cron's poison-order starvation. Age is measured from the **stage-2** (final) approval decision,
437
+ not stage 1 or order date. The query now also **excludes canceled orders** (`salesOrderStageId
438
+ IS NULL OR <> 8`), mirroring the transmit cron's WHERE, fixing a pre-existing count mismatch and
439
+ a canceled order (SA136567) false-alarming forever. New body fields `oldestPendingAgeMinutes` +
440
+ `alarmAgeMinutesThreshold`; no OneUptime criteria change needed — the age alarm rides the
441
+ existing `"alarm":"HIGH"` token ("dumb reporter, smart monitor"). Also recorded that the 1.0
442
+ `App_SystemMonitor_Compass` "orders returned from MITS" sub-check is a **latent no-op** (reads
443
+ `$row['pendingCount']`, a column its query never selects — always reports OK; TRUE-78712),
444
+ superseded by `CompassIncomingPurchaseOrders`. worker2-only, php -l passes, not deployed. (jcardinal)
418
445
  - 2026-08-28 — Made `EmailedAsnImport()` resilient to a transient IMAP TLS handshake timeout
419
446
  that was flipping the monitor Offline and paging even though it self-recovered next run:
420
447
  wrapped token+connect+count in an **in-run retry loop** (`MAX_CONNECT_ATTEMPTS = 3`,
@@ -6,7 +6,7 @@ client: compass-usa
6
6
  type: client-feature
7
7
  status: active
8
8
  updated: 2026-08-31
9
- owners: ["dfranks", "bala"]
9
+ owners: ["dfranks", "bala", "jcardinal"]
10
10
  files:
11
11
  - worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php
12
12
  - worker/crons/toga2/compasscanada/workflow/1_transmit_compassCanada_sales_orders_to_mits.php
@@ -100,6 +100,44 @@ Configuration is **never retroactive**: `shouldNotify()` fires on the first occu
100
100
  escalation, or when the backed-off reminder interval elapses, and returns false for a window with
101
101
  zero events. The first email lands on the **next live rejection**, not on existing history.
102
102
 
103
+ ## Why the backlog happens — poison-order starvation (root cause)
104
+
105
+ The alert almost never fires on **volume**. It fires because one bad order **aborts the whole
106
+ run** and every un-processed order behind it starves. Diagnosed 2026-08-31 against a live
107
+ backlog (0 new, several >24h, one ~20 days old).
108
+
109
+ - The cron loops approved-but-untransmitted orders and `throw`s on any MITS rejection
110
+ (`App_Exception_Business` on a non-200, plain `Exception` on "No Items Found"). A throw
111
+ **ends the entire run**, so every order not yet reached that run stays untransmitted.
112
+ - The driver query uses **`ORDER BY RAND()`**, so *which* orders get processed before the bad
113
+ one is random each run. A stuck "poison" order therefore blocks a **different** set of good
114
+ orders every run — good orders can wait days behind one bad one.
115
+ - Throughput ceiling: the cron sleeps **30s per order** (86400s time limit), scheduled every
116
+ 3 min but overlap-guarded, so it is effectively one long-running instance — ~110 orders/hr.
117
+
118
+ **Two rejection signatures seen in prod** (Logs.Issue id 24, issueKey
119
+ `COMPASS_SALES_ORDER_MITS_TRANSMIT_REJECTED`, ~2329 occurrences since 2026-08-03):
120
+
121
+ 1. **HTTP 400 `The AssetType field is required`** — a line item's catalog Item has
122
+ `assetTypeId = NULL`, so the payload `assetType` is null and MITS rejects the **whole
123
+ order**. This was the active blocker (e.g. item `4U555A-S`; ~10 distinct NULL-assetType
124
+ items blocked 9 of 11 pending orders).
125
+ 2. **HTTP 403 `SalesOrder:Exists`** — MITS already created the order on a prior attempt, but
126
+ our side never recorded `c_dtTransmittedToCompass`, so the cron **retries it forever**.
127
+
128
+ **Fix the data:** assign the correct `assetTypeId` to the affected `Client_Compass.Items`
129
+ (business owns the mapping), and check whether the NetSuite item sync owns `assetType` — if it
130
+ does, it will revert the fix unless the source is corrected too.
131
+
132
+ **Recommended code hardening (NOT yet implemented):** wrap each order's transmit in
133
+ `try/catch` so one MITS rejection raises the business issue but **continues to the next
134
+ order** instead of aborting the batch. This removes the starvation entirely.
135
+
136
+ The worker2 OneUptime monitor now catches this starvation directly: `CompassSalesOrderTransmission`
137
+ alarms on `pendingCount > 10` **OR** `oldestAgeMinutes > 180`, so a single order stuck too long
138
+ pages even when the count is under threshold. See
139
+ [OneUptime push-metric monitors for 2.0 workers](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md).
140
+
103
141
  ## Gotchas / known issues
104
142
 
105
143
  - **Canada is the pre-2026-08-03 design, and it still carries the two problems that migration
@@ -131,6 +169,14 @@ zero events. The first email lands on the **next live rejection**, not on existi
131
169
  when this issue next fires. Watch that run rather than assuming it works.
132
170
 
133
171
  ## Change history
172
+ - 2026-08-31 — Diagnosed the recurring backlog's **root cause**: not volume but **poison-order
173
+ starvation**. One MITS rejection `throw`s and aborts the whole run, and because the driver
174
+ query uses `ORDER BY RAND()`, a stuck order blocks a random set of good orders every run.
175
+ Recorded the two live rejection signatures (`assetTypeId = NULL` → HTTP 400 "The AssetType
176
+ field is required"; already-created order → HTTP 403 "SalesOrder:Exists" retried forever),
177
+ the data fix (assign `assetTypeId`; check NetSuite item sync ownership), and the recommended
178
+ per-order `try/catch` hardening (not yet implemented). Cross-linked the new worker2 age-based
179
+ monitor. (jcardinal)
134
180
  - 2026-08-31 — Added `vburks@togatech.com` alongside `devteam@togatech.com` on **every** in-cron
135
181
  `App_Email_Agilant` alert in both MITS transmit crons (USA has one, Canada has three), so the
136
182
  business stakeholder is alerted on transmission failures. Broadened this doc to cover **both**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.715",
3
+ "version": "1.0.717",
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",