toga-ai 1.0.381 → 1.0.383
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/features/web-funnel-app.md +71 -13
- package/knowledge/2.0/apps/ai-bdr/workflows/safe-call-loop-testing.md +112 -0
- package/knowledge/2.0/apps/api2/features/nested-relationship-writes.md +46 -2
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -0
- package/knowledge/2.0/apps/worker2/features/clickup-task-fluffer-talos.md +156 -0
- package/knowledge/2.0/apps/worker2/features/vapi-webhook-handler.md +70 -5
- package/knowledge/INDEX.md +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: AI-BDR
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
9
|
+
updated: 2026-07-20
|
|
10
10
|
owners: [tcox]
|
|
11
11
|
files:
|
|
12
12
|
- bdr/PLAN.md
|
|
@@ -31,14 +31,19 @@ files:
|
|
|
31
31
|
- bdr/src/components
|
|
32
32
|
- bdr/src/flow
|
|
33
33
|
- bdr/src/server
|
|
34
|
+
- bdr/src/server/toga.ts
|
|
34
35
|
- bdr/src/app/api
|
|
35
36
|
- bdr/test
|
|
37
|
+
- bdr/test/togaPrimaryPhone.test.ts
|
|
36
38
|
- bdr/public
|
|
37
39
|
- bdr/mockup/styles.css
|
|
38
40
|
related:
|
|
39
41
|
- web-funnel-content-model.md
|
|
40
42
|
- bdr-web-funnel-plan.md
|
|
41
43
|
- ../architecture.md
|
|
44
|
+
- ../workflows/safe-call-loop-testing.md
|
|
45
|
+
- ../../worker2/features/vapi-webhook-handler.md
|
|
46
|
+
- ../../api2/features/nested-relationship-writes.md
|
|
42
47
|
---
|
|
43
48
|
|
|
44
49
|
## What this is
|
|
@@ -88,7 +93,9 @@ to its ship state (trusted-IP position for X-Forwarded-For; O(1) LRU-capped limi
|
|
|
88
93
|
- **ESLint flat config** carries a **no-em-dash rule** on shipped strings
|
|
89
94
|
(`Literal` / `TemplateElement` / `JSXText` selectors) — enforces the mockup copy
|
|
90
95
|
rule on in-repo strings (`eslint.config.mjs`).
|
|
91
|
-
- **Vitest** test suite: **
|
|
96
|
+
- **Vitest** test suite: **114 tests, all green** (10 added 2026-07-20 for the primary-phone
|
|
97
|
+
self-heal — null-pointer re-link, changed-number re-link, create-as-last-resort, strict
|
|
98
|
+
`requestCall` vs. absorbing `upsertContactByEmail`; 13 added in the first 2026-07-16 session:
|
|
92
99
|
phone format/prefill/mask, live-anchor consent links, `submitOk` race; the later 2026-07-16
|
|
93
100
|
session added the rate-limiter + trusted-IP + LRU-eviction + `hsCampaignId` validation tests
|
|
94
101
|
to reach 104). Every phase was verified with `tsc` + lint + build + a live SSR `curl` before
|
|
@@ -212,19 +219,33 @@ source of truth for "why isn't my contact being called." Full rules are document
|
|
|
212
219
|
call windows and weekly cadence limits, but still requires an active campaign **with an
|
|
213
220
|
assigned assistant**, `isActive=1`, `isOkayToCall=1`, a live **primary** phone, and
|
|
214
221
|
`attemptCount < maxAttemptsPerContact`.
|
|
215
|
-
- **BUG
|
|
216
|
-
contact has no phone, BDR creates the Toga contact **without** a phone at page-prime; the
|
|
222
|
+
- **BUG (funnel side FIXED — self-heal shipped; api2 platform fix still deferred).** When the
|
|
223
|
+
HubSpot contact has no phone, BDR creates the Toga contact **without** a phone at page-prime; the
|
|
217
224
|
phone the visitor types arrives later via the Call Now `PUT`. api2 creates a
|
|
218
225
|
`ContactPhoneNumber` row on that update but does **not** set
|
|
219
|
-
`Contacts.primaryContactPhoneNumberId
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
`Contacts.primaryContactPhoneNumberId` — the exact api2 root cause (the UPDATE path injects a
|
|
227
|
+
reverse back-reference into the nested child object, defeating the single-key-uuid forced-MATCH
|
|
228
|
+
fast-path) is now documented in
|
|
229
|
+
[api2 nested-relationship-writes.md](../../api2/features/nested-relationship-writes.md). The
|
|
230
|
+
dialer's eligibility SQL `INNER JOIN`s on `primaryContactPhoneNumberId`, so the contact stays
|
|
231
|
+
invisible to the dialer, the funnel still shows success and returns `200` — a silent failure. The
|
|
232
|
+
old `info` site never hit this because CRM-sourced contacts had a phone at CREATE time (api2 links
|
|
233
|
+
the primary on create, not on a later update).
|
|
234
|
+
- **Self-heal (SHIPPED, `src/server/toga.ts`, commit `4ac579e`):** after any contact write that
|
|
235
|
+
carried a phone, if the response echoes `primaryContactPhoneNumber` **null** OR a number whose
|
|
236
|
+
last-10 digits **don't match** what was sent (stale-pointer / changed-number case), the code
|
|
237
|
+
finds the orphaned `ContactPhoneNumber` row uuid (from the echoed contact's `contactPhoneNumbers`
|
|
238
|
+
→ fresh `GET /contacts/{uuid}` → creates one via `POST /contact-phone-numbers` as a last resort)
|
|
239
|
+
and re-links it with an **identifier-only** `PUT {primaryContactPhoneNumber:{uuid}}` (a single-key
|
|
240
|
+
identifier object hits api2's forced-MATCH path). **`requestCall` is strict** (throws → the route
|
|
241
|
+
returns `500`, so the UI never reports success for an undial-able submit); **`upsertContactByEmail`
|
|
242
|
+
absorbs + warns** (lead priming must never break the funnel — call-now re-heals). Covered by 10
|
|
243
|
+
regression tests (written first, failed on old code; 114-test suite green; an independent review
|
|
244
|
+
found + fixed the changed-number case). Resolves the funnel-side bug **once `_dev-sandbox` deploys**
|
|
245
|
+
(deploy still pending — see the deploy note below).
|
|
246
|
+
- **api2 platform fix is still deferred** to its own PR/ticket (gate the reverse-back-reference
|
|
247
|
+
injection to true reverse/has-many relations). The self-heal covers the funnel meanwhile; the
|
|
248
|
+
same api2 bug likely affects `primaryContactEmailAddress` / `primaryContactAddress` on update.
|
|
228
249
|
|
|
229
250
|
## Local dev setup (BDR)
|
|
230
251
|
|
|
@@ -244,6 +265,30 @@ PRODUCTION credential set (`TOGA_API_BASE_URL = https://api.togahub.com/v2`).
|
|
|
244
265
|
- **Restart the dev server after creating `.env.local`** — Next reads env files only at
|
|
245
266
|
server startup. `.env.*` is gitignored in BDR.
|
|
246
267
|
|
|
268
|
+
## Deploy status (AWS Amplify — NOT yet deployed)
|
|
269
|
+
|
|
270
|
+
The BDR site is **not deployed anywhere yet** (as of 2026-07-20). Go-live is blocked on the
|
|
271
|
+
Amplify setup, tracked by ClickUp **868kdf4tj "BDR - create amplify and sandbox route"** (urgent,
|
|
272
|
+
Sprint 82) — still `to do`. (ClickUp shows Alex Peterson as assignee; the team says Jeff picked it
|
|
273
|
+
up — ClickUp is stale, confirm the real owner before assuming.)
|
|
274
|
+
|
|
275
|
+
- **Deploy branch is `_dev-sandbox`** (underscore-prefix convention), **not** `BDR-Development` /
|
|
276
|
+
`BDR-Phase-2`. The primary-phone self-heal (commit `4ac579e`, see the phone-less bug above) is
|
|
277
|
+
already merged into `_dev-sandbox`, so **Amplify's first build ships the fix**.
|
|
278
|
+
- **Amplify must be configured for SSR hosting (NOT static export)** — the app is server-rendered
|
|
279
|
+
(server components, route handlers, and the `proxy.ts` edge proxy all require the SSR/compute
|
|
280
|
+
runtime). Point the app at branch `_dev-sandbox`.
|
|
281
|
+
- **5 server-side env vars are required** (same set as local dev): `TOGA_API_BASE_URL`,
|
|
282
|
+
`TOGA_CLIENT_ID`, `TOGA_CLIENT_API_UUID`, `TOGA_CLIENT_API_SECRET`, `HUBSPOT_ACCESS_TOKEN`.
|
|
283
|
+
**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.
|
|
285
|
+
- **Phase 8 WAF pairing.** `BDR/PLAN.md` Phase 8 specifies a **per-IP rate rule on the CloudFront
|
|
286
|
+
distribution / WAF** that pairs with the in-app token-bucket limiter (the app limiter is
|
|
287
|
+
defense-in-depth only; the edge WAF rule is the authoritative control — see the enumeration
|
|
288
|
+
gotcha). Configure it as part of the Amplify/CloudFront setup, along with the non-gating `cso`
|
|
289
|
+
follow-ups (lock the Amplify origin to CloudFront; verify `CloudFront-Viewer-Address` inject+strip
|
|
290
|
+
before trusting it).
|
|
291
|
+
|
|
247
292
|
## What is parked / not built
|
|
248
293
|
|
|
249
294
|
- **Creator screen + `TweaksPanel` — deliberately NOT ported** (parked per plan
|
|
@@ -371,6 +416,19 @@ PRODUCTION credential set (`TOGA_API_BASE_URL = https://api.togahub.com/v2`).
|
|
|
371
416
|
harness; pull it with `npx toga-ai`.
|
|
372
417
|
|
|
373
418
|
## Change history
|
|
419
|
+
- 2026-07-20 — FIXED the phone-less-contact dialer bug on the funnel side: shipped a primary-phone
|
|
420
|
+
**self-heal** in `src/server/toga.ts` (commit `4ac579e`) — after any contact write carrying a
|
|
421
|
+
phone, if the echoed `primaryContactPhoneNumber` is null or its last-10 digits mismatch, find/create
|
|
422
|
+
the `ContactPhoneNumber` row uuid and re-link via an identifier-only `PUT {primaryContactPhoneNumber:
|
|
423
|
+
{uuid}}` (single-key forced-MATCH). `requestCall` is strict (throws → route 500, UI never falsely
|
|
424
|
+
reports success); `upsertContactByEmail` absorbs+warns (priming never breaks the funnel; call-now
|
|
425
|
+
re-heals). 10 regression tests (114 suite green; independent review found+fixed the changed-number
|
|
426
|
+
case). The **exact api2 root cause** was also located and documented (UPDATE-path reverse-back-ref
|
|
427
|
+
injection defeats the uuid forced-MATCH — see api2 `nested-relationship-writes.md`); the api2 platform
|
|
428
|
+
fix is deferred. Added the **Deploy status** section: BDR is not deployed yet (ClickUp 868kdf4tj still
|
|
429
|
+
to-do); deploy branch is `_dev-sandbox` (now carrying the self-heal); Amplify needs SSR (not static),
|
|
430
|
+
the 5 server env vars (values sourced from `info`'s `.env.local`), and the Phase 8 per-IP WAF rate
|
|
431
|
+
rule. (tcox)
|
|
374
432
|
- 2026-07-16 — Rate-limit mitigation HARDENED to ship state after an independent `cso` security
|
|
375
433
|
review returned **SHIP after two rounds** (supersedes the mid-state recorded in the entry
|
|
376
434
|
below: test count is **104**, not 89, and the review is complete, not "still running"). Added
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Safe AI-BDR Call-Loop Testing (isolated test-campaign runbook)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: ai-bdr
|
|
5
|
+
project: AI-BDR
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-20
|
|
10
|
+
owners: [tcox]
|
|
11
|
+
files:
|
|
12
|
+
- dbchanges2/Client_True/2026-07-20a - AiBdrTcoxTestCampaign.sql
|
|
13
|
+
- worker2/Worker/Ai/Bdr/Vapi.php
|
|
14
|
+
- worker2/Config/beta.ini
|
|
15
|
+
related:
|
|
16
|
+
- new-campaign-onboarding.md
|
|
17
|
+
- ../architecture.md
|
|
18
|
+
- ../../worker2/features/vapi-webhook-handler.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
|
|
23
|
+
How to prove the AI-BDR **dialer → Vapi → end-of-call webhook** loop end-to-end **without
|
|
24
|
+
dialing real prospects**. The production dialer cron (`Core.CronJobs` "AI BDR",
|
|
25
|
+
`ProcessAllActiveCampaigns`, schedule `* * * * *`) fires **every minute**, so any campaign that
|
|
26
|
+
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. The safe technique
|
|
28
|
+
is an **isolated one-contact test campaign with no call windows**. Eligibility mechanics that make
|
|
29
|
+
this work are in [worker2 vapi-webhook-handler.md](../../worker2/features/vapi-webhook-handler.md).
|
|
30
|
+
|
|
31
|
+
## Rule 1 — NEVER revive an ended campaign that has linked prospects
|
|
32
|
+
|
|
33
|
+
An ended campaign (`dateEnd` in the past) is rejected every run by `validateCampaignReady`
|
|
34
|
+
("Campaign has ended"). Flipping `dateEnd`/`isActive` to revive it makes **all** its
|
|
35
|
+
contact-level-dialable contacts eligible at once, and the every-minute cron dials them within
|
|
36
|
+
60s. (Observed live: campaign `26.05 - AI BDR - Ryan Nitti` was blocked **solely** by
|
|
37
|
+
`dateEnd`; it had 132 linked contacts, ~86 contact-level dialable — reviving it would have
|
|
38
|
+
dialed real prospects in one cron tick.) Likewise, do **not** blanket-flip `isOkayToCall=1` on a
|
|
39
|
+
real list: it is two mass writes plus a restore burden, and it silently un-blocks any contact
|
|
40
|
+
that was intentionally set `isOkayToCall=0`.
|
|
41
|
+
|
|
42
|
+
## Rule 2 — Use an isolated one-contact test campaign with NO call windows
|
|
43
|
+
|
|
44
|
+
The safe posture exploits the eligibility split (see vapi-webhook-handler.md): the normal
|
|
45
|
+
(non-requested) dial path requires a `CampaignCallWindows` row for the current day/time, but the
|
|
46
|
+
**requested** path (`dtNextContactRequested <= NOW()`) bypasses windows/cadence. So:
|
|
47
|
+
|
|
48
|
+
1. **Deactivate any ended campaign** that shares the environment (also silences the every-minute
|
|
49
|
+
"Campaign has ended" cron error).
|
|
50
|
+
2. **Create a dedicated test campaign** copied from a known-good campaign's config (same assistant
|
|
51
|
+
/ Cal.com event / timezone, a short window e.g. 7 days, the normal attempt cap), with a
|
|
52
|
+
**hardcoded UUID** so the migration is idempotent and re-runnable.
|
|
53
|
+
3. Create **NO `CampaignCallWindows` rows** for it. With no call windows, the campaign can **only
|
|
54
|
+
ever dial a contact that has a pending `dtNextContactRequested`** — never a normal-cadence
|
|
55
|
+
contact.
|
|
56
|
+
4. **Link exactly ONE test contact** (by uuid, `INNER JOIN`-guarded so the migration no-ops on any
|
|
57
|
+
environment that lacks that contact). That single linked contact is the *only* thing the
|
|
58
|
+
campaign can dial, and only when you set its `dtNextContactRequested`.
|
|
59
|
+
|
|
60
|
+
Reference implementation: `dbchanges2/Client_True/2026-07-20a - AiBdrTcoxTestCampaign.sql`
|
|
61
|
+
(idempotent `NOT EXISTS` guards keyed to the real UNIQUE constraints; passed `sql-reviewer`). This
|
|
62
|
+
proves the loop with **zero prospect exposure** instead of mass-writing flags on real contacts.
|
|
63
|
+
|
|
64
|
+
## Rule 3 — NEVER dry-run against production
|
|
65
|
+
|
|
66
|
+
`dryRun` in the dialer is **not** write-free: `createContactAttempt` runs **before** the dryRun
|
|
67
|
+
early-return, writing a real `ContactAttempts` row (`dtStarted` set, `dtEnded NULL`) that counts
|
|
68
|
+
toward `maxAttemptsPerContact` **and** wedges the contact as in-progress (excluded from all future
|
|
69
|
+
dialing). See the dryRun gotcha in vapi-webhook-handler.md. Unwedge SQL:
|
|
70
|
+
`UPDATE ContactAttempts SET dtEnded=NOW() WHERE contactId=? AND dtEnded IS NULL`.
|
|
71
|
+
|
|
72
|
+
## Rule 4 — Validate the assistant identifier before assigning it
|
|
73
|
+
|
|
74
|
+
`validateCampaignReady` only checks `assistantIdentifier` is **non-empty**, not that it is a
|
|
75
|
+
well-formed Vapi UUID. A corrupt value (e.g. a 37-char uuid-plus-trailing-char) passes the gate
|
|
76
|
+
and then **400s at Vapi** at call time. Confirm the assistant's identifier shape before wiring it
|
|
77
|
+
to any campaign, test or real.
|
|
78
|
+
|
|
79
|
+
## Why a true beta end-to-end CALL is impossible as currently wired
|
|
80
|
+
|
|
81
|
+
Do **not** expect to place a real beta call and see its webhook land in the beta DB:
|
|
82
|
+
|
|
83
|
+
- Vapi is **one production account**. Its assistants' `serverUrl` targets the **production**
|
|
84
|
+
webhook (`webhook.togahub.com`). A beta-placed call's `end-of-call-report` therefore posts to
|
|
85
|
+
**production**, where the `contactAttemptUuid` (created in the **beta** DB) has no matching row
|
|
86
|
+
→ the write-back can never find its attempt.
|
|
87
|
+
- **Beta is good only for rehearsing the SQL / eligibility logic** via a **local worker2 run**
|
|
88
|
+
(not a real call). Note `worker2/Config/beta.ini`'s `aws_worker_queue_url` still points at
|
|
89
|
+
`WorkerProductionQueue` — another reason not to trust beta for anything that enqueues work.
|
|
90
|
+
|
|
91
|
+
A real end-to-end call test must run against production, which is exactly why the isolated
|
|
92
|
+
one-contact test campaign (Rule 2) is the safe vehicle.
|
|
93
|
+
|
|
94
|
+
## dbchanges2 execution caveat — don't ship create + cleanup in the same window
|
|
95
|
+
|
|
96
|
+
The dbchanges2 external executor runs a folder's files **alphabetically in one pass**. Never place
|
|
97
|
+
a create-migration and its cleanup/teardown migration in the **same** run window (same day/folder
|
|
98
|
+
batch) — they will apply together in one pass and the cleanup will immediately undo the create.
|
|
99
|
+
Sequence the cleanup into a later, separately-executed window. (See dbchanges2 architecture for the
|
|
100
|
+
`YYYY-MM-DD<letter>` ordering contract.)
|
|
101
|
+
|
|
102
|
+
## Change history
|
|
103
|
+
- 2026-07-20 — Initial runbook. Captured the safe AI-BDR call-loop test procedure: never revive an
|
|
104
|
+
ended campaign with linked prospects (every-minute cron dials within 60s); use an isolated
|
|
105
|
+
one-contact test campaign with NO `CampaignCallWindows` (only requested `dtNextContactRequested`
|
|
106
|
+
calls can fire) as built in `dbchanges2/Client_True/2026-07-20a`; never dry-run in prod (writes a
|
|
107
|
+
wedging ContactAttempts row); validate the assistant identifier shape (non-empty check lets a
|
|
108
|
+
corrupt uuid 400 at Vapi); a true beta end-to-end call is impossible (single prod Vapi account →
|
|
109
|
+
webhook posts to prod, beta contactAttemptUuid unmatchable); and the dbchanges2 same-window
|
|
110
|
+
create+cleanup execution caveat. (tcox)
|
|
111
|
+
</content>
|
|
112
|
+
</invoke>
|
|
@@ -6,13 +6,15 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-07-
|
|
10
|
-
owners: ["bala", "mhammontree"]
|
|
9
|
+
updated: 2026-07-20
|
|
10
|
+
owners: ["bala", "mhammontree", "tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
related:
|
|
14
14
|
- ../architecture.md
|
|
15
15
|
- ../../../clients/aig/features/entitlement-intake.md
|
|
16
|
+
- ../../ai-bdr/features/web-funnel-app.md
|
|
17
|
+
- ../../worker2/features/vapi-webhook-handler.md
|
|
16
18
|
---
|
|
17
19
|
|
|
18
20
|
## Summary
|
|
@@ -58,6 +60,39 @@ rows on every write.
|
|
|
58
60
|
If you need to link by a business key (like an employee XID), **resolve that key to a uuid in your
|
|
59
61
|
caller first** (e.g. build a `key → uuid` map), then send `{uuid}`.
|
|
60
62
|
|
|
63
|
+
## UPDATE-path back-reference injection defeats the uuid forced-MATCH (primary-pointer bug)
|
|
64
|
+
|
|
65
|
+
A **single-key identifier object** (`{uuid: <x>}` and nothing else) is supposed to hit the
|
|
66
|
+
engine's **forced-MATCH fast-path** (`V2.php:6952`) — link the existing child, never create,
|
|
67
|
+
and let the caller re-point a parent's forward FK to it. On the **CREATE** path this works: the
|
|
68
|
+
set-loop (`V2.php:4601–4616`) applies the child object and the parent FK is assigned at
|
|
69
|
+
`V2.php:4647` with no interference.
|
|
70
|
+
|
|
71
|
+
On the **UPDATE** path it does **not**. When a child model carries a `contactId` FK *back* to
|
|
72
|
+
the parent (as `ContactPhoneNumbers` → `Contacts` does), the update path **injects a reverse
|
|
73
|
+
back-reference** (sets the child's `contactId = parent id`) into the nested child object at
|
|
74
|
+
`V2.php:4985–4996` — added by commit `91d803f` "guard child FK set". That injection turns the
|
|
75
|
+
single-key object into a **multi-key** object, so it no longer qualifies for the forced-MATCH
|
|
76
|
+
fast-path, and it diverts what `getForeignKeyValue()` returns **before** the parent's forward FK
|
|
77
|
+
is assigned at `V2.php:5047`. Net effect: a `PUT {primaryContactPhoneNumber: {uuid}}` does **not**
|
|
78
|
+
set `Contacts.primaryContactPhoneNumberId`, and the `PUT` response **echoes
|
|
79
|
+
`primaryContactPhoneNumber: null`** on the miss — which callers can detect.
|
|
80
|
+
|
|
81
|
+
**Scope.** Any **forward singular FK** whose child model has a back-FK to the parent is exposed
|
|
82
|
+
on update — `primaryContactEmailAddress` / `primaryContactAddress` on `Contacts` are likely
|
|
83
|
+
affected the same way. This is the exact root cause of the **BDR web-funnel "phone-less contact
|
|
84
|
+
is never dialed"** bug (a phone added by a later `PUT` links a `ContactPhoneNumber` row but never
|
|
85
|
+
becomes the primary): see
|
|
86
|
+
[web-funnel-app.md](../../ai-bdr/features/web-funnel-app.md) and
|
|
87
|
+
[vapi-webhook-handler.md](../../worker2/features/vapi-webhook-handler.md).
|
|
88
|
+
|
|
89
|
+
**Suggested platform fix (deferred to its own PR/ticket):** gate the `4985–4996` injection to
|
|
90
|
+
**true reverse / has-many** relations only, so a forward singular-FK child keeps its single-key
|
|
91
|
+
forced-MATCH; confirm `RecordFields.childPolicy` for `Contacts.primaryContactPhoneNumberId` when
|
|
92
|
+
fixing. Until then the caller-side workaround is a follow-up identifier-only `PUT` re-link with a
|
|
93
|
+
self-heal that detects the null/mismatched echo (shipped on the BDR funnel side — see
|
|
94
|
+
web-funnel-app.md).
|
|
95
|
+
|
|
61
96
|
## Per-API overrides (`Apis_RecordFields`)
|
|
62
97
|
|
|
63
98
|
The base identifier flags above (`Core.RecordFields.isIdentifier`) can be **overridden per API**
|
|
@@ -100,6 +135,15 @@ beta/QA threw EV-12 on the injected `entitlementFulfillmentType` — see
|
|
|
100
135
|
|
|
101
136
|
## Change history
|
|
102
137
|
|
|
138
|
+
- 2026-07-20 — Root-caused the **UPDATE-path** failure of the single-key-uuid forced-MATCH: the
|
|
139
|
+
update path injects a reverse back-reference (`contactId = parent id`) into nested child objects
|
|
140
|
+
at `V2.php:4985–4996` (commit `91d803f`) when the child has a back-FK to the parent, turning the
|
|
141
|
+
single-key object multi-key and diverting `getForeignKeyValue()` before the forward FK assignment
|
|
142
|
+
at `:5047` — so `PUT {primaryContactPhoneNumber:{uuid}}` never sets the pointer and the response
|
|
143
|
+
echoes `null`. CREATE path (`:4601–4616` / `:4647`) is unaffected. Scope: any forward singular FK
|
|
144
|
+
whose child has a back-FK to the parent (likely `primaryContactEmailAddress`/`primaryContactAddress`
|
|
145
|
+
too). This is the root cause of the BDR web-funnel phone-less-contact dialer bug; platform fix
|
|
146
|
+
deferred to its own PR (gate the injection to true reverse/has-many relations). (tcox)
|
|
103
147
|
- 2026-07-09 — Documented the per-API `Apis_RecordFields` override layer (`overrideIsIdentifier`,
|
|
104
148
|
`overrideChildPolicy`) and the trap that a `NULL` `overrideIsIdentifier` is read as "not an
|
|
105
149
|
identifier" (`!$row->overrideIsIdentifier`) — an override row set only for a child policy
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
| [ClickUp Project & Opportunity Multi-List Routing](features/clickup-project-routing.md) | Routes ClickUp tasks into the correct **secondary multi-list memberships** based on their custom-field values, via the `clickup` webhook. | worker2/Worker/Clickup/Project.php, worker2/Worker/Clickup.php |
|
|
11
11
|
| [ClickUp Rich-Text Custom Fields via Quill Delta (API)](features/clickup-richtext-api.md) | ClickUp custom text fields (type `text` and long-text) support rich formatting only through a **Quill Delta** written to the undocumented `value_richtext` key o | test/@dave/clickup_md2delta.js, .claude/skills/plan-ticket/scripts/clickup.js |
|
|
12
12
|
| [ClickUp Subtask Activity → Parent Opportunity/Epic Comments](features/clickup-subtask-activity.md) | Surfaces **subtask** progress, completion, and discussion on the top-level **Opportunity** or **Epic** it rolls up to, so a deal/project owner sees activity whe | worker2/Worker/Clickup/Subtask.php, worker2/Worker/Clickup.php, dbchanges2/Team/2026-07-14a - Add ClickupSubtaskActivity ledger.sql |
|
|
13
|
+
| [ClickUp Task Description Fluffer (Talos KB Investigation)](features/clickup-task-fluffer-talos.md) | `_Worker_Clickup_Fluffer` "fluffs" a ClickUp task by sending its name + existing description to the **Talos `dev-core` KB-investigation agent** and writing the | worker2/Worker/Clickup/Fluffer.php, worker2/Worker/Clickup.php, worker2/Config/production.ini, worker2/Config/beta.ini, worker2/Config/dev-rohan-mac.ini |
|
|
13
14
|
| [ClickUp Work Type Automation (Committed / Conditional / Stretch)](features/clickup-work-type-automation.md) | The ClickUp webhook handler (`_Worker_Clickup`) automatically maintains each task's **Work Type** custom field — `Committed`, `Conditional`, or `Stretch` — base | worker2/Worker/Clickup.php, worker2/Tests/Worker/ClickupWorkTypeTest.php |
|
|
14
15
|
| [Compass VIP Support Importer (worker2)](features/compass-vip-support-importer.md) | A worker2 action that ingests Compass's quarterly VIP spreadsheet and assigns each VIP user's support technician by setting `Users.c_supportedByUserId` in `Clie | worker2/Worker/Client/Compass/VipSupport.php |
|
|
15
16
|
| [Creating Worker Actions](features/creating-worker-actions.md) | How to add a new callable Worker action — a PHP class whose `public static` methods are invoked as background jobs (via webhook, cron, or `_Worker::runTask()`). | worker2/Worker/, worker2/Controller/Index.php, _underscore/Worker.php |
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ClickUp Task Description Fluffer (Talos KB Investigation)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-20
|
|
10
|
+
owners: [rgirish]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Clickup/Fluffer.php
|
|
13
|
+
- worker2/Worker/Clickup.php
|
|
14
|
+
- worker2/Config/production.ini
|
|
15
|
+
- worker2/Config/beta.ini
|
|
16
|
+
- worker2/Config/dev-rohan-mac.ini
|
|
17
|
+
related:
|
|
18
|
+
- ./clickup-project-routing.md
|
|
19
|
+
- ./clickup-richtext-api.md
|
|
20
|
+
- ./talos-transcript-ingestion.md
|
|
21
|
+
- ./creating-worker-actions.md
|
|
22
|
+
- ../architecture.md
|
|
23
|
+
- ../../talos/features/talos-agent.md
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Summary
|
|
27
|
+
|
|
28
|
+
`_Worker_Clickup_Fluffer` "fluffs" a ClickUp task by sending its name + existing description
|
|
29
|
+
to the **Talos `dev-core` KB-investigation agent** and writing the returned Markdown report
|
|
30
|
+
back onto the task's `markdown_content`. It runs automatically on every ClickUp
|
|
31
|
+
`taskCreated` webhook and on demand via the `Clickup/Fluffer/Process` action. It is
|
|
32
|
+
**async by design** — the ~3-4 minute Talos call runs as its own worker-tier job and never
|
|
33
|
+
blocks the webhook response.
|
|
34
|
+
|
|
35
|
+
This is TOGA-internal team tooling (ClickUp + Talos), not client-specific.
|
|
36
|
+
|
|
37
|
+
## Key files / entry points
|
|
38
|
+
|
|
39
|
+
- `Worker/Clickup/Fluffer.php` — `abstract class _Worker_Clickup_Fluffer`; action method
|
|
40
|
+
`public static function Process(string $taskId, bool $force = false): string`. Invoked as
|
|
41
|
+
action path `Clickup/Fluffer/Process`.
|
|
42
|
+
- `Worker/Clickup.php` — the `taskCreated` case (~line 787) enqueues the fluffer.
|
|
43
|
+
- `Config/*.ini` `[talos]` section — the **new** `agent_endpoint` + `agent_api_key` keys
|
|
44
|
+
(see Configuration; `beta.ini`'s `[talos]` section was newly added this session).
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
### Enqueue (async)
|
|
49
|
+
`taskCreated` enqueues via
|
|
50
|
+
`_Worker::runTask('Clickup/Fluffer/Process', ['taskId' => $payload->task_id])`. In
|
|
51
|
+
production/beta, `_Worker::runTask` (`_underscore/Worker.php`) does **no MySQL** — it just
|
|
52
|
+
sends the job to SQS and returns immediately, so the fluffer runs later as its own
|
|
53
|
+
worker-tier job and the long Talos call never blocks the webhook response. Only local debug
|
|
54
|
+
mode (`_Environment::isDebugMode()`) runs `runTask` synchronously via a direct HTTP POST to
|
|
55
|
+
the worker.
|
|
56
|
+
|
|
57
|
+
### The Talos agent call
|
|
58
|
+
Uses `_ApiRequest` (not the ClickUp component) for the Talos call:
|
|
59
|
+
`POST https://api.togaiq.com/api/ai/agent`, body:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"assistant_slug": "dev-core",
|
|
64
|
+
"stream": false,
|
|
65
|
+
"format": "json",
|
|
66
|
+
"tool_categories": ["knowledge_base"],
|
|
67
|
+
"messages": [{"role": "user", "content": "<prompt: task name + existing description>"}]
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`_ApiRequest` settings: `setTimeout(280)`, `setConnectTimeout(20)`, `setLogging(false)`,
|
|
72
|
+
`throwExceptionsOnFailure = false`; header **`X-API-Key`** (NOT `Authorization: Bearer`);
|
|
73
|
+
`addHeader('Expect','')` to avoid 100-continue flakiness. The agent returns a Markdown
|
|
74
|
+
`## Talos KB Investigation` report.
|
|
75
|
+
|
|
76
|
+
- **`/api/ai/agent` is a DIFFERENT endpoint from the existing `/api/ai/generate`** used by
|
|
77
|
+
the Transcripts/Sprint features, and it uses a **different key** scoped to the `dev-core`
|
|
78
|
+
assistant slug. Both live in **new** `[talos]` INI keys `agent_endpoint` + `agent_api_key`,
|
|
79
|
+
separate from the existing `api_key`/`endpoint`. Read via
|
|
80
|
+
`_Config::talos('agent_endpoint')` / `_Config::talos('agent_api_key')` — never hardcode.
|
|
81
|
+
- The agent endpoint requires the `feature/api-key-agent-bridge` branch deployed to prod
|
|
82
|
+
(verified live this session — prod returned HTTP 200).
|
|
83
|
+
|
|
84
|
+
### Writing back to the task
|
|
85
|
+
Uses `_Component_Api_Clickup::send('GET'|'PUT', '/task/'.$taskId, $payload)` for ClickUp
|
|
86
|
+
reads/writes (the shared component owns the ClickUp token). The report is written to the
|
|
87
|
+
task's `markdown_content`, which ClickUp renders as rich text server-side.
|
|
88
|
+
|
|
89
|
+
### Idempotency & layout
|
|
90
|
+
A styled header line
|
|
91
|
+
`> **_🤖 Generated by Talos KB Investigation — do not edit above this line manually._**`
|
|
92
|
+
(marker substring **"Generated by Talos"**) leads the generated block; the end sentinel
|
|
93
|
+
**"— end of Talos KB Investigation —"** closes it. Layout order:
|
|
94
|
+
|
|
95
|
+
1. header line
|
|
96
|
+
2. Talos report
|
|
97
|
+
3. end sentinel
|
|
98
|
+
4. preserved human-authored text (everything AFTER the end sentinel)
|
|
99
|
+
|
|
100
|
+
`stripTalosBlock()` keeps the portion after the end sentinel, so human edits below the
|
|
101
|
+
sentinel survive regeneration. The auto path **skips** (returns `"skipped: already fluffed"`)
|
|
102
|
+
if the marker substring is already present; `force=true` regenerates.
|
|
103
|
+
|
|
104
|
+
### Rate limiting
|
|
105
|
+
Reuses the shared constant `_Worker_Clickup_Project::RATE_LIMIT_DELAY_US` (120000 µs = 120ms
|
|
106
|
+
→ 500 ClickUp calls/min) via `usleep()` after **each** ClickUp `send()` call — the same
|
|
107
|
+
convention `Project.php` and `Subtask.php` use, so all Clickup workers cooperate on one
|
|
108
|
+
ClickUp rate budget.
|
|
109
|
+
|
|
110
|
+
## Configuration
|
|
111
|
+
|
|
112
|
+
`worker2/Config/{production,beta,dev-rohan-mac}.ini` `[talos]` section adds:
|
|
113
|
+
|
|
114
|
+
- `agent_endpoint` — the `/api/ai/agent` URL.
|
|
115
|
+
- `agent_api_key` — the `dev-core`-scoped key.
|
|
116
|
+
|
|
117
|
+
Both are separate from the existing `[talos]` `api_key`/`endpoint` (used by
|
|
118
|
+
Transcripts/Sprint). Per existing repo convention the secret lives in the committed INI;
|
|
119
|
+
read it via `_Config::talos(...)`, never hardcode. Documented here by key name/account only,
|
|
120
|
+
never by value.
|
|
121
|
+
|
|
122
|
+
## Gotchas / known issues
|
|
123
|
+
|
|
124
|
+
- **Watchdog race.** The fluffer is an **ACTION** job; its watchdog is the Lambda env
|
|
125
|
+
`MAX_EXECUTION_TIME_WORKER_ACTION` (default 300s). The Talos timeout is set to **280** (just
|
|
126
|
+
under 300) so the job doesn't lose the race against its own watchdog out of the box.
|
|
127
|
+
Recommend raising `MAX_EXECUTION_TIME_WORKER_ACTION` to ~420s so slow investigations
|
|
128
|
+
complete.
|
|
129
|
+
- **`_Component_Api_Clickup::send()` fails in a bare CLI harness with a misleading error.**
|
|
130
|
+
`send()` leaves `_ApiRequest` logging **ON** by default, so `execute()` tries to write an
|
|
131
|
+
api-log row to the Client Logs DB (alias resolves to database name `ClientLogs`) **before**
|
|
132
|
+
the curl call. In a bare CLI harness (or the legacy `action` dispatch path) where the logs
|
|
133
|
+
DB isn't registered, this throws `Unknown database 'ClientLogs'` and surfaces as an
|
|
134
|
+
empty-code `API Error … (HTTP ): no error detail` — even though the ClickUp call would have
|
|
135
|
+
succeeded. The production `workerJobId` dispatch path (`Controller/Index.php`) registers
|
|
136
|
+
`DB_LOGS` + `DB_CLIENT_LOGS` before running the action, so `send()`'s logging works there.
|
|
137
|
+
**Workaround for manual/CLI testing:** call the ClickUp API via `_ApiRequest` with
|
|
138
|
+
`setLogging(false)`, or register the logs DBs first. This is a test-harness gotcha, not a
|
|
139
|
+
code bug. (Same DB-registration class of issue as the worker-action gotchas in
|
|
140
|
+
[Creating Worker Actions](./creating-worker-actions.md).)
|
|
141
|
+
- **Distinct Talos endpoint/key.** Do not reuse the Transcripts/Sprint `[talos]` `api_key` /
|
|
142
|
+
`endpoint` for the agent — `/api/ai/agent` needs the `dev-core`-scoped `agent_api_key`, and
|
|
143
|
+
the header is `X-API-Key`, not `Authorization: Bearer`.
|
|
144
|
+
|
|
145
|
+
## Change history
|
|
146
|
+
|
|
147
|
+
- 2026-07-20 — Built the ClickUp task description fluffer: new `_Worker_Clickup_Fluffer`
|
|
148
|
+
(action `Clickup/Fluffer/Process`, `taskCreated` webhook auto-trigger, `force` regenerate)
|
|
149
|
+
calling the Talos `dev-core` KB-investigation agent at `/api/ai/agent` via `_ApiRequest`
|
|
150
|
+
(`X-API-Key`, `Expect:` cleared, 280s timeout). Report written to ClickUp
|
|
151
|
+
`markdown_content` with a "Generated by Talos" marker + end sentinel for idempotency
|
|
152
|
+
(human text below the sentinel is preserved). New `[talos]` `agent_endpoint`/`agent_api_key`
|
|
153
|
+
keys (beta.ini `[talos]` section newly added). Requires `feature/api-key-agent-bridge` in
|
|
154
|
+
prod (verified HTTP 200). Documented the `_Component_Api_Clickup::send()` CLI logging-DB
|
|
155
|
+
gotcha (`Unknown database 'ClientLogs'`). (rgirish)
|
|
156
|
+
</content>
|
|
@@ -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-20
|
|
10
10
|
owners: [snaredla, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Vapi.php
|
|
@@ -14,6 +14,8 @@ files:
|
|
|
14
14
|
related:
|
|
15
15
|
- ../../ai-bdr/features/call-orchestration.md
|
|
16
16
|
- ../../ai-bdr/features/vapi-integration.md
|
|
17
|
+
- ../../ai-bdr/workflows/safe-call-loop-testing.md
|
|
18
|
+
- ../../api2/features/nested-relationship-writes.md
|
|
17
19
|
---
|
|
18
20
|
|
|
19
21
|
## Summary
|
|
@@ -152,6 +154,34 @@ truth for **"why isn't my contact being called."** Confirmed against source
|
|
|
152
154
|
`initiateCallForContact(…, CALL_TYPE__IMMEDIATE)`. It errors **"not assigned to any active
|
|
153
155
|
campaign"** if no ACTIVE/PENDING campaign-contact link exists.
|
|
154
156
|
|
|
157
|
+
### Finer detail (source-verified 2026-07-20)
|
|
158
|
+
|
|
159
|
+
Where each gate actually lives in `Worker/Ai/Bdr/Vapi.php`, and where the eligibility summary
|
|
160
|
+
above is easy to over-read:
|
|
161
|
+
|
|
162
|
+
- **The campaign gate is split across two methods.** `getActiveCampaignsForDialing` (L177–193)
|
|
163
|
+
filters **only** `Campaigns.isActive = 1` + `INNER JOIN CampaignAssistants` — there is **no
|
|
164
|
+
date check there**. The date-range gate lives in `validateCampaignReady` (L759–789), which
|
|
165
|
+
throws **"Campaign has ended / not started"** and separately requires the `campaignAssistantId`
|
|
166
|
+
to resolve to a **NON-EMPTY `assistantIdentifier`**. So an active campaign with a valid assistant
|
|
167
|
+
can still be rejected every run purely because `dateEnd`/`dateStart` is out of range.
|
|
168
|
+
- **`Campaigns_Contacts.status IN ('PENDING','ACTIVE')` is required ONLY when
|
|
169
|
+
`dtNextContactRequested IS NULL`** (L237). On a requested (web-funnel call-now/scheduled) contact
|
|
170
|
+
the status is not checked. **`PENDING` is a NORMAL dialable state** — the webhook resets links to
|
|
171
|
+
`PENDING` for retries, so "stuck on PENDING" is **not** a blocker signal.
|
|
172
|
+
- **The prior-DNC / terminal-outcome exclusion is BYPASSED when `dtNextContactRequested` is set.**
|
|
173
|
+
The exclusion (L314–331: `c_dncRequested`, `DO_NOT_CALL`, `MEETING_BOOKED` / `DNC_REQUESTED`
|
|
174
|
+
outcomes) applies only on the non-requested path; a customer who actively re-engages
|
|
175
|
+
(`dtNextContactRequested`) is dialed regardless of a prior terminal outcome.
|
|
176
|
+
- **The normal (non-requested) path requires a `CampaignCallWindows` row to EXIST for the current
|
|
177
|
+
day/time.** A campaign with **no** call-window rows can therefore **only ever dial contacts with a
|
|
178
|
+
pending `dtNextContactRequested`** — this is the **safest launch posture for a web-funnel-only
|
|
179
|
+
campaign** (see the safe call-loop testing workflow in
|
|
180
|
+
`../../ai-bdr/workflows/safe-call-loop-testing.md`).
|
|
181
|
+
- **Per-run cap + in-progress exclusion.** Results are `LIMIT MAX_CALLS_PER_MINUTE` per run, and the
|
|
182
|
+
in-progress check excludes any contact with an **open attempt** (`dtStarted` set, `dtEnded NULL`)
|
|
183
|
+
for that campaign — see the dryRun gotcha below for how a stray open attempt wedges a contact.
|
|
184
|
+
|
|
155
185
|
### Gotcha — a phone-less contact is silently never dialed (BDR web funnel)
|
|
156
186
|
|
|
157
187
|
Criterion 4's `INNER JOIN` on `primaryContactPhoneNumberId` is why a contact whose phone was
|
|
@@ -160,10 +190,33 @@ added **after** creation can be invisible to the dialer: **api2 creates the
|
|
|
160
190
|
(it only links the primary on CREATE). The BDR web funnel hits this for any phone-less HubSpot
|
|
161
191
|
contact — the visitor's typed phone arrives via a later `PUT /contacts/{uuid}`, so the row
|
|
162
192
|
exists but the primary pointer stays null and the eligibility join excludes it. Re-submitting
|
|
163
|
-
does not fix it (verified in production).
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
193
|
+
does not fix it (verified in production). The **exact api2 root cause** is now documented — the
|
|
194
|
+
UPDATE path injects a reverse back-reference into the nested child object, defeating the
|
|
195
|
+
single-key-uuid forced-MATCH fast-path (see
|
|
196
|
+
[api2 nested-relationship-writes.md](../../api2/features/nested-relationship-writes.md)). The
|
|
197
|
+
platform fix is deferred; a **funnel-side self-heal has SHIPPED** (`bdr/src/server/toga.ts`):
|
|
198
|
+
after any contact write that carried a phone, if the echoed `primaryContactPhoneNumber` is null or
|
|
199
|
+
its last-10 digits don't match what was sent, BDR finds/creates the `ContactPhoneNumber` row uuid
|
|
200
|
+
and re-links it with an identifier-only `PUT {primaryContactPhoneNumber:{uuid}}`. So on the funnel
|
|
201
|
+
side this is resolved once the fix deploys — see `../../ai-bdr/features/web-funnel-app.md`.
|
|
202
|
+
|
|
203
|
+
### Gotcha — `dryRun` is NOT write-free; it wedges contacts (never dry-run in prod)
|
|
204
|
+
|
|
205
|
+
`createContactAttempt` (which sets `dtStarted = NOW()`, `dtEnded NULL`) executes **BEFORE** the
|
|
206
|
+
`dryRun` early-return (L438–466 / L685–708). So a "dry run" writes a **real `ContactAttempts` row
|
|
207
|
+
for every eligible contact** — it counts toward `maxAttemptsPerContact` **and** wedges the contact
|
|
208
|
+
as "in progress" (the open-attempt check above then excludes it from **all** future dialing) until
|
|
209
|
+
someone sets `dtEnded`. **Never dry-run against production.** Fix candidate: move
|
|
210
|
+
`createContactAttempt` below the `dryRun` return (build the payload with a placeholder uuid).
|
|
211
|
+
Unwedge SQL: `UPDATE ContactAttempts SET dtEnded=NOW() WHERE contactId=? AND dtEnded IS NULL`.
|
|
212
|
+
|
|
213
|
+
### Gotcha — a corrupt `assistantIdentifier` passes validation but 400s at Vapi
|
|
214
|
+
|
|
215
|
+
`validateCampaignReady` only checks that `assistantIdentifier` is **non-empty**, not that it is a
|
|
216
|
+
well-formed Vapi assistant UUID. A corrupt identifier (e.g. a valid uuid with an extra trailing
|
|
217
|
+
character — a 37-char value) **passes** the readiness gate and the campaign is dialed, then the
|
|
218
|
+
Vapi `POST /call/phone` **400s** at call time. Validate the assistant identifier's shape before
|
|
219
|
+
assigning an assistant to any campaign.
|
|
167
220
|
|
|
168
221
|
## Data model
|
|
169
222
|
|
|
@@ -192,6 +245,18 @@ status. See the `ai-bdr` skill / `_Model_True_ContactAttempt` for the full field
|
|
|
192
245
|
- [VAPI Integration — assistants, tools, structured output](../../ai-bdr/features/vapi-integration.md)
|
|
193
246
|
|
|
194
247
|
## Change history
|
|
248
|
+
- 2026-07-20 — Source-verified corrections/extensions to the dialer eligibility rules: the campaign
|
|
249
|
+
gate is split (`getActiveCampaignsForDialing` L177–193 = isActive+assistant, NO date; date range +
|
|
250
|
+
non-empty `assistantIdentifier` in `validateCampaignReady` L759–789); `Campaigns_Contacts.status
|
|
251
|
+
IN (PENDING,ACTIVE)` required ONLY when `dtNextContactRequested IS NULL` (PENDING is normal/dialable);
|
|
252
|
+
prior-DNC/terminal-outcome exclusion is bypassed on the requested path; a campaign with NO
|
|
253
|
+
`CampaignCallWindows` rows can only dial requested contacts (safest web-funnel launch posture);
|
|
254
|
+
per-run `LIMIT MAX_CALLS_PER_MINUTE` + open-attempt exclusion. Added gotchas: (1) `dryRun` is NOT
|
|
255
|
+
write-free — `createContactAttempt` runs before the dryRun return, writing a real ContactAttempts
|
|
256
|
+
row that counts toward the attempt cap AND wedges the contact as in-progress (never dry-run in
|
|
257
|
+
prod; unwedge SQL provided); (2) a corrupt `assistantIdentifier` passes the non-empty check but
|
|
258
|
+
400s at Vapi. Updated the phone-less-contact gotcha: exact api2 root cause now documented and a
|
|
259
|
+
funnel-side self-heal (`bdr/src/server/toga.ts`) has shipped. (tcox)
|
|
195
260
|
- 2026-07-16 — Documented the outbound dialer eligibility rules from source
|
|
196
261
|
`getEligibleContactsForVapi()` (the 6 ALL-of criteria, the `dtNextContactRequested <= NOW()`
|
|
197
262
|
window/cadence bypass = the website call-now/scheduled path, cron entry chain, and the single
|
package/knowledge/INDEX.md
CHANGED
|
@@ -18,7 +18,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
18
18
|
## 2.0 framework
|
|
19
19
|
|
|
20
20
|
- **_underscore** (_Underscore) _(framework core)_ — 35 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
21
|
-
- **worker2** (Worker) —
|
|
21
|
+
- **worker2** (Worker) — 31 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
22
22
|
- **api2** (API) — 12 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
23
23
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
24
24
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
package/package.json
CHANGED