From dfed363db76feec2c5473174a80dcfe779b2cca8 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Fri, 7 Aug 2026 20:01:06 -0700 Subject: [PATCH] [eric] apps: the scaffold ships a durable store and the skill forbids memory-only state, since the runtime dies on a timer --- backend/apps/outputs/app_builder_skill.md | 16 ++++++ .../backend/apps/store/__init__.py | 0 .../backend/apps/store/store.py | 52 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 backend/apps/outputs/webapp_template/backend/apps/store/__init__.py create mode 100644 backend/apps/outputs/webapp_template/backend/apps/store/store.py diff --git a/backend/apps/outputs/app_builder_skill.md b/backend/apps/outputs/app_builder_skill.md index 4d2e2346..9b3bc21c 100644 --- a/backend/apps/outputs/app_builder_skill.md +++ b/backend/apps/outputs/app_builder_skill.md @@ -274,6 +274,22 @@ and flips `BACKEND_PORT` in both `.env` and `.env.example`. Then run - Install your own venv or `pip install` manually. - Edit `backend/run.sh` or the SubApp framework. +**Persist anything the user comes back to. Your process is disposable.** +OpenSwarm freezes this app's process when its card closes and fully kills +it after ~15 minutes idle, on quit, and on crash. A module-level list or +dict is therefore data loss on a timer. The scaffold ships a durable +store; use it (or your own files under `backend/data/`): + +```python +from backend.apps.store.store import load_store, save_store + +data = load_store() # {} on first run, never raises +data["items"] = [*data.get("items", []), new_item] +save_store(data) # atomic write; a kill mid-write keeps the old data +``` + +Holding state only in memory is a bug, not a style choice. + Adding a new endpoint is just adding a new SubApp: ```python diff --git a/backend/apps/outputs/webapp_template/backend/apps/store/__init__.py b/backend/apps/outputs/webapp_template/backend/apps/store/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/outputs/webapp_template/backend/apps/store/store.py b/backend/apps/outputs/webapp_template/backend/apps/store/store.py new file mode 100644 index 00000000..24849a90 --- /dev/null +++ b/backend/apps/outputs/webapp_template/backend/apps/store/store.py @@ -0,0 +1,52 @@ +"""Disk-backed app state. Use this instead of module-level variables for anything worth keeping. + +The app's process is DISPOSABLE: OpenSwarm freezes it when its card closes, kills it after ~15 +minutes idle, on quit, and on crash. A module-level list or dict therefore silently loses the +user's data on a schedule you don't control. This store survives all of that: one JSON file under +backend/data/, written atomically so a kill mid-write can never corrupt it. + + from backend.apps.store.store import load_store, save_store + + items = load_store().get("items", []) + items.append(new_item) + save_store({**load_store(), "items": items}) +""" + +import json +import os +import tempfile +from typing import Any, Dict + +from typeguard import typechecked + +P_BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +DATA_DIR = os.path.join(P_BACKEND_DIR, "data") +STORE_PATH = os.path.join(DATA_DIR, "store.json") + + +@typechecked +def load_store() -> Dict[str, Any]: + """The whole store as a dict; empty on first run or an unreadable file, never an exception.""" + try: + with open(STORE_PATH, "r", encoding="utf-8") as f: + data = json.load(f) + return data if isinstance(data, dict) else {} + except (FileNotFoundError, json.JSONDecodeError, OSError): + return {} + + +@typechecked +def save_store(data: Dict[str, Any]) -> None: + """Replace the store atomically: temp file then rename, so a kill mid-write leaves the old data.""" + os.makedirs(DATA_DIR, exist_ok=True) + fd, tmp = tempfile.mkstemp(dir=DATA_DIR, suffix=".tmp") + try: + with os.fdopen(fd, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=1) + os.replace(tmp, STORE_PATH) + except OSError: + try: + os.unlink(tmp) + except OSError: + pass + raise