Code Farm: per-plot auto-replant pause and crop-lock check; community sync and node work
DevPlace CI / test (push) Failing after 44m45s

Continuous Delivery could never be turned off once a crop was planted. Add a
one-shot per-plot "stop after harvest" flag (POST /game/plot/stop, Devii tool
game_plot_stop, game_plots.stop_after_harvest), cleared by the next plant,
autoreplant claim or Refactor. Autoreplant now also respects crop locks through
the shared _crop_lock predicate. Plan and measurements in happyplanting.md.

Also includes the pending community sync, node network and related work in the
tree; the full suite passed (4999) with all of it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 02:23:43 +02:00
co-authored by Claude Sonnet 5.5
parent ce899fc5e1
commit 02ab747cd0
203 changed files with 11624 additions and 301 deletions
+180 -5
View File
@@ -24,6 +24,7 @@ LABEL_MAX = 60
EVENT_DETAIL_MAX = 500
MEGABYTE = 1024 * 1024
BATCH_SIZE = 500
COUNTER_ROW_LIMIT = 500
HOURS_PATTERN = re.compile(r"^([01]\d|2[0-3]):[0-5]\d$")
PLATFORMS = tuple(sorted({item.adapter for item in SPEC.destinations}))
FLAG_OPEN = "open"
@@ -714,14 +715,18 @@ def enqueue_request(
method: str,
adapter: str,
mode: str,
*,
shadow_of: str = "",
exclude_device: str = "",
) -> str:
uid = _uid()
created = _execute(
"INSERT INTO node_requests (uid, caller_kind, caller_id, url, method, adapter, "
"mode, status, device_uid, result_status, result_bytes, result_body, "
"result_headers, truncated, error, created_at, assigned_at, finished_at) "
"result_headers, truncated, error, device_asn_class, shadow_of, exclude_device, "
"created_at, assigned_at, finished_at) "
"SELECT :uid, :kind, :caller, :url, :method, :adapter, :mode, 'queued', '', 0, 0, "
"'', '', 0, '', :now, '', '' "
"'', '', 0, '', '', :shadow_of, :exclude_device, :now, '', '' "
"WHERE (SELECT COUNT(*) FROM node_requests WHERE status IN ('queued','assigned')) "
"< :max_depth",
{
@@ -732,6 +737,8 @@ def enqueue_request(
"method": method,
"adapter": adapter,
"mode": mode,
"shadow_of": shadow_of,
"exclude_device": exclude_device,
"now": _now_iso(),
"max_depth": LIMITS.max_queue_depth,
},
@@ -766,12 +773,18 @@ def count_open_requests() -> int:
)["c"]
def claim_request(request_uid: str, device_uid: str) -> bool:
def claim_request(request_uid: str, device_uid: str, asn_class: str = "") -> bool:
return (
_execute(
"UPDATE node_requests SET status = 'assigned', device_uid = :device, "
"assigned_at = :now WHERE uid = :uid AND status = 'queued'",
{"device": device_uid, "now": _now_iso(), "uid": request_uid},
"device_asn_class = :asn_class, assigned_at = :now "
"WHERE uid = :uid AND status = 'queued'",
{
"device": device_uid,
"asn_class": asn_class,
"now": _now_iso(),
"uid": request_uid,
},
)
== 1
)
@@ -989,3 +1002,165 @@ def recent_devices(limit: int = ADMIN_LIST_LIMIT) -> list[dict]:
n=limit,
)
]
def set_class_override(device_uid: str, value: str) -> bool:
return (
_execute(
"UPDATE node_devices SET class_override = :value, updated_at = :now "
"WHERE uid = :uid AND deleted_at IS NULL",
{"value": value, "now": _now_iso(), "uid": device_uid},
)
== 1
)
def touch_devices(device_uids, moment: datetime | None = None) -> int:
stamp = (moment or datetime.now(timezone.utc)).isoformat()
touched = 0
for chunk in _batches(device_uids):
placeholders, params = _in_clause(chunk)
touched += _execute(
f"UPDATE node_devices SET last_seen_at = :now WHERE uid IN ({placeholders})",
{"now": stamp, **params},
)
return touched
def flush_counters(deltas: dict[str, int]) -> int:
rows = {name: int(value) for name, value in deltas.items() if int(value) > 0}
if not rows:
return 0
now = _now_iso()
with db:
for name, value in rows.items():
db.executable.execute(
text(
"INSERT INTO node_counters (name, value, updated_at) VALUES (:name, :value, :now) "
"ON CONFLICT(name) DO UPDATE SET value = COALESCE(value, 0) + excluded.value, "
"updated_at = excluded.updated_at"
),
{"name": name, "value": value, "now": now},
)
return len(rows)
def lifetime_counters(prefix: str = "") -> dict[str, int]:
rows = db.query(
"SELECT name, COALESCE(value, 0) AS value FROM node_counters "
"WHERE name LIKE :prefix ESCAPE '\\' ORDER BY name LIMIT :n",
prefix=prefix.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + "%",
n=COUNTER_ROW_LIMIT,
)
return {row["name"]: int(row["value"]) for row in rows}
def save_circuits(rows: list[dict]) -> int:
now = _now_iso()
with db:
db.executable.execute(text("DELETE FROM node_circuits"))
for row in rows[: LIMITS.circuit_snapshot_max]:
db.executable.execute(
text(
"INSERT INTO node_circuits (key, failures, state, opened_at, updated_at) "
"VALUES (:key, :failures, :state, :opened_at, :now)"
),
{
"key": row["key"],
"failures": int(row["failures"]),
"state": row["state"],
"opened_at": float(row["opened_at"]),
"now": now,
},
)
return min(len(rows), LIMITS.circuit_snapshot_max)
def load_circuits() -> list[dict]:
return [
dict(row)
for row in db.query(
"SELECT key, failures, state, opened_at FROM node_circuits LIMIT :n",
n=LIMITS.circuit_snapshot_max,
)
]
def cache_get(key: str, moment: datetime | None = None) -> dict | None:
now = (moment or datetime.now(timezone.utc)).isoformat()
rows = list(
db.query(
"SELECT status, headers, body, stored_at FROM node_cache "
"WHERE key = :key AND expires_at > :now LIMIT 1",
key=key,
now=now,
)
)
return dict(rows[0]) if rows else None
def cache_put(
key: str,
method: str,
url: str,
status: int,
headers: str,
body: str,
size: int,
ttl_seconds: int,
moment: datetime | None = None,
) -> bool:
if ttl_seconds <= 0:
return False
now = moment or datetime.now(timezone.utc)
return (
_execute(
"INSERT INTO node_cache (key, method, url, status, headers, body, size, stored_at, expires_at) "
"VALUES (:key, :method, :url, :status, :headers, :body, :size, :stored, :expires) "
"ON CONFLICT(key) DO UPDATE SET status = excluded.status, headers = excluded.headers, "
"body = excluded.body, size = excluded.size, stored_at = excluded.stored_at, "
"expires_at = excluded.expires_at",
{
"key": key,
"method": method,
"url": url[: LIMITS.url_max_chars],
"status": int(status),
"headers": headers,
"body": body,
"size": int(size),
"stored": now.isoformat(),
"expires": (now + timedelta(seconds=ttl_seconds)).isoformat(),
},
)
== 1
)
def cache_prune(max_bytes: int, moment: datetime | None = None) -> dict[str, int]:
now = (moment or datetime.now(timezone.utc)).isoformat()
expired = _execute("DELETE FROM node_cache WHERE expires_at <= :now", {"now": now})
total = next(iter(db.query("SELECT COALESCE(SUM(size), 0) AS total FROM node_cache")))["total"]
evicted = 0
while total > max_bytes:
rows = list(
db.query("SELECT key, size FROM node_cache ORDER BY stored_at, id LIMIT :n", n=BATCH_SIZE)
)
if not rows:
break
for row in rows:
if total <= max_bytes:
break
evicted += _execute("DELETE FROM node_cache WHERE key = :key", {"key": row["key"]})
total -= int(row["size"] or 0)
return {"expired": expired, "evicted": evicted, "bytes": max(0, int(total))}
def cache_clear() -> int:
return _execute("DELETE FROM node_cache", {})
def cache_stats() -> dict[str, int]:
row = next(
iter(db.query("SELECT COUNT(*) AS entries, COALESCE(SUM(size), 0) AS bytes FROM node_cache"))
)
return {"entries": int(row["entries"]), "bytes": int(row["bytes"])}