toga-ai 1.0.448 → 1.0.449

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
  | [Carrier Shipping Labels (UPS/FedEx) & NetSuite Item Fulfillment](features/carrier-shipping-labels.md) | Backend mechanics behind TOGa Supply's Fulfill & Ship: buying a carrier label (UPS/FedEx), persisting it, and creating the NetSuite Item Fulfillment with tracki | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ItemFulfillments/TrackingNumber.php, _underscore/Component/Library/LabelPdf/LabelPdf.php, _underscore/Component/Library/Carriers/ShipmentRequest/ShipmentRequest.php, _underscore/Component/Library/Carriers/Ups/Ups.php, _underscore/Component/Library/Carriers/Fedex/Fedex.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Trait/Netsuite/SalesOrder.php, _underscore/Component/Library/NetSuite/NetSuite.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/ShippingMethod.php, _underscore/Model.php, _underscore/Cloud.php |
13
13
  | [_Cloud S3 helpers (copy / get / delete / list)](features/cloud-s3-helpers.md) | `_Cloud` centralizes AWS SDK S3 usage for the 2.0 stack so the `S3Client` never leaks into workers or app code. | _underscore/Cloud.php |
14
14
  | [_Component_*/_Model_* project-namespace registration (autoloader) & backslash-qualify traps](features/component-model-namespace-registration.md) | Every **project-local** `_Component_*` and `_Model_*` class in a 2.0 app **must declare the project namespace** at the top of the file: ```php namespace <NAMESP | _underscore/Loader.php, worker2/_.php, api2/_.php, worker2/Component/Forecast/Db/Db.php, worker2/Component/Forecast/SaleImport/SaleImport.php, api2/Component/Api/Netsuite/Netsuite.php |
15
+ | [Re-pointing a DB alias mid-request (_Database::register park/restore)](features/database-alias-repointing.md) | `_Database` keys **all live per-database runtime state by the connection ALIAS** (`Client` / `_underscore::DB_CLIENT`, `ClientLogs`, `Archive`), **not** by the | _underscore/Database.php, _underscore/Query.php, api2/Component/Api/V2/V2.php, api2/Component/Api/CrossClient/CrossClient.php |
15
16
  | [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
16
17
  | [Error Reporting — Issue/Event Aggregation (agreed POST-to-receiver design)](features/error-reporting-issue-event.md) | Platform-wide error-reporting infrastructure for TOGA 2.0, built around a two-table **Issue / Event** aggregation model in the shared **Core Logs DB**. | _underscore/Error.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, dbchanges2/Logs/2026-07-06 - Issue and Event tables.sql |
17
18
  | [Record-Changed Event Publishing (_Event::publish to SQS)](features/event-publish-sqs.md) | `_Event::publish()` (in `_underscore/Event.php`) is the PHP side of the real-time event pipeline. | _underscore/Event.php |
@@ -0,0 +1,120 @@
1
+ ---
2
+ title: Re-pointing a DB alias mid-request (_Database::register park/restore)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-07-27
10
+ owners: ["jcardinal"]
11
+ files:
12
+ - _underscore/Database.php
13
+ - _underscore/Query.php
14
+ - api2/Component/Api/V2/V2.php
15
+ - api2/Component/Api/CrossClient/CrossClient.php
16
+ related:
17
+ - ./per-client-database-connections.md
18
+ - ../../api2/features/cross-client-data-retrieval.md
19
+ - ../architecture.md
20
+ ---
21
+
22
+ ## Summary
23
+
24
+ `_Database` keys **all live per-database runtime state by the connection ALIAS**
25
+ (`Client` / `_underscore::DB_CLIENT`, `ClientLogs`, `Archive`), **not** by the physical schema
26
+ name. Four statics are alias-keyed: `$_connections`, `$_queryCache`, `$_transactionStarts`,
27
+ `$_modelCache`.
28
+
29
+ Historically `_Database::register()` only rewrote `$_registers[$alias]`. It never touched
30
+ `$_connections`, and `getConnection()` returns an already-cached link if one exists under that
31
+ key. So **re-registering an alias changed the credentials but not the open mysqli link** — every
32
+ query after a mid-request client switch silently ran against the *previous* client's database.
33
+ `register()` now **parks and restores** the alias-keyed live state when an alias is re-pointed at a
34
+ different physical database, which makes mid-request client switching actually work.
35
+
36
+ ## Key files / entry points
37
+
38
+ - **`_underscore/Database.php`** — `register()` (park/restore), `registerClientDatabases()`
39
+ (per-client alias registration + Core host lookup), `getConnection()`,
40
+ `transactionCommit()` / `transactionRollback()` / `resolveParkedTransactions()`.
41
+ - **`api2/Component/Api/V2/V2.php`** — a call site that switches the `Client` alias mid-request.
42
+ - **`api2/Component/Api/CrossClient/CrossClient.php`** (~lines 193, 201, 207) — the cross-client
43
+ fan-out uses the same `registerClientDatabases()` + `_underscore::DB_CLIENT` pattern.
44
+
45
+ ## How it works
46
+
47
+ Two new statics on `_Database`:
48
+
49
+ - `public static $_aliases` — alias => the physical database name currently behind it.
50
+ - `private static $_parked` — physical database name => that database's parked live state
51
+ (connection, queryCache, transactionStart, modelCache).
52
+
53
+ On `register()`:
54
+
55
+ 1. If the alias is being pointed at the **same** physical database, this is a **no-op** for live
56
+ state. (This also fixes a latent bug where a redundant `register()` reset an already-begun
57
+ transaction flag from `true` back to `false`.)
58
+ 2. If it is being pointed at a **different** database, the **outgoing** database's connection,
59
+ queryCache, transactionStart and modelCache are parked under its own physical name, and the
60
+ alias slots are cleared.
61
+ 3. The **incoming** database's previously-parked state is restored if present — so an open
62
+ transaction on that database continues uninterrupted. If nothing is parked for it,
63
+ `transactionStart()` is armed as before.
64
+
65
+ `registerClientDatabases()` caches only the **Core host-lookup row** per clientId
66
+ (`static $cachedLookups`), and always runs the three `register()` calls. It **throws** when the
67
+ Core host lookup returns no row.
68
+
69
+ **Request boundary.** `transactionCommit(null)` / `transactionRollback(null)` iterate
70
+ `$_connections`, which is alias-keyed, so a parked database would be skipped and left with an open
71
+ transaction on a pooled connection. The null-branch of both now calls private
72
+ `resolveParkedTransactions(bool $isCommit)`, which sweeps `$_parked` for any entry with
73
+ `transactionStart === true`, commits or rolls it back, and marks it resolved so a later
74
+ switch-back cannot double-commit.
75
+
76
+ ## Key rules
77
+
78
+ - **Re-registering an alias does not, by itself, reconnect.** Any code that changes credentials
79
+ under an existing alias must go through `register()`'s park/restore path.
80
+ - **Any change that hides a connection from `$_connections` must also resolve its transaction at
81
+ the request boundary** — otherwise the transaction leaks onto a pooled connection.
82
+ - **Client-DB config that cannot be resolved must throw, not fall through.** Falling through leaves
83
+ the aliases pointed at the previous client, which reads as "wrong data," not "an error."
84
+
85
+ ## Design tradeoff (deliberate)
86
+
87
+ Park/restore was chosen over re-keying all four caches by **physical database name**, which would
88
+ have touched `Database.php`, `Query.php` and `Model.php`. Park/restore is contained to one function
89
+ and keeps every existing call site working unchanged. It is correct **only while every access to
90
+ those four caches goes through the alias** — nothing accesses them by physical name today. If that
91
+ ever changes, the re-keying approach becomes the right fix.
92
+
93
+ ## Gotchas / known issues
94
+
95
+ - **The original symptom is silent, not an error.** A cross-client user lookup aimed at
96
+ `Client_Compass` returned **0 rows** because it actually ran against `Client_True` (the home
97
+ client). Wrong-database bugs here look like missing data.
98
+ - **The old `static $cached` guard in `registerClientDatabases()` made switch-back a no-op.** It was
99
+ keyed on clientId (commented as an "efficiency patch so we don't re-register and break
100
+ connections") and skipped the entire registration block for any client already seen in the
101
+ request — so the common "switch to B, then switch back to A" pattern never re-registered A.
102
+ Replaced by `$cachedLookups`, which caches only the Core lookup.
103
+ - **`CrossClient.php` had the same latent bug** and needed no edit — the `Database.php` fix corrects
104
+ its cross-client reads in place.
105
+ - **Not yet runtime-tested** as of this capture: `php -l` passes and php-reviewer cleared the diff,
106
+ but no live cross-client run has confirmed it.
107
+
108
+ ## Change history
109
+
110
+ - 2026-07-27 — Initial capture. Root-caused a cross-client API bug to alias-keyed live state in
111
+ `_Database`: re-registering an alias never swapped the open mysqli link, so post-switch queries
112
+ hit the previous client's DB. Added `$_aliases`/`$_parked` park/restore in `register()`,
113
+ `resolveParkedTransactions()` on the null-branch of commit/rollback (parked transactions were
114
+ invisible to the request boundary), a throw when the Core host lookup returns no row, and
115
+ replaced the `static $cached` no-op guard with `$cachedLookups`. (jcardinal)
116
+
117
+ ## Related docs
118
+
119
+ - [Per-Client Database Connections & the Local Logs Trap](./per-client-database-connections.md)
120
+ - [Multi-Client (Cross-Client) Data Retrieval](../../api2/features/cross-client-data-retrieval.md)
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-23
9
+ updated: 2026-07-27
10
10
  owners: ["dfranks", "jcardinal", "mhammontree", "apeterson", "kyalamarthi"]
11
11
  files:
12
12
  - _underscore/Database.php
@@ -16,6 +16,7 @@ files:
16
16
  related:
17
17
  - ../architecture.md
18
18
  - ../workflows/local-db-refresh-from-beta.md
19
+ - ./database-alias-repointing.md
19
20
  ---
20
21
 
21
22
  ## Summary
@@ -118,12 +119,20 @@ here — they live in `Config/*.ini`.)
118
119
  A connect failure here threw past `api()` and got masked by `Route.php` as the misleading line-525
119
120
  "Failed to determine how to render view" error — see the Route.php swallow→mask gotcha in
120
121
  [_underscore architecture](../architecture.md#gotchas--known-issues).
122
+ - **These aliases key ALL live connection state — re-registering one does not reconnect.**
123
+ `$_connections`, `$_queryCache`, `$_transactionStarts` and `$_modelCache` are keyed by the
124
+ alias (`Client`, `ClientLogs`, `Archive`), not by the physical schema, so switching a client
125
+ mid-request needs `_Database::register()`'s park/restore path. See
126
+ [Re-pointing a DB alias mid-request](./database-alias-repointing.md).
121
127
  - Related 1.0 analogue: the legacy `App_` worker has the same hazard writing to `Logs.API`
122
128
  (`db_logs`) — the laptop trap there is documented separately in the worker NetSuite bootstrap
123
129
  notes.
124
130
 
125
131
  ## Change history
126
132
 
133
+ - 2026-07-27 — Recorded that the three per-client aliases key **all** live connection state, so
134
+ re-pointing an alias mid-request does not reconnect on its own; split the detail into
135
+ [Re-pointing a DB alias mid-request](./database-alias-repointing.md). (jcardinal)
127
136
  - 2026-07-23 — Documented that the **`Team` schema** (`Tasks`/`Sprints`, `_Model_Team_*`,
128
137
  `DB_TEAM`) is not a first-class api2 DB (not in `Records.aclDatabase`, which is CORE/CLIENT
129
138
  only) and physically resolves to the **core cluster** (reads → `reader1.core…`, writes →
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: draft
9
- updated: 2026-07-07
9
+ updated: 2026-07-27
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - api2/Component/Api/CrossClient/CrossClient.php
@@ -21,6 +21,7 @@ related:
21
21
  - ./encrypted-user-uuid-auth-handoff.md
22
22
  - ../../_underscore/features/per-client-database-connections.md
23
23
  - ../../_underscore/features/acl-permission-chain.md
24
+ - ../../_underscore/features/database-alias-repointing.md
24
25
  ---
25
26
 
26
27
  ## What it is
@@ -111,10 +112,19 @@ for how it sits among the shared/per-client clusters.
111
112
  emits `(object)$outRow`.
112
113
  - **The X-Cross-Client / X-Cross-User custom-header transport was a dead stub** — the V2 engine
113
114
  never reads such headers. The real transport is the two-phase encrypted-UUID auth handshake.
115
+ - **Switching the `Client` DB alias mid-request used to silently not switch.** `CrossClient.php`
116
+ (~lines 193/201/207) uses `_Database::registerClientDatabases()` + `_underscore::DB_CLIENT`, and
117
+ `_Database` keys its live connection state by **alias**, not physical schema — so cross-client
118
+ reads were executing against the *home* client's DB and returning 0 rows. Fixed in
119
+ `_underscore/Database.php` (no api2 edit needed); see
120
+ [Re-pointing a DB alias mid-request](../../_underscore/features/database-alias-repointing.md).
114
121
  - **`curl_multi` busy-spin guard** — both multi loops `usleep(100)` when
115
122
  `curl_multi_select() === -1`.
116
123
 
117
124
  ## Change history
125
+ - 2026-07-27 — Root-caused cross-client reads hitting the home client's DB: `_Database` keys live
126
+ connection state by alias, so re-registering `DB_CLIENT` never swapped the open link. Fixed in
127
+ `_underscore/Database.php`; CrossClient needed no edit. (jcardinal)
118
128
  - 2026-07-07 — Initial capture: scatter-gather cross-client retrieval engine (orchestrator, watermark
119
129
  k-way merge, keyset pagination Phase 0a/0b, `client`-option delegation, Cache cluster id 145).
120
130
  Code-complete + reviewer-hardened, not yet runtime-tested. (jcardinal)
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
17
17
 
18
18
  ## 2.0 framework
19
19
 
20
- - **_underscore** (_Underscore) _(framework core)_ — 38 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
20
+ - **_underscore** (_Underscore) _(framework core)_ — 39 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
21
  - **worker2** (Worker) — 31 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
22
  - **api2** (API) — 17 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
23
23
  - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.448",
3
+ "version": "1.0.449",
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",