@@ -38,6 +38,7 @@ import com.zeroclaw.ffi.FfiException
3838import com.zeroclaw.ffi.validateConfig
3939import kotlinx.coroutines.CoroutineDispatcher
4040import kotlinx.coroutines.CoroutineScope
41+ import kotlinx.coroutines.CancellationException
4142import kotlinx.coroutines.Dispatchers
4243import kotlinx.coroutines.Job
4344import kotlinx.coroutines.SupervisorJob
@@ -219,49 +220,57 @@ class ZeroClawDaemonService : Service() {
219220 acquireWakeLock()
220221
221222 serviceScope.launch(ioDispatcher) {
222- val settings = settingsRepository.settings.first()
223- val effectiveSettings = resolveEffectiveDefaults(settings)
224- val apiKey = apiKeyRepository.getByProviderFresh(effectiveSettings.defaultProvider)
225-
226- val globalConfig =
227- buildGlobalTomlConfig(effectiveSettings, apiKey)
228- val baseToml = ConfigTomlBuilder .build(globalConfig)
229- val channelsToml =
230- ConfigTomlBuilder .buildChannelsToml(
231- channelConfigRepository.getEnabledWithSecrets(),
232- )
233- val agentsToml = buildAgentsToml()
234- val configToml = baseToml + channelsToml + agentsToml
235-
236- if (! validateConfigOrStop(configToml)) return @launch
237-
238- val conflict = bridge.detectMemoryConflict(effectiveSettings.memoryBackend)
239- if (conflict is MemoryConflict .StaleData ) {
240- val shouldDelete = bridge.awaitConflictResolution(conflict)
241- if (shouldDelete) {
242- bridge.cleanupStaleMemory(conflict)
243- logRepository.append(
244- LogSeverity .INFO ,
245- TAG ,
246- " Cleaned up ${conflict.staleFileCount} stale " +
247- " ${conflict.staleBackend} memory files" ,
223+ try {
224+ val settings = settingsRepository.settings.first()
225+ val effectiveSettings = resolveEffectiveDefaults(settings)
226+ val apiKey = apiKeyRepository.getByProviderFresh(effectiveSettings.defaultProvider)
227+
228+ val globalConfig =
229+ buildGlobalTomlConfig(effectiveSettings, apiKey)
230+ val baseToml = ConfigTomlBuilder .build(globalConfig)
231+ val channelsToml =
232+ ConfigTomlBuilder .buildChannelsToml(
233+ channelConfigRepository.getEnabledWithSecrets(),
248234 )
249- }
250- }
235+ val agentsToml = buildAgentsToml()
236+ val configToml = baseToml + channelsToml + agentsToml
251237
252- retryPolicy.reset()
253- val validPort =
254- if (settings.port in VALID_PORT_RANGE ) {
255- settings.port
256- } else {
257- AppSettings .DEFAULT_PORT
238+ if (! validateConfigOrStop(configToml)) return @launch
239+
240+ val conflict = bridge.detectMemoryConflict(effectiveSettings.memoryBackend)
241+ if (conflict is MemoryConflict .StaleData ) {
242+ val shouldDelete = bridge.awaitConflictResolution(conflict)
243+ if (shouldDelete) {
244+ bridge.cleanupStaleMemory(conflict)
245+ logRepository.append(
246+ LogSeverity .INFO ,
247+ TAG ,
248+ " Cleaned up ${conflict.staleFileCount} stale " +
249+ " ${conflict.staleBackend} memory files" ,
250+ )
251+ }
258252 }
259- attemptStart(
260- configToml = configToml,
261- host = settings.host,
262- port = validPort.toUShort(),
263- memoryBackend = effectiveSettings.memoryBackend,
264- )
253+
254+ retryPolicy.reset()
255+ val validPort =
256+ if (settings.port in VALID_PORT_RANGE ) {
257+ settings.port
258+ } else {
259+ AppSettings .DEFAULT_PORT
260+ }
261+ attemptStart(
262+ configToml = configToml,
263+ host = settings.host,
264+ port = validPort.toUShort(),
265+ memoryBackend = effectiveSettings.memoryBackend,
266+ )
267+ } catch (e: CancellationException ) {
268+ throw e
269+ } catch (e: Throwable ) {
270+ handleUnexpectedStartupError(
271+ LogSanitizer .sanitizeLogMessage(e.message ? : e.javaClass.simpleName),
272+ )
273+ }
265274 }
266275 }
267276
@@ -661,6 +670,15 @@ class ZeroClawDaemonService : Service() {
661670 handleStartupExhausted(errorMsg)
662671 return @launch
663672 }
673+ } catch (e: CancellationException ) {
674+ throw e
675+ } catch (e: Throwable ) {
676+ handleStartupExhausted(
677+ LogSanitizer .sanitizeLogMessage(
678+ e.message ? : e.javaClass.simpleName,
679+ ),
680+ )
681+ return @launch
664682 }
665683 }
666684 } finally {
@@ -696,6 +714,22 @@ class ZeroClawDaemonService : Service() {
696714 stopSelf()
697715 }
698716
717+ /* *
718+ * Handles unexpected startup errors thrown before retry loop starts.
719+ *
720+ * This prevents fatal coroutine crashes from taking down the app process
721+ * when non-FFI runtime exceptions occur while preparing daemon config.
722+ */
723+ private fun handleUnexpectedStartupError (errorMsg : String ) {
724+ Log .e(TAG , " Daemon startup aborted by unexpected error: $errorMsg " )
725+ logRepository.append(LogSeverity .ERROR , TAG , " Startup aborted: $errorMsg " )
726+ activityRepository.record(ActivityType .DAEMON_ERROR , " Startup aborted: $errorMsg " )
727+ notificationManager.updateNotification(ServiceState .ERROR , errorDetail = errorMsg)
728+ releaseWakeLock()
729+ stopForeground(STOP_FOREGROUND_DETACH )
730+ stopSelf()
731+ }
732+
699733 /* *
700734 * Starts a coroutine that periodically polls the daemon for status.
701735 *
0 commit comments