feat(integration): wire v2 workstreams together (engine policy/budget, sandbox, tenant checks, order route, docs)
Engine honours org policy and monthly budget, legacy runner skips engine tasks, sandbox session provisioned on demand, project fallback for threads, reviewer fallback, tenant checks on project ids, memory reflection respects grants, expiry worker, MCP pipeline_status, login redirect, sharing settings, e2e smoke. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
36564e3f2b
commit
fef5a9deab
@@ -64,10 +64,21 @@ def _billing_pass(args: argparse.Namespace):
|
||||
|
||||
async def one_pass() -> WorkerOutcome:
|
||||
end = utcnow()
|
||||
return await worker.roll_up(
|
||||
rolled = await worker.roll_up(
|
||||
period_start=end - timedelta(days=max(1, args.period_days)),
|
||||
period_end=end,
|
||||
)
|
||||
# The same pass also expires stale memory lessons (see BillingWorker.expire_memories).
|
||||
expired = await worker.expire_memories()
|
||||
merged = WorkerOutcome(
|
||||
name=rolled.name,
|
||||
processed=rolled.processed + expired.processed,
|
||||
skipped=rolled.skipped + expired.skipped,
|
||||
failed=rolled.failed + expired.failed,
|
||||
)
|
||||
for message in [*rolled.notes, *expired.notes]:
|
||||
merged.note(message)
|
||||
return merged
|
||||
|
||||
return one_pass
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class AiRuntimeWorker:
|
||||
|
||||
async with worker_session() as session:
|
||||
await self._drain_runs(session, outcome, limit=limit)
|
||||
await self._tick_engine(session, outcome, limit=limit)
|
||||
await self._tick_engine(session, outcome)
|
||||
|
||||
return outcome
|
||||
|
||||
@@ -116,7 +116,7 @@ class AiRuntimeWorker:
|
||||
message=f"کار «{current.title}» در حال اجراست (اجرا {run.id}).",
|
||||
)
|
||||
|
||||
async def _tick_engine(self, session, outcome: WorkerOutcome, *, limit: int) -> None:
|
||||
async def _tick_engine(self, session, outcome: WorkerOutcome) -> None:
|
||||
"""One scheduler tick: move every ready task one step.
|
||||
|
||||
A tick failure is recorded against the worker and the pass ends — the
|
||||
@@ -125,7 +125,10 @@ class AiRuntimeWorker:
|
||||
"""
|
||||
try:
|
||||
scheduler = build_scheduler(session)
|
||||
report = await scheduler.tick(limit=max(limit, 10))
|
||||
# A wide read: states that cannot run this pass (automation off, or nobody free
|
||||
# for their stage) must not fill the window and starve the other organizations.
|
||||
# Scheduler._select still bounds the real work per organization and per pass.
|
||||
report = await scheduler.tick(limit=500)
|
||||
except Exception as exc: # noqa: BLE001 - a worker must survive a bad tick
|
||||
outcome.failed += 1
|
||||
outcome.note(f"engine tick: {type(exc).__name__}: {exc}")
|
||||
|
||||
@@ -9,6 +9,10 @@ background job's.
|
||||
The one hard requirement is **idempotency**. A cron job that runs twice for the
|
||||
same period must not bill twice, so a period that already has an invoice is
|
||||
skipped rather than re-created.
|
||||
|
||||
The same pass also expires memory lessons past their date (``expire_memories``).
|
||||
That is housekeeping, not billing, but this is the one scheduled job every
|
||||
deployment runs, and expiry needs no model provider.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -20,6 +24,7 @@ from app.domain.billing.models import Invoice, InvoiceStatus
|
||||
from app.infrastructure.repositories.audit import UsageLedgerRepository
|
||||
from app.infrastructure.repositories.billing import InvoiceRepository
|
||||
from app.infrastructure.repositories.identity import OrganizationRepository
|
||||
from app.infrastructure.repositories.memory import MemoryRepository
|
||||
from app.workers.base import WorkerOutcome, worker_session
|
||||
|
||||
#: A period shorter than this is almost certainly a bug (a stray manual run),
|
||||
@@ -99,6 +104,20 @@ class BillingWorker:
|
||||
|
||||
return outcome
|
||||
|
||||
async def expire_memories(self) -> WorkerOutcome:
|
||||
"""Soft-delete organization lessons whose expiry has passed.
|
||||
|
||||
Pinned entries never expire, and rules and preferences carry no expiry at
|
||||
all, so only disposable lessons are touched. The sweep covers every
|
||||
organization at once, which is what a platform-wide schedule wants.
|
||||
"""
|
||||
outcome = WorkerOutcome(name=self.name)
|
||||
async with worker_session() as session:
|
||||
expired = await MemoryRepository(session).expire(None)
|
||||
outcome.processed = expired
|
||||
outcome.note(f"memory: {expired} expired lesson(s) soft-deleted")
|
||||
return outcome
|
||||
|
||||
@staticmethod
|
||||
def _naive_utc(value: datetime) -> datetime:
|
||||
"""Normalise a timestamp to naive UTC for comparison.
|
||||
|
||||
Reference in New Issue
Block a user