diff --git a/mobile/modules/crust/android/src/main/java/com/mentra/crust/jsc/JSCRuntime.kt b/mobile/modules/crust/android/src/main/java/com/mentra/crust/jsc/JSCRuntime.kt index 5823aee177..756842742b 100644 --- a/mobile/modules/crust/android/src/main/java/com/mentra/crust/jsc/JSCRuntime.kt +++ b/mobile/modules/crust/android/src/main/java/com/mentra/crust/jsc/JSCRuntime.kt @@ -459,15 +459,15 @@ class JSCRuntime private constructor(private val appContext: Context) { */ fun dispatchToJs(packageName: String, envelopeJson: String) { val record = contexts[packageName] ?: return - // Steady-state NACK — re-arm so a wedged QuickJS context surfaces - // an __error/ready_nack frame after 3s instead of silently - // swallowing the delivery. Skipped during cold-start (timer still - // ticking from spawn). - if (record.readyAcked) { - armReadyNackTimer(record, STEADY_STATE_NACK_TIMEOUT_MS, cold = false) - } try { record.executor.submit { + // Steady-state NACK — re-arm inside the per-context executor so + // the timer measures the actual __deliver call, not time spent + // waiting behind earlier queued work. Skipped during cold-start + // (timer still ticking from spawn). + if (record.readyAcked) { + armReadyNackTimer(record, STEADY_STATE_NACK_TIMEOUT_MS, cold = false) + } // Soft watchdog around evaluate: warn at 5s, kill at 30s. val warn = timerScheduler.schedule({ Log.i(TAG, "watchdog: $packageName __deliver blocked >${WATCHDOG_WARN_MS}ms") diff --git a/mobile/modules/island/src/services/MentraJSRouter.ts b/mobile/modules/island/src/services/MentraJSRouter.ts index 9eea6c6315..e2fa6a9854 100644 --- a/mobile/modules/island/src/services/MentraJSRouter.ts +++ b/mobile/modules/island/src/services/MentraJSRouter.ts @@ -466,7 +466,11 @@ export class MentraJSRouter { // dead and let the host surface a "tap to retry" banner. if (iface === "__error") { const payload = this.tryParseArgs(msg.argsJson) - this.logger.error(`[${packageName}] ${method}`, payload) + if (method === "ready_nack") { + this.logger.warn(`[${packageName}] ${method}`, payload) + } else { + this.logger.error(`[${packageName}] ${method}`, payload) + } if (this.crashController) { // Only treat "exception" + "unhandledRejection" + "uncaught" as // crash signals. `console.error` calls also flow through the diff --git a/mobile/modules/island/src/services/__tests__/MentraJSRouter.test.ts b/mobile/modules/island/src/services/__tests__/MentraJSRouter.test.ts index e4b2eda51e..12b67d5b9a 100644 --- a/mobile/modules/island/src/services/__tests__/MentraJSRouter.test.ts +++ b/mobile/modules/island/src/services/__tests__/MentraJSRouter.test.ts @@ -252,6 +252,18 @@ describe("MentraJSRouter", () => { expect(logger.error).toHaveBeenCalledTimes(1) }) + test("ready_nack logs as warning instead of dev-overlay error", () => { + router.start() + crust.emit("mentrajs_message", { + packageName: "com.foo", + iface: "__error", + method: "ready_nack", + argsJson: '{"phase":"steady-state","timeoutMs":3000}', + }) + expect(logger.warn).toHaveBeenCalledTimes(1) + expect(logger.error).not.toHaveBeenCalled() + }) + test("unknown iface logs a debug line but does NOT crash", () => { router.start() crust.emit("mentrajs_message", {