package squad import ( "encoding/json" "fmt" "os" "path/filepath" "sync" "time" ) // Store 是运行状态持久化端口(Q1 WP4):run/gate/checkpoint 的读写以 // 运行工作区为键。现状文件态收编为 FileStore(行为与此前逐文件直写完全一致); // SQLite 实现进 adapters/sqlite(WP5)。事件流仍归 store/eventsink(子包), // 不在本端口内重复。 type Store interface { SaveRun(st *RunState) error LoadRun(wsDir string) (*RunState, error) SaveGate(wsDir string, gate *HitlGate) error LoadGate(wsDir string) (*HitlGate, error) SaveCheckpoint(wsDir string, cp Checkpoint) error LoadLatestCheckpoint(wsDir string) (*Checkpoint, error) // SaveLeader/LoadLeader 持久化 leader 循环状态(raw JSON bytes; // 文件态即 leader.json)。LoadLeader 不存在时返回 (nil, nil)。 SaveLeader(wsDir string, data []byte) error LoadLeader(wsDir string) ([]byte, error) } // FileStore 把 runs// 目录布局呈现为 Store(现状文件态)。 type FileStore struct{} func (FileStore) SaveRun(st *RunState) error { return st.persist() } func (FileStore) LoadRun(wsDir string) (*RunState, error) { return loadRunState(wsDir) } func (FileStore) SaveGate(wsDir string, gate *HitlGate) error { st := &RunState{} st.Meta.WorkDir = wsDir return st.saveGate(gate) } func (FileStore) LoadGate(wsDir string) (*HitlGate, error) { return loadGate(wsDir) } func (FileStore) SaveLeader(wsDir string, data []byte) error { return os.WriteFile(filepath.Join(wsDir, "leader.json"), data, 0o644) } func (FileStore) LoadLeader(wsDir string) ([]byte, error) { data, err := os.ReadFile(filepath.Join(wsDir, "leader.json")) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } return data, nil } func (FileStore) SaveCheckpoint(wsDir string, cp Checkpoint) error { dir := filepath.Join(wsDir, "checkpoints") if err := os.MkdirAll(dir, 0o755); err != nil { return err } path := filepath.Join(dir, fmt.Sprintf("%03d.json", cp.Superstep)) if err := writeJSON(path, cp); err != nil { return err } return writeJSON(filepath.Join(dir, "latest.json"), cp) } func (FileStore) LoadLatestCheckpoint(wsDir string) (*Checkpoint, error) { data, err := os.ReadFile(filepath.Join(wsDir, "checkpoints", "latest.json")) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } var cp Checkpoint if err := json.Unmarshal(data, &cp); err != nil { return nil, fmt.Errorf("parse latest checkpoint: %w", err) } return &cp, nil } // Checkpoint 是一个 superstep 边界的状态快照(Q1 WP4)。 // GraphHash 是图结构指纹:恢复时与当前 Steps 重编译比对,不一致说明崩溃 // 期间图形态被改过(动态调整或外力),按 WP1 裁定拒绝恢复并提示重新规划。 type Checkpoint struct { Superstep int `json:"superstep"` At time.Time `json:"at"` GraphHash string `json:"graph_hash"` StepStates []StepSnapshot `json:"steps"` wsDir string `json:"-"` } // write 立即同步落盘(经当前 Store 端口——SQLite 后端时进数据库)。 func (cp Checkpoint) write(store Store) error { return store.SaveCheckpoint(cp.wsDir, cp) } // StepSnapshot 是检查点里的单步快照。 type StepSnapshot struct { ID string `json:"id"` Status string `json:"status"` AttemptID string `json:"attempt_id,omitempty"` // F05/U3b:trusted terminal record;用于 recovery 时仅在「后端显式 // 声明 success」时把步骤恢复成 completed(即便目标文件缺失也照恢复), // files 仍然记入 deliverables 做诊断。SideEffects 报告重放安全性: // none → 可重试 // possible → 需 reconciliation(不禁 rerun;只允许显式 reconcile) // Canceled/Aborted 为 true 时本步是取消/中止语义,不应被恢复成 completed。 Terminal string `json:"terminal,omitempty"` // SideEffects 镜像 ExecResult.SideEffects;缺省 = possible(旧读取)。 SideEffects string `json:"side_effects,omitempty"` Canceled bool `json:"canceled,omitempty"` Aborted bool `json:"aborted,omitempty"` } // CheckpointMode 快照写盘档位(Q1 WP4 三档)。 type CheckpointMode string const ( // CheckpointSync 每个 superstep 边界同步写(默认,与现状「逐步全量落盘」一致)。 CheckpointSync CheckpointMode = "sync" // CheckpointAsync 边界快照进队列由单 goroutine 串行写(边界不阻塞;恢复粒度同 sync)。 CheckpointAsync CheckpointMode = "async" // CheckpointOnExit 只在推进退出(终态/挂起/错误)时写最终快照。 CheckpointOnExit CheckpointMode = "on_exit" ) // checkpointWriter 按档位执行边界快照;async 档单 goroutine 串行落盘。 type checkpointWriter struct { mode CheckpointMode store Store ch chan Checkpoint done chan error // flushOnce/flushErr 让出口 flush 幂等:正常收口路径先 flush 一次(失败必须 // 先于任何完成事件可见),defer 再兜底 flush 时不会二次写、也不会重复报错。 flushOnce sync.Once flushErr error } func newCheckpointWriter(mode CheckpointMode, store Store) *checkpointWriter { if mode == "" { mode = CheckpointSync } if store == nil { store = FileStore{} } w := &checkpointWriter{mode: mode, store: store} if mode == CheckpointAsync { w.ch = make(chan Checkpoint, 16) w.done = make(chan error, 1) go func() { var first error for cp := range w.ch { if first != nil { continue } if err := cp.write(w.store); err != nil { first = err } } w.done <- first }() } return w } // boundary 由调用方(flowDriver)在 superstep 边界触发: // sync 立即写;async 入队(满即错,不静默丢);on_exit 跳过。 func (w *checkpointWriter) boundary(wsDir string, cp Checkpoint) error { cp.wsDir = wsDir switch w.mode { case CheckpointAsync: select { case w.ch <- cp: default: return fmt.Errorf("checkpoint queue full at superstep %d", cp.Superstep) } return nil case CheckpointOnExit: return nil default: return cp.write(w.store) } } // flush 在推进退出前调用:async 落完并报告首个错误;on_exit 写最终快照。 // 幂等:重复调用返回首次结果(async 的 chan 只关一次)。 func (w *checkpointWriter) flush(wsDir string, cp Checkpoint) error { w.flushOnce.Do(func() { w.flushErr = w.flushOnceImpl(wsDir, cp) }) return w.flushErr } func (w *checkpointWriter) flushOnceImpl(wsDir string, cp Checkpoint) error { cp.wsDir = wsDir switch w.mode { case CheckpointAsync: close(w.ch) return <-w.done case CheckpointOnExit: return cp.write(w.store) default: return nil } } // persistenceFailurePrefix 标记落盘失败。这类失败必须让运行以可区分的面目收场: // 业务动作(步骤执行、交付物写出)已发生,不回滚、不声称完成。 const persistenceFailurePrefix = "persistence failure" // persistenceFailureText 组装落盘失败的可读说明(stage = 落盘环节)。 func persistenceFailureText(stage string, err error) string { return persistenceFailurePrefix + ": " + stage + ": " + err.Error() } // validateGraphContinuity 恢复连续性校验(Q1 WP4;WP1 的图版本裁定)。 // 最新检查点存在且其图指纹与当前 Steps 重编译不一致时拒绝恢复—— // 崩溃期间图形态被改过,继续推进会把已完成的结果接到错误的图上。 // 检查点不存在(旧运行)或指纹一致则放行。 func validateGraphContinuity(st *RunState) error { latest, err := st.storeOr().LoadLatestCheckpoint(st.Meta.WorkDir) if err != nil || latest == nil { return err } g, gerr := compileGraph(st.Plan.Workflow.Steps) if gerr != nil { return fmt.Errorf("graph continuity: 当前 Steps 无法编译:%w(请人工介入或重新规划)", gerr) } if latest.GraphHash != g.hash() { return fmt.Errorf("graph continuity: 崩溃前后图形态不一致(checkpoint superstep=%d hash=%s,当前=%s)——可能在动态调整或外部修改后崩溃,请重新规划或人工介入", latest.Superstep, latest.GraphHash, g.hash()) } return nil }