toga-ai 1.0.358 → 1.0.359
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -6,6 +6,6 @@
|
|
|
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/components/GoogleAnalytics.tsx, bdr/src/components/RichText.tsx, bdr/src/content, bdr/src/components, bdr/src/flow, bdr/src/server, bdr/src/app/api, bdr/test, 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/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/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/app/api, bdr/test, 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 |
|
|
@@ -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-16
|
|
10
|
+
owners: [akhokhani, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- ai-bdr/docs/client-onboarding-sop.md
|
|
13
13
|
- ai-bdr/docs/vapi-firstmessage-timing-fix.md
|
|
@@ -208,7 +208,11 @@ gracefully handles `"Information not available"`.
|
|
|
208
208
|
**Now committed in `worker2`** as `_Worker_Vapi` (`worker2/Worker/Vapi.php`,
|
|
209
209
|
the webhook) alongside the outbound dialer `_Worker_Ai_Bdr_Vapi`
|
|
210
210
|
(`worker2/Worker/Ai/Bdr/Vapi.php`). Full detail:
|
|
211
|
-
**`2.0/apps/worker2/features/vapi-webhook-handler.md
|
|
211
|
+
**`2.0/apps/worker2/features/vapi-webhook-handler.md`** — which now also documents the
|
|
212
|
+
**dialer eligibility rules** (`getEligibleContactsForVapi()`: the exact ALL-of criteria that
|
|
213
|
+
decide whether a contact is dialed, the `dtNextContactRequested <= NOW()` call-window/cadence
|
|
214
|
+
bypass used by the website "call now/scheduled" path, and the phone-less-contact gotcha where a
|
|
215
|
+
missing `primaryContactPhoneNumberId` silently excludes a contact).
|
|
212
216
|
|
|
213
217
|
> **Corrections confirmed against the live `_Worker_Vapi` code (2026-06-24):**
|
|
214
218
|
> 1. **Structured output is in `artifact.structuredOutputs`** (keyed by a random
|
|
@@ -263,4 +267,7 @@ feature doc in that repo's `features/` and link it from this file's
|
|
|
263
267
|
- **`attemptNumber` is 1-indexed**, not 0-indexed. First call = 1.
|
|
264
268
|
|
|
265
269
|
## Change history
|
|
270
|
+
- 2026-07-16 — Cross-referenced the newly documented dialer eligibility rules
|
|
271
|
+
(`getEligibleContactsForVapi`) in `worker2/features/vapi-webhook-handler.md` from the
|
|
272
|
+
"Where the PHP code lives" section. (tcox)
|
|
266
273
|
- 2026-06-16 — Initial call-orchestration feature doc (PHP ↔ Vapi seam). (akhokhani)
|
|
@@ -21,6 +21,9 @@ files:
|
|
|
21
21
|
- bdr/src/lib/scheduleSlots.ts
|
|
22
22
|
- bdr/src/lib/formatPhone.ts
|
|
23
23
|
- bdr/src/lib/analytics.ts
|
|
24
|
+
- bdr/src/lib/rateLimit.ts
|
|
25
|
+
- bdr/src/proxy.ts
|
|
26
|
+
- bdr/.env.example
|
|
24
27
|
- bdr/src/components/GoogleAnalytics.tsx
|
|
25
28
|
- bdr/src/components/RichText.tsx
|
|
26
29
|
- bdr/src/content
|
|
@@ -53,8 +56,13 @@ fully implemented at commit `34a3910`: `page.tsx primeLead` reads `?hsContactId=
|
|
|
53
56
|
campaign attribution → only `togaUuid` crosses to the client). The 2026-07-16 session
|
|
54
57
|
added the deltas below (phone prefill + live input mask, `submitOk` on Success, GA4
|
|
55
58
|
wiring) and resolved §9.10 (content mechanism — see `web-funnel-content-model.md`) and
|
|
56
|
-
§9.11 (consent links).
|
|
57
|
-
|
|
59
|
+
§9.11 (consent links). A later 2026-07-16 session added an **interim app-layer rate-limit
|
|
60
|
+
mitigation** for the `?hsContactId=` enumeration risk (`src/proxy.ts` + `src/lib/rateLimit.ts`),
|
|
61
|
+
documented the **fully server-side request-a-call seam**, the **outbound dialer eligibility
|
|
62
|
+
rules** (see `../../worker2/features/vapi-webhook-handler.md`), the **phone-less-contact
|
|
63
|
+
dialer bug**, and the **local dev env wiring**. Developer QA verdict: the UI is "very close,
|
|
64
|
+
minimal adjustments to be near exact" vs. the mockup. All checks green: tsc, eslint,
|
|
65
|
+
89/89 Vitest, `next build` (build output labels the proxy `ƒ Proxy (Middleware)`).
|
|
58
66
|
|
|
59
67
|
- **Repo:** `agilantsolutions/BDR` on GitHub, working branch `BDR-Development`. The
|
|
60
68
|
machine-local `.claude/` harness is **gitignored** — committing it would snapshot
|
|
@@ -75,9 +83,15 @@ to be near exact" vs. the mockup. All checks green: tsc, eslint, 79/79 Vitest, `
|
|
|
75
83
|
- **ESLint flat config** carries a **no-em-dash rule** on shipped strings
|
|
76
84
|
(`Literal` / `TemplateElement` / `JSXText` selectors) — enforces the mockup copy
|
|
77
85
|
rule on in-repo strings (`eslint.config.mjs`).
|
|
78
|
-
- **Vitest** test suite: **
|
|
79
|
-
phone format/prefill/mask, live-anchor consent links, `submitOk` race
|
|
80
|
-
|
|
86
|
+
- **Vitest** test suite: **89 tests, all green** (13 added in the first 2026-07-16 session:
|
|
87
|
+
phone format/prefill/mask, live-anchor consent links, `submitOk` race; 10 more in the
|
|
88
|
+
later 2026-07-16 session for the rate limiter). Every phase was verified with `tsc` + lint
|
|
89
|
+
+ build + a live SSR `curl` before commit.
|
|
90
|
+
- **Edge proxy (Next 16 `proxy.ts`).** `src/proxy.ts` is the Next 16 request proxy (the
|
|
91
|
+
successor to the deprecated `middleware.ts` convention — see the Next 16 gotcha) carrying
|
|
92
|
+
the interim rate-limit mitigation. It uses `src/lib/rateLimit.ts`, a pure clock-injected
|
|
93
|
+
in-memory token-bucket limiter (no `server-only`, so it runs in the edge proxy and is
|
|
94
|
+
unit-testable in Node).
|
|
81
95
|
- **Deploy target:** AWS Amplify (SSR), `amplify.yml` present (Phase 8 not done).
|
|
82
96
|
|
|
83
97
|
## Directory map (what got built)
|
|
@@ -99,7 +113,10 @@ to be near exact" vs. the mockup. All checks green: tsc, eslint, 79/79 Vitest, `
|
|
|
99
113
|
- `src/server/*` — the real backend, ported from `info`'s `toga.ts` / `hubspot.ts`
|
|
100
114
|
behind `LeadSink` + `CallbackService` adapters (detail in
|
|
101
115
|
`web-funnel-content-model.md`).
|
|
102
|
-
- `src/app/api/*` — route handlers `call-now`, `call-later`, `contact`.
|
|
116
|
+
- `src/app/api/*` — route handlers `call-now`, `call-later`, `contact`. **These do NOT
|
|
117
|
+
place a call themselves** — see "Request-a-call is fully server-side" below.
|
|
118
|
+
- `src/proxy.ts` — Next 16 edge proxy (rate limiting; see gotcha). `src/lib/rateLimit.ts` —
|
|
119
|
+
the token-bucket limiter it uses.
|
|
103
120
|
- `public/*` — assets pruned to the **default campaign only**, **plus AutoBuild's
|
|
104
121
|
FLOURISH characters** (Owl 1 `owl-02` + Robot 1 `robot-02`, art + talk videos) which
|
|
105
122
|
the assembly cycle needs even though they are not the campaign agent. See the
|
|
@@ -156,6 +173,67 @@ to be near exact" vs. the mockup. All checks green: tsc, eslint, 79/79 Vitest, `
|
|
|
156
173
|
`http(s)` hrefs as live anchors (`target=_blank`, `rel="noopener noreferrer"`, aria-label
|
|
157
174
|
"... (opens in new tab)"); bare `#hash` hrefs keep the mockup's inert `preventDefault`.
|
|
158
175
|
|
|
176
|
+
## Request-a-call is fully server-side ("no API call in my console" is expected)
|
|
177
|
+
|
|
178
|
+
The "request a call" flow (Call Now / Schedule) is **entirely server-side** — this is by
|
|
179
|
+
design, and repeatedly causes "there's no Toga API call in my network tab" confusion.
|
|
180
|
+
|
|
181
|
+
- The **browser only POSTs to the Next route handler** (`/api/call-now`, `/api/call-later`).
|
|
182
|
+
A `200 POST /api/call-now` in the console **is** the success signal.
|
|
183
|
+
- The **Toga calls happen inside that route handler**, server-side: `POST /auth/api` to get a
|
|
184
|
+
token, then `PUT /contacts/{uuid}` against production `api.togahub.com/v2`
|
|
185
|
+
(`src/server/toga.ts`, `src/server/callbackService.ts`, `src/flow/leadActions.ts`). So
|
|
186
|
+
Toga/api2 traffic **never appears in the browser network tab** — only the internal
|
|
187
|
+
`/api/*` hop does.
|
|
188
|
+
- **BDR itself never places a call.** The `PUT /contacts/{uuid}` only flips flags on the
|
|
189
|
+
contact: `dtNextContactRequested` (literal `NOW()` for immediate, a CST-formatted datetime
|
|
190
|
+
for scheduled), `isOkayToCall=1`, `isActive=1`, and the `contactCallType {uuid}` (immediate
|
|
191
|
+
vs. scheduled `CONTACT_CALL_TYPE_UUID`). The actual Vapi call is placed later by the
|
|
192
|
+
**worker2 dialer cron** (`_Worker_Ai_Bdr_Vapi`) — see the eligibility rules below.
|
|
193
|
+
|
|
194
|
+
## The outbound dialer decides whether the call ever happens (worker2)
|
|
195
|
+
|
|
196
|
+
Setting `dtNextContactRequested` does **not** guarantee a call — the worker2 dialer's
|
|
197
|
+
eligibility SQL (`getEligibleContactsForVapi()` in `worker2/Worker/Ai/Bdr/Vapi.php`) is the
|
|
198
|
+
source of truth for "why isn't my contact being called." Full rules are documented in
|
|
199
|
+
**`../../worker2/features/vapi-webhook-handler.md`**. Two consequences matter to the funnel:
|
|
200
|
+
|
|
201
|
+
- The website "call me now / scheduled" path (`dtNextContactRequested <= NOW()`) **bypasses**
|
|
202
|
+
call windows and weekly cadence limits, but still requires an active campaign **with an
|
|
203
|
+
assigned assistant**, `isActive=1`, `isOkayToCall=1`, a live **primary** phone, and
|
|
204
|
+
`attemptCount < maxAttemptsPerContact`.
|
|
205
|
+
- **BUG — a phone-less HubSpot contact can never receive a call (silent).** When the HubSpot
|
|
206
|
+
contact has no phone, BDR creates the Toga contact **without** a phone at page-prime; the
|
|
207
|
+
phone the visitor types arrives later via the Call Now `PUT`. api2 creates a
|
|
208
|
+
`ContactPhoneNumber` row on that update but does **not** set
|
|
209
|
+
`Contacts.primaryContactPhoneNumberId`. The dialer's eligibility SQL `INNER JOIN`s on
|
|
210
|
+
`primaryContactPhoneNumberId`, so the contact stays invisible to the dialer. **Re-submitting
|
|
211
|
+
does not fix it** (verified in production: the phone row exists but the primary pointer
|
|
212
|
+
stays null). The funnel still shows success and returns `200` — a silent failure. The old
|
|
213
|
+
`info` site never hit this because CRM-sourced contacts already had a phone at CREATE time
|
|
214
|
+
(api2 links the primary on create, not on a later update). Fix candidates: api2-side (set
|
|
215
|
+
the primary when a phone row is created during an update) or a BDR workaround (a follow-up
|
|
216
|
+
`PUT` linking the existing phone-row uuid as primary). Affects **any** phone-less HubSpot
|
|
217
|
+
contact and blocks live call testing.
|
|
218
|
+
|
|
219
|
+
## Local dev setup (BDR)
|
|
220
|
+
|
|
221
|
+
BDR local dev needs **5 server-side env vars** in `BDR/.env.local` (gitignored, never
|
|
222
|
+
committed): `HUBSPOT_ACCESS_TOKEN`, `TOGA_API_BASE_URL`, `TOGA_CLIENT_ID`,
|
|
223
|
+
`TOGA_CLIENT_API_UUID`, `TOGA_CLIENT_API_SECRET`. **Where the values live (not the values
|
|
224
|
+
themselves):** source them from the **`info` repo's `.env.local`**, which carries the
|
|
225
|
+
PRODUCTION credential set (`TOGA_API_BASE_URL = https://api.togahub.com/v2`).
|
|
226
|
+
|
|
227
|
+
- **`info/.env.beta` is a DEAD file** — empty URLs and obsolete var names (e.g.
|
|
228
|
+
`TOGA_CLIENT_SECRET` vs. the current `TOGA_CLIENT_API_UUID` / `TOGA_CLIENT_API_SECRET`
|
|
229
|
+
pair). Production is the only working credential set `info` has.
|
|
230
|
+
- **Consequence: local dev hits LIVE Toga** and can trigger **real calls** — treat local
|
|
231
|
+
submits accordingly.
|
|
232
|
+
- **Leave `NEXT_PUBLIC_GA_MEASUREMENT_ID` unset locally** so dev traffic does not pollute the
|
|
233
|
+
production GA4 property (`analytics.ts` no-ops without it).
|
|
234
|
+
- **Restart the dev server after creating `.env.local`** — Next reads env files only at
|
|
235
|
+
server startup. `.env.*` is gitignored in BDR.
|
|
236
|
+
|
|
159
237
|
## What is parked / not built
|
|
160
238
|
|
|
161
239
|
- **Creator screen + `TweaksPanel` — deliberately NOT ported** (parked per plan
|
|
@@ -179,13 +257,39 @@ to be near exact" vs. the mockup. All checks green: tsc, eslint, 79/79 Vitest, `
|
|
|
179
257
|
entry identifiers would ship to Google on every hit. Mitigation (in place): initialize
|
|
180
258
|
with `gtagOptions {send_page_view:false}` and pin a **query-scrubbed** `page_location`
|
|
181
259
|
via `ReactGA.set` before each pageview. Never send raw `location.href` to analytics here.
|
|
182
|
-
- **HIGH
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
**
|
|
188
|
-
|
|
260
|
+
- **HIGH (security) — `?hsContactId=` is an enumerable capability token (interim mitigation
|
|
261
|
+
in place, authoritative fix still deferred).** The entry page is public and HubSpot contact
|
|
262
|
+
ids are small integers; a bot walking ids can (a) scrape each contact's phone from the
|
|
263
|
+
server-rendered prefill, (b) scrape the **full** contact record via `GET /api/contact`
|
|
264
|
+
(see next gotcha), and (c) drive the page-prime Toga upsert, polluting contacts/attribution.
|
|
265
|
+
**Inherited from `info`'s CRM-link URL contract — cannot be changed unilaterally.**
|
|
266
|
+
**Interim app-layer mitigation (BUILT, 2026-07-16):** `src/proxy.ts` + `src/lib/rateLimit.ts`
|
|
267
|
+
rate-limit per client. Entry pages carrying `?hsContactId=` **soft-degrade** over the limit
|
|
268
|
+
(strip `hsContactId`, `NextResponse.rewrite` to render anonymously — a real shared-NAT user
|
|
269
|
+
keeps the funnel minus prefill; a scraper gets no PII and triggers no Toga upsert;
|
|
270
|
+
`hsCampaignId` is preserved). `/api/contact`, `/api/call-now`, `/api/call-later` return a
|
|
271
|
+
`429 {isSuccess:false,…}` envelope with `Retry-After` over the limit. **This is
|
|
272
|
+
defense-in-depth ONLY** — the limiter is per-runtime-instance in-memory (Amplify SSR may run
|
|
273
|
+
several instances), so it is not authoritative. The authoritative controls — a **per-IP edge
|
|
274
|
+
WAF rate rule** (Phase 8) and **opaque/signed CRM link tokens** (raise with the CRM owner) —
|
|
275
|
+
remain deferred. Tracked in `BDR/PLAN.md` §10/§11. (An independent `cso` review of this
|
|
276
|
+
mitigation was still running at capture time; treat it as "in place, pending final review".)
|
|
277
|
+
- **HIGH (security) — `GET /api/contact?hsContactId=` returns the FULL contact record.** Name,
|
|
278
|
+
email, phone, company, and job title by id — a **more direct** PII scrape vector than the SSR
|
|
279
|
+
phone prefill. Now covered by the same rate limiter; **any change to
|
|
280
|
+
`src/app/api/contact/route.ts` must keep it rate-limited.**
|
|
281
|
+
- **Next 16 — `middleware.ts` is DEPRECATED; use `proxy.ts`.** Next 16.2.3 warns "The
|
|
282
|
+
middleware file convention is deprecated. Please use proxy instead"
|
|
283
|
+
(nextjs.org/docs/messages/middleware-to-proxy). Correct convention: `src/proxy.ts` exporting
|
|
284
|
+
a function named `proxy` (or default) plus `export const config: ProxyConfig = { matcher:
|
|
285
|
+
[...] }`; import `NextRequest` / `NextResponse` / `ProxyConfig` from `"next/server"`;
|
|
286
|
+
`NextResponse.next()` / `.rewrite()` are unchanged. Build output labels it
|
|
287
|
+
`ƒ Proxy (Middleware)`. Write `proxy.ts` from the start on Next 16 — do not use `middleware.ts`.
|
|
288
|
+
- **Request-a-call is server-side — expect NO Toga call in the browser network tab.** A
|
|
289
|
+
`200 POST /api/call-now` is the success signal; the `POST /auth/api` + `PUT /contacts/{uuid}`
|
|
290
|
+
happen inside the route handler. See "Request-a-call is fully server-side" above. And note
|
|
291
|
+
setting `dtNextContactRequested` does not guarantee a call — the worker2 dialer eligibility
|
|
292
|
+
SQL decides (a phone-less contact is silently never dialed; see that section).
|
|
189
293
|
- **Standing rule: never add more PII fields to `ClientLeadRef`.** Phone is the one
|
|
190
294
|
deliberate exception (visitor's own number, shown back to them, never logged). Every field
|
|
191
295
|
on `ClientLeadRef` crosses to the public client and is exposed to the enumeration risk
|
|
@@ -218,6 +322,24 @@ to be near exact" vs. the mockup. All checks green: tsc, eslint, 79/79 Vitest, `
|
|
|
218
322
|
harness; pull it with `npx toga-ai`.
|
|
219
323
|
|
|
220
324
|
## Change history
|
|
325
|
+
- 2026-07-16 — Later same-day session. BUILT an interim app-layer rate-limit mitigation for the
|
|
326
|
+
`?hsContactId=` enumeration risk: `src/lib/rateLimit.ts` (pure clock-injected in-memory
|
|
327
|
+
token-bucket, idle-key eviction, no `server-only`) + `src/proxy.ts` (Next 16 proxy). Policy:
|
|
328
|
+
entry pages soft-degrade (strip `hsContactId`, rewrite anonymous, keep `hsCampaignId`);
|
|
329
|
+
`/api/{contact,call-now,call-later}` return `429` over the limit. Defense-in-depth only
|
|
330
|
+
(per-instance memory; edge WAF + signed CRM tokens still deferred to Phase 8); a `cso` review
|
|
331
|
+
was still running at capture. 10 new tests (89 total). DISCOVERED + documented: (1) the
|
|
332
|
+
request-a-call flow is fully server-side (`POST /auth/api` + `PUT /contacts/{uuid}` happen in
|
|
333
|
+
the route handler; browser only sees `/api/*`, so "no Toga call in my console" is expected;
|
|
334
|
+
the `PUT` only flips `dtNextContactRequested`/`isOkayToCall`/`isActive`/`contactCallType`, the
|
|
335
|
+
worker2 dialer places the call); (2) BUG — a phone-less HubSpot contact is silently never
|
|
336
|
+
dialed because api2 doesn't set `primaryContactPhoneNumberId` on a later update and the dialer
|
|
337
|
+
INNER JOINs on it (re-submit does not fix; verified in prod); (3) `GET /api/contact` returns
|
|
338
|
+
the full contact record (name/email/phone/company/title) — a direct PII scrape vector, now
|
|
339
|
+
rate-limited; (4) Next 16 `middleware.ts` is deprecated in favor of `proxy.ts`; (5) local dev
|
|
340
|
+
env wiring (5 server vars in `.env.local` sourced from `info`'s PRODUCTION `.env.local`;
|
|
341
|
+
`.env.beta` is dead; local dev hits LIVE Toga; leave GA id unset; restart to pick up env).
|
|
342
|
+
Dialer eligibility rules recorded in `../../worker2/features/vapi-webhook-handler.md`. (tcox)
|
|
221
343
|
- 2026-07-16 — Phase 6 delta + §9.10/§9.11 resolutions. DISCOVERED the `info` URL/backend
|
|
222
344
|
contract was already fully built at commit `34a3910` (primeLead: `?hsContactId=` +
|
|
223
345
|
`?hsCampaignId=` → HubSpot `getContact` → Toga upsert + attribution → only `togaUuid` to
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [snaredla]
|
|
9
|
+
updated: 2026-07-16
|
|
10
|
+
owners: [snaredla, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Vapi.php
|
|
13
13
|
- worker2/Worker/Ai/Bdr/Vapi.php
|
|
@@ -125,6 +125,46 @@ Therefore an `end-of-call-report` can legitimately arrive **without** a `contact
|
|
|
125
125
|
job. `Contact Attempt not found` should be reserved for a report that *has* a
|
|
126
126
|
`contactAttemptUuid` but no matching row (a real data-integrity anomaly).
|
|
127
127
|
|
|
128
|
+
## Outbound dialer eligibility (`getEligibleContactsForVapi`)
|
|
129
|
+
|
|
130
|
+
The outbound dialer `_Worker_Ai_Bdr_Vapi` (`worker2/Worker/Ai/Bdr/Vapi.php`) is the source of
|
|
131
|
+
truth for **"why isn't my contact being called."** Confirmed against source
|
|
132
|
+
`getEligibleContactsForVapi()` (~L197–306). A contact is dialed **only when ALL** hold:
|
|
133
|
+
|
|
134
|
+
1. It is linked to an **active campaign that has an assigned assistant** —
|
|
135
|
+
`INNER JOIN CampaignAssistants ON Campaigns.campaignAssistantId`. A campaign with
|
|
136
|
+
`campaignAssistantId = null` is **never** dialed.
|
|
137
|
+
2. `Contacts.isActive = 1`.
|
|
138
|
+
3. `Contacts.isOkayToCall = 1`.
|
|
139
|
+
4. A non-empty **PRIMARY** phone — `INNER JOIN ContactPhoneNumbers ON
|
|
140
|
+
Contacts.primaryContactPhoneNumberId` (the primary pointer, **not** just any phone row).
|
|
141
|
+
5. `attemptCount < Campaigns.maxAttemptsPerContact`.
|
|
142
|
+
6. **EITHER** `Contacts.dtNextContactRequested <= NOW()` — this path **BYPASSES call windows
|
|
143
|
+
AND weekly cadence limits**; it is the website "call me now / scheduled" path — **OR**
|
|
144
|
+
(`dtNextContactRequested IS NULL` AND within `CampaignCallWindows` for the campaign timezone
|
|
145
|
+
AND not a `CampaignCallWindowException` date AND under
|
|
146
|
+
`CampaignCadences.maxCallsPerContactPerWeek`).
|
|
147
|
+
|
|
148
|
+
**Cron entry point:** `ProcessAllActiveCampaigns` (~L42) → `ProcessCampaignCalls` →
|
|
149
|
+
`getEligibleContactsForVapi` → `processOutboundCalls`.
|
|
150
|
+
|
|
151
|
+
**Single "Call Now" path:** `InitiateOutboundCall(contactPhoneNumberUuid)` (~L151–170) →
|
|
152
|
+
`initiateCallForContact(…, CALL_TYPE__IMMEDIATE)`. It errors **"not assigned to any active
|
|
153
|
+
campaign"** if no ACTIVE/PENDING campaign-contact link exists.
|
|
154
|
+
|
|
155
|
+
### Gotcha — a phone-less contact is silently never dialed (BDR web funnel)
|
|
156
|
+
|
|
157
|
+
Criterion 4's `INNER JOIN` on `primaryContactPhoneNumberId` is why a contact whose phone was
|
|
158
|
+
added **after** creation can be invisible to the dialer: **api2 creates the
|
|
159
|
+
`ContactPhoneNumber` row but does not set `Contacts.primaryContactPhoneNumberId` on an UPDATE**
|
|
160
|
+
(it only links the primary on CREATE). The BDR web funnel hits this for any phone-less HubSpot
|
|
161
|
+
contact — the visitor's typed phone arrives via a later `PUT /contacts/{uuid}`, so the row
|
|
162
|
+
exists but the primary pointer stays null and the eligibility join excludes it. Re-submitting
|
|
163
|
+
does not fix it (verified in production). See
|
|
164
|
+
`../../ai-bdr/features/web-funnel-app.md` for the funnel side. Fix candidates: api2 sets the
|
|
165
|
+
primary on phone-row create-during-update, or a caller-side `PUT` linking the existing
|
|
166
|
+
phone-row uuid as primary.
|
|
167
|
+
|
|
128
168
|
## Data model
|
|
129
169
|
|
|
130
170
|
Writes to `ContactAttempts` (via Toga 2.0 API) — `dtStarted`, `dtEnded`, `transcript`,
|
|
@@ -152,5 +192,11 @@ status. See the `ai-bdr` skill / `_Model_True_ContactAttempt` for the full field
|
|
|
152
192
|
- [VAPI Integration — assistants, tools, structured output](../../ai-bdr/features/vapi-integration.md)
|
|
153
193
|
|
|
154
194
|
## Change history
|
|
195
|
+
- 2026-07-16 — Documented the outbound dialer eligibility rules from source
|
|
196
|
+
`getEligibleContactsForVapi()` (the 6 ALL-of criteria, the `dtNextContactRequested <= NOW()`
|
|
197
|
+
window/cadence bypass = the website call-now/scheduled path, cron entry chain, and the single
|
|
198
|
+
"Call Now" `InitiateOutboundCall` path). Added the phone-less-contact gotcha: api2 does not
|
|
199
|
+
set `primaryContactPhoneNumberId` on an UPDATE, so the `INNER JOIN` on it silently excludes a
|
|
200
|
+
contact whose phone was added after create (blocks the BDR web funnel; verified in prod). (tcox)
|
|
155
201
|
- 2026-06-24 — Initial doc: webhook handler, contactAttemptUuid write-back, message-type
|
|
156
202
|
filtering bug, inbound/test vs outbound metadata. (snaredla)
|
package/package.json
CHANGED