toga-ai 1.0.813 → 1.0.814
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-15
|
|
10
10
|
owners: [jcardinal, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Controller/Index.php
|
|
@@ -213,6 +213,16 @@ file `Worker/Team/Github.php`.
|
|
|
213
213
|
`_Worker_Infrastructure_Worker_Cleanup`: `WorkerJobs()` deletes `isSuccess=1` rows >90 days
|
|
214
214
|
(≤1000/run); `WebhookLogs()` deletes `Logs.Webhook` >90 days. Two daily CronJobs at 2 AM.
|
|
215
215
|
|
|
216
|
+
**⚠ The OBSERVED `Core.WorkerJobs` retention is ~30 days, not 90 — unexplained.** Measured
|
|
217
|
+
2026-09-15 in prod: `MIN(dtCreated)` across the **whole** table is **2026-08-16 02:00:46**
|
|
218
|
+
(790,597 rows). Either the cron does not behave as described above, or something else prunes the
|
|
219
|
+
table. **Not investigated** — no cause is asserted here.
|
|
220
|
+
|
|
221
|
+
**Triage consequence: a missing `WorkerJobs` row is NOT proof the job never ran.** Any "there is no
|
|
222
|
+
job for this record, so the webhook never fired" conclusion is invalid past the ~30-day floor. That
|
|
223
|
+
exact wrong call was made and retracted on 2026-09-15. Same failure family as *an empty log table is
|
|
224
|
+
not proof a code path never ran*.
|
|
225
|
+
|
|
216
226
|
## Cron actions must return a result on BOTH paths
|
|
217
227
|
|
|
218
228
|
**A new cron action is expected to return a result for a success as well as a failure.** Both
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-15
|
|
10
10
|
owners: ["ajean"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Netsuite/Opportunity.php
|
|
@@ -143,6 +143,25 @@ legitimately be running before the column lands. An **ungated write does not deg
|
|
|
143
143
|
breaks the ENTIRE opportunity sync**, because `importOpportunity()`'s save names a column that does
|
|
144
144
|
not exist and *every* NetSuite webhook fails. A label enhancement must not be able to do that.
|
|
145
145
|
|
|
146
|
+
**⚠ CORRECTED 2026-09-15 — this gate did not prevent that outcome; it happened anyway, for 9 days.**
|
|
147
|
+
The predicted failure ("it breaks the ENTIRE opportunity sync") occurred 2026-09-01 → 2026-09-10
|
|
148
|
+
with the gate in place, because the gate checks the **DB column** and the failing write went through
|
|
149
|
+
the **ORM**: the column existed, but `_underscore`'s `_Model_Forecast_Opportunity` had not yet
|
|
150
|
+
declared the `endCustomerName` property, so `_Model` rejected the field. **The deploy has two
|
|
151
|
+
independent halves and they ship from different repos** — the dbchanges2 migration *and* the
|
|
152
|
+
`_underscore` model declaration. General rule: **gate on the layer the write actually goes through,
|
|
153
|
+
not the layer that is easiest to query.**
|
|
154
|
+
|
|
155
|
+
So there are now **two** gates (worker2 PR #168, open against `_production` as of 2026-09-15):
|
|
156
|
+
|
|
157
|
+
| Gate | Checks | Use for |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `hasEndCustomerColumn()` (unchanged) | `information_schema` column | **raw `_Query` SQL only** — the digest + both backfills |
|
|
160
|
+
| `canWriteEndCustomerNameToModel()` (new) | the above **and** `property_exists(_Model_Forecast_Opportunity::class,'endCustomerName')` | **`importOpportunity()`'s ORM write only** |
|
|
161
|
+
|
|
162
|
+
Each half `error_log`s on its own, naming the repo that is behind. Full incident detail:
|
|
163
|
+
[the outage gotcha on the sync doc](./netsuite-opportunity-sync.md#gotchas--known-issues).
|
|
164
|
+
|
|
146
165
|
- `information_schema.COLUMNS` check, **memoised per process** in a `static` (the answer cannot
|
|
147
166
|
change mid-job, and the import runs per webhook) — `static`, not a constant, so a long-lived
|
|
148
167
|
worker picks the column up on its next job.
|
|
@@ -348,6 +367,13 @@ A one-shot rem+add corrector was scoped and deliberately **not** built.
|
|
|
348
367
|
injection before they go into the digest.
|
|
349
368
|
|
|
350
369
|
## Change history
|
|
370
|
+
- 2026-09-15 — **Corrected the deploy gate: `hasEndCustomerColumn()` did NOT protect the ORM write,
|
|
371
|
+
and the sync was down 9 days (2026-09-01 → 2026-09-10).** The column existed; `_underscore`'s
|
|
372
|
+
`_Model_Forecast_Opportunity` had not declared the `endCustomerName` property, so `_Model`
|
|
373
|
+
rejected the field on every `Netsuite/Opportunity/post|put` (611 failed jobs / 310 opportunities).
|
|
374
|
+
Added `canWriteEndCustomerNameToModel()` for the ORM write and scoped `hasEndCustomerColumn()` to
|
|
375
|
+
raw `_Query` callers (worker2 PR #168, open). Lesson: gate on the layer the write goes through —
|
|
376
|
+
the model declaration is a second deploy half and it lives in a different repo. (ajean)
|
|
351
377
|
- 2026-09-02 — **End Customer now comes from NetSuite `custbody_end_customer`, else mirrors the
|
|
352
378
|
client (worker2 PR #158 + dbchanges2 PR #477; re-import PR #159 — all merged).** Root cause: End
|
|
353
379
|
Customer was derived from the opportunity `entity`, which **is the partner** on a partner-routed
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-15
|
|
10
10
|
owners: ["dfranks", "kyalamarthi", "ajean", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Netsuite.php
|
|
@@ -144,6 +144,12 @@ claim on a never-claimed opportunity, so it does **not** contradict the rule tha
|
|
|
144
144
|
*Consequence: `updateTask()` is unreachable* below). So when triaging a backlog of
|
|
145
145
|
`skipped (no presales lead)`, read it as **"queued, awaiting a lead"**, not "lost".
|
|
146
146
|
|
|
147
|
+
**Confirmed end-to-end a second time, 2026-09-15.** Opportunity **75504** (NS internalId 7377655,
|
|
148
|
+
`Forecast.Opportunities.id` 94076) had no task; replaying its webhook returned
|
|
149
|
+
`{"db":"updated opportunity 7377655 (id=94076, items=1)","clickup":"skipped (no presales lead)"}`.
|
|
150
|
+
Assigning the Presales Lead in NetSuite produced the task. **This gate is the single most common
|
|
151
|
+
reason a specific opportunity has no ClickUp task** — check it before suspecting the sync.
|
|
152
|
+
|
|
147
153
|
Shared helpers: `buildCustomFields()` (the field array, used by both create and update),
|
|
148
154
|
`resolvePresalesUserId()` (cached `Client_True.Users.c_clickupUserIdentifier` lookup → else a
|
|
149
155
|
`GET /team` roster scan that back-fills the cache via `cacheClickupUser()`).
|
|
@@ -306,6 +312,57 @@ Every `Netsuite/Opportunity/post|put` job writes a JSON result into `Core.Worker
|
|
|
306
312
|
string literal or a column alias**, so match the `skipped update (update disabled)` verdict with
|
|
307
313
|
`LIKE '%disabled%'` and alias the column `skippedDisabled`, never `updateDisabled`. See
|
|
308
314
|
[MCP Tool Usage](../../../../standalone/apps/claude/workflows/mcp-tool-usage.md).
|
|
315
|
+
- **⚠ `Core.WorkerJobs` holds only ~30 days — a missing job row is NOT proof the webhook never
|
|
316
|
+
fired.** Measured 2026-09-15: `MIN(dtCreated)` across the **whole** table is **2026-08-16
|
|
317
|
+
02:00:46** (790,597 rows). So any "this opportunity has no job, therefore NetSuite never sent it"
|
|
318
|
+
conclusion is invalid past that horizon — a wrong call made and retracted mid-session on
|
|
319
|
+
2026-09-15. Same failure family as *an empty log table is not proof a code path never ran*. Note
|
|
320
|
+
the **discrepancy**: `worker2/architecture.md` describes the cleanup cron as deleting `isSuccess=1`
|
|
321
|
+
rows older than **90 days**, but the observed floor is ~30 — either the cron's behaviour or that
|
|
322
|
+
description is wrong. Trust the observed value and check before relying on either.
|
|
323
|
+
- **`skipped (no presales lead)` has TWO causes that look identical in the DB.** The gate tests the
|
|
324
|
+
**resolved email**, not the employee reference — so it fires both when NetSuite's
|
|
325
|
+
`custbody_ctc_solution_architect` is **unset** *and* when it is set to an employee whose record has
|
|
326
|
+
**no email**. Do not read the verdict as "the field is empty in NetSuite."
|
|
327
|
+
- **Scale reference for triage** — prod `Forecast.Opportunities` census, 2026-09-15. Most
|
|
328
|
+
opportunities have no ClickUp task, and that is normal:
|
|
329
|
+
|
|
330
|
+
| `netsuiteStatus` | Total | No task | With task |
|
|
331
|
+
|---|---|---|---|
|
|
332
|
+
| In Progress | 2,506 | 2,025 | 481 (~19%) |
|
|
333
|
+
| Closed - Won | 52,931 | 52,808 | 123 |
|
|
334
|
+
| Closed - Lost | 19,235 | 19,042 | 193 |
|
|
335
|
+
|
|
336
|
+
### Backfill = replay the webhook, and bulk replay is safe
|
|
337
|
+
|
|
338
|
+
Re-firing the ingestion endpoint is the repair mechanism for any opportunity the sync missed:
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
curl -X POST https://webhook.togahub.com/netsuite -H 'Content-Type: application/json' \
|
|
342
|
+
-d '{"recordType":"opportunity","eventType":"edit","internalId":<id>}'
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Why bulk replay is safe** (exercised at scale 2026-09-15 — 264 replays at 0.5 s spacing, all 264
|
|
346
|
+
accepted by the ingestion Lambda; 265 jobs succeeded, 1 failed, 14 ClickUp tasks created, 1
|
|
347
|
+
`already claimed`, 250 `no presales lead`):
|
|
348
|
+
|
|
349
|
+
- The atomic `clickupTaskId` claim makes an already-claimed opportunity return
|
|
350
|
+
`skipped (already claimed)` and write **nothing** to ClickUp — **no duplicate tasks**.
|
|
351
|
+
- An opportunity with no presales lead returns `skipped (no presales lead)` and stays queued.
|
|
352
|
+
- `importOpportunity()` is self-healing, so the replay also repairs the Forecast row.
|
|
353
|
+
|
|
354
|
+
**A `404` on replay is the correct signal for a NetSuite-deleted opportunity, not a defect.** The
|
|
355
|
+
single failure in that run (NS internalId **7442501**) was NetSuite REST HTTP 404
|
|
356
|
+
*"The opportunity does not exist"* — the record had been deleted in NetSuite, which is also why it
|
|
357
|
+
was the one row missing from Forecast.
|
|
358
|
+
|
|
359
|
+
### Open item — item-sync watchdog timeouts (NOT investigated)
|
|
360
|
+
|
|
361
|
+
`Netsuite/InventoryItem/put|post` and `Netsuite/NonInventoryItem/*` logged **427 failures** over
|
|
362
|
+
2026-09-01 → 2026-09-10, all with output `watchdog: exceeded maxExecutionTime without completing`.
|
|
363
|
+
**Different root cause from the `endCustomerName` outage** (execution time, not a model error) and
|
|
364
|
+
deliberately out of scope on 2026-09-15. Recorded as a known open item; needs its own ticket. No
|
|
365
|
+
cause is asserted here.
|
|
309
366
|
|
|
310
367
|
### ⚠ A healthy sync reads as dead: new tasks do NOT stay in "Presales Qualification"
|
|
311
368
|
|
|
@@ -341,6 +398,17 @@ Full list ids and the API constraints are in
|
|
|
341
398
|
re-derive them here. **Practical rule is unchanged:** verify by task id or across the whole
|
|
342
399
|
Opportunities folder, never by one list.
|
|
343
400
|
|
|
401
|
+
**More evidence, 2026-09-15 — and a counter-example that pins down *when* the Automation moves a
|
|
402
|
+
task.** After the outage fix, new tasks again landed spread across folder `90117060687`
|
|
403
|
+
("Opportunities"), space `90113928591`, **none** in Presales Qualification (`901111987449`):
|
|
404
|
+
`868m40zf5` (opp 76045 Adyen, 9/10) → Pending Client Response · `868m484vn` (opp 76021 Seminole
|
|
405
|
+
Gaming, 9/11) → BU Discovery (`901112518588`) · `868m5e958` (opp 75740, 9/15) and `868m5fmc3`
|
|
406
|
+
(opp 76115, 9/15) → Contract Negotiations (`901111987463`). **But the 14 tasks created by the
|
|
407
|
+
backfill replay the same day DID stay in Presales Qualification** (e.g. `868m5gexk` opp 72119,
|
|
408
|
+
`868m5ghgt` opp 76051). So the Automation moves a task **only when the stage dropdown changes**, not
|
|
409
|
+
on creation — a task sitting in Presales Qualification means its stage has not moved since birth,
|
|
410
|
+
not that the mover is broken.
|
|
411
|
+
|
|
344
412
|
**The Automation is load-bearing.** NetSuite writes the dropdown and ClickUp→NetSuite reads the
|
|
345
413
|
human's list move; both directions run through it. Treat any change to that Automation as a
|
|
346
414
|
**breaking change** to this integration.
|
|
@@ -383,26 +451,25 @@ the legacy cron's OPPORTUNITIES section:
|
|
|
383
451
|
letter, so the handler runs a one-row SuiteQL query for it.
|
|
384
452
|
- **`endCustomerName`** (added 2026-09-01, `dbchanges2/Forecast/2026-09-01a …`) holds NetSuite
|
|
385
453
|
`custbody_end_customer`'s `refName` — a **name, not a FK**, because nothing upserts an
|
|
386
|
-
end-customer `Customers` row.
|
|
387
|
-
(`hasEndCustomerColumn()`),
|
|
388
|
-
the
|
|
454
|
+
end-customer `Customers` row. **CORRECTED 2026-09-15 — this bullet used to say the write and both
|
|
455
|
+
reads are "gated on the column existing (`hasEndCustomerColumn()`)", which reads as "safe". It was
|
|
456
|
+
not: that gate checks the DB column and the model write took the sync down for 9 days anyway.**
|
|
457
|
+
There are now **two** gates, and they are not interchangeable:
|
|
458
|
+
- `hasEndCustomerColumn()` — `information_schema` column check. Correct **only** for raw `_Query`
|
|
459
|
+
SQL (the digest and the two backfills).
|
|
460
|
+
- `canWriteEndCustomerNameToModel()` — `hasEndCustomerColumn() && property_exists(
|
|
461
|
+
_Model_Forecast_Opportunity::class, 'endCustomerName')`. The **only** gate valid for
|
|
462
|
+
`importOpportunity()`'s ORM write.
|
|
463
|
+
|
|
464
|
+
See the *`endCustomerName` 9-day outage* gotcha below and
|
|
389
465
|
[the labels doc](./netsuite-opportunity-client-labels.md).
|
|
390
|
-
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
every opportunity import dead. `Core.WorkerJobs` census: Sep 1 = 115 ok / 60 fail (last good day),
|
|
398
|
-
Sep 2 → Sep 10 = 0 ok / 100% fail (61 fails on Sep 10 alone). This was a **half-deploy**: the
|
|
399
|
-
migration + the handler write shipped, the model field declaration did not.
|
|
400
|
-
**Durable rule: a new column that the handler writes must ship THREE things together — the
|
|
401
|
-
`dbchanges2` migration, the handler write, AND the `_Model` field declaration.** A column-existence
|
|
402
|
-
guard (`SHOW COLUMNS`) proves nothing about the ORM; both ORMs enumerate *declared* properties, so an
|
|
403
|
-
undeclared field is a hard `__set` throw, not a silent skip. Fixed 2026-09-10 by declaring
|
|
404
|
-
`public $endCustomerName = self::FIELD_CHAR;` (matching sibling char fields clickupTaskId/title/memo
|
|
405
|
-
over the varchar column); deployed to worker2 `_production`, sync recovered.
|
|
466
|
+
- **Durable rule from the outage: a new column the handler writes must ship THREE things together —
|
|
467
|
+
the `dbchanges2` migration, the handler write, AND the `_Model` field declaration.** A
|
|
468
|
+
column-existence guard proves nothing about the ORM; both ORMs enumerate *declared* properties, so
|
|
469
|
+
an undeclared field is a hard `__set` throw, not a silent skip. Fixed 2026-09-10 by declaring
|
|
470
|
+
`public $endCustomerName = self::FIELD_CHAR;` (matching sibling char fields
|
|
471
|
+
`clickupTaskId`/`title`/`memo` over the varchar column). Full incident detail in the
|
|
472
|
+
*`endCustomerName` 9-day outage* gotcha below — do not restate it here.
|
|
406
473
|
- Writes use raw `_Query` against `_underscore::DB_FORECAST` and **must**
|
|
407
474
|
`_Database::transactionCommit(DB_FORECAST)` (lazy-transaction gotcha).
|
|
408
475
|
|
|
@@ -439,6 +506,40 @@ None — platform-wide Forecast sync.
|
|
|
439
506
|
|
|
440
507
|
## Gotchas / known issues
|
|
441
508
|
|
|
509
|
+
- **⚠ The `endCustomerName` 9-day total outage — a column-existence gate does NOT protect an ORM
|
|
510
|
+
write (2026-09-01 → 2026-09-10).** Every `Netsuite/Opportunity/post|put` job threw
|
|
511
|
+
`There is no field called 'endCustomerName' in the '_Model_Forecast_Opportunity' model.`
|
|
512
|
+
(`_underscore/Model.php:498`) from 2026-09-01 12:01 to 2026-09-10 15:48 CDT. **The whole
|
|
513
|
+
NetSuite→Forecast→ClickUp opportunity sync was dead for 9 days and every job was red.**
|
|
514
|
+
- **Cause — a deploy split across TWO repos.** worker2 shipped the `$values['endCustomerName']`
|
|
515
|
+
write (PR #158) while `_underscore/Model/Forecast/Opportunity.php` had not yet declared the
|
|
516
|
+
property. `_Model` builds its field map from **declared properties** in the constructor, so
|
|
517
|
+
`__set` rejected the unknown field. The DB column existed (the migration had been applied) — the
|
|
518
|
+
missing half was the model declaration, in a different repo.
|
|
519
|
+
- **Blast radius:** the throw lands in `importOpportunity()` **before** the Forecast save and
|
|
520
|
+
**before** `maybeCreateClickupTask()`, so affected opportunities got **neither** a Forecast row
|
|
521
|
+
**nor** a ClickUp task. Measured in prod `Core.WorkerJobs`: **611 failed jobs, 310 distinct
|
|
522
|
+
opportunities, 264 still with no ClickUp task**, 1 (NS internalId 7442501) absent from Forecast.
|
|
523
|
+
- **Ended by `_underscore` commit `34f76ee5` "Add end customer field to Opportunity"** (Jeff
|
|
524
|
+
Cardinal, 2026-09-10 15:51:31 CDT — `public $endCustomerName = self::FIELD_CHAR;`) — three
|
|
525
|
+
minutes after the last failure. Per-day `WorkerJobs` census: Sep 1 = 115 ok / 60 fail (the
|
|
526
|
+
failures start midday, which is why this is written up as both "8 days" and "9 days"),
|
|
527
|
+
Sep 2 → Sep 10 = **0 ok / 100% fail**.
|
|
528
|
+
- **The lesson, and it generalizes: gate on the layer the write actually goes through, not the
|
|
529
|
+
layer that is easiest to query.** `hasEndCustomerColumn()`'s own docblock predicted this exact
|
|
530
|
+
failure ("it breaks the ENTIRE opportunity sync") and still did not stop it, because it ran
|
|
531
|
+
`SHOW COLUMNS` against `Forecast.Opportunities` while the write went through the ORM. A column
|
|
532
|
+
guard protects raw SQL only. **A model-declaration gap is a second, independent half of the
|
|
533
|
+
deploy, and it ships from a different repo.**
|
|
534
|
+
- **Fix (worker2 PR #168, open against `_production` as of 2026-09-15, not yet merged).**
|
|
535
|
+
`hasEndCustomerColumn()` is byte-identical and now documented as being for **raw `_Query`
|
|
536
|
+
reads/writes only** — its three callers (`OpportunityStakeholderDigest.php:180`,
|
|
537
|
+
`OpportunityStakeholderBackfill.php:579`, `OpportunityEndCustomerBackfill.php:91`) all build
|
|
538
|
+
plain SQL. New `canWriteEndCustomerNameToModel()` = `hasEndCustomerColumn() && property_exists(
|
|
539
|
+
_Model_Forecast_Opportunity::class, 'endCustomerName')` is used **only** by
|
|
540
|
+
`importOpportunity()`. Each half `error_log`s separately, naming the repo that is behind. No
|
|
541
|
+
migration. **No regression test** — worker2 has no test coverage under `Worker/` to hang one off
|
|
542
|
+
(only `Controller/CompassPeopleTest.php` and `Controller/ClickupDesignTest.php` exist).
|
|
442
543
|
- **`_Model::load()` returns TRUE only on an EXACTLY-ONE match — a duplicate-row self-amplifier in
|
|
443
544
|
any load-then-upsert handler.** `_Model::load()` (`_underscore/Model.php:788–808`) returns FALSE
|
|
444
545
|
for **both** 0 rows **and** 2+ rows — it succeeds only when the lookup matches exactly one row.
|
|
@@ -775,6 +876,27 @@ deprecated** for production opportunity code.
|
|
|
775
876
|
blocked on the Aaron stakeholder decision noted above.
|
|
776
877
|
|
|
777
878
|
## Change history
|
|
879
|
+
- 2026-09-15 — **Root-caused a 9-day TOTAL outage of the opportunity sync and split the deploy gate
|
|
880
|
+
(worker2 PR #168, open).** Same incident jcardinal fixed on the `_underscore` side below; this
|
|
881
|
+
entry adds the worker2-side gate and the measured blast radius. From 2026-09-01 12:01 to
|
|
882
|
+
2026-09-10 15:48 CDT every `Netsuite/Opportunity/post|put` threw `There is no field called
|
|
883
|
+
'endCustomerName' in the '_Model_Forecast_Opportunity' model` — worker2 shipped the write
|
|
884
|
+
(PR #158) before `_underscore` declared the property, and `_Model` builds its field map from
|
|
885
|
+
declared properties. **611 failed jobs / 310 opportunities / 264 left with no ClickUp task** (1,
|
|
886
|
+
NS internalId 7442501, absent from Forecast); ended by `_underscore` `34f76ee5`. The existing
|
|
887
|
+
`hasEndCustomerColumn()` gate did not help because it checks the **DB column** while the write
|
|
888
|
+
goes through the **ORM** — corrected that claim in *Data model* and added
|
|
889
|
+
`canWriteEndCustomerNameToModel()` (`hasEndCustomerColumn() && property_exists(...)`) used only by
|
|
890
|
+
`importOpportunity()`; `hasEndCustomerColumn()` is now scoped to raw `_Query` callers.
|
|
891
|
+
**Also recorded:** `Core.WorkerJobs` retains only **~30 days** (MIN `dtCreated` 2026-08-16), so a
|
|
892
|
+
missing job row proves nothing — and that contradicts architecture.md's 90-day cleanup
|
|
893
|
+
description; **bulk webhook replay is safe** (264 replays, 0 duplicate tasks — the claim, the
|
|
894
|
+
presales gate and self-healing import make it so) and a NetSuite **404 on replay = the record was
|
|
895
|
+
deleted**; `skipped (no presales lead)` has **two** causes (field unset, or the employee has no
|
|
896
|
+
email); a fresh `Forecast.Opportunities` census; more evidence for the list-visibility trap plus
|
|
897
|
+
the counter-example that the Automation moves a task **only on a stage-dropdown change**, not on
|
|
898
|
+
creation. Flagged an unrelated open item: **427** `Netsuite/(Non)InventoryItem/*` watchdog
|
|
899
|
+
execution-time failures in the same window, not investigated. (ajean)
|
|
778
900
|
- 2026-09-10 — **Fixed an 8-day total opportunity-sync outage caused by a half-deploy: a missing ORM
|
|
779
901
|
model field.** `Forecast.Opportunities.endCustomerName` (column added ~Sep 1) and the worker2 handler
|
|
780
902
|
write shipped, but `_Model_Forecast_Opportunity` (`_underscore/Model/Forecast/Opportunity.php`) never
|
package/package.json
CHANGED