toga-ai 1.0.952 → 1.0.954

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.
@@ -12,6 +12,7 @@
12
12
  | [Tools — Developers Folder (UUID & Password Generators)](features/developer-tools.md) | Tools "Developers" folder — the Generate UUID and Generate Password tools (reference example of the one-tool-per-`mvc/<route>/get.php` pattern); open for the ge |
13
13
  | [/emails Email Queue Review (Tools → 2.0 Logs_<Client>.Email)](features/email-queue-review.md) | Staff page to browse any 2.0 client's `Logs_<Client>.Email` queue and release/reject `HELD` review-hold emails. |
14
14
  | [/errors Curation Console (Tools → shared Core Logs DB)](features/errors-curation-console.md) | Internal-only 1.0 Tools triage/curation screen (`/errors`) for the 2.0 Issue/Event error pipeline, reading the shared Core Logs DB via `db_toga2logs`; curates, |
15
+ | [Etilize Refresh page (tools /etilize/refresh → worker2 Etilize actions)](features/etilize-refresh.md) | Tools page to re-pull Etilize specs/images/translations for up to 25 items of any Etilize client by chaining existing worker2 actions; open when changing the pa |
15
16
  | [Force Logout (tools /developers/force-logout → worker2 sets META_LAST_REFRESH_DATETIME)](features/force-logout.md) | Tools page that signs every 2.0 front-end user out after a big deploy by changing Core parameter `META_LAST_REFRESH_DATETIME` through a worker2 job; open to tri |
16
17
  | [GitHub Audit (monthly code-churn report)](features/github-audit.md) | Internal monthly code-churn report at `/developers/github-audit` (lines added/removed across every `agilantsolutions` repo for one month); open for the two-phas |
17
18
  | [Legacy Email Notifier (tools /email-migration/notify)](features/legacy-email-notifier.md) | SSO-gated admin tool at `/email-migration/notify` that emails employees a digest of mail still hitting their legacy domains (asks senders to switch to `@togatec |
@@ -0,0 +1,57 @@
1
+ ---
2
+ title: Etilize Refresh page (tools /etilize/refresh → worker2 Etilize actions)
3
+ framework: "1.0"
4
+ repo: tools
5
+ project: Tools
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-05
10
+ owners: [bala]
11
+ files:
12
+ - tools/_/app/etilize/refresh.php
13
+ - tools/mvc/etilize/refresh/get.php
14
+ - tools/mvc/etilize/refresh/post.php
15
+ - tools/_/app/nav.php
16
+ related:
17
+ - ./worker2-job-bridge.md
18
+ - ./mvc-data-access-patterns.md
19
+ - ./persona-gated-navigation.md
20
+ - ../../../2.0/apps/worker2/features/etilize-catalog-item-import.md
21
+ - ../../../2.0/apps/worker2/features/etilize-item-translation-import.md
22
+ - ../../../clients/compass-canada/features/french-item-feature-translations.md
23
+ ---
24
+
25
+ Tools page to re-pull Etilize specs/images/translations for up to 25 items of any Etilize client by chaining existing worker2 actions; open when changing the page, the step order, or the client list.
26
+
27
+ ## Summary
28
+
29
+ Developers > Etilize Refresh (`/etilize/refresh`, persona **Development Team** only). Pick a client, search items (part number or title), add up to 25 items each with an Etilize PID, tick English and/or a client language (e.g. fr-CA), press Refresh. No new worker2 code: each step is an existing worker2 action queued through the [job bridge](./worker2-job-bridge.md). Works for every client DB that has `Items.c_etilizeProductId` (37 at build time; tested on Compass Canada).
30
+
31
+ ## How it works
32
+
33
+ A run is up to 3 steps, in this order. Each later step runs only for items that did not fail an earlier one.
34
+
35
+ 1. `Etilize/Items/RefreshItemsFromEtilize(client, items[{partNumber, etilizeId}], itemCatalog=<catalog id>)` — one job per internal catalog. Saves the PID, re-pulls specs + images. The PID is validated at Etilize first, so a bad PID writes nothing.
36
+ 2. `Etilize/ItemTranslations/BackfillEtilizeAttributeIds(client, partNumbers)` — **required before translations.** Step 1 creates new `Features` without `c_etilizeAttributeId`; the translation pass matches features by that id, so without it new specs stay English.
37
+ 3. `Etilize/ItemTranslations/ImportItemsLanguageByPartNumbers(client, languageCode, languageName, partNumbers)` — one per ticked language. Reads `Items.c_etilizeProductId`, so it must run after step 1. Language jobs do not block each other.
38
+
39
+ Run state:
40
+ - The chain is advanced **server-side on each poll** (`post.php`), not by page JS.
41
+ - Run state lives in `$_SESSION`, so a closed tab resumes when the page is reopened. One unfinished run per session.
42
+ - Status per job comes from the JSON in `WorkerJobs.output`, not `isSuccess` (see Gotchas).
43
+
44
+ Client list and DB access (`App_Etilize_Refresh`):
45
+ - `Core.Clients` + `Databases` + `DatabaseHosts` for the `ENVIRONMENT` slug, kept to client DBs that have `Items.c_etilizeProductId` (checked in `information_schema`).
46
+ - Client DBs are read over the existing `[database_true]` connection (all prod `Client_` DBs share one writer host). A client whose `DatabaseHosts.hostWriter` differs from the `[database_true]` hostname is refused.
47
+ - The DB name always comes from Core and is regex-checked before use. Core and Client queries are never joined.
48
+
49
+ ## Gotchas
50
+
51
+ - **Etilize actions report failure inside a "successful" job.** They catch their own errors and return `{success:false, errors:["item <partNumber>: <msg>"], fatal?}` while `WorkerJobs.isSuccess = 1`. Read per-item failures from `errors`. General rule in [job bridge](./worker2-job-bridge.md).
52
+ - **Send only the exact worker2 parameter names.** An extra key (e.g. `partNumber` on the backfill) fails the whole job with *Unknown named parameter*. See [creating worker actions](../../../2.0/apps/worker2/features/creating-worker-actions.md).
53
+ - **JSON posted from page JS arrives HTML-escaped** and must be un-escaped before `json_decode` (see [mvc patterns](./mvc-data-access-patterns.md)).
54
+ - **Duplicate jobs from double requests** are guarded at the DB, not the session (see [job bridge](./worker2-job-bridge.md)).
55
+
56
+ ## Change history
57
+ - 2026-10-05 — Created: Etilize Refresh page chaining Refresh → Backfill → per-language translation, server-side chain on poll, session-resumable runs. (bala)
@@ -6,7 +6,7 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-05
10
10
  owners: [jcardinal, bala, ajean]
11
11
  files:
12
12
  - tools/_/app/nav.php
@@ -14,6 +14,7 @@ files:
14
14
  - library/app/error.php
15
15
  - library/app/database.php
16
16
  - library/app/string.php
17
+ - library/app/framework.php
17
18
  related:
18
19
  - ./persona-gated-navigation.md
19
20
  - ./compass-user-persona-admin.md
@@ -92,6 +93,9 @@ This affects **every 1.0 app**, not just Tools.
92
93
 
93
94
  Use the project's own stripper before logging or emailing a request value: `App_String::stripBreaksAndTabs($input, $replaceWith)`. It remains a character filter, not an escaper — still escape/cast before the value reaches SQL.
94
95
 
96
+ ### Every `$_GET`/`$_POST` value is HTML-escaped on arrival — JSON posts break
97
+ `library/app/framework.php` (~line 64) runs every `$_REQUEST`/`$_GET`/`$_POST` value through `App_String::xssProtect()` (htmlspecialchars). JSON posted from page JS arrives as `{&quot;...&quot;}` and `json_decode` returns `NULL`. Undo it **once** with `App_String::xssUnprotect()` before `json_decode`, then validate the decoded values as usual.
98
+
95
99
  ### By-reference row helpers
96
100
  `buildArrayOfRows`, `fetchOne`, and `fetchRow` take `$res` **by reference**. Assign the `query()` result to a **variable first**, then pass it — passing the call result inline emits PHP "Only variables should be passed by reference", which **renders to the page** when `display_errors` is on.
97
101
 
@@ -154,3 +158,6 @@ set_error_handler(fn(): bool => true); // swallow non-fatal notices/deprecations
154
158
  - [SAML SSO auth](./saml-sso-auth.md)
155
159
  - [Tools architecture](../architecture.md)
156
160
  - [1.0 MVC page pattern & app skeleton](../../library/features/mvc-page-pattern-and-app-skeleton.md)
161
+
162
+ ## Change history
163
+ - 2026-10-05 — Added the request-input xssProtect gotcha (JSON posts decode to NULL). (bala)
@@ -6,8 +6,8 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
10
- owners: [jcardinal]
9
+ updated: 2026-10-05
10
+ owners: [jcardinal, bala]
11
11
  files:
12
12
  - tools/assets/css/theme.css
13
13
  - tools/assets/js/theme.js
@@ -97,6 +97,7 @@ Two `<img>` wordmarks are in the markup and CSS shows one per theme (`.login-log
97
97
  3. **Per-file colour multiset vs. `git HEAD`.** Compare the *multiset* of resolved colour values per file, not a line-by-line diff — an index/line-based diff is useless once lines are inserted, whereas a multiset comparison is immune to line-number shifts.
98
98
 
99
99
  ## Gotchas
100
+ - **Global input rules fight compact inputs.** `style.css` sets `input[type=text] { width: 90px }` and a large padded/shadowed rule on every input except `#searchQuery` and `.stat-value`. For a compact input on a Tools page, add class `stat-value` and style it with a 2-class selector (e.g. `.my-table .stat-value`) so it beats `input[type=text]`.
100
101
  - **`assets/css/style.scss` is DEAD in this repo — edit `style.css` directly.** Many recent commits touch the `.css` while the `.scss` was last touched in an old ticket; the `.css` has drifted ~450 lines ahead. There is no sass binary and no `package.json`. "Helpfully" recompiling the SCSS would **silently revert a large amount of shipped work**.
101
102
  - **`App_FrameworkIndex::setHeaderCode()` is the only hook for injecting markup into `<head>` in 1.0** (library echoes `$this->headerCode` inside `<head>` after the CSS/JS includes). It **SETS rather than appends**, and before this feature it was unused anywhere in tools — so a **second caller silently clobbers the first**. If another feature needs head markup, refactor to compose both strings; do not add a second `setHeaderCode()` call.
102
103
  - **A colour literal used as both a foreground and a background must be split into two tokens**, or dark mode inverts it the wrong way. Real cases in tools: `#10113A` (body text *and* nav/public-header background), `#20242c` (text-strong *and* a dark chip fill), `#999` (text grey *and* a badge fill), `#eef0f4` (border *and* badge fill), `#0f172a` (design.css `--d-ink` text *and* the code-block background). Detect these **mechanically** — collect, per candidate token, the set of CSS properties it appears with (background vs. colour/border). Do not eyeball it.
@@ -119,3 +120,6 @@ Two `<img>` wordmarks are in the markup and CSS shows one per theme (`.login-log
119
120
  - [Legacy email notifier](./legacy-email-notifier.md)
120
121
  - [Design demo admin](./design-demo-admin.md)
121
122
  - [Frontend standard](../../../standards/frontend.md)
123
+
124
+ ## Change history
125
+ - 2026-10-05 — Added the global input width/padding gotcha and the `stat-value` opt-out. (bala)
@@ -6,12 +6,14 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-01
9
+ updated: 2026-10-05
10
10
  owners: [mhammontree, bala]
11
11
  files:
12
12
  - tools/_/app/worker.php
13
13
  related:
14
14
  - ./force-logout.md
15
+ - ./etilize-refresh.md
16
+ - ../../../2.0/apps/worker2/features/creating-worker-actions.md
15
17
  - ../../../clients/compass-usa/features/people-file-import-admin-page.md
16
18
  - ./mvc-data-access-patterns.md
17
19
  - ../../../2.0/apps/worker2/architecture.md
@@ -50,6 +52,10 @@ empty result — because the wrapper format is worker2's, not a contract.
50
52
  (c) both succeeded → `ok=true`.
51
53
  A caller that tests only `workerJobId === null` reads (b) as success and then shows "still
52
54
  running" forever, because no worker will ever pick that row up. **Test `ok`, not the id.**
55
+ - **`isSuccess = 1` does not mean the work succeeded.** Actions that catch their own errors (e.g. all Etilize actions) return `{success:false, errors:[...]}` and the job is still stamped `isSuccess = 1`. Decide success from the decoded `output` JSON (`success`, `errors`, `fatal`), not the column.
56
+ - **`Core.WorkerJobs.parameters` is a MySQL JSON column.** MySQL re-orders keys and adds spaces, so `parameters = '<json string>'` never matches. Compare with `parameters = CAST('<json>' AS JSON)`. Filter on the index `WorkerJobs_scheduling_IDX (isSuccess, dtQueued, dtStarted)` first.
57
+ - **Guard duplicate enqueues in the DB, not the session.** Tools sessions use the `rediscluster` handler with no session locking, so two concurrent requests both read and write `$_SESSION`. Before enqueuing, reuse a pending identical row (same action + `CAST` parameters, last 2 minutes).
58
+ - **Send only the action's exact parameter names** — the dispatcher spreads them as named args; one extra key fails the job (*Unknown named parameter*). See [creating worker actions](../../../2.0/apps/worker2/features/creating-worker-actions.md).
53
59
  - **`isSuccess IS NULL` means two different things** — running, or abandoned. Same trap as
54
60
  [stranded worker jobs](../../../2.0/apps/worker2/features/stranded-worker-jobs.md).
55
61
  - **Tools must not speak SFTP (or any integration worker2 owns).** Decided on CTO review
@@ -75,5 +81,6 @@ empty result — because the wrapper format is worker2's, not a contract.
75
81
  4. Dev-bypass login must use a `Client_True` email (e.g. `@togatech.com`, not `@goagilant.com`).
76
82
 
77
83
  ## Change history
84
+ - 2026-10-05 — Added: isSuccess=1 can hide a failed action, JSON-column parameters compare, DB-level duplicate-enqueue guard (Etilize Refresh page). (bala)
78
85
  - 2026-10-01 — Added the local tools→worker2 test setup and the prod-only `[worker]` config fact (TRUE-82414). (bala)
79
86
  - 2026-09-23 — Created from TRUE-82250: documented the three `enqueue()` outcomes, the log-line-wrapped `output`, and the CTO decision that tools never speaks SFTP (phpseclib 1.x sha1-only KEX + credential duplication). (mhammontree)
@@ -6,13 +6,14 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-05
10
10
  owners: [bala]
11
11
  files:
12
12
  - worker2/Worker/Etilize/Items.php
13
13
  related:
14
14
  - ./creating-worker-actions.md
15
15
  - ./etilize-item-translation-import.md
16
+ - ../../../1.0/apps/tools/features/etilize-refresh.md
16
17
  ---
17
18
 
18
19
  Client-generic catalog onboarding: ingest an item catalog from an S3 CSV and enrich each item from the Etilize datasheet, writing directly to `DB_CLIENT`; open when importing/refreshing catalog items or debugging CSV mapping.
@@ -29,7 +30,7 @@ Routes are `Etilize/Items/<Method>` (see [creating-worker-actions](./creating-wo
29
30
 
30
31
  1. `ImportItemsFromCsvFile(string $client, string $s3Url, string $catalogUuid)` — **normal import**: one `ItemPrice` + `VendorItemCost` per item with **NULL currency**, using the Cost / Lease Price columns (e.g. Compass).
31
32
  2. `ImportItemsWithCurrencyPricesFromCsvFile(string $client, string $s3Url, string $catalogUuid)` — **per-currency import**: each currency-coded column becomes its own `ItemPrice` + `VendorItemCost` (e.g. Quad).
32
- 3. `RefreshItemsFromEtilize(string $client, array $items, ?string $catalog = null)` where `items = [{partNumber, etilizeId}]` — looks each item up by `partNumber` and, only if its stored `c_etilizeProductId` differs from the supplied `etilizeId`, updates it and re-pulls the item's features + images from the Etilize datasheet. Returns a per-item changed/unchanged summary.
33
+ 3. `RefreshItemsFromEtilize(string $client, array $items, ?string $catalog = null, ?string $itemCatalog = null)` where `items = [{partNumber, etilizeId}]`. `catalog` is the **Etilize** catalog (default `na`); `itemCatalog` is the **internal** catalog (id or name) that scopes the part-number lookup when a part number exists in several catalogs. Returns a per-item changed/unchanged summary. Run from the Tools [Etilize Refresh page](../../../1.0/apps/tools/features/etilize-refresh.md).
33
34
 
34
35
  ### Import flow
35
36
 
@@ -54,11 +55,14 @@ Both import actions delegate to the shared private `runItemCsvImport(..., bool $
54
55
 
55
56
  ### Etilize re-pull (`RefreshItemsFromEtilize`)
56
57
 
57
- - Item located by `partNumber`. The Etilize id is updated **only when** the supplied `etilizeId` differs from the stored `c_etilizeProductId`.
58
+ - Item located by `partNumber` (scoped by `itemCatalog` if given). The PID is **validated at Etilize first** (`getProduct` must return a product), so a bad PID writes nothing and never overwrites a working one.
59
+ - `c_etilizeProductId` is updated only when it differs; features + images are **re-pulled every run**.
60
+ - New features are created **without `c_etilizeAttributeId`** — run `ItemTranslations/BackfillEtilizeAttributeIds` before any translation import, or the new specs stay English ([translation import](./etilize-item-translation-import.md)).
58
61
  - The feature/image re-pull is **find-or-create** and **does not delete stale features** — intentional.
59
62
 
60
63
  ## Gotchas
61
64
 
65
+ - **Failures come back inside `isSuccess = 1`.** All actions catch their own errors and return `{success:false, errors:["item <partNumber>: <msg>"], fatal?}`; the job is still stamped success. Callers must read the `output` JSON.
62
66
  - CSV layout is fixed at the top: header row, a **skipped** description row, then data from row 3. Feeding a two-row (header+data) file would treat the first data row as the description row.
63
67
  - Columns are matched by header regex, not position — a renamed/missing header silently drops that column rather than mis-mapping by index.
64
68
  - Currency columns not present in the client's `Currencies` table are ignored, so a catalog that ships an unconfigured currency simply won't price it.
@@ -67,3 +71,6 @@ Both import actions delegate to the shared private `runItemCsvImport(..., bool $
67
71
 
68
72
  - [Creating worker actions](./creating-worker-actions.md) — the action class/method conventions.
69
73
  - [Etilize item translation import](./etilize-item-translation-import.md) — non-English text sidecar import for the same items.
74
+
75
+ ## Change history
76
+ - 2026-10-05 — Corrected the RefreshItemsFromEtilize signature (`itemCatalog`), PID pre-validation, re-pull every run, and the failure-in-isSuccess=1 contract. (bala)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-05
10
10
  owners: [bala]
11
11
  files:
12
12
  - worker2/Worker/Etilize/ItemTranslations.php
@@ -33,7 +33,7 @@ Routes are `Etilize/ItemTranslations/<Method>` (see [creating-worker-actions](./
33
33
 
34
34
  1. `ImportAllItemsForLanguage(string $client, string $languageCode, ?string $languageName = null, ?string $catalog = null)` — translates **all** items that have a `c_etilizeProductId`.
35
35
  2. `ImportItemsLanguageByPartNumbers(string $client, string $languageCode, array $partNumbers, ?string $languageName = null, ?string $catalog = null)` — translates only the given part numbers (uses a `partNumber IN (...)` filter).
36
- 3. `BackfillEtilizeAttributeIds(...)` — populates `c_etilizeAttributeId` (on `Features`) and `c_etilizeAttributeGroupId` (on `ItemCategoryFeatureGroups`) from Etilize `en_us`, so features and groups can be matched to their Etilize attribute id. Must run **before** feature-translation import so each French value lands on the right feature/group.
36
+ 3. `BackfillEtilizeAttributeIds(string $client, ?array $partNumbers = null)` — populates `c_etilizeAttributeId` (on `Features`) and `c_etilizeAttributeGroupId` (on `ItemCategoryFeatureGroups`) from Etilize `en_us`, so features and groups can be matched to their Etilize attribute id. Must run **before** feature-translation import so each French value lands on the right feature/group — including after every `RefreshItemsFromEtilize`, which creates new features without the id.
37
37
 
38
38
  ### Import flow
39
39
 
@@ -70,3 +70,6 @@ Beyond `ItemTranslations` (Item title/description/longDescription), `importItemL
70
70
  - [Etilize catalog item import](./etilize-catalog-item-import.md) — the English catalog import these translations sit beside.
71
71
  - [Language Translation Layer](../../api2/features/language-translation-layer.md) — read-side metadata wiring.
72
72
  - [Compass Canada French item-feature translations](../../../clients/compass-canada/features/french-item-feature-translations.md) — the client use case.
73
+
74
+ ## Change history
75
+ - 2026-10-05 — Recorded the BackfillEtilizeAttributeIds signature and that it must re-run after every English refresh. (bala)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-05
9
+ updated: 2026-10-06
10
10
  owners: ["jcardinal", "mhammontree", "bala", "rgirish"]
11
11
  files:
12
12
  - worker2/Worker/Monitor/Compass.php
@@ -509,6 +509,18 @@ against the live SFTP:
509
509
  (`rawlist` returned php-type=array, `type=1` for files, and `mtime` as a unix epoch matching the
510
510
  filename timestamp.)
511
511
 
512
+ Timeout facts (apply the [in-run retry](#suppress-a-one-off-transient-in-the-reporter-not-in-oneuptime-in-run-retry) with these):
513
+
514
+ - **TCP connect timeout = 3rd constructor arg** `new SFTP($host, $port, $timeout)`, default 10s.
515
+ `setTimeout()` does NOT change it.
516
+ - **`setTimeout()` is per-read**, applied separately to key exchange, auth reply, channel open, SFTP
517
+ init, and each readdir packet — one attempt can far exceed one timeout value.
518
+ - Bound retries by an **overall deadline from run start**; split remaining time across steps
519
+ (per-step = min(step cap, remaining / steps)); stop when that falls below a floor. Instance: Staples
520
+ (75s deadline, 15s cap, 4 steps, 5s floor).
521
+ - **Never retry a login refusal** — throw a distinct code; retries can lock the account.
522
+ - `disconnect()` in `finally`.
523
+
512
524
  **Symptom when either is wrong:** reads guarded by `isset()`/`??` silently make the aged-file count
513
525
  always **0**, so the monitor never alarms and reads as perfectly healthy. Test the count against a folder
514
526
  you know has aged files.
@@ -15,7 +15,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
15
15
  - **test** (Test) — 16 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
16
16
  - **toga** (TOGa) — 3 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
17
17
  - **store** (Store) — 3 doc(s) → [1.0/apps/store/INDEX.md](1.0/apps/store/INDEX.md)
18
- - **tools** (Tools) — 24 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
18
+ - **tools** (Tools) — 25 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
19
19
 
20
20
  ## 2.0 framework
21
21
 
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: staples
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-16
10
- owners: ["bala"]
9
+ updated: 2026-10-06
10
+ owners: ["bala", "jcardinal"]
11
11
  files:
12
12
  - worker2/Worker/Monitor/Staples.php
13
13
  - worker2/Config/beta.ini
@@ -60,6 +60,16 @@ The Staples pipeline runs in the **1.0 `worker`** tier, not worker2; worker2 onl
60
60
  - Alarm when **>= 6 aged files** (tunable — can drop to 1 for single-file sensitivity).
61
61
  - **5-minute cron cadence** (`*/5 * * * *`), chosen to pair with the OneUptime heartbeat criteria (received-in-8-min + `alarm:OK` recovery; not-received-15-min offline; not-received-10-min degraded), matching the existing Compass monitor convention rather than the source crons' hourly rate. The fast cadence is for a **reliable heartbeat**, not because the folders change that fast — the folder cutoffs above are calibrated against the **hourly 1.0 crons**, per the shared doc's "calibrate against the tier that runs the business logic" rule.
62
62
 
63
+ ### In-run retry (transient connect failures)
64
+
65
+ Both actions retry the SFTP connect+list inside one run (shared [in-run retry](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md#suppress-a-one-off-transient-in-the-reporter-not-in-oneuptime-in-run-retry) + [phpseclib3 timeout facts](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md#sftp-monitoring-phpseclib3)). Added after a one-off `sftp_read_failed` / `fsockopen(): ... Connection timed out` took the inbound monitor offline (2026-10-06).
66
+
67
+ - `MAX_CONNECT_ATTEMPTS=3`, `RETRY_DELAY_SECONDS=5`, hard `SFTP_DEADLINE_SECONDS=75` from run start.
68
+ - Per-step timeout = min(`SFTP_STEP_TIMEOUT_SECONDS=15`, remaining / `SFTP_STEPS_PER_ATTEMPT=4`); stop retrying below `SFTP_MIN_STEP_TIMEOUT_SECONDS=5`.
69
+ - Login refusal throws `ERROR_CODE_LOGIN_FAILED` — not retried (no account lockout).
70
+ - `$sftp->disconnect()` in `finally`; error push includes `attempts`.
71
+ - Worst case ~75s SFTP + 10s push, under the 120s `maxExecutionTime`.
72
+
63
73
  ## Gotchas
64
74
 
65
75
  - **phpseclib3 `rawlist` returns arrays, and its SFTP type constants are not loaded** — see the shared doc's [SFTP monitoring gotchas](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md#sftp-monitoring-phpseclib3). Getting this wrong silently makes the aged-file count always 0, so the monitor never alarms and looks healthy.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.952",
3
+ "version": "1.0.954",
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",