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.
- package/package.json +5 -1
- package/payload/mac/SOURCE_BRANCH +1 -0
- package/payload/mac/SOURCE_DIRTY +1 -0
- package/payload/mac/SOURCE_REVISION +1 -0
- package/payload/mac/VERSION +1 -0
- package/payload/mac/companiond/integrations/__init__.py +1 -0
- package/payload/mac/companiond/integrations/aperture_cli/__init__.py +23 -0
- package/payload/mac/companiond/integrations/aperture_cli/launch.py +456 -0
- package/payload/mac/companiond/integrations/aperture_cli/status.py +393 -0
- package/payload/mac/companiond/live_activity_publisher.py +380 -0
- package/payload/mac/companiond/llm_route.py +108 -0
- package/payload/mac/companiond/local_mcp_bridge.py +156 -0
- package/payload/mac/companiond/model_status_contract.py +101 -0
- package/payload/mac/companiond/pairdrop_store.py +920 -0
- package/payload/mac/companiond/pairling_connectd_status.py +149 -0
- package/payload/mac/companiond/pairling_devices.py +459 -0
- package/payload/mac/companiond/pairling_pairing.py +404 -0
- package/payload/mac/companiond/pairling_relay_claims.py +232 -0
- package/payload/mac/companiond/pairling_tools.py +706 -0
- package/payload/mac/companiond/pairlingd.py +18438 -0
- package/payload/mac/companiond/providers/__init__.py +1 -0
- package/payload/mac/companiond/providers/base.py +255 -0
- package/payload/mac/companiond/providers/claude.py +127 -0
- package/payload/mac/companiond/providers/codex.py +124 -0
- package/payload/mac/companiond/providers/external.py +46 -0
- package/payload/mac/companiond/providers/registry.py +70 -0
- package/payload/mac/companiond/pty_broker.py +887 -0
- package/payload/mac/companiond/push_dispatcher.py +1990 -0
- package/payload/mac/companiond/push_event_catalog.py +566 -0
- package/payload/mac/companiond/request_proof.py +142 -0
- package/payload/mac/companiond/runtime_contract.py +47 -0
- package/payload/mac/companiond/runtime_manifest.py +197 -0
- package/payload/mac/companiond/runtime_paths.py +87 -0
- package/payload/mac/companiond/safety_monitor.py +542 -0
- package/payload/mac/companiond/sentinel_notifications.py +491 -0
- package/payload/mac/companiond/standard_push_publisher.py +516 -0
- package/payload/mac/companiond/substrate_status_contract.py +139 -0
- package/payload/mac/companiond/terminal_screen_backend.py +332 -0
- package/payload/mac/companiond/terminal_text_sanitizer.py +54 -0
- package/payload/mac/companiond/workstate_feed_contract.py +108 -0
- package/payload/mac/connectd/cmd/pairling-connectd/auth_open_test.go +116 -0
- package/payload/mac/connectd/cmd/pairling-connectd/main.go +345 -0
- package/payload/mac/connectd/cmd/pairling-connectd/upstream_health_test.go +33 -0
- package/payload/mac/connectd/go.mod +51 -0
- package/payload/mac/connectd/go.sum +229 -0
- package/payload/mac/connectd/internal/gateway/proxy.go +597 -0
- package/payload/mac/connectd/internal/gateway/proxy_test.go +531 -0
- package/payload/mac/connectd/internal/runtime/config.go +99 -0
- package/payload/mac/connectd/internal/runtime/config_test.go +29 -0
- package/payload/mac/connectd/internal/status/status.go +300 -0
- package/payload/mac/connectd/internal/status/status_test.go +263 -0
- package/payload/mac/guardian/companion-power-guardian.py +613 -0
- package/payload/mac/guardian/guardian_contract.py +67 -0
- package/payload/mac/install/bootstrap-first-run.sh +206 -0
- package/payload/mac/install/doctor.sh +660 -0
- package/payload/mac/install/install-runtime.sh +1241 -0
- package/payload/mac/install/render-launchd.py +119 -0
- package/payload/mac/install/uninstall-runtime.sh +136 -0
- package/payload/mac/mcp/phone_tools.py +210 -0
- package/payload/mac/packaging/bin/pairling +63 -0
- package/payload-manifest.json +255 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Bounded Pairling APNs and Live Activity event catalog.
|
|
3
|
+
|
|
4
|
+
This module owns the user-facing push copy and APNs-safe Live Activity state.
|
|
5
|
+
It intentionally accepts only bounded semantic labels; raw transcript, prompt,
|
|
6
|
+
command output, credentials, filesystem dumps, and stack traces are ignored.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
SUPPORTED_KINDS = {
|
|
20
|
+
"action_required",
|
|
21
|
+
"turn_result",
|
|
22
|
+
"turn_failed",
|
|
23
|
+
"tool_risk",
|
|
24
|
+
"mac_route_risk",
|
|
25
|
+
"worker_pressure",
|
|
26
|
+
"deploy_result",
|
|
27
|
+
"push_diagnostic",
|
|
28
|
+
}
|
|
29
|
+
CATALOG_KINDS = tuple(sorted(SUPPORTED_KINDS))
|
|
30
|
+
LEGACY_KIND_ALIASES = {
|
|
31
|
+
"turn_done": "turn_result",
|
|
32
|
+
"session_attention": "action_required",
|
|
33
|
+
"mac_health": "mac_route_risk",
|
|
34
|
+
"worker_sentinel": "worker_pressure",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
PHASE_BY_KIND = {
|
|
38
|
+
"action_required": "attention",
|
|
39
|
+
"turn_result": "done",
|
|
40
|
+
"turn_failed": "failed",
|
|
41
|
+
"tool_risk": "risk",
|
|
42
|
+
"mac_route_risk": "risk",
|
|
43
|
+
"worker_pressure": "attention",
|
|
44
|
+
"deploy_result": "done",
|
|
45
|
+
"push_diagnostic": "done",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
INTERRUPTION_BY_KIND = {
|
|
49
|
+
"action_required": "time-sensitive",
|
|
50
|
+
"turn_result": "active",
|
|
51
|
+
"turn_failed": "time-sensitive",
|
|
52
|
+
"tool_risk": "time-sensitive",
|
|
53
|
+
"mac_route_risk": "time-sensitive",
|
|
54
|
+
"worker_pressure": "time-sensitive",
|
|
55
|
+
"deploy_result": "active",
|
|
56
|
+
"push_diagnostic": "passive",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
ALERT_COPY = {
|
|
60
|
+
"action_required": ("Pairling needs approval", "Review the requested action before work continues."),
|
|
61
|
+
"turn_result": ("Pairling turn complete", "A turn finished with a useful result."),
|
|
62
|
+
"turn_failed": ("Pairling turn failed", "A turn failed and needs review."),
|
|
63
|
+
"tool_risk": ("Pairling tool risk", "A tool signal needs review."),
|
|
64
|
+
"mac_route_risk": ("Mac route timed out", "The paired Mac route needs attention."),
|
|
65
|
+
"worker_pressure": ("Pairling worker pressure", "Worker or token pressure needs review."),
|
|
66
|
+
"deploy_result": ("Deploy result ready", "A build or deploy result is available."),
|
|
67
|
+
"push_diagnostic": ("Pairling push test", "Push delivery is configured for this device."),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
PRIVATE_INPUT_KEYS = {
|
|
71
|
+
"raw_transcript",
|
|
72
|
+
"transcript",
|
|
73
|
+
"transcript_text",
|
|
74
|
+
"prompt",
|
|
75
|
+
"prompt_text",
|
|
76
|
+
"command_output",
|
|
77
|
+
"stdout",
|
|
78
|
+
"stderr",
|
|
79
|
+
"credentials",
|
|
80
|
+
"api_key",
|
|
81
|
+
"provider_api_key",
|
|
82
|
+
"filesystem_dump",
|
|
83
|
+
"stack_trace",
|
|
84
|
+
"traceback",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class PushEvent:
|
|
90
|
+
kind: str
|
|
91
|
+
event_id: str | None = None
|
|
92
|
+
source: str = "unknown"
|
|
93
|
+
provider: str | None = None
|
|
94
|
+
session_id: str | None = None
|
|
95
|
+
project: str | None = None
|
|
96
|
+
session_title: str | None = None
|
|
97
|
+
observed_at: float | None = None
|
|
98
|
+
created_at: float | None = None
|
|
99
|
+
phase: str | None = None
|
|
100
|
+
status: str | None = None
|
|
101
|
+
current_step: str | None = None
|
|
102
|
+
latest_event: str | None = None
|
|
103
|
+
result_summary: str | None = None
|
|
104
|
+
required_action: str | None = None
|
|
105
|
+
risk_level: str | None = None
|
|
106
|
+
risk_summary: str | None = None
|
|
107
|
+
route_health: str | None = None
|
|
108
|
+
worker_summary: str | None = None
|
|
109
|
+
build_label: str | None = None
|
|
110
|
+
action_route: str | None = None
|
|
111
|
+
freshness_seconds: int | None = None
|
|
112
|
+
privacy_tier: str = "standard"
|
|
113
|
+
dedupe_key: str | None = None
|
|
114
|
+
thread_id: str | None = None
|
|
115
|
+
collapse_id: str | None = None
|
|
116
|
+
|
|
117
|
+
def __post_init__(self) -> None:
|
|
118
|
+
normalized = catalog_event(
|
|
119
|
+
kind=self.kind,
|
|
120
|
+
event_id=self.event_id,
|
|
121
|
+
source=self.source,
|
|
122
|
+
provider=self.provider,
|
|
123
|
+
session_id=self.session_id,
|
|
124
|
+
project=self.project,
|
|
125
|
+
session_title=self.session_title,
|
|
126
|
+
observed_at=self.observed_at,
|
|
127
|
+
created_at=self.created_at,
|
|
128
|
+
phase=self.phase,
|
|
129
|
+
status=self.status,
|
|
130
|
+
current_step=self.current_step,
|
|
131
|
+
latest_event=self.latest_event,
|
|
132
|
+
result_summary=self.result_summary,
|
|
133
|
+
required_action=self.required_action,
|
|
134
|
+
risk_level=self.risk_level,
|
|
135
|
+
risk_summary=self.risk_summary,
|
|
136
|
+
route_health=self.route_health,
|
|
137
|
+
worker_summary=self.worker_summary,
|
|
138
|
+
build_label=self.build_label,
|
|
139
|
+
action_route=self.action_route,
|
|
140
|
+
freshness_seconds=self.freshness_seconds,
|
|
141
|
+
privacy_tier=self.privacy_tier,
|
|
142
|
+
dedupe_key=self.dedupe_key,
|
|
143
|
+
thread_id=self.thread_id,
|
|
144
|
+
collapse_id=self.collapse_id,
|
|
145
|
+
_skip_push_event_init=True,
|
|
146
|
+
)
|
|
147
|
+
self.__dict__.update(normalized.__dict__)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def catalog_event(
|
|
151
|
+
*,
|
|
152
|
+
kind: str,
|
|
153
|
+
source: str,
|
|
154
|
+
provider: str | None = None,
|
|
155
|
+
session_id: str | None = None,
|
|
156
|
+
project: str | None = None,
|
|
157
|
+
session_title: str | None = None,
|
|
158
|
+
observed_at: float | None = None,
|
|
159
|
+
created_at: float | None = None,
|
|
160
|
+
phase: str | None = None,
|
|
161
|
+
status: str | None = None,
|
|
162
|
+
current_step: str | None = None,
|
|
163
|
+
latest_event: str | None = None,
|
|
164
|
+
result_summary: str | None = None,
|
|
165
|
+
required_action: str | None = None,
|
|
166
|
+
risk_level: str | None = None,
|
|
167
|
+
risk_summary: str | None = None,
|
|
168
|
+
route_health: str | None = None,
|
|
169
|
+
worker_summary: str | None = None,
|
|
170
|
+
build_label: str | None = None,
|
|
171
|
+
action_route: str | None = None,
|
|
172
|
+
freshness_seconds: int | None = None,
|
|
173
|
+
privacy_tier: str = "standard",
|
|
174
|
+
dedupe_key: str | None = None,
|
|
175
|
+
thread_id: str | None = None,
|
|
176
|
+
collapse_id: str | None = None,
|
|
177
|
+
event_id: str | None = None,
|
|
178
|
+
_skip_push_event_init: bool = False,
|
|
179
|
+
**ignored_private_inputs: Any,
|
|
180
|
+
) -> PushEvent:
|
|
181
|
+
"""Build a bounded APNs-safe event.
|
|
182
|
+
|
|
183
|
+
Unknown keyword inputs are intentionally ignored, which lets callers pass a
|
|
184
|
+
larger observed event without accidentally forwarding private material.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
del ignored_private_inputs
|
|
188
|
+
normalized_kind = _canonical_kind(kind)
|
|
189
|
+
now = float(created_at if created_at is not None else time.time())
|
|
190
|
+
observed = float(observed_at if observed_at is not None else now)
|
|
191
|
+
normalized_phase = _bounded_enum(phase or PHASE_BY_KIND[normalized_kind], 32)
|
|
192
|
+
normalized_status = _bounded_enum(status or normalized_phase, 32)
|
|
193
|
+
normalized_provider = _bounded_label(provider, 32)
|
|
194
|
+
normalized_session_id = _bounded_id(session_id)
|
|
195
|
+
route = _safe_route(action_route, session_id=normalized_session_id, kind=normalized_kind)
|
|
196
|
+
normalized_project = _bounded_label(_project_name(project), 60)
|
|
197
|
+
normalized_title = _bounded_label(session_title, 60)
|
|
198
|
+
normalized_result = _bounded_summary(result_summary)
|
|
199
|
+
normalized_required = _bounded_summary(required_action)
|
|
200
|
+
normalized_risk = _bounded_enum(risk_level, 32)
|
|
201
|
+
normalized_risk_summary = _bounded_summary(risk_summary)
|
|
202
|
+
normalized_route_health = _bounded_summary(route_health)
|
|
203
|
+
normalized_worker_summary = _bounded_summary(worker_summary)
|
|
204
|
+
normalized_build_label = _bounded_label(build_label, 60)
|
|
205
|
+
normalized_current_step = _bounded_label(current_step, 60)
|
|
206
|
+
normalized_latest = _bounded_summary(latest_event) or _latest_event(
|
|
207
|
+
normalized_kind,
|
|
208
|
+
current_step=normalized_current_step,
|
|
209
|
+
result_summary=normalized_result,
|
|
210
|
+
required_action=normalized_required,
|
|
211
|
+
risk_summary=normalized_risk_summary,
|
|
212
|
+
worker_summary=normalized_worker_summary,
|
|
213
|
+
)
|
|
214
|
+
fresh = _bounded_int(freshness_seconds, minimum=0, maximum=86_400) if freshness_seconds is not None else None
|
|
215
|
+
stable = {
|
|
216
|
+
"kind": normalized_kind,
|
|
217
|
+
"session_id": normalized_session_id,
|
|
218
|
+
"project": normalized_project,
|
|
219
|
+
"status": normalized_status,
|
|
220
|
+
"result": normalized_result,
|
|
221
|
+
"required": normalized_required,
|
|
222
|
+
"risk": normalized_risk_summary,
|
|
223
|
+
"observed_bucket": int(observed),
|
|
224
|
+
}
|
|
225
|
+
normalized_dedupe = _bounded_id(dedupe_key) or _stable_hash(stable)[:32]
|
|
226
|
+
normalized_thread = _bounded_id(thread_id) or _default_thread_id(normalized_kind, normalized_session_id)
|
|
227
|
+
normalized_collapse = _bounded_id(collapse_id) or f"pairling.{normalized_kind}.{normalized_dedupe}"[:160]
|
|
228
|
+
normalized_event_id = _bounded_id(event_id) or f"push_{normalized_kind}_{_stable_hash(stable)[:24]}"
|
|
229
|
+
if _skip_push_event_init:
|
|
230
|
+
event = object.__new__(PushEvent)
|
|
231
|
+
values = {
|
|
232
|
+
"event_id": normalized_event_id,
|
|
233
|
+
"kind": normalized_kind,
|
|
234
|
+
"source": _bounded_enum(source, 32) or "unknown",
|
|
235
|
+
"provider": normalized_provider,
|
|
236
|
+
"session_id": normalized_session_id,
|
|
237
|
+
"project": normalized_project,
|
|
238
|
+
"session_title": normalized_title,
|
|
239
|
+
"observed_at": observed,
|
|
240
|
+
"created_at": now,
|
|
241
|
+
"phase": normalized_phase,
|
|
242
|
+
"status": normalized_status,
|
|
243
|
+
"current_step": normalized_current_step,
|
|
244
|
+
"latest_event": normalized_latest,
|
|
245
|
+
"result_summary": normalized_result,
|
|
246
|
+
"required_action": normalized_required,
|
|
247
|
+
"risk_level": normalized_risk,
|
|
248
|
+
"risk_summary": normalized_risk_summary,
|
|
249
|
+
"route_health": normalized_route_health,
|
|
250
|
+
"worker_summary": normalized_worker_summary,
|
|
251
|
+
"build_label": normalized_build_label,
|
|
252
|
+
"action_route": route,
|
|
253
|
+
"freshness_seconds": fresh,
|
|
254
|
+
"privacy_tier": _bounded_enum(privacy_tier, 32) or "standard",
|
|
255
|
+
"dedupe_key": normalized_dedupe,
|
|
256
|
+
"thread_id": normalized_thread,
|
|
257
|
+
"collapse_id": normalized_collapse,
|
|
258
|
+
}
|
|
259
|
+
event.__dict__.update(values)
|
|
260
|
+
return event
|
|
261
|
+
return PushEvent(
|
|
262
|
+
event_id=normalized_event_id,
|
|
263
|
+
kind=normalized_kind,
|
|
264
|
+
source=_bounded_enum(source, 32) or "unknown",
|
|
265
|
+
provider=normalized_provider,
|
|
266
|
+
session_id=normalized_session_id,
|
|
267
|
+
project=normalized_project,
|
|
268
|
+
session_title=normalized_title,
|
|
269
|
+
observed_at=observed,
|
|
270
|
+
created_at=now,
|
|
271
|
+
phase=normalized_phase,
|
|
272
|
+
status=normalized_status,
|
|
273
|
+
current_step=normalized_current_step,
|
|
274
|
+
latest_event=normalized_latest,
|
|
275
|
+
result_summary=normalized_result,
|
|
276
|
+
required_action=normalized_required,
|
|
277
|
+
risk_level=normalized_risk,
|
|
278
|
+
risk_summary=normalized_risk_summary,
|
|
279
|
+
route_health=normalized_route_health,
|
|
280
|
+
worker_summary=normalized_worker_summary,
|
|
281
|
+
build_label=normalized_build_label,
|
|
282
|
+
action_route=route,
|
|
283
|
+
freshness_seconds=fresh,
|
|
284
|
+
privacy_tier=_bounded_enum(privacy_tier, 32) or "standard",
|
|
285
|
+
dedupe_key=normalized_dedupe,
|
|
286
|
+
thread_id=normalized_thread,
|
|
287
|
+
collapse_id=normalized_collapse,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def build_push_event(**kwargs: Any) -> PushEvent:
|
|
292
|
+
return catalog_event(**kwargs)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def standard_alert_payload(event: PushEvent) -> dict[str, Any]:
|
|
296
|
+
title, default_body = ALERT_COPY[event.kind]
|
|
297
|
+
if event.kind == "action_required" and event.required_action:
|
|
298
|
+
body = _join_sentence(event.required_action, event.session_title or event.project)
|
|
299
|
+
elif event.kind in {"turn_result", "deploy_result"} and event.result_summary:
|
|
300
|
+
body = _join_sentence(event.result_summary, event.build_label or event.session_title or event.project)
|
|
301
|
+
elif event.kind in {"turn_failed", "tool_risk", "mac_route_risk"} and (event.risk_summary or event.current_step):
|
|
302
|
+
body = _join_sentence(event.risk_summary or event.current_step, event.route_health or event.session_title or event.project)
|
|
303
|
+
elif event.kind == "worker_pressure" and event.worker_summary:
|
|
304
|
+
body = _join_sentence(event.worker_summary, event.required_action or "Review workers.")
|
|
305
|
+
else:
|
|
306
|
+
body = default_body
|
|
307
|
+
payload = {
|
|
308
|
+
"event_id": event.event_id,
|
|
309
|
+
"kind": event.kind,
|
|
310
|
+
"route": event.action_route or "pairling://dashboard",
|
|
311
|
+
"title": _bounded_label(_title_for_event(event, title), 60) or title,
|
|
312
|
+
"body": _bounded_summary(body) or default_body,
|
|
313
|
+
"thread_id": event.thread_id,
|
|
314
|
+
"collapse_id": event.collapse_id,
|
|
315
|
+
"interruption_level": INTERRUPTION_BY_KIND[event.kind],
|
|
316
|
+
"source": event.source,
|
|
317
|
+
"provider": event.provider,
|
|
318
|
+
"session_id": event.session_id,
|
|
319
|
+
"project": event.project,
|
|
320
|
+
"phase": event.phase,
|
|
321
|
+
"observed_at": event.observed_at,
|
|
322
|
+
"privacy_tier": event.privacy_tier,
|
|
323
|
+
}
|
|
324
|
+
payload.update(_semantic_fields(event))
|
|
325
|
+
return payload
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def live_activity_payload(event: PushEvent) -> dict[str, Any]:
|
|
329
|
+
activity_event = "end" if event.phase == "done" else "update"
|
|
330
|
+
stale = 75
|
|
331
|
+
dismissal = 300 if event.required_action or event.phase in {"failed", "attention"} else 120
|
|
332
|
+
return {
|
|
333
|
+
"session_id": event.session_id or "",
|
|
334
|
+
"event": activity_event,
|
|
335
|
+
"event_id": event.event_id,
|
|
336
|
+
"content_state": live_activity_content_state(event),
|
|
337
|
+
"stale_seconds": stale,
|
|
338
|
+
"dismissal_seconds": dismissal,
|
|
339
|
+
"source": event.source,
|
|
340
|
+
"phase": event.phase,
|
|
341
|
+
"project": event.project,
|
|
342
|
+
"observed_at": event.observed_at,
|
|
343
|
+
"collapse_id": event.collapse_id,
|
|
344
|
+
"freshness_seconds": event.freshness_seconds,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def live_activity_content_state(event: PushEvent) -> dict[str, Any]:
|
|
349
|
+
state = event.phase
|
|
350
|
+
payload = {
|
|
351
|
+
"state": state,
|
|
352
|
+
"phase": event.phase,
|
|
353
|
+
"tool": event.current_step,
|
|
354
|
+
"effort": None,
|
|
355
|
+
"tokens": None,
|
|
356
|
+
"verb": _verb_for_phase(event.phase),
|
|
357
|
+
"attentionLevel": _attention_for_event(event),
|
|
358
|
+
"updatedAtEpoch": event.observed_at,
|
|
359
|
+
"eventId": event.event_id,
|
|
360
|
+
"sessionTitle": event.session_title,
|
|
361
|
+
"provider": event.provider,
|
|
362
|
+
"project": event.project,
|
|
363
|
+
"currentStep": event.current_step,
|
|
364
|
+
"latestEvent": event.latest_event,
|
|
365
|
+
"resultSummary": event.result_summary,
|
|
366
|
+
"requiredAction": event.required_action,
|
|
367
|
+
"freshness": _freshness_label(event.freshness_seconds),
|
|
368
|
+
"riskLevel": event.risk_level,
|
|
369
|
+
"riskSummary": event.risk_summary,
|
|
370
|
+
"routeHealth": event.route_health,
|
|
371
|
+
"workerSummary": event.worker_summary,
|
|
372
|
+
"buildLabel": event.build_label,
|
|
373
|
+
"actionRoute": event.action_route,
|
|
374
|
+
}
|
|
375
|
+
return {key: value for key, value in payload.items() if value is not None}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def outbox_metadata(
|
|
379
|
+
event: PushEvent,
|
|
380
|
+
*,
|
|
381
|
+
content_state: dict[str, Any] | None = None,
|
|
382
|
+
sent_at: float,
|
|
383
|
+
apns_outcome: str | None = None,
|
|
384
|
+
) -> dict[str, Any]:
|
|
385
|
+
content_state = content_state or live_activity_content_state(event)
|
|
386
|
+
return {
|
|
387
|
+
"source": event.source,
|
|
388
|
+
"phase": event.phase,
|
|
389
|
+
"project": event.project,
|
|
390
|
+
"observed_at": event.observed_at,
|
|
391
|
+
"sent_at": float(sent_at),
|
|
392
|
+
"collapse_id": event.collapse_id,
|
|
393
|
+
"freshness_seconds_at_send": _freshness_at_send(event, sent_at),
|
|
394
|
+
"content_state_hash": _stable_hash(content_state),
|
|
395
|
+
"apns_outcome": _bounded_enum(apns_outcome, 80),
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _enum(value: Any, allowed: set[str], default: str) -> str:
|
|
400
|
+
text = str(value or "").strip()
|
|
401
|
+
return text if text in allowed else default
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _semantic_fields(event: PushEvent) -> dict[str, Any]:
|
|
405
|
+
fields = {
|
|
406
|
+
"status": event.status,
|
|
407
|
+
"current_step": event.current_step,
|
|
408
|
+
"latest_event": event.latest_event,
|
|
409
|
+
"result_summary": event.result_summary,
|
|
410
|
+
"required_action": event.required_action,
|
|
411
|
+
"risk_level": event.risk_level,
|
|
412
|
+
"risk_summary": event.risk_summary,
|
|
413
|
+
"route_health": event.route_health,
|
|
414
|
+
"worker_summary": event.worker_summary,
|
|
415
|
+
"build_label": event.build_label,
|
|
416
|
+
"action_route": event.action_route,
|
|
417
|
+
"freshness_seconds": event.freshness_seconds,
|
|
418
|
+
"dedupe_key": event.dedupe_key,
|
|
419
|
+
}
|
|
420
|
+
return {key: value for key, value in fields.items() if value not in (None, "")}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _canonical_kind(value: Any) -> str:
|
|
424
|
+
text = str(value or "").strip()
|
|
425
|
+
text = LEGACY_KIND_ALIASES.get(text, text)
|
|
426
|
+
return _enum(text, SUPPORTED_KINDS, "push_diagnostic")
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _bounded_enum(value: Any, limit: int) -> str | None:
|
|
430
|
+
return _bounded_text(value, limit, strip_private=True)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _bounded_id(value: Any) -> str | None:
|
|
434
|
+
text = _bounded_text(value, 160, strip_private=True)
|
|
435
|
+
if not text:
|
|
436
|
+
return None
|
|
437
|
+
return re.sub(r"[^A-Za-z0-9_.:/@#-]", "-", text)[:160]
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _bounded_label(value: Any, limit: int) -> str | None:
|
|
441
|
+
return _bounded_text(value, limit, strip_private=True)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _bounded_summary(value: Any) -> str | None:
|
|
445
|
+
text = _bounded_text(value, 120, strip_private=True)
|
|
446
|
+
if not text:
|
|
447
|
+
return None
|
|
448
|
+
text = re.sub(r"(?i)traceback.*", "Failure details require review.", text).strip()
|
|
449
|
+
return text[:120] or None
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _bounded_text(value: Any, limit: int, *, strip_private: bool) -> str | None:
|
|
453
|
+
if value in (None, ""):
|
|
454
|
+
return None
|
|
455
|
+
text = str(value).replace("\r", " ").replace("\n", " ").strip()
|
|
456
|
+
if strip_private:
|
|
457
|
+
text = re.sub(r"(?i)(prompt|token|api[_-]?key|credential|password|secret)=?[^&\\s]*", "[redacted]", text)
|
|
458
|
+
text = re.sub(r"/Users/[^\\s?&]+", "[path]", text)
|
|
459
|
+
text = re.sub(r"(?i)traceback \\(most recent call last\\):.*", "Failure details require review.", text)
|
|
460
|
+
text = re.sub(r"\\s+", " ", text).strip()
|
|
461
|
+
return text[:limit] if text else None
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _safe_route(value: Any, *, session_id: str | None, kind: str) -> str:
|
|
465
|
+
text = _bounded_text(value, 300, strip_private=True)
|
|
466
|
+
if text and text.startswith("pairling://"):
|
|
467
|
+
return text
|
|
468
|
+
if session_id:
|
|
469
|
+
return "pairling://session/" + session_id
|
|
470
|
+
if kind == "worker_pressure":
|
|
471
|
+
return "pairling://workers"
|
|
472
|
+
if kind == "mac_route_risk":
|
|
473
|
+
return "pairling://health"
|
|
474
|
+
if kind == "push_diagnostic":
|
|
475
|
+
return "pairling://settings/push"
|
|
476
|
+
if kind == "deploy_result":
|
|
477
|
+
return "pairling://builds"
|
|
478
|
+
return "pairling://dashboard"
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _project_name(value: Any) -> str | None:
|
|
482
|
+
text = str(value or "").strip()
|
|
483
|
+
if not text:
|
|
484
|
+
return None
|
|
485
|
+
return text.rstrip("/").rsplit("/", 1)[-1] or text
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _latest_event(kind: str, **parts: Any) -> str | None:
|
|
489
|
+
for key in ["required_action", "risk_summary", "result_summary", "worker_summary", "current_step"]:
|
|
490
|
+
if parts.get(key):
|
|
491
|
+
return _bounded_summary(parts[key])
|
|
492
|
+
return ALERT_COPY[kind][1]
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _title_for_event(event: PushEvent, default: str) -> str:
|
|
496
|
+
if event.kind == "deploy_result" and event.build_label:
|
|
497
|
+
return f"{event.build_label} result"
|
|
498
|
+
if event.kind == "turn_result" and event.project:
|
|
499
|
+
return f"{event.project} turn complete"
|
|
500
|
+
return default
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _join_sentence(left: str | None, right: str | None) -> str:
|
|
504
|
+
if left and right:
|
|
505
|
+
return f"{left}. {right}."
|
|
506
|
+
return left or right or ""
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _verb_for_phase(phase: str) -> str:
|
|
510
|
+
return {
|
|
511
|
+
"attention": "Review",
|
|
512
|
+
"done": "Done",
|
|
513
|
+
"failed": "Failed",
|
|
514
|
+
"stale": "Stale",
|
|
515
|
+
"tool": "Using",
|
|
516
|
+
"responding": "Responding",
|
|
517
|
+
"starting": "Starting",
|
|
518
|
+
}.get(phase, "Thinking")
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _attention_for_event(event: PushEvent) -> str | None:
|
|
522
|
+
if event.phase in {"attention", "stale"}:
|
|
523
|
+
return "warning"
|
|
524
|
+
if event.phase == "failed" or event.risk_level == "critical":
|
|
525
|
+
return "critical"
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _freshness_label(seconds: int | None) -> str | None:
|
|
530
|
+
if seconds is None:
|
|
531
|
+
return "now"
|
|
532
|
+
if seconds <= 0:
|
|
533
|
+
return "now"
|
|
534
|
+
if seconds < 60:
|
|
535
|
+
return f"{seconds}s ago"
|
|
536
|
+
return f"{seconds // 60}m ago"
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _freshness_at_send(event: PushEvent, sent_at: float) -> int:
|
|
540
|
+
if event.freshness_seconds is not None:
|
|
541
|
+
return int(event.freshness_seconds)
|
|
542
|
+
return max(0, int(float(sent_at) - float(event.observed_at)))
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _bounded_int(value: Any, *, minimum: int, maximum: int) -> int:
|
|
546
|
+
try:
|
|
547
|
+
parsed = int(value)
|
|
548
|
+
except (TypeError, ValueError):
|
|
549
|
+
parsed = minimum
|
|
550
|
+
return max(minimum, min(maximum, parsed))
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _default_thread_id(kind: str, session_id: str | None) -> str:
|
|
554
|
+
if session_id:
|
|
555
|
+
return ("pairling.session." + session_id)[:160]
|
|
556
|
+
if kind == "worker_pressure":
|
|
557
|
+
return "pairling.workers"
|
|
558
|
+
if kind == "mac_route_risk":
|
|
559
|
+
return "pairling.health"
|
|
560
|
+
if kind == "deploy_result":
|
|
561
|
+
return "pairling.deploy"
|
|
562
|
+
return "pairling.push"
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _stable_hash(payload: Any) -> str:
|
|
566
|
+
return hashlib.sha256(json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")).hexdigest()
|