toga-ai 1.0.450 → 1.0.451

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 @@
6
6
  | [_underscore Framework Architecture](architecture.md) | `_underscore` is the shared PHP backend framework for **all 2.0 applications**. | _underscore/_underscore.php, _underscore/Loader.php, _underscore/Framework.php, _underscore/Model.php, _underscore/Database.php, _underscore/Query.php, _underscore/Route.php, _underscore/Component.php |
7
7
  | [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Page.php, _underscore/Model/Client/TrackingNumber.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-02c - TrackingNumberNeedsReturnLabelFieldPermission.sql, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql |
8
8
  | [Address Validation (carrier waterfall + validateAddress scripted endpoint)](features/address-validation.md) | `_Model_Client_Address::validateAddress` verifies a US address against a **carrier waterfall (USPS → FedEx → UPS)** and returns a single canonical, carrier-norm | _underscore/Model/Client/Address.php |
9
- | [_ApiRequest ENCODE__JSON now sends Content-Type: application/json](features/apirequest-json-content-type.md) | `_ApiRequest::execute()`'s `ENCODE__JSON` branch json-encoded the request body but never set a `Content-Type` header. | _underscore/ApiRequest.php |
9
+ | [_ApiRequest — JSON encode/decode & api-logging behavior](features/apirequest-json-content-type.md) | `_ApiRequest` is the 2.0 outbound HTTP client. | _underscore/ApiRequest.php |
10
10
  | [Assortment Name Translation (AssortmentTranslations sidecar)](features/assortment-name-translation.md) | Serves Assortment (product-grouping) **names** in multiple languages by adding a per-language **sidecar** table `AssortmentTranslations`, reusing the platform's | _underscore/Model/Client/AssortmentTranslation.php, dbchanges2/Client/2026-06-26a - AssortmentTranslations.sql, dbchanges2/Core/2026-06-26a - AssortmentTranslationsRecord.sql, dbchanges2/Client/2026-06-26b - AssortmentTranslationsAcl.sql |
11
11
  | [Asynchronous Query Execution (writes-only, via Worker)](features/async-query-execution.md) | `_Query` can run a **write** query asynchronously so a long/slow write does not hold a request-scoped DB connection open long enough to hit **"MySQL server has | _underscore/Query.php, worker2/Worker/Infrastructure/Database.php, worker2/Worker/Team/Transcripts.php |
12
12
  | [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
@@ -1,21 +1,27 @@
1
1
  ---
2
- title: _ApiRequest ENCODE__JSON now sends Content-Type: application/json
2
+ title: "_ApiRequest — JSON encode/decode & api-logging behavior"
3
3
  framework: "2.0"
4
4
  repo: _underscore
5
5
  project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-14
9
+ updated: 2026-07-28
10
10
  owners: ["jcardinal"]
11
11
  files:
12
12
  - _underscore/ApiRequest.php
13
13
  related:
14
14
  - ../../worker2/features/oneuptime-worker2-monitoring.md
15
+ - ../../worker2/features/creating-worker-actions.md
16
+ - ../../worker2/features/talos-transcript-ingestion.md
15
17
  ---
16
18
 
17
19
  ## Summary
18
- `_ApiRequest::execute()`'s `ENCODE__JSON` branch json-encoded the request body but never set
20
+ `_ApiRequest` is the 2.0 outbound HTTP client. This doc collects its non-obvious
21
+ encode/decode/retry/logging behavior that callers keep getting bitten by; the original subject
22
+ was the Content-Type fix below.
23
+
24
+ **Original fix (2026-07-14):** `_ApiRequest::execute()`'s `ENCODE__JSON` branch json-encoded the request body but never set
19
25
  a `Content-Type` header. cURL therefore defaulted to `application/x-www-form-urlencoded`, and
20
26
  receivers parsed the entire JSON string as a single form-field NAME — observed at OneUptime as
21
27
  `{"<json>":""}`. Every 2.0 `ENCODE__JSON` POST/PUT/PATCH caller was silently shipping
@@ -30,10 +36,40 @@ case-insensitive scan of already-set headers, so a caller-supplied `Content-Type
30
36
  - This changes the wire format of ALL existing `ENCODE__JSON` callers (they were previously
31
37
  sending form-urlencoded). This is a correction, but smoke-test heavy JSON callers post-deploy:
32
38
  ClickUp, NetSuite, Vapi.
39
+ - **`execute()` decodes a JSON response with `json_decode($payload)` — NO assoc flag — so you
40
+ get `stdClass`, not an array.** Code that indexes the response as `$res['result']['x']` breaks.
41
+ If a caller needs arrays, don't rely on the built-in decode: leave the response handling local
42
+ (`json_decode($response, true)`).
43
+ - **`ENCODE__JSON` re-encodes the payload without `JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE`.**
44
+ For large non-ASCII bodies (e.g. a full meeting transcript) this inflates every such char to
45
+ `\uXXXX`. To preserve the exact wire bytes, `json_encode` with the flags you want yourself and
46
+ pass the **ready JSON string with payloadEncoding left null**.
47
+ - **`setAutoRetry()` is flat-delay and retries EVERY non-2xx.** If you need exponential backoff
48
+ or fail-fast on 4xx (other than 429), keep your own retry loop and construct one `_ApiRequest`
49
+ per attempt — that also gives you one api-log row per attempt.
50
+ - **Transport failures surface as a thrown Exception** from `execute()` (the equivalent of
51
+ `curl_error()`), not a return value. Catch it if your method has an error-return contract.
52
+ - **Api logging depends on `DB_CLIENT_LOGS` being registered.** The logging branch writes via
53
+ `_Model_Client_Logs_Api`, whose `DATABASE` const is `_underscore::DB_CLIENT_LOGS`. In a context
54
+ that hasn't registered it (most workers, CLI harnesses) logging is a **silent no-op** — or
55
+ throws `Unknown database 'ClientLogs'`. Either register the logs DB under that alias or pass
56
+ `setLogging(false)` deliberately. See
57
+ [Creating Worker Actions](../../worker2/features/creating-worker-actions.md#gotchas).
58
+ - **OPEN / not fixed: logging happens in two halves around the HTTP call.** `execute()` inserts
59
+ the request row and **commits before** the call, then `save()`s the response fields **after**.
60
+ For long calls (e.g. a 600s AI timeout) that second save runs on a connection idle for
61
+ minutes — the classic stale-connection failure mode — leaving api-log rows with null
62
+ `responseCode`/`responsePayload`. The correct fix (reconnect/re-register before the post-call
63
+ save) lives **inside `execute()`** and would affect every framework caller, so it is deferred
64
+ pending architecture review. Do **not** work around it per-caller.
33
65
 
34
66
  ## Change history
67
+ - 2026-07-28 — Broadened from the Content-Type fix to the general `_ApiRequest` contract:
68
+ documented the non-assoc `json_decode` response (returns `stdClass`), the `ENCODE__JSON`
69
+ re-encode losing `JSON_UNESCAPED_*`, `setAutoRetry()`'s flat-delay/retry-all behavior,
70
+ exception-on-transport-failure, the `DB_CLIENT_LOGS` dependency for api logging, and the
71
+ **open** two-phase logging / stale-connection issue on long calls. Surfaced migrating
72
+ `worker2 Worker/Team/Transcripts.php::callTalosEndpoint()` off raw curl. (jcardinal)
35
73
  - 2026-07-14 — Fixed `ENCODE__JSON` to send `Content-Type: application/json` (guarded so a
36
74
  caller-set Content-Type wins). Root-caused via OneUptime receiving `{"<json>":""}`. Affects
37
75
  all 2.0 ENCODE__JSON callers. (jcardinal)
38
- </content>
39
- </invoke>
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-21
9
+ updated: 2026-07-28
10
10
  owners: [jcardinal, dfranks, mhammontree]
11
11
  files:
12
12
  - worker2/Worker/
@@ -189,10 +189,29 @@ be reattempted.
189
189
  `$environment = (substr(_Environment::$name, 0, 4) === 'dev-') ? 'dev' : _Environment::$name`.
190
190
  Without it, every `_Model_Client_*` load fails. Proven pattern: `_Worker_Startech` (and
191
191
  `_Worker_Notification_EmailTemplate`). Do this in `initialize()` or at the top of the method.
192
+ - **`_ApiRequest` logging is a silent no-op unless the Logs DB is registered as
193
+ `DB_CLIENT_LOGS`.** `_ApiRequest`'s logging branch writes through `_Model_Client_Logs_Api`,
194
+ whose `DATABASE` const is `_underscore::DB_CLIENT_LOGS`. A worker whose `initialize()` only
195
+ registers, say, `DB_TEAM` will produce **zero** api-log rows (or, in a CLI/test harness,
196
+ `Unknown database 'ClientLogs'`). So for any worker that makes outbound `_ApiRequest` calls,
197
+ pick one deliberately:
198
+ - **Want the calls logged** → register the logs DB in `initialize()`, e.g.
199
+ `_Database::registerDatabase(_Config::databaseLogs('Logs_True', …), _underscore::DB_CLIENT_LOGS)`
200
+ (proven pattern: `Worker/Team/Sprint.php::initialize()`, `Worker/Team/Transcripts.php::initialize()`).
201
+ - **Don't want them logged** → pass `setLogging(false)` explicitly, with a comment saying why
202
+ (proven: `Worker/Clickup/Fluffer.php`, `Worker/Vapi.php`, `Worker/Ai/Bdr/Vapi.php`).
203
+
204
+ Swapping raw curl for `_ApiRequest` "to get logging" without doing the first of those is the
205
+ common trap — the code looks right and logs nothing.
192
206
  - See [architecture.md](../architecture.md) for the always-HTTP-200 rule and the
193
207
  commit-before-SQS transaction pattern that the worker relies on.
194
208
 
195
209
  ## Change history
210
+ - 2026-07-28 — Added the **`_ApiRequest` logging / `DB_CLIENT_LOGS` gotcha**: api-log rows are
211
+ written via `_Model_Client_Logs_Api` (`DATABASE = DB_CLIENT_LOGS`), so a worker must either
212
+ register the Logs DB under that alias in `initialize()` or pass `setLogging(false)`
213
+ deliberately. Surfaced migrating the Talos AI caller in `Worker/Team/Transcripts.php` off raw
214
+ curl. (jcardinal)
196
215
  - 2026-07-21 — Documented that adding a recurring job is **pure data** (a `Core.CronJobs` row;
197
216
  CronScheduler Lambda matches via croniter every minute, no code wiring beyond the row — but
198
217
  the action must be deployed), and the **biweekly self-gating pattern**: for schedules cron
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-13
9
+ updated: 2026-07-28
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - worker2/Worker/Team/Transcripts.php
@@ -30,6 +30,7 @@ related:
30
30
  - ./creating-worker-actions.md
31
31
  - ../architecture.md
32
32
  - ../../_underscore/features/async-query-execution.md
33
+ - ../../_underscore/features/apirequest-json-content-type.md
33
34
  - ../../../1.0/apps/tools/features/talos-kb-documents-admin.md
34
35
  - ../../../1.0/apps/test/features/talos-kb-pipeline.md
35
36
  ---
@@ -136,6 +137,35 @@ helpers).
136
137
  [Background Email-Template Worker](./notification-email-template.md); the template is the
137
138
  TOGA Technology `True`-client template).
138
139
 
140
+ ### `callTalosEndpoint()` — the shared low-level Talos AI caller
141
+ The private static `callTalosEndpoint()` is the single HTTP path used by **both** AI passes
142
+ (`callAICleaningAPI()` = clean+classify, and `buildRecapEmailBody()` = recap JSON). As of
143
+ 2026-07-28 it uses **`_ApiRequest`** instead of raw `curl_init`/`curl_exec`, so every outbound
144
+ Talos call is recorded in the api log. Its contract to the two callers is unchanged:
145
+ `array{success: bool, result?: array, error?: string}`.
146
+
147
+ Three deliberate deviations from the usual worker2 `_ApiRequest` idiom — do not "normalize" them:
148
+
149
+ 1. **No `ENCODE__JSON`.** The payload is `json_encode`d by the caller with
150
+ `JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE` and passed as a **ready JSON string with
151
+ payloadEncoding left null**; the response is decoded locally with `json_decode($r, true)`.
152
+ Reasons: (a) `_ApiRequest::execute()` decodes JSON responses with `json_decode($payload)` —
153
+ **no assoc flag** — returning `stdClass`, while both callers index
154
+ `$res['result']['knowledge_doc']` / `$res['result']['recap_json']` as **arrays**;
155
+ (b) `ENCODE__JSON` re-encodes without the unescaped flags, inflating every non-ASCII char of
156
+ a full meeting transcript to `\uXXXX`. Pre-encoding preserves the wire bytes exactly.
157
+ 2. **The local retry loop is kept; `setAutoRetry()` is not used.** `setAutoRetry()` waits a
158
+ **flat** delay and retries **every** non-2xx. This endpoint needs exponential backoff
159
+ (`AI_BASE_DELAY_MS * 2^(n-1)`, `AI_MAX_ATTEMPTS = 2`) and must fail fast on 4xx other than
160
+ 429. So the surrounding `for` loop is retained and **one `_ApiRequest` is constructed per
161
+ attempt** — which also yields one api-log row per attempt, useful for diagnosing Talos
162
+ flakiness.
163
+ 3. **Transport failures arrive as a thrown Exception** from `execute()` (its equivalent of
164
+ `curl_error()`), caught to preserve the same error-array return shape.
165
+
166
+ Timeouts/attempts are class constants: `AI_DEFAULT_TIMEOUT = 600`, `AI_CONNECT_TIMEOUT = 20`,
167
+ `AI_MAX_ATTEMPTS = 2`, `AI_BASE_DELAY_MS`.
168
+
139
169
  ### Organizer email resolution
140
170
  - `resolveOrganizerEmail()` calls Graph `/users/{id}?$select=mail,userPrincipalName` — but
141
171
  this needs the **`User.Read.All`** app permission, which the app registration **lacks**.
@@ -240,12 +270,40 @@ part of the ingestion loop** (raw reads removed). Credential values live only in
240
270
  `bin/sync-knowledge-bases.php`) to discover `development-team-*` KBs from Bedrock and upsert
241
271
  them into `Team.KnowledgeBases`. **Open architectural point:** two competing registries —
242
272
  `Team.KnowledgeBases` (worker2) vs `Client_True.VectorIndexes` (Tools UI).
273
+ - **`initialize()` MUST register the Logs DB under `DB_CLIENT_LOGS` or the Talos api-logging is
274
+ a silent no-op.** `_ApiRequest`'s logging branch writes via `_Model_Client_Logs_Api`, whose
275
+ `DATABASE` const is `_underscore::DB_CLIENT_LOGS`. `Transcripts::initialize()` previously
276
+ registered only `DB_TEAM`, so swapping curl → `_ApiRequest` would have produced **zero** log
277
+ rows. It now also registers `Logs_True` under `DB_CLIENT_LOGS` via `_Config::databaseLogs(...)`
278
+ (same pattern as `Worker/Team/Sprint.php::initialize()`). The general rule is on
279
+ [Creating Worker Actions](./creating-worker-actions.md#gotchas) — other workers hitting the
280
+ same Talos host instead pass `setLogging(false)` deliberately
281
+ (`Worker/Clickup/Fluffer.php`, `Worker/Vapi.php`, `Worker/Ai/Bdr/Vapi.php`).
282
+ - **KNOWN LIMITATION (deliberately NOT fixed — do not patch it locally): long AI calls can leave
283
+ api-log rows with null `responseCode`/`responsePayload`.** `_ApiRequest` logs in two halves —
284
+ it inserts the request row and commits **before** the HTTP call, then `save()`s the response
285
+ fields **after**. With `AI_DEFAULT_TIMEOUT = 600`, that second save runs on a connection idle
286
+ for up to 10 minutes, which is exactly the worker2 stale-connection failure mode. Expected
287
+ symptom: on the slowest transcripts the api-log row has a request but no response. A real fix
288
+ belongs **inside `_ApiRequest::execute()`** (reconnect/re-register before the post-call save)
289
+ and affects every framework caller, so it is deferred pending architecture review. Do **not**
290
+ work around it in `Transcripts.php`.
243
291
  - **Cross-ACCOUNT S3.** `togaiq` (us-east-1) approved/archive writes use the `[talos]` key;
244
292
  `CopyObject` across accounts is impossible, so archive is get(in-memory)+put. (The old
245
293
  toga-private cross-account read is no longer in the loop.)
246
294
 
247
295
  ## Change history
248
296
 
297
+ - 2026-07-28 — **`callTalosEndpoint()` migrated from raw curl to `_ApiRequest`** so both AI
298
+ passes are recorded in the api log; `initialize()` now also registers `Logs_True` under
299
+ `_underscore::DB_CLIENT_LOGS` (without it the logging is a silent no-op). Deliberately kept
300
+ the pre-encoded JSON string (no `ENCODE__JSON` — `execute()` returns `stdClass`, and re-encode
301
+ loses `JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE`) and the local exponential-backoff loop
302
+ (no `setAutoRetry()` — it is flat-delay and retries every non-2xx). Also fixed an undefined
303
+ `$data` in the exhausted-retry error message (it was `print_r`'d before assignment; now uses
304
+ the raw response string) and replaced the hardcoded curl connect timeout with
305
+ `AI_CONNECT_TIMEOUT = 20`. Caller return contract unchanged, so no caller changed.
306
+ `php -l` clean; not committed. (jcardinal)
249
307
  - 2026-07-13 — Recap email **subject-line date** now displays as `n/j/y` (e.g. `7/9/26`)
250
308
  instead of `Y-m-d`, via `DateTime::createFromFormat` with a fallback to the raw
251
309
  `$dateSegment`. Cosmetic — `$dateSegment` (S3 filename, metadata sidecar, AI recap body)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.450",
3
+ "version": "1.0.451",
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",