Changeset 0.29.0 (#195)

This commit is contained in:
2026-03-17 16:28:47 +00:00
parent 128cbb8174
commit 5d637d3a90
129 changed files with 9418 additions and 3016 deletions

View File

@@ -1,9 +1,7 @@
// Package scheduler — system_builtins.go
//
// v0.28.6: Built-in system functions registered at startup.
// These mirror the background goroutines in main.go but run as visible,
// scheduled, auditable tasks. The goroutines remain as fallback until
// system tasks are validated in production.
// v0.29.0: Raw SQL replaced with store methods.
package scheduler
import (
@@ -12,13 +10,11 @@ import (
"log"
"time"
"git.gobha.me/xcaliber/chat-switchboard/database"
"git.gobha.me/xcaliber/chat-switchboard/store"
"git.gobha.me/xcaliber/chat-switchboard/taskutil"
)
// RegisterBuiltins registers all built-in system functions.
// Called once from main.go at startup.
func RegisterBuiltins() {
taskutil.RegisterSystemFunc("session_cleanup",
"Remove expired anonymous visitor sessions",
@@ -43,7 +39,6 @@ func sessionCleanup(ctx context.Context, stores store.Stores) (string, error) {
if stores.Sessions == nil {
return "skipped: session store not available", nil
}
// Default: expire sessions older than 7 days
cutoff := time.Now().UTC().AddDate(0, 0, -7)
n, err := stores.Sessions.DeleteExpired(ctx, cutoff)
if err != nil {
@@ -59,19 +54,11 @@ func sessionCleanup(ctx context.Context, stores store.Stores) (string, error) {
// ── staleness_check ─────────────────────────
func stalenessCheck(ctx context.Context, stores store.Stores) (string, error) {
// Default: 48 hours idle → stale
cutoff := time.Now().UTC().Add(-48 * 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)
n, err := stores.Channels.MarkStaleWorkflows(ctx, cutoff)
if err != nil {
return "", fmt.Errorf("staleness sweep failed: %w", err)
}
n, _ := res.RowsAffected()
msg := fmt.Sprintf("marked %d idle workflow instances as stale (cutoff: %s)", n, cutoff.Format(time.RFC3339))
if n > 0 {
log.Printf("[system_task] %s", msg)
@@ -82,28 +69,10 @@ func stalenessCheck(ctx context.Context, stores store.Stores) (string, error) {
// ── retention_sweep ─────────────────────────
func retentionSweep(ctx context.Context, stores store.Stores) (string, error) {
if database.IsSQLite() {
return "skipped: retention enforcement requires PostgreSQL (JSON operators)", nil
}
cutoff := time.Now().UTC().Add(-48 * time.Hour)
res, err := database.DB.ExecContext(ctx, database.Q(`
DELETE FROM channels
WHERE type = 'workflow'
AND workflow_status IN ('completed', 'archived')
AND workflow_id IS NOT NULL
AND last_activity_at < $1
AND workflow_id IN (
SELECT id FROM workflows
WHERE retention IS NOT NULL
AND retention->>'mode' = 'delete'
AND (retention->>'delete_after_days')::int > 0
AND channels.last_activity_at < now() - ((retention->>'delete_after_days')::int || ' days')::interval
)
`), cutoff)
n, err := stores.Channels.EnforceWorkflowRetention(ctx)
if err != nil {
return "", fmt.Errorf("retention enforcement failed: %w", err)
}
n, _ := res.RowsAffected()
msg := fmt.Sprintf("deleted %d expired workflow instances (retention policy)", n)
if n > 0 {
log.Printf("[system_task] %s", msg)
@@ -114,14 +83,14 @@ func retentionSweep(ctx context.Context, stores store.Stores) (string, error) {
// ── health_prune ────────────────────────────
func healthPrune(ctx context.Context, stores store.Stores) (string, error) {
if stores.Health == nil {
return "skipped: health store not available", nil
}
cutoff := time.Now().UTC().Add(-7 * 24 * time.Hour)
res, err := database.DB.ExecContext(ctx, database.Q(`
DELETE FROM health_windows WHERE window_start < $1
`), cutoff)
n, err := stores.Health.Prune(ctx, cutoff)
if err != nil {
return "", fmt.Errorf("health prune failed: %w", err)
}
n, _ := res.RowsAffected()
msg := fmt.Sprintf("pruned %d old health windows (cutoff: %s)", n, cutoff.Format(time.RFC3339))
if n > 0 {
log.Printf("[system_task] %s", msg)