← 工作原理 · 原文件 notes/reports/dev/2026-10-03_integration.md(Markdown 源文件已渲染;链接到其他文档的会跳转,指向源码的只显示路径)
2026-10-03 集成:G39 / G49 / G50 / G51 / G52 接入 harness 主流程
把今天几个并行 agent 的产出接进 D 主流程,在 Spark 上跑全量回归、archive 回放和一次 fake 端到端。没有 commit / stash,没有启动正式 run,没有部署 systemd 单元。
0. 结论
- 回归:Spark 全量 58 个测试模块全部通过(
bash scripts/test_on_spark.sh -j 2,最后一次在全部改动之后;集成前的基线是 57/58,失败的是test_proxy_target_leak::KnowledgeCheck::test_repo_passes_strict,现已恢复)。 - 回放(scr-A / B / D 的只读副本):scr-D 终选前 5 从 1 个不同输出变为 5 个不同输出(≥ 3 ✓);scr-B #8 / #11 合并(同时与 #14 合并)✓;scr-D 3 个超时 draft 中 2 个能执行并产出格式合格的预测(≥ 2 ✓),失败族可以再抽 ✓。
- fake 端到端(
search_t1_integration_fake,Spark,假 Engineer / Data agent + 假中继上游):跑完并打包验证通过;relay_cli 文本工具协议、Compliance 删句、Data agent 被拒路径(节点照常继续)、vec-data工具安装、data_epoch、Reviewer 内联材料、--ablate对照、stop_reason、package → verify 全部走通。 - 取舍:两个 agent 改法冲突的地方只有一处需要裁决(§2.3 数据纪元落到选择 / 终选),按“G39 的搜索语义、G52 的数据语义”处理。另有一处为满足 G39 自己的验收,给候选身份加了第三个键(§2.4)。
1. 冲突核对(步骤 1)
| 文件 | 改动方 | 核对 |
|---|---|---|
agent/search/archive.py | G39(same_as_parent、refuted_direction、method_auto、mechanism_active、ablation_json)+ G52(data_epoch) | 两边的列都在同一个 EXTRA_COLUMNS 元组里,走同一条迁移路径(读写打开时 ALTER TABLE,只读打开时取默认值);ablation_json 在 JSON_COLS。insert_imported 带入 G39 的 5 列,不带 data_epoch(纪元是 run 内编号,导入节点在新 run 里为 NULL = 0,重打分时记新 run 的纪元)——这是 G52 的数据语义,保留 |
roles.py | G39(Engineer 停顿计数、strikes)+ G52(single_turn_research / single_turn_datasearch、validate_data_requests) | 互不覆盖;本次再加 G49 的模型覆盖、relay_cli 分派、X-Vec-* 头 |
runenv.py | G39(run_group 停顿计数) | 本次加 vec-data 安装 |
controller.py | G39(主体);G52 / G49 只给了接线清单 | 本次按清单接线 |
| 全部 | — | 改动的 .py 全部 py_compile 通过;agent/search/tests 全部模块在 Spark 通过 |
2. 接线(步骤 2–4)
2.1 G52(15 号文 §7,14 条)
| # | 做了什么 | 位置 |
|---|---|---|
| W1 | load_search_config:cfg["data"] = agent.data.config.normalize(...);角色在原 4 个之外加 compliance(lit-search 或 vec-data 开时,缺省用 reviewer 的模型)和 data(data.fetch 时,缺省用 engineer 的);prompts_by_role 加 compliance_spec.md / data_spec.md(随提示词哈希锁定);compliance_minutes 取自 data 块。data 块新增键 link_auto_admitted(默认 true) | controller.py、agent/data/config.py |
| W2 | 代码快照清单加 agent/data/*.py;data 块(含 fetch_domains)随配置锁定 | SEARCH_CODE_GLOBS |
| W3 | SearchServices.start 启动数据服务(失败 → stop() + SearchError);kill_stale_daemons 加 data.ready;DAEMON_LOGS 加 data_log.jsonl、data_service.log、data_epoch.json、data_reviews、inputs/dynamic;服务配置清单加 data.yaml;打包证据加 data_log.jsonl、data.yaml、data_epoch.json | controller.py |
| W4 | RunEnv.prepare_tools 在 data.enabled 时装 vec-data | runenv.py |
| W5 | build_run_views:每个新建视图后调 dataview.link_auto_admitted(data.link_auto_admitted,默认开);data.enabled 时建 <run_dir>/inputs/dynamic/ | controller.py |
| W6 | research():lit socket 之后调 data_role.researcher_tools,用返回的工具策略,vec-data search 说明追加到生成块;finally 关闭 | controller.py |
| W7 | PLAN 校验通过后调 data_role.compliance_plan;rewrite 时带反馈让 Researcher 重写一次(单独计数,attempt 2 / 3,不占 JSON 无效的重试);第二次仍 rewrite 时用删过的 PLAN,approach 被删空则 gen_failed;结果记进 researcher_context.compliance | controller.py |
| W8 | process_node:PLAN 写入后、Engineer 前,needs_data_agent 时调 run_data_agent;任何异常只记日志、节点照常进 Engineer;llm_calls 加 data / data_jobs,轨迹加 Data agent 流,PLAN 加 _data(入库 id、挂载、纪元前后)并重写 PLAN.json 与 archive | controller.py |
| W9 | _engineer:搜索阶段且 data.enabled 时取 data_role.engineer_data 的 socket、只读目录(inputs/dynamic)、提示行与关闭函数 | controller.py |
| W10 | 搜索阶段开始时起数据审查线程,loop() 结束(终选开始前)和 run() 结束时停 | Controller._run / run / stop_data_worker |
| W11 | process_node 进入 executing 时、execute_node(含种子与崩溃后重执行)、rescore_node 记 data_epoch;evaluate 的结果带 data_epoch,进 seed_scores 的每个种子(晋级与终选的 seed 记录) | controller.py |
| W12 | 选择 / 晋级 / 终选按纪元比较(§2.3);SELECTION.md 写最终纪元,dynamic / auto_admitted 条目用 manifest 里自带的披露文本 | select.py、controller.promote、final.py |
| W13 | review_spec.md 加第 7 条:[compliance: removed] / _compliance 删掉的内容在 METHOD.md 或代码里重新出现判 fail | agent/prompts/review_spec.md |
| W14 | test_on_spark.sh 默认列表本来就自动收所有 test_*.py / *_test.py,新模块(agent.data.test_data、test_compliance、test_model_fallback、test_integration_wiring)已在列表里;脚本头注释写明 | scripts/test_on_spark.sh |
lit-search:现行 9 个 D 模板 research.lit_search.enabled: true、max_queries: 10。
2.2 G49(16 号文 §6,10 条)
| # | 做了什么 |
|---|---|
| 6.1 | load_search_config 用原始 models: 段算 model_fallback.normalize(只在某个角色写了 fallback / by_op 时,且非 fake),ValueError → SearchError;cfg["models"] 仍只放主模型(不用回退的配置 search_hashes["models"] 不变)。model_fallback.ROLES 加 compliance、data |
| 6.2 | make_search_lock 把 models_fallback 从 config 移到锁的顶层(harness.lock_fields 有该键才计入哈希) |
| 6.3 | Controller.__init__ 建 FallbackManager(lock["models_fallback"], run_dir, on_event=ops_event);llm_call 前 select(role, op)(op = 节点算子),后 record;run 熔断器只记成功与整链失败。补充:本次调用失败且链已经切到别的模型时,同一调用立即在新模型上重试一次(失败的流保留为 <role>.fallbackN.jsonl),否则额度耗尽时每个节点都要先丢一次 |
| 6.4 | LLMRunner.call(..., model_override=);CallResult.model 记实际模型;vec-relay-* provider 的 opencode 配置加 options.headers 的 X-Vec-Run/Node/Role/Attempt;stall_seconds 按实际模型取 |
| 6.5 | CredentialMirror 复制 cfg["credential_providers"](链里除 vec-relay-claude-cli 外的全部 provider);sync_back 逐 provider |
| 6.6 | 链里有经 opencode 的 vec-relay-* provider 且网络是 llm_only 时,sandbox.allowed_hosts 必须有 vec-relay.local=127.0.0.1:18790,否则 SearchError |
| 6.7 | ops.probe_models(models, fallback, cfg):链里每个模型都探测,记在 provider_models.chains;vec-relay-* 改为带令牌 GET /healthz 看对应模式 / 上游 |
| 6.8 | 新模块 agent/search/relay_cli.py,framework: relay_cli(只允许 researcher / analyst / reviewer / compliance):宿主机直接 POST /claude-cli/v1/messages。补充:Researcher 开着 lit-search / vec-data search,而订阅路由不接受 tools,所以做了文本工具协议——模型回一个 ``tool 代码块,控制器用本次调用自己的 socket 执行(允许范围与 opencode 的SINGLE_TURN_BASH相同),输出作为下一轮发回,最多 12 轮;流按 opencode 事件格式写,parse_stream/ 看板 / 对账直接可读。Analyst / Reviewer 没有文件工具:Analyst 的材料本来就内联;Reviewer 在 relay_cli(或链里有 relay_cli)时把 solution/、PLAN、视图 manifest、pitfalls 内联进提示(inline_material)。令牌:$VEC_RELAY_TOKEN→ opencode auth.json 的vec-relay-claude-cli/vec-relay-anthropic/vec-relay-bailian→~/.config/vec/relay.token;URL:$VEC_RELAY_URL→ 配置relay.url→http://127.0.0.1:18790。relay_cli 角色不需要 opencode 凭据镜像(_services_and_run` 不再为它们建 mirror) |
| 6.9 | tune.make_llm 用控制器的链(engineer@tune)select / record;report/write.llm_call 加 fallback=、fallback_manager=,vec-relay-claude-cli/* 走订阅路由 |
| 6.10 | 看板 / 对账(calls.jsonl 同步到 Spark)属于部署,未做 |
默认配置(9 个 D 模板:t1_round2/D*.yaml、g24_t2_t3/*_D*.yaml):Researcher / Compliance / Analyst / Reviewer = vec-relay-claude-cli/claude-opus-5-5 + framework: relay_cli,回退 Token Plan 千问;Engineer 仍 alibaba-token-plan-cn/qwen3.8-max(无回退链);Data agent 千问;engineer_draft_model: null(占位:写上模型即给 Engineer 加 by_op.draft 链 [该模型, 千问],需要先加中继别名到 allowed_hosts,加载时会检查)。agent/audit/llm.yaml 默认 relay,fallback_profile: qwen(中继不可用时自动转千问)。
2.3 数据纪元落到选择、晋级、终选(G39 × G52 的取舍)
G52 只给了规则(15 §6),落在 G39 的选择 / 终选代码上,做法:
- 选父节点(
select.compute_u):树里只有一个纪元时与原来逐位一致;有多个纪元时,futs 的排名归一化按纪元分组各做一次(每个纪元内比较)。FUTS 公式、包装层规则都不动(G39 语义)。 - 晋级:前 k 的挑选仍按排名分(G39);被晋级节点的 seed 0 来自旧纪元时,晋级时一并重跑 seed 0,保证 3 seed 均值在同一纪元。
- 终选:终选开始即停审查线程、不再挂载;候选挑选仍按 G39 去重后的排名分;seed 0 来自旧纪元的候选重跑 seed 0;晋级的 seed 1 / 2 只在纪元等于最终纪元时复用;SELECTION.md 写最终纪元。
- 没有 vec-data 的 run(
data.enabled为假):data_epoch为 NULL,上面这些都不触发,行为不变。
2.4 候选身份:第三个键(为满足 G39 自己的验收)
回放发现 scr-B 的 #8 和 #11 代码不同、X3 digest 也不同,但 seed 0 / 1 / 2 分数、四个组分、分量到 4 位小数全部相同(输出只差在评分器分辨率以下,例如浮点噪声)。G39.2 的两个键都合不上它们。加了 candidates.score_twin:两节点都已打分、分数 / 组分 / 分量相等,且至少 2 个共同种子的分数相等(即都晋级过)才算同一候选;只有 seed 0 不够(G39 的测试夹具里大量同分不同输出的行,单 seed 判同会误合并)。另外终选的去重改为在全部排名节点上先做传递分组(candidates.groups,与晋级相同),否则 #14(未晋级,与 #11 同 digest)和 #8 会因为比较顺序都被选上。
2.5 G51 / G50 收尾(步骤 4)
compliance_spec.md第 7、19 行改措辞(T1 禁窗不再写出那个阶段名与区间端点同行;例句“E9.5 的 3 倍”改成“更早阶段的 3 倍”),规则本身不变;scripts/check_knowledge.py --strict-proxy-targetsPASS,test_repo_passes_strict恢复通过。harness.py package:_package→verify(失败则 package 返回 1、不启动审计)→ G50 后台审计 →main里调 G51after_package。测试PackageOrderTest固定了这个顺序。
2.6 协调方追加三项
scoring_def.normalize_ni_guard接受require_gain_on: null(只做非劣检验;此时至少要有一个 noninferior 尺子);noninferiority_verdict、SELECTION.md 与提示里的护栏说明都处理了 null。g40_scale/heart_extrap_s015 / s030 / s045.yaml三个配置可加载(种子只列扫描种子、max_nodes0,未改)。g24_t2_t3的心脏外推配置(t2_heart_extrap_D.yaml、t2_heart_extrap.yaml)改为seeds: [copy_last, damped_shift];全胚插值 D 三个配置改为[copy_last, mix, homolog_pair]。历史配置t2_embryo_interp.yaml(非 D)写成[copy_last, mix],即它当时all的含义,保证可复现——如需也加 homolog_pair 请说。notes/changelogs/experiments.tsv未动。
2.7 现有种子加 --ablate(步骤 5)
| 种子 | --ablate <name> |
|---|---|
| T1 copy_last | 接受并忽略(对照等于自身) |
| T1 ot_moscot | growth → 关掉生长加权重抽样;其它任何名字 → 关掉 OT 位移(只剩生长加权的最后阶段拷贝,也不再解 OT) |
| T1 composition_trend | type_layer → A_TP = 0;cell_layer / maturity / fate → A_CM = A_FATE = 0;其它 → 两层都关(均匀权重分层抽样) |
| T2 mix(心脏、全胚) | 任何名字 → 不插值(只用前一个夹逼输入,与目标不被夹逼时的回退相同) |
| T2 damped_shift | 任何名字 → 不平移(单输入路径,即最后阶段) |
没有改 homolog_pair、scale_only*、chain/、g40_scale/、t3 配置。注意:种子代码变了,护栏基线 baseline_sha256 与评测指纹随之变化;续跑时旧 run 的种子节点不会被复用(会插入新种子),这是预期的。
2.8 规则文本(步骤 6)与 10 号文(步骤 7)
data/external/SPARK_EXCLUDE头注释与 11 号文 §3 改为:下载不违规、使用才违规;外部数据可下载到 Spark 隔离区,第一步按任务删窗(data.post_filter默认 true),原始下载随即删除,只挂删窗后经 Compliance 放行的派生文件;post_filter: false保留旧严格做法。排除的两个 CELLxGENE 原始文件仍不同步(Spark 上不需要)。decisions.tsv追加一行(决定者:用户 2026-10-03 grilling Q9、Q18;依据 15 号文)。15 号文 §4 的“请用户确认”标为已定,15 / 16 / 17 号文 status 更新。- 10 号文:开头一句话、§3 流程图(重画:Compliance、Data agent、relay_cli、数据纪元、平台期、审计门、通知、stop_reason)、§3 说明、§4.3(候选身份、已证否方向、按纪元排名)、§4.5(relay_cli、lit-search 开、vec-data search、Compliance)、§4.6(不设时限、停顿计数、METHOD 占位、数据 socket)、§4.7(tune.p 0)、§4.8(
--ablate对照、data_epoch、去重晋级、终选纪元)、§4.9(package → verify → 审计 → 通知、审计门)、§4.11(模型表、中继、回退链)、§8(run 内数据与规则改写)、§9(通知、外层循环);§11 历史只追加一条,原有历史保留。
3. 改动文件
新建:
agent/search/relay_cli.pyagent/search/tests/test_integration_wiring.py(17 个用例:relay_cli 工具协议与头、额度 / 不可达分类、模型覆盖、llm_call 回退重试、模板、加载与锁、draft 占位与别名检查、按纪元排名、score_twin、无增益护栏、g40 配置加载、凭据镜像、package 顺序、链探测、种子--ablate)agent/search/tests/fake_relay_server.py(假中继上游,测试用)agent/configs/dev/search_t1_integration_fake.yaml(fake 端到端配置)- 本报告
修改:
- 代码:
agent/search/controller.py、roles.py、runenv.py、select.py、final.py、candidates.py、model_fallback.py、ops.py、tune.py、scoring_def.py、context.py、agent/data/config.py、agent/harness.py、agent/report/write.py、agent/audit/llm.py、agent/audit/llm.yaml - 种子:
agent/seeds/T1__val/{copy_last,ot_moscot,composition_trend}/run.py、T2__embryo__val_interp/mix/run.py、T2__heart__val_interp/mix/run.py、T2__heart__val_extrap/damped_shift/run.py - 配置:
agent/configs/experiments/t1_round2/D.yaml、D_s1.yaml;g24_t2_t3/t2_embryo_interp_D{,_s1,_s2}.yaml、t2_heart_interp_D{,_s1,_s2}.yaml、t2_heart_extrap_D.yaml(模型、lit-search、data 块、draft 占位、种子列表);g24_t2_t3/t2_heart_extrap.yaml、t2_embryo_interp.yaml(只改种子列表) - 提示词:
agent/prompts/review_spec.md、compliance_spec.md - 规则与文档:
data/external/SPARK_EXCLUDE、notes/competition/11_data_use_scope.md、notes/changelogs/decisions.tsv、notes/architecture/10_harness_era_architecture.md、15_data_agent_compliance.md、16_llm_relay_fallback.md、17_meta_and_audit_agents.md - 脚本:
scripts/test_on_spark.sh(只改注释)
4. 回归结果
bash scripts/test_on_spark.sh -j 2(Spark,vec.slice,2 并发):
| 轮次 | 结果 |
|---|---|
| 基线(集成前的工作树) | 57 / 58;失败 test_proxy_target_leak(strict 检查:compliance_spec 两行 + homolog_pair README 一行;README 那行之后已被另一 agent 改掉) |
| 接线后第一轮 | 56 / 58;test_codex_engineer(Codex 模型名无 provider 前缀也被送进回退规范化)、test_score_parts(裸 Controller 没有 cfg)——都已修 |
| 最终(全部改动之后) | 58 / 58 通过,0 失败 |
此外 G39 改动后定向复跑:test_g39_search_fixes、test_integration_wiring、test_continuation_promotion、test_ni_guard、test_x3_guard、test_run_correctness 全部通过。
5. 回放(Spark,只读副本 ~/vec/scratch/integration_replay/)
只复制了 archive.sqlite(-wal)、programs.git、三个超时 draft 的节点目录和 scoring_log;分析进程 1 个、BLAS 2 线程、nice。(第一次查看 scr-D 的 draft 行时直接以 mode=ro 只读打开过一次原件 archive,没有写入;之后全部在副本上做。)
| 验收 | 结果 |
|---|---|
| scr-D 终选前 5 ≥ 3 个不同程序 | 旧:#22、25、29、31、32(代码树各不相同,但 5 个是同一个输出,一组)。新:#22、21、23、20、13,5 个不同输出、5 个不同代码树 ✓ |
| scr-B #8 / #11 合并 | 旧前 5 同时有 #8、#11(分数 60.507 完全相同)。新:#8、#11、#14 为同一候选(#11 ~ #14 同 digest,#8 ~ #11 三个种子分数全同),前 5 = #14、17、10、12、19 ✓ |
| scr-A(对照) | 旧前 5 里 #19、20、22、17 是同一输出;新前 5 = #19、14、12、13、7,5 个不同输出 |
| scr-D 3 个超时 draft 中至少 2 个能打分 | 从 engineer.jsonl 的 write / edit 事件重建 solution/run.py(三个都在、编辑全部可应用),在 scr-D 的 X3 视图上执行:#6(ot_cfm)13 s、#28(graph_fate)16 s 正常结束,预测 652 个细胞(X3 视图上限 652,格式合格);#18(manifold_ode)崩溃(torch.pca_lowrank 不接受 generator)。新规则下三个都会执行(占位 METHOD + run.py 存在),2 个可打分、1 个记 crash ✓。#6 会话中的 vec-score 也多次成功(最后一次 48.05) |
| 覆盖报告里失败族可再抽 | 失败 draft 计数:ot_cfm 1、manifold_ode 1、graph_fate 1(都 < failed_cap 2);新规则下的下一次 draft 抽到 ot_cfm(“1 failed draft(s) before”)✓ |
| 晋级去重 | 三个 run 的晋级都已做完,promotion_candidates 新旧都没有待晋级节点(scr-B 新逻辑下多出 #19:去重后前 5 里还没晋级的节点) |
注意:旧 archive 的 digest 只有 X3(视图盲检查用的尺子),按输出判同一候选只能看 X3。
6. fake 端到端(Spark,~/vec/runs/test/20261003-151511-search-t1-integration-fake)
配置 agent/configs/dev/search_t1_integration_fake.yaml:Engineer / Data agent 用 fake_llm;Researcher / Compliance / Analyst / Reviewer 用 relay_cli 指向假中继(agent/search/tests/fake_relay_server.py,127.0.0.1:18791,令牌经 VEC_RELAY_TOKEN);lit-search、vec-data(search + fetch)、plateau、ablation、tune 0、engineer_stall 全开;max_nodes 14。
- 结束:
state: finished,stop_reason: budget(max_nodes),终选 node 2(3 seed 均值 48.61),harness.py package→ verify 全部 ok(fake run 不跑审计、不发邮件)。 - launch 时链探测:4 个 relay_cli 角色经
/healthz报 ok。 - relay_cli:每个 Researcher 调用 3 轮(第 1 轮回 ``
tool:lit-search sources、vec-data status,控制器执行后发回,第 2 轮出 PLAN),X-Vec-*` 头都到了假中继。 - Compliance:每个 PLAN 的 “E10.5 心脏中心肌占 28%(FAKE_MEASURED)” 都被删成
[compliance: removed],_compliance记 1/2 句。 - Data agent:node 4 / 8 的
data_requests(不存在的 GSE999999999)→ 服务在元数据阶段拒绝(Spark 到 NCBI 可达,返回“no samples in the metadata”),DATA_REVIEW.json记录,节点照常进 Engineer。 vec-data在 run 的工具目录;data_epoch在所有执行过的节点上为 0(gen_empty / gen_failed 为 NULL);SELECTION.md 写了 “Final data epoch: 0”。- Reviewer:走 relay_cli,提示含内联材料(
solution/run.py等),判 pass。 --ablate:fake Engineer 的程序不接受该参数,全部记 unclear(符合契约)。- 没有覆盖到:真实入库 / 挂载 / 纪元 +1(需要真实下载,单元测试
agent.data.test_data用假 HTTP 覆盖了 admit → mount → 纪元);回退链(fake run 不建链,单元测试覆盖)。
7. 遗留
- 中继未部署(用户做):
agent/relay/install.sh(VM)与spark_setup.sh --apply。relay_cli 的令牌查找会用 opencode auth.json 里的vec-relay-anthropic条目,所以 spark_setup 现状即可;看板对账(6.10)的 calls.jsonl 同步未做。中继不通时,单轮角色按回退链在 5 次连续失败后切千问(额度类错误立即切);正式 run 前建议先/healthz确认。 - 真实 Opus 是否守文本工具协议没验证过;
researcher_spec.md里仍写的是 bash 形式的lit-search用法,relay_cli 会在提示末尾追加协议说明。第一次真 run 看researcher.jsonl的tool_use事件数。 - D 模板默认
data.fetch: true:下一次正式 run 会真实下载(Spark 上各仓库 API 解析器仍未用真数据验证,见 15 §8)。若想先只开元数据检索,改fetch: false。 - 数据服务的 ping 不返回纪元,launch 日志打印 “epoch None”(只是显示)。
- Engineer 会话中
vec-data fetch --purpose的入库由审查线程处理,Data agent 会话后的decide由run_data_agent处理;同一个 job 若两边都碰到,第二次decide返回错误并记在行里(不会重复入库)。 score_twin只在两节点都有 ≥ 2 个种子时生效:未晋级的“只差浮点噪声”的孪生节点仍可能各占一个终选位(由视图盲检查与终选的 3 seed 阶段兜底)。- 选父节点按纪元分组排名时,新纪元只有一两个节点时它们的 R 会偏高(倾向于先展开新纪元),属于设计取舍,等真的发生挂载后再看。
- G39 遗留照旧:method_auto 节点后来才进入前 k 名时要到终选才审查;
plateau作为 stop_reason 仍未产生;提示词侧的NO_CHANGE:/--ablate写法 /queries_before_refute由提示词 agent 负责。 engineer_draft_model是占位,API key 模式启用后需要同时把vec-relay.local=127.0.0.1:18790加进sandbox.allowed_hosts(加载时会检查并报错)。- 回放副本与 fake run 留在 Spark:
~/vec/scratch/integration_replay/、~/vec/scratch/integration_e2e/、~/vec/runs/test/20261003-151511-search-t1-integration-fake,可删。