Documentation
¶
Overview ¶
adapter.go —— opencode 语义到 executor.Adapter 契约的翻译层。
职责:
- 把 StartServe/CreateSession/PromptAsync/SubscribeEvents 编排成 Adapter 的 五动作:Start(环境物料 → serve → 会话 → 初始 prompt → 订阅映射)、 Send(同一会话续接)、RespondPermission(权限应答转发)、 Stop(kill serve + 关事件流)、Events(事件通道)
- SSE 事件 → AdapterEvent 映射:permission.asked → permission 事件;模型 文本(message.part.updated / message.part.delta)增量累积 → render.log 追加 + 节流 progress;session.status idle → turn.ParseTrailer 分类(ask/finish/none 兜底 git 实况裁决);serve 死亡 → failed result
- 可见性:回合文本增量追加到 <taskDir>/render.log,供 handoff attach (render 流式 endpoint)旁观模型执行
边界:
- 不写 store、不做审批判断(见 executor.go 包级边界):会话 id 等一切持久化 诉求经事件(progress「会话就绪」/ Result.SessionID)或返回值交给 manager 落库
- 不做任务状态机迁移:6 状态迁移完全由 manager 负责,本层只产事件、收指令
- 不重试、不决策:SSE 解析宽容(未知事件 Debug 跳过、绝不 panic); trailer 缺失时兜底只做「是否有新提交」的事实裁决,没有新提交就交协调者
事件映射以真实 SSE 样本为准(spike3/spike5,opencode 1.18.15 serve 模式):
- 文本载体:模型文本走 message.part.updated(properties.part.type=text, 带该 part 全量文本快照)与 message.part.delta(properties.field=text, properties.delta 增量),reasoning/tool 等非 text part 的增量隔离不进 回合;message.updated 只有 properties.info(role/messageID),不带文本, 仅用于探测新回合开始(role=user 首次见到该消息 id 时清空累积,同 id 重发忽略——session.diff 广播后服务端会重发同一 user 消息)
- 回合结束主信号:session.status 的 properties.status.type=idle;同现的 session.idle 与顶层 idle/busy 事件全部忽略,防重复触发分类
- 权限:permission.asked(properties.id 即 PermissionID,properties.permission/ patterns/metadata 拼描述);permission.replied 是应答回显,必须忽略
- 其余类型(server.connected/heartbeat、session.updated/diff、catalog.updated、 integration.updated、reference.updated、plugin.added、step-start/step-finish、 tool/reasoning 等)宽容 Debug 跳过
Package opencode 提供 opencode server 的最小 HTTP/SSE 客户端与 serve 进程管理。
api.go —— opencode server 的 HTTP 客户端与 SSE 事件订阅。
职责:
- 覆盖 MVP 所需的四个端点:POST /session、POST /session/{id}/prompt_async、 POST /session/{id}/permissions/{permID}、GET /event(SSE 事件流)
- 统一的 basic auth(用户名固定 opencode,密码来自 OPENCODE_SERVER_PASSWORD)
- SSE 断流自动指数退避重连(1s→2s→…→30s),直到 ctx 取消
- 两个 http.Client 分工:sseClient 无 Timeout 供 SSE 长连接,httpClient 带 Timeout=30s 供一元调用(半死 server 不永久挂起,why 见 NewAPI 注释)
边界:
- 不理解事件语义:事件类型字段含义、回合结束判定等语义归 adapter(Task 9), 本层只保证「事件 JSON 原样送达 onEvent」与「垃圾行不中断订阅」
- 不管理进程生命周期(见 proc.go)
为什么 SSE 手写解析而不引第三方依赖:SSE 协议只有 data:/event:/空行三种形态, 手写按行解析约 40 行即可完全掌控。事件 payload 可能携带大段文本(plan、diff、 用户问答),必须能按需配置 scanner buffer(本实现 1MB)并按行处理超长 token; 第三方库的 buffer 上限与截断行为不可控,且我们需要的「未知行 Debug 跳过、绝不 中断」宽容语义由自己实现最精确,不依赖库的严格/宽松差异。
probe.go —— 只读存活探测。
职责:
- Probe:读 proc.json,走 Proc.Alive 的既有判据(存活锁 + HTTP 应答), 如实返回存活结论
边界:
- **绝不写**:不回收执行者进程、不占 runs 位、不碰 store、不发事件
- 不打印 procInfo.Password:凭据值绝不进日志,要打只打非敏感字段
proc.go —— opencode serve 进程生命周期管理。
职责:
- 组 `opencode serve --port <随机空闲端口> --hostname 127.0.0.1` 的 argv, 经 prochost 以 detached 方式拉起(shim 承载,见 prochost 包)
- 密码与配置经 Spec.Env 注入(OPENCODE_SERVER_PASSWORD / OPENCODE_CONFIG), argv 不含任何秘密(why 见 serveSpec)
- serve 输出落盘 <taskDir>/serve.log:serve 死亡诊断的持久 stderr
- 就绪探测(轮询 GET /)、存活检查(存活锁 + HTTP 探活)、销毁(按进程组 Kill)
边界:
- 不触碰会话:会话的创建、prompt、权限应答由 api.go 完成,本文件只保证 「serve 进程活着、端口可用」
- 不生成任务级配置(OPENCODE_CONFIG 指向的文件由 Task 10 生成)
为什么进程经 prochost 而不是 agentd 直接 fork:agentd 重启或崩溃时子进程 树会被一并回收,正在执行的任务会无辜中断;prochost 的 shim 以新会话拉起并 持有存活锁,生命周期与 agentd 解耦——agentd 重启后靠 Alive() 探测发现存活 并重连 SSE。实况观测走 agentd 的 render 流式 endpoint(handoff attach)。
Package opencode 的提问通路纯逻辑。
职责:
- renderQuestionTicket:把 opencode 的 question 请求渲染成协调者可读的工单文本
- parseQuestionAnswers:把协调者的自由文本答复折算回 opencode 要的 answers
边界:
- 全部是纯函数:不碰 runState、不发 HTTP、不打日志、不读时钟
- 不认识 SSE 事件结构(解析在 adapter.go 的 mapQuestionAsked 里做)
- 不做截断(由调用方用 turn.ClampQuestion 收口)
reap.go —— 无内存运行态时的确定性兜底回收。
职责:
- Reap:按 proc.json 拿 prochost.Handle 并 Kill
边界:
- 不碰任务状态(adapter 不写 store);回收不掉只返回错误,留不留事件是 manager 的事
reconcile.go —— 断连窗口的会话对账(B38)。
职责:
- Reconcile:查会话尾部与持久化水位比对,把断连期间错过的回合终态补回事件流
边界:
- 不写 store、不改任务状态:补发的事件经既有 evCh 交给 manager,状态迁移归它
- 不发明事件语义:取回的文本交给既有的 turn.ParseTrailer 分类,产出与实时 路径同形的 question / result
- **不捧回权限请求**:opencode 的消息流里 tool part 只有 callID 没有权限 id, 而 RespondPermission 要求真实 id、伪造即 404(更早的 spike 结论,见 adapter.go 的 onReconnect 降级告警)。建一张批了也送不回去的工单比不建更糟, 故 ReconcileOutcome.Pending 在本 adapter 恒为 0
resume.go —— 执行恢复:热重连与冷恢复。
职责:
- Resume:按 ResumeReq 走恢复阶梯,返回实际走到的级别
边界:
- 不判断「该不该恢复」的业务前提(如是否有未决权限工单)——那需要工单知识, 属 manager(见 manager.go 的 volatilePermitter)
- 不改任务状态:adapter 不写 store(见 executor 包级边界)
taskenv.go —— opencode 任务环境物料生成。
职责:
- WriteTaskEnv:在任务目录生成 opencode.json(权限收敛配置)与 prompt.md (回合制纪律 prompt,经 turn.RenderPrompt 渲染),供 serve 进程经 OPENCODE_CONFIG 注入(proc.go)与任务首回合 prompt 使用
边界:
- 不启动进程、不发请求:serve 进程生命周期在 proc.go,HTTP 会话在 api.go
- 回合协议(prompt 模板渲染 / trailer 解析 / git 取证 / 文本截断)在 internal/executor/turn 共享包,本文件不再持有
为什么 permission 是「静态分级」而非全 ask(2026-08-08 dogfooding 修正): 一期曾把 edit/bash 全部设为 ask,真实派发时协调者被 ls/grep/编辑测试文件 这类初级请求连环唤醒,审批噪音让审核流形同虚设——这恰是用户交互式用 opencode 时不存在的问题(用户在场且全局配置宽松)。修正后的分层:
- edit: allow —— 在任务分支上改代码是派发的目的本身,diff 审核兜底; edit 保持 allow(2026-08-09 真机探针复核):越界写入由 external_directory: "ask" 拦截并升级人工,范围内写入本就该直接放行。 翻成 ask 等于给每次正常编辑加一道判完还是放行的空门(B27 复核结论, 见 docs/superpowers/plans/2026-08-09-permission-payload-probe.md §3.1)。
- bash: 模式表 —— 危险模式(rm -rf/sudo/git push/reset --hard/--force/ curl/wget 等,见 bashPermissionRules)ask,其余 allow;
- webfetch/external_directory: ask —— 外访与越出工作区仍逐次确认。
这是三级审批链的第 0 层(静态规则);第 1 层(廉价模型审批者)见二期 spec, 第 2 层是协调者/用户本人。
Index ¶
- Variables
- func WriteTaskEnv(taskDir, taskID, model, planContent string) (configPath, promptPath string, err error)
- type API
- func NewAPI(baseURL, password string) *API
- func NewAPIWithSSEBackoff(baseURL, password string, initial, max time.Duration) *API
- func NewAPIWithSSETiming(baseURL, password string, initial, max, stableAfter time.Duration) *API
- func NewAPIWithUnaryTimeout(baseURL, password string, timeout time.Duration) *API
- func (a *API) CreateSession(ctx context.Context) (sessionID string, err error)
- func (a *API) GetSession(ctx context.Context, sessionID string) (d sessionDetail, err error)
- func (a *API) HasSession(ctx context.Context, sessionID string) (ok bool, err error)
- func (a *API) LastAssistantMessage(ctx context.Context, sessionID string) (msg *SessionMessage, err error)
- func (a *API) ListPendingQuestions(ctx context.Context) (out []PendingQuestion, err error)
- func (a *API) PromptAsync(ctx context.Context, sessionID, text string) (err error)
- func (a *API) RejectQuestion(ctx context.Context, requestID string) (err error)
- func (a *API) ReplyQuestion(ctx context.Context, requestID string, answers [][]string) (err error)
- func (a *API) RespondPermission(ctx context.Context, sessionID, permID, response string) (err error)
- func (a *API) SubscribeEvents(ctx context.Context, onEvent func(json.RawMessage), onReconnect func()) error
- type Adapter
- func (a *Adapter) Events(taskID string) <-chan executor.AdapterEvent
- func (a *Adapter) Probe(req executor.ProbeReq) (executor.ProbeOutcome, error)
- func (a *Adapter) ProcHandle(taskID, taskDir string) (prochost.Handle, error)
- func (a *Adapter) Reap(taskID, taskDir string) error
- func (a *Adapter) Reconcile(ctx context.Context, taskID string) (executor.ReconcileOutcome, error)
- func (a *Adapter) RespondPermission(ctx context.Context, taskID, permID, decision string) (err error)
- func (a *Adapter) Resume(req executor.ResumeReq) (out executor.ResumeOutcome, err error)
- func (a *Adapter) Send(ctx context.Context, taskID, text string) (err error)
- func (a *Adapter) Start(ctx context.Context, req executor.StartReq) (err error)
- func (a *Adapter) Stop(taskID string) (err error)
- type PendingQuestion
- type Proc
- type QuestionInfo
- type QuestionOption
- type SessionMessage
Constants ¶
This section is empty.
Variables ¶
var ErrCustomAnswerRejected = errors.New("opencode 拒绝了自定义答案")
ErrCustomAnswerRejected 表示 opencode 拒绝了本次 reply 携带的答案——最可能的 原因是该问不接受自定义答案(服务端按选项 label 白名单校验)。
为什么要一个专门的哨兵:协调者填了一个不在选项里的答案时,调用方要把它 降级成「重问」而不是报一个语焉不详的 HTTP 错误。只有 4xx 归入本哨兵, 5xx 是服务端故障,与答案内容无关(见 ReplyQuestion)。
Functions ¶
func WriteTaskEnv ¶
func WriteTaskEnv(taskDir, taskID, model, planContent string) (configPath, promptPath string, err error)
WriteTaskEnv 在 taskDir 生成 opencode 配置与任务 prompt,返回二者路径。
参数:
- taskDir: 任务工作目录(须已存在,由调用方保证)
- taskID: 任务 ID,写入 prompt 标题行
- model: 任务级模型覆盖(dispatch --model 折算而来);空则回退环境变量
- planContent: 实现计划全文,原样嵌入 prompt 的「实现计划」段
返回:
- configPath: 生成的 opencode.json 路径
- promptPath: 生成的 prompt.md 路径
- err: 渲染或写文件失败
注意:
- 重复调用幂等覆盖:同名文件会被新内容覆盖,调用方可安全重试
- 配置经结构体 marshal、prompt 经 text/template 渲染,均非字符串拼接
Types ¶
type API ¶
type API struct {
// contains filtered or unexported fields
}
API 是 opencode server 的最小 HTTP 客户端,持有 baseURL 与鉴权密码。
并发安全:字段构造后只读,可被多个 goroutine 同时使用。
func NewAPI ¶
NewAPI 创建 opencode server 客户端。
参数:
- baseURL: opencode serve 的地址(如 http://127.0.0.1:4345),尾斜杠会被剥掉
- password: OPENCODE_SERVER_PASSWORD 的值,与用户名 opencode 拼成 basic auth
func NewAPIWithSSEBackoff ¶
NewAPIWithSSEBackoff 是 NewAPI 的 SSE 退避可注入变体:测试注入毫秒级退避, 让「成功连接后复位」的时间敏感断言不依赖真实 1s..30s 节奏;生产代码一律走 NewAPI 的默认退避。
参数:
- initial/max: SSE 断流重连的初始/封顶退避(见 SubscribeEvents)
func NewAPIWithSSETiming ¶
NewAPIWithSSETiming 在退避区间之外再注入「连接算健康」的存活门槛, 供「退避复位按连接寿命而非按 200 响应」(A-8)的断言把门槛压到毫秒级。
参数:
- stableAfter: 连接存活多久才算健康、才复位退避(生产默认 sseStableAfter)
func NewAPIWithUnaryTimeout ¶
NewAPIWithUnaryTimeout 是 NewAPI 的超时可注入变体:测试注入毫秒级短超时验证 「半死 server 不永久挂起」;生产代码一律走 NewAPI 的 30s 默认值。
参数:
- timeout: 一元调用(httpClient)的超时;SSE 长连接(sseClient)不受影响
func (*API) CreateSession ¶
CreateSession 在 opencode server 上创建会话。
返回:
- sessionID: 新建会话的 id,后续 PromptAsync / RespondPermission 都需要它
- err: 请求或解析失败
func (*API) GetSession ¶
GetSession 取单个会话的详情,用于把子会话归属回父任务。
参数:
- ctx: 上下文;调用方负责叠加 ownershipTimeout
- sessionID: 目标会话 id
返回:
- sessionDetail: 会话详情
- err: sessionID 为空、请求失败、非 2xx、响应解析失败时非 nil,此时详情为零值
注意:
- sessionID 为空直接返回错误,不触达服务端:拿空 id 拼出的 "/session/" 只会 换来一个 404,白白占掉一次超时预算
- 本方法在 SSE 事件回调里同步调用(见 adapter.resolveChildSession), 阻塞的是本任务的事件流——超时必须用 ownershipTimeout 而非 unaryTimeout
func (*API) HasSession ¶
HasSession 检查指定会话 id 是否仍存在于 serve 的会话列表里。
返回:
- ok: 会话是否在场;GET /session 失败或列表解析失败时返回 (false, err)
注意:
- 本方法只用于冷恢复的会话在场校验(spec §5.5.2):会话存在全局 sqlite, 进程重起不影响它,但要确认它真的还在——不能默认。不在就降级新会话
func (*API) LastAssistantMessage ¶
func (a *API) LastAssistantMessage(ctx context.Context, sessionID string) (msg *SessionMessage, err error)
LastAssistantMessage 取会话里最后一条 assistant 消息(对账的数据源,B38)。
参数:
- ctx: 控制单次请求超时
- sessionID: 目标会话
返回:
- (*SessionMessage, nil): 找到了。CompletedMS==0 表示该回合仍在进行
- (nil, nil): 会话里还没有任何 assistant 消息——**合法状态,不是错误**
- (nil, err): 请求或解析失败
注意:
- 只看最后一条 assistant 消息就够,依据是「一个断连窗口内至多跨越一个回合 边界」(spec §2.2);不需要全量拉取比对
- 权限请求**查不回来**:本端点的 tool part 只有 callID 没有权限 id,而 RespondPermission 要求真实 id、伪造即 404(更早的 spike 结论,见 adapter.go 的 onReconnect 降级告警)。故本方法不尝试提取权限
func (*API) ListPendingQuestions ¶
func (a *API) ListPendingQuestions(ctx context.Context) (out []PendingQuestion, err error)
ListPendingQuestions 拉取当前全部挂起的提问请求(跨会话)。
返回:挂起请求列表;请求失败或解析失败时返回错误,列表为 nil
注意:
- 返回的是**全部会话**的挂起请求,调用方必须按 SessionID 过滤出自己的
- agentd 重启后重新发现挂起提问的唯一途径:SSE 无重放语义,重启窗口里 发生的 question.asked 永远收不到
func (*API) PromptAsync ¶
PromptAsync 向会话发送一条 prompt,opencode 随即开始执行,函数立即返回。
参数:
- sessionID: CreateSession 返回的会话 id
- text: prompt 文本(计划内容、用户指令等)
注意:
- 本调用不等待执行结果,执行事件通过 SubscribeEvents 消费
func (*API) RejectQuestion ¶
RejectQuestion 拒绝一条挂起的提问,解除 question 工具的阻塞。
参数:requestID 为 question.asked 事件里的 properties.id
注意:
- 用于「任务要停了但提问还挂着」的兜底解阻塞,不是协调者的正常答复通道
func (*API) ReplyQuestion ¶
ReplyQuestion 把协调者的答案回填给 opencode 的 question 工具,工具随即返回、 回合继续。
参数:
- requestID: question.asked 事件里的 properties.id
- answers: 按问题顺序排列,每项是该问选中的 label 数组
返回:
- 4xx 时返回可 errors.Is 命中 ErrCustomAnswerRejected 的错误(答案不被接受)
- 其余失败返回普通错误
func (*API) RespondPermission ¶
func (a *API) RespondPermission(ctx context.Context, sessionID, permID, response string) (err error)
RespondPermission 应答 opencode 的权限请求。
参数:
- sessionID: 权限所属会话
- permID: 权限请求 id(来自 SSE 事件的 permissionID 字段)
- response: "once"(批准本次)或 "reject"(拒绝)
注意:
- 非法 response 值直接返回错误,不触达服务端
func (*API) SubscribeEvents ¶
func (a *API) SubscribeEvents(ctx context.Context, onEvent func(json.RawMessage), onReconnect func()) error
SubscribeEvents 订阅 GET /event(SSE),每收到一条事件就同步调用 onEvent(raw),直到 ctx 取消。
断流处理:连接意外断开(EOF/网络错误/服务端未就绪)后指数退避重连, 1s→2s→4s→…→30s 封顶,无限重试;ctx 取消时立即返回 nil。
参数:
- onEvent: 每条事件的回调(同步调用:顺序有保证,但回调阻塞会暂停消费)
- onReconnect: 断连后成功重连的回调(首次建连不触发);调用方借此把 「断连间隙可能丢事件」的告警交到业务层(P1-10b,可为 nil)
返回:
- ctx 取消时返回**最后一次连接的失败原因**(最后一次连接健康则返回 nil): 调用方拿它填 FailReason(A-7)。恒返回 nil 会让「看门狗判死 → 事件流退出」 的失败现场只剩 "<nil>",零信息
- 解析器遇到无法恢复的流异常(如单行超过 1MB 上限)时返回错误
注意:
- 未知/解析失败的行(event: 行、注释、非 JSON data)Debug 跳过,绝不中断订阅
type Adapter ¶
type Adapter struct {
// contains filtered or unexported fields
}
Adapter 是 opencode 的 executor.Adapter 实现(语义翻译层)。
并发安全:runs 表由 mu 保护;每个任务的运行态(回合累积、事件通道)只被 该任务自己的订阅 goroutine 访问,不做跨任务共享。
func (*Adapter) Events ¶
func (a *Adapter) Events(taskID string) <-chan executor.AdapterEvent
Events 返回任务的事件流通道(Start 后可用;Stop 或执行终结后关闭)。
注意(P1-11):任务不在运行(未启动/已 Stop/运行态已随终结注销)时返回 **已关闭的通道**而非 nil——契约是「通道关闭 = 执行终结」,消费方(manager 中介循环)靠 range 在关闭时退出;nil 通道会让 for-range 永久阻塞。Dispatch → go mediate 的调度窗口内 serve 若死亡,运行态已注销而中介循环尚未开始:返回 已关闭通道让中介循环立即退出、不泄漏 goroutine;该窗口内已产出的 failed 结果随运行态注销而丢失,是「尚未开始消费」的必然缝隙,任务状态由看门狗与 重启恢复兜底。
func (*Adapter) Probe ¶
Probe 只读探测 opencode serve 是否仍存活(manager 的 prober 可选接口)。
判据与 Resume 共用同一份 Proc.Alive:存活锁被持有且端口有 HTTP 应答, 缺一即视为死亡。
参数:
- req: 探测请求(TaskDir 是 proc.json 所在,即 DataDir/tasks/<id>)
返回:
- Alive=true:serve 仍在
- Alive=false + Note:已判死,Note 给协调者看
- err != nil:探不出结论(proc.json 缺失/损坏),调用方按 unknown 处理
func (*Adapter) ProcHandle ¶
ProcHandle 交出该任务的进程句柄(来自任务目录的 proc.json)。
参数:
- taskID: 任务 ID,仅用于日志定位
- taskDir: 任务目录(凭据所在)
返回:
- 进程句柄;proc.json 不存在或不可解析时返回错误
注意:本方法**只读**,不探活、不发信号——存活判定与回收分别是 prochost.Alive 与 prochost.Sweep 的职责。agentd 以可选接口消费它 (不实现该方法的 adapter 一律按「无凭据」降级,与 reaper/prober 同款路数)。
func (*Adapter) Reap ¶
Reap 在没有内存运行态时按 proc.json 兜底回收 executor 侧资源。
回收顺序:读 proc.json 拿 Handle → prochost.Kill(内部先试锁,锁空闲直接成功)。
为什么不再有「确定性命名兜底」:旧实现在 proc.json 缺失时退到 tmux 会话名 handoff-<id8>,因为会话名可由 taskID 推导。锁+pid 无法从 taskID 推导, proc.json 缺失就是真的无据可查——如实报错交协调者,不猜。
返回:Handle 对应的进程本就不在时返回 nil——目标是「确保它没了」, 不是「确保我杀了它」。
func (*Adapter) Reconcile ¶
Reconcile 把断连期间错过的回合终态补回事件流。
参数:
- ctx: 控制查询超时
- taskID: 目标任务;运行态不在(未 Start / 已 Stop)时返回「无运行态」结论而非错误
返回:
- ReconcileOutcome: 结论。Emitted 只可能是 0 或 1(spec §2.2 的不变量); Pending 恒为 0(见文件头注释)
- err: 查会话失败。**调用方收到错误时只记 WARN、不改任何状态**——一次网络 抖动不该把能恢复的任务判成不可恢复
注意:
- 是否补发由 WatermarkArmed 决定(见 proc.go 的 armed 语义):未 armed 的 legacy 会话**不补发**,只把当前尾部认作已消费基线——否则升级到本版本的 存量任务会在第一次恢复时集体重放最后一个回合;armed 会话空水位即「第一个 回合尚未消费」,必须补发(B38 头号场景)
- 「补发 → 前进水位」在 turnMu 下串行完成,与实时路径的 mapIdle 互斥; 拆成两步会让两条路同时判「未消费」而补出两条终态
func (*Adapter) RespondPermission ¶
func (a *Adapter) RespondPermission(ctx context.Context, taskID, permID, decision string) (err error)
RespondPermission 把协调者的权限裁决转发给 opencode server。
参数:
- permID: 与 permission 事件中的 PermissionID 一致(manager 的 ticket id 经 taskID:permID 命名空间化,此处为裸 permID,由 manager 还原后传入)
- decision: "once"(批准本次)或 "reject"(拒绝)
注意:
- stopCh 已关时拒绝转发:与 Send 同因(见 Send 注释),保留态不接新裁决
func (*Adapter) Resume ¶
Resume 重建 agentd 重启前已在执行的任务(spec §8「存活则重连 SSE 继续」): 从任务目录的 proc.json 恢复 serve 连接凭据并探活(存活锁 + HTTP 应答); 存活则重建 SSE 订阅、看门狗与事件通道,返回 Reattach。
参数:
- req: 恢复请求(TaskDir 是 proc.json 所在,即 DataDir/tasks/<id>; RepoPath 用于重启后重新捕获 git 兜底分类的起点 commit 基线; SessionID 是落库的 opencode 会话 id)
返回:
- Alive=false 时调用方(manager)把任务转 failed 交协调者裁决(保守优于静默)
- err: 重建失败(proc.json 缺失/损坏、SessionID 为空),此时视为不可恢复
注意:
- SessionID 为空时拒绝恢复:mapEvent 按会话 id 过滤事件,空 id 会把全部 事件当「其他会话」丢弃,静默恢复等于无声断流,宁可交协调者裁决
- 重启时正在进行的回合文本累积在内存里已丢失:重建后的回合从 SSE 重放的新 快照重新累积(partSeen/partSnap 重新对账),idle 分类的 git 基线以重启 时刻的 HEAD 为准——这是 MVP 接受的缝隙,由 e2e 清单「agentd 重启」项 实测观察
- 与 Start 的对称性:Stop(done 归档)对恢复出来的运行态同样有效, 会按进程组 Kill 回收执行者资源
func (*Adapter) Send ¶
Send 向同一会话续发指令(原生续接:上下文完整保留)。
参数:
- text: 协调者的回答/修改指令,原样透传,不得加工
注意:
- stopCh 已关(Stop 已介入,运行态可能因 kill 失败被保留)时拒绝发送: 订阅已退出,prompt 发出也没有事件回程,任务会静默挂死——宁可让协调者 看到「任务不在运行」的明确错误
- 有挂起的 question 请求时不发 prompt,改把答复回填给该请求(B49): question 工具阻塞时回合并未结束,发 prompt 会开出第二个回合
func (*Adapter) Start ¶
Start 按「WriteTaskEnv → StartServe → 建会话 → 初始 prompt → 订阅映射」 流程启动任务执行并立即返回。
参数:
- ctx: 控制启动阶段的超时/取消(CreateSession/PromptAsync 受其约束); 不代表执行生命周期(执行延续到 Stop)
- req: 任务快照、计划原文与任务工作目录
返回:
- 任一启动阶段失败(环境物料生成/serve 启动/建会话/发 prompt)返回错误, 调用方(manager)应把任务标记 failed
注意:
- serve 已拉起但后续阶段失败时自动 Kill 清理进程残留,避免半启动进程占端口
func (*Adapter) Stop ¶
Stop 终止任务执行:取消订阅 → kill serve(执行者进程组)→ 事件通道关闭 → 注销运行态。
注意:
- 幂等:重复 Stop 不 panic;事件通道只关闭一次(由订阅 goroutine 持有关闭权)
- kill 失败但 serve 仍存活时**保留运行态**(P1-9):serve 占着端口与模型 会话,drop 掉就没有任何途径回收;保留期间运行态是惰性的(订阅与看门狗 都已退出、事件通道已关),Send/RespondPermission 经 stopCh 守卫拒绝继续执行
- 保留态由 reapRetained 后台重试回收(A-10),重试有上限、放弃时打 Error 交人工(handoff stop 回收):**agentd 重启不会接走它**——RecoverOnStartup 只探测 running/waiting_answer 任务(watchdog.go),而 Stop 只由 Done 在 归档时调用(manager.go),进程内保留态只可能属于已归档任务
- 运行态注销(drop)与 subscribeLoop 退出时的 drop 是幂等的 map 删除, mu 保护下不会重复释放——runs 表因此不随任务累积无界增长
type PendingQuestion ¶
type PendingQuestion struct {
ID string `json:"id"`
SessionID string `json:"sessionID"`
Questions []QuestionInfo `json:"questions"`
}
PendingQuestion 是一条挂起的 question 请求(一次可含多道问题)。
type Proc ¶
Proc 描述一个运行中的 opencode serve 进程。
字段说明:
- Handle: prochost 句柄(shim pid + 存活锁路径),存活与回收都靠它
- Port: serve 监听的端口(127.0.0.1 上)
- Password: 随机生成的 OPENCODE_SERVER_PASSWORD,api.go 的 basic auth 用它
- ServeLogPath: serve 输出日志路径(<taskDir>/serve.log),serve 死后诊断只能读它
func StartServe ¶
func StartServe(ctx context.Context, repoPath, taskID, taskDir, configPath string, env []string, log *slog.Logger) (*Proc, error)
StartServe 经 prochost 拉起 opencode serve 并等待就绪。
参数:
- ctx: 上下文;就绪轮询同样受 ctx 取消影响
- repoPath: 任务仓库路径,作为 serve 的工作目录(cwd)
- taskID: 任务 id,用于日志与任务目录定位
- taskDir: 任务目录(0700),serve.log 与 proc.json 都放这里
- configPath: 任务级 opencode 配置路径(Task 10 生成),注入 OPENCODE_CONFIG
- env: 额外注入的环境变量(形如 KEY=VALUE,来自 env 文件,已解析已展开); 覆盖顺序见 serveSpec 的 why
- log: 本模块日志入口(StartServe 是进程启动点,日志需要显式传入而非走默认)
返回:
- 就绪的 Proc;就绪 = 端口上已有 HTTP 服务响应(含 401:密码校验属后续请求的事, 这里只关心「serve 进程起来且 HTTP 层可应答」)
- 错误:取端口/密码失败、写 proc.json 失败、拉起 shim 失败、10s 内未就绪 (错误信息携带 serve.log 尾部的 serve stderr)
注意:
- 端口选择存在 TOCTOU 竞态(见 freePort),MVP 接受
- 就绪超时后自动 Kill 清理残留进程,避免半启动进程占着端口
type QuestionInfo ¶
type QuestionInfo struct {
Question string `json:"question"`
Header string `json:"header"`
Options []QuestionOption `json:"options"`
Multiple bool `json:"multiple"`
Custom bool `json:"custom"`
}
QuestionInfo 是 opencode question 工具的单个问题。
Multiple 为真表示该问可多选,Custom 为真表示该问接受选项之外的自定义答案。
type QuestionOption ¶
QuestionOption 是一个问题的一个候选项。
type SessionMessage ¶
type SessionMessage struct {
ID string
Role string
CompletedMS int64
ErrorText string
Text string
Finish string
ErrorName string
ToolStatus string
}
SessionMessage 是会话里一条消息的最小形状(对账只需要这几个字段)。
字段说明:
- ID: 消息 id,同时是对账水位的载体
- Role: "assistant" | "user"
- CompletedMS: 完结时刻(毫秒 epoch);**0 表示尚未完结**(消息未 finalize, 在飞或冻结),对账据此判「回合还在跑」
- ErrorText: 非空表示该消息以错误告终(info.error 的原始 JSON)
- Text: 该消息全部文本 part 的拼接结果,交给 turn.ParseTrailer 分类
- Finish: 该消息的完结方式(info.finish),取值实测:""(缺席)/ "tool-calls" / "stop" / "unknown"。**只当正向结束标记用**:finish=="stop" 一定意味着回合 结束,但 finish=="tool-calls" 既可能是「中间工具消息、回合继续」也可能是 「被拒/工具报错而终」——不能反过来当「未结束」判据,须看 ToolStatus 消歧
- ErrorName: 消息级错误的类型名(info.error.name);实测 "MessageAbortedError" 表示会话被 abort 而终(finish 缺席)
- ToolStatus: 最后一条 tool part 的 state.status(取值实测 "running"/ "completed"/"error")。用于把「finish=tool-calls 的回合终态」与「真·回合 中途冻结」区分开:error=被拒/报错而终(补发),completed=中途冻结(不补发)