feat(injects,conversations,scheduler,gateway_tools,api): wake priority, master and parent aliases, scheduled injects start a turn

This commit is contained in:
hh
2026-08-30 20:34:12 +02:00
parent 064379ca90
commit a8491330fa
11 changed files with 215 additions and 47 deletions
+51 -7
View File
@@ -103,6 +103,9 @@ __all__ = [
_log = logging.getLogger("beaver_gateway.core.conversations")
MASTER_ALIAS = "master"
PARENT_ALIAS = "parent"
SEEDS = ("clean", "morning", "copy", "brief")
_STATUSES = ("open", "merged", "closed", "archived")
_DEFAULT_MERGE_PROMPT = (
@@ -342,6 +345,23 @@ class Conversations:
async with self._db.session() as session:
return await session.get(Conversation, row_id)
async def resolve(
self, key: str, *, origin: Conversation | None = None
) -> Conversation | None:
"""A conversation by public id or alias.
``master`` is the open master, ``parent`` the parent of ``origin`` -
the names a job or a branch can use without knowing today's ids.
"""
key = key.strip()
if key == MASTER_ALIAS:
return await self._open_master()
if key == PARENT_ALIAS:
if origin is None or origin.parent_id is None:
return None
return await self.get_row(origin.parent_id)
return await self.get(key)
async def find(
self,
*,
@@ -596,7 +616,7 @@ class Conversations:
if live is not None and live.busy:
return True
pending = await self._queue.pending(cast("int", row.id))
return any(i.priority in ("user", "urgent") for i in pending)
return any(i.priority in ("user", "urgent", "wake") for i in pending)
# ---- routing -------------------------------------------------------
@@ -841,6 +861,22 @@ class Conversations:
origin: str = "system",
interrupt: bool = True,
) -> InjectQueueItem:
if conv.kind == "master" and conv.status != "open":
live = await self._open_master()
if live is None:
_log.error(
"inject (%s) for closed master %s: no open master, it stays there",
origin,
conv.external_id,
)
else:
_log.info(
"inject (%s) for closed master %s goes to %s",
origin,
conv.external_id,
live.external_id,
)
conv = live
item = await self._queue.push(
conversation_id=cast("int", conv.id),
priority=urgency,
@@ -923,12 +959,20 @@ class Conversations:
return result
async def schedule(
self, conv: Conversation, at: str, text: str, *, dedupe_key: str | None = None
self,
conv: Conversation,
at: str,
text: str,
*,
urgency: Priority = "wake",
dedupe_key: str | None = None,
) -> tuple[int | None, datetime]:
if self.scheduler is None:
msg = "no scheduler; `schedule` is unavailable"
raise RuntimeError(msg)
return await self.scheduler.schedule(conv, at, text, dedupe_key=dedupe_key)
return await self.scheduler.schedule(
conv, at, text, urgency=urgency, dedupe_key=dedupe_key
)
async def schedules(self, conv: Conversation | None = None) -> list[dict[str, Any]]:
return await self.scheduler.scheduled(conv) if self.scheduler else []
@@ -1632,15 +1676,15 @@ class Conversations:
) -> tuple[list[InjectQueueItem] | None, float | None]:
if not items:
return None, None
normals = [i for i in items if i.priority == "normal"]
head = items[0]
if head.priority == "urgent":
return [head], None
if head.priority == "user":
return [head, *normals], None
tail = [i for i in items if i is not head and i.priority in ("wake", "normal")]
if head.priority in ("user", "wake"):
return [head, *tail], None
age = (datetime.now(UTC) - _aware(head.created_at)).total_seconds()
if age >= self._normal_window:
return normals, None
return [head, *tail], None
return None, max(self._normal_window - age, 1.0)
async def _run_batch(