diff --git a/apps/backend/cmd/kandev/adapters.go b/apps/backend/cmd/kandev/adapters.go index 38ed84b4a8..a051c50b9e 100644 --- a/apps/backend/cmd/kandev/adapters.go +++ b/apps/backend/cmd/kandev/adapters.go @@ -341,7 +341,8 @@ func (a *lifecycleAdapter) ResolveAgentProfile(ctx context.Context, profileID st } // GetGitLog retrieves the git log for a session from baseCommit to HEAD. -func (a *lifecycleAdapter) GetGitLog(ctx context.Context, sessionID, baseCommit string, limit int) (*client.GitLogResult, error) { +// If targetBranch is provided, uses dynamic merge-base calculation for accurate filtering. +func (a *lifecycleAdapter) GetGitLog(ctx context.Context, sessionID, baseCommit string, limit int, targetBranch string) (*client.GitLogResult, error) { execution, ok := a.mgr.GetExecutionBySessionID(sessionID) if !ok { return nil, nil // No execution, not an error @@ -350,7 +351,7 @@ func (a *lifecycleAdapter) GetGitLog(ctx context.Context, sessionID, baseCommit if agentClient == nil { return nil, nil } - return agentClient.GitLog(ctx, baseCommit, limit) + return agentClient.GitLog(ctx, baseCommit, limit, targetBranch) } // GetCumulativeDiff retrieves the cumulative diff for a session from baseCommit to HEAD. diff --git a/apps/backend/cmd/kandev/gateway.go b/apps/backend/cmd/kandev/gateway.go index 66c9b2ee70..76ea3492df 100644 --- a/apps/backend/cmd/kandev/gateway.go +++ b/apps/backend/cmd/kandev/gateway.go @@ -67,6 +67,22 @@ func (a *sessionReaderAdapter) GetSessionBaseCommit(ctx context.Context, session return session.BaseCommitSHA } +func (a *sessionReaderAdapter) GetSessionBaseBranch(ctx context.Context, sessionID string) string { + session, err := a.repo.GetTaskSession(ctx, sessionID) + if err != nil { + if a.logger != nil { + a.logger.Warn("failed to load session for base branch lookup", + zap.String("session_id", sessionID), + zap.Error(err)) + } + return "" + } + if session == nil { + return "" + } + return session.BaseBranch +} + func provideGateway( ctx context.Context, log *logger.Logger, diff --git a/apps/backend/internal/agent/handlers/git_handlers.go b/apps/backend/internal/agent/handlers/git_handlers.go index d54fe81995..0a25896ceb 100644 --- a/apps/backend/internal/agent/handlers/git_handlers.go +++ b/apps/backend/internal/agent/handlers/git_handlers.go @@ -3,6 +3,7 @@ package handlers import ( "context" + "errors" "fmt" "strings" "time" @@ -25,6 +26,9 @@ type GitOperationFailedCallback func(ctx context.Context, sessionID, taskID, ope // ExecutionLookup provides access to running agent executions by session ID. type ExecutionLookup interface { GetExecutionBySessionID(sessionID string) (*lifecycle.AgentExecution, bool) + // GetOrEnsureExecution returns an existing execution or creates one on-demand. + // Use this for workspace-oriented operations that should survive backend restarts. + GetOrEnsureExecution(ctx context.Context, sessionID string) (*lifecycle.AgentExecution, error) } // SessionReader is a minimal interface for reading session metadata. @@ -34,6 +38,11 @@ type SessionReader interface { // GetSessionBaseCommit returns the base commit SHA for a session. // Returns empty string if not set or on error. GetSessionBaseCommit(ctx context.Context, sessionID string) string + + // GetSessionBaseBranch returns the target branch for a session (e.g., "origin/main"). + // Used for computing merge-base to filter commits accurately after rebases. + // Returns empty string if not set or on error. + GetSessionBaseBranch(ctx context.Context, sessionID string) string } // GitHandlers provides WebSocket handlers for git worktree operations. @@ -635,6 +644,7 @@ type GitCommitsRequest struct { // wsGitCommits handles session.git.commits action // The base commit SHA is always looked up from the session metadata in the database. // This ensures commits are filtered to only those made during the session. +// When a target branch is available, we use dynamic merge-base calculation for accuracy. func (h *GitHandlers) wsGitCommits(ctx context.Context, msg *ws.Message) (*ws.Message, error) { var req GitCommitsRequest if err := msg.ParsePayload(&req); err != nil { @@ -645,21 +655,36 @@ func (h *GitHandlers) wsGitCommits(ctx context.Context, msg *ws.Message) (*ws.Me return nil, fmt.Errorf("session_id is required") } - agentClient, err := h.getAgentCtlClient(req.SessionID) + // Use GetOrEnsureExecution to recover workspace after backend restarts. + // This is a workspace-oriented operation that doesn't require a running agent process. + execution, err := h.lifecycleMgr.GetOrEnsureExecution(ctx, req.SessionID) if err != nil { - if isSessionNotReadyError(err) { + // Check for specific "not ready" errors that indicate the session workspace + // is still being prepared. For these, return ready:false so the client can retry. + if errors.Is(err, lifecycle.ErrSessionWorkspaceNotReady) || isSessionNotReadyError(err) { return ws.NewResponse(msg.ID, msg.Action, map[string]any{ "commits": []any{}, "ready": false, }) } - return nil, err + // For unexpected errors (database failures, etc.), return the error + // so the client can display an appropriate error message. + return nil, fmt.Errorf("failed to get execution for session %s: %w", req.SessionID, err) } - // Look up base commit SHA from the session metadata - var baseCommit string + agentClient := execution.GetAgentCtlClient() + if agentClient == nil { + return ws.NewResponse(msg.ID, msg.Action, map[string]any{ + "commits": []any{}, + "ready": false, + }) + } + + // Look up base commit SHA and target branch from the session metadata + var baseCommit, targetBranch string if h.sessionReader != nil { baseCommit = h.sessionReader.GetSessionBaseCommit(ctx, req.SessionID) + targetBranch = h.sessionReader.GetSessionBaseBranch(ctx, req.SessionID) } // Fallback: if base_commit_sha is not stored in session, use git merge-base @@ -675,7 +700,9 @@ func (h *GitHandlers) wsGitCommits(ctx context.Context, msg *ws.Message) (*ws.Me } } - result, err := agentClient.GitLog(ctx, baseCommit, req.Limit) + // Use target branch for dynamic merge-base calculation. + // This ensures accurate commit filtering even after rebases. + result, err := agentClient.GitLog(ctx, baseCommit, req.Limit, targetBranch) if err != nil { return nil, fmt.Errorf("git log failed: %w", err) } diff --git a/apps/backend/internal/agent/handlers/git_handlers_test.go b/apps/backend/internal/agent/handlers/git_handlers_test.go index 73fbb2dda6..e4ba4083cb 100644 --- a/apps/backend/internal/agent/handlers/git_handlers_test.go +++ b/apps/backend/internal/agent/handlers/git_handlers_test.go @@ -3,6 +3,7 @@ package handlers import ( "context" "encoding/json" + "errors" "fmt" "sync" "testing" @@ -16,6 +17,8 @@ import ( // mockExecutionLookup implements ExecutionLookup for testing. type mockExecutionLookup struct { executions map[string]*lifecycle.AgentExecution + // ensureErr allows tests to inject specific errors from GetOrEnsureExecution + ensureErr error } func (m *mockExecutionLookup) GetExecutionBySessionID(sessionID string) (*lifecycle.AgentExecution, bool) { @@ -26,9 +29,25 @@ func (m *mockExecutionLookup) GetExecutionBySessionID(sessionID string) (*lifecy return exec, ok } +func (m *mockExecutionLookup) GetOrEnsureExecution(_ context.Context, sessionID string) (*lifecycle.AgentExecution, error) { + // Return injected error if set + if m.ensureErr != nil { + return nil, m.ensureErr + } + if m.executions == nil { + return nil, fmt.Errorf("no execution for session %s", sessionID) + } + exec, ok := m.executions[sessionID] + if !ok { + return nil, fmt.Errorf("no execution for session %s", sessionID) + } + return exec, nil +} + // mockSessionReader implements SessionReader for testing. type mockSessionReader struct { - baseCommits map[string]string + baseCommits map[string]string + baseBranches map[string]string } func (m *mockSessionReader) GetSessionBaseCommit(_ context.Context, sessionID string) string { @@ -38,6 +57,13 @@ func (m *mockSessionReader) GetSessionBaseCommit(_ context.Context, sessionID st return m.baseCommits[sessionID] } +func (m *mockSessionReader) GetSessionBaseBranch(_ context.Context, sessionID string) string { + if m.baseBranches == nil { + return "" + } + return m.baseBranches[sessionID] +} + func TestNewGitHandlers(t *testing.T) { log := newTestLogger() lookup := &mockExecutionLookup{} @@ -335,7 +361,10 @@ func TestWsPush_NoExecution(t *testing.T) { func TestWsGitCommits_NotReady(t *testing.T) { log := newTestLogger() - lookup := &mockExecutionLookup{} // no executions → "no agent running" error + // Use "no agent running for session" error which matches isSessionNotReadyError + lookup := &mockExecutionLookup{ + ensureErr: fmt.Errorf("no agent running for session session-1"), + } h := NewGitHandlers(lookup, nil, log) msg, _ := ws.NewRequest("test-1", ws.ActionSessionGitCommits, GitCommitsRequest{SessionID: "session-1"}) @@ -411,3 +440,76 @@ func TestIsSessionNotReadyError(t *testing.T) { }) } } + +func TestWsGitCommits_WorkspaceNotReadyError(t *testing.T) { + // Test that ErrSessionWorkspaceNotReady returns ready:false gracefully + log := newTestLogger() + lookup := &mockExecutionLookup{ + ensureErr: lifecycle.ErrSessionWorkspaceNotReady, + } + h := NewGitHandlers(lookup, nil, log) + + msg, _ := ws.NewRequest("test-1", ws.ActionSessionGitCommits, GitCommitsRequest{SessionID: "session-1"}) + + resp, err := h.wsGitCommits(context.Background(), msg) + if err != nil { + t.Fatalf("expected graceful response for workspace not ready, got error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } + + var payload map[string]any + if err := json.Unmarshal(resp.Payload, &payload); err != nil { + t.Fatalf("failed to parse payload: %v", err) + } + if ready, ok := payload["ready"].(bool); !ok || ready { + t.Errorf("expected ready=false, got %v", payload["ready"]) + } +} + +func TestWsGitCommits_UnexpectedError(t *testing.T) { + // Test that unexpected errors (not workspace-not-ready) are returned as errors + log := newTestLogger() + lookup := &mockExecutionLookup{ + ensureErr: errors.New("database connection failed"), + } + h := NewGitHandlers(lookup, nil, log) + + msg, _ := ws.NewRequest("test-1", ws.ActionSessionGitCommits, GitCommitsRequest{SessionID: "session-1"}) + + _, err := h.wsGitCommits(context.Background(), msg) + if err == nil { + t.Fatal("expected error for unexpected failure") + } + if !errors.Is(err, lookup.ensureErr) && err.Error() != "failed to get execution for session session-1: database connection failed" { + t.Errorf("expected wrapped database error, got: %v", err) + } +} + +func TestWsGitCommits_WrappedWorkspaceNotReadyError(t *testing.T) { + // Test that wrapped ErrSessionWorkspaceNotReady is still detected + log := newTestLogger() + lookup := &mockExecutionLookup{ + ensureErr: fmt.Errorf("some context: %w", lifecycle.ErrSessionWorkspaceNotReady), + } + h := NewGitHandlers(lookup, nil, log) + + msg, _ := ws.NewRequest("test-1", ws.ActionSessionGitCommits, GitCommitsRequest{SessionID: "session-1"}) + + resp, err := h.wsGitCommits(context.Background(), msg) + if err != nil { + t.Fatalf("expected graceful response for wrapped workspace not ready, got error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } + + var payload map[string]any + if err := json.Unmarshal(resp.Payload, &payload); err != nil { + t.Fatalf("failed to parse payload: %v", err) + } + if ready, ok := payload["ready"].(bool); !ok || ready { + t.Errorf("expected ready=false, got %v", payload["ready"]) + } +} diff --git a/apps/backend/internal/agentctl/client/git.go b/apps/backend/internal/agentctl/client/git.go index 237de7be0d..0cf7cf6cbf 100644 --- a/apps/backend/internal/agentctl/client/git.go +++ b/apps/backend/internal/agentctl/client/git.go @@ -326,8 +326,13 @@ type GitCommitInfo struct { // GitLog gets the commit log from baseCommit to HEAD. // If baseCommit is empty, returns recent commits (limited by limit). -func (c *Client) GitLog(ctx context.Context, baseCommit string, limit int) (*GitLogResult, error) { - reqURL := fmt.Sprintf("%s/api/v1/git/log?since=%s&limit=%d", c.baseURL, url.QueryEscape(baseCommit), limit) +// If targetBranch is provided, computes merge-base dynamically for accurate filtering. +func (c *Client) GitLog(ctx context.Context, baseCommit string, limit int, targetBranch string) (*GitLogResult, error) { + reqURL := fmt.Sprintf("%s/api/v1/git/log?since=%s&limit=%d", + c.baseURL, url.QueryEscape(baseCommit), limit) + if targetBranch != "" { + reqURL += "&target_branch=" + url.QueryEscape(targetBranch) + } req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) if err != nil { diff --git a/apps/backend/internal/agentctl/server/api/git.go b/apps/backend/internal/agentctl/server/api/git.go index 3363c4f03d..b422746780 100644 --- a/apps/backend/internal/agentctl/server/api/git.go +++ b/apps/backend/internal/agentctl/server/api/git.go @@ -89,8 +89,9 @@ type GitResetRequest struct { // GitLogRequest for GET /api/v1/git/log type GitLogRequest struct { - Since string `form:"since"` // Base commit SHA (exclusive) - Limit int `form:"limit"` // Max commits to return + Since string `form:"since"` // Base commit SHA (exclusive) + TargetBranch string `form:"target_branch"` // Target branch for merge-base calculation (e.g., "origin/main") + Limit int `form:"limit"` // Max commits to return } // GitCumulativeDiffRequest for GET /api/v1/git/cumulative-diff @@ -541,7 +542,24 @@ func (s *Server) handleGitLog(c *gin.Context) { } gitOp := s.procMgr.GitOperator() - result, err := gitOp.GetLog(c.Request.Context(), req.Since, limit) + + // If target_branch is provided, compute merge-base dynamically. + // This ensures we only show commits not yet merged into the target branch, + // which is accurate even after rebases. + baseCommit := req.Since + if req.TargetBranch != "" { + mergeBase, err := gitOp.GetMergeBase(c.Request.Context(), "HEAD", req.TargetBranch) + if err != nil { + s.logger.Warn("failed to compute merge-base, falling back to since", + zap.String("target_branch", req.TargetBranch), + zap.Error(err)) + // Fall back to the provided base commit + } else if mergeBase != "" { + baseCommit = mergeBase + } + } + + result, err := gitOp.GetLog(c.Request.Context(), baseCommit, limit) if err != nil { s.logger.Error("git log failed", zap.Error(err)) c.JSON(http.StatusInternalServerError, process.GitLogResult{ diff --git a/apps/backend/internal/agentctl/server/process/git_log.go b/apps/backend/internal/agentctl/server/process/git_log.go index b48007705e..06e02a988a 100644 --- a/apps/backend/internal/agentctl/server/process/git_log.go +++ b/apps/backend/internal/agentctl/server/process/git_log.go @@ -342,3 +342,13 @@ func (g *GitOperator) parseCommitDiff(output string) map[string]interface{} { return files } + +// GetMergeBase returns the merge-base commit SHA between two refs (e.g., HEAD and origin/main). +// This is used to determine the common ancestor for filtering commits. +func (g *GitOperator) GetMergeBase(ctx context.Context, ref1, ref2 string) (string, error) { + output, err := g.runGitCommand(ctx, "merge-base", ref1, ref2) + if err != nil { + return "", fmt.Errorf("failed to compute merge-base: %w", err) + } + return strings.TrimSpace(output), nil +} diff --git a/apps/backend/internal/integration/simulated_agent_manager_test.go b/apps/backend/internal/integration/simulated_agent_manager_test.go index 7083b0c9f1..13079decbe 100644 --- a/apps/backend/internal/integration/simulated_agent_manager_test.go +++ b/apps/backend/internal/integration/simulated_agent_manager_test.go @@ -452,7 +452,7 @@ func (s *SimulatedAgentManagerClient) GetExecutionIDForSession(_ context.Context } return "", fmt.Errorf("no execution found for session %s", sessionID) } -func (s *SimulatedAgentManagerClient) GetGitLog(_ context.Context, _, _ string, _ int) (*client.GitLogResult, error) { +func (s *SimulatedAgentManagerClient) GetGitLog(_ context.Context, _, _ string, _ int, _ string) (*client.GitLogResult, error) { return nil, nil } func (s *SimulatedAgentManagerClient) GetCumulativeDiff(_ context.Context, _, _ string) (*client.CumulativeDiffResult, error) { diff --git a/apps/backend/internal/orchestrator/event_handlers_test.go b/apps/backend/internal/orchestrator/event_handlers_test.go index 365c47e22b..777775df3d 100644 --- a/apps/backend/internal/orchestrator/event_handlers_test.go +++ b/apps/backend/internal/orchestrator/event_handlers_test.go @@ -219,7 +219,7 @@ func (m *mockAgentManager) EnsureWorkspaceExecutionForSession(_ context.Context, func (m *mockAgentManager) GetExecutionIDForSession(_ context.Context, _ string) (string, error) { return "", fmt.Errorf("no execution found") } -func (m *mockAgentManager) GetGitLog(_ context.Context, _, _ string, _ int) (*client.GitLogResult, error) { +func (m *mockAgentManager) GetGitLog(_ context.Context, _, _ string, _ int, _ string) (*client.GitLogResult, error) { return nil, nil } func (m *mockAgentManager) GetCumulativeDiff(_ context.Context, _, _ string) (*client.CumulativeDiffResult, error) { diff --git a/apps/backend/internal/orchestrator/executor/executor.go b/apps/backend/internal/orchestrator/executor/executor.go index 3e3aef7090..45c82493cb 100644 --- a/apps/backend/internal/orchestrator/executor/executor.go +++ b/apps/backend/internal/orchestrator/executor/executor.go @@ -165,8 +165,9 @@ type AgentManagerClient interface { GetExecutionIDForSession(ctx context.Context, sessionID string) (string, error) // GetGitLog retrieves the git log for a session from baseCommit to HEAD. + // If targetBranch is provided, uses dynamic merge-base calculation for accurate filtering. // Used for archive snapshot capture. Returns nil, nil if no execution exists. - GetGitLog(ctx context.Context, sessionID, baseCommit string, limit int) (*client.GitLogResult, error) + GetGitLog(ctx context.Context, sessionID, baseCommit string, limit int, targetBranch string) (*client.GitLogResult, error) // GetCumulativeDiff retrieves the cumulative diff for a session from baseCommit to the // working tree (including uncommitted/unstaged changes). Used for archive snapshot capture. diff --git a/apps/backend/internal/orchestrator/executor/executor_mocks_test.go b/apps/backend/internal/orchestrator/executor/executor_mocks_test.go index d5d7f0a938..440a89e536 100644 --- a/apps/backend/internal/orchestrator/executor/executor_mocks_test.go +++ b/apps/backend/internal/orchestrator/executor/executor_mocks_test.go @@ -136,7 +136,7 @@ func (m *mockAgentManager) ResolveAgentProfile(ctx context.Context, profileID st }, nil } -func (m *mockAgentManager) GetGitLog(ctx context.Context, sessionID, baseCommit string, limit int) (*client.GitLogResult, error) { +func (m *mockAgentManager) GetGitLog(ctx context.Context, sessionID, baseCommit string, limit int, targetBranch string) (*client.GitLogResult, error) { return nil, nil } diff --git a/apps/backend/internal/orchestrator/scheduler/scheduler_test.go b/apps/backend/internal/orchestrator/scheduler/scheduler_test.go index 5e55875592..ebcb0b8131 100644 --- a/apps/backend/internal/orchestrator/scheduler/scheduler_test.go +++ b/apps/backend/internal/orchestrator/scheduler/scheduler_test.go @@ -133,7 +133,7 @@ func (m *mockAgentManager) ResolveAgentProfile(ctx context.Context, profileID st }, nil } -func (m *mockAgentManager) GetGitLog(_ context.Context, _, _ string, _ int) (*client.GitLogResult, error) { +func (m *mockAgentManager) GetGitLog(_ context.Context, _, _ string, _ int, _ string) (*client.GitLogResult, error) { return nil, nil } func (m *mockAgentManager) GetCumulativeDiff(_ context.Context, _, _ string) (*client.CumulativeDiffResult, error) { diff --git a/apps/backend/internal/orchestrator/task_operations.go b/apps/backend/internal/orchestrator/task_operations.go index 53c47ddefd..79d77c72b3 100644 --- a/apps/backend/internal/orchestrator/task_operations.go +++ b/apps/backend/internal/orchestrator/task_operations.go @@ -1189,35 +1189,48 @@ func (s *Service) StopExecution(ctx context.Context, executionID string, reason func (s *Service) CaptureArchiveSnapshot(ctx context.Context, sessionID string) error { s.logger.Info("capturing archive snapshot", zap.String("session_id", sessionID)) - baseCommit, err := s.resolveArchiveBaseCommit(ctx, sessionID) + baseCommit, baseBranch, err := s.resolveArchiveBaseCommitAndBranch(ctx, sessionID) if err != nil { return err } - if baseCommit == "" { - s.logger.Debug("no base_commit available, skipping archive snapshot capture", + + // Skip only if we have neither baseCommit nor baseBranch for merge-base calculation + if baseCommit == "" && baseBranch == "" { + s.logger.Debug("no base_commit or base_branch available, skipping archive snapshot capture", zap.String("session_id", sessionID)) return nil } - if !s.captureArchiveCommits(ctx, sessionID, baseCommit) { + // Capture commits - baseBranch can be used for merge-base even if baseCommit is empty + if !s.captureArchiveCommits(ctx, sessionID, baseCommit, baseBranch) { // Agent not running, skip diff capture as well return nil } + // Diff capture requires baseCommit + if baseCommit == "" { + s.logger.Debug("no base_commit available, skipping archive diff capture", + zap.String("session_id", sessionID)) + return nil + } + s.captureArchiveDiff(ctx, sessionID, baseCommit) return nil } -// resolveArchiveBaseCommit retrieves the base commit for archive snapshot capture. -// It first checks the session's stored base_commit_sha, falling back to git status if empty. -func (s *Service) resolveArchiveBaseCommit(ctx context.Context, sessionID string) (string, error) { +// resolveArchiveBaseCommitAndBranch retrieves the base commit and branch for archive snapshot capture. +// It first checks the session's stored values, falling back to git status if empty. +func (s *Service) resolveArchiveBaseCommitAndBranch(ctx context.Context, sessionID string) (string, string, error) { session, err := s.repo.GetTaskSession(ctx, sessionID) if err != nil { - return "", fmt.Errorf("failed to get session: %w", err) + return "", "", fmt.Errorf("failed to get session: %w", err) } - if session.BaseCommitSHA != "" { - return session.BaseCommitSHA, nil + baseCommit := session.BaseCommitSHA + baseBranch := session.BaseBranch + + if baseCommit != "" { + return baseCommit, baseBranch, nil } // Fallback: try to get base commit from git status for legacy sessions @@ -1226,21 +1239,22 @@ func (s *Service) resolveArchiveBaseCommit(ctx context.Context, sessionID string s.logger.Debug("failed to get git status for base commit fallback", zap.String("session_id", sessionID), zap.Error(err)) - return "", nil + return "", baseBranch, nil } if status != nil && status.BaseCommit != "" { s.logger.Debug("using git status base commit as fallback for archive", zap.String("session_id", sessionID), zap.String("base_commit", status.BaseCommit)) - return status.BaseCommit, nil + return status.BaseCommit, baseBranch, nil } - return "", nil + return "", baseBranch, nil } // captureArchiveCommits fetches and saves commits from baseCommit to HEAD. +// If targetBranch is provided, uses dynamic merge-base for accurate filtering. // Returns false if the agent is not running (caller should skip remaining capture). -func (s *Service) captureArchiveCommits(ctx context.Context, sessionID, baseCommit string) bool { - logResult, err := s.agentManager.GetGitLog(ctx, sessionID, baseCommit, 0) // 0 = no limit +func (s *Service) captureArchiveCommits(ctx context.Context, sessionID, baseCommit, targetBranch string) bool { + logResult, err := s.agentManager.GetGitLog(ctx, sessionID, baseCommit, 0, targetBranch) // 0 = no limit if err != nil { s.logger.Warn("failed to capture git log for archive", zap.String("session_id", sessionID),