toga-ai 1.0.395 → 1.0.397
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/worker2/features/vapi-webhook-handler.md +34 -1
- package/knowledge/sessions/2026-07-21-bdr-vapi-golive-tcox.md +73 -0
- 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`
|
|
@@ -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
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: session
|
|
3
|
+
slug: bdr-vapi-golive
|
|
4
|
+
title: BDR VAPI go-live day 2 — loop verified live, four funnel features shipped
|
|
5
|
+
author: tcox
|
|
6
|
+
repos: [bdr, ai-bdr, worker2]
|
|
7
|
+
framework: "2.0"
|
|
8
|
+
client: shared
|
|
9
|
+
status: active
|
|
10
|
+
created: 2026-07-21
|
|
11
|
+
updated: 2026-07-21
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Session: bdr-vapi-golive
|
|
15
|
+
**Date:** 2026-07-21 (continues 2026-07-20-bdr-vapi-golive-tcox)
|
|
16
|
+
**Project/Repo:** bdr + worker2 (2.0)
|
|
17
|
+
**Task:** Verify the AI-BDR call loop end-to-end live on the real campaign and finish the BDR funnel for go-live (enrichment, fast dial, anonymous guard, real share).
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What WORKED
|
|
22
|
+
- **Full call loop verified live on campaign 2** using Rohan's existing-campaign approach: flip ALL campaign-2 contacts `isOkayToCall=0` (tester included), extend `Campaigns.dateEnd`, then the tester's UI Call Now submit re-arms ONLY them (`isOkayToCall=1` + `dtNextContactRequested`). Call arrived from the healthcare number +1 (331) 244-5958 (campaign 2's `c_fromPhoneNumberIdentifier` maps there); webhook write-back confirmed (dtEnded set, dtNext cleared).
|
|
23
|
+
- **Scheduled-call path + CST conversion verified**: an 11:30 slot dialed at 11:30:50 (attempt 183).
|
|
24
|
+
- **Outcome mapping verified**: "call me back..." conversation wrote `c_callOutcome=CALLBACK_SCHEDULED`, `c_actionToTake=SCHEDULE_CALLBACK`, sentiment, qualification score.
|
|
25
|
+
- **Enrichment shipped and live-proven** (bdr: src/server/hubspot.ts, toga.ts, leadSink.ts, app/page.tsx): HubSpot jobtitle/company/industry → Contacts.title/doingBusinessAs/industry (the three no-fallback dialer fields), city/state/website → callContext keys. Contact 248 got title=INTERN, doingBusinessAs="Agilant Solutions, Inc." on its first funnel pass and the Vapi opener used them. Committed by tcox this morning (with the phone-prefill fallback).
|
|
26
|
+
- **Fast dial built** (src/server/workerDialer.ts + callbackService.ts, env TOGA_WORKER_URL): Call Now triggers the worker's `Ai/Bdr/Vapi/InitiateOutboundCall` with the primary phone-row uuid — seconds instead of the 0-60s cron wait; best-effort with cron fallback; scheduled calls excluded. NOT yet live-tested.
|
|
27
|
+
- **Anonymous-visit guard built** (CallNow.tsx, Schedule.tsx, content schema/default): no lead ref → submit disabled + notice, matching info's "Missing contact reference" (replaces silent fake success).
|
|
28
|
+
- **Share button made real** (src/lib/shareMailto.ts, CallSummary.tsx): Send opens the visitor's mail client via mailto: with recipient/subject/summary body. BDR suite 131 tests green, tsc + eslint clean.
|
|
29
|
+
- **Parity audit (info → BDR)**: everything ported; enrichment was net-new in BOTH repos; visit route intentionally dropped; TOGA_PUBLIC_TOKEN_URL and NEXT_PUBLIC_ENV are dead even in info.
|
|
30
|
+
- **Vapi API diagnostics work** with the bearer const at worker2/Worker/Ai/Bdr/Vapi.php:6 (GET /call/{id} endedReason, GET /assistant/{id} config).
|
|
31
|
+
- /capture published 4 doc updates (web-funnel-app, safe-call-loop-testing, vapi-webhook-handler, vapi-integration).
|
|
32
|
+
|
|
33
|
+
## What did NOT work — DO NOT RETRY THESE
|
|
34
|
+
- **Answering the test call silently** → 8 seconds of mutual silence, Vapi `endedReason=customer-ended-call`, empty transcript. The healthcare assistant uses `firstMessageMode=assistant-waits-for-user` (deliberate: first-phoneme timing for voicemail/iPhone screening). **Testers must say "Hello?" first.**
|
|
35
|
+
- **Telling the assistant "call me back in ten minutes"** → structured output stored `c_callbackTime="in ten minutes"` (spelled-out number); worker2 `convertDateTime` (Worker/Vapi.php ~L609) returns null (PHP parses "10 minutes" but not "ten minutes") → `actionScheduleCallback` aborts after error_log "Cannot parse callbackTime" → `dtNextContactRequested` never set, **the promised callback silently never happens**. Open HIGH bug; until fixed, spoken-word callback times are dropped.
|
|
36
|
+
- **The dbchanges2 test-campaign migration** (withdrawn, PR to close unmerged): its INSERT...SELECT missed `c_fromPhoneNumberIdentifier`, which `buildVapiPayload` hard-requires (throws) — the SQL-created campaign would have failed at call time. Raw SQL campaign creation is the wrong vehicle; `_Worker_Ai_Bdr_ZoomInfo::SetupCampaign` is the sanctioned creator.
|
|
37
|
+
- **Vapi API auth with worker2 Config [vapi] api_secret** → `unauthorized`. The working credential is the const at Worker/Ai/Bdr/Vapi.php:6 (itself a security finding — rotate + move to config; value not recorded here).
|
|
38
|
+
- **BDR API client reading /contact-attempts** → 403 (EZ-1). Blocks real call summaries + live call status until the backend grants read ACL.
|
|
39
|
+
|
|
40
|
+
## Not tried yet (candidates for next session)
|
|
41
|
+
- **Fast-dial live test** — restart the dev server (TOGA_WORKER_URL loads at startup), submit Call Now, phone should ring in seconds. First live use of the worker's direct-action endpoint as a product feature.
|
|
42
|
+
- **Self-heal changed-number live test** — Call Now with a different owned number; dialer should ring the new number.
|
|
43
|
+
- **Real summary + live call status** — blocked on the ContactAttempts ACL grant (read-only: uuid, contactId, dtStarted, dtEnded, c_callSummary, c_callOutcome for BDR API client ead1d6ad-04a5-41f9-9a87-669d66ad49a2). One grant unlocks: real summary card content, real Copy/Share body, live status timer.
|
|
44
|
+
- **Callback-time parse fix** (worker2 + Vapi schema): constrain schema callbackTime format AND normalize spelled-out numbers in convertDateTime.
|
|
45
|
+
- **Booking path live test** (Cal.com — campaign 2's eventIdentifier is real).
|
|
46
|
+
- **Restore SQL** = business go-live switch for the ~70 prospects: `UPDATE Contacts c INNER JOIN Campaigns_Contacts cc ON cc.contactId=c.id SET c.isOkayToCall=1 WHERE cc.campaignId=2 AND c.id NOT IN (120,132,134,146,155);` — **never restore those 5 ids (pre-existing DNC)**.
|
|
47
|
+
|
|
48
|
+
## Current file state
|
|
49
|
+
| File | Status | Notes |
|
|
50
|
+
|------|--------|-------|
|
|
51
|
+
| BDR enrichment + prefill (hubspot/toga/leadSink/page + tests) | committed by tcox (BDR-Phase-2), merge state to _dev-sandbox unverified | live-proven on contact 248 |
|
|
52
|
+
| BDR fast-dial + anon guard + share (workerDialer, callbackService, CallNow, Schedule, schema, default, shareMailto, CallSummary, .env.example + 3 test files) | UNCOMMITTED in C:\WWW\BDR | 131 tests green; suggested msg: "feat: fast dial, anonymous submit guard, real share via mailto" |
|
|
53
|
+
| BDR/.env.local | TOGA_WORKER_URL added (local only) | dev server restart needed to load |
|
|
54
|
+
| dbchanges2 feature/ai-bdr-test-campaign (e99a367) | pushed; PR must be CLOSED UNMERGED | superseded + buggy (see above) |
|
|
55
|
+
| Prod Client_True data | campaign 2 dateEnd=2026-07-28; all campaign-2 contacts isOkayToCall=0 EXCEPT 248 | restore = go-live switch; exclude ids 120/132/134/146/155 |
|
|
56
|
+
| Team knowledge | 4 docs updated + pushed today | web-funnel-app, safe-call-loop-testing, vapi-webhook-handler, vapi-integration |
|
|
57
|
+
|
|
58
|
+
## Decisions made
|
|
59
|
+
- **Existing campaign + flip-everyone over a new test campaign** (Rohan): zero campaign-config risk, the UI submit is the only re-arm, and the real Cal.com event stays testable. Rejected: SetupCampaign-created test campaign (viable, kept as Approach B in the runbook), raw SQL (buggy + wrong vehicle).
|
|
60
|
+
- **Share via mailto:, not server-side send**: server-sending client-supplied text through TOGA infra is a spam relay; the visitor's own mail client is honest and zero-backend. Real summary content deferred to the ACL grant.
|
|
61
|
+
- **Fast dial is best-effort**: absorbed failures + cron fallback + in-progress dedupe; env-gated (TOGA_WORKER_URL) so deploys without the var behave exactly as before.
|
|
62
|
+
- **Anonymous submits blocked with notice** (matches info) per tcox's directive that anything diverging from info needs correcting.
|
|
63
|
+
|
|
64
|
+
## Blockers
|
|
65
|
+
- **Amplify app still doesn't exist** (ClickUp 868kdf4tj, Jeff) — 7 env vars now: the 5 server-side + NEXT_PUBLIC_GA_MEASUREMENT_ID + TOGA_WORKER_URL.
|
|
66
|
+
- **ContactAttempts ACL grant** (backend, DB config only) — gates real summaries + live status.
|
|
67
|
+
- **Business decision on the restore SQL** (resumes cold-calling ~70 prospects).
|
|
68
|
+
|
|
69
|
+
## Exact next step
|
|
70
|
+
> In `C:\WWW\BDR`: commit the uncommitted batch ("feat: fast dial, anonymous submit guard, real share via mailto"), push to BDR-Phase-2, merge to _dev-sandbox. Then restart the dev server (`npm run dev`) and submit Call Now — the phone should ring within seconds (fast-dial's first live test). If it takes ~a minute instead, check the dev console for a `[workerDialer]` warning: that means the worker's direct-action endpoint rejected the trigger and the cron fallback placed the call.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
_Saved by /session-save on 2026-07-21_
|
package/package.json
CHANGED