Fix completed runs not appearing in list + add purge-failed endpoint
- Update save_run_cache to also update actor_id, recipe, inputs on conflict - Add logging for actor_id when saving runs to run_cache - Add admin endpoint DELETE /runs/admin/purge-failed to delete all failed runs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,12 +30,9 @@ from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
# Try pip-installed artdag first, fall back to local path
|
||||
try:
|
||||
from artdag.sexp.parser import parse, parse_all, Symbol, Keyword
|
||||
except ImportError:
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "artdag"))
|
||||
from artdag.sexp.parser import parse, parse_all, Symbol, Keyword
|
||||
# Use local sexp_effects parser (supports namespaced symbols like math:sin)
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from sexp_effects.parser import parse, parse_all, Symbol, Keyword
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -54,9 +51,10 @@ class StreamInterpreter:
|
||||
and calls primitives.
|
||||
"""
|
||||
|
||||
def __init__(self, sexp_path: str):
|
||||
def __init__(self, sexp_path: str, actor_id: Optional[str] = None):
|
||||
self.sexp_path = Path(sexp_path)
|
||||
self.sexp_dir = self.sexp_path.parent
|
||||
self.actor_id = actor_id # For friendly name resolution
|
||||
|
||||
text = self.sexp_path.read_text()
|
||||
self.ast = parse(text)
|
||||
@@ -84,6 +82,26 @@ class StreamInterpreter:
|
||||
self.sources_config: Optional[Path] = None
|
||||
self.audio_config: Optional[Path] = None
|
||||
|
||||
# Error tracking
|
||||
self.errors: List[str] = []
|
||||
|
||||
def _resolve_name(self, name: str) -> Optional[Path]:
|
||||
"""Resolve a friendly name to a file path using the naming service."""
|
||||
try:
|
||||
# Import here to avoid circular imports
|
||||
from tasks.streaming import resolve_asset
|
||||
path = resolve_asset(name, self.actor_id)
|
||||
if path:
|
||||
return path
|
||||
except Exception as e:
|
||||
print(f"Warning: failed to resolve name '{name}': {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
def _record_error(self, msg: str):
|
||||
"""Record an error that occurred during evaluation."""
|
||||
self.errors.append(msg)
|
||||
print(f"ERROR: {msg}", file=sys.stderr)
|
||||
|
||||
import random
|
||||
self.rng = random.Random(self.config.get('seed', 42))
|
||||
|
||||
@@ -241,27 +259,50 @@ class StreamInterpreter:
|
||||
self.macros[name] = {'params': params, 'body': body}
|
||||
|
||||
elif cmd == 'effect':
|
||||
# Handle (effect name :path "...") in included files - recursive
|
||||
# Handle (effect name :path "...") or (effect name :name "...") in included files
|
||||
i = 2
|
||||
while i < len(form):
|
||||
if isinstance(form[i], Keyword) and form[i].name == 'path':
|
||||
path = str(form[i + 1]).strip('"')
|
||||
# Resolve relative to the file being loaded
|
||||
full = (effect_path.parent / path).resolve()
|
||||
self._load_effect(full)
|
||||
i += 2
|
||||
if isinstance(form[i], Keyword):
|
||||
kw = form[i].name
|
||||
if kw == 'path':
|
||||
path = str(form[i + 1]).strip('"')
|
||||
full = (effect_path.parent / path).resolve()
|
||||
self._load_effect(full)
|
||||
i += 2
|
||||
elif kw == 'name':
|
||||
fname = str(form[i + 1]).strip('"')
|
||||
resolved = self._resolve_name(fname)
|
||||
if resolved:
|
||||
self._load_effect(resolved)
|
||||
else:
|
||||
raise RuntimeError(f"Could not resolve effect name '{fname}' - make sure it's uploaded and you're logged in")
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
elif cmd == 'include':
|
||||
# Handle (include :path "...") in included files - recursive
|
||||
# Handle (include :path "...") or (include :name "...") in included files
|
||||
i = 1
|
||||
while i < len(form):
|
||||
if isinstance(form[i], Keyword) and form[i].name == 'path':
|
||||
path = str(form[i + 1]).strip('"')
|
||||
full = (effect_path.parent / path).resolve()
|
||||
self._load_effect(full)
|
||||
i += 2
|
||||
if isinstance(form[i], Keyword):
|
||||
kw = form[i].name
|
||||
if kw == 'path':
|
||||
path = str(form[i + 1]).strip('"')
|
||||
full = (effect_path.parent / path).resolve()
|
||||
self._load_effect(full)
|
||||
i += 2
|
||||
elif kw == 'name':
|
||||
fname = str(form[i + 1]).strip('"')
|
||||
resolved = self._resolve_name(fname)
|
||||
if resolved:
|
||||
self._load_effect(resolved)
|
||||
else:
|
||||
raise RuntimeError(f"Could not resolve include name '{fname}' - make sure it's uploaded and you're logged in")
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
@@ -313,22 +354,49 @@ class StreamInterpreter:
|
||||
name = form[1].name if isinstance(form[1], Symbol) else str(form[1])
|
||||
i = 2
|
||||
while i < len(form):
|
||||
if isinstance(form[i], Keyword) and form[i].name == 'path':
|
||||
path = str(form[i + 1]).strip('"')
|
||||
full = (self.sexp_dir / path).resolve()
|
||||
self._load_effect(full)
|
||||
i += 2
|
||||
if isinstance(form[i], Keyword):
|
||||
kw = form[i].name
|
||||
if kw == 'path':
|
||||
path = str(form[i + 1]).strip('"')
|
||||
full = (self.sexp_dir / path).resolve()
|
||||
self._load_effect(full)
|
||||
i += 2
|
||||
elif kw == 'name':
|
||||
# Resolve friendly name to path
|
||||
fname = str(form[i + 1]).strip('"')
|
||||
resolved = self._resolve_name(fname)
|
||||
if resolved:
|
||||
self._load_effect(resolved)
|
||||
else:
|
||||
raise RuntimeError(f"Could not resolve effect name '{fname}' - make sure it's uploaded and you're logged in")
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
elif cmd == 'include':
|
||||
i = 1
|
||||
while i < len(form):
|
||||
if isinstance(form[i], Keyword) and form[i].name == 'path':
|
||||
path = str(form[i + 1]).strip('"')
|
||||
full = (self.sexp_dir / path).resolve()
|
||||
self._load_effect(full)
|
||||
i += 2
|
||||
if isinstance(form[i], Keyword):
|
||||
kw = form[i].name
|
||||
if kw == 'path':
|
||||
path = str(form[i + 1]).strip('"')
|
||||
full = (self.sexp_dir / path).resolve()
|
||||
self._load_effect(full)
|
||||
i += 2
|
||||
elif kw == 'name':
|
||||
# Resolve friendly name to path
|
||||
fname = str(form[i + 1]).strip('"')
|
||||
resolved = self._resolve_name(fname)
|
||||
if resolved:
|
||||
self._load_effect(resolved)
|
||||
else:
|
||||
raise RuntimeError(f"Could not resolve include name '{fname}' - make sure it's uploaded and you're logged in")
|
||||
raise RuntimeError(f"Could not resolve include name '{fname}' - make sure it's uploaded and you're logged in")
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
@@ -337,7 +405,13 @@ class StreamInterpreter:
|
||||
# Skip if already set by config file
|
||||
if self.audio_playback is None:
|
||||
path = str(form[1]).strip('"')
|
||||
self.audio_playback = str((self.sexp_dir / path).resolve())
|
||||
# Try to resolve as friendly name first
|
||||
resolved = self._resolve_name(path)
|
||||
if resolved:
|
||||
self.audio_playback = str(resolved)
|
||||
else:
|
||||
# Fall back to relative path
|
||||
self.audio_playback = str((self.sexp_dir / path).resolve())
|
||||
print(f"Audio playback: {self.audio_playback}", file=sys.stderr)
|
||||
|
||||
elif cmd == 'def':
|
||||
@@ -419,6 +493,10 @@ class StreamInterpreter:
|
||||
if isinstance(expr, Keyword):
|
||||
return expr.name
|
||||
|
||||
# Handle dicts from new parser - evaluate values
|
||||
if isinstance(expr, dict):
|
||||
return {k: self._eval(v, env) for k, v in expr.items()}
|
||||
|
||||
if not isinstance(expr, list) or not expr:
|
||||
return expr
|
||||
|
||||
@@ -685,8 +763,8 @@ class StreamInterpreter:
|
||||
return prim_func(*evaluated_args, **kwargs)
|
||||
return prim_func(*evaluated_args)
|
||||
except Exception as e:
|
||||
print(f"Primitive {op} error: {e}", file=sys.stderr)
|
||||
return None
|
||||
self._record_error(f"Primitive {op} error: {e}")
|
||||
raise RuntimeError(f"Primitive {op} failed: {e}")
|
||||
|
||||
# === Macros (function-like: args evaluated before binding) ===
|
||||
|
||||
@@ -720,8 +798,8 @@ class StreamInterpreter:
|
||||
return prim_func(*evaluated_args, **kwargs)
|
||||
return prim_func(*evaluated_args)
|
||||
except Exception as e:
|
||||
print(f"Primitive {op} error: {e}", file=sys.stderr)
|
||||
return None
|
||||
self._record_error(f"Primitive {op} error: {e}")
|
||||
raise RuntimeError(f"Primitive {op} failed: {e}")
|
||||
|
||||
# Unknown - return as-is
|
||||
return expr
|
||||
|
||||
Reference in New Issue
Block a user