npx claudepluginhub himattm/skills --plugin androidThis skill uses the workspace's default tool permissions.
- "I called `launch { ... collect ... }` but no emissions arrive" — likely a job stuck in suspension
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Guides code writing, review, and refactoring with Karpathy-inspired rules to avoid overcomplication, ensure simplicity, surgical changes, and verifiable success criteria.
Executes ctx7 CLI to fetch up-to-date library documentation, manage AI coding skills (install/search/generate/remove/suggest), and configure Context7 MCP. Useful for current API refs, skill handling, or agent setup.
Share bugs, ideas, or general feedback.
launch { ... collect ... } but no emissions arrive" — likely a job stuck in suspensionviewModelScope job blocking new stateandroid-probe-logging shows launch fires but the body never completesandroid-trace-sections (Perfetto shows coroutine dispatches as scheduling events)android-probe-logging# 1. Project uses kotlinx.coroutines and not some other concurrency primitive
grep -r 'kotlinx-coroutines-core\|kotlinx-coroutines-android' \
app/build.gradle* gradle/libs.versions.toml 2>/dev/null
# 2. Coroutines version (DebugProbes is in coroutines-debug, version-matched)
grep -E 'kotlinx-coroutines.*([0-9]+\.[0-9]+\.[0-9]+)' \
app/build.gradle* gradle/libs.versions.toml 2>/dev/null
# 3. androidx.core version (for ContextCompat.registerReceiver)
grep -E 'androidx.core' app/build.gradle* gradle/libs.versions.toml 2>/dev/null
# 4. Application subclass exists and is wired in the manifest
grep -rE 'class\s+\w+\s*:\s*Application|extends\s+Application' \
app/src/main/java app/src/main/kotlin 2>/dev/null
# 5. Project's Gradle DSL — Kotlin (.kts) or Groovy (no extension)
ls app/build.gradle.kts 2>/dev/null && echo "Kotlin DSL" || echo "Groovy DSL"
Match the coroutines-debug version to your coroutines version. If your app uses kotlinx-coroutines-core:1.7.3, use kotlinx-coroutines-debug:1.7.3. Mismatched versions can ABI-clash at runtime (different Continuation shape, different debug field layout). The example above shows 1.8.1 — substitute your version.
Groovy DSL. Replace the Kotlin DSL line with:
debugImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.8.1'
No androidx.core 1.9+ available. ContextCompat.RECEIVER_NOT_EXPORTED was added in androidx.core:core 1.9.0. If your project pins older, either bump it as a debugImplementation, or skip the broadcast pattern and dump to getFilesDir() + adb pull instead — both forms are functionally equivalent for the dump, the receiver is just convenient.
Java app. DebugProbes works identically from Java; the install call is DebugProbes.INSTANCE.install(). Coroutine-level bugs are mostly Kotlin-only, but a Java/Kotlin mixed project benefits from the Kotlin install path even when the suspect coroutine code is in a Kotlin file consumed by Java.
kotlinx-coroutines-debug ships a DebugProbes API that, once installed, tracks every active coroutine — its state (RUNNING / SUSPENDED), the suspension point's stack, the coroutine's launch-time stack, and parent/child relationships. DebugProbes.dumpCoroutines() prints the lot.
This is the only way to see what your coroutines are currently doing. Logging tells you what they did. Profilers tell you what threads are doing. DebugProbes tells you what coroutines are suspended on — which is where bugs hide.
// app/build.gradle.kts — AGENT_DEBUGPROBES_<id>: temporary, remove before commit
dependencies {
debugImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.8.1")
}
Use debugImplementation so it's never in release. Mark with the sentinel comment for cleanup.
Application.onCreateimport kotlinx.coroutines.debug.DebugProbes
override fun onCreate() {
super.onCreate()
// AGENT_DEBUGPROBES_<id>: temporary probe — remove before commit
if (BuildConfig.DEBUG) {
DebugProbes.install()
}
// ...
}
install() is idempotent and adds modest overhead — fine for debug builds during a probe.
The simplest pattern: dump on a sentinel logcat tag whenever you want a snapshot. Add a temporary dev-menu button or, simpler, expose via adb shell am broadcast:
// In Application.onCreate, AGENT_DEBUGPROBES_<id>:
import androidx.core.content.ContextCompat // androidx.core:core 1.9.0+
ContextCompat.registerReceiver(
this,
object : BroadcastReceiver() {
override fun onReceive(c: Context?, i: Intent?) {
val out = StringBuilder()
DebugProbes.dumpCoroutines(java.io.PrintStream(object : java.io.OutputStream() {
override fun write(b: Int) { out.append(b.toChar()) }
}))
android.util.Log.d("AGENT_DEBUGPROBES_a4f9c2e1", out.toString())
}
},
IntentFilter("AGENT_DUMP_COROUTINES"),
ContextCompat.RECEIVER_NOT_EXPORTED
)
ContextCompat.registerReceiver is required for RECEIVER_NOT_EXPORTED to work across all API levels — the Context.RECEIVER_NOT_EXPORTED constant was added in API 33, but ContextCompat handles older platforms gracefully. If your project doesn't have androidx.core:core 1.9+, either add it as a debugImplementation for the probe, or skip the receiver and write directly to /data/data/<pkg>/files/coroutine-dump.txt plus adb pull.
Then trigger from the host:
adb shell am broadcast -a AGENT_DUMP_COROUTINES
adb logcat -d -s AGENT_DEBUGPROBES_a4f9c2e1 > /tmp/coroutines-dump.txt
Alternatively, write directly to a file the app can write to and adb pull — but the broadcast/logcat path needs no permissions and works on any debug build.
# 1. Get to the state where you suspect a stuck/leaked coroutine
adb shell input tap 540 1200
sleep 2
# 2. Snapshot
adb logcat -c
adb shell am broadcast -a AGENT_DUMP_COROUTINES
sleep 1
adb logcat -d -s AGENT_DEBUGPROBES_a4f9c2e1 > /tmp/coroutines-dump.txt
For leak hunting, snapshot before and after navigating away. Coroutines from the prior screen that survive into the next snapshot are leaks.
A coroutine entry looks like:
Coroutine "coroutine#42":StandaloneCoroutine{Active}, state: SUSPENDED
at kotlinx.coroutines.flow.internal.ChannelFlow.collect(ChannelFlow.kt:51)
at com.example.app.user.UserViewModel$observe$1.invokeSuspend(UserViewModel.kt:32)
Created at:
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith
at com.example.app.user.UserViewModel.observe(UserViewModel.kt:30)
Look for:
state: SUSPENDED with no upstream emission expected → likely missing resumerxViewModel.observe() still active after the screen left → cancellation buglaunch called repeatedly without cancelling the previous jobstate: RUNNING for a long-running job after navigation → blocking work that should have been cancelledDelegate to a Sonnet sub-agent for non-trivial dumps:
Read
/tmp/coroutines-dump.txt. Group coroutines by state and by the first frame insidecom.example.app. Return: (a) total active coroutines, (b) any coroutine SUSPENDED on aChannelorFlow.collect, (c) any duplicate launch points (sameCreated atline, multiple coroutines). Under 100 words.model: "sonnet".
After the fix, repeat steps 4–5. Expect:
rg 'AGENT_DEBUGPROBES_|DebugProbes|coroutines-debug'
Must return zero. Specifically remove:
debugImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-debug:...") from build.gradle.ktsDebugProbes.install() call from Application.onCreateimport kotlinx.coroutines.debug.* linesThen:
./gradlew :app:assembleDebug # confirm it still builds without the dep
rm -f /tmp/coroutines-dump.txt
Missing cancellation in viewModelScope:
Dump shows two coroutines from the same observe() line — one from before rotation, one from after. The first wasn't cancelled. Fix: ensure the upstream flow is stateIn(viewModelScope, ...) or that you cancel the previous job before launching a new one.
Suspended on a never-completing Channel:
Dump shows SUSPENDED at Channel.receive for a coroutine whose upstream sender is gone. The sender side closed without notifying — fix the channel close path.
runBlocking on the main thread:
RUNNING state, at runBlocking in the stack, on the main dispatcher. That's an ANR waiting to happen — should never runBlocking on main.
| Mistake | Fix |
|---|---|
| Skipping the cleanup gate | rg 'DebugProbes|coroutines-debug' must return zero before commit |
Using implementation instead of debugImplementation | The dep would ship to release; always debugImplementation |
Forgetting to remove DebugProbes.install() | The library may not be on the classpath in release → crash. Cleanup grep catches this. |
| Dumping mid-action | dumpCoroutines is a snapshot; pause the flow at a steady state before dumping |
| Reading the full dump inline | Dumps are 100s of lines for non-trivial apps — delegate to Sonnet |
| Single dump for leak detection | Snapshot before AND after navigating away; coroutines that survive are the leaks |
| Treating SUSPENDED as a bug | SUSPENDED is normal for flow collectors; the bug is unexpected SUSPENDED, e.g. on a closed channel |
| Confusing "Created at" with "current location" | Top of the dump is current suspension point; "Created at" is launch site — both matter, for different reasons |