Changeset 0.26.0 (#165)
This commit is contained in:
@@ -138,6 +138,51 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
// Background session cleanup (v0.26.0): remove expired anonymous sessions
|
||||
if cfg.SessionExpiryDays > 0 {
|
||||
go func() {
|
||||
ticker := time.NewTicker(6 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
cutoff := time.Now().UTC().AddDate(0, 0, -cfg.SessionExpiryDays)
|
||||
if n, err := stores.Sessions.DeleteExpired(ctx, cutoff); err != nil {
|
||||
log.Printf("⚠ session cleanup failed: %v", err)
|
||||
} else if n > 0 {
|
||||
log.Printf("🧹 sessions: cleaned up %d expired sessions", n)
|
||||
}
|
||||
cancel()
|
||||
<-ticker.C
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Background workflow staleness sweep (v0.26.2): mark idle instances as stale
|
||||
if cfg.WorkflowStaleHours > 0 {
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
cutoff := time.Now().UTC().Add(-time.Duration(cfg.WorkflowStaleHours) * time.Hour)
|
||||
res, err := database.DB.ExecContext(ctx, database.Q(`
|
||||
UPDATE channels
|
||||
SET workflow_status = 'stale'
|
||||
WHERE type = 'workflow'
|
||||
AND workflow_status = 'active'
|
||||
AND last_activity_at < $1
|
||||
`), cutoff)
|
||||
if err != nil {
|
||||
log.Printf("⚠ workflow staleness sweep failed: %v", err)
|
||||
} else if n, _ := res.RowsAffected(); n > 0 {
|
||||
log.Printf("🧹 workflows: marked %d instances as stale", n)
|
||||
}
|
||||
cancel()
|
||||
<-ticker.C
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Bootstrap admin from env (K8s secret) — upserts on every restart
|
||||
handlers.BootstrapAdmin(cfg, stores)
|
||||
|
||||
@@ -276,6 +321,9 @@ func main() {
|
||||
|
||||
// Memory tools + extraction scanner (v0.18.0)
|
||||
tools.RegisterMemoryTools(stores, kbEmbedder)
|
||||
|
||||
// Workflow tools (v0.26.4 — AI-triggered stage advancement)
|
||||
tools.RegisterWorkflowTools(stores)
|
||||
memExtractor := memory.NewExtractor(stores, roleResolver, kbEmbedder)
|
||||
memScanner := memory.NewScanner(memExtractor, stores, memory.ScannerConfig{})
|
||||
memScanner.Start()
|
||||
@@ -488,6 +536,34 @@ func main() {
|
||||
protected.POST("/persona-groups/:id/members", pgH.AddMember)
|
||||
protected.DELETE("/persona-groups/:id/members/:memberId", pgH.RemoveMember)
|
||||
|
||||
// Workflows (v0.26.1 — team-owned staged processes)
|
||||
wfH := handlers.NewWorkflowHandler(stores)
|
||||
protected.GET("/workflows", wfH.List)
|
||||
protected.POST("/workflows", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfH.Create)
|
||||
protected.GET("/workflows/:id", wfH.Get)
|
||||
protected.PATCH("/workflows/:id", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfH.Update)
|
||||
protected.DELETE("/workflows/:id", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfH.Delete)
|
||||
protected.GET("/workflows/:id/stages", wfH.ListStages)
|
||||
protected.POST("/workflows/:id/stages", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfH.CreateStage)
|
||||
protected.PUT("/workflows/:id/stages/:sid", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfH.UpdateStage)
|
||||
protected.DELETE("/workflows/:id/stages/:sid", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfH.DeleteStage)
|
||||
protected.PATCH("/workflows/:id/stages/reorder", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfH.ReorderStages)
|
||||
protected.POST("/workflows/:id/publish", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfH.Publish)
|
||||
protected.GET("/workflows/:id/versions/:version", wfH.GetVersion)
|
||||
|
||||
// Workflow instances (v0.26.2 — runtime lifecycle)
|
||||
wfInstH := handlers.NewWorkflowInstanceHandler(stores)
|
||||
protected.POST("/workflows/:id/start", wfInstH.Start)
|
||||
protected.GET("/channels/:id/workflow/status", wfInstH.GetStatus)
|
||||
protected.POST("/channels/:id/workflow/advance", wfInstH.Advance)
|
||||
protected.POST("/channels/:id/workflow/reject", wfInstH.Reject)
|
||||
|
||||
// Workflow assignments (v0.26.4 — team assignment queue)
|
||||
wfAssignH := handlers.NewWorkflowAssignmentHandler()
|
||||
protected.GET("/workflow-assignments/mine", wfAssignH.ListMine)
|
||||
protected.POST("/workflow-assignments/:id/claim", wfAssignH.Claim)
|
||||
protected.POST("/workflow-assignments/:id/complete", wfAssignH.Complete)
|
||||
|
||||
// Channel models (v0.20.0 — multi-model @mention routing)
|
||||
chModelH := handlers.NewChannelModelHandler(stores)
|
||||
protected.GET("/channels/:id/models", chModelH.List)
|
||||
@@ -772,6 +848,10 @@ func main() {
|
||||
teamScoped.GET("/roles", teamRoles.ListTeamRoles)
|
||||
teamScoped.PUT("/roles/:role", teamRoles.UpdateTeamRole)
|
||||
teamScoped.DELETE("/roles/:role", teamRoles.DeleteTeamRole)
|
||||
|
||||
// Team workflow assignments (v0.26.4)
|
||||
teamAssignH := handlers.NewWorkflowAssignmentHandler()
|
||||
teamScoped.GET("/assignments", teamAssignH.ListForTeam)
|
||||
}
|
||||
|
||||
// Public global settings (non-admin users can read safe subset)
|
||||
@@ -998,6 +1078,13 @@ func main() {
|
||||
wfAPI.POST("/:id/completions", wfComp.Complete)
|
||||
}
|
||||
|
||||
// Workflow visitor entry (v0.26.3) — public start endpoint
|
||||
// NOTE: Cannot use /api/v1/w/:scope/:slug/start — Gin's radix trie
|
||||
// conflicts with /api/v1/w/:id/messages (different wildcard names at
|
||||
// same path position). Separate namespace avoids the collision.
|
||||
wfEntry := handlers.NewWorkflowEntryHandler(stores)
|
||||
base.POST("/api/v1/workflow-entry/:scope/:slug", wfEntry.StartVisitor)
|
||||
|
||||
bp := cfg.BasePath
|
||||
if bp == "" {
|
||||
bp = "/"
|
||||
|
||||
Reference in New Issue
Block a user