toga-ai 1.0.108 → 1.0.110

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.
@@ -40,10 +40,13 @@ second, independently-gated concern in the same handler.
40
40
  `test/@dave/NetSuite/api-message-queue/` — upload all four `.js` to one File Cabinet folder):
41
41
  - `lib_amq_queue.js` — shared module: record-type/field ids, `transmit()`, `findSendableIds()`.
42
42
  - `ue_api_msg_queue_enqueue.js` — User Event on the synced source records (Opportunity, …):
43
- writes one queue row per create/edit/delete.
44
- - `ue_amq_drain.js` — User Event on the **API Message Queue** record: async trigger — kicks the
45
- scheduled drainer via `N/task` (no inline HTTP, so it never blocks the source save).
46
- - `ss_amq_drain.js` — Scheduled Script: catch-up drainer (retries + anything the UE missed).
43
+ writes one queue row per create/edit/delete, then **kicks the scheduled drainer via `N/task`**
44
+ (async, non-blocking). The immediate trigger lives here — see gotchas for why.
45
+ - `ss_amq_drain.js` — Scheduled Script: drains rows (the on-demand kick *and* a cron safety-net).
46
+ Needs **two deployments**: one `Scheduled` (cron) + one `Not Scheduled` (the on-demand slot
47
+ `task.create` submits into).
48
+ - `ue_amq_drain.js` — **RETIRED.** A UE on the queue record can't fire on enqueuer-created rows
49
+ (UE-can't-trigger-UE); undeploy it.
47
50
  - The custom record itself: **"API Message Queue"**, internal id **1185**.
48
51
 
49
52
  **worker2 side:**
@@ -59,12 +62,12 @@ second, independently-gated concern in the same handler.
59
62
  (`{recordType, internalId, eventType, action}`), `headers`. It maps `record.Type`
60
63
  (lowercase) → the camelCase `recordType` worker2 expects (explicit map; e.g.
61
64
  `salesorder`→`salesOrder`), and `context.type` → CRUD verb (`create`→POST, `edit`/`xedit`→PUT,
62
- `delete`→DELETE).
63
- 2. **Trigger + drain (NetSuite):** the queue record's `ue_amq_drain` UE fires the scheduled
64
- `ss_amq_drain` via `N/task` (async); `ss_amq_drain` also runs on its own schedule.
65
- `transmit()` claims a row (`Created`→`Sending`) before the HTTP call, POSTs to the row's
66
- endpoint, then **deletes on 2xx** / sets `Retry` (+exponential backoff) / `Failed`
67
- (terminal — never auto-deleted; Jeff's data-loss rule).
65
+ `delete`→DELETE). It then **kicks the drainer**:
66
+ `task.create({taskType:SCHEDULED_SCRIPT, scriptId:'customscript_ss_amq_drain'})` — scriptId only,
67
+ no deployment id. If no on-demand slot is free the kick is skipped and the cron backs it up.
68
+ 2. **Drain (NetSuite):** `ss_amq_drain` runs (from the kick or its cron). `transmit()` claims a row
69
+ (`Created`→`Sending`) before the HTTP call, POSTs to the row's endpoint, then **deletes on 2xx** /
70
+ sets `Retry` (+exponential backoff) / `Failed` (terminal — never auto-deleted; Jeff's data-loss rule).
68
71
  3. **Ingest (prod):** `webhook.togahub.com/netsuite` → WebhookIngestion **Python Lambda** →
69
72
  INSERT `Core.WorkerJobs` + SQS → returns 200 immediately. Fully async; outcome lives in
70
73
  `WorkerJobs.isSuccess`/`failureReason`.
@@ -72,7 +75,24 @@ second, independently-gated concern in the same handler.
72
75
  `_Worker::runTask('Netsuite/Opportunity/<verb>', {internalId})`.
73
76
  5. **Handle (worker2):** `_Worker_Netsuite_Opportunity` fetches the record from NetSuite by id
74
77
  and upserts it into `Forecast.Opportunities` (+ `OpportunityItems`), and — only when a
75
- Presales Lead is set — creates the ClickUp opportunity task.
78
+ Presales Lead is set — creates the ClickUp opportunity task. **Both `POST` (create) and `PUT`
79
+ (edit) create the task** (since 2026-06-16); `DELETE` only removes from Forecast.
80
+
81
+ ## ClickUp opportunity task
82
+
83
+ Created by `maybeCreateClickupTask()` → `createTask()` (ported verbatim from the legacy
84
+ `webhook/` handler), gated on a **Presales Lead** (`custbody_ctc_solution_architect` employee →
85
+ email; empty email = skip). Posts to list `901111987449` with `custom_item_id` = `1009`.
86
+
87
+ - **Task name** = `{opportunityNumber} — {customer} — {title}`, e.g.
88
+ `60533 — 560 Endeavor Health — Data Center Facility Consolidation Services` (empty parts dropped).
89
+ - **Custom-field value mapping** (the non-obvious part — these were swapped before 2026-06-16):
90
+ `Opportunity #` (`a5529cdc-…`) ← `tranId`; `Customer #` (`170dc118-…`) ← `entity->refName`
91
+ (the customer **name**, deliberately — not the NetSuite customer number). Plus `Sales Rep`,
92
+ `Presales Lead Email`, and `Presales Lead` (users field, resolved via a cached
93
+ `Client_True.Users.c_clickupUserIdentifier` lookup, falling back to a `GET /team` scan).
94
+ - The ClickUp token comes from `[clickup] token` in the worker2 config (legacy webhook received
95
+ it on the payload; the new contract drops it).
76
96
 
77
97
  ## Data model
78
98
 
@@ -97,6 +117,16 @@ None — platform-wide Forecast sync.
97
117
 
98
118
  ## Gotchas / known issues
99
119
 
120
+ - **A User Event can't trigger another User Event → the immediate trigger lives in the enqueuer.**
121
+ The enqueuer (a UE) creating the queue row does NOT fire any UE on the queue record (only the
122
+ *scheduled* drainer's `submitFields` does). So a drain-UE on the queue record never runs for
123
+ enqueued rows — the enqueuer kicks the drainer itself via `task.create`. (`ue_amq_drain.js` is
124
+ retired for exactly this reason. Evidence: its log only ever showed `xedit`/`Sending`, never `create`.)
125
+ - **`task.create(SCHEDULED_SCRIPT)` needs a `Not Scheduled` deployment.** A `Scheduled` deployment is
126
+ reserved for its recurrence → `NO_DEPLOYMENTS_AVAILABLE`; a `Testing` deployment can't be submitted
127
+ at all. Give `ss_amq_drain` a dedicated `Not Scheduled` deployment as the on-demand slot (add more
128
+ for concurrency) and keep a `Scheduled` one for the cron. Submit by **scriptId only** — the
129
+ deployment id is prefix-doubled (`customdeploycustomdeploy_ss_amq_drain`).
100
130
  - **worker2 routes EVERY request to `_Controller_Index::worker()`** — `worker2/_.php` sets
101
131
  `DEFAULT_CONTROLLER_METHOD = ['_Controller_Index','worker']`, which short-circuits `_Route`'s
102
132
  URI matching. There is no per-URL routing, and `.htaccess` denies direct `.php` files, so you
@@ -115,7 +145,26 @@ None — platform-wide Forecast sync.
115
145
  - **No WebhookIngestion Lambda locally.** Dev points the enqueuer at an ngrok tunnel to local
116
146
  worker2 via the `DEV_OVERRIDE` block, which also wraps the body in worker2's legacy
117
147
  `{action,parameters}` envelope (since there's no Lambda to convert `/netsuite` → an action).
118
- Set `DEV_OVERRIDE.enabled=false` for prod.
148
+ - **`debugWrap` is the dev/prod body-shape switch — not just the URL.** `debugWrap=true` (dev)
149
+ nests the raw `{recordType,internalId,…}` contract inside worker2's legacy `{action,parameters}`
150
+ envelope (`action='Netsuite/Webhook'`); the local `worker()` legacy path needs this because
151
+ there's no Lambda. `debugWrap=false` (prod) sends the raw contract — the WebhookIngestion Lambda
152
+ derives the `Netsuite/Webhook` action from the `/netsuite` URL path itself. Changing only the
153
+ endpoint to prod while leaving `debugWrap` on double-wraps the body and breaks the handler.
154
+ - **Prod cutover: flip `DEV_OVERRIDE.enabled=false` — do NOT comment out the block.** `getConfig()`
155
+ reads `DEV_OVERRIDE.enabled/.endpoint/.debugWrap`; deleting the `const` throws `ReferenceError`,
156
+ which `afterSubmit`'s catch swallows as `AMQ enqueue FAILED` → **no row enqueued, sync silently
157
+ stops.** Flipping `enabled=false` makes `getConfig()` fall through to `DEFAULTS.endpoint`
158
+ (`https://webhook.togahub.com/netsuite`) with `debugWrap` off. Also flip the enqueuer deployment
159
+ `Testing → Released` (Testing fires only for the deploying user). Full checklist in
160
+ `DEPLOY_RUNBOOK.md` §6.
161
+ - **No ClickUp dedup (deferred).** `maybeCreateClickupTask()` only gates on the Presales Lead — it
162
+ does **not** check whether a task already exists. Since both POST and PUT now create, **every edit
163
+ of an opportunity with a Presales Lead creates a NEW ClickUp task** (and the NetSuite-side
164
+ enqueuer fires on every save). Before this hits production traffic, add dedup — recommended:
165
+ store the created ClickUp task id on the `Forecast.Opportunities` row and skip when present.
166
+ Until 2026-06-16 PUT skipped ClickUp entirely, which masked this (an edit produced no duplicate
167
+ only because the edit path didn't create at all).
119
168
  - **NetSuite doubled-prefix ids.** The queue record and its fields were created with the full
120
169
  custom prefix typed into the ID box, so the real ids are doubled:
121
170
  type `customrecordcustomrecord_api_msg_queue`, fields `custrecordcustrecord_amq_*`. The
@@ -124,13 +173,21 @@ None — platform-wide Forecast sync.
124
173
  string throws `Object of class stdClass could not be converted to string`. Normalize before
125
174
  matching.
126
175
  - **Drain dedup** uses a `Sending` claim status: `transmit()` flips `Created`→`Sending` before the
127
- HTTP call so a concurrent pass skips it; the immediate UE trigger only fires for `Created`/
128
- `Pending`, so the drainer's own `Sending`/`Retry`/`Failed` writes don't re-trigger it (a
129
- `skip (status not triggerable)` debug line on those is expected). `findSendableIds` must list
130
- every sendable status (`Created`,`Pending`,`Retry`,`Sending`) — omitting `Created` silently
131
- matches nothing (`candidates:0`).
176
+ HTTP call so a concurrent drainer pass skips it (`Sending` ∉ sendable), narrowing the double-send
177
+ window to the load→claim gap. `findSendableIds` must list every sendable status
178
+ (`Created`,`Pending`,`Retry`,`Sending`) — omitting `Created` silently matches nothing (`candidates:0`).
132
179
 
133
180
  ## Change history
181
+ - 2026-06-16 — ClickUp parity hardening (TRUE-79182): verified the `Opportunity #`/`Customer #`
182
+ custom fields exist on list 901111987449 and **fixed their swapped values**; `PUT` now also
183
+ creates the ClickUp task (was POST-only); task name reformatted to
184
+ `{opp#} — {customer} — {title}`; `createTask()` made private + generic `Exception` throws
185
+ narrowed to `RuntimeException`. Dedup still deferred (see gotcha). (dfranks)
186
+ - 2026-06-16 — Trigger correction: the immediate trigger moved INTO the enqueuer (`task.create`
187
+ the drainer) after discovering a UE can't trigger a UE — `ue_amq_drain` retired. `task.create`
188
+ needs a `Not Scheduled` deployment of `ss_amq_drain` (else `NO_DEPLOYMENTS_AVAILABLE`); submit by
189
+ scriptId only (deployment id is prefix-doubled). Fixed an `itemType`-as-object crash in the
190
+ importer. POST/DELETE coded but only PUT (edit) verified live. (dfranks)
134
191
  - 2026-06-16 — Built the queue enqueuer/drainer SuiteScripts, the worker2 `/netsuite` router, and
135
192
  the `_Worker_Netsuite_Opportunity` Forecast importer (header + items + status), with ClickUp
136
193
  parity. Verified opportunity 5483750 imports end-to-end in dev via ngrok. (dfranks)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.108",
3
+ "version": "1.0.110",
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",