OrderPin 的 ISO 白牌获客外联系统——从找线索到 CRM 商务跟进共十一个环节,目标是把它跑成一条闭环。
外联实际是两套并行系统:系统 A(Hermes run.py 读 iso_outreach_tracker.json)每个工作日 20:00 真的在发信;系统 B(Supabase outreach_sequences)是本看板读的账本,冷发引擎当前停用。实测 tracker ⊂ Supabase,所以要解决的是状态同步方向,不是对齐两套数据。
本看板的数字若无特别说明均属系统 B;两套系统按邮箱去重后的 A ∪ B 合并口径在 §01 第一组指标里(两边不能直接相加);每次更新看板全部数字实查,不沿用上一版。
current_date − N,本版边界 2026-08-10 00:00 UTC结论:三段都在跑,但没接在一起。发送段在真发(A ∪ B 已触达 713 人、真人回复 54 家);供给段 9/3 起恢复,但新增 43 家 0 个带邮箱;加工段 kp_finder 每轮 1800s 超时。回信标记链 9/9 上午修通(此前自 8/26 起冻结两周,「已回复 33」是冻结值)。
建议:唯一队列已定 = Hermes tracker,接线做到 dry-run(候选 25 人,9/9 recheck delta = 0),验证 / 写入两步等 Graham 批(v3.3 + v3.3b 代码侧 9/9 深夜已做完并推送:--diff 外审 2/3 出票,两家同点的严重项已处置,单测 246 绿;push 钩子复审中);20:00 那轮实查:发送器 20:00:49 未登录即中止、0 封,根因是 Gmail 网页会话每 14 天到期 —— 要 Graham 重新登录并把 Workspace 会话时长改长,否则每 14 天断一次。
原因:下面四块就是全部现状 —— 每行自带实测值、时刻与复算路径。过程记录(当日盖章流水、根因叙述、外审台账)在页末 §13 与仓内 md,不占首屏;§13 里的数字是搬出时的值,当前值一律以本节为准。
盖章 2026-09-09 23:40 HKT:五道部署闸(图文一致 / 自检 / --live 16 锚 / --pool / --union 15 锚)本时刻全绿。会漂的数各自带时刻 —— 邮件正文入库每 30 分钟增量、tracker 每工作日 20:00 发信后变、流水线每天 14:00 跑后变;--live 的作用不是让数字永远绿,是让漂移当场可见。
两套系统不能直接相加:tracker 的 866 人全部在 Supabase 的 1,291 个有邮箱的人里,A 的发送 / 回信没有回写 B(B 记到的 T1 发送痕迹只有 A 的 40%)。下面按人(lower(trim(email)))取并集再去重;复算路径 scripts/ab_union.py,部署闸 --union 每次连 tracker + 真库重算。
t1_sent_at 非空 ∪ status=done;下方 B 卡的 274 只数 done,不是一个口径A(tracker)自己只有 replied / unsubscribed / bounced 三个布尔位,没有意图分类;B 有 LLM 意图 + lead_status。合并规则:真人回复的人若 B 知道,用 B 的 lead_status 分类;B 没有任何正文的 A 侧回信单列一行。A 标 replied 的 33 家里 3 家在 B 里只有自动回复,按真人口径不算。复算路径 scripts/ab_union.py,部署闸 --union 每行都比。
| 回复分类(A ∪ B) | 本轮(7/1 起) | 全部时期 | 说明 |
|---|---|---|---|
| 有意向 | 12 | 16 | B 的 LLM 从正文判定 |
| 已拒绝 | 11 | 14 | 含 9/8 晚拒绝的 US Transactions / PayPros |
| 已约会(仅约,未开) | 5 | 13 | 全部时期含 2024 年历史回信 4 家,9/9 才由 LLM 落地 |
| 已开会 | 1 | 1 | 回信者里 lead_status = meeting_held 的(Carolina PayPoint)。与系统 B 卡片的 5 不是一个口径,关系见下一行 |
| 已开会(日历实证,含未回信) | 4 | 5 | = 本表「已开会」1 + 从未回过邮件 3(Business Payment Systems / Competitive Technology / Diversified Payments,自助预约或电话约的)+ 回了信但状态挂着别的 1(Hightech Payments:LLM 判已拒绝 vs 日历判已开会,9/1 起挂真冲突,按规则原状态不动,因此它在本表计入「已拒绝」)。本轮按会议日期 appointment_at ≥ 7/1 |
| 退订(分类) | 2 | 2 | A ∪ B 的退订标志共 3 人(上方卡片),其中 2 人是真人回信里说的 |
| 其他 / 未分类 | 1 | 8 | 含 Alt POS 一条真冲突:文件夹判已约会 vs LLM 判有意向,按规则挂起不动 |
| 仅 A 侧回信,B 无正文可分类 | 0 | 0 | A 的 3 个「只有 tracker 知道」都被 B 判成自动回复,没有剩下的 |
| 合计(真人回复) | 32 | 54 | = 上方「已回复(A ∪ B,真人)」。本轮 = B 侧 last_reply_at ≥ 2026-07-01。lead_status 由 apply_lead_status() 唯一写入(手工 8 / 日历 4 / LLM 14 / 其余为文件夹或历史写入)。每行「人数 = 公司数」 |
--live 16 个锚里的 8 个在这组卡上,判据读源码,折叠不影响meeting_completed),不管有没有回过邮件;= 分类表「已开会」1 + 从未回信 3 + 回了信但状态挂着别的 1管道吞吐 = 最窄那一段。粗管接细管,中间那截还没焊上。行内时刻 = 该行最后一次实查;--live 不管这张表,靠复算路径逐条重跑。
| 段 | 状态 | 实测(带时刻) | 复算路径 |
|---|---|---|---|
| 供给段 | 恢复 · 不够格 | 9/3 起恢复(8/9–9/2 曾断供)。近 30 天真线索 43 条(cra_ncr 26 + tavily 17),全部落在 9/3–9/9 的 6 个自然日。逐步计数:有官网 43/43 → 有摘要 41/43 → 入库时定类:桥 RPC 认的 4 个类目(payment_processor / merchant_services / pos_dealer / restaurant_pos_iso)只有 4/43(non_icp 24 / non_pos 12 / restaurant_tech · unknown · non_iso 各 1)→ lead_contacts 有 KP 15/43、有邮箱 6/43(contact_email 是旧列,不再作口径)→ 全部 qualification_tier=normal 而桥视图只收 high ⇒ 进队列 0/43。同期 62 行 qa_perf 测试已于 23:22 删除(migration 086)(9/9 23:05 HKT 实查) |
max(created_at) 按 source:cra_ncr 2026-09-08 12:00:09Z、tavily_discovery 2026-09-09 06:01:11Z;窗口 created_at >= current_date − 30;逐步计数 = company_leads(窗口内 ∧ source ≠ qa_perf)左联 lead_contacts 按 website / website_summary / icp_category / email 各计;桥条件 = 视图 v_promotable_contacts 的定义(migration 20260901_150000) |
| 加工段 | 每轮超时 | kp_finder 9/3、9/5、9/7、9/8、9/9 五轮全部 ⏰ 超时 (1800s):9/9 那轮 30 分钟只碰了 2 个站,第 1 个撞 ERR_SOCKS_CONNECTION_FAILED(出口隧道 socks5://127.0.0.1:7897 当时断了;23:19 复测已通),且 task_runner 把整段输出攒到任务结束才落盘 ⇒ 每站耗时看不见;kp_found 事件近 7 天 5 个;website_backfill 近 7 天 801 个事件曾全落在 62 行 qa_perf 上,23:22 已随行删除(级联事件 868);icp_v2_reclass:--only-unknown 池里只剩 7 家判完仍是 unknown 的公司,8 个 loop 每天各判一遍、每遍写一条 disqualified 事件 —— 近 7 天 476 个事件落在 8 家上,「1,767」是事件数不是家数;新供给 43 家入库时已定类、不进这个池。9/9 深夜已改成判完不变不写事件、同轮不重判(明天 14:00 那轮验证)(9/9 23:05 HKT 实查) |
v_cron_last_success(kp_finder events_7d = 5);lead_stage_events 近 7 天按 stage_name, source 分组;daily_leadgen.out.log 按日 grep「kp_finder 超时」;反复判 = lead_stage_events 近 7 天 stage=disqualified 按公司分组数事件;隧道 = curl --socks5-hostname 127.0.0.1:7897 api.ipify.org |
| 发送段 | 9/9 停发 · 未登录 | 唯一在真产出的段,9/9 停了。逐日实发 9/2=123 · 9/3=141 · 9/4=62 · 9/7=137 · 9/8=66 · 9/9=0(T1/T2/T3 合计;周末不跑)。T1 余粮 175 ÷ 30 = 5.8 个工作日(tracker 2026-09-09 17:36 HKT,队列没动;9/4 22:24 为 205)。🔴 9/8 20:00 那轮发到第 54 封时会话到期崩溃(T1 0/30);9/9 20:00:49 开跑即「Gmail NOT logged in」中止:0 封、无日报、Telegram 告警已送达。根因:Gmail 网页会话每 14 天到期 —— run.log 三次「未登录」都落在登录后整 14 天(7/21→8/4、8/11→8/25、8/25→9/8 22:04)⇒ 需 Graham run.py --login 重新登录 + 把 Workspace 会话时长改长,否则每 14 天断一次 |
发送器 run.log 按日计 sent 行;余粮 = tracker status='queued',逐条验 175/175 三个 tN.sent_at 全 NULL;崩溃 = cron.out 末尾异常栈;分母 = T1 日上限 30;14 天 = run.log grep「LOGGED IN / not logged in」相邻两条的间隔 |
| 定时任务 | 在跑 | daily_leadgen 14:00 自 9/3 起每天真跑(governor 未再挡;9/3 那轮 2 小时,DST 双候选仍未验证)。pipeline-stale-alert 每天 09:30 在跑,9/9 09:30 报 1 个超期(icp_v2_reclass),当天 14:43 判出第 1 家后 23:05 --dry-run 实跑已「所有带阈值的步骤都在产出」⇒ 明早 09:30 退出码应为 0(可证伪)。LaunchAgents 49 个 plist / 指向不存在文件 1(scan-replies)/ 非 Apple 任务非零退出 7。runtime_governor 磁盘硬闸:空闲 38.6G < 40G ⇒ LinkedIn 自动发送每 15 分钟被挡(9/9 23:19 HKT 实查) |
daily_leadgen.out.log 的 started 行;pipeline_stale_alert.py 09:30 输出;launchctl list + plutil -convert json(plistlib 会静默漏 6 个);governor = runtime_governor.py check 的 PAUSE 行 |
| 项 | 状态 | 实测(时刻) | 判据 · 复算路径 |
|---|---|---|---|
回信标记链(reply_detected → 分类 → lead_status) |
✅ 9/9 11:3x 修 | 曾自 8/26 起冻结两周:8/27–9/8 入库 15 封回信 0 个被标、0 个进分类。修法:扫描器每次跑完做库内集合级对账写标记(自动回复按邮件头 + 主题正则排除;last_reply_at 只增不减),回填 23 家、撤 2 家只有自动回复的错标、修 8 行未来日期。第二层同天暴露:apply_lead_status() 把默认值 other 当强结论,LLM 分类自 9/2 起 0 条落地 —— migration 20260909_113000 后 15 条落地 14、1 条真冲突挂起(Alt POS)。修后真人回信 54(7/1 起 32) |
每次部署实查:真人回信入库未标 0,未来年份的 last_reply_at 0(--live 两条链路判据);backfill_reply_flags.py dry-run 待办 0、reapply_llm_status.py 候选 0(9/9 22:58 HKT 均 0) |
Hermes 每日回信扫描 mail_db_scanner(launchd 09:00) |
✅ 9/9 17:36 修 | 曾 8/27 起连崩 14 天(每天同一句:读本机邮件索引时权限被拒)。根因不是「没授权」,是授给了 launchd 不认的对象:它把磁盘访问授权记在任务的第一个可执行文件上,原任务 bash 包一层再调 python,授给 python 落不到它头上。修法:任务入口改成 python 直跑(daily_scan_and_report.py),发送器 run.py 一字未动。17:36 kickstart 实跑:真人回信 11 / 自动回复 4,tracker replied 28 → 33,退出码 1 → 0。授权对象、责任人与复审安排记在仓内 md |
launchctl list 该任务退出码(9/9 23:00 实查 = 0);扫描日志末行「✓ 完成」。「明确拒绝的两家不再收 T3」:9/9 20:00 那轮未登录即中止、没走到 compute_queue,改为离线复算 —— tracker 里 PayPros / US Transactions / MDM 三家 status=replied ∧ replied=true,run.py:1410 对该状态无条件 continue;全表「replied 标记与 status 不一致」= 0 人 ⇒ 结构上不可能再收 T3(9/9 23:00 HKT) |
| 数据库函数权限缺陷(外审发现) | ✅ 9/3 修 | 未认证请求曾可读到线索数据;2026-09-03 修复并用重放验证关闭(原路径现返回 permission denied)。范围逐个核实后收窄:真正无守卫的 8 个签名撤 7,第 8 个被 13 条 RLS 策略引用故保留。本次是外审偶然发现的,不是判据抓到的 |
复现细节不在本页:仓内 RLS_AUDIT_PROBE_ROOTCAUSE_20260902.md + migration 20260903_130000;建议例行检查「SECURITY DEFINER 函数不得对 PUBLIC/anon 开放」 |
| Hermes 发送器(9/8 崩、9/9 停发) | 🔴 根因已定 · 等登录 | 9/8 发到第 54 封(T3 28 + T2 26)时 Gmail 会话到期、写信框打不开 → 进程退出,30 封 T1 一封没发、无日报;9/9 20:00:49 开跑即「NOT logged in」中止,0 封、无日报、告警已送达。根因 = Gmail 网页会话每 14 天到期(run.log 三个周期:7/21→8/4、8/11→8/25、8/25→9/8)。修法在 Graham 手里:① run.py --login 重新登录;② Workspace 管理台把会话时长从 14 天改长(否则 9/23 前后再断);③ 建议 run.py 把「写信框超时」先按「是否仍登录」判定、照常出日报(本轮未改 run.py) |
run.log grep「logged in」相邻两条间隔 = 14 天;cron.out 末尾;日报行 9/8、9/9 均无。停发一天 = T1 少发 30,余粮按工作日顺延 |
com.grahamcrm.scan-replies |
🔴 已坏 13.2 天 · 建议下线 | 最后成功 2026-08-27 19:22:39,9/9 仍每 30 分钟报错(err.log 631 行同一句)。脚本从未进 main:fd250a9 提交到 feature 分支,切回 main 即消失。9/9 实查它的职责(扫 Mail 库 → LLM 意图 → 写 outreach_sequences)已被 30 分钟 cron 扫描 + 02:00 分类 + 库内对账整条替代 ⇒ 复活会双写,应直接下线(改 launchd 要 Graham 点头) |
launchctl list 退出码 + err.log;下线 = launchctl bootout gui/$(id -u)/com.grahamcrm.scan-replies + 删 plist |
LinkedIn 回复 worker linkedin_reply_send_worker |
🟡 被磁盘闸挡 | 私信跟进那一路 9/2 起恢复真发(9/2 8/20、9/4 9/20、9/8 3/20,mode=REAL,最近 2026-09-08 22:33);回复 worker 累计 20 条(最后 8/16)。9/9 晚实查当前卡点不是 no_editor(那是 09:52 前的历史失败):2 条 approved 待发,每 15 分钟被 runtime_governor 磁盘硬闸挡回(空闲 38.6G < 40G),21:39 另有一次出口守卫未过 |
daily_linkedin.out.log 的 sent=N/20 (mode=REAL);worker 日志 完成: sent=N |
apollo_email_backfill |
🟡 假绿 | 87 次 fail-closed(refusing to spend)100% 被吞成 ✅(同日志带 ✅ 的行 286,9/9 计) |
判据落在退出码而非产出 —— 日志里数 refusing to spend;「拒绝花钱」与「真干完活」在日志上不可区分 |
| 冷发域 | ⏸ 停机第 20 天 | COLDSEND_DAILY_TOTAL=0(2026-08-20 起)。若启用会错发 173 人(已触达→重发 142 + 待发→双发 31;9/9),正常新线索 138 |
run.sh 注释:域龄 60–90 天后(2026-09 中下旬)可重测;风险数的窗口口径见 §13.6 |
| # | 事项 | 现状 → 下一动作 | 详见 |
|---|---|---|---|
| 🔴 重要 | |||
| 1 | I-0 唯一队列两步等批 | dry-run 闭合,候选 25 人(9/9 recheck delta = 0)→ A 验证(≤25 credit,不写任何队列,可先批);B 写 tracker 等 v3.3 外审处置完再 --apply(v3.3 + v3.3b 9/9 深夜已做完并推送 5859da0:账本 / TOCTOU / 审批绑定 / 回滚守卫与回读 / 批次名绑定 / 状态机锁序;--diff 外审 2/3 出票、两家同点的严重项已处置,单测 246 绿;push 钩子复审中) | §03 I-0 / U-1 |
| 2 | openclaw v3.3 | 9/4 推送外审三家「有条件合并」的严重项(read_batch_ledger 漏批次、run_suppression_add TOCTOU、回滚绑定、_matches 误删等)→ 改完补单测、重跑外审再推 | 仓内 plan |
| 3 | 加工段入口定位 | ✅ 9/9 已计数(块 ② 供给段):官网 43 → 摘要 41 → 桥认的类目 4 → 有 KP 15 → 有邮箱 6 → 进队列 0;阻塞点两处 = 入库定类 + 桥视图只收 tier=high;且桥 RPC 没人调度(9/1 手动跑过 30 行后再没跑),视图里现成够格 92 人 / 75 家闲置 → 下一动作:定「桥要不要排程」(决定 ①) | §03 I-1 |
| 4 | icp_v2_reclass 反复判 | ✅ 9/9 已分清:既不是输入为空也不是阈值 —— 池里只剩 7 家判完仍 unknown 的公司被 8 个 loop 每天反复判(1,767 = 事件数);新供给不经此步。已改成判完不变不写事件 / 同轮不重判 → 明天 14:00 那轮 disqualified 事件应 ≤ 7 | §05 难点 05 |
| 5 | kp_finder 超时 + 删 62 行测试数据 | ✅ 62 行已删(migration 086,备份在仓内 .handoff/,085 约束已 VALIDATE);超时根因 = 出口隧道故障 + 输出攒到结束才落盘 → 下一动作:kp_finder 每站计时逐行落盘、隧道断时跳过 chrome 兜底 | §03 U-3 |
| 6 | 反向抽样 1,637 家「肯定类目 ∧ 摘要不足」 | 已进队列 384(按公司名交叉,9/9)→ 抽 100 家测误肯定率 | §03 I-2 |
| 7 | rotate_keys.sh 枚举所有消费方 | 9/1 轮换只更新了一处,另一处 key 死了两天 | §03 I-5 |
| 📈 高产出 | |||
| 8 | Apollo 线 A 全量 --review 量通过率 | 余额 2,021(9/9 实读,8/24 起没动),9/16 15:04Z 归零;有多少合格线索就 reveal 多少 | §03 H-1 / §08 |
| 9 | catch_all 队列 73 人 | ≤5/天、退信 >5% 自动停 —— 等 Graham 定(= 本轮接线产出的 3 倍) | §03 I-0 |
| ⏱ 紧急 | |||
| 10 | 9/9 20:00 发送器那轮 run.log | 三件事:① 再崩没有 ② US Transactions / PayPros 是否被 compute_queue 跳过 ③ 日报行出没出 | 块 ③ 第 2、4 行 |
| 11 | scan-replies 修法 | launchd 不许依赖 git 工作树路径 | 块 ③ 第 5 行 |
| 12 | 3 家 opt-out 清理 | Alt POS / Florida Merchant Services / NRS Pros,均 queued(9/9 按公司名交叉) | §03 U-2 |
| 13 | LinkedIn 回复 worker no_editor | 队列 1 条反复失败 | 块 ③ 第 6 行 |
三个子块:当下运行状态(谁真的在发)→ 目标闭环(该长成什么样)→ 当前断点(差在哪)。
这一轮查出的每件事都是同一个形状:台账说的和实测的不是一回事。所以这张表并置两列。
| 渠道 | 台账怎么说 | 实测 | 状态 |
|---|---|---|---|
run.pyHermes ISO 邮件 |
jobs.json 工作日 20:00 |
最早发送 2026-07-01,累计 1,831 次投递(9/9 tracker 逐条) | 在跑 |
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/9 实测队列 59 人,其中 46 人已到期(runner_node 全部 = hermes-graham,与 .env:33 一致),current_step 分布 1→4。日志实证 9/2 sent=8/20、9/4 9/20、9/8 3/20(mode=REAL) |
在跑 · 46 已到期 |
linkedin_reply_send_worker审批过的回复自动发 |
代码默认 false |
脚本 :91 硬编码 true,每 15 分钟一轮;累计已发 20 条(完成: sent=N 行求和,最后一次 8/16),队列中 1 条且 9/9 09:52 发送失败(no_editor,回 approved 重试) |
在跑 · 发不出 |
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、日志末行),不是看配置里的开关。
| 口径 | 值 | 说明 |
|---|---|---|
| DB「可发」四层收紧 | 1,017 → 900 → 895 → 671 | ①有邮箱∧outreach_status='queued' ②排已发 t1 ③排 unsub/bounce/reply ④排 status∈(disqualified,dead,paused) |
| DB queued 三桶闭合(2026-09-09 10:35 HKT) | 1,017 = 175 + 417 + 425 | tracker 也 queued 175 · DB 标 queued 但 tracker 已发/已回 417(9/4 为 357:Hermes 这 5 天又发了 60 个 DB 仍标 queued 的人)· tracker 没有 425。425 里 status='new' 138 = 旧默认值 108 + 桥推 30;经正向放行 ∧ 无已知坏结果 ∧ 名字可用 后能导入发送器的只有 25 人(9/9 10:32 对 9/4 批次 --recheck:delta = 0,全集 311 = 133 + 148 + 5 + 25) |
| 真·上膛 | 175 | (tracker 2026-09-08 22:04 HKT 快照;9/4 22:24 为 205、9/2 22:33 为 295)tracker 里 queued 且三个 tN_sent_at 全 NULL,本版逐条验 175/175 同形(9/2 那次验的是 295/295) |
| DB 比 tracker 多 | 425 | 这些人没有任何路径会被发出 |
| DB 陈旧 | 417 | (2026-09-09 10:35 HKT;9/4 为 357、9/2 为 297)DB 标 queued,tracker 里其实已发过 ⇒ 只能回填,不能相加 |
| t1/t2/t3 已发 | tracker 691 / 609 / 531;DB 276 / 211 / 187(9/2 起一行没动) | 发送器只读 tracker ⇒ tracker 是发送真相源,DB 仅为其 40% / 35% / 35% |
| 发送总量三口径 | run.log 2,041(含自测)· tracker meta 1,834 · tracker 逐条 1,831(纯外发) | 三个都复算成立(9/9),run.log 比逐条多 210;引用时必须点名口径 |
左侧三段分区(9/2 新增):供给段(来源 → raw_leads)近 30 天产出 43 条真线索(9/3 起恢复,0 个带邮箱),Graham 已定换主线为 Apollo + LinkedIn Sales Navigator;加工段(规则闸 → 邮箱验证)是本轮唯一还有价值的一段,下一步是把新供给接到它的入口上;待发队列是边界(「到待发队列前」指的就是它以上);发送段在本轮范围之外。
一条主路径 + 一条存量回流边,纵向读。分配同事是终点——交给销售后线索就离开外联系统。让它成环的是 company_leads 存量回流到闸前那条边(5,710 家可回流,见 §01 枚举明细),不是从销售末端往回流。当前之所以看着像两条,是因为 Apollo 的 --ingest 从最后一格直插待发队列,而回流边根本不存在。颜色是每个环节的真实状态,不是计划状态。9/2 重画——旧图有三处与实查矛盾:把 Reveal 标成「手动」(实际没有代码)、把邮箱验证标成已打通(实际未调度)、把 LLM 判 ISO 标成未接线(实际 icp_v2_reclass 每天在跑,没接的是 Apollo 那条 --review)。
与 SEND_PATHS_ENUM 同一套纪律:入口没枚举全,闸就会被整条地绕过,而且是静默的。上图九个格是这张表的摘要。下表实查自生产库,「绕过 raw_leads」列精确闭合到 953(= from_raw_lead_id IS NULL 的行数,9/9 23:25;17:16 为 1,015,减的 62 = 已删的 qa_perf 测试行)。
| 来源桶 | 源数 | 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 | 578 | 453 | 0 | 全经 raw |
| POS 厂商合作方 *_partner / *_reseller | 12 | 45 | 45 | 0 | 全经 raw |
| 自动抓取 · Tavily tavily_discovery | 1 | 0 | 261 | 248 | 🔴 每天在跑却几乎全部直写 company_leads(仅 13 条经 raw) |
| 🔴 测试 / 审计垃圾 rls_audit 0(601 行 9/2 已删)/ qa_perf 0(62 行 9/9 23:22 已删) | 0 | 0 | 0 | 0 | 已清 migration 086 删 62 行(级联事件 868,审计入 qa_cleanup_audit),085 约束已 VALIDATE ⇒ 以后写入即拒;回流时仍显式排除(单元测试锁住) |
| Apollo source='apollo' | 0 | 0 | 9 | 9 | 🔴 company_leads 里只有 9 条;真正的 418 条走 --ingest 连 company_leads 都没进 |
| (source 为 null) | 0 | 0 | 0 | 0 | 已清 9/2 时有 9 条来源未知,9/9 实查 0 |
合计绕过 raw_leads | 953 | 精确等于 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' 的 546 行中 418 是 Apollo,真 LinkedIn 只有 128(0 有邮箱,9/9)。任何按 lead_source 做的枚举/闸/统计都会漏掉它。
② cold call 存量 1,203 家——它们在 company_leads 里(已计入上表各桶),但 caller_stage 携带的人工信号(尤其 ask_to_send_email 61 家)目前没有任何自动通路回到闸前。
枚举漏一个,闸就漏一条渠道。LinkedIn engine.js 就是这么漏了 50 天。
icp_v2_reclass 判出 1 家,0.1 天前)9/9 17:16 实查 · 部分恢复结论:本机流水线 9/3–9/9 连续 7 天真跑,真实产出(近 7 天)= promoted 26 / cra_ncr 26 / tavily 17 / phone_validated 95 / kp_found 5 / icp_passed 1;website_backfill 的 801 个事件全部打在 62 行 qa_perf 测试数据上,真实线索的网站补全产出为 0,不计入恢复(「有事件」≠「有真实产出」)。icp_v2_reclass 7/30–9/8 41 天零 icp_passed,9/9 06:43Z 才判出第 1 家 —— 它每天在跑、每天判否(近 30 天 1,767 个 disqualified 事件 vs 判过 1),问题从「不产出」变成「通过率」。近 14 天整天没跑的 4 天(8/26 8/28 9/1 9/2),全被 governor 挡下。
建议:判据落在产出上,不是「进程起没起」。已建 v_cron_last_success(不限窗口取 MAX)+ 独立 launchd 告警。
| 步骤 | 上次真正产出 | 距今 |
|---|---|---|
icp_v2_reclass(信号 = icp_passed,41 天来第 1 个) | 2026-09-09 06:43Z | 0.1 天 ✅ |
phone_backfill / kp_finder(kp_found 5)/ tavily | 2026-09-09 06–08Z | 0.1 天 ✅ |
website_backfill | 2026-09-09 06:12Z | 0.1 天 🔴 近 7 天 801 个事件全在 62 行 qa_perf 测试数据上,真实线索的网站补全产出 = 0,不算恢复 |
promote_raw(放行 cra_ncr 26) | 2026-09-08 12:00Z | 0.9 天 ✅ |
cra_ncr / leadgen_summary | 2026-09-08 06:58Z | 1.1 天 ✅ |
kp_supplement(US SOS,不在本机) | 2026-09-01 | 8.0 天 |
email_enrich | 2026-06-25 | 76 天 |
gmaps_scrape(2026-05-25 停用) | 2026-05-22 | 110 天 |
customer_match / pos_reseller_verify | 2026-04-16~20 | 142+ 天 |
原因:「跑了」≠「产出了」 —— 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 丢掉,连是哪一项资源都不知道。
⚠️ 9/9 10:40 实测(df -g / sysctl vm.swapusage / vm_stat free+inactive):disk 剩 43 G / swap 已用 369 M / mem 30% ⇒ 卡的是白天那两个时刻的内存,而一天只有两次机会。已改成 03/04/05 三次候选,9/3 起每天都有一次过闸。
结论:9/2 时 v_cron_health 有 3 个信号指错了表、2 个没有发射方;migration 084 + 补埋点后,v_cron_last_success 里 tavily_discovery(近 7 天 17 事件)、kp_supplement(9/1)、kp_found(近 7 天 5 事件)、phone_validated(近 7 天 95 事件)都已变绿;icp_v2_reclass 上午还红(最后一个 icp_passed 7/30),14:43 HKT 那轮判出 1 家后也变绿了。
建议:5 项按「下次成功跑完应变绿」的判据全部兑现。icp_v2_reclass 的问题不是通过率:近 30 天 1,767 个 disqualified 事件绝大多数是同 7 家 unknown 公司被每天 8 遍反复判(9/9 深夜已改成判完不变不写、同轮不重判,见 §01 块 ②);新供给不经此步,加工段主线是入库定类的质量(§05 难点 05 的 40% 误判)与桥 RPC 无人调度(视图里现成 92 人)。
| 步骤 | 视图查的 | 实际写的 |
|---|---|---|
tavily_discovery | raw_leads → 0 | company_leads 256(9/9) |
kp_supplement | raw_leads source LIKE 'sos%' → 0 | lead_contacts 331 |
icp_v2_reclass | meta.reclass_source → 0 | stage_name='icp_passed' 3,137(探针级联删除后;7/30 之后的第 1 个是 9/9 06:43Z) |
kp_found | ✅ 已有发射方:9/2 补埋点,9/8 06:14Z 写出 3 个事件(9/2 前只有一次性回填脚本在 2026-04-24 写过一次) | |
phone_validated | ✅ 同上:近 7 天 83 个事件(最近 2026-09-08 07:58Z) | |
原因:一个永远红的监控和一个永远绿的一样不携带信息 —— 这就是它建好之后没人读的原因。唯一的消费方 cron_health_check.py 在本机没有任何调度。
结论:company_leads 近 30 天(≥ 8/10)新增 105 行,其中 62 行曾是测试数据(9/1 写入的 qa_perf,9/9 23:22 已删;rls_audit 探针 601 行已于 9/2 删除,30 天内 0 新增),真线索 43 条。存量测试垃圾现为 0 行;此前 website_backfill 近 7 天 801 个事件全落在那 62 行上,已随行级联删除。
建议:qa_perf 存量已清、085 约束已 VALIDATE(对全表生效)。rls_audit 待 Graham 定。
原因:🔴 不能用约束挡 rls_audit —— 那 108 行来自 RLS 审计的 INSERT 探针(故意插一行验 RLS 拦不拦得住)。挡了它,探针会因「约束」失败,而那个失败长得和「RLS 生效了」一模一样 —— 等于用数据卫生把一个安全控制变成假绿。它的正确修法是修它自己的 cleanup。
⚠️ NOT VALID 期间「约束存在 ⇒ 数据干净」是错的推论:存量 62 行曾在,9/9 23:22 清完后已 VALIDATE(migration 086)。
🚪 闸该落在哪:company_leads 的写入口,不是绿线 promote_raw 上(9/2 实查定的)。
实查(9/9 17:16):promote_raw 近 30 天 promoted 26 条(最后一次 2026-09-08 12:00Z),同期 disqualified 事件 1,767(事件数;绝大多数是同 7 家 unknown 公司被反复判,见 §01 块 ②)——它在跑,放行率仍低;而 company_leads 近 30 天新增 105 家里 79 家(75%)from_raw_lead_id IS NULL(近 90 天 227 里 146)。
⇒ 把闸挂在绿线上只管得住近 30 天入口的 25%,另外 75% 照样从旁边走过去——正是本页反复说的那个形状。
排序:重要 > 高产出 > 紧急。新需求按此插队,不按提出时间。每项按结论 → 建议 → 原因写,全部基于实查。首屏 §01 块 ④ 是本节的索引(2026-09-09 18:45 排序);本节卡片仍按 9/4 分组,未重排。
结论:唯一发送队列 = 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;178 单测;真库 dry-run 闭合,9/9 --recheck delta = 0),剩两步等 Graham:A 验证 25 人(≤25 credit,MillionVerifier 余额 9,921)→ B 按准入清单写 tracker(只在发信 / 回信扫描静默时段内:导入器机器判定 10:00–19:00 HKT,且写前确认发送器、回信扫描、cron 全部静默)。🔴 硬禁令:互斥保护落地前不得自动排程导入;每次导入前须确认发送器 / 回信扫描 / cron 全部静默。另:9/4 23:19 推送外审三家对 v3.2 判「有条件合并」,严重项处置(v3.3)阻塞 B(写 tracker),不阻塞 A(A 只花验证 credit、不写任何队列)—— v3.3 改完并重跑外审之前不做 --apply。同时要定:排程化、catch_all 队列(73 人 = 本轮产出 3 倍)、双队列日落日期。
原因:桥推的 30 人 0/30 进 tracker;DB 侧 425 人无发送路径、417 人 DB 标 queued 而 tracker 已发过(9/9 10:35 HKT)。收益上限很小 —— 候选 25 × 验证通过率 ≈ 16 人 ≈ 半个发送日;价值在「接通」本身:之后每一批放行都有去处。剩余风险(并发写保护依赖人工维护窗)已在内部 plan 登记,不在公开页展开。
结论:供给段 9/3 起恢复(8/9–9/2 那 25 天曾断供),但恢复后新增的 43 家 0 个带邮箱;这 43 家有没有走进加工段各步(网站补全 / KP / 邮箱)本版没有分别计数,只知道 kp_finder 有输入但每轮超时 —— 阻塞点待定位,不能由「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,564 家存量零 CRM 侧去重(_dedup_guard 以 phone=None 调用,控制流必达 return (True,"ok",None))。
结论:其中 384 家已进外联队列(按公司名 lower/trim 与 outreach_sequences 交叉,9/9;9/2 记的 274 未注明口径,不可复算),可能正在给非 ISO 发信。
建议:抽 100 家测误肯定率,排在任务 8 之前。
原因:任务 8 只查否定方向(4,283 家)。1,637 占肯定类目 74%(2,220 家;肯定 = 非 non_* 且非 unknown)。假阴 = 机会成本;假阳 = 实际支出 + 域名声誉。
company_leads 写入口结论:已挡 qa_perf(CHECK ... NOT VALID);rls_audit 待 Graham 定。
建议:先定 rls_audit 怎么办,存量 62 行已于 9/9 清理并 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、9/9 --recheck delta = 0: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 的只有 25(1.6%)(9/9;经 lead_contact_id 关联 287 行,其中 title 非空 25)。另两条路已排除:回填存量职能要花 Apollo credit(plan §不做);关掉 B3 在 v7 已封死。
rls_audit 自己的 cleanup结论:存量测试垃圾 0 行(rls_audit 601 行 9/2 删、qa_perf 62 行 9/9 23:22 删,migration 086);此前 website_backfill 近 7 天 801 个事件全落在那 62 行上,白白消耗抓取配额 —— 已随行级联删除。
建议:修它的 cleanup,不要在写入口挡它(见 I-3)。清干净后 VALIDATE CONSTRAINT。
原因:脚本有 cleanup 代码但显然没清干净;它不在任何调度里 —— 是人工跑审计时留下的。
按你最初描述的闭环逐项对照,这些是缺口。
系统按关键词自动搜、LLM 优化搜索条件。现在全是人工在 Apollo 后台筛完导 CSV。
在消耗 credit 取联系方式之前先判是否 ISO 目标客户。这是你定的硬约束,目前没有自动执行的通路。
已更正我此前写「上游没接进来」是错的——我查到没有叫 leads 的表,就当成没有这件事。实际上游用的就是同一个库:raw_leads 10,186 → company_leads 10,564 → lead_contacts 3,507(9/9),每天 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 且原状态不动。
🔴 9/9 实查:LLM 这一路从 9/2 起一条都没落地 —— 函数里的弱值表写的是 no_info / unknown / auto_reply(前两个不在枚举里),全表默认值 other 被当成强结论,LLM 提议全部记成「真冲突」,lead_status_source='llm' 全库 0 行;六个用例都过是因为用例里没有「默认值 other 对强提议」这一种。migration 20260909_113000 把 other 归弱值后重喂:14 条落地、1 条真冲突(Alt POS)。「实测六个用例符合设计」证明的是用例,不是生产。
本轮反复栽在这上面:退出码 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,564 家公司(9/9),56.0% 的 website_summary 不足 80 字;被判 non_icp 的 4,084 家里 83% 摘要不足,unknown 的 453 家里 99% 不足(9/2 的 942 家含 601 行已删探针)。
→ 人工抽样 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 ...——缺这一行就是判据落在了被测对象之外。另立一条表级基线不变量(回填前后该数必须相等)防止顺手污染 —— 但基线要在动手当刻取:9/9 实查已是 59(9/2 为 61),这条队列在跑,写死 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 的,这种应该是最准确的吧」。方向成立,但「有内容」的定义必须改,否则会产生系统性漏判。
9/9 补查 9/1→9/9:主日历新增 4 个事件(MagicPay 9/1、Salsita 9/2、GlobalOne 9/3、Invoteq 9/3),2 个带 Gemini 纪要,对方 accepted 2 / needsAction 2;4 个外部参会邮箱 0 个在 outreach_sequences。上方六格仍是 05-04→09-01 窗口的 9/2 实查值,窗口固定、不随日期漂。
实读两份纪要正文,两份的 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,9/9 09:16Z 计还剩 7.2 天);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,且终结态记在两个字段里 |
9/9 实查:grahamcrm main 与 origin/main 同步(领先 0);openclaw feat/tier3-ingestion-hardening 远端已在 f5bede2,与本地 HEAD 一致(本地 origin/* 跟踪引用没 fetch,rev-list 会误报领先 81 —— 量远端要用 ls-remote)。9/2 时两仓分别领先 53 / 75 个未推,9/4 已推。生产数据本轮动了 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,710 家(9/9 现值)——8 天落不了地,落地了也不产生一个 reveal。
→ 劈成两条互不阻塞的线:线 A(抢额度,9/16 硬死线)= 每天一次 MCP bulk_match → 落盘 → --ingest,已验证跑通、不需要任何新架构;线 B(管道收敛)无硬死线。线 A 是排期问题,不是工程问题。
raw_leads 移到 company_leads草案第 1 条论证的是层(DB vs 应用),架构图画的是位置(raw_leads 之后)——草案把这两件事当成了一件。
| 闸装在哪 | 入口覆盖率 | 代价 |
|---|---|---|
raw_leads 上 | 90%(漏 1,015 行 + 4 个源,9/9) | 低 |
✅ company_leads 上 | 100%(所有入口的汇聚点) | 需处理 10,564 行存量 |
outreach_sequences 上 | 100% 但太晚 | reveal 已经花过钱了,违反 Graham 的硬约束 |
→ 架构图里 raw_leads 是入口之一,不是统一入口。Graham 说的「统一入口」在语义上是对的(目标态),但把现状画成目标态,下一个人会以为已经收敛了。
草案拿 uq_email_threads_natural_key「当场炸出 19 个重复」证明「应用层去重会静默失败」。但那个先例同时证明:在有存量的表上加约束,约束会立刻对存量生效并失败。
→ ALTER TABLE ... ADD CONSTRAINT ... NOT VALID(毫秒级、不锁表、对新写入立即生效)→ 存量清干净后再 VALIDATE CONSTRAINT。先止血,再清创。⚠️ 但 NOT VALID 期间「约束存在 ⇒ 数据干净」是错的推论——这条必须写进注释,否则它就是下一个假绿。
判定器两个方向都已实证会错(9/9 复算):否定类目 ∧ 摘要不足 4,283 家;肯定类目 ∧ 摘要不足 1,637 家(占肯定类目 74%),其中 384 家已进外联队列(按公司名交叉)。
| 阶段 | 对象 | 家数 | 前置条件 |
|---|---|---|---|
| R0 | ask_to_send_email ∧ 被判不合格 | 11 | 无 每条自带一条独立的人类正向信号 |
| R1 | 可回流 ∧ ask_to_send_email | 50 | R0 的结论(9/2 误写成「∧ 有邮箱」;实为 61 − 11) |
| R2 | 可回流 ∧ 有邮箱(contact_email 或任一 lead_contacts 邮箱) | 537 | 判定器误报率已量化 |
| R3 | 全部可回流 | 5,710 | R2 的 ROI 为正 ∧ 误报率 < 阈值 |
→ 排除集必须写死并单元测试锁住加总闭合(五桶 = 10,502,9/9 23:25):已判不合格 4,753 / 测试垃圾 0(RLS_AUDIT% 那 601 行 9/2 已删,级联带走 lead_stage_events 3,024 + lead_contacts 63;__QA_PERF__% 62 行 9/9 由 migration 086 删除并 VALIDATE 085 约束)/ 已在销售流程 27 / opt-out 12(按 caller_stage 原始计 33 / 16,其中 10 家同时已判不合格,按「不合格优先」归桶后才闭合)/ 可回流 5,710。与 §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 冲突(9/9 按公司名与 caller_stage='cold_call_opt_out' 交叉:Alt POS / Florida Merchant Services / NRS Pros,均 queued;9/2 记的 Focus Merchants、Cervion 已不在交集内),load_company_occupancy() 撞到即 abort。
看板规矩 2:头部是系统简介不是 changelog。9/9 一天盖了八次章,§01 长成了流水账;19:19 HKT 起首屏只留四块(A ∪ B 合并口径 / 流水线健康 / 已坏·已修 / 下一步),下面这些按原文搬到这里(唯一改动:13.4 回信链行里两个 --live 判据的数字去掉了加粗,让判据永远只认 §01 那行;检查器另有「锚全页唯一」判据兜底),数字与时刻均为搬出时的值,不再随部署闸更新;当前值一律以 §01 为准。本段是债务不是资产:下一次精简把它归档到仓内 md 后从页面删除,log 区只留判据纪律。
⚠️ 旧说法一经推翻即登记进 check_dashboard_consistency.py 的 BANNED 由判据拦截;本版新登记 9 条(供给「断供」、LinkedIn「最后真发出 8/16」、kp_finder「真实输入 0」、流水线「上次产出 8/8」、监控「无发射方」、邮箱验证「有码未调度」,以及回信链 3 条:「已回复 = 回复人数」、「三个写入者全部接线」、「23 条漏标」)。
⚠️ 本版有 3 组数字在实时移动,「绿」是看的时刻的函数。盖章时刻 = 2026-09-09 18:40 HKT(当天第八次盖章:10:45 全表重查 → 11:05 回信链披露 → 11:35 回信链修复 → 17:20 外审处置 + 14:00 流水线跑后刷新 → 17:40 Hermes 每日扫描修通 → 18:10 加 A ∪ B 合并口径 → 18:25 回复分类也合并 → 18:40 已开会 5 与 1 的口径关系摆明)。
· 邮件正文入库 —— 回信扫描每 30 分钟增量跑,9/4 22:34 为 2,361,9/9 17:31 已到 2,545(自 9/4 22:34 起 +184;按 created_at >= current_date − 7 计 +422;含 9/8 晚的回信)
· 线索总行 / 待发 —— 本版没动:outreach_sequences 最后一行写于 2026-09-04 14:44Z,之后 5 天无新增(1,571 / 1,297 与上版同值;9/4 之前 LinkedIn 在实时写入)
· 批 3 → 批 2 迁移 + 已触达 —— Hermes 每工作日 20:00 那轮在发,人从「待发」变成「已发」。
9/4 22:34 复算 批 2 74 / 批 3 43 / 已触达 130;9/9 10:16 复算 批 2 86 / 批 3 31 / 已触达 142(已触达 = 批 1 56 + 批 2 86),合计 = 批 1 56 + 批 2 86 + 批 3 31 + 批 4 138 = 311 恒定(窗口内 47 + 86 + 17 + 110 = 260)⇒ 是迁移不是新增。
⇒ 引用这些数必须带时刻;--live 判据的作用不是「让它永远绿」,是让漂移当场可见。
结论:外审发现一处数据库函数权限配置缺陷,未认证请求可读到线索数据。
已于 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 以来第一次真跑;此后 9/4–9/8 每天 started,governor 未再挡(近 14 天整天没跑的只剩 8/26、8/28、9/1、9/2)。 |
| 15:00 那次应跳过 | ⚠️ 不可观测 | 🔴 是判据本身写坏了:14:00 那轮跑了 2 小时,15:00 时它还在跑,第二个候选根本没触发。我当时假设两次独立触发。DST 双候选机制仍未验证,要等冬令时或一次短跑。 |
phone_backfill 埋点 | ✅ 验证生效 | 从「从未」变成 2026-09-03 08:02;9/9 17:16 实查近 7 天 95 个事件,最近 2026-09-09 07:55Z。 |
kp_finder 埋点 | ✅ 9/8 验证生效 | 9/8 06:14Z 首次写出 3 个 kp_found 事件(埋点通了;近 7 天累计 5 个)。但任务本身仍没跑完过:9/3、9/5、9/7、9/8、9/9 五轮全部 1800s 超时后判失败,ERR_BLOCKED_BY_CLIENT 9/3、9/8 各 1 次 —— 找到 KP 和跑完一轮是两件事。 |
| 项 | 实测 | 根因 |
|---|---|---|
✅ 回信标记链(reply_detected → 分类 → lead_status),9/9 已修 | 曾自 8/26 起冻结两周:8/27–9/8 入库的 15 封回信(真人 8 家 9 封 + 自动回复 6 封;按 sender 去重 13,MDM 的自动通知与真人回信同一 sender)0 个被标。9/9 修法:扫描器插入真人 inbound 时同步写标记(自动回复按邮件头 + 主题正则排除,last_reply_at 只增不减);回填 23 家(含 13 家 2023–2026 年初的历史回信)、撤 2 家「只有自动回复」的错标、修 8 行落在 2056–2057 年的 last_reply_at。判据(每次部署实查):真人回信入库未标 0,未来年份的 last_reply_at 0 | 8/29 换上的 mail_scan_emlx.py(cron 每 30 分钟)只插 email_threads,写标记的代码只在被它取代的旧脚本里;下游 intent_classify.py(每天 02:00)只取已标记的行 ⇒ 入库越勤、标记越死。第二层同天暴露:apply_lead_status() 把默认值 other 当强结论,LLM 提议一律记冲突,lead_status_source='llm' 全库 0 行 —— migration 20260909_113000 把 other 归弱值后,15 条分类经同一 RPC 落地 14 条,1 条是真冲突(Alt POS:文件夹判已约会 vs LLM 判有意向,按规则挂起) |
✅ Hermes 每日回信扫描 mail_db_scanner(launchd 09:00),9/9 17:36 修通 | 曾 8/27 起连崩 14 天(每天同一句:读本机邮件索引时权限被拒)。9/9 17:36 在 launchd 下 kickstart 实跑成功:真人回信 11 / 自动回复 4 入账,tracker replied 28 → 33,任务退出码 1 → 0 | 根因不是「没授权」而是授给了 TCC 不认的对象:launchd 把完全磁盘访问记在任务的第一个可执行文件(责任进程)上,子进程沿用它。原任务是 /bin/bash 包一层再调 python3,所以给 python 授权没用。三个临时任务实测:/usr/bin/python3 直跑 ✅、经 bash 一层 ❌、命令行工具框架内的 python3.9 直跑 ❌。修法:任务入口改成 /usr/bin/python3 daily_scan_and_report.py(原 .sh 逻辑逐条搬进 .py,两步作为子进程跑),发送器 run.py 一字未动。发送器开跑前的 IMAP 扫描是另一条标记路径(tracker 的 replied 与看板的 reply_detected 是两套账)。tracker 这次多标的 5 家里含 9/8 晚回的 3 家(MDM Payments / US Transactions / PayPros);发送器的取件逻辑跳过 replied,但「两家明确拒绝的不会再收 T3」要等 9/9 20:00 那轮跑完看日志才算验证过,在此之前只是「已标」 |
com.grahamcrm.scan-replies | 已坏 12.9 天(最后成功 2026-08-27 19:22:39;9/9 仍在报错),err.log 606 行去重后仅 1 行 | 脚本从未进 main:fd250a9(08-27 19:35) 把它提交到 feat/apk-kds-batch-send,切回 main 即消失 —— 13 分钟因果链。修法是「launchd 不许依赖 git 工作树路径」 |
| LinkedIn 发送 | 9/2 起恢复:linkedin_outreach 私信跟进 9/2 8/20、9/4 9/20、9/8 3/20(mode=REAL);8/17→8/31 那段 sent=0 已过去,最近一次真发出 2026-09-08 22:33 | 浏览器侧恢复后即有产出;回复 worker 那一路仍 no_editor(9/9 09:52 sent=0/1,单条回 approved 重试) |
apollo_email_backfill | 87 次 fail-closed(refusing to spend),100% 被吞成 ✅(同日志 apollo_email_backfill 带 ✅ 的行 286,9/9 计) | 判据落在退出码而非产出 —— 「拒绝花钱」与「真干完活」在日志上不可区分 |
| 冷发域 | COLDSEND_DAILY_TOTAL=0,停机第 20 天(2026-08-20 起) | 恢复条件:域龄 60–90 天,run.sh 注释自陈 2026-09 中下旬可重测 |
| LaunchAgents 体检 | 49 个 plist(9/2 为 53),指向不存在文件的 = 1(仅 scan-replies);launchctl 非 Apple 任务非零退出 7 条(17:36 实查:scan-replies 2、pipeline-stale-alert 2、coldsend-verdict 2、apk-watch 2、chinatextbook-sync 2、brief-crosscheck 1、hermes.support −15;iso.daily.scan 已从 1 归 0) | ⚠️ 体检必须用 plutil -convert json,plistlib 会静默漏 6 个 |
pipeline-stale-alert | ✅ 每天 09:30 在跑;9/9 09:30 只剩 1 个超期步骤:icp_v2_reclass 41 天(阈值 2 天);当天 14:43 HKT 它写出 41 天来第 1 个 icp_passed,明早 09:30 应变绿(可证伪)。9/3 首跑时报的是 5 个 | 首跑报的 website_backfill 118 天 正是清掉 601 行探针之后的真值;9/3 流水线恢复后其余 4 个已陆续变绿 |
约会率此前是 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 才是。用一个可被改掉的默认值算出的风险数,方向对不等于数字对。