toga-ai 1.0.494 → 1.0.496

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.
@@ -10,5 +10,6 @@
10
10
  | [NetSuite → TOGa Supply Per-Client Sync (thin wrappers)](features/netsuite-togasupply-per-client-sync.md) | Syncs NetSuite transactions (sales orders, purchase orders, invoices, item receipts, item fulfillments, inventory adjustments) into each TOGa Supply (2.0) clien | worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/sync_togasupply_canon.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql, library/app/api/netsuite/rest.php, library/app/systemmonitor/netsuiteintegration.php |
11
11
  | [OneUptime external uptime monitoring for 1.0 workers](features/oneuptime-worker-uptime-monitoring.md) | Every 1.0 worker box self-reports its liveness to an external OneUptime monitor once per minute by curl-POSTing to a per-worker "Incoming Request" heartbeat URL | library/app/worker.php, worker/crons/worker/worker_heartbeat.php |
12
12
  | [Prudential: Send Shipments for the Day report (daily cron)](features/send-shipments-for-the-day.md) | Daily cron (9:00 PM) that emails Prudential and Dell stakeholders an Excel report of all devices shipped that day, including tracking number, serial number, emp | worker/crons/notifications/reports/send_shipments_for_the_day.php |
13
+ | [Diagnosing frozen 1.0 worker cron check-ins (Sentry "missed" flood)](workflows/diagnosing-frozen-cron-checkins.md) | When 1.0 worker cron timestamps freeze and Sentry project `worker1` fills with **`missed`** check-ins, the intuitive diagnosis — a wedged `App_Framework::isProc | worker/.ebextensions/cron.config, library/app/worker.php |
13
14
  | [isFulfillable Multi-Client Backfill (all togasupply clients)](workflows/isfulfillable-multi-client-backfill.md) | One-time backfill that catches up `Items.isFulfillable` on **existing** items across **all 17 togasupply clients** (AIG, Broward Sheriff, Canon, Endeavor Health | worker/crons/toga2/netsuite/backfill_isfulfillable_all_clients.php, library/app/api/toga2.php |
14
15
  | [Onboarding a Client to the NetSuite TOGa Supply Sync](workflows/onboarding-client-to-netsuite-togasupply-sync.md) | How to add a new TOGa 2 client to the per-client NetSuite → TOGa Supply importer (`worker/crons/toga2/netsuite/`). | worker/crons/toga2/netsuite/sync_togasupply.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql |
@@ -0,0 +1,103 @@
1
+ ---
2
+ title: Diagnosing frozen 1.0 worker cron check-ins (Sentry "missed" flood)
3
+ framework: "1.0"
4
+ repo: worker
5
+ project: Worker
6
+ client: shared
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-08-03
10
+ owners: ["dfranks"]
11
+ files:
12
+ - worker/.ebextensions/cron.config
13
+ - library/app/worker.php
14
+ related:
15
+ - ../architecture.md
16
+ - ../features/oneuptime-worker-uptime-monitoring.md
17
+ - ../../library/features/cron-execution-monitoring.md
18
+ ---
19
+
20
+ ## Summary
21
+
22
+ When 1.0 worker cron timestamps freeze and Sentry project `worker1` fills with **`missed`**
23
+ check-ins, the intuitive diagnosis — a wedged `App_Framework::isProcessRunning()` lock — is
24
+ usually **wrong**. The common cause is **Elastic Beanstalk deployment churn replacing the EC2
25
+ instances underneath the crons**. This workflow is the ordered path that distinguishes the two
26
+ and the interpretation traps along the way.
27
+
28
+ ## Where to look (name mapping)
29
+
30
+ Sentry project **`worker1`** is the 1.0 worker tier running in the Elastic Beanstalk
31
+ environment **`agilant-worker`** (application "Agilant", `us-west-2`, account
32
+ `502614707982`, reachable with the `default` SSO profile). It is a multi-instance autoscaled
33
+ fleet. **There is no EB environment literally named `worker1`** — the Sentry project name and
34
+ the EB environment name do not match, and looking for the wrong one costs real time.
35
+
36
+ ## The failure mode
37
+
38
+ An EB deploy that aborts — events read like `Failed to deploy application`,
39
+ `Unsuccessful command execution on instance id(s) ...`,
40
+ `Cannot complete command execution ... no longer running` — causes EB to **replace
41
+ instances**. In-flight cron processes die with their host, and each Sentry monitor's
42
+ `lastCheckIn` freezes at whatever its now-terminated instance last reported. Nothing is
43
+ stuck; nothing needs clearing.
44
+
45
+ Two consequences worth knowing:
46
+
47
+ - **Check-ins resume on their own**, typically within ~1 minute of the deploy completing.
48
+ No manual intervention, no lock to clear, no instance to restart.
49
+ - EB may warn that an aborted deploy left instances on **mixed application versions**. That
50
+ does need a re-deploy of the intended version — the recovery of the crons does not imply
51
+ the fleet is on the version you meant to ship.
52
+
53
+ ## Diagnostic path
54
+
55
+ 1. **List the project's monitors and find the stall boundary.**
56
+ `GET https://sentry.io/api/0/organizations/<org>/monitors/?project=worker1` — read each
57
+ monitor's `environments[].lastCheckIn`. The cluster of timestamps that all stop at roughly
58
+ the same moment is the boundary.
59
+ 2. **Pull per-monitor check-in history.**
60
+ `GET .../monitors/<slug>/checkins/` — a `missed` entry **with no `duration`** means the run
61
+ never started or never reported. A run that started and then hung looks different (it has a
62
+ start and no clean check-out). This is the first signal separating "process never launched"
63
+ from "process wedged".
64
+ 3. **KEY DISCRIMINATOR — are non-monitored crons on the same host still throwing ordinary
65
+ exceptions in the same Sentry project's issue stream?** If other crons are still erroring
66
+ *after* the stall boundary, then PHP runs and Sentry connectivity both work and the host is
67
+ alive. That rules out "host down / Sentry unreachable" and points at instance replacement
68
+ or a per-cron blockage.
69
+ 4. **Confirm against Elastic Beanstalk.**
70
+ ```
71
+ aws elasticbeanstalk describe-events --environment-name agilant-worker --start-time <ISO>
72
+ aws elasticbeanstalk describe-environment-health --environment-name agilant-worker --attribute-names All
73
+ ```
74
+ Correlate deploy / abort / instance-add / instance-remove timestamps against the stall
75
+ boundary. A deploy window that brackets the boundary is the answer.
76
+
77
+ ## Interpretation caveats
78
+
79
+ - **Timezones.** Sentry returns **UTC**; the developer reporting the symptom is usually
80
+ quoting local **CDT (UTC-5)**. Convert before concluding the windows "don't line up" — they
81
+ usually do.
82
+ - **Establish each monitor's baseline before calling a stale timestamp new.** The `worker1`
83
+ monitor list chronically contains monitors that have been silently dead for weeks or months
84
+ for reasons unrelated to any current incident. During an incident these are noise and must
85
+ not be read as "failed to recover." (Deliberately not listing which ones — that goes stale.
86
+ Check each monitor's own history.)
87
+ - **Separate high-frequency from low-frequency monitors.** A 5-minute monitor should recover
88
+ within minutes; an hourly or daily one will not move until its next scheduled slot, and its
89
+ silence proves nothing.
90
+ - **Alerting-hygiene gap.** Chronically dead cron monitors mean nobody is being alerted on
91
+ those jobs at all. Worth a separate follow-up whenever you notice one.
92
+
93
+ ## After recovery
94
+
95
+ A multi-minute cron outage on this tier means missed windows for order processing, email
96
+ sending, and inbound order imports. These are catch-up-on-next-run designs and generally
97
+ self-drain — but **eyeball queue depth / backlog after recovery rather than assuming clean**.
98
+
99
+ ## Change history
100
+
101
+ - 2026-08-03 — Documented from a production diagnostic session: frozen cron timestamps +
102
+ `missed` check-in flood on `worker1` traced to EB deployment churn on `agilant-worker`, not
103
+ a wedged `isProcessRunning()` lock. (dfranks)
@@ -7,7 +7,7 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-07-29
10
- owners: ["bala", tcox]
10
+ owners: ["bala", "tcox", "dfranks"]
11
11
  files:
12
12
  - _underscore/Email.php
13
13
  - worker2/Worker/Infrastructure/Email/Send.php
@@ -71,11 +71,47 @@ SMTP** using PHPMailer, then flips each row to `SENT` or `FAILED`. So a 2.0 emai
71
71
  `DevTeam@goagilant.com` is a known-good verified sender identity in worker2 config. See
72
72
  [ai-bdr web-funnel-app.md](../../ai-bdr/features/web-funnel-app.md).
73
73
 
74
+ - **⚠ READ-AFTER-WRITE ON THE READER REPLICA LOSES `send()`'S OWN LOG ROW (deterministic).**
75
+ `_Email::send()` INSERTs the `PENDING` `Logs_[Client].Email` row, then reads `$emailLog->id`
76
+ back. On a `_Model`, `save()` sets the PK in memory from `getInsertId()` but does **not** clear
77
+ `_model_needsToBeInitialized`, so the next `->id` access triggers `__get` → `initialize()`,
78
+ which issues a fresh `SELECT ... WHERE id=<new id>`. `DB_CLIENT_LOGS` is registered with a
79
+ **separate `readHost`**, so that SELECT is routed to the **reader replica** — but the INSERT is
80
+ still inside an **uncommitted** lazy transaction, so it has produced no binlog event and the
81
+ replica **can never** see it during this request. The read returns 0 rows and `initialize()`
82
+ throws `"exactly 1 row expected, 0"`. This is **100% deterministic, not a lag race** — an
83
+ uncommitted write cannot replicate. A connection *can* see its own uncommitted writes
84
+ (read-your-own-writes), so forcing the read onto the writer is the fix.
85
+ - **Why only some emails hit it:** approval/order-placed emails send from inside the **api2
86
+ CRUD POST path** (and worker2), which call `_Database::setIsReadHostEnabled(false)`
87
+ (`V2.php` create loop; worker2 `Controller/Index.php`) — their identical read-back hits the
88
+ **writer** and succeeds. The scripted-API path (`/email-templates/sendEmail`) never suppresses
89
+ the read host, so it throws every time. This was the root cause of the Compass In-Transit
90
+ email outage — see
91
+ [`compass-partial-in-transit-delivered-emails.md`](../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md).
92
+ - **Fix direction (smallest blast radius):** wrap the log-row `save()` + id read (+ attachment
93
+ loop) in `_Email::send()` with `_Database::setIsReadHostEnabled(false)`, restoring the prior
94
+ value in a `finally` (get/set at `Database.php`). Fixes all scripted-API emails and hardens the
95
+ interceptor path.
96
+ - **Rejected alternatives:** (a) `commit` right after `save()` before the read — recreates the
97
+ failure as an *intermittent* replication-lag race AND breaks the request's one-transaction
98
+ boundary (orphans the row on a later throw, prematurely commits everything else buffered on the
99
+ connection, strands attachment/API-log writes on a closed lazy transaction). (b) Clear
100
+ `_model_needsToBeInitialized` after INSERT in `_Model::save()` so `->id` reads from memory
101
+ (zero queries) — conceptually the *correct* fix but changes every model in the ORM, too broad
102
+ for a hotfix.
103
+
74
104
  ## Change history
75
105
  - 2026-07-29 — Noted that the SES SMTP credentials/settings now have a **non-PHP consumer**: the
76
106
  BDR funnel sends via nodemailer against the same SES host (us-west-2, port 587, TLS) using
77
107
  `TOGA_SMTP_*` env vars, so a rotation must be coordinated with BDR's `.env.local` + Amplify
78
108
  vars or BDR's summary email goes dark. (tcox)
109
+ - 2026-07-29 — Documented the **deterministic read-after-write failure** in `_Email::send()`:
110
+ reading `$emailLog->id` after INSERT triggers `_Model::__get`→`initialize()`→ a reader-replica
111
+ SELECT for an uncommitted row → `"exactly 1 row expected, 0"` throw. Only the scripted-API send
112
+ path fails (CRUD/worker paths suppress the read host via `setIsReadHostEnabled(false)`); this was
113
+ the root cause of the Compass In-Transit outage. Fix direction = suppress the read host around the
114
+ log-row save+read in `send()`. (dfranks)
79
115
  - 2026-07-28 — Created: documented that `_Email::send()` **queues** (writes a `PENDING`
80
116
  `Logs_[Client].Email` row + attachment BLOBs) rather than transmitting, and that the worker2
81
117
  `Infrastructure/Email/Send` cron (`Core.CronJobs` `* * * * *`) does the actual SES SMTP send
@@ -11,7 +11,7 @@
11
11
  | [Nested FK object embedding is gated by the CHILD record's own ACL](features/nested-fk-acl-embedding.md) | When the V2 JSON engine serializes a foreign-key field into a **nested object** (in `getFullModelData()`, ~V2.php L6016-6060), it re-checks the **child** record | api2/Component/Api/V2/V2.php, dbchanges2/Client_Compass/2026-07-23b - PurchaseOrdersRecordReadAcl.sql |
12
12
  | [Nested-relationship writes & child matching (link vs. create)](features/nested-relationship-writes.md) | When a 2.0 API write payload (`POST`/`PUT`) contains a **nested related object** (e.g. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/ContactEmailAddress.php |
13
13
  | [Record Scripts (computed/aggregate /v2 endpoints — the authoring contract)](features/record-scripts.md) | In api2 you almost never write a controller. | api2/Component/Api/V2/V2.php, _underscore/Model/Team/Sprint.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Query.php |
14
- | [V2 Request Logging & Where Requests Land (client vs core log DB)](features/request-logging.md) | The V2 engine logs **every inbound request** — success *and* failure, with response code and payload — and routes each log entry to the **client** log or the ** | api2/Component/Api/V2/V2.php, _underscore/Model/Client/Logs/Api.php, _underscore/Model/Core/Logs/Api.php |
14
+ | [V2 Request Logging & Where Requests Land (client vs core log DB)](features/request-logging.md) | The V2 engine logs **every inbound request** — success *and* failure, with response code and payload — and routes each log entry to the **client** log or the ** | api2/Component/Api/V2/V2.php, api2/Controller/Index.php, _underscore/Model/Client/Logs/Api.php, _underscore/Model/Core/Logs/Api.php |
15
15
  | [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | The V2 engine can run a Record Script (scripted API) for a **POST** request, and a scripted API can receive its arguments from the **JSON request body** instead | api2/Component/Api/V2/V2.php |
16
16
  | [TOGa IQ Sprint Dashboard API (Record Scripts)](features/sprint-dashboard-api.md) | The internal **TOGa IQ sprint dashboard** is served in production by **six api2 Record Scripts** on `_Model_Team_Sprint` (`_underscore/Model/Team/Sprint.php`), | _underscore/Model/Team/Sprint.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-07-24a - SprintDashboardRecordScripts.sql, dbchanges2/Client_True/2026-07-24b - SprintDashboardScriptAcl.sql |
17
17
  | [Surface action-state via the surface=<slug> request option (M2M-safe)](features/surface-meta-option.md) | An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state (`isVisible`/`isEnabled`) to a GET response **under `meta.surface | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
@@ -7,9 +7,10 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-07-29
10
- owners: ["mhammontree"]
10
+ owners: ["mhammontree", "dfranks"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
+ - api2/Controller/Index.php
13
14
  - _underscore/Model/Client/Logs/Api.php
14
15
  - _underscore/Model/Core/Logs/Api.php
15
16
  related:
@@ -5,7 +5,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
5
5
  ## 1.0 framework
6
6
 
7
7
  - **library** (Library) _(framework core)_ — 14 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
- - **worker** (Worker) — 17 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
8
+ - **worker** (Worker) — 18 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
10
10
  - **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
11
11
  - **togadesk** (TOGa Desk) — 10 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
@@ -0,0 +1,82 @@
1
+ ---
2
+ type: session
3
+ slug: true-79401-multi-email-resolution
4
+ title: TRUE-79401 — closed the multi-email payload-shape question (no api2 work needed)
5
+ author: mhammontree
6
+ repos: [library, api2, _underscore, worker2]
7
+ framework: "both"
8
+ client: aig
9
+ status: active
10
+ created: 2026-08-03
11
+ updated: 2026-08-03
12
+ ---
13
+
14
+ # Session: true-79401-multi-email-resolution
15
+ **Date:** 2026-08-03
16
+ **Project/Repo:** library (1.0 core) — with 2.0 findings in api2 / _underscore / worker2
17
+ **Task:** Review the one open question left on TRUE-79401 (multiple email addresses for AIG in the 1.0 `library` bridge) — namely "do we need an api2 intake mapping?" — and determine whether it was already answered in prior sessions or meetings.
18
+
19
+ ---
20
+
21
+ ## What WORKED
22
+
23
+ - **Found the prior session's record immediately** via the knowledge base, not by code archaeology. `knowledge/1.0/apps/library/features/toga2-api-client-and-bridge.md` held the full TRUE-79401 history from the 2026-07-28 capture, including the open question and the "owner Paulina" note.
24
+ - **Confirmed the library implementation is present in the working tree** — grep for `CUSTOMER_EMAIL_DELIMITER|contactEmailAddresses` in `C:\WWW\library` returned `toga2.php:112` (the `'; '` constant), `:8424` (the `/contacts` fetch `fields` whitelist entry), and `:8499–8505` (the `array_column` → trim/filter/unique → implode → `sqlEscape` chain).
25
+ - **Proved `UserDefined3` is not a real field anywhere.** Grep across all of `C:\WWW` returned exactly two hits: the comment at `library/app/api/toga2.php:8424` and the knowledge doc itself. Zero hits in `api2` or `_underscore`. This was the key evidence that the flat-field branch was never implemented and never intended.
26
+ - **Proved `c_businessUnit` was never built.** Grep for `businessUnit|c_businessUnit` (case-insensitive) across `C:\WWW` returned only unrelated `BusinessUnits` table hits in the `forecast` repo — nothing in `_underscore`, `api2`, or `dbchanges2`. So there is no dead custom field to clean up.
27
+ - **Extracted the Teams transcript from the .docx successfully** after two failures (see below). Working method: `[System.IO.Compression.ZipFile]::ExtractToDirectory()` on the `.docx`, then on `word/document.xml` replace `</w:p>` → newline and `<w:br/>` → newline, strip `<[^>]+>`, and hand-replace the five XML entities. Produced 21,829 chars / 199 lines of readable transcript at `<scratchpad>/aig-transcript/transcript.txt`.
28
+ - **The transcript answered the question**, plus three things nobody asked: purpose is verification-only, the ~15-client-DB column approach was rejected, and the origin of the "VARCHAR(55)" fear.
29
+ - **AIG's mapping spreadsheet (shared as an image by Paulina) closed it definitively** — both `UserDefined3` and `UserDefined4` map to `Entitlements.contact.contactEmailAddresses[].emailAddress`, and the `UserDefined2` (Business Unit) target column is conspicuously **empty**, corroborating that it was dropped.
30
+ - **`/capture` published 4 doc UPDATEs and pushed** — validate OK (305 docs / 30 repos), mirrored to `C:\WWW\.claude\knowledge`, PUSHED.
31
+
32
+ ## What did NOT work — DO NOT RETRY THESE
33
+
34
+ - **Searching the production logs for a multi-email payload.** This was my suggestion and it is a **dead end by construction**: AIG currently sends only one email address and will not enable the extra fields until we tell them the feature is live. Therefore every payload in `Logs_Aig.Api` and in the core/writer `Logs` contains exactly one `primaryContactEmailAddress` and nothing else. There is no multi-email payload to find. **Do not propose a log audit to determine the future payload shape** — the shape is a *specification* question, not a discovery question.
35
+ - **Searching our own transcript store for the 2026-05-01 meeting.** `Team/Transcripts/Export` first ran successfully in production on **2026-06-12** (PR #78) and runs off an incremental watermark with a bounded `lookbackDays`; no multi-week backfill appears in its change history. A May 1 meeting is **not** in `Team.TranscriptExports`, `Team.TranscriptProcessing`, or the `togaiq` KBs despite having been recorded. Recover pre-June meetings from Microsoft Graph or a manual export instead.
36
+ - **Reading the `.docx` with the `Read` tool.** Failed with: `This tool cannot read binary files. The file appears to be a binary .docx file.` Must unzip and strip XML (method above).
37
+ - **`[System.Web.HttpUtility]::HtmlDecode($t)` in the extraction script.** Failed with: `Unable to find type [System.Web.HttpUtility].` — Windows PowerShell 5.1 does not load `System.Web` by default. Either `Add-Type -AssemblyName System.Web` first, or (what I did) hand-replace `&amp; &lt; &gt; &quot; &apos;`. Note the failure was non-terminating, so the script *appeared* to continue and wrote a partially-processed file — the first extraction produced only 6 lines and looked plausible.
38
+ - **Assuming the delimited string was the payload format.** The comma/semicolon discussion in the 2026-05-01 meeting is about **1.0 storage** (`TOGA_AIG.Customers.emailAddress`, a legacy `varchar(255)` that can only hold a string), *not* the inbound payload. The payload is an array of objects. Three layers: nested records inbound → one row per address in `Client_Aig.ContactEmailAddresses` → `'; '`-joined string in 1.0. Easy conflation because the same meeting covers both within minutes.
39
+
40
+ ## Not tried yet (candidates for next session)
41
+
42
+ - **The beta nested-write test.** POST a synthetic AIG entitlement to beta/QA with two nested `contactEmailAddresses` entries and **assert the actual `Client_Aig.ContactEmailAddresses` row count** — not the HTTP status. Must cover **both** a brand-new contact (CREATE) and a **second entitlement for an existing contact** (UPDATE). The UPDATE path is the suspect one: per `2.0/apps/api2/features/nested-relationship-writes.md` (2026-07-20), the reverse back-reference injection at `V2.php:4985–4996` breaks single-key forced-MATCH and explicitly names `primaryContactEmailAddress` as likely affected.
43
+ - **Verifying the library change is merged and deployed to the prod worker.** Git/deploy state of `C:\WWW\library` was **never checked** this session. The code is confirmed present in the working tree only.
44
+ - **The note to Paulina / AIG** — two confirmations (did the mapping document actually reach AIG after the 2026-05-04 email; has their dev acknowledged the `contactEmailAddresses[]` mapping) plus the concrete JSON example.
45
+ - **Hardening the dedupe lookup** — `LIKE '%$primaryEmailAddress%'` instead of the current wildcard-less `LIKE`, or dropping the email fallback in favour of `c_togaCustomerId` alone.
46
+ - **Guarding the unrelated-but-open `prePost` empty-`$itemId` defect** in `_Model_Aig_Entitlement` (a missing sale-item code still 500s instead of returning a clean "Missing AIG item ID"). Pre-existing, documented, out of TRUE-79401 scope.
47
+
48
+ ## Current file state
49
+
50
+ | File | Status | Notes |
51
+ |------|--------|-------|
52
+ | `library/app/api/toga2.php` | **Unchanged this session** | TRUE-79401 code from 2026-07-28 confirmed present at `:112`, `:8424`, `:8499–8505`. No edits made. |
53
+ | `_underscore/Model/Aig/Entitlement.php` | Unchanged (read only) | `postPost` single-recipient email confirmed **correct by design**, not a gap. |
54
+ | `_underscore/Model/Client/ContactEmailAddress.php` | Unchanged (read only) | Confirms the collection is one row per address; FK `contactId` → `_Model_Client_Contact`. |
55
+ | `knowledge/1.0/apps/library/features/toga2-api-client-and-bridge.md` | **Updated + pushed** | "Payload shape OPEN" gotcha **replaced** with a RESOLVED section; new Open items section; 4 new gotchas. |
56
+ | `knowledge/clients/aig/features/entitlement-intake.md` | **Updated + pushed** | New "Business Unit (UserDefined2) — answered and DROPPED" section + 2 gotchas. |
57
+ | `knowledge/2.0/apps/api2/features/nested-relationship-writes.md` | **Updated + pushed** | New framework-level section: a wrong-shaped nested collection returns 201 and silently drops data. |
58
+ | `knowledge/2.0/apps/worker2/features/teams-transcript-export.md` | **Updated + pushed** | New gotcha: no transcript coverage before ~2026-06-12. |
59
+ | `<scratchpad>/aig-transcript/transcript.txt` | Created (temp) | Extracted 2026-05-01 meeting transcript, 199 lines. Scratchpad — not committed. |
60
+
61
+ **No source code was written or changed this session.** It was review + resolution + knowledge capture.
62
+
63
+ ## Decisions made
64
+
65
+ - **No api2 intake mapping will be built.** Rationale: AIG's own field-mapping document maps both `UserDefined3` and `UserDefined4` to `Entitlements.contact.contactEmailAddresses[].emailAddress`, so AIG restructures on their side and the V2 metadata-driven engine writes the collection generically. *Rejected alternative:* an AIG `prePost` interceptor converting flat `c_secondaryEmailAddress`/`c_tertiaryEmailAddress` custom fields into `ContactEmailAddresses` rows — rejected because it caps the address count, requires custom-field definitions plus a release, and the mapping document already specifies the nested form.
66
+ - **Keep the separately-escaped `$primaryEmailAddress` for the dedupe lookup**, despite the 2026-05-01 meeting saying to "get rid of the primary contact email address" entirely. Rationale: the primary-email `LIKE` is the legacy fallback for matching pre-existing 1.0 customers that have no `c_togaCustomerId`; removing it would break that matching outright. Accepted consequence: a stored `'; '`-joined list can no longer satisfy the wildcard-less `LIKE`, which could duplicate a customer if the `c_togaCustomerId` write-back ever fails after the insert.
67
+ - **Multi-recipient notification stays out of scope.** Rationale: the meeting explicitly framed this as verification/lookup for support agents ("it's just for visual, this is for verification, it's a string") and specifically rejected CC/BCC-ing all addresses onto TogaDesk tickets. *Rejected alternative:* widening `postPost`'s registration email to all addresses.
68
+ - **Change 4 (201-with-silent-drop) was recorded on the existing api2 `nested-relationship-writes` feature doc rather than promoted to a `standard`.** Rationale: it is the behavior of one already-documented capability, not a new cross-cutting rule — and it avoided an ELEVATED approval gate.
69
+ - **`worker2` was NOT added to the `aig` client app-scope.** Rationale: the transcript-coverage finding is a shared/internal worker2 fact, not AIG-specific.
70
+
71
+ ## Blockers
72
+
73
+ - **Communication deadlock with AIG, not a code blocker.** AIG will not enable the secondary/tertiary email fields until we tell them the feature is live; we have not told them, and our last message (2026-05-04) asked them to send the email fields "only if they are necessary for the process" — ambiguous phrasing delivered in the same breath as dropping Business Unit, which may have read as a de-scope. **There is no AIG reply to that message.** Both sides are waiting on each other.
74
+ - **Unknown whether the mapping document actually reached AIG.** In the 2026-05-01 meeting Paulina was holding the whole document back until the Business Unit / invoice-file question was answered. That question *was* answered on 2026-05-04, but nothing confirms the document went out afterward.
75
+ - **Deploy state of the library change is unverified** — cannot honestly tell AIG "it's live" until confirmed.
76
+
77
+ ## Exact next step
78
+
79
+ > Draft the note to Paulina for AIG's dev team. It must (1) retract the "only if necessary" ambiguity and state plainly that the secondary/tertiary addresses **are** wanted — unlike Business Unit; (2) include the concrete JSON example, not just the `contactEmailAddresses[].emailAddress` path expression, because a bare-string array `["email2@domain.com"]` returns HTTP 201 and silently drops the addresses (`array_column($list, 'emailAddress')` yields empty); (3) ask the two confirmations — did the mapping document reach AIG after the 2026-05-04 email, and has their dev acknowledged the `contactEmailAddresses[]` mapping. The exact payload fragment to paste is in `knowledge/1.0/apps/library/features/toga2-api-client-and-bridge.md` → the `TRUE-79401 payload shape — RESOLVED` section. Immediately after: check the git/deploy state of `C:\WWW\library` for the TRUE-79401 commit before signalling "live".
80
+
81
+ ---
82
+ _Saved by /session-save on 2026-08-03_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.494",
3
+ "version": "1.0.496",
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",