Skip to content

Improve live activity routing and diagnostics - #3685

Merged
juliusmarminge merged 26 commits into
mainfrom
t3code/live-activities-improvements
Jul 7, 2026
Merged

Improve live activity routing and diagnostics#3685
juliusmarminge merged 26 commits into
mainfrom
t3code/live-activities-improvements

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Jul 4, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds relay-aware APNs routing for mobile registrations, including bundle ID and sandbox vs production environment detection.
  • Improves Live Activity reconciliation by re-registering tokens on foreground and cleaning up orphaned activities on sign-out.
  • Reworks the Agent Activity widget to surface per-row status, safer deep links, and better overflow handling.
  • Adds a new Push Delivery diagnostics section in Settings so users can inspect relay registration and last delivery state.
  • Expands relay-side device diagnostics, APNs delivery tracking, and persistence to improve observability and routing accuracy.

Testing

  • vp check
  • vp run typecheck
  • vp test
  • Added/updated tests for mobile diagnostics, remote registration, widget rendering, and relay APNs delivery/device tracking.
  • Not run: manual device or APNs end-to-end verification

Note

High Risk
Touches relay registration, APNs routing, cloud link modes, and Live Activity lifecycle (including removing push-to-start); misconfiguration or partial deploy could break mobile notifications or mis-route pushes.

Overview
Improves mobile agent awareness end-to-end: relay device registration now sends bundle ID and sandbox vs production APNs routing, tracks whether the relay actually accepted registration (settings toggles and alerts follow that, not just iOS permission), skips duplicate registrations via a persisted signature, and re-registers Live Activity tokens on foreground with short-window deduping. Push-to-start registration is removed in favor of arming a card when the user sends a task (and priming from a relay activity snapshot when idle), plus cleanup on cloud sign-out and a releaseAgentAwarenessRelayTokenProvider path so auth UI remounts do not wipe registration or end activities.

Agent Activity Live Activity UI is reworked (attention-first rows, phase tints aligned with web, deep links, branded T3 mark via new Expo config plugins and actool shell phase). expo-widgets gains frequentUpdates for higher update budget.

T3 Connect vs publishing split: environments can link in publish_only mode (manual endpoint, no Cloudflare tunnel) via CLI (connect link --publish-only, connect publish) and web settings (separate toggles reconciled together). Link proofs accept manual providers with activity-only scopes; server publish path defers transient tombstones and first completed snapshots before publishing.

Relay DB adds bundle_id and aps_environment on mobile devices (migration in diff).

Reviewed by Cursor Bugbot for commit 6f0b32d. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add per-device APNs routing, JWT token caching, and publish-only link mode to Live Activity delivery

  • Stores bundle_id and aps_environment per device in the relay and uses them to route APNs pushes to the correct topic and environment per device rather than using global config.
  • Introduces ApnsProviderTokens service that caches APNs JWTs quantized to 45-minute windows, preventing TooManyProviderTokenUpdates errors; uses deterministic ES256 signing via @noble/curves.
  • Overhauls Live Activity delivery: activities are no longer remotely started; ends are suppressed within a grace window after arming; attention-phase transitions and newly-terminal rows can now trigger alert payloads with haptics; stale terminal aggregates suppress re-alerting.
  • Adds a publish_only cloud link mode in the CLI (t3 connect publish) and web settings UI, allowing agent activity publishing without a managed Cloudflare tunnel; linking with this mode uses providerKind: "manual" and deprovisions any existing tunnel allocation.
  • Reworks the AgentActivity widget to sort rows attention-first, apply per-phase tints from the web palette, deep-link to the most relevant thread, show a branded logo, and display a phase glyph in minimal presentation.
  • Exposes relay registration status (unknown/pending/registered/failed) to the settings UI so notification and Live Activity toggles only appear enabled when the device is actually registered.
  • Risk: makeAggregateState now requires a nowMs parameter and statusForPhase('starting') returns 'Connecting' instead of 'Starting', which are breaking changes for callers.

Macroscope summarized 6f0b32d.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fe216c9a-1cd9-460a-a80d-6d90748a00b5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/live-activities-improvements

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Jul 4, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Missing database migration columns
    • Added the drizzle-kit-generated Postgres migration (migration.sql + snapshot.json) adding bundle_id and aps_environment to relay_mobile_devices, chained onto the previous snapshot.
  • ✅ Fixed: Last delivery ignores HTTP status
    • The Last Delivery row now treats a non-2xx lastDeliveryStatus as a failure even when lastDeliveryError is null, showing "Delivery failed (status)" instead of "Delivered".
  • ✅ Fixed: Diagnostics use global attempt window
    • Replaced the global 100-row window with a DISTINCT ON (device_id) query ordered by created_at DESC so each device's true latest delivery attempt is always returned.

Create PR

Or push these changes by commenting:

@cursor push a97e0b6881
Preview (a97e0b6881)
diff --git a/apps/mobile/src/features/agent-awareness/deviceDiagnostics.test.ts b/apps/mobile/src/features/agent-awareness/deviceDiagnostics.test.ts
--- a/apps/mobile/src/features/agent-awareness/deviceDiagnostics.test.ts
+++ b/apps/mobile/src/features/agent-awareness/deviceDiagnostics.test.ts
@@ -69,6 +69,28 @@
     ]);
   });
 
+  it("treats a non-2xx delivery status without a reason as a failure", () => {
+    const rows = formatDeviceDiagnosticsRows(
+      makeDevice({
+        bundleId: "com.t3tools.t3code.preview",
+        apsEnvironment: "production",
+        hasPushToken: true,
+        hasPushToStartToken: true,
+        hasLiveActivityToken: true,
+        lastDeliveryAt: "2026-06-05T01:02:59.566Z",
+        lastDeliveryKind: "live_activity_end",
+        lastDeliveryStatus: 400,
+        lastDeliveryError: null,
+      }),
+    );
+
+    expect(rows[4]).toEqual({
+      label: "Last Delivery",
+      value: "Delivery failed (400)",
+      tone: "warn",
+    });
+  });
+
   it("reports healthy registrations with a successful delivery", () => {
     const rows = formatDeviceDiagnosticsRows(
       makeDevice({

diff --git a/apps/mobile/src/features/agent-awareness/deviceDiagnostics.ts b/apps/mobile/src/features/agent-awareness/deviceDiagnostics.ts
--- a/apps/mobile/src/features/agent-awareness/deviceDiagnostics.ts
+++ b/apps/mobile/src/features/agent-awareness/deviceDiagnostics.ts
@@ -73,14 +73,21 @@
     });
   }
 
+  // APNs can fail with a non-2xx status without a reason body, so a null
+  // error alone does not mean the delivery succeeded.
+  const deliveryFailed =
+    diagnostics.lastDeliveryError !== null ||
+    (diagnostics.lastDeliveryStatus !== null &&
+      (diagnostics.lastDeliveryStatus < 200 || diagnostics.lastDeliveryStatus >= 300));
+
   if (diagnostics.lastDeliveryAt === null) {
     rows.push({ label: "Last Delivery", value: "None yet", tone: "muted" });
-  } else if (diagnostics.lastDeliveryError !== null) {
+  } else if (deliveryFailed) {
     const status =
       diagnostics.lastDeliveryStatus === null ? "" : ` (${diagnostics.lastDeliveryStatus})`;
     rows.push({
       label: "Last Delivery",
-      value: `${diagnostics.lastDeliveryError}${status}`,
+      value: `${diagnostics.lastDeliveryError ?? "Delivery failed"}${status}`,
       tone: "warn",
     });
   } else {

diff --git a/infra/relay/migrations/postgres/20260704031500_add_mobile_device_apns_route/migration.sql b/infra/relay/migrations/postgres/20260704031500_add_mobile_device_apns_route/migration.sql
new file mode 100644
--- /dev/null
+++ b/infra/relay/migrations/postgres/20260704031500_add_mobile_device_apns_route/migration.sql
@@ -1,0 +1,3 @@
+ALTER TABLE "relay_mobile_devices" ADD COLUMN "bundle_id" varchar(255);
+--> statement-breakpoint
+ALTER TABLE "relay_mobile_devices" ADD COLUMN "aps_environment" varchar(16);

diff --git a/infra/relay/migrations/postgres/20260704031500_add_mobile_device_apns_route/snapshot.json b/infra/relay/migrations/postgres/20260704031500_add_mobile_device_apns_route/snapshot.json
new file mode 100644
--- /dev/null
+++ b/infra/relay/migrations/postgres/20260704031500_add_mobile_device_apns_route/snapshot.json
@@ -1,0 +1,1479 @@
+{
+  "dialect": "postgres",
+  "id": "e00cc7df-a31e-4b8b-b258-d04e7db7758a",
+  "prevIds": ["385d476b-d4f6-48a3-99e6-0a95af4ee4e4"],
+  "version": "8",
+  "ddl": [
+    {
+      "isRlsEnabled": false,
+      "name": "relay_agent_activity_rows",
+      "entityType": "tables",
+      "schema": "public"
+    },
+    {
+      "isRlsEnabled": false,
+      "name": "relay_delivery_attempts",
+      "entityType": "tables",
+      "schema": "public"
+    },
+    {
+      "isRlsEnabled": false,
+      "name": "relay_dpop_proofs",
+      "entityType": "tables",
+      "schema": "public"
+    },
+    {
+      "isRlsEnabled": false,
+      "name": "relay_environment_credentials",
+      "entityType": "tables",
+      "schema": "public"
+    },
+    {
+      "isRlsEnabled": false,
+      "name": "relay_environment_links",
+      "entityType": "tables",
+      "schema": "public"
+    },
+    {
+      "isRlsEnabled": false,
+      "name": "relay_live_activities",
+      "entityType": "tables",
+      "schema": "public"
+    },
+    {
+      "isRlsEnabled": false,
+      "name": "relay_managed_endpoint_allocations",
+      "entityType": "tables",
+      "schema": "public"
+    },
+    {
+      "isRlsEnabled": false,
+      "name": "relay_mobile_devices",
+      "entityType": "tables",
+      "schema": "public"
+    },
+    {
+      "type": "varchar(191)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "environment_id",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_agent_activity_rows"
+    },
+    {
+      "type": "text",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "environment_public_key",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_agent_activity_rows"
+    },
+    {
+      "type": "varchar(191)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "thread_id",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_agent_activity_rows"
+    },
+    {
+      "type": "jsonb",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "state_json",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_agent_activity_rows"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "updated_at",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_agent_activity_rows"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "created_at",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_agent_activity_rows"
+    },
+    {
+      "type": "varchar(36)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "id",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_delivery_attempts"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "created_at",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_delivery_attempts"
+    },
+    {
+      "type": "varchar(255)",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "user_id",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_delivery_attempts"
+    },
+    {
+      "type": "varchar(191)",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "environment_id",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_delivery_attempts"
+    },
+    {
+      "type": "varchar(191)",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "thread_id",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_delivery_attempts"
+    },
+    {
+      "type": "varchar(255)",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "device_id",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_delivery_attempts"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "kind",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_delivery_attempts"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "source_job_id",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_delivery_attempts"
+    },
+    {
+      "type": "varchar(16)",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "token_suffix",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_delivery_attempts"
+    },
+    {
+      "type": "integer",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "apns_status",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_delivery_attempts"
+    },
+    {
+      "type": "text",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "apns_reason",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_delivery_attempts"
+    },
+    {
+      "type": "varchar(128)",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "apns_id",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_delivery_attempts"
+    },
+    {
+      "type": "text",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "transport_error",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_delivery_attempts"
+    },
+    {
+      "type": "varchar(128)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "thumbprint",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_dpop_proofs"
+    },
+    {
+      "type": "varchar(255)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "jti",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_dpop_proofs"
+    },
+    {
+      "type": "integer",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "iat",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_dpop_proofs"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "expires_at",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_dpop_proofs"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "created_at",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_dpop_proofs"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "credential_id",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_credentials"
+    },
+    {
+      "type": "varchar(191)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "environment_id",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_credentials"
+    },
+    {
+      "type": "text",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "environment_public_key",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_credentials"
+    },
+    {
+      "type": "varchar(191)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "credential_hash",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_credentials"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "revoked_at",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_credentials"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "created_at",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_credentials"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "updated_at",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_credentials"
+    },
+    {
+      "type": "varchar(191)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "user_id",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_links"
+    },
+    {
+      "type": "varchar(191)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "environment_id",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_links"
+    },
+    {
+      "type": "text",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": "'T3 Environment'",
+      "generated": null,
+      "identity": null,
+      "name": "environment_label",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_links"
+    },
+    {
+      "type": "text",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "environment_public_key",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_links"
+    },
+    {
+      "type": "text",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "endpoint_http_base_url",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_links"
+    },
+    {
+      "type": "text",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "endpoint_ws_base_url",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_links"
+    },
+    {
+      "type": "varchar(32)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "endpoint_provider_kind",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_links"
+    },
+    {
+      "type": "boolean",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": "true",
+      "generated": null,
+      "identity": null,
+      "name": "notifications_enabled",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_links"
+    },
+    {
+      "type": "boolean",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": "true",
+      "generated": null,
+      "identity": null,
+      "name": "live_activities_enabled",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_links"
+    },
+    {
+      "type": "boolean",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": "false",
+      "generated": null,
+      "identity": null,
+      "name": "managed_tunnels_enabled",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_links"
+    },
+    {
+      "type": "varchar(191)",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "created_by_device_id",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_links"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "revoked_at",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_links"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "created_at",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_links"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "updated_at",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_environment_links"
+    },
+    {
+      "type": "varchar(255)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "user_id",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_live_activities"
+    },
+    {
+      "type": "varchar(255)",
+      "typeSchema": null,
+      "notNull": true,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "device_id",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_live_activities"
+    },
+    {
+      "type": "text",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "activity_push_token",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_live_activities"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "remote_start_queued_at",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_live_activities"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "remote_started_at",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_live_activities"
+    },
+    {
+      "type": "varchar(64)",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
+      "name": "ended_at",
+      "entityType": "columns",
+      "schema": "public",
+      "table": "relay_live_activities"
+    },
+    {
+      "type": "jsonb",
+      "typeSchema": null,
+      "notNull": false,
+      "dimensions": 0,
+      "default": null,
+      "generated": null,
+      "identity": null,
... diff truncated: showing 800 of 1681 lines

You can send follow-ups to the cloud agent here.

Comment thread infra/relay/src/persistence/schema.ts
Comment thread apps/mobile/src/features/agent-awareness/deviceDiagnostics.ts Outdated
Comment thread infra/relay/src/agentActivity/Devices.ts Outdated
platform: varchar("platform", { length: 16 }).notNull().$type<"ios">(),
iosMajorVersion: integer("ios_major_version").notNull(),
appVersion: varchar("app_version", { length: 64 }),
bundleId: varchar("bundle_id", { length: 255 }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High persistence/schema.ts:27

The new bundle_id and aps_environment columns are added to the relay_mobile_devices schema, but no SQL migration is included to create them. After deploy, the code in Devices.ts immediately inserts and selects these columns, so PostgreSQL rejects every device registration and list query with column "bundle_id" does not exist (and aps_environment likewise). Add the corresponding ALTER TABLE relay_mobile_devices ADD COLUMN ... migration under infra/relay/migrations/ so the columns exist before the new code runs.

Also found in 1 other location(s)

infra/relay/src/agentActivity/LiveActivities.ts:201

The new relayMobileDevices.bundleId / relayMobileDevices.apsEnvironment columns are referenced by listTargets (bundle_id, aps_environment) and other relay code, but this PR does not add a matching SQL migration for relay_mobile_devices. After deploying the code to an existing database, any listTargets query will start failing with column relay_mobile_devices.bundle_id does not exist (and likewise for aps_environment), breaking live-activity target lookup until the schema is manually patched.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @infra/relay/src/persistence/schema.ts around line 27:

The new `bundle_id` and `aps_environment` columns are added to the `relay_mobile_devices` schema, but no SQL migration is included to create them. After deploy, the code in `Devices.ts` immediately inserts and selects these columns, so PostgreSQL rejects every device registration and list query with `column "bundle_id" does not exist` (and `aps_environment` likewise). Add the corresponding `ALTER TABLE relay_mobile_devices ADD COLUMN ...` migration under `infra/relay/migrations/` so the columns exist before the new code runs.

Also found in 1 other location(s):
- infra/relay/src/agentActivity/LiveActivities.ts:201 -- The new `relayMobileDevices.bundleId` / `relayMobileDevices.apsEnvironment` columns are referenced by `listTargets` (`bundle_id`, `aps_environment`) and other relay code, but this PR does not add a matching SQL migration for `relay_mobile_devices`. After deploying the code to an existing database, any `listTargets` query will start failing with `column relay_mobile_devices.bundle_id does not exist` (and likewise for `aps_environment`), breaking live-activity target lookup until the schema is manually patched.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium

return yield* liveActivities.listTargets({ userId: input.target.user_id }).pipe(

The stale-job check in processSignedJob only compares the token via isCurrentSignedJobToken, so a queued job created before the device registered bundleId/apsEnvironment (or before those values changed) still passes the staleness guard when the token is unchanged. The job is then sent with the stale or missing routing metadata from the signed payload instead of the device's current values, causing APNs to reject the delivery with BadDeviceToken/DeviceTokenNotForTopic even though the registration has already been corrected. Consider comparing bundleId and apsEnvironment against the current target in the staleness check so jobs with outdated routing are treated as stale.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @infra/relay/src/agentActivity/ApnsDeliveries.ts around line 506:

The stale-job check in `processSignedJob` only compares the token via `isCurrentSignedJobToken`, so a queued job created before the device registered `bundleId`/`apsEnvironment` (or before those values changed) still passes the staleness guard when the token is unchanged. The job is then sent with the stale or missing routing metadata from the signed payload instead of the device's current values, causing APNs to reject the delivery with `BadDeviceToken`/`DeviceTokenNotForTopic` even though the registration has already been corrected. Consider comparing `bundleId` and `apsEnvironment` against the current target in the staleness check so jobs with outdated routing are treated as stale.

@macroscopeapp

macroscopeapp Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

20 blocking correctness issues found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge
juliusmarminge force-pushed the t3code/live-activities-improvements branch from f81c2ca to 1d6309e Compare July 6, 2026 05:57
Comment thread apps/mobile/src/features/settings/SettingsRouteScreen.tsx Outdated
const deepLinkRow = attentionRow ?? row0;
const deepLink =
deepLinkRow && deepLinkRow.deepLink.startsWith("/") && !deepLinkRow.deepLink.startsWith("//")
? `t3code://${deepLinkRow.deepLink.slice(1)}`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium widgets/AgentActivity.tsx:81

widgetURL is hardcoded to the t3code:// scheme, but the app registers t3code-dev for development builds and t3code-preview for preview builds. On dev/preview builds the widget banner's tap URL uses a scheme the containing app does not register, so iOS will not open the app when the banner is tapped. Consider using a scheme that is registered in all build variants (or injecting the active scheme per-build).

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/widgets/AgentActivity.tsx around line 81:

`widgetURL` is hardcoded to the `t3code://` scheme, but the app registers `t3code-dev` for development builds and `t3code-preview` for preview builds. On dev/preview builds the widget banner's tap URL uses a scheme the containing app does not register, so iOS will not open the app when the banner is tapped. Consider using a scheme that is registered in all build variants (or injecting the active scheme per-build).

// while a user ignores an approval prompt, so they get a longer window. The
// underlying database row is left in place: a late publish for the thread
// refreshes updatedAt and the row becomes visible again.
const RUNNING_AGENT_ACTIVITY_ROW_TTL_MS = 2 * 60 * 60 * 1_000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium agentActivity/AgentActivityPublisher.ts:203

RUNNING_AGENT_ACTIVITY_ROW_TTL_MS expires running/starting states after 2 hours based on updatedAt, but relay publishes are event-driven — a thread that stays in the same running phase for over 2 hours without a new event keeps its old updatedAt, so isExpiredAgentActivityState returns true and makeAggregateState filters it out. The aggregate then reports activeCount: 0 (or null/terminal) even though the job is still running, hiding active work from clients. Consider raising the running TTL to match the waiting TTL, or filtering on a heartbeat/last-seen timestamp that is refreshed independently of phase-change events.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @infra/relay/src/agentActivity/AgentActivityPublisher.ts around line 203:

`RUNNING_AGENT_ACTIVITY_ROW_TTL_MS` expires `running`/`starting` states after 2 hours based on `updatedAt`, but relay publishes are event-driven — a thread that stays in the same `running` phase for over 2 hours without a new event keeps its old `updatedAt`, so `isExpiredAgentActivityState` returns `true` and `makeAggregateState` filters it out. The aggregate then reports `activeCount: 0` (or `null`/terminal) even though the job is still running, hiding active work from clients. Consider raising the running TTL to match the waiting TTL, or filtering on a heartbeat/last-seen timestamp that is refreshed independently of phase-change events.

Comment thread apps/mobile/src/widgets/AgentActivity.tsx
Comment thread infra/relay/src/agentActivity/Devices.ts Outdated
Comment thread infra/relay/src/agentActivity/ApnsDeliveries.ts
Comment thread apps/mobile/src/features/agent-awareness/registrationPayload.ts
@juliusmarminge
juliusmarminge force-pushed the t3code/live-activities-improvements branch from 1d6309e to 39d9dca Compare July 6, 2026 06:07
@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Jul 6, 2026
Comment thread apps/mobile/src/features/agent-awareness/remoteRegistration.ts
@cursor

This comment has been minimized.

@juliusmarminge
juliusmarminge force-pushed the t3code/live-activities-improvements branch from 39d9dca to 5225bdb Compare July 6, 2026 07:37
@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 6, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Registration status stuck pending
    • The device-registration queue completion handler now resets a status still left as 'pending' back to 'unknown' when the effect exits without reaching the relay (signed out, missing relay config, cancelled generation, or unsupported platform).
  • ✅ Fixed: Sign-out cleanup on provider clear
    • Added a non-destructive suspend path (suspendAgentAwarenessRelayTokenProvider / suspendCloudRelayAccount) used by CloudAuthBridge unmount and missing-config teardown so Live Activities and the persisted registration record survive while the user remains signed in, keeping destructive cleanup only for real sign-out and account switches.

Create PR

Or push these changes by commenting:

@cursor push fa19061765
Preview (fa19061765)
diff --git a/apps/mobile/src/features/agent-awareness/remoteRegistration.ts b/apps/mobile/src/features/agent-awareness/remoteRegistration.ts
--- a/apps/mobile/src/features/agent-awareness/remoteRegistration.ts
+++ b/apps/mobile/src/features/agent-awareness/remoteRegistration.ts
@@ -143,6 +143,30 @@
   return identity === undefined || identity !== previousIdentity;
 }
 
+function removeRelaySubscriptions(): void {
+  pushToStartSubscription?.remove();
+  pushToStartSubscription = null;
+  pushTokenSubscription?.remove();
+  pushTokenSubscription = null;
+  appStateSubscription?.remove();
+  appStateSubscription = null;
+  if (activeLiveActivityRegistrationRetry) {
+    clearTimeout(activeLiveActivityRegistrationRetry);
+    activeLiveActivityRegistrationRetry = null;
+  }
+}
+
+// Drops the relay token provider without sign-out semantics: the Clerk session
+// can still be valid while the auth bridge tears down (remounts, provider tree
+// changes, missing cloud config). Listeners stop, but local Live Activities,
+// the persisted registration record, and the provider identity stay intact so
+// re-activating the same account does not end activities or force
+// re-registration.
+export function suspendAgentAwarenessRelayTokenProvider(): void {
+  relayTokenProvider = null;
+  removeRelaySubscriptions();
+}
+
 export function setAgentAwarenessRelayTokenProvider(
   provider: (() => Promise<string | null>) | null,
   identity?: string,
@@ -158,16 +182,7 @@
   relayTokenProvider = provider;
   relayTokenProviderIdentity = provider ? (identity ?? null) : null;
   if (!provider) {
-    pushToStartSubscription?.remove();
-    pushToStartSubscription = null;
-    pushTokenSubscription?.remove();
-    pushTokenSubscription = null;
-    appStateSubscription?.remove();
-    appStateSubscription = null;
-    if (activeLiveActivityRegistrationRetry) {
-      clearTimeout(activeLiveActivityRegistrationRetry);
-      activeLiveActivityRegistrationRetry = null;
-    }
+    removeRelaySubscriptions();
     // Without a signed-in user the relay can no longer update or end these
     // activities, so they would sit orphaned on the lock screen.
     endLocalLiveActivities("live activity cleanup after cloud sign-out failed");
@@ -472,6 +487,11 @@
     if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
       setRegistrationStatus("failed");
       logRegistrationError(next.context, squashAtomCommandFailure(result));
+    } else if (registrationStatus === "pending") {
+      // The registration exited without reaching the relay (signed out,
+      // missing relay config, or superseded by a newer generation); don't
+      // leave the status stuck on pending.
+      setRegistrationStatus("unknown");
     }
     logRegistrationDebug("device registration finished", { generation });
     if (activeDeviceRegistration === registration) {

diff --git a/apps/mobile/src/features/cloud/CloudAuthProvider.tsx b/apps/mobile/src/features/cloud/CloudAuthProvider.tsx
--- a/apps/mobile/src/features/cloud/CloudAuthProvider.tsx
+++ b/apps/mobile/src/features/cloud/CloudAuthProvider.tsx
@@ -15,6 +15,7 @@
 import { useAtomCommand } from "../../state/use-atom-command";
 import {
   setAgentAwarenessRelayTokenProvider,
+  suspendAgentAwarenessRelayTokenProvider,
   unregisterAgentAwarenessDeviceForCurrentUser,
 } from "../agent-awareness/remoteRegistration";
 import { resolveCloudPublicConfig, resolveRelayClerkTokenOptions } from "./publicConfig";
@@ -32,6 +33,14 @@
   setManagedRelaySession(appAtomRegistry, null);
 }
 
+// Teardown without sign-out semantics: the Clerk session may still be valid
+// (bridge remounts, provider tree changes, missing cloud config), so local
+// Live Activities and the persisted registration record must survive.
+export function suspendCloudRelayAccount(): void {
+  suspendAgentAwarenessRelayTokenProvider();
+  setManagedRelaySession(appAtomRegistry, null);
+}
+
 export function activateCloudRelayAccount(
   accountId: string,
   tokenProvider: () => Promise<string | null>,
@@ -143,7 +152,7 @@
   useEffect(
     () => () => {
       previousTokenProviderRef.current = null;
-      deactivateCloudRelayAccount();
+      suspendCloudRelayAccount();
     },
     [],
   );
@@ -158,7 +167,7 @@
 
   useEffect(() => {
     if (!publishableKey || !relayUrl) {
-      deactivateCloudRelayAccount();
+      suspendCloudRelayAccount();
     }
   }, [publishableKey, relayUrl]);

You can send follow-ups to the cloud agent here.

Comment thread apps/mobile/src/features/agent-awareness/remoteRegistration.ts
Comment thread apps/mobile/src/features/agent-awareness/remoteRegistration.ts
Comment thread apps/mobile/src/features/agent-awareness/remoteRegistration.ts
// Only reads as on when this device is actually registered with the
// relay; otherwise notifications cannot be delivered regardless of
// the local iOS permission.
value={notificationStatus === "enabled" && deviceRegistered}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium settings/SettingsRouteScreen.tsx:438

The deviceRegistered gate on the Device Notifications switch renders it false when relay registration fails, even though notificationStatus is "enabled". Because the switch shows off, the user cannot toggle it to reach handleDeviceNotificationsChange(false), which is the only path that opens iOS Settings to disable notifications. In the "permission granted, relay registration failed" case the switch misreports notifications as off and removes the in-app affordance for managing them. Consider gating only the Live Activity switch on deviceRegistered, or keeping the Device Notifications switch reflecting the actual iOS permission state so the user can still navigate to system Settings.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/features/settings/SettingsRouteScreen.tsx around line 438:

The `deviceRegistered` gate on the `Device Notifications` switch renders it `false` when relay registration fails, even though `notificationStatus` is `"enabled"`. Because the switch shows off, the user cannot toggle it to reach `handleDeviceNotificationsChange(false)`, which is the only path that opens iOS Settings to disable notifications. In the "permission granted, relay registration failed" case the switch misreports notifications as off and removes the in-app affordance for managing them. Consider gating only the Live Activity switch on `deviceRegistered`, or keeping the `Device Notifications` switch reflecting the actual iOS permission state so the user can still navigate to system Settings.

userId: input.userId,
deviceId: input.deviceId,
token: input.token,
...(input.bundleId ? { bundleId: input.bundleId } : {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High agentActivity/apnsDeliveryJobs.ts:247

makeApnsDeliveryJobPayload includes target.bundleId and target.apsEnvironment in the payload, and signatureForPayload signs that full object. Older relay workers that decode with a previous ApnsDeliveryJobPayload schema strip the new keys on decode (default effect Schema.Struct behavior) and recompute the HMAC over the reduced payload, so verifySignedApnsDeliveryJob returns ApnsDeliveryJobSignatureInvalid for every newly queued job carrying per-device routing. This breaks APNs delivery during rolling deploys when new producers share a queue with old consumers. Consider gating inclusion of these fields behind a flag, or otherwise ensuring the signed payload shape remains decodable by older schema versions, so signatures stay stable across the rollout.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @infra/relay/src/agentActivity/apnsDeliveryJobs.ts around line 247:

`makeApnsDeliveryJobPayload` includes `target.bundleId` and `target.apsEnvironment` in the payload, and `signatureForPayload` signs that full object. Older relay workers that decode with a previous `ApnsDeliveryJobPayload` schema strip the new keys on decode (default `effect` `Schema.Struct` behavior) and recompute the HMAC over the reduced payload, so `verifySignedApnsDeliveryJob` returns `ApnsDeliveryJobSignatureInvalid` for every newly queued job carrying per-device routing. This breaks APNs delivery during rolling deploys when new producers share a queue with old consumers. Consider gating inclusion of these fields behind a flag, or otherwise ensuring the signed payload shape remains decodable by older schema versions, so signatures stay stable across the rollout.

@juliusmarminge
juliusmarminge force-pushed the t3code/live-activities-improvements branch from 5225bdb to 5086c14 Compare July 6, 2026 16:13
Comment thread apps/mobile/app.config.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium

infoPlist: {
NSAppTransportSecurity: {

The frequentUpdates: true option under the expo-widgets plugin does not add the required NSSupportsLiveActivitiesFrequentUpdates key to Info.plist, so iOS keeps the standard Live Activity update budget and frequent updates are never enabled. Set NSSupportsLiveActivitiesFrequentUpdates: true in the ios.infoPlist block to actually grant the entitlement.

    infoPlist: {
+     NSSupportsLiveActivitiesFrequentUpdates: true,
      NSAppTransportSecurity: {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/app.config.ts around lines 96-97:

The `frequentUpdates: true` option under the `expo-widgets` plugin does not add the required `NSSupportsLiveActivitiesFrequentUpdates` key to `Info.plist`, so iOS keeps the standard Live Activity update budget and frequent updates are never enabled. Set `NSSupportsLiveActivitiesFrequentUpdates: true` in the `ios.infoPlist` block to actually grant the entitlement.

return existing?.trim() ? existing : null;
}

export interface AgentAwarenessRegistrationRecord {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium lib/storage.ts:228

AgentAwarenessRegistrationRecord stores only identity and signature, so when the relay base URL changes but the user identity and device payload stay the same, loadAgentAwarenessRegistrationRecord returns the stale record and the app skips registration against the new relay. The new relay never receives this device's registration until sign-out clears the record. Consider including the relay base URL (or a hash of it) in the persisted record so a URL change invalidates the cached registration.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/lib/storage.ts around line 228:

`AgentAwarenessRegistrationRecord` stores only `identity` and `signature`, so when the relay base URL changes but the user identity and device payload stay the same, `loadAgentAwarenessRegistrationRecord` returns the stale record and the app skips registration against the new relay. The new relay never receives this device's registration until sign-out clears the record. Consider including the relay base URL (or a hash of it) in the persisted record so a URL change invalidates the cached registration.

Comment thread apps/mobile/src/features/agent-awareness/remoteRegistration.ts
value={notificationStatus === "enabled" && deviceRegistered}
onValueChange={handleDeviceNotificationsChange}
/>
<SettingsSwitchRow

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High settings/SettingsRouteScreen.tsx:441

When deviceRegistered is false but liveActivitiesEnabled is true in storage, the Live Activity Updates switch renders off even though liveActivityStatus is "enabled". Because onValueChange receives the next visual state (true), tapping the switch calls linkEnvironments() (the enable path) instead of persisting enabled: false, so the user cannot disable Live Activity updates from Settings until registration succeeds. Consider gating the disabled state or the alert on deviceRegistered rather than the switch value, so the toggle still reflects the saved preference and onValueChange fires with the correct next state.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/features/settings/SettingsRouteScreen.tsx around line 441:

When `deviceRegistered` is `false` but `liveActivitiesEnabled` is `true` in storage, the `Live Activity Updates` switch renders off even though `liveActivityStatus` is `"enabled"`. Because `onValueChange` receives the next visual state (`true`), tapping the switch calls `linkEnvironments()` (the enable path) instead of persisting `enabled: false`, so the user cannot disable Live Activity updates from Settings until registration succeeds. Consider gating the disabled state or the alert on `deviceRegistered` rather than the switch `value`, so the toggle still reflects the saved preference and `onValueChange` fires with the correct next state.

Comment thread infra/relay/src/agentActivity/ApnsDeliveries.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

There are 6 total unresolved issues (including 3 from previous reviews).

Autofix Details

Bugbot Autofix prepared fixes for 2 of the 3 issues found in the latest run.

  • ✅ Fixed: Foreground replay ends live activities
    • Replay now skips delivery when the aggregate is null but non-terminal rows still exist, so TTL-expired rows from a healthy long-running agent no longer trigger live_activity_end on foreground while truly orphaned activities (rows removed) are still ended.
  • ✅ Fixed: Same identity skips registration retry
    • setAgentAwarenessRelayTokenProvider now only skips re-enqueueing device registration for an unchanged identity when the registration status is "registered", so failed or stalled registrations retry when the auth effect re-runs.

Create PR

Or push these changes by commenting:

@cursor push 59517fdb7d
Preview (59517fdb7d)
diff --git a/apps/mobile/src/features/agent-awareness/remoteRegistration.ts b/apps/mobile/src/features/agent-awareness/remoteRegistration.ts
--- a/apps/mobile/src/features/agent-awareness/remoteRegistration.ts
+++ b/apps/mobile/src/features/agent-awareness/remoteRegistration.ts
@@ -186,7 +186,11 @@
     refreshActiveLiveActivityRemoteRegistration(),
     "active live activity registration after cloud sign-in failed",
   );
-  if (isExistingIdentity) {
+  // An unchanged identity only skips re-registration when the previous attempt
+  // actually reached the relay; a failed or stalled attempt must retry the next
+  // time the auth effect re-runs, or the device stays unregistered until an
+  // unrelated event (token rotation, environment link) happens to enqueue one.
+  if (isExistingIdentity && registrationStatus === "registered") {
     return;
   }
   enqueueDeviceRegistration({}, "device registration after cloud sign-in failed");

diff --git a/infra/relay/src/agentActivity/AgentActivityPublisher.ts b/infra/relay/src/agentActivity/AgentActivityPublisher.ts
--- a/infra/relay/src/agentActivity/AgentActivityPublisher.ts
+++ b/infra/relay/src/agentActivity/AgentActivityPublisher.ts
@@ -119,6 +119,16 @@
         terminalState: null,
         nowMs: now.epochMilliseconds,
       });
+      // A null aggregate is ambiguous while non-terminal rows still exist:
+      // they may belong to a dead environment, or to a healthy long-running
+      // agent whose meaningful state (and therefore updatedAt) has not changed
+      // within the TTL. Sending in that case would end a possibly-healthy Live
+      // Activity on every foreground, so replay only delivers when live rows
+      // remain or the rows are truly gone (the thread ended and its end push
+      // may have been lost).
+      if (aggregate === null && activeStates.some((state) => !isTerminalPhase(state))) {
+        return null;
+      }
       return yield* apnsDeliveries.sendForTarget({
         target,
         aggregate,

You can send follow-ups to the cloud agent here.

Comment thread infra/relay/src/agentActivity/AgentActivityPublisher.ts
Comment thread apps/mobile/src/features/agent-awareness/remoteRegistration.ts
Comment thread apps/mobile/src/features/agent-awareness/remoteRegistration.ts
@juliusmarminge
juliusmarminge force-pushed the t3code/live-activities-improvements branch from 5086c14 to a31735c Compare July 6, 2026 17:03
Comment thread apps/mobile/src/features/settings/SettingsRouteScreen.tsx
Comment thread apps/server/src/cli/connect.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

There are 7 total unresolved issues (including 6 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Parallel registration marks failed incorrectly
    • Registration attempts now capture a success counter at start and only mark the status failed when no concurrent attempt succeeded while they were in flight, so a stale failure can no longer overwrite a relay-accepted registration.

Create PR

Or push these changes by commenting:

@cursor push d469c8569a
Preview (d469c8569a)
diff --git a/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts b/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts
--- a/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts
+++ b/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts
@@ -535,6 +535,48 @@
     }).pipe(Effect.provide(relayTestLayer));
   });
 
+  it.effect("keeps the registered status when a stale concurrent attempt fails", () => {
+    const fetchMock = vi.fn((request: RequestInfo | URL) => {
+      const url = request instanceof Request ? request.url : String(request);
+      return Promise.resolve(
+        Response.json(
+          url.endsWith("/v1/client/dpop-token")
+            ? {
+                access_token: "relay-dpop-token",
+                issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
+                token_type: "DPoP",
+                expires_in: 300,
+                scope: "mobile:registration",
+              }
+            : { ok: true },
+        ),
+      );
+    });
+    vi.stubGlobal("fetch", fetchMock);
+    Constants.expoConfig!.extra = {
+      relay: {
+        url: "https://relay.example.test/",
+      },
+    };
+
+    // Sign-in enqueues a registration attempt that stays parked in the
+    // background queue while the direct refresh below runs.
+    setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a"));
+
+    return Effect.gen(function* () {
+      yield* refreshAgentAwarenessRegistration();
+      expect(getAgentAwarenessRegistrationStatus()).toBe("registered");
+
+      // The queued sign-in attempt now fails; its stale failure must not
+      // overwrite the registration the relay already accepted.
+      vi.mocked(loadOrCreateAgentAwarenessDeviceId).mockRejectedValueOnce(
+        new Error("device id unavailable"),
+      );
+      yield* runBackgroundOperations();
+      expect(getAgentAwarenessRegistrationStatus()).toBe("registered");
+    }).pipe(Effect.provide(relayTestLayer));
+  });
+
   it("clears registration status on cloud sign-out", () => {
     setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a"));
     setAgentAwarenessRelayTokenProvider(null);

diff --git a/apps/mobile/src/features/agent-awareness/remoteRegistration.ts b/apps/mobile/src/features/agent-awareness/remoteRegistration.ts
--- a/apps/mobile/src/features/agent-awareness/remoteRegistration.ts
+++ b/apps/mobile/src/features/agent-awareness/remoteRegistration.ts
@@ -74,6 +74,13 @@
 let registrationStatus: AgentAwarenessRegistrationStatus = "unknown";
 const registrationStatusListeners = new Set<() => void>();
 
+// Registration runs both through the background queue and directly via
+// refreshAgentAwarenessRegistration, so attempts can overlap. Counting relay
+// acceptances lets a failing attempt detect that a concurrent attempt already
+// succeeded while it was in flight, so a stale failure cannot overwrite a
+// registration the relay accepted.
+let registrationSuccessCount = 0;
+
 function setRegistrationStatus(next: AgentAwarenessRegistrationStatus): void {
   if (registrationStatus === next) {
     return;
@@ -84,6 +91,18 @@
   }
 }
 
+function markRegistrationSucceeded(): void {
+  registrationSuccessCount++;
+  setRegistrationStatus("registered");
+}
+
+function markRegistrationFailed(successCountAtAttemptStart: number): void {
+  if (registrationSuccessCount !== successCountAtAttemptStart) {
+    return;
+  }
+  setRegistrationStatus("failed");
+}
+
 export function getAgentAwarenessRegistrationStatus(): AgentAwarenessRegistrationStatus {
   return registrationStatus;
 }
@@ -316,7 +335,7 @@
       catch: (cause) => cause,
     }).pipe(Effect.orElseSucceed(() => null));
     if (persisted && persisted.identity === identity && persisted.signature === signature) {
-      setRegistrationStatus("registered");
+      markRegistrationSucceeded();
       logRegistrationDebug("relay device registration skipped; already registered for account", {
         expectedGeneration,
       });
@@ -331,7 +350,7 @@
       clerkToken: token,
       payload: body,
     });
-    setRegistrationStatus("registered");
+    markRegistrationSucceeded();
     yield* Effect.promise(() =>
       saveAgentAwarenessRegistrationRecord({ identity, signature }).catch((error: unknown) => {
         logRegistrationError("persist registration record failed", error);
@@ -466,11 +485,12 @@
   };
   activeDeviceRegistration = registration;
   registration.operation = (async () => {
+    const successCountAtStart = registrationSuccessCount;
     const result = await settleAsyncResult(() =>
       runtime.runPromiseExit(registerDevice(next.input, generation)),
     );
     if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
-      setRegistrationStatus("failed");
+      markRegistrationFailed(successCountAtStart);
       logRegistrationError(next.context, squashAtomCommandFailure(result));
     }
     logRegistrationDebug("device registration finished", { generation });
@@ -675,14 +695,17 @@
   never,
   ManagedRelay.ManagedRelayClient
 > {
-  return registerDeviceForCurrentUser().pipe(
-    Effect.catch((error) =>
-      Effect.sync(() => {
-        setRegistrationStatus("failed");
-        logRegistrationError("device registration refresh failed", error);
-      }),
-    ),
-  );
+  return Effect.suspend(() => {
+    const successCountAtStart = registrationSuccessCount;
+    return registerDeviceForCurrentUser().pipe(
+      Effect.catch((error) =>
+        Effect.sync(() => {
+          markRegistrationFailed(successCountAtStart);
+          logRegistrationError("device registration refresh failed", error);
+        }),
+      ),
+    );
+  });
 }
 
 export function __resetAgentAwarenessRemoteRegistrationForTest(): void {
@@ -703,6 +726,7 @@
   activeDeviceRegistration = null;
   pendingDeviceRegistration = null;
   registrationStatus = "unknown";
+  registrationSuccessCount = 0;
   registrationStatusListeners.clear();
 }

You can send follow-ups to the cloud agent here.

Comment thread apps/mobile/src/features/agent-awareness/remoteRegistration.ts Outdated
- Per-device APNs routing (bundle id + environment) so preview/dev builds
  receive pushes from the shared relay (+ migration for the new columns)
- Settings notification/Live Activity toggles reflect actual relay
  registration success; cannot read as enabled when the device is not registered
- Persistent register-once: skip re-registering an unchanged payload for the
  same account across launches; clear only on sign-out
- Headless publish + tunnel-free linking: `t3 connect publish` toggles agent
  activity publishing and auto-establishes a publish-only (no managed tunnel)
  link, and `t3 connect link --publish-only` links for publishing alone, so
  activity flows to mobile clients that reach the environment out of band
  (e.g. Tailscale) without T3 Connect. Relay accepts publish-only links with a
  nominal endpoint; env proofs advertise the manual provider kind
- Foreground Live Activity refresh, stale/ghost-row hardening, dedup fixes
- Tighten widget deep linking and per-row rendering for active agents

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@juliusmarminge
juliusmarminge force-pushed the t3code/live-activities-improvements branch from a31735c to 9c3eb7b Compare July 6, 2026 17:24
Comment thread apps/mobile/src/features/agent-awareness/remoteRegistration.ts
// Development builds are Xcode-signed and receive sandbox APNs tokens;
// preview and production builds are distribution-signed and use production
// APNs. The relay routes each device's pushes accordingly.
export function resolveApsEnvironment(appVariant: unknown): "sandbox" | "production" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium agent-awareness/registrationPayload.ts:8

resolveApsEnvironment returns "production" for every value except the literal string "development". Locally run ios:preview and ios:prod builds launched via expo run:ios are development-signed and receive sandbox APNs tokens, but this function classifies them as "production". The relay then routes pushes to the production APNs gateway for a sandbox token, so notifications and Live Activities fail to deliver for those builds. Consider keying off the signing/provisioning environment (e.g. EAS build profile or Configuration/ entitlements) rather than the variant name, or document why locally-run preview/prod builds are expected to use production APNs.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/features/agent-awareness/registrationPayload.ts around line 8:

`resolveApsEnvironment` returns `"production"` for every value except the literal string `"development"`. Locally run `ios:preview` and `ios:prod` builds launched via `expo run:ios` are development-signed and receive sandbox APNs tokens, but this function classifies them as `"production"`. The relay then routes pushes to the production APNs gateway for a sandbox token, so notifications and Live Activities fail to deliver for those builds. Consider keying off the signing/provisioning environment (e.g. EAS build profile or `Configuration`/ entitlements) rather than the variant name, or document why locally-run `preview`/`prod` builds are expected to use production APNs.

Comment thread apps/server/src/cli/connect.ts
Comment thread apps/mobile/src/features/agent-awareness/remoteRegistration.ts
Comment thread packages/contracts/src/environmentHttp.ts Outdated
Comment on lines 1745 to 1750
primaryCloudLinkState.refresh();
const refreshResult = await refreshRelayEnvironments();
if (refreshResult._tag === "Failure") {
if (!isAtomCommandInterrupted(refreshResult)) {
reportUpdateFailure(squashAtomCommandFailure(refreshResult));
}
setIsUpdating(false);
return;
if (refreshResult._tag === "Failure" && !isAtomCommandInterrupted(refreshResult)) {
reportUpdateFailure(squashAtomCommandFailure(refreshResult));
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium settings/ConnectionsSettings.tsx:1745

When reconcileCloudState completes a link/unlink and preference update successfully, the mutation is already committed and primaryCloudLinkState.refresh() has been called. If the follow-up refreshRelayEnvironments() call at line 1747 then fails, the function returns false, shows an error toast, and suppresses the success toast — so the user is told their T3 Connect change failed even though it was actually applied. Consider returning true before the relay refresh (or not reporting its failure as a mutation failure), so transient relay-discovery errors don't mask a successful mutation.

    primaryCloudLinkState.refresh();
+    return true;
     const refreshResult = await refreshRelayEnvironments();
     if (refreshResult._tag === "Failure" && !isAtomCommandInterrupted(refreshResult)) {
       reportUpdateFailure(squashAtomCommandFailure(refreshResult));
       return false;
     }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/settings/ConnectionsSettings.tsx around lines 1745-1750:

When `reconcileCloudState` completes a link/unlink and preference update successfully, the mutation is already committed and `primaryCloudLinkState.refresh()` has been called. If the follow-up `refreshRelayEnvironments()` call at line 1747 then fails, the function returns `false`, shows an error toast, and suppresses the success toast — so the user is told their T3 Connect change failed even though it was actually applied. Consider returning `true` before the relay refresh (or not reporting its failure as a mutation failure), so transient relay-discovery errors don't mask a successful mutation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High cloud/http.ts:369

makeCloudLinkProof calls isAllowedEndpointOrigin for every link proof, including manual providerKind requests. That helper only permits loopback hosts, so a manual or publish-only link from a non-loopback origin (e.g. Tailscale or directly exposed host) is rejected with Invalid managed endpoint origin., blocking the out-of-band endpoint flow that isSupportedLinkProviderKind was added to support. Consider skipping the loopback origin check when providerKind === "manual".

  if (
    !isSupportedLinkProviderKind(request) ||
    (request.endpoint.providerKind === "cloudflare_tunnel" &&
      !isAllowedEndpointOrigin({
        origin: request.origin,
        requestUrl,
      }))
  ) {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/cloud/http.ts around lines 369-375:

`makeCloudLinkProof` calls `isAllowedEndpointOrigin` for every link proof, including manual `providerKind` requests. That helper only permits loopback hosts, so a manual or publish-only link from a non-loopback origin (e.g. Tailscale or directly exposed host) is rejected with `Invalid managed endpoint origin.`, blocking the out-of-band endpoint flow that `isSupportedLinkProviderKind` was added to support. Consider skipping the loopback origin check when `providerKind === "manual"`.

…re-registration spam

- Rework the AgentActivity layout: attention-first row ordering, single-line
  rows (title + inline project + status) shared by the banner and expanded
  Dynamic Island, centered dot-separated banner headline, up to 5 banner rows,
  a dedicated watch/CarPlay bannerSmall, and a phase-glyph minimal view for
  the shared island
- Match status tints to the web sidebar pills (Sidebar.logic.ts), switching
  between the light (-600) and dark (-300) palette off the activity color
  scheme: iPhone renders on dark material, but macOS mirroring/notification
  center renders on light where the dark palette was illegible
- Align the relay's "Starting" status label to the web sidebar's "Connecting"
- Ship the branded T3 mark to the widget extension: expo-widgets generates the
  target with no Resources phase and hand-added ones are never scheduled, so
  withWidgetLogoAsset.cjs installs the SVG template image set and an
  alwaysOutOfDate actool shell phase (scripts/wire-widget-asset-catalog.cjs
  applies the same wiring to an already-generated ios/ without a prebuild)
- Register a Live Activity push token with the relay only once per accepted
  token: the refresh runs on sign-in, app foreground, and every environment
  connection update, and was re-POSTing identical {deviceId, token} payloads
  in a loop; the register-once set clears on sign-out/identity change

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
// Any registered scheme variant routes back to this app; taps are delivered
// to the widget's containing app, so the prod scheme is safe for all builds.
const deepLinkRow = attentionRow ?? row0;
const deepLink =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium widgets/AgentActivity.tsx:140

The deep-link guard at deepLinkRow.deepLink.startsWith("/") && !deepLinkRow.deepLink.startsWith("//") accepts any path, so values like /settings or /threads/env/thread?x=1 are turned into t3code://settings or t3code://threads/env/thread?x=1. Unlike extractAgentNotificationDeepLink, this allows non-thread paths and query/hash-bearing links to route Live Activity taps to the wrong screen. Consider matching the app's existing normalization so only valid thread links produce a deepLink.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/widgets/AgentActivity.tsx around line 140:

The deep-link guard at `deepLinkRow.deepLink.startsWith("/") && !deepLinkRow.deepLink.startsWith("//")` accepts any path, so values like `/settings` or `/threads/env/thread?x=1` are turned into `t3code://settings` or `t3code://threads/env/thread?x=1`. Unlike `extractAgentNotificationDeepLink`, this allows non-thread paths and query/hash-bearing links to route Live Activity taps to the wrong screen. Consider matching the app's existing normalization so only valid thread links produce a `deepLink`.

@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Bugbot Autofix prepared fixes for all 5 issues found in the latest run.

  • ✅ Fixed: Replay ends newly armed card
    • Arm-on-send registrations now carry an armedForLocalWork flag and the relay skips the registration replay when the aggregate has no live agents, so the freshly armed card is never ended before the work's first publish arrives.
  • ✅ Fixed: Replay paints stale Done state
    • The same armedForLocalWork replay skip also covers terminal-only aggregates (activeCount 0), so a stale recent-Done snapshot can no longer overwrite the just-armed Connecting placeholder.
  • ✅ Fixed: Arming ignores Live Activity preference
    • armAgentAwarenessLiveActivityForLocalWork now loads saved preferences and no-ops when liveActivitiesEnabled is false (or preferences are unreadable) before starting a card or registering tokens.
  • ✅ Fixed: Concurrent sends spawn duplicate cards
    • A module-level in-flight guard reserved synchronously at arm entry makes the instance check and AgentActivity.start effectively atomic, so concurrent sends on different threads can no longer each start a card.
  • ✅ Fixed: Foreground prime skips terminal snapshot
    • Foreground priming now arms a card for any non-null snapshot aggregate instead of requiring activeCount > 0, so terminal-only Done/Failed snapshots are primed on app open.

Create PR

Or push these changes by commenting:

@cursor push 6fb7435bba
Preview (6fb7435bba)
diff --git a/apps/mobile/src/features/agent-awareness/remoteRegistration.ts b/apps/mobile/src/features/agent-awareness/remoteRegistration.ts
--- a/apps/mobile/src/features/agent-awareness/remoteRegistration.ts
+++ b/apps/mobile/src/features/agent-awareness/remoteRegistration.ts
@@ -108,6 +108,7 @@
   };
 }
 let activeLiveActivityRegistrationRetry: ReturnType<typeof setTimeout> | null = null;
+let armLiveActivityForLocalWorkInFlight = false;
 let relayTokenProvider: (() => Promise<string | null>) | null = null;
 let relayTokenProviderIdentity: string | null = null;
 let deviceRegistrationGeneration = 0;
@@ -438,8 +439,9 @@
 // Arms the lock-screen card the moment the user starts agent work from this
 // phone, while the app is still foregrounded and the fresh activity's token
 // can be registered immediately. The seeded row is a best-effort placeholder;
-// the relay's registration replay repaints it with the authoritative
-// aggregate within seconds. No-ops when a card is already armed.
+// the relay repaints it with the authoritative aggregate once the work's
+// first publish lands. No-ops when a card is already armed or the user
+// disabled Live Activity updates in settings.
 export function armAgentAwarenessLiveActivityForLocalWork(input: {
   readonly threadTitle: string;
   readonly projectTitle: string;
@@ -447,40 +449,61 @@
   if (!canRegisterRemoteLiveActivities() || !relayTokenProvider) {
     return;
   }
-  try {
-    if (AgentActivity.getInstances().length > 0) {
-      return;
+  // Reserve arming before the preference load yields: two quick sends on
+  // different threads would otherwise both observe zero instances and each
+  // start a card, while the relay tracks exactly one card per device.
+  if (armLiveActivityForLocalWorkInFlight) {
+    return;
+  }
+  armLiveActivityForLocalWorkInFlight = true;
+  void (async () => {
+    try {
+      // Honor the saved settings toggle before creating anything; without
+      // readable preferences the registration pipeline cannot run either, so
+      // an armed card would never receive updates.
+      const preferences = await loadPreferences().catch((error: unknown) => {
+        logRegistrationError("live activity arming preference load failed", error);
+        return null;
+      });
+      if (preferences === null || preferences.liveActivitiesEnabled === false) {
+        return;
+      }
+      if (AgentActivity.getInstances().length > 0) {
+        return;
+      }
+      const nowIso = new Date(Date.now()).toISOString();
+      const activity = AgentActivity.start({
+        title: "T3 Code",
+        subtitle: "Agent work in progress",
+        activeCount: 1,
+        updatedAt: nowIso,
+        activities: [
+          {
+            environmentId: "",
+            threadId: "",
+            projectTitle: input.projectTitle,
+            threadTitle: input.threadTitle,
+            modelTitle: "",
+            phase: "starting",
+            status: "Connecting",
+            updatedAt: nowIso,
+            deepLink: "/",
+          },
+        ],
+      });
+      logRegistrationDebug("live activity card armed for local work", {
+        threadTitle: input.threadTitle,
+      });
+      runRegistrationInBackground(
+        registerLiveActivityPushToken({ activity, armedForLocalWork: true }).pipe(Effect.asVoid),
+        "live activity arming after local task start failed",
+      );
+    } catch (error) {
+      logRegistrationError("live activity arming failed", error);
+    } finally {
+      armLiveActivityForLocalWorkInFlight = false;
     }
-    const nowIso = new Date(Date.now()).toISOString();
-    const activity = AgentActivity.start({
-      title: "T3 Code",
-      subtitle: "Agent work in progress",
-      activeCount: 1,
-      updatedAt: nowIso,
-      activities: [
-        {
-          environmentId: "",
-          threadId: "",
-          projectTitle: input.projectTitle,
-          threadTitle: input.threadTitle,
-          modelTitle: "",
-          phase: "starting",
-          status: "Connecting",
-          updatedAt: nowIso,
-          deepLink: "/",
-        },
-      ],
-    });
-    logRegistrationDebug("live activity card armed for local work", {
-      threadTitle: input.threadTitle,
-    });
-    runRegistrationInBackground(
-      registerLiveActivityPushToken({ activity }).pipe(Effect.asVoid),
-      "live activity arming after local task start failed",
-    );
-  } catch (error) {
-    logRegistrationError("live activity arming failed", error);
-  }
+  })();
 }
 
 function readAgentActivitySnapshot(): Effect.Effect<
@@ -821,6 +844,7 @@
   deviceRegistrationGeneration++;
   activeDeviceRegistration = null;
   pendingDeviceRegistration = null;
+  armLiveActivityForLocalWorkInFlight = false;
   registrationStatus = "unknown";
   registrationStatusListeners.clear();
   registeredActivityPushTokens.clear();
@@ -853,6 +877,7 @@
 
 export function registerLiveActivityPushToken(input: {
   readonly activity: LiveActivity<AgentActivityProps>;
+  readonly armedForLocalWork?: boolean;
 }): Effect.Effect<boolean, unknown, ManagedRelay.ManagedRelayClient> {
   return Effect.gen(function* () {
     if (!canRegisterRemoteLiveActivities()) {
@@ -893,6 +918,7 @@
           runRegistrationInBackground(
             registerLiveActivityPushTokenValue({
               activityPushToken: event.pushToken,
+              armedForLocalWork: input.armedForLocalWork === true,
             }),
             "live activity token listener registration failed",
           );
@@ -903,12 +929,14 @@
 
     return yield* registerLiveActivityPushTokenValue({
       activityPushToken,
+      armedForLocalWork: input.armedForLocalWork === true,
     });
   });
 }
 
 function registerLiveActivityPushTokenValue(input: {
   readonly activityPushToken: string;
+  readonly armedForLocalWork: boolean;
 }): Effect.Effect<boolean, unknown, ManagedRelay.ManagedRelayClient> {
   return Effect.gen(function* () {
     const acceptedAt = registeredActivityPushTokens.get(input.activityPushToken);
@@ -929,6 +957,7 @@
     const registered = yield* registerLiveActivityWithRelay({
       deviceId,
       activityPushToken: input.activityPushToken,
+      ...(input.armedForLocalWork ? { armedForLocalWork: true } : {}),
     });
     if (registered) {
       registeredActivityPushTokens.set(input.activityPushToken, Date.now());
@@ -1017,7 +1046,10 @@
         }).pipe(Effect.orElseSucceed(() => [] as ReadonlyArray<LiveActivity<AgentActivityProps>>));
         if (armedMeanwhile.length > 0) {
           activities = [...armedMeanwhile];
-        } else if (snapshot?.aggregate && snapshot.aggregate.activeCount > 0) {
+        } else if (snapshot?.aggregate) {
+          // Any non-null aggregate is worth a card: the relay only returns
+          // one when there is content to show — live agents, or recently
+          // finished work still inside its Done/Failed display window.
           const aggregate = snapshot.aggregate;
           const primed = yield* Effect.try({
             try: () =>

diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
--- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
+++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
@@ -528,8 +528,8 @@
     flow.setSubmitting(true);
     // Arm the lock-screen card before the async thread creation: backgrounding
     // the app right after tapping submit would otherwise reject the foreground
-    // -only Activity start. If creation fails, the token registration's replay
-    // finds no work and ends the card within seconds.
+    // -only Activity start. If creation fails, the next reconcile or publish
+    // with nothing to show ends the stranded card.
     armAgentAwarenessLiveActivityForLocalWork({
       threadTitle: deriveThreadTitleFromPrompt(initialMessageText),
       projectTitle: selectedProject.title,

diff --git a/infra/relay/src/agentActivity/AgentActivityPublisher.ts b/infra/relay/src/agentActivity/AgentActivityPublisher.ts
--- a/infra/relay/src/agentActivity/AgentActivityPublisher.ts
+++ b/infra/relay/src/agentActivity/AgentActivityPublisher.ts
@@ -42,6 +42,7 @@
     readonly replayForLiveActivityRegistration: (input: {
       readonly userId: string;
       readonly deviceId: string;
+      readonly armedForLocalWork?: boolean;
     }) => Effect.Effect<RelayDeliveryResult | null, AgentActivityPublishError>;
   }
 >()("t3code-relay/agentActivity/AgentActivityPublisher") {}
@@ -125,6 +126,15 @@
         terminalState: null,
         nowMs: now.epochMilliseconds,
       });
+      // A registration for a card the app just armed for locally started
+      // work races that work's first publish: its rows usually do not exist
+      // yet, so an aggregate without live agents means "not published yet",
+      // not "nothing to show". Delivering here would end the fresh card (and
+      // drop its token) or overwrite the placeholder with stale Done rows —
+      // skip and let the imminent publish paint it instead.
+      if (input.armedForLocalWork === true && (aggregate === null || aggregate.activeCount === 0)) {
+        return null;
+      }
       return yield* apnsDeliveries.sendForTarget({
         target,
         aggregate,

diff --git a/infra/relay/src/agentActivity/MobileRegistrations.ts b/infra/relay/src/agentActivity/MobileRegistrations.ts
--- a/infra/relay/src/agentActivity/MobileRegistrations.ts
+++ b/infra/relay/src/agentActivity/MobileRegistrations.ts
@@ -78,6 +78,7 @@
           .replayForLiveActivityRegistration({
             userId: input.userId,
             deviceId: input.payload.deviceId,
+            ...(input.payload.armedForLocalWork ? { armedForLocalWork: true } : {}),
           })
           .pipe(
             Effect.tapError((error) =>

diff --git a/packages/contracts/src/relay.ts b/packages/contracts/src/relay.ts
--- a/packages/contracts/src/relay.ts
+++ b/packages/contracts/src/relay.ts
@@ -84,6 +84,11 @@
 export const RelayLiveActivityRegistrationRequest = Schema.Struct({
   deviceId: TrimmedNonEmptyString,
   activityPushToken: TrimmedNonEmptyString,
+  // Set when the app armed this activity for work it just started locally.
+  // The registration replay then races the first agent-activity publish, so
+  // an aggregate without live work means "not published yet", not "nothing
+  // to show" — the relay must not end or repaint the freshly armed card.
+  armedForLocalWork: Schema.optional(Schema.Boolean),
 });
 export type RelayLiveActivityRegistrationRequest = typeof RelayLiveActivityRegistrationRequest.Type;

You can send follow-ups to the cloud agent here.

Arming on send registers the card's token immediately, and the
registration replay can reach the relay before the environment's first
publish for the brand-new thread. With end-on-empty, that replay saw an
empty aggregate, ended the seconds-old card, and retired its token — so
the running/input updates that arrived moments later had no address and
the card sat orphaned at its seed "Connecting" content (observed live:
card armed and ended at 05:36:38Z, one second before the thread's
waiting_for_input row landed).

An empty aggregate within two minutes of arming now delivers nothing,
leaving the seed content in place for the state that is about to arrive;
past the grace window, empty still ends the card as designed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread infra/relay/src/agentActivity/ApnsDeliveries.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Grace blocks legitimate activity end
    • LiveActivities.register no longer resets remote_started_at when the same activity token is re-registered (foreground reconciliation), so the freshly-armed grace window measures from the true arming time and orphaned cards receive their live_activity_end once it expires.

Create PR

Or push these changes by commenting:

@cursor push 5ab40bb88e
Preview (5ab40bb88e)
diff --git a/infra/relay/src/agentActivity/LiveActivities.test.ts b/infra/relay/src/agentActivity/LiveActivities.test.ts
--- a/infra/relay/src/agentActivity/LiveActivities.test.ts
+++ b/infra/relay/src/agentActivity/LiveActivities.test.ts
@@ -109,10 +109,12 @@
             remoteStartedAt: null,
           }),
         ]);
+        // The claim skips this device's own row: it must stay intact so the
+        // upsert can tell a same-token re-registration apart from a fresh arm.
         expect(updateConditions.map((condition) => dialect.sqlToQuery(condition))).toEqual([
           {
-            sql: '"relay_live_activities"."activity_push_token" = $1',
-            params: ["activity-push-token"],
+            sql: '(("relay_live_activities"."activity_push_token" = $1) and ((("relay_live_activities"."user_id" <> $2) or ("relay_live_activities"."device_id" <> $3))))',
+            params: ["activity-push-token", "user-2", "device-1"],
           },
         ]);
         expect(insertedValues).toEqual([
@@ -131,12 +133,20 @@
           expect.objectContaining({
             activityPushToken: "activity-push-token",
             remoteStartQueuedAt: null,
-            remoteStartedAt: expect.any(String),
             endedAt: null,
             lastAggregateJson: null,
             lastLiveActivityDeliveryAt: null,
           }),
         );
+        // Re-registering the SAME token (foreground reconciliation) must not
+        // restart the arming clock, or the end-on-empty grace window would
+        // renew forever and orphaned cards would never receive their end.
+        const remoteStartedAtSet = dialect.sqlToQuery(
+          conflictConfigs[0]?.set?.remoteStartedAt as SQL,
+        );
+        expect(remoteStartedAtSet.sql.replace(/\s+/g, " ")).toBe(
+          'case when "relay_live_activities"."activity_push_token" = excluded.activity_push_token then coalesce("relay_live_activities"."remote_started_at", excluded.remote_started_at) else excluded.remote_started_at end',
+        );
       }).pipe(
         Effect.provide(
           LiveActivities.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb))),

diff --git a/infra/relay/src/agentActivity/LiveActivities.ts b/infra/relay/src/agentActivity/LiveActivities.ts
--- a/infra/relay/src/agentActivity/LiveActivities.ts
+++ b/infra/relay/src/agentActivity/LiveActivities.ts
@@ -13,7 +13,7 @@
 import * as Function from "effect/Function";
 import * as Layer from "effect/Layer";
 import * as Schema from "effect/Schema";
-import { and, eq, sql } from "drizzle-orm";
+import { and, eq, ne, or, sql } from "drizzle-orm";
 
 import * as RelayDb from "../db.ts";
 import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";
@@ -141,6 +141,10 @@
         const updatedAt = DateTime.formatIso(yield* DateTime.now);
         const registration = input.registration;
 
+        // Claim the token from any OTHER row that still holds it (a token
+        // addresses exactly one activity). The device's own row is left
+        // untouched so the upsert below can tell a same-token re-registration
+        // apart from a fresh arm.
         yield* db
           .update(relayLiveActivities)
           .set({
@@ -150,7 +154,15 @@
             endedAt: updatedAt,
             updatedAt,
           })
-          .where(eq(relayLiveActivities.activityPushToken, registration.activityPushToken));
+          .where(
+            and(
+              eq(relayLiveActivities.activityPushToken, registration.activityPushToken),
+              or(
+                ne(relayLiveActivities.userId, input.userId),
+                ne(relayLiveActivities.deviceId, registration.deviceId),
+              ),
+            ),
+          );
 
         yield* db
           .insert(relayLiveActivities)
@@ -171,7 +183,17 @@
             set: {
               activityPushToken: registration.activityPushToken,
               remoteStartQueuedAt: null,
-              remoteStartedAt: updatedAt,
+              // remote_started_at records when the CARD was armed, and the
+              // end-on-empty grace window keys off it. Foreground
+              // reconciliation re-registers the same token periodically;
+              // refreshing the arming time there would perpetually renew the
+              // grace and suppress the end an orphaned card is waiting for.
+              // Only a new token (a new activity) restarts the clock.
+              remoteStartedAt: sql`case
+                when ${relayLiveActivities.activityPushToken} = excluded.activity_push_token
+                  then coalesce(${relayLiveActivities.remoteStartedAt}, excluded.remote_started_at)
+                else excluded.remote_started_at
+              end`,
               endedAt: null,
               lastAggregateJson: null,
               lastLiveActivityDeliveryAt: null,

You can send follow-ups to the cloud agent here.

Comment thread infra/relay/src/agentActivity/ApnsDeliveries.ts
The stuck-at-Working card: the thread's publish stream showed running →
deleted with no completed publish in between, and the tombstone debounce
correctly confirmed the null five seconds later — the post-completion
state resolves to no phase PERSISTENTLY. Session teardown settles
still-running turns by session status, and that write can race
turn.completed, leaving latestTurn.state at "interrupted" for a turn
that actually finished. The awareness ladder mapped interrupted turns to
null, so quick finish-then-teardown threads were tombstoned instead of
published as completed: the row vanished, the empty aggregate fell into
the freshly-armed grace window, and the card froze on its last state
with no further publish to correct it.

completedAt survives the state-column race: a turn with a completion
timestamp finished, whatever the settling wrote. The ladder now projects
those as completed — the tombstone confirm publishes Done instead of
deleting the thread — while genuinely interrupted turns (no completedAt)
still resolve to null.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
// that has a completion timestamp finished, whatever the state column says.
// Without this, quick finish-then-teardown threads resolve to null
// persistently and get tombstoned instead of published as completed.
if (thread.latestTurn?.state === "interrupted" && thread.latestTurn.completedAt !== null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/agentAwareness.ts:112

The new fallback at line 112 misclassifies cancelled runs as completed. Turns settled via cancellation paths (interrupt-requested or session ended with interrupted/stopped status) also get state: "interrupted" with a non-null completedAt, so the condition thread.latestTurn?.state === "interrupted" && thread.latestTurn.completedAt !== null matches them too. This causes resolveThreadAwarenessPhase to return "completed" for runs the user actually cancelled, so users see stopped runs as successfully finished. If the intent is only to cover the teardown race where a completed turn's state is overwritten by interrupted, the guard needs to distinguish that case from genuine cancellations — for example, by checking the session status or a separate completion marker. Otherwise, consider documenting why interrupted turns with a completedAt should be treated as completed.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/shared/src/agentAwareness.ts around line 112:

The new fallback at line 112 misclassifies cancelled runs as `completed`. Turns settled via cancellation paths (interrupt-requested or session ended with `interrupted`/`stopped` status) also get `state: "interrupted"` with a non-null `completedAt`, so the condition `thread.latestTurn?.state === "interrupted" && thread.latestTurn.completedAt !== null` matches them too. This causes `resolveThreadAwarenessPhase` to return `"completed"` for runs the user actually cancelled, so users see stopped runs as successfully finished. If the intent is only to cover the teardown race where a `completed` turn's state is overwritten by `interrupted`, the guard needs to distinguish that case from genuine cancellations — for example, by checking the session status or a separate completion marker. Otherwise, consider documenting why interrupted turns with a `completedAt` should be treated as completed.

Comment thread packages/shared/src/agentAwareness.ts
The stuck-at-Working repro survives the completedAt ladder fix: the
confirmed tombstone five seconds after turn completion still resolves
null, so the settled shell holds some combination the static analysis
keeps missing. Instead of guessing another layer deep, the tombstone
paths now log the exact fields the phase ladder reads (session status,
latest turn id/state/completedAt, pendings) both when deferring and when
a confirmation actually publishes — one repro pins the writer.

Also collapses the deferral storm visible at thread birth (six confirm
fibers forked in 600ms): one pending confirmation per thread.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Pending confirm set can leak
    • Wrapped the check-add-fork sequence in a single uninterruptible region so a pendingTombstoneConfirms entry can only exist once the detached confirm fiber (whose ensuring finalizer owns the delete) is guaranteed to have been forked.

Create PR

Or push these changes by commenting:

@cursor push abb02eb729
Preview (abb02eb729)
diff --git a/apps/server/src/relay/AgentAwarenessRelay.ts b/apps/server/src/relay/AgentAwarenessRelay.ts
--- a/apps/server/src/relay/AgentAwarenessRelay.ts
+++ b/apps/server/src/relay/AgentAwarenessRelay.ts
@@ -432,20 +432,30 @@
       // immediately deletes the thread from the lock-screen card mid-
       // conversation. Defer, re-resolve, and only tombstone if it holds;
       // genuinely deleted threads still propagate a few seconds later.
-      if (pendingTombstoneConfirms.has(threadId)) {
-        return;
-      }
-      pendingTombstoneConfirms.add(threadId);
-      yield* Effect.logInfo("agent activity tombstone deferred pending confirmation", {
-        environmentId,
-        threadId,
-        reason: snapshot.reason,
-        shell: describeThreadShellForAwareness(thread),
-      });
-      yield* Effect.forkDetach(
-        confirmTombstoneLater(threadId).pipe(
-          Effect.ensuring(Effect.sync(() => pendingTombstoneConfirms.delete(threadId))),
-        ),
+      // Check-add-fork must be atomic under interruption: a set entry without
+      // a forked confirm fiber (whose `ensuring` owns the delete) would
+      // suppress tombstone confirmation for this thread forever.
+      yield* Effect.uninterruptible(
+        Effect.suspend(() => {
+          if (pendingTombstoneConfirms.has(threadId)) {
+            return Effect.void;
+          }
+          pendingTombstoneConfirms.add(threadId);
+          return Effect.logInfo("agent activity tombstone deferred pending confirmation", {
+            environmentId,
+            threadId,
+            reason: snapshot.reason,
+            shell: describeThreadShellForAwareness(thread),
+          }).pipe(
+            Effect.andThen(
+              Effect.forkDetach(
+                confirmTombstoneLater(threadId).pipe(
+                  Effect.ensuring(Effect.sync(() => pendingTombstoneConfirms.delete(threadId))),
+                ),
+              ),
+            ),
+          );
+        }),
       );
       return;
     }

You can send follow-ups to the cloud agent here.

Comment thread apps/server/src/relay/AgentAwarenessRelay.ts Outdated
Diagnostics from the stuck-at-Working repro finally exposed the data
hole: the confirmed tombstone's shell showed sessionStatus "ready" with
latestTurn entirely absent. Threads whose turns produce no checkpoint
never get a projection_turns row, and thread.session-set clears
latest_turn_id the moment the session settles (activeTurnId goes null) —
so "completed" is unrepresentable through turns for quick Q&A threads.
Their entire lifecycle rode on session status and pendings, and at
completion nothing remained on the awareness ladder: tombstone instead
of Done.

A live session at "ready"/"idle" with nothing pending and nothing
running now projects as completed. This intentionally diverges from the
web sidebar, which resolves the same null but renders it as "no pill" —
a fine default in a list, while the publisher's null means "remove from
the lock-screen card". (The sidebar's own Completed pill needs
latestTurn.completedAt and silently can't fire for these threads either;
persisting turn rows for checkpoint-less turns is the deeper future fix
for both surfaces.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/shared/src/agentAwareness.ts
The full lifecycle finally works end to end, with one wart: a duplicate
Done push notification fired one second after thread creation. Sessions
boot at "ready" before their first turn starts, so the new
ready-means-completed projection produced a momentary completed state at
birth — and since the freshly armed card's token wasn't registered yet,
it fell through to the notification channel. Server restarts had the
same shape at scale: the startup snapshot republished completed for
every recently finished thread, each queuing a push notification.

- Env server: completed as a thread's FIRST published state defers and
  re-resolves like a tombstone — at birth the confirm sees running and
  completed never publishes; genuinely at-rest threads still publish
  five seconds later. Fresh completions (previous state was live) are
  unaffected, keeping the buzz immediate.
- Relay: terminal push notifications only fire when the completion is
  fresh (row updated within two minutes), so replays and restart
  snapshots repaint state without ringing the device.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
if (!activity) {
return null;
}
if (activity.phase === "completed" || activity.phase === "failed") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium agentActivity/ApnsDeliveries.ts:321

notificationForAggregate drops terminal notifications for push-notification-only users when activity.updatedAt is more than two minutes old. activity.updatedAt is the timestamp from the published aggregate state, not the time the notification was first delivered to this device. If relay publishing or replay is delayed by more than two minutes (e.g., after downtime or a late reconnect), the first completed/failed push notification for a notifications-only user is silently suppressed even though it was never delivered before. Consider gating the freshness check on the device's last-known delivery time (e.g., last_push_notification_delivery_at) rather than activity.updatedAt, so only devices that already received a recent terminal notification are suppressed.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @infra/relay/src/agentActivity/ApnsDeliveries.ts around line 321:

`notificationForAggregate` drops terminal notifications for push-notification-only users when `activity.updatedAt` is more than two minutes old. `activity.updatedAt` is the timestamp from the published aggregate state, not the time the notification was first delivered to this device. If relay publishing or replay is delayed by more than two minutes (e.g., after downtime or a late reconnect), the first `completed`/`failed` push notification for a notifications-only user is silently suppressed even though it was never delivered before. Consider gating the freshness check on the device's last-known delivery time (e.g., `last_push_notification_delivery_at`) rather than `activity.updatedAt`, so only devices that already received a recent terminal notification are suppressed.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: LA alerts skip terminal freshness
    • Threaded nowMs into alertForNewlyTerminal and alertForTerminalAggregate and applied the shared TERMINAL_NOTIFICATION_FRESHNESS_MS window via a new isFreshTerminalRow helper, so stale replayed completions no longer alert through Live Activity updates or end events.

Create PR

Or push these changes by commenting:

@cursor push c88cec1a49
Preview (c88cec1a49)
diff --git a/infra/relay/src/agentActivity/ApnsDeliveries.test.ts b/infra/relay/src/agentActivity/ApnsDeliveries.test.ts
--- a/infra/relay/src/agentActivity/ApnsDeliveries.test.ts
+++ b/infra/relay/src/agentActivity/ApnsDeliveries.test.ts
@@ -1525,15 +1525,30 @@
       activities: [{ ...aggregate.activities[0]!, phase: "completed" as const, status: "Done" }],
     };
     expect(
-      ApnsDeliveries.alertForTerminalAggregate({ aggregate: terminalAggregate, preferences }),
+      ApnsDeliveries.alertForTerminalAggregate({
+        aggregate: terminalAggregate,
+        preferences,
+        nowMs: 0,
+      }),
     ).toEqual({ title: "Thread", body: "Done: Project" });
     expect(
       ApnsDeliveries.alertForTerminalAggregate({
         aggregate: terminalAggregate,
         preferences: { ...preferences, notifyOnCompletion: false },
+        nowMs: 0,
       }),
     ).toBeNull();
-    expect(ApnsDeliveries.alertForTerminalAggregate({ aggregate: null, preferences })).toBeNull();
+    expect(
+      ApnsDeliveries.alertForTerminalAggregate({ aggregate: null, preferences, nowMs: 0 }),
+    ).toBeNull();
+    // A completion replayed long after the fact must not ring again.
+    expect(
+      ApnsDeliveries.alertForTerminalAggregate({
+        aggregate: terminalAggregate,
+        preferences,
+        nowMs: 10 * 60 * 1_000,
+      }),
+    ).toBeNull();
   });
 
   it("alerts when a previously active thread finishes mid-flight", () => {
@@ -1552,6 +1567,7 @@
         previousAggregate: aggregate,
         nextAggregate: next,
         preferences,
+        nowMs: 0,
       }),
     ).toEqual({ title: "Thread", body: "Done: Project" });
     // The completion switch mutes it.
@@ -1560,6 +1576,7 @@
         previousAggregate: aggregate,
         nextAggregate: next,
         preferences: { ...preferences, notifyOnCompletion: false },
+        nowMs: 0,
       }),
     ).toBeNull();
     // No baseline means no transition to ring on.
@@ -1568,6 +1585,7 @@
         previousAggregate: null,
         nextAggregate: next,
         preferences,
+        nowMs: 0,
       }),
     ).toBeNull();
     // A Done row that was already terminal (or absent) before stays silent.
@@ -1576,6 +1594,7 @@
         previousAggregate: next,
         nextAggregate: next,
         preferences,
+        nowMs: 0,
       }),
     ).toBeNull();
     expect(
@@ -1583,7 +1602,18 @@
         previousAggregate: { ...aggregate, activities: [attentionRow] },
         nextAggregate: next,
         preferences,
+        nowMs: 0,
       }),
     ).toBeNull();
+    // A stale completion replayed after a restart (previous aggregate still
+    // shows the thread as running) must not ring the device again.
+    expect(
+      ApnsDeliveries.alertForNewlyTerminal({
+        previousAggregate: aggregate,
+        nextAggregate: next,
+        preferences,
+        nowMs: 10 * 60 * 1_000,
+      }),
+    ).toBeNull();
   });
 });

diff --git a/infra/relay/src/agentActivity/ApnsDeliveries.ts b/infra/relay/src/agentActivity/ApnsDeliveries.ts
--- a/infra/relay/src/agentActivity/ApnsDeliveries.ts
+++ b/infra/relay/src/agentActivity/ApnsDeliveries.ts
@@ -210,6 +210,20 @@
   };
 }
 
+// Completions replayed long after the fact (server restarts republish every
+// recently-finished thread) must not ring the device again — on any channel:
+// push notifications, Live Activity update alerts, and end alerts all honor
+// the same freshness window.
+const TERMINAL_NOTIFICATION_FRESHNESS_MS = 2 * 60 * 1_000;
+
+function isFreshTerminalRow(row: { readonly updatedAt: string }, nowMs: number): boolean {
+  const updatedAtMs = Option.match(DateTime.make(row.updatedAt), {
+    onNone: () => null,
+    onSome: (dt) => dt.epochMilliseconds,
+  });
+  return updatedAtMs !== null && nowMs - updatedAtMs <= TERMINAL_NOTIFICATION_FRESHNESS_MS;
+}
+
 // Alert copy for an update whose aggregate contains threads that finished
 // (Done/Failed) since the previously delivered aggregate — the mid-flight
 // completion buzz while other agents keep the activity alive. Requires the
@@ -219,6 +233,7 @@
   readonly previousAggregate: RelayAgentActivityAggregateState | null;
   readonly nextAggregate: RelayAgentActivityAggregateState;
   readonly preferences: RelayAgentAwarenessPreferences | null;
+  readonly nowMs: number;
 }): ApnsLiveActivityAlert | null {
   if (input.previousAggregate === null) {
     return null;
@@ -230,6 +245,9 @@
     if (row.phase !== "completed" && row.phase !== "failed") {
       return false;
     }
+    if (!isFreshTerminalRow(row, input.nowMs)) {
+      return false;
+    }
     const previousPhase = previousPhases.get(row.threadId);
     return (
       previousPhase !== undefined &&
@@ -255,11 +273,15 @@
 export function alertForTerminalAggregate(input: {
   readonly aggregate: RelayAgentActivityAggregateState | null;
   readonly preferences: RelayAgentAwarenessPreferences | null;
+  readonly nowMs: number;
 }): ApnsLiveActivityAlert | null {
   const row = input.aggregate?.activities[0];
   if (!row || (row.phase !== "completed" && row.phase !== "failed")) {
     return null;
   }
+  if (!isFreshTerminalRow(row, input.nowMs)) {
+    return null;
+  }
   if (!alertAllowedForPhase(input.preferences, row.phase)) {
     return null;
   }
@@ -298,10 +320,6 @@
   );
 }
 
-// Completions replayed long after the fact (server restarts republish every
-// recently-finished thread) must not ring the device again.
-const TERMINAL_NOTIFICATION_FRESHNESS_MS = 2 * 60 * 1_000;
-
 function notificationForAggregate(input: {
   readonly target: LiveActivities.TargetRow;
   readonly aggregate: RelayAgentActivityAggregateState | null;
@@ -318,14 +336,11 @@
   if (!activity) {
     return null;
   }
-  if (activity.phase === "completed" || activity.phase === "failed") {
-    const updatedAtMs = Option.match(DateTime.make(activity.updatedAt), {
-      onNone: () => null,
-      onSome: (dt) => dt.epochMilliseconds,
-    });
-    if (updatedAtMs === null || input.nowMs - updatedAtMs > TERMINAL_NOTIFICATION_FRESHNESS_MS) {
-      return null;
-    }
+  if (
+    (activity.phase === "completed" || activity.phase === "failed") &&
+    !isFreshTerminalRow(activity, input.nowMs)
+  ) {
+    return null;
   }
   const enabled =
     (activity.phase === "waiting_for_approval" && preferences.notifyOnApproval) ||
@@ -419,6 +434,7 @@
             previousAggregate,
             nextAggregate,
             preferences,
+            nowMs: input.nowMs,
           }),
       }
     : "suppressed";
@@ -1100,6 +1116,7 @@
             : alertForTerminalAggregate({
                 aggregate: delivery.aggregate,
                 preferences: parsePreferences(input.target.preferences_json),
+                nowMs: input.nowMs,
               })
           : delivery.alert;
       const result = yield* deliveryQueue.enqueueLiveActivity({

You can send follow-ups to the cloud agent here.

Comment thread infra/relay/src/agentActivity/ApnsDeliveries.ts
…A pref gates

Fixes the real findings from the PR review bots ahead of the TestFlight
build:

- Deferred publish confirmations (tombstones and first-state
  completions) now re-enqueue through the same drainable worker as every
  other publish instead of a detached fiber calling the publisher
  directly, so a confirmed tombstone can never race an in-flight live
  update; a deadline map replaces the pending set, which also guarantees
  a single scheduled confirmation per thread and clears when the state
  recovers. (cursor/macroscope High)
- Newly-terminal transitions bypass the 15s live-activity update
  throttle: a completion landing in the same window as a new start keeps
  activeCount unchanged and would previously be suppressed, silently
  dropping the Done transition and its buzz. (macroscope High)
- The newly-terminal alert honors the same 2-minute freshness rule as
  terminal push notifications, so replayed aggregates repaint without
  ringing. (cursor Medium)
- Live Activity arming and priming treat an unset preference as enabled
  (the toggle's default): fresh installs now prime, and arm-on-send
  respects an explicit off. (cursor High/Medium)

Remaining findings triaged as stale (superseded designs), intended
behavior (replay painting recent Done, LA-disabled end retiring the
token), or accepted edge cases (grace extension on re-registration,
completion alerts beyond the 3-row display cap, ready-with-lastError
mapping to Done, cancelled turns showing Done).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Stale confirm deadline publishes tombstone
    • Cleared the thread's publishConfirmDeadlines entry in the unchanged-identity early-return path so a recovered projection cancels the deferred confirmation, preventing a later transient null state from confirming immediately against a stale expired deadline.

Create PR

Or push these changes by commenting:

@cursor push e02169b78e
Preview (e02169b78e)
diff --git a/apps/server/src/relay/AgentAwarenessRelay.ts b/apps/server/src/relay/AgentAwarenessRelay.ts
--- a/apps/server/src/relay/AgentAwarenessRelay.ts
+++ b/apps/server/src/relay/AgentAwarenessRelay.ts
@@ -410,6 +410,11 @@
     const publishIdentity = agentAwarenessPublishIdentity(snapshot.state);
     const publishedStateByThread = yield* Ref.get(publishedStateByThreadRef);
     if (publishedStateByThread.get(threadId) === publishIdentity) {
+      // The projection recovered to the last published state, so any deferred
+      // confirmation (tombstone or first-state completion) is moot. Clear the
+      // deadline so the next divergence gets a fresh deferral window instead
+      // of confirming immediately against a stale, expired deadline.
+      publishConfirmDeadlines.delete(threadId);
       yield* Effect.logDebug("agent activity publish skipped; projected state unchanged", {
         environmentId,
         threadId,

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 97ed442. Configure here.

Comment thread apps/server/src/relay/AgentAwarenessRelay.ts
…d state

A recovered projection exits at the unchanged-identity dedupe, which sits
above the confirmation block, so a deferred publish's deadline survived
recovery. A much later transient null would then find the deadline
already expired and publish its tombstone immediately, bypassing the
deferral window entirely. The dedupe path now clears any pending
deadline: the state is back at what was last published, so the deferred
confirmation is moot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@juliusmarminge juliusmarminge added the 🚀 Mobile Continuous Deployment Trigger Expo preview build label Jul 7, 2026
Fixes the Effect Service Conventions check: the service builds a plain
object with no effectful acquisition, so wrapping it in Effect.sync just
to feed Layer.effect was the make-only-to-force-Layer.effect shape the
conventions call out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🚀 Expo continuous deployment is ready!

  • Project → t3-code
  • Platforms → android, ios
  • Scheme → t3code-preview
  🤖 Android 🍎 iOS
Fingerprint 106610ae982be2d8aa28dda9217ff853daf9d30b 4f388ce49fd16563ef62f563f42c0528f092d42b
Build Details Build Permalink
DetailsDistribution: INTERNAL
Build profile: preview:dev
Runtime version: 106610ae982be2d8aa28dda9217ff853daf9d30b
App version: 0.1.0
Git commit: 74888084257320d42e7562c74996c949010bdf45
Build Permalink
DetailsDistribution: INTERNAL
Build profile: preview:dev
Runtime version: 4f388ce49fd16563ef62f563f42c0528f092d42b
App version: 0.1.0
Git commit: c622444d589c97adb591101ffca9161d785806b0
Update Details Update Permalink
DetailsBranch: pr-3685
Runtime version: 106610ae982be2d8aa28dda9217ff853daf9d30b
Git commit: 74888084257320d42e7562c74996c949010bdf45
Update Permalink
DetailsBranch: pr-3685
Runtime version: 4f388ce49fd16563ef62f563f42c0528f092d42b
Git commit: 74888084257320d42e7562c74996c949010bdf45
Update QR

@juliusmarminge
juliusmarminge merged commit d8e0ba3 into main Jul 7, 2026
16 checks passed
@juliusmarminge
juliusmarminge deleted the t3code/live-activities-improvements branch July 7, 2026 07:56
adamfgr pushed a commit to agriffiths-bots/t3code that referenced this pull request Jul 9, 2026
* Improve live activity routing and diagnostics (pingdotgg#3685)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* Use variant-specific splash icons in mobile app (pingdotgg#3762)

* Fix Expo widget asset wiring order (pingdotgg#3763)

* Extend Done display to 15 minutes and show up to 5 Live Activity banner rows (pingdotgg#3761)

* Lead with the outcome when no agents are active in the Live Activity (pingdotgg#3768)

* Add T3 Connect onboarding for mobile and web (pingdotgg#3765)

* Revert "Add T3 Connect onboarding for mobile and web" (pingdotgg#3776)

* Expose Clerk Google sign-in env vars to Expo (pingdotgg#3772)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Revert "Revert "Add T3 Connect onboarding for mobile and web"" (pingdotgg#3777)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Use rounded depth logo for production splash screen (pingdotgg#3780)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* Upgrade Clerk toolchain to latest versions (pingdotgg#3785)

* fix(release): bump electron-builder so pnpm 11 deduped deps land in the asar (pingdotgg#3790)

* chore: regenerate pnpm-lock after upstream sync (20260709)

Reconcile the lockfile with the fork's package.json set (croner, web-push,
hast-util-*, etc.) after cherry-picking upstream's Clerk toolchain and
electron-builder bumps. Frozen-lockfile install verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rowan <rowan@cardow.co>
Co-authored-by: wizzoapp[bot] <254688279+wizzoapp[bot]@users.noreply.github.com>
tarik02 referenced this pull request in tarik02-org/t3code Jul 13, 2026
* Add middle-click close for right panel tabs (#3161)

Co-authored-by: Julius Marminge <jmarminge@gmail.com>

* fix: warm WSL before preflight in WSL-only backend mode (#3588)

* Add Claude Sonnet 5 as the default Claude model (#3620)

* Restore the ultrathink frame border effect (#3625)

* fix(dev): Fix electron dev launch and add test (#3662)

* Add adaptive split-view layout for iPad/mobile workspace (#3514)

Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius@mac.lan>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(mobile): compile patched native pods from source on EAS (#3667)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Make the thread composer read as elevated liquid glass (#3668)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Upgrade Vite Plus and enable bundled dev opt-in (#3679)

* Surface pending tasks in mobile home and draft flow (#3670)

* fix(mobile): combined test branch — scroll, back-swipe, thread lists, computer switching (#3687)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* Add repo-root favicon.svg so t3 code shows its own icon (#3683)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Load thread snapshots over HTTP before live sync (#3719)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* Fix mobile legend anchor under automatic iOS insets (#3684)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Improve live activity routing and diagnostics (#3685)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* Prevent Add Project sheet from collapsing on relayout (#3759)

* Use variant-specific splash icons in mobile app (#3762)

* Fix Expo widget asset wiring order (#3763)

* Extend Done display to 15 minutes and show up to 5 Live Activity banner rows (#3761)

* Clear VCS presentation state on finish (#3764)

* Lead with the outcome when no agents are active in the Live Activity (#3768)

* Add T3 Connect onboarding for mobile and web (#3765)

* Revert "Add T3 Connect onboarding for mobile and web" (#3776)

* Expose Clerk Google sign-in env vars to Expo (#3772)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Set up Cursor Cloud dev environment (web + Android toolchain) (#3755)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>

* Revert "Revert "Add T3 Connect onboarding for mobile and web"" (#3777)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Use rounded depth logo for production splash screen (#3780)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* fix(release): stage pnpm 11 allowBuilds for desktop installs (#3781)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* Upgrade Clerk toolchain to latest versions (#3785)

* fix(release): bump electron-builder so pnpm 11 deduped deps land in the asar (#3790)

* Fix desktop native optional dependency packaging (#3816)

* [codex] Upgrade Clerk stack (#3821)

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

* [codex] Preserve worktree metadata during branch sync (#3822)

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

* feat(client): persist offline environment data and mobile preferences (#3795)

Co-authored-by: Julius Marminge <julius@mac.lan>
Co-authored-by: codex <codex@users.noreply.github.com>

* [codex] Label max and ultra reasoning (#3824)

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

* fix(mobile): embed fonts and render project favicons reliably (#3823)

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

* Show compact PR number badges in mobile thread rows (#3827)

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

* Expose mobile PR indicator labels to accessibility (#3828)

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

* Fix truncated chat error alert layout (#3899)

* fix(marketing): show platform-appropriate commit shortcut on the website (#3644)

* [codex] Add Android mobile support (#3579)

Co-authored-by: Horus Lugo <horusgoul@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: maria-rcks <maria@kuuro.net>
Co-authored-by: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com>
Co-authored-by: Ben Davis <45952064+bmdavis419@users.noreply.github.com>
Co-authored-by: Alex <me@pixp.cc>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius@mac.lan>

---------

Co-authored-by: Hugo Blom <6117705+huxcrux@users.noreply.github.com>
Co-authored-by: Julius Marminge <jmarminge@gmail.com>
Co-authored-by: Utkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: Theo Browne <me@t3.gg>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius@mac.lan>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
Co-authored-by: Rowan <rowan@cardow.co>
Co-authored-by: Patricio Gómez Meneses <107218376+Prgm-code@users.noreply.github.com>
Co-authored-by: Jake Leventhal <jakeleventhal@me.com>
Co-authored-by: Vedank Purohit <VedankPurohit2@gmail.com>
Co-authored-by: Horus Lugo <horusgoul@gmail.com>
Co-authored-by: maria-rcks <maria@kuuro.net>
Co-authored-by: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com>
Co-authored-by: Ben Davis <45952064+bmdavis419@users.noreply.github.com>
Co-authored-by: Alex <me@pixp.cc>
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Jul 27, 2026
## What's Changed
* Add middle-click close for right panel tabs by @huxcrux in https://github.com/pingdotgg/t3code/pull/3161
* fix: warm WSL before preflight in WSL-only backend mode by @UtkarshUsername in https://github.com/pingdotgg/t3code/pull/3588
* Add Claude Sonnet 5 as the default Claude model by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3620
* Restore the ultrathink frame border effect by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3625
* fix(dev): Fix electron dev launch and add test by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3662
* Add adaptive split-view layout for iPad/mobile workspace by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3514
* fix(mobile): compile patched native pods from source on EAS by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3667
* Make the thread composer read as elevated liquid glass by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3668
* Upgrade Vite Plus and enable bundled dev opt-in by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3679
* Surface pending tasks in mobile home and draft flow by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3670
* fix(mobile): combined test branch — scroll, back-swipe, thread lists, computer switching by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3687
* Add repo-root favicon.svg so t3 code shows its own icon by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3683
* Load thread snapshots over HTTP before live sync by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3719
* Fix mobile legend anchor under automatic iOS insets by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3684
* Improve live activity routing and diagnostics by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3685
* Prevent Add Project sheet from collapsing on relayout by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3759
* Use variant-specific splash icons in mobile app by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3762
* Fix Expo widget asset wiring order by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3763
* Extend Done display to 15 minutes and show up to 5 Live Activity banner rows by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3761
* Clear VCS presentation state on finish by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3764
* Lead with the outcome when no agents are active in the Live Activity by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3768
* Add T3 Connect onboarding for mobile and web by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3765
* Revert "Add T3 Connect onboarding for mobile and web" by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3776
* Expose Clerk Google sign-in env vars to Expo by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3772
* Set up Cursor Cloud dev environment (web + Android toolchain) by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3755
* Revert "Revert "Add T3 Connect onboarding for mobile and web"" by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3777
* Use rounded depth logo for production splash screen by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3780
* fix(release): stage pnpm 11 allowBuilds for desktop installs by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3781
* Upgrade Clerk toolchain to latest versions by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3785
* fix(release): bump electron-builder so pnpm 11 deduped deps land in the asar by @avocardow in https://github.com/pingdotgg/t3code/pull/3790
* Fix desktop native optional dependency packaging by @Prgm-code in https://github.com/pingdotgg/t3code/pull/3816
* [codex] Upgrade Clerk stack by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3821
* [codex] Preserve worktree metadata during branch sync by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3822
* feat(client): persist offline environment data and mobile preferences by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3795
* [codex] Label max and ultra reasoning by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3824
* fix(mobile): embed fonts and render project favicons reliably by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3823
* Show compact PR number badges in mobile thread rows by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3827
* Expose mobile PR indicator labels to accessibility by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3828
* Fix truncated chat error alert layout by @jakeleventhal in https://github.com/pingdotgg/t3code/pull/3899
* fix(marketing): show platform-appropriate commit shortcut on the website by @VedankPurohit in https://github.com/pingdotgg/t3code/pull/3644
* [codex] Add Android mobile support by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3579
* Use client-side fallbacks for missing project favicons by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3959
* Skip stale working-task notifications by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3961
* Prepare Android beta branding and review diff UI by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3967
* perf(web): duty-cycle status animations and remove fixed noise overlay by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3978
* fix(docs): correct CI task-runner commands in ci.md by @kridaydave in https://github.com/pingdotgg/t3code/pull/3990
* fix(docs): repair broken source links in architecture overview by @kridaydave in https://github.com/pingdotgg/t3code/pull/3991
* fix(docs): replace stale codething-mvp absolute paths with repo-relative links by @kridaydave in https://github.com/pingdotgg/t3code/pull/3992
* docs: Add T3 Code Legal Docs by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3972
* Fix Legal modal header crash by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4000
* [codex] Fix onboarding connection status by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4001
* Isolate native diff highlight grammar state by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4029
* Fix macOS fullscreen titlebar spacing by @D3OXY in https://github.com/pingdotgg/t3code/pull/4019
* Prevent duplicate project workspace roots by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3829
* Normalize over-indented markdown list items by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4020
* Resolve localhost preview URLs for remote environments by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4011
* fix(mobile): Send composer images in upload wire format by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4035
* Fix iOS terminal Enter input encoding by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4043
* Add native mobile share target support by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4021
* [codex] Expand real-route app store screenshot harness by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4014
* fix(server): use CLAUDE_CONFIG_DIR instead of HOME for Claude instanc… by @dmstoykov in https://github.com/pingdotgg/t3code/pull/4017
* Fix dropped events during initial thread snapshot by @D3OXY in https://github.com/pingdotgg/t3code/pull/4079
* feat: show nightly update changelog tooltip by @HugoVizcainoSantana in https://github.com/pingdotgg/t3code/pull/3832
* fix(git): treat selected commit paths literally by @EricTsai83 in https://github.com/pingdotgg/t3code/pull/3998
* fix(server): stabilize non-repository Git diagnostics by @EricTsai83 in https://github.com/pingdotgg/t3code/pull/4077
* Refresh app icons across release variants by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4080
* Update marketing GitHub star count by @AmoonPod in https://github.com/pingdotgg/t3code/pull/4088
* fix(marketing): correct Cursor icon color by @AmoonPod in https://github.com/pingdotgg/t3code/pull/4090
* Normalize protocol-relative remote host input as https by @kridaydave in https://github.com/pingdotgg/t3code/pull/3971
* fix(cursor): default binary path to cursor-agent (avoid path conflict w/ grok) by @BunnyGamezsc in https://github.com/pingdotgg/t3code/pull/4094
* Fix documented task-runner commands (bun run -> vp) by @kridaydave in https://github.com/pingdotgg/t3code/pull/3965
* Allow preview panel to grow on wide displays by @olivoil in https://github.com/pingdotgg/t3code/pull/4044
* fix: prevent initial right-click from selecting a context menu item by @Fazalkadivar21 in https://github.com/pingdotgg/t3code/pull/3877
* Fix duplicate keybinding rule when replacing with an existing rule by @kridaydave in https://github.com/pingdotgg/t3code/pull/3969
* fix(server): image upload crashed dispatchCommand with a stack overflow by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3952
* Remove unused code parameter from describePreviewError by @kridaydave in https://github.com/pingdotgg/t3code/pull/3970
* [codex] prevent ACP assistant ID collisions after restarts by @maxwellyoung in https://github.com/pingdotgg/t3code/pull/3932
* fix(web): inset Windows desktop scrollbars from resize edge by @nateEc in https://github.com/pingdotgg/t3code/pull/4097
* [codex] fix mobile composer Enter behavior by @maxwellyoung in https://github.com/pingdotgg/t3code/pull/3930
* feat(server): include runtime model and effort in Codex developer instructions by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3948
* fix(ux): spamming cmd + , no longer stack opening settings by @jamesx0416 in https://github.com/pingdotgg/t3code/pull/2757
* fix(terminal): strip AppImage runtime env from spawned terminals by @leorivastech in https://github.com/pingdotgg/t3code/pull/3108
* fix(server): thread cwd through Claude capability probe (#2048) by @mvanhorn in https://github.com/pingdotgg/t3code/pull/2124
* [codex] fix: guard invalid web timestamps by @StiensWout in https://github.com/pingdotgg/t3code/pull/3515
* [codex] fix: tolerate invalid latest user message timestamps by @StiensWout in https://github.com/pingdotgg/t3code/pull/3521
* [codex] Fix provider update checks restore defaults by @StiensWout in https://github.com/pingdotgg/t3code/pull/3531
* fix(server): skip undecodable provider runtime rows when listing sessions by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3951
* Share MCP OAuth locks across Codex shadow homes by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4104
* Preserve T3 Code identity in macOS development launcher by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4102
* fix(web): increase contrast of question option descriptions by @xxashxx-svg in https://github.com/pingdotgg/t3code/pull/3867
* feat: draft hero landing on the index route by @yordis in https://github.com/pingdotgg/t3code/pull/4055
* feat: file explorer mention actions and zoom-aware context menus by @yordis in https://github.com/pingdotgg/t3code/pull/4054
* fix(mobile): restore iOS home screen branding by @PixPMusic in https://github.com/pingdotgg/t3code/pull/4025
* perf(client): defer active thread cache writes by @Chrrxs in https://github.com/pingdotgg/t3code/pull/4006
* Default diffs to working changes by @jakeleventhal in https://github.com/pingdotgg/t3code/pull/3974
* Add Grok to marketing site provider list by @Aditya190803 in https://github.com/pingdotgg/t3code/pull/3484
* Fix reopening existing Diff tab by @jakeleventhal in https://github.com/pingdotgg/t3code/pull/3973
* Fix sending messages during active turns by @jakeleventhal in https://github.com/pingdotgg/t3code/pull/3919
* [codex] Route OpenCode missing-session errors through Effect by @StiensWout in https://github.com/pingdotgg/t3code/pull/3608
* [fix/feat:ui] Show default option badge by @sandersonstabo in https://github.com/pingdotgg/t3code/pull/3232
* [fix/feat:ui] Preserve open-in editor brand colors by @sandersonstabo in https://github.com/pingdotgg/t3code/pull/3225
* fix(web): handle macOS Home and End in composer by @GuilhermeVieiraDev in https://github.com/pingdotgg/t3code/pull/2508
* Allow failed remote environments to be removed by @zepi2509 in https://github.com/pingdotgg/t3code/pull/4084
* [codex] canonicalize client timestamps by @maxwellyoung in https://github.com/pingdotgg/t3code/pull/4112
* [fix/feat:ui] Make selected menu checks blue by @sandersonstabo in https://github.com/pingdotgg/t3code/pull/3234
* fix(desktop): Validate WSL node version against engine range after probe success by @UtkarshUsername in https://github.com/pingdotgg/t3code/pull/3621
* Refresh splash screen and favicon branding by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4120
* Add terminal selection copy action by @tarik02 in https://github.com/pingdotgg/t3code/pull/2904
* Add isolated app testing workflow by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4121
* feat(web): themed sidebar header art for nightly and dev builds by @maria-rcks in https://github.com/pingdotgg/t3code/pull/4130
* feat: add headless `t3 connect` setup for SSH hosts by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3749
* Refine T3 Connect authorization surfaces by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4159
* fix: increase OpenCode server startup timeout from 5s to 30s by @UtkarshUsername in https://github.com/pingdotgg/t3code/pull/4132
* fix(shared): delete unused agentAwareness phase predicates by @kridaydave in https://github.com/pingdotgg/t3code/pull/4134
* fix(mobile): Stabilize native stack option updates by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4037
* Make test-t3-app skill discoverable by Claude Code by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4162
* fix(web): improve dev sidebar backdrop contrast & remove version pills by @maria-rcks in https://github.com/pingdotgg/t3code/pull/4166
* Fix draft banner stack overlap by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4164
* Add portable mobile app testing guidance by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4165
* fix(client): use lightweight connection probe by @eeinarsson in https://github.com/pingdotgg/t3code/pull/4137
* fix(server): resolve Claude SDK executable path on Windows npm installs by @nsxdavid in https://github.com/pingdotgg/t3code/pull/3740
* Fix project action preview settings persistence by @keeperxy in https://github.com/pingdotgg/t3code/pull/3842
* fix(desktop): allow clipboard writes in the preview browser by @carlosricojr in https://github.com/pingdotgg/t3code/pull/3889
* fix(web): handle sidebar shortcut before editors by @Bortlesboat in https://github.com/pingdotgg/t3code/pull/3921
* fix(server): recognize Bedrock-backed Claude as authenticated by @PieterVanZyl-Dev in https://github.com/pingdotgg/t3code/pull/3931
* Fix incorrect pluralization of “entry” by @McMelonTV in https://github.com/pingdotgg/t3code/pull/3933
* feat(server): title background-task work-log rows with the task name by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3751
* fix: delegate OpenCode session titles to provider by @tris203 in https://github.com/pingdotgg/t3code/pull/3720
* Archive selected threads from the context menu by @theduke in https://github.com/pingdotgg/t3code/pull/3895
* fix(cli): support force removing projects by @Bortlesboat in https://github.com/pingdotgg/t3code/pull/3922
* fix: allow sidebar to be shrunk when wider than viewport by @shoaib050326 in https://github.com/pingdotgg/t3code/pull/2456
* fix(codex): show web search query and url in tool call details by @GuilhermeVieiraDev in https://github.com/pingdotgg/t3code/pull/2093
* Add Codex launch arguments setting by @jamesx0416 in https://github.com/pingdotgg/t3code/pull/2892
* [orchestration] Clear stale active turn when session becomes inactive by @Andrew-Forster in https://github.com/pingdotgg/t3code/pull/3159
* Regenerate Codex reset credit protocol bindings by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4173
* fix(preview): preserve direct localhost navigation by @Chrrxs in https://github.com/pingdotgg/t3code/pull/3939
* Synchronize mobile threads with authoritative shell snapshots by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4163
* Gate iOS glass layout on native support by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4032
* fix(opencode): resume the OpenCode session on follow-ups instead of starting an empty one by @vdmkotai in https://github.com/pingdotgg/t3code/pull/3617
* fix(server): use CLI for OpenCode health check instead of spawning server by @UtkarshUsername in https://github.com/pingdotgg/t3code/pull/4153
* fix(web): scope timeline minimap hover target to the side gutter by @xxashxx-svg in https://github.com/pingdotgg/t3code/pull/3869
* [codex] show complete approval details by @maxwellyoung in https://github.com/pingdotgg/t3code/pull/4111
* fix(web): paint text selection over composer chips by @yordis in https://github.com/pingdotgg/t3code/pull/4139
* [codex] preserve custom model slugs by @maxwellyoung in https://github.com/pingdotgg/t3code/pull/4168
* fix(web): preview workspace images in the file panel by @Rhiz3K in https://github.com/pingdotgg/t3code/pull/3996
* feat(web): drag files from the explorer into the chat composer by @yordis in https://github.com/pingdotgg/t3code/pull/4140
* fix(desktop): preserve main window bounds by @anirudhsama in https://github.com/pingdotgg/t3code/pull/3851
* perf(orchestration): speed up new-chat propagation and offline catch-up by @RusiruSadathana in https://github.com/pingdotgg/t3code/pull/4177
* Finale: upgrade changed files card to fix various UI issues by @sandersonstabo in https://github.com/pingdotgg/t3code/pull/4113
* Pass CLI OAuth config to hosted web deploy by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4186
* fix(web): always show environment chip for remote projects by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4217
* fix(web): keep composer editable while disconnected by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4241
* fix: better defaults — Claude 1M context, Codex gpt-5.6, worktrees from origin main by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4240
* fix(claude): handle all SDK stream messages; stop spurious work-log warning rows by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4244
* Sidebar v2 beta: flat thread list with a server-backed settled lifecycle by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4026
* fix(settings): validate the add-provider wizard step before advancing (#2813) by @leorivastech in https://github.com/pingdotgg/t3code/pull/3100
* fix(claude): isolate capability probe from user MCP servers by @jbbottoms in https://github.com/pingdotgg/t3code/pull/4015
* Preserve connecting status while a turn starts by @D3OXY in https://github.com/pingdotgg/t3code/pull/4101
* fix(server): stop restoring stale OpenCode models by @nateEc in https://github.com/pingdotgg/t3code/pull/4095
* [codex] keep scoped package references as text by @maxwellyoung in https://github.com/pingdotgg/t3code/pull/4167
* fix(web): default provider selection for users without Codex by @mfazekas in https://github.com/pingdotgg/t3code/pull/4117
* Unify temporary worktree branch naming by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4278
* fix(web): use message-square icon for settled icon-less project threads in sidebar v2 by @UtkarshUsername in https://github.com/pingdotgg/t3code/pull/4279
* Stabilize sidebar settling animations by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4280
* Restore Copy Link in chat link context menu by @caezium in https://github.com/pingdotgg/t3code/pull/4161
* fix(desktop): handle EPIPE errors on stdout/stderr to prevent crash dialog by @UtkarshUsername in https://github.com/pingdotgg/t3code/pull/4213
* Preserve draft thread highlighting during promotion by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4283
* Move mobile working timer into the thread timeline by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4285
* Stabilize PR status lookups and provider session lifecycle by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4281
* fix: open command palette instead of custom dialog for new thread picker in SidebarV2 by @UtkarshUsername in https://github.com/pingdotgg/t3code/pull/4269
* fix(server): don't drop sticky PR fallback when remote URL can't be resolved by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4289
* feat(web): copy branch name via right-click in the branch selector by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4275
* Add remote server updates and standalone service management by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4286
* Refine light-mode sidebar surfaces by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4268
* fix(mobile): don't mark Android VPN/Tailscale as offline when connected by @Wraient in https://github.com/pingdotgg/t3code/pull/3949
* improve and prevent silent thread branch drift and PR fetching by @justsomelegs in https://github.com/pingdotgg/t3code/pull/2284
* Refresh web application surfaces and dark-mode dialogs by @maria-rcks in https://github.com/pingdotgg/t3code/pull/4319
* fix(web): new-thread defaults ignored for remote environments by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4276
* feat: add "Auto" runtime mode — AI-reviewed approvals for Codex and Claude by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4272
* Add shared t3.json project configuration support by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4317
* Unify dialog glass and fix composer overlays by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4365
* fix(web): warn before silent Windows updates by @nateEc in https://github.com/pingdotgg/t3code/pull/4350
* [codex] Move project grouping to General settings by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4313
* [codex] Group project scopes in mobile thread lists by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4314
* [codex] Move mobile project grouping to General settings by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4315
* [codex] Deduplicate connection failure messaging by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4367
* Restore grouped project filtering in Sidebar V2 by @shivamhwp in https://github.com/pingdotgg/t3code/pull/4282
* [codex] restore Sidebar V2 project actions by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4373
* [codex] Group projects in new-thread pickers by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4312
* fix(web): restore dark composer toolbar styling by @maria-rcks in https://github.com/pingdotgg/t3code/pull/4375
* Fix thread tooltip folder icon color by @maria-rcks in https://github.com/pingdotgg/t3code/pull/4383
* fix(server): parse CLI version in update preflight by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4389
* fix(web): sidebar v2 polish — jump hints, working duration, in-flight fade, settled sort by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4274
* Fix logical project grouping labels on mobile by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4391
* Add preview color scheme controls and simplify project grouping by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4385
* fix(cli): publish nightly branded favicons by @maria-rcks in https://github.com/pingdotgg/t3code/pull/4372
* Fix thread loading flash by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4396
* fix(client-runtime): keep a warm thread un-settled despite a merged/closed PR by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4309
* Fix composer context strip alignment and glass shell by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4404
* Polish iOS git progress overlay with glass effects by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4387
* Improve composer glass fallbacks by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4406
* feat(web): collapse large git diffs by default to make chat more readable by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4409
* Stop new threads inheriting checkout/branch from viewed thread by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4411
* fix: tone down branch-mismatch banner by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4416
* fix: Claude Code skills discoverable for the composer $ picker by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4414
* fix(web): keep settled threads reachable when opened directly by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4413
* feat(sidebar-v2): thread snoozing by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4311
* Upgrade Clerk packages and Expo integration by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4440
* Increase light-mode contrast for user message bubbles by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4441
* Restore model picker layout and retain iterative test state by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4450
* Color settled PR labels on hover by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4451
* [codex] Fix glass hover compositing artifacts by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4446
* Add Claude Opus 5 model by @thomaslittle in https://github.com/pingdotgg/t3code/pull/4472
* feat(web): add collapse-all toggle to diff panel by @0x4bs3nt in https://github.com/pingdotgg/t3code/pull/4475
* feat(web): show fast mode as a bolt instead of a "Normal" label by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4488
* feat(dev): keep worktree dev state isolated on T3 Code dev servers by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4555
* feat(dev): Make t3 code dev instances shareable over Tailscale by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4556
* fix(dev): skip browser-blocked ports by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4608
* fix: cut websocket throughput in half by pruning activity payloads by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4622
* perf(mobile): defer work-log detail serialization by @saphid in https://github.com/pingdotgg/t3code/pull/4607
* test: account for lazy thread feed details by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4628
* feat(relay): limit managed tunnels per user by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4530
* Add managed tunnel limits migration by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4635
* Add background preview capture and picture-in-picture support by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4397
* feat(web): prompt stash — cmd+S saves the composer to a per-provider queue by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4453
* [codex] Upgrade Effect and Alchemy betas by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4643
* feat: allow new thread creation through project breadcrumbs by @0x4bs3nt in https://github.com/pingdotgg/t3code/pull/4638
* fix(web): scope PR state to the thread branch by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4460
* Drop redundant Relay user indexes by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4648
* feat(connect): release the Cloudflare tunnel when the environment shuts down by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4531
* Fix Relay Worker RuntimeContext wiring by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4653
* Fix live sidebar resize limits and defer Alchemy runtime context by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4655
* fix(web): constrain branch toolbar context by @maxktz in https://github.com/pingdotgg/t3code/pull/4657
* Keep MCP credentials alive across provider turns by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4659
* fix: close actions dropdown when editing by @0x4bs3nt in https://github.com/pingdotgg/t3code/pull/4660
* fix(preview): stabilize PiP viewport identity by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4661
* Add glass styling for thread tooltips and simplify preview tab handling by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4665
* Use tarball archiving for hosted web deploys by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4669
* fix(server): bound editor discovery during config loading by @maria-rcks in https://github.com/pingdotgg/t3code/pull/4291
* Prevent draft thread detail polling before shell registration by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4670
* feat: add configurable source control writing settings by @maria-rcks in https://github.com/pingdotgg/t3code/pull/4204
* feat(diff-panel): show total line additions and deletions by @0x4bs3nt in https://github.com/pingdotgg/t3code/pull/4674
* Clear provider update actions while updating by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4676
* Fix sidebar highlighting for draft threads by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4679
* Use glass surfaces for web toasts by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4681
* Show origin ref in branch trigger label by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4680
* fix(mobile): match react version to react-native 0.85.3 vendored renderer (19.2.3) by @KrzysztofMoch in https://github.com/pingdotgg/t3code/pull/4675

## New Contributors
* @avocardow made their first contribution in https://github.com/pingdotgg/t3code/pull/3790
* @Prgm-code made their first contribution in https://github.com/pingdotgg/t3code/pull/3816
* @jakeleventhal made their first contribution in https://github.com/pingdotgg/t3code/pull/3899
* @VedankPurohit made their first contribution in https://github.com/pingdotgg/t3code/pull/3644
* @kridaydave made their first contribution in https://github.com/pingdotgg/t3code/pull/3990
* @dmstoykov made their first contribution in https://github.com/pingdotgg/t3code/pull/4017
* @HugoVizcainoSantana made their first contribution in https://github.com/pingdotgg/t3code/pull/3832
* @EricTsai83 made their first contribution in https://github.com/pingdotgg/t3code/pull/3998
* @AmoonPod made their first contribution in https://github.com/pingdotgg/t3code/pull/4088
* @BunnyGamezsc made their first contribution in https://github.com/pingdotgg/t3code/pull/4094
* @olivoil made their first contribution in https://github.com/pingdotgg/t3code/pull/4044
* @Fazalkadivar21 made their first contribution in https://github.com/pingdotgg/t3code/pull/3877
* @maxwellyoung made their first contribution in https://github.com/pingdotgg/t3code/pull/3932
* @nateEc made their first contribution in https://github.com/pingdotgg/t3code/pull/4097
* @leorivastech made their first contribution in https://github.com/pingdotgg/t3code/pull/3108
* @xxashxx-svg made their first contribution in https://github.com/pingdotgg/t3code/pull/3867
* @yordis made their first contribution in https://github.com/pingdotgg/t3code/pull/4055
* @Chrrxs made their first contribution in https://github.com/pingdotgg/t3code/pull/4006
* @Aditya190803 made their first contribution in https://github.com/pingdotgg/t3code/pull/3484
* @zepi2509 made their first contribution in https://github.com/pingdotgg/t3code/pull/4084
* @eeinarsson made their first contribution in https://github.com/pingdotgg/t3code/pull/4137
* @keeperxy made their first contribution in https://github.com/pingdotgg/t3code/pull/3842
* @carlosricojr made their first contribution in https://github.com/pingdotgg/t3code/pull/3889
* @Bortlesboat made their first contribution in https://github.com/pingdotgg/t3code/pull/3921
* @PieterVanZyl-Dev made their first contribution in https://github.com/pingdotgg/t3code/pull/3931
* @McMelonTV made their first contribution in https://github.com/pingdotgg/t3code/pull/3933
* @tris203 made their first contribution in https://github.com/pingdotgg/t3code/pull/3720
* @theduke made their first contribution in https://github.com/pingdotgg/t3code/pull/3895
* @shoaib050326 made their first contribution in https://github.com/pingdotgg/t3code/pull/2456
* @vdmkotai made their first contribution in https://github.com/pingdotgg/t3code/pull/3617
* @Rhiz3K made their first contribution in https://github.com/pingdotgg/t3code/pull/3996
* @anirudhsama made their first contribution in https://github.com/pingdotgg/t3code/pull/3851
* @RusiruSadathana made their first contribution in https://github.com/pingdotgg/t3code/pull/4177
* @jbbottoms made their first contribution in https://github.com/pingdotgg/t3code/pull/4015
* @mfazekas made their first contribution in https://github.com/pingdotgg/t3code/pull/4117
* @caezium made their first contribution in https://github.com/pingdotgg/t3code/pull/4161
* @Wraient made their first contribution in https://github.com/pingdotgg/t3code/pull/3949
* @thomaslittle made their first contribution in https://github.com/pingdotgg/t3code/pull/4472
* @0x4bs3nt made their first contribution in https://github.com/pingdotgg/t3code/pull/4475
* @saphid made their first contribution in https://github.com/pingdotgg/t3code/pull/4607
* @maxktz made their first contribution in https://github.com/pingdotgg/t3code/pull/4657
* @KrzysztofMoch made their first contribution in https://github.com/pingdotgg/t3code/pull/4675

**Full Changelog**: https://github.com/pingdotgg/t3code/compare/v0.0.28...v0.0.29

## What's Changed
* Add middle-click close for right panel tabs by @huxcrux in https://github.com/pingdotgg/t3code/pull/3161
* fix: warm WSL before preflight in WSL-only backend mode by @UtkarshUsername in https://github.com/pingdotgg/t3code/pull/3588
* Add Claude Sonnet 5 as the default Claude model by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3620
* Restore the ultrathink frame border effect by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3625
* fix(dev): Fix electron dev launch and add test by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3662
* Add adaptive split-view layout for iPad/mobile workspace by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3514
* fix(mobile): compile patched native pods from source on EAS by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3667
* Make the thread composer read as elevated liquid glass by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3668
* Upgrade Vite Plus and enable bundled dev opt-in by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3679
* Surface pending tasks in mobile home and draft flow by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3670
* fix(mobile): combined test branch — scroll, back-swipe, thread lists, computer switching by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3687
* Add repo-root favicon.svg so t3 code shows its own icon by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3683
* Load thread snapshots over HTTP before live sync by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3719
* Fix mobile legend anchor under automatic iOS insets by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3684
* Improve live activity routing and diagnostics by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3685
* Prevent Add Project sheet from collapsing on relayout by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3759
* Use variant-specific splash icons in mobile app by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3762
* Fix Expo widget asset wiring order by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3763
* Extend Done display to 15 minutes and show up to 5 Live Activity banner rows by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3761
* Clear VCS presentation state on finish by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3764
* Lead with the outcome when no agents are active in the Live Activity by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3768
* Add T3 Connect onboarding for mobile and web by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3765
* Revert "Add T3 Connect onboarding for mobile and web" by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3776
* Expose Clerk Google sign-in env vars to Expo by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3772
* Set up Cursor Cloud dev environment (web + Android toolchain) by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3755
* Revert "Revert "Add T3 Connect onboarding for mobile and web"" by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3777
* Use rounded depth logo for production splash screen by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3780
* fix(release): stage pnpm 11 allowBuilds for desktop installs by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3781
* Upgrade Clerk toolchain to latest versions by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3785
* fix(release): bump electron-builder so pnpm 11 deduped deps land in the asar by @avocardow in https://github.com/pingdotgg/t3code/pull/3790
* Fix desktop native optional dependency packaging by @Prgm-code in https://github.com/pingdotgg/t3code/pull/3816
* [codex] Upgrade Clerk stack by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3821
* [codex] Preserve worktree metadata during branch sync by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3822
* feat(client): persist offline environment data and mobile preferences by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3795
* [codex] Label max and ultra reasoning by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3824
* fix(mobile): embed fonts and render project favicons reliably by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3823
* Show compact PR number badges in mobile thread rows by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3827
* Expose mobile PR indicator labels to accessibility by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3828
* Fix truncated chat error alert layout by @jakeleventhal in https://github.com/pingdotgg/t3code/pull/3899
* fix(marketing): show platform-appropriate commit shortcut on the website by @VedankPurohit in https://github.com/pingdotgg/t3code/pull/3644
* [codex] Add Android mobile support by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3579
* Use client-side fallbacks for missing project favicons by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3959
* Skip stale working-task notifications by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3961
* Prepare Android beta branding and review diff UI by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3967
* perf(web): duty-cycle status animations and remove fixed noise overlay by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3978
* fix(docs): correct CI task-runner commands in ci.md by @kridaydave in https://github.com/pingdotgg/t3code/pull/3990
* fix(docs): repair broken source links in architecture overview by @kridaydave in https://github.com/pingdotgg/t3code/pull/3991
* fix(docs): replace stale codething-mvp absolute paths with repo-relative links by @kridaydave in https://github.com/pingdotgg/t3code/pull/3992
* docs: Add T3 Code Legal Docs by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3972
* Fix Legal modal header crash by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4000
* [codex] Fix onboarding connection status by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4001
* Isolate native diff highlight grammar state by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4029
* Fix macOS fullscreen titlebar spacing by @D3OXY in https://github.com/pingdotgg/t3code/pull/4019
* Prevent duplicate project workspace roots by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/3829
* Normalize over-indented markdown list items by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4020
* Resolve localhost preview URLs for remote environments by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4011
* fix(mobile): Send composer images in upload wire format by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4035
* Fix iOS terminal Enter input encoding by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4043
* Add native mobile share target support by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4021
* [codex] Expand real-route app store screenshot harness by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4014
* fix(server): use CLAUDE_CONFIG_DIR instead of HOME for Claude instanc… by @dmstoykov in https://github.com/pingdotgg/t3code/pull/4017
* Fix dropped events during initial thread snapshot by @D3OXY in https://github.com/pingdotgg/t3code/pull/4079
* feat: show nightly update changelog tooltip by @HugoVizcainoSantana in https://github.com/pingdotgg/t3code/pull/3832
* fix(git): treat selected commit paths literally by @EricTsai83 in https://github.com/pingdotgg/t3code/pull/3998
* fix(server): stabilize non-repository Git diagnostics by @EricTsai83 in https://github.com/pingdotgg/t3code/pull/4077
* Refresh app icons across release variants by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4080
* Update marketing GitHub star count by @AmoonPod in https://github.com/pingdotgg/t3code/pull/4088
* fix(marketing): correct Cursor icon color by @AmoonPod in https://github.com/pingdotgg/t3code/pull/4090
* Normalize protocol-relative remote host input as https by @kridaydave in https://github.com/pingdotgg/t3code/pull/3971
* fix(cursor): default binary path to cursor-agent (avoid path conflict w/ grok) by @BunnyGamezsc in https://github.com/pingdotgg/t3code/pull/4094
* Fix documented task-runner commands (bun run -> vp) by @kridaydave in https://github.com/pingdotgg/t3code/pull/3965
* Allow preview panel to grow on wide displays by @olivoil in https://github.com/pingdotgg/t3code/pull/4044
* fix: prevent initial right-click from selecting a context menu item by @Fazalkadivar21 in https://github.com/pingdotgg/t3code/pull/3877
* Fix duplicate keybinding rule when replacing with an existing rule by @kridaydave in https://github.com/pingdotgg/t3code/pull/3969
* fix(server): image upload crashed dispatchCommand with a stack overflow by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3952
* Remove unused code parameter from describePreviewError by @kridaydave in https://github.com/pingdotgg/t3code/pull/3970
* [codex] prevent ACP assistant ID collisions after restarts by @maxwellyoung in https://github.com/pingdotgg/t3code/pull/3932
* fix(web): inset Windows desktop scrollbars from resize edge by @nateEc in https://github.com/pingdotgg/t3code/pull/4097
* [codex] fix mobile composer Enter behavior by @maxwellyoung in https://github.com/pingdotgg/t3code/pull/3930
* feat(server): include runtime model and effort in Codex developer instructions by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3948
* fix(ux): spamming cmd + , no longer stack opening settings by @jamesx0416 in https://github.com/pingdotgg/t3code/pull/2757
* fix(terminal): strip AppImage runtime env from spawned terminals by @leorivastech in https://github.com/pingdotgg/t3code/pull/3108
* fix(server): thread cwd through Claude capability probe (#2048) by @mvanhorn in https://github.com/pingdotgg/t3code/pull/2124
* [codex] fix: guard invalid web timestamps by @StiensWout in https://github.com/pingdotgg/t3code/pull/3515
* [codex] fix: tolerate invalid latest user message timestamps by @StiensWout in https://github.com/pingdotgg/t3code/pull/3521
* [codex] Fix provider update checks restore defaults by @StiensWout in https://github.com/pingdotgg/t3code/pull/3531
* fix(server): skip undecodable provider runtime rows when listing sessions by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3951
* Share MCP OAuth locks across Codex shadow homes by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4104
* Preserve T3 Code identity in macOS development launcher by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4102
* fix(web): increase contrast of question option descriptions by @xxashxx-svg in https://github.com/pingdotgg/t3code/pull/3867
* feat: draft hero landing on the index route by @yordis in https://github.com/pingdotgg/t3code/pull/4055
* feat: file explorer mention actions and zoom-aware context menus by @yordis in https://github.com/pingdotgg/t3code/pull/4054
* fix(mobile): restore iOS home screen branding by @PixPMusic in https://github.com/pingdotgg/t3code/pull/4025
* perf(client): defer active thread cache writes by @Chrrxs in https://github.com/pingdotgg/t3code/pull/4006
* Default diffs to working changes by @jakeleventhal in https://github.com/pingdotgg/t3code/pull/3974
* Add Grok to marketing site provider list by @Aditya190803 in https://github.com/pingdotgg/t3code/pull/3484
* Fix reopening existing Diff tab by @jakeleventhal in https://github.com/pingdotgg/t3code/pull/3973
* Fix sending messages during active turns by @jakeleventhal in https://github.com/pingdotgg/t3code/pull/3919
* [codex] Route OpenCode missing-session errors through Effect by @StiensWout in https://github.com/pingdotgg/t3code/pull/3608
* [fix/feat:ui] Show default option badge by @sandersonstabo in https://github.com/pingdotgg/t3code/pull/3232
* [fix/feat:ui] Preserve open-in editor brand colors by @sandersonstabo in https://github.com/pingdotgg/t3code/pull/3225
* fix(web): handle macOS Home and End in composer by @GuilhermeVieiraDev in https://github.com/pingdotgg/t3code/pull/2508
* Allow failed remote environments to be removed by @zepi2509 in https://github.com/pingdotgg/t3code/pull/4084
* [codex] canonicalize client timestamps by @maxwellyoung in https://github.com/pingdotgg/t3code/pull/4112
* [fix/feat:ui] Make selected menu checks blue by @sandersonstabo in https://github.com/pingdotgg/t3code/pull/3234
* fix(desktop): Validate WSL node version against engine range after probe success by @UtkarshUsername in https://github.com/pingdotgg/t3code/pull/3621
* Refresh splash screen and favicon branding by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4120
* Add terminal selection copy action by @tarik02 in https://github.com/pingdotgg/t3code/pull/2904
* Add isolated app testing workflow by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4121
* feat(web): themed sidebar header art for nightly and dev builds by @maria-rcks in https://github.com/pingdotgg/t3code/pull/4130
* feat: add headless `t3 connect` setup for SSH hosts by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3749
* Refine T3 Connect authorization surfaces by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4159
* fix: increase OpenCode server startup timeout from 5s to 30s by @UtkarshUsername in https://github.com/pingdotgg/t3code/pull/4132
* fix(shared): delete unused agentAwareness phase predicates by @kridaydave in https://github.com/pingdotgg/t3code/pull/4134
* fix(mobile): Stabilize native stack option updates by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4037
* Make test-t3-app skill discoverable by Claude Code by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4162
* fix(web): improve dev sidebar backdrop contrast & remove version pills by @maria-rcks in https://github.com/pingdotgg/t3code/pull/4166
* Fix draft banner stack overlap by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4164
* Add portable mobile app testing guidance by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4165
* fix(client): use lightweight connection probe by @eeinarsson in https://github.com/pingdotgg/t3code/pull/4137
* fix(server): resolve Claude SDK executable path on Windows npm installs by @nsxdavid in https://github.com/pingdotgg/t3code/pull/3740
* Fix project action preview settings persistence by @keeperxy in https://github.com/pingdotgg/t3code/pull/3842
* fix(desktop): allow clipboard writes in the preview browser by @carlosricojr in https://github.com/pingdotgg/t3code/pull/3889
* fix(web): handle sidebar shortcut before editors by @Bortlesboat in https://github.com/pingdotgg/t3code/pull/3921
* fix(server): recognize Bedrock-backed Claude as authenticated by @PieterVanZyl-Dev in https://github.com/pingdotgg/t3code/pull/3931
* Fix incorrect pluralization of “entry” by @McMelonTV in https://github.com/pingdotgg/t3code/pull/3933
* feat(server): title background-task work-log rows with the task name by @t3dotgg in https://github.com/pingdotgg/t3code/pull/3751
* fix: delegate OpenCode session titles to provider by @tris203 in https://github.com/pingdotgg/t3code/pull/3720
* Archive selected threads from the context menu by @theduke in https://github.com/pingdotgg/t3code/pull/3895
* fix(cli): support force removing projects by @Bortlesboat in https://github.com/pingdotgg/t3code/pull/3922
* fix: allow sidebar to be shrunk when wider than viewport by @shoaib050326 in https://github.com/pingdotgg/t3code/pull/2456
* fix(codex): show web search query and url in tool call details by @GuilhermeVieiraDev in https://github.com/pingdotgg/t3code/pull/2093
* Add Codex launch arguments setting by @jamesx0416 in https://github.com/pingdotgg/t3code/pull/2892
* [orchestration] Clear stale active turn when session becomes inactive by @Andrew-Forster in https://github.com/pingdotgg/t3code/pull/3159
* Regenerate Codex reset credit protocol bindings by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4173
* fix(preview): preserve direct localhost navigation by @Chrrxs in https://github.com/pingdotgg/t3code/pull/3939
* Synchronize mobile threads with authoritative shell snapshots by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4163
* Gate iOS glass layout on native support by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4032
* fix(opencode): resume the OpenCode session on follow-ups instead of starting an empty one by @vdmkotai in https://github.com/pingdotgg/t3code/pull/3617
* fix(server): use CLI for OpenCode health check instead of spawning server by @UtkarshUsername in https://github.com/pingdotgg/t3code/pull/4153
* fix(web): scope timeline minimap hover target to the side gutter by @xxashxx-svg in https://github.com/pingdotgg/t3code/pull/3869
* [codex] show complete approval details by @maxwellyoung in https://github.com/pingdotgg/t3code/pull/4111
* fix(web): paint text selection over composer chips by @yordis in https://github.com/pingdotgg/t3code/pull/4139
* [codex] preserve custom model slugs by @maxwellyoung in https://github.com/pingdotgg/t3code/pull/4168
* fix(web): preview workspace images in the file panel by @Rhiz3K in https://github.com/pingdotgg/t3code/pull/3996
* feat(web): drag files from the explorer into the chat composer by @yordis in https://github.com/pingdotgg/t3code/pull/4140
* fix(desktop): preserve main window bounds by @anirudhsama in https://github.com/pingdotgg/t3code/pull/3851
* perf(orchestration): speed up new-chat propagation and offline catch-up by @RusiruSadathana in https://github.com/pingdotgg/t3code/pull/4177
* Finale: upgrade changed files card to fix various UI issues by @sandersonstabo in https://github.com/pingdotgg/t3code/pull/4113
* Pass CLI OAuth config to hosted web deploy by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4186
* fix(web): always show environment chip for remote projects by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4217
* fix(web): keep composer editable while disconnected by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4241
* fix: better defaults — Claude 1M context, Codex gpt-5.6, worktrees from origin main by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4240
* fix(claude): handle all SDK stream messages; stop spurious work-log warning rows by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4244
* Sidebar v2 beta: flat thread list with a server-backed settled lifecycle by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4026
* fix(settings): validate the add-provider wizard step before advancing (#2813) by @leorivastech in https://github.com/pingdotgg/t3code/pull/3100
* fix(claude): isolate capability probe from user MCP servers by @jbbottoms in https://github.com/pingdotgg/t3code/pull/4015
* Preserve connecting status while a turn starts by @D3OXY in https://github.com/pingdotgg/t3code/pull/4101
* fix(server): stop restoring stale OpenCode models by @nateEc in https://github.com/pingdotgg/t3code/pull/4095
* [codex] keep scoped package references as text by @maxwellyoung in https://github.com/pingdotgg/t3code/pull/4167
* fix(web): default provider selection for users without Codex by @mfazekas in https://github.com/pingdotgg/t3code/pull/4117
* Unify temporary worktree branch naming by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4278
* fix(web): use message-square icon for settled icon-less project threads in sidebar v2 by @UtkarshUsername in https://github.com/pingdotgg/t3code/pull/4279
* Stabilize sidebar settling animations by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4280
* Restore Copy Link in chat link context menu by @caezium in https://github.com/pingdotgg/t3code/pull/4161
* fix(desktop): handle EPIPE errors on stdout/stderr to prevent crash dialog by @UtkarshUsername in https://github.com/pingdotgg/t3code/pull/4213
* Preserve draft thread highlighting during promotion by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4283
* Move mobile working timer into the thread timeline by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4285
* Stabilize PR status lookups and provider session lifecycle by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4281
* fix: open command palette instead of custom dialog for new thread picker in SidebarV2 by @UtkarshUsername in https://github.com/pingdotgg/t3code/pull/4269
* fix(server): don't drop sticky PR fallback when remote URL can't be resolved by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4289
* feat(web): copy branch name via right-click in the branch selector by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4275
* Add remote server updates and standalone service management by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4286
* Refine light-mode sidebar surfaces by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4268
* fix(mobile): don't mark Android VPN/Tailscale as offline when connected by @Wraient in https://github.com/pingdotgg/t3code/pull/3949
* improve and prevent silent thread branch drift and PR fetching by @justsomelegs in https://github.com/pingdotgg/t3code/pull/2284
* Refresh web application surfaces and dark-mode dialogs by @maria-rcks in https://github.com/pingdotgg/t3code/pull/4319
* fix(web): new-thread defaults ignored for remote environments by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4276
* feat: add "Auto" runtime mode — AI-reviewed approvals for Codex and Claude by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4272
* Add shared t3.json project configuration support by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4317
* Unify dialog glass and fix composer overlays by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4365
* fix(web): warn before silent Windows updates by @nateEc in https://github.com/pingdotgg/t3code/pull/4350
* [codex] Move project grouping to General settings by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4313
* [codex] Group project scopes in mobile thread lists by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4314
* [codex] Move mobile project grouping to General settings by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4315
* [codex] Deduplicate connection failure messaging by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4367
* Restore grouped project filtering in Sidebar V2 by @shivamhwp in https://github.com/pingdotgg/t3code/pull/4282
* [codex] restore Sidebar V2 project actions by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4373
* [codex] Group projects in new-thread pickers by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4312
* fix(web): restore dark composer toolbar styling by @maria-rcks in https://github.com/pingdotgg/t3code/pull/4375
* Fix thread tooltip folder icon color by @maria-rcks in https://github.com/pingdotgg/t3code/pull/4383
* fix(server): parse CLI version in update preflight by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4389
* fix(web): sidebar v2 polish — jump hints, working duration, in-flight fade, settled sort by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4274
* Fix logical project grouping labels on mobile by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4391
* Add preview color scheme controls and simplify project grouping by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4385
* fix(cli): publish nightly branded favicons by @maria-rcks in https://github.com/pingdotgg/t3code/pull/4372
* Fix thread loading flash by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4396
* fix(client-runtime): keep a warm thread un-settled despite a merged/closed PR by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4309
* Fix composer context strip alignment and glass shell by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4404
* Polish iOS git progress overlay with glass effects by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4387
* Improve composer glass fallbacks by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4406
* feat(web): collapse large git diffs by default to make chat more readable by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4409
* Stop new threads inheriting checkout/branch from viewed thread by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4411
* fix: tone down branch-mismatch banner by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4416
* fix: Claude Code skills discoverable for the composer $ picker by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4414
* fix(web): keep settled threads reachable when opened directly by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4413
* feat(sidebar-v2): thread snoozing by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4311
* Upgrade Clerk packages and Expo integration by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4440
* Increase light-mode contrast for user message bubbles by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4441
* Restore model picker layout and retain iterative test state by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4450
* Color settled PR labels on hover by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4451
* [codex] Fix glass hover compositing artifacts by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4446
* Add Claude Opus 5 model by @thomaslittle in https://github.com/pingdotgg/t3code/pull/4472
* feat(web): add collapse-all toggle to diff panel by @0x4bs3nt in https://github.com/pingdotgg/t3code/pull/4475
* feat(web): show fast mode as a bolt instead of a "Normal" label by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4488
* feat(dev): keep worktree dev state isolated on T3 Code dev servers by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4555
* feat(dev): Make t3 code dev instances shareable over Tailscale by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4556
* fix(dev): skip browser-blocked ports by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4608
* fix: cut websocket throughput in half by pruning activity payloads by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4622
* perf(mobile): defer work-log detail serialization by @saphid in https://github.com/pingdotgg/t3code/pull/4607
* test: account for lazy thread feed details by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4628
* feat(relay): limit managed tunnels per user by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4530
* Add managed tunnel limits migration by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4635
* Add background preview capture and picture-in-picture support by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4397
* feat(web): prompt stash — cmd+S saves the composer to a per-provider queue by @t3dotgg in https://github.com/pingdotgg/t3code/pull/4453
* [codex] Upgrade Effect and Alchemy betas by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4643
* feat: allow new thread creation through project breadcrumbs by @0x4bs3nt in https://github.com/pingdotgg/t3code/pull/4638
* fix(web): scope PR state to the thread branch by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4460
* Drop redundant Relay user indexes by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4648
* feat(connect): release the Cloudflare tunnel when the environment shuts down by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4531
* Fix Relay Worker RuntimeContext wiring by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4653
* Fix live sidebar resize limits and defer Alchemy runtime context by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4655
* fix(web): constrain branch toolbar context by @maxktz in https://github.com/pingdotgg/t3code/pull/4657
* Keep MCP credentials alive across provider turns by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4659
* fix: close actions dropdown when editing by @0x4bs3nt in https://github.com/pingdotgg/t3code/pull/4660
* fix(preview): stabilize PiP viewport identity by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4661
* Add glass styling for thread tooltips and simplify preview tab handling by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4665
* Use tarball archiving for hosted web deploys by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4669
* fix(server): bound editor discovery during config loading by @maria-rcks in https://github.com/pingdotgg/t3code/pull/4291
* Prevent draft thread detail polling before shell registration by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4670
* feat: add configurable source control writing settings by @maria-rcks in https://github.com/pingdotgg/t3code/pull/4204
* feat(diff-panel): show total line additions and deletions by @0x4bs3nt in https://github.com/pingdotgg/t3code/pull/4674
* Clear provider update actions while updating by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4676
* Fix sidebar highlighting for draft threads by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4679
* Use glass surfaces for web toasts by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4681
* Show origin ref in branch trigger label by @juliusmarminge in https://github.com/pingdotgg/t3code/pull/4680
* fix(mobile): match react version to react-native 0.85.3 vendored renderer (19.2.3) by @KrzysztofMoch in https://github.com/pingdotgg/t3code/pull/4675

## New Contributors
* @avocardow made their first contribution in https://github.com/pingdotgg/t3code/pull/3790
* @Prgm-code made their first contribution in https://github.com/pingdotgg/t3code/pull/3816
* @jakeleventhal made their first contribution in https://github.com/pingdotgg/t3code/pull/3899
* @VedankPurohit made their first contribution in https://github.com/pingdotgg/t3code/pull/3644
* @kridaydave made their first contribution in https://github.com/pingdotgg/t3code/pull/3990
* @dmstoykov made their first contribution in https://github.com/pingdotgg/t3code/pull/4017
* @HugoVizcainoSantana made their first contribution in https://github.com/pingdotgg/t3code/pull/3832
* @EricTsai83 made their first contribution in https://github.com/pingdotgg/t3code/pull/3998
* @AmoonPod made their first contribution in https://github.com/pingdotgg/t3code/pull/4088
* @BunnyGamezsc made their first contribution in https://github.com/pingdotgg/t3code/pull/4094
* @olivoil made their first contribution in https://github.com/pingdotgg/t3code/pull/4044
* @Fazalkadivar21 made their first contribution in https://github.com/pingdotgg/t3code/pull/3877
* @maxwellyoung made their first contribution in https://github.com/pingdotgg/t3code/pull/3932
* @nateEc made their first contribution in https://github.com/pingdotgg/t3code/pull/4097
* @leorivastech made their first contribution in https://github.com/pingdotgg/t3code/pull/3108
* @xxashxx-svg made their first contribution in https://github.com/pingdotgg/t3code/pull/3867
* @yordis made their first contribution in https://github.com/pingdotgg/t3code/pull/4055
* @Chrrxs made their first contribution in https://github.com/pingdotgg/t3code/pull/4006
* @Aditya190803 made their first contribution in https://github.com/pingdotgg/t3code/pull/3484
* @zepi2509 made their first contribution …
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🚀 Mobile Continuous Deployment Trigger Expo preview build size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant