已合并
docs: 文档重构支持项目记忆管理。文档三分重组——design/spec/feature 下沉模块根,spec 按子包拆分 #439
docs: 文档重构支持项目记忆管理。文档三分重组——design/spec/feature 下沉模块根,spec 按子包拆分 #439
已合并
王明琦创建于 19 天前
15 个文件变更+588-218
@@ -0,0 +1,7 @@
1+# CLAUDE.md(根)
2+ 
3+多模块嵌套 git 仓库(独立于外层 jiuwenclaw,提交分开做)。
4+ 
5+核心模块 agent_runtime(会话编排服务,M0–M8 已完成,维护期):一切开发指引见 `applications/agent_runtime/CLAUDE.md`(红线规则/测试/部署/文档体系,先读)。
6+ 
7+其余内容:`service/`(openjiuwen_runtime 服务框架:App/Envelope/SystemContext)、`applications/echo`(App 范式参考)、`docs/{zh,en}/`(平台用户文档,与 agent_runtime 服务无关)。
@@ -0,0 +1,65 @@
1+# CLAUDE.md
2+ 
3+本仓库实现**会话编排服务**(旁路式:gateway 直连 AgentServer Pod,服务只做控制面)。**M0–M6 已全部完成**(含 server 模式真环境验收),维护期。
4+ 
5+## 文档体系与开发前必读(路径均相对本模块根)
6+ 
7+文档三分:`docs/design/`(设计论证与 Lua 全文)、`docs/spec/`(**模块规格,AI 向**:代码在哪/怎么协作/改哪里)、`docs/feature/`(改动史)。
8+ 
9+**文档同步义务(与代码改动同一提交完成):**
10+- 改动涉及 spec 覆盖的内容(模块行为 / Redis 键 / Lua / 接口 / 配置 / 错误码)→ **同步更新对应 spec 文档**(`docs/spec/` 按模块对号入座)。
11+- **较大改动**(新功能 / 行为变化 / 重构 / 里程碑)→ 按 `docs/feature/_TEMPLATE.md` 新建一份记录并登记其 README 索引;**小的修复(局部 bugfix、注释/文案)不用**
12+ 
13+1. `docs/spec/README.md` —— spec 索引 + 一页纸架构(先读)
14+2. `docs/spec/session-manager.md` / `docs/spec/resource-manager.md` / `docs/spec/service-core.md` —— 三模块规格(改代码前读对应模块);e2e 用例逐条说明见 `docs/spec/e2e-test-cases.md`(场景/输入/预期输出)
15+3. `docs/design/Agent-Runtime-HLD.md` —— 架构总览 / 接口契约 / 场景 A–N / Redis 键表(**语义权威,冲突以它为准**;§9 实现与验收状态)
16+4. `docs/design/session-manager-design.md` —— SM 详细设计(7 个 Lua 全文)
17+5. `docs/design/resource-manager-design.md` —— RM 详细设计(6 个 Lua 全文)
18+ 
19+## 红线规则(违反即返工)
20+ 
21+ 
22+## 测试
23+ 
24+```bash
25+cd applications/agent_runtime
26+uv sync --extra local
27+uv run pytest # 114 个用例:状态层 Lua / config 层 / 组件全链路 / HTTP 冒烟 / corner case / 双实例多副本
28+```
29+ 
30+- 构造 `ServiceManager` 必须传 `deploy_mode="subprocess"`(默认 k8s 会挂死测试)。
31+- fakeredis 陷阱:消费组 id=`"0"` bug、pubsub 需共享 FakeServer、EVAL 内 PUBLISH 需实测。
32+- 双实例测试(`tests/integration/test_multi_replica.py` + `_dual_harness.py`):同进程两 App 共享一组 fakeredis/SQLite/FakeK8s,httpx ASGITransport 单事件循环驱动;lifespan 必须先手动驱动(否则 RestAdapter 惰性二建 sysctx 绕过后台 Job)。
33+- 旧 SDK 已知失败用例(非回归)见外层 jiuwenclaw 仓库 CLAUDE.md 末尾清单。
34+ 
35+### 集成冒烟(真环境,部署后回归)
36+ 
37+```bash
38+cd applications/agent_runtime
39+./scripts/integration_smoke.sh # HLD 场景 A–L 端到端;会 FLUSHDB 目标 Redis DB(有防误刷保护)
40+```
41+ 
42+- 参数/前置见 `--help` 与 README;场景 N 待 AgentServer 支持 `GET /health` 后补验。
43+- 经多副本 LB 亦可跑(实测 65/65),前提:部署带 `AGENT_RUNTIME_SCOPE_FULL_TIMEOUT`(deploy 模板默认 8,**须显著小于模板 session_ttl**,否则等待者 deadline 与会话到期碰撞产生混合结果);排查实录见 `docs/spec/e2e-test-cases.md` §8.1。
44+- cleanup 空目标必须用**无匹配 label_selector**,不得指向业务 ns(同 label 真实 AgentServer 会被误删)或不存在的 ns(in-cluster SA 对其 403 而非空列表)。
45+ 
46+### 多副本(真环境)
47+ 
48+```bash
49+# 宿主机双进程(快速):8091/8092 共享 Redis/DB,选主键互斥竞争
50+./scripts/deploy_replicas.sh 2 .env.production.local 8091
51+# K8s 多副本 + Service LB(生产形态):deploy/ 目录(模板+Dockerfile+渲染部署)
52+./deploy/render_and_apply.sh deploy/agent_runtime.env --nodeport
53+# 多副本 e2e(真 LB 单入口,含 failover;单实例自动 DEGRADED)
54+uv run --no-sync python scripts/e2e_multi_replica.py --base-url http://127.0.0.1:30091/api/session \
55+ --redis-url redis://127.0.0.1:30001/2 --namespace default
56+# 压测/浸泡(零依赖,场景化;无 FLUSHDB、不动 cleanup 端点)
57+uv run --no-sync python scripts/load_test.py --base-url http://127.0.0.1:30091/api/session --duration 60
58+```
59+ 
60+## 环境
61+ 
62+- Python 3.11–3.13;Redis 须开 AOF/RDB;DB 用 MySQL/PostgreSQL(禁 SQLite 回退——server 模式;local 模式调试可用)。
63+- 框架:`service/openjiuwen_runtime/service`(App/Envelope/SystemContext);App 范式参考 `applications/echo/echo_server.py`(**不是** a2a_service)。
64+- 部署:`applications/agent_runtime/scripts/deploy.sh local|server`(server 读 `.env.production.local`)。
65+- 本仓库是嵌套 git 仓库(独立于外层 jiuwenclaw),提交分开做。
@@ -10,7 +10,8 @@
10 DB(`service_config_template` / `routing_rule` 表);跨模块只走 Facade,10 DB(`service_config_template` / `routing_rule` 表);跨模块只走 Facade,
11 不直读对方 Redis key。11 不直读对方 Redis key。
12- 设计文档:`docs/design/`(语义权威 = HLD;冲突以 HLD 为准)。12- 设计文档:`docs/design/`(语义权威 = HLD;冲突以 HLD 为准)。
13-- 代码说明:`docs/agent-runtime-code-guide.md`(模块结构 / 关键流程 / Lua 清单 / 测试与部署)。13+- 模块规格(AI 向):`docs/spec/`(README 索引 / service-core / session-manager / resource-manager;代码在哪、怎么协作、改哪里)。
14+- 改动史:`docs/feature/`(每次改动一份文档,见其 README 写作规范)。
14 15 
15## 运行16## 运行
16 17 
@@ -27,7 +28,7 @@ cp agent_runtime.server.env.example .env.production.local
27 28 
28## 测试29## 测试
29 30 
30-> 全部 e2e 用例(场景/输入/预期输出)逐条说明:`docs/e2e-test-cases.md`。31+> 全部 e2e 用例(场景/输入/预期输出)逐条说明:`docs/spec/e2e-test-cases.md`。
31 32 
32```bash33```bash
33cd applications/agent_runtime34cd applications/agent_runtime
@@ -53,7 +54,7 @@ cd applications/agent_runtime
53- 经多副本 LB 亦可跑(实测 65/65)——前提:部署带54- 经多副本 LB 亦可跑(实测 65/65)——前提:部署带
54 `AGENT_RUNTIME_SCOPE_FULL_TIMEOUT`(deploy 模板已默认 8,须显著小于模板55 `AGENT_RUNTIME_SCOPE_FULL_TIMEOUT`(deploy 模板已默认 8,须显著小于模板
55 session_ttl,否则等待者 deadline 与会话到期碰撞产生混合结果)。56 session_ttl,否则等待者 deadline 与会话到期碰撞产生混合结果)。
56- 排查实录与 cleanup 空目标的三个坑见 `docs/e2e-test-cases.md` §8.1。57+ 排查实录与 cleanup 空目标的三个坑见 `docs/spec/e2e-test-cases.md` §8.1。
57 58 
58## 多副本部署与测试(M7)59## 多副本部署与测试(M7)
59 60 
Rdocs/design/Agent-Runtime-HLD.mdapplications/agent_runtime/docs/design/Agent-Runtime-HLD.md+0-0
文件重命名但无更改。
Rdocs/design/resource-manager-design.mdapplications/agent_runtime/docs/design/resource-manager-design.md+0-0
文件重命名但无更改。
Rdocs/design/session-manager-design.mdapplications/agent_runtime/docs/design/session-manager-design.md+0-0
文件重命名但无更改。
@@ -0,0 +1,43 @@
1+# M8:deploy 锁输家改为 follower 等待室——跨副本冷竞争零多余 Pod
2+ 
3+- 日期:2026-08-24
4+- 里程碑 / commit:M8 / `58fc3301`
5+- 涉及模块:resource_manager(acquire 链路)、session_manager(models.pool_config)、双实例测试
6+ 
7+## 背景与动机
8+ 
9+多副本部署下,冷突发(多个 route 同时到达、scope 无 Pod)会发生跨副本冷竞争:多副本同时进 acquire 的 need_deploy 分支,抢 `lock:rm:deploy:{scope}` 选主。
10+ 
11+改前的输家行为:清占位后短暂自旋(0.3s)重跑 ACQUIRE,等锁空闲后**自己再 deploy 一个 Pod**。后果:
12+ 
13+- 输家自建的第 2 个 Pod 在 `max_pods` 内合法,但大概率是空 Pod,要经 SM 空 Pod pass → idle_consider → reclaim 链路自愈——资源浪费,冷突发尾延迟高(实测 30.5s);
14+- 该问题作为开放问题记录在(重组时已删除的)开发交接文档 §十一.1 与 `docs/spec/e2e-test-cases.md` §8.2,本次关闭。
15+ 
16+## 方案(定案,与需求方逐条确认)
17+ 
18+- **等待有界**:follower 等待上界 = `ready_timeout + 10s` 余量(注册开销)。
19+- **leader 失败,follower 不接管、直接失败**:同镜像同环境下 follower 接管大概率也失败,接管只会放大故障;失败判定 = deploy 锁空闲且无新 Pod 注册进展。
20+- **等待室在 Redis**(ZSET + deadline score),准入原子;**防崩溃泄漏**`ZREMRANGEBYSCORE(deadline)` 兜底——裸 SET 只靠 finally 出队,进程崩溃即泄漏(SM waiter 集合的既有教训,这里不重蹈)。
21+- **overflow 严格快失败**:准入上限 `pod_concurrency - 1`(leader 会话之外新 Pod 恰剩这些槽);超限抛 MaxPodsReached→503 NO_POD_AVAILABLE;`pod_concurrency=1` 极端场景不做特殊处理。
22+- **错误路径双清**:deploying 占位 + deploy_followers 成员都进 finally。
23+ 
24+被否方案:输家接管 deploy(见上,放大故障);进程内等待(多副本下无意义,等待室必须共享)。
25+ 
26+## 实现
27+ 
28+- `LUA_DEPLOY_FOLLOWER_GATE`(RM 第 6 个 Lua):先清过期成员 → ZADD 先行 → `ZCARD > pc-1` 自退——纪律同 SM 的 `LUA_WAITER_GATE`(禁止「先查后加」)。新键 `resource_manager:resource:scope:{sid}:deploy_followers`(ZSET,request_id→deadline)。
29+- `orchestrator.py:acquire` 锁忙分支:清占位 → `_follow_leader` 轮询——发现新 Pod 注册且 pod:info 有 sse_url → **直接复用返回**(与 reuse 分支同构);SM 侧重跑仲裁即可,**SM 零改动**、不破「RM 不读 SM 容量键」红线;锁空闲无进展 → DeployFailed;deadline → MaxPodsReached。
30+- `Template.pool_config()` / `resource:scope:{sid}:config` 携带 `pod_concurrency`——**仅用于推导 follower 上限 pc-1**,per-Pod 容量闸门仍在 SM 侧(红线不变)。
31+ 
32+## 验证
33+ 
34+- 单测:pytest 106 → **114**(follower 闸门单测 6 + 双实例 2),连跑 3 次稳定。
35+- 双实例用例 4 收紧:并发冷启动断言从「1 ≤ 部署次数 ≤ 2」收紧为「**恰好 1 次部署**」。
36+- 真环境(2 副本经 LB):M6 冒烟 **65/65**,多副本 e2e **35/35**
37+- 性能实测:冷启动尾延迟 **30.5s → 10.2s**(冷突发从串行多部署变为 1 次部署 + follower 复用),零错误。
38+ 
39+## 影响面
40+ 
41+- 文档同步:HLD 键表+读图;RM 设计 §2.1/§3/§5(Lua 5→6 含全文);code-guide Lua 表+流程;e2e-test-cases §8.2 开放问题关闭。
42+- 测试计数基准更新为 114(模块 CLAUDE.md 同步)。
43+- 注:2026-08-24 文档重组后,原 code-guide 已拆分为 `docs/spec/` 各模块 spec,本文件为该改动的正式记录。
@@ -0,0 +1,20 @@
1+# feature —— 每次改动一份文档
2+ 
3+本目录是**项目记忆**:**较大改动**(新功能 / 行为变化 / 重构 / 部署形态变化 / 里程碑)新建一个文档,记录背景、方案定案、验证证据;**小的修复(局部 bugfix、注释/文案)不用建**。git commit 是事实源,这里的文档提供可读叙事与验收数据,回答「当时为什么这么改、怎么验证的」——这些恰恰是 commit message 和代码里读不出来的。
4+ 
5+## 写作规范
6+ 
7+- **时机**:改动定稿(合并/提交)时写,随改动一起提交;文档头登记 commit hash。
8+- **命名**:`YYYY-MM-<短横线-slug>.md`;有里程碑编号的带编号,如 `2026-08-M8-deploy-lock-follower-waitroom.md`
9+- **结构**:按 [_TEMPLATE.md](_TEMPLATE.md);写完在下方索引表加一行。
10+- **内容纪律**:
11+ - 「与需求方确认的决策」逐条列出——这是防止后人重新踩已否决方案的关键。
12+ - 验证写实测数据(pytest 计数、e2e 结果、延迟数字),不写「已验证」三个字。
13+ - 被否方案与理由值得记,accepted-but-superseded 的决策标注演进关系。
14+- **颗粒度**:一次连贯的改动一份(一个 milestone / 一个有分量的 feature / 一次重构或文档重组),不求与 commit 一一对应。拿不准要不要建时,判据:半年后还有没有人需要知道「当时为什么这么改」。
15+ 
16+## 索引
17+ 
18+| 日期 | 文档 | 一句话 |
19+|---|---|---|
20+| 2026-08 | [M8 deploy 锁输家改 follower 等待室](2026-08-M8-deploy-lock-follower-waitroom.md) | 跨副本冷竞争零多余 Pod,冷启动尾延迟 30.5s→10.2s |
@@ -0,0 +1,27 @@
1+# <标题:做了什么,一句话>
2+ 
3+- 日期:YYYY-MM-DD
4+- 里程碑 / commit:M*(可选)/ <hash>
5+- 涉及模块:session_manager / resource_manager / service-core / 测试 / 部署 / 文档
6+ 
7+## 背景与动机
8+ 
9+为什么改。触发事件(bug 现象 / 验收发现 / 需求变化),以及不改会怎样。
10+ 
11+## 方案
12+ 
13+定案要点,逐条列出。与需求方确认过的决策要写明「确认过」;被否掉的备选方案及其否决理由也记在这里(防止后人重新踩)。
14+ 
15+## 实现
16+ 
17+文件级要点:改了哪些文件、新增键 / Lua / 接口、与红线规则相关的处理(占位清理、原子闸门、跨模块只走 Facade 等)。
18+ 
19+## 验证
20+ 
21+- 单测:pytest 计数变化、新增用例名与断言意图。
22+- 真环境:冒烟 / e2e 项数与结果(如 65/65)。
23+- 性能/行为实测数据(有则必填,给数字)。
24+ 
25+## 影响面
26+ 
27+同步了哪些文档(HLD / design / spec / e2e-test-cases);配置与兼容性注意;遗留开放问题与后续计划。
@@ -0,0 +1,54 @@
1+# agent-runtime 模块规格(spec)索引
2+ 
3+- 读者:AI / 维护工程师。本目录回答「**代码在哪、怎么协作、改哪里**」。
4+- 设计论证与 Lua 全文在 `../design/`;每次改动的记录在 `../feature/`(改动时新建一份,规范见 `../feature/README.md`)。
5+- 语义冲突时以 `../design/Agent-Runtime-HLD.md` 为准。
6+ 
7+## 文档地图
8+ 
9+| 文档 | 内容 | 何时读 |
10+|---|---|---|
11+| 本文件 | 架构一页纸 + 键前缀总览 + 测试/部署入口 | 先读 |
12+| [service-core.md](service-core.md) | 组装(main)/CLI/配置(`AGENT_RUNTIME_*`)/错误码契约/字段分类/工具/部署 | 改装配、配置、错误契约、部署时 |
13+| [session-manager.md](session-manager.md) | SM:route/touch/config_sync/cleanup 编排、7 个 Lua、SM 键表 | 改会话编排/配置层时 |
14+| [resource-manager.md](resource-manager.md) | RM:acquire/后台任务/K8s 适配、6 个 Lua、RM 键表 | 改 Pod 池/扩缩容/清理时 |
15+| [e2e-test-cases.md](e2e-test-cases.md) | 全部 e2e 用例的场景/输入/预期输出 | 写或跑 e2e 时 |
16+| `../design/Agent-Runtime-HLD.md` | 架构总览/接口契约/场景 A–N/Redis 键表(语义权威) | 语义不确定时 |
17+| `../design/session-manager-design.md` | SM 详细设计(7 个 Lua 全文) | 深挖 SM 设计动机 |
18+| `../design/resource-manager-design.md` | RM 详细设计(6 个 Lua 全文) | 深挖 RM 设计动机 |
19+ 
20+## 一页纸架构
21+ 
22+一个进程、一个 App(`/api/session`,端口 8091)、两个模块:
23+ 
24+```
25+gateway ──route/touch──► agent-runtime(uvicorn)
26+claw mgr ──config_sync──► ├─ session_manager 持 App,4 个 HTTP handler
27+运维 ──cleanup──────► └─ resource_manager 无 App,纯 Facade + 后台任务
28+ 两模块共享同一 Redis(前缀隔离)+ 同一 DB,互调只走进程内 Facade
29+数据面(本服务全程旁路):gateway ◄──SSE──► AgentServer Pod(route 返回 pod_sse_url)
30+```
31+ 
32+- 状态分层:**编排态在 Redis**(键前缀见下)、**配置在 DB**(`service_config_template` / `routing_rule` 表)、**Pod 物理态以 K8s 为唯一真相源**
33+- 多副本无状态;后台任务经 Redis 选主锁(`agent_runtime:job:*`)全局单副本执行写操作。
34+- 所有编排态变更走 Lua(EVAL 原子);脚本不传 KEYS,`ARGV[1]` 为键前缀,键名在脚本内拼——调用统一经各模块 `state.py``eval()`
35+ 
36+## Redis 键前缀总览(逐键明细见各模块 spec)
37+ 
38+```
39+session_manager:… SM 编排态(会话四处/scope 闸门/等待队列/候选集/注册表)
40+resource_manager:… RM 编排态(per-scope Pod 池/idle 暖池/deploy 占位/follower 等待室/选主锁)
41+agent_runtime:job:… 后台任务选主锁(main.py:_build_jobs 注册)
42+```
43+ 
44+## 测试与部署(速查)
45+ 
46+```bash
47+cd applications/agent_runtime
48+uv sync --extra local && uv run pytest # 114 用例(fakeredis+SQLite+FakeK8s)
49+./scripts/integration_smoke.sh # 真环境冒烟(场景 A–L;FLUSHDB 目标库,有防误刷)
50+./scripts/deploy_replicas.sh 2 .env.production.local 8091 # 宿主机双进程
51+./deploy/render_and_apply.sh deploy/agent_runtime.env --nodeport # K8s 生产形态
52+```
53+ 
54+用例分层表、多副本 e2e、压测入口与全部前置红线见 `e2e-test-cases.md``service-core.md` §部署。
@@ -2,7 +2,7 @@
2 2 
3- 日期:2026-08-18(M6 冒烟固化于 2026-08-15;M7 多副本补全于 2026-08-18)3- 日期:2026-08-18(M6 冒烟固化于 2026-08-15;M7 多副本补全于 2026-08-18)
4- 读者:执行/评审端到端验收的工程师4- 读者:执行/评审端到端验收的工程师
5-- 配套:语义权威 = `design/Agent-Runtime-HLD.md`(§6 场景、§5 键表);脚本本体在5+- 配套:语义权威 = `../design/Agent-Runtime-HLD.md`(§6 场景、§5 键表);脚本本体在
6 `applications/agent_runtime/scripts/`;本文回答"**每个 e2e 用例:场景、输入、预期输出**"。6 `applications/agent_runtime/scripts/`;本文回答"**每个 e2e 用例:场景、输入、预期输出**"。
7 7 
8---8---
@@ -0,0 +1,124 @@
1+# resource_manager(RM)规格
2+ 
3+> Pod 池管理:acquire(扩容决策)、后台任务(autoscale/reclaim/watch/reconcile)、K8s 适配。
4+> **无 App/端口/prefix**,纯进程内 Facade + 后台任务。**不读 SM 的容量键**(per-Pod 容量闸门在 SM 侧)。
5+> Lua 全文:`lua_scripts.py` 与 `../design/resource-manager-design.md` 双份,改时同步。
6+ 
7+## 文件一览
8+ 
9+| 文件 | 职责 |
10+|---|---|
11+| `facade.py` | `ResourceManagerFacade`(SM→RM 进程内入口,薄封装) |
12+| `orchestrator.py` | acquire(取暖/选主 deploy/follower 等待室)+ idle_consider + update_pool_config + cleanup + 幂等缓存 |
13+| `state.py` | RM Redis 键 schema 唯一出口(`RMKeys`/`ResourceState`) |
14+| `lua_scripts.py` | 6 个 Lua 全文 |
15+| `k8s.py` | `K8sPodClient` 接口 + `RealK8sPodClient`(kubernetes_asyncio)+ `FakeK8sPodClient` |
16+| `sweeper.py` | 四个后台任务(各自带选主锁) |
17+| `models.py` | `PodInfo`/`PodDeployInfo`/判死枚举/label 常量 |
18+ 
19+## facade.py —— SM→RM 契约
20+ 
21+| 方法 | 返回/异常 |
22+|---|---|
23+| `acquire(scope_id, pod_spec, pool_config, request_id)` | `{pod_id, pod_sse_url}`;失败抛 `MaxPodsReached`/`DeployFailed`(SM 映射 503 NO_POD_AVAILABLE) |
24+| `idle_consider(pod_id, scope_id)` | `{transitioned_to_idle}`,幂等 |
25+| `update_pool_config(scope_id, pool_config, pod_spec?)` | `{updated}`(config_sync 触发) |
26+| `cleanup(namespace?, label_selector?)` | `cleaned: int`(运维批删) |
27+ 
28+## orchestrator.py —— acquire 决策树
29+ 
30+```
31+幂等缓存(request_id,键 resource_manager:idem:{rid},TTL 60,命中续期)
32+→ 首见 scope:缓存池参数 + pod_spec_json 到 resource:scope:{sid}:config
33+→ 循环 { LUA_ACQUIRE → (action, pod_id, sse_url):
34+ reuse → 直接返回(deploy_ver 过滤后的暖 Pod,已弹出 idle 池)
35+ max_reached → 清占位 → MaxPodsReached
36+ no_config → continue(上面已写配置)
37+ need_deploy → 抢 lock:rm:deploy:{scope}(TTL 360,盖住 ready_timeout 300+余量):
38+ 赢家 → _deploy_and_register(idle_flag=False)
39+ 输家 → 清占位 → _follow_leader(follower 等待室) }
40+```
41+ 
42+`_deploy_and_register`:`k8s.deploy(pod_spec)`(create+wait Ready)→ 拼 `pod_sse_url = http://{pod_ip}:{sse_port}{sse_path}``LUA_REGISTER`**失败必须清 deploying 占位再抛 DeployFailed(红线:防 max_pods 永久虚高)**。
43+ 
44+`_follow_leader`(M8,deploy 锁输家的等待室):
45+- 准入走 `LUA_DEPLOY_FOLLOWER_GATE` 原子闸门,上限 `pod_concurrency - 1`(leader 会话之外新 Pod 恰剩这些槽);overflow 严格快失败 MaxPodsReached。
46+- 等待有界:`ready_timeout + 10s` 余量;轮询 `resource:scope:{sid}:pods` 出现新 Pod 且 pod:info 有 sse_url → **直接复用返回**(与 reuse 分支同构,SM 侧重跑仲裁即可)。
47+- leader 失败判定:deploy 锁空闲且无新 Pod → `DeployFailed`(**follower 不接管**——同镜像同环境大概率也失败);deadline 到 → MaxPodsReached。
48+- 错误路径双清:占位 + follower 成员都进 finally;崩溃遗留由闸门 `ZREMRANGEBYSCORE(deadline)` 兜底。
49+ 
50+`idle_consider`:`LUA_RELEASE` 转 idle 暖池(起 pod_ttl 计时)+ pod:info.phase=idle;幂等。
51+`update_pool_config`:HSET 覆盖池参数;A 类变更附带 pod_spec 时同时刷 deploy_ver/pod_spec_json(autoscale 补位用新 deploy 字段)。
52+`cleanup`:K8s list+delete,**不操作 Redis 编排态**(被删 Pod 由 watch/reconcile 兜底发现);ns 404 容忍为 cleaned=0,**403 保持 fail-fast**(静默清零会掩盖部署配错)。
53+ 
54+## state.py —— RM 键表(`RMKeys`,前缀 `resource_manager:`,业务键再带 `resource:` 段)
55+ 
56+| 键 | 类型 | 语义 |
57+|---|---|---|
58+| `resource:scope:{sid}:pods` | ZSET | 该 scope 全部 Pod(in_use ∪ idle);**ZCARD+deploying SCARD 参与 max_pods 判定** |
59+| `resource:scope:{sid}:idle` | SET | idle 暖池;acquire 从此取暖 Pod |
60+| `resource:scope:{sid}:config` | HASH | min_idle_pods/max_pods/pod_ttl/pod_concurrency/deploy_ver/pod_spec_json |
61+| `resource:scope:{sid}:deploying` | SET | deploy 占位 token(计入 max_pods,防并发超配) |
62+| `resource:scope:{sid}:deploy_followers` | ZSET | follower 等待室(request_id→deadline 秒级 score;闸门按 deadline 原子清过期) |
63+| `resource:pod:{pod}:info` | HASH | scope_id/pod_sse_url/pod_ip/namespace/phase/created_ts/deploy_ver |
64+| `resource:pod:{pod}:idle_since` | STR | idle 起始(reclaim 计时);存在 ⟺ 在 idle 池 |
65+| `resource:pod:{pod}:health_fails` | STR | 健康探测连续失败次数(场景 N) |
66+| `resource:pods:all` | SET | 全部 pod_id(watch/reconcile 枚举) |
67+| `lock:rm:deploy:{sid}` | STR(NX EX 360) | per-scope deploy 选主串行 |
68+| `lock:rm:autoscale\|reclaim\|watch\|reconcile` | STR(NX EX) | 后台任务 tick 级选主 |
69+| `idem:{request_id}` | STR | acquire 结果幂等缓存(TTL 60) |
70+ 
71+计数全部派生自 SCARD/ZCARD,无独立计数器。
72+ 
73+## lua_scripts.py —— 6 个 Lua
74+ 
75+| 脚本 | 一句话职责 |
76+|---|---|
77+| `LUA_ACQUIRE` | 取暖 Pod 复用(**跳过 deploy_ver 不匹配**——A 类变更后老版本暖 Pod 不外发,按 pod_ttl 自然回收)→ 无匹配判 max_pods(ZCARD pods + SCARD deploying)→ 占位 SADD deploying → need_deploy |
78+| `LUA_PLACEHOLDER` | autoscale 专用占位(判 max_pods + SADD,**不碰 idle 池**——补位不该消耗暖 Pod) |
79+| `LUA_REGISTER` | deploy 成功登记:pod:info / scope:pods / pods:all 同写,清占位;idle_flag=1(热备)入 idle 池 |
80+| `LUA_RELEASE` | idle_consider:转 idle 暖池 + 起 pod_ttl 计时(SADD/SET 天然幂等) |
81+| `LUA_PURGE` | Pod 死亡/reclaim 后清全部 RM key(返回其 scope_id;幂等) |
82+| `LUA_DEPLOY_FOLLOWER_GATE` | follower 等待室原子准入:先 `ZREMRANGEBYSCORE` 清过期 → ZADD 先行 → ZCARD 超限自退(纪律同 LUA_WAITER_GATE,禁止先查后加) |
83+ 
84+约定同 SM:不传 KEYS,`ARGV[1]`=前缀,返回扁平字符串数组。
85+ 
86+## k8s.py —— K8s 适配层
87+ 
88+`K8sPodClient` 抽象(Real/Fake 同签名):`start/close/deploy/delete/get_pod/list_pods/probe_health`
89+ 
90+**RealK8sPodClient**(kubernetes_asyncio):
91+- `start()`:先 `load_incluster_config()`(**同步函数,不可 await**——await 会 TypeError→in-cluster 必挂,M7 修复),ConfigException 再 `await load_kubeconfig()`
92+- `deploy(pod_spec)`:pod_id = `{pod_name}-{随机10}-{随机5}`(**K8s 随机 Pod 名,严禁业务 id 当实例 id——历史死锁根因**);409 名字冲突重命名重试至多 3 次;`_wait_ready` 轮询至 Ready+有 podIP,终态(Failed/Succeeded)/消失/超时 → DeployFailed。
93+- `_build_pod_body`:label `{jiuwenclaw-component: agentserver, app: pod_id}`;NFS 卷挂载;资源 requests/limits;sse_port 必开(名 `sse`),container_port≠sse_port 加 `http`;readiness probe = `GET /health:sse_port`(AgentServer 固定约定,场景 N);restart_policy=Always。
94+- `normalize_phase`:deletion→Terminating;容器 waiting reason(ImagePullBackOff/CrashLoopBackOff/…)优先于 phase。
95+ 
96+**FakeK8sPodClient**(local/单测):deploy 立即 Ready;可编程 `unready_pods`/`dead_pods`/`unhealthy_pods`/`deploy_failures` 模拟异常分支。
97+ 
98+`probe_health(pod_ip, sse_port)`:`GET http://{pod_ip}:{sse_port}/health`,3s 超时,非 200/异常即不健康(K8sPodClient 基类默认实现,Real/Fake 共用)。
99+ 
100+## models.py
101+ 
102+- `DEAD_POD_STATUSES`:Terminating/Failed/CrashLoopBackOff/ImagePullBackOff/ErrImagePull/InvalidImageName。**Pending 不判死**(deploy 靠 ready_timeout 兜)。
103+- `POD_LABEL_SELECTOR = "jiuwenclaw-component=agentserver"`(cleanup 默认 selector)。
104+- `PodDeployInfo`(deploy 产物物理信息)/`PodInfo`(get/list 状态视图:归一化 phase+ready+reason)。
105+ 
106+## sweeper.py —— 四个后台任务
107+ 
108+均 per-scope 操作、不读 SM Redis key、各自带 tick 级选主锁(调度由 main 注入):
109+ 
110+| 任务 | 周期/锁 | 逻辑 |
111+|---|---|---|
112+| `autoscale_once`(场景 H) | 1s / lock:rm:autoscale | 遍历 `known_scope_ids()`(SCAN scope:config):idle < min_idle_pods 且 pods+deploying < max_pods → `LUA_PLACEHOLDER` 占位 → 抢 deploy 锁 → `_deploy_and_register(idle_flag=True)` 热备入池;pod_spec 取 scope:config 缓存(A 类变更后为新值) |
113+| `reclaim_once`(场景 K) | 1s / lock:rm:reclaim | idle 超 min_idle 底数的 excess 中 `aged ≥ pod_ttl` → `_purge_and_notify`(K8s delete → LUA_PURGE → notify_pod_dead);保护最早入 idle 的 min_idle 个(保底热备) |
114+| `watch_once`(场景 J/N) | 10s / lock:rm:watch(TTL 15) | 遍历 pods:all:get_pod 为 None 或 phase∈DEAD → 清理;Running 但 `probe_health` **连续 2 次失败**(health_fails 阈值,防瞬时抖动误杀)→ 半死清理;成功清零计数。sse_port 从 scope:config 的 pod_spec_json 取 |
115+| `reconcile_once`(场景 L) | 30s / lock:rm:reconcile(TTL 60) | ① Redis 有 K8s 无 → PURGE+notify;② RM 持有但 SM 候选集已无的 stale Pod(经 `sm_facade.reconcile_pods`,Facade 单向)→ `LUA_RELEASE` 转 idle 按 pod_ttl 回收 |
116+ 
117+`_purge_and_notify(pod_id)` 三步(K8s delete 若还在 → LUA_PURGE → notify_pod_dead);全幂等,单步失败仅记录(30s reconcile 兜底)。
118+ 
119+## 高频踩点
120+ 
121+- 错误路径必须清占位(deploy 失败 → SREM deploying);follower 路径还要清 deploy_followers 成员——**双清纪律**
122+- 跨副本冷竞争:follower 等待室保证并发冷启动「恰好 1 次部署」;测试断言「窗口零重叠 + Pod ≤ max_pods」,多后端冷突发 NO_POD_AVAILABLE 快失败属预期。
123+- cleanup 空目标必须用**无匹配 label_selector**(业务 ns 同 label 会误删真实 AgentServer;不存在 ns 在 in-cluster SA 下 403 而非空列表)。
124+- 框架 `load_incluster_config` 是同步函数(已修);`kubernetes_asyncio` 仅 server extra 依赖,local 模式不得 import(判定用 `getattr(exc, "status")`)。
@@ -0,0 +1,110 @@
1+# service-core 规格(组装 / 配置 / 错误码 / 字段分类 / 部署)
2+ 
3+> 覆盖 `src/agent_runtime/` 顶层 6 个文件:`main.py`、`cli.py`、`config.py`、`errors.py`、`spec_fields.py`、`util.py`。
4+> SM/RM 模块内文件见 [session-manager.md](session-manager.md) / [resource-manager.md](resource-manager.md)。
5+ 
6+## main.py —— 组装入口(唯一可运行的壳)
7+ 
8+### build_resources(settings, arc) → (redis, db, k8s)
9+ 
10+双模式构造共享物理资源:
11+ 
12+| mode | redis | db | k8s |
13+|---|---|---|---|
14+| `local` | fakeredis(进程内) | 文件型 SQLite(`AGENT_RUNTIME_SQLITE_PATH`,默认 `./agent_runtime_local.db`;`:memory:` 在连接池下会丢表) | `FakeK8sPodClient` |
15+| `server` | `build_redis_client(settings)` | `build_db_handler(settings)`,**None 即 RuntimeError**(禁 SQLite 回退,fail-fast) | `RealK8sPodClient` |
16+ 
17+### OrchestratorSystemContext(SystemContext 子类)
18+ 
19+SM 侧 ctx,级联管理全部生命周期(框架 App 的 lifespan 只认一个 ctx_factory——返回本类):
20+ 
21+- 构造时同时建 **rm_sysctx**(同 redis/db,仅 `key_prefix` 不同:SM=`session_manager`,RM=`resource_manager`)。
22+- `_bind_modules()`:先构造后绑定,破解 SM↔RM 循环引用——
23+ `SessionState`/`ResourceState``SessionManagerFacade`/`ResourceManagerFacade(ResourceOrchestrator)``ConfigStore(push_pool_config=rm_facade.update_pool_config)``SessionOrchestrator``SessionSweeper`/`ResourceSweeper`
24+- `_build_jobs()`:5 个后台任务,全部 `create_single_leader_job`(tick 级 Redis 选主锁,多副本全局单副本执行):
25+ 
26+| 任务 | tick | 锁键(`agent_runtime:job:*`) | 动作 |
27+|---|---|---|---|
28+| sm_sweep | `sweep_interval`(1s) | `sm_sweep` | 到期 pass + 空 Pod pass |
29+| rm_autoscale | `autoscale_interval`(1s) | `rm_autoscale` | min_idle 热备补位 |
30+| rm_reclaim | `reclaim_interval`(1s) | `rm_reclaim` | idle 超 pod_ttl 回收 |
31+| rm_watch | `watch_interval`(10s) | `rm_watch` | 死 Pod 判定 + 健康探测 |
32+| rm_reconcile | `reconcile_interval`(30s) | `rm_reconcile` | 孤儿/stale 对账 |
33+ 
34+- `start()` 顺序:super().start() → rm_sysctx.start() → k8s.start()(**失败仅降级扩缩容,不阻断启动**)→ 启动 5 个 job。`stop()` 逆序。
35+- DB 表初始化:构造参数 `table_definitions=[SERVICE_CONFIG_TEMPLATE_TABLE_DEF, ROUTING_RULE_TABLE_DEF]`(表结构在 `config_store.py`)。
36+ 
37+### create_app(settings, arc, *, resources=None, instance_id=None, own_resources=True)
38+ 
39+构造唯一 App(`prefix=/api/session`,`enable_ws=False`)+ `/healthz` + 4 个 handler。
40+`resources`/`instance_id`/`own_resources` 仅供多实例测试注入共享物理资源(`tests/integration/_dual_harness.py`);生产路径不传。
41+ 
42+### /healthz(main.py:_register_healthz)
43+ 
44+进程就绪探针(K8s probe / deploy_replicas.sh 就绪轮询 / e2e 实例观测)。sysctx 未就绪 → 503;就绪返回 `{ok, instance_id}`。
45+**坑**:模块顶部 `from __future__ import annotations` 下,FastAPI 经 `get_type_hints` 用模块全局解析注解——`Request` 必须顶层 import,函数内局部导入会被当成 query 参数(422)。
46+ 
47+## cli.py —— 命令行入口
48+ 
49+`deploy.sh` 调用。参数:`--mode local|server`(必填)、`--env-file`(dotenv 先加载,`override=False`)、`--host`/`--port`(覆盖 `OPENJIUWEN_SERVICE_*`)。流程:load_dotenv → 设 `AGENT_RUNTIME_MODE` → basicConfig(`AGENT_RUNTIME_LOG_LEVEL`)→ `ServiceConfig.from_env()` + `AgentRuntimeConfig.from_env()` → `create_app` → uvicorn.run。
50+ 
51+## config.py —— AgentRuntimeConfig(本服务自有配置)
52+ 
53+框架级(host/port/redis/db)走 `ServiceConfig.from_env()`(`OPENJIUWEN_SERVICE_*`);本文件只放 `AGENT_RUNTIME_*`:
54+ 
55+| 字段 | env | 默认 | 说明 |
56+|---|---|---|---|
57+| mode | `AGENT_RUNTIME_MODE` | server | server\|local |
58+| kubeconfig | `AGENT_RUNTIME_KUBECONFIG` | None(集群内 SA) | |
59+| default_namespace | `AGENT_RUNTIME_DEFAULT_NAMESPACE` | default | |
60+| sweep_interval | `AGENT_RUNTIME_SWEEP_INTERVAL` | 1 | SM:到期+空 Pod pass |
61+| autoscale_interval | `AGENT_RUNTIME_AUTOSCALE_INTERVAL` | 1 | RM:min_idle 补位(**全局默认,无 per-scope 覆盖**) |
62+| reclaim_interval | `AGENT_RUNTIME_RECLAIM_INTERVAL` | 1 | RM:idle 回收 |
63+| watch_interval | `AGENT_RUNTIME_WATCH_INTERVAL` | 10 | RM:死 Pod+健康探测 |
64+| reconcile_interval | `AGENT_RUNTIME_RECONCILE_INTERVAL` | 30 | RM:对账 |
65+| scope_full_timeout | `AGENT_RUNTIME_SCOPE_FULL_TIMEOUT` | 30.0 | 等待队列阻塞上限(**部署须显著小于 session_ttl**,否则等待者 deadline 与会话到期碰撞) |
66+| default_session_ttl | `AGENT_RUNTIME_DEFAULT_SESSION_TTL` | 60 | touch 兜底 ttl |
67+ 
68+常量:`SM_KEY_PREFIX="session_manager"``RM_KEY_PREFIX="resource_manager"``SERVICE_PREFIX="/api/session"`
69+ 
70+## errors.py —— 错误码契约(语义权威 HLD §3.1)
71+ 
72+`ErrorCode` 常量 + 异常类(均继承 `AgentRuntimeError(FrameworkError)`,类属性 `code`)+ `HTTP_STATUS_MAP` + `register_codes()`(幂等,main.py import 时调用)。
73+ 
74+| 码 | HTTP | retry_after | 场景 |
75+|---|---|---|---|
76+| `SCOPE_QUEUE_FULL` | 503 | ✅ | 等待队列满,快失败 |
77+| `SCOPE_FULL_TIMEOUT` | 504 | ✅ | 队列内等待超时 |
78+| `NO_POD_AVAILABLE` | 503 | ✅ | acquire 失败(MaxPodsReached/DeployFailed 在 SM 侧映射而来) |
79+| `CONFIG_NOT_FOUND` | 503 | ❌ | resolve 无匹配规则/模板禁用 |
80+| `VALIDATION` | 400 | ❌ | 参数错 |
81+| `CONFIG_SYNC_BUSY` | 409 | — | 上一次热更新未完成 / 日落待回收中间态 Pod |
82+ 
83+- `retry_after` 仅过载类携带(秒);Facade 间以 Python 异常传播,handler 捕获后映射为 `ResponseEnvelope(ok=False, error_code, retry_after)`
84+- `MAX_PODS_REACHED` / `DEPLOY_FAILED` 是 RM Facade 内部异常,SM route 捕获后统一映射 `NO_POD_AVAILABLE`,不对外。
85+ 
86+## spec_fields.py —— template 字段分类(SM/RM 静态共享)
87+ 
88+- `DEPLOY_FIELDS`:A 类(deploy 子集,值烘焙进运行中 Pod,变更需日落)。
89+- `DEPLOY_VER_FIELDS = DEPLOY_FIELDS + (ready_timeout, ready_poll_interval)`:deploy 指纹字段集。
90+- `POLICY_FIELDS`:B 类策略(`scope_concurrency/pod_concurrency/session_ttl/pod_ttl/min_idle_pods`)。
91+- **kubeconfig 例外**:在 deploy 子集但**不入指纹**(只影响新 deploy,不日落)。
92+- 约束:SM `Template.deploy_ver()` 与 RM `orchestrator._deploy_ver()` 必须用同一字段集与算法(`util.fingerprint`)——A 类版本过滤依赖两端一致。
93+- **新增 template 字段时**:先在此分类 → 再补 `config_store.py``_COLUMN_OF` 列映射与 `*_TABLE_DEF` 表结构。
94+ 
95+## util.py —— 纯函数
96+ 
97+- `scope_id_of(group_id, bot_id)` = `md5(group_id + "\x00" + bot_id)`;`\x00``(ab,c)`/`(a,bc)` 撞号。
98+- `fingerprint(fields)`:按 key 排序、剔 None、md5 取前 16 hex(deploy_ver 用)。
99+- `s()`/`to_int()`:Redis 返回值 bytes/str 归一(真实 client 是 bytes,fakeredis 可能是 str)。
100+- `now_ts()`:秒级 int(Redis 键内时间统一秒级)。
101+ 
102+## 部署
103+ 
104+- **双进程宿主机**:`scripts/deploy_replicas.sh N [env] [port]`(N 进程共 Redis/DB,`/healthz` 就绪轮询,trap 清理;local 模式 fail-fast)。
105+- **K8s 生产形态**:`deploy/` 目录——`agent_runtime.template.yaml`(SA+Role×2+Deployment 多副本/反亲和//healthz 探针+ClusterIP Service LB)、可选 NodePort(30091)、`Dockerfile`(**build context=仓库根**,保 `../../foundation`/`../../service` 布局,`uv sync --frozen --extra server --no-dev`,logs/ 预建归 appuser)、`render_and_apply.sh`(env 渲染→apply,残留 `<<` 即 fail-fast)、`build_image.sh`
106+- K8s 部署红线:
107+ - `OPENJIUWEN_SERVICE_DEPLOY_REPLICAS=1` 固定(副本数=Deployment replicas;框架该项 >1 会因缺分布式锁后端启动即失败)。
108+ - RBAC 两份:服务 ns + AgentServer 目标 ns(缺则 create pod 403 → route 全 503)。
109+ - Pod 内 MySQL 用户须授权 Pod CIDR(`'agent_runtime'@'10.244.%'`)。
110+ - server 模式硬要求:Redis 开 AOF/RDB;DB 用 MySQL/PostgreSQL。
@@ -0,0 +1,133 @@
1+# session_manager(SM)规格
2+ 
3+> 会话编排:route/touch HTTP 端点、配置层(config_sync)、老化 sweeper。
4+> **持唯一 App**(`/api/session`:8091),注册 4 个 handler。与 RM 互调只走进程内 Facade,**不直读 RM Redis key**
5+> Lua 全文:`lua_scripts.py` 与 `../design/session-manager-design.md` 双份,改时同步。
6+ 
7+## 文件一览
8+ 
9+| 文件 | 职责 |
10+|---|---|
11+| `handlers.py` | 4 个 HTTP handler(route/touch/config_sync/cleanup)+ 错误信封映射 |
12+| `orchestrator.py` | route 主循环(resolve→Lua 仲裁→acquire→等待队列)+ touch |
13+| `state.py` | SM Redis 键 schema 唯一出口 + Lua 调用封装(`SMKeys`/`SessionState`) |
14+| `lua_scripts.py` | 7 个 Lua 全文 |
15+| `config_store.py` | template/routing_rule DB 持久化 + resolve 缓存 + config_sync 编排 |
16+| `sweeper.py` | 到期 pass + 空 Pod pass(每 tick 选主) |
17+| `facade.py` | `SessionManagerFacade`(RM→SM:notify_pod_dead / reconcile_pods) |
18+| `models.py` | `Template` / `ScopeConfig` dataclass |
19+ 
20+## handlers.py —— 对外 4 端点
21+ 
22+| 端点 | handler | 行为 |
23+|---|---|---|
24+| POST /api/session/route | `handle_route` | 同步路由+占额度,返回 `{pod_sse_url, pod_id}`;**幂等键 = metadata.request_id**(框架 idempotency,窗口 60s,回放缓存结果) |
25+| POST /api/session/touch | `handle_touch` | 保活/EOS,返回 `{touched}`;False=已过期/不存在(gateway 回退重新 route) |
26+| POST /api/session/config_sync | `handle_config_sync` | 配置下发,委托 `ConfigStore.config_sync` |
27+| POST /api/session/cleanup | `handle_cleanup` | 运维批删 Pod,委托 `rm_facade.cleanup`(handler 在 SM,逻辑在 RM) |
28+ 
29+- 入参从 `Envelope.metadata`(session_id/group_id/bot_id/request_id)与 `rawdata` 取;`group_id``metadata.extra`
30+- `AgentRuntimeError` 统一捕获 → `ResponseEnvelope(ok=False, error_code, error_message, retry_after)`
31+- handler 无模块级可变状态;服务对象从 `sysctx` 取(`main._bind_modules` 注入)。
32+ 
33+## orchestrator.py —— route 主循环
34+ 
35+`SessionOrchestrator.route(request_id, session_id, group_id, bot_id)`:
36+ 
37+```
38+resolve(scope)(config_store:Redis 缓存→DB,规则优先级 精确>(g,*)>(*,b)>(*,*))
39+→ 循环 { LUA_ROUTE_PLACE 原子仲裁 → (action, pod_id):
40+ refresh/placed → 读 pod:info sse_url 返回(缺失=极端竞态被清,continue 重跑)
41+ scope_full → _wait_for_capacity(场景 F)后重跑
42+ need_acquire → rm_facade.acquire(扩+1)→ state.register_pod → 重跑(新 Pod 必被 first-fit 选中) }
43+finally: 若仍在等待队列 → remove_waiter(异常路径出队)
44+```
45+ 
46+`_wait_for_capacity`(场景 F 有界等待):
47+- `max_waiters = 2 * scope_concurrency`;过 deadline(`scope_full_timeout`)→ 504 ScopeFullTimeout。
48+- **入队只走 `LUA_WAITER_GATE` 原子闸门**(SADD 先行+超限自退);满 → 503 ScopeQueueFull 快失败。
49+- 订阅 `scope:{sid}:free` PubSub + ≤500ms 安全轮询双保险(兜 publish 早于 subscribe 的丢失);收到信号即出队重跑 Lua——**原子 admit 是唯一仲裁,败者重 wait**
50+ 
51+`_acquire_pod`:`MaxPodsReached`/`DeployFailed` → 映射 `NoPodAvailable(503, retry_after=1)`
52+ 
53+`touch(session_id)`:LUA_TOUCH;不存在/已过期返回 False。
54+ 
55+## state.py —— SM 键表(`SMKeys`,全部含 `session_manager:` 前缀)
56+ 
57+| 键 | 类型 | 语义 |
58+|---|---|---|
59+| `session:{sid}` | HASH | 亲和绑定:scope_id/pod_id/expiry/session_ttl |
60+| `session_expiry` | ZSET | 到期时间戳(sweeper 到期 pass 扫它) |
61+| `scope:{sid}:sessions` | SET | 活跃 session;**SCARD = scope_concurrency 闸门** |
62+| `scope:{sid}:pods` | ZSET | first-fit 候选(score=接入序;ZREM 即退出候选——软摘除/idle 通知都用它) |
63+| `scope:{sid}:pod_seq` | STRING | 单调递增,pods 的 score 来源 |
64+| `scope:{sid}:config` | HASH | resolve 缓存(策略字段 + `template_json`;config_sync 主动 DEL 失效) |
65+| `scope:{sid}:waiters` | SET | 等待队列(LUA_WAITER_GATE 原子进出) |
66+| `scope:{sid}:free` | PubSub | 额度释放信号(EVICT 发布/route 订阅) |
67+| `pod:{scope}:{pod}:sessions` | SET | per-Pod 会话;**SCARD < pod_concurrency = per-Pod 容量闸门(SM 侧,RM 不强制)** |
68+| `pod:{scope}:{pod}:info` | HASH | sse_url / deploy_ver |
69+| `pod:{scope}:{pod}:idle_notified` | STR(NX EX 60) | 空 Pod 通知去重 |
70+| `pods:registered` | SET | 全部 `"{scope}:{pod}"`(不变量:scope:pods ⊆ pods:registered) |
71+| `pods:{pod}:scopes` | SET | Pod 被哪些 scope 引用(notify_pod_dead 反查) |
72+| `lock:sweep` / `lock:config_sync` | STR(NX EX) | tick 级选主 / config_sync 串行化(TTL 60) |
73+ 
74+不变量 1:一个活跃会话同时存在于四处(session HASH + scope:sessions + pod:sessions + session_expiry)——EVICT/惰性回收四处同删 + PUBLISH free。
75+ 
76+## lua_scripts.py —— 7 个 Lua
77+ 
78+| 脚本 | 一句话职责 |
79+|---|---|
80+| `LUA_ROUTE_PLACE` | route 原子核心:亲和续期→惰性回收旧绑定→scope 闸门(SCARD)→first-fit(接入序)→达 max_pods 则 scope_full / 否则 need_acquire→原子提交四处同写(复用时清 idle_notified) |
81+| `LUA_EVICT` | session 移除**唯一原语**(四处同删 + PUBLISH free 唤醒等待者;返回 scope/pod/remaining;幂等 noop) |
82+| `LUA_TOUCH` | 保活续期;已过期当场惰性 evict;ttl 就地读 session HASH(不依赖 scope:config) |
83+| `LUA_SWEEP_IDLE_NOTIFY` | 空 Pod 判定(SCARD==0)+ 60s NX 去重 + ZREM 退出候选(堵 reclaim 窗口内 route 直选的竞态 A) |
84+| `LUA_REGISTER_POD` | acquire 成功登记:三处注册(scope:pods/pod:info/pods:registered)+ 接入序 + pods:{pod}:scopes |
85+| `LUA_CLEANUP_POD` | notify_pod_dead 清该 (scope,pod) 全部注册(会话 evict 由调用方先行) |
86+| `LUA_WAITER_GATE` | 等待队列原子入队(SADD 先行 + SCARD 超限自退)——**禁止改回「先 SCARD 再 SADD」**(M6 验收发现的并发超收事故) |
87+ 
88+约定:脚本不传 KEYS,`ARGV[1]`=键前缀,键在脚本内拼;返回扁平字符串数组(`SessionState.eval` 统一转 str)。
89+ 
90+## config_store.py —— 配置层
91+ 
92+**DB 表**(列名沿用 EE 兼容名,映射在 `_COLUMN_OF`):`service_config_template`(`min_idle_pods→min_idle_services``pod_concurrency→service_concurrency``pod_ttl→service_ttl``scope_concurrency→session_concurrency`)、`routing_rule`。表结构常量 `*_TABLE_DEF` 由 main 传给框架建表。
93+ 
94+**resolve(scope_id, group_id, bot_id)**:缓存(`scope:{sid}:config`)命中直接回;miss 读 DB(优先级 精确>(g,\*)>(\*,b)>(\*,\*),模板须 enabled)→ 回写缓存。缓存值 = ScopeConfig 字段 + `template_json`(deploy 子集,need_acquire 时零 DB)。无匹配 → `ConfigNotFound(503)`。
95+ 
96+**config_sync(payload)**(场景 M):`{kind: template|routing_rule, op: create|update|delete|sync, ...}`
97+ 
98+```
99+lock:config_sync 串行化(忙→409 CONFIG_SYNC_BUSY,TTL 60)
100+→ 写 DB(失败立即中止,不碰缓存、不推送——红线:DB 写失败不得刷新缓存)
101+→ template 变更扩散 _propagate_template_change:
102+ 完成判定:受影响 scope 仍有「已日落待回收」中间态 Pod(在 pods:registered 不在候选集)→ 409 拒绝
103+ 逐字段 diff 判类(spec_fields):
104+ A 类(deploy_ver 变)→ 软摘除(ZREM 老版本 Pod 出候选;存量会话不受影响)
105+ + 写新缓存 + 推 RM(新 deploy_ver/pod_spec → 新流量落新 Pod,自然滚动)
106+ B 类(策略字段变) → DEL 缓存 + 推池参数(不带 pod_spec),立即生效
107+routing_rule 任何变更 → 无法定位受影响 scope(缓存无 group/bot)→ SCAN 全量 DEL 缓存(resolve 便宜)
108+```
109+ 
110+`Template.deploy_ver()` / RM `_deploy_ver()` 同一算法(`util.fingerprint` + `DEPLOY_VER_FIELDS`)——A 类过滤两端一致的前提。
111+ 
112+## sweeper.py —— 老化扫描
113+ 
114+`SessionSweeper.sweep_once()`(tick=`sweep_interval` 1s;`lock:sweep` SET NX EX 2 选主,抢不到即跳过):
115+- **到期 pass**:ZRANGEBYSCORE `session_expiry` → 逐个 `LUA_EVICT`(废弃 session 的唯一回收路径)。
116+- **空 Pod pass**(idle_consider 的**唯一触发点**):枚举 `pods:registered` → `LUA_SWEEP_IDLE_NOTIFY` 原子判定(空+未通知过+ZREM 退出候选)→ notified=True 才 **fire-and-forget** `rm_facade.idle_consider`(失败不阻塞;60s 后 idle_notified 过期自愈重发)。统一覆盖三种空 Pod 成因:到期 evict / 惰性 evict / acquire 后从未放置的孤儿。
117+ 
118+## facade.py —— RM→SM 入口
119+ 
120+- `notify_pod_dead(pod_id)`(场景 G):反查 `pods:{pod}:scopes` → 逐 session `LUA_EVICT`(释放额度+唤醒等待者)→ `LUA_CLEANUP_POD` 清注册;幂等。
121+- `reconcile_pods(view)`(场景 L):对 RM 持有的每个 (pod, scope) 查 `scope:pods` 成员资格,非成员=SM 已不用 → stale;只读、单向。
122+ 
123+## models.py
124+ 
125+- `Template`:template 行业务视图。派生:`max_pods = ⌈scope_concurrency/pod_concurrency⌉`(**派生值,不存储,不配置**);`deploy_subset()`=acquire 下发 RM 的 pod_spec;`deploy_ver()`=A 类指纹;`pool_config()`={min_idle_pods, max_pods, pod_ttl, pod_concurrency}(pod_concurrency 仅供 RM follower 等待室推导上限 pc-1)。
126+- `ScopeConfig`:resolve 产物(scope:config 缓存的 HASH 字段一一对应)。
127+ 
128+## 高频踩点
129+ 
130+- 改键名/Lua:HLD §5 键表、`state.py`、SM 详细设计三处同步。
131+- 等待队列入队只准走 `LUA_WAITER_GATE`;「先查后加」已被真环境验收证伪。
132+- config_sync 全局串行锁——e2e 脚本播种配置必须串行,并发即 409。
133+- 经多副本 LB 跑冒烟须设 `AGENT_RUNTIME_SCOPE_FULL_TIMEOUT`(显著小于 session_ttl),排查实录见 `e2e-test-cases.md` §8.1。
@@ -1,214 +0,0 @@
1-# agent-runtime 代码说明文档
2- 
3-- 日期:2026-08-15(M0–M6 完成后整理)
4-- 读者:维护/二次开发本服务的工程师
5-- 配套:语义权威 = `design/Agent-Runtime-HLD.md`;模块细节 = SM/RM 两份详细设计。本文回答"**代码在哪、怎么跑起来、改哪里**",不重复论证设计决策。
6- 
7----
8- 
9-## 1. 一页纸架构
10- 
11-一个进程、一个 App(`/api/session`,端口 8091)、两个模块:
12- 
13-```
14-gateway ──route/touch──► agent-runtime(uvicorn)
15-claw mgr ──config_sync──► ├─ session_manager 持 App,4 个 HTTP handler
16-运维 ──cleanup──────► └─ resource_manager 无 App,纯 Facade + 后台任务
17- 两模块共享同一 Redis(前缀隔离)+ 同一 DB,互调只走进程内 Facade
18-数据面(本服务全程旁路):gateway ◄──SSE──► AgentServer Pod(route 返回 pod_sse_url)
19-```
20- 
21-- 状态分层:**编排态在 Redis**(SM `session_manager:` / RM `resource_manager:` 前缀)、**配置在 DB**(`service_config_template` / `routing_rule` 表)、**Pod 物理态以 K8s 为唯一真相源**
22-- 多副本无状态;后台任务经 Redis 选主锁全局单副本执行写操作。
23- 
24-## 2. 代码地图(`applications/agent_runtime/`)
25- 
26-```
27-src/agent_runtime/
28-├── main.py ★ 组装入口:create_app → OrchestratorSystemContext
29-│ (级联拉起 rm_sysctx + Facade 互绑 + 5 个后台任务)
30-├── cli.py 命令行入口(deploy.sh 调用;--mode local|server)
31-├── config.py AGENT_RUNTIME_* 环境变量(任务周期/超时/namespace)
32-├── errors.py 错误码契约(异常类 + HTTP 状态注册;retry_after 语义)
33-├── util.py scope_id 派生(md5+\x00)/ deploy 指纹 / to_int 等纯函数
34-├── spec_fields.py template 字段分类:A 类 deploy 子集 / B 类策略(静态,两端共享)
35-├── session_manager/ SM 模块
36-│ ├── handlers.py 4 个 HTTP handler(route/touch/config_sync/cleanup)
37-│ ├── orchestrator.py route 编排(resolve→Lua 仲裁→acquire→等待队列) + touch
38-│ ├── state.py SM Redis 键 schema 唯一出口(含 Lua 调用封装)
39-│ ├── lua_scripts.py 7 个 Lua 全文(见 §4)
40-│ ├── sweeper.py 到期 pass(老化)+ 空 Pod pass(idle_consider)
41-│ ├── config_store.py template/routing_rule 持久化 + resolve 缓存 + config_sync
42-│ ├── facade.py SessionManagerFacade(notify_pod_dead / reconcile_pods)
43-│ └── models.py Template/ScopeConfig dataclass
44-└── resource_manager/ RM 模块
45- ├── orchestrator.py acquire(取暖/选主 deploy/封顶)+ idle_consider
46- │ + update_pool_config + cleanup + 结果幂等缓存
47- ├── state.py RM Redis 键 schema 唯一出口
48- ├── lua_scripts.py 6 个 Lua 全文(见 §4)
49- ├── k8s.py RealK8sPodClient(kubernetes_asyncio)/ FakeK8sPodClient
50- │ (deploy 等 Ready、409 重命名重试、判死归一化、/health 探测)
51- ├── sweeper.py autoscale / reclaim / watch(死 Pod+健康探测)/ reconcile
52- ├── facade.py ResourceManagerFacade(acquire / idle_consider / update_pool_config / cleanup)
53- └── models.py PodInfo / PodDeployInfo / 判死枚举 / Pod label 常量
54- 
55-scripts/deploy.sh local|server 启动
56-scripts/e2e_hld_acceptance.py 集成冒烟(场景 A–L,真环境)
57-scripts/integration_smoke.sh 冒烟入口包装
58-tests/ 114 个单测(见 §7)
59-```
60- 
61-## 3. 关键流程(读代码的切入点)
62- 
63-### 3.1 route(`session_manager/orchestrator.py:route`)
64- 
65-```
66-幂等回放(handler 层 ctx.idempotency,request_id 键)
67-→ resolve(scope)(config_store:Redis 缓存 → DB,规则优先级 精确>(g,*)>(*,b)>(*,*))
68-→ 循环 { LUA_ROUTE_PLACE 原子仲裁:
69- refresh/placed → 读 pod:info sse_url 返回
70- scope_full → LUA_WAITER_GATE 原子入队(满→503 快失败;否则订阅 free
71- 等 scope_full_timeout→504;唤醒后重跑仲裁)
72- need_acquire → rm_facade.acquire(扩 +1 Pod)→ register_pod → 重跑 }
73-```
74- 
75-### 3.2 RM acquire(`resource_manager/orchestrator.py:acquire`)
76- 
77-```
78-幂等缓存(request_id)→ LUA_ACQUIRE:
79- reuse(暖 Pod,deploy_ver 过滤)→ 返回
80- max_reached → 抛 MaxPodsReached(SM 映射 503 NO_POD_AVAILABLE)
81- need_deploy → 抢 lock:rm:deploy:{scope} 选主串行 deploy
82- (输家 → follower 等待室:准入≤pc-1/overflow 快失败/等 leader
83- Pod 注册即复用/leader 失败不接管/等待有界)
84-k8s.deploy:create + wait Ready(409 重命名重试;超时/镜像失败 → DeployFailed)
85-错误路径必须清 deploying 占位(红线)
86-```
87- 
88-### 3.3 老化与回收链(场景 D→K)
89- 
90-```
91-SessionSweeper(1s,lock:sweep):
92- 到期 pass ZRANGEBYSCORE session_expiry → 逐个 LUA_EVICT(四处同删 + PUBLISH free)
93- 空 Pod pass pods:registered 枚举 → LUA_SWEEP_IDLE_NOTIFY(原子:空判定+去重+ZREM 候选)
94- → fire-and-forget rm_facade.idle_consider → RM 转 idle 暖池
95-ResourceSweeper:
96- autoscale(1s) idle < min_idle 且未达 max_pods → LUA_PLACEHOLDER 占位 → deploy 热备
97- reclaim(1s) idle 超 min_idle 底数的 excess 中 aged ≥ pod_ttl → K8s delete + PURGE
98- + notify_pod_dead(清 SM 注册;保底热备不被回收)
99- watch(10s) 判死枚举(Terminating/Failed/CrashLoopBackOff/ImagePullBackOff/...,
100- Pending 不判死)→ 清理;Running 但 /health 连续 2 次失败 → 半死清理(场景 N)
101- reconcile(30s) Redis 有 K8s 无 → PURGE;SM 不再引用的 stale Pod → 转 idle
102-```
103- 
104-### 3.4 配置热更新(场景 M,`config_store.py:config_sync`)
105- 
106-```
107-lock:config_sync 串行化(忙 → 409 CONFIG_SYNC_BUSY)
108-→ 写 DB(失败即中止,不碰缓存——红线)
109-→ diff 判类(spec_fields.A/B):
110- A 类(deploy 子集变,deploy_ver 变)→ SM 软摘除(ZREM 老版本 Pod 出候选,存量会话不受影响)
111- + 推 RM(新 deploy_ver/pod_spec,新流量落新 Pod,自然滚动)
112- B 类(策略字段变) → DEL scope:config + 推池参数,立即生效
113-→ 完成判定:受影响 scope 仍有日落待回收的中间态 Pod → 409 拒绝下一次
114-```
115- 
116-## 4. Lua 脚本清单(所有编排态变更,原子;Redis 单线程执行无 race)
117- 
118-| 模块 | 脚本 | 一句话职责 |
119-|---|---|---|
120-| SM | `LUA_ROUTE_PLACE` | route 原子核心:亲和续期/惰性回收/闸门/first-fit/提交 |
121-| SM | `LUA_EVICT` | session 移除唯一原语(四处同删 + PUBLISH free 唤醒等待者) |
122-| SM | `LUA_TOUCH` | 保活续期(惰性 evict 兜底;ttl 就地读 session HASH) |
123-| SM | `LUA_SWEEP_IDLE_NOTIFY` | 空 Pod 判定 + 60s 去重 + ZREM 退出候选(堵竞态 A) |
124-| SM | `LUA_REGISTER_POD` | acquire 成功登记(三处注册 + 接入序) |
125-| SM | `LUA_CLEANUP_POD` | notify_pod_dead 清该 (scope,pod) 全部注册 |
126-| SM | `LUA_WAITER_GATE` | 等待队列原子入队(SADD 先行 + 超限自退;M6 修复的并发超收) |
127-| RM | `LUA_ACQUIRE` | 取暖复用 / need_deploy 占位 / max_reached |
128-| RM | `LUA_REGISTER` | deploy 成功登记(info/池/pods:all,清占位;idle_flag 入暖池) |
129-| RM | `LUA_RELEASE` | idle_consider 转 idle 暖池(起 pod_ttl 计时) |
130-| RM | `LUA_PURGE` | 清该 Pod 全部 RM key(返回其 scope) |
131-| RM | `LUA_PLACEHOLDER` | autoscale 专用占位(计入 max_pods,不碰 idle 池) |
132-| RM | `LUA_DEPLOY_FOLLOWER_GATE` | deploy 锁输家等待室原子准入(ZSET+deadline,≤pc-1;先清过期再 ZADD 先行+超限自退) |
133- 
134-约定:脚本不传 KEYS(键由 `ARGV[1]` 前缀在脚本内拼);调用统一经各自 `state.py``eval()`
135- 
136-## 5. Redis 键速查(全文见 HLD §5 两张表)
137- 
138-```
139-session_manager:session:{sid} HASH 亲和绑定(scope/pod/expiry/ttl)
140-session_manager:session_expiry ZSET 到期时间(sweeper 扫)
141-session_manager:scope:{sid}:sessions SET SCARD = scope 闸门
142-session_manager:scope:{sid}:pods ZSET first-fit 候选(接入序)
143-session_manager:scope:{sid}:config HASH resolve 缓存(config_sync DEL 失效)
144-session_manager:scope:{sid}:waiters SET 等待队列(LUA_WAITER_GATE 原子进出)
145-session_manager:scope:{sid}:free PubSub 额度释放信号
146-session_manager:pod:{scope}:{pod}:sessions|info SET|HASH per-Pod 会话 / sse_url+deploy_ver
147-session_manager:pods:registered SET "{scope}:{pod}"(不变量 5)
148-resource_manager:resource:scope:{sid}:pods|idle|config|deploying|deploy_followers
149- ZSET|SET|HASH|SET|ZSET(follower 等待室,≤pc-1)
150-resource_manager:resource:pod:{pod}:info|idle_since|health_fails HASH|STR|STR
151-resource_manager:resource:pods:all SET 全部 pod_id(watch/reconcile 枚举)
152-resource_manager:lock:rm:deploy:{sid}|autoscale|reclaim|watch|reconcile 选主/串行化锁
153-```
154- 
155-## 6. 错误码(`errors.py`,契约见 HLD §3.1)
156- 
157-| 码 | HTTP | retry_after | 场景 |
158-|---|---|---|---|
159-| `SCOPE_QUEUE_FULL` | 503 | ✅ | 等待队列满,快失败 |
160-| `SCOPE_FULL_TIMEOUT` | 504 | ✅ | 队列内等待超时 |
161-| `NO_POD_AVAILABLE` | 503 | ✅ | acquire 失败(MaxPodsReached/DeployFailed 映射) |
162-| `CONFIG_NOT_FOUND` | 503 | ❌ | resolve 无匹配规则/模板禁用 |
163-| `VALIDATION` | 400 | ❌ | 参数错 |
164-| `CONFIG_SYNC_BUSY` | 409 | — | 上一次热更新未完成 / 日落待回收 |
165- 
166-Facade 间以 Python 异常传播,handler 捕获后映射为错误信封。
167- 
168-## 7. 测试与验收
169- 
170-> 全部 e2e 用例的场景/输入/预期输出逐条说明见 **`docs/e2e-test-cases.md`**
171- 
172-```bash
173-cd applications/agent_runtime
174-uv sync --extra local && uv run pytest # 114 用例,fakeredis+SQLite+FakeK8s
175-./scripts/integration_smoke.sh # 真环境冒烟(场景 A–L;FLUSHDB 目标库,有防误刷)
176-```
177- 
178-| 层 | 文件 | 内容 |
179-|---|---|---|
180-| SM 状态层 | `tests/session_manager/test_sm_state.py` | Lua 原子语义(亲和/first-fit/闸门/老化/幂等) |
181-| RM 状态层 | `tests/resource_manager/test_rm_state.py` | acquire 占位/封顶/暖池/PURGE |
182-| SM config 层 | `tests/session_manager/test_config_store.py` | resolve 优先级/缓存/AB 类 diff/串行化/DB 失败红线 |
183-| RM 业务 | `tests/resource_manager/test_rm_business.py` | watch/健康探测/reconcile/cleanup/update_pool_config |
184-| 组件全链路 | `tests/integration/test_route_flow.py` | 场景 A–K 全链路(Fakeredis 共享态) |
185-| 分支/corner | `tests/integration/test_corner_cases.py` | 边界与异常分支(18 项) |
186-| HTTP 冒烟 | `tests/integration/test_http_smoke.py` | 4 端点契约 + 错误码映射 + /healthz |
187-| **双实例多副本** | `tests/integration/test_multi_replica.py` | 跨副本确定性语义(12 项,`_dual_harness.py` 同进程两 App 共享一组资源) |
188-| 集成冒烟 | `scripts/e2e_hld_acceptance.py` | 真 Redis/MySQL/K8s,65 项断言;场景 N 暂缓(AgentServer /health 未支持) |
189-| 多副本 e2e | `scripts/e2e_multi_replica.py` | 真 LB 单入口 35 项(选主互斥/突发/幂等/传播/failover);<2 实例自动 DEGRADED |
190-| 压测/浸泡 | `scripts/load_test.py` | 场景化(route/route_touch/queued),分位数+错误直方图,周期浸泡报告 |
191- 
192-- 双实例 harness 要点:`create_app(resources=..., instance_id=..., own_resources=False)` 注入共享资源;httpx `ASGITransport` 单事件循环驱动(**先手动驱动 lifespan**,否则 RestAdapter 惰性二建 sysctx 绕过后台 Job);两个 TestClient(双事件循环共享 fakeredis)不可行。
193-- 经多副本 LB 亦可跑(实测 65/65):部署须带 `AGENT_RUNTIME_SCOPE_FULL_TIMEOUT`(模板默认 8,显著小于 session_ttl——漏设曾致等待者 deadline 与会话到期碰撞,F 阶段混合结果);排查实录与 cleanup 空目标三坑见 `docs/e2e-test-cases.md` §8.1。
194- 
195-## 8. 配置与部署
196- 
197-- 启动:`scripts/deploy.sh local|server [env-file]`;server 读 `.env.production.local`(模板 `agent_runtime.server.env.example`)。
198-- 框架配置(`OPENJIUWEN_SERVICE_*`):host/port/Redis URL/DB。服务自有配置(`AGENT_RUNTIME_*`):任务周期、scope_full_timeout、kubeconfig、default_namespace——见 `config.py`。
199-- server 模式硬要求:Redis 开 AOF/RDB;DB 用 MySQL/PostgreSQL(fail-fast,禁 SQLite 回退)。
200-- 双模式实现:`main.py:build_resources`(local=fakeredis+SQLite+FakeK8s;server=真客户端);K8s 层 Real/Fake 同签名(`k8s.py:K8sPodClient`)。
201-- **多副本宿主机**:`scripts/deploy_replicas.sh N [env] [port]`(N 进程共 Redis/DB,`/healthz` 就绪轮询,trap 清理;local 模式 fail-fast)。
202-- **K8s 生产形态**:`deploy/` 目录——`agent_runtime.template.yaml`(SA+Role×2+Deployment 多副本/反亲和//healthz 探针+ClusterIP Service LB)、可选 NodePort(30091)、`Dockerfile`(**build context=仓库根**,保 `../../foundation`/`../../service` 布局,`uv sync --frozen --extra server --no-dev`,logs/ 须预建归 appuser)、`render_and_apply.sh`(env 渲染→apply,残留 `<<` 即 fail-fast)、`build_image.sh`
203-- K8s 部署红线:`OPENJIUWEN_SERVICE_DEPLOY_REPLICAS=1` 固定(副本数=Deployment replicas);RBAC 两份(服务 ns + AgentServer 目标 ns,否则 create pod 403→route 全 503);Pod 内 MySQL 用户须授权 Pod CIDR(`'agent_runtime'@'10.244.%'`)。
204- 
205-## 9. 改动时的注意事项(高频踩点)
206- 
207-- **改键名/Lua**:HLD §5 键表与两个 `state.py` 必须同步;Lua 全文在 SM/RM 详细设计与 `lua_scripts.py` 双份,同步改。
208-- **新增 template 字段**:先分类(`spec_fields.py`:A 类进 deploy 子集与指纹,或 B 类策略),再补 `config_store.py``_COLUMN_OF` 列映射(DB 列名沿用 EE 兼容名)与表结构 `*_TABLE_DEF`
209-- **等待队列**:入队只准走 `LUA_WAITER_GATE`;「先查后加」是已被真环境验收证伪的写法。
210-- **跨模块**:SM↔RM 数据只走 Facade 方法,不直读对方 Redis key(架构红线)。
211-- **测试环境**:构造 `ServiceManager``deploy_mode="subprocess"`;fakeredis pubsub 需共享同一实例。
212-- **真环境验收**:独立 namespace + 独立 Redis DB(冒烟脚本自带防误刷与前置自检)。
213-- **多副本踩点**:选主元数据键 TTL ~3s,观测/采样须 ≤0.5s 间隔(SCAN);`instance_id` 前缀=hostname(容器内即 Pod 名),可反查副本。跨副本冷竞争时 deploy 锁输家会自建第 2 个 Pod(`max_pods` 内,空 Pod 经 empty-pod pass→idle_consider→reclaim 自愈)——测试断言「窗口零重叠+Pod≤max_pods」,不断言「恰好 1 个 Pod」;多后端冷突发 NO_POD_AVAILABLE 快失败属预期。`config_sync` 全局串行锁:脚本播种必须串行,并发即 409。框架 `load_incluster_config`**同步**函数(`k8s.py` 已修,M7 首次在 in-cluster 暴露)。部署须设 `AGENT_RUNTIME_SCOPE_FULL_TIMEOUT` 且显著小于 session_ttl(等待者 deadline≈会话到期会被到期驱逐唤醒,产生 200/503 混合而非干净 504)。
214-- **cleanup 踩点**:目标 ns 不存在时,宿主机 admin 凭据得空列表、in-cluster SA 的 namespaced RBAC 得 403——产品已对 404 容忍为 cleaned=0、403 保持 fail-fast;测试的「空目标」必须用无匹配 label_selector(业务 ns 会误删同 label 真实 AgentServer;min_idle 模板的热备 1s 内重建,刚清空的 ns 也不为空)。