toga-ai 1.0.308 → 1.0.310
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.
|
@@ -7,7 +7,7 @@ client: shared
|
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
9
|
updated: 2026-07-09
|
|
10
|
-
owners: ["jcardinal", "dfranks"]
|
|
10
|
+
owners: ["jcardinal", "dfranks", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- crons/tickets.php
|
|
13
13
|
- crons/tickets_prod.php
|
|
@@ -40,6 +40,19 @@ TOGa Desk ingests support email into tickets through a cron-driven IMAP poller
|
|
|
40
40
|
3. **Parse & route (`emailToTicket()`):** extract from/to/cc/subject/body (with encoding fixes);
|
|
41
41
|
match an existing ticket via `In-Reply-To` + `Message-ID` headers and **plus-addressing**
|
|
42
42
|
(`support+<ticketnum>@…`). Matched ⇒ `addReply()`; otherwise ⇒ `add()`.
|
|
43
|
+
- **Noise suppression (before any ticket/attachment is created):** both feeders drop known
|
|
44
|
+
automated no-noise mail via the shared helper
|
|
45
|
+
`Ticket::isSuppressedNoiseEmail(?string $from, ?string $subject): bool` (static, in
|
|
46
|
+
`class.ticket.php`). It returns true only when the sender matches `/^no-?reply@/` **AND**
|
|
47
|
+
the subject starts with `staples po ` — both required so a real person is never blocked, and
|
|
48
|
+
the sender check is **domain-agnostic** (matches `noreply@togatech.com` /
|
|
49
|
+
`noreply@goagilant.com` and survives future sender-domain flips; the Staples PO alert sender
|
|
50
|
+
flipped `goagilant.com → togatech.com` on 2026-06-02). Wired into BOTH intake feeders:
|
|
51
|
+
`crons/tickets.php` adds it as one more `&&`-clause in the existing IMAP intake guard
|
|
52
|
+
(alongside the delivery-status / `MAILER-DAEMON` / Compass auto-reply filters);
|
|
53
|
+
`crons/pipe.php` uses it as an early guard-and-`exit(0)` right after the `$from`/`$subject`
|
|
54
|
+
parse. Each feeder logs a "suppressed" line. (TRUE-79975 — ~815 such alerts since Jun 2024,
|
|
55
|
+
~704 of them landing in the dept-1 "Support Desk" catch-all and being closed by hand.)
|
|
43
56
|
4. **Side effects:** auto-create a user when the sender domain maps to an existing client; assign
|
|
44
57
|
to a department via the department email lookup; save attachments locally then push to **S3**
|
|
45
58
|
(`App_Cloud::copyFileToS3()`, bucket `asifiles`); optionally generate Talos/TogaIQ summaries.
|
|
@@ -49,7 +62,12 @@ TOGa Desk ingests support email into tickets through a cron-driven IMAP poller
|
|
|
49
62
|
## Data model
|
|
50
63
|
Writes `tickets`, `tickets_replies`, attachment `files`; reads config from the `config` table
|
|
51
64
|
(`tickets_defaultdepartment`, `tickets_defaultclient`, `auto_close_tickets`,
|
|
52
|
-
`email_from_address`, `timezone`). Department routing keys on `tickets_departments.email
|
|
65
|
+
`email_from_address`, `timezone`). Department routing keys on a `tickets_departments.email`
|
|
66
|
+
`LIKE` match against the To/CC addresses; **anything unmatched falls through to config
|
|
67
|
+
`tickets_defaultdepartment` (= dept 1 "Support Desk" catch-all) and `tickets_defaultclient`** —
|
|
68
|
+
which is why unrouted/automated mail piles into "Support Desk". There is **no allowlist/blocklist
|
|
69
|
+
config framework**: noise filtering is ad-hoc inline guards in the crons (the hardcoded Compass
|
|
70
|
+
"Automatic reply:" filter and now `isSuppressedNoiseEmail`).
|
|
53
71
|
|
|
54
72
|
## Gotchas / known issues
|
|
55
73
|
- **`$processName` guard:** `tickets.php` throws if executed directly instead of via an env
|
|
@@ -59,7 +77,13 @@ Writes `tickets`, `tickets_replies`, attachment `files`; reads config from the `
|
|
|
59
77
|
- **Loop prevention** only excludes FROM==department-TO addresses; CC/reply-all across multiple
|
|
60
78
|
department addresses can still loop.
|
|
61
79
|
- **Auto-reply filters** are partly hardcoded (e.g. ignore `TechHub@compass-usa.com`
|
|
62
|
-
"Automatic reply:" messages)
|
|
80
|
+
"Automatic reply:" messages), and now the `isSuppressedNoiseEmail` Staples-PO filter — there
|
|
81
|
+
is no config-driven allow/block list, so each new noise source is another inline guard.
|
|
82
|
+
- **`Ticket::extractEmail()` returned an undefined offset on a no-match** (bare `$matches[0]`).
|
|
83
|
+
With a sender-less/malformed email this codebase promotes the warning to a thrown error
|
|
84
|
+
(`E_ALL` + `App_Error::handleError`), so an intake cron could **halt**. Hardened to
|
|
85
|
+
`return $matches[0] ?? '';` (TRUE-79975) — important because `isSuppressedNoiseEmail` routes
|
|
86
|
+
nullable/malformed sender input through `extractEmail`, putting this latent bug on the hot path.
|
|
63
87
|
- Attachments are deleted locally immediately after the S3 upload with **no retry** if S3 fails.
|
|
64
88
|
- **`adminid` clobber on reply routing:** after `emailToTicket()` sets `$data['adminid']` from
|
|
65
89
|
the FROM address (~line 1407), an asset lookup (~line 1437-1442) overwrites it with the
|
|
@@ -69,6 +93,10 @@ Writes `tickets`, `tickets_replies`, attachment `files`; reads config from the `
|
|
|
69
93
|
classification. See the ticket-lifecycle doc.
|
|
70
94
|
|
|
71
95
|
## Change history
|
|
96
|
+
- 2026-07-09 — added `Ticket::isSuppressedNoiseEmail()` noise suppression (no-reply sender + "Staples PO"
|
|
97
|
+
subject) wired into both feeders before ticket creation; hardened `extractEmail()` no-match to
|
|
98
|
+
`''`; documented the `tickets_defaultdepartment` → "Support Desk" catch-all fall-through
|
|
99
|
+
(TRUE-79975) (mhammontree)
|
|
72
100
|
- 2026-07-09 — documented the `emailToTicket()` adminid-overwrite reply misclassification
|
|
73
101
|
(TRUE-80114 planning investigation) (dfranks)
|
|
74
102
|
- 2026-06-15 — documented from a source read of `crons/tickets*.php` and `emailToTicket()` (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-09
|
|
10
10
|
owners: [jcardinal, mhammontree, bala]
|
|
11
11
|
files:
|
|
12
12
|
- Core/
|
|
@@ -168,35 +168,62 @@ its own header.)
|
|
|
168
168
|
5. Never edit or re-date an already-applied file — add a new dated file instead. Never put new
|
|
169
169
|
work in a `HISTORIC` folder.
|
|
170
170
|
|
|
171
|
-
## Bulk data loads —
|
|
171
|
+
## Bulk data loads — batch, and stage large sets in a temp table
|
|
172
172
|
|
|
173
|
-
When a change inserts many rows (reference-data / code-table loads), write it as
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
When a change inserts many rows (reference-data / code-table loads), write it as **batched
|
|
174
|
+
statements**, not one `INSERT` per row. The database is billed per query and the external
|
|
175
|
+
executor runs each statement as its own round-trip, so N separate inserts cost N times the
|
|
176
|
+
round-trips (and query cost) of a batched load.
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
178
|
+
To stay **idempotent** without a unique key to `INSERT IGNORE` against, anti-join the source
|
|
179
|
+
set to the target and insert only the misses.
|
|
180
|
+
|
|
181
|
+
**Small sets (a few hundred rows):** a single `INSERT ... VALUES (...),(...),...`, or an
|
|
182
|
+
`INSERT ... SELECT` over a `UNION ALL` derived row set, is fine:
|
|
182
183
|
|
|
183
184
|
```sql
|
|
184
185
|
INSERT INTO TargetTable (uuid, keyColumn, otherColumn)
|
|
185
|
-
SELECT
|
|
186
|
-
src.uuid,
|
|
187
|
-
src.keyColumn,
|
|
188
|
-
src.otherColumn
|
|
186
|
+
SELECT src.uuid, src.keyColumn, src.otherColumn
|
|
189
187
|
FROM (
|
|
190
188
|
SELECT '<uuid>' AS uuid, '<key>' AS keyColumn, '<value>' AS otherColumn
|
|
191
189
|
UNION ALL SELECT ...
|
|
192
190
|
) AS src
|
|
193
191
|
LEFT JOIN TargetTable existing ON existing.keyColumn = src.keyColumn
|
|
194
|
-
WHERE
|
|
195
|
-
|
|
192
|
+
WHERE existing.id IS NULL;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**⚠ Large sets — never build them as a long `UNION ALL` chain.** MySQL parses a `UNION ALL`
|
|
196
|
+
chain **recursively**, one parser-stack frame per link. Around ~2,000+ links overflow the
|
|
197
|
+
default 256 KB per-thread stack and the statement dies at **parse time** (before a single row
|
|
198
|
+
is processed) with:
|
|
199
|
+
|
|
200
|
+
ERROR 1436 (HY000): Thread stack overrun: <n> bytes used of a 262144 byte stack, and
|
|
201
|
+
20000 bytes needed. Use 'mysqld --thread_stack=#' to specify a bigger stack.
|
|
202
|
+
|
|
203
|
+
This is a parser-recursion limit — **not** a row-volume, `max_allowed_packet`, or
|
|
204
|
+
server-misconfiguration issue, and **do not** raise `thread_stack` on the server for a one-off
|
|
205
|
+
load. Instead, stage the rows in a **session-scoped temp table via a flat multi-row `VALUES`
|
|
206
|
+
list** (which the parser reads as a flat list, no recursion), then run the anti-join once from
|
|
207
|
+
the staging table:
|
|
208
|
+
|
|
209
|
+
```sql
|
|
210
|
+
CREATE TEMPORARY TABLE _stage (
|
|
211
|
+
uuid VARCHAR(36) NOT NULL, keyColumn VARCHAR(255) NOT NULL, otherColumn TEXT NULL);
|
|
212
|
+
INSERT INTO _stage (uuid, keyColumn, otherColumn)
|
|
213
|
+
VALUES ('<uuid>','<key>','<value>'), ('<uuid>','<key>','<value>'), ... ; -- flat, thousands of rows
|
|
214
|
+
INSERT INTO TargetTable (uuid, keyColumn, otherColumn)
|
|
215
|
+
SELECT src.uuid, src.keyColumn, src.otherColumn
|
|
216
|
+
FROM _stage AS src
|
|
217
|
+
LEFT JOIN TargetTable existing ON existing.keyColumn = src.keyColumn
|
|
218
|
+
WHERE existing.id IS NULL;
|
|
219
|
+
DROP TEMPORARY TABLE _stage;
|
|
196
220
|
```
|
|
197
221
|
|
|
198
|
-
|
|
199
|
-
|
|
222
|
+
The `TEMPORARY` table is session-scoped (safe on production, auto-dropped on disconnect even if
|
|
223
|
+
the run aborts). A multi-row `VALUES` load of a few thousand rows is a few hundred KB — well
|
|
224
|
+
under the 64 MB `max_allowed_packet` default. First hit in
|
|
225
|
+
`Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql` (2,200 AIG SaleItem codes); see
|
|
226
|
+
`clients/aig/features/entitlement-intake.md` → *Uploading new codes*.
|
|
200
227
|
|
|
201
228
|
## Self-referencing DELETE — wrap the subquery in a derived table
|
|
202
229
|
|
|
@@ -88,17 +88,27 @@ When AIG sends a new "Active SaleItemID" spreadsheet (columns `SaleItemID`, `Des
|
|
|
88
88
|
3. Generate **fully-random UUIDs** (e.g. `uuid4`), never the time-based MySQL `UUID()` —
|
|
89
89
|
per the 2.0 standard. Bake literal UUIDs into the SQL since a migration can't call PHP's
|
|
90
90
|
`_String::generateUuid()`.
|
|
91
|
-
4.
|
|
92
|
-
|
|
93
|
-
can't help)
|
|
91
|
+
4. Feed the rows through a **session-scoped temp table populated by a flat multi-row
|
|
92
|
+
`VALUES` list**, then anti-join once into `Items` — idempotent because `Items` has **no
|
|
93
|
+
unique key on `partNumber`** (so `INSERT IGNORE` can't help). Do **not** assemble the rows
|
|
94
|
+
as a long `UNION ALL` derived table: at AIG's ~2,200-code scale that overflows MySQL's
|
|
95
|
+
parser stack and fails at **parse time** with **ERROR 1436 (HY000) Thread stack overrun**
|
|
96
|
+
(see the gotcha below and the dbchanges2 architecture bulk-load rule). A flat `VALUES`
|
|
97
|
+
list does not recurse and stays well under `max_allowed_packet`:
|
|
94
98
|
```sql
|
|
99
|
+
CREATE TEMPORARY TABLE _aig_saleitem_stage (
|
|
100
|
+
uuid VARCHAR(36) NOT NULL, partNumber VARCHAR(255) NOT NULL, description TEXT NULL);
|
|
101
|
+
INSERT INTO _aig_saleitem_stage (uuid, partNumber, description)
|
|
102
|
+
VALUES ('<uuid>','<part>','<desc>'), ('<uuid>','<part>','<desc>'), ... ; -- flat, ~2,200 rows
|
|
95
103
|
INSERT INTO Items (uuid, partNumber, description)
|
|
96
104
|
SELECT src.uuid, src.partNumber, src.description
|
|
97
|
-
FROM
|
|
98
|
-
UNION ALL SELECT ... ) AS src
|
|
105
|
+
FROM _aig_saleitem_stage AS src
|
|
99
106
|
LEFT JOIN Items existing ON existing.partNumber = src.partNumber
|
|
100
107
|
WHERE existing.id IS NULL;
|
|
108
|
+
DROP TEMPORARY TABLE _aig_saleitem_stage;
|
|
101
109
|
```
|
|
110
|
+
The `TEMPORARY` table is session-scoped (safe on production, auto-dropped on disconnect
|
|
111
|
+
even if the run aborts). Do **not** raise `thread_stack` on the server for a one-off load.
|
|
102
112
|
|
|
103
113
|
## Client variations
|
|
104
114
|
|
|
@@ -121,6 +131,13 @@ this interceptor or use this dual-purpose Items pattern.
|
|
|
121
131
|
codes are **not** in the SaleItemID spreadsheet and must be sourced separately if missing.
|
|
122
132
|
- **No unique key on `Items.partNumber`** — use an anti-join for idempotent loads, not
|
|
123
133
|
`INSERT IGNORE`.
|
|
134
|
+
- **A long `UNION ALL` derived table fails at ~2,200 rows with ERROR 1436 "Thread stack
|
|
135
|
+
overrun."** The AIG SaleItem load is exactly this scale. MySQL builds the `UNION ALL`
|
|
136
|
+
chain recursively, so ~2,200 links overflow the default 256 KB per-thread parser stack and
|
|
137
|
+
the statement dies at **parse time** (before any row is processed) — it is a parser-recursion
|
|
138
|
+
limit, **not** a row-volume, `max_allowed_packet`, or server-config problem. Stage the rows
|
|
139
|
+
in a temp table via a flat multi-row `VALUES` list and anti-join from there (see *Uploading
|
|
140
|
+
new codes* step 4). Do not raise `thread_stack` for a one-off load.
|
|
124
141
|
- **An `entitlementFulfillmentType` EV-12 VALIDATION error is NOT a bad client payload —
|
|
125
142
|
it's this interceptor's injected field failing to resolve.** AIG never sends that field;
|
|
126
143
|
the interceptor injects it as `{name: "Repair"}`. The error means the V2 resolver could not
|
|
@@ -136,6 +153,11 @@ this interceptor or use this dual-purpose Items pattern.
|
|
|
136
153
|
|
|
137
154
|
## Change history
|
|
138
155
|
|
|
156
|
+
- 2026-07-09 — TRUE-79534 SaleItem load reworked: the 2,200-code `INSERT ... SELECT FROM
|
|
157
|
+
(<2,199 `UNION ALL` links>)` derived table failed at parse time with ERROR 1436 (Thread
|
|
158
|
+
stack overrun). Rebuilt as a `TEMPORARY` staging table filled by a flat multi-row `VALUES`
|
|
159
|
+
list + the same anti-join into `Items` (identical rows/UUIDs, 0 duplicate partNumbers).
|
|
160
|
+
(mhammontree)
|
|
139
161
|
- 2026-07-09 — TRUE-79978: root-caused an `entitlementFulfillmentType` EV-12 on beta/QA. The
|
|
140
162
|
field is injected (name-only) by `prePost`; the beta `Client_Aig.Apis_RecordFields` override
|
|
141
163
|
row (apiId=2, recordFieldId=1202) had `overrideIsIdentifier=NULL` + `overrideChildPolicy=MATCH_CREATE`,
|
package/package.json
CHANGED