toga-ai 1.0.394 → 1.0.396
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.
- package/knowledge/2.0/apps/ai-bdr/INDEX.md +2 -2
- package/knowledge/2.0/apps/ai-bdr/features/vapi-integration.md +27 -7
- package/knowledge/2.0/apps/ai-bdr/features/web-funnel-app.md +131 -3
- package/knowledge/2.0/apps/ai-bdr/workflows/safe-call-loop-testing.md +64 -5
- package/knowledge/2.0/apps/toga2-supply/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga2-supply/features/fulfill-and-ship.md +42 -0
- package/knowledge/2.0/apps/worker2/features/vapi-webhook-handler.md +34 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
| [BDR Web Funnel — Full Implementation Plan](features/bdr-web-funnel-plan.md) | > **Execution status (2026-07-16):** PLAN **Phases 0–5 are BUILT and QA'd**, Phase 6 > was **mostly already built** (the `info` contract was implemented at comm | bdr/PLAN.md, bdr/mockup/app.jsx, bdr/mockup/screens.jsx, bdr/mockup/components.jsx |
|
|
7
7
|
| [Call Orchestration — PHP Worker ↔ Vapi (the integration seam)](features/call-orchestration.md) | The **PHP worker** is the orchestrator; **Vapi** is the actor. | ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-inbound-callback-assistant-request.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md, ai-bdr/scripts/update_structured_output.py |
|
|
8
8
|
| [Vapi Integration — Assistants, Tools, Structured Output](features/vapi-integration.md) | Everything inside Vapi: the three assistants, the three shared tools, the shared structured-output schema, the Liquid-templated system prompt, and the Python sc | ai-bdr/vapi/templates/assistant.template.json, ai-bdr/vapi/templates/system-prompt.template.md, ai-bdr/prompts/archive/prompt-may-15.txt, ai-bdr/prompts/campaigns/healthcare-2026-03-18.md, ai-bdr/prompts/campaigns/healthcare-v2-2026-03-25.md, ai-bdr/prompts/templates/campaign-template.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py, ai-bdr/scripts/update_structured_output.py, ai-bdr/scripts/update_call_summary_context.py, ai-bdr/scripts/fix_booking_guardrails.py, ai-bdr/scripts/get_assistant.py, ai-bdr/docs/vapi-firstmessage-timing-fix.md, ai-bdr/docs/vapi-voicemail-iphone-screening.md |
|
|
9
|
-
| [Web Funnel — Built Next.js App (structure, stack, how to run)](features/web-funnel-app.md) | The **built** state of the BDR web funnel — the Next.js UI / lead-capture front door of the existing AI-BDR product (see `../architecture.md`). | bdr/PLAN.md, bdr/.gitignore, bdr/eslint.config.mjs, bdr/amplify.yml, bdr/src/app, bdr/src/app/layout.tsx, bdr/src/app/page.tsx, bdr/src/app/studio.css, bdr/src/lib/resolveCampaignId.ts, bdr/src/lib/scheduleSlots.ts, bdr/src/lib/formatPhone.ts, bdr/src/lib/analytics.ts, bdr/src/lib/rateLimit.ts, bdr/src/lib/clientIp.ts, bdr/src/proxy.ts, bdr/.env.example, bdr/src/components/GoogleAnalytics.tsx, bdr/src/components/RichText.tsx, bdr/src/content, bdr/src/components, bdr/src/flow, bdr/src/server, bdr/src/server/toga.ts, bdr/src/app/api, bdr/test, bdr/test/togaPrimaryPhone.test.ts, bdr/public, bdr/mockup/styles.css |
|
|
9
|
+
| [Web Funnel — Built Next.js App (structure, stack, how to run)](features/web-funnel-app.md) | The **built** state of the BDR web funnel — the Next.js UI / lead-capture front door of the existing AI-BDR product (see `../architecture.md`). | bdr/PLAN.md, bdr/.gitignore, bdr/src/server/hubspot.ts, bdr/src/server/leadSink.ts, bdr/src/server/callbackService.ts, bdr/src/server/workerDialer.ts, bdr/src/lib/shareMailto.ts, bdr/src/flow/screens/CallNow.tsx, bdr/src/flow/screens/Schedule.tsx, bdr/src/flow/screens/CallSummary.tsx, bdr/src/content/schema.ts, bdr/src/content/default.ts, bdr/test/togaEnrichment.test.ts, bdr/test/fastDial.test.ts, bdr/test/shareMailto.test.ts, bdr/eslint.config.mjs, bdr/amplify.yml, bdr/src/app, bdr/src/app/layout.tsx, bdr/src/app/page.tsx, bdr/src/app/studio.css, bdr/src/lib/resolveCampaignId.ts, bdr/src/lib/scheduleSlots.ts, bdr/src/lib/formatPhone.ts, bdr/src/lib/analytics.ts, bdr/src/lib/rateLimit.ts, bdr/src/lib/clientIp.ts, bdr/src/proxy.ts, bdr/.env.example, bdr/src/components/GoogleAnalytics.tsx, bdr/src/components/RichText.tsx, bdr/src/content, bdr/src/components, bdr/src/flow, bdr/src/server, bdr/src/server/toga.ts, bdr/src/app/api, bdr/test, bdr/test/togaPrimaryPhone.test.ts, bdr/public, bdr/mockup/styles.css |
|
|
10
10
|
| [Web Funnel — Next.js UI + Config-Driven Campaign Content](features/web-funnel-content-model.md) | > **Status note (RESOLVED 2026-07-16):** the content **contract** below (schema, > provider seam, adapters) is BUILT and QA'd (PLAN Phases 0–5). | bdr/src/content/schema.ts, bdr/src/content/provider.ts, bdr/src/content/useCampaign.ts, bdr/src/content/default.ts, bdr/src/lib/resolveCampaignId.ts, bdr/src/server/leadSink.ts, bdr/src/server/callbackService.ts, bdr/src/app/api/call-now/route.ts, bdr/src/app/api/call-later/route.ts, bdr/src/app/api/contact/route.ts |
|
|
11
11
|
| [New-Campaign Onboarding (6-phase runbook)](workflows/new-campaign-onboarding.md) | The end-to-end procedure for launching a new outbound BDR campaign (industry, offer, target persona, geography). | ai-bdr/docs/client-onboarding-sop.md, ai-bdr/docs/calcom-new-campaign-event-guide.md, ai-bdr/prompts/templates/campaign-template.md, ai-bdr/scripts/update_assistant.py, ai-bdr/scripts/update_system_prompt.py |
|
|
12
|
-
| [Safe AI-BDR Call-Loop Testing (isolated test-campaign runbook)](workflows/safe-call-loop-testing.md) | How to prove the AI-BDR **dialer → Vapi → end-of-call webhook** loop end-to-end **without dialing real prospects**. | dbchanges2/Client_True/2026-07-20a - AiBdrTcoxTestCampaign.sql, worker2/Worker/Ai/Bdr/Vapi.php, worker2/Config/beta.ini |
|
|
12
|
+
| [Safe AI-BDR Call-Loop Testing (isolated test-campaign runbook)](workflows/safe-call-loop-testing.md) | How to prove the AI-BDR **dialer → Vapi → end-of-call webhook** loop end-to-end **without dialing real prospects**. | dbchanges2/Client_True/2026-07-20a - AiBdrTcoxTestCampaign.sql, worker2/Worker/Ai/Bdr/Vapi.php, worker2/Worker/Ai/Bdr/ZoomInfo.php, worker2/Config/beta.ini |
|
|
@@ -6,8 +6,8 @@ project: AI-BDR
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [akhokhani]
|
|
9
|
+
updated: 2026-07-21
|
|
10
|
+
owners: [akhokhani, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- ai-bdr/vapi/templates/assistant.template.json
|
|
13
13
|
- ai-bdr/vapi/templates/system-prompt.template.md
|
|
@@ -71,6 +71,14 @@ firstMessage, system prompt, and the phone number wired to them.
|
|
|
71
71
|
- Healthcare — `+1 (331) 244-5958` — `0c058f55-1610-413c-ae04-c71b30b33a43`
|
|
72
72
|
- Tech demo — `+1 (331) 281-1679` — `0505c46e-1150-4814-9a5c-60ad44287abf`
|
|
73
73
|
|
|
74
|
+
**A campaign's caller ID is its `Campaigns.c_fromPhoneNumberIdentifier`, not "the primary BDR
|
|
75
|
+
number."** Live example: campaign 2 (`26.05 - AI BDR - Ryan Nitti`) carries
|
|
76
|
+
`c_fromPhoneNumberIdentifier = a5d6cbe1-073b-4288-95e8-853ec6782a96`, which maps to the
|
|
77
|
+
**Healthcare** number `+1 (331) 244-5958` — so that campaign dials from the healthcare caller ID,
|
|
78
|
+
not the primary BDR line. `buildVapiPayload` (worker2 dialer) **hard-requires** this field — a
|
|
79
|
+
campaign created without it cannot place a call (see
|
|
80
|
+
`../workflows/safe-call-loop-testing.md`).
|
|
81
|
+
|
|
74
82
|
## Tools (3, shared across assistants)
|
|
75
83
|
|
|
76
84
|
| Tool | Id | Purpose |
|
|
@@ -114,7 +122,7 @@ Returned in `message.analysis.structuredData` on every `end-of-call-report`:
|
|
|
114
122
|
| `contactEmail` | string | Email the prospect confirmed; empty if not collected |
|
|
115
123
|
| `emailMatchedKnown` | boolean | True if confirmed email matches `knownEmail` from variableValues |
|
|
116
124
|
| `meetingTime` | ISO 8601 | Present if `callOutcome=meeting_booked` |
|
|
117
|
-
| `callbackTime` | ISO 8601 | Present if `callOutcome=callback_scheduled`; backend converts a spoken time to ISO |
|
|
125
|
+
| `callbackTime` | ISO 8601 | Present if `callOutcome=callback_scheduled`; backend converts a spoken time to ISO. **BUG (2026-07-21): the model frequently emits natural language with spelled-out numbers (e.g. `"in ten minutes"`) which the PHP parser cannot handle — the callback is then silently dropped. See the convertDateTime gotcha in [worker2 vapi-webhook-handler.md](../../worker2/features/vapi-webhook-handler.md). Fix candidate on this side: constrain this field's schema to ISO 8601 or a numeric-relative format so the model never spells the number out.** |
|
|
118
126
|
| `callbackContactPreference` | string | `phone` / `email` / `sms` |
|
|
119
127
|
| `callType` | string | Mirrors the `callType` variable injected on this call |
|
|
120
128
|
| `nextCallOpeningContext` | string | One-sentence coaching note for the next attempt's opener |
|
|
@@ -153,10 +161,15 @@ ensure UA spoofing is preserved or you'll see 403s with no body.
|
|
|
153
161
|
- **Voicemail detection isn't perfect on iPhone "Silence Unknown Callers".**
|
|
154
162
|
See `docs/vapi-voicemail-iphone-screening.md` for the screening
|
|
155
163
|
workaround.
|
|
156
|
-
- **First-message timing (Option B)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
- **First-message timing (Option B) — TESTERS MUST SPEAK FIRST.** The healthcare assistant
|
|
165
|
+
(`Campaign - HealthCare`, `0c058f55-1610-413c-ae04-c71b30b33a43`) is configured
|
|
166
|
+
`firstMessageMode = "assistant-waits-for-user"`: it will **not** utter a word until the callee
|
|
167
|
+
speaks. This is the documented voicemail/iPhone-screening workaround (Option B), but it means a
|
|
168
|
+
**silent answerer gets silence** — confirmed live 2026-07-21 with an 8-second mutual-silence call
|
|
169
|
+
that Vapi ended with `endedReason = customer-ended-call`. When manually testing this campaign,
|
|
170
|
+
**say "Hello?" after answering** or the call dies silently. The assistant waits for the human's
|
|
171
|
+
first phoneme via the `status-update` server message; don't unsubscribe from `status-update`
|
|
172
|
+
without re-checking the prompt's opener logic.
|
|
160
173
|
- **`GetBookingSlots` can span a weekend.** `fix_booking_guardrails.py`
|
|
161
174
|
enforces a Mon–Fri 9–5 default; relax it cautiously.
|
|
162
175
|
- **`previousCallSummary` is regenerated per call** — don't try to persist
|
|
@@ -168,4 +181,11 @@ ensure UA spoofing is preserved or you'll see 403s with no body.
|
|
|
168
181
|
`callSummary` over a schema fork.
|
|
169
182
|
|
|
170
183
|
## Change history
|
|
184
|
+
- 2026-07-21 — Live go-live findings. Recorded that the healthcare assistant runs
|
|
185
|
+
`firstMessageMode = "assistant-waits-for-user"` (Option B) so a silent answerer gets silence
|
|
186
|
+
(confirmed: 8s mutual-silence call, `endedReason=customer-ended-call`) — testers must speak first.
|
|
187
|
+
Documented that a campaign's caller ID is its `c_fromPhoneNumberIdentifier` (campaign 2's
|
|
188
|
+
`a5d6cbe1-…` → the Healthcare number, not the primary BDR line) and that `buildVapiPayload`
|
|
189
|
+
hard-requires that field. Flagged the `callbackTime` spelled-out-number parse bug (schema-side fix
|
|
190
|
+
candidate: constrain to ISO/numeric-relative) — full detail on the worker2 webhook doc. (tcox)
|
|
171
191
|
- 2026-06-16 — Initial Vapi integration feature doc. (akhokhani)
|
|
@@ -6,11 +6,24 @@ project: AI-BDR
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-21
|
|
10
10
|
owners: [tcox]
|
|
11
11
|
files:
|
|
12
12
|
- bdr/PLAN.md
|
|
13
13
|
- bdr/.gitignore
|
|
14
|
+
- bdr/src/server/hubspot.ts
|
|
15
|
+
- bdr/src/server/leadSink.ts
|
|
16
|
+
- bdr/src/server/callbackService.ts
|
|
17
|
+
- bdr/src/server/workerDialer.ts
|
|
18
|
+
- bdr/src/lib/shareMailto.ts
|
|
19
|
+
- bdr/src/flow/screens/CallNow.tsx
|
|
20
|
+
- bdr/src/flow/screens/Schedule.tsx
|
|
21
|
+
- bdr/src/flow/screens/CallSummary.tsx
|
|
22
|
+
- bdr/src/content/schema.ts
|
|
23
|
+
- bdr/src/content/default.ts
|
|
24
|
+
- bdr/test/togaEnrichment.test.ts
|
|
25
|
+
- bdr/test/fastDial.test.ts
|
|
26
|
+
- bdr/test/shareMailto.test.ts
|
|
14
27
|
- bdr/eslint.config.mjs
|
|
15
28
|
- bdr/amplify.yml
|
|
16
29
|
- bdr/src/app
|
|
@@ -93,7 +106,9 @@ to its ship state (trusted-IP position for X-Forwarded-For; O(1) LRU-capped limi
|
|
|
93
106
|
- **ESLint flat config** carries a **no-em-dash rule** on shipped strings
|
|
94
107
|
(`Literal` / `TemplateElement` / `JSXText` selectors) — enforces the mockup copy
|
|
95
108
|
rule on in-repo strings (`eslint.config.mjs`).
|
|
96
|
-
- **Vitest** test suite: **
|
|
109
|
+
- **Vitest** test suite: **131 tests, all green** (2026-07-21 added `togaEnrichment.test.ts`,
|
|
110
|
+
`fastDial.test.ts`, `shareMailto.test.ts` for the enrichment / fast-dial / share features below;
|
|
111
|
+
10 added 2026-07-20 for the primary-phone
|
|
97
112
|
self-heal — null-pointer re-link, changed-number re-link, create-as-last-resort, strict
|
|
98
113
|
`requestCall` vs. absorbing `upsertContactByEmail`; 13 added in the first 2026-07-16 session:
|
|
99
114
|
phone format/prefill/mask, live-anchor consent links, `submitOk` race; the later 2026-07-16
|
|
@@ -168,6 +183,10 @@ to its ship state (trusted-IP position for X-Forwarded-For; O(1) LRU-capped limi
|
|
|
168
183
|
number next to the fixed `+1` country-code field would dial the wrong country). CallNow
|
|
169
184
|
+ Schedule init their number fields from it. (The old `info` site exposed name + email +
|
|
170
185
|
phone the same way; BDR reduces the client-crossing PII to **phone only**.)
|
|
186
|
+
**Fallback (2026-07-21):** when HubSpot has no phone for the contact, primeLead now falls back to
|
|
187
|
+
the **Toga contact's existing primary phone** (`LeadRef` gained an optional `phone`) — the one
|
|
188
|
+
real parity gap the info-vs-BDR audit found (info's `defaultPhone` behavior). So a known lead
|
|
189
|
+
whose CRM record carries no phone but whose Toga record does still sees their number pre-filled.
|
|
171
190
|
- **Live phone input mask.** `formatPhoneInput()` (`src/lib/formatPhone.ts`) renders a
|
|
172
191
|
progressive `(555) 012-3456` as the user types: hard 10-digit cap (`maxLength=14` on the
|
|
173
192
|
inputs), strips non-digits on paste, idempotent, and backspace-safe by design (formatting
|
|
@@ -247,6 +266,93 @@ source of truth for "why isn't my contact being called." Full rules are document
|
|
|
247
266
|
injection to true reverse/has-many relations). The self-heal covers the funnel meanwhile; the
|
|
248
267
|
same api2 bug likely affects `primaryContactEmailAddress` / `primaryContactAddress` on update.
|
|
249
268
|
|
|
269
|
+
## Contact enrichment (funnel-side, feeds the Vapi opener)
|
|
270
|
+
|
|
271
|
+
The funnel enriches the Toga contact at page-prime so the AI-BDR call opener has real context
|
|
272
|
+
(`src/server/hubspot.ts`, `src/server/toga.ts`, `src/server/leadSink.ts`, `src/app/page.tsx`; tests
|
|
273
|
+
`test/togaEnrichment.test.ts`). The HubSpot fetch was extended to pull **industry / city / state /
|
|
274
|
+
website** on top of the existing properties and maps them two ways:
|
|
275
|
+
|
|
276
|
+
- **Direct Toga columns (worker2's dialer reads these with NO callContext fallback):**
|
|
277
|
+
`jobtitle → Contacts.title`, `company → Contacts.doingBusinessAs`, `industry → Contacts.industry`.
|
|
278
|
+
`buildVapiPayload` reads `title` / `companyName` / `companyIndustry` **directly from these columns**
|
|
279
|
+
and does **not** fall back to `callContext` for them — so if they aren't on the contact row, the
|
|
280
|
+
opener has nothing.
|
|
281
|
+
- **`callContext` JSON keys (the dialer DOES read these as fallback):** `city → personCity`,
|
|
282
|
+
`state → personState`, `website → companyWebsite`.
|
|
283
|
+
|
|
284
|
+
Rules that keep enrichment safe:
|
|
285
|
+
- **Fill-if-empty only** — ZoomInfo CSV import data always wins; enrichment never overwrites a
|
|
286
|
+
populated field (ZoomInfo is how outbound-list contacts get these fields in the first place — see
|
|
287
|
+
the parity audit below).
|
|
288
|
+
- **`callContext` merges** with any existing keys winning.
|
|
289
|
+
- **Enrichment ships as a separate absorbed `PUT`** so that an ACL-rejected field can never break
|
|
290
|
+
core lead capture — the enrichment write is best-effort; the lead-capture write is not.
|
|
291
|
+
- **Live-verified 2026-07-21:** contact 248's first real funnel pass set `title = INTERN` and
|
|
292
|
+
`doingBusinessAs = "Agilant Solutions, Inc."`, and the Vapi opener used them.
|
|
293
|
+
|
|
294
|
+
## Fast dial (skip the up-to-60s cron wait)
|
|
295
|
+
|
|
296
|
+
After a successful **Call Now** write, BDR optionally POSTs the worker's single-contact entry point
|
|
297
|
+
so the call fires in **seconds** instead of waiting up to 60s for the dialer cron
|
|
298
|
+
(`src/server/workerDialer.ts` NEW, `src/server/callbackService.ts`; test `test/fastDial.test.ts`).
|
|
299
|
+
|
|
300
|
+
- Calls the worker action `Ai/Bdr/Vapi/InitiateOutboundCall` with the contact's **primary
|
|
301
|
+
phone-row uuid**.
|
|
302
|
+
- **Gated by optional env `TOGA_WORKER_URL`** — unset ⇒ cron-only (no behavior change).
|
|
303
|
+
- **Strictly best-effort:** 5s timeout, all failures absorbed + logged; the cron remains the
|
|
304
|
+
fallback, and the dialer's own in-progress check prevents a double-dial.
|
|
305
|
+
- **Scheduled calls are deliberately excluded** — they wait for their slot.
|
|
306
|
+
- **Motivation:** the Success screen's "Connecting" animation is scripted theater (~5s) while the
|
|
307
|
+
cron could take 60s; fast dial makes the theater roughly honest.
|
|
308
|
+
- **SECURITY note / ticket:** the worker's direct-action endpoint accepts **unauthenticated** POSTs
|
|
309
|
+
(a pre-existing exposure — now load-bearing for a product feature). Flag for hardening; it is not
|
|
310
|
+
introduced by BDR but BDR now depends on it.
|
|
311
|
+
|
|
312
|
+
## Anonymous-visit guard (no lead reference ⇒ disabled submit)
|
|
313
|
+
|
|
314
|
+
With **no** lead reference (the page was opened without `?hsContactId=`), both submit screens now
|
|
315
|
+
**disable the button** and show a content-driven notice ("We couldn't find your contact reference…
|
|
316
|
+
open this page from the link in your email"), replacing the previous silent no-op that faked success
|
|
317
|
+
(`src/flow/screens/CallNow.tsx`, `src/flow/screens/Schedule.tsx`, `src/content/schema.ts`,
|
|
318
|
+
`src/content/default.ts`). This matches info's "Missing contact reference" behavior. Production links
|
|
319
|
+
come from HubSpot personalization tokens —
|
|
320
|
+
`?campaign=slug&hsContactId={{contact.hs_object_id}}&hsCampaignId=uuid` — or, when the campaign
|
|
321
|
+
bundle pins `togaCampaignUuid`, links need only the slug + contact token.
|
|
322
|
+
|
|
323
|
+
## Share button is a real mailto: (no server-send path)
|
|
324
|
+
|
|
325
|
+
The CallSummary "Send" button now builds a **`mailto:` URL** (recipient, subject, the displayed
|
|
326
|
+
summary as body) and opens the visitor's own mail client — a real share with **no server-side send
|
|
327
|
+
path**, which deliberately avoids the spam-relay surface of server-sending client-supplied text
|
|
328
|
+
(`src/lib/shareMailto.ts` NEW, `src/flow/screens/CallSummary.tsx`, `src/content/default.ts`; test
|
|
329
|
+
`test/shareMailto.test.ts`). Confirmation copy is honest ("Opening your email app").
|
|
330
|
+
|
|
331
|
+
- **LIMITATION:** the summary **content** is still the **simulated rotation** — the real
|
|
332
|
+
`c_callSummary` can't reach the frontend because the BDR API client gets **403 on
|
|
333
|
+
`ContactAttempts`**.
|
|
334
|
+
- **Pending backend ask (DB ACL config, no deploy):** a read-only grant for the BDR API client on
|
|
335
|
+
`ContactAttempts` fields `uuid` / `contactId` / `dtStarted` / `dtEnded` / `c_callSummary` /
|
|
336
|
+
`c_callOutcome`. **One grant unlocks three features:** the real summary in the card, the real
|
|
337
|
+
summary in Copy/Share, and a live call-status indicator.
|
|
338
|
+
|
|
339
|
+
## Parity audit — `info` (original) vs `bdr` (new) (§ closed 2026-07-21)
|
|
340
|
+
|
|
341
|
+
A full audit of the original `info` funnel vs the new BDR funnel closed the open "did we port
|
|
342
|
+
everything?" question in this doc:
|
|
343
|
+
|
|
344
|
+
- **All API work is ported** — routes, the identical 6-property HubSpot fetch, the Toga payloads.
|
|
345
|
+
- **The `c_*` / title / industry enrichment was populated by NEITHER repo** — it is **net-new**
|
|
346
|
+
work (see Contact enrichment above), **not** a port miss. Outbound-list contacts normally get
|
|
347
|
+
those fields from the **ZoomInfo CSV import**.
|
|
348
|
+
- **The `visit` route was intentionally dropped** per PLAN.md (dead 501, no caller).
|
|
349
|
+
- **The only real parity gap was phone prefill fallback** to the Toga contact's existing phone —
|
|
350
|
+
now shipped (see the phone-prefill fallback note above).
|
|
351
|
+
- **Dead env vars:** `TOGA_PUBLIC_TOKEN_URL` and `NEXT_PUBLIC_ENV` were declared but **never read**
|
|
352
|
+
even in info's own code — do not carry them into BDR.
|
|
353
|
+
- **BDR deploy env vars = 7 total:** the 5 server-side ones + `NEXT_PUBLIC_GA_MEASUREMENT_ID` +
|
|
354
|
+
(new 2026-07-21) **`TOGA_WORKER_URL`** (the fast-dial gate; unset ⇒ cron-only).
|
|
355
|
+
|
|
250
356
|
## Local dev setup (BDR)
|
|
251
357
|
|
|
252
358
|
BDR local dev needs **5 server-side env vars** in `BDR/.env.local` (gitignored, never
|
|
@@ -281,7 +387,9 @@ up — ClickUp is stale, confirm the real owner before assuming.)
|
|
|
281
387
|
- **5 server-side env vars are required** (same set as local dev): `TOGA_API_BASE_URL`,
|
|
282
388
|
`TOGA_CLIENT_ID`, `TOGA_CLIENT_API_UUID`, `TOGA_CLIENT_API_SECRET`, `HUBSPOT_ACCESS_TOKEN`.
|
|
283
389
|
**Where the values live (never the values):** the `info` repo's `.env.local` (production
|
|
284
|
-
credential set). Add them in the Amplify console environment config — never commit them.
|
|
390
|
+
credential set). Add them in the Amplify console environment config — never commit them. Two
|
|
391
|
+
optional vars beyond the 5: `NEXT_PUBLIC_GA_MEASUREMENT_ID` (GA4) and **`TOGA_WORKER_URL`** (the
|
|
392
|
+
fast-dial gate — unset ⇒ cron-only). See the parity audit for the full 7-var accounting.
|
|
285
393
|
- **Phase 8 WAF pairing.** `BDR/PLAN.md` Phase 8 specifies a **per-IP rate rule on the CloudFront
|
|
286
394
|
distribution / WAF** that pairs with the in-app token-bucket limiter (the app limiter is
|
|
287
395
|
defense-in-depth only; the edge WAF rule is the authoritative control — see the enumeration
|
|
@@ -416,6 +524,26 @@ up — ClickUp is stale, confirm the real owner before assuming.)
|
|
|
416
524
|
harness; pull it with `npx toga-ai`.
|
|
417
525
|
|
|
418
526
|
## Change history
|
|
527
|
+
- 2026-07-21 — Go-live continuation. **The full AI-BDR call loop was proven end-to-end in
|
|
528
|
+
production** (campaign 2, `26.05 - AI BDR - Ryan Nitti`): UI submit → funnel API → dialer → Vapi
|
|
529
|
+
call → webhook write-back (test method now in `../workflows/safe-call-loop-testing.md`). BUILT:
|
|
530
|
+
(1) **funnel-side contact enrichment** — HubSpot fetch extended with industry/city/state/website,
|
|
531
|
+
mapping title/company/industry to the Toga columns the dialer reads with NO fallback and
|
|
532
|
+
city/state/website into `callContext` (fill-if-empty, ZoomInfo wins, absorbed separate PUT;
|
|
533
|
+
live-verified on contact 248); (2) **phone-prefill fallback** to the Toga contact's existing
|
|
534
|
+
primary phone when HubSpot has none (the one real parity gap); (3) **fast dial** —
|
|
535
|
+
`src/server/workerDialer.ts` POSTs the worker's `InitiateOutboundCall` after a Call Now write so
|
|
536
|
+
the call fires in seconds not up to 60s, gated by optional `TOGA_WORKER_URL`, best-effort,
|
|
537
|
+
scheduled calls excluded (NOTE: the worker direct-action endpoint is unauthenticated — pre-existing
|
|
538
|
+
exposure now load-bearing); (4) **anonymous-visit guard** — no lead ref disables both submit
|
|
539
|
+
buttons with a content-driven notice instead of faking success; (5) **share button** made a real
|
|
540
|
+
`mailto:` (no server send path). Closed the **info-vs-BDR parity audit** (all API work ported; the
|
|
541
|
+
`c_*`/enrichment fields are net-new not a port miss; `visit` route intentionally dropped;
|
|
542
|
+
`TOGA_PUBLIC_TOKEN_URL`/`NEXT_PUBLIC_ENV` are dead; deploy = 7 env vars). LIMITATION recorded: the
|
|
543
|
+
share/card summary is still the simulated rotation because the BDR API client gets 403 on
|
|
544
|
+
`ContactAttempts` — pending a read-only ACL grant (one grant unlocks three features). Suite at
|
|
545
|
+
**131 green**. All of today's BDR work was **uncommitted at capture** (commits go to
|
|
546
|
+
`BDR-Phase-2` → `_dev-sandbox`). (tcox)
|
|
419
547
|
- 2026-07-20 — FIXED the phone-less-contact dialer bug on the funnel side: shipped a primary-phone
|
|
420
548
|
**self-heal** in `src/server/toga.ts` (commit `4ac579e`) — after any contact write carrying a
|
|
421
549
|
phone, if the echoed `primaryContactPhoneNumber` is null or its last-10 digits mismatch, find/create
|
|
@@ -6,11 +6,12 @@ project: AI-BDR
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-21
|
|
10
10
|
owners: [tcox]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_True/2026-07-20a - AiBdrTcoxTestCampaign.sql
|
|
13
13
|
- worker2/Worker/Ai/Bdr/Vapi.php
|
|
14
|
+
- worker2/Worker/Ai/Bdr/ZoomInfo.php
|
|
14
15
|
- worker2/Config/beta.ini
|
|
15
16
|
related:
|
|
16
17
|
- new-campaign-onboarding.md
|
|
@@ -24,9 +25,16 @@ How to prove the AI-BDR **dialer → Vapi → end-of-call webhook** loop end-to-
|
|
|
24
25
|
dialing real prospects**. The production dialer cron (`Core.CronJobs` "AI BDR",
|
|
25
26
|
`ProcessAllActiveCampaigns`, schedule `* * * * *`) fires **every minute**, so any campaign that
|
|
26
27
|
becomes eligible starts dialing live prospects **within 60 seconds** — a test must be built so
|
|
27
|
-
that the *only* contact that can ever be dialed is a deliberate test contact.
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
that the *only* contact that can ever be dialed is a deliberate test contact. There are **two safe
|
|
29
|
+
vehicles**, both proven live:
|
|
30
|
+
|
|
31
|
+
- **Approach A — reuse an EXISTING campaign, DNC-zero everyone first (Rule 2).** The go-live
|
|
32
|
+
approach; use it when you must prove the *real* campaign works end-to-end.
|
|
33
|
+
- **Approach B — an isolated one-contact test campaign with no call windows (Rule 2b).** Use it
|
|
34
|
+
when you only need to rehearse the loop mechanics with zero real prospects linked.
|
|
35
|
+
|
|
36
|
+
Eligibility mechanics that make both work are in
|
|
37
|
+
[worker2 vapi-webhook-handler.md](../../worker2/features/vapi-webhook-handler.md).
|
|
30
38
|
|
|
31
39
|
## Rule 1 — NEVER revive an ended campaign that has linked prospects
|
|
32
40
|
|
|
@@ -39,7 +47,47 @@ dialed real prospects in one cron tick.) Likewise, do **not** blanket-flip `isOk
|
|
|
39
47
|
real list: it is two mass writes plus a restore burden, and it silently un-blocks any contact
|
|
40
48
|
that was intentionally set `isOkayToCall=0`.
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
**The safe exception — flip everyone to `isOkayToCall = 0` FIRST, then revive.** Reviving an ended
|
|
51
|
+
campaign is only dangerous because its contacts are dialable. If you first mass-flip
|
|
52
|
+
`isOkayToCall = 0` for **every** contact on the campaign, then extending `dateEnd` re-activates a
|
|
53
|
+
campaign on which **no one is dialable** — and the funnel's Call Now submit re-arms only the single
|
|
54
|
+
tester (`isOkayToCall = 1` + `dtNextContactRequested`). This is exactly Approach A (Rule 2). The
|
|
55
|
+
direction matters: flipping to `0` is the safe direction; the warning above is about flipping to
|
|
56
|
+
`1`.
|
|
57
|
+
|
|
58
|
+
## Rule 2 (Approach A, PREFERRED for go-live) — reuse an existing campaign, DNC-zero everyone first
|
|
59
|
+
|
|
60
|
+
This is the approach used to prove campaign 2 (`26.05 - AI BDR - Ryan Nitti`) live on 2026-07-21,
|
|
61
|
+
and it **supersedes** the one-contact test campaign (Rule 2b) whenever the goal is to prove the
|
|
62
|
+
**real** campaign works, not just the loop mechanics. It is "Rohan's existing-campaign pattern":
|
|
63
|
+
|
|
64
|
+
1. **Record the pre-existing DNC set FIRST (non-negotiable).** Before touching anything, list every
|
|
65
|
+
contact on the campaign that is *already* `isOkayToCall = 0` for a real reason. For campaign 2
|
|
66
|
+
those are contact ids **120, 132, 134, 146, 155** (all with DNC evidence in `ContactAttempts`).
|
|
67
|
+
The restore statement at the end **must exclude these ids forever** — re-enabling a genuine DNC
|
|
68
|
+
is a compliance incident, not a test-cleanup slip.
|
|
69
|
+
2. **Mass-flip `Contacts.isOkayToCall = 0` for ALL contacts on the campaign** (including the
|
|
70
|
+
tester). Now no one on the campaign is dialable.
|
|
71
|
+
3. **Extend `Campaigns.dateEnd`** to revive the campaign. Because step 2 made everyone
|
|
72
|
+
non-dialable, revival dials **no one**.
|
|
73
|
+
4. **The tester arms themselves through the funnel.** The UI "Call Now" submit sets
|
|
74
|
+
`isOkayToCall = 1` + `dtNextContactRequested` on the tester's contact — re-arming **only** the
|
|
75
|
+
tester. That single requested contact is the only thing that can be dialed.
|
|
76
|
+
5. **Restore** at the end: set `isOkayToCall = 1` back for the contacts that were dialable before
|
|
77
|
+
the test, **excluding the pre-existing DNC ids from step 1** (and excluding the tester if the
|
|
78
|
+
tester should not be re-armed). No snapshot table is needed **provided** the step-1 DNC ids were
|
|
79
|
+
recorded — that list is the whole safety margin.
|
|
80
|
+
|
|
81
|
+
**Do NOT create the campaign via a dbchanges2 migration.** That approach was tried and **withdrawn**
|
|
82
|
+
— the migration missed `Campaigns.c_fromPhoneNumberIdentifier`, which `buildVapiPayload` (worker2
|
|
83
|
+
dialer) **hard-requires**, so a campaign built that way cannot place a call. The **sanctioned
|
|
84
|
+
programmatic campaign creator is `_Worker_Ai_Bdr_ZoomInfo::SetupCampaign`**
|
|
85
|
+
(`worker2/Worker/Ai/Bdr/ZoomInfo.php`): it creates the campaign type + assistant + the campaign
|
|
86
|
+
itself (including all `c_*` fields such as `c_fromPhoneNumberIdentifier`) + call windows + booking
|
|
87
|
+
windows + cadences + meet-with contacts + exceptions in one consistent pass. Use it, not raw SQL,
|
|
88
|
+
to stand up a new campaign.
|
|
89
|
+
|
|
90
|
+
## Rule 2b (Approach B) — isolated one-contact test campaign with NO call windows
|
|
43
91
|
|
|
44
92
|
The safe posture exploits the eligibility split (see vapi-webhook-handler.md): the normal
|
|
45
93
|
(non-requested) dial path requires a `CampaignCallWindows` row for the current day/time, but the
|
|
@@ -100,6 +148,17 @@ Sequence the cleanup into a later, separately-executed window. (See dbchanges2 a
|
|
|
100
148
|
`YYYY-MM-DD<letter>` ordering contract.)
|
|
101
149
|
|
|
102
150
|
## Change history
|
|
151
|
+
- 2026-07-21 — Added **Approach A** (now preferred for go-live), used to prove campaign 2
|
|
152
|
+
(`26.05 - AI BDR - Ryan Nitti`) live end-to-end: reuse an existing campaign by recording its
|
|
153
|
+
pre-existing DNC set first (campaign 2 = contact ids 120/132/134/146/155, which the restore must
|
|
154
|
+
exclude forever), mass-flipping `isOkayToCall = 0` on ALL its contacts, then extending `dateEnd`
|
|
155
|
+
to revive a campaign on which no one is dialable, and letting the funnel Call Now re-arm only the
|
|
156
|
+
tester. Reframed Rule 1's "never blanket-flip isOkayToCall" as direction-specific (flipping to `0`
|
|
157
|
+
first is the safe exception that makes revival safe). Recorded that the **dbchanges2 campaign-
|
|
158
|
+
creation migration was withdrawn** (it missed `c_fromPhoneNumberIdentifier`, which
|
|
159
|
+
`buildVapiPayload` hard-requires) and that **`_Worker_Ai_Bdr_ZoomInfo::SetupCampaign` is the
|
|
160
|
+
sanctioned programmatic campaign creator**. The old one-contact test campaign is retained as
|
|
161
|
+
Approach B (Rule 2b). (tcox)
|
|
103
162
|
- 2026-07-20 — Initial runbook. Captured the safe AI-BDR call-loop test procedure: never revive an
|
|
104
163
|
ended campaign with linked prospects (every-minute cron dials within 60s); use an isolated
|
|
105
164
|
one-contact test campaign with NO `CampaignCallWindows` (only requested `dtNextContactRequested`
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [TOGa Supply (toga2-supply) Architecture](architecture.md) | `toga2-supply` is the **React + Vite frontend** for TOGa Supply — warehouse fulfillment tooling (shipment selection, fulfill & ship against carrier APIs, NetSui | toga2-supply/src/api/toga.ts, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx |
|
|
6
|
-
| [Fulfill & Ship](features/fulfill-and-ship.md) | Fulfill & Ship lets a warehouse user select sales-order line items, enter serials, pick a carrier/method, and in one action: create the Item Fulfillment records | toga2-supply/src/components/ui/Tables/BasicTable/BasicTable.tsx, toga2-supply/src/pages/ShipmentItems/view/forms/ShipmentItemsTable.tsx, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/components/ui/BaseInput/UnitSelect.tsx, toga2-supply/src/pages/EditShipment/view/modals/SerialNumbersModal.tsx, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/pages/EditShipment/view/components/SelectedShipmentItemsTable.tsx, toga2-supply/src/pages/EditShipment/view/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/DUMMYUPDATESHIPMENTFIELDS.json, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/tailwind.config.cjs, toga2-supply/src/pages/EditShipment/view/modals/ReturnShippingModal.tsx, toga2-supply/src/styles/index.scss, toga2-supply/src/components/ui/BaseInput/BaseInput.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/EditShipment/viewModel/signatureTypes.ts, toga2-supply/src/pages/EditShipment/view/modals/SelectReturnAddressModal.tsx, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/formatShipmentData.ts, toga2-supply/src/pages/EditShipment/types.ts, toga2-supply/src/pages/Shipments/view/ShipmentsPage.tsx, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx, toga2-supply/src/pages/Shipments/api/ShipmentsApi.ts, toga2-supply/src/pages/Shipments/types.ts, toga2-supply/src/pages/FulfilledShipments/view/FulfilledShipmentsPage.tsx, toga2-supply/src/components/ui/CardTable/CardTable.tsx, toga2-supply/src/components/ui/CardTable/types.ts, toga2-supply/src/assets/pen-line.svg, _underscore/Model/Client/ItemFulfillment.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Component/Library/Carriers/Ups/Ups.php |
|
|
6
|
+
| [Fulfill & Ship](features/fulfill-and-ship.md) | Fulfill & Ship lets a warehouse user select sales-order line items, enter serials, pick a carrier/method, and in one action: create the Item Fulfillment records | toga2-supply/src/components/ui/Tables/BasicTable/BasicTable.tsx, toga2-supply/src/pages/ShipmentItems/view/forms/ShipmentItemsTable.tsx, toga2-supply/src/pages/ShipmentItems/api/ShipmentItemsApi.ts, toga2-supply/src/pages/ShipmentItems/types.ts, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/RETURNLABELFIELDS.json, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/components/ui/BaseInput/UnitSelect.tsx, toga2-supply/src/pages/EditShipment/view/modals/SerialNumbersModal.tsx, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/pages/EditShipment/view/components/SelectedShipmentItemsTable.tsx, toga2-supply/src/pages/EditShipment/view/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/DUMMYUPDATESHIPMENTFIELDS.json, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/tailwind.config.cjs, toga2-supply/src/pages/EditShipment/view/modals/ReturnShippingModal.tsx, toga2-supply/src/styles/index.scss, toga2-supply/src/components/ui/BaseInput/BaseInput.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/EditShipment/viewModel/signatureTypes.ts, toga2-supply/src/pages/EditShipment/view/modals/SelectReturnAddressModal.tsx, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/formatShipmentData.ts, toga2-supply/src/pages/EditShipment/types.ts, toga2-supply/src/pages/Shipments/view/ShipmentsPage.tsx, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx, toga2-supply/src/pages/Shipments/api/ShipmentsApi.ts, toga2-supply/src/pages/Shipments/types.ts, toga2-supply/src/pages/FulfilledShipments/view/FulfilledShipmentsPage.tsx, toga2-supply/src/components/ui/CardTable/CardTable.tsx, toga2-supply/src/components/ui/CardTable/types.ts, toga2-supply/src/assets/pen-line.svg, _underscore/Model/Client/ItemFulfillment.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Component/Library/Carriers/Ups/Ups.php |
|
|
7
7
|
| [AWS Amplify Build & Deploy (non-prod environments)](workflows/amplify-build-and-deploy.md) | How `toga2-supply` (React + Vite) builds and deploys on **AWS Amplify**. | toga2-supply/amplify.yml, toga2-supply/.gitattributes, toga2-supply/.github/workflows/sync-stage-environments.yml, toga2-supply/.env.qc-security |
|
|
@@ -11,6 +11,9 @@ owners: [mhammontree]
|
|
|
11
11
|
files:
|
|
12
12
|
- toga2-supply/src/components/ui/Tables/BasicTable/BasicTable.tsx
|
|
13
13
|
- toga2-supply/src/pages/ShipmentItems/view/forms/ShipmentItemsTable.tsx
|
|
14
|
+
- toga2-supply/src/pages/ShipmentItems/api/ShipmentItemsApi.ts
|
|
15
|
+
- toga2-supply/src/pages/ShipmentItems/types.ts
|
|
16
|
+
- toga2-supply/src/pages/EditShipment/viewModel/FIELDS/RETURNLABELFIELDS.json
|
|
14
17
|
- toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx
|
|
15
18
|
- toga2-supply/src/components/ui/BaseInput/UnitSelect.tsx
|
|
16
19
|
- toga2-supply/src/pages/EditShipment/view/modals/SerialNumbersModal.tsx
|
|
@@ -309,6 +312,33 @@ the call 403s and returns no label.
|
|
|
309
312
|
|
|
310
313
|
## Gotchas / known issues
|
|
311
314
|
|
|
315
|
+
- **⚠ The item-image tables hardcoded the placeholder and never requested the image.** The Select
|
|
316
|
+
Items (`ShipmentItemsTable`), Update Info, and Add-Serial (`SelectedShipmentItemsTable`) tables
|
|
317
|
+
rendered `imageUrl={placeholderImage}` as a **constant**, so a real item image could never appear.
|
|
318
|
+
The item image is exposed via **two computed SQL fields on the `_underscore` Item model** (referenced,
|
|
319
|
+
not modified this commit) — request **both** in the V2 field selectors and render
|
|
320
|
+
**`item._imageUrl || item._thumbnailImageUrl || placeholder`**:
|
|
321
|
+
- **`_imageUrl`** — resolves `Items.imageId` → `Images.sourceUrl` (the single **primary** image).
|
|
322
|
+
- **`_thumbnailImageUrl`** — resolves from **`ItemImages`** (per-item thumbnail/low/medium/high
|
|
323
|
+
resolutions) via `thumbnailImageId`, ordered `isDefault DESC`, **excluding** `sourceUrl LIKE
|
|
324
|
+
'%Manufacturer%'`.
|
|
325
|
+
|
|
326
|
+
Two distinct DB paths back an item image — `Items.imageId → Images` (one primary) and the `ItemImages`
|
|
327
|
+
table (multi-resolution, `isDefault` flag) — and in both cases the URL lives in `Images.sourceUrl`.
|
|
328
|
+
The `_imageUrl || _thumbnailImageUrl || placeholder` fallback covers imageId-only, ItemImages-only, or
|
|
329
|
+
neither. Wiring an image anywhere in the tool: request **both** computed fields (add to the API field
|
|
330
|
+
selectors + response types), don't hardcode the placeholder. Verified end-to-end against a client DB
|
|
331
|
+
with rolled-back test data. Files: `ShipmentItems/api/ShipmentItemsApi.ts`, `ShipmentItemsTable.tsx`,
|
|
332
|
+
`ShipmentItems/types.ts`, `EditShipment/api/UpdateShipmentApi.ts`, `EditShipmentForm.tsx`,
|
|
333
|
+
`SelectedShipmentItemsTable.tsx`, `EditShipment/types.ts`.
|
|
334
|
+
- **⚠ The shared `ExpandableCellContent` truncates by a FIXED CHARACTER COUNT, not by width — so it
|
|
335
|
+
can't be responsive.** `ExpandableCellContent` cuts a cell at `columnDataLength` characters
|
|
336
|
+
(e.g. 55), so a column truncates early regardless of available width and **does not react to
|
|
337
|
+
window/page resize**. For width-responsive truncation, render the cell as a plain
|
|
338
|
+
`<div className="truncate max-w-[NNvw]" title={full}>` — CSS overflow-ellipsis with a
|
|
339
|
+
viewport-relative max-width so the ellipsis point tracks the page size, and the native `title`
|
|
340
|
+
shows the full text on hover. (Applied to the Description column in `ShipmentItemsTable.tsx` /
|
|
341
|
+
`SelectedShipmentItemsTable.tsx`.)
|
|
312
342
|
- **⚠ A portaled react-select menu loses the control's inherited font size.** `BaseInput`'s
|
|
313
343
|
react-select menus now portal to `<body>` (`menuPortalTarget={document.body}` +
|
|
314
344
|
`styles.menuPortal` `zIndex 9999`, on both single- and multi-select) so the menu can't be
|
|
@@ -588,6 +618,18 @@ not the base `_Model_Client_ItemFulfillment`. Tested with GroWrk; UPS support wa
|
|
|
588
618
|
for Compass and is not yet in prod.
|
|
589
619
|
|
|
590
620
|
## Change history
|
|
621
|
+
- 2026-07-21 — TRUE-79191 (commit `dc900ea4e`, toga2-supply item-image + table polish): fixed the
|
|
622
|
+
**item image never rendering** — the Select Items / Update Info / Add-Serial tables hardcoded
|
|
623
|
+
`imageUrl={placeholderImage}` and never requested the image; now request the two `_underscore` Item
|
|
624
|
+
computed fields **`_imageUrl`** (`Items.imageId → Images.sourceUrl`) and **`_thumbnailImageUrl`**
|
|
625
|
+
(from `ItemImages` via `thumbnailImageId`, `isDefault DESC`, excluding `%Manufacturer%`) and render
|
|
626
|
+
`item._imageUrl || item._thumbnailImageUrl || placeholder` — documented the two-path Images/ItemImages
|
|
627
|
+
data model as a gotcha. Documented the **`ExpandableCellContent` fixed-char-count vs width** gotcha
|
|
628
|
+
(truncates at `columnDataLength` chars, not responsive; use `truncate max-w-[NNvw]` + native `title`
|
|
629
|
+
for width-responsive truncation, applied to the Description column). Added Reference 1/2 placeholder
|
|
630
|
+
hint text to the Edit Return Label modal (`RETURNLABELFIELDS.json`) to match Update Info
|
|
631
|
+
("e.g. NetSuite Sales Order #" / "…Purchase Order #") — cosmetic, Ref 1/2 semantics already recorded.
|
|
632
|
+
(mhammontree)
|
|
591
633
|
- 2026-07-21 — TRUE-79191 (toga2-supply UI fixes + Figma polish): documented three durable, reusable
|
|
592
634
|
gotchas — (1) **`BasicTable` header/value misalignment** (daisyUI base `:where(th,td)` ~16px padding;
|
|
593
635
|
always pass a horizontal `headerCellPadding` matching `cellPadding`, box-shadow-only seam for sticky
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-21
|
|
10
10
|
owners: [snaredla, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Vapi.php
|
|
@@ -218,6 +218,28 @@ character — a 37-char value) **passes** the readiness gate and the campaign is
|
|
|
218
218
|
Vapi `POST /call/phone` **400s** at call time. Validate the assistant identifier's shape before
|
|
219
219
|
assigning an assistant to any campaign.
|
|
220
220
|
|
|
221
|
+
### Gotcha (HIGH — call quality) — a spoken callback time with a SPELLED-OUT number is silently dropped
|
|
222
|
+
|
|
223
|
+
`actionScheduleCallback` (`Worker/Vapi.php` ~L387–431) parses the assistant's
|
|
224
|
+
`structured.callbackTime` through `convertDateTime` (~L609–635), which relies on PHP's date
|
|
225
|
+
parser. PHP handles relative phrasings like `"10 minutes"` and `"tomorrow at 3pm"` — but **NOT
|
|
226
|
+
spelled-out numbers**. When the model emits `c_callbackTime = "in ten minutes"` (natural language,
|
|
227
|
+
number as a word), `convertDateTime` returns **null**, `actionScheduleCallback` `error_log`s
|
|
228
|
+
`"Cannot parse callbackTime"` and **aborts** — `dtNextContactRequested` is never set and `callType`
|
|
229
|
+
is never flipped to `CALLBACK`, so the promised callback **never happens** and nothing surfaces the
|
|
230
|
+
failure. Reproduced live 2026-07-21 (attempt 183, contact 248). Two recommended fixes (do both):
|
|
231
|
+
(a) constrain the Vapi structured-output `callbackTime` schema to ISO 8601 / numeric-relative so the
|
|
232
|
+
model can't spell the number out (see `../../ai-bdr/features/vapi-integration.md`); (b) defensively
|
|
233
|
+
normalize spelled-out numbers (`one`..`sixty`) in `convertDateTime` before parsing.
|
|
234
|
+
|
|
235
|
+
### Gotcha (SECURITY) — the Vapi API bearer token is hardcoded in source
|
|
236
|
+
|
|
237
|
+
The outbound dialer `Worker/Ai/Bdr/Vapi.php` (**line 6**) declares the Vapi API bearer token as a
|
|
238
|
+
`const` in source — so it is **committed to git**. Treat any committed secret as **compromised and
|
|
239
|
+
rotate it**, and move it into the `[vapi]` section of the worker2 `Config/*.ini` files alongside the
|
|
240
|
+
other Vapi secrets (the pattern the rest of the integration already uses). Config, never source, is
|
|
241
|
+
the home for this credential. (Token value is deliberately not recorded here — location only.)
|
|
242
|
+
|
|
221
243
|
## Data model
|
|
222
244
|
|
|
223
245
|
Writes to `ContactAttempts` (via Toga 2.0 API) — `dtStarted`, `dtEnded`, `transcript`,
|
|
@@ -245,6 +267,17 @@ status. See the `ai-bdr` skill / `_Model_True_ContactAttempt` for the full field
|
|
|
245
267
|
- [VAPI Integration — assistants, tools, structured output](../../ai-bdr/features/vapi-integration.md)
|
|
246
268
|
|
|
247
269
|
## Change history
|
|
270
|
+
- 2026-07-21 — Full call loop **proven end-to-end in production** on the real campaign 2
|
|
271
|
+
(`26.05 - AI BDR - Ryan Nitti`): UI submit → funnel API → dialer → Vapi call → webhook write-back.
|
|
272
|
+
Live-verified the documented behavior: the `SCHEDULE_CALLBACK` → `c_callOutcome=CALLBACK_SCHEDULED`
|
|
273
|
+
/ `c_actionToTake=SCHEDULE_CALLBACK` mapping, plus `c_prospectSentiment` + `c_qualificationScore`,
|
|
274
|
+
are all written by the webhook; and the CST timestamp conversion is correct (a scheduled call for
|
|
275
|
+
an 11:30 slot fired at 11:30:50 — `formatCstDateTime` verified live). Added two gotchas: (1) HIGH —
|
|
276
|
+
a spoken `callbackTime` with a **spelled-out** number (`"in ten minutes"`) makes `convertDateTime`
|
|
277
|
+
return null, so `actionScheduleCallback` aborts and the callback silently never happens (repro
|
|
278
|
+
attempt 183 / contact 248; fix both the Vapi schema and `convertDateTime`); (2) SECURITY — the Vapi
|
|
279
|
+
API bearer token is hardcoded as a `const` in `Worker/Ai/Bdr/Vapi.php` line 6 (committed to git) —
|
|
280
|
+
rotate it and move it to the `[vapi]` config section. (tcox)
|
|
248
281
|
- 2026-07-20 — Source-verified corrections/extensions to the dialer eligibility rules: the campaign
|
|
249
282
|
gate is split (`getActiveCampaignsForDialing` L177–193 = isActive+assistant, NO date; date range +
|
|
250
283
|
non-empty `assistantIdentifier` in `validateCampaignReady` L759–789); `Campaigns_Contacts.status
|
package/package.json
CHANGED