This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+230 -28
View File
@@ -36,14 +36,22 @@ type Repository interface {
// Session
InsertSession(ctx context.Context, s *Session) (*Session, error)
GetSessionByID(ctx context.Context, id uuid.UUID) (*Session, error)
// GetSessionByStepID 返回某编排器 step 最近的 session(任意状态);无则 NotFound。
GetSessionByStepID(ctx context.Context, stepID uuid.UUID) (*Session, error)
// GetCrashedSessionForResume 返回某编排器 step 最近一次 crashed/exited 的 session(用于 resume)。
GetCrashedSessionForResume(ctx context.Context, stepID uuid.UUID) (*Session, error)
// requirementID 非 nil 时仅返回关联该需求的 sessions。
ListSessionsByUser(ctx context.Context, userID uuid.UUID, requirementID *uuid.UUID) ([]*Session, error)
ListAllSessions(ctx context.Context, requirementID *uuid.UUID) ([]*Session, error)
UpdateSessionRunning(ctx context.Context, id uuid.UUID, agentSessionID string, pid int32) error
// UpdateSessionSandboxMode 记录本 session 运行所用的沙箱模式(审计/取证)。
UpdateSessionSandboxMode(ctx context.Context, id uuid.UUID, mode string) error
UpdateSessionFinished(ctx context.Context, id uuid.UUID, status SessionStatus, exitCode *int32, lastErr *string) error
// MarkSessionFailedIfActive 把仍处于 starting/running 的 session 标记为 crashed
// 并写入 lastErr;已是终态时不更新(守卫式,避免覆盖 onExit 写入的终态)。
MarkSessionFailedIfActive(ctx context.Context, id uuid.UUID, lastErr string) (bool, error)
// ResetSessionForResume 把崩溃/退出 session 复用为新一轮(回 starting、清运行时字段)。
ResetSessionForResume(ctx context.Context, id uuid.UUID) error
CountActiveSessions(ctx context.Context) (int64, error)
CountActiveSessionsByUser(ctx context.Context, userID uuid.UUID) (int64, error)
ResetStuckSessionsOnRestart(ctx context.Context) ([]*Session, error)
@@ -57,6 +65,32 @@ type Repository interface {
ListEventsSince(ctx context.Context, sessionID uuid.UUID, sinceID int64, limit int32) ([]*Event, error)
PurgeEventsBefore(ctx context.Context, before time.Time) (int64, error)
// 成本核算 / 预算 / reaper
InsertSessionUsage(ctx context.Context, r *SessionUsageRecord) (inserted bool, err error)
AddSessionUsageTotals(ctx context.Context, sid uuid.UUID, dp, dc, dt int64, dCost float64) (totPrompt, totCompletion, totThinking int64, totCost float64, err error)
SumProjectCostUSD(ctx context.Context, projectID uuid.UUID, since time.Time) (float64, error)
MarkSessionTerminatedReason(ctx context.Context, sid uuid.UUID, reason string) error
ListSessionsForReaper(ctx context.Context, wallClockStartedBefore, idleSince time.Time) ([]ReaperSession, error)
ListSessionUsage(ctx context.Context, sessionID uuid.UUID, limit int32) ([]*SessionUsageRecord, error)
SummarizeAcpUsageByUser(ctx context.Context, from, to time.Time) ([]AcpUsageUserRow, error)
SummarizeAcpUsageByModel(ctx context.Context, from, to time.Time) ([]AcpUsageModelRow, error)
SummarizeAcpUsageByDay(ctx context.Context, from, to time.Time) ([]AcpUsageDayRow, error)
// run-history dashboard(只读聚合)
SessionRollup(ctx context.Context, f DashboardFilter) (SessionRollup, error)
SessionsByDay(ctx context.Context, f DashboardFilter) ([]SessionDayRow, error)
SessionTimeline(ctx context.Context, sessionID uuid.UUID) ([]SessionTimelineBucket, error)
// Turn(回合完成检测)
InsertTurn(ctx context.Context, t *Turn) (*Turn, error)
MarkTurnCompleted(ctx context.Context, sessionID uuid.UUID, turnIndex int, stop string, updateCount int) error
MarkTurnAborted(ctx context.Context, sessionID uuid.UUID, turnIndex int) error
IncrementTurnUpdateCount(ctx context.Context, sessionID uuid.UUID, turnIndex int) error
GetLatestTurnBySession(ctx context.Context, sessionID uuid.UUID) (*Turn, error)
ListTurnsBySession(ctx context.Context, sessionID uuid.UUID) ([]*Turn, error)
UpdateSessionLastStopReason(ctx context.Context, sessionID uuid.UUID, reason string) error
PurgeTurnsBefore(ctx context.Context, before time.Time) (int64, error)
// PermissionRequest
InsertPermissionRequest(ctx context.Context, p *PermissionRequest) (*PermissionRequest, error)
GetPermissionRequestByID(ctx context.Context, id uuid.UUID) (*PermissionRequest, error)
@@ -159,6 +193,10 @@ func (r *pgRepo) CreateAgentKind(ctx context.Context, k *AgentKind) (*AgentKind,
ToolAllowlist: normalizeStrSlice(k.ToolAllowlist),
ClientType: string(k.ClientType),
EncryptedMcpServers: k.EncryptedMCPServers,
ModelID: toPgUUIDPtr(k.ModelID),
MaxCostUsd: float64PtrToNumeric(k.MaxCostUSD),
MaxTokens: k.MaxTokens,
MaxWallClockSeconds: k.MaxWallClockSeconds,
})
if err != nil {
return nil, errs.Wrap(err, errs.CodeInternal, "create agent kind")
@@ -218,6 +256,10 @@ func (r *pgRepo) UpdateAgentKind(ctx context.Context, k *AgentKind) (*AgentKind,
ToolAllowlist: normalizeStrSlice(k.ToolAllowlist),
ClientType: string(k.ClientType),
EncryptedMcpServers: k.EncryptedMCPServers,
ModelID: toPgUUIDPtr(k.ModelID),
MaxCostUsd: float64PtrToNumeric(k.MaxCostUSD),
MaxTokens: k.MaxTokens,
MaxWallClockSeconds: k.MaxWallClockSeconds,
})
if err != nil {
return nil, pgxNoRowsToErrNotFound(err, errs.CodeAcpAgentKindNotFound, "agent kind not found")
@@ -300,6 +342,10 @@ func rowToAgentKind(row acpsqlc.AcpAgentKind) *AgentKind {
CreatedBy: fromPgUUID(row.CreatedBy),
CreatedAt: row.CreatedAt.Time,
UpdatedAt: row.UpdatedAt.Time,
ModelID: fromPgUUIDPtr(row.ModelID),
MaxCostUSD: numericToFloat64Ptr(row.MaxCostUsd),
MaxTokens: row.MaxTokens,
MaxWallClockSeconds: row.MaxWallClockSeconds,
}
}
@@ -328,17 +374,21 @@ func normalizeStrSlice(s []string) []string {
func (r *pgRepo) InsertSession(ctx context.Context, s *Session) (*Session, error) {
row, err := r.q.InsertSession(ctx, acpsqlc.InsertSessionParams{
ID: toPgUUID(s.ID),
WorkspaceID: toPgUUID(s.WorkspaceID),
ProjectID: toPgUUID(s.ProjectID),
AgentKindID: toPgUUID(s.AgentKindID),
UserID: toPgUUID(s.UserID),
IssueID: toPgUUIDPtr(s.IssueID),
RequirementID: toPgUUIDPtr(s.RequirementID),
Branch: s.Branch,
CwdPath: s.CwdPath,
IsMainWorktree: s.IsMainWorktree,
Status: string(s.Status),
ID: toPgUUID(s.ID),
WorkspaceID: toPgUUID(s.WorkspaceID),
ProjectID: toPgUUID(s.ProjectID),
AgentKindID: toPgUUID(s.AgentKindID),
UserID: toPgUUID(s.UserID),
IssueID: toPgUUIDPtr(s.IssueID),
RequirementID: toPgUUIDPtr(s.RequirementID),
Branch: s.Branch,
CwdPath: s.CwdPath,
IsMainWorktree: s.IsMainWorktree,
Status: string(s.Status),
OrchestratorStepID: toPgUUIDPtr(s.OrchestratorStepID),
BudgetMaxCostUsd: float64PtrToNumeric(s.BudgetMaxCostUSD),
BudgetMaxTokens: s.BudgetMaxTokens,
BudgetMaxWallClockSeconds: s.BudgetMaxWallClockSeconds,
})
if err != nil {
return nil, errs.Wrap(err, errs.CodeInternal, "insert session")
@@ -354,6 +404,22 @@ func (r *pgRepo) GetSessionByID(ctx context.Context, id uuid.UUID) (*Session, er
return rowToSession(row), nil
}
func (r *pgRepo) GetSessionByStepID(ctx context.Context, stepID uuid.UUID) (*Session, error) {
row, err := r.q.GetSessionByStepID(ctx, toPgUUID(stepID))
if err != nil {
return nil, pgxNoRowsToErrNotFound(err, errs.CodeAcpSessionNotFound, "session not found for step")
}
return rowToSession(row), nil
}
func (r *pgRepo) GetCrashedSessionForResume(ctx context.Context, stepID uuid.UUID) (*Session, error) {
row, err := r.q.GetCrashedSessionForResume(ctx, toPgUUID(stepID))
if err != nil {
return nil, pgxNoRowsToErrNotFound(err, errs.CodeAcpSessionNotFound, "no crashed session to resume")
}
return rowToSession(row), nil
}
func (r *pgRepo) ListSessionsByUser(ctx context.Context, userID uuid.UUID, requirementID *uuid.UUID) ([]*Session, error) {
rows, err := r.q.ListSessionsByUser(ctx, acpsqlc.ListSessionsByUserParams{
UserID: toPgUUID(userID),
@@ -392,6 +458,17 @@ func (r *pgRepo) UpdateSessionRunning(ctx context.Context, id uuid.UUID, agentSe
return nil
}
func (r *pgRepo) UpdateSessionSandboxMode(ctx context.Context, id uuid.UUID, mode string) error {
m := mode
if err := r.q.UpdateSessionSandboxMode(ctx, acpsqlc.UpdateSessionSandboxModeParams{
ID: toPgUUID(id),
SandboxMode: &m,
}); err != nil {
return errs.Wrap(err, errs.CodeInternal, "update session sandbox mode")
}
return nil
}
func (r *pgRepo) UpdateSessionFinished(ctx context.Context, id uuid.UUID, status SessionStatus, exitCode *int32, lastErr *string) error {
if err := r.q.UpdateSessionFinished(ctx, acpsqlc.UpdateSessionFinishedParams{
ID: toPgUUID(id),
@@ -415,6 +492,13 @@ func (r *pgRepo) MarkSessionFailedIfActive(ctx context.Context, id uuid.UUID, la
return n > 0, nil
}
func (r *pgRepo) ResetSessionForResume(ctx context.Context, id uuid.UUID) error {
if err := r.q.ResetSessionForResume(ctx, toPgUUID(id)); err != nil {
return errs.Wrap(err, errs.CodeInternal, "reset session for resume")
}
return nil
}
func (r *pgRepo) CountActiveSessions(ctx context.Context) (int64, error) {
n, err := r.q.CountActiveSessions(ctx)
if err != nil {
@@ -480,23 +564,34 @@ func rowToSession(row acpsqlc.AcpSession) *Session {
endedAt = &t
}
return &Session{
ID: fromPgUUID(row.ID),
WorkspaceID: fromPgUUID(row.WorkspaceID),
ProjectID: fromPgUUID(row.ProjectID),
AgentKindID: fromPgUUID(row.AgentKindID),
UserID: fromPgUUID(row.UserID),
IssueID: fromPgUUIDPtr(row.IssueID),
RequirementID: fromPgUUIDPtr(row.RequirementID),
AgentSessionID: row.AgentSessionID,
Branch: row.Branch,
CwdPath: row.CwdPath,
IsMainWorktree: row.IsMainWorktree,
Status: SessionStatus(row.Status),
PID: row.Pid,
ExitCode: row.ExitCode,
LastError: row.LastError,
StartedAt: row.StartedAt.Time,
EndedAt: endedAt,
ID: fromPgUUID(row.ID),
WorkspaceID: fromPgUUID(row.WorkspaceID),
ProjectID: fromPgUUID(row.ProjectID),
AgentKindID: fromPgUUID(row.AgentKindID),
UserID: fromPgUUID(row.UserID),
IssueID: fromPgUUIDPtr(row.IssueID),
RequirementID: fromPgUUIDPtr(row.RequirementID),
AgentSessionID: row.AgentSessionID,
Branch: row.Branch,
CwdPath: row.CwdPath,
IsMainWorktree: row.IsMainWorktree,
Status: SessionStatus(row.Status),
PID: row.Pid,
ExitCode: row.ExitCode,
LastError: row.LastError,
StartedAt: row.StartedAt.Time,
EndedAt: endedAt,
LastStopReason: row.LastStopReason,
OrchestratorStepID: fromPgUUIDPtr(row.OrchestratorStepID),
PromptTokens: row.PromptTokens,
CompletionTokens: row.CompletionTokens,
ThinkingTokens: row.ThinkingTokens,
TotalCostUSD: numericToFloat64(row.TotalCostUsd),
LastActivityAt: row.LastActivityAt.Time,
BudgetMaxCostUSD: numericToFloat64Ptr(row.BudgetMaxCostUsd),
BudgetMaxTokens: row.BudgetMaxTokens,
BudgetMaxWallClockSeconds: row.BudgetMaxWallClockSeconds,
TerminatedReason: row.TerminatedReason,
}
}
@@ -556,6 +651,113 @@ func rowToEvent(row acpsqlc.AcpEvent) *Event {
}
}
// ===== Turn(回合完成检测)=====
func (r *pgRepo) InsertTurn(ctx context.Context, t *Turn) (*Turn, error) {
row, err := r.q.InsertTurn(ctx, acpsqlc.InsertTurnParams{
SessionID: toPgUUID(t.SessionID),
TurnIndex: int32(t.TurnIndex),
PromptRequestID: t.PromptRequestID,
Status: string(t.Status),
})
if err != nil {
return nil, errs.Wrap(err, errs.CodeInternal, "insert acp turn")
}
return rowToTurn(row), nil
}
func (r *pgRepo) MarkTurnCompleted(ctx context.Context, sessionID uuid.UUID, turnIndex int, stop string, updateCount int) error {
if err := r.q.MarkTurnCompleted(ctx, acpsqlc.MarkTurnCompletedParams{
SessionID: toPgUUID(sessionID),
TurnIndex: int32(turnIndex),
StopReason: &stop,
UpdateCount: int32(updateCount),
}); err != nil {
return errs.Wrap(err, errs.CodeInternal, "mark turn completed")
}
return nil
}
func (r *pgRepo) MarkTurnAborted(ctx context.Context, sessionID uuid.UUID, turnIndex int) error {
if err := r.q.MarkTurnAborted(ctx, acpsqlc.MarkTurnAbortedParams{
SessionID: toPgUUID(sessionID),
TurnIndex: int32(turnIndex),
}); err != nil {
return errs.Wrap(err, errs.CodeInternal, "mark turn aborted")
}
return nil
}
func (r *pgRepo) IncrementTurnUpdateCount(ctx context.Context, sessionID uuid.UUID, turnIndex int) error {
if err := r.q.IncrementTurnUpdateCount(ctx, acpsqlc.IncrementTurnUpdateCountParams{
SessionID: toPgUUID(sessionID),
TurnIndex: int32(turnIndex),
}); err != nil {
return errs.Wrap(err, errs.CodeInternal, "increment turn update count")
}
return nil
}
func (r *pgRepo) GetLatestTurnBySession(ctx context.Context, sessionID uuid.UUID) (*Turn, error) {
row, err := r.q.GetLatestTurnBySession(ctx, toPgUUID(sessionID))
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, errs.Wrap(err, errs.CodeInternal, "get latest turn by session")
}
return rowToTurn(row), nil
}
func (r *pgRepo) ListTurnsBySession(ctx context.Context, sessionID uuid.UUID) ([]*Turn, error) {
rows, err := r.q.ListTurnsBySession(ctx, toPgUUID(sessionID))
if err != nil {
return nil, errs.Wrap(err, errs.CodeInternal, "list turns by session")
}
out := make([]*Turn, 0, len(rows))
for _, row := range rows {
out = append(out, rowToTurn(row))
}
return out, nil
}
func (r *pgRepo) UpdateSessionLastStopReason(ctx context.Context, sessionID uuid.UUID, reason string) error {
if err := r.q.UpdateSessionLastStopReason(ctx, acpsqlc.UpdateSessionLastStopReasonParams{
ID: toPgUUID(sessionID),
LastStopReason: &reason,
}); err != nil {
return errs.Wrap(err, errs.CodeInternal, "update session last stop reason")
}
return nil
}
func (r *pgRepo) PurgeTurnsBefore(ctx context.Context, before time.Time) (int64, error) {
n, err := r.q.PurgeTurnsBefore(ctx, pgtype.Timestamptz{Time: before, Valid: true})
if err != nil {
return 0, errs.Wrap(err, errs.CodeInternal, "purge acp turns")
}
return n, nil
}
func rowToTurn(row acpsqlc.AcpTurn) *Turn {
var completedAt *time.Time
if row.CompletedAt.Valid {
t := row.CompletedAt.Time
completedAt = &t
}
return &Turn{
ID: row.ID,
SessionID: fromPgUUID(row.SessionID),
TurnIndex: int(row.TurnIndex),
PromptRequestID: row.PromptRequestID,
Status: TurnStatus(row.Status),
StopReason: row.StopReason,
UpdateCount: int(row.UpdateCount),
StartedAt: row.StartedAt.Time,
CompletedAt: completedAt,
}
}
// ===== PermissionRequest =====
func (r *pgRepo) InsertPermissionRequest(ctx context.Context, p *PermissionRequest) (*PermissionRequest, error) {