RC-15-PLACE
release-tag / release-image (push) Successful in 3m56s

This commit is contained in:
2026-08-14 10:13:57 +02:00
parent 47c523dd98
commit 52d33b25fd
13 changed files with 1008 additions and 43 deletions
+325
View File
@@ -0,0 +1,325 @@
package data
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
)
var (
ErrPlaceInsufficientPoints = errors.New("not enough place points")
ErrPlaceBounds = errors.New("pixel outside place canvas")
ErrPlaceColor = errors.New("invalid place color")
)
type PlacePixel struct {
X int `json:"x"`
Y int `json:"y"`
ColorIndex int `json:"color_index"`
ClientID string `json:"client_id"`
PlacedAt time.Time `json:"placed_at"`
Revision int64 `json:"revision"`
}
type PlacePixelEvent struct {
X int `json:"x"`
Y int `json:"y"`
ColorIndex int `json:"color_index"`
ClientID string `json:"client_id"`
CreatedAt time.Time `json:"created_at"`
Revision int64 `json:"revision"`
}
type PlaceStats struct {
Revision int64 `json:"revision"`
FilledPixels int64 `json:"filled_pixels"`
TotalPlacements int64 `json:"total_placements"`
Participants int64 `json:"participants"`
}
type PlaceEarning struct {
SourceClientID string `json:"source_client_id"`
TaskID string `json:"task_id"`
OldScore float64 `json:"old_score"`
NewScore float64 `json:"new_score"`
Points float64 `json:"points"`
CreatedAt time.Time `json:"created_at"`
}
type PlaceWallet struct {
ClientID string `json:"client_id"`
BalancePoints float64 `json:"balance_points"`
EarnedPoints float64 `json:"earned_points"`
SpentPoints float64 `json:"spent_points"`
Placements int64 `json:"placements"`
OwnedPixels int64 `json:"owned_pixels"`
AvailablePixels int64 `json:"available_pixels"`
LinkedWorkers []string `json:"linked_workers"`
RecentEarnings []PlaceEarning `json:"recent_earnings"`
}
func milliToPoints(v int64) float64 { return float64(v) / 1000.0 }
func (s *Store) PlaceRevision(ctx context.Context) (int64, error) {
var revision int64
err := s.DB.QueryRowContext(ctx, `SELECT revision FROM place_state WHERE id=1`).Scan(&revision)
return revision, err
}
func (s *Store) PlacePixelsOutside(ctx context.Context, width, height int) (int64, error) {
var n int64
err := s.DB.QueryRowContext(ctx, `SELECT count(*) FROM place_pixels WHERE x>=? OR y>=?`, width, height).Scan(&n)
return n, err
}
func (s *Store) PlaceSnapshot(ctx context.Context) ([]PlacePixel, PlaceStats, error) {
var stats PlaceStats
if err := s.DB.QueryRowContext(ctx, `SELECT revision FROM place_state WHERE id=1`).Scan(&stats.Revision); err != nil {
return nil, stats, err
}
if err := s.DB.QueryRowContext(ctx, `SELECT count(*) FROM place_pixels`).Scan(&stats.FilledPixels); err != nil {
return nil, stats, err
}
if err := s.DB.QueryRowContext(ctx, `SELECT count(*) FROM place_pixel_events`).Scan(&stats.TotalPlacements); err != nil {
return nil, stats, err
}
if err := s.DB.QueryRowContext(ctx, `SELECT count(*) FROM place_wallets WHERE earned_milli>0 OR placements>0`).Scan(&stats.Participants); err != nil {
return nil, stats, err
}
rows, err := s.DB.QueryContext(ctx, `SELECT x,y,color_index,client_id,placed_at,revision FROM place_pixels ORDER BY y,x`)
if err != nil {
return nil, stats, err
}
defer rows.Close()
out := make([]PlacePixel, 0, stats.FilledPixels)
for rows.Next() {
var p PlacePixel
var placedMS int64
if err := rows.Scan(&p.X, &p.Y, &p.ColorIndex, &p.ClientID, &placedMS, &p.Revision); err != nil {
return nil, stats, err
}
p.PlacedAt = time.UnixMilli(placedMS).UTC()
out = append(out, p)
}
return out, stats, rows.Err()
}
func (s *Store) PlaceRecent(ctx context.Context, limit int) ([]PlacePixelEvent, error) {
if limit < 1 || limit > 100 {
limit = 30
}
rows, err := s.DB.QueryContext(ctx, `SELECT x,y,color_index,client_id,created_at,revision FROM place_pixel_events ORDER BY id DESC LIMIT ?`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]PlacePixelEvent, 0, limit)
for rows.Next() {
var e PlacePixelEvent
var createdMS int64
if err := rows.Scan(&e.X, &e.Y, &e.ColorIndex, &e.ClientID, &createdMS, &e.Revision); err != nil {
return nil, err
}
e.CreatedAt = time.UnixMilli(createdMS).UTC()
out = append(out, e)
}
return out, rows.Err()
}
func (s *Store) PlaceChanges(ctx context.Context, after int64, limit int) ([]PlacePixel, int64, error) {
if limit < 1 || limit > 5000 {
limit = 2000
}
var revision int64
if err := s.DB.QueryRowContext(ctx, `SELECT revision FROM place_state WHERE id=1`).Scan(&revision); err != nil {
return nil, 0, err
}
rows, err := s.DB.QueryContext(ctx, `SELECT x,y,color_index,client_id,placed_at,revision FROM place_pixels WHERE revision>? ORDER BY revision LIMIT ?`, after, limit)
if err != nil {
return nil, revision, err
}
defer rows.Close()
out := make([]PlacePixel, 0)
for rows.Next() {
var p PlacePixel
var placedMS int64
if err := rows.Scan(&p.X, &p.Y, &p.ColorIndex, &p.ClientID, &placedMS, &p.Revision); err != nil {
return nil, revision, err
}
p.PlacedAt = time.UnixMilli(placedMS).UTC()
out = append(out, p)
}
if err := rows.Err(); err != nil {
return nil, revision, err
}
if len(out) == limit && len(out) > 0 {
revision = out[len(out)-1].Revision
}
return out, revision, nil
}
func (s *Store) PlaceWallet(ctx context.Context, clientID string, pixelCostPoints int) (PlaceWallet, error) {
now := time.Now().UTC().UnixMilli()
if _, err := s.DB.ExecContext(ctx, `INSERT INTO place_wallets(client_id,updated_at) VALUES(?,?) ON CONFLICT(client_id) DO NOTHING`, clientID, now); err != nil {
return PlaceWallet{}, err
}
var balance, earned, spent int64
var out PlaceWallet
out.ClientID = clientID
out.LinkedWorkers = []string{}
out.RecentEarnings = []PlaceEarning{}
if err := s.DB.QueryRowContext(ctx, `SELECT balance_milli,earned_milli,spent_milli,placements FROM place_wallets WHERE client_id=?`, clientID).
Scan(&balance, &earned, &spent, &out.Placements); err != nil {
return out, err
}
out.BalancePoints = milliToPoints(balance)
out.EarnedPoints = milliToPoints(earned)
out.SpentPoints = milliToPoints(spent)
if err := s.DB.QueryRowContext(ctx, `SELECT count(*) FROM place_pixels WHERE client_id=?`, clientID).Scan(&out.OwnedPixels); err != nil {
return out, err
}
costMilli := int64(pixelCostPoints) * 1000
if costMilli > 0 {
out.AvailablePixels = balance / costMilli
}
workers, err := s.DB.QueryContext(ctx, `SELECT worker_client_id FROM identity_delegations WHERE owner_client_id=? ORDER BY worker_client_id`, clientID)
if err != nil {
return out, err
}
for workers.Next() {
var id string
if err := workers.Scan(&id); err != nil {
workers.Close()
return out, err
}
out.LinkedWorkers = append(out.LinkedWorkers, id)
}
if err := workers.Err(); err != nil {
workers.Close()
return out, err
}
workers.Close()
rows, err := s.DB.QueryContext(ctx, `SELECT source_client_id,task_id,old_score,new_score,points_milli,created_at FROM place_progress_events WHERE owner_client_id=? ORDER BY created_at DESC,id DESC LIMIT 12`, clientID)
if err != nil {
return out, err
}
for rows.Next() {
var e PlaceEarning
var pointsMilli, createdMS int64
if err := rows.Scan(&e.SourceClientID, &e.TaskID, &e.OldScore, &e.NewScore, &pointsMilli, &createdMS); err != nil {
rows.Close()
return out, err
}
e.Points = milliToPoints(pointsMilli)
e.CreatedAt = time.UnixMilli(createdMS).UTC()
out.RecentEarnings = append(out.RecentEarnings, e)
}
if err := rows.Err(); err != nil {
rows.Close()
return out, err
}
rows.Close()
return out, nil
}
func (s *Store) PlacePixelAt(ctx context.Context, x, y int) (PlacePixel, error) {
var p PlacePixel
var placedMS int64
err := s.DB.QueryRowContext(ctx, `SELECT x,y,color_index,client_id,placed_at,revision FROM place_pixels WHERE x=? AND y=?`, x, y).
Scan(&p.X, &p.Y, &p.ColorIndex, &p.ClientID, &placedMS, &p.Revision)
if err != nil {
return p, err
}
p.PlacedAt = time.UnixMilli(placedMS).UTC()
return p, nil
}
// PlacePixel spends points and updates the shared canvas in one transaction.
// There is intentionally no time cooldown: Hunt progress is the scarce resource.
func (s *Store) PlacePixel(ctx context.Context, clientID string, x, y, colorIndex, width, height, pixelCostPoints, paletteSize int) (PlacePixel, PlaceWallet, error) {
if x < 0 || y < 0 || x >= width || y >= height {
return PlacePixel{}, PlaceWallet{}, ErrPlaceBounds
}
if colorIndex < 0 || colorIndex >= paletteSize {
return PlacePixel{}, PlaceWallet{}, ErrPlaceColor
}
costMilli := int64(pixelCostPoints) * 1000
if costMilli <= 0 {
return PlacePixel{}, PlaceWallet{}, fmt.Errorf("invalid place pixel cost")
}
tx, err := s.DB.BeginTx(ctx, nil)
if err != nil {
return PlacePixel{}, PlaceWallet{}, err
}
defer tx.Rollback()
nowMS := time.Now().UTC().UnixMilli()
if _, err := tx.ExecContext(ctx, `INSERT INTO place_wallets(client_id,updated_at) VALUES(?,?) ON CONFLICT(client_id) DO NOTHING`, clientID, nowMS); err != nil {
return PlacePixel{}, PlaceWallet{}, err
}
res, err := tx.ExecContext(ctx, `UPDATE place_wallets SET balance_milli=balance_milli-?,spent_milli=spent_milli+?,placements=placements+1,updated_at=? WHERE client_id=? AND balance_milli>=?`, costMilli, costMilli, nowMS, clientID, costMilli)
if err != nil {
return PlacePixel{}, PlaceWallet{}, err
}
n, _ := res.RowsAffected()
if n != 1 {
return PlacePixel{}, PlaceWallet{}, ErrPlaceInsufficientPoints
}
if _, err := tx.ExecContext(ctx, `UPDATE place_state SET revision=revision+1,updated_at=? WHERE id=1`, nowMS); err != nil {
return PlacePixel{}, PlaceWallet{}, err
}
var revision int64
if err := tx.QueryRowContext(ctx, `SELECT revision FROM place_state WHERE id=1`).Scan(&revision); err != nil {
return PlacePixel{}, PlaceWallet{}, err
}
if _, err := tx.ExecContext(ctx, `INSERT INTO place_pixels(x,y,color_index,client_id,placed_at,revision) VALUES(?,?,?,?,?,?) ON CONFLICT(x,y) DO UPDATE SET color_index=excluded.color_index,client_id=excluded.client_id,placed_at=excluded.placed_at,revision=excluded.revision`, x, y, colorIndex, clientID, nowMS, revision); err != nil {
return PlacePixel{}, PlaceWallet{}, err
}
if _, err := tx.ExecContext(ctx, `INSERT INTO place_pixel_events(x,y,color_index,client_id,cost_milli,created_at,revision) VALUES(?,?,?,?,?,?,?)`, x, y, colorIndex, clientID, costMilli, nowMS, revision); err != nil {
return PlacePixel{}, PlaceWallet{}, err
}
if err := tx.Commit(); err != nil {
return PlacePixel{}, PlaceWallet{}, err
}
p := PlacePixel{X: x, Y: y, ColorIndex: colorIndex, ClientID: clientID, PlacedAt: time.UnixMilli(nowMS).UTC(), Revision: revision}
wallet, err := s.PlaceWallet(ctx, clientID, pixelCostPoints)
return p, wallet, err
}
// placeRewardImprovementTx credits the reward identity for genuine durable
// score progress. Delegated workers resolve to their owner at the instant the
// progress is committed. A minimum of one milli-point ensures every positive
// score increase is represented, even if it is extremely small.
func (s *Store) placeRewardImprovementTx(ctx context.Context, tx *sql.Tx, taskID, sourceClientID string, oldScore, newScore float64, pointsPerScore int, nowMS int64) (string, int64, error) {
if pointsPerScore <= 0 || newScore <= oldScore {
return "", 0, nil
}
owner := sourceClientID
var delegatedOwner string
err := tx.QueryRowContext(ctx, `SELECT owner_client_id FROM identity_delegations WHERE worker_client_id=?`, sourceClientID).Scan(&delegatedOwner)
switch {
case err == nil && delegatedOwner != "":
owner = delegatedOwner
case errors.Is(err, sql.ErrNoRows):
// Direct player progress credits the player identity itself.
case err != nil:
return "", 0, err
}
pointsMilli := int64((newScore-oldScore)*float64(pointsPerScore)*1000.0 + 0.5)
if pointsMilli < 1 {
pointsMilli = 1
}
if _, err := tx.ExecContext(ctx, `INSERT INTO place_wallets(client_id,balance_milli,earned_milli,updated_at) VALUES(?,?,?,?) ON CONFLICT(client_id) DO UPDATE SET balance_milli=place_wallets.balance_milli+excluded.balance_milli,earned_milli=place_wallets.earned_milli+excluded.earned_milli,updated_at=excluded.updated_at`, owner, pointsMilli, pointsMilli, nowMS); err != nil {
return "", 0, err
}
if _, err := tx.ExecContext(ctx, `INSERT INTO place_progress_events(owner_client_id,source_client_id,task_id,old_score,new_score,points_milli,created_at) VALUES(?,?,?,?,?,?,?)`, owner, sourceClientID, taskID, oldScore, newScore, pointsMilli, nowMS); err != nil {
return "", 0, err
}
return owner, pointsMilli, nil
}
+107
View File
@@ -0,0 +1,107 @@
package data
import (
"context"
"errors"
"testing"
"time"
"neuralhunt/internal/auth"
)
func TestPlaceWorkerProgressCreditsOwnerAndFundsPlacements(t *testing.T) {
ctx := context.Background()
db, err := OpenSQLite(ctx, t.TempDir()+"/place.db")
if err != nil {
t.Fatal(err)
}
defer db.Close()
s := New(db)
jwk := auth.PublicJWK{Kty: "EC", Crv: "P-256", X: "AQ", Y: "Ag"}
if err := s.UpsertClient(ctx, "owner", jwk); err != nil {
t.Fatal(err)
}
if err := s.UpsertClient(ctx, "worker", jwk); err != nil {
t.Fatal(err)
}
if err := s.SetIdentityDelegation(ctx, "worker", "owner"); err != nil {
t.Fatal(err)
}
if err := s.EnsureActiveTasks(ctx, 1, 28); err != nil {
t.Fatal(err)
}
tasks, err := s.ActiveTasks(ctx)
if err != nil || len(tasks) != 1 {
t.Fatalf("tasks=%d err=%v", len(tasks), err)
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
t.Fatal(err)
}
owner, milli, err := s.placeRewardImprovementTx(ctx, tx, tasks[0].ID, "worker", 10, 12.5, 100, time.Now().UTC().UnixMilli())
if err != nil {
t.Fatal(err)
}
if owner != "owner" || milli != 250000 {
t.Fatalf("owner=%q milli=%d", owner, milli)
}
if err := tx.Commit(); err != nil {
t.Fatal(err)
}
wallet, err := s.PlaceWallet(ctx, "owner", 100)
if err != nil {
t.Fatal(err)
}
if wallet.BalancePoints != 250 || wallet.AvailablePixels != 2 || len(wallet.LinkedWorkers) != 1 || wallet.LinkedWorkers[0] != "worker" {
t.Fatalf("unexpected wallet: %+v", wallet)
}
first, wallet, err := s.PlacePixel(ctx, "owner", 7, 9, 3, 256, 256, 100, 32)
if err != nil {
t.Fatal(err)
}
if first.Revision != 1 || wallet.BalancePoints != 150 || wallet.Placements != 1 || wallet.OwnedPixels != 1 {
t.Fatalf("first=%+v wallet=%+v", first, wallet)
}
second, wallet, err := s.PlacePixel(ctx, "owner", 7, 9, 8, 256, 256, 100, 32)
if err != nil {
t.Fatal(err)
}
if second.Revision != 2 || wallet.BalancePoints != 50 || wallet.Placements != 2 || wallet.OwnedPixels != 1 {
t.Fatalf("second=%+v wallet=%+v", second, wallet)
}
if _, _, err := s.PlacePixel(ctx, "owner", 8, 9, 1, 256, 256, 100, 32); !errors.Is(err, ErrPlaceInsufficientPoints) {
t.Fatalf("expected insufficient points, got %v", err)
}
pixels, stats, err := s.PlaceSnapshot(ctx)
if err != nil {
t.Fatal(err)
}
if len(pixels) != 1 || pixels[0].ColorIndex != 8 || stats.Revision != 2 || stats.TotalPlacements != 2 {
t.Fatalf("pixels=%+v stats=%+v", pixels, stats)
}
changes, rev, err := s.PlaceChanges(ctx, 0, 100)
if err != nil {
t.Fatal(err)
}
if len(changes) != 1 || changes[0].Revision != 2 || changes[0].ColorIndex != 8 || rev != 2 {
t.Fatalf("changes=%+v rev=%d", changes, rev)
}
recent, err := s.PlaceRecent(ctx, 2)
if err != nil {
t.Fatal(err)
}
if len(recent) != 2 || recent[0].Revision != 2 || recent[1].Revision != 1 {
t.Fatalf("recent=%+v", recent)
}
outside, err := s.PlacePixelsOutside(ctx, 7, 256)
if err != nil || outside != 1 {
t.Fatalf("outside=%d err=%v", outside, err)
}
outside, err = s.PlacePixelsOutside(ctx, 8, 10)
if err != nil || outside != 0 {
t.Fatalf("inside bounds outside=%d err=%v", outside, err)
}
}
+57
View File
@@ -208,3 +208,60 @@ CREATE TABLE IF NOT EXISTS hosted_credit_events (
last_error TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS hosted_credit_events_pending_idx ON hosted_credit_events(delivered_at,created_at);
-- Neural Place: a shared, r/place-inspired canvas funded exclusively by
-- genuine Hunt score improvements. Point balances use milli-points so tiny
-- score increases accumulate instead of being rounded away.
CREATE TABLE IF NOT EXISTS place_wallets (
client_id TEXT PRIMARY KEY REFERENCES clients(id) ON DELETE CASCADE,
balance_milli INTEGER NOT NULL DEFAULT 0,
earned_milli INTEGER NOT NULL DEFAULT 0,
spent_milli INTEGER NOT NULL DEFAULT 0,
placements INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS place_state (
id INTEGER PRIMARY KEY CHECK (id = 1),
revision INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL DEFAULT 0
);
INSERT OR IGNORE INTO place_state(id,revision,updated_at) VALUES(1,0,0);
CREATE TABLE IF NOT EXISTS place_pixels (
x INTEGER NOT NULL,
y INTEGER NOT NULL,
color_index INTEGER NOT NULL,
client_id TEXT NOT NULL,
placed_at INTEGER NOT NULL,
revision INTEGER NOT NULL,
PRIMARY KEY (x,y)
);
CREATE INDEX IF NOT EXISTS place_pixels_revision_idx ON place_pixels(revision);
CREATE INDEX IF NOT EXISTS place_pixels_client_idx ON place_pixels(client_id);
CREATE TABLE IF NOT EXISTS place_pixel_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
x INTEGER NOT NULL,
y INTEGER NOT NULL,
color_index INTEGER NOT NULL,
client_id TEXT NOT NULL,
cost_milli INTEGER NOT NULL,
created_at INTEGER NOT NULL,
revision INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS place_pixel_events_created_idx ON place_pixel_events(created_at DESC);
CREATE INDEX IF NOT EXISTS place_pixel_events_client_idx ON place_pixel_events(client_id,created_at DESC);
CREATE TABLE IF NOT EXISTS place_progress_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_client_id TEXT NOT NULL,
source_client_id TEXT NOT NULL,
task_id TEXT NOT NULL,
old_score REAL NOT NULL,
new_score REAL NOT NULL,
points_milli INTEGER NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS place_progress_owner_idx ON place_progress_events(owner_client_id,created_at DESC);
CREATE INDEX IF NOT EXISTS place_progress_source_idx ON place_progress_events(source_client_id,created_at DESC);
+35 -4
View File
@@ -1689,13 +1689,15 @@ type ProfileCleanupCandidate struct {
}
// InactiveNonWinnerClients returns identities whose persisted last activity is
// older than cutoffMS and that have never been recorded as a task winner.
// older than cutoffMS and that have no durable winner, delegation, or Neural
// Place economy state that must survive generic profile cleanup.
func (s *Store) InactiveNonWinnerClients(ctx context.Context, cutoffMS int64) ([]ProfileCleanupCandidate, error) {
rows, err := s.DB.QueryContext(ctx, `SELECT c.id,c.last_seen
FROM clients c
WHERE c.last_seen < ?
AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.winner_client_id=c.id OR t.winner_worker_client_id=c.id OR t.artifact_owner_client_id=c.id)
AND NOT EXISTS (SELECT 1 FROM identity_delegations d WHERE d.worker_client_id=c.id OR d.owner_client_id=c.id)
AND NOT EXISTS (SELECT 1 FROM place_wallets pw WHERE pw.client_id=c.id AND (pw.earned_milli>0 OR pw.spent_milli>0 OR pw.placements>0 OR pw.balance_milli>0))
ORDER BY c.last_seen ASC`, cutoffMS)
if err != nil {
return nil, err
@@ -1722,8 +1724,19 @@ func (s *Store) OldWinnerCount(ctx context.Context, cutoffMS int64) (int64, erro
return n, err
}
// OldPlaceParticipantCount reports old identities whose earned Place economy
// state protects them from generic profile cleanup. Deleting such an identity
// would otherwise cascade its wallet and destroy durable earned value.
func (s *Store) OldPlaceParticipantCount(ctx context.Context, cutoffMS int64) (int64, error) {
var n int64
err := s.DB.QueryRowContext(ctx, `SELECT count(*) FROM clients c
WHERE c.last_seen < ?
AND EXISTS (SELECT 1 FROM place_wallets pw WHERE pw.client_id=c.id AND (pw.earned_milli>0 OR pw.spent_milli>0 OR pw.placements>0 OR pw.balance_milli>0))`, cutoffMS).Scan(&n)
return n, err
}
// DeleteInactiveNonWinnerClients deletes only the explicitly supplied client
// IDs and re-checks both age and winner protection inside the transaction. The
// IDs and re-checks age plus winner/delegation/Place protection in the transaction. The
// re-check makes a concurrent login safe because login/WS connect refreshes
// clients.last_seen before this DELETE can match it.
func (s *Store) DeleteInactiveNonWinnerClients(ctx context.Context, cutoffMS int64, ids []string) ([]string, error) {
@@ -1746,7 +1759,8 @@ func (s *Store) DeleteInactiveNonWinnerClients(ctx context.Context, cutoffMS int
res, err := tx.ExecContext(ctx, `DELETE FROM clients
WHERE id=? AND last_seen < ?
AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.winner_client_id=clients.id OR t.winner_worker_client_id=clients.id OR t.artifact_owner_client_id=clients.id)
AND NOT EXISTS (SELECT 1 FROM identity_delegations d WHERE d.worker_client_id=clients.id OR d.owner_client_id=clients.id)`, id, cutoffMS)
AND NOT EXISTS (SELECT 1 FROM identity_delegations d WHERE d.worker_client_id=clients.id OR d.owner_client_id=clients.id)
AND NOT EXISTS (SELECT 1 FROM place_wallets pw WHERE pw.client_id=clients.id AND (pw.earned_milli>0 OR pw.spent_milli>0 OR pw.placements>0 OR pw.balance_milli>0))`, id, cutoffMS)
if err != nil {
return nil, err
}
@@ -1771,6 +1785,14 @@ func (s *Store) TouchClient(ctx context.Context, id string) {
_, _ = s.DB.ExecContext(ctx, `UPDATE clients SET last_seen=? WHERE id=?`, time.Now().UTC().UnixMilli(), id)
}
// TouchClientThrottled marks passive authenticated activity without turning a
// frequently-polled read endpoint into a SQLite write on every request.
func (s *Store) TouchClientThrottled(ctx context.Context, id string, minInterval time.Duration) {
now := time.Now().UTC().UnixMilli()
cutoff := now - minInterval.Milliseconds()
_, _ = s.DB.ExecContext(ctx, `UPDATE clients SET last_seen=? WHERE id=? AND last_seen<?`, now, id, cutoff)
}
// Presence lease helpers replace Redis SET NX + TTL.
func (s *Store) AcquirePresence(ctx context.Context, cid, sessionID string, ttl time.Duration) error {
tx, err := s.DB.BeginTx(ctx, nil)
@@ -1861,7 +1883,7 @@ func (s *Store) LoadGuessState(ctx context.Context, taskID, cid string) (GuessSt
// sequence/count values include all losing guesses that happened in memory
// since the previous checkpoint, so a restart resumes from the latest durable
// improvement rather than writing every false guess.
func (s *Store) PersistImprovement(ctx context.Context, t SecretTask, cid, rewardOwner string, nextSeq, guessCount int64, lastGuess time.Time, score float64, guess, sig string, correct bool, beaconPath, beaconBoostedPath string, beaconRound uint64) (Point, error) {
func (s *Store) PersistImprovement(ctx context.Context, t SecretTask, cid, rewardOwner string, nextSeq, guessCount int64, lastGuess time.Time, score float64, guess, sig string, correct bool, beaconPath, beaconBoostedPath string, beaconRound uint64, placePointsPerScore int) (Point, error) {
tx, err := s.DB.BeginTx(ctx, nil)
if err != nil {
return Point{}, err
@@ -1878,6 +1900,10 @@ func (s *Store) PersistImprovement(ctx context.Context, t SecretTask, cid, rewar
if paused != 0 {
return Point{}, ErrTaskPaused
}
var oldScore float64
if err := tx.QueryRowContext(ctx, `SELECT score FROM task_points WHERE task_id=? AND client_id=?`, t.ID, cid).Scan(&oldScore); err != nil && !errors.Is(err, sql.ErrNoRows) {
return Point{}, err
}
x, y, z := core.Position(cid, score)
lastMS := lastGuess.UTC().UnixMilli()
_, err = tx.ExecContext(ctx, `INSERT INTO task_points(task_id,client_id,score,x,y,z,guess_count,next_seq,last_guess_at)
@@ -1887,6 +1913,11 @@ func (s *Store) PersistImprovement(ctx context.Context, t SecretTask, cid, rewar
if err != nil {
return Point{}, err
}
if score > oldScore {
if _, _, err := s.placeRewardImprovementTx(ctx, tx, t.ID, cid, oldScore, score, placePointsPerScore, lastMS); err != nil {
return Point{}, err
}
}
if correct {
if rewardOwner == "" {
rewardOwner = cid