# Executor 从任务工作区读取绑定主 session 的累计分析金额。 from __future__ import annotations import math import sqlite3 from contextlib import closing from dataclasses import dataclass from datetime import datetime, timezone from decimal import Decimal, InvalidOperation from pathlib import Path class AnalysisUsageError(RuntimeError): """保留稳定错误码及可定位读取阶段的原因。""" code = "agent_analysis_usage_unavailable" def __init__(self, reason: str): """接收不含业务数据的原因,用于 Executor 终止诊断。""" self.reason = reason super().__init__(reason) @dataclass(frozen=True) class AnalysisUsage: """一次主 session 只读快照,包含美元累计值及 UTC 检查时间。""" observed_cost: Decimal checked_at: str def _validated_usage(*, observed_cost: object, checked_at: object, minimum_cost: Decimal) -> AnalysisUsage: """校验公共金额快照,拒绝无效金额、无时区时间和金额倒退。""" if not isinstance(observed_cost, (str, int, float)) or isinstance(observed_cost, bool): raise AnalysisUsageError("session_cost_invalid") try: cost = Decimal(str(observed_cost)) except (InvalidOperation, ValueError) as exc: raise AnalysisUsageError("session_cost_invalid") from exc if not cost.is_finite() or cost < 0: raise AnalysisUsageError("session_cost_invalid") if cost < minimum_cost: raise AnalysisUsageError("session_cost_decreased") if not isinstance(checked_at, str): raise AnalysisUsageError("usage_checked_at_invalid") try: parsed = datetime.fromisoformat(checked_at.replace("Z", "+00:00")) except ValueError as exc: raise AnalysisUsageError("usage_checked_at_invalid") from exc if parsed.tzinfo is None: raise AnalysisUsageError("usage_checked_at_invalid") return AnalysisUsage(cost, parsed.astimezone(timezone.utc).isoformat()) def read_analysis_usage( database: Path, *, session_id: str, directory: Path, minimum_cost: Decimal = Decimal(0), ) -> AnalysisUsage: """从任务 OPENCODE_DB 读取已绑定主 session;缺失、冲突及金额减少立即报错。""" if not session_id: raise AnalysisUsageError("session_binding_missing") if not database.is_file(): raise AnalysisUsageError("database_missing") try: with closing(sqlite3.connect(database.absolute().as_uri() + "?mode=ro", uri=True, timeout=0.25)) as connection: connection.execute("PRAGMA query_only = ON") columns = {row[1] for row in connection.execute("PRAGMA table_info(session)")} if not {"id", "parent_id", "directory", "cost"}.issubset(columns): raise AnalysisUsageError("session_schema_incompatible") row = connection.execute( "SELECT parent_id, directory, cost FROM session WHERE id = ?", (session_id,), ).fetchone() except sqlite3.Error as exc: reason = getattr(exc, "sqlite_errorname", type(exc).__name__) raise AnalysisUsageError(f"database_read_error:{reason}") from exc if row is None: raise AnalysisUsageError("session_missing") if row[0] is not None or row[1] != str(directory): raise AnalysisUsageError("session_binding_mismatch") value = row[2] if not isinstance(value, (int, float)) or not math.isfinite(value) or value < 0: raise AnalysisUsageError("session_cost_invalid") return _validated_usage( observed_cost=value, checked_at=datetime.now(timezone.utc).isoformat(), minimum_cost=minimum_cost, )