Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.

Commit 569f376

Browse files
Natfiiclaude
andcommitted
feat(ffi): add thinking extraction, structured logging, and session retry
- Extract inline thinking tags (<think>, <thinking>, <analysis>, <reflection>, <inner_monologue>) from model responses and route them to the thinking card via on_thinking(), preventing them from appearing as visible response text - Forward API-level reasoning_content (o1, o3) to thinking card - Add tracing-android integration with per-crate log filtering: noisy HTTP/TLS crates (hyper, reqwest, rustls) suppressed to WARN, session events logged at INFO with structured fields (tool names, durations, response sizes, iteration counts) - Add init_logging() UniFFI export called at app startup - Fix reqwest TLS: change rustls-tls to rustls-tls-webpki-roots to bundle Mozilla root certificates (Android NDK has no system cert store) - Add ensureSession() retry in TerminalViewModel to recover from session_start race conditions after ADB hot-swap - Fix tool routing: clean up phantom tools, add FfiWebSearchTool wrapper, fix web_search name mismatch in build_android_tool_descs - Add 12 unit tests for extract_thinking_from_text Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2481eff commit 569f376

File tree

7 files changed

+523
-84
lines changed

7 files changed

+523
-84
lines changed

app/src/main/java/com/zeroclaw/android/ZeroClawApplication.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ class ZeroClawApplication :
212212
super.onCreate()
213213
System.loadLibrary("sqlcipher")
214214
System.loadLibrary("zeroclaw")
215+
com.zeroclaw.ffi.initLogging()
215216
verifyCrateVersion()
216217

217218
@Suppress("InjectDispatcher")

app/src/main/java/com/zeroclaw/android/ui/screen/terminal/TerminalViewModel.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ class TerminalViewModel(
8080
private val _history = MutableStateFlow<List<String>>(emptyList())
8181
private val _historyIndex = MutableStateFlow(NO_HISTORY_SELECTION)
8282

83+
/** Whether [sessionStart] has succeeded at least once. */
84+
@Volatile
85+
private var sessionReady = false
86+
8387
/** Observable terminal state combining persisted entries with transient UI state. */
8488
val state: StateFlow<TerminalState> =
8589
combine(
@@ -123,6 +127,7 @@ class TerminalViewModel(
123127
withContext(Dispatchers.IO) {
124128
sessionStart()
125129
}
130+
sessionReady = true
126131
} catch (e: Exception) {
127132
logRepository.append(
128133
LogSeverity.WARN,
@@ -133,6 +138,32 @@ class TerminalViewModel(
133138
}
134139
}
135140

141+
/**
142+
* Attempts [sessionStart] if the initial call in [initAgentSession] failed.
143+
*
144+
* This handles the race where the daemon service hasn't finished
145+
* starting when the ViewModel initialises. Called on [Dispatchers.IO]
146+
* before the first [sessionSend].
147+
*
148+
* @return `true` if a session is now active.
149+
*/
150+
@Suppress("TooGenericExceptionCaught")
151+
private suspend fun ensureSession(): Boolean {
152+
if (sessionReady) return true
153+
return try {
154+
sessionStart()
155+
sessionReady = true
156+
true
157+
} catch (e: Exception) {
158+
logRepository.append(
159+
LogSeverity.WARN,
160+
TAG,
161+
"Session retry failed: ${e.message}",
162+
)
163+
false
164+
}
165+
}
166+
136167
/**
137168
* Tears down the agent session when the ViewModel is destroyed.
138169
*/
@@ -362,6 +393,11 @@ class TerminalViewModel(
362393

363394
try {
364395
withContext(Dispatchers.IO) {
396+
if (!ensureSession()) {
397+
throw IllegalStateException(
398+
"No active session — is the daemon running?",
399+
)
400+
}
365401
sessionSend(
366402
message,
367403
images.map { it.base64Data },

zeroclaw-android/Cargo.lock

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

zeroclaw-android/zeroclaw-ffi/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ tokio = { version = "1.42", features = ["rt-multi-thread", "sync", "macros"] }
2525
tokio-util = { version = "0.7", features = ["rt"] }
2626
serde = { version = "1.0", features = ["derive"] }
2727
serde_json = "1.0"
28-
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] }
28+
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-webpki-roots", "stream"] }
2929
nanohtml2text = "0.1"
3030
futures-util = "0.3"
3131
anyhow = "1.0"
@@ -34,3 +34,7 @@ toml = "0.8"
3434
tracing = "0.1"
3535
chrono = "0.4"
3636
rhai = { version = "1.21", features = ["sync"] }
37+
38+
[target.'cfg(target_os = "android")'.dependencies]
39+
tracing-android = "0.2"
40+
tracing-subscriber = { version = "0.3", default-features = false, features = ["registry", "env-filter"] }

zeroclaw-android/zeroclaw-ffi/src/lib.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,39 @@ use std::sync::Arc;
4242

4343
pub use error::FfiError;
4444

45+
/// Initialises the Rust tracing subscriber for Android logcat output.
46+
///
47+
/// On Android debug builds, routes `tracing` events (info, warn, error)
48+
/// to `__android_log_write` with the tag `"zeroclaw_ffi"`. On release
49+
/// builds or non-Android targets, this is a no-op.
50+
///
51+
/// Safe to call multiple times — the second and subsequent calls are
52+
/// silently ignored by the subscriber registry.
53+
#[uniffi::export]
54+
pub fn init_logging() {
55+
#[cfg(target_os = "android")]
56+
{
57+
use tracing_subscriber::prelude::*;
58+
use tracing_subscriber::EnvFilter;
59+
60+
// Noisy HTTP/TLS crates → WARN only; everything else → DEBUG.
61+
let filter = if cfg!(debug_assertions) {
62+
EnvFilter::new(
63+
"debug,hyper=warn,hyper_util=warn,reqwest=warn,rustls=warn,h2=warn,tower=warn",
64+
)
65+
} else {
66+
EnvFilter::new("warn")
67+
};
68+
69+
if let Ok(layer) = tracing_android::layer("zeroclaw_ffi") {
70+
let _ = tracing_subscriber::registry()
71+
.with(layer.with_filter(filter))
72+
.try_init();
73+
tracing::info!("Rust tracing initialised");
74+
}
75+
}
76+
}
77+
4578
/// Extracts a human-readable message from a caught panic payload.
4679
fn panic_detail(payload: &Box<dyn std::any::Any + Send>) -> String {
4780
payload

0 commit comments

Comments
 (0)