feat(cron): bind scheduled automations to sessions

This commit is contained in:
chengyongru
2026-06-11 19:48:07 +08:00
parent ffae1dca6d
commit a326ba40f4
28 changed files with 1277 additions and 82 deletions
+33
View File
@@ -0,0 +1,33 @@
"""Shared metadata helpers for scheduled automation turns."""
from __future__ import annotations
from typing import Any, Mapping
AUTOMATION_TRIGGER_META = "_automation_trigger"
AUTOMATION_DEFER_UNTIL_IDLE_META = "_defer_until_session_idle"
def automation_trigger(metadata: Mapping[str, Any] | None) -> dict[str, Any] | None:
"""Return structured automation trigger metadata when present."""
raw = (metadata or {}).get(AUTOMATION_TRIGGER_META)
return raw if isinstance(raw, dict) else None
def is_automation_turn(metadata: Mapping[str, Any] | None) -> bool:
return automation_trigger(metadata) is not None
def defer_until_session_idle(metadata: Mapping[str, Any] | None) -> bool:
return bool(
is_automation_turn(metadata)
and (metadata or {}).get(AUTOMATION_DEFER_UNTIL_IDLE_META) is True
)
def automation_run_id(metadata: Mapping[str, Any] | None) -> str | None:
trigger = automation_trigger(metadata)
if not trigger:
return None
value = trigger.get("run_id")
return value if isinstance(value, str) and value else None
+32
View File
@@ -84,6 +84,7 @@ class CronService:
):
self.store_path = store_path
self._action_path = store_path.parent / "action.jsonl"
self._run_records_dir = store_path.parent / "runs"
self._lock = FileLock(str(self._action_path.parent) + ".lock")
self.on_job = on_job
self._store: CronStore | None = None
@@ -325,6 +326,23 @@ class CronService:
tmp_path.unlink(missing_ok=True)
raise
@staticmethod
def _safe_run_record_name(run_id: str) -> str:
return "".join(c if c.isalnum() or c in "._-" else "_" for c in run_id)
def write_run_record(self, run_id: str, record: dict[str, Any]) -> None:
"""Write an internal audit record for one cron execution."""
name = self._safe_run_record_name(run_id)
if not name:
name = str(uuid.uuid4())
path = self._run_records_dir / f"{name}.json"
payload = {
**record,
"run_id": run_id,
"updated_at_ms": _now_ms(),
}
self._atomic_write(path, json.dumps(payload, indent=2, ensure_ascii=False))
async def start(self) -> None:
"""Start the cron service."""
self._running = True
@@ -473,6 +491,20 @@ class CronService:
jobs = store.jobs if include_disabled else [j for j in store.jobs if j.enabled]
return sorted(jobs, key=lambda j: j.state.next_run_at_ms or float('inf'))
def list_bound_agent_jobs_for_session(
self,
session_key: str,
*,
include_disabled: bool = True,
) -> list[CronJob]:
"""Return user-created bound automation jobs owned by *session_key*."""
return [
job
for job in self.list_jobs(include_disabled=include_disabled)
if job.payload.kind == "agent_turn"
and job.payload.session_key == session_key
]
def add_job(
self,
name: str,