toga-ai 1.0.455 → 1.0.457

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-07-27
9
+ updated: 2026-07-28
10
10
  owners: [jcardinal, bala, mhammontree]
11
11
  files:
12
12
  - api2/Controller/Index.php
@@ -141,6 +141,8 @@ Aliases: `DB_CORE`, `DB_CLIENT`, `DB_CLIENT_LOGS`, `DB_CLIENT_ARCHIVE`, `DB_LOGS
141
141
  `DB_STORE_1`/`DB_VISION_1` (legacy). `_Database::registerClientDatabases(clientId,
142
142
  environment)` joins `Clients`/`Databases`/`DatabaseHosts`/`Environments`, preferring the
143
143
  instance's own region; reads→readers, writes→writers, per-connection transactions.
144
+ `DB_CACHE` (shared Cache cluster) is resolved **by name** (`Databases.name = 'Cache'`) — never by
145
+ a hardcoded id, which differs per Core instance.
144
146
 
145
147
  ## Deployment (EB)
146
148
 
@@ -169,11 +171,39 @@ build. These must be **rotated** (treat the committed tokens as compromised) and
169
171
  Parameter Store / EB env properties. **Flag this if you touch config or deploy.** (Location +
170
172
  remediation only — do not record the token value anywhere.)
171
173
 
172
- **Follow-up (deferred, separate ticket): raw exception disclosure to clients.** The error paths that
173
- surface a caught `\Throwable` (now that Route.php rethrows and the bootstrap guard reports failures)
174
- must not return raw `getMessage()`/`getTrace()` output in the client-facing envelope — that leaks
175
- internal paths, schema names, and stack frames to API consumers. Sanitize the client envelope
176
- (generic message + code; full detail to Sentry/logs only). Not yet done.
174
+ ## Known issues / accepted risks
175
+
176
+ Open items a maintainer should know before changing this tier. None are "bugs to fix right now" —
177
+ they are the known sharp edges. Do not re-discover these from scratch.
178
+
179
+ 1. **Raw exception disclosure to clients (deferred, separate ticket).** The error paths that
180
+ surface a caught `\Throwable` (now that Route.php rethrows and the bootstrap guard reports
181
+ failures) must not return raw `getMessage()`/`getTrace()` output in the client-facing envelope —
182
+ that leaks internal paths, schema names, and stack frames to API consumers. Sanitize the client
183
+ envelope (generic message + code; full detail to Sentry/logs only). Not yet done.
184
+ 2. **Committed plaintext credentials, not yet rotated.** See the Security note above —
185
+ `Config/*.ini` secrets and the per-env `.ebextensions/git.*.json` GitHub PAT. Treat as
186
+ compromised until rotated and moved to SSM / EB env properties.
187
+ 3. **The pre-execute phase still runs before `execute()`'s try/catch.** The 2026-07-23 fix wraps the
188
+ Core/Logs bootstrap specifically; it did not move the phase inside the main guard. Any *new* code
189
+ added to the pre-execute block is again outside `execute()`'s protection and must carry its own
190
+ `try/catch (\Throwable)`.
191
+ 4. **Local Logs DB name mismatch reads as "missing".** The Core Logs schema name comes from a
192
+ `Core.Database` row (`id = CORE_LOGS_DATABASE_ID`); a local Logs DB whose actual schema name
193
+ differs throws `Unknown database`. This is environment config, not a code bug — fix the row or
194
+ the local schema name, don't patch the bootstrap.
195
+ 5. **CORS is fully permissive.** `Access-Control-Allow-*` is wide open on every route. Acceptable
196
+ only because auth is bearer-token (not cookie) based — if any cookie/session-backed auth is ever
197
+ added here, this becomes an exploitable hole and must be tightened first.
198
+ 6. **`_underscore` is cloned at build from a moving branch** (`_<ENVIRONMENT>`), not pinned to a
199
+ commit. Two deploys of the same api2 commit can produce different runtime behavior. Check the
200
+ framework branch state when triaging an "it worked yesterday" regression.
201
+ 7. **`V2.php::execute()` is a ~2,000-line monolith** with `processRoutePairs()` recursion beneath
202
+ it. There is no unit-test harness around it; changes are validated by integration traffic. Make
203
+ surgical edits and preserve the transaction/logging invariant.
204
+ 8. **JWT signing-secret rotation accepts current + previous.** During the overlap window a token
205
+ signed with the retired secret still validates. Revocation is therefore not immediate —
206
+ don't rely on rotation alone to lock out a compromised token.
177
207
 
178
208
  ## When making changes here
179
209
 
@@ -191,5 +221,6 @@ internal paths, schema names, and stack frames to API consumers. Sanitize the cl
191
221
  or `execute()`.
192
222
 
193
223
  ## Change history
224
+ - 2026-07-28 — Added a consolidated **Known issues / accepted risks** section (8 items), absorbing the previously free-floating deferred raw-exception-disclosure follow-up as item 1, so the tier's sharp edges (unrotated committed secrets, pre-execute phase still outside the main guard, local Logs DB name mismatch, permissive CORS, unpinned `_underscore` build clone, untested `V2.php` monolith, JWT rotation overlap window) are in one place instead of scattered. Recorded that `DB_CACHE` is resolved by name (`Databases.name = 'Cache'`), never by a hardcoded id, which differs per Core instance. (jcardinal)
194
225
  - 2026-07-27 — Sharpened the committed-secret note: the plaintext GitHub PAT lives in the per-env **`.ebextensions/git.*.json`** files (used by the `prebuild/git.sh` clone hook to pull `_underscore`), must be rotated and moved to SSM / EB env properties (location + remediation only, no value). (mhammontree)
195
226
  - 2026-07-23 — Documented the now-guarded Core/Logs DB bootstrap in the front controller: the pre-execute block runs before the `execute()` try/catch, the Core Logs schema name is resolved from a `Core.Database` row (`id = CORE_LOGS_DATABASE_ID`) so a name-mismatched local Logs DB reads as missing, and the failure is now wrapped in `try/catch (\Throwable)` returning `INVALID_CONFIGURATION` + Sentry instead of a fatal (guarded no-op rollback, `Database.php:219–226`). Added the deferred raw-getMessage/getTrace client-disclosure follow-up to the Security note. (jcardinal)
@@ -24,7 +24,7 @@
24
24
  | [Background Email-Template Worker (_Worker_Notification_EmailTemplate)](features/notification-email-template.md) | `_Worker_Notification_EmailTemplate::Send(...)` dispatches a **stored, client-defined `EmailTemplates` row off-thread** as a background WorkerJob. | worker2/Worker/Notification/EmailTemplate.php, worker2/Worker/Client/True.php, _underscore/Model/Client/EmailTemplate.php |
25
25
  | [DB-Driven Notification (Internal) Email](features/notification-email.md) | Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated, not client-facing transactional mail) are sent through one worker | worker2/Worker/Notification/Email.php, _underscore/Model/Client/EmailTemplate.php, dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql, dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql |
26
26
  | [OneUptime push-metric monitors for 2.0 workers](features/oneuptime-worker2-monitoring.md) | A second, **OneUptime-reporting** monitoring pattern for the 2.0 worker2 tier, ported from the 1.0 `App_SystemMonitor_Compass` monitors. | worker2/Worker/Monitor/Compass.php, worker2/Worker/Client/Compass.php, worker2/composer.json, _underscore/Cloud.php |
27
- | [Platform Cache Cleanup (_Worker_Platform_Cache::Clean)](features/platform-cache-cleanup.md) | `_Worker_Platform_Cache::Clean()` is the reaper for the shared **Cache** cluster that backs api2's [multi-client data retrieval](../../api2/features/cross-clien | worker2/Worker/Platform/Cache.php, worker2/Controller/Index.php, worker2/_.php, dbchanges2/Core/2026-07-27a - PlatformCacheCleanCron.sql |
27
+ | [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 |
28
28
  | [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 |
29
29
  | [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 |
30
30
  | [Talos Pricing Automation (worker2 Cron — AWS Actuals, Calibration, Monthly Report)](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 |
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-07-21
9
+ updated: 2026-07-28
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - worker2/Controller/Index.php
@@ -163,6 +163,37 @@ file `Worker/Team/Github.php`.
163
163
  `_Worker_Infrastructure_Worker_Cleanup`: `WorkerJobs()` deletes `isSuccess=1` rows >90 days
164
164
  (≤1000/run); `WebhookLogs()` deletes `Logs.Webhook` >90 days. Two daily CronJobs at 2 AM.
165
165
 
166
+ ## Cron actions must return a result on BOTH paths
167
+
168
+ **A new cron action is expected to return a result for a success as well as a failure.** Both
169
+ outcomes are recorded against the job, and the **successful** results are what auditing reads — a
170
+ run that completed but returned nothing is indistinguishable in the audit trail from one that never
171
+ executed.
172
+
173
+ So an action must not return only on the failure path, and must not return nothing on success
174
+ because "there was nothing to do". Return a result describing the outcome — including the no-op case
175
+ (`0 deleted, 0 dropped` is a meaningful audit record; silence is not). Make it structured and
176
+ countable rather than prose, so one run can be compared against its neighbours:
177
+
178
+ ```php
179
+ public static function Clean(): string {
180
+ // ... work ...
181
+ return json_encode([
182
+ 'isSuccess' => true,
183
+ 'deletedRowCount' => $deletedRowCount,
184
+ 'droppedTableCount' => $droppedCount,
185
+ 'failedDropCount' => $failedCount,
186
+ ]);
187
+ }
188
+ ```
189
+
190
+ Reserve throwing for genuinely **retriable** conditions, where the queue re-running the job is the
191
+ behaviour you want. A handled, non-retriable failure should return a result saying so rather than
192
+ throwing — throwing costs the full retry cycle and still records nothing useful for the audit.
193
+
194
+ See the worker contract in [2.0 framework rules](../../standards/framework-rules.md) for the
195
+ class/method shape and the named-argument parameter contract.
196
+
166
197
  ## Critical transaction pattern
167
198
 
168
199
  **Any INSERT/UPDATE that must be visible to another connection before an SQS message is
@@ -229,6 +260,12 @@ the MySQL-first design was departed from **on purpose** here.
229
260
 
230
261
  ## Change history
231
262
 
263
+ - 2026-07-28 — Added **Cron actions must return a result on BOTH paths**: a new cron action is
264
+ expected to return a structured result for a success as well as a failure, because both are
265
+ recorded against the job and the successful results are what auditing reads (a completed run that
266
+ returned nothing is indistinguishable from one that never executed). Formalises the intent of the
267
+ 2026-07-21 `output` change below. Also documents `_Worker_Platform_Cache::Truncate()`, a manual
268
+ companion to the scheduled `Clean()` action. (jcardinal)
232
269
  - 2026-07-21 — `Core.WorkerJobs.failureReason` renamed to `output` and repurposed to capture
233
270
  successful run results as well as failures (success writes the composed "Successfully Executed"
234
271
  message, return value `json_encode`d). Multi-producer rename: PHP worker (both paths), `Retry()`
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: Platform Cache Cleanup (_Worker_Platform_Cache::Clean)
2
+ title: Platform Cache Cleanup (_Worker_Platform_Cache — Clean + Truncate)
3
3
  framework: "2.0"
4
4
  repo: worker2
5
5
  project: Worker
@@ -17,20 +17,28 @@ related:
17
17
  - ../../api2/features/cross-client-data-retrieval.md
18
18
  - ./creating-worker-actions.md
19
19
  - ../../_underscore/features/per-client-database-connections.md
20
+ - ../architecture.md
20
21
  ---
21
22
 
22
23
  ## Summary
23
24
 
24
- `_Worker_Platform_Cache::Clean()` is the reaper for the shared **Cache** cluster that backs api2's
25
- [multi-client data retrieval](../../api2/features/cross-client-data-retrieval.md). It deletes
26
- expired `Cache.Tables` rows and drops the per-query `TableResults_<Tables.id>` tables that no
27
- longer have a parent. Without it the Cache schema grows one table per distinct cross-client query,
28
- forever.
25
+ `_Worker_Platform_Cache` owns maintenance of the shared **Cache** cluster that backs api2's
26
+ [multi-client data retrieval](../../api2/features/cross-client-data-retrieval.md). It exposes **two**
27
+ actions on the same class (both listed in the class docblock in `Worker/Platform/Cache.php`):
29
28
 
30
- Action path `Platform/Cache/Clean`, scheduled on a **`*/5`** cron registered in
31
- `dbchanges2/Core/2026-07-27a - PlatformCacheCleanCron.sql`.
29
+ | Action | Path | Scheduled? | Scope |
30
+ |---|---|---|---|
31
+ | `Clean()` | `Platform/Cache/Clean` | yes — `*/5` cron | **expired** entries only, with a 2-minute grace |
32
+ | `Truncate()` | `Platform/Cache/Truncate` | **no — deliberately manual** | **everything**, no grace, spares nothing |
32
33
 
33
- ## How it works
34
+ Without `Clean()` the Cache schema grows one `TableResults_<Tables.id>` table per distinct
35
+ cross-client query, forever. `Truncate()` is the administrative "reset the whole cache" hammer.
36
+
37
+ The `Clean()` cron is registered in `dbchanges2/Core/2026-07-27a - PlatformCacheCleanCron.sql`.
38
+ Both actions return a summary string on **every** path (success and handled failure) — see
39
+ [cron actions must return a result on both paths](../architecture.md#cron-actions-must-return-a-result-on-both-paths).
40
+
41
+ ## How it works — `Clean()` (scheduled reaper)
34
42
 
35
43
  1. **Reap expired parents.** Delete `Cache.Tables` rows whose TTL has elapsed, with a **2-minute
36
44
  grace period** so a request that is mid-read cannot have its rows deleted out from under it.
@@ -41,6 +49,44 @@ Action path `Platform/Cache/Clean`, scheduled on a **`*/5`** cron registered in
41
49
  status is **re-verified on the write host immediately before each `DROP`** — a `DROP TABLE` is
42
50
  irreversible and non-transactional, so a stale read replica must never be the basis for one.
43
51
 
52
+ ## How it works — `Truncate()` (manual, unscheduled)
53
+
54
+ `_Worker_Platform_Cache::Truncate(): string` removes the **entire** cross-client cache. It is
55
+ deliberately **not** registered as a cron — invoke it explicitly:
56
+
57
+ ```php
58
+ _Worker::runTask('Platform/Cache/Truncate', []);
59
+ ```
60
+
61
+ 1. `_Database::useQueryCache(bool: false)` at the top, because the action re-reads tables it has
62
+ just emptied and a cached result set would make it act on stale contents.
63
+ 2. `DELETE FROM Tables` — `Tables_Clients` follows automatically via `ON DELETE CASCADE`.
64
+ 3. A **defensive** `DELETE FROM Tables_Clients` afterwards, to catch any cursor row orphaned by an
65
+ earlier partial failure (the cascade only covers rows whose parent still existed).
66
+ 4. Drop every `TableResults_*` table.
67
+
68
+ **Each delete is committed immediately** with
69
+ `_Database::transactionCommit(database: _underscore::DB_CACHE)`. This is not optional:
70
+ `_Database::register()` opens a **lazy transaction**, so anything left uncommitted is rolled back
71
+ at connection close and the truncate silently does nothing.
72
+
73
+ Drops still go through the existing `dropResultTable()` **allowlist** (`^TableResults_[1-9][0-9]*$`),
74
+ so a table that merely shares the prefix is never dropped. A drop failure is logged and counted and
75
+ does **not** abort the remaining drops.
76
+
77
+ ### `Clean()` vs `Truncate()` — the difference that matters
78
+
79
+ | | `Clean()` | `Truncate()` |
80
+ |---|---|---|
81
+ | Selects | **expired** entries only | **everything** |
82
+ | Grace period | **2 minutes** — in-flight requests keep their rows | **none** |
83
+ | Safe on a busy environment | yes, by design | **no** |
84
+
85
+ `Truncate()` gives a request that is mid-build no protection: it loses its rows and serves an empty
86
+ or short page. Nothing is *corrupted* — the cache is derived data and rebuilds on the next request —
87
+ but the visible-wrong-output window is real, so it must not be run casually against a busy
88
+ environment.
89
+
44
90
  ## Key rules
45
91
 
46
92
  - **Delete-parent-then-drop-child, and re-check on the writer.** Never drop a result table based on
@@ -51,18 +97,41 @@ Action path `Platform/Cache/Clean`, scheduled on a **`*/5`** cron registered in
51
97
  (`Databases.name = 'Cache'`) in `worker2/Controller/Index.php` — the same rule as api2. There is
52
98
  deliberately no `CACHE_DATABASE_ID`: `Databases.id` is auto-assigned per Core instance and a
53
99
  hardcoded id fails silently in every environment where it does not happen to match.
100
+ - **Commit each delete immediately in `Truncate()`.** `_Database::register()`'s lazy transaction
101
+ rolls back at connection close, so an uncommitted truncate is a no-op that *looks* like it worked.
102
+ - **Never bypass `dropResultTable()`'s allowlist.** `^TableResults_[1-9][0-9]*$` is the only thing
103
+ standing between a prefix-sharing table and an irreversible `DROP`.
104
+ - **Both actions return a summary string on every path** — success, no-op, and handled failure. See
105
+ the [worker2 architecture rule](../architecture.md#cron-actions-must-return-a-result-on-both-paths);
106
+ a run that returns nothing is indistinguishable in the audit trail from one that never executed.
54
107
 
55
108
  ## Gotchas
56
109
 
57
110
  - **Worker classes are `abstract class _Worker_<Path>` with `public static` entry methods**,
58
111
  dispatched as `$className::$functionName()`. They do **not** `extends _Worker` — that is the
59
112
  dispatcher. See [Creating Worker Actions](./creating-worker-actions.md). (The 2.0
60
- `standards/framework-rules.md` worker contract still describes the older `extends _Worker` +
61
- `run()` shape and needs reconciling.)
113
+ `standards/framework-rules.md` worker contract now matches this shape — reconciled
114
+ 2026-07-28.)
62
115
  - **Adding the cron is a dbchanges2 change, not a code change** — the schedule lives in Core, so a
63
116
  new worker action is not actually scheduled until that SQL is applied per environment.
117
+ - **`Truncate()` is unscheduled on purpose.** Do not "helpfully" add a cron for it — it has no grace
118
+ period, so on a schedule it would periodically blank live requests' cache rows.
119
+ - **`ON DELETE CASCADE` is not sufficient on its own.** The extra `DELETE FROM Tables_Clients` in
120
+ `Truncate()` exists because an earlier partial failure can leave cursor rows whose parent is
121
+ already gone — the cascade never fires for those.
122
+ - **Stale reads inside a truncate.** Without `_Database::useQueryCache(bool: false)` the action
123
+ re-reads its own just-emptied tables from the query cache and mis-decides what is left.
64
124
 
65
125
  ## Change history
126
+ - 2026-07-28 — Added second action `_Worker_Platform_Cache::Truncate()` on the same class: manual /
127
+ administrative, deliberately **not** scheduled. Removes the entire cross-client cache
128
+ (`DELETE FROM Tables` + cascade, defensive `DELETE FROM Tables_Clients` for orphaned cursors, then
129
+ drops every `TableResults_*`), committing each delete immediately because `_Database::register()`'s
130
+ lazy transaction would otherwise roll it back at connection close; `useQueryCache(false)` since it
131
+ re-reads tables it just emptied; drops still gated by the `dropResultTable()` allowlist and a
132
+ failed drop is logged/counted without aborting. Unlike `Clean()` it applies **no** grace period —
133
+ an in-flight request loses its rows and serves a short page (no corruption; cache rebuilds).
134
+ Both actions return a summary string on every path. (jcardinal)
66
135
  - 2026-07-28 — Initial capture: `_Worker_Platform_Cache::Clean()` reaps expired `Cache.Tables` rows
67
136
  (2-minute grace) and drops orphaned `TableResults_*` tables (parent deleted+committed first,
68
137
  orphan status re-verified on the write host immediately before each DROP); `DB_CACHE` registered
@@ -5,7 +5,7 @@ project: _Underscore
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-06-16
8
+ updated: 2026-07-28
9
9
  owners: [jcardinal]
10
10
  files: []
11
11
  related:
@@ -26,41 +26,52 @@ All framework classes use a leading underscore prefix:
26
26
 
27
27
  - Controllers: `_Controller` base, subclasses `_Controller_<Name>`
28
28
  - Models: `_Model` base, subclasses `_Model_<Name>`
29
- - Workers: `_Worker` base, subclasses `_Worker_<Name>`
29
+ - Workers: `_Worker` dispatcher; action classes are `abstract class _Worker_<Name>` with static entry methods
30
30
  - API handlers: `_Api` base or `_Api_<Name>`
31
31
 
32
32
  Application code in `worker2` and `api2` follows the same leading-underscore convention for framework-extending classes.
33
33
 
34
34
  ## Worker contract
35
35
 
36
- Every worker class must extend `_Worker` and implement the `run()` method.
36
+ Worker action classes **do not extend `_Worker`** and have no `run()` method. A worker is an
37
+ `abstract class` of static entry methods, one method per queue action, mirrored by its file path.
37
38
 
38
39
  ```php
39
- // CORRECT
40
- class _Worker_BatchOrders extends _Worker {
41
- public function run(): void {
42
- // process the job payload from $this->payload
40
+ // CORRECT — worker2/Worker/Platform/Cache.php
41
+ abstract class _Worker_Platform_Cache {
42
+ public static function Clean(int $graceSeconds = 60): void {
43
+ // do the work; parameters arrive as named arguments
43
44
  }
44
45
  }
45
46
  ```
46
47
 
47
- The `run()` method has no parameters and returns void. Job data is accessed via `$this->payload` (or the framework's equivalent property). Do not add parameters to `run()`.
48
+ The class is `abstract` (never instantiated), the entry methods are `public static`, and the
49
+ action's queue name is the `Category/Sub/File/MethodName` path — `Platform/Cache/Clean` above.
50
+ Job data arrives as **method parameters**, not as a payload property: the dispatcher spreads the
51
+ parameter array as named arguments, so parameter names are part of the public contract. Renaming
52
+ a parameter is a breaking change to every queued job already in flight. Give parameters defaults
53
+ where a sensible one exists, so an older enqueued job missing a newly added key still runs.
48
54
 
49
55
  ## Worker dispatch — never call directly
50
56
 
51
- Workers must only be invoked via the queue dispatcher. Never call a worker's `run()` method directly from a controller, another worker, or any non-queue context.
57
+ Workers must only be invoked through the queue. Never call a worker's entry method directly from a
58
+ controller, another worker, or any non-queue context.
52
59
 
53
60
  ```php
54
61
  // CRITICAL VIOLATION — direct call
55
- $worker = new _Worker_BatchOrders($payload);
56
- $worker->run();
62
+ _Worker_Platform_Cache::Clean();
57
63
 
58
- // CORRECT — dispatch through queue
59
- _Queue::dispatch('_Worker_BatchOrders', $payload);
60
- // or the framework's equivalent dispatch method
64
+ // CORRECT — dispatch through the queue
65
+ _Worker::runTask('Platform/Cache/Clean', ['graceSeconds' => 120]);
61
66
  ```
62
67
 
63
- Direct calls bypass retry logic, error handling, visibility timeout, and monitoring. Even in development or testing, use the sync queue adapter — do not call `run()` directly.
68
+ `_Worker::runTask(string $action, array|object $parameters)` is the only supported entry point. The
69
+ action is the `Category/Sub/File/MethodName` path; `parameters` is string-keyed and is spread as
70
+ **named arguments** at dispatch (`$className::$functionName(...$parameters)`), so every key must
71
+ match a parameter name on the target method.
72
+
73
+ Direct calls bypass retry logic, error handling, SQS visibility timeout, WorkerJobs tracking and
74
+ monitoring. Even in development, dispatch through the queue — do not invoke the method directly.
64
75
 
65
76
  ## API response envelope (api2 only)
66
77
 
@@ -127,19 +138,30 @@ Do not add a worker class without registering it. Unregistered workers cannot be
127
138
  Workers must catch specific exceptions and handle them appropriately. A worker that throws an uncaught exception will be retried by the queue (up to the configured retry limit) and then dead-lettered.
128
139
 
129
140
  ```php
130
- public function run(): void {
141
+ public static function ProcessOrder(int $orderId): string {
131
142
  try {
132
- $this->processOrder($this->payload['order_id']);
143
+ self::processOrder(orderId: $orderId);
133
144
  } catch (_Exception_OrderNotFound $e) {
134
145
  // Do not retry — log and discard
135
146
  error_log('Worker: order not found, discarding job: ' . $e->getMessage());
136
- return;
147
+ return json_encode(['isSuccess' => false, 'isRetriable' => false, 'orderId' => $orderId, 'error' => $e->getMessage()]);
137
148
  } catch (_Exception_Database $e) {
138
- // Retriable — re-throw so queue retries
149
+ // Retriable — re-throw so the queue retries
139
150
  error_log('Worker: DB error, will retry: ' . $e->getMessage());
140
151
  throw $e;
141
152
  }
153
+
154
+ return json_encode(['isSuccess' => true, 'orderId' => $orderId]);
142
155
  }
143
156
  ```
144
157
 
145
- Distinguish between retriable errors (transient DB/network issues — re-throw) and non-retriable errors (bad data, missing record — log and return).
158
+ Distinguish between retriable errors (transient DB/network issues — re-throw) and non-retriable errors (bad data, missing record — log and return a failure result).
159
+
160
+ **Always return a result — on the failure path as well as the success path.** Both outcomes are
161
+ recorded against the job, and the successful results are relied on for auditing, so a worker that
162
+ returns nothing (or returns only when it succeeds) leaves a blind spot in the audit trail. Re-throw
163
+ only for genuinely retriable conditions, where the queue's retry is the intended behaviour; a
164
+ handled non-retriable failure should return a result describing what happened rather than throwing.
165
+
166
+ ## Change history
167
+ - 2026-07-28 — Corrected the **Worker contract** and **Worker dispatch** sections to match the actual codebase: worker actions are `abstract class _Worker_<Name>` with `public static` entry methods dispatched via `_Worker::runTask(string $action, array|object $parameters)` on the `Category/Sub/File/MethodName` path, with parameters spread as **named arguments** (`$className::$functionName(...$parameters)`, worker2 `Controller/Index.php:325,567`; `_underscore/Worker.php:8`). This replaces the previous `extends _Worker` / `run()` / `_Queue::dispatch()` guidance — no worker in worker2 follows it, `_Queue::dispatch()` does not exist in the codebase, and following the old text would produce a worker that never dispatches. (jcardinal)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.455",
3
+ "version": "1.0.457",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",