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
@@ -31,6 +31,7 @@ from app.application.engine.runner import TurnRequest, TurnRunner
|
||||
from app.application.engine.state import advance, park_dependents
|
||||
from app.application.ports.delivery_hooks import DeliveryHooks, NullDeliveryHooks
|
||||
from app.application.ports.memory import MemoryPort, NullMemory, TaskOutcome
|
||||
from app.application.ports.model_grants import OrgPolicyView
|
||||
from app.application.ports.repo_workspace import RepoWorkspacePort, WorktreeRef
|
||||
from app.application.ports.sandbox_provider import SandboxProviderPort
|
||||
from app.core.config import settings
|
||||
@@ -165,6 +166,11 @@ class Scheduler:
|
||||
turn_states = await self.states.list_at_stages(TURN_STAGES, limit=limit)
|
||||
merge_states = await self.states.list_at_stages(MERGE_STAGES, limit=limit)
|
||||
|
||||
# One policy read per organization for the whole tick, shared by both lists.
|
||||
policies: dict[str, OrgPolicyView] = {}
|
||||
turn_states = await self._drop_switched_off(turn_states, policies, report)
|
||||
merge_states = await self._drop_switched_off(merge_states, policies, report)
|
||||
|
||||
selected = self._select(turn_states)
|
||||
report.selected = len(selected)
|
||||
for state in selected:
|
||||
@@ -174,6 +180,30 @@ class Scheduler:
|
||||
await self._guarded(self._merge_step, state, report)
|
||||
return report
|
||||
|
||||
async def _drop_switched_off(
|
||||
self,
|
||||
states: list[TaskPipelineState],
|
||||
policies: dict[str, OrgPolicyView],
|
||||
report: TickReport,
|
||||
) -> list[TaskPipelineState]:
|
||||
"""Keep only the states whose organization has automation switched on.
|
||||
|
||||
The owner's switch stops everything up to the merge. A skipped task keeps
|
||||
its stage, so switching automation back on resumes it where it was.
|
||||
"""
|
||||
kept: list[TaskPipelineState] = []
|
||||
for state in states:
|
||||
if state.organization_id not in policies:
|
||||
policies[state.organization_id] = await self.manager.grants.get_org_policy(
|
||||
state.organization_id
|
||||
)
|
||||
if not policies[state.organization_id].automation_enabled:
|
||||
report.skipped += 1
|
||||
report.note(f"task {state.task_id}: اتوماسیون این سازمان خاموش است")
|
||||
continue
|
||||
kept.append(state)
|
||||
return kept
|
||||
|
||||
async def _guarded(self, step, state: TaskPipelineState, report: TickReport) -> None:
|
||||
"""One task must never stop the tick: catch, record, continue."""
|
||||
try:
|
||||
@@ -590,16 +620,32 @@ class Scheduler:
|
||||
await self._post_note(task, reason)
|
||||
|
||||
async def _budget_reason(self, task: Task) -> str | None:
|
||||
"""Why this task may not spend a turn now, or ``None`` when it may.
|
||||
|
||||
Usage is post-paid: the ledger feeds invoices and nothing debits a wallet
|
||||
from usage. A zero balance is therefore the normal starting state of every
|
||||
org, not "out of money", so only an overdrawn wallet (below zero) pauses
|
||||
work. The owner's spend lever is the org's monthly credit budget.
|
||||
"""
|
||||
if self.wallets is not None:
|
||||
wallet = await self.wallets.get_for_org(task.organization_id)
|
||||
# No wallet means no budget was ever set: a fresh org must not be
|
||||
# frozen out of its first task.
|
||||
if wallet is not None and wallet.balance <= 0:
|
||||
# frozen out of its first task. A zero balance is not a pause either.
|
||||
if wallet is not None and wallet.balance < 0:
|
||||
return BUDGET_PAUSED_MESSAGE
|
||||
if self.spend is not None and settings.engine_max_task_credits > 0:
|
||||
used = await self.spend.spend_for_task(task.id)
|
||||
if used >= settings.engine_max_task_credits:
|
||||
return "سقف اعتبار این کار پر شده است؛ کار متوقف شد."
|
||||
if self.spend is not None:
|
||||
policy = await self.manager.grants.get_org_policy(task.organization_id)
|
||||
budget = policy.monthly_credit_budget
|
||||
if budget is not None:
|
||||
# The month is counted in UTC, the clock the ledger stamps rows with.
|
||||
month_start = utcnow().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
spent = await self.spend.spend_for_org_since(task.organization_id, month_start)
|
||||
if spent >= budget:
|
||||
return "سقف اعتبار ماهانهٔ سازمان پر شده است؛ کار متوقف شد."
|
||||
return None
|
||||
|
||||
async def _retry_or_fail(
|
||||
|
||||
Reference in New Issue
Block a user