toga-ai 1.0.482 → 1.0.483

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.
@@ -0,0 +1,201 @@
1
+ ---
2
+ type: session
3
+ slug: bdr-live-call-status-and-callback-bug
4
+ title: BDR funnel live call status + real summary email, and AI-BDR callback root cause
5
+ author: tcox
6
+ repos: [bdr, worker2]
7
+ framework: "2.0"
8
+ client: shared
9
+ status: active
10
+ created: 2026-07-30
11
+ updated: 2026-07-30
12
+ ---
13
+
14
+ # Session: bdr-live-call-status-and-callback-bug
15
+ **Date:** 2026-07-30
16
+ **Project/Repo:** bdr (2.0), plus a read-only worker2 investigation
17
+ **Task:** Make the BDR funnel usable by anyone (remove the contact-reference gate), drive the Success screen from real call data instead of a timed simulation, send the call summary by real email, and then root-cause a production bug where a callback requested during an AI-BDR call never happens.
18
+
19
+ ---
20
+
21
+ ## What WORKED
22
+
23
+ - **Contact-reference gate removed.** Deleted the `hasLead` submit-block and the
24
+ `phone.missingContactNotice` copy key from `src/flow/screens/CallNow.tsx`,
25
+ `src/flow/screens/Schedule.tsx`, `src/content/schema.ts`, `src/content/default.ts`.
26
+ Evidence: grep for `hasLead|missingContactNotice` across the repo returns zero matches;
27
+ design-parity audit against `C:\WWW\BDR\mockup` confirmed the removal restored mockup
28
+ parity (the mockup never had the notice).
29
+ - **Vapi's own API confirmed as a working live call-status source.** `GET https://api.vapi.ai/call`
30
+ returned HTTP 200 with the org's calls. Verified field shape: `id, status, type, createdAt,
31
+ startedAt, endedAt, endedReason, summary, transcript, recordingUrl, customer, analysis,
32
+ messages, cost`. Status enum `queued|ringing|in-progress|forwarding|ended`. Timestamps are
33
+ ISO-8601 UTC with `Z`. **Key fact: `createdAt` = placed, `startedAt` = ANSWERED,
34
+ `endedAt` = hangup.** A real call showed `createdAt 18:53:08 / startedAt 18:53:18 /
35
+ endedAt 18:53:35, endedReason "voicemail"` — the 10s gap is ring time.
36
+ - **Fixed the worker dispatcher reply-prefix bug** in `src/server/workerDialer.ts`. The worker
37
+ wraps every reply as `[<requestId>] Successfully Executed (<n> seconds): <json>`, so the old
38
+ `response.json()` always threw: it logged "immediate dial not placed" on every SUCCESSFUL
39
+ dial and discarded the response body. Now strips the prefix before parsing and returns a
40
+ typed `DialResult` carrying `vapiCallId` / `attemptId`. Evidence: regression test asserts a
41
+ dispatcher-wrapped success reply no longer logs the false warning.
42
+ - **Live call lifecycle on the Success screen.** New `/api/call-status` route, `vapiCall.ts`
43
+ server reader, `useCallStatusViewModel` polling hook. Mapping: `queued`/`ringing` -> dialing
44
+ with NO talk timer; `in-progress`/`forwarding` with `startedAt` -> active, timer counted from
45
+ the ANSWER time with server-clock-offset correction; `ended` -> frozen true talk duration;
46
+ ended with no `startedAt` or a voicemail/no-answer/busy reason -> a distinct not-answered
47
+ state that shows no timer and suppresses the recap card. The old simulation is retained
48
+ verbatim as the fallback whenever status is unavailable.
49
+ - **Post-hangup summary grace.** Vapi writes `summary` asynchronously after hangup, so the
50
+ first ended poll is usually `summary: null`. Added a bounded 30s / 2s-cadence grace that
51
+ keeps polling after ended until the real summary lands, with the screen already showing the
52
+ ended state. Frozen duration provably unchanged across the window (tested).
53
+ - **Real email send of the call summary.** New `/api/share-summary` route + `src/server/mailer.ts`
54
+ (nodemailer) + shared `src/lib/summaryText.ts`, so the Copy button and the emailed body are
55
+ byte-identical by construction. The client may only supply recipient + campaign id + bounded
56
+ variant index + signed token; never body or subject text.
57
+ - **mailto fallback removed entirely** (developer decision mid-session). `src/lib/shareMailto.ts`
58
+ and its test deleted; Send now only ever reaches sent or failed-with-retry.
59
+ - **Security hardening after the CSO audit returned BLOCK** (it flagged an unauthenticated
60
+ arbitrary-recipient mailer). Applied: purpose-scoped HMAC tokens (`status` vs `share`), token
61
+ moved from the URL query string to an `x-status-token` header, a valid share token now
62
+ REQUIRED for every send, Content-Type and `sec-fetch-site` checks, strict recipient validation
63
+ (rejects angle-bracket display-name form, commas, all non-ASCII), SMTP `requireTLS` + TLS 1.2
64
+ + timeouts, `Cache-Control: no-store, private` on every envelope, exact ACL error-code matching
65
+ that fails CLOSED on an unknown 403, upstream AbortSignal timeouts, rejection of future-dated
66
+ token issuance. The Vapi call id travels only inside signed claims, never as a client param.
67
+ - **Final verification:** `npx tsc --noEmit` clean, `npm run lint` clean, `npm test`
68
+ **306/306 passing across 20 files**.
69
+ - **Credentials located and wired** (locations only): 2.0 SMTP settings are hardcoded constants
70
+ in `_underscore/Email.php` (SES us-west-2, port 587, TLS); the Vapi bearer credential is a
71
+ hardcoded constant in `worker2/Worker/Ai/Bdr/Vapi.php`. BDR consumes both as server-only env
72
+ vars `TOGA_SMTP_HOST/PORT/USER/PASS`, `TOGA_EMAIL_FROM`, `TOGA_VAPI_TOKEN` in the gitignored
73
+ `.env.local` (documented in `.env.example`).
74
+ - **Two knowledge captures pushed:** `6ed04d3` (8 docs — live-call-status, the api2 v2 query
75
+ contract, and updates) and `968404e` (3 docs — the callback root cause, with `bala` added as
76
+ co-owner for handoff).
77
+ - **Callback bug root-caused with empirical proof.** Extracted worker2's `convertDateTime` and
78
+ executed it on PHP 8.1.10: 22 of 32 realistic phrasings return null.
79
+
80
+ ## What did NOT work — DO NOT RETRY THESE
81
+
82
+ - **Reading `ContactAttempts` through api2 — HTTP 403 `EZ-1`, record-level block.** Re-verified
83
+ live on 2026-07-30 against `api.togahub.com` with the BDR client credential, on ALL variants:
84
+ nested `/contacts/{uuid}/contact-attempts`, flat `/contact-attempts`, both with and without an
85
+ explicit `fields=` list. Response is `data.contactAttempts: null`. The pending read-only ACL
86
+ grant has still NOT landed. Do not build anything against this resource until it does.
87
+ - **Selecting `Contacts.id`** — `/contacts/{uuid}?fields=id` returns 403 `EZ-2` with
88
+ `identifiers.fields: ["id"]`. The client has no field grant on `Contacts.id`. Filter on
89
+ `Contacts.uuid` instead (api2 does not ACL-check `where`/`join`, only `fields`).
90
+ - **Direct production database access from this laptop.** TCP 3306 to the
91
+ `*.client.database.togahub.com` writer host **times out** (VPN required). This machine also
92
+ has no mysql client, no PHP, and no Python (only the Windows Store stub). Do not plan work
93
+ that assumes direct SQL — hand the developer ready-to-run SQL with pre-flight reads instead.
94
+ - **Reading the api2 auth token from `data.token`** — wrong path, the response has no such key.
95
+ The access token is at **`data.tokens.access`**.
96
+ - **Sharing the raw Claude Code session transcript (`~/.claude/projects/c--WWW/<id>.jsonl`) for
97
+ handoff.** Verified it contains live credential VALUES in plaintext: SMTP password (5
98
+ occurrences), Vapi token (2), Toga client API secret (4), HubSpot access token (4). Use
99
+ `/session-save` + `/session-resume` and the knowledge docs for handoff instead.
100
+ - **(worker2, the production bug) `convertDateTime` parsing natural-language callback times.**
101
+ 22/32 phrasings return null on PHP 8.1.10. The trigger is the PREPOSITION/FILLER WORD, not
102
+ spelled-out numbers as previously recorded: `"10 minutes"` parses but `"in 10 minutes"` does
103
+ not; `"tomorrow 3pm"` parses but `"tomorrow at 3pm"` does not — and that is the exact example
104
+ the code comment claims works.
105
+
106
+ ## Not tried yet (candidates for next session)
107
+
108
+ - A **live end-to-end call test** of the new UI. Not possible yet: campaign 2's `dateEnd` lapsed
109
+ on 2026-07-28 and the re-activation SQL had not been run at save time.
110
+ - **Verifying a real SMTP send.** Credentials are wired but no successful delivery has been
111
+ observed; `TOGA_EMAIL_FROM=DevTeam@goagilant.com` may or may not be an SES-verified identity.
112
+ - **Running Q1-Q5 from `2.0/apps/worker2/features/callback-scheduling.md`** against `Client_True`
113
+ to confirm the callback root cause with production data. Q1 is decisive.
114
+ - **Fixing any of the worker2 callback defects** — this session was diagnosis only; no worker2
115
+ code was modified. bala is picking this up.
116
+ - **Adding the `TOGA_*` env vars to the Amplify console** so the deployed site gets live status
117
+ and email (local `.env.local` only affects dev).
118
+ - **Binding the email recipient to the contact's on-file address** — the stricter anti-abuse
119
+ option; currently a token holder can email an arbitrary recipient (accepted residual risk).
120
+ - **Splitting BDR knowledge out of `2.0/apps/ai-bdr/` into its own `2.0/apps/bdr/` folder with an
121
+ `architecture.md`.** Flagged twice; an elevated call that needs a deliberate decision.
122
+
123
+ ## Current file state
124
+
125
+ All BDR changes are **uncommitted on branch `TRUE-80435`** (working tree only — nothing was
126
+ committed or pushed all session, per the git-workflow rule).
127
+
128
+ | File | Status | Notes |
129
+ |------|--------|-------|
130
+ | `bdr src/flow/screens/CallNow.tsx` | modified | gate + notice removed; submit enables on the digit rule alone |
131
+ | `bdr src/flow/screens/Schedule.tsx` | modified | same removal |
132
+ | `bdr src/flow/screens/Success.tsx` | modified | live stages via a kind-keyed lookup; simulation path preserved verbatim |
133
+ | `bdr src/flow/screens/CallSummary.tsx` | modified | async send with sending/sent/failed; real-summary rendering; no mailto |
134
+ | `bdr src/flow/useCallStatusViewModel.ts` | new | poll loop + pure reducer, cadence, caps, summary grace |
135
+ | `bdr src/flow/callStatusApi.ts` | new | client fetcher, sends the token in a header |
136
+ | `bdr src/flow/shareSummaryApi.ts` | new | maps send outcomes to sent/unavailable/failed |
137
+ | `bdr src/flow/leadActions.ts` | modified | returns a handle `{ok, uuid?, statusToken?, shareToken?}`; `simulated` flag |
138
+ | `bdr src/flow/useAgentFlow.ts` | modified | stores the call handle under the submitGen guard |
139
+ | `bdr src/flow/AgentFlowRoot.tsx` | modified | passes the handle to calling-mode Success only |
140
+ | `bdr src/server/vapiCall.ts` | new | reads `GET api.vapi.ai/call/{id}`; maps to the CallStatus union |
141
+ | `bdr src/server/callStatus.ts` | modified | ContactAttempts path retained as the (403) fallback |
142
+ | `bdr src/server/statusToken.ts` | new | purpose-scoped HMAC, TTL, skew, optional `vapiCallId` claim |
143
+ | `bdr src/server/mailer.ts` | new | nodemailer, requireTLS, timeouts, scrubbed logs |
144
+ | `bdr src/server/workerDialer.ts` | modified | **prefix bug fixed**; returns `DialResult` with `vapiCallId` |
145
+ | `bdr src/server/callbackService.ts` | modified | surfaces `vapiCallId` |
146
+ | `bdr src/server/toga.ts` | modified | added `togaApiGetEnvelope` (needed to read EZ-1/EZ-2 out of a 403) |
147
+ | `bdr src/lib/summaryText.ts` | new | single renderer shared by Copy and the email |
148
+ | `bdr src/lib/emailFormat.ts` | new | strict recipient validation |
149
+ | `bdr src/lib/callStatus.ts` | new | shared CallStatus union + header constant |
150
+ | `bdr src/app/api/call-status/route.ts` | new | token-gated, never 500s, `no-store` |
151
+ | `bdr src/app/api/share-summary/route.ts` | new | share-token required, server-built content only |
152
+ | `bdr src/app/api/call-now/route.ts` | modified | mints both tokens with the `vapiCallId` claim |
153
+ | `bdr src/lib/shareMailto.ts` | **deleted** | mailto fallback removed by developer decision |
154
+ | `bdr src/content/schema.ts` + `default.ts` | modified | live-stage + send-state copy; notice key removed |
155
+ | `bdr src/proxy.ts` | modified | matcher + limiters for the two new routes |
156
+ | `bdr .env.example` | modified | documents SMTP + `TOGA_VAPI_TOKEN` |
157
+ | `bdr .env.local` | modified | real values added locally (gitignored) |
158
+ | `bdr package.json` | modified | added `nodemailer` + `@types/nodemailer` |
159
+ | worker2 / _underscore / api2 | **unmodified** | read-only investigation only |
160
+
161
+ ## Decisions made
162
+
163
+ - **Vapi's API is the live call-status source, not `ContactAttempts`.** Rationale: ContactAttempts
164
+ is 403-blocked AND structurally inferior — `dtStarted` is DIAL time, so it cannot distinguish
165
+ ringing from talking, and `dtStarted` is retroactively rewritten by the webhook. Vapi exposes
166
+ ringing vs answered vs ended plus a real summary. It stays the better source even after the
167
+ grant lands. Rejected: waiting for the ACL grant; continuing to simulate.
168
+ - **The backend contract is unchanged for anonymous submits** (developer decision). The funnel
169
+ loads and accepts a phone number for anyone, but calls still only fire for campaign-linked
170
+ contacts, matching the original BDR backend. Rejected: server-side anonymous contact creation
171
+ (would have required a DNC pre-check that the 403 blocks anyway).
172
+ - **The mailto fallback was removed entirely** (developer decision). Consequence, accepted: with
173
+ SMTP unconfigured the Send button can only reach the failed state.
174
+ - **Token purpose-scoping and header transport**, adopted in response to the CSO BLOCK verdict.
175
+ Rejected: leaving one token to serve both read and send, and leaving it in the query string
176
+ (a 30-minute bearer capability over a named individual's call summary would land in access logs).
177
+ - **Accepted residual risk:** a token holder can email the summary to any address they type
178
+ (preserves the share-with-a-colleague use case). The stricter alternative is on the not-tried list.
179
+ - **BDR knowledge stays filed under `2.0/apps/ai-bdr/`** rather than restructuring mid-capture.
180
+
181
+ ## Blockers
182
+
183
+ - **`ContactAttempts` read-only ACL grant still pending** (403 EZ-1). Blocks only the fallback
184
+ path — the Vapi path works without it. Note for whoever files it: the `c_*` fields are granted
185
+ through `AclCustomFieldPermissions`, a DIFFERENT table from the standard fields.
186
+ - **Campaign 2 (`26.05 - AI BDR - Ryan Nitti`, id 2) `dateEnd` lapsed 2026-07-28**, so no calls
187
+ place at all until it is extended. SQL was handed to the developer; the production DB is not
188
+ reachable from this machine.
189
+ - **No live end-to-end verification yet** of the call UI or a real email delivery.
190
+
191
+ ## Exact next step
192
+
193
+ > In `Client_True`, run the campaign-2 re-activation pre-flight reads first (confirm the dialable
194
+ > count on campaign 2 is 0 or 1 and that contact 248 has no open `ContactAttempts` row with
195
+ > `dtEnded IS NULL`), then flip all campaign-2 contacts except 248 to `isOkayToCall = 0` and
196
+ > extend `Campaigns.dateEnd`. Then restart `npm run dev` in `C:\WWW\BDR`, open the funnel from
197
+ > the `?hsContactId=` link, tap Call Now, and confirm the Success screen shows Ringing with no
198
+ > timer, the timer starting at 00:00 on answer, and Call ended with the real Vapi summary.
199
+
200
+ ---
201
+ _Saved by /session-save on 2026-07-30_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.482",
3
+ "version": "1.0.483",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",