diff --git a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/AssistantActivityBackend.java b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/AssistantActivityBackend.java index 9ebe916..d7bf71c 100644 --- a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/AssistantActivityBackend.java +++ b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/AssistantActivityBackend.java @@ -47,6 +47,7 @@ import java.io.IOException; import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; public class AssistantActivityBackend extends ComponentActivity { public interface ComposeStateCallbacks { @@ -213,7 +214,7 @@ public void run() { private volatile boolean mAgentRunCancelled; private boolean mIslandVoiceLaunch; private boolean mSuppressNextUserAppend; - private int mAgentRunGeneration; + private final AtomicInteger mAgentRunGeneration = new AtomicInteger(); private int mAuditRefreshGeneration; private int mVoiceRunGeneration; private int mChatRunGeneration; @@ -1403,7 +1404,7 @@ private void startOpenAiRealtimeVoiceAgent() { clearVoicePipelineProtection(); mAgentRunCancelled = false; final int voiceGeneration = ++mVoiceRunGeneration; - final int runGeneration = ++mAgentRunGeneration; + final int runGeneration = mAgentRunGeneration.incrementAndGet(); final OpenAiRealtimeVoiceSession session = new OpenAiRealtimeVoiceSession(endpointConfig, liveVoiceContinuityContextJson(), "yolo".equals(mAutonomyMode)); mRunningRealtimeVoiceSession = session; @@ -1482,7 +1483,7 @@ public String callTool(String toolName, String argumentsJson) { @Override public boolean isCancelled() { return mAgentRunCancelled - || runGeneration != mAgentRunGeneration + || runGeneration != mAgentRunGeneration.get() || voiceGeneration != mVoiceRunGeneration || Thread.currentThread().isInterrupted(); } @@ -1567,7 +1568,7 @@ private void startGeminiLiveVoiceAgent() { clearVoicePipelineProtection(); mAgentRunCancelled = false; final int voiceGeneration = ++mVoiceRunGeneration; - final int runGeneration = ++mAgentRunGeneration; + final int runGeneration = mAgentRunGeneration.incrementAndGet(); final GeminiLiveVoiceSession session = new GeminiLiveVoiceSession(apiKey, liveVoiceContinuityContextJson(), "yolo".equals(mAutonomyMode)); mRunningGeminiLiveVoiceSession = session; @@ -1646,7 +1647,7 @@ public String callTool(String toolName, String argumentsJson) { @Override public boolean isCancelled() { return mAgentRunCancelled - || runGeneration != mAgentRunGeneration + || runGeneration != mAgentRunGeneration.get() || voiceGeneration != mVoiceRunGeneration || Thread.currentThread().isInterrupted(); } @@ -2387,7 +2388,7 @@ private void runOneShotActions(final String originalMessage, reloadAutonomyMode(); cancelAgentRun(); mAgentRunCancelled = false; - final int runGeneration = ++mAgentRunGeneration; + final int runGeneration = mAgentRunGeneration.incrementAndGet(); final JSONArray proposedActions = decision.proposedActions(); final ModelAdapter adapter = selectedModelAdapter(modelEndpointConfig()); final FrameworkToolExecutor toolExecutor = new FrameworkToolExecutor(this, agentManager); @@ -2413,7 +2414,7 @@ public void run() { adapter.usesCloud(), adapter.privacyDisclosure()); JSONArray results = new JSONArray(); for (int i = 0; i < proposedActions.length(); i++) { - if (mAgentRunCancelled || runGeneration != mAgentRunGeneration) { + if (mAgentRunCancelled || runGeneration != mAgentRunGeneration.get()) { return; } JSONObject action = proposedActions.optJSONObject(i); @@ -2481,7 +2482,7 @@ private void postOneShotReply(final int runGeneration, final String reply, runOnUiThread(new Runnable() { @Override public void run() { - if (runGeneration != mAgentRunGeneration) { + if (runGeneration != mAgentRunGeneration.get()) { return; } mAgentThread = null; @@ -2505,7 +2506,7 @@ private void showOneShotConfirmation(final int runGeneration, final String taskI runOnUiThread(new Runnable() { @Override public void run() { - if (runGeneration != mAgentRunGeneration) { + if (runGeneration != mAgentRunGeneration.get()) { try { mAgentManager.stopTask(taskId, "{\"reason\":\"one_shot_cancelled\"}"); } catch (RuntimeException ignored) { @@ -3156,7 +3157,7 @@ private void stopTask() { private void cancelAgentRun() { mAgentRunCancelled = true; - mAgentRunGeneration++; + mAgentRunGeneration.incrementAndGet(); ModelAdapter adapter = mRunningModelAdapter; if (adapter != null) { adapter.cancel(); @@ -3226,7 +3227,7 @@ private void runAgent(final String goal) { reloadAutonomyMode(); cancelAgentRun(); mAgentRunCancelled = false; - final int runGeneration = ++mAgentRunGeneration; + final int runGeneration = mAgentRunGeneration.incrementAndGet(); final String taskId = mActiveTaskId; final ModelEndpointConfig endpointConfig = modelEndpointConfig(); setTaskText("Working on: " + goal); @@ -3288,7 +3289,7 @@ public String callTool(String toolName, String argumentsJson) { @Override public boolean isCancelled() { return mAgentRunCancelled - || runGeneration != mAgentRunGeneration + || runGeneration != mAgentRunGeneration.get() || Thread.currentThread().isInterrupted(); } }); @@ -3296,7 +3297,7 @@ public boolean isCancelled() { runOnUiThread(new Runnable() { @Override public void run() { - if (runGeneration != mAgentRunGeneration) { + if (runGeneration != mAgentRunGeneration.get()) { return; } mAgentThread = null; diff --git a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/jobs/OpenPhoneAgentJobScheduler.java b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/jobs/OpenPhoneAgentJobScheduler.java index e5a1162..f9390c0 100644 --- a/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/jobs/OpenPhoneAgentJobScheduler.java +++ b/overlay/packages/apps/OpenPhoneAssistant/src/org/openphone/assistant/jobs/OpenPhoneAgentJobScheduler.java @@ -24,6 +24,8 @@ import org.openphone.assistant.model.OpenAiResponsesAgentAdapter; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; public final class OpenPhoneAgentJobScheduler { private static final String TAG = "OpenPhoneAgentJobs"; @@ -32,9 +34,20 @@ public final class OpenPhoneAgentJobScheduler { private static final long MIN_DELAY_MILLIS = 15_000L; private static final long STUCK_TIMEOUT_MILLIS = 10L * 60L * 1000L; private static final int MAX_DUE_PER_CHECK = 3; + private static final ConcurrentHashMap sCancelFlags = + new ConcurrentHashMap<>(); private OpenPhoneAgentJobScheduler() {} + /** Signals a running background job to stop at its next isCancelled() check. */ + public static void cancelJob(String jobId) { + Long id = parseJobId(jobId); + AtomicBoolean flag = id == null ? null : sCancelFlags.get(id); + if (flag != null) { + flag.set(true); + } + } + public static void checkNow(Context context) { if (context == null) { return; @@ -111,6 +124,8 @@ private static void runJob(Context context, AgentJobRecord job) { return; } String taskId = null; + AtomicBoolean cancelFlag = sCancelFlags.computeIfAbsent(job.id, + id -> new AtomicBoolean(false)); try { String response = agentManager.startTask(taskRequestJson(job)); taskId = parseString(response, "task_id"); @@ -140,7 +155,7 @@ public String callTool(String toolName, String argumentsJson) { @Override public boolean isCancelled() { - return false; + return cancelFlag.get() || Thread.currentThread().isInterrupted(); } }); store.markCompleted(job.id, result, System.currentTimeMillis()); @@ -150,6 +165,7 @@ public boolean isCancelled() { } catch (RuntimeException e) { failJob(context, store, job, "job_error:" + e.getClass().getSimpleName()); } finally { + sCancelFlags.remove(job.id); if (agentManager != null && taskId != null && !taskId.isEmpty()) { try { agentManager.stopTask(taskId, "{\"reason\":\"background_job_finished\"}"); @@ -286,6 +302,17 @@ private static JSONObject parseOrEmpty(String json) { } } + private static Long parseJobId(String jobId) { + if (jobId == null) { + return null; + } + try { + return Long.parseLong(jobId.trim()); + } catch (NumberFormatException e) { + return null; + } + } + private static String parseString(String json, String key) { try { return new JSONObject(json == null ? "{}" : json).optString(key, "");