pairling 0.0.1 → 0.1.0

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.
Files changed (61) hide show
  1. package/package.json +5 -1
  2. package/payload/mac/SOURCE_BRANCH +1 -0
  3. package/payload/mac/SOURCE_DIRTY +1 -0
  4. package/payload/mac/SOURCE_REVISION +1 -0
  5. package/payload/mac/VERSION +1 -0
  6. package/payload/mac/companiond/integrations/__init__.py +1 -0
  7. package/payload/mac/companiond/integrations/aperture_cli/__init__.py +23 -0
  8. package/payload/mac/companiond/integrations/aperture_cli/launch.py +456 -0
  9. package/payload/mac/companiond/integrations/aperture_cli/status.py +393 -0
  10. package/payload/mac/companiond/live_activity_publisher.py +380 -0
  11. package/payload/mac/companiond/llm_route.py +108 -0
  12. package/payload/mac/companiond/local_mcp_bridge.py +156 -0
  13. package/payload/mac/companiond/model_status_contract.py +101 -0
  14. package/payload/mac/companiond/pairdrop_store.py +920 -0
  15. package/payload/mac/companiond/pairling_connectd_status.py +149 -0
  16. package/payload/mac/companiond/pairling_devices.py +459 -0
  17. package/payload/mac/companiond/pairling_pairing.py +404 -0
  18. package/payload/mac/companiond/pairling_relay_claims.py +232 -0
  19. package/payload/mac/companiond/pairling_tools.py +706 -0
  20. package/payload/mac/companiond/pairlingd.py +18438 -0
  21. package/payload/mac/companiond/providers/__init__.py +1 -0
  22. package/payload/mac/companiond/providers/base.py +255 -0
  23. package/payload/mac/companiond/providers/claude.py +127 -0
  24. package/payload/mac/companiond/providers/codex.py +124 -0
  25. package/payload/mac/companiond/providers/external.py +46 -0
  26. package/payload/mac/companiond/providers/registry.py +70 -0
  27. package/payload/mac/companiond/pty_broker.py +887 -0
  28. package/payload/mac/companiond/push_dispatcher.py +1990 -0
  29. package/payload/mac/companiond/push_event_catalog.py +566 -0
  30. package/payload/mac/companiond/request_proof.py +142 -0
  31. package/payload/mac/companiond/runtime_contract.py +47 -0
  32. package/payload/mac/companiond/runtime_manifest.py +197 -0
  33. package/payload/mac/companiond/runtime_paths.py +87 -0
  34. package/payload/mac/companiond/safety_monitor.py +542 -0
  35. package/payload/mac/companiond/sentinel_notifications.py +491 -0
  36. package/payload/mac/companiond/standard_push_publisher.py +516 -0
  37. package/payload/mac/companiond/substrate_status_contract.py +139 -0
  38. package/payload/mac/companiond/terminal_screen_backend.py +332 -0
  39. package/payload/mac/companiond/terminal_text_sanitizer.py +54 -0
  40. package/payload/mac/companiond/workstate_feed_contract.py +108 -0
  41. package/payload/mac/connectd/cmd/pairling-connectd/auth_open_test.go +116 -0
  42. package/payload/mac/connectd/cmd/pairling-connectd/main.go +345 -0
  43. package/payload/mac/connectd/cmd/pairling-connectd/upstream_health_test.go +33 -0
  44. package/payload/mac/connectd/go.mod +51 -0
  45. package/payload/mac/connectd/go.sum +229 -0
  46. package/payload/mac/connectd/internal/gateway/proxy.go +597 -0
  47. package/payload/mac/connectd/internal/gateway/proxy_test.go +531 -0
  48. package/payload/mac/connectd/internal/runtime/config.go +99 -0
  49. package/payload/mac/connectd/internal/runtime/config_test.go +29 -0
  50. package/payload/mac/connectd/internal/status/status.go +300 -0
  51. package/payload/mac/connectd/internal/status/status_test.go +263 -0
  52. package/payload/mac/guardian/companion-power-guardian.py +613 -0
  53. package/payload/mac/guardian/guardian_contract.py +67 -0
  54. package/payload/mac/install/bootstrap-first-run.sh +206 -0
  55. package/payload/mac/install/doctor.sh +660 -0
  56. package/payload/mac/install/install-runtime.sh +1241 -0
  57. package/payload/mac/install/render-launchd.py +119 -0
  58. package/payload/mac/install/uninstall-runtime.sh +136 -0
  59. package/payload/mac/mcp/phone_tools.py +210 -0
  60. package/payload/mac/packaging/bin/pairling +63 -0
  61. package/payload-manifest.json +255 -0
@@ -0,0 +1,491 @@
1
+ #!/usr/bin/env python3
2
+ """Worker/token sentinel notification classifier and local event ledger.
3
+
4
+ This module is deliberately payload-small. It turns worker/token counters into
5
+ bounded notification events and never accepts transcript text, prompts, or raw
6
+ provider credentials as inputs.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import os
14
+ import stat
15
+ import time
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from push_event_catalog import build_push_event, standard_alert_payload
20
+
21
+
22
+ CONTRACT_VERSION = "pairling-sentinel-notifications-v1"
23
+
24
+ DEFAULT_PREFERENCES: dict[str, Any] = {
25
+ "enabled": True,
26
+ "push_enabled": True,
27
+ "resolutions_enabled": False,
28
+ "worker_warning_active": 12,
29
+ "worker_critical_active": 25,
30
+ "worker_watch_active": 8,
31
+ "worker_watch_total": 15,
32
+ "human_idle_warning_minutes": 15,
33
+ "human_idle_critical_minutes": 30,
34
+ "stale_worker_minutes": 60,
35
+ "stale_cleanup_count": 5,
36
+ "token_pressure_ratio": 0.70,
37
+ "cooldown_warning_minutes": 30,
38
+ "cooldown_critical_minutes": 60,
39
+ }
40
+
41
+ _INT_PREFS = {
42
+ "worker_warning_active",
43
+ "worker_critical_active",
44
+ "worker_watch_active",
45
+ "worker_watch_total",
46
+ "human_idle_warning_minutes",
47
+ "human_idle_critical_minutes",
48
+ "stale_worker_minutes",
49
+ "stale_cleanup_count",
50
+ "cooldown_warning_minutes",
51
+ "cooldown_critical_minutes",
52
+ }
53
+ _FLOAT_PREFS = {"token_pressure_ratio"}
54
+ _BOOL_PREFS = {"enabled", "push_enabled", "resolutions_enabled"}
55
+ _LEVEL_RANK = {"info": 0, "watch": 1, "stale_cleanup": 2, "token_pressure": 3, "warning": 4, "critical": 5}
56
+
57
+
58
+ class SentinelNotificationError(Exception):
59
+ def __init__(self, code: str, message: str, status: int = 400):
60
+ super().__init__(message)
61
+ self.code = code
62
+ self.message = message
63
+ self.status = status
64
+
65
+
66
+ class SentinelNotificationCenter:
67
+ def __init__(self, root: Path, *, now_fn=time.time, push_dispatcher=None) -> None:
68
+ self.root = Path(root)
69
+ self.dir = self.root / "sentinel"
70
+ self.preferences_path = self.dir / "preferences.json"
71
+ self.state_path = self.dir / "state.json"
72
+ self.events_path = self.dir / "events.jsonl"
73
+ self.now_fn = now_fn
74
+ self.push_dispatcher = push_dispatcher
75
+
76
+ def preferences(self) -> dict[str, Any]:
77
+ prefs = dict(DEFAULT_PREFERENCES)
78
+ try:
79
+ loaded = json.loads(self.preferences_path.read_text(encoding="utf-8"))
80
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
81
+ loaded = {}
82
+ if isinstance(loaded, dict):
83
+ for key in DEFAULT_PREFERENCES:
84
+ if key in loaded:
85
+ prefs[key] = self._normalize_pref(key, loaded[key])
86
+ return {
87
+ "ok": True,
88
+ "contract_version": CONTRACT_VERSION,
89
+ "preferences": prefs,
90
+ "updated_at": self._state().get("updated_at"),
91
+ }
92
+
93
+ def update_preferences(self, payload: dict[str, Any]) -> dict[str, Any]:
94
+ if not isinstance(payload, dict):
95
+ raise SentinelNotificationError("invalid_preferences", "preferences body must be an object")
96
+ prefs = self.preferences()["preferences"]
97
+ unknown = [key for key in payload if key not in DEFAULT_PREFERENCES]
98
+ if unknown:
99
+ raise SentinelNotificationError("unknown_preference", "unknown sentinel preference: " + unknown[0])
100
+ for key, value in payload.items():
101
+ prefs[key] = self._normalize_pref(key, value)
102
+ self._write_json(self.preferences_path, prefs)
103
+ state = self._state()
104
+ state["updated_at"] = self.now_fn()
105
+ self._write_json(self.state_path, state)
106
+ return self.preferences()
107
+
108
+ def status(
109
+ self,
110
+ *,
111
+ worker_stats: dict[str, Any] | None = None,
112
+ token_sessions: list[dict[str, Any]] | None = None,
113
+ human_idle_minutes: float | None = None,
114
+ ) -> dict[str, Any]:
115
+ prefs = self.preferences()["preferences"]
116
+ classification = self.classify(
117
+ worker_stats or {},
118
+ token_sessions=token_sessions or [],
119
+ human_idle_minutes=human_idle_minutes,
120
+ prefs=prefs,
121
+ )
122
+ state = self._state()
123
+ return {
124
+ "ok": True,
125
+ "contract_version": CONTRACT_VERSION,
126
+ **classification,
127
+ "snoozed_until": self._snoozed_until(classification["dedupe_key"], state),
128
+ "last_push_event_id": state.get("last_push_event_id"),
129
+ "preferences": prefs,
130
+ }
131
+
132
+ def evaluate_now(
133
+ self,
134
+ *,
135
+ worker_stats: dict[str, Any],
136
+ token_sessions: list[dict[str, Any]] | None = None,
137
+ human_idle_minutes: float | None = None,
138
+ device_id: str | None = None,
139
+ force: bool = False,
140
+ ) -> dict[str, Any]:
141
+ status = self.status(
142
+ worker_stats=worker_stats,
143
+ token_sessions=token_sessions or [],
144
+ human_idle_minutes=human_idle_minutes,
145
+ )
146
+ prefs = status["preferences"]
147
+ state = self._state()
148
+ event = self._event_from_status(status, device_id=device_id)
149
+ suppressed = self._suppression_reason(status, prefs, state, force=force, device_id=device_id)
150
+ delivery = None
151
+ if suppressed is None and prefs.get("push_enabled") and self.push_dispatcher is not None and device_id:
152
+ try:
153
+ push_event = build_push_event(
154
+ kind="worker_pressure",
155
+ event_id=event["event_id"],
156
+ source="sentinel",
157
+ provider=str(worker_stats.get("provider") or "all"),
158
+ project=_project_scope(worker_stats),
159
+ observed_at=event["created_at"],
160
+ phase="attention",
161
+ required_action="Open workers to review pressure.",
162
+ risk_level=status["severity"],
163
+ risk_summary=status["body"],
164
+ worker_summary=status["summary"],
165
+ action_route=status["route"],
166
+ dedupe_key=status["dedupe_key"],
167
+ )
168
+ push_payload = standard_alert_payload(push_event)
169
+ push_payload.update({
170
+ "sentinel_event_id": event["event_id"],
171
+ "sentinel_level": event["level"],
172
+ "sentinel_key": event["dedupe_key"],
173
+ "worker_summary": push_event.worker_summary,
174
+ "risk_summary": push_event.risk_summary,
175
+ "required_action": push_event.required_action,
176
+ "phase": push_event.phase,
177
+ "collapse_id": push_event.collapse_id,
178
+ "dedupe_key": push_event.dedupe_key,
179
+ })
180
+ delivery = self.push_dispatcher.record_event(
181
+ device_id=device_id,
182
+ payload=push_payload,
183
+ )
184
+ event["sent"] = bool(delivery.get("ok"))
185
+ event["delivery_outcome"] = (delivery.get("delivery") or {}).get("outcome")
186
+ except Exception as exc: # keep classifier useful even if push is down.
187
+ event["sent"] = False
188
+ event["delivery_outcome"] = f"{type(exc).__name__}: {exc}"[:180]
189
+ elif suppressed is None:
190
+ event["sent"] = False
191
+ event["delivery_outcome"] = "no_push_dispatcher_or_device"
192
+ else:
193
+ event["sent"] = False
194
+ event["suppressed_reason"] = suppressed
195
+
196
+ if suppressed is None:
197
+ sent_events = state.setdefault("sent_events", {})
198
+ sent_events[_sent_event_state_key(event["dedupe_key"], device_id)] = {
199
+ "last_sent_at": self.now_fn(),
200
+ "event_id": event["event_id"],
201
+ "level": event["level"],
202
+ "device_id": str(device_id or "")[:120] or None,
203
+ }
204
+ state["last_push_event_id"] = event["event_id"]
205
+ state["updated_at"] = self.now_fn()
206
+ self._write_json(self.state_path, state)
207
+ self._append_event(event)
208
+ return {
209
+ "ok": True,
210
+ "status": status,
211
+ "event": event,
212
+ "delivery": delivery,
213
+ }
214
+
215
+ def snooze(self, *, key: str | None = None, minutes: int = 60) -> dict[str, Any]:
216
+ minutes = max(1, min(int(minutes or 60), 60 * 24))
217
+ dedupe_key = str(key or "*").strip() or "*"
218
+ state = self._state()
219
+ snoozes = state.setdefault("snoozes", {})
220
+ snoozes[dedupe_key] = self.now_fn() + minutes * 60
221
+ state["updated_at"] = self.now_fn()
222
+ self._write_json(self.state_path, state)
223
+ return {
224
+ "ok": True,
225
+ "key": dedupe_key,
226
+ "snoozed_until": snoozes[dedupe_key],
227
+ }
228
+
229
+ def events(self, *, since: float | None = None, limit: int = 100) -> list[dict[str, Any]]:
230
+ if not self.events_path.exists():
231
+ return []
232
+ since = float(since or 0)
233
+ limit = max(1, min(int(limit or 100), 300))
234
+ events: list[dict[str, Any]] = []
235
+ try:
236
+ for line in self.events_path.read_text(encoding="utf-8").splitlines():
237
+ if not line.strip():
238
+ continue
239
+ try:
240
+ event = json.loads(line)
241
+ except json.JSONDecodeError:
242
+ continue
243
+ if not isinstance(event, dict):
244
+ continue
245
+ if float(event.get("created_at") or 0) > since:
246
+ events.append(event)
247
+ except OSError:
248
+ return []
249
+ events.sort(key=lambda item: float(item.get("created_at") or 0), reverse=True)
250
+ return events[:limit]
251
+
252
+ def classify(
253
+ self,
254
+ worker_stats: dict[str, Any],
255
+ *,
256
+ token_sessions: list[dict[str, Any]],
257
+ human_idle_minutes: float | None,
258
+ prefs: dict[str, Any] | None = None,
259
+ ) -> dict[str, Any]:
260
+ prefs = prefs or self.preferences()["preferences"]
261
+ active = _int(worker_stats.get("automated_active"))
262
+ idle = _int(worker_stats.get("automated_idle"))
263
+ total = _int(worker_stats.get("total"), active + idle)
264
+ stale_ids = worker_stats.get("stale_session_ids") if isinstance(worker_stats.get("stale_session_ids"), list) else []
265
+ stale_count = len(stale_ids)
266
+ token_pressure_sessions = self._token_pressure_sessions(token_sessions, prefs)
267
+ human_idle = float(human_idle_minutes) if human_idle_minutes is not None else None
268
+ unattended_warning = human_idle is None or human_idle >= prefs["human_idle_warning_minutes"]
269
+ unattended_critical = human_idle is None or human_idle >= prefs["human_idle_critical_minutes"]
270
+
271
+ level = "info"
272
+ kind = "quiet"
273
+ severity = "info"
274
+ title = "Pairling sentinel quiet"
275
+ body = "No worker or token condition currently requires a push."
276
+
277
+ if active >= prefs["worker_critical_active"] and unattended_critical:
278
+ level = "critical"
279
+ kind = "worker_swarm"
280
+ severity = "critical"
281
+ title = "Pairling automation needs review"
282
+ body = f"{active} automated sessions are active. Review stale workers before token burn continues."
283
+ elif active >= prefs["worker_warning_active"] and unattended_warning:
284
+ level = "warning"
285
+ kind = "worker_swarm"
286
+ severity = "warning"
287
+ title = "Pairling worker warning"
288
+ idle_text = f" and you have been away for {int(human_idle)} minutes" if human_idle is not None else ""
289
+ body = f"{active} automated sessions are active{idle_text}."
290
+ elif stale_count >= prefs["stale_cleanup_count"]:
291
+ level = "stale_cleanup"
292
+ kind = "stale_cleanup"
293
+ severity = "warning"
294
+ title = "Pairling found stale workers"
295
+ body = f"{stale_count} workers have been idle for over an hour."
296
+ elif token_pressure_sessions and unattended_warning:
297
+ level = "token_pressure"
298
+ kind = "token_pressure"
299
+ severity = "warning"
300
+ title = "Pairling context pressure"
301
+ body = "This turn is near the context limit. Open the session to review."
302
+ elif active >= prefs["worker_watch_active"] or total >= prefs["worker_watch_total"]:
303
+ level = "watch"
304
+ kind = "worker_watch"
305
+ severity = "info"
306
+ title = "Pairling worker watch"
307
+ body = f"{active} active workers, {idle} idle workers."
308
+
309
+ project_scope = _project_scope(worker_stats)
310
+ provider = str(worker_stats.get("provider") or "all")[:40]
311
+ dedupe_key = f"sentinel:{kind}:{provider}:{project_scope}:{severity}"
312
+ return {
313
+ "level": level,
314
+ "kind": kind,
315
+ "severity": severity,
316
+ "summary": f"{active} active workers, {idle} idle, {stale_count} stale",
317
+ "active_workers": active,
318
+ "idle_workers": idle,
319
+ "total_workers": total,
320
+ "stale_workers": stale_count,
321
+ "stale_session_ids": [str(item)[:120] for item in stale_ids[:50]],
322
+ "token_pressure_sessions": token_pressure_sessions,
323
+ "human_idle_minutes": human_idle,
324
+ "dedupe_key": dedupe_key,
325
+ "title": title,
326
+ "body": body,
327
+ "route": "pairling://workers",
328
+ }
329
+
330
+ def _token_pressure_sessions(self, token_sessions: list[dict[str, Any]], prefs: dict[str, Any]) -> list[dict[str, Any]]:
331
+ rows: list[dict[str, Any]] = []
332
+ for item in token_sessions:
333
+ if not isinstance(item, dict):
334
+ continue
335
+ tokens = _int(item.get("tokens") or item.get("total_tokens"))
336
+ context_window = _int(item.get("context_window"))
337
+ if tokens <= 0 or context_window <= 0:
338
+ continue
339
+ ratio = tokens / context_window
340
+ if ratio >= float(prefs["token_pressure_ratio"]):
341
+ rows.append({
342
+ "session_id": str(item.get("session_id") or "")[:120],
343
+ "provider": str(item.get("provider") or "all")[:40],
344
+ "tokens": tokens,
345
+ "context_window": context_window,
346
+ "ratio": round(ratio, 4),
347
+ })
348
+ return rows[:20]
349
+
350
+ def _suppression_reason(
351
+ self,
352
+ status: dict[str, Any],
353
+ prefs: dict[str, Any],
354
+ state: dict[str, Any],
355
+ *,
356
+ force: bool,
357
+ device_id: str | None,
358
+ ) -> str | None:
359
+ if force:
360
+ return None
361
+ if not prefs.get("enabled"):
362
+ return "disabled"
363
+ if status["level"] in {"info", "watch"}:
364
+ return "not_push_level"
365
+ key = status["dedupe_key"]
366
+ snoozed_until = self._snoozed_until(key, state)
367
+ if snoozed_until and snoozed_until > self.now_fn():
368
+ return "snoozed"
369
+ sent = state.get("sent_events", {}).get(_sent_event_state_key(key, device_id))
370
+ if isinstance(sent, dict):
371
+ last_sent_at = float(sent.get("last_sent_at") or 0)
372
+ cooldown = prefs["cooldown_critical_minutes"] if status["severity"] == "critical" else prefs["cooldown_warning_minutes"]
373
+ if self.now_fn() - last_sent_at < cooldown * 60:
374
+ return "cooldown"
375
+ human_idle = status.get("human_idle_minutes")
376
+ if human_idle is not None:
377
+ if status["severity"] == "critical" and human_idle < prefs["human_idle_critical_minutes"]:
378
+ return "human_recent"
379
+ if status["severity"] != "critical" and human_idle < prefs["human_idle_warning_minutes"]:
380
+ return "human_recent"
381
+ return None
382
+
383
+ def _snoozed_until(self, key: str, state: dict[str, Any]) -> float | None:
384
+ snoozes = state.get("snoozes") if isinstance(state.get("snoozes"), dict) else {}
385
+ until = max(float(snoozes.get("*") or 0), float(snoozes.get(key) or 0))
386
+ return until or None
387
+
388
+ def _event_from_status(self, status: dict[str, Any], *, device_id: str | None) -> dict[str, Any]:
389
+ created_at = self.now_fn()
390
+ digest = hashlib.sha256(
391
+ json.dumps({
392
+ "created_at": int(created_at),
393
+ "key": status["dedupe_key"],
394
+ "level": status["level"],
395
+ }, sort_keys=True).encode("utf-8")
396
+ ).hexdigest()[:10]
397
+ return {
398
+ "event_id": f"sent_{int(created_at * 1000)}_{digest}",
399
+ "created_at": created_at,
400
+ "kind": status["kind"],
401
+ "level": status["level"],
402
+ "severity": status["severity"],
403
+ "dedupe_key": status["dedupe_key"],
404
+ "title": status["title"],
405
+ "body": status["body"],
406
+ "route": status["route"],
407
+ "device_id": str(device_id or "")[:120] or None,
408
+ "active_workers": status["active_workers"],
409
+ "idle_workers": status["idle_workers"],
410
+ "stale_workers": status["stale_workers"],
411
+ "token_pressure_sessions": status["token_pressure_sessions"],
412
+ }
413
+
414
+ def _state(self) -> dict[str, Any]:
415
+ try:
416
+ loaded = json.loads(self.state_path.read_text(encoding="utf-8"))
417
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
418
+ loaded = {}
419
+ if not isinstance(loaded, dict):
420
+ loaded = {}
421
+ loaded.setdefault("sent_events", {})
422
+ loaded.setdefault("snoozes", {})
423
+ return loaded
424
+
425
+ def _append_event(self, event: dict[str, Any]) -> None:
426
+ self.dir.mkdir(parents=True, exist_ok=True)
427
+ try:
428
+ os.chmod(self.dir, stat.S_IRWXU)
429
+ except OSError:
430
+ pass
431
+ with self.events_path.open("a", encoding="utf-8") as handle:
432
+ handle.write(json.dumps(event, sort_keys=True) + "\n")
433
+ try:
434
+ os.chmod(self.events_path, 0o600)
435
+ except OSError:
436
+ pass
437
+
438
+ def _write_json(self, path: Path, payload: dict[str, Any]) -> None:
439
+ path.parent.mkdir(parents=True, exist_ok=True)
440
+ try:
441
+ os.chmod(path.parent, stat.S_IRWXU)
442
+ except OSError:
443
+ pass
444
+ tmp = path.with_suffix(path.suffix + ".tmp")
445
+ with tmp.open("w", encoding="utf-8") as handle:
446
+ json.dump(payload, handle, indent=2, sort_keys=True)
447
+ handle.write("\n")
448
+ handle.flush()
449
+ os.fsync(handle.fileno())
450
+ os.replace(tmp, path)
451
+ try:
452
+ os.chmod(path, 0o600)
453
+ except OSError:
454
+ pass
455
+
456
+ def _normalize_pref(self, key: str, value: Any) -> Any:
457
+ if key in _BOOL_PREFS:
458
+ return bool(value)
459
+ if key in _INT_PREFS:
460
+ parsed = max(0, min(int(value), 10000))
461
+ if key.startswith("cooldown") or key.startswith("human_idle") or key == "stale_worker_minutes":
462
+ return max(1, parsed)
463
+ return parsed
464
+ if key in _FLOAT_PREFS:
465
+ return max(0.05, min(float(value), 0.99))
466
+ return value
467
+
468
+
469
+ def _sent_event_state_key(dedupe_key: str, device_id: str | None) -> str:
470
+ device = str(device_id or "").strip()
471
+ if not device:
472
+ return dedupe_key
473
+ return f"{dedupe_key}:device:{device[:120]}"
474
+
475
+
476
+ def _int(value: Any, default: int = 0) -> int:
477
+ try:
478
+ return int(value)
479
+ except (TypeError, ValueError):
480
+ return default
481
+
482
+
483
+ def _project_scope(worker_stats: dict[str, Any]) -> str:
484
+ projects = worker_stats.get("projects")
485
+ if not isinstance(projects, list) or not projects:
486
+ return "all"
487
+ first = projects[0] if isinstance(projects[0], dict) else {}
488
+ path = str(first.get("path") or first.get("project") or "all")
489
+ name = Path(path).name or "all"
490
+ safe = "".join(ch for ch in name if ch.isalnum() or ch in {"-", "_"})[:60]
491
+ return safe or "all"