@mentra/cloud-protocol 3.2.1-dev.314 → 3.3.0-beta.942

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 (29) hide show
  1. package/package.json +1 -1
  2. package/src/__fixtures__/fleet/ack-invalid-reason-on-accepted.json +11 -0
  3. package/src/__fixtures__/fleet/ack-invalid-rejected-without-reason.json +10 -0
  4. package/src/__fixtures__/fleet/ack-invalid-unknown-error.json +3 -0
  5. package/src/__fixtures__/fleet/ack-valid-mixed.json +33 -0
  6. package/src/__fixtures__/fleet/ack-valid-not-installed.json +3 -0
  7. package/src/__fixtures__/fleet/ack-valid-unavailable.json +4 -0
  8. package/src/__fixtures__/fleet/report-invalid-battery-zero-when-unsupported.json +55 -0
  9. package/src/__fixtures__/fleet/report-invalid-email-identity.json +57 -0
  10. package/src/__fixtures__/fleet/report-invalid-empty-batch.json +4 -0
  11. package/src/__fixtures__/fleet/report-invalid-execution-without-package-name.json +32 -0
  12. package/src/__fixtures__/fleet/report-invalid-missing-record-id.json +55 -0
  13. package/src/__fixtures__/fleet/report-invalid-negative-elapsed.json +33 -0
  14. package/src/__fixtures__/fleet/report-invalid-offset-time.json +56 -0
  15. package/src/__fixtures__/fleet/report-invalid-photo-without-operation-id.json +30 -0
  16. package/src/__fixtures__/fleet/report-invalid-snapshot-glasses-without-binding.json +50 -0
  17. package/src/__fixtures__/fleet/report-valid-batch-mixed.json +182 -0
  18. package/src/__fixtures__/fleet/report-valid-coverage-gap.json +27 -0
  19. package/src/__fixtures__/fleet/report-valid-miniapp-launch.json +28 -0
  20. package/src/__fixtures__/fleet/report-valid-photo-operation.json +25 -0
  21. package/src/__fixtures__/fleet/report-valid-snapshot-provisional.json +53 -0
  22. package/src/__fixtures__/fleet/report-valid-snapshot.json +56 -0
  23. package/src/__fixtures__/fleet/report-valid-usage-checkpoint.json +33 -0
  24. package/src/__fixtures__/fleet/report-valid-usage-packed.json +62 -0
  25. package/src/audio.ts +11 -0
  26. package/src/fleet.ts +253 -0
  27. package/src/handshake.ts +5 -1
  28. package/src/index.ts +2 -0
  29. package/src/report-automation.ts +20 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/cloud-protocol",
3
- "version": "3.2.1-dev.314",
3
+ "version": "3.3.0-beta.942",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "exports": {
@@ -0,0 +1,11 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "results": [
4
+ {
5
+ "index": 0,
6
+ "recordId": "rec-x-0002",
7
+ "status": "accepted",
8
+ "reasonCode": "schema_invalid"
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "results": [
4
+ {
5
+ "index": 0,
6
+ "recordId": "rec-x-0001",
7
+ "status": "rejected"
8
+ }
9
+ ]
10
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "error": "fleet_broken"
3
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "results": [
4
+ {
5
+ "index": 0,
6
+ "recordId": "rec-mix-0001",
7
+ "status": "accepted"
8
+ },
9
+ {
10
+ "index": 1,
11
+ "recordId": "rec-mix-0002",
12
+ "status": "duplicate"
13
+ },
14
+ {
15
+ "index": 2,
16
+ "recordId": "rec-mix-0003",
17
+ "status": "retryable",
18
+ "retryAfterSeconds": 30
19
+ },
20
+ {
21
+ "index": 3,
22
+ "recordId": "rec-mix-0004",
23
+ "status": "rejected",
24
+ "reasonCode": "content_mismatch"
25
+ },
26
+ {
27
+ "index": 4,
28
+ "recordId": "rec-mix-0005",
29
+ "status": "rejected",
30
+ "reasonCode": "outside_replay_window"
31
+ }
32
+ ]
33
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "error": "fleet_not_installed"
3
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "error": "fleet_unavailable",
3
+ "retryAfterSeconds": 60
4
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": [
4
+ {
5
+ "schemaVersion": 1,
6
+ "recordId": "rec-bad-0007",
7
+ "phoneDeviceId": "phone-7f3a91",
8
+ "collectorSessionId": "cs-20261013-01",
9
+ "sequence": 0,
10
+ "observedAt": "2026-10-13T11:00:00Z",
11
+ "capabilities": [
12
+ "glasses_connection",
13
+ "glasses_battery",
14
+ "glasses_versions",
15
+ "miniapp_execution",
16
+ "engine_active"
17
+ ],
18
+ "kind": "snapshot",
19
+ "deviceBinding": {
20
+ "kind": "serial",
21
+ "manufacturer": "Mentra",
22
+ "product": "mentra_live",
23
+ "serial": "ML2A-0042-7731"
24
+ },
25
+ "payload": {
26
+ "phone": {
27
+ "model": "Pixel 8",
28
+ "os": "android",
29
+ "osVersion": "15",
30
+ "appVersion": "3.4.0",
31
+ "appBuild": "3400",
32
+ "engineVersion": "1.12.0",
33
+ "sdkVersion": "0.9.2"
34
+ },
35
+ "glasses": {
36
+ "model": "mentra_live",
37
+ "firmware": {
38
+ "apk": "1.7.3",
39
+ "mtk": "2.1.0",
40
+ "bes": "0.8.5"
41
+ },
42
+ "connection": {
43
+ "state": "connected",
44
+ "observedAt": "2026-10-13T11:00:00Z"
45
+ },
46
+ "battery": {
47
+ "state": "unsupported",
48
+ "observedAt": "2026-10-13T11:00:00Z",
49
+ "level": 0
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ]
55
+ }
@@ -0,0 +1,57 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": [
4
+ {
5
+ "schemaVersion": 1,
6
+ "recordId": "rec-bad-0002",
7
+ "phoneDeviceId": "phone-7f3a91",
8
+ "collectorSessionId": "cs-20261013-01",
9
+ "sequence": 0,
10
+ "observedAt": "2026-10-13T11:00:00Z",
11
+ "capabilities": [
12
+ "glasses_connection",
13
+ "glasses_battery",
14
+ "glasses_versions",
15
+ "miniapp_execution",
16
+ "engine_active"
17
+ ],
18
+ "kind": "snapshot",
19
+ "deviceBinding": {
20
+ "kind": "serial",
21
+ "manufacturer": "Mentra",
22
+ "product": "mentra_live",
23
+ "serial": "ML2A-0042-7731"
24
+ },
25
+ "payload": {
26
+ "phone": {
27
+ "model": "Pixel 8",
28
+ "os": "android",
29
+ "osVersion": "15",
30
+ "appVersion": "3.4.0",
31
+ "appBuild": "3400",
32
+ "engineVersion": "1.12.0",
33
+ "sdkVersion": "0.9.2"
34
+ },
35
+ "glasses": {
36
+ "model": "mentra_live",
37
+ "firmware": {
38
+ "apk": "1.7.3",
39
+ "mtk": "2.1.0",
40
+ "bes": "0.8.5"
41
+ },
42
+ "connection": {
43
+ "state": "connected",
44
+ "observedAt": "2026-10-13T11:00:00Z"
45
+ },
46
+ "battery": {
47
+ "state": "measured",
48
+ "observedAt": "2026-10-13T11:00:00Z",
49
+ "level": 76,
50
+ "charging": false
51
+ }
52
+ }
53
+ },
54
+ "email": "someone@example.com"
55
+ }
56
+ ]
57
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": []
4
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": [
4
+ {
5
+ "schemaVersion": 1,
6
+ "recordId": "rec-bad-0005",
7
+ "phoneDeviceId": "phone-7f3a91",
8
+ "collectorSessionId": "cs-20261013-01",
9
+ "sequence": 1,
10
+ "observedAt": "2026-10-13T11:03:10Z",
11
+ "capabilities": [
12
+ "glasses_connection",
13
+ "glasses_battery",
14
+ "glasses_versions",
15
+ "miniapp_execution",
16
+ "engine_active"
17
+ ],
18
+ "kind": "usage_checkpoint",
19
+ "payload": {
20
+ "sessions": [
21
+ {
22
+ "activity": "miniapp_execution",
23
+ "activitySessionId": "act-8d02",
24
+ "startedAt": "2026-10-13T11:02:10Z",
25
+ "elapsedMs": 60000,
26
+ "ended": false
27
+ }
28
+ ]
29
+ }
30
+ }
31
+ ]
32
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": [
4
+ {
5
+ "schemaVersion": 1,
6
+ "phoneDeviceId": "phone-7f3a91",
7
+ "collectorSessionId": "cs-20261013-01",
8
+ "sequence": 1,
9
+ "observedAt": "2026-10-13T11:00:00Z",
10
+ "capabilities": [
11
+ "glasses_connection",
12
+ "glasses_battery",
13
+ "glasses_versions",
14
+ "miniapp_execution",
15
+ "engine_active"
16
+ ],
17
+ "kind": "snapshot",
18
+ "deviceBinding": {
19
+ "kind": "serial",
20
+ "manufacturer": "Mentra",
21
+ "product": "mentra_live",
22
+ "serial": "ML2A-0042-7731"
23
+ },
24
+ "payload": {
25
+ "phone": {
26
+ "model": "Pixel 8",
27
+ "os": "android",
28
+ "osVersion": "15",
29
+ "appVersion": "3.4.0",
30
+ "appBuild": "3400",
31
+ "engineVersion": "1.12.0",
32
+ "sdkVersion": "0.9.2"
33
+ },
34
+ "glasses": {
35
+ "model": "mentra_live",
36
+ "firmware": {
37
+ "apk": "1.7.3",
38
+ "mtk": "2.1.0",
39
+ "bes": "0.8.5"
40
+ },
41
+ "connection": {
42
+ "state": "connected",
43
+ "observedAt": "2026-10-13T11:00:00Z"
44
+ },
45
+ "battery": {
46
+ "state": "measured",
47
+ "observedAt": "2026-10-13T11:00:00Z",
48
+ "level": 76,
49
+ "charging": false
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ]
55
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": [
4
+ {
5
+ "schemaVersion": 1,
6
+ "recordId": "rec-bad-0003",
7
+ "phoneDeviceId": "phone-7f3a91",
8
+ "collectorSessionId": "cs-20261013-01",
9
+ "sequence": 1,
10
+ "observedAt": "2026-10-13T11:03:10Z",
11
+ "capabilities": [
12
+ "glasses_connection",
13
+ "glasses_battery",
14
+ "glasses_versions",
15
+ "miniapp_execution",
16
+ "engine_active"
17
+ ],
18
+ "kind": "usage_checkpoint",
19
+ "payload": {
20
+ "sessions": [
21
+ {
22
+ "activity": "miniapp_execution",
23
+ "activitySessionId": "act-8d02",
24
+ "packageName": "com.mentra.notes",
25
+ "startedAt": "2026-10-13T11:02:10Z",
26
+ "elapsedMs": -5,
27
+ "ended": false
28
+ }
29
+ ]
30
+ }
31
+ }
32
+ ]
33
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": [
4
+ {
5
+ "schemaVersion": 1,
6
+ "recordId": "rec-bad-0001",
7
+ "phoneDeviceId": "phone-7f3a91",
8
+ "collectorSessionId": "cs-20261013-01",
9
+ "sequence": 0,
10
+ "observedAt": "2026-10-13T19:00:00+08:00",
11
+ "capabilities": [
12
+ "glasses_connection",
13
+ "glasses_battery",
14
+ "glasses_versions",
15
+ "miniapp_execution",
16
+ "engine_active"
17
+ ],
18
+ "kind": "snapshot",
19
+ "deviceBinding": {
20
+ "kind": "serial",
21
+ "manufacturer": "Mentra",
22
+ "product": "mentra_live",
23
+ "serial": "ML2A-0042-7731"
24
+ },
25
+ "payload": {
26
+ "phone": {
27
+ "model": "Pixel 8",
28
+ "os": "android",
29
+ "osVersion": "15",
30
+ "appVersion": "3.4.0",
31
+ "appBuild": "3400",
32
+ "engineVersion": "1.12.0",
33
+ "sdkVersion": "0.9.2"
34
+ },
35
+ "glasses": {
36
+ "model": "mentra_live",
37
+ "firmware": {
38
+ "apk": "1.7.3",
39
+ "mtk": "2.1.0",
40
+ "bes": "0.8.5"
41
+ },
42
+ "connection": {
43
+ "state": "connected",
44
+ "observedAt": "2026-10-13T11:00:00Z"
45
+ },
46
+ "battery": {
47
+ "state": "measured",
48
+ "observedAt": "2026-10-13T11:00:00Z",
49
+ "level": 76,
50
+ "charging": false
51
+ }
52
+ }
53
+ }
54
+ }
55
+ ]
56
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": [
4
+ {
5
+ "schemaVersion": 1,
6
+ "recordId": "rec-bad-0006",
7
+ "phoneDeviceId": "phone-7f3a91",
8
+ "collectorSessionId": "cs-20261013-01",
9
+ "sequence": 3,
10
+ "observedAt": "2026-10-13T11:04:00Z",
11
+ "capabilities": [
12
+ "glasses_connection",
13
+ "glasses_battery",
14
+ "glasses_versions",
15
+ "miniapp_execution",
16
+ "engine_active"
17
+ ],
18
+ "kind": "photo_operation",
19
+ "deviceBinding": {
20
+ "kind": "serial",
21
+ "manufacturer": "Mentra",
22
+ "product": "mentra_live",
23
+ "serial": "ML2A-0042-7731"
24
+ },
25
+ "payload": {
26
+ "stage": "captured"
27
+ }
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": [
4
+ {
5
+ "schemaVersion": 1,
6
+ "recordId": "rec-snap-0001",
7
+ "phoneDeviceId": "phone-7f3a91",
8
+ "collectorSessionId": "cs-20261013-01",
9
+ "sequence": 0,
10
+ "observedAt": "2026-10-13T11:00:00Z",
11
+ "capabilities": [
12
+ "glasses_connection",
13
+ "glasses_battery",
14
+ "glasses_versions",
15
+ "miniapp_execution",
16
+ "engine_active"
17
+ ],
18
+ "kind": "snapshot",
19
+ "payload": {
20
+ "phone": {
21
+ "model": "Pixel 8",
22
+ "os": "android",
23
+ "osVersion": "15",
24
+ "appVersion": "3.4.0",
25
+ "appBuild": "3400",
26
+ "engineVersion": "1.12.0",
27
+ "sdkVersion": "0.9.2"
28
+ },
29
+ "glasses": {
30
+ "model": "mentra_live",
31
+ "firmware": {
32
+ "apk": "1.7.3",
33
+ "mtk": "2.1.0",
34
+ "bes": "0.8.5"
35
+ },
36
+ "connection": {
37
+ "state": "connected",
38
+ "observedAt": "2026-10-13T11:00:00Z"
39
+ },
40
+ "battery": {
41
+ "state": "measured",
42
+ "observedAt": "2026-10-13T11:00:00Z",
43
+ "level": 76,
44
+ "charging": false
45
+ }
46
+ }
47
+ }
48
+ }
49
+ ]
50
+ }
@@ -0,0 +1,182 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": [
4
+ {
5
+ "schemaVersion": 1,
6
+ "recordId": "rec-mix-0001",
7
+ "phoneDeviceId": "phone-7f3a91",
8
+ "collectorSessionId": "cs-20261013-01",
9
+ "sequence": 10,
10
+ "observedAt": "2026-10-13T11:00:00Z",
11
+ "capabilities": [
12
+ "glasses_connection",
13
+ "glasses_battery",
14
+ "glasses_versions",
15
+ "miniapp_execution",
16
+ "app_foreground",
17
+ "engine_active",
18
+ "photo_operations"
19
+ ],
20
+ "kind": "snapshot",
21
+ "deviceBinding": {
22
+ "kind": "serial",
23
+ "manufacturer": "Mentra",
24
+ "product": "mentra_live",
25
+ "serial": "ML2A-0042-7731"
26
+ },
27
+ "payload": {
28
+ "phone": {
29
+ "model": "Pixel 8",
30
+ "os": "android",
31
+ "osVersion": "15",
32
+ "appVersion": "3.4.0",
33
+ "appBuild": "3400",
34
+ "engineVersion": "1.12.0",
35
+ "sdkVersion": "0.9.2"
36
+ },
37
+ "glasses": {
38
+ "model": "mentra_live",
39
+ "firmware": {
40
+ "apk": "1.7.3",
41
+ "mtk": "2.1.0",
42
+ "bes": "0.8.5"
43
+ },
44
+ "connection": {
45
+ "state": "connected",
46
+ "observedAt": "2026-10-13T11:00:00Z"
47
+ },
48
+ "battery": {
49
+ "state": "measured",
50
+ "observedAt": "2026-10-13T11:00:00Z",
51
+ "level": 76,
52
+ "charging": false
53
+ }
54
+ }
55
+ }
56
+ },
57
+ {
58
+ "schemaVersion": 1,
59
+ "recordId": "rec-mix-0002",
60
+ "phoneDeviceId": "phone-7f3a91",
61
+ "collectorSessionId": "cs-20261013-01",
62
+ "sequence": 11,
63
+ "observedAt": "2026-10-13T11:02:10Z",
64
+ "capabilities": [
65
+ "glasses_connection",
66
+ "glasses_battery",
67
+ "glasses_versions",
68
+ "miniapp_execution",
69
+ "app_foreground",
70
+ "engine_active",
71
+ "photo_operations"
72
+ ],
73
+ "kind": "miniapp_launch",
74
+ "payload": {
75
+ "packageName": "com.mentra.notes",
76
+ "miniappName": "Mentra Notes",
77
+ "versionName": "2.0.1",
78
+ "start": "user",
79
+ "outcome": "started"
80
+ }
81
+ },
82
+ {
83
+ "schemaVersion": 1,
84
+ "recordId": "rec-mix-0003",
85
+ "phoneDeviceId": "phone-7f3a91",
86
+ "collectorSessionId": "cs-20261013-01",
87
+ "sequence": 12,
88
+ "observedAt": "2026-10-13T11:03:10Z",
89
+ "capabilities": [
90
+ "glasses_connection",
91
+ "glasses_battery",
92
+ "glasses_versions",
93
+ "miniapp_execution",
94
+ "app_foreground",
95
+ "engine_active",
96
+ "photo_operations"
97
+ ],
98
+ "kind": "usage_checkpoint",
99
+ "payload": {
100
+ "sessions": [
101
+ {
102
+ "activity": "glasses_connection",
103
+ "activitySessionId": "act-conn-01",
104
+ "deviceBinding": {
105
+ "kind": "serial",
106
+ "manufacturer": "Mentra",
107
+ "product": "mentra_live",
108
+ "serial": "ML2A-0042-7731"
109
+ },
110
+ "startedAt": "2026-10-13T10:59:00Z",
111
+ "elapsedMs": 250000,
112
+ "ended": false
113
+ },
114
+ {
115
+ "activity": "miniapp_execution",
116
+ "activitySessionId": "act-8d02",
117
+ "packageName": "com.mentra.notes",
118
+ "startedAt": "2026-10-13T11:02:10Z",
119
+ "elapsedMs": 60000,
120
+ "ended": false
121
+ }
122
+ ]
123
+ }
124
+ },
125
+ {
126
+ "schemaVersion": 1,
127
+ "recordId": "rec-mix-0004",
128
+ "phoneDeviceId": "phone-7f3a91",
129
+ "collectorSessionId": "cs-20261013-01",
130
+ "sequence": 13,
131
+ "observedAt": "2026-10-13T11:04:00Z",
132
+ "capabilities": [
133
+ "glasses_connection",
134
+ "glasses_battery",
135
+ "glasses_versions",
136
+ "miniapp_execution",
137
+ "app_foreground",
138
+ "engine_active",
139
+ "photo_operations"
140
+ ],
141
+ "kind": "photo_operation",
142
+ "operationId": "op-photo-41c9",
143
+ "deviceBinding": {
144
+ "kind": "serial",
145
+ "manufacturer": "Mentra",
146
+ "product": "mentra_live",
147
+ "serial": "ML2A-0042-7731"
148
+ },
149
+ "payload": {
150
+ "stage": "requested"
151
+ }
152
+ },
153
+ {
154
+ "schemaVersion": 1,
155
+ "recordId": "rec-mix-0005",
156
+ "phoneDeviceId": "phone-7f3a91",
157
+ "collectorSessionId": "cs-20261013-01",
158
+ "sequence": 14,
159
+ "observedAt": "2026-10-13T11:04:02Z",
160
+ "capabilities": [
161
+ "glasses_connection",
162
+ "glasses_battery",
163
+ "glasses_versions",
164
+ "miniapp_execution",
165
+ "app_foreground",
166
+ "engine_active",
167
+ "photo_operations"
168
+ ],
169
+ "kind": "photo_operation",
170
+ "operationId": "op-photo-41c9",
171
+ "deviceBinding": {
172
+ "kind": "serial",
173
+ "manufacturer": "Mentra",
174
+ "product": "mentra_live",
175
+ "serial": "ML2A-0042-7731"
176
+ },
177
+ "payload": {
178
+ "stage": "captured"
179
+ }
180
+ }
181
+ ]
182
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": [
4
+ {
5
+ "schemaVersion": 1,
6
+ "recordId": "rec-gap-0001",
7
+ "phoneDeviceId": "phone-7f3a91",
8
+ "collectorSessionId": "cs-20261013-01",
9
+ "sequence": 9,
10
+ "observedAt": "2026-10-13T12:00:00Z",
11
+ "capabilities": [
12
+ "glasses_connection",
13
+ "glasses_battery",
14
+ "glasses_versions",
15
+ "miniapp_execution",
16
+ "engine_active"
17
+ ],
18
+ "kind": "coverage_gap",
19
+ "payload": {
20
+ "reason": "queue_dropped",
21
+ "from": "2026-10-13T11:20:00Z",
22
+ "to": "2026-10-13T11:55:00Z",
23
+ "droppedRecordCount": 31
24
+ }
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": [
4
+ {
5
+ "schemaVersion": 1,
6
+ "recordId": "rec-launch-0001",
7
+ "phoneDeviceId": "phone-7f3a91",
8
+ "collectorSessionId": "cs-20261013-01",
9
+ "sequence": 1,
10
+ "observedAt": "2026-10-13T11:02:10Z",
11
+ "capabilities": [
12
+ "glasses_connection",
13
+ "glasses_battery",
14
+ "glasses_versions",
15
+ "miniapp_execution",
16
+ "engine_active"
17
+ ],
18
+ "kind": "miniapp_launch",
19
+ "payload": {
20
+ "packageName": "com.acme.inventory",
21
+ "miniappName": "Acme Inventory",
22
+ "versionName": "2.0.1",
23
+ "start": "user",
24
+ "outcome": "started"
25
+ }
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": [
4
+ {
5
+ "schemaVersion": 1,
6
+ "recordId": "rec-photo-0001",
7
+ "phoneDeviceId": "phone-7f3a91",
8
+ "collectorSessionId": "cs-20261013-01",
9
+ "sequence": 3,
10
+ "observedAt": "2026-10-13T11:04:00Z",
11
+ "capabilities": ["glasses_connection", "photo_operations"],
12
+ "kind": "photo_operation",
13
+ "operationId": "op-photo-41c9",
14
+ "deviceBinding": {
15
+ "kind": "serial",
16
+ "manufacturer": "Mentra",
17
+ "product": "mentra_live",
18
+ "serial": "ML2A-0042-7731"
19
+ },
20
+ "payload": {
21
+ "stage": "captured"
22
+ }
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": [
4
+ {
5
+ "schemaVersion": 1,
6
+ "recordId": "rec-snap-0002",
7
+ "phoneDeviceId": "phone-7f3a91",
8
+ "collectorSessionId": "cs-20261013-01",
9
+ "sequence": 0,
10
+ "observedAt": "2026-10-13T11:00:00Z",
11
+ "capabilities": [
12
+ "glasses_connection",
13
+ "glasses_battery",
14
+ "glasses_versions",
15
+ "miniapp_execution",
16
+ "engine_active"
17
+ ],
18
+ "kind": "snapshot",
19
+ "deviceBinding": {
20
+ "kind": "provisional",
21
+ "bindingId": "bind-5c21e0",
22
+ "product": "mentra_live"
23
+ },
24
+ "payload": {
25
+ "phone": {
26
+ "model": "Pixel 8",
27
+ "os": "android",
28
+ "osVersion": "15",
29
+ "appVersion": "3.4.0",
30
+ "appBuild": "3400",
31
+ "engineVersion": "1.12.0",
32
+ "sdkVersion": "0.9.2"
33
+ },
34
+ "glasses": {
35
+ "model": "mentra_live",
36
+ "firmware": {
37
+ "apk": "1.7.3",
38
+ "mtk": "2.1.0",
39
+ "bes": "0.8.5"
40
+ },
41
+ "connection": {
42
+ "state": "connected",
43
+ "observedAt": "2026-10-13T11:00:00Z"
44
+ },
45
+ "battery": {
46
+ "state": "unsupported",
47
+ "observedAt": "2026-10-13T11:00:00Z"
48
+ }
49
+ }
50
+ }
51
+ }
52
+ ]
53
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": [
4
+ {
5
+ "schemaVersion": 1,
6
+ "recordId": "rec-snap-0001",
7
+ "phoneDeviceId": "phone-7f3a91",
8
+ "collectorSessionId": "cs-20261013-01",
9
+ "sequence": 0,
10
+ "observedAt": "2026-10-13T11:00:00Z",
11
+ "capabilities": [
12
+ "glasses_connection",
13
+ "glasses_battery",
14
+ "glasses_versions",
15
+ "miniapp_execution",
16
+ "engine_active"
17
+ ],
18
+ "kind": "snapshot",
19
+ "deviceBinding": {
20
+ "kind": "serial",
21
+ "manufacturer": "Mentra",
22
+ "product": "mentra_live",
23
+ "serial": "ML2A-0042-7731"
24
+ },
25
+ "payload": {
26
+ "phone": {
27
+ "model": "Pixel 8",
28
+ "os": "android",
29
+ "osVersion": "15",
30
+ "appVersion": "3.4.0",
31
+ "appBuild": "3400",
32
+ "engineVersion": "1.12.0",
33
+ "sdkVersion": "0.9.2"
34
+ },
35
+ "glasses": {
36
+ "model": "mentra_live",
37
+ "firmware": {
38
+ "apk": "1.7.3",
39
+ "mtk": "2.1.0",
40
+ "bes": "0.8.5"
41
+ },
42
+ "connection": {
43
+ "state": "connected",
44
+ "observedAt": "2026-10-13T11:00:00Z"
45
+ },
46
+ "battery": {
47
+ "state": "measured",
48
+ "observedAt": "2026-10-13T11:00:00Z",
49
+ "level": 76,
50
+ "charging": false
51
+ }
52
+ }
53
+ }
54
+ }
55
+ ]
56
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": [
4
+ {
5
+ "schemaVersion": 1,
6
+ "recordId": "rec-cp-0001",
7
+ "phoneDeviceId": "phone-7f3a91",
8
+ "collectorSessionId": "cs-20261013-01",
9
+ "sequence": 2,
10
+ "observedAt": "2026-10-13T11:03:10Z",
11
+ "capabilities": [
12
+ "glasses_connection",
13
+ "glasses_battery",
14
+ "glasses_versions",
15
+ "miniapp_execution",
16
+ "engine_active"
17
+ ],
18
+ "kind": "usage_checkpoint",
19
+ "payload": {
20
+ "sessions": [
21
+ {
22
+ "activity": "miniapp_execution",
23
+ "activitySessionId": "act-8d02",
24
+ "packageName": "com.mentra.notes",
25
+ "startedAt": "2026-10-13T11:02:10Z",
26
+ "elapsedMs": 60000,
27
+ "ended": false
28
+ }
29
+ ]
30
+ }
31
+ }
32
+ ]
33
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "records": [
4
+ {
5
+ "schemaVersion": 1,
6
+ "recordId": "rec-pack-0001",
7
+ "phoneDeviceId": "phone-7f3a91",
8
+ "collectorSessionId": "cs-20261013-01",
9
+ "sequence": 4,
10
+ "observedAt": "2026-10-13T11:03:10Z",
11
+ "capabilities": ["glasses_connection", "miniapp_execution", "app_foreground", "engine_active"],
12
+ "kind": "usage_checkpoint",
13
+ "payload": {
14
+ "sessions": [
15
+ {
16
+ "activity": "glasses_connection",
17
+ "activitySessionId": "act-conn-01",
18
+ "deviceBinding": {
19
+ "kind": "serial",
20
+ "manufacturer": "Mentra",
21
+ "product": "mentra_live",
22
+ "serial": "ML2A-0042-7731"
23
+ },
24
+ "startedAt": "2026-10-13T10:59:00Z",
25
+ "elapsedMs": 250000,
26
+ "ended": false
27
+ },
28
+ {
29
+ "activity": "miniapp_execution",
30
+ "activitySessionId": "act-8d02",
31
+ "packageName": "com.mentra.notes",
32
+ "startedAt": "2026-10-13T11:02:10Z",
33
+ "elapsedMs": 60000,
34
+ "ended": false
35
+ },
36
+ {
37
+ "activity": "miniapp_execution",
38
+ "activitySessionId": "act-8c91",
39
+ "packageName": "com.acme.inventory",
40
+ "startedAt": "2026-10-13T11:00:30Z",
41
+ "elapsedMs": 120000,
42
+ "ended": true
43
+ },
44
+ {
45
+ "activity": "app_foreground",
46
+ "activitySessionId": "act-fg-01",
47
+ "startedAt": "2026-10-13T10:58:30Z",
48
+ "elapsedMs": 280000,
49
+ "ended": false
50
+ },
51
+ {
52
+ "activity": "engine_active",
53
+ "activitySessionId": "act-eng-01",
54
+ "startedAt": "2026-10-13T08:00:00Z",
55
+ "elapsedMs": 10990000,
56
+ "ended": false
57
+ }
58
+ ]
59
+ }
60
+ }
61
+ ]
62
+ }
package/src/audio.ts CHANGED
@@ -56,6 +56,13 @@ export type UdpLivenessAckPayload = z.infer<typeof udpLivenessAckPayloadSchema>;
56
56
 
57
57
  // --- Result types -----------------------------------------------------------
58
58
 
59
+ /** Position on the phone's submitted audio stream, independent of provider latency. */
60
+ export const audioPositionSchema = z.object({
61
+ sessionTag: z.number().int().nonnegative(),
62
+ offsetMs: z.number().nonnegative(),
63
+ });
64
+ export type AudioPosition = z.infer<typeof audioPositionSchema>;
65
+
59
66
  export const transcriptionTokenSchema = z.object({
60
67
  text: z.string(),
61
68
  startMs: z.number(),
@@ -65,6 +72,7 @@ export const transcriptionTokenSchema = z.object({
65
72
  speaker: z.string().optional(),
66
73
  // per-token; mid-sentence language switches are possible
67
74
  detectedLanguage: z.string().optional(),
75
+ audioPosition: audioPositionSchema.optional(),
68
76
  });
69
77
  export type TranscriptionToken = z.infer<typeof transcriptionTokenSchema>;
70
78
 
@@ -88,6 +96,9 @@ export const transcriptionDataSchema = z.object({
88
96
 
89
97
  // Per-token detail for consumers that need it
90
98
  tokens: z.array(transcriptionTokenSchema),
99
+ // Internal result capability: missing timing on a positioned session must
100
+ // be withheld, while an older/unnegotiated session keeps legacy delivery.
101
+ frameTimelineVersion: z.literal(1).optional(),
91
102
 
92
103
  provider: z.string(),
93
104
  timestamp: z.number(),
package/src/fleet.ts ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * @fileoverview The Fleet report and acknowledgement contract.
3
+ *
4
+ * The Mentra App reports what a phone and its glasses did to the organization's Fleet service (MFM), through the
5
+ * phone's own Core. The reporter (via `@mentra/cloud-client`) and MFM both parse with these schemas, so this file is
6
+ * the one definition of what may cross that wire.
7
+ *
8
+ * Records are validated one at a time. The envelope accepts any record shape so that one bad record is rejected on
9
+ * its own, and never strands the rest of its batch.
10
+ */
11
+ import {z} from "zod"
12
+
13
+ export const FLEET_SCHEMA_VERSION = 1 as const
14
+
15
+ export const FLEET_LIMITS = {
16
+ maxRecordsPerBatch: 500,
17
+ maxBatchBytes: 1_048_576,
18
+ maxSessionsPerCheckpoint: 32,
19
+ maxSessionElapsedMs: 7 * 24 * 60 * 60 * 1000,
20
+ phoneQueueMaxRecords: 10_000,
21
+ phoneQueueMaxAgeMs: 7 * 24 * 60 * 60 * 1000,
22
+ replayWindowMs: 14 * 24 * 60 * 60 * 1000,
23
+ maxFutureSkewMs: 5 * 60 * 1000,
24
+ } as const
25
+
26
+ const FleetId = z
27
+ .string()
28
+ .min(1)
29
+ .max(128)
30
+ .regex(/^[A-Za-z0-9._:-]+$/)
31
+ const FLEET_UTC_TIME = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,3}))?Z$/
32
+
33
+ /**
34
+ * Whether `value` names a real instant. `Date.parse` alone is not enough: it normalizes a calendar-invalid date (such
35
+ * as `2025-02-30`) to a later instant, and accepts `24:00:00`. So the parsed instant is formatted back and must
36
+ * reproduce every field of the input, with a missing or short fraction read as milliseconds.
37
+ */
38
+ function isRoundTripUtcTime(value: string): boolean {
39
+ const match = FLEET_UTC_TIME.exec(value)
40
+ if (!match) return false
41
+ const instant = Date.parse(value)
42
+ if (Number.isNaN(instant)) return false
43
+ return new Date(instant).toISOString() === `${match[1]}.${(match[2] ?? "").padEnd(3, "0")}Z`
44
+ }
45
+
46
+ /** RFC 3339 UTC with a `Z`; a numeric offset, a calendar-invalid date, `24:00:00` and a leap second are refused. */
47
+ const FleetUtcTime = z.string().regex(FLEET_UTC_TIME).refine(isRoundTripUtcTime, "not a date")
48
+ const FleetText = z.string().min(1).max(128)
49
+
50
+ export const FleetDeviceBindingSchema = z.discriminatedUnion("kind", [
51
+ z.object({kind: z.literal("serial"), manufacturer: FleetText, product: FleetText, serial: FleetText}).strict(),
52
+ z.object({kind: z.literal("provisional"), bindingId: FleetId, product: FleetText}).strict(),
53
+ ])
54
+
55
+ export const FLEET_CAPABILITIES = [
56
+ "glasses_connection",
57
+ "glasses_battery",
58
+ "glasses_versions",
59
+ "miniapp_execution",
60
+ "app_foreground",
61
+ "engine_active",
62
+ "photo_operations",
63
+ ] as const
64
+
65
+ const Battery = z.discriminatedUnion("state", [
66
+ z
67
+ .object({
68
+ state: z.literal("measured"),
69
+ observedAt: FleetUtcTime,
70
+ level: z.number().int().min(0).max(100),
71
+ charging: z.boolean().optional(),
72
+ })
73
+ .strict(),
74
+ z.object({state: z.literal("unsupported"), observedAt: FleetUtcTime}).strict(),
75
+ z.object({state: z.literal("unknown"), observedAt: FleetUtcTime}).strict(),
76
+ ])
77
+
78
+ export const FleetSnapshotPayloadSchema = z
79
+ .object({
80
+ phone: z
81
+ .object({
82
+ model: FleetText.optional(),
83
+ os: z.enum(["ios", "android"]),
84
+ osVersion: FleetText,
85
+ appVersion: FleetText,
86
+ appBuild: FleetText,
87
+ engineVersion: FleetText.optional(),
88
+ sdkVersion: FleetText.optional(),
89
+ })
90
+ .strict(),
91
+ glasses: z
92
+ .object({
93
+ model: FleetText,
94
+ firmware: z
95
+ .object({
96
+ apk: FleetText.optional(),
97
+ mtk: FleetText.optional(),
98
+ bes: FleetText.optional(),
99
+ os: FleetText.optional(),
100
+ })
101
+ .strict()
102
+ .optional(),
103
+ connection: z
104
+ .object({state: z.enum(["connected", "disconnected", "connecting", "unknown"]), observedAt: FleetUtcTime})
105
+ .strict(),
106
+ battery: Battery.optional(),
107
+ })
108
+ .strict()
109
+ .optional(),
110
+ })
111
+ .strict()
112
+
113
+ export const FleetLaunchPayloadSchema = z
114
+ .object({
115
+ packageName: FleetText,
116
+ miniappName: FleetText.optional(),
117
+ versionName: FleetText.optional(),
118
+ start: z.enum(["user", "automatic"]),
119
+ outcome: z.enum(["started", "failed"]),
120
+ })
121
+ .strict()
122
+
123
+ export const FLEET_ACTIVITIES = ["glasses_connection", "miniapp_execution", "app_foreground", "engine_active"] as const
124
+ export const FleetActivityCheckpointSchema = z
125
+ .object({
126
+ activity: z.enum(FLEET_ACTIVITIES),
127
+ activitySessionId: FleetId,
128
+ packageName: FleetText.optional(),
129
+ deviceBinding: FleetDeviceBindingSchema.optional(),
130
+ startedAt: FleetUtcTime,
131
+ /** Cumulative monotonic elapsed time of this session, never wall-clock subtraction. */
132
+ elapsedMs: z.number().int().min(0).max(FLEET_LIMITS.maxSessionElapsedMs),
133
+ ended: z.boolean(),
134
+ })
135
+ .strict()
136
+ .superRefine((value, ctx) => {
137
+ if ((value.activity === "miniapp_execution") !== (value.packageName !== undefined))
138
+ ctx.addIssue({code: "custom", message: "packageName is required for, and only for, miniapp_execution"})
139
+ if ((value.activity === "glasses_connection") !== (value.deviceBinding !== undefined))
140
+ ctx.addIssue({code: "custom", message: "deviceBinding is required for, and only for, glasses_connection"})
141
+ })
142
+ export type FleetActivity = (typeof FLEET_ACTIVITIES)[number]
143
+ export const FleetUsagePayloadSchema = z
144
+ .object({
145
+ sessions: z.array(FleetActivityCheckpointSchema).min(1).max(FLEET_LIMITS.maxSessionsPerCheckpoint),
146
+ })
147
+ .strict()
148
+
149
+ export const FleetPhotoPayloadSchema = z
150
+ .object({
151
+ stage: z.enum(["requested", "captured", "delivered", "failed"]),
152
+ failedStage: z.enum(["requested", "captured", "delivered"]).optional(),
153
+ })
154
+ .strict()
155
+ .refine((v) => (v.stage === "failed") === (v.failedStage !== undefined), "failedStage goes with stage failed")
156
+
157
+ export const FleetCoverageGapPayloadSchema = z
158
+ .object({
159
+ reason: z.enum(["queue_dropped", "queue_expired", "clock_untrusted", "reporter_stopped", "record_rejected"]),
160
+ from: FleetUtcTime,
161
+ to: FleetUtcTime,
162
+ droppedRecordCount: z.number().int().min(0).optional(),
163
+ })
164
+ .strict()
165
+
166
+ const base = {
167
+ schemaVersion: z.literal(FLEET_SCHEMA_VERSION),
168
+ recordId: FleetId,
169
+ phoneDeviceId: FleetId,
170
+ collectorSessionId: FleetId,
171
+ sequence: z.number().int().min(0),
172
+ observedAt: FleetUtcTime,
173
+ management: z
174
+ .object({associationId: FleetId, generation: z.number().int().min(0)})
175
+ .strict()
176
+ .optional(),
177
+ capabilities: z.array(z.enum(FLEET_CAPABILITIES)).max(FLEET_CAPABILITIES.length),
178
+ }
179
+ export const FleetRecordSchema = z.discriminatedUnion("kind", [
180
+ z
181
+ .object({
182
+ ...base,
183
+ kind: z.literal("snapshot"),
184
+ deviceBinding: FleetDeviceBindingSchema.optional(),
185
+ payload: FleetSnapshotPayloadSchema,
186
+ })
187
+ .strict(),
188
+ z.object({...base, kind: z.literal("miniapp_launch"), payload: FleetLaunchPayloadSchema}).strict(),
189
+ z.object({...base, kind: z.literal("usage_checkpoint"), payload: FleetUsagePayloadSchema}).strict(),
190
+ z
191
+ .object({
192
+ ...base,
193
+ kind: z.literal("photo_operation"),
194
+ operationId: FleetId,
195
+ deviceBinding: FleetDeviceBindingSchema,
196
+ payload: FleetPhotoPayloadSchema,
197
+ })
198
+ .strict(),
199
+ z.object({...base, kind: z.literal("coverage_gap"), payload: FleetCoverageGapPayloadSchema}).strict(),
200
+ ]).superRefine((value, ctx) => {
201
+ // A snapshot's glasses belong to the pair its binding names, so the two come together.
202
+ if (value.kind === "snapshot" && (value.payload.glasses !== undefined) !== (value.deviceBinding !== undefined))
203
+ ctx.addIssue({
204
+ code: "custom",
205
+ path: ["deviceBinding"],
206
+ message: "deviceBinding is required for, and only for, a snapshot that includes glasses",
207
+ })
208
+ })
209
+ export type FleetRecord = z.infer<typeof FleetRecordSchema>
210
+ export type FleetDeviceBinding = z.infer<typeof FleetDeviceBindingSchema>
211
+
212
+ /** The envelope. Records are validated one at a time, so one bad record cannot strand a batch. */
213
+ export const FleetReportSchema = z
214
+ .object({
215
+ schemaVersion: z.literal(FLEET_SCHEMA_VERSION),
216
+ records: z.array(z.unknown()).min(1).max(FLEET_LIMITS.maxRecordsPerBatch),
217
+ })
218
+ .strict()
219
+ export type FleetReport = {schemaVersion: 1; records: FleetRecord[]}
220
+
221
+ export const FLEET_REJECT_REASONS = [
222
+ "schema_invalid",
223
+ "unsupported_schema_version",
224
+ "content_mismatch",
225
+ "outside_replay_window",
226
+ "timestamp_out_of_bounds",
227
+ "account_deleted",
228
+ ] as const
229
+ export const FleetRecordResultSchema = z
230
+ .object({
231
+ index: z.number().int().min(0),
232
+ recordId: FleetId.optional(),
233
+ status: z.enum(["accepted", "duplicate", "retryable", "rejected"]),
234
+ reasonCode: z.enum(FLEET_REJECT_REASONS).optional(),
235
+ retryAfterSeconds: z.number().int().min(0).optional(),
236
+ })
237
+ .strict()
238
+ .superRefine((value, ctx) => {
239
+ if ((value.status === "rejected") !== (value.reasonCode !== undefined))
240
+ ctx.addIssue({code: "custom", message: "reasonCode is required for, and only for, status rejected"})
241
+ })
242
+ export const FleetReportAckSchema = z
243
+ .object({
244
+ schemaVersion: z.literal(FLEET_SCHEMA_VERSION),
245
+ results: z.array(FleetRecordResultSchema),
246
+ })
247
+ .strict()
248
+ export type FleetRecordResult = z.infer<typeof FleetRecordResultSchema>
249
+ export type FleetReportAck = z.infer<typeof FleetReportAckSchema>
250
+
251
+ /** Whole-response error codes Core and MFM answer with. */
252
+ export const FLEET_NOT_INSTALLED = "fleet_not_installed"
253
+ export const FLEET_UNAVAILABLE = "fleet_unavailable"
package/src/handshake.ts CHANGED
@@ -26,9 +26,12 @@ export const connectionInitPayloadSchema = z.object({
26
26
  .object({
27
27
  codec: z.enum(["lc3", "pcm"]),
28
28
  sampleRate: z.number().int(), // e.g. 16000
29
+ frameTimelineVersion: z.literal(1).optional(),
29
30
  // LC3 bitrate / frame size in bytes. Only meaningful when codec is
30
31
  // "lc3"; omitted PCM sessions keep treating payload bytes as PCM.
31
- frameSizeBytes: z.union([z.literal(20), z.literal(40), z.literal(60)]).optional(),
32
+ frameSizeBytes: z
33
+ .union([z.literal(20), z.literal(40), z.literal(60)])
34
+ .optional(),
32
35
  // Optional initial subscription set, seeded atomically with the session
33
36
  // so audio that starts before the first REST update is not transcribed
34
37
  // with an empty set.
@@ -47,6 +50,7 @@ export const connectionAckPayloadSchema = z.object({
47
50
  audio: z
48
51
  .object({
49
52
  sessionTag: z.number().int(), // u32 stamped into UDP audio frames
53
+ frameTimelineVersion: z.literal(1).optional(),
50
54
  udp: z.object({ host: z.string(), port: z.number().int() }),
51
55
  // Per-session key for encrypting UDP audio. Delivered here because the
52
56
  // handshake is over the TLS WebSocket.
package/src/index.ts CHANGED
@@ -16,3 +16,5 @@ export * from "./audio";
16
16
  export * from "./languages";
17
17
  export * from "./camera";
18
18
  export * from "./maps";
19
+ export * from "./report-automation";
20
+ export * from "./fleet";
@@ -0,0 +1,20 @@
1
+ /** Total automated incident submission and required device collection budget. */
2
+ export const AUTOMATED_INCIDENT_TIMEOUT_MS = 120_000;
3
+
4
+ /** Identity of the single device incident request saved by an automated run.
5
+ * This binds report creation, independently of transport or collection completion.
6
+ */
7
+ export interface ReportAutomationCorrelation {
8
+ alertId: string;
9
+ testRunId: string;
10
+ }
11
+
12
+ /** Preserve exact request IDs; never normalize or truncate correlation. */
13
+ export function reportAutomationCorrelation(value: unknown): ReportAutomationCorrelation | null {
14
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
15
+ const row = value as Record<string, unknown>;
16
+ if (Object.keys(row).length !== 2 || typeof row.alertId !== 'string'
17
+ || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/.test(row.alertId)
18
+ || typeof row.testRunId !== 'string' || !row.testRunId.trim() || row.testRunId.length > 160) return null;
19
+ return {alertId: row.alertId, testRunId: row.testRunId};
20
+ }