- dasha-unify: fix Ashtottari birth balance (BUG-1344) - rect-scoring-perf: server-side candidate cap 241 (BUG-1345) - node-runtime-resources: enable user-side session cookie cache, 300 s (BUG-1346) - consult-prep-parallel: classifier/reserve concurrency declined - answer-effort-ab: model key supplied by product at acceptance - drop stricter-than-test line-count red lines (AGENTS.md section 6) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
22 KiB
TASK · 星盘接口与报告的多余子进程、合盘串行、重计算名额轻重混用(issue #待定)
- 日期:2026-10-13
- 来源:「2026-10-13 全仓屎山与性能排查」P1 · 星盘接口子进程与闸门
- 执行方:Grok(Windows + Docker),分支
codex/api-chart-subprocess-gate-20261013,只推这个分支 - 验收:Claude(Linux)
- BUG 编号:BUG-1333~1334(开工时核对
docs/BUG_HISTORY.md最大号;写作时最大号 BUG-1317,BUG-1323~1332 已被同批其它任务书占用)(另:TASK-upstream-sync7-20261014占 BUG-1318~1322,本批号段整体从 BUG-1323 起) - 开工前置:
TASK-api-disk-cache-retention-20261013(分支codex/api-disk-cache-retention-20261013)已合入origin/staging。两单都改scripts/jyotish_api_server.py,且本单 A/B 依赖那一单的缓存键规范化;未合入不得开工。 - 主会话补充的串行链(2026-10-14):引擎/API 文件链按此顺序逐单合入后开工(
upstream-sync7也改jyotish_engine.py/jyotish_api_server.py,若在途,后合一方 rebase):api-disk-cache-retention+report-reference-perf→api-chart-subprocess-gate→pl9-render-split→python-hygiene→dasha-unify→api-server-service-extract。本单的前置以这条链为准(与上一行不一致时以本行为准)。
1. 基线
- 基线 commit:
origin/staging77057a819(开工时以前置单合入后的origin/staging为准,§9 有核对命令)。 - 生产镜像只装
requirements.txt:有vedastro==1.23.25,没有 PyJHora(PyJHora 在requirements-reference-engines.txt,AGPL,只作离线参照)。本单所有计时必须在「模拟生产依赖」下再跑一遍:把只含下面这个sitecustomize.py的目录放在PYTHONPATH最前(子进程会继承)。
# <临时目录>/nojhora/sitecustomize.py —— 只挡 jhora;要模拟「无 VedAstro SDK」时把 'vedastro' 也加进集合
import sys
class _Block:
def find_spec(self, name, path=None, target=None):
if name.split('.')[0] in {'jhora'}:
raise ModuleNotFoundError(f"No module named '{name}'")
return None
sys.meta_path.insert(0, _Block())
2. 事故实证
计时脚本:用 JyotishAPIHandler.__new__(JyotishAPIHandler) 造处理器,直接调 _compute_chart_sync / professional_report_reference.build_professional_report_reference,monkeypatch subprocess.run 记录每次命令与耗时;JYOTISH_API_CHART_CACHE_TTL_SECONDS=0 避开缓存。虚构盘 1990-01-01 12:00 新德里 28.6139/77.2090 +5.5。本机(Linux,无 vedastro SDK、外网关闭)实测:
2.1 /api/chart 不带 skip 每次起 2 个子进程(BUG-1333)
| 请求 | 子进程 | 热态耗时(4 次,后 3 次中位) | 响应体 |
|---|---|---|---|
不带 skip_vedastro_main_entry_overview |
2 个:vedastro_official_capability_runner.py --bundle official_full_snapshot 与 --bundle official_full_capability_catalog |
约 1.03–1.09 s(排查报告另一次测得 0.56 s) | 160,643 字节 |
| 带 skip | 0 | 约 5–9 ms | 55,345 字节 |
- 子进程来自
scripts/jyotish_api_server.py::_attach_vedastro_main_entry_overview→vedastro_evidence_orchestrator.orchestrate_vedastro_evidence(route='overview')→scripts/vedastro_service_adapter.py的subprocess.run。本机没有 SDK,子进程秒败;生产装了 SDK,若开了外网,这两个子进程会真的打 VedAstro,耗时上限是VEDASTRO_TIMEOUT_SECONDS——本机测不到,是环境缺口。 - 出生资料以
--birth-json明文出现在子进程命令行里(同机ps可见)。记录,本单不改。 /api/chart不在scripts/api_heavy_compute_gate.py::HEAVY_COMPUTE_PATHS里(tests/test_api_heavy_compute_gate.py::test_health_and_light_paths_are_not_gated明确断言"/api/chart" not in HEAVY_COMPUTE_PATHS),所以这 1 s 级的子进程调用不受名额约束。
2.2 前端调用方逐一核对
| 调用方 | 是否带 skip | 用到响应里的什么 | 需要 VedAstro 主入口概览吗 |
|---|---|---|---|
frontend/src/lib/chart-view-load.ts(星盘页主盘,payload) |
带 | 盘面 | 否 |
同文件 transit 层 → frontend/src/lib/transit-chart-body.ts::transitChartBody |
带 | 行运盘 | 否 |
frontend/src/app/api/ephemeris/route.ts(两处) |
带 | 盘面 | 否 |
frontend/src/app/api/synastry/route.ts::POST |
不带 | 只读 planets、ascendant、月亮经度(moonLongitude) |
否 |
- 合盘还串行:
selfChart与partnerChart两次await postPython("/api/chart", …)先后执行;romance 分支里两次/api/varga_full(D9)也是串行(business 分支已是Promise.all)。按本机数字,合盘一次光是两次 chart 就约 2 s,带 skip 并发后约 10 ms 级。 - Python 内部:
execute_consultation_workflow的compute_chart步骤按defer_optional_external_evidence传 skip(前台咨询 skip,BUG-161);个人报告、高严谨、主题报告、MCP / API Explorer(API_COMMAND_MAP的chart、full-reading)走不 skip 的完整链。这些保持不变(BUG-161:个人报告保持默认完整工作流)。
2.3 报告每次起 10 个子进程(BUG-1334)
/api/professional_report_reference(edition=full_data、languages=["zh","en"],个人报告长文的真实请求形状,见 frontend/src/lib/personal-report-longform-generate.ts),各跑 2 次:
| 环境 | 总耗时 | 子进程数 | 子进程合计 |
|---|---|---|---|
| 本机(装了 PyJHora) | 19.8 s / 18.4 s | 10 | 6.5 s / 6.0 s |
模拟生产(挡 jhora,本机本就无 vedastro SDK) |
13.5 s / 13.2 s | 11 | 0.71 s / 0.92 s |
子进程明细(模拟生产):
| 子进程 | 次数 | 每次 | 来源 |
|---|---|---|---|
vedastro_official_capability_runner.py(snapshot / catalog)+ 无 SDK 时多一个 vedastro_python_bridge.py |
2–3 | 0.06–0.09 s | 官方快照,报告完整链的一部分,保留 |
oracle_collection_queue.py → oracle_evidence_validator.py |
2 | 0.03–0.27 s | scripts/jyotish_engine.py::_oracle_progress_snapshot:读仓库里的静态文件 references/oracle/dasha_shadbala_oracle_cases.json 做校验,与用户盘无关,每份报告重跑 |
python -c "from scripts.annual_pyjhora_replay import _isolated_tajaka_worker…" |
3 | 0.06–0.08 s | scripts/annual_pyjhora_replay.py::_run_isolated_tajaka_year_lord |
python -c "… _isolated_annual_worker…" |
3 | 0.06–0.08 s | ::_run_isolated_annual_replay |
- 修正排查报告:生产没有 PyJHora 时这 6 个 PyJHora 子进程确实「快速失败但仍付启动开销」,但每个只有约 60–80 ms,合计约 0.4 s;排查报告里的「年度回放 3 次 ~4.2 s、Tajika 3 次 ~0.9 s」只在装了 PyJHora 的机器(开发机、参照环境)成立。生产上的收益是每份报告约 0.5–0.9 s、8 个子进程,不是 5 s。
- 另:
scripts/tajika.py::_resolve_external_pyjhora_python不缓存「找不到 PyJHora」的结论,每次都对python3.14、python3各起一个find_spec探测子进程(当前解释器路径与候选相同时才跳过)。生产镜像里sys.executable与which python3是否同一路径未核实——执行方在 Docker 镜像里核实并写进 PROGRESS。
2.4 重计算名额轻重混用(只评估)
HEAVY_COMPUTE_PATHS共 22 条,共用一个BoundedSemaphore(JYOTISH_HEAVY_COMPUTE_CONCURRENCY=2),满了立刻 429(acquire_heavy_compute_slot非阻塞)。- 排查报告数字(本单 A/B 复测):
/api/consultation_workflow卡片缓存命中时约 0.2 s;个人报告约 18 s(本机模拟生产 13 s);校正打分 5–12 s。两份报告同时在算时,任何一条 0.2 s 的咨询请求都会拿到 429(前端frontend/src/mastra/consultation-workflow.ts映射为workflow_rate_limited)。 - 现在 429 发生时日志里看不出名额被谁占着,staging 上无法从记录确认这种「连带 429」的频率。
3. 根因
- BUG-1333:合盘路由写于 BUG-161 之前或之外,没跟上「只读盘面的调用方带 skip」的约定;
/api/chart默认走完整链(为 MCP / 报告保留),于是任何漏带 skip 的调用方都会白付两个 VedAstro 子进程。串行await是写法问题。不是回归,是约定从没落到这个路由,也没有合同测试拦。 - BUG-1334:PyJHora 回放为了 AGPL 隔离用子进程(正确),但没在父进程先判断「本机根本没装 PyJHora」;oracle 进度快照是给离线审计表用的,被挂在每份报告的技法审计表构建里,又没有进程内复用。
- 名额:闸门按「路径」一刀切,不按耗时分级;BUG-717 明令不得靠放宽路径或调高并发解决 429,所以轻重分名额不是技术问题,是产品取舍。
关联:BUG-161、BUG-301(前台仍取官方层)、BUG-715 / BUG-717(星盘页不得 429、不得放宽 HEAVY_COMPUTE_PATHS 或提高并发)、BUG-720(前台不得无上限排队)、BUG-721(禁止模块级缓存跨请求持有出生资料派生数据)、BUG-723(429 必须单独成档)、BUG-1302(预热不占名额)、BUG-1265 / 验收须模拟无 PyJHora。
4. 决策记录
- 产品负责人 2026-10-13 同意按排查结论全部立单交 Grok 执行;P1,必须 A/B。
- 授权范围:合盘改带 skip 并发;报告里与用户盘无关或必然失败的子进程在父进程短路;给闸门加观测。
- 不推翻 BUG-717:本单不把任何路径移出
HEAVY_COMPUTE_PATHS,不新增第二个并发池,不改JYOTISH_HEAVY_COMPUTE_CONCURRENCY的语义与默认值。轻重分名额只做评估与模拟(T5),结论交产品拍板后另立单。 - 不推翻 BUG-161 / BUG-301:
/api/chart不带 skip 时的默认行为(附 VedAstro 主入口概览)不变;报告、高严谨、MCP 的完整链不变。 /api/chart不带 skip 时占名额(T3)是收紧而非放宽,但会让 MCP 等外部调用方可能拿到 429,所以做成开关、默认关,A/B 有证据再提议打开(打开仍需拍板)。
5. 硬红线
scripts/jyotish_api_server.py:行数只按tests/test_api_server_growth_contract.py的粗护栏(AGENTS.md §6:不另加比测试更严的行数红线,也不得压行凑数),类方法数与JyotishAPIHandler.__new__伪造点不得增长。闸门的判定逻辑(含 BUG-1302 的consult_card_warm_only判断)可以搬进scripts/api_heavy_compute_gate.py,主文件调用点保持一行。frontend/src/app/(app)/page.tsx不碰;不写第二套请求封装。- 不改
deploy/**、.gitea/workflows/**、数据库结构与迁移;不升级依赖。 - 不改引擎数值。所有改动在「模拟生产依赖」与「装了 PyJHora」两种环境下,报告 Markdown(中英)与
fact_table_packet逐字节一致(去掉生成时间戳等已知易变字段,易变字段清单写进差分脚本并在 PROGRESS 列出)。 - 进程内复用只允许缓存与用户无关的结论(PyJHora 是否可导入、静态 oracle 文件的校验结果,键含文件路径 + mtime + 大小);不得缓存任何含出生资料的结果(BUG-721)。
tests/test_api_heavy_compute_gate.py现有断言(含"/api/chart" not in HEAVY_COMPUTE_PATHS、DEFAULT_CONCURRENCY == 2)一条不改。- 改既有断言写「原值 / 新值 / 原因」三栏;golden 来自真实引擎输出。
6. 任务分解
T1 · 合盘:带 skip、两两并发(BUG-1333)
frontend/src/app/api/synastry/route.ts:两次/api/chart请求体加skip_vedastro_main_entry_overview: true(在路由里加,不改synastryBirthPayload,因为global-birth-payloads.ts别处也用);selfChart/partnerChart改Promise.all;romance 分支两次 D9/api/varga_full改Promise.all;/api/synastry调用仍在两者之后。错误语义不变(任一失败仍走现有catch返回blocked)。- 新合同测试(前端):桩
fetch,断言两次/api/chart请求体都带 skip、两次是并发发出(第二次在第一次 resolve 之前已发出);romance 与 business 两分支的最终 JSON 与改前桩输出一致。 - 同时加一条源码合同:
frontend/src/app/api/**与frontend/src/lib/**里所有向 Python/api/chart发的请求体都必须带skip_vedastro_main_entry_overview: true,除非在白名单里写明理由(当前白名单为空)。 - 验收:
tsc --noEmit0 错、npm run lint0 error、新测试与frontend/tests/chart-view-route.test.ts、frontend/tests/ephemeris-route.test.ts单文件通过。 - 分工:Grok 自跑;Claude 复验 + 前端全量
npm test。
T2 · 报告子进程短路(BUG-1334)
scripts/annual_pyjhora_replay.py:在起子进程前,父进程用importlib.util.find_spec("jhora")判断(进程内记一次,可被测试重置)。不可导入时不起子进程,直接返回与「子进程在无 PyJHora 环境下返回的结果」完全相同的 dict。做法:先在模拟生产环境里真实跑一次子进程,把返回值存成 golden(tests/fixtures/…,写明生成命令),短路分支的返回值必须与 golden 相等(request_hash、settings等按入参计算的字段照常计算)。scripts/tajika.py::_resolve_external_pyjhora_python:结果进程内记一次(含「没找到」)。scripts/jyotish_engine.py::_oracle_progress_snapshot:按 (oracle_file路径, mtime, 大小) 进程内记一次;文件变了自动重算。- 不改:VedAstro 官方快照子进程;装了 PyJHora 时的子进程隔离(AGPL 边界,必须仍是子进程)。
- 验收:
- 新测试:无 PyJHora 时
_run_isolated_tajaka_year_lord/_run_isolated_annual_replay不调用subprocess.run且返回值等于 golden;装了 PyJHora(或find_spec被桩成可用)时仍调用子进程;_oracle_progress_snapshot连调两次只跑一次子进程,touch文件后重跑。 - 既有
tests/test_report_reader_main.py、tests/test_report_longform_parity.py、tests/test_report_density_facts.py及grep -rln "annual_pyjhora_replay\|_oracle_progress_snapshot\|_resolve_external_pyjhora_python" tests命中的测试全绿。
- 新测试:无 PyJHora 时
- 分工:Grok 自跑;Claude 复验 + 全量 pytest 与基线逐名比对(引擎相关文件有改动)。
T3 · /api/chart 不带 skip 时占重计算名额(开关,默认关)(BUG-1333)
scripts/api_heavy_compute_gate.py新增acquire_heavy_compute_slot_for_request(path, body)(或同义名):现有按路径的逻辑原样保留;另加:path == "/api/chart"且 body 不带真值skip_vedastro_main_entry_overview且 body 不带async/enqueue且开关JYOTISH_GATE_CHART_OVERVIEW=1时,占一个名额。BUG-1302 的consult_card_warm_only判断一并搬进来。jyotish_api_server.py调用点改成这一个函数(新逻辑放新模块,主文件只留调用)。HEAVY_COMPUTE_PATHS不加/api/chart。- 验收:新测试——开关关时任何
/api/chart都不占名额;开关开时只有不带 skip 的同步请求占名额,带 skip 的、async的不占;test_warm_consultation_does_not_consume_the_user_slot等既有测试不改仍绿。 - 分工:Grok 自跑;Claude 复验。
T4 · 名额观测(BUG-1333 防复发的一部分)
api_heavy_compute_gate.py记录当前占名额的路径(只记路径字符串与开始时间,进程内计数);HeavyComputeBusy抛出时,服务端打一行结构化日志:被拒路径、当前占用者路径列表与已占秒数。不得记 body、出生资料、用户标识。- 若能不增长主文件:
/api/health的返回里加heavy_compute: {limit, in_use}(只数字);做不到不增长就不做,写进 PROGRESS。 - 验收:新测试——饱和时日志行含占用者路径、不含 body 字段;释放后计数归零。
- 分工:Grok 自跑;Claude 复验。
T5 · A/B 验证(P1 必做)
- 脚本
scripts/research/api_chart_subprocess_ab.py(位置执行方定,写进 PROGRESS)。A = 前置单合入后的origin/staging(git show <sha>:<path>导出到临时目录同进程 import),B = 本分支。每组重复 3 次取中位数,两种依赖环境(模拟生产 / 装了 PyJHora)各一遍;Grok 侧若装了 vedastro SDK 且有外网,再加一组「真 SDK」并如实标注。- 合盘:同一对虚构盘,A 的调用序列(两次不带 skip、串行)vs B(两次带 skip、并发),在 Python 侧用线程池模拟并发。度量:总耗时、子进程数、响应字节。正确性:合盘最终 JSON(
selfChart/partnerChart摘要、synastry、relationshipReport)A/B 逐字节一致。 - 报告:
build_professional_report_reference(真实请求形状),度量总耗时、子进程数与合计耗时;正确性:中英 Markdown 与fact_table_packet去易变字段后逐字节一致。 - 闸门模拟(评估用,不改生产行为):用真实
api_heavy_compute_gate模块,按实测耗时用sleep模拟混合负载(例:每分钟 2 份报告、20 次咨询 0.2 s、6 次校正打分 8 s,随机种子固定),统计各路径 429 比例。比较方案:O1 现状;O2 轻路径独立小池(违反 BUG-717,只模拟不实现);O3 轻路径有界等待(≤1.5 s,不增并发,只模拟);O4 名额预留(重路径最多占N-1,只模拟)。输出每个方案下咨询 429 率、报告/校正 429 率、CPU 最高并发数。
- 合盘:同一对虚构盘,A 的调用序列(两次不带 skip、串行)vs B(两次带 skip、并发),在 Python 侧用线程池模拟并发。度量:总耗时、子进程数、响应字节。正确性:合盘最终 JSON(
- 目标:合盘 chart 段耗时 B ≤ A 的 10%、子进程 4 → 0;报告子进程 B ≤ 3(模拟生产)、总耗时不劣于 A;所有正确性比对 0 差异。达不到如实写。
- 开关默认值:T3 默认关;T1、T2 不设开关(纯去冗余,正确性由逐字节比对保证)。
- 记录写
docs/testing/api-chart-subprocess-gate-ab-20261013.md;O1–O4 的模拟结果整理成给产品看的一张表(方案 / 咨询 429 率 / 报告 429 率 / 是否触碰 BUG-717)。 - 分工:Grok 跑并交记录;Claude 在 Linux 上复跑 1、2 两项(数字差 >20% 以 Claude 为准)。
T6 · 记录
docs/BUG_HISTORY.md:BUG-1333(/api/chart漏带 skip 的合盘白付 VedAstro 子进程且串行、不占名额、429 无观测);BUG-1334(报告每份 10 个子进程,其中 PyJHora 回放在生产必然失败、oracle 静态校验每次重跑)。BUG-1334 的「验证」里写清生产收益是约 0.5–0.9 s / 份,不是 5 s。docs/tasks/PROGRESS-api-chart-subprocess-gate-20261013.md,含 T5 O1–O4 结论与「需拍板」一节。CHANGELOG.md:「合盘更快」一句。docs/tasks/README.md索引加本单。- 不改
frontend/DESIGN.md(无 UI 变化)。
7. 让步顺序
时间不够时按此顺序往后砍:T4 的 /api/health 字段 → T3 → T5.3 闸门模拟 → T2 的 _resolve_external_pyjhora_python。T1、T2 主体、T5.1–5.2、T6 不可砍。
8. 验收命令分工
| 谁 | 命令 |
|---|---|
| Grok 自跑 | .venv 下:python -m pytest tests/test_api_heavy_compute_gate.py tests/test_api_server_growth_contract.py tests/test_report_reader_main.py tests/test_report_longform_parity.py tests/test_report_density_facts.py <T2/T3/T4 新测试> -q;前端 tsc --noEmit、npm run lint、T1 新测试与 chart-view-route / ephemeris-route 单文件;T5 脚本。前端全量、Python 快速门、全量 pytest 不跑,以 Claude 为准 |
| Claude 验收 | 上述全部 + run_quality_gate.py --profile quick + 全量 pytest 与基线逐名比对(T2 动了引擎文件)+ 前端全量 npm test + 在挡 jhora 的环境复跑 T2 测试与 T5.1–5.2 + wc -l scripts/jyotish_api_server.py 不超过开工值 |
| 不需要 | test:db;next build 只在 Claude 侧跑一次确认 / 仍 Static(只改了 API 路由) |
9. 开工前置命令
git fetch origin --prune
# 前置单必须已合入:
git log origin/staging --oneline | grep -i "api-disk-cache-retention\|BUG-1330" | head -3 # 必须有结果
git merge-base --is-ancestor origin/codex/api-disk-cache-retention-20261013 origin/staging && echo merged
git worktree add -b codex/api-chart-subprocess-gate-20261013 .worktrees/api-chart-subprocess-gate-20261013 origin/staging
cd .worktrees/api-chart-subprocess-gate-20261013
git log -1 --format=%h
grep -n "^## BUG-" docs/BUG_HISTORY.md | tail -3 # 确认 1333、1334 未被占
wc -l scripts/jyotish_api_server.py # 记下开工行数
grep -rn "skip_vedastro_main_entry_overview\|\"/api/chart\"" frontend/src --include=*.ts --include=*.tsx
grep -rln "annual_pyjhora_replay\|_oracle_progress_snapshot\|_resolve_external_pyjhora_python\|acquire_heavy_compute_slot" scripts tests
# Docker 镜像内核实(写进 PROGRESS):
# python -c "import sys,shutil;print(sys.executable, shutil.which('python3'), shutil.which('python3.14'))"
# python -c "import importlib.util as u;print(u.find_spec('jhora'))" # 预期 None
10. 环境缺口(预告)
- 生产装了 vedastro SDK:不带 skip 的
/api/chart在开外网时的真实耗时(上限VEDASTRO_TIMEOUT_SECONDS)本机测不到;合盘改动部署 staging 后,由 Claude 用虚构盘在内网复测一次合盘耗时,或写进docs/testing/清单交产品负责人在页面上计时。 - 闸门「连带 429」的真实频率:T4 上线后看 staging 日志一周再给产品结论。
- 轻重分名额方案(O2/O3/O4):需产品拍板,本单不实施。