toga-ai 1.0.1126 → 1.0.1127

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.
Files changed (29) hide show
  1. package/knowledge/1.0/apps/library/features/cron-execution-monitoring.md +6 -1
  2. package/knowledge/1.0/apps/library/features/email-queue-attachments.md +5 -2
  3. package/knowledge/1.0/apps/library/features/error-capture-1-0.md +5 -1
  4. package/knowledge/1.0/apps/library/features/php-8-5-runtime-compatibility.md +13 -4
  5. package/knowledge/1.0/apps/store/workflows/deploy-to-elastic-beanstalk-al2023.md +4 -3
  6. package/knowledge/1.0/apps/tools/features/github-audit.md +4 -3
  7. package/knowledge/1.0/apps/tools/features/legacy-email-notifier.md +4 -3
  8. package/knowledge/1.0/apps/tools/features/mvc-data-access-patterns.md +4 -3
  9. package/knowledge/1.0/apps/worker/workflows/running-a-worker-cron-locally.md +4 -2
  10. package/knowledge/1.0/apps/worker1.5/INDEX.md +2 -0
  11. package/knowledge/1.0/apps/worker1.5/architecture.md +12 -3
  12. package/knowledge/1.0/apps/worker1.5/features/committed-secrets-and-log-exposure.md +37 -0
  13. package/knowledge/1.0/apps/worker1.5/workflows/diagnosing-crons-not-running.md +58 -0
  14. package/knowledge/1.0/standards/backend-php.md +3 -3
  15. package/knowledge/1.0/standards/framework-rules.md +11 -11
  16. package/knowledge/2.0/apps/api2/features/request-logging.md +3 -1
  17. package/knowledge/2.0/apps/api2/features/v2-rest-query-contract.md +20 -1
  18. package/knowledge/2.0/apps/worker2/features/oneuptime-worker2-monitoring.md +4 -2
  19. package/knowledge/INDEX.md +1 -1
  20. package/knowledge/clients/compass-usa/features/mits-sales-order-transmission-alerting.md +10 -5
  21. package/knowledge/clients/compass-usa/features/odp-edi-file-retention-and-retry.md +1 -1
  22. package/knowledge/clients/compass-usa/features/oneuptime-office-depot-inbound-files-monitor.md +4 -2
  23. package/knowledge/clients/compass-usa/profile.md +1 -2
  24. package/knowledge/clients/compass-usa/workflows/odp-edi-import-recovery.md +16 -2
  25. package/knowledge/clients/compass-usa/workflows/odp-order-pipeline-to-netsuite.md +9 -3
  26. package/knowledge/clients/nycdoe/profile.md +3 -2
  27. package/knowledge/clients/prudential/profile.md +17 -6
  28. package/knowledge/clients/true/profile.md +3 -2
  29. package/package.json +1 -1
@@ -6,15 +6,17 @@ project: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-01
9
+ updated: 2026-10-23
10
10
  owners: [dfranks, bala, mhammontree]
11
11
  files:
12
12
  - library/app/framework.php
13
13
  related:
14
14
  - ../../worker/architecture.md
15
+ - ../../worker1.5/architecture.md
15
16
  - ./email-queue-attachments.md
16
17
  - ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
17
18
  - ../../worker/workflows/tracing-a-worker-cron-run-in-production.md
19
+ - ../../worker1.5/workflows/diagnosing-crons-not-running.md
18
20
  ---
19
21
 
20
22
  1.0 cron check-in/out lifecycle via `App_Framework::cronInitialization()`/`cronFinished()` → `Common.CronJobExecutions`; open to monitor a 1.0 cron or read its per-run evidence.
@@ -48,6 +50,8 @@ Two consequences:
48
50
  ## Gotchas
49
51
 
50
52
  - **⚠ A run skipped by the overlap guard leaves NO ROW — absence is ambiguous.** `cronInitialization()` calls `exitIfProcessRunning()` **before** the `CronJobExecutions` INSERT, and the `cronFinished(true)` that guard then calls does nothing because `cronLogId` was never set. So a blocked run is indistinguishable from a run that never launched: **a missing row for an expected slot means "skipped", not "hung"**, and a hung run looks different (it has `dtCheckIn` with no `dtCheckOut`). `isProcessRunning()` matches **any** `ps -ef` line containing the script path (it only excludes `/bin/sh` and its own pid), so a stray `tail -f` or editor on that path blocks the cron indefinitely.
53
+ - **⚠ A non-`/bin/sh` crontab shell stops every guarded cron at once (TRUE-83121).** worker1.5's `ebs/cron.worker.php` wrote `SHELL=/bin/bash` into root's crontab, so each job's parent row was `/bin/bash -c "php .../crons/<job>.php ..."`. `isProcessRunning()` matched that parent, so every cron using the default `$exitIfProcessAlreadyRunning = true` (3rd arg of `cronInitialization()`) exited before its INSERT: **222 of 234 crons** silent from 2026-10-22 16:45 to 2026-10-23 10:35 Central (no Office Depot 850s, Staples, Prudential, NetSuite syncs). Only crons passing `false` ran. Fix: remove the `SHELL` line (worker1.5 PR #8). Rule: [worker1.5 Critical rules](../../worker1.5/architecture.md).
54
+ - **Many crons with zero rows since a deploy, while only the `false`-3rd-arg crons still log = the guard matches every run** (shell or `ps` output changed), not real overlap. Steps: [Diagnosing worker1.5 crons that stopped running](../../worker1.5/workflows/diagnosing-crons-not-running.md). worker1.5 rows have `instanceId = ''` ([why](./php-8-5-runtime-compatibility.md)); rows with an EC2 id are the old worker fleet, last at 2026-10-22 16:56 Central.
51
55
  - **`CronJobExecutions` is huge (~120M rows) with no index on `job`.** Never query it per run (e.g. as a cron's watermark) — it scans the table. Use a small dedicated table instead.
52
56
  - **`db_log` has no `CronJobExecutions` table.** A `cronFinished()` UPDATE that runs against the `db_log` connection throws on **every** 1.0 cron job. Both the INSERT and the UPDATE must target `db_common`.
53
57
  - **A `git merge` can silently resurrect pre-refactor logic.** These functions were cleanly consolidated to a single `db_common`-only INSERT/UPDATE pair (commit `0a3a6f75`), but a later `git merge origin/_production` (`509614036605…`) reintroduced the old dual-path structure from `_production`'s still-unmigrated copy — leaving duplicate INSERT/UPDATE blocks and reverting one path's connection argument back to `db_log`. Automatic merge resolution updated the SQL text (renamed the table) but restored the wrong connection name, producing a platform-wide breaking bug disguised as a simple PR-feedback rework. **When a merge touches a function you already refactored on your branch and the base branch never got that refactor, diff against the merge-base — do not just eyeball the current file** — to catch resurrected logic. (Fixed 2026-07-06: consolidated back to one `db_common` INSERT/UPDATE pair.)
@@ -58,4 +62,5 @@ Two consequences:
58
62
  - [OneUptime worker2 monitoring](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md), [Tracing a worker cron run in production](../../worker/workflows/tracing-a-worker-cron-run-in-production.md).
59
63
 
60
64
  ## Change history
65
+ - 2026-10-23 — Added the crontab-shell failure mode (`SHELL=/bin/bash` stopped 222 of 234 worker1.5 crons), the zero-rows tell, and the worker1.5 `instanceId = ''` note (TRUE-83121). (bala)
61
66
  - 2026-10-01 — Added the no-index / ~120M-row gotcha (rejected as a watermark source in TRUE-82180). (mhammontree)
@@ -6,12 +6,13 @@ project: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-23
10
10
  owners: ["mhammontree", "bala"]
11
11
  files:
12
12
  - library/app/email.php
13
13
  - library/phpmailer/class.phpmailer.php
14
14
  - worker/crons/notifications/infrastructure/send_emails.php
15
+ - worker1.5/crons/notifications/infrastructure/send_emails.php
15
16
  - worker/crons/notifications/covid/send_covid_pending_vaccination_approval.php
16
17
  - togadesk/desk/includes/functions.php
17
18
  related:
@@ -58,9 +59,11 @@ Therefore: **deploy `worker` before `togadesk`.** Roll back in the inverse order
58
59
  - **PHPMailer 5 hides a missing attachment.** `AddAttachment()` (`library/phpmailer/class.phpmailer.php` ~L1358) treats an unreadable path as `STOP_CONTINUE` with `$exceptions` off, and sends the message anyway. Absence of an error proves nothing.
59
60
  - **The drain cron's exclusion filter is dead code.** `send_emails.php` ~L34 carries `AND EmailsQueued.serializedEmailObject NOT LIKE '%App_Email%'`. The column holds base64, in which the literal `App_Email` never appears (verified with a PHP round-trip) — the filter matches and excludes nothing. **Not changed**: "fixing" it would stop TOGa Desk mail entirely. Needs a decision from whoever wrote it before anyone touches it.
60
61
  - **OPEN RISK — `max_allowed_packet` on the prod Common cluster is unverified.** Rows can now carry ~9.8 MB of base64. The column is LONGTEXT so it is fine, but MySQL 5.7 defaults `max_allowed_packet` to 4 MB (8.0: 64 MB). If prod is below the budget the INSERT fails as an `App_Query` exception on a live ticket reply — worse than a missing attachment. Check it, and lower `MAX_QUEUED_ATTACHMENT_BYTES` beneath it if needed (the oversized path already degrades to an inline send, the correct behavior).
62
+ - **⚠ OPEN: an email with an empty From fails at SES every time.** `App_Email` puts `fromEmailAddress` into `$mailer->From` with no check, and SES rejects it: `The following From address failed: ... Invalid MAIL FROM address provided`. Seen since at least 2026-10-01 (308 failures after 2026-10-22 12:00) on Elite Helpdesk "Support Ticket #N: Elite - Elite Helpdesk - Contact Center" and Office Depot "Support Ticket #N: Office Depot - Talos Office Depot tickets" mail; ticket #14781 had 260 attempts to 20 recipients. The drain deletes the queue row after one try, so repeats mean the caller sends it again each run. The code that builds these emails is not in the local repos; not fixed. Not caused by worker1.5.
63
+ - **Debug send failures in legacy `Logs.Emails`** (`dtSent`, `isSuccess`, `error`, `sourceJob`, `instanceId`, `subject`, `from`, `to`): group by `from`, `isSuccess`, `error`. On worker1.5 `instanceId` is `''` ([PHP 8.5 compatibility § Gotchas](./php-8-5-runtime-compatibility.md)).
61
64
  - Regression coverage: `test/@Mark/TRUE-80952/test_queued_email_attachments.php` — 26 assertions, no DB, no SMTP, confirmed failing against pre-fix HEAD.
62
65
 
63
66
  ## Related
64
67
 
65
- - [Branded HTML email templates](./email-templates.md), [App_Email side effects & test mode](./email-test-mode-and-write-free-sends.md).
68
+ - [Branded HTML email templates](./email-templates.md), [App_Email side effects & test mode](./email-test-mode-and-write-free-sends.md), [SES sender identity](./email-ses-sender-identity.md).
66
69
  - [Library architecture](../architecture.md), [TOGaDesk notifications](../../togadesk/features/notifications.md).
@@ -6,10 +6,11 @@ project: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-22
9
+ updated: 2026-10-23
10
10
  owners: ["jcardinal", "mhammontree", "bala", "snaredla", "rgirish"]
11
11
  files:
12
12
  - library/app/error/capture.php
13
+ - worker1.5/crons/sync/retailservices/sync_retail_services_client_service.php
13
14
  - library/app/error.php
14
15
  - library/app/exception/business.php
15
16
  - library/app/exception/api.php
@@ -118,6 +119,8 @@ Two things are specifically 1.0:
118
119
  ## Gotchas
119
120
 
120
121
  - **Looking for a 1.0 cron fatal? Query `Logs.Issue` on environment `prod`, ordered by `dtLastOccurred`.** The legacy `Logs.Errors` and `Vision_Log.ErrorLog` tables do **not** carry 1.0 cron fatals — searching them wastes time (2026-09-18, DOE outbound-cron outage). Uncaught exceptions reach here via `App_Error::handleException` → `captureException()`, which returns the short quotable reference. **A `catch` BYPASSES capture** — if you catch per record, call `App_Error_Capture::captureException($e)` yourself, and put only the returned reference (never `$e->getMessage()`, which can carry a raw vendor response body) into an email or log.
122
+ - **Did a fix stop an issue? Count its events per hour.** `Logs.Event` joined to `Logs.Issue` on `Event.issueId = Issue.id`, `WHERE Issue.reference = '<ref>'`, grouped by hour of `Event.dtCreated`. Example: worker1.5 issue `17P` ran 460 to 500 events an hour; the last one was 2026-10-23 10:28:20, one minute after the fix merged (TRUE-83107).
123
+ - **⚠ OPEN: worker1.5 OptimumDesk sync puts a password into the error message.** `crons/sync/retailservices/sync_retail_services_client_service.php` L471 and L591 throw with `print_r($rowAccount, true)`. `$rowAccount` is the `$_ENV` the parent passes, which includes `ClientSuppliers.opDeskPassword`. The message is not redacted, so it lands in `/var/log/worker-cron.log` and in `Issue.subject` / `Event.errorMessage`. Fix: put only ids (`clientAccountId`, `api`) in the message. Rule: never `print_r` a row that carries credentials into an exception or log.
121
124
  - **App_Error writes the real error message nowhere on the server** — only to Sentry and `Logs.Issue`. On a new env where both sends fail (no `[database_toga2logs]`, Sentry blocked) the red box is all you get. Fix per app: wrap the App_Error handlers and also `error_log()` + `syslog(LOG_ERR)` the message; syslog reaches `/var/log/messages`, which EB log bundles include (togadesk `_/app/framework.php`, TRUE-83053 — [Desk deploy § Error visibility](../../togadesk/workflows/deploy-to-elastic-beanstalk-al2023.md#error-visibility)).
122
125
  - **Vision does not capture — use Sentry.** `vision/config.prod.ini` has no `[database_toga2logs]` section, so vision errors never reach `Logs.Issue`; only Sentry project `vision` has them. Do not search `Logs.Issue` for vision errors.
123
126
  - **`\Error` and `\Exception` declare `$message` separately — reflect the right base class.** `App_Error::handleException` (`library/app/error.php` ~646) reflected `\Exception::$message` on a PHP `\Error` (e.g. class not found) → `Cannot access protected property Error::$message`, so the error never reached Sentry, and the catch `debugVar()`-ed the full trace onto the prod page (TRUE-82793). Fixed on library branch TRUE-82959: reflect `'\Error'` when `instanceof \Error`, else `'\Exception'`; the catch now `error_log()`s and only `debugVar()`s under `App_Registry::inDevMode()`.
@@ -131,6 +134,7 @@ Two things are specifically 1.0:
131
134
  - **⚠ The legacy pre-Sentry handler dumps the entire config on any error it renders.** `library/app/DELETE_DOA_PRE-SENTRY_error.php` is a dead-named but still-reachable handler whose `htmlBacktrace()` does `print_r($entry['args'])` for **every** frame. On 2026-08-05 a real error page on **https://towfoundation.togaview.com** rendered ~26 pages containing the whole config array — production DB host/user/password, SES SMTP credentials, NetSuite credentials, Payeezy API key/secret/merchant token, SSL Store token — plus ticket PII. The current `library/app/error.php` is safe (it dumps only under `App_Registry::inDevMode()`), but **both** handlers gate on `inDevMode()`, so the affected host was **evaluating as dev mode in production**. Two actions: (1) verify `inDevMode()` per host in production before trusting any error page not to leak, and (2) delete the legacy handler. Rotation of every exposed credential **was escalated to Jeff on 2026-08-05; confirmation that it was completed is still owed** — treat those credentials as compromised until confirmed. (No values are recorded here or anywhere in the KB.)
132
135
 
133
136
  ## Change history
137
+ - 2026-10-23 — Issue event-rate check; open OptimumDesk password leak in worker1.5 exception messages (TRUE-83107) (bala)
134
138
  - 2026-10-22 — Gotcha: App_Error never logs the message on the server; app-level syslog pattern (TRUE-83053) (rgirish)
135
139
  - 2026-10-22 — `setAccessible()` guarded for PHP 8.5 in `error.php` / `capture.php`; likely cause of the open handler crash (TRUE-83059) (rgirish)
136
140
  - 2026-10-20 — Fixed `\Error` reflection + prod trace dump in `handleException` (TRUE-82959) (snaredla)
@@ -6,8 +6,8 @@ project: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-22
10
- owners: ["rgirish", "jcardinal"]
9
+ updated: 2026-10-23
10
+ owners: ["rgirish", "jcardinal", "bala"]
11
11
  files:
12
12
  - library/app/error.php
13
13
  - library/app/date.php
@@ -23,6 +23,9 @@ files:
23
23
  - library/app/api/cybersource.php
24
24
  - library/app/togadesk/reports.php
25
25
  - library/browser/datagrid.php
26
+ - library/app/framework.php
27
+ - library/app/edi.php
28
+ - library/netsuitetoolkit/NSPHPClient.php
26
29
  related:
27
30
  - error-capture-1-0.md
28
31
  - ../../walmart-activate/workflows/deploy-to-elastic-beanstalk-al2023.md
@@ -37,7 +40,7 @@ How library (1.0) handles PHP 8.4/8.5 deprecations (logged, not fatal), the PHP
37
40
 
38
41
  ## Summary
39
42
 
40
- Since library PR #905 (TRUE-83055, on `_production` 2026-10-22), `App_Error::handleError` logs `E_DEPRECATED` / `E_USER_DEPRECATED` once per file:line per request (`error_log`, via `logDeprecation`) and returns `true`. A deprecation no longer goes through `handleException`, so it does not roll back the open transaction or exit. Every other warning and notice still exits as before. library must stay PHP 7.2 safe (other 1.0 apps run 7.2): fixes are version guards or 7.1-valid syntax only.
43
+ Since library PR #905 (TRUE-83055, on `_production` 2026-10-22), `App_Error::handleError` logs `E_DEPRECATED` / `E_USER_DEPRECATED` once per file:line per request (`error_log`, via `logDeprecation`) and returns `true`. A deprecation no longer goes through `handleException`, so it does not roll back the open transaction or exit (seen on worker1.5 prod: one Staples `sync_staples_cxml.php` run logged deprecations at L548, L786, L795, L835 and kept going). A deprecation masked out by `error_reporting()` is dropped silently. Every other warning and notice still exits as before. library must stay PHP 7.2 safe (other 1.0 apps run 7.2): fixes are version guards or 7.1-valid syntax only.
41
44
 
42
45
  ## How it works
43
46
 
@@ -47,12 +50,14 @@ Fix patterns (TRUE-83059 `d28f9b23`, TRUE-83055 PR #905):
47
50
  |---|---|---|
48
51
  | 8.5 | `curl_close()`, `imagedestroy()`, `xml_parser_free()` | call only when `PHP_VERSION_ID < 80000` (no-ops since 8.0) |
49
52
  | 8.5 | `ReflectionProperty::setAccessible()` | call only when `PHP_VERSION_ID < 80100` |
50
- | 8.4 | implicit nullable `string $x = null` | `?string $x = null` (valid since 7.1) |
53
+ | 8.4 | implicit nullable `string $x = null` | `?string $x = null` (valid since 7.1); e.g. `NSPHPClient::setFields(?array $fieldArray = null)` (PR #908) |
54
+ | 8.1 | `null` into `str_replace` from `App_Edi::cleanText`'s `json_decode` of an unquoted `\u0000...\u007f` string | block removed (PR #908): the decode was always `NULL`, so the replace never changed EDI text on any version; output byte-identical |
51
55
  | 8.2 | `utf8_encode($x)` / `utf8_decode($x)` | `mb_convert_encoding((string) $x, 'UTF-8', 'ISO-8859-1')` / `mb_convert_encoding((string) $x, 'ISO-8859-1', 'UTF-8')` — byte-identical for all 256 bytes, so the Cybersource HMAC is unchanged |
52
56
  | 8.5 | `$http_response_header` (`app/error/capture.php`) | library: `stream_get_meta_data()` (7.2 safe); 8.4+-only apps: `http_get_last_response_headers()` |
53
57
  | 8.x | `(boolean)` / `(integer)` casts | `(bool)` / `(int)` |
54
58
  | 8.2 | dynamic property | declare it on the class (TRUE-83055: `App_Api_Carrier_ShipmentRequest::$shipToAddressLine2`, `App_Email_AigTemplates_SendAigSupportTicketReplyNotification::$contact`) |
55
59
  | 8.1 | `SoapClient` overrides without return types (`app/soaptransaction.php`) | return-type attributes + 6th `$uriParserClass` arg |
60
+ | 8.5 | `case 'x';` (semicolon after `case`) — vendored `php-imap2/src/Roundcube/ImapClient.php:2580` flooded worker1.5's `worker-cron.log` | `case 'x':` (library `53ea57f8`) |
56
61
 
57
62
  State on `_production`: `php -l` on PHP 8.4 shows no deprecations in `app/` or `browser/`.
58
63
 
@@ -70,11 +75,15 @@ Null guards (TRUE-83054, commit `04a2293c`, landed directly on `_production`)
70
75
  - **Still open — vendored third-party folders only:** `LightSaml/`, `simplesaml/`, `jpgraph/`, `mobile_detect/`, `invoiceprocessor/pdf/` raise 8.4 compile-time deprecations (implicit nullable, optional-before-required, `${var}`). Logged, not fatal.
71
76
  - **Warnings and notices still exit.** Only deprecations were softened — an "offset on null" or "undefined index" still kills the request on any PHP version.
72
77
  - **`file_get_contents` to IMDS or any optional endpoint:** the `ignore_errors` stream option covers only HTTP status — a refused or slow connection still warns, and App_Error exits. Wrap the call in a local `set_error_handler` / `restore_error_handler` and check for a `200` status line (togaview TRUE-83060).
78
+ - **The `instance-data` hostname does not resolve on AL2023.** Use `169.254.169.254` only in new code. Old uses: `App_Error_Capture::AWS_METADATA_HOSTS` falls back to it when `169.254.169.254` returns non-200 (e.g. the tags path with instance metadata tags off), so the cron log shows `getaddrinfo for instance-data failed` warnings (noise, capture still works). `App_Cloud::getInstanceId()` (`app/cloud.php` ~L711) calls only `instance-data`, so it returns `''`, and `App_Framework::getInstanceId()` caches that `''` in `instanceId.txt`. That is why worker1.5 writes `instanceId = ''` to `Common.CronJobExecutions` and `Logs.Emails`. `app/error.php` ~L642 (Sentry AWS context) also uses it.
73
79
  - **PHP 7.2 safety was checked by reading, not running** (no 7.2 binary locally on PR #905). The guards use `PHP_VERSION_ID` (5.2.7+) and `?type` (7.1+). Run a 7.2 smoke test if a 7.2 app breaks after this change.
80
+ - **Do not "repair" the removed `App_Edi::cleanText` block.** A working version would replace every character 0-127 (all letters and digits) with spaces and blank every EDI file.
74
81
  - Local dev on PHP 8.2 does not show 8.4/8.5 deprecations — test with an 8.5 binary.
75
82
  - **PHP 8 string-to-number comparison can turn a loop infinite.** `api` subscription: a non-numeric quantity compared to an int looped forever on 8.x. Cast `(int)` before the loop.
76
83
 
77
84
  ## Change history
85
+ - 2026-10-23 — `instance-data` does not resolve on AL2023 (empty instance id, log noise); deprecations-continue behavior confirmed in the worker1.5 cron log (TRUE-83107). (bala)
86
+ - 2026-10-23 — `case 'x';` 8.5 deprecation fixed in vendored php-imap2 (TRUE-83116). (bala)
78
87
  - 2026-10-22 — Dynamic-property fixes; app-level offset-on-false shim pattern from the Desk move (TRUE-83055, TRUE-83053) (rgirish)
79
88
  - 2026-10-22 — `App_Date` / `App_Database` null guards; 8.x break classes from the `api` move (TRUE-83054) (jcardinal)
80
89
  - 2026-10-22 — `$http_response_header` is an 8.5 deprecation; optional-endpoint warning pattern (TRUE-83060) (rgirish)
@@ -6,8 +6,8 @@ project: Store
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-10-22
10
- owners: ["rgirish"]
9
+ updated: 2026-10-23
10
+ owners: ["rgirish", "bala"]
11
11
  files:
12
12
  - store/.ebextensions/001_platform.config
13
13
  - store/.ebextensions/009_setup_phpini.config
@@ -48,12 +48,13 @@ store moved from PHP 7.2 / Amazon Linux 1 to PHP 8.5 / AL2023 on TRUE-83055 (sto
48
48
 
49
49
  - **The S3 `setup_git_libraries.php` clones nothing on AL2023** (it needs IMDSv1 and `/var/app/ondeck`). Use the bash clone. Never use `ignoreErrors` on the clone.
50
50
  - **PHP 8.x code fixes in store (TRUE-83055):**
51
- - `case X;` → `case X:` (49 places: `common/spglobal/header.php`, `common/spglobal/home.php`, `common/nychh/home.php`). 8.5 prints a deprecation, and 1.0 exits on it.
51
+ - `case X;` → `case X:` (49 places: `common/spglobal/header.php`, `common/spglobal/home.php`, `common/nychh/home.php`). 8.5 prints a deprecation (fatal before library PR #905, now only logged).
52
52
  - `App_Database::fetchRow($res)[0]` → `(App_Database::fetchRow($res)[0] ?? null)` (18 places in `_/app/framework.php`, `mvc/users/impersonate/{put,delete}.php`). On no row it warns "offset on null" (since 7.4), and App_Error exits. Seen as Compass login failing when the user has no Location.
53
53
  - **Open: ~149 call sites pass a possibly-NULL DB value to `trim` / `strtolower` / `explode`** (8.1 deprecation). Not fatal: library `_production` logs deprecations instead of exiting (PR #905, see [PHP 8.5 runtime compatibility](../../library/features/php-8-5-runtime-compatibility.md)). Fix them from `error_log`.
54
54
  - **Open security issue (pre-existing, needs its own ticket):** committed `config.*.ini` in the store web root can be downloaded under Apache. Same issue as Walmart Activate; copy togaview's working `deny.conf` fix ([TOGa View deploy](../../togaview/workflows/deploy-to-elastic-beanstalk-al2023.md)). `ebs/git.json` holds a clone token — never copy it out.
55
55
  - **Not yet checked on the new environment:** `getenv('ENVIRONMENT')` under php-fpm behind Apache, and that `/var/app/staging` is removed after deploy (the `005` php-file-watch cron skips while it exists).
56
56
 
57
57
  ## Change history
58
+ - 2026-10-23 — `case X;` note: deprecations are logged, not fatal, since PR #905 (TRUE-83107) (bala)
58
59
  - 2026-10-22 — Live on `togatech-legacy-store-production` (toga-prod); arm64 dart-sass in `021` (TRUE-83055) (rgirish)
59
60
  - 2026-10-22 — Created: store moved to PHP 8.5 / AL2023 (Apache proxy, bash clone, phpini composer, Redis via `ini_set`, 8.x code fixes) (TRUE-83055) (rgirish)
@@ -6,8 +6,8 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-09
10
- owners: [jcardinal]
9
+ updated: 2026-10-23
10
+ owners: [jcardinal, bala]
11
11
  files:
12
12
  - tools/_/app/github/audit.php
13
13
  - tools/mvc/developers/github-audit/get.php
@@ -85,7 +85,7 @@ Behind Tools SSO. `get.php` and `post.php` self-guard with `requireAuth` + `App_
85
85
 
86
86
  ## Gotchas
87
87
  - **The `hidden` attribute is beaten by a class `display:` rule.** The results/spinner box had the HTML `hidden` attribute but still showed on load (looked like the tool auto-ran), because an author `display:flex` class rule beats the UA `[hidden]{display:none}`. Fix: a scoped guard `.<scope> [hidden] { display:none !important; }` (or `:not([hidden])` on the display rule). General server-rendered-UI trap.
88
- - **`curl_close()` is fatal on PHP 8.5** in the 1.0 framework (deprecation → `ErrorException` → aborted request). The audit engine avoids `curl_close()` — same rule as the Design tool. See [Tools architecture](../architecture.md) and framework-rules.
88
+ - **The audit engine avoids `curl_close()`** (deprecated in 8.5, a no-op since 8.0). It aborted the request until library PR #905 (2026-10-22); now deprecations are only logged ([PHP 8.5 compatibility](../../library/features/php-8-5-runtime-compatibility.md)).
89
89
  - **APCu lock is per-EB-instance**, so the single-run guard is per-instance, not org-wide.
90
90
 
91
91
  ## Related
@@ -95,4 +95,5 @@ Behind Tools SSO. `get.php` and `post.php` self-guard with `requireAuth` + `App_
95
95
  - [github_audit.php standalone script](../../test/features/github-audit-script.md)
96
96
 
97
97
  ## Change history
98
+ - 2026-10-23 — `curl_close()` no longer fatal: library logs deprecations (TRUE-83107) (bala)
98
99
  - 2026-10-09 — `token()` now resolves its own `GITHUB_TOKEN`; no longer borrows `App_Design_Github::token()` (deleted when Design moved to S3, TRUE-82616) (jcardinal)
@@ -6,7 +6,7 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-13
9
+ updated: 2026-10-23
10
10
  owners: [bala, jcardinal]
11
11
  files:
12
12
  - tools/mvc/email-migration/notify/get.php
@@ -58,8 +58,8 @@ Prod `tools` runs **multiple Elastic Beanstalk instances behind a load balancer*
58
58
  ### `App_Email::send()` is unusable in the tools runtime; send via SES SMTP directly
59
59
  Every send failed (`sent:0`, all "Send failed") because `App_Email::send()` also does a `Defaults` lookup (Vision / `db_main`), a `Logs.Emails` write (`db_logs`), and an `EmailSuppressionList` check (Common / `db_common`) — that 1.0 email-stack DB wiring is **not** present in the tools app, so it threw for every recipient. Fix: send with **PHPMailer directly** using `App_Email`'s SES SMTP **constants** (`SMTP_HOSTNAME` / `SMTP_PORT` / `SMTP_SECURE` / `SMTP_USERNAME` / `SMTP_PASSWORD` — the same proven SES SMTP creds, `us-west-2`) and **no DB calls**; From `no-reply@togatech.com` / "TOGa IT". **Operational prerequisite:** the `togatech.com` sender identity/domain must be **verified in that us-west-2 SES account** or delivery is rejected. Any future tools-app feature that needs to send email hits the same wall — bypass `App_Email::send()` and go straight to SES.
60
60
 
61
- ### PhpSpreadsheet 1.6.0 deprecations abort the request
62
- The vendored PhpSpreadsheet is **1.6.0** (library composer constraint `^1.6`, which supports only PHP 5.6/7.0) and emits `E_DEPRECATED` all over on PHP 8.x. A single deprecation during the xlsx parse aborts the request, because the 1.0 error handler promotes any PHP notice/deprecation to a thrown exception **and** (when Sentry is absent) echoes "Sentry is not installed…" into the response, corrupting the JSON. Fix: install a **scoped `set_error_handler`** at the top of the endpoint that swallows non-fatal diagnostics (returns `true`) for the duration of the action; real fatals and thrown exceptions still reach the `catch`. The framework mechanism is documented in [mvc-data-access-patterns](./mvc-data-access-patterns.md).
61
+ ### PhpSpreadsheet 1.6.0 deprecations aborted the request (before library PR #905)
62
+ The vendored PhpSpreadsheet is **1.6.0** (library composer constraint `^1.6`, which supports only PHP 5.6/7.0) and emits `E_DEPRECATED` all over on PHP 8.x. Until library PR #905 (2026-10-22) a single deprecation during the xlsx parse aborted the request and (when Sentry is absent) echoed "Sentry is not installed…" into the JSON. Now deprecations are only logged ([PHP 8.5 compatibility](../../library/features/php-8-5-runtime-compatibility.md)); a warning or notice still aborts. Fix: install a **scoped `set_error_handler`** at the top of the endpoint that swallows non-fatal diagnostics (returns `true`) for the duration of the action; real fatals and thrown exceptions still reach the `catch`. The framework mechanism is documented in [mvc-data-access-patterns](./mvc-data-access-patterns.md).
63
63
 
64
64
  **Security:** 1.6 is not safe for untrusted xlsx (XXE scanner bypass, no zip-size limits). New upload features parse in worker2 instead — see [job bridge](./worker2-job-bridge.md).
65
65
 
@@ -73,4 +73,5 @@ The vendored PhpSpreadsheet is **1.6.0** (library composer constraint `^1.6`, wh
73
73
  - [Tools architecture](../architecture.md)
74
74
 
75
75
  ## Change history
76
+ - 2026-10-23 — Deprecations no longer abort since library PR #905 (TRUE-83107) (bala)
76
77
  - 2026-10-13 — Noted PhpSpreadsheet 1.6 is unsafe for untrusted xlsx; new uploads parse in worker2 (bala)
@@ -6,7 +6,7 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-06
9
+ updated: 2026-10-23
10
10
  owners: [jcardinal, bala, ajean]
11
11
  files:
12
12
  - tools/_/app/nav.php
@@ -146,8 +146,8 @@ if (!headers_sent()) { http_response_code(401); } // matches App_MVC::routeTo'
146
146
 
147
147
  `establishSession()` (auth.php) guards `session_regenerate_id` for the same reason; the SSO failure path hit this too — see [saml-sso-auth](./saml-sso-auth.md). The clean long-term fix is to handle status-setting routes **before any output** and `exit`.
148
148
 
149
- ### The error handler promotes ANY PHP notice/deprecation to a thrown exception (and corrupts JSON)
150
- `App_Error::handleError` (`library/app/error.php`) **ignores the `error_reporting` mask** and promotes **any** PHP notice/warning/deprecation to a thrown `ErrorException`. When Sentry is not installed it **also echoes** "Sentry is not installed…" into the response — which **corrupts a JSON response body**. So a single `E_DEPRECATED` (e.g. an old vendored package like PhpSpreadsheet 1.6.0 running on PHP 8.x) aborts the whole request and, on an AJAX endpoint, breaks the JSON the browser is parsing. This is the same escalation behind the preloader-flush 500 above.
149
+ ### The error handler turns ANY PHP notice/warning into an abort (and corrupts JSON)
150
+ `App_Error::handleError` (`library/app/error.php`) **ignores the `error_reporting` mask** for notices and warnings and sends them to `handleException`, which exits. When Sentry is not installed it **also echoes** "Sentry is not installed…" into the response — which **corrupts a JSON response body** on an AJAX endpoint. This is the same escalation behind the preloader-flush 500 above. Deprecations are the exception: since library PR #905 (2026-10-22) they are only logged ([PHP 8.5 compatibility](../../library/features/php-8-5-runtime-compatibility.md)).
151
151
 
152
152
  Fix for a page/endpoint that legitimately triggers non-fatal diagnostics: install a **scoped** `set_error_handler` at the top of the action that **returns `true`** to swallow non-fatal notices for the duration of the action; real fatals and thrown exceptions still reach your `catch`. Scope it tightly — do not disable diagnostics app-wide.
153
153
 
@@ -163,5 +163,6 @@ set_error_handler(fn(): bool => true); // swallow non-fatal notices/deprecations
163
163
  - [1.0 MVC page pattern & app skeleton](../../library/features/mvc-page-pattern-and-app-skeleton.md)
164
164
 
165
165
  ## Change history
166
+ - 2026-10-23 — Deprecations are logged, not aborts, since library PR #905 (TRUE-83107) (bala)
166
167
  - 2026-10-06 — Added the `GET_LOCK` 64-char gotcha (hash the variable part). (ajean)
167
168
  - 2026-10-05 — Added the request-input xssProtect gotcha (JSON posts decode to NULL). (bala)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-10-16
9
+ updated: 2026-10-23
10
10
  owners: ["bala"]
11
11
  files:
12
12
  - worker/_/app/framework.php
@@ -64,7 +64,8 @@ For logic-only tests with no bootstrap, use the
64
64
 
65
65
  ### Step 4 — run on PHP 7.2
66
66
  Use Laragon's PHP 7.2.33 build (matches prod). On PHP 8.1 the old PhpSpreadsheet in `library`
67
- hits an `Iterator` return-type deprecation, which `App_Error` turns into an exit.
67
+ hits an `Iterator` return-type deprecation. Older library builds exited on it; since library PR #905
68
+ (2026-10-22) it is only logged ([PHP 8.5 compatibility](../../library/features/php-8-5-runtime-compatibility.md)).
68
69
 
69
70
  ## Gotchas
70
71
  - **`App_Framework_Worker::initialize()` cannot run twice in one process** — the second
@@ -84,5 +85,6 @@ hits an `Iterator` return-type deprecation, which `App_Error` turns into an exit
84
85
  - [App_Email side effects & test mode](../../library/features/email-test-mode-and-write-free-sends.md)
85
86
 
86
87
  ## Change history
88
+ - 2026-10-23 — Deprecations no longer exit since library PR #905 (TRUE-83107) (bala)
87
89
  - 2026-10-16 — Created from the cron 7 local test harness (prod-pointing configs, Sentry, SMTP,
88
90
  PHP 7.2, Windows `:` file names, NetSuite 500/unreachable test). (bala, TRUE-82888)
@@ -3,4 +3,6 @@
3
3
  | Doc | Summary |
4
4
  |-----|---------|
5
5
  | [Worker 1.5 (1.0 Framework) Architecture](architecture.md) | The single-server 1.0 cron tier that replaced worker's 7-role fleet; open for how the crontab is built from schedules/*.json, the overlap check, and what the cr |
6
+ | [worker1.5 — Committed Secrets & Credentials in Logs (debt register)](features/committed-secrets-and-log-exposure.md) | Open security debt in worker1.5: a GitHub token committed in `ebs/git.json` and a cron that writes OptimumDesk passwords into the cron log; open before touching |
6
7
  | [worker1.5 PHP ini overrides on EB AL2023 (memory_limit 11G, zz-application.ini)](features/php-ini-memory-limit-eb.md) | How worker1.5 sets PHP ini values (memory_limit 11G) on EB `worker15-production`; open before changing any PHP ini setting or wondering why the EB console value |
8
+ | [Diagnosing worker1.5 crons that stopped running (single-server 1.0 tier)](workflows/diagnosing-crons-not-running.md) | Steps to tell "worker1.5 crons are not running" from "a downstream system is failing", find the cause on the box, and recover; open when several 1.0 jobs or bac |
@@ -15,11 +15,14 @@ files:
15
15
  - worker1.5/crons/
16
16
  - worker1.5/mvc/get.php
17
17
  - worker1.5/ebs/setup_phpini.php
18
+ - worker1.5/ebs/setup_git_libraries.php
18
19
  related:
19
20
  - features/php-ini-memory-limit-eb.md
20
21
  - ../worker/architecture.md
21
22
  - ../library/architecture.md
22
23
  - ../../standards/backend-php.md
24
+ - workflows/diagnosing-crons-not-running.md
25
+ - features/committed-secrets-and-log-exposure.md
23
26
  ---
24
27
 
25
28
  The single-server 1.0 cron tier that replaced worker's 7-role fleet; open for how the crontab is built from schedules/*.json, the overlap check, and what the cron list page is (display only).
@@ -32,13 +35,14 @@ The single-server 1.0 cron tier that replaced worker's 7-role fleet; open for ho
32
35
 
33
36
  ## How a job becomes a cron
34
37
 
35
- 1. On deploy, `.platform/hooks/postdeploy/020_cron.sh` links `/var/www/html` to `/var/app/current`, reads `ENVIRONMENT` from EB config, and runs `php ebs/cron.worker.php`.
36
- 2. `cron.worker.php` reads `schedules/cron.<ENVIRONMENT>.json` and writes root's crontab:
38
+ 1. On deploy, `ebs/setup_git_libraries.php` clones `library`, `_underscore`, `dbchanges`, `resources` and `togadesk` (branch `_production` for production, else `_<ENVIRONMENT>`) into the app; a failed clone fails the deploy. **A library fix reaches worker1.5 only on the next worker1.5 deploy.** Clone credentials: [committed secrets](features/committed-secrets-and-log-exposure.md).
39
+ 2. On deploy, `.platform/hooks/postdeploy/020_cron.sh` links `/var/www/html` to `/var/app/current`, reads `ENVIRONMENT` from EB config, and runs `php ebs/cron.worker.php`.
40
+ 3. `cron.worker.php` reads `schedules/cron.<ENVIRONMENT>.json` and writes root's crontab:
37
41
  - `cron` → `php /var/www/html/crons/<cron>`; `url` → `wget -q -O /dev/null http://localhost<url>`; `command` → used as is.
38
42
  - `active: 0` rows are skipped.
39
43
  - Every job appends to the one log file `/var/log/worker-cron.log` (logrotate `/etc/logrotate.d/worker-cron`).
40
44
  - It refuses to install if the file is missing, the JSON is bad, a row has no cron/url/command, or there are 0 active jobs.
41
- 3. Timezone for the installer is `America/Chicago`.
45
+ 4. Timezone for the installer is `America/Chicago`.
42
46
 
43
47
  Schedule files: `cron.alpha/beta/demo/hotfix/sprint/stage/ui/worker.json`. Only `worker` (production) carries the real list. The others hold 1 to 2 rows.
44
48
 
@@ -46,6 +50,11 @@ Schedule files: `cron.alpha/beta/demo/hotfix/sprint/stage/ui/worker.json`. Only
46
50
 
47
51
  `/` renders the schedule files as a list plus a sidebar. **Display only.** Crons never go through it, so a page error does not stop crons. It reads and sorts the schedule files once into `$scheduleSections`. `cron.worker.json` (empty category) shows as section `misc`.
48
52
 
53
+ ## Operating it
54
+
55
+ No CloudWatch log group: every job's output is `/var/log/worker-cron.log` on the box; reach it with SSM Session Manager. `Common.CronJobExecutions.instanceId` is `''` for this server. Crons stalled or many 1.0 backlogs at once: [Diagnosing worker1.5 crons that stopped running](workflows/diagnosing-crons-not-running.md).
56
+
49
57
  ## Gotchas
50
58
  - **Do not read files that are not in this repo on a 1.0 page.** The page read `schedules/cron.worker.catalog.json` (old worker only). The warning became `exit` and the page died about 474 times an hour (9,387 total), probably from a health check on `/`. Fixed in TRUE-83107 (c903b20).
51
59
  - **`App_Misc::getFileList` uses `readdir` and does not sort.** Any loop that keeps state per file must set that state on every file. The old loop set `$jsonContents` only for a non-empty category, so `cron.worker.json` reused the previous file's rows, or warned (and exited) if it came first.
60
+ - **Cron log noise you can ignore:** `PHP Deprecated: ...` lines (the job continues) and `getaddrinfo for instance-data failed` warnings (see [PHP 8.5 compatibility § Gotchas](../library/features/php-8-5-runtime-compatibility.md)). Two open faults show up in the log: the OptimumDesk sync prints a credential row into its exception ([Error capture 1.0 § Gotchas](../library/features/error-capture-1-0.md)), and empty-From emails fail at SES every run ([Email queue § Gotchas](../library/features/email-queue-attachments.md)).
@@ -0,0 +1,37 @@
1
+ ---
2
+ title: "worker1.5 — Committed Secrets & Credentials in Logs (debt register)"
3
+ framework: "1.0"
4
+ repo: worker1.5
5
+ project: Worker 1.5
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-23
10
+ owners: ["bala"]
11
+ files:
12
+ - worker1.5/ebs/git.json
13
+ - worker1.5/ebs/setup_git_libraries.php
14
+ - worker1.5/crons/sync/retailservices/sync_retail_services_client_service.php
15
+ related:
16
+ - ../architecture.md
17
+ - ../../worker/architecture.md
18
+ - ../../../2.0/apps/worker2/features/committed-secrets-and-config-debt.md
19
+ - ../../library/features/error-capture-1-0.md
20
+ ---
21
+
22
+ Open security debt in worker1.5: a GitHub token committed in `ebs/git.json` and a cron that writes OptimumDesk passwords into the cron log; open before touching deploy cloning, rotating credentials, or reading `/var/log/worker-cron.log`.
23
+
24
+ ## Summary
25
+ Two open risks found 2026-10-23 (TRUE-83121). Neither is fixed. Values are never recorded here.
26
+
27
+ ## How it works
28
+ | Risk | Where | What to do |
29
+ |---|---|---|
30
+ | Plaintext GitHub token for account `agilantgit` committed on `_main` | `ebs/git.json` (used at deploy to clone `library`, `_underscore`, `resources`, `dbchanges`, `togadesk`) | Treat as leaked. Rotate it, then move it to SSM / EB env and read it at deploy. Same debt exists in `worker` (copied from it). |
31
+ | OptimumDesk passwords in the error message | `crons/sync/retailservices/sync_retail_services_client_service.php` | Details and fix: [1.0 error capture § Gotchas](../../library/features/error-capture-1-0.md). Treat any `/var/log/worker-cron.log` or `Logs.Issue` row holding it as sensitive. |
32
+
33
+ ## Gotchas
34
+ - `/var/log/worker-cron.log` is one shared file for every job on the server (see [architecture](../architecture.md)), so one leaking cron exposes the secret to anyone reading cron output.
35
+
36
+ ## Change history
37
+ - 2026-10-23 — Created: GitHub token in `ebs/git.json`, OptimumDesk passwords in the cron log (TRUE-83121). (bala)
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: Diagnosing worker1.5 crons that stopped running (single-server 1.0 tier)
3
+ framework: "1.0"
4
+ repo: worker1.5
5
+ project: Worker 1.5
6
+ client: shared
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-10-23
10
+ owners: [bala]
11
+ files:
12
+ - worker1.5/ebs/cron.worker.php
13
+ - worker1.5/ebs/setup_git_libraries.php
14
+ - worker1.5/schedules/cron.worker.json
15
+ - worker1.5/crons/notifications/infrastructure/send_emails.php
16
+ - library/app/framework.php
17
+ related:
18
+ - ../architecture.md
19
+ - ../../library/features/cron-execution-monitoring.md
20
+ - ../../worker/workflows/tracing-a-worker-cron-run-in-production.md
21
+ - ../../library/features/email-queue-attachments.md
22
+ - ../../../clients/compass-usa/features/mits-sales-order-transmission-alerting.md
23
+ ---
24
+
25
+ Steps to tell "worker1.5 crons are not running" from "a downstream system is failing", find the cause on the box, and recover; open when several 1.0 jobs or backlogs stall at once.
26
+
27
+ ## Summary
28
+
29
+ Since TRUE-83078 (2026-10-22) every 1.0 cron runs on ONE server: EB env `worker15-production` (toga-production account, us-east-1, AL2023, PHP 8.5). So one bad crontab or library change stops every 1.0 job at once. Symptoms show up as many unrelated alarms (Compass MITS backlog, Prudential SRs not becoming SOs, NetSuite syncs idle, emails not sent, daily reports missing). Check "is the cron running at all" before debugging any single job.
30
+
31
+ ## Steps
32
+
33
+ 1. **Prove the cron is not running (DB, from your desk).**
34
+ - Legacy `Common.CronJobExecutions`: rows per `job` since the suspected boundary. Zero rows for most jobs = crons exit before check-in (step 3; reading rules in [Cron Execution Monitoring](../../library/features/cron-execution-monitoring.md)). `instanceId` is `''` for worker1.5 rows: host `instance-data` does not resolve on AL2023 ([PHP 8.5 compatibility § Gotchas](../../library/features/php-8-5-runtime-compatibility.md)). Not a cron fault.
35
+ - Business signals that separate "not running" from "downstream rejecting": legacy `Logs.API` last outbound call per endpoint (e.g. MITS), `Client_Compass.SalesOrders.c_dtTransmittedToCompass`, `Common.EmailsQueued` row count (a queue, should drain to ~0), `Logs.Issue` new events. No new calls + no new Issue events = not running.
36
+ 2. **Get on the box.** SSM Session Manager (instance shows Online). There is NO CloudWatch log group for worker15-production. Every job's output goes to `/var/log/worker-cron.log` on the box.
37
+ 3. **Check the overlap guard.**
38
+ - `crontab -l` as root: there must be no `SHELL=` line (cron must use `/bin/sh`). Why: the crontab-shell gotcha in [Cron Execution Monitoring](../../library/features/cron-execution-monitoring.md).
39
+ - `ps -ef | grep <script>`: any row holding the script path (a parent shell, a `tail -f`) blocks it.
40
+ - Tell: the same cron run by hand (`php /var/www/html/crons/<path>`) from an interactive shell works. "Most crons dead, only the `cronInitialization(x, y, false)` ones alive" also fits.
41
+ 4. **Ship the fix through the right deploy.**
42
+ - Crontab fix → `worker1.5/ebs/cron.worker.php` + worker1.5 deploy.
43
+ - A `library` fix reaches worker1.5 only on the NEXT worker1.5 deploy: `ebs/setup_git_libraries.php` clones `library`, `_underscore`, `dbchanges`, `resources`, `togadesk` at branch `_production` during each deploy. Merging library alone changes nothing on the box.
44
+ 5. **Recover after crons run again.**
45
+ - Backlog crons (MITS transmit, SR→SO, email send, NetSuite syncs) drain by themselves. Watch the backlog fall; do not assume.
46
+ - Fixed-time daily/weekly jobs do NOT catch up. Re-run each missed one by hand (`php /var/www/html/crons/<path>` on the box) and tell the people who expect it.
47
+
48
+ ## Gotchas
49
+
50
+ - **A script that calls `isProcessRunning()` itself fails half-way.** `crons/notifications/infrastructure/send_emails.php` passes `false` to `cronInitialization()` (so it checks in every run) but calls `isProcessRunning()` again at line 14 and exits. Under the bash shell it checked in every 3 minutes and sent nothing. A fresh `CronJobExecutions` row is not proof the job did its work.
51
+ - **Schedules come from `worker1.5/schedules/cron.worker.json` only.** The old `worker` repo schedule files no longer drive prod.
52
+
53
+ ## Worked example (2026-10-22 16:56 → 2026-10-23 ~10:30 CT, TRUE-83121)
54
+
55
+ TRUE-83078's installer wrote `SHELL=/bin/bash`. 222 of 234 active crons never checked in. Seen: 16 Compass SOs not sent to MITS (oldest 836 min, MITS returned 200 on every call it got, last call 16:10), all Compass 1.0 workflow crons, NetSuite toga2 syncs, Prudential SR→SO (12 REQs), 141 unsent `Common.EmailsQueued` ticket emails (RUMC, TOW, PC Matic, Apiary, Craftex), and NYC DOE "Send DOE Courier Installation Schedule (7:00 AM)" and "Daily Open and Closed DOE Report (1:00 AM)" missed on 2026-10-23. Fixed by dropping the `SHELL` line (worker1.5 PR #8, `ce0f0b2`). Backlogs drained by ~10:52 CT; missed DOE reports needed a manual re-run.
56
+
57
+ ## Change history
58
+ - 2026-10-23 — Created from the worker1.5 cron outage after the single-server move (TRUE-83121, TRUE-83116, TRUE-83138). (bala)
@@ -5,8 +5,8 @@ project: Library
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-10-06
9
- owners: [jcardinal, rgirish, mhammontree, ajean]
8
+ updated: 2026-10-23
9
+ owners: [jcardinal, rgirish, mhammontree, ajean, bala]
10
10
  files: []
11
11
  related:
12
12
  - ../apps/library/architecture.md
@@ -266,7 +266,7 @@ private static $memoryReserve = null;
266
266
 
267
267
  ### Any PHP warning terminates the request — how to opt out locally
268
268
 
269
- `App_Error::handleError()` routes **every** PHP warning into `handleException()`, which renders the error page and calls **`exit()`**. (2.0's equivalent *throws*, which a `catch` can absorb — 1.0 does not.) So a warning raised inside code that must not escalate — error capture, a monitoring ping, an IMDS probe where `file_get_contents` warns before returning `false`, a retry loop — **kills the request or cron mid-run while reporting an unrelated error.**
269
+ `App_Error::handleError()` routes **every** PHP warning into `handleException()`, which renders the error page and calls **`exit()`**. Deprecations are the one exception: since library PR #905 (2026-10-22) they are logged once per call site and the code continues ([PHP 8.5 compatibility](../apps/library/features/php-8-5-runtime-compatibility.md)). (2.0's equivalent *throws*, which a `catch` can absorb — 1.0 does not.) So a warning raised inside code that must not escalate — error capture, a monitoring ping, an IMDS probe where `file_get_contents` warns before returning `false`, a retry loop — **kills the request or cron mid-run while reporting an unrelated error.**
270
270
 
271
271
  Two sanctioned patterns, in order of preference:
272
272
 
@@ -5,15 +5,15 @@ project: Library
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-09-16
9
- owners: [jcardinal, ajean]
8
+ updated: 2026-10-23
9
+ owners: [jcardinal, ajean, bala]
10
10
  files: []
11
11
  related:
12
12
  - ../apps/library/architecture.md
13
13
  - ../apps/worker/architecture.md
14
14
  - backend-php.md
15
15
  ---
16
- 1.0 `App_` framework conventions — class naming, inheritance, model registry, file mapping, dbchanges, PHP 8.x deprecation deaths, controller error handling; open for any 1.0 repo code.
16
+ 1.0 `App_` framework conventions — class naming, inheritance, model registry, file mapping, dbchanges, PHP 8.x deprecations (logged) vs warnings (exit), controller error handling; open for any 1.0 repo code.
17
17
 
18
18
  Applies to all 1.0 repos: `library` (core) and `worker` (app). Loaded on-demand by `/start` when a 1.0 repo is in scope (not always-on) — see rules-scoping in `CONVENTIONS.md`.
19
19
 
@@ -103,16 +103,16 @@ Applies regardless of which app repo the feature lives in. Even a column only `w
103
103
 
104
104
  Never create a `.sql` file inside a 1.0 app repo. The only SQL that belongs there is query strings embedded in PHP — not standalone migration files.
105
105
 
106
- ## PHP 8.x deprecations abort the request (audit before running on 8.5)
106
+ ## PHP 8.x: deprecations are logged, warnings still abort (audit before running on 8.5)
107
107
 
108
- The 1.0 error handler **terminates** on a PHP deprecation notice (`App_Error::handleError` → `handleException` → `exit`; it does not throw a catchable `ErrorException`), so the request/job dies where the deprecation fires. On PHP 8.5 a single deprecated call in a live path is a hard, uncontainable failure — a surrounding `try/catch` will not save it. See the back-end standard's *"Any PHP warning terminates the request — how to opt out locally"* for the two sanctioned local opt-outs.
108
+ Since library PR #905 (TRUE-83055, `cd91686e`, 2026-10-22) `App_Error::handleError` logs `E_DEPRECATED` / `E_USER_DEPRECATED` once per file:line per request (`PHP Deprecated: <msg> in <file>:<line>` via `error_log`) and the code keeps running. Every other notice and warning still goes `handleException` → `exit` (no catchable `ErrorException`, so a `try/catch` will not save it). See the back-end standard's *"Any PHP warning terminates the request — how to opt out locally"*.
109
109
 
110
- - When porting or running 1.0 code on PHP 8.x, **audit for deprecated-in-8.x calls** in every reachable path before deploy.
111
- - Known offender: **`curl_close()`** — deprecated as of PHP 8.5, a no-op since PHP 8.0. Remove all `curl_close()` calls; the cURL handle closes automatically. (Design Demo Admin incident — see framework-rules-cases.md.)
112
- - In `library`, guard deprecated calls by version (`PHP_VERSION_ID`) and use 7.1-valid syntax (`?string $x = null`) — other 1.0 apps still run PHP 7.2. Fixed/open list: [PHP 8.5 compatibility](../apps/library/features/php-8-5-runtime-compatibility.md).
113
- - Main 8.x break classes in 1.0 apps: `null` into internal string functions (`trim`, `base64_decode`, `stripos` — deprecated 8.1) and reading keys of an empty `fetchRow` / `fetchOne` result (warning since 8.0). Cast `(string)` / default the array at the call site; DB/JSON fields can be null.
114
- - When the root cause is a library helper (e.g. `App_Date`, `App_Database`), fix it in library, keeping it PHP 7.2 safe — see [PHP 8.5 compatibility](../apps/library/features/php-8-5-runtime-compatibility.md).
115
- - Do not suppress the deprecation with `@` (see coding-style rules) — remove/replace the deprecated call.
110
+ - Still remove deprecated calls when porting to 8.x: they fill the log and break in a later PHP. Known: **`curl_close()`** (deprecated 8.5, no-op since 8.0).
111
+ - An app deployed with a library build older than PR #905 still exits on a deprecation.
112
+ - In `library`, guard deprecated calls by version (`PHP_VERSION_ID`) and use 7.1-valid syntax (`?string $x = null`): other 1.0 apps still run PHP 7.2. Fixed/open list: [PHP 8.5 compatibility](../apps/library/features/php-8-5-runtime-compatibility.md).
113
+ - Main 8.x break classes in 1.0 apps: reading keys of an empty `fetchRow` / `fetchOne` result (a **warning** since 8.0, still fatal) and `null` into internal string functions (`trim`, `base64_decode`, `stripos`; a deprecation since 8.1, now logged). Default the array / cast `(string)` at the call site; DB/JSON fields can be null.
114
+ - When the root cause is a library helper (e.g. `App_Date`, `App_Database`), fix it in library, keeping it PHP 7.2 safe.
115
+ - Do not suppress with `@` (see coding-style rules); remove or replace the call.
116
116
 
117
117
  ## Error handling in controllers
118
118
 
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-20
9
+ updated: 2026-10-23
10
10
  owners: ["mhammontree", "dfranks", "bala", "jcardinal", "rgirish"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -67,6 +67,7 @@ Do **not** reach for `_Exception_Business` merely to "make sure the rejection is
67
67
  The commit-the-logs / roll-back-the-data contract holds for the **handled** paths — a validation rejection or an exception the controller catches. It did **not** hold for an **uncaught `Error`** observed in production on 2026-08-04 (a `Call to undefined method` raised from an [API payload interceptor](api-payload-interceptors.md) dispatch): the fatal was reported to the error tracker, but **`Logs_Compass.Api` contained zero 5xx rows for the entire day and the failing request appears nowhere in it at all.** Suspected regression in the recent `Error.php` / Issue-Event table normalization work. Practical consequences:
68
68
  - **Do not conclude "the request never happened" from an empty log table.** For a fatal, absence of a log row is not evidence of absence of the request. Check the error tracker's per-event detail — that may be the *only* record.
69
69
  - **Diagnose from the DATA, not the log.** The data transaction rolls back while a *previous* successful request already committed its own rows, so a fatal on a multi-request write sequence leaves a detectable **orphan / zero-child parent**. Sweep for those instead of grepping logs — see [Compass Order Lifecycle & Data-Integrity Invariants](../../../clients/compass-usa/workflows/order-lifecycle-and-data-integrity.md#detecting-empty-shell-orders-left-by-a-fatal-mid-sequence).
70
+ - **A huge response can OOM in the log write itself.** The api log INSERT inlines the whole JSON `responsePayload` (V2.php ~L2379-2391 → `_Model` save ~L345 → `_Query::setQuery()` L430, which makes a full `strtoupper` copy of the SQL). A multi-MB response → HTTP 500 `Allowed memory size of 268435456 bytes exhausted ... in _underscore/Query.php line 430`, with **no** `Logs_<Client>.Api` row and **no** `Logs.Issue`. Only the caller sees it (Prudential TRUE-83138: 1.0 App error "Failed to JSON decode API response (HTTP 500)" in worker1.5 `worker-cron.log`). Usual cause: an over-deep GET — see [V2 query contract § depth re-expands every child collection](v2-rest-query-contract.md).
70
71
  - **Orphan sweeps only find PARTIAL casualties.** An attempt that rolled back completely leaves no row in any table and, with no API log row, no trail at all.
71
72
  - **It applies to plain GETs too, and the response body comes back EMPTY.** Re-confirmed 2026-08-27: a `GET /v2/table-views/meta` that throws inside `_Model_Client_TableView::meta()` (a `TableViewJoins` row pointing at a deleted `Core` record) returns **500 with no body at all** — no `error` code, no `messages` — and `Logs_CompassCanada.Api` holds no row for it. **An empty 500 body plus a missing Api log row is a usable fingerprint** for "a fatal was thrown before the response was assembled". See [tableview-joins](../../_underscore/features/tableview-joins.md).
72
73
 
@@ -233,6 +234,7 @@ Both a **fallback recovery path** and a **scope diagnostic**. In the TRUE-80179
233
234
  - **`Logs_<Client>.Api` is pruned (once TRUE-81902 deploys)** — GET rows after 30 days, writes after 90. A missing older row is not proof the request never happened. See [Logs retention cleanup](../../worker2/features/logs-retention-cleanup.md).
234
235
 
235
236
  ## Change history
237
+ - 2026-10-23 — A multi-MB response OOMs in the log INSERT (`Query.php` L430) and leaves no Api row or Issue (TRUE-83138). (bala)
236
238
  - 2026-10-20 — Gotcha: local `Logs.Api.sourceIp` NOT NULL (blank Core Logs template) vs nullable prod (TRUE-82934). (jcardinal)
237
239
  - 2026-10-16 — Gotchas: delegator/staff logins log under the target `userId` (trace a browser by XFF in `requestHeaders`); `dtStamp` is server-local; Network Errors leave no row. (bala)
238
240
  - 2026-10-16 — Timing gotcha: `dtStamp` = request start; GETs ~1.1s logged + ~1.5–1.9s unlogged between calls. (rgirish)
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-19
9
+ updated: 2026-10-23
10
10
  owners: [tcox, bala, apeterson, jcardinal, rgirish]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -186,6 +186,24 @@ The same route's **list GET was ~1s**, so this is the **write read-back**, not t
186
186
  `api2`/`_underscore` is an **open follow-up** (needs a `cto` review before touching the shared
187
187
  serializer). Lowering caller depth is a mitigation, not the fix.
188
188
 
189
+ ### ⚠ `depth` re-expands EVERY child collection for EVERY row — one fat collection blows the 256 MB limit (OPEN)
190
+
191
+ `getFullModelData()` pulls each inherent child collection **in full** (V2.php ~L7514-7527) for every
192
+ row it returns, at every level. The guard meant to stop re-expanding a parent never fires: it is
193
+ written as `$skipModelsOfInherentChildren[$modelName][$primaryKeyId]` (~L7285, keyed by the row's
194
+ OWN model + id) but read as `[$childModelName][$parentPrimaryKeyId]` (~L7483). So
195
+ `GET /contacts/{uuid}/contact-addresses?depth=3` returns every address, and each address re-expands
196
+ its contact's addresses, emails, phones, vendors, locations and opt-ins again.
197
+
198
+ - Prudential (TRUE-83138, 2026-10-23): contact 54 had **2,530** ContactAddresses → HTTP 500
199
+ `Allowed memory size of 268435456 bytes exhausted ... in _underscore/Query.php line 430`. The
200
+ read itself is many small per-row queries; memory ran out writing the API log row (see
201
+ [request logging](request-logging.md), "uncaught PHP fatal" section).
202
+ - **Caller rule:** on a route whose parent has a large child collection, send `depth: 1` (or 2) and
203
+ only the `fields` you read.
204
+ - **Engine fix is OPEN:** correct the skip-guard key (shared serializer → `cto` review first).
205
+ Raising api2 `memory_limit` (done 2026-10-23) is a stopgap.
206
+
189
207
  ### Measured cost of a list GET — it is per ROW × per NAMED CALCULATED FIELD × per FK hop
190
208
 
191
209
  One `GET /v2/purchase-order-items` over the **same 1,238 rows** (NYCHH, `sandbox-client`), one curl
@@ -658,6 +676,7 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
658
676
  [V2 request logging](request-logging.md).
659
677
 
660
678
  ## Change history
679
+ - 2026-10-23 — `depth` re-expands every child collection per row (skip-guard key mismatch) → 256 MB OOM on a 2,530-address contact (TRUE-83138). (bala)
661
680
  - 2026-10-19 — `limit` on a list GET = 404 `EV-14`, use `recordsPerPage` (TRUE-82915). (jcardinal)
662
681
  - 2026-10-19 — Self-join `where` needs an `ojoin` alias (`Locations@p`); `_` field ACL bypass does not apply cross-client (TRUE-82911). (jcardinal)
663
682
  - 2026-10-18 — Grouped queries: stored root group key now selected + ACL-gated; known gaps (joined key, total count, `ORDER BY uuid` under ONLY_FULL_GROUP_BY) (TRUE-82876). (jcardinal)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-22
9
+ updated: 2026-10-23
10
10
  owners: ["jcardinal", "mhammontree", "bala", "rgirish"]
11
11
  files:
12
12
  - worker2/Worker/Monitor/Compass.php
@@ -96,7 +96,8 @@ Each measures one Compass/Office Depot (or Compass Canada) integration metric an
96
96
  `c_dtTransmittedToCompass` NULL (not yet transmitted to MITS). Alarms on **either** `pendingCount > 10`
97
97
  **OR** `oldestPendingAgeMinutes > 180` (3h), so a single order stuck too long pages even under the
98
98
  count threshold — this catches the 1.0 cron's
99
- [poison-order starvation](../../../clients/compass-usa/features/mits-sales-order-transmission-alerting.md).
99
+ [poison-order starvation](../../../clients/compass-usa/features/mits-sales-order-transmission-alerting.md)
100
+ **and** the cron not running at all (worker1.5 outage 2026-10-22/23, TRUE-83116 — check that first).
100
101
  **Age is measured from the LATER of the two approval decisions — `GREATEST(stage-1, stage-2)
101
102
  dtDecision`** — because an order is eligible to transmit only once **both** stages are approved.
102
103
  Measuring from **stage-2 alone is wrong** (source of a false HIGH): Compass **VIP auto-approve stamps
@@ -623,6 +624,7 @@ production deploy.**
623
624
  - [OneUptime 1.0 worker uptime monitoring](../../../1.0/apps/worker/features/oneuptime-worker-uptime-monitoring.md) — the 1.0 push-heartbeat predecessor
624
625
 
625
626
  ## Change history
627
+ - 2026-10-23 — `CompassSalesOrderTransmission` also fires when the 1.0 transmit cron is not running (worker1.5 outage, TRUE-83116). (bala)
626
628
  - 2026-10-22 — Added `CompassCanadaSalesOrderTransmission` (cron 98); its first two prod runs hit the undefined-method gotcha (row inserted before deploy). (bala)
627
629
  - 2026-10-16 — Added same-class merge-conflict gotcha; linked NYCHH `TransferOrderEmailsOwed` (TRUE-82758). (mhammontree)
628
630
  - 2026-10-15 — Absence rule must exceed the longest gap between pushes for non-`*/5` monitors; linked NYCHH `AssetTagBackfill` (twice daily, 960 min). (mhammontree)
@@ -7,7 +7,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
7
7
  - **library** (Library) _(framework core)_ — 41 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
8
  - **worker** (Worker) — 43 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
- - **worker1.5** (Worker 1.5) — 2 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
10
+ - **worker1.5** (Worker 1.5) — 4 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
11
11
  - **togadesk** (TOGa Desk) — 18 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
12
12
  - **togaview** (TOGa View) — 9 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
13
13
  - **webhook** (Webhook) — 1 doc(s) → [1.0/apps/webhook/INDEX.md](1.0/apps/webhook/INDEX.md)
@@ -5,11 +5,11 @@ project: _Underscore
5
5
  client: compass-usa
6
6
  type: client-feature
7
7
  status: active
8
- updated: 2026-10-22
8
+ updated: 2026-10-23
9
9
  owners: ["dfranks", "bala", "jcardinal"]
10
10
  files:
11
- - worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php
12
- - worker/crons/toga2/compasscanada/workflow/1_transmit_compassCanada_sales_orders_to_mits.php
11
+ - worker1.5/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php
12
+ - worker1.5/crons/toga2/compasscanada/workflow/1_transmit_compassCanada_sales_orders_to_mits.php
13
13
  - worker2/Worker/Infrastructure/Errors.php
14
14
  - tools/mvc/errors/post.php
15
15
  - _underscore/Model/Core/Logs/Issue.php
@@ -25,6 +25,7 @@ related:
25
25
  - ../../../2.0/apps/_underscore/features/error-reporting-issue-event.md
26
26
  - ../../../1.0/apps/library/features/error-capture-1-0.md
27
27
  - ../../../1.0/apps/tools/features/errors-curation-console.md
28
+ - ../../../1.0/apps/worker1.5/workflows/diagnosing-crons-not-running.md
28
29
  ---
29
30
  How a `1_transmit_..._sales_orders_to_mits.php` failure reaches a human — USA rejections route through Logs Issue/Event, Canada still emails in-cron; open for "add someone to MITS error emails," a stuck transmit backlog, or the poison-order starvation.
30
31
 
@@ -49,7 +50,7 @@ Consequence for anyone handed "add <person> to the MITS error emails": the answe
49
50
 
50
51
  So the USA cron is a **hybrid**: its rejection path is issue-routed, but it still holds one hardcoded email for the no-items case. Only the rejection branch was migrated.
51
52
 
52
- Both crons are live — `worker/schedules/cron.worker.sync.json` schedules `toga2/compass/workflow/1_...` and `toga2/compasscanada/workflow/1_...`. (Per the [ODP pipeline doc's](../workflows/odp-order-pipeline-to-netsuite.md) rule, a grep hit under `crons/toga2/**` is not proof a script runs — these two were verified against the schedule.)
53
+ Both crons are live on the single worker1.5 server (since TRUE-83078, 2026-10-22) — `worker1.5/schedules/cron.worker.json` schedules `toga2/compass/workflow/1_...` (`*/5`) and `toga2/compasscanada/workflow/1_...` (`*/10`). The old `worker` repo schedules no longer run in prod. (Per the [ODP pipeline doc's](../workflows/odp-order-pipeline-to-netsuite.md) rule, a grep hit under `crons/toga2/**` is not proof a script runs — these two were verified against the schedule.)
53
54
 
54
55
  **Current recipients on every in-cron email above:** `devteam@togatech.com` + `vburks@togatech.com` (business stakeholder, added 2026-08-31).
55
56
 
@@ -68,11 +69,14 @@ Both crons are live — `worker/schedules/cron.worker.sync.json` schedules `toga
68
69
 
69
70
  Configuration is **never retroactive**: `shouldNotify()` fires on the first occurrence, on escalation, or when the backed-off reminder interval elapses, and returns false for a window with zero events. The first email lands on the **next live rejection**, not on existing history.
70
71
 
72
+ ### First check on a backlog alarm: is the cron running at all?
73
+ Before reading rejections, rule out a dead cron. 2026-10-22/23 (TRUE-83116): the `CompassSalesOrderTransmission` alarm fired (16 pending, oldest 836 min) with **zero** new `Logs.Issue` 24 events and MITS returning 200 on every call — the last `Logs.API` call to MITS was 16:10 the day before. The transmit cron was not running at all: the worker1.5 crontab shell made the overlap guard exit every run. Signs: no new Issue 24 events + stale last `Logs.API` call + `c_dtTransmittedToCompass` not moving. Steps: [Diagnosing worker1.5 crons that stopped running](../../../1.0/apps/worker1.5/workflows/diagnosing-crons-not-running.md).
74
+
71
75
  ### Why the backlog happens — poison-order starvation (root cause)
72
76
  The alert almost never fires on **volume**. It fires because one bad order **aborts the whole run** and every un-processed order behind it starves. Diagnosed 2026-08-31 against a live backlog (0 new, several >24h, one ~20 days old).
73
77
  - The cron loops approved-but-untransmitted orders and `throw`s on any MITS rejection (`App_Exception_Business` on a non-200, plain `Exception` on "No Items Found"). A throw **ends the entire run**, so every order not yet reached that run stays untransmitted.
74
78
  - The driver query uses **`ORDER BY RAND()`**, so *which* orders get processed before the bad one is random each run. A stuck "poison" order therefore blocks a **different** set of good orders every run — good orders can wait days behind one bad one.
75
- - Throughput ceiling: the cron sleeps **30s per order** (86400s time limit), scheduled every 3 min but overlap-guarded, so it is effectively one long-running instance — ~110 orders/hr.
79
+ - Throughput ceiling: the cron sleeps **30s per order** (86400s time limit), scheduled every 5 min but overlap-guarded, so it is effectively one long-running instance — ~110 orders/hr.
76
80
 
77
81
  **Two rejection signatures seen in prod** (Logs.Issue id 24, issueKey `COMPASS_SALES_ORDER_MITS_TRANSMIT_REJECTED`, ~2329 occurrences since 2026-08-03):
78
82
  1. **HTTP 400 `The AssetType field is required`** — a line item's catalog Item has `assetTypeId = NULL`, so the payload `assetType` is null and MITS rejects the **whole order**. This was the active blocker (e.g. item `4U555A-S`; ~10 distinct NULL-assetType items blocked 9 of 11 pending orders).
@@ -102,6 +106,7 @@ The worker2 OneUptime monitor now catches this starvation directly: `CompassSale
102
106
  - As of 2026-08-04 prod `Logs.IssueEmailAddress` is empty, so this is the **first business-routed issue on the platform**: `processBusinessIssue()` will execute in production for the first time when this issue next fires. Watch that run rather than assuming it works.
103
107
 
104
108
  ## Change history
109
+ - 2026-10-23 — Crons now on worker1.5 (`*/5` / `*/10`); backlog alarm can mean the cron is not running at all, check that first (TRUE-83116). (bala)
105
110
  - 2026-10-06 — Order query no longer joins `Users` on `contactId` (duplicate users caused double POST + false 403 `SalesOrder:Exists` alerts); noted `Logs.API` and the intentional kit config line. (bala)
106
111
 
107
112
  ## Related
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-23
10
10
  owners: ["bala"]
11
11
  files:
12
12
  - worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-10-13
9
+ updated: 2026-10-23
10
10
  owners: ["bala"]
11
11
  files:
12
12
  - worker2/Worker/Monitor/Compass.php
@@ -43,12 +43,14 @@ Payload: `status`, `alarm` (`OK`/`DEGRADED`/`HIGH`), `reason`, `minutesSinceLast
43
43
  First live run 2026-10-13: last file 20 min ago, 214 in 24 h, 0 of 20 stuck, `alarm: OK`.
44
44
 
45
45
  ## Gotchas
46
- - **⚠ There is no `status: "error"` push.** No try/catch (developer standard), so an AWS failure (role, CloudWatch, S3) throws, nothing is pushed, and OneUptime's **"not received in 15 min"** criterion turns it red. A red monitor with no recent body = the checker died, not a backlog; read the worker2 `WorkerJobs` row for the exception.
46
+ - **⚠ There is no `status: "error"` push.** No try/catch (developer standard), so an AWS failure (role, CloudWatch, S3) throws, nothing is pushed, and OneUptime's **"not received in 15 min"** criterion turns it red. A red monitor with no recent body = the checker died, not a backlog; read prod `Core.WorkerJobs.output` for the run output or exception.
47
+ - **It also catches "1.0 crons not running".** The mover is a worker1.5 cron, so a stopped cron host shows as files stuck in `processed/`. Alarmed correctly on 2026-10-22/23 when worker1.5's crontab shell stopped most crons (TRUE-83121, [pipeline gotcha](../workflows/odp-order-pipeline-to-netsuite.md)).
47
48
  - **Not covered:** weekend and holiday silence never alarms; only the 20 newest files are checked, so an old file stuck behind 20 good ones is missed.
48
49
  - Reads the **prod** agreement stream only. The OfficeDepotTest agreement also writes to `/agilant-as2` and is ignored.
49
50
  - The OneUptime import JSON is **not** in git. It was removed from the PR before merge; import JSONs are handed over outside the repo ([runbook](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md#provisioning-a-monitor-runbook)).
50
51
 
51
52
  ## Change history
53
+ - 2026-10-23 — Confirmed it catches a full 1.0 cron stop (worker1.5, TRUE-83121); run output is in `Core.WorkerJobs.output`. (bala)
52
54
  - 2026-10-13 — Created: CloudWatch AS2-Inbound + S3 existence check, business-hours silence alarm; added after the 2026-10-10 Lambda outage left `OfficeDepotEdiImportQueue` green (TRUE-82728). (bala)
53
55
 
54
56
  ## Related
@@ -15,7 +15,6 @@ apps:
15
15
  - worker1.5
16
16
  - worker2
17
17
  - dbchanges2
18
- - library
19
18
  - test
20
19
  - talos-backend
21
20
  - tools
@@ -23,7 +22,7 @@ project: _Underscore
23
22
  client: compass-usa
24
23
  type: profile
25
24
  status: active
26
- updated: 2026-10-22
25
+ updated: 2026-10-23
27
26
  owners: [jcardinal, bala, tcox, apeterson, dfranks, akhokhani, ajean, rgirish]
28
27
  files: []
29
28
  related:
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: compass-usa
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-10-13
9
+ updated: 2026-10-23
10
10
  owners: ["dfranks", "bala"]
11
11
  files:
12
12
  - worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php
@@ -21,6 +21,7 @@ related:
21
21
  - ../../../2.0/apps/api2/features/request-logging.md
22
22
  - clients/compass-usa/profile.md
23
23
  - clients/compass-usa/features/oneuptime-office-depot-inbound-files-monitor.md
24
+ - ../../../1.0/apps/worker1.5/workflows/diagnosing-crons-not-running.md
24
25
  ---
25
26
  Runbook to recover Compass ODP EDI 850 imports: re-drop an x12 that cron 3a deleted after a failure, move AS2 files stuck in `processed/` when nothing reaches 3a, or build the order by SQL when 3a cannot run; open when ODP orders are missing in Supply or never reached NetSuite.
26
27
 
@@ -82,7 +83,19 @@ The files reach S3 but never get moved to `OfficeDepot/`, so 3a has nothing to i
82
83
  3. The AS2 inbound log group (`AS2-Inbound…`) — confirms ODP is still sending.
83
84
  4. If re-saving the S3 event notification fails with `Unable to validate the following destination configurations`, S3 can no longer invoke the Lambda (Lambda suspended or its permission gone).
84
85
 
85
- **Recover:** move the stuck `processed/*.net` files, **filtered by date window**, to the root of `OfficeDepot/` with the same name. Never move the 2024 test files in `processed/`. 3a skips POs that already exist, so replaying a file that was already imported is safe. The stop-gap cron `move_office_depot_as2_files_to_import_folder.php` does this every 5 minutes (live since 2026-10-12; the Lambda is still suspended as of 2026-10-13); turn it off once AWS restores the Lambda.
86
+ **Recover:** move the stuck `processed/*.net` files, **filtered by date window**, to the root of `OfficeDepot/` with the same name. Never move the 2024 test files in `processed/`. 3a skips POs that already exist, so replaying a file that was already imported is safe. The stop-gap cron `move_office_depot_as2_files_to_import_folder.php` does this every 5 minutes (live since 2026-10-12, on worker1.5 since 2026-10-22; the Lambda is still broken as of 2026-10-23); turn it off once AWS restores the Lambda.
87
+
88
+ **If the mover itself has no `Common.CronJobExecutions` rows**, the crons are not running at all (2026-10-22/23: worker1.5 crontab shell). Follow [Diagnosing worker1.5 crons that stopped running](../../../1.0/apps/worker1.5/workflows/diagnosing-crons-not-running.md); the backlog clears itself once 3a and the mover run.
89
+
90
+ ### After a long cron stop — deadlock rollback (PO missing, SO line present)
91
+ When every cron restarts at once, 3a's PO save can deadlock (MySQL **1213**) with cron 2 (`2_transmit_mits_purchase_orders_to_vendors.php`) catching up its backlog. The PO is rolled back, but the file is still logged in `Logs.FileLog` and deleted from S3. Result: an ODP SalesOrder **with its line but no ODP PO**.
92
+
93
+ Recover with Steps 1-2: rebuild the 850 from `Logs.FileLog.fileData` and drop it in the root of `s3://agilant-as2/OfficeDepot/`. It is safe:
94
+ - 3a reuses the ODP SO by number + `customerId`, and matches the existing SO line by part number (no duplicate line).
95
+ - The over-quantity guard does not count the rolled-back PO.
96
+ - A second run of the same file skips, because the PO now exists.
97
+
98
+ Verified 2026-10-23: ODP PO `42278391-5910`, ODP SO `486472538001` (id 128209), `Logs.FileLog` id 905447 → PO id 119085 created, linked to SO 128209, accept 855 sent.
86
99
 
87
100
  ### Last resort — manual import by SQL (3a cannot run)
88
101
  Prefer re-dropping the file (Steps 1-4): 3a also writes the `Logs_Compass` `Api` / `Record` / `RecordField` audit rows and an SQS event, which plain SQL does not. If the order must exist now, write exactly what 3a's api2 calls write for one SA/MR 850.
@@ -114,6 +127,7 @@ After the SQL import:
114
127
  - A **fully** rolled-back import leaves no row anywhere and no API log row, so it will not show up in an orphan sweep at all; only the error tracker's per-event detail can enumerate those.
115
128
 
116
129
  ## Change history
130
+ - 2026-10-23 — Added the deadlock-rollback case after the worker1.5 cron stop (PO 42278391-5910 replayed from FileLog) and the "mover has no CronJobExecutions rows" check (TRUE-83121). (bala)
117
131
  - 2026-10-13 — Added the manual-SQL-import section (exact rows 3a's api2 calls write, hooks that do not run for apiId 1); linked the inbound files monitor; Lambda still suspended, stop-gap cron live. (bala)
118
132
  - 2026-10-12 — Added the "AS2 files stuck in `processed/`" case (Lambda `rename_edi_files` stopped 2026-10-10, 52 POs); corrected the inbound S3 filename to `<uuid>.<msgid>.net`. (bala)
119
133
 
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: compass-usa
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-10-21
9
+ updated: 2026-10-23
10
10
  owners: ["rgirish", "bala", "dfranks", "jcardinal", "mhammontree"]
11
11
  files:
12
12
  - worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php
@@ -36,12 +36,16 @@ related:
36
36
  - 1.0/apps/library/features/app-cloud-aws-credentials.md
37
37
  - clients/compass-usa/features/oneuptime-office-depot-inbound-files-monitor.md
38
38
  - 2.0/apps/dbchanges2/features/netsuite-id-unique-keys.md
39
+ - 1.0/apps/worker1.5/workflows/diagnosing-crons-not-running.md
40
+ - 1.0/apps/worker1.5/architecture.md
39
41
  ---
40
42
  The end-to-end Compass ODP order → NetSuite pipeline through the numbered 1.0 worker crons, plus the "this order never reached NetSuite" diagnostic method; open for any ODP-to-NetSuite tracing or cron-mechanics question.
41
43
 
42
44
  ## Summary
43
45
  The end-to-end **Compass Office Depot (ODP) order → NetSuite** pipeline as it actually runs through the 1.0 `worker` crons under `worker/crons/toga2/compass/`, plus the **diagnostic method** for "this Compass order never reached NetSuite" tickets. The *cron mechanics* companion to the 2.0 [Compass Order Lifecycle & Data-Integrity Invariants](order-lifecycle-and-data-integrity.md) (raw-data shape / repair side). All facts verified against the live `prod` / `Client_Compass` schema and the cron source. The crons are `App_` (1.0) framework code; the data lives in the 2.0 `Client_Compass` / `Logs_Compass` schema.
44
46
 
47
+ **Since 2026-10-22 (TRUE-83078) these crons run on [worker1.5](../../../1.0/apps/worker1.5/architecture.md)**, same `crons/toga2/compass/...` paths; the schedule is `worker1.5/schedules/cron.worker.json` (not `worker/schedules/cron.worker.sync.json`). The old `worker` fleet stopped 2026-10-22 16:56 Central.
48
+
45
49
  The pipeline (customerId / vendorId, in run order):
46
50
  ```
47
51
  Commerce → Compass SalesOrder (SalesOrders.customerId = 2)
@@ -63,7 +67,7 @@ ODP SalesOrder (customerId = 1)
63
67
  1. **`workflow/1_transmit_compass_sales_orders_to_mits.php`** — transmits the Compass `SalesOrder` (customerId=2) to MITS via API.
64
68
  2. **`workflow/2_transmit_mits_purchase_orders_to_vendors.php`** — transmits the MITS-created PO to ODP. ODP integration is **cXML** (auth audience `"Office Depot (Cxml)"`); also supports an **email fallback** path per the vendor's integration type. Sets `PurchaseOrders.dtSubmitted` when transmitted (~2 min after PO creation). Item-less-PO gotcha + CXML-vs-EMAIL routing detail: [MITS PO Transmission to Vendors](../features/mits-po-transmission-to-vendors.md).
65
69
  3. **`workflow/3a_import_office_depot_purchase_orders.php`** — pulls the ODP **850** from S3 bucket `agilant-as2`, prefix `OfficeDepot/` (excluding `OUTBOX/` and `SENT/`; `:46` S3 read, `:54-58` prefix filters) and creates the downstream **ODP SalesOrder** (customerId=1 = Office Depot). **Deletes the S3 object after processing** — so an absent S3 object is expected once ingested, not evidence of a miss.
66
- - **⚠ Corrected 2026-08-28 — the delete is now CONDITIONAL.** 3a used to delete **unconditionally**, so a failed import left nothing to retry and **silently destroyed the order**. It now deletes only when **every PO in the file imported end to end**; otherwise the file stays in `OfficeDepot/` and the next `*/5` run retries it. Which failures count, the reject-855 / error-email / file-log guards, and the retries-forever limitation: [ODP EDI File Retention & Import Retry](../features/odp-edi-file-retention-and-retry.md). **Not deployed as of 2026-08-28** — production still deletes unconditionally, so treat any pre-deploy incident as a lost file.
70
+ - **⚠ Corrected 2026-08-28 — the delete is now CONDITIONAL.** 3a used to delete **unconditionally**, so a failed import left nothing to retry and **silently destroyed the order**. It now deletes only when **every PO in the file imported end to end**; otherwise the file stays in `OfficeDepot/` and the next `*/5` run retries it. Which failures count, the reject-855 / error-email / file-log guards, and the retries-forever limitation: [ODP EDI File Retention & Import Retry](../features/odp-edi-file-retention-and-retry.md). **Still not deployed:** worker1.5 `_main` 3a deletes unconditionally (verified 2026-10-23), so treat any failed import as a lost file.
67
71
  4. **`workflow/4_transmit_office_depot_po_acknowledgements.php`** (every 15 min) sends the **accept** X12 **855** for imported ODP `PurchaseOrders` with `dtAcknowledged IS NULL`, then stamps `dtAcknowledged`. It delegates 855 construction to `App_Edi::buildOfficeDepotPurchaseOrderAcknowledgement855()` (accept mode). A **reject** 855 is instead sent **inline from cron 3a** when the over-quantity guard rejects a PO (a rejected PO is never persisted, so cron 4 can never see it). Cron 3a also runs the over-quantity guard (`officeDepotPurchaseOrderExceedsCompassDemand()`) after the same-PO-number de-dupe and before any SO/PO creation, rejecting a whole PO when ODP re-sends an already-processed Compass SO line under a **new** PO number. See [ODP EDI 855 Acknowledgement + Over-Quantity Guard](../features/odp-edi-855-acknowledgement-and-overquantity-guard.md).
68
72
  5. **`workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php`** — scheduled **every 10 minutes** (`*/10`, verified against `worker/schedules/` on 2026-08-04; this doc previously said "hourly"); picks up ODP SalesOrders (`OfficeDepotSalesOrders.customerId = 1 AND c_dtTransmittedToNetsuite IS NULL`), creates the SO in NetSuite, and writes back `c_dtTransmittedToNetsuite` + `c_netsuiteInternalSalesOrderId` **onto the ODP SalesOrder (customerId=1), never the Compass SO (customerId=2)**. Before sending it checks NetSuite for the order by `externalId` so a lost stamp never creates a duplicate: [duplicate-proof send](#cron-5-duplicate-proof-send--externalid--lookup). Exclusion filters (an order legitimately waiting is not a bug):
69
73
  - `CompassSalesOrders.number NOT LIKE 'MA%'` — MA orders excluded. **They are keyed into NetSuite by hand** (~20 min each), and the worker2 `Monitor/Compass/OfficeDepotNetsuiteIntegration` monitor carries the **same** `NOT LIKE 'MA%'` clause — so MA orders are invisible to both. Coverage is [cron 7's exception report](../../../1.0/apps/worker/features/compass-ma-sales-order-exception-report.md) plus, since 2026-09-08, the dedicated [MA refresh-order monitor](../features/oneuptime-ma-refresh-order-monitor.md).
@@ -83,7 +87,7 @@ AWS account toga-legacy (`502614707982`), `us-west-2`:
83
87
  - **Not in the live path:** Transfer Family workflow `w-f8951fd4252a5b890` "OfficeDepot" (a COPY step to `OfficeDepot/`) is attached to the server OnUpload but has **never run** (no executions in a year of server logs). Do not debug it.
84
88
  - Agreement `a-f1a87e1edc5c442fa` "OfficeDepotTest" also writes to `/agilant-as2`. Canon AS2 uses a separate bucket, `agilant-canon-as2`.
85
89
  - `processed/` also holds old **2024 test `.net` files**. Never move them; any bulk move must filter by date.
86
- - **Stop-gap cron (2026-10-12):** `workflow/move_office_depot_as2_files_to_import_folder.php` (`*/5`, scheduled right after 3a) does the Lambda's job: moves `*.net` from `processed/` to `OfficeDepot/`, only files modified after `2026-10-10 12:30 UTC` and older than 120 s, and throws if the S3 list returns `null`. Committed on `worker` `_production` (`b3f57866`) and live; it authenticates through the `WorkloadsRuntime` role ([AWS credentials](../../../1.0/apps/library/features/app-cloud-aws-credentials.md)), first clean run 2026-10-12 17:00 CDT after the library deploy. **The Lambda is still suspended by AWS as of 2026-10-13** (last log 2026-10-10 12:29 UTC). **Turn the cron off (`"active": 0`) once AWS restores the Lambda.**
90
+ - **Stop-gap cron (2026-10-12):** `workflow/move_office_depot_as2_files_to_import_folder.php` (`*/5`, scheduled right after 3a) does the Lambda's job: moves `*.net` from `processed/` to `OfficeDepot/`, only files modified after `2026-10-10 12:30 UTC` and older than 120 s, and throws if the S3 list returns `null`. Committed on `worker` `_production` (`b3f57866`) and live; it authenticates through the `WorkloadsRuntime` role ([AWS credentials](../../../1.0/apps/library/features/app-cloud-aws-credentials.md)), first clean run 2026-10-12 17:00 CDT after the library deploy. **The Lambda is still broken as of 2026-10-23** (last log 2026-10-10 12:29 UTC); the mover now runs from worker1.5. **Turn the cron off (`"active": 0`) once AWS restores the Lambda.**
87
91
  - **Monitored by** [`Monitor/Compass/OfficeDepotInboundFiles`](../features/oneuptime-office-depot-inbound-files-monitor.md) (cron 93): alarms when a received file is not moved out of `processed/`, or on business-hours silence. `OfficeDepotEdiImportQueue` cannot see this step.
88
92
  - Diagnosing and recovering stuck files: [Recovering a lost ODP EDI import](odp-edi-import-recovery.md#other-case--as2-files-stuck-in-processed-3as-inbox-is-empty).
89
93
 
@@ -197,6 +201,7 @@ The NetSuite writeback lands on the **downstream ODP SalesOrder (customerId=1)**
197
201
  - **⚠ `Client_Compass.SalesOrders.c_netsuiteInternalSalesOrderId` can never be UNIQUE.** The Compass order and its Office Depot order share one NetSuite SO id by design (this workflow's cron 5 stamp and the NetSuite SO sync write the same id). Every NetSuite-id key migration skips this column on Compass — see [NetSuite id UNIQUE keys](../../../2.0/apps/dbchanges2/features/netsuite-id-unique-keys.md). Do not dedupe Compass SOs by NetSuite id.
198
202
  - **⚠ Cron 3a linked every Compass PO line to the LAST ODP SO line (2026-04-28 → 2026-10-06; fixed `worker` `02cc2473`).** Commit `afb520c1` left a scalar `$officeDepotSalesOrderItemUuid` that the customer-PO link loop reused, so all `PurchaseOrderItems_SalesOrderItems` rows for a PO pointed at the last ODP SO line. The recursive IF walk then broke at the ODP→Compass tier and Compass orders got no fulfillment or tracking (proven on SA130861 / SA130762). Fix: link via `$lookupSalesOrderItemUuidFromPurchaseOrderLine[lineNumber]`; repeated vendor part numbers pair one-to-one (`array_shift` on the lookup). Zero bad links on ~440 orders since 2026-10-07. History is repaired by [retrofix2](../../../1.0/apps/test/features/compass-retrofix2-sql-generator.md) (bulk) or [fix_item_fulfillment_chain](../../../1.0/apps/test/features/compass-fix-item-fulfillment-chain.md) (one order). **Since 3a resolves 850 lines through ODP `VendorItems`, the ODP SO line is often the catalog-2 twin of the Compass item — match these tiers by part number, not `itemId`.**
199
203
  - **⚠ 2026-10-10 → 10-12: ODP POs stopped arriving because the Lambda stopped, not 3a.** An attacker used a leaked long-lived IAM key; AWS suspended services including Lambda, so `rename_edi_files` stopped at **07:29 CDT Sat 2026-10-10** and **52 POs** piled up in `processed/`. 67 `.net` files (850s + 997s) were moved by hand on 2026-10-12; 3a created all 52 POs (0 rejects) and cron 5 sent them to NetSuite. The leaked key was deleted the same day; 3a and the move cron now run on the `WorkloadsRuntime` role ([AWS credentials](../../../1.0/apps/library/features/app-cloud-aws-credentials.md)). All stuck POs imported. The `OfficeDepot/` backlog monitor stayed green throughout (its folder was empty); the [inbound files monitor](../features/oneuptime-office-depot-inbound-files-monitor.md) now covers that gap. Symptom of a dead mover: files pile up in `processed/`, `OfficeDepot/` stays empty, no new ODP SalesOrders.
204
+ - **⚠ 2026-10-22 16:45 → 10-23 10:40 Central: no ODP 850 imported because worker1.5 crons were not running (TRUE-83121).** worker1.5's crontab set `SHELL=/bin/bash`, so the already-running guard stopped 3a, the mover, 5 and most other crons before check-in ([diagnosis steps](../../../1.0/apps/worker1.5/workflows/diagnosing-crons-not-running.md)). The [inbound files monitor](../features/oneuptime-office-depot-inbound-files-monitor.md) alarmed correctly (files stuck in `processed/`). Restarting every cron at once caused one 3a/cron 2 deadlock; recovery in [ODP EDI import recovery](odp-edi-import-recovery.md#after-a-long-cron-stop--deadlock-rollback-po-missing-so-line-present).
200
205
  - **⚠ `edi/1_download_edi_s3_create_po_toga.php` is DEAD CODE (correction 2026-08-04).** It appears in **no** schedule file. The scheduled entry *named* "Download EDI From S3 Files & Create PO - EDI Office Depot" (`*/5 * * * *`, `worker/schedules/cron.worker.sync.json` `:254-259`) actually points at **`toga2/compass/workflow/3a_import_office_depot_purchase_orders.php`**. The two scripts have **different idempotency and different API call shapes** — `edi/1`'s create branch POSTs the SO with *nested* items and a `customer.name`, while **3a POSTs the header alone (uuids) and then POSTs the items in a separate request**. Reasoning from `edi/1` gives wrong answers. **General rule: never infer which script runs from its filename or folder** — verify against `worker/schedules/cron.*.json`, and note the schedule entry's **name can be actively misleading**. The folders `compass/edi/`, `compass/DOA_edi_odp_flow/`, and `compass/DOA_Scripts/` are **legacy / inactive**; `compass/edi/3_send_edi_855(reject).php` is an abandoned sketch using **non-standard** reject codes (`BAK01=A1` / `BAK02=RE`) — **do not** follow it.
201
206
  - **⚠ The `OfficeDepot/` listing makes 3a runs overlap (NOT fixed).** An instrumented prod run 2026-08-25 showed the S3 listing returning **138,472 objects** and taking **32 seconds** before any PO was processed, each PO then ~40 seconds. A file-bearing run blows past the `*/5` schedule and the following runs are **silently skipped** by `App_Framework::exitIfProcessRunning()` — leaving **no `CronJobExecutions` row at all**, which reads as "the cron never fired". The object count is driven by `OfficeDepot/SENT/` and `OfficeDepot/OUTBOX/` accumulating: 3a skips those prefixes when *processing* but still *lists* them. Fix candidates (separate work): prune `SENT/`, or list with a tighter prefix. See [Tracing a worker cron run in production](../../1.0/apps/worker/workflows/tracing-a-worker-cron-run-in-production.md).
202
207
  - **⚠ `REF~QC` — not `BEG` — is the value to search on for the ODP SalesOrder.** Searching `PurchaseOrders` / `SalesOrders` for the `BEG` number finds nothing for the SO and wastes time.
@@ -233,6 +238,7 @@ The NetSuite writeback lands on the **downstream ODP SalesOrder (customerId=1)**
233
238
  - **Editing cron recipient lists — a grep hit is not a live hit.** Email recipients are hardcoded in the cron scripts, and **several dormant copies of the same script exist** under `crons/toga2/compass/edi/` and `crons/toga2/compass/DOA_edi_odp_flow/`. Before changing a recipient list, cross-check the file against `worker/schedules/*.json` for a referencing entry whose `active` is not `0`. Only the scheduled copies matter.
234
239
 
235
240
  ## Change history
241
+ - 2026-10-23 — Crons now run on worker1.5 (schedule `cron.worker.json`); 2026-10-22/23 cron-stop outage; retention fix still not deployed; Lambda still broken (TRUE-83121). (bala)
236
242
  - 2026-10-21 — Gotcha: Compass SalesOrders NetSuite SO id is never UNIQUE (TRUE-82291 skips it). (rgirish)
237
243
  - 2026-10-15 — Cron 3a last-ODP-line link bug (since `afb520c1`) root-caused and fixed in `02cc2473`; re-drop does not repair a wrong link. (jcardinal)
238
244
  - 2026-10-14 — TRUE-82797 (pending deploy): cron 5 merged only one ODP SO per Compass SO, so siblings went out as a 2nd NetSuite SO (41 Compass SOs in 60 days). Fix merges all ODP SOs of a Compass SO, splits tablets by `TABLET_KIT_PART_NUMBERS`, externalId = lowest ODP SO id, duplicate-part guard emails devteam. Decided against category grouping. (rgirish)
@@ -3,6 +3,7 @@ title: New York City Department of Education
3
3
  framework: "1.0"
4
4
  apps:
5
5
  - worker
6
+ - worker1.5
6
7
  - library
7
8
  - togadesk
8
9
  - dbchanges
@@ -13,7 +14,7 @@ project: Worker
13
14
  client: nycdoe
14
15
  type: profile
15
16
  status: active
16
- updated: 2026-10-19
17
+ updated: 2026-10-23
17
18
  owners: [mhammontree, sking, bala]
18
19
  files: []
19
20
  related:
@@ -26,7 +27,7 @@ related:
26
27
  NYC DOE client profile (1.0 worker tier live; 2.0 ASN rewrite in progress), ServiceNow + EDI; open for DOE's platform scope, endpoints, API clients, and where its ticket/ASN/EDI logic lives.
27
28
 
28
29
  ## Summary
29
- NYC DOE — TOGA client whose **live** integration runs in the **1.0 worker tier** (~30 cron scripts under `worker/crons/sync/nycdoe/` and `worker/crons/notifications/nycdoe/`, on the `sync` and `notification` worker roles). ServiceNow is DOE's system of record for tickets (Incidents and RITMs); TOGA mirrors those locally, drives fulfillment through TOGa Desk and NetSuite, and pushes status / ETA / proof-of-delivery back. A parallel EDI relationship (850 POs in, invoices out) runs over DOE's SFTP. A **2.0 rewrite (TRUE-80519)** of the ASN intake is being built against the `Client_Nycdoe` tenant — not yet live.
30
+ NYC DOE — TOGA client whose **live** integration runs in the **1.0 worker tier** (~30 cron scripts under `crons/sync/nycdoe/` and `crons/notifications/nycdoe/`; since 2026-10-22 they run from `worker1.5` on one server, schedule `worker1.5/schedules/cron.worker.json`). Fixed-time DOE reports missed during a cron outage do not catch up; re-run them by hand (see [Diagnosing worker1.5 crons that stopped running](../../1.0/apps/worker1.5/workflows/diagnosing-crons-not-running.md)). ServiceNow is DOE's system of record for tickets (Incidents and RITMs); TOGA mirrors those locally, drives fulfillment through TOGa Desk and NetSuite, and pushes status / ETA / proof-of-delivery back. A parallel EDI relationship (850 POs in, invoices out) runs over DOE's SFTP. A **2.0 rewrite (TRUE-80519)** of the ASN intake is being built against the `Client_Nycdoe` tenant — not yet live.
30
31
 
31
32
  ## Platforms & data
32
33
  - **1.0 worker tier** — all live business logic. Local mirror tables in `db_common` (`NYCDOETickets`, `NYCDOELocations`, `AdvanceShippingNotice*`).
@@ -7,6 +7,7 @@ apps:
7
7
  - dbchanges2
8
8
  - websocket
9
9
  - worker
10
+ - worker1.5
10
11
  - worker2
11
12
  - library
12
13
  - toga2-commerce
@@ -17,7 +18,7 @@ project: _Underscore
17
18
  client: prudential
18
19
  type: profile
19
20
  status: active
20
- updated: 2026-10-22
21
+ updated: 2026-10-23
21
22
  owners: ["jcardinal", "rgirish", "bala", "mhammontree"]
22
23
  files: []
23
24
  related:
@@ -40,8 +41,8 @@ related:
40
41
  ## Summary
41
42
  Prudential is a TOGA client whose device-fulfillment flow is driven by **Dell** via the Dell
42
43
  API (`Client_Prudential.Apis.id = 2`). Dell posts Advance Shipping Notices (with tracking and
43
- return-tracking per unit) into the 2.0 platform (DB `Client_Prudential`); the 1.0 worker tier
44
- (`crons/toga2/prudential/`) handles NetSuite sync, ServiceNow (RITM) close/ship updates, and
44
+ return-tracking per unit) into the 2.0 platform (DB `Client_Prudential`); the 1.0 cron tier (since 2026-10-22 the single
45
+ worker1.5 server, `worker1.5/crons/toga2/prudential/`) handles SR → SO/PO generation, NetSuite sync, ServiceNow (RITM) close/ship updates, and
45
46
  order-status transmissions.
46
47
 
47
48
  ## Integrations
@@ -119,11 +120,21 @@ The order-created cron branches on this id; the shipped/delivered crons branch o
119
120
  features/service-request-auth-rejection-alert.md.
120
121
 
121
122
  - **Daily "Prudential REQ's Processing Report" recipients are hardcoded.** Sent by 1.0
122
- `worker/crons/notifications/reports/prudential_exception_report.php` (NOT the sibling
123
- `reports/prudential/` folder), scheduled in `worker/schedules/cron.worker.notification.json` as
123
+ `worker1.5/crons/notifications/reports/prudential_exception_report.php` (NOT the sibling
124
+ `reports/prudential/` folder), scheduled in `worker1.5/schedules/cron.worker.json` as
124
125
  "Prudential Exception Report (9:00 AM)". Recipients = the `$emailTo` PHP array near the top of the
125
126
  file (a hardcoded list of togatech.com addresses, all TO, no CC); no DB table or setting. To add or
126
- remove someone: edit that array, deploy worker `_production` (CodePipeline `Worker`). (TRUE-82925)
127
+ remove someone: edit that array, deploy worker1.5. (TRUE-82925)
128
+
129
+ - **⚠ The SR → SO cron piles up ContactAddresses (OPEN, TRUE-83138).**
130
+ `worker1.5/crons/toga2/prudential/generate_sales_and_purchase_orders_from_service_requests.php`
131
+ (~L182-249) GETs `/contacts/{uuid}/contact-addresses?depth=3`, and when the REQ address differs from
132
+ the first saved one it POSTs a NEW `/contact-addresses` and PUTs the contact's
133
+ `primaryContactAddress` — every run, never reusing a match. Prod 2026-10-23: 15,379 rows for ~20.7k
134
+ contacts; contact 54 had 2,530. That GET then ran api2 out of memory (500, no Api log row; see
135
+ [V2 query contract § depth](../../2.0/apps/api2/features/v2-rest-query-contract.md)) and SRs stopped
136
+ becoming SOs. api2 memory was raised as a stopgap. Open: reuse a matching ContactAddress, and send
137
+ a lower `depth` on that GET.
127
138
 
128
139
  - **SR number race (TRUE-82209: ServiceNow bursts → 1062 `Duplicate entry 'SR…'` → 400 `EV-10`, SR lost).** Fixed by
129
140
  per-client record numbers from `RecordNumbers` (TRUE-82715; SR series continues); the `usleep` stopgap in `_Model_Prudential_ServiceRequest::prePost()`
@@ -29,12 +29,13 @@ apps:
29
29
  - test
30
30
  - togaview
31
31
  - toga
32
+ - worker1.5
32
33
  project: _Underscore
33
34
  client: true
34
35
  type: profile
35
36
  status: active
36
- updated: 2026-10-22
37
- owners: [jcardinal, kyalamarthi, tcox, rgirish, mhammontree, akhokhani]
37
+ updated: 2026-10-23
38
+ owners: [jcardinal, kyalamarthi, tcox, rgirish, mhammontree, akhokhani, bala]
38
39
  files: []
39
40
  related:
40
41
  - features/toga-hd-voice-demo.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1126",
3
+ "version": "1.0.1127",
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",