toga-ai 1.0.690 → 1.0.692

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.
@@ -5,7 +5,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
5
5
  ## 1.0 framework
6
6
 
7
7
  - **library** (Library) _(framework core)_ — 20 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
- - **worker** (Worker) — 30 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
8
+ - **worker** (Worker) — 31 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
10
10
  - **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
11
11
  - **togadesk** (TOGa Desk) — 13 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
@@ -14,6 +14,7 @@
14
14
  | [Compass MR/MA Order Auto-Approval & Status Gate](features/mr-ma-order-approval-and-status.md) | 2.0 | Compass **MR** and **MA** sales orders are system-generated from the MITS / Office Depot EDI pipeline (they do not originate as user-entered SA orders) and must | _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/PurchaseOrder.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
15
15
  | [Compass ODP EDI 850 Line-Item Resolution (VA part number to IN SKU fallback)](features/odp-edi-850-item-resolution.md) | 1.0 | How each **PO1 line** on an inbound Office Depot (ODP) **EDI 850** is resolved to a real `Client_Compass` catalog item before cron `3a_import_office_depot_purch | worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, library/app/Edi.php |
16
16
  | [Compass Office Depot EDI 855 Acknowledgement + Over-Quantity PO Guard](features/odp-edi-855-acknowledgement-and-overquantity-guard.md) | 1.0 | How Compass acknowledges Office Depot (ODP) inbound **EDI 850** purchase orders with an **X12 855**, and the **over-quantity guard** that rejects a duplicate PO | worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, worker/crons/toga2/compass/workflow/4_transmit_office_depot_po_acknowledgements.php, library/app/edi.php |
17
+ | [Compass ODP EDI File Retention & Import Retry (cron 3a)](features/odp-edi-file-retention-and-retry.md) | 1.0 | When the Compass Office Depot **850 importer** (cron `3a`, every 5 min) keeps an EDI file in S3 and when it deletes it. | worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php |
17
18
  | [Compass USA — Fulfilled vs Partially Fulfilled (order total kept; per-line rule built then rejected)](features/order-fulfillment-status-per-line.md) | 2.0 | TOGa Supply shows **Fulfilled** on Compass USA orders that are only partly shipped. | _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Canada/SalesOrder.php |
18
19
  | [Compass PEOPLE-File User Lifecycle (duplicate accounts, reactivation grace window, raw-SQL deactivation)](features/people-file-user-lifecycle.md) | 2.0 | The nightly **PEOPLE** file cron (`_Worker_Client_Compass_PeopleFile`, `worker2/Worker/Client/Compass/PeopleFile.php`) owns the whole `Users` row lifecycle for | worker2/Worker/Client/Compass/PeopleFile.php |
19
20
  | [Persona Model & Levy-Sector Gating (worker2 PEOPLE cron)](features/persona-model-and-levy-gating.md) | 2.0 | Compass USA catalogue visibility is driven by **personas** in `Client_Compass`. | worker2/Worker/Client/Compass/PeopleFile.php |
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-08-25
9
+ updated: 2026-08-28
10
10
  owners: ["jcardinal", "bala"]
11
11
  files:
12
12
  - worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php
@@ -14,6 +14,7 @@ files:
14
14
  - library/app/edi.php
15
15
  related:
16
16
  - odp-edi-850-item-resolution.md
17
+ - odp-edi-file-retention-and-retry.md
17
18
  - ../workflows/odp-order-pipeline-to-netsuite.md
18
19
  - ../workflows/odp-duplicate-po-line-cleanup.md
19
20
  - mits-po-transmission-to-vendors.md
@@ -139,6 +140,29 @@ Throwing **after** `cronFinished()` is safe: the overlap guard
139
140
  cronFinished's `CronJobExecutions` checkout row, and on CLI an uncaught `App_Exception_Business`
140
141
  just echoes `ERROR [ref]:` and exits.
141
142
 
143
+ ### ⚠ The 855 storm got a second route in — and a second guard (2026-08-28)
144
+
145
+ Deferring the throw was enough only while **every** file was deleted after one pass. Cron 3a now
146
+ **keeps a failed EDI file in S3 and retries it every 5 minutes**
147
+ ([file retention & retry](odp-edi-file-retention-and-retry.md)), so the reject branch is reachable
148
+ again on a later attempt — which would re-send the same reject 855 on every run, the exact storm the
149
+ deferred throw was written to prevent.
150
+
151
+ Two things close it:
152
+
153
+ - **`wasPurchaseOrderRejectionAlreadyAcknowledged(string $purchaseOrderNumber): bool`** — the reject
154
+ 855 is now built and sent **only the first time** the guard trips for a PO. A prior send is
155
+ detected by looking for a `Logs.FileLog` row with `job = 'ODP_EDI'` and
156
+ `fileName = '<purchaseOrderNumber>-ACK-REJECT.x12'`. **The acknowledgement log row is the idempotency
157
+ key** — there is no `dtAcknowledged` to lean on here, because a rejected PO is never persisted.
158
+ - **An over-quantity rejection is deliberately NOT treated as an import failure**, so the file is
159
+ still **deleted** on a reject. The reasoning: a reject 855 has already gone back to Office Depot,
160
+ so the file is finished with and retrying it would achieve nothing. (The
161
+ `SKIP_IF_CONTAINS_PART_NUMBERS` skip is treated the same way — an intentional business skip.)
162
+
163
+ So in practice the reject path still ends in a delete, and the helper is the belt-and-braces guard
164
+ for the case where a **different** PO in the same multi-PO file fails and keeps the file alive.
165
+
142
166
  ## ⚠ Open item — reject codes vs ODP's 855 companion guide
143
167
  The reject codes used (`BAK02 = RD`, `ACK01 = IR`) are the **standards-correct** X12 values.
144
168
  They have **not** yet been confirmed against Office Depot's 855 companion guide. If ODP
@@ -147,6 +171,13 @@ A legacy, abandoned sketch (`compass/edi/3_send_edi_855(reject).php`) used **non
147
171
  codes `BAK01 = A1` / `BAK02 = RE` — that path was **deliberately NOT followed**.
148
172
 
149
173
  ## Change history
174
+ - 2026-08-28 — Cron 3a now retries a failed EDI file instead of deleting it, which reopened the
175
+ **855-storm** risk on the reject branch. Added
176
+ `wasPurchaseOrderRejectionAlreadyAcknowledged()` — keyed on a `Logs.FileLog`
177
+ `<PO>-ACK-REJECT.x12` row, since a rejected PO is never persisted and has no `dtAcknowledged` — so
178
+ a reject 855 is sent only on the first trip. Recorded that an over-quantity rejection (and a
179
+ `SKIP_IF_CONTAINS_PART_NUMBERS` skip) is **deliberately not a failure**, so those files are still
180
+ deleted. Not deployed as of 2026-08-28. (bala)
150
181
  - 2026-08-25 — The over-quantity guard is now fed **resolved** part numbers (it ignores parts it
151
182
  cannot match, so a mistyped ODP part number had been bypassing duplicate protection), the
152
183
  **reject 855 echoes `ediPartNumber`** (what ODP sent) instead of our corrected value, and the
@@ -0,0 +1,137 @@
1
+ ---
2
+ title: Compass ODP EDI File Retention & Import Retry (cron 3a)
3
+ framework: "1.0"
4
+ repo: worker
5
+ project: Worker
6
+ client: compass-usa
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-08-28
10
+ owners: ["bala"]
11
+ files:
12
+ - worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php
13
+ related:
14
+ - odp-edi-855-acknowledgement-and-overquantity-guard.md
15
+ - odp-edi-850-item-resolution.md
16
+ - ../workflows/odp-edi-import-recovery.md
17
+ - ../workflows/odp-order-pipeline-to-netsuite.md
18
+ - ../profile.md
19
+ ---
20
+
21
+ ## Summary
22
+ When the Compass Office Depot **850 importer** (cron `3a`, every 5 min) keeps an EDI file in S3 and
23
+ when it deletes it. Until 2026-08-28 the file was deleted **unconditionally** at the end of the 850
24
+ branch, so **any** import failure silently destroyed the order: nothing written, no acknowledgement,
25
+ cron 5 never built a NetSuite SO, and no file left to retry. 3a now deletes the object **only when
26
+ every purchase order in the file imported end to end**; anything less and the file stays in
27
+ `OfficeDepot/` and the next run (5 min later) tries again.
28
+
29
+ > **⚠ Status 2026-08-28 — written in the working tree, NOT committed and NOT deployed.** Production
30
+ > still has the unconditional-delete behavior. Treat the silent-data-loss section below as live
31
+ > production behavior until this ships.
32
+
33
+ ## The failure this was built for (production, 2026-08-27)
34
+
35
+ Office Depot POs `41832546-1214` and `41832551-5910` never reached NetSuite. Between **19:11 and
36
+ 19:16** api2 returned **HTTP 500** (envelope code `EO-1`, "Operation Failed: An internal server
37
+ error occurred") to **every** lookup 3a makes: `GET /items`, `GET /vendor-items`,
38
+ `GET /purchase-orders`. Nothing was written, no 855 was produced, and 3a then deleted the EDI file.
39
+ The orders were only recoverable because the raw x12 was archived in `Logs.FileLog`.
40
+
41
+ This is a **silent data-loss class of bug**, not a one-off: any api2 outage inside the 5-minute
42
+ window destroyed whatever ODP had sent during it.
43
+
44
+ ### ⚠ Where the failure trail actually is (this cost real time)
45
+
46
+ - **NOT in `Logs_Compass.Api`.** The failing api2 calls do **not** appear in the per-client Api log,
47
+ only the surrounding successful `POST /v2/auth/api` and `GET /v2/clients` calls do. Do not conclude
48
+ from an empty `Logs_Compass.Api` that 3a never ran or never called api2.
49
+ - **The reliable trail is the legacy `Logs` schema:** `Logs.Emails` with subject
50
+ `Office Depot PO Import error: <PO>`, plus `Logs.FileLog` with `job = 'ODP_EDI'`.
51
+ - **Diagnostic shape that identifies this exact failure:** a `Logs.FileLog` row for `<PO>.x12` with
52
+ **no** matching `<PO>-ACK.x12` row, **and** no `PurchaseOrders` row in `Client_Compass` for that PO
53
+ number.
54
+ - **The only surviving copy of the EDI is `Logs.FileLog.fileData`.** Recovery is: pull `fileData`,
55
+ write it back byte for byte, re-upload to the **root** of `OfficeDepot/` (never `OUTBOX/` or
56
+ `SENT/`, which 3a skips). Full runbook:
57
+ [Recovering a lost ODP EDI import](../workflows/odp-edi-import-recovery.md).
58
+
59
+ ## How it works
60
+
61
+ ### One flag, one choke point
62
+ Every failure path in 3a already funnelled through **`sendErrorNotification()`**, so that single
63
+ function is the choke point. It now clears the file-scoped flag
64
+ **`$isCurrentEdiFileFullyImported`**, and that flag gates the S3 delete: the object is removed only
65
+ when the flag survives the file, otherwise the PO numbers are pushed onto
66
+ `$retriedPurchaseOrderNumbers` and the file is left in place.
67
+
68
+ **Why it matters:** there are roughly **25 separate `continue` sites** in the 850 branch. Gating on
69
+ the notification function means no error branch can be missed, and a future error path inherits the
70
+ retry for free as long as it reports through `sendErrorNotification()`. The flag is reset to `true`
71
+ at the top of each file's 850 branch, so every file is judged on its own attempt.
72
+
73
+ ### What is deliberately NOT a failure (the file still deletes)
74
+ - **The over-quantity guard rejection** — a reject EDI 855 has already been sent to Office Depot, so
75
+ the file is finished with. See
76
+ [855 Acknowledgement + Over-Quantity Guard](odp-edi-855-acknowledgement-and-overquantity-guard.md).
77
+ - **The `SKIP_IF_CONTAINS_PART_NUMBERS` skip** — an intentional business skip, not an error.
78
+
79
+ ### Supporting guards (retrying breaks things that used to run exactly once)
80
+
81
+ | Helper / constant | Why it exists |
82
+ |---|---|
83
+ | `wasPurchaseOrderRejectionAlreadyAcknowledged()` | A retried file must not re-send the over-quantity **reject 855** on every run. Detects a prior send by looking for a `Logs.FileLog` row named `<PO>-ACK-REJECT.x12`. This is the **855 storm** the original deferred-throw comment warned about, now reachable a second way. |
84
+ | `wasImportErrorAlreadyEmailedRecently()` + `HOURS_BETWEEN_IMPORT_ERROR_EMAILS = 1` | A permanently failing file (e.g. a part missing from the Compass catalog) would email the team every 5 minutes: **288 times a day**. Checks `Logs.Emails` by subject inside the window and stays quiet. |
85
+ | `isEdiFileAlreadyInFileLog()` + `DAYS_TO_SEARCH_FOR_LOGGED_EDI_FILE = 7` | Keeps exactly **one** `Logs.FileLog` row per file no matter how many attempts it takes. Matches on `job`, `fileName`, `fileData` and a bounded `dtStamp`. |
86
+
87
+ The file-log name is the run's imported PO numbers joined by `;` plus `.x12`, so a multi-PO 850
88
+ archives under a semicolon-joined name and searching for a single PO number needs a `LIKE`.
89
+
90
+ ### The new business exception
91
+ **`COMPASS_OFFICE_DEPOT_PO_IMPORT_INCOMPLETE`** (`URGENCY_HIGH`) is thrown from
92
+ `$retriedPurchaseOrderNumbers` **after** `App_Framework::cronFinished()` — the same deferred pattern
93
+ the rejection exception uses, and for the same reason (an inline throw unwinds the loop before the
94
+ per-file bookkeeping).
95
+
96
+ **⚠ Only one throw can win.** It is raised **after** the existing
97
+ `COMPASS_OFFICE_DEPOT_PO_IMPORT_REJECTED_OVER_QUANTITY` throw, deliberately: a rejection is the more
98
+ serious event, so when both happen in one run the rejection is the one that surfaces as a Logs.Issue.
99
+
100
+ ## Why retrying is safe (verified against the code before shipping)
101
+ - The existing **"do we already have this PO"** check (`GET /purchase-orders` by number **and**
102
+ `vendorId` = Office Depot) makes a re-import **idempotent**.
103
+ - It also keeps the **over-quantity guard from firing on a PO a previous attempt already created**,
104
+ because the guard sits *inside* the `totalRecordCount == 0` branch — an already-imported PO never
105
+ reaches it.
106
+ - Sales-order items are matched by **part number** against the existing SO, so a partial import
107
+ backfills rather than duplicates.
108
+
109
+ This is the same idempotency map recorded in the
110
+ [ODP pipeline doc](../workflows/odp-order-pipeline-to-netsuite.md); the retry simply automates the
111
+ re-drop that used to be a manual recovery.
112
+
113
+ ## Gotchas / known issues
114
+ - **⚠ A file that can never import now retries forever.** No cap and no park-after-N behavior was
115
+ added. Unimportable files accumulate in `OfficeDepot/`, lengthening every run against the cron's
116
+ **900-second ceiling** — and that folder is already the subject of the unfixed 138k-object listing
117
+ problem. Flagged to the developer; **not yet decided**.
118
+ - **⚠ Do not deploy with `EMAIL_TO` narrowed.** As observed in the working tree on 2026-08-28,
119
+ `EMAIL_TO` in this cron had been reduced locally to a **single** address instead of the three team
120
+ addresses. That is a local debugging edit, not a change made by this work — restore the full list
121
+ before shipping.
122
+ - Recovery timing: re-importing an old EDI file restarts cron 5's **30-minute** release clock,
123
+ because that clock runs from the Office Depot PO's `dtCreated`. A recovered order waits another
124
+ half hour. See the cron 5 section of the
125
+ [ODP pipeline doc](../workflows/odp-order-pipeline-to-netsuite.md).
126
+
127
+ ## Change history
128
+ - 2026-08-28 — Built: 3a now deletes an EDI file **only when every PO in it imported end to end**,
129
+ gated by `$isCurrentEdiFileFullyImported` cleared inside `sendErrorNotification()` (the one choke
130
+ point all ~25 error paths already used). Over-quantity rejects and `SKIP_IF_CONTAINS_PART_NUMBERS`
131
+ skips still delete. Added `wasPurchaseOrderRejectionAlreadyAcknowledged()` (no repeat reject 855),
132
+ `wasImportErrorAlreadyEmailedRecently()` + `HOURS_BETWEEN_IMPORT_ERROR_EMAILS = 1` (no 288
133
+ emails/day), `isEdiFileAlreadyInFileLog()` + `DAYS_TO_SEARCH_FOR_LOGGED_EDI_FILE = 7` (one file-log
134
+ row per file), and the deferred `COMPASS_OFFICE_DEPOT_PO_IMPORT_INCOMPLETE` exception. Recorded the
135
+ 2026-08-27 api2 500 (`EO-1`) incident that destroyed POs 41832546-1214 / 41832551-5910, and that
136
+ those api2 failures are **absent from `Logs_Compass.Api`** — the trail is `Logs.Emails` +
137
+ `Logs.FileLog`. **Not committed, not deployed.** (bala)
@@ -41,6 +41,7 @@ related:
41
41
  - workflows/odp-duplicate-po-line-cleanup.md
42
42
  - features/odp-edi-850-item-resolution.md
43
43
  - features/odp-edi-855-acknowledgement-and-overquantity-guard.md
44
+ - features/odp-edi-file-retention-and-retry.md
44
45
  - ../../2.0/apps/worker2/features/compass-vip-support-importer.md
45
46
  - ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
46
47
  - ../../2.0/apps/toga2-commerce/features/category-tile-sort-order.md
@@ -113,6 +114,18 @@ separate, related client (see its own profile).
113
114
  [ODP EDI 850 Line-Item Resolution](features/odp-edi-850-item-resolution.md).
114
115
  **Never create an `Items` row so a mangled inbound part number "matches"** — that is what put
115
116
  duplicate junk items in the Agilant catalog (`ODTABKITTINGOPT3` vs `ODPTABKITTINGOPT3`).
117
+ - **⚠ A failed 850 import silently destroys the order (2026-08-28, fix NOT deployed).** Cron 3a
118
+ deleted the EDI file from S3 **unconditionally**, so an api2 outage (HTTP 500 / `EO-1`, 2026-08-27)
119
+ meant ODP POs `41832546-1214` / `41832551-5910` were never imported, never acknowledged, never
120
+ reached NetSuite — and the only surviving copy was `Logs.FileLog`. The fix keeps the file and
121
+ retries it every 5 minutes until every PO in it imports. **These api2 failures do not appear in
122
+ `Logs_Compass.Api`** — use `Logs.Emails` + `Logs.FileLog`:
123
+ [ODP EDI File Retention & Import Retry](features/odp-edi-file-retention-and-retry.md).
124
+ - **Office Depot only 850s back what they buy FROM us (2026-08-28).** A Compass SO can mix a PO ODP
125
+ buys from Toga (returns an 850) with one Toga buys from ODP (never returns one) — both carry
126
+ `vendorId` = Office Depot. Mixed orders can therefore only ever be released to NetSuite by cron 5's
127
+ **30-minute timeout**, so they are consistently half an hour late by design, not by fault. See the
128
+ release gate in [ODP order pipeline to NetSuite](workflows/odp-order-pipeline-to-netsuite.md).
116
129
  - ASN ingestion entry points: cXML to the V2 API (logged in `Logs_Compass.Api`) and
117
130
  `worker/crons/toga2/compass/workflow/3b_import_strategic_systems_advance_shipping_notices.php`.
118
131
 
@@ -6,13 +6,14 @@ project: Worker
6
6
  client: compass-usa
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-08-04
10
- owners: ["dfranks"]
9
+ updated: 2026-08-28
10
+ owners: ["dfranks", "bala"]
11
11
  files:
12
12
  - worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php
13
13
  - worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php
14
14
  - worker/schedules/cron.worker.sync.json
15
15
  related:
16
+ - clients/compass-usa/features/odp-edi-file-retention-and-retry.md
16
17
  - clients/compass-usa/workflows/odp-order-pipeline-to-netsuite.md
17
18
  - clients/compass-usa/workflows/order-lifecycle-and-data-integrity.md
18
19
  - ../../../2.0/apps/api2/features/api-payload-interceptors.md
@@ -30,12 +31,43 @@ The recovery works because the raw x12 is archived in `Logs.FileLog` and **3a is
30
31
  re-uploading the file makes it repair the items, the ODP PO, and both bridges. Verified end-to-end
31
32
  in **production** on 2026-08-04 for ODP PO `41608261-1135` / ODP SO `475390815001`.
32
33
 
34
+ The same runbook covers the **total** loss case, which is more common than the partial one: 3a fails
35
+ **before writing anything** (e.g. api2 is down), deletes the file anyway, and the order simply never
36
+ exists — no zero-item order, no ODP PO, nothing to sweep for. Production example 2026-08-27, ODP POs
37
+ `41832546-1214` and `41832551-5910`, when api2 answered **HTTP 500 / `EO-1`** to every lookup between
38
+ 19:11 and 19:16.
39
+
40
+ > **Once the conditional-delete change ships, most of this becomes unnecessary** — 3a keeps a failed
41
+ > file in `OfficeDepot/` and retries it every 5 minutes on its own. See
42
+ > [ODP EDI File Retention & Import Retry](../features/odp-edi-file-retention-and-retry.md). It was
43
+ > **not deployed as of 2026-08-28**, so this runbook is still live, and it stays the recovery path
44
+ > for anything lost before the deploy.
45
+
33
46
  ## When to use this
34
47
 
35
48
  Symptoms: a zero-item ODP SalesOrder (see the orphan sweeps in
36
49
  [order lifecycle & data integrity](order-lifecycle-and-data-integrity.md)), and cron 5 re-attempting
37
50
  the NetSuite SO every 10 minutes because it selects on `c_dtTransmittedToNetsuite IS NULL`.
38
51
 
52
+ ### The diagnostic shape that identifies a destroyed import
53
+
54
+ For the total-loss case there is no order to notice, so match on the **file log** instead:
55
+
56
+ - a `Logs.FileLog` row (`job = 'ODP_EDI'`) for **`<odpPoNumber>.x12`**, **and**
57
+ - **no** matching **`<odpPoNumber>-ACK.x12`** row (nothing was ever acknowledged), **and**
58
+ - **no** `PurchaseOrders` row in `Client_Compass` for that PO number.
59
+
60
+ That combination means the 850 arrived, was archived, and then died — the file is already gone from
61
+ S3 and the file-log copy is the only one left.
62
+
63
+ ### ⚠ `Logs_Compass.Api` is NOT where these failures show up
64
+
65
+ The failing api2 calls 3a makes (`GET /items`, `GET /vendor-items`, `GET /purchase-orders`) were
66
+ **absent** from the per-client `Logs_Compass.Api` log in the 2026-08-27 incident — only the
67
+ surrounding successful `POST /v2/auth/api` and `GET /v2/clients` calls appear. **Do not** read an
68
+ empty `Logs_Compass.Api` as "3a never ran." The reliable trail is **`Logs.Emails`** (subject
69
+ `Office Depot PO Import error: <PO>`) plus **`Logs.FileLog`** (`job = 'ODP_EDI'`).
70
+
39
71
  ## The runbook
40
72
 
41
73
  ### Step 1 — retrieve the archived x12
@@ -55,13 +87,20 @@ you have a corrupted payload — do not upload it.
55
87
 
56
88
  ### Step 2 — know that S3 has nothing left
57
89
 
58
- 3a **deletes the S3 object after processing** (`:769`), so a failed import leaves no file to retry.
59
- Recovery is a **re-upload**, to:
90
+ 3a **deletes the S3 object after processing**, so a failed import leaves no file to retry. (After
91
+ the conditional-delete change ships this is only true of a *successful* import — see the note in the
92
+ Summary.) Recovery is a **re-upload**, to the **root** of the prefix:
60
93
 
61
94
  ```
62
95
  s3://agilant-as2/OfficeDepot/EDI_<odpPoNumber>.txt
63
96
  ```
64
97
 
98
+ **⚠ The root of `OfficeDepot/`, never `OfficeDepot/OUTBOX/` or `OfficeDepot/SENT/`.** 3a skips both
99
+ of those prefixes when processing, so a file dropped there is archived forever and never imported.
100
+
101
+ Write the bytes back **exactly** as stored: pull `fileData`, write it to disk unmodified, and confirm
102
+ the byte count against `LENGTH(fileData)` before uploading.
103
+
65
104
  ### Step 3 — clear the cause BEFORE re-uploading
66
105
 
67
106
  **Confirm no `ApiPayloadInterceptors` row exists for the `sales-order-items` record** (`recordId 15`)
@@ -90,7 +129,11 @@ confirmation the recovery finished.
90
129
  ## Gotchas / known issues
91
130
 
92
131
  - **⚠ Re-uploading before fixing the root cause destroys the file again** (3a deletes on the way
93
- through). Step 3 is not optional.
132
+ through). Step 3 is not optional. This is exactly what the conditional-delete change removes —
133
+ until it deploys, assume one attempt per recovered file.
134
+ - **⚠ A recovered order waits another 30 minutes.** Cron 5's release timer runs from the Office
135
+ Depot PO's `dtCreated`, i.e. **when we imported the 850** — so re-importing restarts it. See the
136
+ release gate in the [ODP pipeline doc](odp-order-pipeline-to-netsuite.md).
94
137
  - **⚠ The archive filename extension differs from the S3 one** — `.x12` in `Logs.FileLog`,
95
138
  `EDI_<po>.txt` in S3. Searching for the wrong one finds nothing.
96
139
  - **Don't search for the ODP SalesOrder by the `BEG` number.** `BEG` is the ODP *PO*; the SO number
@@ -103,6 +146,14 @@ confirmation the recovery finished.
103
146
 
104
147
  ## Change history
105
148
 
149
+ - 2026-08-28 — Added the **total-loss** case (3a fails before writing anything, so there is no
150
+ zero-item order to sweep for) with the 2026-08-27 api2 **500 / `EO-1`** incident that destroyed ODP
151
+ POs `41832546-1214` / `41832551-5910`; the **file-log diagnostic shape** (`<PO>.x12` present, no
152
+ `<PO>-ACK.x12`, no `PurchaseOrders` row); the warning that these api2 failures are **absent from
153
+ `Logs_Compass.Api`** so `Logs.Emails` + `Logs.FileLog` are the real trail; the **re-upload to the
154
+ root of `OfficeDepot/`, never `OUTBOX/` or `SENT/`** rule; and the note that cron 5's 30-minute
155
+ timer restarts on re-import. Flagged that 3a's **conditional delete + auto-retry** (written
156
+ 2026-08-28, not deployed) largely supersedes this runbook once it ships. (bala)
106
157
  - 2026-08-04 — Documented the runbook after recovering a production ODP order whose item write was
107
158
  killed by an api2 fatal: `Logs.FileLog` (`job = 'ODP_EDI'`, `<po>.x12`, bounded by `fileTimestamp`)
108
159
  as the x12 archive, the trailing-newline/byte-count check, the re-upload path to
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: compass-usa
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-08-25
9
+ updated: 2026-08-28
10
10
  owners: ["rgirish", "bala", "dfranks", "jcardinal", "mhammontree"]
11
11
  files:
12
12
  - worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php
@@ -19,6 +19,7 @@ files:
19
19
  - library/app/client/compass.php
20
20
  related:
21
21
  - 1.0/apps/worker/features/netsuite-sales-order-sales-rep-sourcing.md
22
+ - clients/compass-usa/features/odp-edi-file-retention-and-retry.md
22
23
  - clients/compass-usa/features/odp-edi-850-item-resolution.md
23
24
  - clients/compass-usa/features/odp-edi-855-acknowledgement-and-overquantity-guard.md
24
25
  - 1.0/apps/worker/workflows/tracing-a-worker-cron-run-in-production.md
@@ -67,8 +68,18 @@ ODP SalesOrder (customerId = 1)
67
68
  3. **`workflow/3a_import_office_depot_purchase_orders.php`** — pulls the ODP **850** from S3 bucket
68
69
  `agilant-as2`, prefix `OfficeDepot/` (excluding `OUTBOX/` and `SENT/`; `:46` S3 read,
69
70
  `:54-58` prefix filters) and creates the downstream **ODP SalesOrder** (customerId=1 = Office
70
- Depot). **Deletes the S3 object after processing** (`:769`) — so an absent S3 object is expected
71
- once ingested, not evidence of a miss, *and* a failed import leaves nothing to retry.
71
+ Depot). **Deletes the S3 object after processing** — so an absent S3 object is expected once
72
+ ingested, not evidence of a miss.
73
+
74
+ > **⚠ Corrected 2026-08-28 — the delete is now CONDITIONAL.** 3a used to delete the object
75
+ > **unconditionally**, so a failed import left nothing to retry and **silently destroyed the
76
+ > order**. It now deletes only when **every PO in the file imported end to end**; otherwise the
77
+ > file stays in `OfficeDepot/` and the next `*/5` run retries it. Which failures count, the
78
+ > reject-855 / error-email / file-log guards that retrying made necessary, and the
79
+ > **retries-forever** limitation:
80
+ > [ODP EDI File Retention & Import Retry](../features/odp-edi-file-retention-and-retry.md).
81
+ > **Not deployed as of 2026-08-28** — production still deletes unconditionally, so treat any
82
+ > pre-deploy incident as a lost file.
72
83
 
73
84
  > **⚠ Correction (2026-08-04): `edi/1_download_edi_s3_create_po_toga.php` is DEAD CODE.** It
74
85
  > appears in **no** schedule file. The scheduled entry *named* "Download EDI From S3 Files &
@@ -148,13 +159,72 @@ Cron 3a resolves each line through that SKU when the `VA` text does not match th
148
159
  **onto the ODP SalesOrder (customerId=1), never the Compass SO (customerId=2)**.
149
160
  **Exclusion filters** (an order legitimately waiting is not a bug):
150
161
  - `CompassSalesOrders.number NOT LIKE 'MA%'` — MA orders are excluded.
151
- - **computer-kit orders** (Bundles 187–196) wait for a **2nd PO** before syncing.
152
- - a **30-minute delay** after the first ODP PO is created.
162
+ - the two-part **release gate** below (this is the "30-minute delay").
163
+ - **computer-kit orders** (Bundles 187–196) wait for a **2nd PO** before syncing. This one is
164
+ **not** in the `WHERE` clause — it is a per-row check run in PHP *after* the query
165
+ (`Bundles.number IN (192, 187, 188, 195, 193, 189, 194, 190, 191, 196)`, then a count of that
166
+ Compass SO's POs), so such an order is selected and then skipped.
153
167
 
154
168
  Because the selection is only `c_dtTransmittedToNetsuite IS NULL` (plus `number NOT LIKE 'MA%'`),
155
169
  a **zero-item ODP SalesOrder sits in this queue retrying every 10 minutes** until its lines are
156
170
  restored — the retry loop is the symptom, not the cause.
157
171
 
172
+ ### How cron 5 decides a Compass order is ready for NetSuite (the release gate)
173
+
174
+ The query groups by **`CompassSalesOrders.id`** and releases the order when **either** condition
175
+ holds (verified against the cron source 2026-08-28, the `WHERE` block around `:90-115`):
176
+
177
+ - **(a) Everything ODP owes us has come back.** The count of Compass POs on that Compass SO with
178
+ `vendorId` = Office Depot that have **nothing chained back to them** is **zero** — the test in the
179
+ SQL is `OfficeDepotSO_PO.id IS NULL`.
180
+ - **(b) OR the 30-minute timeout.** The **earliest** Office Depot PO chained to that Compass SO was
181
+ created at least **30 minutes** ago (`IFNULL(MIN(OfficeDepotPO.dtCreated), NOW()) <=
182
+ DATE_SUB(NOW(), INTERVAL 30 MINUTE)`), in which case it transmits with whatever has come back.
183
+
184
+ **Cron 5 never opens an EDI file and never looks in S3.** It infers that the 850 arrived purely from
185
+ the **link rows cron 3a writes**: Compass PO → `PurchaseOrders_SalesOrders` → Office Depot SO →
186
+ `SalesOrders_PurchaseOrders` → Office Depot PO. If 3a did not write the bridges, cron 5 cannot tell
187
+ the difference between "ODP has not answered yet" and "the import died."
188
+
189
+ **⚠ The 30-minute clock runs from the Office Depot PO's `dtCreated`** — i.e. **the moment we
190
+ imported the 850**, not when the Compass order was raised. So re-importing an old EDI file
191
+ **restarts the timer**, and a recovered order waits another 30 minutes before it can transmit.
192
+
193
+ **⚠ A late 850 produces a SECOND NetSuite sales order.** If a PO's 850 arrives *after* the Compass
194
+ SO has already transmitted, the new ODP SO is picked up on its own and creates a **separate**
195
+ NetSuite SO rather than joining the first.
196
+
197
+ Fields on `Client_Compass.SalesOrders`: `c_dtTransmittedToNetsuite`, `c_netsuiteInternalSalesOrderId`.
198
+
199
+ **⚠ Do not infer the schedule from transmission times.** Transmissions cluster at **:00 and :30**,
200
+ which reads like a 30-minute cron — it is not. The schedule is **`*/10`** (re-verified in
201
+ `worker/schedules/cron.worker.sync.json` on 2026-08-28); the clustering is the 30-minute **release
202
+ gate** above, not the cadence.
203
+
204
+ ### ⚠ Office Depot only sends an 850 back for what they buy FROM us
205
+
206
+ Confirmed by the developer with Office Depot (2026-08-28). A single Compass sales order can carry
207
+ **two different kinds** of Compass purchase order, and **both** have `vendorId` = Office Depot:
208
+
209
+ | Direction | Example | Does an 850 come back? |
210
+ |---|---|---|
211
+ | **ODP buys FROM Toga** (we supply the item) | the laptop | **Yes** — 3a turns it into an ODP SalesOrder + PurchaseOrder |
212
+ | **Toga buys FROM ODP** (they supply the item) | accessories | **No** — nothing ever comes back |
213
+
214
+ Worked example (2026-08-28): Compass SO **`SA136541`** carried `50311948-1` (laptop `C40QYUC`,
215
+ returned as ODP PO `41832546-1214`) **and** `50311949-1` (accessories — HDMI adapter, cable, dock,
216
+ mouse — which never returns).
217
+
218
+ **Consequence:** on any Compass SO that **mixes** the two kinds, release condition **(a) can never be
219
+ satisfied**, because the buy-from-ODP PO permanently has nothing chained to it. Those orders are
220
+ **always** released by the 30-minute timeout instead, so they reach NetSuite **half an hour late,
221
+ every single time**. Nothing is lost — but a "why is this order always 30 minutes behind?" ticket
222
+ is this, not a fault.
223
+
224
+ **Open improvement (not done):** narrow condition (a) to count only the POs Office Depot actually
225
+ buys from us. **The distinguishing field in the data has not been identified yet** — `vendorId` is
226
+ the same for both, so this needs a data investigation before any code change.
227
+
158
228
  ### Cron 5 sales rep — sourced from the end customer, not hardcoded (TRUE-80451)
159
229
 
160
230
  The SO `entity` is the **parent** customer **35581 "ODP Veyer (B2B)"**; the real buying account
@@ -272,6 +342,18 @@ wrong record makes every order look "stuck." Use the **join**, never a number ma
272
342
  transmission feature.)
273
343
 
274
344
  ## Change history
345
+ - 2026-08-28 — **Cron 3a's S3 delete is no longer unconditional** (it destroyed the order on any
346
+ failure; corrected the step-3 note) — details in the new
347
+ [ODP EDI File Retention & Import Retry](../features/odp-edi-file-retention-and-retry.md), which
348
+ also records the 2026-08-27 api2 500 incident and that those failures are **absent from
349
+ `Logs_Compass.Api`**. Documented **cron 5's actual release gate** (`OfficeDepotSO_PO.id IS NULL`
350
+ count = 0 **OR** earliest ODP PO `dtCreated` older than 30 min), that cron 5 reads only the
351
+ bridge rows and never S3, that the 30-min clock starts at **our import** (so a recovery waits
352
+ again), that a late 850 makes a **second** NetSuite SO, and that the :00/:30 clustering is the
353
+ gate and **not** the schedule (still `*/10`). Recorded the ODP business rule that **an 850 only
354
+ comes back for what ODP buys FROM us**, so mixed orders (e.g. `SA136541`) can only ever release on
355
+ the timeout. Corrected the Bundles 187–196 filter as a post-query PHP check, not a `WHERE`
356
+ clause. (bala)
275
357
  - 2026-08-25 — Cron 3a now **resolves every 850 line to a catalog item before it writes anything**
276
358
  (a bad line used to leave a half-built ODP SO); documented the `PO1` two-identifier detail
277
359
  (`elements[7]` = drifting `VA` text, `elements[9]` = ODP's reliable `IN` SKU) behind the new
@@ -0,0 +1,209 @@
1
+ ---
2
+ type: session
3
+ slug: talos-streaming-supply
4
+ title: Talos chat + streaming into toga25-supply via LangChain useStream
5
+ author: apeterson
6
+ repos: [toga25-supply, toga-blox, talos]
7
+ framework: "2.0"
8
+ client: shared
9
+ status: active
10
+ created: 2026-08-28
11
+ updated: 2026-08-28
12
+ ---
13
+
14
+ # Session: talos-streaming-supply
15
+ **Date:** 2026-08-28
16
+ **Project/Repo:** toga25-supply + toga-blox + talos (2.0)
17
+ **Task:** Evaluate whether LangChain's React client (`@langchain/react`) is the right way to add real chat and token-by-token streaming to the Talos assistant panel in `toga25-supply`, following the structure already built on blox `TRUE-80692` (the shared UI component) and supply `TRUE-80693` (the host wiring).
18
+
19
+ > **Research/design session — NO application code was written or committed.** The only writes
20
+ > were knowledge-base docs (published, commit `12b41d3`) and one local Claude memory.
21
+
22
+ ---
23
+
24
+ ## What WORKED
25
+
26
+ - **Read both ticket branches without checking them out.** Both working trees were on
27
+ `TRUE-81380`, so everything was read via `git show <branch>:<path>` and
28
+ `git diff TRUE-81380...<branch>`. Evidence: blox `TRUE-80692` = 2,139 new lines, all under
29
+ `src/components/Talos/` (`TalosLauncher.tsx`, `TalosPanel.tsx` 610 lines, `TalosMessage.tsx`,
30
+ `types.ts`, `stub.ts`, `theme.ts`, `Talos.module.css`); supply `TRUE-80693` = 98 lines across
31
+ 5 files (`AppLayout.tsx`, `Header.tsx`, `Header.module.css`, `index.css`, `talos-owl.png`).
32
+ - **Pinpointed the blocking fact.** The blox host contract is
33
+ `onSend: (text: string) => Promise<TalosResponse>` — a single FINISHED answer. Panel state
34
+ (`TalosConversation` / `TalosMessageModel`) is documented "in-memory only". No token delta, no
35
+ `threadId`, no tool-call surface, no interrupt representation. Therefore streaming is **not** an
36
+ adapter-implementation detail; it requires a contract change on `TRUE-80692`.
37
+ - **Found how Talos itself streams.** `talos/src/providers/Stream.tsx` calls `useStream` from
38
+ **`@langchain/langgraph-sdk/react`** (not `@langchain/react`), wrapped in an app-owned React
39
+ context rather than the library's provider. Config:
40
+ `{ apiUrl, apiKey, assistantId, threadId, fetchStateHistory: true,
41
+ defaultHeaders: { Authorization: Bearer <token> }, onCustomEvent, onThreadId }`.
42
+ `threadId` via `nuqs` (Next-only). Bearer comes from
43
+ `NEXT_PUBLIC_AUTH_API_URL=https://api.togahub.com/v2` — i.e. **api2**, issued for the TogaHub client.
44
+ - **Established that the talos browser calls Aegra DIRECTLY, cross-origin.**
45
+ `talos/src/lib/api-url.ts` `getApiUrl()` returns `env.apiUrl` = `NEXT_PUBLIC_API_URL` = an
46
+ absolute external host (`https://api.beta.togaiq.com`), and `useStream({ apiUrl })` gets that
47
+ same absolute URL. So Aegra already serves browser CORS, and a static SPA can call it directly.
48
+ - **Confirmed `@langchain/react` package facts** (`npm view`): v1.0.33; peers
49
+ `react: ^18 || ^19` (fits blox's React-18 cap) and `@langchain/core: ^1.1.48`; depends on
50
+ `@langchain/langgraph-sdk@1.10.0` — it is a newer wrapper over the SDK talos already uses.
51
+ - **Published the knowledge.** `/capture` wrote 5 files (1 CREATE, 4 UPDATE), commit `12b41d3`
52
+ pushed to `origin/_main`, mirror synced.
53
+
54
+ ## What did NOT work — DO NOT RETRY THESE
55
+
56
+ - **"toga25-supply needs a server-side proxy because it's a static Vite SPA on Amplify."**
57
+ **This was my claim and it is WRONG.** I read `talos/src/app/api/[..._path]/route.ts` (a Next.js
58
+ passthrough that forwards to `LANGGRAPH_API_URL`) and assumed it was on the live path. It is
59
+ **vestigial**: `getApiUrl()` returns the absolute external URL, so nothing routes through it, and
60
+ `.env.beta` does not even define `LANGGRAPH_API_URL` (that route would 500 there). `env.ts`'s own
61
+ comment "proxied via /api" is stale and contradicts its value. **Do not re-derive the proxy
62
+ requirement.** Aegra is reachable browser-direct.
63
+ - **Awaiting the token inside `onSend` (e.g. `queryClient.ensureQueryData`) to solve auth-gating
64
+ for streaming.** Exact reason: `useStream` takes the token as a **value** via `defaultHeaders`,
65
+ not a promise — you cannot await inside the hook. That approach works **only** for the one-shot
66
+ `Promise<TalosResponse>` contract. For streaming the gate must sit **above** the hook (don't
67
+ mount the stream until the token query resolves).
68
+ - **`useSuspenseQuery` / Suspense for the auth gate — rejected.** Exact reason: it suspends the
69
+ subtree, so `TalosPanel` unmounts and remounts — flashing and dropping whatever the user typed
70
+ in the composer. In a shared library it's worse: the thrown promise surfaces in host apps that
71
+ never opted into a boundary.
72
+ - **Putting `@langchain/react` (or the SDK) inside `toga-blox` — rejected.** Exact reasons, from
73
+ `2.0/standards/frontend.md`: §13(b) blox peers do not hoist, so a `@langchain/core` peer would
74
+ force all consumer apps (toga2-view, toga2-supply, toga25-supply, toga2-commerce, toga2-hub) to
75
+ install it even if they never render Talos — and a barrel import of blox's index can surface that
76
+ as a Rollup `failed to resolve import` for a component they never referenced. §21: promote into
77
+ blox only after a 2nd consumer validates the contract. Also blox builds unbundled
78
+ (`tsc` + `fix-esm-imports`), which is fragile against a dep with subpath exports.
79
+ - **Adding a Talos-specific rule to `2.0/standards/frontend.md` — rejected by the capture agent,
80
+ correctly.** Exact reason: §21 and §13(b) already state what the decisions rely on, so adding a
81
+ Talos rule at n=1 would violate the very section it cites. The reasoning lives in the app-level
82
+ feature docs and cites the standard instead.
83
+ - **Trusting `kickoff-preflight`'s feature matcher as proof a doc doesn't exist.** It returned
84
+ `features: []` for both chosen repos on `--q="talos chat streaming"`, so I told the capture agent
85
+ this was all new content. **Two docs already existed** —
86
+ `2.0/apps/toga-blox/features/talos-assistant.md` and
87
+ `2.0/apps/toga25-supply/features/talos-integration.md` — and own most of the session's content.
88
+ Search the features dir directly next time.
89
+
90
+ ## Not tried yet (candidates for next session)
91
+
92
+ - **Ask the Aegra owners (`akhokhani`, `jcardinal`) whether a supply-issued api2 JWT is accepted**
93
+ by `AUTH_TYPE=togahub`, or whether a TogaHub-audience token is required. This is the fork between
94
+ "World A" (trivial — read the existing token, pass it as `defaultHeaders`) and "World B" (build a
95
+ token-exchange + connecting/error layer).
96
+ - **Test CORS from a supply origin** against `https://api.beta.togaiq.com` (preflight on a POST with
97
+ `Authorization` + JSON body; confirm SSE isn't buffered by anything in front).
98
+ - **Re-read the CURRENT `TalosResponse`** on blox `_main` — tcox's `484e2a6` widened it (see Blockers).
99
+ - **Write the LangGraph `Message[]` → blox `TalosMessageModel[]` mapper.** This is where the
100
+ estimate lives; the shapes do not line up.
101
+ - **Add controlled-mode props + `connection` to `TalosPanel`** on `TRUE-80692`.
102
+ - **Check what BDR's blox-Talos consumption constrains** — it is now a second consumer of the panel.
103
+ - **Find the Aegra backend repo's actual name** so it can be registered.
104
+ - Verify whether the `Core.Domains` gate hinted at in the dead proxy is real (it pins
105
+ `Origin: http://talos`, a header a browser cannot set from JS).
106
+
107
+ ## Current file state
108
+
109
+ **No application source files were created, modified, or deleted this session.**
110
+
111
+ | File | Status | Notes |
112
+ |------|--------|-------|
113
+ | `knowledge/2.0/apps/talos/features/chat-frontend.md` | CREATED | Documents the talos front-end, its `useStream` stack, the direct cross-origin Aegra call, env/branch reality. Published. |
114
+ | `knowledge/2.0/apps/talos/architecture.md` | UPDATED | Backend-only scope blockquote (verified no `origin/*` ref contains `libs/aegra-api`), backend-scoped `**Critical rules:**` line, change history. Published. |
115
+ | `knowledge/2.0/apps/toga-blox/features/talos-assistant.md` | UPDATED | New "contract cannot express streaming" section + gotcha; merged against tcox's `484e2a6`. Published. |
116
+ | `knowledge/2.0/apps/toga25-supply/features/talos-integration.md` | UPDATED | New "Real streaming backend — design" + "Aegra-side blockers"; corrected a stale `onSend: stub` line. Published. |
117
+ | `knowledge/registry.json` | UPDATED | Dropped `"language": "python"` from `talos` (it is TypeScript/Next.js). Backend repo intentionally NOT added — name unknown. Published. |
118
+ | `~/.claude/.../memory/repo-path-talos.md` | CREATED | Local memory only (not team): talos is at `~/Agilant/talos` and is the Next.js frontend, not the Python backend. |
119
+ | `toga25-supply/package.json` + `package-lock.json` | pre-existing dirty | Modified before this session (blox pin work). Untouched here. |
120
+ | `toga-blox-npm/package-lock.json` | pre-existing dirty | Modified before this session. Untouched here. |
121
+
122
+ ## Decisions made
123
+
124
+ - **Take the streaming path, accepting the `TRUE-80692` contract change.**
125
+ Rationale: developer explicitly chose token-by-token + tool calls + interrupts + thread
126
+ persistence after being shown there is no path to talos-style streaming that leaves the contract
127
+ untouched. *Rejected:* a non-streaming v1 that keeps `Promise<TalosResponse>` and needs zero blox
128
+ changes.
129
+ - **`TalosPanel` becomes CONTROLLED (host passes `messages` / `isStreaming` / `onSubmit` /
130
+ `onStop`), plus an optional `connection?: "connecting" | "ready" | "error"` defaulting to
131
+ `"ready"`.** Rationale: the default keeps every existing consumer and the stub working, so it is
132
+ non-breaking for a published package — which matters more now that BDR also consumes it.
133
+ *Rejected:* widening the adapter with an `AsyncIterable` `onSendStream` and letting the panel keep
134
+ owning conversation state (would have meant using the SDK's `client.runs.stream()` generator, not
135
+ the React hook, and leaves two sources of truth).
136
+ - **Keep the LangChain client APP-LOCAL in `toga25-supply`; blox stays pure UI.**
137
+ Rationale: `frontend.md` §21 (n=1) and §13(b) (peers don't hoist). *Rejected:* a blox-side
138
+ streaming adapter.
139
+ - **For v1 use `useStream` from `@langchain/langgraph-sdk/react`, NOT `@langchain/react`.**
140
+ Rationale: one fewer peer (`@langchain/react` additionally needs `@langchain/core ^1.1.48`); the
141
+ path is proven in-house so `talos/src/providers/Stream.tsx` can be lifted nearly verbatim; and
142
+ `@langchain/react`'s extra surface is largely media hooks/players blox has no components for.
143
+ Take **only** `useStream` for v1 — add `useToolCalls` only when showing tool activity, and the
144
+ interrupt helpers only for human-in-the-loop, which is where `@langchain/react` earns its keep.
145
+ *Revisit it then.*
146
+ - **Write our own context provider, not the library's `StreamProvider`.** Rationale: supply's
147
+ token / tenant / `threadId` must be injected; talos does the same.
148
+ - **Provider placement: above the router outlet, in `AppLayout`.** Rationale: three jobs — make the
149
+ stream a singleton (`useStream` is stateful; two call sites = two conversations), keep the
150
+ connection alive across route changes (a panel unmount kills an in-flight response), and be the
151
+ app↔blox boundary so `TalosPanel` stays app-agnostic and reusable by desk/commerce.
152
+ - **`threadId` in URL search params via react-router 7 `useSearchParams`.** Rationale: matches how
153
+ supply already stores view state; makes conversations shareable and back-button-safe.
154
+ *Rejected:* `nuqs` (Next-only — do not copy that part of talos's provider).
155
+ - **Token held in React Query, not Zustand.** Rationale: `frontend.md` §3 — fetched server state
156
+ belongs in the query cache. Teardown of token + `threadId` + conversation on logout and tenant
157
+ switch per §18.
158
+ - **Make the mid-stream 401 behavior an explicit product decision** (silent refresh and resume vs.
159
+ surface an error and let the user resend) rather than discovering it in QA.
160
+ - **Registry fix: minimal annotation (option 2), not a full doc split.** Rationale: the Aegra
161
+ backend repo's name is unknown, so registration is deferred; dropping `language: python` from
162
+ `talos` immediately stops `/kickoff` loading `python.md` instead of `frontend.md` for a
163
+ TypeScript repo, and is reversible.
164
+
165
+ ## Blockers
166
+
167
+ - **Token audience — the decisive unknown.** Aegra's `AUTH_TYPE=togahub` validates api2 JWTs, but
168
+ supply's token is issued for the **supply** client, not TogaHub. Either Aegra accepts it (World A,
169
+ trivial) or supply needs a handoff (World B) — api2 already exposes `/auth/delegator`,
170
+ `/auth/encrypted`, `/auth/encrypted-user-uuid` for exactly this cross-app identity pass.
171
+ **This decides whether the connecting/error auth layer gets built at all, so answer it before
172
+ designing the provider.**
173
+ - **Aegra CORS allowlist** must include supply's per-client hostnames (`nychh`, `compass`, `quad`,
174
+ …) — a list, not one entry. Symptom if missing: the request never leaves the browser, red console
175
+ error, no response to debug. Looks like a front-end bug; isn't one.
176
+ - **Aegra tenant provisioning is explicit, no auto-create** — an unregistered org returns a clean
177
+ 403 on every call even with a valid token. Supply's client(s) need registering.
178
+ - **`TalosResponse` was widened by tcox on 2026-08-27 (`484e2a6`)** — gained `message` /
179
+ `suggestions` / `cta`, and **`paras` is now OPTIONAL**. Everything recorded about the contract
180
+ here was read off *committed* `TRUE-80692`; tcox's changes were uncommitted at their capture. The
181
+ streaming conclusion survives (the payload still resolves **once**), but **re-read
182
+ `types.ts` on blox `_main` before writing the mapper.**
183
+ - **BDR is now a second consumer of blox Talos** (via a local link; the `TalosAdapter` seam held
184
+ with no bridge layer). The `TalosPanel` contract change ripples to BDR — the additive
185
+ `connection`-defaults-to-`"ready"` design is what protects it.
186
+ - **Aegra backend repo name unknown** — blocks registering it and moving `"language": "python"`
187
+ onto it. Recorded in the architecture doc's change history; until then **no repo loads
188
+ `2.0/standards/python.md`**.
189
+ - **`.env.production` points `LANGGRAPH_API_URL` at BETA** with the comment "Currently not live with
190
+ new version of ACL" — there may be no live production Aegra yet.
191
+ - **Both working trees are on `TRUE-81380`**, not the ticket branches. The developer must switch
192
+ before any code work (Claude does not switch branches).
193
+ - **Harness defect in `knowledge.js publish`:** after a `CONFLICT` it had already committed, then
194
+ re-running printed `NO_CHANGES — nothing new to push` because it only checks for *uncommitted*
195
+ changes — the commit was still unpushed (`ahead 1, behind 2`). Taken at face value the capture
196
+ would have been silently lost locally. `publish` should treat "clean tree, unpushed commits on
197
+ `_main`" as a push case.
198
+
199
+ ## Exact next step
200
+
201
+ > Message the Aegra owners (`akhokhani`, `jcardinal`) and ask: **does Aegra's `AUTH_TYPE=togahub`
202
+ > validator accept a `toga25-supply`-issued api2 JWT, or does it require a TogaHub-audience token?**
203
+ > In parallel (it needs no answer), open
204
+ > `~/Agilant/toga-blox-npm/src/components/Talos/types.ts` on blox `_main` and read the current
205
+ > `TalosResponse` — tcox's `484e2a6` made `paras` optional and added `message`/`suggestions`/`cta`,
206
+ > which changes the mapper's target shape.
207
+
208
+ ---
209
+ _Saved by /session-save on 2026-08-28_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.690",
3
+ "version": "1.0.692",
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",