Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d37c2d5
fix(app): allow local-agent IPC in app CSP
NubsCarson May 16, 2026
ef9c909
fix(app): stabilize Android local runtime boot
NubsCarson May 16, 2026
3ef9c74
fix(app): dedupe local Android UI runtime
NubsCarson May 17, 2026
18d79ca
fix(app): delegate Android local runtime startup
NubsCarson May 17, 2026
a30ead8
test(app): add Pixel local voice benchmark harness
NubsCarson May 17, 2026
be86e33
chore(app): redact Pixel benchmark logs
NubsCarson May 17, 2026
be125cf
Merge remote-tracking branch 'origin/develop' into codex/stock-apk-po…
NubsCarson May 19, 2026
366dbf6
fix(scripts): align local Eliza package links
NubsCarson May 19, 2026
dea0a44
fix(app): clean up local Eliza build drift
NubsCarson May 19, 2026
c983ec3
fix(app): clean package-mode local source drift
NubsCarson May 19, 2026
6861788
fix(app): stabilize package-mode voice pill build
NubsCarson May 19, 2026
7ccc628
fix(app): harden package-mode browser aliases
NubsCarson May 19, 2026
3562818
Merge remote-tracking branch 'refs/remotes/origin/codex/stock-apk-pol…
NubsCarson May 19, 2026
b95e942
fix(homepage): keep steward API types package-safe
NubsCarson May 19, 2026
6c8f89f
Merge remote-tracking branch 'origin/develop' into codex/stock-apk-po…
NubsCarson May 19, 2026
edc03ae
Merge remote-tracking branch 'origin/develop' into codex/stock-apk-po…
NubsCarson May 19, 2026
92ae0f1
Merge branch 'codex/stock-apk-polish-20260516' of github.com:milady-a…
NubsCarson May 19, 2026
175334b
fix(app): reduce Android package-mode complexity
NubsCarson May 19, 2026
fbc44b7
Merge remote-tracking branch 'origin/codex/stock-apk-polish-20260516'…
NubsCarson May 19, 2026
e40ebec
Merge remote-tracking branch 'origin/develop' into codex/stock-apk-po…
NubsCarson May 19, 2026
7c61555
fix(ci): guard package-mode local app linking
NubsCarson May 19, 2026
e741918
fix(ci): satisfy bootstrap guard lint
NubsCarson May 19, 2026
85335e9
fix(ci): guard sibling local package linkers
NubsCarson May 19, 2026
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
2 changes: 1 addition & 1 deletion apps/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<meta http-equiv="Content-Security-Policy" content="default-src 'self' electrobun://* https://* http://localhost:* http://127.0.0.1:*;
script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' http://localhost:* http://127.0.0.1:* https://*;
style-src 'self' 'unsafe-inline' http://localhost:* http://127.0.0.1:* https://*;
connect-src 'self' blob: data: http://localhost:* ws://localhost:* wss://localhost:* http://127.0.0.1:* ws://127.0.0.1:* wss://127.0.0.1:* https://* https://*:* wss://* wss://*:* ws://* ws://*:*;
connect-src 'self' blob: data: eliza-local-agent: http://localhost:* ws://localhost:* wss://localhost:* http://127.0.0.1:* ws://127.0.0.1:* wss://127.0.0.1:* https://* https://*:* wss://* wss://*:* ws://* ws://*:*;
img-src 'self' data: blob: http://localhost:* http://127.0.0.1:* https://*;
media-src 'self' blob: http://localhost:* http://127.0.0.1:* https://*;
frame-src 'self' http://localhost:* http://127.0.0.1:* https://*;
Expand Down
165 changes: 44 additions & 121 deletions apps/app/src/android-local-runtime-boot.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
/**
* android-local-runtime-boot.ts
*
* Activates the on-device Bun agent backend via
* `@elizaos/capacitor-bun-runtime` when:
* Android local-agent startup handoff.
*
* Detects the Android local runtime mode when:
* - Capacitor platform === "android" AND
* - The mobile runtime mode resolves to "local".
*
* On Android the agent runs as a foreground service (`ElizaAgentService`)
* that executes the Bun binary with `agent-bundle.js android-bridge`.
* The `ElizaBunRuntimePlugin` Kotlin implementation delegates lifecycle
* calls to that service via reflection and routes all RPC calls over
* the loopback HTTP surface at `127.0.0.1:31337`.
*
* This module mirrors the shape of `ios-local-runtime-boot.ts` so the
* chat UI can call the same `sendLocalAgentMessage` / `getLocalAgentStatus`
* API regardless of platform.
* MainActivity and RuntimeGate start that service through the Java
* service / Agent bridge path. This renderer boot module must not call
* `ElizaBunRuntime`: the stock Android APK does not implement that
* Capacitor plugin, and invoking it logs
* "ElizaBunRuntime plugin is not implemented on android".
*
* Bridge-not-available (Capacitor web fallback, module missing, native
* `start()` rejects) is logged and silently falls through so cloud paths
* keep working.
* The exported shape stays intentionally narrow for existing imports:
* Android chat/status traffic uses the app-core client and
* `@elizaos/capacitor-agent` transport, not this iOS-style BunRuntime
* adapter.
*/

import { Capacitor } from "@capacitor/core";
import { AGENT_READY_EVENT, dispatchAppEvent } from "@elizaos/app-core";
import { APP_LOG_PREFIX } from "./app-config";

const LOG_PREFIX = `${APP_LOG_PREFIX} [android-local-runtime]`;
Expand All @@ -31,11 +30,6 @@ export const ANDROID_LOCAL_AGENT_LOG_EVENT = "android-local-agent-log";
export const ANDROID_LOCAL_AGENT_ERROR_EVENT = "android-local-agent-error";
export const ANDROID_LOCAL_AGENT_REPLY_EVENT = "android-local-agent-reply";

function dispatchLocalAgentEvent(name: string, detail: unknown): void {
if (typeof document === "undefined") return;
document.dispatchEvent(new CustomEvent(name, { detail }));
}

export interface LocalAgentStatus {
ready: boolean;
model?: string;
Expand All @@ -48,23 +42,9 @@ export interface LocalAgentReply {
conversationId?: string;
}

interface BunRuntimePlugin {
start(
opts: Record<string, unknown>,
): Promise<{ ok: boolean; error?: string }>;
sendMessage(opts: {
message: string;
conversationId?: string;
}): Promise<{ reply: string }>;
getStatus(): Promise<LocalAgentStatus>;
stop(): Promise<void>;
}

type RuntimeState =
| { kind: "idle" }
| { kind: "starting"; promise: Promise<boolean> }
| { kind: "ready"; plugin: BunRuntimePlugin }
| { kind: "unavailable"; reason: string };
| { kind: "delegated"; owner: "ElizaAgentService" };

let runtimeState: RuntimeState = { kind: "idle" };

Expand All @@ -81,119 +61,62 @@ function isApplicable(): boolean {
}
}

async function loadPlugin(): Promise<BunRuntimePlugin | null> {
try {
const mod = await import("@elizaos/capacitor-bun-runtime");
const plugin = (mod as unknown as { ElizaBunRuntime?: BunRuntimePlugin })
.ElizaBunRuntime;
if (!plugin) {
console.warn(
`${LOG_PREFIX} plugin module loaded but ElizaBunRuntime export missing`,
);
return null;
}
return plugin;
} catch (error) {
console.warn(
`${LOG_PREFIX} plugin not available:`,
error instanceof Error ? error.message : error,
);
return null;
}
}

async function startRuntime(): Promise<boolean> {
const plugin = await loadPlugin();
if (!plugin) {
runtimeState = { kind: "unavailable", reason: "plugin-not-loaded" };
return false;
}

try {
const result = await plugin.start({ engine: "bun" });
if (!result.ok) {
const reason = result.error ?? "start-returned-not-ok";
console.warn(`${LOG_PREFIX} start() rejected: ${reason}`);
runtimeState = { kind: "unavailable", reason };
return false;
}
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
console.warn(`${LOG_PREFIX} start() threw:`, reason);
runtimeState = { kind: "unavailable", reason };
return false;
}

runtimeState = { kind: "ready", plugin };
dispatchAppEvent(AGENT_READY_EVENT, { platform: "android", engine: "bun" });
console.log(`${LOG_PREFIX} runtime started`);
return true;
}

/**
* Boot the Android local agent if `Capacitor.getPlatform() === "android"` and
* the resolved runtime mode is "local". Idempotent: subsequent calls return
* the same in-flight promise (when starting) or no-op (when already
* started/unavailable).
* the resolved runtime mode is "local".
*
* Android startup is owned by native Java code:
* - `MainActivity` calls `ElizaAgentService.start()` on launch when
* `ElizaAgentService.shouldAutoStart()` resolves true.
* - `RuntimeGate` calls the registered `Agent.start()` bridge when a
* stock Android user chooses Local mode.
*
* This function is therefore an idempotent no-op handoff. Returning false
* means "this renderer adapter did not start a BunRuntime plugin", not
* "Android local mode is unavailable".
*/
export function bootAndroidLocalRuntimeIfApplicable(): Promise<boolean> {
if (!isApplicable()) return Promise.resolve(false);

if (runtimeState.kind === "ready") return Promise.resolve(true);
if (runtimeState.kind === "unavailable") return Promise.resolve(false);
if (runtimeState.kind === "starting") return runtimeState.promise;
if (runtimeState.kind === "idle") {
runtimeState = { kind: "delegated", owner: "ElizaAgentService" };
console.info(
`${LOG_PREFIX} startup delegated to ElizaAgentService; skipping BunRuntime plugin`,
);
}

const promise = startRuntime();
runtimeState = { kind: "starting", promise };
return promise;
return Promise.resolve(false);
}

/**
* Whether the Android local runtime is fully started and ready to accept
* messages.
* Whether this renderer-owned adapter is ready to accept messages.
* Android local chat/status traffic does not use this adapter.
*/
export function isAndroidLocalRuntimeReady(): boolean {
return runtimeState.kind === "ready";
return false;
}

/**
* Send a single message to the on-device agent. Throws if the runtime
* isn't ready.
* Android local messages are sent through the app-core client /
* `@elizaos/capacitor-agent` transport, not this iOS-style adapter.
*/
export async function sendLocalAgentMessage(
text: string,
conversationId?: string,
): Promise<LocalAgentReply> {
if (runtimeState.kind !== "ready") {
throw new Error(
`Android local runtime not ready (state: ${runtimeState.kind})`,
);
}
const payload = conversationId
? { message: text, conversationId }
: { message: text };
const result = await runtimeState.plugin.sendMessage(payload);
const reply: LocalAgentReply = conversationId
? { reply: result.reply, conversationId }
: { reply: result.reply };
dispatchLocalAgentEvent(ANDROID_LOCAL_AGENT_REPLY_EVENT, reply);
return reply;
void text;
void conversationId;
throw new Error(
`Android local runtime adapter not available (state: ${runtimeState.kind}); use Agent transport`,
);
}

/**
* Read the on-device agent status. Never throws.
* Android local status is read through the app-core client /
* `@elizaos/capacitor-agent` transport. Never throws.
*/
export async function getLocalAgentStatus(): Promise<LocalAgentStatus> {
if (runtimeState.kind !== "ready") return { ready: false };
try {
return await runtimeState.plugin.getStatus();
} catch (error) {
console.warn(
`${LOG_PREFIX} getStatus() failed:`,
error instanceof Error ? error.message : error,
);
return { ready: false };
}
return { ready: false };
}

/** Reset for tests. */
Expand Down
Loading
Loading