Files
Jyotisha/docs/tasks/TASK-consultation-external-evidence-cache-20260915.md
T
Jesse_ChenandClaude Opus 5 e4788dfc00 docs(tasks): 换掉两条增长冻结口径 + page.tsx 状态下沉第一簇 + C1 提前
产品 2026-09-15 三项拍板,落成两份新单与两处既有单的修订:

- 新增 TASK-freeze-metric-change-20260915(无 BUG 号,后面两单的前置):
  两条冻结余量已用完(page.tsx 1951/1951 余 0;api server 11334/11363 余 29),
  冻结从「逼新代码往外走」退化成拦路。实证:page.tsx 行数砍 59% 但 Home()
  的 useState 从 56 涨到 66;api server 225 个类方法只有 12 处真碰 HTTP。
  主门换成耦合指标,行数降为粗护栏;同时推翻 §6「参数式 hook 内部保持
  0 个 React hook」——那正是状态搬不走的原因。
- 新增 TASK-home-state-lowering-20260915(无 BUG 号):先搬 rectification*
  那 15 个 state 进已经是 dynamic 子树的校正面,Home() useState 66 → ≤53。
  零行为变化;串行在 freeze-metric-change + C2 + R3 之后。
- 修订 TASK-consultation-external-evidence-cache-20260915:依赖反转,C1 排在
  API server 拆解之前(它动模块级函数,拆解动类方法);补「不得新增类方法、
  行数余量仅 29」的硬红线。
- 修订 TASK-api-server-decomposition-20260916:串行依赖加 C1 与
  freeze-metric-change;__new__ 计数按 grep 的 4 计(原文 3 是文件数);
  阶段 4 收尾口径改写;基线 11,314 → 11,334。

纯文档推送,不触发门禁、不发布镜像、不部署。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
2026-09-15 23:15:37 +00:00

14 KiB
Raw Blame History

TASK · 普通聊天每一轮都在同步等外网,本地计算只占 3%

  • 日期:2026-09-15
  • 基线 commitorigin/staging @ 6b3248bf
  • 执行分支:codex/consultation-external-evidence-cache-20260915
  • 主要落点:scripts/jyotish_api_server.pyscripts/vedastro_service_adapter.pyscripts/vedastro_user_entrypoint.py
  • 依赖已于 2026-09-15 反转:本单排在 API server 拆解单之前。 原文写的是「串行在 TASK-api-server-decomposition-20260916 之后」,产品拍板改为本单先做——理由见 §3.7,拆解单已同步改为串行在本单之后
  • 与 context-memory 单、session-capacity 单无文件重叠,可并行

1. 事故实证

普通聊天每发一条消息,每个问题域调一次 POST /api/consultation_workflow。本机(Python 3.13 + swisseph)热身后实测:

问题域 第 1 次 第 2 次(同参数) 响应体积
career 928 ms 865 ms 523 k 字符
wealth 493 ms 490 ms 523 k 字符
marriage 493 ms 492 ms 518 k 字符
general 491 ms 493 ms 542 k 字符
timing 523 ms 507 ms 550 k 字符

cProfile 按 tottime 排前五名:

耗时
select.poll(等外网 socket 0.801 s
_ssl._SSLSocket.read 0.786 s
_ssl._SSLSocket.do_handshake 0.206 s
swisseph.calc_ut(本地排盘) 0.022 s
copy.deepcopy 0.048 s

前三名全部来自 _join_foreground_vedastro_run_foreground_vedastro_gatewayvedastro_service_adapterurlopen,目标是 api.vedastro.org,而且每次重新 TLS 握手。本地占星计算连 3% 都不到。

三个护栏数字凑不到一起,符号定位:

位置 含义
jyotish_api_server.py _foreground_vedastro_join_seconds 1.5 s(上限 3 s 前台最多等多久
jyotish_api_server.py _foreground_vedastro_budget_seconds 8 s212 s 后台任务自己跑多久
jyotish_api_server.py _FOREGROUND_VEDASTRO_WORKERS 2 整个进程的前台线程池

_join_foreground_vedastro 超时走 FuturesTimeoutError 分支返回 official_blocked但不 cancel() 那个 future,任务继续跑满 8 秒预算。于是:只要平均每 4 秒有一轮聊天,2 个 worker 就永远被占着,之后每一轮都白等 1.5 秒再拿到 official_blocked——用户付出了等待,拿不到证据,技法表上那一层还是 blocked。

另有一条纯浪费:响应体 52 万字符里 western_evidence_packet122 KB,全仓检索前端零读取点grep -rn "western_evidence_packet" frontend/src 无命中)。算出来、序列化、走 HTTP、在 Node 里 JSON.parse 一遍,然后丢掉。三个域就是 366 KB 的无效解析,全压在 2 vCPU 上。其余键体积:chart 238 KB、consumer_context 43 KB、rectification 23 KB、vedastro_gateway 21 KB。

2. 根因

这份外网证据本来就是按「盘 + 日期」组织的,只是没有按这个键缓存过:

  • vedastro_service_adapter._official_snapshot_reference_date() 在 case 里找不到 reference_date / today / transit_date / current_date 时,缺省取 UTC 当天
  • consultationInputSchemafrontend/src/mastra/consultation-workflow.ts没有任何日期字段,所以普通聊天永远走缺省。
  • _official_dasha_range_body() 把大运区间取成该日期所在自然年的 1 月 1 日到 12 月 31 日

也就是说:本命部分永不变,随时间变的部分粒度是天/年。同一个人一天里问十轮,十次拿到的是同一份东西,却打了十次外网。

3. 决策记录

产品 2026-09-15 授权本单,并明确以下口径:

  1. 不许「干脆不调 VedAstro」。 那正是 BUG-301 修掉的问题(前台跳过后技法表里 VedAstro 云状态永远 blocked)。本单的目标是留住证据、去掉等待,不是二选一。
  2. 缓存键沿用引擎内部已有的那一个:出生数据 + 岁差 + 交点 + UTC 日期。产品明确否决了「自己定一个 N 小时 TTL」——那会和 _official_snapshot_reference_date 的日期键错位,跨 UTC 零点时以为还新鲜、内容其实已经该换。
  3. 新鲜度分档(产品定):
    • 同一天命中 → 直接用,0 等待
    • 跨天了 → 先用旧的(上限 7 天)立刻返回,同时后台刷新,下一轮就是新的。
    • entrypoint = daily_starlanguage(「深入看今日」)→ 要求当天,没有当天的不吃旧的,走下一档。
    • 完全没有缓存 → 才走现在那套 1.5 秒有界等待。
  4. 后台任务必须可取消、线程池必须可排队。 现在「超时不 cancel + 只有 2 个 worker」的组合是本单必须解决的部分,不是顺带。
  5. western_evidence_packet 按需返回,不删计算。 西洋盘本身仍是 must-use 层之一(被读的是别的字段),本单只是不再把这个整包塞进每一轮的响应。
  6. 不动并发闸门(默认 2)、不动 AGENT_TIMEOUT_MS、不动 maxDuration
  7. 本单排在 API server 拆解之前2026-09-15 产品拍板,推翻本任务书首版的排序)。依据:本单要改的 execute_consultation_workflow_join_foreground_vedastro、那三个常量全是模块级函数,而拆解单的核心是把业务逻辑搬出 JyotishAPIHandler ,两边动的是文件的不同部分;且本单的用户价值(每一轮省掉一次外网等待)远高于一次纯搬运。代价是拆解单将来要吸收本单的 diff,已在那一单里写明。

4. 硬红线

  1. 缓存键必须与 _api_chart_cache 的键分开(BUG-161 防复发原文:「快速排盘与完整证据排盘必须使用不同缓存键」)。不得复用同一个目录或同一个 key 函数。
  2. 赶不上外网时,不得把未交付的官方层标成 executed(BUG-301 防复发)。用旧缓存时,证据里必须能看出它是哪一天的。
  3. 缓存内容里不得落盘姓名、邮箱、用户 ID;出生资料派生值只能以哈希进键,不得明文写进缓存文件名(AGENTS §8)。
  4. 前台不得再同步串联 overview + snapshot + range scanBUG-301 防复发,现状已满足,不得回退)。
  5. scripts/jyotish_api_server.py 不得增长(AGENTS §6):新逻辑进 scripts/ 下的独立模块,主文件只做薄注册。具体到数字:开工时实测 11,334 行、上限 11,363余量只有 29 行JyotishAPIHandler 有 225 个方法,本单不得新增任何类方法。 若 TASK-freeze-metric-change-20260915 已经合入,主门改为「类方法数不得增长」,行数粗护栏放宽——以开工当时生效的合同测试为准,但「不新增类方法、新逻辑进独立模块」这条无论口径怎么换都成立。
  6. 不得顺手升级依赖、不得顺手修不在本单里的 warning。

5. 任务分解

5.1 外网证据按「盘 + UTC 日期」缓存

新增一个独立模块(例如 scripts/vedastro_snapshot_cache.py),提供读/写/新鲜度判定:

  • 键:sha256(出生年月日时分秒 + lat + lon + tz + ayanamsa_policy + node_policy + reference_date)

  • 落盘位置与 _api_chart_cache_dir() 不同目录TTL 由 reference_date 与「最多 7 天」共同决定,不再引入第二个 TTL 环境变量。

  • execute_consultation_workflow 里:命中当天 → 直接用,完全不 submit 后台任务;命中 1–7 天前 → 直接用旧的并 submit 一次后台刷新(刷新结果只写缓存,不参与本轮);entrypoint == "daily_starlanguage" 时只接受当天;全未命中 → 现有 1.5 秒有界路径,拿到结果后写缓存。

  • 验收:新增 pytest——同一份 body 连调两次,第二次零外网调用(用 stub/monkeypatch 计数 urlopen 或适配器入口),且两次返回的 vedastro_gateway 逐字相等。

  • 验收:把 reference_date 推到第二天,断言缓存不再命中当天档、走「先用旧的 + 后台刷新」,且本轮不等待

  • 验收:daily_starlanguage 入口在只有昨天缓存时不得使用它。

  • 验收:缓存目录与 _api_chart_cache_dir() 不同,且有一条断言钉死这一点。

5.2 后台任务可取消 + 线程池可排队

_join_foreground_vedastro 超时时必须 future.cancel();已经在跑的任务要能被 temporary_timeout_seconds 之外的显式取消标记打断,或者把预算从 8 秒收到不超过 join 的两倍并说明理由。线程池大小与 join/budget 三个数必须在同一处成组声明,并写明它们的关系(照 consultation-tools.ts 里那段预算注释的写法)。

  • 验收:新增测试——连续发起超过 _FOREGROUND_VEDASTRO_WORKERS 个前台请求,断言第 N+1 个的等待时间仍不超过 join 上限,且不会因为前面的任务没结束而排队变长。
  • 验收:源码合同断言三个数在同一个声明块里,且 budget ≤ k × joink 取实现选定的常量)。

5.3 western_evidence_packet 按需返回

默认不放进 /api/consultation_workflow 的响应;需要它的调用方(个人报告、高严谨工作流,若确有读取点)显式请求。开工第一步先把真实读取点查清楚,查到的写进进度记录,没查到的按「无人读」处理。

  • 验收:grep -rn "western_evidence_packet" frontend/src scripts/ tests/ 的结果写进进度记录,逐个说明保留或去掉。
  • 验收:改后响应体积实测下降幅度写进进度记录(改前 523 k 字符是本单的基线数)。
  • 验收:consultationWorkflowResponseSchema 仍能解析(它是 .passthrough(),去掉一个键不应报错——但必须有测试证明,不能靠推断)。

5.4 在 staging 量一次真实的单域耗时

frontend/src/mastra/consultation-tools.ts 的预算注释写明:域上限 3 是按「staging 实测三域共 62.9 秒」反推的,即一域约 21 秒。本机同样的调用只要 0.5 秒,差 40 倍。这单交付后必须在 staging 上重新量一次:

  • 交付一份可照做的清单进 docs/testing/,内容是:在 staging 上对同一张盘连发两轮同域提问,记录两轮的 consultationToolDurationMs(第一轮冷、第二轮应命中缓存)。

  • 结论写进进度记录:如果单域耗时已显著低于 21 秒,明确写出「三域上限可以放宽到 N」的依据,但本单不改那个上限——放宽是产品决策,另开单。

  • 验收:docs/testing/ 下有这份清单;进度记录里有改前/改后的实测两组数字,或明确写成环境缺口(无 staging 访问权时)。

5.5 两条 Bug 历史

同一变更内写进 docs/BUG_HISTORY.md

  • BUG-727:普通聊天每轮每域同步等外网,本地计算只占 3%;超时不取消 + 2 个 worker 让线程池长期饱和。关联 BUG-161、BUG-301、BUG-718,并写清楚:BUG-301 是故意把 VedAstro 放回前台的(否则技法表永远 blocked),所以本单不是推翻它,而是用缓存同时满足 161 和 301。防复发写成:前台外部证据必须有「盘 + 日期」级缓存;任何有界等待都必须同时取消它等待的后台任务。
  • BUG-728western_evidence_packet 无人读却每轮每域传 122 KB。防复发写成:工作流响应新增大字段前必须有读取点;没有读取点的字段不得进入前台响应。

6. 让步顺序

  1. 5.1(缓存)必须做,它是本单的全部意义。
  2. 5.2(可取消 + 可排队)必须做——只加缓存不修线程池,冷启动和跨天刷新仍会把池子占死。
  3. 5.3 可以砍到下一轮,砍了在进度记录里写明。
  4. 5.4 不得砍,但允许写成环境缺口(没有 staging 访问权时,把清单交出来即可)。
  5. 5.5 不得砍。

7. 开工前置命令

git fetch origin --prune
# 本单不再等 api-server-decomposition;直接以 origin/staging 为基线
git log --oneline origin/staging | head -5
git worktree add -b codex/consultation-external-evidence-cache-20260915 \
  .worktrees/consultation-external-evidence-cache-20260915 origin/staging
cd .worktrees/consultation-external-evidence-cache-20260915
git status -sb | head -1
python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45   # AGENTS §9:本单涉及外部 oracle

开工前必读:docs/research/pre_work_error_ledger.mdAGENTS §9),以及 docs/testing/vedastro-runtime-20260915.mdBUG-719/720 的运行期真相,与本单同一外部服务)。

验收命令:

.venv/bin/python -m pytest tests/test_api_server_security.py \
  tests/test_consultation_consumer_context.py \
  tests/test_vedastro_runtime_ops.py
.venv/bin/python scripts/run_quality_gate.py --profile quick
cd frontend && npx tsx --test tests/consultation-*.test.ts

8. BUG 编号起点

基线 6b3248bf 上最大号 BUG-7202026-09-15 校正四单已预占 721726。本单预占 BUG-727 / 728。开工时核对当时的实际最大号,冲突顺延并在进度记录写明。

9. 不在本单范围

  • JyotishAPIHandler 的拆解本身(本单只做薄注册,拆解排在本单之后)
  • 三域上限 3 → N 的放宽(5.4 只负责量数据,放宽是产品决策)
  • 并发闸门、AGENT_TIMEOUT_MSmaxDuration
  • reference_date 缺省用 UTC 当天、而用户的「今天」是 UTC+8,早上八点前后会错开一天——记为观察项,本单不修
  • 对话记忆与上限(见另外两单)