@@ -34,6 +34,12 @@ class RunForegroundService : Service() {
34 val sessionTitle = intent?.getStringExtra(EXTRA_SESSION_TITLE)?.takeIf { it.isNotBlank() }
34 val sessionTitle = intent?.getStringExtra(EXTRA_SESSION_TITLE)?.takeIf { it.isNotBlank() }
35 val sessionId = intent?.getStringExtra(EXTRA_SESSION_ID)?.takeIf { it.isNotBlank() }
35 val sessionId = intent?.getStringExtra(EXTRA_SESSION_ID)?.takeIf { it.isNotBlank() }
36 val progress = intent?.getStringExtra(EXTRA_PROGRESS)?.takeIf { it.isNotBlank() }
36 val progress = intent?.getStringExtra(EXTRA_PROGRESS)?.takeIf { it.isNotBlank() }
37+ // The service is re-started per progress update (AppContainer re-sends the intent on
38+ // each _runProgressText change). Capture the wall-clock start of the *first* intent
39+ // of this run and reuse it across rebuilds so the chronometer's base doesn't jump
40+ // back to 0:00 every time the agent advances a step. Reset to 0 in onDestroy once
41+ // the run ends (stop() → onDestroy) so the next run starts fresh.
42+ if (runStartMillis == 0L) runStartMillis = System.currentTimeMillis()
37 val notification = buildNotification(sessionTitle, sessionId, progress)
43 val notification = buildNotification(sessionTitle, sessionId, progress)
38 // startForeground can throw ForegroundServiceStartNotAllowedException on
44 // startForeground can throw ForegroundServiceStartNotAllowedException on
39 // Android 12+ if the app is in the background when the service starts. Wrap
45 // Android 12+ if the app is in the background when the service starts. Wrap
@@ -81,7 +87,9 @@ class RunForegroundService : Service() {
81 .setOngoing(true)
87 .setOngoing(true)
82 // Show a live elapsed timer so the user can see how long the current run has
88 // Show a live elapsed timer so the user can see how long the current run has
83 // been going — no per-tick plumbing needed; the system renders the chronometer.
89 // been going — no per-tick plumbing needed; the system renders the chronometer.
84− .setWhen(System.currentTimeMillis())
90+ // runStartMillis is captured once at the start of the run (see onStartCommand)
91+ // and reused across progress-driven rebuilds so the chronometer base is stable.
92+ .setWhen(runStartMillis)
85 .setUsesChronometer(true)
93 .setUsesChronometer(true)
86 .setShowWhen(true)
94 .setShowWhen(true)
87 .setPriority(NotificationCompat.PRIORITY_LOW)
95 .setPriority(NotificationCompat.PRIORITY_LOW)
@@ -150,6 +158,9 @@ class RunForegroundService : Service() {
150 // Safety net: if the process is being killed while a run is in progress,
158 // Safety net: if the process is being killed while a run is in progress,
151 // ensure the notification is removed rather than lingering.
159 // ensure the notification is removed rather than lingering.
152 ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
160 ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
161+ // Reset the run start so the next run's chronometer begins fresh instead of
162+ // inheriting the previous run's elapsed base.
153 super.onDestroy()
164 super.onDestroy()
@@ -159,6 +170,10 @@ class RunForegroundService : Service() {
159 private const val TAG = "RunForegroundService"
170 private const val TAG = "RunForegroundService"
160 private const val NOTIF_ID = 1
171 private const val NOTIF_ID = 1
161 private const val NOTIF_TIMEOUT_ID = 2
172 private const val NOTIF_TIMEOUT_ID = 2
173+ // Wall-clock time the current run started, captured on the first onStartCommand of
174+ // the run and reused across progress-driven notification rebuilds so the chronometer
175+ // base is stable. Reset to 0 in onDestroy when the run ends.
176+ @Volatile private var runStartMillis: Long = 0L
162 // Brand accent for the foreground notifications' small-icon tint circle. Resolved
177 // Brand accent for the foreground notifications' small-icon tint circle. Resolved
163 // from resources (R.color.notif_brand) so it follows the app theme (light/dark)
178 // from resources (R.color.notif_brand) so it follows the app theme (light/dark)
164 // instead of a hardcoded constant. Lazily computed on first use from the app context.
179 // instead of a hardcoded constant. Lazily computed on first use from the app context.