toga-ai 1.0.301 → 1.0.302

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-02
9
+ updated: 2026-07-09
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - tools/mvc/talos/kb-documents/get.php
@@ -68,11 +68,18 @@ Classic view → Edit → Save per tab, with **full-set reconciliation** (transa
68
68
  back on error), trash-icon row removal, and an auto-dismissing toast. Backs the `Team.*`
69
69
  prompt/replacement tables that the worker2 pipeline reads at runtime.
70
70
 
71
+ **Encoding (fixed 2026-07-09):** terms were stored HTML-encoded and then re-escaped at render,
72
+ so they displayed **double-encoded** (e.g. "Grand & Toy"). `vocabulary/post.php` now
73
+ `html_entity_decode()`s values on save so the DB stores **raw** text; `vocabulary/get.php`
74
+ already escapes once at display. Do not re-add encoding on save.
75
+
71
76
  ## Helpers
72
77
 
73
78
  - **`App_Talos_S3`** (`_/app/talos/s3.php`) — S3 client; list KB slugs; list/count approved
74
79
  (UTC→Central for display); presigned URL; get/delete; `moveApprovedDocument`;
75
- `updateApprovedDocument`; `sanitizeTitle`; `copyWithinBucket`; `putObjectBody`; slug
80
+ `updateApprovedDocument`; `sanitizeTitle` (**now converts `/` and `\` to hyphens** — it
81
+ previously converted them to spaces, which let a slashed title create an unintended S3
82
+ sub-folder; matches worker2's `filenameSafeTitle()`); `copyWithinBucket`; `putObjectBody`; slug
76
83
  sanitize+lowercase; approved-prefix path guard. **`client()` credential resolution:** prefers a
77
84
  dedicated **`[talos]`** config section (the **togaiq**-capable key), falling back to `[aws]`, then
78
85
  the SDK default chain — because `App_Talos_S3` only ever talks to **togaiq**, so it needs the
@@ -114,6 +121,12 @@ prompt/replacement tables that the worker2 pipeline reads at runtime.
114
121
 
115
122
  ## Change history
116
123
 
124
+ - 2026-07-09 — Fixed `App_Talos_S3::sanitizeTitle()` to convert `/`,`\` to hyphens (was
125
+ spaces) so a slashed meeting title no longer creates a stray S3 sub-folder — matches
126
+ worker2's `filenameSafeTitle()`; `updateApprovedDocument()` already renames the S3 object and
127
+ updates the metadata-sidecar filename on a title edit. Fixed the vocabulary UI double-encoding
128
+ bug: `vocabulary/post.php` now `html_entity_decode()`s on save so the DB stores raw text
129
+ (`get.php` escapes once at display). (jcardinal)
117
130
  - 2026-07-02 — Added **move** (re-file a doc between KBs) and **edit** (title + Find&Replace + body,
118
131
  ported from the old `kb_processor.php`) to the doc modal, each via get+put (togaiq denies
119
132
  `CopyObject`) and each enqueuing a single `SyncKb("source,dest")` re-sync. Fixed togaiq
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-07
9
+ updated: 2026-07-09
10
10
  owners: ["jcardinal"]
11
11
  files:
12
12
  - _underscore/Query.php
@@ -74,6 +74,23 @@ and is a harmless no-op.
74
74
  connection and started a local transaction it never committed. Fixed by moving the async
75
75
  dispatch to the **top** of `execute()`, before any read/write connection or transaction setup.
76
76
 
77
+ ## When to use async vs. synchronous (usage rule)
78
+
79
+ Async is a **workaround for a stale connection after a long HTTP call**, not a
80
+ general-purpose "make writes faster" switch. Use it **only** for writes that happen *after* a
81
+ long-running (e.g. up to 600s) external call has left the local connection idle enough to hit
82
+ "MySQL server has gone away" — for example the **post-AI `setStatus` writes** in the Talos
83
+ transcript pipeline.
84
+
85
+ **Do NOT use async for a write another poll/step must read back.** An async write is not
86
+ durable on the caller's connection when control returns — it commits later, on the worker's
87
+ connection. In the Talos pipeline the **pre-AI dedupe / discovery-ledger writes**
88
+ (`recordExport`, `loadOrCreateProcessing` INSERT) were originally async and the rows weren't
89
+ durable before the next poll, so dedupe and the watermark failed and meetings reprocessed.
90
+ Those writes were changed to **synchronous + `_Database::transactionCommit(...)`**. Rule of
91
+ thumb: **read-back-before-worker-runs → synchronous+committed; fire-and-forget-after-slow-call
92
+ → async.** A stale read after such a write may also need `_Database::useQueryCache(false)`.
93
+
77
94
  ## Known open items
78
95
 
79
96
  - **SECURITY (open, developer aware).** DB username/password are currently sent in **plaintext**
@@ -83,6 +100,10 @@ and is a harmless no-op.
83
100
 
84
101
  ## Change history
85
102
 
103
+ - 2026-07-09 — Added the sync-vs-async usage rule (async is only for fire-and-forget writes
104
+ after a long external call that staled the connection; writes another step must read back
105
+ must stay synchronous+committed). Prompted by the Talos pipeline, where async pre-AI dedupe
106
+ writes weren't durable before the next poll and caused reprocessing. (jcardinal)
86
107
  - 2026-07-07 — Built writes-only async query execution (`_Query` `isAsync` → Worker
87
108
  `Infrastructure/Database/Query` action); fixed four correctness bugs found in review
88
109
  (constructor order, worker-side commit, double-dispatch sentinel, dangling local
@@ -18,14 +18,14 @@
18
18
  | [NetSuite ↔ ClickUp / TOGA Opportunity Sync (API Message Queue + worker2 webhook)](features/netsuite-opportunity-sync.md) | Outbound sync from NetSuite to TOGA for the record types the Forecast2 importer pulls (opportunities first; sales/items/etc. | worker2/Worker/Netsuite.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Clickup.php, worker2/Worker/Clickup/Opportunity.php, worker2/Controller/Index.php, _underscore/Worker.php, test/@dave/NetSuite/api-message-queue/lib_amq_queue.js, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/ue_amq_drain.js, test/@dave/NetSuite/api-message-queue/ss_amq_drain.js, test/@dave/NetSuite/api-message-queue/DEPLOY_RUNBOOK.md, test/@dave/clickup/backfill_opportunity_numbers.php, test/@dave/clickup/probe_opportunity_fields.php, test/@dave/probe_clickup_desc_match.php, test/@dave/test_model_load_behavior.php, dbchanges2/Forecast/2026-06-25a - Add unique index on Opportunities netsuiteOpportunityInternalId.sql, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
19
19
  | [NetSuite → Forecast Open-Orders Sync (salesOrder webhook → OpenOrderItems)](features/netsuite-salesorder-open-orders-sync.md) | Webhook-driven, single-record port of the legacy open-orders importer (TRUE-79142). | worker2/Worker/Netsuite/SalesOrder.php, worker2/Worker/Netsuite.php, test/@dave/probe_salesorder_rest_shape.php, test/@dave/probe_open_order_lines.php, test/@dave/check_so_status.php, test/@dave/check_so_history.php, test/@dave/probe_so_rest_lines.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_open_order_gating.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
20
20
  | [NetSuite Supporting-Record Webhook Importer (the reusable recipe)](features/netsuite-supporting-record-webhook-importer.md) | A single **repeatable recipe** for porting a legacy daily-pull NetSuite *supporting-record* importer (the lookup/dimension tables behind Forecast2 — Employees, | worker2/Worker/Netsuite/Employee.php, worker2/Worker/Netsuite/Account.php, worker2/Worker/Netsuite/Classification.php, worker2/Worker/Netsuite/Customer.php, worker2/Worker/Netsuite/Item.php, worker2/Worker/Netsuite.php, _underscore/Model/Forecast/Employee.php, _underscore/Model/Forecast/Account.php, _underscore/Model/Forecast/Classification.php, _underscore/Component/Forecast/Db/Db.php, test/@dave/test_employee_lifecycle.php, test/@dave/test_account_lifecycle.php, test/@dave/test_classification_lifecycle.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, worker/crons/toga2/forecast2/import_supporting_records.php |
21
- | [Background Email-Template Worker (_Worker_Notification_EmailTemplate)](features/notification-email-template.md) | `_Worker_Notification_EmailTemplate::Send(...)` dispatches a **stored, client-defined `EmailTemplates` row off-thread** as a background WorkerJob. | worker2/Worker/Notification/EmailTemplate.php, _underscore/Model/Client/EmailTemplate.php |
21
+ | [Background Email-Template Worker (_Worker_Notification_EmailTemplate)](features/notification-email-template.md) | `_Worker_Notification_EmailTemplate::Send(...)` dispatches a **stored, client-defined `EmailTemplates` row off-thread** as a background WorkerJob. | worker2/Worker/Notification/EmailTemplate.php, worker2/Worker/Client/True.php, _underscore/Model/Client/EmailTemplate.php |
22
22
  | [DB-Driven Notification (Internal) Email](features/notification-email.md) | Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated, not client-facing transactional mail) are sent through one worker | worker2/Worker/Notification/Email.php, _underscore/Model/Client/EmailTemplate.php, dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql, dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql |
23
23
  | [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. | worker2/Worker/Startech.php |
24
24
  | [Talos (TOGa IQ) Meeting-Notes Integration & Token Auto-Refresh (consumer)](features/talos-meeting-notes-integration.md) | How a **dev tool / agent consumes Talos (TOGa IQ)** to query the team meeting-notes corpus programmatically. | .claude/skills/plan-ticket/scripts/talos.js |
25
25
  | [Talos Pricing Automation (worker2 Cron — AWS Actuals, Calibration, Monthly Report)](features/talos-pricing-automation.md) | The worker2 half of the **Talos Pricing Platform** (see the talos `pricing-cogs-model` and tools `talos-pricing-ui` docs for the other halves). | worker2/Worker/Talos/Pricing.php, worker2/Database/TalosPricingCrons.sql |
26
- | [Talos Transcript Ingestion Pipeline (worker2 → AWS Bedrock KBs)](features/talos-transcript-ingestion.md) | `_Worker_Team_Transcripts` (in addition to the upstream `Export` action see [Teams Meeting Transcript Export](./teams-transcript-export.md)) now runs a fully | worker2/Worker/Team/Transcripts.php, worker2/bin/reprocess-transcripts.php, worker2/bin/sync-knowledge-bases.php, worker2/Config/production.ini, dbchanges2/Team/2026-06-30a, dbchanges2/Team/2026-06-30b, dbchanges2/Team/2026-06-30c, dbchanges2/Team/2026-06-30d, dbchanges2/Team/2026-06-30e, dbchanges2/Core/2026-06-30a, dbchanges2/Core/2026-07-02a, dbchanges2/Team/2026-07-02a |
26
+ | [Talos Transcript Ingestion Pipeline (worker2 → AWS Bedrock KBs)](features/talos-transcript-ingestion.md) | `_Worker_Team_Transcripts` runs a fully automated, cron-driven pipeline that ingests raw Teams transcripts into the **Talos / TOGa IQ** AWS Bedrock knowledge ba | worker2/Worker/Team/Transcripts.php, worker2/bin/sync-knowledge-bases.php, worker2/Config/production.ini, dbchanges2/Team/2026-06-30a, dbchanges2/Team/2026-06-30b, dbchanges2/Team/2026-06-30c, dbchanges2/Team/2026-06-30d, dbchanges2/Team/2026-06-30e, dbchanges2/Core/2026-06-30a, dbchanges2/Core/2026-07-02a, dbchanges2/Team/2026-07-02a, dbchanges2/Team/2026-07-08a, dbchanges2/Team/2026-07-09a |
27
27
  | [Team Sprint Management & Reporting](features/team-sprint-management.md) | `_Worker_Team_Sprint` (file `Worker/Team/Sprint.php`) is the engine behind TOGA's internal **development-sprint process and reporting**. | worker2/Worker/Team/Sprint.php |
28
- | [Teams Meeting Transcript Export](features/teams-transcript-export.md) | `_Worker_Team_Transcripts` (action `Team/Transcripts/Export`) polls Microsoft Graph for Teams meeting transcripts produced by a set of organizers and archives t | worker2/Worker/Team/Transcripts.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Core/2026-06-18a - Teams Transcript Export schedule.sql |
28
+ | [Teams Meeting Transcript Export](features/teams-transcript-export.md) | > **SUPERSEDED (2026-07-09) — the S3-staging model below is history.** `Export` is now a thin > **GRAPH-DIRECT** cron poller: it no longer archives raw VTT to ` | worker2/Worker/Team/Transcripts.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Core/2026-06-18a - Teams Transcript Export schedule.sql |
29
29
  | [VAPI Webhook Handler (worker2 — AI-BDR end-of-call processing)](features/vapi-webhook-handler.md) | `_Worker_Vapi` ([worker2/Worker/Vapi.php](worker2/Worker/Vapi.php)) is the **PHP side of the AI-BDR call loop** — the webhook that receives VAPI's end-of-call r | worker2/Worker/Vapi.php, worker2/Worker/Ai/Bdr/Vapi.php |
30
30
  | [WJE Freshservice Sync (worker2)](features/wje-freshservice-sync.md) | WJE ("WJE IT", helpdesk `wje.freshservice.com`) is a **Freshservice**-based help-desk client whose tickets, contacts, assets, groups, categories, and canned res | worker2/Worker/Wje.php, _underscore/Component/Api/Wje/Wje.php, _underscore/Model/Wje/Ticket.php, _underscore/Model/Wje/TicketNote.php, _underscore/Model/Wje/Contact.php, _underscore/Model/Wje/Unit.php, _underscore/Model/Wje/TicketTeam.php, _underscore/Model/Wje/TicketCategory.php, _underscore/Model/Wje/AssetType.php, _underscore/Model/Wje/PredefinedReply.php, library/app/api/wje.php, worker/crons/toga2/wje/import_supporting_records.php, worker/crons/toga2/wje/sync_togasupply_wje.php, worker/crons/notifications/reports/wje/wje_common.php, library/app/systemmonitor/wje.php, dbchanges2/Client_Wje/2024-10-04 - WjeOnboarding.sql |
31
31
  | [Ticket → ClickUp Pseudocode Planning (Talos-grounded)](workflows/ticket-to-pseudocode-planning.md) | A repeatable procedure for turning a ClickUp ticket into a reviewed, formatted implementation plan posted back to the ticket's `📝 Pseudocode` custom field. | test/@dave/clickup_md2delta.js |
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-07-07
9
+ updated: 2026-07-09
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - worker2/Controller/Index.php
@@ -154,6 +154,23 @@ worker tier creates the row itself on pickup.) Reason: the caller's MySQL transa
154
154
  open when SQS delivers; the worker reads in a separate connection and, if uncommitted, its
155
155
  SELECT returns nothing and the guard fires ("already processed or not found — skipping").
156
156
 
157
+ ## Long-running AI actions — sync vs. async writes
158
+
159
+ A worker action that makes a long (up to 600s) AI/HTTP call leaves its **local MySQL
160
+ connection idle** long enough to hit "MySQL server has gone away" on the *next* write. The
161
+ pattern that works (proven in the Talos transcript pipeline):
162
+
163
+ - **Writes that a later poll/step must read back go BEFORE the AI call, synchronous +
164
+ committed.** e.g. the transcript dedupe/discovery-ledger inserts. If these are dispatched
165
+ async (via `_Query` writes-only async → `Infrastructure/Database/Query`) the row is **not
166
+ durable** when control returns, so the next cron poll re-selects and reprocesses the same
167
+ item. A stale read immediately after may also need `_Database::useQueryCache(false)`.
168
+ - **Writes AFTER the AI call (e.g. `setStatus`) go async** — the connection is already stale,
169
+ and these are fire-and-forget status updates nobody reads back synchronously.
170
+
171
+ See [async query execution](../_underscore/features/async-query-execution.md) for the `_Query`
172
+ `isAsync` mechanism and its correctness gotchas.
173
+
157
174
  ## Key design decisions
158
175
 
159
176
  MySQL-first for the **Lambda/webhook/cron ingestion paths** (SQS on success) prevents lost
@@ -194,6 +211,10 @@ the MySQL-first design was departed from **on purpose** here.
194
211
 
195
212
  ## Change history
196
213
 
214
+ - 2026-07-09 — Added the "long-running AI actions — sync vs. async writes" rule: reads-back
215
+ writes go before the AI call, synchronous+committed; post-AI status writes go async.
216
+ Distilled from the Talos Graph-direct pipeline (async pre-AI dedupe writes weren't durable
217
+ before the next poll → reprocessing). (jcardinal)
197
218
  - 2026-06-08 — Initial worker2 architecture doc. (jcardinal)
198
219
  - 2026-07-07 — `_Worker::runTask()` moved to SQS-first (caller does no MySQL; worker tier
199
220
  creates/tracks the row on pickup); the former debug-only direct-payload path in
@@ -6,10 +6,11 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-30
10
- owners: ["mhammontree"]
9
+ updated: 2026-07-09
10
+ owners: ["mhammontree", "jcardinal"]
11
11
  files:
12
12
  - worker2/Worker/Notification/EmailTemplate.php
13
+ - worker2/Worker/Client/True.php
13
14
  - _underscore/Model/Client/EmailTemplate.php
14
15
  related:
15
16
  - ./notification-email.md
@@ -36,7 +37,8 @@ silently riding inside — the originating transaction.
36
37
 
37
38
  - `worker2/Worker/Notification/EmailTemplate.php` —
38
39
  `abstract class _Worker_Notification_EmailTemplate`. One method:
39
- `public static Send(string $clientIdentifier, string $uuid, $to, $cc, $bcc, ...$args): string`.
40
+ `public static Send(string $clientIdentifier, string $uuid, string|array $to = [], string|array $cc = [], string|array $bcc = [], ...$args): string`.
41
+ `to`/`cc`/`bcc` accept a string or array and **default to `[]`** when omitted.
40
42
  - Resolves the numeric `clientId` from `$clientIdentifier` via a Core query.
41
43
  - Registers the client DB with
42
44
  `_Database::registerClientDatabases($clientId, $environment)` (see the DB-registration
@@ -61,7 +63,21 @@ silently riding inside — the originating transaction.
61
63
  pattern: `_Worker_Startech`). See
62
64
  [`creating-worker-actions.md`](./creating-worker-actions.md).
63
65
  3. **Dispatch the template.** `_Model_Client_EmailTemplate::send()` loads the `EmailTemplates`
64
- row by UUID, substitutes `{placeholder}` vars from `...$args`, and sends via `_Email`.
66
+ row by UUID, substitutes `{placeholder}` vars from `...$args` into **both the stored subject
67
+ and body** (the body may contain HTML), and sends via `_Email`.
68
+ 4. **Failure handling.** A failed send **THROWS** and is recorded as a failed `WorkerJobs` row
69
+ (`isSuccess=0`) with **no retry**; the failure never affects whatever enqueued it (the send
70
+ is fully decoupled from the originating transaction).
71
+
72
+ ### TOGA Technology (`True`) template
73
+
74
+ The TOGA Technology template constant lives in worker2:
75
+ `_Worker_Client_True::EMAIL_TEMPLATE_UUID__TOGA_TECHNOLOGY = '232d4edb-c2fa-4a8b-b5b9-d5800c962e19'`
76
+ (`worker2/Worker/Client/True.php`); its `clientIdentifier` is `'True'`. The stored template
77
+ must define `{subject}` and `{body}` placeholders (the body may be HTML). This is the template
78
+ the **Talos transcript recap email** uses — see
79
+ [Talos Transcript Ingestion](./talos-transcript-ingestion.md). (The UUID is a template
80
+ identifier, not a secret.)
65
81
 
66
82
  ### Enqueuing it
67
83
 
@@ -86,6 +102,12 @@ flow into `...$args` as the template variables.
86
102
 
87
103
  ## Change history
88
104
 
105
+ - 2026-07-09 — Documented the full `Send` signature (`to`/`cc`/`bcc` default `[]`; extra
106
+ string-keyed params spread as named args into `...$args` → `{placeholder}` subs in subject
107
+ and body, body may be HTML) and the failure contract (throws → failed `WorkerJobs` row,
108
+ `isSuccess=0`, no retry, never affects the enqueuer). Added the TOGA Technology (`True`)
109
+ template constant `_Worker_Client_True::EMAIL_TEMPLATE_UUID__TOGA_TECHNOLOGY` — the template
110
+ the Talos recap email sends through. (jcardinal)
89
111
  - 2026-06-30 — Built the generic background email-template worker
90
112
  `_Worker_Notification_EmailTemplate::Send` (TRUE-79251): resolves the client, registers the
91
113
  client DB via `registerClientDatabases`, and dispatches a stored `EmailTemplates` row by
@@ -6,11 +6,10 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-02
9
+ updated: 2026-07-09
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - worker2/Worker/Team/Transcripts.php
13
- - worker2/bin/reprocess-transcripts.php
14
13
  - worker2/bin/sync-knowledge-bases.php
15
14
  - worker2/Config/production.ini
16
15
  - dbchanges2/Team/2026-06-30a
@@ -21,197 +20,197 @@ files:
21
20
  - dbchanges2/Core/2026-06-30a
22
21
  - dbchanges2/Core/2026-07-02a
23
22
  - dbchanges2/Team/2026-07-02a
23
+ - dbchanges2/Team/2026-07-08a
24
+ - dbchanges2/Team/2026-07-09a
24
25
  related:
25
26
  - ./teams-transcript-export.md
27
+ - ./notification-email-template.md
26
28
  - ./creating-worker-actions.md
27
29
  - ../architecture.md
30
+ - ../../_underscore/features/async-query-execution.md
28
31
  - ../../../1.0/apps/tools/features/talos-kb-documents-admin.md
29
32
  - ../../../1.0/apps/test/features/talos-kb-pipeline.md
30
33
  ---
31
34
 
32
35
  ## Summary
33
36
 
34
- `_Worker_Team_Transcripts` (in addition to the upstream `Export` action
35
- see [Teams Meeting Transcript Export](./teams-transcript-export.md)) now runs a fully
36
- automated, cron-driven pipeline that ingests raw Teams transcripts into the **Talos / TOGa IQ**
37
- AWS Bedrock knowledge bases. It replaces the old **manual** two-page web tooling
38
- (`test/team/talos/kb_processor.php` + `kb_processor.ini` — see
39
- [Talos Knowledge Base Pipeline](../../../1.0/apps/test/features/talos-kb-pipeline.md)); the
40
- cleanup/classify/approve/archive/sync logic is a port of that script, but driven by cron jobs
41
- and by **DB-editable** config instead of an `.ini`.
42
-
43
- Flow: raw VTT at `s3://toga-private/transcripts/{date}/…` → `Scan` enqueues one `Process` job
44
- per unprocessed key `Process` AI-cleans + AI-classifiesapproved `.txt` + Bedrock metadata
45
- sidecar written to `s3://togaiq/development-team/{kb_slug}/approved/` raw archivedBedrock
46
- `startIngestionJob` on that KB's data source.
37
+ `_Worker_Team_Transcripts` runs a fully automated, cron-driven pipeline that ingests raw
38
+ Teams transcripts into the **Talos / TOGa IQ** AWS Bedrock knowledge bases and emails a
39
+ recap to the meeting organizer. **As of 2026-07-09 the pipeline is GRAPH-DIRECT**: there is
40
+ **no S3 staging bucket** in the loop anymore. `Export()` is a thin cron poller that pulls
41
+ transcript references from Microsoft Graph and enqueues one `Process` job each; `Process()`
42
+ downloads the VTT straight from Graph and does everything in-memory. The old
43
+ `s3://toga-private/transcripts/` staging bucket is **no longer used at all**, the `Scan()`
44
+ action was **removed**, and `bin/reprocess-transcripts.php` was **deleted** (the `Export`
45
+ cron replaces it).
46
+
47
+ Flow: `Export` (cron) polls Graph per configured organizerrecords a discovery-ledger row
48
+ in `Team.TranscriptExports` enqueues one `Process` job per transcript`Process` downloads
49
+ the VTT from Graph, strips+groups it, AI clean+classify (pass 1), applies DB replacements,
50
+ uploads approved `.txt` + metadata sidecar to `togaiq`, archives the raw VTT to `togaiq` from
51
+ the in-memory copy → Bedrock `startIngestionJob` → a **second** AI pass returns structured
52
+ JSON that PHP renders to an inline-styled HTML recap email, enqueued to the organizer.
47
53
 
48
54
  ## Key files / entry points
49
55
 
50
- - `Worker/Team/Transcripts.php` — actions `Scan`, `Process(string $sourceKey)`,
51
- `SyncKb(string $kbSlug)` (comma-separated slug list), `SyncKnowledgeBases(bool $apply=true)`
52
- (all alongside the existing `Export`).
53
- - `bin/reprocess-transcripts.php` — standalone CLI backlog reprocessor.
56
+ - `Worker/Team/Transcripts.php` — actions `Export`, `Process`, `SyncKb`,
57
+ `SyncKnowledgeBases`. **`Scan` was removed.**
54
58
  - `bin/sync-knowledge-bases.php` — CLI entry for the Bedrock→`Team.KnowledgeBases` registry
55
59
  sync (`--dry-run` supported).
56
60
  - `Config/production.ini` `[talos]` and `[teams]` sections — S3/Bedrock targets + the two
57
- per-account S3 credential sets (see Configuration).
58
- - DB config + ledger tables in the `Team.*` schema (see Data model), authored in dbchanges2
59
- under `Team/2026-06-30a..e` + `Core/2026-06-30a`.
61
+ per-account S3 credential sets (see Configuration). Note the **toga-private raw read path
62
+ is gone**; the `[teams]` S3 key is no longer used by the ingestion loop.
63
+ - DB config + ledger tables in the `Team.*` schema (see Data model).
60
64
 
61
65
  ## How it works
62
66
 
63
- ### `Scan` (cron)
64
- Lists unprocessed raw transcripts under the `toga-private` `transcripts/` prefix (skips any
65
- `sourceKey` already in the `Team.TranscriptProcessing` ledger) and enqueues one `Process` job
66
- each via `_Worker::runTask('Team/Transcripts/Process', ['sourceKey' => …])`. Scheduled by a
67
- `Core.CronJobs` row (weekday business hours).
68
-
69
- ### `Process(string $sourceKey)`
70
- Port of `kb_processor`, per transcript:
71
- 1. Fetch the raw VTT from S3 (`toga-private`, **us-west-2**). Parse **date + meeting title up
72
- front** (previously parsed from the filename only *after* classification).
73
- 2. Call the TOGa IQ AI endpoint `https://api.togaiq.com/api/ai/generate`, model
74
- `bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0`, with an **extended** `output_schema`
75
- returning **both** `knowledge_doc` **and** `kb_slug` the AI picks the single best active KB
76
- from the supplied list, or `general`. The **meeting title is passed into**
77
- `callAICleaningAPI` and surfaced as a `MEETING TITLE:` block; the classifier instruction
78
- **prioritizes the title** (often the only KB signal on short transcripts). **Classification
79
- rule: customer over company** `toga-technology` is our *own* company, so a meeting involving
80
- both a customer and TOGA staff must classify to the **customer** KB; pick `toga-technology`
81
- only for purely-internal meetings; fall back to `general` when neither can be determined. The
82
- system prompt is **assembled at runtime** from DB tables: a template body with
83
- `{{APPLICATIONS}}`/`{{CLIENTS}}`/`{{PEOPLE}}`/`{{BUSINESS_TERMS}}`/`{{EXAMPLES}}`
84
- placeholders filled from `Team.TranscriptPromptTerms`. The active template body also
85
- **excludes personal/social/non-work content** and must never emit a "Personal Notes" section
86
- (see Change history / the 2026-07-02a Team migration).
87
- 3. Apply DB-driven text replacements from `Team.TranscriptReplacements` (`:c` case-sensitive,
88
- `:w` whole-word, `:cw`) via `preg_replace_callback` **so replacement backrefs stay literal**.
89
- 4. Build the approved name `"{YYYY-MM-DD} - {Title Case}.txt"`.
90
- 5. Upload to `s3://togaiq/development-team/{kb_slug}/approved/` (**us-east-1**, togaiq account
91
- client) plus a `{name}.txt.metadata.json` Bedrock sidecar.
92
- 6. Archive the raw VTT to `…/{kb_slug}/archive/{date}/` via **get(raw) + put(talos)** (NOT
93
- `CopyObject` source and dest are in **different AWS accounts**, so cross-account
94
- `CopyObject` is impossible), then delete the source.
95
- 7. AWS Bedrock `startIngestionJob` on that KB's data source (retry 5×30s on
96
- `ConflictException` / throttling). **If `kb_slug === 'general'`, sync ALL active KBs.**
97
-
98
- ### `SyncKnowledgeBases(bool $apply=true)` (cron)
99
- Keeps `Team.KnowledgeBases` current with **Bedrock as the source of truth** — the table only
100
- had the KBs that existed as S3 folders at build time (`general`, `office-depot`), so the AI
101
- classifier could never pick a KB (e.g. `elite`) that wasn't already a candidate. Lists all
102
- Bedrock KBs (`listKnowledgeBases`, via helper `listAllKnowledgeBases()`), filters to the
103
- `{kb_prefix}*` prefix (`development-team-`), derives the slug, and **upserts**: new rows insert
104
- `isActive=1` (`isGeneral=1` only for `general`), name title-cased, `bedrockKbId` cached; existing
105
- rows keep their name/flags and only refresh `bedrockKbId`. **Nothing is deleted or deactivated.**
106
- CLI entry `bin/sync-knowledge-bases.php` (`--dry-run`). Cron: `Core.CronJobs` row action
107
- `Team/Transcripts/SyncKnowledgeBases`, schedule `0 5 * * *` (daily 05:00 CT), `maxExecutionTime`
108
- 300 (dbchanges2 `Core/2026-07-02a`; depends on the 2026-05-14 `maxExecutionTime` migration).
109
-
110
- Idempotency + status are tracked in `Team.TranscriptProcessing`
111
- (`pending/cleaned/uploaded/archived/synced/failed`).
67
+ ### `Export(int $lookbackDays, ?int $limit = null, array $cc = [], array $bcc = [])` (cron)
68
+ Thin poller. The old `organizers` parameter was **REMOVED** — organizers are always resolved
69
+ from config now.
70
+ 1. For each configured organizer, poll Microsoft Graph for transcripts in the lookback window.
71
+ 2. Resolve the organizer email (see *Organizer email resolution*).
72
+ 3. Record a discovery-ledger row in `Team.TranscriptExports` — **synchronously, then
73
+ committed** (see the durability gotcha).
74
+ 4. Enqueue one `Process` job per transcript.
75
+
76
+ - `$limit` caps enqueues per run (mainly for testing; e.g. `1` = a single meeting).
77
+ - `cc` / `bcc` thread through each `Process` job into the recap email; validated in
78
+ `enqueueRecapEmail` (deduped, organizer excluded). **Fallback:** if the organizer email
79
+ can't be resolved but `cc` is present, the recap sends **TO** the cc addresses.
80
+ - A Graph `callTranscript` already recorded (`alreadyExported()`) is skipped. Export treats a
81
+ Duplicate-entry exception as **"skipped"**, not a hard error.
82
+
83
+ ### `Process(int $transcriptId, string $graphUserId, string $meetingId, string $organizerEmail, string $subject, string $meetingStartIso, array $cc = [], array $bcc = [])`
84
+ 1. Download the VTT **straight from Graph** (no S3 round trip).
85
+ 2. `stripAndGroupVtt()` strips WebVTT timestamps and **merges consecutive same-speaker cues
86
+ into one block** (~40% token reduction before the AI call).
87
+ 3. **AI clean + classify (pass 1)** — same single-best-KB-or-`general` contract as before
88
+ (title prioritized, customer-over-company). System prompt assembled at runtime from the
89
+ `Team.*` prompt tables.
90
+ 4. Apply DB replacements from `Team.TranscriptReplacements` via `preg_replace_callback`.
91
+ 5. Build the approved name and upload the approved `.txt` + `{name}.txt.metadata.json` Bedrock
92
+ sidecar to `s3://togaiq/development-team/{kb_slug}/approved/` (us-east-1, togaiq account).
93
+ 6. **Archive the raw VTT to `togaiq`** (`…/{kb_slug}/archive/{date}/`) **from the in-memory
94
+ Graph copy** no `toga-private` round trip, no cross-account CopyObject.
95
+ 7. Bedrock `startIngestionJob` on that KB's data source (retries on `ConflictException` /
96
+ throttling). `SyncKb` handles multi-slug lists; `syncBedrock()` now syncs a **single** KB
97
+ (the old "general fans out to all KBs" behavior was **intentionally removed**).
98
+ 8. **Second AI pass** returns structured JSON, which **PHP renders to inline-styled HTML** (a
99
+ real `<table>` for Action Items) for a recap email. Subject is built in PHP:
100
+ `"Recap: {Meeting} - {Date}"`.
101
+ 9. Enqueue `Notification/EmailTemplate/Send` to the organizer (see
102
+ [Background Email-Template Worker](./notification-email-template.md); the template is the
103
+ TOGA Technology `True`-client template).
104
+
105
+ ### Organizer email resolution
106
+ - `resolveOrganizerEmail()` calls Graph `/users/{id}?$select=mail,userPrincipalName` but
107
+ this needs the **`User.Read.All`** app permission, which the app registration **lacks**.
108
+ - **Reliable path = the FALLBACK:** `extractOrganizerUpn()` reads
109
+ `participants.organizer.upn` from the meeting. `getMeeting()` now requests
110
+ `$select=subject,startDateTime,participants`; the `OnlineMeetings` read scope already
111
+ exposes the organizer UPN, so **no extra permission** is needed.
112
+
113
+ ### `SyncKnowledgeBases(bool $apply = true)` (cron)
114
+ Keeps `Team.KnowledgeBases` current with **Bedrock as source of truth**. Lists all Bedrock
115
+ KBs, filters to `{kb_prefix}*` (`development-team-`), derives the slug, upserts (new →
116
+ `isActive=1`; existing keep flags, refresh `bedrockKbId`); nothing is deleted. CLI:
117
+ `bin/sync-knowledge-bases.php --dry-run`. Cron `0 5 * * *` (daily 05:00 CT).
112
118
 
113
119
  ### `SyncKb(string $kbSlug)`
114
- On-demand re-sync used by the Tools delete/move/edit flows (see
115
- [Talos KB Documents Admin](../../../1.0/apps/tools/features/talos-kb-documents-admin.md)).
116
- `$kbSlug` now accepts a **comma-separated slug list** and syncs each KB **sequentially** with
117
- `KB_SYNC_DELAY_SECONDS` spacing. This exists because **Bedrock allows only one in-flight
118
- ingestion per KB** — a move that fired two independent parallel `SyncKb` workers raced and one
119
- was dropped (only the destination synced). The Tools move now enqueues a **single**
120
- `SyncKb("source,dest")` job so both KBs sync in one job, in order.
121
-
122
- ### `bin/reprocess-transcripts.php` (CLI backlog reprocessor)
123
- Standalone CLI that bootstraps `_underscore`, lists existing raw transcripts under the
124
- `toga-private` `transcripts/` prefix, and runs each through `Process()`. Flags
125
- `--prefix` / `--limit` / `--dry-run`. Tolerates **both** the old `{slug}/{date}/` layout and the
126
- new `{date}/` layout. Requires the `ENVIRONMENT` env var, e.g.
127
- `ENVIRONMENT=production php bin/reprocess-transcripts.php --dry-run`.
128
-
129
- ### Graph download hardening
130
- The Graph transcript download is hardened against SSRF / token leak: `https`-only, a
131
- `*.microsoft.com` host allowlist, and `FOLLOWLOCATION` off.
120
+ On-demand re-sync used by the Tools delete/move/edit flows. `$kbSlug` accepts a
121
+ **comma-separated slug list** and syncs each KB **sequentially** with `KB_SYNC_DELAY_SECONDS`
122
+ spacing (Bedrock allows only one in-flight ingestion per KB, so parallel syncs race).
132
123
 
133
124
  ## Data model (`Team.*` schema, DB alias core cluster)
134
125
 
135
- Authored in dbchanges2 (`Team/2026-06-30a..e`, `Core/2026-06-30a`, all dated 2026-06-30). All
136
- PKs `INT UNSIGNED` to match the existing Talos Team-table family.
137
-
138
126
  - **`Team.TranscriptReplacements`** — `pattern, replacement, isCaseSensitive, isWholeWord, isActive`.
139
- Seeded ~109 rules from the old `.ini`.
140
- - **`Team.TranscriptPromptTerms`** — `category` ENUM (`application/client/person/businessTerm/example`),
141
- `term, isActive, dtCreated, dtUpdated`. Seeded from the `.ini` CORRECTIONS lists. PEOPLE are
142
- stored as full "First Last" names (only 2 confidently paired; the rest seeded as individual
143
- tokens, intended to be reconciled via the Tools vocabulary UI).
144
- - **`Team.TranscriptPromptTemplate`** — `name, bodyText` (w/ placeholders), `instruction, model,
145
- temperature, maxTokens, timeoutSeconds, isActive`. One active row seeded from the `.ini` system prompt.
146
- - **`Team.KnowledgeBases`** — `slug, name, isGeneral, bedrockKbId, isActive`. Seeded `general` +
147
- `office-depot`, but now **auto-kept-current from Bedrock** by the daily `SyncKnowledgeBases`
148
- action (Bedrock is the source of truth; the table is a cache/allowlist of classifier
149
- candidates). NOTE: the Tools *browse* UI still derives its live KB list from S3 folders, not
150
- this table.
151
- - **`Team.TranscriptProcessing`** — `sourceKey UNIQUE(255), kbSlug, approvedKey, archiveKey,
152
- status` ENUM (`pending/cleaned/uploaded/archived/synced/failed`), `aiModel, failureReason,
153
- dtProcessed`. Pipeline ledger + idempotency.
154
- - **`Core.CronJobs`** INSERT for action `Team/Transcripts/Scan` (weekday business hours);
155
- includes `dtCreated = NOW()` since the column has no default, and depends on the
156
- `maxExecutionTime` column migration.
127
+ - **`Team.TranscriptPromptTerms`** `category` ENUM, `term, isActive, …`.
128
+ - **`Team.TranscriptPromptTemplate`** — `name, bodyText, instruction, model, temperature, …`.
129
+ - **`Team.KnowledgeBases`** — `slug, name, isGeneral, bedrockKbId, isActive`; auto-kept-current
130
+ from Bedrock by the daily `SyncKnowledgeBases`.
131
+ - **`Team.TranscriptProcessing`** pipeline ledger + idempotency. `status` ENUM
132
+ (`pending/cleaned/uploaded/archived/synced/failed`). **`transcriptIdentifier` widened to
133
+ `VARCHAR(768)`** (dbchanges2 `Team/2026-07-09a`).
134
+ - **`Team.TranscriptExports`** — discovery ledger written by `Export`.
135
+ `transcriptIdentifier` **also widened to `VARCHAR(768)`**. Graph-direct columns +
136
+ `organizerEmail` added (dbchanges2 `Team/2026-07-08a`).
157
137
 
158
138
  ## Configuration
159
139
 
160
140
  `worker2/Config/production.ini` `[talos]`: `s3_bucket=togaiq`, `s3_region=us-east-1`,
161
141
  `bedrock_region=us-east-1`, `kb_prefix=development-team-`, `s3_root_path=development-team/`.
162
-
163
- **Two S3 credential sets one per AWS account (see the cross-account gotcha):**
164
- - `[teams]` `s3_access_key_id` / `s3_secret_access_key` the **toga-private** key (raw
165
- transcripts, us-west-2). Read by `s3Client()` / `getS3Client()`.
166
- - `[talos]` `s3_access_key_id` / `s3_secret_access_key` — the **togaiq** key (approved/archive
167
- KB bucket, us-east-1). Read by `getTalosS3Client()` and `getBedrockClient()`.
168
-
169
- Credential **values live only in the deployed `production.ini`** — documented here by section
170
- and account, never by value. Pre-existing plaintext keys should be rotated (separate track).
142
+ The `[talos]` key (togaiq account, us-east-1) is used for approved/archive writes, Bedrock,
143
+ **and now the archive of the raw VTT**. The `[teams]`/`toga-private` S3 key is **no longer
144
+ part of the ingestion loop** (raw reads removed). Credential values live only in the deployed
145
+ `production.ini` documented here by section/account, never by value.
171
146
 
172
147
  ## Gotchas / known issues
173
148
 
174
- - **Cross-ACCOUNT S3 (root cause of repeated PutObject/CopyObject `AccessDenied`).** `toga-private`
175
- (raw, us-west-2) and `togaiq` (KB bucket, us-east-1) live in **different AWS accounts** — **no
176
- single IAM key can write to both.** Credentials are therefore **per-bucket**: `[teams]` key for
177
- toga-private, `[talos]` key for togaiq. Because it spans accounts, **`CopyObject` is impossible**;
178
- the archive step is a **get(raw) + put(talos)**, not a server-side copy.
179
- - **`[talos]` keys were added to `production.ini` only** the beta/development configs still
180
- need them before the pipeline runs in those environments.
181
- - **`general` KB fans out.** A `Process()` classified as `general` (or a `general` sync) triggers
182
- a sync of **all** active KBs, not one.
183
- - **Replacements must use `preg_replace_callback`** so replacement text with `$`/`\` backrefs is
184
- treated literally (a plain `preg_replace` would interpret them).
185
- - **worker2 had no prior CLI precedent** the backlog reprocessor bootstraps by
186
- `chdir($projectRoot)` then `require vendor/autoload.php` + `_underscore.php`, and requires the
187
- `ENVIRONMENT` env var.
188
- - **Deploy TODOs left in code:** verify the live Bedrock KB region and data-source name against
189
- the actual account before first prod run.
190
- - **Pre-existing plaintext secrets** in `worker2/Config/production.ini` and the reference
191
- `kb_processor.ini` (AWS / aegra) should be rotated not introduced by this work; no new secret
192
- literals were added to any new file or migration.
149
+ - **Idempotency: 304-char Graph ids truncated at `VARCHAR(255)` (root cause of endless
150
+ reprocessing).** Graph `callTranscript` ids are ~304 chars; `transcriptIdentifier` columns
151
+ were `VARCHAR(255)`, so ids were silently truncated. `alreadyExported()` compared the full
152
+ (304) id against the stored (255) prefix and **never matched**, AND the INSERT collided on
153
+ the truncated prefix (`error 1062 Duplicate entry … uq_transcript`) so the same meeting
154
+ reprocessed on every poll. **Fixed by widening `transcriptIdentifier` to `VARCHAR(768)`** on
155
+ **both** `TranscriptProcessing` and the legacy `TranscriptExports`. 768 is the max for a
156
+ utf8mb4 single-column UNIQUE index (3072 bytes / 4).
157
+ - **Durability: dedupe-ledger writes must be SYNCHRONOUS + committed.** `recordExport()` and
158
+ the `loadOrCreateProcessing` INSERT had been dispatched **async** (see
159
+ [async query execution](../../_underscore/features/async-query-execution.md)), so the row
160
+ wasn't durable before the next poll dedupe/watermark failed and meetings re-ran. These
161
+ pre-AI writes must run **synchronously** and `_Database::transactionCommit(_underscore::DB_TEAM)`.
162
+ Conversely the **post-AI `setStatus` writes stay async** — those run after long (up to 600s)
163
+ AI HTTP calls, which is exactly when the local Core connection has gone stale ("MySQL server
164
+ has gone away"). `alreadyExported()` must also `_Database::useQueryCache(false)` so it does
165
+ not read a stale empty result.
166
+ - **S3 filename safety: a `/` in a meeting title created an unintended S3 sub-folder.**
167
+ `filenameSafeTitle()` replaces `/` and `\` with `-` when building the approved/archive/
168
+ metadata S3 key; the **email subject keeps the untouched original title**. (Tools-side
169
+ `App_Talos_S3::sanitizeTitle()` was fixed to match — see
170
+ [Talos KB Documents Admin](../../../1.0/apps/tools/features/talos-kb-documents-admin.md).)
171
+ - **KB registry split — a KB known to Bedrock/Client_True but not `Team.KnowledgeBases` is
172
+ silently rejected.** worker2's sync validates a KB slug against `Team.KnowledgeBases`
173
+ (`loadActiveKbSlugs`). A KB added only in Bedrock + `Client_True.VectorIndexes` (the registry
174
+ the newer Tools UI reads) is rejected by `SyncKb` as "unknown or inactive knowledge base" and
175
+ silently no-ops — the worker still returns HTTP-200, so the UI falsely reports "sync
176
+ initiated". **Fix:** run `Team/Transcripts/SyncKnowledgeBases` (or
177
+ `bin/sync-knowledge-bases.php`) to discover `development-team-*` KBs from Bedrock and upsert
178
+ them into `Team.KnowledgeBases`. **Open architectural point:** two competing registries —
179
+ `Team.KnowledgeBases` (worker2) vs `Client_True.VectorIndexes` (Tools UI).
180
+ - **Cross-ACCOUNT S3.** `togaiq` (us-east-1) approved/archive writes use the `[talos]` key;
181
+ `CopyObject` across accounts is impossible, so archive is get(in-memory)+put. (The old
182
+ toga-private cross-account read is no longer in the loop.)
193
183
 
194
184
  ## Change history
195
185
 
196
- - 2026-07-02Fixed cross-**account** S3 creds: `toga-private` and `togaiq` are separate AWS
197
- accounts, so credentials are now per-bucket (`[teams]` vs `[talos]` in `production.ini`) and the
198
- archive step is get+put instead of the impossible cross-account `CopyObject`. Built
199
- `SyncKnowledgeBases` (Bedrock→`Team.KnowledgeBases` upsert) + `bin/sync-knowledge-bases.php` and a
200
- daily `0 5 * * *` cron so the classifier's candidate KB list stays current. `SyncKb` now takes a
201
- comma-separated slug list and syncs sequentially (Bedrock allows one in-flight ingestion per KB;
202
- parallel syncs raced). Classification: meeting title now passed to and prioritized by the AI, and
203
- customer-over-company (a customer meeting stays with the customer even when TOGA staff attend).
204
- Prompt template surgically updated to exclude personal/social content ("Personal Notes"). (jcardinal)
205
- - 2026-06-30 Built the automated cron-driven ingestion pipeline (`Scan`/`Process`/`SyncKb`) +
206
- the `bin/reprocess-transcripts.php` backlog CLI, porting the manual `kb_processor.php`. Added
207
- the `Team.*` config/ledger schema (DB-editable replacements, prompt terms, prompt template, KB
208
- list, processing ledger) and the `Scan` cron. Classification moved from title-based to AI (single
209
- best KB, or `general`). Config vocabulary/replacements are now DB-driven and editable via the
210
- Tools UI. (jcardinal)
186
+ - 2026-07-09 — **Re-architected to GRAPH-DIRECT.** `Export` is now a thin cron poller
187
+ (removed the `organizers` param; added `$limit`, `cc`/`bcc`); `Scan` and
188
+ `bin/reprocess-transcripts.php` deleted; the `toga-private` staging bucket dropped entirely.
189
+ `Process` downloads the VTT straight from Graph, `stripAndGroupVtt()` (~40% token cut), and a
190
+ **second AI pass** now produces a PHP-rendered HTML recap email to the organizer
191
+ (`Notification/EmailTemplate/Send`, `True` template). Organizer email resolved via the
192
+ meeting `participants.organizer.upn` fallback (no `User.Read.All`). Widened
193
+ `transcriptIdentifier` to `VARCHAR(768)` on both ledgers (304-char Graph ids were truncated
194
+ at 255 dedupe never matched + 1062 collision → endless reprocessing). Made the pre-AI
195
+ dedupe/discovery writes synchronous+committed (they had been async and weren't durable before
196
+ the next poll); post-AI status writes stay async. `filenameSafeTitle()` converts `/`,`\` →
197
+ `-` in S3 keys. `syncBedrock()` no longer fans "general" out to all KBs. dbchanges2:
198
+ `Team/2026-07-08a`, `Team/2026-07-09a`. (jcardinal)
199
+ - 2026-07-02 Fixed cross-**account** S3 creds (per-bucket `[teams]`/`[talos]`, archive via
200
+ get+put); built `SyncKnowledgeBases` + `bin/sync-knowledge-bases.php` + daily cron; `SyncKb`
201
+ now takes a comma-separated slug list synced sequentially. Classification: title prioritized,
202
+ customer-over-company. Prompt template excludes personal/social content. (jcardinal)
203
+ - 2026-06-30 — Built the automated cron-driven ingestion pipeline porting the manual
204
+ `kb_processor.php`; added the `Team.*` config/ledger schema; classification moved to AI. (jcardinal)
211
205
 
212
206
  ## Related docs
213
207
 
214
- - [Teams Meeting Transcript Export](./teams-transcript-export.md) — the upstream S3 producer.
208
+ - [Teams Meeting Transcript Export](./teams-transcript-export.md) — now merged into this
209
+ Graph-direct `Export` poller.
210
+ - [Background Email-Template Worker](./notification-email-template.md) — the recap email path.
211
+ - [Async Query Execution](../../_underscore/features/async-query-execution.md) — the sync-vs-async write rule.
215
212
  - [Talos KB Documents Admin](../../../1.0/apps/tools/features/talos-kb-documents-admin.md) — the Tools UI.
216
213
  - [Talos Knowledge Base Pipeline](../../../1.0/apps/test/features/talos-kb-pipeline.md) — the manual script this ports.
217
214
  - [Creating Worker Actions](./creating-worker-actions.md)
215
+ </content>
216
+ </invoke>
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-30
9
+ updated: 2026-07-09
10
10
  owners: ["ajean", "jcardinal"]
11
11
  files:
12
12
  - worker2/Worker/Team/Transcripts.php
@@ -21,6 +21,16 @@ related:
21
21
 
22
22
  ## Summary
23
23
 
24
+ > **SUPERSEDED (2026-07-09) — the S3-staging model below is history.** `Export` is now a thin
25
+ > **GRAPH-DIRECT** cron poller: it no longer archives raw VTT to `s3://toga-private/transcripts/`
26
+ > at all. It records a discovery-ledger row and enqueues one `Process` job per transcript, and
27
+ > `Process` downloads the VTT straight from Graph in-memory (raw is archived to `togaiq`, not
28
+ > toga-private). The `toga-private` staging bucket, the flat `transcripts/{date}/…` layout, and
29
+ > the `Backfill()` re-filer described here are no longer in the loop. See
30
+ > [Talos Transcript Ingestion](./talos-transcript-ingestion.md) for the current pipeline. The
31
+ > Graph organizer-resolution, GUID-vs-UPN, and Entra-permission material below is **still
32
+ > accurate** and remains the reference for how `Export` talks to Graph.
33
+
24
34
  `_Worker_Team_Transcripts` (action `Team/Transcripts/Export`) polls Microsoft Graph for
25
35
  Teams meeting transcripts produced by a set of organizers and archives the raw WebVTT to S3.
26
36
  As of 2026-06-30 it **no longer classifies by client**: raw transcripts land **flat** at
@@ -171,6 +181,13 @@ Policy gap; a `400` only on a UPN means the id was never resolved to a GUID. (A
171
181
 
172
182
  ## Change history
173
183
 
184
+ - 2026-07-09 — **`Export` re-architected to GRAPH-DIRECT** (merged into the ingestion pipeline).
185
+ The `toga-private` staging bucket is dropped: `Export` is now a thin poller that records a
186
+ discovery row and enqueues one `Process` job per transcript; `Process` downloads the VTT
187
+ straight from Graph and archives the raw to `togaiq`. `Export` signature is now
188
+ `Export(int $lookbackDays, ?int $limit = null, array $cc = [], array $bcc = [])` (the
189
+ `organizers` param removed). The S3-layout / `Backfill()` sections above are historical. See
190
+ [Talos Transcript Ingestion](./talos-transcript-ingestion.md). (jcardinal)
174
191
  - 2026-06-30 — **Removed title-based client classification and the client-folder S3 layout.**
175
192
  Raw transcripts now land flat at `transcripts/{date}/…` with no client subfolder; deleted
176
193
  `classify()`/`buildClientIndex()`/`loadClientNames()`/`loadClientAliases()` and the old
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.301",
3
+ "version": "1.0.302",
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",