loopable / api /main.py
fsanyoto's picture
Deploy AIOS web (React glide grid + FastAPI slice)
609fb78 verified
Raw
History Blame Contribute Delete
54.6 kB
"""AIOS web API β€” the ONE shared, stateless process the Streamlit exit is aimed at.
It REUSES the `platform/` data layer verbatim (nothing re-implemented): the canonical,
reconciled Odoo model stays server-side and the browser only ever sees derived JSON. Serves the
JSON API under `/api/*` and the built React bundle (`aios-web/web/dist`) at `/`.
WHAT CHANGED IN EXIT WAVE 1 (2026-07-30) β€” three things, all of them load-bearing:
* **Real sessions replace HTTP Basic.** The whole app used to sit behind one shared
`APP_PASSWORD` with the browser's native Basic prompt, which means every caller was the same
anonymous principal and no route could scope anything. Now a signed stateless cookie carries
a real `core/users` identity (X3), and every v1 route resolves BU + own-book scope from the
user RECORD. `/api/health` stays unauthenticated (liveness only).
* **The overlay fork is deleted.** `aios-web/api/data/overlay.json` was a second writable home
for user-owned fields the Streamlit app keeps in the tenant store. ONE store now (C1c).
* **The SSL shim is env-gated.** It used to run at import, unconditionally.
⚠ TENANT RESOLUTION IS PER REQUEST (X7). Nothing tenant-shaped is held at module level; the
session's tenant claim resolves through `harness.runtime.get_runtime`, which is LRU-bounded. This
is the rule the ~30–80 MB/tenant target depends on β€” EXIT-0 measured the alternative at a ~0.6 GB
commit FLOOR per tenant, duplicated within 2% per tenant, nothing shared.
"""
import os
import sys
from pathlib import Path
# --- the local Odoo SSL quirk, now BEHIND A GATE -------------------------------------------------
# The Windows trust store reports the (valid) Odoo cert as expired, so local runs need an
# unverified default context; the HF Space and the container are unaffected. This used to run
# unconditionally at import β€” i.e. the shipped container disabled TLS verification for every
# outbound HTTPS call it ever made, to Odoo and to everything else, forever. That is a
# man-in-the-middle away from being someone else's data. It is now opt-in, must be set
# deliberately, and is never on by default.
# β›” NEVER set AIOS_INSECURE_SSL in a deployed environment. It exists for one developer laptop.
if os.environ.get("AIOS_INSECURE_SSL") == "1":
import ssl
ssl._create_default_https_context = ssl._create_unverified_context # noqa: S323
# --- reuse the tenant #0 data layer verbatim. RI_DIR overrides the location in the container
# (where the layout differs from the local sibling-dir default). ---
_HERE = Path(__file__).resolve()
_RI = Path(os.environ.get("RI_DIR") or (_HERE.parents[2] / "platform"))
for _p in (str(_RI), str(_HERE.parent)):
if _p not in sys.path:
sys.path.insert(0, _p)
from dotenv import load_dotenv # noqa: E402
load_dotenv(_RI / ".env") # Odoo creds + APP_PASSWORD, git-ignored, never committed/printed
from fastapi import Body, Depends, FastAPI, Request # noqa: E402
from fastapi.middleware.gzip import GZipMiddleware # noqa: E402
from fastapi.responses import JSONResponse # noqa: E402
from fastapi.staticfiles import StaticFiles # noqa: E402
from starlette.exceptions import HTTPException as StarletteHTTPException # noqa: E402
import aios_session # noqa: E402
import routes_admin # noqa: E402
import routes_alerts # noqa: E402 (wave 20 item 25 β€” the Alerts inbox)
import routes_assets # noqa: E402 (wave 18 C2-ASSET β€” catalog product imagery)
import routes_auth # noqa: E402
import routes_automation # noqa: E402 (wave 18 C4-AUTO β€” SESSION D's router, mounted by A)
import routes_changes # noqa: E402 (wave 29 item 20 / C6 β€” F's change token, mounted by A)
import routes_customers # noqa: E402
import routes_grid # noqa: E402
import routes_keychain # noqa: E402 (wave 18 C7 β€” keychain + connectors admin surfaces)
import routes_statements # noqa: E402 (EXIT-6 β€” the statement sender, off Streamlit)
import routes_nav # noqa: E402
import routes_pages # noqa: E402
import routes_platform_admin # noqa: E402 (wave 19 R3/R4 β€” the Loopable cross-tenant plane)
import routes_products # noqa: E402 (wave 15 C-TOPIC β€” gated, not yet in the nav)
import routes_records # noqa: E402
import routes_shares # noqa: E402 (wave 20 R10 β€” grants for views, folders and databases)
import routes_uploads # noqa: E402 (wave 21 C5 β€” tabular preview for Select-from-file)
import routes_tables # noqa: E402 (wave 18 C3-UT β€” user-created databases over the wire)
import routes_connectors # noqa: E402 (wave 23 C11 β€” the connectors directory; SESSION A's router)
import routes_forms # noqa: E402 (wave 23 C9 β€” the PUBLIC form door; SESSION D's router)
import routes_templates # noqa: E402 (wave 23 C12 β€” template apply doors; SESSION E's router)
import routes_odoo_tables # noqa: E402 (wave 27 item 17 β€” Odoo relational; SESSION E's router)
import routes_connected_tables # noqa: E402 (wave 31 T49/C4 β€” the source-neutral grid door; D's)
import routes_web_agent # noqa: E402 (wave 31 R10/C5 β€” the web-browsing agent; E's router, A's line)
import routes_query # noqa: E402 (wave 32 R1/C5 β€” the Query module; E's router, A's line)
from core import grid_events # noqa: E402
from deps import Session, module_gate # noqa: E402
_WEB_DIST = _HERE.parents[1] / "web" / "dist"
# --- THE CANONICAL FIELD CONTRACT: a startup assertion, and the referee gate's subject ----------
# Every field tags its semantic TYPE and its SOURCE. `source='odoo'` is READ-ONLY (Odoo is never
# written); `source='overlay'` is the editable stratum that lives OUTSIDE Odoo. Loaded from the
# ONE canonical file shared with the embedded host (`platform/aios_grid_fields.json`), so
# embed == standalone by construction.
#
# TWO REASONS THIS IS HERE and not folded into the routes:
# 1. It FAILS LOUDLY at import when the file is missing β€” which means the deploy or the RI_DIR
# layout is broken, and discovering that from a 500 on the first customer request instead of
# at startup costs a debugging session. (The pre-wave main.py had this guard; the EXIT-2a
# rewrite dropped it and this restores it.)
# 2. `aios-web/verify_fields_contract.py` β€” the cross-side REFEREE β€” reads `FIELDS` and
# `_PASSTHROUGH_KEYS` from this module to prove the canonical file, `aios_grid.py` and this
# API have not drifted. The rewrite removed them and the referee went red; retargeting the
# gate would have been the wrong repair ([[gate-can-report-green-on-nothing]]: retarget, do
# not delete β€” but only when the subject genuinely moved. Here it should not have moved).
#
# ⚠ THE ROUTES SERVE A SUPERSET OF THIS. `routes_customers._payload` derives its field list from
# `aios_grid.fields_from_workspace(ws)`, which is `FIELDS` PLUS the session user's own custom_ and
# measure_ columns β€” the same list the Streamlit host renders. That is the point: the standalone
# shell now sees the user's own columns instead of the bare base contract. `FIELDS` is the
# canonical FLOOR, asserted below to be exactly what `aios_grid` starts from.
import json as _json_contract # noqa: E402
_FIELDS_PATH = _RI / "aios_grid_fields.json"
if not _FIELDS_PATH.is_file():
raise FileNotFoundError(
f"AIOS web API: canonical field contract missing at {_FIELDS_PATH}. Set RI_DIR to the "
"platform root (it also carries the data layer this API imports)."
)
_contract = _json_contract.loads(_FIELDS_PATH.read_text(encoding="utf-8"))
FIELDS = _contract["fields"] if isinstance(_contract, dict) else _contract
# text/status/select/date pass through untouched; every OTHER odoo field is numeric -> rounded.
# Derived from field TYPE (not a hand-kept key list) so a new text/date field can never be
# wrongly rounded. `select` joined 2026-08-02 (dba) β€” a choice label rounded would be garbage.
_PASSTHROUGH_KEYS = {f["key"] for f in FIELDS
if f["source"] == "odoo" and f["type"] in ("text", "status", "select",
"date")}
app = FastAPI(title="AIOS web API")
# The customers payload measured 1.15 MB of JSON on the live Space, shipped UNCOMPRESSED β€” with
# the 754 KB bundle behind it, most of "the app is slow" was bytes on the wire. gzip takes the
# payload to ~10–15% of that. minimum_size spares the tiny acks the overhead.
app.add_middleware(GZipMiddleware, minimum_size=1024)
@app.exception_handler(StarletteHTTPException)
def _error_shape(request: Request, exc: StarletteHTTPException):
"""ONE error shape for every non-2xx (X2): `{"error": {"code", "message"}}`.
`deps.err` already raises detail in that shape; anything FastAPI raises on its own (a 404, a
422 from a malformed path param) is wrapped here so a client never has to branch on two
different error bodies.
"""
detail = exc.detail
if isinstance(detail, dict) and "error" in detail:
body = detail
else:
body = {"error": {"code": f"http_{exc.status_code}", "message": str(detail)}}
return JSONResponse(body, status_code=exc.status_code,
headers=getattr(exc, "headers", None))
@app.exception_handler(grid_events.StoreUnavailable)
def _store_unavailable(request: Request, exc: grid_events.StoreUnavailable):
"""THE SAFETY NET for "the store is down" β†’ 503, from anywhere.
⚠ WHY THIS IS APP-LEVEL AND NOT A `try` PER ROUTE. It was a try per route first, and a
`StoreUnavailable` raised while BUILDING THE PAYLOAD β€” before the route reached its own
try/except β€” surfaced as a 500. A store outage is a normal operational state and every route
here touches the store at least twice (the workspace read, then the write), so "remember to
wrap it" is a rule that gets forgotten once and then reports the wrong thing. One handler
means a store outage can only ever be a 503, whichever call raised it.
The seam raises this only when the caller passed no `fallback_ws` β€” i.e. exactly on this
adapter, which has no durable session dict to degrade into. A 200 over a write that
evaporated is the failure the whole rule exists to prevent.
"""
return JSONResponse(
{"error": {"code": "store_unavailable",
"message": "the tenant store is unavailable β€” no change was saved"}},
status_code=503)
@app.get("/api/health")
def health():
"""Unauthenticated LIVENESS only β€” it must answer before anyone can sign in, so it may not
reveal anything about the deployment beyond "the process is up". No version, no tenant list,
no config: a health endpoint is the one URL every scanner finds first.
β›” THE VERSION DOES NOT GO HERE, and it was asked to (2026-08-04, when LIVE became a pinned
release and "what is LIVE running" needed an answer). A build identifier tells an unauthenticated
caller exactly which commit's known issues apply. It rides `GET /api/v1/settings` instead, behind
a session β€” and the authoritative copy is the `VERSION` file in the Space repo, which
`deploy_web.space_version()` reads without needing the app to be up at all."""
return {"ok": True}
app.include_router(routes_auth.router)
app.include_router(routes_nav.router)
app.include_router(routes_customers.router)
app.include_router(routes_products.router)
app.include_router(routes_assets.router)
app.include_router(routes_tables.router)
app.include_router(routes_grid.router)
app.include_router(routes_records.router)
# EXIT wave 2: the Y1 page-data envelope (one route for every ported dashboard) and Y4's user
# administration. `routes_pages` imports `pages`, which lazily imports each `pages_*` builder β€” so
# a new page is a builder module plus one registry line, and nothing here changes.
app.include_router(routes_pages.router)
app.include_router(routes_admin.router)
app.include_router(routes_automation.router)
app.include_router(routes_keychain.router)
app.include_router(routes_statements.router)
# Wave 19 (owner item 13, R3): the LOOPABLE admin plane β€” the platform's own cross-tenant view.
# Its own prefix (`/api/v1/platform-admin`), never nested under `/admin`, so there is no path
# ambiguity with `routes_admin`'s `{username}` params and no chance of a tenant-admin route and a
# platform-operator route ever shadowing each other. Every path it declares is gated by
# `core.platform_admin.is_platform_admin`, and `verify_api` enumerates this router to prove it.
app.include_router(routes_platform_admin.router)
# Wave 20 (owner items 25 + 18/23/26): the Alerts inbox and the manage-access surface. Both are
# session-gated rather than admin-gated β€” an alert is a person's own subscription, and sharing is
# something every user does with their own views/folders/databases.
app.include_router(routes_alerts.router)
app.include_router(routes_shares.router)
app.include_router(routes_uploads.router)
# ⭐ WAVE 23 β€” THE THREE NEW ROUTERS. Mounted here, above the static catch-all at the bottom of this
# file, because `app.mount("/", _AppStatic(...), html=True)` swallows everything it is reached by:
# a router included AFTER it answers 404 forever while importing fine, type-checking fine and
# passing its own gate. That is the wave-20 declared-but-unmounted shape with a different cause.
#
# β›” ALL THREE EXISTED, COMPLETE AND GATED, WITH NO MOUNT until the close-out audit β€” three finished
# features that would have shipped dead. The workers each posted a mount ask and said they would
# signal "ready" first; C waited for a signal that never came while the files landed anyway. **The
# lesson is not "read the mailbox harder": `verify_api`'s enumeration is what caught it, so the
# control is that the enumeration must NAME every router in this file.**
app.include_router(routes_templates.router) # item 7 / C12 β€” template registry, session-gated
app.include_router(routes_connectors.router) # item 10 / C11 β€” the connectors directory
# ⚠ routes_forms is the FIRST NEW PUBLIC DOOR since the `_DEV_FIXTURES` scar (see :257 below). Its
# two paths are DELIBERATELY unauthenticated β€” a form is filled in by someone with no account β€” so
# it joins `/api/health`, the automation hook and the tick on the exempt list. It resolves a token
# by scanning tenants with a constant-time compare and answers a uniform 403, so a bad token cannot
# distinguish "no such form" from "not yours", and it never echoes a tenant, table or slug.
app.include_router(routes_forms.router) # item 8 / C9 β€” the PUBLIC form door
# WAVE 27 item 17 β€” E's Odoo relational doors. Mounted in the SAME change that E's module landed,
# because the wave-23 scar is exactly this: three finished routers shipped with no include_router
# line β€” complete, gated, type-clean and 404 for every caller. `verify_api.section_mounts` walks
# `main.app.routes` and pins these two paths by NAME, so an unmounted router now goes RED.
app.include_router(routes_odoo_tables.router)
# ⭐⭐ WAVE 31 Β· T49 / C4 β€” THE SOURCE-NEUTRAL DOOR TO THE SAME CAPABILITY.
# `/api/v1/connected-tables/{key}/rows` is an ALIAS: every request lands in
# `routes_odoo_tables.odoo_table_rows`, so "the Odoo path behaves byte-identically" holds by
# CONSTRUCTION rather than by two implementations that agree on the day they were written. R2 puts
# Meta Ads on the same mirror, and a Meta campaign served from a URL with `odoo` in it is a name
# that lies to every network tab, log line and bug report.
# ⚠ MOUNTED IN THE SAME CHANGE AS THE MODULE, per the line above and for the same wave-23 scar.
app.include_router(routes_connected_tables.router)
# ⭐⭐ WAVE 29, item 20 / R11 / contract C6 β€” F's CHANGE TOKEN. `GET /api/v1/changes?scope=<scope>`
# answers "did this bucket change" for ~zero cost (an in-memory counter; ZERO `store.get()` deep
# copies), which is what lets a filtered view pick up a row created in another tab, by an automation
# or by a connector sync without re-downloading the world.
# β›” THIS LINE IS THE ARTIFACT THIS PROTOCOL LOSES MOST RELIABLY, AND IT WAS ALREADY LOST ONCE HERE:
# F's router and the CLIENT half both shipped complete, so the poller was calling `/api/v1/changes`
# six times a minute and taking a 404 while every one of F's own gates was green. `verify_api`'s D-48
# leg caught it (`unmatched: [('/api/v1/changes', 'apiBridge.ts')]`) β€” a client fetch path with no
# mounted route β€” which is precisely the control the wave-23 scar above was written to install.
# ⚠ Mounted is NOT callable: `section_changes_callable` in `verify_api.py` SIGNS IN and CALLS this
# route, because a route can be mounted and still raise before its own `try:` (D-107's plain-text 500).
app.include_router(routes_changes.router) # item 20 / C6 β€” F's router, A's line
# β›” WAVE 31 (R10 / C5) β€” E's router, taken by the INTEGRATOR under W31-T08's own done-when
# ("no router ships unmounted") because `main.py` is D's fence and D's queue did not reach it.
# It was written, gated and 404-dead: `verify_web_agent.py` asserts THIS LINE and was red at
# 60/61 for it. Four waves of the same defect β€” three routers in wave 23, four features in
# wave 29 β€” is why the assertion exists and why the line is not left for later.
app.include_router(routes_web_agent.router) # R10 / C5 β€” E's router, A's line
# ⭐⭐ WAVE 32 (R1 / C5, cross-fence wiring 6) β€” THE QUERY MODULE. E's router, A's line, and the
# FIFTH consecutive wave in which this exact line is the artifact the protocol nearly loses.
# β›” MOUNTED HERE, IN THIS BLOCK, AND NOT AT THE END OF THE FILE β€” measured by SESSION E in its own
# gate before it front-inserted: `include_router` APPENDS, and `app.mount("/", _AppStatic(...),
# html=True)` swallows everything reached after it, so a router added below that mount answers
# **405 on POST and 404 on GET** while every one of its own tests passes. The comment at :213 states
# the rule; E's measurement is what turns it from advice into a number.
# ⚠ `verify_api` asserts `/api/v1/query` in `app.openapi()["paths"]` β€” NEVER `{r.path for r in
# app.routes}`, which finds nothing in this app because FastAPI wraps included routers (W31).
app.include_router(routes_query.router) # R1 / C5 β€” E's router, A's line
# --- DEPRECATED ALIASES (removed when S2's shell flips; kept so the current bundle keeps working)
# They are the v1 handlers with the v1 session requirement β€” NOT the old unauthenticated Basic
# behavior. An alias that kept the old auth would be a bypass of everything above it.
_GATE = module_gate(routes_customers.MODULE)
@app.get("/api/customers", deprecated=True)
def _customers_alias(session: Session = Depends(_GATE)):
return routes_customers._payload(session)
@app.patch("/api/customers/{pid}", deprecated=True)
def _patch_alias(pid: int, body: dict = Body(default=None),
session: Session = Depends(_GATE)):
return routes_customers.patch_customer(pid, body, session)
def _assert_contract_floor():
"""The canonical FIELDS must be exactly what `aios_grid` starts an empty workspace from.
This is what stops `FIELDS` becoming a constant that exists only to satisfy a gate. If the
canonical JSON and `aios_grid.fields_from_workspace({})` ever disagree, the standalone API and
the embedded host are serving two different schemas and the grid's own contract has forked β€”
the exact drift `verify_fields_contract.py` was written to catch, now also caught at startup
on whatever machine is actually running.
"""
import aios_grid
base = [f for f in aios_grid.fields_from_workspace({}) if not f.get("custom")]
if [f["key"] for f in base] != [f["key"] for f in FIELDS]:
raise RuntimeError(
"AIOS web API: the canonical field contract and aios_grid.fields_from_workspace({}) "
"disagree on the base field set β€” embed and standalone would serve different schemas. "
"Run aios-web/verify_fields_contract.py.")
def _startup_notes():
"""Say the two things an operator must know, once, at import β€” never in a response body."""
if aios_session.EPHEMERAL_SECRET:
print("[aios-api] AIOS_SESSION_SECRET is not set β€” signing with a random per-process "
"key. Sessions will not survive a restart and will not work across workers. "
"Set it in production.")
if os.environ.get("AIOS_INSECURE_SSL") == "1":
print("[aios-api] AIOS_INSECURE_SSL=1 β€” TLS verification is DISABLED for outbound "
"requests. Local development only; never in a deployed environment.")
_assert_contract_floor()
_startup_notes()
# β›” THE STATIC MOUNT IS NOW UNAUTHENTICATED, and that is a change this wave made on purpose.
# Before EXIT-3a, `BasicAuth` middleware gated the WHOLE app including this mount. A branded login
# page cannot live behind a password prompt, so the shell's own assets must be public β€” which they
# are: `index.html`, the JS/CSS bundle and the favicon reveal nothing.
#
# ⚠ WHAT THAT SILENTLY DE-GATED, caught in review rather than in production. `web/dist/` also
# carries `sample_customers.json`, a DEV FIXTURE copied from `web/public/`. Its 8 customers are
# synthetic, but the `agent` column holds REAL EMPLOYEE NAMES, and it went from Basic-gated to
# publicly fetchable in this commit. Nothing needs it: both the API bridge and `useCustomerData`
# deleted their sample fallback on purpose ("NOTHING HERE FALLS BACK TO sample_customers.json"),
# and it survives only because it sits in `web/public/`. So it is refused here β€” 404, the same
# answer as any other path that is not part of the app.
#
# The right long-term fix is deleting it from `web/public/` (S2's lane β€” flagged in the mailbox);
# this guard is what makes the API safe regardless of what the bundle happens to contain.
_DEV_FIXTURES = {"sample_customers.json"}
class _AppStatic(StaticFiles):
async def get_response(self, path, scope):
if Path(path).name in _DEV_FIXTURES:
raise StarletteHTTPException(status_code=404, detail="Not Found")
resp = await super().get_response(path, scope)
# Vite content-hashes everything under assets/ (a change is a NEW url), so those are
# immutable β€” a repeat visit re-downloads zero bytes instead of the whole 750 KB bundle.
# index.html must stay revalidated or a deploy would strand returning browsers on the old
# bundle; ETag/304 makes that revalidation a header exchange, not a transfer.
# ⚠ Normalised first: on a Windows host StaticFiles hands this path with backslashes,
# and `startswith("assets/")` silently skipped every asset (measured on the local probe).
if path.replace("\\", "/").lstrip("/").startswith("assets/"):
resp.headers["Cache-Control"] = "public, max-age=31536000, immutable"
else:
resp.headers["Cache-Control"] = "no-cache"
return resp
# static bundle LAST so /api/* wins; html=True serves index.html at / plus the built assets
if _WEB_DIST.is_dir():
app.mount("/", _AppStatic(directory=str(_WEB_DIST), html=True), name="web")
# --- boot prewarm (AIOS_PREWARM=1 β€” set by the Dockerfile, never by tests) -----------------------
# Without this, the first visitor after every deploy/restart pays the full Odoo pool build in
# their request. The thread warms the CONSOLIDATED scope (None, None) + every registered page's
# default envelope; scoped users still pay their own scope's first build, once.
# Env-gated rather than a startup event so importing `api.main` in a gate (verify_api and friends
# run against fakes) can never fire a live Odoo pull.
def _seed_and_sync_store():
"""Bring the analytical store (harness.datastore) LIVE for this container β€” the app.py
bootstrap, mirrored (2026-07-31, owner item 1).
`harness.datastore` powers every measure column/condition, and ONLY app.py used to call
`ensure_seed()` β€” so on a fresh Space disk this container resolved measures against a store
that never existed and every measure cell served blank. Seeding alone was NOT enough either,
and that was measured live the same day: "datastore seeded in 1.7s" followed by an endless
`api:measure-column: ModelError the data cache is still warming up` β€” `ready()` demands
EVERY entity at phase 'live', and a seed that predates a newer entity leaves it un-synced
forever in a process with no sync loop. The SYNC SPRINT after the seed is what closes the
write_date gap and backfills anything the seed lacks (app.py:7779's exact pattern, bounded
passes). Fail-quiet throughout: no seed/token/Odoo β†’ the columns stay blank, the rows still
serve.
"""
import time as _t
t0 = _t.time()
try:
from harness import datastore as _ds
if _ds.ensure_seed():
print(f"[aios-api] datastore seeded in {_t.time() - t0:.1f}s")
# DEDICATED Odoo connection for this thread (the W6 postmortem rule): the sync's
# search_reads must never interleave on the shared client's xmlrpc transport.
import core.odoo as _odoo
try:
_odoo._tlocal.client = _odoo.OdooClient()
except Exception:
pass
res = {}
for i in range(12):
res = _ds.sync_all(log=lambda *a, **k: None)
print(f"[aios-api] datastore sync pass {i + 1}: "
+ ", ".join(f"{k}={v.get('phase')}" for k, v in sorted(res.items())))
if all(v.get("phase") == "live" for v in res.values()):
break
# ⭐ Wave 21 (item 2, "make sure the metrics are correct"): a cursor sync can never see
# a HARD DELETE, and the downloaded seed carries whatever was deleted since it was cut β€”
# one reconcile pass at boot removes both classes of phantom row before the first
# measure is served. MEASURED 2026-08-05: five deleted sale_order_line rows = $284.25 of
# phantom YTD revenue, stable across re-syncs, zero the moment reconcile ran.
try:
_ds.reconcile_deletes(log=lambda *a, **k: None)
except Exception:
pass
print(f"[aios-api] datastore sync done in {_t.time() - t0:.1f}s "
f"(ready={_ds.ready()})")
# ⭐⭐ 2026-08-09 (wave 28, D-107) β€” THE RELATIONAL REBUILD RUNS AT BOOT, HERE.
#
# β›” IT DID NOT BEFORE, AND NOTHING SAID SO. The rebuild lived only inside
# `_store_resync_loop`, whose very first statement is `sleep(1800)` β€” so the earliest a
# freshly booted container could spawn the four locked databases was T+30 MINUTES. The
# symptom was read as "the boot path is silent": `/odoo-tables/status` polled every 30 s
# across a ~20-minute window over two boots returned the pre-wave schema on all 20
# samples. It was not silent, it had not been asked yet. Both halves of D-107 were like
# this β€” a thing that never ran, mistaken for a thing that ran and failed.
#
# ⚠ WHY HERE AND NOT IN `_prewarm`: the tables are DERIVED FROM THE MIRROR, and this is
# the exact line where the mirror has finished advancing β€” seed, up to twelve sync passes,
# then the delete reconcile. Calling it from the other thread would race the seed and hit
# either `ro_con()`'s "still warming" RuntimeError or, worse, a HALF-SYNCED mirror, which
# is D-107's own hypothesis 3: a partial population trips `MAX_SHRINK` and the rebuild
# refuses β€” correctly, but for a reason that reads like a data loss scare.
# ⚠ Same thread on purpose: it is already a daemon and nothing serves requests behind it.
_pull_meta("boot")
_rebuild_odoo_relational("boot")
# ⭐⭐ W32-T07 β€” owner items 13 and 15, DELIVERED. See `_sweep_automation_schemas`.
# ⚠ AFTER the two above and not before: those advance the mirror and can take minutes, and
# this sweep is unrelated to it β€” putting it last means a slow Odoo sync cannot delay the
# one thing on this path that fixes a grid the owner has asked about twice.
_sweep_automation_schemas("boot")
except Exception as e: # noqa: BLE001
print(f"[aios-api] datastore seed/sync skipped: {e}")
def _pull_meta(why):
"""Pull Meta Ads into THIS container's mirror, before the relational rebuild reads it.
β›” WHY IT HAS TO HAPPEN HERE AND NOT ON A LAPTOP. The mirror is a FILE that lives beside the
process; the Space's copy is seeded from the HF dataset and knows nothing about a DuckDB on a
developer's box. Running `meta_store --sync` locally populates the local mirror and the LIVE
product stays empty β€” which is the whole difference between "the loader works" and "the
product has the data". Odoo is already arranged this way (`sync_all` runs in the container);
this is the same arrangement for the second connector.
⚠ FAIL-QUIET AND SILENT WHEN THERE IS NOTHING TO DO. No token => no Meta => one line, no
error: a tenant that has not connected Meta is a normal state, and this runs on every boot.
⚠ The window is deliberately SHORT here (`META_INSIGHTS_DAYS`, default 7 at boot) because boot
is not the place for a 90-day backfill β€” the resync pass widens it.
"""
try:
from harness import meta_store as _meta
if not _meta.token():
print(f"[aios-api] meta sync skipped ({why}): no META_ADS_ACCESS_TOKEN in this "
f"deployment - the connector is idle, not broken")
return
# β›” PASSED, NOT SET IN THE ENVIRONMENT. `meta_store.INSIGHTS_DAYS` binds at
# IMPORT, so an `os.environ.setdefault` here executed after the module was
# already loaded and changed NOTHING: every boot pulled 90 days instead of 7,
# which is the slow path that trips the per-ad-account rate limit and never
# finishes. A knob read at import cannot be turned by a caller at runtime.
rep = _meta.sync("royal-imports", log=lambda *_a: None, insights_days=7)
for p in rep.get("problems") or []:
print(f"[aios-api] meta sync PROBLEM ({why}): {p}")
print(f"[aios-api] meta sync done ({why}): "
+ ", ".join(f"{k}={v['in_mirror']}" for k, v in sorted(rep["tables"].items())))
except Exception as e: # noqa: BLE001
print(f"[aios-api] meta sync FAILED ({why}): {type(e).__name__}: {e}")
def _rebuild_meta_relational(why, rt):
"""Spawn/refresh the `ut_meta_*` locked databases off the SAME mirror the Odoo half just used.
⭐ CALLED FROM INSIDE `_rebuild_odoo_relational`, ON PURPOSE, and the reason is D-29 rather than
tidiness: `harness/datastore` is a ONE-FILE-AT-A-TIME process global, and that caller has just
established which tenant's file this process holds open. Spawning Meta here inherits that
binding instead of rebinding it under live readers β€” which is the documented way to serve one
tenant's rows to another with nothing raised.
⚠ SILENT WHEN THERE IS NOTHING TO DO. A tenant that never connected Meta has no `meta_*` tables
in its mirror; `refresh` returns `{}` and says so once. That is a normal state, not a failure,
and it must not print an error every 30 minutes for every tenant that does not use Meta.
β›” Its own try/except for the reason the two passes above have theirs: a Meta refusal must not
cancel an Odoo rebuild that already succeeded.
"""
try:
import meta_relational as _meta
counts = _meta.refresh(rt, why)
if counts:
print(f"[aios-api] meta relational rebuild done ({why}): "
+ ", ".join(f"{k}={v}" for k, v in sorted(counts.items())))
try:
import automation_engine as _engine
_engine.refresh_relations(rt, log=lambda *_a: None)
except Exception as e: # noqa: BLE001
print(f"[aios-api] meta relation cells failed ({why}): {type(e).__name__}: {e}")
except Exception as e: # noqa: BLE001
print(f"[aios-api] meta relational rebuild FAILED ({why}): {type(e).__name__}: {e}")
def _sweep_automation_schemas(why):
"""β›”β›” WAVE 32 Β· `W32-T07` β€” MAKE D'S DECLARATIONS REACH TENANTS THAT ALREADY HAVE THE TABLES.
Owner items 13 and 15 β€” the TikTok comments lock, and the comment CONTENT column he says he has
asked for *"many times"*. **Both were already correct in the source.** `TT_COMMENT_FIELDS`
carries `field_def("text", "Comment")` and `TT_LOCKED_TABLES` already contains the comments
table, both since wave 31, with his words quoted in the comment beside them. So this ticket is
not a schema change and there is nothing to design: it is DELIVERY.
β›” THE MECHANISM, WHICH IS THE WHOLE OF IT. `ut_ensure` MERGES fields into an existing table and
stamps `recordMode` β€” but only **when something calls it**, and the only callers are automation
runs. A tenant whose `ut_tt_comments` was spawned before the declaration changed keeps the old
shape until an automation happens to run against it. Nothing sweeps existing tenants. That is
[[a-migration-that-runs-on-the-next-write]], and it is this wave's stated thesis: a declaration
that never reaches a tenant is indistinguishable from one that was never written.
β›”β›” AND IT MUST RUN **IN THE CONTAINER**, WHICH IS WHY THIS IS IN `main.py` AND NOT A SCRIPT.
D-195, measured three times: a developer's CLI write to the tenant store is reverted by the
running Space within a minute (download-modify-upload, last-write-wins) β€” and **the write
reports success every time**, then a FRESH read confirms it, and it is gone by the next poll.
A connector's tables must be spawned BY THE CONTAINER; a CLI spawn is a dry run that lies.
⚠ EVERY TENANT, unlike `_rebuild_odoo_relational` below β€” and the asymmetry is deliberate
rather than an oversight. That function is scoped to royal because it derives from the DuckDB
mirror, and `harness/datastore` is a ONE-FILE-AT-A-TIME process global (D-29): rebinding it per
tenant in a daemon thread can serve one tenant's rows to another with nothing raised. This
sweep touches only `user_tables` through each tenant's own `rt`, which has no such global β€” so
the hazard that scopes that one does not exist here, and TikTok automations run in tenants
other than #0.
⚠ CHEAP ON A CORRECT TENANT: `ut_ensure` short-circuits when nothing changed, so this is a read
per child table on a tenant that is already right, and the whole delivery on one that is not.
⚠ ONE TENANT'S FAILURE MUST NOT STOP THE NEXT. Each is wrapped: a tenant whose store is
unreachable at boot is reported and skipped, never allowed to abort the sweep for everyone.
"""
try:
import automation_engine as _eng
from harness import runtime as _runtime
except Exception as e: # noqa: BLE001
print(f"[aios-api] schema sweep ({why}) SKIPPED β€” import failed: {e}")
return
try:
schemas = _eng.platform_schemas()
except Exception as e: # noqa: BLE001
print(f"[aios-api] schema sweep ({why}) SKIPPED β€” no declarations: {e}")
return
tenants = []
try:
tenants = _runtime.known_tenants()
except Exception as e: # noqa: BLE001
print(f"[aios-api] schema sweep ({why}) SKIPPED β€” tenant list unreadable: {e}")
return
for slug in tenants:
try:
rt = _runtime.get_runtime(slug)
ensured = []
for s in schemas:
for key, child in (s.get("children") or {}).items():
got = _eng.ut_ensure(rt, child["label"], child["fields"], "automation",
key=key, lock_fields=True,
record_mode=child["record_mode"])
if got:
ensured.append(got)
# ⭐ THE RETRACTION (D's `retract_foreign_presets`, D-152), AFTER the children loop.
# β›” THE SWEEP ABOVE MAKES COLUMNS **ARRIVE** AND CANNOT MAKE STALE ONES **LEAVE**, and
# T07's `done-when` asserts both ("no `ut_tt_*` grid carries an Instagram column"). On a
# tenant that ran TikTok before W30-T08 the 26 machine-authored IG columns are still
# there β€” the detector was fixed, the damage never was.
# ⚠ `kept` IS THE HONEST HALF: a foreign column that HOLDS DATA is REPORTED, never
# deleted. If it is non-empty, the screenshot shows a column and the report is the
# answer (W30/R6's second sentence).
st = {}
try:
st = _eng.retract_foreign_presets(rt, log=lambda *a, **k: None) or {}
except Exception as e: # noqa: BLE001
print(f"[aios-api] schema sweep ({why}) {slug}: retraction FAILED: {e}")
print(f"[aios-api] schema sweep ({why}) {slug}: ensured={len(ensured)} "
f"tables={st.get('tables', 0)} columns={st.get('columns', 0)} "
f"cells={st.get('cells', 0)} flags={st.get('flags', 0)} "
f"kept={st.get('kept') or []}")
except Exception as e: # noqa: BLE001
print(f"[aios-api] schema sweep ({why}) {slug}: FAILED: {e}")
# ⭐ A SUCCESS MARKER, for `_rebuild_odoo_relational`'s stated reason: D-107 was chased for a
# day on ABSENT log markers, which cannot tell "it ran and was fine" from "it was never
# reached". Three failure markers and no success marker makes silence ambiguous.
print(f"[aios-api] schema sweep ({why}) done over {len(tenants)} tenant(s)")
def _rebuild_odoo_relational(why):
"""Spawn/refresh the four locked Odoo databases, then compute their cells. `why` is 'boot' or
'resync' and rides every log line, because "it failed" and "it failed at boot, before anyone
could have asked" are different diagnoses.
⭐ THE SUCCESS LINE IS NOT DECORATION β€” it is the control this path lacked. D-107 was chased
for a day on the strength of *absent* log markers, which cannot distinguish "the rebuild ran
and was fine" from "the rebuild was never reached". Three failure markers and no success
marker means silence is ambiguous; now it is not.
⚠ SCOPED TO ROYAL-IMPORTS, deliberately, and it is NOT the D-29 shortcut it resembles. The
caller has just advanced whichever DuckDB file this process holds open β€” tenant #0's β€” and
royal is the only tenant with an Odoo mirror to derive from (R1). Iterating tenants here walks
straight into D-29's documented hazard: `harness/datastore` is a ONE-FILE-AT-A-TIME
process-global, and rebinding it in a daemon thread under live readers can serve one tenant's
rows to another with nothing raised. `is_royal` stays the authority on which slugs qualify.
"""
try:
import odoo_relational as _rel
from harness import runtime as _runtime
if not _rel.is_royal("royal-imports"):
return
_rt = _runtime.get_runtime("royal-imports")
counts = _rel.refresh(_rt, "royal-imports")
# ⭐⭐ AND THEN COMPUTE THE CELLS, which `refresh` does NOT do.
#
# β›” THE FAILURE THIS CLOSES IS THE WORST-LOOKING KIND. `refresh` writes rows and field
# DEFINITIONS; every Link and Rollup cell comes from a separate pass. Those passes used to
# live only on the automation `tick`, which fires from an EXTERNAL EventBridge cron β€” so a
# fresh boot landed 71,954 rows with eleven fully-configured relational columns and every
# one of them BLANK until an unrelated scheduler happened to run. Nothing errors; the
# tables simply look finished and answer nothing.
#
# ⚠ `refresh_relations`, NOT `compute_relation_cells`. The latter computes a change count
# over a blob it was handed and PERSISTS NOTHING; the former walks the tenant and writes.
# Calling the inner one here would return a plausible number and change no cell.
#
# ⚠ ONE try/except PER PASS, for the reason `tick` states at its own copies: a source
# rollup REFUSES loudly on a truncated group set, and a shared block would let that honest
# refusal silently cancel a relational pass that had already succeeded.
try:
import automation_engine as _engine
_engine.refresh_relations(_rt, log=lambda *_a: None)
except Exception as e: # noqa: BLE001
print(f"[aios-api] odoo relation cells failed ({why}): {type(e).__name__}: {e}")
try:
import rollup_sql as _rollup
for _b, _key, _l, _f in _rel.TABLES:
_rollup.compute(_rt, _key)
except Exception as e: # noqa: BLE001
print(f"[aios-api] odoo source rollups failed ({why}): {type(e).__name__}: {e}")
print(f"[aios-api] odoo relational rebuild done ({why}): "
+ ", ".join(f"{k}={v}" for k, v in sorted((counts or {}).items())))
_rebuild_meta_relational(why, _rt)
except Exception as e: # noqa: BLE001
# ⚠ The TYPE is named here as it is in the two inner handlers. The original printed only
# `{e}`, and a bare message is exactly what made D-107 unreadable from the outside: a
# `BinderException` about a missing column is a different action from a timeout or an auth
# failure, and the text alone often does not say which it was.
print(f"[aios-api] odoo relational rebuild failed ({why}): {type(e).__name__}: {e}")
def _store_resync_loop():
"""Keep the analytical mirror current β€” the API-process stand-in for the re-sync the
Streamlit app piggybacks on page renders. Measure memos key on the pool stamp, so a
refreshed pool re-reads the freshly synced store."""
import time as _t
passes = 0
while True:
# ⭐⭐ WAVE 32 Β· OWNER ITEM 11 / R11 β€” THE TENANT'S OWN CADENCE, NOT A HARDCODED 1800.
#
# β›” THIS LINE IS WHY THE SETTING WAS NOT A SETTING. SESSION B built the whole of item 11 β€”
# the 30m/1h/4h/daily/manual presets, the server-side clamp, the config door and
# `odoo_relational.sync_seconds()` which turns the stored preset into seconds β€” and its own
# ticket said the one-line change belonged to A because `main.py` is A's file. That ASK was
# never sent, so the value was stored, displayed, clamped and IGNORED: a person could pick
# "every 4 hours" and the loop would keep resyncing every 30 minutes with nothing anywhere
# reporting the disagreement. Found by `verify_reachability` naming `sync_seconds` as a
# function whose ONLY caller was its own gate [[artifact-with-no-importer]].
#
# ⚠ `None` MEANS MANUAL AND MUST NOT MEAN ZERO. R11's `manual` preset returns None from
# `sync_seconds`; treating that as a falsy interval would spin this loop with no sleep at
# all. It parks at the default cadence instead and simply does no work β€” the tenant asked
# not to be synced automatically, not for the server to stop breathing.
# ⚠ RE-READ EVERY PASS, deliberately: a cadence changed in Settings takes effect on the
# next cycle rather than at the next container restart, which is what makes it a setting.
# ⚠ FAIL-SAFE TO 1800 β€” an unreadable config must not become a tight loop. The floor is
# enforced in `sync_seconds` as well as at the write door, for the same reason.
_every = 1800
try:
import odoo_relational as _rel_cad
from harness import runtime as _rt_cad
_secs = _rel_cad.sync_seconds(_rt_cad.get_runtime("royal-imports"))
_every = 1800 if _secs is None else max(int(_secs), 60)
except Exception: # noqa: BLE001
pass
_t.sleep(_every)
passes += 1
try:
from harness import datastore as _ds
import core.odoo as _odoo
try:
_odoo._tlocal.client = _odoo.OdooClient()
except Exception:
pass
_ds.sync_all(log=lambda *a, **k: None)
# Wave 21 β€” every 4th pass (~2h): purge hard-deleted rows the cursor sync cannot
# see (the boot pass's comment has the measured case). Cheap id-sweep per entity;
# without it deleted Odoo lines inflate every sum on the mirror FOREVER.
if passes % 4 == 0:
_ds.reconcile_deletes(log=lambda *a, **k: None)
except Exception as e: # noqa: BLE001
print(f"[aios-api] store resync failed: {e}")
# ⭐ WAVE 27 item 17 (E's ASK ->A, resolved): the Odoo RELATIONAL tables are rebuilt
# AFTER the mirror they are derived from, in the same pass and in that order β€” deriving
# from a mirror this loop is about to advance would publish a worklist one cycle stale
# every single time.
#
# β›” OUTSIDE the try/except above, NOT folded into it. A failing relational rebuild must
# not swallow the sync's error message, and β€” worse the other way β€” a sync failure must
# not skip a rebuild that had nothing wrong with it. Two independent failures, two
# independent logs. (`_rebuild_odoo_relational` carries its own handlers.)
#
# ⚠ THE REBUILD IS NOT THE FRESHNESS GUARANTEE β€” the rows carry a visible `_refreshed`
# stamp for that. This loop is a daemon thread whose failure path is a `print` (D-29), so
# "the wiring exists" and "the data is current" are different claims and only the stamp
# can tell a user which one they are looking at.
#
# ⭐ ONE IMPLEMENTATION, TWO CALLERS (wave 28). This block used to be the only copy, which
# is what made the boot path silent for 30 minutes after every restart; it is now the
# SECOND caller of the same function `_seed_and_sync_store` calls at boot. A copy here
# would be a second thing to keep in step, and the two would answer differently on the
# next ruling β€” the exact shape the Views top-up was just fixed for on the other side of
# this wave.
# β›” THE META PULL RIDES THE RESYNC TOO, AND LEAVING IT OUT WAS A REAL GAP β€” caught by
# reading the deploy's own boot log rather than by any gate. `_pull_meta` was wired into
# `_seed_and_sync_store` ALONE, i.e. it ran exactly once per container, at boot, behind a
# full Odoo sync. So a boot where the Graph call was rate-limited, slow or simply after the
# thread died left the mirror empty with NOTHING to retry it: the relational rebuild below
# would then find no `meta_*` tables every 30 minutes forever and skip, silently and
# correctly. A connector that can only ever be established at boot is one bad boot away
# from being permanently absent.
# ⚠ Cheap when there is nothing to do: no token => one line and return.
_pull_meta("resync")
_rebuild_odoo_relational("resync")
def _prewarm():
import time as _t
t0 = _t.time()
# ⚠ The store sync runs in its OWN thread, never ahead of the pool warm: a stale seed can
# take many minutes of XML-RPC to close, and the first live probe of this arrangement
# showed the pool warm (42s) silently queued behind it β€” the whole app cold for every
# visitor while a background column channel caught up. Measure cells retry via the
# transient rule until the sync lands; nothing else waits on it.
import threading as _th
_th.Thread(target=_seed_and_sync_store, daemon=True, name="store-seed-sync").start()
try:
from harness import runtime as _runtime
rt = _runtime.get_runtime("royal-imports")
routes_customers.warm_default(rt)
import pages as _pages
_pages.warm_default(rt)
# ⭐⭐ WAVE 30 · T12, THE COLD PATH (owner items 4/5). Automation was the ONE module absent
# from this list, so its memo was always filled by a visitor rather than by boot: call 1 of
# `GET /automations` after every deploy downloaded the whole `user_tables` document (35.8 MB
# ceiling, under the store lock) onto whoever clicked first. Memoising the WARM path β€” two
# waves of it β€” could not touch that, because the cold call is the one that fills the memo.
# ⚠ It elects a STRING and keeps no document; see `warm_default`'s own note on why caching
# the bucket would trade a latency for memory this tier does not have.
import routes_automation as _rauto
_rauto.warm_default(rt)
print(f"[aios-api] prewarm done in {_t.time() - t0:.1f}s")
except Exception as e: # noqa: BLE001 β€” boot must not die on a warm-up
print(f"[aios-api] prewarm skipped: {e}")
# ═════════════════════════════════════════════════════════════════════════════════════════════
# ⭐⭐ W31-T46 / D-160 β€” THE MIRROR IS SEEDED WHETHER OR NOT `AIOS_PREWARM` IS SET.
# ═════════════════════════════════════════════════════════════════════════════════════════════
#
# THE DEFECT, and it has cost a release once already (owner item 12, wave 20). `_prewarm()` was
# the ONLY caller of `_seed_and_sync_store()`, which is the ONLY caller of
# `datastore.ensure_seed()`. So on a fresh Space disk with `AIOS_PREWARM` anything but `1`:
# no `royal.duckdb` β‡’ `datastore.ready()` False forever β‡’ `ro_con()` refuses β‡’ every measure
# column blank AND β€” since wave 30 put the Odoo grids on the mirror β€” **two user-facing grids
# serve nothing**, with the app RUNNING, the deploy green and the tag correct. Last time the
# symptom was read as a pinned tag and a store problem for days.
#
# β›” AND THE FLAG IS EASIER TO LOSE THAN IT LOOKS: `deploy_web.py` pushes `AIOS_PREWARM=1`
# EXPLICITLY (to overwrite a stale `0`, because a Space secret survives a redeploy) β€” but that
# push sits under `if TARGET:`, so an ordinary bare `python deploy_web.py` skips it. A guard
# written for the dangerous case that the ordinary case walks straight past.
#
# ⭐ SO THE SEED IS SPLIT OFF AND MADE UNCONDITIONAL, which is T46's first branch rather than its
# fallback. It is the right half to move because it is the CHEAP, SAFE one: `ensure_seed()`
# touches no Odoo (it is one `hf_hub_download` of a snapshot), returns immediately when the file
# is already there, and returns immediately without `HF_TOKEN`. The EXPENSIVE, live half β€”
# `sync_all()`'s XML-RPC passes, the pool warm, the resync loop β€” stays exactly where it was,
# because the env gate's stated reason is still true: importing `api.main` in a gate must never
# fire a live Odoo pull.
#
# ⚠ THE `DB_PATH.exists()` PRE-CHECK IS WHAT KEEPS THIS FREE. It is a stat, and it is False only
# on a genuinely fresh disk β€” so on every developer box and in every gate run the thread is never
# started at all, and on a fresh Space it does exactly the thing whose absence blanks the grids.
#: What the boot seed did, so a surface can REPORT it instead of an operator inferring it from a
#: blank grid. R6's second sentence: a limit that cannot be removed is reported with its cause.
MIRROR_SEED = {"attempted": False, "seeded": False, "cause": "", "recommendation": ""}
def _seed_mirror_if_absent():
"""Hydrate the analytical mirror when this container has none β€” INDEPENDENT of `AIOS_PREWARM`.
Fail-quiet by design, and it records WHY rather than only whether: "there is no mirror and no
HF_TOKEN to fetch one" and "there is no mirror and the fetch failed" are different operator
actions, and a blank grid cannot tell them apart.
"""
from harness import datastore as _ds
MIRROR_SEED["attempted"] = True
try:
if _ds.ensure_seed():
MIRROR_SEED["seeded"] = True
print("[aios-api] analytical mirror seeded at boot (independent of AIOS_PREWARM)")
return True
if not _ds.DB_PATH.exists():
MIRROR_SEED["cause"] = (
"this container has no analytical mirror and the seed snapshot could not be "
"fetched (no HF_TOKEN, or the dataset was unreachable)")
MIRROR_SEED["recommendation"] = (
"set HF_TOKEN on the deployment; until then every connected grid and every "
"measure column served from the mirror is empty")
print(f"[aios-api] NO ANALYTICAL MIRROR: {MIRROR_SEED['cause']}")
except Exception as e: # noqa: BLE001 β€” boot must not die
MIRROR_SEED["cause"] = f"the boot seed raised {type(e).__name__}: {e}"
MIRROR_SEED["recommendation"] = "check HF_TOKEN and the seed dataset's availability"
print(f"[aios-api] mirror seed skipped: {e}")
return False
try:
from harness import datastore as _ds_boot
if not _ds_boot.DB_PATH.exists():
import threading as _threading_seed
_threading_seed.Thread(target=_seed_mirror_if_absent, daemon=True,
name="mirror-seed").start()
except Exception as e: # noqa: BLE001
print(f"[aios-api] mirror seed not scheduled: {e}")
if os.environ.get("AIOS_PREWARM") == "1":
import threading as _threading
_threading.Thread(target=_prewarm, daemon=True, name="prewarm").start()
_threading.Thread(target=_store_resync_loop, daemon=True, name="store-resync").start()