feat(agent): 运行详情分页与增量读取契约 (#600)

Co-authored-by: ddcat666 <26564566+ddcat666@users.noreply.github.com>
This commit is contained in:
AetherNo2332
2026-09-24 11:31:09 +08:00
committed by ddcat666
parent c4b9a426ab
commit b701b3efd1
11 changed files with 761 additions and 25 deletions

View File

@@ -38,19 +38,72 @@ func (r *Repository) CloudAgent(userID, id string) (*model.CloudAgentExecution,
return &run, err
}
// CloudAgentJournalWindow 是载入执行状态时读取的事件窗口条数。
//
// 事件全量在 cloud_agent_event_records(append-only,主键 run_id + sequence),
// 内存只保留最近这一窗:摘要、卡死判定与个人记忆提取只看最近事件,运行详情与
// SSE 需要更早的记录时按 seq 分页读表。若在这里全量载入,长跑的 run 每读一次
// 运行详情(以及调度器每轮扫描的每个活动 run)都要把整份 journal 拉进内存。
const CloudAgentJournalWindow = 40
func (r *Repository) hydrateCloudAgent(run *model.CloudAgentExecution) error {
if run.CheckpointVersion < 2 {
return nil
}
if err := r.db.Where("run_id = ? AND user_id = ?", run.ID, run.UserID).Order("sequence").Find(&run.Journal).Error; err != nil {
// 只取最近一窗,再翻回升序:与 EventCount 的尾部对齐(不变量见 app 侧解码)。
window := []model.CloudAgentEventRecord{}
if err := r.db.Where("run_id = ? AND user_id = ?", run.ID, run.UserID).
Order("sequence DESC").Limit(CloudAgentJournalWindow).Find(&window).Error; err != nil {
return err
}
run.Journal = make([]model.CloudAgentEventRecord, 0, len(window))
for index := len(window) - 1; index >= 0; index-- {
run.Journal = append(run.Journal, window[index])
}
if err := r.db.Where("run_id = ? AND user_id = ?", run.ID, run.UserID).Order("kind, sequence").Find(&run.Transcript).Error; err != nil {
return err
}
return nil
}
// CloudAgentEventRecords 取 seq > afterSeq 的事件(升序),keyset 分页走
// (run_id, sequence) 主键,用于增量拉取与 SSE 断线重连。
func (r *Repository) CloudAgentEventRecords(userID, runID string, afterSeq, limit int) ([]model.CloudAgentEventRecord, error) {
if limit <= 0 {
limit = CloudAgentJournalWindow
}
records := []model.CloudAgentEventRecord{}
err := r.db.Where("run_id = ? AND user_id = ? AND sequence > ?", runID, userID, afterSeq).
Order("sequence ASC").Limit(limit).Find(&records).Error
return records, err
}
// CloudAgentEventRecordsBefore 取 seq < beforeSeq 的**最近** limit 条(升序返回),
// 供"请求的页比内存窗口更深、要往前补"的读路径使用。
func (r *Repository) CloudAgentEventRecordsBefore(userID, runID string, beforeSeq, limit int) ([]model.CloudAgentEventRecord, error) {
if limit <= 0 {
limit = CloudAgentJournalWindow
}
records := []model.CloudAgentEventRecord{}
err := r.db.Where("run_id = ? AND user_id = ? AND sequence < ?", runID, userID, beforeSeq).
Order("sequence DESC").Limit(limit).Find(&records).Error
if err != nil {
return nil, err
}
for left, right := 0, len(records)-1; left < right; left, right = left+1, right-1 {
records[left], records[right] = records[right], records[left]
}
return records, nil
}
// CloudAgentEventRecordCount 是该运行已入库的事件条数;只做计数,不载入正文。
func (r *Repository) CloudAgentEventRecordCount(userID, runID string) (int64, error) {
var count int64
err := r.db.Model(&model.CloudAgentEventRecord{}).
Where("run_id = ? AND user_id = ?", runID, userID).Count(&count).Error
return count, err
}
func (r *Repository) CloudAgentForActiveTask(userID, taskID string) (*model.CloudAgentExecution, error) {
var run model.CloudAgentExecution
err := r.db.Where("user_id = ? AND active_task_id = ? AND status IN ?", userID, taskID, []string{"running", "queued"}).First(&run).Error
@@ -144,14 +197,34 @@ func (r *Repository) MutateCloudAgent(userID, id string, revision int64, fn func
if err = fn(run, New(tx)); err != nil {
return err
}
if run.EventCount < previousEvents || len(run.Journal) != run.EventCount {
if run.EventCount < previousEvents {
return fmt.Errorf("cloud Agent journal cannot be truncated")
}
// journal 是 append-only:本次转移之前已经载入的事件不许改写。
// 内存里只有最近一窗,窗口右移后更早的行会落在新窗口之外——那些行不在
// run.Journal 里,也不会被下面的 Create 循环碰到(只追加 seq > 水位的新行),
// 因此只要求它们确实比新窗口更早,而不是要求它们仍在窗口内。
current := make(map[int]string, len(run.Journal))
for _, event := range run.Journal {
current[event.Sequence] = event.EventJSON
}
for sequence, body := range previousEventBodies {
if sequence > len(run.Journal) || run.Journal[sequence-1].Sequence != sequence || !sameJSONDocument(run.Journal[sequence-1].EventJSON, body) {
updated, present := current[sequence]
if !present {
if len(run.Journal) == 0 || sequence >= run.Journal[0].Sequence {
return fmt.Errorf("cloud Agent journal is append-only")
}
continue
}
if !sameJSONDocument(updated, body) {
return fmt.Errorf("cloud Agent journal is append-only")
}
}
// 水位与窗口必须自洽:窗口最后一条就是要落库的最后一条,否则说明写入端
// 算错了窗口起点,或行被删掉过。
if len(run.Journal) > 0 && run.Journal[len(run.Journal)-1].Sequence != run.EventCount {
return fmt.Errorf("cloud Agent journal watermark is inconsistent")
}
if err = tx.Omit("Journal", "Transcript").Save(run).Error; err != nil {
return err
}