Skip to content

Send in the recipient's business hours; one bump when a thread goes quiet for 7 days - #129

Merged
ralyodio merged 6 commits into
mainfrom
feat/planner-timing
Oct 7, 2026
Merged

ralyodio merged 6 commits into
mainfrom
feat/planner-timing

Conversation

@ralyodio

@ralyodio ralyodio commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Fourth Hunter outreach-planner feature, fully automated: "send during business hours" and "gone quiet: one bump at 7 days".

Business hours

  • recipientTimezone infers an IANA zone with no lookups. It tries the person's location first, then the company's, then the country code of the mail domain. US state codes only count as a ", XX" suffix, and only after every place name has been tried: "or" and "co" are ordinary words, and "Toronto, ON, CA" is Canada. Anything left over falls back to US Eastern.
  • Autopilot holds a cold card outside Monday–Friday 08:00–17:00 recipient-local time, and the hold reason says when it can send. This check costs no query, so it runs before any check that does.
  • The worker turns the gate on (businessHours: true). Tests leave it off, so the existing suites don't depend on the hour they run at.

One bump after 7 quiet days

  • bumpQuietThreads runs hourly. A thread qualifies when the person answered interested or question, we answered them, and 7+ days have passed with nothing back. It gets exactly one follow-up card, marked by guidance = 'follow_up_bump'.
  • The bump goes through the existing triage answer path: a card, a draft, the policy engine re-asked from live rows, then decideAutoReply. So it sends unattended only where autonomous replies are already allowed, and is an approval card everywhere else. Human approval stays the default.
  • Threads older than 30 days are never bumped, so the first run after deploy can't wake conversations from months ago. Bumps are only created inside the recipient's business hours.
  • The reply composer gets a follow-up mode that grounds the bump in our last message and asks for two or three sentences ending in one easy question.

No migration.

Tests: domain timing.test.ts (12: zones, open/closed hours, next opening), pipeline timing.test.ts (4: gate closed and open, one bump only, nothing before 7 days or after 30). The triage, composer, draft, autopilot and domain suites all pass locally (412 tests).

🤖 Generated with Claude Code

ralyodio and others added 6 commits October 7, 2026 02:12
…retire bounced addresses

Hunter's outreach planner, run by the worker instead of remembered by a person:

- Every address is verified before its first message and again after 90 days
  (MX, then an SMTP RCPT probe where port 25 allows; a blocked port sends on
  MX alone). Verdicts are cached in email_verifications, 25 fresh checks per run.
- A bounce the delivery report pins to an address marks it invalid for good
  and stops the person's cadences. The sender query skips invalid addresses.
- A campaign whose bounce rate passes 2% over 50+ sends pauses itself,
  re-verifies every queued address, drops the failures and resumes on a fresh
  window once a complete pass finds nothing waiting (campaign_list_health).
- Accept-all addresses send only while the campaign bounces at or under 1%.
- GET /autogtm/campaigns/:id/list-health, og leads health, MCP get_list_health.

Migration 0053.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…udget holder

Hunter's planner reaches four people per target company, one at a time, a
month apart: budget holder, pain feeler (end user), blocker (legal, security,
IT, finance) and champion (manager). Now automatic:

- classifyPersona reads the job title deterministically (no model on the
  send path).
- Autopilot orders each company's queued cards by persona behind its best
  card, without changing the order between companies.
- A colleague emailed in the last 21 days, or on an active cadence, who has
  not answered holds the company; everyone else there waits, card intact.
  A reply releases it. Deliberately reverses the old "two mailboxes at one
  company both get written to in one run" test.
- Drafts get a per-role angle added to the step's guidance.
- GET /autogtm/campaigns/:id/accounts, og leads accounts, MCP
  get_campaign_accounts: who was reached, who is next, missing personas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	apps/api/src/autogtm-docs.ts
#	apps/api/src/autogtm.ts
#	apps/cli/src/commands.ts
#	apps/mcp/src/tools.ts
#	packages/domain/src/index.ts
#	packages/pipeline/src/autopilot.ts
#	packages/pipeline/src/index.ts
…es the default

Hunter's planner: test one variable, 50+ recipients per variant, measure by
reply rate, carry winners forward. Now automatic, hourly per workspace:

- decideAbTest: every arm needs 50+ sends; the leader wins at a one-sided
  95% two-proportion z-test against the runner-up, or once every arm has
  200+ the best wins (A keeps its place on a tie). Opens are not an input.
- promoteAbWinners makes the winning angle the step intent, clears the
  variants and records the test in ab_results; later tests on the step
  count only runs after the decision.
- Fix: the variant report counted only `replied`, but the mailbox poll
  writes `responded`, so every polled reply was missing from A/B results.
  The query now lives in the pipeline (variantStats) and reads both.
- GET /cadences/:id/variants now includes `decided`; GET /ab-results;
  og cadences ab <id>; MCP get_ab_results.

Migration 0054.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uiet for 7 days

Hunter's planner: send during business hours; gone quiet, one bump at 7 days.

- recipientTimezone infers an IANA zone from the person's location, then
  the company's, then the mail domain's country code (US state codes only
  as a ", XX" suffix, after every place name). Falls back to US Eastern.
- Autopilot holds a cold card outside Monday-Friday 08:00-17:00 local and
  says when it opens. The worker turns this on; tests stay hour-independent.
- bumpQuietThreads (hourly): an interested/question reply we answered, then
  7+ days of silence, gets exactly one follow-up card, marked by its
  guidance. It goes through the triage answer path (draft, live policy,
  decideAutoReply), so it sends unattended only where autonomous replies are
  already allowed and is an approval card everywhere else. Threads older
  than 30 days are never woken; bumps are only created in business hours.
- The reply composer gets a follow-up mode that grounds in our last message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	apps/server/src/index.ts
#	packages/domain/src/index.ts
#	packages/pipeline/src/autopilot.ts
@ralyodio
ralyodio merged commit ee00b78 into main Oct 7, 2026
4 checks passed
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