Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

302 lines
7.2 KiB
Go

package postgres
import (
"context"
"database/sql"
"errors"
"fmt"
"log"
"strings"
"go.kenn.io/agentsview/internal/db"
)
type automatedAuditPGProgress struct {
RowsPrefetched int
RowsFullText int
}
const fullAutomationCandidatesPG = `SELECT
s.id,
s.first_message,
s.user_message_count,
s.is_automated,
(
SELECT m.content
FROM messages m
WHERE m.session_id = s.id
AND m.role = 'user'
AND COALESCE(m.is_system, false) = false
AND btrim(m.content) <> ''
ORDER BY m.ordinal
LIMIT 1
) AS first_user_message
FROM sessions s`
func backfillIsAutomatedPGWithProgress(
ctx context.Context, pg *sql.DB,
) (automatedAuditPGProgress, error) {
var progress automatedAuditPGProgress
current := db.ClassifierHash()
var stored string
err := pg.QueryRowContext(ctx,
`SELECT value FROM sync_metadata WHERE key = $1`,
db.ClassifierHashKey,
).Scan(&stored)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return progress, fmt.Errorf(
"probing PG classifier hash: %w", err,
)
}
var setIDs, clearIDs []string
classifier := db.SnapshotAutomationClassifier()
if stored == current {
setIDs, clearIDs, err = auditAutomatedMatchingHashPG(
ctx, pg, classifier, &progress,
)
} else {
setIDs, clearIDs, err = auditAutomatedFullPG(
ctx, pg, classifier, &progress,
)
}
if err != nil {
return progress, err
}
if err := batchUpdateAutomatedPG(ctx, pg, setIDs, true); err != nil {
return progress, err
}
if err := batchUpdateAutomatedPG(ctx, pg, clearIDs, false); err != nil {
return progress, err
}
if len(setIDs) > 0 || len(clearIDs) > 0 {
log.Printf(
"pg migration: recomputed is_automated"+
" (set %d, cleared %d)",
len(setIDs), len(clearIDs),
)
}
if _, err := pg.ExecContext(ctx,
`INSERT INTO sync_metadata (key, value)
VALUES ($1, $2)
ON CONFLICT (key) DO UPDATE
SET value = EXCLUDED.value`,
db.ClassifierHashKey, current,
); err != nil {
return progress, fmt.Errorf(
"storing PG classifier hash: %w", err,
)
}
return progress, nil
}
func auditAutomatedFullPG(
ctx context.Context,
pg *sql.DB,
classifier db.AutomationClassifier,
progress *automatedAuditPGProgress,
) (setIDs, clearIDs []string, err error) {
rows, err := pg.QueryContext(ctx, fullAutomationCandidatesPG)
if err != nil {
return nil, nil, fmt.Errorf(
"querying PG automated backfill candidates: %w", err,
)
}
setIDs, clearIDs, count, err := scanFullAutomationCandidatesPG(
rows, classifier,
)
if closeErr := rows.Close(); err == nil && closeErr != nil {
err = closeErr
}
progress.RowsFullText += count
return setIDs, clearIDs, err
}
func auditAutomatedMatchingHashPG(
ctx context.Context,
pg *sql.DB,
classifier db.AutomationClassifier,
progress *automatedAuditPGProgress,
) (setIDs, clearIDs []string, err error) {
rows, err := pg.QueryContext(ctx,
`SELECT
s.id,
s.user_message_count,
s.is_automated,
CASE WHEN s.user_message_count <= 1
THEN left(first_user.content, $1)
END AS first_user_prefix,
CASE WHEN s.user_message_count <= 1
THEN octet_length(first_user.content)
END AS first_user_length,
CASE
WHEN s.user_message_count <= 1
AND s.first_message IS NOT NULL
THEN left(s.first_message, $1)
END AS first_message_prefix,
CASE
WHEN s.user_message_count <= 1
AND s.first_message IS NOT NULL
THEN octet_length(s.first_message)
END AS first_message_length
FROM sessions s
LEFT JOIN LATERAL (
SELECT m.content
FROM messages m
WHERE m.session_id = s.id
AND m.role = 'user'
AND COALESCE(m.is_system, false) = false
AND btrim(m.content) <> ''
ORDER BY m.ordinal
LIMIT 1
) first_user ON s.user_message_count <= 1`,
db.AutomationEvidencePrefixBytes,
)
if err != nil {
return nil, nil, fmt.Errorf(
"querying bounded PG automated audit candidates: %w", err,
)
}
var unresolved []string
for rows.Next() {
var (
id string
userMessageCount int
rowAutomated bool
firstUserPrefix sql.NullString
firstUserLength sql.NullInt64
firstMessagePrefix sql.NullString
firstMessageLength sql.NullInt64
)
if err := rows.Scan(
&id,
&userMessageCount,
&rowAutomated,
&firstUserPrefix,
&firstUserLength,
&firstMessagePrefix,
&firstMessageLength,
); err != nil {
_ = rows.Close()
return nil, nil, fmt.Errorf(
"scanning bounded PG automated audit candidate: %w", err,
)
}
progress.RowsPrefetched++
want, conclusive := classifier.VerdictFromEvidence(
userMessageCount,
automationEvidencePG(firstUserPrefix, firstUserLength),
automationEvidencePG(firstMessagePrefix, firstMessageLength),
)
if !conclusive {
unresolved = append(unresolved, id)
continue
}
setIDs, clearIDs = appendAutomationFlagChangePG(
setIDs, clearIDs, id, rowAutomated, want,
)
}
if err := rows.Err(); err != nil {
_ = rows.Close()
return nil, nil, err
}
if err := rows.Close(); err != nil {
return nil, nil, err
}
const batchSize = 500
for start := 0; start < len(unresolved); start += batchSize {
end := min(start+batchSize, len(unresolved))
batch := unresolved[start:end]
pb := &paramBuilder{}
placeholders := make([]string, len(batch))
for i, id := range batch {
placeholders[i] = pb.add(id)
}
fullRows, err := pg.QueryContext(
ctx,
fullAutomationCandidatesPG+
" WHERE s.id IN ("+strings.Join(placeholders, ",")+")",
pb.args...,
)
if err != nil {
return nil, nil, fmt.Errorf(
"querying unresolved PG automated audit candidates: %w", err,
)
}
batchSet, batchClear, count, scanErr :=
scanFullAutomationCandidatesPG(fullRows, classifier)
if closeErr := fullRows.Close(); scanErr == nil && closeErr != nil {
scanErr = closeErr
}
if scanErr != nil {
return nil, nil, scanErr
}
progress.RowsFullText += count
setIDs = append(setIDs, batchSet...)
clearIDs = append(clearIDs, batchClear...)
}
return setIDs, clearIDs, nil
}
func scanFullAutomationCandidatesPG(
rows *sql.Rows,
classifier db.AutomationClassifier,
) (setIDs, clearIDs []string, count int, err error) {
for rows.Next() {
var (
id string
firstMessage sql.NullString
firstUser sql.NullString
userCount int
rowAutomated bool
)
if err := rows.Scan(
&id, &firstMessage, &userCount, &rowAutomated, &firstUser,
); err != nil {
return nil, nil, count, fmt.Errorf(
"scanning PG automated audit candidate: %w", err,
)
}
count++
want := classifier.IsAutomatedFromTextCandidates(
userCount, firstUser, firstMessage,
)
setIDs, clearIDs = appendAutomationFlagChangePG(
setIDs, clearIDs, id, rowAutomated, want,
)
}
if err := rows.Err(); err != nil {
return nil, nil, count, err
}
return setIDs, clearIDs, count, nil
}
func automationEvidencePG(
prefix sql.NullString,
fullByteLength sql.NullInt64,
) db.AutomationTextEvidence {
return db.AutomationTextEvidence{
Prefix: []byte(prefix.String),
FullByteLength: fullByteLength.Int64,
Valid: fullByteLength.Valid,
}
}
func appendAutomationFlagChangePG(
setIDs, clearIDs []string,
id string,
rowAutomated, want bool,
) ([]string, []string) {
if want && !rowAutomated {
setIDs = append(setIDs, id)
} else if !want && rowAutomated {
clearIDs = append(clearIDs, id)
}
return setIDs, clearIDs
}