Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions .cursor/skills/verify-call-agent/features/accounts.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
# Accounts (`users` row)

The `users` table is the runtime account record. Kickoff looks it up by inbound phone. Stripe stays checkout and billing. call-agent-web POSTs a signed body to Fly `POST /accounts/sync`; Fly is the sole writer. Those Fly routes (`POST /accounts/sync` and `GET /accounts/dashboard`) are intended, not accidental. Dashboard UI stays on Pages (web). CLI `account upsert --from-did` seeds a row, including the assigned Treena number, until that website PR lands. Name is collected on website `/start` and that copy is in settings after signup. Kickoff reads `users.first_name`. Never collected over iMessage. Display name is `first_name` + `prefs.full_name`. There is no `users.name` column. Signed `GET /accounts/dashboard` packages that row plus linq artifacts so the website does not open Neon or R2. Prove with `call-agent verify-accounts`. This skill never sets `NEON_DATABASE_URL` or `ACCOUNT_SYNC_SECRET`.
The `users` table is the runtime account record. Cap model: one shared Treena iMessage bot (`LINQ_FROM_E164`); one shared Telnyx FROM (`TELNYX_FROM_NUMBER` / `TELNYX_PHONE_NUMBER`); each account stores the user's own phone. Kickoff looks it up by inbound phone. Stripe stays checkout and billing. call-agent-web POSTs a signed body to Fly `POST /accounts/sync`; Fly is the sole writer. Those Fly routes (`POST /accounts/sync` and `GET /accounts/dashboard`) are intended, not accidental. Dashboard UI stays on Pages (web). CLI `account upsert` seeds a row until that website PR lands. `--from-did` still writes the legacy column; kickoff does not use it. Name is collected on website `/start` and that copy is in settings after signup. Kickoff reads `users.first_name`. Never collected over iMessage. Display name is `first_name` + `prefs.full_name`. There is no `users.name` column. Signed `GET /accounts/dashboard` packages that row, the sitewide `bot_number`, plus linq artifacts so the website does not open Neon or R2. Prove with `call-agent verify-accounts`. This skill never sets `NEON_DATABASE_URL` or `ACCOUNT_SYNC_SECRET`.

## Sub-features

- `users-row` one `users` row per phone: `first_name`, phone, assigned Treena `from_did`, minutes, retain, prefs, Stripe ids. No `users.name` column.
- `from-did` nullable E.164 Telnyx caller ID. Rejected when equal to `phone`. Additive `ALTER TABLE` on existing SQLite and Neon `users` tables.
- `users-row` one `users` row per phone: `first_name`, the user's own phone, legacy Telnyx `from_did` (not unique, not required at kickoff), minutes, retain, prefs, Stripe ids. No `users.name` column.
- `from-did` nullable legacy E.164. Not unique. Rejected when equal to `phone`. Additive `ALTER TABLE` on existing SQLite and Neon `users` tables. Not the shared iMessage bot and not the live Telnyx FROM.
- `shared-bot` `LINQ_FROM_E164` is the one Text Treena number (sandbox `+12138653024` until ~Sep 18). Dashboard `bot_number` is that key.
- `shared-telnyx-from` `TELNYX_FROM_NUMBER` / `TELNYX_PHONE_NUMBER` is the one Telnyx FROM (Cap `+12135690899`). Kickoff dials from that env, not `users.from_did`.
- `omit-preserve` omitted `first_name`, `prefs`, `minutes_used`, and `from_did` keep the existing values. Explicit `minutes_used` `0` writes 0.
- `account-upsert` CLI seed without HMAC. `--from-did` sets the Treena number. Omitted `--from-did` and `--name` keep existing values.
- `account-upsert` CLI seed without HMAC. `--phone` is the user's number. `--from-did` still writes the legacy column. Omitted `--from-did` and `--name` keep existing values.
- `account-get` read-back of that phone, including `from_did`.
- `account-list` every row (`USER PHONE NAME FROM_DID MINUTES RETAIN`).
- `accounts-sync` HMAC `POST /accounts/sync` upserts; Fly is the only writer. Omitted name/prefs/`minutes_used`/`from_did` keep seeded values.
- `accounts-dashboard` HMAC `GET /accounts/dashboard?phone=` packages `users` + linq thread index + retained `call_report`/transcript. Empty lists when unknown or wiped.
- `kickoff-lookup` inbound phone → `users.first_name`, `users.user_id`, and `users.from_did` for the call.
- `accounts-dashboard` HMAC `GET /accounts/dashboard?phone=` packages `users` + `bot_number` + linq thread index + retained `call_report`/transcript. Empty lists when unknown or wiped.
- `kickoff-lookup` inbound phone → `users.first_name` and `users.user_id`. Telnyx FROM is `TELNYX_FROM_NUMBER`.

## How to get to it (user POV)

- Run `call-agent account upsert --phone +14155552671 --name Nathan --from-did +15555550177`.
- Run `call-agent account get --phone +14155552671`.
- Run `call-agent account list`.
- Website checkout/settings POST to Fly `/accounts/sync` (intended Fly route). Until the writer is live, CLI seed is the L1 path. Name is collected on website `/start` and copied into settings after signup. The website is not expected to send `from_did` this climb. Stripe events without a name must omit `first_name`.
- Website dashboard UI stays on Pages. Fly `GET /accounts/dashboard?phone=` with the same HMAC family is the packaged read. Web must not open Neon/R2.
- Website dashboard UI stays on Pages. Fly `GET /accounts/dashboard?phone=` with the same HMAC family is the packaged read, including `bot_number`. Web must not open Neon/R2.

## Driving it with call-agent

Expand All @@ -39,9 +41,9 @@ Preconditions:

- SQLite `users` is the same file as `dids`. On Fly, set `NEON_DATABASE_URL` or the row dies on redeploy.
- `account upsert` does not need `ACCOUNT_SYNC_SECRET`. The HTTP routes do.
- Kickoff `BookRequest.user_id` is `users.user_id`. The outbound Telnyx caller ID is `users.from_did`. Confirm/dial never calls `ledger.provision`.
- `from_did` must not equal `phone`. Personal iMessage number is not the Treena from-number.
- Kickoff `BookRequest.user_id` is `users.user_id`. The outbound Telnyx caller ID is `TELNYX_FROM_NUMBER` / `TELNYX_PHONE_NUMBER`. Confirm/dial never calls `ledger.provision`. The iMessage bot is `LINQ_FROM_E164`. Kickoff ignores `users.from_did`.
- `from_did` must not equal `phone`. Personal number is not the Telnyx from-number. Two accounts may share one legacy `from_did`.
- Omitted `--from-did` / `--name` (and omitted `from_did` / `first_name` / `prefs` / `minutes_used` on `/accounts/sync`) keep existing values so Stripe sync cannot wipe an ops-seeded DID, name, or minutes count. Explicit `minutes_used` `0` is a write.
- Dashboard HMAC signs the E.164 `phone` query value, not the URL-encoded query string. Unsigned GET is 401. Unknown phone returns empty lists.
- Dashboard HMAC signs the E.164 `phone` query value, not the URL-encoded query string. Unsigned GET is 401. Unknown phone returns empty lists. `bot_number` still comes from `LINQ_FROM_E164`.
- Do not collect a name over iMessage. Name is collected on website `/start` and is in settings after signup. Do not set `LINQ_KICKOFF_CALLER` or `LINQ_KICKOFF_USER_ID`.
- This skill never POSTs `/accounts/sync` live, never GETs `/accounts/dashboard` live, and never deploys Fly.
4 changes: 2 additions & 2 deletions .cursor/skills/verify-call-agent/features/did-provision.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

- Run `call-agent did provision --user <user>` in a terminal.
- Run `call-agent did list` in a terminal.
- Confirm/dial does not provision. Live from-DID is `users.from_did` or CLI `--from-did`.
- Confirm/dial does not provision. Live from-DID is `TELNYX_FROM_NUMBER` / `TELNYX_PHONE_NUMBER` or CLI `--from-did`.

## Driving it with call-agent

Expand All @@ -32,4 +32,4 @@ Preconditions:
- A leftover ledger from a previous run makes the first provision return a number that is not `+15555550100`. Use a fresh `CALL_AGENT_DB_PATH`.
- Stub numbers are assigned in order of first provision, not by user name.
- Live provisioning orders a real number and costs money. This skill never runs it.
- Confirm/dial never calls `Ledger.provision`. `did provision` is an ops verb, not a live-call prerequisite.
- Confirm/dial never calls `Ledger.provision`. `did provision` is an ops verb, not a live-call prerequisite. Cap account model does not assign a unique Treena DID per user. Live from-DID is `TELNYX_FROM_NUMBER` / `TELNYX_PHONE_NUMBER` or CLI `--from-did`. Kickoff ignores `users.from_did`.
2 changes: 1 addition & 1 deletion .cursor/skills/verify-call-agent/features/linq-inbound.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Preconditions:

## Gotchas

- SMS is off. A 501 `missing_app_base_url` means the Worker has no FastAPI origin. FastAPI `POST /linq/webhook` verifies HMAC, then replies via MessagingPort. Live reply needs `LINQ_API_TOKEN` and `LINQ_FROM_E164` on Fly.
- SMS is off. A 501 `missing_app_base_url` means the Worker has no FastAPI origin. FastAPI `POST /linq/webhook` verifies HMAC, then replies via MessagingPort. Live reply needs `LINQ_API_TOKEN` and `LINQ_FROM_E164` (shared bot) on Fly.
- Prod FastAPI is the always-on Fly host `https://hidden-beacon-080.fly.dev` (no path) with `LINQ_WEBHOOK_SIGNING_SECRET`. Nathan's laptop is not that host.
- There is no `call-agent-web` map in this repository. The Worker name is `call-agent-webhooks`.
- Pointing `PUBLIC_BASE_URL` at the Worker sends Telnyx media to `/media-stream` and gets 501.
Expand Down
16 changes: 8 additions & 8 deletions .cursor/skills/verify-call-agent/features/linq-to-call.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Linq to call (closed alpha)

Closed alpha testers text Treena on iMessage. A natural book ask on that chat collects party and time, then the restaurant phone number, then confirm/approve, then kicks off the same general `run_call` path FastAPI already uses for CLI book and quick-ask. If time is missing, Treena asks for time (or party+time) before the phone number. Caller name, kickoff `user_id`, and Telnyx from-DID come from the `users` row for the inbound phone, not from the chat. Name is collected on website `/start` and is in settings after signup. The Worker forwards Linq POST to Fly. Collect and confirm reply 200. Approve returns 202, texts `Okay, I'm going to call them now.`, places the call, hangs up, and texts the result on the same Linq chat. The first message never dials. It does not pause the live call for mid-call chat. Cap recorded `live-prove: PASS (#34)`. qa owns live-prove sign-off (TRE-19). This skill does not redial.
Closed alpha testers text Treena on iMessage. A natural book ask on that chat collects party and time, then the restaurant phone number, then confirm/approve, then kicks off the same general `run_call` path FastAPI already uses for CLI book and quick-ask. If time is missing, Treena asks for time (or party+time) before the phone number. Caller name and kickoff `user_id` come from the `users` row for the inbound phone, not from the chat. Telnyx FROM is the shared env (`TELNYX_FROM_NUMBER` / `TELNYX_PHONE_NUMBER`), not `users.from_did`. Name is collected on website `/start` and is in settings after signup. The Worker forwards Linq POST to Fly. Collect and confirm reply 200. Approve returns 202, texts `Okay, I'm going to call them now.`, places the call, hangs up, and texts the result on the same Linq chat. The first message never dials. It does not pause the live call for mid-call chat. Cap recorded `live-prove: PASS (#34)`. qa owns live-prove sign-off (TRE-19). This skill does not redial.

## Sub-features

Expand All @@ -10,9 +10,9 @@ Closed alpha testers text Treena on iMessage. A natural book ask on that chat co
- `collect-phone` Treena texts `okay, sounds good. What's the restaurant's phone number?` and does not dial. The next text on that chat supplies the E164, then Treena asks to confirm.
- `confirm-before-dial` After the number, Treena texts `Okay, I'm going to call this number. Do you confirm? Reply "approve"`. User must say confirm / approve / yes. Then Treena texts `Okay, I'm going to call them now.` and dials. A first message that already has +E164 never dials.
- `parse-fail` A phone follow-up that is not a number, or a confirm follow-up that is not an affirmative, texts a plain intake line. No `Call failed:` / `Call not placed:` prefix. No dial.
- `account-lookup` Inbound phone must match a `users` row. The call uses that row's first name, `user_id`, and `from_did`.
- `account-gate` No row, a row with no first name, or a row with no `from_did` texts a plain line. Never `Call failed: missing caller name`. Never `ledger.provision`.
- `from-did` Confirm/dial uses `users.from_did` as `DialSpec.from_did`. `run_call` takes a required `from_did` kwarg. `KickoffSession.from_did` stays the Linq iMessage line.
- `account-lookup` Inbound phone must match a `users` row. The call uses that row's first name and `user_id`.
- `account-gate` No row or a row with no first name texts a plain line. Missing `users.from_did` still collects. Never `Call failed: missing caller name`. Never `ledger.provision`.
- `from-did` Confirm/dial uses `resolve_telnyx_from` as `DialSpec.from_did`. `run_call` takes a required `from_did` kwarg. `KickoffSession.from_did` stays the shared Linq iMessage bot (`LINQ_FROM_E164`). Kickoff ignores `users.from_did`.
- `no-number-order` Confirm/dial never calls `ledger.provision`, `search_and_order_number`, or `POST /v2/number_orders`.
- `echo-else` Any other inbound text stays on `Received: {text}`.
- `async-b` HMAC verify, parse, event-id dedupe, then 202 `{ok, kicked_off, event_id}` on the approve follow-up. `BackgroundTasks` runs `run_kickoff` in this process.
Expand All @@ -22,7 +22,7 @@ Closed alpha testers text Treena on iMessage. A natural book ask on that chat co

## How to get to it (user POV)

- Have an account (website checkout, or `call-agent account upsert --phone <your iMessage> --name Nathan --from-did <Treena DID>`) with a first name and assigned Treena number.
- Have an account (website checkout, or `call-agent account upsert --phone <your iMessage> --name Nathan`) with a first name. The number people text is the shared bot `LINQ_FROM_E164`, not a per-user DID. Telnyx FROM is `TELNYX_FROM_NUMBER`.
- Text Treena `can you make a reservation with this restaurant at 7:30 for a party of two?`.
- If she asks for time (or party and time), reply with those before the restaurant number.
- Reply with the restaurant phone number when she asks.
Expand All @@ -45,9 +45,9 @@ Preconditions:
- This lever never hits Telnyx or Linq live. Cap recorded `live-prove: PASS (#34)`. qa owns live-prove sign-off (TRE-19). This skill does not redial.
- `PUBLIC_BASE_URL` and `PUBLIC_WEBHOOK_URL` on Fly must both be `https://hidden-beacon-080.fly.dev`. The Worker does not terminate media.
- One Fly machine: `BOOK` is process-local. Do not scale this app until that is no longer true.
- Confirm/dial never provisions a DID. A missing `users.from_did` texts `NO_FROM_DID_TEXT` and does not buy a number.
- Live kickoff needs a `users` row on Fly's Neon for the tester's iMessage phone, with `from_did` set. Seed with `account upsert --from-did` until call-agent-web POSTs `/accounts/sync`.
- `KickoffSession.from_did` is the Linq iMessage line. Telnyx caller ID is `KickoffParsed.from_did` / `run_call(from_did=...)`.
- Confirm/dial never provisions a DID. Empty `users.from_did` still collects. Live missing `TELNYX_FROM_NUMBER` fails with `I couldn't place the call.` and does not buy a number.
- Live kickoff needs a `users` row on Fly's Neon for the tester's iMessage phone, with a first name. Seed with `account upsert` until call-agent-web POSTs `/accounts/sync`. Set Fly `TELNYX_FROM_NUMBER` / `TELNYX_PHONE_NUMBER=+12135690899`.
- `KickoffSession.from_did` is the shared Linq iMessage bot (`LINQ_FROM_E164`). Telnyx caller ID is `KickoffParsed.from_did` / `run_call(from_did=...)`.
- Mid-call iMessage is not shipped. Do not treat a missing pause/resume as a failed L1 kickoff.
- Pending collect and confirm live at `linq/pending/<chat_id>.json` under artifacts. It is not the live-call pause/resume seam.
- Do not collect a name in iMessage. Name is collected on website `/start` and is in settings after signup. Do not set `LINQ_KICKOFF_CALLER` or `LINQ_KICKOFF_USER_ID`.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
- `cli-roundtrip` upserts `first_name`, `from_did`, prefs, and minutes, then get/list match.
- `omit-preserve` omitted `first_name`, `prefs`, `minutes_used`, and `from_did` keep the existing values.
- `sync-hmac` unsigned POST is 401; signed POST upserts Stripe ids without wiping name or from-DID.
- `dashboard-get` unsigned GET is 401; signed GET packages `users` plus computed `user.name`. Unknown phone is empty lists. Dashboard UI stays on Pages (web).
- `dashboard-get` unsigned GET is 401; signed GET packages `users` plus computed `user.name` and sitewide `bot_number` from `LINQ_FROM_E164`. Unknown phone is empty lists. Dashboard UI stays on Pages (web).
- `no-name-column` schema has `first_name` and `prefs`, not `users.name`.

## How to get to it (user POV)
Expand Down
Loading
Loading