authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 15:13:48-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 15:13:48-07:00
log97de7d7e1535ddf485ea8806c639f27e99cfbcb2
tree2d576773da8b048b3a1d60773c5d05c73bc8bea6
parentd7718e81e0af33d0a2d0f515385ac210f352fffb
signature Signed by PGP key B5690EEEBB952194

[codex] Structure agent awareness registration errors (#3328)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 110 insertions(+), 20 deletions(-)

apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts+23
...@@ -18,6 +18,7 @@ import { cryptoLayer } from "../cloud/dpop";...@@ -18,6 +18,7 @@ import { cryptoLayer } from "../cloud/dpop";
18import { managedRelayClientLayer } from "../cloud/managedRelayLayer";18import { managedRelayClientLayer } from "../cloud/managedRelayLayer";
19import { makeRelayDeviceRegistrationRequest } from "./registrationPayload";19import { makeRelayDeviceRegistrationRequest } from "./registrationPayload";
20import {20import {
21 AgentAwarenessOperationError,
21 __resetAgentAwarenessRemoteRegistrationForTest,22 __resetAgentAwarenessRemoteRegistrationForTest,
22 refreshActiveLiveActivityRemoteRegistration,23 refreshActiveLiveActivityRemoteRegistration,
23 refreshAgentAwarenessRegistration,24 refreshAgentAwarenessRegistration,
...@@ -267,6 +268,28 @@ describe("makeRelayDeviceRegistrationRequest", () => {...@@ -267,6 +268,28 @@ describe("makeRelayDeviceRegistrationRequest", () => {
267 }).pipe(Effect.provide(relayTestLayer));268 }).pipe(Effect.provide(relayTestLayer));
268 });269 });
269270
271 it.effect("preserves Live Activity push-token lookup failures", () => {
272 const cause = new Error("native token lookup failed");
273 const activity = {
274 getPushToken: vi.fn(() => Promise.reject(cause)),
275 addPushTokenListener: vi.fn(),
276 };
277
278 return Effect.gen(function* () {
279 const error = yield* Effect.flip(
280 registerLiveActivityPushToken({ activity: activity as never }),
281 );
282
283 expect(error).toBeInstanceOf(AgentAwarenessOperationError);
284 expect(error).toMatchObject({
285 _tag: "AgentAwarenessOperationError",
286 operation: "read-live-activity-push-token",
287 cause,
288 message: "Agent awareness operation read-live-activity-push-token failed.",
289 });
290 }).pipe(Effect.provide(relayTestLayer));
291 });
292
270 it.effect(293 it.effect(
271 "reports Live Activity token registration as skipped when relay auth is unavailable",294 "reports Live Activity token registration as skipped when relay auth is unavailable",
272 () => {295 () => {
apps/mobile/src/features/agent-awareness/remoteRegistration.ts+87-20
...@@ -2,6 +2,7 @@ import { addPushToStartTokenListener, type LiveActivity } from "expo-widgets";...@@ -2,6 +2,7 @@ import { addPushToStartTokenListener, type LiveActivity } from "expo-widgets";
2import Constants from "expo-constants";2import Constants from "expo-constants";
3import * as Notifications from "expo-notifications";3import * as Notifications from "expo-notifications";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as Schema from "effect/Schema";
5import { Platform } from "react-native";6import { Platform } from "react-native";
6import type { EnvironmentId } from "@t3tools/contracts";7import type { EnvironmentId } from "@t3tools/contracts";
7import {8import {
...@@ -28,6 +29,33 @@ import { resolveCloudPublicConfig } from "../cloud/publicConfig";...@@ -28,6 +29,33 @@ import { resolveCloudPublicConfig } from "../cloud/publicConfig";
28import { makeRelayDeviceRegistrationRequest } from "./registrationPayload";29import { makeRelayDeviceRegistrationRequest } from "./registrationPayload";
2930
30const REMOTE_ACTIVITY_REGISTRATION_RETRY_MS = 15_000;31const REMOTE_ACTIVITY_REGISTRATION_RETRY_MS = 15_000;
32
33const AgentAwarenessOperation = Schema.Literals([
34 "read-notification-permissions",
35 "read-native-push-token",
36 "read-device-registration-relay-token",
37 "read-device-unregistration-relay-token",
38 "read-live-activity-registration-relay-token",
39 "load-device-registration-identifier",
40 "load-device-registration-preferences",
41 "load-device-unregistration-identifier",
42 "read-live-activity-push-token",
43 "load-live-activity-registration-identifier",
44 "list-active-live-activities",
45]);
46
47export class AgentAwarenessOperationError extends Schema.TaggedErrorClass<AgentAwarenessOperationError>()(
48 "AgentAwarenessOperationError",
49 {
50 operation: AgentAwarenessOperation,
51 cause: Schema.Defect(),
52 },
53) {
54 override get message(): string {
55 return `Agent awareness operation ${this.operation} failed.`;
56 }
57}
58
31const environmentConnections = new Map<EnvironmentId, SavedRemoteConnection>();59const environmentConnections = new Map<EnvironmentId, SavedRemoteConnection>();
32const activityPushTokenListeners = new WeakSet<LiveActivity<AgentActivityProps>>();60const activityPushTokenListeners = new WeakSet<LiveActivity<AgentActivityProps>>();
33let pushToStartSubscription: { remove: () => void } | null = null;61let pushToStartSubscription: { remove: () => void } | null = null;
...@@ -137,14 +165,22 @@ function nativePushTokenRegistration(observedPushToken?: string) {...@@ -137,14 +165,22 @@ function nativePushTokenRegistration(observedPushToken?: string) {
137 }165 }
138 const permissions = yield* Effect.tryPromise({166 const permissions = yield* Effect.tryPromise({
139 try: () => Notifications.getPermissionsAsync(),167 try: () => Notifications.getPermissionsAsync(),
140 catch: (error) => error,168 catch: (cause) =>
169 new AgentAwarenessOperationError({
170 operation: "read-notification-permissions",
171 cause,
172 }),
141 });173 });
142 if (!permissions.granted) {174 if (!permissions.granted) {
143 return { notificationsEnabled: false, pushToken: null };175 return { notificationsEnabled: false, pushToken: null };
144 }176 }
145 const token = yield* Effect.tryPromise({177 const token = yield* Effect.tryPromise({
146 try: () => Notifications.getDevicePushTokenAsync(),178 try: () => Notifications.getDevicePushTokenAsync(),
147 catch: (error) => error,179 catch: (cause) =>
180 new AgentAwarenessOperationError({
181 operation: "read-native-push-token",
182 cause,
183 }),
148 }).pipe(184 }).pipe(
149 Effect.tapError((error) =>185 Effect.tapError((error) =>
150 Effect.sync(() => {186 Effect.sync(() => {
...@@ -161,16 +197,19 @@ function nativePushTokenRegistration(observedPushToken?: string) {...@@ -161,16 +197,19 @@ function nativePushTokenRegistration(observedPushToken?: string) {
161 });197 });
162}198}
163199
164const relayToken = Effect.gen(function* () {200const relayToken = (
165 const provider = relayTokenProvider;201 operation: "read-device-registration-relay-token" | "read-live-activity-registration-relay-token",
166 if (!provider) {202) =>
167 return null;203 Effect.gen(function* () {
168 }204 const provider = relayTokenProvider;
169 return yield* Effect.tryPromise({205 if (!provider) {
170 try: provider,206 return null;
171 catch: (error) => error,207 }
208 return yield* Effect.tryPromise({
209 try: provider,
210 catch: (cause) => new AgentAwarenessOperationError({ operation, cause }),
211 });
172 });212 });
173});
174213
175function registerDeviceWithRelay(214function registerDeviceWithRelay(
176 body: RelayDeviceRegistrationRequest,215 body: RelayDeviceRegistrationRequest,
...@@ -185,7 +224,7 @@ function registerDeviceWithRelay(...@@ -185,7 +224,7 @@ function registerDeviceWithRelay(
185 return;224 return;
186 }225 }
187 if (!readRelayConfig()) return;226 if (!readRelayConfig()) return;
188 const token = yield* relayToken;227 const token = yield* relayToken("read-device-registration-relay-token");
189 if (expectedGeneration !== deviceRegistrationGeneration) {228 if (expectedGeneration !== deviceRegistrationGeneration) {
190 logRegistrationDebug("device registration cancelled after auth lookup", {229 logRegistrationDebug("device registration cancelled after auth lookup", {
191 expectedGeneration,230 expectedGeneration,
...@@ -220,7 +259,11 @@ function unregisterDeviceWithRelay(input: {...@@ -220,7 +259,11 @@ function unregisterDeviceWithRelay(input: {
220 if (!readRelayConfig()) return;259 if (!readRelayConfig()) return;
221 const token = yield* Effect.tryPromise({260 const token = yield* Effect.tryPromise({
222 try: input.tokenProvider,261 try: input.tokenProvider,
223 catch: (error) => error,262 catch: (cause) =>
263 new AgentAwarenessOperationError({
264 operation: "read-device-unregistration-relay-token",
265 cause,
266 }),
224 });267 });
225 if (!token) {268 if (!token) {
226 logRegistrationDebug("relay device unregistration skipped; user is not signed in");269 logRegistrationDebug("relay device unregistration skipped; user is not signed in");
...@@ -240,7 +283,7 @@ function registerLiveActivityWithRelay(...@@ -240,7 +283,7 @@ function registerLiveActivityWithRelay(
240): Effect.Effect<boolean, unknown, ManagedRelay.ManagedRelayClient> {283): Effect.Effect<boolean, unknown, ManagedRelay.ManagedRelayClient> {
241 return Effect.gen(function* () {284 return Effect.gen(function* () {
242 if (!readRelayConfig()) return false;285 if (!readRelayConfig()) return false;
243 const token = yield* relayToken;286 const token = yield* relayToken("read-live-activity-registration-relay-token");
244 if (!token) {287 if (!token) {
245 logRegistrationDebug("relay live activity registration skipped; user is not signed in");288 logRegistrationDebug("relay live activity registration skipped; user is not signed in");
246 return false;289 return false;
...@@ -381,11 +424,19 @@ function registerDevice(...@@ -381,11 +424,19 @@ function registerDevice(
381 const [deviceId, preferences] = yield* Effect.all([424 const [deviceId, preferences] = yield* Effect.all([
382 Effect.tryPromise({425 Effect.tryPromise({
383 try: () => loadOrCreateAgentAwarenessDeviceId(),426 try: () => loadOrCreateAgentAwarenessDeviceId(),
384 catch: (error) => error,427 catch: (cause) =>
428 new AgentAwarenessOperationError({
429 operation: "load-device-registration-identifier",
430 cause,
431 }),
385 }),432 }),
386 Effect.tryPromise({433 Effect.tryPromise({
387 try: () => loadPreferences(),434 try: () => loadPreferences(),
388 catch: (error) => error,435 catch: (cause) =>
436 new AgentAwarenessOperationError({
437 operation: "load-device-registration-preferences",
438 cause,
439 }),
389 }),440 }),
390 ]);441 ]);
391 const pushTokenRegistration = yield* nativePushTokenRegistration(input?.observedPushToken);442 const pushTokenRegistration = yield* nativePushTokenRegistration(input?.observedPushToken);
...@@ -519,7 +570,11 @@ export function unregisterAgentAwarenessDeviceForCurrentUser(...@@ -519,7 +570,11 @@ export function unregisterAgentAwarenessDeviceForCurrentUser(
519 return Effect.gen(function* () {570 return Effect.gen(function* () {
520 const deviceId = yield* Effect.tryPromise({571 const deviceId = yield* Effect.tryPromise({
521 try: () => loadAgentAwarenessDeviceId(),572 try: () => loadAgentAwarenessDeviceId(),
522 catch: (error) => error,573 catch: (cause) =>
574 new AgentAwarenessOperationError({
575 operation: "load-device-unregistration-identifier",
576 cause,
577 }),
523 });578 });
524 if (!deviceId) {579 if (!deviceId) {
525 return;580 return;
...@@ -544,7 +599,11 @@ export function registerLiveActivityPushToken(input: {...@@ -544,7 +599,11 @@ export function registerLiveActivityPushToken(input: {
544599
545 const activityPushToken = yield* Effect.tryPromise({600 const activityPushToken = yield* Effect.tryPromise({
546 try: () => input.activity.getPushToken(),601 try: () => input.activity.getPushToken(),
547 catch: (error) => error,602 catch: (cause) =>
603 new AgentAwarenessOperationError({
604 operation: "read-live-activity-push-token",
605 cause,
606 }),
548 });607 });
549 if (!activityPushToken) {608 if (!activityPushToken) {
550 if (activityPushTokenListeners.has(input.activity)) {609 if (activityPushTokenListeners.has(input.activity)) {
...@@ -592,7 +651,11 @@ function registerLiveActivityPushTokenValue(input: {...@@ -592,7 +651,11 @@ function registerLiveActivityPushTokenValue(input: {
592 return Effect.gen(function* () {651 return Effect.gen(function* () {
593 const deviceId = yield* Effect.tryPromise({652 const deviceId = yield* Effect.tryPromise({
594 try: () => loadOrCreateAgentAwarenessDeviceId(),653 try: () => loadOrCreateAgentAwarenessDeviceId(),
595 catch: (error) => error,654 catch: (cause) =>
655 new AgentAwarenessOperationError({
656 operation: "load-live-activity-registration-identifier",
657 cause,
658 }),
596 });659 });
597 const registered = yield* registerLiveActivityWithRelay({660 const registered = yield* registerLiveActivityWithRelay({
598 deviceId,661 deviceId,
...@@ -633,7 +696,11 @@ export function refreshActiveLiveActivityRemoteRegistration(): Effect.Effect<...@@ -633,7 +696,11 @@ export function refreshActiveLiveActivityRemoteRegistration(): Effect.Effect<
633696
634 const activities = yield* Effect.try({697 const activities = yield* Effect.try({
635 try: () => AgentActivity.getInstances(),698 try: () => AgentActivity.getInstances(),
636 catch: (error) => error,699 catch: (cause) =>
700 new AgentAwarenessOperationError({
701 operation: "list-active-live-activities",
702 cause,
703 }),
637 }).pipe(704 }).pipe(
638 Effect.catch((error) =>705 Effect.catch((error) =>
639 Effect.sync(() => {706 Effect.sync(() => {