Skip to content

Send past the held cards, say what holds them, and put a face on each lead - #68

Merged
ralyodio merged 1 commit into
mainfrom
worktree-autopilot-starvation-photos
Sep 10, 2026
Merged

ralyodio merged 1 commit into
mainfrom
worktree-autopilot-starvation-photos

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Why six a day

The digest said "0 new leads, 6 sent, Awaiting approval 1117". Prod had 597 pending email cards, every campaign on autopilot, a 200/day cap. None of the caps were binding.

runAutopilot read candidates ORDER BY priority LIMIT (cap - sentToday). The top 194 all resolved to a handful of shared support@ inboxes already inside the 2/week address limit or the 48h cooldown. Every tick read the same 194, held them all, stopped. 172 cards with a personal address sat at rank 195+ and were never looked at.

What changed

  • Autopilot reads up to a fixed ceiling and stops at the cap, not the window. Address gates run from a per-run memo before the per-person query and before drafting. Budget is read once per run and refreshed after each send. Personal addresses sort ahead of shared ones at equal priority.
  • Hold ledger. "Held back" events are written when the reason is new or changes, not every tick (one day wrote 18,532 identical rows for 120 people). The ledger also tells the digest what is holding the queue.
  • Digest now shows "Autopilot queue: N (M held by a limit)" and "Needs you: N (LinkedIn …, X …, GitHub …)" instead of one "Awaiting approval" number, with notes like "120 held in the autopilot queue: the company inbox already had this week's messages."
  • Imported addresses count as personal. 15,917 consented contacts in person_emails were invisible to the sender, the approve path, the queue badge and the recommender's reachability check. All four now fall back to the imported address. Nothing enrolls them into a campaign automatically.
  • Reseed idle campaigns weekly (URL seeds re-crawled, keyword seeds re-discovered) when no job of theirs is outstanding. Discovery had been dead since Aug 30 because nothing ever returns to a seed.
  • Photos. Gravatar's avatar is stored instead of dropped. ValueSerpClient does one image search per person and accepts a result only when its page is a linkedin.com/in/ profile or the company's own domain, titled with the person's name; the profile URL is recorded as a research identity at 0.6. Sweep is 20/tick and PHOTO_LOOKUPS_PER_DAY (default 300) per workspace. Avatars show in the digest, /prospects, /prospects/[id] and the approval card.

Migration 0033_avatars_and_reseed.sql. New env: VALUESERP_API_KEY (unset means Gravatar only), PHOTO_LOOKUPS_PER_DAY.

Verified

  • bun run check: 1468 tests pass, typecheck clean, format clean.
  • apps/web build passes.
  • New tests: starvation (sendable card behind three held ones with cap 2 is sent), hold dedupe (one event across two runs), imported address used as personal, digest split and held note and avatar, photo sweep hit/miss/cap, reseed url/keyword/recent/busy, ValueSERP corroboration rules.

🤖 Generated with Claude Code

https://claude.ai/code/session_01234vh2fZQDqyXTEY8XBi4S

… lead

Autopilot sent six messages a day against a queue of 597 and a cap of 200.
The candidate window was `cap - sent today` rows in priority order, and the
top of that order was a hundred and twenty prospects who all resolve to a
handful of shared company inboxes already inside their weekly limit. Every
tick read the same window, held every card in it, and stopped, while 172
cards with a personal address sat just past it and were never looked at.

- The window is now a ceiling that only keeps a run finite; the loop stops
  at the cap, not at the window. Address gates run from a per-run memo
  before the per-person query and before any model draft, so a held card
  costs a comparison rather than a query and a draft nobody will send.
- A hold is written to the live feed when it is new or changes, not every
  tick: one day produced 18,532 identical rows for 120 people, enough to
  make the day's events unreadable. `HoldLedger` remembers, and feeds the
  digest.
- The digest splits "Awaiting approval: 1117" into what autopilot will send
  (and how many of those a limit is holding, and why) and what only a
  person can act on, by network.
- An address given in a consented import is the person's own. The sender,
  the approve path, the queue badge and the recommender all read only
  `social_identities`; 15,917 imported contacts were invisible to all four.
- An active campaign whose seed has not been read in a week is asked again
  when it has no work outstanding. Every campaign was seeded exactly once,
  and the digest had read "sites read 0" for ten days.
- A picture for each lead: Gravatar's avatar is kept instead of dropped, and
  a ValueSERP image search accepts a result only when its page is the
  person's LinkedIn profile or their company's own domain, titled with their
  name. The profile URL is recorded as research. Shown in the digest, the
  prospect pages and the approval card; capped per tick and per day.

Migration 0033 adds `people.avatar_url`, `avatar_source`,
`photo_looked_up_at` and `campaigns.reseeded_at`. New env:
`VALUESERP_API_KEY`, `PHOTO_LOOKUPS_PER_DAY`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01234vh2fZQDqyXTEY8XBi4S
@ralyodio
ralyodio merged commit 6f779db into main Sep 10, 2026
4 checks passed
@ralyodio
ralyodio deleted the worktree-autopilot-starvation-photos branch September 10, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant