toga-ai 1.0.507 → 1.0.509
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.
- package/knowledge/1.0/apps/library/INDEX.md +1 -1
- package/knowledge/1.0/apps/library/features/error-capture-1-0.md +47 -5
- package/knowledge/1.0/apps/library/features/toga2-api-client-and-bridge.md +19 -1
- package/knowledge/1.0/apps/tools/INDEX.md +2 -2
- package/knowledge/1.0/apps/tools/features/errors-curation-console.md +57 -0
- package/knowledge/1.0/apps/tools/features/mvc-data-access-patterns.md +35 -1
- package/knowledge/1.0/apps/tools/features/talos-pricing-ui.md +68 -54
- package/knowledge/2.0/apps/_underscore/features/error-reporting-issue-event.md +53 -4
- package/knowledge/2.0/apps/talos/INDEX.md +1 -1
- package/knowledge/2.0/apps/talos/features/pricing-cogs-model.md +73 -3
- package/knowledge/2.0/apps/worker2/INDEX.md +2 -2
- package/knowledge/2.0/apps/worker2/features/error-escalation-cron.md +85 -0
- package/knowledge/2.0/apps/worker2/features/notification-email.md +22 -3
- package/knowledge/2.0/apps/worker2/features/talos-pricing-automation.md +131 -51
- package/knowledge/clients/tow-foundation/profile.md +21 -2
- package/knowledge/clients/true/profile.md +13 -1
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
| [Diagnostic Dialog — View Recommended Services Routing](features/diagnostic-dialog-view-recommended-services.md) | Two "View Recommended Services" buttons exist in the TOGa Refresh 2026 SR view: 1. | library/app/model/toga/diagnostic.php, library/app/model/servicerequest.php |
|
|
9
9
|
| [Elite Freshservice Sync (library)](features/elite-freshservice-sync.md) | `App_Api_Toga2` in `library/app/api/toga2.php` orchestrates bidirectional sync between TOGA 2 and TOGaDesk. | library/app/api/toga2.php |
|
|
10
10
|
| [Branded HTML Email Templates (App_Email_Template)](features/email-templates.md) | `App_Email_Template` (`app/email/template.php`) is the base class for branded HTML emails in the 1.0 (`App_`) framework. | library/app/email/template.php, library/app/email/agilant.php |
|
|
11
|
-
| [Error Capture in 1.0 (App_Error_Capture → shared 2.0 Logs DB)](features/error-capture-1-0.md) | The 1.0 side of the platform error-reporting pipeline (TRUE-78188). | library/app/error/capture.php, library/app/error.php, library/app/exception/business.php, library/app/cloud.php, worker/config.worker.ini, worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php |
|
|
11
|
+
| [Error Capture in 1.0 (App_Error_Capture → shared 2.0 Logs DB)](features/error-capture-1-0.md) | The 1.0 side of the platform error-reporting pipeline (TRUE-78188). | library/app/error/capture.php, library/app/error.php, library/app/exception/business.php, library/app/api/toga2.php, library/app/cloud.php, worker/config.worker.ini, worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php |
|
|
12
12
|
| [1.0 MVC Page Pattern & New-App Skeleton](features/mvc-page-pattern-and-app-skeleton.md) | This is the **reusable recipe for standing up a new 1.0 (`App_`) application** and for adding pages to one — the folder-based MVC routing, the page lifecycle, t | library/app/framework.php, library/app/frameworkindex.php, library/app/mvc.php, library/app/database.php, library/app/model.php, library/app/config.php |
|
|
13
13
|
| [isFulfillable from NetSuite during Item Sync (Phase 1)](features/netsuite-item-isfulfillable-sync.md) | This is the **1.0 (Phase 1)** half of the `isFulfillable` feature: reading the NetSuite `isfulfillable` flag during item sync and stamping it onto the **Agilant | library/app/netsuite.php, library/app/api/toga2.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/backfill_isfulfillable_jul5.php |
|
|
14
14
|
| [NetSuite SuiteQL/REST API Reference](features/netsuite-suiteql-api-reference.md) | General working reference for the Agilant NetSuite integration: how to authenticate, how SuiteQL behaves, and the confirmed schema of the tables/columns/codes w | library/app/api/netsuite/rest.php, library/ssl/netsuite_ec_key.pem, test/@dave/Junk Drawer/nsq.php |
|
|
@@ -12,11 +12,13 @@ files:
|
|
|
12
12
|
- library/app/error/capture.php
|
|
13
13
|
- library/app/error.php
|
|
14
14
|
- library/app/exception/business.php
|
|
15
|
+
- library/app/api/toga2.php
|
|
15
16
|
- library/app/cloud.php
|
|
16
17
|
- worker/config.worker.ini
|
|
17
18
|
- worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php
|
|
18
19
|
related:
|
|
19
20
|
- ../architecture.md
|
|
21
|
+
- ./toga2-api-client-and-bridge.md
|
|
20
22
|
- ../../../2.0/apps/_underscore/features/error-reporting-issue-event.md
|
|
21
23
|
- ../../../2.0/apps/worker2/features/error-escalation-cron.md
|
|
22
24
|
- ../../tools/features/errors-curation-console.md
|
|
@@ -76,13 +78,42 @@ whichever occurrence happened to be seen first — i.e. one arbitrary order numb
|
|
|
76
78
|
|
|
77
79
|
Compass USA's sales-order → MITS transmit rejection was the first business-exception use case.
|
|
78
80
|
|
|
79
|
-
### Client attribution
|
|
81
|
+
### Client attribution — resolved from the API client uuid at `App_Api_Toga2::authenticate()`
|
|
80
82
|
|
|
81
83
|
`App_Error::setCurrentClientId()` exists in 1.0 for parity with 2.0's ambient current-client, but
|
|
82
|
-
**1.0 has no central hook** equivalent to `_Database::registerClientDatabases()`.
|
|
83
|
-
|
|
84
|
-
`
|
|
85
|
-
|
|
84
|
+
**1.0 has no central hook** equivalent to `_Database::registerClientDatabases()`. Because of that,
|
|
85
|
+
the explicit setter was only ever called by **one** hand-edited Compass cron, so
|
|
86
|
+
**every other 1.0 cron recorded `clientId` NULL**. Evidence: `Logs.Event` held **2,093 rows with
|
|
87
|
+
`clientId` NULL** against **107** with a client (all of them 2.0/api2 JWT traffic); issue `1Q`
|
|
88
|
+
traced back to the 1.0 cron `crons/toga2/quad/import_po_and_asn.php`.
|
|
89
|
+
|
|
90
|
+
**`App_Api_Toga2::authenticate()` is 1.0's true choke point.** `send()` authenticates before every
|
|
91
|
+
call, and every 1.0→2.0 cron arrives there holding its `UUID_CLIENT`. It now calls
|
|
92
|
+
`App_Error_Capture::setCurrentClientUuid($uuid)` — a **store only, no lookup**, because
|
|
93
|
+
`authenticate()` is a happy-path function and must not take on a query.
|
|
94
|
+
|
|
95
|
+
`App_Error_Capture` then resolves uuid → `Core.Clients.id` **lazily**, only when an error is
|
|
96
|
+
actually captured, cached per process (`resolveClientIdFromUuid()`, private). It walks
|
|
97
|
+
**`CORE_DATABASE_LINK_CANDIDATES = ['db_toga2core', 'db_prod_toga2core', 'db_core']`** because 1.0
|
|
98
|
+
apps register the same Core connection under different link names. **Every failure path returns
|
|
99
|
+
`null` and records no client rather than throwing.**
|
|
100
|
+
|
|
101
|
+
Precedence is unchanged: an explicit `clientId` argument wins, then an explicit
|
|
102
|
+
`setCurrentClientId()`, then the ambient uuid.
|
|
103
|
+
|
|
104
|
+
> **⚠ The resolution must run INSIDE the existing
|
|
105
|
+
> `App_Error::setThrowExceptionsEnabled(false)` guard.** An unconfigured DB link **warns** before
|
|
106
|
+
> it fails, and in 1.0 a warning outside that guard terminates the cron (see the first gotcha
|
|
107
|
+
> below). Do not hoist the lookup out of the guarded block.
|
|
108
|
+
|
|
109
|
+
### The reference encoder is a byte-identical mirror of 2.0's
|
|
110
|
+
|
|
111
|
+
`capture.php` carries its own copy of `encodeReference()` plus `REFERENCE_ALPHABET`
|
|
112
|
+
(`'0123456789ACDEFGHJKMNPQRSTUVWXYZ'`) and `REFERENCE_OFFSET` (`128`), matching
|
|
113
|
+
`_Model_Core_Logs_Issue` **byte for byte**. Both write the same **UNIQUE** `Issue.reference`
|
|
114
|
+
column in the same shared table, so a divergence between them is a failed INSERT and a **lost**
|
|
115
|
+
error. Change one, change both, deploy together. Rationale for the alphabet and the offset lives in
|
|
116
|
+
the [2.0 doc](../../../2.0/apps/_underscore/features/error-reporting-issue-event.md#the-quotable-reference--base-32-over-an-unambiguous-alphabet-reworked-2026-08-04).
|
|
86
117
|
|
|
87
118
|
### Fingerprint fallback (no usable stack)
|
|
88
119
|
|
|
@@ -151,6 +182,17 @@ exists in `api2`/`worker2`. So 1.0 reads the AL1 container config at
|
|
|
151
182
|
|
|
152
183
|
## Change history
|
|
153
184
|
|
|
185
|
+
- 2026-08-04 (later, **uncommitted/undeployed** at time of writing) — **Fixed: 1.0 errors recorded
|
|
186
|
+
`clientId` NULL on every cron but one** (2,093 NULL `Logs.Event` rows vs 107 with a client; issue
|
|
187
|
+
`1Q` from `crons/toga2/quad/import_po_and_asn.php`). `App_Api_Toga2::authenticate()` — 1.0's real
|
|
188
|
+
choke point, since `send()` authenticates before every call — now records the ambient client uuid
|
|
189
|
+
(store only, no lookup), and `App_Error_Capture` resolves uuid → `Core.Clients.id` lazily and
|
|
190
|
+
per-process on capture, walking `CORE_DATABASE_LINK_CANDIDATES`
|
|
191
|
+
(`db_toga2core`/`db_prod_toga2core`/`db_core`) because 1.0 apps name the Core link differently;
|
|
192
|
+
every failure path returns null rather than throwing, and the lookup deliberately runs inside the
|
|
193
|
+
`setThrowExceptionsEnabled(false)` guard because an unconfigured link *warns* before it fails.
|
|
194
|
+
Also mirrored 2.0's **base-32 reference encoder** (`REFERENCE_ALPHABET` + `REFERENCE_OFFSET = 128`)
|
|
195
|
+
byte-for-byte. (jcardinal)
|
|
154
196
|
- 2026-08-04 — Built the 1.0 side of TRUE-78188: `App_Error_Capture` writing occurrences into the
|
|
155
197
|
shared 2.0 `Logs` DB (opt-in via `[database_toga2logs]`, hand-written escaped SQL);
|
|
156
198
|
`App_Exception_Business` with a fingerprint string byte-identical to 2.0's so one `issueKey`
|
|
@@ -6,7 +6,7 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-04
|
|
10
10
|
owners: [jcardinal, mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/api/toga2.php
|
|
@@ -22,6 +22,7 @@ related:
|
|
|
22
22
|
- netsuite-suiteql-rest-shim.md
|
|
23
23
|
- ../../worker/features/netsuite-togasupply-per-client-sync.md
|
|
24
24
|
- ../architecture.md
|
|
25
|
+
- ./error-capture-1-0.md
|
|
25
26
|
---
|
|
26
27
|
|
|
27
28
|
## Summary
|
|
@@ -77,6 +78,18 @@ belong to the NetSuite importer — documented in the per-client-sync doc, not h
|
|
|
77
78
|
- Returns the decoded response object; callers read `->data->{resource}`, `->meta->nextPage`,
|
|
78
79
|
`->isSuccess`, `->status`, `->messages[].code`.
|
|
79
80
|
|
|
81
|
+
> **`authenticate()` is also 1.0's ambient-client choke point for error reporting** (added
|
|
82
|
+
> 2026-08-04). Every 1.0→2.0 cron passes through it holding its `UUID_CLIENT`, so it calls
|
|
83
|
+
> `App_Error_Capture::setCurrentClientUuid()` — **store only, no lookup**: this is a happy-path
|
|
84
|
+
> function and must not acquire a query. The uuid → `Core.Clients.id` resolution happens lazily
|
|
85
|
+
> inside error capture. Keep it that way; see
|
|
86
|
+
> [error-capture-1-0](./error-capture-1-0.md).
|
|
87
|
+
|
|
88
|
+
> **Latent, pre-existing, uninvestigated:** several `self::$togaClientUuid` references sit in
|
|
89
|
+
> methods of this class (roughly lines 2880–4250) with **no such property declared** — an
|
|
90
|
+
> undefined-property bug waiting to fire (and in 1.0, a warning inside an unguarded path
|
|
91
|
+
> terminates the cron). Not introduced by, and not fixed by, the 2026-08-04 error-reporting work.
|
|
92
|
+
|
|
80
93
|
### Options DSL (how 1.0 expresses 2.0 queries)
|
|
81
94
|
|
|
82
95
|
`$options` is an assoc array assembled into the 2.0 query string:
|
|
@@ -340,6 +353,11 @@ enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
|
|
|
340
353
|
|
|
341
354
|
## Change history
|
|
342
355
|
|
|
356
|
+
- 2026-08-04 (**uncommitted/undeployed** at time of writing) — `authenticate()` now records the
|
|
357
|
+
ambient client uuid for error reporting (`App_Error_Capture::setCurrentClientUuid()`, store-only)
|
|
358
|
+
because it is the one place every 1.0→2.0 cron passes through holding its `UUID_CLIENT` — this is
|
|
359
|
+
what fixed 1.0 errors recording `clientId` NULL. Also noted the pre-existing undeclared
|
|
360
|
+
`self::$togaClientUuid` references (~L2880–4250) as a latent undefined-property bug. (jcardinal)
|
|
343
361
|
- 2026-08-03 (later pass) — TRUE-79401 **static regression harness + one correction.** BUILT a
|
|
344
362
|
no-DB PHP harness (`test/@Mark/AIG/test_multi_email.php`, 12 cases, PHP 7.2.33, `php -l` clean)
|
|
345
363
|
that feeds mocked `/contacts` fragments through the email transform and asserts the delimited
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
| [Tools — Developers Folder (UUID & Password Generators)](features/developer-tools.md) | The first two tools shipped in the Tools app, both under the **Developers** folder and gated to personas **Development Team** / **TOGa Technology**. | tools/mvc/developers/uuid/get.php, tools/mvc/developers/password/get.php |
|
|
10
10
|
| [/errors Curation Console (Tools → shared Core Logs DB)](features/errors-curation-console.md) | Internal-only triage/curation screen for the 2.0 Issue/Event error-reporting pipeline, built as a 1.0 Tools MVC page reading the **shared Core Logs DB** through | tools/mvc/errors/get.php, tools/mvc/errors/post.php, tools/mvc/errors/issue/get.php, tools/assets/css/style.css, tools/_/app/nav.php, tools/config.production.ini |
|
|
11
11
|
| [Legacy Email Notifier (tools /email-migration/notify)](features/legacy-email-notifier.md) | An SSO-gated admin tool at **`/email-migration/notify`** (nav group **Email Migration** > **Legacy Notifier**, personas `['TOGa Technology','Development Team']` | tools/mvc/email-migration/notify/get.php, tools/mvc/email-migration/notify/post.php, tools/_/app/nav.php |
|
|
12
|
-
| [Tools MVC — Routing, CSRF & App_Database Access Patterns](features/mvc-data-access-patterns.md) | The load-bearing 1.0 (`App_`) framework conventions a developer needs when adding a page to the Tools app — URL routing, CSRF, and DB access through `App_Databa | tools/_/app/nav.php, tools/mvc/get.php, library/app/error.php |
|
|
12
|
+
| [Tools MVC — Routing, CSRF & App_Database Access Patterns](features/mvc-data-access-patterns.md) | The load-bearing 1.0 (`App_`) framework conventions a developer needs when adding a page to the Tools app — URL routing, CSRF, and DB access through `App_Databa | tools/_/app/nav.php, tools/mvc/get.php, library/app/error.php, library/app/database.php |
|
|
13
13
|
| [Tools Persona-Gated Navigation (App_Nav)](features/persona-gated-navigation.md) | `App_Nav` is the Tools app's two-level, **persona-gated** navigation. | tools/_/app/nav.php, tools/mvc/get.php |
|
|
14
14
|
| [Tools SAML SSO Consumer & Persona-Gated Auth (App_Auth)](features/saml-sso-auth.md) | `App_Auth` is the Tools app's authentication layer: it consumes the SAML gateway `?saml=` handoff (see the 2.0 SAML downstream integration contract), establishe | tools/_/app/auth.php, tools/mvc/sso/initiate/get.php, tools/mvc/sso/get.php, tools/mvc/login/get.php, tools/mvc/login/post.php, tools/mvc/logout/get.php, tools/mvc/get.php, tools/config.production.ini, tools/config.local.ini |
|
|
15
15
|
| [Talos Knowledge Base Admin UI (KB Documents + Vocabulary)](features/talos-kb-documents-admin.md) | > **PER-AI-MODEL, DATA-DRIVEN SCOPING (2026-07-29).** The KB-documents and Vocabulary admin > UIs were refactored from a single hard-coded **"development-team"* | tools/mvc/talos/kb-documents/get.php, tools/mvc/talos/kb-documents/post.php, tools/mvc/talos/knowledge-bases/get.php, tools/mvc/talos/knowledge-bases/post.php, tools/mvc/talos/vocabulary/get.php, tools/mvc/talos/vocabulary/post.php, tools/_/app/talos/s3.php, tools/_/app/talos/bedrock.php, tools/_/app/pg.php, tools/_/app/model/true/aimodels.php, tools/_/app/model/true/vectorindexes.php, tools/_/app/model/true/aimodels_vectorindexes.php, tools/_/app/worker.php, tools/_/app/nav.php, tools/config.production.ini, tools/config.alpha.ini, tools/.platform/httpd/conf.d/timeouts.conf, tools/.platform/hooks/prebuild/01-install-php-pgsql.sh, tools/.platform/hooks/postdeploy/01-restart-php.sh |
|
|
16
|
-
| [Talos Pricing UI (
|
|
16
|
+
| [Talos Pricing UI (Contracts, Pricing Dashboard, Usage, Settings + Estimator)](features/talos-pricing-ui.md) | The 1.0 (tools app) face of the **Talos Pricing Platform** — a "Talos Pricing" nav folder with four pages plus the estimate engine. | tools/_/app/nav.php, tools/_/app/talos/estimator.php, tools/_/app/talos/usage.php, tools/mvc/talos/contracts/get.php, tools/mvc/talos/contracts/post.php, tools/mvc/talos/pricing/get.php, tools/mvc/talos/usage/get.php, tools/mvc/talos/settings/get.php, tools/mvc/talos/settings/post.php, tools/assets/css/style.css |
|
|
17
17
|
| [Deploying Tools to Elastic Beanstalk (PHP 8.5 / Amazon Linux 2023)](workflows/deploy-to-elastic-beanstalk-al2023.md) | How the **Tools** 1.0 app boots on Elastic Beanstalk running `PHP 8.5 on 64bit Amazon Linux 2023/4.13.1 (aarch64)`. | tools/.ebextensions/004_http_to_https.config, tools/.ebextensions/006_mount-s3fs.config, tools/.ebextensions/007_setup_export_cache_folders.config, tools/.ebextensions/008_setup_ldap.config, tools/.ebextensions/009_setup_phpini.config, tools/.ebextensions/020_setup_git_libraries.config, tools/.ebextensions/050_register_instance_to_shared_application_load_balancer.config, tools/ebs/git.json, tools/ebs/setup_export_cache_folders.php, tools/_/app/frameworkindex.php |
|
|
@@ -72,6 +72,48 @@ curated text, affected clients) right, with an always-present Type chip.
|
|
|
72
72
|
time.
|
|
73
73
|
- Hourly-occurrence chart; fingerprints table re-sized for the narrow column with an icon-only
|
|
74
74
|
Move button.
|
|
75
|
+
- Header chip order is **Issue / Environment / Type / Urgency** — Type was moved ahead of Urgency so
|
|
76
|
+
urgency finishes the row at the far right.
|
|
77
|
+
|
|
78
|
+
### Table presentation rules (established 2026-08-04)
|
|
79
|
+
|
|
80
|
+
**Stacked datetimes in every table on both pages** — listing *Last seen*; detail *Events → When*,
|
|
81
|
+
*Affected clients → Last*, *ClickUp episodes → Opened/Resolved*, *Fingerprints → First seen*. A
|
|
82
|
+
shared `$datetime` closure emits
|
|
83
|
+
`<span class="dt"><span class="dt-date">…</span><span class="dt-time">…</span></span>`, each part
|
|
84
|
+
`nowrap`, split on the single space MySQL puts between date and time; a blank value renders an em
|
|
85
|
+
dash. Tabular figures keep digits aligned down the column.
|
|
86
|
+
|
|
87
|
+
> **Why:** letting a datetime wrap naturally let the browser pick its own break point, and the point
|
|
88
|
+
> it picked was **inside the day of the month** (`2026-08-` / `04`). This replaced the old
|
|
89
|
+
> `.wrap-date` / `.wrap-anywhere` approach, whose now-dead CSS rule was removed.
|
|
90
|
+
|
|
91
|
+
**The summary column has a 100px floor via table overflow, not a cell `min-width`.** The summary
|
|
92
|
+
`<col>` is deliberately left **unwidthed** so it absorbs leftover space — which is also why *every
|
|
93
|
+
other* column must declare a width, or `col-type` absorbs it instead. The floor comes from
|
|
94
|
+
`.issue-listing { min-width: 1026px }` inside a new `.table-scroll { overflow-x: auto }` wrapper.
|
|
95
|
+
|
|
96
|
+
> **⚠ Under `table-layout: fixed` a browser compresses ALL columns proportionally at narrow
|
|
97
|
+
> viewports and ignores `min-width` on a cell.** A cell-level `min-width` therefore cannot work —
|
|
98
|
+
> the table has to overflow and scroll. `col-when` narrowed 168px → 104px now that dates stack (the
|
|
99
|
+
> 64px went to the summary), and a duplicate `.col-type` rule was removed in favour of the
|
|
100
|
+
> pre-existing 104px one. **The pager sits outside the scroll wrapper.**
|
|
101
|
+
|
|
102
|
+
**Whole-row click replaced the two per-cell links** (Issue ID and Summary). The `<tr>` carries
|
|
103
|
+
`class="row-link"`, `data-href`, `tabindex="0"` and an `aria-label`; `.issue-id-link` was renamed
|
|
104
|
+
`.issue-id` (keeping the accent colour as the affordance) and `tr.row-link` got cursor/hover/
|
|
105
|
+
`focus-visible` rules.
|
|
106
|
+
|
|
107
|
+
This page was deliberately **JavaScript-free**, and this is the one thing that cannot be done
|
|
108
|
+
without it: no element but an anchor is clickable, and an anchor cannot wrap a `<tr>`. A small
|
|
109
|
+
**delegated** click+keydown handler was added. Ctrl/Cmd/Shift-click still opens a new tab;
|
|
110
|
+
Enter/Space work because removing the anchors removed the tab stop; clicks on `a`, `button`,
|
|
111
|
+
`input`, `select`, `textarea`, `summary` or `label` are ignored so future in-row controls keep their
|
|
112
|
+
own behaviour.
|
|
113
|
+
|
|
114
|
+
> **Known trade-off:** a screen reader now announces these as **labelled rows, not links**.
|
|
115
|
+
> `role="link"` was deliberately **not** applied, because it would stop the element being a table
|
|
116
|
+
> row.
|
|
75
117
|
|
|
76
118
|
**This page notifies nobody.** Escalation, ClickUp ticketing, and email are owned entirely by
|
|
77
119
|
`_Worker_Infrastructure_Errors::Escalate`. Adding a notify action here would create a second,
|
|
@@ -128,6 +170,21 @@ They were renamed from plural on 2026-08-01, after the pipeline was already in p
|
|
|
128
170
|
|
|
129
171
|
## Change history
|
|
130
172
|
|
|
173
|
+
- 2026-08-04 (later, **uncommitted/undeployed** at time of writing) — UI pass on both `/errors`
|
|
174
|
+
pages: **stacked datetimes** in every table via a shared `$datetime` closure emitting
|
|
175
|
+
`.dt`/`.dt-date`/`.dt-time` (a naturally wrapping datetime broke inside the day of the month), and
|
|
176
|
+
the dead `.wrap-date`/`.wrap-anywhere` rules removed; a **100px floor on the summary column**
|
|
177
|
+
delivered as `.issue-listing { min-width: 1026px }` inside a new `.table-scroll` overflow wrapper
|
|
178
|
+
(with the pager outside it), because `table-layout: fixed` ignores a cell `min-width` and
|
|
179
|
+
compresses all columns proportionally — `col-when` narrowed 168px → 104px and a duplicate
|
|
180
|
+
`.col-type` rule removed; the two per-cell links replaced by a **whole-row click**
|
|
181
|
+
(`tr.row-link` + `data-href` + `tabindex` + `aria-label` and one small delegated click/keydown
|
|
182
|
+
handler — the single unavoidable use of JS on this deliberately JS-free page, since an anchor
|
|
183
|
+
cannot wrap a `<tr>`), honouring Ctrl/Cmd/Shift-click and ignoring clicks on interactive
|
|
184
|
+
descendants, with the accepted trade-off that screen readers announce labelled rows rather than
|
|
185
|
+
links (`role="link"` would stop the element being a row); and issue-detail header order changed to
|
|
186
|
+
Issue / Environment / Type / Urgency. Note **`library` and `tools` must both deploy** for any of
|
|
187
|
+
this to be visible. (jcardinal)
|
|
131
188
|
- 2026-08-04 — Console build-out: the listing gained real pagination (25/page with a filtered
|
|
132
189
|
`COUNT`, replacing a silent `LIMIT 200`), an EB Environment column **and filter**, a
|
|
133
190
|
BUSINESS/TECHNICAL Type column derived from `issueKey` (no new DB column), and a 3-column filter
|
|
@@ -6,12 +6,13 @@ project: Tools
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-04
|
|
10
10
|
owners: [jcardinal, bala]
|
|
11
11
|
files:
|
|
12
12
|
- tools/_/app/nav.php
|
|
13
13
|
- tools/mvc/get.php
|
|
14
14
|
- library/app/error.php
|
|
15
|
+
- library/app/database.php
|
|
15
16
|
related:
|
|
16
17
|
- ./persona-gated-navigation.md
|
|
17
18
|
- ./saml-sso-auth.md
|
|
@@ -71,6 +72,38 @@ $res = App_Database::query($sql, 'db_team');
|
|
|
71
72
|
$rows = App_Database::buildArrayOfRows($res, false);
|
|
72
73
|
```
|
|
73
74
|
|
|
75
|
+
### Gotcha — `fetchOne()` CANNOT be used as an existence test (library-wide footgun)
|
|
76
|
+
`App_Database::fetchOne()` (`library/app/database.php:335-340`) does `mysqli_data_seek()` and
|
|
77
|
+
then returns `$row[$col]` with **no empty-result check**. On zero rows it dereferences `null`,
|
|
78
|
+
and the 1.0 error handler escalates that to an uncaught `ErrorException` — a bare **500**.
|
|
79
|
+
This affects **every 1.0 app**, not just Tools.
|
|
80
|
+
|
|
81
|
+
```php
|
|
82
|
+
// WRONG — 500s whenever the row does not exist (i.e. exactly the case you're testing for)
|
|
83
|
+
$res = App_Database::query("SELECT id FROM TalosClients WHERE slug = '$slug'", 'db_team');
|
|
84
|
+
if (App_Database::fetchOne($res)) { ... }
|
|
85
|
+
|
|
86
|
+
// CORRECT — COUNT(*) always returns exactly one row
|
|
87
|
+
$res = App_Database::query("SELECT COUNT(*) FROM TalosClients WHERE slug = '$slug'", 'db_team');
|
|
88
|
+
$exists = (int) App_Database::fetchOne($res) > 0;
|
|
89
|
+
|
|
90
|
+
// CORRECT — or materialise and check
|
|
91
|
+
$rows = App_Database::buildArrayOfRows($res, false);
|
|
92
|
+
if (!empty($rows)) { ... }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Gotcha — Tools runs PHP 8.5, where a deprecation is a fatal
|
|
96
|
+
The framework escalates deprecations (see the error-handler gotcha below), and **"Using null
|
|
97
|
+
as an array offset" is deprecated in PHP 8.5 even inside `isset()`**. So
|
|
98
|
+
`$arr[$row['nullableColumn']]` is a **fatal**, not a notice. Cast before indexing:
|
|
99
|
+
|
|
100
|
+
```php
|
|
101
|
+
$bucket = $arr[(string) $row['nullableColumn']] ?? null;
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Local dev on PHP 8.2 does not reproduce this.** 8.5-strictness is a hard requirement for
|
|
105
|
+
this app, not a warning — test against it before shipping.
|
|
106
|
+
|
|
74
107
|
## Gotcha — a page CANNOT set HTTP status/headers (preloader already flushed)
|
|
75
108
|
`App_FrameworkIndex::render()` turns on the preloader and calls `App_Page::flushCapture()`
|
|
76
109
|
(`ob_end_flush + flush`, `library/app/page.php:213`) **before** `body() → App_MVC::loadFile()` runs
|
|
@@ -109,6 +142,7 @@ set_error_handler(fn(): bool => true); // swallow non-fatal notices/deprecations
|
|
|
109
142
|
`App_Talos_Estimator` → `_/app/talos/estimator.php`.
|
|
110
143
|
|
|
111
144
|
## Change history
|
|
145
|
+
- 2026-08-04 — Added two gotchas found rebuilding the Talos Pricing UI: (1) `App_Database::fetchOne()` is **not** an existence test — it `data_seek`s and returns `$row[$col]` with no empty check, so zero rows dereferences null → uncaught `ErrorException` → bare 500; a **library-wide** footgun affecting every 1.0 app (use `COUNT(*)` or `buildArrayOfRows` + `empty()`). (2) Tools runs **PHP 8.5**, where "using null as an array offset" is deprecated even inside `isset()` and the framework escalates deprecations to fatals — cast to `(string)` before indexing; PHP 8.2 local dev does not reproduce it. (jcardinal)
|
|
112
146
|
- 2026-08-03 — Added the error-handler escalation gotcha: `App_Error::handleError` promotes ANY PHP
|
|
113
147
|
notice/deprecation to a thrown `ErrorException` (ignoring `error_reporting`) and, without Sentry,
|
|
114
148
|
echoes "Sentry is not installed…" into the response — corrupting JSON on AJAX endpoints; workaround
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Talos Pricing UI (
|
|
2
|
+
title: Talos Pricing UI (Contracts, Pricing Dashboard, Usage, Settings + Estimator)
|
|
3
3
|
framework: "1.0"
|
|
4
4
|
repo: tools
|
|
5
5
|
project: Tools
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-04
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- tools/_/app/nav.php
|
|
13
13
|
- tools/_/app/talos/estimator.php
|
|
14
|
-
- tools/
|
|
15
|
-
- tools/mvc/talos/
|
|
14
|
+
- tools/_/app/talos/usage.php
|
|
15
|
+
- tools/mvc/talos/contracts/get.php
|
|
16
|
+
- tools/mvc/talos/contracts/post.php
|
|
16
17
|
- tools/mvc/talos/pricing/get.php
|
|
17
|
-
- tools/mvc/talos/
|
|
18
|
-
- tools/mvc/talos/
|
|
19
|
-
- tools/mvc/talos/
|
|
18
|
+
- tools/mvc/talos/usage/get.php
|
|
19
|
+
- tools/mvc/talos/settings/get.php
|
|
20
|
+
- tools/mvc/talos/settings/post.php
|
|
20
21
|
- tools/assets/css/style.css
|
|
21
22
|
related:
|
|
22
23
|
- ./persona-gated-navigation.md
|
|
@@ -30,65 +31,78 @@ related:
|
|
|
30
31
|
## Summary
|
|
31
32
|
|
|
32
33
|
The 1.0 (tools app) face of the **Talos Pricing Platform** — a "Talos Pricing" nav folder
|
|
33
|
-
with four pages plus
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
added by the developer separately).
|
|
34
|
+
with four pages plus the estimate engine. Restructured 2026-08-03 to
|
|
35
|
+
**Contracts / Pricing Dashboard / Usage / Pricing Settings**. All pages read the **Team DB**
|
|
36
|
+
through the `db_team` connection (`[database_team]` in `config.*.ini`).
|
|
37
37
|
|
|
38
38
|
## How it works
|
|
39
39
|
|
|
40
|
-
###
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
`personasForRoute()` returns the *union* of folder+action personas, the Factors page must
|
|
45
|
-
**narrow** explicitly with its own `requireAuth([technical])` rather than trusting
|
|
46
|
-
`personasForRoute()`.
|
|
40
|
+
### Contracts (`mvc/talos/contracts/{get,post}.php`) — replaces `onboarding/*`
|
|
41
|
+
The old Onboarding page could only **INSERT** (hardcoded `status='ONBOARDED'`, no edit, no
|
|
42
|
+
delete), which is why nothing ever reached the tables. Contracts is a real lifecycle, reusing
|
|
43
|
+
the existing status enum: **PROSPECT** = draft, **ACTIVE** = signed, **CHURNED** = ended.
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
- **A signed contract can never be hard-deleted.** `TalosPricingBands` is
|
|
46
|
+
`ON DELETE CASCADE`, so deleting the client would destroy the agreed rate schedule and the
|
|
47
|
+
margin history with it. Signed contracts **churn**; they do not delete.
|
|
48
|
+
- **Post-signature edits append to `TalosContractAmendments`** rather than mutating the
|
|
49
|
+
signed terms.
|
|
50
|
+
- Includes an **Actual-vs-Estimate** comparison with variance chips, a month-by-month history
|
|
51
|
+
table plus cost/margin chart, and a one-click **"apply recommended org fee"**.
|
|
52
|
+
|
|
53
|
+
### Pricing Dashboard (`pricing/get.php`) — rebuilt for sales leadership
|
|
54
|
+
Portfolio KPIs, a 12-month blended-margin trend with the target band shaded, an exception
|
|
55
|
+
list with deterministic explanations, and licensed-vs-active utilisation.
|
|
54
56
|
|
|
55
|
-
###
|
|
56
|
-
|
|
57
|
-
org fee / consecutive-months smoothing window. Live JS shows the estimate and recommended
|
|
58
|
-
per-band fees; fees are editable. **"Sign contract" locks the band schedule** and writes
|
|
59
|
-
`TalosClients` + `TalosPricingBands` in a single transaction. Write goes through
|
|
60
|
-
`mvc/talos/onboarding/post.php` (the App_MVC `post.php` convention — see
|
|
61
|
-
`mvc-data-access-patterns`).
|
|
57
|
+
### Usage (`usage/get.php`, `_/app/talos/usage.php`)
|
|
58
|
+
Adds `costIntensity()` and a **Cost intensity** panel (tool-mix driven cost).
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
band the per-user fee auto-steps to the pre-agreed rate, then the org fee is adjusted to
|
|
67
|
-
restore margin. The per-user fee never changes outside the agreed schedule. (Full
|
|
68
|
-
methodology + COGS economics in the talos `pricing-cogs-model` doc.)
|
|
60
|
+
**Fixed:** `clientOptions()` read `TalosClients WHERE status='ONBOARDED'` from an
|
|
61
|
+
always-empty table, so the client selector rendered **empty**. It now calls
|
|
62
|
+
`GET /api/usage/clients` with a Team-DB fallback.
|
|
69
63
|
|
|
70
|
-
###
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
### Pricing Settings (`settings/{get,post}.php`) — replaces `factors/*`
|
|
65
|
+
Technical-only (persona-narrowed on the action, per `mvc-data-access-patterns`). **Measured
|
|
66
|
+
(derived) cost-factor rows are worker-owned and read-only**, enforced server-side with
|
|
67
|
+
`UPDATE ... AND isDerived = 0`. Policy rows are editable and carry full-sentence labels.
|
|
73
68
|
|
|
74
|
-
###
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
-
|
|
78
|
-
|
|
69
|
+
### Removed
|
|
70
|
+
- `mvc/talos/benchmarks/get.php` — read the never-populated `TalosUsageMonthly` and
|
|
71
|
+
duplicated a worse subset of the Usage page.
|
|
72
|
+
- `mvc/talos/onboarding/*` and `mvc/talos/factors/*` — superseded above.
|
|
73
|
+
|
|
74
|
+
### Estimator (`App_Talos_Estimator`, `_/app/talos/estimator.php`)
|
|
75
|
+
Now driven by the **token unit price** and **workload profiles** instead of the feature
|
|
76
|
+
checklist and per-client calibration factor (methodology in `pricing-cogs-model`). Service
|
|
77
|
+
offering (CHAT / VOICE_TO_VOICE / NATURAL_VOICE) describes **modality only** and says nothing
|
|
78
|
+
about cost; cost is driven by tool-call **intensity** via the profile
|
|
79
|
+
(retrieval / general / analytics).
|
|
79
80
|
|
|
80
81
|
## Gotchas
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
|
|
83
|
+
- **`App_Database::fetchOne()` is not an existence test** — it 500s on zero rows. See
|
|
84
|
+
[mvc-data-access-patterns](./mvc-data-access-patterns.md); the original
|
|
85
|
+
`onboarding/post.php` carried this exact bug and would have fataled on the first save
|
|
86
|
+
anyone attempted, which is good evidence nobody ever completed an onboarding through it.
|
|
87
|
+
- **This app runs PHP 8.5 and escalates deprecations to fatals** — `$arr[$row['nullableCol']]`
|
|
88
|
+
is fatal even inside `isset()`. Local dev on PHP 8.2 does **not** reproduce it. Cast to
|
|
89
|
+
`(string)` before indexing. See `mvc-data-access-patterns`.
|
|
90
|
+
- **`.tool--wide` did nothing.** `assets/css/style.css` documented the class as escaping the
|
|
91
|
+
960px `.app-content` cap "via `:has()`", but that rule was never written. Added
|
|
92
|
+
`.app-content:has(.tool--wide) { max-width: 100% }` — which also makes the pre-existing
|
|
93
|
+
`mvc/errors` pages genuinely full-width for the first time.
|
|
94
|
+
- **Put Talos page CSS in `style.css`, not a page-local `<style>` block.** The Contracts page
|
|
95
|
+
**returns early** for list mode, so a `<style>` block placed after that return applied to
|
|
96
|
+
the calculator only and the list rendered with no CSS at all.
|
|
97
|
+
- **Voice multipliers (0.25 / 0.50) are UNVALIDATED** — supplied second-hand, never measured,
|
|
98
|
+
and possibly inverted. Flagged in the UI. **Do not quote voice from them.**
|
|
99
|
+
- **`db_team`** is read/write on Contracts and Settings, read-only on the dashboards.
|
|
87
100
|
|
|
88
101
|
## Security
|
|
89
|
-
- `config.*.ini`
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
- `config.*.ini` carries committed plaintext secrets (team-accepted, see architecture Known
|
|
103
|
+
issues) — **all still owe rotation**. Config keys referenced by name only
|
|
104
|
+
(`[api] talos_backend_url` / `talos_backend_key`); no values recorded. Do not add secrets.
|
|
92
105
|
|
|
93
106
|
## Change history
|
|
107
|
+
- 2026-08-04 — Restructured to **Contracts / Pricing Dashboard / Usage / Pricing Settings**. New Contracts pages replace Onboarding with a real PROSPECT/ACTIVE/CHURNED lifecycle (signed contracts churn, never hard-delete — `TalosPricingBands` cascades; post-signature edits append to `TalosContractAmendments`), plus actual-vs-estimate variance, month history chart and one-click apply-recommended-org-fee. Settings replaces the Cost Factors editor with measured rows read-only (`UPDATE ... AND isDerived = 0`). Benchmarks page deleted. Pricing Dashboard rebuilt for sales leadership. Fixed `usage.php clientOptions()` reading an always-empty `TalosClients` (selector rendered empty) → `GET /api/usage/clients` with Team-DB fallback; added cost-intensity panel. Estimator moved to token unit price + workload profiles. Wrote the missing `.app-content:has(.tool--wide)` rule and consolidated Talos CSS into `style.css`. (jcardinal)
|
|
94
108
|
- 2026-06-29 — Built the Talos Pricing UI: nav folder + 4 pages (Onboarding w/ live JS estimate and contract-signing that locks the band schedule into `TalosClients`+`TalosPricingBands` in one txn; read-only Pricing Dashboard + Usage Benchmarks; technical-only Cost Factors editor) and `App_Talos_Estimator` (reads `TalosCostFactors`, derives conversations/user so sales need not enter it). Reads Team DB via new `db_team` connection. Offerings CHAT/VOICE_TO_VOICE/NATURAL_VOICE = 1.0/0.25/0.50 (voice ambiguous, deferred). (jcardinal)
|
|
@@ -190,15 +190,47 @@ A uniform shape is what lets a viewer render context **generically** (see the To
|
|
|
190
190
|
card/JSON-tree renderer). Consumers must read **both** the old and new key positions during the
|
|
191
191
|
7-day `Event` retention overlap.
|
|
192
192
|
|
|
193
|
-
### The quotable reference
|
|
193
|
+
### The quotable reference — base-32 over an unambiguous alphabet (reworked 2026-08-04)
|
|
194
194
|
|
|
195
|
-
`
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
`Issue.reference` encodes `Issue.id` in **base 32** over
|
|
196
|
+
`REFERENCE_ALPHABET = '0123456789ACDEFGHJKMNPQRSTUVWXYZ'`, most-significant character first, no
|
|
197
|
+
padding (`_Model_Core_Logs_Issue::encodeReference()`). **All ten digits are kept; `O`, `I`, `L`
|
|
198
|
+
and `B` are removed** because in the contexts a reference actually gets used — read aloud on a
|
|
199
|
+
call, typed from a screenshot, pasted into a ClickUp title — they impersonate `0`, `1`, `1` and
|
|
200
|
+
`8`. A reference exists to be quoted, so ambiguity is the one defect it cannot have.
|
|
201
|
+
|
|
202
|
+
It replaced a decimal-counter + letter, base-26 scheme (`1A`, `2Q`, `3O`).
|
|
203
|
+
|
|
204
|
+
> **⚠ `REFERENCE_OFFSET = 128` exists to stop the new scheme from OVERWRITING the old one —
|
|
205
|
+
> do not remove it.** `Issue.reference` is **UNIQUE**, and every legacy value begins `1`–`3`.
|
|
206
|
+
> With no offset, new id 75 encodes to `2A`, which id 27 already holds — the INSERT fails and the
|
|
207
|
+
> error is **lost instead of recorded**, precisely the failure mode this pipeline exists to
|
|
208
|
+
> prevent. The offset burns the first 128 (= 4 × 32) code points so every new reference starts at
|
|
209
|
+
> `4` or later and both generations coexist forever. Verified by script over 200,000 ids: no
|
|
210
|
+
> collisions among themselves, none against the legacy values, no banned characters.
|
|
211
|
+
|
|
212
|
+
The column is `varchar(16)`, so the growth to 3 characters (≈ id 897) and 4 (≈ id 29,000) cannot
|
|
213
|
+
truncate. Because `AUTO_INCREMENT` never reuses ids, gaps are expected and permanent — a reference
|
|
214
|
+
can never be recycled onto a different problem.
|
|
215
|
+
|
|
216
|
+
**The 1.0 encoder in `library/app/error/capture.php` must stay byte-identical** — both write the
|
|
217
|
+
same UNIQUE column in the same shared table. Change one, change the other, deploy together.
|
|
198
218
|
|
|
199
219
|
The reference is written by PHP in a second statement (see MySQL constraints below), which is
|
|
200
220
|
why the column is nullable even though it is always populated.
|
|
201
221
|
|
|
222
|
+
#### Existing data is NOT being truncated (decided 2026-08-04)
|
|
223
|
+
|
|
224
|
+
Nothing needs deleting to adopt the new scheme. References are **stored, never re-derived from
|
|
225
|
+
the id at read time**: the listing search, the Tools Move-fingerprint prompt (which accepts a
|
|
226
|
+
reference *or* a numeric id) and every display path read the stored string, so both generations
|
|
227
|
+
work side by side. Accepted cost: 10 of the 68 existing issues keep ambiguous references (`1B`,
|
|
228
|
+
`1I`, `2B`, `2I`, `2L`, `2O`, `3B`, `3I`, `3L`, `3O`) until they age out through the normal GC.
|
|
229
|
+
|
|
230
|
+
> **If you ever do truncate `Logs.Issue`:** `TRUNCATE` **resets `AUTO_INCREMENT`**, and
|
|
231
|
+
> `IssueFingerprint`, `IssueEmailAddress`, `IssueClickupTask` and `Event` all reference
|
|
232
|
+
> `Issue.id` — they must be cleared in the **same** operation or they orphan onto reused ids.
|
|
233
|
+
|
|
202
234
|
### Shutdown handler
|
|
203
235
|
|
|
204
236
|
`register_shutdown_function` is registered with a **pre-allocated memory reserve**. Before this,
|
|
@@ -249,6 +281,13 @@ DB, so the pipeline is no longer 2.0-only; see
|
|
|
249
281
|
has to opt in by adding a `[database_toga2logs]` config section, so Sentry must remain for any 1.0
|
|
250
282
|
app that has not been wired up yet.
|
|
251
283
|
|
|
284
|
+
## Deploy order for a change that spans this pipeline
|
|
285
|
+
|
|
286
|
+
`_underscore` **first** (api2 and worker2 clone it at prebuild), then api2, worker2, `library`,
|
|
287
|
+
`worker`, `tools`. Anything touching the reference encoder or a shared `Logs` table name must land
|
|
288
|
+
in `_underscore` and `library` in the **same** release, and the `/errors` console work needs
|
|
289
|
+
**both** `library` and `tools` deployed before it is visible.
|
|
290
|
+
|
|
252
291
|
## Gotchas / known issues
|
|
253
292
|
|
|
254
293
|
- **Read-your-writes: reads go to the READ host and cannot see an uncommitted write.** A
|
|
@@ -379,6 +418,16 @@ clientUserId). **Neither was built.** As built instead:
|
|
|
379
418
|
|
|
380
419
|
## Change history
|
|
381
420
|
|
|
421
|
+
- 2026-08-04 (later, **uncommitted/undeployed** at time of writing) — **Reference encoding reworked
|
|
422
|
+
to base 32** over `'0123456789ACDEFGHJKMNPQRSTUVWXYZ'` (all digits kept; `O`/`I`/`L`/`B` dropped
|
|
423
|
+
as impersonators of `0`/`1`/`1`/`8`), replacing the base-26 decimal-counter+letter scheme, with
|
|
424
|
+
**`REFERENCE_OFFSET = 128`** so new values start at `4` and cannot collide with the legacy `1`–`3`
|
|
425
|
+
values on the UNIQUE column — a collision would fail the INSERT and *lose* the error. Verified
|
|
426
|
+
over 200,000 ids. The 1.0 encoder in `library/app/error/capture.php` mirrors it byte-for-byte.
|
|
427
|
+
**Decided: existing Issue/Event data is not truncated** — references are stored and never
|
|
428
|
+
re-derived, so both generations coexist; 10 of 68 issues keep ambiguous references until GC.
|
|
429
|
+
Recorded the TRUNCATE warning (it resets `AUTO_INCREMENT` and four child tables reference
|
|
430
|
+
`Issue.id`). Next production reference will be `68` (max id 72). (jcardinal)
|
|
382
431
|
- 2026-08-04 — **Decided: `_Exception_Business` extends `_Exception_Validation`** so api2 answers
|
|
383
432
|
4xx for a business condition while still recording it — with the ordering trap that every
|
|
384
433
|
`catch (_Exception_Validation …)` must now test business **first** (api2's controller and the
|
|
@@ -7,5 +7,5 @@
|
|
|
7
7
|
| [Deployment — Docker, Compose, Entrypoint, External PG/Redis](features/deployment.md) | TOGa IQ ships as a **single container** (`aegra` service) wrapping the `aegra-api` FastAPI server. | talos/docker-compose.yml, talos/deployments/docker/Dockerfile, talos/deployments/docker/entrypoint.sh |
|
|
8
8
|
| [MCP Servers — clickup-mcp and toga-db-mcp](features/mcp-servers.md) | Two internal **FastMCP** servers exposed over **HTTP** with API-key auth and PM2 process management: - **`clickup-mcp`** — ClickUp workspace surface (spaces / f | talos/mcp-servers/clickup-mcp/src, talos/mcp-servers/clickup-mcp/ecosystem.config.js, talos/mcp-servers/clickup-mcp/ecosystem.dev.config.js, talos/mcp-servers/clickup-mcp/pyproject.toml, talos/mcp-servers/clickup-mcp/.env.example, talos/mcp-servers/toga-db-mcp/src, talos/mcp-servers/toga-db-mcp/clusters.yaml, talos/mcp-servers/toga-db-mcp/ecosystem.config.js, talos/mcp-servers/toga-db-mcp/pyproject.toml, talos/mcp-servers/toga-db-mcp/.env.example |
|
|
9
9
|
| [Observability — Langfuse, OTEL, Prometheus, OneUptime](features/observability.md) | TOGa IQ uses **two complementary tracing planes** plus optional Prometheus metrics and external uptime monitoring: - **Langfuse (native v3 SDK)** — LLM-shaped t | talos/libs/aegra-api/src/aegra_api/observability/__init__.py, talos/libs/aegra-api/src/aegra_api/observability/setup.py, talos/libs/aegra-api/src/aegra_api/observability/base.py, talos/libs/aegra-api/src/aegra_api/observability/langfuse_provider.py, talos/libs/aegra-api/src/aegra_api/observability/langfuse_client.py, talos/libs/aegra-api/src/aegra_api/observability/otel.py, talos/libs/aegra-api/src/aegra_api/observability/metrics.py, talos/libs/aegra-api/src/aegra_api/observability/span_enrichment.py, talos/libs/aegra-api/src/aegra_api/observability/targets |
|
|
10
|
-
| [Pricing & COGS Model (Talos / TOGa IQ Pricing Calculator)](features/pricing-cogs-model.md) | The cost basis and pricing methodology for selling Talos (TOGa IQ) chat and voice deployments. |
|
|
10
|
+
| [Pricing & COGS Model (Talos / TOGa IQ Pricing Calculator)](features/pricing-cogs-model.md) | The cost basis and pricing methodology for selling Talos (TOGa IQ) chat and voice deployments. | worker2/Worker/Talos/Pricing.php, tools/_/app/talos/estimator.php, dbchanges2/Team/2026-08-03a - TalosPricingTokenCostModel.sql |
|
|
11
11
|
| [talos_agent — LangGraph ReAct Agent with Plan, BLP, MCP, Canvas](features/talos-agent.md) | `talos_agent` is the **reference LangGraph agent** shipped under `agents/talos_agent/`. | talos/agents/talos_agent/graph.py, talos/agents/talos_agent/state.py, talos/agents/talos_agent/context.py, talos/agents/talos_agent/prompts.py, talos/agents/talos_agent/tool_router.py, talos/agents/talos_agent/model_helpers.py, talos/agents/talos_agent/config/context.py, talos/agents/talos_agent/config/model_config.py, talos/agents/talos_agent/config/tool_config.py, talos/agents/talos_agent/config/plan_config.py, talos/agents/talos_agent/config/memory_config.py, talos/agents/talos_agent/config/mcp_config.py, talos/agents/talos_agent/config/blp_config.py, talos/agents/talos_agent/config/interrupt_config.py, talos/agents/talos_agent/config/artifact_config.py, talos/agents/talos_agent/plan/planner.py, talos/agents/talos_agent/plan/nodes.py, talos/agents/talos_agent/llm/system_prompt.py, talos/agents/talos_agent/llm/tool_binding.py, talos/agents/talos_agent/llm/message_ops.py, talos/agents/talos_agent/mcp/client.py, talos/agents/talos_agent/mcp/security.py, talos/agents/talos_agent/mcp/session_manager.py, talos/agents/talos_agent/blp/registry.py, talos/agents/talos_agent/blp/embedder.py, talos/agents/talos_agent/blp/nodes.py, talos/agents/talos_agent/tools/__init__.py, talos/agents/talos_agent/tools/code_interpreter.py, talos/agents/talos_agent/tools/canvas.py, talos/agents/talos_agent/tools/knowledge_base.py, talos/agents/talos_agent/tools/skill_injector.py, talos/agents/talos_agent/thread_name/nodes.py |
|
|
@@ -6,9 +6,12 @@ project: TOGa IQ
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-04
|
|
10
10
|
owners: [jcardinal]
|
|
11
|
-
files:
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Talos/Pricing.php
|
|
13
|
+
- tools/_/app/talos/estimator.php
|
|
14
|
+
- dbchanges2/Team/2026-08-03a - TalosPricingTokenCostModel.sql
|
|
12
15
|
related:
|
|
13
16
|
- ../architecture.md
|
|
14
17
|
- aegra-api.md
|
|
@@ -38,7 +41,67 @@ a platform architecture doc captures the cross-framework topology.
|
|
|
38
41
|
Treat the numbers below as the **April 2026 baseline**; re-measure before relying
|
|
39
42
|
on them in a later period.
|
|
40
43
|
|
|
41
|
-
## Cost methodology —
|
|
44
|
+
## Cost methodology — measured token unit price (decision, 2026-08-03) ← CURRENT
|
|
45
|
+
|
|
46
|
+
**Supersedes the Langfuse calibration-factor method below.** Cost is tokens, priced at the
|
|
47
|
+
real AWS rate:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
unitCostPer1kTokens = AWS Bedrock-family actual $ / measured tokens
|
|
51
|
+
clientMonthlyCost = client tokens × unitCostPer1kTokens
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Measured July 2026:** $1,013.76 / 258.3M tokens = **$0.00392447 per 1k tokens**.
|
|
55
|
+
|
|
56
|
+
- **Why not cost-per-conversation.** Measured on True, questions per conversation range
|
|
57
|
+
**1.0–61.0** (median 2.1, p90 6.5) — a 60× spread. A conversation cannot anchor a price.
|
|
58
|
+
- **Why not usage-API dollars.** The usage API reports cost **~2.5× below** the real AWS bill
|
|
59
|
+
because it undercounts prompt caching (cache reads are 47% of True's tokens). Its **token
|
|
60
|
+
counts** are a real measurement and are used; **its dollars never are.**
|
|
61
|
+
- The per-client **calibration factor** is retired.
|
|
62
|
+
|
|
63
|
+
### Workload profiles replace the feature checklist (decision, 2026-08-03)
|
|
64
|
+
|
|
65
|
+
Service offering (CHAT / VOICE_TO_VOICE / NATURAL_VOICE) describes **modality** and says
|
|
66
|
+
nothing about cost. Cost is driven by tool-call **intensity**, which varies by what the client
|
|
67
|
+
uses Talos for. Measured on True, cost is **42% `mcp_or_other`** (app integrations), **26%
|
|
68
|
+
`code_interpreter`**, only **19% `knowledge_base`** — the opposite of what was assumed.
|
|
69
|
+
|
|
70
|
+
Cost per tool call is near flat across categories ($0.046–$0.062; `canvas` the outlier at
|
|
71
|
+
$0.122), so **a single unit price × an intensity figure** models this and a per-tool price
|
|
72
|
+
list is unnecessary. Three seeded profiles in `TalosWorkloadProfiles`:
|
|
73
|
+
**retrieval** / **general** (measured from True) / **analytics** (estimated).
|
|
74
|
+
|
|
75
|
+
### Model mix is a bigger lever than any contracted feature
|
|
76
|
+
|
|
77
|
+
Measured on True: `amazon.nova-pro` serves **70%** of LLM calls at $0.00092/1k while
|
|
78
|
+
`claude-sonnet-4-5` serves **25%** at $0.00142/1k (76.3% cache). A **routing change moves
|
|
79
|
+
COGS more than any feature flag**, so `TalosUsageModelMonthly` records model mix per
|
|
80
|
+
client-month — cheap now, expensive to backfill, and the most likely explanation for a future
|
|
81
|
+
margin swing.
|
|
82
|
+
|
|
83
|
+
### App integrations are recorded but NOT yet priced
|
|
84
|
+
|
|
85
|
+
`tokensPerIntegrationPerUser` is seeded **0**, `isValidated 0`. Integrations are almost
|
|
86
|
+
certainly the largest cost driver (`mcp_or_other` = 42% of True's cost), but a slope needs
|
|
87
|
+
**≥2 clients** with a known integration count and real usage, and there is exactly one.
|
|
88
|
+
Inventing a coefficient repeats the mistake of the old $2.00/user default. The plumbing is
|
|
89
|
+
wired so the estimate, band recommendations, and tool-mix chart all respond the moment a real
|
|
90
|
+
figure is set. Method recorded in the migration: **regress each client's `mcp_or_other`
|
|
91
|
+
tokens-per-user against its `appIntegrationCount`.**
|
|
92
|
+
|
|
93
|
+
### orgFeeShareOfRevenue (decision, 2026-08-03)
|
|
94
|
+
|
|
95
|
+
Default **0.25**. Fixes a double-count: the per-user fee was priced to hit the full target
|
|
96
|
+
margin on its own, so the org-fee recommendation collapsed to ~$0.07 on a $1,036 contract,
|
|
97
|
+
making the one adjustable lever useless. The seat fee now covers `(1 − share)` of target
|
|
98
|
+
revenue and the flat fee covers the rest — **effective price per user is identical, only the
|
|
99
|
+
split changes**. `share = 0` reduces exactly to the old formula. Verified to reconcile to the
|
|
100
|
+
target margin to the cent.
|
|
101
|
+
|
|
102
|
+
## Cost methodology — calibrate Langfuse to AWS (2026-06-29) — SUPERSEDED
|
|
103
|
+
|
|
104
|
+
> Retired 2026-08-03 by the token unit price above. Kept for context on pre-August figures.
|
|
42
105
|
|
|
43
106
|
Langfuse provides the **structure** (granular per-feature / per-user breakdown);
|
|
44
107
|
AWS provides the **absolute dollars**. Each month, per client:
|
|
@@ -181,9 +244,16 @@ Technical / Actuals / Engine tabs, per-client file under the shared AI drive) is
|
|
|
181
244
|
hack, not a bug to silently correct.
|
|
182
245
|
- **OpenSearch +$350/mo is conditional** — only counts when Document Search is
|
|
183
246
|
enabled; do not bake it into every quote.
|
|
247
|
+
- **Voice multipliers 0.25 / 0.50 are UNVALIDATED** — supplied second-hand, never measured,
|
|
248
|
+
and may be **inverted**. Flagged in the UI. Do not quote voice from them.
|
|
249
|
+
- **`infraFixedMonthly` is 0** — shared platform cost has never been measured, so band
|
|
250
|
+
recommendations come out flat. That flat table is the **honest output, not a bug**.
|
|
251
|
+
- **Internal tenants are costed but not quoted** — True (TOGA Technology) feeds the platform
|
|
252
|
+
unit price and is excluded from fee recommendations; see `talos-pricing-automation`.
|
|
184
253
|
- **Do not embed the absolute drive path as canonical** — the calculator lives
|
|
185
254
|
under the team shared AI drive; the path can move.
|
|
186
255
|
|
|
187
256
|
## Change history
|
|
257
|
+
- 2026-08-04 — **Cost model replaced**: measured **token unit price** (AWS Bedrock-family actual ÷ measured tokens; July 2026 = $0.00392447/1k) retires the per-client Langfuse calibration factor. Rejected cost-per-conversation (1.0–61.0 questions/conversation, 60× spread) and usage-API dollars (~2.5× below the AWS bill; undercounts caching — token counts used, dollars never). **Workload profiles** (retrieval/general/analytics) replace the feature checklist: offering = modality only; cost = tool-call intensity, measured 42% mcp_or_other / 26% code_interpreter / 19% knowledge_base with near-flat cost per tool call. Added model-mix tracking (nova-pro 70% of calls at $0.00092/1k vs sonnet-4-5 25% at $0.00142/1k — routing beats any feature flag), **orgFeeShareOfRevenue** (default 0.25) to stop the seat fee double-counting the whole target margin, and integrations recorded-but-unpriced pending a second client. (jcardinal)
|
|
188
258
|
- 2026-06-29 — Pricing **delivery pivoted** from the Excel calculator to a DB platform (Team DB 9 `Talos*` tables + tools 1.0 UI + worker2 crons). Cost methodology recorded as **calibrate Langfuse→AWS** (per-client factor = AWS/Langfuse_corrected; global-blended fallback for new clients; always use cache-corrected Langfuse). Methodology refined: per-user fee is now a **locked band schedule** (steps with headcount) with the **org fee** as the margin lever (was fixed per-user + midpoint-solved flat fee). COGS token economics unchanged. (jcardinal)
|
|
189
259
|
- 2026-06-29 — Initial pricing/COGS model doc: chat token + feature drivers, voice per-minute drivers, two-part flat+per-user methodology with mid-band solve and consecutive-month smoothing. Captured from TALOS Pricing Calculator v9 (replaces v8). v9 verified to reproduce v8 exactly ($0.811182/conversation, $2,071.18/mo for 50 users × 20 conversations, Document Search only). History ×7 vs ×15-exchanges quirk preserved for parity and flagged for technical review. (jcardinal)
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
| [Compass VIP Support Importer (worker2)](features/compass-vip-support-importer.md) | A worker2 action that ingests Compass's quarterly VIP spreadsheet and assigns each VIP user's support technician by setting `Users.c_supportedByUserId` in `Clie | worker2/Worker/Client/Compass/VipSupport.php |
|
|
19
19
|
| [Creating Worker Actions](features/creating-worker-actions.md) | How to add a new callable Worker action — a PHP class whose `public static` methods are invoked as background jobs (via webhook, cron, or `_Worker::runTask()`). | worker2/Worker/, worker2/Controller/Index.php, _underscore/Worker.php |
|
|
20
20
|
| [Elite Freshservice Sync (worker2)](features/elite-freshservice-sync.md) | `_Worker_Elite` processes Freshservice webhook events and syncs them into TOGA 2. | worker2/Worker/Elite.php, worker2/Config/dev-kmaramreddy-laptop.ini |
|
|
21
|
-
| [Error Escalation Cron (Errors::Escalate → ClickUp / email)](features/error-escalation-cron.md) | `_Worker_Infrastructure_Errors::Escalate` (renamed from `SyncWithClickup`) is the sole owner of **escalation, de-escalation, ClickUp ticketing, reminders, busin | worker2/Worker/Infrastructure/Errors.php, worker2/Worker/Clickup/ErrorTask.php, worker2/Worker/Clickup.php, worker2/Controller/Index.php, worker2/Config/production.ini, _underscore/Model/Core/Logs/Issue.php, dbchanges2/Core/2026-07-30a - Error escalation cron job.sql, dbchanges2/Logs/2026-08-03a - Issue clickupPriority.sql |
|
|
21
|
+
| [Error Escalation Cron (Errors::Escalate → ClickUp / email)](features/error-escalation-cron.md) | `_Worker_Infrastructure_Errors::Escalate` (renamed from `SyncWithClickup`) is the sole owner of **escalation, de-escalation, ClickUp ticketing, reminders, busin | worker2/Worker/Infrastructure/Errors.php, worker2/Worker/Notification/Email.php, worker2/Worker/Notification/EmailTemplate.php, worker2/Worker/Client/True.php, worker2/Worker/Clickup/ErrorTask.php, worker2/Worker/Clickup.php, worker2/Controller/Index.php, worker2/Config/production.ini, _underscore/Model/Core/Logs/Issue.php, dbchanges2/Core/2026-07-30a - Error escalation cron job.sql, dbchanges2/Logs/2026-08-03a - Issue clickupPriority.sql |
|
|
22
22
|
| [Etilize Catalog Item Import & Refresh](features/etilize-catalog-item-import.md) | Client-generic catalog onboarding from an S3 CSV plus an Etilize re-pull. | worker2/Worker/Etilize/Items.php |
|
|
23
23
|
| [Etilize Item Translation Import](features/etilize-item-translation-import.md) | The abstract worker class `_Worker_Etilize_ItemTranslations` imports **non-English** item text from Etilize into the client's `ItemTranslations` table. | worker2/Worker/Etilize/ItemTranslations.php |
|
|
24
24
|
| [Monitoring Framework (Orchestrator + Child Monitors)](features/monitoring-framework.md) | A unified, DB-driven monitoring framework for business-critical data flows (Compass POs, Prudential asset imports, AIG closed claims, …). | worker2/Worker/Monitor.php, worker2/Worker/Monitors/, worker2/Worker/Monitors/RateEntitlement.php, worker2/Worker/Notification/Email.php, worker2/Worker/Rate.php, dbchanges2/Core/2026-05-21 - Monitors.sql, dbchanges2/Core/2026-06-29a - Rate Entitlement Contract Monitor.sql |
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
| [Platform Cache Cleanup (_Worker_Platform_Cache — Clean + Truncate)](features/platform-cache-cleanup.md) | `_Worker_Platform_Cache` owns maintenance of the shared **Cache** cluster that backs api2's [multi-client data retrieval](../../api2/features/cross-client-data- | worker2/Worker/Platform/Cache.php, worker2/Controller/Index.php, worker2/_.php, dbchanges2/Core/2026-07-27a - PlatformCacheCleanCron.sql |
|
|
32
32
|
| [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. | worker2/Worker/Startech.php |
|
|
33
33
|
| [Talos (TOGa IQ) Meeting-Notes Integration & Token Auto-Refresh (consumer)](features/talos-meeting-notes-integration.md) | How a **dev tool / agent consumes Talos (TOGa IQ)** to query the team meeting-notes corpus programmatically. | .claude/skills/plan-ticket/scripts/talos.js |
|
|
34
|
-
| [Talos Pricing Automation (worker2 Cron — AWS Actuals,
|
|
34
|
+
| [Talos Pricing Automation (worker2 Cron — Usage Import, AWS Actuals, Margins, Profiles)](features/talos-pricing-automation.md) | The worker2 half of the **Talos Pricing Platform** (see the talos `pricing-cogs-model` and tools `talos-pricing-ui` docs for the other halves). | worker2/Worker/Talos/Pricing.php, worker2/Database/TalosPricingCrons.sql, dbchanges2/Team/2026-08-03a - TalosPricingTokenCostModel.sql, dbchanges2/Team/2026-08-03b - TalosInternalTenant.sql, dbchanges2/Team/2026-08-03c - TalosIntegrationCostFactor.sql, dbchanges2/Core/2026-08-03a - Talos pricing cron jobs.sql |
|
|
35
35
|
| [Talos Transcript Ingestion Pipeline (worker2 → AWS Bedrock KBs)](features/talos-transcript-ingestion.md) | > **DB-DRIVEN AI-MODEL ROUTING (2026-07-29).** Which knowledge base a transcript is cleaned > into is now decided by the **meeting organizer's "home" AI model** | worker2/Worker/Team/Transcripts.php, worker2/bin/sync-knowledge-bases.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Client_True/2026-07-27a - TranscriptAiModelRoutingColumns.sql, dbchanges2/Client_True/2026-07-27b - TranscriptAiModelRoutingData.sql, dbchanges2/Team/2026-07-27a - TranscriptVocabularyAiModelScope.sql, dbchanges2/Team/2026-07-29a - TranscriptVocabularyBackfillAllModels.sql, dbchanges2/Team/2026-06-30a, dbchanges2/Team/2026-06-30b, dbchanges2/Team/2026-06-30c, dbchanges2/Team/2026-06-30d, dbchanges2/Team/2026-06-30e, dbchanges2/Core/2026-06-30a, dbchanges2/Core/2026-07-02a, dbchanges2/Team/2026-07-02a, dbchanges2/Team/2026-07-08a, dbchanges2/Team/2026-07-09a, dbchanges2/Team/2026-07-10a, dbchanges2/Team/2026-07-28a - TranscriptProcessingRetryAttempts.sql, dbchanges2/Team/2026-07-28b - TranscriptPromptTemplateConverseModel.sql, dbchanges2/Core/2026-07-28a - TeamsTranscriptRetryCron.sql |
|
|
36
36
|
| [Team Sprint Management & Reporting](features/team-sprint-management.md) | `_Worker_Team_Sprint` (file `Worker/Team/Sprint.php`) is the engine behind TOGA's internal **development-sprint process and reporting**. | worker2/Worker/Team/Sprint.php, _underscore/Model/Team/Sprint.php, dbchanges2/Core/CronJobs (SprintLockScheduled seed) |
|
|
37
37
|
| [Teams Meeting Transcript Export](features/teams-transcript-export.md) | > **SUPERSEDED (2026-07-09) — the S3-staging model below is history.** `Export` is now a thin > **GRAPH-DIRECT** cron poller: it no longer archives raw VTT to ` | worker2/Worker/Team/Transcripts.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Core/2026-06-18a - Teams Transcript Export schedule.sql |
|
|
@@ -10,6 +10,9 @@ updated: 2026-08-04
|
|
|
10
10
|
owners: ["jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Infrastructure/Errors.php
|
|
13
|
+
- worker2/Worker/Notification/Email.php
|
|
14
|
+
- worker2/Worker/Notification/EmailTemplate.php
|
|
15
|
+
- worker2/Worker/Client/True.php
|
|
13
16
|
- worker2/Worker/Clickup/ErrorTask.php
|
|
14
17
|
- worker2/Worker/Clickup.php
|
|
15
18
|
- worker2/Controller/Index.php
|
|
@@ -23,6 +26,8 @@ related:
|
|
|
23
26
|
- ../../../1.0/apps/library/features/error-capture-1-0.md
|
|
24
27
|
- ./creating-worker-actions.md
|
|
25
28
|
- ./clickup-project-routing.md
|
|
29
|
+
- ./notification-email.md
|
|
30
|
+
- ./notification-email-template.md
|
|
26
31
|
---
|
|
27
32
|
|
|
28
33
|
## Summary
|
|
@@ -107,10 +112,64 @@ which is safe only because the automation never sets status itself.
|
|
|
107
112
|
> stamping `dtAcknowledged` ~1s after `dtCreated` and permanently disabling the neglect axis for
|
|
108
113
|
> that issue. Status webhooks arriving within 60s of creation are ignored.
|
|
109
114
|
|
|
115
|
+
### ⚠ Escalation email had never been sent once — two independent causes (fixed 2026-08-04)
|
|
116
|
+
|
|
117
|
+
Evidence: `Logs_True.Email` contained **zero** error-escalation rows *ever*, while unrelated
|
|
118
|
+
worker2 mail (merge-conflict notices, sprint reports) delivered within ~60s — so the mail pipeline
|
|
119
|
+
itself was healthy. Every `Infrastructure/Errors/Escalate` run reported `"emailsSent":0` alongside
|
|
120
|
+
`"failures":0` and therefore **read as healthy**. All 48 HIGH/URGENT issues sat at
|
|
121
|
+
`notificationCount` 4 — the last rung of the ladder — having sent nothing.
|
|
122
|
+
|
|
123
|
+
**Root cause A (structural — the important one).** `processTechnicalIssue()` returned early when
|
|
124
|
+
the ClickUp task would read identically (priority already correct **and** count unchanged), and
|
|
125
|
+
**that `return` sat before the `if ($shouldNotify)` block.** `persistIssueState()` still ran on the
|
|
126
|
+
way out with `didNotify: $shouldNotify`, stamping `dtLastNotified` and incrementing
|
|
127
|
+
`notificationCount`. So a run that sent nothing still **consumed a rung** of the
|
|
128
|
+
30/60/240/1440-minute `REMINDER_INTERVAL_MINUTES` ladder — and since a live issue normally sits at
|
|
129
|
+
a settled priority with an unchanged count, that was **nearly every run**.
|
|
130
|
+
|
|
131
|
+
Fix: the ClickUp write is now skipped on its own merits, inside
|
|
132
|
+
`if ($needsPriorityWrite || $hasCountChanged)`, and the notification decision is made
|
|
133
|
+
**independently, below it**.
|
|
134
|
+
|
|
135
|
+
> **Lesson worth generalizing: "does ClickUp need writing?" and "does anyone need telling?" are two
|
|
136
|
+
> separate questions.** Conflating them into one early return silenced the entire reminder ladder
|
|
137
|
+
> while every metric reported success.
|
|
138
|
+
|
|
139
|
+
**Root cause B.** Both send paths built a `_Email` and called `->send()` **inline inside the cron's
|
|
140
|
+
open `DB_LOGS` transaction**. `_Email::send()` resolves the client's log database, registers a
|
|
141
|
+
second connection and toggles the read host from in there; anything it threw was caught into
|
|
142
|
+
`error_log` only — invisible in the console *and* in the run summary. Both paths now **enqueue via
|
|
143
|
+
`_Worker::runTask`**, so a failure is a visible failed `WorkerJobs` row.
|
|
144
|
+
|
|
145
|
+
### `emailFailures` in the run summary
|
|
146
|
+
|
|
147
|
+
`Escalate()`'s JSON gained **`emailFailures`**, threaded through `processIssue()` /
|
|
148
|
+
`processTechnicalIssue()`'s outcome arrays and counting notifications that were *meant* to go out
|
|
149
|
+
and did not. It is reported **separately from `failures`** (which counts issues that threw). It
|
|
150
|
+
exists specifically so the failure mode above — `emailsSent:0` with `failures:0` reading as
|
|
151
|
+
healthy — cannot recur silently.
|
|
152
|
+
|
|
110
153
|
### Notification and task presentation
|
|
111
154
|
|
|
112
155
|
- The technical escalation email to **devteam@togatech.com** now **coexists** with the ClickUp
|
|
113
156
|
update. It was an `elseif`, so only one of the two ever fired.
|
|
157
|
+
- **The technical escalation email uses the TOGA Technology stored email template.**
|
|
158
|
+
`sendTechnicalEscalationEmail()` enqueues **`Notification/EmailTemplate/Send`** with
|
|
159
|
+
`clientIdentifier` `'True'` and
|
|
160
|
+
`_Worker_Client_True::EMAIL_TEMPLATE_UUID__TOGA_TECHNOLOGY`
|
|
161
|
+
(`232d4edb-c2fa-4a8b-b5b9-d5800c962e19`) — the same pattern `Team/Transcripts.php` uses. Verified
|
|
162
|
+
against `Client_True.EmailTemplates`: `isActive = 1`; `{subject}` appears in **both** the subject
|
|
163
|
+
line and the branded header banner; `{body}` sits inside a **600px-wide** content cell that
|
|
164
|
+
already sets Plus Jakarta Sans 16px; the row carries its own `sendFromEmailAddress`
|
|
165
|
+
(`noreply@togatech.com`) and a single priority column (`NORMAL`).
|
|
166
|
+
- **`buildTechnicalEmailBody()` is therefore a FRAGMENT, not a document.** Its wrapper was
|
|
167
|
+
changed from `max-width:680px` + its own `font-family` to `max-width:100%` and no font —
|
|
168
|
+
680px would overflow the template's 600px cell.
|
|
169
|
+
- **Per-send priority is not available on the template path**, so urgency rides in the subject
|
|
170
|
+
and body instead.
|
|
171
|
+
- **Business-issue emails moved to `Notification/Email/Send`**, which *does* keep per-client log
|
|
172
|
+
attribution and per-send priority.
|
|
114
173
|
- ClickUp titles carry the **reference only** — `[1E]`, not `[1E-1]`. Including the occurrence
|
|
115
174
|
number made the title churn on every escalation.
|
|
116
175
|
- **Occurrences** and **Last Occurrence** custom fields are synced, and task bodies open with a
|
|
@@ -172,6 +231,13 @@ this controller needs the same treatment.**
|
|
|
172
231
|
|
|
173
232
|
## Gotchas / known issues
|
|
174
233
|
|
|
234
|
+
- **Never send email inline inside this cron's open transaction — enqueue it.** `_Email::send()`
|
|
235
|
+
registers a second connection and toggles the read host, and a throw from in there was only ever
|
|
236
|
+
`error_log`'d. Use `_Worker::runTask` so a failure becomes a visible failed `WorkerJobs` row.
|
|
237
|
+
- **A "nothing changed, return early" shortcut must not sit above the notification block.** It
|
|
238
|
+
consumed reminder-ladder rungs without sending anything, for months, while reporting
|
|
239
|
+
`emailsSent:0 / failures:0` as success. Decide the outbound write and the notification
|
|
240
|
+
independently.
|
|
175
241
|
- **Persist state before, or independently of, any outbound call.** A regression chain took the
|
|
176
242
|
whole run down every minute: `syncCustomFields()` read `$issue->dtLastOccurred`, which
|
|
177
243
|
`loadWorkingSet()` never selected; `_Error::errorHandler()` promotes the undefined-property
|
|
@@ -207,6 +273,25 @@ this controller needs the same treatment.**
|
|
|
207
273
|
|
|
208
274
|
## Change history
|
|
209
275
|
|
|
276
|
+
- 2026-08-04 (later, **uncommitted/undeployed** at time of writing) — **Fixed: escalation email had
|
|
277
|
+
never been sent, ever**, for two independent reasons. (A) `processTechnicalIssue()` returned early
|
|
278
|
+
when the ClickUp task would read identically, and that return sat *above* the `if ($shouldNotify)`
|
|
279
|
+
block while `persistIssueState()` still stamped `dtLastNotified`/`notificationCount` on the way
|
|
280
|
+
out — so nearly every run silently consumed a rung of the 30/60/240/1440-minute ladder; all 48
|
|
281
|
+
HIGH/URGENT issues had reached rung 4 having sent zero mail. The ClickUp write is now gated on its
|
|
282
|
+
own merits (`$needsPriorityWrite || $hasCountChanged`) and the notification decision is made
|
|
283
|
+
independently below it. (B) Both send paths built `_Email` and called `->send()` **inline inside
|
|
284
|
+
the open `DB_LOGS` transaction**, where a throw was only `error_log`'d — both now enqueue via
|
|
285
|
+
`_Worker::runTask`. **Built:** the technical escalation email now goes through the stored TOGA
|
|
286
|
+
Technology template (`Notification/EmailTemplate/Send`, clientIdentifier `True`,
|
|
287
|
+
`_Worker_Client_True::EMAIL_TEMPLATE_UUID__TOGA_TECHNOLOGY`), which makes
|
|
288
|
+
`buildTechnicalEmailBody()` a fragment (`max-width:100%`, no font — 680px would overflow the
|
|
289
|
+
template's 600px cell) and removes per-send priority (urgency moves into the subject/body);
|
|
290
|
+
business emails moved to `Notification/Email/Send`, which keeps per-client log attribution and
|
|
291
|
+
priority. **Built:** an **`emailFailures`** counter in `Escalate()`'s run summary, separate from
|
|
292
|
+
`failures`, so `emailsSent:0 / failures:0` can never read as healthy again. Verified by reading
|
|
293
|
+
only — proof will be `emailsSent` going non-zero (or `emailFailures` naming the problem) on the
|
|
294
|
+
first Escalate run after the worker2 deploy. (jcardinal)
|
|
210
295
|
- 2026-08-04 — **Fixed: worker job failures were never captured at all** — worker2's front
|
|
211
296
|
controller catches `Throwable` and returns 200, so six catch sites in
|
|
212
297
|
`worker2/Controller/Index.php` now call `_Error::captureException()` after rollback and before the
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["mhammontree"]
|
|
9
|
+
updated: 2026-08-04
|
|
10
|
+
owners: ["mhammontree", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Notification/Email.php
|
|
13
13
|
- _underscore/Model/Client/EmailTemplate.php
|
|
@@ -39,7 +39,7 @@ migration backfilled first).
|
|
|
39
39
|
- `worker2/Worker/Notification/Email.php` — `abstract _Worker_Notification_Email`. One method:
|
|
40
40
|
`Send(string $clientIdentifier, string $subject, string $body, string|array $to=[],
|
|
41
41
|
string|array $cc=[], string|array $bcc=[], string $fromEmail='donotreply@togatech.com',
|
|
42
|
-
string $fromName='TOGA Technology')`. The entry point. Self-registers the client DB (no
|
|
42
|
+
string $fromName='TOGA Technology', ?int $priority=null)`. The entry point. Self-registers the client DB (no
|
|
43
43
|
`initialize()` — see below), wraps the body, and sends.
|
|
44
44
|
- `_underscore/Model/Client/EmailTemplate.php` — supplies `WRAPPER_UUID` and
|
|
45
45
|
`renderWrappedBody($subject, $body)`. Documented in
|
|
@@ -69,6 +69,20 @@ migration backfilled first).
|
|
|
69
69
|
5. **Send** via a plain `_Email`: `setClientIdentifier`, `addTo/Cc/Bcc`, `setFrom`,
|
|
70
70
|
`setSubject`, `setBody($wrapped ?? $raw)`, `send()`.
|
|
71
71
|
|
|
72
|
+
### Optional per-send priority (added 2026-08-04)
|
|
73
|
+
|
|
74
|
+
The trailing **`?int $priority = null`** maps to `_Email::setPriority()`, which emits the
|
|
75
|
+
`X-Priority` / `Importance` / `X-MSMail-Priority` headers. Omit it and nothing changes — the
|
|
76
|
+
dispatcher spreads **named** parameters, so a new trailing optional parameter is backward
|
|
77
|
+
compatible for every existing caller and queued job.
|
|
78
|
+
|
|
79
|
+
It was added so that moving the error pipeline's **business** escalation email off an inline
|
|
80
|
+
`_Email` and onto this worker did not silently drop the urgency headers (see
|
|
81
|
+
[error-escalation-cron](./error-escalation-cron.md)). Note the sibling
|
|
82
|
+
[`Notification/EmailTemplate/Send`](./notification-email-template.md) path has **no** per-send
|
|
83
|
+
priority — a stored template carries its own single priority column — so urgency there has to live
|
|
84
|
+
in the subject and body.
|
|
85
|
+
|
|
72
86
|
### Enqueuing it
|
|
73
87
|
|
|
74
88
|
`_Worker::runTask('Notification/Email/Send', ['clientIdentifier'=>…, 'subject'=>…, 'body'=>…, …])`
|
|
@@ -130,6 +144,11 @@ working recipe — verified in classic + new Outlook:
|
|
|
130
144
|
are replaced simultaneously, so a `{body}` literal inside the subject can't be re-expanded.
|
|
131
145
|
|
|
132
146
|
## Change history
|
|
147
|
+
- 2026-08-04 (**uncommitted/undeployed** at time of writing) — Added an optional trailing
|
|
148
|
+
`?int $priority = null` to `Send()`, mapping to `_Email::setPriority()`
|
|
149
|
+
(`X-Priority`/`Importance`/`X-MSMail-Priority`). Backward compatible because the dispatcher
|
|
150
|
+
spreads named parameters. Added so the error pipeline's business escalation email kept its urgency
|
|
151
|
+
headers when it moved off an inline `_Email` onto this worker. (jcardinal)
|
|
133
152
|
- 2026-06-24 — Built the DB-driven notification-email mechanism (TRUE-79240): new
|
|
134
153
|
`_Worker_Notification_Email::Send` entry point; branded shell moved out of code (deleted
|
|
135
154
|
`_underscore/Email/Template.php`, which also carried the 1.0 double-render bug) into a reserved
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Talos Pricing Automation (worker2 Cron — AWS Actuals,
|
|
2
|
+
title: Talos Pricing Automation (worker2 Cron — Usage Import, AWS Actuals, Margins, Profiles)
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
repo: worker2
|
|
5
5
|
project: Worker
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-04
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Talos/Pricing.php
|
|
13
13
|
- worker2/Database/TalosPricingCrons.sql
|
|
14
|
+
- dbchanges2/Team/2026-08-03a - TalosPricingTokenCostModel.sql
|
|
15
|
+
- dbchanges2/Team/2026-08-03b - TalosInternalTenant.sql
|
|
16
|
+
- dbchanges2/Team/2026-08-03c - TalosIntegrationCostFactor.sql
|
|
17
|
+
- dbchanges2/Core/2026-08-03a - Talos pricing cron jobs.sql
|
|
14
18
|
related:
|
|
15
19
|
- ./creating-worker-actions.md
|
|
16
20
|
- ../architecture.md
|
|
@@ -21,64 +25,140 @@ related:
|
|
|
21
25
|
|
|
22
26
|
## Summary
|
|
23
27
|
|
|
24
|
-
The worker2 half of the **Talos Pricing Platform** (see the talos `pricing-cogs-model`
|
|
25
|
-
|
|
26
|
-
**abstract** worker class
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
it via `_underscore::DB_TEAM`.
|
|
28
|
+
The worker2 half of the **Talos Pricing Platform** (see the talos `pricing-cogs-model` and
|
|
29
|
+
tools `talos-pricing-ui` docs for the other halves). `_Worker_Talos_Pricing` is an
|
|
30
|
+
**abstract** worker class whose monthly cron actions turn raw usage and AWS cost data into a
|
|
31
|
+
measured platform **unit price per 1k tokens**, per-client cost, and margin tracking. All
|
|
32
|
+
state lives in the **Team DB** (`Talos*` tables), reached via `_underscore::DB_TEAM`.
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
**Switched on and verified in production 2026-08-03** for period `2026-07-01`. Before that
|
|
35
|
+
every `Talos*` table was empty, the crons were never registered as `Core.CronJobs` rows, and
|
|
36
|
+
the estimator ran only on placeholder constants — i.e. the platform was built but had never
|
|
37
|
+
run. It now reconciles to the AWS bill exactly: July 2026 = **$1,013.76** (True-tagged
|
|
38
|
+
$549.18 + untagged $464.58 + Towfoundation $0.0016), measured unit price
|
|
39
|
+
**$0.00392447 / 1k tokens**.
|
|
33
40
|
|
|
34
41
|
## How it works
|
|
35
42
|
|
|
36
|
-
`initialize()` registers the Team DB connection via `_underscore::DB_TEAM
|
|
37
|
-
`
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
43
|
+
`initialize()` registers the Team DB connection via `_underscore::DB_TEAM`. The four actions
|
|
44
|
+
are `Core.CronJobs` rows (no code wiring — see `creating-worker-actions`), fired on the
|
|
45
|
+
**4th of the month** at 06:00 / 07:00 / 08:00 / 09:00 Central.
|
|
46
|
+
|
|
47
|
+
**The order is load-bearing:** `ImportUsage` → `ImportAwsActuals` → `RecomputeMargins` →
|
|
48
|
+
`RecomputeProfiles`. Each step's inputs are the prior step's outputs; running them out of
|
|
49
|
+
order produces wrong prices rather than an error.
|
|
50
|
+
|
|
51
|
+
**Why the 4th, not the 1st:** the usage API's `token_data_as_of` lagged ~5 days. Reading a
|
|
52
|
+
partial month understates the token denominator and therefore **inflates** the unit price.
|
|
53
|
+
|
|
54
|
+
### 1. ImportUsage (06:00)
|
|
55
|
+
Pulls token/activity data from the Talos usage API into `TalosUsageMonthly` (now split by
|
|
56
|
+
token class) and `TalosUsageModelMonthly` (model mix per client-month).
|
|
57
|
+
|
|
58
|
+
**The API cannot return an arbitrary past calendar month.** It answers only `7d` / `30d` /
|
|
59
|
+
`mtd` / `ytd`, and its xlsx export carries only MTD and YTD sheets. A calendar month is
|
|
60
|
+
therefore derived by **differencing two year-to-date snapshots**, persisted in
|
|
61
|
+
`TalosUsageSnapshots`. Three edge cases are handled explicitly:
|
|
62
|
+
- **First-ever run** — no prior snapshot, so it falls back to the `30d` window and flags the
|
|
63
|
+
row `API_30D_APPROX`.
|
|
64
|
+
- **January year-rollover** — detected, so YTD resetting to zero is not read as negative usage.
|
|
65
|
+
- **Multi-month gap** — flagged `API_YTD_ROLLOVER_GAP` rather than passed off as a clean month.
|
|
66
|
+
|
|
67
|
+
The proper fix is a `from_date`/`to_date` parameter on `/api/usage/overview`; the refresh
|
|
68
|
+
endpoint already accepts `from_date`, so the backend plumbing exists. Open with the Talos
|
|
69
|
+
backend developer.
|
|
70
|
+
|
|
71
|
+
### 2. ImportAwsActuals (07:00)
|
|
72
|
+
AWS Cost Explorer `GetCostAndUsage`, upserted per `(clientIdentifier, periodMonth)` into
|
|
73
|
+
`TalosAwsActualsMonthly`, which now records `unitCostPer1kTokens`, `serviceScope`, and the
|
|
74
|
+
tagged/untagged cost split. Three defects found and fixed here — see Gotchas; each one alone
|
|
75
|
+
made every quote wrong.
|
|
76
|
+
|
|
77
|
+
Untagged spend is attributed to the **defaultTenant**, and every row records
|
|
78
|
+
`isDefaultTenantApplied` so the day a second tenant makes that attribution wrong is visible.
|
|
79
|
+
|
|
80
|
+
### 3. RecomputeMargins (08:00)
|
|
81
|
+
Computes `clientMonthlyCost = client tokens × unitCostPer1kTokens`, margin vs. the client's
|
|
82
|
+
band, the consecutive out-of-band streak, and emits a `TalosFeeRecommendations` row only once
|
|
83
|
+
the streak ≥ the client's `consecutiveMonths`. **Internal tenants are excluded from fee
|
|
84
|
+
recommendations** (see below). The old per-client "calibration factor" is gone.
|
|
85
|
+
|
|
86
|
+
### 4. RecomputeProfiles (09:00)
|
|
87
|
+
Recomputes the measured workload profiles (`TalosWorkloadProfiles`) and the measured cost
|
|
88
|
+
factors from actual tool-mix data.
|
|
89
|
+
|
|
90
|
+
### Measured vs. policy cost factors
|
|
91
|
+
`TalosCostFactors` is now split by `isDerived` / `isValidated` (plus `name`, `description`).
|
|
92
|
+
**Derived rows are worker-owned and must never be hand-edited** — the tools Settings page
|
|
93
|
+
enforces this server-side with `UPDATE ... AND isDerived = 0`.
|
|
94
|
+
|
|
95
|
+
### Internal tenants
|
|
96
|
+
`TalosClients.isInternal` (migration `2026-08-03b`). True is TOGA Technology, not a paying
|
|
97
|
+
client, but it holds nearly all usage and — via `defaultTenant` — all untagged AWS spend, so
|
|
98
|
+
it **must** exist as a row or there is no unit price at all. Modelling it as a zero-revenue
|
|
99
|
+
contract made every margin figure lie (margin computes as 0%, so it sits permanently in
|
|
100
|
+
"Needs attention" and drags blended portfolio margin negative). Internal tenants are
|
|
101
|
+
**costed and do feed the platform unit price** (our own usage consumes real Bedrock capacity)
|
|
102
|
+
but are **excluded from `TalosFeeRecommendations`**.
|
|
103
|
+
|
|
104
|
+
### Schema (dbchanges2 migrations, 2026-08-03)
|
|
105
|
+
- **Team `2026-08-03a` TalosPricingTokenCostModel** — token split on `TalosUsageMonthly`;
|
|
106
|
+
`unitCostPer1kTokens` / `serviceScope` / tagged+untagged cost on `TalosAwsActualsMonthly`;
|
|
107
|
+
measured-vs-policy split on `TalosCostFactors`; new `TalosWorkloadProfiles`,
|
|
108
|
+
`TalosUsageModelMonthly`, `TalosUsageSnapshots`, `TalosContractAmendments`; seeds 13 cost
|
|
109
|
+
factors and 3 workload profiles.
|
|
110
|
+
- **Team `2026-08-03b` TalosInternalTenant** — `isInternal` on `TalosClients`.
|
|
111
|
+
- **Team `2026-08-03c` TalosIntegrationCostFactor** — `tokensPerIntegrationPerUser`.
|
|
112
|
+
- **Core `2026-08-03a` Talos pricing cron jobs** — registers the 4 crons.
|
|
113
|
+
|
|
114
|
+
### Configuration
|
|
115
|
+
The existing `[talos]` config group gains **`usage_url`** and **`usage_api_key`** (the same
|
|
116
|
+
values the tools app holds as `[api] talos_backend_url` / `talos_backend_key`). Keys are
|
|
117
|
+
referenced by name only.
|
|
66
118
|
|
|
67
119
|
## Gotchas / open items
|
|
120
|
+
|
|
121
|
+
- **AWS cost query — three defects that all produced badly wrong prices** (found 2026-08-03):
|
|
122
|
+
1. **No service filter.** The query summed every dollar in the linked account (RDS alone is
|
|
123
|
+
~$9,917/mo), so one client would have been charged ~$18k instead of ~$1k.
|
|
124
|
+
2. **Wrong tag key.** It filtered `Tags.Key='Client'`; no such tag exists. The activated
|
|
125
|
+
cost-allocation tag is **`tenant`** (activated alongside `model` and `model_role`).
|
|
126
|
+
3. **Service-name trap.** AWS bills **each Claude model as its own service**
|
|
127
|
+
("Claude Sonnet 4.5 (Amazon Bedrock Edition)", etc.). Filtering the literal
|
|
128
|
+
`"Amazon Bedrock"` captured **3.3%** of real model spend — $31.49 of $1,013.76 for July
|
|
129
|
+
2026. The family had 8 members in July and grows with every model launch, so the member
|
|
130
|
+
list must be resolved **at run time** via `ce:GetDimensionValues --search-string "Bedrock"`
|
|
131
|
+
and **snapshotted per row**. Never hardcode the service list.
|
|
132
|
+
- **IAM:** the EB instance role now needs **`ce:GetDimensionValues`** in addition to
|
|
133
|
+
`ce:GetCostAndUsage`.
|
|
134
|
+
- **Cost-allocation tagging went live 2026-07-16.** Before that, 100% of Bedrock spend is
|
|
135
|
+
untagged (June 2026: $798.44) and is **not recoverable per-tenant**. After, untagged fell to
|
|
136
|
+
~8% for the rest of July and 0.8% in August. The gap is **temporal and self-correcting**,
|
|
137
|
+
not structural: `defaultTenant` is a backfill mechanism with a shelf life.
|
|
138
|
+
- **July's unit price is ~8% conservative** — the cost numerator is a full calendar month
|
|
139
|
+
while the token denominator was a 30-day rolling window (`API_30D_APPROX`). August
|
|
140
|
+
differences two snapshots and the windows align.
|
|
141
|
+
- **Never use the usage API's dollar figures.** It reports cost ~2.5× **below** the real AWS
|
|
142
|
+
bill because it undercounts prompt caching (cache reads are 47% of True's tokens). Its
|
|
143
|
+
**token counts** are a real measurement and are used; its dollars never are.
|
|
68
144
|
- **Idempotent upsert key** — every fact table is keyed `(clientIdentifier, periodMonth)`
|
|
69
|
-
(usage-by-feature adds `toolCategory`)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
145
|
+
(usage-by-feature adds `toolCategory`) with a UNIQUE, so re-running a cron is safe.
|
|
146
|
+
- **Tow Foundation has no token extract** — `/api/usage/overview` returns 83 messages /
|
|
147
|
+
10 conversations / 2 users for ytd but `tokens=0`, `cost=$0`, and nothing in the last 30
|
|
148
|
+
days; their tagged AWS spend was $0.0016 for July, far too low for 83 real questions. The
|
|
149
|
+
activity counters and the token/cost extract are **separate sources with their own
|
|
150
|
+
`token_data_as_of` high-water mark**, and the extract appears never to have run for that
|
|
151
|
+
org. "Refresh token data" does not populate it. Note the Talos Postgres DB the weekly
|
|
152
|
+
report queries is named **`tenant_true`**, hinting at per-tenant databases — if the extract
|
|
153
|
+
job points at that one DB, no refresh will ever produce Tow Foundation tokens. With the
|
|
154
|
+
Talos backend developer.
|
|
78
155
|
|
|
79
156
|
## Security
|
|
80
|
-
-
|
|
81
|
-
|
|
157
|
+
- Config keys referenced by name only (`[talos] usage_url` / `usage_api_key`); no values are
|
|
158
|
+
recorded anywhere in the knowledge base.
|
|
159
|
+
- Hardcoded AWS SES SMTP credentials live in `_underscore/Email.php` (pre-existing) — flagged
|
|
160
|
+
for rotation. Location only.
|
|
82
161
|
|
|
83
162
|
## Change history
|
|
163
|
+
- 2026-08-04 — **Platform switched on and verified in production** for 2026-07. Cost model replaced: per-client calibration factor → measured **unit cost per 1k tokens** from the Bedrock-family AWS actual. Fixed three AWS cost-query defects (no service filter; wrong tag key `Client` → `tenant`; per-model service names resolved at run time via `ce:GetDimensionValues` and snapshotted — the literal "Amazon Bedrock" captured only 3.3% of spend). New actions `ImportUsage` + `RecomputeProfiles`; `ImportAwsActuals`/`RecomputeMargins` rewritten; crons on the 4th at 06/07/08/09:00 in a load-bearing order. Calendar months derived by differencing YTD snapshots (`API_30D_APPROX` / `API_YTD_ROLLOVER_GAP` flags). Added `isInternal` tenants (costed, feed the unit price, excluded from fee recommendations). New `[talos] usage_url`/`usage_api_key` config keys and `ce:GetDimensionValues` IAM permission. (jcardinal)
|
|
84
164
|
- 2026-06-29 — Built `_Worker_Talos_Pricing` (abstract) with 3 monthly crons — ImportAwsActuals (Cost Explorer), RecomputeMargins (calibrate Langfuse→AWS, rolling-3mo + global-blended fallback, margin/streak/recommendation), MonthlyReport (PhpSpreadsheet xlsx + templated SES email to True). Registers Team DB via `_underscore::DB_TEAM`; crons are `Core.CronJobs` rows on the 4th, 06/07/08:00 CT. Replaces the Excel "Actuals" recompute. (jcardinal)
|
|
@@ -3,12 +3,14 @@ title: "Tow Foundation"
|
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
apps:
|
|
5
5
|
- worker2
|
|
6
|
+
- tools
|
|
7
|
+
- dbchanges2
|
|
6
8
|
project: Worker
|
|
7
9
|
client: tow-foundation
|
|
8
10
|
type: profile
|
|
9
11
|
status: active
|
|
10
|
-
updated: 2026-
|
|
11
|
-
owners: ["rgirish"]
|
|
12
|
+
updated: 2026-08-04
|
|
13
|
+
owners: ["rgirish", "jcardinal"]
|
|
12
14
|
files: []
|
|
13
15
|
related:
|
|
14
16
|
- clients/tow-foundation/features/receipt-processing.md
|
|
@@ -23,6 +25,23 @@ and exported as QuickBooks-ready Excel files.
|
|
|
23
25
|
|
|
24
26
|
No client database is used (no persistence beyond SharePoint and email).
|
|
25
27
|
|
|
28
|
+
## Talos Pricing — first external client
|
|
29
|
+
|
|
30
|
+
Tow Foundation is the **first real external Talos client** (`clientIdentifier` `Towfoundation`).
|
|
31
|
+
A contract was created and signed through the tools Contracts page on 2026-08-03.
|
|
32
|
+
|
|
33
|
+
**Open issue — no token extract exists for this org.** `/api/usage/overview` returns 83
|
|
34
|
+
messages / 10 conversations / 2 users for `ytd` but `tokens = 0` and `cost = $0`, and nothing
|
|
35
|
+
at all in the last 30 days; their tagged AWS spend was **$0.0016** for July, far too low for
|
|
36
|
+
83 real questions. The activity counters and the token/cost extract are separate sources with
|
|
37
|
+
their own `token_data_as_of` high-water mark, and the extract appears never to have run for
|
|
38
|
+
this org — "Refresh token data" does not populate it. The Talos Postgres DB the weekly report
|
|
39
|
+
queries is named `tenant_true`, hinting at per-tenant databases; if the extract job points at
|
|
40
|
+
that one DB, no refresh will ever produce Tow Foundation tokens. With the Talos backend
|
|
41
|
+
developer. Until it is fixed, this client has **no measurable usage and no real margin
|
|
42
|
+
figure**. See
|
|
43
|
+
[talos-pricing-automation](../../2.0/apps/worker2/features/talos-pricing-automation.md).
|
|
44
|
+
|
|
26
45
|
## Contacts
|
|
27
46
|
|
|
28
47
|
- **Jheanelle Gordon** — primary contact; receives the processing completion email
|
|
@@ -11,7 +11,7 @@ project: _Underscore
|
|
|
11
11
|
client: true
|
|
12
12
|
type: profile
|
|
13
13
|
status: active
|
|
14
|
-
updated: 2026-
|
|
14
|
+
updated: 2026-08-04
|
|
15
15
|
owners: [jcardinal, kyalamarthi]
|
|
16
16
|
files: []
|
|
17
17
|
related:
|
|
@@ -39,3 +39,15 @@ The **Tools** app (1.0; repo `tools`) authenticates True users via **SSO** throu
|
|
|
39
39
|
staff persona (see `1.0/apps/tools/`). Its gateway return domain is registered in `Core.Domains`
|
|
40
40
|
(`uuid 2927bc15-e347-4358-a430-fb28f9446d27`, `clientId 1`, `appId 30`, env 1,
|
|
41
41
|
`https://tools.togatech.com/sso`).
|
|
42
|
+
|
|
43
|
+
### Talos Pricing — the internal tenant
|
|
44
|
+
|
|
45
|
+
True is an **internal tenant** in the Talos Pricing platform (`TalosClients.isInternal = 1`,
|
|
46
|
+
formal name **TOGA Technology**), not a paying client. It holds nearly all Talos usage and,
|
|
47
|
+
via `defaultTenant`, all untagged AWS Bedrock spend, so it **must** exist as a contract row
|
|
48
|
+
or there is no platform unit price at all. It is **costed and feeds the unit price** but is
|
|
49
|
+
**excluded from fee recommendations** — modelling it as a zero-revenue contract made margin
|
|
50
|
+
compute as 0%, parking it permanently in "Needs attention" and dragging blended portfolio
|
|
51
|
+
margin negative. A contract row was created and signed 2026-08-03. Measured July 2026:
|
|
52
|
+
$549.18 tagged of a $1,013.76 total. See
|
|
53
|
+
[talos-pricing-automation](../../2.0/apps/worker2/features/talos-pricing-automation.md).
|
package/package.json
CHANGED