toga-ai 1.0.696 → 1.0.697

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.
@@ -302,6 +302,27 @@ the deploy/version timeline before hunting app bugs. **Fixed 2026-08-28:** all t
302
302
  to **Rolling / batch-size 1** (≥1 healthy backend throughout a deploy), which eliminates the blip —
303
303
  config + tradeoffs in the [deploy workflow](workflows/codepipeline-codeconnections-deploy.md).
304
304
 
305
+ **A fifth cause — a brief, self-healing "no-cause TargetGroup reduced health" flip on a
306
+ LOW-TRAFFIC, IDLE 2-instance env is EB Enhanced Health FLAPPINESS, not worker starvation.**
307
+ Distinct from the third cause (that one is the *busy* env, `api-production-1`, under Compass ASN
308
+ worker saturation). This is a single missed ALB `/health` check — a network/agent transient.
309
+ Fingerprint (verified `api-production-3`, eu-west-1, 2026-08-29 13:31 UTC `Severe`, static-`/health`
310
+ build deployed): self-heals in ~2–5 min; throughout, `HealthyHostCount` stays 2 (1-min minimum),
311
+ **0** target/ELB 5xx, sub-second latency, idle CPU, near-zero traffic; ALB target health already
312
+ reads "healthy" by the time you check. On an idle env there is no worker starvation, so **no
313
+ server-side app fix applies** — do not chase it. The mitigation is alert de-sensitization: the
314
+ worker2 EB health monitor now applies a **persistence-delay gate** (a non-5xx `Degraded`/`Severe`
315
+ must persist ≥ `nonFivexxPersistMinutes`, default 5, before it pages), which suppresses these flaps.
316
+ See [worker2 EB health monitor](../worker2/features/elastic-beanstalk-health-monitor.md).
317
+
318
+ **Triage aid — break an EB "% requests failing with HTTP 5xx" flag into ELB 502/503/504 to find
319
+ the cause:** **503** = no healthy backend (a deploy with no capacity); **504** = target timeout
320
+ (worker saturation); **502** = connection dropped mid-response (an `AllAtOnce` deploy killing
321
+ in-flight requests, or a PHP worker crash). Because **ELB-generated 5xx (any of these) never reach
322
+ PHP**, they leave **no row in `Logs.Api`** (api2 logs only *after* a completed response) — so an EB
323
+ "% 5xx" flag with an **empty `Logs.Api`** means an **ELB-level** 5xx, not an app 5xx. Discriminate
324
+ with CloudWatch `HTTPCode_ELB_502/503/504_Count` vs `HTTPCode_Target_5XX_Count`.
325
+
305
326
  ## Known issues / accepted risks
306
327
 
307
328
  Open items a maintainer should know before changing this tier. None are "bugs to fix right now" —
@@ -379,6 +400,14 @@ they are the known sharp edges. Do not re-discover these from scratch.
379
400
  single-caller branches in `V2.php` as unverified until exercised directly.
380
401
 
381
402
  ## Change history
403
+ - 2026-08-29 — Recorded a **fifth EB-`Degraded` cause**: a brief self-healing "no-cause reduced
404
+ health" flip on a low-traffic *idle* 2-instance env (e.g. `api-production-3`, eu-west-1) is EB
405
+ Enhanced Health flappiness (a single missed ALB `/health` check), distinct from the busy-env
406
+ PHP-`/health`-starvation cause — no server-side fix applies; mitigated by the worker2 monitor's
407
+ new non-5xx persistence-delay gate. Added an ELB **502/503/504 discrimination** triage aid
408
+ (503=no healthy backend, 504=target timeout/worker saturation, 502=dropped connection from an
409
+ AllAtOnce deploy or PHP crash); ELB-generated 5xx never reach PHP so they leave no `Logs.Api`
410
+ row — an EB "% 5xx" flag with an empty `Logs.Api` is ELB-level, not app 5xx. (jcardinal)
382
411
  - 2026-08-28 — Recorded a **fourth EB-`Degraded` cause — the deploy-restart ELB 502**: under the old
383
412
  `AllAtOnce`/100% policy a deploy restarts both instances of a 2-instance env at once, dropping
384
413
  in-flight connections → an ELB-502 burst (43 in one minute on `api-production-1`, 2026-08-28) that
@@ -152,6 +152,10 @@ one 15 s interval (~45 s → 30 s) of per-batch health settle.
152
152
  `BatchSize`); `aws:elasticbeanstalk:environment:process:default` (`HealthyThresholdCount`).
153
153
  - Trade-off: Rolling is slower end-to-end than the old ~31 s `AllAtOnce`, and capacity briefly
154
154
  drops to 1/2 instances per batch (fine at current traffic).
155
+ - **Confirmed working in production (2026-08-29).** The 2026-08-29 00:49 UTC deploy ran as
156
+ Rolling batches ("Batch 1 / Batch 2, one instance at a time, register and wait for healthy")
157
+ across all three API prod envs with **no post-deploy 5xx flip** — the `AllAtOnce`
158
+ deploy-restart 502 blip is eliminated in practice, not just in config.
155
159
 
156
160
  ### Deploy-policy guidance (don't reach for immutable expecting speed)
157
161
  - **Immutable does NOT reduce deploy time.** It is the **slowest** policy — it boots a fresh
@@ -295,6 +299,10 @@ aws codeconnections get-connection --connection-arn "<CONN_ARN>" --region "$REGI
295
299
  ```
296
300
 
297
301
  ## Change history
302
+ - 2026-08-29 — Confirmed the Rolling/batch-1 policy works in production: the 2026-08-29
303
+ 00:49 UTC deploy ran as Rolling batches (one instance at a time, register + wait for healthy)
304
+ on all three API prod envs with **no post-deploy 5xx flip** — the AllAtOnce deploy-restart
305
+ 502 blip is eliminated. (jcardinal)
298
306
  - 2026-08-28 — Recorded the **production deploy trigger + policy change**: the `api-production-1/2/3`
299
307
  CodePipelines **auto-deploy on every push to `_production`** (WebhookV2 on the CodeConnections
300
308
  connection, repos `_underscore` + `api2`), so a merge to `_production` **is** a three-region prod
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-25
9
+ updated: 2026-08-29
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - worker2/Worker/Infrastructure/CloudWatch.php
@@ -28,10 +28,13 @@ OneUptime string-matches — OneUptime cannot compare numbers on a pushed body.
28
28
 
29
29
  The load-bearing design decision this doc records is the **paging (alarm) criteria**, and
30
30
  the key **2026-08-25** change: the 5xx page is now driven by a **CloudWatch windowed
31
- request-rate gate**, fully **decoupled from EB's own `HealthStatus`**. A non-5xx
32
- `Degraded`/`Severe` still pages immediately off EB health; but whether a *5xx* problem pages
33
- is decided by the true per-environment 5xx **rate over a trailing window** (default 5 min),
34
- not by EB's ~10-second `ApplicationMetrics` snapshot.
31
+ request-rate gate**, fully **decoupled from EB's own `HealthStatus`**. Whether a *5xx*
32
+ problem pages is decided by the true per-environment 5xx **rate over a trailing window**
33
+ (default 5 min), not by EB's ~10-second `ApplicationMetrics` snapshot. As of **2026-08-29**,
34
+ a non-5xx `Degraded`/`Severe` no longer pages immediately either — it must **persist** past a
35
+ threshold (`nonFivexxPersistMinutes`, default 5) before paging, which suppresses the brief
36
+ self-healing "reduced health" flips that EB Enhanced Health throws on low-traffic 2-instance
37
+ envs (see the persistence gate below).
35
38
 
36
39
  **Why the change:** the old gate read EB `describeEnvironmentHealth`'s `ApplicationMetrics`,
37
40
  a ~10-**second** window. At low traffic a single 500 in a near-empty bucket reads as 100%
@@ -52,6 +55,10 @@ itself runs non-fatal. CloudWatch read failures are caught **internally** and re
52
55
  `_Worker::runTask('Infrastructure/CloudWatch/ElasticBeanstalkHealth', {awsAccountId,
53
56
  environments, oneuptimeUrl, …thresholds})`.
54
57
  - New private `fetchWindowedRequestCounts()` reads the CloudWatch metrics (below).
58
+ - New private `fetchNonOkDepartureAgeSeconds(ElasticBeanstalkClient $eb, string
59
+ $environmentName, int $lookbackSeconds): ?int` returns how long the current non-Ok episode
60
+ has persisted, from EB `describeEvents` (below). **Never throws** — catches `AwsException`
61
+ and returns `null`.
55
62
  - AWS access is obtained through `_Component_Aws_Workloads::assumeCredentials()` (STS-assume
56
63
  a read-only role per account, one assume reused across regions) — see
57
64
  [Cross-account AWS access](./cross-account-aws-access.md), which also documents the
@@ -64,11 +71,14 @@ itself runs non-fatal. CloudWatch read failures are caught **internally** and re
64
71
 
65
72
  Two independent paths now feed `alarm=HIGH`:
66
73
 
67
- 1. **EB-health path (non-5xx).** EB `HealthStatus` `Degraded` or `Severe` that is **not**
68
- attributable solely to 5xx (latency, instances down, a failed deploy, a mixed cause, or
69
- no stated cause) **pages immediately** — unchanged, still driven off EB health.
70
- `Suspended` never pages. `causesAttributeSolelyTo5xx()` is what tells a solely-5xx event
71
- apart from everything else.
74
+ 1. **EB-health path (non-5xx) — now gated on PERSISTENCE (2026-08-29).** EB `HealthStatus`
75
+ `Degraded` or `Severe` that is **not** attributable solely to 5xx (latency, instances
76
+ down, a failed deploy, a mixed cause, or no stated cause) pages **only once the current
77
+ non-Ok episode has persisted ≥ `nonFivexxPersistMinutes`** (new optional param, default 5)
78
+ — it no longer pages on the first snapshot. A sub-threshold blip is **reported but does not
79
+ page**. `Suspended` never pages. `causesAttributeSolelyTo5xx()` is what tells a solely-5xx
80
+ event apart from everything else. See the persistence-gate section below for how episode
81
+ age is measured and the fail-safe.
72
82
  2. **CloudWatch windowed 5xx-rate path.** For a solely-5xx `Degraded`/`Severe`, the page is
73
83
  decided **not** by EB but by the true windowed rate from CloudWatch. Given the window's
74
84
  `requests` (ApplicationRequestsTotal) and `fivexx` (ApplicationRequests5xx), page when:
@@ -91,6 +101,7 @@ The four thresholds are **optional cron parameters** spread as **named arguments
91
101
  | `minRequests` | `100` | Minimum windowed request count before the **share** gate applies |
92
102
  | `min5xxPercent` | `20.0` | 5xx share (percent) at/above which a solely-5xx env pages, once `minRequests` is met |
93
103
  | `min5xxAbsolute` | `20` | Absolute windowed 5xx count at/above which a solely-5xx env pages regardless of share |
104
+ | `nonFivexxPersistMinutes` | `5` | Minutes a **non-5xx** `Degraded`/`Severe` episode must persist before it pages (added 2026-08-29; a trailing optional param — existing `CronJobs.parameters` without the key still run) |
94
105
 
95
106
  Removed this iteration: `shouldPageForHealth()` and the old
96
107
  `HTTP_5XX_ALARM_RATIO` / `HTTP_5XX_ALARM_ABSOLUTE_COUNT` constants (superseded by the
@@ -123,6 +134,43 @@ A CloudWatch read *failure* (as opposed to empty data) sets **`probe=DEGRADED`**
123
134
  gap), distinct from a measured-bad `alarm=HIGH` — same alarm-vs-probe split as the
124
135
  multi-client monitors in [OneUptime push-metric monitors](./oneuptime-worker2-monitoring.md).
125
136
 
137
+ ## Persistence gate on the non-5xx page (2026-08-29)
138
+
139
+ A non-5xx `Degraded`/`Severe` now pages only after the **current non-Ok episode has persisted
140
+ ≥ `nonFivexxPersistMinutes`** (default 5). This exists because the brief "no-cause TargetGroup
141
+ reduced health" flips on low-traffic 2-instance envs are **EB Enhanced Health flappiness** —
142
+ a single missed ALB `/health` check (a network/agent transient), not a real app problem — and
143
+ they self-heal in ~2–5 min. A persistence delay suppresses those while a genuinely sustained
144
+ non-5xx degradation still pages, delayed only by the threshold. (The windowed 5xx-flood gate
145
+ is **unchanged** — it has its own, independent decision.)
146
+
147
+ **Fingerprint of the idle-env flap** (verified `api-production-3`, eu-west-1, 2026-08-29
148
+ 13:31 UTC `Severe`, static-`/health` build deployed): self-heals in ~2–5 min; throughout,
149
+ `HealthyHostCount` stays 2 (1-min minimum), **0** target/ELB 5xx, sub-second latency, idle
150
+ CPU, near-zero traffic; ALB target health already reads "healthy" by the time you look. On an
151
+ idle env there is **no worker starvation**, so there is nothing to fix server-side — the
152
+ mitigation is this monitor persistence-delay gate, not more app code. **This is DISTINCT from
153
+ the PHP-`/health`-starvation cause** (that one is the *busy* env, `api-production-1`, under
154
+ ASN worker saturation, fixed by the static-`/health` file) — see
155
+ [api2 architecture — EB "Degraded" health](../../api2/architecture.md).
156
+
157
+ **Measuring episode age — `fetchNonOkDepartureAgeSeconds()`** reads EB `describeEvents`:
158
+
159
+ - The most recent **"transitioned from Ok to …"** event marks the current episode's start.
160
+ Once non-Ok, later transitions stay between non-Ok states until a to-Ok event; because
161
+ `describeEvents` returns **newest-first**, the first "from Ok to …" match wins.
162
+ - The query is **TIME-bounded**: `StartTime = now − (persistWindow +
163
+ HEALTH_EVENTS_LOOKBACK_BUFFER_SECONDS = 600)`, with a `MaxRecords = 100` cap — so a chatty
164
+ env's deploy/config events cannot push the departure event out of range.
165
+ - **Fail-safe:** an episode **older than the window** returns `null` → the caller **pages**.
166
+ That is correct: if the departure is beyond the lookback, the episode has already persisted
167
+ past the threshold, so paging is the safe outcome. A read failure also returns `null`
168
+ (the method catches `AwsException` and never throws) → same fail-safe.
169
+
170
+ **Auditable suppression:** a sub-threshold blip is reported, not paged, and the suppression is
171
+ made visible — `pendingPersistSeconds` is added to the OneUptime payload and a `pending=Ns`
172
+ annotation to the WorkerJobs summary — so a suppressed episode is never silent.
173
+
126
174
  ## Reading the windowed 5xx rate from CloudWatch (durable AWS reference)
127
175
 
128
176
  - **EB application-request metrics are PER-INSTANCE ONLY.** `ApplicationRequestsTotal`,
@@ -214,6 +262,8 @@ the `probe` token; this is a OneUptime config change the developer applies.
214
262
  `min5xxPercent`; absolute at `min5xxAbsolute-1` vs `min5xxAbsolute`; `requests` just below
215
263
  `minRequests`; empty/zero window; mixed 5xx + non-5xx cause) — deferred pending a harness.
216
264
  - **Deploy fails without degrading health** — still not caught (accepted residual).
265
+ - **`ElasticBeanstalkHealth()` is long** and grew with the persistence gate — php-reviewer
266
+ (0 critical) flagged it as a decomposition candidate; deferred pending the missing harness.
217
267
 
218
268
  ## Gotchas / known issues
219
269
 
@@ -235,6 +285,21 @@ the `probe` token; this is a OneUptime config change the developer applies.
235
285
  record its value in a doc.
236
286
 
237
287
  ## Change history
288
+ - 2026-08-29 — Added a **persistence gate on the non-5xx page**: a non-5xx `Degraded`/`Severe`
289
+ no longer pages on the first snapshot — it must persist ≥ `nonFivexxPersistMinutes` (new
290
+ optional trailing param, default 5; existing `CronJobs.parameters` without the key still
291
+ run) before paging. Episode age comes from EB `describeEvents` via new never-throwing helper
292
+ `fetchNonOkDepartureAgeSeconds()` (most recent "from Ok to …" marks episode start; events
293
+ are newest-first so the first match wins; TIME-bounded `StartTime = now − (persistWindow +
294
+ HEALTH_EVENTS_LOOKBACK_BUFFER_SECONDS=600)`, `MaxRecords=100`, so an episode older than the
295
+ window returns `null` → the caller pages — correct fail-safe). Sub-threshold blips are
296
+ reported not paged, with `pendingPersistSeconds` (OneUptime payload) + `pending=Ns`
297
+ (WorkerJobs summary) so suppressions stay auditable. The windowed 5xx gate is unchanged.
298
+ Rationale: the brief self-healing "no-cause reduced health" flips on low-traffic 2-instance
299
+ envs are EB Enhanced Health flappiness (a single missed ALB `/health` check), not app bugs
300
+ — distinct from the busy-env PHP-`/health`-starvation cause. worker2-only; the param is
301
+ optional-with-default so no `_underscore`/dbchanges2 change. Not yet committed/deployed.
302
+ (jcardinal)
238
303
  - 2026-08-25 — Replaced the EB `ApplicationMetrics` 5xx gate (a ~10s window that false-paged
239
304
  "100% failing" on a lone 500 in a near-empty bucket — real worker-production incident: 16
240
305
  500s / 516 req = 3.1% over 17 min) with a **CloudWatch windowed request-rate gate**. Reads
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.696",
3
+ "version": "1.0.697",
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",