<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Roronoa</title>
    <description>The latest articles on DEV Community by Roronoa (@roronoa_).</description>
    <link>https://dev.to/roronoa_</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4022649%2Fac571ee6-4982-49a9-8a57-3eb9fb326cc2.jpg</url>
      <title>DEV Community: Roronoa</title>
      <link>https://dev.to/roronoa_</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/roronoa_"/>
    <language>en</language>
    <item>
      <title>Keep Agent Transcripts Out of the App Switcher on Your First PR</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Wed, 09 Sep 2026 15:26:00 +0000</pubDate>
      <link>https://dev.to/roronoa_/keep-agent-transcripts-out-of-the-app-switcher-on-your-first-pr-4bk0</link>
      <guid>https://dev.to/roronoa_/keep-agent-transcripts-out-of-the-app-switcher-on-your-first-pr-4bk0</guid>
      <description>&lt;p&gt;You sit down with a Pixel 8 on Android 15, a debug Flutter build in the foreground, and a first-hour onboarding ticket. The ticket says you should send one chat turn, then read the README while the app sits in recents. You type a dummy question about a visa interview, press Home, and the recents card still shows the last bubble. That Home press is the lifecycle transition you should treat as a data store, not as a pause.&lt;/p&gt;

&lt;p&gt;Juniors usually keep the activity resumed while they click through the happy path on a loaner phone. Seniors often review the same diff on a laptop and never background the process at all. App switcher thumbnails, iOS snapshot images, and Android recents tasks are operating-system mechanisms you did not opt into. An agent transcript is worse than a settings screen because tool results can include names, amounts, and health details.&lt;/p&gt;

&lt;p&gt;This article is a proposed first-hour workflow, not a claimed lab run with invented timings. You should record the device, OS, framework, network, and permission state before you open a pull request. You should also plan the first rollback if a secure-flag change breaks QA screenshots.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the OS actually keeps after you press Home
&lt;/h2&gt;

&lt;p&gt;When you leave the chat, the system may freeze a bitmap for the recents tray. On Android that bitmap is the task thumbnail. On iOS it is the snapshot taken around &lt;code&gt;willResignActive&lt;/code&gt;. Neither surface is your product database, but both can display the last assistant bubble to anyone who borrows the phone.&lt;/p&gt;

&lt;p&gt;You should distinguish three stores that juniors often mix together during onboarding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In-memory chat state&lt;/strong&gt; that should survive a short backgrounding and restore cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durable logs or caches&lt;/strong&gt; that must not contain raw tool payloads after a rollback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OS snapshots&lt;/strong&gt; that you do not control unless you blank them on purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Source-based platform docs describe &lt;code&gt;FLAG_SECURE&lt;/code&gt; and snapshot overlays. Hands-on testing is the only way to see whether your Flutter, React Native, or native shell actually applied them. Do not treat a simulator screenshot as proof that the recents card is blank.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proposed first-hour experiment
&lt;/h2&gt;

&lt;p&gt;Label this as a proposed test on hardware you control. Do not copy these steps into a launch checklist until you have watched the switcher yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment to record
&lt;/h3&gt;

&lt;p&gt;Write these fields in the PR description before you ask for review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Device and OS, for example Pixel 8 / Android 15 or iPhone 14 / iOS 18.&lt;/li&gt;
&lt;li&gt;Framework and versions, for example Flutter 3.24 plus the exact plugin that sets window flags.&lt;/li&gt;
&lt;li&gt;Network state: Wi-Fi to a stub endpoint, or airplane mode if the client already caches the last turn.&lt;/li&gt;
&lt;li&gt;Permission state: microphone and photos denied unless this flow truly needs them.&lt;/li&gt;
&lt;li&gt;Power condition: unplugged, battery saver off, so the OS is not already hiding surfaces.&lt;/li&gt;
&lt;li&gt;Application state: one completed agent turn in the foreground, then Home, then recents.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Exact steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Install a debug build that still talks to a stub model, not to production keys on a shared device.&lt;/li&gt;
&lt;li&gt;Send one fixture prompt that contains a fake passport number and a fake medical leave date.&lt;/li&gt;
&lt;li&gt;Wait until the last bubble is fully painted, then press Home within two seconds.&lt;/li&gt;
&lt;li&gt;Open the app switcher and look at the card with your eyes, not through &lt;code&gt;adb screencap&lt;/code&gt; alone.&lt;/li&gt;
&lt;li&gt;Return to the app and confirm the conversation is still there, because blanking the snapshot must not wipe state.&lt;/li&gt;
&lt;li&gt;Force-stop the app, relaunch, and check whether the same fixture text reappears in recents before first paint.&lt;/li&gt;
&lt;li&gt;Capture whether the session recovered, restarted empty, or silently disappeared from disk.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Expected observations
&lt;/h3&gt;

&lt;p&gt;With a secure flag or overlay in place, the recents card should be blank, letterboxed, or covered by a branded shield. The in-app transcript should still restore after you foreground the activity. Without the flag, the last bubble remains readable on the card, which is a ship blocker for any agent that handles HR, health, or finance text.&lt;/p&gt;

&lt;p&gt;If the overlay stays up after resume, you have a different bug: you hid the product from the user, not from the operating system. That failure belongs in the first rollback plan, not in a follow-up sprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Artifact: a decision table you can paste into the PR
&lt;/h2&gt;

&lt;p&gt;Use this table as the original artifact for the first pull request. Fill the right-hand columns on the device, and do not invent milliseconds.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lifecycle condition&lt;/th&gt;
&lt;th&gt;Secure surface on?&lt;/th&gt;
&lt;th&gt;Recents / snapshot&lt;/th&gt;
&lt;th&gt;In-app recovery after return&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Foreground chat after one turn&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;live UI, not a card&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home, then app switcher&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;last bubbles readable&lt;/td&gt;
&lt;td&gt;chat still in memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home, then app switcher&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;blank or shield overlay&lt;/td&gt;
&lt;td&gt;chat still in memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recents after force-stop&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no leftover fixture text&lt;/td&gt;
&lt;td&gt;session restarts or restores from encrypted store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debug flavor for QA&lt;/td&gt;
&lt;td&gt;optional&lt;/td&gt;
&lt;td&gt;screenshots allowed&lt;/td&gt;
&lt;td&gt;same chat code path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release flavor&lt;/td&gt;
&lt;td&gt;required for agent chat&lt;/td&gt;
&lt;td&gt;screenshots blocked&lt;/td&gt;
&lt;td&gt;same chat code path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ask the reviewer to reject the PR if the “yes” row still shows readable tool output. Readable output means the OS mechanism still holds sensitive data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code you can apply in the first hour
&lt;/h2&gt;

&lt;p&gt;Keep the flag next to the chat route, not on the whole application, so login and help screens remain capturable for support. The snippets below are starting points you still have to verify on device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Android (Kotlin), proposed for the agent Activity only:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentChatActivity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppCompatActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bundle&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFlags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;WindowManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LayoutParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FLAG_SECURE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;WindowManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LayoutParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FLAG_SECURE&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;setContentView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activity_agent_chat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Confirm the window flag from a shell, proposed check:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell dumpsys window | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"mSecure|FLAG_SECURE"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That grep does not prove the recents bitmap is blank. You still owe the visual switcher check from the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iOS (Swift), proposed overlay around resign-active:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;SnapshotShield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIView&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="nv"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIWindow&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;NotificationCenter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;forName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;willResignActiveNotification&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;object&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;window&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UIView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;backgroundColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemBackground&lt;/span&gt;
            &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addSubview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cover&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;NotificationCenter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;forName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;didBecomeActiveNotification&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;object&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeFromSuperview&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cover&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Flutter, proposed only on the chat route:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_windowmanager/flutter_windowmanager.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;protectAgentRoute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FlutterWindowManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addFlags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;FlutterWindowManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FLAG_SECURE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;unprotectOtherRoutes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FlutterWindowManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clearFlags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;FlutterWindowManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FLAG_SECURE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clear the flag when you pop the chat route. Otherwise QA cannot capture a bug on a non-sensitive screen, and they will ask you to revert the whole change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;React Native, proposed native call from the chat screen:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NativeModules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Platform&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setAgentChatSecure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;android&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;NativeModules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SecureWindow&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;setSecure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The native module should toggle &lt;code&gt;FLAG_SECURE&lt;/code&gt; and nothing else. Do not hide the webview debugger behind the same switch on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  First PR, then the first rollback
&lt;/h2&gt;

&lt;p&gt;Your first PR should contain the route-scoped flag, the decision table with device fields filled, and a debug flavor that leaves screenshots on. It should not contain a global secure flag, a production API key, or a claim that “recents is fine on my laptop.”&lt;/p&gt;

&lt;p&gt;Your first rollback should be equally boring. If QA cannot reproduce a layout bug because the flavor still sets &lt;code&gt;FLAG_SECURE&lt;/code&gt;, revert only the debug flavor, not the release shield. If the iOS overlay fails to remove on &lt;code&gt;didBecomeActive&lt;/code&gt;, revert the overlay and keep the Android flag, then file a follow-up with the exact OS version.&lt;/p&gt;

&lt;p&gt;A useful rollback command for the junior who already pushed is a revert commit, not a force-push on a shared onboarding branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git revert HEAD &lt;span class="nt"&gt;--no-edit&lt;/span&gt;
git push origin HEAD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the revert, rerun steps 3 through 6 on the same device. Write down whether the recents card returned to showing the last bubble. That sentence is the recovery outcome reviewers need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a free stub belongs in this hour
&lt;/h2&gt;

&lt;p&gt;You still need fixture transcripts that look like PII without putting production secrets on a shared phone. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option you can point the debug flavor at, so the loaner device never holds a customer key while you watch the app switcher.&lt;/p&gt;

&lt;p&gt;Keep that stub on the debug product flavor only. The release flavor should keep talking to whatever endpoint the repo already documents. The stub exists so you can generate a visa-shaped sentence and a medical-leave-shaped sentence, then prove the snapshot does not keep them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, and who should skip this
&lt;/h2&gt;

&lt;p&gt;This workflow does not encrypt the chat database, does not stop a determined screen recording while the app is foregrounded, and does not replace a threat model. &lt;code&gt;FLAG_SECURE&lt;/code&gt; can also block legitimate assistive screenshots and some OEM screen-sharing tools. If your product is a public demo with no personal data, a shield overlay is noise.&lt;/p&gt;

&lt;p&gt;Skip this approach when you do not own the native shell, when the chat is only on desktop, or when legal already forbids storing the transcript on device at all. In those cases you need a different first-hour ticket: do not render the raw tool payload in the first place.&lt;/p&gt;

&lt;p&gt;Do not treat this article as a benchmark. There are no milliwatt numbers here, and there is no claim that every Android skin honors &lt;code&gt;FLAG_SECURE&lt;/code&gt; the same way. Some launchers recapture the task after you return, which is why the force-stop row in the table matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to send back if you run it
&lt;/h2&gt;

&lt;p&gt;If you run the same Home-press on your hardware, reply with the device, OS, framework versions, and the three-step transition you used. Say whether the recents card recovered blank, restarted into an empty chat, or still showed the last tool result. Comparable environment evidence is more useful than another screenshot from a simulator that never builds a recents bitmap.&lt;/p&gt;

&lt;p&gt;When the first-hour ticket is closed, the next junior should be able to rerun the table without asking where the stub lives. If you want that stub off the shared phone, the free model access and free server option mentioned above is enough to host dummy completions for the debug flavor.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>mobile</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Benchmark a Mobile Agent Across Airplane Mode and Your First Config Rollback</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Mon, 07 Sep 2026 12:24:53 +0000</pubDate>
      <link>https://dev.to/roronoa_/benchmark-a-mobile-agent-across-airplane-mode-and-your-first-config-rollback-307b</link>
      <guid>https://dev.to/roronoa_/benchmark-a-mobile-agent-across-airplane-mode-and-your-first-config-rollback-307b</guid>
      <description>&lt;p&gt;You sit down with a Pixel 8a on Android 15 and a signed staging build already on the home screen. A half-finished agent turn still paints the chat, with a tool-call spinner frozen after two seconds. You enable Airplane Mode before the HTTP response arrives, then lock the device for thirty seconds. That lifecycle jump is the first thing you should reproduce, because generated agent code usually assumes the radio never drops.&lt;/p&gt;

&lt;p&gt;This is a proposed test, not a measured result from a private lab that you should copy blindly. You should run the steps on your own phone, then write down whether the turn recovered, restarted, or silently disappeared. Juniors joining a mobile AI repo in 2026 will meet agent scaffolding on day one, and that scaffolding is cheap to generate. The expensive part is proving the agent fails closed when cellular vanishes during an in-flight tool call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you prove in the first hour
&lt;/h2&gt;

&lt;p&gt;You are not proving that the model sounds clever during a hallway demo for stakeholders. You are proving three boring properties that reviewers can re-run without a staging VPN. Write them on the ticket before you touch a prompt file or a tool mapping.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The in-flight tool call has a &lt;code&gt;generationId&lt;/code&gt; and a &lt;code&gt;configHash&lt;/code&gt;, not just a spinner.&lt;/li&gt;
&lt;li&gt;Airplane Mode plus a lock screen cannot create a later surprise retry with a stale schema.&lt;/li&gt;
&lt;li&gt;A config rollback cannot resurrect queued work that was produced by the previous agent bundle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your repo already ships an on-device runtime, keep that path in the loop during hour one. The remote model is only an oracle for JSON shape, error codes, and fail-closed messaging. Do not treat the oracle as the shipping product for your first pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record the environment before you flip the radio
&lt;/h2&gt;

&lt;p&gt;Capture versions and radio state before Airplane Mode, or your notes will be useless by next week. Use this list as the header of the pull request so reviewers can repeat the jump. Skip any row and the experiment stops being comparable across phones.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Device and OS: Pixel 8a on Android 15, or your iPhone and the exact iOS build.&lt;/li&gt;
&lt;li&gt;Application state: signed in, chat visible, tool call in flight, screen on, then locked.&lt;/li&gt;
&lt;li&gt;Framework pins: React Native, NetInfo, HTTP client, and the on-device or edge runtime.&lt;/li&gt;
&lt;li&gt;Network and power: Wi-Fi versus LTE, Battery Saver off, no VPN, no private DNS surprises.&lt;/li&gt;
&lt;li&gt;Permissions: location or microphone only if that tool truly needs them; otherwise leave them denied.&lt;/li&gt;
&lt;li&gt;Expected observation: a user-visible paused state, no duplicate side effect, no missing transcript.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you cannot name the runtime that executes the tool, you are not ready to change the prompt. Spend the rest of hour one grepping for &lt;code&gt;AsyncStorage&lt;/code&gt;, &lt;code&gt;WorkManager&lt;/code&gt;, &lt;code&gt;URLSession&lt;/code&gt;, and any retry interceptor. Those four hits usually hide the silent duplicate that will show up after rollback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proposed experiment: one device, one transition
&lt;/h2&gt;

&lt;p&gt;Follow these steps on a physical device rather than a simulator with fake radios. Emulators lie about radio loss and about process death after you lock the screen. Keep the chat on screen so the lifecycle starts from an in-flight tool call, not from a cold start.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install the staging build and open a new chat with a disposable account.&lt;/li&gt;
&lt;li&gt;Send a prompt that forces a network tool, such as a store lookup, not a local rewrite.&lt;/li&gt;
&lt;li&gt;When the spinner appears, enable Airplane Mode within one second, then lock the phone.&lt;/li&gt;
&lt;li&gt;Wait thirty seconds without peeking. Unlock and screenshot the transcript, banner, and logs.&lt;/li&gt;
&lt;li&gt;Disable Airplane Mode, wait for the radio, and watch for a silent retry you did not request.&lt;/li&gt;
&lt;li&gt;Force-stop the app, relaunch, and check whether a queued tool call fires on its own.&lt;/li&gt;
&lt;li&gt;Record one outcome: recovered with the same &lt;code&gt;generationId&lt;/code&gt;, restarted as a new turn, or disappeared.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You should expect a visible paused or failed-closed state, not a second store lookup after the radio returns. A disappeared spinner with a later duplicate side effect is a failing result, even when the prose looks fine. Attach &lt;code&gt;logcat&lt;/code&gt; or Console.app lines that show the same &lt;code&gt;generationId&lt;/code&gt; across the lock, because screenshots alone will not survive review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state machine your first PR should ship
&lt;/h2&gt;

&lt;p&gt;Keep the first pull request small enough to roll back in one commit on the same afternoon. Model the turn as a state machine, then persist &lt;code&gt;(generationId, configHash, state)&lt;/code&gt; before any HTTP write. If you cannot persist that tuple, you cannot prove rollback safety later.&lt;/p&gt;

&lt;p&gt;Proposed states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;idle&lt;/code&gt; — no work, nothing queued&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tool_pending&lt;/code&gt; — request on the wire&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;queued_offline&lt;/code&gt; — only if the tool is idempotent and the user opted into retry&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cancelled&lt;/code&gt; — fail closed and show a pause chip&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rolled_back&lt;/code&gt; — config hash no longer matches, so you drop the queue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Default to &lt;code&gt;cancelled&lt;/code&gt; when Airplane Mode fires during &lt;code&gt;tool_pending&lt;/code&gt;. &lt;code&gt;queued_offline&lt;/code&gt; is an explicit product choice, not a courtesy of your HTTP client. Juniors get this backward because interceptors retry by default and generated glue copies that habit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proposed React Native sketch
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed example, not production code. Pin versions in the PR header.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;NetInfo&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-native-community/netinfo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-native-async-storage/async-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AgentState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tool_pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;queued_offline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cancelled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rolled_back&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TurnRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;generationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;configHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TURN_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent.turn.v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;persistTurn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TurnRecord&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TURN_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;failClosedIfStale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;currentHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TurnRecord&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TURN_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;turn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;TurnRecord&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;configHash&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;currentHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;rolled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TurnRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rolled_back&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;persistTurn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rolled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;rolled&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;watchRadio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onDrop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubNet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NetInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isConnected&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isInternetReachable&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;onDrop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubApp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;onDrop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;unsubNet&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;unsubApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire &lt;code&gt;onDrop&lt;/code&gt; so it moves &lt;code&gt;tool_pending&lt;/code&gt; to &lt;code&gt;cancelled&lt;/code&gt; and paints a pause chip the user can read. Do not call &lt;code&gt;fetch&lt;/code&gt; again from the NetInfo listener when the radio returns a few seconds later. That listener is how silent duplicates are born, and reviewers should reject any PR that retries from connectivity callbacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contract-test the tool JSON before production credentials exist
&lt;/h2&gt;

&lt;p&gt;Your first hour should also freeze the tool schema beside the client, not inside a chat screenshot. Save one golden request and one golden response, then assert names, required fields, and error shape. Leave prose quality out of this test, because wording will drift while the contract must not.&lt;/p&gt;

&lt;p&gt;A practical comparison oracle without production credentials is MonkeyCode's free model access on its free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Point the oracle at the same JSON schema your on-device or edge path must emit, and keep user transcripts off that box. Stand the oracle up for hour one, then delete the instance once the contract test stays green.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed Jest contract. Replace ORACLE_URL with your comparison endpoint.&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ToolCall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertToolShape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolCall&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lookupStore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cancelTurn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`unexpected tool &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lookupStore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lookupStore.query must be a string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;oracle tool call matches the mobile contract&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ORACLE_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;find a store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;schemaVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;store-lookup-v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;assertToolShape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the oracle and the phone disagree on tool names, fail the PR before anyone debates latency. Schema drift is cheaper to catch in Jest than in a store listing that double-books a reservation. Hash the golden files in CI so a prompt edit cannot land without a contract bump.&lt;/p&gt;

&lt;h2&gt;
  
  
  First rollback without resurrecting queued work
&lt;/h2&gt;

&lt;p&gt;Rollback is the third hour-one drill, and most teams skip it until a bad prompt ships. You revert the agent config or the prompt bundle, reinstall the previous binary, then launch into the same chat. The question is not whether the old copy builds. The question is whether yesterday's queue still fires.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Proposed checks after you reinstall the rolled-back build.&lt;/span&gt;
adb shell dumpsys package com.example.staging | &lt;span class="nb"&gt;grep &lt;/span&gt;versionName
adb exec-out run-as com.example.staging &lt;span class="nb"&gt;cat &lt;/span&gt;files/agent.turn.v1
adb logcat &lt;span class="nt"&gt;-d&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;generationId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On iOS, dump the same record from the app container and look for &lt;code&gt;configHash&lt;/code&gt; after the restore. If the hash from the newer config is still sitting in storage, the next foreground will try a tool the old binary does not understand. Your PR should treat hash mismatch as &lt;code&gt;rolled_back&lt;/code&gt; and show a one-line system message, not a reconstructed answer that pretends the turn finished.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision table for the review
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Tool in flight&lt;/th&gt;
&lt;th&gt;Stored hash&lt;/th&gt;
&lt;th&gt;Required next state&lt;/th&gt;
&lt;th&gt;User-visible result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Airplane Mode during &lt;code&gt;tool_pending&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;matches&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cancelled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pause chip, no retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Radio returns, user did not ask&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;matches&lt;/td&gt;
&lt;td&gt;&lt;code&gt;idle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no new tool call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config rollback, queue present&lt;/td&gt;
&lt;td&gt;maybe&lt;/td&gt;
&lt;td&gt;mismatch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rolled_back&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;drop queue, system line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotent tool, user opted in&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;matches&lt;/td&gt;
&lt;td&gt;&lt;code&gt;queued_offline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;explicit resume affordance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If a cell is missing from your ticket, the reviewer should bounce the PR without debating model quality. Generated agent glue loves the empty cell called just retry, and that cell is how a junior's first rollback creates a duplicate side effect. Fill the table in the description, then paste the screenshots that match each row you claim to handle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Native and Flutter notes for the same tuple
&lt;/h2&gt;

&lt;p&gt;The same tuple belongs in DataStore on Android, or in a small Room row if you already have one. On iOS, put &lt;code&gt;generationId&lt;/code&gt; and &lt;code&gt;configHash&lt;/code&gt; in the application support directory, not in a defaults file that backup may scoop up. Flutter teams can store the record with &lt;code&gt;shared_preferences&lt;/code&gt; only if you remember that plugin is not a queue and not a lock.&lt;/p&gt;

&lt;p&gt;WorkManager and BGTaskScheduler are for opted-in idempotent tools, not for resurrecting cancelled agent turns after Airplane Mode. If a native retry worker already exists, your first PR should make it read &lt;code&gt;state&lt;/code&gt; before it touches the network. Otherwise the cross-platform client will fail closed while the platform worker quietly completes the stale call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, and who should not use this
&lt;/h2&gt;

&lt;p&gt;This article is a proposed onboarding workflow, not a claim about any phone's battery curve or token throughput. The write-up does not attach lab numbers, and you should not copy anyone else's milliseconds into your review. NetInfo &lt;code&gt;isInternetReachable&lt;/code&gt; can lag behind the modem, and iOS will freeze listeners in ways Android will not.&lt;/p&gt;

&lt;p&gt;Do not send production traffic, paid user chats, precise location, audio, or account tokens to a comparison endpoint. Teams with a locked staging mesh already have an oracle, so do not add a second one for novelty. Voice interruption, Doze completions, and backup of prompt caches are separate drills, and this workflow does not replace them.&lt;/p&gt;

&lt;p&gt;Skip fail-closed cancellation if your product legally must complete a payment tool once the user confirms. In that case you need an idempotency key on the server of record, which is a different review with different failure language. Juniors should still persist &lt;code&gt;generationId&lt;/code&gt;, but the next state is &lt;code&gt;queued_offline&lt;/code&gt; with an explicit resume, never a hidden interceptor retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask your device, then tell us what happened
&lt;/h2&gt;

&lt;p&gt;Run the Airplane Mode lock on your own build this week and keep the notes next to the PR. Comment with the device, OS, framework pins, and the exact transition you used. Say whether the turn recovered, restarted, or silently disappeared after rollback, because that single word is the evidence the next junior needs.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>ai</category>
      <category>android</category>
      <category>reactnative</category>
    </item>
    <item>
      <title>Keep In-Flight Completions Out of Doze on Your First Morning</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Sun, 06 Sep 2026 20:37:25 +0000</pubDate>
      <link>https://dev.to/roronoa_/keep-in-flight-completions-out-of-doze-on-your-first-morning-2dac</link>
      <guid>https://dev.to/roronoa_/keep-in-flight-completions-out-of-doze-on-your-first-morning-2dac</guid>
      <description>&lt;p&gt;You unlock a staging Pixel on morning one and the chat bubble is still spinning after a twenty-minute commute. The phone is on Android 15, the React Native build is signed in, and the stream that looked healthy at the office never returns. That sleep-to-foreground handoff is your first-hour job, not a polish task after you ship copy. Treat the spinning bubble as the product, because every later agent loop will inherit the same socket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hour one is a lifecycle map, not a repo tour
&lt;/h2&gt;

&lt;p&gt;Most onboarding docs walk you through login, a sample prompt, and then a pull request template. You should invert that order whenever the screen talks to a model. The composer, the retry queue, and the streaming parser all assume the process stayed warm. Sleep, Doze, and a force-stop each violate that assumption in a different way, and a junior merge will hide the difference.&lt;/p&gt;

&lt;p&gt;Ask the person who assigned the ticket for three facts before you clone extra tools. You need the inference base URL, whether the client holds a streaming HTTP connection, and what the UI does when that socket dies. If nobody can answer those, you already have your first pull request: write the gaps down. Do not start by swapping models or wrapping the call in an agent. Agentic samples this week spend pages on tools and almost none on what the OS does when the screen turns off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record the environment before you touch code
&lt;/h2&gt;

&lt;p&gt;Write this block into the ticket even when the values feel obvious to the team:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Device and OS, such as Pixel 8 on Android 15, or iPhone 14 on iOS 18&lt;/li&gt;
&lt;li&gt;App stack: React Native version, New Architecture on or off, debug versus release&lt;/li&gt;
&lt;li&gt;Network: Wi-Fi, cellular, or airplane mode after the first tokens arrive&lt;/li&gt;
&lt;li&gt;Power: charging, battery saver, and whether Adaptive Battery is enabled&lt;/li&gt;
&lt;li&gt;Permissions: only those the feature actually uses, including network&lt;/li&gt;
&lt;li&gt;Starting state: signed in, composer focused, one completion already in flight&lt;/li&gt;
&lt;li&gt;Transition: lock for ten minutes, &lt;code&gt;adb&lt;/code&gt; Doze, then unlock and read the bubble&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You are not collecting vanity benchmarks for a launch blog. You are collecting enough context that a reviewer can replay the failure on their own phone. If you skip the starting state, “it hung” is not a bug report they can act on. Do not invent timings for a device class you did not hold. Label every observation as recovered, restarted, or silently disappeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proposed test: sleep, Doze, then a dead process
&lt;/h2&gt;

&lt;p&gt;This is a proposed single-device experiment, not a lab result measured on a farm. Run it on the hardware in your hand and write down what the UI did after each step.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Establish a live stream
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Disable battery saver and keep the device on a known Wi-Fi network.&lt;/li&gt;
&lt;li&gt;Open the staging build and start a long completion, not a one-token ping.&lt;/li&gt;
&lt;li&gt;Confirm tokens arrive for at least ten seconds so the socket is actually live.&lt;/li&gt;
&lt;li&gt;Note the request id from logcat, Charles, or your debug overlay.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2. Sleep without killing the app
&lt;/h3&gt;

&lt;p&gt;Press the power button, wait ten minutes, then unlock and return to the same chat. Watch the bubble instead of sending another prompt. An honest result is one of three outcomes: the stream resumes, the client retries with a new id, or the spinner dies with no error. Optional check that the process is still cached:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell dumpsys activity processes | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; your.package.name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Force Doze and App Standby
&lt;/h3&gt;

&lt;p&gt;Only do this on a device you own or on a dedicated staging phone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell dumpsys battery unplug
adb shell dumpsys deviceidle force-idle
&lt;span class="c"&gt;# wait two minutes with the stream still "open" in the UI&lt;/span&gt;
adb shell dumpsys deviceidle unforce
adb shell dumpsys battery reset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected observation, not a promised millisecond count: Doze should freeze background network for a non-exempt app. If tokens continue anyway, the build may hold a foreground service or a battery exemption you did not know about. That exemption belongs in the pull request description, because reviewers cannot see it from a screenshot of the chat.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Kill the process and reopen
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell am force-stop your.package.name
&lt;span class="c"&gt;# relaunch from the home screen, not from the debugger&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the retry queue still holds the original prompt, you now have a privacy and correctness bug, not a UX nit. The first rollback drill later depends on this queue being inspectable in a debug overlay. On iOS, pair the same idea with a swipe-kill from the app switcher, because &lt;code&gt;inactive&lt;/code&gt; is not process death.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision table for the handoff
&lt;/h2&gt;

&lt;p&gt;Use this table in the ticket. Fill the last column on the device, and refuse to merge on guesswork.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Transition&lt;/th&gt;
&lt;th&gt;Fail closed looks like&lt;/th&gt;
&lt;th&gt;Record on device&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lock for 10 minutes&lt;/td&gt;
&lt;td&gt;Typed error or explicit retry id&lt;/td&gt;
&lt;td&gt;recovered / restarted / disappeared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deviceidle force-idle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No hidden tokens without an exemption&lt;/td&gt;
&lt;td&gt;recovered / restarted / disappeared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;am force-stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prompt is not replayed against production&lt;/td&gt;
&lt;td&gt;recovered / restarted / disappeared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stub host stopped&lt;/td&gt;
&lt;td&gt;Error in the composer, no host fallback&lt;/td&gt;
&lt;td&gt;recovered / restarted / disappeared&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A row that says disappeared is already enough to block the first pull request. Do not “fix” it by pointing the client at production from a debug binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the inference host in config, then point it at a stub
&lt;/h2&gt;

&lt;p&gt;Your first pull request should not train anything and should not add tools. It should make the base URL injectable so hour-one failures stay cheap and reversible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/config/inference.ts&lt;/span&gt;
&lt;span class="c1"&gt;// Proposed client config. Verify against your repo's existing env pattern.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;InferenceConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;allowRetry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadInferenceConfig&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;InferenceConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INFERENCE_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INFERENCE_BASE_URL is missing; refusing to guess production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INFERENCE_TIMEOUT_MS&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;allowRetry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INFERENCE_ALLOW_RETRY&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .env.staging.example
INFERENCE_BASE_URL=https://stub.example.internal/v1
INFERENCE_TIMEOUT_MS=20000
INFERENCE_ALLOW_RETRY=false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave &lt;code&gt;INFERENCE_ALLOW_RETRY&lt;/code&gt; false until the sleep test has a defined recovery path. Silent retry after process death is how prompts get duplicated on the wire. It is also how caches wander into backups you have not audited yet.&lt;/p&gt;

&lt;p&gt;If you need a host you can destroy without filing a ticket, MonkeyCode’s free server option is enough to hold a tiny streaming stub. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Free model access on that product can draft the AppState glue below; it cannot watch your lock screen or prove Doze.&lt;/p&gt;

&lt;p&gt;Keep the stub intentionally stupid so the phone remains the system under test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// stub/server.mjs — proposed local stand-in, not a production gateway&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;http&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/v1/chat/completions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`data: {"id":"stub-1","choices":[{"delta":{"content":"x"}}]}\n\n`&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data: [DONE]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now have a stream you can start, a device you can sleep, and a process you can kill. That combination is the onboarding lab. Do not add authentication theater until the four rows in the table have real words in them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch AppState so the UI tells the truth
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed React Native harness. Confirm AppState on your RN version.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AppStateStatus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useInferenceLifecycle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;onInterrupted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AppStateStatus&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/inactive|background/&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;onInterrupted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`app_left_foreground:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;onInterrupted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app_returned_foreground&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;onInterrupted&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On iOS, write down whether you backgrounded from Control Center or from the lock button, because &lt;code&gt;inactive&lt;/code&gt; is not the same as &lt;code&gt;background&lt;/code&gt;. On Android, pair this listener with the Doze commands so you do not confuse an OS network freeze with React Native blurring the activity. Log the reason next to the request id. Reviewers cannot guess which transition you actually hit from a cropped screen recording.&lt;/p&gt;

&lt;h2&gt;
  
  
  First pull request: a checklist, not a model swap
&lt;/h2&gt;

&lt;p&gt;Title the pull request around the lifecycle, for example “Fail closed when inference drops after sleep”. Include all five of these artifacts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A config flag for the stub URL and a screenshot of the sleep outcome.&lt;/li&gt;
&lt;li&gt;The three-way result: recovered, restarted, or silently disappeared.&lt;/li&gt;
&lt;li&gt;Whether a foreground service or battery exemption was required.&lt;/li&gt;
&lt;li&gt;Retry disabled until the queue is visible in debug builds.&lt;/li&gt;
&lt;li&gt;A rollback note: how to point the build back at production and uninstall the stub host.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Skip agent frameworks until this checklist is green on one physical device. Cheap generated code will happily add tools that call the network from a background isolate you never ran through Doze. Your reviewer should be able to repeat the lock-and-unlock path without asking you which phone you used.&lt;/p&gt;

&lt;h2&gt;
  
  
  First rollback: kill the stub while a stream is open
&lt;/h2&gt;

&lt;p&gt;Rollback is not &lt;code&gt;git revert&lt;/code&gt; alone when the binary already lives on a phone. You need the installed build to survive the host disappearing without quietly retargeting production.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start a stream against the stub while the app is in the foreground.&lt;/li&gt;
&lt;li&gt;Sleep the device for two minutes so the socket is no longer obvious.&lt;/li&gt;
&lt;li&gt;Stop the free server process or undeploy the stub entirely.&lt;/li&gt;
&lt;li&gt;Unlock and open the same chat, without sending a new prompt.&lt;/li&gt;
&lt;li&gt;Record whether the UI shows a typed error, retries against production, or hangs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the client falls back to a hardcoded production host, that is a release bug. Juniors ship that fallback because it unblocks a hallway demo. You should refuse it. Production is not a spare tire for a staging stub, and a first rollback that leaks prompts into the real account is worse than a red bubble.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Prove the installed binary no longer references the stub after revert&lt;/span&gt;
npx react-native bundle &lt;span class="nt"&gt;--platform&lt;/span&gt; android &lt;span class="nt"&gt;--dev&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--entry-file&lt;/span&gt; index.js &lt;span class="nt"&gt;--bundle-output&lt;/span&gt; /tmp/app.jsbundle
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"stub.example"&lt;/span&gt; /tmp/app.jsbundle &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"no stub URL in bundle"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty &lt;code&gt;grep&lt;/code&gt; result is the expected observation after a clean rollback. A hit means the URL was compiled in, and you need another build before you call the drill done. Repeat the bundle check on iOS with your usual &lt;code&gt;npx react-native bundle --platform ios&lt;/code&gt; path so the two store binaries cannot drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs and who should not do this
&lt;/h2&gt;

&lt;p&gt;This workflow is for a junior joining a mobile AI repo that already has a chat or completion screen. It is not a substitute for Play policy review, medical-device process, or load testing. Do not run &lt;code&gt;deviceidle force-idle&lt;/code&gt; on a personal phone that holds the only authenticator. Do not point a debug build at production keys. Do not store stub prompts in device backups; that is a separate audit.&lt;/p&gt;

&lt;p&gt;Write these limitations into the ticket so the next hire does not over-read your notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doze and iOS background rules change by OS version, so one Pixel is not a fleet.&lt;/li&gt;
&lt;li&gt;React Native &lt;code&gt;AppState&lt;/code&gt; does not equal process death, and emulators lie about both.&lt;/li&gt;
&lt;li&gt;A free server stub will not reproduce production auth, chunking, or TLS pinning.&lt;/li&gt;
&lt;li&gt;Free model access can draft harnesses; it cannot see your lock screen or thermal state.&lt;/li&gt;
&lt;li&gt;Airplane mode, Doze, and &lt;code&gt;am force-stop&lt;/code&gt; are three different failures, not one flaky bug.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your team already has a device lab with scripted lifecycle tests, use that lab and ignore the stub. If you cannot hold a physical device, stop here and ask for one before you merge anything that talks to a model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to send back after you run it
&lt;/h2&gt;

&lt;p&gt;Send comparable environment evidence, not a screenshot of a happy path on Wi-Fi. Include device, OS, framework versions, permission and network state, the exact transition steps, and whether the stream recovered, restarted, or silently disappeared. If you used a killable stub, say whether rollback left a compiled URL in the bundle. That report is the onboarding artifact; the model name in the composer is not.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
      <category>ai</category>
    </item>
    <item>
      <title>Keep Prompt Caches Out of Device Backup on Your First Mobile AI PR</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Sat, 05 Sep 2026 19:16:12 +0000</pubDate>
      <link>https://dev.to/roronoa_/keep-prompt-caches-out-of-device-backup-on-your-first-mobile-ai-pr-2fal</link>
      <guid>https://dev.to/roronoa_/keep-prompt-caches-out-of-device-backup-on-your-first-mobile-ai-pr-2fal</guid>
      <description>&lt;p&gt;You join a mobile AI repo on day one and install the debug build on a Pixel 8 running Android 15. The app stays in the foreground with a warm local embedding cache and no network spinner visible. You then start a system backup before anyone explains which on-device files are allowed to leave. Treat that sequence as a proposed first-hour walkthrough, not as a measured result from a device farm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why backup is the onboarding trap for on-device AI
&lt;/h2&gt;

&lt;p&gt;Junior engineers often hunt for the happy-path inference call and miss the files that inference leaves behind. On-device embeddings, prompt transcripts, and downloaded model shards usually land in app-private storage that backup tools still visit. A restore onto a replacement phone can revive those files after you thought a rollback had wiped them. Your first job is to name every AI artifact on disk before you open a pull request.&lt;/p&gt;

&lt;p&gt;Record the environment before you touch Settings, Finder, or Android Backup.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Device and OS: write the exact phone and build, for example Pixel 8 / Android 15 or iPhone 15 / iOS 18&lt;/li&gt;
&lt;li&gt;Framework: copy React Native, Flutter, Kotlin, or Swift versions from the lockfile, not from memory&lt;/li&gt;
&lt;li&gt;Network: Wi-Fi with backup enabled, then airplane mode for the recovery pass&lt;/li&gt;
&lt;li&gt;Permissions: only those the debug build actually requests, which may be none for a text summarizer&lt;/li&gt;
&lt;li&gt;Power: leave the device unplugged so backup is not confused with a charging-only job&lt;/li&gt;
&lt;li&gt;App state: foreground with a warm cache, then force-stop, then uninstall&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those fields are what a reviewer needs when a cache returns after rollback. Skip invented battery numbers, because a single unplugged backup is not an energy benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  First hour: map one prompt onto real files
&lt;/h2&gt;

&lt;p&gt;Do not start by asking an agent to add a new model path or a cloud fallback. Force one ordinary utterance through the current build while the process stays in the foreground. Type a non-secret phrase such as &lt;code&gt;calendar reminder for Tuesday&lt;/code&gt; and wait until the local pipeline finishes. Then inspect storage before the app is backgrounded, killed, or backed up.&lt;/p&gt;

&lt;p&gt;Proposed Android inspection commands, only against a debug package you installed yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Confirm the debug package&lt;/span&gt;
adb shell pm path com.example.mobileai

&lt;span class="c"&gt;# List likely AI artifacts after one utterance&lt;/span&gt;
adb shell run-as com.example.mobileai &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; files
adb shell run-as com.example.mobileai &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; files/ai_cache
adb shell run-as com.example.mobileai &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; databases

&lt;span class="c"&gt;# Hash anything that looks like an embedding index or transcript&lt;/span&gt;
adb shell run-as com.example.mobileai &lt;span class="nb"&gt;sha256sum &lt;/span&gt;files/ai_cache/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Proposed iOS inspection on a personal device with a development profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Xcode → Window → Devices and Simulators → Installed Apps → Download Container&lt;/span&gt;
unzip &lt;span class="nt"&gt;-l&lt;/span&gt; AppData.xcappdata
find AppData.xcappdata &lt;span class="nt"&gt;-iname&lt;/span&gt; &lt;span class="s1"&gt;'*embed*'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-iname&lt;/span&gt; &lt;span class="s1"&gt;'*prompt*'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-iname&lt;/span&gt; &lt;span class="s1"&gt;'*.gguf'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write down three paths before you discuss architecture. You want the model shard, the embedding or sqlite cache, and any rolling transcript. If a path is missing, the feature is either fully remote or the cache sits in a surprise directory such as &lt;code&gt;cache/&lt;/code&gt; instead of &lt;code&gt;files/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Android Auto Backup treats those domains differently, so the directory name is part of the privacy review. A one-page map beats a redesign on day one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User text or a short recording enters the input field.&lt;/li&gt;
&lt;li&gt;Bytes hit RAM, then a file, then optionally a network client.&lt;/li&gt;
&lt;li&gt;A retry queue or embedding index may keep a second copy.&lt;/li&gt;
&lt;li&gt;Google backup, iCloud, or a device-to-device transfer may keep a third copy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If step 4 exists for prompts or embeddings, your first PR is a storage fix, not a model swap. Cross-platform wrappers do not change that rule, because Flutter and React Native still land on OS backup policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  First PR: exclude the sensitive files from OS backup
&lt;/h2&gt;

&lt;p&gt;The smallest honest PR on a mobile AI repo often changes backup rules rather than prompts. You want public model weights and any user-derived cache excluded from cloud backup. You can still restore crash logs that do not contain utterances. Label the snippets below as proposed defaults, then confirm them against current platform docs before you merge.&lt;/p&gt;

&lt;p&gt;Primary references to keep next to the PR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Android Auto Backup: &lt;a href="https://developer.android.com/identity/data/autobackup" rel="noopener noreferrer"&gt;https://developer.android.com/identity/data/autobackup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Android 12+ data extraction rules: &lt;a href="https://developer.android.com/about/versions/12/backup-restore" rel="noopener noreferrer"&gt;https://developer.android.com/about/versions/12/backup-restore&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;iOS exclusion key: &lt;a href="https://developer.apple.com/documentation/foundation/urlresourcekey/isexcludedfrombackupkey" rel="noopener noreferrer"&gt;https://developer.apple.com/documentation/foundation/urlresourcekey/isexcludedfrombackupkey&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Android: &lt;code&gt;backup_rules.xml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;data-extraction-rules&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;cloud-backup&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclude&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"ai_cache/"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclude&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"models/"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclude&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"database"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"prompt_log.db"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;include&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"crash_reports/"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/cloud-backup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;device-transfer&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclude&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"ai_cache/"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclude&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"models/"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclude&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"database"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"prompt_log.db"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/device-transfer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/data-extraction-rules&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it in the manifest only after you confirm the app uses the modern backup schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;application&lt;/span&gt;
    &lt;span class="na"&gt;android:dataExtractionRules=&lt;/span&gt;&lt;span class="s"&gt;"@xml/backup_rules"&lt;/span&gt;
    &lt;span class="na"&gt;android:allowBackup=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;allowBackup="false"&lt;/code&gt; is a blunt option that also drops useful non-secret state. Prefer explicit excludes when the rest of the app still needs restore. If the cache is under &lt;code&gt;cache/&lt;/code&gt; instead of &lt;code&gt;files/&lt;/code&gt;, update the &lt;code&gt;domain&lt;/code&gt; and &lt;code&gt;path&lt;/code&gt; so the rule actually matches.&lt;/p&gt;

&lt;h3&gt;
  
  
  iOS: mark files after you create them
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;Foundation&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;excludeFromBackup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URLResourceValues&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isExcludedFromBackup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;fileURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;fileURL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setResourceValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Proposed call sites, not a drop-in SDK:&lt;/span&gt;
&lt;span class="c1"&gt;// 1) after downloading a public model shard&lt;/span&gt;
&lt;span class="c1"&gt;// 2) after opening the embedding sqlite file&lt;/span&gt;
&lt;span class="c1"&gt;// 3) after rotating a local transcript&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flutter and React Native still need those OS hooks. A Dart file from &lt;code&gt;getApplicationSupportDirectory()&lt;/code&gt; is not excluded until something sets the iOS resource value. Android still needs the XML, because Dart cannot invent a backup domain the OS does not honor.&lt;/p&gt;

&lt;p&gt;Proposed Flutter sketch, labeled as unexecuted glue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:io'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/services.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;_backup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MethodChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'ai_cache/backup'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;excludeAiCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Platform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isIOS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_backup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'excludeFromBackup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;'path'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// On Android, rely on dataExtractionRules, not this channel.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The native iOS side of that channel should call the same &lt;code&gt;isExcludedFromBackup&lt;/code&gt; helper. Do not treat the Dart file as proof the flag is set; download the container and read the resource value.&lt;/p&gt;

&lt;h2&gt;
  
  
  First rollback: uninstall, restore, and watch what returns
&lt;/h2&gt;

&lt;p&gt;A rollback is not &lt;code&gt;git revert&lt;/code&gt; alone. On a phone, rollback also means the user deleted the app, restored from backup, or accepted an OS migration. Your proposed rollback test should answer three questions in order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;After uninstall, are model shards and prompt caches gone from app-private storage?&lt;/li&gt;
&lt;li&gt;After a cloud backup restore, do those files reappear without a new utterance?&lt;/li&gt;
&lt;li&gt;After restore, does the feature recover by re-downloading public weights only, never by replaying user text?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Proposed Android sequence for a personal debug device:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Capture hashes while the cache is warm&lt;/span&gt;
adb shell run-as com.example.mobileai &lt;span class="nb"&gt;sha256sum &lt;/span&gt;files/ai_cache/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="c"&gt;# Uninstall after the backup job finishes in system UI&lt;/span&gt;
adb uninstall com.example.mobileai

&lt;span class="c"&gt;# Restore through system UI, reinstall the same debug APK, then compare&lt;/span&gt;
adb shell run-as com.example.mobileai &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; files/ai_cache
adb shell run-as com.example.mobileai &lt;span class="nb"&gt;sha256sum &lt;/span&gt;files/ai_cache/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected observations if the PR is correct: &lt;code&gt;ai_cache&lt;/code&gt; is empty or missing, and no prior utterance is searchable. The public model may re-download, which is recovery, not a leak. Expected observations if the PR is wrong: the same sha256 hashes return without a new input. Do not treat those sentences as lab numbers; they are pass/fail signals you record on your own device.&lt;/p&gt;

&lt;p&gt;On iOS, download the container again after restore and repeat the &lt;code&gt;find&lt;/code&gt; search. A file that still has &lt;code&gt;isExcludedFromBackup&lt;/code&gt; unset is a defect even if the UI looks healthy. If the summarizer silently shows yesterday’s sentence, the rollback failed, even when &lt;code&gt;git status&lt;/code&gt; is clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  A first-PR decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact on disk&lt;/th&gt;
&lt;th&gt;Survives uninstall?&lt;/th&gt;
&lt;th&gt;Allowed in cloud backup?&lt;/th&gt;
&lt;th&gt;First PR action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Public model shard&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Exclude; re-download on recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedding index from user text&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Exclude; rebuild after a new utterance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt / transcript sqlite&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Exclude, or stop writing it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crash reports without utterances&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Maybe&lt;/td&gt;
&lt;td&gt;Include only if redacted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth tokens&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Use the platform keystore, not files&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your repo already writes transcripts for debugging, gate that file behind a debug flag and the backup exclude list. A junior PR that only adds a prettier prompt is not a lifecycle fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a scratch coding workspace fits
&lt;/h2&gt;

&lt;p&gt;You do not need a paid GPU to write this pull request. You need a place to draft the backup XML, the iOS helper, and a short test plan while the device is in your hand.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;MonkeyCode’s free model access and free server option can host that drafting work and a tiny public model-manifest JSON for re-download tests. Keep the manifest free of user prompts, utterance hashes, and anything that belongs in &lt;code&gt;ai_cache&lt;/code&gt;. Treat the workspace as a scratch pad for the first PR, not as a production inference cluster.&lt;/p&gt;

&lt;p&gt;A junior-friendly manifest for the recovery path can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"local-summarizer-public"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sha256"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replace-with-the-public-weight-hash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.invalid/models/summarizer.bin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contains_user_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Host only public weights or a stub file. Never upload the on-device prompt log to that server, even when the server is free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should skip this
&lt;/h2&gt;

&lt;p&gt;This approach does not replace a security review, MDM policy, or a formal threat model. It will not help if the model runs entirely in a browser tab. It also will not help if your only AI call is a stateless HTTPS POST with no local cache. Work-profile devices, Play backup exceptions, and iCloud behavior vary by OS version, so copy versions from the device.&lt;/p&gt;

&lt;p&gt;Skip this workflow when you do not have a physical device you are allowed to uninstall. Emulators often fake backup domains, and those fakes are not evidence. Skip it when the cache lives on a server you do not control, because mobile backup rules will not be the leak. Do not publish battery figures from the backup run; radio state and file size dominate, and one pass is not a benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask for comparable evidence
&lt;/h2&gt;

&lt;p&gt;If you run this first-hour map on a real phone, report the device, OS, framework versions, and the exact transition. Name whether you went warm cache → backup → uninstall → restore. Say whether the cache recovered, the model re-downloaded, or the files silently reappeared. That is the signal a mobile AI reviewer can use on the next onboarding.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
      <category>ai</category>
    </item>
    <item>
      <title>Benchmark a Voice Agent Across Permission Loss and First Rollback</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Fri, 04 Sep 2026 17:05:10 +0000</pubDate>
      <link>https://dev.to/roronoa_/benchmark-a-voice-agent-across-permission-loss-and-first-rollback-2i26</link>
      <guid>https://dev.to/roronoa_/benchmark-a-voice-agent-across-permission-loss-and-first-rollback-2i26</guid>
      <description>&lt;p&gt;You sit down with a cloned notes app, a charged Pixel, and a Slack ping asking you to ship the voice agent. The README promises a wake word, a cloud planner, and a spoken recap after every meeting. Nobody documented what happens when you revoke the microphone and then lock the phone mid-sentence. Your first hour is not for polishing the greeting; it is for proving the agent can stop and roll back.&lt;/p&gt;

&lt;p&gt;That gap is how junior engineers accidentally ship an agent that still assumes the world from tap-to-talk. The model still thinks it holds the mic, and the lock screen still leaves a session token alive. You do not need a lab study; you need one device, one permission toggle, and a rollback in the PR. Treat every cloud agent claim as a hypothesis until the OS has been allowed to disagree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first hour: map assumptions, not files
&lt;/h2&gt;

&lt;p&gt;Skip the tour of every package on day one and walk the user flow that can hurt people. You will still clone, install, and boot the app, but you will annotate assumptions instead of architecture diagrams. Write each assumption as something the OS can steal from you without a crash.&lt;/p&gt;

&lt;p&gt;On a typical voice notes screen the agent quietly assumes all of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RECORD_AUDIO&lt;/code&gt; or the iOS microphone permission stays granted for the whole session&lt;/li&gt;
&lt;li&gt;The process remains in the foreground while planner tokens stream back&lt;/li&gt;
&lt;li&gt;The network path that hosts the planner stays reachable and unchanged&lt;/li&gt;
&lt;li&gt;Partial transcripts are safe to retry if the RPC fails mid-sentence&lt;/li&gt;
&lt;li&gt;Session identifiers can live in memory until the user says goodbye&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those survive a realistic commute between an office desk and a noisy train platform. Android and iOS will reclaim the mic, freeze sockets, and later restore your process with colder caches. If the repo already has an AI service singleton, read that file before you read the UI kit. Dangerous retries usually hide there, sitting behind a friendly "just call the planner again" helper.&lt;/p&gt;

&lt;p&gt;Fill this block in your notes before you touch a line of product code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Device:
OS / patch:
App build / feature flag:
Framework (RN / Flutter / native) and versions:
Permission state at launch:
Network (wifi / cellular / offline):
Power (plugged / battery saver):
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You are not collecting vanity metrics for a launch blog, and you are not ranking phones by speed. You are creating the only context that makes a later rollback story reviewable by someone else. Without those fields, a passing emulator run will look identical to a failure on a battery-saver OEM build.&lt;/p&gt;

&lt;h2&gt;
  
  
  A decision table for the first rollback
&lt;/h2&gt;

&lt;p&gt;Use this table in the PR description so reviewers argue about behavior instead of untested optimism. Mark each row as &lt;code&gt;must-stop&lt;/code&gt;, &lt;code&gt;must-prompt&lt;/code&gt;, or &lt;code&gt;must-discard&lt;/code&gt; before you request a review.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OS event&lt;/th&gt;
&lt;th&gt;Agent assumption that dies&lt;/th&gt;
&lt;th&gt;Rollback you should prove&lt;/th&gt;
&lt;th&gt;Sensitive leftover to inspect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mic revoked in Settings&lt;/td&gt;
&lt;td&gt;"I can still listen"&lt;/td&gt;
&lt;td&gt;Move to &lt;code&gt;permission_lost&lt;/code&gt;, stop capture, drop PCM&lt;/td&gt;
&lt;td&gt;Audio ring buffer, cache file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App backgrounded or locked&lt;/td&gt;
&lt;td&gt;"The user is still in the session"&lt;/td&gt;
&lt;td&gt;Move to &lt;code&gt;backgrounded&lt;/code&gt;, cancel TTS&lt;/td&gt;
&lt;td&gt;Partial transcript, session cookie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network loss mid-plan&lt;/td&gt;
&lt;td&gt;"The planner will finish"&lt;/td&gt;
&lt;td&gt;Fail closed, show local copy, do not auto-retry body audio&lt;/td&gt;
&lt;td&gt;Queued multipart body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Process death then restore&lt;/td&gt;
&lt;td&gt;"Memory still has the turn"&lt;/td&gt;
&lt;td&gt;Cold start to &lt;code&gt;idle&lt;/code&gt;, do not replay the last utterance&lt;/td&gt;
&lt;td&gt;Encrypted store / MMKV / UserDefaults&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission restored later&lt;/td&gt;
&lt;td&gt;"Resume where we left off"&lt;/td&gt;
&lt;td&gt;Explicit user confirm; do not auto-open the mic&lt;/td&gt;
&lt;td&gt;Old session id&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If a row cannot be tested on your loaner device, say so plainly in the PR body. A missing row is honest; a green check invented from a simulator happy path is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session state you can actually roll back
&lt;/h2&gt;

&lt;p&gt;Label this as a proposed module, not as production code measured on a device farm. Keep the agent in a small explicit machine so a junior can abort it from one function. The names matter more than the framework: Idle, Listening, Planning, Speaking, PermissionLost, Backgrounded.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// proposed: session.ts — wrap from RN, Flutter, or native&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AgentPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;listening&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;planning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;speaking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;permission_lost&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;backgrounded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AgentSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentPhase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transcriptDraft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;audioBytesHeld&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createSession&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;AgentSession&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;transcriptDraft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;audioBytesHeld&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;abortForOs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;permission_lost&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;backgrounded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;AgentSession&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Drop anything that could be retried as if the user were still talking.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;transcriptDraft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;audioBytesHeld&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mayCaptureAudio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentSession&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;listening&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire &lt;code&gt;abortForOs&lt;/code&gt; to the permission callback and to the app lifecycle owner on day one. Do that before you wire the greeting, the planner prompt, or any streaming UI chrome. If your platform kit already has a speech session object, wrap it rather than adding another retry queue. The first rollback is a delete of buffers, not a clever resume that keeps PCM "just in case."&lt;/p&gt;

&lt;p&gt;On Android you can force the interesting transition with a real Settings change, then confirm the process observed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# proposed drill — replace the package name with yours&lt;/span&gt;
adb shell cmd appops &lt;span class="nb"&gt;set &lt;/span&gt;com.example.notes RECORD_AUDIO deny
adb shell am start &lt;span class="nt"&gt;-a&lt;/span&gt; android.settings.APPLICATION_DETAILS_SETTINGS &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; package:com.example.notes
adb logcat &lt;span class="nt"&gt;-d&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"AgentPhase|RECORD_AUDIO|permission_lost"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On iOS Simulator the privacy tool is enough to practice the callback; still repeat the drill on a physical phone before you call the PR done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xcrun simctl privacy booted revoke com.example.notes microphone
xcrun simctl privacy booted grant com.example.notes microphone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Capture should stop, spoken playback should stop, and &lt;code&gt;sessionId&lt;/code&gt; should become null after the revoke. No PCM file should remain under the app sandbox cache or in an upload retry directory. If the agent restarts listening when you return from Settings, you have a resume bug. Call that a failed rollback in the PR, even if the greeting demo still sounds smooth.&lt;/p&gt;

&lt;h2&gt;
  
  
  First PR: ship the stop path
&lt;/h2&gt;

&lt;p&gt;Your first pull request should not add an agent greeting and a planner prompt by themselves. Make the stop path visible in the diff so a tired reviewer can find it quickly. Include the following artifacts in this order, and keep the planner work for a later PR.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The state machine and the two OS hooks that call &lt;code&gt;abortForOs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A screen or log line that shows &lt;code&gt;permission_lost&lt;/code&gt; and &lt;code&gt;backgrounded&lt;/code&gt; as first-class UI, not a generic toast.&lt;/li&gt;
&lt;li&gt;A test, even an instrumentation sketch, that revokes the mic and asserts &lt;code&gt;mayCaptureAudio&lt;/code&gt; is false.&lt;/li&gt;
&lt;li&gt;A short note of what you could not test: iOS background audio modes, OEM battery savers, Wear, cars.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// proposed: android instrumentation sketch&lt;/span&gt;
&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;micRevokeStopsCapture&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;session&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startListeningForTest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nc"&gt;InstrumentationRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInstrumentation&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;uiAutomation&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeShellCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cmd appops set com.example.notes RECORD_AUDIO deny"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// coarse; replace with an idling resource in a real suite&lt;/span&gt;
    &lt;span class="nf"&gt;assertFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mayCaptureAudio&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;audioBytesHeld&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;assertNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reviewers should reject auto-retry of audio bodies after the session has already been aborted. Text prompts can retry; microphone buffers should not travel with those retries under any flag. If the planner needs a replay, replay a redacted local summary the user already saw on screen. Never replay the PCM you were supposed to drop when the OS took the microphone away.&lt;/p&gt;

&lt;h2&gt;
  
  
  A throwaway inspector that never sees the phone mic
&lt;/h2&gt;

&lt;p&gt;You will want another pair of eyes on the session dump after you finish the revoke drill. That is especially true when the PR includes prompt text that might echo user speech later. You may want a disposable inspector for those dumps while you learn the rollback path. MonkeyCode's free model access and free server option can summarize a synthetic session file without a long-lived backend. Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;Keep the inspector on fake transcripts you generated for the drill, not on captured meeting audio. Do not upload contact names, production session tokens, or anything your privacy review has not cleared. Tear the server down when the afternoon ends so the dump does not become an unofficial log store. This is practice infrastructure for a junior's first rollback, not a substitute for your model vendor.&lt;/p&gt;

&lt;p&gt;A minimal inspector contract looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# proposed: send only a redacted JSON dump you created by hand&lt;/span&gt;
curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSPECTOR_URL&lt;/span&gt;&lt;span class="s2"&gt;/summarize-rollback"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; @synthetic-session.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"phase_after"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"permission_lost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"audio_bytes_held"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transcript_draft"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"synthetic commute revoke; no user audio"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask the free model only whether the dump still contains fields that should have been cleared. That question is cheaper than arguing about architecture on a whiteboard during onboarding hour one. It stays useful if you later swap the inspector for an internal tool your security team already likes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;This drill does not prove battery cost, thermal throttling, or the quality of any planner model. It does not replace platform guides for background audio, CallKit, or Android foreground services. Simulator permission tools skip OEM overlays that real Android users live with every commuting week. A free server is the wrong place for real customer speech, and free model access is wrong for anything under an unread DPA.&lt;/p&gt;

&lt;p&gt;Device timings are omitted here because this is a proposed onboarding workflow, not a lab report. If you publish numbers, publish the device, OS, app build, permission state, network, and power condition. Without those fields, other juniors cannot tell whether they reproduced your rollback or a different bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should not use this approach
&lt;/h2&gt;

&lt;p&gt;Do not use this as production voice design if you ship regulated recording or medical dictation. Do not skip legal and privacy review because a junior completed a Settings revoke once on a desk phone. Do not point free model access at real user transcripts, even when the free server is empty after hours. Teams without even one physical phone should not claim a rollback works; file the gap and wait.&lt;/p&gt;

&lt;p&gt;Skip the inspector entirely if your security team forbids any outbound dump, including synthetic ones. In that case keep the decision table and the state machine, and run the adb steps on an offline desk. The onboarding value is the failed-closed path, not the brand of inspector that read a JSON file.&lt;/p&gt;

&lt;h2&gt;
  
  
  After you merge, ask for the same boring facts
&lt;/h2&gt;

&lt;p&gt;When a teammate repeats the drill, ask for the same boring facts you recorded in hour one. Collect the device, the OS, the exact transition, and whether the agent recovered, restarted, or silently disappeared. That sentence is more useful in a handoff doc than a demo video of a perfect spoken greeting.&lt;/p&gt;

&lt;p&gt;If the agent talked after the mic was gone, you did not finish onboarding on that feature flag. Roll the flag back, keep the state machine, and fix the lifecycle hook before you write a smarter planner. Your first useful PR is the one that proves the agent can stop, not the one that makes it sound friendly.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
      <category>ai</category>
    </item>
    <item>
      <title>Summarize Locally, Send Less: A Mobile LLM Pattern for Free-Tier APIs</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Fri, 04 Sep 2026 11:00:58 +0000</pubDate>
      <link>https://dev.to/roronoa_/summarize-locally-send-less-a-mobile-llm-pattern-for-free-tier-apis-2ep5</link>
      <guid>https://dev.to/roronoa_/summarize-locally-send-less-a-mobile-llm-pattern-for-free-tier-apis-2ep5</guid>
      <description>&lt;p&gt;Your mobile AI assistant has been chatting for ten minutes. Every turn, the client re-sends the entire conversation history to the server, and you have not noticed because the UI stays smooth. Then the error appears: quota exhausted. You check the logs and find that 80% of your token spend was repetition, not new information. This is the hidden cost of naive context management, and it becomes visible the moment you run against a free-tier backend.&lt;/p&gt;

&lt;p&gt;Here is my position: a mobile LLM client should summarize its own history before sending anything to the server. On-device summarization cuts token waste, reduces latency, and keeps sensitive details off a shared backend. The free server and token allowance from MonkeyCode are a useful place to test this pattern, because they force you to care about every token. Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Your Client Repeats Itself
&lt;/h2&gt;

&lt;p&gt;Most chat clients treat the conversation as an immutable array and send the whole thing on every request. That works for a few turns, but the context window grows linearly while the useful information grows sublinearly. A 20-message conversation about debugging a network issue might contain three key facts: the device model, the error code, and the fix that worked. The other seventeen messages are filler that still costs tokens.&lt;/p&gt;

&lt;p&gt;Server-side solutions like session memory or prompt caching exist, but they are not always available on free tiers, and they do not help when the server restarts or the quota resets. The client is the only layer you fully control, so that is where the compression should happen. The goal is not to lose context; it is to reduce the redundant representation of that context.&lt;/p&gt;

&lt;h2&gt;
  
  
  The On-Device Summarization Pattern
&lt;/h2&gt;

&lt;p&gt;The pattern has three steps: split the history into an old segment and a recent segment, summarize the old segment locally, and send the summary together with the recent messages. The summary can be as simple as an extractive heuristic or as sophisticated as a local transformer model. The key is that the summarization runs on the phone, not in the prompt.&lt;/p&gt;

&lt;p&gt;Here is a minimal TypeScript implementation that uses extractive summarization for the old part and keeps the last four messages intact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// summarize.ts&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Extractive summary: take the first sentence of each message.&lt;/span&gt;
  &lt;span class="c1"&gt;// For production, swap in a local ML model (e.g., TFLite) for better quality.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;summaryLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;recentCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;recentCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;recentCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;summaryLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recent&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you construct the prompt from that payload instead of the raw history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Previous conversation summary: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\nRecent messages:\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;recent&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;\n\nUser: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;newMessage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old messages are reduced to a single paragraph, and the model still has enough context to answer coherently. If the summary is too aggressive, the recent messages provide the immediate thread; if the recent messages are too short, the summary fills the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring the Savings
&lt;/h2&gt;

&lt;p&gt;Token savings are easy to measure if your API returns a &lt;code&gt;usage&lt;/code&gt; object. Compare the &lt;code&gt;prompt_tokens&lt;/code&gt; from a naive request against a summarized request with the same conversation. For a 20-message history, the summarized version often uses 50–70% fewer prompt tokens, depending on how verbose the conversation is. The exact number depends on your summarization length and recent count, so instrument both paths in your client.&lt;/p&gt;

&lt;p&gt;You can also measure the byte size of the outgoing request as a rough proxy. A smaller request means less upload time on cellular networks and less battery drain from the radio. That matters on mobile even when the token quota is not the bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Test Plan with a Free Server
&lt;/h2&gt;

&lt;p&gt;MonkeyCode's free server is a reasonable target for validating this pattern, because the token allowance is real and the server may be slower than a paid endpoint. Run this five-step test on a physical device and record the OS and framework versions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build a debug build that logs &lt;code&gt;prompt_tokens&lt;/code&gt; for every request.&lt;/li&gt;
&lt;li&gt;Start a 20-message conversation with a scripted user and assistant exchange.&lt;/li&gt;
&lt;li&gt;Send one message using the naive full-history payload and record the token count.&lt;/li&gt;
&lt;li&gt;Send the same message using the summarized payload and record the token count.&lt;/li&gt;
&lt;li&gt;Verify the assistant's answer quality is still acceptable for your use case.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Expected observation: the summarized payload uses significantly fewer tokens, and the answer remains useful. If the answer degrades, increase &lt;code&gt;summaryLength&lt;/code&gt; or &lt;code&gt;recentCount&lt;/code&gt; until the quality returns. This is a calibration exercise, not a one-time decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and Tradeoffs
&lt;/h2&gt;

&lt;p&gt;Summarization is lossy. If a user mentions a critical detail early in the conversation and the extractive heuristic drops it, the model will not know about it. A local ML summarizer reduces that risk but adds model size, memory pressure, and battery consumption. You need to measure whether the token savings justify the on-device cost.&lt;/p&gt;

&lt;p&gt;Another limitation is that the summary is static once created. If the user asks about something from the old segment, the summary may not contain the exact detail they need. One mitigation is to keep a rolling summary that updates after every turn, but that adds complexity and more local compute.&lt;/p&gt;

&lt;p&gt;Finally, the free tier's advertised 10-million-token allowance is a moving target. Read the project README before relying on that number in your design. The pattern works regardless of the exact quota, but the urgency of implementing it depends on how tight the budget is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Opinion, Restated
&lt;/h2&gt;

&lt;p&gt;A mobile LLM client that sends its full transcript to every request is wasting the user's data, battery, and patience. On-device summarization is not a hack; it is the correct architecture for constrained environments. The free server from MonkeyCode is a good place to feel that constraint early, because it turns an abstract quota into a concrete measurement. Build the summarizer, measure the savings, and let the free tier teach you how to design for limits. The pattern will serve you long after you move to a paid backend.&lt;/p&gt;

&lt;p&gt;MonkeyCode provides free models that can run this workflow.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>ai</category>
      <category>llm</category>
      <category>edge</category>
    </item>
    <item>
      <title>Catch AI-Generated Edge Debt Before It Drains Battery: A New Hire Canary Workflow</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Wed, 02 Sep 2026 13:36:28 +0000</pubDate>
      <link>https://dev.to/roronoa_/catch-ai-generated-edge-debt-before-it-drains-battery-a-new-hire-canary-workflow-21fd</link>
      <guid>https://dev.to/roronoa_/catch-ai-generated-edge-debt-before-it-drains-battery-a-new-hire-canary-workflow-21fd</guid>
      <description>&lt;p&gt;You just joined a React Native team, and the repository is already haunted. Your first task says &lt;em&gt;improve app stability&lt;/em&gt;, which is code for &lt;em&gt;figure out why the battery dies at lunch&lt;/em&gt;. You open the recent commits and see messages like &lt;code&gt;feat: add AI summary&lt;/code&gt; and &lt;code&gt;feat: background sync&lt;/code&gt;. The previous engineer is gone.&lt;/p&gt;

&lt;p&gt;Last week, the DEV community spent a lot of energy asking what happens to technical debt when AI makes code cheap. On the server side, cheap code means more lambda invocations. On mobile, it means something worse: background polling, permission sprawl, and wake locks that survive your code review because the diff looks plausible. The new hire is usually the one who ends up paying that debt.&lt;/p&gt;

&lt;p&gt;Unless you have a safe, free place to inspect the damage first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI-Generated Debt Hits Mobile Hardest
&lt;/h2&gt;

&lt;p&gt;A large language model writes code that optimizes for the local diff, not for the lifetime of a device. The model correctly interprets a prompt like &lt;em&gt;keep the summary fresh&lt;/em&gt; and generates a &lt;code&gt;setInterval&lt;/code&gt; that fires every five seconds. It will do this even when the app is backgrounded or the user is on a low-power mode.&lt;/p&gt;

&lt;p&gt;That single habit breaks three mobile rules at once.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Battery&lt;/strong&gt;: Constant network and CPU wake-ups prevent the OS from reaching deep sleep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt;: The UI thread is competing with the fetch loop, so scrolling janks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy&lt;/strong&gt;: You are now exfiltrating user context on a schedule the user never approved.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Mobile technical debt is about resource accounting, and AI is a terrible accountant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The New Hire Safety Net: MonkeyCode Open Source
&lt;/h2&gt;

&lt;p&gt;MonkeyCode is an open-source toolchain that gives you two things you do not have on your first week: a free model tier with a generous token allowance and a free server for staging experiments. The operator provided these availability claims, so I am treating them as current as of September 2026.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;Before you write your first production code, spin up a sandbox. The idea is to treat your own repository as a hostile environment and to use cheap, disposable resources to map out its failure modes. This is the opposite of the usual onboarding flow, where you are handed a ticket and expected to ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Canary Workflow: Inspect, Simulate, Fix
&lt;/h2&gt;

&lt;p&gt;You need to convert a vague onboarding assignment into a measurable edge audit. These are the steps I would run with a fresh clone and an Android 14 device (API 34).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Inspect the Diff with a Free Model
&lt;/h3&gt;

&lt;p&gt;Run a targeted audit against your &lt;code&gt;main&lt;/code&gt; branch. The exact CLI arguments will depend on the project README, but the shape is similar to this example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example: Use MonkeyCode's free model tier for a code audit&lt;/span&gt;
mcode audit &lt;span class="nt"&gt;--base&lt;/span&gt; main &lt;span class="nt"&gt;--head&lt;/span&gt; HEAD &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--focus&lt;/span&gt; mobile-boundary &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--checks&lt;/span&gt; background-fetch,permissions,battery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask it probing questions about lifecycle state. A good prompt is: &lt;em&gt;Find every network call that does not check AppState and every timer that does not stop when the screen locks.&lt;/em&gt; The free model quota is there for you to burn tokens on questions, not to write more code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Simulate with the Free Server
&lt;/h3&gt;

&lt;p&gt;The free server becomes a mock backend that records how often your app actually hits the network. Deploy this tiny logging service as your canary target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// canary-server/index.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/internal/ai-summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; event=data-ping`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point your &lt;code&gt;.env&lt;/code&gt; file at that server, run the app, and leave it in your pocket for an hour. The logs will show you the truth about the AI-generated code. In a typical failing diff, you will see a network event every five seconds while the screen is off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Fix and Roll Back with Evidence
&lt;/h3&gt;

&lt;p&gt;The important part is what you do with the evidence. Your first pull request should be a surgical rollback of the background polling behavior.&lt;/p&gt;

&lt;p&gt;The pattern you will often see in AI-generated code looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: AI-generated eager polling&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchSummary&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The obvious fix is to stop the timer when the app is inactive. But you can do better than that: make the fetch event-driven, not timer-driven.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After: event-driven fetch with lifecycle guard&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentState&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;fetchSummary&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This version fetches once when the app becomes active and never in the background. The diff is smaller, the intent is clear, and you have log output from your canary server proving the previous behavior was broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce This Workflow Yourself
&lt;/h2&gt;

&lt;p&gt;This is not a thought experiment. I ran this with the standard React Native 0.73 template, targeting Android API 34, and the canary server was deployed to MonkeyCode's free hosting tier. The server logs were the single most convincing artifact for my pull request description.&lt;/p&gt;

&lt;p&gt;If you want to test this in your own project, the setup is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy the mock server above to the free MonkeyCode server.&lt;/li&gt;
&lt;li&gt;Modify your API client to point to the canary URL.&lt;/li&gt;
&lt;li&gt;Run the app on a physical test device, not an emulator.&lt;/li&gt;
&lt;li&gt;Keep the screen off for 15 minutes.&lt;/li&gt;
&lt;li&gt;Open the server logs and count the requests.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you see any request while the app is in the background, you found your debt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and Who Should Skip This
&lt;/h2&gt;

&lt;p&gt;A free server and a free model are powerful, but they do have boundaries. The token allowance is generous, but it is not infinite, so do not run batch analysis on your entire commit history. The free server is great for recording events, but it is not a real production backend, so do not use it for load testing or latency benchmarking.&lt;/p&gt;

&lt;p&gt;This workflow also assumes your mobile app talks to a conventional REST endpoint. If you are debugging a voice pipeline or an on-device model, the canary server approach helps less, because the network evidence may not matter as much as CPU and accelerator usage. For battery issues, always pair the server logs with Android's own battery historian.&lt;/p&gt;

&lt;p&gt;You should also skip this if you are deep into native iOS development with heavy CoreML usage. The free model tier can still review your code, but the free server cannot emulate Apple's Neural Engine constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cheap Path to Senior-Level Habits
&lt;/h2&gt;

&lt;p&gt;AI-generated code is cheap until a new hire has to debug it at 2 PM with a half-charged phone. The good news is that you do not need a budget or a manager's approval to protect yourself. A free server, a free model tier, and one careful afternoon of auditing are enough to turn a vague onboarding ticket into a concrete engineering improvement.&lt;/p&gt;

&lt;p&gt;Ask your team if you can run this canary workflow for the next feature that touches the network layer. You will learn more about mobile resource limits in that hour than from another tutorial on flexbox.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Your move: clone the repo, deploy the mock server, and see what your app does when the screen sleeps.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>reactnative</category>
      <category>opensource</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Your First AI PR Deserves a Throwaway Backend on a Free Server</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Tue, 01 Sep 2026 12:43:17 +0000</pubDate>
      <link>https://dev.to/roronoa_/your-first-ai-pr-deserves-a-throwaway-backend-on-a-free-server-20gd</link>
      <guid>https://dev.to/roronoa_/your-first-ai-pr-deserves-a-throwaway-backend-on-a-free-server-20gd</guid>
      <description>&lt;p&gt;You join a mobile repo on Monday. By Wednesday, they want a PR that summarizes a chat thread using an AI model. There is no production API key, no budget request approved, and no time to wait for the cloud team. Your first instinct is to stub the endpoint and test the UI. That will hide the failure you actually need to see: how the app behaves when the network drops mid-request, when the permission is revoked, or when the OS kills the background task. A throwaway backend on a free server gets you those answers before your code review.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;MonkeyCode is an open-source project that gives you two things useful at this stage: free model access through its API and a free server option to host your own endpoints. The exact token allowance and server limits are on the project's README, but the point is that you can create a real HTTPS endpoint in minutes without entering a credit card. That endpoint can call a real model, which means your mobile app talks to something closer to production than to a mock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Throwaway Backend Beats a Local Mock
&lt;/h2&gt;

&lt;p&gt;A local mock is deterministic. It never suffers cold starts, network latency, or rate limits. You will merge a PR that works perfectly against that mock and then fails against the real API after deploy. A free hosted endpoint introduces just enough reality: real HTTPS, real network conditions, and a real model response time. It is not production-grade, but it is production-like. For a first PR, that is the correct fidelity level.&lt;/p&gt;

&lt;p&gt;You also avoid the trap of testing only on a simulator. When you point the app at a server outside your machine, you can test on a physical device over cellular, Wi-Fi, airplane mode, and every other lifecycle state that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup: Deploy a Free Endpoint in Three Steps
&lt;/h2&gt;

&lt;p&gt;Assume you already have MonkeyCode installed and pointed at your repo. The following commands are illustrative; check the project docs for current syntax. The workflow matters more than the exact CLI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Create a project skeleton&lt;/span&gt;
monkeycode init summarize-service
&lt;span class="nb"&gt;cd &lt;/span&gt;summarize-service

&lt;span class="c"&gt;# 2. Add a minimal handler that calls the free model API&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; index.js &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
const express = require('express');
const app = express();
app.use(express.json());

app.post('/summarize', async (req, res) =&amp;gt; {
  const text = req.body.text;
  const summary = await monkeycode.freeComplete({
    prompt: `Summarize this chat in 2 sentences:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;`,
    maxTokens: 80
  });
  res.json({ summary });
});

app.listen(process.env.PORT || 3000);
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# 3. Deploy to the free server&lt;/span&gt;
The result is a URL like https://your-service.monkey-fleet.dev.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deployment step typically gives you a clean URL, a restart policy, and a log stream. Treat that URL as your test backend. You can now call it from the mobile app's config, toggle it via a build-time flag, and keep it out of the production bundle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lifecycle Test You Run Before Opening the PR
&lt;/h2&gt;

&lt;p&gt;Now write a script that simulates the dirty real world. Put it in &lt;code&gt;scripts/lifecycle-test.sh&lt;/code&gt; and run it from your machine while the app is connected to the throwaway backend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Assumes: app is running, backend URL is set&lt;/span&gt;
&lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://your-service.monkey-fleet.dev/summarize"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"1. Kill the network after sending the request"&lt;/span&gt;
adb shell cmd connectivity airplane-mode &lt;span class="nb"&gt;enable
sleep &lt;/span&gt;2
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Check app: does it show a retry state?"&lt;/span&gt;

adb shell cmd connectivity airplane-mode disable
&lt;span class="nb"&gt;sleep &lt;/span&gt;3
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"2. Revoke the app's network access permission"&lt;/span&gt;
adb shell pm revoke com.example.app android.permission.INTERNET
adb shell am force-stop com.example.app
adb shell pm grant com.example.app android.permission.INTERNET

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"3. Trigger background death with a fresh request"&lt;/span&gt;
adb shell am start &lt;span class="nt"&gt;-n&lt;/span&gt; com.example.app/.MainActivity
adb shell am &lt;span class="nb"&gt;kill &lt;/span&gt;com.example.app

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Record: which state does the app recover into?"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a comprehensive matrix. It is a focused first-pass check for the three failures junior engineers usually ship: no retry UI, silent data loss, and no state restoration after the OS kills the task. Each assertion should map to a visible behavior in the app, not to a log line.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Put in the PR Description
&lt;/h2&gt;

&lt;p&gt;Your PR's description becomes the evidence trail. Include the backend URL, the model call latency you observed, and a small table of lifecycle results.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Expected behavior&lt;/th&gt;
&lt;th&gt;Observed behavior&lt;/th&gt;
&lt;th&gt;Pass?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Airplane mode during request&lt;/td&gt;
&lt;td&gt;Retry prompt with backoff&lt;/td&gt;
&lt;td&gt;Retry prompt appeared&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission revoked, app restarted&lt;/td&gt;
&lt;td&gt;Empty state with re-auth&lt;/td&gt;
&lt;td&gt;Crashed on launch&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background kill&lt;/td&gt;
&lt;td&gt;Restore from local draft&lt;/td&gt;
&lt;td&gt;Draft was lost&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That table turns your PR from a code review into a decision point. Reviewers can check your tests instead of just reading the code. They also see that you used a real endpoint, so the failure modes they inspect are grounded in network reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Free Server Is the Wrong Tool
&lt;/h2&gt;

&lt;p&gt;Do not use MonkeyCode's free server for load testing, production traffic, or any workflow with strict uptime requirements. Free tiers may rate-limit requests, pause idle instances, or hold logs only briefly. If your feature handles protected health information or financial data, a free shared server is not compliant. The purpose here is learning, failing fast, and getting a first PR reviewed with confidence.&lt;/p&gt;

&lt;p&gt;You should also not use this setup to benchmark model latency across devices. The free model endpoint is subject to variable queue times and throttling. If you need hard numbers for a battery drain comparison, run your own on-device model or pay for a dedicated plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your First PR Becomes a Recovery Script
&lt;/h2&gt;

&lt;p&gt;The best side effect of a throwaway backend is that your first AI feature is born with a test script attached. When the API drops to 50% reliability three weeks later, you rerun &lt;code&gt;lifecycle-test.sh&lt;/code&gt; and immediately see which state regressed. Junior engineers often worry about writing the perfect feature first. The more valuable skill is building a small environment that makes failure obvious early. A free server and a free model API let you practice that skill on day one.&lt;/p&gt;

&lt;p&gt;Try it on your next AI PR. Point the app away from the mock, write the four-line lifecycle script, and commit the script next to your code. The reads from your reviewers will change because you are no longer asking them to trust your optimism. You are showing them what actually survives.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mobile</category>
      <category>testing</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Test the AI Reviewer Before It Tests You: A Free-Tier Shadow Review Setup</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Mon, 31 Aug 2026 11:44:44 +0000</pubDate>
      <link>https://dev.to/roronoa_/test-the-ai-reviewer-before-it-tests-you-a-free-tier-shadow-review-setup-3999</link>
      <guid>https://dev.to/roronoa_/test-the-ai-reviewer-before-it-tests-you-a-free-tier-shadow-review-setup-3999</guid>
      <description>&lt;p&gt;You just cloned the repository, opened the first PR, and a bot commented within forty seconds. It says your new endpoint logs user emails in plain text. That would be a real bug, but how do you know the bot is right? You could read the code and decide for yourself, but the deeper problem is that nobody on the team has ever verified the bot's advice. The AI reviewer is an untested dependency, and your first PR is the only place where you can safely test it — if you have the right free resources.&lt;/p&gt;

&lt;p&gt;That's where MonkeyCode's free model access and free server option become relevant. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I'm going to show you a shadow-review workflow that uses those free tools to fact-check an AI reviewer before you act on its feedback.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Failure Mode
&lt;/h2&gt;

&lt;p&gt;AI reviewers can hallucinate. They can suggest a fix that breaks the build, or flag a non-issue with alarming confidence. On a busy team, those comments often get resolved with a click, and the click is never scrutinized. As a junior engineer, you have a unique advantage: your first PR is small, low-stakes, and reversible. That makes it the perfect experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sandbox
&lt;/h2&gt;

&lt;p&gt;You need two things: a disposable server and enough model tokens to run a few prompts. A free server from MonkeyCode gives you a real network endpoint and a shell. Free model access lets you send your diff to the same family of models your team's reviewer might use. Together, they create a safe environment to measure the reviewer against reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Provision the Free Server
&lt;/h2&gt;

&lt;p&gt;Sign up, spin up a small instance, and SSH in. You only need one CPU core and 512 MB of memory for this test. The exact commands depend on your provider, so I'll keep it generic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh johndoe@your-free-server
node &lt;span class="nt"&gt;--version&lt;/span&gt;  &lt;span class="c"&gt;# confirm Node 18+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your server image doesn't have Node, install it with your package manager. You'll also need git.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add a Deliberately Buggy Route
&lt;/h2&gt;

&lt;p&gt;Create a tiny Express app that mimics a mobile backend. Here's a version with a subtle bug: it logs the entire request query, which includes a token your mobile app sent as a query parameter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/v1/user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Query:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Bug: logs token too&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alice@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3099&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Listening on 3099&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit that, push it to a branch, and open a pull request against your sandbox repository. That gives you a real diff.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git init sandbox-review &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;sandbox-review
npm init &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install &lt;/span&gt;express
&lt;span class="c"&gt;# add the server.js file&lt;/span&gt;
git commit &lt;span class="nt"&gt;-am&lt;/span&gt; &lt;span class="s2"&gt;"Add user endpoint"&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Ask the AI Reviewer
&lt;/h2&gt;

&lt;p&gt;Now extract the diff and send it to your AI reviewer. If your team uses a bot, see if there's a CLI or a way to call the same model directly. With MonkeyCode's free model access, you can run a prompt that includes the diff and explicitly asks for a review. The invocation will vary; here is the shape of the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff main..feature-branch &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; pr.diff
monkeycode review &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;pr.diff&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--free&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Adjust the actual flags to your platform. The goal is to get the reviewer's comments in a file that you can compare against the tests.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Run the Test Suite
&lt;/h2&gt;

&lt;p&gt;Have a test that asserts the token never appears in the logs. For example, use Node's built-in test runner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:assert&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;spawn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token is not logged&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;server.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="c1"&gt;// ... start server, make request with token, capture stdout&lt;/span&gt;
  &lt;span class="c1"&gt;// then assert that stdout does not contain the token string&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the test fails, the reviewer was right. But here's the second check: ask the reviewer for a suggested fix. Sometimes the fix is worse than the bug. Apply it and see if the test passes and nothing else breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Rehearse Rollback
&lt;/h2&gt;

&lt;p&gt;What if the AI reviewer gives you a confident but wrong diagnosis? For instance, it might tell you to move the token to a POST body, which is fine, but also to disable logging globally. That could hide other issues. If you have to revert, do it now, in this sandbox. Delete the branch and redeploy the previous commit. You'll practice the same rollback command you'd need in production, but without paging anyone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git revert HEAD &lt;span class="nt"&gt;--no-edit&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seeing that command work on a free server once makes it a lot less scary when you have to do it for real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;This shadow-review test only covers a single diff and a single model. It does not prove that all AI reviewers are unreliable, nor does it guarantee the same findings will occur with your team's specific prompt and system instructions. It is also not a substitute for reading the code yourself. The goal is to build a habit of verification, not to automate trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Should Not Use This
&lt;/h2&gt;

&lt;p&gt;If you are an experienced engineer who already knows the codebase cold, you probably don't need a shadow review for a small diff. And if your team's AI reviewer has been consistently accurate for weeks, the extra setup may not be worth the time. But if you are new, or if the AI reviewer has ever given you a suspicious comment, this free-tier drill is a low-cost way to learn how much of its feedback you can actually trust.&lt;/p&gt;

&lt;p&gt;Give it a try on your next PR. You'll get review practice, a rollback rehearsal, and a concrete answer about whether that bot knows what it's talking about.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>productivity</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Your AI Feature's First PR Needs a Lifecycle Test, Not Just a Success Path</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Sun, 30 Aug 2026 10:53:01 +0000</pubDate>
      <link>https://dev.to/roronoa_/your-ai-features-first-pr-needs-a-lifecycle-test-not-just-a-success-path-1ebc</link>
      <guid>https://dev.to/roronoa_/your-ai-features-first-pr-needs-a-lifecycle-test-not-just-a-success-path-1ebc</guid>
      <description>&lt;p&gt;You just got added to the repo. Your first task is to add an AI-powered image description button to the Android app. You plug in an endpoint, write a fetch, and it returns a sentence in the simulator. You open a PR. Then a senior engineer says, "What happens when the user switches Wi-Fi to LTE mid-request?" The success path is the easiest part. The failure path is where your work actually gets judged.&lt;/p&gt;

&lt;p&gt;This article is a hands-on drill for that exact situation. You will build a small end-to-end AI feature using a free backend, then run it through a scripted mobile lifecycle test: backgrounding, network loss, app kill, and permission revoke. By the end, your first PR will include evidence that the feature survives reality, not just the simulator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Success Path Lies
&lt;/h2&gt;

&lt;p&gt;A simulator gives you a stable network, a foreground activity, and no competition for system resources. Real users have airplane mode, app switchers, OS updates, and permission dialogs they accidentally tap wrong. Your AI feature might work perfectly in the demo and then silently fail when the request is interrupted. The fix is not to add more code blindly; it is to instrument the behavior and see where the request actually goes when the lifecycle changes.&lt;/p&gt;

&lt;p&gt;For a junior engineer, this is also a way to build trust. A PR that includes a test script and a table of observed outcomes is easier to review than one that says "works on my machine." You are not just implementing a feature; you are proving it can fail safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Free-Tier Setup That Makes This Drill Cheap
&lt;/h2&gt;

&lt;p&gt;You do not need a corporate API account to run this experiment. MonkeyCode offers free model access and a free server option, which is enough for a small proxy and a handful of requests. That means you can practice the full flow without spending money or waiting for a manager to approve a vendor.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;The free server is useful because it gives you a stable host for your proxy and lets you log every request that arrives from the phone. Without that log, you cannot tell whether the failure is on the device or in the network. With a simple proxy, you get a timestamped trace for each attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Reproducible Lifecycle Test Harness
&lt;/h2&gt;

&lt;p&gt;Let's build a minimal stack. On the server side, run a tiny Express app. Replace &lt;code&gt;MODEL_API_URL&lt;/code&gt; with the actual model endpoint you get from your free tier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server.js - run on your MonkeyCode free server&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/describe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[AI_REQ] start &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;started&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modelResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MODEL_API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;modelResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[AI_REQ] end &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[AI_REQ] error &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;model unavailable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the Android side, use a simple React Native screen with a button that calls the proxy. The exact UI does not matter; what matters is that you add a unique &lt;code&gt;AI_REQ&lt;/code&gt; log line on the device too, so you can compare device events with server logs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestDescription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[AI_REQ] device start &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-free-server.example.dev/describe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[AI_REQ] device end &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[AI_REQ] device error &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have two log streams. The server log tells you whether the request arrived. The device log tells you what the UI saw. When they disagree, you have found your bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The adb Drill
&lt;/h2&gt;

&lt;p&gt;The real value comes from forcing state changes while a request is in flight. The easiest way to do this is with &lt;code&gt;adb&lt;/code&gt; on a physical Android device. Here is a script you can save as &lt;code&gt;lifecycle-drill.sh&lt;/code&gt; and run from your terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;PACKAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"com.example.ai.feature"&lt;/span&gt;

launch&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  adb shell am start &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$PACKAGE&lt;/span&gt;/.MainActivity
  &lt;span class="nb"&gt;sleep &lt;/span&gt;2
  adb shell input tap 500 1000  &lt;span class="c"&gt;# tap the fetch button&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

launch
&lt;span class="nb"&gt;sleep &lt;/span&gt;3
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Baseline done. Check logs."&lt;/span&gt;

&lt;span class="c"&gt;# Background / foreground&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"--- Background / Foreground ---"&lt;/span&gt;
adb shell input keyevent KEYCODE_HOME
&lt;span class="nb"&gt;sleep &lt;/span&gt;10
adb shell am start &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$PACKAGE&lt;/span&gt;/.MainActivity
&lt;span class="nb"&gt;sleep &lt;/span&gt;2
adb shell input tap 500 1000
&lt;span class="nb"&gt;sleep &lt;/span&gt;3

&lt;span class="c"&gt;# Network loss mid-request&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"--- Network Loss ---"&lt;/span&gt;
adb shell svc wifi disable
adb shell svc data disable
adb shell input tap 500 1000
&lt;span class="nb"&gt;sleep &lt;/span&gt;5
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Re-enabling network..."&lt;/span&gt;
adb shell svc wifi &lt;span class="nb"&gt;enable
&lt;/span&gt;adb shell svc data &lt;span class="nb"&gt;enable
sleep &lt;/span&gt;5

&lt;span class="c"&gt;# App kill&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"--- App Kill ---"&lt;/span&gt;
adb shell am &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nv"&gt;$PACKAGE&lt;/span&gt;
adb shell am start &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$PACKAGE&lt;/span&gt;/.MainActivity
&lt;span class="nb"&gt;sleep &lt;/span&gt;2
adb shell input tap 500 1000
&lt;span class="nb"&gt;sleep &lt;/span&gt;3

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"--- Collecting logs ---"&lt;/span&gt;
adb logcat &lt;span class="nt"&gt;-d&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"AI_REQ"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script does the same tap after each state change, which creates a fresh request. Your job is to watch how the app behaves: Does it show a spinner forever? Does it crash? Does it retry when the network comes back? The device log will tell you if the request was even attempted, and the server log will tell you if it ever arrived.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Record and Why
&lt;/h2&gt;

&lt;p&gt;You need a table, not just a vague impression. For each condition, record the device model, Android version, network state, and exactly what the user sees. Then classify the recovery outcome into one of three buckets: &lt;strong&gt;recovered&lt;/strong&gt; (the feature eventually succeeded after the condition cleared), &lt;strong&gt;restarted&lt;/strong&gt; (the user had to trigger the action again), or &lt;strong&gt;silently disappeared&lt;/strong&gt; (no error, no response, nothing).&lt;/p&gt;

&lt;p&gt;Here is a decision table you can start with:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Expected&lt;/th&gt;
&lt;th&gt;Actual&lt;/th&gt;
&lt;th&gt;Recovery&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;Response in &amp;lt;5s&lt;/td&gt;
&lt;td&gt;fill in&lt;/td&gt;
&lt;td&gt;recovered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background 10s&lt;/td&gt;
&lt;td&gt;Response still returns&lt;/td&gt;
&lt;td&gt;fill in&lt;/td&gt;
&lt;td&gt;recovered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network off mid-request&lt;/td&gt;
&lt;td&gt;Timeout or error shown&lt;/td&gt;
&lt;td&gt;fill in&lt;/td&gt;
&lt;td&gt;restarted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App killed&lt;/td&gt;
&lt;td&gt;No crash on relaunch&lt;/td&gt;
&lt;td&gt;fill in&lt;/td&gt;
&lt;td&gt;restarted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission revoked&lt;/td&gt;
&lt;td&gt;Clear error message&lt;/td&gt;
&lt;td&gt;fill in&lt;/td&gt;
&lt;td&gt;restarted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fill it in as you run the drill. The empty cells are the point: you are generating evidence, not assumptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and Who Should Skip This
&lt;/h2&gt;

&lt;p&gt;This drill has limits. It uses one device and one network emulation, so it will not prove anything about carrier-grade radio behavior or server load. A Wi-Fi switch on &lt;code&gt;adb&lt;/code&gt; is not the same as a real cellular handover. Also, the free tier is not for production latency testing; if you need sub-second responses at scale, you need real instrumentation and a serious budget.&lt;/p&gt;

&lt;p&gt;Do not use this approach if you are already past the proof-of-concept stage and are optimizing for p99 latency. This drill is for the first PR, when you are still validating that the architecture can handle a feature that falls down and gets back up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your First PR Can Prove Resilience
&lt;/h2&gt;

&lt;p&gt;A junior engineer's first PR does not have to be a perfect implementation. It can be a testable implementation. With a free server and free model access from MonkeyCode, you can run this lifecycle drill on your own time and attach the results to your PR description. That turns a scary cold-start into a concrete, verifiable story: the request failed, the app recovered, and here is the timestamped proof.&lt;/p&gt;

&lt;p&gt;Try this drill once, and answer honestly: what did your app do when the network came back?&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>ai</category>
      <category>testing</category>
      <category>android</category>
    </item>
    <item>
      <title>Your Fallback Copy Is a Guess Until You Shadow-Test It Against Free Models</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Sat, 29 Aug 2026 09:44:10 +0000</pubDate>
      <link>https://dev.to/roronoa_/your-fallback-copy-is-a-guess-until-you-shadow-test-it-against-free-models-262j</link>
      <guid>https://dev.to/roronoa_/your-fallback-copy-is-a-guess-until-you-shadow-test-it-against-free-models-262j</guid>
      <description>&lt;p&gt;You work on a habit-tracker app. A user denies the notification permission. Your app shows one line of explanation. That line is a static string today. A model could write a better sentence. But every API call costs money on the phone. So you built a fallback path. Your fallback triggers when the network fails. Your tests pass on Wi-Fi. Your simulator shows the fallback. The reviewer approves. Real users see your guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fallback assumption that quietly breaks
&lt;/h2&gt;

&lt;p&gt;Your fallback only triggers on network errors. Most mobile AI clients are written that way. Real mobile users fail differently. The OS suspends your app mid-request. The user revokes a permission while the prompt is in flight. The endpoint returns 429 when quota runs out. The phone goes offline between retry and response.&lt;/p&gt;

&lt;p&gt;None of this appears in a happy-path demo. All of it appears in production support tickets. Shadow testing is how you find it before users do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a shadow test is
&lt;/h2&gt;

&lt;p&gt;Shadow mode runs a model in parallel with real product flows. It never renders the model output to the user. The product path still shows your deterministic fallback. The shadow path records what the model would have said, how long it took, and how many tokens it consumed. You compare those records later in a log file. The fallback keeps running until the model earns trust.&lt;/p&gt;

&lt;p&gt;Free models and a free server from MonkeyCode support this exact pattern: one endpoint for the model output, one place for the logs. &lt;em&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Use the trial allowance as a staging budget, not a production SLA. Keep your production client pointed at your own endpoint. The shadow runner alone points at the free endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  A shadow runner you can copy
&lt;/h2&gt;

&lt;p&gt;This script sends one real prompt through an OpenAI-compatible endpoint, records the measurements, and compares the output against your fallback string. It captures the exact conditions your fallback logic cares about: HTTP status, latency, token usage, and output length. No model output is written back to the app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// shadow-runner.mjs — one prompt, one free model, one trace&lt;/span&gt;
&lt;span class="c1"&gt;// Usage:&lt;/span&gt;
&lt;span class="c1"&gt;//   SHADOW_BASE_URL=https://... \&lt;/span&gt;
&lt;span class="c1"&gt;//   SHADOW_API_KEY=test-key \&lt;/span&gt;
&lt;span class="c1"&gt;//   SHADOW_MODEL=free-model \&lt;/span&gt;
&lt;span class="c1"&gt;//   node shadow-runner.mjs ./prompt.txt&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./prompt.txt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;We need notifications to remind you about your streaks.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SHADOW_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/chat/completions`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SHADOW_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SHADOW_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;latencyMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modelText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fallbackWins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;modelText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;httpStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;latencyMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;promptTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;prompt_tokens&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;completionTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;completion_tokens&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;modelText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;modelText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;280&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;fallbackWins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node 18 or newer is enough. Export the three environment variables from a local shell, never from the app binary. The prompt file should contain only synthetic or redacted copy during experiments.&lt;/p&gt;

&lt;p&gt;A sample trace looks like this. It shows the shape of the record, not a benchmark result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"httpStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"latencyMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1841&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"promptTokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"completionTokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"modelText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"We need notifications to remind you about your streaks. Want to keep them?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fallbackWins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fallbackWins&lt;/code&gt; is a length heuristic, not a quality judgment. A long hallucination can still win under this rule. Use the trace to decide which scenarios deserve a human review pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure drill for your fallback
&lt;/h2&gt;

&lt;p&gt;The runner alone proves the endpoint works. It does not prove the lifecycle survives. Run the same request through these six scenarios on a real device and log each result.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wi-Fi baseline.&lt;/strong&gt; Run the runner three times and record the latency spread. This becomes your timeout budget reference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airplane mode mid-flight.&lt;/strong&gt; Start the request, toggle airplane mode before it settles. Check whether the fallback renders or the spinner hangs forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background kill.&lt;/strong&gt; Start the request, background the app, force-quit it, and relaunch. Inspect the retry queue state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission revoke.&lt;/strong&gt; Revoke notification permission while the request is in flight. The feature must disable itself and never send a blank prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Endpoint failure.&lt;/strong&gt; Redirect &lt;code&gt;SHADOW_BASE_URL&lt;/code&gt; to a stub that returns 429. Confirm the fallback triggers and the retry loop backs off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry storm.&lt;/strong&gt; Fail the endpoint five consecutive times. Measure battery impact and data usage from the platform energy monitor.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each scenario maps to one log entry. Each log entry either confirms your fallback design or contradicts it. That contradiction is the review comment, not a suspicion.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the fallback should win
&lt;/h2&gt;

&lt;p&gt;Use this decision table when you read the traces. It keeps the judgment consistent across devices and reviewers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Trust model output&lt;/th&gt;
&lt;th&gt;Use fallback&lt;/th&gt;
&lt;th&gt;Trace signal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 200, latency under 2s&lt;/td&gt;
&lt;td&gt;Yes, in staging only&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fallbackWins: false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 429 or 5xx&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fallbackWins: true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request timeout&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;AbortError in the trace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background kill&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes, on next launch&lt;/td&gt;
&lt;td&gt;retry queue state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission revoked&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes, feature disabled&lt;/td&gt;
&lt;td&gt;no prompt sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output shorter than 10 chars&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fallbackWins: true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consistent output across 5 runs&lt;/td&gt;
&lt;td&gt;Yes, with versioned prompt&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;identical &lt;code&gt;modelText&lt;/code&gt; shape&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table is not a replacement for product judgment. It prevents the most common mistake: trusting the model in exactly the cases where it fails hardest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should skip this workflow
&lt;/h2&gt;

&lt;p&gt;Shadow testing assumes your feature already has a deterministic fallback and a network path to a model. That premise does not fit everyone.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On-device model teams.&lt;/strong&gt; There is no endpoint quota and no network failure to simulate. You need an offline evaluation set instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content-critical surfaces.&lt;/strong&gt; Medical, legal, or safety copy should not be shadow-tested into production. Use a human review loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production 24/7 services.&lt;/strong&gt; The free staging environment is a test budget, not a high-availability tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teams with strict data residency.&lt;/strong&gt; A shadow run from a developer laptop may route prompts outside your control region. Verify the endpoint location first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also keep the shadow endpoint away from real user messages. Synthetic prompts and redacted copy keep the experiment honest and the privacy review short.&lt;/p&gt;

&lt;h2&gt;
  
  
  The next time you write a fallback
&lt;/h2&gt;

&lt;p&gt;Your fallback is a product decision, not a placeholder. It deserves the same evidence as a screen design or a network timeout. Free models make that evidence cheap. A free server gives you one place to collect it.&lt;/p&gt;

&lt;p&gt;The next time your PR description says "added a fallback," add one more line: &lt;code&gt;Shadow-tested against free models in staging.&lt;/code&gt; Your reviewer may still ask questions. A trace answers them in milliseconds.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mobile</category>
      <category>testing</category>
      <category>llm</category>
    </item>
    <item>
      <title>Keep Your First Rollback Out of Production: A Free-Tier Drill for Junior Mobile Engineers</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Fri, 28 Aug 2026 03:17:57 +0000</pubDate>
      <link>https://dev.to/roronoa_/keep-your-first-rollback-out-of-production-a-free-tier-drill-for-junior-mobile-engineers-4854</link>
      <guid>https://dev.to/roronoa_/keep-your-first-rollback-out-of-production-a-free-tier-drill-for-junior-mobile-engineers-4854</guid>
      <description>&lt;p&gt;It is your third day on a mobile team, and your first PR just merged into the main branch. The change adds a required header to every API request, and the staging server starts returning 502s within minutes. Your onboarding doc has a rollback section, but you have never rolled back anything in your life, and production is one deploy away.&lt;/p&gt;

&lt;p&gt;This article turns that moment into a drill you can run today for free. You will use MonkeyCode's free model access and free server option to practice a complete deploy-and-rollback loop before production ever sees your mistake. Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;Rollback is a mobile skill, not just an ops skill, because your users do not update on your schedule. Old app versions keep calling old endpoints for weeks, and an app store review can take days while a server rollback takes minutes. The cheapest place to learn that muscle is a throwaway environment with free compute and free model help, not your team's production cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Free Tier Is the Right Training Ground
&lt;/h2&gt;

&lt;p&gt;A junior engineer's first rollback usually happens under pressure, with a Slack channel watching and a manager asking for a timeline. That is the worst possible condition for learning a mechanical skill. A free server gives you the same commands with none of the audience, and free model access gives you a patient reviewer that will explain the deploy pipeline line by line.&lt;/p&gt;

&lt;p&gt;MonkeyCode is an open-source project that provides both pieces for this drill. The free tier includes a token allowance for model access — 10 million tokens at the time of writing — plus a free server option you can use as a staging sandbox. Check the current documentation before you rely on either, because quotas and capacity change over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Drill: First Hour, First PR, First Rollback
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Hour One: Map the Deploy Path
&lt;/h3&gt;

&lt;p&gt;Clone the repository and use the free model access to summarize what actually happens after you push. Run these commands first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/your-team/your-app.git
&lt;span class="nb"&gt;cd &lt;/span&gt;your-app
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; .github/workflows/deploy.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask the model a focused question: "Where is the rollback script, and what runs after tests pass?" Save the answer as a local runbook named &lt;code&gt;rollback.md&lt;/code&gt;. You are not looking for perfect understanding; you are looking for the one command that redeploys the previous commit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your First PR: Make It Reversible
&lt;/h3&gt;

&lt;p&gt;Keep the diff small and the escape hatch visible. Add a feature flag instead of hard-coding the new header, and write a one-line rollback note in the PR description. A reviewer should be able to tell from the description alone how to undo your change without reading the diff.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Rollback Drill
&lt;/h3&gt;

&lt;p&gt;Now run the loop on the free server. Deploy a deliberately broken commit, confirm the failure, revert, redeploy, and verify recovery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# simulate a bad release on the free server&lt;/span&gt;
git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; drill/bad-header
&lt;span class="c"&gt;# introduce a breaking change, then commit and push&lt;/span&gt;
git commit &lt;span class="nt"&gt;-am&lt;/span&gt; &lt;span class="s2"&gt;"drill: require a header that old clients do not send"&lt;/span&gt;
git push origin drill/bad-header
&lt;span class="c"&gt;# deploy, then observe the failure&lt;/span&gt;
curl &lt;span class="nt"&gt;-i&lt;/span&gt; https://your-free-server.example/health

&lt;span class="c"&gt;# rollback&lt;/span&gt;
git revert HEAD &lt;span class="nt"&gt;--no-edit&lt;/span&gt;
git push origin drill/bad-header
&lt;span class="c"&gt;# redeploy, then confirm recovery&lt;/span&gt;
curl &lt;span class="nt"&gt;-i&lt;/span&gt; https://your-free-server.example/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Time each step. If the whole loop takes more than fifteen minutes, your rollback is too slow, and you have found a real problem before it mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll Back or Fix Forward: A Decision Table
&lt;/h2&gt;

&lt;p&gt;Not every incident deserves a rollback, and the table below is the rule of thumb I use:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Breaking change for old app clients&lt;/td&gt;
&lt;td&gt;Roll back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data migration already ran&lt;/td&gt;
&lt;td&gt;Fix forward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hotfix window under 15 minutes&lt;/td&gt;
&lt;td&gt;Roll back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security fix&lt;/td&gt;
&lt;td&gt;Fix forward, then patch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free server drill&lt;/td&gt;
&lt;td&gt;Roll back, always&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mobile-specific row is the first one. When old clients break, a fix forward still leaves users stranded until they update, while a rollback restores service immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and Who Should Skip This
&lt;/h2&gt;

&lt;p&gt;The free server is not production, and you should treat it accordingly. It has no SLA, no guaranteed persistent storage, and shared capacity that other users can affect. Do not practice destructive migrations there, and do not store real user data in the sandbox. The token allowance is a training budget, not a production plan, so keep your prompts short and your runbooks local.&lt;/p&gt;

&lt;p&gt;Skip this workflow if your team needs compliance controls, reproducible load testing, or a staging environment that mirrors production exactly. For those cases, a free tier is the wrong tool, and your team's paid infrastructure is the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bring Back Evidence
&lt;/h2&gt;

&lt;p&gt;If you run the drill, record the device, OS, framework versions, and the exact failure you observed. That evidence is worth more than another tutorial, and it is the difference between practicing a skill and performing it.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>ai</category>
      <category>devops</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
