Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

703 lines
25 KiB
Go

package agent
import (
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"github.com/google/uuid"
agenttargetbiz "github.com/tutti-os/tutti/services/tuttid/biz/agenttarget"
tuttitypes "github.com/tutti-os/tutti/services/tuttid/types"
)
const (
WorktreeIsolationMode = "worktree"
worktreeIsolationContextKey = "isolation"
worktreeDirtyBaseWarningCode = "worktree_base_dirty"
worktreeGitOperationTimeout = 30 * time.Second
managedWorktreeStateCreating = "creating"
managedWorktreeStateReady = "ready"
)
var (
ErrNotAGitRepo = errors.New("cwd is not a git repository")
ErrGitUnavailable = errors.New("git is unavailable")
ErrUnsupportedRepoLayout = errors.New("git repository layout is unsupported")
ErrWorktreeCreateFailed = errors.New("git worktree creation failed")
ErrManagedWorktreeNotFound = errors.New("managed worktree not found")
ErrManagedWorktreeDirty = errors.New("managed worktree has uncommitted changes")
ErrManagedWorktreeAhead = errors.New("managed worktree has commits beyond its base commit")
ErrManagedWorktreeChanged = errors.New("managed worktree branch changed during deletion")
)
type WorktreeIsolationError struct {
Kind error
Detail string
}
type SessionWorktreeSupportErrorCode string
const (
SessionWorktreeSupportGitUnavailable SessionWorktreeSupportErrorCode = "git-unavailable"
SessionWorktreeSupportNotGitRepo SessionWorktreeSupportErrorCode = "not-git-repo"
SessionWorktreeSupportTargetUnsupported SessionWorktreeSupportErrorCode = "agent-target-unsupported"
SessionWorktreeSupportUnsupportedRepoLayout SessionWorktreeSupportErrorCode = "unsupported-repo-layout"
)
type SessionWorktreeSupport struct {
Supported bool
Root string
ErrorCode SessionWorktreeSupportErrorCode
}
type sessionWorktreeSource struct {
Cwd string
RepoRoot string
GitCommonDir string
BaseCommit string
}
type sessionWorktreeLaunch struct {
Isolation SessionIsolation
Cwd string
Warnings []SessionWarning
Created bool
}
// ManagedWorktree is a workspace-scoped Git resource with an explicit
// lifecycle. Sessions may run with its path as cwd, but they do not own,
// retain, or release the resource.
type ManagedWorktree struct {
WorktreeID string
WorkspaceID string
RepoRoot string
WorktreePath string
Branch string
BaseCommit string
RelativeCwd string
}
func (e *WorktreeIsolationError) Error() string {
if e == nil {
return ""
}
if detail := strings.TrimSpace(e.Detail); detail != "" {
return detail
}
if e.Kind != nil {
return e.Kind.Error()
}
return "worktree isolation failed"
}
func (e *WorktreeIsolationError) Unwrap() error {
if e == nil {
return nil
}
return e.Kind
}
type managedWorktreeRecord struct {
WorktreeID string `json:"worktreeId,omitempty"`
State string `json:"state,omitempty"`
WorktreePath string `json:"worktreePath"`
Branch string `json:"branch"`
BaseCommit string `json:"baseCommit"`
WorkspaceID string `json:"workspaceId"`
RepoRoot string `json:"repoRoot"`
RelativeCwd string `json:"relativeCwd,omitempty"`
CreationRequestHash string `json:"creationRequestHash,omitempty"`
// LegacySessionID is read only for compatibility with metadata created
// before worktrees became independent resources. It is never interpreted
// as ownership and is omitted from newly written records.
LegacySessionID string `json:"sessionId,omitempty"`
// GitCommonDir anchors explicit git operations to the main repository's git
// directory, which stays valid even when RepoRoot was itself a linked
// worktree that has since been removed.
GitCommonDir string `json:"gitCommonDir,omitempty"`
}
func worktreeGitAnchor(record managedWorktreeRecord) (string, []string) {
if common := strings.TrimSpace(record.GitCommonDir); common != "" {
return common, []string{"--git-dir", common}
}
return record.RepoRoot, nil
}
func gitRepoOutput(ctx context.Context, record managedWorktreeRecord, args ...string) (string, error) {
dir, prefix := worktreeGitAnchor(record)
return gitOutput(ctx, dir, append(append([]string(nil), prefix...), args...)...)
}
func (s *Service) worktreeStateDir() string {
if s != nil {
if stateDir := strings.TrimSpace(s.WorktreeStateDir); stateDir != "" {
return filepath.Clean(stateDir)
}
}
return tuttitypes.DefaultStateDir()
}
func (s *Service) worktreeLock() *sync.RWMutex {
if s != nil && s.worktreeIsolationLock != nil {
return s.worktreeIsolationLock
}
return &s.worktreeIsolationMu
}
func (s *Service) createSessionWorktree(
ctx context.Context,
workspaceID string,
cwd string,
sessionID string,
) (sessionWorktreeLaunch, error) {
return createSessionWorktree(ctx, s.worktreeStateDir(), workspaceID, cwd, sessionID)
}
func (s *Service) ResolveSessionWorktreeSupport(
ctx context.Context,
workspaceID string,
agentTargetID string,
cwd string,
) (SessionWorktreeSupport, error) {
workspaceID = strings.TrimSpace(workspaceID)
agentTargetID = strings.TrimSpace(agentTargetID)
if workspaceID == "" || agentTargetID == "" || strings.TrimSpace(cwd) == "" {
return SessionWorktreeSupport{}, ErrInvalidArgument
}
if strings.HasPrefix(agentTargetID, "shared-agent:") {
return SessionWorktreeSupport{ErrorCode: SessionWorktreeSupportTargetUnsupported}, nil
}
input := CreateSessionInput{AgentTargetID: agentTargetID}
launch, err := s.resolveCreateSessionLaunch(ctx, workspaceID, &input)
if err != nil {
return SessionWorktreeSupport{}, err
}
if !sessionWorktreeTargetSupported(agentTargetID, launch.ProviderTargetRef) {
return SessionWorktreeSupport{ErrorCode: SessionWorktreeSupportTargetUnsupported}, nil
}
return s.ResolveSessionWorktreeSupportForPath(ctx, workspaceID, cwd)
}
func sessionWorktreeTargetSupported(agentTargetID string, providerTargetRef map[string]any) bool {
if strings.HasPrefix(strings.TrimSpace(agentTargetID), "shared-agent:") {
return false
}
switch providerTargetRefKind(providerTargetRef) {
case agenttargetbiz.LaunchRefTypeBuiltinLocal, agenttargetbiz.LaunchRefTypeAgentExtension:
return true
default:
return false
}
}
func (*Service) ResolveSessionWorktreeSupportForPath(
ctx context.Context,
workspaceID string,
cwd string,
) (SessionWorktreeSupport, error) {
if strings.TrimSpace(workspaceID) == "" || strings.TrimSpace(cwd) == "" {
return SessionWorktreeSupport{}, ErrInvalidArgument
}
source, err := resolveSessionWorktreeSource(ctx, cwd)
if err == nil {
return SessionWorktreeSupport{Supported: true, Root: source.RepoRoot}, nil
}
support := SessionWorktreeSupport{Supported: false}
switch {
case errors.Is(err, ErrGitUnavailable):
support.ErrorCode = SessionWorktreeSupportGitUnavailable
case errors.Is(err, ErrNotAGitRepo):
support.ErrorCode = SessionWorktreeSupportNotGitRepo
default:
support.ErrorCode = SessionWorktreeSupportUnsupportedRepoLayout
}
return support, nil
}
func createSessionWorktree(
ctx context.Context,
stateDir string,
workspaceID string,
cwd string,
creationRequestID string,
) (sessionWorktreeLaunch, error) {
source, err := resolveSessionWorktreeSource(ctx, cwd)
if err != nil {
return sessionWorktreeLaunch{}, err
}
repoRoot := source.RepoRoot
gitCommonDir := source.GitCommonDir
baseCommit := source.BaseCommit
relativeCwd, err := sessionWorktreeRelativeCwd(source)
if err != nil {
return sessionWorktreeLaunch{}, err
}
workspaceID = strings.TrimSpace(workspaceID)
creationRequestID = strings.TrimSpace(creationRequestID)
if workspaceID == "" || creationRequestID == "" {
return sessionWorktreeLaunch{}, &WorktreeIsolationError{Kind: ErrWorktreeCreateFailed, Detail: "worktree creation request identity is required"}
}
worktreesRoot := filepath.Join(filepath.Clean(stateDir), "agent", "worktrees")
creationRequestHash := managedWorktreeCreationRequestHash(workspaceID, creationRequestID)
if reused, found, reuseErr := reuseManagedWorktree(
ctx,
worktreesRoot,
workspaceID,
creationRequestID,
creationRequestHash,
source,
relativeCwd,
); reuseErr != nil {
return sessionWorktreeLaunch{}, reuseErr
} else if found {
return reused, nil
}
worktreeID := uuid.NewString()
worktreePath := filepath.Join(worktreesRoot, worktreeID)
branch := "tutti/worktree/" + worktreeID
if _, statErr := os.Lstat(worktreePath); statErr == nil || !errors.Is(statErr, os.ErrNotExist) {
return sessionWorktreeLaunch{}, &WorktreeIsolationError{Kind: ErrWorktreeCreateFailed, Detail: "managed worktree path already exists"}
}
if _, statErr := os.Lstat(worktreeRecordPath(worktreesRoot, worktreeID)); statErr == nil || !errors.Is(statErr, os.ErrNotExist) {
return sessionWorktreeLaunch{}, &WorktreeIsolationError{Kind: ErrWorktreeCreateFailed, Detail: "managed worktree metadata already exists"}
}
if _, branchErr := gitOutput(ctx, repoRoot, "show-ref", "--verify", "refs/heads/"+branch); branchErr == nil {
return sessionWorktreeLaunch{}, &WorktreeIsolationError{Kind: ErrWorktreeCreateFailed, Detail: "managed worktree branch already exists"}
}
info := SessionIsolation{WorktreeID: worktreeID, Mode: WorktreeIsolationMode, WorktreePath: worktreePath, Branch: branch, BaseCommit: baseCommit}
record := managedWorktreeRecord{
WorktreeID: info.WorktreeID, State: managedWorktreeStateCreating,
WorktreePath: info.WorktreePath,
Branch: info.Branch, BaseCommit: info.BaseCommit,
WorkspaceID: workspaceID, RepoRoot: repoRoot, GitCommonDir: gitCommonDir,
RelativeCwd: relativeCwd, CreationRequestHash: creationRequestHash,
}
if err := writeManagedWorktreeRecord(worktreesRoot, record); err != nil {
return sessionWorktreeLaunch{}, &WorktreeIsolationError{Kind: ErrWorktreeCreateFailed, Detail: err.Error()}
}
created := false
defer func() {
if !created {
rollbackManagedWorktree(context.Background(), worktreesRoot, record)
}
}()
if _, err := gitOutput(ctx, repoRoot, "worktree", "add", "-b", branch, worktreePath, baseCommit); err != nil {
return sessionWorktreeLaunch{}, &WorktreeIsolationError{Kind: ErrWorktreeCreateFailed, Detail: gitErrorDetail(err)}
}
runtimeCwd := sessionWorktreeRuntimeCwd(worktreePath, relativeCwd)
if stat, statErr := os.Stat(runtimeCwd); statErr != nil || !stat.IsDir() {
detail := "selected project directory does not exist in the isolated worktree"
if statErr != nil && !errors.Is(statErr, os.ErrNotExist) {
detail = statErr.Error()
}
return sessionWorktreeLaunch{}, &WorktreeIsolationError{Kind: ErrWorktreeCreateFailed, Detail: detail}
}
record.State = managedWorktreeStateReady
if err := replaceManagedWorktreeRecord(worktreesRoot, record); err != nil {
return sessionWorktreeLaunch{}, &WorktreeIsolationError{Kind: ErrWorktreeCreateFailed, Detail: err.Error()}
}
created = true
return sessionWorktreeLaunch{
Isolation: info,
Cwd: runtimeCwd,
Warnings: sessionWorktreeWarnings(ctx, repoRoot),
Created: true,
}, nil
}
func sessionWorktreeRelativeCwd(source sessionWorktreeSource) (string, error) {
relative, err := filepath.Rel(source.RepoRoot, source.Cwd)
if err != nil {
return "", &WorktreeIsolationError{Kind: ErrUnsupportedRepoLayout, Detail: err.Error()}
}
relative = filepath.Clean(relative)
if filepath.IsAbs(relative) || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
return "", &WorktreeIsolationError{Kind: ErrUnsupportedRepoLayout, Detail: "selected project directory is outside the repository root"}
}
return relative, nil
}
func sessionWorktreeRuntimeCwd(worktreePath string, relativeCwd string) string {
if relativeCwd == "" || relativeCwd == "." {
return filepath.Clean(worktreePath)
}
return filepath.Join(filepath.Clean(worktreePath), relativeCwd)
}
func managedWorktreeCreationRequestHash(workspaceID string, requestID string) string {
digest := sha256.Sum256([]byte(strings.TrimSpace(workspaceID) + "\x00" + strings.TrimSpace(requestID)))
return fmt.Sprintf("%x", digest[:])
}
func reuseManagedWorktree(
ctx context.Context,
worktreesRoot string,
workspaceID string,
creationRequestID string,
creationRequestHash string,
source sessionWorktreeSource,
relativeCwd string,
) (sessionWorktreeLaunch, bool, error) {
record, found, err := findManagedWorktreeForCreationRequest(
worktreesRoot, creationRequestID, creationRequestHash,
)
if err != nil {
return sessionWorktreeLaunch{}, false, &WorktreeIsolationError{Kind: ErrWorktreeCreateFailed, Detail: "managed worktree metadata is unreadable"}
}
if !found {
return sessionWorktreeLaunch{}, false, nil
}
recordedRelativeCwd := filepath.Clean(strings.TrimSpace(record.RelativeCwd))
if strings.TrimSpace(record.RelativeCwd) == "" {
recordedRelativeCwd = "."
}
if strings.TrimSpace(record.WorkspaceID) != workspaceID ||
canonicalWorktreePath(record.RepoRoot) != canonicalWorktreePath(source.RepoRoot) ||
recordedRelativeCwd != relativeCwd ||
strings.TrimSpace(record.WorktreeID) == "" ||
canonicalWorktreePath(record.WorktreePath) != canonicalWorktreePath(filepath.Join(worktreesRoot, record.WorktreeID)) ||
strings.TrimSpace(record.BaseCommit) == "" ||
(record.State != "" && record.State != managedWorktreeStateCreating && record.State != managedWorktreeStateReady) {
return sessionWorktreeLaunch{}, false, &WorktreeIsolationError{Kind: ErrWorktreeCreateFailed, Detail: "managed worktree metadata does not match the retried launch"}
}
worktreePath := record.WorktreePath
topLevel, err := gitOutput(ctx, record.WorktreePath, "rev-parse", "--show-toplevel")
if err != nil || canonicalWorktreePath(strings.TrimSpace(topLevel)) != canonicalWorktreePath(worktreePath) {
if record.State == managedWorktreeStateCreating {
rollbackManagedWorktree(ctx, worktreesRoot, record)
return sessionWorktreeLaunch{}, false, nil
}
return sessionWorktreeLaunch{}, false, &WorktreeIsolationError{Kind: ErrWorktreeCreateFailed, Detail: "session worktree checkout is unavailable"}
}
runtimeCwd := sessionWorktreeRuntimeCwd(worktreePath, relativeCwd)
if stat, statErr := os.Stat(runtimeCwd); statErr != nil || !stat.IsDir() {
if record.State == managedWorktreeStateCreating {
rollbackManagedWorktree(ctx, worktreesRoot, record)
return sessionWorktreeLaunch{}, false, nil
}
return sessionWorktreeLaunch{}, false, &WorktreeIsolationError{Kind: ErrWorktreeCreateFailed, Detail: "selected project directory is unavailable in the isolated worktree"}
}
if record.State == managedWorktreeStateCreating {
record.State = managedWorktreeStateReady
if err := replaceManagedWorktreeRecord(worktreesRoot, record); err != nil {
return sessionWorktreeLaunch{}, false, &WorktreeIsolationError{Kind: ErrWorktreeCreateFailed, Detail: err.Error()}
}
}
return sessionWorktreeLaunch{
Isolation: managedWorktreeIsolation(record),
Cwd: runtimeCwd,
Warnings: sessionWorktreeWarnings(ctx, source.RepoRoot),
Created: false,
}, true, nil
}
func managedWorktreeIsolation(record managedWorktreeRecord) SessionIsolation {
return SessionIsolation{
WorktreeID: strings.TrimSpace(record.WorktreeID), Mode: WorktreeIsolationMode,
WorktreePath: strings.TrimSpace(record.WorktreePath),
Branch: strings.TrimSpace(record.Branch), BaseCommit: strings.TrimSpace(record.BaseCommit),
}
}
func findManagedWorktreeForCreationRequest(
worktreesRoot string,
legacyRequestID string,
creationRequestHash string,
) (managedWorktreeRecord, bool, error) {
entries, err := os.ReadDir(worktreeRecordsDir(worktreesRoot))
if errors.Is(err, os.ErrNotExist) {
return managedWorktreeRecord{}, false, nil
}
if err != nil {
return managedWorktreeRecord{}, false, err
}
for _, entry := range entries {
if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
continue
}
record, readErr := readManagedWorktreeRecord(filepath.Join(worktreeRecordsDir(worktreesRoot), entry.Name()))
if readErr != nil {
// A damaged unrelated record must not make every future Worktree create
// unavailable. Listing still surfaces the damaged record to an operator,
// and this scan never removes it.
continue
}
if strings.TrimSpace(record.CreationRequestHash) == creationRequestHash ||
(record.CreationRequestHash == "" && strings.TrimSpace(record.LegacySessionID) == legacyRequestID) {
return record, true, nil
}
}
return managedWorktreeRecord{}, false, nil
}
func sessionWorktreeWarnings(ctx context.Context, repoRoot string) []SessionWarning {
if status, statusErr := gitOutput(ctx, repoRoot, "status", "--porcelain"); statusErr == nil && strings.TrimSpace(status) != "" {
return []SessionWarning{{
Code: worktreeDirtyBaseWarningCode,
Message: "The source checkout has uncommitted changes; the isolated worktree is based on HEAD and does not include them.",
}}
}
return nil
}
func resolveSessionWorktreeSource(ctx context.Context, cwd string) (sessionWorktreeSource, error) {
if _, err := exec.LookPath("git"); err != nil {
return sessionWorktreeSource{}, &WorktreeIsolationError{Kind: ErrGitUnavailable, Detail: err.Error()}
}
cwd = strings.TrimSpace(cwd)
if cwd == "" {
return sessionWorktreeSource{}, &WorktreeIsolationError{Kind: ErrNotAGitRepo}
}
absCwd, err := filepath.Abs(cwd)
if err != nil {
return sessionWorktreeSource{}, &WorktreeIsolationError{Kind: ErrNotAGitRepo, Detail: err.Error()}
}
if resolved, resolveErr := filepath.EvalSymlinks(absCwd); resolveErr == nil {
absCwd = resolved
}
repoRoot, err := gitOutput(ctx, absCwd, "rev-parse", "--show-toplevel")
if err != nil || strings.TrimSpace(repoRoot) == "" {
return sessionWorktreeSource{}, &WorktreeIsolationError{Kind: ErrNotAGitRepo, Detail: gitErrorDetail(err)}
}
repoRoot = filepath.Clean(strings.TrimSpace(repoRoot))
if resolved, resolveErr := filepath.EvalSymlinks(repoRoot); resolveErr == nil {
repoRoot = resolved
}
if superproject, superErr := gitOutput(ctx, repoRoot, "rev-parse", "--show-superproject-working-tree"); superErr == nil && strings.TrimSpace(superproject) != "" {
return sessionWorktreeSource{}, &WorktreeIsolationError{Kind: ErrUnsupportedRepoLayout, Detail: "git submodules are not supported for worktree isolation"}
}
if outerRoot, outerErr := gitOutput(ctx, filepath.Dir(repoRoot), "rev-parse", "--show-toplevel"); outerErr == nil && strings.TrimSpace(outerRoot) != "" && filepath.Clean(strings.TrimSpace(outerRoot)) != repoRoot {
return sessionWorktreeSource{}, &WorktreeIsolationError{Kind: ErrUnsupportedRepoLayout, Detail: "nested git repositories are not supported for worktree isolation"}
}
commonDirOut, err := gitOutput(ctx, absCwd, "rev-parse", "--git-common-dir")
if err != nil || strings.TrimSpace(commonDirOut) == "" {
return sessionWorktreeSource{}, &WorktreeIsolationError{Kind: ErrWorktreeCreateFailed, Detail: gitErrorDetail(err)}
}
gitCommonDir := strings.TrimSpace(commonDirOut)
if !filepath.IsAbs(gitCommonDir) {
gitCommonDir = filepath.Join(absCwd, gitCommonDir)
}
gitCommonDir = filepath.Clean(gitCommonDir)
if resolved, resolveErr := filepath.EvalSymlinks(gitCommonDir); resolveErr == nil {
gitCommonDir = resolved
}
baseCommit, err := gitOutput(ctx, repoRoot, "rev-parse", "HEAD")
if err != nil || strings.TrimSpace(baseCommit) == "" {
return sessionWorktreeSource{}, &WorktreeIsolationError{Kind: ErrWorktreeCreateFailed, Detail: gitErrorDetail(err)}
}
return sessionWorktreeSource{
Cwd: absCwd,
RepoRoot: repoRoot,
GitCommonDir: gitCommonDir,
BaseCommit: strings.TrimSpace(baseCommit),
}, nil
}
type gitCommandError struct {
err error
detail string
}
func (e *gitCommandError) Error() string { return strings.TrimSpace(e.detail) }
func (e *gitCommandError) Unwrap() error { return e.err }
func gitOutput(ctx context.Context, cwd string, args ...string) (string, error) {
commandCtx, cancel := context.WithTimeout(ctx, worktreeGitOperationTimeout)
defer cancel()
cmd := exec.CommandContext(commandCtx, "git", args...)
cmd.Dir = cwd
cmd.Env = gitEnvScopedToDir()
out, err := cmd.CombinedOutput()
if err != nil {
detail := strings.TrimSpace(string(out))
if detail == "" {
detail = err.Error()
}
return "", &gitCommandError{err: err, detail: detail}
}
return string(out), nil
}
func gitErrorDetail(err error) string {
if err == nil {
return ""
}
return strings.TrimSpace(err.Error())
}
func sessionIsolationRuntimeContext(runtimeContext map[string]any, isolation SessionIsolation) map[string]any {
result := clonePayload(runtimeContext)
if result == nil {
result = map[string]any{}
}
result[worktreeIsolationContextKey] = map[string]any{
"worktreeId": isolation.WorktreeID,
"mode": isolation.Mode, "worktreePath": isolation.WorktreePath,
"branch": isolation.Branch, "baseCommit": isolation.BaseCommit,
}
return result
}
func sessionIsolationFromRuntimeContext(runtimeContext map[string]any) *SessionIsolation {
raw := runtimeContext[worktreeIsolationContextKey]
if raw == nil {
return nil
}
data, err := json.Marshal(raw)
if err != nil {
return nil
}
var isolation SessionIsolation
if err := json.Unmarshal(data, &isolation); err != nil || strings.TrimSpace(isolation.Mode) == "" {
return nil
}
return &isolation
}
func worktreeRecordsDir(worktreesRoot string) string {
return filepath.Join(worktreesRoot, ".metadata")
}
func worktreeRecordPath(worktreesRoot string, worktreeID string) string {
return filepath.Join(worktreeRecordsDir(worktreesRoot), worktreeID+".json")
}
func writeManagedWorktreeRecord(worktreesRoot string, record managedWorktreeRecord) error {
if err := os.MkdirAll(worktreeRecordsDir(worktreesRoot), 0o700); err != nil {
return fmt.Errorf("create worktree metadata directory: %w", err)
}
data, err := json.Marshal(record)
if err != nil {
return fmt.Errorf("marshal worktree metadata: %w", err)
}
path := worktreeRecordPath(worktreesRoot, record.WorktreeID)
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
return fmt.Errorf("write worktree metadata: %w", err)
}
written := false
defer func() {
_ = file.Close()
if !written {
_ = os.Remove(path)
}
}()
if _, err := file.Write(data); err != nil {
return fmt.Errorf("write worktree metadata: %w", err)
}
if err := file.Close(); err != nil {
return fmt.Errorf("write worktree metadata: %w", err)
}
written = true
return nil
}
func replaceManagedWorktreeRecord(worktreesRoot string, record managedWorktreeRecord) error {
dir := worktreeRecordsDir(worktreesRoot)
data, err := json.Marshal(record)
if err != nil {
return fmt.Errorf("marshal worktree metadata: %w", err)
}
temporary, err := os.CreateTemp(dir, "."+record.WorktreeID+"-*.tmp")
if err != nil {
return fmt.Errorf("create worktree metadata update: %w", err)
}
replaced := false
defer func() {
_ = temporary.Close()
if !replaced {
_ = os.Remove(temporary.Name())
}
}()
if err := temporary.Chmod(0o600); err != nil {
return fmt.Errorf("secure worktree metadata update: %w", err)
}
if _, err := temporary.Write(data); err != nil {
return fmt.Errorf("write worktree metadata update: %w", err)
}
if err := temporary.Sync(); err != nil {
return fmt.Errorf("sync worktree metadata update: %w", err)
}
if err := temporary.Close(); err != nil {
return fmt.Errorf("close worktree metadata update: %w", err)
}
if err := os.Rename(temporary.Name(), worktreeRecordPath(worktreesRoot, record.WorktreeID)); err != nil {
return fmt.Errorf("replace worktree metadata: %w", err)
}
replaced = true
return nil
}
func rollbackManagedWorktree(ctx context.Context, worktreesRoot string, record managedWorktreeRecord) {
_, _ = gitRepoOutput(ctx, record, "worktree", "remove", "--force", record.WorktreePath)
_ = os.RemoveAll(record.WorktreePath)
_, _ = gitRepoOutput(ctx, record, "branch", "-D", record.Branch)
_, _ = gitRepoOutput(ctx, record, "worktree", "prune")
_ = os.Remove(worktreeRecordPath(worktreesRoot, record.WorktreeID))
}
func (s *Service) rollbackSessionWorktree(ctx context.Context, isolation SessionIsolation) {
worktreesRoot := filepath.Join(s.worktreeStateDir(), "agent", "worktrees")
worktreeID := strings.TrimSpace(isolation.WorktreeID)
if worktreeID == "" {
worktreeID = filepath.Base(isolation.WorktreePath)
}
record, err := readManagedWorktreeRecord(worktreeRecordPath(worktreesRoot, worktreeID))
if err == nil {
rollbackManagedWorktree(ctx, worktreesRoot, record)
}
}
func readManagedWorktreeRecord(path string) (managedWorktreeRecord, error) {
data, err := os.ReadFile(path)
if err != nil {
return managedWorktreeRecord{}, err
}
var record managedWorktreeRecord
if err := json.Unmarshal(data, &record); err != nil {
return managedWorktreeRecord{}, err
}
if strings.TrimSpace(record.WorktreeID) == "" {
record.WorktreeID = strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
}
return record, nil
}
func canonicalWorktreePath(path string) string {
path = strings.TrimSpace(path)
if path == "" {
return ""
}
abs, err := filepath.Abs(path)
if err != nil {
return ""
}
existing := abs
var suffix []string
for {
if _, statErr := os.Stat(existing); statErr == nil {
break
}
parent := filepath.Dir(existing)
if parent == existing {
return filepath.Clean(abs)
}
suffix = append([]string{filepath.Base(existing)}, suffix...)
existing = parent
}
if resolved, resolveErr := filepath.EvalSymlinks(existing); resolveErr == nil {
abs = filepath.Join(append([]string{resolved}, suffix...)...)
}
return filepath.Clean(abs)
}