alter-runtime 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alter_runtime/__init__.py +11 -0
- alter_runtime/adapters/__init__.py +19 -0
- alter_runtime/adapters/claude_jsonl_watcher.py +545 -0
- alter_runtime/adapters/git_watcher.py +457 -0
- alter_runtime/adapters/household/__init__.py +29 -0
- alter_runtime/adapters/household/_base.py +138 -0
- alter_runtime/adapters/household/compost/__init__.py +17 -0
- alter_runtime/adapters/household/compost/adapter.py +81 -0
- alter_runtime/adapters/household/compost/storage.py +75 -0
- alter_runtime/adapters/household/compost/tests/__init__.py +0 -0
- alter_runtime/adapters/household/compost/tests/test_adapter.py +62 -0
- alter_runtime/adapters/household/compost/tests/test_storage.py +23 -0
- alter_runtime/adapters/household/compost/tests/test_traits.py +38 -0
- alter_runtime/adapters/household/compost/traits.py +79 -0
- alter_runtime/adapters/household/self_hoster/__init__.py +30 -0
- alter_runtime/adapters/household/self_hoster/adapter.py +248 -0
- alter_runtime/adapters/household/self_hoster/storage.py +83 -0
- alter_runtime/adapters/household/self_hoster/tests/__init__.py +0 -0
- alter_runtime/adapters/household/self_hoster/tests/test_adapter.py +216 -0
- alter_runtime/adapters/household/self_hoster/tests/test_storage.py +25 -0
- alter_runtime/adapters/household/self_hoster/tests/test_traits.py +55 -0
- alter_runtime/adapters/household/self_hoster/traits.py +105 -0
- alter_runtime/adapters/household/tapo_ecosystem/__init__.py +22 -0
- alter_runtime/adapters/household/tapo_ecosystem/adapter.py +98 -0
- alter_runtime/adapters/household/tapo_ecosystem/storage.py +95 -0
- alter_runtime/adapters/household/tapo_ecosystem/tests/__init__.py +0 -0
- alter_runtime/adapters/household/tapo_ecosystem/tests/test_adapter.py +55 -0
- alter_runtime/adapters/household/tapo_ecosystem/tests/test_storage.py +28 -0
- alter_runtime/adapters/household/tapo_ecosystem/tests/test_traits.py +45 -0
- alter_runtime/adapters/household/tapo_ecosystem/traits.py +97 -0
- alter_runtime/adapters/household/workshop_tools/__init__.py +25 -0
- alter_runtime/adapters/household/workshop_tools/adapter.py +77 -0
- alter_runtime/adapters/household/workshop_tools/storage.py +92 -0
- alter_runtime/adapters/household/workshop_tools/tests/__init__.py +0 -0
- alter_runtime/adapters/household/workshop_tools/tests/test_adapter.py +48 -0
- alter_runtime/adapters/household/workshop_tools/tests/test_storage.py +26 -0
- alter_runtime/adapters/household/workshop_tools/tests/test_traits.py +45 -0
- alter_runtime/adapters/household/workshop_tools/traits.py +95 -0
- alter_runtime/adapters/worktree_watcher.py +378 -0
- alter_runtime/atlas/__init__.py +48 -0
- alter_runtime/atlas/base.py +102 -0
- alter_runtime/atlas/ledger.py +196 -0
- alter_runtime/atlas/observations.py +136 -0
- alter_runtime/atlas/schema.py +106 -0
- alter_runtime/cap_cache.py +392 -0
- alter_runtime/cli.py +517 -0
- alter_runtime/clients/__init__.py +0 -0
- alter_runtime/clients/token_usage_client.py +273 -0
- alter_runtime/config.py +648 -0
- alter_runtime/consent.py +425 -0
- alter_runtime/daemon.py +518 -0
- alter_runtime/floor_loop.py +335 -0
- alter_runtime/floor_preflight.py +734 -0
- alter_runtime/http_auth.py +173 -0
- alter_runtime/notifiers/__init__.py +18 -0
- alter_runtime/notifiers/desktop.py +321 -0
- alter_runtime/sdk/__init__.py +12 -0
- alter_runtime/sdk/client.py +399 -0
- alter_runtime/service_install.py +616 -0
- alter_runtime/services/__init__.py +59 -0
- alter_runtime/services/launchd/com.alter.runtime.plist.in +90 -0
- alter_runtime/services/systemd/alter-runtime.service.in +74 -0
- alter_runtime/services/systemd/cf-access-env.conf.in +29 -0
- alter_runtime/sockets/__init__.py +20 -0
- alter_runtime/sockets/dbus.py +272 -0
- alter_runtime/sockets/unix.py +702 -0
- alter_runtime/subscribers/__init__.py +58 -0
- alter_runtime/subscribers/active_sessions_cron_emitter.py +313 -0
- alter_runtime/subscribers/active_sessions_do_publisher.py +1159 -0
- alter_runtime/subscribers/active_sessions_gc.py +432 -0
- alter_runtime/subscribers/active_sessions_writer.py +446 -0
- alter_runtime/subscribers/adapters_writer.py +415 -0
- alter_runtime/subscribers/agent_frames.py +461 -0
- alter_runtime/subscribers/bus.py +188 -0
- alter_runtime/subscribers/cache_writer.py +347 -0
- alter_runtime/subscribers/ceremony_echo.py +290 -0
- alter_runtime/subscribers/do_sse.py +864 -0
- alter_runtime/subscribers/ebpf.py +506 -0
- alter_runtime/subscribers/inbox_writer.py +469 -0
- alter_runtime/subscribers/mcp_fallback.py +391 -0
- alter_runtime/subscribers/presence_writer.py +426 -0
- alter_runtime/subscribers/session_presence.py +467 -0
- alter_runtime/subscribers/sse.py +125 -0
- alter_runtime/subscribers/weave_intent_writer.py +608 -0
- alter_runtime/update_loop.py +519 -0
- alter_runtime/weave/__init__.py +21 -0
- alter_runtime/weave/resolver.py +544 -0
- alter_runtime-0.3.0.dist-info/METADATA +289 -0
- alter_runtime-0.3.0.dist-info/RECORD +92 -0
- alter_runtime-0.3.0.dist-info/WHEEL +4 -0
- alter_runtime-0.3.0.dist-info/entry_points.txt +2 -0
- alter_runtime-0.3.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""FloorLoop — supervised daemon-side floor preflight component.
|
|
2
|
+
|
|
3
|
+
D-MIN-VERSION-FLOOR-1 Phase 3. Sister to ``UpdateLoop``: the update loop
|
|
4
|
+
keeps the daemon fresh by polling the release manifest; the floor loop
|
|
5
|
+
keeps the daemon honest by polling the server-side floor and gating the
|
|
6
|
+
local RPC surface when the running version slips below it.
|
|
7
|
+
|
|
8
|
+
**Cadence (per DR §6).**
|
|
9
|
+
|
|
10
|
+
* Immediate preflight on daemon start.
|
|
11
|
+
* Every ``poll_interval_seconds`` afterwards (default 24h, ±5% jitter for
|
|
12
|
+
fingerprint hygiene — same window as ``UpdateLoop``).
|
|
13
|
+
|
|
14
|
+
**State surface.**
|
|
15
|
+
|
|
16
|
+
The loop holds a single :class:`FloorState` shared with the Unix-socket
|
|
17
|
+
server. The state carries:
|
|
18
|
+
|
|
19
|
+
* The most recent :class:`FloorVerdict` (or ``None`` until the first poll
|
|
20
|
+
completes — initial state is "permit, unknown" so the daemon never blocks
|
|
21
|
+
during cold start).
|
|
22
|
+
* A monotonic counter of successful polls (used by tests + telemetry).
|
|
23
|
+
* A monotonic counter of failed polls.
|
|
24
|
+
|
|
25
|
+
The socket server reads ``state.is_locked()`` on every authenticated method.
|
|
26
|
+
Locked means "the verdict is below floor"; permit-with-warn or unknown
|
|
27
|
+
state never locks.
|
|
28
|
+
|
|
29
|
+
**A15.3 carve-out.**
|
|
30
|
+
|
|
31
|
+
The state object exposes :meth:`allow_safety_critical` so the socket
|
|
32
|
+
server can let through ``ingest`` calls whose payload carries
|
|
33
|
+
``urgency: "critical"``. The carve-out is enforced at the socket layer,
|
|
34
|
+
not here, but lives close to the state object so the policy is single-
|
|
35
|
+
sourced.
|
|
36
|
+
|
|
37
|
+
**No new dependencies.** ``httpx``, ``asyncio``, and stdlib are sufficient.
|
|
38
|
+
The floor preflight module owns all I/O.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import asyncio
|
|
44
|
+
import logging
|
|
45
|
+
import random
|
|
46
|
+
from dataclasses import dataclass
|
|
47
|
+
from typing import Any
|
|
48
|
+
|
|
49
|
+
from alter_runtime import __version__ as RUNTIME_VERSION
|
|
50
|
+
from alter_runtime.config import DaemonConfig
|
|
51
|
+
from alter_runtime.daemon import Component
|
|
52
|
+
from alter_runtime.floor_preflight import (
|
|
53
|
+
FloorVerdict,
|
|
54
|
+
preflight,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"FloorLoop",
|
|
59
|
+
"FloorState",
|
|
60
|
+
"SAFETY_CRITICAL_INGEST_KINDS",
|
|
61
|
+
"SAFETY_CRITICAL_INGEST_URGENCIES",
|
|
62
|
+
"is_safety_critical_call",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
logger = logging.getLogger("alter_runtime.floor_loop")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# A15.3 carve-out — safety-critical comms allowlist
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
#
|
|
73
|
+
# DR §6 + wave-2 amendment A15.3: MCP server preflight MUST permit
|
|
74
|
+
# ``alter_ingest`` and ``alter_escalations`` calls when the payload carries
|
|
75
|
+
# ``urgency: critical``, even below floor. Floor MUST NOT block emergency
|
|
76
|
+
# comms.
|
|
77
|
+
#
|
|
78
|
+
# The local Unix-socket surface speaks JSON-RPC, not MCP. The closest
|
|
79
|
+
# substrate-honest carry-over is:
|
|
80
|
+
#
|
|
81
|
+
# * ``ingest`` method calls whose payload includes ``urgency: critical`` or
|
|
82
|
+
# ``urgency: emergency`` (both treated as critical-class to widen the
|
|
83
|
+
# safety net).
|
|
84
|
+
# * ``escalate`` / ``escalation`` ingest kinds — pre-allowed regardless of
|
|
85
|
+
# urgency tag, because the kind name is itself the critical signal.
|
|
86
|
+
#
|
|
87
|
+
# This is the daemon-local extension of A15.3, not a re-interpretation. If
|
|
88
|
+
# a new RPC-layer escalation verb lands in a follow-up phase, the
|
|
89
|
+
# allowlist below is the single edit point — keep the carve-out in sync
|
|
90
|
+
# with whatever the public MCP servers expose.
|
|
91
|
+
|
|
92
|
+
#: Ingest kinds that bypass the floor gate regardless of urgency tag.
|
|
93
|
+
#: Empty in v1 — the runtime's local ingest kind whitelist is consent-
|
|
94
|
+
#: scoped and doesn't expose an escalation verb. Reserved for forward
|
|
95
|
+
#: compatibility with mcp-alter / mcp-alter-collective once their floor
|
|
96
|
+
#: preflight lands.
|
|
97
|
+
SAFETY_CRITICAL_INGEST_KINDS: frozenset[str] = frozenset()
|
|
98
|
+
|
|
99
|
+
#: Urgency tags that promote an ``ingest`` payload to safety-critical class.
|
|
100
|
+
SAFETY_CRITICAL_INGEST_URGENCIES: frozenset[str] = frozenset({"critical", "emergency"})
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def is_safety_critical_call(method: str, payload: dict[str, Any] | None) -> bool:
|
|
104
|
+
"""Return ``True`` when the local RPC call MUST bypass the floor gate.
|
|
105
|
+
|
|
106
|
+
Applied at the Unix-socket dispatch layer after the auth handshake. The
|
|
107
|
+
function is permissive by design — the cost of an unnecessary permit on
|
|
108
|
+
a safety-critical-looking call is far lower than the cost of blocking a
|
|
109
|
+
real emergency revoke / panic / kill-switch payload.
|
|
110
|
+
|
|
111
|
+
Method-level allowlist:
|
|
112
|
+
|
|
113
|
+
* ``ping``, ``auth`` — always permitted (pre-auth handshake, not gated).
|
|
114
|
+
* ``ingest`` with ``payload['urgency']`` in
|
|
115
|
+
:data:`SAFETY_CRITICAL_INGEST_URGENCIES` — promoted to safety-critical.
|
|
116
|
+
* ``ingest`` with ``kind`` in :data:`SAFETY_CRITICAL_INGEST_KINDS` —
|
|
117
|
+
promoted regardless of payload.
|
|
118
|
+
"""
|
|
119
|
+
if method in ("ping", "auth"):
|
|
120
|
+
return True
|
|
121
|
+
if method != "ingest":
|
|
122
|
+
return False
|
|
123
|
+
if not isinstance(payload, dict):
|
|
124
|
+
return False
|
|
125
|
+
kind = payload.get("kind")
|
|
126
|
+
if isinstance(kind, str) and kind in SAFETY_CRITICAL_INGEST_KINDS:
|
|
127
|
+
return True
|
|
128
|
+
# Check both top-level urgency (on the request) and nested urgency
|
|
129
|
+
# (inside the ingest payload dict). Either signal flips the carve-out.
|
|
130
|
+
top_urgency = payload.get("urgency")
|
|
131
|
+
if isinstance(top_urgency, str) and top_urgency.lower() in SAFETY_CRITICAL_INGEST_URGENCIES:
|
|
132
|
+
return True
|
|
133
|
+
nested = payload.get("payload")
|
|
134
|
+
if isinstance(nested, dict):
|
|
135
|
+
nested_urgency = nested.get("urgency")
|
|
136
|
+
if (
|
|
137
|
+
isinstance(nested_urgency, str)
|
|
138
|
+
and nested_urgency.lower() in SAFETY_CRITICAL_INGEST_URGENCIES
|
|
139
|
+
):
|
|
140
|
+
return True
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# FloorState
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass
|
|
150
|
+
class FloorState:
|
|
151
|
+
"""Shared mutable state for the floor gate.
|
|
152
|
+
|
|
153
|
+
The :class:`FloorLoop` writes; the :class:`UnixSocketServer` reads. No
|
|
154
|
+
lock is needed because asyncio is single-threaded — the read is atomic
|
|
155
|
+
relative to the write (Python dataclass attribute assignment is a single
|
|
156
|
+
bytecode op for the reference-store case).
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
last_verdict: FloorVerdict | None = None
|
|
160
|
+
successful_polls: int = 0
|
|
161
|
+
failed_polls: int = 0
|
|
162
|
+
#: Set to ``True`` after the first successful preflight. Until then the
|
|
163
|
+
#: socket server treats the state as "unknown → permit" so cold-start
|
|
164
|
+
#: never blocks.
|
|
165
|
+
has_polled: bool = False
|
|
166
|
+
|
|
167
|
+
def is_locked(self) -> bool:
|
|
168
|
+
"""Return ``True`` when the daemon is below floor.
|
|
169
|
+
|
|
170
|
+
``False`` covers both "above floor" and "unknown / no doc yet" — the
|
|
171
|
+
server-side gate is load-bearing for unknown cases, so the daemon
|
|
172
|
+
does not block locally without a verdict.
|
|
173
|
+
"""
|
|
174
|
+
if self.last_verdict is None:
|
|
175
|
+
return False
|
|
176
|
+
return not self.last_verdict.ok
|
|
177
|
+
|
|
178
|
+
def envelope_payload(self) -> dict[str, Any] | None:
|
|
179
|
+
"""Return the canonical envelope dict for the current state.
|
|
180
|
+
|
|
181
|
+
Returns ``None`` when not locked. The dict is byte-shape-equal to
|
|
182
|
+
the Phase 1 server reject body and the Phase 2 CLI envelope.
|
|
183
|
+
"""
|
|
184
|
+
if not self.is_locked() or self.last_verdict is None:
|
|
185
|
+
return None
|
|
186
|
+
envelope = self.last_verdict.envelope
|
|
187
|
+
if envelope is None:
|
|
188
|
+
return None
|
|
189
|
+
return envelope.to_dict()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# FloorLoop component
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass
|
|
198
|
+
class _LoopConfig:
|
|
199
|
+
"""Internal config snapshot — keeps the run loop pure."""
|
|
200
|
+
|
|
201
|
+
api_base: str
|
|
202
|
+
poll_interval_seconds: float
|
|
203
|
+
initial_jitter_seconds: float = 0.0
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class FloorLoop(Component):
|
|
207
|
+
"""Supervised component that drives :class:`FloorState`.
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
config:
|
|
212
|
+
Daemon config — only ``mcp_fallback_endpoint`` is consulted. The
|
|
213
|
+
floor lives at ``/api/v1/clients/min-version`` on the same host.
|
|
214
|
+
state:
|
|
215
|
+
Shared :class:`FloorState` written by the loop and read by the
|
|
216
|
+
socket server.
|
|
217
|
+
poll_interval_seconds:
|
|
218
|
+
Override for the 24h default. Tests inject a tiny value to
|
|
219
|
+
exercise the multi-poll path.
|
|
220
|
+
api_base:
|
|
221
|
+
Override the API base derived from ``config``. Tests inject a
|
|
222
|
+
``httpx.MockTransport`` URL.
|
|
223
|
+
rng:
|
|
224
|
+
Optional ``random.Random`` for deterministic jitter in tests.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
name = "floor_loop"
|
|
228
|
+
|
|
229
|
+
#: Default 24h cadence — same value as ``UpdateLoop``. Matches DR §6.
|
|
230
|
+
DEFAULT_POLL_INTERVAL_SECONDS: float = 24 * 60 * 60
|
|
231
|
+
|
|
232
|
+
#: Jitter window (±5 %) on the poll interval. Same magnitude as
|
|
233
|
+
#: ``UpdateLoop`` so the two periodic outbound calls stay loosely
|
|
234
|
+
#: phased relative to each other.
|
|
235
|
+
JITTER_FRACTION: float = 0.05
|
|
236
|
+
|
|
237
|
+
def __init__(
|
|
238
|
+
self,
|
|
239
|
+
config: DaemonConfig,
|
|
240
|
+
state: FloorState,
|
|
241
|
+
*,
|
|
242
|
+
poll_interval_seconds: float | None = None,
|
|
243
|
+
api_base: str | None = None,
|
|
244
|
+
rng: random.Random | None = None,
|
|
245
|
+
) -> None:
|
|
246
|
+
self._state = state
|
|
247
|
+
self._rng = rng or random.Random()
|
|
248
|
+
interval = (
|
|
249
|
+
poll_interval_seconds
|
|
250
|
+
if poll_interval_seconds is not None
|
|
251
|
+
else self.DEFAULT_POLL_INTERVAL_SECONDS
|
|
252
|
+
)
|
|
253
|
+
base = api_base if api_base is not None else self._derive_api_base(config)
|
|
254
|
+
self._cfg = _LoopConfig(
|
|
255
|
+
api_base=base,
|
|
256
|
+
poll_interval_seconds=float(interval),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def _derive_api_base(config: DaemonConfig) -> str:
|
|
261
|
+
"""Derive the API base from ``mcp_fallback_endpoint``.
|
|
262
|
+
|
|
263
|
+
``mcp_fallback_endpoint`` is ``https://api.truealter.com/api/v1/mcp``
|
|
264
|
+
by default. We strip the ``/api/v1/mcp`` suffix to recover the
|
|
265
|
+
bare API base. If the endpoint shape changes the floor preflight
|
|
266
|
+
URL still resolves cleanly through
|
|
267
|
+
:func:`floor_preflight._resolve_floor_url`.
|
|
268
|
+
"""
|
|
269
|
+
endpoint = (config.mcp_fallback_endpoint or "").rstrip("/")
|
|
270
|
+
for tail in ("/api/v1/mcp", "/api/v1", "/v1/mcp", "/v1"):
|
|
271
|
+
if endpoint.endswith(tail):
|
|
272
|
+
return endpoint[: -len(tail)]
|
|
273
|
+
return endpoint or "https://api.truealter.com"
|
|
274
|
+
|
|
275
|
+
async def run(self) -> None:
|
|
276
|
+
"""Run the poll loop until cancelled."""
|
|
277
|
+
logger.info(
|
|
278
|
+
"floor_loop starting interval=%.0fs api_base=%s",
|
|
279
|
+
self._cfg.poll_interval_seconds,
|
|
280
|
+
self._cfg.api_base,
|
|
281
|
+
)
|
|
282
|
+
await self._tick()
|
|
283
|
+
while True:
|
|
284
|
+
wait = self._next_wait()
|
|
285
|
+
try:
|
|
286
|
+
await asyncio.sleep(wait)
|
|
287
|
+
except asyncio.CancelledError:
|
|
288
|
+
raise
|
|
289
|
+
await self._tick()
|
|
290
|
+
|
|
291
|
+
def _next_wait(self) -> float:
|
|
292
|
+
"""Compute the next sleep duration with ±5 % jitter."""
|
|
293
|
+
base = self._cfg.poll_interval_seconds
|
|
294
|
+
if base <= 0:
|
|
295
|
+
return 0.0
|
|
296
|
+
delta = base * self.JITTER_FRACTION
|
|
297
|
+
# rng.uniform(-delta, +delta) yields the jitter offset; clamp to
|
|
298
|
+
# avoid a non-positive sleep.
|
|
299
|
+
jitter = self._rng.uniform(-delta, delta)
|
|
300
|
+
return max(0.0, base + jitter)
|
|
301
|
+
|
|
302
|
+
async def _tick(self) -> None:
|
|
303
|
+
"""One preflight pass. Updates ``state`` regardless of outcome."""
|
|
304
|
+
try:
|
|
305
|
+
verdict = await preflight(
|
|
306
|
+
api_base=self._cfg.api_base,
|
|
307
|
+
daemon_version=RUNTIME_VERSION,
|
|
308
|
+
)
|
|
309
|
+
except Exception as exc: # pragma: no cover — preflight catches its own
|
|
310
|
+
logger.warning("floor_loop_preflight_unhandled err=%s", exc)
|
|
311
|
+
self._state.failed_polls += 1
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
self._state.last_verdict = verdict
|
|
315
|
+
self._state.has_polled = True
|
|
316
|
+
if verdict.envelope is not None:
|
|
317
|
+
logger.warning(
|
|
318
|
+
"floor_loop verdict=below-floor version=%s min_version=%s upgrade=%s",
|
|
319
|
+
verdict.envelope.client_version,
|
|
320
|
+
verdict.envelope.min_version,
|
|
321
|
+
verdict.envelope.upgrade_cmd,
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
logger.info(
|
|
325
|
+
"floor_loop verdict=ok diagnostic=%s warn=%s",
|
|
326
|
+
verdict.diagnostic,
|
|
327
|
+
verdict.warn or "",
|
|
328
|
+
)
|
|
329
|
+
if verdict.diagnostic in (
|
|
330
|
+
"no-cache-no-fetch-permit",
|
|
331
|
+
"stale-cache-permit-warn",
|
|
332
|
+
):
|
|
333
|
+
self._state.failed_polls += 1
|
|
334
|
+
else:
|
|
335
|
+
self._state.successful_polls += 1
|