feat(journal): auto-scheduler for curator (Phase 2)
The curator now runs automatically every 15 minutes against any journal conversation that has user messages newer than its last curator run. Manual triggers from Phase 1b still work and now also stamp the timestamp so the scheduler doesn't double-process. Migration 0048: - conversations.last_curator_run_at (timestamptz, nullable). - Partial index ix_conversations_journal_last_curator on the column filtered to conversation_type='journal'. The scheduler's candidate query is "journal AND (NULL OR stale)" so an index narrowed to journal rows is the right shape — index size stays small even on instances with many non-journal conversations. models/conversation.py: - New `last_curator_run_at` column on Conversation. DateTime imported. services/curator_scheduler.py (new): - IntervalTrigger every 15 min via BackgroundScheduler (same pattern as journal_scheduler.py). - _candidate_conversations(): SELECT journal conversations where the newest user message is newer than last_curator_run_at (or NULL). Capped at 20 per sweep so a backlog after downtime doesn't stall the scheduler. - _sweep() processes candidates sequentially under an asyncio.Lock so overlapping ticks can't double-fire on the same conversation. Failed runs leave the timestamp alone — natural retry on next sweep. - start_/stop_curator_scheduler() wired into app.py boot/shutdown. routes/journal.py: - Manual /api/journal/curator/run/<conv_id> stamps last_curator_run_at on success. Errors don't stamp so the scheduler retries. What's still pending: - Phase 3: feedback loop (curator summary into chat context). Currently the curator's summary lives in the run result but doesn't reach the chat model. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -201,6 +201,22 @@ async def trigger_curator_run(conv_id: int):
|
||||
|
||||
from fabledassistant.services.curator import run_curator_for_conversation
|
||||
result = await run_curator_for_conversation(conv_id)
|
||||
|
||||
# Stamp last_curator_run_at on success so the scheduler doesn't
|
||||
# immediately re-process the same conversation on its next sweep.
|
||||
# Errored runs intentionally leave the timestamp alone so the
|
||||
# scheduler retries them.
|
||||
if not result.error:
|
||||
import datetime as _dt
|
||||
from sqlalchemy import update as _update
|
||||
async with _async_session() as _sess:
|
||||
await _sess.execute(
|
||||
_update(_Conversation)
|
||||
.where(_Conversation.id == conv_id)
|
||||
.values(last_curator_run_at=_dt.datetime.now(_dt.timezone.utc))
|
||||
)
|
||||
await _sess.commit()
|
||||
|
||||
return jsonify(result.to_dict())
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user