5809ad3adb
Shared runtime - One agent entry point (runtime.invoke_agent_for_user / prepare_agent_turn) for web, email, iMessage, WhatsApp, scheduled and cron, so identity, permissions, memories, summaries and metrics match across channels. - Serialize turns per checkpoint thread; /query validates input and returns 409 while a durable run owns the conversation. Reconnectable chat (durable runs) - POST /runs creates an idempotent, owner-scoped run; GET .../events replays an ordered SSE log by cursor, so generation survives refresh and device switches. Stop cancels; disconnect only detaches. /stream-query is a compatibility adapter. - Coalesce streamed deltas (150 ms) and wake same-process subscribers instead of polling; keep buffered text when a run is stopped. - Drain runs on shutdown; serve via Hypercorn with a shutdown timeout covering the drain (and no longer in debug mode). CHAT_RUN_* settings are forwarded. Scheduled messages and cron - Atomic delivery claims, attempt history and explicit retries; ambiguous sends become "uncertain" and are never resent automatically. - Cron triggers need a run_id/Idempotency-Key; legacy jobs need an explicit owner. - Cancelling never rewrites a started delivery; series stop via tombstones. Legacy sent rows are not forked into new series. Malformed bodies return 400. History, integrations and retrieval - Message-content search, pinned conversations, JSON/Markdown export and a memory editor. - Integration status panel and YNAB data-freshness provenance. - Date-filtered document/note search with stable citations and a source viewer. Migration 20 is generated by aerich. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
26 lines
944 B
Python
26 lines
944 B
Python
"""Request-body checks shared by the scheduled message and cron endpoints."""
|
|
|
|
from quart import jsonify, request
|
|
|
|
|
|
async def json_object(text_fields: tuple[str, ...], nullable: tuple[str, ...] = ()):
|
|
"""Return (data, None) for a non-empty object whose text fields are strings.
|
|
|
|
Fields in `nullable` may also be an explicit null. Otherwise return
|
|
(None, error_response) so malformed bodies get a 400 instead of failing on
|
|
`.get`/`.strip` with a 500.
|
|
"""
|
|
data = await request.get_json(silent=True)
|
|
if not isinstance(data, dict) or not data:
|
|
return None, (jsonify({"error": "Invalid payload"}), 400)
|
|
wrong = [
|
|
name
|
|
for name in text_fields
|
|
if name in data
|
|
and not isinstance(data[name], str)
|
|
and not (data[name] is None and name in nullable)
|
|
]
|
|
if wrong:
|
|
return None, (jsonify({"error": f"{', '.join(wrong)} must be text"}), 400)
|
|
return data, None
|