feat(journal): closeout catch-up on startup when slot already passed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 14:36:29 -04:00
parent b88d5ee6b3
commit fc6ebf81eb
2 changed files with 79 additions and 0 deletions
@@ -111,6 +111,37 @@ def _run_closeout_threadsafe(user_id: int) -> None:
asyncio.run_coroutine_threadsafe(_do_closeout(user_id), _loop)
async def _closeout_catchup(user_id: int) -> None:
"""On startup, run yesterday's closeout once if the slot already passed
and no entry for yesterday exists in observations_raw.
"""
try:
tz_str = await get_user_timezone(user_id)
tz = _resolve_tz(tz_str)
config = await get_journal_config(user_id)
if not config.get("closeout_enabled", True):
return
rollover_hour = int(config.get("day_rollover_hour", 4))
now = datetime.datetime.now(tz)
# Slot hasn't passed yet today → wait for the cron.
if now.hour < rollover_hour:
return
yesterday = (now - datetime.timedelta(days=1)).date()
from fabledassistant.services.user_profile import get_profile
profile = await get_profile(user_id)
existing_dates = {
(e or {}).get("date") for e in (profile.observations_raw or [])
}
if yesterday.isoformat() in existing_dates:
return
from fabledassistant.services.journal_closeout import run_for_user
await run_for_user(user_id=user_id, yesterday=yesterday)
except Exception:
logger.exception("Closeout catch-up failed for user %d", user_id)
async def update_user_schedule(user_id: int) -> None:
"""Add or replace this user's daily-prep + closeout jobs from current config."""
if _scheduler is None:
@@ -155,6 +186,9 @@ async def _register_all_user_jobs() -> None:
users = (await session.execute(select(User))).scalars().all()
for user in users:
await update_user_schedule(user.id)
# Fire catch-up asynchronously so a slow LLM call doesn't block startup
if _loop is not None:
asyncio.run_coroutine_threadsafe(_closeout_catchup(user.id), _loop)
def start_journal_scheduler(loop: asyncio.AbstractEventLoop) -> None: