> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# Lambda 并发决策

> **2026-07-14 迁移说明**：本文从 `docs/operations/runbooks/` 移到 `docs/system-design/`。它记录的是**为什么**这些并发值这样选（design decision record），不是 incident 时**该做什么**（runbook）——放 system-design 才对。旧路径 `docs/operations/runbooks/lambda-concurrency-decisions.md` 留了指针 stub。
>
> ⚠️ **live 值以代码为准（code is SoT）**：本文正文是 2026-04-30 的决策快照，正文里的具体数字（如"扩到 5"）**已经又变过**。当前生效值看 `lib/stacks/*.ts`——例如 contacts-analyzer 现在是 **reservedConcurrency=6 + SQS maxConcurrency=3**（2026-07-14 对照 `lib/stacks/contacts-stack.ts` 核实），比本文正文写的 5+5 又调过一轮。读正文取"决策方法论 + frame correction trail"，取当前数字请回代码。

> **状态**: 决策快照 + 调查方法论(post PR #639 ContactsAnalyzer cron enable verification 调查产出)
> **日期**: 2026-04-30
> **关联 Issue**: [callytics-infrastructure #691](https://github.com/retaintive/callytics-infrastructure/issues/691)
> **关联 PR**: [#639](https://github.com/retaintive/callytics-infrastructure/pull/639) (cron enable, merged 2026-04-29)
> **范围**: ContactsAnalyzer / TranscribeProcessor / AiAnalysisProcessor 三个 Lambda 的 `reservedConcurrency` 设计决策、历史演变、实测容量、扩容 baseline
> **读者**: 半年后回来想知道"为什么 contacts-analyzer 是 2"的人 / 想做类似容量调查的工程师 / 接手扩 5→10 决策的人

***

## What-We-Did — 这次调查的真实步骤

PR #639 把 ContactsAnalyzer 的 EventBridge daily cron 从 `DISABLED` 翻成 `ENABLED`,2026-04-29 01:28 UTC merge,之后 32 小时内 cron 跑了两次(04-29 + 04-30 各一次 06:00 UTC)。最初的 verification 看完 CloudWatch 数据后写进 issue #691,标了两个 hidden issue,其中 hidden issue #1 是 _"`reservedConcurrency=2` 是 cron drain 时间瓶颈,prod rollout 前应评估扩容"_。这份 doc 记录的是接下来对 hidden issue #1 的深挖。

调查 entry point 是一个 frame 问题:**"调高 `reservedConcurrency` 会不会破坏其他服务?"** 用户(Max)直觉里这个数字跟 RingCentral / OneRouter quota 有关,改它牵一发动全身。第一轮我(AI)用 `callytics-impact-check` skill 给了一份 impact report,标 OneRouter quota 为 🔴 breaking risk,说必须先做"方案 D"(给 ai-analysis-processor 补 reservedConcurrency 关历史 hole)。我同时 cross-validate 给 Codex + Gemini 看,两家都同意 "D 是 prerequisite"。

但 Max push back 了一句关键的:_"为什么和我记得的不一样,我们要 call ringcentral api,it has rate limit too. if we have too many lambdas working at one time, we will have issues."_ 这一脚把我的 frame 踢碎了。我开始走 `deep-research` skill,做了三件事:读 RingCentral 官方文档拿到具体数字、grep codebase 看代码注释里历史决策、跑 CloudWatch 精确 filter 看真实 429 行为。结果三方面都跟我之前的 impact report 矛盾。

接着进 `first-principles` 做 frame audit,确认了 _"lower bound = limiting factor"_ 这个泛化 frame **对 contacts-analyzer 不适用** — 因为 RC 跟 OneRouter 在不同资源池,contacts-analyzer 的 cron 路径根本不调 RC。Evidence 够 ship test 不够 ship prod。Max 接着说 _"send more, like 30"_,我跑了 burst 实测(165 个并发请求 OneRouter,0 个 429)。这一步把 OneRouter quota 从"未知"变"实证健康到 30+",所有后续判断都站住了。

最后两轮 push back 还修正了两个 misleading 表述。一是我说 "ai-analysis-processor 没设 reservedConcurrency" 让 Max 误以为它完全不限,实际它有 SQS event source 的 `maxConcurrency=2` 间接限速,只是用了不同 AWS feature。二是 Max 问 "can you check git history and changelog to see why we have this rule?",`git blame` 出 6 个 commit 揭示 `reservedConcurrency=2` 不是 design choice,是**一次 35 条 DLQ 危机逼出来的硬数字**(2025-11-23 commit `201b2887`),从 unlimited → 5 → 15 → 2 三次迭代才稳定。

整个调查 5 个 skill 串起来:`callytics-impact-check`(被推翻) → `deep-research`(找证据) → `first-principles`(frame audit) → empirical burst test(实测推翻) → `crystallize`(本文)。最有价值的不是"扩 2→5"这个结论,是**这条 frame correction trail**。

***

## What-We-Researched — 证据来源

### RingCentral 官方 quota(已知 hard cap)

[RingCentral API Rate Limits](https://developers.ringcentral.com/guide/basics/rate-limits) 的官方数字:

> Light: 50 req/user/min; Medium: 40 req/user/min; **Heavy: 10 req/user/min**; Auth: 5 req/user/min。所有 tier 60s penalty。

Call Log Read + Recording Download 都属 Heavy tier。这是**已经在 codebase 里 hard-coded** 的:`lib/stacks/lambda-stack.ts:93` 注释 `"SAFE: RingCentral limit ~10 API calls/min, each execution = 2-3 calls"` 直接对应 Heavy tier 数字。

### OneRouter quota(实测得出,没有公开文档)

`llm.onerouter.pro` 的所有者其实是 **Infron AI**(网站标题 _"Infron AI — The #1 Unified AI Gateway"_),不是公开 LLM 网关 OpenRouter。Infron 网站只说 _"Scale infinitely with flexible capacity and **custom rate limits**"_,WebSearch 公开找不到 RPM/TPM 数字。

实测路径:本机直连 burst test。脚本 `/tmp/onerouter-burst.py`,目标 `https://llm.onerouter.pro/v1/chat/completions`,模型 `x-ai/grok-4.1-fast-reasoning`(production 用的同款)。第一次 burst 5 个全部 HTTP 403 Cloudflare error 1010 — 是 CF bot detection。加上 `User-Agent: openai-python/1.30.0` 后正常。

| Burst N | Prompt            | Success | 429 | 5xx | p95 latency |
| ------: | ----------------- | ------: | --: | --: | ----------: |
|       5 | 10 token          |     5/5 |   0 |   0 |       1.96s |
|      10 | 10 token          |   10/10 |   0 |   0 |       1.99s |
|      20 | 10 token          |   20/20 |   0 |   0 |       3.38s |
|      30 | 10 token          |   30/30 |   0 |   0 |       2.58s |
|      50 | 10 token          |   50/50 |   0 |   0 |       3.64s |
|      75 | 10 token          |   75/75 |   0 |   0 |       2.74s |
|     100 | 10 token          | 100/100 |   0 |   0 |       3.63s |
|       5 | \~3.6KB realistic |     5/5 |   0 |   0 |       5.53s |
|      15 | \~3.6KB realistic |   15/15 |   0 |   0 |       5.18s |
|      30 | \~3.6KB realistic |   30/30 |   0 |   0 |       6.12s |

总 165 个并发请求,**0 个 429,0 个 5xx**。OneRouter 在 ≥30 并发(realistic prompt 尺寸)下健康。我们当前 contacts-analyzer 4 in-flight peak 是它真实容量的 **\<14%**。

### Git blame — 6 个关键 commit

为什么是 2,不是 design choice 是 incident driven。完整时间线:

| 日期             | Commit                  | 谁           | 做了什么                                                                                    | 当时的理由                                                                                                                                                              |
| -------------- | ----------------------- | ----------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 2025-11-05     | `b4d8a26f`              | maxwang     | 初始创建 transcribe-processor + ai-analysis-processor                                       | 都没设 reservedConcurrency                                                                                                                                            |
| **2025-11-23** | **`201b2887`**          | **maxwang** | **transcribe-processor: 设 reservedConcurrency=2**                                       | **35 条 DLQ 在堆积!35 concurrent × 2.5 RC calls = 87.5 calls/min 远超 RC 10/min 上限。从 unlimited → 5 → 15 → 2 三次迭代才稳定**                                                    |
| 2025-11-24     | `20242e5e`              | maxwang     | reconciliation-worker 创建时直接设 reservedConcurrency=2                                      | 注释 _"CRITICAL: Prevent RingCentral 429 rate limit storms"_,吸取教训                                                                                                    |
| 2025-11-28     | `088a4f5d`              | maxwang     | transcribe-processor 加 SQS `maxConcurrency=2`                                           | CloudFormation 报错 _"MaximumConcurrency: 3 is greater than Function Reserved Concurrency: 2"_ — **平台 hard rule**,SQS maxConcurrency 必须 ≤ Lambda reservedConcurrency |
| 2025-12-26     | `94ac099e`              | P (Peter)   | ai-analysis-processor: 删除 reservedConcurrency,加注释 _"Bedrock has no strict rate limits"_ | 当时模型是 Bedrock,认为配额够大                                                                                                                                               |
| 2026-03-19     | `2f222fc9` + `48972169` | P + Max     | contacts-analyzer 创建,reservedConcurrency=2 + SQS maxConcurrency=2 双保险                   | OneRouter,沿用 transcribe pattern                                                                                                                                    |
| 2026-04-21     | infra#618               | Max         | contacts-analyzer 79 条 DLQ — 加 retry + 150s timeout(没改 concurrency)                     | OneRouter 60s timeout + 0 retry                                                                                                                                    |

[Commit `201b2887` PR description](https://github.com/retaintive/callytics-infrastructure/commit/201b2887) 里写明数学:`2 executions × 2.5 calls/exec = 5 calls/min`,RC limit 10/min,**50% safety margin**。这是**有数据支撑的硬约束**。

但 contacts-analyzer 的 `reservedConcurrency=2` 没有等价的数据 trail — 没有 design doc / RFC / commit message 解释数字怎么来的。看 `2f222fc9` commit message,是新 Lambda 创建时 _conservative 沿用 transcribe pattern_,不是基于 OneRouter 实测。

### CloudWatch 真实 retry / 429 行为(精确 filter)

第一轮我用粗 filter `"429"` 拿到 contacts-analyzer 5 events / 7 days,但 sample 显示是 _"Wrote analysis with task decisions"_ 这种 log 里恰好出现 `429` 字符串的误判。换精确 filter 后真实数字:

| Lambda                |            "Retrying" | "exhausted" | "OneRouter API timeout" |               04-30 cron 窗(8h) |
| --------------------- | --------------------: | ----------: | ----------------------: | -----------------------------: |
| contacts-analyzer     |                     5 |           1 |                       0 |                      0 / 0 / 0 |
| ai-analysis-processor |                     0 |           2 |                       0 |                      0 / 0 / 0 |
| transcribe-processor  | n/a (RC 不是 OneRouter) |         n/a |                     n/a | clone-and-redrive 0(RC 也没 429) |

contacts-analyzer 唯一 1 次 exhausted = **2026-04-27 22:44**,在 cron enable **之前** 2 天。04-29 cron enable 之后两次 cron run 全部一次性成功。

RC 端 7 天数字(transcribe-processor 7d invocations 15,870):**92 次 clone-and-redrive,33 次 rate-limited log,46 次 Retry-After header** — 错误率 0.58%。RC 是真热瓶颈,但 04-30 cron 窗内 0 次触发,说明 cron 流量没把 RC 推到 limit 以上。

***

## Conclusions-Reached — 决策与判断

**核心结论**: contacts-analyzer 的 `reservedConcurrency=2` 是 conservative 沿用,不是 OneRouter 实测约束。今天实测 30+ 并发健康,扩到 5 是 reasoning over inertia,不是冒险。

四个组成判断:

**(1) 三个 Lambda 的 `reservedConcurrency` 是 per-Lambda independent setting,不是共享池**。改 contacts-analyzer 不影响 transcribe-processor 的 2,也不影响 ai-analysis-processor。AWS account-wide concurrency limit(default 1000)是唯一共享上限,我们当前 \<10,远充足。

**(2) ai-analysis-processor 的 "no limit" 是表述 misleading**。它没设 Lambda 层 `reservedConcurrency`(commit `94ac099e` 删的),但有 SQS event source `maxConcurrency=2`(`lambda-stack.ts:731`,commit `088a4f5d` 加的)。两种限速作用一样,只是用不同 AWS feature 实现。当前 max=2 是有效约束。

> **历史包袱**: ai-analysis-processor 的代码注释 _"Bedrock has no strict rate limits"_ 现在 stale 了 — 已切到 OneRouter,但注释从 2025-12-26 起一直没更新。这是**真 issue**(将来加新触发源时容易踩坑),但**不是 contacts-analyzer 扩容的 prerequisite**。我之前判断它是"必须先补的 hole"是错的 — 它现在只有一条 SQS 触发源,SQS maxConcurrency 已经罩住。

**(3) RC 不在 contacts-analyzer 的资源池里**。contacts-analyzer 调 OneRouter + Neon,**不调 RC**。RC 错误率 0.58%(7d 92 次 clone-and-redrive)是真问题,但那是 transcribe-processor / message-processor 的问题,跟扩 contacts-analyzer 速度**无关**。"lower bound = limiting factor" 这个泛化 frame 在串行依赖链上才适用,而 EventBridge cron → contacts-analyzer 这条路径完全 bypass RC。

**(4) 扩到 5 是安全的,扩到 10 也健康,15+ 进入 unobserved territory**。理由:

```
2 → 5    OneRouter 实测 30+ 并发健康(safety margin >6×)
         SQS maxConcurrency 必须同步改到 5 (CloudFormation 平台约束)
         Cron drain time 从 3.5h → ~50min (削 76%)
         Per-call ❸ 路径不再被 cron starve

5 → 10   OneRouter 仍健康(safety margin >3×)
         Drain time → ~25min (再削 50%)
         开始要监控 Neon write contention + retry rate

10 → 15  技术 OK(实测 safety margin >2×)
         边际收益递减 (再削只省 17min)
         Neon ON CONFLICT 冲突频率上升(由 SQS FIFO MessageGroupId 部分缓解)

15+ → 30 实测范围内,但 Neon / SQS / 业务逻辑多个维度无 baseline
         不推荐除非有具体 prod traffic 推动
```

**渐进式扩容的理由**: 虽然 OneRouter 实测安全,但 Neon write contention / 数据 race condition / 业务语义 ON CONFLICT 在并发 >5 没 baseline 数据。先 2→5 观察一周(retry rate / Neon error / OneRouter latency),再决定要不要 5→10。

***

## Open-Threads — 没解决的问题

**ai-analysis-processor stale 注释**(P3,不阻塞)。`lambda-stack.ts:194` 写的 _"Bedrock has no strict rate limits"_ 已经过时,现在是 OneRouter。建议改成 _"OneRouter limited via SQS event source maxConcurrency=2 — 见 line 731"_,顺手考虑加 Lambda 层 `reservedConcurrency=2` 作为防御性双保险(将来加 cron / on-demand / API 触发时不会绕过保护)。开独立 issue 追踪,不进 contacts-analyzer 扩容 PR。

**Prod 流量推断未做**。Test 环境 cron dispatch \~491 contact / day,prod 流量未知 — 可能 5-10× test。OneRouter 实测 30+ 并发健康,但那是同时 30 个 short prompt;prod 如果每天 5,000 contact 跑 cron 8h,total throughput 约束可能跟 burst 上限不同。**建议**: prod rollout 前用 prod 量级跑一次 dry-run(把 cron Lambda 的 AI\_ANALYSIS\_ENABLED 临时设 false 看 dispatch 数字),或者在 test 环境分批 enqueue 模拟 5,000 burst。

**Neon write contention 上限未测**。reservedConcurrency 5 在 Neon HTTP API 层不会撞 connection cap(无 conn 限制),但 ON CONFLICT (phone, store\_id) 在并发 >5 的实际 retry 率没数据。**建议**: 扩 2→5 后第一周监控 Neon 端 transaction retry / `last_contact_analysis_at` write conflict log,作为 5→10 的 baseline。

**OneRouter cap 真上限不知**。本次实测到 100 个并发短 prompt + 30 个 realistic prompt 全过,**没找到真 cap**。如果将来要扩超过 15,值得做更激进的 burst test(N=200/300)或直接联系 Infron AI 拿配额数字。

**RC 错误率 0.58% 独立追踪**。跟本 doc 不直接相关,但 7d 92 次 clone-and-redrive 说明 RC heavy tier 已经在 hot zone。如果 prod 流量翻倍,transcribe-processor / message-processor 才是要先评估扩容的对象 — 但那是独立工作,不是这次扩 contacts-analyzer 的副产品。

***

## Next-Session-Entry-Point — 接手指南

**接手扩 contacts-analyzer 2→5 的人**: 改两行 `lib/stacks/contacts-stack.ts`(line 85 `reservedConcurrentExecutions: 2` → `5`,line 130 注释 `maxConcurrency=2` 上面 SqsEventSource 的实际值同步改 5),CHANGELOG 加一行 `feat(contacts-analyzer): reservedConcurrency 2→5 based on burst test (165 concurrent OneRouter requests, 0 errors) — see lambda-concurrency-decisions.md`。Commit message 引用本 doc + git history 6 commit timeline。CDK deploy 前跑 `npm run cdk:diff` 确认只动 contacts-analyzer,不 touch transcribe / ai-analysis。Deploy 后 1 周观察 OneRouter retry rate(`logger.error` Discord/Lark/Sentry alert)和 Neon transaction retry。

**接手 ai-analysis-processor 注释 fix 的人**: 这是独立 P3 issue,跟扩容 PR 不耦合。改 `lambda-stack.ts:194` 注释 + 考虑加 `reservedConcurrentExecutions: 2` 作双保险。SQS maxConcurrency=2 已经在 line 731,所以加 Lambda 层 reservedConcurrency=2 不改变现状,只是防御性写明 intent + 防将来加新触发源踩坑。

**接手将来扩 5→10 决策的人**: 第一步看本 doc _Open-Threads_ 段的"Neon write contention 上限未测"— 有没有过去一周的 retry rate 数据。如果 contacts-analyzer 5→10 的扩容收益(drain time 削 25min)值得花 prod 量级 dry-run 验证,就做;不值得就停在 5。OneRouter 实测安全 margin 仍 >3×,不需要重测。

**接手类似容量调查的工程师 — 调查方法论 5 步**:

1. **Frame audit 优先**: 不要被 plausible-sounding judgment 带偏。"X 改动会影响 Y" 这种声明,先验证 X 跟 Y 真的在同一资源池 / 串行链上。我第一轮 impact report 错判 OneRouter 是 breaking risk,根因是没区分 RC 池跟 OneRouter 池。
2. **Codebase 历史考古**: `git blame` + `git log` commit message 比 design doc 更可信。`reservedConcurrency=2` 在 design doc 里完全没解释,但 commit `201b2887` 的 PR description 写明 `35 条 DLQ 危机 + 87.5 calls/min 算式 + 50% safety margin`。Code-as-source-of-truth 不只是代码,是代码 + git 元数据。
3. **官方文档拿 hard cap,实测拿 actual cap**: RC 的 10/min 是官方文档 hard cap 不会变。OneRouter 的 cap Infron AI 不公开,只能 burst 实测。两种 cap 不能混用 — hard cap 不需要实测,actual cap 需要持续验证。
4. **Cross-CLI consult 是 second opinion 不是 verdict**: Codex + Gemini 第一轮都被我误导(因为我给的 brief 里有错误前提),它们 confidently 说"D 是 prerequisite"。Max 的直觉 push back 才是真的 frame correction signal。AI consult 在 frame 错的时候只会强化错 frame。
5. **Burst test 早做不晚做**: 165 个 OneRouter 请求 + 60s cooldown 一共耗时 \~10 分钟,$0.50 cost。这一步把 OneRouter quota 从"未知"变"实证健康",比之前 1 小时调研 + 跨 CLI consult 更 actionable。**遇到 quota 未知的 capacity 问题,先 burst 实测,再讨论方案**。

***

## Appendix — Frame Correction Trail

为什么这次调查值得记录,是因为 frame 改了 4 次。每次都是 Max 的 push back 触发。这条 trail 对将来类似调查有方法论价值。

**Frame v1**(我的): "调高 contacts-analyzer reservedConcurrency 会同时影响 RC 和 OneRouter pool" → 错误,把两个不相关资源池混了。

**Frame v2**(Max push back): "we call ringcentral api, it has rate limit too" → 修正了"OneRouter 是唯一约束"的错觉,但仍然把 RC 当 contacts-analyzer 直接相关因素。

**Frame v3**(deep-research 后): "RC 不在 contacts-analyzer 资源池,但 OneRouter quota 未知是 risk" → 修正了 RC 关系,但仍把 OneRouter quota 当 unknown 当 risk。

**Frame v4**(Max "send 30" + burst test): "OneRouter 实测 30+ 并发健康,RC 跟 contacts-analyzer 独立" → 完整修正,evidence-driven。

**Frame v5**(Max "ai-analysis has no limit, why we limit this?"): "ai-analysis-processor 的 no-Lambda-layer-limit 是 misleading 表述,它有 SQS 层 maxConcurrency=2 等价限速" → 修正了"ai-analysis 是 hole 必须先补"的错判。

每次 frame correction 都打掉了一个我之前 confidently 给的判断。最大的教训是 **plausible-sounding 不等于 correct** — Codex + Gemini cross-validate 在 frame 错的时候只会强化错 frame,不能当 verdict。Max 的直觉 push back("为什么和我记得的不一样")是最有价值的 frame audit signal,值得 trace 到底。

***

## Update 2026-04-30 — PR #693 Implementation Outcome + Codex Review Cycle + Downstream Burst Verification

v1 doc 的判断("扩 2→5 安全,有 OneRouter 6× safety margin")在 v1 写的时候只测过 OneRouter 一条路径。本次 update 记录 PR #693 实施后的两件事:**Codex adversarial review 抓到的 2 个 real issue + 2 个 challenge**,以及**为了 address 那 2 个 challenge 在 Neon ephemeral branch 上跑的端到端 burst test**。结果是 v1 决策 evidence-完备地落地,merged 进 main。

### Codex Review Cycle — 2 Real Issues Caught(post-PR-open 阶段)

提 PR #693 后立刻让 Codex 做 adversarial review(`cat brief.md | codex exec`,brief 包含完整 diff + body + context)。Codex 4 段返回:bugs / hidden assumptions / TDD gap / single biggest risk。**它抓到了 2 个真问题,我修了。**

第一个问题是注释逻辑颠倒。我在 `lambda-stack.ts:194` 写了 _"Until now SQS event source maxConcurrency=5 was the only effective limiter"_ — 但 PR 改之前 maxConcurrency 实际是 2,改之后才是 5,_"Until now"_ 这个时间锚点指 pre-PR 状态,所以应该是 _"Until now SQS event source maxConcurrency=2 was the only effective limiter"_。读注释的人会一头雾水,这是 documentation bug。修后:_"Before this PR the SQS event source maxConcurrency=2 (now bumped to 5 alongside, see line \~754)"_,逻辑顺了。

第二个问题更严重,是真 TDD gap。我以为我加的 ai-analysis-processor 测试已经 cover SQS event source 的 maxConcurrency 改动,但 Codex 指出 `lambda-stack-worker.test.ts:179` 的 _"Event source mapping has maxConcurrency of 2"_ 测试用的是 `.some(MaximumConcurrency === 2)` pattern — 它只检查 _"任何一个"_ SQS event source mapping 的 maxConcurrency 是 2 就 pass。本 repo 有 3 个 SQS event source mapping (transcribe-processor / reconciliation-worker / ai-analysis-processor),前两个仍是 2(它们调 RC,不动),所以**就算我把 ai-analysis-processor 改回 2(回滚错了 / merge 冲突 / 手抖),这个测试仍 pass** — false security。

修法是在 `test/lambda-stack-xray.test.ts` 加 anchored test:`findResources('AWS::Lambda::EventSourceMapping')` → `find(mapping => mapping.Properties.FunctionName.Ref.startsWith('ResultProcessorTS'))` → assert `ScalingConfig.MaximumConcurrency === 5`。新测试**精确**指向 ai-analysis-processor 这一条 mapping,任何回滚都会被抓到。

我做了一次 retroactive 验证证明新测试真的能 catch:把 ai-analysis SQS maxConcurrency 临时改回 2 → 跑测试 → **新测试 fail (16/17 pass)**;改回 5 → **17/17 pass**。Test 真锁住了行为。

剩下 2 个不是 issue 而是 challenge,Codex 自己也说 _"noted, not code-fixed"_。Challenge 1 是 PR body wording:_"contacts-analyzer only calls OneRouter + Neon"_ 不准确,它还调 PhoneStoreAssignments DynamoDB(`handler.ts:188-198` via `resolvePhoneIdentity`)。这个 wording 错误不影响"扩 5 安全"的结论 — DDB GetItem 在我们规模无 quota 压力 — 但措辞应该改正。Challenge 2 是 _"OneRouter burst test 不能覆盖 prod end-to-end:真实路径还有 validation retries、Neon writes、DynamoDB lookup、SSM/Secrets cold path"_ — 5× concurrency 可能放大 Neon write contention 或 DDB throttle 或 retry traffic,burst test 只测了 OneRouter network capacity。

Codex 的 single biggest risk 总结说得很到位:_"最大风险不是 OneRouter 429,而是下游端到端容量误判:5 并发会放大 Neon/DynamoDB writes/queries 和 retry traffic。若 Neon contention 或 AI validation retry 上升,SQS 会延长 drain、放大重试成本,并继续占满 reserved concurrency。"_ 这才是真正应该 verify 的。

### Downstream Burst Test — Neon Ephemeral Branch Isolation

为了真正 close challenge 2,在 Neon `callytics-test` project 上创了 ephemeral branch `concurrency-burst-test-2026-04-30`(branch ID `br-delicate-mud-akp0gxj2`,copy-on-write fork 秒级,完整 7,330 contacts 数据),burst test 跑在 fork 上,跑完直接 delete branch — parent test 数据 0 影响。这是 CLAUDE.md 里 canonical 的 backfill / migration safety pattern,也适用于 capacity test。

Test 模拟 contacts-analyzer 实际写 pattern:`UPDATE contacts SET last_contact_analysis_at = NOW(), lead_status = ?, updated_at = NOW() WHERE phone = ? AND store_id = ? RETURNING phone, store_id, last_contact_analysis_at`。三个并发 level (5 / 15 / 30),30s cooldown 让 Neon autoscale 沉淀。

第一组测**不同 row 的 UPDATE**(典型 cron / per-call 场景,每个 worker 写不同 contact):

|  N | Status   | Latency p50 |    p95 |    Max |
| -: | -------- | ----------: | -----: | -----: |
|  5 | 5/5 OK   |      1474ms | 1484ms | 1484ms |
| 15 | 15/15 OK |      1018ms | 1020ms | 1020ms |
| 30 | 30/30 OK |      1065ms | 1356ms | 1356ms |

注意 N=30 的 latency 比 N=5 还快 — connection pool 暖了之后 UPDATE 更快。0 serialization failures, 0 deadlocks。**Neon 在 30 并发不同 row 完全顶得住**,这是 6× 我们新 cap 的安全边际。

第二组测**同 row 的 UPDATE worst case**(模拟极端场景:cron + per-call ❸ + reconciliation 同时触发同一 contact),全部 worker 打同一行:

|  N | Status   |    p50 |    p95 |    Max | Queue depth signal (max-min) |
| -: | -------- | -----: | -----: | -----: | ---------------------------: |
|  5 | 5/5 OK   | 1466ms | 1713ms | 1713ms |                        448ms |
| 15 | 15/15 OK | 1727ms | 2429ms | 2429ms |                       1427ms |
| 30 | 30/30 OK | 2644ms | 3998ms | 4099ms |                       3125ms |

queue depth signal(max-min)随 N 线性增长是预期的:Postgres 在 row level 序列化每个 UPDATE,晚到的 worker 等前面 commit。**但没有 deadlock,没有 serialization failure** — Postgres 自己处理得很干净。N=5(我们实际 cap)worst case 也只 1.7s,完全可以接受;就算极端到 N=30 也只需 4.1s,远低于 Lambda 15min timeout。SQS FIFO MessageGroupId 在大多数情况下已经把 cross-source race 挡住,这个测试是 worst-of-worst 的 sanity 验证。

第三组测 **DDB GSI 查询**(对应 `resolvePhoneIdentity` 调 PhoneStoreAssignments `phoneNumber-index`),用真实 phones 跑 5 / 15 / 30 并发:

|  N | Status   |    p50 |    p95 |    Max |
| -: | -------- | -----: | -----: | -----: |
|  5 | 5/5 OK   | 1761ms | 2595ms | 2595ms |
| 15 | 15/15 OK |  383ms |  418ms |  418ms |
| 30 | 30/30 OK |  870ms |  927ms |  951ms |

N=5 第一次 cold start 是 boto3 client init,后续 N=15/30 都 \< 1s。PAY\_PER\_REQUEST 模式 auto-scale 不需要 capacity planning,30 并发 query 0 throttle / 0 RequestLimitExceeded / 0 ProvisionedThroughputExceeded。

### 三方汇总 — 5× Concurrency 安全边际验证

| 资源                        | 当前 peak (N=4) |  新 cap (N=5) | 实测 ≥30 表现                         | Codex challenge 2 verdict           |
| ------------------------- | ------------: | -----------: | --------------------------------- | ----------------------------------- |
| OneRouter API             |   4 in-flight |  5 in-flight | 165 burst, 0 errors               | ✅ refuted (no acute capacity issue) |
| Neon UPDATE 不同 row        |  4 concurrent | 5 concurrent | 30 concurrent in 1.4s             | ✅ refuted                           |
| Neon UPDATE 同 row (worst) |            罕见 |           罕见 | 30 concurrent in 4.1s, 0 deadlock | ✅ refuted                           |
| DDB GSI lookup            |  4 concurrent | 5 concurrent | 30 concurrent in 1s, 0 throttle   | ✅ refuted                           |

Codex 的 challenge 2 在 5× concurrency 下 empirically 不成立。剩下未验证的是 **prod-volume sustained throughput**(我们的是 burst,不是 soak)、**cold-start cascade after Lambda warm-up window**、**AI validation retry storms under OneRouter response variability** — 这些 remain post-merge-monitor 期(2026-05-07 截止)的 watch items,但**没有 acute capacity issue 阻止 deploy**。

### PR #693 Merge Outcome + What Landed on Main

PR #693 于 2026-04-30 18:22 UTC merge,squash commit `572c39f`,`deploy-test.yml` auto-triggered for `572c39f`。最终 diff:5 files changed (+89/-12),Codex 反复 review 的版本。Test suite 从 v1 doc 写时的 148 涨到 150(+2 anchored tests:contacts-analyzer 和 ai-analysis-processor 各一个,前者 specific reservedConcurrency=5 + maxConcurrency=5,后者 specific anchored to ResultProcessorTS Ref 的 maxConcurrency=5)。

Resource pool 表格(更新版,补 DDB 维度):

| Lambda                | OneRouter | Neon writes |    DDB GSI lookup   |                      RingCentral                      |
| --------------------- | :-------: | :---------: | :-----------------: | :---------------------------------------------------: |
| contacts-analyzer     |     ✅     |      ✅      | ✅ (verified ≥30 OK) |                           ❌                           |
| ai-analysis-processor |     ✅     |      ✅      |          ❌          | ❌(transcribe-processor 上游 reservedConcurrency=2 间接节流) |
| transcribe-processor  |     ❌     |      ❌      |          ❌          |         ✅(gates the chain at RC heavy 10/min)         |

contacts-analyzer 不调 RC 的论点不变 — 改 contacts-analyzer reservedConcurrency 跟 RC pool 完全独立。新版表格补充了 DDB 这一栏,跟 Codex challenge 1 的修正一致。

### Reproducer Scripts

3 个 burst test scripts:

- `/tmp/onerouter-burst.py` — OneRouter `llm.onerouter.pro/v1/chat/completions` 并发 burst,模拟 contacts-analyzer 实际 prompt 大小(\~3.6KB realistic + 短 prompt 两组)
- `/tmp/neon-burst.py` — 30 个不同 (phone, store\_id) row 并发 UPDATE,connection-per-thread
- `/tmp/neon-burst-conflict.py` — 30 个 worker 同 row 并发 UPDATE(worst-case 序列化测试)
- `/tmp/ddb-burst.py` — PhoneStoreAssignments GSI `phoneNumber-index` query 并发 burst,boto3 client-per-thread

Scripts 当前在 `/tmp/`(本机),没 commit 到 repo。如果将来要做类似 capacity 调研,可以先来这份 doc 找参考实现,然后从 git history (`git log --grep=concurrency-bump --since=2026-04-30`) 找到 PR #693 ↔ docs PR #263 的双向 link 链。

### v2 总结 — Methodology Reinforcement

v1 doc 末尾说 _"plausible-sounding 不等于 correct"_ — 这一轮 Codex review 是这个原则的**延伸验证**。我自己第一版 PR 自信地说 _"OneRouter 验证完了,Neon HTTP API 无 connection cap 所以肯定没问题"_ — 这是 plausible-sounding。Codex 抓出 _"你只测了一个维度,真实路径还有 3 个"_,我接受 + 跑实测 + 数据全过 = "假设变成验证"。这跟 v1 用户 push back 触发的 frame correction 是同款 pattern,不同的是这次是 LLM agent 帮我做了这步。

新增到 v1 methodology(第 5 步)的 corollary:**Burst test 不止做一次,任何 plausible-sounding 安全性 claim 都该问 "我有没有数据?**" — 如果答案是 "我假设它 OK",那就是 v1 第 5 步说的 "未实测假设"。这次 Codex 等于代替用户问了这个问题。Cross-CLI consult 不当 verdict 但当**前置 challenge 来源**确实有用,前提是给的 brief 不已经被你的错 frame 污染过。

### v2 Open Threads(超出本 update)

剩 1 个没 close 的(独立 followup,跟本 doc 主题相关但不属于这次 update 范围):

🟡 **Hidden Issue #2 from #691** — 24h cron lookback vs 5,001 stale never-analyzed contact backlog mismatch。3 options(A: 一次性 backfill \~$50 OneRouter cost / B: cron lookback 24h → 168h cost 7× / C: 接受 stale 只用 cron 维护新流量)。本 PR 不解决,独立追踪在 issue #691。
