Files
Agent-System-Backend/internal/runmgr/rewind.go
T
weichen adcd8fefd2 Fence U3 account writes and stabilize directory cursors
Why: Account disable could be followed by late Backend business writes, API key creation could implicitly create users, account bootstrap locks could invert, and directory cursors were process-local or leaked out-of-scope changes.

Evidence: Local go test ./... and isolated PostgreSQL package suites passed on the staged candidate; exact committed-head SELF replay and stage 6 human review remain pending.

Impact: Adds final account transactions and external-effect checks across HTTP, Host, Bus and run manager; tightens API key issuance and persists versioned cursor signing. Delegated RDM rename still needs the separately proposed real-operator contract, so A1 and stage 6 are not complete.

Refs: Documents U3 r1, approved A1-A4 amendment fb3a821, pending delegated-projection amendment 5783940, Dev feature/iam-u3-account-directory-recovery.
2026-10-14 07:33:54 +00:00

154 lines
4.3 KiB
Go

package runmgr
import (
"context"
"fmt"
"log"
"path/filepath"
"strings"
"agent-native/runtime/checkpoint"
core "agent-system-core"
)
func (m *Manager) checkpointFile(owner, convID string) (string, error) {
if m == nil || m.workspaceRoot == "" || owner == "" || convID == "" {
return "", fmt.Errorf("未配置工作区,不能记录文件检查点")
}
return filepath.Join(m.workspaceRoot, owner, convID, ".agentx", "checkpoints.jsonl"), nil
}
// OpenCheckpoints 打开这个会话的文件检查点日志。
func (m *Manager) OpenCheckpoints(owner, convID string) (*checkpoint.Log, error) {
path, err := m.checkpointFile(owner, convID)
if err != nil {
return nil, err
}
return checkpoint.Open(path)
}
func (c *runCtx) OpenCheckpoints() (*checkpoint.Log, error) {
if c == nil || c.mgr == nil {
return nil, fmt.Errorf("未配置工作区,不能记录文件检查点")
}
return c.mgr.OpenCheckpoints(c.run.Owner, c.run.ConversationID)
}
func (c *runCtx) CheckpointAnchor() string {
if c == nil {
return ""
}
return c.userMessage.ID
}
// NoteFileCheckpoint 记录一次工作区写入的改前内容。
func (c *runCtx) NoteFileCheckpoint(rel string, before []byte, existed bool, after []byte) error {
log, err := c.OpenCheckpoints()
if err != nil {
return err
}
root := c.ConversationSandbox()
return log.Record(c.CheckpointAnchor(), root, rel, before, existed, after)
}
func (t *toolCtx) NoteFileCheckpoint(rel string, before []byte, existed bool, after []byte) error {
if t == nil || t.rc == nil {
return nil
}
return t.rc.NoteFileCheckpoint(rel, before, existed, after)
}
// PlanRewind 列出从这条用户消息起会被回滚的文件,不改磁盘。
func (m *Manager) PlanRewind(owner, convID, messageID string) (checkpoint.Result, error) {
ordered, err := m.userMessageIDs(convID, messageID)
if err != nil {
return checkpoint.Result{}, err
}
log, err := m.OpenCheckpoints(owner, convID)
if err != nil {
return checkpoint.Result{}, err
}
return log.Plan(messageID, ordered)
}
// RewindFiles 把文件退回到这条用户消息之前,并追加一条系统说明。
func (m *Manager) RewindFiles(owner, convID, messageID string) (checkpoint.Result, error) {
ordered, err := m.userMessageIDs(convID, messageID)
if err != nil {
return checkpoint.Result{}, err
}
cpLog, err := m.OpenCheckpoints(owner, convID)
if err != nil {
return checkpoint.Result{}, err
}
if m.accountEffectiveAuthorization != nil {
if err := m.authorizeAccountEffective(context.Background(), owner); err != nil {
return checkpoint.Result{}, err
}
}
res, err := cpLog.Rewind(messageID, ordered)
if err != nil {
return checkpoint.Result{}, err
}
// 保留策略(N3 WP3):成功的回滚后按 anchor 数裁到 200,日志有界。
if cerr := cpLog.Compact(200); cerr != nil {
log.Printf("[runmgr] checkpoint compact %s: %v", convID, cerr)
}
note := rewindNote(res)
msg := core.Message{
ConversationID: convID, Role: core.RoleSystem,
Parts: []core.Part{{Type: core.PartText, Text: note}},
}
if m.accountEffectiveAuthorization != nil {
err = m.st.AppendMessageWithRequestAccountChecked(owner, m.accountAllowImplicitLocal, &msg, "")
} else {
err = m.st.AppendMessage(&msg)
}
if err != nil {
return res, err
}
return res, nil
}
func (m *Manager) userMessageIDs(convID, messageID string) ([]string, error) {
msgs, err := m.st.ListMessages(convID)
if err != nil {
return nil, err
}
var ordered []string
found := false
for _, msg := range msgs {
if msg.Role != core.RoleUser {
continue
}
ordered = append(ordered, msg.ID)
if msg.ID == messageID {
found = true
}
}
if !found {
return nil, fmt.Errorf("找不到用户消息 %s", messageID)
}
return ordered, nil
}
func rewindNote(res checkpoint.Result) string {
var b strings.Builder
b.WriteString("已把文件回滚到这条消息之前。")
if len(res.Restored) == 0 && len(res.Skipped) == 0 {
b.WriteString("没有需要回滚的文件。")
return b.String()
}
if len(res.Restored) > 0 {
b.WriteString("已恢复:")
b.WriteString(strings.Join(res.Restored, "、"))
b.WriteString("。")
}
if len(res.Skipped) > 0 {
b.WriteString("已跳过:")
b.WriteString(strings.Join(res.Skipped, "、"))
b.WriteString("(文件在追踪之外被改过)。")
}
return b.String()
}