OrderPin 的 ISO 白牌获客外联系统——从找线索到 CRM 商务跟进共十一个环节,目标是把它跑成一条闭环。
外联实际是两套并行系统:系统 A(Hermes run.py 读 iso_outreach_tracker.json)每个工作日 20:00 真的在发信;系统 B(Supabase outreach_sequences)是本看板读的账本,冷发引擎当前停用。实测 tracker ⊂ Supabase,所以要解决的是状态同步方向,不是对齐两套数据。
本看板的数字若无特别说明均属系统 B;每次更新看板全部数字实查,不沿用上一版。
current_date − N,本版边界 2026-08-03 00:00 UTC结论:三段都在跑,但没接在一起。唯一在真产出的是发送段,它靠一份存量名单,T1 口径还剩 6.8 个工作日(2026-09-04 22:24 HKT)。
建议:唯一队列已定 = Hermes tracker,DB 队列降为「候选供给态」;接线已做到 dry-run(候选 25 人),验证与写入两步等 Graham 批准 —— 之后加工段的产物才有地方去。
原因(全部本版实查):供给段断供 25 天(最后一条真线索 2026-08-08 10:00:16Z);
加工段 kp_finder 两条腿脚本层真实输入都是 0;
发送段真发送器只读 Hermes tracker,DB 队列零邮件消费者。
⚠️ 本版把 18 条旧说法登记为已废(见 check_dashboard_consistency.py 的 BANNED),
其中 3 条是本会话自己先说错、后自查翻的:探针存量 592(实为 663 全集口径)、
「假数据在染绿流水线监控」(v_cron_last_success 今日才建,不可能有历史染绿)、
「kp_finder 在加工 64 条假数据」(脚本层过滤后真实输入是 0,不是垃圾,是空)。
⚠️ 本版有 3 组数字在实时移动,「绿」是看的时刻的函数。盖章时刻 = 2026-09-04 23:18 HKT。
· 邮件正文入库 —— 回信扫描每 30 分钟增量跑,9/3 的 20 分钟内从 2,121 → 2,175 → 2,284;9/4 17:35 已到 2,329、22:34 已到 2,361
· 线索总行 / 待发 —— LinkedIn 在实时写入(近 2 小时 14:10 桶 2 行、14:20 桶 5 行)
· 批 3 → 批 2 迁移 + 已触达 —— Hermes 每工作日 20:00 那轮在发,人从「待发」变成「已发」。
9/2 晚 20 分钟内连跳两次(38→39→40 / 79→78→77 / 94→95→96);9/4 17:32 复算 批 2 60 / 批 3 57 / 已触达 116,22:34 再复算 74 / 43 / 130(今晚 20:00 那轮发了 30),合计 311 恒定 ⇒ 是迁移不是新增。
⇒ 引用这些数必须带时刻;--live 判据的作用不是「让它永远绿」,是让漂移当场可见。
管道吞吐 = 最窄那一段。粗管接细管,中间那截还没焊上。
| 段 | 实测 | 复算路径 |
|---|---|---|
| 供给段 | 断供 25 天。近 30 天真线索 32 条(cra_ncr 17 + tavily 15),但只落在 6 个自然日(08-01~08-08);08-09 起非测试新增 0;这 207 行窗口内带 contact_email 的 0/207 |
max(created_at) where source in ('cra_ncr','tavily_discovery') = 2026-08-08 10:00:16Z |
| 加工段 | kp_finder 两条腿真实输入 0 / 0(谓词口径 62 / 5)。主查询那 62 条 website 全 NULL,被 kp_finder.py:1874 客户端过滤;兜底那 5 条 discarded_at 全非空 |
谓词加 coalesce(btrim(website),'')<>'' → 0;兜底加 discarded_at is null + ecs<>'confirmed' → 0 |
| 发送段 | 唯一在真产出的段。逐日实发 8/26=142 · 8/27=88 · 8/28=92 · 8/31=138 · 9/1=33(周末 cron 1-5 不跑)。T1 余粮 6.8 个工作日(205 ÷ 30,2026-09-04 22:24 HKT,仅 T1 口径;9/2 22:33 时为 295) | grep "sent ->" run.log | cut -c1-10 | sort | uniq -c;余粮 = tracker 中 status='queued' = 205(22:24 HKT,今晚 20:00 那轮已发 30);分母见 run.sh:60 |
| 口径 | 值 | 说明 |
|---|---|---|
| DB「可发」四层收紧 | 1,017 → 900 → 895 → 671 | ①有邮箱∧outreach_status='queued' ②排已发 t1 ③排 unsub/bounce/reply ④排 status∈(disqualified,dead,paused) |
| DB queued 三桶闭合(9/4 17:46 HKT) | 1,017 = 235 + 357 + 425 | tracker 也 queued 235 · DB 标 queued 但 tracker 已发/已回 357 · tracker 没有 425。425 里 status='new' 138 = 旧默认值 108 + 桥推 30;经正向放行 ∧ 无已知坏结果 ∧ 名字可用 后能导入发送器的只有 25 人(9/4 22:24 适配器 dry-run,全集 311 = 133 + 148 + 5 + 25) |
| 真·上膛 | 205 | (2026-09-04 22:24 HKT;9/2 22:33 为 295)tracker 里 queued 且三个 tN_sent_at 全 NULL,逐条验 295/295 同形 |
| DB 比 tracker 多 | 425 | 这些人没有任何路径会被发出 |
| DB 陈旧 | 357 | (2026-09-04 17:46 HKT;9/2 为 297)DB 标 queued,tracker 里其实已发过 ⇒ 只能回填,不能相加 |
| t1/t2/t3 已发 | tracker 571 / 465 / 386;DB 276 / 211 / 187 | 发送器只读 tracker ⇒ tracker 是发送真相源,DB 仅为其 48% / 45% / 48% |
| 发送总量三口径 | run.log 1,512(含自测)· tracker meta 1,425 · tracker 逐条 1,422(纯外发) | 三个都复算成立,差 ~90;引用时必须点名口径 |
结论:外审发现一处数据库函数权限配置缺陷,未认证请求可读到线索数据。
已于 2026-09-03 修复并用重放验证关闭(原攻击路径现返回 permission denied)。
建议:把「SECURITY DEFINER 函数不得对 PUBLIC/anon 开放」做成上线前的例行检查项 ——
本次是靠外审偶然发现的,不是靠判据抓到的。
原因:涉事函数是 SECURITY DEFINER 且源码内无鉴权,
而 Supabase 的默认是函数对 PUBLIC 可执行 —— 两者叠加才成洞。
⚠️ 范围经逐个核实收窄:全库看似有 ~65 个同类函数,但其中绝大多数自带内部鉴权,
真正无守卫的是 8 个签名,已撤 7 个(第 8 个被 13 条 RLS 策略引用,撤了会打断 RLS,故保留)。
复现细节与完整因果链不在本页 —— 见仓内
RLS_AUDIT_PROBE_ROOTCAUSE_20260902.md 与 migration 20260903_130000。
| 判据 | 结果 | 说明 |
|---|---|---|
| 14:00 那次应真跑 | ✅ 成立 | 2026-09-03 14:00:10 -- daily_leadgen started (hour=02 ET) --,16:03 跑完(11 passed / 2 failed)。自 8/31 以来第一次真跑。 |
| 15:00 那次应跳过 | ⚠️ 不可观测 | 🔴 是判据本身写坏了:14:00 那轮跑了 2 小时,15:00 时它还在跑,第二个候选根本没触发。我当时假设两次独立触发。DST 双候选机制仍未验证,要等冬令时或一次短跑。 |
phone_backfill 埋点 | ✅ 验证生效 | 从「从未」变成 2026-09-03 08:02,24h 内 23 个事件。 |
kp_finder 埋点 | ⏳ 仍未验证 | 不是埋点坏,也不是没输入 —— 是第三种:这轮它有输入(website_backfill 补了 62 个网站后队列就有料),但每个站都撞 chrome 基建故障 (ERR_BLOCKED_BY_CLIENT) → DEFER → 一个 KP 都没找到 → 1800s 超时。 |
| 项 | 实测 | 根因 |
|---|---|---|
com.grahamcrm.scan-replies | 已坏 5.97 天(最后成功 2026-08-27 19:22:39),err.log 287 行去重后仅 1 行 | 脚本从未进 main:fd250a9(08-27 19:35) 把它提交到 feat/apk-kds-batch-send,切回 main 即消失 —— 13 分钟因果链。修法是「launchd 不许依赖 git 工作树路径」 |
| LinkedIn 发送 | 最后真发出 2026-08-16;此后 8/17→8/31 约 443 次尝试全部 sent=0 | 非磁盘。8/30 governor 一次没挡、放行 50 次仍 0 ⇒ 真因在浏览器侧(no_editor / not_connected) |
apollo_email_backfill | 82 次 fail-closed,100% 被吞成 ✅(总 ✅ 138) | 判据落在退出码而非产出 —— 「拒绝花钱」与「真干完活」在日志上不可区分 |
| 冷发域 | COLDSEND_DAILY_TOTAL=0,停机第 13 天 | 恢复条件:域龄 60–90 天,run.sh 注释自陈 2026-09 中下旬可重测 |
| LaunchAgents 体检 | 53 个 plist,指向不存在文件的 = 1(仅 scan-replies);launchctl 非零退出 7 条 | ⚠️ 体检必须用 plutil -convert json,plistlib 会静默漏 6 个 |
pipeline-stale-alert | ✅ 9/3 09:30 已首跑,正确报出 5 个步骤长期无产出 | 首跑报的 website_backfill 118 天 正是清掉 601 行探针之后的真值 —— 不删的话它第一次开口就会少报 3 个月 |
| 回复分类 | 人数 | 说明 |
|---|---|---|
| 有意向 | 13 | LLM 从正文判定,置信度均值 0.82 |
| 已拒绝 | 8 | 含明确退订 |
| 已约会(仅约,未开) | 6 | 回复者中;全库含未回信的共 9 |
| 其他 | 5 | 竞品来信、无实质内容等 |
| 已开会 | 1 | 9/2 日历实证后从 0 变成真值(另 4 家未回信但开过会) |
| 合计 | 33 | = 已回复人数,仍然对上。本表每行「人数 = 公司数」(13/13、8/8、6/6、5/5、1/1),无同公司重复计数 |
约会率此前是 21 ÷ 32——分子取全库、分母取回复者,不同源,没有业务含义。后拆成触达约会率与回信转化率,标签里写明分母。
2026-09-02 已重算:分母取 t1_sent_at ≥ 2026-07-01 的 249 人(排除 27 条 t1_sent_at 早于外联启动的历史污染行),分子取日历实证且会议晚于首次触达的 3 人 ⇒ 触达→真开会率 = 1.2%,对照旧口径 21/276 = 7.6%。
⚠️ 这是下界——日历只能证明开过,不能证伪(电话/Zoom 会不产生纪要,取消事件 API 取不到)。另有 10 条无任何邮件/渠道佐证的标记已清除(走 apply_lead_status 唯一写入口)。
窗口大小来自环境变量 COLD_EMAIL_MAX_AGE_DAYS(默认 30)——150 不是硬上界,173 才是。用一个可被改掉的默认值算出的风险数,方向对不等于数字对。
三个子块:当下运行状态(谁真的在发)→ 目标闭环(该长成什么样)→ 当前断点(差在哪)。
这一轮查出的每件事都是同一个形状:台账说的和实测的不是一回事。所以这张表并置两列。
| 渠道 | 台账怎么说 | 实测 | 状态 |
|---|---|---|---|
run.pyHermes ISO 邮件 |
jobs.json 工作日 20:00 |
最早发送 2026-07-01,累计 1,422 次投递 | 在跑 |
engine.jsLinkedIn 好友邀请 |
jobs.json:enabled=falsepaused_at 2026-07-14 |
launchd 那一路从未关闭。日志 452 条发送记录,100% 发生在标记暂停之后;跨 49 天、27 个发送日、451 人 | 台账说停了 |
linkedin_outreach_v3LinkedIn 私信跟进 |
代码默认 REAL_SEND=false |
.env:55 覆盖为 true,launchd 已调度。9/2 实测队列里有 61 人已到期(runner_node 全部 = hermes-graham,与 .env:33 一致),current_step 分布 1→4,步进在推进。日志实证 8-17 sent=2/20 (mode=REAL) |
在跑 · 61 待发 |
linkedin_reply_send_worker审批过的回复自动发 |
代码默认 false |
脚本 :91 硬编码 true,每 15 分钟一轮;已发 21 条,队列中 1 条 |
在跑 |
cold_email_engine |
2026-06-25 恢复 | daily_leadgen.sh:260 整行注释;注释写明「活库 contacted 行 = 0 = 事实空转」 |
已停 |
send_emails_direct.py |
— | 无调度,但零护栏:给个 JSON 名单就全发,不查抑制名单、不限量 | 误跑成本最高 |
9/2 新增一个同族陷阱:daily_linkedin.out.log 里有两个任务名——linkedin_followups 每次都打「今日无需跟进的 LinkedIn 联系人」,而真正发 DM 的是 linkedin_outreach(sent=2/20 (mode=REAL))。按任务名判断「followups 在不在发」会得出完全相反的结论。
上个会话查到过 linkedin-iso,判断「它跑的是 engine.js,根本不碰 tracker」就排除了——「不碰 tracker」≠「不发信」。按「它写不写我关心的那张表」筛发送器,会整条渠道地漏掉。查一个 job 停没停,判据是看它的产出(state.json 的 added_today、日志末行),不是看配置里的开关。
左侧三段分区(9/2 新增):供给段(来源 → raw_leads)近 30 天只产出 30 条真线索,Graham 已定换主线为 Apollo + LinkedIn Sales Navigator;加工段(规则闸 → 邮箱验证)是本轮唯一还有价值的一段,下一步是把新供给接到它的入口上;待发队列是边界(「到待发队列前」指的就是它以上);发送段在本轮范围之外。
一条主路径 + 一条存量回流边,纵向读。分配同事是终点——交给销售后线索就离开外联系统。让它成环的是 company_leads 存量回流到闸前那条边(5,680 家可回流,见 §01 枚举明细),不是从销售末端往回流。当前之所以看着像两条,是因为 Apollo 的 --ingest 从最后一格直插待发队列,而回流边根本不存在。颜色是每个环节的真实状态,不是计划状态。9/2 重画——旧图有三处与实查矛盾:把 Reveal 标成「手动」(实际没有代码)、把邮箱验证标成已打通(实际未调度)、把 LLM 判 ISO 标成未接线(实际 icp_v2_reclass 每天在跑,没接的是 Apollo 那条 --review)。
与 SEND_PATHS_ENUM 同一套纪律:入口没枚举全,闸就会被整条地绕过,而且是静默的。上图九个格是这张表的摘要。下表实查自生产库,「绕过 raw_leads」列精确闭合到 1,599(= from_raw_lead_id IS NULL 的行数)。
| 来源桶 | 源数 | raw_leads | company_leads | 绕过 raw | 闸能不能看见 / 备注 |
|---|---|---|---|---|---|
| 自动抓取 · Google google_places / google_maps | 2 | 5,405 | 5,585 | 696 | 大部分经 raw_leads,696 条直写——这批闸看不见 |
| 一次性表格导入 import_batch=wechat_20260620 · 5 个 sheet | 5 | 3,409 | 3,409 | 0 | 全经 raw 但 0/3,409 有邮箱;source 名写 20260620 而实际导入是 2026-04-22;online_ordering sheet 里 99 家名字像餐厅(终端商户,不是 ISO) |
| 展会名单 tradeshow_linga/neaa/eta/discovery | 4 | 749 | 740 | 0 | 全经 raw |
| 其它单点源 cra_ncr / linkedin / clover_fiserv / pax | 5 | 540 | 427 | 0 | 全经 raw |
| POS 厂商合作方 *_partner / *_reseller | 12 | 45 | 45 | 0 | 全经 raw |
| 自动抓取 · Tavily tavily_discovery | 1 | 0 | 244 | 231 | 🔴 每天在跑却几乎全部直写 company_leads(仅 13 条经 raw) |
| 🔴 测试 / 审计垃圾 rls_audit 592 / qa_perf 62 | 2 | 0 | 654 | 654 | 🔴 不是线索,回流时必须显式排除,且要单元测试锁住 |
| Apollo source='apollo' | 0 | 0 | 9 | 9 | 🔴 company_leads 里只有 9 条;真正的 418 条走 --ingest 连 company_leads 都没进 |
| (source 为 null) | 1 | 0 | 9 | 9 | 🔴 来源未知的 9 条 —— 未知写入方,需查清 |
合计绕过 raw_leads | 1,599 | 精确等于 from_raw_lead_id IS NULL 的行数 ⇒ 枚举闭合 | |||
⚠️ 还有两条不在这张表里的入口,因为它们根本不落 company_leads:
① Apollo --ingest 直插待发队列 418 条(图中红弧)——三张上游表一张没进;且它不是一次性验证:分 5 次入库(7/22 158、7/26 4、7/27 13、8/10 83、8/18 160),81 行 t1_sent_at 非空(已真发信),其中 44 行 status='disqualified' 仍 outreach_status='queued'、2 行判否后照样发了出去。
🔴 比图上画的更隐蔽:这 418 行的 lead_source 全写成 linkedin,lead_source ilike '%apollo%' 实查 0 行,只能靠 discovery_meta 认出来 ⇒ 队列里 lead_source='linkedin' 的 533 行中 418 是 Apollo,真 LinkedIn 只有 115。任何按 lead_source 做的枚举/闸/统计都会漏掉它。
② cold call 存量 1,203 家——它们在 company_leads 里(已计入上表各桶),但 caller_stage 携带的人工信号(尤其 ask_to_send_email 61 家)目前没有任何自动通路回到闸前。
枚举漏一个,闸就漏一条渠道。LinkedIn engine.js 就是这么漏了 50 天。
结论:本机流水线除 US SOS 那条腿外,25 天没有产出过任何东西。近 14 天另有 6 天整天没跑(8/20 8/23 8/25 8/26 9/1 9/2),全被 governor 挡下、全静默。
建议:判据落在产出上,不是「进程起没起」。已建 v_cron_last_success(不限窗口取 MAX)+ 独立 launchd 告警。
| 步骤 | 上次真正产出 | 距今 |
|---|---|---|
kp_supplement(US SOS,不在本机) | 2026-09-01 | 0.9 天 ✅ |
leadgen_summary(raw_leads 任意) | 2026-08-28 | 4.3 天 |
promote_raw / tavily / cra_ncr / website_backfill / icp_v2 | 2026-08-08 | 25 天 |
email_enrich | 2026-06-25 | 69 天 |
gmaps_scrape(2026-05-25 停用) | 2026-05-22 | 103 天 |
customer_match / pos_reseller_verify | 2026-04-16~20 | 135+ 天 |
原因:「跑了」≠「产出了」 —— 8/31 那次跑完了每一步,但 cra_ncr +0、pos_reseller empty directory、phone_backfill 三次 serpapi 超时 ⇒ 净产出 0。
🔴 告警装在被跳过的那段里面:daily_leadgen.sh:47 governor 不过就 exit 0,而 leadgen_alert 在第 320 行 —— 流水线越是没跑,告警越不会响。且 check 的输出被 >/dev/null 2>&1 丢掉,连是哪一项资源都不知道。
⚠️ 此刻实测 governor 是 OK 的(disk 68.97 G / swap 0 / mem free 53%)⇒ 卡的是白天那两个时刻的内存,而一天只有两次机会。已改成 03/04/05 三次。
结论:v_cron_health 有 3 个信号指错了表,另有 2 个从来没有过发射方。
建议:3 个信号已在 migration 084 指对;2 个埋点已补,下次成功跑完应变绿 —— 这是可证伪的验收判据。
| 步骤 | 视图查的 | 实际写的 |
|---|---|---|
tavily_discovery | raw_leads → 0 | company_leads 244 |
kp_supplement | raw_leads source LIKE 'sos%' → 0 | lead_contacts 331 |
icp_v2_reclass | meta.reclass_source → 0 | stage_name='icp_passed' 3,160 |
kp_found | 🔴 从来没有过发射方 —— 只有一次性回填脚本 funnel_event_tracker.py(零调用方)在 2026-04-24 写过一次 | |
phone_validated | 🔴 同上 | |
原因:一个永远红的监控和一个永远绿的一样不携带信息 —— 这就是它建好之后没人读的原因。唯一的消费方 cron_health_check.py 在本机没有任何调度。
结论:company_leads 近 30 天新增 200 行,其中 170 行是测试数据(rls_audit 108 / qa_perf 62,9/1 还在写),真线索只有 30 条。存量测试垃圾 654 行。
建议:已加 CHECK ... NOT VALID 挡 qa_perf(只对新写入生效)。rls_audit 待 Graham 定。
原因:🔴 不能用约束挡 rls_audit —— 那 108 行来自 RLS 审计的 INSERT 探针(故意插一行验 RLS 拦不拦得住)。挡了它,探针会因「约束」失败,而那个失败长得和「RLS 生效了」一模一样 —— 等于用数据卫生把一个安全控制变成假绿。它的正确修法是修它自己的 cleanup。
⚠️ NOT VALID 期间「约束存在 ⇒ 数据干净」是错的推论:存量 654 行仍在,清干净后才能 VALIDATE。
🚪 闸该落在哪:company_leads 的写入口,不是绿线 promote_raw 上(9/2 实查定的)。
实查:promote_raw 近 30 天 promoted 仅 17 条(最后一次 2026-08-08 10:00Z,25 天前),同期 disqualified 4,004——它在跑,但几乎不放行;而 company_leads 近 30 天新增 200 家里 183 家(91.5%)from_raw_lead_id IS NULL(近 90 天 585 里 512)。
⇒ 把闸挂在绿线上只管得住今天入口的 8.5%,另外 91.5% 照样从旁边走过去——正是本页反复说的那个形状。
排序:重要 > 高产出 > 紧急。新需求按此插队,不按提出时间。每项按结论 → 建议 → 原因写,全部基于实查。
结论:唯一发送队列 = Hermes tracker;outreach_sequences 降为「候选供给态」(不是账本);导入审计 jsonl 是「导入事实与幂等记录」。依据:真发送器 run.py 只读 tracker、零 DB 客户端;DB 侧零邮件消费者(冷发引擎自 2026-07-16 起停用);MERGE_PLAN v8.3 七轮外审已把「DB 当队列」否掉。方向经 4 路 AI 评审 + 三家外审两轮(3/3 出票)认可。
建议:接线已实现(openclaw db_queue_to_stage.py 只读 DB → 批次快照 → 既有导入器一次原子写 tracker;153 单测;真库 dry-run 闭合),剩两步等 Graham:A 验证 25 人(≤25 credit)→ B 按准入清单写 tracker(冻结窗外)。同时要定:排程化、catch_all 队列(73 人 = 本轮产出 3 倍)、双队列日落日期。
原因:桥推的 30 人 0/30 进 tracker;DB 侧 425 人无发送路径、357 人 DB 标 queued 而 tracker 已发过(17:46 HKT)。收益上限很小 —— 候选 25 × 验证通过率 ≈ 16 人 ≈ 半个发送日;价值在「接通」本身:之后每一批放行都有去处。剩余风险(并发写保护依赖人工维护窗)已在内部 plan 登记,不在公开页展开。
结论:供给段已断供 25 天(最后一条真线索 2026-08-08 10:00:16Z);08-09 起非测试新增 = 0。方案已写并过外审 3/3 出票(方向获背书,但三家均给 P0)。
建议:取双写不取改路——Apollo 保持现有两条输出不动,增写 company_leads + lead_contacts。但排在 I-0 之后:双写买不到一封邮件。
原因:Apollo 走 --ingest 只写 outreach_sequences,company_leads 里 source ilike '%apollo%' 全表仅 9 行、近 30 天 0 行 ⇒ 额度与库存完全脱钩。且 Apollo 今天对 10,537 家存量零 CRM 侧去重(_dedup_guard 以 phone=None 调用,控制流必达 return (True,"ok",None))。
结论:其中 274 家已进外联队列,可能正在给非 ISO 发信。
建议:抽 100 家测误肯定率,排在任务 8 之前。
原因:任务 8 只查否定方向(4,772 家)。1,660 占肯定类目 73%。假阴 = 机会成本;假阳 = 实际支出 + 域名声誉。
company_leads 写入口结论:已挡 qa_perf(CHECK ... NOT VALID);rls_audit 待 Graham 定。
建议:先定 rls_audit 怎么办,再谈存量 654 行清理与 VALIDATE。规则 6a 已拍板,配额闸本身可接但名额为 0。
原因:挡 rls_audit 会让 RLS 审计的 INSERT 探针因「约束」失败,而那个失败长得和「RLS 生效了」一模一样。它的正确修法是修自己的 cleanup。
结论:四轮外审均判「不建议直接真写」,已停止外审循环。
建议:做隔离库演练,不再加文档。前置:① D7 冻结 Hermes 的可执行命令(阻塞)② 演练 ③ ck_lead_status_source 加 'tracker' ④ 清 3 家 opt-out(规则 6a 已拍板,不再是阻塞项)。
原因:36 条 ≥2 家命中已全修(v8.3),但命中数 12→9→8→7 未趋近 0,且后三轮最严重的缺陷均来自上一轮的修复动作。
rotate_keys.sh 枚举所有消费方结论:9/1 轮换只更新了一处,openclaw/.env 的 key 死了两天。
建议:轮换后逐个消费方实拉一行,401 即报错退出。
原因:脚本自述「orderpin-iso.env 是所有脚本唯一的引用点」,而 daily_leadgen.sh:15 source 的是 openclaw/.env。
结论:链路全通,但额度用不完不是问题 —— 瓶颈是合格线索供给。同一批源页面上,公司层已被挡掉的 657 人里 42.9% 公司名含 payment/merchant,net-new 2,215 人只有 1.9%,密度差 22 倍(放宽到 pay|pos|iso 仍差 6 倍)。
建议:有多少合格线索就 reveal 多少。先全量 --review 量通过率;不够时从筛选条件代价表入手,不要拿同样条件补跑搜索。
原因:在这个池子上花掉 2,021 credit 比让它作废更糟——等于往队列灌 2,000 个非 ISO,代价是发信支出 + 域名声誉。上周期也作废约 70%(~1,759/2,510),当时定性同样是「主因是没用完」——同一件事第二次发生。--review 首批 24 条 KEEP=0,判出的 GoTab/Pomodo(POS 软件)、Decorative Paint(油漆厂)就是这个池子的真实成分。
⚠️ 0/24 是 cands[:N] 前缀不是随机,不能外推。⚠️ 密度差用 scripts/check_dashboard_consistency.py --pool 每次重算;上一版的 17.7 倍 → 22 倍,见 §08。
ask_to_send_email ∧ 被判不合格的 11 条结论:不烧 credit、不烧 LLM 配额。
建议:排在任务 8 之前。
原因:电话中对方明确说「发邮件给我」,机器判不合格 —— 每条自带一条独立的人类正向信号,信息量大于随机抽 200 家。
结论:样本已固定落盘且可复现。
建议:等 I-2 的反向抽样结论一起看。
原因:两个方向的误判率放在一起,才能判定「判定器能不能信」。icp_category 实有 13 个值,任务 8 只取 non_icp+unknown。
结论:MillionVerifier 不是「有码未调度」——它是 CSV→JSONL 的离线工具,不读库也不写库,已在 Apollo phase-2 链上被 promote_phase2_to_tracker.py 消费。
建议:不要接进 daily_leadgen.sh;随 I-1 一起长在 Apollo 主线上。
原因:接进 daily 会打破 Graham 2026-07-29 定的「按文件解耦、Hermes 代码零改动」边界。这是我先前判断错的一条,已更正。
结论:30 条全部 status='new'、t1_sent_at 全 NULL、进 tracker 的 0/30 → 9/4 适配器 dry-run:25 条候选(5 条单 token 名字留人工),全部未验证,等批准 A/B。
建议:批 A(验证)→ 批 B(写 tracker)→ 下一个工作日 20:00 起真发。
原因:晋升口写 outreach_sequences,发信器读它自己的 tracker。本项目第四次「建好了、过了外审、没接线」。
结论:规则 6a Graham 9/2 拍板取「接受名额 0」——闸照接,槽 2 只对以后走闸进来、带 function 的新公司开放;存量 1,442 家维持现有 1 个联系人。剩下的阻塞只有 3 家 opt-out 冲突。
建议:清 3 家 opt-out,然后按「闸落在 company_leads 写入口」接线。
原因:6a =「占位者职能查不出 ⇒ 拒第 2 人」,是 B3「必须换职能」的 fail-closed 实现。实查 discovery_meta->>'function' 与 classifier_version 非空各 0 行;1,571 个占位者里能摸到 title 的只有 26(1.7%)。另两条路已排除:回填存量职能要花 Apollo credit(plan §不做);关掉 B3 在 v7 已封死。
rls_audit 自己的 cleanup结论:存量测试垃圾 654 行,近 30 天还在增(rls_audit 108)。
建议:修它的 cleanup,不要在写入口挡它(见 I-3)。清干净后 VALIDATE CONSTRAINT。
原因:脚本有 cleanup 代码但显然没清干净;它不在任何调度里 —— 是人工跑审计时留下的。
按你最初描述的闭环逐项对照,这些是缺口。
系统按关键词自动搜、LLM 优化搜索条件。现在全是人工在 Apollo 后台筛完导 CSV。
在消耗 credit 取联系方式之前先判是否 ISO 目标客户。这是你定的硬约束,目前没有自动执行的通路。
已更正我此前写「上游没接进来」是错的——我查到没有叫 leads 的表,就当成没有这件事。实际上游用的就是同一个库:raw_leads 10,166 → company_leads 10,537 → lead_contacts 3,480,每天 10:00 自动跑。
真正缺的是 lead_contacts → outreach_sequences 这一节没有外键,只能靠邮箱字符串对;晋升标准也散在 10 个脚本里没有单一定义。方案见 LEADSGEN_BRIDGE_DESIGN_20260901.md。
🔴 2026-09-02 实查:桥建好了,但通向一个没人读的队列。首批晋升的 30 人全部 status='new' / outreach_status='queued'、t1_sent_at 全为 NULL,进 Hermes tracker(真正在发信的那套)的是 0 / 30。晋升口写 outreach_sequences,而发信器读它自己的 tracker。
⇒ 「等一周看首批 30 的回复率」等的是一封永远不会发出的信。这是本项目第四次「建好了、过了外审、没接线」(前三次:sync.py 调度、shadow_ingest 从未运行、iso_send_sink 零引用)。
你定的规则是开完一次会就转入 CRM 销售跟进。目前没有这条通道,也没有数据格式对齐方案。
assigned_to 字段存在但无人写入。后续两位同事的邮箱接进来后,分配是必需的。
正文入库后现在具备了上下文条件,但生成侧还没做。
这几条不是「还没做」,是做起来有真实阻力的。
此前判「无解法」的结论是对的,但只对邮件这条路:Meeting Notes 文件夹 354 封邮件里匹配到线索的是 0 封——会议纪要是内部文档,收发件人都不是客户。
→ 换成 Google 日历 API 就直接有了:参会人邮箱是干净的联结键。主日历 2026-05→09 共 52 个事件、58 个外部参会邮箱、35 个带 Gemini 纪要。取不到不是因为数据不存在,是因为一直在错的地方找。详见 §09。
该文件夹同时装着我发出的 Invitation:、对方的 Accepted: 和 Declined:。直接按文件夹归属,会把两个 LLM 以 0.95 置信度判为「拒绝」的人翻成「已约会」。
→ 修了三轮才对:第一轮不过滤、第二轮在邮件层面否决(排除了 21 封但一个人都没防住,因为他们还有别的邮件在同一文件夹)、第三轮改到联系人层面才正确。
lead_status 同时被手工标记、文件夹检测、LLM 分类写入,彼此没有优先级约定。
→ 实查交叉表:33 个回复者里两边一致 23、一方有信息另一方无信号 10、真正互相矛盾 0 条。已落成 apply_lead_status():手工永远生效、弱结论(other/auto_reply)不覆盖强结论、真冲突标记 status_conflict 且原状态不动。三个写入者全部接线,实测六个用例符合设计。
本轮反复栽在这上面:退出码 0 是管道里 tail 的;「各类之和 == 扫描总数」在 0 == 0 时恒成立,于是扫到 0 封也打印「✓ 无静默丢弃」;连跑两次看 inserted 是否为 0 这个判据,会被确定性 message_id 掩盖成假绿。
→ 现有三道防线:seen==0 退 4;去重失效退 3(触发条件 inserted>20 且命中率 <10%,由 100/5% 收紧而来);另加一级只警示不拦的「插入量异常」。三者都用端到端测试验过真实退出码,不是只测布尔表达式。
icp_v2_reclass.py 的注释写明「不重新爬 website(省 LLM/HTTP 配额),直接用旧摘要判」。实查 10,537 家公司,57.8% 的 website_summary 不足 80 字;被判 non_icp 的 4,081 家里 83% 摘要不足,unknown 的 942 家里 99% 不足。
→ 人工抽样 20 家疑似误判 8 家(40%)。Graham 手工核了三家全部坐实:Nextera POS 的库存域名已死、COCARD 的摘要抓自错误域名、Coastal 的官网首页明写在代理 KwickPOS。证据就在网站上,判定器选择了不去看。否定结论会让公司永远进不了队列,且无任何报警。方案见 TASK8_ICP_RECLASSIFY_DESIGN_20260901.md。
reply_detected 漏标 41%;stage_entered_at 26 家挤在一小时内(录入时刻);t1_sent_at 有两条早于外联系统启动;created_at ≠ 首次接触(TWC 的往来在 2024 年,却落在 30 天冷发窗口内)。
→ 判「约没约过会」最强的信号是日历,不是任何布尔字段。9/2 再升一级:从「日历邮件」(Accepted: / Invitation:)升到日历 API 的 Meet 抄本时长——邮件只能证明「约了」,抄本时长能证明「开了,开了多久」。任何「时间戳」在用作判据前都该先看它的分布。
_is_closed_customer() 是 Apollo 拉线索时的入口闸,不在 Hermes 发信路径上。run.py 的 compute_queue 只有三道闸(tracker 状态、tracker 抑制名单、booking_suppress),全都不查 CRM 阶段。
→ 一个联系人先进 tracker、后在 CRM 被标成拒绝,入口闸就再也拦不住它。实测影响面 1 人(已单独止血,2027-01-15 自动放行)——入口闸基本有效,不是系统性泛滥。结构性的出口闸要改生产发送器,未做。
判断闸必须在 reveal 之前,否则每验证一个不合格线索都在烧钱。这决定了上游自动化不能简单地「先取数据再筛」。
→ 需要先证明 reveal 前的信息足够 LLM 做判断,再谈自动化。
目标:让 Supabase 成为统一观测账本(不是「单一事实源」——阶段 2 后 tracker 仍是发送决策的权威源)。九轮外审,每轮都出实质 P0——而且后三轮里,最严重的那条都是上一轮的修复动作造出来的。🛑 第九轮后已停循环,详见 §11。
| 轮次 | 被打中什么 | 形状 |
|---|---|---|
| 1 · 内部 | 6 条攻击命中 5 | 基本功 |
| 2 · Graham | 推翻根本定位——「不迁移发送器」是我自己设的非目标,不是查出来的约束 | 定位 |
| 3 · 外审 | 把「两组读方不重叠」当优点,实际是缺陷:不干扰 = 不生效 | 观察对、推论反 |
| 4 · 外审 | 历史 99 人没有任何阶段会修复他们 | 数字对、机制对、接不上 |
| 5 · 外审 | 待发的 79 人会被双发;且刚立的「唯一权威」规矩自己就没执行 | 问题框窄了 |
| 6 · 外审 | 三家全部否决直接执行,12 条 ≥2 家命中(含 staged 会被 run_promote() 放回 new) | 没审下游读方 |
| 7 · 外审 | 三家仍不建议执行,9 条 ≥2 家命中——最重的两条是我修第六轮时亲手造的 | 修复动作本身是缺陷来源 |
| 8 · 外审 | 三家第三次不建议执行,8 条 ≥2 家命中——paused 一个值背三种语义、coalesce 把假阴翻成假阳,又都是我修出来的 | 同上,且静态检查抓不到 |
| 9 · 外审 | 三家第四次不建议执行,7 条 ≥2 家命中——最重的一条是 L3 只改 Supabase paused,根本挡不住 Hermes 发信 | 🔴 把发送侧的问题装在了账本侧 |
阶段 1.5 要把 94 人从 new 改成 contacted,而这 94 人100% 是 lead_source=linkedin,正好撞上 linkedin_outreach_v3 的取件条件(status=eq.contacted,真发 flag 已开)。
→ 目前靠 next_action_at / runner_node 为 NULL 挡着,那是巧合不是设计——冷发引擎发完就会同时写这两个字段。所以回填不得写它们。外审看不出这条,因为它只看文档,看不到 .env 和 launchd。
v7 写的复验是表级的:「lead_source=linkedin ∧ status=contacted ∧ 两字段非空 的行数必须为 0」。实查动手前就已经是 61 行,不是 0。原文那个「0 人」是对 94 人回填 cohort 的口径,被复述成了表的现状。
→ 两种错法都致命:照原样验 ⇒ 回填后看到 61 行会错误归因成「我造出来的」;反过来把它「修」成 0 ⇒ 打断一条合法的在跑队列(A3 是设计内的 accept 后跟进)。
正确形式必须限定在回填 cohort 内:where id = any(:backfill_ids) and ...——缺这一行就是判据落在了被测对象之外。另立一条表级基线不变量(回填前后该数必须都等于 61)防止顺手污染。
共同形状:每一轮都在一个更大的范围上漏掉了东西,而每一轮我都以为这次齐了。暴露面 89 → 99 → 178 → 173,对象从 tracker 的 866 人改成库侧全部 311 人。v7→v8.2 已连送第六至第九轮外审,四轮共 36 条 ≥2 家命中、全部修完(现为 v8.3)。🛑 第九轮后停止循环——不收敛,改做演练。「这次应该干净了」这个念头本身就是信号——第七轮最重的两条正是修第六轮时新造的。阶段 1.5 已按证据强度分三批:批 1(56 人,库内自证、零外部依赖)可先跑,实测与日历零重叠、与那 61 行也零重叠。
Graham 9/2 提的:「已经开过会的联系人,日历上可以和冷发列表交叉对比,尤其是带 meeting note 的,这种应该是最准确的吧」。方向成立,但「有内容」的定义必须改,否则会产生系统性漏判。
实读两份纪要正文,两份的 Summary 都是空的,原文一字不差:“A summary wasn't produced for this meeting because there wasn't enough conversation in a supported language.” 会是中英混说的,Gemini 直接拒绝出摘要。
→ 真正有内容的是 Transcript 时长:Competitive Technology 00:08:38、Carolina PayPoint 00:09:34。判据落在时长上,不是摘要上——否则这批中文团队的会议会被系统性判成「没开过」。
证明对方到场并发言。本轮未达到——没统计说话人数,可见片段里只有 Graham 一个人。
证明 Meet 真的开了并在录音。本轮已达到这一级,8 人的结论都建立在这里。
responseStatus='accepted'
对方答应了,无到场证据。全部外部参会者里 accepted 54 / needsAction 31 / tentative 3 / declined 1。
needsAction
没有任何证据。约了而已——这正是 appointment_scheduled 这个字段一直在混淆的那一层。
| 证据 | 公司 | 库侧 status / outreach_status | tracker.status |
|---|---|---|---|
| 本人参会 | Business Payment Systems | staged / done | done 已挡 |
| 本人参会 | Carolina PayPoint | staged / done | replied 已挡 |
| 本人参会 | Competitive Technology Solutions | staged / queued ← 库里以为待发 | done 已挡 |
| 本人参会 | Diversified Payments | staged / done | done 已挡 |
| 同公司 | Advanced Merchant Services | staged / done | done 已挡 |
| 同公司 | BAMS Holding Group | staged / done | done 已挡 |
| 同公司 | Business Payment Systems | staged / done | done 已挡 |
| 同公司 | Premier Merchant Services | staged / done | done 已挡 |
8/8 在 tracker 里是终态,Hermes compute_queue 会跳过 ⇒ 即时重复触达风险 = 0。但这层保护是巧合不是设计:done 的语义是「三封序列跑完了」,不是「我们见过面了」,任何一次重新导入都会绕过它——与 §07 那条 LinkedIn 情形同形。
顺带机器化解决了一个挂起的待办:2ee19f8b Competitive Tech 原记着「需人工看那封 Re: Invitation: 正文,机器判不了」。日历直接判了——5/29 有会议实体、对方 accepted、抄本 8 分 38 秒、OrderPin 侧三人在列。
· 说话人数未统计 ⇒ 停在 L2,没到 L1
· Recording 附件未采集——我给采集写的字段规格是「title 含 Notes」,把 2 个录像附件滤掉了(Invoteq 6/03、Gobble 7/29)。录像是比纪要更硬的证据,被我自己的过滤条件排除了
· 取消的事件取不到(API 无 showDeleted)⇒ 无法回答「约了但取消了」
· 窗口只到 2026-05-01。邮件实证最早到 2023-12-21(EatNnet),「历史上开过会」的完整问题需要把窗口拉到 2023
· LinkedIn 已邀请的 436 人无邮箱,无法与日历做邮箱联结
另:58 个外部参会邮箱里只有 5 个在 outreach_sequences 里——大部分会议来自对方自助预约(19 场描述含「Booked by」),不是外联打出来的。这从一个新角度再次印证了 §02 那条「触达约会率的分子被污染」。
Graham 说的是「用完本月配额」,实查计费周期是 2026-08-16 → 09-16T15:04Z,不是自然月。按月底算会多给自己 14 天。
结论:好线索已被捞走。同一批源页面里,公司层已被挡掉的 657 人中 42.9% 公司名含 payment/merchant;net-new 2,215 人只有 1.9%——密度差 22 倍(放宽到 pay|pos|iso 仍差 6 倍)。
建议:不要追「把 2,021 用完」。额度的正确用法是「有多少合格线索就 reveal 多少」。真问题是搜索条件为什么产出 97% 无关线索——那是供给质量问题,应从已实测的筛选条件代价表入手,而不是用同样条件补跑更多搜索。
原因:① 在这个池子上花掉 2,021 credit 比让它作废更糟——等于往外联队列灌 2,000 个非 ISO 联系人,代价是发信支出 + 域名声誉。② 上个周期也作废了约 70%(~1,759 / 2,510),当时定性同样是「主因是没用完,不是用在低质量线索上」——同一件事第二次发生。③ --review 首批 24 条 KEEP=0,判出的 GoTab / Pomodo(POS 软件,A 类)、Decorative Paint(油漆厂,D 类)就是这个池子的真实成分。
⚠️ 0/24 不能外推:--sample 取 cands[:N] 是前缀不是随机。但它与整池的关键词密度一致(前 24 条含支付关键词 0 家)。
⚠️ 这次 0/24 不是已知的两个假 0:两家独立同意分类且理由具体。对照 2026-07-26 那次假 0——20/24 名字字面写着 Merchant Services 却被判「无证据」,那是 rubric 自我否决。
⚠️ 上一版的密度差 17.7 倍 → 22 倍:旧口径拿「1,340 人中 633 家」当分母,而 counts.filtered=1,340 里只有 657 落进了 prefilter_20260902.json,另外 683 人(已 reveal/已在库/台账)根本没落盘 ⇒ 那组数复算不出来。现口径只用落盘文件里能枚举的两群,判据 --pool 每次重算。
结论:codex 两批全部 300s 超时,panel_produced = {codex:0, deepseek:24, qwen:24},共识自适应降为 ≥2/2(两家必须都同意)。
建议:已钉死 Xtoken/gpt-5.6-sol(实测真实尺寸 prompt 52s / rc=0 / 合法 JSON,远低于 300s 上限)。Graham 已定不加回退。
原因:apollo_netnew_leadpull.py:1595 裸调 codex exec,不传 --model ⇒ 吃 ~/.codex/config.toml 的 XTokenClaude/claude-fable-5 @ model_reasoning_effort=xhigh。而 extreview:411 显式覆盖成 Xtoken/gpt-5.5 并带回退——同一个中转站,只有 extreview 绕开了 fable-5。中转站实测在跑(:10100 回 200,供 23 个模型,含 Xtoken/gpt-5.6-sol)。
⚠️ codex 是这个 panel 里唯一不走 mods 的腿;它缺席时 DeepSeek+千问共用同一个二进制与 role prompt,输入侧缺陷会让两票一起错且照样出票。
响应里有四个看着像配额的字段。此刻已消耗 489,但:
| 字段 | 值 | 会动吗 |
|---|---|---|
num_credits_remaining | 2021 | ✅ 唯一会动 8/24 是 2059 |
effective_num_lead_credits | 2510 | 恒定 |
num_lead_credits_used | 0 | 恒定 —— 实际已用 489 |
total_unified_credits_used | 0 | 恒定 |
→ 仓里已经栽过一次:曾用 effective_num_lead_credits 的「2510 → 2510 零 delta」证明「搜索免费」。那个字段本来就不动,零 delta 不构成任何证据——结论碰巧对,证据是假绿。读错字段 = 做一个永远不会跳闸的闸。
先前结论是「REST 拿不到 credit,只能走 MCP」。错在探测判据写窄了——REST 请求漏传了 include_credit_usage=true(MCP 那边传了)。
| 端点 | 结果 |
|---|---|
/v1/users/api_profile?include_credit_usage=true | 200 含余额 ← 脚本用这条 |
/v1/users/api_profile(不带参数) | 200,仅 6 个字段,无 credit |
/v1/users/api_profile + ?api_key= | 422 |
/v1/usage_stats/credit_usage | 404 ← 仓里注释说的是这条 |
MCP apollo_usage_stats_credit_usage_stats | 含周期起止 ← 只有这条给归零日 |
→ 闸可以纯脚本实现,不依赖 MCP(余额够用)。但归零日 REST 拿不到——已定 B+C 组合,A(写死常量)不实现,见下方实现块。写死一个会过期又不报警的常量不可接受,那正是本项目反复栽的形状。
写完「.env 的 key 是死的」之后继续追「就算换好 key,钱花得出去吗」——花不出去,而且另外两层是别人刻意设的:
| # | 阻断 | 实证 | 能不能改 |
|---|---|---|---|
| 1 | .env 的 APOLLO_API_KEY 无效 | /auth/health 回 is_logged_in=false;/email_accounts·/labels·/typed_custom_fields 三个免费端点全 401。与 Keychain 里的 master key 指纹不同 | 可修(换 key) |
| 2 | REST search/match 被 plan-gate | apollo_netnew_leadpull.py:39-47 模块自述:/mixed_people/search 回 403 API_INACCESSIBLE;run_pull():2013 因此是返回 2 的 stub | 账号计划 · 改代码没用 |
| 3 | DB ledger 对 apollo 硬封 | 实查 pg_proc:reserve_quota() 里明写 IF ... lower(btrim(p_provider)) = 'apollo' THEN → RETURN FALSE(migration 076b) | 刻意封的 |
→ 链条实证:_reserve_credit():327 调 guard("apollo",N) → reserve_quota 对 apollo 恒 FALSE → 抛 QuotaHalt → 一分不花。⇒ 无论实时闸开不开、.env 的 key 修不修,apollo_email_backfill 这条路都花不出去钱。
→ 只验读余额那把 key 是不够的:闸读余额用 master key,花钱却用另一把。只验读的那把,就会做出一个「余额 2,021、充足、放行」→ 然后每次 reveal 全 401 的闸——钱一分没花、额度一个没救回来,而日志看起来一切正常。已加 verify_spend_key() 在读余额之前先验花钱那把。
| 决定什么 | 需要 Graham 做什么 | |
|---|---|---|
| A | 启用实时闸——以后花 Apollo 钱时按实时余额判,读不到就停 + 邮件 | 设 APOLLO_LIVE_QUOTA_GATE=1 + APOLLO_CREDITS_PER_MATCH,并处理 .env 的死 key |
| B | 🔴 按合格供给 reveal——9/16 前能过 --review 多少就 reveal 多少,不以用完 2,021 为目标 | 排期:先跑全量 --review 量通过率;通过的才走 MCP bulk_match → 落盘 → --ingest。⚠️ --ingest 现在写的 lead_source='linkedin' 且 lead_contact_id 全 NULL,接之前先修(见 §01 注 ①) |
→ A 不做也能做 B;A 做了也不能替代 B。我一开始把 A 当成了 B 的前置条件——那是错的,而且是本项目的典型错法:把一个架构目标摆在一个有硬死线的业务目标前面。
scripts/leadgen/apollo_quota_live.py + 判据自检 + pytest。开关 APOLLO_LIVE_QUOTA_GATE 未设 ⇒ 生产行为一字不变。
| 验证项(全部实跑,零 credit 消耗) | 结果 |
|---|---|
| REST 读余额 | num_credits_remaining=2021 |
| MCP 交叉验证 | lead_credit.left_over=2021 与 REST 一致 |
| 8/24 记录反推 | 2059 − 38 = 2021 精确对上 |
| 判据自检(9 条喂已知坏样本验它会红 + 1 条干净样本) | 10/10 |
| 变异测试 A:余额回退到恒定字段 | 自检第 1 条变红 |
| 变异测试 B:读不到就放行 | 自检第 4/5/6 条变红 |
| 变异测试 C:去掉 24h 去重 | 自检第 7 条变红 |
| 既有 guardrail 测试 / 新增 pytest | 50 passed 无回归 + 8 passed |
| Python 3.9 与 3.11 双跑(cron 有 3.9 回退路径) | 都 10/10 |
生产 env 下 check | allow=False, reason='gate_disabled' 开关确实是关的 |
→ 归零日选了 B+C 组合,A(写死常量)不实现:B = MCP 读到的周期落盘 + fetched_at 过期告警(已落 2026-09-16T15:04:08Z,还剩 14.8 天);C = 余额单调下降,一次 ≥100 的上跳即判重置,且能证伪 B(自检第 9 条专测这个)。A 被否的理由不是「不准」,是「它不准的时候没有任何人会知道」——与看板 §08 判据纪律里那几条同族。
① fail-closed 拒绝花钱(money path 永不 fail-open,本仓已立的规矩)
② 邮件通知 Graham(他 9/2 明确要求)
→ 两件必须同时做。只做 ① 就是静默停摆——配额会被悄悄拖到 9/16 作废,而这正是本仓最常见的失效形状;只做 ② 就是 fail-open 花钱。
当前之所以一分没花,就是 ① 在生效而 ② 不存在:APOLLO_INCLUDED_MONTHLY_CREDITS 未设 ⇒ run():606 显式 return,日志 8/29–8/31 每天打 refusing to spend,没有任何人被通知。
起因是「面板数字一直不准」。查下来不是 SQL 分类条件写错,是更上游的东西坏了。
① governor 拦截原因写进日志(原来被 >/dev/null 2>&1 丢掉)+ TG 通知;② 新建 pipeline_stale_alert.py + 独立 launchd 09:30,与 daily_leadgen 解耦(原来告警在被跳过的那段里面);③ 调度 10:00/11:00 → 03/04/05。
④ migration 084 v_cron_last_success(不限窗口取 MAX,回答「上次好是什么时候」);⑤ 补 kp_found/phone_validated 两个从来没有过的埋点;⑥ migration 085 挡 qa_perf 写进生产线索表。
判据自检 5 条全过(含「视图整行消失也必须红」);约束用坏样本实证:qa_perf 被挡、正常来源放行、探针行零残留。openclaw workspace 604ff03。
③ 供给主线换成 Apollo + LinkedIn Sales Navigator MCP,Google Places 不恢复——它当年找的主要是 merchant(终端商户),而现在要找的是 ISO。这同时解释了库里那批「名字像餐厅」的线索是怎么来的。
① 规则 6a 取「接受名额 0」:配额闸照接,槽 2 只对以后走闸进来、带 function 的新公司开放,存量 1,442 家维持 1 个联系人。备选两条已排除——回填存量职能要烧 Apollo credit(plan §不做),关掉 B3 在 v7 已封死。
② 闸落在 company_leads 写入口,不挂在 promote_raw 上。实查:绿线近 30 天只放行 17 条,而 company_leads 新增的 91.5% 根本没走 raw_leads。
扫描脚本第 62 行取了正文,第 74 行拼输出时没带上它。2,354 行 body_text 全空 → LLM 只能看标题 → 每次回「内容为空,无法判断」→ 全判 other。
过去两天反复改 SQL 分类条件,改的都是症状。修复后 2,106/2,132 行有实质正文,interested 从恒为 0 变成 13。
改为直读 ~/Library/Mail/V10/**/*.emlx,一并修掉四个坑:locale 日期被解析成 12193 年、PostgREST 1000 行截断、不匹配就静默丢弃、sent_at 写当前时间。
立「各类之和 == 扫描总数」等式断言,全量 16,940 封实测成立。
八值枚举 + DB CHECK 约束;Python 与 TypeScript 各一份共享常量;统计函数重写为单一数据源。回复分类合计 = 已回复人数,首次自洽。
删除 1,496 行脏数据(日期最远到 2057、94% 未关联线索)、去重 1,586 行冗余、修正 19 行方向错误。每步删除前完整备份落盘。
凭据从 8 处硬编码收敛到单一 0600 文件;1.5 MB 含 1,467 处明文邮箱的 SQL 移出仓库(历史干净);移除一个每 30 分钟失败、plist 里带明文 API key 的定时任务。
收回了 anon 对线索表的读写与 5 个 SECURITY DEFINER RPC 的执行权(其中一个会把 33 条真实客户邮箱返给任何持 anon key 的人)。双边验证:anon 401、authenticated 仍读到 1,528 行。
密钥本身仍未轮换——见下一步第 1 条。
外审指出「用应用层护栏补偿数据库不变量属复杂度错配」——这条对,而且建索引时当场炸出 19 个重复:一条直接 SQL UPDATE 把 19 行方向翻转,正好撞上已有的 19 行,当时无人复查。
现在 uq_email_threads_natural_key 唯一索引是最终保证,应用层去重降为优化。去重预检也从写入后移到了写入前。
原判据是 unsubscribed ∨ bounced ∨ reply_detected。实测 reply_detected 漏标 41%——56 个有 inbound 邮件的线索里 23 条标着 false,Alt POS 收了 13 封仍标未回复。
现加入「存在非自动回复的 inbound 邮件」,并排除 Automatic reply:——只看「有没有 inbound」同样是单信号源。据此补处置 4 条漏筛的真回复。
COLDSEND_HALT §6-② 卡了两周,因为「不确定有没有可靠的拒绝时间戳」。查清了:stage_entered_at 不可用(30 家里 26 家挤在 2026-04-16 一小时内,是录入时刻不是拒绝时刻),last_follow_up 可用。
放行 5 家(付款/已转出仍永久排除,无时间戳 fail-closed)。真实数据验证:改动前挡 125 家,正好对上代码注释里的「应挡 125 家」。
把「谁会向外发信」完整列了一遍——此前从没列过,所以每轮的暴露面都在扩大(89 → 99 → 178 → 173)。查出 engine.js 在标记暂停后又发了 49 天。
合并方案在「改了一段没改引用同一结论的其它段落」上连续犯了 14 次——立规矩没用,因为规矩只约束将来写的字,不动已经写下的复述。
现落成 scripts/check_plan_consistency.py,有退出码,按章节区分历史区与当前区。不再每次现写 grep。
三次交付三次失败:cron 读不到 token、哈希跨进程随机导致去重失效、时间戳两侧格式不一致。第四次加上完全磁盘访问权限后,seen: 16,940,首次自动运行成功。
判据纪律、外审逐轮台账、内部 debate 记录。这些是「怎么走到今天」的证据链,不是当前要做的事 —— 读当前状态请看 §01–§03。
都是真栽过的,写在这里是为了下一轮不再栽。
| 看起来是 | 实际是 |
|---|---|
| 退出码 0 = 成功 | 那可能是管道里最后一个命令的退出码。判据要落在数据上 |
| cron 装了 = 在跑 | 112 次 traceback、0 次进 main。要看日志内容,不是文件存在 |
| 交互式 shell 测通 = 能部署 | 环境变量、PATH、磁盘权限全不同。用 env -i 模拟 |
| 切了配置 = 新配置在工作 | TCC 授权对已运行进程不生效,要重启进程 |
| 护栏没报警 = 一切正常 | 0 == 0 恒成立。护栏自己会说谎,要构造反例验它 |
| 404 和 401 差不多 | 404 是表不存在,401 是有表没权限。含义相反 |
| 分布统计随手查一下 | PostgREST 默认只返 1000 行,采样让 linkedin 从 533 缩成 155 |
| 文档写了「已验证」 | 要有那次运行的证据。声称验证过 ≠ 验证过 |
| 它不碰我关心的那张表 = 不发信 | 按「写不写我关心的表」筛发送器,会整条渠道地漏掉。LinkedIn 那条就这么漏了 50 天 |
配置里写着 paused = 停了 | 同一个 job 名下可能有两套调度。查停没停要看产出,不是看开关 |
| 文档说「现在是永久排除」= 规则如此 | 那可能是在陈述实现缺陷。规则是「6 个月后可再发」。别拿实现当规则 |
| 字段名叫什么就是什么 | reply_detected 漏标 41%;stage_entered_at 是录入时刻。时间戳用作判据前先看分布 |
| 我 grep 过了,干净了 | 连续 14 次漏改。grep -v 的排除条件会把要查的行一起滤掉;grep -c 无匹配时 exit 1。判据本身要先用已知答案验一次 |
| 数出来 8 条就是 8 个 | 单位错了:那 8 是联系人行,实际是 7 家公司(Business Payment Systems 占 2 行)。而且里面 4 行是「同公司但本人没到场」,被算成了已开会。「已开会」的计数单位必须是公司,报数前先问:一行代表一个什么 |
| 会议纪要「有内容」= 有 Gemini Summary | 实读两份,Summary 全空——「conversation not in a supported language」。会是中英混说的,Gemini 直接拒绝出摘要。真证据是 transcript 时长(8:38 / 9:34)。用 summary 筛 = 中文团队会议全漏 |
| 纪要挂在这个事件上 = 这次会的纪要 | 同一份文档(自身日期 8/4)同时挂在 8/6 和 8/18 两个事件上——改期系列会继承前序附件。要用文档自己的日期,否则一次会算成三次 |
| 同域名 = 同公司 | 域名联结初值 38 行,绝大部分是 gmail.com 撞的。剔掉免费邮箱域后是 8 行。规模先估大又发生了一次 |
| 日历里没有 = 没开过会 | 只能证明开过,不能证伪:取消的事件被 API 服务端过滤(无 showDeleted),电话/Zoom 会不产生纪要(52 个事件里 17 个没有) |
任务名叫 followups = 它在管跟进 | 它每次都打「今日无需跟进」,真发 DM 的是另一个叫 linkedin_outreach 的任务。按名字找会得出完全相反的结论 |
在 customers 表里 = 是现有客户 | 367 行里 won_at 大多为空。判成交要看 current_stage / won_at,且终结态记在两个字段里 |
截至 2026-09-02 日历轮,grahamcrm 本地 main 领先 origin/main 53 个 commit(9/2 深夜实查);openclaw workspace 在 feat/tier3-ingestion-hardening 分支上,领先它自己的远端分支 75 个(此前记的「2 个」是那一轮的新增数,不是未推总数——我第一次量它时还量错了对象,去查了一条没人在动的 main,得到 0)。均未推远端。生产数据本轮动了 18 行(8 行 status、10 行 lead_status),每次都重跑了该表全套不变量。参与方:Claude (Opus 5) 实现与复核、Kiro/codex 独立复核、外审三家(Codex / DeepSeek / 千问)、Graham 执行需要人工权限的操作与最终拍板。
Codex「不建议直接执行」/ DeepSeek「不具备安全执行条件」/ 千问「有条件不通过」。12 条被 ≥2 家独立命中。A7 上轮已修,9/2 晚修完剩余 11 条并裁决了三家互相矛盾的 4 处,文档升 v8。⚠️ 其中 A6 / A10 各有一半不成立——三家都说的话,也可能是三家都只看了文档。
| # | 条目 | 命中 | 状态 |
|---|---|---|---|
| A1 | 全文人数口径不平(56+38+79+140=313 ≠ 311,实查已确认差 2) | 3 家 | 已修 |
| A2 | 冷发引擎「硬关闭」仍是要求不是机制(无 flag 名 / 默认值 / 失败行为) | 3 家 | 已修 |
| A3 | 阶段 1.5 缺备份 / 事务边界 / 回滚 —— PostgREST 无事务,「整批回滚」无可执行路径 | 3 家 | 已修 |
| A4 | 静默失败无补偿闭环,阶段 3 只发现不修复 | 3 家 | 已修 |
| A5 | 自动回复正则漏检,且 subject 为 NULL 被三值逻辑误伤(需 coalesce) | 3 家 | 已修 |
| A6 | 邮箱映射未封死,一邮箱多 sequence 会写错人 | 3 家 | 已修 |
| A7 | §4 表级归零判据与正文 v7.1 的 cohort 级修正打架 | 2 家 | ✅ 已修 |
| A8 | 未来新入队人群双发防护缺失(只修了存量) | 2 家 | 已修 |
| A9 | staged/contacted 下游读方未做条件级审计 | 2 家 | 已修 |
| A10 | 阶段 1 判据仍以 297 面板口径为基线 | 2 家 | 已修 |
| A11 | apply_lead_status() 终态白名单与 manual 优先级未定义 | 2 家 | 已修 |
| A12 | 批 2(38 人)仅 tracker 佐证,直接改 contacted 会造新假账 | 2 家 | 已修 |
我在 §3 阶段 1.5 把 LinkedIn 复验判据改成 cohort 级,§4 判据表忘了跟着改,仍写表级「必须 = 0」——而表级实际是 61 条合法的在跑队列。后果双向:误判回填失败,或为了归零把 61 条合法队列清掉。
→ 关键在于我的一致性脚本为什么没抓到:它只查过期数字,而这次漏改的是判据类型——数字一个都没错。已给检查器加第 2 类判据(表级归零断言必须带 cohort 限定),并双向验过:构造回归样本 exit=2 精确定位到行,修好的文档 exit=0。一个从没红过的检查等于没有检查。
| 轮 | ≥2 家命中 | 其中由上一轮修复动作引入的 |
|---|---|---|
| 6 | 12 | —(首轮) |
| 7 | 9 | 🔴 2 条,且是最严重的两条 |
| 8 | 8 | 🔴 2 条,且含最严重的那条 |
| 9 | 7 | 🔴 2 条(D1/D3),且 D1 是最严重的那条 |
→ 命中数在降(12→9→8→7),但没有趋近 0,且新缺陷的严重度没有下降。每轮修 N 条、引入约 2 条,而引入的那 2 条一直排在最严重的位置。同时三家在第八、九两轮都独立建议先做预生产演练而不是继续审文档,第九轮三家还都把「文档过大、执行路径被历史台账淹没」列成了问题——这份文档现在 1,800+ 行,相当一部分是外审台账本身。
→ 决定:不再进入第十轮文档外审,下一步是隔离库演练。一句话:文档审查已经审到了它自己会制造缺陷的区间;而演练能发现的东西,文档审不出来。
→ ⚠️ 这不是「通过了」。三家仍明确判定「不建议直接进入生产真写」,该结论保持有效。四项硬前置不变:① D7 阻塞项(查清冻结 Hermes 发送器的可执行命令)② 隔离库演练跑通「快照→分块写→复验→撤销→复验」③ ck_lead_status_source 加 'tracker' ④ 3 家 opt-out 清理 + 规则 6a 处置。恢复外审的条件是拿演练产出去审,不是拿文档。
§07 白纸黑字:「tracker 仍是发送决策的权威源,Supabase 是发送后的派生账本」。我却连着三轮用 status 值去解决「别再发这个人」。
→ 改 status 不会让 Hermes 少发一封信,它只是让账本看起来像挡住了。L3 的阻断已改走 tracker 抑制名单(compute_queue 真正查的三道闸之一)。paused 从此只剩两种原因,C1 为了兜住多义性而发明的 paused_reason 机制整个删掉。
→ 当一个修法需要发明新机制来兜住它自己引入的歧义时,先怀疑被修的那个设计。
→ 另一条(D3):v8.1 说每日 guard「只是补丁」说轻了,v8.2 说第二道闸是「唯一硬闭环」说重了——两次都为了让结论干净而说过头。闸和不变量的差别不是强度,是能不能被绕过而不被发现。
| 轮 | 最严重的那条 | 它是遗留缺陷还是我修出来的 |
|---|---|---|
| 6 | staged 会被 run_promote() 放回 new | 遗留 |
| 7 | paused 没有出口 | 🔴 我修第六轮时造的(裁决①) |
| 8 | paused 一个值背三种语义 | 🔴 我修第七轮时造的(B1 + B6 撞在一起) |
| 8 | coalesce 把假阴翻成假阳 | 🔴 我修第六轮时造的(A5) |
→ 连续三轮,最严重的缺陷都来自上一轮的修复动作。§08 那张「同一个毛病第 N 次」记的是漏改;这张记的是修改本身。后者更难防:漏改可以靠脚本查,而「改 A 的时候在 B 上开了个洞」没有任何静态检查能抓到。
→ 已落成合并方案 §2b 的第三条写作规矩:每修一条外审意见,必须显式回答「这个改动新增了哪些状态 / 路径 / 语义?谁会读它们?它们怎么离开?」三条实证教训同一个根 —— 状态机既要看入边也要看出边(B1);一个值承载多种原因,转换规则必然对其中至少一种是错的(C1);修一个方向的偏差前先问它会不会把偏差翻到另一个方向(C5)。我在改一个点,而它是一张图上的节点。
| # | 条目 | 命中 | 处置 |
|---|---|---|---|
| C1 | 🔴 paused 一个值背三种语义(批3 / queued guard / L3 待审),释放规则按「有没有发信」判 ⇒ 静默清空人工队列 | 3 家 | 已修 原因落 outreach_log,释放按原因分叉 |
| C2 | 硬关闭只挡首封(:496),t2/t3(:784)与 LinkedIn DM(:1678)走另外的谓词,flag 关着照发 | 2 家 | 已修 四个取件点全列 |
| C3 | 批 2 抽样 10/38 不足 | 3 家 | 改判 38/38 全验 |
| C4 | 每日 queued → paused guard 是定时补丁不是结构性防护 | 3 家 | 已澄清 硬闭环本来就在 A2 第二道闸 |
| C5 | coalesce(subject,'') 把假阴翻成了假阳:空主题 inbound 被当成真回复 | 1 家(成立且是我造的) | 已修 空主题路由 L3,两边都不站 |
| C6 | outreach_status 值域/判据口径矛盾 | 2 家 | 已实查钉死 CHECK 允许 8 值,在用只有 2 个 |
| C7 | 补偿式撤销有 crash 窗口,并发下不可靠 | 3 家 | 已修 快照先写后改 + manifest + A4 兜底 |
| C8 | 冻结窗口漏掉了真正在发信的那个 —— Hermes 调度器 | 2 家 | 已补 |
→ C3 是一次裁决改判:第七轮三家方向相反(Codex「不够且没必要」/ 千问「要 38/38」/ DeepSeek「样本量不足」)构成 1:1:1 分裂,按规则没自行拍板;第八轮 Codex 改口后 ≥2 家一致,分裂消解 ⇒ 主会话拍板。理由不是「多多益善」:总体只有 38,抽样省下的 28 次核对,换来的是一个永远无法消除的推广风险,而写入不可逆。总体小到可以全查时,抽样不是节省,是自找的不确定性。
→ C4 的处置是「承认它不是防护」而不是「把它升级成防护」:防双发的硬闭环本来就在 A2 第二道闸({status='new' 邮箱} ∩ {tracker 866} == ∅ 不满足则冷发拒绝启动)。guard 挂了、跑晚了、顺序错了都不会导致双发 —— 因为冷发根本没启动。外审问的「调度顺序怎么定」,前提就不成立。
→ 🔴 三家一致的一条流程建议已采纳为执行前置:真写前必须在隔离库完整跑一遍「快照 → 分块写 → 复验 → 撤销 → 复验」并落盘两次复验输出。这也是对「第八轮还在出 P0」最诚实的回应:文档已经审到收益递减的区间,而演练能发现的东西,文档审不出来。
Codex「仍不建议直接执行」/ DeepSeek「仍存在会导致方案执行失效或产生新的静默风险的内部矛盾」/ 千问「不要直接进入生产真写」。3 家出票,无降级。
| # | 条目 | 命中 | 处置 |
|---|---|---|---|
| B1 | 🔴 paused 没有出口——批 3 被 Hermes 发送后永远停在 paused | 3 家 | 已修 矩阵前置 new → new 或 paused |
| B2 | 🔴 口径漏改第 16 次(判据表仍写批3→staged、§2b 指针行仍 175/96、§5 风险表仍旧值、窗口内仍 89) | 3 家 | 已修 4 处 + 三条检查器缺口全堵 |
| B3 | 冷发第二道闸只看得见批 1 的 56 人,批 2/批 3 对它完全隐形 | 3 家 | 已修 改成 {new 邮箱} ∩ {tracker 866} == ∅ |
| B4 | A8 的 queued → paused 没有触发机制——sink 只在发完信后跑,而 queued 的人恰恰没发过 | 2 家 | 已修 移到独立每日任务 |
| B5 | 批 4「不动」/ 终态体检归零 / 311→138 三者互相矛盾 | 2 家 | 已修 改成 311 → 138−k,k 由 dry-run 给出 |
| B6 | L3「不改 status」= 疑似回过信的人在等裁决期间照常收冷发 | 2 家 | 已修 new → paused |
| B7 | apply_lead_status(...,'tracker') 未迁移即被设计依赖 | 2 家 | 已修 升为带验证查询的有序前置 |
| B8 | 补偿式撤销覆盖不了已发生的副作用;执行期间未冻结写方 | 3 家 | 已修 写明边界 + launchd 冻结窗口 |
| B9 | run_promote() 本身的无差别 promote 缺陷未修 | 2 家 | 单独立项 写明本方案不修的理由 |
→ B1 的教训值得单独记:裁决①本身没错(staged 确实会被 run_promote() 放回 new),错在换一个状态值 = 换一整套下游语义,而我只审了「谁会读它」,没审「它怎么离开这个状态」。状态机既要看入边,也要看出边。staged 有出边(方向错了);paused 一条出边都没有——我把「有一条坏出边」换成了「没有出边」,并把它当成了改进。
→ B4 是同族的另一面:把一条规则写进「状态转换矩阵」,会让人默认「有个东西会执行它」。但矩阵是被动的,它只描述「事件来了怎么办」,不产生事件。没有事件源的矩阵行 = 一条永远不执行的规则,而它在文档里长得和其它行一模一样。
| 漏掉的位置 | 为什么没报 | 堵法 |
|---|---|---|
| §2b 的权威指针行 | 整节豁免太粗:§2b 在历史节名单里,但它装着唯一权威指针表——那是当前内容 | 带「唯一权威」标记的行即使在历史节里也必须查 |
| §5 风险表那一行 | 🔴 行级标记连坐(本轮新形状):行尾一句合法的「v6 曾写…」把同一行行首的当前结论一起豁免了 | 标记只豁免它自己所在的从句 + 紧邻它前面的那个从句(括号注的是它前面那段) |
| 批 3 目标值 / 窗口内 89 | staged 是目标值名不是数字(第 1 类看不见);89 从没登记进 STALE | 补登记 + 台账节纳入历史豁免 |
→ 中间那条与 v7 那次「grep -v '§0d' 把要修的行一起过滤掉了」是同一族:豁免规则的粒度比缺陷的粒度粗,豁免就会盖住缺陷。
→ ⚠️ 放松豁免规则之后必须回来验它仍会红,否则就是「改到绿为止」——本仓最常见的自欺形状。已对三条缺口各钉一条回归自检(喂当初真漏掉的那三行原文)+ 一条反向控制(纯历史叙述不许误报)。自检 4 条 → 9 条,全过。
v7 让批 3 的 79 人写 staged,语义写的是「已进入外联流程、由 Hermes 负责发」。实查发现 staged 在本仓的真实语义是反的:
apollo_netnew_leadpull.py:2086 run_promote()
取件谓词 status = eq.staged(默认不带 lead_source 过滤)
动作 sb_patch(..., {"status": "new"}) ← staged → new
→ 那等于给正在修的「双发」装了一个单条人工命令(--promote --apply)就能复位的开关,而执行那条命令的人看不到任何提示说这 79 人是 Hermes 的。⇒ 裁决①:批 3 改写 paused——零自动写方、已在 OCCUPYING_STATUSES 内、且不宣称已触达。
| # | 分歧 | 裁决 | 一句话理由 |
|---|---|---|---|
| ① | 批 3 怎么处置(扩展 / 改矩阵 / 暂缓) | 改矩阵:写 paused | staged 会被 run_promote() 一条命令放回 new |
| ② | 文档还能不能出现硬编码人数 | 可以,但必须可复算 | 常量的问题不是「可能不准」,是「不准时没人会知道」;挂到会报警的载体上就能留 |
| ③ | 终态处置的自动化程度 | 分 L1/L2/L3,单信号一律不自动 | 终态不可逆且静默,而假阴(漏标 41%)与假阳("Away from Desk")两个方向都已实证 |
| ④ | 「整批回滚」是实现它还是降级承诺 | 降级承诺 + 实现补偿式撤销 | 承诺一个做不到的机制比承认做不到更危险——执行者会按「反正能回滚」的胆量去跑 |
真库 + tracker 快照复算:311 = 56 + 38 + 79 + 138(v7 写的 140 是错的,四批加总 313≠311)。
| 批 | 总数 | 30 天窗口内 | 风险 |
|---|---|---|---|
| 批 1 · 库内自证 | 56 | 47 | 重发 |
| 批 2 · 仅 tracker 知道 | 38 | 38 | 重发 |
批 3 · tracker queued | 79 | 65 | 双发 |
| 批 4 · 不在 tracker | 138 | 110 | 正常新线索 |
合计 = 库侧 status='new' | 311 | 260 | — |
→ 前两类判据都抓不到 A1:第 1 类查过期数字,可 56/38/79 三个数全是对的;第 2 类查判据类型,与人数无关。漏的是几个正确数字之间的算术关系——每个单独看都「实查过、带日期」,加起来才露馅。⇒ 新增 第 3 类判据(加总平衡,离线,带 --selftest)+ recount_plan_headcounts.py(连真库 + tracker 复算,实跑与文档完全一致)。
① 我先断言「这三列是纯数据列,没有触发器」——实查有 2 个。outreach_sequences_anchor_n1 在 email/linkedin_url/discovery_meta 上触发且会抛异常,正好挡住我原本打算在 A12 里写的 discovery_meta.backfill_provenance;touch_updated_at 每次写都刷 updated_at,直接推翻了「撤销时比对快照时刻」这个 CAS 基准——用快照时刻当基准,撤销脚本会一行都改不了却看起来很安全。
② A6 / A10 各有一半不成立。实查重复邮箱 0 组(A6 说的「一邮箱多 sequence」今天不存在,真洞是 267 行根本没有 email,其中 62 行已 staged/contacted);297 = 571 − 274 今天仍精确成立(A10 错在它被写成裸数字、且混淆了存量与日增)。
→ 外审看不到库。不实查就照单全改,会把方案改坏在一个不存在的问题上。
Graham 9/2 定的新主线:把「Apollo 搜索产出 → 邮箱验证」这段打通并验证成全自动。
v1 草案 9/2 晚跑了内部对抗 debate,5 条攻击命中 5 条,其中一条推翻了标题目标本身。计划落在 .handoff/LEAD_PIPELINE_AUTOMATION_PLAN.md,已送外审。
三重阻断里有两重改代码绕不过去(详见 §10):REST search/match 被 plan-gate 回 403 API_INACCESSIBLE;reserve_quota() 对 provider='apollo' 硬 RETURN FALSE(migration 076b,刻意封的)。
→ reveal 只能走 Apollo MCP,而 MCP 要在 Claude 会话里调。目标改成「全自动除了 S4 这一个有名有姓的人工环」。这条要写在最显眼的地方——否则下一个人会花几天去「打通 S4 的自动化」,而那几天的产出必然是 0。本项目已有四次「建好了、过了外审、没接线」,这次的形状更糟:接线本身不可能成功,而失败原因在仓库外。
死线是「任务 7 9/10 开工」+「2,021 credit 9/16 归零」。而计划内容是改数据库层闸 + 收敛 9 个入口 + 存量清理 + 回流 5,680 家——8 天落不了地,落地了也不产生一个 reveal。
→ 劈成两条互不阻塞的线:线 A(抢额度,9/16 硬死线)= 每天一次 MCP bulk_match → 落盘 → --ingest,已验证跑通、不需要任何新架构;线 B(管道收敛)无硬死线。线 A 是排期问题,不是工程问题。
raw_leads 移到 company_leads草案第 1 条论证的是层(DB vs 应用),架构图画的是位置(raw_leads 之后)——草案把这两件事当成了一件。
| 闸装在哪 | 入口覆盖率 | 代价 |
|---|---|---|
raw_leads 上 | 86%(漏 1,599 行 + 4 个源) | 低 |
✅ company_leads 上 | 100%(所有入口的汇聚点) | 需处理 10,537 行存量 |
outreach_sequences 上 | 100% 但太晚 | reveal 已经花过钱了,违反 Graham 的硬约束 |
→ 架构图里 raw_leads 是入口之一,不是统一入口。Graham 说的「统一入口」在语义上是对的(目标态),但把现状画成目标态,下一个人会以为已经收敛了。
草案拿 uq_email_threads_natural_key「当场炸出 19 个重复」证明「应用层去重会静默失败」。但那个先例同时证明:在有存量的表上加约束,约束会立刻对存量生效并失败。
→ ALTER TABLE ... ADD CONSTRAINT ... NOT VALID(毫秒级、不锁表、对新写入立即生效)→ 存量清干净后再 VALIDATE CONSTRAINT。先止血,再清创。⚠️ 但 NOT VALID 期间「约束存在 ⇒ 数据干净」是错的推论——这条必须写进注释,否则它就是下一个假绿。
判定器两个方向都已实证会错:否定类目 ∧ 摘要不足 4,772 家;肯定类目 ∧ 摘要不足 1,660 家(占肯定类目 73%),其中 274 家已进外联队列。
| 阶段 | 对象 | 家数 | 前置条件 |
|---|---|---|---|
| R0 | ask_to_send_email ∧ 被判不合格 | 11 | 无 每条自带一条独立的人类正向信号 |
| R1 | ask_to_send_email ∧ 有邮箱 | 50 | R0 的结论 |
| R2 | 可回流 ∧ 有邮箱 | 468 | 判定器误报率已量化 |
| R3 | 全部可回流 | 5,680 | R2 的 ROI 为正 ∧ 误报率 < 阈值 |
→ 排除集必须写死并单元测试锁住加总闭合(五桶 = 10,537):已判不合格 4,739 / 测试垃圾 663→62(RLS_AUDIT% 那 601 行已删,级联带走 lead_stage_events 3,024 + lead_contacts 63;仅剩 __QA_PERF__% 62,已由 migration 085 挡住新增)/ 已在销售流程 33 / opt-out 16。与 §11 的第 3 类判据同款——加总闭合是判据,不是注释。
| 环节 | 自动 | 说明 |
|---|---|---|
| S1 Apollo 搜索 | 人工 | Graham 明确接受手动触发 |
| S2 规则闸 | 可全自动 | 确定性规则,可测 |
| S3 LLM 判 ISO | 可全自动 | 但误报率必须先量化(攻击 4) |
| S4 Reveal | 🔴 不可能全自动 | 账号计划限制,不是选择——必须有 Claude 会话调 MCP |
| S5 邮箱验证 | 可全自动 | MillionVerifier 已通 |
| 晋升进外联队列 | 人工 | 现状就是 --promote --apply 的人工闸门,保持 |
9/2 已拍板:接上确实只能开出 0 个名额(不是此前估的 143)——outreach_sequences 1,571 行里 discovery_meta->>'function' 非空 0、classifier_version 非空 0 ⇒ 规则 6a 对 100% 的存量公司触发。Graham 取「接受名额 0」:闸照接,槽 2 只对以后走闸进来、带 function 的新公司开放,存量 1,442 家维持 1 个联系人(见 §09)。另外硬阻塞比文档记的宽:实查 3 家 opt-out 冲突(Focus Merchants done / Cervion queued / Alt POS queued),load_company_occupancy() 撞到即 abort。