Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ flowchart LR

## MobileCore Link Status

MobileCode no longer hard-codes a local Qwen model. `MobileCoreClient` resolves the active model, runtime, revision, backend, quantization, capabilities, artifact state, resource preflight, recommendations, and performance metrics from the co-installed MobileCore service. The in-app TuiMa sheet can load, unload, and switch installed models by public `model_id`; ordinary clients never receive or submit absolute model paths.
MobileCode no longer hard-codes a local Qwen model. `MobileCoreClient` resolves the active model, runtime, revision, backend, quantization, capabilities, artifact state, resource preflight, Android background-restriction state, recommendations, and performance metrics from the co-installed MobileCore service. The in-app TuiMa sheet can load, unload, and switch installed models by public `model_id`; ordinary clients never receive or submit absolute model paths. A background-restricted MobileCore remains visible for recovery but is removed from eligible local routes before inference payloads are sent.

Image and audio buttons appear only when the active local runtime advertises the corresponding capability. Attachment bytes stay in memory, are sent only to `127.0.0.1`, are never persisted in chat turns or evidence, and never fall back to a cloud provider. Local inference evidence records safe model/runtime/latency metadata while omitting prompts, media, credentials, and payloads.

Expand Down Expand Up @@ -423,7 +423,7 @@ See:
- [Release assets](https://github.com/Harzva/mobilecode/releases/tag/v0.1.77) - Android `pure` APK and CI-generated platform evidence for the credential-safe protocol-gated MobileCore local inference candidate.
- [Signed Android v0.1.77 workflow](https://github.com/Harzva/mobilecode/actions/runs/31128587598) - the downloaded official `0.1.77+67` APK has SHA-256 `f008ede0e0305c835c3bf45bcc56f22c4fc911d0ae10b513f298d1bdfb0a1c1d`, verifies with the MobileCode release certificate, contains zero recognizable key/JWT/Bearer or private-host-path patterns, and clean-launches on the Android 16 ARM64 emulator.
- v0.1.77 makes the public release workflows fail closed if they reference or compile raw provider keys, relay bearer tokens, or an OAuth client secret. Public relay URLs, OAuth client IDs, and redirect URIs remain allowed configuration; users may still save their own provider key through the app's secure-storage flow.
- Fresh official-package pairing reopened one MobileCore background gate: after MobileCore updated its foreground notification, Android reported the service as non-foreground and froze the process when MobileCode became active. MobileCore foreground-service notification updates must be fixed before claiming unattended dual-app recovery.
- MobileCore `0.1.4-rc6` closes the emulator foreground-service regression. A 40-poll Android 16 lane kept MobileCode resumed while MobileCore retained its real local model and foreground service with zero failed health polls, freezes, FGS timeouts, ANRs, OOMs, or SIGABRTs. Android `background_restricted` is now a typed fail-closed routing state, not a hidden timeout.
- The former v0.1.76 official APK was withdrawn after post-build review found that the old public workflow supplied runtime service credentials as Dart compile definitions. The explicitly named debug-signed QA APK remains emulator evidence only; affected provider credentials should be rotated outside the repository.
- v0.1.76 requires the `mobilecore.local` v2 compatibility handshake before local model control or inference. Missing, malformed, or unsupported protocols fail closed with typed evidence; MobileCore still cannot perform Phone Use actions.
- Local timeouts and explicit Agent pauses now request native inference cancellation, while measured slow runtimes receive a bounded next-response budget; overlapping MobileCore work fails as `runtime_busy` instead of racing the shared llama context.
Expand Down
23 changes: 23 additions & 0 deletions docs/mobilecore-dual-app-qa.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,29 @@ background-recovery gate and identifies the next fix: MobileCore must preserve
foreground-service state when refreshing its notification before the official
v0.1.77 APK is credited with a controlled local-chat pairing.

### MobileCore rc6 background closure and typed recovery state

MobileCore `0.1.4-rc6` reasserts its explicitly typed `dataSync` foreground
notification before model work on every service delivery. The acceptance lane
also discovered that Android had marked one emulator install as
`background_restricted`; AOSP intentionally strips foreground-service status
from packages in that state. That restricted run is recorded as a rejected
precondition, not as a runtime failure or pass, and neither production App
changes secure settings automatically.

After restoring the emulator through the same user-controlled background-use
policy represented by Android Battery settings, MobileCode remained resumed
for 40 authenticated polls (about two minutes) while MobileCore retained the
real Qwen2.5 0.5B model. All 40 health requests passed, the service ended with
`isForeground=true`, both processes remained alive and unfrozen, and the
filtered safety log contained no FGS timeout, ANR, OOM, or SIGABRT marker.

The follow-up client contract adds `background_restricted` to MobileCore
`/health`. MobileCode maps `true` to a typed, fail-closed recovery state,
preserves the active-model metadata for diagnosis, shows a Battery-settings
instruction, and sends no local inference payload until the restriction is
cleared. The full Flutter suite now passes 578 tests.

## Local vision chain

A separate controlled emulator check used a Qwen3.5 0.8B main GGUF plus its mmproj. `/v1/models` exposed the projector as metadata on the main model, loading returned `image_input=true`, and `/health` reported `runtime=llama.cpp/libmtmd`. A real JPEG data-URI request completed through the same OpenAI-compatible endpoint with 93 total tokens and 542 MB reported runtime memory. There was no crash, ANR, or OOM.
Expand Down
7 changes: 7 additions & 0 deletions mobile_agent/lib/screens/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14334,6 +14334,13 @@ class _ChatPanelState extends State<_ChatPanel> {
'Preflight ${data.health.preflight.ok ? 'ready' : data.health.preflight.failureCode ?? 'not ready'} · image ${data.health.capabilities.imageInput ? 'yes' : 'no'} · audio ${data.health.capabilities.audioInput ? 'yes' : 'no'}',
style: const TextStyle(color: _muted, fontSize: 12),
),
if (data.health.backgroundRestricted) ...[
const SizedBox(height: 5),
const Text(
'Android is restricting MobileCore background operation. Allow background use in MobileCore battery settings before selecting the local route.',
style: TextStyle(color: _amber, fontSize: 12),
),
],
if (data.health.canInfer) ...[
const SizedBox(height: 10),
OutlinedButton.icon(
Expand Down
18 changes: 17 additions & 1 deletion mobile_agent/lib/services/tuima_provider_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ class TuimaHealth {
this.projectorArtifact = const MobileCoreArtifactHealth(),
this.preflight = const MobileCorePreflight(),
this.audioSampleRateHz = 0,
this.backgroundRestricted = false,
this.failureCode,
this.failure,
});
Expand All @@ -566,6 +567,7 @@ class TuimaHealth {
final MobileCoreArtifactHealth projectorArtifact;
final MobileCorePreflight preflight;
final int audioSampleRateHz;
final bool backgroundRestricted;
final String? failureCode;
final String? failure;

Expand Down Expand Up @@ -597,6 +599,7 @@ class TuimaHealth {
'capabilities': capabilities.evidenceSnapshot,
'preflightOk': preflight.ok,
'preflightFailure': preflight.failureCode,
'backgroundRestricted': backgroundRestricted,
if (failureCode != null) 'failureCode': failureCode,
'mainArtifactPresent': mainArtifact.present,
'mainArtifactVerified': mainArtifact.verified,
Expand Down Expand Up @@ -707,9 +710,10 @@ class MobileCoreClient {
}
final protocol = MobileCoreProtocol.fromJson(payload['protocol']);
final modelLoaded = payload['model_loaded'] == true;
final backgroundRestricted = payload['background_restricted'] == true;
final artifacts = _stringMap(payload['artifacts']);
return TuimaHealth(
state: modelLoaded
state: modelLoaded && !backgroundRestricted
? TuimaConnectionState.modelReady
: TuimaConnectionState.serviceReady,
version: payload['version']?.toString() ?? '',
Expand All @@ -725,6 +729,11 @@ class MobileCoreClient {
MobileCoreArtifactHealth.fromJson(artifacts['mmproj']),
preflight: MobileCorePreflight.fromJson(payload['preflight']),
audioSampleRateHz: _asInt(payload['audio_sample_rate_hz']),
backgroundRestricted: backgroundRestricted,
failureCode: backgroundRestricted ? 'background_restricted' : null,
failure: backgroundRestricted
? 'Android is preventing MobileCore from remaining active in the background.'
: null,
);
} on MobileCoreProviderException catch (error) {
return TuimaHealth.unavailable(
Expand Down Expand Up @@ -1133,6 +1142,13 @@ class MobileCoreClient {
Future<TuimaHealth> _requireCompatibleModel(String model) async {
final health = await _requireCompatibleHealth();
final requested = model.trim();
if (health.backgroundRestricted) {
throw const MobileCoreProviderException(
code: 'background_restricted',
message:
'MobileCore background operation is restricted. Allow it in Android Battery settings before local inference.',
);
}
if (!health.canInfer || health.activeModel == null) {
throw const MobileCoreProviderException(
code: 'model_not_loaded',
Expand Down
40 changes: 40 additions & 0 deletions mobile_agent/test/services/tuima_provider_service_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,45 @@ void main() {
expect(health.evidenceMetadata['failureCode'], 'protocol_missing');
});

test('fails closed when Android background-restricts MobileCore', () async {
var chatRequests = 0;
server.listen((request) async {
request.response.headers.contentType = ContentType.json;
if (request.uri.path == '/health') {
request.response.write(jsonEncode({
..._healthPayload(),
'background_restricted': true,
}));
} else {
chatRequests += 1;
request.response.write('{"choices":[]}');
}
await request.response.close();
});

final health = await service.probe();
expect(health.state, TuimaConnectionState.serviceReady);
expect(health.canInfer, isFalse);
expect(health.backgroundRestricted, isTrue);
expect(health.failureCode, 'background_restricted');
expect(health.activeModel, 'qwen');

await expectLater(
service.completeChat(
messages: const [
{'role': 'user', 'content': 'hello'}
],
model: 'qwen',
),
throwsA(isA<MobileCoreProviderException>().having(
(error) => error.code,
'code',
'background_restricted',
)),
);
expect(chatRequests, 0);
});

test('blocks model control when the protocol major is unsupported',
() async {
var controlRequests = 0;
Expand Down Expand Up @@ -955,6 +994,7 @@ Map<String, Object?> _healthPayload({String model = 'qwen'}) => {
'quantization': 'Q4_K_M',
'active_model': model,
'model_loaded': true,
'background_restricted': false,
'capabilities': const {
'text_input': true,
'text_output': true,
Expand Down
Loading