issue #10 的两项修复已在主分支落地并实测(feat/agentenv-integration @ a7c6ca0,58 包全绿):
修复 1(方案 A):e2bentry/adapters.go 创建时 MemoryBytes<=0 → 512MiB 默认(与 agent materializer 同源)。kind 实测:零内存 passthrough create 201 → manifest memoryBytes=536870912;显式值恒保留。plan scan 的 5 个沙箱全部带估算(零 UnknownMemoryEstimate)。
修复 2(第二层 CAS):migrate-backed Start 只 CAS plan 文档。kind 实测:balance plan started (migrate-backed) 稳定出现(修复前每轮 PlanFound→BLOCKED concurrent budget change)。
连带发现并修复的三个结构性缺陷(迁移跑通前必倒的三张牌,均已实测确认并修复):
- ReserveTx 锁内重验不认自己的栅栏:protectBalanceSource 给源节点设 PlanOwner,乐观评分有 own-operation 旁路、锁内重验没有 → 迁移自己的重预订撞自己的栅栏 → ErrNoSafeCandidate×16 → s3-resume 必失败。修复:锁内重验对 manual-migration/balance-migration kind + 匹配 PlanOwner 的旁路(stranger 仍拒)。
- resume 双重预订:executor ReservePinned 已持 hold,S3 的 balanced-resume 又重预订 → SandboxOperationInProgress。修复:PrepareLocalResume 对 hold-already-held 短路。
- 迁移 resume 裸 spec:无 image/cpu → agent 500。修复:resumeSpec 从 manifest 补全。
验证状态(对应 6.3 的缺口):调度侧 S1(gate)/S2(pause)/S3(resume)全链路真实打通,S4 精确命中 AgentENV mock 结构性缺口:backend does not support capturing a snapshot from a persisted paused state(三重重复=确定性拒绝)——S4-S10 闭环归属 AgentENV mock 实现(pause-first 架构下 persisted-paused 捕获未实现),issue 材料已备(精确错误+复现路径)。回滚/cooldown/dead-letter/paused 落地全部实测。
benchmark 重部署提示:主分支含全部修复(ede4a92..a7c6ca0 共 9 笔);rsync 临时树与本分支的实现有差异(如 placement 三处兜底在本分支不存在——本分支对显式零内存保持 placement 拒绝语义,由 adapter 层兜底),建议以本分支为准重新构建部署。


六、迁移不发生的第二层根因(CAS 冲突)——已修复
在方案 A(创建时写 512MiB 默认内存)部署后,
plan scan不再UnknownMemoryEstimate(300 沙箱全部cand=1 rel=536870912),spread 超阈值后进入PendingBalance/PlanningRequired,且每次SCAN moves=1 reason=PlanFound(找到迁移方案),但紧跟BLOCKED balance: concurrent budget change,execution 文档始终只有begin(无任何 pause/snapshot/transfer 动作)——迁移方案每次找到却永远无法提交。6.1 根因
internal/balance/v2/migrate_engine.go的Start()(CAS 提交 plan 时):docs := append([]m.BalanceDocument(nil), snap.Documents...) // 连带 node 文档 payload, _ := json.Marshal(p) docs = append(docs, m.BalanceDocument{Kind: "plan", ID: "active", Version: d.Version, Payload: payload}) ok, err := e.Ledger.DB.CompareBalance(ctx, docs) // CAS 整个快照snap.Documents含 2 个 node 文档(ledger.go:110只读 node 文档)。而 nodeagent 每 5s 上报一次物理内存样本(Report()→CompareBalance更新 node 文档 version),实测 node 文档 version 从 727/376 在几分钟内涨到 752/399。compareBalance对每个提交文档做UPDATE ... WHERE version = ?乐观 CAS(metadatastore/balance.go:120),只校验提交列表内的文档。migrate-backedStart不修改 node 文档(无 protocol engine 的 PlanOwner 机制),却连带 CAS 了高频变化的 node 文档版本 → 读取快照后到提交之间 node 文档必被 nodeagent 更新 → CAS 必败 →ErrConflict→planning blocked。6.2 修复
migrate_engine.goStart()的 CAS 改为只提交 plan 文档(CompareBalance只传 plan 文档),不再连带 node 文档。payload, _ := json.Marshal(p) ok, err := e.Ledger.DB.CompareBalance(ctx, []m.BalanceDocument{ {Kind: "plan", ID: "active", Version: d.Version, Payload: payload}, })对比:protocol engine(
migration/engine.go:90-114)的Start会修改 node 文档(设置PlanOwner),所以它必须连带 node 文档 CAS;migrate-backed 不修改 node 文档,连带 CAS 是多余且必然失败的。Advance的save()本就只 CAS 单个 plan 文档(低频变化),不受影响。6.3 验证状态
fix-cas-plan镜像,rollout 成功,scheduler 正常(spread 0.027 Satisfied)。fix-cas-plan部署后 2 小时内,spread 从未超过 0.1 阈值(最大 0.0597,155 次 observation 全Satisfied/SpreadWithinThreshold),迁移路径从未被真正触发——plan started→move completed尚无一次实测。此前的PlanFound → BLOCKED concurrent budget change是在旧镜像(连带 node 文档 CAS)下的现象;新镜像下没有过一次真实的PlanningRequired → Start → Advance机会。plan started→move completed→ spread 回落,才能闭环验证 CAS 修复。用户当前不注入压力,验证留待自然偏斜或用户确认的样本注入方式。一、问题现象
Satisfied/SpreadWithinThreshold158 次、PendingBalance/PlanningRequired72 次、PendingBalance/AwaitingOperationObservation122 次。balance plan scan skips每次列出 356 个沙箱全部跳过,仅两种原因:UnknownMemoryEstimate(绝大多数)LifecycleOwned(少数)二、根因(数据实锤)
2.1 迁移规划被阻断的核心判定
planner.go:116-120:if it.Resources.MemoryBytes <= 0 { skip "UnknownMemoryEstimate" }algorithm.go:222(Search 迁移候选):// 依赖 sb.Memory.Known && sb.Memory.Movable && Release>0 && Arrival>0 // 无内存估算则无法构造 Move2.2 数据库全量统计
sandbox_manifest表 356 个 manifest 全量检查(MySQL 实测):faceOrigin: e2bresources为空对象或memoryBytes=0抽样 manifest 实况:
"resources": {}(空),faceOrigin: e2b,launch.image.uri 为swb-bombsimon-wsl-108。2.3 创建链路全程不携带内存(写入链路实锤)
E2B image-passthrough 创建的完整链路,resources 全程为空:
internal/schedv2/e2bapi/types.go:97-126—NewSandbox请求体根本没有 resources 字段(openapi 官方 E2B schema 不带资源),SDK 无从传入内存信息。internal/schedv2/e2bapi/create.go:209-238—createEntryRequest组装e2bentry.CreateRequest时不设置 Resources(保持零值)。internal/schedv2/e2bentry/adapters.go:43-46—CreateRequestToIntent把零值直接拷进intent.Resources{CPUMillis:0, MemoryBytes:0}。internal/schedv2/e2bentry/entry.go:538-568—buildIntentManifest把 intent JSON 原样持久化为spec_json→ 落库resources:{}。internal/balance/v2/planner.go:116-120— 迁移规划读到MemoryBytes<=0→UnknownMemoryEstimate→ 356/356 全跳过。2.4 为什么"创建能跑、迁移不能跑"
placement(创建选点)路径三处都有兜底,值统一为 512 MiB:
internal/balance/algorithm/algorithm.go:133-139requested<=0 → defaultMemoryEstimate(512<<20)internal/balance/ledger/placement.go:70-71requested<=0 → defaultMemoryEstimateinternal/balance/ledger/placement.go:204-205requested<=0 → defaultMemoryEstimateagent 侧运行时同样兜底:
internal/agent/server/materializer.go:546memoryBytes<=0 → defaultMaterializeMemory("512Mi")而迁移路径读 manifest 持久化的真实 spec(
MemoryBytes=0),没有任何兜底 → 全跳过。结论:创建路径与迁移路径对内存信息的依赖不同,导致"检测到不平衡但无米下锅"。
三、修复方案
方案 A(推荐,治本):创建时写入默认内存估算
改动点:
internal/schedv2/e2bentry/adapters.go:43-46,在CreateRequestToIntent构造intent.Resources时,若MemoryBytes<=0则填 512 MiB(复用与 placement/agent 相同的默认值)。Resources: intent.Resources{ CPUMillis: req.Resources.CPUMillis, MemoryBytes: req.Resources.MemoryBytes, }, // ↓ 改为 Resources: intent.Resources{ CPUMillis: req.Resources.CPUMillis, MemoryBytes: req.Resources.MemoryBytes, }, // 若 MemoryBytes <= 0 → 填 defaultMemoryEstimate (512MiB)效果:
UnknownMemoryEstimate)。风险:极小,一处函数改动。
方案 B(治标,不推荐):planner 兜底
改动点:
internal/balance/v2/planner.go:116遇MemoryBytes<=0按 512 MiB 估算。效果:迁移立即可跑。
风险:掩盖 manifest 数据缺失;规划器凭空猜内存,与落库数据不一致。
方案 C(最精确,暂不可行):模板元数据解析
改动点:从 e2b template 读内存。
效果:最精确。
风险:schedv2 无 Template Manager 对应(已登记 gap),无可靠元数据来源。
四、实施与验证计划(方案 A)
internal/schedv2/e2bentry/adapters.go(MemoryBytes<=0 → 512MiB)。/home/k8s-data/build/SandboxSched/。90.91.181.239:5000。resources.memoryBytes非 0。plan scan skips不再出现UnknownMemoryEstimate。Satisfied。五、已知边界