Files
Agent-System/internal/events/cutover.go
T
weichenandCursor 265f09c1f2 fix(server): cache health probes, surface SSE marshal errors, fail closed on missing duties
Health checks reuse a two-second snapshot and bound the schema query. Task SSE writes an error frame when an event cannot be encoded. ResolveDocTask errors when the frozen plan has no duty row, while explicit none-duties stay zero. Drop the unwired semantic-summary replace env, record the intentional dev versus release model split, document the test/main cutover path, and archive pre-goose SQL.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-19 09:16:18 +00:00

177 lines
4.9 KiB
Go

package events
import (
"context"
"fmt"
"strings"
"chat-agent-system/internal/app/uow"
"github.com/jackc/pgx/v5/pgxpool"
)
const OpsCutoverEnvironment = "dev"
// OpsCutoverPromotionPath is how test and main change consumer modes.
// ApplyOpsCutover stays dev-only. After a dev observation window, copy the
// reviewed mode with an authorized promotion receipt (SQL or migration), first
// on test, then on main. Do not widen ValidateOpsCutover to accept those envs.
const OpsCutoverPromotionPath = "dev-observe -> promotion-receipt on test -> same receipt on main; ApplyOpsCutover rejects non-dev"
// ValidateOpsCutover is the shared-dev gate for cluster mode changes.
// test/main, FAV, and unknown consumers are rejected before any write.
func ValidateOpsCutover(environment, consumer, mode string) error {
if strings.TrimSpace(environment) != OpsCutoverEnvironment {
return fmt.Errorf("%w: environment %q", ErrOpsCutover, environment)
}
switch consumer {
case ConsumerNotify, ConsumerWorkflowAdvance, ConsumerProjection:
default:
return fmt.Errorf("%w: consumer %q", ErrOpsCutover, consumer)
}
switch mode {
case ModeOff, ModeShadow, ModeLive:
default:
return fmt.Errorf("%w: mode %q", ErrOpsCutover, mode)
}
return nil
}
func ApplyOpsCutover(ctx context.Context, pool *pgxpool.Pool, environment, consumer, mode string) error {
if err := ValidateOpsCutover(environment, consumer, mode); err != nil {
return err
}
return SetMode(ctx, pool, environment, consumer, mode)
}
type ModeSnapshot struct {
Environment string `json:"environment"`
Consumer string `json:"consumer"`
Mode string `json:"mode"`
Revision int64 `json:"revision"`
}
type BacklogRow struct {
Consumer string `json:"consumer"`
Status string `json:"status"`
Count int64 `json:"count"`
}
func ListModes(ctx context.Context, pool *pgxpool.Pool) ([]ModeSnapshot, error) {
rows, err := pool.Query(ctx, `
SELECT environment, consumer, mode, revision
FROM event_consumer_modes
ORDER BY environment, consumer
`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []ModeSnapshot
for rows.Next() {
var m ModeSnapshot
if err := rows.Scan(&m.Environment, &m.Consumer, &m.Mode, &m.Revision); err != nil {
return nil, err
}
out = append(out, m)
}
return out, rows.Err()
}
func ListBacklog(ctx context.Context, pool *pgxpool.Pool) ([]BacklogRow, error) {
rows, err := pool.Query(ctx, `
SELECT consumer, status, COUNT(*)
FROM event_deliveries
GROUP BY consumer, status
ORDER BY consumer, status
`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []BacklogRow
for rows.Next() {
var r BacklogRow
if err := rows.Scan(&r.Consumer, &r.Status, &r.Count); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
type pendingDelivery struct {
EventID string
Consumer string
EventName string
Environment string
ProjectID int64
SchemaVersion int
PayloadDigest string
}
func loadPending(ctx context.Context, pool *pgxpool.Pool) ([]pendingDelivery, error) {
rows, err := pool.Query(ctx, `
SELECT d.event_id, d.consumer, e.event_name, e.environment, e.project_id, e.schema_version, COALESCE(e.payload_digest, '')
FROM event_deliveries d
INNER JOIN event_envelopes e ON e.event_id = d.event_id
WHERE d.status IN ('pending','leased')
`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []pendingDelivery
for rows.Next() {
var p pendingDelivery
if err := rows.Scan(&p.EventID, &p.Consumer, &p.EventName, &p.Environment, &p.ProjectID, &p.SchemaVersion, &p.PayloadDigest); err != nil {
return nil, err
}
out = append(out, p)
}
return out, rows.Err()
}
func ClassifyPending(ctx context.Context, pool *pgxpool.Pool) ([]InventoryRow, error) {
pending, err := loadPending(ctx, pool)
if err != nil {
return nil, err
}
out := make([]InventoryRow, 0, len(pending))
for _, p := range pending {
env := Envelope{
EventID: p.EventID, EventName: p.EventName, SchemaVersion: p.SchemaVersion,
Environment: p.Environment, ProjectID: p.ProjectID, PayloadDigest: p.PayloadDigest,
}
out = append(out, ClassifyEnvelope(env, p.Consumer))
}
return out, nil
}
// QuarantineIllegalPending persists quarantine for pending deliveries whose
// consumer is not on the contract (including FAV). Safe envelopes are left untouched.
func QuarantineIllegalPending(ctx context.Context, pool *pgxpool.Pool, operator, approval string) (int, error) {
if operator == "" || approval == "" {
return 0, ErrIntegrityFailure
}
rows, err := ClassifyPending(ctx, pool)
if err != nil {
return 0, err
}
n := 0
unit := uow.NewPG(pool)
for _, row := range rows {
if row.Classification != ClassificationQuarantine {
continue
}
err := unit.Within(ctx, func(scope uow.TxScope) error {
return PersistInventory(uow.WithScope(ctx, scope), row, operator, approval)
})
if err != nil {
return n, err
}
n++
}
return n, nil
}