AGENTS.md@internal/http · git:20261023.dd5a6e4 · 2026-10-23 · sha256 8d11adccc8eb4767
AGENTS.md@internal/http git:20261023.dd5a6e4A
Immutable. This exact content never changes and is served at /api/v1/blob/8d11adccc8eb4767.
# internal/http — REST / WebSocket 与任务生命周期 > 范围:本目录内不可派生的约束。全局见仓库根 `AGENTS.md` §6.9。 ## 边界 gin REST + melody WebSocket + `TaskManager` 的任务生命周期。不负责暂停语义本身(在 `internal/pause`),不负责 Agent 装配(在 `internal/app`)。 ## 硬约束 **1. 每个建任务 ctx 的地方都必须走 `NewTaskContext`**(`task_gate.go:8-30`)。闸门与并发域 **只挂在 ctx 上、不在结构体里另存一份**:`Task.Gate()` 是从 ctx 读回来的,`supervise.From(ctx)` 同理。 两处各存一份的话,任何"换了 ctx 忘了换闸门"的地方都会让 `/pause` 停在一个没人在等的闸门上—— 那条路径没有报错,只是什么都不发生。漏用 `NewTaskContext` 不会静默失败(`/pause` 会拒绝并说明), 但表现是"这一条路径上的任务从来停不了",而它正是最常被新增的那条路径。 **2. 续跑分支换 ctx 就是换闸门**(`server.go:244-247`):旧闸门属于已经结束的那一轮, 留着只会停错对象。 **3. 锁不许跨过 IO。** 6 个状态点(`SetTaskResult/Error/Progress` + pause/continue/cancel)在 `tm.lock` 内**只编字节**,`taskAction.deliver` 在 Unlock 之后做 `Socket.Write`/`Broadcast` (`task_manager.go:95-140,168-190,247-282,291-322,326-344`)。理由是一个卡住的浏览器标签能顶住 全部状态更新与读锁。要测这条不变量得能把 IO 卡住,所以有 `writeSession` / `melodyBroadcast` / `flushHook` 三个 seam——**探针必须用不需要投递的路径**,否则带 socket 的 setter 会停在它自己的 慢客户端上,那测的不是"别人的锁压着"(`task_manager_concurrency_test.go:17-42`)。 **4. `paused` 是第五档状态**,因为闸门不产生 `task_complete`(`types.go:70-79`)。 `ContinueTask` 的判据是"状态仍 paused **且**闸门仍按着",而不是"闸门按过"(`task_manager.go:286-291,305-310`)—— 暂停后被取消的必须显式拒绝。`Blocked` 在按下瞬间必为 false,不许骗人(`:270-279`)。 **5. 任务表终态过期只在 `CreateTask` 时清扫**,不起定时 goroutine;`running/paused` 一律不动 (`task_manager.go:64-80`)。`taskRetention` 是 seam(`:31-33`)。 **6. 每任务一个调度器且走 `NewDefaultMessageDispatcher()`**(`task_executor.go:35`、`websocket.go:182`)—— 容量只有一处定义(根 AGENTS.md §11 坑 35)。收尾必须 `Shutdown + WarnDropped + Drain 任务域` (`task_executor.go:43-50,103-121`):不等的话周期保存会带着已取消的 taskID 继续写文件。 **7. `seq` / `delegation_id` 放进 realtime envelope 的 `Data` map,不改 `protocol.AgentEventEnvelope`**(`task_executor.go:72-77`)——那是生成物,且里面还有生成器不产出的 手写类型(见 `internal/AGENTS.md` 的 protocol 一节)。WS 向**所有**连接广播,没有按会话过滤。 ## 入口 `task_gate.go`(最短的契约文件)、`task_manager.go`、`task_executor.go`。 ## 闸门 `task_pause_test.go:37 TestTaskContextCarriesSuperviseDomain`(两条任务共用一条域即红)、 `:69 TestTaskContextCarriesGate`(改成结构体另存一份即红)、`:99`、`:137 TestPauseContinueRoundTrip`、 `:182 TestContinueRefusesCancelledWhilePaused`; `task_manager_concurrency_test.go:46 TestSlowSocketDoesNotBlockTaskManager`、 `:93 TestPauseBroadcastDoesNotHoldTaskManagerLock`、`:136`。 **并发域的第二条纪律**:跑任务的那条 goroutine **不进任务自己的域**(`supervise` 挂在 ctx 上、 收尾时 `Drain`,把执行体自己算进去就是收尾时等自己 → 死锁)。